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

329 lines
40 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 4. 目标架构与文件级改造清单
### 4.0 重构原则(必须逐条遵守)
1. **只在现有仓库内重构**:不得新建工程、不得换框架、不得引入服务端依赖;沿用 pnpm@11.8.0 monorepo 与 Electron ^44.3.0 + Vue ^3.5.42 + Element Plus ^2.14.5 + Pinia ^4.0.3 + Vite ^8.3.0 + TS ~5.9.3。
2. **渐进式替换,不一次性推倒**:每次改动后应用必须仍可启动、旧 `config.json` 仍可加载;用"门面(facade)+ 新模块"的方式把旧调用点隔离,避免一次改 `src/main/index.ts`、`tray.ts`、`smoke.ts` 的全部 import。
3. **单一事实来源**:任何"主进程与渲染进程各算一次"的数值(窗口包围盒、列表高度、命中矩形、细缝尺寸、字号推导)必须收口到 `packages/shared/src/island/`,两端只调用同一函数。现状三处漂移见 02 §2.3。
4. **保留现有业务语义**:消息去重、上课暂存、紧急展开、已读回执、作业免打扰指纹、课件覆盖优先级、离线补弹,全部保持(02 §2.5/§2.12);ClassIsland 只改变"视觉与布局表现"。
5. **两套风格族(family)共存,且 family 只作用于 fixed**:`appearance.family` 取值 `'classisland' | 'winisland'`;**默认 `classisland`**;`winisland` 保留原胶囊+详情卡外观;老用户升级默认 `winisland`(迁移见 05)。**`custom` 布局始终按 ClassIsland 规格渲染,不受 family 影响**(family 只决定 fixed 用哪套渲染器)。
6. **`fixed` 重新定义**:在 `family='classisland'` 下,`fixed` 不再是胶囊+详情卡,而是 **ClassIsland 风格信息行**(默认 1 行 = 日期 + 课程表,行高 40、圆角 8、背景不透明度 0.5、3px 进度条、消息以行内气泡呈现)。原"胶囊+详情卡"只在 `family='winisland'` 下保留。这是本轮最重要的语义变化,所有文件改造必须按此分叉。
7. **一个原生窗口**:多种布局/风格共用一个 `BrowserWindow`;切换 family/layout 不得新开窗口(现状约束,见 `docs/app/island.md:8-10`、`src/main/island.ts:468-634`)。
8. **不打扰纪律**:默认验证无头/离屏,不创建可见岛或托盘;只有显式 `--visual` 才做桌面像素验收。依据:`docs/dev/testing.md:9,29-32`。
9. **调色板与风格分离**:`style: black|glass|tinted` 继续保留,写为 winisland 调色板字段(canonical `appearance.winislandStyle`,旧 `style` 作为兼容镜像,不删除);family 与 style 独立,切换 family 时保留用户 style 原值。
10. **外观新增定稿字段**:`colorScheme: 'light'|'dark'|'system'`(默认 system)、`theme: 'fluent'|'classic'`(默认 fluent)、`effectEnabled`(默认 true)、`effectScale: 1|0.75|0.5`(默认 1)、`progressBar:{height:3,show:true}`;`fontSize` 语义随 family:classisland=正文基准(新装默认 16,派生 14/18/20),winisland=旧语义(默认 13,迁移保留原值)。
11. **版本只保留一个**:落盘版本只有 `islandLayout.version=2`;`schemaVersion` 仅允许出现在 IPC payload 作运行时校验,不得落盘。
12. **fixed 消息/提醒口径**:行内气泡同一时刻只显示 1 条 active;多条折叠为「还有 N 条」并可打开应用内通知中心;`prepare/on-class/break` 以行内容接管呈现、`ending` 只用蓝色圆形秒数徽标;队列/去重/暂存规则不变。
### 4.1 目标模块边界与目录
必须把灵动岛拆成四个边界清晰的部分。目录与职责如下(标注「新增」的目录/文件由本轮创建;其余为移动或改造):
```text
packages/shared/src/island/ # 新增:岛的唯一契约层(纯 TS,无 DOM/Electron)
├─ index.ts # 统一导出(供 desktop-client 与 web-admin 可复用)
├─ schema.ts # IslandLayoutV2 / IslandAppearanceV2 / IslandRowV2 / IslandNodeV2 类型
├─ defaults.ts # ClassIsland 规格默认值(family/行/组件/外观)
├─ normalize.ts # normalizeIslandLayoutV2 / normalizeAppearanceV2(校验+夹取)
├─ migrate.ts # migrateIslandLayoutV1ToV2(旧 config → v2)
├─ metrics.ts # CLASSISLAND_ISLAND_METRICS + WINISLAND_ISLAND_METRICS + 排版系数
├─ geometry.ts # 锚点/包围盒/细缝/触摸贴合尺寸的纯函数(主/渲染共用)
├─ messages.ts # 去重、排序、列表布局、行内气泡分组(纯函数)
├─ lessons.ts # 课程提醒计算(从 main/lesson-reminder.ts 上移)
├─ dto.ts # IslandState/IslandNotification/ScheduleDto 等对外 DTO 与映射
└─ ipc.ts # IPC 通道名与 payload 类型(主进程与 preload 共用)
packages/desktop-client/src/main/island/ # 新增:主进程岛引擎(从 island.ts 拆出)
├─ index.ts # 门面:export { island, registerIslandIpc, ... }(旧 import 不变)
├─ controller.ts # 状态机编排:消息、上课状态、预览、动作分发
├─ window.ts # 建窗、固定包围盒、锚点布局、可见性/焦点/透明度
├─ hit-test.ts # 命中轮询、岛体矩形、interactive 决策(含 setShape 降级)
├─ touch.ts # 触摸模式、贴合、失焦宽限、常驻控件
├─ messages.ts # 队列存储 + 合并 + 超时计时器(无 UI)
├─ lessons.ts # 每秒推进课程提醒
├─ weather.ts # 从 island-weather.ts 移入
├─ courseware.ts # 课件覆盖状态桥
├─ preview.ts # 设置页"测试灵动岛"的预览注入
└─ ipc.ts # island:* 全部 handler(从 island.ts 尾部搬来)
packages/desktop-client/src/island/ # 渲染进程(现有目录内重组)
├─ main.ts # Keep:tokens + motion + mount
├─ LayoutIslandApp.vue # Change:分发(custom→CustomIslandRuntime;fixed 按 family 二选一)
├─ family/
│ ├─ classisland/
│ │ ├─ FixedInfoRow.vue # 新增:ClassIsland 信息行(fixed 新默认)
│ │ ├─ InlineMessageBubble.vue # 新增:行内消息气泡(fixed 下替代详情卡)
│ │ └─ ClassislandTheme.css # 新增:Fluent/Classic 两套行样式与动画
│ └─ winisland/
│ ├─ ClassicIsland.vue # Change:由 IslandApp.vue 迁移并改名(原胶囊+详情卡)
│ └─ winisland.css # Change:由 classic-island.css 迁移(数值不变,仅 fixed 使用)
├─ custom/
│ ├─ CustomIslandRuntime.vue # Change:始终按 ClassIsland 行/组件规格重写测量与适配(不看 family)
│ ├─ CustomRows.vue # 新增:常态行 / 展开行渲染(行级外观)
│ ├─ CustomRow.vue # 新增:单行(行设置、隐藏规则、进度条)
│ └─ CustomRowSettingsDrawer.vue# 新增:行设置抽屉(背景/字号/圆角/隐藏;字段由 09 定义)
├─ shell/
│ ├─ IslandShell.vue # 新增:squircle 形状 + 弹簧形变 + 命中上报(fixed 两族共用)
│ ├─ MessagePanel.vue # Change:由 ClassicNotifications.vue 改造(单条/列表/行内气泡)
│ └─ LessonBar.vue # Change:由 LessonReminderContent.vue 改造(ClassIsland 遮罩)
├─ components/ # 新增:按 kind 拆分的组件渲染器
│ ├─ index.ts # node.kind → 渲染器映射
│ ├─ DateNode.vue / ClockNode.vue / TextNode.vue / CountdownNode.vue
│ ├─ ScheduleNode.vue / WeatherNode.vue / SeparatorNode.vue
│ └─ ContainerNode.vue # group/stack/slide/rolling(rolling 支持 mode continuous|pause)
├─ motion/ # Keep:spring.ts、squircle.ts(可加适配器)
├─ styles/ # 新增:shared.css / classisland.css / custom.css(custom 始终 classisland 命名空间)
└─ Legacy/ # 可选:保留旧文件名再导出门面(ComponentNode.vue 等)
packages/desktop-client/src/renderer/island/ # 主窗口侧的桥(现有)
├─ bridge.ts # Keep:数据桥(课表/上课状态/通知/作业/叫人/补弹/已读)
└─ editor/ # 新增:编辑器逻辑 composables(从 IslandEditorView 抽出)
├─ useIslandDraft.ts # 草稿、脏标记、保存队列、修订号
├─ useIslandDrag.ts # 指针拖拽/落点/自动滚动
└─ useIslandPreview.ts# 内嵌预览缩放与测量
packages/desktop-client/src/renderer/ # 设置/编辑器页面(现有)
├─ views/settings/IslandSettingsView.vue # Change:family 选择器 + 条件外观
├─ views/settings/IslandEditorView.vue # Change:调用新版架构 + 行设置
├─ components/IslandComponentSettings.vue # Change:ClassIsland 组件字段
├─ components/IslandEditorNode.vue # Keep(可加行级拖拽)
├─ components/IslandRowSettingsDrawer.vue # 新增:行设置抽屉
└─ styles/island-editor.css # Change:ClassIsland 编辑器视觉
```
### 4.2 文件级 Keep / Change / Add / Remove 总表
| 文件/目录 | 类型 | 一句话目标 |
|---|---|---|
| `packages/shared/src/island-layout.ts` | Change | 保留导出名兼容;实现迁移到 `island/` 新目录,本文件仅再导出 |
| `packages/shared/src/constants.ts` | Change | 保留旧 WinIsland 常量;新增 ClassIsland 度量与 family 常量(迁移见 05) |
| `packages/shared/src/types.ts` | Change | `IslandAppearance` 增加 `family`/`colorScheme`/`effectEnabled`/`effectScale`/`progressBar`/`theme`/`winislandStyle`;新增 `IslandState.family/pendingStaged/bubblePhase/bubbleDeadline`、`IslandNotification.level`、`IslandLessonReminder.progress/leftSeconds`(运行期,不落盘) |
| `packages/shared/src/island/ipc.ts` | Add | IPC 通道名与 payload 类型;`island:push` 增加 `source`/`dedupeKey` |
| `packages/shared/src/motion.ts` | Keep | 不动令牌数值(03/10 部分引用) |
| `packages/shared/src/class-plan.ts` | Keep | `IslandTimePointDto`/`TimePointType` 不动 |
| `src/main/island.ts` | Change | 2445 行 → 门面(约 30 行),实际逻辑移入 `main/island/*` |
| `src/main/island/*.ts` | Add | 见 4.1(controller/window/hit-test/touch/messages/lessons/weather/courseware/preview/ipc) |
| `src/main/config.ts` | Change | 启动时执行 v2 迁移钩子(唯一版本=`islandLayout.version`);写盘前校验 `family/colorScheme/fontSize/effect*`;保留合并写/退出 flush |
| `src/main/index.ts` | Change | `island.init()` 改调新门面;注册编辑器/预览所需 IPC |
| `src/main/ipc.ts` | Change | 岛相关 handler 全部委托 `main/island/ipc.ts`;主窗口配置剔除规则保留 |
| `src/main/courseware.ts` | Change | 改为调用 `island.setCoursewareTransfer`(门面保留同名方法) |
| `src/main/tray.ts` | Change | 菜单文案与入口不变;`island.editLayout()` 改由新控制器实现 |
| `src/main/island/messages.ts` | Add | 消息队列 + 合并 + 超时 + **作业指纹/去重表**(见 4.3.13) |
| `src/main/island/ipc.ts` | Add | 校验 `island:push` 的 `source/dedupeKey`,投递前查重 |
| `src/main/smoke.ts` | Change | 保留全部既有读取器与注入点(名称/语义不变,见 4.7 第 11 条);新增 v2 断言(行高/行数/气泡/迁移);改造归属 part 11 测试基线 |
| `src/preload/index.ts` | Change | 新增 `family/colorScheme` 感知的配置读写;`islandPush` 透传 `source/dedupeKey`;保留全部旧方法名 |
| `src/preload/island.ts` | Change | 新增行/编辑器预览相关事件;旧事件名与语义不变 |
| `src/types/desktop.d.ts` | Change | 配置形状加外观新字段与 `islandLayout.version`;新增 v2 DTO 类型;`islandPush` payload 扩展 |
| `src/island/LayoutIslandApp.vue` | Change | 分发:`custom` → CustomIslandRuntime;`fixed` 按 family 二选一(见 4.3.1) |
| `src/island/IslandApp.vue` | Change | 迁出为 `family/winisland/ClassicIsland.vue`;旧文件保留再导出(防断裂) |
| `src/island/CustomIslandRuntime.vue` | Change | **始终按 ClassIsland 行/组件规格**重写测量、行渲染、行设置抽屉;不受 family 影响 |
| `src/island/ComponentNode.vue` | Change | 拆为 `components/*Node.vue`;本文件保留兼容导出;滚动组件新增 `settings.mode('continuous'|'pause')` |
| `src/island/ClassicNotifications.vue` | Change | 改为 `shell/MessagePanel.vue`;新增行内气泡模式(同一时刻 1 条 active) |
| `src/island/LessonReminderContent.vue` | Change | 改为 `shell/LessonBar.vue`;遮罩参数按 ClassIsland 重算;fixed 下 prepare/on-class/break 行内容接管、ending 仅徽标 |
| `src/island/CoursewareTransfer.vue` | Keep | 课件覆盖卡保持现状(424×132 等,见 02 §2.10) |
| `src/island/WeatherBrief.vue` | Keep | 天气简报保持现状(Open-Meteo/15 分钟/缓存标记);**本轮不引入 ClassIsland 天气服务** |
| `src/island/spring.ts`、`squircle.ts` | Keep | 弹簧与连续圆角数值不动(10 部分仍按此写) |
| `src/island/classic-island.css` | Change | 改名 `family/winisland/winisland.css`(fixed 专用);数值不变,选择器按新 DOM 调整 |
| `src/island/custom-runtime.css`、`custom-island.css` | Change | 移到 `styles/custom.css` 与 `custom/*`;**始终按 ClassIsland 行规格重写**(custom 不受 family 影响) |
| `src/island/lesson-ending.css` | Keep | 秒数徽标样式不动 |
| `src/island/main.ts` | Keep | 入口与令牌注入顺序不动 |
| `src/renderer/views/settings/IslandSettingsView.vue` | Change | 顶部加 `family` 选择器(只影响 fixed);新增 colorScheme/theme/effectEnabled/effectScale/progressBar;fontSize 语义随 family 展示 |
| `src/renderer/views/settings/IslandEditorView.vue` | Change | 数据改为 v2;行设置抽屉;预览按 ClassIsland 规格;保存队列/上限保留;拆分节奏按 09 文件计划(见 4.3.11) |
| `src/renderer/components/IslandComponentSettings.vue` | Change | 组件字段与 ClassIsland 规格对齐(含滚动 mode);旧字段保留只读兼容 |
| `src/renderer/components/IslandEditorNode.vue` | Keep | 标签节点可复用;若加行级拖拽则小幅 Change |
| `src/renderer/components/IslandRowSettingsDrawer.vue` | Add | 行级设置的唯一入口(字段清单由 09 定义) |
| `src/renderer/island/bridge.ts` | Change | 作业投递增加 `source:'homework-local'|'realtime'|'replay'` 与 `dedupeKey`;保留本机指纹快速路径(见 4.3.13) |
| `src/renderer/views/HomeworkView.vue` | Keep | `markHomeworkCreatedLocally` 调用点保留(录入前/后登记指纹,见 4.3.13) |
| `src/renderer/styles/island-editor.css` | Change | 编辑器视觉按 ClassIsland(间距/分隔/虚线框/预览画布) |
| `packages/desktop-client/scripts/smoke.mjs` | Change | 视觉/几何断言改为按 family 分套;默认 quick 仍无头 |
| `docs/app/island.md`、`docs/dev/implementation.md` | Change | 口径更新为 family 与 fixed 新语义(文档不是代码,但必须与实现同步) |
### 4.3 关键文件改造详情(逐个文件:现状 → 目标 → 接口 → 风险)
#### 4.3.1 `src/island/LayoutIslandApp.vue`(Change,按 layout + family 分发)
- **现状**:只按 `layout==='custom'` 二选一(`CustomIslandRuntime` / `FixedIsland`),课件优先覆盖。依据:`packages/desktop-client/src/island/LayoutIslandApp.vue:56-75`。
- **目标**(定稿分发规则):
- `layout==='custom'` → **始终** `custom/CustomIslandRuntime.vue`(ClassIsland 行/组件规格),**不看 family**;
- `layout==='fixed'` 且 `appearance.family==='classisland'` → 新增 `family/classisland/FixedInfoRow.vue`(信息行 + 行内气泡);
- `layout==='fixed'` 且 `appearance.family==='winisland'` → `family/winisland/ClassicIsland.vue`(原胶囊+详情卡,行为不变);
- 课件传输仍最高优先级,覆盖以上所有分支。
- **接口**:props 保持 `settings/state/schedules/timePoints/appearance/nativeVisible`;新增计算 `family = appearance.family ?? 'classisland'` 仅用于 fixed 分支;不新增 IPC 通道。
- **为什么**:family 只影响 fixed,custom 不需要两套渲染;分发规则收窄后,custom 的数据与视觉可以独立演进。
- **风险**:切换 family 只改变 fixed 渲染器,custom 不受影响——必须保证切换时若当前 layout=custom 不重建 custom 运行时(避免方案展开态/测量状态丢失);切换命中矩形时 `reportHit(null)` 并 `syncTargets(true)`。
#### 4.3.2 `src/island/IslandApp.vue` → `family/winisland/ClassicIsland.vue`(Change,大改)
- **现状**:683 行,胶囊层 + 展开层 + 课程提醒 + 命中上报 + 弹簧形变全在一个文件;窗口固定包围盒内的岛由 `morph` 驱动。依据:`src/island/IslandApp.vue:44-570`。
- **目标**:
1. 保留并迁移全部 winisland 行为(胶囊/展开/紧急/叫人/多条列表/课程提醒/命中),**视觉与交互不变**(老用户回退路径)。
2. 把"壳"(squircle 路径、弹簧、`--card-path`、命中上报)抽到 `shell/IslandShell.vue`,把消息展示抽到 `shell/MessagePanel.vue`,把课程提醒抽到 `shell/LessonBar.vue`;`ClassicIsland.vue` 只负责"胶囊内容 + 展开内容"的编排。
3. 文件顶部保留兼容导出:`export { default } from './family/winisland/ClassicIsland.vue'`,避免旧 import 断裂。
- **接口**:props 必须兼容现有 `{ embedded?, suspended? }`;继续调用 `window.island.sendAction/setInteractive/setHitRect`;继续使用 `ISLAND_SIZE_DELTA`/`ISLAND_LIST_METRICS`。
- **为什么**:ClassIsland fixed 取代胶囊后,旧实现仍要作为 `winisland` 保留;抽壳后两族共用同一套命中/形变,减少重复实现。
- **风险**:抽壳会改变 DOM 层级,现有 CSS 选择器(`.island-card .content`、`.pill-inner`)与冒烟断言依赖 DOM;必须保留 `.island-card`、`.island-root`、`data-anchor`、`data-style` 等关键钩子。
#### 4.3.3 `family/classisland/FixedInfoRow.vue`(Add,fixed 的新默认)
- **目标**:ClassIsland 信息行的固定布局:默认 1 行,行高 40、行内水平内边距 12、行间距 5、圆角 8、背景不透明度 0.5(背景色取主题 token + `appearance.winislandStyle`/surface 材质)、1px 描边、字号由 `appearance.fontSize`(classisland 下=正文基准,默认 16)派生 14/18/20、进度条高度取 `appearance.progressBar.height`(默认 3)、`progressBar.show=false` 时不显示;默认行 = 日期组件 + 课程表组件;消息以**行内气泡**呈现(普通/重要/紧急/叫人/作业),不再弹"详情卡";强提醒特效仅在 `effectEnabled=true` 时叠加,尺寸按 `effectScale`。
- **接口**:props `{ state, appearance, schedules, timePoints, now, editing? }`;内部复用 `components/*Node.vue` 与 `shell/MessagePanel.vue`(行内模式);只读 `appearance.colorScheme` 决定行底色/文字对比。
- **为什么**:ClassIsland 的主界面就是"一行行信息",fixed 只有变成信息行才符合重构目标。
- **风险**:fixed 原有"点胶囊展开卡片"的点击目标消失,必须保证消息可读(行内气泡可点开详情面板);命中矩形上报必须按真实行矩形,不能用旧胶囊尺寸(窗口包围盒由 geometry.ts 计算)。
#### 4.3.4 `family/classisland/InlineMessageBubble.vue`(Add,行内气泡)
- **目标**:**同一时刻只显示 1 条 active 气泡**;点击展开详情/操作;有多条未处理消息时折叠为「还有 N 条」,点击打开应用内通知中心(沿用 ClassHelper 现有通知中心路由),不并列展示多条;两阶段表现为先横向展开成条、再展开详情面板(动画由 08/10 定义);普通/重要/紧急/叫人/作业五类样式;紧急消息仍按现有业务规则"立即展开、无视上课"。
- **接口**:props `{ active, pendingCount, mode, listExpanded }`;动作只通过 `window.island.sendAction`,不得直接改状态;数据来源是 `IslandState.active/queued` 与 `IslandNotification.level`。
- **为什么**:ClassIsland 的提醒是"行内/气泡"而非独立卡片;不改变业务队列,只换表现。
- **风险**:行内气泡只改变行内内容测量与截断(正文最多 2 行、超出省略),**不改变窗口包围盒**(fixed 窗口尺寸固定为 min(工作区宽,960) × (40+2×pad));「还有 N 条」跳转必须带未读上下文,不得丢失已读回执。
#### 4.3.5 `custom/CustomIslandRuntime.vue`(Change,大改;始终 ClassIsland 规格)
- **现状**:三页(compact/details/lesson)交叉淡入 + 自然内容测量 + 铃铛 + 命中上报;行/组件来自 `scheme.rows`。依据:`src/island/CustomIslandRuntime.vue:52-336,338-527`。
- **目标**:保留"测量 + 三页"骨架,**统一按 ClassIsland 行规格**重写(**不区分 family**):单行基准高 40、行间距 5、行内水平留白 12、行背景/圆角/不透明度/字号覆盖、行级隐藏规则、行级进度条(3px,含义见 05 §5.3);行设置抽屉入口(字段由 09 定义);`rows`/`expandedRows` 的编辑语义不变;滚动组件支持 `settings.mode: 'continuous'|'pause'`(pause 时到边界停顿 `settings.interval`)。
- **接口**:props 不变;新增读取 `row.appearance/row.hideRules/row.progress`;`window.island.reportResidentControl` 继续上报铃铛矩形;`window.island.reportSize` 继续上报适配后尺寸;CSS 使用 `classisland` 命名空间(custom 不受 family 影响)。
- **为什么**:ClassIsland 组件库/行模型与本应用 schemes 高度相似,且 family 最终只作用于 fixed;custom 统一到 ClassIsland 规格可避免"两套自定义外观"。
- **风险**:老 custom 用户升级后视觉会变化(自定义布局统一到 ClassIsland 规格),必须在发布说明与设置页提示;`ResizeObserver` + `requestAnimationFrame` 测量链对行数敏感,改写后的几何只由 metrics 计算,不用动画中的壳反推(现状注释见 `:73-79`)。
#### 4.3.6 `src/main/island.ts` → `main/island/*`(Change + Add,大重构)
- **现状**:2445 行单文件(分区见 02 §2.2)。
- **目标**:
- `index.ts`:门面导出 `island`、`registerIslandIpc`、`recomputeSizes`、`normalizeAppearance`、`getSliverSize`、`ISLAND_SIZES` 等旧导出名,**保证 `main/index.ts`、`tray.ts`、`courseware.ts`、`smoke.ts` 的 import 不改**。
- `controller.ts`:状态与编排;持有 `IslandState`;消息/上课/预览/动作分发。
- `window.ts`:建窗、固定包围盒/触摸贴合、锚点、可见性/焦点/透明度、`syncWindow` 拆分后的"可见性决策"。
- `hit-test.ts`:60ms 轮询、`setInteractive`、`setHitRect`、`expectedHitRect`、`setShape` 降级(非 Windows 见 4.6)。
- `messages.ts`:`pushNotification` 分流、合并、排序、超时计时器、`dismissActive`、`clearPendingForClose`、`handleAction`。
- `lessons.ts`:`LessonReminderTracker` 每秒推进与 `lessonShowing` 判定。
- `ipc.ts`:`registerIslandIpc()` 全部 handler(含发送方校验与外观合并节流 16ms)。
- **接口**:必须保留全部旧公开方法与冒烟读取器(02 §2.5 第 9 条清单);新增 `getMetrics()`、`getLayoutVersion()` 供编辑器/冒烟断言。
- **为什么**:不拆文件无法安全替换 fixed 渲染、无法让 engine 复用 shared 纯函数,也无法让 ChatGPT 在单文件里做可靠改动。
- **风险**:拆分过程中最容易出现"私有字段跨模块互相引用";必须先把 `IslandState`、`WindowBox`、`HitRect`、`Metrics` 定义到 `types`,模块间只经接口通信;拆完立即用现有 `verify:desktop`(无头)回归。
#### 4.3.7 `src/island/ComponentNode.vue` → `components/*Node.vue`(Change)
- **现状**:456 行,14 类 kind 的条件渲染 + 课程表折叠算法 + 编辑手柄 + 行内样式。依据:`src/island/ComponentNode.vue:29-210,212-293`。
- **目标**:按 kind 拆文件,`components/index.ts` 提供 `resolveNodeRenderer(kind)`;每个组件只负责自己的内容与样式;课程表的"折叠/展开/隐藏/进度/徽标"逻辑放进 `ScheduleNode.vue` 并复用 02 §2.8/§2.9 规则。
- **接口**:保持 `IslandComponentNode` 数据结构不变(字段名/默认值见 `packages/shared/src/island-layout.ts:77-108`);编辑态仍用 `node-handle` 拖拽句柄。
- **为什么**:ClassIsland 组件规格要为每类组件补字段(字号覆盖、进度条、显示模式),单文件条件链会继续膨胀。
- **风险**:`<style scoped>` 拆分后类名作用域变化;必须把跨组件公共样式放 `styles/components.css`,避免样式丢失。
#### 4.3.8 `src/island/ClassicNotifications.vue` → `shell/MessagePanel.vue`(Change)
- **现状**:单条详情卡 + 多条列表卡 + 按钮行;只用于展开态。依据:`src/island/ClassicNotifications.vue:104-233`。
- **目标**:同一组件支持三种形态:`detail`(winisland 详情卡)、`list`(winisland 多条列表)、`inline`(classisland fixed 行内气泡);按钮语义(打开应用/标为已读/知道了/收到)与批量语义不变;`canMarkRead` 排除作业的规则保留。
- **接口**:props 增加 `variant: 'detail'|'list'|'inline'`;保持 `state/appearance/maxHeight`。
- **为什么**:fixed 默认不再有详情卡,但消息仍需可操作;共用一套面板避免三处重复实现按钮/已读逻辑。
- **风险**:`inline` 形态的高度必须计入行高与窗口包围盒(由 `shared/island/messages.ts` 提供),不得在渲染层单独硬编码。
#### 4.3.9 `src/island/LessonReminderContent.vue` → `shell/LessonBar.vue`(Change)
- **现状**:5 区斜切遮罩 + 进度条 + 秒数徽标;遮罩区段 360ms/错峰 90-120ms、开 `1-(1-p)^4`、关 `p²`;文字 260ms、scale 1.1→1.0、750ms。依据:`src/island/LessonReminderContent.vue:59-123,133-137`。
- **目标**:按 ClassIsland 规格校准:遮罩斜切权重 `[0,0.1,0.2,0.4,0.2,0.1]`(与 03/14 部分一致)、区段 360ms、错峰 120ms、开 QuarticEaseOut、关 QuadraticEaseIn、文字 delay 260ms;行高 40、圆角 8。**叠加口径(定稿)**:fixed(classisland)下 `prepare/on-class/break` 以**行内容接管**呈现(临时替换课程表内容,动画由 08/10 定义);`ending` 不接管行内容,只在课程名右侧显示蓝色圆形秒数徽标;custom 不接管(保持课程组件 + 徽标)。
- **接口**:props 保持 `{ reminder, now, appearance, motion }`;新增 `variant: 'winisland'|'classisland'`。
- **为什么**:ClassIsland 的提醒遮罩是"整行被斜切色块覆盖 + 文字弹入",与现有实现同源但参数不同;必须抽成共用组件。
- **风险**:`ref` 宽度依赖 `ResizeObserver`,在 fixed 行内宽度变化频繁;必须避免测量死循环(只在行宽变化时重算多边形)。
#### 4.3.10 `src/renderer/views/settings/IslandSettingsView.vue`(Change)
- **现状**:布局二选一、外观滑块、测试预览。依据:`src/renderer/views/settings/IslandSettingsView.vue:26-160,162-392`。
- **目标**:
1. 顶部先选 **风格族 family**(classisland 默认 / winisland 兼容)——**只影响 fixed**,说明文案必须写清"自定义布局始终使用 ClassIsland 规格";
2. `fixed` 的说明按 family 变(classisland=fixed 信息行;winisland=fixed 胶囊+详情卡);
3. 新增外观控件:`colorScheme`(浅色/深色/跟随系统,默认跟随)、`theme`(fluent/classic,仅 classisland)、`effectEnabled`(强提醒特效总开关)、`effectScale`(1/0.75/0.5)、`progressBar.show/height`(默认 3px)、`fontSize`(classisland 显示为"正文字号(16 基准)"、winisland 显示为旧"基础字号(13 基准)");
4. 调色板 `winislandStyle`(black/glass/tinted)仅在 winisland 下显示;切换 family 时**保留用户原值**,不重置;
5. "测试灵动岛"必须按当前 family + layout 推预览。
- **接口**:`window.desktop.saveConfig({ island: {...} })`、`islandSetAppearance` 不变;新增字段随 `island` 下发(纯数据,禁止传 Proxy)。
- **为什么**:family 是重构后最上层的开关,必须最先可见;新增外观字段需要用户可发现、可关闭。
- **风险**:切换 family 会立即改变 fixed 外观;必须先弹确认"切换后仅 fixed 表现改变,自定义方案与 style 不变",且不得自动改 layout。
#### 4.3.11 `src/renderer/views/settings/IslandEditorView.vue`(Change)
- **现状**:组件标签 + 内嵌预览 + 方案菜单 + 组件库 + 检查器 + 指针拖拽 + 保存队列。依据:`IslandEditorView.vue:25-130,130-540,541+`。
- **目标**:数据切到 v2;增加"行"选择与行设置抽屉(`IslandRowSettingsDrawer.vue`,字段清单由 09 定义);预览统一按 ClassIsland 规格渲染(custom 编辑也用 `CustomRows`);保留上限(200 组件/20 方案)、保存队列、离开前保存、JSON 导入导出。
- **接口**:仍通过 `window.desktop.getConfig/saveConfig`;`normalizeIslandLayouts` 改为 `normalizeIslandLayoutV2`;`getIslandSchedules/getIslandTimePoints` 不动。
- **为什么**:ClassIsland 编辑器的核心是"行/组件/行设置抽屉/组件库拖拽",现有编辑器已有 80% 骨架。
- **风险**:编辑器是 855 行大文件;**拆分节奏按 09 的文件计划执行,不强制本轮先抽 composables**;若单文件继续膨胀,再按 4.1 的 `renderer/island/editor/` 抽取,抽取时不得改变保存队列/修订号语义。
#### 4.3.12 `src/main/config.ts`(Change,迁移接入点)
- **现状**:`PersistedConfig` 含 `islandLayout`/`island`,逐字段归一化,120ms 合并写。依据:`src/main/config.ts:26-84,198-233,334-346`。
- **目标**:**不新增文件级版本号**;读取 `islandLayout` 时若 `version` 缺失 → 调用 `migrateIslandLayoutV1ToV2`(含 family/外观默认值,见 05),迁移结果写回并备份;`saveConfig` 收到 `islandLayout` 时先 `normalizeIslandLayoutV2`;`island` 增加 `family/colorScheme/theme/effectEnabled/effectScale/progressBar/winislandStyle` 归一化。
- **接口**:导出 `getConfig/saveConfig/flushConfigSync` 签名不变;新增 `getIslandLayoutV2()` 供主进程使用。
- **为什么**:迁移必须在任何消费者(岛窗口、编辑器、冒烟)之前完成,且只能发生一次;版本字段只保留 `islandLayout.version` 可避免两套版本号分叉。
- **风险**:迁移写盘与"用户正在编辑"竞争;必须保证迁移只在 `app.whenReady()` 后、岛窗口创建前执行一次,失败时保留原文件并让应用以默认 v2(只读)启动。
#### 4.3.13 作业指纹链路(本机刚录作业不上岛)
- **现状**:渲染层 `src/renderer/island/bridge.ts` 用"内容指纹 + id"避免本机录入的作业弹上岛——`homeworkFingerprint()` 由 `classId|assignDate|title|content` 拼成,`markHomeworkCreatedLocally()` 在发请求前登记指纹、回来后补记 id,`isLocalHomework()` 在 `pushHomeworkToIsland()` 前判定;指纹 TTL 60 秒。依据:`bridge.ts:191-267`;调用点在 `src/renderer/views/HomeworkView.vue:419-428`(录入前/后登记)。
- **目标(双层去重,职责明确)**:
1. **渲染层(快速路径)**:保留现有 `isLocalHomework()` 判定,命中则根本不发 `islandPush`;`markHomeworkCreatedLocally()` 调用点保留,不改交互时序。
2. **主进程(兜底去重)**:`island:push` payload 增加 `source: 'realtime'|'homework-local'|'replay'|'call'` 与 `dedupeKey`(作业用同一指纹;普通通知/叫人用 `source+id`);`main/island/ipc.ts` 校验字段、`main/island/messages.ts` 维护 LRU 去重表(key→过期时间,TTL 60s),命中直接丢弃并记 debug 日志。
3. **IPC 归属**:渲染层 `bridge.ts` 计算并传 `source/dedupeKey` → preload `islandPush` 原样透传 → 主进程 `main/island/ipc.ts` 校验 → `messages.ts` 去重后入队。不得在 preload 里做业务判断,也不得让主进程反查渲染层 store。
- **接口**:`IslandPushPayload`(`packages/shared/src/island/ipc.ts`)新增两个可选字段;旧调用点缺字段时主进程按 `id` 去重(兼容)。
- **为什么**:现状只有渲染层一层判定,实时事件先于接口响应到达时仍可能漏判;主进程兜底可保证"同一台机器录的作业绝不上岛",且离线补弹/重连重复投递也被覆盖。
- **风险**:指纹跨班/跨天误伤(必须带 classId+assignDate);主进程去重表不能无限增长(LRU+TTL);冒烟需要能注入/清理去重表(提供 `testResetHomeworkDedupe()` 或在冒烟模式禁用去重,名称归 part 11 测试基线)。
### 4.4 IPC 契约 v2(增量、不改旧通道语义)
| 通道 | 方向 | 现状 | v2 变更 |
|---|---|---|---|
| `island:get-layout` | 岛→主 | 返回 `islandLayout` | 返回 v2(`version:2`;含 fixed/schemes) |
| `island:save-layout` | 岛→主 | 归一化后保存 | 先 `normalizeIslandLayoutV2`,返回 v2 |
| `island:state` | 主→岛 | `IslandState` | 增加 `family/pendingStaged/bubblePhase/bubbleDeadline`(运行期,不落盘);旧字段不删 |
| `island:appearance` | 主→岛 | `IslandAppearance` | 增加 `family/colorScheme/theme/effectEnabled/effectScale/progressBar/winislandStyle`;旧字段保留 |
| `island:push` | 应用窗口→主 | `{notification, context}` | 增加 `source`/`dedupeKey`(作业指纹去重,见 4.3.13);缺省按 `id` 去重 |
| `island:action` | 岛→主 | 8 种动作 | 保留;新增 `open-message`(classisland fixed 行内气泡打开详情)与 `open-notifications`(「还有 N 条」打开应用内通知中心) |
| `island:content-size` | 岛→主 | 自定义尺寸上报 | 增加 `mode:'custom'|'classisland-fixed'` |
| `classisland:island:get-metrics` | 新 | — | 返回当前 layout/family 的 metrics(编辑/冒烟/调试用) |
| `classisland:island:editor-open` | 主→应用窗口 | 已有 `island:open-editor` | 保留旧通道;新通道可带 `{rowId,nodeId}` 定位 |
| `island:set-hit-rect`/`island:set-interactive` | 岛→主 | 已有 | 语义不变;fixed 信息行按真实行矩形上报 |
1. payload 可携带 `schemaVersion:2` **仅用于运行时校验/调试**,**不得落盘**;落盘版本只有 `islandLayout.version`。接收方遇到未知字段必须忽略而不是拒绝。
2. 通道名继续集中在 `src/main/channels.ts` 或 `shared/island/ipc.ts` 的**单一常量表**中;preload 不得直接写字符串字面量。
3. 旧通道(`island:get-layout` 等)不得改名/删除;改名只能通过"新通道 + 旧通道转发"完成,至少保留一个版本周期。
4. 所有冒烟注入/读取通道(`island:set-interactive`、`island:set-hit-rect`、`island:set-touch-mode`、`island:alive` 及主进程方法 `setTestInputPassthrough/setHitTestCursor/getWindowBounds/getHitRect/getInteractive/setBlurCollapseEnabled/setTouchMode/getTouchMode/isRecovering`)名称与语义保持不变,改造归属 part 11 测试基线。
### 4.5 迁移与门面的落地顺序(必须按序提交)
1. **M0 建契约层**:`shared/island/` 类型、默认值、normalize、migrate、metrics、geometry、messages、lessons、ipc;只加不删,旧 `island-layout.ts`/`constants.ts` 保持再导出。
2. **M1 拆主进程**:`main/island/*` 拆分 + 门面;行为与现状 1:1(不改变任何可见行为),用无头 `verify:desktop` 回归。
3. **M2 迁移层接入**:`config.ts` 读时迁移 + 版本号;用旧配置夹具验证(fixed/custom 各存一份)。
4. **M3 渲染层抽壳**:`shell/IslandShell.vue`、`MessagePanel.vue`、`LessonBar.vue`;winisland 外观不变。
5. **M4 新增 classisland fixed**:`FixedInfoRow.vue` + `InlineMessageBubble.vue` + `IslandSettingsView` family 选择器;默认新装走 classisland,老配置走 winisland;family 只影响 fixed。
6. **M5 自定义行改造**:`CustomIslandRuntime` 行级外观/隐藏规则/进度条/滚动 mode/行设置抽屉;custom 始终 ClassIsland 规格(老 custom 用户升级后有视觉变化,需在发布说明提示)。
7. **M6 编辑器改造**:v2 数据、行设置、预览、保存队列。
8. **M7 收尾**:CSS/文档/冒烟断言/迁移回滚演练。
每一步都必须"可独立回滚":新增文件先经 feature 开关(`appearance.family`/`config.islandLayout.version`)控制;不得在某一步同时删除旧文件与旧逻辑。
### 4.6 风险清单与强制缓解
| 风险 | 触发点 | 缓解(必须实现) |
|---|---|---|
| 拆分 `island.ts` 导致隐藏状态丢失 | M1 | 先对齐私有字段到接口;拆分后逐项对比 `getState()/getWindowBounds()/getHitRect()` 冒烟输出 |
| DOM/CSS 选择器断裂 | M3/M4 | 保留 `.island-card/.island-root/[data-anchor]/[data-style]` 钩子;视觉回归由 --visual 专项做 |
| 两套 family 逻辑分叉 | M4 | 业务状态机只保留一份(controller);family 只选择 fixed 渲染器,custom 不分叉 |
| fixed 语义变化引起老用户困惑 | M4 | 老配置迁移默认 `winisland`;设置页显著位置显示 family 并解释两种 fixed;custom 升级有视觉变化的提示 |
| custom 用户升级后观感变化 | M5 | 统一 ClassIsland 规格是定稿;发布说明+设置页提示;数据不变、可回滚 config 备份 |
| 迁移把用户自定义方案写坏 | M2 | 迁移前备份 `config.json`;迁移失败保留原文件;提供"恢复旧版布局" |
| 编辑器 855 行继续膨胀 | M6 | 先抽 composables;单文件不超过 500 行;新逻辑进 `renderer/island/editor/` |
| 非 Windows 触摸命中退化 | M1 | `setShape` 仅 win32 分支;其它平台走"整窗穿透 + 可见区命中"(02 §2.4 第 5 条) |
| 性能(行数/组件数增加) | M5 | metrics 计算缓存;测量只在依赖变化时执行;列表/行使用 `will-change` 谨慎 |
| 无头验证覆盖不到视觉 | M7 | 默认 quick 保持无头;视觉断言只在显式 `--visual` 下跑,并在文档写明 |
### 4.7 验收标准(第 4 部分)
1. 仓库中存在 `packages/shared/src/island/` 与 `packages/desktop-client/src/main/island/` 两个新目录,且 `src/main/island.ts` 只剩门面(≤50 行,内容为 import/export 与类型再导出)。
2. `src/main/index.ts`、`tray.ts`、`courseware.ts`、`smoke.ts` 对 `./island.js` 的 import 与调用**不需要改动**(门面签名兼容)。
3. `LayoutIslandApp.vue` 分发正确:`custom` 始终 `CustomIslandRuntime`(与 family 无关);`fixed` 在 classisland 下用 `FixedInfoRow`、winisland 下用 `ClassicIsland`;切换不新建第二个窗口、不残留旧命中矩形。
4. `fixed` 在 `family='classisland'` 下渲染为信息行(默认 1 行:日期 + 课程表;行高 40、圆角 8、背景不透明度 0.5、`progressBar.height` 默认 3px、消息行内气泡);在 `family='winisland'` 下保留原胶囊+详情卡行为与既有冒烟断言。
5. `CustomIslandRuntime` 支持行级外观/隐藏规则/进度条,行设置抽屉可打开并保存;滚动组件支持 `mode:'continuous'|'pause'`;`rows`/`expandedRows` 编辑语义与现状一致。
6. 外观新字段全部落地:`family`(只影响 fixed)、`colorScheme`、`theme`、`effectEnabled`、`effectScale`、`progressBar`、`winislandStyle`;`fontSize` 在 classisland 下为正文基准(新装 16)、winisland 下旧语义(13,迁移保留原值)。
7. 编辑器 / 设置页 / 组件设置全部读写 v2 数据;旧 v1 配置在首次启动被迁移且不丢字段(迁移细节按 05 验收)。
8. 所有新增/修改的 IPC 通道登记在单一常量表;旧通道全部保留;preload 未暴露 `ipcRenderer`;`island:push` 带 `source/dedupeKey` 且主进程有兜底去重表。
9. 默认 `pnpm verify:desktop`(无头 quick)通过;不得因为重构把任何断言改成"跳过";冒烟注入点名称/语义全部保留。
10. 代码中不存在 `setBackgroundMaterial`;不存在绕过 contextBridge 直接访问 Node 的渲染层代码;custom 不受 family 影响、不引用 winisland 样式命名空间。
11. 作业指纹链路双层生效:本机录入后 60 秒内,实时事件/接口响应先后到达都不产生岛消息;跨班/跨天不误伤;去重表有 TTL 上限且可被冒烟清理。
12. 文件级改造清单(4.2/4.3)逐项落地;`docs/app/island.md`、`docs/dev/implementation.md` 的 family/fixed/custom 描述与实现一致。