Files
classhelper-island-refactor…/out/vol5-动画资源验收与结语(14-19).md
T
ClassIsland Prompt Kit 22c1784bab feat: ClassIsland / ClassHelper 灵动岛提示词套件
- 重构版主提示词(14 parts,面向 ClassHelper 灵动岛重构)
- ClassIsland 全量规格提示词(20 parts,含分卷与逐章版本)
- 投喂指南、分卷、逐节文件
- 调研记录、一致性基线、生成与质检脚本
2026-11-12 21:59:58 +08:00

969 lines
100 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 14. 动画与动效总表
本节是「动效施工图」。实现者必须逐条实现下表的触发、属性、起止值、时长、曲线、延迟与实现方式,**不得**自行「优化」时长或曲线。所有数值均来自 ClassIsland 源码;标注【推断】的为源码未直接给出、由同类动画或 Web 平台换算得出。
### 14.0 全局动效规则
#### 14.0.1 动画级别(AnimationLevel)
必须实现 3 档,默认 **标准(1)**(`ClassIsland/Models/Settings.cs:1851`;`Views/SettingPages/GeneralSettingsPage.axaml:280-293` 的三项为「禁用 / 标准 / 华丽」,绑定 `SelectedIndex`):
| 级别 | 文案 | 行为要求 | 证据 |
|---|---|---|---|
| 0 | 禁用 | `IThemeService.IsTransientDisabled = true`(禁用过渡动画),Popup 入场、抽屉/竖抽屉动画、Workflow 等全部关闭;但**主界面**的提醒动画与部分控件不在此列(设置项描述明确写「禁用动画选项不适用于应用主界面和部分控件」) | `App.axaml.cs:725`;`IThemeService.cs:44`;`PopupIntroAnimationBehavior.cs:67`;`DrawerHost.axaml.cs:70`;`GeneralSettingsPage.axaml:283` |
| 1 | 标准(默认) | 列表进入动画关闭、Popup 入场可用;不启用「行/组件淡入」「WrapPanel 尺寸过渡」等高成本动画 | `StackPanelIntroAnimationBehavior.cs:46`;`DrawerHost.axaml.cs:70` |
| 2 | 华丽 | 额外启用:组件/行淡入(250–300ms)、WrapPanel 子项位置隐式动画(225ms 默认) | `MainWindowLine.cs:382-400`;`ComponentPresenter.axaml.cs:224-243`;`WrapPanelResizingAnimationAssist.cs:51-56` |
实现细节:
- 该设置必须持久化并在启动时同步到全局单例(对齐 `IThemeService.AnimationLevel` 静态属性),任何动画入口先查级别再播放(源码模式见 `MainWindowLine.cs:384`)。
- 「禁用动画等待」(`IsWaitForTransientDisabled`)是另一独立开关:默认阻塞操作会等待动画播完,开启后不等待(`IThemeService.cs:46-49`;`Settings.cs:773-782`)。Web 实现等价为「不 await 动画 Promise」。
- 必须同时响应系统 `prefers-reduced-motion: reduce`:将其视为「级别 0 + 时长压到 1ms」(与 ClassHelper `renderer/styles/index.css:909-917` 一致),且**加载指示器必须放慢而不是停**(`index.css:919-930`)。
#### 14.0.2 缓动别名表(必须集中定义,禁止各组件手写)
| 别名 | cubic-bezier | 用途 | 来源 |
|---|---|---|---|
| `--ci-ease-out` | `cubic-bezier(0.16, 1, 0.3, 1)` | 绝大多数进出场(ClassHelper 主缓动) | `ClassHelper/packages/shared/src/motion.ts:24` |
| `--ci-ease-in-out` | `cubic-bezier(0.77, 0, 0.175, 1)` | 对称来回(抽屉推拉、内容互换) | `motion.ts:26` |
| `--ci-ease-drawer` | `cubic-bezier(0.32, 0.72, 0, 1)` | 侧向抽屉 | `motion.ts:28` |
| `--ci-ease-fl-1` | `cubic-bezier(0.65, 0, 0.35, 1)` | Fluent 行宽过渡/L 型 S 曲线 | `FluentTheme/Styles.axaml:269` |
| `--ci-ease-fl-2` | `cubic-bezier(0.25, 1, 0.5, 1)` | 遮罩文字淡入、行淡入 | `FluentTheme/Styles.axaml:333`;`MainWindowLine.cs:397` |
| `--ci-ease-fl-3` | `cubic-bezier(0.5, 0, 0.75, 0)` | 遮罩退出缩放 | `FluentTheme/Styles.axaml:389` |
| `--ci-ease-popup` | `cubic-bezier(0.22, 1, 0.36, 1)` | Popup 入场(Semi 风格) | `PopupIntroAnimationBehavior.cs:77,86` |
| `--ci-ease-tutorial` | `cubic-bezier(0.16, 1, 0.3, 1)` | 教程高亮/聚光 | `TutorialHighlightControl.axaml:13` |
| `--ci-ease-list` | `cubic-bezier(0.00, 1.00, 0.00, 1.00)` | 列表错峰入场 | `StackPanelIntroAnimation.axaml:22` |
| `--ci-ease-text` | `cubic-bezier(0.33, 1, 0.68, 1)` | OOBE 文字收拢、字符翻入 | `OobeIntroAnimationControl.axaml:93,105`;`.axaml.cs:151` |
| `--ci-ease-block` | `cubic-bezier(0.76, 0, 0.24, 1)` | OOBE 方块放大 | `OobeIntroAnimationControl.axaml:48` |
| `--ci-ease-root` | `cubic-bezier(0.00, 0.83, 0.17, 0.90)` | OOBE 整场缩放 | `OobeIntroAnimationControl.axaml:16` |
| `--ci-ease-char-out` | `cubic-bezier(0.32, 0, 0.67, 0)` | OOBE 字符翻出 | `OobeIntroAnimationControl.axaml.cs:117` |
| `--ci-ease-spin` | `cubic-bezier(0.50, 0.25, 0.50, 0.75)` | 更新图标旋转 | `UpdateSettingsPage.axaml:90` |
| `--ci-ease-overshoot` | `cubic-bezier(0.00, 1.00, 0.50, 1.10)` | 经典主题行宽过冲 | `ClassicTheme/Styles.axaml:207` |
| `--ci-ease-step` | `cubic-bezier(0, 0, 0, 1)` | 抽屉/关于页 (`0,0 0,1`) | `DrawerHost.axaml:46-48`;`AboutSettingsPage.axaml:137` |
| Avalonia `SineEaseIn` | `cubic-bezier(0.12, 0, 0.39, 0)`【推断】 | 遮罩层淡入/淡出 | `FluentTheme/Styles.axaml:299` |
| Avalonia `SineEaseOut` | `cubic-bezier(0.61, 1, 0.88, 1)`【推断】 | 图标宽度/进度透明度 | `AnimatedIconButton.axaml:24` |
| Avalonia `SineEaseInOut` | `cubic-bezier(0.37, 0, 0.63, 1)`【推断】 | 呼吸/进度过渡 | `WorkflowProgressIndicatorItemControl.axaml:19,32,60` |
| Avalonia `QuadraticEaseIn` | `cubic-bezier(0.11, 0, 0.5, 0)`【推断】 | 经典遮罩下压、涟漪放大 | `ClassicTheme/Styles.axaml:271`;`RippleEffect.axaml.cs:94` |
| Avalonia `QuadraticEaseOut` | `cubic-bezier(0.5, 1, 0.89, 1)`【推断】 | 经典遮罩上滑 | `ClassicTheme/Styles.axaml:311` |
| Avalonia `ExponentialEaseOut` | `cubic-bezier(0.16, 1, 0.3, 1)`【推断】 | 闪屏滑入 | `SplashWindow.axaml.cs:111,116` |
| Avalonia `CubicEaseOut` | `cubic-bezier(0.33, 1, 0.68, 1)`【推断】 | 闪屏进度 | `SplashWindow.axaml.cs:75` |
| Avalonia `CubicEaseInOut` | `cubic-bezier(0.65, 0, 0.35, 1)`【推断】 | WrapPanel 默认隐式动画 | `WrapPanelResizingAnimationAssist.cs:40` |
#### 14.0.3 时长常量表
| 常量 | 值 | 使用场景 |
|---|---|---|
| `--ci-dur-instant` | 90ms | 按压反馈、行透明度 |
| `--ci-dur-fast` | 100–140ms | 行/组件淡入淡出、抽屉内容、遮罩退出 |
| `--ci-dur-base` | 150–250ms | Popup、遮罩文字、列表滑入 |
| `--ci-dur-slow` | 300–360ms | 行宽过渡(Fluent)、遮罩分块、组件淡入 |
| `--ci-dur-overshoot` | 550ms | 行宽过渡(经典) |
| `--ci-dur-long` | 750ms | OOBE 字符、教程高亮、遮罩缩放 |
| `--ci-dur-loop` | 1000–3000ms | 循环/呼吸/旋转/OOBE 整场 |
#### 14.0.4 动效编排原则(实现时逐条遵守)
1. **进场慢、退场快**:进场承担「让用户理解发生了什么」的职责,可以 200–300ms;退场承担「别挡路」的职责,必须 100–150ms 结束。源码中提醒遮罩文字淡入 250ms、淡出 200ms;抽屉 250ms;弹窗退出 140ms(`index.css:638-644`)都遵循此规律。实现者不得把退场做得比进场更慢。
2. **延迟用于建立因果**:遮罩分块先扫入(0–360ms),文字随后(延迟 260ms),投影最后(延迟 600ms);OOBE 字符按正弦曲线错峰。延迟必须表达「谁先谁后」,**不得**所有元素同一时刻启动。
3. **错峰必须封顶**:列表入场 25ms/项(源码)、ClassHelper 35ms/项且第 9 项起封顶、页面块 30ms/块且封顶 180ms(`motion.ts:175-179`;`index.css:857-883`)。超过 10 项仍线性累加会让最后一项等超过 1 秒,属于缺陷。
4. **状态变化必须可打断**:任何「进/出」成对动画必须共用同一元素、同一属性、可反向;反向时用新动画直接覆盖旧动画(不要排队)。遮罩的 in/out 伪类互斥实现(`MainWindowLine.cs:726-791`)就是范例。
5. **不在动画里做业务**:动画只表达状态,不得在 `animationend` 里触发网络请求、不得用动画时长做业务计时;业务定时器与动画解耦。
6. **动效不得改变布局语义**:动画结束后元素的最终几何必须与「无动画直接跳到终态」完全一致;判定方法:动画播放中与播放后分别测量 `getBoundingClientRect`,最终值必须逐像素相等。
7. **一致性优先于炫技**:同一个交互(如「抽屉打开」)在全应用只能有一种时长与曲线;新交互必须复用 `--ci-ease-*` 与 `--ci-dur-*`,不得新增私有曲线。
### 14.1 A. 主界面(岛)动效
主界面动效分三类:**状态过渡**(A1–A4、A8–A9)、**几何过渡**(A5–A7、A11)、**数据过渡**(A10、A12)。状态过渡可直接用 CSS transition;几何过渡必须避免回流(见 14.7 第 1 条);数据过渡(进度条)必须做「比例插值」而不是「每帧重算 DOM」。三类不得混用同一个动画实例,否则打断时会互相污染。
| 编号 | 名称 | 触发 | 变化属性 | 起止值 | 时长 | 曲线 | 延迟/交错 | 实现方式 | 证据 |
|---|---|---|---|---|---|---|---|---|---|
| A1 | 窗口背景换色 | 应用明暗主题切换 | `background` | 旧色 → 新色 | 200ms | 默认线性 | 无 | CSS `transition: background-color 200ms` | `MainWindow.axaml:40-44` |
| A2 | 行淡入 | 行由隐藏变可见 | `opacity` | 0 → 1 | 250ms | `(0.25,1,0.5,1)` | 无 | WAAPI/composition `opacity`,`AnimationLevel≥2` | `MainWindowLine.cs:336-344,382-401` |
| A3 | 行透明度过渡 | 悬停淡出开关生效 | `opacity` | 1 ↔ 0.05 | 100ms | 默认 | 无 | CSS transition | `MainWindowLine.axaml:51,73-75`;`FluentTheme/Styles.axaml:268` |
| A4 | 组件淡入 | 组件挂载/由隐藏变可见 | `opacity` | 0 → 1 | 300ms | `(0.65,0,0.35,1)` | 无 | composition `opacity`,`AnimationLevel≥2` | `ComponentPresenter.axaml.cs:224-243,263-266` |
| A5 | 行宽过渡(Fluent) | 组件内容变化导致背景宽度变化 | `width` | 旧宽 → 新宽 | 300ms | `(0.65,0,0.35,1)` | 无 | CSS transition(背景层绝对定位,禁止回流文字) | `FluentTheme/Styles.axaml:264-273` |
| A6 | 行宽过渡(经典) | 同上 | `width` | 旧宽 → 新宽 | 550ms | `(0,1,0.5,1.1)` 过冲 | 无 | 同上;曲线必须带过冲,不得改成 ease-out | `ClassicTheme/Styles.axaml:202-211` |
| A7 | WrapPanel 子项位置过渡 | 子项增删/重排 | `offset`(x/y) | 旧位置 → 新位置 | 默认 225ms;主界面覆盖 300ms | `CubicEaseInOut`;主界面 `(0.65,0,0.35,1)` | 无 | composition 隐式动画;Web 用 FLIP(先读后写)| `WrapPanelResizingAnimationAssist.cs:45-49,130-137`;`MainWindow.axaml:197-198` |
| A8 | 编辑模式背景切换 | 进入/退出编辑模式 | 窗口背景、`backdrop-filter` | 透明 → 亚克力 | 200ms | 默认 | 无 | CSS transition(只动背景层) | `MainWindow.axaml:177-181`;`MainWindow.axaml.cs:510-517` |
| A9 | 拖动项抬升 | 拖动开始/结束 | `opacity`、`z-index` | 1 → 0.5(拖动中) | 即时 | — | 无 | 直接切 class(不做动画) | `MainWindow.axaml:478-481` |
| A10 | 缩放重置 | 点击「重置视图」 | `transform: scale` | 当前 → 1.0 | 200ms【推断】 | `(0.16,1,0.3,1)` | 无 | WAAPI 动画 transform | `MainWindow.axaml:606-609`;ZoomBorder `MinZoom 0.5/MaxZoom 3/ZoomSpeed 1.2`(`:199-209`) |
| A11 | 分体主界面切换 | 开启/关闭分体 | 行内边距、圆角、背景宽 | 合并 ↔ 分体 | 与 A5 同步 300ms | 同 A5 | 无 | class 切换 + 宽度过渡 | `FluentTheme/Styles.axaml:245-259` |
| A12 | 进度条值过渡 | 每秒/每 50ms 更新 | `value`/`transform: scaleX` | 旧比例 → 新比例 | 50ms | 线性 | 无 | CSS transition `transform` | `FluentTheme/Styles.axaml:137-145`;`ClassicTheme/Styles.axaml:123-131` |
### 14.2 B. 提醒与强提醒动效
提醒是「岛」最重要的动效场景,必须实现两套完全不同的遮罩形态(Fluent/经典),并共用同一套队列与遮罩生命周期状态机。状态机必须严格按源码实现:`:mask-anim` → `:mask-in` →(可选)`:overlay-anim`/`:overlay-in` → `:mask-out` → `:overlay-out`,任一状态切换都通过类名驱动(`Controls/MainWindowLine.cs:724-791`)。**禁止**用 JS 计时器手动改元素 style 来「模拟」状态机,否则队列切换时会错乱。
强提醒特效是**独立置顶窗口**(`TopmostEffectWindow`),不属于岛 DOM;特效元素播放完成后自行移除,窗口内元素清空即隐藏窗口(`TopmostEffectWindow.axaml.cs:50-67,90-98`)。特效必须支持 `NotificationEffectRenderingScale` 缩放(默认 1.0,大屏自动降到 0.75/0.5,见 part 03 表)。
| 编号 | 名称 | 触发 | 属性 | 起止值 | 时长 | 曲线 | 延迟/交错 | 实现方式 | 证据 |
|---|---|---|---|---|---|---|---|---|---|
| B1 | Fluent 遮罩斜切扫入 | `:mask-in` | 5 个区段宽度进度 | 0 → 1 | 每段 360ms | `QuarticEaseOut`(≈`(0.16,1,0.3,1)`【推断】) | 段间 120ms:区 0/4 同起,1/3 延迟 90ms,2 延迟 120ms | canvas/SVG 自绘 + rAF 或 WAAPI 驱动进度 | `SlantedMaskControl.cs:166-202,225-251`;`FluentTheme/Styles.axaml:175-180` |
| B2 | Fluent 遮罩文字淡入 | `:mask-in` | `opacity` | 0 → 1 | 250ms | `(0.25,1,0.5,1)` | 延迟 260ms | CSS animation `both` | `FluentTheme/Styles.axaml:329-340` |
| B3 | Fluent 遮罩文字缩放 | `:mask-in` | `scale` | 1.1 → 1.0 | 750ms | `(0.25,1,0.5,1)` | 延迟 260ms | CSS transform animation | `FluentTheme/Styles.axaml:341-352` |
| B4 | Fluent 遮罩投影淡入 | `:mask-in` | 背景层 `opacity`、`box-shadow` | 0 → 1;阴影 `0 8 32 #66000000` | 100ms | 默认 | 延迟 600ms | CSS animation + `transition: box-shadow 100ms` | `FluentTheme/Styles.axaml:169-173,355-367` |
| B5 | Fluent 遮罩文字淡出 | `:mask-out` | `opacity` | 1 → 0 | 200ms | 默认 | 无 | CSS animation | `FluentTheme/Styles.axaml:376-386` |
| B6 | Fluent 遮罩文字放大退出 | `:mask-out` | `scale` | 1.0 → 1.1 | 200ms | `(0.5,0,0.75,0)` | 无 | CSS animation | `FluentTheme/Styles.axaml:387-398` |
| B7 | Fluent 遮罩背景淡出 | `:mask-out` | 背景层 `opacity` | 1 → 0 | 100ms | 默认 | 无 | CSS animation | `FluentTheme/Styles.axaml:401-409` |
| B8 | 覆盖层淡入/淡出 | `:overlay-in`/`:overlay-out` | `opacity` | 0 ↔ 1 | 200ms | `SineEaseIn`≈`(0.12,0,0.39,0)`【推断】 | 无 | CSS transition | `FluentTheme/Styles.axaml:419-451` |
| B9 | 内容上滑进入(遮罩层内) | 遮罩内内容切换 | `translateY` | `-50` → 0 | 200ms | `SineEaseIn` | 无 | CSS transition | `ClassicTheme/Styles.axaml:237-249,266-281` 相关;主窗口 `TranslateTransform Y=-50`(`ClassicTheme/Styles.axaml:87,148`) |
| B10 | Classic 遮罩下压 | `:mask-in` | `translateY` | -50 → 0 | 200ms | `QuadraticEaseIn` | 无 | CSS animation `both` | `ClassicTheme/Styles.axaml:265-281` |
| B11 | Classic 遮罩文字淡入 | `:mask-in` | `opacity` | 0 → 1 | 100ms | 默认 | 延迟 200ms | CSS animation | `ClassicTheme/Styles.axaml:288-300` |
| B12 | Classic 遮罩上滑退出 | `:mask-out` | `translateY` | 0 → 50 | 200ms | `QuadraticEaseOut` | 延迟 100ms | CSS animation `forward` | `ClassicTheme/Styles.axaml:305-318` |
| B13 | Classic 遮罩文字淡出 | `:mask-out` | `opacity` | 1 → 0 | 100ms | 默认 | 无 | CSS animation | `ClassicTheme/Styles.axaml:319-331` |
| B14 | Classic 行投影淡入 | `:mask-in` | `effect`(`DropShadowEffect`) | opacity 0 → 1,blur 8 | 100ms | 线性 | 无 | CSS `box-shadow` transition(禁止 filter) | `ClassicTheme/Styles.axaml:34-45,283-287` |
| B15 | 提醒进度条 | 提醒剩余时间更新 | `value` | 旧 → 新 | 50ms | 线性 | 无 | CSS transition `transform: scaleX` | `FluentTheme/Styles.axaml:137-145` |
| B16 | 强提醒涟漪扩散 | 提醒开始(特效开启) | `scale` | 0 → 1 | 600ms | `QuadraticEaseIn` | 无 | WAAPI `transform`,原点=岛中心 | `RippleEffect.axaml.cs:86-96` |
| B17 | 强提醒涟漪淡出 | 同上 | `opacity` | 1 → 0 | 600ms | `SineEaseIn` | 无 | WAAPI `opacity` | `RippleEffect.axaml.cs:102-106` |
| B18 | 强提醒结束回调 | 特效完成 | — | — | 总 750ms | — | 600ms 动画后等 150ms | `setTimeout(750)` 后移除特效元素 | `RippleEffect.axaml.cs:108-109`;`TopmostEffectWindow.axaml.cs:90-98` |
| B19 | 逐字滚动提醒 | 提醒内容超宽 | `offset.x` | 右边界 → 左边界循环 | `Duration / RepeatCount` | 线性 | 无 | WAAPI 无限循环,每帧线性位移 | `RollingTextTemplate.axaml.cs:39-62`;`RollingTextTemplateData.RepeatCount` 默认 2 |
| B20 | 滚动提醒重排修正 | 容器尺寸变化 | `offset.x` | 按当前进度重算 | 即时重播 | 线性 | 无 | 重新 `startAnimation`(源码在 LayoutUpdated 重设) | `RollingTextTemplate.axaml.cs:75-80` |
### 14.3 C. 启动、OOBE、欢迎动效
启动链路是「闪屏 → 主界面」两段式:闪屏自身有滑入与进度动画,主界面出现时行/组件按动画级别淡入(A2/A4)。OOBE 是唯一允许使用 3 秒级长动画的场景,必须整段可跳过(用户点击任意处立即进入下一步),且在 `AnimationLevel=0` 时直接呈现终态(`OobeIntroAnimationControl.axaml.cs:161-164` 只在 Loaded 时播放;实现时须先判断级别)。
OOBE 的字符动画是「11 个方块 + 11 个字符」双轨并行的复杂编排,必须按源码公式逐字计算延迟(C6),不得改成固定 50ms 等间隔——源码用正弦分布让中部字符更密、两端更疏(`OobeIntroAnimationControl.axaml.cs:37`)。
| 编号 | 名称 | 触发 | 属性 | 起止值 | 时长 | 曲线 | 延迟/交错 | 实现方式 | 证据 |
|---|---|---|---|---|---|---|---|---|---|
| C1 | 闪屏 Logo 滑入 | 闪屏启动 | `translateX` | 32 → 0 | 300ms | `ExponentialEaseInOut`≈`(0.16,1,0.3,1)`【推断】 | 0ms | WAAPI transform | `SplashWindow.axaml.cs:101-119` |
| C2 | 闪屏状态栏滑入 | 闪屏启动 | `translateX` | 32 → 0 | 300ms | 同上 | 150ms | WAAPI transform | `SplashWindow.axaml.cs:53-54,109-119` |
| C3 | 闪屏淡入 | 闪屏启动 | `opacity` | 0 → 1 | 300ms | `CubicEaseOut` | 与 C1/C2 同延迟 | WAAPI opacity | `SplashWindow.axaml.cs:114-120` |
| C4 | 闪屏进度条 | 启动进度更新 | `translateX` | `-400·(1-p)` 到目标 | `max((Δp)/8, 0.5)s`;结束时 150ms | `CubicEaseOut` | 无 | WAAPI transform | `SplashWindow.axaml.cs:64-82,126` |
| C5 | OOBE 整场进场 | 欢迎页加载 | `scale`、`opacity` | 1.25 → 1.0;0 → 1 | 3000ms | `(0,0.83,0.17,0.90)` | 无 | CSS animation `both` | `OobeIntroAnimationControl.axaml:14-30` |
| C6 | OOBE 字符错峰翻入 | 进场后逐字 | `translateY`、`opacity` | 50 → 0;0 → 1 | `delay·9·1.5 + 750` ms | `(0.25,1,0.5,1)`(入) | 逐字延迟 `sin((i+2)/(n+2)·π/2)·500/n` ms | WAAPI 逐元素动画,串行 await | `OobeIntroAnimationControl.axaml.cs:30-54,65-124` |
| C7 | OOBE 字符翻出 | 同一字符动画后段 | `rotate3D.angleX`、`opacity` | 0 → 90°;1 → 0 | 同上 | `(0.32,0,0.67,0)` | 内嵌于 C6 关键帧 | WAAPI 关键帧(0%/timeMs/timeMs+750/100%) | `OobeIntroAnimationControl.axaml.cs:92-118` |
| C8 | OOBE 文字块翻入 | 字符对转向后 | `rotate3D.angleX`、`opacity` | -90° → 0;0 → 1 | `delay·9·2 + 750` ms | `(0.33,1,0.68,1)` | 内嵌 | WAAPI 关键帧 | `OobeIntroAnimationControl.axaml.cs:126-158` |
| C9 | OOBE 方块放大 | 第 7 个方块 | `scaleX`、`background` | 1 → 2.17;灰 → `#00BFFF` | 250ms | `(0.76,0,0.24,1)` | 延迟 307ms | CSS animation | `OobeIntroAnimationControl.axaml:44-58` |
| C10 | OOBE 文字收拢 | 动画末尾 | `gap`/`letter-spacing`、`min-width` | 4px → 0;32px → 0 | 700ms | `(0.33,1,0.68,1)` | 无 | CSS animation | `OobeIntroAnimationControl.axaml:88-116` |
| C11 | 欢迎页内容淡入 | 欢迎页显示 | `opacity` | 0 → 1 | 200ms | 默认 | 无 | CSS animation `both` | `Views/WelcomePages/WelcomePage.axaml:11-24` |
| C12 | 欢迎页 Logo 布局 | 欢迎页显示 | 尺寸 | 80×80 | — | — | — | 静态,非动画 | `WelcomePage.axaml:29-33` |
### 14.4 D. 弹层、抽屉、列表与设置动效
本组是「应用外壳」动效,必须复用 ClassHelper 既有 token 与 Element Plus 覆盖层(`renderer/styles/index.css:567-756` 已把弹窗/抽屉/浮层/消息条的过渡统一替换为弹簧与 `--ch-ease-out`)。实现时**不得**再写第二套弹窗动画;新弹层一律继承既有 class。
列表错峰(D12)必须是「真实逐项启动」而不是一次性 `animation-delay` 批量:源码用 25ms 间隔的 DispatcherTimer 逐项置位(`StackPanelIntroAnimationBehavior.cs:74-90`),逐项启动才能保证「列表在动画过程中被重新排序/过滤」时新项不等待旧延迟。
| 编号 | 名称 | 触发 | 属性 | 起止值 | 时长 | 曲线 | 延迟/交错 | 实现方式 | 证据 |
|---|---|---|---|---|---|---|---|---|---|
| D1 | Popup 淡入 | 任意 Popup 打开 | `opacity` | 0 → 1 | 150ms | `(0.22,1,0.36,1)` | 无 | WAAPI(`AnimationLevel≥1`) | `PopupIntroAnimationBehavior.cs:73-78` |
| D2 | Popup 缩放淡入 | 同上 | `scale` | 0.925 → 1.0 | 150ms | `(0.22,1,0.36,1)` | 无 | WAAPI;`transform-origin` 按 placement 决定(Bottom→上中、Right→左中、Pointer→左上、Center→中心) | `PopupIntroAnimationBehavior.cs:80-112` |
| D3 | 横抽屉推入 | 抽屉打开 | `translateX`、`opacity` | 抽屉宽 → 0;0 → 1 | 250ms | `cubic-bezier(0,0,0,1)` | 无 | CSS transition | `DrawerHost.axaml:42-59` |
| D4 | 横抽屉遮罩 | 抽屉打开 | `opacity` | 0 → 1 | 250ms | `(0,0,0,1)` | 无 | CSS transition | `DrawerHost.axaml:52-58,68-70` |
| D5 | 竖抽屉推入 | 抽屉打开/收起 | `translateY`(Canvas.Bottom)、`height` | 关闭高度 → 0 | 250ms | `(0,0,0,1)` | 无 | CSS transition(用 transform 代替 bottom/height) | `VerticalDrawer.axaml:29-36,136-155` |
| D6 | 竖抽屉遮罩 | 抽屉打开 | `opacity` | 0 → 1 | 250ms | `(0,0,0,1)` | 无 | CSS transition | `VerticalDrawer.axaml:126-134,150-153` |
| D7 | 图标按钮展开 | 展开/收起 | `width`、文字 `opacity` | 图标宽 ↔ 全宽;0 ↔ 1 | `Duration` 属性(默认【推断】200ms) | `SineEaseOut`(宽)/`SineEaseInOut`(文字) | 无 | CSS transition | `AnimatedIconButton.axaml:22-46` |
| D8 | 图标按钮底色 | 悬停/主题变化 | `background` | 旧色 → 新色 | 150ms | 默认 | 无 | CSS transition | `AnimatedIconButton.axaml:81-88` |
| D9 | Shimmer 骨架淡出 | 内容加载完成 | `opacity` | 1 → 0(骨架);0 → 1(内容) | 200ms | `(0.65,0,0.35,1)` | 无 | CSS transition | `Shimmer.axaml:22-38,43-54` |
| D10 | 列表滑入 | 选中项变化 | `translateY`、`opacity` | -40 → 0;0 → 1 | 250ms | `(0.65,0,0.35,1)` | 无 | CSS animation `forward` | `SlidingListBox.axaml:38-55` |
| D11 | 列表滑出 | 同上 | `translateY`、`opacity` | 0 → 40;1 → 0 | 250ms | `(0.65,0,0.35,1)` | 无 | CSS animation | `SlidingListBox.axaml:57-74` |
| D12 | 堆叠面板错峰入场 | 面板首次加载 | `translateY`、`opacity` | 50 → 0;0 → 1 | 750ms | `(0,1,0,1)` | **每项间隔 25ms**(DispatcherTimer) | rAF/`setInterval(25ms)` 逐项加 class | `StackPanelIntroAnimationBehavior.cs:74-90`;`StackPanelIntroAnimation.axaml:11-35` |
| D13 | 工作流进度条颜色 | 状态变化 | `background` | 灰 ↔ accent | 500ms | `SineEaseInOut` | 无 | CSS transition | `WorkflowProgressIndicatorItemControl.axaml:16-22` |
| D14 | 工作流不确定态呼吸 | 进行中且无进度 | `opacity` | 0.85 → 0.4 → 0.85 | 750ms | `SineEaseInOut` | 无限交替 | CSS animation alternate infinite | `WorkflowProgressIndicatorItemControl.axaml:28-44` |
| D15 | 工作流确定性进度 | 有进度值 | `value` | 旧 → 新 | 180ms | `SineEaseInOut` | 无 | CSS transition transform | `WorkflowProgressIndicatorItemControl.axaml:57-67` |
| D16 | 工作流完成淡入 | 完成后 | `opacity` | 0 → 1 | 300ms | `SineEaseOut` | 无 | CSS transition | `WorkflowProgressIndicatorItemControl.axaml:63-65` |
| D17 | TeachingTip 箭头循环 | 教学提示显示 | `translateY`、`rotateY` | 0→-8→0;0→180° | 1000ms | `(0.5,1,0.89,1)`/`(0.11,0,0.5,0)`/`(0.66,0,0.34,1)` | 无限 | CSS animation infinite | `MyTeachingTipStyles.axaml:223-251` |
| D18 | 教程高亮入场 | 教程步骤开始 | `margin`、`opacity` | -64 → 0;0 → 1 | 750ms | `(0.16,1,0.3,1)` | 无 | CSS animation `both` | `TutorialHighlightControl.axaml:11-24` |
| D19 | 教程高亮呼吸循环 | 教程步骤持续 | `margin`、`opacity` | -64→0→0;0→1→1 | 1750ms | `(0.16,1,0.3,1)` | 延迟 1750ms,无限 | CSS animation infinite | `TutorialHighlightControl.axaml:25-44` |
| D20 | 教程聚光入场 | 教程步骤开始 | `margin`、`opacity` | 64 → 0;0 → 1 | 750ms | `(0.16,1,0.3,1)` | 无 | CSS animation | `TutorialSpotlightControl.axaml:11-35` |
| D21 | 更新图标旋转 | 下载更新中 | `rotate` | 0 → 360° | 1000ms | `(0.5,0.25,0.5,0.75)` | 无限 | CSS animation infinite | `UpdateSettingsPage.axaml:88-99` |
| D22 | 关于页头部淡入 | 页面加载 | `opacity` | 0 → 1 | 200ms | `(0,0,0,1)` | 延迟 250ms | CSS animation `both` | `AboutSettingsPage.axaml:133-147` |
| D23 | 拖拽高亮脉冲 | 拖动组件中 | `opacity` | 0.1 → 0.6 → 0.1 | 1000ms | `SineEaseInOut` | 无限交替 | CSS animation alternate infinite | `EditableComponentsListBox.axaml:307-335` |
| D24 | 设置面板显隐 | 子设置展开 | `opacity` | 0 ↔ 1 | 200ms | `(0,1,0,1)` | 无 | CSS transition | `WeatherSettingsPage.axaml:207-214` |
| D25 | 自动化项显隐 | 条件切换 | `opacity` | 0 ↔ 1 | 100ms | 默认 | 无 | CSS transition | `AutomationSettingsPage.axaml:140-142` |
| D26 | 列表项淡入(课表) | 课表项状态变化 | `opacity` | 0 ↔ 1 | 150ms | 默认 | 无 | CSS transition | `LessonsListBox.axaml:69-77` |
### 14.5 E. 组件动效
组件动效的原则是「**动内容不动容器**」:滚动、滑动、倒计时进度都在组件内部完成,绝不触发行高/行宽变化(否则会触发 A5/A6 的行宽过渡,产生连锁抖动)。滚动组件的速度单位是「像素/秒」,必须由设置项驱动并做下限保护(`Math.Clamp(SpeedPixelPerSecond, 1, int.MaxValue)`,`RollingComponent.axaml.cs:96`),暂停时长默认 10 秒,暂停位置默认 0(`RollingComponentSettings.cs`)。
时钟、天气图标、时间线色块**不做**动画:它们是高频更新的信息元素,任何过渡都会让「时间在走」变得可感知延迟。源码只在字号/颜色上做静态样式(`ClockComponent.axaml:14`、`WeatherComponent.axaml:261-305`)。
| 编号 | 名称 | 触发 | 属性 | 起止值 | 时长 | 曲线 | 延迟/交错 | 实现方式 | 证据 |
|---|---|---|---|---|---|---|---|---|---|
| E1 | 滚动组件滚动 | 内容宽于容器 | `translateX` | 0 → -(W+offset) → 容器宽 → 0 | `(暂停+滚动)` 秒;速度 40px/s | 线性 | 暂停 10s(默认) | WAAPI 关键帧无限循环 | `Controls/Components/RollingComponent.axaml.cs:85-135`;`RollingComponentSettings.cs`(`SpeedPixelPerSecond=40`、`PauseSeconds=10`) |
| E2 | 滚动组件重置 | 停止滚动/规则命中 | `translateX` | 归零 | 1s(空动画占位) | 线性 | 无 | 用 1s 静止关键帧覆盖旧动画 | `RollingComponent.axaml.cs:137-157` |
| E3 | 滚动组件停止规则 | 规则命中 | 滚动状态 | 滚动 → 停止 | 即时 | — | 无 | 取消动画并归位 | `RollingComponent.axaml.cs:171-179` |
| E4 | 滑动组件切换 | 选中项变化 | `translateY`、`opacity` | -40→0 / 0→40;0↔1 | 250ms | `(0.65,0,0.35,1)` | 无 | CSS animation `forward` | `Controls/Components/SlideComponent.axaml:69-107` |
| E5 | 滑动组件禁用动画 | 设置关闭动画 | — | 立即切换 | 0ms | — | — | 关闭时直接切 `display` | `SlideComponent.axaml:51-53` |
| E6 | 倒计时进度 | 计时更新 | 进度值 | 旧 → 新 | 50ms(同 A12) | 线性 | 无 | CSS transition;降低精度时按 `max(10, 总秒/500)` 取整 | `CountDownComponent.axaml.cs:288-295`;`Settings.cs:850` |
| E7 | 时钟秒变化 | 秒变化 | 文本内容 | — | 无动画 | — | — | 直接替换文本(不要做数字翻转动画) | `ClockComponent.axaml:14`(仅字号) |
| E8 | 天气图标切换 | 天气数据更新 | 图标字形 | 旧 → 新 | 无动画 | — | — | 直接替换(4 套模板,见 part 15) | `Controls/WeatherIcons/*.axaml` |
| E9 | 时间线色块 | 时间点类型 | 颜色/高度 | 类型色 | 无动画 | — | — | 静态 | `Themes/Generic.xaml:65-128` |
| E10 | 时间线装饰器出现 | 悬停/选中 | 透明度 | 0 → 1 | 200ms【推断】 | 默认 | 无 | 由行为控制(`TimeLineListItemBehavior`),Web 用 CSS transition | `Themes/Generic.xaml:190-290` |
| E11 | 提醒遮罩分块绘制 | 遮罩打开 | 5 段平行四边形宽度 | 0 → 全宽 | 同 B1 | 同 B1 | 同 B1 | canvas 绘制(每帧按进度重绘) | `SlantedMaskControl.cs:273-343` |
### 14.6 F. ClassHelper 既有动效(必须复用,不得重造)
ClassHelper 已经沉淀了一套完整动效体系,**必须原样复用**,只在「ClassIsland 特有场景」上做增量:
| 既有能力 | 位置 | 复用方式 |
|---|---|---|
| 动效 token 注入 | `packages/shared/src/motion.ts` → 两端 `composables/motion.ts` | 在 `app.mount()` **之前**写入 `:root`(首帧即就位,无 FOUC) |
| 弹簧曲线转 CSS `linear()` | `motion.ts:152-164` | 直接调用 `springLinearEasing()`,禁止手写采样点 |
| 减少动态效果 | `composables/motion.ts:53-72` | 主界面/管理中心统一使用 `useReducedMotion()` |
| 悬停能力检测 | `composables/motion.ts:81-99` | 所有 hover-only 效果必须先过 `useHoverCapable()`(教室触摸屏幽灵 hover) |
| 数字滚动 | `composables/motion.ts:129-167` | 倒计时/统计数字直接复用 `useCountUp()` |
| 错峰入场 | `composables/motion.ts:175-179` | 列表/卡片直接复用 `useStaggerIn()` |
| 整页换肤 | `composables/motion.ts:201-237` | 应用主题切换复用 `useThemeReveal()` |
硬约束:**不得**在岛渲染进程里引入主渲染进程的 `--ch-*` 动效 token(ClassHelper 明确「岛是另一个渲染进程,刻意不参与这里的动效体系」,`motion.ts:29-31`)。岛必须使用自己的 `--island-*` 与 `SpringValue`。
| 编号 | 名称 | 触发 | 属性 | 起止值 | 时长 | 曲线 | 延迟/交错 | 实现方式 | 证据 |
|---|---|---|---|---|---|---|---|---|---|
| F1 | 岛尺寸弹簧 | 模式/尺寸变化 | `width`、`height`、`radius` | 当前 → 目标 | 由物理积分决定 | 弹簧 `stiffness=0.10`、`damping=0.68` | 无 | 每帧 `SpringValue.step()`,60fps 单位 | `ClassHelper/src/island/spring.ts:26-114`;`IslandApp.vue:216-322` |
| F2 | 岛换向限速 | 目标方向反转 | `velocity` | 反向保留 35%,帧位移 ≤ 剩余 20% | 每帧 | — | 无 | `redirectVelocity` | `spring.ts:66-82` |
| F3 | 岛零过冲钳制 | 越过目标 | `value`、`velocity` | 吸附目标、速度清零 | 即时 | — | 无 | `step()` 内判定 | `spring.ts:100-107` |
| F4 | 岛淡入(窗口) | 窗口显示 | 窗口 `opacity` | 当前 → 目标 | 每帧 +0.16,16ms 步进(约 60–110ms) | 线性 | 无 | 主进程 `setOpacity` 定时步进 | `src/main/island.ts:1674-1691` |
| F5 | 岛淡出(窗口) | 窗口隐藏 | 窗口 `opacity` | 当前 → 0 | 每帧 -0.2,16ms 步进(约 50–80ms) | 线性 | 无 | 主进程 `setOpacity`,到 ≤0.02 后 `hide()` | `island.ts:1693-1715` |
| F6 | 岛显示方式 | 显示 | 焦点 | `showInactive()` 不抢焦点 | 即时 | — | 无 | 主进程 | `island.ts:1426,1458,1490` |
| F7 | 弹窗弹簧入场 | 弹窗打开 | `transform`、`opacity` | `translateY(-12px) scale(.97)` → 0/1 | `SPRING_PANEL` 自然落定(约 463ms) | 弹簧 `linear()` 或 `cubic-bezier(.16,1,.3,1)` 回退 | 无 | CSS animation | `index.css:596-605,634-636`;`motion.ts:52,220-230` |
| F8 | 弹窗快速退出 | 弹窗关闭 | `transform`、`opacity` | → `translateY(-6px) scale(.985)` | 140ms | `(0.16,1,0.3,1)` | 无 | CSS animation | `index.css:607-616,642-644` |
| F9 | 抽屉弹簧 | 抽屉打开/关闭 | `transform`、`opacity` | 侧边 ↔ 0 | `SPRING_PANEL` 463ms / `--ch-dur-base` 200ms | 弹簧 / ease-out | 无 | CSS transition | `index.css:699-703` |
| F10 | 浮层缩放 | 下拉/选择器 | `transform`、`opacity` | `scale(.95)` → 1 | `SPRING_SWAP` 约 263ms | 弹簧 | 无 | CSS transition | `index.css:710-737`;`motion.ts:50,228-229` |
| F11 | 消息条进出 | 消息出现/消失 | `transform`、`opacity`、`top/bottom` | 位移 | 弹簧 263ms / 140ms | 弹簧 / ease-out | 无 | CSS transition | `index.css:739-756` |
| F12 | 按钮按压 | 按下/抬起 | `transform: scale` | 1 → 0.97 → 1 | 按下 90ms;抬起 `SPRING_PRESS` 约 194ms【推断】 | 按下 ease-out;抬起弹簧 | 无 | CSS transition(按下与基础规则分离) | `index.css:769-782`;`motion.ts:48,178` |
| F13 | 卡片抬升 | 悬停(仅 hover 设备) | `transform: translateY`、`box-shadow` | 0 → -1px | 263ms | 弹簧/ease-out | 无 | CSS transition,**必须**包在 `@media (hover:hover) and (pointer:fine)` 内 | `index.css:791-802` |
| F14 | 列表错峰入场 | 列表渲染 | `opacity`、`translateY` | 0/6px → 1/0 | 320ms | `(0.16,1,0.3,1)` | 每项 35ms,第 9 项起封顶 | CSS animation + 内联 `--ch-reveal-delay` | `index.css:814-828`;`motion.ts:175-179` |
| F15 | 页面块错峰入场 | 路由切换 | `opacity`、`translateY` | 0/6px → 1/0 | 320ms | `(0.16,1,0.3,1)` | 30ms/块,封顶 180ms | CSS animation `nth-child` | `index.css:846-883` |
| F16 | 整页换肤扩散 | 切换主题 | `clip-path: circle()` | 0px → 最远角半径 | 480ms(320×1.5) | `(0.16,1,0.3,1)` | 无 | View Transition API + WAAPI | `composables/motion.ts:201-237` |
| F17 | 侧栏折叠 | 折叠/展开 | `width`、`gap` | 全宽 ↔ 64–80px | 260ms | `cubic-bezier(0.4,0,0.2,1)` | 无 | CSS transition | `index.css:56-75` |
| F18 | 减少动态效果 | 系统偏好 | 全部时长 | → 1ms | 1ms | — | — | 全局媒体查询覆盖(加载指示器例外,放慢到 1.6s) | `index.css:904-930` |
### 14.7 实现方式规范(必须遵守)
1. **优先使用合成器友好属性**:只对 `transform` 与 `opacity` 启 CSS transition/WAAPI。源码中 `BackgroundWidth`(A5/A6)与 `Margin`(D18/D19)属于布局属性,Web 实现必须做等价转换:
- 行宽过渡:背景层绝对定位于内容层之下,宽度用 `clip-path: inset(0 calc(100% - var(--w)) 0 0 round <r>)` 或 `transform: scaleX()`(配合 `transform-origin: left`)实现,**不得**对包含文字的容器直接 `transition: width`(会导致逐帧重排)。
- 教程高亮:把 `margin` 动画换成 `transform: scale()`(围绕中心放大)或 `inset` 的 `clip-path` 动画,视觉等价。
2. **弹簧动画**:岛尺寸/圆角必须用 ClassHelper 的 `SpringValue`(F1–F3),不得改用 CSS 弹簧曲线;两条路径不能混用(ClassHelper `motion.ts:29-33` 明确「岛是另一个渲染进程,不参与本动效体系」)。
3. **弹簧曲线用于 CSS 时**:过冲曲线只能用于 `transform`/`clip-path`,**禁止用于 `opacity`**(会被裁剪到 [0,1],观感变成「到顶就停」)(`motion.ts:148-151`)。
4. **`filter` 禁忌**:卡片类元素禁止用 `filter: drop-shadow` 做投影(会新建渲染表面,开合瞬间闪帧),必须用 `box-shadow`(`classic-island.css:114-120`)。
5. **`will-change`**:只允许在动画进行中的元素上临时添加 `will-change: transform, opacity`,动画结束(`animationend`/`transitionend`)立即移除;禁止全局常驻。
6. **禁止动画的属性**:`z-index`、`display`(用 `visibility`/`opacity` 或先 `display` 后下一帧启动)、`pointer-events`。需要「显隐 + 动画」时用双 rAF 或 `requestAnimationFrame` 起手。
7. **中断语义**:任何进场动画被反向触发时必须能平滑打断(`FillMode.Both/Forward` 对应 `animation-fill-mode`;用 `getAnimations().forEach(a => a.cancel())` 清理旧动画)。
8. **统一调度**:动画必须在 `AnimationLevel=0` 时统一短路(除主界面提醒);同一时刻同一元素不得叠加两条竞争动画。
9. **帧率与预算**:岛内动画(弹簧步进)必须跑在独立渲染进程的 rAF 上;主渲染进程的 60fps 预算为 16.7ms,任何单帧主线程任务禁止超过 8ms;动画期间不得读取 `offsetWidth` 等强制同步布局属性。
10. **禁止「看起来像动画的定时器」**:所有时长必须来自 14.0.3 常量表,不得在组件里写 `300`、`0.3s` 之类字面量。
#### 14.7.1 几何动画的 FLIP 流程(必须按此实现)
源码大量动画布局属性(宽度、间距、边距),Web 端必须用 FLIP 换算成合成器动画:
1. **First**:在状态改变前读取元素 `getBoundingClientRect()` 并缓存(只在必要时读一次,避免强制同步布局)。
2. **Last**:改完 class/数据后,下一帧再读一次新几何。
3. **Invert**:计算 `dx = old.left - new.left`、`dy = old.top - new.top`、`sx = old.width / new.width`,对元素设置 `transform: translate(dx,dy) scale(sx)` 并临时关掉 transition。
4. **Play**:强制一次重排(读 `offsetHeight` 或 `getBoundingClientRect`),再恢复 transition 并清空 transform,让浏览器从「反演位置」过渡到「最终位置」。
FLIP 的硬性要求:整个流程必须在**同一帧内**完成读取与写入的分离(先批量读、再批量写),否则会触发多次回流;对列表批量移动时必须逐个元素做,不得在父容器上做 scale(会把文字一起拉伸)。
#### 14.7.2 行宽过渡的推荐实现(替换源码的 BackgroundWidth 动画)
源码动画的是背景层 `Width`(`FluentTheme/Styles.axaml:81`、`ClassicTheme/Styles.axaml:63`),因为背景层只承载底色与描边,不承载文字。Web 端必须照此分层:
```html
<div class="island-line" style="--line-w: 420px; --line-r: 8px">
<div class="island-bg"></div> <!-- 绝对定位;只动画这一层 -->
<div class="island-content">…</div>
</div>
```
```css
.island-bg{
position:absolute; inset:0 auto 0 0; width:var(--line-w);
border-radius:var(--line-r); background:var(--ci-island-bg);
transition: width var(--ci-line-w-dur) var(--ci-line-w-ease);
will-change: width; /* 仅动画期间由 JS 加上 */
}
```
- Fluent:`--ci-line-w-dur:300ms; --ci-line-w-ease:cubic-bezier(.65,0,.35,1)`。
- 经典:`--ci-line-w-dur:550ms; --ci-line-w-ease:cubic-bezier(0,1,0.5,1.1)`。
- 内容层宽度变化必须瞬时(不加过渡),否则文字会在动画中抖动。
- 背景层宽度变化会触发布局,但因为它只有底色、且 `contain: strict` 可限制影响范围,实测可接受;若追求纯合成器动画,可改用 `clip-path: inset(0 calc(100% - var(--line-w)) 0 0 round var(--line-r))`,但圆角与描边的贴合需要额外验证。
#### 14.7.3 斜切遮罩的 canvas 实现要点
若选择 canvas 复刻 `SlantedMaskControl`:
- 每个区段的几何:中心点 `rCenter`、当前宽度 `currentW = fullW × progress`、斜切偏移 `offset = tan(30°) × h`(`SlantedMaskControl.cs:281-343`)。
- 绘制顺序:区 0/4(两端)→ 区 1/3 → 区 2(中间),与 B1 的交错延迟一致。
- 进度驱动:`requestAnimationFrame` 中累加时间,按 `progress = easeOutQuart(t)` 重算并 `ctx.clearRect` + 重绘;`RegionDurationMs=360`、`StageStaggerMs=120` 必须可配置(主题可覆盖,见 part 09 的 `--ci-island-mask-*`)。
- 绘制完成后必须停止 rAF,不得常驻渲染循环;遮罩关闭时按 B5–B7 反向播放。
- 若用 DOM/SVG 实现,5 个 `polygon` 的 `points` 用 CSS 变量驱动 `clip-path`,但注意 `clip-path` 的百分比无法表达斜切角,必须用 `path()` 或 `polygon()` 的绝对像素值并在 resize 时重算。
#### 14.7.4 动效的测试钩子(必须提供)
为便于验收,实现必须暴露以下测试接口(仅开发构建可用):
- `window.__ciMotion.getState(id)`:返回某动画的 `{playing, elapsed, duration, easing}`。
- `window.__ciMotion.list()`:返回当前所有运行中动画的 id 与元素选择器。
- `window.__ciMotion.setLevel(0|1|2)`:运行时切换动画级别。
- `window.__ciMotion.disable()`:等价 `prefers-reduced-motion`。
这些钩子必须不影响生产构建(通过 `import.meta.env.DEV` 或独立 dev 入口注入)。
### 14.8 与源码的差异说明(必须写进实现注释)
- 源码用 Avalonia `Transitions`(属性级隐式动画)与 `Animation`(关键帧)两套机制;Web 端对应 `transition` 与 `@keyframes`/WAAPI。二者的「延迟 + 填充模式」语义必须一一对应,特别是 `FillMode="Both"`(对应 `animation-fill-mode: both`),漏掉会在延迟期间闪出终态(`StackPanelIntroAnimation.axaml:22`、`FluentTheme/Styles.axaml:332`)。
- 源码 `ClassicTheme` 的行宽过渡带过冲(`1.10`),这是「经典主题手感」的一部分,**不得**为了「统一」改成 Fluent 的曲线。
- 源码 `SlantedMaskControl` 用 `canvas` 逐帧重绘 5 段平行四边形(`SlantedMaskControl.cs:273-343`),Web 端可以用 canvas 2D 或 5 个 `clip-path` 的多边形,但**分块权重必须保持** `[0.0, 0.1, 0.2, 0.4, 0.2, 0.1]`(`SlantedMaskControl.cs:298`),且斜切角固定 60°(偏移 `tan(30°)·h`,`:284`)。
- 源码的时间轴用 `DispatcherTimer(25ms)` 驱动列表项逐个入场(`StackPanelIntroAnimationBehavior.cs:74-90`);Web 端用 `setTimeout` 链或 `requestAnimationFrame` 时间片,间隔仍然 25ms,不得改成 CSS `animation-delay` 批量(会与「列表顺序变化」脱节)。
#### 14.8.1 性能预算与降级矩阵
| 场景 | 目标帧率 | 单帧主线程预算 | 降级顺序 | 说明 |
|---|---|---|---|---|
| 岛弹簧(独立渲染进程) | 60fps | 该进程 8ms | ① 降低步进频率到 30fps ② 关闭圆角形变只动尺寸 ③ 直接落位 | 必须在独立进程,不得与主界面共用事件循环 |
| 提醒遮罩(canvas 重绘) | 60fps | 4ms | ① 减到 3 段 ② 降到 30fps ③ 改为纯透明度过渡 | 重绘面积大,必须先算好路径再绘制 |
| 列表错峰入场 | 60fps | 8ms | ① 只动画 opacity ② 减少同屏项数 ③ 直接显示 | 禁止对每项做 FLIP |
| 窗口级淡入淡出(主进程) | 30fps 足够 | — | 直接落位 | 主进程 `setOpacity` 步进不占渲染线程 |
| 整页换肤(View Transition) | 60fps | 16ms | ① 缩短到 320ms ② 关掉圆形扩散改交叉淡入 ③ 直接切换 | 文档不可见时必须同步切换(`motion.ts:195-200`) |
降级必须是**自动**的:连续 30 帧低于 45fps 时自动降一级,并在 5 秒后尝试恢复;降级不得改变最终视觉终态,只改变过程。
#### 14.8.2 可访问性要求
1. **减少动态效果**:必须响应系统偏好,且提供应用内「动画级别=禁用」作为等效手段(两者任一命中即降级)。
2. **不得用闪烁传达信息**:任何闪烁频率必须低于 3Hz;源码没有任何闪烁动画,实现也不得新增。
3. **动画不得是唯一反馈**:按钮按压、开关切换等必须同时有颜色/状态变化,不能只靠缩放。
4. **焦点不丢**:动画期间焦点元素不得被移除;抽屉/弹窗关闭后焦点必须回到触发按钮。
5. **文字可读**:动画过程中文字不得低于 4.5:1 对比度(遮罩淡入时文字必须等遮罩底色到位后再出现,源码用延迟 260ms 实现,见 B2)。
6. **超大字号**:用户把 4 档字号调到 40px 时,所有含固定高度的动画容器必须允许内容溢出或自适应,不得裁切文字。
### 14.9 验收标准
1. **逐条对照**:本节 A1–A12、B1–B20、C1–C12、D1–D26、E1–E11、F1–F18 共 **99 条**动效全部实现;抽测任意 10 条,时长误差 ≤ 1 帧(16.7ms),曲线肉眼无差异。
2. **动画级别**:切到「禁用」后,抽屉/Popup/列表错峰动画不再播放(主界面提醒动画保留);切到「华丽」后行淡入与组件淡入出现;切回「标准」后二者消失。
3. **减少动态效果**:系统开启 `prefers-reduced-motion` 后,全部动画在 1 帧内完成,页面不出现闪烁或「终态先行」;加载指示器仍在转(放慢至 1.6s)。
4. **打断测试**:连续快速开关同一抽屉/Popup/遮罩 10 次,无卡死、无重影、无「停在半路」;遮罩快速 in→out 切换后元素状态正确复位。
5. **性能**:动画期间主线程帧耗时 P95 < 8ms、掉帧 < 2 帧/秒;岛弹簧动画在独立渲染进程内不掉帧(对照 ClassHelper 的实现位置)。
6. **填充模式**:所有带延迟的动画在延迟期间不显示终态(检查 D12、D18、B2、C6)。
7. **曲线一致性**:`--ci-ease-*` 只在一处定义;`grep` 组件样式不得出现 `cubic-bezier(` 字面量(动画表定义处除外)。
8. **层叠与裁剪**:遮罩动画期间文字不被裁切、圆角外的像素不泄漏;分体主界面下每行动画独立,不出现整排一起抖动。
9. **焦点与命中**:动画期间点击穿透状态正确(非交互态不拦截点击,交互态可点);窗口淡出后不残留不可见但可点击的层。
10. **可复现**:同一操作重复 20 次,动画起始/结束时刻的视觉状态一致,无累积误差(尤其是 A5/A6 的行宽与 E1 的滚动偏移)。
---
## 15. 资源清单(图标 / 字体 / 音效 / 天气图标 / 教程 / 本地化)
本节要求实现者**按清单准备并放置资源**,不得缺项、不得自行替换为网络图片。所有路径均以 ClassIsland 仓库为参照;实现时统一放到 `packages/desktop-client/src/renderer/assets/`(渲染进程资源)与 `packages/desktop-client/resources/`(打包资源)两处,并通过构建脚本生成多尺寸产物(见 15.9)。
### 15.1 应用图标
#### 15.1.1 主图标(多尺寸 PNG)
| 文件 | 尺寸 | 用途 | 证据 |
|---|---|---|---|
| `Assets/AppLogo.png` | 192×192 | 应用内窗口图标、关于页、欢迎页 Logo | `ClassIsland/Assets/AppLogo.png`;`App.axaml:29-31`;`WelcomePage.axaml:29-33` |
| `Assets/AppLogo@48w.png` | 48×48 | 小尺寸(任务栏/列表) | 实测文件头 |
| `Assets/AppLogo@384w.png` | 384×384 | 中尺寸(设置页/关于页展示) | 实测文件头 |
| `Assets/AppLogo@768w.png` | 768×768 | 大尺寸(高分屏) | 实测文件头 |
| `Assets/AppLogo_Fade.png` | 192×193 | 淡入用的半透明 Logo(启动/遮罩) | 实测文件头 |
| `Assets/AppLogo_Monochrome.png` | 2715×2003 | 单色 Logo(水印/背景纹样) | 实测文件头 |
| `Assets/AppLogo_AppLogo.svg` | 矢量 | 可缩放图标源 | 文件名 |
| `Assets/AppLogo.ico` | 多尺寸 ICO:16/24/32/48/64/256×2 | Windows 可执行文件图标 | 解析 ICO 头得 7 个条目 |
| `Assets/FreedesktopIcons/AppLogo@128w.png` | 128×128 | Linux Freedesktop 图标 | 实测文件头 |
| `AppLogo@32w.png`(`ClassIsland.Desktop/Assets/`) | 32×32 | Windows 任务栏/托盘小图标 | 实测文件头 |
#### 15.1.2 macOS 图标集(`ClassIsland.Desktop/Assets/AppLogo.xcassets/AppIcon.appiconset/`)
必须提供:16×16(1x/2x)、32×32(1x/2x)、128×128(1x/2x)、256×256(1x/2x)、512×512(1x/2x)、以及无后缀的 192/384/768 三档。实测目录内共 16 个 PNG,命名遵循 `AppLogo_<尺寸>_<倍率>.png` 与 `AppLogo@<宽>w.png` 两种规则,实现时按 `electron-builder` 的 `icon.icns` 要求重新生成即可,但**必须保留这两个命名族**以免资源引用失效。
#### 15.1.3 Windows 打包瓦片(`ClassIsland.Packaging/Images/`)
| 资源 | 尺寸(100%/200%/400%) | 用途 |
|---|---|---|
| `Square44x44Logo` | 44 / 88 / 176 | 开始菜单小图标 |
| `Square44x44Logo.targetsize-16/24/48/256` | 16/24/48/256 | 任务栏/Alt-Tab 变体 |
| `Square150x150Logo` | 150 / 300 / 600 | 开始菜单中等图标 |
| `Wide310x150Logo` | 310×150 / 620×300 / 1240×600 | 宽幅磁贴 |
| `LargeTile` | 310 / 620 / 1240 | 大磁贴 |
| `SmallTile` | 71 / 142 / 284 | 小磁贴 |
| `StoreLogo` | 50 / 100 / 200 | 商店图标 |
| `SplashScreen` | 620×300 / 1240×600 / 2480×1200 | 系统级启动图(与自绘闪屏无关) |
| `LockScreenLogo` | 48×48 | 锁屏 |
(以上均为实测文件头尺寸;`ClassIsland/Images/` 下另有一份 `.scale-200` 版本,实现时只保留打包用的那份即可。)
#### 15.1.4 其他图标
| 资源 | 尺寸 | 用途 |
|---|---|---|
| `Assets/Marketplace/Logo.png` | 512×512 | 插件/主题市场 Logo |
| `Assets/Marketplace/Logo@256w/128w/64w.png` | 256/128/64 | 市场图标多尺寸 |
| `Assets/Marketplace/Logo.svg` | 矢量 | 市场矢量源 |
| `ClassIsland.ExamplePlugin/icon.png` | 510×510 | 示例插件图标(插件必须自带 510×510 图标) |
| `ClassIsland.Launcher/Assets/AppLogo.ico` | 18 条目 ICO(16–256) | 启动器图标 |
验收要求:所有图标必须**本地打包**、离线可用;PNG 必须由同一矢量源派生,不得出现「不同尺寸风格不一致」;ICO 必须内嵌 16/24/32/48/64/256 六档。
### 15.2 字体
#### 15.2.1 正文/界面字体:HarmonyOS Sans SC
| 文件 | 字重 | 用途 | 证据 |
|---|---|---|---|
| `Assets/Fonts/HarmonyOS_Sans_SC_Light.ttf` | 300 | 轻量标题/水印 | 文件名 |
| `Assets/Fonts/HarmonyOS_Sans_SC_Regular.ttf` | 400 | 正文 | 文件名 |
| `Assets/Fonts/HarmonyOS_Sans_SC_Medium.ttf` | 500 | 默认字重(岛内主文字) | `Settings.cs:101` |
| `Assets/Fonts/HarmonyOS_Sans_SC_Bold.ttf` | 700 | 标题/强调 | 文件名 |
- 字体族注册名:`HarmonyOS Sans SC`;全局资源键 `AppFont`,字体栈见 part 03 §3.2.1(`Assets/Fonts/Fonts.axaml:3`;`Assets/Fonts/Fonts.xaml:3`)。
- **许可**:`Assets/Fonts/LICENSE.txt` 为「HarmonyOS Sans Fonts License Agreement」,版权归华为;分发时必须**原样保留**该许可文件,且不得单独售卖字体。应用本体为 GPL-3.0(`LICENSE.txt`),字体许可与代码许可分离。
- 实现要求:把 4 个 ttf 放进 Electron 打包资源,`@font-face` 声明 4 个 `font-weight`,`font-display: swap`;**禁止**声明 `font-display: block`(会白屏到字体加载完)。
#### 15.2.2 图标字体(三套,必须全部内置)
| 字体 | 文件 | 大小 | 附带映射 | 用途 | 证据 |
|---|---|---|---|---|---|
| Fluent System Icons | `ClassIsland.Core/Assets/Fonts/FluentSystemIcons-Resizable.ttf` | 1,472,748 B | `FluentSystemIcons-Resizable.json`(262,830 B) | 全局 UI 图标(Segoe Fluent 风格) | `AppBase.cs:142`;`ThemeBase.axaml:46` |
| Lucide | `ClassIsland.Core/Assets/Fonts/lucide.ttf` | 715,192 B | `lucide.json`(45,030 B) | 天气图标(默认)、部分 UI 图标 | `AppBase.cs:147`;`ThemeBase.axaml:47` |
| SF Symbols | `ClassIsland.Core/Assets/Fonts/SF-Symbols.ttf` | 174,616 B | — | 天气图标(Apple 风格) | `AppBase.cs:152` |
- 字体族名分别为 `FluentSystemIcons-Resizable`、`lucide`、`SF-Symbols`;Avalonia 资源 URI 形如 `avares://ClassIsland.Core/Assets/Fonts/#lucide`,Web 实现改为 `@font-face { font-family:'lucide'; src:url('lucide.ttf') }` 并保留同名 `font-family`。
- JSON 映射文件必须作为图标选择器数据源(Codepoint/名称表),不得只带 ttf 不带 json——图标选择器需要按名称搜索。
- 许可:应用整体 GPL-3.0;Fluent System Icons 与 Lucide 通常为 MIT/ISC【推断:仓库内未放独立许可文件,按上游项目许可执行】;SF Symbols 字体来自 Apple 符号集,**仅允许在 Apple 平台或已获授权场景使用**,跨平台分发时须评估并在「关于/许可」页声明。
#### 15.2.3 字体相关的硬性要求
1. 字体必须在应用启动**首帧前**注册完成(`document.fonts.ready` 前不得渲染最终布局,可用 `font-display: swap` + 骨架屏规避跳动)。
2. 不得使用系统缺失字体作为唯一来源;`HarmonyOS Sans SC` 必须随包分发。
3. 用户自定义字体(`MainWindowFont`)只允许从**系统已安装字体**中选择(源码 `AppearanceSettingsPage.axaml:409-446` 枚举系统字体),实现时用 `queryLocalFonts()`(需权限)或主进程枚举字体列表,二者都不可用时至少提供内置 4 字重 + 3 图标字体。
4. 字体文件不得被 CSP 阻止:`font-src 'self'` 必须放行本地资源。
### 15.3 音效
| 资源 | 大小 | 用途 | 默认设置 | 证据 |
|---|---|---|---|---|
| `Assets/Media/Notification/1.wav` | 130,012 B | 提醒音效(唯一内置音效) | `IsNotificationSoundEnabled=true`;`NotificationSoundPath=""`(表示使用内置音效);`NotificationSoundVolume=1.0` | 实测文件;`Settings.cs:144-145,1584-1593` |
| 自定义音效 | 用户文件 | 用户可指定本地音频路径 | 支持 wav/mp3 等系统可解码格式【推断】 | `Settings.cs:1496-1505` |
| 语音合成 | 系统/Edge TTS | 提醒朗读 | `IsSpeechEnabled=true`;`SpeechVolume=1.0`;`SpeechSource=0`;Edge TTS 默认音色 `zh-CN-XiaoxiaoNeural` | `Settings.cs:135-139,1406-1428,1452-1461` |
实现要求:
- 音频必须用 `HTMLAudioElement` 或 Web Audio 播放,**不得**在主进程用系统播放器;播放前需用户手势解锁(Electron 可用 `webPreferences.autoplayPolicy: 'no-user-gesture-required'`,但必须在配置中显式声明)。
- 音量 = `NotificationSoundVolume × 系统音量`,范围 0–1;语音音量独立(`SpeechVolume`)。
- 同一条提醒**只播一次**音效(源码用 `MaskSession.HasSoundsPlayed` 去重,`MainWindowLine.cs:746-751`);提醒队列切换时不得重复触发。
- 语音提供方必须可插拔:系统语音(Windows SAPI)、Edge TTS、GPT-SoVITS 三选一(`App.Services.xaml.cs:281-285` 注册三个 provider),默认走系统语音或 Edge TTS,且必须支持「无网络时降级到系统语音」。
- 内置音效 1.wav 必须打包进 `resources/audio/`,不得 CDN 引用。
### 15.4 天气图标(4 套模板)
#### 15.4.1 模板注册表
| 模板 id | 显示名 | 默认 | 实现方式 | 尺寸参数 | 证据 |
|---|---|---|---|---|---|
| `classisland.weatherIcons.lucide` | Lucide(默认) | ✅ `WeatherIconId="classisland.weatherIcons.lucide"` | `Viewbox` + `LucideIcon` | 16×16,FontSize 16,Margin 1 | `App.Services.xaml.cs:287`;`Settings.cs:2106`;`LucideWeatherIconTemplate.axaml` |
| `classisland.weatherIcons.fluentDesign` | Fluent Design | — | `Viewbox` + `FluentIcon` | 20×20,FontSize 24,Margin `0 1.2 0 -1.2` | `App.Services.xaml.cs:288`;`FluentDesignWeatherIconTemplate.axaml` |
| `classisland.weatherIcons.sfSymbols` | SF Symbols | — | `Viewbox` + `SFSymbolsIcon` | 16×16 | `App.Services.xaml.cs:289`;`SFSymbolsWeatherIconTemplate.axaml` |
| `classisland.weatherIcons.simpleText` | 纯文本 | — | 文本 + 分隔符 `~` | 文本随字号 | `App.Services.xaml.cs:290`;`SimpleTextWeatherIconTemplate.axaml` |
实现要求:模板必须可插拔(插件可注册新模板),模板注册信息至少包含 `{id, name, renderer}`;切换模板立即生效且不需要重启(源码 `WeatherService.LoadWeatherIconTemplate()` 在设置变化时重新取模板,`Services/WeatherService.cs:84-90`)。
#### 15.4.2 天气代码 → 名称 → 图标(必须完整映射,共 40 个代码)
数据源:小米天气状态表 `Assets/XiaomiWeather/xiaomi_weather_status.json`;图标映射:`ClassIsland/Converters/WeatherConverters.cs`。下表前端三列(Fluent/Lucide/SF)为源码私有区码位,后端实现时请**原样保留码位**,Web 端用 `data-icon` 名称 + 本地字体渲染。
| code | 天气名 | Fluent 码位 | Lucide 码位 | SF 码位 |
|---|---|---|---|---|
| 0 | 晴 | `\uf465` | `\ue2b1` | `\uFE42` |
| 1 | 多云 | `\uf44f` | `\ue216` | `\uFDC9` |
| 2 | 阴 | `\uf43d` | `\ue217` | `\uFDF1` |
| 3 | 阵雨 | `\uf455` | `\ue2fb` | `\uFDE9` |
| 4 | 雷阵雨 | `\uf46b` | `\ue090` | `\uFDCD` |
| 5 | 雷阵雨并伴有冰雹 | `\uf46b` | `\ue08f` | `\uFDDD` |
| 6 | 雨夹雪 | `\uf459` | `\ue094` | `\uFDD4` |
| 7 | 小雨 | `\uf43f` | `\ue08e` | `\uFDED` |
| 8 | 中雨 | `\uf453` | `\ue092` | `\uFDE9` |
| 9 | 大雨 | `\uf453` | `\ue092` | `\uFDE5` |
| 10 | 暴雨 | `\uf453` | `\ue093` | `\uFDE4` |
| 11 | 大暴雨 | `\uf453` | `\ue093` | `\uFDE4` |
| 12 | 特大暴雨 | `\uf453` | `\ue093` | `\uFDE4` |
| 13 | 阵雪 | `\uf45d` | `\ue376` | `\uFDD9` |
| 14 | 小雪 | `\uf45b` | `\ue094` | `\uFDD9` |
| 15 | 中雪 | `\uf45b` | `\ue094` | `\uFDD9` |
| 16 | 大雪 | `\uf45b` | `\ue094` | `\uFDD9` |
| 17 | 暴雪 | `\uf45b` | `\ue094` | `\uFDD9` |
| 18 | 雾 | `\uf443` | `\ue214` | `\uFDE1` |
| 19 | 冻雨 | `\uf461` | `\ue08f` | `\uFDD5` |
| 20 | 沙尘暴 | `\uf441` | `\ue218` | `\uFDB1` |
| 21 | 小雨-中雨 | `\uf453` | `\ue092` | `\uFDED` |
| 22 | 中雨-大雨 | `\uf453` | `\ue092` | `\uFDE9` |
| 23 | 大雨-暴雨 | `\uf453` | `\ue093` | `\uFDE5` |
| 24 | 暴雨-大暴雨 | `\uf453` | `\ue093` | `\uFDE5` |
| 25 | 大暴雨-特大暴雨 | `\uf453` | `\ue093` | `\uFDE5` |
| 26 | 小雪-中雪 | `\uf45b` | `\ue094` | `\uFDD9` |
| 27 | 中雪-大雪 | `\uf45b` | `\ue094` | `\uFDD9` |
| 28 | 大雪-暴雪 | `\uf45b` | `\ue094` | `\uFDD9` |
| 29 | 浮尘 | `\uf449` | `\ue0f4` | `\uFE2E` |
| 30 | 扬沙 | `\uf441` | `\ue0f4` | `\uFE2E` |
| 31 | 强沙尘暴 | `\uf441` | `\ue218` | `\uFE2E` |
| 32 | 飑 | `\uf463` | `\ue093` | `\uFDAD` |
| 33 | 龙卷风 | `\uf463` | `\ue218` | `\uFDA3` |
| 34 | 弱高吹雪 | `\uf45b` | `\ue094` | `\uFDD9` |
| 35 | 轻雾 | `\uf443` | `\ue214` | `\uFDE1` |
| 53 | 霾 | `\uf449` | `\ue0f4` | `\uFDE1` |
| 301 | 雨 | `\uf453` | `\ue092` | `\uFDE9` |
| 302 | 雪 | `\uf45b` | `\ue094` | `\uFDD9` |
| 99 及未知 | 未知 | `\uee31` | `\ue091` | `\uFD8C` |
实现硬要求:
- 未知代码必须回落到「未知」图标,**不得**抛异常或显示空白。
- 纯文本模板必须实现「今天~明天」的 `~` 分隔符逻辑(模板内 `Separator` 仅在 `To` 场景可见,`SimpleTextWeatherIconTemplate.axaml`),且文本颜色跟随 `--ci-text-*`。
- 天气图标颜色:晴天/温度等常规图标用前景色;雨天图标可选用 `--ci-weather-rain`(亮 `#0000FF` / 暗 `#00BFFF`),见 part 03。
### 15.5 贴纸资源(HoYoStickers)
`Assets/HoYoStickers/` 内共 **22** 个 PNG,按「角色_动作」命名,用于教程/提醒的图标表达式 `sticker(名字)`(`App.axaml.cs:253` 注册 `IconExpressionHelper` 的 `sticker` 处理器)。实现时必须支持 `sticker(帕姆_嗨)` 这种写法并渲染对应 PNG。
| 角色 | 尺寸 | 文件(动作) |
|---|---|---|
| 光辉矢愿 | 162×162(6 个) | `不可以看`、`回眸`、`小喇叭`、`想我了吗`、`瞄准`、`遨游` |
| 帕姆 | 510×510(5 个)/450×450(1 个) | `不可以`、`哭哭`、`嗨`(450)、`注意`、`点赞`、`震惊` |
| 昔涟 | 510×510(4 个) | `回眸`、`守护`、`收到`、`爱` |
| 白厄 | 510×510(6 个) | `再见`、`我吗`、`战斗`、`掉线`、`没事` |
实现要求:贴纸必须按**原始比例**缩放(`object-fit: contain`),不得拉伸;建议在 UI 中统一缩放到 96×96 或 128×128,并保留 `image-rendering: -webkit-optimize-contrast` 以求清晰。图标表达式解析器必须支持 `fluent(码位)`、`lucide(码位)`、`sfsymbols(码位)`、`bitmap(路径)`、`sticker(名字)` 五种前缀(`App.axaml.cs:250-260`),未知前缀回落为文本。
### 15.6 教程资源
#### 15.6.1 教程定义文件(JSON)
内置教程组:`Assets/Tutorials/classisland.getStarted.json`(另有 `classisland.sp.json`、`classisland.test.json` 仅作开发样例,**不要**在正式版启用)。数据结构(逐字段实现):
| 层级 | 字段 | 类型 | 说明 |
|---|---|---|---|
| 顶层 | `Id` | string | 教程组 id,如 `classisland.getStarted` |
| 顶层 | `Title` / `Description` | string | 组标题/描述(中文) |
| 顶层 | `Tutorials` | array | 教程条目列表 |
| 教程 | `Id` / `Title` / `Description` | string | 教程 id/标题/描述 |
| 教程 | `ActualBanner` / `Banner` | string | Banner 资源 URI(`avares://`) |
| 教程 | `Ordinary` | int | 是否为普通教程(0/1) |
| 教程 | `Paragraphs` | array | 段落列表 |
| 段落 | `Id` / `Title` / `Description` | string | 段落标识/标题/描述 |
| 段落 | `TopLevelClassName` | string | 目标窗口类名(如 `ClassIsland.MainWindow`) |
| 段落 | `IsBarrierEnabled` | bool | 是否启用教程遮罩屏障 |
| 段落 | `InitializeActions` | array | 进入段落时执行的行动 |
| 段落 | `Content` | array | 每屏内容 |
| 内容 | `Title` / `Content` | string | 标题/正文(中文) |
| 内容 | `IconExpression` | string | 图标表达式(如 `sticker(帕姆_嗨)`) |
| 内容 | `HeroImage` | string | 大图 URI(可空) |
| 内容 | `TargetSelector` / `PointToTarget` / `ModalTarget` | string/bool | 指向的目标与是否模态 |
| 内容 | `RightButtonText` / `LeftButtonText` | string | 按钮文案(可空) |
| 内容 | `RightButtonActions` / `LeftButtonActions` / `InitializeActions` | array | 行动列表,元素为 `{Kind:int, StringParameter:string}` |
| 内容 | `WaitForNextCommand` / `UseLightDismiss` | bool | 交互策略 |
| 内容 | `Tag` / `Script` / `Xmlns` | string/object | 扩展位 |
(来源:`Assets/Tutorials/classisland.getStarted.json:1-200+`。行动 `Kind` 的具体枚举含义在源码 `TutorialActionKind` 中定义,实现时必须对照该枚举逐值映射,不得自行编号;本节不臆造枚举值。)
#### 15.6.2 教程图片
| 组/条目 | 文件 | 大小 |
|---|---|---|
| welcome | `banner.webp` | 42,664 B |
| welcome | `mainMenu.webp` / `openAppSettings.webp` / `openEditMode.webp` / `openProfileEditor.webp` / `openTutorial.webp` | 约 25–27 KB/张 |
| componentsEditing | `banner.webp` / `ruleset.webp` | 28,292 / 48,738 B |
| profileEditing | `banner.webp` / `时间表.webp` / `科目.webp` / `课表.webp` / `课表与时间表关系.webp` | 25,900–109,912 B |
另有默认图:`Assets/DefaultTutorialBanner.webp`(10,416 B)、`Assets/DefaultTutorialGroupBanner.webp`(46,540 B),用作无 Banner 时的兜底。
要求:图片统一用 WebP(保留 `.webp` 扩展名),懒加载;教程播放时按窗口宽度自适应,不得变形。
### 15.7 本地化
#### 15.7.1 现状(必须如实实现,不得虚构更多语言)
ClassIsland v2 的本地化是**局部**的,只覆盖两个命名空间(`Assets/Localization/`):
| 命名空间 | 资源文件 | 语言 |
|---|---|---|
| `SettingsPage`(设置页) | `Localization.SettingsPage.resx`(默认/中文)、`.zh-Hans.resx`、`.zh-Hant.resx`、`.en.resx` | 简体中文、繁体中文、英文 |
| `SettingsPage/General`(常规设置子页) | 同上 4 个 | 同上 |
| `TrayMenu`(托盘菜单) | 同上 4 个 | 同上 |
- 默认语言为中文(`resx` 无文化后缀文件即默认),并强制 `Thread.CurrentThread.CurrentUICulture = zh-CN`(`ClassIsland/App.axaml.cs:512-513`)。
- 其余界面文案**硬编码中文**,没有资源化。因此「支持多语言」不是现状能力。
#### 15.7.2 实现要求(在 Web 栈中的等价方案)
1. 必须建立 `i18n` 层(推荐 `vue-i18n`),语言键命名与 resx 的 `Key` 保持一致,便于数据迁移。
2. 首期必须提供 **zh-Hans(默认)、zh-Hant、en** 三份文案;其余界面文案随实现逐步资源化,**不得**伪造「已支持 N 种语言」。
3. 语言切换必须持久化到配置,并立即刷新界面(无需重启);日期/时间格式必须跟随语言(`Intl.DateTimeFormat`)。
4. 本地化文件不得包含 HTML 标签;带占位符的文案必须使用命名参数(`{name}`),禁止字符串拼接。
5. 字体必须覆盖三语:英文用 HarmonyOS Sans SC 的拉丁字形即可;繁体中文若依赖系统字体兜底,必须在 fallback 栈中显式列出(`PingFang TC`、`Microsoft JhengHei`)。
### 15.8 其他资源与文档
| 资源 | 用途 | 要求 | 证据 |
|---|---|---|---|
| `Assets/Documents/ChangeLog.md` | 更新日志(关于页展示) | 必须随版本更新,按版本倒序 | 文件名 |
| `Assets/Documents/Privacy_.md` | 隐私政策(关于页展示) | 必须原样保留;含 Sentry/上报说明 | 文件名 |
| `LICENSE.txt`(根、各项目、Assets) | GPL-3.0 许可证 | 必须随安装包分发 | `LICENSE.txt` |
| `Assets/TrustedPublicKeys/ClassIsland.MetadataPublisher.asc` | 更新/市场元数据签名公钥 | 必须内置,用于校验更新包 | 文件名 |
| `Assets/ShortcutTemplates/ClassSwap.url`、`cn.classisland.app.desktop` | 换课/桌面快捷方式模板 | Windows `.url` 与 Linux `.desktop` 两套 | 文件名 |
| `Assets/Highlighting/Lua.xshd` | 自动化脚本 Lua 语法高亮定义 | 编辑器必须使用该配色 | 文件名 |
| `Assets/default-subjects.json` | 默认科目表(23 个科目,含固定 GUID:语文/数学/英语/历史/政治/物理/化学/生物/地理/信息技术/体育/自习/通用技术/音乐/美术/选修课/社团/心理/早读/班会/周测 等) | 必须原样内置,GUID 不得重新生成(迁移依赖) | `Assets/default-subjects.json` |
| `Assets/AsciiLogo.txt` | ASCII 艺术 Logo(控制台/崩溃页) | 6 行,宽 63 字符 | 文件内容 |
| `Assets/Tellings.txt` | 关于页/加载页随机「一句话」(含彩蛋) | 至少 20 条,需保留「关于页连续点击图标 10 次开调试」等提示 | 文件内容 |
| `Assets/dependencies.g.json` | NuGet 依赖许可清单(供「许可」页展示) | Web 栈需生成 `licenses.json` 等价物(npm 依赖 + 字体许可) | `Models/NugetLicenseInfo.cs`、`Views/WelcomePages/LicensePage.axaml` |
| `Assets/XiaomiWeather/xiaomi_weather_status.json` | 天气代码→中文名映射(40 条) | 与 15.4.2 表一致 | 文件内容 |
| `Assets/BannerMain_App2.png` | 2031×600 | 关于页/更新页主视觉 | 实测文件头 |
| `Assets/Demo_MainMenu.png`、`Demo_OpenProfileSettingsWindow.png` | 1608×878 | 教程/引导演示图 | 实测文件头 |
| `Assets/ClassIsland.ai` | 矢量源文件 | 设计源文件,可不打包 | 文件名 |
| `Assets/XamlThemePreviews/classisland.fluent.png`、`classisland.classic.png` | 1920×350 | 主题市场预览图(见 part 09) | 实测文件头 |
### 15.9 ClassHelper 侧资源落点
| 资源类别 | 落点 | 生成/校验方式 |
|---|---|---|
| 应用图标 | `build/classhelper.png`(源)+ 设计导出的 `.ico` | 用 `pnpm icons` 派生其余尺寸(与 ClassHelper 现有约定一致:图标源是 `build/classhelper.png` 与设计导出的 `.ico`,`pnpm icons` 派生其余资源) |
| Web 品牌图 | `packages/web-admin/public/logo.png` 或站点 `/logo.png` | Web 用 `/logo.png`,客户端用 `BRAND_LOGO_URL` 常量(避免 `file://` 绝对路径问题) |
| 字体 | `packages/desktop-client/resources/fonts/` | 4 个 HarmonyOS TTF + 3 个图标 TTF + 2 个 JSON 映射;构建时校验文件存在与哈希 |
| 音效 | `packages/desktop-client/resources/audio/1.wav` | 校验大小 130,012 B(或替换为等价资源但保持单文件、可循环) |
| 贴纸 | `packages/desktop-client/resources/stickers/` | 22 个 PNG,文件名保持中文原名(打包后可用 URL 编码访问) |
| 教程 | `packages/desktop-client/resources/tutorials/` | JSON + WebP;启动时校验 id 唯一 |
| 主题预览 | `packages/desktop-client/resources/theme-previews/` | 1920×350 PNG 两张 |
| 许可清单 | `packages/desktop-client/resources/licenses.json` | 构建时从 `package.json`/`node_modules` 生成;必须包含字体与图标字体的许可条目 |
### 15.10 资源命名、加载与缓存规范
1. **命名**:图片用 `kebab-case` 或保持中文原名(贴纸、教程图必须保留原名,因为教程 JSON 以名字引用);字体文件名保持上游原名(`HarmonyOS_Sans_SC_Medium.ttf`),不得改成 `medium.ttf` 之类丢失来源信息的名字;音效用 `1.wav` 这类稳定短名,便于设置里持久化路径。
2. **目录约定**:`renderer/assets/{icons,fonts,audio,stickers,tutorials,theme-previews}/`,每类一个目录;禁止在组件目录里散落资源文件。
3. **加载方式**:
- 图标字体、正文 TTF 在应用启动时通过 `@font-face` 预加载(`crossorigin` 同源),并用 `document.fonts.load()` 显式触发;
- 贴纸/教程图/主题预览图走懒加载(`loading="lazy"` + IntersectionObserver),未加载时显示 `Shimmer` 骨架(part 14 D9);
- 音效在首次提醒时按需加载并缓存 `AudioBuffer`,不得每次播放都重新 `fetch`。
4. **缓存**:所有资源 URL 必须带内容哈希(构建期生成 `asset-manifest.json`),Electron 生产环境用 `file://` + 哈希避免陈旧缓存;开发环境禁用强缓存。
5. **失败降级**:图片加载失败必须显示「图裂了」占位(ClassIsland 的行为:`SystemFillColorCriticalBackgroundBrush` 底 + 提示「喔唷,图裂啦」,`StylesBase.axaml:214-223`),不得留白或抛错;字体加载失败必须回落系统字体并保持布局不跳变(用 `font-display: swap` + 固定行高)。
6. **安全**:禁止加载 `http(s)` 远程图片作为 UI 资源;所有资源必须走打包路径,CSP 中 `img-src 'self' data:`、`font-src 'self'`、`media-src 'self'`。
7. **引用完整性**:构建时扫描 CSS/TS/JSON 中的资源引用,所有引用必须命中 `asset-manifest.json`;反向扫描资源目录,未被引用的资源要么删除要么登记为「运行时动态加载」白名单。
### 15.11 资源版本与许可核对
- 「关于」页必须提供「许可」入口,展示:应用许可(GPL-3.0)、字体许可(HarmonyOS Sans License)、图标字体许可(Fluent/Lucide/SF)、贴纸素材来源、第三方依赖(npm 依赖清单)。ClassIsland 用生成文件 `dependencies.g.json` + `LicensePage.axaml` 展示 NuGet 依赖,Web 实现必须生成等价的 `licenses.json`。
- 每次发版必须核对:新增/替换的资源是否引入新的许可义务;贴纸、Banner、Demo 图是否为项目自有或有授权(HoYoStickers 为《崩坏:星穹铁道》同人素材,**仅可作为内置彩蛋、不得用于商业宣传**)。
- 版本号变更时必须同步「关于」页的 `AppVersionLong` 展示(源码用编译期常量 `AppBase.AppVersionLong`,Web 端改为读 `package.json.version`)。
补充说明:ClassIsland 的「关于」页还会展示更新日志、隐私政策与致谢名单,这些内容都是随包分发的 Markdown 文档。实现时不得改为在线拉取——教室环境常常断网,关于页必须在离线时完整可读。更新日志与隐私政策的内容以 `Assets/Documents/` 下的文件为准,发版时同步更新,不允许只改版本号不改日志。
### 15.12 验收标准
1. **完整性**:本节列出的资源 100% 存在于仓库;用脚本比对清单与文件,缺一项即失败(允许同一资源用等价格式替换,但必须在清单中登记变更)。
2. **图标**:`electron-builder` 打包后 Windows exe/installer、Linux AppImage、macOS icns 三端图标均正确;ICO 内嵌 6 档尺寸;任务栏/开始菜单/关于页三处图标一致。
3. **字体**:断网启动,界面仍显示 HarmonyOS Sans SC(非回退字体);图标字体全部可渲染(抽查 Fluent/Lucide/SF 各 3 个码位);`document.fonts.check()` 对 4 个正文字重与 3 个图标族返回 true。
4. **字体许可**:安装包内包含 `fonts/LICENSE.txt`(HarmonyOS 许可)与 `licenses.json`;「关于 → 许可」页可查看并可滚动到底。
5. **音效**:默认配置下提醒有声音;把音量调到 0 静音;自定义音效路径失效时回落内置 1.wav 且不崩溃;同一条提醒只响一次。
6. **天气图标**:4 套模板均可切换且立即生效;40 个天气代码逐个渲染正确(截图对比名称与图标);未知代码 99 显示「未知」图标。
7. **贴纸**:`sticker(帕姆_嗨)` 在教程中正确显示;22 张贴纸全部可加载(无 404);比例不变形。
8. **教程**:`classisland.getStarted` 三个教程组可完整播放到结束;跳过后再次进入从第一步开始;教程图片无拉伸、无破图。
9. **本地化**:切换 en/zh-Hans/zh-Hant 后设置页与托盘菜单文案切换且立即生效;日期格式随之变化;其余页面若无翻译必须回落中文而不是显示 key。
10. **离线**:在完全断网、无缓存的干净环境中启动,所有资源(字体/图标/音效/贴纸/教程图)均本地可用,控制台无对外请求(除用户主动触发的天气/更新功能)。
11. **体积**:字体 + 图标 + 音效 + 贴纸 + 教程图的总增量不超过 30MB 打包后体积;超过时必须说明取舍(例如贴纸按需下载,但 ClassIsland 的语义是离线可用,默认应全量内置)。
---
## 16. 质量门禁与验收清单
> 本部分是硬性验收标准。你必须逐条执行并报告结果;任何一项未通过都不得声称「已完成」。所有自动验证必须在无头/离屏模式下执行,禁止弹出可见窗口、禁止播放声音、禁止操作真实课表数据。
### 16.1 工程与构建门禁
- [ ] `pnpm install --frozen-lockfile` 在 Node 20.19+ 下成功;`pnpm-lock.yaml` 已提交。
- [ ] `pnpm -r typecheck` 0 错误;`pnpm lint` 0 错误(warning 允许但需在报告中列出)。
- [ ] `pnpm build` 产出:主进程 `dist/main`、preload `dist/preload`、两个渲染入口(`index.html`、`island.html`)以及静态资源;`island.html` 与 `index.html` 分别可独立加载。
- [ ] 打包产物:Windows NSIS/portable 安装后能启动;卸载不残留用户配置(或按需保留并提示);macOS/Linux 至少构建成功。
- [ ] 依赖清单固定版本并说明用途;无重复的同类库(例如不得同时使用两个日期库、两个状态库)。
- [ ] 仓库内无 `node_modules`、无构建产物、无日志、无密钥;`.gitignore` 完整。
- [ ] README 包含:环境要求、开发命令、构建命令、打包命令、目录说明、数据目录位置、插件/主题开发入口。
### 16.2 像素级视觉验收(主界面)
在 1920×1080、缩放 100% 的无头离屏截图(`--headless --screenshot` 或 Playwright Electron)下,与下列基准逐项比对(容差:颜色 ±2/255,位置/尺寸 ±1px,透明度 ±1%):
- [ ] 岛默认位置:屏幕中上侧;上边距 = `WindowDockingOffsetY`(默认 0)+ 系统工作区顶部安全区;水平居中误差 0px。
- [ ] 默认行宽:内容自适应(`Width=auto`);编辑模式下 `MinWidth = ActualClientBound.Width`;圆角 8px(`RadiusX=RadiusY=8`,可用设置改)。
- [ ] 背景不透明度 0.5(`Opacity=0.5`);默认深色背景(`BackgroundColor` 默认黑,未启用自定义背景色时使用主题背景);背景模糊/毛玻璃在支持的平台上启用,不支持时降级为半透明纯色且视觉不突变。
- [ ] 行垂直间距 5px(`MainWindowLineVerticalMargin=5`);组件水平外边距 6px(`Margin="6 0"`)。
- [ ] 字号:次要 14、主体 16、强调 18、超大 20(可由设置覆盖);默认字重 Medium(`MainWindowFontWeight2=500`);字体为系统字体回退链。
- [ ] 进度条:位于当前课程行底部,宽度=行宽,高度见 4.4(建议 3px),颜色=强调色;进度精度默认降低(`ReduceProgressAccuracy=true`,即按整秒/整分更新,不逐帧抖动)。
- [ ] 隐藏/显示、编辑模式进入退出、上下文菜单弹出均无白屏、无黑边、无窗口阴影残影。
- [ ] 深色/浅色主题各截一张、Fluent/Classic 主题各截一张、125%/150%/200% 缩放各一张,全部与第 3、4、9 部分的规格一致。
### 16.3 功能行为验收(按模块)
**主界面与窗口**
- [ ] 6 个停靠位置、X/Y 偏移、多显示器索引生效;切换显示器后岛跟随。
- [ ] 不抢焦点:显示/切换页面时,当前前台应用(记事本/浏览器)不失去焦点,Alt+Tab 不出现岛。
- [ ] 任务栏:默认不显示;仅当「窗口模式」或「录屏模式」开启时显示。
- [ ] 鼠标穿透:开启后点击岛不触发任何事件;右键菜单仍可通过托盘打开;触摸设备长按可打开菜单。
- [ ] 隐藏规则:`HideOnClass` 上课隐藏、`HideOnMaxWindow` 前台最大窗口隐藏、`HideOnFullscreen` 全屏隐藏(排除进程名单生效)、高级规则集命中隐藏;任何隐藏后进入编辑模式必须强制显示。
- [ ] 临时显示:通过托盘/右键/自动化「显示主界面」在隐藏期间强制显示,并保持到超时或手动隐藏(行为与第 4.5 节一致)。
- [ ] 关闭再启动:位置/尺寸/缩放/透明度/主题/静默状态全部保持。
- [ ] 单实例:重复启动时旧实例收到信号并显示主界面(或按设置行为),新进程 1s 内退出。
- [ ] 开机自启开关生效(注册表/LaunchAgent/autostart 三平台),关闭后删除启动项。
**组件**
- [ ] 全部内置组件可添加、设置、删除、拖拽排序、嵌套;配置重新加载后视觉与行为一致。
- [ ] 每个组件的默认值、空数据兜底、超长文本截断/滚动、隐藏规则、显示优先级符合第 5 部分。
- [ ] 组件动画(时钟、倒计时、滚动、滑动、天气图标)在 60fps 下无抖动;浏览器 DevTools 无 layout thrash 警告。
- [ ] 组件在编辑模式下可被选中并显示虚线框(1.5px、虚线 1,1.5、圆角描边、Opacity 0.75);退出编辑后完全消失。
**课表/档案**
- [ ] 时间点类型:上课/课间/分割线/行动;时间点可拖拽改时间、可快速录入、可跨天;校验不允许重叠/倒序。
- [ ] 周次轮换:单周、双周、1–9 周自定义轮换、偏移、起始周日期;跨学期/跨年正确。
- [ ] 临时课表:单日、跨天、按日期区间;预定课表到点自动启用、结束自动恢复;临时课表与正式课表优先级符合第 6 部分。
- [ ] 换课:同日/跨日、按节次交换、恢复;影响提醒与当前课程。
- [ ] Excel 导入:支持 xlsx/xls/csv;模板列映射;错误单元格定位到行列;导入前预览;取消不落库。
- [ ] Excel 导出:导出当前档案课表,格式可再导入(往返一致)。
- [ ] CSES JSON 导入:字段映射与失败提示。
- [ ] 多档案:新建/复制/重命名/删除/切换;删除有二次确认与备份;默认档案在无课时兜底。
- [ ] 档案损坏/版本过旧:提示可恢复,不允许静默清空。
**提醒**
- [ ] 队列:高优先级插队、同级先进先出、同 id 去重、最长显示时长、可手动清除。
- [ ] 上课提醒:根据时间表在上课时间点触发;课前 N 秒(默认 60s)触发准备提醒;下课前倒计时(默认 60s)显示;下课提醒;放学提醒。
- [ ] 提醒内容:学科名、教师名、课间名、下一节课程、天气预警、自定义文本;布局与字号符合第 7 部分。
- [ ] 强提醒(Topmost):全屏/大区域遮罩、超时自动关闭、点击/按键关闭、在岛所在的显示器上显示;不抢焦点。
- [ ] 音效:内置音效播放、音量、单次/循环;无音频设备时降级不报错;`-q/--quiet` 静默启动后 10 秒内丢弃提醒(ClassIsland 2.1 无独立「静音时段」,不得虚构该功能;如实现 P2 免打扰需默认关闭并标【推断】)。
- [ ] TTS:系统语音/Edge TTS 可切换、语速/音量/音色、失败回退;不阻塞 UI。
- [ ] 提醒设置:按提供者启用/排序/参数;修改后立即生效。
- [ ] 提醒历史:最近 N 条可查、可清空。
**自动化**
- [ ] 触发器全部可用(时间点/CRON/信号/URI/托盘菜单/窗口状态/当前课程/天气/日出日落)。
- [ ] 行动全部可用(提醒/运行程序/打开文件/打开网页/关机/睡眠/重启/修改设置/广播信号/切换课表/发通知);危险行动有二次确认与超时保护;测试模式禁止真实关机。
- [ ] 规则集:AND/OR/NOT、嵌套、比较运算、空值处理;求值结果与第 8 部分一致。
- [ ] 执行日志:时间、触发源、行动、结果、耗时;失败重试有上限;不会无限循环。
- [ ] 自动化关闭时不执行任何行动。
**设置与窗口**
- [ ] 每个设置页与控件、默认值、枚举项与第 10 部分清单一致;搜索可命中设置标题/关键词。
- [ ] 设置即时生效(防抖 300ms 内);需要重启的项明确提示并提供重启按钮。
- [ ] 设置导入/导出、恢复默认、覆盖层(SettingsOverlay)按预期工作。
- [ ] OOBE 可完整走通、可跳过、可从设置再次打开;OOBE 未完成时主窗口不出现或被引导遮罩。
- [ ] 所有次级窗口的尺寸/标题/按钮/关闭行为符合第 11 部分;Esc 关闭;关闭设置不退出应用。
- [ ] 崩溃窗口、恢复模式、更新进度、数据迁移、教程、授权、日志窗口可正常打开/关闭,无内存泄漏(关闭后窗口对象被释放)。
**主题/资源/本地化**
- [ ] 内置主题切换全覆盖、无残留样式、无闪烁;非法主题包被拒绝且不崩溃。
- [ ] 若实现 P2 壁纸取色(ClassIsland 2.1 已停用、默认关闭),取色结果稳定且浅色/深色下前景对比度 ≥ 4.5:1(WCAG AA);色盲安全抽查;未实现时不得在设置页露出可用的取色入口(按源码禁用隐藏)。
- [ ] 图标/字体/音效资源齐全,缺失时使用兜底资源;无 404。
- [ ] 中文文案 100% 覆盖;文案集中在常量文件;ClassIsland 2.1 只有 3 种语言且仅覆盖设置页与托盘菜单(其余硬编码中文),因此本实现交付「完整 zh-CN + 文案集中」,不得宣称全量多语言;若要加 en-US 只能作为 P2 增量且不得遗漏中文兜底。
### 16.4 动画验收
- [ ] 第 14 部分动画总表中的每一项都实现,时长误差 ≤ 16ms,曲线一致(用 DevTools Animations 面板或离屏逐帧截图验证关键帧)。
- [ ] 岛出现/隐藏、行展开/折叠、提醒进出、强提醒、OOBE 转场、主题切换、设置页切换全部流畅;长任务不阻塞动画(用 Performance 面板确认帧率 ≥ 55fps)。
- [ ] `prefers-reduced-motion` / 设置中的「减少动画」(`AnimationLevel=0`)生效:仅保留必要的透明度变化,位移与缩放归零。
- [ ] 动画结束后无残留 `will-change`、无未清理的 `requestAnimationFrame`/定时器。
- [ ] 弹簧动画参数与 `spring.ts` 一致(stiffness 0.10、damping 0.68、snap 0.05px、换向保留 35%),不得用 CSS 过渡近似替代(行高/尺寸变化场景)。
### 16.5 数据完整性与容错
- [ ] 所有写入原子化(临时文件 + rename);断电/强杀后配置仍可读。
- [ ] 自动备份:默认开启、每 7 天一次、最多保留 16 份(`IsAutoBackupEnabled=true`、`AutoBackupIntervalDays=7`、`AutoBackupLimit=16`),可从备份恢复。
- [ ] 配置版本迁移:旧版本字段兼容,迁移前备份,迁移失败自动回滚并提示。
- [ ] 配置校验:非法 JSON、未知字段、越界数值(如半径 -1、透明度 >1)被修正或拒绝,不崩溃。
- [ ] 崩溃恢复:渲染进程崩溃后自动重建(限流:1 分钟内最多 3 次,超过则进入安全模式);GPU 崩溃降级软件渲染;安全模式下禁用插件/主题并提示。
- [ ] 数据目录可配置;卸载/重置时不误删用户手动备份。
### 16.6 性能与资源
- [ ] 冷启动到岛可见 ≤ 3s(SSD,不联网)。
- [ ] 空闲 CPU 平均 ≤ 1%(岛可见、时钟每秒刷新);内存(主进程+两个渲染进程)≤ 300MB。
- [ ] 连续运行 24h 无内存增长(增长 ≤ 10%)、无句柄泄漏、无崩溃。
- [ ] 提醒密集场景(1 分钟 20 条)不卡顿、不丢提醒、不重复。
- [ ] 打包体积:Windows 安装包 ≤ 150MB(不含可选模型/音色资源)。
- [ ] 首屏构建资源 ≤ 2MB(gzip);岛渲染进程启动 ≤ 500ms。
### 16.7 平台与兼容性
- [ ] Windows 10 21H2+、Windows 11:透明/置顶/点击穿透/多显示器/DPI 125%/150%/200% 正常。
- [ ] macOS 12+:透明窗口、置顶、全屏隐藏、通知权限(如使用系统通知)。
- [ ] Linux X11:透明窗口、置顶、点击穿透;Wayland 明确提示不支持并给出降级说明。
- [ ] 触摸设备:单击/长按/滑动不冲突;编辑模式可拖拽。
- [ ] 屏幕阅读器可读出设置控件;键盘 Tab 顺序合理;所有按钮有 `aria-label` 或可见文本。
### 16.8 安全验收
- [ ] `nodeIntegration: false`、`contextIsolation: true`、`sandbox: true`(或提供等效隔离);preload 仅暴露白名单 API。
- [ ] 所有 IPC 通道校验参数类型与来源窗口;渲染层不能任意读写文件/执行命令。
- [ ] CSP 严格(`default-src 'self'`;`connect-src` 仅允许天气/更新/插件市场域名);无 `unsafe-eval`(Vue 生产构建)。
- [ ] 更新包必须校验哈希/签名;拒绝非 HTTPS 下载(本地镜像可通过设置允许但需明确警告)。
- [ ] 自动化行动执行命令有白名单/确认;日志脱敏(不含完整路径中的用户名/令牌)。
- [ ] 插件默认无 Node 权限;插件权限声明在清单中,安装时展示。
- [ ] 本地数据不上传;崩溃上报默认关闭/需同意(`IsReportingEnabled` 默认 true 但仅上报匿名崩溃,需在隐私页说明)。
### 16.9 交付物清单
- [ ] 源码 + `pnpm-lock.yaml` + 构建脚本。
- [ ] `README.md`、`docs/architecture.md`、`docs/data-schema.md`、`docs/plugin-api.md`、`docs/theme-api.md`。
- [ ] 示例档案(含 3 周轮换)、示例主题、示例插件(P1)。
- [ ] 无头测试:单元测试(时间/课表/规则/配置校验)+ 组件测试(岛渲染快照)+ 端到端测试(启动、开设置、切主题、导入课表)。
- [ ] 验收报告:逐条勾选 16.1–16.9,附截图路径与测试输出摘要;未通过项必须列出原因与修复计划。
---
## 17. 开发里程碑与实现顺序
> 必须按 M0→M8 顺序推进;每个里程碑结束都要能「无头启动 + 通过该阶段验收」。禁止先堆功能再补架构。
### M0 工程脚手架与进程骨架(0.5–1 天)
- 交付:
- pnpm monorepo:`pnpm-workspace.yaml`(`packages/*`)、根 `package.json`(`packageManager: pnpm@11.8.0`)、`tsconfig.base.json`、Prettier/ESLint。
- `packages/desktop-client`:Electron 主进程入口、`island.html` 与 `index.html` 两个 Vite 入口、preload 白名单、单实例锁、托盘、日志、`config.json` 原子读写、默认 `settings.json`。
- `packages/shared`:类型定义、常量、schema、时间工具;`packages/ui`:`tokens.css`、基础控件。
- 验证(无头):`pnpm typecheck`、`pnpm lint`、`pnpm build` 通过;主进程以 `--headless` 启动后**不创建任何可见窗口**,5 秒内输出 `app-ready` 并自动退出;配置文件按 schema 生成且二次启动可读回。
- 完成标准:`config.json` 字段完整、原子写、损坏时自动回退默认值并记录日志;窗口/托盘代码有开关(headless 模式全部跳过)。
### M1 岛窗口 + 时钟引擎 + 基础显示(1–2 天)
- 交付:
- 独立岛 BrowserWindow:透明、无边框、置顶、不抢焦点、默认不显示在任务栏、按 `WindowDockingLocation` 6 个位置停靠、多显示器选择、缩放、DPI 处理、关闭/隐藏动画。
- 岛结构:多行容器(`Lines`)、行背景、连续圆角、背景不透明度、毛玻璃/纯色、进度条、遮罩层宿主。
- 时间引擎:NTP 对时(可关闭)、手动偏移、倍速调试、休眠唤醒后重算。
- 默认组件:日期 + 时钟 + 欢迎文本;右键原生菜单(完整项,见 4.7)。
- 验证:1920×1080@100% 下离屏截图与第 4 部分基准逐项比对;切换 6 个停靠位置、125%/150% 缩放截图无模糊/偏移;岛空闲 CPU < 1%。
- 完成标准:无边框、透明、点击穿透可切换、圆角与阴影正确、菜单可用、时钟跨分钟无跳变。
### M2 组件系统(2–3 天)
- 交付:
- 组件注册表、Component 数据模型(`Id/Name/Type/Settings/Children/IsVisible/HidingRules`)、组件设置面板、组件的添加/删除/拖拽排序/嵌套。
- 内置组件按第 5 部分顺序实现:分隔线 → 文本 → 日期 → 时钟 → 倒计时 → 课程表 → 天气 → 滚动 → 滑动 → 堆叠 → 分组 → 容器。
- 编辑模式:选中框、拖动把手、行设置抽屉、添加组件面板、组件设置弹层。
- 验证:每个组件在编辑模式可增删改、保存后重载不丢失;空数据/超长文本/多语言/深色主题截图验收;拖拽排序有动画且不闪烁。
- 完成标准:组件配置 JSON 与第 13 部分 schema 一致;关闭编辑模式无选中框残留。
### M3 课程表与档案系统(3–4 天)
- 交付:
- 时间表编辑器(时间点增删、上课/课间/分割线/行动、分钟级拖拽、快速录入、跨天)。
- 课表编辑器(周模板、单日覆盖、多周轮换 1–9 周、群组、预定/临时课表、单日/跨天换课)。
- 档案管理(新建/复制/删除/导入/导出/切换、默认档案、档案校验)。
- Excel 导入(模板下载、表头映射、预览、错误定位)、Excel 导出、CSES JSON 导入。
- 课程状态机:当前/下一节、课间、放学、跨天、提前上课/拖堂、时间点行动。
- 验证:构造 3 周轮换课表 + 临时换课 + 跨天场景,无头单元测试断言当前课程/剩余时间/下一节;Excel 往返导入导出数据一致;损坏档案提示可恢复。
- 完成标准:课程计算函数覆盖率 ≥ 90%;所有档案操作有备份。
### M4 提醒系统(2–3 天)
- 交付:
- 提醒队列与优先级、去重、排队/插队、提醒历史、清除全部提醒。
- 内置提醒提供者:上课、下课、课前准备(提前 N 秒)、放学、天气预警、天气预报、管理通知、自定义(行动触发)。
- 提醒 UI:气泡(行内展开/胶囊)、强提醒全屏特效;音效、TTS(系统 TTS/Edge TTS)、音量;-q/--quiet 静默启动后 10 秒内丢弃提醒(无独立静音时段,不得虚构)。
- 提醒设置页与各提供者设置。
- 验证:无头注入假时间,断言 6:00–23:00 全时间点提醒触发一次且仅一次;队列优先级符合第 7 部分;气泡/强提醒动画时长与曲线与第 14 部分一致;关闭音效后不播放。
- 完成标准:重复提醒按 id 去重;上课期间普通提醒按规则暂存/延后;不阻塞主线程。
### M5 设置中心 + OOBE(2–3 天)
- 交付:
- 设置窗口:左侧导航(分组 + 图标 + 搜索)、右侧页面、面包屑/返回、即时保存、重启提示、恢复默认。
- 全部设置页与控件、默认值、禁用条件(见第 10 部分)。
- OOBE(8 页):欢迎 → 许可证 → 翻新/迎新 → 基本 → 颜色 → 外观 → 系统 → 完成;支持跳过与再次打开。
- 验证:设置项与第 10 部分清单逐项对照;改变关键设置(圆角/透明度/字号/停靠/主题)后岛即时生效;重启后保持;搜索命中正确。
- 完成标准:设置写入防抖 ≤ 300ms;连续修改不产生大量磁盘写(合并写入)。
### M6 主题系统(+ 可选壁纸取色 P2)(1–2 天)
- 交付:
- 内置 Fluent/Classic 主题的完整 CSS 变量与组件样式差异。
- 主题包格式(`theme.json` + `theme.css` + `preview.png`)、安装/卸载/启用、冲突处理。
- 自定义背景色/前景色;系统深浅色跟随。
- P2(可选,默认关闭):壁纸取色——屏幕/壁纸采样、主色提取、深浅色适配、调色板候选(ClassIsland 2.1 已停用该功能,只给算法与数据结构,不纳入 P1 验收)。
- 验证:切换主题后主界面/设置/弹窗全部生效且无闪烁;非法主题包不崩溃;若实现 P2 取色,则抽查取色结果与壁纸截图的视觉一致性。
- 完成标准:主题变量覆盖第 3 部分全部 token;主题预览图自动生成或提供占位。
### M7 自动化 + 天气 + 高级窗口(2–3 天)
- 交付:
- 自动化引擎:触发器轮询/事件、行动执行、规则集求值、执行日志、失败重试/限流、危险行动二次确认。
- 内置触发器与行动完整清单(见第 8 部分);规则类型与嵌套。
- 天气服务:定位/城市、当前/逐小时/3 天、预警、降水提醒、缓存与失败回退、图标主题。
- 次级窗口:换课、ClassPlanDetails、Excel 导入/导出向导、崩溃/恢复、更新进度、时间调整、数据迁移、教程、关于、日志、调试。
- 验证:构造自动化场景(时间点触发提醒、窗口全屏时隐藏岛、下雨提醒)无头断言;断网时天气显示缓存并提示;危险行动(关机/睡眠)在测试模式下不得真正执行。
- 完成标准:自动化配置可导出/导入;执行记录可查;异常不会导致应用退出。
### M8 插件、更新、恢复与打磨(2–4 天)
- 交付:
- 插件宿主(P1):清单、权限、启用/禁用、生命周期、扩展点(组件/提醒提供者/触发器/行动/设置页)、示例插件。
- 更新:检查/通道/镜像/下载/哈希校验/静默安装/失败回滚;`UpdateService` 对应 `update.ts`。
- 崩溃守卫与恢复模式:渲染进程崩溃自动重建(限流)、GPU 崩溃降级软件渲染、配置损坏恢复、安全模式。
- 教程系统、文案集中(zh-CN;en-US 为 P2 可选)、无障碍、性能优化、打包脚本、文档、示例主题。
- 验证:打包产物在干净系统安装运行;断网/拔电源模拟崩溃后数据不丢;更新流程灰度与回滚;插件启用后无卡顿。
- 完成标准:全部 P0/P1 验收项通过;安装包体积与内存占用达标(见 16.6)。
### 里程碑总览表
| 里程碑 | 核心风险 | 完成标志 |
|---|---|---|
| M0 | 进程模型/透明窗口/配置原子写 | headless 启动 < 5s,无可见窗口 |
| M1 | 透明置顶窗口跨平台差异、DPI | 6 停靠位置像素级正确 |
| M2 | 组件递归模型与编辑模式 | 组件配置闭环 |
| M3 | 课表状态机/周次/导入 | 单元测试覆盖核心算法 |
| M4 | 提醒去重与音视频调度 | 全时间点场景通过 |
| M5 | 设置项数量庞大 | 设置清单 100% 对照 |
| M6 | 主题变量覆盖与取色 | 切换无残留 |
| M7 | 自动化安全/天气容错 | 危险行动有保护 |
| M8 | 插件沙箱/更新安全 | 打包安装可用 |
---
## 18. 禁止事项与实现边界
> 以下为硬性红线。违反任一项即视为交付不合格。
### 18.1 关于「不打扰」的硬性要求
1. **禁止任何会显示可见窗口或发出声音的自动验证**:自动化测试、冒烟脚本、构建检查必须在 `--headless` / 离屏 / 隐藏窗口模式下运行;不得创建可见的岛、托盘气泡、系统通知、声音。验证脚本必须默认静默,需要人工观察时由显式参数开启(例如 `--visual`),且不得作为 CI 默认。
2. **禁止在开发/构建过程中自动创建桌面快捷方式、开机自启项、全局快捷键**;这些只能由用户在设置中主动开启。
3. **禁止把主窗口做成会抢占焦点或进入任务栏的普通窗口**:默认 `focusable=false`、`skipTaskbar=true`、`show=false` 直到首帧准备好;不得使用 `setAlwaysOnTop` 之外的 API 强制置顶到系统级(如 Windows 的 `SetWindowPos HWND_TOPMOST` 需与 Electron 的置顶设置一致,避免与全屏应用冲突)。
4. **禁止用 `setBackgroundMaterial()`(Electron 的 Windows 材质 API)破坏逐像素透明**;透明背景必须由无边框透明窗口 + CSS 自绘实现,以保证圆角与阴影的正确性。
5. **禁止自动播放声音**:提醒音效仅在提醒触发时播放;调试/测试静音;首次启动不播放。
### 18.2 技术红线
6. **禁止让 Element Plus 的全局默认样式进入岛渲染进程**:`island.html` 不得引入 Element Plus 的全局 CSS;岛使用自研 CSS + 设计 tokens。Element Plus 仅用于设置中心等管理型界面。
7. **禁止在渲染进程开启 Node 集成**;preload 不得暴露 `ipcRenderer`、`require`、`process`、文件系统能力;跨桥只传纯数据(不得传 Vue Proxy/函数/类实例)。
8. **禁止把动画交给 `setInterval`/`setTimeout` 逐帧驱动**(弹簧动画用 rAF + 物理积分;其他用 CSS transition/keyframes/Web Animations)。
9. **禁止用 JS 直接改 `width/height/top/left` 触发同步布局做动画**;优先 `transform/opacity`,尺寸变化用 FLIP 或弹簧 + `ResizeObserver` 批量更新。
10. **禁止使用 `unsafe-eval`、`new Function`、远程加载并执行 JS**;CSP 必须严格;不得通过 `file://` 加载用户可写目录中的可执行脚本。
11. **禁止把核心功能依赖云端**:断网时课表、提醒、自动化、组件、设置、主题必须全部正常;天气/更新/插件市场允许降级。
12. **禁止把敏感信息(令牌、密码、完整用户路径)写入日志或打包进安装包**;`check-bundle-secrets` 式构建门禁必须保留(若实现管理端可选模块)。
13. **禁止硬编码绝对路径**;所有资源使用相对/`app://` 协议;品牌图片通过打包资源 URL 引用。
14. **禁止引入与原技术栈冲突的框架**(如再引入 React/Angular、再引入第二套 UI 组件库做岛);确需引入库时必须说明不可替代性并锁定版本。
### 18.3 数据与安全红线
15. **禁止未备份就迁移/覆盖用户数据**:任何 schema 升级、导入、恢复操作先写备份,失败可回滚;删除档案/主题/插件必须二次确认。
16. **禁止静默删除或重置用户配置**;配置损坏时保留原文件为 `.bak` 并提示。
17. **禁止在测试中使用真实班级数据**;测试必须使用临时目录与夹具数据;测试结束清理临时数据,不得触碰用户真实配置目录。
18. **禁止自动化静默执行危险行动**:关机/重启/注销/锁屏/删除文件等行动必须二次确认(或明确的免确认设置),测试模式必须假执行;执行前做权限与参数校验,执行后写审计日志。
19. **禁止插件/主题获得默认 Node 权限**;插件代码在受限环境运行(独立进程或 Worker + 白名单 API),安装前展示权限。
20. **禁止执行任意 URL/命令**:仅允许显式协议(http/https)与用户指定程序路径;路径参数做规范化与存在性校验,防止命令注入。
### 18.4 质量与流程红线
21. **禁止用占位符/`TODO` 交付核心功能**:所有 P0/P1 功能必须真实可用;未完成项必须在验收报告中显式列出,不得隐藏。
22. **禁止伪造验收结果**:截图、日志、测试输出必须来自实际运行;未验证的项标注「未验证」。
23. **禁止为了通过验收修改验收标准或删除测试**;标准以本提示词为准。
24. **禁止跳过像素级检查**:主界面、设置窗口、提醒、OOBE 至少各保留一份基准截图与 diff 报告。
25. **禁止把「默认值」写成 0/空导致功能不可见**:所有默认值必须与第 10、13 部分的默认值表一致。
26. **禁止破坏兼容性**:数据 schema、插件 API、主题变量一旦发布,只允许增量变更;破坏性变更必须提供迁移与版本字段。
### 18.5 本提示词未明确时的裁决顺序
当实现细节与本提示词冲突或缺失时,按以下优先级裁决:
1. 本提示词明确写出的数值与行为(最高优先级)。
2. ClassIsland 的公开文档与截图中可见的行为(可参考 `docs.classisland.tech`,但不得引入其代码)。
3. ClassHelper 已实现的桌面岛约定(`repos/ClassHelper-master/packages/desktop-client/src/island` 与 `src/main/island.ts`)。
4. Fluent Design / iOS 灵动岛的一般设计规范。
5. 你的工程判断(必须在代码注释与交付说明中标注「推断」)。
---
## 19. 结语(给实现者的最后要求)
- 先读完本提示词全部章节,列出你认为有歧义的地方并一次性向用户确认;不要边猜边写核心架构。
- 先搭出可无头启动、可截图对比的骨架,再逐模块填充;每完成一个里程碑就运行第 16 部分对应验收,不要最后一次性补测试。
- 代码组织保持「主进程薄、引擎纯函数、渲染层无副作用」:所有业务计算(时间、课表、规则)放在 `packages/shared` 并可单元测试;主进程只做编排与系统集成;渲染层只做展示与交互。
- 交付时提供一份「与 ClassIsland 的差异说明」:列出本实现因技术栈不同而做的等价替换(例如 Avalonia 控件 → Vue 组件、XAML 资源字典 → CSS 变量、插件 DLL → npm 插件包),以及未实现的 P2 能力。
- 最后再对照第 16 部分做一次完整自检,并对照第 18 部分确认没有触碰任何红线;确认无误后再声称完成。