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

386 lines
64 KiB
Markdown
Raw Permalink 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.
## 9. 编辑器与设置页重构(IslandSettingsView / IslandEditorView / IslandComponentSettings)
本 part 规定「灵动岛设置页」与「自定义方案编辑器」在重构后的完整实现。所有改动**必须在现有 ClassHelper 仓库内完成**,沿用 pnpm monorepo、Electron 44、Vue 3.5、Element Plus 2.14、Pinia、Vue Router、Vite 与既有 `@classhelper/shared` 契约;**不得新建工程、不得引入 Avalonia/C#、不得新增服务端依赖、不得新建额外桌面窗口来做编辑预览**。所有数值以 `packages/...:行号` 标注证据;ClassIsland 侧以 `repos/ClassIsland/...:行号` 标注;无法从源码确认的写【推断】。
### 9.1 口径与术语(与 parts-r/01、05、07 对齐)
1. **`family` 是 fixed 形态的外观族**:`islandAppearance.family: 'classisland' | 'winisland'`,默认 `'classisland'`(新安装默认)。两种取值只改变 **fixed** 的外观:
- `classisland` + `fixed`:**ClassIsland 风格信息行**,默认 1 行 = 日期 + 课程表,行高 40、圆角 8、背景不透明度 0.5、底部 3px 进度条,消息到来时在同一行内展开 ClassIsland 风格气泡(业务规则仍用 ClassHelper 的暂存/紧急/叫人/已读/去重);
- `winisland` + `fixed`:保留「胶囊 + 详情卡」旧形态(兼容老用户);
- `classisland` + `custom` 与 `winisland` + `custom`:**都使用 ClassIsland 规格的行/组件**(行高 40、行内留白 12、组件契约见 parts-r/07、编辑模式见本 part 9.9–9.16),`family` 对 custom 不产生影响。
**禁止**把 `classisland` 下的 fixed 写成小胶囊;**禁止**让 custom 因 `family=winisland` 回退成旧 custom 观感。
2. **`layout` 与 `family` 正交**:`layout: 'fixed' | 'custom'` 决定「消息行 / 常驻组件方案」与是否开放编辑器;`family` 只决定 **fixed** 的视觉族。设置页必须在 `layout === 'custom'` 时把 `family` 控件置灰,并显示「自定义方案始终使用 ClassIsland 规格;外观族只作用于经典 fixed 形态。」;`layout === 'fixed'` 时 `family` 可用且实时生效。
3. **升级口径**:老配置文件没有 `island.family` 字段时,必须迁移为 `'winisland'`(保持用户既有 fixed 胶囊观感;custom 始终 ClassIsland 规格,迁移不影响);新安装/清空配置时用 `'classisland'`(`fontSize` 默认 16)。**迁移必须保留用户原有 `appearance.fontSize` 数值**(不因 family 迁移改成 13 或 16);若旧配置无该字段,按迁移后的 family 取默认(winisland=13 / classisland=16)。迁移实现见 9.18,需与 parts-r/05 的 `islandLayout.version` 迁移函数同批落地。
4. **术语**:组件(`IslandComponentNode`)、行(`IslandLayoutRow`)、方案(`IslandLayoutScheme`)、常态内容(`rows`)、无通知展开内容(`expandedRows`)、外观(`IslandAppearance`)。组件种类集合固定为 11 类编辑器可见:`date/clock/weather/countdown/schedule/text/separator/slide/rolling/group/stack`;消息类 `notification/homework/call` 不在组件库露出,仅兼容既有节点(`packages/shared/src/island-layout.ts:3-18`、`:45-60`;`packages/desktop-client/src/renderer/views/settings/IslandEditorView.vue:58-70`)。
5. **fixed 的内置信息行 v2 是只读内置方案,不可编辑**:`classisland` + `fixed` 固定为 1 行 = 日期 + 课程表,该行属于**内置方案**,**不进入方案编辑器**(不占 `schemes/rows/nodes`,编辑器只服务 custom)。设置页对 fixed 只提供 9.5 的外观参数与 9.4-4 的静态缩略示意,**不得**提供行/组件编辑入口、不得出现「编辑固定行」按钮。后续版本若开放 fixed 行编辑,必须升级 `islandLayout.version` 并在 parts-r/05 增补契约;v2 不开放。
6. **`editingLayout` 死标志不属本 part**:`main/island.ts` 中该标志只有读取点、无写入点的问题归 parts-r/04 处理;本 part 只规定“编辑模式由主窗口路由页承担、岛窗口不新增编辑态”,`island.ts` 的清理以 parts-r/04 为准,09 不修改该文件。
### 9.2 文件级改造清单(只列本 part 负责的文件)
| 文件 | 改造类型 | 必须做的事 |
|---|---|---|
| `packages/desktop-client/src/renderer/views/settings/IslandSettingsView.vue` | 大改 | 重排信息架构;新增 `family` 切换与两套外观参数;把课程提醒设置并入本页;保留实时预览/保存/恢复默认/预览效果 |
| `packages/desktop-client/src/renderer/views/settings/IslandEditorView.vue` | 中改 | 保留全部既有编辑能力;行/画布/选中框/抽屉/组件库按 ClassIsland 编辑模式对齐;组件设置抽屉拆为「内容/外观/尺寸/隐藏条件」四页 |
| `packages/desktop-client/src/renderer/components/IslandComponentSettings.vue` | 大改 | 四页签结构;补齐 11 类组件的全部设置项;字段/控件/默认值/范围见 9.15 |
| `packages/desktop-client/src/renderer/components/IslandEditorNode.vue` | 中改 | 选中框改为 1.5px 虚线 1/1.5;容器虚线;组件外框左右各 6px;拖拽手感与 ARIA 保持 |
| `packages/desktop-client/src/renderer/styles/island-editor.css` | 中改 | 行高 40、行距 scheme.spacing、组件外边距 6、抽屉 250ms 曲线、缩放/落点/ghost 样式 |
| `packages/desktop-client/src/renderer/styles/index.css` | 小改 | 设置页新增的 `.island-family-*`、`.island-preview-thumb` 等局部样式;复用既有 `--ch-*` 令牌(`index.css:21-54`、`:103-131`) |
| `packages/shared/src/island-layout.ts` | 中改 | `IslandAppearance` 扩展字段的默认值与 normalize 边界;`normalizeIslandLayouts` 的 12 行/200 节点/6 层/20 方案上限保持(`:256-403`)——字段最终名以 parts-r/05 为准 |
| `packages/desktop-client/src/main/config.ts` | 小改 | 外观新字段迁移(老用户 family=winisland)与 normalize;沿用 120ms 合并写盘(`:252-298`) |
| `packages/desktop-client/src/main/ipc.ts` | 不改逻辑 | 确认 `saveConfig` 收到 `islandLayout` 后调用 `island.syncCustomLayout()`(`:34-45`);外观仍走 `island:set-appearance`/`island:get-appearance`(`:2389-2410`) |
| `packages/desktop-client/src/types/desktop.d.ts` | 小改 | `saveConfig`/`islandSetAppearance` 的纯数据注释保留(`:143-175`) |
**不得删改** `main/island.ts` 的窗口/命中/几何逻辑;编辑器与设置页只能通过既有 IPC 影响它(本 part 不承担岛渲染)。
### 9.3 设置页信息架构(重构后)
设置页仍挂在 `/settings/island`(`packages/desktop-client/src/renderer/router/index.ts:63-68`),侧栏标题「灵动岛」(`layouts/ClientLayout.vue:48-55`)。页面外层沿用 `.page`(`padding: 16px 20px 24px`)、`.page-header`(`margin-bottom: 12px`)、`.page-title`(`20px/600`)(`styles/index.css:186-204`),整体改为「页头 → 布局形态卡 → 外观卡 → 课程提醒卡 → 指针与窗口卡 → 底部操作条」,每张卡为 `el-card shadow="never"`(圆角 8、1px 描边、`backdrop-filter: blur(20px) saturate(1.1)`,`index.css` 的 `.el-card` 规则)。
1. **页头**:左侧 `h2`「灵动岛」;右侧显示当前形态徽标 `el-tag`:`经典信息行`(family=classisland、layout=fixed)/ `自定义常驻`(layout=custom)/ `经典胶囊(兼容)`(family=winisland、layout=fixed)。徽标随保存结果更新,不额外请求。
2. **布局形态卡**(保留既有两张选择卡并升级文案与点击):
- 左卡「经典灵动岛」:`family=classisland` 时描述必须改为「ClassIsland 风格信息行:日期 + 课程表,课中进度条;消息在同一行内展开,v2 为只读内置方案」;`family=winisland` 时保留现有描述「显示课程进度、上下课提醒与班级消息」;
- 右卡「自定义灵动岛」:描述保留「ClassIsland 风格,常驻显示时钟、课程等组件」(custom 始终 ClassIsland 规格,与 family 无关);
- 卡片样式沿用 `.layout-choice`(padding 20、圆角 12、选中 `border-color: var(--el-color-primary)` + `background: var(--el-color-primary-light-9)`,`:450-478`);点击调用 `chooseLayout('fixed'|'custom')`,立即写 `saveConfig({ islandLayout: { ...config.islandLayout, layout, residentEnabled: mode === 'custom' } })`(现有实现 `IslandSettingsView.vue:44-59` 保留);
- 选择 `custom` 后追加「编辑入口」块(现有 `.layout-edit-entry`,`:192-198`、`:479-496`):说明「编辑常态内容与无通知展开内容;课前自动提示,通知操作与经典岛共用。」+ 主按钮「编辑岛内组件」→ `router.push('/settings/island/editor')`;
- 选择 `fixed` 后**不得**出现任何行/组件编辑入口:fixed 的 classisland 信息行是只读内置方案(见 9.1-5),卡片说明里必须写明「固定信息行由内置方案渲染,暂不支持编辑」。
3. **外观卡**:标题「外观、位置与弹出」保留;顶部说明两段改为:
- `classisland`:「默认信息行高度 40、圆角 8、背景不透明度 0.5;紧急消息立即展开,课中普通通知暂存、下课后展开;自定义岛可点击铃铛主动查看。」
- `winisland`:保留现有「紧急消息立即展开;课中普通通知暂存,下课后展开。自定义岛可点击铃铛主动查看。」
- 卡内第一行必须是 **视觉风格(family)** 控件(9.4),随后按 family 分支渲染参数组。
4. **课程提醒卡**:新增(内容见 9.7),单一数据源固定为 `config.json.islandLayout.lessonReminders`;原 `/settings/reminder` 页的「上下课提醒」只保留只读摘要 + 跳转,不得保留可编辑控件(避免双写)。
5. **指针与窗口卡**:`layout=custom` 时显示「指针避让」整块(现有 `.resident-window-options`,`:199-218`、`:423-444`);`classisland` fixed 下增加「触摸后自动恢复」说明【推断:由 ClassHelper 触摸失焦宽限承担,无独立参数】。
6. **底部操作条**(保留现有):`保存设置`(primary,loading)、`预览效果`、`恢复默认`;下方 InfoBar「拖动滑块即为实时预览;保存后写入本地配置,重启客户端仍然生效」(`:385-395` 原样保留,文案可保留)。
### 9.4 视觉风格(family)与两套参数组
1. **控件(必做)**:`el-segmented`(Element Plus 2.14 可用)或两个 `el-radio-button`,标签:
- `ClassIsland 风格`(value `classisland`,默认):「信息行 / 半透明横条,行高 40、圆角 8、底部进度条;正文基准字号 16」;
- `经典 WinIsland(兼容)`(value `winisland`):「保留胶囊 + 详情卡外观,仅建议老用户保持;岛字号默认 13」。
下方常驻一行 Warning 说明(`el-alert type="warning"`,不可关闭):「从旧版本升级的配置默认使用经典 WinIsland 外观;切换到 ClassIsland 风格会改变经典形态的外观,消息暂存/紧急/叫人等规则不变。」——这就是**老用户升级默认 winisland** 的用户侧说明。
2. **切换行为**:`family` 写入 `islandAppearance.family`,通过 `applyIslandAppearance({ family })` 实时应用(`islandSetAppearance`),点「保存设置」才写 `config.island`。切换不得重置另一族的参数值(两族参数分别保留在 `IslandAppearance` 中)。
**字号语义随 family 切换**:`classisland` 下 `appearance.fontSize` 是**正文基准字号**,标签文案必须为「正文字号(ClassIsland 基准)」,默认 `16`,并由它派生 14/18/20 三档阶梯(次级/强调/大号);`winisland` 下保持旧语义,标签文案必须为「岛字号(经典)」,默认 `13`,岛内全部文本按旧系数缩放。切换 family 时必须切换标签文案与「默认值」提示(如滑块右侧显示「默认 16」/「默认 13」),但**不得**在切换瞬间强制改写用户已设置的值;仅当值为空/非法时才用当前 family 的默认值。
3. **可见性规则**:`layout === 'custom'` 时 `family` 控件 `disabled`,并显示「自定义方案始终使用 ClassIsland 规格;外观族只作用于经典固定形态。」;`layout === 'fixed'` 时按 `family` 显示下方参数组:`classisland` 显示 9.5,`winisland` 显示 9.6。
4. **固定信息行预览缩略**(新增,P1):在 `classisland` fixed 下于外观卡顶部放一个纯 CSS 缩略图(宽 360×高 44,圆角 8,背景 `#000000` + 不透明度 0.5,内含「10月17日 周五」「语文 08:00-08:40」两段文本与底部 3px 进度条),标注「示意:默认 1 行 = 日期 + 课程表;实际内容由日程数据驱动」。**禁止**用截图或额外窗口实现,必须是 DOM/CSS(与「编辑预览不创建额外桌面窗口」同源约束,`docs/app/island.md:47`)。
### 9.5 classisland 族外观参数(逐项)
所有控件改变时立即调用 `applyIslandAppearance({...})` 实时预览;`saveIslandAppearance()` 再写盘。范围/默认值证据见 `packages/shared/src/constants.ts:264-302`、`packages/shared/src/island-layout.ts:20-36`;新增字段以 parts-r/05 为准。
| # | 控件(label-width 110 左侧标签) | 字段 | 控件类型与范围 | 默认 | 生效时机 | 备注 |
|---|---|---|---|---|---|---|
| 1 | `界面缩放 ${scale}` | `appearance.scale`【新】 | `el-slider` 0.75–2.5,step 0.05,tooltip `F2` | 1 | 即时 | ClassIsland `Scale` 语义(`repos/ClassIsland/ClassIsland/Views/SettingPages/AppearanceSettingsPage.axaml:27-40`);`layout=custom` 时隐藏(缩放在编辑器方案设置内) |
| 2 | `圆角 ${radius}px` | `appearance.radius`(有则用;缺省取当前方案) | `el-slider` 0–20,step 1 | 8 | 即时 | `layout=custom` 时同时写 `activeScheme.radius` 并 `saveConfig`(保留现有同步,`IslandSettingsView.vue:38-40`、`:113-122`) |
| 3 | `背景不透明度 ${round(opacity*100)}%` | `appearance.opacity` | `el-slider` 0–1,step 0.05 | 0.5 | 即时 | `layout=custom` 时同步 `activeScheme.opacity` |
| 4 | `行间距 ${spacing}px` | `appearance.spacing`【新】 | `el-slider` 0–150,step 1 | 5 | 即时 | 与 `scheme.spacing` 同步(custom);ClassIsland `MainWindowLineVerticalMargin` 默认 5(`Models/Settings.cs:219`) |
| 5 | `主题色` | `appearance.accent` | `el-color-picker` | `#0a84ff` | 即时 | 用于高亮、按钮与进度条;ClassIsland 强调色默认 `#00BFFF`(`Settings.cs:35`),本实现沿用 ClassHelper 主题色 |
| 6 | `前景(文字)颜色` | `appearance.foreground`【新】 | `el-color-picker` + 开关「自定义」 | 关 / `#ffffff` | 即时 | 自定义前景色覆盖主题派生色(ClassIsland `IsCustomForegroundColorEnabled`+`CustomForegroundColor`,`AppearanceSettingsPage.axaml:536-551`) |
| 7 | `字体` | `appearance.fontFamily`【新】 | `el-select` 可搜索,默认项文案「HarmonyOS Sans SC(默认)」 | 默认字体 | 即时 | 选项来自系统字体或内置列表;ClassIsland 同(`:405-448`) |
| 8 | `字重` | `appearance.fontWeight`【新】 | `el-select`:Thin/ExtraLight/Light/Normal/Medium/SemiBold/Bold/ExtraBold/Black/ExtraBlack | Medium(500) | 即时 | ClassIsland `MainWindowFontWeight2` 默认 Medium(`Settings.cs:101`) |
| 9 | 次级字号(派生 14) | `appearance.secondaryFontSize`【新】 | `el-input-number` 8–96,后缀 px | 14(未显式设置时由 `fontSize` 派生阶梯给出) | 即时 | 时间点附加信息等;classisland 默认阶梯 14/16/18/20 |
| 10 | `正文字号(ClassIsland 基准) ${fontSize}px` | `appearance.fontSize` | `el-input-number` 8–96 或 slider 8–40,后缀 px | **16** | 即时 | **classisland 语义:正文基准字号**(`repos/ClassIsland/ClassIsland/Views/SettingPages/AppearanceSettingsPage.axaml:482-533` 四档字号以正文为基准);派生 14/18/20 三档;`bodyFontSize` 若存在则作为显式覆盖(以 parts-r/05 为准) |
| 11 | 强调字号(派生 18) | `appearance.emphasizedFontSize`【新】 | 同上 | 18(派生) | 即时 | 课程表、时钟、提醒标题、天气预报 |
| 12 | 大号字号(派生 20) | `appearance.largeFontSize`【新】 | `el-input-number` 8–128,后缀 px | 20(派生) | 即时 | 课程预报 |
| 13 | `分体显示` | `appearance.separated`【新】 | `el-switch` | 关 | 即时 | 行之间透明分隔;ClassIsland `IsIslandSeperated`(`Settings.cs:1852`) |
| 14 | `进度条` | `appearance.progressBar`【新】`{ show, height, color }` | 开关 + 高度 2–6px(默认 3)+ 颜色(默认强调色) | 显示/3px/强调色 | 即时 | ClassIsland 3px 进度条(brief §1.2);隐藏时行底不留空白 |
| 15 | `显示位置` | `appearance.position` | `el-select` 6 项(顶/底 × 左/中/右) | `top-center` | 即时 | 现有 `ISLAND_POSITIONS`;classisland fixed 默认 `top-center` |
| 16 | `左右边距` | `appearance.marginX` | `el-slider` 0–200,step 2,居中停靠时禁用 | 8 | 即时 | 仅左右停靠生效,保留现有禁用逻辑与解释(`IslandSettingsView.vue:88-99`、`:324-337`) |
| 17 | `上下边距` | `appearance.marginY` | `el-slider` 0–160,step 2 | 8 | 即时 | 停靠顶/底生效 |
| 18 | `跟随鼠标屏幕` | `appearance.followCursorDisplay` | `el-switch` | 关 | 即时 | 多显示器 |
| 19 | `动画` | `appearance.animations` | `el-switch` | 开 | 即时 | 关闭后展开/收起瞬时完成 |
| 20 | `动画速度` | `appearance.speed` | `el-slider` 0.5–2,step 0.1 | 1 | 即时 | 与 `spring.ts`/CSS 时长倍率联动(part 10) |
| 21 | `始终置顶` | `appearance.alwaysOnTop` | `el-switch` | 开 | 即时 | |
| 22 | `明暗主题` | `appearance.theme`【新】`'auto'|'light'|'dark'` | `el-select`:跟随应用/明亮/黑暗 | 跟随应用 | 即时 | 岛主题独立于主窗口 `config.theme`;ClassIsland 应用主题三态(`AppearanceSettingsPage.axaml:123-135`) |
新增字段(1/4/6/7/8/9–14/22)**必须**由 parts-r/05 定稿字段名与 normalize 边界;本表给出默认值,任何越界值在 `normalizeIslandAppearance` 内钳制,禁止出现 NaN(现有归一化参考 `main/island.ts:2229` 的注释与 `config.ts` 的 normalize 调用)。
**字号单一事实来源**:`appearance.fontSize` 在 classisland 族是正文基准(默认 16),四档字号中「正文 = fontSize」「次级/强调/大号 = fontSize - 2 / + 2 / + 4」;若用户显式修改次级/强调/大号,则写 `secondaryFontSize/emphasizedFontSize/largeFontSize` 覆盖派生值(字段最终由 parts-r/05 定)。`layout=custom` 时字号由编辑器方案与组件外观控制,设置页这几项隐藏。
### 9.6 winisland 族外观参数(兼容保留,不得删)
`family === 'winisland'` 时渲染下列全部控件,字段、范围、默认值、文案与现有实现一致,**禁止改变默认外观**(老用户回退依赖它):
| # | 控件 | 字段 | 范围/step | 默认 |
|---|---|---|---|---|
| 1 | `高度 ${height}px` | `appearance.height` | 36–72/1 | 44 |
| 2 | `宽度 ${width}px` | `appearance.width` | 220–420/2 | 268 |
| 3 | `圆角 ${radius}px` | `appearance.radius` | 8–32/1 | 20 |
| 4 | `不透明度 ${%}` | `appearance.opacity` | 0.4–1/0.05 | 1 |
| 5 | `岛字号(经典) ${fontSize}px` | `appearance.fontSize` | 11–20/1 | 13(旧版语义;新装默认由 family 决定为 16,见下) |
| 6 | `动画速度 ${speed}x` | `appearance.speed` | 0.5–2/0.1 | 1 |
| 7 | `主题色` | `appearance.accent` | 颜色 | `#0a84ff` |
| 8 | `视觉风格` | `appearance.style` | 纯黑 `black`/毛玻璃 `glass`/主题色渐变 `tinted` | `black` |
| 9 | `显示位置` | `appearance.position` | 6 项 | `top-center` |
| 10 | `左右边距` | `appearance.marginX` | 0–200/2,居中禁用 | 8 |
| 11 | `上下边距` | `appearance.marginY` | 0–160/2 | 8 |
| 12 | `跟随鼠标屏幕` | `appearance.followCursorDisplay` | 开关 | 关 |
| 13 | `动画` | `appearance.animations` | 开关 | 开 |
| 14 | `始终置顶` | `appearance.alwaysOnTop` | 开关 | 开 |
| 15 | `空闲细缝` | `appearance.idleSliver` | 开关 | 关 |
| 16 | `预览效果` | — | 按钮:`islandPush` 一条 `priority: 'NORMAL'`、`context.preview = true` 的本地通知(30 秒自动消失、失焦不收起) | — |
证据:默认值与范围 `shared/src/constants.ts:264-302`;字段语义 `shared/src/types.ts:1145-1188`;`ISLAND_STYLE_LABELS` `constants.ts:323-330`;预览实现 `IslandSettingsView.vue:141-155`。字号/高度/宽度滑块若 `family=classisland` 时被隐藏,**不得**清空其值(切回 winisland 仍生效)。
**`fontSize` 双语义(必须按 family 区分)**:`winisland` 下它是「岛字号」,保持旧语义与旧系数、旧版默认 `13`(`constants.ts:272`),标签必须写「岛字号(经典)」;`classisland` 下它是「正文字号基准」,默认 `16`,派生 14/18/20 阶梯(见 9.5)。老配置升级时**保留用户原来的 `fontSize` 数值**,不因 family 迁移而改写;新安装默认 `16`。设置页在切换 family 时只切换标签与默认值提示,不强制改写已有值。
### 9.7 课程提醒设置(并入本页)
在「课程提醒」卡内按 ClassHelper 既有 `lessonReminders` 字段渲染 5 个开关 + 2 个阈值;保存走 `saveConfig({ islandLayout: { ...config.islandLayout, lessonReminders: {...} } })`(现有 `ReminderSettingsView.vue:8-24` 的逻辑迁移过来),**迁移不改变字段名与默认值**(`shared/src/island-layout.ts:140-158`)。
| 控件 | 字段 | 控件类型/范围 | 默认 | 语义对齐 |
|---|---|---|---|---|
| 准备上课提醒 | `prepareEnabled` | `el-switch` | 开 | ClassIsland `IsClassPrepareNotificationEnabled`(`Settings.cs:47`) |
| 上课提醒 | `onClassEnabled` | 开关 | 开 | `IsClassChangingNotificationEnabled`(`Settings.cs:46`) |
| 即将下课倒数 | `endingEnabled` | 开关 | 开 | 下课前秒数徽标;ClassIsland 蓝色圆形徽标、剩余一秒显示 `-1` |
| 经典岛课中常显课程进度 | `classProgressEnabled` | 开关 | 开 | 仅 windowisland/经典形态显示;classisland fixed 用行底进度条 |
| 下课提醒与下节课预告 | `breakEnabled` | 开关 | 开 | `IsClassOffNotificationEnabled`(`Settings.cs:55`) |
| 课前提前(秒) | `prepareSeconds` | `el-input-number` 5–1800,step 5 | 60 | ClassIsland `ClassPrepareNotifySeconds`(`Settings.cs:43`) |
| 下课倒数提前(秒) | `endingSeconds` | `el-input-number` 5–600,step 5 | 60 | ClassHelper 既有语义;ClassIsland 无同名项,标注【映射】 |
布局:每行使用现有 `.lesson-setting-row`(左右分布、`padding: 12px 0`、底部分隔线 `1px solid var(--ch-border)`,`ReminderSettingsView.vue:126-137`),阈值行紧随 5 个开关之后。所有改动即时保存;保存失败 `ElMessage.error('提醒设置保存失败')`(沿用现有文案)。
提醒页 `/settings/reminder` 必须改为:顶部链接卡「上下课提醒已移至【灵动岛 → 课程提醒】」→ 按钮跳 `/settings/island`;其余「通知提醒」「成绩查询」卡保持不变。**不得**两处都可编辑,避免双写。
### 9.8 保存、实时预览与失败重试(设置页)
1. **两条通道必须区分**:
- 外观改动 → `window.desktop.islandSetAppearance({ ...appearance })`(纯对象;主进程 16ms 合并,`main/island.ts:2389-2410`)→ 立即生效但不落盘;
- 点「保存设置」→ `window.desktop.saveConfig({ island: { ...appearance } })` → 落盘(合并写 120ms,`main/config.ts:252-298`)→ 成功 `ElMessage.success('个性化设置已保存')`,失败弹出错误并保留 `savingIsland=false` 以允许重试。
2. **禁止传响应式 Proxy**:所有 IPC 入参必须是展开后的纯对象(`types/desktop.d.ts:143-152`、`:166-175`;现有注释已写明 `An object could not be cloned.` 的坑)。设置页已有的 `{ ...island.value }`、`JSON.parse(JSON.stringify(...))` 写法必须保留。
3. **圆角/行距/不透明度同步 custom 方案**:当 `layout === 'custom'` 时,这三项除写 appearance 外还要写 `activeScheme.radius/opacity/spacing` 并 `saveConfig({ islandLayout: 深拷贝 })`;失败提示「圆角保存失败,请重试」(现有文案,`IslandSettingsView.vue:115-120`),并**不要**回滚滑块显示值(下次保存重试)。
4. **恢复默认**:`恢复默认` 按钮必须 `confirmAction('恢复灵动岛的默认外观?', '恢复默认', { type: 'warning' })`,确认后 `applyIslandAppearance({ ...DEFAULT_ISLAND_APPEARANCE })` 再保存;**新默认** 为 classisland 族参数(`DEFAULT_ISLAND_APPEARANCE` 扩展后),但恢复默认**不得**覆盖 `family`(用户显式选择要保留;如产品要一并重置,需二次确认)。
5. **载入与刷新**:`onMounted` 调 `islandGetAppearance()` 与 `getConfig()`;`window.addEventListener('focus', loadLayout)`(现有 `:82-86`)保留,确保从编辑器返回后方案列表/圆角同步。
6. **预览效果按钮**:仅 `family=winisland` 显示(classisland fixed 预览用缩略图 + 真实运行态验证);点击推送预览通知(id 前缀 `appearance-test-`),说明文案必须含「拖动滑块即可实时预览」。
### 9.9 编辑器总体布局(像素级)
文件:`IslandEditorView.vue`;样式:`styles/island-editor.css`。保留现有 DOM 结构(smoke 依赖,见 9.20),只做尺寸/对齐调整。
1. **页面容器**:`.island-editor-page` 保持 `display:flex; flex-direction:column; flex:1 0 auto; padding: 12px 24px 16px; background: var(--ch-bg)`(`:1-10`);`:has(> .island-editor-page)` 让 `.main` 也纵向 flex(`:11-14`)。
2. **顶部操作条** `.editor-actions`:右对齐、`gap:8px`、`min-height:30px`、`margin-bottom:4px`(`:19-25`);元素依次为保存状态文本(12px,见 9.18)、`返回设置`(text small)、`应用并返回`(primary small,`loading=!!pending`,`disabled=!ready`)。
3. **页头** `.component-heading`:`align-items:flex-end; gap:24px; margin-bottom:20px`(`:35-39`);左侧标题「组件」`font-size: 34px*var(--ch-font-scale)`、`font-weight:600`、`line-height:1.4`、`margin-bottom:20px`;说明「以下组件将显示在主界面上,从组件库拖动组件以添加组件。」16px(`:40-53`);右侧方案工具区 `flex: 0 1 348px; min-width:300px`(`:54-57`)。
4. **错误条**:`error` 非空时在页头下显示 `el-alert type="error"`,内容为错误消息 + `重试` 按钮(`ready ? save() : load()`)(`:608-610`)。
5. **内容工具条** `.surface-toolbar`:左右两组;左侧 `.surface-tabs` 两个按钮「常态内容」「无通知展开内容」(12px,选中下方 2px 主色下划线,`:102-124`);右侧 `.surface-actions` 三个按钮:`新增行`、`行设置`、`进入编辑模式/退出编辑模式`(最后一个选中时 primary)(`:612-653`)。**「新增行」文案不可改**(smoke 断言依赖)。
6. **预览画布** `.preview-canvas`:`min-height:80px; overflow:auto; padding:10px 0 20px`(`:125-129`);`live-preview` 时改为 `display:grid; place-items:center; min-height:160px; padding:24px; background:var(--ch-layer-alt); border-radius:6px; margin-top:8px`(`:184-192`)。
7. **标签态** `.component-lines`(`min-width:100%; width:max-content`,`:130-133`)逐行渲染 `.component-line`:
- `display:flex; align-items:center; min-height:46px; padding:2px 4px; border:1px solid transparent; border-radius:var(--ch-radius-control); gap:12px`;行与行间距改为 `margin-top: var(--preview-gap)`(值取 `scheme.spacing`,默认 5),**不再固定 6px**(原 `:143-145`;对齐 ClassIsland 行距默认 5);
- 选中/激活行 `border-color: var(--ch-border-strong)`;拖放目标行 `outline:2px dashed var(--ch-accent); outline-offset:-2px; background:var(--ch-accent-soft)`(保留 `:228-232`)。
- 行把手 `.row-handle`:宽 `20px`(原 24,改以对齐 ClassIsland 拖拽把手宽度 20,`repos/ClassIsland/ClassIsland/MainWindow.axaml:496`)、`align-self:stretch`、图标「Ⅱ」16px、`cursor:grab`、`touch-action:none`;点击选中行、拖动排序(保留双行为)。
- 行内 `.line-components`:`display:flex; align-items:center; gap: var(--row-gap)`(取 `row.gap`,默认 8)、`min-height:40px`(对齐行高 40)、`flex:1`、`padding-right:28px`(为行尾按钮留位,保留);
- 空行占位 `.empty-line` 保留「将组件拖到这里,或点击下方组件添加」。
8. **实时预览态** `.island-preview`:`display:flex; flex-direction:column; gap:var(--preview-gap); width:max-content; padding:12px; border-radius:var(--preview-radius); background:#202024; color:#ffffffeb`(`:193-202`);`.preview-row` 改 `min-width:120px; min-height:40px`(原 44);`separated` 时容器透明、每行独立 `padding:12px;background:#202024`(`:210-217`);缩放通过 `zoom: scheme.scale * zoom` 生效(现有模板 `:700-708`)。
9. **组件库** `.component-library`:上边框分隔、`margin:0 -24px; padding:14px 24px 24px`;标题「组件库」17px/600 + 3px 主色下划线;网格 `grid-template-columns: repeat(4, minmax(0,1fr)); gap:12px 20px`;每项 `min-height:76px; padding:10px 8px; gap:12px; border:1px solid var(--ch-border); border-radius:var(--ch-radius-control)`,图标 36×36,名称 18px/500,描述 14px 次级(`:249-330`);响应式 1150px→3 列、800px→2 列、520px→1 列(`:371-417`)。**11 项顺序必须为**:文本、分割线、课程表、日期、时钟、天气简报、倒计时、轮播容器、滚动容器、分组容器、堆叠容器(`IslandEditorView.vue:58-70`)。
10. **抽屉** `.el-drawer.island-inspector`:宽 `min(360px, 94vw)`、`modal=false`、`modal-penetrable`、`append-to-body`、`lock-scroll=false`(`:781-790`);标题「组件设置」;内部顶部 `el-radio-group` 三个页签「方案/行/组件」(组件不可选时 disabled);抽屉背景/输入框样式沿用 `island-editor.css:346-361`。**抽屉动画统一 250ms `cubic-bezier(0,0,0,1)`**(对齐 ClassIsland `DrawerHost`:`repos/ClassIsland/ClassIsland.Core/Controls/DrawerHost.axaml:42-59`),并在 `AnimationLevel=0`/`prefers-reduced-motion` 时改为无动画。
11. **拖拽幽灵** `.drag-ghost`:`position:fixed; z-index:10000; padding:10px 16px; border-radius:6px; border:1px solid var(--ch-accent); box-shadow:var(--ch-shadow-flyout)`,位置 `pointer.x+16 / pointer.y+16`(`:331-342`、模板 `:774-780`)。
12. **导入文件输入**:隐藏 `<input type=file accept="application/json,.json">`(`:773`)保留。
13. **骨架**:`ready=false` 时 `el-skeleton :rows="8" animated`;`ready=true` 才渲染全部编辑 UI(`:611`)。
### 9.10 方案栏(保留并升级)
1. **选择方案**:`el-select` 绑定 `settings.activeSchemeId`,选项 `scheme.name`;`disabled=!ready`;`@change="switchScheme"`(重置选中 + 保存)。`aria-label="当前方案"` 保留(`:557-568`)。
2. **重置当前方案**:图标按钮(Refresh)`aria-label="重置当前方案"`、`title="重置当前方案"`;确认框标题「重置方案」,正文「将「{方案名}」的组件和外观恢复为默认配置?」,按钮「重置/取消」,`type: warning`;确认后用默认方案内容替换但**保留原 id 与 name**,并校验组件容量(现有 `resetScheme`,`:403-422`)。
3. **新建方案**(Plus,`aria-label="新建方案"`):`ElMessageBox.prompt('为新的组件方案命名', '新建方案', { inputValue:'新方案', inputValidator: 1–64 字符, confirmButtonText:'创建', cancelButtonText:'取消' })`;方案数上限 20(`schemeCapacity`,`:379-383`);新方案内容复制默认方案并**重生成全部 id**(`:384-402`、`renewScheme` `:371-378`);创建后自动选中并保存。
4. **管理方案**(FolderOpened 图标,`aria-label="管理方案"`,`el-dropdown trigger=click`)菜单顺序固定:`方案设置`、`复制方案`、分隔、`导入方案`、`导出方案`、分隔、`删除方案`(方案数 < 2 时禁用)(`:589-604`、`:423-432`)。
5. **复制方案**:深拷贝当前方案,名称 `${name.slice(0,61)} 副本`,重生成 id,push 后选中;受 20 方案与 200 组件上限约束(`:214-230`)。
6. **删除方案**:`ElMessageBox.confirm('删除方案「X」?', '删除方案', { confirmButtonText:'删除', cancelButtonText:'取消', type:'warning' })`;删除后 `activeSchemeId = schemes[0].id` 并保存(`:231-245`)。
7. **导出方案**:JSON 结构 `{ format: 'classhelper-island-layout', scheme }`、2 空格缩进、`Blob` 下载;文件名把 `[<>:"/\\|?*]` 替换为 `_`;下载后 1s `revokeObjectURL`(`:246-257`)。**该 format 字符串是老用户导入兼容契约,不得改。**
8. **导入方案**:文件 ≤ 1 MB;解析后校验 `format === 'classhelper-island-layout'` 且 `scheme.rows` 为非空数组;经 `normalizeIslandLayouts` 归一化;重生成全部 id;容量校验;成功 `ElMessage.success('方案已导入')`,失败 `ElMessage.error(错误信息)`;`finally` 清空 input(`:258-289`)。
9. **方案设置抽屉页**:名称(`maxlength=64`)、`界面缩放`(input-number 0.75–2.5、step 0.05,绑 `scheme.scale`)、`行间距`(slider 0–150,绑 `scheme.spacing`)、`常态圆角`(slider 0–20,绑 `scheme.radius`)、`各行分离显示`(switch,绑 `scheme.separated`);底部提示「字体、透明度、位置和动画与灵动岛共用设置。」+ 链接「前往灵动岛外观设置」→ `/settings/island`(`:832-857`)。**必须保留**这些字段与控件类型(ClassIsland 方案级参数:缩放/行距/圆角/分离,见 `repos/ClassIsland/ClassIsland/Views/SettingPages/AppearanceSettingsPage.axaml:27-118`)。
### 9.11 行与行设置(ClassIsland 编辑模式对齐)
1. **行列表**:每行渲染 `row.name`(编辑态以把手 aria-label「编辑{行名}」、title「{行名},拖动排序,点击设置」暴露,`IslandEditorView.vue:670-678`);行数上限 12(`addRow`,`:142-159`,文案「每种内容最多添加 12 行」)。
2. **新增行**:保留工具条「新增行」按钮;**新增**行列表末尾居中的第二入口:32px 高、图标 `E00D`(20px)、文字「新增行」,样式与 ClassIsland「新主界面行」一致(`repos/ClassIsland/ClassIsland/MainWindow.axaml:449-455`),点击行为与工具条相同。新行默认 `name: 组件行 N`、`main:false`、`allowNotifications:true`、`gap:8`、`nodes:[]`。
3. **排序**:拖行把手到另一行位置上/下方即可重排(现有 `dropPreview` 的 `current.rowId` 分支,`:505-512`);也提供抽屉内「上移/下移」按钮(`:824-830`)。
4. **主行**:抽屉按钮「设为主要行」;已是主行时按钮禁用且文案「已设为主要行」;设置主行会清除其他行 `main`(`setMain`,`:210-213`);行数为 1 时不允许删除(`removeRow`,`:202-209`)。
5. **允许提醒**:开关「允许显示提醒」绑定 `row.allowNotifications`;提示文案「提醒优先使用主要行;关闭提醒后使用下一条允许提醒的行。」(`:817-823`)。
6. **行间距**:`el-input-number` 0–64,绑 `row.gap`(组件间距);方案级行间距在方案设置抽屉(`scheme.spacing` 0–150)。**两者不得混用**。
7. **行设置抽屉规格**:标题「组件行」;字段顺序固定:当前行下拉(可切换编辑行)、行名称(maxlength 64)、组件间距、允许显示提醒、主行按钮、提示、上移/下移/删除此行(danger plain)。抽屉沿用 360px 非模态规格;打开/关闭动画 250ms `cubic-bezier(0,0,0,1)`。
8. **ClassIsland 对齐点(必须实现)**:
- 选中框:`1.5px` 虚线、`dasharray:1,1.5`、`opacity:0.75`、圆角跟随行/组件圆角;选中色 `AccentFillColorDefaultBrush` 等价主色,未选中容器为 `SurfaceStrokeColorDefaultBrush` 等价 `--ch-border-strong`(`repos/ClassIsland/ClassIsland/Controls/EditMode/EditableComponentsListBox.axaml:225-251`;`MainWindow.axaml:353-366`);
- 拖动把手:行把手 20px 宽、组件 chip 自身可拖(`TouchDragThumb` 等价);拖拽时被拖元素 `opacity:0.5`(ClassIsland 拖动预览透明度 0.5,`MainWindow.axaml:478-481`;ClassHelper 可保留现有 ghost 不降低原元素透明度,二选一但需统一);
- 行内组件外边距:组件外层**左右各 6px**(`Margin="6 0"`,`repos/ClassIsland/ClassIsland/MainWindow.axaml:388-399`、`:538-548`),叠加行内 `gap` 后形成视觉间距;运行态组件无此外边距;
- 行内容留白:行内左右 `12px`(`Margin="12 0"`、`MinWidth="20"`,`repos/ClassIsland/ClassIsland/XamlThemes/FluentTheme/Styles.axaml:203-232`);编辑态为可选中的把手/加号额外留位;
- 行高:`40px`(`IslandContainerHeight`,`repos/ClassIsland/ClassIsland/Controls/MainWindowLine.axaml:16`;`MainWindow.axaml:530-537` 容器行高 40)。
9. **既有交互必须保留**:行把手拖动排序、点击行选中并打开行设置、空行点击打开行设置、行尾「新增行」、主行/提醒开关的即时保存、最多 12 行提示。
### 9.12 组件库(11 类,保留既有 DOM 契约)
1. 数据源固定 11 项、顺序见 9.9-9;每项包含 `kind`、描述(现有 `library` 数组,`IslandEditorView.vue:58-70`)。
2. 每个库项必须是 `button`,带 `data-library-kind={kind}`、`aria-label="添加{中文名}"`;保留 `pointerdown` 开始拖拽、`click` 直接添加(两者共存,拖动后 `suppressClick` 防误加,`:754-768`、`:447`、`:500-503`)。
3. 直接添加逻辑:若当前选中容器则加入该容器(层级 ≥ 6 时警告「最多嵌套 6 层容器」),否则加入当前行末尾;添加后自动选中并保存(`add`,`:130-141`)。
4. 组件图标使用 `ComponentIcon`(`:763`);名称与描述分别 18px/14px(随 `--ch-font-scale` 缩放)。
5. 消息类组件(`notification/homework/call`)**不得**出现在库中,但既有方案里的这些节点必须继续渲染与可编辑(只读提示「此组件使用实时数据。可在外观和尺寸设置中调整显示。」)。
### 9.13 拖拽规格(三类源,必须保留现有实现)
1. **统一 Pointer Events**:`pointerdown`(仅 `button===0`)→ 在源元素 `setPointerCapture`(合成事件失败时 try/catch 忽略)→ `pointermove` 超过 **6px** 才判定开始拖拽 → `pointerup` 落点;`pointercancel`/`blur`/画布外释放一律取消(`beginDrag/moveDrag/dropPreview/cancelDrag`,`IslandEditorView.vue:433-503`;模板事件绑定 `:674`、`:688`、`:730`、`:760`)。
2. **三种拖拽源**:组件库项(`kind` 源)、组件 chip(`nodeId` 源)、行把手(`rowId` 源)。
3. **落点判定**:
- 用 `document.elementFromPoint` + `closest('[data-row]')` 找行;无行则取消;
- 行内找 `[data-node]`:未命中节点 → 落到行尾;命中节点 → 计算指针在节点宽度内的比例 `ratio`;
- 容器节点(`group/stack/slide/rolling`)且 `0.2 < ratio < 0.8` → `inside`;否则 `ratio > 0.5` → `after`,否则 `before`(`dragTarget`,`:462-481`);
- 拖拽期间落点高亮:行级 `outline:2px dashed`,节点 `before/after` 显示 3px 主色竖条(`IslandEditorNode.vue:107-123`),`inside` 高亮容器(`:107-110`)。
4. **约束**:单次拖拽内不得把父节点放进自己的后代(`all([node]).some(child.id === target.node.id)` → 忽略,`:517`);目标层级 + 被拖子树高度 ≤ 6(`:518-522`);组件总数 ≤ 200(`capacity`,`:338-347`)。
5. **落库**:移动完成后 `selectedId` 指向被拖节点、`rowId` 指向目标行、`panel='component'`,立即 `save()`(`:534-537`)。
6. **ghost**:仅 `pointer.moved` 后显示,文本为组件中文名或行名(`dragging.label`),偏移 +16/+16。
7. **触摸**:必须继续支持 `pointerType==='touch'`(现有 smoke 用合成 touch 事件验证跨行拖动,`main/smoke-layout.ts:74-91`);拖动时 `touch-action:none`、禁止页面滚动(`.component-chip`、`.row-handle`、`.library-item` 已设置)。
### 9.14 选中、组件设置抽屉(四页签)
1. **选中**:点击组件 chip → `selectNode`:设置 `selectedId`、`rowId`、`panel='component'`、打开抽屉;点击行把手/空行 → `selectRow`:清空 `selectedId`、`panel='row'`;切换方案/常态/展开内容时 `resetSelection()`(`IslandEditorView.vue:106-125`)。选中框按 9.11-8 规格。
2. **抽屉结构**:顶部 `el-radio-group`(方案 / 行 / 组件);`component` 页签在无选中时禁用;内容区渲染 `IslandComponentSettings`(组件)或行/方案表单(`IslandEditorView.vue:791-857`)。
3. **四页签**(`IslandComponentSettings.vue` 重构,顺序固定):`内容`、`外观`、`尺寸`、`隐藏条件`。每页签使用 `el-form label-position="top"`;选项选择类控件宽度 100%;数字/颜色控件与标签同一行(现有 `:283-307` 样式保留)。
4. **跨页签即时保存**:任一控件 `@change` 必须 `emit('change')` → 编辑器 `save()`(现有模式);不设「应用」按钮。
5. **删除/复制/包裹入口**:内容页底部固定「包裹到容器」下拉(`data-action="wrap"`)、「复制组件」、「删除」(danger plain)(`:152-170`);复制节点时深拷贝并重生成自身与所有后代 id(`copyNode`,`IslandEditorView.vue:180-188`);包裹受层级与容量约束(`wrap`,`:189-201`)。
6. **抽屉 DOM 契约**:`.component-settings`、`.el-tabs__item` 数量为 4、`.el-tab-pane` 的可见页内容互斥;编辑器 smoke 会按页签断言内容(现有 3 页签断言见 `main/smoke-layout.ts:111-137`,重构后由 parts-r/11 同步更新为 4 页签)。
### 9.15 组件设置项清单(与 parts-r/07 组件契约一一对应)
字段名直接来自 `packages/shared/src/island-layout.ts:77-108`(`settings`/`appearance`/`advanced`)。默认值与取值范围以 `normalizeIslandLayouts` 的 clamp 为准(`:268-332`)。**任何新增字段必须先落到 shared 契约并在 parts-r/07 说明渲染方式。**
#### 9.15.1 内容页(按 kind)
| kind | 控件 | 字段 | 类型/范围 | 默认 | 生效 |
|---|---|---|---|---|---|
| 全部 | (无内容设置时)提示 | — | 文本「此组件使用实时数据。可在外观和尺寸设置中调整显示。」 | — | — |
| `clock` | 12 小时制 | `settings.hour12` | switch | false | 即时 |
| `clock` | 显示秒 | `settings.seconds` | switch | true | 即时 |
| `countdown` | 倒计时目标 | `settings.target` | select:自动 `auto`/下一节课 `next`/本节下课 `end`/放学 `school`/指定日期 `date` | `auto`(编辑器新建时置为 `date`,`makeNode` `IslandEditorView.vue:359-363`) | 即时 |
| `countdown` | 目标名称(仅 date) | `settings.countdownTitle` | input maxlength 64 | `目标日` | 即时 |
| `countdown` | 目标日期(仅 date) | `settings.targetDate` | `ch-date-picker` `YYYY-MM-DD`,必须 `isDayKey` 合法 | `''` | 即时 |
| `weather` | 地点名称 | `settings.locationName` | input maxlength 64 | `''` | 即时 |
| `weather` | 纬度 | `settings.latitude` | input-number -90–90,precision 4,允许 `null` | `null` | 即时 |
| `weather` | 经度 | `settings.longitude` | input-number -180–180,precision 4,允许 `null` | `null` | 即时 |
| `text` | 文字内容 | `settings.text` | textarea rows 3,≤2000 字 | `班级信息` | 即时 |
| `text` | 启用模板变量 | `settings.template` | switch;提示「支持 {date} 日期、{time} 时间、{course} 当前课程。」 | false | 即时 |
| `schedule` | 显示课程 | `settings.display` | select:当前时间点 `current`/下一节课 `next`/今日时间点 `today` | `current` | 即时 |
| `schedule` | 显示时间点数(仅 today) | `settings.count` | input-number 1–30(normalize 1–40) | 5 | 即时 |
| `schedule` | 当前时间点显示起止时间 | `settings.showPeriod` | switch | true | 即时 |
| `schedule` | 当前课程显示进度 | `settings.showProgress` | switch | true | 即时 |
| `group`/`stack`/`slide`/`rolling` | 排列方向 | `settings.direction` | select:横向 `horizontal`/纵向 `vertical` | `horizontal` | 即时 |
| 同上 | 组件间距 | `settings.gap` | input-number 0–64 | 8 | 即时 |
| `slide` | 切换间隔(秒) | `settings.interval` | input-number 1–300 | 5 | 即时 |
| `rolling` | 滚动速度 | `settings.speed` | input-number 1–200 | 30 | 即时 |
| `separator`/`date`/`notification`/`homework`/`call` | — | — | 无内容参数(消息类显示只读提示) | — | — |
#### 9.15.2 外观页(所有 kind 通用,`appearance`)
| 控件 | 字段 | 类型/范围 | 默认 | 生效 |
|---|---|---|---|---|
| 独立外观 | `appearance.resourceOverridingEnabled` | switch | false | 即时;关闭时其余项禁用并沿用岛外观 |
| 辅助文字字号 | `appearance.secondaryFontSize` | input-number 8–96 | 14 | 即时 |
| 正文字号 | `appearance.bodyFontSize` | 同上 | 16 | 即时;**组件级覆盖,仅 custom 使用**;classisland fixed 的四档字号来自 `IslandAppearance.fontSize` 派生(见 9.5),本项不参与 |
| 强调文字字号 | `appearance.emphasizedFontSize` | 同上 | 18 | 即时 |
| 大号文字字号 | `appearance.largeFontSize` | input-number 8–128 | 20 | 即时 |
| 自定义文字颜色 | `appearance.customForegroundEnabled` + `appearance.foreground` | switch + color-picker(`#rrggbb`) | 关 / `#ffffff` | 即时 |
| 自定义背景颜色 | `appearance.customBackgroundEnabled` + `appearance.background` | switch + color-picker | 关 / `#202024` | 即时 |
| 自定义背景透明度 | `appearance.customBackgroundOpacityEnabled` + `appearance.backgroundOpacity` | switch + input-number 0–1 step 0.05 | 关 / 0.5 | 即时 |
| 自定义圆角 | `appearance.customCornerRadiusEnabled` + `appearance.cornerRadius` | switch + input-number 0–100(ClassIsland 上限 20 用于岛;组件可为 100) | 关 / 16 | 即时 |
| 组件透明度 | `appearance.opacity` | slider 0–1 step 0.05 | 1 | 即时 |
#### 9.15.3 尺寸页(原「高级」拆分,`advanced`)
| 控件 | 字段 | 类型/范围 | 默认 | 生效 |
|---|---|---|---|---|
| 对齐方式 | `advanced.align` | select:拉伸 `stretch`/起始 `start`/居中 `center`/末端 `end` | `stretch` | 即时 |
| 固定宽度(px) | `advanced.width` | input-number 0–2000(0=自适应) | 0 | 即时 |
| 最大宽度(px) | `advanced.maxWidth` | input-number 0–2000(0=不限制) | 0 | 即时 |
| 提示 | — | 「宽度为 0 时自适应;对齐在父容器设定宽度时生效。」 | — | — |
#### 9.15.4 隐藏条件页
| 控件 | 字段 | 类型/取值 | 默认 | 生效 |
|---|---|---|---|---|
| 隐藏条件(多选) | `advanced.hideRules` | checkbox-group:上课时 `in-class`/没有消息时 `no-message`/没有当前课程时 `no-class` | `[]` | 即时;数组只保留这三个合法值(normalize `:321-328`) |
| 高级说明 | — | 文本「隐藏条件基于当前课堂与消息状态即时生效;不会释放组件资源。」 | — | — |
ClassIsland 对齐:ClassIsland 用「按规则隐藏 + 规则集」承载同一语义(`repos/ClassIsland/ClassIsland/Views/SettingPages/ComponentsSettingsPage.axaml:423-443`);本轮**不引入规则集引擎**,保留 ClassHelper 三枚举,标注【映射】。若 parts-r/05 增补 `advanced.hideRules` 扩展,必须同步本页与 parts-r/07。
### 9.16 预览与「进入编辑模式」
1. **`editMode` 为页面内状态**(`ref(false)`),**不打开额外桌面窗口、不注入 iframe**(`docs/app/island.md:47`);smoke 会断言不存在 `.layout-editor`/`iframe`(`main/smoke-layout.ts:69-72`、`:154`)。
2. **进入**:工具条「进入编辑模式」→ `editMode=true`;画布切换为 `.island-preview` 真实组件渲染:`ComponentNode` 以 `:node :state :now :schedules :time-points editing :selected-id alert-node-id=""` 渲染,`state` 使用本地空状态对象(`IslandEditorView.vue:47-57`、`:719-732`);容器组件在编辑时展示全部子组件(`ComponentNode` 的 `editing` 分支由 parts-r/07 定义)。
3. **预览缩放**:仅在编辑模式显示 `.preview-toolbar`:文本「预览缩放」+ `el-slider` 0.3–1.5、step 0.1、tooltip `%` + 「重置缩放」(置 1)+ 提示「点选组件调整内容、外观和隐藏条件」(`:739-750`);缩放与方案缩放相乘(`zoom: scheme.scale * zoom`)。
4. **布局约束**:`preview-canvas` 必须完整容纳预览且不遮挡工具条——`canvas.bottom <= footer.top + 1` 且页面无横向滚动(smoke 断言 `main/smoke-layout.ts:138-148`);`separated` 时按 9.9-8 渲染。
5. **点击编辑态组件**:与标签态一致打开抽屉(`@select`);背景点击(`.preview-row @click.self`)选中该行。
6. **退出**:再次点击「退出编辑模式」回到标签态;抽屉保持打开状态但选中不变;`Esc` 优先关闭抽屉、再退出编辑模式(见 9.17)。
7. **实时同步真实岛**:编辑器每次 `save()` 通过 `saveConfig` 触发主进程 `island.syncCustomLayout()`(`main/ipc.ts:34-45`;`main/island.ts:320-338`),真实岛收到 `island:layout` 后热更新(`LayoutIslandApp.vue:35-51`);**因此“预览”= 页面内预览 + 真实岛热更新,不新增窗口**。
### 9.17 键盘、触摸与焦点
| 场景 | 按键 | 行为 |
|---|---|---|
| 编辑器全局 | `Esc` | 抽屉打开→关闭抽屉;否则编辑模式→退出编辑模式;否则返回 `/settings/island`(需保存成功后) |
| 选中组件 | `Delete`/`Backspace` | 删除选中组件(无后代时直接删;有后代弹确认「删除后其子组件一并删除」,确认后删);删除后 `panel='row'` |
| 选中组件 | `Ctrl+D`/`Cmd+D` | 复制选中组件(同「复制组件」,受 200 上限约束) |
| 选中组件/行 | `Enter`/`Space` | 打开组件设置/行设置抽屉 |
| 行把手/组件 chip | `Tab` → `Enter` | 键盘聚焦并选中;拖动需指针;不提供仅键盘拖动(标注【推断】,作为 P2 可加方向键移动) |
| 抽屉 | `Tab` 循环限制在抽屉内 | `el-drawer` 自带焦点陷阱,保留 |
| 设置页 | 无全局快捷键 | 滑块可用方向键微调(Element Plus 默认) |
触摸:所有拖拽继续使用 Pointer Events(鼠标/触摸/笔统一);触摸时不得触发页面滚动与文本选择;`touch-action:none` 只加在可拖元素;抽屉在窄屏(<520px)宽 `94vw`。焦点可见性:所有自绘按钮必须有 `:focus-visible` 2px 主色 outline + 2px offset(现有 `.library-item/.row-handle/.surface-tabs button` 规则保留,`island-editor.css:305-310`)。
### 9.18 数据流、契约与失败处理
1. **读取**:编辑器 `load()` → `window.desktop.getConfig()` → `settings = normalizeIslandLayouts(config.islandLayout)`、`appearance = config.island`;失败显示错误条并提供「重试」(现有 `load`, `IslandEditorView.vue:300-313`)。设置页同时读 `getConfig()` 与 `islandGetAppearance()`。
2. **保存队列**(编辑器):每次改动 `save()`:
- `snapshot = normalizeIslandLayouts(JSON.parse(JSON.stringify(settings)))`(必须先深拷贝纯对象,禁止 Proxy 过桥);
- `revision++`、`pending++`、`saved=false`;
- 串行 promise 链 `saveConfig({ islandLayout: snapshot })`;成功且 `current===revision` 时清 `error`、`saved=true`;失败写 `error`、`saved=false`;`finally pending--`(现有 `:81-105`)。
- 状态文本固定四态:`error` → 「保存失败」;`pending>0` → 「正在保存…」;`saved` → 「已自动保存」;否则「更改自动保存到本机」(`:543-546`)。
3. **退出保存**:`onBeforeRouteLeave(async () => { if (!ready) return true; await save(); return !error })`——有错不发离开(`:333-337`);「应用并返回」`apply()`:强制 `settings.layout='custom'`、`residentEnabled=true`,`await save()`,无错误才 `ElMessage.success('自定义灵动岛已应用')` 并 `router.push('/settings/island')`(`:290-299`)。
4. **失败重试**:错误条 `重试` 按钮按 `ready ? save() : load()`;保存失败不得清空本地编辑内容;不得自动回滚到磁盘值(避免用户输入)
丢失;若连续失败需在控制台/日志打出 IPC 错误(沿用主进程 logger)。
5. **应用/保存语义**:编辑器每次改动即 `saveConfig`(含磁盘合并写 120ms,`main/config.ts:252-298`),所以「退出保存」不是唯一写入点;**保存成功≠已点“应用”**,但配置已写入 `islandLayout`。`layout` 只有在「应用并返回」或设置页选择自定义时才切到 `custom`。
6. **主进程侧**:`ipcMain.handle(IPC_CHANNELS.saveConfig)` → `saveConfig(allowed)` → 若 `allowed.islandLayout` 则 `island.syncCustomLayout()`(`main/ipc.ts:34-45`);`syncCustomLayout()` 在常驻开关切换时重置 state,向岛渲染进程 `send('island:layout', getConfig().islandLayout)`(`main/island.ts:320-338`);岛渲染层 `onLayout` 更新(`LayoutIslandApp.vue:35-51`)。
7. **外观通道**:`island:set-appearance` 主进程 16ms 合并后 `island.setAppearance`(`main/island.ts:2389-2410`);`island:get-appearance` 会先 flush 再返回(`:2407-2410`)。外观不落盘,除非设置页点保存写 `config.island`。
8. **迁移**:`config.ts` 读盘时 `normalizeIslandLayouts` + `normalizeIslandAppearance`(`:198-233`)。老用户(无 `island.family`)→ `'winisland'`;新装默认 `'classisland'`。实现方式(二选一,必须由 parts-r/05 定稿):
- 在 `readPersistedFromDisk` 里判断 `parsed.island` 是否存在且 `family` 缺省 → `family:'winisland'`;或
- `islandLayout.version < 2` 且 `config.island` 存在 → 迁移为 winisland,并写 `version=2`。
**`fontSize` 迁移规则**:旧配置已有 `island.fontSize`(例如 13)时**原样保留**,不得因 family 从 classisland 变 winisland(或反向)而改写;旧配置没有该字段时才按迁移后的 family 取默认(`winisland=13` / `classisland=16`)。新安装/清空配置:`family='classisland'` 且 `fontSize=16`。`DEFAULT_ISLAND_APPEARANCE` 的 `fontSize` 默认值需按此改为 16,旧默认 13 仅作为 winisland 语义说明保留在文档与文案中。
迁移失败必须回落默认配置并记日志,不得让设置页崩溃。
9. **禁止**:在渲染进程直接写 config 文件;在编辑器内创建第二个 BrowserWindow;把 `ref`/`reactive` 直接传 IPC;用 `setBackgroundMaterial()` 影响岛透明(`AGENTS.md` 客户端章节)。
10. **契约对照(必须在 PR 中核对)**:本 part 用到的字段均来自 `IslandComponentNode.settings/appearance/advanced`、`IslandLayoutRow`、`IslandLayoutScheme`、`IslandLayoutSettings`、`IslandAppearance`(`shared/src/island-layout.ts:61-158`、`shared/src/types.ts:1146-1188`);新增字段以 parts-r/05 为准,组件渲染细节以 parts-r/07 为准,消息/课程提醒视觉以 parts-r/08 为准。
### 9.19 状态、空态、主题与性能约束
1. **加载/空态**:
- `ready=false` 前只渲染页头/方案栏骨架,禁止允许编辑(避免对未加载的 `scheme` 写入);编辑器用 `el-skeleton :rows="8"`,设置页用 `v-loading` 或同款骨架;
- `settings.schemes` 由 `normalizeIslandLayouts` 保证至少 1 个(空输入回落默认方案,`shared/src/island-layout.ts:399-402`);若运行期出现空数组(异常导入),必须立即用 `defaultIslandLayouts().schemes[0]` 补位并提示「方案数据异常,已恢复默认方案」;
- 行无节点时显示空行占位;容器无子组件时显示「拖入组件」(`IslandEditorNode.vue:47`);未选中组件时抽屉「组件」页签禁用;
- 删除当前选中节点后,`panel` 回到 `row`、`selectedId` 清空;切换方案/常态/展开内容时全部选中态清空(现有 `resetSelection`,`IslandEditorView.vue:119-125`)。
2. **错误态**:`load()` 失败(非桌面客户端打开、`getConfig` 抛错)→ 页面显示 `el-alert type="error"` + 错误文本 + `重试`;编辑功能全部不可用。保存失败只显示错误条与状态文本,不弹全局遮罩,不丢失编辑内存。
3. **主题与关怀模式**:设置页与编辑器必须只使用 `--ch-*` 令牌与 `var(--ch-font-scale)` 缩放文字(`styles/index.css:21-131`、`:103`),不得写死浅色/深色;深色模式由 `html.dark` 变量覆盖,且 `smoke-layout.ts:149-157` 会断言深色下 `.island-editor-page` 的 `--ch-text` 存在。**例外**:编辑器的岛预览区(`.island-preview`)固定使用岛自身底色 `#202024` 与 `#ffffffeb` 文字,以真实预览岛外观,不跟随设置页主题;该区域的组件颜色仍遵循 `appearance` 覆盖。关怀模式放大字号时,组件库名称/描述、按钮文字跟随 `--ch-font-scale`,但抽屉宽度、行高、组件外边距等几何令牌不变(`AGENTS.md` §5「关怀模式放大字号/控件,不改变几何令牌」)。
4. **响应式**:编辑器按现有断点:≤1150px 组件库 3 列、≤800px 2 列且方案工具区换行、≤520px 1 列并收窄页面左右 padding(`island-editor.css:371-417`);设置页在 ≤680px 时布局选择卡改单列(`IslandSettingsView.vue:497-505`),抽屉宽度 `min(360px, 94vw)` 自适应。
5. **性能**:拖动 `pointermove` 只在 `dragging` 存在时处理,且必须 `passive:false` 才可 `preventDefault`(现有 `{ passive: false }`,`IslandEditorView.vue:315`);保存用串行队列 + revision 去陈旧结果,禁止每次输入都直接 `await getConfig()`;节点上限 200/行 12/方案 20/深度 6 必须在写入前检查而不是写入后裁剪;编辑页不得创建额外 BrowserWindow、不得轮询 `getConfig`(只 `focus` 时刷新一次设置页)。
6. **无障碍**:所有自绘可点元素使用原生 `button` 或 `role="button"` + `tabindex="0"`;保留现有 `aria-label`(新建/重置/管理方案、添加组件、编辑组件、编辑行);拖拽不是唯一操作路径,必须同时提供按钮/下拉的等价操作(添加组件、上移/下移、包裹、复制、删除),以满足键盘与触屏用户。
### 9.20 既有实现与测试契约(重构不得破坏)
以下行为/文案/DOM 已被现有冒烟与代码依赖,重构时必须保留或由 parts-r/11 同步更新测试:
1. 路由:`/settings/island`、`/settings/island/editor`(`router/index.ts:63-74`);侧栏「灵动岛」入口(`ClientLayout.vue:48-55`);岛内「编辑」通过 `onIslandEditorOpen` 事件把主窗口切到编辑器(`ClientLayout.vue:23-26`;主进程 `main/index.ts:229-235`)。
2. 编辑器:`.component-library` 首组 11 个 `.library-item`;按钮文本「新增行」「行设置」「进入编辑模式/退出编辑模式」「应用并返回」「返回设置」;`data-row`/`data-node`/`data-library-kind`/`data-action=wrap`;`.component-settings .el-tabs__item` 页签;`.preview-canvas/.preview-toolbar/.island-preview`(`main/smoke-layout.ts:40-73`、`:111-148`、`:186-226`)。
3. 方案:`aria-label` 为「新建方案」「重置当前方案」「管理方案」;下拉项文案「方案设置/复制方案/导入方案/导出方案/删除方案」;导入 format 字符串 `classhelper-island-layout`;导入 ≤1 MB;新建/复制重生成 id;删除保持至少 1 个方案(`:264-360`)。
4. 设置页:`.layout-choice` 两张卡、`编辑岛内组件` 按钮、指针避让开关、保存/预览/恢复默认按钮与实时预览(`IslandSettingsView.vue:170-397`)。
5. 保存队列行为与状态文本(9.18-2);失败重试按钮;`应用并返回` 成功后才离开。
6. 这些断言若因重构改变(例如组件设置页签从 3 变 4),**必须**在同一 PR 更新 parts-r/11 的冒烟脚本,禁止只改实现不改测试。
### 9.21 验收标准
实现完成后逐项验证并记录结果(全部通过才算完成;验证必须无头/离屏,禁止启动可见岛做验收,除非显式 `--visual`):
1. **family 与默认**:新配置(清空 config)打开设置页,`family` 默认 `ClassIsland 风格` 且 `appearance.fontSize=16`、标签「正文字号(ClassIsland 基准)」;老配置(人为删除 `island.family`)重载后自动为 `经典 WinIsland(兼容)` 且外观与升级前一致(对比截图/配置 JSON),其原有 `fontSize`(如 13)保持不变、标签「岛字号(经典)」。
2. **fixed 语义**:`family=classisland`、`layout=fixed` 时,岛外观为信息行(行高 40、圆角 8、背景不透明度 0.5、底部 3px 进度条),默认显示日期 + 课程表;**不出现**胶囊/详情卡;来消息时在同一条内展开气泡;`family=winisland` 时恢复胶囊 + 详情卡。`layout=fixed` 时设置页不得出现任何行/组件编辑入口(fixed v2 为只读内置方案)。
3. **外观参数**:classisland 族 22 项、winisland 族 15 项逐一改动——拖动时岛即时变化(≤1 帧合并),点保存后 `config.json.island` 落盘;重启客户端后保持;非法值(如缩放 9、圆角 -1、颜色 `red`)被钳制/回落,不出现 NaN。**字号语义**:classisland 下改「正文字号(ClassIsland 基准)」时正文 = 该值、次级/强调/大号按 14/18/20 阶梯派生,默认 16;winisland 下标签为「岛字号(经典)」、默认 13;两种 family 间切换标签与默认值提示正确、不强制改写已有值。
4. **custom 与 family**:`layout=custom` 时 `family` 控件禁用并显示「自定义方案始终使用 ClassIsland 规格;外观族只作用于经典固定形态。」;custom 的渲染始终为 ClassIsland 规格(不因 `family=winisland` 回退旧观感);设置页「圆角/不透明度/行间距」改动同时写入 `schemes[activeSchemeId]` 并落盘;「界面缩放」在 custom 下隐藏并有说明;`schemes/rows/nodes` 编辑器只服务 custom,fixed 无编辑入口。
5. **课程提醒**:5 个开关 + 2 个阈值改动即时保存到 `config.json.islandLayout.lessonReminders`(**唯一数据源**);阈值越界被钳制到 5–1800 / 5–600;`/settings/reminder` 只显示只读摘要 + 跳转链接,页面内无可编辑的上课提醒控件。
6. **编辑器布局**:编辑器页 `padding 12/24/16`;行最小高 46(标签态)/40(预览态);行距等于 `scheme.spacing`;组件外层左右各 6px;选中框 1.5px 虚线 `1,1.5`、opacity .75;行把手 20px;「新增行」在工具条与行列表末尾均可用。
7. **四页签**:组件设置抽屉依次为「内容/外观/尺寸/隐藏条件」;11 类组件的控件、默认值、范围与 9.15 表一致;切换组件时回到「内容」页。
8. **拖拽**:组件库→行、组件库→容器(20%–80% 区域)、行内排序、跨行移动、行排序均可用(鼠标与触摸各测一次);拖动阈值 6px;拖动父组件到自身后代被拒绝且数据不变;嵌套超过 6 层、组件超过 200、行超过 12、方案超过 20 均给出提示且不写入。
9. **方案**:新建(提示输入,1–64 字,最多 20 个)、复制(名称加「副本」、id 全量重生)、重置(保留 id/name)、删除(确认,至少保留 1 个)、导入(≤1 MB,format 校验,id 重生)、导出(文件名非法字符替换、可再导入)全部通过;`activeSchemeId` 始终有效。
10. **保存与失败**:连续快速拖动/输入时状态文本按「正在保存…」→「已自动保存」变化;人为让 `saveConfig` reject(mock IPC)时显示「保存失败」+ 错误条「重试」,且未点应用不允许离开;重试成功后错误清除。
11. **编辑模式**:进入后页面内渲染真实组件(无额外窗口/iframe),缩放 0.3–1.5 生效且「重置缩放」回 100%;预览不遮挡工具条、无横向滚动;退出后回到标签态;真实岛在保存后热更新(`island:layout` 生效)。
12. **键盘/触摸**:`Esc` 逐层退出(抽屉→编辑模式→返回);`Delete` 删除选中组件(有子组件先确认);`Ctrl+D` 复制;所有自绘按钮有 `:focus-visible` 焦点环;触摸拖动不触发页面滚动。
13. **无回归**:9.20 列出的 DOM/文案/路由/导入格式全部保持;现有冒烟脚本(若未按 9.20-6 更新)中的编辑器断言通过;未新增桌面窗口、未改 `main/island.ts` 的透明/命中/几何逻辑;`pnpm typecheck` 与 `pnpm lint` 通过(不执行构建/运行验证);9.19 的状态/空态/主题/响应式行为逐一验证。