- 重构版主提示词(14 parts,面向 ClassHelper 灵动岛重构) - ClassIsland 全量规格提示词(20 parts,含分卷与逐章版本) - 投喂指南、分卷、逐节文件 - 调研记录、一致性基线、生成与质检脚本
112 KiB
6. 设计 tokens 与视觉基线(双风格:ClassIsland 默认 / winisland 兼容)
本节是整轮重构的「视觉地基」。实现者必须先把 token 落到 packages/desktop-client/src/island/ 下的独立样式入口,再改任何两个岛的渲染文件。所有像素、颜色、字号、圆角、阴影、透明度必须从这里取值,禁止在 IslandApp.vue、CustomIslandRuntime.vue、ComponentNode.vue、ClassicNotifications.vue、LessonReminderContent.vue、custom-runtime.css 里写裸值。
6.0 双风格模型(必须先建,再谈 token)
外观契约新增/改为(packages/shared/src/types.ts 的 IslandAppearance 扩展,config.ts 的 normalizeIslandAppearance 同步):
| 字段 | 类型 | 默认 | 含义 | 证据 |
|---|---|---|---|---|
family |
'classisland' | 'winisland' |
新装 classisland;老用户升级为 winisland(见 §6.12 迁移规则) |
视觉家族 | Lead 裁决第 1 条(parts-r/01-objective.md:60) |
theme |
'fluent' | 'classic' |
fluent |
仅 family='classisland' 有效;对应 ClassIsland 两套 XamlTheme |
repos/ClassIsland/ClassIsland/App.Services.xaml.cs:297-315 |
colorScheme |
'light' | 'dark' | 'system' |
system |
仅 family='classisland' 有效;跟随应用深浅色 |
repos/ClassIsland/ClassIsland/Models/Settings.cs:979-989、Services/ThemeService.cs:53-63 |
style |
'black' | 'glass' | 'tinted' |
black |
仅 family='winisland' 有效(现有语义不变) |
packages/shared/src/constants.ts:265-290,324-330 |
animations |
boolean |
true |
岛动画总开关 | constants.ts:273;IslandApp.vue:249 |
speed |
number |
1(0.5–2) |
动画速度倍率(按比例缩放所有时长) | constants.ts:274;IslandApp.vue:291 |
fontSize |
number |
13(11–20) |
base 字号,四档字号由它派生 | constants.ts:272;IslandApp.vue:527 |
height / width |
number | 44 / 268 | 仅 winisland 的胶囊几何;classisland 下用于「信息行最小宽/高」约束(见 §6.5) | constants.ts:266-267;IslandApp.vue:188-204 |
radius |
number | 20(8–32) | winisland 胶囊基准半径;classisland 下取 8(范围 0–20),见 §6.4 |
constants.ts:268;IslandApp.vue:209-211 |
实现要求:
- 岛根元素必须同时挂三个数据属性,CSS 用它们切 token 与几何:
data-family="classisland|winisland"、data-theme="fluent|classic"、data-color-scheme="light|dark"(winisland 恒为dark)。禁止用 JS 拼内联样式切风格。 family='classisland'时不使用spring.ts/squircle.ts:几何用标准border-radius+ CSS transition;family='winisland'时继续用弹簧形变与超椭圆路径(Lead 裁决第 4 条,parts-r/01-objective.md:52)。- 两套 family 的 token 不得混用:classisland 的静态值定义在
--ci-*;winisland 的调色板保持--wn-*;运行时外观注入继续用--island-*(两套共用这几个动态变量:--island-font/--island-radius/--island-accent/--island-dur/--island-anim)。 - Token 文件必须是单一来源:新建
packages/desktop-client/src/island/tokens.css,在island.html的样式链最前面 import(src/island/main.ts已 import@classhelper/ui/tokens.css,见main.ts:1),并由IslandApp.vue与CustomIslandRuntime.vue共同继承。禁止在.vue的<style>里重新定义全局 token。 - 主题切换动画只允许改颜色类 token,绝不覆盖圆角/间距/字号/动画 token(与主窗口纪律一致:
docs/dev/motion.md:125-126)。
6.1 变量分层与命名规范
| 层 | 前缀 | 归属 | 是否可被运行时覆盖 | 本重构动作 |
|---|---|---|---|---|
| 1 应用外壳 | --ch-ui-*、--ch-*、--el-* |
packages/ui/src/tokens.css、theme.css、renderer/styles/index.css |
是(主窗口主题) | KEEP,不改值;岛只读引用(如编辑器 UI) |
| 2 ClassIsland 岛静态 token | --ci-* |
新增 src/island/tokens.css |
否(改值只能改源码) | NEW |
| 3 岛运行时外观 | --island-* |
IslandApp.vue:522-528、CustomIslandRuntime.vue:102-123 注入 |
是(设置页实时) | KEEP 并扩展(补 --island-row-*) |
| 4 winisland 调色板 | --wn-* |
classic-island.css:60-82(.island-card 内) |
是(data-style 切换) |
KEEP(仅 winisland 生效) |
| 5 组件方案 | --custom-*、--resident-*、--row-gap |
custom-island.css、custom-runtime.css 由 scheme 注入 |
是 | KEEP,值按 §6.5 改默认 |
命名冲突处理(必读):现有 LessonReminderContent.vue:133-137 已在组件作用域注入 --ci-speed、--ci-duration、--ci-title-duration、--ci-scale-duration、--ci-delay,ComponentNode.vue/custom-runtime.css 还使用 --ci-body-font。这些是组件级变量,与本节新增的静态 token 同前缀,容易误伤。处理办法:
- 新增静态 token 中禁止使用这 6 个名字;
- M5 阶段把
LessonReminderContent.vue的 5 个变量重命名为--ci-lesson-speed/-duration/-title-duration/-scale-duration/-delay,保留旧名作为 alias(var(--ci-lesson-duration, var(--ci-duration, 200ms)))一版后删除; --ci-body-font保留(custom 行基准字号),在 tokens.css 中定义为var(--ci-fs-body)。
6.1.1 --ci-* 变量速查表(必须在 tokens.css 中一次性定义齐)
实现者按此表逐行写入,缺一个都会在第 7/10 部分的样式里解析为空、导致整条声明失效。
| 变量 | 默认(classisland 深色) | 用途 |
|---|---|---|
--ci-bg |
#202020 |
岛/行背景色(配合 --ci-opacity-bg) |
--ci-bg-message |
#202020 |
阅读消息/详情实底 |
--ci-fill-1 |
rgba(255,255,255,.06) |
行内浅底(列表行、chip) |
--ci-fill-2 |
rgba(255,255,255,.10) |
悬停底、按钮底 |
--ci-fill-3 |
rgba(255,255,255,.14) |
徽标底、强强调底 |
--ci-border |
rgba(255,255,255,.15) |
岛/行 1px 描边 |
--ci-border-strong |
rgba(255,255,255,.24) |
选中/拖拽目标描边 |
--ci-border-accent |
var(--ci-accent) |
强调边(遮罩边缘) |
--ci-divider |
rgba(255,255,255,.09) |
行内分隔线 |
--ci-focus-ring |
var(--ci-accent) |
焦点环颜色 |
--ci-text-1 |
#FFFFFF |
主文字 |
--ci-text-2 |
rgba(255,255,255,.855) |
次要文字 |
--ci-text-3 |
rgba(255,255,255,.773) |
辅助文字 |
--ci-text-4 |
rgba(255,255,255,.529) |
装饰文字 |
--ci-text-5 |
rgba(255,255,255,.365) |
禁用文字 |
--ci-text-on-accent |
#FFFFFF |
accent 底上文字 |
--ci-accent |
#00BFFF(运行时被 --island-accent 覆盖) |
强调色 |
--ci-accent-soft |
accent @10% | 强调淡底 |
--ci-accent-soft-strong |
accent @14% | 强调淡底加强 |
--ci-state-urgent |
#FF453A |
紧急 |
--ci-state-call |
#FF9F0A |
叫人 |
--ci-state-homework |
var(--ci-accent) |
作业 |
--ci-state-ok |
#228B22 |
正常/优 |
--ci-warning |
#FFA500 |
警告 |
--ci-danger |
#CD5C5C |
危险/错误 |
--ci-tint-urgent |
径向 rgba(255,69,58,.22) |
紧急内层染色 |
--ci-tint-call |
径向 rgba(255,159,10,.20) |
叫人内层染色 |
--ci-progress-h |
3px |
行内进度条高度 |
--ci-progress-track |
rgba(255,255,255,.15) |
进度条轨道 |
--ci-progress-fill |
var(--ci-accent) |
进度条填充 |
--ci-progress-radius |
0 |
进度条圆角 |
--ci-mask-fill |
var(--ci-accent) |
斜切遮罩填充 |
--ci-mask-text |
#FFFFFF |
遮罩文字 |
--ci-radius-island |
8px |
岛圆角(0–20) |
--ci-radius-row |
8px |
单行圆角(分体) |
--ci-radius-card |
8px |
详情卡/气泡圆角 |
--ci-radius-control |
4px |
控件/列表行圆角 |
--ci-radius-pill |
999px |
胶囊/徽标圆角 |
--ci-row-h |
40px |
行高 |
--ci-row-pad-x |
12px |
行内左右内边距 |
--ci-row-min-w |
20px |
行内容最小宽 |
--ci-row-gap |
5px |
行间距(可调 0–150) |
--ci-comp-gap |
12px |
相邻组件间距(单侧 6) |
--ci-shadow-pad |
10px |
窗口/阴影留白 |
--ci-shadow-island |
0 4px 8px 2px rgba(0,0,0,.28) |
岛投影 |
--ci-shadow-mask |
0 8px 32px rgba(0,0,0,.4) |
提醒时投影 |
--ci-shadow-popup |
0 8px 16px rgba(0,0,0,.45) |
岛内弹层投影 |
--ci-opacity-bg |
0.5 |
背景不透明度 |
--ci-opacity-faded |
0.05 |
指针避让淡化 |
--ci-fs-secondary |
14px(派生) |
次要字号 |
--ci-fs-body |
16px(派生) |
正文字号 |
--ci-fs-emphasized |
18px(派生) |
强调字号 |
--ci-fs-large |
20px(派生) |
大号字号 |
--ci-fw-regular |
400 |
常规字重 |
--ci-fw-medium |
500 |
中等字重 |
--ci-fw-bold |
700 |
粗字重 |
--ci-font |
HarmonyOS 栈 | 正文字体族 |
--ci-z-mask |
2 |
遮罩层 |
--ci-z-mask-text |
3 |
遮罩文字层 |
--ci-z-popup |
4 |
岛内弹层 |
--ci-z-drag-ghost |
10000 |
编辑器拖拽幽灵 |
--ci-dur-instant |
90ms |
高频反馈 |
--ci-dur-fast |
100ms |
淡出/短过渡 |
--ci-dur-base |
200ms |
默认过渡 |
--ci-dur-slow |
300ms |
行宽(Fluent) |
--ci-dur-overshoot |
550ms |
行宽(Classic) |
--ci-dur-long |
750ms |
遮罩缩放 |
--ci-ease-out |
cubic-bezier(.16,1,.3,1) |
ClassHelper 主缓动 |
--ci-ease-fl-1 |
cubic-bezier(.65,0,.35,1) |
Fluent 行宽 |
--ci-ease-fl-2 |
cubic-bezier(.25,1,.5,1) |
遮罩文字/缩放 |
--ci-ease-fl-3 |
cubic-bezier(.5,0,.75,0) |
遮罩退出 |
--ci-ease-overshoot |
cubic-bezier(0,1,.5,1.1) |
Classic 行宽过冲 |
--ci-ease-quartic-out |
cubic-bezier(.16,1,.3,1)【推断】 |
区段扫入 |
--ci-ease-quadratic-in |
cubic-bezier(.11,0,.5,0)【推断】 |
区段收拢 |
--ci-ease-sine-in |
cubic-bezier(.12,0,.39,0)【推断】 |
覆盖层淡入 |
--ci-dur-mask-region |
360ms |
单个斜切区段 |
--ci-dur-mask-stagger |
120ms |
区段错峰 |
--ci-delay-mask-text |
260ms |
遮罩文字延迟 |
--ci-dur-mask-text |
250ms |
遮罩文字淡入 |
--ci-dur-mask-scale |
750ms |
遮罩文字缩放 |
--ci-delay-mask |
600ms |
遮罩投影延迟 |
--ci-dur-mask-shadow |
100ms |
遮罩投影/背景淡入 |
--ci-dur-mask-exit |
200ms |
遮罩退出 |
6.2 颜色 token 表
6.2.1 背景与表面
| Token | ClassIsland 目标值(family=classisland) | ClassHelper 现状(变量/值) | 迁移策略 |
|---|---|---|---|
--ci-bg |
深色:#202020 @ opacity 0.5(Fluent 用 SolidBackgroundFillColorSecondaryBrush,Classic 用 SolidBackgroundFillColorBaseBrush);浅色:#F3F3F3 @ 0.5 |
--wn-bg:#000000(black)/rgba(32,32,36,0.804)(glass)/#0b0b0f(tinted),见 classic-island.css:60-82;custom:--wn-bg: #20202080(custom-runtime.css:241-245) |
NEW --ci-bg;classisland 下替换 .island-card 背景;winisland 保留 --wn-bg;custom 在 classisland family 下把方案默认色从 #000000 改为 #202020(island-layout.ts:246),并叠加 0.5 不透明度 |
--ci-bg-message |
#202020(不透明,阅读消息/详情时用实底) |
.message-surface { --wn-bg:#202020 }(classic-island.css:84-86、custom-runtime.css:253-255) |
KEEP 值,改名 --ci-bg-message 作为语义 token;winisland 继续用 .message-surface 覆盖 --wn-bg |
--ci-bg-elevated |
rgba(255,255,255,0.06)(行内 chip/进度底/列表行底) |
rgba(255,255,255,0.045)(.list-row,classic-island.css:389)、rgba(255,255,255,0.12)(.row-badge、.pill-type)、#ffffff0d/#ffffff1a(custom) |
CHANGE:classisland 统一到 6%/10%/14% 三档:--ci-fill-1/2/3 |
--ci-fill-1/2/3 |
6% / 10% / 14% 白(深色);4% / 8% / 12% 黑(浅色) | 同上散落 | NEW |
--ci-mask-fill |
强调色(AccentFillColorDefaultBrush,ClassIsland 提醒遮罩填充) |
当前无(ClassHelper 消息用胶囊/详情卡,无遮罩) | NEW,classisland fixed 消息气泡与 lesson 提醒共用 |
--ci-tint-urgent |
rgba(255,69,58,0.22) 径向 |
.urgent .tint 同值(classic-island.css:168-171) |
KEEP 值,归入 classisland 状态色 |
--ci-tint-call |
rgba(255,159,10,0.20) 径向 |
.call .tint 同值(:173-176) |
KEEP 值 |
--ci-tint-tinted |
accent 30% → transparent 135° 渐变 | [data-style='tinted'] .tint(:178-185) |
KEEP(仅 winisland tinted) |
--ci-bg-hover |
rgba(255,255,255,0.10)(classisland 行/按钮 hover) |
--wn-surface-hover: rgba(255,255,255,0.18) / #ffffff1a |
CHANGE:classisland 用 10%,winisland 保留 18% |
6.2.2 描边
| Token | ClassIsland 目标值 | ClassHelper 现状 | 迁移策略 |
|---|---|---|---|
--ci-border |
1px rgba(255,255,255,0.15)(深色)/ rgba(0,0,0,0.08)(浅色) |
--wn-border: rgba(255,255,255,0.118)(classic-island.css:62);glass 0.157(:74);custom inset 0 0 0 1px #ffffff1e(custom-island.css:13) |
NEW;classisland 行容器 border:1px solid var(--ci-border);winisland 保留描边画在 SVG path 上(stroke-width:1,:110-113) |
--ci-border-strong |
rgba(255,255,255,0.24)(选中/拖拽目标) |
#ffffff2b(custom-island.css:171)、.node-editing:hover #ffffff28(ComponentNode.vue:432) |
NEW,统一 24% |
--ci-border-accent |
accent 100%(遮罩/强提醒强调边) | --state-color 混合描边(classic-island.css:156-159,62% 状态色) |
CHANGE:classisland 用 accent 纯色;winisland 保留混合规则 |
--ci-divider |
rgba(255,255,255,0.09)(行内分隔) |
--wn-divider: rgba(255,255,255,0.09)(:68) |
KEEP 值,语义化改名 |
--ci-focus-ring |
2px accent,outline-offset:2px(设置/编辑器)+ -3px(岛内按钮) |
#8dafff/#adc4ff/#85aaff 三处不同(custom-island.css:65-68、custom-runtime.css:190-193、ComponentNode.vue:394) |
CHANGE:classisland 统一 2px solid var(--ci-accent);winisland 保留原色 |
6.2.3 文字阶梯
| Token | ClassIsland 目标值(深色) | ClassHelper 现状 | 迁移策略 |
|---|---|---|---|
--ci-text-1 |
#FFFFFF(100%) |
--wn-text-1: rgba(255,255,255,0.92)(classic-island.css:63) |
CHANGE:classisland 用 100%(ClassIsland 深层级 255 alpha,见 repos/ClassIsland/ClassIsland.Core/Helpers/UI/ControlColorHelper.cs:24-33);winisland 保留 0.92 |
--ci-text-2 |
rgba(255,255,255,0.855) |
--wn-text-2: 0.72(:64) |
CHANGE:classisland 85.5%;winisland 72% |
--ci-text-3 |
rgba(255,255,255,0.773) |
--wn-text-3: 0.58(:65) |
CHANGE:classisland 77.3%;winisland 58% |
--ci-text-4 |
rgba(255,255,255,0.529) |
无(classic 用 0.34 的 chevron,:317) |
NEW |
--ci-text-5 |
rgba(255,255,255,0.365) |
#ffffff60(custom-island.css:88)、#ffffff80(custom-runtime.css:145) |
NEW |
--ci-text-on-accent |
#FFFFFF(深色遮罩上) |
#fff(.title,classic-island.css:628)、#0b0b0f(.solid-btn,:711) |
NEW,遮罩/强提醒文字统一白;winisland solid-btn 保留深色字 |
| 浅色方案 | #1B1B1B / rgba(0,0,0,0.855) / 0.773 / 0.529 / 0.365 |
岛当前恒为深色 | NEW:classisland family 支持浅色(§6.10) |
字号必须同时遵守 ISLAND_TYPE_SCALE(winisland)与四档阶梯(classisland),见 §6.3。
6.2.4 强调色与状态色
| Token | ClassIsland 目标值 | ClassHelper 现状 | 迁移策略 |
|---|---|---|---|
--ci-accent |
ClassIsland 默认 #00BFFF(DeepSkyBlue,repos/ClassIsland/ClassIsland/Models/Settings.cs:35);运行时由 appearance.accent 覆盖 |
DEFAULT_ISLAND_APPEARANCE.accent = '#0a84ff'(constants.ts:271);IslandApp.vue:107 注入 |
KEEP 用户值;family='classisland' 且用户从未改过 accent(仍等于旧默认 #0a84ff)时迁移为 #00BFFF;否则保留。--island-accent 继续作为运行时变量 |
--ci-accent-soft |
accent @ 10%(ClassIsland CustomizedAccentBarBackground1Brush 运行时 = accent@10%,repos/ClassIsland/ClassIsland/Services/ThemeService.cs:66-74) |
color-mix(in srgb, var(--island-accent) 20%/22%/26%, transparent)(classic-island.css:375,524,545) |
CHANGE:classisland 统一 10%/14% 两档 --ci-accent-soft、--ci-accent-soft-strong |
--ci-state-urgent |
#FF453A |
#ff453a(classic-island.css:170,260,514,551) |
KEEP 值,改名 token |
--ci-state-call |
#FF9F0A |
#ff9f0a(:175,256,519,567) |
KEEP 值 |
--ci-state-homework |
accent | accent(classic-island.css:263-265,417-419,523-526) |
KEEP |
--ci-state-ok |
#228B22(可选用天气/正常态) |
无 | NEW(与 ClassIsland AQI 1 同值,见 Themes/LightTheme.axaml:10) |
--ci-danger |
#CD5C5C(IndianRed,ClassIsland 危险文字) |
#ffb4ab(课件错误,CoursewareTransfer.vue:127) |
KEEP 现状(课件)+新增 classisland --ci-danger |
--ci-warning |
#FFA500 |
无 | NEW |
--ci-on-state |
#FFFFFF |
.badge-urgent #ff6961 等 |
KEEP winisland |
6.2.5 进度条与遮罩
| Token | ClassIsland 目标值 | ClassHelper 现状 | 迁移策略 |
|---|---|---|---|
--ci-progress-h |
3px(ClassIsland 行内进度条) |
2px(.lesson-progress,ComponentNode.vue:351)、2px(.ci-lesson-progress,LessonReminderContent.vue:312)、5px(课件,CoursewareTransfer.vue:111) |
CHANGE:classisland family 行内进度条 3px;课件保持 5px(不同组件,不受此 token 管) |
--ci-progress-track |
rgba(255,255,255,0.15) |
#ffffff24(ComponentNode.vue:352) |
CHANGE |
--ci-progress-fill |
var(--island-accent) |
accent(ComponentNode.vue:359、LessonReminderContent.vue:313) |
KEEP |
--ci-progress-radius |
0(ClassIsland 进度条直角) |
1px(ComponentNode.vue:354) |
CHANGE:classisland 0;winisland 保留 1px |
--ci-mask-fill |
accent(斜切遮罩填充,ClassIsland AccentFillColorDefaultBrush,repos/ClassIsland/ClassIsland/XamlThemes/FluentTheme/Styles.axaml:180) |
var(--island-accent, #60a5fa)(LessonReminderContent.vue:251) |
KEEP 语义,改用 --ci-mask-fill |
--ci-mask-text |
#FFFFFF(ClassIsland TextOnAccentFillColorPrimaryBrush) |
#fff |
KEEP |
6.2.6 Fluent / Classic 主题色差(classisland family 内部)
| Token | Fluent(默认) | Classic | 证据 |
|---|---|---|---|
| 行背景 | SolidBackgroundFillColorSecondaryBrush(暗 #1C1C1C【推断】) |
SolidBackgroundFillColorBaseBrush(暗 #202020【推断】) |
repos/ClassIsland/ClassIsland/XamlThemes/FluentTheme/Styles.axaml:45;ClassicTheme/Styles.axaml:214 |
| 描边 | 1px ControlElevationBorderBrush |
无显式描边 | FluentTheme:46-47 |
| 阴影 | 0 4 8 2 #48000000 |
常态无投影(提醒时 DropShadowEffect blur 8 opacity 1) |
FluentTheme:49;ClassicTheme:36-45,283-287 |
| 圆角 | 用 RadiusX(默认 8,0–20) |
直角(圆角 0) | FluentTheme:48;ClassicTheme:46-59 |
| 遮罩动画 | 斜切分块 + 文字缩放 | 整块下压/上滑 | 见第 10 部分 |
实现:data-theme="fluent|classic" 只切换上表 5 项与第 10 部分的动画参数,不得切换字号/间距。
6.3 字体 token
| Token | ClassIsland 目标值(classisland family) | ClassHelper 现状 | 迁移策略 |
|---|---|---|---|
--ci-font |
'HarmonyOS Sans SC','Microsoft YaHei UI','Noto Sans CJK SC','Noto Sans SC','PingFang SC','Source Han Sans SC',SimHei,sans-serif(ClassIsland 字体栈,repos/ClassIsland/ClassIsland/Assets/Fonts/Fonts.axaml:3) |
-apple-system,'Segoe UI Variable Display','Segoe UI','Microsoft YaHei','PingFang SC','Helvetica Neue',sans-serif(classic-island.css:22-25) |
CHANGE:classisland family 用新栈;winisland 保留旧栈。不内置 TTF(许可与体积原因,HarmonyOS 字体许可见其 LICENSE.txt);若后续要内置,只加 @font-face 不改栈顺序 |
--ci-font-mono |
数字等宽:font-variant-numeric: tabular-nums |
已用(lesson-ending.css:11、LessonReminderContent.vue:226) |
KEEP,抽为工具类 .ci-num |
--ci-fs-secondary |
14px |
由 --secondary: (fontSize/13)*14(CustomIslandRuntime.vue:119)、.pill-sub 0.74×、small 12px(ComponentNode.vue:436) |
CHANGE:classisland 固定 4 档由 fontSize 按 14/16/18/20 比例派生(见下) |
--ci-fs-body |
16px |
--island-font 基准 13(winisland) |
CHANGE |
--ci-fs-emphasized |
18px |
--emphasized 默认 20(ComponentNode.vue:441) |
CHANGE |
--ci-fs-large |
20px |
--large 默认 28(ComponentNode.vue:444) |
CHANGE |
--ci-fw-regular/medium/bold |
400 / 500 / 700 | 600(.pill-title、.row-title、.badge)、700(.title)、600(.solid-btn) |
CHANGE:classisland 行文字 500、标题 700;winisland 保留 600/700 |
| 行高 | body 1.25、说明 1.4(【推断】按 ClassIsland 40px 行高与 16px 字号比例) |
1.15(.pill-title/.pill-sub)、1.4(.content-text) |
CHANGE:classisland 1.25;winisland 保留 1.15 |
appearance.fontSize |
11–20,classisland 下表示 body 基准(默认 16);winisland 保持 13(基准字号) | 同一字段,默认 13 | CHANGE 语义:按 family 解释;迁移时 winisland 用户不变,classisland 新装默认 16 |
classisland 四档派生公式(必须逐字实现):
ratio = appearance.fontSize / 16 // 默认 16 → 1.0
--ci-fs-secondary = 14 * ratio
--ci-fs-body = 16 * ratio
--ci-fs-emphasized = 18 * ratio
--ci-fs-large = 20 * ratio
这与现有 custom 的派生方式同构(CustomIslandRuntime.vue:119-122 用 /13 基准),只是基准从 13 换成 16。winisland 继续使用 ISLAND_TYPE_SCALE(constants.ts:354-367):title 1.08 / body 0.95 / meta 0.78 / badge 0.76 / pillTitle 0.92 / pillSub 0.74。
6.4 圆角 token
| Token | classisland 目标 | winisland 现状 | custom 现状 | 迁移策略 |
|---|---|---|---|---|
--ci-radius-island |
8px(0–20 可调;appearance.radius 复用该范围) |
20px(8–32;squircle 基准半径) | scheme.radius 默认 8 |
NEW;classisland 用标准 border-radius,不生成 squircle path |
--ci-radius-row |
8px(分体行与整体行同值) |
— | — | NEW |
--ci-radius-card |
8px(详情卡/气泡) |
展开卡 = min(48×radius/20, w/2, h/2)(IslandApp.vue:209-211) |
min(appearance.radius, w/2, h/2)(CustomIslandRuntime.vue:129-133) |
NEW |
--ci-radius-control |
4px(按钮/列表行/徽标内控件) |
calc(font×0.5)(.list-row、.row-icon) |
6px/8px 混用(custom-island.css:79,126) |
CHANGE:classisland 4px;winisland 保留比例派生 |
--ci-radius-pill |
999px |
999px(.pill-type/.badge/.ghost-btn) |
7px 圆点 | KEEP |
--ci-radius-progress |
0 |
1px | 1px | CHANGE(见 §6.2.5) |
| squircle 参数 | 不使用 | 指数 4.2、每角 14 段(squircle.ts:21,24) |
同 winisland(共用 squirclePath) |
KEEP(仅 winisland/custom) |
| 节点自定义圆角 | 默认 8(ClassIsland RadiusX) |
— | islandNode().appearance.cornerRadius = 16(island-layout.ts:211) |
CHANGE:新默认 8;旧配置保留 16(无损迁移,不强制改) |
6.5 几何与间距 token
| Token | classisland 目标 | ClassHelper 现状/变量 | 迁移策略 |
|---|---|---|---|
行高 --ci-row-h |
40px(fixed 气泡同样恒 40,不随内容增高) |
--resident-height: 40、min-height: max(40px, font×2.5)(custom-runtime.css:77,110,113);winisland 胶囊高 appearance.height 默认 44 |
NEW;classisland 行高固定 40(不随字号/内容变;正文最多 2 行、超出省略,不得撑高行——见第 7 部分) |
行内边距 --ci-row-pad-x |
12px |
--resident-padding/padding 0 12px(custom-runtime.css:76);winisland padding: 0 calc(font×0.78) |
NEW;classisland 固定 12 |
行最小宽 --ci-row-min-w |
20px |
无 | NEW(对应 ClassIsland MinWidth=20,repos/ClassIsland/.../FluentTheme/Styles.axaml:109) |
行间距 --ci-row-gap |
5px(0–150 可调) |
scheme.spacing 默认 5(island-layout.ts:251)、.layout-rows gap: var(--custom-row-gap) |
KEEP 值,classisland 下用 --ci-row-gap 驱动 --custom-row-gap |
组件间距 --ci-comp-gap |
相邻 12px(单侧 6px) | --row-gap 默认 12(scheme/row gap,island-layout.ts:169,236;custom-runtime.css:117) |
KEEP;classisland 组件 padding 单侧 = calc(var(--row-gap)/2) 即 6px |
窗口/阴影留白 --ci-shadow-pad |
10px |
ISLAND_SHADOW_PAD = 10(constants.ts:351) |
KEEP |
自动布置容器 --ci-window-max |
960×640(custom 常驻画布) |
windowBox():min(960, area.width) × min(640, max(60, area.height-marginY))(main/island.ts:1521-1528) |
KEEP |
| 固定包围盒(fixed) | 宽 = min(工作区宽, 960)、高 = 40 + 2×pad(pad 默认 20,覆盖阴影);行内容在窗口内按停靠对齐并在超出时截断/省略;消息到达不改变窗口尺寸 |
BBOX_SIZE 取最大形态 + 20(island.ts:108-112,1543-1547) |
CHANGE:classisland 下包围盒 = 行宽 + 2×10;行宽变化时窗口在隐藏帧改尺寸(保留"不可见时改大小"策略);单条气泡恒 40px、不因内容扩窗,maxCardHeight 只用于 custom 多条列表与强提醒窗口 |
| 胶囊几何 | 不适用 | width 268 / height 44(constants.ts:266-267),ISLAND_SIZE_DELTA(:344-348)、ISLAND_SLIVER_WIDTH 6(:336)、sliver height max(12,min(28,h-22))(IslandApp.vue:196) |
KEEP(仅 winisland) |
| 课程提醒条 | 高 40、宽 max-content、最小 300、最大 800 |
同(LessonReminderContent.vue:196-208) |
KEEP |
| 课件卡 | 424×132 |
COURSEWARE_ISLAND_SIZE(shared/src/courseware.ts:4)、padding 17/24、radius 24(CoursewareTransfer.vue:71-72) |
KEEP(独立组件,不套 classisland 行) |
| 编辑器预览行 | 40px(与真岛一致) | .preview-row min-height:44px(island-editor.css:207)、padding 12、背景 #202024(:193-201) |
CHANGE:44→40,背景改 --ci-bg |
| 图标/徽标 | 下课前秒数徽标 30×30 圆形 | .lesson-ending-badge 30×30(lesson-ending.css:3-5) |
KEEP |
6.6 阴影 token
| Token | classisland 目标 | ClassHelper 现状 | 迁移策略 |
|---|---|---|---|
--ci-shadow-island |
0 4px 8px 2px rgba(0,0,0,0.28)(即 ClassIsland 0 4 8 2 #48000000) |
无(winisland 用 filter: drop-shadow(0 2px 3px rgba(0,0,0,.11)),classic-island.css:123,148-153) |
NEW:classisland 用 box-shadow 挂行容器(矩形 + 8px 圆角),禁止 filter: drop-shadow(会闪帧,classic-island.css:114-120) |
--ci-shadow-mask |
0 8px 32px rgba(0,0,0,0.4)(提醒时) |
无 | NEW |
--ci-shadow-popup |
0 8px 16px rgba(0,0,0,0.45)(深色) |
--ch-shadow-flyout(index.css:53) |
NEW(岛内弹层用) |
--ci-shadow-custom |
内描边 + 小投影 | inset 0 0 0 1px #ffffff1e, 0 2px 3px #0000001c(custom-island.css:12-14) |
KEEP(custom 行);classisland family 下改 inset 0 0 0 1px var(--ci-border), 0 4px 8px 2px rgba(0,0,0,.28) |
| 投影常驻原则 | 只改参数、不增删 filter/box-shadow 声明 |
已实现(classic-island.css:114-123) |
KEEP |
6.7 透明与不透明度 token
| Token | classisland 目标 | ClassHelper 现状/公式 | 迁移策略 |
|---|---|---|---|
--ci-opacity-bg |
0.5(行背景不透明度,与 ClassIsland 默认 Opacity=0.5 一致) |
scheme.opacity 默认 0.5(island-layout.ts:248);winisland appearance.opacity 默认 1 |
KEEP 语义分开:背景不透明度(0.5)≠ 窗口不透明度(1)。classisland 下 --ci-opacity-bg=scheme.opacity;winisland 无背景层不透明度概念 |
| 窗口不透明度 | 1;阅读消息时强制 1 |
displayOpacity:readingMessage ? 1 : appearance.opacity(main/island.ts:233-240) |
KEEP |
| 淡化比例 | 0.05 |
CLASSISLAND_ISLAND_METRICS.fadedOpacity = 0.05(shared/src/island-layout.ts:35),用于 main/island.ts:1441 |
KEEP |
| 玻璃/毛玻璃 | classisland 不使用 | --wn-bg: rgba(32,32,36,0.804)(classic-island.css:73);不采样桌面模糊(文档 winisland-design-tokens.md:23) |
KEEP(winisland only) |
| 消息实底 | 1.0 不透明 #202020 |
.message-surface(见 §6.2.1) |
KEEP |
| 遮罩文字层 | accent 上白字 100% | 同 | KEEP |
| 禁用动画 | 不涉及透明度 | `--island-anim: 0 | 1(IslandApp.vue:527);animations=false时所有transition/animation` 应为 0 或直接落位 |
6.8 层级(z-index)token
| 层 | classisland 目标 | ClassHelper 现状 | 迁移策略 |
|---|---|---|---|
| 岛根层 | 0 | .island-root/.resident-window |
KEEP |
| 形状/背景层 | 0 | .shape(classic-island.css:103-124)、svg.shape |
KEEP |
| 内容层 | 1 | .content/.resident-page |
KEEP |
| 遮罩层(消息/提醒) | 2 | .ci-mask(LessonReminderContent.vue:246-253)、.tint |
NEW 统一:--ci-z-mask:2 |
| 遮罩文字层 | 3 | .ci-mask-title(:254-268) |
NEW:--ci-z-mask-text:3 |
| 弹层/下拉(岛内) | 4 | 无 | NEW |
| 编辑器拖拽幽灵 | 10000 |
.drag-ghost z-index:10000(island-editor.css:331-334)、.ch-guide 10000 |
KEEP |
| 强提醒窗口 | 独立 BrowserWindow,alwaysOnTop + 'screen-saver' |
岛窗口同级别(island.ts:514) |
NEW:效果窗口层级与岛同级或更高,见第 8 部分 |
| 岛窗口层级 | screen-saver(置顶) |
setAlwaysOnTop(alwaysOnTop,'screen-saver')(island.ts:514) |
KEEP |
6.9 DPI 与缩放 token
| 规则 | 要求 | 证据 |
|---|---|---|
| 逻辑单位 | 岛内全部用 CSS px = Electron DIP;不手动乘 devicePixelRatio | winisland-design-tokens.md:53、01-objective.md:86 |
| 阴影留白 | 10 CSS px(窗口比岛大 20) | ISLAND_SHADOW_PAD = 10(constants.ts:351);island.ts:1621-1636 |
| 画布上限 | custom 常驻窗 ≤ 960×640(或工作区高 − marginY) | island.ts:1521-1528 |
| 尺寸最小钳制 | 自定义窗口 ≥ 40 宽/24 高;可用画布 ≥ 40 | island.ts:389-390、CustomIslandRuntime.vue:262-263 |
| 方案缩放 | scale 0.75–2.5(custom 常驻);实际 fit.scale = min(scheme.scale, availW/naturalW, availH/naturalH) |
island-layout.ts:371-375;CustomIslandRuntime.vue:72-80 |
| 字号范围 | 11–20(appearance.fontSize);classisland 派生 4 档后仍受「行高 40」约束,溢出用省略而非撑行 |
constants.ts:298;§7 组件规格 |
| 主窗口缩放 | 设置页/编辑器沿用 --ch-font-scale(1 / 关怀 1.15),不影响岛 |
renderer/styles/index.css:103,973-979 |
| DPI 断言 | 截图/像素验收按 devicePixelRatio 换算基准图;125%/150% 下 ±1px 容差 |
01-objective.md:70 |
| 屏幕定位 | 全部用工作区 workArea + 锚点 + marginX/marginY;左右边距只在左/右停靠生效,主停靠中线不受影响 |
winisland-design-tokens.md:26;island.ts:1615-1648 |
6.10 深浅色 token
family='classisland' 必须同时支持深/浅色;family='winisland' 恒为深色(不随应用主题)。
| Token | 深色(默认) | 浅色 |
|---|---|---|
--ci-bg |
#202020 @0.5 |
#F3F3F3 @0.5 |
--ci-border |
rgba(255,255,255,0.15) |
rgba(0,0,0,0.08) |
--ci-text-1..5 |
白 100/85.5/77.3/52.9/36.5% | 黑 100/85.5/77.3/52.9/36.5% |
--ci-fill-1/2/3 |
白 6/10/14% | 黑 4/8/12% |
--ci-shadow-island |
0 4px 8px 2px rgba(0,0,0,0.28) |
0 4px 8px 2px rgba(0,0,0,0.18) |
--ci-progress-track |
白 15% | 黑 12% |
| 遮罩/强提醒 | accent 底 + 白字 | 同(accent 底 + 白字) |
| 切换时机 | 主窗口 theme 变化 → 经 preload 下发 island:appearance,岛根 data-color-scheme 变;颜色过渡 200ms |
同 |
| 默认 | colorScheme='system'(跟随应用;应用默认 Theme=2 深色,repos/ClassIsland/.../Settings.cs:32) |
— |
实现纪律:html.dark 式全局覆盖不适用于岛(岛是独立渲染入口,island.html 不继承主窗口 html.dark)。必须显式传 colorScheme 并在岛根节点切 data-color-scheme。切换只改颜色,几何/动效 token 保持不变(docs/dev/motion.md:125-130)。
6.11 两套风格差异总表(默认选择点)
| 维度 | classisland(默认) | winisland(兼容) |
|---|---|---|
| fixed 形态 | 信息行(多行,行高恒 40,组件横排,底部 3px 进度条);单条气泡也在 40px 行内、正文最多 2 行省略 | 胶囊 268×44 + 展开详情卡(ISLAND_SIZE_DELTA 展开 156×186) |
| 形状 | 标准 border-radius: 8px |
squircle 连续圆角(指数 4.2 / 14 段),胶囊满圆 h/2,展开 min(48r/20,…) |
| 形变 | CSS transition(行宽/透明度;行高恒 40 不变) | SpringValue 帧积分(stiffness 0.10 / damping 0.68 / 换向 35% / 上限 20% / snap 0.05px) |
| 内容切换 | 行内容淡入淡出 + 气泡两阶段 | 胶囊层 1-1.5p 退净、展开层 p² 进入 |
| 字号 | 14/16/18/20 四档(由 fontSize 派生) | --island-font×排版系数(title 1.08/body .95/meta .78/…) |
| 字重 | 500 正文 / 700 标题 | 600 标题 / 700 大标题 |
| 颜色 | 深/浅色主题 + Fluent/Classic 两套;背景 0.5 透明 | 纯黑/毛玻璃/主题色渐变三套;恒深色 |
| 状态色 | accent 遮罩 + 状态描边 | 红/橙/accent 内层微染 + 状态描边 |
| 进度条 | 3px、直角 | 1–2px、1px 圆角 |
| 主题切换 | Fluent↔Classic / 深↔浅 各 200ms 颜色过渡 | 无主题概念,style 切换即时 |
| 设置项 | theme+colorScheme 新增;height/width/radius 退化为约束 |
height/width/radius/style/idleSliver 全部保留 |
| 默认动画 | 见第 10 部分 classisland 列 | 见第 10 部分 winisland 列 |
6.12 配置迁移与兼容策略(token 相关部分)
config.json.island增加family、theme、colorScheme、winislandStyle(可选,默认为旧style的值)。- 老用户升级:
family缺失 → 判为老用户 →family='winisland',winislandStyle = 旧 style,theme/colorScheme写默认值但忽略。新安装(无 config 或island缺失)→classisland。 - 旧
style='black'|'glass'|'tinted'保留字段(winisland 使用),同时写入winislandStyle;classisland family 下忽略style。 appearance.accent保持用户值;仅当值等于旧默认#0a84ff且迁移到 classisland 时改#00BFFF,并在迁移日志中记录。appearance.fontSize:winisland 保持 13;迁移到 classisland 时若仍为 13 → 改为 16(避免 4 档过小),并记录。scheme.radius/node.cornerRadius不做强制改写(无损);classisland 渲染时若radius超过 20 则钳制到 20(appearance.radius范围扩到 0–32 仅为兼容 winisland,classisland 侧钳制)。- 迁移必须幂等:
normalizeIslandAppearance()对已有family的配置不得再次改值。
6.13 文件落点与实施顺序
| 步骤 | 文件 | 动作 |
|---|---|---|
| 1 | packages/shared/src/types.ts |
扩展 IslandAppearance(family/theme/colorScheme/winislandStyle);导出 ISLAND_FAMILIES、ISLAND_THEMES、ISLAND_COLOR_SCHEMES |
| 2 | packages/shared/src/constants.ts |
新增 classisland 默认值常量(DEFAULT_CLASSISLAND_FONT=16、ISLAND_CLASSISLAND_RANGES);把 ISLAND_SIZE_DELTA 标注为 winisland-only |
| 3 | packages/shared/src/island-layout.ts |
CLASSISLAND_ISLAND_METRICS 增加 componentGap:12、progressHeight:3、rowMinWidth:20、shadowPad:10、字号四档保持现有值 |
| 4 | 新建 packages/desktop-client/src/island/tokens.css |
写 --ci-* 静态 token(含 [data-family]、[data-theme]、[data-color-scheme] 组合选择器) |
| 5 | src/island/main.ts |
在 applyMotionTokens() 前 import ./tokens.css |
| 6 | IslandApp.vue / CustomIslandRuntime.vue |
根元素挂 data-family/data-theme/data-color-scheme;style 注入补 --island-row-*/--ci-fs-* 派生值 |
| 7 | classic-island.css / custom-island.css / custom-runtime.css / lesson-ending.css |
把硬编码颜色/尺寸改为引用 --ci-*(winisland 分支保留 --wn-*/--custom-*);[data-family='classisland'] 下覆盖布局 |
| 8 | src/main/island.ts |
下发 family/theme/colorScheme;窗口包围盒按 classisland 行宽计算(保留固定包围盒与"不可见时改尺寸") |
| 9 | 设置页/编辑器 | IslandSettingsView.vue 增加风格/主题/深浅色;island-editor.css 预览行 44→40、背景 token 化 |
| 10 | docs/dev/winisland-design-tokens.md、docs/app/island.md |
标注双风格与默认值,新增 docs/dev/island-refactor.md 的 token 章 |
6.14 常见错误与自查清单
实现者必须主动规避以下 13 类错误(每一条都对应真实会出现的问题):
- 把
--ci-*写进主窗口的renderer/styles/index.css:岛是独立渲染入口,主窗口的:root不共享;写错位置会导致岛内变量解析为空、整条transition简写失效(docs/dev/motion.md:58-59已记录该坑)。 - 漏掉
island.html的样式引入顺序:tokens.css必须在classic-island.css/custom-*.css之前 import,否则后者的var()在解析时找不到值。 - 用
html.dark给岛做浅色:岛不加载主窗口的<html class="dark">,必须data-color-scheme。 - 在 classisland family 里继续用 squircle:
squirclePath()只在 winisland/custom 分支调用;classisland 用border-radius: var(--ci-radius-island)。 - 把
appearance.opacity同时当作背景不透明度:窗口不透明度(0.4–1,阅读消息强制 1)与背景不透明度(默认 0.5)是两个 token;混用会导致"消息看不清"或"背景太实"。 - 给行容器加
backdrop-filter:会模糊整块矩形并破坏逐像素透明;毛玻璃只在 winisland 的glass里以半透明色模拟(docs/dev/winisland-design-tokens.md:23-24)。 - 用
filter: drop-shadow做 classisland 行投影:会新建合成表面导致开合闪帧;classisland 用box-shadow(classic-island.css:114-120)。 - 在 classisland 分支继续用
--wn-text-*:winisland 的 0.92/0.72/0.58 与 classisland 的 100/85.5/77.3/52.9/36.5 不是一套;混用会让同一行文字出现两种灰。 - 字号派生写成固定四档:classisland 四档必须由
fontSize/16比例派生;写死 14/16/18/20 会让设置页字号滑杆失效。 - 把圆角钳制只写在 CSS:
aspect变化时appearance.radius可能大于行高一半,必须在 JS 里也钳制到min(radius, w/2, h/2)(现有IslandApp.vue:334-341已做,classisland 分支同样要做)。 - 迁移时无条件改用户设置:只有"仍等于旧默认值"的 accent/fontSize 才允许改;用户调过的值必须原样保留(§6.12)。
- 忘记给编辑器预览同步 token:
island-editor.css的预览行、背景、圆角必须与真岛一致,否则"预览一个样、上屏另一个样"。 - 把 ClassHelper 旧展开卡行为带进 classisland fixed:fixed 行高恒 40,气泡/遮罩/进度条都在行内完成,正文最多 2 行省略;不得写「行高随内容增高」「行高 200ms 过渡」「为气泡扩窗」,
maxCardHeight只服务 custom 多条列表与强提醒窗口。
自查命令(无头执行,不启动 Electron):
# 1) 颜色字面量只允许出现在 token/兼容定义处
grep -rn "#[0-9a-fA-F]\{6\}" packages/desktop-client/src/island
# 2) 检查 classisland 分支没有误用 winisland 变量
grep -rn "\-\-wn-" packages/desktop-client/src/island/classic-island.css | head -40
# 3) token 是否齐全(每个 --ci- 使用处都能在 tokens.css 找到定义)
grep -rho "\-\-ci-[a-z0-9-]*" packages/desktop-client/src/island | sort -u > /tmp/used.txt
grep -ho "\-\-ci-[a-z0-9-]*" packages/desktop-client/src/island/tokens.css | sort -u > /tmp/def.txt
comm -23 /tmp/used.txt /tmp/def.txt
# 4) 确认 package.json 无新增依赖
git diff packages/*/package.json package.json
6.15 验收标准
- 单一来源:
grep -rn "#[0-9a-fA-F]\{6\}" packages/desktop-client/src/island只允许命中tokens.css、classic-island.css的--wn-*定义块、custom-*.css的兼容块与 SVG 渐变;两个.vue文件内联样式里不得出现颜色字面量(--state-color计算值除外)。 - 双风格隔离:切到
winisland后,岛仍显示 268×44 胶囊 + squircle + 弹簧开合,像素 diff 与重构前一致(±1px);切到classisland后无 squircle path、无弹簧形变,行高 40/圆角 8/进度条 3px。 - 默认值:新安装
family='classisland'、theme='fluent'、colorScheme='system';旧 fixtures 升级后family='winisland'且winislandStyle等于旧style。 - 字号:classisland 下把
fontSize从 16 改到 20,四档变为 17.5/20/22.5/25,行高仍 40 且内容按第 7 部分规则省略;winisland 下改字号时全部文本按排版系数等比缩放。 - 颜色:深/浅色各截一张基准图,
--ci-text-1..5、背景 0.5、描边 1px、进度条轨道/填充与 §6.2/§6.10 表逐项一致(颜色 ±2/255)。 - Fluent/Classic:切换主题后,行背景色、描边有无、阴影、圆角(Fluent 8 / Classic 0)按 §6.2.6 变化,字号与间距不变。
- 不透明度语义分离:把行背景不透明度调到 0.2、窗口不透明度调到 0.8,两者独立生效;打开一条消息时窗口不透明度强制 1(
displayOpacity),关闭后回到 0.8。 - 层级:消息遮罩在内容之上、遮罩文字在遮罩之上;编辑器拖拽幽灵
z-index:10000高于一切;岛窗口始终screen-saver层级。 - DPI:100%/125%/150%/200% 四档下,行高 40、圆角 8、字号逻辑值不变,无 0.5px 模糊;截图基准按 devicePixelRatio 换算后 ±1px。
- 迁移幂等:对同一份配置连续运行
normalizeIslandAppearance()三次,输出字节级一致;旧字段residentEnabled、style仍在且语义不变。 - 兼容别名:
--ci-lesson-*重命名期间旧--ci-duration等变量仍生效(至少一个版本),组件视觉无变化。 - 无新依赖/无新框架:
package.json依赖零新增;token 只用 CSS 自定义属性,不引入 CSS-in-JS 或新 UI 库。 - fixed 气泡恒定 40px:构造超长正文(>500 字)的消息,行高仍为 40、正文最多 2 行且以省略号收尾,窗口
bounds不因该消息变化;maxCardHeight只在 custom 多条列表/强提醒窗口路径被读取。 - reduce-motion 口径:系统开启
prefers-reduced-motion时,岛动画仍由appearance.animations/speed决定(默认不覆盖);仅当 P2 选项respectReducedMotion=true时按animations=false落位。
7. 岛结构与组件规格(ClassHelper 落地版)
本部分是「重构 ClassHelper 灵动岛」的视觉与组件规范。它不是新建工程的说明,而是对现有 packages/desktop-client 的改造指令:所有改动都必须落到既有文件(src/island/*、src/main/island.ts、packages/shared/src/*、src/renderer/views/settings/*)上,技术栈、构建方式、IPC 白名单、单窗口模型一律不变。
7.0 改造口径与文件级改动清单
总口径(必须遵守):
- 目标视觉默认改为 ClassIsland 规格(行高 40、行内边距 12、行距 5、圆角 8、背景不透明度 0.5、字号 14/16/18/20、进度条 3px、描边 1px、阴影
0 4 8 2 #48000000);ClassHelper 的 WinIsland 经典外观只作为 fixed 的兼容外观族保留(islandAppearance.family === 'winisland')。新安装默认family='classisland';老用户升级保持winisland,避免观感突变。 family只影响fixed:classisland下 fixed = ClassIsland 风格信息行(1 行 = 日期 + 课程表,行高 40、圆角 8、背景 0.5、底部 3px 进度条;消息在固定 40px 行内展开 ClassIsland 气泡,保留 ClassHelper 既有暂存/紧急/叫人/已读/去重规则);winisland下 fixed = 旧"胶囊 + 详情卡"(零改动)。custom始终使用 ClassIsland 规格,不提供 winisland 回退(custom 现有 rows/nodes 结构本就接近 ClassIsland,只做细化与像素对齐,见 7.2、7.4)。- 不新增第二个窗口、不新增框架、不改服务端:
LayoutIslandApp.vue仍是唯一渲染入口,main/island.ts仍是唯一窗口持有者(packages/desktop-client/src/island/LayoutIslandApp.vue:1-76)。 - 消息/提醒/课程提醒的业务语义(优先级、去重、上课暂存、紧急展开、已读回执)归第 8 部分;本部分只规定它们在岛上的容器、尺寸、颜色、动画。
- 所有像素值以本文与第 6 部分 tokens 为准;实现时不得再保留"ClassIsland 用 border-radius、ClassHelper 用 squircle"这种分歧——
fixed由appearance.family决定走哪套,custom固定走 ClassIsland 套;代码里两套都在,但同一时刻只启用一套。
必须改动的文件(按改动类型):
| 文件 | 改动类型 | 内容 |
|---|---|---|
packages/shared/src/constants.ts |
扩展 | 新增 ISLAND_FAMILIES + ISLAND_FAMILY_LABELS;DEFAULT_ISLAND_APPEARANCE 增加 family: 'classisland';新增 ClassIsland 规格常量(行高/内边距/行距/圆角/进度条/描边/阴影/字号)供渲染与主进程共用 |
packages/shared/src/island-layout.ts |
扩展 | 组件 settings 增补 ClassIsland 语义字段(见 7.4);appearance 增补 customMargin(可选);normalizeIslandLayouts() 增加 family 迁移(老用户 → winisland)与规范化范围 |
packages/desktop-client/src/main/island.ts |
改造 | normalizeAppearance 接受 family;windowBox 的 fixed/custom 包围盒按 7.1.2 调整;updateResidentPointer 的淡出比例沿用 shared 常量;新增 island:report-layout-metrics 承载"内容最大高" |
packages/desktop-client/src/island/IslandApp.vue |
改造 | 增加 data-family="classisland" 分支:fixed 改为"信息行 + 行内气泡"(见 7.3.2);winisland 分支保持原样 |
packages/desktop-client/src/island/CustomIslandRuntime.vue |
改造 | custom 常驻卡改为 ClassIsland 行容器(背景/圆角/内边距/行距/进度条/淡出);classisland 分支的 border-radius 与行背景 |
packages/desktop-client/src/island/ComponentNode.vue |
改造 | 组件内边距 6 → 0(改由行 padding 承担);字号 token 改为 ClassIsland 的 14/16/18/20;新增 schedule 的 ClassIsland 折叠规则与进度条 3px;新增 countdown 的 ClassIsland 文案 |
packages/desktop-client/src/island/WeatherBrief.vue |
改造 | 按 ClassIsland 天气简报的图标/数值排版(见 7.4.3) |
packages/desktop-client/src/island/classic-island.css |
扩展 | 新增 [data-family='winisland'] 下的三套 data-style 变量组保持现值;classisland 的行/气泡样式写进 custom-runtime.css |
packages/desktop-client/src/island/custom-runtime.css |
改造 | 行/背景/进度条/淡出/编辑器选中框的 ClassIsland 规格 |
packages/desktop-client/src/island/custom-island.css |
改造 | .custom-card 的 ClassIsland 背景/圆角/行距变量 |
packages/desktop-client/src/island/LessonReminderContent.vue |
复用 | 其 5 段斜切遮罩算法必须提取为共享遮罩组件,供通知遮罩复用(见 7.2.6) |
packages/desktop-client/src/renderer/views/settings/IslandSettingsView.vue |
改造 | 新增"外观族"下拉(绑定 appearance.family,默认 classisland,仅 layout='fixed' 可见);原"视觉风格"下拉仅在 fixed+winisland 下可见;custom 下两项都隐藏;圆角/行距/行高滑块范围按 ClassIsland 对齐 |
packages/desktop-client/src/renderer/views/settings/IslandEditorView.vue + island-editor.css + IslandEditorNode.vue |
改造 | 选中框改 1.5px 虚线、行高与块级尺寸按 ClassIsland(见 7.5) |
packages/desktop-client/src/renderer/components/IslandComponentSettings.vue |
改造 | 组件设置项按 7.4 的字段表补充(隐藏规则、宽度、对齐、边距、字体覆盖) |
不得改动:main.ts 的零依赖策略(packages/desktop-client/src/island/main.ts:6-11)、preload/island.ts 的白名单结构(:14-62)、spring.ts 的弹簧参数(spring.ts:26-30)、squircle.ts 的超椭圆参数(squircle.ts:21-24)。
7.1 岛窗口与几何
7.1.1 窗口创建参数(保持不变,只能增加)
沿用现有 BrowserWindow 配置(packages/desktop-client/src/main/island.ts:480-512),并逐条核对:
frame: false、transparent: true、resizable: false、movable: false、minimizable/maximizable/fullscreenable: false(:484-490)。skipTaskbar: true、hasShadow: false(:491-492)——阴影由 CSS/SVG 画,不得用系统窗口阴影。focusable: true必须保留(:502):Windows 下关掉可激活性会丢mousedown,岛点不动(:493-501的实机结论)。"不抢焦点"靠showInactive()+ 收起时blur()实现(applyFocusable,:1663-1672)。alwaysOnTop: true+setAlwaysOnTop(appearance.alwaysOnTop, 'screen-saver')+setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true })(:503, 514-515)。type: 'toolbar'、setIgnoreMouseEvents(true, { forward: true })(:504, 527)。webPreferences:contextIsolation: true、sandbox: true、backgroundThrottling: false、preload 指向island.js(:505-511)。- 禁止:
win.setBackgroundMaterial()/vibrancy/backgroundMaterial(:523-524的历史结论;逐像素透明是硬约束)。禁止setContentProtection(岛不是隐私窗口)。 - 导航与新窗口一律拒绝(
:516-522)。
7.1.2 窗口几何:fixed(两种外观族)/ custom
fixed 的两种口径(由 appearance.family 决定,不是同一个形态)
fixed 只表示"岛按经典交互工作(收到消息才出现)",它的外观由 family 决定:
A. family='classisland'(新安装默认):ClassIsland 风格信息行
- 形态重定义:fixed 不再是小胶囊,而是一条 ClassIsland 风格的信息行——默认 1 行 = 日期 + 课程表,行高 40、圆角 8、背景不透明度 0.5、1px 描边、阴影
0 4 8 2 #48000000、底部 3px 进度条(课程进度 / 提醒倒计时)。 - 隐藏语义不变:无消息时仍按现有
state.mode='hidden'隐藏窗口(或按idleSliver留细缝,main/island.ts:1483-1496);消息到来才显示行。 - 消息到来:在同一行内、行高 40px 的固定容器里渲染 ClassIsland 风格气泡——气泡不是"把行撑高",而是整块 40px 行换上遮罩层 + 内容层 + 进度层(见 7.2.8)。行高恒为 40,禁止临时增高到 64 等(
--ci-island-height: 40px不得被气泡改写)。保留 ClassHelper 既有业务规则(普通上课暂存、紧急立即展开、普通叫人可点、已读回执、按 id 去重),业务规则归第 8 部分,视觉归 7.2.8。 - 窗口几何(固定包围盒,开合时不动):
- 宽 = 行内容自然宽 + 2 × 12(行内边距)+ 2 ×
ISLAND_SHADOW_PAD(=10,packages/shared/src/constants.ts:351); - 高恒为 40 + 2 ×
ISLAND_SHADOW_PAD(无消息 / 有消息都一样;气泡在行内,不改变窗口高度); - 只在"行宽跨档"、显示器/DPI 变化或"窗口不可见"时
setBounds(沿用applyWindowLayout的比较逻辑,main/island.ts:1598-1612)。
- 宽 = 行内容自然宽 + 2 × 12(行内边距)+ 2 ×
- 行宽/内容宽必须由渲染层测量后上报:
reportSize({ width, height })(CustomIslandRuntime.vue:255)+setHitRect(IslandApp.vue:421-454),主进程不得自行猜内容尺寸。 - 气泡宽度上限 720【推断:按 1080p 下 ClassIsland 岛常用宽度取整】;超宽时课程表按
settings.count截断(7.4.5),气泡正文最多 2 行(7.2.8)。 - 鼠标交互:行上可点(展开/收起气泡、按钮);点行外空白 → 收起气泡(不改变 hidden 语义)。
B. family='winisland'(老用户升级默认):胶囊 + 详情卡
- 保留现有全部几何:
windowBox()普通模式取max(BBOX_SIZE, 当前形态 + 2*pad)(main/island.ts:1543-1548);触摸模式取"当前形态 + 2*pad"并贴合岛体(:1549-1552)。 - 胶囊 =
appearance.width × height(默认 268×44,constants.ts:266-267),展开/紧急/叫人 = 胶囊 +ISLAND_SIZE_DELTA(:344-348);岛本体尺寸由渲染层弹簧逼近(IslandApp.vue:216-220, 238-264)。 - 圆角 =
winisland规格(胶囊h/2、展开卡min(48×radius/20, w/2, h/2)),背景 = 纯黑/毛玻璃/主题色渐变三选一。
custom(常驻):固定画布 + 内容自然测量(始终 ClassIsland 规格,与 family 无关)
- 外观不走
family:custom 无论family是classisland还是winisland,都按 7.2 的 ClassIsland 行/背景/进度条/遮罩规格渲染;family只决定 fixed 用哪种外观(7.3.1)。设置页在layout='custom'时不得展示/生效family与winisland色调选项。 - 现在的窗口画布已经是"固定画布":
windowBox()在residentEnabled时返回min(960, area.width) × min(640, max(60, area.height - marginY))(main/island.ts:1522-1529),这与 ClassIsland 的固定包围盒思路一致,保留。 - 目标态改动:画布高度上限改为"内容上限"而不是固定 640——渲染进程上报
reportLayoutMetrics({ contentMaxHeight })(新增 IPC),主进程取min(area.height - marginY, contentMaxHeight + 2*pad);默认contentMaxHeight = 常态行数 × (40 + 行距) + 24。理由:ClassIsland 的岛高度只由行决定(repos/ClassIsland/ClassIsland/MainWindow.axaml.cs:916),640 会让屏幕上方出现一块"看不见但可能吃掉点击"的余量(虽然命中已按 hitRect 控制,但窗口越大越依赖 hitRect 的准确性)。 available(渲染层可用尺寸)保持现值算法:innerWidth/innerHeight - 2*ISLAND_SHADOW_PAD - 4(CustomIslandRuntime.vue:260-266),最小值 40。- 内容缩放:
fitted()取min(scheme.scale, available.width/natural.width, available.height/natural.height)(:72-80),保留;scheme.scale范围 0.75–2.5(island-layout.ts:370-375)。 customSize初值 424×80(main/island.ts:261)保留,仅作为首帧占位;首次reportSize后覆盖。
7.1.3 锚点、停靠与多显示器
必须沿用现有算法(main/island.ts:1331-1351),逐条核对:
- 停靠位置 6 值:
top-center / top-left / top-right / bottom-center / bottom-left / bottom-right(constants.ts:305-312)。 - 垂直:
vMode = position.startsWith('bottom') ? 'bottom' : 'top';vValue = bottom ? area.y + area.height - marginY : area.y + marginY(:1341-1342)。 - 水平:
hMode = endsWith('left') ? 'left' : endsWith('right') ? 'right' : 'center';hValue = left ? area.x + marginX : right ? area.x + area.width - marginX : round(area.x + area.width/2)(:1343-1349)。 - 居中停靠不受
marginX影响(docs/dev/winisland-design-tokens.md:26),只有左右停靠用水平边距。 - 锚点全部取整(
Math.round),避免 0.5px 抖动(:1325-1330)。 - 窗口定位时窗口比岛大
ISLAND_SHADOW_PAD,因此要再偏移一个 pad,让岛的边/中心落在锚点上(windowBounds,:1615-1648):左停靠x = hValue - pad、右停靠x = hValue + pad - width、居中x = hValue - round(width/2);y = bottom ? vValue + pad - height : vValue - pad。 - 多显示器:
targetDisplay()=followCursorDisplay ? getDisplayNearestPoint(cursor) : getPrimaryDisplay()(:1354-1358);换屏检测挂在既有 60ms 光标轮询上(syncAnchorDisplay,:1370-1379),不得新增定时器。 - DPI:Electron 的
BrowserWindow.setBounds用 DIP,渲染层 CSS px 与 DIP 一一对应,禁止在渲染层再乘devicePixelRatio或zoomFactor;appearance里的宽高/边距全部是 DIP(constants.ts:264-290的注释口径)。 - 目标态新增:ClassIsland 的"竖直安全区"(Fluent 20px / Classic 0,
repos/ClassIsland/ClassIsland/App.Services.xaml.cs:314)在 ClassHelper 里以 P2 / 可选实现——仅当appearance.family === 'classisland'且设置项开启时,把锚点向屏幕内侧再推safe像素;默认 0,不得改变现有视觉位置。
7.1.4 点击穿透、命中与触摸
这是岛最容易出 regress 的部分,必须逐条保持:
- 窗口默认
setIgnoreMouseEvents(true, { forward: true })(main/island.ts:527)。 - 渲染层只上报"进入岛体"(
setInteractive(true)),不上报关闭;关闭由主进程按真实光标轮询决定(IslandApp.vue:456-487的整段说明)。理由:两边都下发会让窗口在"接收/穿透"之间每几毫秒翻转,表现为"点了没反应"。 - 命中矩形
hitRect:渲染层用.island-card/.resident-card的getBoundingClientRect()上报(IslandApp.vue:421-454、CustomIslandRuntime.vue:200-213),限流 50ms 且尾随发送一次;主进程按 60ms 兜底校正(main/island.ts:1824-1836)。 - 触摸模式:渲染层按
navigator.maxTouchPoints > 0上报setTouchMode(true)(IslandApp.vue:546-549),主进程切到"窗口贴合岛体 + 始终接收输入"(main/island.ts:1891-1899, 1795-1807)。触摸模式的窗口只增不减,收小要等窗口隐藏后再做(syncTouchLayout,:1560-1578)。 - 失焦宽限:
BLUR_GRACE_MS = 500、触摸/编辑态BLUR_GRACE_TOUCH_MS = 1200(main/island.ts:2287, 2297);blur 时若指针仍在岛上则忽略(:565-578)。 - 常驻模式的指针避让淡化:
residentFaded由updateResidentPointer()计算(:1851-1877),触发条件包含"非编辑、非课程提醒、非展开、非触摸操作后 1s、未悬停铃铛",淡化比例 =CLASSISLAND_ISLAND_METRICS.fadedOpacity = 0.05(island-layout.ts:35)。 - 编辑预览时不创建额外桌面窗口(
docs/app/island.md:47),编辑态由island:set-editing控制setInteractive(true)(main/island.ts:1430-1432)。
7.1.5 窗口护栏(不得删减)
- 渲染进程心跳:渲染层每 5s
alive()(LayoutIslandApp.vue:36),主进程RENDERER_ALIVE_TIMEOUT_MS = 20_000(main/island.ts:2315),超时隐藏并重建窗口,重建限流 3 次 / 10 分钟(:2317-2318)。 - 淡入淡出:步进 0.16 / 0.2、16ms 一步(
fadeIn/fadeOut,:1674-1710);appearance.animations === false时直接落位。 - 窗口只在"尺寸/位置设置变化"或"不可见"时重排(
applyWindowLayout前先比较 4 个值,:1598-1612);禁止在开合动画中改窗口几何(:1477-1479的注释)。 suspended(被 custom 或课件覆盖时)必须停掉经典岛的测量与命中(IslandApp.vue:238-246的syncTargets早退)。
7.2 行、背景、进度条、遮罩与淡出(ClassIsland 规格:custom 恒定 / fixed 默认)
7.2.1 行容器 DOM(目标态)
CustomIslandRuntime.vue 的常态页与展开页都必须渲染成下面这棵树(现状见 :396-504,需按此重排):
<div class="resident-window" data-anchor="left|center|right" data-bottom="true|false" data-family="classisland" :data-style="appearance.style">
<section class="custom-card resident-card island-card">
<svg class="shape" v-if="appearance.family === 'winisland'"><path :d="path" /></svg>
<!-- classisland:不用 squircle 形状层,圆角由 CSS border-radius 承担(见 7.2.2) -->
<span class="tint" aria-hidden="true"></span>
<div class="resident-page resident-compact-page">
<div class="resident-source resident-compact">
<div class="layout-rows" style="gap: var(--ci-line-gap)">
<section class="layout-row" data-row="main">
<div class="row-content">
<div class="row-components" style="gap: var(--ci-component-gap)">
<ComponentNode … />
</div>
</div>
</section>
</div>
<button class="resident-bell">…</button> <!-- 消息入口,见 7.4.12 -->
</div>
</div>
<div class="resident-page resident-detail-page">…</div>
<div class="resident-page resident-lesson-page">…</div>
</section>
</div>
强制要求:
- 行容器类名保持
.layout-row/.row-content/.row-components(现有 CSS 已依赖,custom-island.css:145-164、custom-runtime.css:98-121),不要改名;新增的规格通过 CSS 变量注入。 .layout-rows的gap从scheme.spacing改为var(--ci-line-gap)(默认 5px,island-layout.ts:34),scheme.spacing仅作为覆盖值(编辑器"行间距"仍是它)。- 组件列表的
gap从row.gap改为var(--ci-component-gap)(默认 12px)。语义映射:ClassIsland 的相邻组件间距 = 两个Margin="6 0"之和 = 12(repos/ClassIsland/ClassIsland/MainWindow.axaml:396)。因此row.gap的默认值应从 12 改为 12 并作为覆盖(island-layout.ts:343默认 8 → 改为 12),范围 0–64 保留。
7.2.2 行的像素规格(ClassIsland 规格)
必须写死为 CSS 变量并在两个 CSS 文件里引用(custom-runtime.css 为主):
.resident-window {
--ci-line-height: 40px; /* ClassIsland IslandContainerHeight,MainWindowLine.axaml:16 */
--ci-line-pad-x: 12px; /* GridContentRoot Margin="12 0",FluentTheme/Styles.axaml:110 */
--ci-line-gap: 5px; /* MainWindowLineVerticalMargin 默认 5,Models/Settings.cs:219 */
--ci-component-gap: 12px; /* 组件 Margin="6 0" ×2,MainWindow.axaml:396 */
--ci-radius-island: 8px; /* RadiusX 默认 8,Models/Settings.cs:183 */
--ci-island-opacity: 0.5; /* Opacity 默认 0.5,Models/Settings.cs:64 */
--ci-line-opacity: 1; /* 行整体不透明度默认 1.0,MainWindowLineSettings.cs:42 */
--ci-stroke-island-w: 1px; /* line-background BorderThickness=1,FluentTheme/Styles.axaml:47 */
--ci-shadow-island: 0 4px 8px 2px #48000000; /* FluentTheme/Styles.axaml:49 */
--ci-progress-height: 3px; /* 本轮裁决,所有进度条共用 */
--ci-fade-ratio: 0.05; /* CLASSISLAND_ISLAND_METRICS.fadedOpacity,island-layout.ts:35 */
}
行本体:
.layout-row {
min-height: var(--ci-line-height);
padding: 0 var(--ci-line-pad-x); /* ClassIsland 的 12px 内边距搬到行上 */
box-sizing: border-box;
display: flex;
align-items: center;
opacity: var(--ci-line-opacity);
}
.row-components {
display: flex;
align-items: center;
gap: var(--ci-component-gap);
min-height: var(--ci-line-height);
}
方案级覆盖:第 5、13 部分在 scheme 上新增 rowHeight / componentGap / showProgress 三个字段(默认 40 / 12 / true)。渲染时按"scheme 覆盖 > 外观族默认"写入 CSS 变量:
.resident-card {
--ci-line-height: v-bind('scheme.rowHeight ? scheme.rowHeight + "px" : "40px"');
--ci-component-gap: v-bind('scheme.componentGap + "px"');
}
showProgress=false 时所有行内进度条(课程进度、倒计时进度)隐藏;行距仍是 --ci-line-gap(默认 5,来自 scheme.spacing,island-layout.ts:34)。
背景(两种形态都要):
/* 整块岛背景(separated=false) */
.resident-card {
background: var(--ci-bg-secondary); /* SolidBackgroundFillColorSecondaryBrush */
border: var(--ci-stroke-island-w) solid var(--ci-stroke-island);
border-radius: var(--ci-radius-island);
box-shadow: var(--ci-shadow-island);
opacity: var(--ci-island-opacity); /* 只作用背景,不作用文字 */
}
/* 分体(scheme.separated=true):每行一个独立胶囊 */
.resident-card.resident-separated { background: transparent; box-shadow: none; border: 0; padding: 0; }
.resident-separated .layout-row {
background: var(--ci-bg-secondary);
border: var(--ci-stroke-island-w) solid var(--ci-stroke-island);
border-radius: var(--ci-radius-island);
box-shadow: var(--ci-shadow-island);
opacity: var(--ci-island-opacity);
padding: 0 var(--ci-line-pad-x);
}
硬性要求:背景不透明度只允许作用在背景层;文字/图标必须 100% 不透明(ClassIsland 把 Opacity 绑在 Border.line-background 上,repos/ClassIsland/ClassIsland/XamlThemes/FluentTheme/Styles.axaml:44)。现有实现里卡片 opacity 由主进程 win.setOpacity() 统一控制(main/island.ts:1440-1442),不得把背景不透明度也塞进窗口 opacity——必须拆成"窗口 opacity(用户可调 0.4–1)+ 背景层 alpha(0.5)"两层。
7.2.3 组件内边距的归零
现在 ComponentNode 自身有 padding: 6px(ComponentNode.vue:295-301),行 .row-components 的 gap 又是 row.gap,两者叠加会让实际间距变成 gap + 12。ClassIsland 是"组件 0 内边距 + 行 12px 内边距 + 组件 6/6 外边距"。
改造:
.component-node { padding: 0; } /* 由行 padding 承担左右留白 */
.row-components { gap: var(--ci-component-gap); } /* 12px */
同时在 ComponentNode 上移除 overflow: hidden(ComponentNode.vue:296)→ 改为 overflow: visible,否则倒计时进度环、天气预警告牌会被裁掉;裁切交给行(clip-path 或 overflow: hidden 挂在 .layout-row)。
7.2.4 进度条
三条进度条统一 3px、border-radius: 1.5px、贴底、transform-origin: left:
| 位置 | 现状 | 目标 |
|---|---|---|
| 课程行内进度 | .lesson-progress { height: 2px; background: #ffffff24 }(ComponentNode.vue:348-355) |
height: var(--ci-progress-height);底色 rgba(255,255,255,0.08);填充 var(--ci-accent) |
| 课程提醒卡进度 | .ci-lesson-progress { height: 2px }(LessonReminderContent.vue:307-316) |
同上,高度 3px |
| 通知遮罩进度(新增) | 无 | 同 7.2.6,贴在遮罩层底部 |
过渡:课程进度 transition: transform 1s linear(ComponentNode.vue:361)保留;提醒进度 50ms linear(LessonReminderContent.vue:315)保留;通知倒计时进度 100ms linear(prefers-reduced-motion 时 0)。
7.2.5 淡出与透明度
- 整窗淡入淡出:主进程步进 0.16/0.2 × 16ms(
main/island.ts:1674-1710)保留。 - 指针避让淡化:
residentFaded→win.setOpacity(displayOpacity × 0.05)(:1440-1442+island-layout.ts:35)保留;必须保证展开态、编辑态、课程提醒期间不淡化(:1864-1873)。 - 行级淡出(新增,P2):ClassIsland 的
IsLineFaded是 0.05(repos/ClassIsland/ClassIsland/Controls/MainWindowLine.axaml:73-75);ClassHelper 已有"整窗淡化",默认不叠加行级淡化,仅当pointerFadeReversed语义需要时用行级 opacity 过渡 100ms 实现。
7.2.6 斜切遮罩(通知遮罩必须复用课程提醒的实现)
ClassIsland 的遮罩是 5 段斜切,参数为:区段权重 [0, 0.1, 0.2, 0.4, 0.2, 0.1]、60° 斜面、区段 360ms、错峰 120ms、开 QuarticEaseOut / 关 QuadraticEaseIn、文字 delay 260ms + 250ms 淡入 + scale 1.1→1.0 / 750ms(repos/ClassIsland/ClassIsland/Controls/SlantedMaskControl.cs:177-200, 284-308;行模板 XamlThemes/FluentTheme/Styles.axaml:329-367)。
现状:这套算法已经存在于 LessonReminderContent.vue:59-102,并且参数完全对得上——edges = [0, 0.1, 0.3, 0.7, 0.9, 1](等价权重 0/0.1/0.2/0.4/0.2/0.1)、offset = tan(30°) × 40、delays = open ? [0,90,120,90,0] : [120,90,0,90,120]、ease = 1-(1-p)^4 / p²、总时长 480ms、兜底 600ms。
改造要求:
- 把
LessonReminderContent.vue的遮罩部分抽成packages/desktop-client/src/island/SlantedMask.vue(props:width、height、open: boolean、accent: string、motion: boolean、speed: number),LessonReminderContent与新的通知遮罩共用。 - 通知遮罩的 DOM 必须与 ClassIsland 对齐:
<div class="ci-mask-layer" :class="{ open }">
<svg class="ci-mask" :viewBox="`0 0 ${width} 40`" preserveAspectRatio="none">
<polygon v-for="(p, i) in polygons" :key="i" :points="p" />
</svg>
<div class="ci-mask-title" :class="{ 'ci-title-in': open }">…通知标题…</div>
</div>
- 遮罩填充色 =
var(--ci-accent-fill)(ClassIsland 用AccentFillColorDefaultBrush);文字色 =var(--ci-text-on-accent)。 prefers-reduced-motion或appearance.animations === false时直接落位(regions全部 0/1),不做逐帧插值(LessonReminderContent.vue:77-80已有该分支,保留)。- 不得给遮罩层加
filter: blur()或backdrop-filter(透明窗口下不可靠)。
7.2.7 行/组件隐藏规则
ClassHelper 现有的 advanced.hideRules 只有三值:in-class / no-message / no-class(island-layout.ts:101-106,判定在 ComponentNode.vue:108-120)。目标是 ClassIsland 的规则集(HideMode 基本/高级、HideOnClass、HideOnMaxWindow、HideOnFullscreen、规则集编辑器,repos/ClassIsland/ClassIsland/Models/Settings.cs:185, 418-505)。
本轮落地口径:
- 保留现有三值并保证语义不变(
in-class↔ 上课隐藏、no-message↔ 无消息隐藏、no-class↔ 无课程隐藏)。 - 新增(v2 schema,见第 5 部分):
hideRules允许字符串扩展'max-window' | 'fullscreen',判定数据由主进程通过IslandState新增字段foreground: { maximized: boolean; fullscreen: boolean }下发(主进程已有前台窗口检测能力,repos/ClassIsland/ClassIsland/MainWindow.axaml.cs:444-457为语义参考)。 - 行级隐藏(ClassIsland 的
MainWindowLineSettings.HideOnRule)本轮只做"行整体可隐藏",P1;规则集编辑器(RulesetControl)不在本轮范围,P2。 - 编辑器必须能编辑
hideRules(现有三值)与align/width/maxWidth;新增两项后要同步IslandComponentSettings.vue与normalizeIslandLayouts的白名单(island-layout.ts:321-328)。
7.2.8 消息气泡(family='classisland' 的 fixed 模式,40px 行内)
硬性约束:气泡不得增高行、不得增高窗口。 行高恒 40px(--ci-island-height: 40px 不得被气泡改写),气泡的遮罩、内容、进度条全部在这个 40px 容器内完成。
DOM(叠在行内容之上,position: absolute; inset: 0):
<div class="ci-row-shell"> <!-- height: 40px; position: relative -->
<div class="layout-row">…常态行组件…</div> <!-- 气泡展开时 opacity → 0(不卸载,保留宽度测量) -->
<div class="ci-row-bubble" v-if="mode==='expanded'"> <!-- inset: 0; border-radius: 8px; overflow: hidden -->
<SlantedMask class="ci-bubble-mask" :open="true" :width="rowWidth" :height="40" />
<div class="ci-bubble-row"> <!-- display:flex; align-items:center; height:40px; padding: 0 12px -->
<span class="ci-bubble-icon">…</span> <!-- 16×16 线性图标 -->
<span class="ci-bubble-badge">紧急</span> <!-- 可选;高 18、圆角 9、字号 11 -->
<div class="ci-bubble-text"> <!-- min-width:0; flex:1; display:flex; flex-direction:column; justify-content:center -->
<span class="ci-bubble-title">…单行省略…</span>
<span class="ci-bubble-body">…最多 2 行…</span>
</div>
<span class="ci-bubble-actions">…按钮…</span>
</div>
<div class="ci-bubble-progress" :style="{ transform: `scaleX(${leftProgress})` }" />
</div>
</div>
CSS 规格:
.ci-row-bubble { position: absolute; inset: 0; border-radius: var(--ci-radius-island); overflow: hidden; }
.ci-bubble-text { min-width: 0; flex: 1 1 auto; display: flex; flex-direction: column; gap: 0; }
.ci-bubble-title {
font-size: var(--ci-fs-body); /* 16px */
font-weight: 600; line-height: 20px; /* 40px 行内两行:20 + 18 */
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ci-bubble-body {
font-size: var(--ci-fs-secondary); /* 14px */
line-height: 18px; color: var(--ci-text-2);
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
overflow: hidden; /* 正文最多 2 行,超出省略 */
}
.ci-bubble-progress {
position: absolute; left: 0; bottom: 0; width: 100%; height: var(--ci-progress-height);
background: var(--ci-accent-fill); transform-origin: left;
transition: transform 50ms linear;
}
行内的排版预算:标题 1 行(20px)+ 正文 2 行(18×2=36)已超过 40px,因此实际规则是——标题占用 1 行时正文最多 1 行;无标题时正文最多 2 行;两种都靠 -webkit-line-clamp + 固定行高把内容锁在 40px 内,不得用 overflow: visible 让文字溢出到行外。
动画(严格按 ClassIsland 原版参数):
| 阶段 | 属性 | 时长 / 延迟 | 缓动 |
|---|---|---|---|
| 遮罩斜掠(开) | 5 段 scaleX 0→1 |
每段 360ms,错峰 0/90/90/120/120ms | cubic-bezier(0.165,0.84,0.44,1)(QuarticEaseOut) |
| 遮罩斜掠(关) | 5 段 scaleX 1→0 |
每段 360ms,反向错峰 | cubic-bezier(0.55,0.085,0.68,0.53)(QuadraticEaseIn) |
| 气泡文字淡入 | opacity 0→1 |
250ms,delay 260ms | cubic-bezier(0.25,1,0.5,1) |
| 气泡文字缩放 | scale 1.1→1.0 |
750ms,delay 260ms | cubic-bezier(0.25,1,0.5,1) |
| 气泡文字淡出 | opacity 1→0 |
200ms | 线性 |
| 文字缩回 | scale 1.0→1.1 |
200ms | cubic-bezier(0.5,0,0.75,0) |
| 常态行淡出 | opacity 1→0 |
200ms | SineEaseIn(同 ClassIsland 的 GridContentRoot 过渡) |
| 进度条 | scaleX |
50ms | 线性 |
以上参数与 ClassIsland 完全一致(repos/ClassIsland/ClassIsland/Controls/SlantedMaskControl.cs:177-200;XamlThemes/FluentTheme/Styles.axaml:329-367)。
Classic 主题(appearance.theme='classic'):遮罩改为 translateY(-50px → 0)、200ms QuadraticEaseIn;退出 0 → +50px、delay 100ms、200ms QuadraticEaseOut;文字 200ms delay / 100ms(XamlThemes/ClassicTheme/Styles.axaml:265-331)。
禁止:islandListLayout() 的高度再参与 fixed 气泡(它只用于 custom 详情页与 winisland 详情卡);把气泡做成"行下方新增一块 64px 高的区域"。
7.3 外观族:family='classisland'(默认)与 family='winisland'(兼容,仅 fixed)
适用范围(硬性):appearance.family 只影响 fixed 模式的外观;custom 始终按 ClassIsland 规格渲染(7.2、7.4),不提供 winisland 回退。
7.3.1 数据与开关(字段名以第 5 部分为准)
新增顶层字段 appearance.family: 'classisland' | 'winisland',与既有 appearance.style(black/glass/tinted 色调)不是一个概念:
family决定 fixed 长什么样、怎么排(信息行 vs 胶囊、圆角 8 vs squircle);对custom无意义;style只在fixed + family='winisland'下有意义(纯黑/毛玻璃/主题色渐变),其余情况忽略。
落地规则:
packages/shared/src/constants.ts新增ISLAND_FAMILIES = ['classisland','winisland'] as const+ISLAND_FAMILY_LABELS(classisland: 'ClassIsland(默认)'、winisland: 'WinIsland 经典');ISLAND_STYLES(:324)与ISLAND_STYLE_LABELS(:326-330)保持原样不动。DEFAULT_ISLAND_APPEARANCE增加family: 'classisland'(:265-290)——这是"新安装默认"。normalizeAppearance()接受family白名单(main/island.ts:2244-2264),非法值回退'classisland'。- 升级迁移(必须与第 5 部分一致):旧配置没有
family字段 → 迁移时写入family: 'winisland'(老用户观感不变);新安装 →'classisland'。迁移函数放在packages/shared/src/island-layout.ts的normalizeIslandLayouts()里,与version一并处理。 - 设置页拆分(
IslandSettingsView.vue:291-305现绑定style):- "外观族"下拉绑定
appearance.family,仅在layout='fixed'时可见; - "视觉风格/色调"下拉仅在
layout='fixed' && family='winisland'时可见; layout='custom'时两者都隐藏(custom 恒 ClassIsland 规格),只保留圆角/行高/行距/字号等 Scheme 级设置;- 切换必须即时生效并可回退(走既有
appearance通道,preload/island.ts:48-49)。
- "外观族"下拉绑定
- 主题轴(
appearance.theme: 'fluent' | 'classic')由第 6 部分定义;本部分只要求"行背景色/描边/阴影/遮罩动画参数随 theme 切换",落点在custom-runtime.css的[data-theme='classic']覆盖组(custom 与 fixed-classisland 共用)。
7.3.2 family='classisland'(默认,必须实现)
fixed 模式下(重定义后)= 信息行 + 行内气泡:
| 维度 | 规格 | 落点 |
|---|---|---|
| 形态 | 1 行信息行(默认 日期 + 课程表),不是胶囊 | IslandApp.vue 新增 classisland 模板分支(见下方改造点) |
| 行高 | 40 | --ci-line-height |
| 行内边距 | 左右 12 | --ci-line-pad-x |
| 圆角 | 8(矩形圆角,不用 squircle) | border-radius: var(--ci-radius-island) |
| 背景 | 不透明度 0.5 + 1px 描边 + 阴影 0 4 8 2 #48000000 |
7.2.2 |
| 进度条 | 底部 3px(课程进度 / 提醒倒计时) | 7.2.4 |
| 字号 | 14 / 16 / 18 / 20 | --ci-fs-* |
| 消息 | 在固定 40px 行内渲染 ClassIsland 气泡:斜切遮罩(5 段)+ 标题 + 正文(最多 2 行省略)+ 底部 3px 倒计时进度条;不增高行、不增高窗口 | 7.2.8 + 第 8 部分 |
| 空闲 | 保持 hidden / idleSliver 语义(不改成常驻) |
main/island.ts:1483-1496 |
| 形变 | 无高度形变:行高恒 40;只有"行 ↔ 气泡"的内容层交叉淡入/遮罩斜掠;radius 恒 8 |
IslandApp.vue:216-264 的 classisland 分支 |
custom 模式下 = 现有 schemes/rows/nodes 模型升级,始终 ClassIsland 规格(7.2、7.4 全部适用),与 family 无关;课程提醒接管规则见 7.3.3。
关键改造点(IslandApp.vue):现有 fixed 模板是"胶囊层 + 展开层 + 课程层"(:606-678),胶囊内容是"图标 + 标题 + 副标题 + chevron"(:616-659)。改造要求:
<!-- family=classisland:fixed 信息行(固定 40px 高,气泡在行内叠加) -->
<div class="ci-row-shell" style="height: 40px">
<div class="layout-row"><div class="row-components">
<ComponentNode v-for="node in fixedRowNodes" … />
</div></div>
<div v-if="mode==='expanded'" class="ci-row-bubble" role="region">
<SlantedMask class="ci-row-mask" :open="true" :width="rowWidth" :height="40" />
<div class="ci-bubble-content">…标题 / 正文(2 行省略)/ 操作…</div>
<div class="ci-bubble-progress" :style="{ transform: `scaleX(${leftProgress})` }" />
</div>
</div>
fixedRowNodes= 默认[islandNode('date','fixed-date'), islandNode('schedule','fixed-schedule')];来源可以新建一个内置 fixed 方案(与 custom 的 scheme 结构复用),不要把节点写死在模板里。targetSize()增加classisland分支:高度恒 40,{ width: mode==='expanded' ? max(rowNaturalWidth + 24, 420) : rowNaturalWidth + 24, height: 40 };禁止出现40 + bubbleHeight之类的增高分支。- 展开/收起不做高度弹簧:
mode切换时只播"行内容 → 气泡"的遮罩斜掠与内容交叉淡入(7.2.8 的 360/120ms 与 260/250ms 参数);行高、窗口高全程不动。 family='winisland'时走原模板,逻辑零改动。
7.3.3 fixed 与 custom 差异总表(含课程提醒接管)
先给出三种组合的行为差异总表(后续 7.3.4 细化 winisland、7.3.5 说明共享约束),不得自行发明第四种组合:
| 维度 | fixed + family='classisland'(默认) |
fixed + family='winisland'(兼容) |
custom(常驻,恒 ClassIsland 规格,family 不适用) |
|---|---|---|---|
| 常态 | 隐藏(或细缝);消息到来才显示 1 行信息行 | 隐藏;消息到来弹胶囊 | 常驻方案 rows |
| 行高 | 恒 40 | 胶囊 44 / 展开卡按内容 | scheme.rowHeight(默认 40) |
| 消息展示 | 固定 40px 行内气泡(遮罩+标题+正文 2 行+3px 进度),行不增高 | 胶囊 → 详情卡(高度按 islandListLayout),窗口高度随形态变 |
铃铛 + 详情页(resident-detail-page),不改成行内气泡 |
| 课程提醒 | 显示完整课程提醒条:LessonReminderContent 渲染在 40px 行内(准备/上课/课间/下课前) |
显示课程提醒条(现有 ci-fixed-lesson 覆盖) |
不因 in-class/prepare/break 临时接管整岛;课程进度由 schedule 组件展示;ending 只在课程名右侧显示蓝色圆形秒数徽标(lesson-ending.css 的 30×30);不新增"临时接管一行"行为 |
| 课程进度 | 行底 3px 进度条(当前课程) | 课程提醒条底部 2px(改 3px,7.2.4) | schedule 组件内 3px 进度条 |
| 多消息 | 第 8 部分规则;气泡内最多 2 行正文,超出省略 | 列表模式(islandListLayout) |
详情页 ClassicNotifications list-mode |
| 空闲交互 | 鼠标移入不淡化(隐藏态);显示时按命中接收 | 指针避让淡化 0.05 | 指针避让淡化 0.05(updateResidentPointer) |
| 窗口几何 | 宽 = min(工作区宽, 960),高恒 40+2×pad(pad 默认 20) | 固定包围盒(最大形态 + 2×pad) | 固定画布(内容上限 + 2×pad) |
custom 课程提醒的落地改动(必须做,且只做这些):
CustomIslandRuntime.vue:31-33的lesson计算改为 恒null(不再让 prepare/on-class/break/in-class 触发resident-lesson页),并从模板移除/停用resident-lesson-page(:505-524)——custom 不再整岛接管。- 保留
ending徽标:ComponentNode.vue的.lesson-ending-badge(:261-266,样式lesson-ending.css:1-16)继续在课程名右侧显示-{remaining},尺寸 30×30、#0a84ff底、白字 12px/600、等宽数字。 - 不得新增 "custom 下临时接管一行显示课程提醒" 的行为(这是本轮明确排除的范围)。
7.3.4 family='winisland'(兼容,仅 fixed,保持现有外观)
- 几何:胶囊
width × height(默认 268×44)、圆角 =min(appearance.radius, h/2)(IslandApp.vue:206-212)、展开卡半径 =min(48 × radius/20, w/2, h/2)。 - 形状:squircle 超椭圆(
squircle.ts:21-24,指数 4.2、每角 14 段)。 - 底色:
appearance.style的black/glass/tinted三套 CSS 变量组(classic-island.css:60-86),用data-style属性表达(现有模板已写:data-style="appearance.style",IslandApp.vue:587、CustomIslandRuntime.vue:348)。 - 排版:
--island-font基础字号 + 系数(ISLAND_TYPE_SCALE,constants.ts:354-367),胶囊内边距0 font*0.78、gapfont*0.62(classic-island.css:207-216)。 - 阴影:只在展开态
0 2px 3px rgba(0,0,0,.11),且声明常驻只改参数(classic-island.css:110-124, 148-153)。 - 交互:胶囊点击展开、点击卡片空白收起、失焦宽限 500ms——全部保持现状。
7.3.5 三种组合的共享约束
无论 layout 与 family 如何组合都必须满足:
- 命中/穿透/触摸/失焦/心跳逻辑完全不变(7.1.4、7.1.5):family 只改 fixed 的渲染,不改输入、不影响 custom。
- 编辑器与设置页共用同一份
islandLayout数据;family不改变数据模型(除 7.4 的字段增补);custom的 scheme/rows/nodes 在两套 family 下渲染完全一致。 - 所有组合的
ComponentNode是同一个组件,规格共用 7.4;差异只在行容器、背景层与气泡容器。 prefers-reduced-motion与应用内"动画"开关同时生效;关闭动画时所有弹簧直接snap()(CustomIslandRuntime.vue:148-151、IslandApp.vue:249-256)。- 可回退:feature flag(
family)必须在 M2/M3 阶段即可一键切回旧 fixed 渲染;custom无回退档(始终 ClassIsland 规格),验收时fixed×2 family与custom三条路径都要跑(第 11、12 部分)。
7.4 组件逐个规格(11 内置 + 3 消息入口 + 课件覆盖)
统一约定(所有组件适用):
- 契约字段:
id / kind / settings / appearance / advanced / children(island-layout.ts:77-108)。 appearance:resourceOverridingEnabled / secondaryFontSize / bodyFontSize / emphasizedFontSize / largeFontSize / customForegroundEnabled / foreground / customBackgroundEnabled / background / customBackgroundOpacityEnabled / backgroundOpacity / customCornerRadiusEnabled / cornerRadius / opacity(:61-76)。advanced:hideRules / align / width / maxWidth(:101-106)。- 默认值来源:
islandNode()(:173-216),规范化边界在normalizeIslandLayouts(:257-403)。 - 外观应用方式:
ComponentNode.style计算(ComponentNode.vue:179-210)——opacity、width、maxWidth、alignSelf(仅父级有宽度时)、textAlign、自定义前景/背景/圆角、字号覆盖、容器方向与 gap。 - 字体:
--secondary默认 14、--emphasized默认 18、--large默认 20(由CustomIslandRuntime.vue:119-122按appearance.fontSize/13比例算出)→ 改为直接使用--ci-fs-secondary/emphasized/large(14/18/20),不再按字号比例缩放;只有appearance.fontSize作为"整岛缩放"时再乘系数。 - 图标:统一用
ComponentIcon.vue的 24×24 线性图标(ComponentIcon.vue:4-35,stroke-width 1.5);ClassIsland 用 FluentSystemIcons,不引入新图标字体,按语义映射(见各组件)。
7.4.1 date 日期
- 数据字段:仅
settings常规字段(text等未使用)。 - 渲染:
new Date(now).toLocaleDateString('zh-CN', { month:'long', day:'numeric', weekday:'long' })(ComponentNode.vue:134-135),如11月12日星期三。 - ClassIsland 基准:
ddd MM/dd格式(repos/ClassIsland/ClassIsland/Controls/Components/DateComponent.axaml:11),即周三 11/12。 - 改造:默认格式改为
周三 11/12(短星期 + 补零月日 + 斜杠),保留zh-CN区域;字号var(--ci-fs-body)(16px)、white-space: nowrap、font-variant-numeric: tabular-nums。 - 空数据兜底:
now异常时显示--,不得抛错。 - 动画:无。
7.4.2 clock 时钟
- 数据字段:
settings.hour12(默认false)、settings.seconds(默认true,island-layout.ts:182)。 - 渲染:
toLocaleTimeString('zh-CN', { hour:'2-digit', minute:'2-digit', ...(seconds?{second:'2-digit'}:{}), hour12 })(ComponentNode.vue:136-142)。 - ClassIsland 基准:
HH:mm/HH:mm:ss、冒号闪动(repos/ClassIsland/ClassIsland/Controls/Components/ClockComponent.axaml.cs:71-76)。 - 改造:
- 秒数默认改
false(与 ClassIsland 一致,Models/ComponentSettings/ClockComponentSettings.cs的ShowSeconds=false),旧配置不动; - 新增
settings.flashSeparator(默认false,P2 才做闪动);闪动规则sep = !flash || seconds || 秒%2===1; - 字号
var(--ci-fs-emphasized)(18px)、font-variant-numeric: tabular-nums、white-space: nowrap(现有:305-308已有)。
- 秒数默认改
- 空数据兜底:无。
- 动画:秒变化不做过渡(避免数字滚动抖动)。
7.4.3 weather 天气简报
- 数据字段:
settings.locationName / latitude / longitude(island-layout.ts:87-90)。 - 数据源:
bridge.getIslandWeather({ latitude, longitude })(preload/island.ts:15),Open-Meteo,15 分钟刷新,断网保留缓存并标"缓存"(docs/app/island.md:45)。 - 渲染:
WeatherBrief.vue(现有 2.9KB,需按下面改写)。
<span class="weather-brief" :data-stale="stale">
<icon class="weather-icon" /> <!-- 22×22,用 ComponentIcon 或天气码映射 -->
<span class="weather-temp">
<span class="value">23</span><span class="unit">°C</span>
</span>
<span v-if="windSpeed" class="weather-wind">…</span>
</span>
- ClassIsland 基准(
repos/ClassIsland/ClassIsland/Controls/Components/WeatherComponent.axaml:51-184):横排gap 6;图标 22×22;温度值 + 单位margin-left:6;湿度/风/气压/体感/AQI 为可选主信息(一次只显示一种);AQI 6 级配色;预警药丸高 22、圆角 11、文字 14px。 - 改造(P1 做图标 + 温度;其余 P2):
- 图标尺寸 22×22、
margin-right: 6px; - 温度
font-size: var(--ci-fs-body)(16px); - 地点缺失(
latitude/longitude === null)→ 显示"未设置地点"(opacity: 0.7),不得显示NaN; - 数据缺失 → 只显示图标占位 +
--; stale === true→ 追加小字"缓存"(var(--ci-fs-caption),opacity: 0.6)。
- 图标尺寸 22×22、
- 动画:数据刷新时数字变化不做过渡(避免每 15 分钟闪一下)。
7.4.4 countdown 倒计时
- 数据字段(
island-layout.ts:85-88):target: 'auto'|'next'|'end'|'school'|'date'、targetDate、countdownTitle。 - 渲染逻辑:
ComponentNode.vue:150-176(自动/下一节/本节下课/放学/指定日期五种目标,文案距上课 m:ss/距下课 m:ss/距放学 m:ss/距X N天)。 - ClassIsland 基准(
repos/ClassIsland/ClassIsland/Controls/Components/CountDownComponent.axaml:64-114):距离 <名称> 还有 <数值>,紧凑模式去掉"距离/还有";进度环 22×22(或底部进度条);自定义格式 token%D/%H/%M/%S/%P/%p/%L/%d/%h/%m/%s/%x。 - 改造(P1):
- 文案结构改为
distance(默认"距离")+countdownTitle+connector(默认"还有")+ 数值;新增settings.compact(默认false); - 新增
settings.showProgress(默认false)+settings.progressMode: 'ring'|'bar'(默认ring):ring 用 22×22 SVG 弧(stroke-width 3.1、stroke-linecap: round、半径 9.5),bar 用 3px 底部进度条; - 数值格式化:
%m/%s补两位、%D/%H/%M/%S向上取整(与 ClassIsland 一致),默认格式%m:%s(mm:ss); - 颜色:
var(--ci-text-1);settings.color覆盖(P2)。
- 文案结构改为
- 空数据兜底:无课程数据 →
今日课程已结束(现有:170);目标日期为空 →请设置目标日期(:152)。 - 动画:进度条
transition: transform 1s linear;ring 的stroke-dashoffset用 1s 线性过渡。
7.4.5 schedule 课程表
最复杂,必须逐条对照 ClassIsland。
- 数据字段:
settings.display: 'current'|'next'|'today'(默认current)、settings.count(默认 5,范围 1–40)、settings.showPeriod(默认true)、settings.showProgress(默认true)。 - 数据源:
schedules+timePoints(ComponentNode.vue:30-61),时间点优先用服务端完整时间表,缺失时回退课表条目。 - 显示规则(现有,保留):
ComponentNode.vue:74-97——current:当前时间点;无则下一节;next:下一节课 1 条;today:过滤DIVIDER、当前时间点、未隐藏的LESSON,按count取窗口并让当前项居中。
- ClassIsland 基准(
repos/ClassIsland/ClassIsland/Controls/Components/ScheduleComponent.axaml+LessonControlExpanded.axaml):- 每个时间点一个"课程 chip":左右各
16px × ScheduleSpacing(默认 1)、科目名18px Bold、附加信息 14px、进度条 3px; - 隐藏规则:
HideFinishedClass、ShowCurrentLessonOnlyOnClass、IsHideDefault、课间只在当前时展开; - 明天课表 4 种模式(P2);
- 空状态占位"今天没有课程。"/"今日课程已全部结束。"。
- 每个时间点一个"课程 chip":左右各
- 改造(P1):
<div class="node node-schedule" :data-display="display">
<div v-for="p in schedulePoints" :key="p.id" class="lesson-line" :class="{ current, break: p.timeType==='BREAK' }">
<strong class="lesson-name">{{ p.courseName || p.name || '—' }}</strong>
<span v-if="isEnding" class="lesson-ending-badge">-{{ remaining }}</span> <!-- 30×30 正圆,见 lesson-ending.css -->
<small v-if="current && settings.showPeriod" class="lesson-time">{{ p.startTime }}–{{ p.endTime }}</small>
<span v-if="current && p.timeType==='LESSON' && settings.showProgress" class="lesson-progress">
<i :style="{ transform: `scaleX(${elapsed})` }" />
</span>
</div>
<span v-if="!schedulePoints.length" class="lesson-empty">今天没有课程。</span>
</div>
- 课程名
var(--ci-fs-emphasized)(18px)+font-weight: 700;时间/附加信息var(--ci-fs-secondary)(14px); - 行内 gap 6px(现状
ComponentNode.vue:313-321已是 6px,保留); - 进度条 3px(7.2.4);
- 已结束课程
opacity: 0.6(ClassIslandLessonsListBox.axaml:56-58),过渡 150mscubic-bezier(0.645,0.045,0.355,1); - 分割线:
1×18、opacity: 0.25(现状:334-342),可提到 24px 与 ClassIsland 的 25px 对齐(P2); - 课后占位:现有"今日课程已结束"只用于 countdown;schedule 需新增
今天没有课程。/今日课程已全部结束。两态(ClassIslandLessonControlSettings默认文案)。 - 空数据兜底:
timePoints与schedules都为空 → 显示占位、不占高度超过 40px;只有schedules→ 回退按条目渲染(现有逻辑)。 - 动画:切换时间点时课程名不做滑动(ClassIsland 是整行裁切),只做进度条与徽标的数值更新。
7.4.6 text 文本
- 数据字段:
settings.text(默认'班级信息')、settings.template(默认false)。 - 渲染:模板变量
{date}/{time}/{course}(ComponentNode.vue:143-149)。 - ClassIsland 基准:
TextComponent(文本内容 + 自定义字体颜色 + 字体大小 16–30)。 - 改造:字号
var(--ci-fs-body);超长text-overflow: ellipsis(现有:125-129已有);模板变量保留;新增settings.fontSize(默认 16,范围 16–30)写入 inlinefont-size。 - 空数据兜底:
text为空 →display: none(不占宽度与间距)。 - 动画:无。
7.4.7 separator 分割线
- 数据字段:无。
- 现状:
1px × 24px、opacity: 0.25(ComponentNode.vue:369-377,模板:249)。 - ClassIsland 基准:
2×25、StrokeThickness=2、opacity 0.8(repos/ClassIsland/ClassIsland/Controls/Components/SeparatorComponent.axaml:10-14)。 - 改造:宽
2px、高25px、颜色var(--ci-text-fill-secondary)(rgba(255,255,255,0.72))、opacity: 0.8。 - 空数据兜底:无。
- 动画:无。
7.4.8 slide 轮播容器
- 数据字段:
settings.interval(默认 5,范围 1–300)、settings.direction、settings.gap。 - 现状:非编辑时按
floor(now / (interval*1000)) % children.length只渲染一个子项(ComponentNode.vue:121-130),无过渡动画。 - ClassIsland 基准:切换间隔默认 15s、三种模式(循环/随机/往复)、启用动画时 250ms 上滑淡入 + 上滑淡出、
cubic-bezier(0.65,0,0.35,1)、位移 ±40px(repos/ClassIsland/ClassIsland/Controls/Components/SlideComponent.axaml:72-107)。 - 改造(P1):
- 新增
settings.interval默认改 15(旧配置保留)、范围 1–300; - 新增
settings.slideMode: 'loop'|'random'|'repeat'(默认loop); - 新增
settings.animate(默认false):开启后切换用 CSS 类slide-in/slide-out,250ms、translateY(-40px → 0)、opacity 0 → 1;关闭时直接换(现状); - 子项
max-height: var(--ci-line-height)。
- 新增
- 空数据兜底:0 子项 → 塌缩为 0 宽;1 子项 → 不轮播。
- 动画:
prefers-reduced-motion时关闭过渡。
7.4.9 rolling 滚动容器
- 数据字段:
settings.speed(默认 30,范围 1–200)。 - 现状:CSS 动画
rolling(translateX(30%) → -80%)+--rolling-duration: max(1, 200/speed)s,无限线性(ComponentNode.vue:384-392, 446-453,:205)。 - ClassIsland 基准(
repos/ClassIsland/ClassIsland/Controls/Components/RollingComponent.axaml.cs:85-135):仅当内容宽 > 容器宽才滚动;dur = innerW / speed、可暂停、可回程;速度默认 40px/s。 - 改造(P1):
- 默认速度改 40;
--rolling-duration: calc(var(--rolling-distance) / var(--rolling-speed) * 1s); - 仅当
scrollWidth > clientWidth时加rolling类(用 JS 测量,ResizeObserver监听容器与内容); - 保留"编辑时不滚动"(
:390-392); - 暂停/停止规则 P2。
- 默认速度改 40;
- 空数据兜底:内容不溢出 → 静止、不抖动。
- 动画:
linear、无限循环;prefers-reduced-motion时静止。
7.4.10 group 分组容器
- 数据字段:
children、settings.direction(默认horizontal)、settings.gap(默认 8)。 - 现状:
display:flex、gap、align-items: center(ComponentNode.vue:206-208, 378-380)。 - ClassIsland 基准:分组容器 = 横向
StackPanel+Margin="-6 0"+ 子项Margin="6 0"(repos/ClassIsland/ClassIsland/Controls/Components/GroupComponent.axaml:13-33),即子项间距 12。 - 改造:
gap默认改 12;子项外边距 0;容器自身不带内边距。 - 空数据兜底:0 子项 → 0 宽。
- 动画:无(子项变化时行宽由行过渡)。
7.4.11 stack 堆叠容器
- 数据字段:
children、settings.direction、settings.gap。 - 现状:
flex-wrap: wrap(ComponentNode.vue:381-383)——这是"换行排列",不是 ClassIsland 的堆叠语义。 - ClassIsland 基准:
Panel,所有子项占据同一矩形、各自按对齐摆放(repos/ClassIsland/ClassIsland/Controls/Components/StackComponent.axaml:12-32)。 - 改造(P1):
.stack { display: grid; },所有子项grid-area: 1 / 1,align-self/justify-self由advanced.align决定;容器宽高取子项测量最大值;子项间距 0。 - 空数据兜底:0 子项 → 0 宽。
- 动画:无。
7.4.12 消息入口 notification / homework / call
这三个 kind 不是普通内容组件,而是"消息入口":
- 现状:渲染时在
ComponentNode里走分支(ComponentNode.vue:285-288),只有node.id === alertNodeId时才渲染<slot name="alerts">,否则编辑态显示文案;数据来自IslandState.active/queued。 - 组件库只展示前 11 类(
docs/app/island.md:40),消息类由铃铛与消息面板承担。 - ClassIsland 对应:通知行/提醒气泡(行内遮罩 + 浮层 + 进度条,
repos/ClassIsland/ClassIsland/XamlThemes/FluentTheme/Styles.axaml:70-202)。 - 改造(P1):
- 保留三个 kind 作为"入口标记":
notification= 通知入口、homework= 作业入口、call= 叫人入口; - 铃铛显示逻辑(
CustomIslandRuntime.vue:421-441)改为:有消息时显示图标 + 数量徽标(99+上限),无消息时显示 chevron;铃铛图标沿用ComponentIcon的notification路径; - 徽标尺寸保持 14px 高、圆角 7、
#f45b58(custom-island.css:94-110),文字 9px/600;ClassIsland 风格下允许改为var(--ci-accent-fill)底 + 白字(一行样式覆盖); - 消息卡本体的内容与业务由第 8 部分规定;本部分只规定容器:
custom详情页:高度 =islandListLayout().height(constants.ts:445+),宽度 = 岛上可用宽度,位置在resident-detail-page;fixed + classisland:固定 40px 行内气泡(7.2.8),islandListLayout()不参与;fixed + winisland:沿用现有详情卡高度 =islandListLayout().height。
- 保留三个 kind 作为"入口标记":
- 空数据兜底:无消息 → 三个入口都不渲染(
hidden计算已处理,ComponentNode.vue:108-120)。 - 动画:徽标出现
resident-indicator-in(custom-runtime.css:147-149, 199-206)保留;custom详情页展开/收起走高度弹簧;fixed + classisland走 7.2.8 的遮罩/文字动画,不做高度动画。
7.4.13 CoursewareTransfer 课件覆盖
- 触发:
state.coursewareTransfer非空时,LayoutIslandApp.vue:58, 71-75直接渲染课件卡,并让经典岛suspended、自定义岛不渲染(:57)。 - 现状视觉:
position: fixed、尺寸COURSEWARE_ISLAND_SIZE、padding 17px 24px、border-radius 24px、底色rgba(20,20,24,0.97)、阴影0 2px 8px #0004、字号 13/1.4、进度条 5px(CoursewareTransfer.vue:67-131)。 - ClassIsland 对应:课件传输不在 ClassIsland 中,属 ClassHelper 独有能力 → 保留现状视觉,只做两点对齐:
- 锚点/边距与岛一致(左/中/右 +
ISLAND_SHADOW_PAD,CoursewareTransfer.vue:12-25),尺寸不随style变化; pointer-events: none保留(:81),并由setHitRect上报矩形(:35-38),保证不吞桌面点击。
- 锚点/边距与岛一致(左/中/右 +
- 空数据兜底:
transfer.error存在时显示错误行(:64),不隐藏整卡。
7.4.14 组件默认值总表(必须与代码一致)
islandNode() 当前默认(island-layout.ts:173-216)必须逐项落实;本轮改动用 粗体 标注:
| 字段 | 当前默认 | 本轮目标 |
|---|---|---|
settings.text |
班级信息 |
保留 |
settings.template |
false |
保留 |
settings.hour12 |
false |
保留 |
settings.seconds |
true |
保留(新节点默认仍 true,兼容) |
settings.target |
auto |
保留 |
settings.targetDate |
'' |
保留 |
settings.countdownTitle |
目标日 |
保留 |
settings.locationName |
'' |
保留 |
settings.latitude/longitude |
null |
保留 |
settings.display |
current |
保留 |
settings.count |
5 |
保留 |
settings.showPeriod |
true |
保留 |
settings.showProgress |
true |
保留 |
settings.direction |
horizontal |
保留 |
settings.gap |
8 |
12(组/堆叠容器子项间距) |
settings.interval |
5 |
15(轮播间隔,秒) |
settings.speed |
30 |
40(滚动 px/s) |
appearance.* 字号 |
14/16/18/20 | 保留(改用 --ci-fs-*) |
appearance.background |
#202024 |
保留 |
appearance.backgroundOpacity |
0.5 |
保留 |
appearance.cornerRadius |
16 |
8(与 ClassIsland 圆角一致) |
appearance.opacity |
1 |
保留 |
advanced.align |
stretch |
保留 |
advanced.width/maxWidth |
0/0 |
保留 |
7.5 编辑器渲染侧结构(组件库 / 拖拽 / 选中框 / 行设置)
编辑器是主窗口里的设置页(IslandEditorView.vue,860 行),不是岛的渲染进程;但它的 DOM/CSS 必须与岛保持同一套 token,避免"编辑器里好看、上岛变形"。
7.5.1 两态:标签排列 vs 实际预览
- 非编辑态(
editMode=false):component-lines排列行与组件 chip(IslandEditorView.vue:659-695),每行一个.component-line。 - 编辑态(
editMode=true):island-preview按方案的真实布局渲染(:698-738),支持预览缩放0.3–1.5、步进0.1、重置(:739-750)。 - 必须保留:"编辑预览不创建额外桌面窗口"(
docs/app/island.md:47)。
7.5.2 尺寸与选中框(ClassIsland 规格)
| 元素 | 现状 | 目标 |
|---|---|---|
行容器 .component-line |
min-height: 54px、padding: 4px、border: 1px solid transparent、radius: var(--ch-radius-control)(island-editor.css:134-141) |
min-height: 48px(行高 40 + 上下 4)、padding: 4px 8px |
行把手 .row-handle |
width: 24px(:149-154) |
保留 24px |
| 组件 selecetd | background: var(--ch-accent-soft)、border-color: var(--ch-accent)(IslandEditorNode.vue:86-89) |
改为 1.5px 虚线:outline: 1.5px dashed var(--ci-accent);outline-offset: -1.5px;不加背景色 |
| 容器组件 | border: 1px dashed var(--ch-border-strong)、padding: 3px(:90-94) |
1.5px dashed、padding: 3px、border-radius: 8px |
| 拖入指示 | .drop-before/after 3px 强调色竖条(:111-123);.drop-inside 2px 实线(:107-110) |
保留,颜色改 var(--ci-accent)、圆角 1.5px |
| 拖拽幽灵 | .drag-ghost(IslandEditorView.vue:774-780) |
保留,padding: 4px 8px、border-radius: 6px、背景 var(--ci-bg-tertiary) |
| 详情抽屉 | el-drawer 宽 min(360px, 94vw)(:785) |
保留 |
7.5.3 行设置面板字段
现有行面板(IslandEditorView.vue:804-831):当前行、行名称(≤64 字)、组件间距(0–64)、允许显示提醒、设为主要行、上移/下移/删除。
目标态新增(与 ClassIsland 对齐,P1):
- 行不透明度
opacity(默认 1.0,范围 0.1–1,步进 0.05)——写在行style.opacity; - 行隐藏规则
hideOnRule(默认 false)+ 规则集(P2); - 行圆角覆盖(P2)、行背景色/不透明度覆盖(P2,对应 ClassIsland 行级"自定义背景颜色/不透明度/圆角")。
保存路径不变:bridge.saveLayout()(preload/island.ts:17)→ island:save-layout。
7.5.4 组件库
- 11 个条目(
date/clock/weather/countdown/schedule/text/separator/slide/rolling/group/stack),每项ComponentIcon+ 名称 + 描述(IslandEditorView.vue:751-770,数据来自ISLAND_COMPONENT_LABELS+ 本地描述表)。 - 交互:
pointerdown开始拖拽、click点击添加(:760-761)。 - 目标态:图标尺寸 18×18(
IslandEditorNode.vue:74-78)保持不变;卡片min-height: 76px(island-editor.css:285)保留;不得把notification/homework/call加回组件库(docs/app/island.md:40的既有约定)。
7.5.5 渲染侧必须新增/调整的 DOM 与 token
必须新增(岛渲染进程):
.ci-row-shell(classisland风格 fixed 模式的行容器);.ci-mask-layer/.ci-mask-title(7.2.6);.lesson-empty(课程表占位);.resident-bell的[data-family='classisland']覆盖;.layout-row[data-hide='true'](隐藏规则命中)。
必须新增(CSS 变量,挂在 :root 与 .resident-card 上):
--ci-line-height: 40px; --ci-line-pad-x: 12px; --ci-line-gap: 5px; --ci-component-gap: 12px;
--ci-radius-island: 8px; --ci-island-opacity: 0.5; --ci-line-opacity: 1;
--ci-stroke-island: #ffffff26; --ci-stroke-island-w: 1px;
--ci-shadow-island: 0 4px 8px 2px #48000000;
--ci-progress-height: 3px;
--ci-fade-ratio: 0.05;
--ci-fs-secondary: 14px; --ci-fs-body: 16px; --ci-fs-emphasized: 18px; --ci-fs-large: 20px;
--ci-bg-secondary: #20202080; --ci-text-1: rgba(255,255,255,0.92); --ci-text-2: rgba(255,255,255,0.72);
--ci-accent: (用户强调色); --ci-accent-fill: color-mix(in srgb, var(--ci-accent) 100%, transparent);
命名对齐:packages/shared/src/island-layout.ts:20-36 的 CLASSISLAND_ISLAND_METRICS 已经是这些值的单一来源(rowHeight:40、horizontalPadding:12、radius:8、opacity:0.5、rowSpacing:5、字号 14/16/18/20、fadedOpacity:0.05)。渲染层必须从该常量生成 CSS 变量(通过 style 内联或 applyAppearance 写入 document.documentElement.style,位置同 CustomIslandRuntime.vue:102-124),不得在 CSS 里再抄一份数字。
7.6 验收标准
实现者必须逐条自查,并只在显式 --visual 的视觉验证里做桌面像素比对(默认后台验证不显示岛):
- 默认值:新安装
appearance.family = 'classisland';旧配置(无family)升级后family = 'winisland'且 fixed 观感与升级前一致,不报错、不丢组件数据;设置页可一键切换并即时预览、可回退。 1.1family只影响 fixed:layout='custom'时切family不改变任何渲染(custom 恒 ClassIsland 规格),设置页在 custom 下不展示"外观族/视觉风格"两项;custom无 winisland 回退档。 family='classisland'的 fixed 模式是信息行不是胶囊:默认 1 行 = 日期 + 课程表、行高 40、圆角 8、背景不透明度 0.5、底部 3px 进度条;无消息时不显示(或按idleSliver留细缝);消息到来时在同一条 40px 行内渲染 ClassIsland 气泡(斜切遮罩 + 标题 + 正文 + 倒计时进度),收起后回到常态行。 2.1 气泡不增高行:气泡出现/消失前后,行高恒 40px(像素量测误差 ≤1px),窗口高恒 40+2×pad,win.getBounds()不变;正文最多 2 行、超出省略号,不得出现文字溢出到行外或行高变成 48/56/64。 2.2 气泡动画参数与 ClassIsland 一致:遮罩每段 360ms、错峰 0/90/90/120/120ms(总 480ms)、开 QuarticEaseOut / 关 QuadraticEaseIn;文字 250ms delay 260ms + scale 1.1→1.0/750ms;退出 200ms、缩回cubic-bezier(0.5,0,0.75,0);theme='classic'走 translateY 变体。family='winisland'的 fixed 模式仍是胶囊 + 详情卡:胶囊 268×44、圆角h/2、squircle、纯黑/玻璃/渐变三色调、展开态阴影0 2px 3px rgba(0,0,0,.11);与改造前逐像素一致。- 岛在 6 个停靠位置、两个显示器、125%/150% DPI 下位置正确:居中停靠不受
marginX影响;左右停靠距屏幕边缘 =marginX;底停靠时窗口贴底且岛不被裁。 - 窗口几何:开合过程中
win.getBounds()不变化(用主进程日志或注入断言验证);只有设置变化/不可见时才setBounds;classislandfixed 的窗口高恒为 40 + 2×ISLAND_SHADOW_PAD。 - 行:可见行高
40px(像素量测)、行内边距 12、行距 5、圆角 8、描边 1、阴影0 4 8 2 #48000000;相邻组件间距 12;进度条 3px。 - 背景不透明度 0.5 只影响背景:文字在截图里为纯白 0.92/0.72 层级,不随背景变透明。
- 三条进度条高度都是 3px(课程、课程提醒、通知气泡),且不出现 2px 残留。
custom模式:rows与expandedRows都按行渲染;scheme.separated=true时每行独立背景胶囊,无整块背景;custom 渲染恒按 ClassIsland 规格,切换family不产生任何视觉变化。 9.1 custom 不被课程提醒接管:prepare / on-class / break / in-class 期间岛仍显示常态行与铃铛(课程进度由schedule组件呈现),不出现整岛替换;ending只在课程名右侧显示蓝色圆形秒数徽标(30×30、-N);不存在"临时接管一行"的行为。- 遮罩:通知/课程提醒共用
SlantedMask,5 段从两侧向中间打开(总 480ms)、关闭反向;开启减少动态效果或关闭动画时直接落位。 - 触摸屏:手指能点开岛(触摸模式生效),窗口贴合岛体,失焦宽限 1200ms;非触摸屏行为与改造前完全一致。
- 鼠标穿透:岛外点击一律落到桌面;命中矩形随形变更新(限流 50ms),形变结束后无"看不见却点不动"的死区。
- 11 个组件逐项:
date显示周三 11/12、clock显示HH:mm、weather显示 22px 图标 + 温度且无数据时不出现NaN、countdown文案与进度环/条可选、schedule当前项高亮 + 3px 进度 + 已结束 0.6 淡化 + 空占位、text空内容不占宽、separator2×25/0.8、slide15s 切换且可关动画、rolling仅溢出时滚动且 40px/s、group间距 12、stack子项完全重叠。 - 消息入口:铃铛数量徽标正确(1–99,>99 显示
99+),无消息显示 chevron;notification/homework/call不在组件库出现,但旧方案里的节点仍能渲染。 - 课件覆盖:传输中出现课件卡且岛隐藏;锚点与边距正确;
pointer-events: none,不吞点击。 - 编辑器:选中组件为 1.5px 虚线强调框;容器组件为 1.5px 虚线;拖入指示在 before/after/inside 三种位置正确;预览缩放 0.3–1.5 不导致岛变形错位。
prefers-reduced-motion与设置里的"动画"开关都生效:关闭时所有弹簧snap()、滚动静止、遮罩直接落位。- 不得出现:
setBackgroundMaterial、backdrop-filter、第二个BrowserWindow、把窗口尺寸写进 CSS 的做法、在动画帧里setBounds。