- 重构版主提示词(14 parts,面向 ClassHelper 灵动岛重构) - ClassIsland 全量规格提示词(20 parts,含分卷与逐章版本) - 投喂指南、分卷、逐节文件 - 调研记录、一致性基线、生成与质检脚本
70 KiB
10. 动画与动效映射(ClassHelper 现状 → ClassIsland 目标;双风格默认值)
本节是动效施工图。实现者必须先把「风格开关」接好,再按表逐条替换动画:family='classisland'(默认)走 ClassIsland 规格的 CSS transition/keyframes + 遮罩分块;family='winisland'(老用户兼容)保留现有弹簧形变与胶囊开合。同一条动画不得同时用两套实现,由 data-family 与 appearance.animations 决定分支。
10.0 总则与不可协商的规则
- 动画级别只有一个开关:岛的动画由
appearance.animations(boolean)总控,不是 ClassIsland 的AnimationLevel(那是主窗口的设置)。animations=false时所有过渡必须「瞬落」:弹簧snap()、CSStransition/animation: 0ms、遮罩直接设终值(现有实现见IslandApp.vue:249-256、LessonReminderContent.vue:77-80)。 - 速度倍率:
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。 - 弹簧仅限 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)。 - 窗口几何不动画:开合动画全部发生在固定包围盒内部(
main/island.ts:1412-1417)。classisland 的 fixed 信息行也必须沿用这一约束:行宽/气泡变化在渲染层完成,窗口只在「不可见时」或「模式/设置变化」时setBounds(island.ts:1476-1479,1598-1612)。fixed 单条气泡的行高恒为 40px,气泡(遮罩/内容/进度条)全部在 40px 行内完成,不因内容改行高、也不因此扩窗;maxCardHeight只用于 custom 的多条消息列表与强提醒窗口的尺寸计算。唯一例外是「多条消息列表」比普通形态更高,窗口必须在形变开始前先长高(island.ts:1514-1518),与单条气泡无关。 - 不可打断的动画可被新状态覆盖:新状态直接接管(取消旧 rAF/定时器、重设目标),不得排队、不得闪回。现有
syncTargets(snap)、animate()、animateMask()都按此实现(IslandApp.vue:238-264、CustomIslandRuntime.vue:127-177、LessonReminderContent.vue:73-102)。 - 隐藏页/窗口不可见必须暂停:custom 的
motion = animations && !document.hidden && nativeVisible(CustomIslandRuntime.vue:57-58);classic 通过props.suspended停 rAF(IslandApp.vue:240-246)。新增动画(遮罩、气泡、强提醒)同样要遵守:页面隐藏时不得继续跑 rAF;恢复可见时按当前状态直接落位(不要补播)。 - 减少动态效果(prefers-reduced-motion)不覆盖岛:ClassHelper 明确「应用内动画开关决定岛动画,不能用主窗口的减少动态效果说明替代岛自身规则」(
docs/app/island.md:57;docs/dev/motion.md:78)。岛动画的唯一开关是appearance.animations,appearance.speed统一缩放时长;实现者不得让系统 reduced-motion 强制关闭岛动画或改写其时长,只在设置页给出提示文案。若未来用户明确要求「尊重系统设置」,只能作为 P2 可选项(设置项respectReducedMotion,默认false),默认行为保持不变。 - 帧预算:岛渲染进程 60fps;任意单帧主线程 ≤ 8ms;动画只作用于
transform/opacity/clip-path/box-shadow(颜色类)与 SVGpoints(遮罩),禁止逐帧改width/height/top/left触发重排(弹簧形变改的是 SVG path 与元素尺寸,属于既有例外,仅 winisland 使用)。 - 兜底定时器:任何依赖 rAF 的动画必须有「到点直接落位」的兜底,因为 Windows 会把置顶透明小窗判定为被遮挡而把 rAF 压到 1fps(
IslandApp.vue:266-286的MORPH_FALLBACK_MS=1200)。classisland 的 CSS transition 天然有终点,但遮罩(SVG 分块)与任何 JS 驱动的动画仍需兜底(现有 600/speed,LessonReminderContent.vue:82-89)。 - 动画结束清场:动画结束必须:移除临时
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 实现方式规范(逐条遵守)
- classisland 优先 CSS:几何用
transition(width/height/border-radius/opacity/box-shadow/background-color),分块遮罩用 JS 进度 + SVGpolygon(现有实现LessonReminderContent.vue:59-102可直接复用/抽组件),文字用@keyframes(var(--ci-*)时长/曲线)。 - winisland 保留弹簧:
SpringValue参数与兜底不得改(spring.ts:26-114);syncTargets的snap分支必须保留。 - 遮罩复用:把
LessonReminderContent.vue里的 5 区遮罩抽成IslandMask.vue(props:open、duration、stagger、fill、motion、speed),消息气泡与课程提醒共用;禁止复制第二份实现。 - 进度条统一样式:抽
.ci-progress类(高度var(--ci-progress-h)、轨道var(--ci-progress-track)、填充var(--ci-progress-fill)、圆角var(--ci-progress-radius)),用transform: scaleX()驱动;不要用width。 - FLIP 只用于重排:编辑器拖拽排序、组件位置变化用 FLIP(先批量读、再写 transform、下一帧过渡),不得对每帧读布局。
- 强制落位:所有
transition在data-family='classisland'下用 CSS;animations=false时通过根元素data-anim='0'把transition-duration/animation-duration覆盖为0ms(参照--island-anim的做法,IslandApp.vue:527),并让 JS 跳过 rAF。 - 不要动画窗口 bounds:仅 M0 新建窗口、模式切换、显示器/边距变化时
setBounds,且节流 ≥100ms(parts-r/01-objective.md:81)。 - 可取消:每个动画入口先取消同目标旧动画(
cancelAnimationFrame+ 清 timer + 重置变量),再启动新动画;状态机以「目标值」而不是「播放进度」为准。 - 无障碍:
aria-live="polite"保留在课程/消息条(LessonReminderContent.vue:140);动画不改变语义顺序;prefers-reduced-motion下编辑器 UI 动画压到 1ms(主窗口规则,index.css:909-917)。 - 测试钩子:在 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 验收标准
- 逐条覆盖:本节 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、曲线一致。
- 双风格默认值:
family='classisland'下无弹簧形变、无 squircle;行宽过渡 300/550ms;气泡遮罩 360+120、文字 260/250、缩放 750、退出 200;family='winisland'下胶囊开合仍为弹簧、收缩 360ms 后淡出、窗口淡入 +0.16/16ms、淡出 −0.2/16ms。 - 总开关:
animations=false时所有动画在 1 帧内完成,不出现「终态先行」;rAF/定时器数量归零(DevTools Performance 验证)。 - 速度:
speed=2时所有时长减半、弹簧更快;speed=0.5时加倍;不得出现某条动画没乘 speed 的不一致。 - 暂停:主窗口最小化/岛窗口
hide()/document.hidden时无 rAF、无 CSS 动画推进;恢复可见后直接呈现当前状态(不补播)。 - 可打断:快速连续展开/收起、连续推送 5 条消息、消息中途标记已读 → 无闪回、无残留、无「停在半路」;队列切换时旧遮罩被取消。
- 视觉一致:遮罩 5 区权重与 60° 斜切与 ClassIsland 一致;文字先于底色结束(260ms 延迟)不出现「白字压白底」;Classic 主题下遮罩整块下压/上滑(200ms)。
- 进度条:classisland 3px、50ms 线性;winisland 2px、1s 线性;进度条用
transform而非width(Performance 面板无 layout)。 - 无窗口抖动:开合/气泡/列表过程中窗口
setBounds调用次数为 0(隐藏帧除外);触摸模式下窗口只增不减。 - 性能:主界面过渡 ≥55fps、气泡/强提醒 ≥50fps;单帧主线程 ≤8ms;动画结束无残留
will-change/rAF/timer(parts-r/11-compat-tests.md:46、12-acceptance.md:73)。 - 无头验证:
verify:desktop默认不创建可见岛即可断言动画状态机与时长(通过测试钩子读取);只有--visual才做逐帧/截图验收。 - 文档:
docs/dev/island-refactor.md的动画表与本节一致;docs/app/island.md补充 family 与动画开关说明。 - fixed 气泡不增高:推送超长正文消息,行高恒 40px、正文最多 2 行省略、窗口 bounds 不变;全过程无
height/max-height过渡,maxCardHeight未被单条气泡读取。 - 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 配置迁移与回滚(硬要求)
- 版本化:
IslandLayoutSettings.version从1(旧配置缺失视为 1)升级到2;迁移函数必须幂等(重复执行结果一致)。 - 迁移顺序:读取原始 JSON → 备份到
config.json.bak.<时间戳>(或配置目录backups/)→ 应用迁移(只补齐/转换,不删除用户数据)→normalizeIslandLayouts()校验与夹取 → 原子写(临时文件 + rename)。 - 必须迁移的字段:
- 顶层新增
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),旧值必须原样保留。
- 顶层新增
- 回滚:迁移失败或新版启动检测到配置不可用时,必须能恢复备份并启动旧结构(主进程启动早期读
config.json的islandLayout失败时使用默认值 + 保留原文件为.corrupt),不得清空用户方案。 - 降级兼容:若旧版本客户端读到新配置,必须忽略未知字段并使用默认值;禁止因未知字段崩溃。版本号高于当前可识别值时,主进程写日志并走只读模式(不自动覆盖新配置)。
- 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 测试基线(无头优先)
- 单元测试(shared):
normalizeIslandLayouts边界、迁移幂等、版本识别、颜色/数值夹取、组件树限制(12 行/200 节点/6 层/20 方案)、消息排序/去重、课程状态机(准备/上课/课间/下课前/放学/跨天/跨周)。 - 主进程测试:窗口几何计算(6 停靠 × DPI × 多屏)、hit rect/触摸模式切换、消息队列与暂存、课程提醒调度、天气缓存、课件覆盖优先级、配置写入原子性与回滚。
- 组件测试:固定数据下
IslandApp.vue/CustomIslandRuntime.vue的渲染快照(DOM 结构 + 关键 computed 样式),不创建真实窗口(可用 happy-dom/离屏渲染)。 - 集成(无头):
pnpm verify:desktop默认后台快速检查——不得创建可见岛/托盘;覆盖启动、布局切换、消息推送、课程提醒、编辑器保存/读取、迁移。 - 视觉(显式):只有
pnpm verify:desktop:visual/--visual才允许真实桌面、动画采样、截图;截图输出目录用临时路径;不得把窗口对照图发布为展示图。 - 数据纪律:测试使用临时 SQLite/夹具,禁止使用真实班级、真实密码、真实通知;远控/安装器使用假执行器。
- 证据:每次汇报附实际命令输出摘要(通过/失败数量),不得沿用历史数字。
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 lint0 error(warning 需列出)。pnpm typecheck0 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.vue11 类组件默认样式/空数据/布局约束。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玻璃/纯黑/渐变保持;主题切换无闪烁。 - 交互细节:点击区域、悬停、触摸、键盘、拖拽阈值、选中/取消、右键(如适用)。
- 第 10 部分动画表逐项;60fps;可打断;
- 验证: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 的像素/动画验收需要稳定的基准图与固定测试数据,禁止用真实班级数据。