- 重构版主提示词(14 parts,面向 ClassHelper 灵动岛重构) - ClassIsland 全量规格提示词(20 parts,含分卷与逐章版本) - 投喂指南、分卷、逐节文件 - 调研记录、一致性基线、生成与质检脚本
105 KiB
8. 消息、提醒、课程提醒、强提醒特效、天气与课件覆盖(ClassHelper 落地规格)
本节是「消息与课程」这一部分在 ClassHelper 现有仓库内 的落地规格。前提口径(与 parts-r/01、parts-r/02 保持一致):
- 默认风格
appearance.family = 'classisland'(对应配置键islandAppearance.style,落盘字段以 parts-r/05 为准):fixed不再是胶囊+详情卡,而是 ClassIsland 风格信息行——默认 1 行 = 日期 + 课程表,行高 40、圆角 8、背景不透明度 0.5、底部 3px 进度条;消息到来时在同一行内展开 ClassIsland 风格气泡。原「胶囊 + 详情卡」只在family='winisland'兼容形态保留(老用户升级默认winisland,新安装默认classisland)。 custom常驻:保留现有schemes/rows/nodes/expandedRows数据模型与编辑器,渲染与交互按 ClassIsland 规格重构;消息入口仍是铃铛 + 共用消息面板。- 数据不迁移:通知/作业/叫人/课表/课件/天气全部沿用 ClassHelper 现有服务端与本地链路(
IslandNotification、ScheduleDto、IslandTimePointDto、CoursewareTransfer、IslandWeather)。 - 裁决优先级:ClassHelper 业务语义优先,ClassIsland 视觉表现优先;逐项裁决见 8.12。
8.1 数据契约(复用为主,最小扩展)
8.1.1 现有类型必须原样复用
| 类型 | 位置 | 用途 |
|---|---|---|
NotificationPriority = 'LOW'|'NORMAL'|'HIGH'|'URGENT' |
packages/shared/src/types.ts:30 |
业务优先级(四档) |
PRIORITY_RANK |
packages/shared/src/constants.ts:203-208 |
排序与合并共用的权重(LOW 0 / NORMAL 1 / HIGH 2 / URGENT 3) |
IslandNotification |
packages/shared/src/types.ts:1131-1143 |
岛消息载荷:id/title/content/priority/createdAt/courseName/teacherName/kind/subtitle |
IslandNotificationKind = 'notification'|'homework'|'call' |
packages/shared/src/types.ts:1111 |
消息类型 |
IslandState |
packages/shared/src/types.ts:1198-1230 |
coursewareTransfer/mode/lessonReminder/active/queued/inClass/currentPeriodEnd/reason/listExpanded/maxCardHeight/updatedAt |
IslandLessonReminder |
packages/shared/src/types.ts:1114-1128 |
kind: prepare/on-class/break/ending/in-class 等字段 |
LessonReminderSettings |
packages/shared/src/island-layout.ts:141-158 |
五类提醒开关 + prepareSeconds/endingSeconds |
ScheduleDto |
packages/shared/src/types.ts:545-564 |
dayOfWeek(1=周一…7=周日)/startTime/endTime/weekParity/course |
IslandTimePointDto |
packages/shared/src/class-plan.ts:76-87 |
timeType/name/courseName/startTime/endTime/isHideDefault |
CoursewareTransfer |
packages/shared/src/courseware.ts:82-94 |
stage/receivedBytes/totalBytes/progress/error |
IslandWeather |
packages/shared/src/island-layout.ts:38-44 |
temperature/code/windSpeed/fetchedAt/stale |
8.1.2 必须新增/扩展的字段(只加不改,保证迁移可回滚)
IslandState新增:family: 'classisland' | 'winisland':由外观下发给渲染进程,决定渲染形态;pendingStaged: number:上课期间被暂存的消息数(用于行内角标/铃铛计数);bubblePhase: 'hidden' | 'mask' | 'overlay' | 'closing':气泡阶段(仅fixed使用,见 8.4);bubbleDeadline: number:当前气泡阶段结束时间戳(毫秒),供渲染进程做进度条与自动收起(避免渲染进程自己计时导致主/渲染漂移)。
IslandNotification新增可选level?: 'emergency'|'high'|'normal'|'low':由priority映射而来(URGENT→emergency、HIGH→high、NORMAL→normal、LOW→low),用于 ClassIsland 三级排序键的中间键;不改变服务端 DTO,只在主进程派生。IslandLessonReminder新增progress: number(0–1,课程进度)与leftSeconds: number(整数秒,供徽标直接渲染),由主进程计算下发,渲染进程不得自行Date.now()推算(防止跨进程时钟漂移与系统时间跳变)。islandLayout新增version与family(见 parts-r/05);旧residentEnabled保留为兼容字段。
8.1.3 通知 DTO → 岛载荷的映射(单一入口)
必须在主进程的推送链路(现有 pushNotification 的调用方)集中实现,禁止各处自行拼装:
toIslandNotification(dto: NotificationDto, kindHint?): IslandNotification {
id: dto.id,
title: dto.title,
content: dto.content,
priority: dto.priority,
level: MAP[dto.priority],
createdAt: dto.createdAt,
kind: kindHint ?? 'notification', // 作业=homework;叫人=call(保留现有双播合并)
courseName: dto.course?.name ?? null,
teacherName: dto.teacher?.name ?? null,
subtitle: dto.subtitle ?? null
}
- 作业消息
kind='homework',id为本地合成的homework-<id>(packages/desktop-client/src/main/island.ts:1061-1065),不得拿它去调「标为已读」接口。 - 叫人消息
kind='call',标题由服务端buildCallTitle生成(packages/shared/src/constants.ts:571-575:请 XXX 同学找 XXX 老师)。
8.2 去重、合并与排序(ClassHelper 业务规则,必须保留)
- 按 id 去重:同一 id 只保留一条,
pendingNotifications()以 Map 去重(packages/desktop-client/src/main/island.ts:738-749;渲染侧同口径src/island/IslandApp.vue:86-97)。 - 通知/叫人双播合并:
createCall同时发notification:new与call:new,同 id 两条到达时必须合并而非覆盖:kind取更特殊的(call 优先),priority取更高(PRIORITY_RANK比较)(island.ts:713-724)。这是硬性要求,否则会出现「叫人胶囊显示成新消息、上课被当普通通知隐藏」。 - 排序口径:
PRIORITY_RANK降序 →createdAt降序(新的在前)(island.ts:744-748)。不得改用 ClassIsland 的三级键打乱业务优先级——ClassIsland 的「提供方优先级」在 ClassHelper 中没有对应概念,只有服务端单通道消息,因此本重构的排序键固定为(业务优先级 → 时间);若要引入 ClassIsland 的「通知等级」作为第二键,仅在与时间同序时作为稳定排序的次级键,不得跨优先级反转。 - 未读与已读:未读数以服务端为准(
GET /notifications/unread-count,docs/app/notifications.md:45-47);岛内「标为已读 / 全部已读」必须调用既有markRead/markAllRead处理器,成功后通知中心列表同步(island.ts:1044-1070;packages/desktop-client/src/renderer/stores/notifications.ts:102-122)。 - 实时撤回:收到
notification:deleted后必须从岛队列与当前展示中移除(island.ts:1210-1216),缓存与通知列表同步清理(docs/app/notifications.md:34)。 - 本机作业指纹:本机刚录的作业通过指纹避免重复提醒自己——这是既有规则,必须保留(
docs/app/island.md:25);实现位置在推送入口,不在岛渲染层。
8.3 队列与展示状态机(三形态对照 + 伪代码)
IslandState 的状态字段固定为:mode(hidden|pill|expanded)、active、queued、inClass、currentPeriodEnd、reason(new|after-class|urgent|call|closing)、listExpanded、maxCardHeight + 新增 bubblePhase/pendingStaged。
8.3.1 三种形态的展示规则对照
| 场景 | fixed + classisland(默认) |
fixed + winisland(兼容) |
custom |
|---|---|---|---|
| 空闲 | 信息行常驻(日期+课程表),无消息时不显示气泡 | 隐藏或细缝(现有规则) | 常驻组件行 |
| 课间普通通知/作业 | 行内气泡自动展开(遮罩→正文),可点开应用 | 胶囊,点击详情 | 铃铛 + 未处理数量,点击面板 |
| 课堂普通通知/作业 | 暂存:不展开气泡,行内显示「N 条暂存」角标(muted),下课补弹 | 暂存,下课补弹(现有) | 铃铛提示,允许主动查看 |
| 普通叫人(课间) | 行内气泡自动展开(kind=call 样式) | 胶囊(可点) | 铃铛 |
| 普通叫人(课堂) | 保持可见的「叫人」chip,不自动展开;点击后展开气泡 | 保留可点胶囊,不自动展开 | 铃铛,主动查看 |
| 紧急通知/紧急叫人 | 立即展开气泡(无视上课),并触发强提醒特效 | 立即展开 | 铃铛 + 提示,用户主动展开(可自动提示) |
| 处理完成 | 气泡收回,信息行恢复 | 队列清空后隐藏/细缝 | 回常态行 |
| 课件传输 | 覆盖整岛内容(信息行也覆盖),见 8.10 | 同 | 同 |
8.3.2 pushNotification 分支(必须逐条实现,以现有实现为基底)
现实现(packages/desktop-client/src/main/island.ts:794-867)分支顺序不得打乱,只在「展示方式」处按形态分流:
push(raw, ctx):
state.listExpanded = false
n = mergeNotification(findKnown(raw.id), raw)
inClass = ctx.inClass ?? state.inClass; state.inClass = inClass
if (ctx.currentPeriodEnd !== undefined) state.currentPeriodEnd = ...
if (ctx.preview) → 预览分支(30s 自收,失焦不收起)
if (isImmediate(n)): // priority === 'URGENT'(含紧急叫人)
正在展示的其它消息回队列
activate(n, n.kind==='call' ? 'call' : 'urgent', 'expanded')
→ classisland: bubblePhase='mask',并触发出特效
else if (inClass && n.kind==='call'):
activate(n, 'call', 'pill') // classisland: 只显示 chip,不展开气泡
else if (inClass):
queued = [..., n]; mode='hidden'(classisland: 保持行可见,pendingStaged++)
else:
上一条 active 回队列;queued 追加 n;activate(n, 'new', 'pill')
→ classisland: bubblePhase='mask',行内自动展开气泡
8.3.3 消息动作表(渲染 → 主进程,island.ts:929-1077)
| 动作 | 语义 | 备注 |
|---|---|---|
expand |
展开当前/最新消息 | classisland:进入气泡;winisland:展开详情卡;custom:打开消息面板 |
collapse |
收起 | 上课时普通通知直接隐藏;紧急/叫人回缩为 chip/胶囊 |
expand-list |
多条时「展开更多」 | 由 islandListLayout 决定行数与提示(见 8.6.3) |
dismiss |
单条「知道了」 | 不改已读 |
dismiss-all |
整批「知道了」 | 不改已读 |
mark-read |
标为已读 | 调后端,失败要回滚 UI 状态 |
mark-all-read |
整批已读 | 过滤掉 kind==='homework' 的本地 id |
open-app |
打开应用内通知页 | 列表行点击也走这里 |
8.3.4 自动收起超时(必须原值保留,island.ts:124-132)
| 形态 | 超时 |
|---|---|
pill(胶囊/chip) |
15 000ms |
| 课后补发展开 | 14 000ms |
| 课后补发胶囊 | 25 000ms |
| 展开(普通) | 20 000ms |
| 紧急 | 45 000ms |
| 叫人 | 90 000ms |
收回动画 CLOSE_ANIMATION_MS |
360ms(随 speed 缩放,island.ts:121) |
收起后的关键规则(island.ts:1266-1296):只要还有未处理消息(active 或 queued 非空),收起后必须保持 chip/胶囊/行内角标,不得直接消失;彻底隐藏只在「知道了 / 已读 / 队列清空 / 进入上课」时发生。
8.4 ClassIsland 风格气泡(fixed 默认形态,两阶段)
8.4.1 阶段机(必须由主进程状态驱动,渲染进程只渲染)
bubblePhase: hidden → mask →(有正文)overlay → closing → hidden
└(无正文/正文时长为 0)→ closing → hidden
- 进入
mask:行仍为 40 高,气泡在同一行内展开;遮罩背景为强调色,斜切遮罩扫入,遮罩文字(标题/徽标)延迟入场。 mask时长:默认 3 000ms;若该消息带正文且正文有明确EndTime/时长,则按正文时长决定(不得把正文时长叠加到遮罩上导致总时长翻倍)。- 进入
overlay:遮罩退出、正文淡入;底部显示剩余时间进度条(见 8.4.4)。 closing:正文/遮罩退出(200ms),随后窗口/行恢复;reason='closing'时行内不得显示任何「新消息」文案(防闪烁,沿用现有isClosing语义src/island/IslandApp.vue:73-80)。- 任一阶段都可被新消息打断:紧急消息直接覆盖当前气泡(当前消息回队列),普通消息按队列顺序等待。
8.4.2 行内气泡几何(像素)
- 行高固定 40;行圆角 8(
--ci-radius);行背景不透明度 0.5(只作用背景层)。 - 气泡内容内边距:水平 12(ClassIsland 行内容盒
Margin="12 0");遮罩文字层内边距 12 0;遮罩文字图标 20×20、间距 8。 - 气泡最大高度 = 行高 40(正文单行/双行截断,超出省略);多条列表形态时高度由
islandListLayout计算(见 8.6.3)。 - 气泡底部进度条:高 3px、贴底、圆角 1.5、
transform-origin:left、scaleX(progress)、值过渡 50ms linear(ClassIsland 3px 规格;现有课程提醒条是 2px,两者不得混用,见 8.8)。 - 遮罩背景:强调色实底(
--island-accent,默认#0a84ff);文字色#fff,标题 16px/600,副文本 12px/0.72 透明度。
8.4.3 气泡动画(ClassIsland 参数 + ClassHelper 弹簧的裁决)
裁决:岛的整体尺寸/圆角继续用 ClassHelper 的 SpringValue(stiffness=0.10、damping=0.68、换向保留 35% 动量、单帧位移 ≤ 剩余 20%、snap 0.05px,packages/desktop-client/src/island/spring.ts:26-114);气泡内部的遮罩扫入/文字/进度用 ClassIsland 的 CSS/WAAPI 参数。两条动画路径不得互相接管。
| 动画 | 参数 | 实现 |
|---|---|---|
| 斜切遮罩扫入 | 5 区段,权重 [0,0.1,0.2,0.4,0.2,0.1],每段 360ms,错峰 120ms(区 0/4 同时、1/3 延迟 90ms、2 延迟 120ms),缓动 QuarticEaseOut |
SVG polygon 5 片,或 clip-path 5 段;--ci-speed 缩放 |
| 斜切遮罩退出 | 反向:中间先收,1/3 延迟 90ms,0/4 延迟 120ms,缓动 QuadraticEaseIn,每段 360ms | 同上,反向 |
| 遮罩文字淡入 | delay 260ms、250ms、opacity 0→1、cubic-bezier(0.25,1,0.5,1) |
CSS animation both |
| 遮罩文字缩放 | delay 260ms、750ms、scale 1.1→1.0、同曲线 |
CSS transform |
| 遮罩退出文字 | 200ms、opacity 1→0 + scale 1.0→1.1、cubic-bezier(0.5,0,0.75,0) |
CSS |
| 正文淡入淡出 | 200ms、SineEaseIn |
CSS transition |
| 进度条值 | 50ms linear |
transform: scaleX |
| 行宽/行高变化 | 由弹簧决定(约 300–500ms 自然落定) | SpringValue.step() |
速度与降级:appearance.speed(0.5–2)必须同比缩放上述所有 ms:duration = base / speed,延迟同理;animations=false 时全部时长置 0(直接终态),但不得改变最终几何。系统「减少动态效果」时按 speed=2 且时长压到 1ms 处理。
8.4.4 气泡内容(信息层次)
- 遮罩阶段:类型图标(通知/作业/叫人 三套 SVG,沿用
src/island/ClassicNotifications.vue:115-132)+ 类型徽标(紧急/重要/普通/低或叫人/新作业)+ 一行标题;紧急加「需立即查看」、叫人加「老师正在等你」、课后补发加「下课后补发」(同一套live-dot语义)。 - 正文阶段:标题(单行省略)+ 正文(最多 2 行省略)+ 元信息(
教师名 · 时间)+ 可选subtitle(叫人提示语,如「请到办公室找我」)+ 按钮行(打开应用/标为已读/知道了;叫人时为收到)。 - 多条列表(
listMode):标题"N 条待处理通知",每行「类型图标 + 标题 +教师名 · 时间+ 类型徽标」,默认显示前 3 行,超出提示「展开更多(还有 N 条)」或「更多请前往应用内操作」(packages/shared/src/constants.ts:379-402,445-474)。
8.4.5 ClassicNotifications.vue 的改造与 winisland 兼容形态
packages/desktop-client/src/island/ClassicNotifications.vue 不得整体废弃,必须拆成「气泡内容」组件并同时服务两种形态:
- 两阶段复用:
fixed+classisland下,mask阶段只渲染它的header部分(类型图标 + 类型徽标 + 标题 + 紧急/叫人提示),overlay阶段再渲染title / body / footer(标题、正文、元信息、按钮行);列表形态(多条)只在overlay阶段出现。原组件的size/listLayout/pendingNotifications计算逻辑保留,改为由父组件传入。 winisland兼容:整卡(header + title + body + footer 一次性渲染)继续用于详情卡,交互与尺寸不变;只把文案/徽标/按钮语义与新气泡对齐(叫人时按钮「收到」等)。- 不得复制两份实现:图标、徽标、行布局、按钮行必须共用同一组件与同一份
classic-island.css,避免两种形态文案不一致。
8.5 强提醒全屏特效(Topmost)
8.5.1 行为
- 触发条件(全部满足):消息
priority==='URGENT'(紧急通知或紧急叫人);appearance.animations===true且appearance.effectEnabled(新增外观开关,默认true);岛当前可见(不是编辑模式、不是预览、不是隐藏);本节特效与气泡可同时出现。 - 表现:以岛中心为原点,强调色涟漪扩散到覆盖主界面所在屏幕;扩散 600ms、同步淡出 600ms,750ms 后移除元素并隐藏特效窗口。
- 独立窗口:新建
topmost-effect渲染入口与独立 BrowserWindow(transparent:true、frame:false、skipTaskbar:true、alwaysOnTop:true、showInactive、setIgnoreMouseEvents(true)、逐屏一个窗口);禁止setBackgroundMaterial();窗口内元素清空即隐藏,不销毁。 - 缩放与多屏:
effectScale(默认 1.0,复用现有ISLAND_SHADOW_PAD之外的独立字段)缩放窗口与圆心;屏幕/DPI 变化重算;4K@150% 下涟漪必须覆盖整屏无接缝。 - 降级:
animations=false、系统减少动态、编辑模式、窗口重建中、系统休眠恢复后的 1s 内 → 不播特效(只播气泡),不得阻塞消息展示。 - 非目标:本轮不做音效与 TTS(ClassHelper 无音频通道);
docs/app/notifications.md:23的「约 45 秒收起」仍由队列超时实现,与特效无关。
8.5.2 像素/动画参数
| 项 | 值 |
|---|---|
| 涟漪元素 | 全屏 SVG/CSS 圆(border-radius:50% 或 SVG circle),pointer-events:none |
| 扩散 | scale 0→1(半径 = 圆心到屏幕最远角),600ms,QuadraticEaseIn(≈cubic-bezier(0.11,0,0.5,0)) |
| 淡出 | opacity 1→0,600ms,SineEaseIn(≈cubic-bezier(0.12,0,0.39,0)) |
| 颜色 | --island-accent,默认 #0a84ff,透明度按 1→0 |
| 生命周期 | setTimeout(750 / speed) 后移除 |
| 窗口 | 覆盖 display.workArea;scale 后宽高乘系数 |
8.6 课程提醒五类(prepare / on-class / break / ending / progress)
命名对照:任务/文档口中的
progress(课中进度)在代码里对应IslandLessonReminder.kind === 'in-class',开关为classProgressEnabled;其余四类同名。本节一律用代码枚举值,并在需要时注明「progress」。
8.6.1 设置与默认值
islandLayout.lessonReminders(packages/shared/src/island-layout.ts:141-158,390-398):
| 字段 | 默认 | 范围 |
|---|---|---|
prepareEnabled |
true |
— |
onClassEnabled |
true |
— |
breakEnabled |
true |
— |
endingEnabled |
true |
— |
classProgressEnabled |
true |
— |
prepareSeconds |
60 |
5–1800 |
endingSeconds |
60 |
5–600 |
8.6.2 判定算法(LessonReminderTracker.update,必须逐行实现)
输入:schedules(当天过滤后按 startTime 升序)、now、settings。输出 IslandLessonReminder | null。证据:packages/desktop-client/src/main/lesson-reminder.ts:17-109。
- 当天课表 =
dayOfWeek === (now.getDay() || 7)的条目,按开始时间升序(周日=7)。 current= 满足start ≤ now < end的条目;next= 第一个start > now;ended=end ≤ now且距end< 12 000ms 的最近一条。- kind 判定顺序(自上而下,命中即止):
current && now - current.start < 5 000 && onClassEnabled→on-class;breakEnabled && !current && ended && now - ended.end < (next ? 12 000 : 5 000) && (now - ended.end < 5 000 || this.current?.id === keyOf(ended,'break'))→break;current && endingEnabled && current.end - now ≤ endingSeconds*1000→ending;!current && next && prepareEnabled && next.start - now ≤ prepareSeconds*1000→prepare;current && classProgressEnabled→in-class。
maskUntil / endsAt / lessonStartsAt / lessonEndsAt必须按源码公式:
| kind | maskUntil | endsAt |
|---|---|---|
| ending / in-class | entered(无遮罩) |
end |
| prepare | min(start, entered + 3000) |
start |
| break 且有下一节 | entered + 2000 |
end + 12000 |
| break 无下一节 | entered + 5000 |
end + 5000 |
| on-class | entered + 5000 |
start + 5000 |
id = "${dayKey}:${scheduleId}:${start}:${end}:${kind}";entered为同一 id 的连续保持时刻(避免同一提醒重复计时)。- 课程名
course?.name ?? '课程';时间字符串截到HH:mm(导入数据可能带秒,lesson-reminder.ts:78-79)。 - 渲染进程不得自行推导 kind,只消费主进程下发的
lessonReminder(以及新增的progress/leftSeconds)。
8.6.3 课程提醒条 UI(像素)
| 项 | 值 | 依据 |
|---|---|---|
| 容器 | 高 40、宽 max-content、min-width 300、max-width 800、圆角 8、背景 #20202080 |
LessonReminderContent.vue:196-208 |
| 信息层 | 高 40、居中、gap 12、padding 0 14、white-space:nowrap |
:209-216 |
| 主字号 | 16(课程名 20、时间 14、遮罩标题 18) | :204,235-245,262 |
| 进度条 | 高 2px、贴底、强调色、scaleX(progress)、transition 50ms linear |
:307-316 |
| 遮罩 | 5 片斜切 SVG、tan(30°)*40 偏移、区域边界 [0,0.1,0.3,0.7,0.9,1]、每片 360ms |
:62-72 |
| 遮罩时序 | 开:delays [0,90,120,90,0];关:[120,90,0,90,120];eased = p^4(开)/ p²(关) |
:91-99 |
| 兜底 | rAF 停摆时 600/speed ms 后强制到位 |
:83-89 |
| 文字动画 | 淡入 250ms/延迟 260ms、缩放 750ms/延迟 260ms,曲线 (0.25,1,0.5,1);退出 200ms、(0.5,0,0.75,0) |
:126-138,273-306 |
文案规则(LessonReminderContent.vue:14-23,143-169):
| kind | 左侧 | 课程侧 |
|---|---|---|
| prepare | 距上课还剩 N 秒 |
下节课是:课程名 + 时间 |
| on-class | 开始上课 |
本节课:课程名 + 时间 |
| break | 课间休息 |
下节课是:下一节课程名 + 下一节时间 |
| ending | (无左侧倒计时文字,只显示课程侧) | 本节课:课程名 + 时间 + 右侧蓝色秒数徽标(见 8.7) |
| in-class | 距下课还剩 N 秒 |
本节课:课程名 + 时间(无徽标) |
- 无课程时课程名显示
"今日课程已结束"。 leftSeconds = ceil((目标时间 - now)/1000),下限 0;目标时间:ending/in-class 用lessonEndsAt,其余用lessonStartsAt。progress:ending/in-class 用「已上课时间 / 总课时」,其余用「剩余时长 / 本提醒总时长」,钳制 0–1。
8.6.4 三形态差异(必须实现)
| 形态 | 行为 |
|---|---|
fixed + classisland |
课程提醒以行内条接管岛内容:信息行临时替换为提醒条(宽 = 行宽,高 40);ending 用徽标而非遮罩;in-class(进度)默认不使用提醒条(由课程表组件常驻显示进度),仅当用户开启 classProgressEnabled 且组件不可用时回退为提醒条【推断】 |
fixed + winisland |
保留现有实现:customLayout=false 时 ending 不接管(island.ts:173-185 的 lessonShowing 已排除 custom 的 ending),fixed 的 ending 仍以提醒条显示并使用徽标(docs/app/island.md:70) |
custom |
customLayout=true 时:kind==='in-class' 不接管岛(由 schedule 组件常驻显示进度);kind==='ending' 不接管岛内容,只在当前课程名右侧显示徽标(island.ts:198-199;docs/app/island.md:65-68) |
必须遵守的三条硬规则:
- 没有「即将下课」遮罩:
ending绝不显示 ClassIsland 式全行遮罩,只显示课程名右侧的蓝色秒数徽标(docs/app/island.md:68)。 - 不替换自定义岛常态组件:
custom模式下课程提醒(除 prepare/on-class/break 的临时接管)不得替换常态组件行。 - 关闭开关即不显示:
endingEnabled=false时不显示徽标;endingSeconds只影响徽标出现时机,不影响其他提醒。
8.7 下课前蓝色秒数徽标(重点规格)
8.7.1 视觉
| 项 | 值 |
|---|---|
| 位置 | 课程名 <strong> 右侧,与课程名同一行(inline-flex,flex:0 0 auto) |
| 形态 | 正圆 30×30,border-radius:50% |
| 背景 | 蓝色 #0a84ff(与默认强调色同值;可随 --island-accent 同步【推断:现实现写死 #0a84ff】) |
| 文字 | -N,白色 #fff,12px / 600,font-variant-numeric: tabular-nums,line-height:1,水平垂直居中 |
| 无障碍 | aria-label="距下课N秒" |
| 证据 | packages/desktop-client/src/island/lesson-ending.css:1-16;LessonReminderContent.vue:160-167 |
8.7.2 行为
- 显示条件(全部满足):
kind==='ending'(即current && endingEnabled && end - now ≤ endingSeconds*1000);leftSeconds ≥ 0;课表数据存在endTime。 - 数值:
leftSeconds = ceil((lessonEndsAt - now)/1000);显示为-${leftSeconds}。 - 剩余一秒显示
-1:当真实剩余时间在(0, 1000]ms时ceil结果为 1,徽标显示-1;不得显示-0,也不得在剩余 0 时继续显示(到点后ending条件不再成立,徽标消失)。这是用户明确要求的行为,必须有单测用例(now = end - 1ms→-1)。 - 阈值与开关:
endingSeconds默认 60(5–600);endingEnabled=false时完全不出现徽标;animations=false也不影响徽标(徽标不是动画)。 - 出现/消失动画:徽标可淡入 100ms(
opacity 0→1)并随课程提醒条一起退出;不得用缩放弹跳(避免课堂投影上的视觉干扰)。 - 两种岛都要用:
fixed(两种 family)与custom都必须显示该徽标;custom下徽标挂在schedule组件的当前课程名右侧,由组件内部渲染同一lesson-ending.css,不得复制第二套样式。 - 边界:
- 无时间点数据、按
ScheduleDto回退显示课程时,leftSeconds同样由endTime计算,徽标照常显示; - 跨天(课程结束时间 ≤ 开始时间)视为数据异常,不显示徽标并记一次告警;
- 系统时间跳变:以主进程下发的
leftSeconds为准,渲染进程不得自行用Date.now()覆盖; - 连续多节连堂:每节课结束时徽标各自独立计算,切换瞬间不得出现「-60 → -1」跳变(切换时应先隐藏上一节徽标再显示新一节)。
- 无时间点数据、按
8.8 课程进度条(两处,不得混用)
| 位置 | 高度 | 颜色 | 过渡 | 取值 |
|---|---|---|---|---|
| 课程提醒条底部(ClassHelper 既有) | 2px | --island-accent |
transform 50ms linear |
见 8.6.3 的 progress 公式 |
| 课程表组件内「当前课程」进度(ClassIsland 规格) | 3px | 强调色 | transform 50ms linear |
已上/总课时,钳 0–1 |
裁决:两者同时出现时(fixed + classisland 信息行同时含课程表与提醒条)保留两条——提醒条 2px 贴岛底,组件内进度 3px 贴课程项底;不得合并成一条,也不得把提醒条改成 3px(会与 ClassIsland 「行内进度条 3px」混淆)。渲染必须用 scaleX + transform-origin:left,不得对宽度做过渡。
8.9 天气简报(Open-Meteo,保留 ClassHelper 语义)
8.9.1 数据链路(不得改)
渲染组件 WeatherBrief.vue → window.island.getIslandWeather({latitude, longitude})(preload 白名单)→ IPC island:weather(packages/desktop-client/src/main/ipc.ts:23)→ 主进程 getIslandWeather(packages/desktop-client/src/main/island-weather.ts)→ Open-Meteo。
8.9.2 服务行为(必须原样保留)
- 固定服务与参数,不接受渲染层传 URL:
https://api.open-meteo.com/v1/forecast+current=temperature_2m,weather_code,wind_speed_10m+forecast_days=1(island-weather.ts:35-41)。 - 坐标校验:
latitude/longitude必须是有限数字,纬度绝对值 ≤90、经度 ≤180,否则抛「请设置有效的经纬度」(:14-22)。 - 缓存键 = 坐标定点 4 位(
:23);成功缓存 15 分钟(:60);失败缓存 60 秒退避并保留旧数据(:63);同一坐标并发请求复用同一 Promise(:29-30);缓存上限 32 条(:68)。 - 请求安全:
AbortSignal.timeout(8000)、redirect:'error'、响应体 > 64 000 字节拒收(:42-45);自动验收模式下不访问外网(:33-34)。 - 数据校验:三个数值字段必须都是有限数字,否则「天气数据格式不正确」(
:47-52)。 stale:成功返回时按fetchedAt距今 ≥ 15 分钟标记;失败返回旧数据时强制stale:true(:26,64)。- 渲染侧:每 60 秒刷新一次、坐标变化立即刷新、失败显示「天气暂不可用」、未设置坐标显示「请设置天气地点」、
stale || unavailable时追加· 缓存(WeatherBrief.vue:36-49,68-81)。
8.9.3 天气文案与建议(保留)
WMO 天气码映射(WeatherBrief.vue:50-60):0 晴、1/2/3 少云/多云/阴、45/48 雾、51-57 毛毛雨、61-67/80-82 雨、71-77/85/86 雪、95-99 雷雨、其他 天气。建议文案:code ≥ 95 → 雷雨天气,注意避雷;windSpeed ≥ 40 → 风力较大,注意出行(:61-66)。
8.9.4 ClassIsland 视觉映射与范围
- ClassIsland 天气组件的「6 种主信息模式」本轮只取默认一种:
地名 + 天气文字 + 温度(取整)+ 可选建议,以 ClassIsland 的次要/正文字号(14/16)呈现;不实现多模式切换。 - 不实现 ClassIsland 的气象预警、三天天气预报、逐小时预报(数据源无对应字段;
docs/app/island.md:45明确「不提供官方气象预警」)。 - 组件内刷新失败不得阻塞岛其他内容;天气与服务端无关,不参与消息队列。
8.10 课件传输覆盖(最高优先级内容)
8.10.1 优先级规则(必须实现,island.ts:649-676)
- 课件传输(
coursewareTransfer)覆盖整岛内容:fixed的两种 family 与custom都必须显示;getState()时若「当前有正在展示的紧急消息或叫人(mode!=='hidden' && active && (priority==='URGENT' || kind==='call'))」则课件让位,状态里返回coursewareTransfer:null。 - 其余消息不因课件覆盖而丢弃:
active/queued保持原状,课件结束后按队列规则继续展示。 - 课件传输期间不触发强提醒特效(课件不是紧急消息)。
- 课件覆盖时窗口几何按
COURSEWARE_ISLAND_SIZE(424×132,packages/shared/src/courseware.ts:4)+ISLAND_SHADOW_PAD定位;custom常驻也临时改为该尺寸,结束后恢复原方案尺寸。
8.10.2 UI 与像素(保留现有值)
| 项 | 值 |
|---|---|
| 卡片 | 424×132,padding 17 24,border-radius 24,背景 rgba(20,20,24,0.97),阴影 0 2px 8px #0004 |
| 字体 | 13px/1.4 'Segoe UI','Microsoft YaHei',sans-serif |
| 状态行 | 12px,#ffffffb8;百分比/「失败」用强调色、600 |
| 文件名 | 16px、600、单行省略、margin 5px 0 |
| 元信息 | 类型 · 教师 + 已收/总量 MB(保留 1 位小数) |
| 进度条 | 高 5px、圆角 8、accent-color,进度槽 #ffffff30 |
| 错误 | 11px、#ffb4ab、单行省略 |
| 证据 | CoursewareTransfer.vue:67-131 |
8.10.3 阶段与失败
- 阶段文案(
CoursewareTransfer.vue:26-34):downloading 正在接收课件、verifying 正在校验文件、completed 课件接收完成、failed 课件接收失败。 - 完成后由主进程/服务端链路校验 SHA256 并回执(
docs/app/courseware.md:28);岛只负责展示,不执行文件。 - 失败必须显示
error文案并允许用户点击卡片打开课件页(pointer-events在课件卡上为none,点击交给整岛open-app);失败卡片停留至用户处理或下一个传输覆盖,建议停留 30s 后自动恢复常态【推断】。 - 传输中窗口重建/渲染进程崩溃:主进程重新下发
coursewareTransfer状态,恢复展示。
8.11 教师端 / 班级端差异
- 岛只在班级端运行:教师/管理员使用 Web 管理端,不启动岛;客户端以
CLASS_DEVICE登录时才有「本班」消息。 - 班级端:接收本班通知/作业/叫人/课件;课程提醒基于服务端解析后的本班课表(
ScheduleDto/IslandTimePointDto)。 - 教师端(Web):无岛;发布 URGENT 时的二次确认是服务端规则(
docs/app/notifications.md:25-29),与岛无关。 - 未登录/离线:不展示在线消息;已缓存消息可只读展示(不得伪造未读状态);课程提醒与天气可离线工作(课表来自本地缓存,天气有缓存)。
- 权限:已读/撤回等操作失败(403/401)时,岛必须回滚本地状态并提示「无权限或登录已失效」,不得假装成功。
8.12 冲突裁决表(ClassHelper 业务优先,ClassIsland 视觉优先)
| 主题 | ClassHelper 既有语义(胜出) | ClassIsland 视觉(采用) | 落地规则 |
|---|---|---|---|
| 消息排序 | PRIORITY_RANK → 时间 |
提供方优先级/通知等级 | 按 8.2 第 3 条:业务优先级优先,等级仅作稳定次级键 |
| 上课暂存 | 上课期间普通通知暂存,下课补弹 | ClassIsland 上课照常播提醒 | 暂存规则保留;信息行保持可见但不展开气泡 |
| 紧急消息 | 立即展开、45s 收起 | ClassIsland 气泡两阶段 | 立即展开气泡 + 撒特效;45s 超时保留 |
| 普通叫人 | 上课也保留可点入口,不自动展开 | ClassIsland 无「叫人」概念 | 上课显示可点 chip;点击展开气泡 |
| 去重 | 按 id + 双播合并 | 同一 request 只入队一次 | 两者都实现(8.2) |
| 已读 | 服务端已读 + 本地回执 | ClassIsland 无 | 保留现有,失败回滚 |
| 收起动画 | 收回动画 360ms/窗口淡出 | 遮罩退出 200ms | 遮罩退出用 ClassIsland 参数;岛形变用弹簧;窗口淡出保留 |
| 多消息列表 | 前 3 条 + 展开更多 | ClassIsland 无列表形态 | 用 ClassIsland 行高/字号重排列表(共享 islandListLayout) |
| 课程提醒 kind | prepare/on-class/break/ending/in-class | ClassIsland 只有上下课/课前/放学 | 保留五类;ending 用徽标,不做「即将下课」遮罩 |
| 课程提醒条高度 | 40 | 行高 40 | 一致,沿用 40 |
| 进度条 | 2px(提醒条) | 3px(行/组件) | 两处各自保留,见 8.8 |
| 天气 | Open-Meteo 15 分钟 + 缓存标记 | 小米天气 + 预警 | 保留 Open-Meteo;不实现预警/预报 |
| 课件覆盖 | 覆盖岛内容 | 无对应 | 保留 ClassHelper 规则;紧急/叫人时不覆盖 |
| 音效/TTS | 无 | 有 | 不实现,不进入验收 |
| 自动化/插件/管理端 | 无 | 有 | 不实现 |
8.13 边界与失败降级
- 无课表/课表为空:
lessonReminder=null;信息行显示日期 + 课程表占位(今天没有课程。);课程提醒全部不触发;不得因此隐藏整行。 - 上课状态未知(
inClass未下发/服务端断线):按「非上课」处理(消息正常展示),不得猜测;恢复后以下发值为准。 - 时间跳变/时区变化:课程提醒 id 含
dayKey,跨天自动重建;时间回退不得重复触发同一 id 的提醒(entered逻辑),时间前跳不得补发多条。 - 消息风暴(一次 > 20 条):列表最多
maxRows=20,其余提示「更多请前往应用内操作」;不得逐条弹气泡;同一 id 重复到达按合并规则处理。 - 重复广播 / 断线重连重放:以 id 去重;重连后以服务端列表为准做一次全量对账(新增/删除/已读状态),不得凭队列恢复弹窗。
- 已读请求失败:
markRead/markAllRead失败时恢复本地未读状态并提示;不得把消息从队列删除。 - 课件传输中断/断网:显示
failed + error;恢复网络后由心跳补拉(docs/app/courseware.md:26-28);岛不得自行重试下载。 - 渲染进程崩溃/窗口重建:主进程保留
state,重建后一次性下发;课件/课程提醒/消息不丢;重建限流沿用现有(3 次/10 分钟)。 - 系统睡眠/唤醒:唤醒后立即重算课程提醒与上课状态;跨过的时间点不补弹(与 8.13 第 3 条一致);课件若在睡眠中完成,按最新状态展示。
- 多屏切换/DPI 变化:重算命中矩形、气泡与特效窗口;切换瞬间不得显示残影(先隐藏特效窗口再重算)。
- 无网络启动:消息/课表用离线缓存(按服务器与班级隔离),天气显示缓存或「暂不可用」;课程提醒只要课表缓存存在就必须工作。
- 编辑模式/设置预览:编辑模式不弹新气泡、不播特效、课程提醒条不接管编辑画布;预览模式停留 30s 自动结束(
PREVIEW_DURATION_MS,island.ts:2299-2300),不写已读。
8.14 验收标准(可勾选,全部通过才算完成)
消息与队列
- 同一 id 的通知与叫人双播到达:只出现一条,
kind='call'、priority取高者;不会降级成普通通知。 - 课间收到普通通知:
fixed+classisland行内自动展开气泡(先遮罩后正文);winisland为胶囊;custom铃铛计数 +1。 - 上课收到普通通知:不弹气泡,行内显示暂存角标;下课自动补弹最新一条,其余保留「还有 N 条」。
- 上课收到普通叫人:显示可点 chip,不自动展开;点击后展开气泡并可「收到」。
- 紧急通知/紧急叫人:无论上课与否立即展开气泡,并在 45s/90s 后按规则收起(有未处理时保持 chip/角标)。
- 「知道了」与「标为已读」语义区分正确;整批已读会过滤 homework 本地 id,不报「通知不存在」。
- 实时撤回后,岛内该消息从 active 与 queued 同时消失。
- 消息列表默认 3 行、超出显示「展开更多(还有 N 条)」;展开后受屏幕高度限制改提示「更多请前往应用内操作」。
气泡视觉
- 气泡遮罩 5 区段斜切扫入,区段 360ms、错峰 120ms;文字延迟 260ms 淡入 250ms + 缩放 750ms;退出 200ms 且缩放回 1.1;
speed改变后所有时长同比缩放。 - 行高 40、圆角 8、背景不透明度 0.5、底部进度条 3px、值过渡 50ms 线性;文字始终不透明。
animations=false或系统减少动态时,气泡直接终态,最终几何与有动画时一致。
强提醒特效
- 紧急消息触发全屏涟漪:600ms 扩散 + 600ms 淡出,750ms 后移除;多屏各一窗口;
effectScale生效。 - 编辑模式、隐藏状态、窗口重建中不播特效,但气泡照常;特效窗口不接收鼠标、不改变岛命中。
课程提醒
- 五类提醒按 8.6.2 算法在构造的时间轴下逐一出现,且同一提醒不重复;关闭对应开关后不出现。
custom下in-class与ending不接管岛;fixed下ending显示徽标而非遮罩;任何形态都没有「即将下课」遮罩。- 蓝色秒数徽标:正圆 30×30、
#0a84ff、12px/600、显示-N;now = end - 1ms时显示-1;endingEnabled=false不显示。 - 课程进度:提醒条 2px、组件内 3px,两条同时存在且均贴底、均用
scaleX。
天气与课件
- 天气 15 分钟缓存、失败 60s 退避并保留旧数据、断网显示「· 缓存」、坐标非法显示「请设置天气地点」;同一坐标并发请求只发一次网络请求。
- 课件传输覆盖整岛(424×132),显示文件名/类型/教师/已收-总量/进度;紧急或叫人在播时课件让位且不丢消息;完成后 SHA256 校验通过才显示「完成」。
边界
- 跨天、时间跳变、睡眠唤醒、渲染进程崩溃重建后,消息不丢、不重复弹、课程提醒状态正确。
- 无课表/无网络/未登录时,岛显示合理占位且不崩溃;已读失败会回滚并提示。
- 全程未出现音效/TTS、自动化、插件、主题市场等非目标功能;也未出现第二个岛窗口(课件与特效窗口除外,且均为独立用途)。
9. 编辑器与设置页重构(IslandSettingsView / IslandEditorView / IslandComponentSettings)
本 part 规定「灵动岛设置页」与「自定义方案编辑器」在重构后的完整实现。所有改动必须在现有 ClassHelper 仓库内完成,沿用 pnpm monorepo、Electron 44、Vue 3.5、Element Plus 2.14、Pinia、Vue Router、Vite 与既有 @classhelper/shared 契约;不得新建工程、不得引入 Avalonia/C#、不得新增服务端依赖、不得新建额外桌面窗口来做编辑预览。所有数值以 packages/...:行号 标注证据;ClassIsland 侧以 repos/ClassIsland/...:行号 标注;无法从源码确认的写【推断】。
9.1 口径与术语(与 parts-r/01、05、07 对齐)
family是 fixed 形态的外观族:islandAppearance.family: 'classisland' | 'winisland',默认'classisland'(新安装默认)。两种取值只改变 fixed 的外观:classisland+fixed:ClassIsland 风格信息行,默认 1 行 = 日期 + 课程表,行高 40、圆角 8、背景不透明度 0.5、底部 3px 进度条,消息到来时在同一行内展开 ClassIsland 风格气泡(业务规则仍用 ClassHelper 的暂存/紧急/叫人/已读/去重);winisland+fixed:保留「胶囊 + 详情卡」旧形态(兼容老用户);classisland+custom与winisland+custom:都使用 ClassIsland 规格的行/组件(行高 40、行内留白 12、组件契约见 parts-r/07、编辑模式见本 part 9.9–9.16),family对 custom 不产生影响。 禁止把classisland下的 fixed 写成小胶囊;禁止让 custom 因family=winisland回退成旧 custom 观感。
layout与family正交:layout: 'fixed' | 'custom'决定「消息行 / 常驻组件方案」与是否开放编辑器;family只决定 fixed 的视觉族。设置页必须在layout === 'custom'时把family控件置灰,并显示「自定义方案始终使用 ClassIsland 规格;外观族只作用于经典 fixed 形态。」;layout === 'fixed'时family可用且实时生效。- 升级口径:老配置文件没有
island.family字段时,必须迁移为'winisland'(保持用户既有 fixed 胶囊观感;custom 始终 ClassIsland 规格,迁移不影响);新安装/清空配置时用'classisland'(fontSize默认 16)。迁移必须保留用户原有appearance.fontSize数值(不因 family 迁移改成 13 或 16);若旧配置无该字段,按迁移后的 family 取默认(winisland=13 / classisland=16)。迁移实现见 9.18,需与 parts-r/05 的islandLayout.version迁移函数同批落地。 - 术语:组件(
IslandComponentNode)、行(IslandLayoutRow)、方案(IslandLayoutScheme)、常态内容(rows)、无通知展开内容(expandedRows)、外观(IslandAppearance)。组件种类集合固定为 11 类编辑器可见:date/clock/weather/countdown/schedule/text/separator/slide/rolling/group/stack;消息类notification/homework/call不在组件库露出,仅兼容既有节点(packages/shared/src/island-layout.ts:3-18、:45-60;packages/desktop-client/src/renderer/views/settings/IslandEditorView.vue:58-70)。 - fixed 的内置信息行 v2 是只读内置方案,不可编辑:
classisland+fixed固定为 1 行 = 日期 + 课程表,该行属于内置方案,不进入方案编辑器(不占schemes/rows/nodes,编辑器只服务 custom)。设置页对 fixed 只提供 9.5 的外观参数与 9.4-4 的静态缩略示意,不得提供行/组件编辑入口、不得出现「编辑固定行」按钮。后续版本若开放 fixed 行编辑,必须升级islandLayout.version并在 parts-r/05 增补契约;v2 不开放。 editingLayout死标志不属本 part:main/island.ts中该标志只有读取点、无写入点的问题归 parts-r/04 处理;本 part 只规定“编辑模式由主窗口路由页承担、岛窗口不新增编辑态”,island.ts的清理以 parts-r/04 为准,09 不修改该文件。
9.2 文件级改造清单(只列本 part 负责的文件)
| 文件 | 改造类型 | 必须做的事 |
|---|---|---|
packages/desktop-client/src/renderer/views/settings/IslandSettingsView.vue |
大改 | 重排信息架构;新增 family 切换与两套外观参数;把课程提醒设置并入本页;保留实时预览/保存/恢复默认/预览效果 |
packages/desktop-client/src/renderer/views/settings/IslandEditorView.vue |
中改 | 保留全部既有编辑能力;行/画布/选中框/抽屉/组件库按 ClassIsland 编辑模式对齐;组件设置抽屉拆为「内容/外观/尺寸/隐藏条件」四页 |
packages/desktop-client/src/renderer/components/IslandComponentSettings.vue |
大改 | 四页签结构;补齐 11 类组件的全部设置项;字段/控件/默认值/范围见 9.15 |
packages/desktop-client/src/renderer/components/IslandEditorNode.vue |
中改 | 选中框改为 1.5px 虚线 1/1.5;容器虚线;组件外框左右各 6px;拖拽手感与 ARIA 保持 |
packages/desktop-client/src/renderer/styles/island-editor.css |
中改 | 行高 40、行距 scheme.spacing、组件外边距 6、抽屉 250ms 曲线、缩放/落点/ghost 样式 |
packages/desktop-client/src/renderer/styles/index.css |
小改 | 设置页新增的 .island-family-*、.island-preview-thumb 等局部样式;复用既有 --ch-* 令牌(index.css:21-54、:103-131) |
packages/shared/src/island-layout.ts |
中改 | IslandAppearance 扩展字段的默认值与 normalize 边界;normalizeIslandLayouts 的 12 行/200 节点/6 层/20 方案上限保持(:256-403)——字段最终名以 parts-r/05 为准 |
packages/desktop-client/src/main/config.ts |
小改 | 外观新字段迁移(老用户 family=winisland)与 normalize;沿用 120ms 合并写盘(:252-298) |
packages/desktop-client/src/main/ipc.ts |
不改逻辑 | 确认 saveConfig 收到 islandLayout 后调用 island.syncCustomLayout()(:34-45);外观仍走 island:set-appearance/island:get-appearance(:2389-2410) |
packages/desktop-client/src/types/desktop.d.ts |
小改 | saveConfig/islandSetAppearance 的纯数据注释保留(:143-175) |
不得删改 main/island.ts 的窗口/命中/几何逻辑;编辑器与设置页只能通过既有 IPC 影响它(本 part 不承担岛渲染)。
9.3 设置页信息架构(重构后)
设置页仍挂在 /settings/island(packages/desktop-client/src/renderer/router/index.ts:63-68),侧栏标题「灵动岛」(layouts/ClientLayout.vue:48-55)。页面外层沿用 .page(padding: 16px 20px 24px)、.page-header(margin-bottom: 12px)、.page-title(20px/600)(styles/index.css:186-204),整体改为「页头 → 布局形态卡 → 外观卡 → 课程提醒卡 → 指针与窗口卡 → 底部操作条」,每张卡为 el-card shadow="never"(圆角 8、1px 描边、backdrop-filter: blur(20px) saturate(1.1),index.css 的 .el-card 规则)。
- 页头:左侧
h2「灵动岛」;右侧显示当前形态徽标el-tag:经典信息行(family=classisland、layout=fixed)/自定义常驻(layout=custom)/经典胶囊(兼容)(family=winisland、layout=fixed)。徽标随保存结果更新,不额外请求。 - 布局形态卡(保留既有两张选择卡并升级文案与点击):
- 左卡「经典灵动岛」:
family=classisland时描述必须改为「ClassIsland 风格信息行:日期 + 课程表,课中进度条;消息在同一行内展开,v2 为只读内置方案」;family=winisland时保留现有描述「显示课程进度、上下课提醒与班级消息」; - 右卡「自定义灵动岛」:描述保留「ClassIsland 风格,常驻显示时钟、课程等组件」(custom 始终 ClassIsland 规格,与 family 无关);
- 卡片样式沿用
.layout-choice(padding 20、圆角 12、选中border-color: var(--el-color-primary)+background: var(--el-color-primary-light-9),:450-478);点击调用chooseLayout('fixed'|'custom'),立即写saveConfig({ islandLayout: { ...config.islandLayout, layout, residentEnabled: mode === 'custom' } })(现有实现IslandSettingsView.vue:44-59保留); - 选择
custom后追加「编辑入口」块(现有.layout-edit-entry,:192-198、:479-496):说明「编辑常态内容与无通知展开内容;课前自动提示,通知操作与经典岛共用。」+ 主按钮「编辑岛内组件」→router.push('/settings/island/editor'); - 选择
fixed后不得出现任何行/组件编辑入口:fixed 的 classisland 信息行是只读内置方案(见 9.1-5),卡片说明里必须写明「固定信息行由内置方案渲染,暂不支持编辑」。
- 左卡「经典灵动岛」:
- 外观卡:标题「外观、位置与弹出」保留;顶部说明两段改为:
classisland:「默认信息行高度 40、圆角 8、背景不透明度 0.5;紧急消息立即展开,课中普通通知暂存、下课后展开;自定义岛可点击铃铛主动查看。」winisland:保留现有「紧急消息立即展开;课中普通通知暂存,下课后展开。自定义岛可点击铃铛主动查看。」- 卡内第一行必须是 视觉风格(family) 控件(9.4),随后按 family 分支渲染参数组。
- 课程提醒卡:新增(内容见 9.7),单一数据源固定为
config.json.islandLayout.lessonReminders;原/settings/reminder页的「上下课提醒」只保留只读摘要 + 跳转,不得保留可编辑控件(避免双写)。 - 指针与窗口卡:
layout=custom时显示「指针避让」整块(现有.resident-window-options,:199-218、:423-444);classislandfixed 下增加「触摸后自动恢复」说明【推断:由 ClassHelper 触摸失焦宽限承担,无独立参数】。 - 底部操作条(保留现有):
保存设置(primary,loading)、预览效果、恢复默认;下方 InfoBar「拖动滑块即为实时预览;保存后写入本地配置,重启客户端仍然生效」(:385-395原样保留,文案可保留)。
9.4 视觉风格(family)与两套参数组
- 控件(必做):
el-segmented(Element Plus 2.14 可用)或两个el-radio-button,标签:ClassIsland 风格(valueclassisland,默认):「信息行 / 半透明横条,行高 40、圆角 8、底部进度条;正文基准字号 16」;经典 WinIsland(兼容)(valuewinisland):「保留胶囊 + 详情卡外观,仅建议老用户保持;岛字号默认 13」。 下方常驻一行 Warning 说明(el-alert type="warning",不可关闭):「从旧版本升级的配置默认使用经典 WinIsland 外观;切换到 ClassIsland 风格会改变经典形态的外观,消息暂存/紧急/叫人等规则不变。」——这就是老用户升级默认 winisland 的用户侧说明。
- 切换行为:
family写入islandAppearance.family,通过applyIslandAppearance({ family })实时应用(islandSetAppearance),点「保存设置」才写config.island。切换不得重置另一族的参数值(两族参数分别保留在IslandAppearance中)。 字号语义随 family 切换:classisland下appearance.fontSize是正文基准字号,标签文案必须为「正文字号(ClassIsland 基准)」,默认16,并由它派生 14/18/20 三档阶梯(次级/强调/大号);winisland下保持旧语义,标签文案必须为「岛字号(经典)」,默认13,岛内全部文本按旧系数缩放。切换 family 时必须切换标签文案与「默认值」提示(如滑块右侧显示「默认 16」/「默认 13」),但不得在切换瞬间强制改写用户已设置的值;仅当值为空/非法时才用当前 family 的默认值。 - 可见性规则:
layout === 'custom'时family控件disabled,并显示「自定义方案始终使用 ClassIsland 规格;外观族只作用于经典固定形态。」;layout === 'fixed'时按family显示下方参数组:classisland显示 9.5,winisland显示 9.6。 - 固定信息行预览缩略(新增,P1):在
classislandfixed 下于外观卡顶部放一个纯 CSS 缩略图(宽 360×高 44,圆角 8,背景#000000+ 不透明度 0.5,内含「10月17日 周五」「语文 08:00-08:40」两段文本与底部 3px 进度条),标注「示意:默认 1 行 = 日期 + 课程表;实际内容由日程数据驱动」。禁止用截图或额外窗口实现,必须是 DOM/CSS(与「编辑预览不创建额外桌面窗口」同源约束,docs/app/island.md:47)。
9.5 classisland 族外观参数(逐项)
所有控件改变时立即调用 applyIslandAppearance({...}) 实时预览;saveIslandAppearance() 再写盘。范围/默认值证据见 packages/shared/src/constants.ts:264-302、packages/shared/src/island-layout.ts:20-36;新增字段以 parts-r/05 为准。
| # | 控件(label-width 110 左侧标签) | 字段 | 控件类型与范围 | 默认 | 生效时机 | 备注 |
|---|---|---|---|---|---|---|
| 1 | 界面缩放 ${scale} |
appearance.scale【新】 |
el-slider 0.75–2.5,step 0.05,tooltip F2 |
1 | 即时 | ClassIsland Scale 语义(repos/ClassIsland/ClassIsland/Views/SettingPages/AppearanceSettingsPage.axaml:27-40);layout=custom 时隐藏(缩放在编辑器方案设置内) |
| 2 | 圆角 ${radius}px |
appearance.radius(有则用;缺省取当前方案) |
el-slider 0–20,step 1 |
8 | 即时 | layout=custom 时同时写 activeScheme.radius 并 saveConfig(保留现有同步,IslandSettingsView.vue:38-40、:113-122) |
| 3 | 背景不透明度 ${round(opacity*100)}% |
appearance.opacity |
el-slider 0–1,step 0.05 |
0.5 | 即时 | layout=custom 时同步 activeScheme.opacity |
| 4 | 行间距 ${spacing}px |
appearance.spacing【新】 |
el-slider 0–150,step 1 |
5 | 即时 | 与 scheme.spacing 同步(custom);ClassIsland MainWindowLineVerticalMargin 默认 5(Models/Settings.cs:219) |
| 5 | 主题色 |
appearance.accent |
el-color-picker |
#0a84ff |
即时 | 用于高亮、按钮与进度条;ClassIsland 强调色默认 #00BFFF(Settings.cs:35),本实现沿用 ClassHelper 主题色 |
| 6 | 前景(文字)颜色 |
appearance.foreground【新】 |
el-color-picker + 开关「自定义」 |
关 / #ffffff |
即时 | 自定义前景色覆盖主题派生色(ClassIsland IsCustomForegroundColorEnabled+CustomForegroundColor,AppearanceSettingsPage.axaml:536-551) |
| 7 | 字体 |
appearance.fontFamily【新】 |
el-select 可搜索,默认项文案「HarmonyOS Sans SC(默认)」 |
默认字体 | 即时 | 选项来自系统字体或内置列表;ClassIsland 同(:405-448) |
| 8 | 字重 |
appearance.fontWeight【新】 |
el-select:Thin/ExtraLight/Light/Normal/Medium/SemiBold/Bold/ExtraBold/Black/ExtraBlack |
Medium(500) | 即时 | ClassIsland MainWindowFontWeight2 默认 Medium(Settings.cs:101) |
| 9 | 次级字号(派生 14) | appearance.secondaryFontSize【新】 |
el-input-number 8–96,后缀 px |
14(未显式设置时由 fontSize 派生阶梯给出) |
即时 | 时间点附加信息等;classisland 默认阶梯 14/16/18/20 |
| 10 | 正文字号(ClassIsland 基准) ${fontSize}px |
appearance.fontSize |
el-input-number 8–96 或 slider 8–40,后缀 px |
16 | 即时 | classisland 语义:正文基准字号(repos/ClassIsland/ClassIsland/Views/SettingPages/AppearanceSettingsPage.axaml:482-533 四档字号以正文为基准);派生 14/18/20 三档;bodyFontSize 若存在则作为显式覆盖(以 parts-r/05 为准) |
| 11 | 强调字号(派生 18) | appearance.emphasizedFontSize【新】 |
同上 | 18(派生) | 即时 | 课程表、时钟、提醒标题、天气预报 |
| 12 | 大号字号(派生 20) | appearance.largeFontSize【新】 |
el-input-number 8–128,后缀 px |
20(派生) | 即时 | 课程预报 |
| 13 | 分体显示 |
appearance.separated【新】 |
el-switch |
关 | 即时 | 行之间透明分隔;ClassIsland IsIslandSeperated(Settings.cs:1852) |
| 14 | 进度条 |
appearance.progressBar【新】{ show, height, color } |
开关 + 高度 2–6px(默认 3)+ 颜色(默认强调色) | 显示/3px/强调色 | 即时 | ClassIsland 3px 进度条(brief §1.2);隐藏时行底不留空白 |
| 15 | 显示位置 |
appearance.position |
el-select 6 项(顶/底 × 左/中/右) |
top-center |
即时 | 现有 ISLAND_POSITIONS;classisland fixed 默认 top-center |
| 16 | 左右边距 |
appearance.marginX |
el-slider 0–200,step 2,居中停靠时禁用 |
8 | 即时 | 仅左右停靠生效,保留现有禁用逻辑与解释(IslandSettingsView.vue:88-99、:324-337) |
| 17 | 上下边距 |
appearance.marginY |
el-slider 0–160,step 2 |
8 | 即时 | 停靠顶/底生效 |
| 18 | 跟随鼠标屏幕 |
appearance.followCursorDisplay |
el-switch |
关 | 即时 | 多显示器 |
| 19 | 动画 |
appearance.animations |
el-switch |
开 | 即时 | 关闭后展开/收起瞬时完成 |
| 20 | 动画速度 |
appearance.speed |
el-slider 0.5–2,step 0.1 |
1 | 即时 | 与 spring.ts/CSS 时长倍率联动(part 10) |
| 21 | 始终置顶 |
appearance.alwaysOnTop |
el-switch |
开 | 即时 | |
| 22 | 明暗主题 |
appearance.theme【新】'auto'|'light'|'dark' |
el-select:跟随应用/明亮/黑暗 |
跟随应用 | 即时 | 岛主题独立于主窗口 config.theme;ClassIsland 应用主题三态(AppearanceSettingsPage.axaml:123-135) |
新增字段(1/4/6/7/8/9–14/22)必须由 parts-r/05 定稿字段名与 normalize 边界;本表给出默认值,任何越界值在 normalizeIslandAppearance 内钳制,禁止出现 NaN(现有归一化参考 main/island.ts:2229 的注释与 config.ts 的 normalize 调用)。
字号单一事实来源:appearance.fontSize 在 classisland 族是正文基准(默认 16),四档字号中「正文 = fontSize」「次级/强调/大号 = fontSize - 2 / + 2 / + 4」;若用户显式修改次级/强调/大号,则写 secondaryFontSize/emphasizedFontSize/largeFontSize 覆盖派生值(字段最终由 parts-r/05 定)。layout=custom 时字号由编辑器方案与组件外观控制,设置页这几项隐藏。
9.6 winisland 族外观参数(兼容保留,不得删)
family === 'winisland' 时渲染下列全部控件,字段、范围、默认值、文案与现有实现一致,禁止改变默认外观(老用户回退依赖它):
| # | 控件 | 字段 | 范围/step | 默认 |
|---|---|---|---|---|
| 1 | 高度 ${height}px |
appearance.height |
36–72/1 | 44 |
| 2 | 宽度 ${width}px |
appearance.width |
220–420/2 | 268 |
| 3 | 圆角 ${radius}px |
appearance.radius |
8–32/1 | 20 |
| 4 | 不透明度 ${%} |
appearance.opacity |
0.4–1/0.05 | 1 |
| 5 | 岛字号(经典) ${fontSize}px |
appearance.fontSize |
11–20/1 | 13(旧版语义;新装默认由 family 决定为 16,见下) |
| 6 | 动画速度 ${speed}x |
appearance.speed |
0.5–2/0.1 | 1 |
| 7 | 主题色 |
appearance.accent |
颜色 | #0a84ff |
| 8 | 视觉风格 |
appearance.style |
纯黑 black/毛玻璃 glass/主题色渐变 tinted |
black |
| 9 | 显示位置 |
appearance.position |
6 项 | top-center |
| 10 | 左右边距 |
appearance.marginX |
0–200/2,居中禁用 | 8 |
| 11 | 上下边距 |
appearance.marginY |
0–160/2 | 8 |
| 12 | 跟随鼠标屏幕 |
appearance.followCursorDisplay |
开关 | 关 |
| 13 | 动画 |
appearance.animations |
开关 | 开 |
| 14 | 始终置顶 |
appearance.alwaysOnTop |
开关 | 开 |
| 15 | 空闲细缝 |
appearance.idleSliver |
开关 | 关 |
| 16 | 预览效果 |
— | 按钮:islandPush 一条 priority: 'NORMAL'、context.preview = true 的本地通知(30 秒自动消失、失焦不收起) |
— |
证据:默认值与范围 shared/src/constants.ts:264-302;字段语义 shared/src/types.ts:1145-1188;ISLAND_STYLE_LABELS constants.ts:323-330;预览实现 IslandSettingsView.vue:141-155。字号/高度/宽度滑块若 family=classisland 时被隐藏,不得清空其值(切回 winisland 仍生效)。
fontSize 双语义(必须按 family 区分):winisland 下它是「岛字号」,保持旧语义与旧系数、旧版默认 13(constants.ts:272),标签必须写「岛字号(经典)」;classisland 下它是「正文字号基准」,默认 16,派生 14/18/20 阶梯(见 9.5)。老配置升级时保留用户原来的 fontSize 数值,不因 family 迁移而改写;新安装默认 16。设置页在切换 family 时只切换标签与默认值提示,不强制改写已有值。
9.7 课程提醒设置(并入本页)
在「课程提醒」卡内按 ClassHelper 既有 lessonReminders 字段渲染 5 个开关 + 2 个阈值;保存走 saveConfig({ islandLayout: { ...config.islandLayout, lessonReminders: {...} } })(现有 ReminderSettingsView.vue:8-24 的逻辑迁移过来),迁移不改变字段名与默认值(shared/src/island-layout.ts:140-158)。
| 控件 | 字段 | 控件类型/范围 | 默认 | 语义对齐 |
|---|---|---|---|---|
| 准备上课提醒 | prepareEnabled |
el-switch |
开 | ClassIsland IsClassPrepareNotificationEnabled(Settings.cs:47) |
| 上课提醒 | onClassEnabled |
开关 | 开 | IsClassChangingNotificationEnabled(Settings.cs:46) |
| 即将下课倒数 | endingEnabled |
开关 | 开 | 下课前秒数徽标;ClassIsland 蓝色圆形徽标、剩余一秒显示 -1 |
| 经典岛课中常显课程进度 | classProgressEnabled |
开关 | 开 | 仅 windowisland/经典形态显示;classisland fixed 用行底进度条 |
| 下课提醒与下节课预告 | breakEnabled |
开关 | 开 | IsClassOffNotificationEnabled(Settings.cs:55) |
| 课前提前(秒) | prepareSeconds |
el-input-number 5–1800,step 5 |
60 | ClassIsland ClassPrepareNotifySeconds(Settings.cs:43) |
| 下课倒数提前(秒) | endingSeconds |
el-input-number 5–600,step 5 |
60 | ClassHelper 既有语义;ClassIsland 无同名项,标注【映射】 |
布局:每行使用现有 .lesson-setting-row(左右分布、padding: 12px 0、底部分隔线 1px solid var(--ch-border),ReminderSettingsView.vue:126-137),阈值行紧随 5 个开关之后。所有改动即时保存;保存失败 ElMessage.error('提醒设置保存失败')(沿用现有文案)。
提醒页 /settings/reminder 必须改为:顶部链接卡「上下课提醒已移至【灵动岛 → 课程提醒】」→ 按钮跳 /settings/island;其余「通知提醒」「成绩查询」卡保持不变。不得两处都可编辑,避免双写。
9.8 保存、实时预览与失败重试(设置页)
- 两条通道必须区分:
- 外观改动 →
window.desktop.islandSetAppearance({ ...appearance })(纯对象;主进程 16ms 合并,main/island.ts:2389-2410)→ 立即生效但不落盘; - 点「保存设置」→
window.desktop.saveConfig({ island: { ...appearance } })→ 落盘(合并写 120ms,main/config.ts:252-298)→ 成功ElMessage.success('个性化设置已保存'),失败弹出错误并保留savingIsland=false以允许重试。
- 外观改动 →
- 禁止传响应式 Proxy:所有 IPC 入参必须是展开后的纯对象(
types/desktop.d.ts:143-152、:166-175;现有注释已写明An object could not be cloned.的坑)。设置页已有的{ ...island.value }、JSON.parse(JSON.stringify(...))写法必须保留。 - 圆角/行距/不透明度同步 custom 方案:当
layout === 'custom'时,这三项除写 appearance 外还要写activeScheme.radius/opacity/spacing并saveConfig({ islandLayout: 深拷贝 });失败提示「圆角保存失败,请重试」(现有文案,IslandSettingsView.vue:115-120),并不要回滚滑块显示值(下次保存重试)。 - 恢复默认:
恢复默认按钮必须confirmAction('恢复灵动岛的默认外观?', '恢复默认', { type: 'warning' }),确认后applyIslandAppearance({ ...DEFAULT_ISLAND_APPEARANCE })再保存;新默认 为 classisland 族参数(DEFAULT_ISLAND_APPEARANCE扩展后),但恢复默认不得覆盖family(用户显式选择要保留;如产品要一并重置,需二次确认)。 - 载入与刷新:
onMounted调islandGetAppearance()与getConfig();window.addEventListener('focus', loadLayout)(现有:82-86)保留,确保从编辑器返回后方案列表/圆角同步。 - 预览效果按钮:仅
family=winisland显示(classisland fixed 预览用缩略图 + 真实运行态验证);点击推送预览通知(id 前缀appearance-test-),说明文案必须含「拖动滑块即可实时预览」。
9.9 编辑器总体布局(像素级)
文件:IslandEditorView.vue;样式:styles/island-editor.css。保留现有 DOM 结构(smoke 依赖,见 9.20),只做尺寸/对齐调整。
- 页面容器:
.island-editor-page保持display:flex; flex-direction:column; flex:1 0 auto; padding: 12px 24px 16px; background: var(--ch-bg)(:1-10);:has(> .island-editor-page)让.main也纵向 flex(:11-14)。 - 顶部操作条
.editor-actions:右对齐、gap:8px、min-height:30px、margin-bottom:4px(:19-25);元素依次为保存状态文本(12px,见 9.18)、返回设置(text small)、应用并返回(primary small,loading=!!pending,disabled=!ready)。 - 页头
.component-heading:align-items:flex-end; gap:24px; margin-bottom:20px(:35-39);左侧标题「组件」font-size: 34px*var(--ch-font-scale)、font-weight:600、line-height:1.4、margin-bottom:20px;说明「以下组件将显示在主界面上,从组件库拖动组件以添加组件。」16px(:40-53);右侧方案工具区flex: 0 1 348px; min-width:300px(:54-57)。 - 错误条:
error非空时在页头下显示el-alert type="error",内容为错误消息 +重试按钮(ready ? save() : load())(:608-610)。 - 内容工具条
.surface-toolbar:左右两组;左侧.surface-tabs两个按钮「常态内容」「无通知展开内容」(12px,选中下方 2px 主色下划线,:102-124);右侧.surface-actions三个按钮:新增行、行设置、进入编辑模式/退出编辑模式(最后一个选中时 primary)(:612-653)。「新增行」文案不可改(smoke 断言依赖)。 - 预览画布
.preview-canvas:min-height:80px; overflow:auto; padding:10px 0 20px(:125-129);live-preview时改为display:grid; place-items:center; min-height:160px; padding:24px; background:var(--ch-layer-alt); border-radius:6px; margin-top:8px(:184-192)。 - 标签态
.component-lines(min-width:100%; width:max-content,:130-133)逐行渲染.component-line:display:flex; align-items:center; min-height:46px; padding:2px 4px; border:1px solid transparent; border-radius:var(--ch-radius-control); gap:12px;行与行间距改为margin-top: var(--preview-gap)(值取scheme.spacing,默认 5),不再固定 6px(原:143-145;对齐 ClassIsland 行距默认 5);- 选中/激活行
border-color: var(--ch-border-strong);拖放目标行outline:2px dashed var(--ch-accent); outline-offset:-2px; background:var(--ch-accent-soft)(保留:228-232)。 - 行把手
.row-handle:宽20px(原 24,改以对齐 ClassIsland 拖拽把手宽度 20,repos/ClassIsland/ClassIsland/MainWindow.axaml:496)、align-self:stretch、图标「Ⅱ」16px、cursor:grab、touch-action:none;点击选中行、拖动排序(保留双行为)。 - 行内
.line-components:display:flex; align-items:center; gap: var(--row-gap)(取row.gap,默认 8)、min-height:40px(对齐行高 40)、flex:1、padding-right:28px(为行尾按钮留位,保留); - 空行占位
.empty-line保留「将组件拖到这里,或点击下方组件添加」。
- 实时预览态
.island-preview:display:flex; flex-direction:column; gap:var(--preview-gap); width:max-content; padding:12px; border-radius:var(--preview-radius); background:#202024; color:#ffffffeb(:193-202);.preview-row改min-width:120px; min-height:40px(原 44);separated时容器透明、每行独立padding:12px;background:#202024(:210-217);缩放通过zoom: scheme.scale * zoom生效(现有模板:700-708)。 - 组件库
.component-library:上边框分隔、margin:0 -24px; padding:14px 24px 24px;标题「组件库」17px/600 + 3px 主色下划线;网格grid-template-columns: repeat(4, minmax(0,1fr)); gap:12px 20px;每项min-height:76px; padding:10px 8px; gap:12px; border:1px solid var(--ch-border); border-radius:var(--ch-radius-control),图标 36×36,名称 18px/500,描述 14px 次级(:249-330);响应式 1150px→3 列、800px→2 列、520px→1 列(:371-417)。11 项顺序必须为:文本、分割线、课程表、日期、时钟、天气简报、倒计时、轮播容器、滚动容器、分组容器、堆叠容器(IslandEditorView.vue:58-70)。 - 抽屉
.el-drawer.island-inspector:宽min(360px, 94vw)、modal=false、modal-penetrable、append-to-body、lock-scroll=false(:781-790);标题「组件设置」;内部顶部el-radio-group三个页签「方案/行/组件」(组件不可选时 disabled);抽屉背景/输入框样式沿用island-editor.css:346-361。抽屉动画统一 250mscubic-bezier(0,0,0,1)(对齐 ClassIslandDrawerHost:repos/ClassIsland/ClassIsland.Core/Controls/DrawerHost.axaml:42-59),并在AnimationLevel=0/prefers-reduced-motion时改为无动画。 - 拖拽幽灵
.drag-ghost:position:fixed; z-index:10000; padding:10px 16px; border-radius:6px; border:1px solid var(--ch-accent); box-shadow:var(--ch-shadow-flyout),位置pointer.x+16 / pointer.y+16(:331-342、模板:774-780)。 - 导入文件输入:隐藏
<input type=file accept="application/json,.json">(:773)保留。 - 骨架:
ready=false时el-skeleton :rows="8" animated;ready=true才渲染全部编辑 UI(:611)。
9.10 方案栏(保留并升级)
- 选择方案:
el-select绑定settings.activeSchemeId,选项scheme.name;disabled=!ready;@change="switchScheme"(重置选中 + 保存)。aria-label="当前方案"保留(:557-568)。 - 重置当前方案:图标按钮(Refresh)
aria-label="重置当前方案"、title="重置当前方案";确认框标题「重置方案」,正文「将「{方案名}」的组件和外观恢复为默认配置?」,按钮「重置/取消」,type: warning;确认后用默认方案内容替换但保留原 id 与 name,并校验组件容量(现有resetScheme,:403-422)。 - 新建方案(Plus,
aria-label="新建方案"):ElMessageBox.prompt('为新的组件方案命名', '新建方案', { inputValue:'新方案', inputValidator: 1–64 字符, confirmButtonText:'创建', cancelButtonText:'取消' });方案数上限 20(schemeCapacity,:379-383);新方案内容复制默认方案并重生成全部 id(:384-402、renewScheme:371-378);创建后自动选中并保存。 - 管理方案(FolderOpened 图标,
aria-label="管理方案",el-dropdown trigger=click)菜单顺序固定:方案设置、复制方案、分隔、导入方案、导出方案、分隔、删除方案(方案数 < 2 时禁用)(:589-604、:423-432)。 - 复制方案:深拷贝当前方案,名称
${name.slice(0,61)} 副本,重生成 id,push 后选中;受 20 方案与 200 组件上限约束(:214-230)。 - 删除方案:
ElMessageBox.confirm('删除方案「X」?', '删除方案', { confirmButtonText:'删除', cancelButtonText:'取消', type:'warning' });删除后activeSchemeId = schemes[0].id并保存(:231-245)。 - 导出方案:JSON 结构
{ format: 'classhelper-island-layout', scheme }、2 空格缩进、Blob下载;文件名把[<>:"/\\|?*]替换为_;下载后 1srevokeObjectURL(:246-257)。该 format 字符串是老用户导入兼容契约,不得改。 - 导入方案:文件 ≤ 1 MB;解析后校验
format === 'classhelper-island-layout'且scheme.rows为非空数组;经normalizeIslandLayouts归一化;重生成全部 id;容量校验;成功ElMessage.success('方案已导入'),失败ElMessage.error(错误信息);finally清空 input(:258-289)。 - 方案设置抽屉页:名称(
maxlength=64)、界面缩放(input-number 0.75–2.5、step 0.05,绑scheme.scale)、行间距(slider 0–150,绑scheme.spacing)、常态圆角(slider 0–20,绑scheme.radius)、各行分离显示(switch,绑scheme.separated);底部提示「字体、透明度、位置和动画与灵动岛共用设置。」+ 链接「前往灵动岛外观设置」→/settings/island(:832-857)。必须保留这些字段与控件类型(ClassIsland 方案级参数:缩放/行距/圆角/分离,见repos/ClassIsland/ClassIsland/Views/SettingPages/AppearanceSettingsPage.axaml:27-118)。
9.11 行与行设置(ClassIsland 编辑模式对齐)
- 行列表:每行渲染
row.name(编辑态以把手 aria-label「编辑{行名}」、title「{行名},拖动排序,点击设置」暴露,IslandEditorView.vue:670-678);行数上限 12(addRow,:142-159,文案「每种内容最多添加 12 行」)。 - 新增行:保留工具条「新增行」按钮;新增行列表末尾居中的第二入口:32px 高、图标
E00D(20px)、文字「新增行」,样式与 ClassIsland「新主界面行」一致(repos/ClassIsland/ClassIsland/MainWindow.axaml:449-455),点击行为与工具条相同。新行默认name: 组件行 N、main:false、allowNotifications:true、gap:8、nodes:[]。 - 排序:拖行把手到另一行位置上/下方即可重排(现有
dropPreview的current.rowId分支,:505-512);也提供抽屉内「上移/下移」按钮(:824-830)。 - 主行:抽屉按钮「设为主要行」;已是主行时按钮禁用且文案「已设为主要行」;设置主行会清除其他行
main(setMain,:210-213);行数为 1 时不允许删除(removeRow,:202-209)。 - 允许提醒:开关「允许显示提醒」绑定
row.allowNotifications;提示文案「提醒优先使用主要行;关闭提醒后使用下一条允许提醒的行。」(:817-823)。 - 行间距:
el-input-number0–64,绑row.gap(组件间距);方案级行间距在方案设置抽屉(scheme.spacing0–150)。两者不得混用。 - 行设置抽屉规格:标题「组件行」;字段顺序固定:当前行下拉(可切换编辑行)、行名称(maxlength 64)、组件间距、允许显示提醒、主行按钮、提示、上移/下移/删除此行(danger plain)。抽屉沿用 360px 非模态规格;打开/关闭动画 250ms
cubic-bezier(0,0,0,1)。 - ClassIsland 对齐点(必须实现):
- 选中框:
1.5px虚线、dasharray:1,1.5、opacity:0.75、圆角跟随行/组件圆角;选中色AccentFillColorDefaultBrush等价主色,未选中容器为SurfaceStrokeColorDefaultBrush等价--ch-border-strong(repos/ClassIsland/ClassIsland/Controls/EditMode/EditableComponentsListBox.axaml:225-251;MainWindow.axaml:353-366); - 拖动把手:行把手 20px 宽、组件 chip 自身可拖(
TouchDragThumb等价);拖拽时被拖元素opacity:0.5(ClassIsland 拖动预览透明度 0.5,MainWindow.axaml:478-481;ClassHelper 可保留现有 ghost 不降低原元素透明度,二选一但需统一); - 行内组件外边距:组件外层左右各 6px(
Margin="6 0",repos/ClassIsland/ClassIsland/MainWindow.axaml:388-399、:538-548),叠加行内gap后形成视觉间距;运行态组件无此外边距; - 行内容留白:行内左右
12px(Margin="12 0"、MinWidth="20",repos/ClassIsland/ClassIsland/XamlThemes/FluentTheme/Styles.axaml:203-232);编辑态为可选中的把手/加号额外留位; - 行高:
40px(IslandContainerHeight,repos/ClassIsland/ClassIsland/Controls/MainWindowLine.axaml:16;MainWindow.axaml:530-537容器行高 40)。
- 选中框:
- 既有交互必须保留:行把手拖动排序、点击行选中并打开行设置、空行点击打开行设置、行尾「新增行」、主行/提醒开关的即时保存、最多 12 行提示。
9.12 组件库(11 类,保留既有 DOM 契约)
- 数据源固定 11 项、顺序见 9.9-9;每项包含
kind、描述(现有library数组,IslandEditorView.vue:58-70)。 - 每个库项必须是
button,带data-library-kind={kind}、aria-label="添加{中文名}";保留pointerdown开始拖拽、click直接添加(两者共存,拖动后suppressClick防误加,:754-768、:447、:500-503)。 - 直接添加逻辑:若当前选中容器则加入该容器(层级 ≥ 6 时警告「最多嵌套 6 层容器」),否则加入当前行末尾;添加后自动选中并保存(
add,:130-141)。 - 组件图标使用
ComponentIcon(:763);名称与描述分别 18px/14px(随--ch-font-scale缩放)。 - 消息类组件(
notification/homework/call)不得出现在库中,但既有方案里的这些节点必须继续渲染与可编辑(只读提示「此组件使用实时数据。可在外观和尺寸设置中调整显示。」)。
9.13 拖拽规格(三类源,必须保留现有实现)
- 统一 Pointer Events:
pointerdown(仅button===0)→ 在源元素setPointerCapture(合成事件失败时 try/catch 忽略)→pointermove超过 6px 才判定开始拖拽 →pointerup落点;pointercancel/blur/画布外释放一律取消(beginDrag/moveDrag/dropPreview/cancelDrag,IslandEditorView.vue:433-503;模板事件绑定:674、:688、:730、:760)。 - 三种拖拽源:组件库项(
kind源)、组件 chip(nodeId源)、行把手(rowId源)。 - 落点判定:
- 用
document.elementFromPoint+closest('[data-row]')找行;无行则取消; - 行内找
[data-node]:未命中节点 → 落到行尾;命中节点 → 计算指针在节点宽度内的比例ratio; - 容器节点(
group/stack/slide/rolling)且0.2 < ratio < 0.8→inside;否则ratio > 0.5→after,否则before(dragTarget,:462-481); - 拖拽期间落点高亮:行级
outline:2px dashed,节点before/after显示 3px 主色竖条(IslandEditorNode.vue:107-123),inside高亮容器(:107-110)。
- 用
- 约束:单次拖拽内不得把父节点放进自己的后代(
all([node]).some(child.id === target.node.id)→ 忽略,:517);目标层级 + 被拖子树高度 ≤ 6(:518-522);组件总数 ≤ 200(capacity,:338-347)。 - 落库:移动完成后
selectedId指向被拖节点、rowId指向目标行、panel='component',立即save()(:534-537)。 - ghost:仅
pointer.moved后显示,文本为组件中文名或行名(dragging.label),偏移 +16/+16。 - 触摸:必须继续支持
pointerType==='touch'(现有 smoke 用合成 touch 事件验证跨行拖动,main/smoke-layout.ts:74-91);拖动时touch-action:none、禁止页面滚动(.component-chip、.row-handle、.library-item已设置)。
9.14 选中、组件设置抽屉(四页签)
- 选中:点击组件 chip →
selectNode:设置selectedId、rowId、panel='component'、打开抽屉;点击行把手/空行 →selectRow:清空selectedId、panel='row';切换方案/常态/展开内容时resetSelection()(IslandEditorView.vue:106-125)。选中框按 9.11-8 规格。 - 抽屉结构:顶部
el-radio-group(方案 / 行 / 组件);component页签在无选中时禁用;内容区渲染IslandComponentSettings(组件)或行/方案表单(IslandEditorView.vue:791-857)。 - 四页签(
IslandComponentSettings.vue重构,顺序固定):内容、外观、尺寸、隐藏条件。每页签使用el-form label-position="top";选项选择类控件宽度 100%;数字/颜色控件与标签同一行(现有:283-307样式保留)。 - 跨页签即时保存:任一控件
@change必须emit('change')→ 编辑器save()(现有模式);不设「应用」按钮。 - 删除/复制/包裹入口:内容页底部固定「包裹到容器」下拉(
data-action="wrap")、「复制组件」、「删除」(danger plain)(:152-170);复制节点时深拷贝并重生成自身与所有后代 id(copyNode,IslandEditorView.vue:180-188);包裹受层级与容量约束(wrap,:189-201)。 - 抽屉 DOM 契约:
.component-settings、.el-tabs__item数量为 4、.el-tab-pane的可见页内容互斥;编辑器 smoke 会按页签断言内容(现有 3 页签断言见main/smoke-layout.ts:111-137,重构后由 parts-r/11 同步更新为 4 页签)。
9.15 组件设置项清单(与 parts-r/07 组件契约一一对应)
字段名直接来自 packages/shared/src/island-layout.ts:77-108(settings/appearance/advanced)。默认值与取值范围以 normalizeIslandLayouts 的 clamp 为准(:268-332)。任何新增字段必须先落到 shared 契约并在 parts-r/07 说明渲染方式。
9.15.1 内容页(按 kind)
| kind | 控件 | 字段 | 类型/范围 | 默认 | 生效 |
|---|---|---|---|---|---|
| 全部 | (无内容设置时)提示 | — | 文本「此组件使用实时数据。可在外观和尺寸设置中调整显示。」 | — | — |
clock |
12 小时制 | settings.hour12 |
switch | false | 即时 |
clock |
显示秒 | settings.seconds |
switch | true | 即时 |
countdown |
倒计时目标 | settings.target |
select:自动 auto/下一节课 next/本节下课 end/放学 school/指定日期 date |
auto(编辑器新建时置为 date,makeNode IslandEditorView.vue:359-363) |
即时 |
countdown |
目标名称(仅 date) | settings.countdownTitle |
input maxlength 64 | 目标日 |
即时 |
countdown |
目标日期(仅 date) | settings.targetDate |
ch-date-picker YYYY-MM-DD,必须 isDayKey 合法 |
'' |
即时 |
weather |
地点名称 | settings.locationName |
input maxlength 64 | '' |
即时 |
weather |
纬度 | settings.latitude |
input-number -90–90,precision 4,允许 null |
null |
即时 |
weather |
经度 | settings.longitude |
input-number -180–180,precision 4,允许 null |
null |
即时 |
text |
文字内容 | settings.text |
textarea rows 3,≤2000 字 | 班级信息 |
即时 |
text |
启用模板变量 | settings.template |
switch;提示「支持 {date} 日期、{time} 时间、{course} 当前课程。」 | false | 即时 |
schedule |
显示课程 | settings.display |
select:当前时间点 current/下一节课 next/今日时间点 today |
current |
即时 |
schedule |
显示时间点数(仅 today) | settings.count |
input-number 1–30(normalize 1–40) | 5 | 即时 |
schedule |
当前时间点显示起止时间 | settings.showPeriod |
switch | true | 即时 |
schedule |
当前课程显示进度 | settings.showProgress |
switch | true | 即时 |
group/stack/slide/rolling |
排列方向 | settings.direction |
select:横向 horizontal/纵向 vertical |
horizontal |
即时 |
| 同上 | 组件间距 | settings.gap |
input-number 0–64 | 8 | 即时 |
slide |
切换间隔(秒) | settings.interval |
input-number 1–300 | 5 | 即时 |
rolling |
滚动速度 | settings.speed |
input-number 1–200 | 30 | 即时 |
separator/date/notification/homework/call |
— | — | 无内容参数(消息类显示只读提示) | — | — |
9.15.2 外观页(所有 kind 通用,appearance)
| 控件 | 字段 | 类型/范围 | 默认 | 生效 |
|---|---|---|---|---|
| 独立外观 | appearance.resourceOverridingEnabled |
switch | false | 即时;关闭时其余项禁用并沿用岛外观 |
| 辅助文字字号 | appearance.secondaryFontSize |
input-number 8–96 | 14 | 即时 |
| 正文字号 | appearance.bodyFontSize |
同上 | 16 | 即时;组件级覆盖,仅 custom 使用;classisland fixed 的四档字号来自 IslandAppearance.fontSize 派生(见 9.5),本项不参与 |
| 强调文字字号 | appearance.emphasizedFontSize |
同上 | 18 | 即时 |
| 大号文字字号 | appearance.largeFontSize |
input-number 8–128 | 20 | 即时 |
| 自定义文字颜色 | appearance.customForegroundEnabled + appearance.foreground |
switch + color-picker(#rrggbb) |
关 / #ffffff |
即时 |
| 自定义背景颜色 | appearance.customBackgroundEnabled + appearance.background |
switch + color-picker | 关 / #202024 |
即时 |
| 自定义背景透明度 | appearance.customBackgroundOpacityEnabled + appearance.backgroundOpacity |
switch + input-number 0–1 step 0.05 | 关 / 0.5 | 即时 |
| 自定义圆角 | appearance.customCornerRadiusEnabled + appearance.cornerRadius |
switch + input-number 0–100(ClassIsland 上限 20 用于岛;组件可为 100) | 关 / 16 | 即时 |
| 组件透明度 | appearance.opacity |
slider 0–1 step 0.05 | 1 | 即时 |
9.15.3 尺寸页(原「高级」拆分,advanced)
| 控件 | 字段 | 类型/范围 | 默认 | 生效 |
|---|---|---|---|---|
| 对齐方式 | advanced.align |
select:拉伸 stretch/起始 start/居中 center/末端 end |
stretch |
即时 |
| 固定宽度(px) | advanced.width |
input-number 0–2000(0=自适应) | 0 | 即时 |
| 最大宽度(px) | advanced.maxWidth |
input-number 0–2000(0=不限制) | 0 | 即时 |
| 提示 | — | 「宽度为 0 时自适应;对齐在父容器设定宽度时生效。」 | — | — |
9.15.4 隐藏条件页
| 控件 | 字段 | 类型/取值 | 默认 | 生效 |
|---|---|---|---|---|
| 隐藏条件(多选) | advanced.hideRules |
checkbox-group:上课时 in-class/没有消息时 no-message/没有当前课程时 no-class |
[] |
即时;数组只保留这三个合法值(normalize :321-328) |
| 高级说明 | — | 文本「隐藏条件基于当前课堂与消息状态即时生效;不会释放组件资源。」 | — | — |
ClassIsland 对齐:ClassIsland 用「按规则隐藏 + 规则集」承载同一语义(repos/ClassIsland/ClassIsland/Views/SettingPages/ComponentsSettingsPage.axaml:423-443);本轮不引入规则集引擎,保留 ClassHelper 三枚举,标注【映射】。若 parts-r/05 增补 advanced.hideRules 扩展,必须同步本页与 parts-r/07。
9.16 预览与「进入编辑模式」
editMode为页面内状态(ref(false)),不打开额外桌面窗口、不注入 iframe(docs/app/island.md:47);smoke 会断言不存在.layout-editor/iframe(main/smoke-layout.ts:69-72、:154)。- 进入:工具条「进入编辑模式」→
editMode=true;画布切换为.island-preview真实组件渲染:ComponentNode以:node :state :now :schedules :time-points editing :selected-id alert-node-id=""渲染,state使用本地空状态对象(IslandEditorView.vue:47-57、:719-732);容器组件在编辑时展示全部子组件(ComponentNode的editing分支由 parts-r/07 定义)。 - 预览缩放:仅在编辑模式显示
.preview-toolbar:文本「预览缩放」+el-slider0.3–1.5、step 0.1、tooltip%+ 「重置缩放」(置 1)+ 提示「点选组件调整内容、外观和隐藏条件」(:739-750);缩放与方案缩放相乘(zoom: scheme.scale * zoom)。 - 布局约束:
preview-canvas必须完整容纳预览且不遮挡工具条——canvas.bottom <= footer.top + 1且页面无横向滚动(smoke 断言main/smoke-layout.ts:138-148);separated时按 9.9-8 渲染。 - 点击编辑态组件:与标签态一致打开抽屉(
@select);背景点击(.preview-row @click.self)选中该行。 - 退出:再次点击「退出编辑模式」回到标签态;抽屉保持打开状态但选中不变;
Esc优先关闭抽屉、再退出编辑模式(见 9.17)。 - 实时同步真实岛:编辑器每次
save()通过saveConfig触发主进程island.syncCustomLayout()(main/ipc.ts:34-45;main/island.ts:320-338),真实岛收到island:layout后热更新(LayoutIslandApp.vue:35-51);因此“预览”= 页面内预览 + 真实岛热更新,不新增窗口。
9.17 键盘、触摸与焦点
| 场景 | 按键 | 行为 |
|---|---|---|
| 编辑器全局 | Esc |
抽屉打开→关闭抽屉;否则编辑模式→退出编辑模式;否则返回 /settings/island(需保存成功后) |
| 选中组件 | Delete/Backspace |
删除选中组件(无后代时直接删;有后代弹确认「删除后其子组件一并删除」,确认后删);删除后 panel='row' |
| 选中组件 | Ctrl+D/Cmd+D |
复制选中组件(同「复制组件」,受 200 上限约束) |
| 选中组件/行 | Enter/Space |
打开组件设置/行设置抽屉 |
| 行把手/组件 chip | Tab → Enter |
键盘聚焦并选中;拖动需指针;不提供仅键盘拖动(标注【推断】,作为 P2 可加方向键移动) |
| 抽屉 | Tab 循环限制在抽屉内 |
el-drawer 自带焦点陷阱,保留 |
| 设置页 | 无全局快捷键 | 滑块可用方向键微调(Element Plus 默认) |
触摸:所有拖拽继续使用 Pointer Events(鼠标/触摸/笔统一);触摸时不得触发页面滚动与文本选择;touch-action:none 只加在可拖元素;抽屉在窄屏(<520px)宽 94vw。焦点可见性:所有自绘按钮必须有 :focus-visible 2px 主色 outline + 2px offset(现有 .library-item/.row-handle/.surface-tabs button 规则保留,island-editor.css:305-310)。
9.18 数据流、契约与失败处理
-
读取:编辑器
load()→window.desktop.getConfig()→settings = normalizeIslandLayouts(config.islandLayout)、appearance = config.island;失败显示错误条并提供「重试」(现有load,IslandEditorView.vue:300-313)。设置页同时读getConfig()与islandGetAppearance()。 -
保存队列(编辑器):每次改动
save():snapshot = normalizeIslandLayouts(JSON.parse(JSON.stringify(settings)))(必须先深拷贝纯对象,禁止 Proxy 过桥);revision++、pending++、saved=false;- 串行 promise 链
saveConfig({ islandLayout: snapshot });成功且current===revision时清error、saved=true;失败写error、saved=false;finally pending--(现有:81-105)。 - 状态文本固定四态:
error→ 「保存失败」;pending>0→ 「正在保存…」;saved→ 「已自动保存」;否则「更改自动保存到本机」(:543-546)。
-
退出保存:
onBeforeRouteLeave(async () => { if (!ready) return true; await save(); return !error })——有错不发离开(:333-337);「应用并返回」apply():强制settings.layout='custom'、residentEnabled=true,await save(),无错误才ElMessage.success('自定义灵动岛已应用')并router.push('/settings/island')(:290-299)。 -
失败重试:错误条
重试按钮按ready ? save() : load();保存失败不得清空本地编辑内容;不得自动回滚到磁盘值(避免用户输入)丢失;若连续失败需在控制台/日志打出 IPC 错误(沿用主进程 logger)。
-
应用/保存语义:编辑器每次改动即
saveConfig(含磁盘合并写 120ms,main/config.ts:252-298),所以「退出保存」不是唯一写入点;保存成功≠已点“应用”,但配置已写入islandLayout。layout只有在「应用并返回」或设置页选择自定义时才切到custom。 -
主进程侧:
ipcMain.handle(IPC_CHANNELS.saveConfig)→saveConfig(allowed)→ 若allowed.islandLayout则island.syncCustomLayout()(main/ipc.ts:34-45);syncCustomLayout()在常驻开关切换时重置 state,向岛渲染进程send('island:layout', getConfig().islandLayout)(main/island.ts:320-338);岛渲染层onLayout更新(LayoutIslandApp.vue:35-51)。 -
外观通道:
island:set-appearance主进程 16ms 合并后island.setAppearance(main/island.ts:2389-2410);island:get-appearance会先 flush 再返回(:2407-2410)。外观不落盘,除非设置页点保存写config.island。 -
迁移:
config.ts读盘时normalizeIslandLayouts+normalizeIslandAppearance(:198-233)。老用户(无island.family)→'winisland';新装默认'classisland'。实现方式(二选一,必须由 parts-r/05 定稿):- 在
readPersistedFromDisk里判断parsed.island是否存在且family缺省 →family:'winisland';或 islandLayout.version < 2且config.island存在 → 迁移为 winisland,并写version=2。fontSize迁移规则:旧配置已有island.fontSize(例如 13)时原样保留,不得因 family 从 classisland 变 winisland(或反向)而改写;旧配置没有该字段时才按迁移后的 family 取默认(winisland=13/classisland=16)。新安装/清空配置:family='classisland'且fontSize=16。DEFAULT_ISLAND_APPEARANCE的fontSize默认值需按此改为 16,旧默认 13 仅作为 winisland 语义说明保留在文档与文案中。 迁移失败必须回落默认配置并记日志,不得让设置页崩溃。
- 在
-
禁止:在渲染进程直接写 config 文件;在编辑器内创建第二个 BrowserWindow;把
ref/reactive直接传 IPC;用setBackgroundMaterial()影响岛透明(AGENTS.md客户端章节)。 -
契约对照(必须在 PR 中核对):本 part 用到的字段均来自
IslandComponentNode.settings/appearance/advanced、IslandLayoutRow、IslandLayoutScheme、IslandLayoutSettings、IslandAppearance(shared/src/island-layout.ts:61-158、shared/src/types.ts:1146-1188);新增字段以 parts-r/05 为准,组件渲染细节以 parts-r/07 为准,消息/课程提醒视觉以 parts-r/08 为准。
9.19 状态、空态、主题与性能约束
- 加载/空态:
ready=false前只渲染页头/方案栏骨架,禁止允许编辑(避免对未加载的scheme写入);编辑器用el-skeleton :rows="8",设置页用v-loading或同款骨架;settings.schemes由normalizeIslandLayouts保证至少 1 个(空输入回落默认方案,shared/src/island-layout.ts:399-402);若运行期出现空数组(异常导入),必须立即用defaultIslandLayouts().schemes[0]补位并提示「方案数据异常,已恢复默认方案」;- 行无节点时显示空行占位;容器无子组件时显示「拖入组件」(
IslandEditorNode.vue:47);未选中组件时抽屉「组件」页签禁用; - 删除当前选中节点后,
panel回到row、selectedId清空;切换方案/常态/展开内容时全部选中态清空(现有resetSelection,IslandEditorView.vue:119-125)。
- 错误态:
load()失败(非桌面客户端打开、getConfig抛错)→ 页面显示el-alert type="error"+ 错误文本 +重试;编辑功能全部不可用。保存失败只显示错误条与状态文本,不弹全局遮罩,不丢失编辑内存。 - 主题与关怀模式:设置页与编辑器必须只使用
--ch-*令牌与var(--ch-font-scale)缩放文字(styles/index.css:21-131、:103),不得写死浅色/深色;深色模式由html.dark变量覆盖,且smoke-layout.ts:149-157会断言深色下.island-editor-page的--ch-text存在。例外:编辑器的岛预览区(.island-preview)固定使用岛自身底色#202024与#ffffffeb文字,以真实预览岛外观,不跟随设置页主题;该区域的组件颜色仍遵循appearance覆盖。关怀模式放大字号时,组件库名称/描述、按钮文字跟随--ch-font-scale,但抽屉宽度、行高、组件外边距等几何令牌不变(AGENTS.md§5「关怀模式放大字号/控件,不改变几何令牌」)。 - 响应式:编辑器按现有断点:≤1150px 组件库 3 列、≤800px 2 列且方案工具区换行、≤520px 1 列并收窄页面左右 padding(
island-editor.css:371-417);设置页在 ≤680px 时布局选择卡改单列(IslandSettingsView.vue:497-505),抽屉宽度min(360px, 94vw)自适应。 - 性能:拖动
pointermove只在dragging存在时处理,且必须passive:false才可preventDefault(现有{ passive: false },IslandEditorView.vue:315);保存用串行队列 + revision 去陈旧结果,禁止每次输入都直接await getConfig();节点上限 200/行 12/方案 20/深度 6 必须在写入前检查而不是写入后裁剪;编辑页不得创建额外 BrowserWindow、不得轮询getConfig(只focus时刷新一次设置页)。 - 无障碍:所有自绘可点元素使用原生
button或role="button"+tabindex="0";保留现有aria-label(新建/重置/管理方案、添加组件、编辑组件、编辑行);拖拽不是唯一操作路径,必须同时提供按钮/下拉的等价操作(添加组件、上移/下移、包裹、复制、删除),以满足键盘与触屏用户。
9.20 既有实现与测试契约(重构不得破坏)
以下行为/文案/DOM 已被现有冒烟与代码依赖,重构时必须保留或由 parts-r/11 同步更新测试:
- 路由:
/settings/island、/settings/island/editor(router/index.ts:63-74);侧栏「灵动岛」入口(ClientLayout.vue:48-55);岛内「编辑」通过onIslandEditorOpen事件把主窗口切到编辑器(ClientLayout.vue:23-26;主进程main/index.ts:229-235)。 - 编辑器:
.component-library首组 11 个.library-item;按钮文本「新增行」「行设置」「进入编辑模式/退出编辑模式」「应用并返回」「返回设置」;data-row/data-node/data-library-kind/data-action=wrap;.component-settings .el-tabs__item页签;.preview-canvas/.preview-toolbar/.island-preview(main/smoke-layout.ts:40-73、:111-148、:186-226)。 - 方案:
aria-label为「新建方案」「重置当前方案」「管理方案」;下拉项文案「方案设置/复制方案/导入方案/导出方案/删除方案」;导入 format 字符串classhelper-island-layout;导入 ≤1 MB;新建/复制重生成 id;删除保持至少 1 个方案(:264-360)。 - 设置页:
.layout-choice两张卡、编辑岛内组件按钮、指针避让开关、保存/预览/恢复默认按钮与实时预览(IslandSettingsView.vue:170-397)。 - 保存队列行为与状态文本(9.18-2);失败重试按钮;
应用并返回成功后才离开。 - 这些断言若因重构改变(例如组件设置页签从 3 变 4),必须在同一 PR 更新 parts-r/11 的冒烟脚本,禁止只改实现不改测试。
9.21 验收标准
实现完成后逐项验证并记录结果(全部通过才算完成;验证必须无头/离屏,禁止启动可见岛做验收,除非显式 --visual):
- family 与默认:新配置(清空 config)打开设置页,
family默认ClassIsland 风格且appearance.fontSize=16、标签「正文字号(ClassIsland 基准)」;老配置(人为删除island.family)重载后自动为经典 WinIsland(兼容)且外观与升级前一致(对比截图/配置 JSON),其原有fontSize(如 13)保持不变、标签「岛字号(经典)」。 - fixed 语义:
family=classisland、layout=fixed时,岛外观为信息行(行高 40、圆角 8、背景不透明度 0.5、底部 3px 进度条),默认显示日期 + 课程表;不出现胶囊/详情卡;来消息时在同一条内展开气泡;family=winisland时恢复胶囊 + 详情卡。layout=fixed时设置页不得出现任何行/组件编辑入口(fixed v2 为只读内置方案)。 - 外观参数:classisland 族 22 项、winisland 族 15 项逐一改动——拖动时岛即时变化(≤1 帧合并),点保存后
config.json.island落盘;重启客户端后保持;非法值(如缩放 9、圆角 -1、颜色red)被钳制/回落,不出现 NaN。字号语义:classisland 下改「正文字号(ClassIsland 基准)」时正文 = 该值、次级/强调/大号按 14/18/20 阶梯派生,默认 16;winisland 下标签为「岛字号(经典)」、默认 13;两种 family 间切换标签与默认值提示正确、不强制改写已有值。 - custom 与 family:
layout=custom时family控件禁用并显示「自定义方案始终使用 ClassIsland 规格;外观族只作用于经典固定形态。」;custom 的渲染始终为 ClassIsland 规格(不因family=winisland回退旧观感);设置页「圆角/不透明度/行间距」改动同时写入schemes[activeSchemeId]并落盘;「界面缩放」在 custom 下隐藏并有说明;schemes/rows/nodes编辑器只服务 custom,fixed 无编辑入口。 - 课程提醒:5 个开关 + 2 个阈值改动即时保存到
config.json.islandLayout.lessonReminders(唯一数据源);阈值越界被钳制到 5–1800 / 5–600;/settings/reminder只显示只读摘要 + 跳转链接,页面内无可编辑的上课提醒控件。 - 编辑器布局:编辑器页
padding 12/24/16;行最小高 46(标签态)/40(预览态);行距等于scheme.spacing;组件外层左右各 6px;选中框 1.5px 虚线1,1.5、opacity .75;行把手 20px;「新增行」在工具条与行列表末尾均可用。 - 四页签:组件设置抽屉依次为「内容/外观/尺寸/隐藏条件」;11 类组件的控件、默认值、范围与 9.15 表一致;切换组件时回到「内容」页。
- 拖拽:组件库→行、组件库→容器(20%–80% 区域)、行内排序、跨行移动、行排序均可用(鼠标与触摸各测一次);拖动阈值 6px;拖动父组件到自身后代被拒绝且数据不变;嵌套超过 6 层、组件超过 200、行超过 12、方案超过 20 均给出提示且不写入。
- 方案:新建(提示输入,1–64 字,最多 20 个)、复制(名称加「副本」、id 全量重生)、重置(保留 id/name)、删除(确认,至少保留 1 个)、导入(≤1 MB,format 校验,id 重生)、导出(文件名非法字符替换、可再导入)全部通过;
activeSchemeId始终有效。 - 保存与失败:连续快速拖动/输入时状态文本按「正在保存…」→「已自动保存」变化;人为让
saveConfigreject(mock IPC)时显示「保存失败」+ 错误条「重试」,且未点应用不允许离开;重试成功后错误清除。 - 编辑模式:进入后页面内渲染真实组件(无额外窗口/iframe),缩放 0.3–1.5 生效且「重置缩放」回 100%;预览不遮挡工具条、无横向滚动;退出后回到标签态;真实岛在保存后热更新(
island:layout生效)。 - 键盘/触摸:
Esc逐层退出(抽屉→编辑模式→返回);Delete删除选中组件(有子组件先确认);Ctrl+D复制;所有自绘按钮有:focus-visible焦点环;触摸拖动不触发页面滚动。 - 无回归:9.20 列出的 DOM/文案/路由/导入格式全部保持;现有冒烟脚本(若未按 9.20-6 更新)中的编辑器断言通过;未新增桌面窗口、未改
main/island.ts的透明/命中/几何逻辑;pnpm typecheck与pnpm lint通过(不执行构建/运行验证);9.19 的状态/空态/主题/响应式行为逐一验证。