Files
classhelper-island-refactor…/parts-r/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

40 KiB
Raw Blame History

4. 目标架构与文件级改造清单

4.0 重构原则(必须逐条遵守)

  1. 只在现有仓库内重构:不得新建工程、不得换框架、不得引入服务端依赖;沿用 pnpm@11.8.0 monorepo 与 Electron ^44.3.0 + Vue ^3.5.42 + Element Plus ^2.14.5 + Pinia ^4.0.3 + Vite ^8.3.0 + TS ~5.9.3。
  2. 渐进式替换,不一次性推倒:每次改动后应用必须仍可启动、旧 config.json 仍可加载;用"门面(facade)+ 新模块"的方式把旧调用点隔离,避免一次改 src/main/index.ts、tray.ts、smoke.ts 的全部 import。
  3. 单一事实来源:任何"主进程与渲染进程各算一次"的数值(窗口包围盒、列表高度、命中矩形、细缝尺寸、字号推导)必须收口到 packages/shared/src/island/,两端只调用同一函数。现状三处漂移见 02 §2.3。
  4. 保留现有业务语义:消息去重、上课暂存、紧急展开、已读回执、作业免打扰指纹、课件覆盖优先级、离线补弹,全部保持(02 §2.5/§2.12);ClassIsland 只改变"视觉与布局表现"。
  5. 两套风格族(family)共存,且 family 只作用于 fixed:appearance.family 取值 'classisland' | 'winisland';默认 classisland;winisland 保留原胶囊+详情卡外观;老用户升级默认 winisland(迁移见 05)。custom 布局始终按 ClassIsland 规格渲染,不受 family 影响(family 只决定 fixed 用哪套渲染器)。
  6. fixed 重新定义:在 family='classisland' 下,fixed 不再是胶囊+详情卡,而是 ClassIsland 风格信息行(默认 1 行 = 日期 + 课程表,行高 40、圆角 8、背景不透明度 0.5、3px 进度条、消息以行内气泡呈现)。原"胶囊+详情卡"只在 family='winisland' 下保留。这是本轮最重要的语义变化,所有文件改造必须按此分叉。
  7. 一个原生窗口:多种布局/风格共用一个 BrowserWindow;切换 family/layout 不得新开窗口(现状约束,见 docs/app/island.md:8-10、src/main/island.ts:468-634)。
  8. 不打扰纪律:默认验证无头/离屏,不创建可见岛或托盘;只有显式 --visual 才做桌面像素验收。依据:docs/dev/testing.md:9,29-32。
  9. 调色板与风格分离:style: black|glass|tinted 继续保留,写为 winisland 调色板字段(canonical appearance.winislandStyle,旧 style 作为兼容镜像,不删除);family 与 style 独立,切换 family 时保留用户 style 原值。
  10. 外观新增定稿字段:colorScheme: 'light'|'dark'|'system'(默认 system)、theme: 'fluent'|'classic'(默认 fluent)、effectEnabled(默认 true)、effectScale: 1|0.75|0.5(默认 1)、progressBar:{height:3,show:true};fontSize 语义随 family:classisland=正文基准(新装默认 16,派生 14/18/20),winisland=旧语义(默认 13,迁移保留原值)。
  11. 版本只保留一个:落盘版本只有 islandLayout.version=2;schemaVersion 仅允许出现在 IPC payload 作运行时校验,不得落盘。
  12. fixed 消息/提醒口径:行内气泡同一时刻只显示 1 条 active;多条折叠为「还有 N 条」并可打开应用内通知中心;prepare/on-class/break 以行内容接管呈现、ending 只用蓝色圆形秒数徽标;队列/去重/暂存规则不变。

4.1 目标模块边界与目录

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

packages/shared/src/island/                    # 新增:岛的唯一契约层(纯 TS,无 DOM/Electron)
├─ index.ts             # 统一导出(供 desktop-client 与 web-admin 可复用)
├─ schema.ts            # IslandLayoutV2 / IslandAppearanceV2 / IslandRowV2 / IslandNodeV2 类型
├─ defaults.ts          # ClassIsland 规格默认值(family/行/组件/外观)
├─ normalize.ts         # normalizeIslandLayoutV2 / normalizeAppearanceV2(校验+夹取)
├─ migrate.ts           # migrateIslandLayoutV1ToV2(旧 config → v2)
├─ metrics.ts           # CLASSISLAND_ISLAND_METRICS + WINISLAND_ISLAND_METRICS + 排版系数
├─ geometry.ts          # 锚点/包围盒/细缝/触摸贴合尺寸的纯函数(主/渲染共用)
├─ messages.ts          # 去重、排序、列表布局、行内气泡分组(纯函数)
├─ lessons.ts           # 课程提醒计算(从 main/lesson-reminder.ts 上移)
├─ dto.ts               # IslandState/IslandNotification/ScheduleDto 等对外 DTO 与映射
└─ ipc.ts               # IPC 通道名与 payload 类型(主进程与 preload 共用)

