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

600 lines
70 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.
## 10. 动画与动效映射(ClassHelper 现状 → ClassIsland 目标;双风格默认值)
本节是动效施工图。实现者必须先把「风格开关」接好,再按表逐条替换动画:`family='classisland'`(默认)走 ClassIsland 规格的 CSS transition/keyframes + 遮罩分块;`family='winisland'`(老用户兼容)保留现有弹簧形变与胶囊开合。**同一条动画不得同时用两套实现**,由 `data-family` 与 `appearance.animations` 决定分支。
### 10.0 总则与不可协商的规则
1. **动画级别只有一个开关**:岛的动画由 `appearance.animations`(boolean)总控,**不是** ClassIsland 的 `AnimationLevel`(那是主窗口的设置)。`animations=false` 时所有过渡必须「瞬落」:弹簧 `snap()`、CSS `transition/animation: 0ms`、遮罩直接设终值(现有实现见 `IslandApp.vue:249-256`、`LessonReminderContent.vue:77-80`)。
2. **速度倍率**:`appearance.speed`(0.5–2)按比例缩放所有时长:`实际时长 = 基准时长 / speed`。现有实现已贯穿:弹簧 `dt × speed`(`IslandApp.vue:291`)、窗口淡入 `IslandApp.vue` 之外的 `--island-dur = 320/max(.25,speed)`(`:528`)、课程条 `--ci-duration = 200/speed` 等(`LessonReminderContent.vue:134-137`)。新增动画一律除以 `speed`。
3. **弹簧仅限 winisland**:`family='classisland'` 的几何变化用 CSS transition;`family='winisland'` 继续用 `SpringValue`(stiffness 0.10 / damping 0.68 / 换向保留 35% / 单帧 ≤ 剩余 20% / snap 0.05px / 零过冲钳制,`packages/desktop-client/src/island/spring.ts:26-114`)。**不得**在 classisland 分支引入弹簧(Lead 裁决,`parts-r/01-objective.md:52`)。
4. **窗口几何不动画**:开合动画全部发生在固定包围盒内部(`main/island.ts:1412-1417`)。classisland 的 fixed 信息行也必须沿用这一约束:行宽/气泡变化在渲染层完成,窗口只在「不可见时」或「模式/设置变化」时 `setBounds`(`island.ts:1476-1479,1598-1612`)。**fixed 单条气泡的行高恒为 40px**,气泡(遮罩/内容/进度条)全部在 40px 行内完成,不因内容改行高、也不因此扩窗;`maxCardHeight` 只用于 custom 的多条消息列表与强提醒窗口的尺寸计算。唯一例外是「多条消息列表」比普通形态更高,窗口必须在形变开始前先长高(`island.ts:1514-1518`),与单条气泡无关。
5. **不可打断的动画可被新状态覆盖**:新状态直接接管(取消旧 rAF/定时器、重设目标),不得排队、不得闪回。现有 `syncTargets(snap)`、`animate()`、`animateMask()` 都按此实现(`IslandApp.vue:238-264`、`CustomIslandRuntime.vue:127-177`、`LessonReminderContent.vue:73-102`)。
6. **隐藏页/窗口不可见必须暂停**:custom 的 `motion = animations && !document.hidden && nativeVisible`(`CustomIslandRuntime.vue:57-58`);classic 通过 `props.suspended` 停 rAF(`IslandApp.vue:240-246`)。新增动画(遮罩、气泡、强提醒)同样要遵守:页面隐藏时不得继续跑 rAF;恢复可见时按当前状态**直接落位**(不要补播)。
7. **减少动态效果(prefers-reduced-motion)不覆盖岛**:ClassHelper 明确「应用内动画开关决定岛动画,不能用主窗口的减少动态效果说明替代岛自身规则」(`docs/app/island.md:57`;`docs/dev/motion.md:78`)。岛动画的**唯一开关**是 `appearance.animations`,`appearance.speed` 统一缩放时长;实现者**不得**让系统 reduced-motion 强制关闭岛动画或改写其时长,只在设置页给出提示文案。若未来用户明确要求「尊重系统设置」,只能作为 P2 可选项(设置项 `respectReducedMotion`,默认 `false`),默认行为保持不变。
8. **帧预算**:岛渲染进程 60fps;任意单帧主线程 ≤ 8ms;动画只作用于 `transform`/`opacity`/`clip-path`/`box-shadow`(颜色类)与 SVG `points`(遮罩),**禁止**逐帧改 `width/height/top/left` 触发重排(弹簧形变改的是 SVG path 与元素尺寸,属于既有例外,仅 winisland 使用)。
9. **兜底定时器**:任何依赖 rAF 的动画必须有「到点直接落位」的兜底,因为 Windows 会把置顶透明小窗判定为被遮挡而把 rAF 压到 1fps(`IslandApp.vue:266-286` 的 `MORPH_FALLBACK_MS=1200`)。classisland 的 CSS transition 天然有终点,但遮罩(SVG 分块)与任何 JS 驱动的动画仍需兜底(现有 600/speed,`LessonReminderContent.vue:82-89`)。
10. **动画结束清场**:动画结束必须:移除临时 `will-change`、取消 rAF/定时器、移除 `closing` 等临时类;不得留下持续运行的循环(`12-acceptance.md:73`)。
### 10.1 ClassHelper 现状动画清单(重构前基线,必须保留语义)
| 编号 | 现状动画 | 触发 | 当前实现 | 关键数值/证据 |
|---|---|---|---|---|
| S1 | classic 胶囊↔展开卡形变 | 消息展开/收起 | `SpringValue` 逐帧逼近 width/height/radius | 目标尺寸见 `IslandApp.vue:188-204`;圆角胶囊 `min(radius,h/2)`、展开 `min(48×radius/20,w/2,h/2)`(`:207-212`);弹簧参数 `spring.ts:26-30` |
| S2 | 内容交叉淡入 | 形变过程中 | 胶囊层 `1-1.5p`、展开层 `p²` | `IslandApp.vue:346-351`;progress 由高度差推导(`:304-309`) |
| S3 | 收回动画 | 点「知道了/标为已读」 | 形态先切回胶囊,360ms 后淡出隐藏 | `CLOSE_ANIMATION_MS = 360`(`main/island.ts:114-121`) |
| S4 | 窗口淡入/淡出 | 显示/隐藏 | 主进程逐帧改 `setOpacity` | 淡入每 16ms +0.16;淡出每 16ms −0.2,≤0.02 后 `hide()`(`island.ts:1674-1715`,`island.ts:1686,1705`) |
| S5 | custom 形变 | 展开/收起/课程条切换 | width/height/radius + content/lesson 进度弹簧 | `CustomIslandRuntime.vue:88-177`;`snapThreshold:0.001`(`:92-93`);兜底 `1200/speed`(`:156`) |
| S6 | custom 页面交叉淡入 | 常态↔展开↔课程 | `compactAlpha=1-1.5p`、`detailsAlpha=p²`、`lessonProgress` | `CustomIslandRuntime.vue:99-100`;三页 `opacity/visibility` 绑定(`:385-511`) |
| S7 | custom 测量适配 | 内容/字体/视口变化 | `ResizeObserver` + `nextTick` 量 `offsetWidth/offsetHeight` + `fitted()` | `CustomIslandRuntime.vue:72-80,238-266` |
| S8 | 课程提醒遮罩 | 准备/上课/下课/下课前 | SVG 5 个多边形按进度重绘 | 区段 360ms、开错峰 `[0,90,120,90,0]`、关 `[120,90,0,90,120]`、总 480ms、开 `1-(1-p)^4`、关 `p²`(`LessonReminderContent.vue:59-102`) |
| S9 | 课程提醒标题 | 遮罩出现/消失 | CSS animation | 入 250ms 延迟 260ms `(0.25,1,0.5,1)`;缩放 750ms 延迟 260ms;出 200ms linear 反向(`:134-137,273-280`) |
| S10 | 课程提醒进度条 | 每秒更新 | `transform: scaleX` transition | 50ms linear(`LessonReminderContent.vue:307-316`,高度 2px) |
| S11 | 组件行课程进度 | 每秒更新 | `transform: scaleX` transition | 1s linear(`ComponentNode.vue:348-362`,高度 2px) |
| S12 | 滚动组件 | 文字超宽 | CSS keyframes | `rolling` 30%→−80%,时长 `max(1,200/speed)` 秒,linear infinite;编辑时停(`ComponentNode.vue:205,384-392,446-453`) |
| S13 | 轮播组件 | 定时切换 | 计算当前索引,瞬时替换 | `interval` 秒(`ComponentNode.vue:121-130`;`island-layout.ts:196` 默认 5s) |
| S14 | 课程徽标 | 下课前 | 静态圆形 | 30×30、`#0a84ff`、12px/600、tabular-nums(`lesson-ending.css:3-16`) |
| S15 | 徽标入场 | 铃铛计数出现 | CSS animation | `resident-indicator-in` 200/speed ms fade(`custom-runtime.css:147-149,199-206`) |
| S16 | 课件覆盖 | 课件开始/结束 | 瞬时显示/隐藏,无动画 | `CoursewareTransfer.vue`(无 transition) |
| S17 | 悬停/按压 | 指针悬停、按下 | CSS transition | 按钮/列表行 140ms ease-out(`classic-island.css:393,480,613-616,699-702`);按下 `scale(.97)`(`:724-727`);仅 hover 设备(`:736-750`) |
| S18 | 编辑预览 | 打开编辑器 | 静态排版,无动画 | `island-editor.css:193-227`(预览行 44px 高) |
| S19 | 外观变化 | 设置页改宽高/字号/配色 | 重写 CSS 变量,瞬时生效 | `IslandApp.vue:175-176,516-528`;`CustomIslandRuntime.vue:102-124` |
| S20 | 失焦宽限 | 展开后失焦 | 宽限期内不收起(非动画但影响观感) | 鼠标 500ms、触摸 1200ms(`island.ts:2287,2297`) |
### 10.2 映射总表(目标规格;每条给出双风格默认值)
说明:`CS` = `family='classisland'`(默认),`WI` = `family='winisland'`(兼容)。「实现」列 `CSS-T` 表示 CSS transition、`CSS-A` 表示 CSS keyframes/WAAPI、`SVG-rAF` 表示 SVG/Canvas 按进度重绘、`Spring` 表示 `SpringValue` 帧积分、`主进程` 表示 Electron 主进程逐帧/定时。
#### A. 岛出现 / 隐藏 / 开合
classisland fixed 下「岛」不再是胶囊,而是一条信息行:出现/隐藏必须轻、快、不抢注意力,避免每来一条消息就在屏幕顶部弹一个大卡片。开合类动画只允许改变行背景层与不透明度,内容层文字不得跟着宽度动画重排(否则会出现「文字先铺开再被省略号收回」的抖动,ClassHelper 的 `classic-island.css:196-206` 已记录同类问题)。winisland 分支的胶囊开合仍按原有弹簧语义执行,不得为了「统一」把弹簧搬到 classisland。
**fixed 行高恒 40px(硬约束)**:气泡的遮罩、正文、进度条全部在 40px 行内完成;正文最多 **2 行**、超出用省略号(`display:-webkit-box; -webkit-line-clamp:2; overflow:hidden`),**不得**出现「行高随内容增高」「行高 200ms 过渡」「为气泡扩窗」这类 ClassHelper 旧展开卡行为。`maxCardHeight` 只参与 custom 的多条消息列表与强提醒窗口尺寸计算,单条气泡不读它。
| 编号 | 场景 | 触发 | 属性 | 起止值 | 时长 | 曲线 | 延迟/交错 | 实现 | 降级 | 现状对照 |
|---|---|---|---|---|---|---|---|---|---|---|
| A1 | CS fixed 岛出现 | 首次显示/新消息首次弹出 | `opacity`、`translateY` | 0 / 8px → 1 / 0 | 250ms | `cubic-bezier(.25,1,.5,1)` | 0 | CSS-T | `animations=false` 直接落位 | 无(新) |
| A2 | CS fixed 岛隐藏 | 消息清空且无课程 | `opacity` | 1 → 0 | 150ms | `cubic-bezier(.16,1,.3,1)` | 0 | CSS-T 后主进程 `hide()`(隐藏帧) | 无动画直接 hide | 对应 S4 |
| A3 | CS 行宽度变化 | 内容/字号/组件变化 | `width`(行背景层) | 当前 → 目标 | 300ms(Fluent)/550ms(Classic) | `(.65,0,.35,1)` / `(0,1,.5,1.1)` | 0 | CSS-T(背景层绝对定位,文字层瞬时) | 直接落位 | 对应 S19 |
| A4 | CS 单条气泡布局(行高固定) | 单条消息出现/内容变化 | 行高(不变)、正文行数 | **恒 40px**;正文 0→2 行(超出省略),内容在行内完成遮罩/进度 | 0(不改行高) | — | — | 固定 `--ci-row-h`;`-webkit-line-clamp:2`;不扩窗 | 同(直接布局) | 对应 S3/S8;**不得**沿用旧展开卡增高行为 |
| A5 | CS 分体行出现 | 开启 `separated` | 行间距、圆角 | 0 → 5px、0 → 8px | 200ms | `(.16,1,.3,1)` | 每行 0 | CSS-T | 直接落位 | 无(新) |
| A6 | WI 胶囊↔展开 | 展开/收起 | width/height/radius | 胶囊 `w×h×r(h/2)` ↔ 展开 `w+156 × h+186 × min(48r/20)` | 由弹簧决定(约 300–500ms) | 弹簧(近临界阻尼,无过冲) | 0 | Spring | `animations=false` → `snap()` | S1 |
| A7 | WI 内容交叉淡入 | 同 A6 | 两层 `opacity` | 胶囊 `1−1.5p`;展开 `p²` | 跟随 p | 线性映射 | 0 | JS 逐帧绑定 | 直接显示终态 | S2 |
| A8 | WI 收回并淡出 | 点知道了/标为已读 | 形态→胶囊,再窗口淡出 | 展开 → 胶囊;opacity→0 | 360ms 形态 + 淡出(−0.2/16ms) | 弹簧 + 线性 | 360ms 后淡出 | Spring + 主进程 | 立即 hide | S3/S4 |
| A9 | WI 窗口淡入 | 显示 | 窗口 `opacity` | 当前 → `displayOpacity` | 每 16ms +0.16(约 60–110ms) | 线性 | 0 | 主进程 | 立即设置 | S4 |
| A10 | WI 窗口淡出 | 隐藏 | 窗口 `opacity` | 当前 → 0 | 每 16ms −0.2(约 50–80ms) | 线性 | ≤0.02 后 `hide()` | 主进程 | 立即 hide | S4 |
| A11 | 上课时段隐藏 | 进入上课且无紧急/叫人 | 窗口可见性 | 可见 → 隐藏 | 0(立即) | — | — | 主进程 `hideImmediately()` | 同 | `island.ts:1463-1473` |
#### B. 行内布局与组件
classisland 信息行的内部布局必须「稳」:数据每秒变化时只换文本、不动布局;只有组件增删、方案切换、拖拽排序时才允许位移类动画。行内所有组件共用 --ci-fs-* 与 --ci-row-h,任何组件的默认尺寸都不得把行撑高(超高时内部滚动或省略,见第 7 部分)。
| 编号 | 场景 | 触发 | 属性 | 起止值 | 时长 | 曲线 | 延迟/交错 | 实现 | 降级 | 现状对照 |
|---|---|---|---|---|---|---|---|---|---|---|
| B1 | CS 组件淡入 | 组件首次挂载/由隐藏转可见 | `opacity` | 0 → 1 | 300ms | `(.65,0,.35,1)` | 0 | CSS-T | 直接显示 | 对应 ClassIsland 组件淡入(`repos/ClassIsland/.../ComponentPresenter.axaml.cs:224-243`) |
| B2 | CS 首次多行错峰入场 | 岛首次显示且 ≥2 行 | `opacity`、`translateY` | 0 / 6px → 1 / 0 | 320ms | `(.16,1,.3,1)` | 每行 30ms,封顶 180ms | CSS-A(按行 index 计算 delay) | 跳过 | 对应 ClassIsland 列表错峰(25ms/项,`StackPanelIntroAnimationBehavior.cs:74-90`);本处取 30ms 更克制 |
| B3 | CS 组件重排过渡 | 编辑器中拖动排序 | `transform` | 旧位置 → 新位置 | 225ms | `CubicEaseInOut`≈`(.65,0,.35,1)`【推断】 | 0 | FLIP(先读后写) | 直接跳位 | 对应 `WrapPanelResizingAnimationAssist.cs:45-49` |
| B4 | 数据文本刷新 | 时钟/日期/倒计时每秒 | 文本内容 | — | 0 | — | — | 直接替换(`tabular-nums`) | 同 | 无动画是**有意**的(ClassIsland 也不做数字翻转) |
| B5 | CS 消息气泡宽度 | 单条气泡换行/字数变化 | 宽度 | 当前 → 内容宽 | 200ms | `(.16,1,.3,1)` | 0 | CSS-T | 直接落位 | 无(新) |
#### C. 进度条
进度条是岛内唯一「高频更新」的动画元素:它必须用 transform: scaleX() 而非宽度,且每次更新只触发合成层。ClassIsland 的倒计时进度用 50ms 线性过渡(repos/ClassIsland/ClassIsland/XamlThemes/FluentTheme/Styles.axaml:137-145),classisland 风格沿用;winisland 的课程进度保持 1s 线性(每秒一 tick),两者视觉一致但实现成本不同。
| 编号 | 场景 | 触发 | 属性 | 起止值 | 时长 | 曲线 | 延迟 | 实现 | 降级 | 现状对照 |
|---|---|---|---|---|---|---|---|---|---|---|
| C1 | 提醒倒计时进度 | 剩余时间更新 | `transform: scaleX` | 旧比例 → 新比例 | 50ms | 线性 | 0 | CSS-T | 直接落位 | 对应 ClassIsland `FluentTheme/Styles.axaml:137-145` |
| C2 | CS 行课程进度 | 每秒更新 | `scaleX` | 旧 → 新 | 50ms(每 50ms 更新一次时长)或 1s linear(每秒更新) | 线性 | 0 | CSS-T | 直接落位 | 现状 1s linear(S11),ClassIsland 50ms;**默认选 50ms**,若性能紧张可切 1s(见 §10.3 选择点) |
| C3 | WI 课程进度 | 每秒更新 | `scaleX` | 旧 → 新 | 1s | 线性 | 0 | CSS-T | 直接落位 | S11 保留 |
| C4 | 课件进度 | 传输事件 | 原生 `<progress>` | 旧 → 新 | 150ms | 线性 | 0 | CSS-T(`progress` 值即时,条宽过渡) | 直接落位 | S16 |
| C5 | 进度条高度 | 组件挂载 | `height` | 2px → 3px(CS) | 0(静态) | — | — | 静态 token `--ci-progress-h` | 同 | ClassIsland 3px;现状 2px |
#### D. 通知气泡两阶段 / 斜切遮罩(classisland 默认核心)
这是本轮重构最核心的动效。ClassIsland 的提醒是「先遮罩、后内容」的两阶段:斜切分块从两侧向中间扫入,文字延迟 260ms 再淡入并轻微缩放,最后背景投影浮现;退出时反向收拢并放大退出。ClassHelper 的 LessonReminderContent.vue 已经实现过一版(edges [0,.1,.3,.7,.9,1] 等价于权重 [0,.1,.2,.4,.2,.1]),本轮必须把它抽成公共组件并让消息气泡复用,不得再写第二份。
| 编号 | 场景 | 触发 | 属性 | 起止值 | 时长 | 曲线 | 延迟/交错 | 实现 | 降级 | 现状对照 |
|---|---|---|---|---|---|---|---|---|---|---|
| D1 | 遮罩扫入(开) | 气泡/课程提醒出现 | 5 个区段宽度进度 | 0 → 1 | 每段 360ms | QuarticEaseOut(`1−(1−p)^4`) | 区段错峰 `[0,90,120,90,0]`,总 480ms | SVG-rAF 或 CSS `clip-path` | 直接设 1 | S8(现有实现一致) |
| D2 | 遮罩收拢(关) | 气泡/提醒消失 | 5 个区段 | 1 → 0 | 每段 360ms | QuadraticEaseIn(`p²`) | 错峰 `[120,90,0,90,120]`,总 480ms | SVG-rAF | 直接设 0 | S8 |
| D3 | 遮罩形状 | 任意宽度 | 5 段权重、斜切角 | 权重 `[0,.1,.2,.4,.2,.1]`;偏移 `tan(30°)×40` | — | — | — | SVG `polygon` / `clip-path` | 同 | S8(现有 edges `[0,.1,.3,.7,.9,1]` 等价) |
| D4 | 遮罩文字淡入 | 遮罩打开 | `opacity` | 0 → 1 | 250ms | `(.25,1,.5,1)` | 延迟 260ms | CSS-A `both` | 直接显示 | S9 |
| D5 | 遮罩文字缩放 | 遮罩打开 | `scale` | 1.1 → 1.0 | 750ms | `(.25,1,.5,1)` | 延迟 260ms | CSS-A | 直接落位 | S9 |
| D6 | 遮罩文字淡出 | 遮罩关闭 | `opacity`、`scale` | 1/1.0 → 0/1.1 | 200ms | 线性(透明度)/`(.5,0,.75,0)`(缩放) | 0 | CSS-A | 直接隐藏 | S9 |
| D7 | 遮罩投影淡入 | 遮罩打开 | `box-shadow` | 无 → `0 8px 32px rgba(0,0,0,.4)` | 100ms | 线性 | 延迟 600ms | CSS-T | 直接到位 | ClassIsland `FluentTheme:355-367` |
| D8 | 遮罩底色淡入 | 遮罩打开 | 背景 overlay `opacity` | 0 → 1 | 100ms | 线性 | 延迟 600ms | CSS-A | 直接到位 | `FluentTheme:355-367` |
| D9 | 覆盖层淡入 | 遮罩打开后正文/详情出现 | `opacity` | 0 → 1 | 200ms | SineEaseIn≈`(.12,0,.39,0)`【推断】 | 0 | CSS-T | 直接显示 | ClassIsland `FluentTheme:419-451` |
| D10 | 覆盖层淡出 | 正文结束/关闭 | `opacity` | 1 → 0 | 200ms | SineEaseIn | 0 | CSS-T | 直接隐藏 | 同上 |
| D11 | Classic 遮罩(主题=classic) | 同 D1/D2 | `translateY` | −50 → 0(入)/0 → 50(出) | 200ms | QuadraticEaseIn/Out | 出延迟 100ms | CSS-A | 直接落位 | ClassIsland `ClassicTheme/Styles.axaml:265-331` |
| D12 | Classic 文字淡入/淡出 | 同上 | `opacity` | 0→1(100ms 延迟 200ms)/1→0(100ms) | 100ms | 线性 | 见说明 | CSS-A | 直接落位 | 同上 |
| D13 | 多行气泡错峰 | 列表 ≥2 条 | `opacity`、`translateY` | 0 / 6px → 1 / 0 | 200ms | `(.16,1,.3,1)` | 每行 30ms,封顶 120ms | CSS-A | 直接显示 | 无(新增,参考 ClassHelper 主窗口列表错峰) |
| D14 | 气泡内进度条 | 倒计时更新 | `scaleX` | 旧 → 新 | 50ms | 线性 | 0 | CSS-T | 直接落位 | 对应 C1 |
| D15 | 气泡图标/徽标入场 | 气泡出现 | `scale`、`opacity` | .9/0 → 1/1 | 150ms | `(.16,1,.3,1)` | 0 | CSS-A | 直接显示 | 无(新增) |
| D16 | 关闭后进入下一批 | 队列非空 | 遮罩重播 | 同 D1/D2 | 同 | 同 | 先 D2 再 D1(交叉 100ms) | SVG-rAF | 直接切 | ClassHelper 队列现为「直接换内容」,需补过渡 |
| D17 | 关闭并淡出 | 队列清空 | 遮罩 + 窗口 | 遮罩收拢 + `opacity→0` | 480 + 150ms | 同 | 遮罩结束后淡出 | SVG-rAF + CSS-T | 直接 hide | 对应 S3(WI 用 360ms) |
#### E. 强提醒特效(新增)
强提醒是新增的独立透明窗口(不抢焦点、不阻塞、超时自关),其动画只有「涟漪扩散」一条,其余都是窗口显隐。特效必须在岛所在显示器上铺到足够大的区域,尺寸按 ClassIsland 规则随屏高缩放,且编辑/预览模式下绝不创建。
| 编号 | 场景 | 触发 | 属性 | 起止值 | 时长 | 曲线 | 延迟 | 实现 | 降级 | 现状对照 |
|---|---|---|---|---|---|---|---|---|---|---|
| E1 | 特效窗口出现 | 强调提醒 | 窗口可见性 | 隐藏 → 显示 | 0 | — | 0 | 主进程 `showInactive()` | 同 | ClassIsland `TopmostEffectWindow` |
| E2 | 涟漪扩散 | 特效开始 | `scale` | 0 → 1 | 600ms | QuadraticEaseIn | 0 | WAAPI(独立窗口) | 直接显示静态 | ClassIsland `RippleEffect.axaml.cs:92-96` |
| E3 | 涟漪淡出 | 同上 | `opacity` | 1 → 0 | 600ms | SineEaseIn | 0 | WAAPI | 直接移除 | `RippleEffect.axaml.cs:102-106` |
| E4 | 特效收尾 | 750ms 后 | 移除元素/隐藏窗口 | — | — | — | 750ms 总时长 | 主进程 `hide()` | 立即隐藏 | `RippleEffect.axaml.cs:108-109` |
| E5 | 特效渲染缩放 | 屏高 ≥1400/≥2000 | 窗口尺寸 | 1× → .75× /.5× | 0 | — | — | 主进程按屏算 | 固定 1× | `repos/ClassIsland/.../MainWindow.axaml.cs:377-392` |
#### F. 课程徽标 / 课程提醒
课程提醒的视觉规格已经在 LessonReminderBar 中实现过半(倒计时/课程名/时间/三段信息/遮罩/进度条)。本轮需要补齐的是「入场/退场」「徽标出现」「徽标颜色随剩余时间变化」三处,并确保徽标数字 -1 的边界行为(剩余 1 秒显示 -1,不显示「即将下课」遮罩,docs/app/island.md:68-70)。
| 编号 | 场景 | 触发 | 属性 | 起止值 | 时长 | 曲线 | 延迟 | 实现 | 降级 | 现状对照 |
|---|---|---|---|---|---|---|---|---|---|---|
| F1 | 课程提醒条入场 | 提醒开始(CS fixed) | `opacity`、`translateY` | 0 / 8px → 1 / 0 | 200ms | `(.16,1,.3,1)` | 0 | CSS-T | 直接显示 | 无入场动画(新增) |
| F2 | 课程提醒条退场 | 提醒结束 | `opacity` | 1 → 0 | 150ms | `(.16,1,.3,1)` | 0 | CSS-T | 直接隐藏 | 无(新增) |
| F3 | 课程遮罩 | 提醒开始(mask 阶段) | 同 D1/D2 | 同 | 360/120 | 同 | 同 | 复用遮罩组件 | 直接落位 | S8 |
| F4 | 下课前徽标出现 | `ending` 且开启倒数 | `scale`、`opacity` | .8/0 → 1/1 | 150ms | `(.16,1,.3,1)` | 0 | CSS-A | 直接显示 | S14(静态) |
| F5 | 徽标数字更新 | 每秒 | 文本 | `-N` → `-(N-1)`,最后 `-1` | 0 | — | — | 直接替换(tabular-nums) | 同 | `docs/app/island.md:68-70` |
| F6 | 徽标颜色 | 剩余 ≤10s | 颜色 | `--ci-accent` → `--ci-state-urgent`【决策】 | 150ms | 线性 | 0 | CSS-T | 直接换色 | 无(新增,可选) |
| F7 | 课程进度条 | 每秒 | `scaleX` | 旧 → 新 | 50ms(CS)/1s(WI) | 线性 | 0 | CSS-T | 直接落位 | S10/S11 |
| F8 | 课程条接管岛(custom) | 课程提醒出现 | 页面交叉 | 常态 → 课程条 | CS 200ms / WI 弹簧 | `(.16,1,.3,1)` / 弹簧 | 0 | CSS-T / Spring | 直接切 | S5/S6 |
#### G. 组件滚动 / 轮播
滚动与轮播是 custom 方案里唯一的「持续动画」。它们必须遵守两条纪律:① 页面隐藏/窗口不可见时暂停;② 编辑模式下暂停并展开全部子组件,便于选择与排序。classisland 风格的轮播切换按 ClassIsland 的 250ms 上下滑动,winisland 保留瞬时切换。
| 编号 | 场景 | 触发 | 属性 | 起止值 | 时长 | 曲线 | 延迟 | 实现 | 降级 | 现状对照 |
|---|---|---|---|---|---|---|---|---|---|---|
| G1 | 滚动组件(默认连续) | 内容超宽 | `translateX` | `30%` → `−80%` | `max(1,200/speed)` 秒 | 线性无限循环 | 0 | CSS-A | 静止并省略号 | S12 保留 |
| G2 | 滚动组件(ClassIsland 暂停式,可选) | 设置 `mode='pause'` | `translateX` | 0 → `−(innerW)` 停顿 → 回 0 | `innerW/40 + pause(默认10s) + outerW/40` 秒 | 线性无限 | 无 | WAAPI 关键帧 | 回落 G1 | 对应 ClassIsland `RollingComponent.axaml.cs:85-135`(速度 40px/s、暂停 10s) |
| G3 | 轮播切换(CS) | 定时 | `translateY`、`opacity` | 旧:−40→0 / 0→1;新:0→40 / 1→0 | 250ms | `(.65,0,.35,1)` | 0 | CSS-A | 瞬时替换 | ClassIsland `SlideComponent.axaml:69-107`;现状 S13 无动画 |
| G4 | 轮播切换(WI) | 定时 | 瞬时替换 | — | 0 | — | — | 直接换 | 同 | S13 保留 |
| G5 | 编辑模式下滚动/轮播 | 进入编辑器 | 暂停/展开全部 | 动画 → 停止 | 0 | — | — | 移除 animation / 显示全部子节点 | 同 | `ComponentNode.vue:388-392` |
| G6 | 隐藏页暂停 | `document.hidden` / 窗口不可见 | 暂停所有组件动画 | 运行 → 暂停 | 0 | — | — | `motion` 计算 / CSS `animation-play-state` | 同 | `CustomIslandRuntime.vue:57-58` |
#### H. 主题 / 风格切换
风格切换(classisland↔winisland)是「整岛换一套几何与实现」,必须避免「半新半旧」的中间帧:切换时先做 200ms 交叉淡化,再在同一帧替换 data-family 与变量,最后淡入。主题切换(Fluent↔Classic)与深浅色只改颜色 token,几何与动画参数不动——这是 ClassHelper 与 ClassIsland 共同的纪律(docs/dev/motion.md:125-130)。
| 编号 | 场景 | 触发 | 属性 | 起止值 | 时长 | 曲线 | 延迟 | 实现 | 降级 | 现状对照 |
|---|---|---|---|---|---|---|---|---|---|---|
| H1 | family 切换 | 设置页切换 classisland↔winisland | 整岛 `opacity` + 变量 | 1→0.6→1(快速交叉) | 200ms 出 + 200ms 入 | `(.16,1,.3,1)` | 0 | CSS-T(切 `data-family` 前后加过渡类) | 直接切 | S19(现为瞬时) |
| H2 | theme 切换(Fluent↔Classic) | 设置页切换 | 背景/描边/阴影颜色 | 旧 → 新 | 200ms | 线性 | 0 | CSS-T(仅颜色 token) | 直接切 | 对应 ClassIsland 窗口 `BrushTransition 200ms`(`MainWindow.axaml:40-44`) |
| H3 | 深浅色切换 | 应用主题变化 | 颜色 token | 旧 → 新 | 200ms | 线性 | 0 | CSS-T | 直接切 | 同 H2 |
| H4 | 强调色变化 | 设置页改 accent | `--island-accent` 派生色 | 旧 → 新 | 150ms | 线性 | 0 | CSS-T(颜色) | 直接切 | S19 |
#### I. 消息列表 / 徽标 / 交互
交互类动画是「手感」而非「表演」:悬停 140ms、按压 90ms、徽标 200ms,全部短于 250ms,且只作用于可交互元素。所有 hover 效果必须包在 @media (hover:hover) and (pointer:fine) 内,避免触摸屏幽灵 hover(classic-island.css:729-750)。
| 编号 | 场景 | 触发 | 属性 | 起止值 | 时长 | 曲线 | 延迟 | 实现 | 降级 | 现状对照 |
|---|---|---|---|---|---|---|---|---|---|---|
| I1 | 铃铛徽标入场 | 未读数 0→N | `opacity` | 0 → 1 | 200ms/speed | `(.16,1,.3,1)` | 0 | CSS-A | 直接显示 | S15 保留 |
| I2 | 列表行悬停 | 指针移入(仅 hover 设备) | `background` | 旧 → `--ci-fill-2` | 140ms | `(.16,1,.3,1)` | 0 | CSS-T | 无 hover | S17 |
| I3 | 按钮按压 | 按下/松开 | `scale` | 1 → .97 → 1 | 90ms 按下 / 抬起 200ms | `(.16,1,.3,1)` | 0 | CSS-T | 直接 | S17 |
| I4 | 图标按钮悬停 | 指针移入 | 背景/前景 | 旧 → 新 | 140ms | `(.16,1,.3,1)` | 0 | CSS-T | 无 | S17 |
| I5 | 选中/焦点环 | Tab 键聚焦 | `outline` | 无 → 2px accent | 0 | — | — | CSS 静态 | 同 | 无(新增 a11y) |
| I6 | 多条消息列表高度变化 | 展开更多/新消息(**仅 custom 多条列表**,非单条气泡) | 列表卡片高度 | 旧 → 新 | CS 200ms / WI 弹簧 | `(.16,1,.3,1)` / 弹簧 | 0 | CSS-T/Spring + 主进程先扩窗 | 直接落位 | 现有 `islandListLayout`(度量);单条气泡不适用,行高恒 40 |
#### J. 编辑预览 / 拖拽
编辑器在主窗口里运行,动画要克制:进入/退出编辑模式 200ms 交叉淡化即可;拖拽必须跟手(0ms 跟随),只对「落位排序」做 150ms FLIP。预览区不创建额外桌面窗口(01-objective.md:35、docs/app/island.md:47)。
| 编号 | 场景 | 触发 | 属性 | 起止值 | 时长 | 曲线 | 延迟 | 实现 | 降级 | 现状对照 |
|---|---|---|---|---|---|---|---|---|---|---|
| J1 | 进入/退出编辑模式 | 点「进入编辑模式」 | 整页 `opacity` | 0 ↔ 1 | 200ms | `(.16,1,.3,1)` | 0 | CSS-T | 直接切 | 无(新增) |
| J2 | 拖拽幽灵 | 拖动组件 | 跟随指针 | — | 0 | — | — | 直接定位(`island-editor.css:331-334`) | 同 | S18 |
| J3 | 拖拽排序 | 放下组件 | `transform` | 旧位 → 新位 | 150ms | `(.16,1,.3,1)` | 0 | FLIP | 直接跳 | 无(新增) |
| J4 | 放置高亮 | 拖到行上 | `outline`/背景 | 无 → 2px accent 虚线 | 0/100ms | 线性 | 0 | CSS 静态 | 同 | `island-editor.css:228-232` |
| J5 | 预览缩放滑杆 | 拖动 | `--resident-scale` | 连续 | 0(实时跟随) | — | — | 直接赋值 | 同 | `island-editor.css:193-217` |
| J6 | 保存状态 | 保存中/失败 | 文本颜色 | 灰 ↔ 红 | 0 | — | — | 直接切 | 同 | `island-editor.css:27-34` |
#### K. 课件覆盖
| 编号 | 场景 | 触发 | 属性 | 起止值 | 时长 | 曲线 | 延迟 | 实现 | 降级 | 现状对照 |
|---|---|---|---|---|---|---|---|---|---|---|
| K1 | 课件卡入场 | 传输开始 | `opacity`、`translateY` | 0 / 8px → 1 / 0 | 200ms | `(.16,1,.3,1)` | 0 | CSS-T | 直接显示 | S16(无动画) |
| K2 | 课件进度 | 进度事件 | 进度条宽度 | 旧 → 新 | 150ms | 线性 | 0 | CSS-T | 直接跳 | S16 |
| K3 | 课件卡退场 | 完成/失败后 3s | `opacity` | 1 → 0 | 150ms | `(.16,1,.3,1)` | 3s 停留 | CSS-T 后主进程恢复岛 | 直接隐藏 | S16 |
#### L. 暂停 / 降级 / 兜底
| 编号 | 规则 | 触发 | 行为 | 证据 |
|---|---|---|---|---|
| L1 | 动画总开关 | `animations=false` | 弹簧 `snap`、CSS 过渡 0ms、遮罩设终值、rAF 不启动 | `IslandApp.vue:249-253`、`CustomIslandRuntime.vue:148-151`、`LessonReminderContent.vue:77-80` |
| L2 | 速度倍率 | `speed≠1` | 所有时长 `/speed`;弹簧 `dt×speed`;兜底定时器 `/speed` | `IslandApp.vue:291,528`、`CustomIslandRuntime.vue:156,220-222`、`LessonReminderContent.vue:134-137` |
| L3 | 页面隐藏 | `document.hidden` | 暂停弹簧/rAF;恢复时按当前状态落位 | `CustomIslandRuntime.vue:57-58,269-273` |
| L4 | 窗口不可见 | 主进程 `hide()` | 渲染层收到 `nativeVisible=false`,暂停 | `CustomIslandRuntime.vue:58`;preload `island:visibility`(`main/island.ts:528-533`) |
| L5 | 系统减少动态效果 | `prefers-reduced-motion` | **不覆盖岛**;岛动画唯一开关是 `appearance.animations`,`speed` 统一缩放;仅编辑器/设置 UI 遵循系统设置 | `docs/app/island.md:57`、`docs/dev/motion.md:78` |
| L5b | 尊重系统设置(P2,可选) | 用户显式开启 `respectReducedMotion` | 系统 reduced-motion 时按 `animations=false` 落位;**默认关闭** | 本行为可选实现,默认不得启用 |
| L6 | rAF 被限流 | Windows 判定遮挡 | 兜底定时器到点直接落位(形变 1200/speed、遮罩 600/speed、命中 600/speed) | `IslandApp.vue:236,266-286`、`LessonReminderContent.vue:82-89`、`CustomIslandRuntime.vue:220-222` |
| L7 | 触摸窗口缩放 | 触摸模式形态变化 | 变大立即、变小等隐藏后收(不做动画) | `main/island.ts:1555-1578` |
| L8 | 失焦宽限 | 展开后 blur | 500ms(鼠标)/1200ms(触摸)内不收起,避免焦点抖动导致误收起 | `main/island.ts:2287,2297,557-578` |
### 10.2.1 消息气泡完整时间线(按毫秒对齐,验收时逐点核对)
以一条普通通知在 `family='classisland'`、`speed=1`、`animations=true`、`theme='fluent'` 下为例,完整时间线必须严格如下(同一条消息的遮罩、文字、投影、进度条、窗口状态都按此对齐):
| 时刻 | 事件 | 实现要点 |
|---|---|---|
| t=0 | 主进程把消息压入队列并把 `mode` 切为 `expanded`;窗口使用预设包围盒(宽 = min(工作区宽,960)、高 = 40+2×pad),**不因气泡扩窗** | 窗口 `setBounds` 不得在可见时发生(`main/island.ts:1476-1479`) |
| t=0 | 岛出现(若原本隐藏):`opacity 0→1 / translateY 8→0`,250ms | A1;同时开始窗口淡入(winisland 为 +0.16/16ms) |
| t=0 | 5 个斜切区段开始扫入:区 0/4 立即、区 1/3 延迟 90ms、区 2 延迟 120ms,每段 360ms QuarticEaseOut | D1;总 480ms |
| t=0 | 气泡图标/徽标入场 150ms(scale .9→1) | D15 |
| t=260 | 遮罩文字开始淡入 250ms + 缩放 1.1→1.0 750ms | D4/D5;两个 CSS animation 必须同时挂、各自 `both` |
| t=480 | 遮罩完全覆盖行(5 区到位) | D1 结束 |
| t=600 | 背景投影/遮罩底色开始淡入 100ms | D7/D8 |
| t=700 | 投影/底色到位;如为「有正文」的提醒,覆盖层正文开始 200ms 淡入 | D9 |
| 正文超长时 | **行高恒 40px**;正文最多 2 行、超出省略号 | 不扩窗、不改行高;`maxCardHeight` 不参与单条气泡 |
| 展示期 | 倒计时进度每 50ms 更新一次、每次 50ms 线性过渡 | D14 |
| 收尾 | 用户点「知道了/标为已读」或超时:覆盖层 200ms 淡出 → 遮罩反向收拢(错峰 `[120,90,0,90,120]`,每段 360ms)→ 窗口 150ms 淡出并隐藏 | D2/D10/D17 |
| 队列非空 | 上一批收拢到 50% 时启动下一批扫入,交叉 100ms,不得出现空白帧 | D16 |
对应 winisland 路径:`t=0` 时改为胶囊→展开卡弹簧形变(目标宽高由 `ISLAND_SIZE_DELTA` 决定),文字交叉淡入按 `1−1.5p` / `p²`,点「知道了」后 360ms 收回再淡出(A6–A8)。两条路径共享同一消息状态机与队列规则,只有「表现层」不同。
### 10.3 冲突与选择点(必须按默认执行,并保留开关)
| 编号 | 冲突点 | ClassHelper 现状 | ClassIsland 目标 | **默认选择** | 开关/回退 |
|---|---|---|---|---|---|
| X1 | fixed 形态 | 268×44 胶囊 + 详情卡 | 40px 信息行 + 行内气泡 | **classisland:信息行**;winisland:胶囊 | `appearance.family` |
| X2 | 开合实现 | 弹簧形变 | CSS transition(行宽/行高) | classisland 用 CSS;winisland 用弹簧 | `family` |
| X3 | 单条气泡的尺寸 | 卡片长高(最大 ~220) | 40px 行内气泡,正文 2 行省略 | **行高恒 40、不增高、不扩窗**;`maxCardHeight` 只用于 custom 多条列表与强提醒窗口 | `family`;无「增高」回退项(WinIsland 原展开卡保留在 winisland fixed) |
| X4 | 遮罩形态 | 无(直接换卡片) | 斜切 5 区分块 + 文字缩放 | **两风格都实现遮罩**(winisland 的详情卡也用遮罩增强观感,但保留卡片尺寸动画) | `family` 决定是否叠加卡片尺寸动画 |
| X5 | 滚动组件 | 连续 30%→−80% 跑马灯 | 40px/s + 10s 暂停 + 回程 | **默认连续**(ClassHelper 观感),设置项可切 `mode='pause'` | 组件设置 `settings.speed` 已有;新增 `settings.mode`【推断字段名】 |
| X6 | 轮播切换 | 瞬时 | 250ms 上下滑 + 淡入淡出 | **classisland 250ms**;winisland 瞬时 | `family` |
| X7 | 课程进度条 | 1s linear | 50ms linear | **classisland 50ms;winisland 1s** | `family` |
| X8 | 进度条高度 | 2px | 3px | **classisland 3px;winisland 2px** | `family` |
| X9 | 系统减少动态效果 | 岛不受其影响 | 同 ClassHelper(岛自身开关) | **不覆盖岛**:岛动画唯一开关 `appearance.animations`,`speed` 缩放时长;如用户显式要求,可提供 P2 选项 `respectReducedMotion`(默认 false) | `animations` / P2 选项 |
| X10 | 消息列表错峰 | 无 | ClassIsland 无(列表瞬时) | **新增 30ms 错峰但封顶 120ms**,可在 `animations=false` 时关闭 | `animations` |
| X11 | 主题切换 | 瞬时 | 200ms 颜色过渡 | **200ms**(classisland);winisland 瞬时 | `family` |
### 10.4 性能预算与降级矩阵
| 场景 | 目标 | 降级顺序 | 说明 |
|---|---|---|---|
| 弹簧形变(winisland) | 60fps、单帧 ≤8ms | ① 降步进到 30fps ② 只动尺寸不动圆角 ③ `snap()` | 必须在岛渲染进程 rAF,独立于主进程 |
| CSS 行宽过渡(classisland;行高恒 40 不参与过渡) | 60fps | ① 去掉阴影过渡 ② 直接落位 | 只过渡背景层,文字层瞬时;避免触发整行重排 |
| 斜切遮罩(SVG 5 区) | 60fps、单帧 ≤4ms | ① 降为 3 区 ② 降到 30fps ③ 静态遮罩 | 每帧重算 `points`,避免重绘整棵 SVG |
| 消息气泡(40px 行内遮罩 + 2 行正文) | ≥50fps | ① 关闭错峰 ② 关闭文字缩放 ③ 直接切 | 与 11 部分性能基线一致(`parts-r/11-compat-tests.md:46`);气泡不改变行高 |
| 强提醒特效 | ≥50fps | ① 关闭涟漪改静态色块 ② 缩小渲染缩放 ③ 不显示 | 特效窗口独立进程/独立窗口,动画结束即关 |
| 多条列表高度变化(custom) | 60fps | ① 关闭过渡直接跳 ② 限制可见行数 | 窗口先扩再形变,避免裁切;单条气泡不适用 |
| 主窗口隐藏 | 0 动画 | — | 岛仍可见时不得因主窗口隐藏而停;只有岛窗口隐藏/页面隐藏才暂停 |
### 10.5 实现方式规范(逐条遵守)
1. **classisland 优先 CSS**:几何用 `transition`(`width/height/border-radius/opacity/box-shadow/background-color`),分块遮罩用 JS 进度 + SVG `polygon`(现有实现 `LessonReminderContent.vue:59-102` 可直接复用/抽组件),文字用 `@keyframes`(`var(--ci-*)` 时长/曲线)。
2. **winisland 保留弹簧**:`SpringValue` 参数与兜底不得改(`spring.ts:26-114`);`syncTargets` 的 `snap` 分支必须保留。
3. **遮罩复用**:把 `LessonReminderContent.vue` 里的 5 区遮罩抽成 `IslandMask.vue`(props:`open`、`duration`、`stagger`、`fill`、`motion`、`speed`),消息气泡与课程提醒共用;禁止复制第二份实现。
4. **进度条统一样式**:抽 `.ci-progress` 类(高度 `var(--ci-progress-h)`、轨道 `var(--ci-progress-track)`、填充 `var(--ci-progress-fill)`、圆角 `var(--ci-progress-radius)`),用 `transform: scaleX()` 驱动;**不要**用 `width`。
5. **FLIP 只用于重排**:编辑器拖拽排序、组件位置变化用 FLIP(先批量读、再写 transform、下一帧过渡),不得对每帧读布局。
6. **强制落位**:所有 `transition` 在 `data-family='classisland'` 下用 CSS;`animations=false` 时通过根元素 `data-anim='0'` 把 `transition-duration/animation-duration` 覆盖为 `0ms`(参照 `--island-anim` 的做法,`IslandApp.vue:527`),并让 JS 跳过 rAF。
7. **不要动画窗口 bounds**:仅 M0 新建窗口、模式切换、显示器/边距变化时 `setBounds`,且节流 ≥100ms(`parts-r/01-objective.md:81`)。
8. **可取消**:每个动画入口先取消同目标旧动画(`cancelAnimationFrame` + 清 timer + 重置变量),再启动新动画;状态机以「目标值」而不是「播放进度」为准。
9. **无障碍**:`aria-live="polite"` 保留在课程/消息条(`LessonReminderContent.vue:140`);动画不改变语义顺序;`prefers-reduced-motion` 下编辑器 UI 动画压到 1ms(主窗口规则,`index.css:909-917`)。
10. **测试钩子**:在 dev 构建暴露 `window.__islandMotion = { list(), state(), setSpeed(x), disable() }`,供无头验证读取(`--visual` 才做画面验证)。
### 10.6 逐文件改动清单
| 文件 | 动作 |
|---|---|
| `packages/desktop-client/src/island/spring.ts` | **KEEP**;仅允许为 classisland 增加 `snapTo()` 便捷方法(可选),不得改默认参数 |
| `packages/desktop-client/src/island/squircle.ts` | **KEEP(仅 winisland/custom 使用)**;classisland 不调用 |
| `src/island/IslandApp.vue` | **大改**:family 分支;classisland fixed 改为信息行渲染;保留消息状态机与 winisland 胶囊路径;接 `animations/speed` 与暂停 |
| `src/island/CustomIslandRuntime.vue` | **改**:classisland 下行宽用 CSS 过渡、行高恒 40(不随内容增高);保留测量与 winisland 弹簧分支;`motion` 规则扩展 |
| `src/island/ClassicNotifications.vue` | **改**:气泡两阶段(斜切遮罩 + 文字 + 缩放);多行错峰;保留消息类型/排序/回执 |
| `src/island/LessonReminderContent.vue` | **改**:遮罩抽成共用组件;进度 2→3px;标题/缩小时长走 token;变量改名 `--ci-lesson-*`(保留 alias) |
| `src/island/ComponentNode.vue` | **改**:滚动/轮播动画按 family;课程进度 50ms/1s;`--secondary/--emphasized/--large` 改用 `--ci-fs-*` |
| `src/island/CoursewareTransfer.vue` | **改**:K1–K3 入场/退场/进度过渡;其余 KEEP |
| `src/island/classic-island.css` | **改**:classisland 分支 token 化;winisland 分支保持;悬停/按压过渡统一 token |
| `src/island/custom-island.css` / `custom-runtime.css` | **改**:行高/间距/进度条/圆角 token 化;classisland 背景/描边/阴影;预览行 44→40 |
| `src/island/lesson-ending.css` | **改**:徽标颜色 token 化;新增出现动画类(可选) |
| `src/island/tokens.css` | **新增**:`--ci-*` + `--ci-dur-*`/`--ci-ease-*` |
| `src/main/island.ts` | **改**:下发 `family/theme/colorScheme`;classisland 包围盒计算;关闭/淡出时序不变;特效窗口控制 |
| `src/renderer/styles/island-editor.css` | **改**:J1/J3/J4 动画;预览 40px;token 化 |
| `docs/dev/island-refactor.md` | **新增**:动画表、降级矩阵、测试钩子说明 |
### 10.7 验收标准
1. **逐条覆盖**:本节 A1–A11、B1–B5、C1–C5、D1–D17、E1–E5、F1–F8、G1–G6、H1–H4、I1–I6、J1–J6、K1–K3、L1–L8 全部实现;抽测 12 条(覆盖两种 family),时长误差 ≤16ms、曲线一致。
2. **双风格默认值**:`family='classisland'` 下无弹簧形变、无 squircle;行宽过渡 300/550ms;气泡遮罩 360+120、文字 260/250、缩放 750、退出 200;`family='winisland'` 下胶囊开合仍为弹簧、收缩 360ms 后淡出、窗口淡入 +0.16/16ms、淡出 −0.2/16ms。
3. **总开关**:`animations=false` 时所有动画在 1 帧内完成,不出现「终态先行」;rAF/定时器数量归零(DevTools Performance 验证)。
4. **速度**:`speed=2` 时所有时长减半、弹簧更快;`speed=0.5` 时加倍;不得出现某条动画没乘 speed 的不一致。
5. **暂停**:主窗口最小化/岛窗口 `hide()`/`document.hidden` 时无 rAF、无 CSS 动画推进;恢复可见后直接呈现当前状态(不补播)。
6. **可打断**:快速连续展开/收起、连续推送 5 条消息、消息中途标记已读 → 无闪回、无残留、无「停在半路」;队列切换时旧遮罩被取消。
7. **视觉一致**:遮罩 5 区权重与 60° 斜切与 ClassIsland 一致;文字先于底色结束(260ms 延迟)不出现「白字压白底」;Classic 主题下遮罩整块下压/上滑(200ms)。
8. **进度条**:classisland 3px、50ms 线性;winisland 2px、1s 线性;进度条用 `transform` 而非 `width`(Performance 面板无 layout)。
9. **无窗口抖动**:开合/气泡/列表过程中窗口 `setBounds` 调用次数为 0(隐藏帧除外);触摸模式下窗口只增不减。
10. **性能**:主界面过渡 ≥55fps、气泡/强提醒 ≥50fps;单帧主线程 ≤8ms;动画结束无残留 `will-change`/rAF/timer(`parts-r/11-compat-tests.md:46`、`12-acceptance.md:73`)。
11. **无头验证**:`verify:desktop` 默认不创建可见岛即可断言动画状态机与时长(通过测试钩子读取);只有 `--visual` 才做逐帧/截图验收。
12. **文档**:`docs/dev/island-refactor.md` 的动画表与本节一致;`docs/app/island.md` 补充 family 与动画开关说明。
13. **fixed 气泡不增高**:推送超长正文消息,行高恒 40px、正文最多 2 行省略、窗口 bounds 不变;全过程无 `height`/`max-height` 过渡,`maxCardHeight` 未被单条气泡读取。
14. **reduce-motion 口径**:系统开启 `prefers-reduced-motion` 后,岛动画仍按 `appearance.animations`/`speed` 播放(默认不覆盖);只有 P2 选项 `respectReducedMotion=true` 时才按 `animations=false` 落位,且该选项默认关闭。
---
## 11. 兼容性、性能、平台与测试基线
> 本部分规定「重构不能把原来能用的东西弄坏」,以及如何在不打扰用户的前提下验证。所有验证默认必须无头/离屏执行。
### 11.1 兼容矩阵(必须逐项通过)
| 维度 | 必须保持兼容的内容 | 验证方式 |
|---|---|---|
| 配置 | 旧 `config.json.islandLayout`(`fixed`/`custom`、`residentEnabled`、`schemes`/`rows`/`nodes`/`expandedRows`/`appearance`/`lessonReminders`/`pointerFade*`)无损升级 | 迁移单测 + fixtures 回放 |
| 布局模式 | `fixed` 与 `custom` 均可切换、可保存、可重启恢复;两布局共用一个原生岛窗口,切布局不新开窗口 | 无头集成测试 + 主进程单测 |
| 风格 | `family` 只影响 fixed:`winisland` 保持旧胶囊观感,`classisland` 为默认信息行;`custom` 始终 ClassIsland 规格;切换即时生效、可回退 | 截图(仅 `--visual`)+ 视觉回归 |
| 消息 | 通知/作业/叫人的类型、优先级、id 去重、排序、上课暂存、紧急展开、叫人可点胶囊、已读回执、安静模式、教师/班级端差异不变 | 状态机单测 + 无头消息场景 |
| 课程 | 课表解析结果(`ScheduleDto`/`IslandTimePointDto`)、准备/上课/课间/下课前/课中进度提醒、下课前秒数徽标、当前/下一节/今日时间点显示;**行为变更**:custom 不再临时接管岛显示课程提醒(仅保留 ending 徽标,进度由课程组件显示),fixed 按 ClassIsland 接管 | 时间状态机单测 + 假时钟无头场景 |
| 天气 | Open-Meteo 数据、15 分钟刷新、断网缓存标记"缓存"、无官方预警提示 | 无头 mock 网络 + 快照 |
| 课件 | 课件传输状态优先覆盖岛内容、SHA256 校验、回执、不自动运行文件 | 无头课件场景 |
| 编辑器 | 方案/行/组件/导入导出/预览/重置/新建/复制/删除、12 行/200 节点/6 层/20 方案边界 | 单测 + 组件测试 |
| 窗口/输入 | 透明、置顶、不占任务栏、点击穿透、触摸模式、失焦宽限、固定几何护栏、单实例 | 无头窗口参数断言 + 手工触摸说明 |
| 平台 | Windows 10/11(主目标)、macOS、Linux X11;DPI 100/125/150/200;多显示器与热插拔 | 平台矩阵 + 无头几何断言 |
| 性能 | 启动、空闲 CPU、内存、消息风暴、动画帧率 | 性能脚本 + 采样 |
### 11.2 配置迁移与回滚(硬要求)
1. **版本化**:`IslandLayoutSettings.version` 从 `1`(旧配置缺失视为 1)升级到 `2`;迁移函数必须**幂等**(重复执行结果一致)。
2. **迁移顺序**:读取原始 JSON → 备份到 `config.json.bak.<时间戳>`(或配置目录 `backups/`)→ 应用迁移(只补齐/转换,不删除用户数据)→ `normalizeIslandLayouts()` 校验与夹取 → 原子写(临时文件 + rename)。
3. **必须迁移的字段**:
- 顶层新增 `version`、`appearance.family`(老用户默认 `'winisland'`,新安装 `'classisland'`)、`appearance.theme`(`'fluent'`/`'classic'`)、`strongNotification` 开关与参数、`progressBar` 参数、`rowStyle`(行背景/分隔/圆角覆盖);
- scheme 新增 `rowHeight`(默认 40)、`componentGap`(默认 12,对应单侧 6)、`showProgress`(默认 true)、`hideRules` 扩展(保留旧 `in-class`/`no-message`/`no-class`,新增 `full-screen`/`max-window` 需按 12 部分验收);
- 组件新增字段(如 schedule 的 `display` 扩展、countdown 的 `showProgress`、clock 的 `showSeconds`),旧值必须原样保留。
4. **回滚**:迁移失败或新版启动检测到配置不可用时,必须能恢复备份并启动旧结构(主进程启动早期读 `config.json` 的 `islandLayout` 失败时使用默认值 + 保留原文件为 `.corrupt`),不得清空用户方案。
5. **降级兼容**:若旧版本客户端读到新配置,必须忽略未知字段并使用默认值;禁止因未知字段崩溃。版本号高于当前可识别值时,主进程写日志并走只读模式(不自动覆盖新配置)。
6. **fixtures**:`packages/shared` 测试中必须包含:fixed 默认、custom 多方案/多行/嵌套容器/旧 residentEnabled、极端值(超 12 行/200 节点/6 层/颜色非法/坐标为 null)、旧字段缺失、迁移幂等、迁移失败回滚。
### 11.3 IPC 与契约兼容
- 保留现有通道:`island:get-schedules`、`island:get-time-points`、`island:get-layout`、`island:save-layout`、`island:edit-layout`、`island:get-editing`、`island:get-visible`、`island:resident-control`、`island:resident-touch`、`island:set-editing`、`island:content-size`、`island:remove-notification`、`island:push`、`island:class-state`、`island:alive`、`island:get-state`、`island:set-appearance`、`island:get-appearance`、`island:set-interactive`、`island:set-hit-rect`、`island:set-touch-mode`、`island:action`。
- 新增通道必须遵守:`ipcMain.handle/on` 前校验 `event.sender === island.getWindow()?.webContents`(或主窗口 webContents),payload 必须是纯数据且做类型/范围校验;preload 白名单同步更新 `src/preload/island.ts` 与 `src/types/desktop.d.ts`。
- 新增通道建议(如需要):`island:strong-notification`(主进程→岛/强提醒窗口)、`island:get-metrics`(渲染层上报行/组件度量)、`island:preview-mode`(编辑器预览)。不得把 Vue Proxy/函数/类实例跨 contextBridge 传递。
- 渲染进程心跳、幽灵窗口检测、渲染崩溃自愈(限流 3 次/10 分钟)与恢复逻辑保持不变。
### 11.4 性能预算(超预算必须优化)
- 岛渲染进程启动 ≤ 500ms(从窗口创建到首帧可见);冷启动到岛可见 ≤ 3s(不含网络)。
- 空闲 CPU 平均 ≤ 1%(岛可见、时钟/课程正常刷新);消息到达/展开瞬间峰值 ≤ 15% 且 ≤ 500ms。
- 主进程 + 岛渲染进程内存 ≤ 250MB(不含可选强提醒窗口);连续运行 24h 增长 ≤ 10%。
- 消息风暴:1 分钟 20 条不丢、不重复、不卡顿;队列长度上限与丢弃策略必须有日志。
- 动画:主界面过渡 ≥ 55fps;消息气泡/强提醒 ≥ 50fps;不得因背景页(主窗口隐藏)继续动画。
- 包体:本次重构不得新增 > 200KB 的运行时依赖;不得引入新 UI 库/图标库(图标仍用现有方案)。
### 11.5 平台差异与窗口护栏
- Windows:透明/置顶/点击穿透/`setShape`(若用)只在 Windows 分支;Windows 10 不支持 Mica 时退回纯色;高 DPI 下窗口 bounds 用 DIP。
- macOS:透明窗口、置顶、全屏空间行为;不使用 `setBackgroundMaterial`;点击穿透退化为整窗 `setIgnoreMouseEvents(true,{forward:true})` + 命中通道。
- Linux X11:透明窗口与置顶可用;Wayland 明确不支持并给提示(与 ClassHelper 既有说明一致)。
- 多显示器:`position`/`followCursorDisplay`/停靠偏移语义保持不变;显示器热插拔/DPI 变化必须重算包围盒与锚点,节流 ≥100ms。
- 触摸:触摸模式仍以"窗口贴合岛体 + 始终接收输入"为原则;编辑模式与触摸手势不能互相抢占;失焦宽限(鼠标 500ms / 触摸 1200ms)保持不变。
- 强提醒窗口:无边框、置顶、不抢焦点、`skipTaskbar`、独立渲染入口或复用岛窗口的独立层;关闭后释放;不得在编辑器/预览中创建。
### 11.6 测试基线(无头优先)
1. **单元测试(shared)**:`normalizeIslandLayouts` 边界、迁移幂等、版本识别、颜色/数值夹取、组件树限制(12 行/200 节点/6 层/20 方案)、消息排序/去重、课程状态机(准备/上课/课间/下课前/放学/跨天/跨周)。
2. **主进程测试**:窗口几何计算(6 停靠 × DPI × 多屏)、hit rect/触摸模式切换、消息队列与暂存、课程提醒调度、天气缓存、课件覆盖优先级、配置写入原子性与回滚。
3. **组件测试**:固定数据下 `IslandApp.vue`/`CustomIslandRuntime.vue` 的渲染快照(DOM 结构 + 关键 computed 样式),不创建真实窗口(可用 happy-dom/离屏渲染)。
4. **集成(无头)**:`pnpm verify:desktop` 默认后台快速检查——不得创建可见岛/托盘;覆盖启动、布局切换、消息推送、课程提醒、编辑器保存/读取、迁移。
5. **视觉(显式)**:只有 `pnpm verify:desktop:visual` / `--visual` 才允许真实桌面、动画采样、截图;截图输出目录用临时路径;不得把窗口对照图发布为展示图。
6. **数据纪律**:测试使用临时 SQLite/夹具,禁止使用真实班级、真实密码、真实通知;远控/安装器使用假执行器。
7. **证据**:每次汇报附实际命令输出摘要(通过/失败数量),不得沿用历史数字。
### 11.7 观测与诊断
- 主进程日志必须记录:配置迁移(旧→新字段、备份路径、结果)、窗口几何重算原因、消息队列变化(入队/出队/丢弃/去重)、课程状态切换、天气刷新/失败、强提醒创建/销毁、渲染崩溃/重建(限流计数)。
- 岛渲染进程必须上报心跳与度量(行/组件尺寸、可见性);主进程对"幽灵窗口"(渲染卡死但窗口在)执行重建(限流)。
- 诊断页/日志中不得包含用户密码、令牌、完整数据库路径;日志轮转与大小上限沿用现有 logger。
- 提供 `docs/dev/island-refactor.md` 的排障章节:黑屏/透明失效/点击穿透/闪烁/动画卡顿/消息不弹/课程不刷新/迁移失败的症状 → 排查步骤 → 回滚方法。
### 11.8 文档与交付说明
- 更新 `docs/app/island.md`:新默认观感、风格切换、editor 编辑模式、强提醒、行为表(含 fixed 重定义)。
- 更新 `docs/dev/implementation.md`:岛的新架构入口、schema v2、迁移函数位置。
- 新增 `docs/dev/island-refactor.md`:架构图、文件职责、数据契约、动画表、迁移与回滚、测试命令。
- `docs/` 中的展示截图必须来自显式视觉验收且人工确认;不得使用重构中的对照图/失败截图。
- 交付说明必须包含:与 ClassIsland 的差异清单(技术栈/能力取舍)、未实现项及原因、性能数据、回滚步骤。
### 11.9 本部分验收标准
- [ ] fixtures 覆盖 11.2 全部场景,迁移全部通过且幂等。
- [ ] 旧配置在重构版启动后方案、组件、外观、课程提醒开关全部保留,且可切回 `winisland` 观感。
- [ ] 所有既有 IPC 通道仍可用,新增通道有 sender 校验与类型校验。
- [ ] `pnpm verify:desktop` 无参数运行时**不创建可见岛/托盘/声音**;`--visual` 才创建。
- [ ] 多显示器/DPI 切换后岛位置与尺寸正确,无 1px 抖动。
- [ ] 24h 无内存增长 > 10%、无崩溃、无幽灵窗口。
- [ ] 文档更新完成,含迁移/回滚/差异说明。
---
## 12. 验收清单(逐项勾选,不得跳过)
> 所有自动验证默认无头/离屏;`--visual` 才允许真实桌面。任一项未通过都不得声称完成。
### 12.1 工程门禁
- [ ] `pnpm lint` 0 error(warning 需列出)。
- [ ] `pnpm typecheck` 0 error(先构建 shared)。
- [ ] `pnpm build:desktop` 成功;`island.html` 与主窗口产物分别可加载。
- [ ] 未新增 UI 框架/状态库/图标库;新增依赖(若有)≤ 200KB 且说明不可替代性。
- [ ] 只改 `packages/shared`、`packages/desktop-client`、`packages/ui`(如需)与 docs;服务端/Web 管理端零改动。
- [ ] 所有新增 IPC 通道有 sender 校验、payload 类型校验;preload 白名单与 `desktop.d.ts` 同步。
- [ ] 无 `console.log` 残留;日志走统一 logger。
### 12.2 视觉验收(像素级)
在 1920×1080@100%、2560×1440@125%、深色/浅色、`family=classisland`/`winisland`、Fluent/Classic 主题下截取离屏基准图,逐项比对(颜色 ±2/255、尺寸/位置 ±1px、透明度 ±1%):
**fixed(classisland 风格)**
- [ ] fixed 窗口包围盒固定:宽 = min(工作区宽, 960)、高 = 40 + 2×pad(pad 默认 20);消息到达/展开/多条列表均不改变窗口尺寸。
- [ ] 可见行高 40px,行内容左右内边距 12px,最小内容宽 20px;行间距 5px。
- [ ] 行背景:圆角 8px、背景不透明度 0.5、1px 描边、阴影 `0 4 8 2 #48000000`(Fluent)。
- [ ] 字号:次要 14 / 主体 16 / 强调 18 / 超大 20;字重 Medium;正文 `line-height: 1.25`。
- [ ] 进度条:高 3px、贴底、圆角 1.5px、颜色为强调色、按设置精度更新。
- [ ] 默认 1 行 = 日期 + 课程表;无课表时显示占位文本("今天没有课程。"/"今日课程已全部结束。")。
- [ ] 课程表显示当前/下一节/今日时间点模式,当前项展开显示起止时间,其余折叠;课间与"默认隐藏"时间点折叠时隐藏,分割线显示竖线,行动不显示。
- [ ] 下课前秒数徽标:蓝色圆形、显示在课程名右侧、剩余 1 秒显示 `-1`、不显示"即将下课"遮罩、不替换常态组件。
- [ ] 消息气泡:遮罩 5 区权重 `[0,0.1,0.2,0.4,0.2,0.1]`、60° 斜切、区段 360ms + 错峰 120ms、开 QuarticEaseOut / 关 QuadraticEaseIn;文字延迟 260/250ms、scale 1.1→1.0/750ms;退出 200ms `(0.5,0,0.75,0)`;行高 40。
- [ ] 强提醒:全屏/大区域遮罩、超时关闭、不抢焦点、在岛所在显示器出现;关闭后无残影。
- [ ] `winisland` 风格:胶囊 268×44(默认值可配置 220–420 × 36–72)、连续圆角(超椭圆 4.2/14 段)、弹簧形变、纯黑/毛玻璃/主题色渐变、阴影常驻;不得与 classisland 风格混用。
**custom(始终 ClassIsland 规格,family 不影响 custom)**
- [ ] 行高 40、行间距 5、组件外边距 6/6、行背景圆角 8、背景不透明度 0.5。
- [ ] 行结构:`Auto, *, Auto` 三列(左拖动把手 20px / 中组件 / 右行操作区);选中框 1.5px 虚线 `1,1.5`、Opacity 0.75、圆角同背景。
- [ ] 多行、分体岛(separated)、主行标记、允许提醒标记、隐藏规则、组件外观覆盖(字号/前景/背景/圆角/透明度/宽度/对齐)逐项与设置一致。
- [ ] 编辑器中的组件库、行、容器嵌套、选中态、抽屉尺寸与间距符合第 9 部分。
- [ ] 所有组件的默认样式/空数据兜底/超长文本截断/滚动行为符合第 7 部分。
**窗口与层级**
- [ ] 岛窗口无边框、透明、置顶、默认不占任务栏、默认不抢焦点;`winisland` 与 `classisland` 风格均不出现白底/黑边/窗口阴影残影。
- [ ] 6 个停靠位 × 偏移(X/Y)× 多显示器索引正确;`followCursorDisplay` 行为保持。
- [ ] 编辑模式窗口/画布尺寸正确:屏宽 ×(屏高−1px)、四边 60px 内缩、开启命中。
- [ ] 深浅色/主题切换无残留样式;岛动画由 `appearance.animations` 开关控制(系统 `prefers-reduced-motion` 不强制关闭岛动画,遵循 ClassHelper 既有文档约定)。
### 12.3 功能验收
**消息与提醒**
- [ ] 通知/作业/叫人三类消息的图标、标题、正文、优先级、时间显示符合第 8 部分。
- [ ] id 去重;同批消息按"优先级 → 时间"排序;列表"展开更多"与"超出工作区提示"正确。
- [ ] 上课期间普通通知/作业暂存、下课补弹;普通叫人保留可点胶囊;紧急通知/紧急叫人立即展开;与 ClassHelper 现有规则逐条一致。
- [ ] 已读/知道了/标为已读/批处理的语义与通知中心状态一致;本机新录作业指纹去重。
- [ ] 自动收起超时:胶囊 15s、展开 20s、紧急 45s、叫人 90s(按现有常量),可打断、可重入。
- [ ] 强提醒仅对开启"强调提醒"的消息出现;关闭后不出现;超时自动关闭;不阻塞岛与其他应用。
- [ ] 消息风暴 20 条/分钟:不丢、不重复、不卡顿、队列上限有日志。
**课程与天气**
- [ ] 准备/上课/课间/下课前倒数/课中进度五类提醒按设置触发,阈值默认 60s 可调。
- [ ] 课程状态机跨天、跨周、临时课表、预定课表、调课场景正确(与 ClassHelper 现有解析结果一致)。
- [ ] 课程提醒接管:fixed(classisland)按 ClassIsland 行内容接管显示 prepare/on-class/break;custom 不再临时接管整岛(移除 `resident-lesson-page`),进度由课程组件显示,ending 只保留课程名右侧 30×30 蓝色圆形秒数徽标。
- [ ] 天气:Open-Meteo 15 分钟刷新、断网缓存并显示"缓存"、失败重试与降级、无官方气象预警措辞。
- [ ] 课件传输:传输中覆盖岛内容,显示文件名/类型/教师/字节/进度,完成后校验并回执;不自动运行文件。
**编辑器与设置**
- [ ] 设置页:经典/自定义切换、风格切换、外观参数、课程提醒开关、指针避让、动画开关、强提醒开关全部即时保存并生效。
- [ ] 编辑器:方案栏(新建/复制/删除/重命名/导入导出/重置)、组件库 11 类、行(新增/排序/主行/允许提醒/删除)、容器嵌套、拖拽(行内/跨行/库→行/库→容器)、选中与组件设置(内容/外观/尺寸/隐藏条件)、编辑模式预览、预览缩放、退出保存/失败重试。
- [ ] 边界:12 行/200 节点/6 层/20 方案;超限拒绝并提示;非法颜色/数值被夹取;未知组件/未知 Kind 不崩溃。
- [ ] 旧方案导入后可直接渲染(迁移测试)。
### 12.4 动画验收
- [ ] 第 10 部分动画表逐项实现;时长误差 ≤ 16ms,曲线一致(DevTools Animations 或逐帧截图验证)。
- [ ] 岛出现/隐藏、行展开/折叠、行宽/高度、进度条、消息气泡两阶段、强提醒、课程徽标、滚动/轮播、主题切换全部流畅(≥55fps)。
- [ ] 不可打断的动画可被新状态覆盖(不排队、不闪回)。
- [ ] `animations=false` 生效(唯一岛动画开关);`speed` 统一缩放时长;失焦/隐藏页暂停;恢复后不补播。
- [ ] 动画结束无残留 `will-change`、定时器、rAF。
### 12.5 兼容与迁移验收
- [ ] 旧 `fixed`/`custom` 配置 fixtures 升级后:方案、行、组件、外观、课程提醒、指针避让、`residentEnabled` 全部保留;迁移幂等。
- [ ] 升级失败可回滚(备份恢复);不可用时保留原文件为 `.corrupt` 并启动默认配置,不得清空。
- [ ] 新配置被旧客户端读取时忽略未知字段、不崩溃(如能构造旧客户端则测;否则单测模拟)。
- [ ] `winisland` 风格下旧观感/旧交互无回归(对照基线图)。
- [ ] 切换 fixed↔custom、风格切换、方案切换均不新开窗口、不丢配置。
### 12.6 无头与不打扰验收
- [ ] `pnpm verify:desktop`(不带 `--visual`)全程不创建可见岛、托盘气泡、系统通知、声音;测试结束无残留进程/窗口。
- [ ] 使用独立 userData;不触碰真实配置/真实班级/真实通知。
- [ ] `--visual` 仅在显式允许时运行,且截图输出到临时目录;不自动发布。
- [ ] 崩溃自愈限流有效:1 分钟内最多重建 3 次,超过进入安全模式/停止重建。
- [ ] 日志无密码/令牌/敏感路径。
### 12.7 性能验收
- [ ] 岛渲染进程启动 ≤ 500ms;冷启动到岛可见 ≤ 3s。
- [ ] 空闲 CPU ≤ 1%;消息风暴峰值 ≤ 15% 且 ≤ 500ms。
- [ ] 主进程 + 岛渲染进程内存 ≤ 250MB;24h 增长 ≤ 10%;无句柄泄漏。
- [ ] 包体增量 ≤ 200KB(除说明的例外);无新增大图标/字体文件。
### 12.8 文档与交付物
- [ ] `docs/app/island.md`、`docs/dev/implementation.md` 更新;新增 `docs/dev/island-refactor.md`。
- [ ] 架构图、数据契约、动画表、迁移与回滚、测试命令齐全。
- [ ] 交付说明:与 ClassIsland 差异、未实现项、性能数据、回滚步骤。
- [ ] 第 12 部分本清单逐项勾选并附验证输出摘要;未验证项标注原因。
### 12.9 禁止事项(验收时再次确认)
- [ ] 未通过修改验收标准/删除测试来"通过"。
- [ ] 未因重构删除既有功能(消息、课程、天气、课件、编辑、导入导出、指针避让、触摸)。
- [ ] 未引入需要联网/登录的新依赖。
- [ ] 未把 ClassIsland 的 C#/XAML/本地档案代码复制进 ClassHelper。
---
## 13. 重构里程碑(M0–M7)
> 必须按顺序推进;每个里程碑结束都要「可无头运行 + 通过该阶段验收 + 既有功能不回归」。禁止先大改 UI 再补架构与测试。
### M0 基线固化与安全网(0.5–1 天)
- 交付:
- 读通现状并输出文件级计划(第 4 部分)与迁移计划(第 5 部分)。
- 为现有行为建立无头基线:`normalizeIslandLayouts` 边界用例、消息排序/去重、课程状态机、窗口几何计算、旧配置 fixtures(fixed/custom/嵌套/极端值)。
- 记录性能基线:启动耗时、空闲 CPU、内存、消息风暴耗时。
- 在 `verify:desktop` 中补充"不创建可见窗口/托盘"的断言。
- 验证:`pnpm typecheck`、`pnpm verify:desktop`(无头)通过;fixtures 全部可回放。
- 完成标准:没有行为改动;所有基线用例通过;性能数字记录在 `docs/dev/island-refactor.md` 草案。
### M1 共享层 v2:schema、常量、迁移、tokens(1–2 天)
- 交付:
- `packages/shared/src/island-layout.ts`:`version=2`、`appearance.family`、`appearance.theme`、`strongNotification`、`progressBar`、`rowStyle`、scheme 的 `rowHeight/componentGap/showProgress`、组件新字段;`normalizeIslandLayouts()` 迁移+校验(12 行/200 节点/6 层/20 方案、颜色/数值夹取)。
- 常量/token:ClassIsland 行高 40、内边距 12、行距 5、圆角 8、透明度 0.5、字号 14/16/18/20、进度条 3px 等;`winisland` 兼容值保留。
- 迁移函数(幂等)+ 备份/回滚;单元测试覆盖全部 fixtures。
- 验证:单测(迁移、夹取、边界、幂等);`typecheck`。
- 完成标准:旧配置升级后数据无损;新 schema 能被现有代码读取(旧渲染仍可运行);迁移失败可回滚。
### M2 渲染引擎:行/组件/tokens(2–3 天)
- 交付:
- 统一行/组件渲染模型:fixed 与 custom 共用行结构、背景、进度条、选中框、隐藏规则、淡出、圆角/阴影。
- `IslandApp.vue` 重构为 ClassIsland 信息行(默认日期+课程表),`CustomIslandRuntime.vue` 行/组件按新规格;`ComponentNode.vue` 11 类组件默认样式/空数据/布局约束。
- `family` 双轨渲染仅作用于 fixed(classisland 信息行 / winisland 旧胶囊);custom 统一 ClassIsland 规格;默认按用户升级策略选择;设置页可切换。
- DOM/CSS token 结构(第 6/7 部分)落地;旧 `winisland` 视觉保持。
- 验证:组件快照测试(关键 computed 样式/DOM 结构);无头启动渲染;`--visual` 像素基准(1920×1080、深/浅色、两风格)。
- 完成标准:两风格可切换且互不污染;行/组件像素值达标;旧观感无回归。
### M3 消息、课程提醒、强提醒、天气、课件(2–3 天)
- 交付:
- 气泡两阶段(遮罩斜切 360/120ms、文字 260/250ms、scale 1.1→1.0/750ms、退出 200ms)与队列/去重/暂存/紧急/叫人规则。
- 课程提醒五类 + 下课前蓝色秒数徽标 + 课程进度条。
- 强提醒(Topmost)独立透明窗口:全屏/大区域、超时关闭、不抢焦点、多显示器、仅在强调提醒开启时出现;编辑/预览不创建。
- 天气显示/缓存标记;课件覆盖优先与进度。
- 验证:消息/课程单测(假时钟、假队列);无头推送场景;`--visual` 气泡/强提醒动画采样。
- 完成标准:ClassHelper 既有消息/课程规则逐条不变;视觉/动画达标;强提醒不影响其他应用。
### M4 设置页与编辑器重构(2–3 天)
- 交付:
- `IslandSettingsView.vue`:经典/自定义切换、风格切换、外观参数(字号阶梯/行距/进度条/强提醒)、课程提醒、指针避让、动画开关;即时保存、校验、回显。
- `IslandEditorView.vue`:方案栏、组件库、行/容器、拖拽、组件设置抽屉、编辑模式预览、预览缩放、退出保存/失败重试;与 ClassIsland 编辑模式对齐(选中框、行设置、添加行、组件边距)。
- `IslandComponentSettings.vue`:字段与第 7 部分一一对应;未知字段保留。
- 验证:组件测试 + 无头保存/读取 + 迁移后旧方案编辑;`--visual` 编辑器截图。
- 完成标准:所有设置即时生效;编辑器可用、保存可靠、退出不丢;12 行/200 节点/6 层/20 方案边界提示。
### M5 动画、主题、细节打磨(1–2 天)
- 交付:
- 第 10 部分动画表逐项;60fps;可打断;`animations=false`/减少动画/失焦/隐藏页暂停。
- Fluent/Classic 两套岛主题;`winisland` 玻璃/纯黑/渐变保持;主题切换无闪烁。
- 交互细节:点击区域、悬停、触摸、键盘、拖拽阈值、选中/取消、右键(如适用)。
- 验证:DevTools/帧采样;逐项动画检查;像素 diff;无头回归。
- 完成标准:动画误差 ≤16ms、帧率达标;主题/风格切换无残留;减少动画生效。
### M6 迁移、兼容与回归(1–2 天)
- 交付:
- 迁移 fixtures 全量回放(fixed/custom/旧 residentEnabled/嵌套容器/极端值/非法数据);回滚演练。
- `winisland` 观感对照基线;旧交互逐条回归(消息/课程/天气/课件/编辑器/触摸/多屏)。
- 旧版本读新配置的兼容处理;日志/诊断/文档更新。
- 验证:迁移单测 + 回滚测试 + `pnpm verify:desktop` + `--visual` 对照。
- 完成标准:旧配置 100% 无损;回滚可用;旧观感/旧交互无回归。
### M7 全量验收与交付(1–2 天)
- 交付:
- 第 12 部分逐项勾选;性能数据(启动/CPU/内存/帧率/消息风暴);差异说明;回滚说明。
- `pnpm lint`、`pnpm typecheck`、`pnpm build:desktop`、`pnpm verify:desktop`(无头)、`--visual` 视觉验收记录。
- `docs/app/island.md`、`docs/dev/implementation.md`、`docs/dev/island-refactor.md` 完成。
- 验证:按第 12 部分执行;未通过项必须列出原因与修复计划。
- 完成标准:第 12 部分全通过(或明确标注例外并获得确认);无可见窗口打扰的默认验证可重复通过。
### 里程碑风险提示
- M1 迁移必须先于任何渲染改动,否则旧配置会在开发中损坏。
- M2/M3 必须保留 feature flag(`family` 与内部开关),确保随时可回退到旧渲染。
- M4 编辑器依赖 M1 的 schema 与 M2 的渲染组件;不得并行乱改。
- M5/M6 的像素/动画验收需要稳定的基准图与固定测试数据,禁止用真实班级数据。