- 重构版主提示词(14 parts,面向 ClassHelper 灵动岛重构) - ClassIsland 全量规格提示词(20 parts,含分卷与逐章版本) - 投喂指南、分卷、逐节文件 - 调研记录、一致性基线、生成与质检脚本
464 KiB
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 部分)。
使用说明(给人类读者,不投喂给模型)
- 投喂方式:全文约 12 万 tokens,建议按
out/投喂指南-重构版.md的五卷/逐章方式投喂;一次性投喂需 ≥ 128k 上下文模型。投喂前请把 ClassHelper 仓库(或至少packages/desktop-client、packages/shared、packages/ui、docs/)提供给 ChatGPT(作为代码上下文或附件)。 - 推荐开场:「请先阅读第 0–2 部分,复述你对现状、目标与 7 条裁决的理解;然后给出文件级重构计划(含迁移与回滚),等我确认后再按 M0–M7 逐阶段实现。」
- 证据约定:
packages/...:行号指 ClassHelper 仓库相对路径;repos/ClassIsland/...:行号是 ClassIsland 基准来源(相对本工作区),只用于理解行为,不是运行时依赖。 - 验收纪律:任何「已完成」的声明都必须附带无头验证输出与第 12 部分的逐项勾选结果;未验证项必须标注。
目录
- 使用说明(给人类读者,不投喂给模型)
-
- 给 ChatGPT 的任务声明(从本节开始执行)
-
- 重构目标与范围
- 1.1 为什么重构、以什么为基准
- 1.2 目标状态(重构完成后用户看到什么)
- 1.3 范围(In / Out)
- 1.4 模块级 Keep / Change / Add / Remove
- 1.5 关键设计裁决(必须逐条执行)
- 1.6 成功标准(重构完成的定义)
- 1.7 主要风险与对策
- 1.8 阅读顺序提示
-
- 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 部分)
-
- 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 验收标准(行为级)
-
- 目标架构与文件级改造清单
- 4.0 重构原则(必须逐条遵守)
- 4.1 目标模块边界与目录
- 4.2 文件级 Keep / Change / Add / Remove 总表
- 4.3 关键文件改造详情(逐个文件:现状 → 目标 → 接口 → 风险)
- 4.4 IPC 契约 v2(增量、不改旧通道语义)
- 4.5 迁移与门面的落地顺序(必须按序提交)
- 4.6 风险清单与强制缓解
- 4.7 验收标准(第 4 部分)
-
- 数据契约、schema v2 与配置迁移
- 5.0 总则(必须遵守)
- 5.1 落点与版本治理
- 5.2
appearancev2(config.json.island) - 5.3
islandLayoutv2(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 部分)
-
- 设计 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 验收标准
-
- 岛结构与组件规格(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 验收标准
-
- 消息、提醒、课程提醒、强提醒特效、天气与课件覆盖(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 验收标准(可勾选,全部通过才算完成)
-
- 编辑器与设置页重构(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 验收标准
-
- 动画与动效映射(ClassHelper 现状 → ClassIsland 目标;双风格默认值)
- 10.0 总则与不可协商的规则
- 10.1 ClassHelper 现状动画清单(重构前基线,必须保留语义)
- 10.2 映射总表(目标规格;每条给出双风格默认值)
- 10.2.1 消息气泡完整时间线(按毫秒对齐,验收时逐点核对)
- 10.3 冲突与选择点(必须按默认执行,并保留开关)
- 10.4 性能预算与降级矩阵
- 10.5 实现方式规范(逐条遵守)
- 10.6 逐文件改动清单
- 10.7 验收标准
-
- 兼容性、性能、平台与测试基线
- 11.1 兼容矩阵(必须逐项通过)
- 11.2 配置迁移与回滚(硬要求)
- 11.3 IPC 与契约兼容
- 11.4 性能预算(超预算必须优化)
- 11.5 平台差异与窗口护栏
- 11.6 测试基线(无头优先)
- 11.7 观测与诊断
- 11.8 文档与交付说明
- 11.9 本部分验收标准
-
- 验收清单(逐项勾选,不得跳过)
- 12.1 工程门禁
- 12.2 视觉验收(像素级)
- 12.3 功能验收
- 12.4 动画验收
- 12.5 兼容与迁移验收
- 12.6 无头与不打扰验收
- 12.7 性能验收
- 12.8 文档与交付物
- 12.9 禁止事项(验收时再次确认)
-
- 重构里程碑(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 不可协商的技术与工程约束
- 技术栈原样沿用: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。
- 包边界不变:
packages/shared(类型/常量/纯函数/岛 schema)、packages/ui(共享控件/tokens)、packages/desktop-client(Electron 主进程 + preload + 主窗口渲染 + 独立岛渲染island.html);服务端packages/server与 Web 管理端packages/web-admin不在本次改动范围。 - 岛窗口仍是独立透明 BrowserWindow + 独立
island.html渲染入口 + 独立 island preload;主窗口设置页只作为编辑器入口,编辑预览不创建额外桌面窗口。 - 渲染层零 Node 权限:preload 仅暴露 contextBridge 白名单,不暴露
ipcRenderer;nodeIntegration:false、contextIsolation:true、sandbox:true。 - 禁止
setBackgroundMaterial()/vibrancy/backdrop-filter等会破坏逐像素透明的 API;透明、圆角、阴影全部由 CSS 自绘。 - 数据来源不变:课表/时间点/通知/作业/叫人/课件仍来自 ClassHelper 服务端解析结果(
ScheduleDto、IslandTimePointDto、IslandState、IslandNotification等);不得引入 ClassIsland 的本地 Profile/ClassPlan 档案体系。 - 配置仍写入
config.json.islandLayout(本机,不同步服务端);新增字段必须可迁移、可回滚,旧fixed/custom配置无损升级。 - 既有硬约束必须保留:
winisland兼容形态下的连续圆角与投影常驻、触摸输入、失焦宽限、固定窗口几何护栏;所有形态下消息按 id 去重、通知/叫人双播不重复、课件传输优先、默认验证不创建可见岛或托盘、冒烟假执行不真关机。 - 所有自动验证无头执行;不得修改验收标准来「通过」;未验证项必须标注。
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 工作方式
- 先通读本提示词与 ClassHelper 相关源码,输出:①你对现状与目标的理解复述;②文件级重构计划(含每个文件的改动类型、接口变化、迁移与回滚);③你识别出的歧义点(一次性提问);④M0 的无头验证方案。
- 严格按第 13 部分的 M0–M7 顺序推进;每个里程碑结束都交付「可无头启动 + 通过该阶段验收 + 不回归既有功能」的状态。
- 每个模块完成后写清:改了什么、证据(文件/行号/行为变化)、未完成/推断项、验证方法与结果。
- 遇到本提示词未覆盖的细节:ClassHelper 既有行为优先(业务语义与既有交互),ClassIsland 规格优先(视觉、像素、动画);仍不明确时在代码与交付说明中标注【推断】。
- 不得顺手重构无关模块;保持 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 的工程结构、数据来源与业务规则。
你必须记住三条基准优先级:
- ClassHelper 业务语义优先:消息去重、优先级、上课暂存、紧急展开、叫人胶囊、已读回执、班级设备、课件传输优先等规则不得改变;
- ClassIsland 视觉与动画优先:像素、字号、圆角、间距、颜色、动画时长与曲线以本提示词第 6、7、10 部分为准;
- ClassHelper 既有工程与配置不得推翻:pnpm monorepo、独立岛渲染进程、
config.json.islandLayout、IslandComponentNode/IslandLayoutScheme数据模型、编辑器入口、无头验证纪律继续有效。
1.2 目标状态(重构完成后用户看到什么)
- fixed(经典)默认变成 ClassIsland 风格的课表信息行:屏幕顶部/底部悬浮一条或多条信息行(默认 1 行:日期 + 课程表;行高 40、圆角 8、背景不透明度 0.5、字号 14/16/18/20、底部 3px 进度条),显示当天课程、当前课程、剩余分钟/倒计时;不再是 268×44 的小胶囊。
- 消息到来时的表现:普通通知/作业在岛内展开为 ClassIsland 风格的气泡(行内遮罩 + 内容两阶段:遮罩扫入 360ms + 错峰 120ms,文字延迟 260ms 淡入并 1.1→1.0 缩放 750ms),紧急消息立即展开;保留 ClassHelper 的上课暂存、普通叫人可点胶囊、紧急立即展开等规则。
- 强提醒:可配置的 ClassIsland 强提醒(Topmost 特效)在岛所在显示器全屏/大区域显示,超时自动关闭,不抢焦点、不阻塞操作;默认仅对"强调提醒"启用。
- custom(自定义常驻):保留现有方案/行/组件/编辑器,但渲染与交互升级为 ClassIsland 规格:行高 40、行间距 5、组件外边距 6/6、行设置抽屉、选中框 1.5px 虚线、拖拽把手、隐藏规则、行/组件外观覆盖、进入编辑模式(实际内容预览)、退出保存。
- winisland 兼容风格(只作用于 fixed):
appearance.family = 'winisland'时保留现有经典 fixed 观感(连续圆角、胶囊 268×44、纯黑/毛玻璃/主题色渐变、弹簧形变);custom不随 family 变化,始终使用 ClassIsland 规格(其编辑器/行/组件结构保持不变,只按 07 细化像素)。默认新装classisland,老用户升级迁移为winisland。 - 设置与编辑器:灵动岛设置页新增风格选择、ClassIsland 外观参数(字号阶梯、行间距、进度条、强提醒开关、主题 Fluent/Classic 选择);自定义方案编辑器增加"编辑模式"与行设置抽屉;所有改动即时保存、失败可重试、退出不丢改动。
- 数据与兼容:旧
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 关键设计裁决(必须逐条执行)
- 固定形态风格双轨、默认 ClassIsland:
appearance.family: 'classisland' | 'winisland'只决定 fixed 的形态(信息行 vs 旧胶囊);custom始终 ClassIsland 规格。新装默认classisland;旧用户升级保持winisland(避免 fixed 观感突变);设置页在 fixed 下提供切换并即时预览。 - fixed 重定义:fixed 不再是"小胶囊消息岛",而是 ClassIsland 风格信息行(默认 1 行,日期+课程表;消息时行内气泡)。原胶囊形态只在
winisland风格下保留。 - custom 保留数据模型:schemes/rows/nodes/expandedRows 不变,只扩展字段与渲染;已有用户方案无需重做即可获得新视觉。
- 数据不迁移:继续消费 ClassHelper 的课表/通知/作业/叫人/课件数据;ClassIsland 的 Lesson/Notification 语义只作为行为参考。
- 强提醒是新增窗口而非替换岛:ClassIsland 的 TopmostEffect 以独立透明窗口实现(无边框、置顶、不抢焦点、超时关闭);只在消息的"强调提醒"开关开启时出现;编辑/预览模式不创建。
- 配置版本化:
islandLayout.version(整数,当前 2);normalizeIslandLayouts()先迁移再归一化;迁移前备份config.json,失败回滚并提示。 - 验证无头:所有自动验证必须后台执行;
--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 阅读与使用方式
- 你在重构前必须先读懂本部分。本部分不是评价现状好坏,而是逐文件、逐行号给出"现在到底是什么样"。重构的每一处改动都必须能在本部分找到对应的现状依据。
- 本部分所有行号以
repos/ClassHelper-master/为工作区相对根,写作packages/desktop-client/...:行号;与 ClassIsland 对照时写repos/ClassIsland/...:行号。行号是行区间起点,读源码时按主题名检索同样有效。 - 现状基线里出现的所有数值(尺寸、时长、阈值、限制)都是重构时必须先保持、再按新规格替换的对象;不得因为"要改成 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 再导出 |
- 重构时不得在这个文件里继续堆逻辑。你必须按 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 命中、穿透、触摸与"点不动"防护
现状把"命中"当成一级问题处理,重构必须原样保留这些防护:
- 窗口默认整块穿透(
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。 - 主进程 60ms 轮询读系统光标,并与渲染进程上报的岛体矩形比对来校正命中(Windows 下 mousemove 转发不可靠)。依据:
src/main/island.ts:2049-2078,2026-2047。 - 岛体矩形由渲染进程限流上报(最小间隔 50ms,尾随保证最后一次送达)。依据:
src/island/IslandApp.vue:412-454。 - 触摸屏:渲染进程按
navigator.maxTouchPoints > 0或首次touchstart上报setTouchMode(true);主进程据此让窗口贴合岛体并始终接收输入,且命中判定的入参被强制为 true。依据:src/island/IslandApp.vue:538-549、src/main/island.ts:245-257,1795-1822,1891-1902。 - Windows 专属
setShape用于触摸模式下把窗口裁成岛体+8px 留白;非 Windows 平台没有该能力,重构必须按 04 的降级方案处理(整窗穿透 + 岛可见区命中),不得依赖setShape才能点。依据:src/main/island.ts:278-316。 - 失焦收起:展开态获取焦点,点到别处
blur→ 回缩胶囊;但必须有两层防护——指针仍在岛上则不收起;刚展开的宽限期内不收起(鼠标 500ms、触摸 1200ms)。依据:src/main/island.ts:557-578,2287-2297,2020-2023。 - 窗口「始终可激活」(
focusable:true),收起时用blur()而不是关掉可激活性——Windows 下WS_EX_NOACTIVATE会丢mousedown,胶囊就点不动。依据:src/main/island.ts:1655-1672。
2.5 消息状态机(fixed 经典模式)
- 状态载体
IslandState:mode: 'hidden'|'pill'|'expanded'、active、queued、inClass、currentPeriodEnd、reason、listExpanded、maxCardHeight、lessonReminder、coursewareTransfer、updatedAt。依据:packages/shared/src/types.ts:1198-1230。 - 分流规则(必须作为重构的行为基线):紧急(
priority==='URGENT')立即展开、无视上课;上课期间普通通知只入队并隐藏;上课期间普通叫人保持胶囊可点、不自动展开;上课期间用户主动点开的通知允许显示(explicitShowId);预览模式(设置页"预览效果")直接展开、失焦不收起、30 秒自动收尾。依据:src/main/island.ts:684-702,794-867,2338-2380。 - 双链路合并:同一 id 经
notification:new与call:new双播时必须合并——类型取"更特殊"(call 优先)、优先级取更高,避免叫人被降级成普通通知。依据:src/main/island.ts:704-724。 - 排序:按优先级权重(
PRIORITY_RANK:LOW 0 / NORMAL 1 / HIGH 2 / URGENT 3)降序,同级按createdAt倒序。依据:packages/shared/src/constants.ts:183-209、src/main/island.ts:726-749。 - 超时表(毫秒):胶囊 15000、下课后展开 14000、下课后胶囊 25000、展开 20000、紧急 45000、叫人 90000;关闭动画 360;收回时先切胶囊、待动画后再淡出隐藏。依据:
src/main/island.ts:121-132,1106-1128。 - 收起的判定:只要还有未处理消息(当前或队列)就保持胶囊;队列清空/用户点了知道了/标为已读/进入上课才彻底隐藏。依据:
src/main/island.ts:1266-1296。 - 多条消息:展开态且待处理 ≥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。 - 动作:
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。 - 预览/冒烟注入口:
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 行)
- 卡片结构:
.island-root > .island-card > svg.shape + span.content >(tint + pill-layer + ClassicNotifications + LessonReminderContent)。依据:src/island/IslandApp.vue:573-681。 - 内容交叉淡入:胶囊层
1 - progress*1.5退净(进度 2/3 处),展开层progress²进入;progress由高度差推导。依据::304-309,346-351。 - 胶囊标题按类型:叫人→"老师叫你"、作业→"新作业"、紧急→"紧急通知"、普通→"新消息";多条时汇总为「新消息:叫人/作业/通知(共 N 条)」。依据:
:115-146。 - 停靠:
data-anchor决定内容贴边(左贴左/右贴右/居中);居中用left:50% + translateX(-50%),左右/上下停靠留ISLAND_SHADOW_PAD。依据::353-383、src/island/classic-island.css:207-233。 - 形状与投影:路径
fill: var(--wn-bg)+ 1px 描边;投影始终声明、只在展开态改参数(避免加/去 filter 造成开合闪一帧)。依据:src/island/classic-island.css:102-153。 - 状态色:紧急红
#ff453a、叫人橙#ff9f0a、作业用 accent;只做内层径向微染 + 描边,不外溢。依据:src/island/IslandApp.vue:151-157、classic-island.css:155-185。 - 内容层裁剪:
.content{clip-path:var(--card-path)};圆角路径由 SVGpath()注入 CSS 变量--card-path。依据:classic-island.css:127-133、IslandApp.vue:363-371。 - 轮廓保护:
island-root.embedded用于设置页内嵌预览(去掉绝对定位)。依据:classic-island.css:41-47、IslandApp.vue:578-581。
2.7 自定义布局(custom)运行时与数据
- 数据根:
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。 - 组件方案
IslandLayoutSettings:layout、residentEnabled、pointerFadeEnabled/Reversed、lessonReminders、activeSchemeId、schemes[]。依据:packages/shared/src/island-layout.ts:131-150。 - 方案
IslandLayoutScheme:id/name/rows/expandedRows/scale/color/foreground/opacity/radius/separated/spacing;rows为常态行、expandedRows为"无消息时点击展开"的内容。依据:island-layout.ts:109-130。 - 行
IslandLayoutRow:id/name/main/allowNotifications/gap/nodes;main行唯一(normalize 会强制只留一个)。依据:island-layout.ts:109-116,334-356。 - 节点
IslandComponentNode:id/kind/settings/appearance/advanced/children;kind14 类(date/clock/weather/countdown/schedule/text/separator/slide/rolling/group/stack/notification/homework/call)。依据:island-layout.ts:3-18,77-108。 settings:文本/模板、12 小时制、秒、倒计时目标与目标日、地点/经纬度、显示模式(当前/下一节/今日)、显示数量、显示起止时间、显示进度、排列方向、间距、轮播间隔、滚动速度。依据:island-layout.ts:80-99。appearance:资源覆盖开关、四档字号、前景/背景/透明度/圆角覆盖、整体透明度;advanced:隐藏规则(in-class/no-message/no-class)、对齐、固定宽/最大宽。依据:island-layout.ts:61-76,100-107。- 归一化边界(重构后仍必须保留的硬边界):最多 12 行、200 节点、6 层、方案 ≤20;颜色必须
#rrggbb;非法 kind 丢弃;重复 id 重命名;main行唯一。依据:island-layout.ts:256-403(尤其:269,337,358)。 - 自定义运行时:先测量自然内容(compact/details/lesson 三份),再按工作区与方案
scale适配;两个页面层(常态/详情)交叉淡入,第三层承载课程提醒;铃铛显示未处理数(>99 显示99+)。依据:src/island/CustomIslandRuntime.vue:58-124,238-323,338-527。 - 鼠标避让淡化:常驻模式按指针是否在岛内切换透明度到
CLASSISLAND_ISLAND_METRICS.fadedOpacity=0.05(可反转);触摸/编辑/课程提醒期间暂停。依据:src/main/island.ts:1851-1877、island-layout.ts:35。 - 编辑布局时窗口
interview行为:editingLayout下不参与命中轮询、不自动收起;island:edit-layout与托盘入口都走editLayout()。依据:src/main/island.ts:350-356,1430-1433。 - 常驻窗口几何:窗口固定为
min(960, 工作区宽) × min(640, 工作区高 - marginY),内容在画布内弹簧形变;常驻开合期间窗口不做逐帧 setBounds。依据:src/main/island.ts:1522-1528、CustomIslandRuntime.vue:214-216。
2.8 课程提醒与课程组件
LessonReminderTracker只用服务端解析后的ScheduleDto计算五类提醒:准备(默认提前 60s)、上课、课间、下课前倒数(默认 60s)、课中进度;每种提醒的进入/退出时间由maskUntil/endsAt表达。依据:src/main/lesson-reminder.ts:14-110、packages/shared/src/island-layout.ts:140-158。- 主进程每秒刷新一次提醒;自定义布局下
in-class提醒不接管岛体(课程组件持续显示进度),ending提醒只作为课程名右侧秒数徽标。依据:src/main/island.ts:171-213、docs/app/island.md:65-70。 - 提醒条 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。 - 经典模式临近下课用课程名右侧蓝色圆形徽标显示剩余秒数(剩余 1 秒显示
-1),不显示"即将下课"遮罩。依据:docs/app/island.md:68-70、CustomIslandRuntime.vue:30-33、ComponentNode.vue:259-266。 - 关闭"即将下课倒数"后不得显示徽标;阈值沿用本机提醒设置。依据:
docs/app/island.md:69、lesson-reminder.ts:54-60。
2.9 天气现状
- 天气只在主进程抓取(固定服务、固定参数、不接受渲染层传 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。 - 渲染层
WeatherBrief.vue每分钟刷新一次;天气码映射文案(晴/少云/多云/阴/雾/毛毛雨/雨/雪/雷雨);断网或陈旧时显示"· 缓存";未配置坐标提示"请设置天气地点"。依据:src/island/WeatherBrief.vue:12-90。 - 冒烟模式禁止访问外部天气(
ELECTRON_SMOKE_TEST==='1'且使用真实 fetch 时直接抛错)。依据:island-weather.ts:33-34。
2.10 课件传输覆盖
- 主进程持有
coursewareTransfer,coursewareShowing时优先覆盖岛内容;syncWindow分支强制显示、取消淡入、不接收鼠标;formSize()返回课件固定尺寸。依据:src/main/island.ts:661-676,1420-1429,1956、src/main/courseware.ts:36。 - 课件卡尺寸
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)
- 桥接职责:从 IndexedDB 缓存/服务端取当天课表与完整时间点,每秒复算"是否上课",变化时通过
window.desktop.islandSetClassState下发给主进程;主进程据此决定通知立即弹/暂存/下课补弹。依据:src/renderer/island/bridge.ts:21-56,148-172,416-435。 - 课表来源
/schedules/active-week,同时构建IslandTimePointDto[](含课间/分割线/隐藏标记),课程名优先取日课表条目、无条目再回退时间点默认课程;离线缓存只有条目时退化为"只有上课时间点"。依据:bridge.ts:57-96,107-136。 - 通知投递:普通通知、新作业(带"本机录入免打扰"指纹去重)、叫人(带"请尽快前往"副文案)分别打包成
IslandNotification,随inClass/currentPeriodEnd上下文推给主进程。依据:bridge.ts:174-289。 - 离线补弹:恢复联网时取最近 5 条离线未读补投,推完清离线起点;冒烟模式跳过。依据:
bridge.ts:291-364。 - 已读回流:主进程收到岛的"标为已读"后转发给主窗口,桥接订阅并调用通知 store 标记已读(离线先本地改)。依据:
bridge.ts:366-405、src/main/index.ts:237-245。 - 生命周期:登录后启动、退出登录/切班时停止;停止时必须清定时器、解订阅、清本地课表并主动同步
inClass:false。依据:bridge.ts:437-473、src/renderer/App.vue:64-93、src/renderer/layouts/ClientLayout.vue:396-405。
2.13 配置与数据契约现状
config.json的岛相关字段:islandLayout(完整方案)与island(外观);两者都有逐字段归一化与默认值。依据:src/main/config.ts:26-84,124-163,198-233,334-346,435-523;配置文件路径userData/config.json见:165-167。- 写入语义:内存缓存 + 120ms 合并写 + 退出前
flushConfigSync;不允许每次读取都同步读盘。依据:config.ts:235-346。 - 外观契约
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。 - 经典外观默认:
width 268、height 44、radius 20、fontSize 13、opacity 1、accent #0a84ff、style 'black'、position 'top-center'、marginX/Y 8。依据:constants.ts:265-290。 - 语义色与排版系数:
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。 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 现状(重构必须保持的契约)
- 主窗口桥接:
getConfig/saveConfig/clearConfig/appInfo/openExternal;岛:islandPush/islandRemoveNotification/islandSetClassState/islandGetState/islandSetAppearance/islandGetAppearance/onIslandMarkRead/onIslandMarkAllRead;另有islandEditLayout/onIslandEditorOpen。依据:src/preload/index.ts:25-99。 - 岛窗口桥接:
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。 - 主进程侧注册:
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。 - 安全规则:所有
invoke必须校验发送方是岛窗口(event.sender === island.getWindow()?.webContents);渲染进程不能读写配置(get-layout是唯一读取口,save-layout由主进程做归一化)。依据:src/main/island.ts:2339-2356,2345-2350。
2.15 编辑器与设置页现状
- 设置页「灵动岛」:布局二选一(fixed/custom)、自定义方案入口、鼠标避让两个开关、外观滑块(宽/高/圆角/透明度/字号/速度)、主题色、视觉风格、位置、左右/上下边距、跟随鼠标屏幕、动画开关、始终置顶、空闲细缝、"测试灵动岛"(本地推一条 30 秒预览通知)。依据:
src/renderer/views/settings/IslandSettingsView.vue:26-160,162-392。 - 设置页改动即生效:外观走
window.desktop.islandSetAppearance实时下发,保存才落盘;自定义模式下圆角滑块会同步写当前方案圆角。依据:IslandSettingsView.vue:112-133。 - 编辑器是主窗口的设置子路由
/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。 - 编辑器保存:深拷贝 +
normalizeIslandLayouts+ 串行保存队列(saveQueue)+ 修订号(只有最后一次成功才算 saved)+ 路由离开前等待保存;上限:组件总数 200、方案 20。依据:IslandEditorView.vue:81-105,338-347,379-383。 - 拖动/放置:指针事件实现拖拽(
beginDrag/startDrag/dragTarget/moveDrag/dropPreview),编辑标签节点用IslandEditorNode.vue,支持 before/after/inside 三种落点。依据:IslandEditorView.vue:443-539、src/renderer/components/IslandEditorNode.vue:1-124。 - 组件设置抽屉:内容/外观/高级三页;内容按 kind 提供字段,外观含独立字号/前景/背景/透明度/圆角/整体透明度,高级含对齐/固定宽/最大宽/隐藏条件;底部支持包裹到容器、复制、删除。依据:
src/renderer/components/IslandComponentSettings.vue:26-307。 - 编辑预览:编辑器内嵌预览区(
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)
- 保留经典岛连续圆角、投影常驻、触摸输入、失焦宽限、固定窗口几何护栏。依据:
repos/ClassHelper-master/AGENTS.md§4「灵动岛」。 - 禁止
setBackgroundMaterial();毛玻璃只能是 CSS 半透明。依据:src/main/island.ts:1726-1748。 - 消息按 id 去重;通知/叫人双播不得重复;本机录入作业不上岛(指纹 + id 双判定)。依据:
src/main/island.ts:713-724、bridge.ts:191-267。 - 编辑预览不创建额外桌面窗口;打开编辑页不自动切布局,应用后才启用自定义方案。依据:
docs/app/island.md:47。 - 默认验证不创建可见岛/托盘;只有显式
--visual才做桌面像素/动画验收;冒烟模式跳过外部天气与离线补弹。依据:docs/dev/testing.md:9,29-32、island-weather.ts:33-34、bridge.ts:326-331。 - 经典岛与自定义岛共用一个原生窗口,切布局不得新开第二个岛。依据:
docs/app/island.md:8-10、src/main/island.ts:468-634(单this.win)。 - 左右边距只在左/右停靠生效,居中锚点永远是屏幕中线;
marginX对居中无效必须在 UI 说明。依据:docs/app/island.md:54、IslandSettingsView.vue:88-94。 - 所有验证不得打扰真实桌面(不弹窗、不发系统通知、不挪真实鼠标);冒烟注入点只能改"逻辑光标"。依据:
src/main/island.ts:1910-1930。
2.17 现状问题清单(重构动因,逐条对应 04 的改造项)
src/main/island.ts单文件 2445 行,窗口/命中/消息/课程/课件/外观全部耦合,难以按 ClassIsland 规格替换视觉与交互。- 几何与尺寸常量分散在主进程
BASE/SIZES、sharedISLAND_SIZE_DELTA、渲染进程targetSize()三处,主/渲染各算一次,改动时容易漂移。 - 经典与自定义是两套独立运行时,但共享消息面板、课程提醒、命中上报;重构需要抽出公共"岛壳 + 消息面板 + 课程条"层,避免两套 UI 各改一遍。
- 数据里没有版本号:
config.islandLayout无version,新增 ClassIsland 行/组件/外观字段时无法做确定性迁移与回滚。 - 现状行/组件缺少 ClassIsland 的行级外观(背景色/不透明度/圆角/字号覆盖)、行级隐藏规则、行级进度条等字段;只有节点级覆盖。
- 提醒只有"条状提醒 + 徽标",没有 ClassIsland 的两阶段气泡与强提醒 Topmost 特效;需要在视觉层新增但不得改变现有消息队列语义。
- 主进程状态机把"提醒优先级、上课守卫、预览、课件"混在一个
syncWindow里,重构需要拆成"状态投影 + 可见性决策 + 几何/命中"三层。 - 编辑器保存/校验逻辑在 Vue 侧,主进程只做
normalizeIslandLayouts;重构应把 schema 校验/迁移收口到 shared,编辑器和主进程共用。
2.18 验收标准(第 2 部分)
- 你能在不运行程序的情况下,仅凭本部分定位:岛窗口创建位置、消息分流规则、命中轮询、触摸模式、失焦宽限、收回动画、课程提醒、课件覆盖、设置页与编辑器的入口文件与行号。
- 你能列出重构前必须保留的 8 条硬约束与 8 条现状问题,并说明每条对应到 04 的哪个改造项。
- 你能复述现状数据流:服务端
ScheduleDto→bridge.ts→islandSetClassState→ 主进程状态机 →island:state→ 渲染进程;以及通知/作业/叫人三条投递链路的差异。 - 你能指出任何一处"主进程与渲染进程各算一次"的几何/布局逻辑(窗口包围盒、列表高度、命中矩形、细缝尺寸、字号推导),并确认 04 已把它们收口到单一事实来源。
- 本部分引用到的每个数值都能追到
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)包含「背景层 + 内容层(组件集合) + 提醒层(遮罩/正文) + 进度条」。行是提醒的宿主,也是编辑的最小单位。
- 行高固定 40,且同一行内所有组件共享该高度;组件区左右内边距 12、最小内容宽 20(
repos/ClassIsland/ClassIsland/Controls/MainWindowLine.axaml:16;repos/ClassIsland/ClassIsland/XamlThemes/FluentTheme/Styles.axaml:110)。 - 行数无硬上限,但默认只有 1 行;行间距默认 5(
repos/ClassIsland/ClassIsland/Models/Settings.cs:219,范围 0–150、步进 5)。 - 必须存在且只存在一行「主行」(MainLine):删除到只剩 1 行时不得再删,必须提示「至少需要保留 1 个主界面行。」(
repos/ClassIsland/ClassIsland/Views/EditModeView.axaml:428文案,MainWindow.axaml.cs的删除守卫)。 - 行模板三分支(必须按条件选,不得混用,
repos/ClassIsland/ClassIsland/XamlThemes/FluentTheme/Styles.axaml:275-278):- Full:
IsNotificationEnabled = true,含遮罩/正文/进度条; - Min:
IsNotificationEnabled = false,只有背景与内容; - 分体(Separated):
IsIslandSeperated=true或行级IslandSeparationMode=2,整行背景改为每个顶级组件各自胶囊。
- Full:
- 行的可见性(必须逐条实现,
repos/ClassIsland/ClassIsland/Controls/MainWindowLine.axaml:29-49):
visible = ( (!IsAllComponentsHid || IsOverlayOpen) && (HideOnRule ? IsVisible : true) ) || IsEditMode
IsAllComponentsHid:该行所有组件都被隐藏时整行消失,不能留一条空胶囊;IsOverlayOpen:该行正在播提醒时必须可见(即使组件全隐藏);HideOnRule:该行命中隐藏规则集时隐藏;规则集变化要实时重算(订阅规则状态更新);- 编辑模式下永远可见。
- 行级不透明度默认 1.0(范围 0.1–1、步进 0.05),只作用于行内容;背景不透明度是另一个独立设置(默认 0.5),只作用于背景层,文字必须始终 100% 不透明(
repos/ClassIsland/ClassIsland/Models/Settings.cs:64,183-184)。 - 鼠标淡出(必须实现):
IsMouseInFadingEnabled(默认 true)与IsMouseInFadingReversed(默认 false)决定「指针不在行内时行变淡到 0.05」,判定用行的屏幕矩形(含 DPI 与缩放),编辑模式不淡出(repos/ClassIsland/ClassIsland/Controls/MainWindowLine.cs:503-509,619-637)。 - 行背景宽度 = 内容实际宽度,不是窗口宽度;内容宽度变化必须平滑跟随(Fluent 300ms / Classic 550ms),且不得触发窗口尺寸变化(
repos/ClassIsland/ClassIsland/Controls/MainWindowLine.cs:614-617;动画细节见第 10 章)。 - 行的提醒宿主能力:
AcceptsNotificationRequests = IsNotificationEnabled && !IsAllComponentsHid && !_isOverlayOpen && !_isUnloading——只有同时满足这些条件的行才能接收提醒;多行时由提醒分发规则决定落到哪一行(repos/ClassIsland/ClassIsland/Controls/MainWindowLine.cs:814)。 - 行级设置(必须保留在编辑器中可改):不透明度、是否允许显示提醒、隐藏规则、圆角/间距(继承全局或覆盖)、附加设置(见第 7 章)。
3.2 组件系统(11 类 + 容器)
必须实现 ClassIsland 的 11 种内置组件与 4 种容器(repos/ClassIsland/ClassIsland/Controls/Components/),并保留 ClassHelper 现有的同名组件作为承载:
| 组件 | 职责(行为) | 关键默认值 |
|---|---|---|
日期 DateComponent |
显示「今天」或自定义格式日期;支持农历/自定义模板(模板语法按组件设置) | 显示今天 |
时钟 ClockComponent |
显示当前时间;支持 12/24 小时、秒、自定义格式 | 24 小时制、显示秒 |
倒计时 CountDownComponent |
目标时间倒计时;目标可固定日期或「按课表倒数至上课/下课/放学」 | 见 3.7 |
课程表 ScheduleComponent |
今日时间点/当前课程/下一节,见 3.7 | 见 3.7 设置表 |
天气简报 WeatherComponent |
天气图标 + 温度 + 文本;多种主信息模式 | 见 3.10 |
文本 TextComponent |
静态文本,支持模板变量与滚动 | 见组件设置 |
滚动容器 RollingComponent |
单行内容超宽时循环滚动 | 见组件设置 |
轮播容器 SlideComponent |
多个子组件按间隔轮播 | 见组件设置 |
分组容器 GroupComponent |
子组件横向成组,统一间距 | 见组件设置 |
堆叠容器 StackComponent |
子组件纵向堆叠 | 见组件设置 |
分割线 SeparatorComponent |
竖线分隔,2px 宽 25px 高(视觉细节归第 7 章) | — |
必须实现的组件通用语义:
- 组件宿主间距:行内相邻组件水平间距 12(每个组件
Margin="6 0");容器组件内部用Margin="-6 0"抵消,避免嵌套累积(repos/ClassIsland/ClassIsland/MainWindow.axaml:396)。 - 组件可见性:组件自身有
IsVisible;行内「全部组件都不可见」→ 整行IsAllComponentsHid(见 3.1)。 - 组件级隐藏规则(
custom模式必须对齐):按in-class(上课中)、no-message(无消息)、no-class(无课/放学后)三个条件隐藏;ClassHelper 现有advanced.hideRules的三值语义与之一一对应,必须保留。 - 组件宽高:
advanced.align/width/maxWidth必须生效;stretch为默认,剩余空间按 flex 分配。 - 组件的附加设置:附加设置可挂在科目/时间点/课表/时间表(ClassIsland 语义,见第 7 章),本重构只保留 ClassHelper 已有的「课程控件附加设置」语义,不引入 ClassIsland 本地档案。
- 编辑态:所有组件必须可被点击选中、可拖动排序、可跨行移动;容器组件展开子项供选择(第 9 章编辑器规范)。
- 空数据兜底:任何组件在无数据时必须渲染占位(不得空白、不得抛错),占位文案与场景见第 7 章。
3.3 编辑模式(行为语义)
必须实现「编辑模式」作为岛的唯一配置入口,行为如下(repos/ClassIsland/ClassIsland/MainWindow.axaml.cs:510-528,1391-1403):
- 进入条件:菜单「编辑主界面…」或 URI
classisland://app/edit;进入前必须通过权限校验(ClassHelper 侧对应「已登录班级设备」判断)。 - 进入步骤(按序):置编辑标志 → 激活窗口 → 挂载编辑视图 → 同步特效窗口的编辑态(编辑期间不播强提醒特效)→ 更新窗口层级 → 编辑模式下窗口必须接收鼠标。
- 编辑画布:画布覆盖整个屏幕;岛内容在画布内按停靠位置排布,并预留底部操作栏空间(操作栏高度 64 的语义见第 7/9 章);编辑模式内容整体垂直居中。
- 编辑态可见性:所有行、所有组件(含被隐藏规则隐藏的)都必须可见可选中;隐藏规则命中时以半透明/斜纹提示,而不是直接消失。
- 底部操作栏(行为):重置视图、外观抽屉、添加组件抽屉、组件配置方案、完成;「完成」退出并保存。
- 两级抽屉:主抽屉(外观/添加组件/配置方案)与次抽屉(组件设置);抽屉打开时挂载「逐项入场」动画;拖拽组件时主抽屉自动临时收起,结束后恢复。
- 退出:关闭抽屉 → 清编辑标志 → 清教程提示 → 保存组件配置(保存必须幂等、原子:写临时文件再替换);退出后恢复逐像素透明与点击穿透。
- 编辑期间:不得创建额外桌面窗口;不得因编辑导致提醒被丢弃(提醒进队列,退出后按规则补播);强提醒特效丢弃但提醒本体照常。
- 配置方案:支持多套组件配置方案(ClassHelper 现有
schemes),方案切换即时生效并持久化;方案 JSON 导入导出走本机配置,不上服务端。
3.4 隐藏与显示规则(完整判定)
必须实现两档 hideMode(默认 0,repos/ClassIsland/ClassIsland/Models/Settings.cs:185;repos/ClassIsland/ClassIsland/MainWindow.axaml:150-163):
- 0 基本:三个独立开关,全部默认
false:HideOnClass:当前时间状态为「上课」时隐藏;HideOnMaxWindow:前台窗口最大化时隐藏;HideOnFullscreen:前台窗口全屏时隐藏。
- 1 高级:用规则集
HideRules求值(规则集/逻辑见第 8 章的规则语义;ClassHelper 侧只保留独立开关 + 上课状态条件,不引入完整规则集)。
检测数据源与频率:
- 全屏/最大化状态在每个主计时器 tick 检测,基准屏幕必须是「岛所在屏幕」(
repos/ClassIsland/ClassIsland/MainWindow.axaml.cs:451-456)。 - 排除进程名单
ExcludedFullscreenWindow默认["explorer"]:这些进程在前台时不算全屏/最大化(repos/ClassIsland/ClassIsland/Models/Settings.cs:56-59)。 - 强制显示(覆盖隐藏):正在播放提醒或处于编辑模式时,即使命中隐藏条件也必须显示,并临时置顶(见 3.9 的置顶锁语义)。
- 手动隐藏:
IsMainWindowVisible(默认 true)为用户手动开关;手动隐藏时同时不接受新提醒(与 3.1 第 9 条一致)。 - 全部隐藏:所有行都隐藏时,可见内容
opacity:0,窗口包围盒不变、窗口不销毁,保证提醒到来能立刻显示(repos/ClassIsland/ClassIsland/MainWindow.axaml:135-141)。 - ClassHelper 映射要求:
custom模式的「常驻」不能被HideOnClass直接隐藏——上课时只允许按hideRules:in-class隐藏指定组件;fixed+classisland信息行按 ClassIsland 默认(HideOnClass=false)上课也保持可见,只是「普通消息暂存、紧急/叫人例外」;不得回到「上课即整岛隐藏」;fixed+winisland兼容形态保留 ClassHelper 原规则(上课时普通消息暂存并隐藏岛体,紧急与叫人例外,见 3.9 与第 8 章)。
3.5 多显示器、停靠与点击穿透
- 6 个停靠位置:顶/底 × 左/中/右(
0/1/2贴工作区上边、3/4/5贴下边;左/中/右分别为贴左/居中/贴右),窗口始终横跨整屏,岛在窗口内形变(repos/ClassIsland/ClassIsland/Models/Settings.cs:48)。 - 偏移:
WindowDockingOffsetX/Y必须生效;顶部停靠时正值向下偏移不得被屏幕边缘裁掉(偏移吞进安全区)。 - 多显示器:
WindowDockingMonitorIndex选择目标屏;可选「跟随鼠标所在屏」(ClassHelper 现有followCursorDisplay保留)。 - DPI:所有几何与命中必须按「物理像素 × 缩放」换算,4K@150% 与 1080p@100% 下不得出现 1px 白边、黑边、圆角锯齿;圆角为 0 时是直角,为最大值时左右边缘为半圆。
- 点击穿透:非编辑模式下,岛默认鼠标穿透(点击落到桌面应用);只有指针真正在岛体上时才接收鼠标;编辑模式下必须接收(
repos/ClassIsland/ClassIsland/MainWindow.axaml.cs:839;ClassHelperpackages/desktop-client/src/main/island.ts:2000-2080的命中轮询必须保留)。 - 置顶锁:提醒播放期间临时置顶,多提醒时以「锁计数」方式管理——只有最后一个锁释放才还原层级,避免闪烁(
repos/ClassIsland/ClassIsland/MainWindow.axaml.cs:1056-1092)。
3.6 时间状态机与跨天(课程计算的唯一事实源)
- 主计时器周期 50ms,优先级 Render;每 tick 顺序:
PreMainTimerTicked→ 计算 →PostMainTimerTicked(repos/ClassIsland/ClassIsland/Services/LessonsService.cs:39-45,373-388)。ClassHelper 映射为主进程单一 1s/50ms 混合节拍(秒级 UI 用 1s,跨点判定用 50ms【推断:可降到 200ms,见 3.13 性能非功能】)。 - 时间状态枚举:
None / OnClass / PrepareOnClass(预留) / Breaking / AfterSchool(repos/ClassIsland/ClassIsland.Shared/Enums/TimeState.cs:6-27)。 - 每 tick 计算顺序(必须逐条实现):
- 清理过期临时课表/临时层/临时课表群;
- 若「加载课表」关闭 → 当前课表为空;
- 选当天课表(预定 > 临时 > 常规,详见 parts/06 §6.2);
now = 当前本地时间;- 当前时间点 = 有效时间点中第一个
TimeType∈{0,1}且StartTime ≤ now ≤ EndTime; - 上课 →
CurrentState=OnClass并映射科目;课间 →CurrentState=Breaking,CurrentSubject=课间科目(名称=课间名); - 下一节 = 第一个
TimeType=0 && EndTime ≥ now;下一个课间 = 第一个TimeType=1 && EndTime ≥ now; - 剩余时间:上课时算「距下课」,否则算「距上课」;下限钳 0;
- 下一节与下一课间都不存在 →
AfterSchool; - 统一回写(未算出用默认值);状态变化时才发事件,顺序固定「先
CurrentTimeStateChanged,再按新状态发OnClass/OnBreakingTime/OnAfterSchool」。
- 有效时间点的双向裁剪:禁用一节早课 → 其前所有时间点不显示;禁用一节晚课 → 其后所有时间点不显示(
repos/ClassIsland/ClassIsland.Shared/Models/Profile/ClassPlan.cs:54-98)。ClassHelper 侧映射为「ScheduleDto投影 + 当前时间点/下一节,由服务端解析结果决定」,不得在客户端重写轮换规则。 - 跨天:每次 tick 检测日期变化;跨天必须重新取当天课表、清除「今天」的已提醒标记、重置课程提醒状态机(
LessonReminderTracker的dayKey变化),不得沿用昨天的「已提醒」状态。 - 时间突变(手动改系统时间、NTP 校正、睡眠唤醒):下一次 tick 必须按新时间重新判定状态;不得补发错过的提醒,也不得因时间回退而重复发出同一提醒(用「状态变化」而非「时间比较」触发)。
3.7 课程表组件的行为(重点)
课程表组件是「一行多条课程」的核心,ClassHelper 的 schedule 组件必须按下列行为实现(视觉像素见第 7 章):
3.7.1 显示哪些时间点(必须逐条判定,repos/ClassIsland/ClassIsland/Controls/Components/ScheduleComponent.axaml.cs:243-251)
对当天每个时间点:
TimeType ∈ {2 分割线, 3 行动}不作为课程项显示(分割线由专门的竖线控件渲染;行动不显示);HideFinishedClass=true且EndTime < now→ 不显示;IsHideDefault=true且不是当前选中项 → 不显示;TimeType==1(课间)且不是当前选中项 → 不显示;ShowCurrentLessonOnlyOnClass=true且当前选中项是上课类型且该项不是当前项 → 不显示。
映射到 ClassHelper(custom 模式的 schedule 组件):display:'today' 必须实现「只当前项展开、其余折叠」的 ClassIsland 语义:
- 上课点折叠只显示课程名;当前时间点展开显示「课程名 + 起止时间」;
- 课间与
isHideDefault的上课点折叠时完全隐藏,轮到它们成为当前项时才出现; - 分割线
DIVIDER渲染为一条竖线;ACTION完全不显示(packages/shared/src/class-plan.ts:4,70-87;packages/desktop-client/src/island/ComponentNode.vue:48-80); - 无时间点数据时回退按
ScheduleDto条目显示课程(docs/app/island.md:69); - 倒计时/徽标只在「当前时间点临近结束」时出现,不得常驻。
3.7.2 三种课程呈现
必须实现三种课程项(repos/ClassIsland/ClassIsland.Core/Controls/LessonsControls/):
- 展开型:科目名(强调字号)+ 附加信息 + 进度条;左右留白按「课程表文本间距」缩放(默认 1,范围 0.2–1,左右各 16×间距);
- 最小型:只显示科目首字(
Subject.Initial),用于IsHideDefault时间点或「上课时仅显示当前课程」; - 分割线:2px 竖线、高 25、次要色、透明度 0.8。
已结束课程淡化:FadeCompletedClasses 默认 true,透明度 0.6,过渡 150ms(CubicEaseInOut);HighlightChangedClass 默认 false,换课课程高亮(强调色块)。
3.7.3 附加信息与倒计时(必须实现 6 种 ExtraInfoType)
ShowExtraInfoOnTimePoint=true 时,当前课程可显示(repos/ClassIsland/ClassIsland/Controls/Components/LessonControlExpanded.axaml:85-171):
| 值 | 显示 | 格式 |
|---|---|---|
| 0 | 起止时间 | hh:mm-hh:mm |
| 1 | 已过时间 | -mm:ss |
| 2 | 剩余时间 | -mm:ss |
| 3 | 进度百分比 | N% |
| 4 | 模糊倒计时 | -<1min(按 CountdownSeconds 向上取整) |
| 5 | 精确倒计时 | -mm:ss |
当前课程剩余时间药丸:IsCountdownEnabled=true 时,当前时间点剩余 ≤ CountdownSeconds(默认 60)才出现;精确模式 -mm:ss,非精确模式 <<数值(IsNonExactCountdownEnabled=false,LessonControlExpanded.axaml:173-214)。
课程进度条:贴底、Maximum=1、值为课程进度、高度 3px(视觉细节归第 7 章);进度更新必须按「比例插值」而非每帧重算布局。
3.7.4 空状态与明天课表
- 空占位(默认显示,
ShowPlaceholderOnEmptyClassPlan=true):- 今天没有课表 →
"今天没有课程。"; - 今日课程全部结束且
HideFinishedClass=true→"今日课程已全部结束。"; - 垂直居中、正文色;占位与课程列表互斥。
- 今天没有课表 →
- 明天课表
TomorrowScheduleShowMode默认1:0不显示、1放学后显示、2总是显示、3今天无可显示课程时显示;显示时左侧出现「明天」药丸、主列表隐藏。 AfterSchool判定 = 当前时间状态为放学后或今天没有课表。
3.7.5 课程表组件设置默认值(必须照抄)
| 字段 | 默认 | 说明 |
|---|---|---|
ShowExtraInfoOnTimePoint |
true |
显示附加信息 |
ExtraInfoType |
0 |
0–5 |
IsCountdownEnabled |
true |
结束倒计时药丸 |
CountdownSeconds |
60 |
阈值(秒) |
IsNonExactCountdownEnabled |
false |
模糊倒计时 |
ScheduleSpacing |
1 |
0.2–1 |
ShowCurrentLessonOnlyOnClass |
false |
上课仅显示当前课程 |
HideFinishedClass |
false |
不显示已结束课程 |
FadeCompletedClasses |
true |
淡化已上课程 |
HighlightChangedClass |
false |
标记换课 |
ShowPlaceholderOnEmptyClassPlan |
true |
空态占位 |
PlaceholderTextNoClass |
"今天没有课程。" |
文案 |
PlaceholderTextAllClassEnded |
"今日课程已全部结束。" |
文案 |
TomorrowScheduleShowMode |
1 |
0–3 |
(证据:repos/ClassIsland/ClassIsland/Models/ComponentSettings/LessonControlSettings.cs:8-23;repos/ClassIsland/ClassIsland/Models/Settings.cs:103-109,208-209,221。)
3.8 提醒队列与优先级(行为)
ClassIsland 的提醒是统一管线,必须实现以下行为:
- 一切提醒都进同一管线:上下课、放学、天气、集控、行动触发的提醒共用队列与消费者;不得各写一套弹窗。
- 两层 UI:遮罩(Mask,短文案,第一阶段)+ 正文(Overlay,详细内容,第二阶段);无正文时只播遮罩(
repos/ClassIsland/ClassIsland.Core/Models/Notification/NotificationRequest.cs:40-63)。 - 队列与优先级:
- 提供方优先级:
NotificationProvidersPriority数组下标越小越先播;设置页可拖动排序。 - 同提供方内先入队先播(FIFO)。
- ClassIsland 源码的
IsPriorityOverride比较器存在取反异常(repos/ClassIsland/ClassIsland.Shared/Models/Notification/NotificationPriority.cs:47-50);本轮目标语义按第 8 章裁决:三级排序键(提供方下标 → 通知等级 Emergency>High>Normal>Low → 入队序),不照抄异常行为。
- 提供方优先级:
- 去重:同一 request 对象只入队一次;出队用「已弹出集合」避免链式头节点重复;天气预警按
Detail会话内去重;ClassHelper 侧另有「同 id 通知/叫人双播合并」规则(见第 8 章),两者不冲突。 - 链式提醒:数组请求串成链,任一子请求取消即取消全部;准备上课 + 上课是典型链(
repos/ClassIsland/ClassIsland/Services/NotificationHostService.cs:205-254)。 - 消费者:按优先级排序,只有
AcceptsNotificationRequests && QueuedNotificationCount<=0才推送;无消费者时退回队列;播放完主动拉取后续(NotificationHostService.cs:337-419)。 - 播放状态机:
None → Queued → Playing →(Paused/Cancelled/Completed);取消/完成分别触发对应令牌回调;被中断但未被取消的提醒必须重新入队(NotificationHostService.cs:488-522)。 - 取消与清理:
CancelAllNotifications清空队列 + 取消所有正在播放的请求;用户入口为主界面右键菜单「清除全部提醒」,仅在「有提醒正在播放」时可见(repos/ClassIsland/ClassIsland/MainWindow.axaml:86-94);不实现独立「清除提醒浮动按钮窗口」(源码#if false遗留)。
3.9 通知两层 UI(气泡 + 强提醒)
3.9.1 气泡(行内)行为
- 遮罩阶段:背景切换为强调色 + 斜切遮罩扫入 + 遮罩文字入场;此阶段不显示详情。
- 遮罩结束:有正文则切正文(背景恢复、正文淡入、底部进度条显示剩余时间);无正文则直接收起。
- 正文阶段:内容 + 底部进度条(
Maximum=1、Value=剩余进度、值过渡 50ms 线性);显示时长由内容Duration或EndTime决定。 - 结束时:正文淡出 → 遮罩退出 → 行恢复原状;队列里还有提醒则继续播下一条。
- 提醒期间行的可见性:强制可见(覆盖隐藏规则),并置顶;提醒结束释放置顶锁。
- 关键动画参数(语义;完整表见第 10 章):斜切遮罩 5 区段、区段 360ms、错峰 120ms、开
QuarticEaseOut/关QuadraticEaseIn;遮罩文字delay 260ms、淡入 250ms、缩放 1.1→1.0、750ms,曲线(0.25,1,0.5,1);退出文字 200ms、缩放 1.0→1.1、曲线(0.5,0,0.75,0);正文淡入淡出 200ms、SineEaseIn。Classic 主题是另一套(上滑/下压)语义,必须两套都保留。 - 音效与 TTS:ClassIsland 支持提醒音效与语音(默认关闭语音、音效音量 1.0);本轮重构不实现音效/TTS(ClassHelper 无音频通道,见 3.13),只保留视觉提醒。
3.9.2 强提醒(Topmost 全屏特效)行为
- 触发:提醒开始且生效设置开启「强调特效」、主界面可见、行未全部隐藏、本次遮罩尚未播过声音。
- 表现:从行中心向全屏扩散的涟漪(默认强调色),扩散 600ms + 淡出 600ms,750ms 后移除元素;特效窗口为独立透明置顶窗口,逐屏覆盖主界面所在屏幕,按
NotificationEffectRenderingScale(默认 1.0)缩放。 - 生命周期:特效元素集合为空即隐藏窗口,不销毁;非
OSShutdown/ApplicationShutdown时禁止关闭窗口;编辑模式下丢弃新特效。 - 必须支持多屏:每个显示器一个特效窗口,屏幕/DPI 变化时重算位置与尺寸。
- ClassHelper 映射:
custom/fixed共用同一套特效窗口;动画速度倍率缩放 600/750ms;animations=false时不播特效只播气泡。
3.10 天气展示(行为)
- 数据频率:天气服务每 5 分钟刷新一次,启动立即刷新一次;刷新成功才置「已刷新」标志,所有依赖天气的提醒/规则先判该标志(
repos/ClassIsland/ClassIsland/Services/WeatherService.cs:41-44,69-71,386-389)。 - 缓存:最近一次天气持久化在设置中,重启先展示旧数据并标注更新时间;刷新失败保留旧数据,不清空。
- 组件展示:天气组件有多种主信息模式(图标 + 温度 + 文本;默认展示当前天气+温度);无数据时显示占位。
- 提醒:气象预警与天气预报的提醒在「上课 / 课间」事件时触发;预警按
Detail会话内去重、显示时长按文本长度钳制 10–90 秒;预报正文 15 秒;预警要求数据已刷新。 - 本轮范围:ClassHelper 已有 Open-Meteo 天气简报(15 分钟缓存、断网标「缓存」、无官方气象预警)。重构后必须保留 ClassHelper 数据源与行为,ClassIsland 的「气象预警/三天预报/逐小时预报提醒」不实现(见第 8 章冲突裁决)。
3.11 主题语义(Fluent vs Classic 的差异)
必须保留两套「风格」的概念(ClassHelper islandAppearance.style 默认 classisland,回退 winisland),差异只体现在行为参数上:
| 维度 | Fluent(默认目标) | Classic(可选回退) |
|---|---|---|
| 行宽过渡 | 300ms、(0.65,0,0.35,1) |
550ms、(0,1,0.5,1.1)(带过冲) |
| 遮罩入场 | 斜切 5 区段扫入 + 文字延迟 260ms | 整层上滑/下压(Y -50→0,200ms QuadraticEaseIn) |
| 遮罩退场 | 文字淡出 200ms + 缩放 | 下移 0→50、200ms QuadraticEaseOut、延迟 100ms |
| 行列表面 | 中性填充 + 1px 描边 + 阴影 0 4 8 2 #48000000 |
类似,但强调色/描边更强 |
| 分体 | 每组件独立胶囊(min-width 40、padding 0 16) | 同语义,参数略异 |
| 主题色来源 | 系统强调色/自定义 | 同 |
硬约束:背景不透明度只作用于背景层;文字始终不透明;投影必须常驻声明、只改参数(禁止在开合瞬间增删 filter);两套主题共用同一行为状态机,只切换视觉参数。
3.12 动画语义总则(实现时必须遵守)
- 进场慢、退场快:进场 200–300ms,退场 100–150ms;提醒遮罩文字淡入 250ms、淡出 200ms,属于「进场慢退场快」的允许例外(遮罩退场本身另有 100–200ms 的层退出)。
- 延迟表达因果:先扫入(0–360ms)、再文字(+260ms)、再投影(+600ms);不得所有元素同时启动。
- 错峰封顶:列表入场每项 25–35ms,超过 10 项必须封顶;不得线性累加到 1 秒以上。
- 可打断:进/出必须成对、可反向,反向时新动画直接覆盖旧动画(不排队);不得用 JS 计时器模拟状态机。
- 动画不改业务:动画只表达状态,不得在动画结束回调里发请求或用动画时长当业务计时。
- 最终几何一致:动画结束后元素几何必须与「无动画直跳终态」逐像素一致。
- 动画级别:
AnimationLevel0/1/2(默认 1);级别 0 禁用动画但主界面提醒动画不在此列(设置项描述明确);同时响应系统「减少动态效果」(视为级别 0 + 时长 1ms,加载指示器放慢不停)。 - 性能禁忌:不对包含文字的容器做
width/margin过渡(用绝对定位背景层或clip-path/scaleX);卡片投影用box-shadow不用filter: drop-shadow;透明窗口禁用setBackgroundMaterial()。
3.13 本轮不实现的 ClassIsland 能力(必须写进实现说明,不得遗漏标注)
以下能力在 ClassIsland 基准里存在,但本轮重构明确不实现,实现者不得自行扩展、也不得因缺失判为缺陷:
- 自动化系统(触发器/行动/规则集/时间点行动):不实现;岛内不出现自动化入口。
- 插件系统与插件市场:不实现;不引入
.cipx、不加载第三方 DLL/模块。 - 主题市场/主题包/壁纸取色:不实现(壁纸取色在 ClassIsland 也已是 P2 停用状态);只做岛内两套风格(
classisland/winisland)。 - 集控/管理端(ManagementServer、策略、审计、授权级别):不实现;ClassHelper 用服务端权限体系替代。
- Excel 导入/导出、档案导入导出、CSES 互转:不实现(属 ClassIsland 本地档案能力);ClassHelper 继续用服务端课表/YAML 导入。
- 自动更新器、启动闪屏、OOBE、欢迎向导、教程中心、崩溃恢复窗口、作者/致谢窗口:不实现(属其它 part/非岛范围)。
- 提醒音效与 TTS(系统 TTS / Edge TTS):不实现;本轮只做视觉提醒。若未来需要,作为独立扩展项,默认关闭。
- ClassIsland 的「即将下课」遮罩:不实现——课程临近下课的提示统一走 ClassHelper 的「课程名右侧蓝色秒数徽标」(见第 8 章裁决)。
- 本地档案/Profile/TimeLayout/CSES 体系:不实现;数据继续用 ClassHelper 服务端解析结果(
ScheduleDto/IslandTimePointDto)。 - 通知音效音量、静音时段、语音源切换:不实现(ClassIsland 有,ClassHelper 无对应通道)。
3.14 验收标准(行为级)
实现者必须逐条自查,全部通过才算完成本节:
- 多行场景:1→5→1 行增删后,行可见性与「主行」约束正确;只剩 1 行时删除被拒绝且提示「至少需要保留 1 个主界面行。」。
- 行可见性表达式三态:组件全隐藏时整行消失;提醒期间强制可见;编辑模式下隐藏行也可见可选。
- 隐藏规则:
hideMode=0三个开关各自生效,explorer前台时不算全屏;提醒/编辑时覆盖隐藏;手动隐藏后不再接收新提醒;全部隐藏时内容透明度为 0 但窗口不销毁。 - 时间状态机:构造「上课→课间→放学」一天,状态与事件顺序正确(状态改变事件先于具体事件);禁用一节早课/晚课后,时间点的双向裁剪与「今天没有课程」占位一致。
- 课程表组件:6 种附加信息、倒计时药丸阈值 60 秒、已结束课程淡化 0.6、明天课表 4 种模式、无课表/全部结束两种占位文案全部正确。
- 提醒:两条提醒先后入队,第一条播完自动播第二条;提醒期间行强制可见并置顶,结束后释放;右键「清除全部提醒」仅在有提醒时出现并立即清空。
- 强提醒:开启特效时从岛中心扩散涟漪,600ms 扩散 + 600ms 淡出、750ms 后消失;编辑模式下不播;多屏时覆盖岛所在屏。
- 主题:Fluent 与 Classic 两套的遮罩入场/退场行为不同(斜切 vs 上下滑),行宽过渡 300ms vs 550ms;切换主题不改变任何业务状态。
- 动画:
AnimationLevel=0时除岛提醒外无动画;系统「减少动态效果」下时长压到 1ms 且加载指示器不停;动画结束后几何与直跳终态一致。 - 非目标确认:界面上不存在自动化、插件、主题市场、集控、Excel 导入、音效/TTS 入口;代码中不存在对应依赖。
- 形态映射:新安装默认
family='classisland'的信息行(日期+课程表、行高 40、底部 3px 进度条),升级用户默认family='winisland'的胶囊形态;custom仍走 schemes/rows/nodes;三种形态切换不丢消息、不重建第二个岛窗口。
4. 目标架构与文件级改造清单
4.0 重构原则(必须逐条遵守)
- 只在现有仓库内重构:不得新建工程、不得换框架、不得引入服务端依赖;沿用 pnpm@11.8.0 monorepo 与 Electron ^44.3.0 + Vue ^3.5.42 + Element Plus ^2.14.5 + Pinia ^4.0.3 + Vite ^8.3.0 + TS ~5.9.3。
- 渐进式替换,不一次性推倒:每次改动后应用必须仍可启动、旧
config.json仍可加载;用"门面(facade)+ 新模块"的方式把旧调用点隔离,避免一次改src/main/index.ts、tray.ts、smoke.ts的全部 import。 - 单一事实来源:任何"主进程与渲染进程各算一次"的数值(窗口包围盒、列表高度、命中矩形、细缝尺寸、字号推导)必须收口到
packages/shared/src/island/,两端只调用同一函数。现状三处漂移见 02 §2.3。 - 保留现有业务语义:消息去重、上课暂存、紧急展开、已读回执、作业免打扰指纹、课件覆盖优先级、离线补弹,全部保持(02 §2.5/§2.12);ClassIsland 只改变"视觉与布局表现"。
- 两套风格族(family)共存,且 family 只作用于 fixed:
appearance.family取值'classisland' | 'winisland';默认classisland;winisland保留原胶囊+详情卡外观;老用户升级默认winisland(迁移见 05)。custom布局始终按 ClassIsland 规格渲染,不受 family 影响(family 只决定 fixed 用哪套渲染器)。 fixed重新定义:在family='classisland'下,fixed不再是胶囊+详情卡,而是 ClassIsland 风格信息行(默认 1 行 = 日期 + 课程表,行高 40、圆角 8、背景不透明度 0.5、3px 进度条、消息以行内气泡呈现)。原"胶囊+详情卡"只在family='winisland'下保留。这是本轮最重要的语义变化,所有文件改造必须按此分叉。- 一个原生窗口:多种布局/风格共用一个
BrowserWindow;切换 family/layout 不得新开窗口(现状约束,见docs/app/island.md:8-10、src/main/island.ts:468-634)。 - 不打扰纪律:默认验证无头/离屏,不创建可见岛或托盘;只有显式
--visual才做桌面像素验收。依据:docs/dev/testing.md:9,29-32。 - 调色板与风格分离:
style: black|glass|tinted继续保留,写为 winisland 调色板字段(canonicalappearance.winislandStyle,旧style作为兼容镜像,不删除);family 与 style 独立,切换 family 时保留用户 style 原值。 - 外观新增定稿字段:
colorScheme: 'light'|'dark'|'system'(默认 system)、theme: 'fluent'|'classic'(默认 fluent)、effectEnabled(默认 true)、effectScale: 1|0.75|0.5(默认 1)、progressBar:{height:3,show:true};fontSize语义随 family:classisland=正文基准(新装默认 16,派生 14/18/20),winisland=旧语义(默认 13,迁移保留原值)。 - 版本只保留一个:落盘版本只有
islandLayout.version=2;schemaVersion仅允许出现在 IPC payload 作运行时校验,不得落盘。 - fixed 消息/提醒口径:行内气泡同一时刻只显示 1 条 active;多条折叠为「还有 N 条」并可打开应用内通知中心;
prepare/on-class/break以行内容接管呈现、ending只用蓝色圆形秒数徽标;队列/去重/暂存规则不变。
4.1 目标模块边界与目录
必须把灵动岛拆成四个边界清晰的部分。目录与职责如下(标注「新增」的目录/文件由本轮创建;其余为移动或改造):
packages/shared/src/island/ # 新增:岛的唯一契约层(纯 TS,无 DOM/Electron)
├─ index.ts # 统一导出(供 desktop-client 与 web-admin 可复用)
├─ schema.ts # IslandLayoutV2 / IslandAppearanceV2 / IslandRowV2 / IslandNodeV2 类型
├─ defaults.ts # ClassIsland 规格默认值(family/行/组件/外观)
├─ normalize.ts # normalizeIslandLayoutV2 / normalizeAppearanceV2(校验+夹取)
├─ migrate.ts # migrateIslandLayoutV1ToV2(旧 config → v2)
├─ metrics.ts # CLASSISLAND_ISLAND_METRICS + WINISLAND_ISLAND_METRICS + 排版系数
├─ geometry.ts # 锚点/包围盒/细缝/触摸贴合尺寸的纯函数(主/渲染共用)
├─ messages.ts # 去重、排序、列表布局、行内气泡分组(纯函数)
├─ lessons.ts # 课程提醒计算(从 main/lesson-reminder.ts 上移)
├─ dto.ts # IslandState/IslandNotification/ScheduleDto 等对外 DTO 与映射
└─ ipc.ts # IPC 通道名与 payload 类型(主进程与 preload 共用)
packages/desktop-client/src/main/island/ # 新增:主进程岛引擎(从 island.ts 拆出)
├─ index.ts # 门面:export { island, registerIslandIpc, ... }(旧 import 不变)
├─ controller.ts # 状态机编排:消息、上课状态、预览、动作分发
├─ window.ts # 建窗、固定包围盒、锚点布局、可见性/焦点/透明度
├─ hit-test.ts # 命中轮询、岛体矩形、interactive 决策(含 setShape 降级)
├─ touch.ts # 触摸模式、贴合、失焦宽限、常驻控件
├─ messages.ts # 队列存储 + 合并 + 超时计时器(无 UI)
├─ lessons.ts # 每秒推进课程提醒
├─ weather.ts # 从 island-weather.ts 移入
├─ courseware.ts # 课件覆盖状态桥
├─ preview.ts # 设置页"测试灵动岛"的预览注入
└─ ipc.ts # island:* 全部 handler(从 island.ts 尾部搬来)
packages/desktop-client/src/island/ # 渲染进程(现有目录内重组)
├─ main.ts # Keep:tokens + motion + mount
├─ LayoutIslandApp.vue # Change:分发(custom→CustomIslandRuntime;fixed 按 family 二选一)
├─ family/
│ ├─ classisland/
│ │ ├─ FixedInfoRow.vue # 新增:ClassIsland 信息行(fixed 新默认)
│ │ ├─ InlineMessageBubble.vue # 新增:行内消息气泡(fixed 下替代详情卡)
│ │ └─ ClassislandTheme.css # 新增:Fluent/Classic 两套行样式与动画
│ └─ winisland/
│ ├─ ClassicIsland.vue # Change:由 IslandApp.vue 迁移并改名(原胶囊+详情卡)
│ └─ winisland.css # Change:由 classic-island.css 迁移(数值不变,仅 fixed 使用)
├─ custom/
│ ├─ CustomIslandRuntime.vue # Change:始终按 ClassIsland 行/组件规格重写测量与适配(不看 family)
│ ├─ CustomRows.vue # 新增:常态行 / 展开行渲染(行级外观)
│ ├─ CustomRow.vue # 新增:单行(行设置、隐藏规则、进度条)
│ └─ CustomRowSettingsDrawer.vue# 新增:行设置抽屉(背景/字号/圆角/隐藏;字段由 09 定义)
├─ shell/
│ ├─ IslandShell.vue # 新增:squircle 形状 + 弹簧形变 + 命中上报(fixed 两族共用)
│ ├─ MessagePanel.vue # Change:由 ClassicNotifications.vue 改造(单条/列表/行内气泡)
│ └─ LessonBar.vue # Change:由 LessonReminderContent.vue 改造(ClassIsland 遮罩)
├─ components/ # 新增:按 kind 拆分的组件渲染器
│ ├─ index.ts # node.kind → 渲染器映射
│ ├─ DateNode.vue / ClockNode.vue / TextNode.vue / CountdownNode.vue
│ ├─ ScheduleNode.vue / WeatherNode.vue / SeparatorNode.vue
│ └─ ContainerNode.vue # group/stack/slide/rolling(rolling 支持 mode continuous|pause)
├─ motion/ # Keep:spring.ts、squircle.ts(可加适配器)
├─ styles/ # 新增:shared.css / classisland.css / custom.css(custom 始终 classisland 命名空间)
└─ Legacy/ # 可选:保留旧文件名再导出门面(ComponentNode.vue 等)
packages/desktop-client/src/renderer/island/ # 主窗口侧的桥(现有)
├─ bridge.ts # Keep:数据桥(课表/上课状态/通知/作业/叫人/补弹/已读)
└─ editor/ # 新增:编辑器逻辑 composables(从 IslandEditorView 抽出)
├─ useIslandDraft.ts # 草稿、脏标记、保存队列、修订号
├─ useIslandDrag.ts # 指针拖拽/落点/自动滚动
└─ useIslandPreview.ts# 内嵌预览缩放与测量
packages/desktop-client/src/renderer/ # 设置/编辑器页面(现有)
├─ views/settings/IslandSettingsView.vue # Change:family 选择器 + 条件外观
├─ views/settings/IslandEditorView.vue # Change:调用新版架构 + 行设置
├─ components/IslandComponentSettings.vue # Change:ClassIsland 组件字段
├─ components/IslandEditorNode.vue # Keep(可加行级拖拽)
├─ components/IslandRowSettingsDrawer.vue # 新增:行设置抽屉
└─ styles/island-editor.css # Change:ClassIsland 编辑器视觉
4.2 文件级 Keep / Change / Add / Remove 总表
| 文件/目录 | 类型 | 一句话目标 |
|---|---|---|
packages/shared/src/island-layout.ts |
Change | 保留导出名兼容;实现迁移到 island/ 新目录,本文件仅再导出 |
packages/shared/src/constants.ts |
Change | 保留旧 WinIsland 常量;新增 ClassIsland 度量与 family 常量(迁移见 05) |
packages/shared/src/types.ts |
Change | IslandAppearance 增加 family/colorScheme/effectEnabled/effectScale/progressBar/theme/winislandStyle;新增 IslandState.family/pendingStaged/bubblePhase/bubbleDeadline、IslandNotification.level、IslandLessonReminder.progress/leftSeconds(运行期,不落盘) |
packages/shared/src/island/ipc.ts |
Add | IPC 通道名与 payload 类型;island:push 增加 source/dedupeKey |
packages/shared/src/motion.ts |
Keep | 不动令牌数值(03/10 部分引用) |
packages/shared/src/class-plan.ts |
Keep | IslandTimePointDto/TimePointType 不动 |
src/main/island.ts |
Change | 2445 行 → 门面(约 30 行),实际逻辑移入 main/island/* |
src/main/island/*.ts |
Add | 见 4.1(controller/window/hit-test/touch/messages/lessons/weather/courseware/preview/ipc) |
src/main/config.ts |
Change | 启动时执行 v2 迁移钩子(唯一版本=islandLayout.version);写盘前校验 family/colorScheme/fontSize/effect*;保留合并写/退出 flush |
src/main/index.ts |
Change | island.init() 改调新门面;注册编辑器/预览所需 IPC |
src/main/ipc.ts |
Change | 岛相关 handler 全部委托 main/island/ipc.ts;主窗口配置剔除规则保留 |
src/main/courseware.ts |
Change | 改为调用 island.setCoursewareTransfer(门面保留同名方法) |
src/main/tray.ts |
Change | 菜单文案与入口不变;island.editLayout() 改由新控制器实现 |
src/main/island/messages.ts |
Add | 消息队列 + 合并 + 超时 + 作业指纹/去重表(见 4.3.13) |
src/main/island/ipc.ts |
Add | 校验 island:push 的 source/dedupeKey,投递前查重 |
src/main/smoke.ts |
Change | 保留全部既有读取器与注入点(名称/语义不变,见 4.7 第 11 条);新增 v2 断言(行高/行数/气泡/迁移);改造归属 part 11 测试基线 |
src/preload/index.ts |
Change | 新增 family/colorScheme 感知的配置读写;islandPush 透传 source/dedupeKey;保留全部旧方法名 |
src/preload/island.ts |
Change | 新增行/编辑器预览相关事件;旧事件名与语义不变 |
src/types/desktop.d.ts |
Change | 配置形状加外观新字段与 islandLayout.version;新增 v2 DTO 类型;islandPush payload 扩展 |
src/island/LayoutIslandApp.vue |
Change | 分发:custom → CustomIslandRuntime;fixed 按 family 二选一(见 4.3.1) |
src/island/IslandApp.vue |
Change | 迁出为 family/winisland/ClassicIsland.vue;旧文件保留再导出(防断裂) |
src/island/CustomIslandRuntime.vue |
Change | 始终按 ClassIsland 行/组件规格重写测量、行渲染、行设置抽屉;不受 family 影响 |
src/island/ComponentNode.vue |
Change | 拆为 components/*Node.vue;本文件保留兼容导出;滚动组件新增 `settings.mode('continuous' |
src/island/ClassicNotifications.vue |
Change | 改为 shell/MessagePanel.vue;新增行内气泡模式(同一时刻 1 条 active) |
src/island/LessonReminderContent.vue |
Change | 改为 shell/LessonBar.vue;遮罩参数按 ClassIsland 重算;fixed 下 prepare/on-class/break 行内容接管、ending 仅徽标 |
src/island/CoursewareTransfer.vue |
Keep | 课件覆盖卡保持现状(424×132 等,见 02 §2.10) |
src/island/WeatherBrief.vue |
Keep | 天气简报保持现状(Open-Meteo/15 分钟/缓存标记);本轮不引入 ClassIsland 天气服务 |
src/island/spring.ts、squircle.ts |
Keep | 弹簧与连续圆角数值不动(10 部分仍按此写) |
src/island/classic-island.css |
Change | 改名 family/winisland/winisland.css(fixed 专用);数值不变,选择器按新 DOM 调整 |
src/island/custom-runtime.css、custom-island.css |
Change | 移到 styles/custom.css 与 custom/*;始终按 ClassIsland 行规格重写(custom 不受 family 影响) |
src/island/lesson-ending.css |
Keep | 秒数徽标样式不动 |
src/island/main.ts |
Keep | 入口与令牌注入顺序不动 |
src/renderer/views/settings/IslandSettingsView.vue |
Change | 顶部加 family 选择器(只影响 fixed);新增 colorScheme/theme/effectEnabled/effectScale/progressBar;fontSize 语义随 family 展示 |
src/renderer/views/settings/IslandEditorView.vue |
Change | 数据改为 v2;行设置抽屉;预览按 ClassIsland 规格;保存队列/上限保留;拆分节奏按 09 文件计划(见 4.3.11) |
src/renderer/components/IslandComponentSettings.vue |
Change | 组件字段与 ClassIsland 规格对齐(含滚动 mode);旧字段保留只读兼容 |
src/renderer/components/IslandEditorNode.vue |
Keep | 标签节点可复用;若加行级拖拽则小幅 Change |
src/renderer/components/IslandRowSettingsDrawer.vue |
Add | 行级设置的唯一入口(字段清单由 09 定义) |
src/renderer/island/bridge.ts |
Change | 作业投递增加 `source:'homework-local' |
src/renderer/views/HomeworkView.vue |
Keep | markHomeworkCreatedLocally 调用点保留(录入前/后登记指纹,见 4.3.13) |
src/renderer/styles/island-editor.css |
Change | 编辑器视觉按 ClassIsland(间距/分隔/虚线框/预览画布) |
packages/desktop-client/scripts/smoke.mjs |
Change | 视觉/几何断言改为按 family 分套;默认 quick 仍无头 |
docs/app/island.md、docs/dev/implementation.md |
Change | 口径更新为 family 与 fixed 新语义(文档不是代码,但必须与实现同步) |
4.3 关键文件改造详情(逐个文件:现状 → 目标 → 接口 → 风险)
4.3.1 src/island/LayoutIslandApp.vue(Change,按 layout + family 分发)
- 现状:只按
layout==='custom'二选一(CustomIslandRuntime/FixedIsland),课件优先覆盖。依据:packages/desktop-client/src/island/LayoutIslandApp.vue:56-75。 - 目标(定稿分发规则):
layout==='custom'→ 始终custom/CustomIslandRuntime.vue(ClassIsland 行/组件规格),不看 family;layout==='fixed'且appearance.family==='classisland'→ 新增family/classisland/FixedInfoRow.vue(信息行 + 行内气泡);layout==='fixed'且appearance.family==='winisland'→family/winisland/ClassicIsland.vue(原胶囊+详情卡,行为不变);- 课件传输仍最高优先级,覆盖以上所有分支。
- 接口:props 保持
settings/state/schedules/timePoints/appearance/nativeVisible;新增计算family = appearance.family ?? 'classisland'仅用于 fixed 分支;不新增 IPC 通道。 - 为什么:family 只影响 fixed,custom 不需要两套渲染;分发规则收窄后,custom 的数据与视觉可以独立演进。
- 风险:切换 family 只改变 fixed 渲染器,custom 不受影响——必须保证切换时若当前 layout=custom 不重建 custom 运行时(避免方案展开态/测量状态丢失);切换命中矩形时
reportHit(null)并syncTargets(true)。
4.3.2 src/island/IslandApp.vue → family/winisland/ClassicIsland.vue(Change,大改)
- 现状:683 行,胶囊层 + 展开层 + 课程提醒 + 命中上报 + 弹簧形变全在一个文件;窗口固定包围盒内的岛由
morph驱动。依据:src/island/IslandApp.vue:44-570。 - 目标:
- 保留并迁移全部 winisland 行为(胶囊/展开/紧急/叫人/多条列表/课程提醒/命中),视觉与交互不变(老用户回退路径)。
- 把"壳"(squircle 路径、弹簧、
--card-path、命中上报)抽到shell/IslandShell.vue,把消息展示抽到shell/MessagePanel.vue,把课程提醒抽到shell/LessonBar.vue;ClassicIsland.vue只负责"胶囊内容 + 展开内容"的编排。 - 文件顶部保留兼容导出:
export { default } from './family/winisland/ClassicIsland.vue',避免旧 import 断裂。
- 接口:props 必须兼容现有
{ embedded?, suspended? };继续调用window.island.sendAction/setInteractive/setHitRect;继续使用ISLAND_SIZE_DELTA/ISLAND_LIST_METRICS。 - 为什么:ClassIsland fixed 取代胶囊后,旧实现仍要作为
winisland保留;抽壳后两族共用同一套命中/形变,减少重复实现。 - 风险:抽壳会改变 DOM 层级,现有 CSS 选择器(
.island-card .content、.pill-inner)与冒烟断言依赖 DOM;必须保留.island-card、.island-root、data-anchor、data-style等关键钩子。
4.3.3 family/classisland/FixedInfoRow.vue(Add,fixed 的新默认)
- 目标:ClassIsland 信息行的固定布局:默认 1 行,行高 40、行内水平内边距 12、行间距 5、圆角 8、背景不透明度 0.5(背景色取主题 token +
appearance.winislandStyle/surface 材质)、1px 描边、字号由appearance.fontSize(classisland 下=正文基准,默认 16)派生 14/18/20、进度条高度取appearance.progressBar.height(默认 3)、progressBar.show=false时不显示;默认行 = 日期组件 + 课程表组件;消息以行内气泡呈现(普通/重要/紧急/叫人/作业),不再弹"详情卡";强提醒特效仅在effectEnabled=true时叠加,尺寸按effectScale。 - 接口:props
{ state, appearance, schedules, timePoints, now, editing? };内部复用components/*Node.vue与shell/MessagePanel.vue(行内模式);只读appearance.colorScheme决定行底色/文字对比。 - 为什么:ClassIsland 的主界面就是"一行行信息",fixed 只有变成信息行才符合重构目标。
- 风险:fixed 原有"点胶囊展开卡片"的点击目标消失,必须保证消息可读(行内气泡可点开详情面板);命中矩形上报必须按真实行矩形,不能用旧胶囊尺寸(窗口包围盒由 geometry.ts 计算)。
4.3.4 family/classisland/InlineMessageBubble.vue(Add,行内气泡)
- 目标:同一时刻只显示 1 条 active 气泡;点击展开详情/操作;有多条未处理消息时折叠为「还有 N 条」,点击打开应用内通知中心(沿用 ClassHelper 现有通知中心路由),不并列展示多条;两阶段表现为先横向展开成条、再展开详情面板(动画由 08/10 定义);普通/重要/紧急/叫人/作业五类样式;紧急消息仍按现有业务规则"立即展开、无视上课"。
- 接口:props
{ active, pendingCount, mode, listExpanded };动作只通过window.island.sendAction,不得直接改状态;数据来源是IslandState.active/queued与IslandNotification.level。 - 为什么:ClassIsland 的提醒是"行内/气泡"而非独立卡片;不改变业务队列,只换表现。
- 风险:行内气泡只改变行内内容测量与截断(正文最多 2 行、超出省略),不改变窗口包围盒(fixed 窗口尺寸固定为 min(工作区宽,960) × (40+2×pad));「还有 N 条」跳转必须带未读上下文,不得丢失已读回执。
4.3.5 custom/CustomIslandRuntime.vue(Change,大改;始终 ClassIsland 规格)
- 现状:三页(compact/details/lesson)交叉淡入 + 自然内容测量 + 铃铛 + 命中上报;行/组件来自
scheme.rows。依据:src/island/CustomIslandRuntime.vue:52-336,338-527。 - 目标:保留"测量 + 三页"骨架,统一按 ClassIsland 行规格重写(不区分 family):单行基准高 40、行间距 5、行内水平留白 12、行背景/圆角/不透明度/字号覆盖、行级隐藏规则、行级进度条(3px,含义见 05 §5.3);行设置抽屉入口(字段由 09 定义);
rows/expandedRows的编辑语义不变;滚动组件支持settings.mode: 'continuous'|'pause'(pause 时到边界停顿settings.interval)。 - 接口:props 不变;新增读取
row.appearance/row.hideRules/row.progress;window.island.reportResidentControl继续上报铃铛矩形;window.island.reportSize继续上报适配后尺寸;CSS 使用classisland命名空间(custom 不受 family 影响)。 - 为什么:ClassIsland 组件库/行模型与本应用 schemes 高度相似,且 family 最终只作用于 fixed;custom 统一到 ClassIsland 规格可避免"两套自定义外观"。
- 风险:老 custom 用户升级后视觉会变化(自定义布局统一到 ClassIsland 规格),必须在发布说明与设置页提示;
ResizeObserver+requestAnimationFrame测量链对行数敏感,改写后的几何只由 metrics 计算,不用动画中的壳反推(现状注释见:73-79)。
4.3.6 src/main/island.ts → main/island/*(Change + Add,大重构)
- 现状:2445 行单文件(分区见 02 §2.2)。
- 目标:
index.ts:门面导出island、registerIslandIpc、recomputeSizes、normalizeAppearance、getSliverSize、ISLAND_SIZES等旧导出名,保证main/index.ts、tray.ts、courseware.ts、smoke.ts的 import 不改。controller.ts:状态与编排;持有IslandState;消息/上课/预览/动作分发。window.ts:建窗、固定包围盒/触摸贴合、锚点、可见性/焦点/透明度、syncWindow拆分后的"可见性决策"。hit-test.ts:60ms 轮询、setInteractive、setHitRect、expectedHitRect、setShape降级(非 Windows 见 4.6)。messages.ts:pushNotification分流、合并、排序、超时计时器、dismissActive、clearPendingForClose、handleAction。lessons.ts:LessonReminderTracker每秒推进与lessonShowing判定。ipc.ts:registerIslandIpc()全部 handler(含发送方校验与外观合并节流 16ms)。
- 接口:必须保留全部旧公开方法与冒烟读取器(02 §2.5 第 9 条清单);新增
getMetrics()、getLayoutVersion()供编辑器/冒烟断言。 - 为什么:不拆文件无法安全替换 fixed 渲染、无法让 engine 复用 shared 纯函数,也无法让 ChatGPT 在单文件里做可靠改动。
- 风险:拆分过程中最容易出现"私有字段跨模块互相引用";必须先把
IslandState、WindowBox、HitRect、Metrics定义到types,模块间只经接口通信;拆完立即用现有verify:desktop(无头)回归。
4.3.7 src/island/ComponentNode.vue → components/*Node.vue(Change)
- 现状:456 行,14 类 kind 的条件渲染 + 课程表折叠算法 + 编辑手柄 + 行内样式。依据:
src/island/ComponentNode.vue:29-210,212-293。 - 目标:按 kind 拆文件,
components/index.ts提供resolveNodeRenderer(kind);每个组件只负责自己的内容与样式;课程表的"折叠/展开/隐藏/进度/徽标"逻辑放进ScheduleNode.vue并复用 02 §2.8/§2.9 规则。 - 接口:保持
IslandComponentNode数据结构不变(字段名/默认值见packages/shared/src/island-layout.ts:77-108);编辑态仍用node-handle拖拽句柄。 - 为什么:ClassIsland 组件规格要为每类组件补字段(字号覆盖、进度条、显示模式),单文件条件链会继续膨胀。
- 风险:
<style scoped>拆分后类名作用域变化;必须把跨组件公共样式放styles/components.css,避免样式丢失。
4.3.8 src/island/ClassicNotifications.vue → shell/MessagePanel.vue(Change)
- 现状:单条详情卡 + 多条列表卡 + 按钮行;只用于展开态。依据:
src/island/ClassicNotifications.vue:104-233。 - 目标:同一组件支持三种形态:
detail(winisland 详情卡)、list(winisland 多条列表)、inline(classisland fixed 行内气泡);按钮语义(打开应用/标为已读/知道了/收到)与批量语义不变;canMarkRead排除作业的规则保留。 - 接口:props 增加
variant: 'detail'|'list'|'inline';保持state/appearance/maxHeight。 - 为什么:fixed 默认不再有详情卡,但消息仍需可操作;共用一套面板避免三处重复实现按钮/已读逻辑。
- 风险:
inline形态的高度必须计入行高与窗口包围盒(由shared/island/messages.ts提供),不得在渲染层单独硬编码。
4.3.9 src/island/LessonReminderContent.vue → shell/LessonBar.vue(Change)
- 现状:5 区斜切遮罩 + 进度条 + 秒数徽标;遮罩区段 360ms/错峰 90-120ms、开
1-(1-p)^4、关p²;文字 260ms、scale 1.1→1.0、750ms。依据:src/island/LessonReminderContent.vue:59-123,133-137。 - 目标:按 ClassIsland 规格校准:遮罩斜切权重
[0,0.1,0.2,0.4,0.2,0.1](与 03/14 部分一致)、区段 360ms、错峰 120ms、开 QuarticEaseOut、关 QuadraticEaseIn、文字 delay 260ms;行高 40、圆角 8。叠加口径(定稿):fixed(classisland)下prepare/on-class/break以行内容接管呈现(临时替换课程表内容,动画由 08/10 定义);ending不接管行内容,只在课程名右侧显示蓝色圆形秒数徽标;custom 不接管(保持课程组件 + 徽标)。 - 接口:props 保持
{ reminder, now, appearance, motion };新增variant: 'winisland'|'classisland'。 - 为什么:ClassIsland 的提醒遮罩是"整行被斜切色块覆盖 + 文字弹入",与现有实现同源但参数不同;必须抽成共用组件。
- 风险:
ref宽度依赖ResizeObserver,在 fixed 行内宽度变化频繁;必须避免测量死循环(只在行宽变化时重算多边形)。
4.3.10 src/renderer/views/settings/IslandSettingsView.vue(Change)
- 现状:布局二选一、外观滑块、测试预览。依据:
src/renderer/views/settings/IslandSettingsView.vue:26-160,162-392。 - 目标:
- 顶部先选 风格族 family(classisland 默认 / winisland 兼容)——只影响 fixed,说明文案必须写清"自定义布局始终使用 ClassIsland 规格";
fixed的说明按 family 变(classisland=fixed 信息行;winisland=fixed 胶囊+详情卡);- 新增外观控件:
colorScheme(浅色/深色/跟随系统,默认跟随)、theme(fluent/classic,仅 classisland)、effectEnabled(强提醒特效总开关)、effectScale(1/0.75/0.5)、progressBar.show/height(默认 3px)、fontSize(classisland 显示为"正文字号(16 基准)"、winisland 显示为旧"基础字号(13 基准)"); - 调色板
winislandStyle(black/glass/tinted)仅在 winisland 下显示;切换 family 时保留用户原值,不重置; - "测试灵动岛"必须按当前 family + layout 推预览。
- 接口:
window.desktop.saveConfig({ island: {...} })、islandSetAppearance不变;新增字段随island下发(纯数据,禁止传 Proxy)。 - 为什么:family 是重构后最上层的开关,必须最先可见;新增外观字段需要用户可发现、可关闭。
- 风险:切换 family 会立即改变 fixed 外观;必须先弹确认"切换后仅 fixed 表现改变,自定义方案与 style 不变",且不得自动改 layout。
4.3.11 src/renderer/views/settings/IslandEditorView.vue(Change)
- 现状:组件标签 + 内嵌预览 + 方案菜单 + 组件库 + 检查器 + 指针拖拽 + 保存队列。依据:
IslandEditorView.vue:25-130,130-540,541+。 - 目标:数据切到 v2;增加"行"选择与行设置抽屉(
IslandRowSettingsDrawer.vue,字段清单由 09 定义);预览统一按 ClassIsland 规格渲染(custom 编辑也用CustomRows);保留上限(200 组件/20 方案)、保存队列、离开前保存、JSON 导入导出。 - 接口:仍通过
window.desktop.getConfig/saveConfig;normalizeIslandLayouts改为normalizeIslandLayoutV2;getIslandSchedules/getIslandTimePoints不动。 - 为什么:ClassIsland 编辑器的核心是"行/组件/行设置抽屉/组件库拖拽",现有编辑器已有 80% 骨架。
- 风险:编辑器是 855 行大文件;拆分节奏按 09 的文件计划执行,不强制本轮先抽 composables;若单文件继续膨胀,再按 4.1 的
renderer/island/editor/抽取,抽取时不得改变保存队列/修订号语义。
4.3.12 src/main/config.ts(Change,迁移接入点)
- 现状:
PersistedConfig含islandLayout/island,逐字段归一化,120ms 合并写。依据:src/main/config.ts:26-84,198-233,334-346。 - 目标:不新增文件级版本号;读取
islandLayout时若version缺失 → 调用migrateIslandLayoutV1ToV2(含 family/外观默认值,见 05),迁移结果写回并备份;saveConfig收到islandLayout时先normalizeIslandLayoutV2;island增加family/colorScheme/theme/effectEnabled/effectScale/progressBar/winislandStyle归一化。 - 接口:导出
getConfig/saveConfig/flushConfigSync签名不变;新增getIslandLayoutV2()供主进程使用。 - 为什么:迁移必须在任何消费者(岛窗口、编辑器、冒烟)之前完成,且只能发生一次;版本字段只保留
islandLayout.version可避免两套版本号分叉。 - 风险:迁移写盘与"用户正在编辑"竞争;必须保证迁移只在
app.whenReady()后、岛窗口创建前执行一次,失败时保留原文件并让应用以默认 v2(只读)启动。
4.3.13 作业指纹链路(本机刚录作业不上岛)
- 现状:渲染层
src/renderer/island/bridge.ts用"内容指纹 + id"避免本机录入的作业弹上岛——homeworkFingerprint()由classId|assignDate|title|content拼成,markHomeworkCreatedLocally()在发请求前登记指纹、回来后补记 id,isLocalHomework()在pushHomeworkToIsland()前判定;指纹 TTL 60 秒。依据:bridge.ts:191-267;调用点在src/renderer/views/HomeworkView.vue:419-428(录入前/后登记)。 - 目标(双层去重,职责明确):
- 渲染层(快速路径):保留现有
isLocalHomework()判定,命中则根本不发islandPush;markHomeworkCreatedLocally()调用点保留,不改交互时序。 - 主进程(兜底去重):
island:pushpayload 增加source: 'realtime'|'homework-local'|'replay'|'call'与dedupeKey(作业用同一指纹;普通通知/叫人用source+id);main/island/ipc.ts校验字段、main/island/messages.ts维护 LRU 去重表(key→过期时间,TTL 60s),命中直接丢弃并记 debug 日志。 - IPC 归属:渲染层
bridge.ts计算并传source/dedupeKey→ preloadislandPush原样透传 → 主进程main/island/ipc.ts校验 →messages.ts去重后入队。不得在 preload 里做业务判断,也不得让主进程反查渲染层 store。
- 渲染层(快速路径):保留现有
- 接口:
IslandPushPayload(packages/shared/src/island/ipc.ts)新增两个可选字段;旧调用点缺字段时主进程按id去重(兼容)。 - 为什么:现状只有渲染层一层判定,实时事件先于接口响应到达时仍可能漏判;主进程兜底可保证"同一台机器录的作业绝不上岛",且离线补弹/重连重复投递也被覆盖。
- 风险:指纹跨班/跨天误伤(必须带 classId+assignDate);主进程去重表不能无限增长(LRU+TTL);冒烟需要能注入/清理去重表(提供
testResetHomeworkDedupe()或在冒烟模式禁用去重,名称归 part 11 测试基线)。
4.4 IPC 契约 v2(增量、不改旧通道语义)
| 通道 | 方向 | 现状 | v2 变更 |
|---|---|---|---|
island:get-layout |
岛→主 | 返回 islandLayout |
返回 v2(version:2;含 fixed/schemes) |
island:save-layout |
岛→主 | 归一化后保存 | 先 normalizeIslandLayoutV2,返回 v2 |
island:state |
主→岛 | IslandState |
增加 family/pendingStaged/bubblePhase/bubbleDeadline(运行期,不落盘);旧字段不删 |
island:appearance |
主→岛 | IslandAppearance |
增加 family/colorScheme/theme/effectEnabled/effectScale/progressBar/winislandStyle;旧字段保留 |
island:push |
应用窗口→主 | {notification, context} |
增加 source/dedupeKey(作业指纹去重,见 4.3.13);缺省按 id 去重 |
island:action |
岛→主 | 8 种动作 | 保留;新增 open-message(classisland fixed 行内气泡打开详情)与 open-notifications(「还有 N 条」打开应用内通知中心) |
island:content-size |
岛→主 | 自定义尺寸上报 | 增加 `mode:'custom' |
classisland:island:get-metrics |
新 | — | 返回当前 layout/family 的 metrics(编辑/冒烟/调试用) |
classisland:island:editor-open |
主→应用窗口 | 已有 island:open-editor |
保留旧通道;新通道可带 {rowId,nodeId} 定位 |
island:set-hit-rect/island:set-interactive |
岛→主 | 已有 | 语义不变;fixed 信息行按真实行矩形上报 |
- payload 可携带
schemaVersion:2仅用于运行时校验/调试,不得落盘;落盘版本只有islandLayout.version。接收方遇到未知字段必须忽略而不是拒绝。 - 通道名继续集中在
src/main/channels.ts或shared/island/ipc.ts的单一常量表中;preload 不得直接写字符串字面量。 - 旧通道(
island:get-layout等)不得改名/删除;改名只能通过"新通道 + 旧通道转发"完成,至少保留一个版本周期。 - 所有冒烟注入/读取通道(
island:set-interactive、island:set-hit-rect、island:set-touch-mode、island:alive及主进程方法setTestInputPassthrough/setHitTestCursor/getWindowBounds/getHitRect/getInteractive/setBlurCollapseEnabled/setTouchMode/getTouchMode/isRecovering)名称与语义保持不变,改造归属 part 11 测试基线。
4.5 迁移与门面的落地顺序(必须按序提交)
- M0 建契约层:
shared/island/类型、默认值、normalize、migrate、metrics、geometry、messages、lessons、ipc;只加不删,旧island-layout.ts/constants.ts保持再导出。 - M1 拆主进程:
main/island/*拆分 + 门面;行为与现状 1:1(不改变任何可见行为),用无头verify:desktop回归。 - M2 迁移层接入:
config.ts读时迁移 + 版本号;用旧配置夹具验证(fixed/custom 各存一份)。 - M3 渲染层抽壳:
shell/IslandShell.vue、MessagePanel.vue、LessonBar.vue;winisland 外观不变。 - M4 新增 classisland fixed:
FixedInfoRow.vue+InlineMessageBubble.vue+IslandSettingsViewfamily 选择器;默认新装走 classisland,老配置走 winisland;family 只影响 fixed。 - M5 自定义行改造:
CustomIslandRuntime行级外观/隐藏规则/进度条/滚动 mode/行设置抽屉;custom 始终 ClassIsland 规格(老 custom 用户升级后有视觉变化,需在发布说明提示)。 - M6 编辑器改造:v2 数据、行设置、预览、保存队列。
- M7 收尾:CSS/文档/冒烟断言/迁移回滚演练。
每一步都必须"可独立回滚":新增文件先经 feature 开关(appearance.family/config.islandLayout.version)控制;不得在某一步同时删除旧文件与旧逻辑。
4.6 风险清单与强制缓解
| 风险 | 触发点 | 缓解(必须实现) |
|---|---|---|
拆分 island.ts 导致隐藏状态丢失 |
M1 | 先对齐私有字段到接口;拆分后逐项对比 getState()/getWindowBounds()/getHitRect() 冒烟输出 |
| DOM/CSS 选择器断裂 | M3/M4 | 保留 .island-card/.island-root/[data-anchor]/[data-style] 钩子;视觉回归由 --visual 专项做 |
| 两套 family 逻辑分叉 | M4 | 业务状态机只保留一份(controller);family 只选择 fixed 渲染器,custom 不分叉 |
| fixed 语义变化引起老用户困惑 | M4 | 老配置迁移默认 winisland;设置页显著位置显示 family 并解释两种 fixed;custom 升级有视觉变化的提示 |
| custom 用户升级后观感变化 | M5 | 统一 ClassIsland 规格是定稿;发布说明+设置页提示;数据不变、可回滚 config 备份 |
| 迁移把用户自定义方案写坏 | M2 | 迁移前备份 config.json;迁移失败保留原文件;提供"恢复旧版布局" |
| 编辑器 855 行继续膨胀 | M6 | 先抽 composables;单文件不超过 500 行;新逻辑进 renderer/island/editor/ |
| 非 Windows 触摸命中退化 | M1 | setShape 仅 win32 分支;其它平台走"整窗穿透 + 可见区命中"(02 §2.4 第 5 条) |
| 性能(行数/组件数增加) | M5 | metrics 计算缓存;测量只在依赖变化时执行;列表/行使用 will-change 谨慎 |
| 无头验证覆盖不到视觉 | M7 | 默认 quick 保持无头;视觉断言只在显式 --visual 下跑,并在文档写明 |
4.7 验收标准(第 4 部分)
- 仓库中存在
packages/shared/src/island/与packages/desktop-client/src/main/island/两个新目录,且src/main/island.ts只剩门面(≤50 行,内容为 import/export 与类型再导出)。 src/main/index.ts、tray.ts、courseware.ts、smoke.ts对./island.js的 import 与调用不需要改动(门面签名兼容)。LayoutIslandApp.vue分发正确:custom始终CustomIslandRuntime(与 family 无关);fixed在 classisland 下用FixedInfoRow、winisland 下用ClassicIsland;切换不新建第二个窗口、不残留旧命中矩形。fixed在family='classisland'下渲染为信息行(默认 1 行:日期 + 课程表;行高 40、圆角 8、背景不透明度 0.5、progressBar.height默认 3px、消息行内气泡);在family='winisland'下保留原胶囊+详情卡行为与既有冒烟断言。CustomIslandRuntime支持行级外观/隐藏规则/进度条,行设置抽屉可打开并保存;滚动组件支持mode:'continuous'|'pause';rows/expandedRows编辑语义与现状一致。- 外观新字段全部落地:
family(只影响 fixed)、colorScheme、theme、effectEnabled、effectScale、progressBar、winislandStyle;fontSize在 classisland 下为正文基准(新装 16)、winisland 下旧语义(13,迁移保留原值)。 - 编辑器 / 设置页 / 组件设置全部读写 v2 数据;旧 v1 配置在首次启动被迁移且不丢字段(迁移细节按 05 验收)。
- 所有新增/修改的 IPC 通道登记在单一常量表;旧通道全部保留;preload 未暴露
ipcRenderer;island:push带source/dedupeKey且主进程有兜底去重表。 - 默认
pnpm verify:desktop(无头 quick)通过;不得因为重构把任何断言改成"跳过";冒烟注入点名称/语义全部保留。 - 代码中不存在
setBackgroundMaterial;不存在绕过 contextBridge 直接访问 Node 的渲染层代码;custom 不受 family 影响、不引用 winisland 样式命名空间。 - 作业指纹链路双层生效:本机录入后 60 秒内,实时事件/接口响应先后到达都不产生岛消息;跨班/跨天不误伤;去重表有 TTL 上限且可被冒烟清理。
- 文件级改造清单(4.2/4.3)逐项落地;
docs/app/island.md、docs/dev/implementation.md的 family/fixed/custom 描述与实现一致。
5. 数据契约、schema v2 与配置迁移
5.0 总则(必须遵守)
- 本章定义重构后的唯一数据契约。所有读写岛配置的代码(主进程
config.ts、岛渲染进程、编辑器、设置页、冒烟)必须只经过packages/shared/src/island/schema.ts的类型与normalize.ts/migrate.ts的函数,不得各自解析 JSON。 - 旧配置必须无损升级:
config.json现有的islandLayout(fixed/custom、schemes、nodes、expandedRows、residentEnabled、pointerFade、lessonReminders)与island(appearance 全字段)一个都不许丢;升级失败必须可回滚。 - 新增
appearance.family:取值'classisland' | 'winisland'。新装默认classisland;已存在配置的老用户升级默认winisland。fixed的语义随 family 分叉:classisland下 fixed = ClassIsland 信息行(默认 1 行 = 日期 + 课程表,行高 40、圆角 8、不透明度 0.5、3px 进度条、消息行内气泡);winisland下 fixed = 原胶囊 + 详情卡。 islandLayout.version必须写入;当前版本2。版本号是唯一迁移判据,不得用"字段是否存在"猜版本(字段判断只作为迁移内部的兜底)。- 写入必须沿用现有
config.json的"内存缓存 + 120ms 合并写 + 退出前 flush"语义(packages/desktop-client/src/main/config.ts:235-346),不得为迁移新增同步写盘路径;迁移本身例外(必须一次写成功并立即 flush)。 - 客户端不得把 v2 数据写回给旧版本可见的字段后又不写版本号;写 v2 必须同时写
version:2,保证降级/回滚可判断。
5.1 落点与版本治理
| 数据 | 位置 | 版本字段 | 说明 |
|---|---|---|---|
| 运行期配置 | <userData>/config.json |
无独立文件版本 | 版本只在 islandLayout.version 上表达(唯一版本字段) |
| 岛布局 | config.json.islandLayout |
version(新增,当前 2) |
方案/行/节点/外观的结构版本;唯一迁移判据 |
| 岛外观 | config.json.island |
无独立版本 | 增加 family、theme、effectEnabled、effectScale、progressBar、fontSizes;style 保留为 surface 层 |
| 迁移前备份 | <userData>/config.json.v1.bak |
— | 迁移前完整拷贝,只保留一份(覆盖式) |
| 迁移标记 | <userData>/config.island-migration.json |
{from,to,at,appVersion} |
仅诊断用,不参与版本判断、不是第二套版本字段 |
- 只保留
islandLayout.version一个落盘版本字段:不得新增config.schemaVersion或其它文件级版本号;迁移判定只看islandLayout.version(缺失=v1)。 - 迁移必须在
app.whenReady()后、island.init()之前完成一次;不得由渲染进程触发,也不得在岛窗口创建后再迁移(否则窗口会先按旧数据渲染一帧)。 - 迁移成功后必须立即
flushConfigSync();失败时不得写回半成品:保留原config.json,删除.tmp,应用以内存默认 v2 运行并在设置页显示"迁移失败,已保留原配置"。 - 读到
islandLayout.version > 2时应用必须只读(不写回),并在设置页提示"配置来自更高版本"。
5.2 appearance v2(config.json.island)
在现有 IslandAppearance 基础上只增不改,旧字段全部保留(packages/shared/src/types.ts:1145-1195)。新增/语义变化如下:
| 字段 | 类型 | 默认值(新装) | 说明与依据 |
|---|---|---|---|
family |
'classisland' | 'winisland' |
'classisland'(老用户迁移为 'winisland') |
风格族字段的唯一落点;作用域只限 fixed(classisland=信息行;winisland=旧胶囊+详情);custom 始终 ClassIsland 规格、不受影响 |
colorScheme |
'light' | 'dark' | 'system' |
'system' |
岛配色模式:跟随应用主题(system,默认)/强制浅色/强制深色;只影响岛自身渲染,不改主窗口主题 |
theme |
'fluent' | 'classic' |
'fluent' |
仅 family='classisland' 的 fixed 使用;对应 ClassIsland Fluent/Classic 两套主题;具体 token 由 06 定义 |
effectEnabled |
boolean | true |
强提醒特效总开关(对应 ClassIsland 的提醒特效 Topmost/Effect 是否启用);关闭后只保留行内气泡/普通提醒 |
effectScale |
1 | 0.75 | 0.5 |
1 |
强提醒特效渲染缩放(对应 ClassIsland NotificationEffectRenderingScale,默认 1.0);只允许三档 |
progressBar |
{height:number, show:boolean} |
{height:3, show:true} |
岛内进度条统一配置:高度默认 3px,总开关默认显示;语义见 5.3 行级进度条说明 |
fontSize |
number | classisland=16(正文基准,派生 14/18/20 阶梯);winisland=13(旧语义) |
family='classisland' 时它是正文基准字号,fontSizes 由它派生;family='winisland' 保持旧语义;迁移时旧配置保留原值,新安装默认 16 |
fontSizes |
{secondary:number, body:number, emphasized:number, large:number} |
由 fontSize 派生:classisland 14/16/18/20;winisland ≈14/16/18/20(按 fontSize/13 比例) |
ClassIsland 四档字号;显式配置优先于派生(见 5.4) |
height / width |
number | 44 / 268 |
仅 family='winisland' 的胶囊尺寸;classisland 下不作为信息行尺寸 |
radius |
number | 20(winisland)/8(classisland 行为默认) |
classisland 下作为行默认圆角;解析时取 family 对应默认 |
opacity |
number | 1 |
岛整体不透明度;classisland 下行背景不透明度另见行设置(默认 0.5) |
winislandStyle |
'black' | 'glass' | 'tinted' |
'black' |
winisland 调色板字段的 canonical 名:纯黑/半透明/主题色渐变;仅 family='winisland' 的 fixed 使用 |
style |
'black' | 'glass' | 'tinted' |
镜像 winislandStyle |
兼容镜像字段(不删除):旧配置读入时若没有 winislandStyle 则用它;写出时同时写 winislandStyle 与 style,保持旧版本可读;建议 v3 再评估废弃 |
position/alwaysOnTop/animations/speed/idleSliver/marginX/marginY/followCursorDisplay/accent |
不变 | 不变 | fixed(两种 family)与 custom 共用;classisland fixed 也用停靠与边距 |
- 字段职责必须严格区分:
family只决定 fixed 用哪套渲染器(ClassIsland 信息行 / WinIsland 胶囊);winislandStyle(兼容镜像style)只决定 winisland 的调色板;theme只决定 classisland fixed 的 Fluent/Classic;colorScheme只决定浅色/深色/跟随。四者不得互相覆盖、不得合并。custom不使用family/theme/winislandStyle(始终 ClassIsland 规格),但仍读取colorScheme决定明暗。 family归一化:只接受两个值,非法值回落'classisland'(新装默认);迁移函数负责把老用户写成'winisland'(见 5.4)。colorScheme归一化:只接受light/dark/system,非法回落system;缺失补system。fontSize归一化:8–96;classisland 下作为正文基准,fontSizes缺失时由它派生(body=fontSize,secondary=round(fontSize*14/16),emphasized=round(fontSize*18/16),large=round(fontSize*20/16));winisland 下保持旧语义(fontSizes缺失时按fontSize/13 × 14/16/18/20推导)。迁移时旧fontSize原值保留,不强行改成 16。fontSizes归一化:每项 8–96;显式值优先于派生;缺失项按第 4 条补齐。theme仅 classisland fixed 生效;非法值回落'fluent';winisland下忽略但不删除该字段。effectEnabled必须为 boolean(缺省 true);effectScale必须夹取到[1, 0.75, 0.5](缺省 1,非法值取 1);progressBar.height夹取 1–8、progressBar.show缺省 true;progressBar缺失时整对象补默认。winislandStyle归一化:仅black/glass/tinted合法,非法回落black;style作为镜像字段同步写;读入优先winislandStyle,回退style。切换 family 时保留用户winislandStyle原值,不重置(UI 默认值提示归 09)。
5.3 islandLayout v2(config.json.islandLayout)
v2 顶层(IslandLayoutV2):
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
version |
number | 2 |
必须存在 |
layout |
'fixed' | 'custom' |
'fixed' |
布局模式(沿用旧字段) |
fixed |
FixedSchemeV2 |
见下 | classisland fixed 信息行配置;winisland 下忽略 |
schemes |
IslandLayoutSchemeV2[] |
1 个默认方案 | 自定义方案(沿用旧结构并扩展) |
activeSchemeId |
string | 'default' |
当前方案 |
residentEnabled |
boolean | layout==='custom' |
兼容字段;不得再作为独立开关 |
pointerFadeEnabled / pointerFadeReversed |
boolean | true / false |
鼠标避让淡化(沿用) |
lessonReminders |
LessonReminderSettings |
沿用旧默认 | 准备/上课/课间/结束/课中进度开关与秒数 |
updatedAt |
number | 0 |
最近保存时间(仅诊断) |
FixedSchemeV2(classisland fixed 的信息行):
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
rows |
IslandRowV2[] |
1 行 = 日期 + 课程表 | 信息行;默认只有一行 |
mainRowId |
string | 首行 id | 主行(承载消息气泡/课程提醒) |
rowHeight |
number | 40 |
ClassIsland 行高(32–64 夹取) |
rowSpacing |
number | 5 |
行间距(0–150) |
backgroundOpacity |
number | 0.5 |
行背景不透明度 |
cornerRadius |
number | 8 |
行圆角(0–20) |
progressBarHeight |
number | 3 |
行内进度条高度(3px) |
messageBubble |
'inline' | 'card' |
'inline' |
fixed 下消息表现:inline=行内气泡,card=老式详情卡(仅回退用) |
maxRows |
number | 3 |
信息行最多行数(自动换行/溢出策略由 07 定义) |
IslandLayoutSchemeV2(在 v1 方案上扩展):
| 字段 | 类型 | 默认值 | 变化 |
|---|---|---|---|
id/name/rows/expandedRows |
沿用 | 沿用 | 结构不变;行内容升级为 IslandRowV2 |
scale |
number | 1 |
0.75–2.5(沿用于 CLASSISLAND_ISLAND_METRICS.scaleMin/Max) |
color/foreground/opacity/radius/separated/spacing |
沿用 | 沿用 | radius 0–20;spacing 0–150 |
rowHeight |
number | 40 |
新增:行高 32–64 |
rowSpacing |
number | 5 |
新增:与 spacing 同步;spacing 保留兼容 |
theme |
'fluent' | 'classic' |
继承 appearance.theme |
新增:方案级主题覆盖 |
fontSizes |
{secondary,body,emphasized,large} |
继承 appearance.fontSizes |
新增:方案级字号覆盖 |
progressBarHeight |
number | 3 |
新增:行内进度条高度 |
unrecognizedNodes |
object[] |
[] |
新增:未知 kind 的原始节点(不得丢弃,见 5.5 第 6 条) |
IslandRowV2(行):
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
id/name/main/allowNotifications/gap/nodes |
沿用 | 沿用 | main 行唯一;gap 0–64 |
appearance |
RowAppearance |
见下 | 行级外观(新增) |
hideRules |
RowHideRules |
全 false | 行级隐藏规则(新增) |
progress |
RowProgress |
见下 | 行级进度条(新增) |
RowAppearance:backgroundEnabled=false、backgroundColor='#000000'、backgroundOpacity=0.5、foregroundColor='#ffffff'、cornerRadius=8、opacity=1、fontSizes(继承方案/外观)。
RowHideRules:inClass=false、noMessage=false、noClass=false、fullscreen=false;全部为 false 表示不隐藏。语义对应 ClassIsland 的行隐藏规则(repos/ClassIsland/ClassIsland.Core/Models/Components/MainWindowLineSettings.cs 的 HideOnRule/HidingRules)。
RowProgress:enabled=true、style='line'、height=3、color='accent'、showRemaining=true。
行内 3px 进度条的语义(数据来源不在本层实现):进度条表示"当前显示内容的时间进度"——消息气泡显示时 = 该消息剩余显示时间;否则 = 当前课程进度。具体数据来源与取值规则由 07/08 定义,本 schema 只保证 progress(行级)与 appearance.progressBar(全局)提供 height/show 两个开关;行设置抽屉的完整字段由 09 定义,本章只登记字段名(appearance/hideRules/progress)并要求与 09 对齐,不得在 05 里发明抽屉字段。
IslandNodeV2(节点,在 v1 IslandComponentNode 上扩展,字段名不变):
| 字段 | 变化 |
|---|---|
id/kind/appearance/advanced/children |
沿用(appearance 四档字号默认改为继承行/方案) |
settings.schedule.display |
沿用 `'current' |
settings.schedule.progressStyle |
新增:`'bar' |
settings.schedule.showProgress |
沿用;默认 true |
settings.schedule.showPeriod/count |
沿用 |
settings.mode(仅 kind='rolling') |
新增:`'continuous' |
advanced.hideRules |
沿用 in-class/no-message/no-class;新增 fullscreen(与行级一致) |
advanced.rowSpan |
新增:仅容器组件使用,默认 1 |
默认方案(新装/重置):layout:'fixed'、family:'classisland'、fixed.rows=[{main:true, allowNotifications:true, gap:12, nodes:[date, schedule]}],schemes=[默认方案:main 行含 date/clock/schedule/notification]。数值依据:packages/shared/src/island-layout.ts:217-255(现有默认方案)+ CLASSISLAND_ISLAND_METRICS(:19-36)。注意:schemes 是 custom 布局数据,描述 ClassIsland 规格且不随 family 改变;fixed 配置只在 family='classisland' 时由 fixed 渲染器读取。
5.4 v1 → v2 迁移算法(必须逐步实现,可重入)
输入判定:
- 读取
config.json(不存在 → 全新安装:直接构造 v2 默认,family='classisland',不写迁移标记)。 - 文件存在但
islandLayout.version缺失:视为 v1(唯一版本判据;不得再引入schemaVersion作为第二套版本号)。 - 老用户判定:
islandLayout.version缺失 或 文件里存在islandLayout/island任一字段 → 迁移时family='winisland'(保持原外观,不惊吓用户)。 - 全新安装判定:文件不存在、或既无
islandLayout又无island→family='classisland'。
步骤(必须按序、每步失败可中止):
step 0 备份:把 config.json 原样复制到 config.json.v1.bak(存在则覆盖);写 config.island-migration.json {from,to,at}(仅诊断,不是版本字段)
step 1 外观迁移(appearance):
- family := 'winisland'(老用户)| 'classisland'(新装)
- colorScheme := 'system'(缺失补默认;已有值原样保留)
- winislandStyle := 旧 style(缺省 'black');style := winislandStyle(兼容镜像)
- theme := 'fluent'(新装默认;老用户也写 'fluent',仅 classisland fixed 生效)
- effectEnabled := true(缺失补默认)
- effectScale := 1(缺失补默认;只允许 1/0.75/0.5)
- progressBar := { height:3, show:true }(缺失补默认)
- fontSize:迁移保留旧值(classisland 新装写 16;老用户写原值)
- fontSizes := 由 fontSize 派生:
老用户(winisland):secondary=round(fontSize*14/13), body=round(fontSize*16/13),
emphasized=round(fontSize*18/13), large=round(fontSize*20/13)
新装(classisland):14/16/18/20(fontSize=16 为正文基准)
- 所有旧字段原样保留(height/width/radius/opacity/accent/fontSize/style/...)
step 2 布局迁移(islandLayout):
- version := 2(唯一落盘版本字段)
- layout := 旧 layout(缺省 'fixed')
- residentEnabled := (layout==='custom')(覆盖旧的独立值,保证一致)
- pointerFadeEnabled/pointerFadeReversed/lessonReminders 原样保留
- fixed := 若旧文件无 fixed 字段 → 生成 FixedSchemeV2 默认(1 行 date+schedule;progressBar 继承 appearance.progressBar)
- schemes := 旧 schemes 逐个升级:
rowHeight := 40;rowSpacing := 旧 spacing;theme := 'fluent';
fontSizes := 继承 appearance.fontSizes;progressBarHeight := appearance.progressBar.height;unrecognizedNodes := []
每行补 appearance/hideRules/progress 默认;
每节点补 settings.schedule.progressStyle='line'、advanced.rowSpan=1;
若节点 kind='rolling' 补 settings.mode='continuous'(pause 保留旧 interval 语义)
- 未知 kind 的原节点移入该方案的 unrecognizedNodes(保留原始 JSON,不参与渲染)
step 3 校验与夹取:调用 normalizeIslandLayoutV2 / normalizeAppearanceV2(见 5.5);
校验失败项收集成 warnings,不得中止;结构性错误(schemes 为空等)用默认方案兜底
step 4 写回:saveConfig({ islandLayout, island }) 后立即 flushConfigSync()(**不写 schemaVersion**)
step 5 读回校验:重新读取并 deep-equal 比较关键字段(islandLayout.version/family/layout/方案数/节点数);
不一致 → 从 config.json.v1.bak 恢复、删除迁移标记、报错并降级为"只读默认 v2"
- 可重入/幂等:若
islandLayout.version===2,迁移函数必须直接返回原对象(不写盘、不改字段);重复执行不得产生二次修改。 - 老用户 fixed 不变:迁移后
family='winisland',fixed 仍渲染原胶囊+详情卡(ClassicIsland.vue),因此老用户升级后视觉 0 变化;只有用户主动在设置页切到classisland或新装用户才看到信息行。 - custom 老用户:
custom始终按 ClassIsland 规格渲染、不受 family 影响;迁移只升级数据(补行级默认、滚动 mode 等),不改 schema 结构。老 custom 用户升级后视觉会变为 ClassIsland 行规格(这是定稿行为),发布说明与设置页必须提示;fixed 用户(默认 winisland)不受影响。 - 编辑器打开时必须先读迁移后的 v2;若
islandLayout.version不匹配(例如用户用旧版应用改过文件),必须触发一次"再迁移"而不是把 v1 当 v2 解析。 - 迁移必须记录 warning 列表到内存并在设置页"存储/诊断"可查看(哪几项被夹取、哪几个未知组件被保留)。
5.5 校验与夹取(normalizeIslandLayoutV2 / normalizeAppearanceV2)
必须在 shared 中实现,主进程保存前、编辑器加载后、IPC 存入前三处共用同一个函数。规则:
version:仅接受2;缺失/非法按迁移处理,不得静默改成 2 后继续用旧结构。layout:'fixed'|'custom',非法回落'fixed'。family:'classisland'|'winisland',非法回落'classisland'。- 数量上限:行 ≤12/方案、节点 ≤200/全部方案、嵌套深度 ≤6、方案 ≤20;
main行唯一(多余取消)。 - 数值夹取:
rowHeight 32–64、rowSpacing 0–150、gap 0–64、radius 0–20、opacity 0–1、backgroundOpacity 0–1、fontSizes 8–96、scale 0.75–2.5、rowSpan 1–6、progressBarHeight 1–8;非法/NaN 回落默认值(禁止Math.max(min, NaN)这种写法,参考src/main/island.ts:2225-2241的 fallback 说明)。 - 未知 kind:不得丢弃。把原始节点 JSON 放入
scheme.unrecognizedNodes(最多 200 条),渲染时忽略并显示"未知组件(已保留)";这样用旧版应用打开再保存也不会丢数据。 - 颜色:统一
#rrggbb小写;非法回落默认;透明度字段独立,不把#rrggbbaa塞进颜色字段。 - id:空/非字符串/重复 → 重新生成(
node-<n>/row-<n>/scheme-<n>);id 最长 100 字符。 - 文本:
settings.text≤2000 字符、locationName/countdownTitle≤64;超长截断并记 warning。 - 经纬度:
latitude -90..90、longitude -180..180,可空;空表示未配置(组件显示"请设置天气地点")。 - 未知字段:v2 对象里出现但类型未定义的字段必须原样保留在对象上(不删除),以便前向兼容。
lessonReminders:prepareSeconds 5–1800、endingSeconds 5–600(依据现有packages/shared/src/island-layout.ts:390-398)。winislandStyle/style:两者都只接受black/glass/tinted,非法回落black;读入优先winislandStyle、回退style,写出两者同步(兼容镜像);不得因 family 迁移而删除style;family 与调色板相互独立(family 决定 fixed 用哪套渲染,winislandStyle 决定 winisland 的调色板)。theme:只接受fluent/classic,非法回落fluent;具体色板/圆角/阴影 token 由 06 定义,05 只登记字段与默认值。colorScheme:只接受light/dark/system,非法回落system;system时跟随应用html.dark状态决定岛明暗(映射逻辑由渲染层实现,数据层只存枚举)。fontSize:8–96,缺省按 family 取(classisland=16 正文基准、winisland=13 旧基线);fontSizes缺失时按 5.2 第 4 条派生;显式fontSizes优先;迁移不得覆盖旧fontSize。effectEnabled:boolean,缺省true;effectScale:只接受1/0.75/0.5,缺省1,非法取1;progressBar:{height:1–8, show:boolean},缺省{3,true}。- 进度条高度优先级:
行 progress.height>方案 progressBarHeight>appearance.progressBar.height;任一缺失沿上一级回落,最终默认 3。progressBar.show=false时所有行都不显示(行级enabled=false只关该行)。 - 滚动组件:
settings.mode只接受continuous/pause,缺省continuous;pause时停顿秒数复用settings.interval(夹取 1–300);continuous时speed夹取 1–200(沿用现有范围)。 - 渲染/样式命名空间只由 layout(fixed 的 family)决定,不写入数据:
fixed + family='winisland'→ winisland 命名空间;fixed + family='classisland'→ classisland 命名空间;custom始终 classisland 命名空间、不受 family 影响;normalize 不得改写样式类名/选择器相关字段。 IslandNotification.level、IslandLessonReminder.progress/leftSeconds、IslandState.family/pendingStaged/bubblePhase/bubbleDeadline均为运行期派生字段,normalize 不处理、不落盘。
5.6 DTO 映射(ClassHelper 数据 → ClassIsland 语义)
fixed(classisland)行内气泡规则:同一时刻只显示 1 条 active 气泡;点击展开详情/操作;有多条未处理消息时不并列显示,而是折叠为「还有 N 条」,点击打开应用内通知中心(沿用 ClassHelper 现有通知中心路由);消息队列、去重、上课暂存、已读回执规则完全不变(02 §2.5/§2.12)。
IslandState → ClassIsland 主界面状态:
| ClassHelper 字段 | ClassIsland 语义 | 渲染映射(classisland fixed) |
|---|---|---|
mode:'hidden' |
岛不可见(仅提醒特效时临时显示) | 信息行隐藏/仅保留细缝 |
mode:'pill' |
紧凑态 | 行内气泡/铃铛提示 |
mode:'expanded' |
展开态 | 行展开 + 消息面板 |
active/queued |
提醒队列(当前播放/待播) | 行内气泡列表,按 PRIORITY_RANK 排序 |
inClass |
TimeState=OnClass |
行隐藏规则 inClass 生效、提醒遮罩可用 |
currentPeriodEnd |
当前时间点结束 | 进度条/倒计时取值 |
reason |
提醒来源(新消息/课后补发/紧急/叫人/收回中) | 气泡入场动画与角标 |
listExpanded |
列表是否展开更多 | 行内"展开更多" |
lessonReminder |
课级提醒(本地) | 行覆盖层(遮罩/秒数徽标) |
coursewareTransfer |
课件提示(覆盖态) | 最高优先级覆盖全部行 |
IslandNotification → ClassIsland 提醒语义:
| ClassHelper 字段 | ClassIsland 对应 | 规则 |
|---|---|---|
priority: LOW/NORMAL/HIGH/URGENT |
提醒队列优先级(NotificationPriority) |
URGENT=插队立即播放;HIGH/NORMAL/LOW 按排序入队 |
kind:'notification' |
普通提醒(Mask+Overlay 两阶段) | 两阶段时长取 5s/5s(repos/ClassIsland/...NotificationRequest.cs:17-20) |
kind:'homework' |
作业提醒(本地合成 id homework-<id>) |
不进入已读回执;不可"标为已读" |
kind:'call' |
叫人(Call) | 上课时保持可点、不自动展开;紧急叫人按 URGENT |
title/content/teacherName/courseName/createdAt |
提醒标题/正文/来源/时间 | 气泡第一行标题、第二行来源+时间 |
id |
去重键 | 双链路(通知/叫人)合并时 id 相同即合并(见 02 §2.5 第 3 条) |
ScheduleDto → ClassIsland 课表语义(仅映射,不落 Profile 文件):
| ClassHelper 字段 | ClassIsland 概念 | 映射规则 |
|---|---|---|
id |
ClassInfo/条目标识 |
仅内部使用 |
course.courseName |
Subject.Name |
直接展示;null 显示"—" |
dayOfWeek(1–7) |
TimeRule.WeekDay |
1=周一…7=周日 |
startTime/endTime(HH:mm) |
TimeLayoutItem.StartTime/EndTime |
转 HH:mm:00;只用于显示与进度计算 |
weekStart/weekEnd |
周次范围 | 用于"本周是否有课" |
weekParity: ALL/ODD/EVEN |
TimeRule.WeekCountDiv(0/1/2)+ WeekCountDivTotal=2 |
ALL→0;ODD→1;EVEN→2 |
location |
附加信息 | 仅在气泡/详情展示,不参与时间点 |
IslandTimePointDto → TimeLayoutItem:
| ClassHelper | ClassIsland | 规则 |
|---|---|---|
timeType: LESSON/BREAK/DIVIDER/ACTION |
TimeType: 0/1/2/3 |
一一对应(repos/ClassIsland/...TimeLayoutItem.cs:119-138) |
name(课间名/时间点名) |
BreakName/名称 |
课间空时显示"课间休息" |
courseName |
默认课程 | null 等价 Guid.Empty(无课) |
startTime/endTime |
开始/结束 | 用于行内课程表折叠与进度 |
isHideDefault |
IsHideDefault |
折叠时隐藏、轮到时展开 |
- 上述映射只用于渲染语义对齐,不得把
ScheduleDto落盘成 ClassIsland 档案;数据仍以服务端解析结果为准(02 §2.12)。 CoursewareTransfer映射为"最高优先级覆盖态",不进入提醒队列、不参与已读;映射字段name/category/teacherName/receivedBytes/totalBytes/progress/stage/error直接使用(packages/shared/src/courseware.ts、src/island/CoursewareTransfer.vue:12-65)。
运行期状态扩展(IslandState,不落盘,必须随状态推送):
| 字段 | 类型 | 默认值 | 语义 |
|---|---|---|---|
family |
'classisland' | 'winisland' |
当前 appearance.family |
当前生效风格;渲染进程据此切换 CSS 命名空间 |
pendingStaged |
number | 0 |
上课期间被暂存、下课后待补弹的消息数(对应现有 queued 中因上课被压住的数量;queued.length 不变) |
bubblePhase |
'idle' | 'mask-in' | 'content-in' | 'out' |
'idle' |
行内气泡/提醒的两阶段播放阶段(固定 5s/5s 时长的可视化映射) |
bubbleDeadline |
number | 0 |
当前气泡自动收起的时间戳(毫秒);0 表示无 |
IslandState的新字段只存在于运行期推送(island:state),不得写进config.json、不得被编辑器保存;旧渲染进程缺字段时必须按默认值渲染(前向兼容)。pendingStaged必须由主进程根据现有上课暂存逻辑计算(02 §2.5 第 2 条),不得让渲染进程自行推断。
IslandNotification.level(展示层级,派生字段):
priority |
level |
用途 |
|---|---|---|
URGENT |
'emergency' |
紧急气泡/强提醒特效可触发 |
HIGH |
'high' |
高亮气泡/角标 |
NORMAL |
'normal' |
普通气泡 |
LOW |
'low' |
弱化展示 |
level 由 priority 派生,不落库、不持久化;通知进入岛队列时在 main/island/messages.ts 统一计算,渲染层只读取。
IslandLessonReminder 扩展字段(运行期,不落盘):
| 字段 | 类型 | 语义 |
|---|---|---|
progress |
number (0–1) | 当前提醒/课程进度:ending/in-class = (now-lessonStartsAt)/(lessonEndsAt-lessonStartsAt);其余 = (endsAt-now)/(endsAt-startedAt)(与现有 LessonReminderContent.vue:36-50 计算一致) |
leftSeconds |
number | 下课前剩余秒(ceil((lessonEndsAt-now)/1000),clamp ≥0),供蓝色圆形秒数徽标与遮罩标题使用;现有 LessonReminderContent.vue:24-35 已有同义计算 |
kind |
不变 | 仍为 prepare/on-class/break/ending/in-class |
fixed(classisland)下的提醒叠加口径:prepare/on-class/break 以行内容接管呈现(临时替换课程表内容,动画由 08/10 定义);ending 不接管行内容,只在课程名右侧显示蓝色圆形秒数徽标(leftSeconds 驱动);custom 模式不接管,保持课程组件 + 秒数徽标。该口径只影响渲染,不改变 02 §2.8 的五类提醒计算与开关语义。
5.7 IPC payload 版本与兼容
- IPC payload 可以携带
schemaVersion:2仅用于运行时校验/调试,不得落盘、不得作为配置版本判据;配置版本只认islandLayout.version。island:state/island:appearance/island:get-layout三个高频 payload 建议带上该字段(旧字段保留,新增字段可选)。 - 接收方规则:未知字段忽略、缺字段回落默认;不得因为缺
family就拒绝渲染(老渲染进程遇到新主进程时应按winisland降级,反之按classisland)。 island:save-layout必须返回归一化后的 v2 结果(编辑器以返回值为准,避免"本地看着对、主进程夹取后不一致")。- 主进程
getConfig()返回给渲染进程的islandLayout必须是 v2;island必须含family与新增外观字段(theme/effectEnabled/effectScale/progressBar)。 - 旧通道名全部保留(见 04 §4.4);新增通道必须同时在
shared/island/ipc.ts与 preload 中登记,不得字符串散落。 - 作业指纹相关 payload(见 04 §4.3.13)必须带
source(realtime/homework-local/replay/call)与dedupeKey;主进程按 key 去重,渲染进程保留现有"本机刚录作业免上岛"快速路径。
5.8 编辑器预览与临时状态(不持久化)
- 编辑器的内嵌预览、缩放、选中项、拖拽态属于临时状态,不得写入
config.json;只有"应用/保存"才写 v2。 - "无消息展开内容(expandedRows)"与常态
rows分开编辑、分开保存;activeSchemeId只在切换方案时写。 - 设置页"测试灵动岛"仍走
islandPush+context.preview=true(30 秒预览、失焦不收起,见 02 §2.15),预览消息 id 必须带前缀(如appearance-test-)以便清理;预览不写历史、不改已读。 - 编辑器打开时若检测到迁移 warning,必须在顶部显示"部分旧设置已按新格式调整(可查看详情)",不得静默。
5.9 回滚与降级
- 回滚入口:设置页"灵动岛 → 恢复升级前布局"必须存在;回滚步骤:先备份当前
config.json→ 把config.json.v1.bak覆盖回去(该文件天然没有islandLayout.version,即 v1)→ 提示重启 → 重启后旧版本行为恢复(family=winisland/fixed=胶囊)。 - 迁移失败降级:应用不得崩溃;以内存默认 v2 启动(不写盘),岛显示默认信息行,设置页显示错误与"恢复备份"按钮。
- 前向兼容:读到
islandLayout.version>2时,应用只读该配置(不覆盖写),在设置页提示"配置由更高版本创建";不存在其他版本字段可用于判断。 - 文件损坏:
config.json非法 JSON 时沿用现有.bak回退逻辑(config.ts现状:读取失败回落默认),回滚后仍必须按islandLayout.version缺失执行一次迁移判定;不得用损坏文件覆盖.bak。 - 旧版应用打开新文件:不可避免(旧版不认识 v2 字段),因此 v2 必须保持"未知字段保留"原则(5.5 第 11 条)并把关键新数据放在旧版会忽略而不是报错的位置;旧版保存后新字段可能丢失,需在文档提示用户不要跨版本反复切换。
5.10 版本变更记录与兼容矩阵
必须维护一张可追溯的变更表(代码里以常量数组或文档形式存在),用于判断"某个字段从哪版开始有、哪版废弃":
| 变更 | 版本 | 兼容策略 |
|---|---|---|
islandLayout.version 新增(唯一落盘版本字段) |
v2 | 缺失=1,按 5.4 迁移 |
appearance.family 新增 |
v2 | 缺失时:老配置迁移=winisland;新装=classisland |
appearance.theme(fluent/classic)新增 |
v2 | 缺失=fluent;仅 classisland 生效;具体 token 由 06 定义 |
appearance.effectEnabled / effectScale 新增 |
v2 | 缺失 true/1;effectScale 只允许 1/0.75/0.5 |
appearance.progressBar {height,show} 新增 |
v2 | 缺失 {3,true};高度优先级 行 > 方案 > 外观 |
appearance.fontSizes 新增 |
v2 | 缺失时按旧 fontSize 比例推导(5.4 step1) |
fixed(信息行)新增 |
v2 | 缺失时生成默认 1 行(date+schedule);仅 classisland fixed 使用 |
行级 appearance/hideRules/progress 新增 |
v2 | 缺失时补默认;字段名与 09 对齐;winisland fixed 下保留数据但忽略(custom 始终使用) |
方案级 rowHeight/rowSpacing/theme/fontSizes/progressBarHeight 新增 |
v2 | 缺失时继承 appearance 默认 |
appearance.colorScheme 新增 |
v2 | 缺失=system;非法回落 system |
appearance.fontSize 语义按 family 分叉 |
v2 | classisland=正文基准(新装 16,派生 14/18/20);winisland=旧语义(13);迁移保留旧值 |
appearance.winislandStyle 新增,style 转兼容镜像 |
v2 | 读优先 winislandStyle、回退 style;写两者同步;style 不删除;切换 family 保留原值 |
滚动组件 settings.mode 新增 |
v2 | 缺失=continuous;非法回落 continuous;pause 复用 settings.interval |
residentEnabled 转纯兼容 |
v2 | 读出后与 layout==='custom' 对齐;不再作为独立开关 |
family 作用域 |
v2 | 只影响 fixed;custom 始终 ClassIsland 规格、不受 family 影响 |
旧 style 字段 |
保留镜像 | 本轮不删除;与 family/winislandStyle 独立;建议 v3 再评估废弃 |
迁移标记文件 config.island-migration.json |
2 | 仅诊断用途;不是版本字段、不参与运行逻辑 |
回滚文件 config.json.v1.bak |
2 | 迁移前覆盖写;回滚后仍保留,供二次回滚 |
兼容矩阵(读写方向必须明确):
- 新读旧(v2 代码读 v1 文件):允许,走 5.4 迁移;迁移后内存中只存在 v2。
- 新读新:允许,走 normalize;
islandLayout.version更高时只读。 - 旧读新(旧代码读 v2 文件):旧代码会忽略未知字段继续运行(因为 v2 把新字段放在其不认识的键上),但旧代码保存后会丢新字段;因此必须在文档与发布说明中写明"不要用旧版应用反复编辑新配置"。
- 新写旧:禁止。v2 代码写盘必须带
islandLayout.version:2,不得输出 v1 结构,也不得写任何文件级版本号。 - 迁移与编辑器并发:编辑器打开时若检测到
config.json被迁移,必须重新加载(比较updatedAt/mtime或读取返回值),不得用旧草稿覆盖迁移结果;保存前再次getConfig核对islandLayout.version。
兼容性红线(不得违反):
- 不得删除任何仍被旧配置使用的字段名(如
style、residentEnabled、spacing、expandedRows、pointerFadeEnabled)。 - 不得把对象结构从"方案数组"改成"对象字典"或反之(旧编辑器/旧冒烟会解析失败)。
- 不得改变
ISLAND_LIST_METRICS与ISLAND_SIZE_DELTA的语义(可以新增 classisland 专用 metrics,但旧常量必须继续导出且数值不变),因为大量冒烟断言按旧值计算(packages/shared/src/constants.ts:344-351,379-402)。 - 不得让 v2 的
rows与 v1 的rows在同一键上结构不兼容:v2 行只增加字段,不重命名已有字段。
5.11 验收标准(第 5 部分)
- 新装启动后
config.json不含任何schemaVersion;含island.family:'classisland'、island.colorScheme:'system'、island.fontSize:16、island.theme:'fluent'、island.effectEnabled:true、island.effectScale:1、island.progressBar:{height:3,show:true}、islandLayout.version:2、fixed.rows默认 1 行(日期+课程表);layout:'fixed',界面呈现 ClassIsland 信息行。 - 用一份旧 v1 配置(fixed、custom 各一份,含 schemes/nodes/appearance/residentEnabled)启动:迁移成功、无字段丢失、
family:'winisland'、旧胶囊外观不变、fontSize保留原值、style/winislandStyle一致;config.json.v1.bak存在。 - 迁移后再次启动:不重复迁移、不重复写盘(可用文件 mtime/内容哈希断言幂等)。
- 迁移失败模拟(把
config.json改坏/只读)时:原文件不被破坏、应用以默认 v2 启动、设置页显示错误与恢复入口。 - "恢复升级前布局"可把 v1 备份恢复回来并重启生效;恢复前自动再备份当前配置。
normalizeIslandLayoutV2对行数>12、节点>200、深度>6、方案>20、非法颜色/数值/kind 全部夹取或保留到unrecognizedNodes;不崩溃、不丢未知组件。- 老用户切到
classisland后 fixed 变信息行、切回winisland恢复胶囊;family 切换时style/winislandStyle与fontSize保持用户原值不变;custom 始终 ClassIsland 规格、切换 family 不改变 custom 渲染。 - 编辑器中常态行/展开行分开保存;应用后
islandLayout为 v2 且island:save-layout返回值与本地一致。 IslandState/IslandNotification/ScheduleDto/IslandTimePointDto的映射表逐项实现:优先级、上课态、时间点类型、单双周、作业/叫人特殊语义均可按 5.6 追踪。- IPC 兼容:旧通道全部可用;payload 可选带
schemaVersion仅作校验(不落盘);缺字段时按 family 降级渲染而不是报错。 - 编辑器/设置页不把预览、缩放、选中项写入配置;预览消息不写历史、不改已读。
- 读到
islandLayout.version>2时应用只读并提示,不得覆盖用户文件。 - 外观新字段:关闭
effectEnabled后强提醒特效不再渲染(行内气泡/普通提醒仍工作);effectScale只接受 1/0.75/0.5 并实际改变特效尺寸;progressBar.show=false时所有行进度条隐藏,行级高度覆盖方案/外观。 - 运行期状态:
IslandState含family/pendingStaged/bubblePhase/bubbleDeadline且不写入 config.json;上课暂存时pendingStaged与队列数一致;气泡阶段按idle→mask-in→content-in→out推进。 - 派生字段:
IslandNotification.level按URGENT→emergency/HIGH→high/NORMAL→normal/LOW→low生成且不落盘;IslandLessonReminder.progress在 0–1、leftSeconds与蓝色圆形徽标一致(剩余 1 秒显示-1)。 - fixed(classisland)下
prepare/on-class/break以行内容接管呈现、ending只显示秒数徽标;custom 下不接管(课程组件 + 徽标);两种 family 的提醒开关语义一致。 - fixed 行内气泡同一时刻只显示 1 条 active;多条折叠为「还有 N 条」并可打开应用内通知中心;队列/去重/暂存规则不变。
family='winisland'只影响 fixed(保持现有胶囊观感);custom 始终按 ClassIsland 规格渲染、不受 family 影响、不引用 winisland 样式命名空间;family='classisland'的 fixed 使用 classisland 命名空间。- 经
pnpm verify:desktop(默认无头 quick)验证时,所有既有冒烟注入点仍可用:setTestInputPassthrough/setHitTestCursor/getWindowBounds/getHitRect/getInteractive/setBlurCollapseEnabled/setTouchMode/getTouchMode/isRecovering等名称与语义不变(改造归属 part 11 测试基线)。 colorScheme三种取值均生效:system跟随应用深浅色(html.dark)、light/dark强制;非法值回落system;切换后岛即时重渲染且不改主窗口主题。fontSize语义按 family:classisland 下改fontSize会派生 14/18/20 阶梯并整体缩放行文字;winisland 下保持旧"基准字号"行为;迁移用户旧值不被覆盖。- 滚动组件
settings.mode:continuous持续滚动、pause到边界停顿settings.interval秒;normalize 后取值合法,旧配置缺失补continuous。
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 |
实现要求:
- 岛根元素必须同时挂三个数据属性,CSS 用它们切 token 与几何:
data-family="classisland|winisland"、data-theme="fluent|classic"、data-color-scheme="light|dark"(winisland 恒为dark)。禁止用 JS 拼内联样式切风格。 family='classisland'时不使用spring.ts/squircle.ts:几何用标准border-radius+ CSS transition;family='winisland'时继续用弹簧形变与超椭圆路径(Lead 裁决第 4 条,parts-r/01-objective.md:52)。- 两套 family 的 token 不得混用:classisland 的静态值定义在
--ci-*;winisland 的调色板保持--wn-*;运行时外观注入继续用--island-*(两套共用这几个动态变量:--island-font/--island-radius/--island-accent/--island-dur/--island-anim)。 - 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。 - 主题切换动画只允许改颜色类 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 相关部分)
config.json.island增加family、theme、colorScheme、winislandStyle(可选,默认为旧style的值)。- 老用户升级:
family缺失 → 判为老用户 →family='winisland',winislandStyle = 旧 style,theme/colorScheme写默认值但忽略。新安装(无 config 或island缺失)→classisland。 - 旧
style='black'|'glass'|'tinted'保留字段(winisland 使用),同时写入winislandStyle;classisland family 下忽略style。 appearance.accent保持用户值;仅当值等于旧默认#0a84ff且迁移到 classisland 时改#00BFFF,并在迁移日志中记录。appearance.fontSize:winisland 保持 13;迁移到 classisland 时若仍为 13 → 改为 16(避免 4 档过小),并记录。scheme.radius/node.cornerRadius不做强制改写(无损);classisland 渲染时若radius超过 20 则钳制到 20(appearance.radius范围扩到 0–32 仅为兼容 winisland,classisland 侧钳制)。- 迁移必须幂等:
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 类错误(每一条都对应真实会出现的问题):
- 把
--ci-*写进主窗口的renderer/styles/index.css:岛是独立渲染入口,主窗口的:root不共享;写错位置会导致岛内变量解析为空、整条transition简写失效(docs/dev/motion.md:58-59已记录该坑)。 - 漏掉
island.html的样式引入顺序:tokens.css必须在classic-island.css/custom-*.css之前 import,否则后者的var()在解析时找不到值。 - 用
html.dark给岛做浅色:岛不加载主窗口的<html class="dark">,必须data-color-scheme。 - 在 classisland family 里继续用 squircle:
squirclePath()只在 winisland/custom 分支调用;classisland 用border-radius: var(--ci-radius-island)。 - 把
appearance.opacity同时当作背景不透明度:窗口不透明度(0.4–1,阅读消息强制 1)与背景不透明度(默认 0.5)是两个 token;混用会导致"消息看不清"或"背景太实"。 - 给行容器加
backdrop-filter:会模糊整块矩形并破坏逐像素透明;毛玻璃只在 winisland 的glass里以半透明色模拟(docs/dev/winisland-design-tokens.md:23-24)。 - 用
filter: drop-shadow做 classisland 行投影:会新建合成表面导致开合闪帧;classisland 用box-shadow(classic-island.css:114-120)。 - 在 classisland 分支继续用
--wn-text-*:winisland 的 0.92/0.72/0.58 与 classisland 的 100/85.5/77.3/52.9/36.5 不是一套;混用会让同一行文字出现两种灰。 - 字号派生写成固定四档:classisland 四档必须由
fontSize/16比例派生;写死 14/16/18/20 会让设置页字号滑杆失效。 - 把圆角钳制只写在 CSS:
aspect变化时appearance.radius可能大于行高一半,必须在 JS 里也钳制到min(radius, w/2, h/2)(现有IslandApp.vue:334-341已做,classisland 分支同样要做)。 - 迁移时无条件改用户设置:只有"仍等于旧默认值"的 accent/fontSize 才允许改;用户调过的值必须原样保留(§6.12)。
- 忘记给编辑器预览同步 token:
island-editor.css的预览行、背景、圆角必须与真岛一致,否则"预览一个样、上屏另一个样"。 - 把 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 验收标准
- 单一来源:
grep -rn "#[0-9a-fA-F]\{6\}" packages/desktop-client/src/island只允许命中tokens.css、classic-island.css的--wn-*定义块、custom-*.css的兼容块与 SVG 渐变;两个.vue文件内联样式里不得出现颜色字面量(--state-color计算值除外)。 - 双风格隔离:切到
winisland后,岛仍显示 268×44 胶囊 + squircle + 弹簧开合,像素 diff 与重构前一致(±1px);切到classisland后无 squircle path、无弹簧形变,行高 40/圆角 8/进度条 3px。 - 默认值:新安装
family='classisland'、theme='fluent'、colorScheme='system';旧 fixtures 升级后family='winisland'且winislandStyle等于旧style。 - 字号:classisland 下把
fontSize从 16 改到 20,四档变为 17.5/20/22.5/25,行高仍 40 且内容按第 7 部分规则省略;winisland 下改字号时全部文本按排版系数等比缩放。 - 颜色:深/浅色各截一张基准图,
--ci-text-1..5、背景 0.5、描边 1px、进度条轨道/填充与 §6.2/§6.10 表逐项一致(颜色 ±2/255)。 - Fluent/Classic:切换主题后,行背景色、描边有无、阴影、圆角(Fluent 8 / Classic 0)按 §6.2.6 变化,字号与间距不变。
- 不透明度语义分离:把行背景不透明度调到 0.2、窗口不透明度调到 0.8,两者独立生效;打开一条消息时窗口不透明度强制 1(
displayOpacity),关闭后回到 0.8。 - 层级:消息遮罩在内容之上、遮罩文字在遮罩之上;编辑器拖拽幽灵
z-index:10000高于一切;岛窗口始终screen-saver层级。 - DPI:100%/125%/150%/200% 四档下,行高 40、圆角 8、字号逻辑值不变,无 0.5px 模糊;截图基准按 devicePixelRatio 换算后 ±1px。
- 迁移幂等:对同一份配置连续运行
normalizeIslandAppearance()三次,输出字节级一致;旧字段residentEnabled、style仍在且语义不变。 - 兼容别名:
--ci-lesson-*重命名期间旧--ci-duration等变量仍生效(至少一个版本),组件视觉无变化。 - 无新依赖/无新框架:
package.json依赖零新增;token 只用 CSS 自定义属性,不引入 CSS-in-JS 或新 UI 库。 - fixed 气泡恒定 40px:构造超长正文(>500 字)的消息,行高仍为 40、正文最多 2 行且以省略号收尾,窗口
bounds不因该消息变化;maxCardHeight只在 custom 多条列表/强提醒窗口路径被读取。 - 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 改造口径与文件级改动清单
总口径(必须遵守):
- 目标视觉默认改为 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,避免观感突变。 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)。- 不新增第二个窗口、不新增框架、不改服务端:
LayoutIslandApp.vue仍是唯一渲染入口,main/island.ts仍是唯一窗口持有者(packages/desktop-client/src/island/LayoutIslandApp.vue:1-76)。 - 消息/提醒/课程提醒的业务语义(优先级、去重、上课暂存、紧急展开、已读回执)归第 8 部分;本部分只规定它们在岛上的容器、尺寸、颜色、动画。
- 所有像素值以本文与第 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 的部分,必须逐条保持:
- 窗口默认
setIgnoreMouseEvents(true, { forward: true })(main/island.ts:527)。 - 渲染层只上报"进入岛体"(
setInteractive(true)),不上报关闭;关闭由主进程按真实光标轮询决定(IslandApp.vue:456-487的整段说明)。理由:两边都下发会让窗口在"接收/穿透"之间每几毫秒翻转,表现为"点了没反应"。 - 命中矩形
hitRect:渲染层用.island-card/.resident-card的getBoundingClientRect()上报(IslandApp.vue:421-454、CustomIslandRuntime.vue:200-213),限流 50ms 且尾随发送一次;主进程按 60ms 兜底校正(main/island.ts:1824-1836)。 - 触摸模式:渲染层按
navigator.maxTouchPoints > 0上报setTouchMode(true)(IslandApp.vue:546-549),主进程切到"窗口贴合岛体 + 始终接收输入"(main/island.ts:1891-1899, 1795-1807)。触摸模式的窗口只增不减,收小要等窗口隐藏后再做(syncTouchLayout,:1560-1578)。 - 失焦宽限:
BLUR_GRACE_MS = 500、触摸/编辑态BLUR_GRACE_TOUCH_MS = 1200(main/island.ts:2287, 2297);blur 时若指针仍在岛上则忽略(:565-578)。 - 常驻模式的指针避让淡化:
residentFaded由updateResidentPointer()计算(:1851-1877),触发条件包含"非编辑、非课程提醒、非展开、非触摸操作后 1s、未悬停铃铛",淡化比例 =CLASSISLAND_ISLAND_METRICS.fadedOpacity = 0.05(island-layout.ts:35)。 - 编辑预览时不创建额外桌面窗口(
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。
改造要求:
- 把
LessonReminderContent.vue的遮罩部分抽成packages/desktop-client/src/island/SlantedMask.vue(props:width、height、open: boolean、accent: string、motion: boolean、speed: number),LessonReminderContent与新的通知遮罩共用。 - 通知遮罩的 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>
- 遮罩填充色 =
var(--ci-accent-fill)(ClassIsland 用AccentFillColorDefaultBrush);文字色 =var(--ci-text-on-accent)。 prefers-reduced-motion或appearance.animations === false时直接落位(regions全部 0/1),不做逐帧插值(LessonReminderContent.vue:77-80已有该分支,保留)。- 不得给遮罩层加
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'下有意义(纯黑/毛玻璃/主题色渐变),其余情况忽略。
落地规则:
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)保持原样不动。DEFAULT_ISLAND_APPEARANCE增加family: 'classisland'(:265-290)——这是"新安装默认"。normalizeAppearance()接受family白名单(main/island.ts:2244-2264),非法值回退'classisland'。- 升级迁移(必须与第 5 部分一致):旧配置没有
family字段 → 迁移时写入family: 'winisland'(老用户观感不变);新安装 →'classisland'。迁移函数放在packages/shared/src/island-layout.ts的normalizeIslandLayouts()里,与version一并处理。 - 设置页拆分(
IslandSettingsView.vue:291-305现绑定style):- "外观族"下拉绑定
appearance.family,仅在layout='fixed'时可见; - "视觉风格/色调"下拉仅在
layout='fixed' && family='winisland'时可见; layout='custom'时两者都隐藏(custom 恒 ClassIsland 规格),只保留圆角/行高/行距/字号等 Scheme 级设置;- 切换必须即时生效并可回退(走既有
appearance通道,preload/island.ts:48-49)。
- "外观族"下拉绑定
- 主题轴(
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、gapfont*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)。
- 图标尺寸 22×22、
- 动画:数据刷新时数字变化不做过渡(避免每 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);
- 空状态占位"今天没有课程。"/"今日课程已全部结束。"。
- 每个时间点一个"课程 chip":左右各
- 改造(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(ClassIslandLessonsListBox.axaml:56-58),过渡 150mscubic-bezier(0.645,0.045,0.355,1); - 分割线:
1×18、opacity: 0.25(现状:334-342),可提到 24px 与 ClassIsland 的 25px 对齐(P2); - 课后占位:现有"今日课程已结束"只用于 countdown;schedule 需新增
今天没有课程。/今日课程已全部结束。两态(ClassIslandLessonControlSettings默认文案)。 - 空数据兜底:
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)写入 inlinefont-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。
- 默认速度改 40;
- 空数据兜底:内容不溢出 → 静止、不抖动。
- 动画:
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。
- 保留三个 kind 作为"入口标记":
- 空数据兜底:无消息 → 三个入口都不渲染(
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 独有能力 → 保留现状视觉,只做两点对齐:
- 锚点/边距与岛一致(左/中/右 +
ISLAND_SHADOW_PAD,CoursewareTransfer.vue:12-25),尺寸不随style变化; 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 的视觉验证里做桌面像素比对(默认后台验证不显示岛):
- 默认值:新安装
appearance.family = 'classisland';旧配置(无family)升级后family = 'winisland'且 fixed 观感与升级前一致,不报错、不丢组件数据;设置页可一键切换并即时预览、可回退。 1.1family只影响 fixed:layout='custom'时切family不改变任何渲染(custom 恒 ClassIsland 规格),设置页在 custom 下不展示"外观族/视觉风格"两项;custom无 winisland 回退档。 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 变体。family='winisland'的 fixed 模式仍是胶囊 + 详情卡:胶囊 268×44、圆角h/2、squircle、纯黑/玻璃/渐变三色调、展开态阴影0 2px 3px rgba(0,0,0,.11);与改造前逐像素一致。- 岛在 6 个停靠位置、两个显示器、125%/150% DPI 下位置正确:居中停靠不受
marginX影响;左右停靠距屏幕边缘 =marginX;底停靠时窗口贴底且岛不被裁。 - 窗口几何:开合过程中
win.getBounds()不变化(用主进程日志或注入断言验证);只有设置变化/不可见时才setBounds;classislandfixed 的窗口高恒为 40 + 2×20 = 80 DIP。 - 行:可见行高
40px(像素量测)、行内边距 12、行距 5、圆角 8、描边 1、阴影0 4 8 2 #48000000;相邻组件间距 12;进度条 3px。 - 背景不透明度 0.5 只影响背景:文字在截图里为纯白 0.92/0.72 层级,不随背景变透明。
- 三条进度条高度都是 3px(课程、课程提醒、通知气泡),且不出现 2px 残留。
custom模式:rows与expandedRows都按行渲染;scheme.separated=true时每行独立背景胶囊,无整块背景;custom 渲染恒按 ClassIsland 规格,切换family不产生任何视觉变化。 9.1 custom 不被课程提醒接管:prepare / on-class / break / in-class 期间岛仍显示常态行与铃铛(课程进度由schedule组件呈现),不出现整岛替换;ending只在课程名右侧显示蓝色圆形秒数徽标(30×30、-N);不存在"临时接管一行"的行为。- 遮罩:通知/课程提醒共用
SlantedMask,5 段从两侧向中间打开(总 480ms)、关闭反向;开启减少动态效果或关闭动画时直接落位。 - 触摸屏:手指能点开岛(触摸模式生效),窗口贴合岛体,失焦宽限 1200ms;非触摸屏行为与改造前完全一致。
- 鼠标穿透:岛外点击一律落到桌面;命中矩形随形变更新(限流 50ms),形变结束后无"看不见却点不动"的死区。
- 11 个组件逐项:
date显示周三 11/12、clock显示HH:mm、weather显示 22px 图标 + 温度且无数据时不出现NaN、countdown文案与进度环/条可选、schedule当前项高亮 + 3px 进度 + 已结束 0.6 淡化 + 空占位、text空内容不占宽、separator2×25/0.8、slide15s 切换且可关动画、rolling仅溢出时滚动且 40px/s、group间距 12、stack子项完全重叠。 - 消息入口:铃铛数量徽标正确(1–99,>99 显示
99+),无消息显示 chevron;notification/homework/call不在组件库出现,但旧方案里的节点仍能渲染。 - 课件覆盖:传输中出现课件卡且岛隐藏;锚点与边距正确;
pointer-events: none,不吞点击。 - 编辑器:选中组件为 1.5px 虚线强调框;容器组件为 1.5px 虚线;拖入指示在 before/after/inside 三种位置正确;预览缩放 0.3–1.5 不导致岛变形错位。
prefers-reduced-motion与设置里的"动画"开关都生效:关闭时所有弹簧snap()、滚动静止、遮罩直接落位。- 不得出现:
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 必须新增/扩展的字段(只加不改,保证迁移可回滚)
IslandState新增:family: 'classisland' | 'winisland':由外观下发给渲染进程,决定渲染形态;pendingStaged: number:上课期间被暂存的消息数(用于行内角标/铃铛计数);bubblePhase: 'hidden' | 'mask' | 'overlay' | 'closing':气泡阶段(仅fixed使用,见 8.4);bubbleDeadline: number:当前气泡阶段结束时间戳(毫秒),供渲染进程做进度条与自动收起(避免渲染进程自己计时导致主/渲染漂移)。
IslandNotification新增可选level?: 'emergency'|'high'|'normal'|'low':由priority映射而来(URGENT→emergency、HIGH→high、NORMAL→normal、LOW→low),用于 ClassIsland 三级排序键的中间键;不改变服务端 DTO,只在主进程派生。IslandLessonReminder新增progress: number(0–1,课程进度)与leftSeconds: number(整数秒,供徽标直接渲染),由主进程计算下发,渲染进程不得自行Date.now()推算(防止跨进程时钟漂移与系统时间跳变)。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 业务规则,必须保留)
- 按 id 去重:同一 id 只保留一条,
pendingNotifications()以 Map 去重(packages/desktop-client/src/main/island.ts:738-749;渲染侧同口径src/island/IslandApp.vue:86-97)。 - 通知/叫人双播合并:
createCall同时发notification:new与call:new,同 id 两条到达时必须合并而非覆盖:kind取更特殊的(call 优先),priority取更高(PRIORITY_RANK比较)(island.ts:713-724)。这是硬性要求,否则会出现「叫人胶囊显示成新消息、上课被当普通通知隐藏」。 - 排序口径:
PRIORITY_RANK降序 →createdAt降序(新的在前)(island.ts:744-748)。不得改用 ClassIsland 的三级键打乱业务优先级——ClassIsland 的「提供方优先级」在 ClassHelper 中没有对应概念,只有服务端单通道消息,因此本重构的排序键固定为(业务优先级 → 时间);若要引入 ClassIsland 的「通知等级」作为第二键,仅在与时间同序时作为稳定排序的次级键,不得跨优先级反转。 - 未读与已读:未读数以服务端为准(
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)。 - 实时撤回:收到
notification:deleted后必须从岛队列与当前展示中移除(island.ts:1210-1216),缓存与通知列表同步清理(docs/app/notifications.md:34)。 - 本机作业指纹:本机刚录的作业通过指纹避免重复提醒自己——这是既有规则,必须保留(
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 不得整体废弃,必须拆成「气泡内容」组件并同时服务两种形态:
- 两阶段复用:
fixed+classisland下,mask阶段只渲染它的header部分(类型图标 + 类型徽标 + 标题 + 紧急/叫人提示),overlay阶段再渲染title / body / footer(标题、正文、元信息、按钮行);列表形态(多条)只在overlay阶段出现。原组件的size/listLayout/pendingNotifications计算逻辑保留,改为由父组件传入。 winisland兼容:整卡(header + title + body + footer 一次性渲染)继续用于详情卡,交互与尺寸不变;只把文案/徽标/按钮语义与新气泡对齐(叫人时按钮「收到」等)。- 不得复制两份实现:图标、徽标、行布局、按钮行必须共用同一组件与同一份
classic-island.css,避免两种形态文案不一致。
8.5 强提醒全屏特效(Topmost)
8.5.1 行为
- 触发条件(全部满足):消息
priority==='URGENT'(紧急通知或紧急叫人);appearance.animations===true且appearance.effectEnabled(新增外观开关,默认true);岛当前可见(不是编辑模式、不是预览、不是隐藏);本节特效与气泡可同时出现。 - 表现:以岛中心为原点,强调色涟漪扩散到覆盖主界面所在屏幕;扩散 600ms、同步淡出 600ms,750ms 后移除元素并隐藏特效窗口。
- 独立窗口:新建
topmost-effect渲染入口与独立 BrowserWindow(transparent:true、frame:false、skipTaskbar:true、alwaysOnTop:true、showInactive、setIgnoreMouseEvents(true)、逐屏一个窗口);禁止setBackgroundMaterial();窗口内元素清空即隐藏,不销毁。 - 缩放与多屏:
effectScale(默认 1.0,复用现有ISLAND_SHADOW_PAD之外的独立字段)缩放窗口与圆心;屏幕/DPI 变化重算;4K@150% 下涟漪必须覆盖整屏无接缝。 - 降级:
animations=false、系统减少动态、编辑模式、窗口重建中、系统休眠恢复后的 1s 内 → 不播特效(只播气泡),不得阻塞消息展示。 - 非目标:本轮不做音效与 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。
- 当天课表 =
dayOfWeek === (now.getDay() || 7)的条目,按开始时间升序(周日=7)。 current= 满足start ≤ now < end的条目;next= 第一个start > now;ended=end ≤ now且距end< 12 000ms 的最近一条。- kind 判定顺序(自上而下,命中即止):
current && now - current.start < 5 000 && onClassEnabled→on-class;breakEnabled && !current && ended && now - ended.end < (next ? 12 000 : 5 000) && (now - ended.end < 5 000 || this.current?.id === keyOf(ended,'break'))→break;current && endingEnabled && current.end - now ≤ endingSeconds*1000→ending;!current && next && prepareEnabled && next.start - now ≤ prepareSeconds*1000→prepare;current && classProgressEnabled→in-class。
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 |
id = "${dayKey}:${scheduleId}:${start}:${end}:${kind}";entered为同一 id 的连续保持时刻(避免同一提醒重复计时)。- 课程名
course?.name ?? '课程';时间字符串截到HH:mm(导入数据可能带秒,lesson-reminder.ts:78-79)。 - 渲染进程不得自行推导 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) |
必须遵守的三条硬规则:
- 没有「即将下课」遮罩:
ending绝不显示 ClassIsland 式全行遮罩,只显示课程名右侧的蓝色秒数徽标(docs/app/island.md:68)。 - 不替换自定义岛常态组件:
custom模式下课程提醒(除 prepare/on-class/break 的临时接管)不得替换常态组件行。 - 关闭开关即不显示:
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 行为
- 显示条件(全部满足):
kind==='ending'(即current && endingEnabled && end - now ≤ endingSeconds*1000);leftSeconds ≥ 0;课表数据存在endTime。 - 数值:
leftSeconds = ceil((lessonEndsAt - now)/1000);显示为-${leftSeconds}。 - 剩余一秒显示
-1:当真实剩余时间在(0, 1000]ms时ceil结果为 1,徽标显示-1;不得显示-0,也不得在剩余 0 时继续显示(到点后ending条件不再成立,徽标消失)。这是用户明确要求的行为,必须有单测用例(now = end - 1ms→-1)。 - 阈值与开关:
endingSeconds默认 60(5–600);endingEnabled=false时完全不出现徽标;animations=false也不影响徽标(徽标不是动画)。 - 出现/消失动画:徽标可淡入 100ms(
opacity 0→1)并随课程提醒条一起退出;不得用缩放弹跳(避免课堂投影上的视觉干扰)。 - 两种岛都要用:
fixed(两种 family)与custom都必须显示该徽标;custom下徽标挂在schedule组件的当前课程名右侧,由组件内部渲染同一lesson-ending.css,不得复制第二套样式。 - 边界:
- 无时间点数据、按
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 服务行为(必须原样保留)
- 固定服务与参数,不接受渲染层传 URL:
https://api.open-meteo.com/v1/forecast+current=temperature_2m,weather_code,wind_speed_10m+forecast_days=1(island-weather.ts:35-41)。 - 坐标校验:
latitude/longitude必须是有限数字,纬度绝对值 ≤90、经度 ≤180,否则抛「请设置有效的经纬度」(:14-22)。 - 缓存键 = 坐标定点 4 位(
:23);成功缓存 15 分钟(:60);失败缓存 60 秒退避并保留旧数据(:63);同一坐标并发请求复用同一 Promise(:29-30);缓存上限 32 条(:68)。 - 请求安全:
AbortSignal.timeout(8000)、redirect:'error'、响应体 > 64 000 字节拒收(:42-45);自动验收模式下不访问外网(:33-34)。 - 数据校验:三个数值字段必须都是有限数字,否则「天气数据格式不正确」(
:47-52)。 stale:成功返回时按fetchedAt距今 ≥ 15 分钟标记;失败返回旧数据时强制stale:true(:26,64)。- 渲染侧:每 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)
- 课件传输(
coursewareTransfer)覆盖整岛内容:fixed的两种 family 与custom都必须显示;getState()时若「当前有正在展示的紧急消息或叫人(mode!=='hidden' && active && (priority==='URGENT' || kind==='call'))」则课件让位,状态里返回coursewareTransfer:null。 - 其余消息不因课件覆盖而丢弃:
active/queued保持原状,课件结束后按队列规则继续展示。 - 课件传输期间不触发强提醒特效(课件不是紧急消息)。
- 课件覆盖时窗口几何按
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 教师端 / 班级端差异
- 岛只在班级端运行:教师/管理员使用 Web 管理端,不启动岛;客户端以
CLASS_DEVICE登录时才有「本班」消息。 - 班级端:接收本班通知/作业/叫人/课件;课程提醒基于服务端解析后的本班课表(
ScheduleDto/IslandTimePointDto)。 - 教师端(Web):无岛;发布 URGENT 时的二次确认是服务端规则(
docs/app/notifications.md:25-29),与岛无关。 - 未登录/离线:不展示在线消息;已缓存消息可只读展示(不得伪造未读状态);课程提醒与天气可离线工作(课表来自本地缓存,天气有缓存)。
- 权限:已读/撤回等操作失败(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 边界与失败降级
- 无课表/课表为空:
lessonReminder=null;信息行显示日期 + 课程表占位(今天没有课程。);课程提醒全部不触发;不得因此隐藏整行。 - 上课状态未知(
inClass未下发/服务端断线):按「非上课」处理(消息正常展示),不得猜测;恢复后以下发值为准。 - 时间跳变/时区变化:课程提醒 id 含
dayKey,跨天自动重建;时间回退不得重复触发同一 id 的提醒(entered逻辑),时间前跳不得补发多条。 - 消息风暴(一次 > 20 条):列表最多
maxRows=20,其余提示「更多请前往应用内操作」;不得逐条弹气泡;同一 id 重复到达按合并规则处理。 - 重复广播 / 断线重连重放:以 id 去重;重连后以服务端列表为准做一次全量对账(新增/删除/已读状态),不得凭队列恢复弹窗。
- 已读请求失败:
markRead/markAllRead失败时恢复本地未读状态并提示;不得把消息从队列删除。 - 课件传输中断/断网:显示
failed + error;恢复网络后由心跳补拉(docs/app/courseware.md:26-28);岛不得自行重试下载。 - 渲染进程崩溃/窗口重建:主进程保留
state,重建后一次性下发;课件/课程提醒/消息不丢;重建限流沿用现有(3 次/10 分钟)。 - 系统睡眠/唤醒:唤醒后立即重算课程提醒与上课状态;跨过的时间点不补弹(与 8.13 第 3 条一致);课件若在睡眠中完成,按最新状态展示。
- 多屏切换/DPI 变化:重算命中矩形、气泡与特效窗口;切换瞬间不得显示残影(先隐藏特效窗口再重算)。
- 无网络启动:消息/课表用离线缓存(按服务器与班级隔离),天气显示缓存或「暂不可用」;课程提醒只要课表缓存存在就必须工作。
- 编辑模式/设置预览:编辑模式不弹新气泡、不播特效、课程提醒条不接管编辑画布;预览模式停留 30s 自动结束(
PREVIEW_DURATION_MS,island.ts:2299-2300),不写已读。
8.14 验收标准(可勾选,全部通过才算完成)
消息与队列
- 同一 id 的通知与叫人双播到达:只出现一条,
kind='call'、priority取高者;不会降级成普通通知。 - 课间收到普通通知:
fixed+classisland行内自动展开气泡(先遮罩后正文);winisland为胶囊;custom铃铛计数 +1。 - 上课收到普通通知:不弹气泡,行内显示暂存角标;下课自动补弹最新一条,其余保留「还有 N 条」。
- 上课收到普通叫人:显示可点 chip,不自动展开;点击后展开气泡并可「收到」。
- 紧急通知/紧急叫人:无论上课与否立即展开气泡,并在 45s/90s 后按规则收起(有未处理时保持 chip/角标)。
- 「知道了」与「标为已读」语义区分正确;整批已读会过滤 homework 本地 id,不报「通知不存在」。
- 实时撤回后,岛内该消息从 active 与 queued 同时消失。
- 消息列表默认 3 行、超出显示「展开更多(还有 N 条)」;展开后受屏幕高度限制改提示「更多请前往应用内操作」。
气泡视觉
- 气泡遮罩 5 区段斜切扫入,区段 360ms、错峰 120ms;文字延迟 260ms 淡入 250ms + 缩放 750ms;退出 200ms 且缩放回 1.1;
speed改变后所有时长同比缩放。 - 行高 40、圆角 8、背景不透明度 0.5、底部进度条 3px、值过渡 50ms 线性;文字始终不透明。
animations=false或系统减少动态时,气泡直接终态,最终几何与有动画时一致。
强提醒特效
- 紧急消息触发全屏涟漪:600ms 扩散 + 600ms 淡出,750ms 后移除;多屏各一窗口;
effectScale生效。 - 编辑模式、隐藏状态、窗口重建中不播特效,但气泡照常;特效窗口不接收鼠标、不改变岛命中。
课程提醒
- 五类提醒按 8.6.2 算法在构造的时间轴下逐一出现,且同一提醒不重复;关闭对应开关后不出现。
custom下in-class与ending不接管岛;fixed下ending显示徽标而非遮罩;任何形态都没有「即将下课」遮罩。- 蓝色秒数徽标:正圆 30×30、
#0a84ff、12px/600、显示-N;now = end - 1ms时显示-1;endingEnabled=false不显示。 - 课程进度:提醒条 2px、组件内 3px,两条同时存在且均贴底、均用
scaleX。
天气与课件
- 天气 15 分钟缓存、失败 60s 退避并保留旧数据、断网显示「· 缓存」、坐标非法显示「请设置天气地点」;同一坐标并发请求只发一次网络请求。
- 课件传输覆盖整岛(424×132),显示文件名/类型/教师/已收-总量/进度;紧急或叫人在播时课件让位且不丢消息;完成后 SHA256 校验通过才显示「完成」。
边界
- 跨天、时间跳变、睡眠唤醒、渲染进程崩溃重建后,消息不丢、不重复弹、课程提醒状态正确。
- 无课表/无网络/未登录时,岛显示合理占位且不崩溃;已读失败会回滚并提示。
- 全程未出现音效/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 对齐)
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 观感。
layout与family正交:layout: 'fixed' | 'custom'决定「消息行 / 常驻组件方案」与是否开放编辑器;family只决定 fixed 的视觉族。设置页必须在layout === 'custom'时把family控件置灰,并显示「自定义方案始终使用 ClassIsland 规格;外观族只作用于经典 fixed 形态。」;layout === 'fixed'时family可用且实时生效。- 升级口径:老配置文件没有
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迁移函数同批落地。 - 术语:组件(
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)。 - fixed 的内置信息行 v2 是只读内置方案,不可编辑:
classisland+fixed固定为 1 行 = 日期 + 课程表,该行属于内置方案,不进入方案编辑器(不占schemes/rows/nodes,编辑器只服务 custom)。设置页对 fixed 只提供 9.5 的外观参数与 9.4-4 的静态缩略示意,不得提供行/组件编辑入口、不得出现「编辑固定行」按钮。后续版本若开放 fixed 行编辑,必须升级islandLayout.version并在 parts-r/05 增补契约;v2 不开放。 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 规则)。
- 页头:左侧
h2「灵动岛」;右侧显示当前形态徽标el-tag:经典信息行(family=classisland、layout=fixed)/自定义常驻(layout=custom)/经典胶囊(兼容)(family=winisland、layout=fixed)。徽标随保存结果更新,不额外请求。 - 布局形态卡(保留既有两张选择卡并升级文案与点击):
- 左卡「经典灵动岛」:
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),卡片说明里必须写明「固定信息行由内置方案渲染,暂不支持编辑」。
- 左卡「经典灵动岛」:
- 外观卡:标题「外观、位置与弹出」保留;顶部说明两段改为:
classisland:「默认信息行高度 40、圆角 8、背景不透明度 0.5;紧急消息立即展开,课中普通通知暂存、下课后展开;自定义岛可点击铃铛主动查看。」winisland:保留现有「紧急消息立即展开;课中普通通知暂存,下课后展开。自定义岛可点击铃铛主动查看。」- 卡内第一行必须是 视觉风格(family) 控件(9.4),随后按 family 分支渲染参数组。
- 课程提醒卡:新增(内容见 9.7),单一数据源固定为
config.json.islandLayout.lessonReminders;原/settings/reminder页的「上下课提醒」只保留只读摘要 + 跳转,不得保留可编辑控件(避免双写)。 - 指针与窗口卡:
layout=custom时显示「指针避让」整块(现有.resident-window-options,:199-218、:423-444);classislandfixed 下增加「触摸后自动恢复」说明【推断:由 ClassHelper 触摸失焦宽限承担,无独立参数】。 - 底部操作条(保留现有):
保存设置(primary,loading)、预览效果、恢复默认;下方 InfoBar「拖动滑块即为实时预览;保存后写入本地配置,重启客户端仍然生效」(:385-395原样保留,文案可保留)。
9.4 视觉风格(family)与两套参数组
- 控件(必做):
el-segmented(Element Plus 2.14 可用)或两个el-radio-button,标签:ClassIsland 风格(valueclassisland,默认):「信息行 / 半透明横条,行高 40、圆角 8、底部进度条;正文基准字号 16」;经典 WinIsland(兼容)(valuewinisland):「保留胶囊 + 详情卡外观,仅建议老用户保持;岛字号默认 13」。 下方常驻一行 Warning 说明(el-alert type="warning",不可关闭):「从旧版本升级的配置默认使用经典 WinIsland 外观;切换到 ClassIsland 风格会改变经典形态的外观,消息暂存/紧急/叫人等规则不变。」——这就是老用户升级默认 winisland 的用户侧说明。
- 切换行为:
family写入islandAppearance.family,通过applyIslandAppearance({ family })实时应用(islandSetAppearance),点「保存设置」才写config.island。切换不得重置另一族的参数值(两族参数分别保留在IslandAppearance中)。 字号语义随 family 切换:classisland下appearance.fontSize是正文基准字号,标签文案必须为「正文字号(ClassIsland 基准)」,默认16,并由它派生 14/18/20 三档阶梯(次级/强调/大号);winisland下保持旧语义,标签文案必须为「岛字号(经典)」,默认13,岛内全部文本按旧系数缩放。切换 family 时必须切换标签文案与「默认值」提示(如滑块右侧显示「默认 16」/「默认 13」),但不得在切换瞬间强制改写用户已设置的值;仅当值为空/非法时才用当前 family 的默认值。 - 可见性规则:
layout === 'custom'时family控件disabled,并显示「自定义方案始终使用 ClassIsland 规格;外观族只作用于经典固定形态。」;layout === 'fixed'时按family显示下方参数组:classisland显示 9.5,winisland显示 9.6。 - 固定信息行预览缩略(新增,P1):在
classislandfixed 下于外观卡顶部放一个纯 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 保存、实时预览与失败重试(设置页)
- 两条通道必须区分:
- 外观改动 →
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以允许重试。
- 外观改动 →
- 禁止传响应式 Proxy:所有 IPC 入参必须是展开后的纯对象(
types/desktop.d.ts:143-152、:166-175;现有注释已写明An object could not be cloned.的坑)。设置页已有的{ ...island.value }、JSON.parse(JSON.stringify(...))写法必须保留。 - 圆角/行距/不透明度同步 custom 方案:当
layout === 'custom'时,这三项除写 appearance 外还要写activeScheme.radius/opacity/spacing并saveConfig({ islandLayout: 深拷贝 });失败提示「圆角保存失败,请重试」(现有文案,IslandSettingsView.vue:115-120),并不要回滚滑块显示值(下次保存重试)。 - 恢复默认:
恢复默认按钮必须confirmAction('恢复灵动岛的默认外观?', '恢复默认', { type: 'warning' }),确认后applyIslandAppearance({ ...DEFAULT_ISLAND_APPEARANCE })再保存;新默认 为 classisland 族参数(DEFAULT_ISLAND_APPEARANCE扩展后),但恢复默认不得覆盖family(用户显式选择要保留;如产品要一并重置,需二次确认)。 - 载入与刷新:
onMounted调islandGetAppearance()与getConfig();window.addEventListener('focus', loadLayout)(现有:82-86)保留,确保从编辑器返回后方案列表/圆角同步。 - 预览效果按钮:仅
family=winisland显示(classisland fixed 预览用缩略图 + 真实运行态验证);点击推送预览通知(id 前缀appearance-test-),说明文案必须含「拖动滑块即可实时预览」。
9.9 编辑器总体布局(像素级)
文件:IslandEditorView.vue;样式:styles/island-editor.css。保留现有 DOM 结构(smoke 依赖,见 9.20),只做尺寸/对齐调整。
- 页面容器:
.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)。 - 顶部操作条
.editor-actions:右对齐、gap:8px、min-height:30px、margin-bottom:4px(:19-25);元素依次为保存状态文本(12px,见 9.18)、返回设置(text small)、应用并返回(primary small,loading=!!pending,disabled=!ready)。 - 页头
.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)。 - 错误条:
error非空时在页头下显示el-alert type="error",内容为错误消息 +重试按钮(ready ? save() : load())(:608-610)。 - 内容工具条
.surface-toolbar:左右两组;左侧.surface-tabs两个按钮「常态内容」「无通知展开内容」(12px,选中下方 2px 主色下划线,:102-124);右侧.surface-actions三个按钮:新增行、行设置、进入编辑模式/退出编辑模式(最后一个选中时 primary)(:612-653)。「新增行」文案不可改(smoke 断言依赖)。 - 预览画布
.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)。 - 标签态
.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保留「将组件拖到这里,或点击下方组件添加」。
- 实时预览态
.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)。 - 组件库
.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)。 - 抽屉
.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。抽屉动画统一 250mscubic-bezier(0,0,0,1)(对齐 ClassIslandDrawerHost:repos/ClassIsland/ClassIsland.Core/Controls/DrawerHost.axaml:42-59),并在AnimationLevel=0/prefers-reduced-motion时改为无动画。 - 拖拽幽灵
.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)。 - 导入文件输入:隐藏
<input type=file accept="application/json,.json">(:773)保留。 - 骨架:
ready=false时el-skeleton :rows="8" animated;ready=true才渲染全部编辑 UI(:611)。
9.10 方案栏(保留并升级)
- 选择方案:
el-select绑定settings.activeSchemeId,选项scheme.name;disabled=!ready;@change="switchScheme"(重置选中 + 保存)。aria-label="当前方案"保留(:557-568)。 - 重置当前方案:图标按钮(Refresh)
aria-label="重置当前方案"、title="重置当前方案";确认框标题「重置方案」,正文「将「{方案名}」的组件和外观恢复为默认配置?」,按钮「重置/取消」,type: warning;确认后用默认方案内容替换但保留原 id 与 name,并校验组件容量(现有resetScheme,:403-422)。 - 新建方案(Plus,
aria-label="新建方案"):ElMessageBox.prompt('为新的组件方案命名', '新建方案', { inputValue:'新方案', inputValidator: 1–64 字符, confirmButtonText:'创建', cancelButtonText:'取消' });方案数上限 20(schemeCapacity,:379-383);新方案内容复制默认方案并重生成全部 id(:384-402、renewScheme:371-378);创建后自动选中并保存。 - 管理方案(FolderOpened 图标,
aria-label="管理方案",el-dropdown trigger=click)菜单顺序固定:方案设置、复制方案、分隔、导入方案、导出方案、分隔、删除方案(方案数 < 2 时禁用)(:589-604、:423-432)。 - 复制方案:深拷贝当前方案,名称
${name.slice(0,61)} 副本,重生成 id,push 后选中;受 20 方案与 200 组件上限约束(:214-230)。 - 删除方案:
ElMessageBox.confirm('删除方案「X」?', '删除方案', { confirmButtonText:'删除', cancelButtonText:'取消', type:'warning' });删除后activeSchemeId = schemes[0].id并保存(:231-245)。 - 导出方案:JSON 结构
{ format: 'classhelper-island-layout', scheme }、2 空格缩进、Blob下载;文件名把[<>:"/\\|?*]替换为_;下载后 1srevokeObjectURL(:246-257)。该 format 字符串是老用户导入兼容契约,不得改。 - 导入方案:文件 ≤ 1 MB;解析后校验
format === 'classhelper-island-layout'且scheme.rows为非空数组;经normalizeIslandLayouts归一化;重生成全部 id;容量校验;成功ElMessage.success('方案已导入'),失败ElMessage.error(错误信息);finally清空 input(:258-289)。 - 方案设置抽屉页:名称(
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 编辑模式对齐)
- 行列表:每行渲染
row.name(编辑态以把手 aria-label「编辑{行名}」、title「{行名},拖动排序,点击设置」暴露,IslandEditorView.vue:670-678);行数上限 12(addRow,:142-159,文案「每种内容最多添加 12 行」)。 - 新增行:保留工具条「新增行」按钮;新增行列表末尾居中的第二入口:32px 高、图标
E00D(20px)、文字「新增行」,样式与 ClassIsland「新主界面行」一致(repos/ClassIsland/ClassIsland/MainWindow.axaml:449-455),点击行为与工具条相同。新行默认name: 组件行 N、main:false、allowNotifications:true、gap:8、nodes:[]。 - 排序:拖行把手到另一行位置上/下方即可重排(现有
dropPreview的current.rowId分支,:505-512);也提供抽屉内「上移/下移」按钮(:824-830)。 - 主行:抽屉按钮「设为主要行」;已是主行时按钮禁用且文案「已设为主要行」;设置主行会清除其他行
main(setMain,:210-213);行数为 1 时不允许删除(removeRow,:202-209)。 - 允许提醒:开关「允许显示提醒」绑定
row.allowNotifications;提示文案「提醒优先使用主要行;关闭提醒后使用下一条允许提醒的行。」(:817-823)。 - 行间距:
el-input-number0–64,绑row.gap(组件间距);方案级行间距在方案设置抽屉(scheme.spacing0–150)。两者不得混用。 - 行设置抽屉规格:标题「组件行」;字段顺序固定:当前行下拉(可切换编辑行)、行名称(maxlength 64)、组件间距、允许显示提醒、主行按钮、提示、上移/下移/删除此行(danger plain)。抽屉沿用 360px 非模态规格;打开/关闭动画 250ms
cubic-bezier(0,0,0,1)。 - 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)。
- 选中框:
- 既有交互必须保留:行把手拖动排序、点击行选中并打开行设置、空行点击打开行设置、行尾「新增行」、主行/提醒开关的即时保存、最多 12 行提示。
9.12 组件库(11 类,保留既有 DOM 契约)
- 数据源固定 11 项、顺序见 9.9-9;每项包含
kind、描述(现有library数组,IslandEditorView.vue:58-70)。 - 每个库项必须是
button,带data-library-kind={kind}、aria-label="添加{中文名}";保留pointerdown开始拖拽、click直接添加(两者共存,拖动后suppressClick防误加,:754-768、:447、:500-503)。 - 直接添加逻辑:若当前选中容器则加入该容器(层级 ≥ 6 时警告「最多嵌套 6 层容器」),否则加入当前行末尾;添加后自动选中并保存(
add,:130-141)。 - 组件图标使用
ComponentIcon(:763);名称与描述分别 18px/14px(随--ch-font-scale缩放)。 - 消息类组件(
notification/homework/call)不得出现在库中,但既有方案里的这些节点必须继续渲染与可编辑(只读提示「此组件使用实时数据。可在外观和尺寸设置中调整显示。」)。
9.13 拖拽规格(三类源,必须保留现有实现)
- 统一 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)。 - 三种拖拽源:组件库项(
kind源)、组件 chip(nodeId源)、行把手(rowId源)。 - 落点判定:
- 用
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)。
- 用
- 约束:单次拖拽内不得把父节点放进自己的后代(
all([node]).some(child.id === target.node.id)→ 忽略,:517);目标层级 + 被拖子树高度 ≤ 6(:518-522);组件总数 ≤ 200(capacity,:338-347)。 - 落库:移动完成后
selectedId指向被拖节点、rowId指向目标行、panel='component',立即save()(:534-537)。 - ghost:仅
pointer.moved后显示,文本为组件中文名或行名(dragging.label),偏移 +16/+16。 - 触摸:必须继续支持
pointerType==='touch'(现有 smoke 用合成 touch 事件验证跨行拖动,main/smoke-layout.ts:74-91);拖动时touch-action:none、禁止页面滚动(.component-chip、.row-handle、.library-item已设置)。
9.14 选中、组件设置抽屉(四页签)
- 选中:点击组件 chip →
selectNode:设置selectedId、rowId、panel='component'、打开抽屉;点击行把手/空行 →selectRow:清空selectedId、panel='row';切换方案/常态/展开内容时resetSelection()(IslandEditorView.vue:106-125)。选中框按 9.11-8 规格。 - 抽屉结构:顶部
el-radio-group(方案 / 行 / 组件);component页签在无选中时禁用;内容区渲染IslandComponentSettings(组件)或行/方案表单(IslandEditorView.vue:791-857)。 - 四页签(
IslandComponentSettings.vue重构,顺序固定):内容、外观、尺寸、隐藏条件。每页签使用el-form label-position="top";选项选择类控件宽度 100%;数字/颜色控件与标签同一行(现有:283-307样式保留)。 - 跨页签即时保存:任一控件
@change必须emit('change')→ 编辑器save()(现有模式);不设「应用」按钮。 - 删除/复制/包裹入口:内容页底部固定「包裹到容器」下拉(
data-action="wrap")、「复制组件」、「删除」(danger plain)(:152-170);复制节点时深拷贝并重生成自身与所有后代 id(copyNode,IslandEditorView.vue:180-188);包裹受层级与容量约束(wrap,:189-201)。 - 抽屉 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 预览与「进入编辑模式」
editMode为页面内状态(ref(false)),不打开额外桌面窗口、不注入 iframe(docs/app/island.md:47);smoke 会断言不存在.layout-editor/iframe(main/smoke-layout.ts:69-72、:154)。- 进入:工具条「进入编辑模式」→
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 定义)。 - 预览缩放:仅在编辑模式显示
.preview-toolbar:文本「预览缩放」+el-slider0.3–1.5、step 0.1、tooltip%+ 「重置缩放」(置 1)+ 提示「点选组件调整内容、外观和隐藏条件」(:739-750);缩放与方案缩放相乘(zoom: scheme.scale * zoom)。 - 布局约束:
preview-canvas必须完整容纳预览且不遮挡工具条——canvas.bottom <= footer.top + 1且页面无横向滚动(smoke 断言main/smoke-layout.ts:138-148);separated时按 9.9-8 渲染。 - 点击编辑态组件:与标签态一致打开抽屉(
@select);背景点击(.preview-row @click.self)选中该行。 - 退出:再次点击「退出编辑模式」回到标签态;抽屉保持打开状态但选中不变;
Esc优先关闭抽屉、再退出编辑模式(见 9.17)。 - 实时同步真实岛:编辑器每次
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 数据流、契约与失败处理
-
读取:编辑器
load()→window.desktop.getConfig()→settings = normalizeIslandLayouts(config.islandLayout)、appearance = config.island;失败显示错误条并提供「重试」(现有load,IslandEditorView.vue:300-313)。设置页同时读getConfig()与islandGetAppearance()。 -
保存队列(编辑器):每次改动
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)。
-
退出保存:
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)。 -
失败重试:错误条
重试按钮按ready ? save() : load();保存失败不得清空本地编辑内容;不得自动回滚到磁盘值(避免用户输入)丢失;若连续失败需在控制台/日志打出 IPC 错误(沿用主进程 logger)。
-
应用/保存语义:编辑器每次改动即
saveConfig(含磁盘合并写 120ms,main/config.ts:252-298),所以「退出保存」不是唯一写入点;保存成功≠已点“应用”,但配置已写入islandLayout。layout只有在「应用并返回」或设置页选择自定义时才切到custom。 -
主进程侧:
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)。 -
外观通道:
island:set-appearance主进程 16ms 合并后island.setAppearance(main/island.ts:2389-2410);island:get-appearance会先 flush 再返回(:2407-2410)。外观不落盘,除非设置页点保存写config.island。 -
迁移:
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 语义说明保留在文档与文案中。 迁移失败必须回落默认配置并记日志,不得让设置页崩溃。
- 在
-
禁止:在渲染进程直接写 config 文件;在编辑器内创建第二个 BrowserWindow;把
ref/reactive直接传 IPC;用setBackgroundMaterial()影响岛透明(AGENTS.md客户端章节)。 -
契约对照(必须在 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 状态、空态、主题与性能约束
- 加载/空态:
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)。
- 错误态:
load()失败(非桌面客户端打开、getConfig抛错)→ 页面显示el-alert type="error"+ 错误文本 +重试;编辑功能全部不可用。保存失败只显示错误条与状态文本,不弹全局遮罩,不丢失编辑内存。 - 主题与关怀模式:设置页与编辑器必须只使用
--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「关怀模式放大字号/控件,不改变几何令牌」)。 - 响应式:编辑器按现有断点:≤1150px 组件库 3 列、≤800px 2 列且方案工具区换行、≤520px 1 列并收窄页面左右 padding(
island-editor.css:371-417);设置页在 ≤680px 时布局选择卡改单列(IslandSettingsView.vue:497-505),抽屉宽度min(360px, 94vw)自适应。 - 性能:拖动
pointermove只在dragging存在时处理,且必须passive:false才可preventDefault(现有{ passive: false },IslandEditorView.vue:315);保存用串行队列 + revision 去陈旧结果,禁止每次输入都直接await getConfig();节点上限 200/行 12/方案 20/深度 6 必须在写入前检查而不是写入后裁剪;编辑页不得创建额外 BrowserWindow、不得轮询getConfig(只focus时刷新一次设置页)。 - 无障碍:所有自绘可点元素使用原生
button或role="button"+tabindex="0";保留现有aria-label(新建/重置/管理方案、添加组件、编辑组件、编辑行);拖拽不是唯一操作路径,必须同时提供按钮/下拉的等价操作(添加组件、上移/下移、包裹、复制、删除),以满足键盘与触屏用户。
9.20 既有实现与测试契约(重构不得破坏)
以下行为/文案/DOM 已被现有冒烟与代码依赖,重构时必须保留或由 parts-r/11 同步更新测试:
- 路由:
/settings/island、/settings/island/editor(router/index.ts:63-74);侧栏「灵动岛」入口(ClientLayout.vue:48-55);岛内「编辑」通过onIslandEditorOpen事件把主窗口切到编辑器(ClientLayout.vue:23-26;主进程main/index.ts:229-235)。 - 编辑器:
.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)。 - 方案:
aria-label为「新建方案」「重置当前方案」「管理方案」;下拉项文案「方案设置/复制方案/导入方案/导出方案/删除方案」;导入 format 字符串classhelper-island-layout;导入 ≤1 MB;新建/复制重生成 id;删除保持至少 1 个方案(:264-360)。 - 设置页:
.layout-choice两张卡、编辑岛内组件按钮、指针避让开关、保存/预览/恢复默认按钮与实时预览(IslandSettingsView.vue:170-397)。 - 保存队列行为与状态文本(9.18-2);失败重试按钮;
应用并返回成功后才离开。 - 这些断言若因重构改变(例如组件设置页签从 3 变 4),必须在同一 PR 更新 parts-r/11 的冒烟脚本,禁止只改实现不改测试。
9.21 验收标准
实现完成后逐项验证并记录结果(全部通过才算完成;验证必须无头/离屏,禁止启动可见岛做验收,除非显式 --visual):
- family 与默认:新配置(清空 config)打开设置页,
family默认ClassIsland 风格且appearance.fontSize=16、标签「正文字号(ClassIsland 基准)」;老配置(人为删除island.family)重载后自动为经典 WinIsland(兼容)且外观与升级前一致(对比截图/配置 JSON),其原有fontSize(如 13)保持不变、标签「岛字号(经典)」。 - fixed 语义:
family=classisland、layout=fixed时,岛外观为信息行(行高 40、圆角 8、背景不透明度 0.5、底部 3px 进度条),默认显示日期 + 课程表;不出现胶囊/详情卡;来消息时在同一条内展开气泡;family=winisland时恢复胶囊 + 详情卡。layout=fixed时设置页不得出现任何行/组件编辑入口(fixed v2 为只读内置方案)。 - 外观参数:classisland 族 22 项、winisland 族 15 项逐一改动——拖动时岛即时变化(≤1 帧合并),点保存后
config.json.island落盘;重启客户端后保持;非法值(如缩放 9、圆角 -1、颜色red)被钳制/回落,不出现 NaN。字号语义:classisland 下改「正文字号(ClassIsland 基准)」时正文 = 该值、次级/强调/大号按 14/18/20 阶梯派生,默认 16;winisland 下标签为「岛字号(经典)」、默认 13;两种 family 间切换标签与默认值提示正确、不强制改写已有值。 - custom 与 family:
layout=custom时family控件禁用并显示「自定义方案始终使用 ClassIsland 规格;外观族只作用于经典固定形态。」;custom 的渲染始终为 ClassIsland 规格(不因family=winisland回退旧观感);设置页「圆角/不透明度/行间距」改动同时写入schemes[activeSchemeId]并落盘;「界面缩放」在 custom 下隐藏并有说明;schemes/rows/nodes编辑器只服务 custom,fixed 无编辑入口。 - 课程提醒:5 个开关 + 2 个阈值改动即时保存到
config.json.islandLayout.lessonReminders(唯一数据源);阈值越界被钳制到 5–1800 / 5–600;/settings/reminder只显示只读摘要 + 跳转链接,页面内无可编辑的上课提醒控件。 - 编辑器布局:编辑器页
padding 12/24/16;行最小高 46(标签态)/40(预览态);行距等于scheme.spacing;组件外层左右各 6px;选中框 1.5px 虚线1,1.5、opacity .75;行把手 20px;「新增行」在工具条与行列表末尾均可用。 - 四页签:组件设置抽屉依次为「内容/外观/尺寸/隐藏条件」;11 类组件的控件、默认值、范围与 9.15 表一致;切换组件时回到「内容」页。
- 拖拽:组件库→行、组件库→容器(20%–80% 区域)、行内排序、跨行移动、行排序均可用(鼠标与触摸各测一次);拖动阈值 6px;拖动父组件到自身后代被拒绝且数据不变;嵌套超过 6 层、组件超过 200、行超过 12、方案超过 20 均给出提示且不写入。
- 方案:新建(提示输入,1–64 字,最多 20 个)、复制(名称加「副本」、id 全量重生)、重置(保留 id/name)、删除(确认,至少保留 1 个)、导入(≤1 MB,format 校验,id 重生)、导出(文件名非法字符替换、可再导入)全部通过;
activeSchemeId始终有效。 - 保存与失败:连续快速拖动/输入时状态文本按「正在保存…」→「已自动保存」变化;人为让
saveConfigreject(mock IPC)时显示「保存失败」+ 错误条「重试」,且未点应用不允许离开;重试成功后错误清除。 - 编辑模式:进入后页面内渲染真实组件(无额外窗口/iframe),缩放 0.3–1.5 生效且「重置缩放」回 100%;预览不遮挡工具条、无横向滚动;退出后回到标签态;真实岛在保存后热更新(
island:layout生效)。 - 键盘/触摸:
Esc逐层退出(抽屉→编辑模式→返回);Delete删除选中组件(有子组件先确认);Ctrl+D复制;所有自绘按钮有:focus-visible焦点环;触摸拖动不触发页面滚动。 - 无回归: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 总则与不可协商的规则
- 动画级别只有一个开关:岛的动画由
appearance.animations(boolean)总控,不是 ClassIsland 的AnimationLevel(那是主窗口的设置)。animations=false时所有过渡必须「瞬落」:弹簧snap()、CSStransition/animation: 0ms、遮罩直接设终值(现有实现见IslandApp.vue:249-256、LessonReminderContent.vue:77-80)。 - 速度倍率:
appearance.speed(0.5–2)按比例缩放所有时长:实际时长 = 基准时长 / speed。现有实现已贯穿:弹簧dt × speed(IslandApp.vue:291)、窗口淡入IslandApp.vue之外的--island-dur = 320/max(.25,speed)(:528)、课程条--ci-duration = 200/speed等(LessonReminderContent.vue:134-137)。新增动画一律除以speed。 - 弹簧仅限 winisland:
family='classisland'的几何变化用 CSS transition;family='winisland'继续用SpringValue(stiffness 0.10 / damping 0.68 / 换向保留 35% / 单帧 ≤ 剩余 20% / snap 0.05px / 零过冲钳制,packages/desktop-client/src/island/spring.ts:26-114)。不得在 classisland 分支引入弹簧(Lead 裁决,parts-r/01-objective.md:52)。 - 窗口几何不动画:开合动画全部发生在固定包围盒内部(
main/island.ts:1412-1417)。classisland 的 fixed 信息行也必须沿用这一约束:行宽/气泡变化在渲染层完成,窗口只在「不可见时」或「模式/设置变化」时setBounds(island.ts:1476-1479,1598-1612)。fixed 单条气泡的行高恒为 40px,气泡(遮罩/内容/进度条)全部在 40px 行内完成,不因内容改行高、也不因此扩窗;maxCardHeight只用于 custom 的多条消息列表与强提醒窗口的尺寸计算。唯一例外是「多条消息列表」比普通形态更高,窗口必须在形变开始前先长高(island.ts:1514-1518),与单条气泡无关。 - 不可打断的动画可被新状态覆盖:新状态直接接管(取消旧 rAF/定时器、重设目标),不得排队、不得闪回。现有
syncTargets(snap)、animate()、animateMask()都按此实现(IslandApp.vue:238-264、CustomIslandRuntime.vue:127-177、LessonReminderContent.vue:73-102)。 - 隐藏页/窗口不可见必须暂停:custom 的
motion = animations && !document.hidden && nativeVisible(CustomIslandRuntime.vue:57-58);classic 通过props.suspended停 rAF(IslandApp.vue:240-246)。新增动画(遮罩、气泡、强提醒)同样要遵守:页面隐藏时不得继续跑 rAF;恢复可见时按当前状态直接落位(不要补播)。 - 减少动态效果(prefers-reduced-motion)不覆盖岛:ClassHelper 明确「应用内动画开关决定岛动画,不能用主窗口的减少动态效果说明替代岛自身规则」(
docs/app/island.md:57;docs/dev/motion.md:78)。岛动画的唯一开关是appearance.animations,appearance.speed统一缩放时长;实现者不得让系统 reduced-motion 强制关闭岛动画或改写其时长,只在设置页给出提示文案。若未来用户明确要求「尊重系统设置」,只能作为 P2 可选项(设置项respectReducedMotion,默认false),默认行为保持不变。 - 帧预算:岛渲染进程 60fps;任意单帧主线程 ≤ 8ms;动画只作用于
transform/opacity/clip-path/box-shadow(颜色类)与 SVGpoints(遮罩),禁止逐帧改width/height/top/left触发重排(弹簧形变改的是 SVG path 与元素尺寸,属于既有例外,仅 winisland 使用)。 - 兜底定时器:任何依赖 rAF 的动画必须有「到点直接落位」的兜底,因为 Windows 会把置顶透明小窗判定为被遮挡而把 rAF 压到 1fps(
IslandApp.vue:266-286的MORPH_FALLBACK_MS=1200)。classisland 的 CSS transition 天然有终点,但遮罩(SVG 分块)与任何 JS 驱动的动画仍需兜底(现有 600/speed,LessonReminderContent.vue:82-89)。 - 动画结束清场:动画结束必须:移除临时
will-change、取消 rAF/定时器、移除closing等临时类;不得留下持续运行的循环(12-acceptance.md:73)。
10.1 ClassHelper 现状动画清单(重构前基线,必须保留语义)
| 编号 | 现状动画 | 触发 | 当前实现 | 关键数值/证据 |
|---|---|---|---|---|
| S1 | classic 胶囊↔展开卡形变 | 消息展开/收起 | SpringValue 逐帧逼近 width/height/radius |
目标尺寸见 IslandApp.vue:188-204;圆角胶囊 min(radius,h/2)、展开 min(48×radius/20,w/2,h/2)(:207-212);弹簧参数 spring.ts:26-30 |
| S2 | 内容交叉淡入 | 形变过程中 | 胶囊层 1-1.5p、展开层 p² |
IslandApp.vue:346-351;progress 由高度差推导(:304-309) |
| S3 | 收回动画 | 点「知道了/标为已读」 | 形态先切回胶囊,360ms 后淡出隐藏 | CLOSE_ANIMATION_MS = 360(main/island.ts:114-121) |
| S4 | 窗口淡入/淡出 | 显示/隐藏 | 主进程逐帧改 setOpacity |
淡入每 16ms +0.16;淡出每 16ms −0.2,≤0.02 后 hide()(island.ts:1674-1715,island.ts:1686,1705) |
| S5 | custom 形变 | 展开/收起/课程条切换 | width/height/radius + content/lesson 进度弹簧 | CustomIslandRuntime.vue:88-177;snapThreshold:0.001(:92-93);兜底 1200/speed(:156) |
| S6 | custom 页面交叉淡入 | 常态↔展开↔课程 | compactAlpha=1-1.5p、detailsAlpha=p²、lessonProgress |
CustomIslandRuntime.vue:99-100;三页 opacity/visibility 绑定(:385-511) |
| S7 | custom 测量适配 | 内容/字体/视口变化 | ResizeObserver + nextTick 量 offsetWidth/offsetHeight + fitted() |
CustomIslandRuntime.vue:72-80,238-266 |
| S8 | 课程提醒遮罩 | 准备/上课/下课/下课前 | SVG 5 个多边形按进度重绘 | 区段 360ms、开错峰 [0,90,120,90,0]、关 [120,90,0,90,120]、总 480ms、开 1-(1-p)^4、关 p²(LessonReminderContent.vue:59-102) |
| S9 | 课程提醒标题 | 遮罩出现/消失 | CSS animation | 入 250ms 延迟 260ms (0.25,1,0.5,1);缩放 750ms 延迟 260ms;出 200ms linear 反向(:134-137,273-280) |
| S10 | 课程提醒进度条 | 每秒更新 | transform: scaleX transition |
50ms linear(LessonReminderContent.vue:307-316,高度 2px) |
| S11 | 组件行课程进度 | 每秒更新 | transform: scaleX transition |
1s linear(ComponentNode.vue:348-362,高度 2px) |
| S12 | 滚动组件 | 文字超宽 | CSS keyframes | rolling 30%→−80%,时长 max(1,200/speed) 秒,linear infinite;编辑时停(ComponentNode.vue:205,384-392,446-453) |
| S13 | 轮播组件 | 定时切换 | 计算当前索引,瞬时替换 | interval 秒(ComponentNode.vue:121-130;island-layout.ts:196 默认 5s) |
| S14 | 课程徽标 | 下课前 | 静态圆形 | 30×30、#0a84ff、12px/600、tabular-nums(lesson-ending.css:3-16) |
| S15 | 徽标入场 | 铃铛计数出现 | CSS animation | resident-indicator-in 200/speed ms fade(custom-runtime.css:147-149,199-206) |
| S16 | 课件覆盖 | 课件开始/结束 | 瞬时显示/隐藏,无动画 | CoursewareTransfer.vue(无 transition) |
| S17 | 悬停/按压 | 指针悬停、按下 | CSS transition | 按钮/列表行 140ms ease-out(classic-island.css:393,480,613-616,699-702);按下 scale(.97)(:724-727);仅 hover 设备(:736-750) |
| S18 | 编辑预览 | 打开编辑器 | 静态排版,无动画 | island-editor.css:193-227(预览行 44px 高) |
| S19 | 外观变化 | 设置页改宽高/字号/配色 | 重写 CSS 变量,瞬时生效 | IslandApp.vue:175-176,516-528;CustomIslandRuntime.vue:102-124 |
| S20 | 失焦宽限 | 展开后失焦 | 宽限期内不收起(非动画但影响观感) | 鼠标 500ms、触摸 1200ms(island.ts:2287,2297) |
10.2 映射总表(目标规格;每条给出双风格默认值)
说明:CS = family='classisland'(默认),WI = family='winisland'(兼容)。「实现」列 CSS-T 表示 CSS transition、CSS-A 表示 CSS keyframes/WAAPI、SVG-rAF 表示 SVG/Canvas 按进度重绘、Spring 表示 SpringValue 帧积分、主进程 表示 Electron 主进程逐帧/定时。
A. 岛出现 / 隐藏 / 开合
classisland fixed 下「岛」不再是胶囊,而是一条信息行:出现/隐藏必须轻、快、不抢注意力,避免每来一条消息就在屏幕顶部弹一个大卡片。开合类动画只允许改变行背景层与不透明度,内容层文字不得跟着宽度动画重排(否则会出现「文字先铺开再被省略号收回」的抖动,ClassHelper 的 classic-island.css:196-206 已记录同类问题)。winisland 分支的胶囊开合仍按原有弹簧语义执行,不得为了「统一」把弹簧搬到 classisland。
fixed 行高恒 40px(硬约束):气泡的遮罩、正文、进度条全部在 40px 行内完成;正文最多 2 行、超出用省略号(display:-webkit-box; -webkit-line-clamp:2; overflow:hidden),不得出现「行高随内容增高」「行高 200ms 过渡」「为气泡扩窗」这类 ClassHelper 旧展开卡行为。maxCardHeight 只参与 custom 的多条消息列表与强提醒窗口尺寸计算,单条气泡不读它。
| 编号 | 场景 | 触发 | 属性 | 起止值 | 时长 | 曲线 | 延迟/交错 | 实现 | 降级 | 现状对照 |
|---|---|---|---|---|---|---|---|---|---|---|
| A1 | CS fixed 岛出现 | 首次显示/新消息首次弹出 | opacity、translateY |
0 / 8px → 1 / 0 | 250ms | cubic-bezier(.25,1,.5,1) |
0 | CSS-T | animations=false 直接落位 |
无(新) |
| A2 | CS fixed 岛隐藏 | 消息清空且无课程 | opacity |
1 → 0 | 150ms | cubic-bezier(.16,1,.3,1) |
0 | CSS-T 后主进程 hide()(隐藏帧) |
无动画直接 hide | 对应 S4 |
| A3 | CS 行宽度变化 | 内容/字号/组件变化 | width(行背景层) |
当前 → 目标 | 300ms(Fluent)/550ms(Classic) | (.65,0,.35,1) / (0,1,.5,1.1) |
0 | CSS-T(背景层绝对定位,文字层瞬时) | 直接落位 | 对应 S19 |
| A4 | CS 单条气泡布局(行高固定) | 单条消息出现/内容变化 | 行高(不变)、正文行数 | 恒 40px;正文 0→2 行(超出省略),内容在行内完成遮罩/进度 | 0(不改行高) | — | — | 固定 --ci-row-h;-webkit-line-clamp:2;不扩窗 |
同(直接布局) | 对应 S3/S8;不得沿用旧展开卡增高行为 |
| A5 | CS 分体行出现 | 开启 separated |
行间距、圆角 | 0 → 5px、0 → 8px | 200ms | (.16,1,.3,1) |
每行 0 | CSS-T | 直接落位 | 无(新) |
| A6 | WI 胶囊↔展开 | 展开/收起 | width/height/radius | 胶囊 w×h×r(h/2) ↔ 展开 w+156 × h+186 × min(48r/20) |
由弹簧决定(约 300–500ms) | 弹簧(近临界阻尼,无过冲) | 0 | Spring | animations=false → snap() |
S1 |
| A7 | WI 内容交叉淡入 | 同 A6 | 两层 opacity |
胶囊 1−1.5p;展开 p² |
跟随 p | 线性映射 | 0 | JS 逐帧绑定 | 直接显示终态 | S2 |
| A8 | WI 收回并淡出 | 点知道了/标为已读 | 形态→胶囊,再窗口淡出 | 展开 → 胶囊;opacity→0 | 360ms 形态 + 淡出(−0.2/16ms) | 弹簧 + 线性 | 360ms 后淡出 | Spring + 主进程 | 立即 hide | S3/S4 |
| A9 | WI 窗口淡入 | 显示 | 窗口 opacity |
当前 → displayOpacity |
每 16ms +0.16(约 60–110ms) | 线性 | 0 | 主进程 | 立即设置 | S4 |
| A10 | WI 窗口淡出 | 隐藏 | 窗口 opacity |
当前 → 0 | 每 16ms −0.2(约 50–80ms) | 线性 | ≤0.02 后 hide() |
主进程 | 立即 hide | S4 |
| A11 | 上课时段隐藏 | 进入上课且无紧急/叫人 | 窗口可见性 | 可见 → 隐藏 | 0(立即) | — | — | 主进程 hideImmediately() |
同 | island.ts:1463-1473 |
B. 行内布局与组件
classisland 信息行的内部布局必须「稳」:数据每秒变化时只换文本、不动布局;只有组件增删、方案切换、拖拽排序时才允许位移类动画。行内所有组件共用 --ci-fs-* 与 --ci-row-h,任何组件的默认尺寸都不得把行撑高(超高时内部滚动或省略,见第 7 部分)。
| 编号 | 场景 | 触发 | 属性 | 起止值 | 时长 | 曲线 | 延迟/交错 | 实现 | 降级 | 现状对照 |
|---|---|---|---|---|---|---|---|---|---|---|
| B1 | CS 组件淡入 | 组件首次挂载/由隐藏转可见 | opacity |
0 → 1 | 300ms | (.65,0,.35,1) |
0 | CSS-T | 直接显示 | 对应 ClassIsland 组件淡入(repos/ClassIsland/.../ComponentPresenter.axaml.cs:224-243) |
| B2 | CS 首次多行错峰入场 | 岛首次显示且 ≥2 行 | opacity、translateY |
0 / 6px → 1 / 0 | 320ms | (.16,1,.3,1) |
每行 30ms,封顶 180ms | CSS-A(按行 index 计算 delay) | 跳过 | 对应 ClassIsland 列表错峰(25ms/项,StackPanelIntroAnimationBehavior.cs:74-90);本处取 30ms 更克制 |
| B3 | CS 组件重排过渡 | 编辑器中拖动排序 | transform |
旧位置 → 新位置 | 225ms | CubicEaseInOut≈(.65,0,.35,1)【推断】 |
0 | FLIP(先读后写) | 直接跳位 | 对应 WrapPanelResizingAnimationAssist.cs:45-49 |
| B4 | 数据文本刷新 | 时钟/日期/倒计时每秒 | 文本内容 | — | 0 | — | — | 直接替换(tabular-nums) |
同 | 无动画是有意的(ClassIsland 也不做数字翻转) |
| B5 | CS 消息气泡宽度 | 单条气泡换行/字数变化 | 宽度 | 当前 → 内容宽 | 200ms | (.16,1,.3,1) |
0 | CSS-T | 直接落位 | 无(新) |
C. 进度条
进度条是岛内唯一「高频更新」的动画元素:它必须用 transform: scaleX() 而非宽度,且每次更新只触发合成层。ClassIsland 的倒计时进度用 50ms 线性过渡(repos/ClassIsland/ClassIsland/XamlThemes/FluentTheme/Styles.axaml:137-145),classisland 风格沿用;winisland 的课程进度保持 1s 线性(每秒一 tick),两者视觉一致但实现成本不同。
| 编号 | 场景 | 触发 | 属性 | 起止值 | 时长 | 曲线 | 延迟 | 实现 | 降级 | 现状对照 |
|---|---|---|---|---|---|---|---|---|---|---|
| C1 | 提醒倒计时进度 | 剩余时间更新 | transform: scaleX |
旧比例 → 新比例 | 50ms | 线性 | 0 | CSS-T | 直接落位 | 对应 ClassIsland FluentTheme/Styles.axaml:137-145 |
| C2 | CS 行课程进度 | 每秒更新 | scaleX |
旧 → 新 | 50ms(每 50ms 更新一次时长)或 1s linear(每秒更新) | 线性 | 0 | CSS-T | 直接落位 | 现状 1s linear(S11),ClassIsland 50ms;默认选 50ms,若性能紧张可切 1s(见 §10.3 选择点) |
| C3 | WI 课程进度 | 每秒更新 | scaleX |
旧 → 新 | 1s | 线性 | 0 | CSS-T | 直接落位 | S11 保留 |
| C4 | 课件进度 | 传输事件 | 原生 <progress> |
旧 → 新 | 150ms | 线性 | 0 | CSS-T(progress 值即时,条宽过渡) |
直接落位 | S16 |
| C5 | 进度条高度 | 组件挂载 | height |
2px → 3px(CS) | 0(静态) | — | — | 静态 token --ci-progress-h |
同 | ClassIsland 3px;现状 2px |
D. 通知气泡两阶段 / 斜切遮罩(classisland 默认核心)
这是本轮重构最核心的动效。ClassIsland 的提醒是「先遮罩、后内容」的两阶段:斜切分块从两侧向中间扫入,文字延迟 260ms 再淡入并轻微缩放,最后背景投影浮现;退出时反向收拢并放大退出。ClassHelper 的 LessonReminderContent.vue 已经实现过一版(edges [0,.1,.3,.7,.9,1] 等价于权重 [0,.1,.2,.4,.2,.1]),本轮必须把它抽成公共组件并让消息气泡复用,不得再写第二份。
| 编号 | 场景 | 触发 | 属性 | 起止值 | 时长 | 曲线 | 延迟/交错 | 实现 | 降级 | 现状对照 |
|---|---|---|---|---|---|---|---|---|---|---|
| D1 | 遮罩扫入(开) | 气泡/课程提醒出现 | 5 个区段宽度进度 | 0 → 1 | 每段 360ms | QuarticEaseOut(1−(1−p)^4) |
区段错峰 [0,90,120,90,0],总 480ms |
SVG-rAF 或 CSS clip-path |
直接设 1 | S8(现有实现一致) |
| D2 | 遮罩收拢(关) | 气泡/提醒消失 | 5 个区段 | 1 → 0 | 每段 360ms | QuadraticEaseIn(p²) |
错峰 [120,90,0,90,120],总 480ms |
SVG-rAF | 直接设 0 | S8 |
| D3 | 遮罩形状 | 任意宽度 | 5 段权重、斜切角 | 权重 [0,.1,.2,.4,.2,.1];偏移 tan(30°)×40 |
— | — | — | SVG polygon / clip-path |
同 | S8(现有 edges [0,.1,.3,.7,.9,1] 等价) |
| D4 | 遮罩文字淡入 | 遮罩打开 | opacity |
0 → 1 | 250ms | (.25,1,.5,1) |
延迟 260ms | CSS-A both |
直接显示 | S9 |
| D5 | 遮罩文字缩放 | 遮罩打开 | scale |
1.1 → 1.0 | 750ms | (.25,1,.5,1) |
延迟 260ms | CSS-A | 直接落位 | S9 |
| D6 | 遮罩文字淡出 | 遮罩关闭 | opacity、scale |
1/1.0 → 0/1.1 | 200ms | 线性(透明度)/(.5,0,.75,0)(缩放) |
0 | CSS-A | 直接隐藏 | S9 |
| D7 | 遮罩投影淡入 | 遮罩打开 | box-shadow |
无 → 0 8px 32px rgba(0,0,0,.4) |
100ms | 线性 | 延迟 600ms | CSS-T | 直接到位 | ClassIsland FluentTheme:355-367 |
| D8 | 遮罩底色淡入 | 遮罩打开 | 背景 overlay opacity |
0 → 1 | 100ms | 线性 | 延迟 600ms | CSS-A | 直接到位 | FluentTheme:355-367 |
| D9 | 覆盖层淡入 | 遮罩打开后正文/详情出现 | opacity |
0 → 1 | 200ms | SineEaseIn≈(.12,0,.39,0)【推断】 |
0 | CSS-T | 直接显示 | ClassIsland FluentTheme:419-451 |
| D10 | 覆盖层淡出 | 正文结束/关闭 | opacity |
1 → 0 | 200ms | SineEaseIn | 0 | CSS-T | 直接隐藏 | 同上 |
| D11 | Classic 遮罩(主题=classic) | 同 D1/D2 | translateY |
−50 → 0(入)/0 → 50(出) | 200ms | QuadraticEaseIn/Out | 出延迟 100ms | CSS-A | 直接落位 | ClassIsland ClassicTheme/Styles.axaml:265-331 |
| D12 | Classic 文字淡入/淡出 | 同上 | opacity |
0→1(100ms 延迟 200ms)/1→0(100ms) | 100ms | 线性 | 见说明 | CSS-A | 直接落位 | 同上 |
| D13 | 多行气泡错峰 | 列表 ≥2 条 | opacity、translateY |
0 / 6px → 1 / 0 | 200ms | (.16,1,.3,1) |
每行 30ms,封顶 120ms | CSS-A | 直接显示 | 无(新增,参考 ClassHelper 主窗口列表错峰) |
| D14 | 气泡内进度条 | 倒计时更新 | scaleX |
旧 → 新 | 50ms | 线性 | 0 | CSS-T | 直接落位 | 对应 C1 |
| D15 | 气泡图标/徽标入场 | 气泡出现 | scale、opacity |
.9/0 → 1/1 | 150ms | (.16,1,.3,1) |
0 | CSS-A | 直接显示 | 无(新增) |
| D16 | 关闭后进入下一批 | 队列非空 | 遮罩重播 | 同 D1/D2 | 同 | 同 | 先 D2 再 D1(交叉 100ms) | SVG-rAF | 直接切 | ClassHelper 队列现为「直接换内容」,需补过渡 |
| D17 | 关闭并淡出 | 队列清空 | 遮罩 + 窗口 | 遮罩收拢 + opacity→0 |
480 + 150ms | 同 | 遮罩结束后淡出 | SVG-rAF + CSS-T | 直接 hide | 对应 S3(WI 用 360ms) |
E. 强提醒特效(新增)
强提醒是新增的独立透明窗口(不抢焦点、不阻塞、超时自关),其动画只有「涟漪扩散」一条,其余都是窗口显隐。特效必须在岛所在显示器上铺到足够大的区域,尺寸按 ClassIsland 规则随屏高缩放,且编辑/预览模式下绝不创建。
| 编号 | 场景 | 触发 | 属性 | 起止值 | 时长 | 曲线 | 延迟 | 实现 | 降级 | 现状对照 |
|---|---|---|---|---|---|---|---|---|---|---|
| E1 | 特效窗口出现 | 强调提醒 | 窗口可见性 | 隐藏 → 显示 | 0 | — | 0 | 主进程 showInactive() |
同 | ClassIsland TopmostEffectWindow |
| E2 | 涟漪扩散 | 特效开始 | scale |
0 → 1 | 600ms | QuadraticEaseIn | 0 | WAAPI(独立窗口) | 直接显示静态 | ClassIsland RippleEffect.axaml.cs:92-96 |
| E3 | 涟漪淡出 | 同上 | opacity |
1 → 0 | 600ms | SineEaseIn | 0 | WAAPI | 直接移除 | RippleEffect.axaml.cs:102-106 |
| E4 | 特效收尾 | 750ms 后 | 移除元素/隐藏窗口 | — | — | — | 750ms 总时长 | 主进程 hide() |
立即隐藏 | RippleEffect.axaml.cs:108-109 |
| E5 | 特效渲染缩放 | 屏高 ≥1400/≥2000 | 窗口尺寸 | 1× → .75× /.5× | 0 | — | — | 主进程按屏算 | 固定 1× | repos/ClassIsland/.../MainWindow.axaml.cs:377-392 |
F. 课程徽标 / 课程提醒
课程提醒的视觉规格已经在 LessonReminderBar 中实现过半(倒计时/课程名/时间/三段信息/遮罩/进度条)。本轮需要补齐的是「入场/退场」「徽标出现」「徽标颜色随剩余时间变化」三处,并确保徽标数字 -1 的边界行为(剩余 1 秒显示 -1,不显示「即将下课」遮罩,docs/app/island.md:68-70)。
| 编号 | 场景 | 触发 | 属性 | 起止值 | 时长 | 曲线 | 延迟 | 实现 | 降级 | 现状对照 |
|---|---|---|---|---|---|---|---|---|---|---|
| F1 | 课程提醒条入场 | 提醒开始(CS fixed) | opacity、translateY |
0 / 8px → 1 / 0 | 200ms | (.16,1,.3,1) |
0 | CSS-T | 直接显示 | 无入场动画(新增) |
| F2 | 课程提醒条退场 | 提醒结束 | opacity |
1 → 0 | 150ms | (.16,1,.3,1) |
0 | CSS-T | 直接隐藏 | 无(新增) |
| F3 | 课程遮罩 | 提醒开始(mask 阶段) | 同 D1/D2 | 同 | 360/120 | 同 | 同 | 复用遮罩组件 | 直接落位 | S8 |
| F4 | 下课前徽标出现 | ending 且开启倒数 |
scale、opacity |
.8/0 → 1/1 | 150ms | (.16,1,.3,1) |
0 | CSS-A | 直接显示 | S14(静态) |
| F5 | 徽标数字更新 | 每秒 | 文本 | -N → -(N-1),最后 -1 |
0 | — | — | 直接替换(tabular-nums) | 同 | docs/app/island.md:68-70 |
| F6 | 徽标颜色 | 剩余 ≤10s | 颜色 | --ci-accent → --ci-state-urgent【决策】 |
150ms | 线性 | 0 | CSS-T | 直接换色 | 无(新增,可选) |
| F7 | 课程进度条 | 每秒 | scaleX |
旧 → 新 | 50ms(CS)/1s(WI) | 线性 | 0 | CSS-T | 直接落位 | S10/S11 |
| F8 | 课程条接管岛(custom) | 课程提醒出现 | 页面交叉 | 常态 → 课程条 | CS 200ms / WI 弹簧 | (.16,1,.3,1) / 弹簧 |
0 | CSS-T / Spring | 直接切 | S5/S6 |
G. 组件滚动 / 轮播
滚动与轮播是 custom 方案里唯一的「持续动画」。它们必须遵守两条纪律:① 页面隐藏/窗口不可见时暂停;② 编辑模式下暂停并展开全部子组件,便于选择与排序。classisland 风格的轮播切换按 ClassIsland 的 250ms 上下滑动,winisland 保留瞬时切换。
| 编号 | 场景 | 触发 | 属性 | 起止值 | 时长 | 曲线 | 延迟 | 实现 | 降级 | 现状对照 |
|---|---|---|---|---|---|---|---|---|---|---|
| G1 | 滚动组件(默认连续) | 内容超宽 | translateX |
30% → −80% |
max(1,200/speed) 秒 |
线性无限循环 | 0 | CSS-A | 静止并省略号 | S12 保留 |
| G2 | 滚动组件(ClassIsland 暂停式,可选) | 设置 mode='pause' |
translateX |
0 → −(innerW) 停顿 → 回 0 |
innerW/40 + pause(默认10s) + outerW/40 秒 |
线性无限 | 无 | WAAPI 关键帧 | 回落 G1 | 对应 ClassIsland RollingComponent.axaml.cs:85-135(速度 40px/s、暂停 10s) |
| G3 | 轮播切换(CS) | 定时 | translateY、opacity |
旧:−40→0 / 0→1;新:0→40 / 1→0 | 250ms | (.65,0,.35,1) |
0 | CSS-A | 瞬时替换 | ClassIsland SlideComponent.axaml:69-107;现状 S13 无动画 |
| G4 | 轮播切换(WI) | 定时 | 瞬时替换 | — | 0 | — | — | 直接换 | 同 | S13 保留 |
| G5 | 编辑模式下滚动/轮播 | 进入编辑器 | 暂停/展开全部 | 动画 → 停止 | 0 | — | — | 移除 animation / 显示全部子节点 | 同 | ComponentNode.vue:388-392 |
| G6 | 隐藏页暂停 | document.hidden / 窗口不可见 |
暂停所有组件动画 | 运行 → 暂停 | 0 | — | — | motion 计算 / CSS animation-play-state |
同 | CustomIslandRuntime.vue:57-58 |
H. 主题 / 风格切换
风格切换(classisland↔winisland)是「整岛换一套几何与实现」,必须避免「半新半旧」的中间帧:切换时先做 200ms 交叉淡化,再在同一帧替换 data-family 与变量,最后淡入。主题切换(Fluent↔Classic)与深浅色只改颜色 token,几何与动画参数不动——这是 ClassHelper 与 ClassIsland 共同的纪律(docs/dev/motion.md:125-130)。
| 编号 | 场景 | 触发 | 属性 | 起止值 | 时长 | 曲线 | 延迟 | 实现 | 降级 | 现状对照 |
|---|---|---|---|---|---|---|---|---|---|---|
| H1 | family 切换 | 设置页切换 classisland↔winisland | 整岛 opacity + 变量 |
1→0.6→1(快速交叉) | 200ms 出 + 200ms 入 | (.16,1,.3,1) |
0 | CSS-T(切 data-family 前后加过渡类) |
直接切 | S19(现为瞬时) |
| H2 | theme 切换(Fluent↔Classic) | 设置页切换 | 背景/描边/阴影颜色 | 旧 → 新 | 200ms | 线性 | 0 | CSS-T(仅颜色 token) | 直接切 | 对应 ClassIsland 窗口 BrushTransition 200ms(MainWindow.axaml:40-44) |
| H3 | 深浅色切换 | 应用主题变化 | 颜色 token | 旧 → 新 | 200ms | 线性 | 0 | CSS-T | 直接切 | 同 H2 |
| H4 | 强调色变化 | 设置页改 accent | --island-accent 派生色 |
旧 → 新 | 150ms | 线性 | 0 | CSS-T(颜色) | 直接切 | S19 |
I. 消息列表 / 徽标 / 交互
交互类动画是「手感」而非「表演」:悬停 140ms、按压 90ms、徽标 200ms,全部短于 250ms,且只作用于可交互元素。所有 hover 效果必须包在 @media (hover:hover) and (pointer:fine) 内,避免触摸屏幽灵 hover(classic-island.css:729-750)。
| 编号 | 场景 | 触发 | 属性 | 起止值 | 时长 | 曲线 | 延迟 | 实现 | 降级 | 现状对照 |
|---|---|---|---|---|---|---|---|---|---|---|
| I1 | 铃铛徽标入场 | 未读数 0→N | opacity |
0 → 1 | 200ms/speed | (.16,1,.3,1) |
0 | CSS-A | 直接显示 | S15 保留 |
| I2 | 列表行悬停 | 指针移入(仅 hover 设备) | background |
旧 → --ci-fill-2 |
140ms | (.16,1,.3,1) |
0 | CSS-T | 无 hover | S17 |
| I3 | 按钮按压 | 按下/松开 | scale |
1 → .97 → 1 | 90ms 按下 / 抬起 200ms | (.16,1,.3,1) |
0 | CSS-T | 直接 | S17 |
| I4 | 图标按钮悬停 | 指针移入 | 背景/前景 | 旧 → 新 | 140ms | (.16,1,.3,1) |
0 | CSS-T | 无 | S17 |
| I5 | 选中/焦点环 | Tab 键聚焦 | outline |
无 → 2px accent | 0 | — | — | CSS 静态 | 同 | 无(新增 a11y) |
| I6 | 多条消息列表高度变化 | 展开更多/新消息(仅 custom 多条列表,非单条气泡) | 列表卡片高度 | 旧 → 新 | CS 200ms / WI 弹簧 | (.16,1,.3,1) / 弹簧 |
0 | CSS-T/Spring + 主进程先扩窗 | 直接落位 | 现有 islandListLayout(度量);单条气泡不适用,行高恒 40 |
J. 编辑预览 / 拖拽
编辑器在主窗口里运行,动画要克制:进入/退出编辑模式 200ms 交叉淡化即可;拖拽必须跟手(0ms 跟随),只对「落位排序」做 150ms FLIP。预览区不创建额外桌面窗口(01-objective.md:35、docs/app/island.md:47)。
| 编号 | 场景 | 触发 | 属性 | 起止值 | 时长 | 曲线 | 延迟 | 实现 | 降级 | 现状对照 |
|---|---|---|---|---|---|---|---|---|---|---|
| J1 | 进入/退出编辑模式 | 点「进入编辑模式」 | 整页 opacity |
0 ↔ 1 | 200ms | (.16,1,.3,1) |
0 | CSS-T | 直接切 | 无(新增) |
| J2 | 拖拽幽灵 | 拖动组件 | 跟随指针 | — | 0 | — | — | 直接定位(island-editor.css:331-334) |
同 | S18 |
| J3 | 拖拽排序 | 放下组件 | transform |
旧位 → 新位 | 150ms | (.16,1,.3,1) |
0 | FLIP | 直接跳 | 无(新增) |
| J4 | 放置高亮 | 拖到行上 | outline/背景 |
无 → 2px accent 虚线 | 0/100ms | 线性 | 0 | CSS 静态 | 同 | island-editor.css:228-232 |
| J5 | 预览缩放滑杆 | 拖动 | --resident-scale |
连续 | 0(实时跟随) | — | — | 直接赋值 | 同 | island-editor.css:193-217 |
| J6 | 保存状态 | 保存中/失败 | 文本颜色 | 灰 ↔ 红 | 0 | — | — | 直接切 | 同 | island-editor.css:27-34 |
K. 课件覆盖
| 编号 | 场景 | 触发 | 属性 | 起止值 | 时长 | 曲线 | 延迟 | 实现 | 降级 | 现状对照 |
|---|---|---|---|---|---|---|---|---|---|---|
| K1 | 课件卡入场 | 传输开始 | opacity、translateY |
0 / 8px → 1 / 0 | 200ms | (.16,1,.3,1) |
0 | CSS-T | 直接显示 | S16(无动画) |
| K2 | 课件进度 | 进度事件 | 进度条宽度 | 旧 → 新 | 150ms | 线性 | 0 | CSS-T | 直接跳 | S16 |
| K3 | 课件卡退场 | 完成/失败后 3s | opacity |
1 → 0 | 150ms | (.16,1,.3,1) |
3s 停留 | CSS-T 后主进程恢复岛 | 直接隐藏 | S16 |
L. 暂停 / 降级 / 兜底
| 编号 | 规则 | 触发 | 行为 | 证据 |
|---|---|---|---|---|
| L1 | 动画总开关 | animations=false |
弹簧 snap、CSS 过渡 0ms、遮罩设终值、rAF 不启动 |
IslandApp.vue:249-253、CustomIslandRuntime.vue:148-151、LessonReminderContent.vue:77-80 |
| L2 | 速度倍率 | speed≠1 |
所有时长 /speed;弹簧 dt×speed;兜底定时器 /speed |
IslandApp.vue:291,528、CustomIslandRuntime.vue:156,220-222、LessonReminderContent.vue:134-137 |
| L3 | 页面隐藏 | document.hidden |
暂停弹簧/rAF;恢复时按当前状态落位 | CustomIslandRuntime.vue:57-58,269-273 |
| L4 | 窗口不可见 | 主进程 hide() |
渲染层收到 nativeVisible=false,暂停 |
CustomIslandRuntime.vue:58;preload island:visibility(main/island.ts:528-533) |
| L5 | 系统减少动态效果 | prefers-reduced-motion |
不覆盖岛;岛动画唯一开关是 appearance.animations,speed 统一缩放;仅编辑器/设置 UI 遵循系统设置 |
docs/app/island.md:57、docs/dev/motion.md:78 |
| L5b | 尊重系统设置(P2,可选) | 用户显式开启 respectReducedMotion |
系统 reduced-motion 时按 animations=false 落位;默认关闭 |
本行为可选实现,默认不得启用 |
| L6 | rAF 被限流 | Windows 判定遮挡 | 兜底定时器到点直接落位(形变 1200/speed、遮罩 600/speed、命中 600/speed) | IslandApp.vue:236,266-286、LessonReminderContent.vue:82-89、CustomIslandRuntime.vue:220-222 |
| L7 | 触摸窗口缩放 | 触摸模式形态变化 | 变大立即、变小等隐藏后收(不做动画) | main/island.ts:1555-1578 |
| L8 | 失焦宽限 | 展开后 blur | 500ms(鼠标)/1200ms(触摸)内不收起,避免焦点抖动导致误收起 | main/island.ts:2287,2297,557-578 |
10.2.1 消息气泡完整时间线(按毫秒对齐,验收时逐点核对)
以一条普通通知在 family='classisland'、speed=1、animations=true、theme='fluent' 下为例,完整时间线必须严格如下(同一条消息的遮罩、文字、投影、进度条、窗口状态都按此对齐):
| 时刻 | 事件 | 实现要点 |
|---|---|---|
| t=0 | 主进程把消息压入队列并把 mode 切为 expanded;窗口使用预设包围盒(宽 = min(工作区宽,960)、高 = 40+2×pad),不因气泡扩窗 |
窗口 setBounds 不得在可见时发生(main/island.ts:1476-1479) |
| t=0 | 岛出现(若原本隐藏):opacity 0→1 / translateY 8→0,250ms |
A1;同时开始窗口淡入(winisland 为 +0.16/16ms) |
| t=0 | 5 个斜切区段开始扫入:区 0/4 立即、区 1/3 延迟 90ms、区 2 延迟 120ms,每段 360ms QuarticEaseOut | D1;总 480ms |
| t=0 | 气泡图标/徽标入场 150ms(scale .9→1) | D15 |
| t=260 | 遮罩文字开始淡入 250ms + 缩放 1.1→1.0 750ms | D4/D5;两个 CSS animation 必须同时挂、各自 both |
| t=480 | 遮罩完全覆盖行(5 区到位) | D1 结束 |
| t=600 | 背景投影/遮罩底色开始淡入 100ms | D7/D8 |
| t=700 | 投影/底色到位;如为「有正文」的提醒,覆盖层正文开始 200ms 淡入 | D9 |
| 正文超长时 | 行高恒 40px;正文最多 2 行、超出省略号 | 不扩窗、不改行高;maxCardHeight 不参与单条气泡 |
| 展示期 | 倒计时进度每 50ms 更新一次、每次 50ms 线性过渡 | D14 |
| 收尾 | 用户点「知道了/标为已读」或超时:覆盖层 200ms 淡出 → 遮罩反向收拢(错峰 [120,90,0,90,120],每段 360ms)→ 窗口 150ms 淡出并隐藏 |
D2/D10/D17 |
| 队列非空 | 上一批收拢到 50% 时启动下一批扫入,交叉 100ms,不得出现空白帧 | D16 |
对应 winisland 路径:t=0 时改为胶囊→展开卡弹簧形变(目标宽高由 ISLAND_SIZE_DELTA 决定),文字交叉淡入按 1−1.5p / p²,点「知道了」后 360ms 收回再淡出(A6–A8)。两条路径共享同一消息状态机与队列规则,只有「表现层」不同。
10.3 冲突与选择点(必须按默认执行,并保留开关)
| 编号 | 冲突点 | ClassHelper 现状 | ClassIsland 目标 | 默认选择 | 开关/回退 |
|---|---|---|---|---|---|
| X1 | fixed 形态 | 268×44 胶囊 + 详情卡 | 40px 信息行 + 行内气泡 | classisland:信息行;winisland:胶囊 | appearance.family |
| X2 | 开合实现 | 弹簧形变 | CSS transition(行宽/行高) | classisland 用 CSS;winisland 用弹簧 | family |
| X3 | 单条气泡的尺寸 | 卡片长高(最大 ~220) | 40px 行内气泡,正文 2 行省略 | 行高恒 40、不增高、不扩窗;maxCardHeight 只用于 custom 多条列表与强提醒窗口 |
family;无「增高」回退项(WinIsland 原展开卡保留在 winisland fixed) |
| X4 | 遮罩形态 | 无(直接换卡片) | 斜切 5 区分块 + 文字缩放 | 两风格都实现遮罩(winisland 的详情卡也用遮罩增强观感,但保留卡片尺寸动画) | family 决定是否叠加卡片尺寸动画 |
| X5 | 滚动组件 | 连续 30%→−80% 跑马灯 | 40px/s + 10s 暂停 + 回程 | 默认连续(ClassHelper 观感),设置项可切 mode='pause' |
组件设置 settings.speed 已有;新增 settings.mode【推断字段名】 |
| X6 | 轮播切换 | 瞬时 | 250ms 上下滑 + 淡入淡出 | classisland 250ms;winisland 瞬时 | family |
| X7 | 课程进度条 | 1s linear | 50ms linear | classisland 50ms;winisland 1s | family |
| X8 | 进度条高度 | 2px | 3px | classisland 3px;winisland 2px | family |
| X9 | 系统减少动态效果 | 岛不受其影响 | 同 ClassHelper(岛自身开关) | 不覆盖岛:岛动画唯一开关 appearance.animations,speed 缩放时长;如用户显式要求,可提供 P2 选项 respectReducedMotion(默认 false) |
animations / P2 选项 |
| X10 | 消息列表错峰 | 无 | ClassIsland 无(列表瞬时) | 新增 30ms 错峰但封顶 120ms,可在 animations=false 时关闭 |
animations |
| X11 | 主题切换 | 瞬时 | 200ms 颜色过渡 | 200ms(classisland);winisland 瞬时 | family |
10.4 性能预算与降级矩阵
| 场景 | 目标 | 降级顺序 | 说明 |
|---|---|---|---|
| 弹簧形变(winisland) | 60fps、单帧 ≤8ms | ① 降步进到 30fps ② 只动尺寸不动圆角 ③ snap() |
必须在岛渲染进程 rAF,独立于主进程 |
| CSS 行宽过渡(classisland;行高恒 40 不参与过渡) | 60fps | ① 去掉阴影过渡 ② 直接落位 | 只过渡背景层,文字层瞬时;避免触发整行重排 |
| 斜切遮罩(SVG 5 区) | 60fps、单帧 ≤4ms | ① 降为 3 区 ② 降到 30fps ③ 静态遮罩 | 每帧重算 points,避免重绘整棵 SVG |
| 消息气泡(40px 行内遮罩 + 2 行正文) | ≥50fps | ① 关闭错峰 ② 关闭文字缩放 ③ 直接切 | 与 11 部分性能基线一致(parts-r/11-compat-tests.md:46);气泡不改变行高 |
| 强提醒特效 | ≥50fps | ① 关闭涟漪改静态色块 ② 缩小渲染缩放 ③ 不显示 | 特效窗口独立进程/独立窗口,动画结束即关 |
| 多条列表高度变化(custom) | 60fps | ① 关闭过渡直接跳 ② 限制可见行数 | 窗口先扩再形变,避免裁切;单条气泡不适用 |
| 主窗口隐藏 | 0 动画 | — | 岛仍可见时不得因主窗口隐藏而停;只有岛窗口隐藏/页面隐藏才暂停 |
10.5 实现方式规范(逐条遵守)
- classisland 优先 CSS:几何用
transition(width/height/border-radius/opacity/box-shadow/background-color),分块遮罩用 JS 进度 + SVGpolygon(现有实现LessonReminderContent.vue:59-102可直接复用/抽组件),文字用@keyframes(var(--ci-*)时长/曲线)。 - winisland 保留弹簧:
SpringValue参数与兜底不得改(spring.ts:26-114);syncTargets的snap分支必须保留。 - 遮罩复用:把
LessonReminderContent.vue里的 5 区遮罩抽成IslandMask.vue(props:open、duration、stagger、fill、motion、speed),消息气泡与课程提醒共用;禁止复制第二份实现。 - 进度条统一样式:抽
.ci-progress类(高度var(--ci-progress-h)、轨道var(--ci-progress-track)、填充var(--ci-progress-fill)、圆角var(--ci-progress-radius)),用transform: scaleX()驱动;不要用width。 - FLIP 只用于重排:编辑器拖拽排序、组件位置变化用 FLIP(先批量读、再写 transform、下一帧过渡),不得对每帧读布局。
- 强制落位:所有
transition在data-family='classisland'下用 CSS;animations=false时通过根元素data-anim='0'把transition-duration/animation-duration覆盖为0ms(参照--island-anim的做法,IslandApp.vue:527),并让 JS 跳过 rAF。 - 不要动画窗口 bounds:仅 M0 新建窗口、模式切换、显示器/边距变化时
setBounds,且节流 ≥100ms(parts-r/01-objective.md:81)。 - 可取消:每个动画入口先取消同目标旧动画(
cancelAnimationFrame+ 清 timer + 重置变量),再启动新动画;状态机以「目标值」而不是「播放进度」为准。 - 无障碍:
aria-live="polite"保留在课程/消息条(LessonReminderContent.vue:140);动画不改变语义顺序;prefers-reduced-motion下编辑器 UI 动画压到 1ms(主窗口规则,index.css:909-917)。 - 测试钩子:在 dev 构建暴露
window.__islandMotion = { list(), state(), setSpeed(x), disable() },供无头验证读取(--visual才做画面验证)。
10.6 逐文件改动清单
| 文件 | 动作 |
|---|---|
packages/desktop-client/src/island/spring.ts |
KEEP;仅允许为 classisland 增加 snapTo() 便捷方法(可选),不得改默认参数 |
packages/desktop-client/src/island/squircle.ts |
KEEP(仅 winisland/custom 使用);classisland 不调用 |
src/island/IslandApp.vue |
大改:family 分支;classisland fixed 改为信息行渲染;保留消息状态机与 winisland 胶囊路径;接 animations/speed 与暂停 |
src/island/CustomIslandRuntime.vue |
改:classisland 下行宽用 CSS 过渡、行高恒 40(不随内容增高);保留测量与 winisland 弹簧分支;motion 规则扩展 |
src/island/ClassicNotifications.vue |
改:气泡两阶段(斜切遮罩 + 文字 + 缩放);多行错峰;保留消息类型/排序/回执 |
src/island/LessonReminderContent.vue |
改:遮罩抽成共用组件;进度 2→3px;标题/缩小时长走 token;变量改名 --ci-lesson-*(保留 alias) |
src/island/ComponentNode.vue |
改:滚动/轮播动画按 family;课程进度 50ms/1s;--secondary/--emphasized/--large 改用 --ci-fs-* |
src/island/CoursewareTransfer.vue |
改:K1–K3 入场/退场/进度过渡;其余 KEEP |
src/island/classic-island.css |
改:classisland 分支 token 化;winisland 分支保持;悬停/按压过渡统一 token |
src/island/custom-island.css / custom-runtime.css |
改:行高/间距/进度条/圆角 token 化;classisland 背景/描边/阴影;预览行 44→40 |
src/island/lesson-ending.css |
改:徽标颜色 token 化;新增出现动画类(可选) |
src/island/tokens.css |
新增:--ci-* + --ci-dur-*/--ci-ease-* |
src/main/island.ts |
改:下发 family/theme/colorScheme;classisland 包围盒计算;关闭/淡出时序不变;特效窗口控制 |
src/renderer/styles/island-editor.css |
改:J1/J3/J4 动画;预览 40px;token 化 |
docs/dev/island-refactor.md |
新增:动画表、降级矩阵、测试钩子说明 |
10.7 验收标准
- 逐条覆盖:本节 A1–A11、B1–B5、C1–C5、D1–D17、E1–E5、F1–F8、G1–G6、H1–H4、I1–I6、J1–J6、K1–K3、L1–L8 全部实现;抽测 12 条(覆盖两种 family),时长误差 ≤16ms、曲线一致。
- 双风格默认值:
family='classisland'下无弹簧形变、无 squircle;行宽过渡 300/550ms;气泡遮罩 360+120、文字 260/250、缩放 750、退出 200;family='winisland'下胶囊开合仍为弹簧、收缩 360ms 后淡出、窗口淡入 +0.16/16ms、淡出 −0.2/16ms。 - 总开关:
animations=false时所有动画在 1 帧内完成,不出现「终态先行」;rAF/定时器数量归零(DevTools Performance 验证)。 - 速度:
speed=2时所有时长减半、弹簧更快;speed=0.5时加倍;不得出现某条动画没乘 speed 的不一致。 - 暂停:主窗口最小化/岛窗口
hide()/document.hidden时无 rAF、无 CSS 动画推进;恢复可见后直接呈现当前状态(不补播)。 - 可打断:快速连续展开/收起、连续推送 5 条消息、消息中途标记已读 → 无闪回、无残留、无「停在半路」;队列切换时旧遮罩被取消。
- 视觉一致:遮罩 5 区权重与 60° 斜切与 ClassIsland 一致;文字先于底色结束(260ms 延迟)不出现「白字压白底」;Classic 主题下遮罩整块下压/上滑(200ms)。
- 进度条:classisland 3px、50ms 线性;winisland 2px、1s 线性;进度条用
transform而非width(Performance 面板无 layout)。 - 无窗口抖动:开合/气泡/列表过程中窗口
setBounds调用次数为 0(隐藏帧除外);触摸模式下窗口只增不减。 - 性能:主界面过渡 ≥55fps、气泡/强提醒 ≥50fps;单帧主线程 ≤8ms;动画结束无残留
will-change/rAF/timer(parts-r/11-compat-tests.md:46、12-acceptance.md:73)。 - 无头验证:
verify:desktop默认不创建可见岛即可断言动画状态机与时长(通过测试钩子读取);只有--visual才做逐帧/截图验收。 - 文档:
docs/dev/island-refactor.md的动画表与本节一致;docs/app/island.md补充 family 与动画开关说明。 - fixed 气泡不增高:推送超长正文消息,行高恒 40px、正文最多 2 行省略、窗口 bounds 不变;全过程无
height/max-height过渡,maxCardHeight未被单条气泡读取。 - reduce-motion 口径:系统开启
prefers-reduced-motion后,岛动画仍按appearance.animations/speed播放(默认不覆盖);只有 P2 选项respectReducedMotion=true时才按animations=false落位,且该选项默认关闭。
11. 兼容性、性能、平台与测试基线
本部分规定「重构不能把原来能用的东西弄坏」,以及如何在不打扰用户的前提下验证。所有验证默认必须无头/离屏执行。
11.1 兼容矩阵(必须逐项通过)
| 维度 | 必须保持兼容的内容 | 验证方式 |
|---|---|---|
| 配置 | 旧 config.json.islandLayout(fixed/custom、residentEnabled、schemes/rows/nodes/expandedRows/appearance/lessonReminders/pointerFade*)无损升级 |
迁移单测 + fixtures 回放 |
| 布局模式 | fixed 与 custom 均可切换、可保存、可重启恢复;两布局共用一个原生岛窗口,切布局不新开窗口 |
无头集成测试 + 主进程单测 |
| 风格 | family 只影响 fixed:winisland 保持旧胶囊观感,classisland 为默认信息行;custom 始终 ClassIsland 规格;切换即时生效、可回退 |
截图(仅 --visual)+ 视觉回归 |
| 消息 | 通知/作业/叫人的类型、优先级、id 去重、排序、上课暂存、紧急展开、叫人可点胶囊、已读回执、安静模式、教师/班级端差异不变 | 状态机单测 + 无头消息场景 |
| 课程 | 课表解析结果(ScheduleDto/IslandTimePointDto)、准备/上课/课间/下课前/课中进度提醒、下课前秒数徽标、当前/下一节/今日时间点显示;行为变更:custom 不再临时接管岛显示课程提醒(仅保留 ending 徽标,进度由课程组件显示),fixed 按 ClassIsland 接管 |
时间状态机单测 + 假时钟无头场景 |
| 天气 | Open-Meteo 数据、15 分钟刷新、断网缓存标记"缓存"、无官方预警提示 | 无头 mock 网络 + 快照 |
| 课件 | 课件传输状态优先覆盖岛内容、SHA256 校验、回执、不自动运行文件 | 无头课件场景 |
| 编辑器 | 方案/行/组件/导入导出/预览/重置/新建/复制/删除、12 行/200 节点/6 层/20 方案边界 | 单测 + 组件测试 |
| 窗口/输入 | 透明、置顶、不占任务栏、点击穿透、触摸模式、失焦宽限、固定几何护栏、单实例 | 无头窗口参数断言 + 手工触摸说明 |
| 平台 | Windows 10/11(主目标)、macOS、Linux X11;DPI 100/125/150/200;多显示器与热插拔 | 平台矩阵 + 无头几何断言 |
| 性能 | 启动、空闲 CPU、内存、消息风暴、动画帧率 | 性能脚本 + 采样 |
11.2 配置迁移与回滚(硬要求)
- 版本化:
IslandLayoutSettings.version从1(旧配置缺失视为 1)升级到2;迁移函数必须幂等(重复执行结果一致)。 - 迁移顺序:读取原始 JSON → 备份到
config.json.bak.<时间戳>(或配置目录backups/)→ 应用迁移(只补齐/转换,不删除用户数据)→normalizeIslandLayouts()校验与夹取 → 原子写(临时文件 + rename)。 - 必须迁移的字段:
- 顶层新增
version、appearance.family(老用户默认'winisland',新安装'classisland')、appearance.theme('fluent'/'classic')、strongNotification开关与参数、progressBar参数、rowStyle(行背景/分隔/圆角覆盖); - scheme 新增
rowHeight(默认 40)、componentGap(默认 12,对应单侧 6)、showProgress(默认 true)、hideRules扩展(保留旧in-class/no-message/no-class,新增full-screen/max-window需按 12 部分验收); - 组件新增字段(如 schedule 的
display扩展、countdown 的showProgress、clock 的showSeconds),旧值必须原样保留。
- 顶层新增
- 回滚:迁移失败或新版启动检测到配置不可用时,必须能恢复备份并启动旧结构(主进程启动早期读
config.json的islandLayout失败时使用默认值 + 保留原文件为.corrupt),不得清空用户方案。 - 降级兼容:若旧版本客户端读到新配置,必须忽略未知字段并使用默认值;禁止因未知字段崩溃。版本号高于当前可识别值时,主进程写日志并走只读模式(不自动覆盖新配置)。
- fixtures:
packages/shared测试中必须包含:fixed 默认、custom 多方案/多行/嵌套容器/旧 residentEnabled、极端值(超 12 行/200 节点/6 层/颜色非法/坐标为 null)、旧字段缺失、迁移幂等、迁移失败回滚。
11.3 IPC 与契约兼容
- 保留现有通道:
island:get-schedules、island:get-time-points、island:get-layout、island:save-layout、island:edit-layout、island:get-editing、island:get-visible、island:resident-control、island:resident-touch、island:set-editing、island:content-size、island:remove-notification、island:push、island:class-state、island:alive、island:get-state、island:set-appearance、island:get-appearance、island:set-interactive、island:set-hit-rect、island:set-touch-mode、island:action。 - 新增通道必须遵守:
ipcMain.handle/on前校验event.sender === island.getWindow()?.webContents(或主窗口 webContents),payload 必须是纯数据且做类型/范围校验;preload 白名单同步更新src/preload/island.ts与src/types/desktop.d.ts。 - 新增通道建议(如需要):
island:strong-notification(主进程→岛/强提醒窗口)、island:get-metrics(渲染层上报行/组件度量)、island:preview-mode(编辑器预览)。不得把 Vue Proxy/函数/类实例跨 contextBridge 传递。 - 渲染进程心跳、幽灵窗口检测、渲染崩溃自愈(限流 3 次/10 分钟)与恢复逻辑保持不变。
11.4 性能预算(超预算必须优化)
- 岛渲染进程启动 ≤ 500ms(从窗口创建到首帧可见);冷启动到岛可见 ≤ 3s(不含网络)。
- 空闲 CPU 平均 ≤ 1%(岛可见、时钟/课程正常刷新);消息到达/展开瞬间峰值 ≤ 15% 且 ≤ 500ms。
- 主进程 + 岛渲染进程内存 ≤ 250MB(不含可选强提醒窗口);连续运行 24h 增长 ≤ 10%。
- 消息风暴:1 分钟 20 条不丢、不重复、不卡顿;队列长度上限与丢弃策略必须有日志。
- 动画:主界面过渡 ≥ 55fps;消息气泡/强提醒 ≥ 50fps;不得因背景页(主窗口隐藏)继续动画。
- 包体:本次重构不得新增 > 200KB 的运行时依赖;不得引入新 UI 库/图标库(图标仍用现有方案)。
11.5 平台差异与窗口护栏
- Windows:透明/置顶/点击穿透/
setShape(若用)只在 Windows 分支;Windows 10 不支持 Mica 时退回纯色;高 DPI 下窗口 bounds 用 DIP。 - macOS:透明窗口、置顶、全屏空间行为;不使用
setBackgroundMaterial;点击穿透退化为整窗setIgnoreMouseEvents(true,{forward:true})+ 命中通道。 - Linux X11:透明窗口与置顶可用;Wayland 明确不支持并给提示(与 ClassHelper 既有说明一致)。
- 多显示器:
position/followCursorDisplay/停靠偏移语义保持不变;显示器热插拔/DPI 变化必须重算包围盒与锚点,节流 ≥100ms。 - 触摸:触摸模式仍以"窗口贴合岛体 + 始终接收输入"为原则;编辑模式与触摸手势不能互相抢占;失焦宽限(鼠标 500ms / 触摸 1200ms)保持不变。
- 强提醒窗口:无边框、置顶、不抢焦点、
skipTaskbar、独立渲染入口或复用岛窗口的独立层;关闭后释放;不得在编辑器/预览中创建。
11.6 测试基线(无头优先)
- 单元测试(shared):
normalizeIslandLayouts边界、迁移幂等、版本识别、颜色/数值夹取、组件树限制(12 行/200 节点/6 层/20 方案)、消息排序/去重、课程状态机(准备/上课/课间/下课前/放学/跨天/跨周)。 - 主进程测试:窗口几何计算(6 停靠 × DPI × 多屏)、hit rect/触摸模式切换、消息队列与暂存、课程提醒调度、天气缓存、课件覆盖优先级、配置写入原子性与回滚。
- 组件测试:固定数据下
IslandApp.vue/CustomIslandRuntime.vue的渲染快照(DOM 结构 + 关键 computed 样式),不创建真实窗口(可用 happy-dom/离屏渲染)。 - 集成(无头):
pnpm verify:desktop默认后台快速检查——不得创建可见岛/托盘;覆盖启动、布局切换、消息推送、课程提醒、编辑器保存/读取、迁移。 - 视觉(显式):只有
pnpm verify:desktop:visual/--visual才允许真实桌面、动画采样、截图;截图输出目录用临时路径;不得把窗口对照图发布为展示图。 - 数据纪律:测试使用临时 SQLite/夹具,禁止使用真实班级、真实密码、真实通知;远控/安装器使用假执行器。
- 证据:每次汇报附实际命令输出摘要(通过/失败数量),不得沿用历史数字。
11.7 观测与诊断
- 主进程日志必须记录:配置迁移(旧→新字段、备份路径、结果)、窗口几何重算原因、消息队列变化(入队/出队/丢弃/去重)、课程状态切换、天气刷新/失败、强提醒创建/销毁、渲染崩溃/重建(限流计数)。
- 岛渲染进程必须上报心跳与度量(行/组件尺寸、可见性);主进程对"幽灵窗口"(渲染卡死但窗口在)执行重建(限流)。
- 诊断页/日志中不得包含用户密码、令牌、完整数据库路径;日志轮转与大小上限沿用现有 logger。
- 提供
docs/dev/island-refactor.md的排障章节:黑屏/透明失效/点击穿透/闪烁/动画卡顿/消息不弹/课程不刷新/迁移失败的症状 → 排查步骤 → 回滚方法。
11.8 文档与交付说明
- 更新
docs/app/island.md:新默认观感、风格切换、editor 编辑模式、强提醒、行为表(含 fixed 重定义)。 - 更新
docs/dev/implementation.md:岛的新架构入口、schema v2、迁移函数位置。 - 新增
docs/dev/island-refactor.md:架构图、文件职责、数据契约、动画表、迁移与回滚、测试命令。 docs/中的展示截图必须来自显式视觉验收且人工确认;不得使用重构中的对照图/失败截图。- 交付说明必须包含:与 ClassIsland 的差异清单(技术栈/能力取舍)、未实现项及原因、性能数据、回滚步骤。
11.9 本部分验收标准
- fixtures 覆盖 11.2 全部场景,迁移全部通过且幂等。
- 旧配置在重构版启动后方案、组件、外观、课程提醒开关全部保留,且可切回
winisland观感。 - 所有既有 IPC 通道仍可用,新增通道有 sender 校验与类型校验。
pnpm verify:desktop无参数运行时不创建可见岛/托盘/声音;--visual才创建。- 多显示器/DPI 切换后岛位置与尺寸正确,无 1px 抖动。
- 24h 无内存增长 > 10%、无崩溃、无幽灵窗口。
- 文档更新完成,含迁移/回滚/差异说明。
12. 验收清单(逐项勾选,不得跳过)
所有自动验证默认无头/离屏;
--visual才允许真实桌面。任一项未通过都不得声称完成。
12.1 工程门禁
pnpm lint0 error(warning 需列出)。pnpm typecheck0 error(先构建 shared)。pnpm build:desktop成功;island.html与主窗口产物分别可加载。- 未新增 UI 框架/状态库/图标库;新增依赖(若有)≤ 200KB 且说明不可替代性。
- 只改
packages/shared、packages/desktop-client、packages/ui(如需)与 docs;服务端/Web 管理端零改动。 - 所有新增 IPC 通道有 sender 校验、payload 类型校验;preload 白名单与
desktop.d.ts同步。 - 无
console.log残留;日志走统一 logger。
12.2 视觉验收(像素级)
在 1920×1080@100%、2560×1440@125%、深色/浅色、family=classisland/winisland、Fluent/Classic 主题下截取离屏基准图,逐项比对(颜色 ±2/255、尺寸/位置 ±1px、透明度 ±1%):
fixed(classisland 风格)
- fixed 窗口包围盒固定:宽 = min(工作区宽, 960)、高 = 40 + 2×pad(pad 默认 20);消息到达/展开/多条列表均不改变窗口尺寸。
- 可见行高 40px,行内容左右内边距 12px,最小内容宽 20px;行间距 5px。
- 行背景:圆角 8px、背景不透明度 0.5、1px 描边、阴影
0 4 8 2 #48000000(Fluent)。 - 字号:次要 14 / 主体 16 / 强调 18 / 超大 20;字重 Medium;正文
line-height: 1.25。 - 进度条:高 3px、贴底、圆角 1.5px、颜色为强调色、按设置精度更新。
- 默认 1 行 = 日期 + 课程表;无课表时显示占位文本("今天没有课程。"/"今日课程已全部结束。")。
- 课程表显示当前/下一节/今日时间点模式,当前项展开显示起止时间,其余折叠;课间与"默认隐藏"时间点折叠时隐藏,分割线显示竖线,行动不显示。
- 下课前秒数徽标:蓝色圆形、显示在课程名右侧、剩余 1 秒显示
-1、不显示"即将下课"遮罩、不替换常态组件。 - 消息气泡:遮罩 5 区权重
[0,0.1,0.2,0.4,0.2,0.1]、60° 斜切、区段 360ms + 错峰 120ms、开 QuarticEaseOut / 关 QuadraticEaseIn;文字延迟 260/250ms、scale 1.1→1.0/750ms;退出 200ms(0.5,0,0.75,0);行高 40。 - 强提醒:全屏/大区域遮罩、超时关闭、不抢焦点、在岛所在显示器出现;关闭后无残影。
winisland风格:胶囊 268×44(默认值可配置 220–420 × 36–72)、连续圆角(超椭圆 4.2/14 段)、弹簧形变、纯黑/毛玻璃/主题色渐变、阴影常驻;不得与 classisland 风格混用。
custom(始终 ClassIsland 规格,family 不影响 custom)
- 行高 40、行间距 5、组件外边距 6/6、行背景圆角 8、背景不透明度 0.5。
- 行结构:
Auto, *, Auto三列(左拖动把手 20px / 中组件 / 右行操作区);选中框 1.5px 虚线1,1.5、Opacity 0.75、圆角同背景。 - 多行、分体岛(separated)、主行标记、允许提醒标记、隐藏规则、组件外观覆盖(字号/前景/背景/圆角/透明度/宽度/对齐)逐项与设置一致。
- 编辑器中的组件库、行、容器嵌套、选中态、抽屉尺寸与间距符合第 9 部分。
- 所有组件的默认样式/空数据兜底/超长文本截断/滚动行为符合第 7 部分。
窗口与层级
- 岛窗口无边框、透明、置顶、默认不占任务栏、默认不抢焦点;
winisland与classisland风格均不出现白底/黑边/窗口阴影残影。 - 6 个停靠位 × 偏移(X/Y)× 多显示器索引正确;
followCursorDisplay行为保持。 - 编辑模式窗口/画布尺寸正确:屏宽 ×(屏高−1px)、四边 60px 内缩、开启命中。
- 深浅色/主题切换无残留样式;岛动画由
appearance.animations开关控制(系统prefers-reduced-motion不强制关闭岛动画,遵循 ClassHelper 既有文档约定)。
12.3 功能验收
消息与提醒
- 通知/作业/叫人三类消息的图标、标题、正文、优先级、时间显示符合第 8 部分。
- id 去重;同批消息按"优先级 → 时间"排序;列表"展开更多"与"超出工作区提示"正确。
- 上课期间普通通知/作业暂存、下课补弹;普通叫人保留可点胶囊;紧急通知/紧急叫人立即展开;与 ClassHelper 现有规则逐条一致。
- 已读/知道了/标为已读/批处理的语义与通知中心状态一致;本机新录作业指纹去重。
- 自动收起超时:胶囊 15s、展开 20s、紧急 45s、叫人 90s(按现有常量),可打断、可重入。
- 强提醒仅对开启"强调提醒"的消息出现;关闭后不出现;超时自动关闭;不阻塞岛与其他应用。
- 消息风暴 20 条/分钟:不丢、不重复、不卡顿、队列上限有日志。
课程与天气
- 准备/上课/课间/下课前倒数/课中进度五类提醒按设置触发,阈值默认 60s 可调。
- 课程状态机跨天、跨周、临时课表、预定课表、调课场景正确(与 ClassHelper 现有解析结果一致)。
- 课程提醒接管:fixed(classisland)按 ClassIsland 行内容接管显示 prepare/on-class/break;custom 不再临时接管整岛(移除
resident-lesson-page),进度由课程组件显示,ending 只保留课程名右侧 30×30 蓝色圆形秒数徽标。 - 天气:Open-Meteo 15 分钟刷新、断网缓存并显示"缓存"、失败重试与降级、无官方气象预警措辞。
- 课件传输:传输中覆盖岛内容,显示文件名/类型/教师/字节/进度,完成后校验并回执;不自动运行文件。
编辑器与设置
- 设置页:经典/自定义切换、风格切换、外观参数、课程提醒开关、指针避让、动画开关、强提醒开关全部即时保存并生效。
- 编辑器:方案栏(新建/复制/删除/重命名/导入导出/重置)、组件库 11 类、行(新增/排序/主行/允许提醒/删除)、容器嵌套、拖拽(行内/跨行/库→行/库→容器)、选中与组件设置(内容/外观/尺寸/隐藏条件)、编辑模式预览、预览缩放、退出保存/失败重试。
- 边界:12 行/200 节点/6 层/20 方案;超限拒绝并提示;非法颜色/数值被夹取;未知组件/未知 Kind 不崩溃。
- 旧方案导入后可直接渲染(迁移测试)。
12.4 动画验收
- 第 10 部分动画表逐项实现;时长误差 ≤ 16ms,曲线一致(DevTools Animations 或逐帧截图验证)。
- 岛出现/隐藏、行展开/折叠、行宽/高度、进度条、消息气泡两阶段、强提醒、课程徽标、滚动/轮播、主题切换全部流畅(≥55fps)。
- 不可打断的动画可被新状态覆盖(不排队、不闪回)。
animations=false生效(唯一岛动画开关);speed统一缩放时长;失焦/隐藏页暂停;恢复后不补播。- 动画结束无残留
will-change、定时器、rAF。
12.5 兼容与迁移验收
- 旧
fixed/custom配置 fixtures 升级后:方案、行、组件、外观、课程提醒、指针避让、residentEnabled全部保留;迁移幂等。 - 升级失败可回滚(备份恢复);不可用时保留原文件为
.corrupt并启动默认配置,不得清空。 - 新配置被旧客户端读取时忽略未知字段、不崩溃(如能构造旧客户端则测;否则单测模拟)。
winisland风格下旧观感/旧交互无回归(对照基线图)。- 切换 fixed↔custom、风格切换、方案切换均不新开窗口、不丢配置。
12.6 无头与不打扰验收
pnpm verify:desktop(不带--visual)全程不创建可见岛、托盘气泡、系统通知、声音;测试结束无残留进程/窗口。- 使用独立 userData;不触碰真实配置/真实班级/真实通知。
--visual仅在显式允许时运行,且截图输出到临时目录;不自动发布。- 崩溃自愈限流有效:1 分钟内最多重建 3 次,超过进入安全模式/停止重建。
- 日志无密码/令牌/敏感路径。
12.7 性能验收
- 岛渲染进程启动 ≤ 500ms;冷启动到岛可见 ≤ 3s。
- 空闲 CPU ≤ 1%;消息风暴峰值 ≤ 15% 且 ≤ 500ms。
- 主进程 + 岛渲染进程内存 ≤ 250MB;24h 增长 ≤ 10%;无句柄泄漏。
- 包体增量 ≤ 200KB(除说明的例外);无新增大图标/字体文件。
12.8 文档与交付物
docs/app/island.md、docs/dev/implementation.md更新;新增docs/dev/island-refactor.md。- 架构图、数据契约、动画表、迁移与回滚、测试命令齐全。
- 交付说明:与 ClassIsland 差异、未实现项、性能数据、回滚步骤。
- 第 12 部分本清单逐项勾选并附验证输出摘要;未验证项标注原因。
12.9 禁止事项(验收时再次确认)
- 未通过修改验收标准/删除测试来"通过"。
- 未因重构删除既有功能(消息、课程、天气、课件、编辑、导入导出、指针避让、触摸)。
- 未引入需要联网/登录的新依赖。
- 未把 ClassIsland 的 C#/XAML/本地档案代码复制进 ClassHelper。
13. 重构里程碑(M0–M7)
必须按顺序推进;每个里程碑结束都要「可无头运行 + 通过该阶段验收 + 既有功能不回归」。禁止先大改 UI 再补架构与测试。
M0 基线固化与安全网(0.5–1 天)
- 交付:
- 读通现状并输出文件级计划(第 4 部分)与迁移计划(第 5 部分)。
- 为现有行为建立无头基线:
normalizeIslandLayouts边界用例、消息排序/去重、课程状态机、窗口几何计算、旧配置 fixtures(fixed/custom/嵌套/极端值)。 - 记录性能基线:启动耗时、空闲 CPU、内存、消息风暴耗时。
- 在
verify:desktop中补充"不创建可见窗口/托盘"的断言。
- 验证:
pnpm typecheck、pnpm verify:desktop(无头)通过;fixtures 全部可回放。 - 完成标准:没有行为改动;所有基线用例通过;性能数字记录在
docs/dev/island-refactor.md草案。
M1 共享层 v2:schema、常量、迁移、tokens(1–2 天)
- 交付:
packages/shared/src/island-layout.ts:version=2、appearance.family、appearance.theme、strongNotification、progressBar、rowStyle、scheme 的rowHeight/componentGap/showProgress、组件新字段;normalizeIslandLayouts()迁移+校验(12 行/200 节点/6 层/20 方案、颜色/数值夹取)。- 常量/token:ClassIsland 行高 40、内边距 12、行距 5、圆角 8、透明度 0.5、字号 14/16/18/20、进度条 3px 等;
winisland兼容值保留。 - 迁移函数(幂等)+ 备份/回滚;单元测试覆盖全部 fixtures。
- 验证:单测(迁移、夹取、边界、幂等);
typecheck。 - 完成标准:旧配置升级后数据无损;新 schema 能被现有代码读取(旧渲染仍可运行);迁移失败可回滚。
M2 渲染引擎:行/组件/tokens(2–3 天)
- 交付:
- 统一行/组件渲染模型:fixed 与 custom 共用行结构、背景、进度条、选中框、隐藏规则、淡出、圆角/阴影。
IslandApp.vue重构为 ClassIsland 信息行(默认日期+课程表),CustomIslandRuntime.vue行/组件按新规格;ComponentNode.vue11 类组件默认样式/空数据/布局约束。family双轨渲染仅作用于 fixed(classisland 信息行 / winisland 旧胶囊);custom 统一 ClassIsland 规格;默认按用户升级策略选择;设置页可切换。- DOM/CSS token 结构(第 6/7 部分)落地;旧
winisland视觉保持。
- 验证:组件快照测试(关键 computed 样式/DOM 结构);无头启动渲染;
--visual像素基准(1920×1080、深/浅色、两风格)。 - 完成标准:两风格可切换且互不污染;行/组件像素值达标;旧观感无回归。
M3 消息、课程提醒、强提醒、天气、课件(2–3 天)
- 交付:
- 气泡两阶段(遮罩斜切 360/120ms、文字 260/250ms、scale 1.1→1.0/750ms、退出 200ms)与队列/去重/暂存/紧急/叫人规则。
- 课程提醒五类 + 下课前蓝色秒数徽标 + 课程进度条。
- 强提醒(Topmost)独立透明窗口:全屏/大区域、超时关闭、不抢焦点、多显示器、仅在强调提醒开启时出现;编辑/预览不创建。
- 天气显示/缓存标记;课件覆盖优先与进度。
- 验证:消息/课程单测(假时钟、假队列);无头推送场景;
--visual气泡/强提醒动画采样。 - 完成标准:ClassHelper 既有消息/课程规则逐条不变;视觉/动画达标;强提醒不影响其他应用。
M4 设置页与编辑器重构(2–3 天)
- 交付:
IslandSettingsView.vue:经典/自定义切换、风格切换、外观参数(字号阶梯/行距/进度条/强提醒)、课程提醒、指针避让、动画开关;即时保存、校验、回显。IslandEditorView.vue:方案栏、组件库、行/容器、拖拽、组件设置抽屉、编辑模式预览、预览缩放、退出保存/失败重试;与 ClassIsland 编辑模式对齐(选中框、行设置、添加行、组件边距)。IslandComponentSettings.vue:字段与第 7 部分一一对应;未知字段保留。
- 验证:组件测试 + 无头保存/读取 + 迁移后旧方案编辑;
--visual编辑器截图。 - 完成标准:所有设置即时生效;编辑器可用、保存可靠、退出不丢;12 行/200 节点/6 层/20 方案边界提示。
M5 动画、主题、细节打磨(1–2 天)
- 交付:
- 第 10 部分动画表逐项;60fps;可打断;
animations=false/减少动画/失焦/隐藏页暂停。 - Fluent/Classic 两套岛主题;
winisland玻璃/纯黑/渐变保持;主题切换无闪烁。 - 交互细节:点击区域、悬停、触摸、键盘、拖拽阈值、选中/取消、右键(如适用)。
- 第 10 部分动画表逐项;60fps;可打断;
- 验证:DevTools/帧采样;逐项动画检查;像素 diff;无头回归。
- 完成标准:动画误差 ≤16ms、帧率达标;主题/风格切换无残留;减少动画生效。
M6 迁移、兼容与回归(1–2 天)
- 交付:
- 迁移 fixtures 全量回放(fixed/custom/旧 residentEnabled/嵌套容器/极端值/非法数据);回滚演练。
winisland观感对照基线;旧交互逐条回归(消息/课程/天气/课件/编辑器/触摸/多屏)。- 旧版本读新配置的兼容处理;日志/诊断/文档更新。
- 验证:迁移单测 + 回滚测试 +
pnpm verify:desktop+--visual对照。 - 完成标准:旧配置 100% 无损;回滚可用;旧观感/旧交互无回归。
M7 全量验收与交付(1–2 天)
- 交付:
- 第 12 部分逐项勾选;性能数据(启动/CPU/内存/帧率/消息风暴);差异说明;回滚说明。
pnpm lint、pnpm typecheck、pnpm build:desktop、pnpm verify:desktop(无头)、--visual视觉验收记录。docs/app/island.md、docs/dev/implementation.md、docs/dev/island-refactor.md完成。
- 验证:按第 12 部分执行;未通过项必须列出原因与修复计划。
- 完成标准:第 12 部分全通过(或明确标注例外并获得确认);无可见窗口打扰的默认验证可重复通过。
里程碑风险提示
- M1 迁移必须先于任何渲染改动,否则旧配置会在开发中损坏。
- M2/M3 必须保留 feature flag(
family与内部开关),确保随时可回退到旧渲染。 - M4 编辑器依赖 M1 的 schema 与 M2 的渲染组件;不得并行乱改。
- M5/M6 的像素/动画验收需要稳定的基准图与固定测试数据,禁止用真实班级数据。