packages/desktop-client/src/main/island/       # 新增:主进程岛引擎(从 island.ts 拆出)
├─ index.ts             # 门面:export { island, registerIslandIpc, ... }(旧 import 不变)
├─ controller.ts        # 状态机编排:消息、上课状态、预览、动作分发
├─ window.ts            # 建窗、固定包围盒、锚点布局、可见性/焦点/透明度
├─ hit-test.ts          # 命中轮询、岛体矩形、interactive 决策(含 setShape 降级)
├─ touch.ts             # 触摸模式、贴合、失焦宽限、常驻控件
├─ messages.ts          # 队列存储 + 合并 + 超时计时器(无 UI)
├─ lessons.ts           # 每秒推进课程提醒
├─ weather.ts           # 从 island-weather.ts 移入
├─ courseware.ts        # 课件覆盖状态桥
├─ preview.ts           # 设置页"测试灵动岛"的预览注入
└─ ipc.ts               # island:* 全部 handler(从 island.ts 尾部搬来)

packages/desktop-client/src/island/            # 渲染进程(现有目录内重组)
├─ main.ts              # Keep:tokens + motion + mount
├─ LayoutIslandApp.vue  # Change:分发(custom→CustomIslandRuntime;fixed 按 family 二选一)
├─ family/
│  ├─ classisland/
│  │  ├─ FixedInfoRow.vue        # 新增:ClassIsland 信息行(fixed 新默认)
│  │  ├─ InlineMessageBubble.vue # 新增:行内消息气泡(fixed 下替代详情卡)
│  │  └─ ClassislandTheme.css    # 新增:Fluent/Classic 两套行样式与动画
│  └─ winisland/
│     ├─ ClassicIsland.vue       # Change:由 IslandApp.vue 迁移并改名(原胶囊+详情卡)
│     └─ winisland.css           # Change:由 classic-island.css 迁移(数值不变,仅 fixed 使用)
├─ custom/
│  ├─ CustomIslandRuntime.vue    # Change:始终按 ClassIsland 行/组件规格重写测量与适配(不看 family)
│  ├─ CustomRows.vue             # 新增:常态行 / 展开行渲染(行级外观)
│  ├─ CustomRow.vue              # 新增:单行(行设置、隐藏规则、进度条)
│  └─ CustomRowSettingsDrawer.vue# 新增:行设置抽屉(背景/字号/圆角/隐藏;字段由 09 定义)
├─ shell/
│  ├─ IslandShell.vue            # 新增:squircle 形状 + 弹簧形变 + 命中上报(fixed 两族共用)
│  ├─ MessagePanel.vue           # Change:由 ClassicNotifications.vue 改造(单条/列表/行内气泡)
│  └─ LessonBar.vue              # Change:由 LessonReminderContent.vue 改造(ClassIsland 遮罩)
├─ components/                   # 新增:按 kind 拆分的组件渲染器
│  ├─ index.ts                   # node.kind → 渲染器映射
│  ├─ DateNode.vue / ClockNode.vue / TextNode.vue / CountdownNode.vue
│  ├─ ScheduleNode.vue / WeatherNode.vue / SeparatorNode.vue
│  └─ ContainerNode.vue          # group/stack/slide/rolling(rolling 支持 mode continuous|pause)
├─ motion/                       # Keep:spring.ts、squircle.ts(可加适配器)
├─ styles/                       # 新增:shared.css / classisland.css / custom.css(custom 始终 classisland 命名空间)
└─ Legacy/                       # 可选:保留旧文件名再导出门面(ComponentNode.vue 等)

