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

64 KiB
Raw Blame History

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 的状态/空态/主题/响应式行为逐一验证。