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

31 KiB
Raw Blame History

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 落地要求:

: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 的结构必须为:

{ "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 八叉树评分公式(必须逐项实现)

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):

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)的主线程长任务;大规模图片解码必须异步。