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

2073 lines
266 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 9. 主题系统(内置主题、主题包格式、壁纸取色、主题预览、自定义颜色)
实现者必须把「主题」拆成**两层独立系统**,不得混为一谈:
1. **应用主题(App Theme)**:控制整个应用(设置中心、窗口、组件库)的明暗与强调色。对应 ClassIsland 的 `IThemeService` / `ThemeService`,只做三件事:切换 ThemeVariant(浅/深/跟随系统)、设置强调色、扩散强调色派生画刷。
2. **主界面主题(Island Theme / XamlTheme)**:只控制「岛」的形状、背景、描边、投影、遮罩动画与排版系数。对应 ClassIsland 的 `IXamlThemeService` + `XamlThemes/*`,它是**可安装、可叠加、可市场分发**的主题包。
两层必须有各自的状态、各自的持久化字段、各自的设置页面分区。**不得**把「换岛皮肤」实现成「换应用 accent」。
### 9.1 应用主题(明暗 + 强调色)
必须实现以下状态字段(与源码一致,便于数据迁移):
| 字段 | 类型 | 默认 | 含义 | 证据 |
|---|---|---|---|---|
| `Theme` | int | `2` | `0=跟随系统`、`1=明亮`、`2=黑暗` | `ClassIsland/Models/Settings.cs:32,979-989` |
| `PrimaryColor` | color | `#00BFFF` | 自定义强调色 | `Settings.cs:35,991-1001` |
| `SecondaryColor` | color | `#7FFFD4` | 次级强调色 | `Settings.cs:36,1003-1012` |
| `ColorSource` | int | `2` | `0=自定义`、`1=壁纸主题色`、`2=跟随系统`、`3=屏幕主题色` | `Settings.cs:83,1014-1023` |
| `TargetLightValue` | double | `0.6` | 取色目标明度 | `Settings.cs:85,1107-1116` |
| `WallpaperColorPlatte` | color[] | `[]` | 壁纸调色板(最多 5 色) | `Settings.cs:1025-1034` |
| `SelectedPlatteIndex` | int | `0` | 当前调色板选择 | `Settings.cs:1041-1050` |
| `IsWallpaperAutoUpdateEnabled` | bool | `false` | 壁纸变化时自动取色 | `Settings.cs:92,1052-1061` |
| `WallpaperAutoUpdateIntervalSeconds` | int | `60` | 自动取色轮询间隔(秒) | `Settings.cs:93,1063-1072` |
| `UseExperimentColorPickingMethod` | bool | `false` | 启用实验性取色算法 | `Settings.cs:1096-1105` |
切换逻辑必须严格实现(`ClassIsland/MainWindow.axaml.cs:743-764` 与 `ClassIsland/Services/ThemeService.cs:43-75`):
1. `themeMode=0` → 主题变体 `Default` 且 `preferSystemTheme=true`(系统变浅即浅、变深即深);
2. `themeMode=1/2` → 显式 `Light`/`Dark`,并 `preferSystemTheme=false`;
3. 强调色:`ColorSource=0` 用 `PrimaryColor`;`1`/`3` 用 `SelectedPlatte`;`2` 传 `null` 表示跟随系统强调色;
4. 强调色变化后**必须重算** `CustomizedAccentBarBackground1Brush` = 强调色 + alpha `0.10`(`ThemeService.cs:66-74`、`Settings.cs:1037-1039`);
5. 应用主题变化必须触发 `ThemeUpdated` 事件,通知所有依赖主题的组件重绘;事件名与语义照抄 `ClassIsland.Core/Abstractions/Services/IThemeService.cs:16`。
CSS 落地要求:
```css
:root { --ci-accent: #00bfff; --ci-accent-soft: color-mix(in srgb, var(--ci-accent) 10%, transparent); }
html.dark { /* 深色下的主题覆盖只允许改「派生值」,强调色本身仍由同一变量驱动 */ }
```
`--ci-accent` 必须由运行时 JS 写入 `document.documentElement.style`(与 ClassHelper 注入动效 token 的方式一致,见 `packages/desktop-client/src/renderer/composables/motion.ts:35-45`),**不得**通过重新生成样式表字符串实现。
### 9.2 内置主界面主题
必须内置且仅内置两套主界面主题,id 与名称不得更改(主题包 id 是持久化键):
| id | 名称 | 描述 | Banner(预览图) | 竖直安全区 | 证据 |
|---|---|---|---|---|---|
| `classisland.classic` | 经典 | ClassIsland 的经典外观。 | `avares://ClassIsland/Assets/XamlThemePreviews/classisland.classic.png` | `0` | `ClassIsland/App.Services.xaml.cs:297-305` |
| `classisland.fluent` | Fluent | 焕然一新的 ClassIsland 外观。 | `avares://ClassIsland/Assets/XamlThemePreviews/classisland.fluent.png` | `20` | `App.Services.xaml.cs:306-315` |
- 两套主题的作者均为 `ClassIsland`,URL 均为 `https://github.com/ClassIsland/ClassIsland`。
- 两张预览图均为 **1920×350 PNG**(实测文件头,`Assets/XamlThemePreviews/`),实现时必须按 1920×350 的比例裁切展示,不得拉伸变形。
- 默认启用列表必须只包含 `classisland.fluent`(`Services/XamlThemeService.cs:81-84`)。
- 首次启动写入 `EnabledThemes.json`(`XamlThemeService.cs:48`),内容为字符串数组,顺序即加载优先级。
#### 9.2.1 Fluent 与 Classic 的差异(逐项实现,不得混用)
| 差异项 | Fluent 主题 | Classic 主题 | 证据 |
|---|---|---|---|
| 行背景画刷 | `SolidBackgroundFillColorSecondaryBrush` | `SolidBackgroundFillColorBaseBrush` | `XamlThemes/FluentTheme/Styles.axaml:45`;`ClassicTheme/Styles.axaml:214` |
| 行背景描边 | `ControlElevationBorderBrush`,1px | 无显式描边(依赖背景色与投影) | `FluentTheme:46-47` |
| 行投影 | `BoxShadow 0 4 8 2 #48000000` | 常态无投影(`DropShadowEffect` Opacity=0),提醒时升为 `BlurRadius=8, Opacity=1` | `FluentTheme:49`;`ClassicTheme:36-45,283-287` |
| 圆角来源 | `MainWindowStylesAssist.CornerRadius / 1.0` | 无圆角(矩形裁剪) | `FluentTheme:48`;`ClassicTheme:46-59` |
| 背景宽度动画 | `300ms`,`cubic-bezier(0.65, 0, 0.35, 1.0)` | `550ms`,`cubic-bezier(0.00, 1.00, 0.50, 1.10)`(**带过冲**) | `FluentTheme:269`;`ClassicTheme:207` |
| 提醒遮罩实现 | 自绘斜切分块(`SlantedMaskControl`)+ 文字缩放 + 投影淡入 | 整块下压/上滑(`TranslateTransform Y=-50 → 0` / `0 → 50`) | `FluentTheme:175-197`;`ClassicTheme:78-91,142-154` |
| 提醒遮罩参数 | `RegionDurationMs=360`、`StageStaggerMs=120`;文字 250ms 延迟 260ms;缩放 1.1→1.0 用时 750ms 延迟 260ms;投影 100ms 延迟 600ms | 遮罩下压 200ms(QuadraticEaseIn);文字 100ms 延迟 200ms;退出 200ms 延迟 100ms + 文字 100ms | 见 part 14 表 #10–#14 |
| 分体主界面(行独立成岛) | 支持:`IsIslandSeperated=True` 时每行独立背景、独立 `border-radius`,组件左右外扩 `Margin="-2 0"`、最小宽 `40`、内容 `Margin="16 0"` | 不支持分体(无对应样式) | `FluentTheme:245-259`;`Settings.cs:1287-1296` |
| 内容裁剪 | 用 `clip-host` + `WidthDoubleToRectConverter` 生成按圆角的 Rect 裁剪 | 用 `Grid.Clip` 矩形裁剪,无圆角 | `FluentTheme:21-38`;`ClassicTheme:46-59` |
| `IslandContainerHeight` | `40` | `40` | `FluentTheme:65`;`ClassicTheme:16` |
| 未启用提醒时的行模板 | 最小模板(无遮罩层、无进度条) | 最小模板(同样精简) | `FluentTheme:203-232,275-278`;`ClassicTheme:158-196,219-222` |
**验收硬要求**:在两种主题下分别触发同一条提醒,动画形态必须明显不同(Fluent 为「斜切分块扫入 + 文字缩放」,Classic 为「整块下压 + 上滑退出」),且时长与上表逐毫秒一致。
### 9.3 主题包格式(必须实现安装/卸载/打包/市场)
#### 9.3.1 目录与文件
| 路径 | 内容 | 证据 |
|---|---|---|
| `<配置目录>/Themes/<主题id>/` | 已安装主题目录(每主题一个文件夹) | `Services/XamlThemeService.cs:47,440-447` |
| `<配置目录>/Themes/<id>/manifest.yml` | 主题清单(必需,缺失时回退为「目录名即 id/name」) | `XamlThemeService.cs:249-279` |
| `<配置目录>/Themes/<id>/Styles.axaml` | 主题样式入口(必需) | `XamlThemeService.cs:117-120` |
| `<配置目录>/Themes/<id>/banner.png`(或 manifest 指定) | 预览图 | `XamlThemeService.cs:271` |
| `<配置目录>/Themes/<id>/.uninstall` | 卸载标记(存在即下次启动删除整个目录) | `XamlThemeService.cs:413-423`;`ThemeInfo.cs:IsUninstalling` |
| `<配置目录>/EnabledThemes.json` | 启用主题 id 数组,顺序=加载顺序 | `XamlThemeService.cs:48,63,80-86` |
| `<缓存目录>/ThemePackages/<id>.zip` | 下载完成的主题包缓存,启动时自动安装 | `XamlThemeService.cs:49,390-411,426-450` |
| `<插件市场索引目录>/<源名>/themes.json` | 市场主题索引 | `XamlThemeService.cs:171-220` |
#### 9.3.2 `manifest.yml` 字段(逐字段实现,camelCase)
| 字段 | 类型 | 默认 | 必填 | 说明 | 证据 |
|---|---|---|---|---|---|
| `id` | string | `""` | 是 | 主题唯一 id(建议 `作者.主题`) | `ClassIsland.Core/Models/XamlTheme/ThemeManifest.cs` |
| `name` | string | `""` | 是 | 显示名 | 同上 |
| `description` | string | `""` | 否 | 一句话描述 | 同上 |
| `version` | string | `"0.0.0.0"` | 否 | 语义化版本,用于市场更新判断 | 同上;`XamlThemeService.cs:201-206` |
| `author` | string | `""` | 否 | 作者 | 同上 |
| `url` | string | `""` | 否 | 项目地址 | 同上 |
| `banner` | string | `"banner.png"` | 否 | 预览图相对路径 | 同上 |
| `verticalSafeAreaPx` | double | `0` | 否 | 竖直安全区像素 | 同上 |
市场索引 `themes.json` 的结构必须为:
```json
{ "themes": [ { "manifest": { ...上表字段... },
"downloadUrl": "https://.../{root}/xxx.zip",
"downloadMd5": "……" } ] }
```
其中 `downloadUrl` 必须支持 `{root}` 占位符,由所选镜像替换(`XamlThemeService.cs:194,213`);下载后必须校验 MD5 再落盘(`XamlThemeService.cs:345-351`)。
#### 9.3.3 加载与覆盖规则
1. 启动时先注册内置主题(`IXamlThemeService.IntegratedThemes`,`ClassIsland.Core/Abstractions/Services/IXamlThemeService.cs:21`),再扫描 `Themes/` 目录加载外部主题;
2. 只加载 `EnabledThemes` 中列出的主题,且**按数组顺序依次叠加**:后加载主题覆盖前面主题的同名样式(`IXamlThemeService.cs:63-66`);
3. 任一主题样式加载失败必须**隔离**:标记 `IsError`、记录异常、继续加载后续主题,不得导致应用崩溃(`XamlThemeService.cs:112-133`);
4. 主题的 `VerticalSafeAreaPx` 取所有已启用主题的**最大值**(不是求和)(`XamlThemeService.cs:111-135`);
5. 主题安装/卸载/换序后必须提示「重启以应用更改」(`XamlThemeService.cs:232-236,369-374`);
6. 安全模式(`--safe`)下不得加载任何主题(`XamlThemeService.cs:87-90,101-104`)。
#### 9.3.4 打包
`PackageThemeAsync(id, outputPath)` 必须把主题目录整体 zip 成包,且不得在包内包含 `.uninstall`(`XamlThemeService.cs:452-466`)。实现时对应一个「导出主题包」命令。
### 9.4 主题包在 ClassHelper 栈中的落地格式(CSS 版)
由于实现栈是 Web 技术,主题包不再执行 XAML,必须改为「**样式清单 + 受控 CSS**」。实现者必须定义如下主题包结构(这是本节的核心交付物):
```
<主题id>/
manifest.yml(与 9.3.2 完全一致,保持兼容)
theme.json ← 新增,主题变量清单
theme.css ← 新增,受控 CSS(只允许声明 CSS 变量与白名单选择器)
banner.png(1920×350)
```
`theme.json` 必须包含:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `schemaVersion` | int | 是 | 固定 `1` |
| `id` / `name` / `version` / `author` / `description` | string | 是 | 与 manifest 一致(双写校验) |
| `verticalSafeAreaPx` | number | 否 | 与 manifest 一致 |
| `tokens` | object | 是 | `{ "island.bg": "#202020cc", "island.radius": "8px", ... }`,键名必须来自 9.4.1 白名单 |
| `styles` | string[] | 否 | 允许的主题 CSS 选择器 id(见 9.4.2),空表示只用 token |
| `fonts` | object | 否 | `{ "islandFamily": "...", "weight": 500 }` |
| `assets` | string[] | 否 | 主题自带资源相对路径 |
#### 9.4.1 Token 白名单(主题只能改这些,改别的无效)
必须允许覆盖:`--ci-accent`(仅当主题声明 `allowAccentOverride: true`,默认 false,防止主题抢走用户强调色)、`--ci-island-bg`、`--ci-island-bg-opacity`、`--ci-island-border-color/width`、`--ci-island-radius`、`--ci-island-shadow`、`--ci-island-font`、`--ci-island-fs-*`、`--ci-island-line-gap`、`--ci-island-pad-x`、`--ci-island-mask-*`、`--ci-island-progress-*`、`--ci-island-fade-opacity`。
必须禁止覆盖:`--ci-text-1..5`(由可访问性/自定义前景色决定)、`--ci-z-*`、`--ci-island-height`(固定 40,改它会破坏窗口几何)、`--ci-scale`、以及任何 `--ch-*` 应用外壳 token。
#### 9.4.2 受控选择器(styles 白名单)
允许:`.:island-root`、`.island-card`、`.island-background`、`.island-content`、`.island-overlay`、`.island-mask`、`.island-progress`、`.island-component`、`.island-separator`。禁止:`*`、`html`、`body`、`#app`、`[class*=...]`、任何 `@import`、任何 `url(http…)`、任何 `position: fixed`、任何 `z-index`。加载时必须用 CSSOM 解析并逐条校验,不合格条目丢弃并记录警告,不得整包拒绝(隔离原则同 9.3.3 第 3 条)。
### 9.5 XAML 主题 → CSS 主题的映射规则
实现者必须按下表做等价映射;遇到不在表内的 XAML 特性,先实现等价 CSS 再补文档,**不得**用「运行一段脚本改 DOM」绕过。
| ClassIsland(Avalonia) | 实现(Web) | 说明 |
|---|---|---|
| `ResourceDictionary` + `x:Key` | CSS 变量 + `theme.json.tokens` | 资源键名转 kebab-case 并加 `ci-` 前缀 |
| `DynamicResource Key` | `var(--ci-…)` | 运行时可变;`StaticResource` 则写成固定值 |
| `Style Selector="controls|MainWindowLine"` | `.island-line` | 控件类名 → 固定类名 |
| `Style Selector="^:mask-in"`(伪类) | `.island-line.is-mask-in` | 伪类 → 状态类 |
| `Style.Animations` + `KeyFrame` | `@keyframes` + `animation` | 时长/曲线/延迟逐值迁移(见 part 14) |
| `Transitions` + `DoubleTransition` | `transition` | 属性名映射见下 |
| `DoubleTransition Property="BackgroundWidth"` | `transition: width` / `inline-size` | 源码动画的是背景层宽度 |
| `DoubleTransition Property="Opacity"` | `transition: opacity` | — |
| `BoxShadowsTransition Property="BoxShadow"` | `transition: box-shadow` | — |
| `BrushTransition Property="Background"` | `transition: background-color` | — |
| `Easing="0.65, 0, 0.35, 1.0"` | `cubic-bezier(.65,0,.35,1)` | 逐值照抄 |
| `Easing="SineEaseIn"` | `cubic-bezier(.12,0,.39,0)`【推断:按 Avalonia SineEaseIn 定义换算】 | 或 `ease-in` 近似 |
| `QuadraticEaseIn/Out` | `cubic-bezier(.11,0,.5,0)` / `cubic-bezier(.5,1,.89,1)`【推断】 | 按 Avalonia 定义换算 |
| `ExponentialEase EaseOut` | `cubic-bezier(.16,1,.3,1)` 近似 | 用于闪屏/OOBE |
| `FillMode="Both"` | `animation-fill-mode: both` | 必须保留,否则延迟期会闪终态 |
| `IterationCount="Infinite"` | `animation-iteration-count: infinite` | — |
| `PlaybackDirection="Alternate"` | `animation-direction: alternate` | — |
| `Clip` + `WidthDoubleToRectConverter` | `border-radius` + `overflow:hidden` 的内层 | 圆角裁剪 |
| `OpacityMask` 渐变 | `mask-image: linear-gradient(...)` | 主题卡底部渐隐 |
| `DropShadowEffect` | `filter: drop-shadow(...)`(仅限非卡片元素) | 卡片禁止用 filter(会闪帧) |
| `AcrylicBlur` | `backdrop-filter: blur(30px) saturate(1.2)`【推断】 | 只用于编辑模式整窗底 |
| 加载顺序覆盖 | 同优先级下「后插入的 `<style>` 生效」 | 必须用一个 `<style id="ci-theme-runtime">` 承载全部主题变量,按数组顺序覆盖 |
映射的硬性边界(源码行为决定的):
- `IsIslandSeperated` 是 Fluent 主题专属能力:主题包只有在 `theme.json.capabilities` 里声明 `"separatedIsland": true` 时,才允许显示「分体主界面」开关(`ThemesSettingsPage.axaml:76-84` 的提示文案即此含义)。
- 主题只能改「主界面」,**绝不能**影响设置中心视觉;设置中心由应用主题控制。
- 主题的 `VerticalSafeAreaPx` 会直接改变窗口位置,必须参与窗口几何计算(见 part 04)。
### 9.6 壁纸/屏幕取色
必须实现「强调色来源」的完整算法,否则 `ColorSource=1/3` 形同虚设。源码中的取色实现目前位于 `ClassIsland/Services/WallpaperPickingService.cs`,但**整个文件被 `#if false` 包裹**(第 1 行 `#if false`、第 379 行 `#endif`),且依赖注入被注释(`App.Services.xaml.cs:62`)、调用点被注释(`App.axaml.cs:762`、`AppearanceSettingsPage.axaml.cs:32-33,53`)。因此:
- 现状 = **功能被停用**,只剩 `MainWindow.axaml.cs:744-763` 的分支读取 `SelectedPlatte`;
- 更强的证据:设置页「主题色来源」下拉里,`系统壁纸` 与 `屏幕主题色` 两项被写死为 `IsVisible="False" IsEnabled="False"`,用户根本无法选中(`Views/SettingPages/AppearanceSettingsPage.axaml:146-158`);
- 实现要求 = **按此算法重新实现为可选功能**(默认关闭),并把「壁纸取色」在设置页标注为实验性;若实现者判断风险过高,可只实现 `ColorSource=0(自定义)` 与 `2(跟随系统)` 两项,并在文档中明确声明 `1`/`3` 不支持;
- 本节算法细节依据 dead code,标注【依据停用实现】。
#### 9.6.1 取色流程
1. 触发时机:注册表 `Control Panel\Desktop` 变更、系统 `UserPreferenceChanged(Desktop)`、或定时器(间隔 = `WallpaperAutoUpdateIntervalSeconds`,默认 60s)(`WallpaperPickingService.cs:45-51,91-94,117-128,136-162`);
2. 取图:`ColorSource=3` 直接截取「岛所在显示器」的全屏;否则截取桌面窗口(类名默认 `Progman`,可被 `WallpaperClassName` 覆盖);兼容模式则直接读注册表 `WallPaper` 路径并等比缩放(`WallpaperPickingService.cs:164-222,253-261`);
3. 缩放:图片宽度统一转换(`ConvertToBitmapImage(bitmap, bitmap.Width)`,`WallpaperPickingService.cs:278`);
4. 主算法(旧):八叉树量化(`ClassIsland/ColorOctTreeNode.cs`)+ HSV 评分;
5. 备选算法(实验):`AccentColorHelper.GetAccentColor(bytes, w, h)`(`ClassIsland/Helpers/AccentColorHelper.cs`,注明来自 Starward 项目);
6. 结果:取评分前 **5** 色写入 `WallpaperColorPlatte`,并把 `SelectedPlatteIndex` 重置为 `0`(`WallpaperPickingService.cs:295-307,337-341`);
7. 失败兜底:`SelectedPlatte` 返回 `DodgerBlue`(`Settings.cs:1037-1039`)。
#### 9.6.2 八叉树评分公式(必须逐项实现)
```text
score(c) = (S + V × (-(V - right) × (V - left) × 4)) × log2(count)
其中 right = TargetLightValue - 0.5,left = TargetLightValue + 0.5
S = HSV 饱和度(0..1),V = HSV 明度(0..1),count = 该颜色像素数
```
排序规则:先按 `score` 降序,再按像素数 `count` 降序(`WallpaperPickingService.cs:326-336`)。HSV 转换公式(`WallpaperPickingService.cs:55-63`):
```text
max = max(R,G,B)/255, min = min(R,G,B)/255
S = max == 0 ? 0 : 1 - min/max
V = max
H 不参与评分(源码未计算)
```
实现要点:
- `TargetLightValue` 默认 `0.6`,即偏好「中等偏亮」的颜色;UI 上必须暴露该滑杆(0–1)。
- 必须是纯函数、可单测:输入 RGBA 像素数组与宽高,输出最多 5 个颜色,顺序稳定。
- 取色必须放在 Web Worker 或主进程执行,**不得**阻塞渲染线程(图片可能 4K)。
#### 9.6.3 取色 UI
必须实现「壁纸预览 + 调色板选择」窗口:展示当前壁纸截图与 5 个候选色块,点击某个色块即把 `SelectedPlatteIndex` 设为该索引并实时应用强调色。源码窗口 `ClassIsland/Views/WallpaperPreviewWindow.xaml(.cs)` 仍存在(构造函数注入 `WallpaperPickingService` 并调用 `GetWallpaperAsync()`,`WallpaperPreviewWindow.xaml.cs:24-40`),可作为布局参考。
### 9.7 自定义颜色(用户手动覆盖)
必须实现「全局」与「单行」两级覆盖,优先级:单行 > 全局 > 主题默认。
| 设置 | 全局字段 | 行级字段 | 默认 | 证据 |
|---|---|---|---|---|
| 自定义背景色开关 | `IsCustomBackgroundColorEnabled` | `IsCustomBackgroundColorEnabled` | `false` | `Settings.cs:33,1118-1127`;`MainWindowLineSettings.cs:39` |
| 背景色 | `BackgroundColor` | `BackgroundColor` | `#000000` | `Settings.cs:34,1129-1138`;`MainWindowLineSettings.cs:38` |
| 背景不透明度 | `Opacity` | `BackgroundOpacity` + 开关 | `0.5` / `0.5` | `Settings.cs:64`;`MainWindowLineSettings.cs:36-37` |
| 自定义前景色开关 | `IsCustomForegroundColorEnabled` | 同名 | `false` | `Settings.cs:1253-1262`;`MainWindowLineSettings.cs:34` |
| 前景色 | `CustomForegroundColor` | `ForegroundColor` | `DodgerBlue`=`#1E90FF` | `Settings.cs:203,1264-1273`;`MainWindowLineSettings.cs:35` |
| 圆角 | `RadiusX/Y` | `CustomCornerRadius` + 开关 | `8` / `0` | `Settings.cs:183-184`;`MainWindowLineSettings.cs:40-41` |
| 字号 4 档 | 4 个字段 | 同名 + `IsResourceOverridingEnabled` | 14/16/18/20 | `Settings.cs:197-200`;`MainWindowLineSettings.cs:29-33` |
| 行不透明度 | — | `Opacity` | `1.0` | `MainWindowLineSettings.cs:42` |
实现要求:
- 自定义背景色必须作用在「背景层」,不得作用在内容层,否则文字会被一起染色(`FluentTheme/Styles.axaml:51-56`)。
- 自定义前景色必须按 part 03 的 5 级 alpha 扩散,且要覆盖组件内部的 `Foreground` 与 `TextElement.Foreground` 两处。
- 行级覆盖必须使用「内联 CSS 变量 + 局部 class」,关闭时移除内联变量,与源码 `ClearValue` 语义一致(`MainWindowCustomizableNodeHelper.cs:39-67`)。
### 9.8 主题预览卡片(主题市场 UI)
主题设置页必须按以下规格实现(源码 `ClassIsland/Views/SettingPages/ThemesSettingsPage.axaml`):
- 顶部 `InfoBar`:提示「启用支持的主题(如 Fluent)后可在【外观】开启【分体主界面】」;带「前往【外观】设置」按钮;可关闭并持久化(`ThemesSettingsPage.axaml:76-84`)。
- 分类 TabStrip:`市场` / `已安装` 两个标签,紧凑样式(`compact`,字号 14、字重 Medium、最小高 28)(`ThemesSettingsPage.axaml:86-92`;`StylesBase.axaml:129-142`)。
- 工具栏:`刷新市场`、`重载主题`、`打开主题文件夹…`、`主题加载顺序`(打开右侧 Drawer,可拖拽排序)(`ThemesSettingsPage.axaml:94-111,35-67`)。
- 网格:横向 WrapPanel,单元高 `150`、目标宽 `328`、间距 `2`(`ThemesSettingsPage.axaml:137-146`)。
- 卡片:圆角 `4`、1px 描边、`overflow:hidden`;内容为 Banner(`UniformToFill` 铺满)+ 底部渐变遮罩(`#00ffffff`→0.65→`#00ffffff`→1.0→`#ffffffff`)+ 名称(16px Medium)+ 作者(12px)+ 操作区(`ThemesSettingsPage.axaml:147-172`)。
- 操作区按钮:28×28、圆角 14:错误(红色图标)、更多(打开文件夹/打包/卸载/撤销卸载)、安装、更新、重启以应用、启用开关(`ThemesSettingsPage.axaml:206-299`)。
- 首次进入主题页必须显示**安全警告**(主题可能执行任意代码),用户点击「同意并开始使用主题」后才展示列表;警告状态持久化(`IsThemeWarningVisible`,`Settings.cs:2391`;`ThemesSettingsPage.axaml:309-366`)。
- 未加载/加载失败的主题卡片必须 `opacity:.8` 并显示错误入口(`ThemesSettingsPage.axaml:176-185,208-213`)。
### 9.9 ClassHelper 侧映射
| ClassIsland 概念 | ClassHelper 落点 | 要求 |
|---|---|---|
| 应用主题 store | `packages/desktop-client/src/renderer/stores/ui.ts`(既有主题切换) | 增加 `accentSource`、`primaryColor`、`wallpaperPalette` 状态 |
| 主界面主题 store | 新增 `src/island/theme.ts` + `config.json` 字段 | 结构:`{ enabled: string[], active: string }`,与 `EnabledThemes.json` 兼容 |
| 主题包加载 | 主进程读 `themes/<id>/{manifest.yml,theme.json,theme.css}` | 用 `js-yaml` 解析 manifest;CSS 用 CSSOM 白名单校验后注入 `<style>` |
| 岛视觉 token | `classic-island.css` 的 `--wn-*`(黑/玻璃/着色三套预设,`classic-island.css:60-82`)+ `custom-island.css` 的 `--custom-color`/`--custom-radius` | 主题直接覆盖这些变量 |
| 主题预览 | 复用 `packages/ui` 的卡片组件 | 图片走 `AsyncImage` 等价组件(懒加载 + 占位 shimmer) |
| 主题切换动效 | `composables/motion.ts` 的 `useThemeReveal()` | 保持「圆形扩散」400–480ms;`prefers-reduced-motion` 与页面不可见时必须同步切换(`motion.ts:201-237`) |
| 持久化 | 本地 JSON 文件(主进程) | 禁止引入服务端依赖(ClassIsland 是本地单机应用) |
### 9.10 外观设置页(主题相关控件的精确规格)
「外观」设置页是应用主题的唯一入口,必须逐控件实现(源码 `ClassIsland/Views/SettingPages/AppearanceSettingsPage.axaml`)。控件全部使用 `SettingsExpander`(左侧图标 + 标题 + 描述,右侧控件),容器为 `StackPanel.settings-container`(`margin 12 12 18 12`、`max-width 960`、`spacing 4`),整页带 `.animated-intro` 错峰入场(`StylesBase.axaml:67-71`;`StackPanelIntroAnimation.axaml`)。
| 分组 | 控件 | 类型 | 范围/选项 | 默认 | 绑定字段 | 证据 |
|---|---|---|---|---|---|---|
| 基本 | 界面缩放 | Slider 180px + 数值气泡(F2) | 0.75–2.5,步进 0.05 | 1.0 | `Scale` | `AppearanceSettingsPage.axaml:27-39` |
| 基本 | 背景颜色 | ColorPicker + ToggleSwitch | 任意颜色 | `#000000`,开关关 | `BackgroundColor` / `IsCustomBackgroundColorEnabled` | `:43-58` |
| 基本 | 背景不透明度 | Slider 180px(F2) | 0–1,步进 0.05 | 0.5 | `Opacity` | `:61-72` |
| 基本 | 圆角半径 | Slider 180px(F1) | 0–20,步进 0.5 | 8 | `RadiusX` | `:75-87` |
| 基本 | 行间隔 | Slider 130px(F1)+ NumericUpDown 125px | 0–150,步进 5 | 5 | `MainWindowLineVerticalMargin` | `:90-109` |
| 基本 | 分体主界面 | ToggleSwitch | 开/关 | 关 | `IsIslandSeperated` | `:112-118` |
| 主题 | 应用主题 | ComboBox(跟随系统/明亮/黑暗) | 3 项,默认索引 2 | 黑暗 | `Theme` | `:123-135` |
| 主题 | 主题色来源 | ComboBox(自定义/系统壁纸/系统/屏幕主题色) | 后两项禁用隐藏,见 9.6 | 系统 | `ColorSource` | `:138-160` |
| 主题 | 强调色 | ColorPicker(仅 `ColorSource=0` 时可见) | 任意颜色 | `#00BFFF` | `PrimaryColor` | `:165-180` |
| 字体 | 自定义字体 | ComboBox(系统字体列表,单项高 32、下拉宽 280) | 系统字体或默认 | HarmonyOS Sans SC(默认) | `MainWindowFont` | `:405-448` |
| 字体 | 自定义字重 | ComboBox(Thin…ExtraBlack 共 11 档) | 100–900 | Medium(500) | `MainWindowFontWeight2` | `:449-480` |
| 字体 | 字体大小 | 4 × NumberBox(后缀 px,SpinButton 内联) | 1–40 | 14/16/18/20 | 4 个字号字段 | `:482-533` |
| 字体 | 字体颜色 | ColorPicker + ToggleSwitch | 任意颜色 | DodgerBlue,开关关 | `CustomForegroundColor` / `IsCustomForegroundColorEnabled` | `:536-551` |
| 字体 | 字体预览 / 字号预览 | 两个 Tab | 只读文本框 | — | — | `:553-592` |
实现硬要求:
- 任何设置改变必须**立即生效**,不得要求重启(主题包除外,见 9.3.3 第 5 条)。
- 颜色选择器必须支持「输入 HEX + 取色器 + 最近使用」,且透明度与 RGB 分开控制(源码用 Avalonia `ColorPicker`,Web 端可用同类组件)。
- 字号/缩放/圆角/不透明度四个滑块必须显示数值气泡(源码 `SliderDragTooltipAssist`,`StylesBase.axaml:178-191`),且开启 `IsSnapToTickEnabled`。
- 「强调色」控件必须跟随 `ColorSource` 显隐;不得在跟随系统时仍显示一个无效的颜色选择器。
- 字体列表必须包含「HarmonyOS Sans SC(默认)」这一项,且该项显示中文名而非字体内部名(`AppearanceSettingsPage.axaml:425-445`)。
### 9.11 主题调试、兼容性与安全边界
1. **调试入口**:开发者菜单提供「重载主题」(`ThemesSettingsPage.axaml:99-101`),必须能在不重启应用的情况下重新加载全部已启用主题的变量与样式;加载失败主题的错误信息必须可从卡片上的错误按钮打开抽屉查看完整堆栈(`ThemesSettingsPage.axaml:24-30,206-213`)。
2. **版本兼容**:`ThemeManifest.version` 必须能被语义化解析;市场比较使用 `Version.TryParse`(`XamlThemeService.cs:201-206`)。主题包必须声明 `minAppVersion`(新增字段,默认 `"0.0.0"`),低于该版本时禁用安装并提示。
3. **安全边界**:主题不得访问文件系统、网络与 Node API;在 Electron 下必须运行在「只读 CSS/JSON」模型中,禁止 `eval`、`new Function`、内联 `on*` 事件(与 ClassHelper preload 白名单纪律一致:`ClassHelper/AGENTS.md` 第 4 节「preload 仅暴露 contextBridge 白名单」)。违规主题必须整包拒绝安装,而不是降级运行。
4. **主题与提醒/组件的耦合**:主题可以改遮罩的颜色与时长,但**不得**改动提醒的排队与优先级逻辑;可以改组件容器的圆角与间距,但不得改组件内部的业务渲染。实现时把「主题变量」与「组件状态」严格分层。
5. **性能底线**:主题切换必须在 100ms 内完成变量注入;主题包 CSS 总大小上限建议 64KB,超过时提示;加载 20 个主题的总耗时不得超过 200ms(解析 + 校验)。
6. **降级**:若主题声明的 token 非法(如把圆角写成 `-8px`、把时长写成 `0ms`),必须回落到默认值并记录警告,不得静默忽略整份主题。
### 9.12 验收标准
1. **双层隔离**:切换主界面主题时,设置中心颜色/圆角/字体**完全不动**;切换应用明暗时,岛主题的圆角/描边样式**完全不动**。
2. **内置主题**:列表恰好 2 个内置主题(Fluent / 经典),Banner 比例 1920×350,Fluent 安全区为 20、经典为 0;默认只启用 Fluent。
3. **主题包安装**:把 zip 放入 `ThemePackages` 后重启,主题出现在「已安装」;`manifest.yml` 缺失时用目录名兜底;`Styles`/`theme.css` 语法错误时该主题标记为错误且其他主题照常工作。
4. **加载顺序**:拖动排序后 `EnabledThemes.json` 顺序变化,重启后叠加效果随顺序变化(后加载覆盖先加载)。
5. **卸载**:点「卸载」写入 `.uninstall` 并置灰,重启后目录被删除;「撤销卸载」可恢复。
6. **安全**:未同意安全警告前看不到主题列表;含 `@import`/`url(http…)`/`position:fixed`/`z-index` 的主题 CSS 被逐条拒绝且记录警告。
7. **壁纸取色**:给定一张固定测试图,算法稳定输出 5 色且顺序可复现;把 `TargetLightValue` 从 0.6 调到 0.2/0.9,首选色亮度相应变化;取色过程中 UI 不卡顿(主线程帧耗时 < 16ms)。
8. **自定义颜色优先级**:同时设置全局背景色与行级背景色,行级生效;关闭行级开关后回落全局;再关闭全局后回落主题。
9. **分体主界面**:Fluent 下开启分体后每行独立成岛、圆角独立、行间出现间距;经典主题下该开关不可用或无效并有说明。
10. **市场更新**:本地主题版本低于市场版本时出现「更新」按钮;下载完成后提示重启;MD5 不匹配时安装失败并报错。
11. **主题安全区**:启用 Fluent 后岛与屏幕边缘出现 20px 竖直安全区;同时启用多个主题时取最大值而非累加。
12. **不阻塞**:主题切换/加载不得造成超过 1 帧(16.7ms)的主线程长任务;大规模图片解码必须异步。
---
## 10. 设置中心
本节规定「应用设置」窗口(ClassIsland 对应 `SettingsWindowNew`)的全部实现细节。实现者必须把它实现为 ClassHelper desktop-client 渲染进程中的一个独立 BrowserWindow(Electron `BrowserWindow`,URL 固定为 `#/settings`),窗口内是 Vue 3 + Vue Router + Element Plus 的单页应用;所有页面、控件、默认值、禁用条件、生效时机必须逐项对齐本节的规格。所有设置项必须落到一个 Pinia store(store 名固定为 `useSettingsStore`),字段名沿用本节给出的 JSON 字段名(即 ClassIsland `Settings` 的属性名),以便与主界面(岛)共享。
### 10.1 设置窗口外壳(必须逐像素对齐)
1. **窗口参数**。必须创建宽度 `1240`、高度 `550`、最小宽度 `560` 的 BrowserWindow;`minWidth: 560`,`width: 1240`,`height: 550`,标题为「应用设置」,图标使用 `AppLogo.ico`,`show: false` 首帧就绪后再 `show()`(对应 `repos/ClassIsland/ClassIsland/Views/SettingsWindowNew.axaml:16-22`)。
2. **窗口样式**。必须无系统标题栏(`frame: false` / `titleBarStyle: 'hidden'`),启用 Mica/Acrylic 类背景(Windows 11 21H2+ 且 WinUI Composition 可用时才启用 Mica,其余平台退回纯色背景;证据 `repos/ClassIsland/ClassIsland.Core/Controls/MyWindow.cs:215-229`)。Electron 中不得调用会破坏逐像素透明与 Mica 的 `setBackgroundMaterial()`,改用 `backgroundMaterial: 'mica'`(Windows 11)或窗口样式类实现;不得同时叠加两层不透明背景。
3. **自定义标题栏**。高度必须为 `48px`,必须 `-webkit-app-region: drag`(对应 `viewModel.TitleBar.Height = 48`,`repos/ClassIsland/ClassIsland/Views/SettingsWindowNew.axaml.cs:135`),其内部所有按钮必须 `-webkit-app-region: no-drag`。标题栏从左到右必须依次为:
- 返回按钮:宽 `48px`、高 `40px`、`margin: 0 -8px`,tooltip「返回」,仅在路由可以后退时显示(`:127-136`);
- 导航窗格切换按钮:宽 `48px`,`margin: 0 -8px -8px`,仅在 NavigationView 折叠为 minimal 模式时显示(`:137-144`);
- 图标:`18×18`,`margin: 12px 4px`,`pointer-events: none`(`:146-153`);
- 标题文字:`font-size: 12px`,垂直居中,`pointer-events: none`(`:155-159`);
- 右侧区,间距 `8px`:需要重启按钮(`accent` 样式,tooltip「部分设置需要重启才能生效,点击以重启应用。」,文案「需要重启」,带重启图标)、版本号文本(`font-size: 14px`,次级前景色,tooltip「应用版本」)、「更多选项…」透明图标按钮(`:162-250`);
- 窗口控制按钮预留位:右侧必须保留 `140px` 空白区域(`:252`);在 macOS 上左侧还需预留 `60px` 红绿灯空位(`:120-126`)。
4. **「更多选项…」菜单**必须按以下顺序与文案渲染(`:176-249`):
1. 实验性功能…(图标 `E508`)→ 打开宽度 `332px` 的右侧抽屉;
2. 调试窗口规则…(`F485`)→ 打开 `WindowRuleDebugWindow`;
3. 创建快捷方式…(`E0B1`,仅 Windows/Linux,Windows/macOS 分支按源码 `OnPlatform`)→ 子菜单:创建桌面快捷方式(`E62F`)、创建开始菜单快捷方式(`E06F`)、创建快捷换课图标(`E15F`,仅 Windows);
4. 数据迁移…(`E083`)→ 打开 `DataTransferWindow` 模态对话框;
5. 分隔线;
6. 集控选项…(`E2B4`)→ 跳转 `classisland://app/settings/management?ci_keepHistory=true`;
7. 加入管理…(`E2B6`,未启用集控时显示)、退出管理…(`E2B8`,已启用时显示);
8. 分隔线;
9. 日志…(`E510`)→ 打开 `AppLogsWindow`;
10. 导出诊断数据(`E601`)→ 先弹确认对话框,再保存 `.zip`;
11. 重启到恢复模式(`F4C5`)→ 需「退出应用」授权等级;
12. 分隔线;
13. 打开日志目录…(`E889`)、打开数据目录…(`E881`)、打开应用目录…(`E885`)。
5. **打开行为**。必须单例:首次打开前必须校验「编辑设置」授权等级(`ManagementService.CredentialConfig.EditSettingsAuthorizeLevel`,默认「无」即不校验),校验失败直接返回、不显示窗口(`SettingsWindowNew.axaml.cs:458-480`)。已打开时若最小化则还原并 `focus()`。必须支持从 URI `classisland://app/settings/<pageId>[?ci_keepHistory=true]` 直接打开指定页(`OpenUri`,`:490-499`)。
6. **首帧遮罩**。窗口首次渲染完成前,必须以不透明底色覆盖整窗并居中显示 `96×96` 的 `AppLogo@384w.png`,导航完成后再淡出/移除(`SettingsWindowNew.axaml:258-264`,`ViewModel.IsCoverVisible` 初始 `true`)。
7. **关闭行为**。用户点关闭按钮时**不得退出应用**:必须拦截 `close` 事件,改为 `hide()`,并依次执行「保存设置」「保存组件配置」「保存自动化配置」;随后按缓存策略清理页面缓存并触发 GC(`SettingsWindowNew.axaml.cs:501-518`)。仅当关闭原因是应用退出或系统关机时才真正关闭。
8. **需要重启提示**。任何页面通过命令请求重启时,必须把 `IsRequestedRestart` 置 `true`,标题栏出现「需要重启」accent 按钮;点击后弹出模态对话框:标题「需要重启」,正文「部分设置需要重启以应用更改。」,主按钮「重启」,关闭按钮「取消」,默认聚焦主按钮;确认后调用应用重启(`SettingsWindowNew.axaml.cs:536-560`)。
9. **回声洞(加载彩蛋)**。当 `ShowEchoCaveWhenSettingsPageLoading === true` 且正在导航、且管理策略未禁用彩蛋时,页面左下角显示一行提示:`margin: 32px 8px 8px 16px`,`opacity: 0.75`,图标 `EA8A` + 文本,文本取 `Assets/Tellings.txt` 的逐行内容并随机洗牌、不重复播放;文案禁用时整块隐藏(`SettingsWindowNew.axaml:71-91`、`.cs:262-288`)。管理策略 `DisableEasterEggs` 为真时必须隐藏。
### 10.2 导航结构(左侧 NavigationView)
1. **形态**。必须使用 NavigationView 的 `Auto` 模式:宽屏时左侧固定导航窗格,宽度必须为 `283px`(`SettingsWindowNew.axaml:52-60` 的 `OpenPaneLength="283"`),内容区占剩余宽度;窄屏时折叠为汉堡菜单(minimal 模式),且此时**不得**显示 NavigationView 自带的窗格切换按钮,改用标题栏内的自定义切换按钮(`:62-64`)。
2. **响应式阈值**。窗口宽度 `< 800` 时进入压缩模式;窗口最大化时强制退出压缩模式(`SettingsWindowNew.axaml.cs:437-446`)。压缩模式下:标题容器与内容宽度自适应,不再受 `960px` 限制;宽度恢复后重新展开导航。
3. **导航项来源**。设置页面必须由「注册表」驱动:每个页面注册 `{id, name, unSelectedIconGlyph, selectedIconGlyph, category, groupId, hideDefault, useFullWidth, hidePageTitle}`;导航项按 `category` 枚举升序分块,块内保持注册顺序;**每个 category 块之间必须插入一条分隔线**(最后一个块后不加)(`SettingsWindowNew.axaml.cs:145-227`)。
4. **分类与顺序**(`SettingsPageCategory` 枚举 Internal=0、External=1、About=2、Debug=3;`repos/ClassIsland/ClassIsland.Core/Enums/SettingsWindow/SettingsPageCategory.cs:6-24`)。注册顺序决定同块内次序(`App.Services.xaml.cs:156-184`):
- 分组「通用」(id `classisland.general`,图标 `EF27`,展开后子项顺序):基本、时钟、存储、隐私、翻新与迎新、高级;
- 分组「主界面」(id `classisland.mainwindow`,图标 `EC85`):组件、外观、窗口;
- Internal 无分组:提醒、天气、自动化、更新(仅当 `UpdateService.AllowedPackageTypes` 包含当前打包类型时注册);
- External:插件、主题;
- Debug:测试页面(仅开发)、调试;
- About:关于 ClassIsland、集控、集控凭据设置、集控策略(后两者 `hideDefault = true`,不出现在默认导航中,仅通过 URI 打开)、出错啦!(`hideDefault = true`,错误页)。
5. **分组渲染规则**。同一分组只渲染一个可展开的父项,分组内页面为子项;首次导航到子项时父项必须自动展开;选中子项时父项必须显示 `IsChildSelected` 状态(`SettingsWindowNew.axaml.cs:849-877`)。
6. **过滤规则(管理员策略)**。满足以下任一条件的页面必须从导航中移除且不可通过 URI 进入:
- `HideDefault === true`;
- 策略 `DisableSettingsEditing` 为真且页面 category ∈ {Internal, External};
- 策略 `DisableDebugMenu` 为真且 category = Debug;
- `Settings.IsDebugOptionsEnabled === false` 且 category = Debug(`SettingsWindowNew.axaml.cs:150-158`)。
- 当 `DisableSettingsEditing` 为真时,窗口必须以「关于」页作为启动页(同文件 `:125-128`)。
7. **面包屑/标题**。内容区顶部必须有页面标题(`TitleTextBlockStyle`,字号约 28px【推断】),容器 `margin: 12px 16px 0`;当页面声明 `useFullWidth` 时不做 `960px` 限宽,否则标题容器 `max-width: 960px`;当页面声明 `hidePageTitle` 时整块不渲染(`SettingsWindowNew.axaml:92-104`)。
8. **后退栈**。默认每次从导航栏切换页面都必须清空后退栈(不允许跨页返回);仅当 URI 带 `ci_keepHistory=true` 时保留后退栈(`SettingsWindowNew.axaml.cs:372-395`)。ClassIsland **没有**「设置搜索/全局搜索框」,本实现**不得**擅自添加搜索框;若产品确需增强,只能作为 P2 可选增强(默认关闭、独立开关、并在界面标注为非原版功能)。
9. **导航动画与骨架**。页面切换必须播放淡入(`SettingsService.Settings.AnimationLevel >= 1` 时),并在导航期间把 `IsNavigating` 置真以暂停交互、显示回声洞。
### 10.3 页面容器与通用控件(必须映射到 Element Plus)
1. **页面容器**。所有普通设置页必须使用 `padding` 为 `12px 12px 18px 12px`、`max-width: 960px`、子项间距 `4px` 的纵向容器(`repos/ClassIsland/ClassIsland.Core/StylesBase.axaml:67-71`、`ThemeBase.axaml:37` 的 `SettingsContainerWidth = 960`)。在 CSS 中实现为 `.settings-container { margin: 12px 12px 18px 12px; max-width: 960px; display: flex; flex-direction: column; gap: 4px; }`。
2. **页面内分区标题**。使用「图标 + 文本」的行内标题(`IconText`),`margin: 0 0 4px`;分区之间使用 `Separator`(`margin: 0 12px 0 4px`)。Element Plus 中必须用 `<div class="settings-section-title">` 实现。
3. **SettingsExpander(可展开设置卡片)**。必须实现为「卡片行」组件:左侧为 `20×20` 图标(导航图标尺寸 `20`,见 `ThemeBase.axaml:42`)+ 标题 + 描述(次级文本,`font-size: 12px`,可换行),右侧为 `footer` 控件(开关/下拉/滑块/按钮/数值框);展开后下方追加若干 `SettingsExpanderItem` 子行,子行左边距约 `36px`【推断】。Element Plus 映射:`<el-collapse>` 或自定义 `.settings-card`(`background: CardBackgroundFillColorDefaultBrush`、`border: 1px solid CardStrokeColorDefaultBrush`、`border-radius: 4px`、行高约 `64px`【推断】)。
4. **开关**。所有布尔设置必须使用开关(ToggleSwitch),打开态文本「开」、关闭态文本「关」(`StylesBase.axaml:95-98`)。Element Plus 中必须使用 `<el-switch>` 并在需要文字时设置 `active-text="开" inactive-text="关"`;在卡片右侧显示时隐藏开关内部文本,只保留轨道(对应 `SettingsExpanderItem` 内 ToggleSwitch 的 `PART_ContentPresenter` 隐藏,`StylesBase.axaml:100-106`)。
5. **数值框(Field)**。带单位后缀的数值框必须使用「前缀/后缀字段」组件:宽度 `175px`,后缀如 `px`/`秒`/`周`/`天`/`次`/`ms`(见各页);数值框内部使用 `SpinButtonPlacementMode="Inline"`,最小值 `1`、最大值 `40`(字体大小)等按页指定(`AppearanceSettingsPage.axaml:485-496`)。Element Plus 映射:`<el-input-number controls-position="right">` 外面包一层 `.field`(后缀绝对定位在右侧,`padding-right: 24px`)。
6. **滑块**。带数值提示的滑块必须使用 `auto-tooltip` 行为:拖动时在指针位置显示格式化后的值(`StylesBase.axaml:178-191`),格式化字符串按页指定(`F1`/`F2`)。Element Plus `<el-slider :show-tooltip="true">` 已等价。
7. **抽屉(DrawerHost)**。设置页内的所有「编辑/管理」弹层必须使用右侧抽屉:`placement: right`,宽度由内容决定(见各页),带半透明遮罩(`SmokeFillColorDefaultBrush`)和 `box-shadow: 0 0 8px rgba(0,0,0,0.2)`(`repos/ClassIsland/ClassIsland.Core/Controls/DrawerHost.axaml:16-37`)。动画:`250ms cubic-bezier(0, 0, 0, 1)` 同时作用于位移与透明度,遮罩同步淡入(同文件 `:42-59`);`AnimationLevel === 0` 时不得播放动画。必须支持按 `Esc` 关闭、点击遮罩关闭(`DrawerHost.axaml.cs:78-89`、`:115-118`)。Element Plus 映射:`<el-drawer direction="rtl" :size="...">`,并把过渡曲线覆盖为 `cubic-bezier(0,0,0,1)`。
8. **卡片组(cardContainer)**。关于/集控/恢复等页面使用的「卡片列表」:子项间距 `1px`、首项圆角 `4px 4px 0 0`、末项圆角 `0 0 4px 4px`、单项圆角 `4px`(`StylesBase.axaml:255-263`)。
9. **其它通用类**:`l-instructive`(次级说明文本,`opacity` 约 0.75);`items-valign-center`(行内垂直居中);`animated-intro`(入场时子元素依次淡入,`AnimationLevel >= 1` 时启用);`Foreground="IndianRed"` 表示危险操作。实现时必须提供等价 CSS 类,颜色映射见 part 3。
### 10.4 每个设置页的逐项规格
以下每页都必须按「标题/图标 → 分区 → 控件 → 绑定字段 → 默认值 → 枚举 → 生效时机 → 禁用条件」实现。默认值来源统一为 `repos/ClassIsland/ClassIsland/Models/Settings.cs` 的字段初始化(下文以 `Settings.cs:行号` 标注)。
#### 10.4.1 基本(id `general`,图标无自定义 → 默认 `EF27/EF26`,分组「通用」)
源文件 `Views/SettingPages/GeneralSettingsPage.axaml`。
1. **顶部公告区**:当 `ShowSellingAnnouncement` 为真时显示 `Warning` 级 InfoBar,固定文案「ClassIsland 是开源免费的软件,官方没有提供任何形式的付费支持服务,源代码仓库地址在 https://github.com/ClassIsland/ClassIsland/。如果您通过有偿协助等付费方式取得本应用,在遇到问题时请在与卖家约定的服务框架下,优先向卖家求助。如果卖家没有提供您预期的服务,请退款或通过其它形式积极维护您的合法权益。」(`:32-36`)。
2. **行为分区(图标 `E84F`,标题「行为」)**:
- 开机自启:开关,绑定 `DesktopService.IsAutoStartEnabled`(桌面集成,不写入 Settings.json),说明「在您的系统启动时自动运行本应用。」(`:55-62`);
- 注册 Url 协议:开关,绑定 `DesktopService.IsUrlSchemeRegistered`,仅 Windows/Linux 显示,说明含 `classisland://`(`:64-76`);
- 学期开始时间:日期选择器,宽度 `120px`,绑定 `SingleWeekStartTime`,默认「今天所在周的周日」(`Settings.cs:37-38`),说明「设置学期首日,该日期将作为多周轮换课表的计算起点和每周的第一天。」;右侧附「调整多周轮换偏移…」按钮(图标 `E324`)打开飞层 `WeekOffsetSettingsControl`(`:78-117`);
- 最大多周轮换周数(子项):数值框最小 `2`、最大 `9`、格式 `0`、后缀「周」,绑定 `MultiWeekRotationMaxCycle`,默认 `4`(`Settings.cs:61`);
- 点击托盘图标行为:下拉,绑定 `TaskBarIconClickBehavior`,默认 Windows=`0`、其他平台=`4`(`Settings.cs:102`)。选项(5 项):打开主菜单(仅 Windows 可用)、打开档案编辑窗口、显示/隐藏主界面、打开换课窗口、打开设置窗口;macOS 隐藏整项;Linux 说明追加「此选项可能因为桌面环境差异而无法工作。」(`:121-161`);
- 教学安全模式:开关绑定 `IsCriticalSafeMode`(默认 `false`,`Settings.cs:207`),右侧下拉绑定 `CriticalSafeModeMethod`(仅在开启时显示,宽度 `135px`),选项:退出应用(0)、静默重启应用(1)、显示通知(2)、忽略(不推荐)(3);关闭时显示禁用状态的「显示崩溃报告」下拉(`:163-189`);
- 显示启动加载界面:开关绑定 `IsSplashEnabled`(默认 `false`,`Settings.cs:115`)。展开后子项:启动图标(文本框 `min-width: 150`、`max-width: 250`,水印「(默认图标)」,绑定 `SplashCustomLogoSource`,右侧附带图片文件选择按钮)、启动文字(文本框 `min-width: 200`、`max-width: 300`,水印「正在启动…」,绑定 `SplashCustomText`)、显示详细状态信息(开关,绑定 `ShowDetailedStatusOnSplash`,默认 `false`,`Settings.cs:222`)、「自定义启动界面功能已被您所在的组织禁用。」(当 `DisableSplashCustomize` 时替换上面三项)、「预览启动界面」(可点击行,图标 `E44E`,点击显示 5 秒启动界面并置 `IsSplashPreviewing`,行尾显示 24×24 进度环)(`:191-243`)。
3. **界面分区(图标 `F485`,标题「界面」)**:
- 隐藏窗口:TabControl(compact)切换 `HideMode`(默认 `0`)。
- 「基础模式」页:三个复选框——上课时(`HideOnClass`,默认 `false`)、前台是最大化窗口时(`HideOnMaxWindow`,默认 `false`)、前台是全屏窗口时(`HideOnFullscreen`,默认 `false`);
- 「高级模式」页:按钮「编辑规则集…」(图标 `F17E`)打开宽度 `600px` 的规则集抽屉(`HideRules`)(`:249-278`);
- 动画级别:下拉宽度 `120px`,绑定 `AnimationLevel`,默认 `1`(标准),选项:禁用(0)、标准(1)、华丽(2)。**修改后必须请求重启**(`GeneralSettingsPage.axaml.cs:37-44`)(`:281-293`);
- 禁用动画等待:开关绑定 `IsWaitForTransientDisabled`,默认 `false`,**修改后必须请求重启**(`Settings.cs:206`、`.axaml.cs:37-44`);
- 降低主界面进度条精度:开关绑定 `ReduceProgressAccuracy`,默认 `true`(`Settings.cs:1853`)(`:306-312`)。
#### 10.4.2 时钟(id `clock`,图标 `E4C4/E4C3`,分组「通用」)
源 `ClockSettingsPage.axaml`。
1. **顶部时钟**:文本 `HH:mm:ss`,`font-size: 36px`,`font-weight: 500`;下方为日期长格式文本(次级说明样式)(`:15-20`)。
2. 「使用精确时间」卡片:开关绑定 `IsExactTimeEnabled`(默认 `true`,`Settings.cs:140`),展开后子项:
- 「时间服务器」文本框绑定 `ExactTimeServer`,默认 `ntp.aliyun.com`(`Settings.cs:139`);
- 「立即同步」按钮(图标 `E0B5`),下方显示 `ExactTimeService.SyncStatusMessage` 状态文本(`:24-53`)。
3. 「时间偏移」卡片:右侧带「秒」后缀的数值框(`min-width: 147`,Inline 步进),绑定 `TimeOffsetSeconds`,默认 `0`(`Settings.cs:141`);展开后子项:
- 「自动时间偏移」:数值框(同样 `min-width: 147`,绑定 `TimeAutoAdjustSeconds`,默认 `0`,`Settings.cs:147`)与开关(绑定 `IsTimeAutoAdjustEnabled`,默认 `false`,`Settings.cs:146`)并排,开关关闭时数值框禁用;
- 「对齐铃声」(可点击行,无图标)→ 打开 `TimeAdjustmentWindow`(详见 part 11)(`:56-91`)。
#### 10.4.3 存储(id `storage`,图标 `E6B7/E6B6`,分组「通用」)
源 `StorageSettingsPage.axaml`。
1. 当存在配置文件错误(`ConfigureFileHelper.Errors.Count > 0`)时,顶部显示 Warning InfoBar:「ClassIsland 部分配置文件已损坏且无法加载,这些配置文件已恢复至默认值。」带「详细信息…」按钮(图标 `EC2E`)跳转 `classisland://app/config-errors`(`:17-28`)。
2. **备份分区(图标 `E081`,标题「备份」)**:
- 「启用自动备份」:开关绑定 `IsAutoBackupEnabled`,默认 `true`(`Settings.cs:187`)。展开后子项:
- 自动备份间隔:数值框最小 `0`、宽 `150px`、后缀「天」、Inline,绑定 `AutoBackupIntervalDays`,默认 `7`(`Settings.cs:191`);
- 自动备份上限:数值框最小 `0`、宽 `150px`、后缀「个」,绑定 `AutoBackupLimit`,默认 `16`;说明「设置为 0 则为无上限。」(`Settings.cs:190`);
- 上次成功自动备份:只读展示 `LastAutoBackupTime`(`Settings.cs:188`)。
- 「手动备份」卡片:右侧在有操作时显示 20×20 进度环,成功后显示 `E423` 图标(tooltip「备份成功。」)。子项:立即备份(点击,备份中禁用)、查看备份文件…(`EC2D`)、恢复备份…(`EC2D`,说明「重启到恢复模式并恢复到指定的备份。」)、备份文件占用大小(只读 `BackupFilesSize`,默认「计算中...」,`Settings.cs:189`)。手动备份内容必须包含 `Config/` 全部配置文件、`Profiles/` 档案文件与 `Settings.json`(`:76-107`)。
#### 10.4.4 隐私(id `privacy`,图标 `EF65/EF64`,分组「通用」)
源 `PrivacySettingsPage.axaml`。必须实现:
1. 标题「匿名使用情况分析」(`SubtitleTextBlockStyle`),正文「本应用将通过 SentrySDK 来收集本应用中的使用情况与错误信息,以改进本应用。您的信息会匿名上传,不会包含您的个人信息。您可以自行启用或禁用此功能。」;下方「更多信息请见《ClassIsland 隐私政策》。」(超链接打开隐私政策)(`:18-31`)。
2. 「启用匿名使用情况分析」卡片:开关绑定 `IsSentryEnabled`(存储于本地全局存储,默认 `true`;`Settings.cs:518-535`)。说明中必须保留「为了确保环境块完全刷新,可能需要重启计算机才能使本设置完全生效。」(`:33-39`)。策略 `CanPolicyDisable`(容器 Tag)语义:允许被集控禁用【推断】。
#### 10.4.5 翻新与迎新(id `refreshing`,图标 `E0B8/E0B9`,分组「通用」)
源 `RefreshingSettingsPage.axaml`。实现要点:
1. 「定期翻新」卡片:开关绑定 `IsRefreshingToastEnabled`(默认 `true`,`Settings.cs:1865`);说明「当在长时间未启动 ClassIsland 的寒假/暑假等学期结束后的长假后启动 ClassIsland 时,显示桌面通知询问是否重新设置应用的部分设置以适应新学期。」展开后子项为「默认翻新范围」(文本)+ 4 个复选框:
- 档案(`RefreshingScopes.Profile`),勾选后的副作用文案必须变为「将创建并切换到一个全新的配置文件。」;
- 课表(`RefreshingScopes.ProfileClassPlans`)、时间表(`RefreshingScopes.ProfileTimeLayouts`)——仅当「档案」未勾选时显示,并在其右侧提供「选择保留的内容…」按钮(图标 `F1A8`)打开 `RefreshingScopesConfigDialog`;「课表」「时间表」互斥逻辑:不勾选「档案」时才出现;
- 组件配置(`RefreshingScopes.Components`,副作用文案「将禁用已经启用的插件。」适用于插件项);
- 自动化(`RefreshingScopes.Automations`);
- 插件(`RefreshingScopes.Plugins`,勾选后描述为「将禁用已经启用的插件。」);
- 「立即翻新」(可点击行,图标 `E44E`)(`:14-58`)。
2. 「迎新模式」卡片:开关绑定 `RefreshingToastIsOnboardingGuide`(默认 `false`,`Settings.cs:1869`);说明必须包含「此选项在生效一次后将自动关闭。」;展开后子项:通知标题(文本框 `min-width: 120`、`max-width: 450`,绑定 `OnboardingToastTitle`,默认见 `RefreshingService.DefaultOnboardingToastTitle`)、通知内容(多行文本框,`AcceptsReturn`,绑定 `OnboardingToastBody`)、「重置为默认值」、「测试通知」(`:61-86`)。
3. 分区「通知选项」(图标 `E408`):
- 「通知显示间隔」:数值框最小 `1`、最大 `33550337`、格式 `0`、后缀「天」,绑定 `RefreshingToastThresholdDays`,默认 `20`(`Settings.cs:1866`);子项「在下次启动时显示通知」:开关绑定 `ShowRefreshingToastOnNextStart`(默认 `false`),开关开启时旁边额外显示「立即退出应用」按钮(`:92-118`);
- 「最高通知显示次数」:数值框最小 `1`、最大 `16`、后缀「次」,绑定 `MaxRefreshingToastCounts`,默认 `5`(`Settings.cs:1873`);子项显示「剩余通知次数:{LeftRefreshingToastCounts}」,当剩余次数 `!== 0` 时显示「清空」按钮(`:121-154`)。
#### 10.4.6 高级(id `advanced`,图标 `F49F/F49E`,分组「通用」)
源 `AdvancedSettingsPage.axaml`。
1. 顶部必须显示不可关闭的 Warning InfoBar:标题「小心!」,正文「修改本页面的设置存在风险,请谨慎操作。」(`:13-16`)。
2. 「自动禁用异常插件」开关:绑定 `AutoDisableCorruptPlugins`,默认 `true`(`Settings.cs:1844`)(`:18-25`)。
3. 「渲染模式」下拉(仅 Windows 显示):选项「默认(0)/软件(不推荐)(1)/AngleEgl(2)/Wgl(3)/Vulkan(4)」,**修改后重启生效**,此设置仅本设备生效(`:28-46`)。
4. 「使用系统标题栏」开关(仅 Windows 且非 arm64):需重启【推断】(`:49-61`)。
5. 「兼容缩放模式」开关(仅 Linux):忽略 `QT_SCREEN_SCALE_FACTORS` 等环境变量;需重启【推断】(`:63-74`)。
#### 10.4.7 组件(id `components`,图标 `E06F/E06F`,分组「主界面」)
源 `ComponentsSettingsPage.axaml`(644 行,非 `settings-container` 结构,必须自绘三栏式编辑器)。
1. **整体布局**:外层 `margin: 12px 0`,6 行:0 Auto(占位)、1 Auto(配置方案栏)、2 行高 `64`(最小 `43.2`、最大 `250`,行列表)、3 Auto(分隔条 `height: 2px`,`margin: 0 -12px`)、4 `*`(组件库/设置区)、5 Auto(InfoBar)(`:162-171`)。
2. **配置方案栏**:左侧说明文本「以下组件将显示在主界面上,从组件库拖动组件以添加组件。」;右侧:字段「配置方案」+ 下拉(`min-width: 150`、`max-width: 300`,绑定 `CurrentComponentConfig`,默认 `"Default"`,`Settings.cs:159`)、刷新按钮(`E0B5`)、新建…(`E00D`)、打开配置文件夹…(`E88D`)(`:174-205`)。
3. **主界面行列表**:每行高 `40px`,结构为「拖拽把手(宽 20)+ 组件横向列表 + 选中态操作区」。选中某行时右侧浮出操作区:`padding: 4px`、圆角 `4px`、背景 `AcrylicBackgroundFillColorBaseBrush`,内含 4 个按钮:主要行开关(`E994`,tooltip「此行是主要行」)、提醒开关(`E02B`,tooltip「此行可以显示提醒」)、在下方插入一行(`E00D`)、删除(`E61D`,`IndianRed`)(`:207-288`)。
4. **组件横向列表**:高度 `40px`,水平滚动;每项 = 拖拽把手 + `14×14` 图标 + 名称 + 选中时出现的「更多选项…」(`EBAC`)菜单与「查看子组件」(`EC30`,仅容器组件)与删除按钮(`E61D`)。「更多选项」菜单项固定顺序:向上移动一行(`E197`)、向下移动一行(`E0CB`)、移动到选中的容器(`E280`)、从容器组件移出(`E286`)、分隔线、包裹到新容器(子菜单列出全部容器组件,带图标)、创建副本(`EEB9`)(`:27-155`)。
5. **子组件视图**:选中容器组件并点击「查看子组件」后,在组件设置区上方出现卡片(圆角 `4px`、边框 `CardStrokeColorDefaultBrush`、背景 `CardBackgroundFillColorDefaultBrush`),头部 `padding: 8px`,含「返回上一层级」按钮(`E0C7`)、标题「此组件的子组件」、「关闭」按钮(`E671`)(`:296-340`)。
6. **下方 TabControl**:`compact`、`max-width: 960px`、内容拉伸,四个页签:
- 组件库(按来源分组,组名 `font-weight: 600`;每个组件项高 `50px`:拖拽把手宽 24 + 32×32 图标 + 名称 + 12px 描述;每组使用 WrapPanel,目标宽度 `225px` 自动换列)(`:348-413`);
- 组件设置(仅在选中组件时显示,渲染该组件的设置控件)(`:414-417`);
- 高级设置(选中组件的通用高级设置,含「按规则隐藏」开关与规则集编辑)(`:418-422`);
- 行设置(选中行时显示):按规则隐藏(开关+编辑规则集按钮)、分体主界面(下拉:继承(0)/禁用(1)/启用(2))、分区「外观」(图标 `EC4A`):行不透明度滑块(`0.1–1`,步进 `0.05`,`F2`)、自定义背景颜色(颜色选择器+开关)、自定义背景不透明度(滑块 `0~1`)、自定义背景圆角半径(滑块 `0–20`,步进 `0.5`,`F1`)、分区「字体」(图标 `F26F`):字体大小(1–40px 四档,覆盖外观设置)、字体颜色(颜色选择器+开关)(`:423-614`)。
7. 右下角浮动工具条(选中组件时):AnimatedIconButton「编辑…」(图标 `F17E`,仅在「按规则隐藏」开启时可点)+ 复选框「按规则隐藏」(绑定 `SelectedComponentSettings.HideOnRule`)(`:617-631`)。
8. 底部 InfoBar:「新功能:现在可以使用⌈编辑模式⌋更加直观地编辑主界面上的组件!」+「进入编辑模式」按钮 → `classisland://app/edit?fromSettingsPage=true`(`:634-642`)。
#### 10.4.8 外观(id `appearance`,图标 `E51E/E51D`,分组「主界面」)
源 `AppearanceSettingsPage.axaml`。必须严格按下列默认值实现(值来源 `Settings.cs`):
1. 「界面缩放」:滑块宽 `180px`,最小 `0.75`、最大 `2.5`、步进 `0.05`、吸附、拖动显示 `F2`,绑定 `Scale`,默认 `1.0`(`Settings.cs:86`)(`:27-40`)。
2. 「背景颜色」:颜色选择器(仅自定义开启时显示,`margin-right: 6px`)+ 开关,绑定 `BackgroundColor`(默认 `Black`)与 `IsCustomBackgroundColorEnabled`(默认 `false`)(`Settings.cs:33-34`)。
3. 「背景不透明度」:滑块 `0–1`、步进 `0.05`、`F2`,绑定 `Opacity`,默认 `0.5`(`Settings.cs:64`)。
4. 「圆角半径」:滑块 `0–20`、步进 `0.5`、`F1`,绑定 `RadiusX`,默认 `8.0`(`Settings.cs:183`;Y 半径 `RadiusY` 同为 `8.0`,本页只暴露 X)。
5. 「行间隔」:滑块 `0–150`、步进 `5`、`F1`,宽 `130px`;右侧数值框宽 `125px`、最小 `0`;绑定 `MainWindowLineVerticalMargin`,默认 `5`(`Settings.cs:219`)。
6. 「分体主界面」:开关绑定 `IsIslandSeperated`,默认 `false`(`Settings.cs:1852`),说明「启用后组件会分开。」。
7. 分区「主题」(图标 `E51E`,标题「主题」):
- 「应用主题」下拉:跟随系统(0)/明亮(1)/黑暗(2),绑定 `Theme`,默认 `2`(`Settings.cs:32`),每项带图标(`E5CB`/`F465`/`F44B`);
- 「主题色来源」下拉:自定义(0)/系统壁纸(1,禁用隐藏)/系统(2,默认)/屏幕主题色(3,禁用隐藏),绑定 `ColorSource`,默认 `2`(`Settings.cs:83`);
- 「强调色」颜色选择器:仅在 `ColorSource === 0` 时显示,绑定 `PrimaryColor`,默认 `#00BFFF`(`Colors.DeepSkyBlue`,`Settings.cs:35`);副色 `SecondaryColor` 默认 `#7FFFD4`(Aquamarine),本页未暴露【推断:仅由主题使用】。
8. 分区「字体」(图标 `F26F`,标题「字体」):
- 「自定义字体」下拉:`max-dropdown-height: 360px`,弹出层宽 `280px`,底部右对齐;每项按自身字体渲染、单行截断、`height: 20px`,默认项显示文本「HarmonyOS Sans SC(默认)」;绑定 `MainWindowFont`,默认 `MainWindow.DefaultFontFamilyKey`(`Settings.cs:95`);
- 「自定义字重」下拉:选项 Thin/ExtraLight/Light/Normal/Medium(默认)/SemiBold/Bold/ExtraBold/Black/ExtraBlack,每项以对应字重预览;绑定 `MainWindowFontWeight2`(整型,默认 `(int)FontWeight.Medium` = 500,`Settings.cs:101`);
- 「字体大小」四行:次级字体大小(默认 `14`)、正文字体大小(`16`)、强调字体大小(`18`)、大号字体大小(`20`);均为「px」后缀数值框,范围 `1–40`,宽 `175px`(`Settings.cs:197-200`);
- 「字体颜色」:颜色选择器(仅开启时)+ 开关,绑定 `CustomForegroundColor`(默认 `#1E90FF`,DodgerBlue)与 `IsCustomForegroundColorEnabled`(默认 `false`)(`Settings.cs:202-203`)。
9. 底部 TabControl(compact)两个页签:
- 「字体预览」:只读可编辑文本框,`font-size: 17px`、`padding: 8px`、居中;内容必须为四行:「当有一天你不再纠结于答案 当我们又重逢于天涯或沧海」/「语文 数学 英语 历史 物理 政治 化学 生物 地理」/「the quick brown fox jumps over a lazy dog」/「THE QUICK BROWN FOX JUMPS OVER A LAZY DOG」/「8:00-12:00 1234567890 +-*/」;应用当前字体与字重(`:553-569`);
- 「字号预览」:字段「测试文本」+ 四行分别以四种字号渲染同一文本(`:570-591`)。
#### 10.4.9 窗口(id `window`,图标 `F485/F484`,分组「主界面」)
源 `WindowSettingsPage.axaml`。
1. **停靠位置选择器**(`:24-127`):左侧为 `220px` 高的模拟屏幕边框(`border: 8px solid TextFillColorPrimaryBrush`、圆角 `4px`),屏幕底部叠加 `16px` 高的模拟任务栏(背景 `#99000000`,左端 `E064` 图标 `10×10`、`margin: 3px`,右端时间文本 `8px` Light)。屏幕内 6 个单选按钮,分两行三列,分别对应 `WindowDockingLocation` 值 `0`(左上)、`1`(上中,默认)、`2`(右上)、`3`(左下)、`4`(下中)、`5`(右下)(默认值 `Settings.cs:48`)。右侧:字段「向右偏移」(`WindowDockingOffsetX`,默认 `0`,`Settings.cs:49`)、「向下偏移」(`WindowDockingOffsetY`,默认 `0`,`Settings.cs:50`)、「显示器」下拉(列出所有屏幕:「图标 + 显示名 (宽x高)」,绑定 `WindowDockingMonitorIndex`,默认 `0`,`Settings.cs:51`)、「刷新显示器列表」按钮(`E0B5`)、说明文本「应用会以左图的位置显示在指定的显示器上。如果指定的显示器不存在,那么应用会显示在主显示器上。」。
2. **InfoBar**:RawInput 初始化失败时显示 Warning「初始化 RawInput 失败,将使用兼容模式判断光标位置。详细请查看日志。」;启用 RawInput 时显示「使用原始输入判断鼠标位置时,如果前台窗口的进程以管理员身份运行,可能会出现无法判定鼠标位置的情况。以管理员身份运行 ClassIsland 可以解决此问题。」(`:130-138`)。
3. **「窗口层级」**(`:141-188`):下拉默认选中 `WindowLayer`,默认 `1`(置顶);选项:置底(0)、置顶(1)。子项「窗口层级设置频率」下拉绑定 `WindowTopmostRecheckMode`(默认 `0`):窗口层级变化时(默认)(0)、前台窗口变化时(1)、「每 50ms」(2,后带橙色 `F431` 图标)、「每 1ms」(3,后带橙红色 `F431` 图标)。
4. **「指针移入淡化」**(`:191-236`):开关绑定 `IsMouseInFadingEnabled`(默认 `true`,`Settings.cs:193`);子项:
- 「使用原始输入」开关(仅 Windows),绑定 `UseRawInput`,默认 `false`(`Settings.cs:192`);
- 「触屏点击后自动恢复时间」:数值框 `width: 150px`、最小 `0`、后缀「ms」、Inline,绑定 `TouchInFadingDurationMs`,默认 `0`(`Settings.cs:195`);仅当启用原始输入时显示;
- 「反转指针移入淡化」开关:绑定 `IsMouseInFadingReversed`,默认 `false`(`Settings.cs:194`)。
5. **「使用原始屏幕尺寸」** 开关:绑定 `IsIgnoreWorkAreaEnabled`,默认 `false`(`Settings.cs:1835-1838`),说明「若启用,主界面调整位置时将忽略系统工作区设置(通常包括任务栏和部分顶栏应用)。」(`:239-245`)。
6. **「录制模式」** 开关:绑定 `IsScreenRecordingModeEnabled`,默认 `false`(`Settings.cs:1861`),说明必须包含「这会导致主界面窗口显示在任务栏中且主界面窗口只会在一个虚拟桌面上显示」(`:248-254`)。
7. **「阻止截图」**(仅 Windows):开关绑定 `IsWindowCaptureBlockingEnabled`,默认 `false`(`Settings.cs:1862`)(`:256-267`)。
#### 10.4.10 提醒(id `notification`,图标 `E02B/E02A`,分组:Internal 无分组)
源 `NotificationSettingsPage.axaml`(497 行)。必须实现为「左列表 + 右详情」两栏布局(`ListDetailView`,两栏等分;容器 `margin: 12px`;压缩阈值 `800px`,压缩时变为列表→详情可返回的单栏,见 `Controls/ListDetailView.axaml.cs:49-56`、`.axaml:46-71`)。
1. **左栏**:
- 「启用提醒」卡片:开关绑定 `IsNotificationEnabled`,默认 `true`(`Settings.cs:91`);说明「启用提醒后,将会在对重要事件发出醒目提醒。拖动提醒提供方以调整对应类型提醒的优先级。」(`:64-72`);
- 当所有主界面行都关闭提醒时显示 Warning InfoBar:「提醒可能无法显示」「您禁用了所有主界面行的提醒功能,这可能会导致所有提醒都无法显示。要修复此问题,您需要在【组件】页面至少为一个主界面行开启提醒。」+「前往【组件】页面」按钮(`:74-83`);
- TabControl(compact):页签「提醒提供方」= 提供方列表;页签「高级设置」= 全局提醒设置。
- 提供方列表项:拖拽把手 + `24×24` 图标(`margin: 6px 6px 10px 6px`)+ 名称(`font-size: 16px`,`font-weight: 700`)+ 描述(`12px` 次级)+ 右侧设置指示器;选中项的底部展开「提醒渠道」Expander(仅有渠道时显示),渠道列表项:`24×24` 图标 + 名称 `14px` Medium + 描述 `12px`(`:86-204`);
- 高级设置页:见下 4 小节。
2. **右栏**(选中提供方后):
- 页签「基本设置」:渲染该提供方的设置控件;无设置时居中显示 `72×72` 图标 `EF26` + 「该提醒提供方没有设置。」(`:372-390`);
- 页签「高级设置」:首行「对此提醒来源启用特殊高级设置」开关(绑定 `ProviderSettings.IsSettingsEnabled`,说明「启用后,此处的设置将覆盖全局设置和提醒请求中要求的设置。」),随后 InfoBar 提示同样文案;四个开关:启用提醒语音(`IsSpeechEnabled`)、启用提醒强调特效(`IsNotificationEffectEnabled`)、启用提醒音效(`IsNotificationSoundEnabled`,展开后含「自定义音效」文本框 + 文件选择)、置顶提醒(`IsNotificationTopmostEnabled`)(`:391-495`)。
3. **全局高级设置四组**:
- 「启用提醒语音」开关 `AllowNotificationSpeech`(默认 `false`,`Settings.cs:153`)→ 子项:复选框「默认启用」`IsSpeechEnabled`(默认 `true`,`Settings.cs:135`)、语音引擎下拉(`SelectedSpeechProvider`,默认 `"classisland.speech.edgeTts"`,`Settings.cs:1847`)、语音引擎设置控件(动态)、语音音量滑块(`0–1`,步进 `0.01`,`F2`,宽 `150px`,绑定 `SpeechVolume`,默认 `1.0`)、测试朗读文本框、测试语音按钮(`:223-281`);
- 「启用提醒强调特效」开关 `AllowNotificationEffect`(默认 `true`,`Settings.cs:154`)→ 子项「默认启用」`IsNotificationEffectEnabled`(默认 `true`,`Settings.cs:142`)(`:285-297`);
- 「启用提醒音效」开关 `AllowNotificationSound`(默认 `false`,`Settings.cs:155`)→ 子项:「默认启用」`IsNotificationSoundEnabled`(默认 `true`)、音效音量滑块(`NotificationSoundVolume`,默认 `1.0`,`Settings.cs:182`)、自定义音效(文本框 `150px`,水印「(默认音效)」,`NotificationSoundPath` 默认空,附音频文件选择按钮)(`:300-349`);
- 「置顶提醒」开关 `AllowNotificationTopmost`(默认 `true`,`Settings.cs:156`)→ 子项「默认启用」`IsNotificationTopmostEnabled`(默认 `true`,`Settings.cs:149`)(`:352-364`)。
#### 10.4.11 天气(id `weather`,图标 `F44F/F44E`,分组:Internal 无分组)
源 `WeatherSettingsPage.axaml`。
1. **天气预览卡**(`:68-150`):背景 `ControlFillColorDefaultBrush`,`padding: 20px`,圆角 `12px`,内部行间距 `16px`。三列:
- 左:天气图标(宽 `52–120`、高 `52`)+ 温度(`font-size: 32px` SemiBold,单位 `18px`)+ 「更新于 HH:mm」(`12px`,`opacity: 0.6`);
- 中(间距 `28px`):湿度、空气质量、风力三组,每组标签 `12px`(`opacity: 0.8`)+ 图标 `14px` + 数值 Medium;
- 右:刷新按钮 `40×40`(图标 `E0B5`);刷新中改为 40×40 进度环;
- 灾害预警列表:每条为 `Expander`(`padding: 12px 8px`,`margin-top: 4px`),头图为 `22×22` 图标 + 标题;展开显示详情(`13px`,`opacity: 0.8`)。
2. **位置设置**(`:154-275`):卡片右侧用 Chip ListBox 切换 `WeatherLocationSource`,两个选项「选择城市」(0)、「坐标定位」(1,仅平台支持定位时显示,默认 `0`,`Settings.cs:1840`)。子项:
- 城市选择(仅 0):显示 `CityName`(默认「北京 (北京, 中国)」,`Settings.cs:100`)+ 「编辑城市」按钮(`E7C9`,主色)→ 打开宽 `320px` 的城市搜索抽屉:标题「搜索城市」、搜索框(水印「搜索城市或省份…」,右侧加载时显示 24×24 进度环)、结果单选列表(每项 RadioButton)、空态「找不到符合条件的城市。请更换搜索关键字,然后再试一次。」(`:26-64`);
- 位置坐标(仅 1):经度/纬度文本框(`min-width: 75px`,前缀「经度/纬度」、后缀「°」),支持「显示坐标」遮罩(模糊 `blur(16px)` + `opacity: 0.75` + 禁止命中,过渡 `150ms` 曲线 `cubic-bezier(0,1,0,1)`)、「自动定位」按钮(`EBD2`)(`:191-258`);
- 自动获取位置(仅 1):开关 `AutoRefreshWeatherLocation`,默认 `false`(`Settings.cs:1841`)。
3. 分区「外观与高级设置」(图标 `E713`):
- 「天气图标包」下拉:选项来自注册的天气图标模板,绑定 `WeatherIconId`,默认 `"classisland.weatherIcons.lucide"`(`Settings.cs:1849`),`min-width: 120px`;
- 「排除的气象预警」:文本域(宽 `260px`、`max-height: 200px`、多行),绑定 `ExcludedWeatherAlerts`(字符串集合,一行一条);说明「包含以下字符串的预警将不会显示,一行一条排除项。」;
- 「不使用 TLS 协议获取天气」开关:绑定 `NoTLSWeatherRequests`,默认 `false`(`Settings.cs:1842`)。
4. 底部提示块(`ControlFillColorDefaultBrush`、圆角 `8px`、`padding: 12px`、`font-size: 12px`、`opacity: 0.8`):必须含两个跳转链接「【提醒】页面」→ `classisland://app/settings/notification`、「【组件】页面」→ `classisland://app/settings/components`(`:321-329`)。
#### 10.4.12 自动化(id `automation`,图标 `EEF1/EEF0`,分组:Internal 无分组)
源 `AutomationSettingsPage.axaml`。
1. **首次使用须知页**(`IsAutomationWarningVisible === true` 时整页替换,默认 `true`,`Settings.cs:161`):居中,顶部 75×75 应用图标 + 箭头(`E00C`,25px,左右 `margin: 16px`)+ 75×75 自动化图标;标题「自动化」;正文两行(带全角缩进)「 欢迎使用自动化!」「 自动化可以使 ClassIsland 根据条件自行动出行动,」「 比如修改应用设置、打开应用,等等。」;「阅读自动化帮助」链接按钮(`E C2D`,主色);警告框(`SystemFillColorCautionBackgroundBrush`,`padding: 10px 10px 14px`,圆角 `4px`):图标 `F430` + 「警告」+ 正文「自动化允许自动修改 ClassIsland 的各项设置,并调用」「 外部程序,可能有一定安全风险。」「 不当的自动化编写可能对正常教学造成影响,请勿滥用。」;底部 accent 按钮「同意并开始使用自动化」(`E424`)(`:333-400`)。
2. **主界面**(两栏 `ListDetailView`):左栏:
- 「启用自动化」卡片:开关绑定 `IsAutomationEnabled`(默认 `false`,`Settings.cs:160`),右侧带「帮助…」按钮(`E23C`,tooltip「打开自动化帮助文档…」);该卡片自适应宽度阈值必须设为 `495px`(`:25-49`);
- 操作行:「添加自动化」按钮(`E00D`,主色,tooltip「添加一个自动化。」);右侧配置方案下拉(`min-width: 120`、`max-width: 150`,绑定 `CurrentAutomationConfig`,默认 `"Default"`,`Settings.cs:162`,打开下拉时刷新列表)+「新建…」(`E689`)+「打开…」(`E88D`);
- 自动化列表:每项高 `42px`,结构 `拖拽把手(20) + 名称(13px Medium) + WorkflowProgressIndicator(透明度 0→1,过渡 100ms) + 启用开关`;列表项可拖拽排序(垂直方向阈值 `3px`)(`:87-157`)。
3. **右栏标题区**(选中自动化后):自动化名称文本框(`min-width: 150`、`font-size: 15px` Medium、水印「自动化名称」)+「启用恢复」复选框(tooltip「该自动化是否启用恢复。」)+ 运行状态控件 + 分隔竖线(`1×25px`,`TextFillColorDisabledBrush`)+「复制」按钮(`E58B`)+「删除」按钮(`E61D`,`IndianRed`);该组容器最小宽度按恢复开关状态为 `359px` 或 `391px`(`:161-217`)。
4. **右栏编辑器**(三个 Expander,均默认展开,间距 `margin: 2px 2px 2px 10px`,标题栏背景 `CustomizedAccentBarBackground1Brush`):
- 「当事件触发时」(图标 `E84F`):触发器设置控件(绑定 `Triggers`);
- 「并且满足规则集时」(图标 `F17E`):标题行右侧开关绑定 `IsConditionEnabled`;关闭时显示「工作流运行时不会考虑规则集。」;
- 「触发行动」(图标 `E01F`):行动设置控件(绑定 `ActionSet`);
- 若 `ActionSet.IsRevertEnabled` 为真,底部显示提示行(图标 `E01D`)「当规则集不再满足时,将自动恢复行动。」(`:223-323`)。
5. 未选中自动化时的空态:居中 `75×75` 贴纸(`光辉矢愿_瞄准.png`,缺失时回退图标 `EEF1`)+ 「在左侧选择或添加一个自动化,然后在此处进行设置。」+「阅读自动化帮助」链接(`:299-320`)。
#### 10.4.13 更新(id `update`,图标 `E161/E160`,分组:Internal 无分组;仅当打包类型允许更新时注册)
源 `UpdateSettingsPage.axaml`。
1. **状态头部**:左侧 `48px` FluentIcon,图标随 `LastUpdateStatus` 变化;当「正在下载更新」时图标以 `1s` 无限循环旋转(`cubic-bezier(0.50,0.25,0.50,0.75)`,0°→360°)(`:77-134`)。右侧第一行 `font-size: 24px` Medium 的状态文案;第二行:已是最新时显示「上次检查更新时间:{LastCheckUpdateTime}」,否则显示「发现新版本:{DistributionInfo.Version}」。
2. InfoBar:网络错误(Error,可关闭,标题「更新时发生错误,请检查您的网络连接并重试。」+ 异常消息)、部署错误(Error,可关闭,标题「部署时发生错误,请稍后重试。」)、已部署(Informational,标题「更新将在重启后应用」,正文「已成功部署更新,下次启动应用时将启动新版本。您也可以现在重启以立即使用新版本的 ClassIsland。」)(`:135-152`)。
3. 操作按钮(随状态切换,`AnimatedPanel` 自适应换行,非 Idle 时整体禁用):
- 已是最新:「检查更新」(accent);
- 有更新:「下载并安装」(accent SplitButton,Flyout 含「仅下载」)、「检查更新」;
- 已下载:「安装更新」(accent);
- 已部署:「重启并应用更新」(accent);
- 非「已是最新/已部署」状态另有一个「检查更新」普通按钮(`:153-225`)。
4. 下载进度区(非 Idle 时可见):`ProgressBar`(`max = DownloadingCountTotal`,`value = DownloadedCount`,非「下载更新」状态或进度不确定时为 indeterminate)+ 状态文案(`UpdateWorkingStatus` 映射);另有两个按钮「查看下载状态」(打开宽度 `450px` 的下载任务抽屉)、「取消」(仅下载中显示)(`:227-277`)。
5. TabControl(compact,Idle 时可用):
- 「更新日志」:Markdown 渲染 `NewVersionChangeLogDocument`;已是最新时显示 81×81 贴纸(`光辉矢愿_遨游.png`)+ 「已经是最新版啦,真棒,夸夸你哦♪」(禁用彩蛋时改为「真棒,您已更新到最新版本!」)+「查看更新日志」按钮(打开宽度 `500px` 的更新日志抽屉)(`:291-341`);
- 「更新设置」:「更新模式」下拉(从不自动更新(0)/自动检查更新并通知(1)/自动检查更新并下载(2)/自动检查更新并安装(3),绑定 `UpdateMode`,默认 `3`,`Settings.cs:77`);「更新通道」下拉(选项来自分发元数据,绑定 `SelectedUpdateChannelV3`,默认 `Guid.Empty`,`Settings.cs:1854`)+ 当前通道描述 + 「强制检查更新」(可点击行,图标 `E44E`,说明「强制将应用更新到当前通道上的最新版本,即使此版本比应用当前版本更旧。」)(`:342-385`);
- 「(debug)调试设置」页签仅开发版显示:覆盖 PDC 根 Url(`DebugPhainonRootUrlOverride`)、覆盖子频道(`DebugSubChannelOverride`)、覆盖元数据公钥(`DebugPublicKeyOverride`,多行)(`:386-399`)。
#### 10.4.14 插件(id `classisland.plugins`,图标 `E071/E071`,External;`[FullWidthPage]` 全宽)
源 `PluginsSettingsPage.axaml`(884 行)。
1. **首次使用警告页**(`IsPluginMarketWarningVisible`,默认 `true`,`Settings.cs:204`):居中,75×75 应用图标 + `E00C`(25px)+ 75×75 插件图标 `E070`;标题「插件」;正文「欢迎使用插件!插件可以扩展 ClassIsland 的功能,比如添加一个组件,或者和其它软件联动,等等。」;警告框(宽 `320px`,`SystemFillColorCautionBackgroundBrush`,圆角 `4px`,`padding: 8px`)文案必须完整保留(含「插件可以以 ClassIsland 的身份使用各项系统资源并请求任意权限……」);底部 accent 按钮「同意并开始使用插件」(`E424`)。整个主界面在此状态下必须 `IsEnabled=false`(`:509-511`、`:768-823`)。
2. **主布局**:两栏,左栏 `margin: 4px 0`:
- 顶部搜索框(水印「搜索插件…」,左侧 `EEF3` 图标,`padding: 26px 4px`、`border-thickness: 0`),支持 Enter/输入过滤(`:518-531`);
- 下方工具栏:TabStrip(市场(0)/已安装(1),compact)+「刷新」按钮(`E0B5`)+「更多选项…」(`EBAC`)菜单:从缓存重载插件源(`E0B5`)、管理插件源…(`E99A`)、插件更新设置…(`E161`)、分隔线、从本地安装…(`E071`)、分隔线、打开插件文件夹…(`E88D`);加载插件源时工具栏下方显示不确定 ProgressBar(`:532-576`);
- 插件列表项:`48×48` 图标(`margin-right: 6px`)+ 名称(`16px` Medium)+ 描述(`12px`)+ 右侧状态图标组(下载量、Stars、卸载待重启、需重启 `E0BC`、错误 `E808`、有更新 `E1A0`、已安装 `E5AC`,均 `16×16`)+ 操作按钮组(安装/更新 28×28 圆角按钮、进度环 28×28、重启按钮、启用复选框);本地插件项 `opacity` 规则:未加载本地插件 `0.75`、已加载 `1`、市场项 `1`(`:577-756`);
- 空列表模板:居中 `72×72` 贴纸(`光辉矢愿_不可以看.png`)+ 「找不到符合条件的插件,请调整筛选条件后重试」(本地且无插件时改为「还没有安装任何插件,不妨去插件市场看看吧」+「前往插件市场」按钮 `F08B`)(`:461-505`)。
3. **右栏详情**(选中插件):头部 `60×60` 图标(`margin-right: 8px`)+ 名称(`24px` Medium)+ 元信息行「版本 | 项目主页 | 作者 |」+ 状态胶囊(圆角 `999px`、`padding: 10px 3px`):已加载(`SystemFillColorSuccessBackgroundBrush`,`E426`)、已禁用(`CardBackgroundFillColorSecondaryBrush`,`E673`)、错误(`SystemFillColorCriticalBackgroundBrush`,`E673`)、未加载;操作区:已启用复选框、更新按钮(`E1A0`)、移除按钮(`E61D`)、撤销移除(`E625`)、安装(`E0D3`)、重启以应用更改(`E0BD`)、OS 不兼容 Warning InfoBar、下载进度条、下载失败 Error InfoBar(含「更换插件镜像源」链接)(`:30-221`);
- 详情 TabStrip(compact)三个页签:概览(Markdown 渲染插件 README,加载时顶部 3px 不确定进度条)、错误信息(仅异常时显示页签,可选择文本)、依赖项(DataGrid:列「插件 ID」`*`、列「必选?」`80px` 复选框列,`font-size: 14px`)(`:222-293`)。
4. **插件更新设置抽屉**(宽 `320px`):两个复选框「插件有更新时通知我」(`IsPluginsUpdateNotificationEnabled`,默认 `true`,`Settings.cs:1859`)、「自动更新插件」(`IsPluginsAutoUpdateEnabled`,默认 `true`,`Settings.cs:1858`)(`:297-308`)。
5. **管理插件源抽屉**(宽 `500px`):内置插件源镜像下拉(`OfficialSelectedMirror`,默认 `"github"`;镜像字典默认含 `github`→`https://github.com`、`ghproxy`→`https://mirror.ghproxy.com/https://github.com`、`moeyy`→`https://github.moeyy.xyz/https://github.com`,`Settings.cs:171-176`);复选框「忽略 SSL/TLS 证书错误」(`IgnoreSslForPluginMirrors`,默认 `false`);自定义插件源表格(列:Url `4*` 可编辑、镜像 `*` 下拉),上方「添加」「删除」按钮(`:310-384`)。
6. **安装确认对话框**:宽约 400【推断】,内容为插件预览卡片(图标 `48×48` 圆角 `4px`、名称 `16px` Bold、作者 `12px` `opacity: 0.7`、版本 `v{0}`、描述 `12px`),底部警告条「安装未知来源的插件可能存在风险。」(`SystemFillColorCautionBackgroundBrush`、`padding: 10px`、圆角 `6px`),`max-height: 400px` 滚动(`:387-459`)。
7. **拖拽安装遮罩**:整页半透明遮罩(`opacity: 0.75`)+ 居中卡片(`max-width: 420px`、圆角 `12px`、`padding: 20px`、`ControlFillColorDefaultBrush` 背景、`CardStrokeColorDefaultBrush` 边框):`80px` 图标(合法 `E162` / 非法 `EA39`)+ 粗体提示 + 副提示 + 「共 N 个文件,识别到 M 个插件包」(`12px`,`opacity: 0.55`)。解析本地插件时另显示全屏遮罩 + 64×64 进度环 + 「正在解析...」(`:825-882`)。
#### 10.4.15 主题(id `classisland.themes`,图标 `E6AF/E6AE`,External)
源 `ThemesSettingsPage.axaml`。
1. **首次使用警告页**(`IsThemeWarningVisible`,默认 `true`,`Settings.cs:1848`):75×75 应用图标 + `E00C` + 主题图标 `E6AF`(75×75);标题「主题」;正文「欢迎使用主题!主题可以修改甚至完全重塑 ClassIsland 主界面的外观。您可以使用主题功能打造出独一无二的 ClassIsland 界面。」;警告框宽 `320px`(`SystemFillColorCautionBackgroundBrush`,圆角 `4px`,`padding: 8px`)文案必须完整保留(含「主题在加载过程中会转译为可执行代码,可以以 ClassIsland 的身份和权限执行任意代码」);accent 按钮「同意并开始使用主题」(`E424`)(`:309-366`)。
2. **InfoBar**(同意后可见):消息「在启用了支持的主题(如 Fluent)后,您可以在直接【外观】设置中开启【分体主界面】选项使主界面分开。……」,右侧「前往【外观】设置」链接;受 `IsThemeSeparateInfoVisible` 控制可关闭(`:77-84`)。
3. **工具栏**:TabStrip(市场(0)/已安装(1),compact);右侧 4 个透明按钮:刷新市场(`E0B5`)、重载主题(`E0BD`)、打开主题文件夹…(`E88D`)、主题加载顺序(`E13F`)(`:86-112`)。
4. **主题卡片网格**:ListBox + WrapPanel(`item-height: 150px`,行为按目标宽度 `328px` 自动换列)。卡片:圆角 `4px`、边框 `1px`、裁剪溢出;结构 = 横幅图(`UniformToFill`)+ 底部渐变遮罩(不透明遮罩从 `0%`/`65%` 透明到 `100%` 不透明)+ 底部文字区(名称 `16px` Medium + 作者 `12px` + 网站按钮 `E905`)+ 右侧操作(错误 `E808` 红色、更多选项 `EBAC`:打开主题文件夹…/打包主题…/卸载主题/撤销卸载、安装按钮 `E0D2` 28×28 主色圆角 `14px`、更新按钮 `E1A0`、下载进度环 28×28、重启按钮 `E0BC`、启用开关);未加载的本地主题 `opacity: 0.8`(`:120-307`)。
5. **主题加载顺序抽屉**(宽 `300px`):标题「主题加载顺序」,说明「主题将按照以下的顺序加载,后加载的主题可以覆盖前面主题的样式。拖动以对主题进行排序。」;列表项 = 拖拽把手 + 主题名;仅支持垂直拖拽(水平阈值 `33550336` 即禁用、垂直阈值 `3px`)(`:35-67`)。
6. **错误信息抽屉**(宽 `450px`):标题「错误信息」+ 只读等宽文本框(`:24-30`)。
#### 10.4.16 调试(id `debug`,图标 `E2C8/E2C7`,Debug)
源 `DebugPage.axaml`。仅当 `IsDebugOptionsEnabled` 为真且未禁用调试菜单时出现在导航中;页内必须整体受 `DisableDebugMenu` 控制可见性。
1. 顶部不可关闭 Error InfoBar:「仅供调试,除非你能知道您在做什么,请不要使用以下按钮。」(`:17-20`)。
2. 「启用调试菜单」卡片:开关 `IsDebugOptionsEnabled`(默认 `false`,`Settings.cs:79`);子项:调试菜单开关 `IsDebugEnabled`(默认 `false`)、主窗口调试界面开关 `IsMainWindowDebugEnabled`(默认 `false`)。
3. 「调试时间偏移」:日期选择器 + 时间选择器(含秒)+「重置」按钮;修改后必须以 `DebugTimeOffsetSeconds`(默认 `0`,`Settings.cs:112`)驱动全局时间;`DebugTimeSpeed` 默认 `1.0`(`Settings.cs:111`)。
4. 「课程服务」四个可点击行:触发上课、触发下课、启动主计时器、停止主计时器。
5. 「应用设置」三个可点击行:覆盖设置版本、显示插件使用须知(重置 `IsPluginMarketWarningVisible = true`)、显示自动化使用须知(重置 `IsAutomationWarningVisible = true`)。
6. 「功能测试」:转储课表关系图、打开 Markdown 文档、显示测试通知、测试定位、「正在集控管理」开关(只读显示)。
7. 「垃圾回收」:整卡可点击,执行 `GC.Collect()`。
8. 「时间流速」:数值框(`min-width: 102`,绑定 `DebugTimeSpeed`)+ 重置按钮(`E4CC`)。
9. 「危险操作」:崩溃测试、崩溃测试(延迟 3 秒)、崩溃测试(Task)、崩溃测试(扔到栈顶)、`Environment.FailFast();`(`:22-130`)。
#### 10.4.17 关于 ClassIsland(id `about`,图标 `E9E4/E9E3`,About)
源 `AboutSettingsPage.axaml`。必须实现为「吸顶大图 + 卡片组」:
1. **头部**:StickyScrollViewer(折叠后高度 `58px`),头部内容最大高度 `300px`,横幅图 `BannerMain_App2.png` 居中最大宽 `960px`,底部叠加标题「关于 ClassIsland」(`TitleTextBlockStyle`);头部背景为 `#0d1111`,左右各一块径向渐变(`#ff0c0e1f` → `#ff0d1111`),渐变光晕出现动画:延迟 `250ms`、时长 `200ms`、曲线 `cubic-bezier(0,0,0,1)`,透明度 `0 → 1`;横幅文字区不透明度随滚动由 `StickyScrollViewer.HeaderFadingOpacity` 渐隐(`:117-186`)。
2. 四张卡片(默认全部展开):
- 「ClassIsland」卡片:`AppLogo.png` 图标,右侧显示版本号(`12px`,次级色);子项正文「本项目基于 GNU General Public License v3.0 获得许可。」+「开放源代码许可」「隐私政策」超链接(超链接 `padding: 4px`、`margin: 2px`)+「Copyright (c) 2023-{当前年份} HelloWRC」;子项「常用链接」:项目主页、帮助文档、GitHub、加入社区;
- 「遇到问题?」卡片:问题反馈/功能建议、功能投票、查看诊断信息(诊断信息抽屉 `550×400`,等宽字体只读文本框);
- 「鸣谢」卡片:贡献人员(抽屉宽 `300px`,含说明、刷新按钮、名单列表:头像 `32×32` 圆角 `16px` + 姓名 `15px` + 贡献 `11px` Light)、第三方库(抽屉宽 `300px`,列表项由图标 `E07D`、包名 `14px` Bold、网站/许可证链接组成;许可证文本抽屉为等宽只读文本框);
- 「回声洞」卡片:可点击行内逐字打字机效果显示随机文案(点击后重播)(`:188-309`);
- 底部居中文案(`13px`,次级):「谨以此应用,献给现在或曾经陪伴着我的同学们。愿时光流转,各位能与所念之人重逢。」(`:311-316`)。
#### 10.4.18 集控 / 集控凭据 / 集控策略(id `management`、`management.credentials`、`management.policy`,About,`hideDefault`)
源 `ManagementSettingsPage.axaml`、`ManagementCredentialsSettingsPage.axaml`、`ManagementPolicySettingsPage.axaml`。
1. **集控页**三组卡片:信息卡(标题「集控」+ 说明;受管时追加「您目前正在受 {OrganizationName} 的管理。」)、「加入管理」(未受管时显示;点击打开 `JoinManagementDialog`)、「了解更多」(跳转文档);「管理」卡(未受管时显示):「密码保护设置…」→ `classisland://app/settings/management.credentials?ci_keepHistory=true`、「限制策略…」→ `classisland://app/settings/management.policy?ci_keepHistory=true`;「客户端信息」卡:CUID(只读)、受管时显示 ID;受管时另显示「服务端信息」卡(服务端类型)(`:17-115`)。
2. **集控策略页**(必须整页在未通过「编辑策略」授权时保持锁定:`IsEnabled` 取 `IsLocked` 的反):
- InfoBar:「部分策略设置可能需要重启才能生效。」+「重启应用」按钮;
- 分区「档案」(图标 `E699`):禁止编辑档案(`DisableProfileEditing`)、禁止编辑课表(`DisableProfileClassPlanEditing`)、禁止编辑时间表(`DisableProfileTimeLayoutEditing`)、禁止编辑科目(`DisableProfileSubjectsEditing`),说明文案必须保留「临时换课和启用临时课表功能不受影响。」;
- 分区「应用设置」(图标 `EF27`):禁止编辑应用设置(`DisableSettingsEditing`)、禁止自定义启动加载界面(`DisableSplashCustomize`)、禁用调试菜单(`DisableDebugMenu`);
- 分区「杂项」(图标 `EBAA`):允许退出集控(`AllowExitManagement`)、禁用彩蛋(`DisableEasterEggs`)(`:17-111`)。
3. **集控凭据页**:
- 分区「认证」(图标 `EAF2`):「用户认证方式」「管理员认证方式」两个 `CredentialEditControl`(可增删认证方式、可拖拽排序);
- 分区「操作授权等级」(图标 `E350`):8 行「左标签 + 右下拉(无/用户/管理员)」,标签与绑定依次为:编辑授权设置(`EditAuthorizeSettingsAuthorizeLevel`)、编辑策略(`EditPolicyAuthorizeLevel`)、退出集控(`ExitManagementAuthorizeLevel`)、编辑课表(`EditProfileAuthorizeLevel`)、编辑应用设置(`EditSettingsAuthorizeLevel`)、换课(`ChangeLessonsAuthorizeLevel`)、退出应用(`ExitApplicationAuthorizeLevel`);图标依次 `E350/E2B4/E2B8/E6BB/EF27/E15F/E0DF`(`:19-99`)。
#### 10.4.19 出错啦 / 404 页(id `_error`,About,`hideDefault`)
源 `ErrorSettingsPage.axaml`。居中布局(`spacing: 8px`):`80×80` 贴纸(`帕姆_哭哭.png`,缺失回退 `E6BF` 80px)→ 导航异常时显示「欧呦,出错啦!」(Subtitle)+「在尝试导航到请求的的页面时发生了一个未知错误,详细请见应用日志。」(13px);页面不存在时显示「404」(Subtitle)+「找不到请求的页面。」(13px)(`:11-34`)。
### 10.5 设置持久化、缓存与叠层(行为级要求)
1. **即时保存**。任何设置项的值发生变化后,必须**立刻**把整个配置(`config.json`,ClassIsland 对应 `Settings.json`)写盘(ClassIsland 在每个属性 setter 触发 `OnPropertyChanged` 后调用 `SaveSettings(propertyName)`,见 `repos/ClassIsland/ClassIsland/Services/SettingsService.cs:261-280`、`:171-179`)。实现时:Pinia store 的每个 action 必须在修改状态后调度一次持久化(Electron 主进程写 `userData/config/Settings.json`),允许 `requestAnimationFrame` 级别的合并,但**不得**设置超过 300ms 的防抖【推断:300ms 为工程上限,源码为同步写盘】。
2. **禁止自动保存的字段**。标注 `[JsonIgnore]` 的派生字段(如 `IsSentryEnabled` 实际存于本地全局存储、`ShowSellingAnnouncement`)必须写入独立的 `localStorage`/`global.json`,不得写进 `Settings.json`(`Settings.cs:518-535`、`:2615-2628`)。
3. **页面缓存策略**。`SettingsPagesCachePolicy`:`0` = 不缓存页面实例;`1` = 仅在设置窗口打开期间缓存;`>=2` = 跨打开会话缓存【推断:源码仅在 `>=1` 缓存、`<=1` 时关闭窗口清空,`2` 语义由此推得】(`Settings.cs:213`、`SettingsWindowNew.axaml.cs:425-428`、`:513-516`)。实现时映射为 Vue 的 `<keep-alive :include>` 控制:`0` 不缓存该页组件;`1` 缓存到窗口关闭;`2` 缓存到进程结束。
4. **设置叠层(SettingsOverlay)**。必须实现「叠层」机制以支持自动化/集控临时改设置:`SettingsOverlays` 为 `Dictionary<propertyName, OrderedDictionary<key, value>>`,JSON 键名必须序列化为 `SettingsOverlay`(旧版兼容,`Settings.cs:263-273`)。`AddSettingsOverlay(guid, name, value)` 的语义:首次写入时把当前值保存到键 `"@"`,随后写入 `guid` 键并立即应用;`RemoveSettingsOverlay(guid, name)` 移除后必须以集合中最后一个值回滚属性,集合仅剩 1 项(即只剩 `"@"`)时删除整个属性条目(`SettingsService.cs:191-259`)。任何用户在设置界面手动修改某属性时,必须移除该属性的全部叠层(同文件 `:261-266`)。
5. **管理员策略限制**。策略值来自集控清单(本地无集控时来自 `Management.json`),设置页必须在渲染时读取;对禁止编辑的页面必须整页禁用(如提醒页 `IsEnabled = !DisableSettingsEditing`,`NotificationSettingsPage.axaml:44`),而不是仅隐藏控件。策略项与设置项冲突时,以策略为准。
6. **重启类设置**。至少以下设置必须标记为「需要重启」并触发重启提示:`AnimationLevel`、`IsWaitForTransientDisabled`(`GeneralSettingsPage.axaml.cs:37-44`)、渲染模式、使用系统标题栏、兼容缩放模式(`AdvancedSettingsPage`,源码通过 ViewModel 写入设备级配置;重启需求为【推断】)。实现时必须提供统一的 `requestRestart(reason)` 接口。
7. **不存在的功能**。源码中**没有**设置搜索、没有「恢复默认值」全局按钮、没有设置项级重置按钮。实现者不得编造;如需增加,必须作为 P2 可选增强并在文档中标注。
8. **文案常量集中**。设置窗口与全部设置页的中文文案必须集中放在常量文件(固定路径 `src/renderer/constants/texts.zh-CN.ts`,按页面分节,与 part 11 的其它窗口共用同一文件),组件内只引用常量键、不得散落中文字面量;文案内容以本 part 与 part 11 给出的中文原文为准,后续接入 i18n 时以该常量文件为唯一来源。
### 10.6 键盘与交互细节
1. `Esc`:优先关闭打开的抽屉;无抽屉时关闭「更多选项」菜单;再无效时不做任何事(不得关闭设置窗口)。
2. 后退:`Alt+←` 不做映射(源码无此绑定)【推断:FluentAvalonia NavigationView 默认无该快捷键】;后退仅通过标题栏/页面内返回按钮触发。
3. 拖动排序(组件行、提醒提供方、自动化、主题顺序)必须实现「水平方向禁用」的排序行为:`HorizontalDragThreshold = 33550336`、`VerticalDragThreshold = 3`,即只允许垂直拖动(多页一致,如 `NotificationSettingsPage.axaml:92-102`、`ThemesSettingsPage.axaml:54-65`)。
4. 悬停提示必须使用 `ToolTip`,延迟约 400ms【推断】;滑块拖动时必须实时显示数值 tooltip。
### 10.7 验收标准
实现完成后,必须逐条自测并全部通过:
1. 设置窗口首次打开宽 `1240`、高 `550`;把宽度拖到 `799` 时进入压缩导航,拖到 `800` 时恢复,最大化时导航始终展开。
2. 左侧导航顺序与本文 10.2 第 4 条完全一致;「通用」与「主界面」为可展开分组,分组之间与分类块之间存在分隔线;「关于」块排在最后且不出现「集控凭据设置/集控策略/出错啦」。
3. 打开「外观」页,把「背景不透明度」拖到 `0.30` 后关闭设置窗口再重开,值仍为 `0.30`,且 `config.json` 中 `opacity` 为 `0.3`(即时保存生效)。
4. 打开「组件」页,能从组件库拖拽一个组件到某一行、能插入行、能删除行、能切换「主要行/提醒」开关,且变更立即写入组件配置。
5. 打开「提醒」页,窗口宽度 `< 800` 时变为单栏、选中提供方后进入详情并可返回。
6. 打开「天气」页,点击「编辑城市」弹出自右侧宽 `320px` 的抽屉,`Esc` 可关闭,遮罩点击可关闭,动画时长 `250ms`。
7. 打开「主题」首次警告页点「同意并开始使用主题」后,警告页不再出现且主题网格可加载。
8. 触发任何「需要重启」的设置后,标题栏出现「需要重启」按钮,点击弹出标题「需要重启」的对话框,「重启」按钮为默认按钮。
9. 管理策略勾选「禁止编辑应用设置」并重启后:非关于类设置页从导航消失,窗口默认落在「关于 ClassIsland」。
10. 所有数值与枚举必须与本文列出的默认值一致(可用 `config.json` 全文比对;每一处偏差都算失败)。
---
## 11. 其它窗口与向导(闪屏 / OOBE / 崩溃 / 恢复 / 更新 / 换课 / 导入导出 / 迁移 / 教程 / 授权 / 日志)
本节规定除主界面(岛)与设置窗口之外的全部用户可见窗口。实现技术栈为 ClassHelper desktop-client 的 Electron 多窗口模型:**每个窗口必须是一个独立 BrowserWindow + 独立渲染入口**(或同一个渲染进程内的独立路由 + 独立 BrowserWindow,二者择一但同一类窗口必须一致),窗口参数、模态关系、键盘行为、关闭行为必须完全按本节实现。所有窗口必须复用统一窗口外壳组件(对应 ClassIsland 的 `MyWindow`):Windows 11 21H2+ 且支持 WinUI Composition 时启用 Mica(背景透明 + `windowTransparencyLevel: 'mica'`),否则使用纯色背景 `SolidBackgroundFillColorBaseBrush`;开发版/开源水印以绝对定位浮层实现(`repos/ClassIsland/ClassIsland.Core/Controls/MyWindow.cs:215-229`、`:178-198`)。
### 11.1 通用窗口规则(所有窗口必须遵守)
1. **标题栏**。除显式声明「无边框/透明」的窗口外,一律使用自定义标题栏,高度 `48px`(与设置窗口一致;`SettingsWindowNew.axaml.cs:135`)。窗口标题必须写入窗口标题属性(用于任务栏与 Alt+Tab)。
2. **模态**。设置、欢迎向导、数据迁移、Excel 导入/导出、授权、日志等窗口必须作为「主窗口的模态/非模态子窗口」处理:非模态窗口(设置、日志、教学中心、档案编辑)关闭时为 `hide()`;模态窗口(欢迎向导、数据迁移、更新进度、任务对话框)关闭时真正 `destroy()`,并把结果回传调用方。
3. **默认快捷键(所有窗口)**:`F3` 循环切换渲染调试浮层(无 → FPS → FPS+布局/渲染耗时),`Shift+F3` 直接进入第二级;`F6` 切换触摸模式(`Shift+F6` 同时抑制自动触摸判定);`F7` 关闭当前窗口全部 Toast(`MyWindow.cs:115-166`)。实现者必须在每个窗口注册同样的开发者快捷键(生产构建可移除)。
4. **Toast/通知条**。所有窗口必须支持右上/底部 Toast 队列(标题、正文、级别:灾难/错误/警告/信息/成功、是否可关闭、是否自动关闭);Toast 必须以 Adorner 形式浮于窗口内容之上,不改变布局(`MyWindow.cs:185-188`)。
5. **对话框**。普通确认类对话框必须使用 Fluent 风格 ContentDialog:标题、正文、主按钮、次按钮、关闭按钮、默认按钮;内容宽度约 `320–548px`【推断:FluentAvalonia 默认】,居中于父窗口并带遮罩。需要展示 Markdown/复杂内容时使用 TaskDialog(带标题栏、按钮列表、可勾选扩展区)。
6. **文案常量集中**。全部窗口文案必须集中存放在常量文件(固定路径 `src/renderer/constants/texts.zh-CN.ts`,按窗口分节),组件内只允许引用常量键、不得散落中文字面量;文案内容以本节给出的中文原文为准,后续接入 i18n 时以该常量文件为唯一来源。
### 11.2 欢迎向导(OOBE)`WelcomeWindow`
源:`Views/WelcomeWindow.axaml:14-20`、`WelcomeWindow.axaml.cs:62-243`、`Views/WelcomePages/*`。
1. **窗口参数**:`800×600`,`CenterScreen`,`CanResize=false`,`ShowAsDialog=true`(模态体验),启用 Mica,标题「ClassIsland」,Linux 下窗口类型必须为 `Normal`(`:14-20`)。内容区必须只有一个导航 Frame,无标题栏文字、无额外 chrome。
2. **页面序列**(必须支持三种启动模式,并按条件裁剪页面;`WelcomeWindow.axaml.cs:62-90`):
- 完整 OOBE(首次启动,`IsOnboarding=true` 且非翻新):欢迎页 → 许可条款 → 翻新页 → 基本页 → 颜色主题页 → 外观页 → 系统页 → 完成页(共 8 页);
- 翻新模式(`isRefreshing=true`):保留翻新页,去掉欢迎页/许可页/系统页;
- 翻新完成回访(`isRefreshCompleted=true`):同时去掉翻新页;
- 非 OOBE 且非翻新:许可页/欢迎页/系统页/翻新页全部去掉,仅剩基本/颜色/外观/完成。
3. **导航命令**:前进/后退/完成必须是三个路由命令;前进时索引 `min(index+1, count-1)`,后退时 `max(index-1, 0)`(`:126-142`)。所有页面共享同一个 ViewModel(设置、快捷方式选项、翻新范围),页面切换时不得重建状态。
4. **欢迎页(WelcomePage)**:整页 `Grid` 三行(`*, Auto, *`)。第 0 行:应用图标 `80×80`,底部对齐、距文字 `8px`;第 1 行:「ClassIsland」11 个字母逐字水平排列,`font-size: 32px`、`font-weight: Medium`,字间初始 `4px` 间距;第 2 行:主按钮(图标 `E130`,`48×48`、圆角 `24px`、accent,tooltip「下一步」)居中,下方 `margin: 8px`;页面底部居中显示版本号(`opacity: 0.75`)+ 三个超链接「数据迁移」「故障排除/恢复备份」「加入管理」(超链接 `padding: 4px`)(`WelcomePages/WelcomePage.axaml:26-101`)。
5. **欢迎页开场动画(必须逐帧还原)**:页面加载后先播放 OOBE 片头动画(`OobeIntroAnimationControl`),动画结束后页面内容容器才以 `200ms` 淡入(透明度 0→1,`WelcomePage.axaml:11-25`、`.axaml.cs:41-44`)。片头动画规格(`Controls/OobeIntroAnimationControl.axaml:13-144`、`.axaml.cs:25-165`):
- 结构:11 个 `32×32` 方块(间距 `4px`、背景 `ControlStrongFillColorDefaultBrush`、初始 `opacity: 0`、`translateY(50px)`、`transform-origin: 0 12px`)与 11 个字母(`font-size: 32px`、Medium、`min-width: 32px`、初始 `opacity: 0`、3D 翻转 `rotateX`)叠放在同一中心位置;
- 方块入场:逐个出现,延迟按 `delay_i = sin((i+2)/(n+2) * π/2) * 500 / n` 毫秒(n=11)计算;每个方块动画时长 `delay*9*1.5+750ms`,关键帧:`0%` 透明且下移 `50px` → `delay*9 ms` 时完全不透明且归位(`cubic-bezier(0.25,1,0.5,1)`)→ 停留 `+750ms` → 随 `rotateX 0°→90°` 翻出并消失(`cubic-bezier(0.32,0,0.67,0)`);第 6 个方块(索引 6)必须改成「宽度放大到 `2.17` 且变 `#00bfff`」的特殊高亮动画(`:44-58`、`:65-124`);
- 字母入场:与方块交错,单个字母动画时长 `delay*9*2+750ms`,在方块翻出后由 `rotateX(-90°)` 翻入(`cubic-bezier(0.33,1,0.68,1)`);
- 收尾:全部字母出现后,字母容器的字间距从 `4px` 过渡到 `0`、每个字母 `min-width` 从 `32px` 过渡到 `0`,时长 `700ms`、曲线 `cubic-bezier(0.33,1,0.68,1)`,随后整页 `opacity 0→1`、`scale 1.25→1.0`(时长 `3.0s`,曲线 `cubic-bezier(0,0.83,0.17,0.90)`);
- 动画期间必须禁用「下一步」交互【推断:源码未显式禁用,但按钮在动画下层】。
6. **许可条款页(LicensePage)**:页面内容 `max-width: 600px`、居中;标题「同意许可条款」(Subtitle)+ 说明「要继续使用 ClassIsland,您必须阅读并同意以下许可条款。」;两个复选框,文案分别为「我已阅读并同意《ClassIsland 开源许可》。」「我已阅读并同意《ClassIsland 隐私政策》。」,书名号部分是超链接,点击打开许可/隐私对话框;主按钮(`48×48` 圆角 24 accent,「下一步」)**仅在两个复选框都勾选时可用**(`WelcomePages/LicensePage.axaml:9-43`)。
7. **翻新页(RefreshingPage)**:标题「翻新」+ 说明「选择要重置的内容。选中的内容将被重置至初始状态,重置前应用将创建一个备份,以便还原至重置前的状态。」;5 个复选框,均带图标与描述:档案(`E699`,「包含课表、时间表等信息。」)、课表(`E6B1`,缩进 `22px`,仅当「档案」未勾选时显示)、时间表(`F0EB`,缩进 `22px`,同上)、组件配置(`E06F`,「主界面显示的内容。」)、自动化(`EEF1`,「应用在特定情况下自动执行的内容。」)、插件(`E071`,「可以扩展应用的功能。」);「档案」未勾选时另显示超链接「选择要保留的课表和时间表…」;执行翻新期间所有复选框禁用,并显示「正在翻新,请稍后…」+ 不确定进度条(`WelcomePages/RefreshingPage.axaml:15-90`)。
8. **基本页(GeneralPage)**:`max-width: 600px`;标题「基本」+「配置 ClassIsland 的基本设置。」;两条设置:「学期开始时间」(日期选择器 `min-width: 120px`,绑定 `SingleWeekStartTime`)、「匿名使用情况分析」(开关,绑定 `IsSentryEnabled`,说明与隐私页一致);主按钮「下一步」(`WelcomePages/GeneralPage.axaml:11-49`)。
9. **颜色主题页(ColorThemePage)**:标题「颜色主题」+「设置 ClassIsland 的主题外观。」;横向 ListBox 三张主题预览卡(每项 `margin: 0 8px 0 4px`,预览图宽 `144px`、高约 `75px`):第一张为「跟随系统」(左半暗色 `ThemeDemo` 宽 `144` 裁剪到 `0..72`,右半亮色裁剪到 `72..144`),第二张「明亮」(整幅亮色),第三张「黑暗」(整幅暗色);每张卡下方有 RadioButton 文案「跟随系统/明亮/黑暗」,选中项对应 `Theme`=0/1/2;底部「上一步」「下一步」两个 `48×48` 圆角按钮(`WelcomePages/ColorThemePage.axaml:22-79`)。
10. **外观页(AppearancePage)**:标题「外观」+「调整 ClassIsland 主界面的外观。」;三条设置:界面缩放(滑块 `0.75–2.5`,步进 `0.05`,宽 `180px`,`F2`)、背景不透明度(`0–1`,步进 `0.05`,`F2`)、圆角半径(`0–20`,步进 `0.5`,`F1`);底部「上一步」「下一步」(`WelcomePages/AppearancePage.axaml:12-79`)。
11. **系统页(SystemPage)**:标题「系统」+「设置ClassIsland的系统集成功能,这些功能可以优化您使用本软件的体验。」;复选框(`padding: 8px 0`,含图标与描述):开机自启(`E342`,macOS 隐藏)、创建桌面快捷方式(`E62F`,macOS 隐藏)、创建开始菜单快捷方式(`E064`,deb 包与 macOS 隐藏)、注册 Url 协议(`EAB0`,仅 Windows)、「在桌面创建换课快捷方式」(`E15F`,仅 Windows,缩进 `22px`,且仅在勾选「注册 Url 协议」时可用);底部「上一步」「下一步」(`WelcomePages/SystemPage.axaml:16-100`)。
12. **完成页(FinishPage)**:`Carousel` 宽 `600px`,三页:① 「点击任务栏托盘图标可以打开应用主菜单。」+ 截图 `Demo_MainMenu.png`;② 「要编辑课表和时间点,请在应用主菜单中点击【编辑档案】。」+ 截图 `Demo_OpenProfileSettingsWindow.png`;③ 85×85 贴纸(`白厄_再见.png`,回退 `E31A`)+「完成」+「恭喜!应用的基本设置已完成,感谢您选用 ClassIsland。点击【完成图标】以结束设置向导。」。Carousel 页面切换必须使用「水平滑动 `300ms` `cubic-bezier(0.76,0,0.24,1)` + 交叉淡入 `300ms`」的复合过渡;底部按钮:非最后一页显示「上一步」「下一步」,最后一页显示「上一步」+「完成」(图标 `E423`,`48×48` 圆角 24 accent)(`WelcomePages/FinishPage.axaml:14-98`)。
13. **完成行为**:点击完成必须依次执行:置 `CanClose=true`、`IsWizardCompleted=true`;若为 OOBE:在 Windows 创建开机启动/开始菜单/桌面快捷方式(按系统页勾选)并注册 Url 协议,Linux 写 `~/.config/autostart/cn.classisland.app.desktop`、`~/.local/share/applications/cn.classisland.app.desktop`、桌面 `ClassIsland.desktop` 并复制图标;重置教程完成状态;关闭 `RefreshingToastIsOnboardingGuide`;若为翻新:清零剩余通知次数;最后关闭窗口(`WelcomeWindow.axaml.cs:186-218`)。
14. **关闭保护**:OOBE 模式下用户点关闭必须弹出模态对话框:标题「退出 ClassIsland」,正文「您需要完成设置才能开始使用本应用。关闭此窗口将直接退出应用。」,主按钮「退出」,次按钮「取消」;确认后关闭窗口并退出应用(`.axaml.cs:220-243`)。
15. **欢迎页底部三个入口**:「数据迁移」→ 把向导页面序列替换为 [欢迎页, 许可页, 数据迁移页] 并前进;「故障排除/恢复备份」→ 以参数 `-m -r` 重启到恢复模式;「加入管理」→ 打开 `JoinManagementDialog`(`WelcomePage.axaml.cs:46-69`)。
### 11.3 启动闪屏 `SplashWindow`
源:`Views/SplashWindow.axaml:1-100`、`.axaml.cs:41-130`、`Services/SplashService.cs`。
1. **窗口参数**:`400×100`,`CenterScreen`,`Topmost`,`SystemDecorations=None`,背景 `Transparent`,`ShowInTaskbar=false`,`ShowActivated=false`,`TransparencyLevelHint=AcrylicBlur,Transparent`,强制暗色主题;窗口圆角约 `8px`【推断:Fluent 默认】,内容容器 `padding: 17px`、背景 `#88111111`、`border: 1px` 描边(`ControlStrokeColorOnAccentDefaultBrush`)。
2. **布局**:4 行 `*, 8, *, Auto`。第 0 行:Logo(`height: 24px`,`margin-right: 8px`)+「ClassIsland」文字(`20px` Medium,仅当未自定义 Logo 时显示;自定义 Logo 时只显示图片);第 2 行:`ProgressRing` `24×24` + 状态文本(`16px` Regular,绑定 `SplashStatus`);第 2 行左下角叠加版本号(`9px`,`opacity: 0.5`,`margin: -16px 0 0 -14px`);第 3 行:`400×4` 白色进度条(`margin-bottom: -17px`)。
3. **入场动画**:Logo 与状态行分别做「从右侧 `32px` 滑入 + 淡入」:位移 `300ms` `ExponentialEaseInOut`(`cubic-bezier(0.87,0,0.13,1)` 近似【推断】),透明度 `300ms` `CubicEaseOut`;状态行延迟 `150ms`,Logo 延迟 `0ms`(`.axaml.cs:53-54`、`:101-121`)。
4. **进度条动画**:进度值 `p`(0–100)变化时,以合成动画把白色填充条的 `X` 偏移从当前值补间到 `-400*(1-p/100)`;时长 = `max((p - 上次p)/8, 0.5)` 秒;结束(`EndSplash`)时以 `150ms` 补间到 100%(`.axaml.cs:64-82`、`:123-130`);缓动 `CubicEaseOut`。
5. **文案**:默认「正在启动…」;用户自定义 `SplashCustomText` 非空时必须用它替换;仅当 `ShowDetailedStatusOnSplash=true` 时才允许用详细启动阶段文案覆盖(`SplashService.cs:27-33`、`:89-92`)。集控策略 `DisableSplashCustomize` 为真时必须清空自定义 Logo/文字/详细状态。
6. **启用条件**:仅当 `Settings.IsSplashEnabled=true`(默认 `false`,`Models/Settings.cs:115`)时显示;该窗口不是主窗口,必须由主进程在应用初始化最早阶段创建,并在主窗口显示后销毁。
### 11.4 崩溃窗口 `CrashWindow`
源:`Views/CrashWindow.axaml:1-99`、`.cs:16-86`。
1. **窗口参数**:`800×450`,`CenterScreen`,启用 Mica,标题「ClassIsland 崩溃报告」,图标 `AppLogo.ico`。
2. **布局**(`Grid margin: 12px`,6 行 2 列):
- 头部:`48×48` 贴纸(`光辉矢愿_不可以看.png`,回退 `E809`),`margin: 0 -8px 0 0`;标题 `TitleTextBlockStyle`「ClassIsland 发生错误 ヽ(*。>Д<)o゜」(禁用彩蛋时改为「ClassIsland 发生错误」),`margin: 8px 0 16px 16px`;
- 正文(`14px`,可换行):「ClassIsland 碰到了严重错误而无法正常运行。您可以保存下方的错误信息并向他人寻求帮助。如果您认为这是由软件本身的错误所致,请点击下方“反馈问题”按钮。」
- 错误信息:只读多行文本框,`font-size: 13px`、无边框、`padding: 0`、顶部对齐(`TextBoxCrashInfo`);
- 当异常不可忽略时显示 Error InfoBar:「这个异常无法被忽略,您需要重启或退出应用。很抱歉对您的使用造成不便。」;
- 分隔线(`margin: -12px 8px`);
- 底部工具条:背景 `LayerOnMicaBaseAltFillColorDefaultBrush`,`margin: -12px 0 -12px -12px`,左右 `padding: 12px`。左侧按钮:调试(`E2C8`,仅 critical 时显示,点击 `Debugger.Launch()`)、忽略(`E671`,仅在允许忽略时显示,点击关闭窗口)、退出应用(`E0DF`)、重启应用(`E0BD`,accent);右侧按钮:复制(`E58B`,全选并复制错误信息)、反馈问题(`ED11`)。
3. **反馈问题**:必须打开浏览器到 `https://github.com/ClassIsland/ClassIsland/issues/new`,并带查询参数 `template=BugReport.yml&stacktrace=<URL 编码的崩溃信息>&app_version=<版本>&os_version=<系统版本>`(`.cs:66-77`)。
4. **退出语义**:critical 时「退出应用」必须调用 `Environment.Exit(1)`;非 critical 时走正常退出流程(`.cs:42-52`)。
### 11.5 恢复模式 `RecoveryWindow` 与恢复页
源:`Views/RecoveryWindow.axaml:12-21`、`Views/RecoveryPages/HomePage.axaml:1-85`、`RecoverBackupPage.axaml:1-45`。
1. **进入方式**:以命令参数 `-m -r` 重启(等价于「重启到恢复模式」);恢复模式窗口为主窗口:`450×600`,`CenterScreen`,标题「恢复」,图标 `AppLogo.png`,内容区为导航 Frame(`CacheSize=0`)。
2. **主页(HomePage)**:`margin: 8px`,纵向间距 `6px`;标题「选择一个选项」+「选择一个操作以继续。」(`margin: 0 2px 10px`)。三组「卡片组」(子项间距 `1px`、首尾圆角 `4px`),全部为可点击行(`SettingsExpanderItem`),右侧统一 `E44E` 箭头图标:
- 第 1 组:继续使用 ClassIsland(`E131`);以安全模式继续使用 ClassIsland(`E131`,描述「禁用所有插件和自动化功能。」);以诊断模式继续使用 ClassIsland(`E131`,描述「在日志中记录详细的信息,并启用控制台输出。」);打开日志目录(`E889`,`EC2E`);
- 第 2 组:恢复备份(`E195`,描述「从先前的备份恢复处于正确状态的配置文件。」);回滚(`E135`,描述「回滚到之前的 ClassIsland 版本」,默认禁用);
- 第 3 组:清除临时文件(`E623`,描述「删除所有缓存和临时文件。」);重置应用设置(`E0BD`,描述「重置应用的设置信息,不影响档案、组件、自动化、插件等的配置。」);重置全部配置(`E6B9`,描述「重置应用的所有配置,包括应用设置、组件配置、自动化配置、插件设置等,不影响档案配置。」);重置全部信息(`E12F`,描述「重置应用的所有信息,恢复到安装前的状态。」)。
3. **恢复备份页(RecoverBackupPage)**:三行布局:顶部 `margin: 8px` 一行:返回按钮(图标 `E109`,tooltip「返回」,`margin-right: 16px`,working 时禁用)+ 标题「选择要恢复的备份」;中部为备份列表(`IsEnabled` 随 working 反转),working 时列表顶部叠加不确定进度条;底部(`margin: 8px`):两个单选按钮「增量恢复:替换备份中已有的文件,保留新添加的文件」(默认,值 0)与「完整恢复:完整恢复到选定备份,不保留新添加的文件」(值 1,`margin-top: 2px`);右下 accent 按钮「恢复到选定备份」(图标 `E195`),未选择备份时禁用。
4. **安全/诊断模式标识**:以安全模式启动时,主界面与标题必须标明当前处于安全模式;诊断模式必须写出更详细日志并附带控制台输出【推断:由启动参数控制,UI 标识在源码中未直接可见】。
### 11.6 更新相关窗口与流程
1. **更新进度窗口 `UpdateProgressWindow`**(`Views/UpdateProgressWindow.xaml:8-38`):`300×100`,`WindowStartupLocation=Manual`、初始位置 `left: 50, top: 50`(主进程可再调整到屏幕角落),标题「正在更新 ClassIsland…」,`ResizeMode=NoResize`,背景 `SolidBackgroundFillColorBaseBrush`,字体 `HarmonyOS Sans` 14px。内容 `margin: 16px`,两行:第一行两行文本「正在更新 ClassIsland……」+ 动态进度文本(`ProgressText`);第二行不确定进度条(`margin-top: 8px`)。关闭行为必须被拦截(`Closing` 事件取消关闭),防止更新中途被关掉。
2. **自动更新模式**(`Settings.UpdateMode`,默认 `3`,`Models/Settings.cs:77`):
- `0`:只允许手动检查;
- `1`:启动时自动检查,发现更新只提示;
- `2`:自动检查并自动下载;
- `3`:自动检查、下载并安装(下次启动生效)。
对应设置页见 part 10.4.13。
3. **更新状态机**(`UpdateStatus`):已是最新 / 有更新 / 已下载 / 已部署;按钮与文案切换规则见 part 10.4.13。更新包校验必须使用 `UpdateArtifactHash`(默认空,`Models/Settings.cs:157`),并按通道元数据公钥校验签名【推断:由 `UpdateService` 实现,源码见 `Services/UpdateService.cs`】。
4. **部署后提示**:部署完成必须通过 InfoBar 提示「更新将在重启后应用」,并允许「重启并应用更新」;`AutoInstallUpdateNextStartup` 默认 `true`(`Models/Settings.cs:78`),即下次启动自动安装。
### 11.7 换课窗口 `ClassChangingWindow`
源:`Views/ClassChangingWindow.axaml:1-273`、`.cs:53-192`。
1. **窗口参数**:`600×250`,`MinWidth=730`、`MinHeight=175`,`SizeToContent=WidthAndHeight`,`CenterScreen`,启用 Mica,标题「换课」,图标 `AppLogo.png`。根 Grid `MinWidth=730`、`MinHeight=175`。
2. **顶部标题条(AppBar)**:`padding: 8px`,内含「换课」标题(`TitleTextBlockStyle`,Medium,`margin-right: 16px`)+ 目标课表按钮(图标 `E6B1` + 课表名,tooltip「要换课的课表」)+ 箭头图标 `E131` + 操作提示文本。提示文本随步骤切换:第 0 步「选择要调换的课程。」;第 1 步「选择要与刚刚选择的课程调换的课程。」(`:219-256`)。
3. **主体 Carousel**(两页):过渡必须为「水平滑动 `300ms` `cubic-bezier(0.76,0,0.24,1)` + 交叉淡入 `300ms`」(`:52-60`)。
- 第 0 页:`LessonsListBox` 居中(当前课表全部课程),`margin: 0 8px`,`SelectionChanged` 后若合法值则自动进入第 1 步;
- 第 1 页:`margin: 8px`,三列 `Auto, Auto, *`:左列为课程卡片(`LessonControlExpanded`,`height: 40px`,展示当前选中的课程/时间点/科目);中列为「替换 / 交换」TabStrip(`compact toggle`;选中「替换」= `IsSwapMode=false`,选中「交换」= `IsSwapMode=true`);右列为二级 Carousel(垂直滑动 `300ms` 同曲线 + 交叉淡入):「替换」页 = 字段「换课成…」+ 科目下拉(`min-width: 100px`);「交换」页 = 第二个课程选择列表(`:61-140`)。
4. **底部工具条**:背景 `LayerOnMicaBaseAltFillColorDefaultBrush`,左右 `margin: 6px`。左侧:取消(图标 `E671`,`IsCancel`,tooltip「取消换课操作。」)、上一步(`E109`,仅第 1 步可见)、超链接「高级换课...」(tooltip「打开调课界面,支持跨天的换课选项和更复杂的调课方案。」→ 打开档案编辑窗口的 `classisland://app/profile/adjustment` 并关闭本窗口)。右侧:下一步(`E131`,accent,仅在第 0 步且已选中课程时显示)、复选框「永久换课」(`margin: 4px -8px`;tooltip「如果选择,将不会在换课时创建临时层课表,而会直接应用到原课表上。」;仅当课表不是临时层时显示;受 `DisableProfileTimeLayoutEditing`/`DisableProfileEditing` 策略禁用)、「确认换课」按钮(`E424`,accent,`margin-left: 12px`)(`:146-216`)。
5. **交互与数据逻辑**:
- 选择课程时必须把行索引换算成「上课时间点索引」(仅统计 `TimeType==0` 的上课项),换算失败必须 Toast 警告「选择的课程无效。」(`.cs:45-73`);
- 「交换」模式:互换两节课的 `SubjectId`;「替换」模式:把第一节课的 `SubjectId` 改为目标科目,目标科目为空则直接返回(`.cs:136-157`);
- **临时层语义**:未勾选「永久换课」且当前课表不是临时层时,必须先检查是否已存在临时层课表:若存在,弹出确认对话框(标题「覆盖当前的临时层」,正文「当前已经存在一个临时层课表,如果继续换课,那么该临时层将被覆盖。是否继续?」,主按钮「继续」、次按钮「取消」,默认主按钮);确认后清除临时层,再为新换课创建临时层(`.cs:99-119`);
- 换课完成后必须保存档案,并在集控受管时上报审计事件 `ClassChangeCompleted`(字段:ChangeMode、ClassPlanId、SourceClassIndex、SourceClassSubjectId、TargetClassIndex、TargetClassSubjectId、WriteToSourceClassPlan)(`.cs:159-172`);
- `Esc` 必须等价于取消(取消按钮 `IsCancel=true`);
- 窗口 `Unloaded` 时必须清空 `DataContext`(`.cs:183-186`)。
### 11.8 课表详细信息 `ClassPlanDetailsWindow`
> 本窗口**必须实现(P1)**,不得省略:它是课表/时间点附加设置的唯一可视化诊断入口。
源:`Views/ClassPlanDetailsWindow.axaml:1-199`。
1. **窗口参数**:`800×500`,启用 Mica,标题「课表详细信息」,图标默认。
2. **布局**:两行(`Auto, *`)、三列(`1*` 最小 `300` 最大 `500`、`5px` 分隔、`2*`),内容区背景 `LayerOnMicaBaseAltFillColorDefaultBrush` 铺满三列。
3. **左列**:只读 DataGrid(水平网格线),列:「时间」(起止时间 `HH:mm:ss-HH:mm:ss`,`margin: 4px`)、「科目」(科目名);`font-size: 14px`;选中行绑定 `SelectedLesson`。
4. **中列**:`GridSplitter`,方向 Columns。
5. **右列**(选中课程后):「附加设置结果」页签:
- 顶部一行:字段「要查看的附加设置」+ 下拉(`min-width: 150px`,每项「图标 + 名称」,选项来自全部已注册附加设置控件)+「刷新」按钮(`E0B5`);
- 说明文本(`margin: 4px`):「以下的附加设置将按从上到下的顺序使用。」+ 汇总文案;
- 列表:每项高 `36px`,左侧 `4px` 宽状态条(启用 = 主色渐变半透明块 + 主色竖条;被覆盖 = 警告色竖条),右侧渲染该附加设置的只读展示控件;状态条所在行必须有 `opacity: 0.25` 的高亮背景层(`:101-173`)。
6. **标题条**:「课表详细信息」(`font-size: 24px`,Medium,`margin: 8px`);右上「刷新」按钮(`E0B5`)。
### 11.9 Excel 导入向导 `ExcelImportWindow`
> **实现定性(必读)**:本实现**必须**把「从表格导入课表」做成**可用功能,优先级 P1**,不得以「遗留/可选」为由跳过。事实背景:ClassIsland 源码中该窗口的 XAML 仍在仓库(`Views/ExcelImportWindow.xaml`,992 行),但代码隐藏与 DI 注册均已停用(`Views/ExcelImportWindow.xaml.cs:1` 与 `Views/ExcelExportWindow.xaml.cs:1` 以 `#if false` 包裹整份代码,`App.Services.xaml.cs:204` 的 `services.AddTransient<ExcelImportWindow>()` 被注释)。**ClassIsland 源码中该窗口已停用,本实现以源码布局为参考重建**。技术约束:读写 `.xlsx` 必须使用纯 JavaScript 的 `xlsx` 库(SheetJS)或等价实现,**不得**引入 ReoGrid、WPF、`System.Windows.Forms` 或任何 .NET 互操作依赖;表格预览/编辑网格必须用 Vue 组件重建(虚拟滚动表格或 canvas 网格),交互与本文一致。
源:`Views/ExcelImportWindow.xaml:1-992`、`.cs:320-420`、`:746-822`(源码已停用,本实现以 XAML 布局与流程为规格依据)。
1. **窗口参数与入口**:默认 `WindowState=Maximized`(设计值 `450×800`),标题「从表格导入课表」,允许拖放文件,关闭前需保存清理;主容器为 `DialogHost` 等价的全窗遮罩容器。必须提供可到达的 UI 入口(建议放在档案编辑窗口的课表列表工具栏「从表格导入…」;入口的归属与位置归 part 06,但本窗口不得成为不可达的死功能)。
2. **三区域布局**(`Grid` 列定义随状态切换,`:182-221`):
- 未选择文件时:左列宽 `0`、中列宽 `0`、右列占满(引导卡居中);
- 已选择文件后:左列 `*`(表格)、中列 `5px`(分隔条实际宽 `10px`)、右列宽 `500px`(`min-width: 350`、`max-width: 800`);
- 行定义:第 0 行 Auto(页头)、第 1 行 `*`。
3. **页头**:普通状态为 `ColorZone`(PrimaryMid,`padding: 16px`,Elevation Dp4,`margin-bottom: 4px`)内「从表格导入课表」(Headline6);选区模式时页头变为「在下方表格中选择区域」+ 右侧当前选区(等宽字体,Headline6)+「确定」按钮(`Check` 图标),并叠加呼吸动画:选区模式下页头底部高亮条在 `0.1 → 0.25 → 0.1` 之间循环,单次 `1s`、`QuadraticEase EaseOut`、无限重复(`:64-82`、`:238-272`)。
4. **表格区**:必须用 Vue 网格组件重建,实现源码中 ReoGrid 的以下能力:可切换只读、显示并切换工作表标签(标签宽度 `500`)、文本默认黑色、支持单元格区域选择与合并/取消合并;单元格右键菜单固定 6 项:复制(`Ctrl+C`)、粘贴(`Ctrl+V`)、剪切(`Ctrl+X`)、清除内容(`Del`)、合并单元格、取消单元格合并(源码对应 `:274-308`;**不得引入 ReoGrid 依赖**)。
5. **右列向导容器**:`ScrollViewer`(`margin: 8px`)+ `Transitioner`(页面切换用 `CircleWipe` 圆形擦除过渡),共 **17 个页面(索引 0–16)**,页面标题一律 `MaterialDesignHeadline5TextBlock`、`margin-bottom: 12px`,正文 `TextBlock` 自动换行、`margin-bottom: 4px`:
- 0 「选择文件」:居中 `FileTableOutline` 75×75 图标 + 「从表格导入课表」(20px Bold)+「要开始导入,请将表格文件拖入到本窗口」+「或者」+「浏览文件…」按钮(`FolderOutline`);文件选择过滤器必须为「表格文件(*.xlsx)|*.xlsx」,标题「选择要导入的表格文件」(`:336-357`、`.cs:746-759`);
- 1 「加载中」:81×81 `帕姆_点赞` 贴纸 + 「正在加载你的表格……」+「坐和放宽,滚回功率,加载马上就好!」(13px,删除线);
- 2 「欢迎」:说明 +「开始」按钮(`ArrowRight`;若以「仅导入时间表」模式打开则直接跳到 5);
- 3 「询问界面形式」:「您希望本向导如何引导您?」+ 两个选项「详细」(对每个选项和流程做出详细解释,适用于刚接触本软件的新手)/「简洁」(简化流程和语言说明,提供简洁的体验,适用于熟悉本软件的用户)+「上一步」;
- 4 「从哪里导入时间表?」:三个选项:「我的时间表在这个文件中」/「我的时间表另一个文件中」/「我想手动创建时间表」+「上一步」;
- 5 「选择时间安排区域」:说明「点击下方文本框以从表格中选择包含时间点安排信息的区域。选区只能为单列或单行选区。Tips:若选区遇到合并的单元格,可以在单元格上右键取消单元格合并。」+ 区域选择文本框(水印「选择时间安排区域」)+ 无效时红色警告「选择区域无效。」+「下一步」(仅在区域合法且非选区模式时可用);合法性规则:选区必须为单行或单列(`.cs:761-777`);
- 6 「手动创建时间表」:共用「选择时间表」说明 + 时间表下拉(按 `Key` 选中)+「打开档案编辑窗口」按钮(`FileDocumentEditOutline`)+「下一步」(未选择时间表时禁用);
- 7 「时间表导入结果」:时间表名称输入框(Hint「时间表名称」)+「刷新」「编辑」按钮 + 只读时间线预览(`TimeLineListControl`,`Scale=3`,支持拖动平移)+ 导航按钮 +「跳过课表导入」(仅非「仅时间表」模式);
- 8 「选择课程区域」:区域选择文本框(水印「选择课程区域」)+ 无效警告 + 导航按钮(区域合法时可用);
- 9 「选择行列对应关系」:「点击下方高亮圆形按钮以选择读取课表的方向。」+ 切换按钮(未选中显示 `ArrowRight`、选中显示 `ArrowDown`)+ 文案「每竖列为一个课表。」/「每横行为一个课表。」+ 导航按钮;
- 10 「如何导入时间对应关系?」:两个选项「自动识别」「手动指定」;
- 11 「时间对应关系」:左右两个多选列表:左侧为识别区域(每行复选框内容为 `{值}`),右侧为该表格时间点(`HH:mm:ss-HH:mm:ss`);说明「设置每行对应的时间点。在识别课表时,仅会识别左侧选中的区域。在右侧选择该表格中包含的时间点,选中的时间点在识别时会按照顺序匹配左侧选择的区域。」+「下一步」;
- 12 「选择创建课表的区域」:「将从选择的区域创建课表。」+ 课表来源列表(选中项左边显示 Check 图标)+ 导航按钮(未选择来源时禁用);
- 13 「课表导入结果」:「已识别选中区域上的课程。请检查识别结果是否正确,并修改不正确的课程。」+ 课表名称输入框(Hint「课表名称」)+ DataGrid(行头宽 `150`;列「时间」只读、列「科目」为可编辑科目下拉)+ 导航按钮;
- 14 「课表设置」:「调整刚刚导入的课表的触发条件等设置。」+ 名称输入框 + 时间表下拉(禁用)+ 复选框「自动启用」+ 说明「当课表设置为自动启用时,将会根据触发规则自动启用该课表。否则只能通过临时课表或临时层手动启用该课表。」+ 启用时显示时间规则编辑控件;
- 15 「已导入的课表」:列表(`FileChartOutline` + 名称)+ 三个可点击项:「继续导入课表。」(`ArrowRight`)、「使用新的时间表继续导入课表。」(`UndoVariant`)、「完成导入。」(`Check`);
- 16 「完成」:`光辉矢愿_回眸` 贴纸(63×63)+「恭喜!您已经完成了课表导入!现在您可以关闭本页面了。」+「完成」按钮。
页面跳转映射与返回映射必须按源码实现(`.cs:345-420`),例如:`TimeLayoutSource→4`、`ImportTimeLayoutFromThisFile→5`、`CreateTimeLayoutManually→6`、`TimePointImportResult→7`、`SelectSubjectsPosition→8`(仅时间表模式为 16)、`SelectVerticalMode→9`、`RowClassesTimeRelationshipImportAuto/Man→11`、`SelectClassPlanArea→12`、`PreviewClassPlan→13`、`ClassPlanDetails→14`、`AllClassPlansView→15`、`Finish→16`;返回时 `3/4→2`、`5/6→4`、`7→5`、`8→7`(时间表来源=0)或 `8→9`(来源=2)、`9→6`、`10→8`、`11→10`、`12→11`、`13→12`、`14→13`、`15→14`、`16→15`。
6. **拖放**:拖入文件时整窗显示遮罩(`SolidBackgroundFillColorBaseBrush`,`opacity: 0.8`)+ 居中 `FileImport` 64×64 图标 +「将表格文件拖入到此处,松手即可开始导入。」;仅当拖入数据包含文件且尚未选择文件时显示(`:972-986`、`.cs:289-291`)。
7. **错误对话框**(全窗遮罩对话框):「无法打开文件」(标题「无法打开文件」,正文包含文件名与异常信息,按钮「确定」);「无法以兼容模式打开」;「兼容性警告」(「无法打开文件“…”因为本文件中有一些不兼容的格式和字体。您可以尝试以兼容模式打开。若选择以兼容模式打开,应用将为此文件创建一个临时副本,并移除副本中不兼容的格式和字体。」,按钮「取消」「在兼容模式中继续」)。用 `xlsx` 库实现时,「兼容模式」等价于:以 `cellStyles:false`/忽略样式与公式的方式二次解析同一文件,失败才进入「无法以兼容模式打开」(源码对应 `:85-164`)。
8. **关闭行为**:关闭窗口时必须取消选中时间表的激活状态(`SelectedTimeLayout.IsActivated=false`、`IsActivatedManually=false`),避免导入中断留下脏数据(`.cs:801-805`)。
### 11.10 Excel 导出窗口 `ExcelExportWindow`
> **实现定性(必读)**:本实现**必须**把「导出到 Excel 表格」做成**可用功能,优先级 P1**。事实背景:源码中该窗口 XAML 保留、代码隐藏位于 `#if false`(`Views/ExcelExportWindow.xaml.cs:1`)、未在 DI 中注册(`App.Services.xaml.cs:205` 被注释)。**ClassIsland 源码中该窗口已停用,本实现以源码布局为参考重建**。技术约束:必须使用 `xlsx`(SheetJS)或等价纯 JS 库生成/读取 `.xlsx`,不得依赖 ReoGrid/WPF;表格网格必须用 Vue 组件重建,并支持本文列出的单元格样式(字体、字号、粗斜体、下划线、水平对齐、合并/取消合并、字体色、背景色、框线、行高列宽)。
源:`Views/ExcelExportWindow.xaml:1-301`。
1. **窗口参数**:`1400×700`,默认 `WindowState=Maximized`,标题「导出到 Excel 表格」,背景 `SolidBackgroundFillColorBaseBrush`,字体 HarmonyOS Sans 14px。
2. **布局**:三列 `*`、`Auto`、`360px`(最小 `300`、最大 `400`);三行 `Auto`、`Auto`、`*`。第 0 行为 Snackbar 层,第 1 行为工具栏(跨三列),第 2 行为表格 + 右侧设置面板。
3. **表格区**:必须用 Vue 网格组件重建(不得引入 ReoGrid),实现可编辑单元格、工作表标签(标签宽度 `500`)、区域选择、合并/取消合并,以及复制/粘贴/剪切/清除内容(带快捷键提示)右键菜单。
4. **工具栏(普通模式)**:保存…(`ContentSaveOutline`)、保存并在默认 Excel 文件编辑器中打开(`MicrosoftExcel`)、分隔线、撤销(`Undo`,随 `CanUndo` 禁用)、重做(`Redo`)、分隔线、字体下拉(宽 `150px`,可编辑,每项用自身字体渲染)、字号数值框(宽 `75px`,后缀 `px`)、减小字号、增大字号、加粗/斜体/下划线(多选切换)、分隔线、水平对齐(无/左/居中/右/两端)、分隔线、合并单元格、取消合并。
5. **工具栏(选区模式)**:「完成选择」按钮(`Check`)+ 分隔线 +「当前选区:」+ 选区坐标(等宽字体 `16px`)。
6. **右侧面板**:`TabControl`,页签位于右侧(`TabStripPlacement=Right`),每个页签 `min-width: 42px`、`height: 42px`、内边距 `4px`:
- 「课表」页(图标 `FileChartOutline`):标题「课表」+「基准位置」选择框(进入选区模式)+ 课表多选列表(CheckBox + 课表名);
- 「样式」页(图标 `StyleOutline`):标题「样式」+ 复选框「包含教师名称」+ 下拉「水平对齐方式」(无/左/居中/右/两端)+ 下拉「垂直对齐方式」(默认/顶部/居中/低端)+「字体颜色」颜色选择器 +「背景颜色」颜色选择器 + 下拉「单元格框线」(无/全框线/全粗框线)+ 并排数值框「行高」「列宽」(后缀 `px`)+「刷新内容」按钮;
- 「保存」页(图标 `ContentSaveOutline`):标题「保存」+「保存到 Excel 表格」+ 说明 +「保存并在默认 Excel 文件编辑器中打开」+ 说明。
7. **Snackbar**:所有保存/错误提示必须以底部/顶部 Snackbar 显示(MaterialDesign Snackbar),不得使用阻塞对话框。
### 11.11 数据迁移 `DataTransferWindow` / `DataTransferPage`
源:`Views/DataTransferWindow.axaml:1-20`、`.cs:13-57`、`Views/DataTransferPage.axaml:1-273`、`.axaml.cs:54-120`。
1. **窗口参数**:`800×600`,`CenterScreen`,`ShowAsDialog=true`,`CanResize=false`,`ShowInTaskbar=true`,启用 Mica,标题「数据迁移」,图标 `AppLogo.png`;内容整体替换为 `DataTransferPage`。
2. **页面**:`ScrollViewer` 内 `Grid max-width: 600px` 居中 + `Carousel` 宽 `600px`(5 页,页面过渡为水平滑动 `300ms` `cubic-bezier(0.76,0,0.24,1)` + 交叉淡入 `300ms`):
- 页 0「数据迁移」:标题 +「向 ClassIsland 导入或从 ClassIsland 导出数据。」+ TabControl:
- 导入页签三个可点击卡片:ClassIsland 2.x(应用图标)、ClassIsland 1.7.0.x(应用图标)、Class Widgets 1.2(`E06F`);
- 导出页签:InfoBar 说明「此处的数据导出功能主要用于在不同 ClassIsland 实例间转移数据。若需导出诊断信息,请前往【应用设置】->【更多选项…】->【导出诊断数据】;若需备份应用数据,请使用【应用设置】->【通用】->【存储】中的备份功能。」+ 卡片「ClassIsland 2.x」;
- 页 1「选择要导入的数据 / 选择要导出的数据」:显示来源描述文本 +「选择要导入的信息 / 选择要导出的信息」+ 3 个复选框:课表配置(`E699`,「包括课表内容、时间安排、科目定义等信息。」)、应用设置(`EF27`,「包括应用的各个首选项,如隐藏设置、颜色设置、提醒/铃声设置、天气信息等。」)、其它配置(`E06F`,「组件、自动化等配置文件。」)+「上一步」「下一步」(`48×48` 圆角 24);
- 页 2「导入来源 / 导出目标」:「选择要导入的数据来源。 / 选择要导出到的目标。」+ 卡片「浏览…」(`E88D`,说明「手动浏览导入位置 / 手动浏览导出位置」)+「上一步」;
- 页 3「正在导入 / 正在导出」:「请坐和放宽,导入很快就能完成。」+ `ProgressRing` `48×48`(不确定);
- 页 4「导入完成 / 导出完成」:「已成功完成导入。/ 已成功完成导出。」+ 完成按钮(`E423`,`48×48` 圆角 24 accent)。
所有文案必须随「导入/导出」模式动态切换(通过容器 `.export` 类实现,`:80-88` 等)。
3. **导入来源语义**:
- ClassIsland 1.x:说明「支持从 1.x 版本的 ClassIsland 导入课表、组件配置、自动化配置、应用设置、部分插件和主题等数据。」;浏览时选择旧实例的 `ClassIsland.exe`,以该可执行文件所在目录为根(`.cs:77-114`);
- ClassIsland 2.x:说明「支持从 ClassIsland 2.x 数据文件导入课表、组件配置、自动化配置、应用设置、部分插件和主题等数据。」;
- 导出:说明「支持导出为可以导入到其它 ClassIsland 2 实例的文件格式。」;
- Class Widgets 1:说明「支持从 Class Widgets 1 导入全部课表信息和大部分配置。」;
- 导入前若需要重启(如替换正在使用的配置),必须弹出模态确认「重启以继续」(`.cs:116-120`)。
4. **外部调用**:应用必须支持以 `-i` 等参数在启动时执行迁移并把结果(`ImportStage`、`ImportName`)传入页面;`ImportComplete` 时必须直接把页面切到索引 4(`.cs:52-56`)。
### 11.12 授权窗口 `AuthorizeWindow` 与集控加入对话框
1. **AuthorizeWindow**(`Views/AuthorizeWindow.axaml:14-152`):宽 `500`,`SizeToContent=Height`,`CanResize=false`,`MinHeight=300`、`MaxHeight=700`,`CenterScreen`,`Topmost=true`,`ShowAsDialog=true`,`CanMaximize=false`,标题「ClassIsland」。内容 `margin: 16px`,三行两列:
- 头部(普通模式):「验证您的身份」(Subtitle)+「要继续操作,请通过下列的方式验证您的身份。」;编辑模式:「编辑认证项目」+「编辑进行此操作需要的认证方式。」;
- 编辑模式工具行:「添加认证方式」(`E00D`)+ 认证方式下拉(`min-width: 100px`,每项「图标 + 名称」);
- 认证方式列表:每项两行:第一行 `20×20` 图标(主色)+「用{名称}继续」(14px Medium)+ 右侧(选中且编辑模式时显示)拖拽把手与删除按钮(`E671`,`IndianRed`,点击弹出 Flyout:「要删除此认证方式吗?」「此操作无法撤销。」+「删除」accent 按钮);第二行渲染该认证方式的输入控件(如密码框);列表项可垂直拖拽排序(水平阈值禁用、垂直阈值 `3px`);
- 底部:两列等宽按钮「取消」(`IsCancel`)与「确定」(accent,仅编辑模式显示);
- 认证完成必须通过 `CompleteAuthorizeCommand` 路由命令触发,成功后关闭并返回结果。
2. **密码认证控件**(`Controls/AuthorizeProvider/PasswordAuthorizeProvider.axaml:15-66`):密码输入框(`PasswordChar=●`,水印「密码」,`margin-top: 8px`,按 Enter 直接提交)+(编辑模式下)确认密码框(水印「确认密码」,失焦/输入时校验)+ 右侧「确定」圆形按钮(`32×32`、圆角 `16px`、accent,tooltip「确定」);错误提示行(红色):密码不正确。/ 确认密码与密码不一致。/ 请输入密码。;已设置密码时显示「密码已设定。」+「更改密码」按钮(`E7C9`)。
3. **JoinManagementDialog**(`Controls/JoinManagementDialog.axaml:12-64`):宽 `400`,`SizeToContent=Height`,`CanResize=false`,标题「加入管理」。内容 `margin: 16px`:标题「加入管理」+ 说明「将当前ClassIsland实例连接到管理组织。」+ 配置文件行(左「配置文件:{路径}」单行截断 + 右侧文件选择按钮,文件类型为集控配置文件)+ 字段「ID(可选)」+ 错误文本(红色,按需显示)+ 不确定进度条(工作中)+ 底部两列按钮「取消」(`IsCancel`)「连接」(默认按钮,accent,仅当配置已加载时可用)。
### 11.13 教学(教程)窗口组
1. **教学中心 `TutorialCenterWindow`**(`Views/TutorialCenterWindow.axaml:14-248`、`.cs:31-84`):`1240×550`,启用 Mica,标题「教学中心」,图标 `AppLogo.ico`。结构:
- 顶部 TabStrip(`navigation` 样式,`font-size: 20px`)按「教程组」切换;右上「停止当前教程」按钮(`F087`,仅教程运行中显示);
- 内容区背景 `LayerOnMicaBaseAltFillColorDefaultBrush`;`TabControl`(`navigation`)承载当前教程组的 StickyScrollViewer;
- 头部大图:最大高度 `200px`,横幅 `DefaultTutorialGroupBanner.webp` 右对齐;背景为 `#2bbdbc` 叠加 60° 旋转的 `#19ffffff` 棋盘格纹理(`DestinationRect 64×64`,格子 32×32);头部底部标题行(`margin: 12px 8px 18px`):组标题(TitleTextBlockStyle)+ 组描述(14px 次级);头部随滚动折叠,折叠高度等于标题行高度;
- 教程卡片网格:WrapPanel 自动换列(目标宽度 `328px`),卡片:圆角 `4px`、裁剪;横幅图高 `100px`(`UniformToFill`)+ 内容区(`padding: 12px 8px 12px`,间距 `6px`):标题 `16px` Medium、描述 `12px` 次级、底部进度条(高度紧贴底部,`max = 段落数`,`value = 已完成段落数`);
- 点击卡片打开右侧详情 Pane:`SplitView` 右置、`DisplayMode=Inline`、`OpenPaneLength=328px`、`CompactPaneLength=0`;Pane 内 `padding: 12px`:教程标题 `20px` Medium、描述、进度文本「已完成 X/Y 个段落。」、「播放教程」accent 按钮(教程运行中禁用)+「或者,选择下方的段落播放:」+ 段落列表(已完成段落标题加删除线、斜体、`opacity: 0.75`,并在标题后显示主色「已完成」;每项右侧悬停/选中时显示「播放」按钮 `EDB9`);
- 关闭窗口时若因用户操作则 `hide()`(保持单例),应用退出时正常销毁(`.cs:74-84`)。
2. **教程播放控制条 `TutorialControllerWindow`**(`Views/TutorialControllerWindow.axaml:1-46`):无系统装饰条(`SystemDecorations=BorderOnly`)、透明背景 + `AcrylicBlur`、`SizeToContent=WidthAndHeight`、`Topmost`、`ShowInTaskbar=false`、`CanResize=false`、标题「教程控制窗口」。内容:半透明底色 `SolidBackgroundFillColorBaseBrush`(`opacity: 0.5`)+ 横向排列(`margin: 12px 4px`,间距 `4px`):「教学进行中…」(`E959`,`opacity: 0.75`)+「跳过」按钮(`E444`,透明样式);跳过按钮点击弹出 Flyout(`max-width: 328px`,间距 `8px`):「跳过教程」(粗体)+「是否确定跳过本段落教程?您稍后可以在【主菜单】->【教学…】中重看本段教程。」+「跳过」accent 按钮。该窗口位置必须由主进程放在主界面附近(右下角、距屏幕边缘 `16px`【推断】),并在教程结束时销毁。
3. **教程编辑器 `TutorialEditorWindow`**(`Views/TutorialEditorWindow.axaml:15-377`):`WindowState=Maximized`,标题「教程编辑器」。结构:
- 菜单栏(跨 3 列):「文件」含 打开…(`Ctrl+O`)、保存…(`Ctrl+S`)、另存为…(`Ctrl+Shift+S`)、分隔线、教程章节信息…(`Ctrl+I`);「运行」含 运行当前教程(`F5`)、从当前段落开始运行(`Ctrl+F5`)、分隔线、停止运行(`Shift+F5`);
- 列定义 `220px, Auto, *, Auto, 328px`,行定义 `Auto, Auto, *, Auto`;
- 第 1 列:教程列表(顶部「添加/复制/删除」按钮,图标 `E00D/E58B/E61D`,删除为 `IndianRed`;列表项「图标 + 标题」,可垂直拖拽排序);
- 第 3 列:段落列表(同上按钮);第 5 列:语句 DataGrid(列:标题 `*`、内容 `3*`、目标选择器 `2*`;`RowHeaderWidth=24`,行头为拖拽把手;支持行拖拽排序);
- 最右侧 `328px`:选中对象的属性面板;教程组属性:ID/标题/描述;教程属性:ID/标题/描述/Banner(水印「(默认)」);段落属性:基本页(ID/标题/描述/TopLevel 类名/复选框「启用屏障」)+「进入事件」页;语句属性:基本页(标题/内容/头图/图标/目标控件选择器/教学提示摆放模式 14 种(Auto/Top/Bottom/Left/Right/TopRight/TopLeft/BottomRight/BottomLeft/LeftTop/LeftBottom/RightTop/RightBottom/Center)/指向目标/高亮目标/使目标具有模态/可等待被外部推动/Tag/使用 LightDismiss 代替右侧按钮/右侧按钮/左侧按钮)+「进入事件」「左按钮事件」「右按钮事件」三个行动编辑页 +「脚本」页(启用脚本开关 +「编辑脚本…」按钮打开脚本编辑器)。
- 关闭窗口时必须拦截并处理未保存修改(`Closing` 事件),窗口失焦时不得自动关闭。
4. **教程脚本编辑器 `TutorialScriptEditWindow`**(`Views/TutorialScriptEditWindow.axaml:1-29`):`800×520`,`MinWidth=480`、`MinHeight=320`,标题「编辑教程脚本」。顶部一行(`margin: 8px`,间距 `6px`):「Lua 教程脚本」(`E943`)+ 复选框「启用脚本」;主体为代码编辑器:显示行号、深色主题(背景 `#1E1E1E`、前景 `#D4D4D4`)、等宽字体 `JetBrains Mono, Cascadia Code, Consolas, Menlo, Monospace`、`font-size: 14px`、`margin: 0 8px 8px`。脚本语言为 Lua;失焦/输入时不得丢失内容,关闭时保存。
5. **教程行动类型(TutorialActionKind)映射表(必须逐值实现,JSON 数字不可改)**:
枚举定义:`ClassIsland.Core/Enums/Tutorial/TutorialActionKind.cs:6-44`,共 **9 项(0–8)**;模型字段:`TutorialAction.Kind`(`ClassIsland.Core/Models/Tutorial/TutorialAction.cs:18`)、`StringParameter`(同文件 `:23`,字符串参数)、`ActionSet`(同文件 `:29`,仅 `InvokeActionSet` 使用,序列化时空值省略)。ClassIsland 的 JSON 由 `System.Text.Json` 默认选项写出(`ClassIsland.Shared/Helpers/ConfigureFileHelper.cs:30-36` 只设置了 `WriteIndented`,未注册字符串枚举转换器),因此 `Kind` 在 JSON 中是**数字**;本实现读写教程 JSON 时必须使用 `Kind`(PascalCase)与数字值,**不得**改成 `kind` 或字符串。
| 数值 | 枚举名 | 中文语义(编辑器文案) | 触发来源 / 控制条入口 | 执行语义与边界(源码证据) |
|---|---|---|---|---|
| 0 | `None` | 无 | 任意动作数组 | 什么都不做(`TutorialService.cs:622-623`)。 |
| 1 | `NextSentence` | 下一句 | 语句右按钮 / LightDismiss / TeachingTip 关闭 / 聚光灯点击(均以参数 1 调用 `PostSentenceWorks`) | 若当前段还有下一句则进入下一句;否则尝试进入下一段;已是最后一段则停止教学(`TutorialService.cs:624-626`、`:678-691`;按钮参数注入 `:404-416`;`PostSentenceWorks` `:586-600`;聚光灯 `:503-506`)。**初始化动作中不执行**(`isInit` 守卫)。 |
| 2 | `PreviousSentence` | 上一句 | 语句左按钮(以参数 0 调用 `PostSentenceWorks`) | 若存在上一句则回退到上一句;否则尝试回退到上一段;再失败则停止教学(`TutorialService.cs:635-647`)。默认左按钮文字为空、默认动作为 `PreviousSentence`(`TutorialSentence.cs:84-94`)。初始化动作中**允许执行**(源码未加 `!isInit` 守卫)。 |
| 3 | `NextParagraph` | 下一段 | 任意动作数组 | 进入下一段并先把当前段落标记为已完成;若已是最后一段则停止教学(`TutorialService.cs:627-634`、`:693-707`)。**初始化动作中不执行**。 |
| 4 | `PreviousParagraph` | 上一段 | 任意动作数组 | 回退一段;已到第一段则停止教学(`TutorialService.cs:648-655`、`:709-724`)。**初始化动作中不执行**。 |
| 5 | `JumpParagraph` | 跳转到段落 | 任意动作数组 + `StringParameter` | 参数为段落路径:含 `/` 时按 `tutorialId/paragraphId` 全局查找,只有段落 id 时在当前教程内查找;查不到时**静默忽略**(`TutorialService.cs:656-663`、`ParseParagraphPath` `:519-553`)。编辑器仅在选中该项时显示参数输入框(`Controls/TutorialEditor/TutorialActionsEditControl.axaml:64-79`)。**初始化动作中不执行**。 |
| 6 | `Stop` | 停止 | 任意动作数组 | 立即 `StopTutorial()`:关闭控制条、清理教学提示、标记当前段落完成、清空当前教程/段落/句子(`TutorialService.cs:664-665`、`:565-575`)。初始化动作中**也执行**。 |
| 7 | `InvokeActionSet` | 执行行动 | 任意动作数组 + `ActionSet` | 调用自动化服务执行动作集;`ActionSet == null` 时忽略(`TutorialService.cs:667-668`)。编辑器在切换到该项时必须初始化 `ActionSet`(`TutorialActionsEditControl.axaml.cs:62-65`)。初始化动作中**也执行**。 |
| 8 | `InvokeUri` | 执行 Uri | 任意动作数组 + `StringParameter` | 把参数作为 Uri 交给 Uri 导航服务(支持 `classisland://` 内部路由);参数非法时源码会抛异常(`TutorialService.cs:670-671`)——本实现必须捕获、写 warning 日志并继续,不得崩溃。初始化动作中**也执行**。 |
| ≥9 | 未知值 | — | — | 源码 `default:` 抛 `ArgumentOutOfRangeException`(`TutorialService.cs:673-674`);**本实现必须改为:忽略该动作并写 warning 日志(含 Kind 数值与动作索引),继续执行后续动作**。 |
执行上下文与顺序(必须一致):
- 动作数组共 4 组:段落 `InitializeActions`(段落开始时执行,`TutorialService.cs:244-251`)、语句 `InitializeActions`(语句开始时执行,`:362-379`)、语句 `LeftButtonActions`、语句 `RightButtonActions`;每组内按数组顺序**同步串行**执行(`:608-616`),执行期间 `_isInvokingActions=true`,此时外部推进(`Next` 命令/LightDismiss)必须被忽略(`:202-212`)。
- 「初始化动作」等价于 `isInit=true`:`NextSentence`/`NextParagraph`/`PreviousParagraph`/`JumpParagraph` 被跳过,其余照常执行;`LeftButtonActions`/`RightButtonActions` 等价于 `isInit=false`。
- 控制条(`TutorialControllerWindow`)的「跳过」按钮**不经过 Kind 枚举**:它等价于 `SkipTutorial()`(把当前段落标记为已完成并停止教学,`TutorialControllerWindow.axaml.cs:54-57`、`TutorialService.cs:577-584`)。
- Lua 脚本不是 Kind 的执行者,但可在 5 个钩子里干预:`onInitializing` 返回 `false` 会中止该语句初始化(**不执行** InitializeActions)、`onLeftButtonClicked` 返回 `false` 会阻止左按钮动作、其余钩子失败只记错误;脚本全局变量固定为 `service`(教程服务,可直接调 `service:PostSentenceWorks(0|1)`、`service:JumpToParagraph("tutorialId/paragraphId")`、`service:InvokeActions(...)`)与 `context`(`TutorialService.cs:454-500`、`:586-616`)。脚本异常必须弹出错误 Toast 并写 error 日志,不得中断整个教程。
JSON 兼容性与 TypeScript 常量(必须照抄):
```json
{ "Kind": 5, "StringParameter": "classisland.getStarted/welcome", "ActionSet": null }
```
```ts
// src/shared/tutorial-action-kind.ts —— 数字必须与 ClassIsland 的 TutorialActionKind 完全一致
export const TutorialActionKind = {
None: 0,
NextSentence: 1,
PreviousSentence: 2,
NextParagraph: 3,
PreviousParagraph: 4,
JumpParagraph: 5,
Stop: 6,
InvokeActionSet: 7,
InvokeUri: 8,
} as const;
export type TutorialActionKindValue = (typeof TutorialActionKind)[keyof typeof TutorialActionKind];
// 读取时兼容三种来源:数字(ClassIsland 原生)、枚举名字符串(插件手写)、未知值
export function normalizeTutorialActionKind(raw: unknown): TutorialActionKindValue | null;
```
读取规则:字段名必须以 `Kind`/`StringParameter`/`ActionSet` 精确匹配(.NET 默认 `PropertyNameCaseInsensitive = false`,写错大小写会读不到);`Kind` 既接受 `0–8` 数字,也接受枚举名字符串(大小写敏感)作为容错;**写出时必须写数字**,以保证导回 ClassIsland 时兼容。未知值/未知名字符串:`normalizeTutorialActionKind` 返回 `null`,调用方跳过该动作并在日志中记录 `unknown TutorialActionKind: <raw>`(warning 级别),**禁止崩溃或静默改变 Kind**。
### 11.14 日志窗口 `AppLogsWindow`
源:`Views/AppLogsWindow.axaml:1-178`、`.cs`。
1. **窗口参数**:`1200×600`,启用 Mica,标题「日志」,单例(关闭即 `hide()`,重复打开时 `show()`+`focus()`)。
2. **布局**:三行(`Auto, *, Auto`):
- 顶部左侧为筛选 Chip 列表(多选可切换):灾难(`E84E`)、错误(`E808`)、警告(`F430`)、信息(`E9E4`)、调试(`E2C7`)、追踪(`E50F`);默认选中级别必须与日志级别配置一致【推断:默认全部开启】;
- 顶部右侧搜索框(水印「筛选日志…」,`min-width: 200px`),输入即时过滤;
- 中部 DataGrid(只读、水平网格线、背景 `LayerOnMicaBaseAltFillColorDefaultBrush`,`font-size: 13px`):列「时间」(Auto,`margin: 2px 9px 0`)、「等级」(Auto:胶囊样式——灾难/错误使用 `SystemFillColorCriticalBackgroundBrush` + 粗体,警告使用 `SystemFillColorCautionBackgroundBrush` + 粗体,调试/追踪 `opacity: 0.75`,均配图标与中文级别名)、「区域」(Auto,`max-width: 250px`,可换行可选择)、「内容」(`*`,可换行可选择);
- 底部右对齐按钮:清空日志(`E61D`)、复制选中(`E58B`),均为透明按钮。
3. **行为**:日志必须来自主进程统一日志总线(`IPC` 推送),行数上限固定为 `10000` 条并在超出时裁掉最旧条目【推断:上限值由工程约定给出,源码未硬编码】;关闭时必须取消订阅、停止追加,避免内存泄漏。
### 11.15 其余窗口与对话框(逐个规格)
1. **配置文件错误窗口 `ConfigErrorsWindow`**(`Views/ConfigErrorsWindow.xaml:1-106`):`1200×600`,标题「配置文件错误」,纯色背景 + HarmonyOS Sans 14。结构:第 0 行 ToolBar(`ToolBarTray.IsLocked=true`)含「恢复备份」按钮(`Restore` 图标);第 1 行说明文本「下方的配置文件因意外断电等原因已损坏且无法加载,现已自动加载备份或使用默认值。您可以重启到恢复模式并从备份中恢复配置文件。」;第 2 行 DataGrid(只读、像素滚动、禁止重排列)四列:状态图标(警告=黄色 `WarningOutline`,错误=红色 `ErrorOutline`)、「路径」、状态文本(警告「已恢复至上次正确的配置」/错误「已重置为默认配置」)、「操作」(复制详细信息按钮)。打开方式:设置→存储页 InfoBar 的「详细信息…」或 URI `classisland://app/config-errors`。
2. **实例已存在窗口 `InstanceExistedWindow`**(`Views/InstanceExistedWindow.xaml:1-54`):`800×450`,`CenterScreen`,`ResizeMode=NoResize`,标题「ClassIsland 正在运行」。三行:说明文本(两段:已在运行请通过托盘图标操作;若看不到主界面可能是选择了隐藏主界面、规则隐藏或自动化修改)、演示图(`OpenTaskBarMenuDemo`,指引用户点托盘)、右下按钮「重启应用」(图标 `Restart`)与「我知道了」(图标 `Close`,纯色背景按钮样式)。
3. **窗口选择器 `WindowsPicker`**(`Views/WindowsPicker.xaml:1-149`):`1300×600`,标题「选择壁纸窗口」,用于选择壁纸层窗口类名。结构:第 0 行 Material ColorZone 标题栏(PrimaryMid,Dp4,`padding: 16px`):「选择窗口」(Headline6)+ 右侧「刷新」按钮(`Refresh`,24px)+「更多选项…」弹出框(含复选框「仅显示全屏窗口」);第 1 行窗口缩略图网格(WrapPanel):每项含截图(`height: 150px`,`Uniform`)、左下角 `32×32` 应用图标(圆角 4)、右下角选中标记(`40×40` 圆形主色 + 白色 Check,带 `BlurRadius=6, ShadowDepth=0` 阴影)、下方四行文本:描述(18px Bold)、类名、窗口标题、进程名(均 13px);第 2 行分隔线;第 3 行底栏(`margin: 8px`):`WindowMaximize` 图标 + 文本框(Hint「窗口类名」)+ 右侧「取消」「确定」按钮(`Cancel`/`Check` 图标);加载中显示全屏遮罩 + `StarRailLoadingControl`。
4. **壁纸预览窗口 `WallpaperPreviewWindow`**(`Views/WallpaperPreviewWindow.xaml:1-74`):`800×450`,`CenterOwner`,标题「壁纸提取结果预览」。内容:整窗壁纸图(`Uniform`、高质量缩放);底部悬浮操作条(半透明底 `opacity: 0.8`,`margin: 8px`):「刷新」按钮(左,工作中禁用)、「关闭」按钮(右,`IsCancel`);工作中底部显示不确定进度条;数据未就绪时整窗显示加载遮罩(`TeyvatLoadingControl` 右下角)。
5. **顶层特效窗口 `TopmostEffectWindow`**(`Views/TopmostEffectWindow.axaml:1-35`):`1×1`、`Topmost`、无边框(`SystemDecorations=None`)、背景透明,标题「顶层效果窗口」。它在运行时会由主进程调整为覆盖**整个屏幕**(含任务栏)的透明置顶窗口,用于全屏提醒特效;内容为 `ItemsControl`(面板为 `Grid`,所有特效控件叠放并拉伸),每个特效控件外层包 `Border` 拉伸。关闭行为必须拦截(除应用退出外不关闭)。
6. **清除通知按钮窗口 `ClearNotificationButtonWindow`(不实现)**:该窗口在 ClassIsland 2.1 中由 `#if false` 包裹、内容仅为空按钮,属于遗留未完成代码。本实现**不得**照搬该窗口;清除提醒必须在主界面(岛)的右键菜单中提供「清除全部提醒」菜单项(该菜单的完整交互归 part 04 / part 07)。源码仅作反例记录:`Views/ClearNotificationButtonWindow.xaml:1-15`、`Views/ClearNotificationButtonWindow.xaml.cs`。
7. **翻新范围对话框 `RefreshingScopesConfigDialog`**(`Views/RefreshingScopesConfigDialog.axaml:1-60`):`400×600`,`CenterOwner`,`ShowAsDialog=true`,`CanResize=false`,标题「编辑翻新范围」。三行(`Auto * Auto`,`margin: 12px`):头部「选择翻新时要保留的内容」(Subtitle)+「以下内容在翻新时不会被删除。」(次级);TabControl(compact)两个页签「课表」「时间表」,各为多选列表(每项 CheckBox + 名称,`SelectionMode=Multiple,Toggle`);底部(`margin: -12px 0 -12px -12px`,背景 `LayerOnMicaBaseAltFillColorDefaultBrush`)右侧「确定」accent 按钮(`IsCancel=true`)。
8. **窗口规则调试工具 `WindowRuleDebugWindow`**(`Views/WindowRuleDebugWindow.axaml:1-30`):`400` 宽、`SizeToContent=Height`、`Topmost`、启用 Mica,标题「窗口规则调试工具」。内容为可选择文本的多行信息:窗口句柄、窗口类名、窗口标题、窗口状态、窗口进程名(每行「标签:值」)。窗口加载后必须持续监听前台窗口变化并刷新。
9. **截图辅助窗口 `ScreenshotHelperWindow`**(`Views/ScreenshotHelperWindow.axaml:1-37`,仅开发/内部):标题「截图」。四个字段:窗口下拉(每项「标题 (Id=…) [Type]」)、保存文件夹文本框、缩放数值框、底部「刷新」「截图」按钮(截图为主色)。实现可选,但不得在正式版暴露给普通用户。
10. **功能调试窗口 `FeatureDebugWindow`**(`Views/FeatureDebugWindow.xaml:1-246`,仅开发):`800×450`,标题「FeatureDebugWindow」,TabControl 多页(WeatherIcon、ScheduleCalendar、MarkdownTest、SecureDesktop、AuthorizeWindow 等),用于内部测试;正式构建必须移除入口。
11. **开发者门户 `DevPortalWindow`**(`Views/DevPortalWindow.axaml:1-269`,仅开发):`1200×700`,`CenterScreen`,启用 Mica,标题「DevPortal」。顶部 TabControl(`navigation` 样式,覆盖整窗,距顶 `48px` 的底色层),页签:首页(调试时间偏移、测试选择器解析、测试教程、贴纸高亮演示、StartSplash/EndSplash)、控件(ChipListBox、Toast、Shimmer、平行四边形/斜切遮罩动画、贴纸、TutorialBarrier 等)、提醒、LDV、OOBE_Intro、Markdown(左编辑器右预览)、调课日历、WeatherIcon、StickyScrollViewer、竖直抽屉、SDG。正式构建必须不可达。
12. **通知中心**:ClassIsland **没有**独立的通知中心窗口;提醒以「提醒队列 + 主界面提醒气泡 + 全屏强调特效」形式呈现(见 part 07)。实现者不得创建通知中心窗口,以免与主界面提醒语义冲突;若要提供历史通知列表,必须作为可选扩展并明确不属于 ClassIsland 原版功能。
### 11.16 全局快捷键与关闭行为总表
| 场景 | 快捷键 | 行为 |
|---|---|---|
| 任意窗口 | `F3` / `Shift+F3` | 切换/升级渲染调试浮层(FPS → FPS+布局/渲染耗时) |
| 任意窗口 | `F6` / `Shift+F6` | 切换触摸模式 / 同时抑制自动触摸判定 |
| 任意窗口 | `F7` | 关闭当前窗口所有 Toast |
| 设置窗口 | `Esc` | 关闭最上层抽屉/菜单,不关闭窗口 |
| 换课窗口 | `Esc` | 取消并关闭 |
| 授权窗口 | `Enter`(密码框内) | 提交认证 |
| 数据迁移/欢迎向导 | `Enter`(默认按钮) | 执行默认操作 |
| 教程编辑器 | `Ctrl+O`/`Ctrl+S`/`Ctrl+Shift+S`/`Ctrl+I` | 打开/保存/另存为/章节信息 |
| 教程编辑器 | `F5`/`Ctrl+F5`/`Shift+F5` | 运行当前教程/从当前段落运行/停止运行 |
| Excel 导入导出表格 | `Ctrl+C`/`Ctrl+V`/`Ctrl+X`/`Del` | 复制/粘贴/剪切/清除内容 |
| 日志窗口 | 无 | 通过筛选与搜索操作 |
关闭行为必须满足:
1. 非模态单例窗口(设置、日志、教学中心):关闭 = 隐藏;应用退出时统一销毁。
2. 模态窗口(欢迎向导、数据迁移、Excel 导入/导出、更新进度、授权、翻新范围):关闭 = 销毁并回传结果;更新进度窗口在更新过程中必须拒绝关闭。
3. 换课窗口:`Esc`/取消 = 直接关闭且不写入数据;确认后关闭并持久化。
4. 顶层特效窗口:仅由提醒系统控制生命周期,用户不可关闭。
5. 崩溃窗口:必须允许用户选择「忽略」继续(非 critical)或「退出/重启」。
### 11.17 验收标准
实现完成后必须逐项验证(全部通过才算合格):
1. 首次启动进入 OOBE:窗口 `800×600` 居中、不可缩放;片头字母/方块动画时长与顺序符合 11.2 第 5 条;欢迎页「下一步」可前进;OOBE 关闭时弹出「退出 ClassIsland」确认框,取消后仍停留在向导内。
2. 许可页未勾选两个复选框时「下一步」不可用;勾选后可用;系统页「在桌面创建换课快捷方式」在未勾选「注册 Url 协议」时不可用。
3. 完成页 Carousel 有 3 页,过渡为水平滑动+交叉淡入 `300ms`;最后一页按钮变为「完成」,点击后关闭窗口并按勾选项创建快捷方式。
4. 开启「显示启动加载界面」后启动:闪屏为 `400×100` 顶部居中、无边框、半透明、`17px` 内边距;Logo 与状态行分别延迟 `0/150ms` 滑入;进度条从左侧填充至 100% 后闪屏关闭。
5. 制造一次未处理异常:崩溃窗口 `800×450`,错误文本可选择复制;「反馈问题」打开 GitHub issue 页面且 URL 携带 stacktrace/app_version/os_version;critical 异常时「退出应用」使进程以退出码 1 结束。
6. 以恢复模式启动:恢复窗口 `450×600`,主页 10 个操作项齐全;进入「恢复备份」后列表、两种恢复模式单选、右下按钮布局与禁用规则符合 11.5 第 3 条。
7. 打开换课窗口:`MinWidth=730`;选中课程后自动进入第 2 步;「替换/交换」切换时右侧内容垂直滑动 `300ms`;存在临时层时确认「覆盖当前的临时层」;勾选「永久换课」后直接改写原课表;取消后课表未变化。
8. 打开 Excel 导入:拖入 `.xlsx` 后进入页面 2(欢迎),按导航映射可完整走到页面 16「完成」;选择非单行/单列区域时出现红色「选择区域无效。」且不可下一步;选区模式页头有呼吸高亮动画;导入结果写入档案后可在课表中看到对应课程;实现中不得出现 ReoGrid/WPF/`System.Windows.Forms` 依赖。
9. 打开 Excel 导出:窗口最大化,右侧三个页签在右边缘;「样式」页可改字体颜色/背景/框线并「刷新内容」;保存后的 `.xlsx` 在 Excel/WPS 中可正常打开且课表内容、合并单元格、边框与列宽行高与界面一致。
10. 打开数据迁移:窗口 `800×600` 不可缩放且为模态;导入/导出两组文案随模式切换;ClassIsland 1.x 导入需选择旧版 `ClassIsland.exe`。
11. 打开日志窗口:五类筛选与搜索同时生效;灾难/错误/警告行有不同背景色;「复制选中」只复制选中行;关闭后再次打开仍为同一窗口实例且日志继续累积。
12. 打开教学中心:`1240×550`;教程卡片按 `328px` 目标宽度自动换列;点击卡片右侧滑出 `328px` 详情栏;「播放教程」启动后出现顶部「停止当前教程」按钮与置顶「教学进行中…」控制条。
13. 所有窗口在 Windows 11 上呈现 Mica 背景、无系统标题栏;在不支持 Mica 的环境退化为纯色背景且不出现黑边或透视异常。
14. 所有模态窗口在有父窗口时居中于父窗口;所有窗口的 `Esc`、`Enter`、`F3/F6/F7` 行为符合 11.16 表。
15. 教程行动类型:构造覆盖 `Kind = 0–8` 全部 9 个值的测试教程 JSON(含 `JumpParagraph` 的两种路径形式、`InvokeUri` 的 `classisland://` 路由、`InvokeActionSet` 的空/非空 `ActionSet`、以及段落与语句两处 `InitializeActions` 中的 `NextSentence/NextParagraph/PreviousParagraph/JumpParagraph` 应被跳过),逐值验证行为与 11.13 第 5 条映射表一致;再注入 `Kind = 9`、`Kind = -1`、`Kind = "NotAKind"` 与 `kind`(小写键)等异常数据,要求:不崩溃、不卡死、未知动作被跳过、日志中出现 `unknown TutorialActionKind` warning,且后续动作照常执行。
---
## 12. 插件系统、跨进程通信、单实例与自动更新
### 12.1 目标与边界
1. 你必须实现一套**进程隔离、权限受控、可市场分发**的插件系统,能力范围对齐 ClassIsland 的插件体系(组件、提醒提供者、自动化触发器/行动、设置页、主题、信息提供者),但实现形态必须是 Electron 下的「npm 风格动态模块 + utilityProcess 宿主」,不得沿用 .NET 程序集加载。
2. 你必须实现完整的跨进程通信层:主进程 ↔ 应用窗口渲染进程 ↔ 岛渲染进程 ↔ 插件宿主进程 ↔ 更新助手。**任何进程间只传纯数据**,不得传函数、类实例、Proxy。
3. 你必须实现单实例、URI 协议唤起、自动更新、崩溃自愈与看门狗。这些能力的失败模式直接决定"教室里能不能无人值守跑一学期"。
4. 你**不得**把插件能力做成"直接在主进程 eval 用户代码"。插件崩溃/死循环不得拖垮主进程,这是本部分的第一验收项。
### 12.2 ClassIsland 插件体系事实(必须对齐的语义)
**包格式与目录**:
- 插件包扩展名 `.cipx`,本质是 zip。依据:`repos/ClassIsland/ClassIsland.Core/Abstractions/Services/IPluginService.cs:17`、`repos/ClassIsland/ClassIsland/Services/PluginService.cs:79-107`。
- 包内必须含 `manifest.yml`;解压后安装到 `Plugins/<插件Id>/`;包缓存目录为 `Cache/PluginPackages/`;插件自己的设置目录为 `Config/Plugins/<插件Id>/`。依据:`PluginService.cs:35-44`。
- 安装流程:扫描包缓存里的 `*.cipx` → 读取 `manifest.yml`(`IgnoreUnmatchedProperties` + **CamelCase 命名约定**)→ 删除同名旧目录(保留 `.disabled` 状态)→ 解压 → 删除包文件。依据:`PluginService.cs:57-107`。
- 启用/禁用状态用插件目录下的标记文件表达:`.disabled` 存在即禁用;`.uninstall` 存在表示"下次启动时删除该插件目录"。依据:`repos/ClassIsland/ClassIsland.Core/Models/Plugin/PluginInfo.cs:47-100`。
**清单字段(`manifest.yml`,YAML 键为 camelCase)**:
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| `entranceAssembly` | string | `""` | 入口程序集(本实现改为 `main.mjs` 入口文件) |
| `name` | string | `""` | 显示名称 |
| `id` | string | `""` | 插件唯一 ID(本实现要求反向域名式,如 `com.example.weather`) |
| `description` | string | `""` | 描述 |
| `icon` | string | `icon.png` | 图标相对路径 |
| `readme` | string | `README.md` | 自述文件相对路径 |
| `url` | string? | `null` | 项目地址 |
| `version` | string | `""` | 插件版本(semver) |
| `apiVersion` | string | `""` | 目标宿主 API 版本;本实现要求 `>=2.0.0` |
| `author` | string | `""` | 作者 |
| `dependencies` | `{id, isRequired}[]` | `[]` | 依赖;`isRequired` 默认 `true` |
| `supportedOSPlatforms` | string[] | 全平台 | 有效值 `windows`/`linux`/`osx` |
依据:`repos/ClassIsland/ClassIsland.Core/Models/Plugin/PluginManifest.cs:12-75`、`repos/ClassIsland/ClassIsland.Core/Models/Plugin/PluginDependency.cs:6-17`。
**清单命名兼容(强制)**:本实现统一写 **camelCase**(与 ClassIsland `manifest.yml` 的 CamelCase 约定一致);同时必须**兼容读取** PascalCase 旧清单(`Id`/`Name`/`EntranceAssembly`/`ApiVersion`/`SupportedOSPlatforms` 等)。加载时必须先做 `camelCase ↔ PascalCase` 归一化再校验;同名键以 camelCase 为准(不得让旧拼写覆盖新值);无法识别的键忽略并记 debug 日志。依据:`PluginService.cs:68-72`(`IgnoreUnmatchedProperties` + `CamelCaseNamingConvention`)。
**加载规则**:
- 按依赖拓扑排序加载;必选依赖缺失或不可加载 → 该插件置为 `Error`;检测到循环依赖必须抛错并拒绝整组。依据:`PluginService.cs:288-339`。
- `apiVersion < 2.0.0.0` → 拒绝加载并记录 `Error` 与异常说明(对齐 `PluginService.cs:168-173`)。
- 加载状态枚举固定四种:`NotLoaded`/`Loaded`/`Disabled`/`Error`,并带 `Exception` 供设置页展示。依据:`repos/ClassIsland/ClassIsland.Core/Enums/PluginLoadStatus.cs`。
- 每个插件有独立的"注册上下文":插件在 `Initialize` 期间注册的组件会自动归属该插件。依据:`PluginService.cs:211-221`(`ComponentRegistryService.CurrentRegisteringPlugin`)。
- 加载失败/损坏的插件必须在下次启动自动禁用(`Settings.AutoDisableCorruptPlugins` 默认 `true`,`Settings.CorruptPluginsDisabledLastSession` 记录上次会话是否发生过自动禁用)。依据:`repos/ClassIsland/ClassIsland/Models/Settings.cs:1844-1845`。
**插件市场**:
- 索引结构 `PluginIndex { plugins[], downloadMirrors{ name: root } }`;条目在 `PluginInfo` 之上增加 `downloadUrl` 与 `downloadMd5`。依据:`repos/ClassIsland/ClassIsland.Core/Models/Plugin/PluginIndex.cs:12-21`、`PluginIndexItem.cs:8-19`。
- 下载镜像:镜像根替换下载链接里的模板;内置回退镜像 `github`、`ghproxy`、`moeyy`;官方索引地址带时间戳参数 `https://get.classisland.tech/d/ClassIsland-Ningbo-S3/classisland/plugin/index.zip?time={time}`;索引默认 7 天刷新一次。依据:`repos/ClassIsland/ClassIsland/Services/PluginMarketService.cs:43-48,66-69,212`。
- 设置项:`userPluginIndexes`、`additionalPluginIndexes`、`pluginIndexSelectedMirrors`、`officialSelectedMirror`(默认 `github`)、`ignoreSslForPluginMirrors`(默认 `false`)、`isPluginsAutoUpdateEnabled`(默认 `true`)、`isPluginsUpdateNotificationEnabled`(默认 `true`)、`isPluginMarketWarningVisible`。依据:`Settings.cs:166-178,1858-1859,204`。
- SDK 打包:插件项目 `CreateCipx=true` 时,构建后自动把输出目录压成 `<项目名>.cipx`,并在 `checksums.md` 里写一行 HTML 注释形式的 MD5 JSON(`<!-- CLASSISLAND_PKG_MD5 {...} -->`)。依据:`repos/ClassIsland/ClassIsland.PluginSdk/ClassIsland.PluginSdk.targets`(`CreateCipx` Target)与 `generate-md5.ps1`。
### 12.3 本实现的插件规范(必须逐项落地)
**包与目录**:
1. 插件包扩展名沿用 `.cipx`(zip),内部结构固定:
```text
my-plugin.cipx
├─ manifest.yml # 必须存在;字段见下表
├─ main.mjs # 必须存在;ESM 入口,默认导出 activate/deactivate
├─ icon.png # 可选,默认 icon.png
├─ README.md # 可选
└─ assets/ # 可选静态资源(只允许相对路径引用)
```
2. 运行期目录:`<userData>/plugins/<id>/`(解压后)、`<userData>/plugin-cache/packages/*.cipx`(下载缓存)、`<userData>/plugin-data/<id>/`(插件配置与缓存)、`<userData>/plugin-cache/index.json`(市场索引缓存)。必须与 13 部分的目录规划一致。
3. 启用状态用 `<pluginDir>/.disabled` 标记,卸载用 `<pluginDir>/.uninstall` 标记;**不得**用配置里的布尔值表达启用状态(否则用户手工操作目录后状态会不一致)。依据:`PluginInfo.cs:47-100`。
4. 插件数据写入必须由宿主代理:插件调用 `ctx.storage.set(key, value)`,宿主写到 `<userData>/plugin-data/<id>/storage.json`,**不得**给插件直接 `fs` 访问。
**清单(`manifest.yml`,camelCase,字段与默认值必须与 12.2 表一致,另加以下扩展)**:
```yaml
id: com.example.weather-plus
name: 天气增强
description: 提供逐小时降水提醒
author: Example
version: 1.2.0
apiVersion: 2.0.0
icon: icon.png
readme: README.md
entry: main.mjs
url: https://example.com/weather-plus
supportedOSPlatforms: [windows, linux, osx]
dependencies:
- id: com.classisland.core-miniinfo
isRequired: true
permissions:
- notifications:push # 允许发提醒
- automation:register # 允许注册触发器/行动
- components:register # 允许注册岛组件
- settings:page # 允许注册设置页
- storage:read
- storage:write
- network:api.example.com # 仅允许访问列出的域名(精确匹配,禁止通配)
```
5. `permissions` 是**本实现在 ClassIsland 基础上的强制安全增强**:ClassIsland 的插件是 DLL 全信任,本实现必须默认拒绝、按声明放行。任何未声明的能力调用都必须抛 `PluginPermissionError` 并写 warn 日志;设置页必须在启用前展示权限清单并二次确认。**【推断】**
6. `apiVersion` 必须是 semver;宿主 API 当前版本固定为 `2.0.0`,`apiVersion` 主版本不同即拒绝加载;次版本号高于宿主时给出"可能不兼容"警告但仍允许(与 ClassIsland 的"最高版本门槛"语义相反,此处以兼容性优先【推断】)。
7. 依赖解析必须:先校验所有 `isRequired` 依赖存在且 `Loaded`;再做拓扑排序;循环依赖必须报错并列出环上的 ID;可选依赖缺失只记 info。依据:`PluginService.cs:306-339`。
**生命周期与宿主**:
1. 插件宿主必须是 `utilityProcess.fork(path.join(__dirname,'plugin-host.js'))` 启动的独立进程;插件代码在该进程内用 `await import(pathToFileURL(entry))` 加载。**不得**在主进程 `import()` 插件代码。
2. 宿主暴露给插件的 API 对象(`ctx`)只允许包含:`ctx.manifest`、`ctx.apiVersion`、`ctx.log`、`ctx.events`、`ctx.storage`、`ctx.notifications`、`ctx.components`、`ctx.automation`、`ctx.settings`、`ctx.themes`、`ctx.commands`、`ctx.timers`。禁止暴露 `process`、`require`、`fs`、`child_process`、`net`。
3. 插件导出的入口签名固定:
```js
export async function activate(ctx) { /* 注册能力;不要在这里做耗时 I/O */ }
export async function deactivate(ctx) { /* 清理定时器与监听器 */ }
export const manifest = {/* 可选:与 manifest.yml 一致 */};
```
4. 宿主必须为每个插件维护独立状态机:`discovered → validating → disabled | loading → loaded | error → restart-required`。状态与错误信息通过 IPC `classisland:plugins:list` 交给设置页渲染。
5. 插件加载超时必须为 5 秒;单个插件崩溃后宿主必须重启该插件(最多 2 次),仍失败则置 `error` 并在**下一次启动**写入 `.disabled`(对齐 `AutoDisableCorruptPlugins` 语义)。同一次运行内不得无限重启。
6. 启用/禁用/卸载必须标记"需要重启",并在 UI 上明确显示「重启 ClassIsland 后生效」;不得热替换已加载插件(热替换会留下旧的全局监听器)。依据:`PluginInfo.RestartRequired`(`PluginInfo.cs:159-172`)。
7. 插件注册的扩展点必须进入统一注册表,重复 ID 时后注册者失败并记录错误(不得静默覆盖)。依据:ClassIsland 的 `ComponentRegistryService`/`RulesetRegistryExtensions` 单注册表模式。
**扩展点(必须全部实现并映射)**:
| ClassIsland 扩展点 | 插件 API | 说明 |
|---|---|---|
| 组件(`ComponentBase` 派生 + 注册表) | `ctx.components.register({id, name, group, settingsSchema, render})` | 组件设置必须用声明式 schema;`render` 是可序列化描述或受控的 Vue 组件名,不得注入任意 DOM |
| 提醒提供者(`NotificationProvider`) | `ctx.notifications.registerProvider({id, name, request})` | 提供者只能"请求提醒",是否播放由宿主队列决定 |
| 自动化触发器 | `ctx.automation.registerTrigger({id, name, schema, setup})` | 触发回调可发信号给宿主 |
| 自动化行动 | `ctx.automation.registerAction({id, name, schema, run})` | 行动执行受权限约束(执行外部程序需 `exec` 权限) |
| 设置页 | `ctx.settings.registerPage({id, title, schema, section})` | 只能是声明式 schema,由宿主统一表单渲染;**不得**提供任意 HTML/iframe(见下方安全红线) |
| 主题 | `ctx.themes.registerManifest(themeManifest)` | 主题包格式见 9 部分 |
| 迷你信息提供者 | `ctx.miniInfo.registerProvider({id, name, fetch})` | 天气/温度等文本信息,带超时与缓存 |
| 设置叠层 | `ctx.settings.overlay({key, value})` | 对齐 ClassIsland `AddSettingsOverlay` 的"临时覆盖并自动还原"语义(`repos/ClassIsland/ClassIsland/Services/SettingsService.cs:191-259`) |
**设置页安全红线(强制)**:插件设置页**只能**是声明式 schema(字段名、类型、默认值、校验规则、布局分组、可见性条件),由宿主用统一表单渲染;**禁止**插件注入任意 HTML/CSS/脚本,**禁止**用 iframe/webview 承载插件设置页,**禁止**插件直接访问 DOM 或 `window`。schema 允许的控件仅限:文本、多行文本、数字、开关、下拉、单选、多选、颜色、时间、文件路径(由宿主弹选择器)、按钮(只能触发宿主声明允许的行动)。任何试图绕过 schema 的插件必须加载失败并记 error。
**插件市场(本实现)**:
1. 索引 JSON schema 必须为:
```json
{
"plugins": [{
"id": "com.example.weather-plus",
"name": "天气增强",
"description": "……",
"author": "Example",
"version": "1.2.0",
"apiVersion": "2.0.0",
"supportedOSPlatforms": ["windows"],
"downloadUrl": "{root}/example/weather-plus/releases/download/v1.2.0/weather-plus.cipx",
"downloadMd5": "…32位十六进制…",
"downloadSha256": "…64位十六进制…",
"downloadSize": 1048576,
"repoOwner": "example",
"repoName": "weather-plus",
"assetsRoot": "master",
"artifactName": "weather-plus.cipx"
}],
"downloadMirrors": { "github": "https://github.com", "ghproxy": "https://mirror.ghproxy.com/https://github.com" }
}
```
2. `downloadUrl` 必须支持 `{root}` 模板,按当前选中镜像替换;镜像失败必须按列表回退,不得只试一个。
3. 索引必须支持"多源":**默认不内置在线官方索引**,只有用户源(`userPluginIndexes`);内置源只允许是随安装包分发的离线示例索引。每源独立记录"最后刷新时间/错误/是否过期";7 天未刷新时后台刷新,失败保留旧索引并标 `stale`。依据:`PluginMarketService.cs:43-69,107-120`(ClassIsland 的刷新语义)+ 本应用默认离线策略【推断】。
4. 下载必须**同时校验 MD5 与 SHA-256**:MD5 用于兼容 ClassIsland 生态,SHA-256 用于本实现的安全底线;任一不符即删除文件并报错。不得"只校验 MD5"。
5. 下载完成后必须走与安装包相同的原子流程(`.part` → 校验 → rename),不得把半截 zip 交给解压器。
6. 插件更新必须遵守「上课不打断」:`IsPluginsAutoUpdateEnabled=true` 时,只在**非上课时段**且设置窗口不可见时才后台下载;安装必须等用户确认或下次启动。依据:ClassIsland 默认值(`Settings.cs:1858-1859`)+ 本应用"零打扰"原则。
7. **默认离线(强制)**:安装包只能内置一份离线示例索引(`resources/plugin-index.example.json`,0–2 条示例,默认不可安装或只指向本地文件)。用户必须**手动添加索引 URL 并二次确认**后才允许联网;确认弹窗必须写明"将从该地址下载并执行第三方代码,请确认来源可信"。网络失败、索引 404、源不可达均不得影响本机已安装插件与本地索引。主题市场遵循同一默认策略(详见 9 部分)。
### 12.4 跨进程通信(IPC)设计(必须完整实现)
**通道命名与组织**:
1. 通道名必须为 `classisland:<domain>:<action>` 的 kebab 形式,全部集中在 `src/main/channels.ts`(**零 import 叶子模块**)。依据:`repos/ClassHelper-master/packages/desktop-client/src/main/channels.ts:16-28`。
2. 通道分三类,不得混用:
- `invoke/handle`:请求-响应(读写配置、查询状态、执行命令);
- `send/on`:单向事件(岛事件、心跳、交互上报);
- `webContents.send`:主进程→渲染进程推送(状态、外观、进度)。
3. 必须实现的通道清单(最小集,新增通道必须同表登记):
| 通道 | 方向 | 用途 |
|---|---|---|
| `classisland:config:get` / `:save` / `:reset` | 双向 | 读/局部写/重置配置;`:save` 必须剔除更新安装字段 |
| `classisland:app:info` | 双向 | 版本、Electron/Chrome/Node 版本、平台、userData、配置路径 |
| `classisland:app:get-autolaunch` / `:set-autolaunch` | 双向 | 开机自启(以系统登录项为准) |
| `classisland:app:open-external` / `:open-log-dir` / `:open-devtools` | 双向 | 受控系统能力 |
| `classisland:app:window` | 双向 | 显示/隐藏/最小化/聚焦设置窗口 |
| `classisland:profile:list` / `:load` / `:save` / `:create` / `:delete` / `:import` / `:export` | 双向 | 档案增删改查与导入导出 |
| `classisland:schedule:get` / `:save` | 双向 | 课表/时间表读写 |
| `classisland:notification:list` / `:clear` / `:test` | 双向 | 提醒列表与测试提醒 |
| `classisland:automation:list` / `:save` / `:run` | 双向 | 自动化配置与手动触发 |
| `classisland:plugins:list` / `:enable` / `:disable` / `:uninstall` / `:install-package` / `:market-refresh` | 双向 | 插件管理 |
| `classisland:update:check` / `:download` / `:install` / `:progress` | 双向 | 更新 |
| `classisland:backup:list` / `:create` / `:restore` | 双向 | 备份 |
| `island:push` / `island:remove-notification` / `island:class-state` / `island:get-state` | 双向 | 主窗口→岛 |
| `island:set-appearance` / `island:get-appearance` | 双向 | 外观热更新 |
| `island:action` / `island:set-interactive` / `island:set-hit-rect` / `island:set-touch-mode` / `island:alive` / `island:report-size` / `island:viewport` | 岛→主进程 | 用户操作、命中、触摸、心跳、尺寸上报 |
| `island:state` / `island:appearance` / `island:visibility` / `island:schedules` / `island:time-points` / `island:layout` / `island:editing` | 主进程→岛 | 状态与数据推送 |
| `island:mark-read` / `island:mark-all-read` / `island:open-editor` | 主进程→应用窗口 | 岛侧操作回流到应用窗口/编辑器 |
依据:`repos/ClassHelper-master/packages/desktop-client/src/main/channels.ts:16-28`、`src/preload/island.ts:64-121`、`src/preload/index.ts:25-99`(通道命名与成对关系)。
**Payload 与错误约定**:
4. 所有跨桥接数据必须是结构化克隆可序列化值:无函数、无 Symbol、无 Proxy、无 `Date` 之外的自定义类;`Error` 必须转成 `{code, message, detail?}` 普通对象。依据:`src/preload/index.ts:17-24` 的注释。
5. 单个 IPC 消息必须 ≤ 8MB;大文件(备份、导出档案、主题包)一律"传路径不传内容",由主进程读文件。
6. 所有 `invoke` 处理必须:校验 sender 必须是本应用的窗口(比对 `event.senderFrame.url` 白名单)→ 校验参数(类型、范围、长度)→ 执行业务 → 返回 `{ok:true, data}` 或 `{ok:false, error}`。**不得让异常穿透 IPC 抛给渲染层**(生产构建的堆栈会泄漏内部路径)。
7. 每次 `invoke` 在渲染层必须包一层 10 秒超时;超时必须提示"操作超时,请重试",不得让按钮永久转圈。
8. 高频事件必须节流:外观滑块 ≤ 20 次/秒(50ms)、岛状态推送 100ms 合并(尾部保留最后一次)、心跳 1 次/秒。依据:`island.ts:216-224`、`auto-update.ts:216-219` 的节流写法。
9. 事件订阅必须可取消:preload 上的 `onXxx(handler)` 必须返回 `() => void` 移除监听;组件卸载时调用。依据:`src/preload/index.ts:32-36,56-60`。
**岛窗口专用通信纪律**:
10. 岛渲染进程只允许两类操作:订阅主进程推来的状态/外观/课表,以及上报用户操作与命中信息。不得让岛直接读写配置。依据:`src/preload/island.ts:10-13` 的注释。
11. 命中控制必须"只上报 true,关闭由主进程按真实光标位置决定":双向下发会让窗口在"接收/穿透"之间抖动,表现为"点了没反应"。依据:`src/types/desktop.d.ts:221-228`。
12. 岛必须每秒发一次心跳(`alive()`);主进程超过 3 秒未收到心跳即判定"幽灵窗口",隐藏并重建。依据:`src/main/island.ts:425-430,541-599`(`lastAliveAt`、`checkRendererAlive` 思路)。
13. 触摸屏必须上报 `setTouchMode(navigator.maxTouchPoints > 0)`;触摸模式下窗口必须贴合岛体(岛体+10px 阴影留白)并始终接收输入。依据:`src/main/island.ts:245-316`。
14. 主进程必须对岛窗口做导航白名单与 `window-open(e) = deny`;岛是置顶窗口,被导航走等于把 `window.island` 桥交给陌生页面。依据:`src/main/island.ts:32-50,516-522`。
**可选本地只读 HTTP API(外部程序集成,默认关闭)**:
15. 本实现**不恢复** ClassIsland 的命名管道 IPC;如需让外部程序(脚本、班级大屏看板、第三方展示页)读取当前课程,只能启用"本地只读 HTTP API",且必须满足以下全部约束:
- 只监听 `127.0.0.1`(不得监听 `0.0.0.0`、不得绑定局域网地址);
- 端口**随机分配**(`listen(0)` 后读取实际端口),端口与开关写入 `config.localApi`,默认 `enabled=false`;
- 开启时必须生成**一次性 token**(随机 32 字节,base64url),请求必须带 `Authorization: Bearer <token>`;token 在设置页可见/可重置,关闭即失效;
- **只提供读接口**:`GET /v1/status`(当前时间点/上下课状态/剩余时间)、`GET /v1/schedule/today`(当天课表)、`GET /v1/profile/summary`(档案名/科目表摘要);**不得提供任何写操作**(不得改课表、不得发提醒、不得触发自动化、不得改配置);
- 响应必须含 CORS 白名单(默认不放开跨域;放开需用户在设置页显式勾选并提示风险);
- 每次启用/停用必须写审计日志;非法 token 返回 401 且限流(≥10 次/分钟即临时封禁)。
依据:ClassIsland 的 `IPublicLessonsService`/`IPublicProfileService` 是"读服务为主"的公开契约(`repos/ClassIsland/ClassIsland.Shared.IPC/Abstractions/Services/IPublic*.cs`),本实现以只读 HTTP 等价替代,不引入写能力。**【推断】**
### 12.5 单实例与 URI 协议(必须实现)
1. 单实例:`app.requestSingleInstanceLock()`;未拿到锁必须 `app.quit()` 并立即返回;拿到锁必须监听 `second-instance` → 显示/聚焦主窗口(若被最小化先 `restore`)。依据:`repos/ClassHelper-master/packages/desktop-client/src/main/index.ts:176-182,279-285`。
2. `second-instance` 必须解析新实例的 `argv`:若带 `classisland://` URI,转交现有实例导航;若带 `--hidden` 只做唤醒不抢焦点。参考 `repos/ClassIsland/ClassIsland/Program.cs:88-110,141-159` 的"第二次启动把 URI 通过 IPC 发给已运行实例后退出"语义。
3. 必须注册 `classisland://` 协议:Windows 用 `app.setAsDefaultProtocolClient('classisland')` 并在 NSIS 安装器中登记;macOS 监听 `open-url`;Linux 通过 `.desktop` 的 `MimeType=x-scheme-handler/classisland`。
4. URI 语法固定:`classisland://<host>/<path>?<query>`;允许的 host 与行为:
- `app/settings/<page>` → 打开设置页(例:`classisland://app/settings/update` 对齐 ClassIsland `UpdateService.cs:326`);
- `app/notification` → 打开提醒历史;
- `lesson/current` → 显示当前课程详情;
- `profile/import?path=<urlencoded>` → 导入档案(必须二次确认);
- `action/<id>` → 触发自动化行动(仅当该行动声明 `uriTrigger=true` 且用户在设置中允许)。
未知 host/path 必须忽略并提示「无法识别的链接」,不得静默执行。URI 长度上限 2048,必须 URL 解码后再解析,禁止 `..` 路径穿越。
5. 单实例锁必须与"冒烟/隔离配置"兼容:验收时有独立的 `userData`,否则会被真实实例的锁挡住而"静默退出(退出码 0、无输出)"。依据:`src/main/index.ts:29-32` 注释。
### 12.6 自动更新(必须按此流程实现)
**来源与通道**:
1. 更新通道至少三档:`stable`(默认)、`beta`、`dev`;通道描述含 `id`、`name`、`description`、`rootUrl`。**默认只接受 `https://`**;镜像/自定义源必须由用户显式开启并二次确认"非官方来源风险",默认不启用。清单必须校验 SHA-256;清单签名可选,一旦提供则必须是 Ed25519(带 `keyId`),客户端内置公钥表(支持多 keyId,便于轮换),验签失败必须拒绝整个更新;未签名清单必须在设置页显著标注"未签名发布"并写审计日志。依据:`repos/ClassIsland/ClassIsland/Services/AppUpdating/UpdateService.cs:660-669`(`DetachedSignatureProcessor` 的验签语义)。
2. 检查更新必须支持:启动后延迟 30 秒检查一次、之后每 6 小时一次、设置页手动检查、`classisland://app/settings/update` 深链进入。检查间隔与 `LastCheckUpdateTime` 必须持久化,避免频繁联网。依据:`Settings.cs:75-78`。
3. 更新状态必须走固定枚举:`UpToDate`(已是最新)、`UpdateAvailable`(发现新版)、`UpdateDownloaded`(已下载)、`UpdateDeployed`(已部署待重启);工作状态:`Idle`、`Checking`、`Downloading`、`Extracting`。依据:`repos/ClassIsland/ClassIsland.Shared/Enums/UpdateStatus.cs`、`UpdateWorkingStatus.cs`。
**清单与下载**:
4. 更新清单必须包含:`version`、`releaseNotes`、`pubDate`、`minOsVersion`、`files[{path,size,sha256,url}]`、`installer`(整包)或 `components`(分组件增量);清单可选附带 `signature{alg:"ed25519", keyId, sig}`,只要附带就必须校验通过。**不得**只给一个"最新版本号"就让客户端猜下载地址。
5. 下载必须:先查磁盘空间(≥ 3 倍包大小)→ 写 `.part` → 流式写盘(不得整包读进内存)→ 校验长度 → 校验 SHA-256 → `rename` 为正式文件。依据:`repos/ClassHelper-master/packages/desktop-client/src/main/auto-update.ts:342-394`。
6. 下载必须支持断点续传与超时(单包 10 分钟);网络失败必须按 1/2/4/8 分钟退避重试 3 次,仍失败则置 `failed` 并展示错误码,不得无限重试。依据:`auto-update.ts:124-125,407-414`。
7. 增量更新必须至少支持"按文件 SHA-256 跳过未变化文件"的差分(对齐 `UpdateService.cs:447-485` 的 `AllowDiffUpdate` 语义);差分失败必须自动回退到整包下载,不得让用户停在半更新状态。
**安装时机与安全**:
8. 安装策略三档,必须严格实现:
- `immediate`:下载校验完成后立即安装(仍须满足"不在上课"除非用户显式确认);
- `after-school`:到达 `updateNotBefore`(放学时间)后且不在上课才安装;
- `idle`(默认):不在上课、且主窗口/设置窗口隐藏满 5 分钟(`CLIENT_UPDATE_IDLE_MS`)才安装。依据:`packages/shared/src/constants.ts:677`、`auto-update.ts:474-506`。
9. 关闭自动更新(`autoUpdate=false`)时,**不得**安装任何更新,但仍可展示"有可用更新"的提示;不得静默强装。依据:`auto-update.ts:280-296,458`。
10. 安装必须交给独立助手,并采用**固定安装目录 + 回滚副本**(不做 side-by-side 多版本并存):主进程把安装包路径、SHA-256、版本、回滚目录写入 `update-plan.json` → 生成/校验助手 → 助手就绪回执 → 主进程退出 → 助手等待旧进程结束 → 先把当前安装目录的关键文件(`ClassIsland.exe`、`resources/app.asar` 等)复制到 `updates/rollback/<旧版本>/` → 替换/安装 → 启动新版并检查版本 → 写 `install-result.json`(成功/失败+错误)→ 失败用回滚副本恢复旧版并重新拉起。依据:`auto-update.ts:516-573`(就绪→commit→退出→结果文件)与 `repos/ClassIsland/ClassIsland/Models/AppUpdating/DeploymentLock.cs`(记录已存在文件与组件根目录以便回滚)。
11. 安装过程中必须避免"文件被占用":不得覆盖正在运行的 `exe`/`asar`;助手必须从 `updates/` 目录运行。依据:`auto-update.ts:75-88`(专用目录、只接受目录内 exe)。
12. 安装包路径必须做双重校验:`path.dirname(realpath(file)) === realpath(updatesDir)` 且扩展名 `.exe`;任何不符必须拒绝执行并清空待安装记录。依据:`auto-update.ts:477-490`。
13. 更新失败必须留下可诊断证据:`update.log`(含 HTTP 状态、哈希期望/实际、助手退出码),并在设置页"更新"页展示最近一次失败原因(可复制)。
14. 版本回退必须保留上一版安装包与 `updates/rollback/<旧版本>/` 回滚副本及"上次可用版本"标记;新版启动失败(连续 2 次崩溃)必须提示"回退到上一版本"并给出按钮。**【推断】**
15. 插件更新与主程序更新必须**分开**:插件更新不得要求重启整个应用(除非插件声明 `restartRequired`);主程序更新必须重启。两者进度不得互相覆盖同一个 `policy.json`。
### 12.7 崩溃自愈与看门狗(必须实现)
1. 主进程必须监听以下事件并分别处理(不得合并成一个"崩溃就重启"):
| 事件 | 处理 | 限流 |
|---|---|---|
| 设置窗口 `render-process-gone` | 静默销毁并重建窗口;保留配置与状态 | 10 分钟内最多 3 次,超限只记日志 |
| 设置窗口 `unresponsive` 超过 10 秒 | 重建窗口;期间 `responsive` 恢复则取消 | 同上 |
| 岛 `render-process-gone` | 隐藏死窗口 → 重建岛窗口 → 重推状态 | 同上 |
| 岛心跳超时(>3 秒) | 判定幽灵窗口 → 隐藏并重建 | 同上 |
| `child-process-gone` | 记录 type/reason/exitCode;插件宿主崩溃按插件粒度重启 | — |
| 主进程 `uncaughtException` | 记录完整堆栈;仅当"不在上课且所有窗口不可见"才自动重启 | 10 分钟内最多 3 次 |
| 内存超限 | 1.5GB(`MemoryLimitBytes=1500000000`)时触发一次自重启(带静默参数),并记录 `-psmk` 语义的"上次因 MLE 退出"标记 | 每次启动最多 1 次 |
依据:`repos/ClassHelper-master/packages/desktop-client/src/main/crash-guard.ts:20-25,49-75,103-184`;`repos/ClassIsland/ClassIsland/Services/MemoryWatchDogService.cs:16-70`(60 秒检查一次、1.5GB 阈值、用 `-q -m -psmk` 重启)。
2. 重建窗口前必须 `hide()` 再 `destroy()`,避免留下一帧残影或白屏。依据:`crash-guard.ts:139-145`。
3. 冒烟/自动化验收模式下**绝对禁止** `app.relaunch()`(会把验证父进程搞乱);这条必须写进代码分支并以环境变量控制。依据:`crash-guard.ts:153-159`。
4. 岛窗口自愈必须同时处理"渲染进程没了"和"窗口还在但点不动"两种形态;后者只能靠心跳发现。依据:`island.ts:583-599`。
5. 所有自愈操作必须写日志:`reason`、`exitCode`、本窗口期第几次、是否达到限流上限。日志是教室现场排障的唯一证据。
### 12.8 验收标准(第 12 部分)
1. 插件安装/启用/禁用/卸载全流程可用:安装后重启生效、`.disabled` 标记正确、卸载后目录被删除;同一插件重复安装不丢用户配置。
2. 依赖排序正确:A 依赖 B 时先加载 B;必选依赖缺失时 A 状态为 `Error` 且错误信息可在设置页看到;循环依赖给出明确错误。
3. 插件崩溃不会导致主进程退出:杀死插件宿主进程后,主进程存活、岛继续刷新、设置页显示"插件已停止"。
4. 未声明权限的插件调用受控 API 时必须被拒绝并有日志,且不会抛到渲染进程。
5. 插件市场能读取索引、切换镜像、下载并校验 MD5+SHA-256;校验失败时文件被删除并报错。
6. 通道白名单:渲染进程无法调用未登记通道;`window.desktop` 中不存在 `ipcRenderer`/`fs`/`process`。
7. 任意跨桥接传参若含 Vue 响应式对象,必须抛出可读错误(`An object could not be cloned.`)而不是静默失效;所有调用点已展开为纯数据。
8. 第二次启动应用不产生第二个实例,而是唤起已有窗口;`classisland://app/settings/update` 能在已运行实例中打开更新页。
9. 更新全流程:检查 → 下载 → 校验 → 等待空闲 → 助手安装 → 新版启动写回执;`autoUpdate=false` 时绝不安装;上课时段绝不安装。
10. 更新助手失败时能回滚并写 `install-result.json`,设置页能展示失败原因。
11. 主窗口渲染进程崩溃后 10 分钟内最多重建 3 次,之后停止并可人工介入;日志含原因与次数。
12. 内存看门狗在超过 1.5GB 时自重启,且重启后保留配置、跳过一次引导、记录 MLE 标记。
13. 岛心跳超时能触发重建;重建后状态、课表、外观全部恢复(不出现空白岛)。
14. 所有新增 IPC 通道都登记在 `channels.ts` 与 12.4 表中,且类型定义在 `packages/shared/src/ipc.ts` 中单一来源。
15. 可选本地 HTTP API 默认关闭;开启后只监听 `127.0.0.1`、端口随机、请求必须带一次性 token、只存在 `GET /v1/status`、`GET /v1/schedule/today`、`GET /v1/profile/summary` 三个只读接口,任何写请求必须返回 405;关闭后端口立即不可达。
16. 插件市场默认只有离线示例索引;添加外部索引 URL 必须二次确认后才联网;断网/源不可达时本机插件与本地索引不受影响。
17. 插件设置页只能渲染声明式 schema;插件若尝试注入 HTML/iframe 或直接操作 DOM,必须被拒绝并记 error。
18. 插件清单 camelCase 写入、PascalCase 旧清单可正常加载(同一插件两种清单导入后行为一致)。
19. 更新默认只接受 `https://`;未签名清单在设置页显示"未签名发布"标记;签名清单的 `keyId` 未在公钥表内时拒绝更新。
20. 更新安装使用固定安装目录,替换前生成 `updates/rollback/<旧版本>/` 回滚副本;安装失败能回滚并写 `install-result.json`;磁盘上不出现 side-by-side 并存的多个 `app-<版本>` 目录。
---
## 13. 数据模型、存储、备份与迁移
### 13.1 存储总则(必须遵守)
1. 本应用是**纯本地单机应用**,所有设置、档案、课表、组件、自动化、提醒历史、主题、插件配置必须存在本机,**不得**依赖任何服务器;断网、无账号、首次启动都必须完整可用。
2. 存储根目录必须是 `app.getPath('userData')`(Windows 为 `%APPDATA%\ClassIsland`,macOS 为 `~/Library/Application Support/ClassIsland`,Linux 为 `~/.config/ClassIsland`)。**不得**把用户数据写进安装目录(安装目录可能只读、且升级会被整体替换)。依据:`repos/ClassHelper-master/packages/desktop-client/src/main/config.ts:165-167`(`path.join(app.getPath('userData'), 'config.json')`)与 `repos/ClassIsland/ClassIsland.Core/CommonDirectories.cs:16-28`(ClassIsland 的 `AppRootFolderPath`/`Config`/`Logs` 划分)。
3. 所有持久化文件必须是 **UTF-8 无 BOM**、LF 换行、两空格缩进的 JSON;字段名大小写必须按第 13.3/13.4 节的约定(ClassIsland 兼容文件用 PascalCase,本应用运行期配置用 camelCase)。读入时必须**同时容忍**紧凑格式(无缩进、无换行)与带注释之外的一切合法 JSON;**不得**写注释(ClassIsland 的 `examples/example.json` 带注释只是示例,真实加载器不支持注释,见 `repos/ClassIsland/examples/example.json:1-2`)。
4. 所有写入必须**原子化**:写 `<file>.tmp` → `fsync` → `rename` 覆盖目标;禁止直接 `writeFile` 覆盖正在被读的文件。依据:`repos/ClassIsland/ClassIsland.Shared/Helpers/ConfigureFileHelper.cs:182-188`(`FileStream.Flush(true)` 后关闭)。
5. 每次成功读取 JSON 后必须把原文件复制为 `<file>.bak`;读取失败时自动回退加载 `.bak`,成功后把 `.bak` 复制回目标(对齐 `ConfigureFileHelper.LoadConfigUnWrapped` 的语义,见 `ConfigureFileHelper.cs:63-96`)。若 `.bak` 也失败,必须使用**全默认值**并把错误加入"配置错误列表"(`ConfigError{ Path, Code, IsFallback, Exception }`,见 `repos/ClassIsland/ClassIsland.Shared/Models/ConfigError.cs`),在设置页"诊断/存储"中展示。
6. 所有配置对象必须"**缺失字段用默认值补齐、未知字段忽略、越界值夹紧**"。禁止因为新增字段就让老配置文件加载失败。依据:`repos/ClassIsland/ClassIsland/Services/SettingsService.cs:63-118`(加载失败只记日志并继续)与 `config.ts:198-233`(逐字段归一化)。
7. 所有写入必须经过**单一写入入口**(`storage.ts` 的 `saveXxx()`),禁止业务代码直接 `fs.writeFile`;该入口负责合并写、备份、事件通知。依据:`config.ts:235-346`。
### 13.2 目录与文件清单(必须逐项命名一致)
```text
<userData>/ # app.getPath('userData')
├─ config.json # 运行期设置(camelCase;对应 ClassIsland Settings.json)
├─ Settings.json # ClassIsland 格式兼容文件(PascalCase;仅只读导入/导出,运行期不读写)
├─ data/
│ ├─ profiles/
│ │ ├─ Default.json # Profile 档案(PascalCase,与 ClassIsland 同构)
│ │ └─ <档案名>.json
│ ├─ component-layouts/
│ │ └─ Default.json # ComponentProfile(PascalCase)
│ ├─ automations/
│ │ └─ Default.json # Workflow[] 数组(PascalCase)
│ ├─ themes/
│ │ ├─ <主题Id>/ # 主题包解压目录(manifest、样式、资源)
│ │ └─ EnabledThemes.json # string[],启用中的主题 Id
│ ├─ plugins/ # 插件解压目录:<插件Id>/
│ ├─ plugin-data/ # 插件私有存储:<插件Id>/storage.json
│ ├─ notification-history.json # 通知历史(用于提醒历史视图)
│ └─ generated/
│ └─ default-subjects.json # 首次启动写入的默认科目表
├─ backups/ # 自动/手动备份 zip
├─ logs/
│ └─ main.log # 超过 1MB 轮转为 main.log.1
├─ cache/
│ ├─ plugin-index/ # 插件市场索引缓存
│ ├─ theme-packages/ # 主题包下载缓存 <id>.zip
│ └─ update/ # 更新清单/元数据缓存
└─ updates/ # 待安装更新包、policy.json、install-result.json
```
依据:`repos/ClassIsland/ClassIsland/Services/PluginService.cs:35-44`(插件三目录)、`XamlThemeService.cs:47-49`(主题两目录 + `EnabledThemes.json`)、`AutomationService.cs:27-32`(自动化目录 + `<CurrentAutomationConfig>.json`)、`ComponentsService.cs:28-58`(`ComponentLayouts/<CurrentComponentConfig>.json`)、`ProfileService.cs:48-58`(`Profiles/` 与 `<SelectedProfile>`)、`FileFolderService.cs:23-49`(需要预创建的目录清单)、`auto-update.ts:131-133`(`updates/`)、`logger.ts:32-39`(`logs/main.log`)、`config.ts:165-167`(`config.json`)。
### 13.3 `config.json` 完整 schema(字段级,含默认值)
`config.json` 是 ClassIsland `Settings.json` 的超集;下表的"ClassIsland 对应字段"必须保留语义,默认值必须与源码一致。**任何字段缺失都必须落到默认值,任何越界值都必须夹紧到合法区间。**
**`Settings.json` 兼容层(强制)**:运行期只读写 `config.json`;`Settings.json` 只作为「ClassIsland 格式导入/导出兼容层」存在——导入时**只读**解析(`PascalCase` → 本实现字段映射),导出时生成一份 ClassIsland 可识别的 `Settings.json`(`PascalCase`)。**禁止双向双写**(不得同时写 `config.json` 与 `Settings.json`,否则两份文件必然会分叉)。导入映射必须保留历史拼写,尤其是 `IsIslandSeperated`(ClassIsland 源码就是少一个 `a`,`repos/ClassIsland/ClassIsland/Models/Settings.cs:1852`)——本实现内部字段可写作 `isIslandSeparated`,但导入/导出映射表必须双向接受原拼写。**【推断】**
#### 13.3.1 外观与主界面
| 字段 | 类型 | 默认值 | 合法区间/枚举 | ClassIsland 依据 |
|---|---|---|---|---|
| `theme` | number | `2` | `0` 亮 / `1` 暗 / `2` 跟随系统 | `Settings.cs:32` |
| `primaryColor` | string | `#00BFFF` | `#RRGGBBAA` | `Settings.cs:35`(DeepSkyBlue) |
| `secondaryColor` | string | `#7FFFD4` | 同上 | `Settings.cs:36`(Aquamarine) |
| `isCustomBackgroundColorEnabled` | boolean | `false` | — | `Settings.cs:33` |
| `backgroundColor` | string | `#000000FF` | 同上 | `Settings.cs:34`(Colors.Black) |
| `isCustomForegroundColorEnabled` | boolean | `false` | — | `Settings.cs:202` |
| `customForegroundColor` | string | `#1E90FF` | 同上 | `Settings.cs:203`(DodgerBlue) |
| `opacity` | number | `0.5` | `0.1`–`1` | `Settings.cs:64` |
| `scale` | number | `1.0` | `0.75`–`2.5` | `Settings.cs:86` |
| `radiusX` / `radiusY` | number | `8` / `8` | `0`–`20` | `Settings.cs:183-184` |
| `mainWindowFont` | string | 跟随系统默认字体 | 合法字体族名 | `Settings.cs:95` |
| `mainWindowFontWeight` | number | `500`(Medium) | `100`–`900` | `Settings.cs:101` |
| `mainWindowSecondaryFontSize` | number | `14` | `8`–`96` | `Settings.cs:197` |
| `mainWindowBodyFontSize` | number | `16` | 同上 | `Settings.cs:198` |
| `mainWindowEmphasizedFontSize` | number | `18` | 同上 | `Settings.cs:199` |
| `mainWindowLargeFontSize` | number | `20` | 同上 | `Settings.cs:200` |
| `mainWindowLineVerticalMargin` | number | `5` | `0`–`150` | `Settings.cs:219` |
| `isIslandSeperated` | boolean | `false` | — | `Settings.cs:1852`(拼写保留) |
| `reduceProgressAccuracy` | boolean | `true` | — | `Settings.cs:1853` |
| `animationLevel` | number | `1` | `0` 关闭 / `1` 标准 / `2` 全部 | `Settings.cs:1851` |
| `isScreenRecordingModeEnabled` | boolean | `false` | — | `Settings.cs:1861` |
| `isWindowCaptureBlockingEnabled` | boolean | `false` | — | `Settings.cs:1862` |
#### 13.3.2 窗口、停靠与隐藏
| 字段 | 类型 | 默认值 | 说明 | 依据 |
|---|---|---|---|---|
| `windowDockingLocation` | number | `1` | `0`–`5`:左上/中上/右上/左下/中下/右下 | `Settings.cs:48` |
| `windowDockingOffsetX/Y` | number | `0`/`0` | 停靠偏移(px) | `Settings.cs:49-50` |
| `windowDockingMonitorIndex` | number | `0` | 多显示器索引 | `Settings.cs:51` |
| `windowLayer` | number | `1` | 窗口层级(普通/置顶/桌面) | `Settings.cs:52` |
| `isMouseClickingEnabled` | boolean | `false` | 鼠标穿透(true = 点击穿透) | `Settings.cs:53` |
| `hideOnClass` | boolean | `false` | 上课自动隐藏 | `Settings.cs:45` |
| `hideOnMaxWindow` | boolean | `false` | 有最大化窗口时隐藏 | `Settings.cs:63` |
| `hideOnFullscreen` | boolean | `false` | 全屏时隐藏 | `Settings.cs:54` |
| `excludedFullscreenWindow` | string[] | `[]` | 全屏检测排除的进程名 | `Settings.cs:56-59` |
| `hideMode` | number | `0` | `0` 基本(上课/最大化/全屏);`1` 高级(规则集) | `Settings.cs:185` |
| `hideRules` | Ruleset | 空规则集 | 见 13.5.3 | `Settings.cs:186` |
| `useRawInput` | boolean | `false` | 原始输入(兼容某些输入法) | `Settings.cs:192` |
| `isMouseInFadingEnabled` | boolean | `true` | 鼠标进入时淡化 | `Settings.cs:193` |
| `isMouseInFadingReversed` | boolean | `false` | 反向淡化 | `Settings.cs:194` |
| `touchInFadingDurationMs` | number | `0` | 触摸淡化时长 | `Settings.cs:195` |
| `isCompatibleWindowTransparentEnabled` | boolean | `false` | 兼容透明(老显卡) | `Settings.cs:196` |
| `windowTopmostRecheckMode` | number | `0` | 置顶复查策略 | `Settings.cs:1860` |
| `showCurrentLessonOnlyOnClass` | boolean | `false` | — | `Settings.cs:209` |
| `isSwapMode` | boolean | `true` | 换课模式 | `Settings.cs:210` |
**坐标口径(强制)**:`windowDockingOffsetX/Y`、`scale`、`radiusX/Y`、所有字号与组件尺寸/间距字段一律以 **DIP(CSS px)** 存储与解释;窗口 bounds 也按 DIP 交给 Electron。**禁止**把 `devicePixelRatio` 乘进这些字段;只有截图/像素级视觉验收时才用 `devicePixelRatio` 把 DIP 换算成物理像素(与 04 部分同一口径)。
#### 13.3.3 课程与时间
| 字段 | 类型 | 默认值 | 依据 |
|---|---|---|---|
| `selectedProfile` | string | `"Default.json"` | `Settings.cs:66` |
| `singleWeekStartTime` | string(ISO) | 首次启动时写入当天 00:00 | `Settings.cs:60`(与其相关的周次计算见 ProfileService) |
| `multiWeekRotationOffset` | number[5] | `[-1,-1,0,0,0]` | `Settings.cs:60` |
| `multiWeekRotationMaxCycle` | number | `4` | `Settings.cs:61` |
| `classPrepareNotifySeconds` | number | `60` | `Settings.cs:43` |
| `isClassPrepareNotificationEnabled` | boolean | `true` | `Settings.cs:47` |
| `isClassChangingNotificationEnabled` | boolean | `true` | `Settings.cs:46` |
| `isClassOffNotificationEnabled` | boolean | `true` | `Settings.cs:55` |
| `showDate` | boolean | `true` | `Settings.cs:44` |
| `showExtraInfoOnTimePoint` | boolean | `true` | `Settings.cs:103` |
| `extraInfoType` | number | `0` | `Settings.cs:104`(0 持续时间/1 已过/2 剩余/3 已过百分比/4 剩余时间/5 倒计时) |
| `isCountdownEnabled` | boolean | `true` | `Settings.cs:105` |
| `countdownSeconds` | number | `60` | `Settings.cs:106` |
| `extraInfo4ShowSecondsSeconds` | number | `300` | `Settings.cs:107` |
| `defaultOnClassTimePointMinutes` | number | `40` | `Settings.cs:108` |
| `defaultBreakingTimePointMinutes` | number | `10` | `Settings.cs:109` |
| `isNonExactCountdownEnabled` | boolean | `false` | `Settings.cs:221` |
#### 13.3.4 提醒、语音与时间同步
| 字段 | 类型 | 默认值 | 依据 |
|---|---|---|---|
| `isNotificationEnabled` | boolean | `true` | `Settings.cs:91` |
| `notificationProvidersEnableStates` | map<string,bool> | `{}` | `Settings.cs:88` |
| `notificationProvidersPriority` | string[] | `[]` | `Settings.cs:89` |
| `notificationProvidersSettings` | map<string,any> | `{}` | `Settings.cs:90` |
| `notificationProvidersNotifySettings` | map<string,NotificationSettings> | `{}` | `Settings.cs:143` |
| `notificationChannelsNotifySettings` | map<string,NotificationSettings> | `{}` | `Settings.cs:1846` |
| `isNotificationEffectEnabled` | boolean | `true` | `Settings.cs:142` |
| `isNotificationTopmostEnabled` | boolean | `true` | `Settings.cs:149` |
| `notificationEffectRenderingScale` | number | `1.0` | `Settings.cs:150` |
| `isNotificationEffectRenderingScaleAutoSet` | boolean | `false` | `Settings.cs:151` |
| `isNotificationSoundEnabled` | boolean | `true` | `Settings.cs:144` |
| `notificationSoundPath` | string | `""` | `Settings.cs:145` |
| `notificationSoundVolume` | number | `1.0` | `Settings.cs:182` |
| `allowNotificationSpeech/Effect/Sound/Topmost` | boolean | `false`/`true`/`false`/`true` | `Settings.cs:153-156` |
| `notificationUseStandaloneEffectUiThread` | boolean | `true` | `Settings.cs:1837` |
| `isSpeechEnabled` | boolean | `true` | `Settings.cs:135` |
| `speechVolume` | number | `1.0` | `Settings.cs:136` |
| `speechSource` | number | `0` | `Settings.cs:137` |
| `selectedSpeechProvider` | string | `"classisland.speech.edgeTts"` | `Settings.cs:1847` |
| `edgeTtsVoiceName` | string | `"zh-CN-XiaoxiaoNeural"` | `Settings.cs:138` |
| `isSystemSpeechSystemExist` | boolean | `false`(运行时探测) | `Settings.cs:133` |
| `exactTimeServer` | string | `"ntp.aliyun.com"` | `Settings.cs:139` |
| `isExactTimeEnabled` | boolean | `true` | `Settings.cs:140` |
| `timeOffsetSeconds` | number | `0` | `Settings.cs:141` |
| `isTimeAutoAdjustEnabled` | boolean | `false` | `Settings.cs:146` |
| `timeAutoAdjustSeconds` | number | `0` | `Settings.cs:147` |
| `lastTimeAdjustDateTime` | string(ISO) | 当前时间 | `Settings.cs:148` |
#### 13.3.5 组件、自动化、主题
| 字段 | 类型 | 默认值 | 依据 |
|---|---|---|---|
| `currentComponentConfig` | string | `"Default"` | `Settings.cs:159` |
| `isAutomationEnabled` | boolean | `false` | `Settings.cs:160` |
| `currentAutomationConfig` | string | `"Default"` | `Settings.cs:162` |
| `isAutomationWarningVisible` | boolean | `true` | `Settings.cs:161` |
| `selectedPlatte` | string | `#1E90FFFF` | `Settings.cs:80`(DodgerBlue) |
| `selectedPlatteIndex` | number | `0` | `Settings.cs:81` |
| `wallpaperColorPlatte` | string[] | 5 个 DodgerBlue | `Settings.cs:82` |
| `colorSource` | number | `2` | `Settings.cs:83`(0 无/1 壁纸/2 手动) |
| `wallpaperClassName` | string | `""` | `Settings.cs:84` |
| `targetLightValue` | number | `0.6` | `Settings.cs:85` |
| `isWallpaperAutoUpdateEnabled` | boolean | `false` | `Settings.cs:92` |
| `wallpaperAutoUpdateIntervalSeconds` | number | `60` | `Settings.cs:93` |
| `weatherIconId` | string | `"classisland.weatherIcons.lucide"` | `Settings.cs:1849` |
| `isRollingComponentWarningVisible` | boolean | `true` | `Settings.cs:1850` |
| `isThemeWarningVisible` | boolean | `true` | `Settings.cs:1848` |
| `isThemeSeparateInfoVisible` | boolean | `true` | `Settings.cs:1875` |
#### 13.3.6 天气
| 字段 | 类型 | 默认值 | 依据 |
|---|---|---|---|
| `cityId` | string | `"weathercn:101010100"` | `Settings.cs:99` |
| `cityName` | string | `"北京 (北京, 中国)"` | `Settings.cs:100` |
| `lastWeatherInfo` | object | `{}` | `Settings.cs:98` |
| `weatherLatitude` / `weatherLongitude` | number | `0`/`0` | `Settings.cs:1838-1839` |
| `weatherLocationSource` | number | `0` | `Settings.cs:1840` |
| `autoRefreshWeatherLocation` | boolean | `false` | `Settings.cs:1841` |
| `noTLSWeatherRequests` | boolean | `false` | `Settings.cs:1842` |
| `excludedWeatherAlerts` | string[] | `[]` | `Settings.cs:158` |
| `selectedMiniInfoProvider` | string? | 内置天气提供者 Guid | `Settings.cs:97` |
| `miniInfoProviderSettings` | map<string,any> | `{}` | `Settings.cs:96` |
#### 13.3.7 更新、插件、备份、诊断
| 字段 | 类型 | 默认值 | 依据 |
|---|---|---|---|
| `updateMode` | number | `3` | `Settings.cs:77` |
| `autoInstallUpdateNextStartup` | boolean | `true` | `Settings.cs:78` |
| `selectedUpdateChannelV2` | string | `"stable"` | `Settings.cs:217` |
| `selectedUpdateMirrorV2` | string | `"main"` | `Settings.cs:216` |
| `selectedUpdateChannelV3` | string(Guid) | `Guid.Empty` | `Settings.cs:1854` |
| `lastCheckUpdateTime` | string(ISO) | `DateTime.MinValue` | `Settings.cs:75` |
| `lastUpdateStatus` | number | `0`(UpToDate) | `Settings.cs:76` |
| `updateArtifactHash` | string | `""` | `Settings.cs:157` |
| `ignoredUpdateVersion` | string | `""` | `config.ts:76`(本应用字段,兼容旧行为) |
| `userPluginIndexes` | string[] | `[]` | `Settings.cs:167` |
| `additionalPluginIndexes` | map<string,string> | `{}` | `Settings.cs:168` |
| `pluginIndexes` | object[] | `[]` | `Settings.cs:169` |
| `pluginIndexSelectedMirrors` | map<string,string> | `{}` | `Settings.cs:166` |
| `officialSelectedMirror` | string | `"github"` | `Settings.cs:170` |
| `officialIndexMirrors` | map<string,string> | `{github: https://github.com, ghproxy: …, moeyy: …}` | `Settings.cs:171-176` |
| `ignoreSslForPluginMirrors` | boolean | `false` | `Settings.cs:177` |
| `isPluginMarketWarningVisible` | boolean | `true` | `Settings.cs:204` |
| `isPluginsAutoUpdateEnabled` | boolean | `true` | `Settings.cs:1858` |
| `isPluginsUpdateNotificationEnabled` | boolean | `true` | `Settings.cs:1859` |
| `autoDisableCorruptPlugins` | boolean | `true` | `Settings.cs:1844` |
| `corruptPluginsDisabledLastSession` | boolean | `false` | `Settings.cs:1845` |
| `lastRefreshPluginSourceTime` | string(ISO) | `MinValue` | `Settings.cs:180` |
| `isAutoBackupEnabled` | boolean | `true` | `Settings.cs:187` |
| `lastAutoBackupTime` | string(ISO) | 当前时间 | `Settings.cs:188` |
| `autoBackupLimit` | number | `16` | `Settings.cs:190` |
| `autoBackupIntervalDays` | number | `7` | `Settings.cs:191` |
| `backupFilesSize` | string | `"计算中..."` | `Settings.cs:189` |
| `firstLaunchTime` | string(ISO) | 当前时间 | `Settings.cs:127` |
| `diagnosticStartupCount` | number | `0` | `Settings.cs:128` |
| `diagnosticCrashCount` | number | `0` | `Settings.cs:129` |
| `diagnosticLastCrashTime` | string(ISO) | `MinValue` | `Settings.cs:130` |
| `diagnosticMemoryKillCount` | number | `0` | `Settings.cs:131` |
| `diagnosticLastMemoryKillTime` | string(ISO) | 当前时间 | `Settings.cs:132` |
| `developerMode` | boolean | `false` | `config.ts:79`(本应用字段,决定是否显示开发者页) |
| `careMode` | boolean | `false` | `config.ts:80`(关怀模式) |
| `isDebugEnabled` | boolean | `false` | `Settings.cs:65` |
| `isDebugOptionsEnabled` | boolean | `false` | `Settings.cs:79` |
| `isDebugConsoleEnabled` | boolean | `false` | `Settings.cs:118` |
| `debugAnimationScale` | number | `1.0` | `Settings.cs:110` |
| `debugTimeSpeed` | number | `1.0` | `Settings.cs:111` |
| `debugTimeOffsetSeconds` | number | `0` | `Settings.cs:112` |
| `isSplashEnabled` | boolean | `false` | `Settings.cs:115` |
| `splashCustomText` | string | `""` | `Settings.cs:116` |
| `showDetailedStatusOnSplash` | boolean | `false` | `Settings.cs:222` |
| `isWelcomeWindowShowed` | boolean | `false` | `Settings.cs:68` |
| `onboardingVersion` | number | `0` | `config.ts:41` |
| `isReportingEnabled` | boolean | `true` | `Settings.cs:69` |
| `isFallbackModeEnabled` | boolean | `true` | `Settings.cs:94` |
| `isCriticalSafeMode` | boolean | `false` | `Settings.cs:207` |
| `criticalSafeModeMethod` | number | `0` | `Settings.cs:1836` |
| `trustedProfileIds` | string(Guid)[] | `[]` | `Settings.cs:220` |
| `taskBarIconClickBehavior` | number | Windows `0`,其它 `4` | `Settings.cs:102` |
| `settingsPagesCachePolicy` | number | `0` | `Settings.cs:213` |
| `isRefreshingToastEnabled` | boolean | `true` | `Settings.cs:1865` |
| `refreshingToastThresholdDays` | number | `20` | `Settings.cs:1866` |
| `maxRefreshingToastCounts` | number | `5` | `Settings.cs:1873` |
**字段级兼容规则(必须实现)**:
1. 新增字段一律有默认值,缺失即用默认值(`SettingsService.LoadSettingsAsync` 缺失文件时直接返回 `new Settings()`,见 `SettingsService.cs:73-77`)。
2. 字段类型不匹配时用默认值覆盖并记 warn,不得整体抛错(对齐 `config.ts:198-233` 每个字段逐个 `typeof` 判断)。
3. 枚举越界值回落到默认值(如 `theme` 非 `0/1/2` → `2`)。
4. 语义变更或废弃的字段必须保留读取(忽略)一个版本,禁止删除后导致老配置迁移失败。**不得**删除 `isIslandSeperated` 这类历史拼写字段(`Settings.cs:1852`)。
5. `deviceKey` 类"机器身份"字段一旦生成必须永不变;如由外部传入不同值必须拒绝并记 warn(对齐 `config.ts:501-505`)。
### 13.4 档案(Profile)schema — 与 ClassIsland 完全同构
档案文件:`data/profiles/<SelectedProfile>`,默认 `Default.json`。**字段名必须保留 PascalCase**,以便直接导入/导出 ClassIsland 档案。
#### 13.4.1 Profile 根对象
| 字段 | 类型 | 默认值 | 说明 | 依据 |
|---|---|---|---|---|
| `Name` | string | `""` | 档案名称 | `Profile.cs:227-236` |
| `Id` | string(Guid) | 新建 Guid | 档案 ID(用于"信任档案"判断) | `Profile.cs:452-463` |
| `TimeLayouts` | map<Guid,TimeLayout> | `{}` | 时间表字典,键为 Guid | `Profile.cs:241-250` |
| `ClassPlans` | map<Guid,ClassPlan> | `{}` | 课表字典 | `Profile.cs:255-270` |
| `Subjects` | map<Guid,Subject> | 首次启动写入默认科目表 | 科目字典 | `Profile.cs:284-293` |
| `EditingSubjects` | Subject[] | `[JsonIgnore]` 不落盘 | 编辑期临时集合 | `Profile.cs:298-308` |
| `IsOverlayClassPlanEnabled` | boolean | `false` | 是否启用临时层课表 | `Profile.cs:313-322` |
| `OverlayClassPlanId` | string(Guid)? | `null` | 当前临时层课表 ID | `Profile.cs:327-337` |
| `TempClassPlanId` | string(Guid)? | `null` | 临时课表 ID | `Profile.cs:342-351` |
| `TempClassPlanSetupTime` | string(ISO) | 当前时间 | 临时课表设置时间 | `Profile.cs:356-365` |
| `ClassPlanGroups` | map<Guid,ClassPlanGroup> | 必含默认群 + 全局群 | 课表群 | `Profile.cs:370-379` |
| `SelectedClassPlanGroupId` | string(Guid) | 默认群 Guid `ACAF4EF0-E261-4262-B941-34EA93CB4369` | 当前选中群 | `Profile.cs:384-393`、`ClassPlanGroup.cs:47-52` |
| `TempClassPlanGroupId` | string(Guid)? | `null` | 临时课表群 | `Profile.cs:398-407` |
| `TempClassPlanGroupExpireTime` | string(ISO) | 当前时间 | 临时群失效时间 | `Profile.cs:412-421` |
| `IsTempClassPlanGroupEnabled` | boolean | `false` | — | `Profile.cs:426-435` |
| `TempClassPlanGroupType` | number | `Inherit` | `Inherit`/`Override` 等(枚举) | `Profile.cs:440-449` |
| `OrderedSchedules` | map<DateTime,OrderedSchedule> | `{}` | 预定课表(日期→课表 ID) | `Profile.cs:468-477` |
**初始化规则**:新档案必须自动加入两个群:默认群(`ACAF4EF0-E261-4262-B941-34EA93CB4369`,名称"默认")与全局群(`Guid.Empty`,名称"全局课表群",`IsGlobal=true`)。依据:`Profile.cs:44-59`。
#### 13.4.2 TimeLayout(时间表)
| 字段 | 类型 | 默认值 | 依据 |
|---|---|---|---|
| `Name` | string | `"新时间表"` | `TimeLayout.cs:17` |
| `Layouts` | TimeLayoutItem[] | `[]` | `TimeLayout.cs:254-267` |
| `IsOverlay` | boolean | `false` | `TimeLayout.cs:26-35` |
| `OverlaySourceId` | string(Guid)? | `null` | `TimeLayout.cs:40-49` |
| `IsActivated` / `IsActivatedManually` | boolean | `[JsonIgnore]` 不落盘 | `TimeLayout.cs:272-297` |
#### 13.4.3 TimeLayoutItem(时间点)
| 字段 | 类型 | 默认值 | 说明 | 依据 |
|---|---|---|---|---|
| `StartTime` | TimeSpan 字符串 | `"00:00:00"` | 开始时间(当天内的偏移) | `TimeLayoutItem.cs:62-77` |
| `EndTime` | TimeSpan 字符串 | `"00:00:00"` | 结束时间;`TimeType` 为 2/3 时强制等于 `StartTime` | `TimeLayoutItem.cs:82-93`、`:70-73` |
| `TimeType` | number | `0` | `0` 上课 / `1` 课间 / `2` 分割线 / `3` 行动 | `TimeLayoutItem.cs:128-138` |
| `IsHideDefault` | boolean | `false` | 默认隐藏(仅上课类型有效) | `TimeLayoutItem.cs:143-153` |
| `DefaultClassId` | string(Guid) | `Guid.Empty`(空字符串也解析为 Empty) | 默认科目 | `TimeLayoutItem.cs:158-169`、`GuidEmptyFallbackConverter` |
| `BreakName` | string | `""` | 自定义课间名;展示用 `BreakNameText`(空则"课间休息") | `TimeLayoutItem.cs:180-191,174-175` |
| `ActionSet` | ActionSet? | `null` | 仅 `TimeType=3` 时有意义 | `TimeLayoutItem.cs:196-205` |
| `StartSecond` / `EndSecond` | string | `[Obsolete]` | 旧版字段,读取时忽略、写出时不得生成 | `TimeLayoutItem.cs:26-57` |
| `attachedObjects` | map<Guid, object> | `{}`(导入时接受 `AttachedObjects` 原拼写) | **一等公民的附加设置字典**(课级提醒/天气/课程显示等),见 13.4.8 | `repos/ClassIsland/ClassIsland.Shared/AttachableSettingsObject.cs:10-15` |
**校验规则**:`StartTime > EndTime` 必须拒绝(对齐 `TimeLayoutItem.EnsureTime`,`TimeLayoutItem.cs:95-101`);同一时间表内时间点必须按 `StartTime` 升序,重叠允许但必须保留排序;分割线/行动点不占课程位(见 13.4.4 的"有效时间点")。
#### 13.4.4 ClassPlan(课表)
| 字段 | 类型 | 默认值 | 说明 | 依据 |
|---|---|---|---|---|
| `Name` | string | `"新课表"` | — | `ClassPlan.cs:440-449` |
| `TimeLayoutId` | string(Guid) | `Guid.Empty` | 引用时间表 | `ClassPlan.cs:396-407` |
| `TimeRule` | TimeRule | `{WeekDay:0, WeekCountDiv:0, WeekCountDivTotal:2}` | 周次规则 | `ClassPlan.cs:412-421`、`TimeRule.cs:13-15` |
| `Classes` | ClassInfo[] | `[]` | 课程列表,长度 = 时间表中上课类型点数 | `ClassPlan.cs:426-435,451-491` |
| `IsActivated` | boolean | `[JsonIgnore]` 不落盘 | 当前激活 | `ClassPlan.cs:527-537` |
| `IsOverlay` | boolean | `false` | 临时层课表 | `ClassPlan.cs:542-551` |
| `OverlaySourceId` | string(Guid)? | `null` | 临时层源课表 | `ClassPlan.cs:556-565` |
| `OverlaySetupTime` | string(ISO) | 当前时间 | — | `ClassPlan.cs:570-579` |
| `IsEnabled` | boolean | `true` | 是否默认启用 | `ClassPlan.cs:584-593` |
| `AssociatedGroup` | string(Guid) | 默认群 Guid | 所属课表群 | `ClassPlan.cs:598-607` |
**有效时间点规则**(必须实现 `ValidTimeLayoutItems`):只取 `TimeType ∈ {0,1,2}`;从前后两个方向扫描 `ClassInfo.IsEnabled`,把被禁用课程"夹住"的时间点全部隐藏;`TimeType=3` 的行动点永不显示。依据:`ClassPlan.cs:54-98`。
#### 13.4.5 ClassInfo(一节课)
| 字段 | 类型 | 默认值 | 依据 |
|---|---|---|---|
| `SubjectId` | string(Guid) | `Guid.Empty`(空串也解析为 Empty) | `ClassInfo.cs:70-80` |
| `IsChangedClass` | boolean | `false` | `ClassInfo.cs:85-94` |
| `IsEnabled` | boolean | `true` | `ClassInfo.cs:99-108` |
| `Index` / `CurrentTimeLayout` / `IsEmpty` | — | `[JsonIgnore]` 不落盘 | `ClassInfo.cs:21-64,110-121` |
#### 13.4.6 Subject(科目)
| 字段 | 类型 | 默认值 | 依据 |
|---|---|---|---|
| `Name` | string | `""` | `Subject.cs:19-30` |
| `Initial` | string | `""`(`Name` 变化且 `Initial` 为空时自动取首字) | `Subject.cs:35-44` |
| `TeacherName` | string | `""` | `Subject.cs:49-58` |
| `IsOutDoor` | boolean | `false` | `Subject.cs:63-72` |
内置常量必须实现:`Fallback = {Name:"???", Initial:"?"}`、`Empty = {Name:"", Initial:""}`、`Breaking = {Name:"课间休息", Initial:"休"}`。依据:`Subject.cs:79-101`。教师姓氏提取必须支持复姓表(万俟/司马/上官/欧阳……)与英文姓名取最后一个单词。依据:`Subject.cs:14,107-140`。
#### 13.4.7 课表群与预定
- `ClassPlanGroup { Name: "新课表群", IsGlobal: false }`;默认群 Guid `ACAF4EF0-E261-4262-B941-34EA93CB4369`;全局群 Guid `Guid.Empty`;解散群时课表移到默认群,删除群时群内课表一并删除。依据:`ClassPlanGroup.cs`、`Profile.cs:104-137`。
- `OrderedSchedule { ClassPlanId: Guid }`;`OrderedSchedules` 的键是**日期**(本地日期,序列化为 `"2026-11-12T00:00:00"`)。
- 临时层规则:每天启动/跨天时必须 `CleanExpiredTempClassPlan()`:删除今天以前的 `OrderedSchedules`;删除未被任何预定引用的临时课表;删除未被引用的临时时间表。依据:`ProfileService.cs:295-328`。
#### 13.4.8 附加设置(`attachedObjects`)——一等公民
**定位**:附加设置**不是** 1.7 遗留字段,而是 ClassIsland 2.x 仍在使用的核心机制:可附着对象(`AttachableSettingsObject`)自带 `Dictionary<Guid, object?> AttachedObjects`,键是"附加设置提供者的 Guid",值是设置对象;界面通过 `AttachedSettingsControlPresenter` 读写它(`repos/ClassIsland/ClassIsland.Shared/AttachableSettingsObject.cs:10-15`、`repos/ClassIsland/ClassIsland.Core/Controls/AttachedSettingsControlPresenter.axaml.cs:241-253`)。本实现必须把它升级为**一等公民字段**,供 06/07 部分的"课级提醒/课级附加行动/天气提醒"直接引用。
**承载对象**(均必须有 `attachedObjects` 字段):
| 对象 | 目标枚举 | 说明 |
|---|---|---|
| `Profile`(档案级) | — | 本实现扩展的档案级附加设置;ClassIsland 2.x 的可附着基类未覆盖档案,档案级为本实现新增【推断】 |
| `ClassPlan`(课表级) | `ClassPlan` | 整张课表的附加设置 |
| `ClassInfo`(课级) | `Lesson` | 单节课的附加设置(最常用,例如这节课用自定义提醒文案) |
| `TimeLayout`(时间表级) | `TimeLayout` | 整张时间表的附加设置 |
| `TimeLayoutItem`(时间点级) | `TimePoint` | 单个时间点的附加设置 |
| `Subject`(科目级) | `Subject` | 科目的附加设置 |
字段定义:`attachedObjects: map<GuidString, object>`,默认 `{}`;序列化字段名 **`attachedObjects`(camelCase)**;导入 ClassIsland 档案时**接受原拼写 `AttachedObjects`**(PascalCase),导出/写出统一 camelCase。值必须是普通 JSON 对象(结构化克隆可序列化),不得存函数或类实例。依据:`AttachableSettingsObject.cs:12-15`(字典键为 Guid);`AttachedSettingsControlPresenter.axaml.cs:245-253`(目标对象 → `AttachedSettingsTargets` 的映射)。
**已知附加设置 Guid 与设置对象类型(必须内置映射)**:
| Guid | 名称 | 设置对象类型 | 允许附着目标 | 依据 |
|---|---|---|---|---|
| `58e5b69a-764a-472b-bcf7-003b6a8c7fdf` | 课程显示设置 | `LessonControlAttachedSettings` | ClassPlan / TimeLayout / Lesson / Subject / TimePoint | `LessonControlAttachedSettingsControl.axaml.cs:10-14`、`AttachedSettingsTargets.cs` |
| `08f0d9c3-c770-4093-a3d0-02f3d90c24bc` | 上课提醒设置 | `ClassNotificationAttachedSettings` | ClassPlan / TimeLayout / Lesson / Subject / TimePoint | `ClassNotificationAttachedSettingsControl.axaml.cs:11-15` |
| `8fbc3a26-6d20-44dd-b895-b9411e3ddc51` | 放学提醒设置 | `AfterSchoolNotificationAttachedSettings` | ClassPlan / TimeLayout | `AfterSchoolNotificationAttachedSettingsControl.axaml.cs:10-12` |
| `7625de96-38aa-4b71-b478-3f156dd9458d` | 天气提醒设置 | `WeatherNotificationAttachedSettings` | TimePoint | `WeatherNotificationAttachedSettingsControl.axaml.cs:10-12` |
**设置对象字段(默认值必须逐项一致)**:
- `LessonControlAttachedSettings`:`IsAttachSettingsEnabled=false`、`ShowExtraInfoOnTimePoint=true`、`ExtraInfoType=0`、`IsCountdownEnabled=true`、`CountdownSeconds=60`、`ExtraInfo4ShowSecondsSeconds=0`、`ScheduleSpacing=1`、`ShowCurrentLessonOnlyOnClass=false`、`IsNonExactCountdownEnabled=false`。依据:`repos/ClassIsland/ClassIsland.Core/Models/AttachedSettings/LessonControlAttachedSettings.cs:7-17`。
- `ClassNotificationAttachedSettings`:`IsClassOnNotificationEnabled=true`、`IsClassOnPreparingNotificationEnabled=true`、`IsClassOffNotificationEnabled=true`、`ClassPreparingDeltaTime=60`、`ClassOnPreparingText="准备上课,请回到座位并保持安静,做好上课准备。"`、`OutdoorClassOnPreparingText="下节课程为户外课程,请合理规划时间,做好上课准备。"`、`ClassOnPreparingMaskText="即将上课"`、`OutdoorClassOnPreparingMaskText="即将上课"`、`ClassOnMaskText="上课"`、`ClassOffMaskText="课间休息"`、`ClassOffOverlayText=""`、`IsAttachSettingsEnabled=false`。依据:`repos/ClassIsland/ClassIsland/Models/AttachedSettings/ClassNotificationAttachedSettings.cs:7-24`。
- `AfterSchoolNotificationAttachedSettings`:`IsEnabled=true`、`NotificationMsg="今天的课程已结束,请同学们有序离开。"`、`IsAttachSettingsEnabled=false`。依据:`repos/ClassIsland/ClassIsland/Models/AttachedSettings/AfterSchoolNotificationAttachedSettings.cs:7-14`。
- `WeatherNotificationAttachedSettings`:`AlertShowMode=Default`、`ForecastShowMode=Default`、`IsAttachSettingsEnabled=true`。依据:`repos/ClassIsland/ClassIsland/Models/AttachedSettings/WeatherNotificationAttachedSettings.cs:7-17`。
- 所有设置对象都带 `IsAttachSettingsEnabled`:**只有它为 true 时附加设置才生效**;为 false 时按空对象处理并允许回落到全局设置。
**读写与兼容规则(强制)**:
1. 读取时必须"按目标对象类型过滤 Guid":若某 Guid 允许的目标不包含当前对象类型,保留数据但**不得生效**(不能因为档案来自别处就崩溃)。
2. 未知 Guid 必须原样保留(不删除、不报错),以便插件卸载后重装仍能恢复设置;UI 显示为"未知附加设置(可删除)"。
3. 值里出现 `null` 时按"未设置"处理;导出的 JSON 必须省略值为 `null` 的键(对齐 `object?` 语义)。
4. 导入 ClassIsland 档案的 `AttachedObjects` 时:键 Guid 统一小写、值按上表类型反序列化;未知 Guid 的 `JsonElement` 原样保留;**不得**丢弃。
5. 导出为 ClassIsland 档案时字段名写 `AttachedObjects`(PascalCase),以保持双向可导入。
6. 附加设置必须参与 `ProfileService.SaveProfile` 的保存触发(对象属性变化 → 档案保存),并在 UI 中提供"清除该对象全部附加设置"的入口。
### 13.5 组件布局(ComponentProfile)schema
文件:`data/component-layouts/<CurrentComponentConfig>.json`,默认 `Default.json`。依据:`ComponentsService.cs:28-58,122-166`。
#### 13.5.1 顶层
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| `Lines` | MainWindowLineSettings[] | 1 行 + 2 个默认组件 | 主界面行集合 |
默认组件配置的固定内容:一行,含两个组件,`Id` 分别为 `df3f8295-21f6-482e-bada-fa0e5f14bb66`(日期)与 `1db2017d-e374-4bc6-9d57-0b4adf03a6b8`(时间)。依据:`ComponentsService.cs:33-52`。**这两个 GUID 必须保留**(导入 ClassIsland 组件布局时按 GUID 匹配)。
#### 13.5.2 MainWindowLineSettings(主界面行)
| 字段 | 类型 | 默认值 | 依据 |
|---|---|---|---|
| `Children` | ComponentSettings[] | `[]` | `MainWindowLineSettings.cs` |
| `IsMainLine` | boolean | `false` | 同上 |
| `IsNotificationEnabled` | boolean | `true` | 同上 |
| `IsResourceOverridingEnabled` | boolean | `false` | 同上 |
| `MainWindowSecondaryFontSize` / `Body` / `Emphasized` / `Large` | number | `14/16/18/20` | 同上 |
| `IsCustomForegroundColorEnabled` / `ForegroundColor` | boolean / string | `false` / `#1E90FFFF` | 同上 |
| `BackgroundOpacity` / `IsCustomBackgroundOpacityEnabled` | number / boolean | `0.5` / `false` | 同上 |
| `BackgroundColor` / `IsCustomBackgroundColorEnabled` | string / boolean | `#000000FF` / `false` | 同上 |
| `CustomCornerRadius` / `IsCustomCornerRadiusEnabled` | number / boolean | `0` / `false` | 同上 |
| `Opacity` | number | `1.0` | 同上 |
| `IsVisible` | boolean | `true` | 同上 |
| `HideOnRule` | boolean | `false` | 同上 |
| `HidingRules` | Ruleset | 空规则集 | 同上 |
| `IslandSeparationMode` | number | `0` | 同上(`0` 默认 / `1` 禁用 / `2` 启用) |
#### 13.5.3 ComponentSettings(单个组件)
| 字段 | 类型 | 默认值 | 依据 |
|---|---|---|---|
| `Id` | string | 组件 GUID(小写) | `ComponentSettings.cs:56-66` |
| `NameCache` | string | `""`(组件未加载时显示) | `:71-80` |
| `Settings` | object? | 组件专属设置对象 | `:85-95` |
| `HideOnRule` | boolean | `false` | `:100-109` |
| `HidingRules` | Ruleset | 空规则集 | `:114-123` |
| `IsResourceOverridingEnabled` | boolean | `false` | `:149-158` |
| `MainWindowSecondaryFontSize` / `Body` / `Emphasized` / `Large` | number | `14/16/18/20` | `:161-180` 起 |
| `ForegroundColor` / `IsCustomForegroundColorEnabled` | string / boolean | `#1E90FFFF` / `false` | `:28-29` |
| `IsMinWidthEnabled` / `MinWidth` | boolean / number | `false` / `100` | `:30-31` |
| `IsMaxWidthEnabled` / `MaxWidth` | boolean / number | `false` / `300` | `:32-33` |
| `IsFixedWidthEnabled` / `FixedWidth` | boolean / number | `false` / `200` | `:34-35` |
| `HorizontalAlignment` | enum | `Stretch` | `:36`(`Left/Center/Right/Stretch`) |
| `RelativeLineNumber` | number | `0` | `:37` |
| `IsVisible` | boolean | `true` | `:38` |
| `BackgroundOpacity` / `IsCustomBackgroundOpacityEnabled` | number / boolean | `0.5` / `false` | `:40-41` |
| `BackgroundColor` / `IsCustomBackgroundColorEnabled` | string / boolean | `#000000FF` / `false` | `:42-43` |
| `CustomCornerRadius` / `IsCustomCornerRadiusEnabled` | number / boolean | `8` / `false` | `:44-45` |
| `Opacity` | number | `1.0` | `:46` |
| `IsCustomMarginEnabled` + `MarginLeft/Top/Right/Bottom` | boolean + number×4 | `false` + `0` | `:47-51` |
组件专属设置对象必须在 `data/component-layouts/` 同一文件内序列化,其字段名按组件设置模型:`ClockComponentSettings`、`CountDownComponentSettings`、`LessonControlSettings`、`RollingComponentSettings`、`SlideComponentSettings`、`StackComponentSettings`、`GroupComponentSettings`、`TextComponentSettings`、`WeatherComponentSettings`。依据:`repos/ClassIsland/ClassIsland/Models/ComponentSettings/*.cs`。**组件设置的字段名与默认值必须逐项对齐这 9 个模型**(具体见第 5 部分组件规格)。
**组件迁移规则**:加载组件布局时,若组件 `Id` 命中 `ComponentRegistryService.MigrationPairs`,必须把 `Id` 改成新 GUID、`IsMigrated=true`、`MigrationSource=旧 GUID`(两者均 `[JsonIgnore]` 不落盘),迁移后立即保存。依据:`ComponentsService.cs:140-161`、`ComponentSettings.cs:125-128`。
### 13.6 自动化配置 schema
文件:`data/automations/<CurrentAutomationConfig>.json`,默认 `Default.json`;顶层类型是 **Workflow 数组**。依据:`AutomationService.cs:27-32,173-208,441`。
| 层级 | 字段 | 类型 | 默认值 | 依据 |
|---|---|---|---|---|
| Workflow | `Triggers` | TriggerSettings[] | `[]` | `Workflow.cs:12-14` |
| Workflow | `IsConditionEnabled` | boolean | `false` | `Workflow.cs:17-18` |
| Workflow | `Ruleset` | Ruleset | 空规则集 | `Workflow.cs:21-22` |
| Workflow | `ActionSet` | ActionSet | 新行动组 | `Workflow.cs:25-26` |
| TriggerSettings | `Id` | string | `""` | `TriggerSettings.cs:14-30` |
| TriggerSettings | `Settings` | object? | `null` | `TriggerSettings.cs:33-45` |
| ActionSet | `Name` | string | `"新行动组"` | `ActionSet.cs:74-77` |
| ActionSet | `Actions` | ActionItem[] | `[]` | `ActionSet.cs:79-83`(序列化名必须为 `Actions`) |
| ActionSet | `IsEnabled` | boolean | `true` | `ActionSet.cs:88` |
| ActionSet | `IsRevertEnabled` | boolean | `false` | `ActionSet.cs:93` |
| ActionSet | `Status` | number | `Normal` | `ActionSet.cs:104-105` |
| ActionSet | `Guid` | string(Guid) | 新建 Guid | `ActionSet.cs:115` |
| ActionItem | `Id` | string | `""` | `ActionItem.cs:43` |
| ActionItem | `Settings` | object? | `null`(null 时不写该字段) | `ActionItem.cs:50-51` |
**规则集(Ruleset)嵌套结构**(必须精确实现):
```json
{
"Mode": 0,
"IsReversed": false,
"Groups": [
{
"Rules": [{ "IsReversed": false, "Id": "classisland.lessons.currentTimeStateChanged", "Settings": {} }],
"Mode": 1,
"IsReversed": false,
"IsEnabled": true
}
]
}
```
- `Ruleset.Mode` 默认 `Or`(`Ruleset.cs:14`)、`RuleGroup.Mode` 默认 `And`(`RuleGroup.cs:14`);两者均支持 `IsReversed`;`RuleGroup.IsEnabled` 默认 `true`;`Ruleset.Groups` 默认含一个"一个空规则"的组(`Ruleset.cs:16`)。`State` 是运行期字段(`[JsonIgnore]`),不得落盘。
- 触发器 ID 必须使用以下注册表 ID(导入 ClassIsland 自动化时按此匹配):
| 触发器 | ID | 依据 |
|---|---|---|
| 上课时 | `classisland.lessons.onClass` | `OnClassTrigger.cs:8` |
| 课间休息时 | `classisland.lessons.onBreakingTime` | `OnBreakingTimeTrigger.cs:8` |
| 放学时 | `classisland.lessons.onAfterSchool` | `OnAfterSchoolTrigger.cs:8` |
| 当前时间状态变化 | `classisland.lessons.currentTimeStateChanged` | `CurrentTimeStateChangedTrigger.cs:8` |
| 特定时间点前 | `classisland.lessons.preTimePoint` | `PreTimePointTrigger.cs:13` |
| 应用启动时 | `classisland.lifetime.startup` | `AppStartupTrigger.cs:10` |
| 应用退出时 | `classisland.lifetime.stopping` | `AppStoppingTrigger.cs:8` |
| cron | `classisland.cron` | `CronTrigger.cs:15` |
| 收到信号时 | `classisland.signal` | `SignalTrigger.cs:8` |
| 调用 Uri 时 | `classisland.uri` | `UriTrigger.cs:8` |
| 从托盘菜单运行 | `classisland.trayMenu` | `TrayMenuTrigger.cs:11` |
| 规则集更新时 | `classisland.ruleSet.rulesetChanged` | `RulesetChangedTrigger.cs:8` |
行动 ID:`classisland.showNotification`(显示提醒)、`classisland.notification.weather`(天气提醒)、`classisland.os.run`(运行)、`classisland.action.sleep`(等待)、`classisland.app.quit`(退出)、`classisland.app.restart`(重启)、`classisland.settings`(应用设置)。依据:`repos/ClassIsland/ClassIsland/Services/Automation/Actions/*.cs` 的 `ActionInfo` 特性。
### 13.7 提醒数据
1. **运行期队列不落盘**(重启丢失可接受);**通知历史必须落盘**到 `data/notification-history.json`,结构为:
```json
{
"schemaVersion": 1,
"items": [
{
"id": "uuid",
"providerId": "classisland.notification.class",
"title": "上课提醒",
"content": "语文课 08:00-08:45",
"priority": 3,
"channelId": "classisland.channel.desktop",
"createdAt": "2026-11-12T07:59:00",
"read": false,
"speech": "准备上课",
"overlayDurationMs": 5000,
"maskDurationMs": 5000
}
]
}
```
2. 历史保留条数由设置项 `notificationHistoryLimit` 控制:默认 `500`,合法区间 `100–5000`;超限按最旧优先滚动删除(不得一次性清空)。"清空通知"必须二次确认并写审计日志。落盘必须**批量节流**:累积 ≥ 5 条或距上次写入 ≥ 5 秒才落盘一次,退出时强制 flush;不得每来一条通知就写一次盘。**【推断】**(数量上限为工程决策,ClassIsland 不落盘历史。)
3. 提醒设置对象 `NotificationSettings` 的字段与默认值必须为:`IsNotificationEnabled=true`、`IsSpeechEnabled=false`、`IsNotificationEffectEnabled=true`、`IsNotificationSoundEnabled=false`、`NotificationSoundPath=""`、`IsSettingsEnabled=false`、`IsNotificationTopmostEnabled=false`。依据:`repos/ClassIsland/ClassIsland.Shared/Models/Notification/NotificationSettings.cs:11-17`。
4. 提醒播放会话字段:`OverlayContent`、`MaskContent`、`OverlayDuration`(默认 5s)、`MaskDuration`(默认 5s)、`TargetOverlayEndTime`、`TargetMaskEndTime`、`OverlaySpeechContent`、`MaskSpeechContent`、`IsSpeechEnabled`(默认 true)、`RequestNotificationSettings`、`ChannelId`、`State`、`LeftProgress`(默认 1.0)。依据:`repos/ClassIsland/ClassIsland.Core/Models/Notification/NotificationRequest.cs:14-160`、`repos/ClassIsland/ClassIsland.Shared/Models/Notification/NotificationRequest.cs:16-20`。
5. 优先级排序必须与 ClassIsland 一致:`IsPriorityOverride` 最高(插队);否则先比 `Priority`(数值小优先),再比入队 `Index`。依据:`NotificationPriority.CompareTo`(`repos/ClassIsland/ClassIsland.Shared/Models/Notification/NotificationPriority.cs`)。
### 13.8 主题数据
1. 主题目录:`data/themes/<主题Id>/`(解压后),启用列表 `data/themes/EnabledThemes.json`(`string[]`),主题包缓存 `cache/theme-packages/<主题Id>.zip`。依据:`XamlThemeService.cs:47-49,392-440`。
2. 主题清单 `ThemeManifest` 字段与默认值:`Id=""`、`Name=""`、`Author=""`、`Url=""`、`Description=""`、`Version="0.0.0.0"`、`Banner="banner.png"`、`VerticalSafeAreaPx=0`。依据:`repos/ClassIsland/ClassIsland.Core/Models/XamlTheme/ThemeManifest.cs`。
3. 启用/禁用主题后必须重建样式表并持久化;**禁用全部主题时必须回落到内置主题**,不得白屏。
4. 主题包安装必须:校验 zip 路径安全 → 校验清单必填字段 → 解压到 `data/themes/<Id>/` → 若已存在则保留用户自定义值(按字段合并,不整目录覆盖)。
5. **默认离线(强制)**:主题市场与插件市场同一策略——安装包**不内置在线官方索引**,只内置离线示例索引;用户必须手动添加索引 URL 并二次确认后才联网;网络失败不得影响本机已安装主题与 `EnabledThemes.json`。依据:与 12.3 第 7 条同一产品决策【推断】。
### 13.9 其他存储
1. **机器级全局存储**:`Global.json`(键值对字符串),ClassIsland 放在 `%LOCALAPPDATA%\ClassIsland\Global\Global.json`;本实现放 `<userData>/global.json`,用于"上次运行版本、是否首次启动、Sentry 开关、MLE 标记"等不适合放进用户配置的信息。依据:`repos/ClassIsland/ClassIsland.Core/Services/GlobalStorageService.cs`。
2. **设置叠层**:自动化/插件临时覆盖设置时必须记录"原值 + 覆盖者 Guid",移除覆盖者后按栈顶恢复;不得直接改配置后忘记恢复。依据:`repos/ClassIsland/ClassIsland/Services/SettingsService.cs:191-259`。
3. **信任档案列表**:`trustedProfileIds` 默认 `[]`;首次加载未信任档案时必须提示"该档案来自外部,是否信任";不允许自动化在未信任档案下运行(对齐 `AutomationService` 对 `IsCurrentProfileTrusted` 的检查)。依据:`ProfileService.cs:166-176,398-422`。
4. **缓存目录可安全清空**:`cache/` 下任何内容删除后不得影响功能(下次自动重建);设置页"清理缓存"不得删除 `data/`、`backups/`、`config.json`。
### 13.10 备份、恢复与迁移(必须实现)
**加载时备份**:
1. 每次读取 JSON 成功 → 复制为 `.bak`;失败 → 用 `.bak` 恢复并记入错误列表。依据:`ConfigureFileHelper.cs:63-96`。
2. `.bak` 只保留一份(覆盖式),不做多版本滚动;多版本由 `backups/` 承担。
**整包备份**:
1. 备份内容固定为:`config.json`、`Settings.json`(若存在)、`data/` 全目录、`data/themes/EnabledThemes.json`;**不得**包含 `cache/`、`logs/`、`updates/`、插件包缓存。依据:`repos/ClassIsland/ClassIsland/Services/FileFolderService.cs:115-168`(备份 `Config/` + `Profiles/` + `Settings.json`,排除缓存/日志)。
2. 备份文件名:自动备份 `Auto_Backup_yy-MMM-dd_HH-mm-ss.zip`,手动备份 `Backup_yy-MMM-dd_HH-mm-ss.zip`。依据:`FileFolderService.cs:128-132`。
3. 自动备份策略:`isAutoBackupEnabled=true` 时,距上次备份 ≥ `autoBackupIntervalDays`(默认 7 天)才执行;只保留最近 `autoBackupLimit`(默认 16)份 `Auto_*`,按文件修改时间倒序删除多余。依据:`FileFolderService.cs:83-113`。
4. 备份写入必须"先写 `.tmp` 再 rename";备份失败不得影响启动与当前数据(只记日志并在设置页提示)。
5. 恢复流程:选择备份 → **先自动创建一份"恢复前快照"** → 校验 zip 结构(必须含 `config.json` 或 `Settings.json` + `data/profiles/Default.json`)→ 解压到临时目录 → 原子替换 → 提示"重启后生效" → 重启应用。恢复失败必须回滚到快照。
6. 备份必须可在资源管理器里直接解压查看(不得加密、不得用私有格式)。
**版本迁移**:
1. 必须在 `config.json` 写入 `schemaVersion`(当前 `1`),并在 `data/profiles/*.json` 内写 `SchemaVersion`(当前 `1`,允许缺省视为 1)。**这是本实现的工程必需字段**(ClassIsland 没有,但迁移流水线需要显式版本号)。**【推断】**
2. 迁移执行顺序:读取文件 → 记录 `fromVersion` → 逐级执行 `migrations[fromVersion → fromVersion+1]` → 写回 → 备份迁移前文件到 `backups/migrate-<from>-to-<to>-<时间>.zip`。任一步失败必须中止并保留原文件,提示"迁移失败,已保留原配置"。
3. ClassIsland 形迁移规则必须实现(依据 `repos/ClassIsland/ClassIsland/Services/SettingsService.cs:134-168`):
- 旧版本 `LastAppVersion < 2.0.0.2`:重新登记 URL 协议(`classisland://`)。
- 旧版本 `LastAppVersion < 2.0.3.0`:重置教程初始化标记(`WillMigrateInitTutorial`)。
- 旧版本 `LastAppVersion < 2.0.3.3`:重写开机自启快捷方式(先禁用再启用)。
- 迁移完成后必须把 `LastAppVersion` 更新为当前版本(`SettingsService.cs:112`)。
4. 从 ClassIsland 导入必须支持两种入口:`--importV1 <dir>`(1.x)、`--importV2 <dir>`(2.x);导入前必须自动备份当前数据;导入后必须展示"导入结果清单"(成功项/跳过项/失败项及原因),不得静默覆盖。依据:`repos/ClassIsland/ClassIsland/Program.cs:60-65`。
5. 组件迁移:`ComponentRegistryService.MigrationPairs` 必须内置一份"旧 GUID → 新 GUID"映射;导入旧组件布局时按映射改写并保存。依据:`ComponentsService.cs:140-161`。
6. 空 GUID 兼容:所有 `Guid` 字段读到 `""` 必须解析为 `Guid.Empty`;写出时 `Guid.Empty` 写成 `""`(对齐 `GuidEmptyFallbackConverter` 的双向规则)。依据:`repos/ClassIsland/ClassIsland.Shared/JsonConverters/GuidEmptyFallbackConverter.cs`。
7. 颜色兼容:读入支持 `#RRGGBB`、`#RRGGBBAA`、`#AARRGGBB` 三种写法(ClassIsland 写出为 `#RRGGBBAA`,Alpha 在尾部);写出统一 `#RRGGBBAA` 大写。依据:`repos/ClassIsland/ClassIsland.Core/Converters/ColorHexJsonConverter.cs`(`AlphaComponentPosition.Trailing`)。
8. 时间兼容:`DateTime` 写出为 ISO `yyyy-MM-ddTHH:mm:ss`(无时区,本地时间);`TimeSpan` 写出为 `hh:mm:ss`(超过 24h 用 `d.hh:mm:ss`);读入必须同时接受带时区与不带时区的 ISO 字符串,并按本地时区归一。依据:`repos/ClassIsland/examples/example.json:10-11` 与 System.Text.Json 默认 `TimeSpan` 格式。
9. 枚举兼容:所有枚举按**数字**落盘(如 `TimeType`、`UpdateStatus`、`RulesetLogicalMode`),不得写出名字;读到未知数字必须夹紧到合法范围并记 warn。依据:ClassIsland 使用默认 `System.Text.Json` 序列化(`ConfigureFileHelper.SerializerOptions = new()`,`ConfigureFileHelper.cs:30`),枚举默认输出数字。
10. 迁移必须**幂等**:同一版本重复执行不得产生二次修改(用 `schemaVersion` 与字段存在性双重判断)。
**Excel 导入导出(P1 必须实现,xlsx 重建)**:
11. 必须用 `xlsx`(SheetJS)在**主进程/utilityProcess** 中重建 ClassIsland 的 Excel 导入导出能力;ClassIsland 的 `unvell.ReoGrid` + WPF 窗口实现(`repos/ClassIsland/ClassIsland/Views/ExcelImportWindow.xaml.cs:29-44`、`ExcelExportWindow.xaml.cs:23-43`)**只作字段与流程参考**,不得照搬 UI。
12. 导入最低能力:读取 `.xlsx` → 选择工作表 → 识别"行 = 节次 / 列 = 星期"的网格 → 生成 `ClassPlan` + 对应 `TimeLayout`;必须支持合并单元格、空节次、科目名去重、教师名写入 `Subject.TeacherName`;解析失败必须给出行列级错误提示,且**不得**覆盖现有档案(先写入临时档案,用户确认后替换)。
13. 导出最低能力:把当前档案/课表写成 `.xlsx`(一个工作表一张课表,首行星期、首列节次与起止时间),首行冻结;导出文件默认落 `下载/ClassIsland-课表-<日期>.xlsx`,由用户选择路径。
14. 导入导出必须遵守 P1 数据兼容:导出的 xlsx 再导入必须得到等价档案(科目/时间表/课表/周次规则不丢);单文件上限 8MB(对齐 ClassHelper 服务端表格上限,见 `repos/ClassHelper-master/AGENTS.md` §2);导入前自动备份当前档案。
### 13.11 读写一致性与错误处理
1. 所有加载函数必须返回"数据 + 警告列表",不得只返回数据;设置页与诊断页必须能展示"哪些文件回退了备份、哪些字段被重置"。
2. 阻止"写入正在读取的文件":每个文件维护内存版本号,写入前比较版本;发现外部修改(用户在资源管理器里改了 JSON)必须提示"配置已被外部修改,是否重新加载",禁止静默覆盖用户改动。**【推断】**
3. 任何写入失败必须:记 error(含路径、错误码、可用磁盘空间)→ 提示用户 → 保留旧文件不变(不得留下半截文件)。
4. 磁盘空间不足(< 100MB)时禁止自动备份并提示"清理磁盘";备份与导出前必须检查可用空间。
5. 读取时必须限制单文件大小(配置文件 ≤ 8MB、档案 ≤ 32MB、备份 zip ≤ 512MB),超限拒绝并提示,防止恶意文件把内存打爆。**【推断】**
### 13.12 天气数据 schema(与 07 天气服务对齐)
1. **缓存位置与字段**:最后一份天气必须持久化在 `config.json` 的 `lastWeatherInfo`(与 ClassIsland 把 `Settings.LastWeatherInfo` 当缓存一致,见 `repos/ClassIsland/ClassIsland/Services/WeatherService.cs:48-51,387`),并附加 `lastWeatherInfoUpdatedAt`(ISO 本地时间)与 `lastWeatherInfoStale`(boolean)。缓存**不得**因查询失败被清空:失败时保留旧值并标记 `stale`,由 UI 展示"天气数据可能已过期"。
2. **刷新节奏**:天气服务必须每 5 分钟检查一次(`WeatherService.cs:41-44`)、启动后立即查询一次(`:71`);查询失败按 30s/60s/120s 退避重试 3 次后停到下个周期。请求默认使用 `https://`;仅当用户显式开启 `noTLSWeatherRequests` 时才允许 `http://`(对齐 `WeatherService.cs:34`)。
3. **来源抽象**:不得把第三方接口写死在渲染层。必须定义 `WeatherProvider` 接口(`id`、`name`、`requiresNetwork`、`fetch()`),内置"小米天气"参考实现并允许替换:城市信息 `GET {base}/location/city/info?locationKey=<cityId>&locale=zh_cn`、全量天气 `GET {base}/weather/all?latitude=<lat>&longitude=<lon>&locationKey=<cityId>&days=15&appKey=<key>&sign=<sign>&isGlobal=false&locale=zh_cn`(依据 `WeatherService.cs:327,358-360`)。`base/appKey/sign` 必须可配置、不得出现在渲染进程产物中;默认天气提示"需联网"。
4. **预警清洗**:查询结果必须按 `excludedWeatherAlerts`(默认 `[]`)过滤——`Title` 或 `Detail` 包含排除词即剔除(`WeatherService.cs:363-374`);同一 `Type` 只保留 `PubTime` 最新一条(`:376-385`);`alerts` 按 `pubTime` 倒序展示。
5. **图标与码表**:`weatherIconId` 默认 `classisland.weatherIcons.lucide`(`Settings.cs:1849`);天气码到文案的映射必须来自内置资源 `Assets/XiaomiWeather/xiaomi_weather_status.json`(`WeatherService.cs:228-233`),并提供 `getWeatherTextByCode(code)` 等价函数。
6. **数据结构(字段名必须与 07 一致,JSON 键为 camelCase;ClassIsland 原始模型用 `[JsonPropertyName]` 指定小写键,可直接对齐)**:
| 类型 | 字段 | 类型 | 默认值 |
|---|---|---|---|
| `WeatherInfo` | `current` | `CurrentWeather` | 空对象 |
| | `alerts` | `WeatherAlert[]` | `[]` |
| | `updateTime` | number(Unix 毫秒) | `0` |
| | `forecastDaily` | `ForecastDaily` | 空对象 |
| | `forecastHourly` | `ForecastHourly` | 空对象 |
| | `minutely` | `Minutely` | 空对象 |
| | `aqi` | `AqiInfo` | 空对象 |
| `CurrentWeather` | `feelsLike` / `humidity` / `pressure` / `temperature` / `visibility` | `ValueUnitPair {value:string, unit:string}` | `{"",""}` |
| | `weather` | string(天气码) | `"99"` |
| | `pubTime` | string(ISO) | 查询时间 |
| | `wind` | `WindInfo {direction:ValueUnitPair, speed:ValueUnitPair}` | 空对象 |
| `WeatherAlert` | `locationKey` / `alertId` / `title` / `type` / `level` / `detail` | string | `""` |
| | `pubTime` | string(ISO) | 当前时间 |
| | `images` | `map<string,string>` | `{}`(至少含 `icon`) |
| `ForecastDaily` | `precipitationProbability` | `StatusValueBase<string[]>` | `{value:[]}` |
| | `temperature` / `weather` / `sunRiseSet` | `StatusValueBase<RangedValue[]>`(`RangedValue {from,to}`) | `{value:[]}` |
| `ForecastHourly` | `temperature` / `weather` | `StatusValueBase<number[]>` | `{value:[]}` |
| `Minutely` | `precipitation` | `MinutelyPrecipitation {value:number[]}` | `{value:[]}` |
| `AqiInfo` | `aqi` | string | `"0.0"` |
| `WeatherQueryResult` | `isSuccess` / `isPrecisionDegraded` | boolean | `false` |
| | `errorMessage` | string? | `null` |
| | `degradedPrecision` | number | `0` |
7. **派生字段(不得落盘,读取时计算)**:`WeatherInfo.updateTime` → 本地时间;`MinutelyPrecipitation.rainRemainingMinutes`(首值为 0 时返回"接下来多少分钟开始下雨",首值 >0 时返回负的"还有多少分钟停雨",算法见 `repos/ClassIsland/ClassIsland.Core/Models/Weather/MinutelyPrecipitation.cs`);`AqiInfo.aqiLevel`(1–6,按 50/100/150/200/300 分档,见 `AqiInfo.cs`)。
8. **与 07 的契约**:07 天气服务必须复用本节字段名与派生字段;规则集 handler ID 必须为 `classisland.weather.currentWeather`、`classisland.weather.tomorrowWeather`、`classisland.weather.hasWeatherAlert`、`classisland.weather.rainTime`、`classisland.weather.sunRiseSet`(`WeatherService.cs:64-68`);提醒文案与图标渲染由 07 定义,但**读取的数据结构只能是本节 schema**。
### 13.13 验收标准(第 13 部分)
1. 首次启动能在 `<userData>/` 下生成完整的目录结构与默认 `config.json`、`Default.json` 档案、`Default.json` 组件布局、默认科目表;目录清单与 13.2 完全一致。
2. 删除 `config.json` 后应用能正常启动并重建默认配置;删除 `data/` 后能重建默认档案与组件布局,不白屏、不崩溃。
3. 故意把 `config.json` 改成非法 JSON 后启动:应用回退到 `.bak`;若 `.bak` 也非法则使用默认值并进入"配置错误"提示,不得启动失败。
4. `Settings.json`/档案的字段名、类型、默认值与 13.3/13.4 表一致;用 ClassIsland 的真实档案 JSON 导入后,时间表、课表、科目、周次规则全部正确解析(含 `TimeType=3` 与 `IsHideDefault`、`AttachedObjects`)。**并且**:运行期只写 `config.json`,`Settings.json` 只读导入/单独导出、不双向双写;`IsIslandSeperated` 原拼写能被导入与导出映射。
5. 写入过程中断电(模拟 `rename` 前杀进程)不会损坏原文件;`.tmp` 残留有清理机制。
6. 自动备份:把 `lastAutoBackupTime` 改为 8 天前,启动后生成 `Auto_Backup_*.zip`;备份超过 16 份时最旧的 `Auto_*` 被删除,手动 `Backup_*` 不受影响。
7. 备份 zip 内必须含 `config.json`、`data/profiles/Default.json`、`data/component-layouts/Default.json`、`data/automations/Default.json`,且不含 `cache/`、`logs/`、`updates/`。
8. 恢复备份:选择备份 → 自动生成恢复前快照 → 恢复 → 重启后数据与备份一致;中途失败能回滚。
9. 迁移流水线:把 `schemaVersion` 改为 0 的旧结构文件,启动后能迁移到 1 并保留原字段;迁移失败时原文件不被破坏。
10. 组件布局导入旧 GUID 后按 `MigrationPairs` 改写,且迁移后立即保存、不重复迁移。
11. 空 GUID、`#RRGGBB`、带时区 ISO、数字枚举四类兼容用例全部通过(读写往返不丢信息)。
12. 档案切换/删除后 `config.selectedProfile` 指向有效文件;指向不存在文件时回落到 `Default.json` 并提示。
13. 自动化配置按 `Workflow[]` 落盘;触发器/行动 ID 与 13.6 表一致;规则集 `Mode`/`IsReversed`/`IsEnabled` 往返不丢。
14. 通知历史落盘、默认上限 500(可配 100–5000)、超限滚动删除、写入节流(≥5 条或 ≥5s)、清空需二次确认;历史文件损坏时回退为空列表并记警告,不影响提醒播放。
15. 所有写入路径都能在世界可读的 JSON 中直接看到内容(无私有封装、无加密),且 UTF-8 无 BOM、LF 换行。
16. 附加设置:Profile/ClassPlan/ClassInfo/TimeLayout/TimeLayoutItem/Subject 都能读写 `attachedObjects`;4 个已知 Guid 的设置对象默认值正确;导入 `AttachedObjects`(PascalCase)后可按目标生效;未知 Guid 不丢失;导出写回 `AttachedObjects`。
17. 天气:`lastWeatherInfo` 缓存可持久化、失败时保留旧值并标 `stale`;`WeatherInfo`/`WeatherAlert`/`ForecastDaily`/`ForecastHourly`/`Minutely`/`AqiInfo`/`WeatherQueryResult` 字段与 13.12 完全一致;预警按排除词过滤并按 Type 保留最新;5 个天气规则 ID 可被 07 使用。
18. Excel:能从 `.xlsx` 导入生成课表(先临时档案、确认后替换、导入前备份),能导出 `.xlsx` 再导入得到等价档案;8MB 上限;所有解析在 utilityProcess 或 worker 中执行,不阻塞主进程。
19. 坐标:`config.json` 中所有几何字段(`windowDockingOffsetX/Y`、`scale`、`radiusX/Y`、字号、间距)均为 DIP 数值;把 `scale` 由 1 改为 1.5 后窗口与岛按 CSS px 等比放大,不出现 devicePixelRatio 二次换算。
20. 主题市场默认离线:无内置在线官方索引;手动添加 URL 需二次确认;断网时本机已安装主题与 `EnabledThemes.json` 正常工作。