## 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 组件规格要为每类组件补字段(字号覆盖、进度条、显示模式),单文件条件链会继续膨胀。 - **风险**:`