- 重构版主提示词(14 parts,面向 ClassHelper 灵动岛重构) - ClassIsland 全量规格提示词(20 parts,含分卷与逐章版本) - 投喂指南、分卷、逐节文件 - 调研记录、一致性基线、生成与质检脚本
114 KiB
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)包含「背景层 + 内容层(组件集合) + 提醒层(遮罩/正文) + 进度条」。行是提醒的宿主,也是编辑的最小单位。
- 行高固定 40,且同一行内所有组件共享该高度;组件区左右内边距 12、最小内容宽 20(
repos/ClassIsland/ClassIsland/Controls/MainWindowLine.axaml:16;repos/ClassIsland/ClassIsland/XamlThemes/FluentTheme/Styles.axaml:110)。 - 行数无硬上限,但默认只有 1 行;行间距默认 5(
repos/ClassIsland/ClassIsland/Models/Settings.cs:219,范围 0–150、步进 5)。 - 必须存在且只存在一行「主行」(MainLine):删除到只剩 1 行时不得再删,必须提示「至少需要保留 1 个主界面行。」(
repos/ClassIsland/ClassIsland/Views/EditModeView.axaml:428文案,MainWindow.axaml.cs的删除守卫)。 - 行模板三分支(必须按条件选,不得混用,
repos/ClassIsland/ClassIsland/XamlThemes/FluentTheme/Styles.axaml:275-278):- Full:
IsNotificationEnabled = true,含遮罩/正文/进度条; - Min:
IsNotificationEnabled = false,只有背景与内容; - 分体(Separated):
IsIslandSeperated=true或行级IslandSeparationMode=2,整行背景改为每个顶级组件各自胶囊。
- Full:
- 行的可见性(必须逐条实现,
repos/ClassIsland/ClassIsland/Controls/MainWindowLine.axaml:29-49):
visible = ( (!IsAllComponentsHid || IsOverlayOpen) && (HideOnRule ? IsVisible : true) ) || IsEditMode
IsAllComponentsHid:该行所有组件都被隐藏时整行消失,不能留一条空胶囊;IsOverlayOpen:该行正在播提醒时必须可见(即使组件全隐藏);HideOnRule:该行命中隐藏规则集时隐藏;规则集变化要实时重算(订阅规则状态更新);- 编辑模式下永远可见。
- 行级不透明度默认 1.0(范围 0.1–1、步进 0.05),只作用于行内容;背景不透明度是另一个独立设置(默认 0.5),只作用于背景层,文字必须始终 100% 不透明(
repos/ClassIsland/ClassIsland/Models/Settings.cs:64,183-184)。 - 鼠标淡出(必须实现):
IsMouseInFadingEnabled(默认 true)与IsMouseInFadingReversed(默认 false)决定「指针不在行内时行变淡到 0.05」,判定用行的屏幕矩形(含 DPI 与缩放),编辑模式不淡出(repos/ClassIsland/ClassIsland/Controls/MainWindowLine.cs:503-509,619-637)。 - 行背景宽度 = 内容实际宽度,不是窗口宽度;内容宽度变化必须平滑跟随(Fluent 300ms / Classic 550ms),且不得触发窗口尺寸变化(
repos/ClassIsland/ClassIsland/Controls/MainWindowLine.cs:614-617;动画细节见第 10 章)。 - 行的提醒宿主能力:
AcceptsNotificationRequests = IsNotificationEnabled && !IsAllComponentsHid && !_isOverlayOpen && !_isUnloading——只有同时满足这些条件的行才能接收提醒;多行时由提醒分发规则决定落到哪一行(repos/ClassIsland/ClassIsland/Controls/MainWindowLine.cs:814)。 - 行级设置(必须保留在编辑器中可改):不透明度、是否允许显示提醒、隐藏规则、圆角/间距(继承全局或覆盖)、附加设置(见第 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 章) | — |
必须实现的组件通用语义:
- 组件宿主间距:行内相邻组件水平间距 12(每个组件
Margin="6 0");容器组件内部用Margin="-6 0"抵消,避免嵌套累积(repos/ClassIsland/ClassIsland/MainWindow.axaml:396)。 - 组件可见性:组件自身有
IsVisible;行内「全部组件都不可见」→ 整行IsAllComponentsHid(见 3.1)。 - 组件级隐藏规则(
custom模式必须对齐):按in-class(上课中)、no-message(无消息)、no-class(无课/放学后)三个条件隐藏;ClassHelper 现有advanced.hideRules的三值语义与之一一对应,必须保留。 - 组件宽高:
advanced.align/width/maxWidth必须生效;stretch为默认,剩余空间按 flex 分配。 - 组件的附加设置:附加设置可挂在科目/时间点/课表/时间表(ClassIsland 语义,见第 7 章),本重构只保留 ClassHelper 已有的「课程控件附加设置」语义,不引入 ClassIsland 本地档案。
- 编辑态:所有组件必须可被点击选中、可拖动排序、可跨行移动;容器组件展开子项供选择(第 9 章编辑器规范)。
- 空数据兜底:任何组件在无数据时必须渲染占位(不得空白、不得抛错),占位文案与场景见第 7 章。
3.3 编辑模式(行为语义)
必须实现「编辑模式」作为岛的唯一配置入口,行为如下(repos/ClassIsland/ClassIsland/MainWindow.axaml.cs:510-528,1391-1403):
- 进入条件:菜单「编辑主界面…」或 URI
classisland://app/edit;进入前必须通过权限校验(ClassHelper 侧对应「已登录班级设备」判断)。 - 进入步骤(按序):置编辑标志 → 激活窗口 → 挂载编辑视图 → 同步特效窗口的编辑态(编辑期间不播强提醒特效)→ 更新窗口层级 → 编辑模式下窗口必须接收鼠标。
- 编辑画布:画布覆盖整个屏幕;岛内容在画布内按停靠位置排布,并预留底部操作栏空间(操作栏高度 64 的语义见第 7/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 侧只保留独立开关 + 上课状态条件,不引入完整规则集)。
检测数据源与频率:
- 全屏/最大化状态在每个主计时器 tick 检测,基准屏幕必须是「岛所在屏幕」(
repos/ClassIsland/ClassIsland/MainWindow.axaml.cs:451-456)。 - 排除进程名单
ExcludedFullscreenWindow默认["explorer"]:这些进程在前台时不算全屏/最大化(repos/ClassIsland/ClassIsland/Models/Settings.cs:56-59)。 - 强制显示(覆盖隐藏):正在播放提醒或处于编辑模式时,即使命中隐藏条件也必须显示,并临时置顶(见 3.9 的置顶锁语义)。
- 手动隐藏:
IsMainWindowVisible(默认 true)为用户手动开关;手动隐藏时同时不接受新提醒(与 3.1 第 9 条一致)。 - 全部隐藏:所有行都隐藏时,可见内容
opacity:0,窗口包围盒不变、窗口不销毁,保证提醒到来能立刻显示(repos/ClassIsland/ClassIsland/MainWindow.axaml:135-141)。 - ClassHelper 映射要求:
custom模式的「常驻」不能被HideOnClass直接隐藏——上课时只允许按hideRules:in-class隐藏指定组件;fixed+classisland信息行按 ClassIsland 默认(HideOnClass=false)上课也保持可见,只是「普通消息暂存、紧急/叫人例外」;不得回到「上课即整岛隐藏」;fixed+winisland兼容形态保留 ClassHelper 原规则(上课时普通消息暂存并隐藏岛体,紧急与叫人例外,见 3.9 与第 8 章)。
3.5 多显示器、停靠与点击穿透
- 6 个停靠位置:顶/底 × 左/中/右(
0/1/2贴工作区上边、3/4/5贴下边;左/中/右分别为贴左/居中/贴右),窗口始终横跨整屏,岛在窗口内形变(repos/ClassIsland/ClassIsland/Models/Settings.cs:48)。 - 偏移:
WindowDockingOffsetX/Y必须生效;顶部停靠时正值向下偏移不得被屏幕边缘裁掉(偏移吞进安全区)。 - 多显示器:
WindowDockingMonitorIndex选择目标屏;可选「跟随鼠标所在屏」(ClassHelper 现有followCursorDisplay保留)。 - DPI:所有几何与命中必须按「物理像素 × 缩放」换算,4K@150% 与 1080p@100% 下不得出现 1px 白边、黑边、圆角锯齿;圆角为 0 时是直角,为最大值时左右边缘为半圆。
- 点击穿透:非编辑模式下,岛默认鼠标穿透(点击落到桌面应用);只有指针真正在岛体上时才接收鼠标;编辑模式下必须接收(
repos/ClassIsland/ClassIsland/MainWindow.axaml.cs:839;ClassHelperpackages/desktop-client/src/main/island.ts:2000-2080的命中轮询必须保留)。 - 置顶锁:提醒播放期间临时置顶,多提醒时以「锁计数」方式管理——只有最后一个锁释放才还原层级,避免闪烁(
repos/ClassIsland/ClassIsland/MainWindow.axaml.cs:1056-1092)。
3.6 时间状态机与跨天(课程计算的唯一事实源)
- 主计时器周期 50ms,优先级 Render;每 tick 顺序:
PreMainTimerTicked→ 计算 →PostMainTimerTicked(repos/ClassIsland/ClassIsland/Services/LessonsService.cs:39-45,373-388)。ClassHelper 映射为主进程单一 1s/50ms 混合节拍(秒级 UI 用 1s,跨点判定用 50ms【推断:可降到 200ms,见 3.13 性能非功能】)。 - 时间状态枚举:
None / OnClass / PrepareOnClass(预留) / Breaking / AfterSchool(repos/ClassIsland/ClassIsland.Shared/Enums/TimeState.cs:6-27)。 - 每 tick 计算顺序(必须逐条实现):
- 清理过期临时课表/临时层/临时课表群;
- 若「加载课表」关闭 → 当前课表为空;
- 选当天课表(预定 > 临时 > 常规,详见 parts/06 §6.2);
now = 当前本地时间;- 当前时间点 = 有效时间点中第一个
TimeType∈{0,1}且StartTime ≤ now ≤ EndTime; - 上课 →
CurrentState=OnClass并映射科目;课间 →CurrentState=Breaking,CurrentSubject=课间科目(名称=课间名); - 下一节 = 第一个
TimeType=0 && EndTime ≥ now;下一个课间 = 第一个TimeType=1 && EndTime ≥ now; - 剩余时间:上课时算「距下课」,否则算「距上课」;下限钳 0;
- 下一节与下一课间都不存在 →
AfterSchool; - 统一回写(未算出用默认值);状态变化时才发事件,顺序固定「先
CurrentTimeStateChanged,再按新状态发OnClass/OnBreakingTime/OnAfterSchool」。
- 有效时间点的双向裁剪:禁用一节早课 → 其前所有时间点不显示;禁用一节晚课 → 其后所有时间点不显示(
repos/ClassIsland/ClassIsland.Shared/Models/Profile/ClassPlan.cs:54-98)。ClassHelper 侧映射为「ScheduleDto投影 + 当前时间点/下一节,由服务端解析结果决定」,不得在客户端重写轮换规则。 - 跨天:每次 tick 检测日期变化;跨天必须重新取当天课表、清除「今天」的已提醒标记、重置课程提醒状态机(
LessonReminderTracker的dayKey变化),不得沿用昨天的「已提醒」状态。 - 时间突变(手动改系统时间、NTP 校正、睡眠唤醒):下一次 tick 必须按新时间重新判定状态;不得补发错过的提醒,也不得因时间回退而重复发出同一提醒(用「状态变化」而非「时间比较」触发)。
3.7 课程表组件的行为(重点)
课程表组件是「一行多条课程」的核心,ClassHelper 的 schedule 组件必须按下列行为实现(视觉像素见第 7 章):
3.7.1 显示哪些时间点(必须逐条判定,repos/ClassIsland/ClassIsland/Controls/Components/ScheduleComponent.axaml.cs:243-251)
对当天每个时间点:
TimeType ∈ {2 分割线, 3 行动}不作为课程项显示(分割线由专门的竖线控件渲染;行动不显示);HideFinishedClass=true且EndTime < now→ 不显示;IsHideDefault=true且不是当前选中项 → 不显示;TimeType==1(课间)且不是当前选中项 → 不显示;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 的提醒是统一管线,必须实现以下行为:
- 一切提醒都进同一管线:上下课、放学、天气、集控、行动触发的提醒共用队列与消费者;不得各写一套弹窗。
- 两层 UI:遮罩(Mask,短文案,第一阶段)+ 正文(Overlay,详细内容,第二阶段);无正文时只播遮罩(
repos/ClassIsland/ClassIsland.Core/Models/Notification/NotificationRequest.cs:40-63)。 - 队列与优先级:
- 提供方优先级:
NotificationProvidersPriority数组下标越小越先播;设置页可拖动排序。 - 同提供方内先入队先播(FIFO)。
- ClassIsland 源码的
IsPriorityOverride比较器存在取反异常(repos/ClassIsland/ClassIsland.Shared/Models/Notification/NotificationPriority.cs:47-50);本轮目标语义按第 8 章裁决:三级排序键(提供方下标 → 通知等级 Emergency>High>Normal>Low → 入队序),不照抄异常行为。
- 提供方优先级:
- 去重:同一 request 对象只入队一次;出队用「已弹出集合」避免链式头节点重复;天气预警按
Detail会话内去重;ClassHelper 侧另有「同 id 通知/叫人双播合并」规则(见第 8 章),两者不冲突。 - 链式提醒:数组请求串成链,任一子请求取消即取消全部;准备上课 + 上课是典型链(
repos/ClassIsland/ClassIsland/Services/NotificationHostService.cs:205-254)。 - 消费者:按优先级排序,只有
AcceptsNotificationRequests && QueuedNotificationCount<=0才推送;无消费者时退回队列;播放完主动拉取后续(NotificationHostService.cs:337-419)。 - 播放状态机:
None → Queued → Playing →(Paused/Cancelled/Completed);取消/完成分别触发对应令牌回调;被中断但未被取消的提醒必须重新入队(NotificationHostService.cs:488-522)。 - 取消与清理:
CancelAllNotifications清空队列 + 取消所有正在播放的请求;用户入口为主界面右键菜单「清除全部提醒」,仅在「有提醒正在播放」时可见(repos/ClassIsland/ClassIsland/MainWindow.axaml:86-94);不实现独立「清除提醒浮动按钮窗口」(源码#if false遗留)。
3.9 通知两层 UI(气泡 + 强提醒)
3.9.1 气泡(行内)行为
- 遮罩阶段:背景切换为强调色 + 斜切遮罩扫入 + 遮罩文字入场;此阶段不显示详情。
- 遮罩结束:有正文则切正文(背景恢复、正文淡入、底部进度条显示剩余时间);无正文则直接收起。
- 正文阶段:内容 + 底部进度条(
Maximum=1、Value=剩余进度、值过渡 50ms 线性);显示时长由内容Duration或EndTime决定。 - 结束时:正文淡出 → 遮罩退出 → 行恢复原状;队列里还有提醒则继续播下一条。
- 提醒期间行的可见性:强制可见(覆盖隐藏规则),并置顶;提醒结束释放置顶锁。
- 关键动画参数(语义;完整表见第 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 主题是另一套(上滑/下压)语义,必须两套都保留。 - 音效与 TTS:ClassIsland 支持提醒音效与语音(默认关闭语音、音效音量 1.0);本轮重构不实现音效/TTS(ClassHelper 无音频通道,见 3.13),只保留视觉提醒。
3.9.2 强提醒(Topmost 全屏特效)行为
- 触发:提醒开始且生效设置开启「强调特效」、主界面可见、行未全部隐藏、本次遮罩尚未播过声音。
- 表现:从行中心向全屏扩散的涟漪(默认强调色),扩散 600ms + 淡出 600ms,750ms 后移除元素;特效窗口为独立透明置顶窗口,逐屏覆盖主界面所在屏幕,按
NotificationEffectRenderingScale(默认 1.0)缩放。 - 生命周期:特效元素集合为空即隐藏窗口,不销毁;非
OSShutdown/ApplicationShutdown时禁止关闭窗口;编辑模式下丢弃新特效。 - 必须支持多屏:每个显示器一个特效窗口,屏幕/DPI 变化时重算位置与尺寸。
- ClassHelper 映射:
custom/fixed共用同一套特效窗口;动画速度倍率缩放 600/750ms;animations=false时不播特效只播气泡。
3.10 天气展示(行为)
- 数据频率:天气服务每 5 分钟刷新一次,启动立即刷新一次;刷新成功才置「已刷新」标志,所有依赖天气的提醒/规则先判该标志(
repos/ClassIsland/ClassIsland/Services/WeatherService.cs:41-44,69-71,386-389)。 - 缓存:最近一次天气持久化在设置中,重启先展示旧数据并标注更新时间;刷新失败保留旧数据,不清空。
- 组件展示:天气组件有多种主信息模式(图标 + 温度 + 文本;默认展示当前天气+温度);无数据时显示占位。
- 提醒:气象预警与天气预报的提醒在「上课 / 课间」事件时触发;预警按
Detail会话内去重、显示时长按文本长度钳制 10–90 秒;预报正文 15 秒;预警要求数据已刷新。 - 本轮范围: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 动画语义总则(实现时必须遵守)
- 进场慢、退场快:进场 200–300ms,退场 100–150ms;提醒遮罩文字淡入 250ms、淡出 200ms,属于「进场慢退场快」的允许例外(遮罩退场本身另有 100–200ms 的层退出)。
- 延迟表达因果:先扫入(0–360ms)、再文字(+260ms)、再投影(+600ms);不得所有元素同时启动。
- 错峰封顶:列表入场每项 25–35ms,超过 10 项必须封顶;不得线性累加到 1 秒以上。
- 可打断:进/出必须成对、可反向,反向时新动画直接覆盖旧动画(不排队);不得用 JS 计时器模拟状态机。
- 动画不改业务:动画只表达状态,不得在动画结束回调里发请求或用动画时长当业务计时。
- 最终几何一致:动画结束后元素几何必须与「无动画直跳终态」逐像素一致。
- 动画级别:
AnimationLevel0/1/2(默认 1);级别 0 禁用动画但主界面提醒动画不在此列(设置项描述明确);同时响应系统「减少动态效果」(视为级别 0 + 时长 1ms,加载指示器放慢不停)。 - 性能禁忌:不对包含文字的容器做
width/margin过渡(用绝对定位背景层或clip-path/scaleX);卡片投影用box-shadow不用filter: drop-shadow;透明窗口禁用setBackgroundMaterial()。
3.13 本轮不实现的 ClassIsland 能力(必须写进实现说明,不得遗漏标注)
以下能力在 ClassIsland 基准里存在,但本轮重构明确不实现,实现者不得自行扩展、也不得因缺失判为缺陷:
- 自动化系统(触发器/行动/规则集/时间点行动):不实现;岛内不出现自动化入口。
- 插件系统与插件市场:不实现;不引入
.cipx、不加载第三方 DLL/模块。 - 主题市场/主题包/壁纸取色:不实现(壁纸取色在 ClassIsland 也已是 P2 停用状态);只做岛内两套风格(
classisland/winisland)。 - 集控/管理端(ManagementServer、策略、审计、授权级别):不实现;ClassHelper 用服务端权限体系替代。
- Excel 导入/导出、档案导入导出、CSES 互转:不实现(属 ClassIsland 本地档案能力);ClassHelper 继续用服务端课表/YAML 导入。
- 自动更新器、启动闪屏、OOBE、欢迎向导、教程中心、崩溃恢复窗口、作者/致谢窗口:不实现(属其它 part/非岛范围)。
- 提醒音效与 TTS(系统 TTS / Edge TTS):不实现;本轮只做视觉提醒。若未来需要,作为独立扩展项,默认关闭。
- ClassIsland 的「即将下课」遮罩:不实现——课程临近下课的提示统一走 ClassHelper 的「课程名右侧蓝色秒数徽标」(见第 8 章裁决)。
- 本地档案/Profile/TimeLayout/CSES 体系:不实现;数据继续用 ClassHelper 服务端解析结果(
ScheduleDto/IslandTimePointDto)。 - 通知音效音量、静音时段、语音源切换:不实现(ClassIsland 有,ClassHelper 无对应通道)。
3.14 验收标准(行为级)
实现者必须逐条自查,全部通过才算完成本节:
- 多行场景:1→5→1 行增删后,行可见性与「主行」约束正确;只剩 1 行时删除被拒绝且提示「至少需要保留 1 个主界面行。」。
- 行可见性表达式三态:组件全隐藏时整行消失;提醒期间强制可见;编辑模式下隐藏行也可见可选。
- 隐藏规则:
hideMode=0三个开关各自生效,explorer前台时不算全屏;提醒/编辑时覆盖隐藏;手动隐藏后不再接收新提醒;全部隐藏时内容透明度为 0 但窗口不销毁。 - 时间状态机:构造「上课→课间→放学」一天,状态与事件顺序正确(状态改变事件先于具体事件);禁用一节早课/晚课后,时间点的双向裁剪与「今天没有课程」占位一致。
- 课程表组件:6 种附加信息、倒计时药丸阈值 60 秒、已结束课程淡化 0.6、明天课表 4 种模式、无课表/全部结束两种占位文案全部正确。
- 提醒:两条提醒先后入队,第一条播完自动播第二条;提醒期间行强制可见并置顶,结束后释放;右键「清除全部提醒」仅在有提醒时出现并立即清空。
- 强提醒:开启特效时从岛中心扩散涟漪,600ms 扩散 + 600ms 淡出、750ms 后消失;编辑模式下不播;多屏时覆盖岛所在屏。
- 主题:Fluent 与 Classic 两套的遮罩入场/退场行为不同(斜切 vs 上下滑),行宽过渡 300ms vs 550ms;切换主题不改变任何业务状态。
- 动画:
AnimationLevel=0时除岛提醒外无动画;系统「减少动态效果」下时长压到 1ms 且加载指示器不停;动画结束后几何与直跳终态一致。 - 非目标确认:界面上不存在自动化、插件、主题市场、集控、Excel 导入、音效/TTS 入口;代码中不存在对应依赖。
- 形态映射:新安装默认
family='classisland'的信息行(日期+课程表、行高 40、底部 3px 进度条),升级用户默认family='winisland'的胶囊形态;custom仍走 schemes/rows/nodes;三种形态切换不丢消息、不重建第二个岛窗口。
4. 目标架构与文件级改造清单
4.0 重构原则(必须逐条遵守)
- 只在现有仓库内重构:不得新建工程、不得换框架、不得引入服务端依赖;沿用 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。
- 渐进式替换,不一次性推倒:每次改动后应用必须仍可启动、旧
config.json仍可加载;用"门面(facade)+ 新模块"的方式把旧调用点隔离,避免一次改src/main/index.ts、tray.ts、smoke.ts的全部 import。 - 单一事实来源:任何"主进程与渲染进程各算一次"的数值(窗口包围盒、列表高度、命中矩形、细缝尺寸、字号推导)必须收口到
packages/shared/src/island/,两端只调用同一函数。现状三处漂移见 02 §2.3。 - 保留现有业务语义:消息去重、上课暂存、紧急展开、已读回执、作业免打扰指纹、课件覆盖优先级、离线补弹,全部保持(02 §2.5/§2.12);ClassIsland 只改变"视觉与布局表现"。
- 两套风格族(family)共存,且 family 只作用于 fixed:
appearance.family取值'classisland' | 'winisland';默认classisland;winisland保留原胶囊+详情卡外观;老用户升级默认winisland(迁移见 05)。custom布局始终按 ClassIsland 规格渲染,不受 family 影响(family 只决定 fixed 用哪套渲染器)。 fixed重新定义:在family='classisland'下,fixed不再是胶囊+详情卡,而是 ClassIsland 风格信息行(默认 1 行 = 日期 + 课程表,行高 40、圆角 8、背景不透明度 0.5、3px 进度条、消息以行内气泡呈现)。原"胶囊+详情卡"只在family='winisland'下保留。这是本轮最重要的语义变化,所有文件改造必须按此分叉。- 一个原生窗口:多种布局/风格共用一个
BrowserWindow;切换 family/layout 不得新开窗口(现状约束,见docs/app/island.md:8-10、src/main/island.ts:468-634)。 - 不打扰纪律:默认验证无头/离屏,不创建可见岛或托盘;只有显式
--visual才做桌面像素验收。依据:docs/dev/testing.md:9,29-32。 - 调色板与风格分离:
style: black|glass|tinted继续保留,写为 winisland 调色板字段(canonicalappearance.winislandStyle,旧style作为兼容镜像,不删除);family 与 style 独立,切换 family 时保留用户 style 原值。 - 外观新增定稿字段:
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,迁移保留原值)。 - 版本只保留一个:落盘版本只有
islandLayout.version=2;schemaVersion仅允许出现在 IPC payload 作运行时校验,不得落盘。 - fixed 消息/提醒口径:行内气泡同一时刻只显示 1 条 active;多条折叠为「还有 N 条」并可打开应用内通知中心;
prepare/on-class/break以行内容接管呈现、ending只用蓝色圆形秒数徽标;队列/去重/暂存规则不变。
4.1 目标模块边界与目录
必须把灵动岛拆成四个边界清晰的部分。目录与职责如下(标注「新增」的目录/文件由本轮创建;其余为移动或改造):
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' |
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' |
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。 - 目标:
- 保留并迁移全部 winisland 行为(胶囊/展开/紧急/叫人/多条列表/课程提醒/命中),视觉与交互不变(老用户回退路径)。
- 把"壳"(squircle 路径、弹簧、
--card-path、命中上报)抽到shell/IslandShell.vue,把消息展示抽到shell/MessagePanel.vue,把课程提醒抽到shell/LessonBar.vue;ClassicIsland.vue只负责"胶囊内容 + 展开内容"的编排。 - 文件顶部保留兼容导出:
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。 - 目标:
- 顶部先选 风格族 family(classisland 默认 / winisland 兼容)——只影响 fixed,说明文案必须写清"自定义布局始终使用 ClassIsland 规格";
fixed的说明按 family 变(classisland=fixed 信息行;winisland=fixed 胶囊+详情卡);- 新增外观控件:
colorScheme(浅色/深色/跟随系统,默认跟随)、theme(fluent/classic,仅 classisland)、effectEnabled(强提醒特效总开关)、effectScale(1/0.75/0.5)、progressBar.show/height(默认 3px)、fontSize(classisland 显示为"正文字号(16 基准)"、winisland 显示为旧"基础字号(13 基准)"); - 调色板
winislandStyle(black/glass/tinted)仅在 winisland 下显示;切换 family 时保留用户原值,不重置; - "测试灵动岛"必须按当前 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(录入前/后登记)。 - 目标(双层去重,职责明确):
- 渲染层(快速路径):保留现有
isLocalHomework()判定,命中则根本不发islandPush;markHomeworkCreatedLocally()调用点保留,不改交互时序。 - 主进程(兜底去重):
island:pushpayload 增加source: 'realtime'|'homework-local'|'replay'|'call'与dedupeKey(作业用同一指纹;普通通知/叫人用source+id);main/island/ipc.ts校验字段、main/island/messages.ts维护 LRU 去重表(key→过期时间,TTL 60s),命中直接丢弃并记 debug 日志。 - IPC 归属:渲染层
bridge.ts计算并传source/dedupeKey→ preloadislandPush原样透传 → 主进程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:island:get-metrics |
新 | — | 返回当前 layout/family 的 metrics(编辑/冒烟/调试用) |
classisland:island:editor-open |
主→应用窗口 | 已有 island:open-editor |
保留旧通道;新通道可带 {rowId,nodeId} 定位 |
island:set-hit-rect/island:set-interactive |
岛→主 | 已有 | 语义不变;fixed 信息行按真实行矩形上报 |
- payload 可携带
schemaVersion:2仅用于运行时校验/调试,不得落盘;落盘版本只有islandLayout.version。接收方遇到未知字段必须忽略而不是拒绝。 - 通道名继续集中在
src/main/channels.ts或shared/island/ipc.ts的单一常量表中;preload 不得直接写字符串字面量。 - 旧通道(
island:get-layout等)不得改名/删除;改名只能通过"新通道 + 旧通道转发"完成,至少保留一个版本周期。 - 所有冒烟注入/读取通道(
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 迁移与门面的落地顺序(必须按序提交)
- M0 建契约层:
shared/island/类型、默认值、normalize、migrate、metrics、geometry、messages、lessons、ipc;只加不删,旧island-layout.ts/constants.ts保持再导出。 - M1 拆主进程:
main/island/*拆分 + 门面;行为与现状 1:1(不改变任何可见行为),用无头verify:desktop回归。 - M2 迁移层接入:
config.ts读时迁移 + 版本号;用旧配置夹具验证(fixed/custom 各存一份)。 - M3 渲染层抽壳:
shell/IslandShell.vue、MessagePanel.vue、LessonBar.vue;winisland 外观不变。 - M4 新增 classisland fixed:
FixedInfoRow.vue+InlineMessageBubble.vue+IslandSettingsViewfamily 选择器;默认新装走 classisland,老配置走 winisland;family 只影响 fixed。 - M5 自定义行改造:
CustomIslandRuntime行级外观/隐藏规则/进度条/滚动 mode/行设置抽屉;custom 始终 ClassIsland 规格(老 custom 用户升级后有视觉变化,需在发布说明提示)。 - M6 编辑器改造:v2 数据、行设置、预览、保存队列。
- 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 部分)
- 仓库中存在
packages/shared/src/island/与packages/desktop-client/src/main/island/两个新目录,且src/main/island.ts只剩门面(≤50 行,内容为 import/export 与类型再导出)。 src/main/index.ts、tray.ts、courseware.ts、smoke.ts对./island.js的 import 与调用不需要改动(门面签名兼容)。LayoutIslandApp.vue分发正确:custom始终CustomIslandRuntime(与 family 无关);fixed在 classisland 下用FixedInfoRow、winisland 下用ClassicIsland;切换不新建第二个窗口、不残留旧命中矩形。fixed在family='classisland'下渲染为信息行(默认 1 行:日期 + 课程表;行高 40、圆角 8、背景不透明度 0.5、progressBar.height默认 3px、消息行内气泡);在family='winisland'下保留原胶囊+详情卡行为与既有冒烟断言。CustomIslandRuntime支持行级外观/隐藏规则/进度条,行设置抽屉可打开并保存;滚动组件支持mode:'continuous'|'pause';rows/expandedRows编辑语义与现状一致。- 外观新字段全部落地:
family(只影响 fixed)、colorScheme、theme、effectEnabled、effectScale、progressBar、winislandStyle;fontSize在 classisland 下为正文基准(新装 16)、winisland 下旧语义(13,迁移保留原值)。 - 编辑器 / 设置页 / 组件设置全部读写 v2 数据;旧 v1 配置在首次启动被迁移且不丢字段(迁移细节按 05 验收)。
- 所有新增/修改的 IPC 通道登记在单一常量表;旧通道全部保留;preload 未暴露
ipcRenderer;island:push带source/dedupeKey且主进程有兜底去重表。 - 默认
pnpm verify:desktop(无头 quick)通过;不得因为重构把任何断言改成"跳过";冒烟注入点名称/语义全部保留。 - 代码中不存在
setBackgroundMaterial;不存在绕过 contextBridge 直接访问 Node 的渲染层代码;custom 不受 family 影响、不引用 winisland 样式命名空间。 - 作业指纹链路双层生效:本机录入后 60 秒内,实时事件/接口响应先后到达都不产生岛消息;跨班/跨天不误伤;去重表有 TTL 上限且可被冒烟清理。
- 文件级改造清单(4.2/4.3)逐项落地;
docs/app/island.md、docs/dev/implementation.md的 family/fixed/custom 描述与实现一致。
5. 数据契约、schema v2 与配置迁移
5.0 总则(必须遵守)
- 本章定义重构后的唯一数据契约。所有读写岛配置的代码(主进程
config.ts、岛渲染进程、编辑器、设置页、冒烟)必须只经过packages/shared/src/island/schema.ts的类型与normalize.ts/migrate.ts的函数,不得各自解析 JSON。 - 旧配置必须无损升级:
config.json现有的islandLayout(fixed/custom、schemes、nodes、expandedRows、residentEnabled、pointerFade、lessonReminders)与island(appearance 全字段)一个都不许丢;升级失败必须可回滚。 - 新增
appearance.family:取值'classisland' | 'winisland'。新装默认classisland;已存在配置的老用户升级默认winisland。fixed的语义随 family 分叉:classisland下 fixed = ClassIsland 信息行(默认 1 行 = 日期 + 课程表,行高 40、圆角 8、不透明度 0.5、3px 进度条、消息行内气泡);winisland下 fixed = 原胶囊 + 详情卡。 islandLayout.version必须写入;当前版本2。版本号是唯一迁移判据,不得用"字段是否存在"猜版本(字段判断只作为迁移内部的兜底)。- 写入必须沿用现有
config.json的"内存缓存 + 120ms 合并写 + 退出前 flush"语义(packages/desktop-client/src/main/config.ts:235-346),不得为迁移新增同步写盘路径;迁移本身例外(必须一次写成功并立即 flush)。 - 客户端不得把 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} |
仅诊断用,不参与版本判断、不是第二套版本字段 |
- 只保留
islandLayout.version一个落盘版本字段:不得新增config.schemaVersion或其它文件级版本号;迁移判定只看islandLayout.version(缺失=v1)。 - 迁移必须在
app.whenReady()后、island.init()之前完成一次;不得由渲染进程触发,也不得在岛窗口创建后再迁移(否则窗口会先按旧数据渲染一帧)。 - 迁移成功后必须立即
flushConfigSync();失败时不得写回半成品:保留原config.json,删除.tmp,应用以内存默认 v2 运行并在设置页显示"迁移失败,已保留原配置"。 - 读到
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 也用停靠与边距 |
- 字段职责必须严格区分:
family只决定 fixed 用哪套渲染器(ClassIsland 信息行 / WinIsland 胶囊);winislandStyle(兼容镜像style)只决定 winisland 的调色板;theme只决定 classisland fixed 的 Fluent/Classic;colorScheme只决定浅色/深色/跟随。四者不得互相覆盖、不得合并。custom不使用family/theme/winislandStyle(始终 ClassIsland 规格),但仍读取colorScheme决定明暗。 family归一化:只接受两个值,非法值回落'classisland'(新装默认);迁移函数负责把老用户写成'winisland'(见 5.4)。colorScheme归一化:只接受light/dark/system,非法回落system;缺失补system。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。fontSizes归一化:每项 8–96;显式值优先于派生;缺失项按第 4 条补齐。theme仅 classisland fixed 生效;非法值回落'fluent';winisland下忽略但不删除该字段。effectEnabled必须为 boolean(缺省 true);effectScale必须夹取到[1, 0.75, 0.5](缺省 1,非法值取 1);progressBar.height夹取 1–8、progressBar.show缺省 true;progressBar缺失时整对象补默认。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' |
settings.schedule.progressStyle |
新增:`'bar' |
settings.schedule.showProgress |
沿用;默认 true |
settings.schedule.showPeriod/count |
沿用 |
settings.mode(仅 kind='rolling') |
新增:`'continuous' |
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 迁移算法(必须逐步实现,可重入)
输入判定:
- 读取
config.json(不存在 → 全新安装:直接构造 v2 默认,family='classisland',不写迁移标记)。 - 文件存在但
islandLayout.version缺失:视为 v1(唯一版本判据;不得再引入schemaVersion作为第二套版本号)。 - 老用户判定:
islandLayout.version缺失 或 文件里存在islandLayout/island任一字段 → 迁移时family='winisland'(保持原外观,不惊吓用户)。 - 全新安装判定:文件不存在、或既无
islandLayout又无island→family='classisland'。
步骤(必须按序、每步失败可中止):
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"
- 可重入/幂等:若
islandLayout.version===2,迁移函数必须直接返回原对象(不写盘、不改字段);重复执行不得产生二次修改。 - 老用户 fixed 不变:迁移后
family='winisland',fixed 仍渲染原胶囊+详情卡(ClassicIsland.vue),因此老用户升级后视觉 0 变化;只有用户主动在设置页切到classisland或新装用户才看到信息行。 - custom 老用户:
custom始终按 ClassIsland 规格渲染、不受 family 影响;迁移只升级数据(补行级默认、滚动 mode 等),不改 schema 结构。老 custom 用户升级后视觉会变为 ClassIsland 行规格(这是定稿行为),发布说明与设置页必须提示;fixed 用户(默认 winisland)不受影响。 - 编辑器打开时必须先读迁移后的 v2;若
islandLayout.version不匹配(例如用户用旧版应用改过文件),必须触发一次"再迁移"而不是把 v1 当 v2 解析。 - 迁移必须记录 warning 列表到内存并在设置页"存储/诊断"可查看(哪几项被夹取、哪几个未知组件被保留)。
5.5 校验与夹取(normalizeIslandLayoutV2 / normalizeAppearanceV2)
必须在 shared 中实现,主进程保存前、编辑器加载后、IPC 存入前三处共用同一个函数。规则:
version:仅接受2;缺失/非法按迁移处理,不得静默改成 2 后继续用旧结构。layout:'fixed'|'custom',非法回落'fixed'。family:'classisland'|'winisland',非法回落'classisland'。- 数量上限:行 ≤12/方案、节点 ≤200/全部方案、嵌套深度 ≤6、方案 ≤20;
main行唯一(多余取消)。 - 数值夹取:
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 说明)。 - 未知 kind:不得丢弃。把原始节点 JSON 放入
scheme.unrecognizedNodes(最多 200 条),渲染时忽略并显示"未知组件(已保留)";这样用旧版应用打开再保存也不会丢数据。 - 颜色:统一
#rrggbb小写;非法回落默认;透明度字段独立,不把#rrggbbaa塞进颜色字段。 - id:空/非字符串/重复 → 重新生成(
node-<n>/row-<n>/scheme-<n>);id 最长 100 字符。 - 文本:
settings.text≤2000 字符、locationName/countdownTitle≤64;超长截断并记 warning。 - 经纬度:
latitude -90..90、longitude -180..180,可空;空表示未配置(组件显示"请设置天气地点")。 - 未知字段:v2 对象里出现但类型未定义的字段必须原样保留在对象上(不删除),以便前向兼容。
lessonReminders:prepareSeconds 5–1800、endingSeconds 5–600(依据现有packages/shared/src/island-layout.ts:390-398)。winislandStyle/style:两者都只接受black/glass/tinted,非法回落black;读入优先winislandStyle、回退style,写出两者同步(兼容镜像);不得因 family 迁移而删除style;family 与调色板相互独立(family 决定 fixed 用哪套渲染,winislandStyle 决定 winisland 的调色板)。theme:只接受fluent/classic,非法回落fluent;具体色板/圆角/阴影 token 由 06 定义,05 只登记字段与默认值。colorScheme:只接受light/dark/system,非法回落system;system时跟随应用html.dark状态决定岛明暗(映射逻辑由渲染层实现,数据层只存枚举)。fontSize:8–96,缺省按 family 取(classisland=16 正文基准、winisland=13 旧基线);fontSizes缺失时按 5.2 第 4 条派生;显式fontSizes优先;迁移不得覆盖旧fontSize。effectEnabled:boolean,缺省true;effectScale:只接受1/0.75/0.5,缺省1,非法取1;progressBar:{height:1–8, show:boolean},缺省{3,true}。- 进度条高度优先级:
行 progress.height>方案 progressBarHeight>appearance.progressBar.height;任一缺失沿上一级回落,最终默认 3。progressBar.show=false时所有行都不显示(行级enabled=false只关该行)。 - 滚动组件:
settings.mode只接受continuous/pause,缺省continuous;pause时停顿秒数复用settings.interval(夹取 1–300);continuous时speed夹取 1–200(沿用现有范围)。 - 渲染/样式命名空间只由 layout(fixed 的 family)决定,不写入数据:
fixed + family='winisland'→ winisland 命名空间;fixed + family='classisland'→ classisland 命名空间;custom始终 classisland 命名空间、不受 family 影响;normalize 不得改写样式类名/选择器相关字段。 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 |
折叠时隐藏、轮到时展开 |
- 上述映射只用于渲染语义对齐,不得把
ScheduleDto落盘成 ClassIsland 档案;数据仍以服务端解析结果为准(02 §2.12)。 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 表示无 |
IslandState的新字段只存在于运行期推送(island:state),不得写进config.json、不得被编辑器保存;旧渲染进程缺字段时必须按默认值渲染(前向兼容)。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 版本与兼容
- IPC payload 可以携带
schemaVersion:2仅用于运行时校验/调试,不得落盘、不得作为配置版本判据;配置版本只认islandLayout.version。island:state/island:appearance/island:get-layout三个高频 payload 建议带上该字段(旧字段保留,新增字段可选)。 - 接收方规则:未知字段忽略、缺字段回落默认;不得因为缺
family就拒绝渲染(老渲染进程遇到新主进程时应按winisland降级,反之按classisland)。 island:save-layout必须返回归一化后的 v2 结果(编辑器以返回值为准,避免"本地看着对、主进程夹取后不一致")。- 主进程
getConfig()返回给渲染进程的islandLayout必须是 v2;island必须含family与新增外观字段(theme/effectEnabled/effectScale/progressBar)。 - 旧通道名全部保留(见 04 §4.4);新增通道必须同时在
shared/island/ipc.ts与 preload 中登记,不得字符串散落。 - 作业指纹相关 payload(见 04 §4.3.13)必须带
source(realtime/homework-local/replay/call)与dedupeKey;主进程按 key 去重,渲染进程保留现有"本机刚录作业免上岛"快速路径。
5.8 编辑器预览与临时状态(不持久化)
- 编辑器的内嵌预览、缩放、选中项、拖拽态属于临时状态,不得写入
config.json;只有"应用/保存"才写 v2。 - "无消息展开内容(expandedRows)"与常态
rows分开编辑、分开保存;activeSchemeId只在切换方案时写。 - 设置页"测试灵动岛"仍走
islandPush+context.preview=true(30 秒预览、失焦不收起,见 02 §2.15),预览消息 id 必须带前缀(如appearance-test-)以便清理;预览不写历史、不改已读。 - 编辑器打开时若检测到迁移 warning,必须在顶部显示"部分旧设置已按新格式调整(可查看详情)",不得静默。
5.9 回滚与降级
- 回滚入口:设置页"灵动岛 → 恢复升级前布局"必须存在;回滚步骤:先备份当前
config.json→ 把config.json.v1.bak覆盖回去(该文件天然没有islandLayout.version,即 v1)→ 提示重启 → 重启后旧版本行为恢复(family=winisland/fixed=胶囊)。 - 迁移失败降级:应用不得崩溃;以内存默认 v2 启动(不写盘),岛显示默认信息行,设置页显示错误与"恢复备份"按钮。
- 前向兼容:读到
islandLayout.version>2时,应用只读该配置(不覆盖写),在设置页提示"配置由更高版本创建";不存在其他版本字段可用于判断。 - 文件损坏:
config.json非法 JSON 时沿用现有.bak回退逻辑(config.ts现状:读取失败回落默认),回滚后仍必须按islandLayout.version缺失执行一次迁移判定;不得用损坏文件覆盖.bak。 - 旧版应用打开新文件:不可避免(旧版不认识 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 | 迁移前覆盖写;回滚后仍保留,供二次回滚 |
兼容矩阵(读写方向必须明确):
- 新读旧(v2 代码读 v1 文件):允许,走 5.4 迁移;迁移后内存中只存在 v2。
- 新读新:允许,走 normalize;
islandLayout.version更高时只读。 - 旧读新(旧代码读 v2 文件):旧代码会忽略未知字段继续运行(因为 v2 把新字段放在其不认识的键上),但旧代码保存后会丢新字段;因此必须在文档与发布说明中写明"不要用旧版应用反复编辑新配置"。
- 新写旧:禁止。v2 代码写盘必须带
islandLayout.version:2,不得输出 v1 结构,也不得写任何文件级版本号。 - 迁移与编辑器并发:编辑器打开时若检测到
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 部分)
- 新装启动后
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 信息行。 - 用一份旧 v1 配置(fixed、custom 各一份,含 schemes/nodes/appearance/residentEnabled)启动:迁移成功、无字段丢失、
family:'winisland'、旧胶囊外观不变、fontSize保留原值、style/winislandStyle一致;config.json.v1.bak存在。 - 迁移后再次启动:不重复迁移、不重复写盘(可用文件 mtime/内容哈希断言幂等)。
- 迁移失败模拟(把
config.json改坏/只读)时:原文件不被破坏、应用以默认 v2 启动、设置页显示错误与恢复入口。 - "恢复升级前布局"可把 v1 备份恢复回来并重启生效;恢复前自动再备份当前配置。
normalizeIslandLayoutV2对行数>12、节点>200、深度>6、方案>20、非法颜色/数值/kind 全部夹取或保留到unrecognizedNodes;不崩溃、不丢未知组件。- 老用户切到
classisland后 fixed 变信息行、切回winisland恢复胶囊;family 切换时style/winislandStyle与fontSize保持用户原值不变;custom 始终 ClassIsland 规格、切换 family 不改变 custom 渲染。 - 编辑器中常态行/展开行分开保存;应用后
islandLayout为 v2 且island:save-layout返回值与本地一致。 IslandState/IslandNotification/ScheduleDto/IslandTimePointDto的映射表逐项实现:优先级、上课态、时间点类型、单双周、作业/叫人特殊语义均可按 5.6 追踪。- IPC 兼容:旧通道全部可用;payload 可选带
schemaVersion仅作校验(不落盘);缺字段时按 family 降级渲染而不是报错。 - 编辑器/设置页不把预览、缩放、选中项写入配置;预览消息不写历史、不改已读。
- 读到
islandLayout.version>2时应用只读并提示,不得覆盖用户文件。 - 外观新字段:关闭
effectEnabled后强提醒特效不再渲染(行内气泡/普通提醒仍工作);effectScale只接受 1/0.75/0.5 并实际改变特效尺寸;progressBar.show=false时所有行进度条隐藏,行级高度覆盖方案/外观。 - 运行期状态:
IslandState含family/pendingStaged/bubblePhase/bubbleDeadline且不写入 config.json;上课暂存时pendingStaged与队列数一致;气泡阶段按idle→mask-in→content-in→out推进。 - 派生字段:
IslandNotification.level按URGENT→emergency/HIGH→high/NORMAL→normal/LOW→low生成且不落盘;IslandLessonReminder.progress在 0–1、leftSeconds与蓝色圆形徽标一致(剩余 1 秒显示-1)。 - fixed(classisland)下
prepare/on-class/break以行内容接管呈现、ending只显示秒数徽标;custom 下不接管(课程组件 + 徽标);两种 family 的提醒开关语义一致。 - fixed 行内气泡同一时刻只显示 1 条 active;多条折叠为「还有 N 条」并可打开应用内通知中心;队列/去重/暂存规则不变。
family='winisland'只影响 fixed(保持现有胶囊观感);custom 始终按 ClassIsland 规格渲染、不受 family 影响、不引用 winisland 样式命名空间;family='classisland'的 fixed 使用 classisland 命名空间。- 经
pnpm verify:desktop(默认无头 quick)验证时,所有既有冒烟注入点仍可用:setTestInputPassthrough/setHitTestCursor/getWindowBounds/getHitRect/getInteractive/setBlurCollapseEnabled/setTouchMode/getTouchMode/isRecovering等名称与语义不变(改造归属 part 11 测试基线)。 colorScheme三种取值均生效:system跟随应用深浅色(html.dark)、light/dark强制;非法值回落system;切换后岛即时重渲染且不改主窗口主题。fontSize语义按 family:classisland 下改fontSize会派生 14/18/20 阶梯并整体缩放行文字;winisland 下保持旧"基准字号"行为;迁移用户旧值不被覆盖。- 滚动组件
settings.mode:continuous持续滚动、pause到边界停顿settings.interval秒;normalize 后取值合法,旧配置缺失补continuous。