packages/desktop-client/src/renderer/island/   # 主窗口侧的桥(现有)
├─ bridge.ts             # Keep:数据桥(课表/上课状态/通知/作业/叫人/补弹/已读)
└─ editor/               # 新增:编辑器逻辑 composables(从 IslandEditorView 抽出)
   ├─ useIslandDraft.ts  # 草稿、脏标记、保存队列、修订号
   ├─ useIslandDrag.ts   # 指针拖拽/落点/自动滚动
   └─ useIslandPreview.ts# 内嵌预览缩放与测量

packages/desktop-client/src/renderer/          # 设置/编辑器页面(现有)
├─ views/settings/IslandSettingsView.vue      # Change:family 选择器 + 条件外观
├─ views/settings/IslandEditorView.vue        # Change:调用新版架构 + 行设置
├─ components/IslandComponentSettings.vue     # Change:ClassIsland 组件字段
├─ components/IslandEditorNode.vue            # Keep(可加行级拖拽)
├─ components/IslandRowSettingsDrawer.vue     # 新增:行设置抽屉
└─ styles/island-editor.css                   # Change:ClassIsland 编辑器视觉

4.2 文件级 Keep / Change / Add / Remove 总表

文件/目录 类型 一句话目标
packages/shared/src/island-layout.ts Change 保留导出名兼容;实现迁移到 island/ 新目录,本文件仅再导出
packages/shared/src/constants.ts Change 保留旧 WinIsland 常量;新增 ClassIsland 度量与 family 常量(迁移见 05)
packages/shared/src/types.ts Change IslandAppearance 增加 family/colorScheme/effectEnabled/effectScale/progressBar/theme/winislandStyle;新增 IslandState.family/pendingStaged/bubblePhase/bubbleDeadline、IslandNotification.level、IslandLessonReminder.progress/leftSeconds(运行期,不落盘)
packages/shared/src/island/ipc.ts Add IPC 通道名与 payload 类型;island:push 增加 source/dedupeKey
packages/shared/src/motion.ts Keep 不动令牌数值(03/10 部分引用)
packages/shared/src/class-plan.ts Keep IslandTimePointDto/TimePointType 不动
src/main/island.ts Change 2445 行 → 门面(约 30 行),实际逻辑移入 main/island/*
src/main/island/*.ts Add 见 4.1(controller/window/hit-test/touch/messages/lessons/weather/courseware/preview/ipc)
src/main/config.ts Change 启动时执行 v2 迁移钩子(唯一版本=islandLayout.version);写盘前校验 family/colorScheme/fontSize/effect*;保留合并写/退出 flush
src/main/index.ts Change island.init() 改调新门面;注册编辑器/预览所需 IPC
src/main/ipc.ts Change 岛相关 handler 全部委托 main/island/ipc.ts;主窗口配置剔除规则保留
src/main/courseware.ts Change 改为调用 island.setCoursewareTransfer(门面保留同名方法)
src/main/tray.ts Change 菜单文案与入口不变;island.editLayout() 改由新控制器实现
src/main/island/messages.ts Add 消息队列 + 合并 + 超时 + 作业指纹/去重表(见 4.3.13)
src/main/island/ipc.ts Add 校验 island:push 的 source/dedupeKey,投递前查重
src/main/smoke.ts Change 保留全部既有读取器与注入点(名称/语义不变,见 4.7 第 11 条);新增 v2 断言(行高/行数/气泡/迁移);改造归属 part 11 测试基线
src/preload/index.ts Change 新增 family/colorScheme 感知的配置读写;islandPush 透传 source/dedupeKey;保留全部旧方法名
src/preload/island.ts Change 新增行/编辑器预览相关事件;旧事件名与语义不变
src/types/desktop.d.ts Change 配置形状加外观新字段与 islandLayout.version;新增 v2 DTO 类型;islandPush payload 扩展
src/island/LayoutIslandApp.vue Change 分发:custom → CustomIslandRuntime;fixed 按 family 二选一(见 4.3.1)
src/island/IslandApp.vue Change 迁出为 family/winisland/ClassicIsland.vue;旧文件保留再导出(防断裂)
src/island/CustomIslandRuntime.vue Change 始终按 ClassIsland 行/组件规格重写测量、行渲染、行设置抽屉;不受 family 影响
src/island/ComponentNode.vue Change 拆为 components/*Node.vue;本文件保留兼容导出;滚动组件新增 `settings.mode('continuous'
src/island/ClassicNotifications.vue Change 改为 shell/MessagePanel.vue;新增行内气泡模式(同一时刻 1 条 active)
src/island/LessonReminderContent.vue Change 改为 shell/LessonBar.vue;遮罩参数按 ClassIsland 重算;fixed 下 prepare/on-class/break 行内容接管、ending 仅徽标
src/island/CoursewareTransfer.vue Keep 课件覆盖卡保持现状(424×132 等,见 02 §2.10)
src/island/WeatherBrief.vue Keep 天气简报保持现状(Open-Meteo/15 分钟/缓存标记);本轮不引入 ClassIsland 天气服务
src/island/spring.ts、squircle.ts Keep 弹簧与连续圆角数值不动(10 部分仍按此写)
src/island/classic-island.css Change 改名 family/winisland/winisland.css(fixed 专用);数值不变,选择器按新 DOM 调整
src/island/custom-runtime.css、custom-island.css Change 移到 styles/custom.css 与 custom/*;始终按 ClassIsland 行规格重写(custom 不受 family 影响)
src/island/lesson-ending.css Keep 秒数徽标样式不动
src/island/main.ts Keep 入口与令牌注入顺序不动
src/renderer/views/settings/IslandSettingsView.vue Change 顶部加 family 选择器(只影响 fixed);新增 colorScheme/theme/effectEnabled/effectScale/progressBar;fontSize 语义随 family 展示
src/renderer/views/settings/IslandEditorView.vue Change 数据改为 v2;行设置抽屉;预览按 ClassIsland 规格;保存队列/上限保留;拆分节奏按 09 文件计划(见 4.3.11)
src/renderer/components/IslandComponentSettings.vue Change 组件字段与 ClassIsland 规格对齐(含滚动 mode);旧字段保留只读兼容
src/renderer/components/IslandEditorNode.vue Keep 标签节点可复用;若加行级拖拽则小幅 Change
src/renderer/components/IslandRowSettingsDrawer.vue Add 行级设置的唯一入口(字段清单由 09 定义)
src/renderer/island/bridge.ts Change 作业投递增加 `source:'homework-local'
src/renderer/views/HomeworkView.vue Keep markHomeworkCreatedLocally 调用点保留(录入前/后登记指纹,见 4.3.13)
src/renderer/styles/island-editor.css Change 编辑器视觉按 ClassIsland(间距/分隔/虚线框/预览画布)
packages/desktop-client/scripts/smoke.mjs Change 视觉/几何断言改为按 family 分套;默认 quick 仍无头
docs/app/island.md、docs/dev/implementation.md Change 口径更新为 family 与 fixed 新语义(文档不是代码,但必须与实现同步)

4.3 关键文件改造详情(逐个文件:现状 → 目标 → 接口 → 风险)

4.3.1 src/island/LayoutIslandApp.vue(Change,按 layout + family 分发)

  • 现状:只按 layout==='custom' 二选一(CustomIslandRuntime / FixedIsland),课件优先覆盖。依据:packages/desktop-client/src/island/LayoutIslandApp.vue:56-75。
  • 目标(定稿分发规则):
    • layout==='custom' → 始终 custom/CustomIslandRuntime.vue(ClassIsland 行/组件规格),不看 family;
    • layout==='fixed' 且 appearance.family==='classisland' → 新增 family/classisland/FixedInfoRow.vue(信息行 + 行内气泡);
    • layout==='fixed' 且 appearance.family==='winisland' → family/winisland/ClassicIsland.vue(原胶囊+详情卡,行为不变);
    • 课件传输仍最高优先级,覆盖以上所有分支。
  • 接口:props 保持 settings/state/schedules/timePoints/appearance/nativeVisible;新增计算 family = appearance.family ?? 'classisland' 仅用于 fixed 分支;不新增 IPC 通道。
  • 为什么:family 只影响 fixed,custom 不需要两套渲染;分发规则收窄后,custom 的数据与视觉可以独立演进。
  • 风险:切换 family 只改变 fixed 渲染器,custom 不受影响——必须保证切换时若当前 layout=custom 不重建 custom 运行时(避免方案展开态/测量状态丢失);切换命中矩形时 reportHit(null) 并 syncTargets(true)。

4.3.2 src/island/IslandApp.vue → family/winisland/ClassicIsland.vue(Change,大改)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

4.6 风险清单与强制缓解

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

4.7 验收标准(第 4 部分)

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