- 重构版主提示词(14 parts,面向 ClassHelper 灵动岛重构) - ClassIsland 全量规格提示词(20 parts,含分卷与逐章版本) - 投喂指南、分卷、逐节文件 - 调研记录、一致性基线、生成与质检脚本
1225 lines
112 KiB
Markdown
1225 lines
112 KiB
Markdown
## 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` |
|
||
|
||
实现要求:
|
||
|
||
1. 岛根元素必须同时挂三个数据属性,CSS 用它们切 token 与几何:`data-family="classisland|winisland"`、`data-theme="fluent|classic"`、`data-color-scheme="light|dark"`(winisland 恒为 `dark`)。禁止用 JS 拼内联样式切风格。
|
||
2. `family='classisland'` 时**不使用** `spring.ts` / `squircle.ts`:几何用标准 `border-radius` + CSS transition;`family='winisland'` 时继续用弹簧形变与超椭圆路径(Lead 裁决第 4 条,`parts-r/01-objective.md:52`)。
|
||
3. 两套 family 的 token **不得混用**:classisland 的静态值定义在 `--ci-*`;winisland 的调色板保持 `--wn-*`;运行时外观注入继续用 `--island-*`(两套共用这几个动态变量:`--island-font`/`--island-radius`/`--island-accent`/`--island-dur`/`--island-anim`)。
|
||
4. 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。
|
||
5. 主题切换动画只允许改颜色类 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 四档派生公式(必须逐字实现)**:
|
||
|
||
```text
|
||
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 或直接落位 | **KEEP 扩展**:classisland 新增 `--ci-anim: 0|1`,与 `--island-anim` 二选一由 family 决定 |
|
||
|
||
### 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 相关部分)
|
||
|
||
1. `config.json.island` 增加 `family`、`theme`、`colorScheme`、`winislandStyle`(可选,默认为旧 `style` 的值)。
|
||
2. **老用户升级**:`family` 缺失 → 判为老用户 → `family='winisland'`,`winislandStyle = 旧 style`,`theme/colorScheme` 写默认值但忽略。**新安装**(无 config 或 `island` 缺失)→ `classisland`。
|
||
3. 旧 `style='black'|'glass'|'tinted'` 保留字段(winisland 使用),同时写入 `winislandStyle`;classisland family 下忽略 `style`。
|
||
4. `appearance.accent` 保持用户值;仅当值等于旧默认 `#0a84ff` 且迁移到 classisland 时改 `#00BFFF`,并在迁移日志中记录。
|
||
5. `appearance.fontSize`:winisland 保持 13;迁移到 classisland 时若仍为 13 → 改为 16(避免 4 档过小),并记录。
|
||
6. `scheme.radius`/`node.cornerRadius` 不做强制改写(无损);classisland 渲染时若 `radius` 超过 20 则钳制到 20(`appearance.radius` 范围扩到 0–32 仅为兼容 winisland,classisland 侧钳制)。
|
||
7. 迁移必须幂等:`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 类错误(每一条都对应真实会出现的问题):
|
||
|
||
1. **把 `--ci-*` 写进主窗口的 `renderer/styles/index.css`**:岛是独立渲染入口,主窗口的 `:root` 不共享;写错位置会导致岛内变量解析为空、整条 `transition` 简写失效(`docs/dev/motion.md:58-59` 已记录该坑)。
|
||
2. **漏掉 `island.html` 的样式引入顺序**:`tokens.css` 必须在 `classic-island.css`/`custom-*.css` 之前 import,否则后者的 `var()` 在解析时找不到值。
|
||
3. **用 `html.dark` 给岛做浅色**:岛不加载主窗口的 `<html class="dark">`,必须 `data-color-scheme`。
|
||
4. **在 classisland family 里继续用 squircle**:`squirclePath()` 只在 winisland/custom 分支调用;classisland 用 `border-radius: var(--ci-radius-island)`。
|
||
5. **把 `appearance.opacity` 同时当作背景不透明度**:窗口不透明度(0.4–1,阅读消息强制 1)与背景不透明度(默认 0.5)是两个 token;混用会导致"消息看不清"或"背景太实"。
|
||
6. **给行容器加 `backdrop-filter`**:会模糊整块矩形并破坏逐像素透明;毛玻璃只在 winisland 的 `glass` 里以半透明色模拟(`docs/dev/winisland-design-tokens.md:23-24`)。
|
||
7. **用 `filter: drop-shadow` 做 classisland 行投影**:会新建合成表面导致开合闪帧;classisland 用 `box-shadow`(`classic-island.css:114-120`)。
|
||
8. **在 classisland 分支继续用 `--wn-text-*`**:winisland 的 0.92/0.72/0.58 与 classisland 的 100/85.5/77.3/52.9/36.5 不是一套;混用会让同一行文字出现两种灰。
|
||
9. **字号派生写成固定四档**:classisland 四档必须由 `fontSize/16` 比例派生;写死 14/16/18/20 会让设置页字号滑杆失效。
|
||
10. **把圆角钳制只写在 CSS**:`aspect` 变化时 `appearance.radius` 可能大于行高一半,必须在 JS 里也钳制到 `min(radius, w/2, h/2)`(现有 `IslandApp.vue:334-341` 已做,classisland 分支同样要做)。
|
||
11. **迁移时无条件改用户设置**:只有"仍等于旧默认值"的 accent/fontSize 才允许改;用户调过的值必须原样保留(§6.12)。
|
||
12. **忘记给编辑器预览同步 token**:`island-editor.css` 的预览行、背景、圆角必须与真岛一致,否则"预览一个样、上屏另一个样"。
|
||
13. **把 ClassHelper 旧展开卡行为带进 classisland fixed**:fixed 行高恒 40,气泡/遮罩/进度条都在行内完成,正文最多 2 行省略;**不得**写「行高随内容增高」「行高 200ms 过渡」「为气泡扩窗」,`maxCardHeight` 只服务 custom 多条列表与强提醒窗口。
|
||
|
||
自查命令(无头执行,不启动 Electron):
|
||
|
||
```bash
|
||
# 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 验收标准
|
||
|
||
1. **单一来源**:`grep -rn "#[0-9a-fA-F]\{6\}" packages/desktop-client/src/island` 只允许命中 `tokens.css`、`classic-island.css` 的 `--wn-*` 定义块、`custom-*.css` 的兼容块与 SVG 渐变;两个 `.vue` 文件内联样式里不得出现颜色字面量(`--state-color` 计算值除外)。
|
||
2. **双风格隔离**:切到 `winisland` 后,岛仍显示 268×44 胶囊 + squircle + 弹簧开合,像素 diff 与重构前一致(±1px);切到 `classisland` 后无 squircle path、无弹簧形变,行高 40/圆角 8/进度条 3px。
|
||
3. **默认值**:新安装 `family='classisland'`、`theme='fluent'`、`colorScheme='system'`;旧 fixtures 升级后 `family='winisland'` 且 `winislandStyle` 等于旧 `style`。
|
||
4. **字号**:classisland 下把 `fontSize` 从 16 改到 20,四档变为 17.5/20/22.5/25,行高仍 40 且内容按第 7 部分规则省略;winisland 下改字号时全部文本按排版系数等比缩放。
|
||
5. **颜色**:深/浅色各截一张基准图,`--ci-text-1..5`、背景 0.5、描边 1px、进度条轨道/填充与 §6.2/§6.10 表逐项一致(颜色 ±2/255)。
|
||
6. **Fluent/Classic**:切换主题后,行背景色、描边有无、阴影、圆角(Fluent 8 / Classic 0)按 §6.2.6 变化,字号与间距不变。
|
||
7. **不透明度语义分离**:把行背景不透明度调到 0.2、窗口不透明度调到 0.8,两者独立生效;打开一条消息时窗口不透明度强制 1(`displayOpacity`),关闭后回到 0.8。
|
||
8. **层级**:消息遮罩在内容之上、遮罩文字在遮罩之上;编辑器拖拽幽灵 `z-index:10000` 高于一切;岛窗口始终 `screen-saver` 层级。
|
||
9. **DPI**:100%/125%/150%/200% 四档下,行高 40、圆角 8、字号逻辑值不变,无 0.5px 模糊;截图基准按 devicePixelRatio 换算后 ±1px。
|
||
10. **迁移幂等**:对同一份配置连续运行 `normalizeIslandAppearance()` 三次,输出字节级一致;旧字段 `residentEnabled`、`style` 仍在且语义不变。
|
||
11. **兼容别名**:`--ci-lesson-*` 重命名期间旧 `--ci-duration` 等变量仍生效(至少一个版本),组件视觉无变化。
|
||
12. **无新依赖/无新框架**:`package.json` 依赖零新增;token 只用 CSS 自定义属性,不引入 CSS-in-JS 或新 UI 库。
|
||
13. **fixed 气泡恒定 40px**:构造超长正文(>500 字)的消息,行高仍为 40、正文最多 2 行且以省略号收尾,窗口 `bounds` 不因该消息变化;`maxCardHeight` 只在 custom 多条列表/强提醒窗口路径被读取。
|
||
14. **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 改造口径与文件级改动清单
|
||
|
||
**总口径(必须遵守)**:
|
||
|
||
1. 目标视觉**默认改为 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`,避免观感突变**。
|
||
2. **`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)。
|
||
3. 不新增第二个窗口、不新增框架、不改服务端:`LayoutIslandApp.vue` 仍是唯一渲染入口,`main/island.ts` 仍是唯一窗口持有者(`packages/desktop-client/src/island/LayoutIslandApp.vue:1-76`)。
|
||
4. 消息/提醒/课程提醒的业务语义(优先级、去重、上课暂存、紧急展开、已读回执)归第 8 部分;本部分只规定它们**在岛上的容器、尺寸、颜色、动画**。
|
||
5. 所有像素值以本文与第 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`)。
|
||
- 行宽/内容宽必须由渲染层测量后上报:`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 的部分,**必须逐条保持**:
|
||
|
||
1. 窗口默认 `setIgnoreMouseEvents(true, { forward: true })`(`main/island.ts:527`)。
|
||
2. 渲染层只上报"进入岛体"(`setInteractive(true)`),**不上报关闭**;关闭由主进程按真实光标轮询决定(`IslandApp.vue:456-487` 的整段说明)。理由:两边都下发会让窗口在"接收/穿透"之间每几毫秒翻转,表现为"点了没反应"。
|
||
3. 命中矩形 `hitRect`:渲染层用 `.island-card`/`.resident-card` 的 `getBoundingClientRect()` 上报(`IslandApp.vue:421-454`、`CustomIslandRuntime.vue:200-213`),限流 50ms 且尾随发送一次;主进程按 60ms 兜底校正(`main/island.ts:1824-1836`)。
|
||
4. 触摸模式:渲染层按 `navigator.maxTouchPoints > 0` 上报 `setTouchMode(true)`(`IslandApp.vue:546-549`),主进程切到"窗口贴合岛体 + 始终接收输入"(`main/island.ts:1891-1899, 1795-1807`)。触摸模式的窗口**只增不减**,收小要等窗口隐藏后再做(`syncTouchLayout`,`:1560-1578`)。
|
||
5. 失焦宽限:`BLUR_GRACE_MS = 500`、触摸/编辑态 `BLUR_GRACE_TOUCH_MS = 1200`(`main/island.ts:2287, 2297`);blur 时若指针仍在岛上则忽略(`:565-578`)。
|
||
6. 常驻模式的指针避让淡化:`residentFaded` 由 `updateResidentPointer()` 计算(`:1851-1877`),触发条件包含"非编辑、非课程提醒、非展开、非触摸操作后 1s、未悬停铃铛",淡化比例 = `CLASSISLAND_ISLAND_METRICS.fadedOpacity = 0.05`(`island-layout.ts:35`)。
|
||
7. 编辑预览时**不创建额外桌面窗口**(`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`,需按此重排):
|
||
|
||
```html
|
||
<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` 为主):
|
||
|
||
```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 */
|
||
}
|
||
```
|
||
|
||
行本体:
|
||
|
||
```css
|
||
.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 变量:
|
||
|
||
```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`)。
|
||
|
||
背景(两种形态都要):
|
||
|
||
```css
|
||
/* 整块岛背景(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 外边距"。
|
||
|
||
改造:
|
||
|
||
```css
|
||
.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。
|
||
|
||
**改造要求**:
|
||
|
||
1. 把 `LessonReminderContent.vue` 的遮罩部分抽成 `packages/desktop-client/src/island/SlantedMask.vue`(props:`width`、`height`、`open: boolean`、`accent: string`、`motion: boolean`、`speed: number`),`LessonReminderContent` 与新的通知遮罩共用。
|
||
2. 通知遮罩的 DOM 必须与 ClassIsland 对齐:
|
||
|
||
```html
|
||
<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>
|
||
```
|
||
|
||
3. 遮罩填充色 = `var(--ci-accent-fill)`(ClassIsland 用 `AccentFillColorDefaultBrush`);文字色 = `var(--ci-text-on-accent)`。
|
||
4. `prefers-reduced-motion` 或 `appearance.animations === false` 时直接落位(`regions` 全部 0/1),不做逐帧插值(`LessonReminderContent.vue:77-80` 已有该分支,保留)。
|
||
5. **不得**给遮罩层加 `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`):
|
||
|
||
```html
|
||
<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 规格:
|
||
|
||
```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'` 下有意义(纯黑/毛玻璃/主题色渐变),其余情况忽略。
|
||
|
||
落地规则:
|
||
|
||
1. `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`)**保持原样不动**。
|
||
2. `DEFAULT_ISLAND_APPEARANCE` 增加 `family: 'classisland'`(`:265-290`)——这是"新安装默认"。
|
||
3. `normalizeAppearance()` 接受 `family` 白名单(`main/island.ts:2244-2264`),非法值回退 `'classisland'`。
|
||
4. **升级迁移(必须与第 5 部分一致)**:旧配置没有 `family` 字段 → 迁移时写入 `family: 'winisland'`(老用户观感不变);新安装 → `'classisland'`。迁移函数放在 `packages/shared/src/island-layout.ts` 的 `normalizeIslandLayouts()` 里,与 `version` 一并处理。
|
||
5. 设置页拆分(`IslandSettingsView.vue:291-305` 现绑定 `style`):
|
||
- "外观族"下拉绑定 `appearance.family`,**仅在 `layout='fixed'` 时可见**;
|
||
- "视觉风格/色调"下拉仅在 `layout='fixed' && family='winisland'` 时可见;
|
||
- `layout='custom'` 时两者都隐藏(custom 恒 ClassIsland 规格),只保留圆角/行高/行距/字号等 Scheme 级设置;
|
||
- 切换必须即时生效并可回退(走既有 `appearance` 通道,`preload/island.ts:48-49`)。
|
||
6. 主题轴(`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`)。改造要求:
|
||
|
||
```html
|
||
<!-- 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`、gap `font*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,需按下面改写)。
|
||
|
||
```html
|
||
<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`)。
|
||
- 动画:数据刷新时数字变化不做过渡(避免每 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);
|
||
- 空状态占位"今天没有课程。"/"今日课程已全部结束。"。
|
||
- **改造(P1)**:
|
||
|
||
```html
|
||
<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`(ClassIsland `LessonsListBox.axaml:56-58`),过渡 150ms `cubic-bezier(0.645,0.045,0.355,1)`;
|
||
- 分割线:`1×18`、`opacity: 0.25`(现状 `:334-342`),可提到 24px 与 ClassIsland 的 25px 对齐(P2);
|
||
- 课后占位:现有"今日课程已结束"只用于 countdown;schedule 需新增 `今天没有课程。`/`今日课程已全部结束。` 两态(ClassIsland `LessonControlSettings` 默认文案)。
|
||
- 空数据兜底:`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)写入 inline `font-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。
|
||
- 空数据兜底:内容不溢出 → 静止、不抖动。
|
||
- 动画:`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`。
|
||
- 空数据兜底:无消息 → 三个入口都不渲染(`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 独有能力 → **保留现状视觉**,只做两点对齐:
|
||
1. 锚点/边距与岛一致(左/中/右 + `ISLAND_SHADOW_PAD`,`CoursewareTransfer.vue:12-25`),尺寸不随 `style` 变化;
|
||
2. `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` 上):
|
||
|
||
```css
|
||
--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` 的视觉验证里**做桌面像素比对(默认后台验证不显示岛):
|
||
|
||
1. 默认值:新安装 `appearance.family = 'classisland'`;旧配置(无 `family`)升级后 `family = 'winisland'` 且 fixed 观感与升级前一致,不报错、不丢组件数据;设置页可一键切换并即时预览、可回退。
|
||
1.1 **`family` 只影响 fixed**:`layout='custom'` 时切 `family` 不改变任何渲染(custom 恒 ClassIsland 规格),设置页在 custom 下不展示"外观族/视觉风格"两项;`custom` 无 winisland 回退档。
|
||
2. `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 变体。
|
||
3. `family='winisland'` 的 fixed 模式仍是胶囊 + 详情卡:胶囊 268×44、圆角 `h/2`、squircle、纯黑/玻璃/渐变三色调、展开态阴影 `0 2px 3px rgba(0,0,0,.11)`;与改造前逐像素一致。
|
||
4. 岛在 6 个停靠位置、两个显示器、125%/150% DPI 下位置正确:居中停靠不受 `marginX` 影响;左右停靠距屏幕边缘 = `marginX`;底停靠时窗口贴底且岛不被裁。
|
||
5. 窗口几何:开合过程中 `win.getBounds()` **不变化**(用主进程日志或注入断言验证);只有设置变化/不可见时才 `setBounds`;`classisland` fixed 的窗口高恒为 40 + 2×`ISLAND_SHADOW_PAD`。
|
||
6. 行:可见行高 `40px`(像素量测)、行内边距 12、行距 5、圆角 8、描边 1、阴影 `0 4 8 2 #48000000`;相邻组件间距 12;进度条 3px。
|
||
7. 背景不透明度 0.5 只影响背景:文字在截图里为纯白 0.92/0.72 层级,不随背景变透明。
|
||
8. 三条进度条高度都是 3px(课程、课程提醒、通知气泡),且不出现 2px 残留。
|
||
9. `custom` 模式:`rows` 与 `expandedRows` 都按行渲染;`scheme.separated=true` 时每行独立背景胶囊,无整块背景;**custom 渲染恒按 ClassIsland 规格,切换 `family` 不产生任何视觉变化**。
|
||
9.1 **custom 不被课程提醒接管**:prepare / on-class / break / in-class 期间岛仍显示常态行与铃铛(课程进度由 `schedule` 组件呈现),不出现整岛替换;`ending` 只在课程名右侧显示蓝色圆形秒数徽标(30×30、`-N`);不存在"临时接管一行"的行为。
|
||
10. 遮罩:通知/课程提醒共用 `SlantedMask`,5 段从两侧向中间打开(总 480ms)、关闭反向;开启减少动态效果或关闭动画时直接落位。
|
||
11. 触摸屏:手指能点开岛(触摸模式生效),窗口贴合岛体,失焦宽限 1200ms;非触摸屏行为与改造前完全一致。
|
||
12. 鼠标穿透:岛外点击一律落到桌面;命中矩形随形变更新(限流 50ms),形变结束后无"看不见却点不动"的死区。
|
||
13. 11 个组件逐项:`date` 显示 `周三 11/12`、`clock` 显示 `HH:mm`、`weather` 显示 22px 图标 + 温度且无数据时不出现 `NaN`、`countdown` 文案与进度环/条可选、`schedule` 当前项高亮 + 3px 进度 + 已结束 0.6 淡化 + 空占位、`text` 空内容不占宽、`separator` 2×25/0.8、`slide` 15s 切换且可关动画、`rolling` 仅溢出时滚动且 40px/s、`group` 间距 12、`stack` 子项完全重叠。
|
||
14. 消息入口:铃铛数量徽标正确(1–99,>99 显示 `99+`),无消息显示 chevron;`notification/homework/call` 不在组件库出现,但旧方案里的节点仍能渲染。
|
||
15. 课件覆盖:传输中出现课件卡且岛隐藏;锚点与边距正确;`pointer-events: none`,不吞点击。
|
||
16. 编辑器:选中组件为 1.5px 虚线强调框;容器组件为 1.5px 虚线;拖入指示在 before/after/inside 三种位置正确;预览缩放 0.3–1.5 不导致岛变形错位。
|
||
17. `prefers-reduced-motion` 与设置里的"动画"开关都生效:关闭时所有弹簧 `snap()`、滚动静止、遮罩直接落位。
|
||
18. 不得出现:`setBackgroundMaterial`、`backdrop-filter`、第二个 `BrowserWindow`、把窗口尺寸写进 CSS 的做法、在动画帧里 `setBounds`。
|