- 重构版主提示词(14 parts,面向 ClassHelper 灵动岛重构) - ClassIsland 全量规格提示词(20 parts,含分卷与逐章版本) - 投喂指南、分卷、逐节文件 - 调研记录、一致性基线、生成与质检脚本
31 KiB
9. 主题系统(内置主题、主题包格式、壁纸取色、主题预览、自定义颜色)
实现者必须把「主题」拆成两层独立系统,不得混为一谈:
- 应用主题(App Theme):控制整个应用(设置中心、窗口、组件库)的明暗与强调色。对应 ClassIsland 的
IThemeService/ThemeService,只做三件事:切换 ThemeVariant(浅/深/跟随系统)、设置强调色、扩散强调色派生画刷。 - 主界面主题(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):
themeMode=0→ 主题变体Default且preferSystemTheme=true(系统变浅即浅、变深即深);themeMode=1/2→ 显式Light/Dark,并preferSystemTheme=false;- 强调色:
ColorSource=0用PrimaryColor;1/3用SelectedPlatte;2传null表示跟随系统强调色; - 强调色变化后必须重算
CustomizedAccentBarBackground1Brush= 强调色 + alpha0.10(ThemeService.cs:66-74、Settings.cs:1037-1039); - 应用主题变化必须触发
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 加载与覆盖规则
- 启动时先注册内置主题(
IXamlThemeService.IntegratedThemes,ClassIsland.Core/Abstractions/Services/IXamlThemeService.cs:21),再扫描Themes/目录加载外部主题; - 只加载
EnabledThemes中列出的主题,且按数组顺序依次叠加:后加载主题覆盖前面主题的同名样式(IXamlThemeService.cs:63-66); - 任一主题样式加载失败必须隔离:标记
IsError、记录异常、继续加载后续主题,不得导致应用崩溃(XamlThemeService.cs:112-133); - 主题的
VerticalSafeAreaPx取所有已启用主题的最大值(不是求和)(XamlThemeService.cs:111-135); - 主题安装/卸载/换序后必须提示「重启以应用更改」(
XamlThemeService.cs:232-236,369-374); - 安全模式(
--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 取色流程
- 触发时机:注册表
Control Panel\Desktop变更、系统UserPreferenceChanged(Desktop)、或定时器(间隔 =WallpaperAutoUpdateIntervalSeconds,默认 60s)(WallpaperPickingService.cs:45-51,91-94,117-128,136-162); - 取图:
ColorSource=3直接截取「岛所在显示器」的全屏;否则截取桌面窗口(类名默认Progman,可被WallpaperClassName覆盖);兼容模式则直接读注册表WallPaper路径并等比缩放(WallpaperPickingService.cs:164-222,253-261); - 缩放:图片宽度统一转换(
ConvertToBitmapImage(bitmap, bitmap.Width),WallpaperPickingService.cs:278); - 主算法(旧):八叉树量化(
ClassIsland/ColorOctTreeNode.cs)+ HSV 评分; - 备选算法(实验):
AccentColorHelper.GetAccentColor(bytes, w, h)(ClassIsland/Helpers/AccentColorHelper.cs,注明来自 Starward 项目); - 结果:取评分前 5 色写入
WallpaperColorPlatte,并把SelectedPlatteIndex重置为0(WallpaperPickingService.cs:295-307,337-341); - 失败兜底:
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 主题调试、兼容性与安全边界
- 调试入口:开发者菜单提供「重载主题」(
ThemesSettingsPage.axaml:99-101),必须能在不重启应用的情况下重新加载全部已启用主题的变量与样式;加载失败主题的错误信息必须可从卡片上的错误按钮打开抽屉查看完整堆栈(ThemesSettingsPage.axaml:24-30,206-213)。 - 版本兼容:
ThemeManifest.version必须能被语义化解析;市场比较使用Version.TryParse(XamlThemeService.cs:201-206)。主题包必须声明minAppVersion(新增字段,默认"0.0.0"),低于该版本时禁用安装并提示。 - 安全边界:主题不得访问文件系统、网络与 Node API;在 Electron 下必须运行在「只读 CSS/JSON」模型中,禁止
eval、new Function、内联on*事件(与 ClassHelper preload 白名单纪律一致:ClassHelper/AGENTS.md第 4 节「preload 仅暴露 contextBridge 白名单」)。违规主题必须整包拒绝安装,而不是降级运行。 - 主题与提醒/组件的耦合:主题可以改遮罩的颜色与时长,但不得改动提醒的排队与优先级逻辑;可以改组件容器的圆角与间距,但不得改组件内部的业务渲染。实现时把「主题变量」与「组件状态」严格分层。
- 性能底线:主题切换必须在 100ms 内完成变量注入;主题包 CSS 总大小上限建议 64KB,超过时提示;加载 20 个主题的总耗时不得超过 200ms(解析 + 校验)。
- 降级:若主题声明的 token 非法(如把圆角写成
-8px、把时长写成0ms),必须回落到默认值并记录警告,不得静默忽略整份主题。
9.12 验收标准
- 双层隔离:切换主界面主题时,设置中心颜色/圆角/字体完全不动;切换应用明暗时,岛主题的圆角/描边样式完全不动。
- 内置主题:列表恰好 2 个内置主题(Fluent / 经典),Banner 比例 1920×350,Fluent 安全区为 20、经典为 0;默认只启用 Fluent。
- 主题包安装:把 zip 放入
ThemePackages后重启,主题出现在「已安装」;manifest.yml缺失时用目录名兜底;Styles/theme.css语法错误时该主题标记为错误且其他主题照常工作。 - 加载顺序:拖动排序后
EnabledThemes.json顺序变化,重启后叠加效果随顺序变化(后加载覆盖先加载)。 - 卸载:点「卸载」写入
.uninstall并置灰,重启后目录被删除;「撤销卸载」可恢复。 - 安全:未同意安全警告前看不到主题列表;含
@import/url(http…)/position:fixed/z-index的主题 CSS 被逐条拒绝且记录警告。 - 壁纸取色:给定一张固定测试图,算法稳定输出 5 色且顺序可复现;把
TargetLightValue从 0.6 调到 0.2/0.9,首选色亮度相应变化;取色过程中 UI 不卡顿(主线程帧耗时 < 16ms)。 - 自定义颜色优先级:同时设置全局背景色与行级背景色,行级生效;关闭行级开关后回落全局;再关闭全局后回落主题。
- 分体主界面:Fluent 下开启分体后每行独立成岛、圆角独立、行间出现间距;经典主题下该开关不可用或无效并有说明。
- 市场更新:本地主题版本低于市场版本时出现「更新」按钮;下载完成后提示重启;MD5 不匹配时安装失败并报错。
- 主题安全区:启用 Fluent 后岛与屏幕边缘出现 20px 竖直安全区;同时启用多个主题时取最大值而非累加。
- 不阻塞:主题切换/加载不得造成超过 1 帧(16.7ms)的主线程长任务;大规模图片解码必须异步。