feat: ClassIsland / ClassHelper 灵动岛提示词套件
- 重构版主提示词(14 parts,面向 ClassHelper 灵动岛重构) - ClassIsland 全量规格提示词(20 parts,含分卷与逐章版本) - 投喂指南、分卷、逐节文件 - 调研记录、一致性基线、生成与质检脚本
This commit is contained in:
commit
22c1784bab
117 files changed
+47759
No files matched your search
@@ -0,0 +1,968 @@
|
||||
## 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 部分确认没有触碰任何红线;确认无误后再声称完成。
|
||||
Reference in new issue
Block a user