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

464 KiB
Raw Blame History

ClassHelper 灵动岛重构:给 ChatGPT 的完整实现提示词

文档性质:这是一份「提示词」而非分析报告。请把本文件(或按章节分卷)投喂给 ChatGPT,要求它在 ClassHelper 现有仓库(LoveChengke/ClassHelper,pnpm monorepo,Electron + Vue 3)中,以 ClassIsland(ClassIsland/ClassIsland,C#/Avalonia)的灵动岛(主界面)为基准,重构 ClassHelper 的灵动岛子系统。不是从零新建项目,不重写服务端与管理端,不更换技术栈。

信息来源:ClassHelper 工作区源码(repos/ClassHelper-master)与 ClassIsland v2.1 源码(repos/ClassIsland)。文中所有确切数值、文件名、行为均尽量标注 packages/...:行号 或 repos/ClassIsland/...:行号;无源码依据处标注【推断】。ClassIsland 的源码只作为行为与视觉基准,不得把其 C#/Avalonia 代码、XAML、本地档案体系引入 ClassHelper。

重要纪律:本重构的一切自动验证(构建、冒烟、截图、动画检查)必须无头/离屏运行,禁止弹出可见窗口、禁止播放声音、禁止干扰用户桌面。ClassHelper 仓库默认的 verify:desktop / verify:packaged 就是后台快速检查,只有显式 --visual 才允许可见验收——重构后必须保持这一约定(见第 11、12 部分)。

使用说明(给人类读者,不投喂给模型)

  1. 投喂方式:全文约 12 万 tokens,建议按 out/投喂指南-重构版.md 的五卷/逐章方式投喂;一次性投喂需 ≥ 128k 上下文模型。投喂前请把 ClassHelper 仓库(或至少 packages/desktop-client、packages/shared、packages/ui、docs/)提供给 ChatGPT(作为代码上下文或附件)。
  2. 推荐开场:「请先阅读第 0–2 部分,复述你对现状、目标与 7 条裁决的理解;然后给出文件级重构计划(含迁移与回滚),等我确认后再按 M0–M7 逐阶段实现。」
  3. 证据约定:packages/...:行号 指 ClassHelper 仓库相对路径;repos/ClassIsland/...:行号 是 ClassIsland 基准来源(相对本工作区),只用于理解行为,不是运行时依赖。
  4. 验收纪律:任何「已完成」的声明都必须附带无头验证输出与第 12 部分的逐项勾选结果;未验证项必须标注。

目录

  • 使用说明(给人类读者,不投喂给模型)
    1. 给 ChatGPT 的任务声明(从本节开始执行)
    1. 重构目标与范围
    • 1.1 为什么重构、以什么为基准
    • 1.2 目标状态(重构完成后用户看到什么)
    • 1.3 范围(In / Out)
    • 1.4 模块级 Keep / Change / Add / Remove
    • 1.5 关键设计裁决(必须逐条执行)
    • 1.6 成功标准(重构完成的定义)
    • 1.7 主要风险与对策
    • 1.8 阅读顺序提示
    1. ClassHelper 灵动岛现状基线(重构的"现状地图")
    • 2.0 阅读与使用方式
    • 2.1 进程、窗口与装配全景
    • 2.2 主进程岛控制器分区地图(src/main/island.ts,共 2445 行)
    • 2.3 窗口几何:固定包围盒 + 岛内形变
    • 2.4 命中、穿透、触摸与"点不动"防护
    • 2.5 消息状态机(fixed 经典模式)
    • 2.6 经典模式渲染(src/island/IslandApp.vue,683 行)
    • 2.7 自定义布局(custom)运行时与数据
    • 2.8 课程提醒与课程组件
    • 2.9 天气现状
    • 2.10 课件传输覆盖
    • 2.11 渲染入口与组件清单(现状文件级地图)
    • 2.12 主窗口 → 主进程桥接(src/renderer/island/bridge.ts)
    • 2.13 配置与数据契约现状
    • 2.14 IPC 现状(重构必须保持的契约)
    • 2.15 编辑器与设置页现状
    • 2.16 既有硬约束(重构不得破坏,逐条对应 AGENTS.md §4)
    • 2.17 现状问题清单(重构动因,逐条对应 04 的改造项)
    • 2.18 验收标准(第 2 部分)
    1. ClassIsland 灵动岛行为基准(重构对照规格)
    • 3.1 主界面行(行为模型)
    • 3.2 组件系统(11 类 + 容器)
    • 3.3 编辑模式(行为语义)
    • 3.4 隐藏与显示规则(完整判定)
    • 3.5 多显示器、停靠与点击穿透
    • 3.6 时间状态机与跨天(课程计算的唯一事实源)
    • 3.7 课程表组件的行为(重点)
    • 3.8 提醒队列与优先级(行为)
    • 3.9 通知两层 UI(气泡 + 强提醒)
    • 3.10 天气展示(行为)
    • 3.11 主题语义(Fluent vs Classic 的差异)
    • 3.12 动画语义总则(实现时必须遵守)
    • 3.13 本轮不实现的 ClassIsland 能力(必须写进实现说明,不得遗漏标注)
    • 3.14 验收标准(行为级)
    1. 目标架构与文件级改造清单
    • 4.0 重构原则(必须逐条遵守)
    • 4.1 目标模块边界与目录
    • 4.2 文件级 Keep / Change / Add / Remove 总表
    • 4.3 关键文件改造详情(逐个文件:现状 → 目标 → 接口 → 风险)
    • 4.4 IPC 契约 v2(增量、不改旧通道语义)
    • 4.5 迁移与门面的落地顺序(必须按序提交)
    • 4.6 风险清单与强制缓解
    • 4.7 验收标准(第 4 部分)
    1. 数据契约、schema v2 与配置迁移
    • 5.0 总则(必须遵守)
    • 5.1 落点与版本治理
    • 5.2 appearance v2(config.json.island)
    • 5.3 islandLayout v2(config.json.islandLayout)
    • 5.4 v1 → v2 迁移算法(必须逐步实现,可重入)
    • 5.5 校验与夹取(normalizeIslandLayoutV2 / normalizeAppearanceV2)
    • 5.6 DTO 映射(ClassHelper 数据 → ClassIsland 语义)
    • 5.7 IPC payload 版本与兼容
    • 5.8 编辑器预览与临时状态(不持久化)
    • 5.9 回滚与降级
    • 5.10 版本变更记录与兼容矩阵
    • 5.11 验收标准(第 5 部分)
    1. 设计 tokens 与视觉基线(双风格:ClassIsland 默认 / winisland 兼容)
    • 6.0 双风格模型(必须先建,再谈 token)
    • 6.1 变量分层与命名规范
    • 6.1.1 --ci-* 变量速查表(必须在 tokens.css 中一次性定义齐)
    • 6.2 颜色 token 表
    • 6.3 字体 token
    • 6.4 圆角 token
    • 6.5 几何与间距 token
    • 6.6 阴影 token
    • 6.7 透明与不透明度 token
    • 6.8 层级(z-index)token
    • 6.9 DPI 与缩放 token
    • 6.10 深浅色 token
    • 6.11 两套风格差异总表(默认选择点)
    • 6.12 配置迁移与兼容策略(token 相关部分)
    • 6.13 文件落点与实施顺序
    • 6.14 常见错误与自查清单
    • 6.15 验收标准
    1. 岛结构与组件规格(ClassHelper 落地版)
    • 7.0 改造口径与文件级改动清单
    • 7.1 岛窗口与几何
    • 7.2 行、背景、进度条、遮罩与淡出(ClassIsland 规格:custom 恒定 / fixed 默认)
    • 7.3 外观族:family='classisland'(默认)与 family='winisland'(兼容,仅 fixed)
    • 7.4 组件逐个规格(11 内置 + 3 消息入口 + 课件覆盖)
    • 7.5 编辑器渲染侧结构(组件库 / 拖拽 / 选中框 / 行设置)
    • 7.6 验收标准
    1. 消息、提醒、课程提醒、强提醒特效、天气与课件覆盖(ClassHelper 落地规格)
    • 8.1 数据契约(复用为主,最小扩展)
    • 8.2 去重、合并与排序(ClassHelper 业务规则,必须保留)
    • 8.3 队列与展示状态机(三形态对照 + 伪代码)
    • 8.4 ClassIsland 风格气泡(fixed 默认形态,两阶段)
    • 8.5 强提醒全屏特效(Topmost)
    • 8.6 课程提醒五类(prepare / on-class / break / ending / progress)
    • 8.7 下课前蓝色秒数徽标(重点规格)
    • 8.8 课程进度条(两处,不得混用)
    • 8.9 天气简报(Open-Meteo,保留 ClassHelper 语义)
    • 8.10 课件传输覆盖(最高优先级内容)
    • 8.11 教师端 / 班级端差异
    • 8.12 冲突裁决表(ClassHelper 业务优先,ClassIsland 视觉优先)
    • 8.13 边界与失败降级
    • 8.14 验收标准(可勾选,全部通过才算完成)
    1. 编辑器与设置页重构(IslandSettingsView / IslandEditorView / IslandComponentSettings)
    • 9.1 口径与术语(与 parts-r/01、05、07 对齐)
    • 9.2 文件级改造清单(只列本 part 负责的文件)
    • 9.3 设置页信息架构(重构后)
    • 9.4 视觉风格(family)与两套参数组
    • 9.5 classisland 族外观参数(逐项)
    • 9.6 winisland 族外观参数(兼容保留,不得删)
    • 9.7 课程提醒设置(并入本页)
    • 9.8 保存、实时预览与失败重试(设置页)
    • 9.9 编辑器总体布局(像素级)
    • 9.10 方案栏(保留并升级)
    • 9.11 行与行设置(ClassIsland 编辑模式对齐)
    • 9.12 组件库(11 类,保留既有 DOM 契约)
    • 9.13 拖拽规格(三类源,必须保留现有实现)
    • 9.14 选中、组件设置抽屉(四页签)
    • 9.15 组件设置项清单(与 parts-r/07 组件契约一一对应)
    • 9.16 预览与「进入编辑模式」
    • 9.17 键盘、触摸与焦点
    • 9.18 数据流、契约与失败处理
    • 9.19 状态、空态、主题与性能约束
    • 9.20 既有实现与测试契约(重构不得破坏)
    • 9.21 验收标准
    1. 动画与动效映射(ClassHelper 现状 → ClassIsland 目标;双风格默认值)
    • 10.0 总则与不可协商的规则
    • 10.1 ClassHelper 现状动画清单(重构前基线,必须保留语义)
    • 10.2 映射总表(目标规格;每条给出双风格默认值)
    • 10.2.1 消息气泡完整时间线(按毫秒对齐,验收时逐点核对)
    • 10.3 冲突与选择点(必须按默认执行,并保留开关)
    • 10.4 性能预算与降级矩阵
    • 10.5 实现方式规范(逐条遵守)
    • 10.6 逐文件改动清单
    • 10.7 验收标准
    1. 兼容性、性能、平台与测试基线
    • 11.1 兼容矩阵(必须逐项通过)
    • 11.2 配置迁移与回滚(硬要求)
    • 11.3 IPC 与契约兼容
    • 11.4 性能预算(超预算必须优化)
    • 11.5 平台差异与窗口护栏
    • 11.6 测试基线(无头优先)
    • 11.7 观测与诊断
    • 11.8 文档与交付说明
    • 11.9 本部分验收标准
    1. 验收清单(逐项勾选,不得跳过)
    • 12.1 工程门禁
    • 12.2 视觉验收(像素级)
    • 12.3 功能验收
    • 12.4 动画验收
    • 12.5 兼容与迁移验收
    • 12.6 无头与不打扰验收
    • 12.7 性能验收
    • 12.8 文档与交付物
    • 12.9 禁止事项(验收时再次确认)
    1. 重构里程碑(M0–M7)
    • M0 基线固化与安全网(0.5–1 天)
    • M1 共享层 v2:schema、常量、迁移、tokens(1–2 天)
    • M2 渲染引擎:行/组件/tokens(2–3 天)
    • M3 消息、课程提醒、强提醒、天气、课件(2–3 天)
    • M4 设置页与编辑器重构(2–3 天)
    • M5 动画、主题、细节打磨(1–2 天)
    • M6 迁移、兼容与回归(1–2 天)
    • M7 全量验收与交付(1–2 天)
    • 里程碑风险提示

0. 给 ChatGPT 的任务声明(从本节开始执行)

0.1 你的任务

你是一名资深 Electron + Vue 3 + TypeScript 桌面工程师,熟悉透明置顶窗口、逐像素自绘 UI、CSS 动画与主进程/渲染进程协作。你的任务是在 ClassHelper 现有 monorepo 中重构灵动岛(island)子系统:

  • 保留 ClassHelper 的工程结构、技术栈、服务端数据来源、既有业务规则与配置体系;
  • 以 ClassIsland 的灵动岛为视觉与行为基准,重构 ClassHelper 的 fixed(经典)与 custom(自定义常驻)两套布局的渲染、交互、动画、设置与编辑器;
  • 新增 winisland 兼容风格开关,使老用户可保留现有观感;默认使用 ClassIsland 风格;
  • 不新建应用、不重写服务端/数据库/Web 管理端、不引入新的 UI 框架。

最终交付是可合并进 ClassHelper 的代码改动 + 数据迁移 + 文档 + 无头测试 + 验收报告,而不是示例代码或原型。

0.2 不可协商的技术与工程约束

  1. 技术栈原样沿用:pnpm@11.8.0 monorepo;Electron ^44.3.0;Vue ^3.5.42;Element Plus ^2.14.5(仅设置/编辑器界面);Pinia ^4.0.3;Vue Router ^5.3.1;Vite ^8.3.0;TypeScript ~5.9.3;主进程/preload 用 esbuild,渲染层用 Vite。
  2. 包边界不变:packages/shared(类型/常量/纯函数/岛 schema)、packages/ui(共享控件/tokens)、packages/desktop-client(Electron 主进程 + preload + 主窗口渲染 + 独立岛渲染 island.html);服务端 packages/server 与 Web 管理端 packages/web-admin 不在本次改动范围。
  3. 岛窗口仍是独立透明 BrowserWindow + 独立 island.html 渲染入口 + 独立 island preload;主窗口设置页只作为编辑器入口,编辑预览不创建额外桌面窗口。
  4. 渲染层零 Node 权限:preload 仅暴露 contextBridge 白名单,不暴露 ipcRenderer;nodeIntegration:false、contextIsolation:true、sandbox:true。
  5. 禁止 setBackgroundMaterial() / vibrancy / backdrop-filter 等会破坏逐像素透明的 API;透明、圆角、阴影全部由 CSS 自绘。
  6. 数据来源不变:课表/时间点/通知/作业/叫人/课件仍来自 ClassHelper 服务端解析结果(ScheduleDto、IslandTimePointDto、IslandState、IslandNotification 等);不得引入 ClassIsland 的本地 Profile/ClassPlan 档案体系。
  7. 配置仍写入 config.json.islandLayout(本机,不同步服务端);新增字段必须可迁移、可回滚,旧 fixed/custom 配置无损升级。
  8. 既有硬约束必须保留:winisland 兼容形态下的连续圆角与投影常驻、触摸输入、失焦宽限、固定窗口几何护栏;所有形态下消息按 id 去重、通知/叫人双播不重复、课件传输优先、默认验证不创建可见岛或托盘、冒烟假执行不真关机。
  9. 所有自动验证无头执行;不得修改验收标准来「通过」;未验证项必须标注。

0.3 交付物

  • 代码:packages/shared、packages/desktop-client(main/preload/island/renderer)的改动;如涉及 packages/ui 的 token 复用也需提交。
  • 数据迁移:config.json.islandLayout 的版本化迁移函数、旧配置升级、失败回滚路径;不得丢失用户的 schemes/rows/nodes/外观设置。
  • 文档:更新 docs/app/island.md 与 docs/dev/implementation.md 的岛章节;新增本次重构的 docs/dev/island-refactor.md(架构、数据契约、动画表、迁移说明)。
  • 测试:无头单元测试(schema 迁移、normalize、消息队列、课程状态机)+ 组件测试(岛渲染快照)+ 既有无头 verify:desktop 的适配;视觉验收仅在 --visual 下执行。
  • 报告:第 12 部分验收清单逐项勾选;与 ClassIsland 的差异说明;未实现项与原因;性能数据(启动、空闲 CPU、内存、动画帧率)。

0.4 工作方式

  1. 先通读本提示词与 ClassHelper 相关源码,输出:①你对现状与目标的理解复述;②文件级重构计划(含每个文件的改动类型、接口变化、迁移与回滚);③你识别出的歧义点(一次性提问);④M0 的无头验证方案。
  2. 严格按第 13 部分的 M0–M7 顺序推进;每个里程碑结束都交付「可无头启动 + 通过该阶段验收 + 不回归既有功能」的状态。
  3. 每个模块完成后写清:改了什么、证据(文件/行号/行为变化)、未完成/推断项、验证方法与结果。
  4. 遇到本提示词未覆盖的细节:ClassHelper 既有行为优先(业务语义与既有交互),ClassIsland 规格优先(视觉、像素、动画);仍不明确时在代码与交付说明中标注【推断】。
  5. 不得顺手重构无关模块;保持 diff 可审阅、可回滚。

0.5 术语表

  • 灵动岛 / 岛(island):ClassHelper 的独立透明窗口与 island.html 渲染的悬浮界面。
  • fixed(经典):ClassHelper 现有「胶囊 + 详情卡」消息型布局。
  • custom(自定义常驻):ClassHelper 现有「多行 + 组件 + 组件方案」布局,常驻显示。
  • winisland:ClassHelper 现有观感(WinIsland 风格:连续圆角、胶囊 268×44、纯黑/毛玻璃/主题色渐变);作为兼容风格保留。
  • ClassIsland 风格:本重构的默认观感,按 ClassIsland 主界面的行/组件/像素/动画规格实现。
  • 行(row / line):承载组件的横向容器(对应 ClassIsland MainWindowLine)。
  • 组件(component / node):IslandComponentNode,最小显示单元(日期/时钟/天气/倒计时/课程表/文本/容器等)。
  • 方案(scheme):IslandLayoutScheme,一套常态 rows 与无消息展开 expandedRows 的组件布局。
  • 消息(message):通知/作业/叫人的统称(IslandNotification)。
  • 强提醒(Topmost effect):ClassIsland 的全屏强调特效窗口;本次以 Electron 独立窗口实现,不抢焦点、不阻塞。
  • 无头验证:不创建可见窗口/托盘/声音的自动化检查(ClassHelper 的 verify:desktop 默认模式)。

0.6 阅读顺序

0 → 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → 9 → 10 → 11 → 12 → 13。第 2 部分先建立现状地图,第 3 部分给出行为基准,第 4/5 部分给改造清单与数据迁移,第 6/7/10 部分给视觉、组件、动画规格,第 8/9 部分给消息与编辑器,第 11/12/13 部分给兼容、验收、里程碑。


1. 重构目标与范围

1.1 为什么重构、以什么为基准

ClassHelper 的灵动岛已经具备完整的工程骨架:独立透明窗口、fixed/custom 两种布局、多方案组件编辑器、消息队列、课程提醒、天气、课件覆盖。但它的视觉语言来自 WinIsland(连续圆角、小胶囊、弹簧形变、纯黑/毛玻璃),与 ClassIsland 的「Fluent 信息行 + 组件化 + 提醒气泡 + 强提醒特效 + 精细过渡」存在系统性差距。本次重构以 ClassIsland v2.1 的灵动岛(MainWindow / MainWindowLine / Components / Notification / TopmostEffect)为行为与视觉基准,把 ClassHelper 的岛升级为同等精细度,同时保留 ClassHelper 的工程结构、数据来源与业务规则。

你必须记住三条基准优先级:

  1. ClassHelper 业务语义优先:消息去重、优先级、上课暂存、紧急展开、叫人胶囊、已读回执、班级设备、课件传输优先等规则不得改变;
  2. ClassIsland 视觉与动画优先:像素、字号、圆角、间距、颜色、动画时长与曲线以本提示词第 6、7、10 部分为准;
  3. ClassHelper 既有工程与配置不得推翻:pnpm monorepo、独立岛渲染进程、config.json.islandLayout、IslandComponentNode/IslandLayoutScheme 数据模型、编辑器入口、无头验证纪律继续有效。

1.2 目标状态(重构完成后用户看到什么)

  1. fixed(经典)默认变成 ClassIsland 风格的课表信息行:屏幕顶部/底部悬浮一条或多条信息行(默认 1 行:日期 + 课程表;行高 40、圆角 8、背景不透明度 0.5、字号 14/16/18/20、底部 3px 进度条),显示当天课程、当前课程、剩余分钟/倒计时;不再是 268×44 的小胶囊。
  2. 消息到来时的表现:普通通知/作业在岛内展开为 ClassIsland 风格的气泡(行内遮罩 + 内容两阶段:遮罩扫入 360ms + 错峰 120ms,文字延迟 260ms 淡入并 1.1→1.0 缩放 750ms),紧急消息立即展开;保留 ClassHelper 的上课暂存、普通叫人可点胶囊、紧急立即展开等规则。
  3. 强提醒:可配置的 ClassIsland 强提醒(Topmost 特效)在岛所在显示器全屏/大区域显示,超时自动关闭,不抢焦点、不阻塞操作;默认仅对"强调提醒"启用。
  4. custom(自定义常驻):保留现有方案/行/组件/编辑器,但渲染与交互升级为 ClassIsland 规格:行高 40、行间距 5、组件外边距 6/6、行设置抽屉、选中框 1.5px 虚线、拖拽把手、隐藏规则、行/组件外观覆盖、进入编辑模式(实际内容预览)、退出保存。
  5. winisland 兼容风格(只作用于 fixed):appearance.family = 'winisland' 时保留现有经典 fixed 观感(连续圆角、胶囊 268×44、纯黑/毛玻璃/主题色渐变、弹簧形变);custom 不随 family 变化,始终使用 ClassIsland 规格(其编辑器/行/组件结构保持不变,只按 07 细化像素)。默认新装 classisland,老用户升级迁移为 winisland。
  6. 设置与编辑器:灵动岛设置页新增风格选择、ClassIsland 外观参数(字号阶梯、行间距、进度条、强提醒开关、主题 Fluent/Classic 选择);自定义方案编辑器增加"编辑模式"与行设置抽屉;所有改动即时保存、失败可重试、退出不丢改动。
  7. 数据与兼容:旧 config.json.islandLayout 无损迁移;用户的 schemes/rows/nodes、外观值、编辑器习惯全部保留;迁移失败可回滚;旧版本客户端读到新配置时按"忽略未知字段 + 默认值"处理(或明确拒绝并提示升级)。

1.3 范围(In / Out)

必须做(In):

  • 岛窗口与渲染层:island.html、LayoutIslandApp.vue、IslandApp.vue、CustomIslandRuntime.vue、ComponentNode.vue、ClassicNotifications.vue、LessonReminderContent.vue、CoursewareTransfer.vue、spring.ts、squircle.ts、各 CSS。
  • 主进程岛控制器:src/main/island.ts(窗口几何、命中、触摸、消息队列、课程提醒、天气、课件、编辑预览、保存配置、预览/应用)。
  • preload 与 bridge:src/preload/island.ts、src/renderer/island/bridge.ts、src/types/desktop.d.ts 的岛契约。
  • 共享层:packages/shared/src/{island-layout,constants,motion}.ts 的 schema、常量、归一化、迁移。
  • 设置/编辑器:IslandSettingsView.vue、IslandEditorView.vue、IslandComponentSettings.vue、IslandEditorNode.vue、island-editor.css,以及 router 中岛编辑路由的入口。
  • 配置:config.json.islandLayout 版本化迁移、原子写、回滚。
  • 文档与无头测试。

不做(Out,最多在交付说明中标注为后续):

  • 不重写服务端/数据库/Web 管理端;不改变服务端接口字段与权限。
  • 不引入 ClassIsland 的 Profile/TimeLayout 本地档案、插件系统、自动化系统、管理端、主题市场、Excel 导入、更新器。
  • 不引入新框架/新 UI 库/新状态库;不更换构建管线。
  • 不把岛改成多窗口;仍只有一个原生岛窗口 + 一个可选的强提醒特效窗口。
  • 不改动主窗口(设置/作业/成绩/通知中心)布局,除编辑器入口与预览联动外。

1.4 模块级 Keep / Change / Add / Remove

模块 处理 说明
岛窗口创建/几何/命中/触摸/失焦宽限 Keep + Change 保留固定包围盒与透明窗口护栏;按 ClassIsland 行高/宽度/停靠语义重算几何;fixed 从胶囊尺寸改为信息行尺寸;custom 保持测量适配但按 ClassIsland 行模型约束
IslandApp.vue(经典) Change(大改) 从"胶囊+详情卡"重构为"ClassIsland 信息行 + 通知气泡 + 进度条";保留消息状态机与紧急/上课规则
CustomIslandRuntime.vue(自定义) Change 保留测量/缩放/行渲染;行高、行距、组件边距、背景、圆角、进度条、选中框、拖拽按 ClassIsland 规格;编辑模式重做
ComponentNode.vue / 组件库 Change + Add 11 类组件的默认样式/尺寸/字号/空数据按 ClassIsland 规格;新增进度条/倒计时徽标/课程时间点展开等子结构
ClassicNotifications.vue Change 气泡两阶段(斜切遮罩/文字/缩放)与 ClassIsland 参数对齐;保留 ClassHelper 消息类型/排序/回执
LessonReminderContent.vue Change 课程提醒条/倒计时徽标按 ClassIsland 布局与字号;下课前蓝色圆形秒数徽标(含 -1)
CoursewareTransfer.vue Keep 覆盖优先逻辑不变,只按新视觉加外壳/动画
spring.ts / squircle.ts Keep(条件使用) ClassIsland 风格默认用 CSS transition + 标准圆角;winisland 风格继续用弹簧/连续圆角;不得把弹簧用于 ClassIsland 规格动画
main/island.ts Change 窗口几何、状态模式、消息队列、课程提醒、天气、编辑预览按新交互调整;新增强提醒窗口控制、编辑模式、迁移调用;保留单实例/恢复/限流
shared/island-layout.ts Change(v2) 新增 version、风格/主题/强提醒/进度条等字段;新增 ClassIsland 组件设置字段;normalize/migrate;旧配置无损升级
设置页/编辑器 Change + Add 新增风格切换、ClassIsland 外观项、编辑模式、行设置抽屉;保留方案/行/组件/导入导出
文档/测试 Change + Add 更新 docs;新增无头测试;--visual 保持可选

1.5 关键设计裁决(必须逐条执行)

  1. 固定形态风格双轨、默认 ClassIsland:appearance.family: 'classisland' | 'winisland' 只决定 fixed 的形态(信息行 vs 旧胶囊);custom 始终 ClassIsland 规格。新装默认 classisland;旧用户升级保持 winisland(避免 fixed 观感突变);设置页在 fixed 下提供切换并即时预览。
  2. fixed 重定义:fixed 不再是"小胶囊消息岛",而是 ClassIsland 风格信息行(默认 1 行,日期+课程表;消息时行内气泡)。原胶囊形态只在 winisland 风格下保留。
  3. custom 保留数据模型:schemes/rows/nodes/expandedRows 不变,只扩展字段与渲染;已有用户方案无需重做即可获得新视觉。
  4. 数据不迁移:继续消费 ClassHelper 的课表/通知/作业/叫人/课件数据;ClassIsland 的 Lesson/Notification 语义只作为行为参考。
  5. 强提醒是新增窗口而非替换岛:ClassIsland 的 TopmostEffect 以独立透明窗口实现(无边框、置顶、不抢焦点、超时关闭);只在消息的"强调提醒"开关开启时出现;编辑/预览模式不创建。
  6. 配置版本化:islandLayout.version(整数,当前 2);normalizeIslandLayouts() 先迁移再归一化;迁移前备份 config.json,失败回滚并提示。
  7. 验证无头:所有自动验证必须后台执行;--visual 才允许可见岛、动画、截图验收;不得为验收创建真实班级/发送测试通知/重置密码。

1.6 成功标准(重构完成的定义)

  • 视觉:第 6/7 部分的像素值在 1920×1080@100%、1080p@125%/150%、深/浅色、ClassIsland Fluent/Classic 两套主题、ClassHelper winisland 风格下逐项对齐(容差:颜色 ±2/255,尺寸/位置 ±1px,透明度 ±1%)。
  • 动画:第 10 部分动画表逐项实现(时长误差 ≤ 16ms、曲线一致);60fps;减少动画/失焦/隐藏页暂停生效。
  • 功能:消息/课程提醒/天气/课件/编辑器/设置/迁移全部可用;无回归(见第 12 部分)。
  • 兼容:旧配置升级成功率 100%(本仓库 fixtures 内);回滚可用;旧字段 residentEnabled 继续兼容。
  • 性能:岛渲染进程启动 ≤ 500ms;空闲 CPU ≤ 1%;主+岛进程内存 ≤ 250MB;消息风暴 20 条/分钟无卡顿/丢失/重复。
  • 纪律:verify:desktop 默认不创建可见岛/托盘;无 GUI 打扰。

1.7 主要风险与对策

风险 对策
透明窗口改尺寸导致闪烁 固定包围盒 + 窗内形变(保留现有策略);setBounds 仅模式/设置/显示器变化且节流 ≥100ms
fixed 重定义导致老用户不习惯 winisland 风格保留 + 升级默认沿用旧风格 + 设置页可切换
消息状态机重构引入竞态 先写状态机单测(去重/优先级/暂存/紧急/回执),再改 UI;所有状态变更走单一入口
编辑模式拖拽与触摸冲突 编辑模式暂停指针避让/消息展开;触摸与鼠标分别走命中/宽限路径
配置迁移破坏用户方案 迁移前备份;schema 校验 + 归一化;失败回滚;fixtures 覆盖 fixed/custom/旧版字段
多显示器/DPI 偏差 全部使用 DIP;屏幕变化事件驱动重算;截图验收用 devicePixelRatio 换算
动画掉帧 只用 transform/opacity;弹簧仅限 winisland;长任务移出主线程;帧率监控

1.8 阅读顺序提示

  • 先读第 2 部分(ClassHelper 现状)与第 3 部分(ClassIsland 行为基准),确认"保留什么、改什么"。
  • 第 4/5 部分是施工图与数据迁移,必须在写代码前读完。
  • 第 6/7/10 部分是像素与动画的硬规格,逐条落实。
  • 第 8/9 部分处理消息、课程、强提醒与编辑器。
  • 第 11/12/13 部分是兼容、验收与里程碑,按顺序执行。

2. ClassHelper 灵动岛现状基线(重构的"现状地图")

2.0 阅读与使用方式

  1. 你在重构前必须先读懂本部分。本部分不是评价现状好坏,而是逐文件、逐行号给出"现在到底是什么样"。重构的每一处改动都必须能在本部分找到对应的现状依据。
  2. 本部分所有行号以 repos/ClassHelper-master/ 为工作区相对根,写作 packages/desktop-client/...:行号;与 ClassIsland 对照时写 repos/ClassIsland/...:行号。行号是行区间起点,读源码时按主题名检索同样有效。
  3. 现状基线里出现的所有数值(尺寸、时长、阈值、限制)都是重构时必须先保持、再按新规格替换的对象;不得因为"要改成 ClassIsland 风格"就整体删除,至少要先在迁移层保留等价语义。

2.1 进程、窗口与装配全景

灵动岛是独立渲染进程 + 独立透明 BrowserWindow,与主窗口并存,不占任务栏:

  • 主进程在 app.whenReady() 里 registerIpcHandlers()、registerIslandIpc(),随后调用 island.init(devUrl ? ${devUrl}/island.html : null);生产环境加载 dist/renderer/island.html。依据:packages/desktop-client/src/main/index.ts:188-221、packages/desktop-client/src/main/island.ts:613-617。
  • 主进程把三件事接给灵动岛:island.setOpenAppHandler(打开应用)、island.setEditLayoutHandler(打开编辑器)、island.setMarkReadHandler/setMarkAllReadHandler(把"标为已读"转给主窗口渲染进程)。依据:src/main/index.ts:221-245。
  • 托盘菜单提供「编辑灵动岛布局」,调用 island.editLayout();退出清理时 island.destroy()。依据:src/main/tray.ts:42、src/main/index.ts:304-307。
  • 灵动岛窗口创建参数(重构后仍必须保留这些窗口级语义):frame:false、transparent:true、resizable:false、movable:false、minimizable:false、maximizable:false、fullscreenable:false、skipTaskbar:true、hasShadow:false、focusable:true、alwaysOnTop:true、type:'toolbar',webPreferences 带独立 preload(../preload/island.js)、contextIsolation:true、nodeIntegration:false、sandbox:true、backgroundThrottling:false。依据:src/main/island.ts:480-512。
  • 创建后设置 setAlwaysOnTop(appearance.alwaysOnTop,'screen-saver')、setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true})、拒绝所有新开窗口与非法导航、默认 setIgnoreMouseEvents(true,{forward:true})。依据:src/main/island.ts:514-527;导航白名单实现见 :32-50。
  • 严禁调用 setBackgroundMaterial()。现状代码在 src/main/island.ts:1726-1748 有整段踩坑说明:一旦调用(包括 'none')窗口就不再逐像素透明,卡片外圈会永久留白底;glass 风格因此改用 CSS 半透明色 rgba(32,32,36,0.804) 降级(src/island/classic-island.css:71-77)。

2.2 主进程岛控制器分区地图(src/main/island.ts,共 2445 行)

这是本轮重构最需要拆分的文件。按现状行区间整理如下(ChatGPT 必须按此表定位旧实现):

行区间 现状职责 关键符号
1-130 导入、开发/生产 URL 白名单、窗口尺寸基线与形态增量、收回动画时长、各状态超时表 prodIslandUrl、isAllowedIslandNavigation、BASE、ISLAND_SIZE_DELTA、CLOSE_ANIMATION_MS=360、TIMEOUTS
134-166 对外载荷/动作类型 IslandPushContext、IslandClassStatePayload、IslandActionName
168-460 IslandController 字段与 getter:课程提醒、计时器、窗口、状态、外观、触摸/常驻/编辑、命中、预览、心跳、恢复 lessonCue、collapseTimer、closeTimer、fadeToken、touchMode、residentShapeTimer、previewId、recoverAt
468-634 init():恢复外观、算尺寸、建窗、导航限制、失焦/无响应/心跳监听、加载页面、首帧同步 init、render-process-gone、unresponsive、console-message
636-680 窗口/课表/时间点读取、状态组装、课件状态、就绪判断 getWindow、getState、setCoursewareTransfer
684-767 消息语义:立即展开判定、可点开判定、双播合并、待处理排序、列表布局 isImmediate、isOpenable、mergeNotification、pendingNotifications、listLayout
770-915 视口高度、收到通知的分流、上课/下课状态迁移、下课补弹 refreshViewport、pushNotification、setClassState、flushQueueAfterClass
929-1077 用户动作状态机:展开/收起/展开更多/知道了/整批/标已读/打开应用 handleAction
1079-1214 隐藏、收回动画、立即隐藏、销毁、移除通知 hide、beginClose、finishClose、hideImmediately、destroy、removeNotification
1216-1322 关闭/激活/收起定时/清理/预览收尾 dismissActive、clearPendingForClose、activate、scheduleCollapse、clearTimers
1324-1410 锚点与目标屏、换屏重排、状态落地与推送 resolveAnchor、targetDisplay、syncAnchorDisplay、setState、emit
1412-1512 可见性/焦点/透明度总协调(含上课隐藏守卫) syncWindow
1514-1648 固定包围盒/触摸贴合/重排/窗口矩形 windowBox、syncTouchLayout、relayout、applyWindowLayout、windowBounds
1650-1724 焦点策略、淡入淡出、取消几何动画 applyFocusable、fadeIn、fadeOut、cancelBoundsAnimation
1749-1783 外观热更新 setAppearance
1785-1910 命中开关、岛体矩形、常驻控件/触摸、鼠标避让淡化、触摸模式 setInteractive、setHitRect、updateResidentPointer、setTouchMode
1912-2047 冒烟注入点、坐标换算、兜底岛体矩形、岛体命中判定 setTestInputPassthrough、setHitTestCursor、cursorLocal、expectedHitRect、isCursorOnIsland、syncHitFromCursor
2049-2157 60ms 命中轮询、渲染心跳、窗口自愈(10 分钟 3 次) startHitPoll、checkRendererAlive、recoverWindow
2159-2184 冒烟读取器(交互/窗口/外观) getInteractive、getWindowBounds、getAppearance、emitAppearance
2186-2264 尺寸换算、细缝尺寸、外观夹紧 recomputeSizes、getSliverSize、normalizeAppearance
2266-2334 留白/失焦宽限/预览时长/恢复限流/接口类型 ISLAND_MARGIN=8、BLUR_GRACE_MS=500、BLUR_GRACE_TOUCH_MS=1200、PREVIEW_DURATION_MS=30000、MIN_CARD_HEIGHT=120、RENDERER_ALIVE_TIMEOUT_MS=20000
2335-2445 单例导出与 IPC 注册 island、registerIslandIpc、ISLAND_SIZES/URGENT/CALL/TIMEOUTS 再导出
  1. 重构时不得在这个文件里继续堆逻辑。你必须按 04 部分的模块边界把它拆成 island/ 目录(controller/window/hit/touch/messages/lessons/weather/courseware/ipc),并保留 src/main/island.ts 作为再导出门面(export { island, registerIslandIpc } from './island/index.js'),以免一次性改动 main/index.ts、tray.ts、courseware.ts、smoke.ts 的调用点。

2.3 窗口几何:固定包围盒 + 岛内形变

现状采用「窗口一次建大、岛在画布内形变」的模型(照搬 WinIsland):

  • 形态基线:胶囊 216×34,展开 = 胶囊 + ISLAND_SIZE_DELTA.expanded,紧急/叫人各再加大。依据:src/main/island.ts:72-96、packages/shared/src/constants.ts:344-351。
  • recomputeSizes(appearance) 按用户可调宽高联动所有形态,并计算 BBOX_SIZE = 最大形态 + 2×ISLAND_SHADOW_PAD(留白 10)。依据:src/main/island.ts:2187-2215、packages/shared/src/constants.ts:351。
  • 普通(鼠标)模式窗口尺寸恒为包围盒;触摸模式窗口贴合当前形态;列表形态允许窗口先长高。依据:src/main/island.ts:1521-1553。
  • 窗口位置由锚点推出:居中用 center - round(width/2),左/右贴边留 pad,顶/底同理;自定义模式再夹到工作区内。依据:src/main/island.ts:1615-1648。
  • 渲染进程侧同样维护一套弹簧目标尺寸:胶囊取外观宽高,展开/紧急/叫人取增量,细缝取 6px 宽。依据:src/island/IslandApp.vue:188-204、packages/shared/src/constants.ts:336。
  • 连续圆角:squirclePath(w,h,r) 用超椭圆指数 4.2、每角 14 段;胶囊圆角取 min(r, h/2),展开取 min(48*r/20, w/2, h/2)。依据:src/island/squircle.ts:21-24,44-81、src/island/IslandApp.vue:206-212,343-344。
  • 弹簧:SpringValue 默认 stiffness=0.10、damping=0.68、snapThreshold=0.05px、换向保留 35% 动量、单帧位移 ≤ 剩余距离 20%、MAX_DT=6 帧;渲染进程按 delta * appearance.speed 缩放帧时间。依据:src/island/spring.ts:26-114、src/island/IslandApp.vue:287-332。

2.4 命中、穿透、触摸与"点不动"防护

现状把"命中"当成一级问题处理,重构必须原样保留这些防护:

  1. 窗口默认整块穿透(setIgnoreMouseEvents(true,{forward:true})),由渲染进程 mousemove 命中后上报 island:set-interactive(true);关闭命中一律由主进程按真实光标位置决定(双向下发会让窗口在穿透/接收之间抖动,表现为"点了没反应")。依据:src/main/island.ts:2412-2418、src/preload/island.ts 的 setInteractive、src/island/IslandApp.vue:456-487。
  2. 主进程 60ms 轮询读系统光标,并与渲染进程上报的岛体矩形比对来校正命中(Windows 下 mousemove 转发不可靠)。依据:src/main/island.ts:2049-2078,2026-2047。
  3. 岛体矩形由渲染进程限流上报(最小间隔 50ms,尾随保证最后一次送达)。依据:src/island/IslandApp.vue:412-454。
  4. 触摸屏:渲染进程按 navigator.maxTouchPoints > 0 或首次 touchstart 上报 setTouchMode(true);主进程据此让窗口贴合岛体并始终接收输入,且命中判定的入参被强制为 true。依据:src/island/IslandApp.vue:538-549、src/main/island.ts:245-257,1795-1822,1891-1902。
  5. Windows 专属 setShape 用于触摸模式下把窗口裁成岛体+8px 留白;非 Windows 平台没有该能力,重构必须按 04 的降级方案处理(整窗穿透 + 岛可见区命中),不得依赖 setShape 才能点。依据:src/main/island.ts:278-316。
  6. 失焦收起:展开态获取焦点,点到别处 blur → 回缩胶囊;但必须有两层防护——指针仍在岛上则不收起;刚展开的宽限期内不收起(鼠标 500ms、触摸 1200ms)。依据:src/main/island.ts:557-578,2287-2297,2020-2023。
  7. 窗口「始终可激活」(focusable:true),收起时用 blur() 而不是关掉可激活性——Windows 下 WS_EX_NOACTIVATE 会丢 mousedown,胶囊就点不动。依据:src/main/island.ts:1655-1672。

2.5 消息状态机(fixed 经典模式)

  1. 状态载体 IslandState:mode: 'hidden'|'pill'|'expanded'、active、queued、inClass、currentPeriodEnd、reason、listExpanded、maxCardHeight、lessonReminder、coursewareTransfer、updatedAt。依据:packages/shared/src/types.ts:1198-1230。
  2. 分流规则(必须作为重构的行为基线):紧急(priority==='URGENT')立即展开、无视上课;上课期间普通通知只入队并隐藏;上课期间普通叫人保持胶囊可点、不自动展开;上课期间用户主动点开的通知允许显示(explicitShowId);预览模式(设置页"预览效果")直接展开、失焦不收起、30 秒自动收尾。依据:src/main/island.ts:684-702,794-867,2338-2380。
  3. 双链路合并:同一 id 经 notification:new 与 call:new 双播时必须合并——类型取"更特殊"(call 优先)、优先级取更高,避免叫人被降级成普通通知。依据:src/main/island.ts:704-724。
  4. 排序:按优先级权重(PRIORITY_RANK:LOW 0 / NORMAL 1 / HIGH 2 / URGENT 3)降序,同级按 createdAt 倒序。依据:packages/shared/src/constants.ts:183-209、src/main/island.ts:726-749。
  5. 超时表(毫秒):胶囊 15000、下课后展开 14000、下课后胶囊 25000、展开 20000、紧急 45000、叫人 90000;关闭动画 360;收回时先切胶囊、待动画后再淡出隐藏。依据:src/main/island.ts:121-132,1106-1128。
  6. 收起的判定:只要还有未处理消息(当前或队列)就保持胶囊;队列清空/用户点了知道了/标为已读/进入上课才彻底隐藏。依据:src/main/island.ts:1266-1296。
  7. 多条消息:展开态且待处理 ≥2 时进入列表形态;默认显示 3 条 + 「展开更多(还有 N 条)」;放不下时提示"更多请前往应用内操作";卡片高度由共享 islandListLayout() 计算,主进程与渲染进程必须算同一个数。依据:src/main/island.ts:755-765,770-785、packages/shared/src/constants.ts:379-471、src/island/ClassicNotifications.vue:34-58。
  8. 动作:expand、collapse、expand-list、dismiss、dismiss-all、mark-read、mark-all-read、open-app;其中 mark-read/mark-all-read 通过主进程回调转给主窗口通知中心;作业卡 id 是本地合成 homework-<id>,整批已读时必须排除。依据:src/main/island.ts:150-166,1044-1070、src/renderer/island/bridge.ts:191-211。
  9. 预览/冒烟注入口:preview、setTestInputPassthrough、setHitTestCursor、setBlurCollapseEnabled、setTouchMode、getWindowBounds、getHitRect、getInteractive、isRecovering 等 重构后必须继续存在(大量冒烟断言依赖),不得改名或删除。依据:src/main/island.ts:187-190,416,927-1930,2154-2183。

2.6 经典模式渲染(src/island/IslandApp.vue,683 行)

  1. 卡片结构:.island-root > .island-card > svg.shape + span.content >(tint + pill-layer + ClassicNotifications + LessonReminderContent)。依据:src/island/IslandApp.vue:573-681。
  2. 内容交叉淡入:胶囊层 1 - progress*1.5 退净(进度 2/3 处),展开层 progress² 进入;progress 由高度差推导。依据::304-309,346-351。
  3. 胶囊标题按类型:叫人→"老师叫你"、作业→"新作业"、紧急→"紧急通知"、普通→"新消息";多条时汇总为「新消息:叫人/作业/通知(共 N 条)」。依据::115-146。
  4. 停靠:data-anchor 决定内容贴边(左贴左/右贴右/居中);居中用 left:50% + translateX(-50%),左右/上下停靠留 ISLAND_SHADOW_PAD。依据::353-383、src/island/classic-island.css:207-233。
  5. 形状与投影:路径 fill: var(--wn-bg) + 1px 描边;投影始终声明、只在展开态改参数(避免加/去 filter 造成开合闪一帧)。依据:src/island/classic-island.css:102-153。
  6. 状态色:紧急红 #ff453a、叫人橙 #ff9f0a、作业用 accent;只做内层径向微染 + 描边,不外溢。依据:src/island/IslandApp.vue:151-157、classic-island.css:155-185。
  7. 内容层裁剪:.content{clip-path:var(--card-path)};圆角路径由 SVG path() 注入 CSS 变量 --card-path。依据:classic-island.css:127-133、IslandApp.vue:363-371。
  8. 轮廓保护:island-root.embedded 用于设置页内嵌预览(去掉绝对定位)。依据:classic-island.css:41-47、IslandApp.vue:578-581。

2.7 自定义布局(custom)运行时与数据

  1. 数据根:config.json.islandLayout,默认 layout:'fixed';residentEnabled 仅作兼容字段,custom 即常驻。依据:packages/desktop-client/src/main/config.ts:26-36,198-233、packages/shared/src/island-layout.ts:131-139,384-403。
  2. 组件方案 IslandLayoutSettings:layout、residentEnabled、pointerFadeEnabled/Reversed、lessonReminders、activeSchemeId、schemes[]。依据:packages/shared/src/island-layout.ts:131-150。
  3. 方案 IslandLayoutScheme:id/name/rows/expandedRows/scale/color/foreground/opacity/radius/separated/spacing;rows 为常态行、expandedRows 为"无消息时点击展开"的内容。依据:island-layout.ts:109-130。
  4. 行 IslandLayoutRow:id/name/main/allowNotifications/gap/nodes;main 行唯一(normalize 会强制只留一个)。依据:island-layout.ts:109-116,334-356。
  5. 节点 IslandComponentNode:id/kind/settings/appearance/advanced/children;kind 14 类(date/clock/weather/countdown/schedule/text/separator/slide/rolling/group/stack/notification/homework/call)。依据:island-layout.ts:3-18,77-108。
  6. settings:文本/模板、12 小时制、秒、倒计时目标与目标日、地点/经纬度、显示模式(当前/下一节/今日)、显示数量、显示起止时间、显示进度、排列方向、间距、轮播间隔、滚动速度。依据:island-layout.ts:80-99。
  7. appearance:资源覆盖开关、四档字号、前景/背景/透明度/圆角覆盖、整体透明度;advanced:隐藏规则(in-class/no-message/no-class)、对齐、固定宽/最大宽。依据:island-layout.ts:61-76,100-107。
  8. 归一化边界(重构后仍必须保留的硬边界):最多 12 行、200 节点、6 层、方案 ≤20;颜色必须 #rrggbb;非法 kind 丢弃;重复 id 重命名;main 行唯一。依据:island-layout.ts:256-403(尤其 :269,337,358)。
  9. 自定义运行时:先测量自然内容(compact/details/lesson 三份),再按工作区与方案 scale 适配;两个页面层(常态/详情)交叉淡入,第三层承载课程提醒;铃铛显示未处理数(>99 显示 99+)。依据:src/island/CustomIslandRuntime.vue:58-124,238-323,338-527。
  10. 鼠标避让淡化:常驻模式按指针是否在岛内切换透明度到 CLASSISLAND_ISLAND_METRICS.fadedOpacity=0.05(可反转);触摸/编辑/课程提醒期间暂停。依据:src/main/island.ts:1851-1877、island-layout.ts:35。
  11. 编辑布局时窗口 interview 行为:editingLayout 下不参与命中轮询、不自动收起;island:edit-layout 与托盘入口都走 editLayout()。依据:src/main/island.ts:350-356,1430-1433。
  12. 常驻窗口几何:窗口固定为 min(960, 工作区宽) × min(640, 工作区高 - marginY),内容在画布内弹簧形变;常驻开合期间窗口不做逐帧 setBounds。依据:src/main/island.ts:1522-1528、CustomIslandRuntime.vue:214-216。

2.8 课程提醒与课程组件

  1. LessonReminderTracker 只用服务端解析后的 ScheduleDto 计算五类提醒:准备(默认提前 60s)、上课、课间、下课前倒数(默认 60s)、课中进度;每种提醒的进入/退出时间由 maskUntil/endsAt 表达。依据:src/main/lesson-reminder.ts:14-110、packages/shared/src/island-layout.ts:140-158。
  2. 主进程每秒刷新一次提醒;自定义布局下 in-class 提醒不接管岛体(课程组件持续显示进度),ending 提醒只作为课程名右侧秒数徽标。依据:src/main/island.ts:171-213、docs/app/island.md:65-70。
  3. 提醒条 UI:高 40、圆角 8、底色 #20202080;5 个斜切区域(30° 偏移)覆盖式打开/关闭,区段 360ms、错峰延迟 [0,90,120,90,0](开)/反向(关);开 1-(1-p)^4、关 p²;文字延迟 260ms、scale 1.1→1.0、750ms;进度条 2px、transform 50ms linear。依据:src/island/LessonReminderContent.vue:59-123,133-137,195-317。
  4. 经典模式临近下课用课程名右侧蓝色圆形徽标显示剩余秒数(剩余 1 秒显示 -1),不显示"即将下课"遮罩。依据:docs/app/island.md:68-70、CustomIslandRuntime.vue:30-33、ComponentNode.vue:259-266。
  5. 关闭"即将下课倒数"后不得显示徽标;阈值沿用本机提醒设置。依据:docs/app/island.md:69、lesson-reminder.ts:54-60。

2.9 天气现状

  1. 天气只在主进程抓取(固定服务、固定参数、不接受渲染层传 URL):api.open-meteo.com/v1/forecast,取 temperature_2m/weather_code/wind_speed_10m,8 秒超时、redirect:'error'、响应 ≤64KB;成功缓存 15 分钟,失败保留旧值并标 stale/退避 60 秒,缓存最多 32 个坐标。依据:src/main/island-weather.ts:1-73。
  2. 渲染层 WeatherBrief.vue 每分钟刷新一次;天气码映射文案(晴/少云/多云/阴/雾/毛毛雨/雨/雪/雷雨);断网或陈旧时显示"· 缓存";未配置坐标提示"请设置天气地点"。依据:src/island/WeatherBrief.vue:12-90。
  3. 冒烟模式禁止访问外部天气(ELECTRON_SMOKE_TEST==='1' 且使用真实 fetch 时直接抛错)。依据:island-weather.ts:33-34。

2.10 课件传输覆盖

  1. 主进程持有 coursewareTransfer,coursewareShowing 时优先覆盖岛内容;syncWindow 分支强制显示、取消淡入、不接收鼠标;formSize() 返回课件固定尺寸。依据:src/main/island.ts:661-676,1420-1429,1956、src/main/courseware.ts:36。
  2. 课件卡尺寸 424×132,内边距 17px 24px、圆角 24、底色 rgba(20,20,24,.97)、进度条 5px、字号 13/16/12/11;显示文件名、类型、教师、已收/总 MB、进度%、错误行。依据:packages/shared/src/courseware.ts:4、src/island/CoursewareTransfer.vue:12-132。

2.11 渲染入口与组件清单(现状文件级地图)

文件 行数 现状职责
src/island/main.ts 30 岛渲染入口:引入 tokens.css、applyMotionTokens()、挂载 LayoutIslandApp
src/island/LayoutIslandApp.vue 76 读取 layout/state/appearance/schedules/timePoints,选择经典/自定义/课件三种渲染
src/island/IslandApp.vue 683 经典岛:胶囊+展开卡+形变+命中+课程提醒
src/island/CustomIslandRuntime.vue 528 自定义常驻:测量适配+三页交叉淡入+铃铛+消息面板
src/island/ComponentNode.vue 456 14 类组件的渲染与课程表折叠规则;编辑态拖拽手柄
src/island/ComponentIcon.vue 36 组件图标(编辑库与铃铛)
src/island/ClassicNotifications.vue 234 单条详情卡/多条列表卡/按钮行(打开应用/标为已读/知道了)
src/island/LessonReminderContent.vue 318 课程提醒条 + 5 区斜切遮罩 + 进度条 + 徽标
src/island/CoursewareTransfer.vue 132 课件传输覆盖卡
src/island/WeatherBrief.vue 91 天气简报组件
src/island/spring.ts 115 SpringValue 帧积分弹簧
src/island/squircle.ts 81 连续圆角路径
src/island/classic-island.css 757 经典岛全部样式(胶囊/展开/列表/按钮/状态色)
src/island/custom-island.css 205 自定义行/组件/铃铛基础样式
src/island/custom-runtime.css 255 常驻三页/停靠/分隔/消息宿主样式
src/island/lesson-ending.css 16 课程名右侧秒数徽标
src/renderer/island/bridge.ts 473 主窗口→主进程的岛数据桥:课表/上课状态/通知/作业/叫人/补弹/已读
src/preload/island.ts 123 岛窗口 contextBridge 白名单
src/preload/index.ts 101 主窗口 contextBridge(含岛推送 API)
src/types/desktop.d.ts 294 桥接契约与 config 形状

2.12 主窗口 → 主进程桥接(src/renderer/island/bridge.ts)

  1. 桥接职责:从 IndexedDB 缓存/服务端取当天课表与完整时间点,每秒复算"是否上课",变化时通过 window.desktop.islandSetClassState 下发给主进程;主进程据此决定通知立即弹/暂存/下课补弹。依据:src/renderer/island/bridge.ts:21-56,148-172,416-435。
  2. 课表来源 /schedules/active-week,同时构建 IslandTimePointDto[](含课间/分割线/隐藏标记),课程名优先取日课表条目、无条目再回退时间点默认课程;离线缓存只有条目时退化为"只有上课时间点"。依据:bridge.ts:57-96,107-136。
  3. 通知投递:普通通知、新作业(带"本机录入免打扰"指纹去重)、叫人(带"请尽快前往"副文案)分别打包成 IslandNotification,随 inClass/currentPeriodEnd 上下文推给主进程。依据:bridge.ts:174-289。
  4. 离线补弹:恢复联网时取最近 5 条离线未读补投,推完清离线起点;冒烟模式跳过。依据:bridge.ts:291-364。
  5. 已读回流:主进程收到岛的"标为已读"后转发给主窗口,桥接订阅并调用通知 store 标记已读(离线先本地改)。依据:bridge.ts:366-405、src/main/index.ts:237-245。
  6. 生命周期:登录后启动、退出登录/切班时停止;停止时必须清定时器、解订阅、清本地课表并主动同步 inClass:false。依据:bridge.ts:437-473、src/renderer/App.vue:64-93、src/renderer/layouts/ClientLayout.vue:396-405。

2.13 配置与数据契约现状

  1. config.json 的岛相关字段:islandLayout(完整方案)与 island(外观);两者都有逐字段归一化与默认值。依据:src/main/config.ts:26-84,124-163,198-233,334-346,435-523;配置文件路径 userData/config.json 见 :165-167。
  2. 写入语义:内存缓存 + 120ms 合并写 + 退出前 flushConfigSync;不允许每次读取都同步读盘。依据:config.ts:235-346。
  3. 外观契约 IslandAppearance:height/width/radius/opacity/accent/fontSize/animations/speed/position/alwaysOnTop/style/idleSliver/marginX/marginY/followCursorDisplay;范围 36–72 / 220–420 / 8–32 / 0.4–1 / 11–20 / 0.5–2 / 0–200 / 0–160。依据:packages/shared/src/types.ts:1145-1195、constants.ts:265-302。
  4. 经典外观默认:width 268、height 44、radius 20、fontSize 13、opacity 1、accent #0a84ff、style 'black'、position 'top-center'、marginX/Y 8。依据:constants.ts:265-290。
  5. 语义色与排版系数:ISLAND_TYPE_SCALE(title 1.08/body 0.95/meta 0.78/badge 0.76/pillTitle 0.92/pillSub 0.74)必须与 classic-island.css 顶部注释一致;所有字号由 --island-font 推导。依据:constants.ts:354-364、classic-island.css:1-7。
  6. CLASSISLAND_ISLAND_METRICS(重构后的目标几何基线,已存在于 shared):rowHeight 40、水平内边距 12、圆角 8(最大 20)、背景不透明度 0.5、缩放 1(0.75–2.5)、字号 14/16/18/20、行间距 5(最大 150)、避让淡出 0.05。依据:packages/shared/src/island-layout.ts:19-36。

2.14 IPC 现状(重构必须保持的契约)

  1. 主窗口桥接:getConfig/saveConfig/clearConfig/appInfo/openExternal;岛:islandPush/islandRemoveNotification/islandSetClassState/islandGetState/islandSetAppearance/islandGetAppearance/onIslandMarkRead/onIslandMarkAllRead;另有 islandEditLayout/onIslandEditorOpen。依据:src/preload/index.ts:25-99。
  2. 岛窗口桥接:getLayout/saveLayout/onLayout、getEditing/setEditing/onEditing、getVisible/onVisibility、reportSize/reportResidentControl/noteResidentTouch/onViewport、getSchedules/onSchedules、getTimePoints/onTimePoints、getState/onState/sendAction、getAppearance/onAppearance/setInteractive/setHitRect/setTouchMode/alive。依据:src/preload/island.ts:14-121。
  3. 主进程侧注册:island:get-schedules、island:get-time-points、island:get-layout、island:save-layout、island:edit-layout、island:get-editing、island:get-visible、island:resident-control、island:resident-touch、island:set-editing、island:content-size、island:remove-notification、island:push、island:class-state、island:alive、island:get-state、island:set-appearance、island:get-appearance、island:set-interactive、island:set-hit-rect、island:set-touch-mode、island:action。依据:src/main/island.ts:2338-2438。
  4. 安全规则:所有 invoke 必须校验发送方是岛窗口(event.sender === island.getWindow()?.webContents);渲染进程不能读写配置(get-layout 是唯一读取口,save-layout 由主进程做归一化)。依据:src/main/island.ts:2339-2356,2345-2350。

2.15 编辑器与设置页现状

  1. 设置页「灵动岛」:布局二选一(fixed/custom)、自定义方案入口、鼠标避让两个开关、外观滑块(宽/高/圆角/透明度/字号/速度)、主题色、视觉风格、位置、左右/上下边距、跟随鼠标屏幕、动画开关、始终置顶、空闲细缝、"测试灵动岛"(本地推一条 30 秒预览通知)。依据:src/renderer/views/settings/IslandSettingsView.vue:26-160,162-392。
  2. 设置页改动即生效:外观走 window.desktop.islandSetAppearance 实时下发,保存才落盘;自定义模式下圆角滑块会同步写当前方案圆角。依据:IslandSettingsView.vue:112-133。
  3. 编辑器是主窗口的设置子路由 /settings/island/editor(不创建额外桌面窗口):上方渲染组件标签/预览、右上角方案菜单(新建/复制/删除/重置/JSON 导入导出)、下方组件库(11 类)+ 检查器;分别编辑常态 rows 与无消息展开 expandedRows。依据:src/renderer/views/settings/IslandEditorView.vue:25-70,290-337,541+、docs/app/island.md:36-48、src/renderer/router/index.ts:69-74。
  4. 编辑器保存:深拷贝 + normalizeIslandLayouts + 串行保存队列(saveQueue)+ 修订号(只有最后一次成功才算 saved)+ 路由离开前等待保存;上限:组件总数 200、方案 20。依据:IslandEditorView.vue:81-105,338-347,379-383。
  5. 拖动/放置:指针事件实现拖拽(beginDrag/startDrag/dragTarget/moveDrag/dropPreview),编辑标签节点用 IslandEditorNode.vue,支持 before/after/inside 三种落点。依据:IslandEditorView.vue:443-539、src/renderer/components/IslandEditorNode.vue:1-124。
  6. 组件设置抽屉:内容/外观/高级三页;内容按 kind 提供字段,外观含独立字号/前景/背景/透明度/圆角/整体透明度,高级含对齐/固定宽/最大宽/隐藏条件;底部支持包裹到容器、复制、删除。依据:src/renderer/components/IslandComponentSettings.vue:26-307。
  7. 编辑预览:编辑器内嵌预览区(preview-canvas/island-preview)用岛内组件渲染,缩放滑块控制;不创建第二个桌面窗口,也不自动切到自定义布局(点"应用"才切)。依据:IslandEditorView.vue:290-299、docs/app/island.md:47、src/renderer/styles/island-editor.css:125-249。

2.16 既有硬约束(重构不得破坏,逐条对应 AGENTS.md §4)

  1. 保留经典岛连续圆角、投影常驻、触摸输入、失焦宽限、固定窗口几何护栏。依据:repos/ClassHelper-master/AGENTS.md §4「灵动岛」。
  2. 禁止 setBackgroundMaterial();毛玻璃只能是 CSS 半透明。依据:src/main/island.ts:1726-1748。
  3. 消息按 id 去重;通知/叫人双播不得重复;本机录入作业不上岛(指纹 + id 双判定)。依据:src/main/island.ts:713-724、bridge.ts:191-267。
  4. 编辑预览不创建额外桌面窗口;打开编辑页不自动切布局,应用后才启用自定义方案。依据:docs/app/island.md:47。
  5. 默认验证不创建可见岛/托盘;只有显式 --visual 才做桌面像素/动画验收;冒烟模式跳过外部天气与离线补弹。依据:docs/dev/testing.md:9,29-32、island-weather.ts:33-34、bridge.ts:326-331。
  6. 经典岛与自定义岛共用一个原生窗口,切布局不得新开第二个岛。依据:docs/app/island.md:8-10、src/main/island.ts:468-634(单 this.win)。
  7. 左右边距只在左/右停靠生效,居中锚点永远是屏幕中线;marginX 对居中无效必须在 UI 说明。依据:docs/app/island.md:54、IslandSettingsView.vue:88-94。
  8. 所有验证不得打扰真实桌面(不弹窗、不发系统通知、不挪真实鼠标);冒烟注入点只能改"逻辑光标"。依据:src/main/island.ts:1910-1930。

2.17 现状问题清单(重构动因,逐条对应 04 的改造项)

  1. src/main/island.ts 单文件 2445 行,窗口/命中/消息/课程/课件/外观全部耦合,难以按 ClassIsland 规格替换视觉与交互。
  2. 几何与尺寸常量分散在主进程 BASE/SIZES、shared ISLAND_SIZE_DELTA、渲染进程 targetSize() 三处,主/渲染各算一次,改动时容易漂移。
  3. 经典与自定义是两套独立运行时,但共享消息面板、课程提醒、命中上报;重构需要抽出公共"岛壳 + 消息面板 + 课程条"层,避免两套 UI 各改一遍。
  4. 数据里没有版本号:config.islandLayout 无 version,新增 ClassIsland 行/组件/外观字段时无法做确定性迁移与回滚。
  5. 现状行/组件缺少 ClassIsland 的行级外观(背景色/不透明度/圆角/字号覆盖)、行级隐藏规则、行级进度条等字段;只有节点级覆盖。
  6. 提醒只有"条状提醒 + 徽标",没有 ClassIsland 的两阶段气泡与强提醒 Topmost 特效;需要在视觉层新增但不得改变现有消息队列语义。
  7. 主进程状态机把"提醒优先级、上课守卫、预览、课件"混在一个 syncWindow 里,重构需要拆成"状态投影 + 可见性决策 + 几何/命中"三层。
  8. 编辑器保存/校验逻辑在 Vue 侧,主进程只做 normalizeIslandLayouts;重构应把 schema 校验/迁移收口到 shared,编辑器和主进程共用。

2.18 验收标准(第 2 部分)

  1. 你能在不运行程序的情况下,仅凭本部分定位:岛窗口创建位置、消息分流规则、命中轮询、触摸模式、失焦宽限、收回动画、课程提醒、课件覆盖、设置页与编辑器的入口文件与行号。
  2. 你能列出重构前必须保留的 8 条硬约束与 8 条现状问题,并说明每条对应到 04 的哪个改造项。
  3. 你能复述现状数据流:服务端 ScheduleDto → bridge.ts → islandSetClassState → 主进程状态机 → island:state → 渲染进程;以及通知/作业/叫人三条投递链路的差异。
  4. 你能指出任何一处"主进程与渲染进程各算一次"的几何/布局逻辑(窗口包围盒、列表高度、命中矩形、细缝尺寸、字号推导),并确认 04 已把它们收口到单一事实来源。
  5. 本部分引用到的每个数值都能追到 packages/...:行号 或 packages/shared/...:行号;没有"凭印象"的数值。

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:该行命中隐藏规则集时隐藏;规则集变化要实时重算(订阅规则状态更新);
  • 编辑模式下永远可见。
  1. 行级不透明度默认 1.0(范围 0.1–1、步进 0.05),只作用于行内容;背景不透明度是另一个独立设置(默认 0.5),只作用于背景层,文字必须始终 100% 不透明(repos/ClassIsland/ClassIsland/Models/Settings.cs:64,183-184)。
  2. 鼠标淡出(必须实现):IsMouseInFadingEnabled(默认 true)与 IsMouseInFadingReversed(默认 false)决定「指针不在行内时行变淡到 0.05」,判定用行的屏幕矩形(含 DPI 与缩放),编辑模式不淡出(repos/ClassIsland/ClassIsland/Controls/MainWindowLine.cs:503-509,619-637)。
  3. 行背景宽度 = 内容实际宽度,不是窗口宽度;内容宽度变化必须平滑跟随(Fluent 300ms / Classic 550ms),且不得触发窗口尺寸变化(repos/ClassIsland/ClassIsland/Controls/MainWindowLine.cs:614-617;动画细节见第 10 章)。
  4. 行的提醒宿主能力:AcceptsNotificationRequests = IsNotificationEnabled && !IsAllComponentsHid && !_isOverlayOpen && !_isUnloading——只有同时满足这些条件的行才能接收提醒;多行时由提醒分发规则决定落到哪一行(repos/ClassIsland/ClassIsland/Controls/MainWindowLine.cs:814)。
  5. 行级设置(必须保留在编辑器中可改):不透明度、是否允许显示提醒、隐藏规则、圆角/间距(继承全局或覆盖)、附加设置(见第 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 目标模块边界与目录

必须把灵动岛拆成四个边界清晰的部分。目录与职责如下(标注「新增」的目录/文件由本轮创建;其余为移动或改造):

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。
  • 目标:
    1. 保留并迁移全部 winisland 行为(胶囊/展开/紧急/叫人/多条列表/课程提醒/命中),视觉与交互不变(老用户回退路径)。
    2. 把"壳"(squircle 路径、弹簧、--card-path、命中上报)抽到 shell/IslandShell.vue,把消息展示抽到 shell/MessagePanel.vue,把课程提醒抽到 shell/LessonBar.vue;ClassicIsland.vue 只负责"胶囊内容 + 展开内容"的编排。
    3. 文件顶部保留兼容导出:export { default } from './family/winisland/ClassicIsland.vue',避免旧 import 断裂。
  • 接口:props 必须兼容现有 { embedded?, suspended? };继续调用 window.island.sendAction/setInteractive/setHitRect;继续使用 ISLAND_SIZE_DELTA/ISLAND_LIST_METRICS。
  • 为什么:ClassIsland fixed 取代胶囊后,旧实现仍要作为 winisland 保留;抽壳后两族共用同一套命中/形变,减少重复实现。
  • 风险:抽壳会改变 DOM 层级,现有 CSS 选择器(.island-card .content、.pill-inner)与冒烟断言依赖 DOM;必须保留 .island-card、.island-root、data-anchor、data-style 等关键钩子。

4.3.3 family/classisland/FixedInfoRow.vue(Add,fixed 的新默认)

  • 目标:ClassIsland 信息行的固定布局:默认 1 行,行高 40、行内水平内边距 12、行间距 5、圆角 8、背景不透明度 0.5(背景色取主题 token + appearance.winislandStyle/surface 材质)、1px 描边、字号由 appearance.fontSize(classisland 下=正文基准,默认 16)派生 14/18/20、进度条高度取 appearance.progressBar.height(默认 3)、progressBar.show=false 时不显示;默认行 = 日期组件 + 课程表组件;消息以行内气泡呈现(普通/重要/紧急/叫人/作业),不再弹"详情卡";强提醒特效仅在 effectEnabled=true 时叠加,尺寸按 effectScale。
  • 接口:props { state, appearance, schedules, timePoints, now, editing? };内部复用 components/*Node.vue 与 shell/MessagePanel.vue(行内模式);只读 appearance.colorScheme 决定行底色/文字对比。
  • 为什么:ClassIsland 的主界面就是"一行行信息",fixed 只有变成信息行才符合重构目标。
  • 风险:fixed 原有"点胶囊展开卡片"的点击目标消失,必须保证消息可读(行内气泡可点开详情面板);命中矩形上报必须按真实行矩形,不能用旧胶囊尺寸(窗口包围盒由 geometry.ts 计算)。

4.3.4 family/classisland/InlineMessageBubble.vue(Add,行内气泡)

  • 目标:同一时刻只显示 1 条 active 气泡;点击展开详情/操作;有多条未处理消息时折叠为「还有 N 条」,点击打开应用内通知中心(沿用 ClassHelper 现有通知中心路由),不并列展示多条;两阶段表现为先横向展开成条、再展开详情面板(动画由 08/10 定义);普通/重要/紧急/叫人/作业五类样式;紧急消息仍按现有业务规则"立即展开、无视上课"。
  • 接口:props { active, pendingCount, mode, listExpanded };动作只通过 window.island.sendAction,不得直接改状态;数据来源是 IslandState.active/queued 与 IslandNotification.level。
  • 为什么:ClassIsland 的提醒是"行内/气泡"而非独立卡片;不改变业务队列,只换表现。
  • 风险:行内气泡只改变行内内容测量与截断(正文最多 2 行、超出省略),不改变窗口包围盒(fixed 窗口尺寸固定为 min(工作区宽,960) × (40+2×pad));「还有 N 条」跳转必须带未读上下文,不得丢失已读回执。

4.3.5 custom/CustomIslandRuntime.vue(Change,大改;始终 ClassIsland 规格)

  • 现状:三页(compact/details/lesson)交叉淡入 + 自然内容测量 + 铃铛 + 命中上报;行/组件来自 scheme.rows。依据:src/island/CustomIslandRuntime.vue:52-336,338-527。
  • 目标:保留"测量 + 三页"骨架,统一按 ClassIsland 行规格重写(不区分 family):单行基准高 40、行间距 5、行内水平留白 12、行背景/圆角/不透明度/字号覆盖、行级隐藏规则、行级进度条(3px,含义见 05 §5.3);行设置抽屉入口(字段由 09 定义);rows/expandedRows 的编辑语义不变;滚动组件支持 settings.mode: 'continuous'|'pause'(pause 时到边界停顿 settings.interval)。
  • 接口:props 不变;新增读取 row.appearance/row.hideRules/row.progress;window.island.reportResidentControl 继续上报铃铛矩形;window.island.reportSize 继续上报适配后尺寸;CSS 使用 classisland 命名空间(custom 不受 family 影响)。
  • 为什么:ClassIsland 组件库/行模型与本应用 schemes 高度相似,且 family 最终只作用于 fixed;custom 统一到 ClassIsland 规格可避免"两套自定义外观"。
  • 风险:老 custom 用户升级后视觉会变化(自定义布局统一到 ClassIsland 规格),必须在发布说明与设置页提示;ResizeObserver + requestAnimationFrame 测量链对行数敏感,改写后的几何只由 metrics 计算,不用动画中的壳反推(现状注释见 :73-79)。

4.3.6 src/main/island.ts → main/island/*(Change + Add,大重构)

  • 现状:2445 行单文件(分区见 02 §2.2)。
  • 目标:
    • index.ts:门面导出 island、registerIslandIpc、recomputeSizes、normalizeAppearance、getSliverSize、ISLAND_SIZES 等旧导出名,保证 main/index.ts、tray.ts、courseware.ts、smoke.ts 的 import 不改。
    • controller.ts:状态与编排;持有 IslandState;消息/上课/预览/动作分发。
    • window.ts:建窗、固定包围盒/触摸贴合、锚点、可见性/焦点/透明度、syncWindow 拆分后的"可见性决策"。
    • hit-test.ts:60ms 轮询、setInteractive、setHitRect、expectedHitRect、setShape 降级(非 Windows 见 4.6)。
    • messages.ts:pushNotification 分流、合并、排序、超时计时器、dismissActive、clearPendingForClose、handleAction。
    • lessons.ts:LessonReminderTracker 每秒推进与 lessonShowing 判定。
    • ipc.ts:registerIslandIpc() 全部 handler(含发送方校验与外观合并节流 16ms)。
  • 接口:必须保留全部旧公开方法与冒烟读取器(02 §2.5 第 9 条清单);新增 getMetrics()、getLayoutVersion() 供编辑器/冒烟断言。
  • 为什么:不拆文件无法安全替换 fixed 渲染、无法让 engine 复用 shared 纯函数,也无法让 ChatGPT 在单文件里做可靠改动。
  • 风险:拆分过程中最容易出现"私有字段跨模块互相引用";必须先把 IslandState、WindowBox、HitRect、Metrics 定义到 types,模块间只经接口通信;拆完立即用现有 verify:desktop(无头)回归。

4.3.7 src/island/ComponentNode.vue → components/*Node.vue(Change)

  • 现状:456 行,14 类 kind 的条件渲染 + 课程表折叠算法 + 编辑手柄 + 行内样式。依据:src/island/ComponentNode.vue:29-210,212-293。
  • 目标:按 kind 拆文件,components/index.ts 提供 resolveNodeRenderer(kind);每个组件只负责自己的内容与样式;课程表的"折叠/展开/隐藏/进度/徽标"逻辑放进 ScheduleNode.vue 并复用 02 §2.8/§2.9 规则。
  • 接口:保持 IslandComponentNode 数据结构不变(字段名/默认值见 packages/shared/src/island-layout.ts:77-108);编辑态仍用 node-handle 拖拽句柄。
  • 为什么:ClassIsland 组件规格要为每类组件补字段(字号覆盖、进度条、显示模式),单文件条件链会继续膨胀。
  • 风险:<style scoped> 拆分后类名作用域变化;必须把跨组件公共样式放 styles/components.css,避免样式丢失。

4.3.8 src/island/ClassicNotifications.vue → shell/MessagePanel.vue(Change)

  • 现状:单条详情卡 + 多条列表卡 + 按钮行;只用于展开态。依据:src/island/ClassicNotifications.vue:104-233。
  • 目标:同一组件支持三种形态:detail(winisland 详情卡)、list(winisland 多条列表)、inline(classisland fixed 行内气泡);按钮语义(打开应用/标为已读/知道了/收到)与批量语义不变;canMarkRead 排除作业的规则保留。
  • 接口:props 增加 variant: 'detail'|'list'|'inline';保持 state/appearance/maxHeight。
  • 为什么:fixed 默认不再有详情卡,但消息仍需可操作;共用一套面板避免三处重复实现按钮/已读逻辑。
  • 风险:inline 形态的高度必须计入行高与窗口包围盒(由 shared/island/messages.ts 提供),不得在渲染层单独硬编码。

4.3.9 src/island/LessonReminderContent.vue → shell/LessonBar.vue(Change)

  • 现状:5 区斜切遮罩 + 进度条 + 秒数徽标;遮罩区段 360ms/错峰 90-120ms、开 1-(1-p)^4、关 p²;文字 260ms、scale 1.1→1.0、750ms。依据:src/island/LessonReminderContent.vue:59-123,133-137。
  • 目标:按 ClassIsland 规格校准:遮罩斜切权重 [0,0.1,0.2,0.4,0.2,0.1](与 03/14 部分一致)、区段 360ms、错峰 120ms、开 QuarticEaseOut、关 QuadraticEaseIn、文字 delay 260ms;行高 40、圆角 8。叠加口径(定稿):fixed(classisland)下 prepare/on-class/break 以行内容接管呈现(临时替换课程表内容,动画由 08/10 定义);ending 不接管行内容,只在课程名右侧显示蓝色圆形秒数徽标;custom 不接管(保持课程组件 + 徽标)。
  • 接口:props 保持 { reminder, now, appearance, motion };新增 variant: 'winisland'|'classisland'。
  • 为什么:ClassIsland 的提醒遮罩是"整行被斜切色块覆盖 + 文字弹入",与现有实现同源但参数不同;必须抽成共用组件。
  • 风险:ref 宽度依赖 ResizeObserver,在 fixed 行内宽度变化频繁;必须避免测量死循环(只在行宽变化时重算多边形)。

4.3.10 src/renderer/views/settings/IslandSettingsView.vue(Change)

  • 现状:布局二选一、外观滑块、测试预览。依据:src/renderer/views/settings/IslandSettingsView.vue:26-160,162-392。
  • 目标:
    1. 顶部先选 风格族 family(classisland 默认 / winisland 兼容)——只影响 fixed,说明文案必须写清"自定义布局始终使用 ClassIsland 规格";
    2. fixed 的说明按 family 变(classisland=fixed 信息行;winisland=fixed 胶囊+详情卡);
    3. 新增外观控件:colorScheme(浅色/深色/跟随系统,默认跟随)、theme(fluent/classic,仅 classisland)、effectEnabled(强提醒特效总开关)、effectScale(1/0.75/0.5)、progressBar.show/height(默认 3px)、fontSize(classisland 显示为"正文字号(16 基准)"、winisland 显示为旧"基础字号(13 基准)");
    4. 调色板 winislandStyle(black/glass/tinted)仅在 winisland 下显示;切换 family 时保留用户原值,不重置;
    5. "测试灵动岛"必须按当前 family + layout 推预览。
  • 接口:window.desktop.saveConfig({ island: {...} })、islandSetAppearance 不变;新增字段随 island 下发(纯数据,禁止传 Proxy)。
  • 为什么:family 是重构后最上层的开关,必须最先可见;新增外观字段需要用户可发现、可关闭。
  • 风险:切换 family 会立即改变 fixed 外观;必须先弹确认"切换后仅 fixed 表现改变,自定义方案与 style 不变",且不得自动改 layout。

4.3.11 src/renderer/views/settings/IslandEditorView.vue(Change)

  • 现状:组件标签 + 内嵌预览 + 方案菜单 + 组件库 + 检查器 + 指针拖拽 + 保存队列。依据:IslandEditorView.vue:25-130,130-540,541+。
  • 目标:数据切到 v2;增加"行"选择与行设置抽屉(IslandRowSettingsDrawer.vue,字段清单由 09 定义);预览统一按 ClassIsland 规格渲染(custom 编辑也用 CustomRows);保留上限(200 组件/20 方案)、保存队列、离开前保存、JSON 导入导出。
  • 接口:仍通过 window.desktop.getConfig/saveConfig;normalizeIslandLayouts 改为 normalizeIslandLayoutV2;getIslandSchedules/getIslandTimePoints 不动。
  • 为什么:ClassIsland 编辑器的核心是"行/组件/行设置抽屉/组件库拖拽",现有编辑器已有 80% 骨架。
  • 风险:编辑器是 855 行大文件;拆分节奏按 09 的文件计划执行,不强制本轮先抽 composables;若单文件继续膨胀,再按 4.1 的 renderer/island/editor/ 抽取,抽取时不得改变保存队列/修订号语义。

4.3.12 src/main/config.ts(Change,迁移接入点)

  • 现状:PersistedConfig 含 islandLayout/island,逐字段归一化,120ms 合并写。依据:src/main/config.ts:26-84,198-233,334-346。
  • 目标:不新增文件级版本号;读取 islandLayout 时若 version 缺失 → 调用 migrateIslandLayoutV1ToV2(含 family/外观默认值,见 05),迁移结果写回并备份;saveConfig 收到 islandLayout 时先 normalizeIslandLayoutV2;island 增加 family/colorScheme/theme/effectEnabled/effectScale/progressBar/winislandStyle 归一化。
  • 接口:导出 getConfig/saveConfig/flushConfigSync 签名不变;新增 getIslandLayoutV2() 供主进程使用。
  • 为什么:迁移必须在任何消费者(岛窗口、编辑器、冒烟)之前完成,且只能发生一次;版本字段只保留 islandLayout.version 可避免两套版本号分叉。
  • 风险:迁移写盘与"用户正在编辑"竞争;必须保证迁移只在 app.whenReady() 后、岛窗口创建前执行一次,失败时保留原文件并让应用以默认 v2(只读)启动。

4.3.13 作业指纹链路(本机刚录作业不上岛)

  • 现状:渲染层 src/renderer/island/bridge.ts 用"内容指纹 + id"避免本机录入的作业弹上岛——homeworkFingerprint() 由 classId|assignDate|title|content 拼成,markHomeworkCreatedLocally() 在发请求前登记指纹、回来后补记 id,isLocalHomework() 在 pushHomeworkToIsland() 前判定;指纹 TTL 60 秒。依据:bridge.ts:191-267;调用点在 src/renderer/views/HomeworkView.vue:419-428(录入前/后登记)。
  • 目标(双层去重,职责明确):
    1. 渲染层(快速路径):保留现有 isLocalHomework() 判定,命中则根本不发 islandPush;markHomeworkCreatedLocally() 调用点保留,不改交互时序。
    2. 主进程(兜底去重):island:push payload 增加 source: 'realtime'|'homework-local'|'replay'|'call' 与 dedupeKey(作业用同一指纹;普通通知/叫人用 source+id);main/island/ipc.ts 校验字段、main/island/messages.ts 维护 LRU 去重表(key→过期时间,TTL 60s),命中直接丢弃并记 debug 日志。
    3. IPC 归属:渲染层 bridge.ts 计算并传 source/dedupeKey → preload islandPush 原样透传 → 主进程 main/island/ipc.ts 校验 → messages.ts 去重后入队。不得在 preload 里做业务判断,也不得让主进程反查渲染层 store。
  • 接口:IslandPushPayload(packages/shared/src/island/ipc.ts)新增两个可选字段;旧调用点缺字段时主进程按 id 去重(兼容)。
  • 为什么:现状只有渲染层一层判定,实时事件先于接口响应到达时仍可能漏判;主进程兜底可保证"同一台机器录的作业绝不上岛",且离线补弹/重连重复投递也被覆盖。
  • 风险:指纹跨班/跨天误伤(必须带 classId+assignDate);主进程去重表不能无限增长(LRU+TTL);冒烟需要能注入/清理去重表(提供 testResetHomeworkDedupe() 或在冒烟模式禁用去重,名称归 part 11 测试基线)。

4.4 IPC 契约 v2(增量、不改旧通道语义)

通道 方向 现状 v2 变更
island:get-layout 岛→主 返回 islandLayout 返回 v2(version:2;含 fixed/schemes)
island:save-layout 岛→主 归一化后保存 先 normalizeIslandLayoutV2,返回 v2
island:state 主→岛 IslandState 增加 family/pendingStaged/bubblePhase/bubbleDeadline(运行期,不落盘);旧字段不删
island:appearance 主→岛 IslandAppearance 增加 family/colorScheme/theme/effectEnabled/effectScale/progressBar/winislandStyle;旧字段保留
island:push 应用窗口→主 {notification, context} 增加 source/dedupeKey(作业指纹去重,见 4.3.13);缺省按 id 去重
island:action 岛→主 8 种动作 保留;新增 open-message(classisland fixed 行内气泡打开详情)与 open-notifications(「还有 N 条」打开应用内通知中心)
island:content-size 岛→主 自定义尺寸上报 增加 `mode:'custom'
classisland:island:get-metrics 新 — 返回当前 layout/family 的 metrics(编辑/冒烟/调试用)
classisland:island:editor-open 主→应用窗口 已有 island:open-editor 保留旧通道;新通道可带 {rowId,nodeId} 定位
island:set-hit-rect/island:set-interactive 岛→主 已有 语义不变;fixed 信息行按真实行矩形上报
  1. payload 可携带 schemaVersion:2 仅用于运行时校验/调试,不得落盘;落盘版本只有 islandLayout.version。接收方遇到未知字段必须忽略而不是拒绝。
  2. 通道名继续集中在 src/main/channels.ts 或 shared/island/ipc.ts 的单一常量表中;preload 不得直接写字符串字面量。
  3. 旧通道(island:get-layout 等)不得改名/删除;改名只能通过"新通道 + 旧通道转发"完成,至少保留一个版本周期。
  4. 所有冒烟注入/读取通道(island:set-interactive、island:set-hit-rect、island:set-touch-mode、island:alive 及主进程方法 setTestInputPassthrough/setHitTestCursor/getWindowBounds/getHitRect/getInteractive/setBlurCollapseEnabled/setTouchMode/getTouchMode/isRecovering)名称与语义保持不变,改造归属 part 11 测试基线。

4.5 迁移与门面的落地顺序(必须按序提交)

  1. M0 建契约层:shared/island/ 类型、默认值、normalize、migrate、metrics、geometry、messages、lessons、ipc;只加不删,旧 island-layout.ts/constants.ts 保持再导出。
  2. M1 拆主进程:main/island/* 拆分 + 门面;行为与现状 1:1(不改变任何可见行为),用无头 verify:desktop 回归。
  3. M2 迁移层接入:config.ts 读时迁移 + 版本号;用旧配置夹具验证(fixed/custom 各存一份)。
  4. M3 渲染层抽壳:shell/IslandShell.vue、MessagePanel.vue、LessonBar.vue;winisland 外观不变。
  5. M4 新增 classisland fixed:FixedInfoRow.vue + InlineMessageBubble.vue + IslandSettingsView family 选择器;默认新装走 classisland,老配置走 winisland;family 只影响 fixed。
  6. M5 自定义行改造:CustomIslandRuntime 行级外观/隐藏规则/进度条/滚动 mode/行设置抽屉;custom 始终 ClassIsland 规格(老 custom 用户升级后有视觉变化,需在发布说明提示)。
  7. M6 编辑器改造:v2 数据、行设置、预览、保存队列。
  8. M7 收尾:CSS/文档/冒烟断言/迁移回滚演练。

每一步都必须"可独立回滚":新增文件先经 feature 开关(appearance.family/config.islandLayout.version)控制;不得在某一步同时删除旧文件与旧逻辑。

4.6 风险清单与强制缓解

风险 触发点 缓解(必须实现)
拆分 island.ts 导致隐藏状态丢失 M1 先对齐私有字段到接口;拆分后逐项对比 getState()/getWindowBounds()/getHitRect() 冒烟输出
DOM/CSS 选择器断裂 M3/M4 保留 .island-card/.island-root/[data-anchor]/[data-style] 钩子;视觉回归由 --visual 专项做
两套 family 逻辑分叉 M4 业务状态机只保留一份(controller);family 只选择 fixed 渲染器,custom 不分叉
fixed 语义变化引起老用户困惑 M4 老配置迁移默认 winisland;设置页显著位置显示 family 并解释两种 fixed;custom 升级有视觉变化的提示
custom 用户升级后观感变化 M5 统一 ClassIsland 规格是定稿;发布说明+设置页提示;数据不变、可回滚 config 备份
迁移把用户自定义方案写坏 M2 迁移前备份 config.json;迁移失败保留原文件;提供"恢复旧版布局"
编辑器 855 行继续膨胀 M6 先抽 composables;单文件不超过 500 行;新逻辑进 renderer/island/editor/
非 Windows 触摸命中退化 M1 setShape 仅 win32 分支;其它平台走"整窗穿透 + 可见区命中"(02 §2.4 第 5 条)
性能(行数/组件数增加) M5 metrics 计算缓存;测量只在依赖变化时执行;列表/行使用 will-change 谨慎
无头验证覆盖不到视觉 M7 默认 quick 保持无头;视觉断言只在显式 --visual 下跑,并在文档写明

4.7 验收标准(第 4 部分)

  1. 仓库中存在 packages/shared/src/island/ 与 packages/desktop-client/src/main/island/ 两个新目录,且 src/main/island.ts 只剩门面(≤50 行,内容为 import/export 与类型再导出)。
  2. src/main/index.ts、tray.ts、courseware.ts、smoke.ts 对 ./island.js 的 import 与调用不需要改动(门面签名兼容)。
  3. LayoutIslandApp.vue 分发正确:custom 始终 CustomIslandRuntime(与 family 无关);fixed 在 classisland 下用 FixedInfoRow、winisland 下用 ClassicIsland;切换不新建第二个窗口、不残留旧命中矩形。
  4. fixed 在 family='classisland' 下渲染为信息行(默认 1 行:日期 + 课程表;行高 40、圆角 8、背景不透明度 0.5、progressBar.height 默认 3px、消息行内气泡);在 family='winisland' 下保留原胶囊+详情卡行为与既有冒烟断言。
  5. CustomIslandRuntime 支持行级外观/隐藏规则/进度条,行设置抽屉可打开并保存;滚动组件支持 mode:'continuous'|'pause';rows/expandedRows 编辑语义与现状一致。
  6. 外观新字段全部落地:family(只影响 fixed)、colorScheme、theme、effectEnabled、effectScale、progressBar、winislandStyle;fontSize 在 classisland 下为正文基准(新装 16)、winisland 下旧语义(13,迁移保留原值)。
  7. 编辑器 / 设置页 / 组件设置全部读写 v2 数据;旧 v1 配置在首次启动被迁移且不丢字段(迁移细节按 05 验收)。
  8. 所有新增/修改的 IPC 通道登记在单一常量表;旧通道全部保留;preload 未暴露 ipcRenderer;island:push 带 source/dedupeKey 且主进程有兜底去重表。
  9. 默认 pnpm verify:desktop(无头 quick)通过;不得因为重构把任何断言改成"跳过";冒烟注入点名称/语义全部保留。
  10. 代码中不存在 setBackgroundMaterial;不存在绕过 contextBridge 直接访问 Node 的渲染层代码;custom 不受 family 影响、不引用 winisland 样式命名空间。
  11. 作业指纹链路双层生效:本机录入后 60 秒内,实时事件/接口响应先后到达都不产生岛消息;跨班/跨天不误伤;去重表有 TTL 上限且可被冒烟清理。
  12. 文件级改造清单(4.2/4.3)逐项落地;docs/app/island.md、docs/dev/implementation.md 的 family/fixed/custom 描述与实现一致。

5. 数据契约、schema v2 与配置迁移

5.0 总则(必须遵守)

  1. 本章定义重构后的唯一数据契约。所有读写岛配置的代码(主进程 config.ts、岛渲染进程、编辑器、设置页、冒烟)必须只经过 packages/shared/src/island/schema.ts 的类型与 normalize.ts/migrate.ts 的函数,不得各自解析 JSON。
  2. 旧配置必须无损升级:config.json 现有的 islandLayout(fixed/custom、schemes、nodes、expandedRows、residentEnabled、pointerFade、lessonReminders)与 island(appearance 全字段)一个都不许丢;升级失败必须可回滚。
  3. 新增 appearance.family:取值 'classisland' | 'winisland'。新装默认 classisland;已存在配置的老用户升级默认 winisland。fixed 的语义随 family 分叉:classisland 下 fixed = ClassIsland 信息行(默认 1 行 = 日期 + 课程表,行高 40、圆角 8、不透明度 0.5、3px 进度条、消息行内气泡);winisland 下 fixed = 原胶囊 + 详情卡。
  4. islandLayout.version 必须写入;当前版本 2。版本号是唯一迁移判据,不得用"字段是否存在"猜版本(字段判断只作为迁移内部的兜底)。
  5. 写入必须沿用现有 config.json 的"内存缓存 + 120ms 合并写 + 退出前 flush"语义(packages/desktop-client/src/main/config.ts:235-346),不得为迁移新增同步写盘路径;迁移本身例外(必须一次写成功并立即 flush)。
  6. 客户端不得把 v2 数据写回给旧版本可见的字段后又不写版本号;写 v2 必须同时写 version:2,保证降级/回滚可判断。

5.1 落点与版本治理

数据 位置 版本字段 说明
运行期配置 <userData>/config.json 无独立文件版本 版本只在 islandLayout.version 上表达(唯一版本字段)
岛布局 config.json.islandLayout version(新增,当前 2) 方案/行/节点/外观的结构版本;唯一迁移判据
岛外观 config.json.island 无独立版本 增加 family、theme、effectEnabled、effectScale、progressBar、fontSizes;style 保留为 surface 层
迁移前备份 <userData>/config.json.v1.bak — 迁移前完整拷贝,只保留一份(覆盖式)
迁移标记 <userData>/config.island-migration.json {from,to,at,appVersion} 仅诊断用,不参与版本判断、不是第二套版本字段
  1. 只保留 islandLayout.version 一个落盘版本字段:不得新增 config.schemaVersion 或其它文件级版本号;迁移判定只看 islandLayout.version(缺失=v1)。
  2. 迁移必须在 app.whenReady() 后、island.init() 之前完成一次;不得由渲染进程触发,也不得在岛窗口创建后再迁移(否则窗口会先按旧数据渲染一帧)。
  3. 迁移成功后必须立即 flushConfigSync();失败时不得写回半成品:保留原 config.json,删除 .tmp,应用以内存默认 v2 运行并在设置页显示"迁移失败,已保留原配置"。
  4. 读到 islandLayout.version > 2 时应用必须只读(不写回),并在设置页提示"配置来自更高版本"。

5.2 appearance v2(config.json.island)

在现有 IslandAppearance 基础上只增不改,旧字段全部保留(packages/shared/src/types.ts:1145-1195)。新增/语义变化如下:

字段 类型 默认值(新装) 说明与依据
family 'classisland' | 'winisland' 'classisland'(老用户迁移为 'winisland') 风格族字段的唯一落点;作用域只限 fixed(classisland=信息行;winisland=旧胶囊+详情);custom 始终 ClassIsland 规格、不受影响
colorScheme 'light' | 'dark' | 'system' 'system' 岛配色模式:跟随应用主题(system,默认)/强制浅色/强制深色;只影响岛自身渲染,不改主窗口主题
theme 'fluent' | 'classic' 'fluent' 仅 family='classisland' 的 fixed 使用;对应 ClassIsland Fluent/Classic 两套主题;具体 token 由 06 定义
effectEnabled boolean true 强提醒特效总开关(对应 ClassIsland 的提醒特效 Topmost/Effect 是否启用);关闭后只保留行内气泡/普通提醒
effectScale 1 | 0.75 | 0.5 1 强提醒特效渲染缩放(对应 ClassIsland NotificationEffectRenderingScale,默认 1.0);只允许三档
progressBar {height:number, show:boolean} {height:3, show:true} 岛内进度条统一配置:高度默认 3px,总开关默认显示;语义见 5.3 行级进度条说明
fontSize number classisland=16(正文基准,派生 14/18/20 阶梯);winisland=13(旧语义) family='classisland' 时它是正文基准字号,fontSizes 由它派生;family='winisland' 保持旧语义;迁移时旧配置保留原值,新安装默认 16
fontSizes {secondary:number, body:number, emphasized:number, large:number} 由 fontSize 派生:classisland 14/16/18/20;winisland ≈14/16/18/20(按 fontSize/13 比例) ClassIsland 四档字号;显式配置优先于派生(见 5.4)
height / width number 44 / 268 仅 family='winisland' 的胶囊尺寸;classisland 下不作为信息行尺寸
radius number 20(winisland)/8(classisland 行为默认) classisland 下作为行默认圆角;解析时取 family 对应默认
opacity number 1 岛整体不透明度;classisland 下行背景不透明度另见行设置(默认 0.5)
winislandStyle 'black' | 'glass' | 'tinted' 'black' winisland 调色板字段的 canonical 名:纯黑/半透明/主题色渐变;仅 family='winisland' 的 fixed 使用
style 'black' | 'glass' | 'tinted' 镜像 winislandStyle 兼容镜像字段(不删除):旧配置读入时若没有 winislandStyle 则用它;写出时同时写 winislandStyle 与 style,保持旧版本可读;建议 v3 再评估废弃
position/alwaysOnTop/animations/speed/idleSliver/marginX/marginY/followCursorDisplay/accent 不变 不变 fixed(两种 family)与 custom 共用;classisland fixed 也用停靠与边距
  1. 字段职责必须严格区分:family 只决定 fixed 用哪套渲染器(ClassIsland 信息行 / WinIsland 胶囊);winislandStyle(兼容镜像 style)只决定 winisland 的调色板;theme 只决定 classisland fixed 的 Fluent/Classic;colorScheme 只决定浅色/深色/跟随。四者不得互相覆盖、不得合并。custom 不使用 family/theme/winislandStyle(始终 ClassIsland 规格),但仍读取 colorScheme 决定明暗。
  2. family 归一化:只接受两个值,非法值回落 'classisland'(新装默认);迁移函数负责把老用户写成 'winisland'(见 5.4)。
  3. colorScheme 归一化:只接受 light/dark/system,非法回落 system;缺失补 system。
  4. fontSize 归一化:8–96;classisland 下作为正文基准,fontSizes 缺失时由它派生(body=fontSize,secondary=round(fontSize*14/16),emphasized=round(fontSize*18/16),large=round(fontSize*20/16));winisland 下保持旧语义(fontSizes 缺失时按 fontSize/13 × 14/16/18/20 推导)。迁移时旧 fontSize 原值保留,不强行改成 16。
  5. fontSizes 归一化:每项 8–96;显式值优先于派生;缺失项按第 4 条补齐。
  6. theme 仅 classisland fixed 生效;非法值回落 'fluent';winisland 下忽略但不删除该字段。
  7. effectEnabled 必须为 boolean(缺省 true);effectScale 必须夹取到 [1, 0.75, 0.5](缺省 1,非法值取 1);progressBar.height 夹取 1–8、progressBar.show 缺省 true;progressBar 缺失时整对象补默认。
  8. winislandStyle 归一化:仅 black/glass/tinted 合法,非法回落 black;style 作为镜像字段同步写;读入优先 winislandStyle,回退 style。切换 family 时保留用户 winislandStyle 原值,不重置(UI 默认值提示归 09)。

5.3 islandLayout v2(config.json.islandLayout)

v2 顶层(IslandLayoutV2):

字段 类型 默认值 说明
version number 2 必须存在
layout 'fixed' | 'custom' 'fixed' 布局模式(沿用旧字段)
fixed FixedSchemeV2 见下 classisland fixed 信息行配置;winisland 下忽略
schemes IslandLayoutSchemeV2[] 1 个默认方案 自定义方案(沿用旧结构并扩展)
activeSchemeId string 'default' 当前方案
residentEnabled boolean layout==='custom' 兼容字段;不得再作为独立开关
pointerFadeEnabled / pointerFadeReversed boolean true / false 鼠标避让淡化(沿用)
lessonReminders LessonReminderSettings 沿用旧默认 准备/上课/课间/结束/课中进度开关与秒数
updatedAt number 0 最近保存时间(仅诊断)

FixedSchemeV2(classisland fixed 的信息行):

字段 类型 默认值 说明
rows IslandRowV2[] 1 行 = 日期 + 课程表 信息行;默认只有一行
mainRowId string 首行 id 主行(承载消息气泡/课程提醒)
rowHeight number 40 ClassIsland 行高(32–64 夹取)
rowSpacing number 5 行间距(0–150)
backgroundOpacity number 0.5 行背景不透明度
cornerRadius number 8 行圆角(0–20)
progressBarHeight number 3 行内进度条高度(3px)
messageBubble 'inline' | 'card' 'inline' fixed 下消息表现:inline=行内气泡,card=老式详情卡(仅回退用)
maxRows number 3 信息行最多行数(自动换行/溢出策略由 07 定义)

IslandLayoutSchemeV2(在 v1 方案上扩展):

字段 类型 默认值 变化
id/name/rows/expandedRows 沿用 沿用 结构不变;行内容升级为 IslandRowV2
scale number 1 0.75–2.5(沿用于 CLASSISLAND_ISLAND_METRICS.scaleMin/Max)
color/foreground/opacity/radius/separated/spacing 沿用 沿用 radius 0–20;spacing 0–150
rowHeight number 40 新增:行高 32–64
rowSpacing number 5 新增:与 spacing 同步;spacing 保留兼容
theme 'fluent' | 'classic' 继承 appearance.theme 新增:方案级主题覆盖
fontSizes {secondary,body,emphasized,large} 继承 appearance.fontSizes 新增:方案级字号覆盖
progressBarHeight number 3 新增:行内进度条高度
unrecognizedNodes object[] [] 新增:未知 kind 的原始节点(不得丢弃,见 5.5 第 6 条)

IslandRowV2(行):

字段 类型 默认值 说明
id/name/main/allowNotifications/gap/nodes 沿用 沿用 main 行唯一;gap 0–64
appearance RowAppearance 见下 行级外观(新增)
hideRules RowHideRules 全 false 行级隐藏规则(新增)
progress RowProgress 见下 行级进度条(新增)

RowAppearance:backgroundEnabled=false、backgroundColor='#000000'、backgroundOpacity=0.5、foregroundColor='#ffffff'、cornerRadius=8、opacity=1、fontSizes(继承方案/外观)。

RowHideRules:inClass=false、noMessage=false、noClass=false、fullscreen=false;全部为 false 表示不隐藏。语义对应 ClassIsland 的行隐藏规则(repos/ClassIsland/ClassIsland.Core/Models/Components/MainWindowLineSettings.cs 的 HideOnRule/HidingRules)。

RowProgress:enabled=true、style='line'、height=3、color='accent'、showRemaining=true。

行内 3px 进度条的语义(数据来源不在本层实现):进度条表示"当前显示内容的时间进度"——消息气泡显示时 = 该消息剩余显示时间;否则 = 当前课程进度。具体数据来源与取值规则由 07/08 定义,本 schema 只保证 progress(行级)与 appearance.progressBar(全局)提供 height/show 两个开关;行设置抽屉的完整字段由 09 定义,本章只登记字段名(appearance/hideRules/progress)并要求与 09 对齐,不得在 05 里发明抽屉字段。

IslandNodeV2(节点,在 v1 IslandComponentNode 上扩展,字段名不变):

字段 变化
id/kind/appearance/advanced/children 沿用(appearance 四档字号默认改为继承行/方案)
settings.schedule.display 沿用 `'current'
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 迁移算法(必须逐步实现,可重入)

输入判定:

  1. 读取 config.json(不存在 → 全新安装:直接构造 v2 默认,family='classisland',不写迁移标记)。
  2. 文件存在但 islandLayout.version 缺失:视为 v1(唯一版本判据;不得再引入 schemaVersion 作为第二套版本号)。
  3. 老用户判定:islandLayout.version 缺失 或 文件里存在 islandLayout/island 任一字段 → 迁移时 family='winisland'(保持原外观,不惊吓用户)。
  4. 全新安装判定:文件不存在、或既无 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"
  1. 可重入/幂等:若 islandLayout.version===2,迁移函数必须直接返回原对象(不写盘、不改字段);重复执行不得产生二次修改。
  2. 老用户 fixed 不变:迁移后 family='winisland',fixed 仍渲染原胶囊+详情卡(ClassicIsland.vue),因此老用户升级后视觉 0 变化;只有用户主动在设置页切到 classisland 或新装用户才看到信息行。
  3. custom 老用户:custom 始终按 ClassIsland 规格渲染、不受 family 影响;迁移只升级数据(补行级默认、滚动 mode 等),不改 schema 结构。老 custom 用户升级后视觉会变为 ClassIsland 行规格(这是定稿行为),发布说明与设置页必须提示;fixed 用户(默认 winisland)不受影响。
  4. 编辑器打开时必须先读迁移后的 v2;若 islandLayout.version 不匹配(例如用户用旧版应用改过文件),必须触发一次"再迁移"而不是把 v1 当 v2 解析。
  5. 迁移必须记录 warning 列表到内存并在设置页"存储/诊断"可查看(哪几项被夹取、哪几个未知组件被保留)。

5.5 校验与夹取(normalizeIslandLayoutV2 / normalizeAppearanceV2)

必须在 shared 中实现,主进程保存前、编辑器加载后、IPC 存入前三处共用同一个函数。规则:

  1. version:仅接受 2;缺失/非法按迁移处理,不得静默改成 2 后继续用旧结构。
  2. layout:'fixed'|'custom',非法回落 'fixed'。
  3. family:'classisland'|'winisland',非法回落 'classisland'。
  4. 数量上限:行 ≤12/方案、节点 ≤200/全部方案、嵌套深度 ≤6、方案 ≤20;main 行唯一(多余取消)。
  5. 数值夹取:rowHeight 32–64、rowSpacing 0–150、gap 0–64、radius 0–20、opacity 0–1、backgroundOpacity 0–1、fontSizes 8–96、scale 0.75–2.5、rowSpan 1–6、progressBarHeight 1–8;非法/NaN 回落默认值(禁止 Math.max(min, NaN) 这种写法,参考 src/main/island.ts:2225-2241 的 fallback 说明)。
  6. 未知 kind:不得丢弃。把原始节点 JSON 放入 scheme.unrecognizedNodes(最多 200 条),渲染时忽略并显示"未知组件(已保留)";这样用旧版应用打开再保存也不会丢数据。
  7. 颜色:统一 #rrggbb 小写;非法回落默认;透明度字段独立,不把 #rrggbbaa 塞进颜色字段。
  8. id:空/非字符串/重复 → 重新生成(node-<n>/row-<n>/scheme-<n>);id 最长 100 字符。
  9. 文本:settings.text ≤2000 字符、locationName/countdownTitle ≤64;超长截断并记 warning。
  10. 经纬度:latitude -90..90、longitude -180..180,可空;空表示未配置(组件显示"请设置天气地点")。
  11. 未知字段:v2 对象里出现但类型未定义的字段必须原样保留在对象上(不删除),以便前向兼容。
  12. lessonReminders:prepareSeconds 5–1800、endingSeconds 5–600(依据现有 packages/shared/src/island-layout.ts:390-398)。
  13. winislandStyle / style:两者都只接受 black/glass/tinted,非法回落 black;读入优先 winislandStyle、回退 style,写出两者同步(兼容镜像);不得因 family 迁移而删除 style;family 与调色板相互独立(family 决定 fixed 用哪套渲染,winislandStyle 决定 winisland 的调色板)。
  14. theme:只接受 fluent/classic,非法回落 fluent;具体色板/圆角/阴影 token 由 06 定义,05 只登记字段与默认值。
  15. colorScheme:只接受 light/dark/system,非法回落 system;system 时跟随应用 html.dark 状态决定岛明暗(映射逻辑由渲染层实现,数据层只存枚举)。
  16. fontSize:8–96,缺省按 family 取(classisland=16 正文基准、winisland=13 旧基线);fontSizes 缺失时按 5.2 第 4 条派生;显式 fontSizes 优先;迁移不得覆盖旧 fontSize。
  17. effectEnabled:boolean,缺省 true;effectScale:只接受 1/0.75/0.5,缺省 1,非法取 1;progressBar:{height:1–8, show:boolean},缺省 {3,true}。
  18. 进度条高度优先级:行 progress.height > 方案 progressBarHeight > appearance.progressBar.height;任一缺失沿上一级回落,最终默认 3。progressBar.show=false 时所有行都不显示(行级 enabled=false 只关该行)。
  19. 滚动组件:settings.mode 只接受 continuous/pause,缺省 continuous;pause 时停顿秒数复用 settings.interval(夹取 1–300);continuous 时 speed 夹取 1–200(沿用现有范围)。
  20. 渲染/样式命名空间只由 layout(fixed 的 family)决定,不写入数据:fixed + family='winisland' → winisland 命名空间;fixed + family='classisland' → classisland 命名空间;custom 始终 classisland 命名空间、不受 family 影响;normalize 不得改写样式类名/选择器相关字段。
  21. IslandNotification.level、IslandLessonReminder.progress/leftSeconds、IslandState.family/pendingStaged/bubblePhase/bubbleDeadline 均为运行期派生字段,normalize 不处理、不落盘。

5.6 DTO 映射(ClassHelper 数据 → ClassIsland 语义)

fixed(classisland)行内气泡规则:同一时刻只显示 1 条 active 气泡;点击展开详情/操作;有多条未处理消息时不并列显示,而是折叠为「还有 N 条」,点击打开应用内通知中心(沿用 ClassHelper 现有通知中心路由);消息队列、去重、上课暂存、已读回执规则完全不变(02 §2.5/§2.12)。

IslandState → ClassIsland 主界面状态:

ClassHelper 字段 ClassIsland 语义 渲染映射(classisland fixed)
mode:'hidden' 岛不可见(仅提醒特效时临时显示) 信息行隐藏/仅保留细缝
mode:'pill' 紧凑态 行内气泡/铃铛提示
mode:'expanded' 展开态 行展开 + 消息面板
active/queued 提醒队列(当前播放/待播) 行内气泡列表,按 PRIORITY_RANK 排序
inClass TimeState=OnClass 行隐藏规则 inClass 生效、提醒遮罩可用
currentPeriodEnd 当前时间点结束 进度条/倒计时取值
reason 提醒来源(新消息/课后补发/紧急/叫人/收回中) 气泡入场动画与角标
listExpanded 列表是否展开更多 行内"展开更多"
lessonReminder 课级提醒(本地) 行覆盖层(遮罩/秒数徽标)
coursewareTransfer 课件提示(覆盖态) 最高优先级覆盖全部行

IslandNotification → ClassIsland 提醒语义:

ClassHelper 字段 ClassIsland 对应 规则
priority: LOW/NORMAL/HIGH/URGENT 提醒队列优先级(NotificationPriority) URGENT=插队立即播放;HIGH/NORMAL/LOW 按排序入队
kind:'notification' 普通提醒(Mask+Overlay 两阶段) 两阶段时长取 5s/5s(repos/ClassIsland/...NotificationRequest.cs:17-20)
kind:'homework' 作业提醒(本地合成 id homework-<id>) 不进入已读回执;不可"标为已读"
kind:'call' 叫人(Call) 上课时保持可点、不自动展开;紧急叫人按 URGENT
title/content/teacherName/courseName/createdAt 提醒标题/正文/来源/时间 气泡第一行标题、第二行来源+时间
id 去重键 双链路(通知/叫人)合并时 id 相同即合并(见 02 §2.5 第 3 条)

ScheduleDto → ClassIsland 课表语义(仅映射,不落 Profile 文件):

ClassHelper 字段 ClassIsland 概念 映射规则
id ClassInfo/条目标识 仅内部使用
course.courseName Subject.Name 直接展示;null 显示"—"
dayOfWeek(1–7) TimeRule.WeekDay 1=周一…7=周日
startTime/endTime(HH:mm) TimeLayoutItem.StartTime/EndTime 转 HH:mm:00;只用于显示与进度计算
weekStart/weekEnd 周次范围 用于"本周是否有课"
weekParity: ALL/ODD/EVEN TimeRule.WeekCountDiv(0/1/2)+ WeekCountDivTotal=2 ALL→0;ODD→1;EVEN→2
location 附加信息 仅在气泡/详情展示,不参与时间点

IslandTimePointDto → TimeLayoutItem:

ClassHelper ClassIsland 规则
timeType: LESSON/BREAK/DIVIDER/ACTION TimeType: 0/1/2/3 一一对应(repos/ClassIsland/...TimeLayoutItem.cs:119-138)
name(课间名/时间点名) BreakName/名称 课间空时显示"课间休息"
courseName 默认课程 null 等价 Guid.Empty(无课)
startTime/endTime 开始/结束 用于行内课程表折叠与进度
isHideDefault IsHideDefault 折叠时隐藏、轮到时展开
  1. 上述映射只用于渲染语义对齐,不得把 ScheduleDto 落盘成 ClassIsland 档案;数据仍以服务端解析结果为准(02 §2.12)。
  2. CoursewareTransfer 映射为"最高优先级覆盖态",不进入提醒队列、不参与已读;映射字段 name/category/teacherName/receivedBytes/totalBytes/progress/stage/error 直接使用(packages/shared/src/courseware.ts、src/island/CoursewareTransfer.vue:12-65)。

运行期状态扩展(IslandState,不落盘,必须随状态推送):

字段 类型 默认值 语义
family 'classisland' | 'winisland' 当前 appearance.family 当前生效风格;渲染进程据此切换 CSS 命名空间
pendingStaged number 0 上课期间被暂存、下课后待补弹的消息数(对应现有 queued 中因上课被压住的数量;queued.length 不变)
bubblePhase 'idle' | 'mask-in' | 'content-in' | 'out' 'idle' 行内气泡/提醒的两阶段播放阶段(固定 5s/5s 时长的可视化映射)
bubbleDeadline number 0 当前气泡自动收起的时间戳(毫秒);0 表示无
  1. IslandState 的新字段只存在于运行期推送(island:state),不得写进 config.json、不得被编辑器保存;旧渲染进程缺字段时必须按默认值渲染(前向兼容)。
  2. pendingStaged 必须由主进程根据现有上课暂存逻辑计算(02 §2.5 第 2 条),不得让渲染进程自行推断。

IslandNotification.level(展示层级,派生字段):

priority level 用途
URGENT 'emergency' 紧急气泡/强提醒特效可触发
HIGH 'high' 高亮气泡/角标
NORMAL 'normal' 普通气泡
LOW 'low' 弱化展示

level 由 priority 派生,不落库、不持久化;通知进入岛队列时在 main/island/messages.ts 统一计算,渲染层只读取。

IslandLessonReminder 扩展字段(运行期,不落盘):

字段 类型 语义
progress number (0–1) 当前提醒/课程进度:ending/in-class = (now-lessonStartsAt)/(lessonEndsAt-lessonStartsAt);其余 = (endsAt-now)/(endsAt-startedAt)(与现有 LessonReminderContent.vue:36-50 计算一致)
leftSeconds number 下课前剩余秒(ceil((lessonEndsAt-now)/1000),clamp ≥0),供蓝色圆形秒数徽标与遮罩标题使用;现有 LessonReminderContent.vue:24-35 已有同义计算
kind 不变 仍为 prepare/on-class/break/ending/in-class

fixed(classisland)下的提醒叠加口径:prepare/on-class/break 以行内容接管呈现(临时替换课程表内容,动画由 08/10 定义);ending 不接管行内容,只在课程名右侧显示蓝色圆形秒数徽标(leftSeconds 驱动);custom 模式不接管,保持课程组件 + 秒数徽标。该口径只影响渲染,不改变 02 §2.8 的五类提醒计算与开关语义。

5.7 IPC payload 版本与兼容

  1. IPC payload 可以携带 schemaVersion:2 仅用于运行时校验/调试,不得落盘、不得作为配置版本判据;配置版本只认 islandLayout.version。island:state/island:appearance/island:get-layout 三个高频 payload 建议带上该字段(旧字段保留,新增字段可选)。
  2. 接收方规则:未知字段忽略、缺字段回落默认;不得因为缺 family 就拒绝渲染(老渲染进程遇到新主进程时应按 winisland 降级,反之按 classisland)。
  3. island:save-layout 必须返回归一化后的 v2 结果(编辑器以返回值为准,避免"本地看着对、主进程夹取后不一致")。
  4. 主进程 getConfig() 返回给渲染进程的 islandLayout 必须是 v2;island 必须含 family 与新增外观字段(theme/effectEnabled/effectScale/progressBar)。
  5. 旧通道名全部保留(见 04 §4.4);新增通道必须同时在 shared/island/ipc.ts 与 preload 中登记,不得字符串散落。
  6. 作业指纹相关 payload(见 04 §4.3.13)必须带 source(realtime/homework-local/replay/call)与 dedupeKey;主进程按 key 去重,渲染进程保留现有"本机刚录作业免上岛"快速路径。

5.8 编辑器预览与临时状态(不持久化)

  1. 编辑器的内嵌预览、缩放、选中项、拖拽态属于临时状态,不得写入 config.json;只有"应用/保存"才写 v2。
  2. "无消息展开内容(expandedRows)"与常态 rows 分开编辑、分开保存;activeSchemeId 只在切换方案时写。
  3. 设置页"测试灵动岛"仍走 islandPush + context.preview=true(30 秒预览、失焦不收起,见 02 §2.15),预览消息 id 必须带前缀(如 appearance-test-)以便清理;预览不写历史、不改已读。
  4. 编辑器打开时若检测到迁移 warning,必须在顶部显示"部分旧设置已按新格式调整(可查看详情)",不得静默。

5.9 回滚与降级

  1. 回滚入口:设置页"灵动岛 → 恢复升级前布局"必须存在;回滚步骤:先备份当前 config.json → 把 config.json.v1.bak 覆盖回去(该文件天然没有 islandLayout.version,即 v1)→ 提示重启 → 重启后旧版本行为恢复(family=winisland/fixed=胶囊)。
  2. 迁移失败降级:应用不得崩溃;以内存默认 v2 启动(不写盘),岛显示默认信息行,设置页显示错误与"恢复备份"按钮。
  3. 前向兼容:读到 islandLayout.version>2 时,应用只读该配置(不覆盖写),在设置页提示"配置由更高版本创建";不存在其他版本字段可用于判断。
  4. 文件损坏:config.json 非法 JSON 时沿用现有 .bak 回退逻辑(config.ts 现状:读取失败回落默认),回滚后仍必须按 islandLayout.version 缺失执行一次迁移判定;不得用损坏文件覆盖 .bak。
  5. 旧版应用打开新文件:不可避免(旧版不认识 v2 字段),因此 v2 必须保持"未知字段保留"原则(5.5 第 11 条)并把关键新数据放在旧版会忽略而不是报错的位置;旧版保存后新字段可能丢失,需在文档提示用户不要跨版本反复切换。

5.10 版本变更记录与兼容矩阵

必须维护一张可追溯的变更表(代码里以常量数组或文档形式存在),用于判断"某个字段从哪版开始有、哪版废弃":

变更 版本 兼容策略
islandLayout.version 新增(唯一落盘版本字段) v2 缺失=1,按 5.4 迁移
appearance.family 新增 v2 缺失时:老配置迁移=winisland;新装=classisland
appearance.theme(fluent/classic)新增 v2 缺失=fluent;仅 classisland 生效;具体 token 由 06 定义
appearance.effectEnabled / effectScale 新增 v2 缺失 true/1;effectScale 只允许 1/0.75/0.5
appearance.progressBar {height,show} 新增 v2 缺失 {3,true};高度优先级 行 > 方案 > 外观
appearance.fontSizes 新增 v2 缺失时按旧 fontSize 比例推导(5.4 step1)
fixed(信息行)新增 v2 缺失时生成默认 1 行(date+schedule);仅 classisland fixed 使用
行级 appearance/hideRules/progress 新增 v2 缺失时补默认;字段名与 09 对齐;winisland fixed 下保留数据但忽略(custom 始终使用)
方案级 rowHeight/rowSpacing/theme/fontSizes/progressBarHeight 新增 v2 缺失时继承 appearance 默认
appearance.colorScheme 新增 v2 缺失=system;非法回落 system
appearance.fontSize 语义按 family 分叉 v2 classisland=正文基准(新装 16,派生 14/18/20);winisland=旧语义(13);迁移保留旧值
appearance.winislandStyle 新增,style 转兼容镜像 v2 读优先 winislandStyle、回退 style;写两者同步;style 不删除;切换 family 保留原值
滚动组件 settings.mode 新增 v2 缺失=continuous;非法回落 continuous;pause 复用 settings.interval
residentEnabled 转纯兼容 v2 读出后与 layout==='custom' 对齐;不再作为独立开关
family 作用域 v2 只影响 fixed;custom 始终 ClassIsland 规格、不受 family 影响
旧 style 字段 保留镜像 本轮不删除;与 family/winislandStyle 独立;建议 v3 再评估废弃
迁移标记文件 config.island-migration.json 2 仅诊断用途;不是版本字段、不参与运行逻辑
回滚文件 config.json.v1.bak 2 迁移前覆盖写;回滚后仍保留,供二次回滚

兼容矩阵(读写方向必须明确):

  1. 新读旧(v2 代码读 v1 文件):允许,走 5.4 迁移;迁移后内存中只存在 v2。
  2. 新读新:允许,走 normalize;islandLayout.version 更高时只读。
  3. 旧读新(旧代码读 v2 文件):旧代码会忽略未知字段继续运行(因为 v2 把新字段放在其不认识的键上),但旧代码保存后会丢新字段;因此必须在文档与发布说明中写明"不要用旧版应用反复编辑新配置"。
  4. 新写旧:禁止。v2 代码写盘必须带 islandLayout.version:2,不得输出 v1 结构,也不得写任何文件级版本号。
  5. 迁移与编辑器并发:编辑器打开时若检测到 config.json 被迁移,必须重新加载(比较 updatedAt/mtime 或读取返回值),不得用旧草稿覆盖迁移结果;保存前再次 getConfig 核对 islandLayout.version。

兼容性红线(不得违反):

  • 不得删除任何仍被旧配置使用的字段名(如 style、residentEnabled、spacing、expandedRows、pointerFadeEnabled)。
  • 不得把对象结构从"方案数组"改成"对象字典"或反之(旧编辑器/旧冒烟会解析失败)。
  • 不得改变 ISLAND_LIST_METRICS 与 ISLAND_SIZE_DELTA 的语义(可以新增 classisland 专用 metrics,但旧常量必须继续导出且数值不变),因为大量冒烟断言按旧值计算(packages/shared/src/constants.ts:344-351,379-402)。
  • 不得让 v2 的 rows 与 v1 的 rows 在同一键上结构不兼容:v2 行只增加字段,不重命名已有字段。

5.11 验收标准(第 5 部分)

  1. 新装启动后 config.json 不含任何 schemaVersion;含 island.family:'classisland'、island.colorScheme:'system'、island.fontSize:16、island.theme:'fluent'、island.effectEnabled:true、island.effectScale:1、island.progressBar:{height:3,show:true}、islandLayout.version:2、fixed.rows 默认 1 行(日期+课程表);layout:'fixed',界面呈现 ClassIsland 信息行。
  2. 用一份旧 v1 配置(fixed、custom 各一份,含 schemes/nodes/appearance/residentEnabled)启动:迁移成功、无字段丢失、family:'winisland'、旧胶囊外观不变、fontSize 保留原值、style/winislandStyle 一致;config.json.v1.bak 存在。
  3. 迁移后再次启动:不重复迁移、不重复写盘(可用文件 mtime/内容哈希断言幂等)。
  4. 迁移失败模拟(把 config.json 改坏/只读)时:原文件不被破坏、应用以默认 v2 启动、设置页显示错误与恢复入口。
  5. "恢复升级前布局"可把 v1 备份恢复回来并重启生效;恢复前自动再备份当前配置。
  6. normalizeIslandLayoutV2 对行数>12、节点>200、深度>6、方案>20、非法颜色/数值/kind 全部夹取或保留到 unrecognizedNodes;不崩溃、不丢未知组件。
  7. 老用户切到 classisland 后 fixed 变信息行、切回 winisland 恢复胶囊;family 切换时 style/winislandStyle 与 fontSize 保持用户原值不变;custom 始终 ClassIsland 规格、切换 family 不改变 custom 渲染。
  8. 编辑器中常态行/展开行分开保存;应用后 islandLayout 为 v2 且 island:save-layout 返回值与本地一致。
  9. IslandState/IslandNotification/ScheduleDto/IslandTimePointDto 的映射表逐项实现:优先级、上课态、时间点类型、单双周、作业/叫人特殊语义均可按 5.6 追踪。
  10. IPC 兼容:旧通道全部可用;payload 可选带 schemaVersion 仅作校验(不落盘);缺字段时按 family 降级渲染而不是报错。
  11. 编辑器/设置页不把预览、缩放、选中项写入配置;预览消息不写历史、不改已读。
  12. 读到 islandLayout.version>2 时应用只读并提示,不得覆盖用户文件。
  13. 外观新字段:关闭 effectEnabled 后强提醒特效不再渲染(行内气泡/普通提醒仍工作);effectScale 只接受 1/0.75/0.5 并实际改变特效尺寸;progressBar.show=false 时所有行进度条隐藏,行级高度覆盖方案/外观。
  14. 运行期状态:IslandState 含 family/pendingStaged/bubblePhase/bubbleDeadline 且不写入 config.json;上课暂存时 pendingStaged 与队列数一致;气泡阶段按 idle→mask-in→content-in→out 推进。
  15. 派生字段:IslandNotification.level 按 URGENT→emergency/HIGH→high/NORMAL→normal/LOW→low 生成且不落盘;IslandLessonReminder.progress 在 0–1、leftSeconds 与蓝色圆形徽标一致(剩余 1 秒显示 -1)。
  16. fixed(classisland)下 prepare/on-class/break 以行内容接管呈现、ending 只显示秒数徽标;custom 下不接管(课程组件 + 徽标);两种 family 的提醒开关语义一致。
  17. fixed 行内气泡同一时刻只显示 1 条 active;多条折叠为「还有 N 条」并可打开应用内通知中心;队列/去重/暂存规则不变。
  18. family='winisland' 只影响 fixed(保持现有胶囊观感);custom 始终按 ClassIsland 规格渲染、不受 family 影响、不引用 winisland 样式命名空间;family='classisland' 的 fixed 使用 classisland 命名空间。
  19. 经 pnpm verify:desktop(默认无头 quick)验证时,所有既有冒烟注入点仍可用:setTestInputPassthrough/setHitTestCursor/getWindowBounds/getHitRect/getInteractive/setBlurCollapseEnabled/setTouchMode/getTouchMode/isRecovering 等名称与语义不变(改造归属 part 11 测试基线)。
  20. colorScheme 三种取值均生效:system 跟随应用深浅色(html.dark)、light/dark 强制;非法值回落 system;切换后岛即时重渲染且不改主窗口主题。
  21. fontSize 语义按 family:classisland 下改 fontSize 会派生 14/18/20 阶梯并整体缩放行文字;winisland 下保持旧"基准字号"行为;迁移用户旧值不被覆盖。
  22. 滚动组件 settings.mode:continuous 持续滚动、pause 到边界停顿 settings.interval 秒;normalize 后取值合法,旧配置缺失补 continuous。

6. 设计 tokens 与视觉基线(双风格:ClassIsland 默认 / winisland 兼容)

本节是整轮重构的「视觉地基」。实现者必须先把 token 落到 packages/desktop-client/src/island/ 下的独立样式入口,再改任何两个岛的渲染文件。所有像素、颜色、字号、圆角、阴影、透明度必须从这里取值,禁止在 IslandApp.vue、CustomIslandRuntime.vue、ComponentNode.vue、ClassicNotifications.vue、LessonReminderContent.vue、custom-runtime.css 里写裸值。

6.0 双风格模型(必须先建,再谈 token)

外观契约新增/改为(packages/shared/src/types.ts 的 IslandAppearance 扩展,config.ts 的 normalizeIslandAppearance 同步):

字段 类型 默认 含义 证据
family 'classisland' | 'winisland' 新装 classisland;老用户升级为 winisland(见 §6.12 迁移规则) 视觉家族 Lead 裁决第 1 条(parts-r/01-objective.md:60)
theme 'fluent' | 'classic' fluent 仅 family='classisland' 有效;对应 ClassIsland 两套 XamlTheme repos/ClassIsland/ClassIsland/App.Services.xaml.cs:297-315
colorScheme 'light' | 'dark' | 'system' system 仅 family='classisland' 有效;跟随应用深浅色 repos/ClassIsland/ClassIsland/Models/Settings.cs:979-989、Services/ThemeService.cs:53-63
style 'black' | 'glass' | 'tinted' black 仅 family='winisland' 有效(现有语义不变) packages/shared/src/constants.ts:265-290,324-330
animations boolean true 岛动画总开关 constants.ts:273;IslandApp.vue:249
speed number 1(0.5–2) 动画速度倍率(按比例缩放所有时长) constants.ts:274;IslandApp.vue:291
fontSize number 13(11–20) base 字号,四档字号由它派生 constants.ts:272;IslandApp.vue:527
height / width number 44 / 268 仅 winisland 的胶囊几何;classisland 下用于「信息行最小宽/高」约束(见 §6.5) constants.ts:266-267;IslandApp.vue:188-204
radius number 20(8–32) winisland 胶囊基准半径;classisland 下取 8(范围 0–20),见 §6.4 constants.ts:268;IslandApp.vue:209-211

实现要求:

  1. 岛根元素必须同时挂三个数据属性,CSS 用它们切 token 与几何:data-family="classisland|winisland"、data-theme="fluent|classic"、data-color-scheme="light|dark"(winisland 恒为 dark)。禁止用 JS 拼内联样式切风格。
  2. family='classisland' 时不使用 spring.ts / squircle.ts:几何用标准 border-radius + CSS transition;family='winisland' 时继续用弹簧形变与超椭圆路径(Lead 裁决第 4 条,parts-r/01-objective.md:52)。
  3. 两套 family 的 token 不得混用:classisland 的静态值定义在 --ci-*;winisland 的调色板保持 --wn-*;运行时外观注入继续用 --island-*(两套共用这几个动态变量:--island-font/--island-radius/--island-accent/--island-dur/--island-anim)。
  4. Token 文件必须是单一来源:新建 packages/desktop-client/src/island/tokens.css,在 island.html 的样式链最前面 import(src/island/main.ts 已 import @classhelper/ui/tokens.css,见 main.ts:1),并由 IslandApp.vue 与 CustomIslandRuntime.vue 共同继承。禁止在 .vue 的 <style> 里重新定义全局 token。
  5. 主题切换动画只允许改颜色类 token,绝不覆盖圆角/间距/字号/动画 token(与主窗口纪律一致:docs/dev/motion.md:125-126)。

6.1 变量分层与命名规范

层 前缀 归属 是否可被运行时覆盖 本重构动作
1 应用外壳 --ch-ui-*、--ch-*、--el-* packages/ui/src/tokens.css、theme.css、renderer/styles/index.css 是(主窗口主题) KEEP,不改值;岛只读引用(如编辑器 UI)
2 ClassIsland 岛静态 token --ci-* 新增 src/island/tokens.css 否(改值只能改源码) NEW
3 岛运行时外观 --island-* IslandApp.vue:522-528、CustomIslandRuntime.vue:102-123 注入 是(设置页实时) KEEP 并扩展(补 --island-row-*)
4 winisland 调色板 --wn-* classic-island.css:60-82(.island-card 内) 是(data-style 切换) KEEP(仅 winisland 生效)
5 组件方案 --custom-*、--resident-*、--row-gap custom-island.css、custom-runtime.css 由 scheme 注入 是 KEEP,值按 §6.5 改默认

命名冲突处理(必读):现有 LessonReminderContent.vue:133-137 已在组件作用域注入 --ci-speed、--ci-duration、--ci-title-duration、--ci-scale-duration、--ci-delay,ComponentNode.vue/custom-runtime.css 还使用 --ci-body-font。这些是组件级变量,与本节新增的静态 token 同前缀,容易误伤。处理办法:

  • 新增静态 token 中禁止使用这 6 个名字;
  • M5 阶段把 LessonReminderContent.vue 的 5 个变量重命名为 --ci-lesson-speed/-duration/-title-duration/-scale-duration/-delay,保留旧名作为 alias(var(--ci-lesson-duration, var(--ci-duration, 200ms)))一版后删除;
  • --ci-body-font 保留(custom 行基准字号),在 tokens.css 中定义为 var(--ci-fs-body)。

6.1.1 --ci-* 变量速查表(必须在 tokens.css 中一次性定义齐)

实现者按此表逐行写入,缺一个都会在第 7/10 部分的样式里解析为空、导致整条声明失效。

变量 默认(classisland 深色) 用途
--ci-bg #202020 岛/行背景色(配合 --ci-opacity-bg)
--ci-bg-message #202020 阅读消息/详情实底
--ci-fill-1 rgba(255,255,255,.06) 行内浅底(列表行、chip)
--ci-fill-2 rgba(255,255,255,.10) 悬停底、按钮底
--ci-fill-3 rgba(255,255,255,.14) 徽标底、强强调底
--ci-border rgba(255,255,255,.15) 岛/行 1px 描边
--ci-border-strong rgba(255,255,255,.24) 选中/拖拽目标描边
--ci-border-accent var(--ci-accent) 强调边(遮罩边缘)
--ci-divider rgba(255,255,255,.09) 行内分隔线
--ci-focus-ring var(--ci-accent) 焦点环颜色
--ci-text-1 #FFFFFF 主文字
--ci-text-2 rgba(255,255,255,.855) 次要文字
--ci-text-3 rgba(255,255,255,.773) 辅助文字
--ci-text-4 rgba(255,255,255,.529) 装饰文字
--ci-text-5 rgba(255,255,255,.365) 禁用文字
--ci-text-on-accent #FFFFFF accent 底上文字
--ci-accent #00BFFF(运行时被 --island-accent 覆盖) 强调色
--ci-accent-soft accent @10% 强调淡底
--ci-accent-soft-strong accent @14% 强调淡底加强
--ci-state-urgent #FF453A 紧急
--ci-state-call #FF9F0A 叫人
--ci-state-homework var(--ci-accent) 作业
--ci-state-ok #228B22 正常/优
--ci-warning #FFA500 警告
--ci-danger #CD5C5C 危险/错误
--ci-tint-urgent 径向 rgba(255,69,58,.22) 紧急内层染色
--ci-tint-call 径向 rgba(255,159,10,.20) 叫人内层染色
--ci-progress-h 3px 行内进度条高度
--ci-progress-track rgba(255,255,255,.15) 进度条轨道
--ci-progress-fill var(--ci-accent) 进度条填充
--ci-progress-radius 0 进度条圆角
--ci-mask-fill var(--ci-accent) 斜切遮罩填充
--ci-mask-text #FFFFFF 遮罩文字
--ci-radius-island 8px 岛圆角(0–20)
--ci-radius-row 8px 单行圆角(分体)
--ci-radius-card 8px 详情卡/气泡圆角
--ci-radius-control 4px 控件/列表行圆角
--ci-radius-pill 999px 胶囊/徽标圆角
--ci-row-h 40px 行高
--ci-row-pad-x 12px 行内左右内边距
--ci-row-min-w 20px 行内容最小宽
--ci-row-gap 5px 行间距(可调 0–150)
--ci-comp-gap 12px 相邻组件间距(单侧 6)
--ci-shadow-pad 10px 窗口/阴影留白
--ci-shadow-island 0 4px 8px 2px rgba(0,0,0,.28) 岛投影
--ci-shadow-mask 0 8px 32px rgba(0,0,0,.4) 提醒时投影
--ci-shadow-popup 0 8px 16px rgba(0,0,0,.45) 岛内弹层投影
--ci-opacity-bg 0.5 背景不透明度
--ci-opacity-faded 0.05 指针避让淡化
--ci-fs-secondary 14px(派生) 次要字号
--ci-fs-body 16px(派生) 正文字号
--ci-fs-emphasized 18px(派生) 强调字号
--ci-fs-large 20px(派生) 大号字号
--ci-fw-regular 400 常规字重
--ci-fw-medium 500 中等字重
--ci-fw-bold 700 粗字重
--ci-font HarmonyOS 栈 正文字体族
--ci-z-mask 2 遮罩层
--ci-z-mask-text 3 遮罩文字层
--ci-z-popup 4 岛内弹层
--ci-z-drag-ghost 10000 编辑器拖拽幽灵
--ci-dur-instant 90ms 高频反馈
--ci-dur-fast 100ms 淡出/短过渡
--ci-dur-base 200ms 默认过渡
--ci-dur-slow 300ms 行宽(Fluent)
--ci-dur-overshoot 550ms 行宽(Classic)
--ci-dur-long 750ms 遮罩缩放
--ci-ease-out cubic-bezier(.16,1,.3,1) ClassHelper 主缓动
--ci-ease-fl-1 cubic-bezier(.65,0,.35,1) Fluent 行宽
--ci-ease-fl-2 cubic-bezier(.25,1,.5,1) 遮罩文字/缩放
--ci-ease-fl-3 cubic-bezier(.5,0,.75,0) 遮罩退出
--ci-ease-overshoot cubic-bezier(0,1,.5,1.1) Classic 行宽过冲
--ci-ease-quartic-out cubic-bezier(.16,1,.3,1)【推断】 区段扫入
--ci-ease-quadratic-in cubic-bezier(.11,0,.5,0)【推断】 区段收拢
--ci-ease-sine-in cubic-bezier(.12,0,.39,0)【推断】 覆盖层淡入
--ci-dur-mask-region 360ms 单个斜切区段
--ci-dur-mask-stagger 120ms 区段错峰
--ci-delay-mask-text 260ms 遮罩文字延迟
--ci-dur-mask-text 250ms 遮罩文字淡入
--ci-dur-mask-scale 750ms 遮罩文字缩放
--ci-delay-mask 600ms 遮罩投影延迟
--ci-dur-mask-shadow 100ms 遮罩投影/背景淡入
--ci-dur-mask-exit 200ms 遮罩退出

6.2 颜色 token 表

6.2.1 背景与表面

Token ClassIsland 目标值(family=classisland) ClassHelper 现状(变量/值) 迁移策略
--ci-bg 深色:#202020 @ opacity 0.5(Fluent 用 SolidBackgroundFillColorSecondaryBrush,Classic 用 SolidBackgroundFillColorBaseBrush);浅色:#F3F3F3 @ 0.5 --wn-bg:#000000(black)/rgba(32,32,36,0.804)(glass)/#0b0b0f(tinted),见 classic-island.css:60-82;custom:--wn-bg: #20202080(custom-runtime.css:241-245) NEW --ci-bg;classisland 下替换 .island-card 背景;winisland 保留 --wn-bg;custom 在 classisland family 下把方案默认色从 #000000 改为 #202020(island-layout.ts:246),并叠加 0.5 不透明度
--ci-bg-message #202020(不透明,阅读消息/详情时用实底) .message-surface { --wn-bg:#202020 }(classic-island.css:84-86、custom-runtime.css:253-255) KEEP 值,改名 --ci-bg-message 作为语义 token;winisland 继续用 .message-surface 覆盖 --wn-bg
--ci-bg-elevated rgba(255,255,255,0.06)(行内 chip/进度底/列表行底) rgba(255,255,255,0.045)(.list-row,classic-island.css:389)、rgba(255,255,255,0.12)(.row-badge、.pill-type)、#ffffff0d/#ffffff1a(custom) CHANGE:classisland 统一到 6%/10%/14% 三档:--ci-fill-1/2/3
--ci-fill-1/2/3 6% / 10% / 14% 白(深色);4% / 8% / 12% 黑(浅色) 同上散落 NEW
--ci-mask-fill 强调色(AccentFillColorDefaultBrush,ClassIsland 提醒遮罩填充) 当前无(ClassHelper 消息用胶囊/详情卡,无遮罩) NEW,classisland fixed 消息气泡与 lesson 提醒共用
--ci-tint-urgent rgba(255,69,58,0.22) 径向 .urgent .tint 同值(classic-island.css:168-171) KEEP 值,归入 classisland 状态色
--ci-tint-call rgba(255,159,10,0.20) 径向 .call .tint 同值(:173-176) KEEP 值
--ci-tint-tinted accent 30% → transparent 135° 渐变 [data-style='tinted'] .tint(:178-185) KEEP(仅 winisland tinted)
--ci-bg-hover rgba(255,255,255,0.10)(classisland 行/按钮 hover) --wn-surface-hover: rgba(255,255,255,0.18) / #ffffff1a CHANGE:classisland 用 10%,winisland 保留 18%

6.2.2 描边

Token ClassIsland 目标值 ClassHelper 现状 迁移策略
--ci-border 1px rgba(255,255,255,0.15)(深色)/ rgba(0,0,0,0.08)(浅色) --wn-border: rgba(255,255,255,0.118)(classic-island.css:62);glass 0.157(:74);custom inset 0 0 0 1px #ffffff1e(custom-island.css:13) NEW;classisland 行容器 border:1px solid var(--ci-border);winisland 保留描边画在 SVG path 上(stroke-width:1,:110-113)
--ci-border-strong rgba(255,255,255,0.24)(选中/拖拽目标) #ffffff2b(custom-island.css:171)、.node-editing:hover #ffffff28(ComponentNode.vue:432) NEW,统一 24%
--ci-border-accent accent 100%(遮罩/强提醒强调边) --state-color 混合描边(classic-island.css:156-159,62% 状态色) CHANGE:classisland 用 accent 纯色;winisland 保留混合规则
--ci-divider rgba(255,255,255,0.09)(行内分隔) --wn-divider: rgba(255,255,255,0.09)(:68) KEEP 值,语义化改名
--ci-focus-ring 2px accent,outline-offset:2px(设置/编辑器)+ -3px(岛内按钮) #8dafff/#adc4ff/#85aaff 三处不同(custom-island.css:65-68、custom-runtime.css:190-193、ComponentNode.vue:394) CHANGE:classisland 统一 2px solid var(--ci-accent);winisland 保留原色

6.2.3 文字阶梯

Token ClassIsland 目标值(深色) ClassHelper 现状 迁移策略
--ci-text-1 #FFFFFF(100%) --wn-text-1: rgba(255,255,255,0.92)(classic-island.css:63) CHANGE:classisland 用 100%(ClassIsland 深层级 255 alpha,见 repos/ClassIsland/ClassIsland.Core/Helpers/UI/ControlColorHelper.cs:24-33);winisland 保留 0.92
--ci-text-2 rgba(255,255,255,0.855) --wn-text-2: 0.72(:64) CHANGE:classisland 85.5%;winisland 72%
--ci-text-3 rgba(255,255,255,0.773) --wn-text-3: 0.58(:65) CHANGE:classisland 77.3%;winisland 58%
--ci-text-4 rgba(255,255,255,0.529) 无(classic 用 0.34 的 chevron,:317) NEW
--ci-text-5 rgba(255,255,255,0.365) #ffffff60(custom-island.css:88)、#ffffff80(custom-runtime.css:145) NEW
--ci-text-on-accent #FFFFFF(深色遮罩上) #fff(.title,classic-island.css:628)、#0b0b0f(.solid-btn,:711) NEW,遮罩/强提醒文字统一白;winisland solid-btn 保留深色字
浅色方案 #1B1B1B / rgba(0,0,0,0.855) / 0.773 / 0.529 / 0.365 岛当前恒为深色 NEW:classisland family 支持浅色(§6.10)

字号必须同时遵守 ISLAND_TYPE_SCALE(winisland)与四档阶梯(classisland),见 §6.3。

6.2.4 强调色与状态色

Token ClassIsland 目标值 ClassHelper 现状 迁移策略
--ci-accent ClassIsland 默认 #00BFFF(DeepSkyBlue,repos/ClassIsland/ClassIsland/Models/Settings.cs:35);运行时由 appearance.accent 覆盖 DEFAULT_ISLAND_APPEARANCE.accent = '#0a84ff'(constants.ts:271);IslandApp.vue:107 注入 KEEP 用户值;family='classisland' 且用户从未改过 accent(仍等于旧默认 #0a84ff)时迁移为 #00BFFF;否则保留。--island-accent 继续作为运行时变量
--ci-accent-soft accent @ 10%(ClassIsland CustomizedAccentBarBackground1Brush 运行时 = accent@10%,repos/ClassIsland/ClassIsland/Services/ThemeService.cs:66-74) color-mix(in srgb, var(--island-accent) 20%/22%/26%, transparent)(classic-island.css:375,524,545) CHANGE:classisland 统一 10%/14% 两档 --ci-accent-soft、--ci-accent-soft-strong
--ci-state-urgent #FF453A #ff453a(classic-island.css:170,260,514,551) KEEP 值,改名 token
--ci-state-call #FF9F0A #ff9f0a(:175,256,519,567) KEEP 值
--ci-state-homework accent accent(classic-island.css:263-265,417-419,523-526) KEEP
--ci-state-ok #228B22(可选用天气/正常态) 无 NEW(与 ClassIsland AQI 1 同值,见 Themes/LightTheme.axaml:10)
--ci-danger #CD5C5C(IndianRed,ClassIsland 危险文字) #ffb4ab(课件错误,CoursewareTransfer.vue:127) KEEP 现状(课件)+新增 classisland --ci-danger
--ci-warning #FFA500 无 NEW
--ci-on-state #FFFFFF .badge-urgent #ff6961 等 KEEP winisland

6.2.5 进度条与遮罩

Token ClassIsland 目标值 ClassHelper 现状 迁移策略
--ci-progress-h 3px(ClassIsland 行内进度条) 2px(.lesson-progress,ComponentNode.vue:351)、2px(.ci-lesson-progress,LessonReminderContent.vue:312)、5px(课件,CoursewareTransfer.vue:111) CHANGE:classisland family 行内进度条 3px;课件保持 5px(不同组件,不受此 token 管)
--ci-progress-track rgba(255,255,255,0.15) #ffffff24(ComponentNode.vue:352) CHANGE
--ci-progress-fill var(--island-accent) accent(ComponentNode.vue:359、LessonReminderContent.vue:313) KEEP
--ci-progress-radius 0(ClassIsland 进度条直角) 1px(ComponentNode.vue:354) CHANGE:classisland 0;winisland 保留 1px
--ci-mask-fill accent(斜切遮罩填充,ClassIsland AccentFillColorDefaultBrush,repos/ClassIsland/ClassIsland/XamlThemes/FluentTheme/Styles.axaml:180) var(--island-accent, #60a5fa)(LessonReminderContent.vue:251) KEEP 语义,改用 --ci-mask-fill
--ci-mask-text #FFFFFF(ClassIsland TextOnAccentFillColorPrimaryBrush) #fff KEEP

6.2.6 Fluent / Classic 主题色差(classisland family 内部)

Token Fluent(默认) Classic 证据
行背景 SolidBackgroundFillColorSecondaryBrush(暗 #1C1C1C【推断】) SolidBackgroundFillColorBaseBrush(暗 #202020【推断】) repos/ClassIsland/ClassIsland/XamlThemes/FluentTheme/Styles.axaml:45;ClassicTheme/Styles.axaml:214
描边 1px ControlElevationBorderBrush 无显式描边 FluentTheme:46-47
阴影 0 4 8 2 #48000000 常态无投影(提醒时 DropShadowEffect blur 8 opacity 1) FluentTheme:49;ClassicTheme:36-45,283-287
圆角 用 RadiusX(默认 8,0–20) 直角(圆角 0) FluentTheme:48;ClassicTheme:46-59
遮罩动画 斜切分块 + 文字缩放 整块下压/上滑 见第 10 部分

实现:data-theme="fluent|classic" 只切换上表 5 项与第 10 部分的动画参数,不得切换字号/间距。

6.3 字体 token

Token ClassIsland 目标值(classisland family) ClassHelper 现状 迁移策略
--ci-font 'HarmonyOS Sans SC','Microsoft YaHei UI','Noto Sans CJK SC','Noto Sans SC','PingFang SC','Source Han Sans SC',SimHei,sans-serif(ClassIsland 字体栈,repos/ClassIsland/ClassIsland/Assets/Fonts/Fonts.axaml:3) -apple-system,'Segoe UI Variable Display','Segoe UI','Microsoft YaHei','PingFang SC','Helvetica Neue',sans-serif(classic-island.css:22-25) CHANGE:classisland family 用新栈;winisland 保留旧栈。不内置 TTF(许可与体积原因,HarmonyOS 字体许可见其 LICENSE.txt);若后续要内置,只加 @font-face 不改栈顺序
--ci-font-mono 数字等宽:font-variant-numeric: tabular-nums 已用(lesson-ending.css:11、LessonReminderContent.vue:226) KEEP,抽为工具类 .ci-num
--ci-fs-secondary 14px 由 --secondary: (fontSize/13)*14(CustomIslandRuntime.vue:119)、.pill-sub 0.74×、small 12px(ComponentNode.vue:436) CHANGE:classisland 固定 4 档由 fontSize 按 14/16/18/20 比例派生(见下)
--ci-fs-body 16px --island-font 基准 13(winisland) CHANGE
--ci-fs-emphasized 18px --emphasized 默认 20(ComponentNode.vue:441) CHANGE
--ci-fs-large 20px --large 默认 28(ComponentNode.vue:444) CHANGE
--ci-fw-regular/medium/bold 400 / 500 / 700 600(.pill-title、.row-title、.badge)、700(.title)、600(.solid-btn) CHANGE:classisland 行文字 500、标题 700;winisland 保留 600/700
行高 body 1.25、说明 1.4(【推断】按 ClassIsland 40px 行高与 16px 字号比例) 1.15(.pill-title/.pill-sub)、1.4(.content-text) CHANGE:classisland 1.25;winisland 保留 1.15
appearance.fontSize 11–20,classisland 下表示 body 基准(默认 16);winisland 保持 13(基准字号) 同一字段,默认 13 CHANGE 语义:按 family 解释;迁移时 winisland 用户不变,classisland 新装默认 16

classisland 四档派生公式(必须逐字实现):

ratio = appearance.fontSize / 16          // 默认 16 → 1.0
--ci-fs-secondary  = 14 * ratio
--ci-fs-body       = 16 * ratio
--ci-fs-emphasized = 18 * ratio
--ci-fs-large      = 20 * ratio

这与现有 custom 的派生方式同构(CustomIslandRuntime.vue:119-122 用 /13 基准),只是基准从 13 换成 16。winisland 继续使用 ISLAND_TYPE_SCALE(constants.ts:354-367):title 1.08 / body 0.95 / meta 0.78 / badge 0.76 / pillTitle 0.92 / pillSub 0.74。

6.4 圆角 token

Token classisland 目标 winisland 现状 custom 现状 迁移策略
--ci-radius-island 8px(0–20 可调;appearance.radius 复用该范围) 20px(8–32;squircle 基准半径) scheme.radius 默认 8 NEW;classisland 用标准 border-radius,不生成 squircle path
--ci-radius-row 8px(分体行与整体行同值) — — NEW
--ci-radius-card 8px(详情卡/气泡) 展开卡 = min(48×radius/20, w/2, h/2)(IslandApp.vue:209-211) min(appearance.radius, w/2, h/2)(CustomIslandRuntime.vue:129-133) NEW
--ci-radius-control 4px(按钮/列表行/徽标内控件) calc(font×0.5)(.list-row、.row-icon) 6px/8px 混用(custom-island.css:79,126) CHANGE:classisland 4px;winisland 保留比例派生
--ci-radius-pill 999px 999px(.pill-type/.badge/.ghost-btn) 7px 圆点 KEEP
--ci-radius-progress 0 1px 1px CHANGE(见 §6.2.5)
squircle 参数 不使用 指数 4.2、每角 14 段(squircle.ts:21,24) 同 winisland(共用 squirclePath) KEEP(仅 winisland/custom)
节点自定义圆角 默认 8(ClassIsland RadiusX) — islandNode().appearance.cornerRadius = 16(island-layout.ts:211) CHANGE:新默认 8;旧配置保留 16(无损迁移,不强制改)

6.5 几何与间距 token

Token classisland 目标 ClassHelper 现状/变量 迁移策略
行高 --ci-row-h 40px(fixed 气泡同样恒 40,不随内容增高) --resident-height: 40、min-height: max(40px, font×2.5)(custom-runtime.css:77,110,113);winisland 胶囊高 appearance.height 默认 44 NEW;classisland 行高固定 40(不随字号/内容变;正文最多 2 行、超出省略,不得撑高行——见第 7 部分)
行内边距 --ci-row-pad-x 12px --resident-padding/padding 0 12px(custom-runtime.css:76);winisland padding: 0 calc(font×0.78) NEW;classisland 固定 12
行最小宽 --ci-row-min-w 20px 无 NEW(对应 ClassIsland MinWidth=20,repos/ClassIsland/.../FluentTheme/Styles.axaml:109)
行间距 --ci-row-gap 5px(0–150 可调) scheme.spacing 默认 5(island-layout.ts:251)、.layout-rows gap: var(--custom-row-gap) KEEP 值,classisland 下用 --ci-row-gap 驱动 --custom-row-gap
组件间距 --ci-comp-gap 相邻 12px(单侧 6px) --row-gap 默认 12(scheme/row gap,island-layout.ts:169,236;custom-runtime.css:117) KEEP;classisland 组件 padding 单侧 = calc(var(--row-gap)/2) 即 6px
窗口/阴影留白 --ci-shadow-pad 10px ISLAND_SHADOW_PAD = 10(constants.ts:351) KEEP
自动布置容器 --ci-window-max 960×640(custom 常驻画布) windowBox():min(960, area.width) × min(640, max(60, area.height-marginY))(main/island.ts:1521-1528) KEEP
固定包围盒(fixed) 宽 = min(工作区宽, 960)、高 = 40 + 2×pad(pad 默认 20,覆盖阴影);行内容在窗口内按停靠对齐并在超出时截断/省略;消息到达不改变窗口尺寸 BBOX_SIZE 取最大形态 + 20(island.ts:108-112,1543-1547) CHANGE:classisland 下包围盒 = 行宽 + 2×10;行宽变化时窗口在隐藏帧改尺寸(保留"不可见时改大小"策略);单条气泡恒 40px、不因内容扩窗,maxCardHeight 只用于 custom 多条列表与强提醒窗口
胶囊几何 不适用 width 268 / height 44(constants.ts:266-267),ISLAND_SIZE_DELTA(:344-348)、ISLAND_SLIVER_WIDTH 6(:336)、sliver height max(12,min(28,h-22))(IslandApp.vue:196) KEEP(仅 winisland)
课程提醒条 高 40、宽 max-content、最小 300、最大 800 同(LessonReminderContent.vue:196-208) KEEP
课件卡 424×132 COURSEWARE_ISLAND_SIZE(shared/src/courseware.ts:4)、padding 17/24、radius 24(CoursewareTransfer.vue:71-72) KEEP(独立组件,不套 classisland 行)
编辑器预览行 40px(与真岛一致) .preview-row min-height:44px(island-editor.css:207)、padding 12、背景 #202024(:193-201) CHANGE:44→40,背景改 --ci-bg
图标/徽标 下课前秒数徽标 30×30 圆形 .lesson-ending-badge 30×30(lesson-ending.css:3-5) KEEP

6.6 阴影 token

Token classisland 目标 ClassHelper 现状 迁移策略
--ci-shadow-island 0 4px 8px 2px rgba(0,0,0,0.28)(即 ClassIsland 0 4 8 2 #48000000) 无(winisland 用 filter: drop-shadow(0 2px 3px rgba(0,0,0,.11)),classic-island.css:123,148-153) NEW:classisland 用 box-shadow 挂行容器(矩形 + 8px 圆角),禁止 filter: drop-shadow(会闪帧,classic-island.css:114-120)
--ci-shadow-mask 0 8px 32px rgba(0,0,0,0.4)(提醒时) 无 NEW
--ci-shadow-popup 0 8px 16px rgba(0,0,0,0.45)(深色) --ch-shadow-flyout(index.css:53) NEW(岛内弹层用)
--ci-shadow-custom 内描边 + 小投影 inset 0 0 0 1px #ffffff1e, 0 2px 3px #0000001c(custom-island.css:12-14) KEEP(custom 行);classisland family 下改 inset 0 0 0 1px var(--ci-border), 0 4px 8px 2px rgba(0,0,0,.28)
投影常驻原则 只改参数、不增删 filter/box-shadow 声明 已实现(classic-island.css:114-123) KEEP

6.7 透明与不透明度 token

Token classisland 目标 ClassHelper 现状/公式 迁移策略
--ci-opacity-bg 0.5(行背景不透明度,与 ClassIsland 默认 Opacity=0.5 一致) scheme.opacity 默认 0.5(island-layout.ts:248);winisland appearance.opacity 默认 1 KEEP 语义分开:背景不透明度(0.5)≠ 窗口不透明度(1)。classisland 下 --ci-opacity-bg=scheme.opacity;winisland 无背景层不透明度概念
窗口不透明度 1;阅读消息时强制 1 displayOpacity:readingMessage ? 1 : appearance.opacity(main/island.ts:233-240) KEEP
淡化比例 0.05 CLASSISLAND_ISLAND_METRICS.fadedOpacity = 0.05(shared/src/island-layout.ts:35),用于 main/island.ts:1441 KEEP
玻璃/毛玻璃 classisland 不使用 --wn-bg: rgba(32,32,36,0.804)(classic-island.css:73);不采样桌面模糊(文档 winisland-design-tokens.md:23) KEEP(winisland only)
消息实底 1.0 不透明 #202020 .message-surface(见 §6.2.1) KEEP
遮罩文字层 accent 上白字 100% 同 KEEP
禁用动画 不涉及透明度 `--island-anim: 0 1(IslandApp.vue:527);animations=false时所有transition/animation` 应为 0 或直接落位

6.8 层级(z-index)token

层 classisland 目标 ClassHelper 现状 迁移策略
岛根层 0 .island-root/.resident-window KEEP
形状/背景层 0 .shape(classic-island.css:103-124)、svg.shape KEEP
内容层 1 .content/.resident-page KEEP
遮罩层(消息/提醒) 2 .ci-mask(LessonReminderContent.vue:246-253)、.tint NEW 统一:--ci-z-mask:2
遮罩文字层 3 .ci-mask-title(:254-268) NEW:--ci-z-mask-text:3
弹层/下拉(岛内) 4 无 NEW
编辑器拖拽幽灵 10000 .drag-ghost z-index:10000(island-editor.css:331-334)、.ch-guide 10000 KEEP
强提醒窗口 独立 BrowserWindow,alwaysOnTop + 'screen-saver' 岛窗口同级别(island.ts:514) NEW:效果窗口层级与岛同级或更高,见第 8 部分
岛窗口层级 screen-saver(置顶) setAlwaysOnTop(alwaysOnTop,'screen-saver')(island.ts:514) KEEP

6.9 DPI 与缩放 token

规则 要求 证据
逻辑单位 岛内全部用 CSS px = Electron DIP;不手动乘 devicePixelRatio winisland-design-tokens.md:53、01-objective.md:86
阴影留白 10 CSS px(窗口比岛大 20) ISLAND_SHADOW_PAD = 10(constants.ts:351);island.ts:1621-1636
画布上限 custom 常驻窗 ≤ 960×640(或工作区高 − marginY) island.ts:1521-1528
尺寸最小钳制 自定义窗口 ≥ 40 宽/24 高;可用画布 ≥ 40 island.ts:389-390、CustomIslandRuntime.vue:262-263
方案缩放 scale 0.75–2.5(custom 常驻);实际 fit.scale = min(scheme.scale, availW/naturalW, availH/naturalH) island-layout.ts:371-375;CustomIslandRuntime.vue:72-80
字号范围 11–20(appearance.fontSize);classisland 派生 4 档后仍受「行高 40」约束,溢出用省略而非撑行 constants.ts:298;§7 组件规格
主窗口缩放 设置页/编辑器沿用 --ch-font-scale(1 / 关怀 1.15),不影响岛 renderer/styles/index.css:103,973-979
DPI 断言 截图/像素验收按 devicePixelRatio 换算基准图;125%/150% 下 ±1px 容差 01-objective.md:70
屏幕定位 全部用工作区 workArea + 锚点 + marginX/marginY;左右边距只在左/右停靠生效,主停靠中线不受影响 winisland-design-tokens.md:26;island.ts:1615-1648

6.10 深浅色 token

family='classisland' 必须同时支持深/浅色;family='winisland' 恒为深色(不随应用主题)。

Token 深色(默认) 浅色
--ci-bg #202020 @0.5 #F3F3F3 @0.5
--ci-border rgba(255,255,255,0.15) rgba(0,0,0,0.08)
--ci-text-1..5 白 100/85.5/77.3/52.9/36.5% 黑 100/85.5/77.3/52.9/36.5%
--ci-fill-1/2/3 白 6/10/14% 黑 4/8/12%
--ci-shadow-island 0 4px 8px 2px rgba(0,0,0,0.28) 0 4px 8px 2px rgba(0,0,0,0.18)
--ci-progress-track 白 15% 黑 12%
遮罩/强提醒 accent 底 + 白字 同(accent 底 + 白字)
切换时机 主窗口 theme 变化 → 经 preload 下发 island:appearance,岛根 data-color-scheme 变;颜色过渡 200ms 同
默认 colorScheme='system'(跟随应用;应用默认 Theme=2 深色,repos/ClassIsland/.../Settings.cs:32) —

实现纪律:html.dark 式全局覆盖不适用于岛(岛是独立渲染入口,island.html 不继承主窗口 html.dark)。必须显式传 colorScheme 并在岛根节点切 data-color-scheme。切换只改颜色,几何/动效 token 保持不变(docs/dev/motion.md:125-130)。

6.11 两套风格差异总表(默认选择点)

维度 classisland(默认) winisland(兼容)
fixed 形态 信息行(多行,行高恒 40,组件横排,底部 3px 进度条);单条气泡也在 40px 行内、正文最多 2 行省略 胶囊 268×44 + 展开详情卡(ISLAND_SIZE_DELTA 展开 156×186)
形状 标准 border-radius: 8px squircle 连续圆角(指数 4.2 / 14 段),胶囊满圆 h/2,展开 min(48r/20,…)
形变 CSS transition(行宽/透明度;行高恒 40 不变) SpringValue 帧积分(stiffness 0.10 / damping 0.68 / 换向 35% / 上限 20% / snap 0.05px)
内容切换 行内容淡入淡出 + 气泡两阶段 胶囊层 1-1.5p 退净、展开层 p² 进入
字号 14/16/18/20 四档(由 fontSize 派生) --island-font×排版系数(title 1.08/body .95/meta .78/…)
字重 500 正文 / 700 标题 600 标题 / 700 大标题
颜色 深/浅色主题 + Fluent/Classic 两套;背景 0.5 透明 纯黑/毛玻璃/主题色渐变三套;恒深色
状态色 accent 遮罩 + 状态描边 红/橙/accent 内层微染 + 状态描边
进度条 3px、直角 1–2px、1px 圆角
主题切换 Fluent↔Classic / 深↔浅 各 200ms 颜色过渡 无主题概念,style 切换即时
设置项 theme+colorScheme 新增;height/width/radius 退化为约束 height/width/radius/style/idleSliver 全部保留
默认动画 见第 10 部分 classisland 列 见第 10 部分 winisland 列

6.12 配置迁移与兼容策略(token 相关部分)

  1. config.json.island 增加 family、theme、colorScheme、winislandStyle(可选,默认为旧 style 的值)。
  2. 老用户升级:family 缺失 → 判为老用户 → family='winisland',winislandStyle = 旧 style,theme/colorScheme 写默认值但忽略。新安装(无 config 或 island 缺失)→ classisland。
  3. 旧 style='black'|'glass'|'tinted' 保留字段(winisland 使用),同时写入 winislandStyle;classisland family 下忽略 style。
  4. appearance.accent 保持用户值;仅当值等于旧默认 #0a84ff 且迁移到 classisland 时改 #00BFFF,并在迁移日志中记录。
  5. appearance.fontSize:winisland 保持 13;迁移到 classisland 时若仍为 13 → 改为 16(避免 4 档过小),并记录。
  6. scheme.radius/node.cornerRadius 不做强制改写(无损);classisland 渲染时若 radius 超过 20 则钳制到 20(appearance.radius 范围扩到 0–32 仅为兼容 winisland,classisland 侧钳制)。
  7. 迁移必须幂等:normalizeIslandAppearance() 对已有 family 的配置不得再次改值。

6.13 文件落点与实施顺序

步骤 文件 动作
1 packages/shared/src/types.ts 扩展 IslandAppearance(family/theme/colorScheme/winislandStyle);导出 ISLAND_FAMILIES、ISLAND_THEMES、ISLAND_COLOR_SCHEMES
2 packages/shared/src/constants.ts 新增 classisland 默认值常量(DEFAULT_CLASSISLAND_FONT=16、ISLAND_CLASSISLAND_RANGES);把 ISLAND_SIZE_DELTA 标注为 winisland-only
3 packages/shared/src/island-layout.ts CLASSISLAND_ISLAND_METRICS 增加 componentGap:12、progressHeight:3、rowMinWidth:20、shadowPad:10、字号四档保持现有值
4 新建 packages/desktop-client/src/island/tokens.css 写 --ci-* 静态 token(含 [data-family]、[data-theme]、[data-color-scheme] 组合选择器)
5 src/island/main.ts 在 applyMotionTokens() 前 import ./tokens.css
6 IslandApp.vue / CustomIslandRuntime.vue 根元素挂 data-family/data-theme/data-color-scheme;style 注入补 --island-row-*/--ci-fs-* 派生值
7 classic-island.css / custom-island.css / custom-runtime.css / lesson-ending.css 把硬编码颜色/尺寸改为引用 --ci-*(winisland 分支保留 --wn-*/--custom-*);[data-family='classisland'] 下覆盖布局
8 src/main/island.ts 下发 family/theme/colorScheme;窗口包围盒按 classisland 行宽计算(保留固定包围盒与"不可见时改尺寸")
9 设置页/编辑器 IslandSettingsView.vue 增加风格/主题/深浅色;island-editor.css 预览行 44→40、背景 token 化
10 docs/dev/winisland-design-tokens.md、docs/app/island.md 标注双风格与默认值,新增 docs/dev/island-refactor.md 的 token 章

6.14 常见错误与自查清单

实现者必须主动规避以下 13 类错误(每一条都对应真实会出现的问题):

  1. 把 --ci-* 写进主窗口的 renderer/styles/index.css:岛是独立渲染入口,主窗口的 :root 不共享;写错位置会导致岛内变量解析为空、整条 transition 简写失效(docs/dev/motion.md:58-59 已记录该坑)。
  2. 漏掉 island.html 的样式引入顺序:tokens.css 必须在 classic-island.css/custom-*.css 之前 import,否则后者的 var() 在解析时找不到值。
  3. 用 html.dark 给岛做浅色:岛不加载主窗口的 <html class="dark">,必须 data-color-scheme。
  4. 在 classisland family 里继续用 squircle:squirclePath() 只在 winisland/custom 分支调用;classisland 用 border-radius: var(--ci-radius-island)。
  5. 把 appearance.opacity 同时当作背景不透明度:窗口不透明度(0.4–1,阅读消息强制 1)与背景不透明度(默认 0.5)是两个 token;混用会导致"消息看不清"或"背景太实"。
  6. 给行容器加 backdrop-filter:会模糊整块矩形并破坏逐像素透明;毛玻璃只在 winisland 的 glass 里以半透明色模拟(docs/dev/winisland-design-tokens.md:23-24)。
  7. 用 filter: drop-shadow 做 classisland 行投影:会新建合成表面导致开合闪帧;classisland 用 box-shadow(classic-island.css:114-120)。
  8. 在 classisland 分支继续用 --wn-text-*:winisland 的 0.92/0.72/0.58 与 classisland 的 100/85.5/77.3/52.9/36.5 不是一套;混用会让同一行文字出现两种灰。
  9. 字号派生写成固定四档:classisland 四档必须由 fontSize/16 比例派生;写死 14/16/18/20 会让设置页字号滑杆失效。
  10. 把圆角钳制只写在 CSS:aspect 变化时 appearance.radius 可能大于行高一半,必须在 JS 里也钳制到 min(radius, w/2, h/2)(现有 IslandApp.vue:334-341 已做,classisland 分支同样要做)。
  11. 迁移时无条件改用户设置:只有"仍等于旧默认值"的 accent/fontSize 才允许改;用户调过的值必须原样保留(§6.12)。
  12. 忘记给编辑器预览同步 token:island-editor.css 的预览行、背景、圆角必须与真岛一致,否则"预览一个样、上屏另一个样"。
  13. 把 ClassHelper 旧展开卡行为带进 classisland fixed:fixed 行高恒 40,气泡/遮罩/进度条都在行内完成,正文最多 2 行省略;不得写「行高随内容增高」「行高 200ms 过渡」「为气泡扩窗」,maxCardHeight 只服务 custom 多条列表与强提醒窗口。

自查命令(无头执行,不启动 Electron):

# 1) 颜色字面量只允许出现在 token/兼容定义处
grep -rn "#[0-9a-fA-F]\{6\}" packages/desktop-client/src/island
# 2) 检查 classisland 分支没有误用 winisland 变量
grep -rn "\-\-wn-" packages/desktop-client/src/island/classic-island.css | head -40
# 3) token 是否齐全(每个 --ci- 使用处都能在 tokens.css 找到定义)
grep -rho "\-\-ci-[a-z0-9-]*" packages/desktop-client/src/island | sort -u > /tmp/used.txt
grep -ho "\-\-ci-[a-z0-9-]*" packages/desktop-client/src/island/tokens.css | sort -u > /tmp/def.txt
comm -23 /tmp/used.txt /tmp/def.txt
# 4) 确认 package.json 无新增依赖
git diff packages/*/package.json package.json

6.15 验收标准

  1. 单一来源:grep -rn "#[0-9a-fA-F]\{6\}" packages/desktop-client/src/island 只允许命中 tokens.css、classic-island.css 的 --wn-* 定义块、custom-*.css 的兼容块与 SVG 渐变;两个 .vue 文件内联样式里不得出现颜色字面量(--state-color 计算值除外)。
  2. 双风格隔离:切到 winisland 后,岛仍显示 268×44 胶囊 + squircle + 弹簧开合,像素 diff 与重构前一致(±1px);切到 classisland 后无 squircle path、无弹簧形变,行高 40/圆角 8/进度条 3px。
  3. 默认值:新安装 family='classisland'、theme='fluent'、colorScheme='system';旧 fixtures 升级后 family='winisland' 且 winislandStyle 等于旧 style。
  4. 字号:classisland 下把 fontSize 从 16 改到 20,四档变为 17.5/20/22.5/25,行高仍 40 且内容按第 7 部分规则省略;winisland 下改字号时全部文本按排版系数等比缩放。
  5. 颜色:深/浅色各截一张基准图,--ci-text-1..5、背景 0.5、描边 1px、进度条轨道/填充与 §6.2/§6.10 表逐项一致(颜色 ±2/255)。
  6. Fluent/Classic:切换主题后,行背景色、描边有无、阴影、圆角(Fluent 8 / Classic 0)按 §6.2.6 变化,字号与间距不变。
  7. 不透明度语义分离:把行背景不透明度调到 0.2、窗口不透明度调到 0.8,两者独立生效;打开一条消息时窗口不透明度强制 1(displayOpacity),关闭后回到 0.8。
  8. 层级:消息遮罩在内容之上、遮罩文字在遮罩之上;编辑器拖拽幽灵 z-index:10000 高于一切;岛窗口始终 screen-saver 层级。
  9. DPI:100%/125%/150%/200% 四档下,行高 40、圆角 8、字号逻辑值不变,无 0.5px 模糊;截图基准按 devicePixelRatio 换算后 ±1px。
  10. 迁移幂等:对同一份配置连续运行 normalizeIslandAppearance() 三次,输出字节级一致;旧字段 residentEnabled、style 仍在且语义不变。
  11. 兼容别名:--ci-lesson-* 重命名期间旧 --ci-duration 等变量仍生效(至少一个版本),组件视觉无变化。
  12. 无新依赖/无新框架:package.json 依赖零新增;token 只用 CSS 自定义属性,不引入 CSS-in-JS 或新 UI 库。
  13. fixed 气泡恒定 40px:构造超长正文(>500 字)的消息,行高仍为 40、正文最多 2 行且以省略号收尾,窗口 bounds 不因该消息变化;maxCardHeight 只在 custom 多条列表/强提醒窗口路径被读取。
  14. reduce-motion 口径:系统开启 prefers-reduced-motion 时,岛动画仍由 appearance.animations/speed 决定(默认不覆盖);仅当 P2 选项 respectReducedMotion=true 时按 animations=false 落位。

7. 岛结构与组件规格(ClassHelper 落地版)

本部分是「重构 ClassHelper 灵动岛」的视觉与组件规范。它不是新建工程的说明,而是对现有 packages/desktop-client 的改造指令:所有改动都必须落到既有文件(src/island/*、src/main/island.ts、packages/shared/src/*、src/renderer/views/settings/*)上,技术栈、构建方式、IPC 白名单、单窗口模型一律不变。

7.0 改造口径与文件级改动清单

总口径(必须遵守):

  1. 目标视觉默认改为 ClassIsland 规格(行高 40、行内边距 12、行距 5、圆角 8、背景不透明度 0.5、字号 14/16/18/20、进度条 3px、描边 1px、阴影 0 4 8 2 #48000000);ClassHelper 的 WinIsland 经典外观只作为 fixed 的兼容外观族保留(islandAppearance.family === 'winisland')。新安装默认 family='classisland';老用户升级保持 winisland,避免观感突变。
  2. family 只影响 fixed:classisland 下 fixed = ClassIsland 风格信息行(1 行 = 日期 + 课程表,行高 40、圆角 8、背景 0.5、底部 3px 进度条;消息在固定 40px 行内展开 ClassIsland 气泡,保留 ClassHelper 既有暂存/紧急/叫人/已读/去重规则);winisland 下 fixed = 旧"胶囊 + 详情卡"(零改动)。custom 始终使用 ClassIsland 规格,不提供 winisland 回退(custom 现有 rows/nodes 结构本就接近 ClassIsland,只做细化与像素对齐,见 7.2、7.4)。
  3. 不新增第二个窗口、不新增框架、不改服务端:LayoutIslandApp.vue 仍是唯一渲染入口,main/island.ts 仍是唯一窗口持有者(packages/desktop-client/src/island/LayoutIslandApp.vue:1-76)。
  4. 消息/提醒/课程提醒的业务语义(优先级、去重、上课暂存、紧急展开、已读回执)归第 8 部分;本部分只规定它们在岛上的容器、尺寸、颜色、动画。
  5. 所有像素值以本文与第 6 部分 tokens 为准;实现时不得再保留"ClassIsland 用 border-radius、ClassHelper 用 squircle"这种分歧——fixed 由 appearance.family 决定走哪套,custom 固定走 ClassIsland 套;代码里两套都在,但同一时刻只启用一套。

必须改动的文件(按改动类型):

文件 改动类型 内容
packages/shared/src/constants.ts 扩展 新增 ISLAND_FAMILIES + ISLAND_FAMILY_LABELS;DEFAULT_ISLAND_APPEARANCE 增加 family: 'classisland';新增 ClassIsland 规格常量(行高/内边距/行距/圆角/进度条/描边/阴影/字号)供渲染与主进程共用
packages/shared/src/island-layout.ts 扩展 组件 settings 增补 ClassIsland 语义字段(见 7.4);appearance 增补 customMargin(可选);normalizeIslandLayouts() 增加 family 迁移(老用户 → winisland)与规范化范围
packages/desktop-client/src/main/island.ts 改造 normalizeAppearance 接受 family;windowBox 的 fixed/custom 包围盒按 7.1.2 调整;updateResidentPointer 的淡出比例沿用 shared 常量;新增 island:report-layout-metrics 承载"内容最大高"
packages/desktop-client/src/island/IslandApp.vue 改造 增加 data-family="classisland" 分支:fixed 改为"信息行 + 行内气泡"(见 7.3.2);winisland 分支保持原样
packages/desktop-client/src/island/CustomIslandRuntime.vue 改造 custom 常驻卡改为 ClassIsland 行容器(背景/圆角/内边距/行距/进度条/淡出);classisland 分支的 border-radius 与行背景
packages/desktop-client/src/island/ComponentNode.vue 改造 组件内边距 6 → 0(改由行 padding 承担);字号 token 改为 ClassIsland 的 14/16/18/20;新增 schedule 的 ClassIsland 折叠规则与进度条 3px;新增 countdown 的 ClassIsland 文案
packages/desktop-client/src/island/WeatherBrief.vue 改造 按 ClassIsland 天气简报的图标/数值排版(见 7.4.3)
packages/desktop-client/src/island/classic-island.css 扩展 新增 [data-family='winisland'] 下的三套 data-style 变量组保持现值;classisland 的行/气泡样式写进 custom-runtime.css
packages/desktop-client/src/island/custom-runtime.css 改造 行/背景/进度条/淡出/编辑器选中框的 ClassIsland 规格
packages/desktop-client/src/island/custom-island.css 改造 .custom-card 的 ClassIsland 背景/圆角/行距变量
packages/desktop-client/src/island/LessonReminderContent.vue 复用 其 5 段斜切遮罩算法必须提取为共享遮罩组件,供通知遮罩复用(见 7.2.6)
packages/desktop-client/src/renderer/views/settings/IslandSettingsView.vue 改造 新增"外观族"下拉(绑定 appearance.family,默认 classisland,仅 layout='fixed' 可见);原"视觉风格"下拉仅在 fixed+winisland 下可见;custom 下两项都隐藏;圆角/行距/行高滑块范围按 ClassIsland 对齐
packages/desktop-client/src/renderer/views/settings/IslandEditorView.vue + island-editor.css + IslandEditorNode.vue 改造 选中框改 1.5px 虚线、行高与块级尺寸按 ClassIsland(见 7.5)
packages/desktop-client/src/renderer/components/IslandComponentSettings.vue 改造 组件设置项按 7.4 的字段表补充(隐藏规则、宽度、对齐、边距、字体覆盖)

不得改动:main.ts 的零依赖策略(packages/desktop-client/src/island/main.ts:6-11)、preload/island.ts 的白名单结构(:14-62)、spring.ts 的弹簧参数(spring.ts:26-30)、squircle.ts 的超椭圆参数(squircle.ts:21-24)。

7.1 岛窗口与几何

7.1.1 窗口创建参数(保持不变,只能增加)

沿用现有 BrowserWindow 配置(packages/desktop-client/src/main/island.ts:480-512),并逐条核对:

  • frame: false、transparent: true、resizable: false、movable: false、minimizable/maximizable/fullscreenable: false(:484-490)。
  • skipTaskbar: true、hasShadow: false(:491-492)——阴影由 CSS/SVG 画,不得用系统窗口阴影。
  • focusable: true 必须保留(:502):Windows 下关掉可激活性会丢 mousedown,岛点不动(:493-501 的实机结论)。"不抢焦点"靠 showInactive() + 收起时 blur() 实现(applyFocusable,:1663-1672)。
  • alwaysOnTop: true + setAlwaysOnTop(appearance.alwaysOnTop, 'screen-saver') + setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true })(:503, 514-515)。
  • type: 'toolbar'、setIgnoreMouseEvents(true, { forward: true })(:504, 527)。
  • webPreferences: contextIsolation: true、sandbox: true、backgroundThrottling: false、preload 指向 island.js(:505-511)。
  • 禁止:win.setBackgroundMaterial() / vibrancy / backgroundMaterial(:523-524 的历史结论;逐像素透明是硬约束)。禁止 setContentProtection(岛不是隐私窗口)。
  • 导航与新窗口一律拒绝(:516-522)。

7.1.2 窗口几何:fixed(两种外观族)/ custom

fixed 的两种口径(由 appearance.family 决定,不是同一个形态)

fixed 只表示"岛按经典交互工作(收到消息才出现)",它的外观由 family 决定:

A. family='classisland'(新安装默认):ClassIsland 风格信息行

  • 形态重定义:fixed 不再是小胶囊,而是一条 ClassIsland 风格的信息行——默认 1 行 = 日期 + 课程表,行高 40、圆角 8、背景不透明度 0.5、1px 描边、阴影 0 4 8 2 #48000000、底部 3px 进度条(课程进度 / 提醒倒计时)。
  • 隐藏语义不变:无消息时仍按现有 state.mode='hidden' 隐藏窗口(或按 idleSliver 留细缝,main/island.ts:1483-1496);消息到来才显示行。
  • 消息到来:在同一行内、行高 40px 的固定容器里渲染 ClassIsland 风格气泡——气泡不是"把行撑高",而是整块 40px 行换上遮罩层 + 内容层 + 进度层(见 7.2.8)。行高恒为 40,禁止临时增高到 64 等(--ci-island-height: 40px 不得被气泡改写)。保留 ClassHelper 既有业务规则(普通上课暂存、紧急立即展开、普通叫人可点、已读回执、按 id 去重),业务规则归第 8 部分,视觉归 7.2.8。
  • 窗口几何(预置固定包围盒,开合/消息变化时都不改窗口尺寸):
    • 宽 = min(目标显示器工作区宽, 960) DIP(960 为本实现的岛内容上限;行内容在窗口内按停靠位对齐、超出截断);不得按"行内容自然宽"逐次扩缩窗口;
    • 高 = 40 + 2 × 20 = 80 DIP(20 = ClassIsland Fluent 安全区/阴影余量;行高 40 恒不变,气泡、正文 2 行、进度条都在 40 行内完成);
    • 只在显示器/DPI 变化、停靠/偏移设置变化、窗口不可见时重算 setBounds;行内容变化(课程名、秒数、消息出现/收起)禁止改窗口尺寸(main/island.ts:1598-1612 的比较逻辑仅用于上述设置变化);
    • 可见行背景宽度 = 行内容自然宽(≤ 窗口宽),水平对齐按停靠位:左/中/右;行背景之外是透明余量,用户不可见。
  • 行宽/内容宽仍必须由渲染层测量后上报(reportSize/setHitRect,CustomIslandRuntime.vue:255、IslandApp.vue:421-454),用于行背景宽度、截断与命中矩形,不用于改窗口尺寸。
  • 气泡宽度上限 720【推断:按 1080p 下 ClassIsland 岛常用宽度取整】;超宽时课程表按 settings.count 截断(7.4.5),气泡正文最多 2 行(7.2.8)。
  • 鼠标交互:行上可点(展开/收起气泡、按钮);点行外空白 → 收起气泡(不改变 hidden 语义)。

B. family='winisland'(老用户升级默认):胶囊 + 详情卡

  • 保留现有全部几何:windowBox() 普通模式取 max(BBOX_SIZE, 当前形态 + 2*pad)(main/island.ts:1543-1548);触摸模式取"当前形态 + 2*pad"并贴合岛体(:1549-1552)。
  • 胶囊 = appearance.width × height(默认 268×44,constants.ts:266-267),展开/紧急/叫人 = 胶囊 + ISLAND_SIZE_DELTA(:344-348);岛本体尺寸由渲染层弹簧逼近(IslandApp.vue:216-220, 238-264)。
  • 圆角 = winisland 规格(胶囊 h/2、展开卡 min(48×radius/20, w/2, h/2)),背景 = 纯黑/毛玻璃/主题色渐变三选一。

custom(常驻):固定画布 + 内容自然测量(始终 ClassIsland 规格,与 family 无关)

  • 外观不走 family:custom 无论 family 是 classisland 还是 winisland,都按 7.2 的 ClassIsland 行/背景/进度条/遮罩规格渲染;family 只决定 fixed 用哪种外观(7.3.1)。设置页在 layout='custom' 时不得展示/生效 family 与 winisland 色调选项。
  • 现在的窗口画布已经是"固定画布":windowBox() 在 residentEnabled 时返回 min(960, area.width) × min(640, max(60, area.height - marginY))(main/island.ts:1522-1529),这与 ClassIsland 的固定包围盒思路一致,保留。
  • 目标态改动:画布高度上限改为"内容上限"而不是固定 640——渲染进程上报 reportLayoutMetrics({ contentMaxHeight })(新增 IPC),主进程取 min(area.height - marginY, contentMaxHeight + 2*pad);默认 contentMaxHeight = 常态行数 × (40 + 行距) + 24。理由:ClassIsland 的岛高度只由行决定(repos/ClassIsland/ClassIsland/MainWindow.axaml.cs:916),640 会让屏幕上方出现一块"看不见但可能吃掉点击"的余量(虽然命中已按 hitRect 控制,但窗口越大越依赖 hitRect 的准确性)。
  • available(渲染层可用尺寸)保持现值算法:innerWidth/innerHeight - 2*ISLAND_SHADOW_PAD - 4(CustomIslandRuntime.vue:260-266),最小值 40。
  • 内容缩放:fitted() 取 min(scheme.scale, available.width/natural.width, available.height/natural.height)(:72-80),保留;scheme.scale 范围 0.75–2.5(island-layout.ts:370-375)。
  • customSize 初值 424×80(main/island.ts:261)保留,仅作为首帧占位;首次 reportSize 后覆盖。

7.1.3 锚点、停靠与多显示器

必须沿用现有算法(main/island.ts:1331-1351),逐条核对:

  • 停靠位置 6 值:top-center / top-left / top-right / bottom-center / bottom-left / bottom-right(constants.ts:305-312)。
  • 垂直:vMode = position.startsWith('bottom') ? 'bottom' : 'top';vValue = bottom ? area.y + area.height - marginY : area.y + marginY(:1341-1342)。
  • 水平:hMode = endsWith('left') ? 'left' : endsWith('right') ? 'right' : 'center';hValue = left ? area.x + marginX : right ? area.x + area.width - marginX : round(area.x + area.width/2)(:1343-1349)。
  • 居中停靠不受 marginX 影响(docs/dev/winisland-design-tokens.md:26),只有左右停靠用水平边距。
  • 锚点全部取整(Math.round),避免 0.5px 抖动(:1325-1330)。
  • 窗口定位时窗口比岛大 ISLAND_SHADOW_PAD,因此要再偏移一个 pad,让岛的边/中心落在锚点上(windowBounds,:1615-1648):左停靠 x = hValue - pad、右停靠 x = hValue + pad - width、居中 x = hValue - round(width/2);y = bottom ? vValue + pad - height : vValue - pad。
  • 多显示器:targetDisplay() = followCursorDisplay ? getDisplayNearestPoint(cursor) : getPrimaryDisplay()(:1354-1358);换屏检测挂在既有 60ms 光标轮询上(syncAnchorDisplay,:1370-1379),不得新增定时器。
  • DPI:Electron 的 BrowserWindow.setBounds 用 DIP,渲染层 CSS px 与 DIP 一一对应,禁止在渲染层再乘 devicePixelRatio 或 zoomFactor;appearance 里的宽高/边距全部是 DIP(constants.ts:264-290 的注释口径)。
  • 目标态新增:ClassIsland 的"竖直安全区"(Fluent 20px / Classic 0,repos/ClassIsland/ClassIsland/App.Services.xaml.cs:314)在 ClassHelper 里以 P2 / 可选实现——仅当 appearance.family === 'classisland' 且设置项开启时,把锚点向屏幕内侧再推 safe 像素;默认 0,不得改变现有视觉位置。

7.1.4 点击穿透、命中与触摸

这是岛最容易出 regress 的部分,必须逐条保持:

  1. 窗口默认 setIgnoreMouseEvents(true, { forward: true })(main/island.ts:527)。
  2. 渲染层只上报"进入岛体"(setInteractive(true)),不上报关闭;关闭由主进程按真实光标轮询决定(IslandApp.vue:456-487 的整段说明)。理由:两边都下发会让窗口在"接收/穿透"之间每几毫秒翻转,表现为"点了没反应"。
  3. 命中矩形 hitRect:渲染层用 .island-card/.resident-card 的 getBoundingClientRect() 上报(IslandApp.vue:421-454、CustomIslandRuntime.vue:200-213),限流 50ms 且尾随发送一次;主进程按 60ms 兜底校正(main/island.ts:1824-1836)。
  4. 触摸模式:渲染层按 navigator.maxTouchPoints > 0 上报 setTouchMode(true)(IslandApp.vue:546-549),主进程切到"窗口贴合岛体 + 始终接收输入"(main/island.ts:1891-1899, 1795-1807)。触摸模式的窗口只增不减,收小要等窗口隐藏后再做(syncTouchLayout,:1560-1578)。
  5. 失焦宽限:BLUR_GRACE_MS = 500、触摸/编辑态 BLUR_GRACE_TOUCH_MS = 1200(main/island.ts:2287, 2297);blur 时若指针仍在岛上则忽略(:565-578)。
  6. 常驻模式的指针避让淡化:residentFaded 由 updateResidentPointer() 计算(:1851-1877),触发条件包含"非编辑、非课程提醒、非展开、非触摸操作后 1s、未悬停铃铛",淡化比例 = CLASSISLAND_ISLAND_METRICS.fadedOpacity = 0.05(island-layout.ts:35)。
  7. 编辑预览时不创建额外桌面窗口(docs/app/island.md:47),编辑态由 island:set-editing 控制 setInteractive(true)(main/island.ts:1430-1432)。

7.1.5 窗口护栏(不得删减)

  • 渲染进程心跳:渲染层每 5s alive()(LayoutIslandApp.vue:36),主进程 RENDERER_ALIVE_TIMEOUT_MS = 20_000(main/island.ts:2315),超时隐藏并重建窗口,重建限流 3 次 / 10 分钟(:2317-2318)。
  • 淡入淡出:步进 0.16 / 0.2、16ms 一步(fadeIn/fadeOut,:1674-1710);appearance.animations === false 时直接落位。
  • 窗口只在"尺寸/位置设置变化"或"不可见"时重排(applyWindowLayout 前先比较 4 个值,:1598-1612);禁止在开合动画中改窗口几何(:1477-1479 的注释)。
  • suspended(被 custom 或课件覆盖时)必须停掉经典岛的测量与命中(IslandApp.vue:238-246 的 syncTargets 早退)。

7.2 行、背景、进度条、遮罩与淡出(ClassIsland 规格:custom 恒定 / fixed 默认)

7.2.1 行容器 DOM(目标态)

CustomIslandRuntime.vue 的常态页与展开页都必须渲染成下面这棵树(现状见 :396-504,需按此重排):

<div class="resident-window" data-anchor="left|center|right" data-bottom="true|false" data-family="classisland" :data-style="appearance.style">
  <section class="custom-card resident-card island-card">
    <svg class="shape" v-if="appearance.family === 'winisland'"><path :d="path" /></svg>
    <!-- classisland:不用 squircle 形状层,圆角由 CSS border-radius 承担(见 7.2.2) -->
    <span class="tint" aria-hidden="true"></span>
    <div class="resident-page resident-compact-page">
      <div class="resident-source resident-compact">
        <div class="layout-rows" style="gap: var(--ci-line-gap)">
          <section class="layout-row" data-row="main">
            <div class="row-content">
              <div class="row-components" style="gap: var(--ci-component-gap)">
                <ComponentNode … />
              </div>
            </div>
          </section>
        </div>
        <button class="resident-bell">…</button>       <!-- 消息入口,见 7.4.12 -->
      </div>
    </div>
    <div class="resident-page resident-detail-page">…</div>
    <div class="resident-page resident-lesson-page">…</div>
  </section>
</div>

强制要求:

  • 行容器类名保持 .layout-row / .row-content / .row-components(现有 CSS 已依赖,custom-island.css:145-164、custom-runtime.css:98-121),不要改名;新增的规格通过 CSS 变量注入。
  • .layout-rows 的 gap 从 scheme.spacing 改为 var(--ci-line-gap)(默认 5px,island-layout.ts:34),scheme.spacing 仅作为覆盖值(编辑器"行间距"仍是它)。
  • 组件列表的 gap 从 row.gap 改为 var(--ci-component-gap)(默认 12px)。语义映射:ClassIsland 的相邻组件间距 = 两个 Margin="6 0" 之和 = 12(repos/ClassIsland/ClassIsland/MainWindow.axaml:396)。因此 row.gap 的默认值应从 12 改为 12 并作为覆盖(island-layout.ts:343 默认 8 → 改为 12),范围 0–64 保留。

7.2.2 行的像素规格(ClassIsland 规格)

必须写死为 CSS 变量并在两个 CSS 文件里引用(custom-runtime.css 为主):

.resident-window {
  --ci-line-height: 40px;        /* ClassIsland IslandContainerHeight,MainWindowLine.axaml:16 */
  --ci-line-pad-x: 12px;         /* GridContentRoot Margin="12 0",FluentTheme/Styles.axaml:110 */
  --ci-line-gap: 5px;            /* MainWindowLineVerticalMargin 默认 5,Models/Settings.cs:219 */
  --ci-component-gap: 12px;      /* 组件 Margin="6 0" ×2,MainWindow.axaml:396 */
  --ci-radius-island: 8px;       /* RadiusX 默认 8,Models/Settings.cs:183 */
  --ci-island-opacity: 0.5;      /* Opacity 默认 0.5,Models/Settings.cs:64 */
  --ci-line-opacity: 1;          /* 行整体不透明度默认 1.0,MainWindowLineSettings.cs:42 */
  --ci-stroke-island-w: 1px;     /* line-background BorderThickness=1,FluentTheme/Styles.axaml:47 */
  --ci-shadow-island: 0 4px 8px 2px #48000000;   /* FluentTheme/Styles.axaml:49 */
  --ci-progress-height: 3px;     /* 本轮裁决,所有进度条共用 */
  --ci-fade-ratio: 0.05;         /* CLASSISLAND_ISLAND_METRICS.fadedOpacity,island-layout.ts:35 */
}

行本体:

.layout-row {
  min-height: var(--ci-line-height);
  padding: 0 var(--ci-line-pad-x);       /* ClassIsland 的 12px 内边距搬到行上 */
  box-sizing: border-box;
  display: flex;
  align-items: center;
  opacity: var(--ci-line-opacity);
}
.row-components {
  display: flex;
  align-items: center;
  gap: var(--ci-component-gap);
  min-height: var(--ci-line-height);
}

方案级覆盖:第 5、13 部分在 scheme 上新增 rowHeight / componentGap / showProgress 三个字段(默认 40 / 12 / true)。渲染时按"scheme 覆盖 > 外观族默认"写入 CSS 变量:

.resident-card {
  --ci-line-height: v-bind('scheme.rowHeight ? scheme.rowHeight + "px" : "40px"');
  --ci-component-gap: v-bind('scheme.componentGap + "px"');
}

showProgress=false 时所有行内进度条(课程进度、倒计时进度)隐藏;行距仍是 --ci-line-gap(默认 5,来自 scheme.spacing,island-layout.ts:34)。

背景(两种形态都要):

/* 整块岛背景(separated=false) */
.resident-card {
  background: var(--ci-bg-secondary);                    /* SolidBackgroundFillColorSecondaryBrush */
  border: var(--ci-stroke-island-w) solid var(--ci-stroke-island);
  border-radius: var(--ci-radius-island);
  box-shadow: var(--ci-shadow-island);
  opacity: var(--ci-island-opacity);                     /* 只作用背景,不作用文字 */
}
/* 分体(scheme.separated=true):每行一个独立胶囊 */
.resident-card.resident-separated { background: transparent; box-shadow: none; border: 0; padding: 0; }
.resident-separated .layout-row {
  background: var(--ci-bg-secondary);
  border: var(--ci-stroke-island-w) solid var(--ci-stroke-island);
  border-radius: var(--ci-radius-island);
  box-shadow: var(--ci-shadow-island);
  opacity: var(--ci-island-opacity);
  padding: 0 var(--ci-line-pad-x);
}

硬性要求:背景不透明度只允许作用在背景层;文字/图标必须 100% 不透明(ClassIsland 把 Opacity 绑在 Border.line-background 上,repos/ClassIsland/ClassIsland/XamlThemes/FluentTheme/Styles.axaml:44)。现有实现里卡片 opacity 由主进程 win.setOpacity() 统一控制(main/island.ts:1440-1442),不得把背景不透明度也塞进窗口 opacity——必须拆成"窗口 opacity(用户可调 0.4–1)+ 背景层 alpha(0.5)"两层。

7.2.3 组件内边距的归零

现在 ComponentNode 自身有 padding: 6px(ComponentNode.vue:295-301),行 .row-components 的 gap 又是 row.gap,两者叠加会让实际间距变成 gap + 12。ClassIsland 是"组件 0 内边距 + 行 12px 内边距 + 组件 6/6 外边距"。

改造:

.component-node { padding: 0; }                 /* 由行 padding 承担左右留白 */
.row-components { gap: var(--ci-component-gap); }  /* 12px */

同时在 ComponentNode 上移除 overflow: hidden(ComponentNode.vue:296)→ 改为 overflow: visible,否则倒计时进度环、天气预警告牌会被裁掉;裁切交给行(clip-path 或 overflow: hidden 挂在 .layout-row)。

7.2.4 进度条

三条进度条统一 3px、border-radius: 1.5px、贴底、transform-origin: left:

位置 现状 目标
课程行内进度 .lesson-progress { height: 2px; background: #ffffff24 }(ComponentNode.vue:348-355) height: var(--ci-progress-height);底色 rgba(255,255,255,0.08);填充 var(--ci-accent)
课程提醒卡进度 .ci-lesson-progress { height: 2px }(LessonReminderContent.vue:307-316) 同上,高度 3px
通知遮罩进度(新增) 无 同 7.2.6,贴在遮罩层底部

过渡:课程进度 transition: transform 1s linear(ComponentNode.vue:361)保留;提醒进度 50ms linear(LessonReminderContent.vue:315)保留;通知倒计时进度 100ms linear(prefers-reduced-motion 时 0)。

7.2.5 淡出与透明度

  • 整窗淡入淡出:主进程步进 0.16/0.2 × 16ms(main/island.ts:1674-1710)保留。
  • 指针避让淡化:residentFaded → win.setOpacity(displayOpacity × 0.05)(:1440-1442 + island-layout.ts:35)保留;必须保证展开态、编辑态、课程提醒期间不淡化(:1864-1873)。
  • 行级淡出(新增,P2):ClassIsland 的 IsLineFaded 是 0.05(repos/ClassIsland/ClassIsland/Controls/MainWindowLine.axaml:73-75);ClassHelper 已有"整窗淡化",默认不叠加行级淡化,仅当 pointerFadeReversed 语义需要时用行级 opacity 过渡 100ms 实现。

7.2.6 斜切遮罩(通知遮罩必须复用课程提醒的实现)

ClassIsland 的遮罩是 5 段斜切,参数为:区段权重 [0, 0.1, 0.2, 0.4, 0.2, 0.1]、60° 斜面、区段 360ms、错峰 120ms、开 QuarticEaseOut / 关 QuadraticEaseIn、文字 delay 260ms + 250ms 淡入 + scale 1.1→1.0 / 750ms(repos/ClassIsland/ClassIsland/Controls/SlantedMaskControl.cs:177-200, 284-308;行模板 XamlThemes/FluentTheme/Styles.axaml:329-367)。

现状:这套算法已经存在于 LessonReminderContent.vue:59-102,并且参数完全对得上——edges = [0, 0.1, 0.3, 0.7, 0.9, 1](等价权重 0/0.1/0.2/0.4/0.2/0.1)、offset = tan(30°) × 40、delays = open ? [0,90,120,90,0] : [120,90,0,90,120]、ease = 1-(1-p)^4 / p²、总时长 480ms、兜底 600ms。

改造要求:

  1. 把 LessonReminderContent.vue 的遮罩部分抽成 packages/desktop-client/src/island/SlantedMask.vue(props:width、height、open: boolean、accent: string、motion: boolean、speed: number),LessonReminderContent 与新的通知遮罩共用。
  2. 通知遮罩的 DOM 必须与 ClassIsland 对齐:
<div class="ci-mask-layer" :class="{ open }">
  <svg class="ci-mask" :viewBox="`0 0 ${width} 40`" preserveAspectRatio="none">
    <polygon v-for="(p, i) in polygons" :key="i" :points="p" />
  </svg>
  <div class="ci-mask-title" :class="{ 'ci-title-in': open }">…通知标题…</div>
</div>
  1. 遮罩填充色 = var(--ci-accent-fill)(ClassIsland 用 AccentFillColorDefaultBrush);文字色 = var(--ci-text-on-accent)。
  2. prefers-reduced-motion 或 appearance.animations === false 时直接落位(regions 全部 0/1),不做逐帧插值(LessonReminderContent.vue:77-80 已有该分支,保留)。
  3. 不得给遮罩层加 filter: blur() 或 backdrop-filter(透明窗口下不可靠)。

7.2.7 行/组件隐藏规则

ClassHelper 现有的 advanced.hideRules 只有三值:in-class / no-message / no-class(island-layout.ts:101-106,判定在 ComponentNode.vue:108-120)。目标是 ClassIsland 的规则集(HideMode 基本/高级、HideOnClass、HideOnMaxWindow、HideOnFullscreen、规则集编辑器,repos/ClassIsland/ClassIsland/Models/Settings.cs:185, 418-505)。

本轮落地口径:

  • 保留现有三值并保证语义不变(in-class ↔ 上课隐藏、no-message ↔ 无消息隐藏、no-class ↔ 无课程隐藏)。
  • 新增(v2 schema,见第 5 部分):hideRules 允许字符串扩展 'max-window' | 'fullscreen',判定数据由主进程通过 IslandState 新增字段 foreground: { maximized: boolean; fullscreen: boolean } 下发(主进程已有前台窗口检测能力,repos/ClassIsland/ClassIsland/MainWindow.axaml.cs:444-457 为语义参考)。
  • 行级隐藏(ClassIsland 的 MainWindowLineSettings.HideOnRule)本轮只做"行整体可隐藏",P1;规则集编辑器(RulesetControl)不在本轮范围,P2。
  • 编辑器必须能编辑 hideRules(现有三值)与 align/width/maxWidth;新增两项后要同步 IslandComponentSettings.vue 与 normalizeIslandLayouts 的白名单(island-layout.ts:321-328)。

7.2.8 消息气泡(family='classisland' 的 fixed 模式,40px 行内)

硬性约束:气泡不得增高行、不得增高窗口。 行高恒 40px(--ci-island-height: 40px 不得被气泡改写),气泡的遮罩、内容、进度条全部在这个 40px 容器内完成。

DOM(叠在行内容之上,position: absolute; inset: 0):

<div class="ci-row-shell">                       <!-- height: 40px; position: relative -->
  <div class="layout-row">…常态行组件…</div>       <!-- 气泡展开时 opacity → 0(不卸载,保留宽度测量) -->
  <div class="ci-row-bubble" v-if="mode==='expanded'">   <!-- inset: 0; border-radius: 8px; overflow: hidden -->
    <SlantedMask class="ci-bubble-mask" :open="true" :width="rowWidth" :height="40" />
    <div class="ci-bubble-row">                   <!-- display:flex; align-items:center; height:40px; padding: 0 12px -->
      <span class="ci-bubble-icon">…</span>       <!-- 16×16 线性图标 -->
      <span class="ci-bubble-badge">紧急</span>    <!-- 可选;高 18、圆角 9、字号 11 -->
      <div class="ci-bubble-text">                <!-- min-width:0; flex:1; display:flex; flex-direction:column; justify-content:center -->
        <span class="ci-bubble-title">…单行省略…</span>
        <span class="ci-bubble-body">…最多 2 行…</span>
      </div>
      <span class="ci-bubble-actions">…按钮…</span>
    </div>
    <div class="ci-bubble-progress" :style="{ transform: `scaleX(${leftProgress})` }" />
  </div>
</div>

CSS 规格:

.ci-row-bubble { position: absolute; inset: 0; border-radius: var(--ci-radius-island); overflow: hidden; }
.ci-bubble-text { min-width: 0; flex: 1 1 auto; display: flex; flex-direction: column; gap: 0; }
.ci-bubble-title {
  font-size: var(--ci-fs-body);          /* 16px */
  font-weight: 600; line-height: 20px;   /* 40px 行内两行:20 + 18 */
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ci-bubble-body {
  font-size: var(--ci-fs-secondary);     /* 14px */
  line-height: 18px; color: var(--ci-text-2);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;                       /* 正文最多 2 行,超出省略 */
}
.ci-bubble-progress {
  position: absolute; left: 0; bottom: 0; width: 100%; height: var(--ci-progress-height);
  background: var(--ci-accent-fill); transform-origin: left;
  transition: transform 50ms linear;
}

行内的排版预算:标题 1 行(20px)+ 正文 2 行(18×2=36)已超过 40px,因此实际规则是——标题占用 1 行时正文最多 1 行;无标题时正文最多 2 行;两种都靠 -webkit-line-clamp + 固定行高把内容锁在 40px 内,不得用 overflow: visible 让文字溢出到行外。

动画(严格按 ClassIsland 原版参数):

阶段 属性 时长 / 延迟 缓动
遮罩斜掠(开) 5 段 scaleX 0→1 每段 360ms,错峰 0/90/90/120/120ms cubic-bezier(0.165,0.84,0.44,1)(QuarticEaseOut)
遮罩斜掠(关) 5 段 scaleX 1→0 每段 360ms,反向错峰 cubic-bezier(0.55,0.085,0.68,0.53)(QuadraticEaseIn)
气泡文字淡入 opacity 0→1 250ms,delay 260ms cubic-bezier(0.25,1,0.5,1)
气泡文字缩放 scale 1.1→1.0 750ms,delay 260ms cubic-bezier(0.25,1,0.5,1)
气泡文字淡出 opacity 1→0 200ms 线性
文字缩回 scale 1.0→1.1 200ms cubic-bezier(0.5,0,0.75,0)
常态行淡出 opacity 1→0 200ms SineEaseIn(同 ClassIsland 的 GridContentRoot 过渡)
进度条 scaleX 50ms 线性

以上参数与 ClassIsland 完全一致(repos/ClassIsland/ClassIsland/Controls/SlantedMaskControl.cs:177-200;XamlThemes/FluentTheme/Styles.axaml:329-367)。

Classic 主题(appearance.theme='classic'):遮罩改为 translateY(-50px → 0)、200ms QuadraticEaseIn;退出 0 → +50px、delay 100ms、200ms QuadraticEaseOut;文字 200ms delay / 100ms(XamlThemes/ClassicTheme/Styles.axaml:265-331)。

禁止:islandListLayout() 的高度再参与 fixed 气泡(它只用于 custom 详情页与 winisland 详情卡);把气泡做成"行下方新增一块 64px 高的区域"。

7.3 外观族:family='classisland'(默认)与 family='winisland'(兼容,仅 fixed)

适用范围(硬性):appearance.family 只影响 fixed 模式的外观;custom 始终按 ClassIsland 规格渲染(7.2、7.4),不提供 winisland 回退。

7.3.1 数据与开关(字段名以第 5 部分为准)

新增顶层字段 appearance.family: 'classisland' | 'winisland',与既有 appearance.style(black/glass/tinted 色调)不是一个概念:

  • family 决定 fixed 长什么样、怎么排(信息行 vs 胶囊、圆角 8 vs squircle);对 custom 无意义;
  • style 只在 fixed + family='winisland' 下有意义(纯黑/毛玻璃/主题色渐变),其余情况忽略。

落地规则:

  1. packages/shared/src/constants.ts 新增 ISLAND_FAMILIES = ['classisland','winisland'] as const + ISLAND_FAMILY_LABELS(classisland: 'ClassIsland(默认)'、winisland: 'WinIsland 经典');ISLAND_STYLES(:324)与 ISLAND_STYLE_LABELS(:326-330)保持原样不动。
  2. DEFAULT_ISLAND_APPEARANCE 增加 family: 'classisland'(:265-290)——这是"新安装默认"。
  3. normalizeAppearance() 接受 family 白名单(main/island.ts:2244-2264),非法值回退 'classisland'。
  4. 升级迁移(必须与第 5 部分一致):旧配置没有 family 字段 → 迁移时写入 family: 'winisland'(老用户观感不变);新安装 → 'classisland'。迁移函数放在 packages/shared/src/island-layout.ts 的 normalizeIslandLayouts() 里,与 version 一并处理。
  5. 设置页拆分(IslandSettingsView.vue:291-305 现绑定 style):
    • "外观族"下拉绑定 appearance.family,仅在 layout='fixed' 时可见;
    • "视觉风格/色调"下拉仅在 layout='fixed' && family='winisland' 时可见;
    • layout='custom' 时两者都隐藏(custom 恒 ClassIsland 规格),只保留圆角/行高/行距/字号等 Scheme 级设置;
    • 切换必须即时生效并可回退(走既有 appearance 通道,preload/island.ts:48-49)。
  6. 主题轴(appearance.theme: 'fluent' | 'classic')由第 6 部分定义;本部分只要求"行背景色/描边/阴影/遮罩动画参数随 theme 切换",落点在 custom-runtime.css 的 [data-theme='classic'] 覆盖组(custom 与 fixed-classisland 共用)。

7.3.2 family='classisland'(默认,必须实现)

fixed 模式下(重定义后)= 信息行 + 行内气泡:

维度 规格 落点
形态 1 行信息行(默认 日期 + 课程表),不是胶囊 IslandApp.vue 新增 classisland 模板分支(见下方改造点)
行高 40 --ci-line-height
行内边距 左右 12 --ci-line-pad-x
圆角 8(矩形圆角,不用 squircle) border-radius: var(--ci-radius-island)
背景 不透明度 0.5 + 1px 描边 + 阴影 0 4 8 2 #48000000 7.2.2
进度条 底部 3px(课程进度 / 提醒倒计时) 7.2.4
字号 14 / 16 / 18 / 20 --ci-fs-*
消息 在固定 40px 行内渲染 ClassIsland 气泡:斜切遮罩(5 段)+ 标题 + 正文(最多 2 行省略)+ 底部 3px 倒计时进度条;不增高行、不增高窗口 7.2.8 + 第 8 部分
空闲 保持 hidden / idleSliver 语义(不改成常驻) main/island.ts:1483-1496
形变 无高度形变:行高恒 40;只有"行 ↔ 气泡"的内容层交叉淡入/遮罩斜掠;radius 恒 8 IslandApp.vue:216-264 的 classisland 分支

custom 模式下 = 现有 schemes/rows/nodes 模型升级,始终 ClassIsland 规格(7.2、7.4 全部适用),与 family 无关;课程提醒接管规则见 7.3.3。

关键改造点(IslandApp.vue):现有 fixed 模板是"胶囊层 + 展开层 + 课程层"(:606-678),胶囊内容是"图标 + 标题 + 副标题 + chevron"(:616-659)。改造要求:

<!-- family=classisland:fixed 信息行(固定 40px 高,气泡在行内叠加) -->
<div class="ci-row-shell" style="height: 40px">
  <div class="layout-row"><div class="row-components">
    <ComponentNode v-for="node in fixedRowNodes" … />
  </div></div>
  <div v-if="mode==='expanded'" class="ci-row-bubble" role="region">
    <SlantedMask class="ci-row-mask" :open="true" :width="rowWidth" :height="40" />
    <div class="ci-bubble-content">…标题 / 正文(2 行省略)/ 操作…</div>
    <div class="ci-bubble-progress" :style="{ transform: `scaleX(${leftProgress})` }" />
  </div>
</div>
  • fixedRowNodes = 默认 [islandNode('date','fixed-date'), islandNode('schedule','fixed-schedule')];来源可以新建一个内置 fixed 方案(与 custom 的 scheme 结构复用),不要把节点写死在模板里。
  • targetSize() 增加 classisland 分支:高度恒 40,{ width: mode==='expanded' ? max(rowNaturalWidth + 24, 420) : rowNaturalWidth + 24, height: 40 };禁止出现 40 + bubbleHeight 之类的增高分支。
  • 展开/收起不做高度弹簧:mode 切换时只播"行内容 → 气泡"的遮罩斜掠与内容交叉淡入(7.2.8 的 360/120ms 与 260/250ms 参数);行高、窗口高全程不动。
  • family='winisland' 时走原模板,逻辑零改动。

7.3.3 fixed 与 custom 差异总表(含课程提醒接管)

先给出三种组合的行为差异总表(后续 7.3.4 细化 winisland、7.3.5 说明共享约束),不得自行发明第四种组合:

维度 fixed + family='classisland'(默认) fixed + family='winisland'(兼容) custom(常驻,恒 ClassIsland 规格,family 不适用)
常态 隐藏(或细缝);消息到来才显示 1 行信息行 隐藏;消息到来弹胶囊 常驻方案 rows
行高 恒 40 胶囊 44 / 展开卡按内容 scheme.rowHeight(默认 40)
消息展示 固定 40px 行内气泡(遮罩+标题+正文 2 行+3px 进度),行不增高 胶囊 → 详情卡(高度按 islandListLayout),窗口高度随形态变 铃铛 + 详情页(resident-detail-page),不改成行内气泡
课程提醒 显示完整课程提醒条:LessonReminderContent 渲染在 40px 行内(准备/上课/课间/下课前) 显示课程提醒条(现有 ci-fixed-lesson 覆盖) 不因 in-class/prepare/break 临时接管整岛;课程进度由 schedule 组件展示;ending 只在课程名右侧显示蓝色圆形秒数徽标(lesson-ending.css 的 30×30);不新增"临时接管一行"行为
课程进度 行底 3px 进度条(当前课程) 课程提醒条底部 2px(改 3px,7.2.4) schedule 组件内 3px 进度条
多消息 第 8 部分规则;气泡内最多 2 行正文,超出省略 列表模式(islandListLayout) 详情页 ClassicNotifications list-mode
空闲交互 鼠标移入不淡化(隐藏态);显示时按命中接收 指针避让淡化 0.05 指针避让淡化 0.05(updateResidentPointer)
窗口几何 宽 = min(工作区宽, 960),高恒 40+2×pad(pad 默认 20) 固定包围盒(最大形态 + 2×pad) 固定画布(内容上限 + 2×pad)

custom 课程提醒的落地改动(必须做,且只做这些):

  • CustomIslandRuntime.vue:31-33 的 lesson 计算改为 恒 null(不再让 prepare/on-class/break/in-class 触发 resident-lesson 页),并从模板移除/停用 resident-lesson-page(:505-524)——custom 不再整岛接管。
  • 保留 ending 徽标:ComponentNode.vue 的 .lesson-ending-badge(:261-266,样式 lesson-ending.css:1-16)继续在课程名右侧显示 -{remaining},尺寸 30×30、#0a84ff 底、白字 12px/600、等宽数字。
  • 不得新增 "custom 下临时接管一行显示课程提醒" 的行为(这是本轮明确排除的范围)。

7.3.4 family='winisland'(兼容,仅 fixed,保持现有外观)

  • 几何:胶囊 width × height(默认 268×44)、圆角 = min(appearance.radius, h/2)(IslandApp.vue:206-212)、展开卡半径 = min(48 × radius/20, w/2, h/2)。
  • 形状:squircle 超椭圆(squircle.ts:21-24,指数 4.2、每角 14 段)。
  • 底色:appearance.style 的 black / glass / tinted 三套 CSS 变量组(classic-island.css:60-86),用 data-style 属性表达(现有模板已写 :data-style="appearance.style",IslandApp.vue:587、CustomIslandRuntime.vue:348)。
  • 排版:--island-font 基础字号 + 系数(ISLAND_TYPE_SCALE,constants.ts:354-367),胶囊内边距 0 font*0.78、gap font*0.62(classic-island.css:207-216)。
  • 阴影:只在展开态 0 2px 3px rgba(0,0,0,.11),且声明常驻只改参数(classic-island.css:110-124, 148-153)。
  • 交互:胶囊点击展开、点击卡片空白收起、失焦宽限 500ms——全部保持现状。

7.3.5 三种组合的共享约束

无论 layout 与 family 如何组合都必须满足:

  • 命中/穿透/触摸/失焦/心跳逻辑完全不变(7.1.4、7.1.5):family 只改 fixed 的渲染,不改输入、不影响 custom。
  • 编辑器与设置页共用同一份 islandLayout 数据;family 不改变数据模型(除 7.4 的字段增补);custom 的 scheme/rows/nodes 在两套 family 下渲染完全一致。
  • 所有组合的 ComponentNode 是同一个组件,规格共用 7.4;差异只在行容器、背景层与气泡容器。
  • prefers-reduced-motion 与应用内"动画"开关同时生效;关闭动画时所有弹簧直接 snap()(CustomIslandRuntime.vue:148-151、IslandApp.vue:249-256)。
  • 可回退:feature flag(family)必须在 M2/M3 阶段即可一键切回旧 fixed 渲染;custom 无回退档(始终 ClassIsland 规格),验收时 fixed×2 family 与 custom 三条路径都要跑(第 11、12 部分)。

7.4 组件逐个规格(11 内置 + 3 消息入口 + 课件覆盖)

统一约定(所有组件适用):

  • 契约字段:id / kind / settings / appearance / advanced / children(island-layout.ts:77-108)。
  • appearance:resourceOverridingEnabled / secondaryFontSize / bodyFontSize / emphasizedFontSize / largeFontSize / customForegroundEnabled / foreground / customBackgroundEnabled / background / customBackgroundOpacityEnabled / backgroundOpacity / customCornerRadiusEnabled / cornerRadius / opacity(:61-76)。
  • advanced:hideRules / align / width / maxWidth(:101-106)。
  • 默认值来源:islandNode()(:173-216),规范化边界在 normalizeIslandLayouts(:257-403)。
  • 外观应用方式:ComponentNode.style 计算(ComponentNode.vue:179-210)——opacity、width、maxWidth、alignSelf(仅父级有宽度时)、textAlign、自定义前景/背景/圆角、字号覆盖、容器方向与 gap。
  • 字体:--secondary 默认 14、--emphasized 默认 18、--large 默认 20(由 CustomIslandRuntime.vue:119-122 按 appearance.fontSize/13 比例算出)→ 改为直接使用 --ci-fs-secondary/emphasized/large(14/18/20),不再按字号比例缩放;只有 appearance.fontSize 作为"整岛缩放"时再乘系数。
  • 图标:统一用 ComponentIcon.vue 的 24×24 线性图标(ComponentIcon.vue:4-35,stroke-width 1.5);ClassIsland 用 FluentSystemIcons,不引入新图标字体,按语义映射(见各组件)。

7.4.1 date 日期

  • 数据字段:仅 settings 常规字段(text 等未使用)。
  • 渲染:new Date(now).toLocaleDateString('zh-CN', { month:'long', day:'numeric', weekday:'long' })(ComponentNode.vue:134-135),如 11月12日星期三。
  • ClassIsland 基准:ddd MM/dd 格式(repos/ClassIsland/ClassIsland/Controls/Components/DateComponent.axaml:11),即 周三 11/12。
  • 改造:默认格式改为 周三 11/12(短星期 + 补零月日 + 斜杠),保留 zh-CN 区域;字号 var(--ci-fs-body)(16px)、white-space: nowrap、font-variant-numeric: tabular-nums。
  • 空数据兜底:now 异常时显示 --,不得抛错。
  • 动画:无。

7.4.2 clock 时钟

  • 数据字段:settings.hour12(默认 false)、settings.seconds(默认 true,island-layout.ts:182)。
  • 渲染:toLocaleTimeString('zh-CN', { hour:'2-digit', minute:'2-digit', ...(seconds?{second:'2-digit'}:{}), hour12 })(ComponentNode.vue:136-142)。
  • ClassIsland 基准:HH:mm / HH:mm:ss、冒号闪动(repos/ClassIsland/ClassIsland/Controls/Components/ClockComponent.axaml.cs:71-76)。
  • 改造:
    • 秒数默认改 false(与 ClassIsland 一致,Models/ComponentSettings/ClockComponentSettings.cs 的 ShowSeconds=false),旧配置不动;
    • 新增 settings.flashSeparator(默认 false,P2 才做闪动);闪动规则 sep = !flash || seconds || 秒%2===1;
    • 字号 var(--ci-fs-emphasized)(18px)、font-variant-numeric: tabular-nums、white-space: nowrap(现有 :305-308 已有)。
  • 空数据兜底:无。
  • 动画:秒变化不做过渡(避免数字滚动抖动)。

7.4.3 weather 天气简报

  • 数据字段:settings.locationName / latitude / longitude(island-layout.ts:87-90)。
  • 数据源:bridge.getIslandWeather({ latitude, longitude })(preload/island.ts:15),Open-Meteo,15 分钟刷新,断网保留缓存并标"缓存"(docs/app/island.md:45)。
  • 渲染:WeatherBrief.vue(现有 2.9KB,需按下面改写)。
<span class="weather-brief" :data-stale="stale">
  <icon class="weather-icon" />            <!-- 22×22,用 ComponentIcon 或天气码映射 -->
  <span class="weather-temp">
    <span class="value">23</span><span class="unit">°C</span>
  </span>
  <span v-if="windSpeed" class="weather-wind">…</span>
</span>
  • ClassIsland 基准(repos/ClassIsland/ClassIsland/Controls/Components/WeatherComponent.axaml:51-184):横排 gap 6;图标 22×22;温度值 + 单位 margin-left:6;湿度/风/气压/体感/AQI 为可选主信息(一次只显示一种);AQI 6 级配色;预警药丸高 22、圆角 11、文字 14px。
  • 改造(P1 做图标 + 温度;其余 P2):
    • 图标尺寸 22×22、margin-right: 6px;
    • 温度 font-size: var(--ci-fs-body)(16px);
    • 地点缺失(latitude/longitude === null)→ 显示"未设置地点"(opacity: 0.7),不得显示 NaN;
    • 数据缺失 → 只显示图标占位 + --;
    • stale === true → 追加小字"缓存"(var(--ci-fs-caption),opacity: 0.6)。
  • 动画:数据刷新时数字变化不做过渡(避免每 15 分钟闪一下)。

7.4.4 countdown 倒计时

  • 数据字段(island-layout.ts:85-88):target: 'auto'|'next'|'end'|'school'|'date'、targetDate、countdownTitle。
  • 渲染逻辑:ComponentNode.vue:150-176(自动/下一节/本节下课/放学/指定日期五种目标,文案 距上课 m:ss / 距下课 m:ss / 距放学 m:ss / 距X N天)。
  • ClassIsland 基准(repos/ClassIsland/ClassIsland/Controls/Components/CountDownComponent.axaml:64-114):距离 <名称> 还有 <数值>,紧凑模式去掉"距离/还有";进度环 22×22(或底部进度条);自定义格式 token %D/%H/%M/%S/%P/%p/%L/%d/%h/%m/%s/%x。
  • 改造(P1):
    • 文案结构改为 distance(默认"距离")+ countdownTitle + connector(默认"还有")+ 数值;新增 settings.compact(默认 false);
    • 新增 settings.showProgress(默认 false)+ settings.progressMode: 'ring'|'bar'(默认 ring):ring 用 22×22 SVG 弧(stroke-width 3.1、stroke-linecap: round、半径 9.5),bar 用 3px 底部进度条;
    • 数值格式化:%m/%s 补两位、%D/%H/%M/%S 向上取整(与 ClassIsland 一致),默认格式 %m:%s(mm:ss);
    • 颜色:var(--ci-text-1);settings.color 覆盖(P2)。
  • 空数据兜底:无课程数据 → 今日课程已结束(现有 :170);目标日期为空 → 请设置目标日期(:152)。
  • 动画:进度条 transition: transform 1s linear;ring 的 stroke-dashoffset 用 1s 线性过渡。

7.4.5 schedule 课程表

最复杂,必须逐条对照 ClassIsland。

  • 数据字段:settings.display: 'current'|'next'|'today'(默认 current)、settings.count(默认 5,范围 1–40)、settings.showPeriod(默认 true)、settings.showProgress(默认 true)。
  • 数据源:schedules + timePoints(ComponentNode.vue:30-61),时间点优先用服务端完整时间表,缺失时回退课表条目。
  • 显示规则(现有,保留):ComponentNode.vue:74-97——
    • current:当前时间点;无则下一节;
    • next:下一节课 1 条;
    • today:过滤 DIVIDER、当前时间点、未隐藏的 LESSON,按 count 取窗口并让当前项居中。
  • ClassIsland 基准(repos/ClassIsland/ClassIsland/Controls/Components/ScheduleComponent.axaml + LessonControlExpanded.axaml):
    • 每个时间点一个"课程 chip":左右各 16px × ScheduleSpacing(默认 1)、科目名 18px Bold、附加信息 14px、进度条 3px;
    • 隐藏规则:HideFinishedClass、ShowCurrentLessonOnlyOnClass、IsHideDefault、课间只在当前时展开;
    • 明天课表 4 种模式(P2);
    • 空状态占位"今天没有课程。"/"今日课程已全部结束。"。
  • 改造(P1):
<div class="node node-schedule" :data-display="display">
  <div v-for="p in schedulePoints" :key="p.id" class="lesson-line" :class="{ current, break: p.timeType==='BREAK' }">
    <strong class="lesson-name">{{ p.courseName || p.name || '—' }}</strong>
    <span v-if="isEnding" class="lesson-ending-badge">-{{ remaining }}</span>   <!-- 30×30 正圆,见 lesson-ending.css -->
    <small v-if="current && settings.showPeriod" class="lesson-time">{{ p.startTime }}–{{ p.endTime }}</small>
    <span v-if="current && p.timeType==='LESSON' && settings.showProgress" class="lesson-progress">
      <i :style="{ transform: `scaleX(${elapsed})` }" />
    </span>
  </div>
  <span v-if="!schedulePoints.length" class="lesson-empty">今天没有课程。</span>
</div>
  • 课程名 var(--ci-fs-emphasized)(18px)+ font-weight: 700;时间/附加信息 var(--ci-fs-secondary)(14px);
  • 行内 gap 6px(现状 ComponentNode.vue:313-321 已是 6px,保留);
  • 进度条 3px(7.2.4);
  • 已结束课程 opacity: 0.6(ClassIsland LessonsListBox.axaml:56-58),过渡 150ms cubic-bezier(0.645,0.045,0.355,1);
  • 分割线:1×18、opacity: 0.25(现状 :334-342),可提到 24px 与 ClassIsland 的 25px 对齐(P2);
  • 课后占位:现有"今日课程已结束"只用于 countdown;schedule 需新增 今天没有课程。/今日课程已全部结束。 两态(ClassIsland LessonControlSettings 默认文案)。
  • 空数据兜底:timePoints 与 schedules 都为空 → 显示占位、不占高度超过 40px;只有 schedules → 回退按条目渲染(现有逻辑)。
  • 动画:切换时间点时课程名不做滑动(ClassIsland 是整行裁切),只做进度条与徽标的数值更新。

7.4.6 text 文本

  • 数据字段:settings.text(默认 '班级信息')、settings.template(默认 false)。
  • 渲染:模板变量 {date} / {time} / {course}(ComponentNode.vue:143-149)。
  • ClassIsland 基准:TextComponent(文本内容 + 自定义字体颜色 + 字体大小 16–30)。
  • 改造:字号 var(--ci-fs-body);超长 text-overflow: ellipsis(现有 :125-129 已有);模板变量保留;新增 settings.fontSize(默认 16,范围 16–30)写入 inline font-size。
  • 空数据兜底:text 为空 → display: none(不占宽度与间距)。
  • 动画:无。

7.4.7 separator 分割线

  • 数据字段:无。
  • 现状:1px × 24px、opacity: 0.25(ComponentNode.vue:369-377,模板 :249)。
  • ClassIsland 基准:2×25、StrokeThickness=2、opacity 0.8(repos/ClassIsland/ClassIsland/Controls/Components/SeparatorComponent.axaml:10-14)。
  • 改造:宽 2px、高 25px、颜色 var(--ci-text-fill-secondary)(rgba(255,255,255,0.72))、opacity: 0.8。
  • 空数据兜底:无。
  • 动画:无。

7.4.8 slide 轮播容器

  • 数据字段:settings.interval(默认 5,范围 1–300)、settings.direction、settings.gap。
  • 现状:非编辑时按 floor(now / (interval*1000)) % children.length 只渲染一个子项(ComponentNode.vue:121-130),无过渡动画。
  • ClassIsland 基准:切换间隔默认 15s、三种模式(循环/随机/往复)、启用动画时 250ms 上滑淡入 + 上滑淡出、cubic-bezier(0.65,0,0.35,1)、位移 ±40px(repos/ClassIsland/ClassIsland/Controls/Components/SlideComponent.axaml:72-107)。
  • 改造(P1):
    • 新增 settings.interval 默认改 15(旧配置保留)、范围 1–300;
    • 新增 settings.slideMode: 'loop'|'random'|'repeat'(默认 loop);
    • 新增 settings.animate(默认 false):开启后切换用 CSS 类 slide-in/slide-out,250ms、translateY(-40px → 0)、opacity 0 → 1;关闭时直接换(现状);
    • 子项 max-height: var(--ci-line-height)。
  • 空数据兜底:0 子项 → 塌缩为 0 宽;1 子项 → 不轮播。
  • 动画:prefers-reduced-motion 时关闭过渡。

7.4.9 rolling 滚动容器

  • 数据字段:settings.speed(默认 30,范围 1–200)。
  • 现状:CSS 动画 rolling(translateX(30%) → -80%)+ --rolling-duration: max(1, 200/speed)s,无限线性(ComponentNode.vue:384-392, 446-453,:205)。
  • ClassIsland 基准(repos/ClassIsland/ClassIsland/Controls/Components/RollingComponent.axaml.cs:85-135):仅当内容宽 > 容器宽才滚动;dur = innerW / speed、可暂停、可回程;速度默认 40px/s。
  • 改造(P1):
    • 默认速度改 40;--rolling-duration: calc(var(--rolling-distance) / var(--rolling-speed) * 1s);
    • 仅当 scrollWidth > clientWidth 时加 rolling 类(用 JS 测量,ResizeObserver 监听容器与内容);
    • 保留"编辑时不滚动"(:390-392);
    • 暂停/停止规则 P2。
  • 空数据兜底:内容不溢出 → 静止、不抖动。
  • 动画:linear、无限循环;prefers-reduced-motion 时静止。

7.4.10 group 分组容器

  • 数据字段:children、settings.direction(默认 horizontal)、settings.gap(默认 8)。
  • 现状:display:flex、gap、align-items: center(ComponentNode.vue:206-208, 378-380)。
  • ClassIsland 基准:分组容器 = 横向 StackPanel + Margin="-6 0" + 子项 Margin="6 0"(repos/ClassIsland/ClassIsland/Controls/Components/GroupComponent.axaml:13-33),即子项间距 12。
  • 改造:gap 默认改 12;子项外边距 0;容器自身不带内边距。
  • 空数据兜底:0 子项 → 0 宽。
  • 动画:无(子项变化时行宽由行过渡)。

7.4.11 stack 堆叠容器

  • 数据字段:children、settings.direction、settings.gap。
  • 现状:flex-wrap: wrap(ComponentNode.vue:381-383)——这是"换行排列",不是 ClassIsland 的堆叠语义。
  • ClassIsland 基准:Panel,所有子项占据同一矩形、各自按对齐摆放(repos/ClassIsland/ClassIsland/Controls/Components/StackComponent.axaml:12-32)。
  • 改造(P1):.stack { display: grid; },所有子项 grid-area: 1 / 1,align-self/justify-self 由 advanced.align 决定;容器宽高取子项测量最大值;子项间距 0。
  • 空数据兜底:0 子项 → 0 宽。
  • 动画:无。

7.4.12 消息入口 notification / homework / call

这三个 kind 不是普通内容组件,而是"消息入口":

  • 现状:渲染时在 ComponentNode 里走分支(ComponentNode.vue:285-288),只有 node.id === alertNodeId 时才渲染 <slot name="alerts">,否则编辑态显示文案;数据来自 IslandState.active/queued。
  • 组件库只展示前 11 类(docs/app/island.md:40),消息类由铃铛与消息面板承担。
  • ClassIsland 对应:通知行/提醒气泡(行内遮罩 + 浮层 + 进度条,repos/ClassIsland/ClassIsland/XamlThemes/FluentTheme/Styles.axaml:70-202)。
  • 改造(P1):
    • 保留三个 kind 作为"入口标记":notification = 通知入口、homework = 作业入口、call = 叫人入口;
    • 铃铛显示逻辑(CustomIslandRuntime.vue:421-441)改为:有消息时显示图标 + 数量徽标(99+ 上限),无消息时显示 chevron;铃铛图标沿用 ComponentIcon 的 notification 路径;
    • 徽标尺寸保持 14px 高、圆角 7、#f45b58(custom-island.css:94-110),文字 9px/600;ClassIsland 风格下允许改为 var(--ci-accent-fill) 底 + 白字(一行样式覆盖);
    • 消息卡本体的内容与业务由第 8 部分规定;本部分只规定容器:
      • custom 详情页:高度 = islandListLayout().height(constants.ts:445+),宽度 = 岛上可用宽度,位置在 resident-detail-page;
      • fixed + classisland:固定 40px 行内气泡(7.2.8),islandListLayout() 不参与;
      • fixed + winisland:沿用现有详情卡高度 = islandListLayout().height。
  • 空数据兜底:无消息 → 三个入口都不渲染(hidden 计算已处理,ComponentNode.vue:108-120)。
  • 动画:徽标出现 resident-indicator-in(custom-runtime.css:147-149, 199-206)保留;custom 详情页展开/收起走高度弹簧;fixed + classisland 走 7.2.8 的遮罩/文字动画,不做高度动画。

7.4.13 CoursewareTransfer 课件覆盖

  • 触发:state.coursewareTransfer 非空时,LayoutIslandApp.vue:58, 71-75 直接渲染课件卡,并让经典岛 suspended、自定义岛不渲染(:57)。
  • 现状视觉:position: fixed、尺寸 COURSEWARE_ISLAND_SIZE、padding 17px 24px、border-radius 24px、底色 rgba(20,20,24,0.97)、阴影 0 2px 8px #0004、字号 13/1.4、进度条 5px(CoursewareTransfer.vue:67-131)。
  • ClassIsland 对应:课件传输不在 ClassIsland 中,属 ClassHelper 独有能力 → 保留现状视觉,只做两点对齐:
    1. 锚点/边距与岛一致(左/中/右 + ISLAND_SHADOW_PAD,CoursewareTransfer.vue:12-25),尺寸不随 style 变化;
    2. pointer-events: none 保留(:81),并由 setHitRect 上报矩形(:35-38),保证不吞桌面点击。
  • 空数据兜底:transfer.error 存在时显示错误行(:64),不隐藏整卡。

7.4.14 组件默认值总表(必须与代码一致)

islandNode() 当前默认(island-layout.ts:173-216)必须逐项落实;本轮改动用 粗体 标注:

字段 当前默认 本轮目标
settings.text 班级信息 保留
settings.template false 保留
settings.hour12 false 保留
settings.seconds true 保留(新节点默认仍 true,兼容)
settings.target auto 保留
settings.targetDate '' 保留
settings.countdownTitle 目标日 保留
settings.locationName '' 保留
settings.latitude/longitude null 保留
settings.display current 保留
settings.count 5 保留
settings.showPeriod true 保留
settings.showProgress true 保留
settings.direction horizontal 保留
settings.gap 8 12(组/堆叠容器子项间距)
settings.interval 5 15(轮播间隔,秒)
settings.speed 30 40(滚动 px/s)
appearance.* 字号 14/16/18/20 保留(改用 --ci-fs-*)
appearance.background #202024 保留
appearance.backgroundOpacity 0.5 保留
appearance.cornerRadius 16 8(与 ClassIsland 圆角一致)
appearance.opacity 1 保留
advanced.align stretch 保留
advanced.width/maxWidth 0/0 保留

7.5 编辑器渲染侧结构(组件库 / 拖拽 / 选中框 / 行设置)

编辑器是主窗口里的设置页(IslandEditorView.vue,860 行),不是岛的渲染进程;但它的 DOM/CSS 必须与岛保持同一套 token,避免"编辑器里好看、上岛变形"。

7.5.1 两态:标签排列 vs 实际预览

  • 非编辑态(editMode=false):component-lines 排列行与组件 chip(IslandEditorView.vue:659-695),每行一个 .component-line。
  • 编辑态(editMode=true):island-preview 按方案的真实布局渲染(:698-738),支持预览缩放 0.3–1.5、步进 0.1、重置(:739-750)。
  • 必须保留:"编辑预览不创建额外桌面窗口"(docs/app/island.md:47)。

7.5.2 尺寸与选中框(ClassIsland 规格)

元素 现状 目标
行容器 .component-line min-height: 54px、padding: 4px、border: 1px solid transparent、radius: var(--ch-radius-control)(island-editor.css:134-141) min-height: 48px(行高 40 + 上下 4)、padding: 4px 8px
行把手 .row-handle width: 24px(:149-154) 保留 24px
组件 selecetd background: var(--ch-accent-soft)、border-color: var(--ch-accent)(IslandEditorNode.vue:86-89) 改为 1.5px 虚线:outline: 1.5px dashed var(--ci-accent);outline-offset: -1.5px;不加背景色
容器组件 border: 1px dashed var(--ch-border-strong)、padding: 3px(:90-94) 1.5px dashed、padding: 3px、border-radius: 8px
拖入指示 .drop-before/after 3px 强调色竖条(:111-123);.drop-inside 2px 实线(:107-110) 保留,颜色改 var(--ci-accent)、圆角 1.5px
拖拽幽灵 .drag-ghost(IslandEditorView.vue:774-780) 保留,padding: 4px 8px、border-radius: 6px、背景 var(--ci-bg-tertiary)
详情抽屉 el-drawer 宽 min(360px, 94vw)(:785) 保留

7.5.3 行设置面板字段

现有行面板(IslandEditorView.vue:804-831):当前行、行名称(≤64 字)、组件间距(0–64)、允许显示提醒、设为主要行、上移/下移/删除。

目标态新增(与 ClassIsland 对齐,P1):

  • 行不透明度 opacity(默认 1.0,范围 0.1–1,步进 0.05)——写在行 style.opacity;
  • 行隐藏规则 hideOnRule(默认 false)+ 规则集(P2);
  • 行圆角覆盖(P2)、行背景色/不透明度覆盖(P2,对应 ClassIsland 行级"自定义背景颜色/不透明度/圆角")。

保存路径不变:bridge.saveLayout()(preload/island.ts:17)→ island:save-layout。

7.5.4 组件库

  • 11 个条目(date/clock/weather/countdown/schedule/text/separator/slide/rolling/group/stack),每项 ComponentIcon + 名称 + 描述(IslandEditorView.vue:751-770,数据来自 ISLAND_COMPONENT_LABELS + 本地描述表)。
  • 交互:pointerdown 开始拖拽、click 点击添加(:760-761)。
  • 目标态:图标尺寸 18×18(IslandEditorNode.vue:74-78)保持不变;卡片 min-height: 76px(island-editor.css:285)保留;不得把 notification/homework/call 加回组件库(docs/app/island.md:40 的既有约定)。

7.5.5 渲染侧必须新增/调整的 DOM 与 token

必须新增(岛渲染进程):

  • .ci-row-shell(classisland 风格 fixed 模式的行容器);
  • .ci-mask-layer / .ci-mask-title(7.2.6);
  • .lesson-empty(课程表占位);
  • .resident-bell 的 [data-family='classisland'] 覆盖;
  • .layout-row[data-hide='true'](隐藏规则命中)。

必须新增(CSS 变量,挂在 :root 与 .resident-card 上):

--ci-line-height: 40px; --ci-line-pad-x: 12px; --ci-line-gap: 5px; --ci-component-gap: 12px;
--ci-radius-island: 8px; --ci-island-opacity: 0.5; --ci-line-opacity: 1;
--ci-stroke-island: #ffffff26; --ci-stroke-island-w: 1px;
--ci-shadow-island: 0 4px 8px 2px #48000000;
--ci-progress-height: 3px;
--ci-fade-ratio: 0.05;
--ci-fs-secondary: 14px; --ci-fs-body: 16px; --ci-fs-emphasized: 18px; --ci-fs-large: 20px;
--ci-bg-secondary: #20202080; --ci-text-1: rgba(255,255,255,0.92); --ci-text-2: rgba(255,255,255,0.72);
--ci-accent: (用户强调色); --ci-accent-fill: color-mix(in srgb, var(--ci-accent) 100%, transparent);

命名对齐:packages/shared/src/island-layout.ts:20-36 的 CLASSISLAND_ISLAND_METRICS 已经是这些值的单一来源(rowHeight:40、horizontalPadding:12、radius:8、opacity:0.5、rowSpacing:5、字号 14/16/18/20、fadedOpacity:0.05)。渲染层必须从该常量生成 CSS 变量(通过 style 内联或 applyAppearance 写入 document.documentElement.style,位置同 CustomIslandRuntime.vue:102-124),不得在 CSS 里再抄一份数字。

7.6 验收标准

实现者必须逐条自查,并只在显式 --visual 的视觉验证里做桌面像素比对(默认后台验证不显示岛):

  1. 默认值:新安装 appearance.family = 'classisland';旧配置(无 family)升级后 family = 'winisland' 且 fixed 观感与升级前一致,不报错、不丢组件数据;设置页可一键切换并即时预览、可回退。 1.1 family 只影响 fixed:layout='custom' 时切 family 不改变任何渲染(custom 恒 ClassIsland 规格),设置页在 custom 下不展示"外观族/视觉风格"两项;custom 无 winisland 回退档。
  2. family='classisland' 的 fixed 模式是信息行不是胶囊:默认 1 行 = 日期 + 课程表、行高 40、圆角 8、背景不透明度 0.5、底部 3px 进度条;无消息时不显示(或按 idleSliver 留细缝);消息到来时在同一条 40px 行内渲染 ClassIsland 气泡(斜切遮罩 + 标题 + 正文 + 倒计时进度),收起后回到常态行。 2.1 气泡不增高行:气泡出现/消失前后,行高恒 40px(像素量测误差 ≤1px),窗口高恒 40+2×pad,win.getBounds() 不变;正文最多 2 行、超出省略号,不得出现文字溢出到行外或行高变成 48/56/64。 2.2 气泡动画参数与 ClassIsland 一致:遮罩每段 360ms、错峰 0/90/90/120/120ms(总 480ms)、开 QuarticEaseOut / 关 QuadraticEaseIn;文字 250ms delay 260ms + scale 1.1→1.0/750ms;退出 200ms、缩回 cubic-bezier(0.5,0,0.75,0);theme='classic' 走 translateY 变体。
  3. family='winisland' 的 fixed 模式仍是胶囊 + 详情卡:胶囊 268×44、圆角 h/2、squircle、纯黑/玻璃/渐变三色调、展开态阴影 0 2px 3px rgba(0,0,0,.11);与改造前逐像素一致。
  4. 岛在 6 个停靠位置、两个显示器、125%/150% DPI 下位置正确:居中停靠不受 marginX 影响;左右停靠距屏幕边缘 = marginX;底停靠时窗口贴底且岛不被裁。
  5. 窗口几何:开合过程中 win.getBounds() 不变化(用主进程日志或注入断言验证);只有设置变化/不可见时才 setBounds;classisland fixed 的窗口高恒为 40 + 2×20 = 80 DIP。
  6. 行:可见行高 40px(像素量测)、行内边距 12、行距 5、圆角 8、描边 1、阴影 0 4 8 2 #48000000;相邻组件间距 12;进度条 3px。
  7. 背景不透明度 0.5 只影响背景:文字在截图里为纯白 0.92/0.72 层级,不随背景变透明。
  8. 三条进度条高度都是 3px(课程、课程提醒、通知气泡),且不出现 2px 残留。
  9. custom 模式:rows 与 expandedRows 都按行渲染;scheme.separated=true 时每行独立背景胶囊,无整块背景;custom 渲染恒按 ClassIsland 规格,切换 family 不产生任何视觉变化。 9.1 custom 不被课程提醒接管:prepare / on-class / break / in-class 期间岛仍显示常态行与铃铛(课程进度由 schedule 组件呈现),不出现整岛替换;ending 只在课程名右侧显示蓝色圆形秒数徽标(30×30、-N);不存在"临时接管一行"的行为。
  10. 遮罩:通知/课程提醒共用 SlantedMask,5 段从两侧向中间打开(总 480ms)、关闭反向;开启减少动态效果或关闭动画时直接落位。
  11. 触摸屏:手指能点开岛(触摸模式生效),窗口贴合岛体,失焦宽限 1200ms;非触摸屏行为与改造前完全一致。
  12. 鼠标穿透:岛外点击一律落到桌面;命中矩形随形变更新(限流 50ms),形变结束后无"看不见却点不动"的死区。
  13. 11 个组件逐项:date 显示 周三 11/12、clock 显示 HH:mm、weather 显示 22px 图标 + 温度且无数据时不出现 NaN、countdown 文案与进度环/条可选、schedule 当前项高亮 + 3px 进度 + 已结束 0.6 淡化 + 空占位、text 空内容不占宽、separator 2×25/0.8、slide 15s 切换且可关动画、rolling 仅溢出时滚动且 40px/s、group 间距 12、stack 子项完全重叠。
  14. 消息入口:铃铛数量徽标正确(1–99,>99 显示 99+),无消息显示 chevron;notification/homework/call 不在组件库出现,但旧方案里的节点仍能渲染。
  15. 课件覆盖:传输中出现课件卡且岛隐藏;锚点与边距正确;pointer-events: none,不吞点击。
  16. 编辑器:选中组件为 1.5px 虚线强调框;容器组件为 1.5px 虚线;拖入指示在 before/after/inside 三种位置正确;预览缩放 0.3–1.5 不导致岛变形错位。
  17. prefers-reduced-motion 与设置里的"动画"开关都生效:关闭时所有弹簧 snap()、滚动静止、遮罩直接落位。
  18. 不得出现:setBackgroundMaterial、backdrop-filter、第二个 BrowserWindow、把窗口尺寸写进 CSS 的做法、在动画帧里 setBounds。

8. 消息、提醒、课程提醒、强提醒特效、天气与课件覆盖(ClassHelper 落地规格)

本节是「消息与课程」这一部分在 ClassHelper 现有仓库内 的落地规格。前提口径(与 parts-r/01、parts-r/02 保持一致):

  • 默认风格 appearance.family = 'classisland'(对应配置键 islandAppearance.style,落盘字段以 parts-r/05 为准):fixed 不再是胶囊+详情卡,而是 ClassIsland 风格信息行——默认 1 行 = 日期 + 课程表,行高 40、圆角 8、背景不透明度 0.5、底部 3px 进度条;消息到来时在同一行内展开 ClassIsland 风格气泡。原「胶囊 + 详情卡」只在 family='winisland' 兼容形态保留(老用户升级默认 winisland,新安装默认 classisland)。
  • custom 常驻:保留现有 schemes/rows/nodes/expandedRows 数据模型与编辑器,渲染与交互按 ClassIsland 规格重构;消息入口仍是铃铛 + 共用消息面板。
  • 数据不迁移:通知/作业/叫人/课表/课件/天气全部沿用 ClassHelper 现有服务端与本地链路(IslandNotification、ScheduleDto、IslandTimePointDto、CoursewareTransfer、IslandWeather)。
  • 裁决优先级:ClassHelper 业务语义优先,ClassIsland 视觉表现优先;逐项裁决见 8.12。

8.1 数据契约(复用为主,最小扩展)

8.1.1 现有类型必须原样复用

类型 位置 用途
NotificationPriority = 'LOW'|'NORMAL'|'HIGH'|'URGENT' packages/shared/src/types.ts:30 业务优先级(四档)
PRIORITY_RANK packages/shared/src/constants.ts:203-208 排序与合并共用的权重(LOW 0 / NORMAL 1 / HIGH 2 / URGENT 3)
IslandNotification packages/shared/src/types.ts:1131-1143 岛消息载荷:id/title/content/priority/createdAt/courseName/teacherName/kind/subtitle
IslandNotificationKind = 'notification'|'homework'|'call' packages/shared/src/types.ts:1111 消息类型
IslandState packages/shared/src/types.ts:1198-1230 coursewareTransfer/mode/lessonReminder/active/queued/inClass/currentPeriodEnd/reason/listExpanded/maxCardHeight/updatedAt
IslandLessonReminder packages/shared/src/types.ts:1114-1128 kind: prepare/on-class/break/ending/in-class 等字段
LessonReminderSettings packages/shared/src/island-layout.ts:141-158 五类提醒开关 + prepareSeconds/endingSeconds
ScheduleDto packages/shared/src/types.ts:545-564 dayOfWeek(1=周一…7=周日)/startTime/endTime/weekParity/course
IslandTimePointDto packages/shared/src/class-plan.ts:76-87 timeType/name/courseName/startTime/endTime/isHideDefault
CoursewareTransfer packages/shared/src/courseware.ts:82-94 stage/receivedBytes/totalBytes/progress/error
IslandWeather packages/shared/src/island-layout.ts:38-44 temperature/code/windSpeed/fetchedAt/stale

8.1.2 必须新增/扩展的字段(只加不改,保证迁移可回滚)

  1. IslandState 新增:
    • family: 'classisland' | 'winisland':由外观下发给渲染进程,决定渲染形态;
    • pendingStaged: number:上课期间被暂存的消息数(用于行内角标/铃铛计数);
    • bubblePhase: 'hidden' | 'mask' | 'overlay' | 'closing':气泡阶段(仅 fixed 使用,见 8.4);
    • bubbleDeadline: number:当前气泡阶段结束时间戳(毫秒),供渲染进程做进度条与自动收起(避免渲染进程自己计时导致主/渲染漂移)。
  2. IslandNotification 新增可选 level?: 'emergency'|'high'|'normal'|'low':由 priority 映射而来(URGENT→emergency、HIGH→high、NORMAL→normal、LOW→low),用于 ClassIsland 三级排序键的中间键;不改变服务端 DTO,只在主进程派生。
  3. IslandLessonReminder 新增 progress: number(0–1,课程进度)与 leftSeconds: number(整数秒,供徽标直接渲染),由主进程计算下发,渲染进程不得自行 Date.now() 推算(防止跨进程时钟漂移与系统时间跳变)。
  4. islandLayout 新增 version 与 family(见 parts-r/05);旧 residentEnabled 保留为兼容字段。

8.1.3 通知 DTO → 岛载荷的映射(单一入口)

必须在主进程的推送链路(现有 pushNotification 的调用方)集中实现,禁止各处自行拼装:

toIslandNotification(dto: NotificationDto, kindHint?): IslandNotification {
  id: dto.id,
  title: dto.title,
  content: dto.content,
  priority: dto.priority,
  level: MAP[dto.priority],
  createdAt: dto.createdAt,
  kind: kindHint ?? 'notification',          // 作业=homework;叫人=call(保留现有双播合并)
  courseName: dto.course?.name ?? null,
  teacherName: dto.teacher?.name ?? null,
  subtitle: dto.subtitle ?? null
}
  • 作业消息 kind='homework',id 为本地合成的 homework-<id>(packages/desktop-client/src/main/island.ts:1061-1065),不得拿它去调「标为已读」接口。
  • 叫人消息 kind='call',标题由服务端 buildCallTitle 生成(packages/shared/src/constants.ts:571-575:请 XXX 同学找 XXX 老师)。

8.2 去重、合并与排序(ClassHelper 业务规则,必须保留)

  1. 按 id 去重:同一 id 只保留一条,pendingNotifications() 以 Map 去重(packages/desktop-client/src/main/island.ts:738-749;渲染侧同口径 src/island/IslandApp.vue:86-97)。
  2. 通知/叫人双播合并:createCall 同时发 notification:new 与 call:new,同 id 两条到达时必须合并而非覆盖:kind 取更特殊的(call 优先),priority 取更高(PRIORITY_RANK 比较)(island.ts:713-724)。这是硬性要求,否则会出现「叫人胶囊显示成新消息、上课被当普通通知隐藏」。
  3. 排序口径:PRIORITY_RANK 降序 → createdAt 降序(新的在前)(island.ts:744-748)。不得改用 ClassIsland 的三级键打乱业务优先级——ClassIsland 的「提供方优先级」在 ClassHelper 中没有对应概念,只有服务端单通道消息,因此本重构的排序键固定为(业务优先级 → 时间);若要引入 ClassIsland 的「通知等级」作为第二键,仅在与时间同序时作为稳定排序的次级键,不得跨优先级反转。
  4. 未读与已读:未读数以服务端为准(GET /notifications/unread-count,docs/app/notifications.md:45-47);岛内「标为已读 / 全部已读」必须调用既有 markRead/markAllRead 处理器,成功后通知中心列表同步(island.ts:1044-1070;packages/desktop-client/src/renderer/stores/notifications.ts:102-122)。
  5. 实时撤回:收到 notification:deleted 后必须从岛队列与当前展示中移除(island.ts:1210-1216),缓存与通知列表同步清理(docs/app/notifications.md:34)。
  6. 本机作业指纹:本机刚录的作业通过指纹避免重复提醒自己——这是既有规则,必须保留(docs/app/island.md:25);实现位置在推送入口,不在岛渲染层。

8.3 队列与展示状态机(三形态对照 + 伪代码)

IslandState 的状态字段固定为:mode(hidden|pill|expanded)、active、queued、inClass、currentPeriodEnd、reason(new|after-class|urgent|call|closing)、listExpanded、maxCardHeight + 新增 bubblePhase/pendingStaged。

8.3.1 三种形态的展示规则对照

场景 fixed + classisland(默认) fixed + winisland(兼容) custom
空闲 信息行常驻(日期+课程表),无消息时不显示气泡 隐藏或细缝(现有规则) 常驻组件行
课间普通通知/作业 行内气泡自动展开(遮罩→正文),可点开应用 胶囊,点击详情 铃铛 + 未处理数量,点击面板
课堂普通通知/作业 暂存:不展开气泡,行内显示「N 条暂存」角标(muted),下课补弹 暂存,下课补弹(现有) 铃铛提示,允许主动查看
普通叫人(课间) 行内气泡自动展开(kind=call 样式) 胶囊(可点) 铃铛
普通叫人(课堂) 保持可见的「叫人」chip,不自动展开;点击后展开气泡 保留可点胶囊,不自动展开 铃铛,主动查看
紧急通知/紧急叫人 立即展开气泡(无视上课),并触发强提醒特效 立即展开 铃铛 + 提示,用户主动展开(可自动提示)
处理完成 气泡收回,信息行恢复 队列清空后隐藏/细缝 回常态行
课件传输 覆盖整岛内容(信息行也覆盖),见 8.10 同 同

8.3.2 pushNotification 分支(必须逐条实现,以现有实现为基底)

现实现(packages/desktop-client/src/main/island.ts:794-867)分支顺序不得打乱,只在「展示方式」处按形态分流:

push(raw, ctx):
  state.listExpanded = false
  n = mergeNotification(findKnown(raw.id), raw)
  inClass = ctx.inClass ?? state.inClass; state.inClass = inClass
  if (ctx.currentPeriodEnd !== undefined) state.currentPeriodEnd = ...
  if (ctx.preview) → 预览分支(30s 自收,失焦不收起)
  if (isImmediate(n)):            // priority === 'URGENT'(含紧急叫人)
      正在展示的其它消息回队列
      activate(n, n.kind==='call' ? 'call' : 'urgent', 'expanded')
      → classisland: bubblePhase='mask',并触发出特效
  else if (inClass && n.kind==='call'):
      activate(n, 'call', 'pill')  // classisland: 只显示 chip,不展开气泡
  else if (inClass):
      queued = [..., n];  mode='hidden'(classisland: 保持行可见,pendingStaged++)
  else:
      上一条 active 回队列;queued 追加 n;activate(n, 'new', 'pill')
      → classisland: bubblePhase='mask',行内自动展开气泡

8.3.3 消息动作表(渲染 → 主进程,island.ts:929-1077)

动作 语义 备注
expand 展开当前/最新消息 classisland:进入气泡;winisland:展开详情卡;custom:打开消息面板
collapse 收起 上课时普通通知直接隐藏;紧急/叫人回缩为 chip/胶囊
expand-list 多条时「展开更多」 由 islandListLayout 决定行数与提示(见 8.6.3)
dismiss 单条「知道了」 不改已读
dismiss-all 整批「知道了」 不改已读
mark-read 标为已读 调后端,失败要回滚 UI 状态
mark-all-read 整批已读 过滤掉 kind==='homework' 的本地 id
open-app 打开应用内通知页 列表行点击也走这里

8.3.4 自动收起超时(必须原值保留,island.ts:124-132)

形态 超时
pill(胶囊/chip) 15 000ms
课后补发展开 14 000ms
课后补发胶囊 25 000ms
展开(普通) 20 000ms
紧急 45 000ms
叫人 90 000ms
收回动画 CLOSE_ANIMATION_MS 360ms(随 speed 缩放,island.ts:121)

收起后的关键规则(island.ts:1266-1296):只要还有未处理消息(active 或 queued 非空),收起后必须保持 chip/胶囊/行内角标,不得直接消失;彻底隐藏只在「知道了 / 已读 / 队列清空 / 进入上课」时发生。

8.4 ClassIsland 风格气泡(fixed 默认形态,两阶段)

8.4.1 阶段机(必须由主进程状态驱动,渲染进程只渲染)

bubblePhase: hidden → mask →(有正文)overlay → closing → hidden
                       └(无正文/正文时长为 0)→ closing → hidden
  • 进入 mask:行仍为 40 高,气泡在同一行内展开;遮罩背景为强调色,斜切遮罩扫入,遮罩文字(标题/徽标)延迟入场。
  • mask 时长:默认 3 000ms;若该消息带正文且正文有明确 EndTime/时长,则按正文时长决定(不得把正文时长叠加到遮罩上导致总时长翻倍)。
  • 进入 overlay:遮罩退出、正文淡入;底部显示剩余时间进度条(见 8.4.4)。
  • closing:正文/遮罩退出(200ms),随后窗口/行恢复;reason='closing' 时行内不得显示任何「新消息」文案(防闪烁,沿用现有 isClosing 语义 src/island/IslandApp.vue:73-80)。
  • 任一阶段都可被新消息打断:紧急消息直接覆盖当前气泡(当前消息回队列),普通消息按队列顺序等待。

8.4.2 行内气泡几何(像素)

  • 行高固定 40;行圆角 8(--ci-radius);行背景不透明度 0.5(只作用背景层)。
  • 气泡内容内边距:水平 12(ClassIsland 行内容盒 Margin="12 0");遮罩文字层内边距 12 0;遮罩文字图标 20×20、间距 8。
  • 气泡最大高度 = 行高 40(正文单行/双行截断,超出省略);多条列表形态时高度由 islandListLayout 计算(见 8.6.3)。
  • 气泡底部进度条:高 3px、贴底、圆角 1.5、transform-origin:left、scaleX(progress)、值过渡 50ms linear(ClassIsland 3px 规格;现有课程提醒条是 2px,两者不得混用,见 8.8)。
  • 遮罩背景:强调色实底(--island-accent,默认 #0a84ff);文字色 #fff,标题 16px/600,副文本 12px/0.72 透明度。

8.4.3 气泡动画(ClassIsland 参数 + ClassHelper 弹簧的裁决)

裁决:岛的整体尺寸/圆角继续用 ClassHelper 的 SpringValue(stiffness=0.10、damping=0.68、换向保留 35% 动量、单帧位移 ≤ 剩余 20%、snap 0.05px,packages/desktop-client/src/island/spring.ts:26-114);气泡内部的遮罩扫入/文字/进度用 ClassIsland 的 CSS/WAAPI 参数。两条动画路径不得互相接管。

动画 参数 实现
斜切遮罩扫入 5 区段,权重 [0,0.1,0.2,0.4,0.2,0.1],每段 360ms,错峰 120ms(区 0/4 同时、1/3 延迟 90ms、2 延迟 120ms),缓动 QuarticEaseOut SVG polygon 5 片,或 clip-path 5 段;--ci-speed 缩放
斜切遮罩退出 反向:中间先收,1/3 延迟 90ms,0/4 延迟 120ms,缓动 QuadraticEaseIn,每段 360ms 同上,反向
遮罩文字淡入 delay 260ms、250ms、opacity 0→1、cubic-bezier(0.25,1,0.5,1) CSS animation both
遮罩文字缩放 delay 260ms、750ms、scale 1.1→1.0、同曲线 CSS transform
遮罩退出文字 200ms、opacity 1→0 + scale 1.0→1.1、cubic-bezier(0.5,0,0.75,0) CSS
正文淡入淡出 200ms、SineEaseIn CSS transition
进度条值 50ms linear transform: scaleX
行宽/行高变化 由弹簧决定(约 300–500ms 自然落定) SpringValue.step()

速度与降级:appearance.speed(0.5–2)必须同比缩放上述所有 ms:duration = base / speed,延迟同理;animations=false 时全部时长置 0(直接终态),但不得改变最终几何。系统「减少动态效果」时按 speed=2 且时长压到 1ms 处理。

8.4.4 气泡内容(信息层次)

  • 遮罩阶段:类型图标(通知/作业/叫人 三套 SVG,沿用 src/island/ClassicNotifications.vue:115-132)+ 类型徽标(紧急/重要/普通/低 或 叫人/新作业)+ 一行标题;紧急加「需立即查看」、叫人加「老师正在等你」、课后补发加「下课后补发」(同一套 live-dot 语义)。
  • 正文阶段:标题(单行省略)+ 正文(最多 2 行省略)+ 元信息(教师名 · 时间)+ 可选 subtitle(叫人提示语,如「请到办公室找我」)+ 按钮行(打开应用 / 标为已读 / 知道了;叫人时为 收到)。
  • 多条列表(listMode):标题 "N 条待处理通知",每行「类型图标 + 标题 + 教师名 · 时间 + 类型徽标」,默认显示前 3 行,超出提示「展开更多(还有 N 条)」或「更多请前往应用内操作」(packages/shared/src/constants.ts:379-402,445-474)。

8.4.5 ClassicNotifications.vue 的改造与 winisland 兼容形态

packages/desktop-client/src/island/ClassicNotifications.vue 不得整体废弃,必须拆成「气泡内容」组件并同时服务两种形态:

  1. 两阶段复用:fixed + classisland 下,mask 阶段只渲染它的 header 部分(类型图标 + 类型徽标 + 标题 + 紧急/叫人提示),overlay 阶段再渲染 title / body / footer(标题、正文、元信息、按钮行);列表形态(多条)只在 overlay 阶段出现。原组件的 size/listLayout/pendingNotifications 计算逻辑保留,改为由父组件传入。
  2. winisland 兼容:整卡(header + title + body + footer 一次性渲染)继续用于详情卡,交互与尺寸不变;只把文案/徽标/按钮语义与新气泡对齐(叫人 时按钮「收到」等)。
  3. 不得复制两份实现:图标、徽标、行布局、按钮行必须共用同一组件与同一份 classic-island.css,避免两种形态文案不一致。

8.5 强提醒全屏特效(Topmost)

8.5.1 行为

  1. 触发条件(全部满足):消息 priority==='URGENT'(紧急通知或紧急叫人);appearance.animations===true 且 appearance.effectEnabled(新增外观开关,默认 true);岛当前可见(不是编辑模式、不是预览、不是隐藏);本节特效与气泡可同时出现。
  2. 表现:以岛中心为原点,强调色涟漪扩散到覆盖主界面所在屏幕;扩散 600ms、同步淡出 600ms,750ms 后移除元素并隐藏特效窗口。
  3. 独立窗口:新建 topmost-effect 渲染入口与独立 BrowserWindow(transparent:true、frame:false、skipTaskbar:true、alwaysOnTop:true、showInactive、setIgnoreMouseEvents(true)、逐屏一个窗口);禁止 setBackgroundMaterial();窗口内元素清空即隐藏,不销毁。
  4. 缩放与多屏:effectScale(默认 1.0,复用现有 ISLAND_SHADOW_PAD 之外的独立字段)缩放窗口与圆心;屏幕/DPI 变化重算;4K@150% 下涟漪必须覆盖整屏无接缝。
  5. 降级:animations=false、系统减少动态、编辑模式、窗口重建中、系统休眠恢复后的 1s 内 → 不播特效(只播气泡),不得阻塞消息展示。
  6. 非目标:本轮不做音效与 TTS(ClassHelper 无音频通道);docs/app/notifications.md:23 的「约 45 秒收起」仍由队列超时实现,与特效无关。

8.5.2 像素/动画参数

项 值
涟漪元素 全屏 SVG/CSS 圆(border-radius:50% 或 SVG circle),pointer-events:none
扩散 scale 0→1(半径 = 圆心到屏幕最远角),600ms,QuadraticEaseIn(≈cubic-bezier(0.11,0,0.5,0))
淡出 opacity 1→0,600ms,SineEaseIn(≈cubic-bezier(0.12,0,0.39,0))
颜色 --island-accent,默认 #0a84ff,透明度按 1→0
生命周期 setTimeout(750 / speed) 后移除
窗口 覆盖 display.workArea;scale 后宽高乘系数

8.6 课程提醒五类(prepare / on-class / break / ending / progress)

命名对照:任务/文档口中的 progress(课中进度)在代码里对应 IslandLessonReminder.kind === 'in-class',开关为 classProgressEnabled;其余四类同名。本节一律用代码枚举值,并在需要时注明「progress」。

8.6.1 设置与默认值

islandLayout.lessonReminders(packages/shared/src/island-layout.ts:141-158,390-398):

字段 默认 范围
prepareEnabled true —
onClassEnabled true —
breakEnabled true —
endingEnabled true —
classProgressEnabled true —
prepareSeconds 60 5–1800
endingSeconds 60 5–600

8.6.2 判定算法(LessonReminderTracker.update,必须逐行实现)

输入:schedules(当天过滤后按 startTime 升序)、now、settings。输出 IslandLessonReminder | null。证据:packages/desktop-client/src/main/lesson-reminder.ts:17-109。

  1. 当天课表 = dayOfWeek === (now.getDay() || 7) 的条目,按开始时间升序(周日=7)。
  2. current = 满足 start ≤ now < end 的条目;next = 第一个 start > now;ended = end ≤ now 且距 end < 12 000ms 的最近一条。
  3. kind 判定顺序(自上而下,命中即止):
    1. current && now - current.start < 5 000 && onClassEnabled → on-class;
    2. breakEnabled && !current && ended && now - ended.end < (next ? 12 000 : 5 000) && (now - ended.end < 5 000 || this.current?.id === keyOf(ended,'break')) → break;
    3. current && endingEnabled && current.end - now ≤ endingSeconds*1000 → ending;
    4. !current && next && prepareEnabled && next.start - now ≤ prepareSeconds*1000 → prepare;
    5. current && classProgressEnabled → in-class。
  4. maskUntil / endsAt / lessonStartsAt / lessonEndsAt 必须按源码公式:
kind maskUntil endsAt
ending / in-class entered(无遮罩) end
prepare min(start, entered + 3000) start
break 且有下一节 entered + 2000 end + 12000
break 无下一节 entered + 5000 end + 5000
on-class entered + 5000 start + 5000
  1. id = "${dayKey}:${scheduleId}:${start}:${end}:${kind}";entered 为同一 id 的连续保持时刻(避免同一提醒重复计时)。
  2. 课程名 course?.name ?? '课程';时间字符串截到 HH:mm(导入数据可能带秒,lesson-reminder.ts:78-79)。
  3. 渲染进程不得自行推导 kind,只消费主进程下发的 lessonReminder(以及新增的 progress/leftSeconds)。

8.6.3 课程提醒条 UI(像素)

项 值 依据
容器 高 40、宽 max-content、min-width 300、max-width 800、圆角 8、背景 #20202080 LessonReminderContent.vue:196-208
信息层 高 40、居中、gap 12、padding 0 14、white-space:nowrap :209-216
主字号 16(课程名 20、时间 14、遮罩标题 18) :204,235-245,262
进度条 高 2px、贴底、强调色、scaleX(progress)、transition 50ms linear :307-316
遮罩 5 片斜切 SVG、tan(30°)*40 偏移、区域边界 [0,0.1,0.3,0.7,0.9,1]、每片 360ms :62-72
遮罩时序 开:delays [0,90,120,90,0];关:[120,90,0,90,120];eased = p^4(开)/ p²(关) :91-99
兜底 rAF 停摆时 600/speed ms 后强制到位 :83-89
文字动画 淡入 250ms/延迟 260ms、缩放 750ms/延迟 260ms,曲线 (0.25,1,0.5,1);退出 200ms、(0.5,0,0.75,0) :126-138,273-306

文案规则(LessonReminderContent.vue:14-23,143-169):

kind 左侧 课程侧
prepare 距上课还剩 N 秒 下节课是:课程名 + 时间
on-class 开始上课 本节课:课程名 + 时间
break 课间休息 下节课是:下一节课程名 + 下一节时间
ending (无左侧倒计时文字,只显示课程侧) 本节课:课程名 + 时间 + 右侧蓝色秒数徽标(见 8.7)
in-class 距下课还剩 N 秒 本节课:课程名 + 时间(无徽标)
  • 无课程时课程名显示 "今日课程已结束"。
  • leftSeconds = ceil((目标时间 - now)/1000),下限 0;目标时间:ending/in-class 用 lessonEndsAt,其余用 lessonStartsAt。
  • progress:ending/in-class 用「已上课时间 / 总课时」,其余用「剩余时长 / 本提醒总时长」,钳制 0–1。

8.6.4 三形态差异(必须实现)

形态 行为
fixed + classisland 课程提醒以行内条接管岛内容:信息行临时替换为提醒条(宽 = 行宽,高 40);ending 用徽标而非遮罩;in-class(进度)默认不使用提醒条(由课程表组件常驻显示进度),仅当用户开启 classProgressEnabled 且组件不可用时回退为提醒条【推断】
fixed + winisland 保留现有实现:customLayout=false 时 ending 不接管(island.ts:173-185 的 lessonShowing 已排除 custom 的 ending),fixed 的 ending 仍以提醒条显示并使用徽标(docs/app/island.md:70)
custom customLayout=true 时:kind==='in-class' 不接管岛(由 schedule 组件常驻显示进度);kind==='ending' 不接管岛内容,只在当前课程名右侧显示徽标(island.ts:198-199;docs/app/island.md:65-68)

必须遵守的三条硬规则:

  1. 没有「即将下课」遮罩:ending 绝不显示 ClassIsland 式全行遮罩,只显示课程名右侧的蓝色秒数徽标(docs/app/island.md:68)。
  2. 不替换自定义岛常态组件:custom 模式下课程提醒(除 prepare/on-class/break 的临时接管)不得替换常态组件行。
  3. 关闭开关即不显示:endingEnabled=false 时不显示徽标;endingSeconds 只影响徽标出现时机,不影响其他提醒。

8.7 下课前蓝色秒数徽标(重点规格)

8.7.1 视觉

项 值
位置 课程名 <strong> 右侧,与课程名同一行(inline-flex,flex:0 0 auto)
形态 正圆 30×30,border-radius:50%
背景 蓝色 #0a84ff(与默认强调色同值;可随 --island-accent 同步【推断:现实现写死 #0a84ff】)
文字 -N,白色 #fff,12px / 600,font-variant-numeric: tabular-nums,line-height:1,水平垂直居中
无障碍 aria-label="距下课N秒"
证据 packages/desktop-client/src/island/lesson-ending.css:1-16;LessonReminderContent.vue:160-167

8.7.2 行为

  1. 显示条件(全部满足):kind==='ending'(即 current && endingEnabled && end - now ≤ endingSeconds*1000);leftSeconds ≥ 0;课表数据存在 endTime。
  2. 数值:leftSeconds = ceil((lessonEndsAt - now)/1000);显示为 -${leftSeconds}。
  3. 剩余一秒显示 -1:当真实剩余时间在 (0, 1000]ms 时 ceil 结果为 1,徽标显示 -1;不得显示 -0,也不得在剩余 0 时继续显示(到点后 ending 条件不再成立,徽标消失)。这是用户明确要求的行为,必须有单测用例(now = end - 1ms → -1)。
  4. 阈值与开关:endingSeconds 默认 60(5–600);endingEnabled=false 时完全不出现徽标;animations=false 也不影响徽标(徽标不是动画)。
  5. 出现/消失动画:徽标可淡入 100ms(opacity 0→1)并随课程提醒条一起退出;不得用缩放弹跳(避免课堂投影上的视觉干扰)。
  6. 两种岛都要用:fixed(两种 family)与 custom 都必须显示该徽标;custom 下徽标挂在 schedule 组件的当前课程名右侧,由组件内部渲染同一 lesson-ending.css,不得复制第二套样式。
  7. 边界:
    • 无时间点数据、按 ScheduleDto 回退显示课程时,leftSeconds 同样由 endTime 计算,徽标照常显示;
    • 跨天(课程结束时间 ≤ 开始时间)视为数据异常,不显示徽标并记一次告警;
    • 系统时间跳变:以主进程下发的 leftSeconds 为准,渲染进程不得自行用 Date.now() 覆盖;
    • 连续多节连堂:每节课结束时徽标各自独立计算,切换瞬间不得出现「-60 → -1」跳变(切换时应先隐藏上一节徽标再显示新一节)。

8.8 课程进度条(两处,不得混用)

位置 高度 颜色 过渡 取值
课程提醒条底部(ClassHelper 既有) 2px --island-accent transform 50ms linear 见 8.6.3 的 progress 公式
课程表组件内「当前课程」进度(ClassIsland 规格) 3px 强调色 transform 50ms linear 已上/总课时,钳 0–1

裁决:两者同时出现时(fixed + classisland 信息行同时含课程表与提醒条)保留两条——提醒条 2px 贴岛底,组件内进度 3px 贴课程项底;不得合并成一条,也不得把提醒条改成 3px(会与 ClassIsland 「行内进度条 3px」混淆)。渲染必须用 scaleX + transform-origin:left,不得对宽度做过渡。

8.9 天气简报(Open-Meteo,保留 ClassHelper 语义)

8.9.1 数据链路(不得改)

渲染组件 WeatherBrief.vue → window.island.getIslandWeather({latitude, longitude})(preload 白名单)→ IPC island:weather(packages/desktop-client/src/main/ipc.ts:23)→ 主进程 getIslandWeather(packages/desktop-client/src/main/island-weather.ts)→ Open-Meteo。

8.9.2 服务行为(必须原样保留)

  1. 固定服务与参数,不接受渲染层传 URL:https://api.open-meteo.com/v1/forecast + current=temperature_2m,weather_code,wind_speed_10m + forecast_days=1(island-weather.ts:35-41)。
  2. 坐标校验:latitude/longitude 必须是有限数字,纬度绝对值 ≤90、经度 ≤180,否则抛「请设置有效的经纬度」(:14-22)。
  3. 缓存键 = 坐标定点 4 位(:23);成功缓存 15 分钟(:60);失败缓存 60 秒退避并保留旧数据(:63);同一坐标并发请求复用同一 Promise(:29-30);缓存上限 32 条(:68)。
  4. 请求安全:AbortSignal.timeout(8000)、redirect:'error'、响应体 > 64 000 字节拒收(:42-45);自动验收模式下不访问外网(:33-34)。
  5. 数据校验:三个数值字段必须都是有限数字,否则「天气数据格式不正确」(:47-52)。
  6. stale:成功返回时按 fetchedAt 距今 ≥ 15 分钟标记;失败返回旧数据时强制 stale:true(:26,64)。
  7. 渲染侧:每 60 秒刷新一次、坐标变化立即刷新、失败显示「天气暂不可用」、未设置坐标显示「请设置天气地点」、stale || unavailable 时追加 · 缓存(WeatherBrief.vue:36-49,68-81)。

8.9.3 天气文案与建议(保留)

WMO 天气码映射(WeatherBrief.vue:50-60):0 晴、1/2/3 少云/多云/阴、45/48 雾、51-57 毛毛雨、61-67/80-82 雨、71-77/85/86 雪、95-99 雷雨、其他 天气。建议文案:code ≥ 95 → 雷雨天气,注意避雷;windSpeed ≥ 40 → 风力较大,注意出行(:61-66)。

8.9.4 ClassIsland 视觉映射与范围

  • ClassIsland 天气组件的「6 种主信息模式」本轮只取默认一种:地名 + 天气文字 + 温度(取整)+ 可选建议,以 ClassIsland 的次要/正文字号(14/16)呈现;不实现多模式切换。
  • 不实现 ClassIsland 的气象预警、三天天气预报、逐小时预报(数据源无对应字段;docs/app/island.md:45 明确「不提供官方气象预警」)。
  • 组件内刷新失败不得阻塞岛其他内容;天气与服务端无关,不参与消息队列。

8.10 课件传输覆盖(最高优先级内容)

8.10.1 优先级规则(必须实现,island.ts:649-676)

  1. 课件传输(coursewareTransfer)覆盖整岛内容:fixed 的两种 family 与 custom 都必须显示;getState() 时若「当前有正在展示的紧急消息或叫人(mode!=='hidden' && active && (priority==='URGENT' || kind==='call'))」则课件让位,状态里返回 coursewareTransfer:null。
  2. 其余消息不因课件覆盖而丢弃:active/queued 保持原状,课件结束后按队列规则继续展示。
  3. 课件传输期间不触发强提醒特效(课件不是紧急消息)。
  4. 课件覆盖时窗口几何按 COURSEWARE_ISLAND_SIZE(424×132,packages/shared/src/courseware.ts:4)+ ISLAND_SHADOW_PAD 定位;custom 常驻也临时改为该尺寸,结束后恢复原方案尺寸。

8.10.2 UI 与像素(保留现有值)

项 值
卡片 424×132,padding 17 24,border-radius 24,背景 rgba(20,20,24,0.97),阴影 0 2px 8px #0004
字体 13px/1.4 'Segoe UI','Microsoft YaHei',sans-serif
状态行 12px,#ffffffb8;百分比/「失败」用强调色、600
文件名 16px、600、单行省略、margin 5px 0
元信息 类型 · 教师 + 已收/总量 MB(保留 1 位小数)
进度条 高 5px、圆角 8、accent-color,进度槽 #ffffff30
错误 11px、#ffb4ab、单行省略
证据 CoursewareTransfer.vue:67-131

8.10.3 阶段与失败

  • 阶段文案(CoursewareTransfer.vue:26-34):downloading 正在接收课件、verifying 正在校验文件、completed 课件接收完成、failed 课件接收失败。
  • 完成后由主进程/服务端链路校验 SHA256 并回执(docs/app/courseware.md:28);岛只负责展示,不执行文件。
  • 失败必须显示 error 文案并允许用户点击卡片打开课件页(pointer-events 在课件卡上为 none,点击交给整岛 open-app);失败卡片停留至用户处理或下一个传输覆盖,建议停留 30s 后自动恢复常态【推断】。
  • 传输中窗口重建/渲染进程崩溃:主进程重新下发 coursewareTransfer 状态,恢复展示。

8.11 教师端 / 班级端差异

  1. 岛只在班级端运行:教师/管理员使用 Web 管理端,不启动岛;客户端以 CLASS_DEVICE 登录时才有「本班」消息。
  2. 班级端:接收本班通知/作业/叫人/课件;课程提醒基于服务端解析后的本班课表(ScheduleDto/IslandTimePointDto)。
  3. 教师端(Web):无岛;发布 URGENT 时的二次确认是服务端规则(docs/app/notifications.md:25-29),与岛无关。
  4. 未登录/离线:不展示在线消息;已缓存消息可只读展示(不得伪造未读状态);课程提醒与天气可离线工作(课表来自本地缓存,天气有缓存)。
  5. 权限:已读/撤回等操作失败(403/401)时,岛必须回滚本地状态并提示「无权限或登录已失效」,不得假装成功。

8.12 冲突裁决表(ClassHelper 业务优先,ClassIsland 视觉优先)

主题 ClassHelper 既有语义(胜出) ClassIsland 视觉(采用) 落地规则
消息排序 PRIORITY_RANK → 时间 提供方优先级/通知等级 按 8.2 第 3 条:业务优先级优先,等级仅作稳定次级键
上课暂存 上课期间普通通知暂存,下课补弹 ClassIsland 上课照常播提醒 暂存规则保留;信息行保持可见但不展开气泡
紧急消息 立即展开、45s 收起 ClassIsland 气泡两阶段 立即展开气泡 + 撒特效;45s 超时保留
普通叫人 上课也保留可点入口,不自动展开 ClassIsland 无「叫人」概念 上课显示可点 chip;点击展开气泡
去重 按 id + 双播合并 同一 request 只入队一次 两者都实现(8.2)
已读 服务端已读 + 本地回执 ClassIsland 无 保留现有,失败回滚
收起动画 收回动画 360ms/窗口淡出 遮罩退出 200ms 遮罩退出用 ClassIsland 参数;岛形变用弹簧;窗口淡出保留
多消息列表 前 3 条 + 展开更多 ClassIsland 无列表形态 用 ClassIsland 行高/字号重排列表(共享 islandListLayout)
课程提醒 kind prepare/on-class/break/ending/in-class ClassIsland 只有上下课/课前/放学 保留五类;ending 用徽标,不做「即将下课」遮罩
课程提醒条高度 40 行高 40 一致,沿用 40
进度条 2px(提醒条) 3px(行/组件) 两处各自保留,见 8.8
天气 Open-Meteo 15 分钟 + 缓存标记 小米天气 + 预警 保留 Open-Meteo;不实现预警/预报
课件覆盖 覆盖岛内容 无对应 保留 ClassHelper 规则;紧急/叫人时不覆盖
音效/TTS 无 有 不实现,不进入验收
自动化/插件/管理端 无 有 不实现

8.13 边界与失败降级

  1. 无课表/课表为空:lessonReminder=null;信息行显示日期 + 课程表占位(今天没有课程。);课程提醒全部不触发;不得因此隐藏整行。
  2. 上课状态未知(inClass 未下发/服务端断线):按「非上课」处理(消息正常展示),不得猜测;恢复后以下发值为准。
  3. 时间跳变/时区变化:课程提醒 id 含 dayKey,跨天自动重建;时间回退不得重复触发同一 id 的提醒(entered 逻辑),时间前跳不得补发多条。
  4. 消息风暴(一次 > 20 条):列表最多 maxRows=20,其余提示「更多请前往应用内操作」;不得逐条弹气泡;同一 id 重复到达按合并规则处理。
  5. 重复广播 / 断线重连重放:以 id 去重;重连后以服务端列表为准做一次全量对账(新增/删除/已读状态),不得凭队列恢复弹窗。
  6. 已读请求失败:markRead/markAllRead 失败时恢复本地未读状态并提示;不得把消息从队列删除。
  7. 课件传输中断/断网:显示 failed + error;恢复网络后由心跳补拉(docs/app/courseware.md:26-28);岛不得自行重试下载。
  8. 渲染进程崩溃/窗口重建:主进程保留 state,重建后一次性下发;课件/课程提醒/消息不丢;重建限流沿用现有(3 次/10 分钟)。
  9. 系统睡眠/唤醒:唤醒后立即重算课程提醒与上课状态;跨过的时间点不补弹(与 8.13 第 3 条一致);课件若在睡眠中完成,按最新状态展示。
  10. 多屏切换/DPI 变化:重算命中矩形、气泡与特效窗口;切换瞬间不得显示残影(先隐藏特效窗口再重算)。
  11. 无网络启动:消息/课表用离线缓存(按服务器与班级隔离),天气显示缓存或「暂不可用」;课程提醒只要课表缓存存在就必须工作。
  12. 编辑模式/设置预览:编辑模式不弹新气泡、不播特效、课程提醒条不接管编辑画布;预览模式停留 30s 自动结束(PREVIEW_DURATION_MS,island.ts:2299-2300),不写已读。

8.14 验收标准(可勾选,全部通过才算完成)

消息与队列

  1. 同一 id 的通知与叫人双播到达:只出现一条,kind='call'、priority 取高者;不会降级成普通通知。
  2. 课间收到普通通知:fixed+classisland 行内自动展开气泡(先遮罩后正文);winisland 为胶囊;custom 铃铛计数 +1。
  3. 上课收到普通通知:不弹气泡,行内显示暂存角标;下课自动补弹最新一条,其余保留「还有 N 条」。
  4. 上课收到普通叫人:显示可点 chip,不自动展开;点击后展开气泡并可「收到」。
  5. 紧急通知/紧急叫人:无论上课与否立即展开气泡,并在 45s/90s 后按规则收起(有未处理时保持 chip/角标)。
  6. 「知道了」与「标为已读」语义区分正确;整批已读会过滤 homework 本地 id,不报「通知不存在」。
  7. 实时撤回后,岛内该消息从 active 与 queued 同时消失。
  8. 消息列表默认 3 行、超出显示「展开更多(还有 N 条)」;展开后受屏幕高度限制改提示「更多请前往应用内操作」。

气泡视觉

  1. 气泡遮罩 5 区段斜切扫入,区段 360ms、错峰 120ms;文字延迟 260ms 淡入 250ms + 缩放 750ms;退出 200ms 且缩放回 1.1;speed 改变后所有时长同比缩放。
  2. 行高 40、圆角 8、背景不透明度 0.5、底部进度条 3px、值过渡 50ms 线性;文字始终不透明。
  3. animations=false 或系统减少动态时,气泡直接终态,最终几何与有动画时一致。

强提醒特效

  1. 紧急消息触发全屏涟漪:600ms 扩散 + 600ms 淡出,750ms 后移除;多屏各一窗口;effectScale 生效。
  2. 编辑模式、隐藏状态、窗口重建中不播特效,但气泡照常;特效窗口不接收鼠标、不改变岛命中。

课程提醒

  1. 五类提醒按 8.6.2 算法在构造的时间轴下逐一出现,且同一提醒不重复;关闭对应开关后不出现。
  2. custom 下 in-class 与 ending 不接管岛;fixed 下 ending 显示徽标而非遮罩;任何形态都没有「即将下课」遮罩。
  3. 蓝色秒数徽标:正圆 30×30、#0a84ff、12px/600、显示 -N;now = end - 1ms 时显示 -1;endingEnabled=false 不显示。
  4. 课程进度:提醒条 2px、组件内 3px,两条同时存在且均贴底、均用 scaleX。

天气与课件

  1. 天气 15 分钟缓存、失败 60s 退避并保留旧数据、断网显示「· 缓存」、坐标非法显示「请设置天气地点」;同一坐标并发请求只发一次网络请求。
  2. 课件传输覆盖整岛(424×132),显示文件名/类型/教师/已收-总量/进度;紧急或叫人在播时课件让位且不丢消息;完成后 SHA256 校验通过才显示「完成」。

边界

  1. 跨天、时间跳变、睡眠唤醒、渲染进程崩溃重建后,消息不丢、不重复弹、课程提醒状态正确。
  2. 无课表/无网络/未登录时,岛显示合理占位且不崩溃;已读失败会回滚并提示。
  3. 全程未出现音效/TTS、自动化、插件、主题市场等非目标功能;也未出现第二个岛窗口(课件与特效窗口除外,且均为独立用途)。

9. 编辑器与设置页重构(IslandSettingsView / IslandEditorView / IslandComponentSettings)

本 part 规定「灵动岛设置页」与「自定义方案编辑器」在重构后的完整实现。所有改动必须在现有 ClassHelper 仓库内完成,沿用 pnpm monorepo、Electron 44、Vue 3.5、Element Plus 2.14、Pinia、Vue Router、Vite 与既有 @classhelper/shared 契约;不得新建工程、不得引入 Avalonia/C#、不得新增服务端依赖、不得新建额外桌面窗口来做编辑预览。所有数值以 packages/...:行号 标注证据;ClassIsland 侧以 repos/ClassIsland/...:行号 标注;无法从源码确认的写【推断】。

9.1 口径与术语(与 parts-r/01、05、07 对齐)

  1. family 是 fixed 形态的外观族:islandAppearance.family: 'classisland' | 'winisland',默认 'classisland'(新安装默认)。两种取值只改变 fixed 的外观:
    • classisland + fixed:ClassIsland 风格信息行,默认 1 行 = 日期 + 课程表,行高 40、圆角 8、背景不透明度 0.5、底部 3px 进度条,消息到来时在同一行内展开 ClassIsland 风格气泡(业务规则仍用 ClassHelper 的暂存/紧急/叫人/已读/去重);
    • winisland + fixed:保留「胶囊 + 详情卡」旧形态(兼容老用户);
    • classisland + custom 与 winisland + custom:都使用 ClassIsland 规格的行/组件(行高 40、行内留白 12、组件契约见 parts-r/07、编辑模式见本 part 9.9–9.16),family 对 custom 不产生影响。 禁止把 classisland 下的 fixed 写成小胶囊;禁止让 custom 因 family=winisland 回退成旧 custom 观感。
  2. layout 与 family 正交:layout: 'fixed' | 'custom' 决定「消息行 / 常驻组件方案」与是否开放编辑器;family 只决定 fixed 的视觉族。设置页必须在 layout === 'custom' 时把 family 控件置灰,并显示「自定义方案始终使用 ClassIsland 规格;外观族只作用于经典 fixed 形态。」;layout === 'fixed' 时 family 可用且实时生效。
  3. 升级口径:老配置文件没有 island.family 字段时,必须迁移为 'winisland'(保持用户既有 fixed 胶囊观感;custom 始终 ClassIsland 规格,迁移不影响);新安装/清空配置时用 'classisland'(fontSize 默认 16)。迁移必须保留用户原有 appearance.fontSize 数值(不因 family 迁移改成 13 或 16);若旧配置无该字段,按迁移后的 family 取默认(winisland=13 / classisland=16)。迁移实现见 9.18,需与 parts-r/05 的 islandLayout.version 迁移函数同批落地。
  4. 术语:组件(IslandComponentNode)、行(IslandLayoutRow)、方案(IslandLayoutScheme)、常态内容(rows)、无通知展开内容(expandedRows)、外观(IslandAppearance)。组件种类集合固定为 11 类编辑器可见:date/clock/weather/countdown/schedule/text/separator/slide/rolling/group/stack;消息类 notification/homework/call 不在组件库露出,仅兼容既有节点(packages/shared/src/island-layout.ts:3-18、:45-60;packages/desktop-client/src/renderer/views/settings/IslandEditorView.vue:58-70)。
  5. fixed 的内置信息行 v2 是只读内置方案,不可编辑:classisland + fixed 固定为 1 行 = 日期 + 课程表,该行属于内置方案,不进入方案编辑器(不占 schemes/rows/nodes,编辑器只服务 custom)。设置页对 fixed 只提供 9.5 的外观参数与 9.4-4 的静态缩略示意,不得提供行/组件编辑入口、不得出现「编辑固定行」按钮。后续版本若开放 fixed 行编辑,必须升级 islandLayout.version 并在 parts-r/05 增补契约;v2 不开放。
  6. editingLayout 死标志不属本 part:main/island.ts 中该标志只有读取点、无写入点的问题归 parts-r/04 处理;本 part 只规定“编辑模式由主窗口路由页承担、岛窗口不新增编辑态”,island.ts 的清理以 parts-r/04 为准,09 不修改该文件。

9.2 文件级改造清单(只列本 part 负责的文件)

文件 改造类型 必须做的事
packages/desktop-client/src/renderer/views/settings/IslandSettingsView.vue 大改 重排信息架构;新增 family 切换与两套外观参数;把课程提醒设置并入本页;保留实时预览/保存/恢复默认/预览效果
packages/desktop-client/src/renderer/views/settings/IslandEditorView.vue 中改 保留全部既有编辑能力;行/画布/选中框/抽屉/组件库按 ClassIsland 编辑模式对齐;组件设置抽屉拆为「内容/外观/尺寸/隐藏条件」四页
packages/desktop-client/src/renderer/components/IslandComponentSettings.vue 大改 四页签结构;补齐 11 类组件的全部设置项;字段/控件/默认值/范围见 9.15
packages/desktop-client/src/renderer/components/IslandEditorNode.vue 中改 选中框改为 1.5px 虚线 1/1.5;容器虚线;组件外框左右各 6px;拖拽手感与 ARIA 保持
packages/desktop-client/src/renderer/styles/island-editor.css 中改 行高 40、行距 scheme.spacing、组件外边距 6、抽屉 250ms 曲线、缩放/落点/ghost 样式
packages/desktop-client/src/renderer/styles/index.css 小改 设置页新增的 .island-family-*、.island-preview-thumb 等局部样式;复用既有 --ch-* 令牌(index.css:21-54、:103-131)
packages/shared/src/island-layout.ts 中改 IslandAppearance 扩展字段的默认值与 normalize 边界;normalizeIslandLayouts 的 12 行/200 节点/6 层/20 方案上限保持(:256-403)——字段最终名以 parts-r/05 为准
packages/desktop-client/src/main/config.ts 小改 外观新字段迁移(老用户 family=winisland)与 normalize;沿用 120ms 合并写盘(:252-298)
packages/desktop-client/src/main/ipc.ts 不改逻辑 确认 saveConfig 收到 islandLayout 后调用 island.syncCustomLayout()(:34-45);外观仍走 island:set-appearance/island:get-appearance(:2389-2410)
packages/desktop-client/src/types/desktop.d.ts 小改 saveConfig/islandSetAppearance 的纯数据注释保留(:143-175)

不得删改 main/island.ts 的窗口/命中/几何逻辑;编辑器与设置页只能通过既有 IPC 影响它(本 part 不承担岛渲染)。

9.3 设置页信息架构(重构后)

设置页仍挂在 /settings/island(packages/desktop-client/src/renderer/router/index.ts:63-68),侧栏标题「灵动岛」(layouts/ClientLayout.vue:48-55)。页面外层沿用 .page(padding: 16px 20px 24px)、.page-header(margin-bottom: 12px)、.page-title(20px/600)(styles/index.css:186-204),整体改为「页头 → 布局形态卡 → 外观卡 → 课程提醒卡 → 指针与窗口卡 → 底部操作条」,每张卡为 el-card shadow="never"(圆角 8、1px 描边、backdrop-filter: blur(20px) saturate(1.1),index.css 的 .el-card 规则)。

  1. 页头:左侧 h2「灵动岛」;右侧显示当前形态徽标 el-tag:经典信息行(family=classisland、layout=fixed)/ 自定义常驻(layout=custom)/ 经典胶囊(兼容)(family=winisland、layout=fixed)。徽标随保存结果更新,不额外请求。
  2. 布局形态卡(保留既有两张选择卡并升级文案与点击):
    • 左卡「经典灵动岛」:family=classisland 时描述必须改为「ClassIsland 风格信息行:日期 + 课程表,课中进度条;消息在同一行内展开,v2 为只读内置方案」;family=winisland 时保留现有描述「显示课程进度、上下课提醒与班级消息」;
    • 右卡「自定义灵动岛」:描述保留「ClassIsland 风格,常驻显示时钟、课程等组件」(custom 始终 ClassIsland 规格,与 family 无关);
    • 卡片样式沿用 .layout-choice(padding 20、圆角 12、选中 border-color: var(--el-color-primary) + background: var(--el-color-primary-light-9),:450-478);点击调用 chooseLayout('fixed'|'custom'),立即写 saveConfig({ islandLayout: { ...config.islandLayout, layout, residentEnabled: mode === 'custom' } })(现有实现 IslandSettingsView.vue:44-59 保留);
    • 选择 custom 后追加「编辑入口」块(现有 .layout-edit-entry,:192-198、:479-496):说明「编辑常态内容与无通知展开内容;课前自动提示,通知操作与经典岛共用。」+ 主按钮「编辑岛内组件」→ router.push('/settings/island/editor');
    • 选择 fixed 后不得出现任何行/组件编辑入口:fixed 的 classisland 信息行是只读内置方案(见 9.1-5),卡片说明里必须写明「固定信息行由内置方案渲染,暂不支持编辑」。
  3. 外观卡:标题「外观、位置与弹出」保留;顶部说明两段改为:
    • classisland:「默认信息行高度 40、圆角 8、背景不透明度 0.5;紧急消息立即展开,课中普通通知暂存、下课后展开;自定义岛可点击铃铛主动查看。」
    • winisland:保留现有「紧急消息立即展开;课中普通通知暂存,下课后展开。自定义岛可点击铃铛主动查看。」
    • 卡内第一行必须是 视觉风格(family) 控件(9.4),随后按 family 分支渲染参数组。
  4. 课程提醒卡:新增(内容见 9.7),单一数据源固定为 config.json.islandLayout.lessonReminders;原 /settings/reminder 页的「上下课提醒」只保留只读摘要 + 跳转,不得保留可编辑控件(避免双写)。
  5. 指针与窗口卡:layout=custom 时显示「指针避让」整块(现有 .resident-window-options,:199-218、:423-444);classisland fixed 下增加「触摸后自动恢复」说明【推断:由 ClassHelper 触摸失焦宽限承担,无独立参数】。
  6. 底部操作条(保留现有):保存设置(primary,loading)、预览效果、恢复默认;下方 InfoBar「拖动滑块即为实时预览;保存后写入本地配置,重启客户端仍然生效」(:385-395 原样保留,文案可保留)。

9.4 视觉风格(family)与两套参数组

  1. 控件(必做):el-segmented(Element Plus 2.14 可用)或两个 el-radio-button,标签:
    • ClassIsland 风格(value classisland,默认):「信息行 / 半透明横条,行高 40、圆角 8、底部进度条;正文基准字号 16」;
    • 经典 WinIsland(兼容)(value winisland):「保留胶囊 + 详情卡外观,仅建议老用户保持;岛字号默认 13」。 下方常驻一行 Warning 说明(el-alert type="warning",不可关闭):「从旧版本升级的配置默认使用经典 WinIsland 外观;切换到 ClassIsland 风格会改变经典形态的外观,消息暂存/紧急/叫人等规则不变。」——这就是老用户升级默认 winisland 的用户侧说明。
  2. 切换行为:family 写入 islandAppearance.family,通过 applyIslandAppearance({ family }) 实时应用(islandSetAppearance),点「保存设置」才写 config.island。切换不得重置另一族的参数值(两族参数分别保留在 IslandAppearance 中)。 字号语义随 family 切换:classisland 下 appearance.fontSize 是正文基准字号,标签文案必须为「正文字号(ClassIsland 基准)」,默认 16,并由它派生 14/18/20 三档阶梯(次级/强调/大号);winisland 下保持旧语义,标签文案必须为「岛字号(经典)」,默认 13,岛内全部文本按旧系数缩放。切换 family 时必须切换标签文案与「默认值」提示(如滑块右侧显示「默认 16」/「默认 13」),但不得在切换瞬间强制改写用户已设置的值;仅当值为空/非法时才用当前 family 的默认值。
  3. 可见性规则:layout === 'custom' 时 family 控件 disabled,并显示「自定义方案始终使用 ClassIsland 规格;外观族只作用于经典固定形态。」;layout === 'fixed' 时按 family 显示下方参数组:classisland 显示 9.5,winisland 显示 9.6。
  4. 固定信息行预览缩略(新增,P1):在 classisland fixed 下于外观卡顶部放一个纯 CSS 缩略图(宽 360×高 44,圆角 8,背景 #000000 + 不透明度 0.5,内含「10月17日 周五」「语文 08:00-08:40」两段文本与底部 3px 进度条),标注「示意:默认 1 行 = 日期 + 课程表;实际内容由日程数据驱动」。禁止用截图或额外窗口实现,必须是 DOM/CSS(与「编辑预览不创建额外桌面窗口」同源约束,docs/app/island.md:47)。

9.5 classisland 族外观参数(逐项)

所有控件改变时立即调用 applyIslandAppearance({...}) 实时预览;saveIslandAppearance() 再写盘。范围/默认值证据见 packages/shared/src/constants.ts:264-302、packages/shared/src/island-layout.ts:20-36;新增字段以 parts-r/05 为准。

# 控件(label-width 110 左侧标签) 字段 控件类型与范围 默认 生效时机 备注
1 界面缩放 ${scale} appearance.scale【新】 el-slider 0.75–2.5,step 0.05,tooltip F2 1 即时 ClassIsland Scale 语义(repos/ClassIsland/ClassIsland/Views/SettingPages/AppearanceSettingsPage.axaml:27-40);layout=custom 时隐藏(缩放在编辑器方案设置内)
2 圆角 ${radius}px appearance.radius(有则用;缺省取当前方案) el-slider 0–20,step 1 8 即时 layout=custom 时同时写 activeScheme.radius 并 saveConfig(保留现有同步,IslandSettingsView.vue:38-40、:113-122)
3 背景不透明度 ${round(opacity*100)}% appearance.opacity el-slider 0–1,step 0.05 0.5 即时 layout=custom 时同步 activeScheme.opacity
4 行间距 ${spacing}px appearance.spacing【新】 el-slider 0–150,step 1 5 即时 与 scheme.spacing 同步(custom);ClassIsland MainWindowLineVerticalMargin 默认 5(Models/Settings.cs:219)
5 主题色 appearance.accent el-color-picker #0a84ff 即时 用于高亮、按钮与进度条;ClassIsland 强调色默认 #00BFFF(Settings.cs:35),本实现沿用 ClassHelper 主题色
6 前景(文字)颜色 appearance.foreground【新】 el-color-picker + 开关「自定义」 关 / #ffffff 即时 自定义前景色覆盖主题派生色(ClassIsland IsCustomForegroundColorEnabled+CustomForegroundColor,AppearanceSettingsPage.axaml:536-551)
7 字体 appearance.fontFamily【新】 el-select 可搜索,默认项文案「HarmonyOS Sans SC(默认)」 默认字体 即时 选项来自系统字体或内置列表;ClassIsland 同(:405-448)
8 字重 appearance.fontWeight【新】 el-select:Thin/ExtraLight/Light/Normal/Medium/SemiBold/Bold/ExtraBold/Black/ExtraBlack Medium(500) 即时 ClassIsland MainWindowFontWeight2 默认 Medium(Settings.cs:101)
9 次级字号(派生 14) appearance.secondaryFontSize【新】 el-input-number 8–96,后缀 px 14(未显式设置时由 fontSize 派生阶梯给出) 即时 时间点附加信息等;classisland 默认阶梯 14/16/18/20
10 正文字号(ClassIsland 基准) ${fontSize}px appearance.fontSize el-input-number 8–96 或 slider 8–40,后缀 px 16 即时 classisland 语义:正文基准字号(repos/ClassIsland/ClassIsland/Views/SettingPages/AppearanceSettingsPage.axaml:482-533 四档字号以正文为基准);派生 14/18/20 三档;bodyFontSize 若存在则作为显式覆盖(以 parts-r/05 为准)
11 强调字号(派生 18) appearance.emphasizedFontSize【新】 同上 18(派生) 即时 课程表、时钟、提醒标题、天气预报
12 大号字号(派生 20) appearance.largeFontSize【新】 el-input-number 8–128,后缀 px 20(派生) 即时 课程预报
13 分体显示 appearance.separated【新】 el-switch 关 即时 行之间透明分隔;ClassIsland IsIslandSeperated(Settings.cs:1852)
14 进度条 appearance.progressBar【新】{ show, height, color } 开关 + 高度 2–6px(默认 3)+ 颜色(默认强调色) 显示/3px/强调色 即时 ClassIsland 3px 进度条(brief §1.2);隐藏时行底不留空白
15 显示位置 appearance.position el-select 6 项(顶/底 × 左/中/右) top-center 即时 现有 ISLAND_POSITIONS;classisland fixed 默认 top-center
16 左右边距 appearance.marginX el-slider 0–200,step 2,居中停靠时禁用 8 即时 仅左右停靠生效,保留现有禁用逻辑与解释(IslandSettingsView.vue:88-99、:324-337)
17 上下边距 appearance.marginY el-slider 0–160,step 2 8 即时 停靠顶/底生效
18 跟随鼠标屏幕 appearance.followCursorDisplay el-switch 关 即时 多显示器
19 动画 appearance.animations el-switch 开 即时 关闭后展开/收起瞬时完成
20 动画速度 appearance.speed el-slider 0.5–2,step 0.1 1 即时 与 spring.ts/CSS 时长倍率联动(part 10)
21 始终置顶 appearance.alwaysOnTop el-switch 开 即时
22 明暗主题 appearance.theme【新】'auto'|'light'|'dark' el-select:跟随应用/明亮/黑暗 跟随应用 即时 岛主题独立于主窗口 config.theme;ClassIsland 应用主题三态(AppearanceSettingsPage.axaml:123-135)

新增字段(1/4/6/7/8/9–14/22)必须由 parts-r/05 定稿字段名与 normalize 边界;本表给出默认值,任何越界值在 normalizeIslandAppearance 内钳制,禁止出现 NaN(现有归一化参考 main/island.ts:2229 的注释与 config.ts 的 normalize 调用)。

字号单一事实来源:appearance.fontSize 在 classisland 族是正文基准(默认 16),四档字号中「正文 = fontSize」「次级/强调/大号 = fontSize - 2 / + 2 / + 4」;若用户显式修改次级/强调/大号,则写 secondaryFontSize/emphasizedFontSize/largeFontSize 覆盖派生值(字段最终由 parts-r/05 定)。layout=custom 时字号由编辑器方案与组件外观控制,设置页这几项隐藏。

9.6 winisland 族外观参数(兼容保留,不得删)

family === 'winisland' 时渲染下列全部控件,字段、范围、默认值、文案与现有实现一致,禁止改变默认外观(老用户回退依赖它):

# 控件 字段 范围/step 默认
1 高度 ${height}px appearance.height 36–72/1 44
2 宽度 ${width}px appearance.width 220–420/2 268
3 圆角 ${radius}px appearance.radius 8–32/1 20
4 不透明度 ${%} appearance.opacity 0.4–1/0.05 1
5 岛字号(经典) ${fontSize}px appearance.fontSize 11–20/1 13(旧版语义;新装默认由 family 决定为 16,见下)
6 动画速度 ${speed}x appearance.speed 0.5–2/0.1 1
7 主题色 appearance.accent 颜色 #0a84ff
8 视觉风格 appearance.style 纯黑 black/毛玻璃 glass/主题色渐变 tinted black
9 显示位置 appearance.position 6 项 top-center
10 左右边距 appearance.marginX 0–200/2,居中禁用 8
11 上下边距 appearance.marginY 0–160/2 8
12 跟随鼠标屏幕 appearance.followCursorDisplay 开关 关
13 动画 appearance.animations 开关 开
14 始终置顶 appearance.alwaysOnTop 开关 开
15 空闲细缝 appearance.idleSliver 开关 关
16 预览效果 — 按钮:islandPush 一条 priority: 'NORMAL'、context.preview = true 的本地通知(30 秒自动消失、失焦不收起) —

证据:默认值与范围 shared/src/constants.ts:264-302;字段语义 shared/src/types.ts:1145-1188;ISLAND_STYLE_LABELS constants.ts:323-330;预览实现 IslandSettingsView.vue:141-155。字号/高度/宽度滑块若 family=classisland 时被隐藏,不得清空其值(切回 winisland 仍生效)。

fontSize 双语义(必须按 family 区分):winisland 下它是「岛字号」,保持旧语义与旧系数、旧版默认 13(constants.ts:272),标签必须写「岛字号(经典)」;classisland 下它是「正文字号基准」,默认 16,派生 14/18/20 阶梯(见 9.5)。老配置升级时保留用户原来的 fontSize 数值,不因 family 迁移而改写;新安装默认 16。设置页在切换 family 时只切换标签与默认值提示,不强制改写已有值。

9.7 课程提醒设置(并入本页)

在「课程提醒」卡内按 ClassHelper 既有 lessonReminders 字段渲染 5 个开关 + 2 个阈值;保存走 saveConfig({ islandLayout: { ...config.islandLayout, lessonReminders: {...} } })(现有 ReminderSettingsView.vue:8-24 的逻辑迁移过来),迁移不改变字段名与默认值(shared/src/island-layout.ts:140-158)。

控件 字段 控件类型/范围 默认 语义对齐
准备上课提醒 prepareEnabled el-switch 开 ClassIsland IsClassPrepareNotificationEnabled(Settings.cs:47)
上课提醒 onClassEnabled 开关 开 IsClassChangingNotificationEnabled(Settings.cs:46)
即将下课倒数 endingEnabled 开关 开 下课前秒数徽标;ClassIsland 蓝色圆形徽标、剩余一秒显示 -1
经典岛课中常显课程进度 classProgressEnabled 开关 开 仅 windowisland/经典形态显示;classisland fixed 用行底进度条
下课提醒与下节课预告 breakEnabled 开关 开 IsClassOffNotificationEnabled(Settings.cs:55)
课前提前(秒) prepareSeconds el-input-number 5–1800,step 5 60 ClassIsland ClassPrepareNotifySeconds(Settings.cs:43)
下课倒数提前(秒) endingSeconds el-input-number 5–600,step 5 60 ClassHelper 既有语义;ClassIsland 无同名项,标注【映射】

布局:每行使用现有 .lesson-setting-row(左右分布、padding: 12px 0、底部分隔线 1px solid var(--ch-border),ReminderSettingsView.vue:126-137),阈值行紧随 5 个开关之后。所有改动即时保存;保存失败 ElMessage.error('提醒设置保存失败')(沿用现有文案)。 提醒页 /settings/reminder 必须改为:顶部链接卡「上下课提醒已移至【灵动岛 → 课程提醒】」→ 按钮跳 /settings/island;其余「通知提醒」「成绩查询」卡保持不变。不得两处都可编辑,避免双写。

9.8 保存、实时预览与失败重试(设置页)

  1. 两条通道必须区分:
    • 外观改动 → window.desktop.islandSetAppearance({ ...appearance })(纯对象;主进程 16ms 合并,main/island.ts:2389-2410)→ 立即生效但不落盘;
    • 点「保存设置」→ window.desktop.saveConfig({ island: { ...appearance } }) → 落盘(合并写 120ms,main/config.ts:252-298)→ 成功 ElMessage.success('个性化设置已保存'),失败弹出错误并保留 savingIsland=false 以允许重试。
  2. 禁止传响应式 Proxy:所有 IPC 入参必须是展开后的纯对象(types/desktop.d.ts:143-152、:166-175;现有注释已写明 An object could not be cloned. 的坑)。设置页已有的 { ...island.value }、JSON.parse(JSON.stringify(...)) 写法必须保留。
  3. 圆角/行距/不透明度同步 custom 方案:当 layout === 'custom' 时,这三项除写 appearance 外还要写 activeScheme.radius/opacity/spacing 并 saveConfig({ islandLayout: 深拷贝 });失败提示「圆角保存失败,请重试」(现有文案,IslandSettingsView.vue:115-120),并不要回滚滑块显示值(下次保存重试)。
  4. 恢复默认:恢复默认 按钮必须 confirmAction('恢复灵动岛的默认外观?', '恢复默认', { type: 'warning' }),确认后 applyIslandAppearance({ ...DEFAULT_ISLAND_APPEARANCE }) 再保存;新默认 为 classisland 族参数(DEFAULT_ISLAND_APPEARANCE 扩展后),但恢复默认不得覆盖 family(用户显式选择要保留;如产品要一并重置,需二次确认)。
  5. 载入与刷新:onMounted 调 islandGetAppearance() 与 getConfig();window.addEventListener('focus', loadLayout)(现有 :82-86)保留,确保从编辑器返回后方案列表/圆角同步。
  6. 预览效果按钮:仅 family=winisland 显示(classisland fixed 预览用缩略图 + 真实运行态验证);点击推送预览通知(id 前缀 appearance-test-),说明文案必须含「拖动滑块即可实时预览」。

9.9 编辑器总体布局(像素级)

文件:IslandEditorView.vue;样式:styles/island-editor.css。保留现有 DOM 结构(smoke 依赖,见 9.20),只做尺寸/对齐调整。

  1. 页面容器:.island-editor-page 保持 display:flex; flex-direction:column; flex:1 0 auto; padding: 12px 24px 16px; background: var(--ch-bg)(:1-10);:has(> .island-editor-page) 让 .main 也纵向 flex(:11-14)。
  2. 顶部操作条 .editor-actions:右对齐、gap:8px、min-height:30px、margin-bottom:4px(:19-25);元素依次为保存状态文本(12px,见 9.18)、返回设置(text small)、应用并返回(primary small,loading=!!pending,disabled=!ready)。
  3. 页头 .component-heading:align-items:flex-end; gap:24px; margin-bottom:20px(:35-39);左侧标题「组件」font-size: 34px*var(--ch-font-scale)、font-weight:600、line-height:1.4、margin-bottom:20px;说明「以下组件将显示在主界面上,从组件库拖动组件以添加组件。」16px(:40-53);右侧方案工具区 flex: 0 1 348px; min-width:300px(:54-57)。
  4. 错误条:error 非空时在页头下显示 el-alert type="error",内容为错误消息 + 重试 按钮(ready ? save() : load())(:608-610)。
  5. 内容工具条 .surface-toolbar:左右两组;左侧 .surface-tabs 两个按钮「常态内容」「无通知展开内容」(12px,选中下方 2px 主色下划线,:102-124);右侧 .surface-actions 三个按钮:新增行、行设置、进入编辑模式/退出编辑模式(最后一个选中时 primary)(:612-653)。「新增行」文案不可改(smoke 断言依赖)。
  6. 预览画布 .preview-canvas:min-height:80px; overflow:auto; padding:10px 0 20px(:125-129);live-preview 时改为 display:grid; place-items:center; min-height:160px; padding:24px; background:var(--ch-layer-alt); border-radius:6px; margin-top:8px(:184-192)。
  7. 标签态 .component-lines(min-width:100%; width:max-content,:130-133)逐行渲染 .component-line:
    • display:flex; align-items:center; min-height:46px; padding:2px 4px; border:1px solid transparent; border-radius:var(--ch-radius-control); gap:12px;行与行间距改为 margin-top: var(--preview-gap)(值取 scheme.spacing,默认 5),不再固定 6px(原 :143-145;对齐 ClassIsland 行距默认 5);
    • 选中/激活行 border-color: var(--ch-border-strong);拖放目标行 outline:2px dashed var(--ch-accent); outline-offset:-2px; background:var(--ch-accent-soft)(保留 :228-232)。
    • 行把手 .row-handle:宽 20px(原 24,改以对齐 ClassIsland 拖拽把手宽度 20,repos/ClassIsland/ClassIsland/MainWindow.axaml:496)、align-self:stretch、图标「Ⅱ」16px、cursor:grab、touch-action:none;点击选中行、拖动排序(保留双行为)。
    • 行内 .line-components:display:flex; align-items:center; gap: var(--row-gap)(取 row.gap,默认 8)、min-height:40px(对齐行高 40)、flex:1、padding-right:28px(为行尾按钮留位,保留);
    • 空行占位 .empty-line 保留「将组件拖到这里,或点击下方组件添加」。
  8. 实时预览态 .island-preview:display:flex; flex-direction:column; gap:var(--preview-gap); width:max-content; padding:12px; border-radius:var(--preview-radius); background:#202024; color:#ffffffeb(:193-202);.preview-row 改 min-width:120px; min-height:40px(原 44);separated 时容器透明、每行独立 padding:12px;background:#202024(:210-217);缩放通过 zoom: scheme.scale * zoom 生效(现有模板 :700-708)。
  9. 组件库 .component-library:上边框分隔、margin:0 -24px; padding:14px 24px 24px;标题「组件库」17px/600 + 3px 主色下划线;网格 grid-template-columns: repeat(4, minmax(0,1fr)); gap:12px 20px;每项 min-height:76px; padding:10px 8px; gap:12px; border:1px solid var(--ch-border); border-radius:var(--ch-radius-control),图标 36×36,名称 18px/500,描述 14px 次级(:249-330);响应式 1150px→3 列、800px→2 列、520px→1 列(:371-417)。11 项顺序必须为:文本、分割线、课程表、日期、时钟、天气简报、倒计时、轮播容器、滚动容器、分组容器、堆叠容器(IslandEditorView.vue:58-70)。
  10. 抽屉 .el-drawer.island-inspector:宽 min(360px, 94vw)、modal=false、modal-penetrable、append-to-body、lock-scroll=false(:781-790);标题「组件设置」;内部顶部 el-radio-group 三个页签「方案/行/组件」(组件不可选时 disabled);抽屉背景/输入框样式沿用 island-editor.css:346-361。抽屉动画统一 250ms cubic-bezier(0,0,0,1)(对齐 ClassIsland DrawerHost:repos/ClassIsland/ClassIsland.Core/Controls/DrawerHost.axaml:42-59),并在 AnimationLevel=0/prefers-reduced-motion 时改为无动画。
  11. 拖拽幽灵 .drag-ghost:position:fixed; z-index:10000; padding:10px 16px; border-radius:6px; border:1px solid var(--ch-accent); box-shadow:var(--ch-shadow-flyout),位置 pointer.x+16 / pointer.y+16(:331-342、模板 :774-780)。
  12. 导入文件输入:隐藏 <input type=file accept="application/json,.json">(:773)保留。
  13. 骨架:ready=false 时 el-skeleton :rows="8" animated;ready=true 才渲染全部编辑 UI(:611)。

9.10 方案栏(保留并升级)

  1. 选择方案:el-select 绑定 settings.activeSchemeId,选项 scheme.name;disabled=!ready;@change="switchScheme"(重置选中 + 保存)。aria-label="当前方案" 保留(:557-568)。
  2. 重置当前方案:图标按钮(Refresh)aria-label="重置当前方案"、title="重置当前方案";确认框标题「重置方案」,正文「将「{方案名}」的组件和外观恢复为默认配置?」,按钮「重置/取消」,type: warning;确认后用默认方案内容替换但保留原 id 与 name,并校验组件容量(现有 resetScheme,:403-422)。
  3. 新建方案(Plus,aria-label="新建方案"):ElMessageBox.prompt('为新的组件方案命名', '新建方案', { inputValue:'新方案', inputValidator: 1–64 字符, confirmButtonText:'创建', cancelButtonText:'取消' });方案数上限 20(schemeCapacity,:379-383);新方案内容复制默认方案并重生成全部 id(:384-402、renewScheme :371-378);创建后自动选中并保存。
  4. 管理方案(FolderOpened 图标,aria-label="管理方案",el-dropdown trigger=click)菜单顺序固定:方案设置、复制方案、分隔、导入方案、导出方案、分隔、删除方案(方案数 < 2 时禁用)(:589-604、:423-432)。
  5. 复制方案:深拷贝当前方案,名称 ${name.slice(0,61)} 副本,重生成 id,push 后选中;受 20 方案与 200 组件上限约束(:214-230)。
  6. 删除方案:ElMessageBox.confirm('删除方案「X」?', '删除方案', { confirmButtonText:'删除', cancelButtonText:'取消', type:'warning' });删除后 activeSchemeId = schemes[0].id 并保存(:231-245)。
  7. 导出方案:JSON 结构 { format: 'classhelper-island-layout', scheme }、2 空格缩进、Blob 下载;文件名把 [<>:"/\\|?*] 替换为 _;下载后 1s revokeObjectURL(:246-257)。该 format 字符串是老用户导入兼容契约,不得改。
  8. 导入方案:文件 ≤ 1 MB;解析后校验 format === 'classhelper-island-layout' 且 scheme.rows 为非空数组;经 normalizeIslandLayouts 归一化;重生成全部 id;容量校验;成功 ElMessage.success('方案已导入'),失败 ElMessage.error(错误信息);finally 清空 input(:258-289)。
  9. 方案设置抽屉页:名称(maxlength=64)、界面缩放(input-number 0.75–2.5、step 0.05,绑 scheme.scale)、行间距(slider 0–150,绑 scheme.spacing)、常态圆角(slider 0–20,绑 scheme.radius)、各行分离显示(switch,绑 scheme.separated);底部提示「字体、透明度、位置和动画与灵动岛共用设置。」+ 链接「前往灵动岛外观设置」→ /settings/island(:832-857)。必须保留这些字段与控件类型(ClassIsland 方案级参数:缩放/行距/圆角/分离,见 repos/ClassIsland/ClassIsland/Views/SettingPages/AppearanceSettingsPage.axaml:27-118)。

9.11 行与行设置(ClassIsland 编辑模式对齐)

  1. 行列表:每行渲染 row.name(编辑态以把手 aria-label「编辑{行名}」、title「{行名},拖动排序,点击设置」暴露,IslandEditorView.vue:670-678);行数上限 12(addRow,:142-159,文案「每种内容最多添加 12 行」)。
  2. 新增行:保留工具条「新增行」按钮;新增行列表末尾居中的第二入口:32px 高、图标 E00D(20px)、文字「新增行」,样式与 ClassIsland「新主界面行」一致(repos/ClassIsland/ClassIsland/MainWindow.axaml:449-455),点击行为与工具条相同。新行默认 name: 组件行 N、main:false、allowNotifications:true、gap:8、nodes:[]。
  3. 排序:拖行把手到另一行位置上/下方即可重排(现有 dropPreview 的 current.rowId 分支,:505-512);也提供抽屉内「上移/下移」按钮(:824-830)。
  4. 主行:抽屉按钮「设为主要行」;已是主行时按钮禁用且文案「已设为主要行」;设置主行会清除其他行 main(setMain,:210-213);行数为 1 时不允许删除(removeRow,:202-209)。
  5. 允许提醒:开关「允许显示提醒」绑定 row.allowNotifications;提示文案「提醒优先使用主要行;关闭提醒后使用下一条允许提醒的行。」(:817-823)。
  6. 行间距:el-input-number 0–64,绑 row.gap(组件间距);方案级行间距在方案设置抽屉(scheme.spacing 0–150)。两者不得混用。
  7. 行设置抽屉规格:标题「组件行」;字段顺序固定:当前行下拉(可切换编辑行)、行名称(maxlength 64)、组件间距、允许显示提醒、主行按钮、提示、上移/下移/删除此行(danger plain)。抽屉沿用 360px 非模态规格;打开/关闭动画 250ms cubic-bezier(0,0,0,1)。
  8. ClassIsland 对齐点(必须实现):
    • 选中框:1.5px 虚线、dasharray:1,1.5、opacity:0.75、圆角跟随行/组件圆角;选中色 AccentFillColorDefaultBrush 等价主色,未选中容器为 SurfaceStrokeColorDefaultBrush 等价 --ch-border-strong(repos/ClassIsland/ClassIsland/Controls/EditMode/EditableComponentsListBox.axaml:225-251;MainWindow.axaml:353-366);
    • 拖动把手:行把手 20px 宽、组件 chip 自身可拖(TouchDragThumb 等价);拖拽时被拖元素 opacity:0.5(ClassIsland 拖动预览透明度 0.5,MainWindow.axaml:478-481;ClassHelper 可保留现有 ghost 不降低原元素透明度,二选一但需统一);
    • 行内组件外边距:组件外层左右各 6px(Margin="6 0",repos/ClassIsland/ClassIsland/MainWindow.axaml:388-399、:538-548),叠加行内 gap 后形成视觉间距;运行态组件无此外边距;
    • 行内容留白:行内左右 12px(Margin="12 0"、MinWidth="20",repos/ClassIsland/ClassIsland/XamlThemes/FluentTheme/Styles.axaml:203-232);编辑态为可选中的把手/加号额外留位;
    • 行高:40px(IslandContainerHeight,repos/ClassIsland/ClassIsland/Controls/MainWindowLine.axaml:16;MainWindow.axaml:530-537 容器行高 40)。
  9. 既有交互必须保留:行把手拖动排序、点击行选中并打开行设置、空行点击打开行设置、行尾「新增行」、主行/提醒开关的即时保存、最多 12 行提示。

9.12 组件库(11 类,保留既有 DOM 契约)

  1. 数据源固定 11 项、顺序见 9.9-9;每项包含 kind、描述(现有 library 数组,IslandEditorView.vue:58-70)。
  2. 每个库项必须是 button,带 data-library-kind={kind}、aria-label="添加{中文名}";保留 pointerdown 开始拖拽、click 直接添加(两者共存,拖动后 suppressClick 防误加,:754-768、:447、:500-503)。
  3. 直接添加逻辑:若当前选中容器则加入该容器(层级 ≥ 6 时警告「最多嵌套 6 层容器」),否则加入当前行末尾;添加后自动选中并保存(add,:130-141)。
  4. 组件图标使用 ComponentIcon(:763);名称与描述分别 18px/14px(随 --ch-font-scale 缩放)。
  5. 消息类组件(notification/homework/call)不得出现在库中,但既有方案里的这些节点必须继续渲染与可编辑(只读提示「此组件使用实时数据。可在外观和尺寸设置中调整显示。」)。

9.13 拖拽规格(三类源,必须保留现有实现)

  1. 统一 Pointer Events:pointerdown(仅 button===0)→ 在源元素 setPointerCapture(合成事件失败时 try/catch 忽略)→ pointermove 超过 6px 才判定开始拖拽 → pointerup 落点;pointercancel/blur/画布外释放一律取消(beginDrag/moveDrag/dropPreview/cancelDrag,IslandEditorView.vue:433-503;模板事件绑定 :674、:688、:730、:760)。
  2. 三种拖拽源:组件库项(kind 源)、组件 chip(nodeId 源)、行把手(rowId 源)。
  3. 落点判定:
    • 用 document.elementFromPoint + closest('[data-row]') 找行;无行则取消;
    • 行内找 [data-node]:未命中节点 → 落到行尾;命中节点 → 计算指针在节点宽度内的比例 ratio;
    • 容器节点(group/stack/slide/rolling)且 0.2 < ratio < 0.8 → inside;否则 ratio > 0.5 → after,否则 before(dragTarget,:462-481);
    • 拖拽期间落点高亮:行级 outline:2px dashed,节点 before/after 显示 3px 主色竖条(IslandEditorNode.vue:107-123),inside 高亮容器(:107-110)。
  4. 约束:单次拖拽内不得把父节点放进自己的后代(all([node]).some(child.id === target.node.id) → 忽略,:517);目标层级 + 被拖子树高度 ≤ 6(:518-522);组件总数 ≤ 200(capacity,:338-347)。
  5. 落库:移动完成后 selectedId 指向被拖节点、rowId 指向目标行、panel='component',立即 save()(:534-537)。
  6. ghost:仅 pointer.moved 后显示,文本为组件中文名或行名(dragging.label),偏移 +16/+16。
  7. 触摸:必须继续支持 pointerType==='touch'(现有 smoke 用合成 touch 事件验证跨行拖动,main/smoke-layout.ts:74-91);拖动时 touch-action:none、禁止页面滚动(.component-chip、.row-handle、.library-item 已设置)。

9.14 选中、组件设置抽屉(四页签)

  1. 选中:点击组件 chip → selectNode:设置 selectedId、rowId、panel='component'、打开抽屉;点击行把手/空行 → selectRow:清空 selectedId、panel='row';切换方案/常态/展开内容时 resetSelection()(IslandEditorView.vue:106-125)。选中框按 9.11-8 规格。
  2. 抽屉结构:顶部 el-radio-group(方案 / 行 / 组件);component 页签在无选中时禁用;内容区渲染 IslandComponentSettings(组件)或行/方案表单(IslandEditorView.vue:791-857)。
  3. 四页签(IslandComponentSettings.vue 重构,顺序固定):内容、外观、尺寸、隐藏条件。每页签使用 el-form label-position="top";选项选择类控件宽度 100%;数字/颜色控件与标签同一行(现有 :283-307 样式保留)。
  4. 跨页签即时保存:任一控件 @change 必须 emit('change') → 编辑器 save()(现有模式);不设「应用」按钮。
  5. 删除/复制/包裹入口:内容页底部固定「包裹到容器」下拉(data-action="wrap")、「复制组件」、「删除」(danger plain)(:152-170);复制节点时深拷贝并重生成自身与所有后代 id(copyNode,IslandEditorView.vue:180-188);包裹受层级与容量约束(wrap,:189-201)。
  6. 抽屉 DOM 契约:.component-settings、.el-tabs__item 数量为 4、.el-tab-pane 的可见页内容互斥;编辑器 smoke 会按页签断言内容(现有 3 页签断言见 main/smoke-layout.ts:111-137,重构后由 parts-r/11 同步更新为 4 页签)。

9.15 组件设置项清单(与 parts-r/07 组件契约一一对应)

字段名直接来自 packages/shared/src/island-layout.ts:77-108(settings/appearance/advanced)。默认值与取值范围以 normalizeIslandLayouts 的 clamp 为准(:268-332)。任何新增字段必须先落到 shared 契约并在 parts-r/07 说明渲染方式。

9.15.1 内容页(按 kind)

kind 控件 字段 类型/范围 默认 生效
全部 (无内容设置时)提示 — 文本「此组件使用实时数据。可在外观和尺寸设置中调整显示。」 — —
clock 12 小时制 settings.hour12 switch false 即时
clock 显示秒 settings.seconds switch true 即时
countdown 倒计时目标 settings.target select:自动 auto/下一节课 next/本节下课 end/放学 school/指定日期 date auto(编辑器新建时置为 date,makeNode IslandEditorView.vue:359-363) 即时
countdown 目标名称(仅 date) settings.countdownTitle input maxlength 64 目标日 即时
countdown 目标日期(仅 date) settings.targetDate ch-date-picker YYYY-MM-DD,必须 isDayKey 合法 '' 即时
weather 地点名称 settings.locationName input maxlength 64 '' 即时
weather 纬度 settings.latitude input-number -90–90,precision 4,允许 null null 即时
weather 经度 settings.longitude input-number -180–180,precision 4,允许 null null 即时
text 文字内容 settings.text textarea rows 3,≤2000 字 班级信息 即时
text 启用模板变量 settings.template switch;提示「支持 {date} 日期、{time} 时间、{course} 当前课程。」 false 即时
schedule 显示课程 settings.display select:当前时间点 current/下一节课 next/今日时间点 today current 即时
schedule 显示时间点数(仅 today) settings.count input-number 1–30(normalize 1–40) 5 即时
schedule 当前时间点显示起止时间 settings.showPeriod switch true 即时
schedule 当前课程显示进度 settings.showProgress switch true 即时
group/stack/slide/rolling 排列方向 settings.direction select:横向 horizontal/纵向 vertical horizontal 即时
同上 组件间距 settings.gap input-number 0–64 8 即时
slide 切换间隔(秒) settings.interval input-number 1–300 5 即时
rolling 滚动速度 settings.speed input-number 1–200 30 即时
separator/date/notification/homework/call — — 无内容参数(消息类显示只读提示) — —

9.15.2 外观页(所有 kind 通用,appearance)

控件 字段 类型/范围 默认 生效
独立外观 appearance.resourceOverridingEnabled switch false 即时;关闭时其余项禁用并沿用岛外观
辅助文字字号 appearance.secondaryFontSize input-number 8–96 14 即时
正文字号 appearance.bodyFontSize 同上 16 即时;组件级覆盖,仅 custom 使用;classisland fixed 的四档字号来自 IslandAppearance.fontSize 派生(见 9.5),本项不参与
强调文字字号 appearance.emphasizedFontSize 同上 18 即时
大号文字字号 appearance.largeFontSize input-number 8–128 20 即时
自定义文字颜色 appearance.customForegroundEnabled + appearance.foreground switch + color-picker(#rrggbb) 关 / #ffffff 即时
自定义背景颜色 appearance.customBackgroundEnabled + appearance.background switch + color-picker 关 / #202024 即时
自定义背景透明度 appearance.customBackgroundOpacityEnabled + appearance.backgroundOpacity switch + input-number 0–1 step 0.05 关 / 0.5 即时
自定义圆角 appearance.customCornerRadiusEnabled + appearance.cornerRadius switch + input-number 0–100(ClassIsland 上限 20 用于岛;组件可为 100) 关 / 16 即时
组件透明度 appearance.opacity slider 0–1 step 0.05 1 即时

9.15.3 尺寸页(原「高级」拆分,advanced)

控件 字段 类型/范围 默认 生效
对齐方式 advanced.align select:拉伸 stretch/起始 start/居中 center/末端 end stretch 即时
固定宽度(px) advanced.width input-number 0–2000(0=自适应) 0 即时
最大宽度(px) advanced.maxWidth input-number 0–2000(0=不限制) 0 即时
提示 — 「宽度为 0 时自适应;对齐在父容器设定宽度时生效。」 — —

9.15.4 隐藏条件页

控件 字段 类型/取值 默认 生效
隐藏条件(多选) advanced.hideRules checkbox-group:上课时 in-class/没有消息时 no-message/没有当前课程时 no-class [] 即时;数组只保留这三个合法值(normalize :321-328)
高级说明 — 文本「隐藏条件基于当前课堂与消息状态即时生效;不会释放组件资源。」 — —

ClassIsland 对齐:ClassIsland 用「按规则隐藏 + 规则集」承载同一语义(repos/ClassIsland/ClassIsland/Views/SettingPages/ComponentsSettingsPage.axaml:423-443);本轮不引入规则集引擎,保留 ClassHelper 三枚举,标注【映射】。若 parts-r/05 增补 advanced.hideRules 扩展,必须同步本页与 parts-r/07。

9.16 预览与「进入编辑模式」

  1. editMode 为页面内状态(ref(false)),不打开额外桌面窗口、不注入 iframe(docs/app/island.md:47);smoke 会断言不存在 .layout-editor/iframe(main/smoke-layout.ts:69-72、:154)。
  2. 进入:工具条「进入编辑模式」→ editMode=true;画布切换为 .island-preview 真实组件渲染:ComponentNode 以 :node :state :now :schedules :time-points editing :selected-id alert-node-id="" 渲染,state 使用本地空状态对象(IslandEditorView.vue:47-57、:719-732);容器组件在编辑时展示全部子组件(ComponentNode 的 editing 分支由 parts-r/07 定义)。
  3. 预览缩放:仅在编辑模式显示 .preview-toolbar:文本「预览缩放」+ el-slider 0.3–1.5、step 0.1、tooltip % + 「重置缩放」(置 1)+ 提示「点选组件调整内容、外观和隐藏条件」(:739-750);缩放与方案缩放相乘(zoom: scheme.scale * zoom)。
  4. 布局约束:preview-canvas 必须完整容纳预览且不遮挡工具条——canvas.bottom <= footer.top + 1 且页面无横向滚动(smoke 断言 main/smoke-layout.ts:138-148);separated 时按 9.9-8 渲染。
  5. 点击编辑态组件:与标签态一致打开抽屉(@select);背景点击(.preview-row @click.self)选中该行。
  6. 退出:再次点击「退出编辑模式」回到标签态;抽屉保持打开状态但选中不变;Esc 优先关闭抽屉、再退出编辑模式(见 9.17)。
  7. 实时同步真实岛:编辑器每次 save() 通过 saveConfig 触发主进程 island.syncCustomLayout()(main/ipc.ts:34-45;main/island.ts:320-338),真实岛收到 island:layout 后热更新(LayoutIslandApp.vue:35-51);因此“预览”= 页面内预览 + 真实岛热更新,不新增窗口。

9.17 键盘、触摸与焦点

场景 按键 行为
编辑器全局 Esc 抽屉打开→关闭抽屉;否则编辑模式→退出编辑模式;否则返回 /settings/island(需保存成功后)
选中组件 Delete/Backspace 删除选中组件(无后代时直接删;有后代弹确认「删除后其子组件一并删除」,确认后删);删除后 panel='row'
选中组件 Ctrl+D/Cmd+D 复制选中组件(同「复制组件」,受 200 上限约束)
选中组件/行 Enter/Space 打开组件设置/行设置抽屉
行把手/组件 chip Tab → Enter 键盘聚焦并选中;拖动需指针;不提供仅键盘拖动(标注【推断】,作为 P2 可加方向键移动)
抽屉 Tab 循环限制在抽屉内 el-drawer 自带焦点陷阱,保留
设置页 无全局快捷键 滑块可用方向键微调(Element Plus 默认)

触摸:所有拖拽继续使用 Pointer Events(鼠标/触摸/笔统一);触摸时不得触发页面滚动与文本选择;touch-action:none 只加在可拖元素;抽屉在窄屏(<520px)宽 94vw。焦点可见性:所有自绘按钮必须有 :focus-visible 2px 主色 outline + 2px offset(现有 .library-item/.row-handle/.surface-tabs button 规则保留,island-editor.css:305-310)。

9.18 数据流、契约与失败处理

  1. 读取:编辑器 load() → window.desktop.getConfig() → settings = normalizeIslandLayouts(config.islandLayout)、appearance = config.island;失败显示错误条并提供「重试」(现有 load, IslandEditorView.vue:300-313)。设置页同时读 getConfig() 与 islandGetAppearance()。

  2. 保存队列(编辑器):每次改动 save():

    • snapshot = normalizeIslandLayouts(JSON.parse(JSON.stringify(settings)))(必须先深拷贝纯对象,禁止 Proxy 过桥);
    • revision++、pending++、saved=false;
    • 串行 promise 链 saveConfig({ islandLayout: snapshot });成功且 current===revision 时清 error、saved=true;失败写 error、saved=false;finally pending--(现有 :81-105)。
    • 状态文本固定四态:error → 「保存失败」;pending>0 → 「正在保存…」;saved → 「已自动保存」;否则「更改自动保存到本机」(:543-546)。
  3. 退出保存:onBeforeRouteLeave(async () => { if (!ready) return true; await save(); return !error })——有错不发离开(:333-337);「应用并返回」apply():强制 settings.layout='custom'、residentEnabled=true,await save(),无错误才 ElMessage.success('自定义灵动岛已应用') 并 router.push('/settings/island')(:290-299)。

  4. 失败重试:错误条 重试 按钮按 ready ? save() : load();保存失败不得清空本地编辑内容;不得自动回滚到磁盘值(避免用户输入)

    丢失;若连续失败需在控制台/日志打出 IPC 错误(沿用主进程 logger)。

  5. 应用/保存语义:编辑器每次改动即 saveConfig(含磁盘合并写 120ms,main/config.ts:252-298),所以「退出保存」不是唯一写入点;保存成功≠已点“应用”,但配置已写入 islandLayout。layout 只有在「应用并返回」或设置页选择自定义时才切到 custom。

  6. 主进程侧:ipcMain.handle(IPC_CHANNELS.saveConfig) → saveConfig(allowed) → 若 allowed.islandLayout 则 island.syncCustomLayout()(main/ipc.ts:34-45);syncCustomLayout() 在常驻开关切换时重置 state,向岛渲染进程 send('island:layout', getConfig().islandLayout)(main/island.ts:320-338);岛渲染层 onLayout 更新(LayoutIslandApp.vue:35-51)。

  7. 外观通道:island:set-appearance 主进程 16ms 合并后 island.setAppearance(main/island.ts:2389-2410);island:get-appearance 会先 flush 再返回(:2407-2410)。外观不落盘,除非设置页点保存写 config.island。

  8. 迁移:config.ts 读盘时 normalizeIslandLayouts + normalizeIslandAppearance(:198-233)。老用户(无 island.family)→ 'winisland';新装默认 'classisland'。实现方式(二选一,必须由 parts-r/05 定稿):

    • 在 readPersistedFromDisk 里判断 parsed.island 是否存在且 family 缺省 → family:'winisland';或
    • islandLayout.version < 2 且 config.island 存在 → 迁移为 winisland,并写 version=2。 fontSize 迁移规则:旧配置已有 island.fontSize(例如 13)时原样保留,不得因 family 从 classisland 变 winisland(或反向)而改写;旧配置没有该字段时才按迁移后的 family 取默认(winisland=13 / classisland=16)。新安装/清空配置:family='classisland' 且 fontSize=16。DEFAULT_ISLAND_APPEARANCE 的 fontSize 默认值需按此改为 16,旧默认 13 仅作为 winisland 语义说明保留在文档与文案中。 迁移失败必须回落默认配置并记日志,不得让设置页崩溃。
  9. 禁止:在渲染进程直接写 config 文件;在编辑器内创建第二个 BrowserWindow;把 ref/reactive 直接传 IPC;用 setBackgroundMaterial() 影响岛透明(AGENTS.md 客户端章节)。

  10. 契约对照(必须在 PR 中核对):本 part 用到的字段均来自 IslandComponentNode.settings/appearance/advanced、IslandLayoutRow、IslandLayoutScheme、IslandLayoutSettings、IslandAppearance(shared/src/island-layout.ts:61-158、shared/src/types.ts:1146-1188);新增字段以 parts-r/05 为准,组件渲染细节以 parts-r/07 为准,消息/课程提醒视觉以 parts-r/08 为准。

9.19 状态、空态、主题与性能约束

  1. 加载/空态:
    • ready=false 前只渲染页头/方案栏骨架,禁止允许编辑(避免对未加载的 scheme 写入);编辑器用 el-skeleton :rows="8",设置页用 v-loading 或同款骨架;
    • settings.schemes 由 normalizeIslandLayouts 保证至少 1 个(空输入回落默认方案,shared/src/island-layout.ts:399-402);若运行期出现空数组(异常导入),必须立即用 defaultIslandLayouts().schemes[0] 补位并提示「方案数据异常,已恢复默认方案」;
    • 行无节点时显示空行占位;容器无子组件时显示「拖入组件」(IslandEditorNode.vue:47);未选中组件时抽屉「组件」页签禁用;
    • 删除当前选中节点后,panel 回到 row、selectedId 清空;切换方案/常态/展开内容时全部选中态清空(现有 resetSelection,IslandEditorView.vue:119-125)。
  2. 错误态:load() 失败(非桌面客户端打开、getConfig 抛错)→ 页面显示 el-alert type="error" + 错误文本 + 重试;编辑功能全部不可用。保存失败只显示错误条与状态文本,不弹全局遮罩,不丢失编辑内存。
  3. 主题与关怀模式:设置页与编辑器必须只使用 --ch-* 令牌与 var(--ch-font-scale) 缩放文字(styles/index.css:21-131、:103),不得写死浅色/深色;深色模式由 html.dark 变量覆盖,且 smoke-layout.ts:149-157 会断言深色下 .island-editor-page 的 --ch-text 存在。例外:编辑器的岛预览区(.island-preview)固定使用岛自身底色 #202024 与 #ffffffeb 文字,以真实预览岛外观,不跟随设置页主题;该区域的组件颜色仍遵循 appearance 覆盖。关怀模式放大字号时,组件库名称/描述、按钮文字跟随 --ch-font-scale,但抽屉宽度、行高、组件外边距等几何令牌不变(AGENTS.md §5「关怀模式放大字号/控件,不改变几何令牌」)。
  4. 响应式:编辑器按现有断点:≤1150px 组件库 3 列、≤800px 2 列且方案工具区换行、≤520px 1 列并收窄页面左右 padding(island-editor.css:371-417);设置页在 ≤680px 时布局选择卡改单列(IslandSettingsView.vue:497-505),抽屉宽度 min(360px, 94vw) 自适应。
  5. 性能:拖动 pointermove 只在 dragging 存在时处理,且必须 passive:false 才可 preventDefault(现有 { passive: false },IslandEditorView.vue:315);保存用串行队列 + revision 去陈旧结果,禁止每次输入都直接 await getConfig();节点上限 200/行 12/方案 20/深度 6 必须在写入前检查而不是写入后裁剪;编辑页不得创建额外 BrowserWindow、不得轮询 getConfig(只 focus 时刷新一次设置页)。
  6. 无障碍:所有自绘可点元素使用原生 button 或 role="button" + tabindex="0";保留现有 aria-label(新建/重置/管理方案、添加组件、编辑组件、编辑行);拖拽不是唯一操作路径,必须同时提供按钮/下拉的等价操作(添加组件、上移/下移、包裹、复制、删除),以满足键盘与触屏用户。

9.20 既有实现与测试契约(重构不得破坏)

以下行为/文案/DOM 已被现有冒烟与代码依赖,重构时必须保留或由 parts-r/11 同步更新测试:

  1. 路由:/settings/island、/settings/island/editor(router/index.ts:63-74);侧栏「灵动岛」入口(ClientLayout.vue:48-55);岛内「编辑」通过 onIslandEditorOpen 事件把主窗口切到编辑器(ClientLayout.vue:23-26;主进程 main/index.ts:229-235)。
  2. 编辑器:.component-library 首组 11 个 .library-item;按钮文本「新增行」「行设置」「进入编辑模式/退出编辑模式」「应用并返回」「返回设置」;data-row/data-node/data-library-kind/data-action=wrap;.component-settings .el-tabs__item 页签;.preview-canvas/.preview-toolbar/.island-preview(main/smoke-layout.ts:40-73、:111-148、:186-226)。
  3. 方案:aria-label 为「新建方案」「重置当前方案」「管理方案」;下拉项文案「方案设置/复制方案/导入方案/导出方案/删除方案」;导入 format 字符串 classhelper-island-layout;导入 ≤1 MB;新建/复制重生成 id;删除保持至少 1 个方案(:264-360)。
  4. 设置页:.layout-choice 两张卡、编辑岛内组件 按钮、指针避让开关、保存/预览/恢复默认按钮与实时预览(IslandSettingsView.vue:170-397)。
  5. 保存队列行为与状态文本(9.18-2);失败重试按钮;应用并返回 成功后才离开。
  6. 这些断言若因重构改变(例如组件设置页签从 3 变 4),必须在同一 PR 更新 parts-r/11 的冒烟脚本,禁止只改实现不改测试。

9.21 验收标准

实现完成后逐项验证并记录结果(全部通过才算完成;验证必须无头/离屏,禁止启动可见岛做验收,除非显式 --visual):

  1. family 与默认:新配置(清空 config)打开设置页,family 默认 ClassIsland 风格 且 appearance.fontSize=16、标签「正文字号(ClassIsland 基准)」;老配置(人为删除 island.family)重载后自动为 经典 WinIsland(兼容) 且外观与升级前一致(对比截图/配置 JSON),其原有 fontSize(如 13)保持不变、标签「岛字号(经典)」。
  2. fixed 语义:family=classisland、layout=fixed 时,岛外观为信息行(行高 40、圆角 8、背景不透明度 0.5、底部 3px 进度条),默认显示日期 + 课程表;不出现胶囊/详情卡;来消息时在同一条内展开气泡;family=winisland 时恢复胶囊 + 详情卡。layout=fixed 时设置页不得出现任何行/组件编辑入口(fixed v2 为只读内置方案)。
  3. 外观参数:classisland 族 22 项、winisland 族 15 项逐一改动——拖动时岛即时变化(≤1 帧合并),点保存后 config.json.island 落盘;重启客户端后保持;非法值(如缩放 9、圆角 -1、颜色 red)被钳制/回落,不出现 NaN。字号语义:classisland 下改「正文字号(ClassIsland 基准)」时正文 = 该值、次级/强调/大号按 14/18/20 阶梯派生,默认 16;winisland 下标签为「岛字号(经典)」、默认 13;两种 family 间切换标签与默认值提示正确、不强制改写已有值。
  4. custom 与 family:layout=custom 时 family 控件禁用并显示「自定义方案始终使用 ClassIsland 规格;外观族只作用于经典固定形态。」;custom 的渲染始终为 ClassIsland 规格(不因 family=winisland 回退旧观感);设置页「圆角/不透明度/行间距」改动同时写入 schemes[activeSchemeId] 并落盘;「界面缩放」在 custom 下隐藏并有说明;schemes/rows/nodes 编辑器只服务 custom,fixed 无编辑入口。
  5. 课程提醒:5 个开关 + 2 个阈值改动即时保存到 config.json.islandLayout.lessonReminders(唯一数据源);阈值越界被钳制到 5–1800 / 5–600;/settings/reminder 只显示只读摘要 + 跳转链接,页面内无可编辑的上课提醒控件。
  6. 编辑器布局:编辑器页 padding 12/24/16;行最小高 46(标签态)/40(预览态);行距等于 scheme.spacing;组件外层左右各 6px;选中框 1.5px 虚线 1,1.5、opacity .75;行把手 20px;「新增行」在工具条与行列表末尾均可用。
  7. 四页签:组件设置抽屉依次为「内容/外观/尺寸/隐藏条件」;11 类组件的控件、默认值、范围与 9.15 表一致;切换组件时回到「内容」页。
  8. 拖拽:组件库→行、组件库→容器(20%–80% 区域)、行内排序、跨行移动、行排序均可用(鼠标与触摸各测一次);拖动阈值 6px;拖动父组件到自身后代被拒绝且数据不变;嵌套超过 6 层、组件超过 200、行超过 12、方案超过 20 均给出提示且不写入。
  9. 方案:新建(提示输入,1–64 字,最多 20 个)、复制(名称加「副本」、id 全量重生)、重置(保留 id/name)、删除(确认,至少保留 1 个)、导入(≤1 MB,format 校验,id 重生)、导出(文件名非法字符替换、可再导入)全部通过;activeSchemeId 始终有效。
  10. 保存与失败:连续快速拖动/输入时状态文本按「正在保存…」→「已自动保存」变化;人为让 saveConfig reject(mock IPC)时显示「保存失败」+ 错误条「重试」,且未点应用不允许离开;重试成功后错误清除。
  11. 编辑模式:进入后页面内渲染真实组件(无额外窗口/iframe),缩放 0.3–1.5 生效且「重置缩放」回 100%;预览不遮挡工具条、无横向滚动;退出后回到标签态;真实岛在保存后热更新(island:layout 生效)。
  12. 键盘/触摸:Esc 逐层退出(抽屉→编辑模式→返回);Delete 删除选中组件(有子组件先确认);Ctrl+D 复制;所有自绘按钮有 :focus-visible 焦点环;触摸拖动不触发页面滚动。
  13. 无回归:9.20 列出的 DOM/文案/路由/导入格式全部保持;现有冒烟脚本(若未按 9.20-6 更新)中的编辑器断言通过;未新增桌面窗口、未改 main/island.ts 的透明/命中/几何逻辑;pnpm typecheck 与 pnpm lint 通过(不执行构建/运行验证);9.19 的状态/空态/主题/响应式行为逐一验证。

10. 动画与动效映射(ClassHelper 现状 → ClassIsland 目标;双风格默认值)

本节是动效施工图。实现者必须先把「风格开关」接好,再按表逐条替换动画:family='classisland'(默认)走 ClassIsland 规格的 CSS transition/keyframes + 遮罩分块;family='winisland'(老用户兼容)保留现有弹簧形变与胶囊开合。同一条动画不得同时用两套实现,由 data-family 与 appearance.animations 决定分支。

10.0 总则与不可协商的规则

  1. 动画级别只有一个开关:岛的动画由 appearance.animations(boolean)总控,不是 ClassIsland 的 AnimationLevel(那是主窗口的设置)。animations=false 时所有过渡必须「瞬落」:弹簧 snap()、CSS transition/animation: 0ms、遮罩直接设终值(现有实现见 IslandApp.vue:249-256、LessonReminderContent.vue:77-80)。
  2. 速度倍率:appearance.speed(0.5–2)按比例缩放所有时长:实际时长 = 基准时长 / speed。现有实现已贯穿:弹簧 dt × speed(IslandApp.vue:291)、窗口淡入 IslandApp.vue 之外的 --island-dur = 320/max(.25,speed)(:528)、课程条 --ci-duration = 200/speed 等(LessonReminderContent.vue:134-137)。新增动画一律除以 speed。
  3. 弹簧仅限 winisland:family='classisland' 的几何变化用 CSS transition;family='winisland' 继续用 SpringValue(stiffness 0.10 / damping 0.68 / 换向保留 35% / 单帧 ≤ 剩余 20% / snap 0.05px / 零过冲钳制,packages/desktop-client/src/island/spring.ts:26-114)。不得在 classisland 分支引入弹簧(Lead 裁决,parts-r/01-objective.md:52)。
  4. 窗口几何不动画:开合动画全部发生在固定包围盒内部(main/island.ts:1412-1417)。classisland 的 fixed 信息行也必须沿用这一约束:行宽/气泡变化在渲染层完成,窗口只在「不可见时」或「模式/设置变化」时 setBounds(island.ts:1476-1479,1598-1612)。fixed 单条气泡的行高恒为 40px,气泡(遮罩/内容/进度条)全部在 40px 行内完成,不因内容改行高、也不因此扩窗;maxCardHeight 只用于 custom 的多条消息列表与强提醒窗口的尺寸计算。唯一例外是「多条消息列表」比普通形态更高,窗口必须在形变开始前先长高(island.ts:1514-1518),与单条气泡无关。
  5. 不可打断的动画可被新状态覆盖:新状态直接接管(取消旧 rAF/定时器、重设目标),不得排队、不得闪回。现有 syncTargets(snap)、animate()、animateMask() 都按此实现(IslandApp.vue:238-264、CustomIslandRuntime.vue:127-177、LessonReminderContent.vue:73-102)。
  6. 隐藏页/窗口不可见必须暂停:custom 的 motion = animations && !document.hidden && nativeVisible(CustomIslandRuntime.vue:57-58);classic 通过 props.suspended 停 rAF(IslandApp.vue:240-246)。新增动画(遮罩、气泡、强提醒)同样要遵守:页面隐藏时不得继续跑 rAF;恢复可见时按当前状态直接落位(不要补播)。
  7. 减少动态效果(prefers-reduced-motion)不覆盖岛:ClassHelper 明确「应用内动画开关决定岛动画,不能用主窗口的减少动态效果说明替代岛自身规则」(docs/app/island.md:57;docs/dev/motion.md:78)。岛动画的唯一开关是 appearance.animations,appearance.speed 统一缩放时长;实现者不得让系统 reduced-motion 强制关闭岛动画或改写其时长,只在设置页给出提示文案。若未来用户明确要求「尊重系统设置」,只能作为 P2 可选项(设置项 respectReducedMotion,默认 false),默认行为保持不变。
  8. 帧预算:岛渲染进程 60fps;任意单帧主线程 ≤ 8ms;动画只作用于 transform/opacity/clip-path/box-shadow(颜色类)与 SVG points(遮罩),禁止逐帧改 width/height/top/left 触发重排(弹簧形变改的是 SVG path 与元素尺寸,属于既有例外,仅 winisland 使用)。
  9. 兜底定时器:任何依赖 rAF 的动画必须有「到点直接落位」的兜底,因为 Windows 会把置顶透明小窗判定为被遮挡而把 rAF 压到 1fps(IslandApp.vue:266-286 的 MORPH_FALLBACK_MS=1200)。classisland 的 CSS transition 天然有终点,但遮罩(SVG 分块)与任何 JS 驱动的动画仍需兜底(现有 600/speed,LessonReminderContent.vue:82-89)。
  10. 动画结束清场:动画结束必须:移除临时 will-change、取消 rAF/定时器、移除 closing 等临时类;不得留下持续运行的循环(12-acceptance.md:73)。

10.1 ClassHelper 现状动画清单(重构前基线,必须保留语义)

编号 现状动画 触发 当前实现 关键数值/证据
S1 classic 胶囊↔展开卡形变 消息展开/收起 SpringValue 逐帧逼近 width/height/radius 目标尺寸见 IslandApp.vue:188-204;圆角胶囊 min(radius,h/2)、展开 min(48×radius/20,w/2,h/2)(:207-212);弹簧参数 spring.ts:26-30
S2 内容交叉淡入 形变过程中 胶囊层 1-1.5p、展开层 p² IslandApp.vue:346-351;progress 由高度差推导(:304-309)
S3 收回动画 点「知道了/标为已读」 形态先切回胶囊,360ms 后淡出隐藏 CLOSE_ANIMATION_MS = 360(main/island.ts:114-121)
S4 窗口淡入/淡出 显示/隐藏 主进程逐帧改 setOpacity 淡入每 16ms +0.16;淡出每 16ms −0.2,≤0.02 后 hide()(island.ts:1674-1715,island.ts:1686,1705)
S5 custom 形变 展开/收起/课程条切换 width/height/radius + content/lesson 进度弹簧 CustomIslandRuntime.vue:88-177;snapThreshold:0.001(:92-93);兜底 1200/speed(:156)
S6 custom 页面交叉淡入 常态↔展开↔课程 compactAlpha=1-1.5p、detailsAlpha=p²、lessonProgress CustomIslandRuntime.vue:99-100;三页 opacity/visibility 绑定(:385-511)
S7 custom 测量适配 内容/字体/视口变化 ResizeObserver + nextTick 量 offsetWidth/offsetHeight + fitted() CustomIslandRuntime.vue:72-80,238-266
S8 课程提醒遮罩 准备/上课/下课/下课前 SVG 5 个多边形按进度重绘 区段 360ms、开错峰 [0,90,120,90,0]、关 [120,90,0,90,120]、总 480ms、开 1-(1-p)^4、关 p²(LessonReminderContent.vue:59-102)
S9 课程提醒标题 遮罩出现/消失 CSS animation 入 250ms 延迟 260ms (0.25,1,0.5,1);缩放 750ms 延迟 260ms;出 200ms linear 反向(:134-137,273-280)
S10 课程提醒进度条 每秒更新 transform: scaleX transition 50ms linear(LessonReminderContent.vue:307-316,高度 2px)
S11 组件行课程进度 每秒更新 transform: scaleX transition 1s linear(ComponentNode.vue:348-362,高度 2px)
S12 滚动组件 文字超宽 CSS keyframes rolling 30%→−80%,时长 max(1,200/speed) 秒,linear infinite;编辑时停(ComponentNode.vue:205,384-392,446-453)
S13 轮播组件 定时切换 计算当前索引,瞬时替换 interval 秒(ComponentNode.vue:121-130;island-layout.ts:196 默认 5s)
S14 课程徽标 下课前 静态圆形 30×30、#0a84ff、12px/600、tabular-nums(lesson-ending.css:3-16)
S15 徽标入场 铃铛计数出现 CSS animation resident-indicator-in 200/speed ms fade(custom-runtime.css:147-149,199-206)
S16 课件覆盖 课件开始/结束 瞬时显示/隐藏,无动画 CoursewareTransfer.vue(无 transition)
S17 悬停/按压 指针悬停、按下 CSS transition 按钮/列表行 140ms ease-out(classic-island.css:393,480,613-616,699-702);按下 scale(.97)(:724-727);仅 hover 设备(:736-750)
S18 编辑预览 打开编辑器 静态排版,无动画 island-editor.css:193-227(预览行 44px 高)
S19 外观变化 设置页改宽高/字号/配色 重写 CSS 变量,瞬时生效 IslandApp.vue:175-176,516-528;CustomIslandRuntime.vue:102-124
S20 失焦宽限 展开后失焦 宽限期内不收起(非动画但影响观感) 鼠标 500ms、触摸 1200ms(island.ts:2287,2297)

10.2 映射总表(目标规格;每条给出双风格默认值)

说明:CS = family='classisland'(默认),WI = family='winisland'(兼容)。「实现」列 CSS-T 表示 CSS transition、CSS-A 表示 CSS keyframes/WAAPI、SVG-rAF 表示 SVG/Canvas 按进度重绘、Spring 表示 SpringValue 帧积分、主进程 表示 Electron 主进程逐帧/定时。

A. 岛出现 / 隐藏 / 开合

classisland fixed 下「岛」不再是胶囊,而是一条信息行:出现/隐藏必须轻、快、不抢注意力,避免每来一条消息就在屏幕顶部弹一个大卡片。开合类动画只允许改变行背景层与不透明度,内容层文字不得跟着宽度动画重排(否则会出现「文字先铺开再被省略号收回」的抖动,ClassHelper 的 classic-island.css:196-206 已记录同类问题)。winisland 分支的胶囊开合仍按原有弹簧语义执行,不得为了「统一」把弹簧搬到 classisland。

fixed 行高恒 40px(硬约束):气泡的遮罩、正文、进度条全部在 40px 行内完成;正文最多 2 行、超出用省略号(display:-webkit-box; -webkit-line-clamp:2; overflow:hidden),不得出现「行高随内容增高」「行高 200ms 过渡」「为气泡扩窗」这类 ClassHelper 旧展开卡行为。maxCardHeight 只参与 custom 的多条消息列表与强提醒窗口尺寸计算,单条气泡不读它。

编号 场景 触发 属性 起止值 时长 曲线 延迟/交错 实现 降级 现状对照
A1 CS fixed 岛出现 首次显示/新消息首次弹出 opacity、translateY 0 / 8px → 1 / 0 250ms cubic-bezier(.25,1,.5,1) 0 CSS-T animations=false 直接落位 无(新)
A2 CS fixed 岛隐藏 消息清空且无课程 opacity 1 → 0 150ms cubic-bezier(.16,1,.3,1) 0 CSS-T 后主进程 hide()(隐藏帧) 无动画直接 hide 对应 S4
A3 CS 行宽度变化 内容/字号/组件变化 width(行背景层) 当前 → 目标 300ms(Fluent)/550ms(Classic) (.65,0,.35,1) / (0,1,.5,1.1) 0 CSS-T(背景层绝对定位,文字层瞬时) 直接落位 对应 S19
A4 CS 单条气泡布局(行高固定) 单条消息出现/内容变化 行高(不变)、正文行数 恒 40px;正文 0→2 行(超出省略),内容在行内完成遮罩/进度 0(不改行高) — — 固定 --ci-row-h;-webkit-line-clamp:2;不扩窗 同(直接布局) 对应 S3/S8;不得沿用旧展开卡增高行为
A5 CS 分体行出现 开启 separated 行间距、圆角 0 → 5px、0 → 8px 200ms (.16,1,.3,1) 每行 0 CSS-T 直接落位 无(新)
A6 WI 胶囊↔展开 展开/收起 width/height/radius 胶囊 w×h×r(h/2) ↔ 展开 w+156 × h+186 × min(48r/20) 由弹簧决定(约 300–500ms) 弹簧(近临界阻尼,无过冲) 0 Spring animations=false → snap() S1
A7 WI 内容交叉淡入 同 A6 两层 opacity 胶囊 1−1.5p;展开 p² 跟随 p 线性映射 0 JS 逐帧绑定 直接显示终态 S2
A8 WI 收回并淡出 点知道了/标为已读 形态→胶囊,再窗口淡出 展开 → 胶囊;opacity→0 360ms 形态 + 淡出(−0.2/16ms) 弹簧 + 线性 360ms 后淡出 Spring + 主进程 立即 hide S3/S4
A9 WI 窗口淡入 显示 窗口 opacity 当前 → displayOpacity 每 16ms +0.16(约 60–110ms) 线性 0 主进程 立即设置 S4
A10 WI 窗口淡出 隐藏 窗口 opacity 当前 → 0 每 16ms −0.2(约 50–80ms) 线性 ≤0.02 后 hide() 主进程 立即 hide S4
A11 上课时段隐藏 进入上课且无紧急/叫人 窗口可见性 可见 → 隐藏 0(立即) — — 主进程 hideImmediately() 同 island.ts:1463-1473

B. 行内布局与组件

classisland 信息行的内部布局必须「稳」:数据每秒变化时只换文本、不动布局;只有组件增删、方案切换、拖拽排序时才允许位移类动画。行内所有组件共用 --ci-fs-* 与 --ci-row-h,任何组件的默认尺寸都不得把行撑高(超高时内部滚动或省略,见第 7 部分)。

编号 场景 触发 属性 起止值 时长 曲线 延迟/交错 实现 降级 现状对照
B1 CS 组件淡入 组件首次挂载/由隐藏转可见 opacity 0 → 1 300ms (.65,0,.35,1) 0 CSS-T 直接显示 对应 ClassIsland 组件淡入(repos/ClassIsland/.../ComponentPresenter.axaml.cs:224-243)
B2 CS 首次多行错峰入场 岛首次显示且 ≥2 行 opacity、translateY 0 / 6px → 1 / 0 320ms (.16,1,.3,1) 每行 30ms,封顶 180ms CSS-A(按行 index 计算 delay) 跳过 对应 ClassIsland 列表错峰(25ms/项,StackPanelIntroAnimationBehavior.cs:74-90);本处取 30ms 更克制
B3 CS 组件重排过渡 编辑器中拖动排序 transform 旧位置 → 新位置 225ms CubicEaseInOut≈(.65,0,.35,1)【推断】 0 FLIP(先读后写) 直接跳位 对应 WrapPanelResizingAnimationAssist.cs:45-49
B4 数据文本刷新 时钟/日期/倒计时每秒 文本内容 — 0 — — 直接替换(tabular-nums) 同 无动画是有意的(ClassIsland 也不做数字翻转)
B5 CS 消息气泡宽度 单条气泡换行/字数变化 宽度 当前 → 内容宽 200ms (.16,1,.3,1) 0 CSS-T 直接落位 无(新)

C. 进度条

进度条是岛内唯一「高频更新」的动画元素:它必须用 transform: scaleX() 而非宽度,且每次更新只触发合成层。ClassIsland 的倒计时进度用 50ms 线性过渡(repos/ClassIsland/ClassIsland/XamlThemes/FluentTheme/Styles.axaml:137-145),classisland 风格沿用;winisland 的课程进度保持 1s 线性(每秒一 tick),两者视觉一致但实现成本不同。

编号 场景 触发 属性 起止值 时长 曲线 延迟 实现 降级 现状对照
C1 提醒倒计时进度 剩余时间更新 transform: scaleX 旧比例 → 新比例 50ms 线性 0 CSS-T 直接落位 对应 ClassIsland FluentTheme/Styles.axaml:137-145
C2 CS 行课程进度 每秒更新 scaleX 旧 → 新 50ms(每 50ms 更新一次时长)或 1s linear(每秒更新) 线性 0 CSS-T 直接落位 现状 1s linear(S11),ClassIsland 50ms;默认选 50ms,若性能紧张可切 1s(见 §10.3 选择点)
C3 WI 课程进度 每秒更新 scaleX 旧 → 新 1s 线性 0 CSS-T 直接落位 S11 保留
C4 课件进度 传输事件 原生 <progress> 旧 → 新 150ms 线性 0 CSS-T(progress 值即时,条宽过渡) 直接落位 S16
C5 进度条高度 组件挂载 height 2px → 3px(CS) 0(静态) — — 静态 token --ci-progress-h 同 ClassIsland 3px;现状 2px

D. 通知气泡两阶段 / 斜切遮罩(classisland 默认核心)

这是本轮重构最核心的动效。ClassIsland 的提醒是「先遮罩、后内容」的两阶段:斜切分块从两侧向中间扫入,文字延迟 260ms 再淡入并轻微缩放,最后背景投影浮现;退出时反向收拢并放大退出。ClassHelper 的 LessonReminderContent.vue 已经实现过一版(edges [0,.1,.3,.7,.9,1] 等价于权重 [0,.1,.2,.4,.2,.1]),本轮必须把它抽成公共组件并让消息气泡复用,不得再写第二份。

编号 场景 触发 属性 起止值 时长 曲线 延迟/交错 实现 降级 现状对照
D1 遮罩扫入(开) 气泡/课程提醒出现 5 个区段宽度进度 0 → 1 每段 360ms QuarticEaseOut(1−(1−p)^4) 区段错峰 [0,90,120,90,0],总 480ms SVG-rAF 或 CSS clip-path 直接设 1 S8(现有实现一致)
D2 遮罩收拢(关) 气泡/提醒消失 5 个区段 1 → 0 每段 360ms QuadraticEaseIn(p²) 错峰 [120,90,0,90,120],总 480ms SVG-rAF 直接设 0 S8
D3 遮罩形状 任意宽度 5 段权重、斜切角 权重 [0,.1,.2,.4,.2,.1];偏移 tan(30°)×40 — — — SVG polygon / clip-path 同 S8(现有 edges [0,.1,.3,.7,.9,1] 等价)
D4 遮罩文字淡入 遮罩打开 opacity 0 → 1 250ms (.25,1,.5,1) 延迟 260ms CSS-A both 直接显示 S9
D5 遮罩文字缩放 遮罩打开 scale 1.1 → 1.0 750ms (.25,1,.5,1) 延迟 260ms CSS-A 直接落位 S9
D6 遮罩文字淡出 遮罩关闭 opacity、scale 1/1.0 → 0/1.1 200ms 线性(透明度)/(.5,0,.75,0)(缩放) 0 CSS-A 直接隐藏 S9
D7 遮罩投影淡入 遮罩打开 box-shadow 无 → 0 8px 32px rgba(0,0,0,.4) 100ms 线性 延迟 600ms CSS-T 直接到位 ClassIsland FluentTheme:355-367
D8 遮罩底色淡入 遮罩打开 背景 overlay opacity 0 → 1 100ms 线性 延迟 600ms CSS-A 直接到位 FluentTheme:355-367
D9 覆盖层淡入 遮罩打开后正文/详情出现 opacity 0 → 1 200ms SineEaseIn≈(.12,0,.39,0)【推断】 0 CSS-T 直接显示 ClassIsland FluentTheme:419-451
D10 覆盖层淡出 正文结束/关闭 opacity 1 → 0 200ms SineEaseIn 0 CSS-T 直接隐藏 同上
D11 Classic 遮罩(主题=classic) 同 D1/D2 translateY −50 → 0(入)/0 → 50(出) 200ms QuadraticEaseIn/Out 出延迟 100ms CSS-A 直接落位 ClassIsland ClassicTheme/Styles.axaml:265-331
D12 Classic 文字淡入/淡出 同上 opacity 0→1(100ms 延迟 200ms)/1→0(100ms) 100ms 线性 见说明 CSS-A 直接落位 同上
D13 多行气泡错峰 列表 ≥2 条 opacity、translateY 0 / 6px → 1 / 0 200ms (.16,1,.3,1) 每行 30ms,封顶 120ms CSS-A 直接显示 无(新增,参考 ClassHelper 主窗口列表错峰)
D14 气泡内进度条 倒计时更新 scaleX 旧 → 新 50ms 线性 0 CSS-T 直接落位 对应 C1
D15 气泡图标/徽标入场 气泡出现 scale、opacity .9/0 → 1/1 150ms (.16,1,.3,1) 0 CSS-A 直接显示 无(新增)
D16 关闭后进入下一批 队列非空 遮罩重播 同 D1/D2 同 同 先 D2 再 D1(交叉 100ms) SVG-rAF 直接切 ClassHelper 队列现为「直接换内容」,需补过渡
D17 关闭并淡出 队列清空 遮罩 + 窗口 遮罩收拢 + opacity→0 480 + 150ms 同 遮罩结束后淡出 SVG-rAF + CSS-T 直接 hide 对应 S3(WI 用 360ms)

E. 强提醒特效(新增)

强提醒是新增的独立透明窗口(不抢焦点、不阻塞、超时自关),其动画只有「涟漪扩散」一条,其余都是窗口显隐。特效必须在岛所在显示器上铺到足够大的区域,尺寸按 ClassIsland 规则随屏高缩放,且编辑/预览模式下绝不创建。

编号 场景 触发 属性 起止值 时长 曲线 延迟 实现 降级 现状对照
E1 特效窗口出现 强调提醒 窗口可见性 隐藏 → 显示 0 — 0 主进程 showInactive() 同 ClassIsland TopmostEffectWindow
E2 涟漪扩散 特效开始 scale 0 → 1 600ms QuadraticEaseIn 0 WAAPI(独立窗口) 直接显示静态 ClassIsland RippleEffect.axaml.cs:92-96
E3 涟漪淡出 同上 opacity 1 → 0 600ms SineEaseIn 0 WAAPI 直接移除 RippleEffect.axaml.cs:102-106
E4 特效收尾 750ms 后 移除元素/隐藏窗口 — — — 750ms 总时长 主进程 hide() 立即隐藏 RippleEffect.axaml.cs:108-109
E5 特效渲染缩放 屏高 ≥1400/≥2000 窗口尺寸 1× → .75× /.5× 0 — — 主进程按屏算 固定 1× repos/ClassIsland/.../MainWindow.axaml.cs:377-392

F. 课程徽标 / 课程提醒

课程提醒的视觉规格已经在 LessonReminderBar 中实现过半(倒计时/课程名/时间/三段信息/遮罩/进度条)。本轮需要补齐的是「入场/退场」「徽标出现」「徽标颜色随剩余时间变化」三处,并确保徽标数字 -1 的边界行为(剩余 1 秒显示 -1,不显示「即将下课」遮罩,docs/app/island.md:68-70)。

编号 场景 触发 属性 起止值 时长 曲线 延迟 实现 降级 现状对照
F1 课程提醒条入场 提醒开始(CS fixed) opacity、translateY 0 / 8px → 1 / 0 200ms (.16,1,.3,1) 0 CSS-T 直接显示 无入场动画(新增)
F2 课程提醒条退场 提醒结束 opacity 1 → 0 150ms (.16,1,.3,1) 0 CSS-T 直接隐藏 无(新增)
F3 课程遮罩 提醒开始(mask 阶段) 同 D1/D2 同 360/120 同 同 复用遮罩组件 直接落位 S8
F4 下课前徽标出现 ending 且开启倒数 scale、opacity .8/0 → 1/1 150ms (.16,1,.3,1) 0 CSS-A 直接显示 S14(静态)
F5 徽标数字更新 每秒 文本 -N → -(N-1),最后 -1 0 — — 直接替换(tabular-nums) 同 docs/app/island.md:68-70
F6 徽标颜色 剩余 ≤10s 颜色 --ci-accent → --ci-state-urgent【决策】 150ms 线性 0 CSS-T 直接换色 无(新增,可选)
F7 课程进度条 每秒 scaleX 旧 → 新 50ms(CS)/1s(WI) 线性 0 CSS-T 直接落位 S10/S11
F8 课程条接管岛(custom) 课程提醒出现 页面交叉 常态 → 课程条 CS 200ms / WI 弹簧 (.16,1,.3,1) / 弹簧 0 CSS-T / Spring 直接切 S5/S6

G. 组件滚动 / 轮播

滚动与轮播是 custom 方案里唯一的「持续动画」。它们必须遵守两条纪律:① 页面隐藏/窗口不可见时暂停;② 编辑模式下暂停并展开全部子组件,便于选择与排序。classisland 风格的轮播切换按 ClassIsland 的 250ms 上下滑动,winisland 保留瞬时切换。

编号 场景 触发 属性 起止值 时长 曲线 延迟 实现 降级 现状对照
G1 滚动组件(默认连续) 内容超宽 translateX 30% → −80% max(1,200/speed) 秒 线性无限循环 0 CSS-A 静止并省略号 S12 保留
G2 滚动组件(ClassIsland 暂停式,可选) 设置 mode='pause' translateX 0 → −(innerW) 停顿 → 回 0 innerW/40 + pause(默认10s) + outerW/40 秒 线性无限 无 WAAPI 关键帧 回落 G1 对应 ClassIsland RollingComponent.axaml.cs:85-135(速度 40px/s、暂停 10s)
G3 轮播切换(CS) 定时 translateY、opacity 旧:−40→0 / 0→1;新:0→40 / 1→0 250ms (.65,0,.35,1) 0 CSS-A 瞬时替换 ClassIsland SlideComponent.axaml:69-107;现状 S13 无动画
G4 轮播切换(WI) 定时 瞬时替换 — 0 — — 直接换 同 S13 保留
G5 编辑模式下滚动/轮播 进入编辑器 暂停/展开全部 动画 → 停止 0 — — 移除 animation / 显示全部子节点 同 ComponentNode.vue:388-392
G6 隐藏页暂停 document.hidden / 窗口不可见 暂停所有组件动画 运行 → 暂停 0 — — motion 计算 / CSS animation-play-state 同 CustomIslandRuntime.vue:57-58

H. 主题 / 风格切换

风格切换(classisland↔winisland)是「整岛换一套几何与实现」,必须避免「半新半旧」的中间帧:切换时先做 200ms 交叉淡化,再在同一帧替换 data-family 与变量,最后淡入。主题切换(Fluent↔Classic)与深浅色只改颜色 token,几何与动画参数不动——这是 ClassHelper 与 ClassIsland 共同的纪律(docs/dev/motion.md:125-130)。

编号 场景 触发 属性 起止值 时长 曲线 延迟 实现 降级 现状对照
H1 family 切换 设置页切换 classisland↔winisland 整岛 opacity + 变量 1→0.6→1(快速交叉) 200ms 出 + 200ms 入 (.16,1,.3,1) 0 CSS-T(切 data-family 前后加过渡类) 直接切 S19(现为瞬时)
H2 theme 切换(Fluent↔Classic) 设置页切换 背景/描边/阴影颜色 旧 → 新 200ms 线性 0 CSS-T(仅颜色 token) 直接切 对应 ClassIsland 窗口 BrushTransition 200ms(MainWindow.axaml:40-44)
H3 深浅色切换 应用主题变化 颜色 token 旧 → 新 200ms 线性 0 CSS-T 直接切 同 H2
H4 强调色变化 设置页改 accent --island-accent 派生色 旧 → 新 150ms 线性 0 CSS-T(颜色) 直接切 S19

I. 消息列表 / 徽标 / 交互

交互类动画是「手感」而非「表演」:悬停 140ms、按压 90ms、徽标 200ms,全部短于 250ms,且只作用于可交互元素。所有 hover 效果必须包在 @media (hover:hover) and (pointer:fine) 内,避免触摸屏幽灵 hover(classic-island.css:729-750)。

编号 场景 触发 属性 起止值 时长 曲线 延迟 实现 降级 现状对照
I1 铃铛徽标入场 未读数 0→N opacity 0 → 1 200ms/speed (.16,1,.3,1) 0 CSS-A 直接显示 S15 保留
I2 列表行悬停 指针移入(仅 hover 设备) background 旧 → --ci-fill-2 140ms (.16,1,.3,1) 0 CSS-T 无 hover S17
I3 按钮按压 按下/松开 scale 1 → .97 → 1 90ms 按下 / 抬起 200ms (.16,1,.3,1) 0 CSS-T 直接 S17
I4 图标按钮悬停 指针移入 背景/前景 旧 → 新 140ms (.16,1,.3,1) 0 CSS-T 无 S17
I5 选中/焦点环 Tab 键聚焦 outline 无 → 2px accent 0 — — CSS 静态 同 无(新增 a11y)
I6 多条消息列表高度变化 展开更多/新消息(仅 custom 多条列表,非单条气泡) 列表卡片高度 旧 → 新 CS 200ms / WI 弹簧 (.16,1,.3,1) / 弹簧 0 CSS-T/Spring + 主进程先扩窗 直接落位 现有 islandListLayout(度量);单条气泡不适用,行高恒 40

J. 编辑预览 / 拖拽

编辑器在主窗口里运行,动画要克制:进入/退出编辑模式 200ms 交叉淡化即可;拖拽必须跟手(0ms 跟随),只对「落位排序」做 150ms FLIP。预览区不创建额外桌面窗口(01-objective.md:35、docs/app/island.md:47)。

编号 场景 触发 属性 起止值 时长 曲线 延迟 实现 降级 现状对照
J1 进入/退出编辑模式 点「进入编辑模式」 整页 opacity 0 ↔ 1 200ms (.16,1,.3,1) 0 CSS-T 直接切 无(新增)
J2 拖拽幽灵 拖动组件 跟随指针 — 0 — — 直接定位(island-editor.css:331-334) 同 S18
J3 拖拽排序 放下组件 transform 旧位 → 新位 150ms (.16,1,.3,1) 0 FLIP 直接跳 无(新增)
J4 放置高亮 拖到行上 outline/背景 无 → 2px accent 虚线 0/100ms 线性 0 CSS 静态 同 island-editor.css:228-232
J5 预览缩放滑杆 拖动 --resident-scale 连续 0(实时跟随) — — 直接赋值 同 island-editor.css:193-217
J6 保存状态 保存中/失败 文本颜色 灰 ↔ 红 0 — — 直接切 同 island-editor.css:27-34

K. 课件覆盖

编号 场景 触发 属性 起止值 时长 曲线 延迟 实现 降级 现状对照
K1 课件卡入场 传输开始 opacity、translateY 0 / 8px → 1 / 0 200ms (.16,1,.3,1) 0 CSS-T 直接显示 S16(无动画)
K2 课件进度 进度事件 进度条宽度 旧 → 新 150ms 线性 0 CSS-T 直接跳 S16
K3 课件卡退场 完成/失败后 3s opacity 1 → 0 150ms (.16,1,.3,1) 3s 停留 CSS-T 后主进程恢复岛 直接隐藏 S16

L. 暂停 / 降级 / 兜底

编号 规则 触发 行为 证据
L1 动画总开关 animations=false 弹簧 snap、CSS 过渡 0ms、遮罩设终值、rAF 不启动 IslandApp.vue:249-253、CustomIslandRuntime.vue:148-151、LessonReminderContent.vue:77-80
L2 速度倍率 speed≠1 所有时长 /speed;弹簧 dt×speed;兜底定时器 /speed IslandApp.vue:291,528、CustomIslandRuntime.vue:156,220-222、LessonReminderContent.vue:134-137
L3 页面隐藏 document.hidden 暂停弹簧/rAF;恢复时按当前状态落位 CustomIslandRuntime.vue:57-58,269-273
L4 窗口不可见 主进程 hide() 渲染层收到 nativeVisible=false,暂停 CustomIslandRuntime.vue:58;preload island:visibility(main/island.ts:528-533)
L5 系统减少动态效果 prefers-reduced-motion 不覆盖岛;岛动画唯一开关是 appearance.animations,speed 统一缩放;仅编辑器/设置 UI 遵循系统设置 docs/app/island.md:57、docs/dev/motion.md:78
L5b 尊重系统设置(P2,可选) 用户显式开启 respectReducedMotion 系统 reduced-motion 时按 animations=false 落位;默认关闭 本行为可选实现,默认不得启用
L6 rAF 被限流 Windows 判定遮挡 兜底定时器到点直接落位(形变 1200/speed、遮罩 600/speed、命中 600/speed) IslandApp.vue:236,266-286、LessonReminderContent.vue:82-89、CustomIslandRuntime.vue:220-222
L7 触摸窗口缩放 触摸模式形态变化 变大立即、变小等隐藏后收(不做动画) main/island.ts:1555-1578
L8 失焦宽限 展开后 blur 500ms(鼠标)/1200ms(触摸)内不收起,避免焦点抖动导致误收起 main/island.ts:2287,2297,557-578

10.2.1 消息气泡完整时间线(按毫秒对齐,验收时逐点核对)

以一条普通通知在 family='classisland'、speed=1、animations=true、theme='fluent' 下为例,完整时间线必须严格如下(同一条消息的遮罩、文字、投影、进度条、窗口状态都按此对齐):

时刻 事件 实现要点
t=0 主进程把消息压入队列并把 mode 切为 expanded;窗口使用预设包围盒(宽 = min(工作区宽,960)、高 = 40+2×pad),不因气泡扩窗 窗口 setBounds 不得在可见时发生(main/island.ts:1476-1479)
t=0 岛出现(若原本隐藏):opacity 0→1 / translateY 8→0,250ms A1;同时开始窗口淡入(winisland 为 +0.16/16ms)
t=0 5 个斜切区段开始扫入:区 0/4 立即、区 1/3 延迟 90ms、区 2 延迟 120ms,每段 360ms QuarticEaseOut D1;总 480ms
t=0 气泡图标/徽标入场 150ms(scale .9→1) D15
t=260 遮罩文字开始淡入 250ms + 缩放 1.1→1.0 750ms D4/D5;两个 CSS animation 必须同时挂、各自 both
t=480 遮罩完全覆盖行(5 区到位) D1 结束
t=600 背景投影/遮罩底色开始淡入 100ms D7/D8
t=700 投影/底色到位;如为「有正文」的提醒,覆盖层正文开始 200ms 淡入 D9
正文超长时 行高恒 40px;正文最多 2 行、超出省略号 不扩窗、不改行高;maxCardHeight 不参与单条气泡
展示期 倒计时进度每 50ms 更新一次、每次 50ms 线性过渡 D14
收尾 用户点「知道了/标为已读」或超时:覆盖层 200ms 淡出 → 遮罩反向收拢(错峰 [120,90,0,90,120],每段 360ms)→ 窗口 150ms 淡出并隐藏 D2/D10/D17
队列非空 上一批收拢到 50% 时启动下一批扫入,交叉 100ms,不得出现空白帧 D16

对应 winisland 路径:t=0 时改为胶囊→展开卡弹簧形变(目标宽高由 ISLAND_SIZE_DELTA 决定),文字交叉淡入按 1−1.5p / p²,点「知道了」后 360ms 收回再淡出(A6–A8)。两条路径共享同一消息状态机与队列规则,只有「表现层」不同。

10.3 冲突与选择点(必须按默认执行,并保留开关)

编号 冲突点 ClassHelper 现状 ClassIsland 目标 默认选择 开关/回退
X1 fixed 形态 268×44 胶囊 + 详情卡 40px 信息行 + 行内气泡 classisland:信息行;winisland:胶囊 appearance.family
X2 开合实现 弹簧形变 CSS transition(行宽/行高) classisland 用 CSS;winisland 用弹簧 family
X3 单条气泡的尺寸 卡片长高(最大 ~220) 40px 行内气泡,正文 2 行省略 行高恒 40、不增高、不扩窗;maxCardHeight 只用于 custom 多条列表与强提醒窗口 family;无「增高」回退项(WinIsland 原展开卡保留在 winisland fixed)
X4 遮罩形态 无(直接换卡片) 斜切 5 区分块 + 文字缩放 两风格都实现遮罩(winisland 的详情卡也用遮罩增强观感,但保留卡片尺寸动画) family 决定是否叠加卡片尺寸动画
X5 滚动组件 连续 30%→−80% 跑马灯 40px/s + 10s 暂停 + 回程 默认连续(ClassHelper 观感),设置项可切 mode='pause' 组件设置 settings.speed 已有;新增 settings.mode【推断字段名】
X6 轮播切换 瞬时 250ms 上下滑 + 淡入淡出 classisland 250ms;winisland 瞬时 family
X7 课程进度条 1s linear 50ms linear classisland 50ms;winisland 1s family
X8 进度条高度 2px 3px classisland 3px;winisland 2px family
X9 系统减少动态效果 岛不受其影响 同 ClassHelper(岛自身开关) 不覆盖岛:岛动画唯一开关 appearance.animations,speed 缩放时长;如用户显式要求,可提供 P2 选项 respectReducedMotion(默认 false) animations / P2 选项
X10 消息列表错峰 无 ClassIsland 无(列表瞬时) 新增 30ms 错峰但封顶 120ms,可在 animations=false 时关闭 animations
X11 主题切换 瞬时 200ms 颜色过渡 200ms(classisland);winisland 瞬时 family

10.4 性能预算与降级矩阵

场景 目标 降级顺序 说明
弹簧形变(winisland) 60fps、单帧 ≤8ms ① 降步进到 30fps ② 只动尺寸不动圆角 ③ snap() 必须在岛渲染进程 rAF,独立于主进程
CSS 行宽过渡(classisland;行高恒 40 不参与过渡) 60fps ① 去掉阴影过渡 ② 直接落位 只过渡背景层,文字层瞬时;避免触发整行重排
斜切遮罩(SVG 5 区) 60fps、单帧 ≤4ms ① 降为 3 区 ② 降到 30fps ③ 静态遮罩 每帧重算 points,避免重绘整棵 SVG
消息气泡(40px 行内遮罩 + 2 行正文) ≥50fps ① 关闭错峰 ② 关闭文字缩放 ③ 直接切 与 11 部分性能基线一致(parts-r/11-compat-tests.md:46);气泡不改变行高
强提醒特效 ≥50fps ① 关闭涟漪改静态色块 ② 缩小渲染缩放 ③ 不显示 特效窗口独立进程/独立窗口,动画结束即关
多条列表高度变化(custom) 60fps ① 关闭过渡直接跳 ② 限制可见行数 窗口先扩再形变,避免裁切;单条气泡不适用
主窗口隐藏 0 动画 — 岛仍可见时不得因主窗口隐藏而停;只有岛窗口隐藏/页面隐藏才暂停

10.5 实现方式规范(逐条遵守)

  1. classisland 优先 CSS:几何用 transition(width/height/border-radius/opacity/box-shadow/background-color),分块遮罩用 JS 进度 + SVG polygon(现有实现 LessonReminderContent.vue:59-102 可直接复用/抽组件),文字用 @keyframes(var(--ci-*) 时长/曲线)。
  2. winisland 保留弹簧:SpringValue 参数与兜底不得改(spring.ts:26-114);syncTargets 的 snap 分支必须保留。
  3. 遮罩复用:把 LessonReminderContent.vue 里的 5 区遮罩抽成 IslandMask.vue(props:open、duration、stagger、fill、motion、speed),消息气泡与课程提醒共用;禁止复制第二份实现。
  4. 进度条统一样式:抽 .ci-progress 类(高度 var(--ci-progress-h)、轨道 var(--ci-progress-track)、填充 var(--ci-progress-fill)、圆角 var(--ci-progress-radius)),用 transform: scaleX() 驱动;不要用 width。
  5. FLIP 只用于重排:编辑器拖拽排序、组件位置变化用 FLIP(先批量读、再写 transform、下一帧过渡),不得对每帧读布局。
  6. 强制落位:所有 transition 在 data-family='classisland' 下用 CSS;animations=false 时通过根元素 data-anim='0' 把 transition-duration/animation-duration 覆盖为 0ms(参照 --island-anim 的做法,IslandApp.vue:527),并让 JS 跳过 rAF。
  7. 不要动画窗口 bounds:仅 M0 新建窗口、模式切换、显示器/边距变化时 setBounds,且节流 ≥100ms(parts-r/01-objective.md:81)。
  8. 可取消:每个动画入口先取消同目标旧动画(cancelAnimationFrame + 清 timer + 重置变量),再启动新动画;状态机以「目标值」而不是「播放进度」为准。
  9. 无障碍:aria-live="polite" 保留在课程/消息条(LessonReminderContent.vue:140);动画不改变语义顺序;prefers-reduced-motion 下编辑器 UI 动画压到 1ms(主窗口规则,index.css:909-917)。
  10. 测试钩子:在 dev 构建暴露 window.__islandMotion = { list(), state(), setSpeed(x), disable() },供无头验证读取(--visual 才做画面验证)。

10.6 逐文件改动清单

文件 动作
packages/desktop-client/src/island/spring.ts KEEP;仅允许为 classisland 增加 snapTo() 便捷方法(可选),不得改默认参数
packages/desktop-client/src/island/squircle.ts KEEP(仅 winisland/custom 使用);classisland 不调用
src/island/IslandApp.vue 大改:family 分支;classisland fixed 改为信息行渲染;保留消息状态机与 winisland 胶囊路径;接 animations/speed 与暂停
src/island/CustomIslandRuntime.vue 改:classisland 下行宽用 CSS 过渡、行高恒 40(不随内容增高);保留测量与 winisland 弹簧分支;motion 规则扩展
src/island/ClassicNotifications.vue 改:气泡两阶段(斜切遮罩 + 文字 + 缩放);多行错峰;保留消息类型/排序/回执
src/island/LessonReminderContent.vue 改:遮罩抽成共用组件;进度 2→3px;标题/缩小时长走 token;变量改名 --ci-lesson-*(保留 alias)
src/island/ComponentNode.vue 改:滚动/轮播动画按 family;课程进度 50ms/1s;--secondary/--emphasized/--large 改用 --ci-fs-*
src/island/CoursewareTransfer.vue 改:K1–K3 入场/退场/进度过渡;其余 KEEP
src/island/classic-island.css 改:classisland 分支 token 化;winisland 分支保持;悬停/按压过渡统一 token
src/island/custom-island.css / custom-runtime.css 改:行高/间距/进度条/圆角 token 化;classisland 背景/描边/阴影;预览行 44→40
src/island/lesson-ending.css 改:徽标颜色 token 化;新增出现动画类(可选)
src/island/tokens.css 新增:--ci-* + --ci-dur-*/--ci-ease-*
src/main/island.ts 改:下发 family/theme/colorScheme;classisland 包围盒计算;关闭/淡出时序不变;特效窗口控制
src/renderer/styles/island-editor.css 改:J1/J3/J4 动画;预览 40px;token 化
docs/dev/island-refactor.md 新增:动画表、降级矩阵、测试钩子说明

10.7 验收标准

  1. 逐条覆盖:本节 A1–A11、B1–B5、C1–C5、D1–D17、E1–E5、F1–F8、G1–G6、H1–H4、I1–I6、J1–J6、K1–K3、L1–L8 全部实现;抽测 12 条(覆盖两种 family),时长误差 ≤16ms、曲线一致。
  2. 双风格默认值:family='classisland' 下无弹簧形变、无 squircle;行宽过渡 300/550ms;气泡遮罩 360+120、文字 260/250、缩放 750、退出 200;family='winisland' 下胶囊开合仍为弹簧、收缩 360ms 后淡出、窗口淡入 +0.16/16ms、淡出 −0.2/16ms。
  3. 总开关:animations=false 时所有动画在 1 帧内完成,不出现「终态先行」;rAF/定时器数量归零(DevTools Performance 验证)。
  4. 速度:speed=2 时所有时长减半、弹簧更快;speed=0.5 时加倍;不得出现某条动画没乘 speed 的不一致。
  5. 暂停:主窗口最小化/岛窗口 hide()/document.hidden 时无 rAF、无 CSS 动画推进;恢复可见后直接呈现当前状态(不补播)。
  6. 可打断:快速连续展开/收起、连续推送 5 条消息、消息中途标记已读 → 无闪回、无残留、无「停在半路」;队列切换时旧遮罩被取消。
  7. 视觉一致:遮罩 5 区权重与 60° 斜切与 ClassIsland 一致;文字先于底色结束(260ms 延迟)不出现「白字压白底」;Classic 主题下遮罩整块下压/上滑(200ms)。
  8. 进度条:classisland 3px、50ms 线性;winisland 2px、1s 线性;进度条用 transform 而非 width(Performance 面板无 layout)。
  9. 无窗口抖动:开合/气泡/列表过程中窗口 setBounds 调用次数为 0(隐藏帧除外);触摸模式下窗口只增不减。
  10. 性能:主界面过渡 ≥55fps、气泡/强提醒 ≥50fps;单帧主线程 ≤8ms;动画结束无残留 will-change/rAF/timer(parts-r/11-compat-tests.md:46、12-acceptance.md:73)。
  11. 无头验证:verify:desktop 默认不创建可见岛即可断言动画状态机与时长(通过测试钩子读取);只有 --visual 才做逐帧/截图验收。
  12. 文档:docs/dev/island-refactor.md 的动画表与本节一致;docs/app/island.md 补充 family 与动画开关说明。
  13. fixed 气泡不增高:推送超长正文消息,行高恒 40px、正文最多 2 行省略、窗口 bounds 不变;全过程无 height/max-height 过渡,maxCardHeight 未被单条气泡读取。
  14. reduce-motion 口径:系统开启 prefers-reduced-motion 后,岛动画仍按 appearance.animations/speed 播放(默认不覆盖);只有 P2 选项 respectReducedMotion=true 时才按 animations=false 落位,且该选项默认关闭。

11. 兼容性、性能、平台与测试基线

本部分规定「重构不能把原来能用的东西弄坏」,以及如何在不打扰用户的前提下验证。所有验证默认必须无头/离屏执行。

11.1 兼容矩阵(必须逐项通过)

维度 必须保持兼容的内容 验证方式
配置 旧 config.json.islandLayout(fixed/custom、residentEnabled、schemes/rows/nodes/expandedRows/appearance/lessonReminders/pointerFade*)无损升级 迁移单测 + fixtures 回放
布局模式 fixed 与 custom 均可切换、可保存、可重启恢复;两布局共用一个原生岛窗口,切布局不新开窗口 无头集成测试 + 主进程单测
风格 family 只影响 fixed:winisland 保持旧胶囊观感,classisland 为默认信息行;custom 始终 ClassIsland 规格;切换即时生效、可回退 截图(仅 --visual)+ 视觉回归
消息 通知/作业/叫人的类型、优先级、id 去重、排序、上课暂存、紧急展开、叫人可点胶囊、已读回执、安静模式、教师/班级端差异不变 状态机单测 + 无头消息场景
课程 课表解析结果(ScheduleDto/IslandTimePointDto)、准备/上课/课间/下课前/课中进度提醒、下课前秒数徽标、当前/下一节/今日时间点显示;行为变更:custom 不再临时接管岛显示课程提醒(仅保留 ending 徽标,进度由课程组件显示),fixed 按 ClassIsland 接管 时间状态机单测 + 假时钟无头场景
天气 Open-Meteo 数据、15 分钟刷新、断网缓存标记"缓存"、无官方预警提示 无头 mock 网络 + 快照
课件 课件传输状态优先覆盖岛内容、SHA256 校验、回执、不自动运行文件 无头课件场景
编辑器 方案/行/组件/导入导出/预览/重置/新建/复制/删除、12 行/200 节点/6 层/20 方案边界 单测 + 组件测试
窗口/输入 透明、置顶、不占任务栏、点击穿透、触摸模式、失焦宽限、固定几何护栏、单实例 无头窗口参数断言 + 手工触摸说明
平台 Windows 10/11(主目标)、macOS、Linux X11;DPI 100/125/150/200;多显示器与热插拔 平台矩阵 + 无头几何断言
性能 启动、空闲 CPU、内存、消息风暴、动画帧率 性能脚本 + 采样

11.2 配置迁移与回滚(硬要求)

  1. 版本化:IslandLayoutSettings.version 从 1(旧配置缺失视为 1)升级到 2;迁移函数必须幂等(重复执行结果一致)。
  2. 迁移顺序:读取原始 JSON → 备份到 config.json.bak.<时间戳>(或配置目录 backups/)→ 应用迁移(只补齐/转换,不删除用户数据)→ normalizeIslandLayouts() 校验与夹取 → 原子写(临时文件 + rename)。
  3. 必须迁移的字段:
    • 顶层新增 version、appearance.family(老用户默认 'winisland',新安装 'classisland')、appearance.theme('fluent'/'classic')、strongNotification 开关与参数、progressBar 参数、rowStyle(行背景/分隔/圆角覆盖);
    • scheme 新增 rowHeight(默认 40)、componentGap(默认 12,对应单侧 6)、showProgress(默认 true)、hideRules 扩展(保留旧 in-class/no-message/no-class,新增 full-screen/max-window 需按 12 部分验收);
    • 组件新增字段(如 schedule 的 display 扩展、countdown 的 showProgress、clock 的 showSeconds),旧值必须原样保留。
  4. 回滚:迁移失败或新版启动检测到配置不可用时,必须能恢复备份并启动旧结构(主进程启动早期读 config.json 的 islandLayout 失败时使用默认值 + 保留原文件为 .corrupt),不得清空用户方案。
  5. 降级兼容:若旧版本客户端读到新配置,必须忽略未知字段并使用默认值;禁止因未知字段崩溃。版本号高于当前可识别值时,主进程写日志并走只读模式(不自动覆盖新配置)。
  6. fixtures:packages/shared 测试中必须包含:fixed 默认、custom 多方案/多行/嵌套容器/旧 residentEnabled、极端值(超 12 行/200 节点/6 层/颜色非法/坐标为 null)、旧字段缺失、迁移幂等、迁移失败回滚。

11.3 IPC 与契约兼容

  • 保留现有通道:island:get-schedules、island:get-time-points、island:get-layout、island:save-layout、island:edit-layout、island:get-editing、island:get-visible、island:resident-control、island:resident-touch、island:set-editing、island:content-size、island:remove-notification、island:push、island:class-state、island:alive、island:get-state、island:set-appearance、island:get-appearance、island:set-interactive、island:set-hit-rect、island:set-touch-mode、island:action。
  • 新增通道必须遵守:ipcMain.handle/on 前校验 event.sender === island.getWindow()?.webContents(或主窗口 webContents),payload 必须是纯数据且做类型/范围校验;preload 白名单同步更新 src/preload/island.ts 与 src/types/desktop.d.ts。
  • 新增通道建议(如需要):island:strong-notification(主进程→岛/强提醒窗口)、island:get-metrics(渲染层上报行/组件度量)、island:preview-mode(编辑器预览)。不得把 Vue Proxy/函数/类实例跨 contextBridge 传递。
  • 渲染进程心跳、幽灵窗口检测、渲染崩溃自愈(限流 3 次/10 分钟)与恢复逻辑保持不变。

11.4 性能预算(超预算必须优化)

  • 岛渲染进程启动 ≤ 500ms(从窗口创建到首帧可见);冷启动到岛可见 ≤ 3s(不含网络)。
  • 空闲 CPU 平均 ≤ 1%(岛可见、时钟/课程正常刷新);消息到达/展开瞬间峰值 ≤ 15% 且 ≤ 500ms。
  • 主进程 + 岛渲染进程内存 ≤ 250MB(不含可选强提醒窗口);连续运行 24h 增长 ≤ 10%。
  • 消息风暴:1 分钟 20 条不丢、不重复、不卡顿;队列长度上限与丢弃策略必须有日志。
  • 动画:主界面过渡 ≥ 55fps;消息气泡/强提醒 ≥ 50fps;不得因背景页(主窗口隐藏)继续动画。
  • 包体:本次重构不得新增 > 200KB 的运行时依赖;不得引入新 UI 库/图标库(图标仍用现有方案)。

11.5 平台差异与窗口护栏

  • Windows:透明/置顶/点击穿透/setShape(若用)只在 Windows 分支;Windows 10 不支持 Mica 时退回纯色;高 DPI 下窗口 bounds 用 DIP。
  • macOS:透明窗口、置顶、全屏空间行为;不使用 setBackgroundMaterial;点击穿透退化为整窗 setIgnoreMouseEvents(true,{forward:true}) + 命中通道。
  • Linux X11:透明窗口与置顶可用;Wayland 明确不支持并给提示(与 ClassHelper 既有说明一致)。
  • 多显示器:position/followCursorDisplay/停靠偏移语义保持不变;显示器热插拔/DPI 变化必须重算包围盒与锚点,节流 ≥100ms。
  • 触摸:触摸模式仍以"窗口贴合岛体 + 始终接收输入"为原则;编辑模式与触摸手势不能互相抢占;失焦宽限(鼠标 500ms / 触摸 1200ms)保持不变。
  • 强提醒窗口:无边框、置顶、不抢焦点、skipTaskbar、独立渲染入口或复用岛窗口的独立层;关闭后释放;不得在编辑器/预览中创建。

11.6 测试基线(无头优先)

  1. 单元测试(shared):normalizeIslandLayouts 边界、迁移幂等、版本识别、颜色/数值夹取、组件树限制(12 行/200 节点/6 层/20 方案)、消息排序/去重、课程状态机(准备/上课/课间/下课前/放学/跨天/跨周)。
  2. 主进程测试:窗口几何计算(6 停靠 × DPI × 多屏)、hit rect/触摸模式切换、消息队列与暂存、课程提醒调度、天气缓存、课件覆盖优先级、配置写入原子性与回滚。
  3. 组件测试:固定数据下 IslandApp.vue/CustomIslandRuntime.vue 的渲染快照(DOM 结构 + 关键 computed 样式),不创建真实窗口(可用 happy-dom/离屏渲染)。
  4. 集成(无头):pnpm verify:desktop 默认后台快速检查——不得创建可见岛/托盘;覆盖启动、布局切换、消息推送、课程提醒、编辑器保存/读取、迁移。
  5. 视觉(显式):只有 pnpm verify:desktop:visual / --visual 才允许真实桌面、动画采样、截图;截图输出目录用临时路径;不得把窗口对照图发布为展示图。
  6. 数据纪律:测试使用临时 SQLite/夹具,禁止使用真实班级、真实密码、真实通知;远控/安装器使用假执行器。
  7. 证据:每次汇报附实际命令输出摘要(通过/失败数量),不得沿用历史数字。

11.7 观测与诊断

  • 主进程日志必须记录:配置迁移(旧→新字段、备份路径、结果)、窗口几何重算原因、消息队列变化(入队/出队/丢弃/去重)、课程状态切换、天气刷新/失败、强提醒创建/销毁、渲染崩溃/重建(限流计数)。
  • 岛渲染进程必须上报心跳与度量(行/组件尺寸、可见性);主进程对"幽灵窗口"(渲染卡死但窗口在)执行重建(限流)。
  • 诊断页/日志中不得包含用户密码、令牌、完整数据库路径;日志轮转与大小上限沿用现有 logger。
  • 提供 docs/dev/island-refactor.md 的排障章节:黑屏/透明失效/点击穿透/闪烁/动画卡顿/消息不弹/课程不刷新/迁移失败的症状 → 排查步骤 → 回滚方法。

11.8 文档与交付说明

  • 更新 docs/app/island.md:新默认观感、风格切换、editor 编辑模式、强提醒、行为表(含 fixed 重定义)。
  • 更新 docs/dev/implementation.md:岛的新架构入口、schema v2、迁移函数位置。
  • 新增 docs/dev/island-refactor.md:架构图、文件职责、数据契约、动画表、迁移与回滚、测试命令。
  • docs/ 中的展示截图必须来自显式视觉验收且人工确认;不得使用重构中的对照图/失败截图。
  • 交付说明必须包含:与 ClassIsland 的差异清单(技术栈/能力取舍)、未实现项及原因、性能数据、回滚步骤。

11.9 本部分验收标准

  • fixtures 覆盖 11.2 全部场景,迁移全部通过且幂等。
  • 旧配置在重构版启动后方案、组件、外观、课程提醒开关全部保留,且可切回 winisland 观感。
  • 所有既有 IPC 通道仍可用,新增通道有 sender 校验与类型校验。
  • pnpm verify:desktop 无参数运行时不创建可见岛/托盘/声音;--visual 才创建。
  • 多显示器/DPI 切换后岛位置与尺寸正确,无 1px 抖动。
  • 24h 无内存增长 > 10%、无崩溃、无幽灵窗口。
  • 文档更新完成,含迁移/回滚/差异说明。

12. 验收清单(逐项勾选,不得跳过)

所有自动验证默认无头/离屏;--visual 才允许真实桌面。任一项未通过都不得声称完成。

12.1 工程门禁

  • pnpm lint 0 error(warning 需列出)。
  • pnpm typecheck 0 error(先构建 shared)。
  • pnpm build:desktop 成功;island.html 与主窗口产物分别可加载。
  • 未新增 UI 框架/状态库/图标库;新增依赖(若有)≤ 200KB 且说明不可替代性。
  • 只改 packages/shared、packages/desktop-client、packages/ui(如需)与 docs;服务端/Web 管理端零改动。
  • 所有新增 IPC 通道有 sender 校验、payload 类型校验;preload 白名单与 desktop.d.ts 同步。
  • 无 console.log 残留;日志走统一 logger。

12.2 视觉验收(像素级)

在 1920×1080@100%、2560×1440@125%、深色/浅色、family=classisland/winisland、Fluent/Classic 主题下截取离屏基准图,逐项比对(颜色 ±2/255、尺寸/位置 ±1px、透明度 ±1%):

fixed(classisland 风格)

  • fixed 窗口包围盒固定:宽 = min(工作区宽, 960)、高 = 40 + 2×pad(pad 默认 20);消息到达/展开/多条列表均不改变窗口尺寸。
  • 可见行高 40px,行内容左右内边距 12px,最小内容宽 20px;行间距 5px。
  • 行背景:圆角 8px、背景不透明度 0.5、1px 描边、阴影 0 4 8 2 #48000000(Fluent)。
  • 字号:次要 14 / 主体 16 / 强调 18 / 超大 20;字重 Medium;正文 line-height: 1.25。
  • 进度条:高 3px、贴底、圆角 1.5px、颜色为强调色、按设置精度更新。
  • 默认 1 行 = 日期 + 课程表;无课表时显示占位文本("今天没有课程。"/"今日课程已全部结束。")。
  • 课程表显示当前/下一节/今日时间点模式,当前项展开显示起止时间,其余折叠;课间与"默认隐藏"时间点折叠时隐藏,分割线显示竖线,行动不显示。
  • 下课前秒数徽标:蓝色圆形、显示在课程名右侧、剩余 1 秒显示 -1、不显示"即将下课"遮罩、不替换常态组件。
  • 消息气泡:遮罩 5 区权重 [0,0.1,0.2,0.4,0.2,0.1]、60° 斜切、区段 360ms + 错峰 120ms、开 QuarticEaseOut / 关 QuadraticEaseIn;文字延迟 260/250ms、scale 1.1→1.0/750ms;退出 200ms (0.5,0,0.75,0);行高 40。
  • 强提醒:全屏/大区域遮罩、超时关闭、不抢焦点、在岛所在显示器出现;关闭后无残影。
  • winisland 风格:胶囊 268×44(默认值可配置 220–420 × 36–72)、连续圆角(超椭圆 4.2/14 段)、弹簧形变、纯黑/毛玻璃/主题色渐变、阴影常驻;不得与 classisland 风格混用。

custom(始终 ClassIsland 规格,family 不影响 custom)

  • 行高 40、行间距 5、组件外边距 6/6、行背景圆角 8、背景不透明度 0.5。
  • 行结构:Auto, *, Auto 三列(左拖动把手 20px / 中组件 / 右行操作区);选中框 1.5px 虚线 1,1.5、Opacity 0.75、圆角同背景。
  • 多行、分体岛(separated)、主行标记、允许提醒标记、隐藏规则、组件外观覆盖(字号/前景/背景/圆角/透明度/宽度/对齐)逐项与设置一致。
  • 编辑器中的组件库、行、容器嵌套、选中态、抽屉尺寸与间距符合第 9 部分。
  • 所有组件的默认样式/空数据兜底/超长文本截断/滚动行为符合第 7 部分。

窗口与层级

  • 岛窗口无边框、透明、置顶、默认不占任务栏、默认不抢焦点;winisland 与 classisland 风格均不出现白底/黑边/窗口阴影残影。
  • 6 个停靠位 × 偏移(X/Y)× 多显示器索引正确;followCursorDisplay 行为保持。
  • 编辑模式窗口/画布尺寸正确:屏宽 ×(屏高−1px)、四边 60px 内缩、开启命中。
  • 深浅色/主题切换无残留样式;岛动画由 appearance.animations 开关控制(系统 prefers-reduced-motion 不强制关闭岛动画,遵循 ClassHelper 既有文档约定)。

12.3 功能验收

消息与提醒

  • 通知/作业/叫人三类消息的图标、标题、正文、优先级、时间显示符合第 8 部分。
  • id 去重;同批消息按"优先级 → 时间"排序;列表"展开更多"与"超出工作区提示"正确。
  • 上课期间普通通知/作业暂存、下课补弹;普通叫人保留可点胶囊;紧急通知/紧急叫人立即展开;与 ClassHelper 现有规则逐条一致。
  • 已读/知道了/标为已读/批处理的语义与通知中心状态一致;本机新录作业指纹去重。
  • 自动收起超时:胶囊 15s、展开 20s、紧急 45s、叫人 90s(按现有常量),可打断、可重入。
  • 强提醒仅对开启"强调提醒"的消息出现;关闭后不出现;超时自动关闭;不阻塞岛与其他应用。
  • 消息风暴 20 条/分钟:不丢、不重复、不卡顿、队列上限有日志。

课程与天气

  • 准备/上课/课间/下课前倒数/课中进度五类提醒按设置触发,阈值默认 60s 可调。
  • 课程状态机跨天、跨周、临时课表、预定课表、调课场景正确(与 ClassHelper 现有解析结果一致)。
  • 课程提醒接管:fixed(classisland)按 ClassIsland 行内容接管显示 prepare/on-class/break;custom 不再临时接管整岛(移除 resident-lesson-page),进度由课程组件显示,ending 只保留课程名右侧 30×30 蓝色圆形秒数徽标。
  • 天气:Open-Meteo 15 分钟刷新、断网缓存并显示"缓存"、失败重试与降级、无官方气象预警措辞。
  • 课件传输:传输中覆盖岛内容,显示文件名/类型/教师/字节/进度,完成后校验并回执;不自动运行文件。

编辑器与设置

  • 设置页:经典/自定义切换、风格切换、外观参数、课程提醒开关、指针避让、动画开关、强提醒开关全部即时保存并生效。
  • 编辑器:方案栏(新建/复制/删除/重命名/导入导出/重置)、组件库 11 类、行(新增/排序/主行/允许提醒/删除)、容器嵌套、拖拽(行内/跨行/库→行/库→容器)、选中与组件设置(内容/外观/尺寸/隐藏条件)、编辑模式预览、预览缩放、退出保存/失败重试。
  • 边界:12 行/200 节点/6 层/20 方案;超限拒绝并提示;非法颜色/数值被夹取;未知组件/未知 Kind 不崩溃。
  • 旧方案导入后可直接渲染(迁移测试)。

12.4 动画验收

  • 第 10 部分动画表逐项实现;时长误差 ≤ 16ms,曲线一致(DevTools Animations 或逐帧截图验证)。
  • 岛出现/隐藏、行展开/折叠、行宽/高度、进度条、消息气泡两阶段、强提醒、课程徽标、滚动/轮播、主题切换全部流畅(≥55fps)。
  • 不可打断的动画可被新状态覆盖(不排队、不闪回)。
  • animations=false 生效(唯一岛动画开关);speed 统一缩放时长;失焦/隐藏页暂停;恢复后不补播。
  • 动画结束无残留 will-change、定时器、rAF。

12.5 兼容与迁移验收

  • 旧 fixed/custom 配置 fixtures 升级后:方案、行、组件、外观、课程提醒、指针避让、residentEnabled 全部保留;迁移幂等。
  • 升级失败可回滚(备份恢复);不可用时保留原文件为 .corrupt 并启动默认配置,不得清空。
  • 新配置被旧客户端读取时忽略未知字段、不崩溃(如能构造旧客户端则测;否则单测模拟)。
  • winisland 风格下旧观感/旧交互无回归(对照基线图)。
  • 切换 fixed↔custom、风格切换、方案切换均不新开窗口、不丢配置。

12.6 无头与不打扰验收

  • pnpm verify:desktop(不带 --visual)全程不创建可见岛、托盘气泡、系统通知、声音;测试结束无残留进程/窗口。
  • 使用独立 userData;不触碰真实配置/真实班级/真实通知。
  • --visual 仅在显式允许时运行,且截图输出到临时目录;不自动发布。
  • 崩溃自愈限流有效:1 分钟内最多重建 3 次,超过进入安全模式/停止重建。
  • 日志无密码/令牌/敏感路径。

12.7 性能验收

  • 岛渲染进程启动 ≤ 500ms;冷启动到岛可见 ≤ 3s。
  • 空闲 CPU ≤ 1%;消息风暴峰值 ≤ 15% 且 ≤ 500ms。
  • 主进程 + 岛渲染进程内存 ≤ 250MB;24h 增长 ≤ 10%;无句柄泄漏。
  • 包体增量 ≤ 200KB(除说明的例外);无新增大图标/字体文件。

12.8 文档与交付物

  • docs/app/island.md、docs/dev/implementation.md 更新;新增 docs/dev/island-refactor.md。
  • 架构图、数据契约、动画表、迁移与回滚、测试命令齐全。
  • 交付说明:与 ClassIsland 差异、未实现项、性能数据、回滚步骤。
  • 第 12 部分本清单逐项勾选并附验证输出摘要;未验证项标注原因。

12.9 禁止事项(验收时再次确认)

  • 未通过修改验收标准/删除测试来"通过"。
  • 未因重构删除既有功能(消息、课程、天气、课件、编辑、导入导出、指针避让、触摸)。
  • 未引入需要联网/登录的新依赖。
  • 未把 ClassIsland 的 C#/XAML/本地档案代码复制进 ClassHelper。

13. 重构里程碑(M0–M7)

必须按顺序推进;每个里程碑结束都要「可无头运行 + 通过该阶段验收 + 既有功能不回归」。禁止先大改 UI 再补架构与测试。

M0 基线固化与安全网(0.5–1 天)

  • 交付:
    • 读通现状并输出文件级计划(第 4 部分)与迁移计划(第 5 部分)。
    • 为现有行为建立无头基线:normalizeIslandLayouts 边界用例、消息排序/去重、课程状态机、窗口几何计算、旧配置 fixtures(fixed/custom/嵌套/极端值)。
    • 记录性能基线:启动耗时、空闲 CPU、内存、消息风暴耗时。
    • 在 verify:desktop 中补充"不创建可见窗口/托盘"的断言。
  • 验证:pnpm typecheck、pnpm verify:desktop(无头)通过;fixtures 全部可回放。
  • 完成标准:没有行为改动;所有基线用例通过;性能数字记录在 docs/dev/island-refactor.md 草案。

M1 共享层 v2:schema、常量、迁移、tokens(1–2 天)

  • 交付:
    • packages/shared/src/island-layout.ts:version=2、appearance.family、appearance.theme、strongNotification、progressBar、rowStyle、scheme 的 rowHeight/componentGap/showProgress、组件新字段;normalizeIslandLayouts() 迁移+校验(12 行/200 节点/6 层/20 方案、颜色/数值夹取)。
    • 常量/token:ClassIsland 行高 40、内边距 12、行距 5、圆角 8、透明度 0.5、字号 14/16/18/20、进度条 3px 等;winisland 兼容值保留。
    • 迁移函数(幂等)+ 备份/回滚;单元测试覆盖全部 fixtures。
  • 验证:单测(迁移、夹取、边界、幂等);typecheck。
  • 完成标准:旧配置升级后数据无损;新 schema 能被现有代码读取(旧渲染仍可运行);迁移失败可回滚。

M2 渲染引擎:行/组件/tokens(2–3 天)

  • 交付:
    • 统一行/组件渲染模型:fixed 与 custom 共用行结构、背景、进度条、选中框、隐藏规则、淡出、圆角/阴影。
    • IslandApp.vue 重构为 ClassIsland 信息行(默认日期+课程表),CustomIslandRuntime.vue 行/组件按新规格;ComponentNode.vue 11 类组件默认样式/空数据/布局约束。
    • family 双轨渲染仅作用于 fixed(classisland 信息行 / winisland 旧胶囊);custom 统一 ClassIsland 规格;默认按用户升级策略选择;设置页可切换。
    • DOM/CSS token 结构(第 6/7 部分)落地;旧 winisland 视觉保持。
  • 验证:组件快照测试(关键 computed 样式/DOM 结构);无头启动渲染;--visual 像素基准(1920×1080、深/浅色、两风格)。
  • 完成标准:两风格可切换且互不污染;行/组件像素值达标;旧观感无回归。

M3 消息、课程提醒、强提醒、天气、课件(2–3 天)

  • 交付:
    • 气泡两阶段(遮罩斜切 360/120ms、文字 260/250ms、scale 1.1→1.0/750ms、退出 200ms)与队列/去重/暂存/紧急/叫人规则。
    • 课程提醒五类 + 下课前蓝色秒数徽标 + 课程进度条。
    • 强提醒(Topmost)独立透明窗口:全屏/大区域、超时关闭、不抢焦点、多显示器、仅在强调提醒开启时出现;编辑/预览不创建。
    • 天气显示/缓存标记;课件覆盖优先与进度。
  • 验证:消息/课程单测(假时钟、假队列);无头推送场景;--visual 气泡/强提醒动画采样。
  • 完成标准:ClassHelper 既有消息/课程规则逐条不变;视觉/动画达标;强提醒不影响其他应用。

M4 设置页与编辑器重构(2–3 天)

  • 交付:
    • IslandSettingsView.vue:经典/自定义切换、风格切换、外观参数(字号阶梯/行距/进度条/强提醒)、课程提醒、指针避让、动画开关;即时保存、校验、回显。
    • IslandEditorView.vue:方案栏、组件库、行/容器、拖拽、组件设置抽屉、编辑模式预览、预览缩放、退出保存/失败重试;与 ClassIsland 编辑模式对齐(选中框、行设置、添加行、组件边距)。
    • IslandComponentSettings.vue:字段与第 7 部分一一对应;未知字段保留。
  • 验证:组件测试 + 无头保存/读取 + 迁移后旧方案编辑;--visual 编辑器截图。
  • 完成标准:所有设置即时生效;编辑器可用、保存可靠、退出不丢;12 行/200 节点/6 层/20 方案边界提示。

M5 动画、主题、细节打磨(1–2 天)

  • 交付:
    • 第 10 部分动画表逐项;60fps;可打断;animations=false/减少动画/失焦/隐藏页暂停。
    • Fluent/Classic 两套岛主题;winisland 玻璃/纯黑/渐变保持;主题切换无闪烁。
    • 交互细节:点击区域、悬停、触摸、键盘、拖拽阈值、选中/取消、右键(如适用)。
  • 验证:DevTools/帧采样;逐项动画检查;像素 diff;无头回归。
  • 完成标准:动画误差 ≤16ms、帧率达标;主题/风格切换无残留;减少动画生效。

M6 迁移、兼容与回归(1–2 天)

  • 交付:
    • 迁移 fixtures 全量回放(fixed/custom/旧 residentEnabled/嵌套容器/极端值/非法数据);回滚演练。
    • winisland 观感对照基线;旧交互逐条回归(消息/课程/天气/课件/编辑器/触摸/多屏)。
    • 旧版本读新配置的兼容处理;日志/诊断/文档更新。
  • 验证:迁移单测 + 回滚测试 + pnpm verify:desktop + --visual 对照。
  • 完成标准:旧配置 100% 无损;回滚可用;旧观感/旧交互无回归。

M7 全量验收与交付(1–2 天)

  • 交付:
    • 第 12 部分逐项勾选;性能数据(启动/CPU/内存/帧率/消息风暴);差异说明;回滚说明。
    • pnpm lint、pnpm typecheck、pnpm build:desktop、pnpm verify:desktop(无头)、--visual 视觉验收记录。
    • docs/app/island.md、docs/dev/implementation.md、docs/dev/island-refactor.md 完成。
  • 验证:按第 12 部分执行;未通过项必须列出原因与修复计划。
  • 完成标准:第 12 部分全通过(或明确标注例外并获得确认);无可见窗口打扰的默认验证可重复通过。

里程碑风险提示

  • M1 迁移必须先于任何渲染改动,否则旧配置会在开发中损坏。
  • M2/M3 必须保留 feature flag(family 与内部开关),确保随时可回退到旧渲染。
  • M4 编辑器依赖 M1 的 schema 与 M2 的渲染组件;不得并行乱改。
  • M5/M6 的像素/动画验收需要稳定的基准图与固定测试数据,禁止用真实班级数据。