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

1036 lines
114 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.
## 3. ClassIsland 灵动岛行为基准(重构对照规格)
本节是**行为基准**,不是像素规范。实现者(ChatGPT)在 ClassHelper 现有仓库内重构灵动岛时,必须让「`fixed` 默认风格(ClassIsland 信息行)」与「`custom` 常驻模式」在**行为语义**上与 ClassIsland 主界面(MainWindow / MainWindowLine / 组件系统 / 提醒系统)对齐;`fixed` 的 `winisland` 兼容风格只保留原胶囊交互即可,不要求对齐 ClassIsland。具体颜色、间距、圆角、阴影、字号、动画曲线上限等像素值一律以第 6 章(tokens)、第 7 章(结构与组件)、第 10 章(动画)为准,本节只给出「必须实现什么行为、判定顺序、默认值与边界」。凡本节与 ClassIsland 源码冲突,以源码行为为准;凡与 ClassHelper 既有业务规则冲突,以第 8 章的裁决表为准(ClassHelper 业务优先、ClassIsland 视觉优先)。
#### 3.0.1 与 ClassHelper 三种运行形态的映射(口径,必须一致)
| ClassHelper 形态 | 目标语义 | 说明 |
|---|---|---|
| `fixed` + 默认风格 `classisland` | **ClassIsland 风格信息行** | 默认 1 行 = 日期 + 课程表;行高 40、圆角 8、背景不透明度 0.5、底部 3px 进度条;消息到来时**在同一行内**展开 ClassIsland 风格气泡(遮罩阶段 + 正文阶段),暂存/紧急/叫人/已读/去重规则沿用 ClassHelper |
| `fixed` + 风格 `winisland` | **原「胶囊 + 详情卡」形态** | 老用户升级默认保持 `winisland`;只做必要的动画/修 bug,不按 ClassIsland 规格重做几何与气泡 |
| `custom` | **常驻多行** | 保留 schemes/rows/nodes/expandedRows/编辑器数据模型,按 ClassIsland 的行/组件/编辑/隐藏/动画规格重构渲染与交互 |
风格枚举落盘字段以 parts-r/05 为准(本节统一写作 `appearance.family = 'classisland' | 'winisland'`,对应配置键 `islandAppearance.style`);新安装默认 `classisland`,升级用户默认 `winisland`。下文凡写「`fixed`」而未特别说明,均指默认风格 `classisland` 的信息行。
证据写法:`repos/ClassIsland/...` 指本工作区内的 ClassIsland 源码;`packages/...` 指 ClassHelper 源码。未在源码中确认的写【推断】。
### 3.1 主界面行(行为模型)
**必须实现的模型**:主界面是一个**多行容器**,每行(MainWindowLine)包含「背景层 + 内容层(组件集合) + 提醒层(遮罩/正文) + 进度条」。行是提醒的宿主,也是编辑的最小单位。
1. **行高固定 40**,且同一行内所有组件共享该高度;组件区左右内边距 12、最小内容宽 20(`repos/ClassIsland/ClassIsland/Controls/MainWindowLine.axaml:16`;`repos/ClassIsland/ClassIsland/XamlThemes/FluentTheme/Styles.axaml:110`)。
2. **行数无硬上限**,但默认只有 1 行;行间距默认 5(`repos/ClassIsland/ClassIsland/Models/Settings.cs:219`,范围 0–150、步进 5)。
3. **必须存在且只存在一行「主行」**(MainLine):删除到只剩 1 行时不得再删,必须提示「至少需要保留 1 个主界面行。」(`repos/ClassIsland/ClassIsland/Views/EditModeView.axaml:428` 文案,`MainWindow.axaml.cs` 的删除守卫)。
4. **行模板三分支**(必须按条件选,不得混用,`repos/ClassIsland/ClassIsland/XamlThemes/FluentTheme/Styles.axaml:275-278`):
- Full:`IsNotificationEnabled = true`,含遮罩/正文/进度条;
- Min:`IsNotificationEnabled = false`,只有背景与内容;
- 分体(Separated):`IsIslandSeperated=true` 或行级 `IslandSeparationMode=2`,整行背景改为每个顶级组件各自胶囊。
5. **行的可见性**(必须逐条实现,`repos/ClassIsland/ClassIsland/Controls/MainWindowLine.axaml:29-49`):
```
visible = ( (!IsAllComponentsHid || IsOverlayOpen) && (HideOnRule ? IsVisible : true) ) || IsEditMode
```
- `IsAllComponentsHid`:该行所有组件都被隐藏时整行消失,不能留一条空胶囊;
- `IsOverlayOpen`:该行正在播提醒时必须可见(即使组件全隐藏);
- `HideOnRule`:该行命中隐藏规则集时隐藏;规则集变化要实时重算(订阅规则状态更新);
- 编辑模式下**永远可见**。
6. **行级不透明度**默认 1.0(范围 0.1–1、步进 0.05),**只作用于行内容**;背景不透明度是另一个独立设置(默认 0.5),**只作用于背景层**,文字必须始终 100% 不透明(`repos/ClassIsland/ClassIsland/Models/Settings.cs:64,183-184`)。
7. **鼠标淡出**(必须实现):`IsMouseInFadingEnabled`(默认 true)与 `IsMouseInFadingReversed`(默认 false)决定「指针不在行内时行变淡到 0.05」,判定用行的屏幕矩形(含 DPI 与缩放),编辑模式不淡出(`repos/ClassIsland/ClassIsland/Controls/MainWindowLine.cs:503-509,619-637`)。
8. **行背景宽度 = 内容实际宽度**,不是窗口宽度;内容宽度变化必须平滑跟随(Fluent 300ms / Classic 550ms),且**不得触发窗口尺寸变化**(`repos/ClassIsland/ClassIsland/Controls/MainWindowLine.cs:614-617`;动画细节见第 10 章)。
9. **行的提醒宿主能力**:`AcceptsNotificationRequests = IsNotificationEnabled && !IsAllComponentsHid && !_isOverlayOpen && !_isUnloading`——只有同时满足这些条件的行才能接收提醒;多行时由提醒分发规则决定落到哪一行(`repos/ClassIsland/ClassIsland/Controls/MainWindowLine.cs:814`)。
10. **行级设置**(必须保留在编辑器中可改):不透明度、是否允许显示提醒、隐藏规则、圆角/间距(继承全局或覆盖)、附加设置(见第 7 章)。
### 3.2 组件系统(11 类 + 容器)
必须实现 ClassIsland 的 11 种内置组件与 4 种容器(`repos/ClassIsland/ClassIsland/Controls/Components/`),并保留 ClassHelper 现有的同名组件作为承载:
| 组件 | 职责(行为) | 关键默认值 |
|---|---|---|
| 日期 `DateComponent` | 显示「今天」或自定义格式日期;支持农历/自定义模板(模板语法按组件设置) | 显示今天 |
| 时钟 `ClockComponent` | 显示当前时间;支持 12/24 小时、秒、自定义格式 | 24 小时制、显示秒 |
| 倒计时 `CountDownComponent` | 目标时间倒计时;目标可固定日期或「按课表倒数至上课/下课/放学」 | 见 3.7 |
| 课程表 `ScheduleComponent` | 今日时间点/当前课程/下一节,见 3.7 | 见 3.7 设置表 |
| 天气简报 `WeatherComponent` | 天气图标 + 温度 + 文本;多种主信息模式 | 见 3.10 |
| 文本 `TextComponent` | 静态文本,支持模板变量与滚动 | 见组件设置 |
| 滚动容器 `RollingComponent` | 单行内容超宽时循环滚动 | 见组件设置 |
| 轮播容器 `SlideComponent` | 多个子组件按间隔轮播 | 见组件设置 |
| 分组容器 `GroupComponent` | 子组件横向成组,统一间距 | 见组件设置 |
| 堆叠容器 `StackComponent` | 子组件纵向堆叠 | 见组件设置 |
| 分割线 `SeparatorComponent` | 竖线分隔,2px 宽 25px 高(视觉细节归第 7 章) | — |
**必须实现的组件通用语义**:
1. **组件宿主间距**:行内相邻组件水平间距 12(每个组件 `Margin="6 0"`);容器组件内部用 `Margin="-6 0"` 抵消,避免嵌套累积(`repos/ClassIsland/ClassIsland/MainWindow.axaml:396`)。
2. **组件可见性**:组件自身有 `IsVisible`;行内「全部组件都不可见」→ 整行 `IsAllComponentsHid`(见 3.1)。
3. **组件级隐藏规则**(`custom` 模式必须对齐):按 `in-class`(上课中)、`no-message`(无消息)、`no-class`(无课/放学后)三个条件隐藏;ClassHelper 现有 `advanced.hideRules` 的三值语义与之一一对应,必须保留。
4. **组件宽高**:`advanced.align/width/maxWidth` 必须生效;`stretch` 为默认,剩余空间按 flex 分配。
5. **组件的附加设置**:附加设置可挂在科目/时间点/课表/时间表(ClassIsland 语义,见第 7 章),本重构只保留 ClassHelper 已有的「课程控件附加设置」语义,不引入 ClassIsland 本地档案。
6. **编辑态**:所有组件必须可被点击选中、可拖动排序、可跨行移动;容器组件展开子项供选择(第 9 章编辑器规范)。
7. **空数据兜底**:任何组件在无数据时必须渲染占位(不得空白、不得抛错),占位文案与场景见第 7 章。
### 3.3 编辑模式(行为语义)
必须实现「编辑模式」作为岛的唯一配置入口,行为如下(`repos/ClassIsland/ClassIsland/MainWindow.axaml.cs:510-528,1391-1403`):
1. **进入条件**:菜单「编辑主界面…」或 URI `classisland://app/edit`;进入前必须通过权限校验(ClassHelper 侧对应「已登录班级设备」判断)。
2. **进入步骤(按序)**:置编辑标志 → 激活窗口 → 挂载编辑视图 → 同步特效窗口的编辑态(编辑期间**不播强提醒特效**)→ 更新窗口层级 → 编辑模式下窗口必须接收鼠标。
3. **编辑画布**:画布覆盖整个屏幕;岛内容在画布内按停靠位置排布,并预留底部操作栏空间(操作栏高度 64 的语义见第 7/9 章);编辑模式内容整体垂直居中。
4. **编辑态可见性**:所有行、所有组件(含被隐藏规则隐藏的)都必须可见可选中;隐藏规则命中时以半透明/斜纹提示,而不是直接消失。
5. **底部操作栏(行为)**:重置视图、外观抽屉、添加组件抽屉、组件配置方案、完成;「完成」退出并**保存**。
6. **两级抽屉**:主抽屉(外观/添加组件/配置方案)与次抽屉(组件设置);抽屉打开时挂载「逐项入场」动画;拖拽组件时主抽屉自动临时收起,结束后恢复。
7. **退出**:关闭抽屉 → 清编辑标志 → 清教程提示 → **保存组件配置**(保存必须幂等、原子:写临时文件再替换);退出后恢复逐像素透明与点击穿透。
8. **编辑期间**:不得创建额外桌面窗口;不得因编辑导致提醒被丢弃(提醒进队列,退出后按规则补播);强提醒特效丢弃但提醒本体照常。
9. **配置方案**:支持多套组件配置方案(ClassHelper 现有 `schemes`),方案切换即时生效并持久化;方案 JSON 导入导出走本机配置,不上服务端。
### 3.4 隐藏与显示规则(完整判定)
必须实现两档 `hideMode`(默认 `0`,`repos/ClassIsland/ClassIsland/Models/Settings.cs:185`;`repos/ClassIsland/ClassIsland/MainWindow.axaml:150-163`):
- **0 基本**:三个独立开关,全部默认 `false`:
- `HideOnClass`:当前时间状态为「上课」时隐藏;
- `HideOnMaxWindow`:前台窗口最大化时隐藏;
- `HideOnFullscreen`:前台窗口全屏时隐藏。
- **1 高级**:用规则集 `HideRules` 求值(规则集/逻辑见第 8 章的规则语义;ClassHelper 侧只保留独立开关 + 上课状态条件,不引入完整规则集)。
**检测数据源与频率**:
1. 全屏/最大化状态在**每个主计时器 tick** 检测,基准屏幕必须是「岛所在屏幕」(`repos/ClassIsland/ClassIsland/MainWindow.axaml.cs:451-456`)。
2. 排除进程名单 `ExcludedFullscreenWindow` 默认 `["explorer"]`:这些进程在前台时不算全屏/最大化(`repos/ClassIsland/ClassIsland/Models/Settings.cs:56-59`)。
3. **强制显示**(覆盖隐藏):正在播放提醒或处于编辑模式时,即使命中隐藏条件也必须显示,并临时置顶(见 3.9 的置顶锁语义)。
4. **手动隐藏**:`IsMainWindowVisible`(默认 true)为用户手动开关;手动隐藏时同时不接受新提醒(与 3.1 第 9 条一致)。
5. **全部隐藏**:所有行都隐藏时,可见内容 `opacity:0`,**窗口包围盒不变、窗口不销毁**,保证提醒到来能立刻显示(`repos/ClassIsland/ClassIsland/MainWindow.axaml:135-141`)。
6. **ClassHelper 映射要求**:
- `custom` 模式的「常驻」不能被 `HideOnClass` 直接隐藏——上课时只允许按 `hideRules:in-class` 隐藏**指定组件**;
- `fixed` + `classisland` 信息行按 ClassIsland 默认(`HideOnClass=false`)**上课也保持可见**,只是「普通消息暂存、紧急/叫人例外」;不得回到「上课即整岛隐藏」;
- `fixed` + `winisland` 兼容形态保留 ClassHelper 原规则(上课时普通消息暂存并隐藏岛体,紧急与叫人例外,见 3.9 与第 8 章)。
### 3.5 多显示器、停靠与点击穿透
1. **6 个停靠位置**:顶/底 × 左/中/右(`0/1/2` 贴工作区上边、`3/4/5` 贴下边;左/中/右分别为贴左/居中/贴右),窗口始终横跨整屏,岛在窗口内形变(`repos/ClassIsland/ClassIsland/Models/Settings.cs:48`)。
2. **偏移**:`WindowDockingOffsetX/Y` 必须生效;顶部停靠时正值向下偏移不得被屏幕边缘裁掉(偏移吞进安全区)。
3. **多显示器**:`WindowDockingMonitorIndex` 选择目标屏;可选「跟随鼠标所在屏」(ClassHelper 现有 `followCursorDisplay` 保留)。
4. **DPI**:所有几何与命中必须按「物理像素 × 缩放」换算,4K@150% 与 1080p@100% 下不得出现 1px 白边、黑边、圆角锯齿;圆角为 0 时是直角,为最大值时左右边缘为半圆。
5. **点击穿透**:非编辑模式下,岛默认鼠标穿透(点击落到桌面应用);只有指针真正在岛体上时才接收鼠标;编辑模式下必须接收(`repos/ClassIsland/ClassIsland/MainWindow.axaml.cs:839`;ClassHelper `packages/desktop-client/src/main/island.ts:2000-2080` 的命中轮询必须保留)。
6. **置顶锁**:提醒播放期间临时置顶,多提醒时以「锁计数」方式管理——只有最后一个锁释放才还原层级,避免闪烁(`repos/ClassIsland/ClassIsland/MainWindow.axaml.cs:1056-1092`)。
### 3.6 时间状态机与跨天(课程计算的唯一事实源)
1. **主计时器周期 50ms**,优先级 Render;每 tick 顺序:`PreMainTimerTicked` → 计算 → `PostMainTimerTicked`(`repos/ClassIsland/ClassIsland/Services/LessonsService.cs:39-45,373-388`)。ClassHelper 映射为**主进程单一 1s/50ms 混合节拍**(秒级 UI 用 1s,跨点判定用 50ms【推断:可降到 200ms,见 3.13 性能非功能】)。
2. **时间状态枚举**:`None / OnClass / PrepareOnClass(预留) / Breaking / AfterSchool`(`repos/ClassIsland/ClassIsland.Shared/Enums/TimeState.cs:6-27`)。
3. **每 tick 计算顺序**(必须逐条实现):
1. 清理过期临时课表/临时层/临时课表群;
2. 若「加载课表」关闭 → 当前课表为空;
3. 选当天课表(预定 > 临时 > 常规,详见 parts/06 §6.2);
4. `now = 当前本地时间`;
5. 当前时间点 = 有效时间点中第一个 `TimeType∈{0,1}` 且 `StartTime ≤ now ≤ EndTime`;
6. 上课 → `CurrentState=OnClass` 并映射科目;课间 → `CurrentState=Breaking`,`CurrentSubject=课间科目`(名称=课间名);
7. 下一节 = 第一个 `TimeType=0 && EndTime ≥ now`;下一个课间 = 第一个 `TimeType=1 && EndTime ≥ now`;
8. 剩余时间:上课时算「距下课」,否则算「距上课」;**下限钳 0**;
9. 下一节与下一课间都不存在 → `AfterSchool`;
10. 统一回写(未算出用默认值);状态变化时才发事件,顺序固定「先 `CurrentTimeStateChanged`,再按新状态发 `OnClass` / `OnBreakingTime` / `OnAfterSchool`」。
4. **有效时间点的双向裁剪**:禁用一节早课 → 其前所有时间点不显示;禁用一节晚课 → 其后所有时间点不显示(`repos/ClassIsland/ClassIsland.Shared/Models/Profile/ClassPlan.cs:54-98`)。ClassHelper 侧映射为「`ScheduleDto` 投影 + 当前时间点/下一节,由服务端解析结果决定」,**不得在客户端重写轮换规则**。
5. **跨天**:每次 tick 检测日期变化;跨天必须重新取当天课表、清除「今天」的已提醒标记、重置课程提醒状态机(`LessonReminderTracker` 的 `dayKey` 变化),不得沿用昨天的「已提醒」状态。
6. **时间突变**(手动改系统时间、NTP 校正、睡眠唤醒):下一次 tick 必须按新时间重新判定状态;不得补发错过的提醒,也不得因时间回退而重复发出同一提醒(用「状态变化」而非「时间比较」触发)。
### 3.7 课程表组件的行为(重点)
课程表组件是「一行多条课程」的核心,ClassHelper 的 `schedule` 组件必须按下列行为实现(视觉像素见第 7 章):
#### 3.7.1 显示哪些时间点(必须逐条判定,`repos/ClassIsland/ClassIsland/Controls/Components/ScheduleComponent.axaml.cs:243-251`)
对当天每个时间点:
1. `TimeType ∈ {2 分割线, 3 行动}` 不作为课程项显示(分割线由专门的竖线控件渲染;行动不显示);
2. `HideFinishedClass=true` 且 `EndTime < now` → 不显示;
3. `IsHideDefault=true` 且不是当前选中项 → 不显示;
4. `TimeType==1`(课间)且不是当前选中项 → 不显示;
5. `ShowCurrentLessonOnlyOnClass=true` 且当前选中项是上课类型且该项不是当前项 → 不显示。
**映射到 ClassHelper(`custom` 模式的 `schedule` 组件)**:`display:'today'` 必须实现「只当前项展开、其余折叠」的 ClassIsland 语义:
- 上课点折叠只显示课程名;**当前时间点展开显示「课程名 + 起止时间」**;
- 课间与 `isHideDefault` 的上课点**折叠时完全隐藏**,轮到它们成为当前项时才出现;
- 分割线 `DIVIDER` 渲染为一条竖线;`ACTION` 完全不显示(`packages/shared/src/class-plan.ts:4,70-87`;`packages/desktop-client/src/island/ComponentNode.vue:48-80`);
- 无时间点数据时**回退按 `ScheduleDto` 条目显示课程**(`docs/app/island.md:69`);
- **倒计时/徽标只在「当前时间点临近结束」时出现**,不得常驻。
#### 3.7.2 三种课程呈现
必须实现三种课程项(`repos/ClassIsland/ClassIsland.Core/Controls/LessonsControls/`):
- **展开型**:科目名(强调字号)+ 附加信息 + 进度条;左右留白按「课程表文本间距」缩放(默认 1,范围 0.2–1,左右各 16×间距);
- **最小型**:只显示科目首字(`Subject.Initial`),用于 `IsHideDefault` 时间点或「上课时仅显示当前课程」;
- **分割线**:2px 竖线、高 25、次要色、透明度 0.8。
**已结束课程淡化**:`FadeCompletedClasses` 默认 true,透明度 0.6,过渡 150ms(`CubicEaseInOut`);`HighlightChangedClass` 默认 false,换课课程高亮(强调色块)。
#### 3.7.3 附加信息与倒计时(必须实现 6 种 `ExtraInfoType`)
`ShowExtraInfoOnTimePoint=true` 时,当前课程可显示(`repos/ClassIsland/ClassIsland/Controls/Components/LessonControlExpanded.axaml:85-171`):
| 值 | 显示 | 格式 |
|---|---|---|
| 0 | 起止时间 | `hh:mm-hh:mm` |
| 1 | 已过时间 | `-mm:ss` |
| 2 | 剩余时间 | `-mm:ss` |
| 3 | 进度百分比 | `N%` |
| 4 | 模糊倒计时 | `-<1min`(按 `CountdownSeconds` 向上取整) |
| 5 | 精确倒计时 | `-mm:ss` |
**当前课程剩余时间药丸**:`IsCountdownEnabled=true` 时,当前时间点剩余 ≤ `CountdownSeconds`(默认 60)才出现;精确模式 `-mm:ss`,非精确模式 `<<数值`(`IsNonExactCountdownEnabled=false`,`LessonControlExpanded.axaml:173-214`)。
**课程进度条**:贴底、`Maximum=1`、值为课程进度、高度 3px(视觉细节归第 7 章);进度更新必须按「比例插值」而非每帧重算布局。
#### 3.7.4 空状态与明天课表
- 空占位(默认显示,`ShowPlaceholderOnEmptyClassPlan=true`):
- 今天没有课表 → `"今天没有课程。"`;
- 今日课程全部结束且 `HideFinishedClass=true` → `"今日课程已全部结束。"`;
- 垂直居中、正文色;占位与课程列表互斥。
- 明天课表 `TomorrowScheduleShowMode` 默认 `1`:`0` 不显示、`1` 放学后显示、`2` 总是显示、`3` 今天无可显示课程时显示;显示时左侧出现「明天」药丸、主列表隐藏。
- `AfterSchool` 判定 = 当前时间状态为放学后**或**今天没有课表。
#### 3.7.5 课程表组件设置默认值(必须照抄)
| 字段 | 默认 | 说明 |
|---|---|---|
| `ShowExtraInfoOnTimePoint` | `true` | 显示附加信息 |
| `ExtraInfoType` | `0` | 0–5 |
| `IsCountdownEnabled` | `true` | 结束倒计时药丸 |
| `CountdownSeconds` | `60` | 阈值(秒) |
| `IsNonExactCountdownEnabled` | `false` | 模糊倒计时 |
| `ScheduleSpacing` | `1` | 0.2–1 |
| `ShowCurrentLessonOnlyOnClass` | `false` | 上课仅显示当前课程 |
| `HideFinishedClass` | `false` | 不显示已结束课程 |
| `FadeCompletedClasses` | `true` | 淡化已上课程 |
| `HighlightChangedClass` | `false` | 标记换课 |
| `ShowPlaceholderOnEmptyClassPlan` | `true` | 空态占位 |
| `PlaceholderTextNoClass` | `"今天没有课程。"` | 文案 |
| `PlaceholderTextAllClassEnded` | `"今日课程已全部结束。"` | 文案 |
| `TomorrowScheduleShowMode` | `1` | 0–3 |
(证据:`repos/ClassIsland/ClassIsland/Models/ComponentSettings/LessonControlSettings.cs:8-23`;`repos/ClassIsland/ClassIsland/Models/Settings.cs:103-109,208-209,221`。)
### 3.8 提醒队列与优先级(行为)
ClassIsland 的提醒是**统一管线**,必须实现以下行为:
1. **一切提醒都进同一管线**:上下课、放学、天气、集控、行动触发的提醒共用队列与消费者;不得各写一套弹窗。
2. **两层 UI**:遮罩(Mask,短文案,第一阶段)+ 正文(Overlay,详细内容,第二阶段);无正文时只播遮罩(`repos/ClassIsland/ClassIsland.Core/Models/Notification/NotificationRequest.cs:40-63`)。
3. **队列与优先级**:
- 提供方优先级:`NotificationProvidersPriority` 数组下标越小越先播;设置页可拖动排序。
- 同提供方内先入队先播(FIFO)。
- ClassIsland 源码的 `IsPriorityOverride` 比较器存在取反异常(`repos/ClassIsland/ClassIsland.Shared/Models/Notification/NotificationPriority.cs:47-50`);**本轮目标语义**按第 8 章裁决:三级排序键(提供方下标 → 通知等级 Emergency>High>Normal>Low → 入队序),不照抄异常行为。
4. **去重**:同一 request 对象只入队一次;出队用「已弹出集合」避免链式头节点重复;天气预警按 `Detail` 会话内去重;ClassHelper 侧另有「同 id 通知/叫人双播合并」规则(见第 8 章),两者不冲突。
5. **链式提醒**:数组请求串成链,任一子请求取消即取消全部;准备上课 + 上课是典型链(`repos/ClassIsland/ClassIsland/Services/NotificationHostService.cs:205-254`)。
6. **消费者**:按优先级排序,只有 `AcceptsNotificationRequests && QueuedNotificationCount<=0` 才推送;无消费者时退回队列;播放完主动拉取后续(`NotificationHostService.cs:337-419`)。
7. **播放状态机**:`None → Queued → Playing →(Paused/Cancelled/Completed)`;取消/完成分别触发对应令牌回调;被中断但未被取消的提醒必须**重新入队**(`NotificationHostService.cs:488-522`)。
8. **取消与清理**:`CancelAllNotifications` 清空队列 + 取消所有正在播放的请求;用户入口为主界面右键菜单「清除全部提醒」,仅在「有提醒正在播放」时可见(`repos/ClassIsland/ClassIsland/MainWindow.axaml:86-94`);**不实现**独立「清除提醒浮动按钮窗口」(源码 `#if false` 遗留)。
### 3.9 通知两层 UI(气泡 + 强提醒)
#### 3.9.1 气泡(行内)行为
1. 遮罩阶段:背景切换为强调色 + 斜切遮罩扫入 + 遮罩文字入场;此阶段不显示详情。
2. 遮罩结束:有正文则切正文(背景恢复、正文淡入、底部进度条显示剩余时间);无正文则直接收起。
3. 正文阶段:内容 + 底部进度条(`Maximum=1`、`Value=剩余进度`、值过渡 50ms 线性);显示时长由内容 `Duration` 或 `EndTime` 决定。
4. 结束时:正文淡出 → 遮罩退出 → 行恢复原状;队列里还有提醒则继续播下一条。
5. **提醒期间行的可见性**:强制可见(覆盖隐藏规则),并置顶;提醒结束释放置顶锁。
6. **关键动画参数**(语义;完整表见第 10 章):斜切遮罩 5 区段、区段 360ms、错峰 120ms、开 `QuarticEaseOut`/关 `QuadraticEaseIn`;遮罩文字 `delay 260ms`、淡入 250ms、缩放 1.1→1.0、750ms,曲线 `(0.25,1,0.5,1)`;退出文字 200ms、缩放 1.0→1.1、曲线 `(0.5,0,0.75,0)`;正文淡入淡出 200ms、`SineEaseIn`。Classic 主题是另一套(上滑/下压)语义,必须两套都保留。
7. **音效与 TTS**:ClassIsland 支持提醒音效与语音(默认关闭语音、音效音量 1.0);**本轮重构不实现音效/TTS**(ClassHelper 无音频通道,见 3.13),只保留视觉提醒。
#### 3.9.2 强提醒(Topmost 全屏特效)行为
1. 触发:提醒开始且生效设置开启「强调特效」、主界面可见、行未全部隐藏、本次遮罩尚未播过声音。
2. 表现:从行中心向全屏扩散的涟漪(默认强调色),扩散 600ms + 淡出 600ms,750ms 后移除元素;特效窗口为**独立透明置顶窗口**,逐屏覆盖主界面所在屏幕,按 `NotificationEffectRenderingScale`(默认 1.0)缩放。
3. 生命周期:特效元素集合为空即隐藏窗口,不销毁;非 `OSShutdown/ApplicationShutdown` 时禁止关闭窗口;编辑模式下丢弃新特效。
4. **必须支持多屏**:每个显示器一个特效窗口,屏幕/DPI 变化时重算位置与尺寸。
5. ClassHelper 映射:`custom`/`fixed` 共用同一套特效窗口;动画速度倍率缩放 600/750ms;`animations=false` 时不播特效只播气泡。
### 3.10 天气展示(行为)
1. **数据频率**:天气服务每 5 分钟刷新一次,启动立即刷新一次;刷新成功才置「已刷新」标志,所有依赖天气的提醒/规则先判该标志(`repos/ClassIsland/ClassIsland/Services/WeatherService.cs:41-44,69-71,386-389`)。
2. **缓存**:最近一次天气持久化在设置中,重启先展示旧数据并标注更新时间;刷新失败保留旧数据,不清空。
3. **组件展示**:天气组件有多种主信息模式(图标 + 温度 + 文本;默认展示当前天气+温度);无数据时显示占位。
4. **提醒**:气象预警与天气预报的提醒在「上课 / 课间」事件时触发;预警按 `Detail` 会话内去重、显示时长按文本长度钳制 10–90 秒;预报正文 15 秒;预警要求数据已刷新。
5. **本轮范围**:ClassHelper 已有 Open-Meteo 天气简报(15 分钟缓存、断网标「缓存」、**无官方气象预警**)。重构后必须**保留 ClassHelper 数据源与行为**,ClassIsland 的「气象预警/三天预报/逐小时预报提醒」**不实现**(见第 8 章冲突裁决)。
### 3.11 主题语义(Fluent vs Classic 的差异)
必须保留两套「风格」的概念(ClassHelper `islandAppearance.style` 默认 `classisland`,回退 `winisland`),差异只体现在**行为参数**上:
| 维度 | Fluent(默认目标) | Classic(可选回退) |
|---|---|---|
| 行宽过渡 | 300ms、`(0.65,0,0.35,1)` | 550ms、`(0,1,0.5,1.1)`(带过冲) |
| 遮罩入场 | 斜切 5 区段扫入 + 文字延迟 260ms | 整层上滑/下压(`Y -50→0`,200ms `QuadraticEaseIn`) |
| 遮罩退场 | 文字淡出 200ms + 缩放 | 下移 `0→50`、200ms `QuadraticEaseOut`、延迟 100ms |
| 行列表面 | 中性填充 + 1px 描边 + 阴影 `0 4 8 2 #48000000` | 类似,但强调色/描边更强 |
| 分体 | 每组件独立胶囊(min-width 40、padding 0 16) | 同语义,参数略异 |
| 主题色来源 | 系统强调色/自定义 | 同 |
**硬约束**:背景不透明度只作用于背景层;文字始终不透明;投影必须常驻声明、只改参数(禁止在开合瞬间增删 `filter`);两套主题共用同一行为状态机,只切换视觉参数。
### 3.12 动画语义总则(实现时必须遵守)
1. **进场慢、退场快**:进场 200–300ms,退场 100–150ms;提醒遮罩文字淡入 250ms、淡出 200ms,属于「进场慢退场快」的允许例外(遮罩退场本身另有 100–200ms 的层退出)。
2. **延迟表达因果**:先扫入(0–360ms)、再文字(+260ms)、再投影(+600ms);不得所有元素同时启动。
3. **错峰封顶**:列表入场每项 25–35ms,超过 10 项必须封顶;不得线性累加到 1 秒以上。
4. **可打断**:进/出必须成对、可反向,反向时新动画直接覆盖旧动画(不排队);不得用 JS 计时器模拟状态机。
5. **动画不改业务**:动画只表达状态,不得在动画结束回调里发请求或用动画时长当业务计时。
6. **最终几何一致**:动画结束后元素几何必须与「无动画直跳终态」逐像素一致。
7. **动画级别**:`AnimationLevel` 0/1/2(默认 1);级别 0 禁用动画但**主界面提醒动画不在此列**(设置项描述明确);同时响应系统「减少动态效果」(视为级别 0 + 时长 1ms,加载指示器放慢不停)。
8. **性能禁忌**:不对包含文字的容器做 `width/margin` 过渡(用绝对定位背景层或 `clip-path`/`scaleX`);卡片投影用 `box-shadow` 不用 `filter: drop-shadow`;透明窗口**禁用** `setBackgroundMaterial()`。
### 3.13 本轮不实现的 ClassIsland 能力(必须写进实现说明,不得遗漏标注)
以下能力在 ClassIsland 基准里存在,但**本轮重构明确不实现**,实现者不得自行扩展、也不得因缺失判为缺陷:
1. **自动化系统**(触发器/行动/规则集/时间点行动):不实现;岛内不出现自动化入口。
2. **插件系统与插件市场**:不实现;不引入 `.cipx`、不加载第三方 DLL/模块。
3. **主题市场/主题包/壁纸取色**:不实现(壁纸取色在 ClassIsland 也已是 P2 停用状态);只做岛内两套风格(`classisland`/`winisland`)。
4. **集控/管理端**(ManagementServer、策略、审计、授权级别):不实现;ClassHelper 用服务端权限体系替代。
5. **Excel 导入/导出、档案导入导出、CSES 互转**:不实现(属 ClassIsland 本地档案能力);ClassHelper 继续用服务端课表/YAML 导入。
6. **自动更新器、启动闪屏、OOBE、欢迎向导、教程中心、崩溃恢复窗口、作者/致谢窗口**:不实现(属其它 part/非岛范围)。
7. **提醒音效与 TTS(系统 TTS / Edge TTS)**:不实现;本轮只做视觉提醒。若未来需要,作为独立扩展项,默认关闭。
8. **ClassIsland 的「即将下课」遮罩**:不实现——课程临近下课的提示统一走 ClassHelper 的「课程名右侧蓝色秒数徽标」(见第 8 章裁决)。
9. **本地档案/Profile/TimeLayout/CSES 体系**:不实现;数据继续用 ClassHelper 服务端解析结果(`ScheduleDto`/`IslandTimePointDto`)。
10. **通知音效音量、静音时段、语音源切换**:不实现(ClassIsland 有,ClassHelper 无对应通道)。
### 3.14 验收标准(行为级)
实现者必须逐条自查,全部通过才算完成本节:
1. 多行场景:1→5→1 行增删后,行可见性与「主行」约束正确;只剩 1 行时删除被拒绝且提示「至少需要保留 1 个主界面行。」。
2. 行可见性表达式三态:组件全隐藏时整行消失;提醒期间强制可见;编辑模式下隐藏行也可见可选。
3. 隐藏规则:`hideMode=0` 三个开关各自生效,`explorer` 前台时不算全屏;提醒/编辑时覆盖隐藏;手动隐藏后不再接收新提醒;全部隐藏时内容透明度为 0 但窗口不销毁。
4. 时间状态机:构造「上课→课间→放学」一天,状态与事件顺序正确(状态改变事件先于具体事件);禁用一节早课/晚课后,时间点的双向裁剪与「今天没有课程」占位一致。
5. 课程表组件:6 种附加信息、倒计时药丸阈值 60 秒、已结束课程淡化 0.6、明天课表 4 种模式、无课表/全部结束两种占位文案全部正确。
6. 提醒:两条提醒先后入队,第一条播完自动播第二条;提醒期间行强制可见并置顶,结束后释放;右键「清除全部提醒」仅在有提醒时出现并立即清空。
7. 强提醒:开启特效时从岛中心扩散涟漪,600ms 扩散 + 600ms 淡出、750ms 后消失;编辑模式下不播;多屏时覆盖岛所在屏。
8. 主题:Fluent 与 Classic 两套的遮罩入场/退场行为不同(斜切 vs 上下滑),行宽过渡 300ms vs 550ms;切换主题不改变任何业务状态。
9. 动画:`AnimationLevel=0` 时除岛提醒外无动画;系统「减少动态效果」下时长压到 1ms 且加载指示器不停;动画结束后几何与直跳终态一致。
10. 非目标确认:界面上不存在自动化、插件、主题市场、集控、Excel 导入、音效/TTS 入口;代码中不存在对应依赖。
11. 形态映射:新安装默认 `family='classisland'` 的信息行(日期+课程表、行高 40、底部 3px 进度条),升级用户默认 `family='winisland'` 的胶囊形态;`custom` 仍走 schemes/rows/nodes;三种形态切换不丢消息、不重建第二个岛窗口。
---
## 4. 目标架构与文件级改造清单
### 4.0 重构原则(必须逐条遵守)
1. **只在现有仓库内重构**:不得新建工程、不得换框架、不得引入服务端依赖;沿用 pnpm@11.8.0 monorepo 与 Electron ^44.3.0 + Vue ^3.5.42 + Element Plus ^2.14.5 + Pinia ^4.0.3 + Vite ^8.3.0 + TS ~5.9.3。
2. **渐进式替换,不一次性推倒**:每次改动后应用必须仍可启动、旧 `config.json` 仍可加载;用"门面(facade)+ 新模块"的方式把旧调用点隔离,避免一次改 `src/main/index.ts`、`tray.ts`、`smoke.ts` 的全部 import。
3. **单一事实来源**:任何"主进程与渲染进程各算一次"的数值(窗口包围盒、列表高度、命中矩形、细缝尺寸、字号推导)必须收口到 `packages/shared/src/island/`,两端只调用同一函数。现状三处漂移见 02 §2.3。
4. **保留现有业务语义**:消息去重、上课暂存、紧急展开、已读回执、作业免打扰指纹、课件覆盖优先级、离线补弹,全部保持(02 §2.5/§2.12);ClassIsland 只改变"视觉与布局表现"。
5. **两套风格族(family)共存,且 family 只作用于 fixed**:`appearance.family` 取值 `'classisland' | 'winisland'`;**默认 `classisland`**;`winisland` 保留原胶囊+详情卡外观;老用户升级默认 `winisland`(迁移见 05)。**`custom` 布局始终按 ClassIsland 规格渲染,不受 family 影响**(family 只决定 fixed 用哪套渲染器)。
6. **`fixed` 重新定义**:在 `family='classisland'` 下,`fixed` 不再是胶囊+详情卡,而是 **ClassIsland 风格信息行**(默认 1 行 = 日期 + 课程表,行高 40、圆角 8、背景不透明度 0.5、3px 进度条、消息以行内气泡呈现)。原"胶囊+详情卡"只在 `family='winisland'` 下保留。这是本轮最重要的语义变化,所有文件改造必须按此分叉。
7. **一个原生窗口**:多种布局/风格共用一个 `BrowserWindow`;切换 family/layout 不得新开窗口(现状约束,见 `docs/app/island.md:8-10`、`src/main/island.ts:468-634`)。
8. **不打扰纪律**:默认验证无头/离屏,不创建可见岛或托盘;只有显式 `--visual` 才做桌面像素验收。依据:`docs/dev/testing.md:9,29-32`。
9. **调色板与风格分离**:`style: black|glass|tinted` 继续保留,写为 winisland 调色板字段(canonical `appearance.winislandStyle`,旧 `style` 作为兼容镜像,不删除);family 与 style 独立,切换 family 时保留用户 style 原值。
10. **外观新增定稿字段**:`colorScheme: 'light'|'dark'|'system'`(默认 system)、`theme: 'fluent'|'classic'`(默认 fluent)、`effectEnabled`(默认 true)、`effectScale: 1|0.75|0.5`(默认 1)、`progressBar:{height:3,show:true}`;`fontSize` 语义随 family:classisland=正文基准(新装默认 16,派生 14/18/20),winisland=旧语义(默认 13,迁移保留原值)。
11. **版本只保留一个**:落盘版本只有 `islandLayout.version=2`;`schemaVersion` 仅允许出现在 IPC payload 作运行时校验,不得落盘。
12. **fixed 消息/提醒口径**:行内气泡同一时刻只显示 1 条 active;多条折叠为「还有 N 条」并可打开应用内通知中心;`prepare/on-class/break` 以行内容接管呈现、`ending` 只用蓝色圆形秒数徽标;队列/去重/暂存规则不变。
### 4.1 目标模块边界与目录
必须把灵动岛拆成四个边界清晰的部分。目录与职责如下(标注「新增」的目录/文件由本轮创建;其余为移动或改造):
```text
packages/shared/src/island/ # 新增:岛的唯一契约层(纯 TS,无 DOM/Electron)
├─ index.ts # 统一导出(供 desktop-client 与 web-admin 可复用)
├─ schema.ts # IslandLayoutV2 / IslandAppearanceV2 / IslandRowV2 / IslandNodeV2 类型
├─ defaults.ts # ClassIsland 规格默认值(family/行/组件/外观)
├─ normalize.ts # normalizeIslandLayoutV2 / normalizeAppearanceV2(校验+夹取)
├─ migrate.ts # migrateIslandLayoutV1ToV2(旧 config → v2)
├─ metrics.ts # CLASSISLAND_ISLAND_METRICS + WINISLAND_ISLAND_METRICS + 排版系数
├─ geometry.ts # 锚点/包围盒/细缝/触摸贴合尺寸的纯函数(主/渲染共用)
├─ messages.ts # 去重、排序、列表布局、行内气泡分组(纯函数)
├─ lessons.ts # 课程提醒计算(从 main/lesson-reminder.ts 上移)
├─ dto.ts # IslandState/IslandNotification/ScheduleDto 等对外 DTO 与映射
└─ ipc.ts # IPC 通道名与 payload 类型(主进程与 preload 共用)
packages/desktop-client/src/main/island/ # 新增:主进程岛引擎(从 island.ts 拆出)
├─ index.ts # 门面:export { island, registerIslandIpc, ... }(旧 import 不变)
├─ controller.ts # 状态机编排:消息、上课状态、预览、动作分发
├─ window.ts # 建窗、固定包围盒、锚点布局、可见性/焦点/透明度
├─ hit-test.ts # 命中轮询、岛体矩形、interactive 决策(含 setShape 降级)
├─ touch.ts # 触摸模式、贴合、失焦宽限、常驻控件
├─ messages.ts # 队列存储 + 合并 + 超时计时器(无 UI)
├─ lessons.ts # 每秒推进课程提醒
├─ weather.ts # 从 island-weather.ts 移入
├─ courseware.ts # 课件覆盖状态桥
├─ preview.ts # 设置页"测试灵动岛"的预览注入
└─ ipc.ts # island:* 全部 handler(从 island.ts 尾部搬来)
packages/desktop-client/src/island/ # 渲染进程(现有目录内重组)
├─ main.ts # Keep:tokens + motion + mount
├─ LayoutIslandApp.vue # Change:分发(custom→CustomIslandRuntime;fixed 按 family 二选一)
├─ family/
│ ├─ classisland/
│ │ ├─ FixedInfoRow.vue # 新增:ClassIsland 信息行(fixed 新默认)
│ │ ├─ InlineMessageBubble.vue # 新增:行内消息气泡(fixed 下替代详情卡)
│ │ └─ ClassislandTheme.css # 新增:Fluent/Classic 两套行样式与动画
│ └─ winisland/
│ ├─ ClassicIsland.vue # Change:由 IslandApp.vue 迁移并改名(原胶囊+详情卡)
│ └─ winisland.css # Change:由 classic-island.css 迁移(数值不变,仅 fixed 使用)
├─ custom/
│ ├─ CustomIslandRuntime.vue # Change:始终按 ClassIsland 行/组件规格重写测量与适配(不看 family)
│ ├─ CustomRows.vue # 新增:常态行 / 展开行渲染(行级外观)
│ ├─ CustomRow.vue # 新增:单行(行设置、隐藏规则、进度条)
│ └─ CustomRowSettingsDrawer.vue# 新增:行设置抽屉(背景/字号/圆角/隐藏;字段由 09 定义)
├─ shell/
│ ├─ IslandShell.vue # 新增:squircle 形状 + 弹簧形变 + 命中上报(fixed 两族共用)
│ ├─ MessagePanel.vue # Change:由 ClassicNotifications.vue 改造(单条/列表/行内气泡)
│ └─ LessonBar.vue # Change:由 LessonReminderContent.vue 改造(ClassIsland 遮罩)
├─ components/ # 新增:按 kind 拆分的组件渲染器
│ ├─ index.ts # node.kind → 渲染器映射
│ ├─ DateNode.vue / ClockNode.vue / TextNode.vue / CountdownNode.vue
│ ├─ ScheduleNode.vue / WeatherNode.vue / SeparatorNode.vue
│ └─ ContainerNode.vue # group/stack/slide/rolling(rolling 支持 mode continuous|pause)
├─ motion/ # Keep:spring.ts、squircle.ts(可加适配器)
├─ styles/ # 新增:shared.css / classisland.css / custom.css(custom 始终 classisland 命名空间)
└─ Legacy/ # 可选:保留旧文件名再导出门面(ComponentNode.vue 等)
packages/desktop-client/src/renderer/island/ # 主窗口侧的桥(现有)
├─ bridge.ts # Keep:数据桥(课表/上课状态/通知/作业/叫人/补弹/已读)
└─ editor/ # 新增:编辑器逻辑 composables(从 IslandEditorView 抽出)
├─ useIslandDraft.ts # 草稿、脏标记、保存队列、修订号
├─ useIslandDrag.ts # 指针拖拽/落点/自动滚动
└─ useIslandPreview.ts# 内嵌预览缩放与测量
packages/desktop-client/src/renderer/ # 设置/编辑器页面(现有)
├─ views/settings/IslandSettingsView.vue # Change:family 选择器 + 条件外观
├─ views/settings/IslandEditorView.vue # Change:调用新版架构 + 行设置
├─ components/IslandComponentSettings.vue # Change:ClassIsland 组件字段
├─ components/IslandEditorNode.vue # Keep(可加行级拖拽)
├─ components/IslandRowSettingsDrawer.vue # 新增:行设置抽屉
└─ styles/island-editor.css # Change:ClassIsland 编辑器视觉
```
### 4.2 文件级 Keep / Change / Add / Remove 总表
| 文件/目录 | 类型 | 一句话目标 |
|---|---|---|
| `packages/shared/src/island-layout.ts` | Change | 保留导出名兼容;实现迁移到 `island/` 新目录,本文件仅再导出 |
| `packages/shared/src/constants.ts` | Change | 保留旧 WinIsland 常量;新增 ClassIsland 度量与 family 常量(迁移见 05) |
| `packages/shared/src/types.ts` | Change | `IslandAppearance` 增加 `family`/`colorScheme`/`effectEnabled`/`effectScale`/`progressBar`/`theme`/`winislandStyle`;新增 `IslandState.family/pendingStaged/bubblePhase/bubbleDeadline`、`IslandNotification.level`、`IslandLessonReminder.progress/leftSeconds`(运行期,不落盘) |
| `packages/shared/src/island/ipc.ts` | Add | IPC 通道名与 payload 类型;`island:push` 增加 `source`/`dedupeKey` |
| `packages/shared/src/motion.ts` | Keep | 不动令牌数值(03/10 部分引用) |
| `packages/shared/src/class-plan.ts` | Keep | `IslandTimePointDto`/`TimePointType` 不动 |
| `src/main/island.ts` | Change | 2445 行 → 门面(约 30 行),实际逻辑移入 `main/island/*` |
| `src/main/island/*.ts` | Add | 见 4.1(controller/window/hit-test/touch/messages/lessons/weather/courseware/preview/ipc) |
| `src/main/config.ts` | Change | 启动时执行 v2 迁移钩子(唯一版本=`islandLayout.version`);写盘前校验 `family/colorScheme/fontSize/effect*`;保留合并写/退出 flush |
| `src/main/index.ts` | Change | `island.init()` 改调新门面;注册编辑器/预览所需 IPC |
| `src/main/ipc.ts` | Change | 岛相关 handler 全部委托 `main/island/ipc.ts`;主窗口配置剔除规则保留 |
| `src/main/courseware.ts` | Change | 改为调用 `island.setCoursewareTransfer`(门面保留同名方法) |
| `src/main/tray.ts` | Change | 菜单文案与入口不变;`island.editLayout()` 改由新控制器实现 |
| `src/main/island/messages.ts` | Add | 消息队列 + 合并 + 超时 + **作业指纹/去重表**(见 4.3.13) |
| `src/main/island/ipc.ts` | Add | 校验 `island:push` 的 `source/dedupeKey`,投递前查重 |
| `src/main/smoke.ts` | Change | 保留全部既有读取器与注入点(名称/语义不变,见 4.7 第 11 条);新增 v2 断言(行高/行数/气泡/迁移);改造归属 part 11 测试基线 |
| `src/preload/index.ts` | Change | 新增 `family/colorScheme` 感知的配置读写;`islandPush` 透传 `source/dedupeKey`;保留全部旧方法名 |
| `src/preload/island.ts` | Change | 新增行/编辑器预览相关事件;旧事件名与语义不变 |
| `src/types/desktop.d.ts` | Change | 配置形状加外观新字段与 `islandLayout.version`;新增 v2 DTO 类型;`islandPush` payload 扩展 |
| `src/island/LayoutIslandApp.vue` | Change | 分发:`custom` → CustomIslandRuntime;`fixed` 按 family 二选一(见 4.3.1) |
| `src/island/IslandApp.vue` | Change | 迁出为 `family/winisland/ClassicIsland.vue`;旧文件保留再导出(防断裂) |
| `src/island/CustomIslandRuntime.vue` | Change | **始终按 ClassIsland 行/组件规格**重写测量、行渲染、行设置抽屉;不受 family 影响 |
| `src/island/ComponentNode.vue` | Change | 拆为 `components/*Node.vue`;本文件保留兼容导出;滚动组件新增 `settings.mode('continuous'|'pause')` |
| `src/island/ClassicNotifications.vue` | Change | 改为 `shell/MessagePanel.vue`;新增行内气泡模式(同一时刻 1 条 active) |
| `src/island/LessonReminderContent.vue` | Change | 改为 `shell/LessonBar.vue`;遮罩参数按 ClassIsland 重算;fixed 下 prepare/on-class/break 行内容接管、ending 仅徽标 |
| `src/island/CoursewareTransfer.vue` | Keep | 课件覆盖卡保持现状(424×132 等,见 02 §2.10) |
| `src/island/WeatherBrief.vue` | Keep | 天气简报保持现状(Open-Meteo/15 分钟/缓存标记);**本轮不引入 ClassIsland 天气服务** |
| `src/island/spring.ts`、`squircle.ts` | Keep | 弹簧与连续圆角数值不动(10 部分仍按此写) |
| `src/island/classic-island.css` | Change | 改名 `family/winisland/winisland.css`(fixed 专用);数值不变,选择器按新 DOM 调整 |
| `src/island/custom-runtime.css`、`custom-island.css` | Change | 移到 `styles/custom.css` 与 `custom/*`;**始终按 ClassIsland 行规格重写**(custom 不受 family 影响) |
| `src/island/lesson-ending.css` | Keep | 秒数徽标样式不动 |
| `src/island/main.ts` | Keep | 入口与令牌注入顺序不动 |
| `src/renderer/views/settings/IslandSettingsView.vue` | Change | 顶部加 `family` 选择器(只影响 fixed);新增 colorScheme/theme/effectEnabled/effectScale/progressBar;fontSize 语义随 family 展示 |
| `src/renderer/views/settings/IslandEditorView.vue` | Change | 数据改为 v2;行设置抽屉;预览按 ClassIsland 规格;保存队列/上限保留;拆分节奏按 09 文件计划(见 4.3.11) |
| `src/renderer/components/IslandComponentSettings.vue` | Change | 组件字段与 ClassIsland 规格对齐(含滚动 mode);旧字段保留只读兼容 |
| `src/renderer/components/IslandEditorNode.vue` | Keep | 标签节点可复用;若加行级拖拽则小幅 Change |
| `src/renderer/components/IslandRowSettingsDrawer.vue` | Add | 行级设置的唯一入口(字段清单由 09 定义) |
| `src/renderer/island/bridge.ts` | Change | 作业投递增加 `source:'homework-local'|'realtime'|'replay'` 与 `dedupeKey`;保留本机指纹快速路径(见 4.3.13) |
| `src/renderer/views/HomeworkView.vue` | Keep | `markHomeworkCreatedLocally` 调用点保留(录入前/后登记指纹,见 4.3.13) |
| `src/renderer/styles/island-editor.css` | Change | 编辑器视觉按 ClassIsland(间距/分隔/虚线框/预览画布) |
| `packages/desktop-client/scripts/smoke.mjs` | Change | 视觉/几何断言改为按 family 分套;默认 quick 仍无头 |
| `docs/app/island.md`、`docs/dev/implementation.md` | Change | 口径更新为 family 与 fixed 新语义(文档不是代码,但必须与实现同步) |
### 4.3 关键文件改造详情(逐个文件:现状 → 目标 → 接口 → 风险)
#### 4.3.1 `src/island/LayoutIslandApp.vue`(Change,按 layout + family 分发)
- **现状**:只按 `layout==='custom'` 二选一(`CustomIslandRuntime` / `FixedIsland`),课件优先覆盖。依据:`packages/desktop-client/src/island/LayoutIslandApp.vue:56-75`。
- **目标**(定稿分发规则):
- `layout==='custom'` → **始终** `custom/CustomIslandRuntime.vue`(ClassIsland 行/组件规格),**不看 family**;
- `layout==='fixed'` 且 `appearance.family==='classisland'` → 新增 `family/classisland/FixedInfoRow.vue`(信息行 + 行内气泡);
- `layout==='fixed'` 且 `appearance.family==='winisland'` → `family/winisland/ClassicIsland.vue`(原胶囊+详情卡,行为不变);
- 课件传输仍最高优先级,覆盖以上所有分支。
- **接口**:props 保持 `settings/state/schedules/timePoints/appearance/nativeVisible`;新增计算 `family = appearance.family ?? 'classisland'` 仅用于 fixed 分支;不新增 IPC 通道。
- **为什么**:family 只影响 fixed,custom 不需要两套渲染;分发规则收窄后,custom 的数据与视觉可以独立演进。
- **风险**:切换 family 只改变 fixed 渲染器,custom 不受影响——必须保证切换时若当前 layout=custom 不重建 custom 运行时(避免方案展开态/测量状态丢失);切换命中矩形时 `reportHit(null)` 并 `syncTargets(true)`。
#### 4.3.2 `src/island/IslandApp.vue` → `family/winisland/ClassicIsland.vue`(Change,大改)
- **现状**:683 行,胶囊层 + 展开层 + 课程提醒 + 命中上报 + 弹簧形变全在一个文件;窗口固定包围盒内的岛由 `morph` 驱动。依据:`src/island/IslandApp.vue:44-570`。
- **目标**:
1. 保留并迁移全部 winisland 行为(胶囊/展开/紧急/叫人/多条列表/课程提醒/命中),**视觉与交互不变**(老用户回退路径)。
2. 把"壳"(squircle 路径、弹簧、`--card-path`、命中上报)抽到 `shell/IslandShell.vue`,把消息展示抽到 `shell/MessagePanel.vue`,把课程提醒抽到 `shell/LessonBar.vue`;`ClassicIsland.vue` 只负责"胶囊内容 + 展开内容"的编排。
3. 文件顶部保留兼容导出:`export { default } from './family/winisland/ClassicIsland.vue'`,避免旧 import 断裂。
- **接口**:props 必须兼容现有 `{ embedded?, suspended? }`;继续调用 `window.island.sendAction/setInteractive/setHitRect`;继续使用 `ISLAND_SIZE_DELTA`/`ISLAND_LIST_METRICS`。
- **为什么**:ClassIsland fixed 取代胶囊后,旧实现仍要作为 `winisland` 保留;抽壳后两族共用同一套命中/形变,减少重复实现。
- **风险**:抽壳会改变 DOM 层级,现有 CSS 选择器(`.island-card .content`、`.pill-inner`)与冒烟断言依赖 DOM;必须保留 `.island-card`、`.island-root`、`data-anchor`、`data-style` 等关键钩子。
#### 4.3.3 `family/classisland/FixedInfoRow.vue`(Add,fixed 的新默认)
- **目标**:ClassIsland 信息行的固定布局:默认 1 行,行高 40、行内水平内边距 12、行间距 5、圆角 8、背景不透明度 0.5(背景色取主题 token + `appearance.winislandStyle`/surface 材质)、1px 描边、字号由 `appearance.fontSize`(classisland 下=正文基准,默认 16)派生 14/18/20、进度条高度取 `appearance.progressBar.height`(默认 3)、`progressBar.show=false` 时不显示;默认行 = 日期组件 + 课程表组件;消息以**行内气泡**呈现(普通/重要/紧急/叫人/作业),不再弹"详情卡";强提醒特效仅在 `effectEnabled=true` 时叠加,尺寸按 `effectScale`。
- **接口**:props `{ state, appearance, schedules, timePoints, now, editing? }`;内部复用 `components/*Node.vue` 与 `shell/MessagePanel.vue`(行内模式);只读 `appearance.colorScheme` 决定行底色/文字对比。
- **为什么**:ClassIsland 的主界面就是"一行行信息",fixed 只有变成信息行才符合重构目标。
- **风险**:fixed 原有"点胶囊展开卡片"的点击目标消失,必须保证消息可读(行内气泡可点开详情面板);命中矩形上报必须按真实行矩形,不能用旧胶囊尺寸(窗口包围盒由 geometry.ts 计算)。
#### 4.3.4 `family/classisland/InlineMessageBubble.vue`(Add,行内气泡)
- **目标**:**同一时刻只显示 1 条 active 气泡**;点击展开详情/操作;有多条未处理消息时折叠为「还有 N 条」,点击打开应用内通知中心(沿用 ClassHelper 现有通知中心路由),不并列展示多条;两阶段表现为先横向展开成条、再展开详情面板(动画由 08/10 定义);普通/重要/紧急/叫人/作业五类样式;紧急消息仍按现有业务规则"立即展开、无视上课"。
- **接口**:props `{ active, pendingCount, mode, listExpanded }`;动作只通过 `window.island.sendAction`,不得直接改状态;数据来源是 `IslandState.active/queued` 与 `IslandNotification.level`。
- **为什么**:ClassIsland 的提醒是"行内/气泡"而非独立卡片;不改变业务队列,只换表现。
- **风险**:行内气泡只改变行内内容测量与截断(正文最多 2 行、超出省略),**不改变窗口包围盒**(fixed 窗口尺寸固定为 min(工作区宽,960) × (40+2×pad));「还有 N 条」跳转必须带未读上下文,不得丢失已读回执。
#### 4.3.5 `custom/CustomIslandRuntime.vue`(Change,大改;始终 ClassIsland 规格)
- **现状**:三页(compact/details/lesson)交叉淡入 + 自然内容测量 + 铃铛 + 命中上报;行/组件来自 `scheme.rows`。依据:`src/island/CustomIslandRuntime.vue:52-336,338-527`。
- **目标**:保留"测量 + 三页"骨架,**统一按 ClassIsland 行规格**重写(**不区分 family**):单行基准高 40、行间距 5、行内水平留白 12、行背景/圆角/不透明度/字号覆盖、行级隐藏规则、行级进度条(3px,含义见 05 §5.3);行设置抽屉入口(字段由 09 定义);`rows`/`expandedRows` 的编辑语义不变;滚动组件支持 `settings.mode: 'continuous'|'pause'`(pause 时到边界停顿 `settings.interval`)。
- **接口**:props 不变;新增读取 `row.appearance/row.hideRules/row.progress`;`window.island.reportResidentControl` 继续上报铃铛矩形;`window.island.reportSize` 继续上报适配后尺寸;CSS 使用 `classisland` 命名空间(custom 不受 family 影响)。
- **为什么**:ClassIsland 组件库/行模型与本应用 schemes 高度相似,且 family 最终只作用于 fixed;custom 统一到 ClassIsland 规格可避免"两套自定义外观"。
- **风险**:老 custom 用户升级后视觉会变化(自定义布局统一到 ClassIsland 规格),必须在发布说明与设置页提示;`ResizeObserver` + `requestAnimationFrame` 测量链对行数敏感,改写后的几何只由 metrics 计算,不用动画中的壳反推(现状注释见 `:73-79`)。
#### 4.3.6 `src/main/island.ts` → `main/island/*`(Change + Add,大重构)
- **现状**:2445 行单文件(分区见 02 §2.2)。
- **目标**:
- `index.ts`:门面导出 `island`、`registerIslandIpc`、`recomputeSizes`、`normalizeAppearance`、`getSliverSize`、`ISLAND_SIZES` 等旧导出名,**保证 `main/index.ts`、`tray.ts`、`courseware.ts`、`smoke.ts` 的 import 不改**。
- `controller.ts`:状态与编排;持有 `IslandState`;消息/上课/预览/动作分发。
- `window.ts`:建窗、固定包围盒/触摸贴合、锚点、可见性/焦点/透明度、`syncWindow` 拆分后的"可见性决策"。
- `hit-test.ts`:60ms 轮询、`setInteractive`、`setHitRect`、`expectedHitRect`、`setShape` 降级(非 Windows 见 4.6)。
- `messages.ts`:`pushNotification` 分流、合并、排序、超时计时器、`dismissActive`、`clearPendingForClose`、`handleAction`。
- `lessons.ts`:`LessonReminderTracker` 每秒推进与 `lessonShowing` 判定。
- `ipc.ts`:`registerIslandIpc()` 全部 handler(含发送方校验与外观合并节流 16ms)。
- **接口**:必须保留全部旧公开方法与冒烟读取器(02 §2.5 第 9 条清单);新增 `getMetrics()`、`getLayoutVersion()` 供编辑器/冒烟断言。
- **为什么**:不拆文件无法安全替换 fixed 渲染、无法让 engine 复用 shared 纯函数,也无法让 ChatGPT 在单文件里做可靠改动。
- **风险**:拆分过程中最容易出现"私有字段跨模块互相引用";必须先把 `IslandState`、`WindowBox`、`HitRect`、`Metrics` 定义到 `types`,模块间只经接口通信;拆完立即用现有 `verify:desktop`(无头)回归。
#### 4.3.7 `src/island/ComponentNode.vue` → `components/*Node.vue`(Change)
- **现状**:456 行,14 类 kind 的条件渲染 + 课程表折叠算法 + 编辑手柄 + 行内样式。依据:`src/island/ComponentNode.vue:29-210,212-293`。
- **目标**:按 kind 拆文件,`components/index.ts` 提供 `resolveNodeRenderer(kind)`;每个组件只负责自己的内容与样式;课程表的"折叠/展开/隐藏/进度/徽标"逻辑放进 `ScheduleNode.vue` 并复用 02 §2.8/§2.9 规则。
- **接口**:保持 `IslandComponentNode` 数据结构不变(字段名/默认值见 `packages/shared/src/island-layout.ts:77-108`);编辑态仍用 `node-handle` 拖拽句柄。
- **为什么**:ClassIsland 组件规格要为每类组件补字段(字号覆盖、进度条、显示模式),单文件条件链会继续膨胀。
- **风险**:`<style scoped>` 拆分后类名作用域变化;必须把跨组件公共样式放 `styles/components.css`,避免样式丢失。
#### 4.3.8 `src/island/ClassicNotifications.vue` → `shell/MessagePanel.vue`(Change)
- **现状**:单条详情卡 + 多条列表卡 + 按钮行;只用于展开态。依据:`src/island/ClassicNotifications.vue:104-233`。
- **目标**:同一组件支持三种形态:`detail`(winisland 详情卡)、`list`(winisland 多条列表)、`inline`(classisland fixed 行内气泡);按钮语义(打开应用/标为已读/知道了/收到)与批量语义不变;`canMarkRead` 排除作业的规则保留。
- **接口**:props 增加 `variant: 'detail'|'list'|'inline'`;保持 `state/appearance/maxHeight`。
- **为什么**:fixed 默认不再有详情卡,但消息仍需可操作;共用一套面板避免三处重复实现按钮/已读逻辑。
- **风险**:`inline` 形态的高度必须计入行高与窗口包围盒(由 `shared/island/messages.ts` 提供),不得在渲染层单独硬编码。
#### 4.3.9 `src/island/LessonReminderContent.vue` → `shell/LessonBar.vue`(Change)
- **现状**:5 区斜切遮罩 + 进度条 + 秒数徽标;遮罩区段 360ms/错峰 90-120ms、开 `1-(1-p)^4`、关 `p²`;文字 260ms、scale 1.1→1.0、750ms。依据:`src/island/LessonReminderContent.vue:59-123,133-137`。
- **目标**:按 ClassIsland 规格校准:遮罩斜切权重 `[0,0.1,0.2,0.4,0.2,0.1]`(与 03/14 部分一致)、区段 360ms、错峰 120ms、开 QuarticEaseOut、关 QuadraticEaseIn、文字 delay 260ms;行高 40、圆角 8。**叠加口径(定稿)**:fixed(classisland)下 `prepare/on-class/break` 以**行内容接管**呈现(临时替换课程表内容,动画由 08/10 定义);`ending` 不接管行内容,只在课程名右侧显示蓝色圆形秒数徽标;custom 不接管(保持课程组件 + 徽标)。
- **接口**:props 保持 `{ reminder, now, appearance, motion }`;新增 `variant: 'winisland'|'classisland'`。
- **为什么**:ClassIsland 的提醒遮罩是"整行被斜切色块覆盖 + 文字弹入",与现有实现同源但参数不同;必须抽成共用组件。
- **风险**:`ref` 宽度依赖 `ResizeObserver`,在 fixed 行内宽度变化频繁;必须避免测量死循环(只在行宽变化时重算多边形)。
#### 4.3.10 `src/renderer/views/settings/IslandSettingsView.vue`(Change)
- **现状**:布局二选一、外观滑块、测试预览。依据:`src/renderer/views/settings/IslandSettingsView.vue:26-160,162-392`。
- **目标**:
1. 顶部先选 **风格族 family**(classisland 默认 / winisland 兼容)——**只影响 fixed**,说明文案必须写清"自定义布局始终使用 ClassIsland 规格";
2. `fixed` 的说明按 family 变(classisland=fixed 信息行;winisland=fixed 胶囊+详情卡);
3. 新增外观控件:`colorScheme`(浅色/深色/跟随系统,默认跟随)、`theme`(fluent/classic,仅 classisland)、`effectEnabled`(强提醒特效总开关)、`effectScale`(1/0.75/0.5)、`progressBar.show/height`(默认 3px)、`fontSize`(classisland 显示为"正文字号(16 基准)"、winisland 显示为旧"基础字号(13 基准)");
4. 调色板 `winislandStyle`(black/glass/tinted)仅在 winisland 下显示;切换 family 时**保留用户原值**,不重置;
5. "测试灵动岛"必须按当前 family + layout 推预览。
- **接口**:`window.desktop.saveConfig({ island: {...} })`、`islandSetAppearance` 不变;新增字段随 `island` 下发(纯数据,禁止传 Proxy)。
- **为什么**:family 是重构后最上层的开关,必须最先可见;新增外观字段需要用户可发现、可关闭。
- **风险**:切换 family 会立即改变 fixed 外观;必须先弹确认"切换后仅 fixed 表现改变,自定义方案与 style 不变",且不得自动改 layout。
#### 4.3.11 `src/renderer/views/settings/IslandEditorView.vue`(Change)
- **现状**:组件标签 + 内嵌预览 + 方案菜单 + 组件库 + 检查器 + 指针拖拽 + 保存队列。依据:`IslandEditorView.vue:25-130,130-540,541+`。
- **目标**:数据切到 v2;增加"行"选择与行设置抽屉(`IslandRowSettingsDrawer.vue`,字段清单由 09 定义);预览统一按 ClassIsland 规格渲染(custom 编辑也用 `CustomRows`);保留上限(200 组件/20 方案)、保存队列、离开前保存、JSON 导入导出。
- **接口**:仍通过 `window.desktop.getConfig/saveConfig`;`normalizeIslandLayouts` 改为 `normalizeIslandLayoutV2`;`getIslandSchedules/getIslandTimePoints` 不动。
- **为什么**:ClassIsland 编辑器的核心是"行/组件/行设置抽屉/组件库拖拽",现有编辑器已有 80% 骨架。
- **风险**:编辑器是 855 行大文件;**拆分节奏按 09 的文件计划执行,不强制本轮先抽 composables**;若单文件继续膨胀,再按 4.1 的 `renderer/island/editor/` 抽取,抽取时不得改变保存队列/修订号语义。
#### 4.3.12 `src/main/config.ts`(Change,迁移接入点)
- **现状**:`PersistedConfig` 含 `islandLayout`/`island`,逐字段归一化,120ms 合并写。依据:`src/main/config.ts:26-84,198-233,334-346`。
- **目标**:**不新增文件级版本号**;读取 `islandLayout` 时若 `version` 缺失 → 调用 `migrateIslandLayoutV1ToV2`(含 family/外观默认值,见 05),迁移结果写回并备份;`saveConfig` 收到 `islandLayout` 时先 `normalizeIslandLayoutV2`;`island` 增加 `family/colorScheme/theme/effectEnabled/effectScale/progressBar/winislandStyle` 归一化。
- **接口**:导出 `getConfig/saveConfig/flushConfigSync` 签名不变;新增 `getIslandLayoutV2()` 供主进程使用。
- **为什么**:迁移必须在任何消费者(岛窗口、编辑器、冒烟)之前完成,且只能发生一次;版本字段只保留 `islandLayout.version` 可避免两套版本号分叉。
- **风险**:迁移写盘与"用户正在编辑"竞争;必须保证迁移只在 `app.whenReady()` 后、岛窗口创建前执行一次,失败时保留原文件并让应用以默认 v2(只读)启动。
#### 4.3.13 作业指纹链路(本机刚录作业不上岛)
- **现状**:渲染层 `src/renderer/island/bridge.ts` 用"内容指纹 + id"避免本机录入的作业弹上岛——`homeworkFingerprint()` 由 `classId|assignDate|title|content` 拼成,`markHomeworkCreatedLocally()` 在发请求前登记指纹、回来后补记 id,`isLocalHomework()` 在 `pushHomeworkToIsland()` 前判定;指纹 TTL 60 秒。依据:`bridge.ts:191-267`;调用点在 `src/renderer/views/HomeworkView.vue:419-428`(录入前/后登记)。
- **目标(双层去重,职责明确)**:
1. **渲染层(快速路径)**:保留现有 `isLocalHomework()` 判定,命中则根本不发 `islandPush`;`markHomeworkCreatedLocally()` 调用点保留,不改交互时序。
2. **主进程(兜底去重)**:`island:push` payload 增加 `source: 'realtime'|'homework-local'|'replay'|'call'` 与 `dedupeKey`(作业用同一指纹;普通通知/叫人用 `source+id`);`main/island/ipc.ts` 校验字段、`main/island/messages.ts` 维护 LRU 去重表(key→过期时间,TTL 60s),命中直接丢弃并记 debug 日志。
3. **IPC 归属**:渲染层 `bridge.ts` 计算并传 `source/dedupeKey` → preload `islandPush` 原样透传 → 主进程 `main/island/ipc.ts` 校验 → `messages.ts` 去重后入队。不得在 preload 里做业务判断,也不得让主进程反查渲染层 store。
- **接口**:`IslandPushPayload`(`packages/shared/src/island/ipc.ts`)新增两个可选字段;旧调用点缺字段时主进程按 `id` 去重(兼容)。
- **为什么**:现状只有渲染层一层判定,实时事件先于接口响应到达时仍可能漏判;主进程兜底可保证"同一台机器录的作业绝不上岛",且离线补弹/重连重复投递也被覆盖。
- **风险**:指纹跨班/跨天误伤(必须带 classId+assignDate);主进程去重表不能无限增长(LRU+TTL);冒烟需要能注入/清理去重表(提供 `testResetHomeworkDedupe()` 或在冒烟模式禁用去重,名称归 part 11 测试基线)。
### 4.4 IPC 契约 v2(增量、不改旧通道语义)
| 通道 | 方向 | 现状 | v2 变更 |
|---|---|---|---|
| `island:get-layout` | 岛→主 | 返回 `islandLayout` | 返回 v2(`version:2`;含 fixed/schemes) |
| `island:save-layout` | 岛→主 | 归一化后保存 | 先 `normalizeIslandLayoutV2`,返回 v2 |
| `island:state` | 主→岛 | `IslandState` | 增加 `family/pendingStaged/bubblePhase/bubbleDeadline`(运行期,不落盘);旧字段不删 |
| `island:appearance` | 主→岛 | `IslandAppearance` | 增加 `family/colorScheme/theme/effectEnabled/effectScale/progressBar/winislandStyle`;旧字段保留 |
| `island:push` | 应用窗口→主 | `{notification, context}` | 增加 `source`/`dedupeKey`(作业指纹去重,见 4.3.13);缺省按 `id` 去重 |
| `island:action` | 岛→主 | 8 种动作 | 保留;新增 `open-message`(classisland fixed 行内气泡打开详情)与 `open-notifications`(「还有 N 条」打开应用内通知中心) |
| `island:content-size` | 岛→主 | 自定义尺寸上报 | 增加 `mode:'custom'|'classisland-fixed'` |
| `classisland:island:get-metrics` | 新 | — | 返回当前 layout/family 的 metrics(编辑/冒烟/调试用) |
| `classisland:island:editor-open` | 主→应用窗口 | 已有 `island:open-editor` | 保留旧通道;新通道可带 `{rowId,nodeId}` 定位 |
| `island:set-hit-rect`/`island:set-interactive` | 岛→主 | 已有 | 语义不变;fixed 信息行按真实行矩形上报 |
1. payload 可携带 `schemaVersion:2` **仅用于运行时校验/调试**,**不得落盘**;落盘版本只有 `islandLayout.version`。接收方遇到未知字段必须忽略而不是拒绝。
2. 通道名继续集中在 `src/main/channels.ts` 或 `shared/island/ipc.ts` 的**单一常量表**中;preload 不得直接写字符串字面量。
3. 旧通道(`island:get-layout` 等)不得改名/删除;改名只能通过"新通道 + 旧通道转发"完成,至少保留一个版本周期。
4. 所有冒烟注入/读取通道(`island:set-interactive`、`island:set-hit-rect`、`island:set-touch-mode`、`island:alive` 及主进程方法 `setTestInputPassthrough/setHitTestCursor/getWindowBounds/getHitRect/getInteractive/setBlurCollapseEnabled/setTouchMode/getTouchMode/isRecovering`)名称与语义保持不变,改造归属 part 11 测试基线。
### 4.5 迁移与门面的落地顺序(必须按序提交)
1. **M0 建契约层**:`shared/island/` 类型、默认值、normalize、migrate、metrics、geometry、messages、lessons、ipc;只加不删,旧 `island-layout.ts`/`constants.ts` 保持再导出。
2. **M1 拆主进程**:`main/island/*` 拆分 + 门面;行为与现状 1:1(不改变任何可见行为),用无头 `verify:desktop` 回归。
3. **M2 迁移层接入**:`config.ts` 读时迁移 + 版本号;用旧配置夹具验证(fixed/custom 各存一份)。
4. **M3 渲染层抽壳**:`shell/IslandShell.vue`、`MessagePanel.vue`、`LessonBar.vue`;winisland 外观不变。
5. **M4 新增 classisland fixed**:`FixedInfoRow.vue` + `InlineMessageBubble.vue` + `IslandSettingsView` family 选择器;默认新装走 classisland,老配置走 winisland;family 只影响 fixed。
6. **M5 自定义行改造**:`CustomIslandRuntime` 行级外观/隐藏规则/进度条/滚动 mode/行设置抽屉;custom 始终 ClassIsland 规格(老 custom 用户升级后有视觉变化,需在发布说明提示)。
7. **M6 编辑器改造**:v2 数据、行设置、预览、保存队列。
8. **M7 收尾**:CSS/文档/冒烟断言/迁移回滚演练。
每一步都必须"可独立回滚":新增文件先经 feature 开关(`appearance.family`/`config.islandLayout.version`)控制;不得在某一步同时删除旧文件与旧逻辑。
### 4.6 风险清单与强制缓解
| 风险 | 触发点 | 缓解(必须实现) |
|---|---|---|
| 拆分 `island.ts` 导致隐藏状态丢失 | M1 | 先对齐私有字段到接口;拆分后逐项对比 `getState()/getWindowBounds()/getHitRect()` 冒烟输出 |
| DOM/CSS 选择器断裂 | M3/M4 | 保留 `.island-card/.island-root/[data-anchor]/[data-style]` 钩子;视觉回归由 --visual 专项做 |
| 两套 family 逻辑分叉 | M4 | 业务状态机只保留一份(controller);family 只选择 fixed 渲染器,custom 不分叉 |
| fixed 语义变化引起老用户困惑 | M4 | 老配置迁移默认 `winisland`;设置页显著位置显示 family 并解释两种 fixed;custom 升级有视觉变化的提示 |
| custom 用户升级后观感变化 | M5 | 统一 ClassIsland 规格是定稿;发布说明+设置页提示;数据不变、可回滚 config 备份 |
| 迁移把用户自定义方案写坏 | M2 | 迁移前备份 `config.json`;迁移失败保留原文件;提供"恢复旧版布局" |
| 编辑器 855 行继续膨胀 | M6 | 先抽 composables;单文件不超过 500 行;新逻辑进 `renderer/island/editor/` |
| 非 Windows 触摸命中退化 | M1 | `setShape` 仅 win32 分支;其它平台走"整窗穿透 + 可见区命中"(02 §2.4 第 5 条) |
| 性能(行数/组件数增加) | M5 | metrics 计算缓存;测量只在依赖变化时执行;列表/行使用 `will-change` 谨慎 |
| 无头验证覆盖不到视觉 | M7 | 默认 quick 保持无头;视觉断言只在显式 `--visual` 下跑,并在文档写明 |
### 4.7 验收标准(第 4 部分)
1. 仓库中存在 `packages/shared/src/island/` 与 `packages/desktop-client/src/main/island/` 两个新目录,且 `src/main/island.ts` 只剩门面(≤50 行,内容为 import/export 与类型再导出)。
2. `src/main/index.ts`、`tray.ts`、`courseware.ts`、`smoke.ts` 对 `./island.js` 的 import 与调用**不需要改动**(门面签名兼容)。
3. `LayoutIslandApp.vue` 分发正确:`custom` 始终 `CustomIslandRuntime`(与 family 无关);`fixed` 在 classisland 下用 `FixedInfoRow`、winisland 下用 `ClassicIsland`;切换不新建第二个窗口、不残留旧命中矩形。
4. `fixed` 在 `family='classisland'` 下渲染为信息行(默认 1 行:日期 + 课程表;行高 40、圆角 8、背景不透明度 0.5、`progressBar.height` 默认 3px、消息行内气泡);在 `family='winisland'` 下保留原胶囊+详情卡行为与既有冒烟断言。
5. `CustomIslandRuntime` 支持行级外观/隐藏规则/进度条,行设置抽屉可打开并保存;滚动组件支持 `mode:'continuous'|'pause'`;`rows`/`expandedRows` 编辑语义与现状一致。
6. 外观新字段全部落地:`family`(只影响 fixed)、`colorScheme`、`theme`、`effectEnabled`、`effectScale`、`progressBar`、`winislandStyle`;`fontSize` 在 classisland 下为正文基准(新装 16)、winisland 下旧语义(13,迁移保留原值)。
7. 编辑器 / 设置页 / 组件设置全部读写 v2 数据;旧 v1 配置在首次启动被迁移且不丢字段(迁移细节按 05 验收)。
8. 所有新增/修改的 IPC 通道登记在单一常量表;旧通道全部保留;preload 未暴露 `ipcRenderer`;`island:push` 带 `source/dedupeKey` 且主进程有兜底去重表。
9. 默认 `pnpm verify:desktop`(无头 quick)通过;不得因为重构把任何断言改成"跳过";冒烟注入点名称/语义全部保留。
10. 代码中不存在 `setBackgroundMaterial`;不存在绕过 contextBridge 直接访问 Node 的渲染层代码;custom 不受 family 影响、不引用 winisland 样式命名空间。
11. 作业指纹链路双层生效:本机录入后 60 秒内,实时事件/接口响应先后到达都不产生岛消息;跨班/跨天不误伤;去重表有 TTL 上限且可被冒烟清理。
12. 文件级改造清单(4.2/4.3)逐项落地;`docs/app/island.md`、`docs/dev/implementation.md` 的 family/fixed/custom 描述与实现一致。
---
## 5. 数据契约、schema v2 与配置迁移
### 5.0 总则(必须遵守)
1. 本章定义重构后的**唯一数据契约**。所有读写岛配置的代码(主进程 `config.ts`、岛渲染进程、编辑器、设置页、冒烟)必须只经过 `packages/shared/src/island/schema.ts` 的类型与 `normalize.ts`/`migrate.ts` 的函数,不得各自解析 JSON。
2. 旧配置必须**无损升级**:`config.json` 现有的 `islandLayout`(fixed/custom、schemes、nodes、expandedRows、residentEnabled、pointerFade、lessonReminders)与 `island`(appearance 全字段)一个都不许丢;升级失败必须可回滚。
3. 新增 `appearance.family`:取值 `'classisland' | 'winisland'`。**新装默认 `classisland`;已存在配置的老用户升级默认 `winisland`**。`fixed` 的语义随 family 分叉:`classisland` 下 fixed = ClassIsland 信息行(默认 1 行 = 日期 + 课程表,行高 40、圆角 8、不透明度 0.5、3px 进度条、消息行内气泡);`winisland` 下 fixed = 原胶囊 + 详情卡。
4. `islandLayout.version` 必须写入;当前版本 `2`。版本号是**唯一迁移判据**,不得用"字段是否存在"猜版本(字段判断只作为迁移内部的兜底)。
5. 写入必须沿用现有 `config.json` 的"内存缓存 + 120ms 合并写 + 退出前 flush"语义(`packages/desktop-client/src/main/config.ts:235-346`),**不得**为迁移新增同步写盘路径;迁移本身例外(必须一次写成功并立即 flush)。
6. 客户端不得把 v2 数据写回给旧版本可见的字段后又不写版本号;写 v2 必须同时写 `version:2`,保证降级/回滚可判断。
### 5.1 落点与版本治理
| 数据 | 位置 | 版本字段 | 说明 |
|---|---|---|---|
| 运行期配置 | `<userData>/config.json` | 无独立文件版本 | 版本只在 `islandLayout.version` 上表达(唯一版本字段) |
| 岛布局 | `config.json.islandLayout` | `version`(新增,当前 2) | 方案/行/节点/外观的结构版本;唯一迁移判据 |
| 岛外观 | `config.json.island` | 无独立版本 | 增加 `family`、`theme`、`effectEnabled`、`effectScale`、`progressBar`、`fontSizes`;`style` 保留为 surface 层 |
| 迁移前备份 | `<userData>/config.json.v1.bak` | — | 迁移前完整拷贝,只保留一份(覆盖式) |
| 迁移标记 | `<userData>/config.island-migration.json` | `{from,to,at,appVersion}` | **仅诊断用**,不参与版本判断、不是第二套版本字段 |
1. **只保留 `islandLayout.version` 一个落盘版本字段**:不得新增 `config.schemaVersion` 或其它文件级版本号;迁移判定只看 `islandLayout.version`(缺失=v1)。
2. 迁移必须在 `app.whenReady()` 后、`island.init()` 之前完成一次;不得由渲染进程触发,也不得在岛窗口创建后再迁移(否则窗口会先按旧数据渲染一帧)。
3. 迁移成功后必须立即 `flushConfigSync()`;失败时不得写回半成品:保留原 `config.json`,删除 `.tmp`,应用以内存默认 v2 运行并在设置页显示"迁移失败,已保留原配置"。
4. 读到 `islandLayout.version > 2` 时应用必须**只读**(不写回),并在设置页提示"配置来自更高版本"。
### 5.2 `appearance` v2(`config.json.island`)
在现有 `IslandAppearance` 基础上**只增不改**,旧字段全部保留(`packages/shared/src/types.ts:1145-1195`)。新增/语义变化如下:
| 字段 | 类型 | 默认值(新装) | 说明与依据 |
|---|---|---|---|
| `family` | `'classisland' \| 'winisland'` | `'classisland'`(老用户迁移为 `'winisland'`) | **风格族字段的唯一落点**;**作用域只限 fixed**(classisland=信息行;winisland=旧胶囊+详情);`custom` 始终 ClassIsland 规格、不受影响 |
| `colorScheme` | `'light' \| 'dark' \| 'system'` | `'system'` | 岛配色模式:跟随应用主题(system,默认)/强制浅色/强制深色;只影响岛自身渲染,不改主窗口主题 |
| `theme` | `'fluent' \| 'classic'` | `'fluent'` | 仅 `family='classisland'` 的 fixed 使用;对应 ClassIsland Fluent/Classic 两套主题;具体 token 由 06 定义 |
| `effectEnabled` | boolean | `true` | **强提醒特效总开关**(对应 ClassIsland 的提醒特效 Topmost/Effect 是否启用);关闭后只保留行内气泡/普通提醒 |
| `effectScale` | `1 \| 0.75 \| 0.5` | `1` | 强提醒特效渲染缩放(对应 ClassIsland `NotificationEffectRenderingScale`,默认 1.0);只允许三档 |
| `progressBar` | `{height:number, show:boolean}` | `{height:3, show:true}` | 岛内进度条统一配置:高度默认 3px,总开关默认显示;语义见 5.3 行级进度条说明 |
| `fontSize` | number | classisland=`16`(正文基准,派生 14/18/20 阶梯);winisland=`13`(旧语义) | `family='classisland'` 时它是正文基准字号,`fontSizes` 由它派生;`family='winisland'` 保持旧语义;**迁移时旧配置保留原值,新安装默认 16** |
| `fontSizes` | `{secondary:number, body:number, emphasized:number, large:number}` | 由 `fontSize` 派生:classisland `14/16/18/20`;winisland `≈14/16/18/20`(按 `fontSize/13` 比例) | ClassIsland 四档字号;显式配置优先于派生(见 5.4) |
| `height` / `width` | number | `44` / `268` | 仅 `family='winisland'` 的胶囊尺寸;classisland 下不作为信息行尺寸 |
| `radius` | number | `20`(winisland)/`8`(classisland 行为默认) | classisland 下作为行默认圆角;解析时取 `family` 对应默认 |
| `opacity` | number | `1` | 岛整体不透明度;classisland 下行背景不透明度另见行设置(默认 0.5) |
| `winislandStyle` | `'black' \| 'glass' \| 'tinted'` | `'black'` | **winisland 调色板字段的 canonical 名**:纯黑/半透明/主题色渐变;仅 `family='winisland'` 的 fixed 使用 |
| `style` | `'black' \| 'glass' \| 'tinted'` | 镜像 `winislandStyle` | **兼容镜像字段(不删除)**:旧配置读入时若没有 `winislandStyle` 则用它;写出时同时写 `winislandStyle` 与 `style`,保持旧版本可读;建议 v3 再评估废弃 |
| `position`/`alwaysOnTop`/`animations`/`speed`/`idleSliver`/`marginX`/`marginY`/`followCursorDisplay`/`accent` | 不变 | 不变 | fixed(两种 family)与 custom 共用;classisland fixed 也用停靠与边距 |
1. **字段职责必须严格区分**:`family` 只决定 **fixed** 用哪套渲染器(ClassIsland 信息行 / WinIsland 胶囊);`winislandStyle`(兼容镜像 `style`)只决定 winisland 的调色板;`theme` 只决定 classisland fixed 的 Fluent/Classic;`colorScheme` 只决定浅色/深色/跟随。四者不得互相覆盖、不得合并。`custom` 不使用 `family`/`theme`/`winislandStyle`(始终 ClassIsland 规格),但仍读取 `colorScheme` 决定明暗。
2. `family` 归一化:只接受两个值,非法值回落 `'classisland'`(新装默认);迁移函数负责把老用户写成 `'winisland'`(见 5.4)。
3. `colorScheme` 归一化:只接受 `light/dark/system`,非法回落 `system`;缺失补 `system`。
4. `fontSize` 归一化:8–96;classisland 下作为正文基准,`fontSizes` 缺失时由它派生(body=`fontSize`,secondary=`round(fontSize*14/16)`,emphasized=`round(fontSize*18/16)`,large=`round(fontSize*20/16)`);winisland 下保持旧语义(`fontSizes` 缺失时按 `fontSize/13 × 14/16/18/20` 推导)。迁移时旧 `fontSize` 原值保留,不强行改成 16。
5. `fontSizes` 归一化:每项 8–96;显式值优先于派生;缺失项按第 4 条补齐。
6. `theme` 仅 classisland fixed 生效;非法值回落 `'fluent'`;`winisland` 下忽略但不删除该字段。
7. `effectEnabled` 必须为 boolean(缺省 true);`effectScale` 必须夹取到 `[1, 0.75, 0.5]`(缺省 1,非法值取 1);`progressBar.height` 夹取 1–8、`progressBar.show` 缺省 true;`progressBar` 缺失时整对象补默认。
8. `winislandStyle` 归一化:仅 `black/glass/tinted` 合法,非法回落 `black`;`style` 作为镜像字段同步写;读入优先 `winislandStyle`,回退 `style`。**切换 family 时保留用户 `winislandStyle` 原值,不重置**(UI 默认值提示归 09)。
### 5.3 `islandLayout` v2(`config.json.islandLayout`)
v2 顶层(`IslandLayoutV2`):
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| `version` | number | `2` | 必须存在 |
| `layout` | `'fixed' \| 'custom'` | `'fixed'` | 布局模式(沿用旧字段) |
| `fixed` | `FixedSchemeV2` | 见下 | classisland fixed 信息行配置;winisland 下忽略 |
| `schemes` | `IslandLayoutSchemeV2[]` | 1 个默认方案 | 自定义方案(沿用旧结构并扩展) |
| `activeSchemeId` | string | `'default'` | 当前方案 |
| `residentEnabled` | boolean | `layout==='custom'` | 兼容字段;不得再作为独立开关 |
| `pointerFadeEnabled` / `pointerFadeReversed` | boolean | `true` / `false` | 鼠标避让淡化(沿用) |
| `lessonReminders` | `LessonReminderSettings` | 沿用旧默认 | 准备/上课/课间/结束/课中进度开关与秒数 |
| `updatedAt` | number | `0` | 最近保存时间(仅诊断) |
`FixedSchemeV2`(classisland fixed 的信息行):
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| `rows` | `IslandRowV2[]` | 1 行 = 日期 + 课程表 | 信息行;默认只有一行 |
| `mainRowId` | string | 首行 id | 主行(承载消息气泡/课程提醒) |
| `rowHeight` | number | `40` | ClassIsland 行高(32–64 夹取) |
| `rowSpacing` | number | `5` | 行间距(0–150) |
| `backgroundOpacity` | number | `0.5` | 行背景不透明度 |
| `cornerRadius` | number | `8` | 行圆角(0–20) |
| `progressBarHeight` | number | `3` | 行内进度条高度(3px) |
| `messageBubble` | `'inline' \| 'card'` | `'inline'` | fixed 下消息表现:inline=行内气泡,card=老式详情卡(仅回退用) |
| `maxRows` | number | `3` | 信息行最多行数(自动换行/溢出策略由 07 定义) |
`IslandLayoutSchemeV2`(在 v1 方案上扩展):
| 字段 | 类型 | 默认值 | 变化 |
|---|---|---|---|
| `id`/`name`/`rows`/`expandedRows` | 沿用 | 沿用 | 结构不变;行内容升级为 `IslandRowV2` |
| `scale` | number | `1` | 0.75–2.5(沿用于 `CLASSISLAND_ISLAND_METRICS.scaleMin/Max`) |
| `color`/`foreground`/`opacity`/`radius`/`separated`/`spacing` | 沿用 | 沿用 | `radius` 0–20;`spacing` 0–150 |
| `rowHeight` | number | `40` | 新增:行高 32–64 |
| `rowSpacing` | number | `5` | 新增:与 `spacing` 同步;`spacing` 保留兼容 |
| `theme` | `'fluent' \| 'classic'` | 继承 `appearance.theme` | 新增:方案级主题覆盖 |
| `fontSizes` | `{secondary,body,emphasized,large}` | 继承 `appearance.fontSizes` | 新增:方案级字号覆盖 |
| `progressBarHeight` | number | `3` | 新增:行内进度条高度 |
| `unrecognizedNodes` | `object[]` | `[]` | 新增:未知 kind 的原始节点(**不得丢弃**,见 5.5 第 6 条) |
`IslandRowV2`(行):
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| `id`/`name`/`main`/`allowNotifications`/`gap`/`nodes` | 沿用 | 沿用 | `main` 行唯一;`gap` 0–64 |
| `appearance` | `RowAppearance` | 见下 | 行级外观(新增) |
| `hideRules` | `RowHideRules` | 全 false | 行级隐藏规则(新增) |
| `progress` | `RowProgress` | 见下 | 行级进度条(新增) |
`RowAppearance`:`backgroundEnabled=false`、`backgroundColor='#000000'`、`backgroundOpacity=0.5`、`foregroundColor='#ffffff'`、`cornerRadius=8`、`opacity=1`、`fontSizes`(继承方案/外观)。
`RowHideRules`:`inClass=false`、`noMessage=false`、`noClass=false`、`fullscreen=false`;全部为 false 表示不隐藏。语义对应 ClassIsland 的行隐藏规则(`repos/ClassIsland/ClassIsland.Core/Models/Components/MainWindowLineSettings.cs` 的 `HideOnRule`/`HidingRules`)。
`RowProgress`:`enabled=true`、`style='line'`、`height=3`、`color='accent'`、`showRemaining=true`。
**行内 3px 进度条的语义(数据来源不在本层实现)**:进度条表示"当前显示内容的时间进度"——消息气泡显示时 = 该消息剩余显示时间;否则 = 当前课程进度。具体数据来源与取值规则由 07/08 定义,本 schema 只保证 `progress`(行级)与 `appearance.progressBar`(全局)提供 `height/show` 两个开关;行设置抽屉的完整字段由 09 定义,本章只登记字段名(`appearance/hideRules/progress`)并要求与 09 对齐,不得在 05 里发明抽屉字段。
`IslandNodeV2`(节点,在 v1 `IslandComponentNode` 上扩展,字段名不变):
| 字段 | 变化 |
|---|---|
| `id`/`kind`/`appearance`/`advanced`/`children` | 沿用(`appearance` 四档字号默认改为继承行/方案) |
| `settings.schedule.display` | 沿用 `'current'|'next'|'today'`;新增 `today` 折叠规则由此驱动 |
| `settings.schedule.progressStyle` | 新增:`'bar'|'line'`,默认 `'line'`(3px) |
| `settings.schedule.showProgress` | 沿用;默认 `true` |
| `settings.schedule.showPeriod`/`count` | 沿用 |
| `settings.mode`(仅 `kind='rolling'`) | 新增:`'continuous' | 'pause'`,默认 `'continuous'`;`pause` 表示滚动到边界后停顿,停顿时长由同组件的 `settings.interval`(秒,1–300)控制 |
| `advanced.hideRules` | 沿用 `in-class/no-message/no-class`;新增 `fullscreen`(与行级一致) |
| `advanced.rowSpan` | 新增:仅容器组件使用,默认 `1` |
**默认方案(新装/重置)**:`layout:'fixed'`、`family:'classisland'`、`fixed.rows=[{main:true, allowNotifications:true, gap:12, nodes:[date, schedule]}]`,`schemes=[默认方案:main 行含 date/clock/schedule/notification]`。数值依据:`packages/shared/src/island-layout.ts:217-255`(现有默认方案)+ `CLASSISLAND_ISLAND_METRICS`(`:19-36`)。注意:`schemes` 是 custom 布局数据,**描述 ClassIsland 规格**且不随 family 改变;`fixed` 配置只在 `family='classisland'` 时由 fixed 渲染器读取。
### 5.4 v1 → v2 迁移算法(必须逐步实现,可重入)
**输入判定**:
1. 读取 `config.json`(不存在 → 全新安装:直接构造 v2 默认,`family='classisland'`,**不写迁移标记**)。
2. 文件存在但 `islandLayout.version` 缺失:视为 v1(唯一版本判据;**不得**再引入 `schemaVersion` 作为第二套版本号)。
3. 老用户判定:`islandLayout.version` 缺失 **或** 文件里存在 `islandLayout`/`island` 任一字段 → 迁移时 `family='winisland'`(保持原外观,不惊吓用户)。
4. 全新安装判定:文件不存在、或既无 `islandLayout` 又无 `island` → `family='classisland'`。
**步骤(必须按序、每步失败可中止)**:
```text
step 0 备份:把 config.json 原样复制到 config.json.v1.bak(存在则覆盖);写 config.island-migration.json {from,to,at}(仅诊断,不是版本字段)
step 1 外观迁移(appearance):
- family := 'winisland'(老用户)| 'classisland'(新装)
- colorScheme := 'system'(缺失补默认;已有值原样保留)
- winislandStyle := 旧 style(缺省 'black');style := winislandStyle(兼容镜像)
- theme := 'fluent'(新装默认;老用户也写 'fluent',仅 classisland fixed 生效)
- effectEnabled := true(缺失补默认)
- effectScale := 1(缺失补默认;只允许 1/0.75/0.5)
- progressBar := { height:3, show:true }(缺失补默认)
- fontSize:迁移保留旧值(classisland 新装写 16;老用户写原值)
- fontSizes := 由 fontSize 派生:
老用户(winisland):secondary=round(fontSize*14/13), body=round(fontSize*16/13),
emphasized=round(fontSize*18/13), large=round(fontSize*20/13)
新装(classisland):14/16/18/20(fontSize=16 为正文基准)
- 所有旧字段原样保留(height/width/radius/opacity/accent/fontSize/style/...)
step 2 布局迁移(islandLayout):
- version := 2(唯一落盘版本字段)
- layout := 旧 layout(缺省 'fixed')
- residentEnabled := (layout==='custom')(覆盖旧的独立值,保证一致)
- pointerFadeEnabled/pointerFadeReversed/lessonReminders 原样保留
- fixed := 若旧文件无 fixed 字段 → 生成 FixedSchemeV2 默认(1 行 date+schedule;progressBar 继承 appearance.progressBar)
- schemes := 旧 schemes 逐个升级:
rowHeight := 40;rowSpacing := 旧 spacing;theme := 'fluent';
fontSizes := 继承 appearance.fontSizes;progressBarHeight := appearance.progressBar.height;unrecognizedNodes := []
每行补 appearance/hideRules/progress 默认;
每节点补 settings.schedule.progressStyle='line'、advanced.rowSpan=1;
若节点 kind='rolling' 补 settings.mode='continuous'(pause 保留旧 interval 语义)
- 未知 kind 的原节点移入该方案的 unrecognizedNodes(保留原始 JSON,不参与渲染)
step 3 校验与夹取:调用 normalizeIslandLayoutV2 / normalizeAppearanceV2(见 5.5);
校验失败项收集成 warnings,不得中止;结构性错误(schemes 为空等)用默认方案兜底
step 4 写回:saveConfig({ islandLayout, island }) 后立即 flushConfigSync()(**不写 schemaVersion**)
step 5 读回校验:重新读取并 deep-equal 比较关键字段(islandLayout.version/family/layout/方案数/节点数);
不一致 → 从 config.json.v1.bak 恢复、删除迁移标记、报错并降级为"只读默认 v2"
```
1. **可重入/幂等**:若 `islandLayout.version===2`,迁移函数必须直接返回原对象(不写盘、不改字段);重复执行不得产生二次修改。
2. **老用户 fixed 不变**:迁移后 `family='winisland'`,fixed 仍渲染原胶囊+详情卡(`ClassicIsland.vue`),因此老用户升级后视觉 0 变化;只有用户主动在设置页切到 `classisland` 或新装用户才看到信息行。
3. **custom 老用户**:`custom` **始终按 ClassIsland 规格渲染、不受 family 影响**;迁移只升级数据(补行级默认、滚动 mode 等),不改 schema 结构。老 custom 用户升级后视觉会变为 ClassIsland 行规格(这是定稿行为),发布说明与设置页必须提示;fixed 用户(默认 winisland)不受影响。
4. 编辑器打开时必须先读迁移后的 v2;若 `islandLayout.version` 不匹配(例如用户用旧版应用改过文件),必须触发一次"再迁移"而不是把 v1 当 v2 解析。
5. 迁移必须记录 warning 列表到内存并在设置页"存储/诊断"可查看(哪几项被夹取、哪几个未知组件被保留)。
### 5.5 校验与夹取(`normalizeIslandLayoutV2` / `normalizeAppearanceV2`)
必须在 shared 中实现,主进程保存前、编辑器加载后、IPC 存入前**三处共用同一个函数**。规则:
1. `version`:仅接受 `2`;缺失/非法按迁移处理,不得静默改成 2 后继续用旧结构。
2. `layout`:`'fixed'|'custom'`,非法回落 `'fixed'`。
3. `family`:`'classisland'|'winisland'`,非法回落 `'classisland'`。
4. 数量上限:行 ≤12/方案、节点 ≤200/全部方案、嵌套深度 ≤6、方案 ≤20;`main` 行唯一(多余取消)。
5. 数值夹取:`rowHeight 32–64`、`rowSpacing 0–150`、`gap 0–64`、`radius 0–20`、`opacity 0–1`、`backgroundOpacity 0–1`、`fontSizes 8–96`、`scale 0.75–2.5`、`rowSpan 1–6`、`progressBarHeight 1–8`;非法/NaN 回落默认值(禁止 `Math.max(min, NaN)` 这种写法,参考 `src/main/island.ts:2225-2241` 的 fallback 说明)。
6. 未知 kind:**不得丢弃**。把原始节点 JSON 放入 `scheme.unrecognizedNodes`(最多 200 条),渲染时忽略并显示"未知组件(已保留)";这样用旧版应用打开再保存也不会丢数据。
7. 颜色:统一 `#rrggbb` 小写;非法回落默认;透明度字段独立,不把 `#rrggbbaa` 塞进颜色字段。
8. id:空/非字符串/重复 → 重新生成(`node-<n>`/`row-<n>`/`scheme-<n>`);id 最长 100 字符。
9. 文本:`settings.text` ≤2000 字符、`locationName/countdownTitle` ≤64;超长截断并记 warning。
10. 经纬度:`latitude -90..90`、`longitude -180..180`,可空;空表示未配置(组件显示"请设置天气地点")。
11. 未知字段:v2 对象里出现但类型未定义的字段必须**原样保留**在对象上(不删除),以便前向兼容。
12. `lessonReminders`:`prepareSeconds 5–1800`、`endingSeconds 5–600`(依据现有 `packages/shared/src/island-layout.ts:390-398`)。
13. `winislandStyle` / `style`:两者都只接受 `black/glass/tinted`,非法回落 `black`;读入优先 `winislandStyle`、回退 `style`,写出两者同步(兼容镜像);**不得**因 family 迁移而删除 `style`;family 与调色板相互独立(family 决定 fixed 用哪套渲染,winislandStyle 决定 winisland 的调色板)。
14. `theme`:只接受 `fluent/classic`,非法回落 `fluent`;具体色板/圆角/阴影 token 由 06 定义,05 只登记字段与默认值。
15. `colorScheme`:只接受 `light/dark/system`,非法回落 `system`;`system` 时跟随应用 `html.dark` 状态决定岛明暗(映射逻辑由渲染层实现,数据层只存枚举)。
16. `fontSize`:8–96,缺省按 family 取(classisland=16 正文基准、winisland=13 旧基线);`fontSizes` 缺失时按 5.2 第 4 条派生;显式 `fontSizes` 优先;迁移不得覆盖旧 `fontSize`。
17. `effectEnabled`:boolean,缺省 `true`;`effectScale`:只接受 `1/0.75/0.5`,缺省 `1`,非法取 `1`;`progressBar`:`{height:1–8, show:boolean}`,缺省 `{3,true}`。
18. 进度条高度优先级:`行 progress.height` > `方案 progressBarHeight` > `appearance.progressBar.height`;任一缺失沿上一级回落,最终默认 3。`progressBar.show=false` 时所有行都不显示(行级 `enabled=false` 只关该行)。
19. 滚动组件:`settings.mode` 只接受 `continuous/pause`,缺省 `continuous`;`pause` 时停顿秒数复用 `settings.interval`(夹取 1–300);`continuous` 时 `speed` 夹取 1–200(沿用现有范围)。
20. 渲染/样式命名空间**只由 layout(fixed 的 family)决定,不写入数据**:`fixed + family='winisland'` → winisland 命名空间;`fixed + family='classisland'` → classisland 命名空间;**`custom` 始终 classisland 命名空间、不受 family 影响**;normalize 不得改写样式类名/选择器相关字段。
21. `IslandNotification.level`、`IslandLessonReminder.progress/leftSeconds`、`IslandState.family/pendingStaged/bubblePhase/bubbleDeadline` 均为**运行期派生字段**,normalize 不处理、不落盘。
### 5.6 DTO 映射(ClassHelper 数据 → ClassIsland 语义)
**fixed(classisland)行内气泡规则**:同一时刻只显示 1 条 active 气泡;点击展开详情/操作;有多条未处理消息时不并列显示,而是折叠为「还有 N 条」,点击打开应用内通知中心(沿用 ClassHelper 现有通知中心路由);消息队列、去重、上课暂存、已读回执规则完全不变(02 §2.5/§2.12)。
**`IslandState` → ClassIsland 主界面状态**:
| ClassHelper 字段 | ClassIsland 语义 | 渲染映射(classisland fixed) |
|---|---|---|
| `mode:'hidden'` | 岛不可见(仅提醒特效时临时显示) | 信息行隐藏/仅保留细缝 |
| `mode:'pill'` | 紧凑态 | 行内气泡/铃铛提示 |
| `mode:'expanded'` | 展开态 | 行展开 + 消息面板 |
| `active`/`queued` | 提醒队列(当前播放/待播) | 行内气泡列表,按 `PRIORITY_RANK` 排序 |
| `inClass` | `TimeState=OnClass` | 行隐藏规则 `inClass` 生效、提醒遮罩可用 |
| `currentPeriodEnd` | 当前时间点结束 | 进度条/倒计时取值 |
| `reason` | 提醒来源(新消息/课后补发/紧急/叫人/收回中) | 气泡入场动画与角标 |
| `listExpanded` | 列表是否展开更多 | 行内"展开更多" |
| `lessonReminder` | 课级提醒(本地) | 行覆盖层(遮罩/秒数徽标) |
| `coursewareTransfer` | 课件提示(覆盖态) | 最高优先级覆盖全部行 |
**`IslandNotification` → ClassIsland 提醒语义**:
| ClassHelper 字段 | ClassIsland 对应 | 规则 |
|---|---|---|
| `priority: LOW/NORMAL/HIGH/URGENT` | 提醒队列优先级(`NotificationPriority`) | URGENT=插队立即播放;HIGH/NORMAL/LOW 按排序入队 |
| `kind:'notification'` | 普通提醒(Mask+Overlay 两阶段) | 两阶段时长取 5s/5s(`repos/ClassIsland/...NotificationRequest.cs:17-20`) |
| `kind:'homework'` | 作业提醒(本地合成 id `homework-<id>`) | 不进入已读回执;不可"标为已读" |
| `kind:'call'` | 叫人(Call) | 上课时保持可点、不自动展开;紧急叫人按 URGENT |
| `title/content/teacherName/courseName/createdAt` | 提醒标题/正文/来源/时间 | 气泡第一行标题、第二行来源+时间 |
| `id` | 去重键 | 双链路(通知/叫人)合并时 id 相同即合并(见 02 §2.5 第 3 条) |
**`ScheduleDto` → ClassIsland 课表语义(仅映射,不落 Profile 文件)**:
| ClassHelper 字段 | ClassIsland 概念 | 映射规则 |
|---|---|---|
| `id` | `ClassInfo`/条目标识 | 仅内部使用 |
| `course.courseName` | `Subject.Name` | 直接展示;null 显示"—" |
| `dayOfWeek`(1–7) | `TimeRule.WeekDay` | 1=周一…7=周日 |
| `startTime`/`endTime`(HH:mm) | `TimeLayoutItem.StartTime/EndTime` | 转 `HH:mm:00`;只用于显示与进度计算 |
| `weekStart`/`weekEnd` | 周次范围 | 用于"本周是否有课" |
| `weekParity: ALL/ODD/EVEN` | `TimeRule.WeekCountDiv`(0/1/2)+ `WeekCountDivTotal=2` | ALL→0;ODD→1;EVEN→2 |
| `location` | 附加信息 | 仅在气泡/详情展示,不参与时间点 |
**`IslandTimePointDto` → `TimeLayoutItem`**:
| ClassHelper | ClassIsland | 规则 |
|---|---|---|
| `timeType: LESSON/BREAK/DIVIDER/ACTION` | `TimeType: 0/1/2/3` | 一一对应(`repos/ClassIsland/...TimeLayoutItem.cs:119-138`) |
| `name`(课间名/时间点名) | `BreakName`/名称 | 课间空时显示"课间休息" |
| `courseName` | 默认课程 | null 等价 `Guid.Empty`(无课) |
| `startTime`/`endTime` | 开始/结束 | 用于行内课程表折叠与进度 |
| `isHideDefault` | `IsHideDefault` | 折叠时隐藏、轮到时展开 |
1. 上述映射只用于**渲染语义对齐**,不得把 `ScheduleDto` 落盘成 ClassIsland 档案;数据仍以服务端解析结果为准(02 §2.12)。
2. `CoursewareTransfer` 映射为"最高优先级覆盖态",不进入提醒队列、不参与已读;映射字段 `name/category/teacherName/receivedBytes/totalBytes/progress/stage/error` 直接使用(`packages/shared/src/courseware.ts`、`src/island/CoursewareTransfer.vue:12-65`)。
**运行期状态扩展(`IslandState`,不落盘,必须随状态推送)**:
| 字段 | 类型 | 默认值 | 语义 |
|---|---|---|---|
| `family` | `'classisland' \| 'winisland'` | 当前 `appearance.family` | 当前生效风格;渲染进程据此切换 CSS 命名空间 |
| `pendingStaged` | number | `0` | 上课期间被暂存、下课后待补弹的消息数(对应现有 `queued` 中因上课被压住的数量;`queued.length` 不变) |
| `bubblePhase` | `'idle' \| 'mask-in' \| 'content-in' \| 'out'` | `'idle'` | 行内气泡/提醒的两阶段播放阶段(固定 5s/5s 时长的可视化映射) |
| `bubbleDeadline` | number | `0` | 当前气泡自动收起的时间戳(毫秒);`0` 表示无 |
1. `IslandState` 的新字段只存在于**运行期推送**(`island:state`),不得写进 `config.json`、不得被编辑器保存;旧渲染进程缺字段时必须按默认值渲染(前向兼容)。
2. `pendingStaged` 必须由主进程根据现有上课暂存逻辑计算(02 §2.5 第 2 条),不得让渲染进程自行推断。
**`IslandNotification.level`(展示层级,派生字段)**:
| `priority` | `level` | 用途 |
|---|---|---|
| `URGENT` | `'emergency'` | 紧急气泡/强提醒特效可触发 |
| `HIGH` | `'high'` | 高亮气泡/角标 |
| `NORMAL` | `'normal'` | 普通气泡 |
| `LOW` | `'low'` | 弱化展示 |
`level` 由 priority 派生,**不落库、不持久化**;通知进入岛队列时在 `main/island/messages.ts` 统一计算,渲染层只读取。
**`IslandLessonReminder` 扩展字段(运行期,不落盘)**:
| 字段 | 类型 | 语义 |
|---|---|---|
| `progress` | number (0–1) | 当前提醒/课程进度:`ending/in-class` = `(now-lessonStartsAt)/(lessonEndsAt-lessonStartsAt)`;其余 = `(endsAt-now)/(endsAt-startedAt)`(与现有 `LessonReminderContent.vue:36-50` 计算一致) |
| `leftSeconds` | number | 下课前剩余秒(`ceil((lessonEndsAt-now)/1000)`,clamp ≥0),供蓝色圆形秒数徽标与遮罩标题使用;现有 `LessonReminderContent.vue:24-35` 已有同义计算 |
| `kind` | 不变 | 仍为 `prepare/on-class/break/ending/in-class` |
**fixed(classisland)下的提醒叠加口径**:`prepare`/`on-class`/`break` 以**行内容接管**呈现(临时替换课程表内容,动画由 08/10 定义);`ending` 不接管行内容,只在课程名右侧显示蓝色圆形秒数徽标(`leftSeconds` 驱动);`custom` 模式不接管,保持课程组件 + 秒数徽标。该口径只影响渲染,不改变 02 §2.8 的五类提醒计算与开关语义。
### 5.7 IPC payload 版本与兼容
1. IPC payload **可以**携带 `schemaVersion:2` 仅用于运行时校验/调试,**不得落盘、不得作为配置版本判据**;配置版本只认 `islandLayout.version`。`island:state`/`island:appearance`/`island:get-layout` 三个高频 payload 建议带上该字段(旧字段保留,新增字段可选)。
2. 接收方规则:**未知字段忽略、缺字段回落默认**;不得因为缺 `family` 就拒绝渲染(老渲染进程遇到新主进程时应按 `winisland` 降级,反之按 `classisland`)。
3. `island:save-layout` 必须返回归一化后的 v2 结果(编辑器以返回值为准,避免"本地看着对、主进程夹取后不一致")。
4. 主进程 `getConfig()` 返回给渲染进程的 `islandLayout` 必须是 v2;`island` 必须含 `family` 与新增外观字段(`theme/effectEnabled/effectScale/progressBar`)。
5. 旧通道名全部保留(见 04 §4.4);新增通道必须同时在 `shared/island/ipc.ts` 与 preload 中登记,不得字符串散落。
6. 作业指纹相关 payload(见 04 §4.3.13)必须带 `source`(`realtime/homework-local/replay/call`)与 `dedupeKey`;主进程按 key 去重,渲染进程保留现有"本机刚录作业免上岛"快速路径。
### 5.8 编辑器预览与临时状态(不持久化)
1. 编辑器的内嵌预览、缩放、选中项、拖拽态属于**临时状态**,不得写入 `config.json`;只有"应用/保存"才写 v2。
2. "无消息展开内容(expandedRows)"与常态 `rows` 分开编辑、分开保存;`activeSchemeId` 只在切换方案时写。
3. 设置页"测试灵动岛"仍走 `islandPush` + `context.preview=true`(30 秒预览、失焦不收起,见 02 §2.15),预览消息 id 必须带前缀(如 `appearance-test-`)以便清理;预览不写历史、不改已读。
4. 编辑器打开时若检测到迁移 warning,必须在顶部显示"部分旧设置已按新格式调整(可查看详情)",不得静默。
### 5.9 回滚与降级
1. **回滚入口**:设置页"灵动岛 → 恢复升级前布局"必须存在;回滚步骤:先备份当前 `config.json` → 把 `config.json.v1.bak` 覆盖回去(该文件天然没有 `islandLayout.version`,即 v1)→ 提示重启 → 重启后旧版本行为恢复(family=winisland/fixed=胶囊)。
2. **迁移失败降级**:应用不得崩溃;以内存默认 v2 启动(不写盘),岛显示默认信息行,设置页显示错误与"恢复备份"按钮。
3. **前向兼容**:读到 `islandLayout.version>2` 时,应用只读该配置(不覆盖写),在设置页提示"配置由更高版本创建";不存在其他版本字段可用于判断。
4. **文件损坏**:`config.json` 非法 JSON 时沿用现有 `.bak` 回退逻辑(`config.ts` 现状:读取失败回落默认),回滚后仍必须按 `islandLayout.version` 缺失执行一次迁移判定;不得用损坏文件覆盖 `.bak`。
5. **旧版应用打开新文件**:不可避免(旧版不认识 v2 字段),因此 v2 必须保持"未知字段保留"原则(5.5 第 11 条)并把关键新数据放在旧版会**忽略而不是报错**的位置;旧版保存后新字段可能丢失,需在文档提示用户不要跨版本反复切换。
### 5.10 版本变更记录与兼容矩阵
必须维护一张可追溯的变更表(代码里以常量数组或文档形式存在),用于判断"某个字段从哪版开始有、哪版废弃":
| 变更 | 版本 | 兼容策略 |
|---|---|---|
| `islandLayout.version` 新增(唯一落盘版本字段) | v2 | 缺失=1,按 5.4 迁移 |
| `appearance.family` 新增 | v2 | 缺失时:老配置迁移=winisland;新装=classisland |
| `appearance.theme`(fluent/classic)新增 | v2 | 缺失=fluent;仅 classisland 生效;具体 token 由 06 定义 |
| `appearance.effectEnabled` / `effectScale` 新增 | v2 | 缺失 `true`/`1`;effectScale 只允许 1/0.75/0.5 |
| `appearance.progressBar {height,show}` 新增 | v2 | 缺失 `{3,true}`;高度优先级 行 > 方案 > 外观 |
| `appearance.fontSizes` 新增 | v2 | 缺失时按旧 `fontSize` 比例推导(5.4 step1) |
| `fixed`(信息行)新增 | v2 | 缺失时生成默认 1 行(date+schedule);仅 classisland fixed 使用 |
| 行级 `appearance/hideRules/progress` 新增 | v2 | 缺失时补默认;字段名与 09 对齐;winisland fixed 下保留数据但忽略(custom 始终使用) |
| 方案级 `rowHeight/rowSpacing/theme/fontSizes/progressBarHeight` 新增 | v2 | 缺失时继承 appearance 默认 |
| `appearance.colorScheme` 新增 | v2 | 缺失=`system`;非法回落 `system` |
| `appearance.fontSize` 语义按 family 分叉 | v2 | classisland=正文基准(新装 16,派生 14/18/20);winisland=旧语义(13);迁移保留旧值 |
| `appearance.winislandStyle` 新增,`style` 转兼容镜像 | v2 | 读优先 `winislandStyle`、回退 `style`;写两者同步;`style` 不删除;切换 family 保留原值 |
| 滚动组件 `settings.mode` 新增 | v2 | 缺失=`continuous`;非法回落 `continuous`;`pause` 复用 `settings.interval` |
| `residentEnabled` 转纯兼容 | v2 | 读出后与 `layout==='custom'` 对齐;不再作为独立开关 |
| `family` 作用域 | v2 | **只影响 fixed**;custom 始终 ClassIsland 规格、不受 family 影响 |
| 旧 `style` 字段 | 保留镜像 | 本轮不删除;与 family/winislandStyle 独立;建议 v3 再评估废弃 |
| 迁移标记文件 `config.island-migration.json` | 2 | 仅诊断用途;不是版本字段、不参与运行逻辑 |
| 回滚文件 `config.json.v1.bak` | 2 | 迁移前覆盖写;回滚后仍保留,供二次回滚 |
**兼容矩阵(读写方向必须明确)**:
1. **新读旧**(v2 代码读 v1 文件):允许,走 5.4 迁移;迁移后内存中只存在 v2。
2. **新读新**:允许,走 normalize;`islandLayout.version` 更高时只读。
3. **旧读新**(旧代码读 v2 文件):旧代码会忽略未知字段继续运行(因为 v2 把新字段放在其不认识的键上),但**旧代码保存后会丢新字段**;因此必须在文档与发布说明中写明"不要用旧版应用反复编辑新配置"。
4. **新写旧**:禁止。v2 代码写盘必须带 `islandLayout.version:2`,不得输出 v1 结构,也不得写任何文件级版本号。
5. **迁移与编辑器并发**:编辑器打开时若检测到 `config.json` 被迁移,必须重新加载(比较 `updatedAt`/`mtime` 或读取返回值),不得用旧草稿覆盖迁移结果;保存前再次 `getConfig` 核对 `islandLayout.version`。
**兼容性红线(不得违反)**:
- 不得删除任何仍被旧配置使用的字段名(如 `style`、`residentEnabled`、`spacing`、`expandedRows`、`pointerFadeEnabled`)。
- 不得把对象结构从"方案数组"改成"对象字典"或反之(旧编辑器/旧冒烟会解析失败)。
- 不得改变 `ISLAND_LIST_METRICS` 与 `ISLAND_SIZE_DELTA` 的**语义**(可以新增 classisland 专用 metrics,但旧常量必须继续导出且数值不变),因为大量冒烟断言按旧值计算(`packages/shared/src/constants.ts:344-351,379-402`)。
- 不得让 v2 的 `rows` 与 v1 的 `rows` 在同一键上结构不兼容:v2 行只**增加**字段,不重命名已有字段。
### 5.11 验收标准(第 5 部分)
1. 新装启动后 `config.json` 不含任何 `schemaVersion`;含 `island.family:'classisland'`、`island.colorScheme:'system'`、`island.fontSize:16`、`island.theme:'fluent'`、`island.effectEnabled:true`、`island.effectScale:1`、`island.progressBar:{height:3,show:true}`、`islandLayout.version:2`、`fixed.rows` 默认 1 行(日期+课程表);`layout:'fixed'`,界面呈现 ClassIsland 信息行。
2. 用一份旧 v1 配置(fixed、custom 各一份,含 schemes/nodes/appearance/residentEnabled)启动:迁移成功、无字段丢失、`family:'winisland'`、旧胶囊外观不变、`fontSize` 保留原值、`style`/`winislandStyle` 一致;`config.json.v1.bak` 存在。
3. 迁移后再次启动:不重复迁移、不重复写盘(可用文件 mtime/内容哈希断言幂等)。
4. 迁移失败模拟(把 `config.json` 改坏/只读)时:原文件不被破坏、应用以默认 v2 启动、设置页显示错误与恢复入口。
5. "恢复升级前布局"可把 v1 备份恢复回来并重启生效;恢复前自动再备份当前配置。
6. `normalizeIslandLayoutV2` 对行数>12、节点>200、深度>6、方案>20、非法颜色/数值/kind 全部夹取或保留到 `unrecognizedNodes`;不崩溃、不丢未知组件。
7. 老用户切到 `classisland` 后 fixed 变信息行、切回 `winisland` 恢复胶囊;**family 切换时 `style`/`winislandStyle` 与 `fontSize` 保持用户原值不变**;custom 始终 ClassIsland 规格、切换 family 不改变 custom 渲染。
8. 编辑器中常态行/展开行分开保存;应用后 `islandLayout` 为 v2 且 `island:save-layout` 返回值与本地一致。
9. `IslandState`/`IslandNotification`/`ScheduleDto`/`IslandTimePointDto` 的映射表逐项实现:优先级、上课态、时间点类型、单双周、作业/叫人特殊语义均可按 5.6 追踪。
10. IPC 兼容:旧通道全部可用;payload 可选带 `schemaVersion` 仅作校验(不落盘);缺字段时按 family 降级渲染而不是报错。
11. 编辑器/设置页不把预览、缩放、选中项写入配置;预览消息不写历史、不改已读。
12. 读到 `islandLayout.version>2` 时应用只读并提示,不得覆盖用户文件。
13. 外观新字段:关闭 `effectEnabled` 后强提醒特效不再渲染(行内气泡/普通提醒仍工作);`effectScale` 只接受 1/0.75/0.5 并实际改变特效尺寸;`progressBar.show=false` 时所有行进度条隐藏,行级高度覆盖方案/外观。
14. 运行期状态:`IslandState` 含 `family/pendingStaged/bubblePhase/bubbleDeadline` 且**不写入 config.json**;上课暂存时 `pendingStaged` 与队列数一致;气泡阶段按 `idle→mask-in→content-in→out` 推进。
15. 派生字段:`IslandNotification.level` 按 `URGENT→emergency/HIGH→high/NORMAL→normal/LOW→low` 生成且不落盘;`IslandLessonReminder.progress` 在 0–1、`leftSeconds` 与蓝色圆形徽标一致(剩余 1 秒显示 `-1`)。
16. fixed(classisland)下 `prepare/on-class/break` 以行内容接管呈现、`ending` 只显示秒数徽标;custom 下不接管(课程组件 + 徽标);两种 family 的提醒开关语义一致。
17. fixed 行内气泡同一时刻只显示 1 条 active;多条折叠为「还有 N 条」并可打开应用内通知中心;队列/去重/暂存规则不变。
18. `family='winisland'` 只影响 fixed(保持现有胶囊观感);custom 始终按 ClassIsland 规格渲染、不受 family 影响、不引用 winisland 样式命名空间;`family='classisland'` 的 fixed 使用 classisland 命名空间。
19. 经 `pnpm verify:desktop`(默认无头 quick)验证时,所有既有冒烟注入点仍可用:`setTestInputPassthrough`/`setHitTestCursor`/`getWindowBounds`/`getHitRect`/`getInteractive`/`setBlurCollapseEnabled`/`setTouchMode`/`getTouchMode`/`isRecovering` 等名称与语义不变(改造归属 part 11 测试基线)。
20. `colorScheme` 三种取值均生效:`system` 跟随应用深浅色(`html.dark`)、`light`/`dark` 强制;非法值回落 `system`;切换后岛即时重渲染且不改主窗口主题。
21. `fontSize` 语义按 family:classisland 下改 `fontSize` 会派生 14/18/20 阶梯并整体缩放行文字;winisland 下保持旧"基准字号"行为;迁移用户旧值不被覆盖。
22. 滚动组件 `settings.mode`:`continuous` 持续滚动、`pause` 到边界停顿 `settings.interval` 秒;normalize 后取值合法,旧配置缺失补 `continuous`。