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

159 KiB
Raw Blame History

3. 设计系统 Token 总表(颜色 / 字体 / 圆角 / 间距 / 阴影 / 层级 / DPI / 深浅色)

本节是整份提示词的「地基」。实现者必须先落地本节的 token(CSS 自定义属性 + TypeScript 常量),再开始写任何界面。任何界面代码不得出现裸色值、裸字号、裸圆角,一律引用本节 token。

实现技术栈为 ClassHelper 的 Electron + Vue 3 栈,宿主已有 packages/ui/src/tokens.css 的 --ch-ui-* 与桌面端 renderer/styles/index.css 的 --ch-*。保留并复用这些既有 token 作为「应用外壳层」,新增 --ci-* 命名空间承载 ClassIsland 主界面(岛)层 token。两层必须分开:--ch-* 管设置中心/管理页,--ci-* 管岛与课程信息。

3.1 颜色 Token

3.1.1 强调色与派生色

Token 名 默认值 用途 建议 CSS 变量 源码证据
PrimaryColor(用户强调色) #00BFFF(Colors.DeepSkyBlue) 设置项「强调色」的默认值;ColorSource=0 时作为全站 accent --ci-accent ClassIsland/Models/Settings.cs:35、Settings.cs:992
SecondaryColor #7FFFD4(Colors.Aquamarine) 备用强调色(组件/图表次级色) --ci-accent-2 Settings.cs:36、Settings.cs:1003
FluentAvalonia 初始 accent DodgerBlue=#1E90FF 主题服务尚未介入时的启动色 --ci-accent-fallback ClassIsland/App.axaml:17
SelectedPlatte 兜底 DodgerBlue=#1E90FF 壁纸调色板取不到颜色时的兜底 --ci-accent-palette-fallback Settings.cs:1036-1039、Settings.cs:80
AccentFillColorDefaultBrush 由 accent 派生(Fluent 资源) 提醒遮罩填充、教程高亮框、进度条强调 --ci-accent-fill ClassIsland.Core/StylesBase.axaml:90-93、XamlThemes/FluentTheme/Styles.axaml:180
AccentFillColorTertiaryBrush accent 低透明度变体 课程列表展开态底色 --ci-accent-fill-tertiary ClassIsland.Core/Controls/LessonsControls/LessonControlExpanded.axaml:182
AccentTextFillColorPrimaryBrush accent 文字色 拖拽高亮描边、TeachingTip 指示箭头 --ci-accent-text EditableComponentsListBox.axaml:310、MyTeachingTipStyles.axaml:213
TextOnAccentFillColorPrimaryBrush 白/黑(随 accent 对比度) 遮罩上的文字 --ci-text-on-accent FluentTheme/Styles.axaml:162,182;ClassicTheme/Styles.axaml:142
CustomizedAccentBarBackground1Brush 亮 #FFDDE6F0 / 暗 #26374C;运行时被覆盖为 accent 的 10% 透明 「强调按钮」淡底、Button.foreground-accent --ci-accent-soft Themes/LightTheme.axaml:18、Themes/DarkTheme.axaml:18、Services/ThemeService.cs:66-74

运行时强调色决策必须严格按以下分支实现(源码见 ClassIsland/MainWindow.axaml.cs:743-764):

  1. ColorSource == 0(自定义)→ 使用 PrimaryColor;
  2. ColorSource == 1(壁纸主题色)或 3(屏幕主题色)→ 使用 SelectedPlatte(壁纸调色板第 SelectedPlatteIndex 项);
  3. ColorSource == 2(跟随系统)→ 传 null,交给系统强调色;
  4. 其余 → 兜底 DodgerBlue。

--ci-accent-soft 必须以 color-mix(in srgb, var(--ci-accent) 10%, transparent) 实时推导,不得写死。ClassHelper 已有等价写法可复用:packages/ui/src/theme.css:4-9 用 color-mix() 派生 Element Plus 的 --el-color-primary-light-*。

3.1.2 前景文字阶梯(前景色 5 级)

主界面自定义前景色时,前景色不是 1 个值,而是由同一个基色派生出 5 个 alpha 版本,必须按下表实现(源码:ClassIsland.Core/Helpers/UI/ControlColorHelper.cs:20-52):

层级 资源键 alpha 用途 建议 CSS 变量
1 TextFillColorPrimaryBrush 255(100%) 主要文字、标题 --ci-text-1
2 TextFillColorInformativeSecondaryBrush 218(85.5%) 次要说明(本项目覆盖值) --ci-text-2
3 TextFillColorSecondaryBrush 197(77.3%) 辅助文字 --ci-text-3
4 TextFillColorTertiaryBrush 135(52.9%) 装饰/弱提示 --ci-text-4
5 TextFillColorDisabledBrush 93(36.5%) 禁用态 --ci-text-5

实现规则:

  • 未启用自定义前景色时,上述 5 个 token 使用 Fluent 默认值:亮色 #E4000000 / #9E000000 / #72000000 / #5C000000,深色 #FFFFFFFF 及对应 alpha 版本【推断:FluentAvalonia 资源值不在本仓库,按 Fluent 2 规范给出】。
  • 启用自定义前景色时,5 个 token 全部按上表 alpha 从 CustomForegroundColor 派生,且必须同时重设 TemplatedControl.Foreground 与 TextElement.Foreground,否则模板内文字会漏改(ControlColorHelper.cs:34-40)。
  • 关闭自定义前景色时,必须 Remove 这 5 个局部资源并 SetValue(..., AvaloniaProperty.UnsetValue) 回落(ControlColorHelper.cs:44-50)——映射到 CSS 即删除元素上的内联 --ci-text-*,回落 :root。
  • 两个默认覆盖值:亮色 TextFillColorInformativeSecondaryBrush = #c0000000(75% 黑),深色 #daffffff(85.5% 白)(Themes/LightTheme.axaml:20、Themes/DarkTheme.axaml:20)。
  • 语义层级类必须映射为 CSS 工具类:.l-primary|.l1 → --ci-text-1、.l-secondary|.l2 → --ci-text-2、.l-instructive|.l3 → --ci-text-3、.l-decoration|.l4 → --ci-text-4、.l-inactive|.l5 → --ci-text-5(ClassIsland.Core/Themes/Layers.axaml:10-43)。

3.1.3 背景与表面色

Token 名 亮色 深色 用途 建议 CSS 变量 源码证据
SolidBackgroundFillColorBaseBrush #F3F3F3【推断】 #202020【推断】 经典主题岛背景、编辑模式窗口底、抽屉内容底、主题卡渐变底 --ci-bg-base ClassicTheme/Styles.axaml:214;DrawerHost.axaml:29;MainWindow.axaml:188
SolidBackgroundFillColorSecondaryBrush #EEEEEE【推断】 #1C1C1C【推断】 Fluent 主题岛背景 --ci-bg-secondary FluentTheme/Styles.axaml:45
SolidBackgroundFillColorTertiaryBrush #F9F9F9【推断】 #282828【推断】 弹出层底(.popup-bg) --ci-bg-tertiary StylesBase.axaml:291
ControlFillColorSecondaryBrush 半透明白/黑 同左 主题卡底色 --ci-fill-secondary ThemesSettingsPage.axaml:149
SmokeFillColorDefaultBrush 半透明遮罩 同左 编辑模式窗口背景、抽屉忽略层 --ci-smoke MainWindow.axaml:178;DrawerHost.axaml:20
Layer 应用底色 #F3F3F3 #161719 设置中心/管理页页面底(沿用 ClassHelper) --ch-bg ClassHelper/renderer/styles/index.css:27,114
Layer 卡片底 rgba(255,255,255,.7) rgba(32,33,36,.72) 应用外壳卡片 --ch-layer 同上 :28,115
CustomBackgroundColor #000000(默认) 同左 岛自定义背景色默认值 --ci-island-bg-custom Settings.cs:34、Settings.cs:1129

主界面行的背景最终呈现必须叠加两个不透明度:

  • 自定义背景色本身的不透明度:--ci-island-bg-opacity,默认 0.5(Settings.cs:64、:1141;Fluent 用 MainWindowStylesAssist.BackgroundOpacity 绑定到 Border.Opacity,见 FluentTheme/Styles.axaml:44);
  • 整行不透明度:MainWindowLineSettings.Opacity,默认 1.0(ClassIsland.Core/Models/Components/MainWindowLineSettings.cs:42;Controls/MainWindowLine.axaml:51)。
  • 鼠标移出淡化态:整行 opacity = 0.05(Controls/MainWindowLine.axaml:73-75)。

3.1.4 描边与分隔线

Token 名 值 用途 建议 CSS 变量 源码证据
岛描边 1px ControlElevationBorderBrush 经典/Fluent 行背景描边 --ci-stroke-island / 宽度 --ci-stroke-island-w:1px FluentTheme/Styles.axaml:46-47;ClassicTheme/Styles.axaml:61-65
ControlStrokeColorDefaultBrush 亮色半透明黑 编辑模式选中框描边 --ci-stroke-control MainWindow.axaml:357
DividerStrokeColorDefaultBrush 亮色半透明黑 操作条分隔线(1px,长 25px) --ci-stroke-divider MainWindow.axaml:610-612
SurfaceStrokeColorFlyoutBrush 半透明 弹窗描边 --ci-stroke-flyout StylesBase.axaml:292
ControlStrongFillColorDefaultBrush 中灰 OOBE 方块、Workflow 进度条底、Shimmer 底 --ci-fill-strong OobeIntroAnimationControl.axaml:34;WorkflowProgressIndicatorItemControl.axaml:26;Shimmer.axaml:11
主题卡描边 1px ControlElevationBorderBrush 主题网格卡片 --ci-stroke-card ThemesSettingsPage.axaml:150-151
拖拽高亮描边 1px accent 组件拖拽占位 --ci-stroke-drag EditableComponentsListBox.axaml:310-311
教程高亮描边 2.5px(外框)/ 3px(四角) 教程 spotlight --ci-stroke-tutorial TutorialHighlightControl.axaml:96-118

3.1.5 状态色

天气与空气质量状态色(浅色/深色两套,必须都实现):

语义 亮色 深色 建议 CSS 变量 源码证据
AQI 1 优 ForestGreen=#228B22 MediumSeaGreen=#3CB371 --ci-aqi-1 LightTheme.axaml:10;DarkTheme.axaml:10
AQI 2 良 #CCCC00 Yellow=#FFFF00 --ci-aqi-2 :11
AQI 3 轻度 DarkOrange=#FF8C00 Orange=#FFA500 --ci-aqi-3 :12
AQI 4 中度 IndianRed=#CD5C5C 同亮色 --ci-aqi-4 :13
AQI 5 重度 DarkRed=#8B0000 同亮色 --ci-aqi-5 :14
AQI 6 严重 Purple=#800080 MediumPurple=#9370DB --ci-aqi-6 :15
雨天图标色 Blue=#0000FF DeepSkyBlue=#00BFFF --ci-weather-rain LightTheme.axaml:3;DarkTheme.axaml:3
上课发光 DropShadowEffect blur 6(亮)/2(暗),颜色 #FFFF00 同左 --ci-glow-class LightTheme.axaml:4-7;DarkTheme.axaml:4-7
危险/删除按钮 IndianRed=#CD5C5C 同左 --ci-danger Themes/Generic.xaml:225-226,275-276;MainWindow.axaml:435
警告/错误图标 SystemFillColorCriticalBrush / SystemFillColorCautionBackgroundBrush 同左 --ci-warning-* StylesBase.axaml:209,217;ThemesSettingsPage.axaml:343

3.1.6 阴影与遮罩色(必须逐值实现)

用途 值 建议 CSS 变量 源码证据
Fluent 岛投影 0 4 8 2 #48000000 --ci-shadow-island: 0 4px 8px 2px #48000000 FluentTheme/Styles.axaml:49
提醒遮罩时岛投影 0 8 32 0 #66000000 --ci-shadow-mask FluentTheme/Styles.axaml:324,327
竖抽屉/遮罩卡投影 0 8 32 0 #66000000 --ci-shadow-drawer VerticalDrawer.axaml:45
横抽屉内浮层投影 0 0 8 #33000000 --ci-shadow-drawer-x DrawerHost.axaml:33
经典主题提醒时行投影 DropShadowEffect BlurRadius=8, Opacity=1, Offset=0 --ci-shadow-classic-mask ClassicTheme/Styles.axaml:283-287
时间线装饰器阴影 DropShadowEffect opacity 0.4, blur 8 --ci-shadow-adorn Themes/Generic.xaml:215-219
弹窗底渐变遮罩 #00ffffff→(0.65)#00ffffff→(1)#ffffffff --ci-mask-fade ThemesSettingsPage.axaml:162-169
教程聚光遮罩 #99000000(60% 黑) --ci-scrim-tutorial TutorialSpotlightControl.axaml:45-61
编辑模式调试叠加 #77222222 --ci-debug-bg MainWindow.axaml:645
启动闪屏底 #88111111 + 1px ControlStrokeColorOnAccentDefaultBrush --ci-splash-bg SplashWindow.axaml:24-28
主题卡渐变底 径向 #ff0c0e1f→#ff0d1111(关于页) --ci-brand-gradient-a/b AboutSettingsPage.axaml:153-154
关于页正文底 #0d1111 --ci-brand-dark AboutSettingsPage.axaml:131
通知涟漪默认填充 AccentFillColorDefaultBrush(可被 NotificationContent.Color 覆盖) --ci-ripple RippleEffect.axaml.cs:61;MainWindowLine.cs:750
通知涟漪描边宽 200(Ellipse StrokeThickness) --ci-ripple-stroke:200 RippleEffect.axaml:15

3.2 字体 Token

3.2.1 字体族

Token 值 用途 建议 CSS 变量/写法
AppFont(全局默认) HarmonyOS Sans SC, Microsoft YaHei UI, Noto Sans CJK SC, Noto Sans SC, PingFang SC, Source Han Sans SC, SimHei, WenQuanYi Micro Hei, sans-serif, FluentSystemIcons-Resizable 全应用默认字体 --ci-font: 'HarmonyOS Sans SC', 'Microsoft YaHei UI', 'Noto Sans CJK SC', 'Noto Sans SC', 'PingFang SC', 'Source Han Sans SC', SimHei, sans-serif
MainWindowFont(岛字体,用户可改) 默认等于 DefaultFontFamilyKey:avares://ClassIsland/Assets/Fonts/#HarmonyOS Sans SC, ...FluentSystemIcons-Resizable 只作用于岛的 ZoomBorder 文本 --ci-font-island(元素级覆盖)
HarmonyOsSans(旧键) HarmonyOS Sans SC, Microsoft YaHei UI, SimHei 旧样式/右键菜单 兼容别名
BackendFontFamily Microsoft YaHei UI 后端渲染/图表兜底 --ci-font-backend
图标字体 1 FluentSystemIcons-Resizable(avares://ClassIsland.Core/Assets/Fonts/#FluentSystemIcons-Resizable) 全局 Fluent 图标 --ci-font-icon-fluent
图标字体 2 lucide(#lucide) Lucide 天气图标等 --ci-font-icon-lucide
图标字体 3 SF-Symbols(#SF-Symbols) SFSymbols 天气图标 --ci-font-icon-sf

证据:ClassIsland/Assets/Fonts/Fonts.axaml:3、Assets/Fonts/Fonts.xaml:3、MainWindow.axaml.cs:185-189、App.axaml:48、ClassIsland.Core/AppBase.cs:142-152、ClassIsland.Core/ThemeBase.axaml:46-47。

实现要求:

  • 字体栈必须按上表顺序声明,任何一环缺失即回落下一位;不得只写 HarmonyOS Sans SC。
  • 字体文件必须以本地资源打包,禁止运行时从网络加载字体(离线教室环境)。
  • 图标字体与正文字体必须拆成两个 token,图标组件禁止继承正文字体族。

3.2.2 字重

字重设置以整数保存,映射表必须逐值实现(ClassIsland.Core/Converters/IntToFontWeightConverter.cs:16-28):

数值 字重名 CSS
100 Thin 100
200 ExtraLight 200
300 Light 300
400 Normal 400
500 Medium(默认) 500
600 SemiBold 600
700 Bold 700
800 ExtraBold 800
900 Black 900
其他 Normal 400

默认字重来自 Settings.cs:101((int)FontWeight.Medium = 500)。岛内绝大多数文字必须用 500(Medium),标题可用 700(Bold),说明文字用 400(Regular)。

4 档核心字号在设置页的输入范围均为 1–40 px(AppearanceSettingsPage.axaml:482-533,NumberBox 的 Minimum=1、Maximum=40)。实现时必须允许 1–40 的整数/小数输入,并在预览区用四行示例文字实时展示 4 档效果(源码「字号预览」tab:AppearanceSettingsPage.axaml:570-591)。

3.2.3 字号阶梯(核心 4 档 + 扩展档)

核心 4 档是用户可调的「主界面字号」,默认值与用途如下(Settings.cs:197-200;ClassIsland.Core/ThemeBase.axaml:32-35;Settings.cs:1209-1251):

Token 默认 px 用途 建议 CSS 变量 证据
MainWindowSecondaryFontSize 14 次要信息:天气副标题、日期星期、倒计时标签 --ci-fs-secondary ThemeBase.axaml:32;WeatherComponent.axaml:261,294,296,305
MainWindowBodyFontSize 16 正文:课程名、时间、温度 --ci-fs-body ThemeBase.axaml:33;MainWindow.axaml:313;CountDownComponent.axaml:65
MainWindowEmphasizedFontSize 18 强调:时钟、提醒主文案 --ci-fs-emphasized ThemeBase.axaml:34;ClockComponent.axaml:14;TwoIconsMaskTemplate.axaml:14
MainWindowLargeFontSize 20 大号标题 --ci-fs-large ThemeBase.axaml:35

扩展固定档(非用户设置,必须写死为 token 并说明来源):

字号 用途 建议 CSS 变量 证据
9 启动闪屏版本号(0.5 不透明度) --ci-fs-version SplashWindow.axaml:86-87
12 主题卡作者名 --ci-fs-caption ThemesSettingsPage.axaml:191
13 组件设置小字、ClassHelper 控件基准 --ci-fs-control-base CountDownComponentSettingsControl.axaml:185-201;ClassHelper/renderer/styles/index.css:97
14 通用控件/次级标签 --ci-fs-label MainWindow.axaml:406,452,497
16 主题卡名称、闪屏状态文字 --ci-fs-title ThemesSettingsPage.axaml:188;SplashWindow.axaml:80
17 滚动提醒文字 --ci-fs-rolling RollingTextTemplate.axaml:26
18 OOBE 说明文字 --ci-fs-oobe-body WelcomeWindowIntroControl.xaml:166
20 启动 logo 文字、导航标签 --ci-fs-brand SplashWindow.axaml:56-58;StylesBase.axaml:118-123
22 天气温度/图标文字 --ci-fs-weather WeatherComponent.axaml:86-175
32 OOBE 大标题 --ci-fs-oobe-title OobeIntroAnimationControl.axaml:69,87;WelcomePage.axaml:38

实现硬约束:

  • 4 档核心字号必须由用户设置驱动,且必须能作用到「组件自身」与「单个主界面行」两个作用域:行级覆盖时把 4 个变量写到行根元素的 style 上;关闭行级覆盖时移除内联变量,回落 :root(MainWindowCustomizableNodeHelper.cs:20-37)。
  • 缩放:所有字号乘以界面缩放 --ci-scale(默认 1.0),但不得用浏览器 zoom,必须用布局缩放以保持像素对齐(见 3.8)。
  • 行高:源码未给出显式 LineHeight,建议正文 line-height: 1.25、说明文字 1.6【推断:依据 ClassHelper theme.css:60-63 的 line-height:1.6 与 ClassIsland 主界面 40px 行高/16px 字号的比值】。

3.3 圆角 Token

Token 默认值 用途 建议 CSS 变量 证据
RadiusX / RadiusY(岛圆角) 8(UI 范围 0–20,步进 0.5,提示文案:调至最大时左右边缘为半圆) 主界面行背景圆角;SizeDoubleToCornerRadiusConverter 以 ConverterParameter=1.0 直接取值(除以 1 即原值) --ci-radius-island: 8px Settings.cs:183-184、:1187-1207;AppearanceSettingsPage.axaml:75-87;FluentTheme/Styles.axaml:48,91,124,160-168,252;SizeDoubleToCornerRadiusConverter.cs:12-18
行级自定义圆角 0(关闭) 单行覆盖圆角 --ci-radius-island-local MainWindowLineSettings.cs:40-41
岛内容高度 40 所有行的容器高度(Fluent/Classic 同值) --ci-island-height: 40px FluentTheme/Styles.axaml:65;ClassicTheme/Styles.axaml:16;MainWindowLine.axaml:16
弹窗圆角 6 .popup-bg、编辑模式操作条 --ci-radius-popup: 6px StylesBase.axaml:295;MainWindow.axaml:488,599
卡片圆角 4 设置项卡片、主题卡、Shimmer、时间线装饰器、OOBE 提示 --ci-radius-card: 4px StylesBase.axaml:257-260;ThemesSettingsPage.axaml:152,161;Themes/Generic.xaml:214
控件圆角 4 按钮/输入框等 --ci-radius-control: 4px ClassHelper index.css:52;theme.css:10
抽屉圆角 8 8 0 0 底部竖抽屉 --ci-radius-drawer: 8px 8px 0 0 VerticalDrawer.axaml:9
胶囊圆角 999 徽标、胶囊按钮 --ci-radius-pill: 999px classic-island.css:308,454,541,694
圆形按钮 14(28×28) 主题卡安装按钮 --ci-radius-icon-btn: 14px ThemesSettingsPage.axaml:246-249
OOBE 大圆钮 24(48×48) 欢迎页「下一步」 --ci-radius-oobe-btn: 24px WelcomePage.axaml:61-65
岛内小元素 0.5 × --ci-island-font / 0.36 × --ci-island-font 岛内图标块 用 calc 派生 classic-island.css:388,403,476,502

实现硬约束:

  • 岛圆角必须四角同值(源码把 RadiusX 同时传给宽高两个方向:FluentTheme/Styles.axaml:31-34 传两次 RadiusX),不得实现「椭圆圆角」。
  • 圆角必须作用于「背景层」与「内容裁剪层」两层,且两层同值,否则内容会溢出圆角外(FluentTheme/Styles.axaml:48,91,124,160-168)。
  • 若引入 ClassHelper 的连续圆角(squircle),必须保持 SUPERELLIPSE_EXPONENT = 4.2、每角 CORNER_SEGMENTS = 14、展开卡半径 min(48*scale, w/2, h/2) 不变(ClassHelper/packages/desktop-client/src/island/squircle.ts:21,24,79-81)。默认实现必须用 CSS border-radius(与 Avalonia CornerRadius 的直角圆弧一致,保证与 ClassIsland 逐像素相同);连续圆角只能由主题包显式启用(P2),不得作为默认岛外形。

3.4 间距 Token

Token 值 用途 建议 CSS 变量 证据
行内容左右内边距 12 0 岛内容区 GridContentRoot.Margin --ci-island-pad-x: 12px FluentTheme/Styles.axaml:110;ClassicTheme/Styles.axaml:100
行内容最小宽度 20 MinWidth --ci-island-min-w: 20px FluentTheme/Styles.axaml:109
组件间距 6 0 每个 ComponentPresenter.Margin;token 值取相邻间距 12px(每个组件左右各 6px),供 calc(var(--ci-component-gap) / 2) 反推单侧 margin --ci-component-gap: 12px MainWindow.axaml:396,546;与 part 04 §4.4 对齐
行间距 5 多行堆叠 StackPanel.Spacing(用户设置) --ci-line-gap: 5px Settings.cs:219;MainWindow.axaml:344
设置页容器 margin 12 12 18 12;max-width 960;spacing 4 设置页正文 --ci-settings-pad / --ci-settings-max-w: 960px StylesBase.axaml:67-71;ThemeBase.axaml:37
组件设置容器 margin 4 0 4 12;max-width 960;spacing 4 子组件设置 --ci-component-settings-pad StylesBase.axaml:73-77
弹窗内边距 12 .popup-bg --ci-popup-pad: 12px StylesBase.axaml:294
主题卡网格 单元高 150;目标宽 328;单元 margin 2;卡内 padding 8 主题市场网格 --ci-theme-card-h: 150px、--ci-theme-card-w: 328px ThemesSettingsPage.axaml:139-142,172
编辑模式操作条 底距 64;padding 4;按钮间距 6 编辑态底部工具条 --ci-editbar-bottom: 64px MainWindow.axaml:595-604
闪屏 尺寸 400×100;padding 17;logo 高 24;行距 8;底部进度条高 4 启动闪屏 --ci-splash-w: 400px、--ci-splash-h: 100px SplashWindow.axaml:16-17,24,29,42,92-96
岛内水平内边距(ClassHelper 风格) 0.78 × 字号 岛胶囊左右内边距 calc 派生 classic-island.css:215
进度条高度 3px 岛内倒计时/提醒进度条统一高度 --ci-progress-height: 3px 与 part 04 §4.4 对齐;源码进度条未显式设高(FluentTheme/Styles.axaml:126-146),取 FluentAvalonia ProgressBar 默认值【推断】
岛内元素间距(ClassHelper) 0.62 × 字号 岛内行间距 calc 派生 classic-island.css:212

3.5 透明与模糊 Token

Token 值 用途 建议实现 证据
主窗口透明级别 Transparent 非编辑态的岛窗口必须逐像素透明 Electron transparent: true + backgroundColor: '#00000000' MainWindow.axaml:168-176
编辑模式透明级别 AcrylicBlur, Transparent(回退 Transparent) 编辑态窗口使用亚克力 只能用 CSS backdrop-filter 在自绘层模拟,不得用 setBackgroundMaterial(会破坏逐像素透明) MainWindow.axaml:177-181;ClassHelper island.ts:1726-1734 明确禁止
普通窗口模式 若禁用透明则 TransparencyLevelHint=None 窗口模式 transparent:false MainWindow.axaml:187-190
闪屏透明 AcrylicBlur, Transparent 启动闪屏 backdrop-filter: blur(...) + 半透明底 SplashWindow.axaml:21
岛背景不透明度 0.5(默认范围 0–1,步进 0.05,滑块带数值气泡) 背景层 opacity --ci-island-opacity(主名;兼容别名 --ci-island-bg-opacity) Settings.cs:64,1141;AppearanceSettingsPage.axaml:61-72;FluentTheme/Styles.axaml:44
行整体不透明度 1.0(行级设置,可被单行覆盖) 行级 opacity --ci-line-opacity MainWindowLineSettings.cs:42
鼠标移出淡化 0.05 悬停淡出模式 opacity:.05 MainWindowLine.axaml:73-75
提醒特效渲染缩放 1.0;屏高 ≥1400 → 0.75;≥2000 → 0.5(自动时) 强提醒涟漪窗口缩放 --ci-effect-scale Settings.cs:150;MainWindow.axaml.cs:377-392
遮罩动画层级不透明度 0 → 1(0.1s) 遮罩出现时附加投影 box-shadow transition FluentTheme/Styles.axaml:355-367

硬规则:

  • 不得给岛卡片加 backdrop-filter(会把整块矩形模糊掉);模糊只允许出现在「编辑模式窗口背景」与「闪屏底」这两个整窗层。
  • 岛必须是「透明窗口 + 自绘圆角卡片」,卡片外区域完全透明、可点击穿透(与 ClassHelper 一致,见 island.ts:250-257)。

3.6 层级(z-index)Token

层 数值 用途 建议 CSS 变量 证据
窗口层级-底层 WindowLayer=0 岛沉到桌面层(Bottommost),仅提醒/编辑时置顶 --ci-z-window:0 Settings.cs:52;MainWindow.axaml.cs:843-874
窗口层级-置顶 WindowLayer=1(默认) 岛始终置顶 --ci-z-window-top:1 同上;默认值 Settings.cs:52
岛内容层 0 正常内容 — —
遮罩/覆盖层 1 内容之上、背景之下 --ci-z-mask:1 FluentTheme/Styles.axaml:155-197
拖拽占位 1(且 opacity .5) 编辑态拖动项 --ci-z-drag:1 MainWindow.axaml:478-481
时间线装饰器 9999 分割线/行动装饰器必须压住时间块 --ci-z-timeline-adorn:9999 Themes/Generic.xaml:193,243
「无穷大」哨兵 33550336 必须永远最前的元素(OOBE 方块、时间线提示、拖拽阈值) --ci-z-sentinel:33550336 OobeIntroAnimationControl.axaml:45;TimeLineListControl.axaml:309;MainWindow.axaml:334
提醒特效窗口 独立 Topmost 窗口 全屏涟漪,高于一切 独立 BrowserWindow alwaysOnTop Views/TopmostEffectWindow.axaml.cs:120-128
教程聚光灯 独立置顶窗口 遮罩挖洞 独立窗口 Controls/Tutorial/TutorialSpotlightControl.axaml:41
抽屉遮罩 覆盖内容 横/竖抽屉 --ci-z-drawer:10【推断】 DrawerHost.axaml、VerticalDrawer.axaml
应用外壳引导层 10000 全屏引导 --ch-z-guide:10000 ClassHelper/packages/ui/src/theme.css:155
主题切换快照层 旧 1 / 新 2 View Transition 两帧 --ch-z-view-old/new ClassHelper/renderer/styles/index.css:948-954

实现必须满足:

  • 岛内容、遮罩、覆盖层在同一「行」内按上表叠放;多行之间不重叠(MainWindowLine 的 ClipToBounds=False,行本身不裁剪)。
  • 33550336 是源码里反复使用的「无穷大」哨兵(同时被用作拖拽阈值 HorizontalDragThreshold="33550336" 表达「禁止横向拖动」,见 MainWindow.axaml:334)。实现时可直接用 z-index: 2147483647 代替,但必须在常量注释里保留原值语义。
  • 编辑器/设置中心内任何弹窗、Tooltip 都必须高于主内容;不得用负 z-index 实现遮罩。

3.7 DPI 与缩放规则

规则 数值/算法 证据
界面缩放用户设置 Scale,默认 1.0,UI 范围 0.75–2.5,步进 0.05(滑块带数值气泡,格式 F2) Settings.cs:86,1153;Views/SettingPages/AppearanceSettingsPage.axaml:27-39
缩放实现 用 LayoutTransform(布局缩放)+ ScaleTransform,同时作用于所有行;不得用 transform: scale() 直接缩放窗口内容(会模糊) MainWindow.axaml:250-265
编码缩放补偿 GridRootLeft = Width/10*(Scale-1)、GridRootTop = Height/10*(Scale-1) MainWindow.axaml.cs:881-882,938-939
编辑模式缩放边界 MinZoom 0.5 / MaxZoom 3 / ZoomSpeed 1.2,EnableConstrains=True,显示缩放指示 MainWindow.axaml:199-209
窗口尺寸换算 逻辑宽 = 物理宽 / dpiX;逻辑高 = (物理高 − 1) / dpiY(−1 防被误判全屏) MainWindow.axaml.cs:969-971
窗口定位 使用物理像素 PixelPoint,不做 dpi 换算 MainWindow.axaml.cs:905-909
安全区(主题声明) 竖直安全区 = 当前启用主题 VerticalSafeAreaPx 的最大值;Fluent 主题为 20 Services/XamlThemeService.cs:111-135;App.Services.xaml.cs:314
教程运行时的安全区 主题安全区与 150 取大者 MainWindow.axaml.cs:898-902
通知特效窗口缩放 1 / dpiX MainWindow.axaml.cs:929
鼠标命中判定 屏幕坐标与 Bounds * dpi * Scale 比较 Controls/MainWindowLine.cs:625-636
字体缩放(关怀模式,应用外壳) --ch-font-scale: 1;关怀模式 1.15,只放大字号不动几何 ClassHelper/renderer/styles/index.css:103,973-979
岛内字号缩放(ClassHelper 模型) 所有岛内尺寸由 --island-font 用 calc() 派生 classic-island.css:2-7,57

实现要求:

  • 所有视觉常量必须定义在 96 DPI 的 CSS px 基准上,再由 --ci-scale 统一放大;不得为不同 DPI 写两套值。
  • 多显示器:每行/每窗口使用「所在显示器」的 DPI 与工作区;切换显示器必须重新计算窗口尺寸与位置(GetSelectedScreenSafe,MainWindow.axaml.cs:806-812)。
  • Scale 改变时必须重算窗口宽高与安全区,不能只改 CSS transform。

3.8 深浅色规则

规则 要求 证据
主题模式枚举 0=跟随系统、1=明亮、2=黑暗;默认 2(黑暗) Settings.cs:32,979-989;Services/ThemeService.cs:53-59
跟随系统实现 themeVariant=Default + preferSystemTheme=true;系统切换要实时响应 ThemeService.cs:53-63
资源覆盖机制 Light/Dark 两套 ThemeDictionary,同名 key 覆盖:RainyColorBrush、LuminousClass、6 个 AQI、CustomizedAccentBarBackground1Brush、TextFillColorInformativeSecondaryBrush App.axaml:35-38;Themes/LightTheme.axaml、Themes/DarkTheme.axaml
自定义前景色优先级 用户自定义前景色 > 主题前景色;关闭时回落主题 ControlColorHelper.cs:20-52
自定义背景色优先级 用户自定义背景色 > 主题背景;关闭时回落 SolidBackgroundFillColor* FluentTheme/Styles.axaml:51-56;ClassicTheme/Styles.axaml:216-218
深色下的阴影 阴影颜色不变(黑),只调不透明度;不得在深色下把投影改成白色 Fluent #48000000/#66000000/#33000000 与 VerticalDrawer.axaml:47 注释「Even in WinUI, shadow is always black regardless of light/dark mode」
ClassHelper 深色底色 应用壳亮 #F3F3F3/暗 #161719;卡片 rgba(255,255,255,.7) / rgba(32,33,36,.72) renderer/styles/index.css:27-48,114-137
主题切换过渡 窗口 Background 用 200ms 过渡;整页换肤用圆形扩散 View Transition(320×1.5=480ms) MainWindow.axaml:40-44;ClassHelper/renderer/composables/motion.ts:226-235

实现禁止项:

  • 禁止在组件里写死 #fff/#000;必须走 --ci-text-*。
  • 禁止只做深色、不做亮色(两套必须同步验收)。
  • 禁止在深色模式使用纯黑 #000000 作为岛背景(默认自定义背景色是黑,但必须靠 --ci-island-bg-opacity 降为 50% 并叠加主题描边,否则与桌面融为一体)。

3.9 ClassHelper 侧落点(实现映射表)

内容 落点文件 要求
应用外壳 token packages/ui/src/tokens.css 保留 --ch-ui-*,新增 --ci-* 只读引用
应用外壳主题 packages/ui/src/theme.css 把 --ci-accent 注入 --el-color-primary 系列,color-mix 派生
桌面端全局样式 packages/desktop-client/src/renderer/styles/index.css 在 :root 与 html.dark 中定义 --ci-* 的亮/暗值
岛渲染进程样式 src/island/classic-island.css、custom-island.css 定义 --island-* 与 --wn-*(classic-island.css:60-82)
动效常量 packages/shared/src/motion.ts + 桌面端/管理端 composables/motion.ts 新增 token 只加在 shared;不得两端各抄一份(motion.ts:14-16 明确约束)
设计常量的 TS 形态 packages/shared/src/constants.ts 输出 CLASSISLAND_TOKENS 常量对象,供窗口几何计算复用

3.10 落地顺序与常见错误

实现顺序必须严格如下,不得跳步:

  1. 先在 packages/shared/src/constants.ts 建立常量对象(颜色、字号、圆角、间距、层级、DPI 基准),字段名与本节 token 名一一对应,导出为只读(as const)。
  2. 在 packages/ui/src/tokens.css 的 :root 写入亮色值,在 html.dark 写入深色值;两处必须保持键集合完全一致,缺一个键都算不合格。
  3. 在 packages/desktop-client/src/renderer/styles/index.css 引入 --ci-* 的桌面端覆盖(通知特效窗口、闪屏窗口等独立渲染入口要单独引入同一份 token)。
  4. 在 src/island/classic-island.css 与 custom-island.css 里把 --island-* 映射到 --ci-*,禁止在岛样式里重新定义颜色字面量。
  5. 最后才写组件;组件只允许消费 token。

常见错误(必须在实现时主动规避):

  • 把「行级字号覆盖」写成给整个岛设置字号:这样改一行会连带改所有行,与源码作用域不符(源码把资源写在行控件自身,见 MainWindowCustomizableNodeHelper.cs:20-26)。
  • 把 5 级前景色实现成「同一颜色的不同 opacity 属性」:opacity 会连带影响子元素与阴影,源码用的是颜色 alpha 通道(ControlColorHelper.cs:24-33),必须写进颜色值本身。
  • 用 filter: drop-shadow() 代替 box-shadow 实现岛投影:filter 会新建渲染表面,开合时出现闪帧(ClassHelper classic-island.css:114-120 已记录该问题)。
  • 在深色主题下把阴影改成 rgba(0,0,0,.6) 以外的颜色,或直接删除阴影:源码在两种主题下都用黑色阴影,只改透明度。
  • 忘记「安全区」:Fluent 主题声明了 20px 竖直安全区,岛贴边时会空出这段距离(App.Services.xaml.cs:314、XamlThemeService.cs:111-135),漏实现会导致岛压住任务栏或屏幕边缘。
  • 忘记 Scale 变化时重算窗口:只改 CSS 会导致点击热区与可见区域错位(源码在 MainWindow.axaml.cs:876-931 每次都重算 Bounds 与 ActualClientBound)。
  • 把「编辑模式操作条」的层级设在岛内容之下:它必须浮在行内容之上,且底距 64px(MainWindow.axaml:595)。

3.11 验收标准

实现者必须逐条自查,全部通过才算完成本节:

  1. 颜色:全仓库(渲染进程 + 主进程 + shared)搜索,除 token 定义文件外不得出现 # 开头的颜色字面量;grep -r "#[0-9a-fA-F]\{6\}" 的命中只允许出现在 token 文件与主题包预览样式中。
  2. 强调色:修改 --ci-accent 一处,岛背景描边、提醒遮罩、进度条、按钮强调态、天气图标强调色全部同步变化,无残留默认蓝。
  3. 5 级前景:开启自定义前景色后,5 个 alpha 分别为 100%/85.5%/77.3%/52.9%/36.5%;关闭后 5 个变量被移除并回落。
  4. 字号:把 4 档字号分别改成 10/20/30/40,岛内所有文字按对应档位变化且不溢出 40px 行高外的容器;行级覆盖可只影响单行。
  5. 圆角:把 --ci-radius-island 从 8 改到 20,背景层与裁剪层同时变化,内容不溢出四角;其他卡片圆角不受影响。
  6. 间距:改 --ci-island-pad-x 与 --ci-component-gap,内容宽度变化后背景宽度动画仍正常(见 part 14)。
  7. 透明:非编辑态岛卡片外区域 100% 透明(用取色器/截图验证 alpha=0);编辑态出现亚克力底;全程不允许整窗材质导致卡片外出现色块。
  8. 层级:编辑态拖动项浮在其他项之上;提醒遮罩覆盖内容但低于右上角操作按钮(若有);33550336 哨兵元素永远最前。
  9. DPI:在 100%/125%/150%/200% 四档缩放下,岛高度逻辑值恒为 40、圆角逻辑值恒为 8、文字不模糊;跨显示器拖动后重新计算,无半个像素的缝。
  10. 深浅色:Light/Dark 各截图对照,两类主题的 AQI 色、雨天色、说明文字 alpha 与上表一致;深色下阴影仍为黑色。
  11. CSS 变量唯一来源:--ci-* 只在两处定义(:root 与 html.dark),组件内只引用不重定义(行级作用域覆盖除外)。
  12. 字符集:所有 token 名使用 kebab-case,前缀统一为 ci-;禁止出现 --ci-primaryColor 这类混合命名。

4. 主界面(岛):窗口、行、背景、进度、遮罩、编辑模式

本节是「主界面(MainWindow,俗称岛)」的完整实现规格。实现技术栈固定为 ClassHelper 的 Electron + Vue 3 + TypeScript:主进程用 BrowserWindow 复刻 Avalonia Window 的窗口级行为,渲染进程用 Vue 单文件组件 + CSS 复刻 MainWindow.axaml 的控件树与全部像素参数。所有 Avalonia 的"逻辑像素(DIP,96 DPI 基准)"一律直接对应 CSS px;Settings.Scale 用 transform: scale() 实现。

4.1 术语与实现映射总则

必须先建立下面这张映射表,后文所有规则都按它落地:

ClassIsland 概念 源码实体 本实现替代物
主界面窗口 MainWindow : Window(repos/ClassIsland/ClassIsland/MainWindow.axaml) 独立 island.html 渲染入口 + 主进程 BrowserWindow(透明、无边框、置顶、跳过任务栏)
主界面行 MainWindowLine(repos/ClassIsland/ClassIsland/Controls/MainWindowLine.cs) IslandLine.vue(Vue 组件),一行一个
组件宿主 ComponentPresenter(repos/ClassIsland/ClassIsland.Core/Controls/ComponentPresenter.axaml) ComponentHost.vue(统一承载宽度约束/透明度/隐藏规则/裁剪)
组件 DateComponent / ClockComponent / … components/DateNode.vue 等,见第 5 章
主题样式 XamlThemes/FluentTheme/Styles.axaml、ClassicTheme/Styles.axaml island.css + CSS 变量主题包(--island-*)
编辑模式视图 Controls/EditMode/EditModeView.axaml IslandEditMode.vue(抽屉 + 拖动 + 选择)
提醒宿主 行模板内的 Overlay / Mask 层 行内 .line-mask / .line-overlay

强制约定:

  1. 不得把主界面做成"一个自适应大小的 div 窗口",也不得照搬 ClassIsland 的"窗口高度随内容自适应"。采用 4.3.1 的固定包围盒:窗口宽度 = 所选显示器整屏宽(DIP),高度 = 预设最大内容高 + 上下安全余量;可见岛在窗口内形变,其高度 = 行高之和 + 行间距之和。窗口大于岛的透明区域不会遮挡桌面(Windows 侧整窗被设为鼠标穿透,见 4.2)。
  2. 不得使用 win.setBackgroundMaterial()(亚克力/mica)。ClassIsland 的透明是逐像素 alpha + WS_EX_LAYERED,用系统材质会破坏像素级透明与圆角(repos/ClassHelper-master/packages/desktop-client/src/main/island.ts:523-524 的既有结论)。
  3. 不得给主界面行加 CSS box-shadow 之外的重滤镜;Fluent 主题只有 BoxShadow,Classic 主题只有提醒遮罩期才挂 DropShadowEffect。
  4. 所有尺寸、时长、缓动必须按本文给的确切值写死为常量或 CSS 变量,不得凭观感微调。

4.2 窗口(BrowserWindow)逐项规格

按下面顺序创建窗口(默认非编辑模式):

const win = new BrowserWindow({
  ...geometry,                 // 见 4.3,x / y / width / height
  show: false,
  frame: false,                // Avalonia: SystemDecorations=None(MainWindow.axaml:171)
  transparent: true,           // Background=Transparent + TransparencyLevelHint=Transparent(:169-170)
  resizable: false,            // CanResize=False(:172)
  movable: false,
  minimizable: false,          // CanMinimize=False(:174)
  maximizable: false,          // CanMaximize=False(:173)
  fullscreenable: false,
  skipTaskbar: true,           // 默认不进任务栏;例外见下
  hasShadow: false,            // 阴影由岛自己的 CSS box-shadow 画
  focusable: false,            // 普通态不抢焦点(配合 showInactive)
  alwaysOnTop: true,           // WindowLayer 默认 1=置顶(Settings.cs:52)
  type: 'toolbar',             // 对应 WS_EX_TOOLWINDOW(MainWindow.axaml.cs:838)
  backgroundColor: '#00000000',
  webPreferences: {
    preload: /* island preload 白名单 */,
    contextIsolation: true,
    nodeIntegration: false,
    sandbox: true,
    backgroundThrottling: false,
  },
});

逐条规则:

  1. 透明与无边框:transparent: true + frame: false + hasShadow: false + backgroundColor: '#00000000'。HTML 根节点(html, body, #app)必须 margin:0;padding:0;width:100%;height:100%;background:transparent;overflow:hidden;-webkit-user-select:none;user-select:none(对齐 repos/ClassHelper-master/packages/desktop-client/src/island/classic-island.css:8-19)。
  2. 鼠标穿透(核心):ClassIsland 在 Windows 上给主窗口加 WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_NOACTIVATE 并调用 SetLayeredWindowAttributes(repos/ClassIsland/platforms/ClassIsland.Platforms.Windows/Services/WindowPlatformService.cs:130-150),因此非编辑模式下整个窗口对鼠标完全透明,点击直接落到桌面。由此得:
    • 非编辑模式:调用 win.setIgnoreMouseEvents(true, { forward: true }),且不得注册任何需要点击的交互(行、组件都不可点)。
    • 编辑模式:调用 win.setIgnoreMouseEvents(false) 恢复命中,并 win.focus()。
    • 不得实现"点岛展开/收起"这类交互;ClassIsland 的主界面是纯展示层。旧设置项 IsMouseClickingEnabled 已被废弃并被强制关闭(repos/ClassIsland/ClassIsland/MainWindow.axaml.cs:733-737,默认值 false 见 Models/Settings.cs:53)。
  3. 不抢焦点:显示时用 win.showInactive();非编辑态 focusable: false。若为了接收触摸事件必须保持可激活,则只允许在编辑模式内切换 focusable,且退出编辑模式后立即恢复。
  4. 置顶/置底:Settings.WindowLayer 取值 0=置底、1=置顶,默认 1(Models/Settings.cs:52, :1961-1978)。置顶时 win.setAlwaysOnTop(true, 'screen-saver') 并 win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });置底时 win.setAlwaysOnTop(false) 并用 setAlwaysOnTop(false) + 必要时 win.moveBottom()(Windows 上等价于 HWND_BOTTOM,见 WindowPlatformService.cs:151-159)。置顶优先级还要被三个条件覆盖(MainWindow.axaml.cs:791-803, 843-874):
    • 处于自由窗口模式(编辑模式的 windowed)或编辑模式时为普通窗口;
    • IsNotificationWindowExplicitShowed=true(有提醒/编辑)时无论 WindowLayer 都置顶;
    • WindowTopmostRecheckMode 决定重查时机:0 由窗口 Z 序变化事件驱动、1 前台窗口变化时、2 主计时器每 tick、3 1ms 高频计时器(MainWindow.axaml.cs:420-436, 461-466, 687-704,默认 WindowTopmostRecheckMode = 0 见 Models/Settings.cs:1860)。本实现的复核策略见 4.2.1,禁止照搬 3 的 1ms 轮询。
  5. 任务栏显示:ShowInTaskbar 是 IsWindowMode || IsScreenRecordingModeEnabled 的逻辑或(MainWindow.axaml:34-39)。即只有"编辑模式 + 自由窗口"或"屏幕录制模式"才进任务栏。实现用 win.setSkipTaskbar(!(isWindowedEditMode || settings.isScreenRecordingModeEnabled))。
    • P2 / 能力说明:IsWindowMode(= EditModeIsWindowMode && IsEditMode,ViewModels/MainViewModel.cs:363)本任务不实现具体 UI 入口,只保留设置字段与"编辑模式可切成自由窗口"的能力说明。依据:ClassIsland 2.1 中该设置字段存在,但编辑页(Controls/EditMode/EditModeView.axaml)没有可见的切换按钮【推断:入口可能在教程步骤或后续版本】,实现者不得臆造按钮。
  6. 防截屏:IsWindowCaptureBlockingEnabled=true 时调用 win.setContentProtection(true)(对应 WDA_EXCLUDEFROMCAPTURE,WindowPlatformService.cs:169-172, MainWindow.axaml.cs:840)。
  7. 屏幕录制模式:IsScreenRecordingModeEnabled=true 时:不进 ToolWindow(任务栏可见)、不被置底。默认 false(Models/Settings.cs:1861)。
  8. 窗口关闭拦截:主界面窗口不得被用户关闭。ClassIsland 在 Closing 事件里 e.Cancel = true,只有应用主动退出才放行(MainWindow.axaml.cs:577-590)。实现:win.on('close', (e) => { if (!appQuitting) e.preventDefault(); })。
  9. 禁止的窗口能力:不得 setAlwaysOnTop 到 'floating' 以下级别;不得开启 thickFrame;不得给窗口加系统菜单;不得设置 roundedCorners(圆角由 CSS 画)。
  10. 单实例守卫:见第 12 章(本部分不重复),但主界面窗口必须在单实例锁之后创建。

4.2.1 置顶复核与性能底线

必须事件驱动 + 兜底轮询,禁止任何 1ms/10ms 级高频轮询。

  • 事件源(全部接上):win.on('focus'/'blur')、screen 的 display-added/removed/metrics-changed、编辑模式进出、setVisibleOnAllWorkspaces 变化、提醒开始/结束(对应 TopmostLock 的 Acquire/Release,MainWindow.axaml.cs:1056-1092)。
  • 兜底轮询:仅在"当前是置顶态且窗口可见"时启用,间隔 ≥1000ms,每次只做一次轻量 setAlwaysOnTop(true, 'screen-saver') 复核(对抗系统抢 Z 序)。窗口隐藏/置底时停止轮询。
  • 禁止实现 WindowTopmostRecheckMode === 3 的 1ms 高频计时器(MainWindow.axaml.cs:136-139):那是原生 SetWindowPos 成本极低的平台红利,Electron 侧照搬会持续占用 CPU 并让渲染进程掉帧。
  • 同类频率约束(本部分涉及的所有动画/刷新):
    • 时钟/日期/倒计时/课程表:1s 节拍(整秒对齐);
    • 提醒进度条:100ms(ReduceProgressAccuracy=true 时 250ms 即可);
    • 行淡出/透明度过渡:由 CSS transition 驱动,不得用 JS 每帧写样式;
    • 鼠标位置命中判定:只在 pointermove(编辑模式)或 200ms 兜底轮询(非编辑模式不需要,因为整窗穿透)。

4.3 几何:包围盒、可见内容盒与编辑画布

所有坐标计算以「所选显示器」为基准,显示器由 Settings.WindowDockingMonitorIndex 选定;越界或负数时回退主显示器(MainWindow.axaml.cs:806-812,默认 0 见 Models/Settings.cs:51)。设:

  • screen:所选显示器;screen.bounds 为整屏物理像素,screen.workArea 为工作区物理像素;
  • dpiX = screen.scaleFactor、dpiY = screen.scaleFactor(Avalonia 取 screen.Scaling,MainWindow.axaml.cs:1019-1037);
  • ox = Settings.WindowDockingOffsetX、oy = Settings.WindowDockingOffsetY(单位:物理像素,默认 0、0,Models/Settings.cs:49-50);
  • ignoreArea = Settings.IsIgnoreWorkAreaEnabled(默认 false,Models/Settings.cs:1835);
  • docking = Settings.WindowDockingLocation(默认 1,Models/Settings.cs:48),取值为:
###############
# 0    1    2 #
#             #
# 3    4    5 #
###############

即 0/1/2 = 上(左/中/右)、3/4/5 = 下(左/中/右)(Models/Settings.cs:1810-1823)。

4.3.1 实现策略裁决:固定包围盒 + 窗内形变

ClassIsland 的原生模型是"窗口高度随内容自适应"(SizeToContent = Height,MainWindow.axaml.cs:781;窗口高 = 内容高 + 安全区,:916)。本实现不得照搬这一条:Electron 的逐像素透明窗口在频繁 setBounds 时会触发 DWM 重排与透明表面重建,产生整块闪烁/掉帧,而行增删、提醒展开、时钟秒数出现都会触发高度变化。因此采用 ClassHelper 已验证的「固定包围盒 + 窗内形变」方案(repos/ClassHelper-master/packages/desktop-client/src/main/island.ts:480-512,注释 :78-90):

  1. 窗口创建时一次算好包围盒:
    • winW = 目标屏幕宽(DIP) = screen.bounds.width / dpiX;
    • winH = contentMaxHeight + 2 * safePadding,其中 contentMaxHeight = 预设最大内容高(默认按"当前可见行数 × (40 + 行间距 5) + 上下安全区"计算,并留出一行余量),safePadding ≥ 20(阴影 + 光晕余量,对齐 ISLAND_SHADOW_PAD = 10 的双侧口径)。
  2. 可见岛在窗口内形变:窗口不动,岛用 CSS height 过渡(或第 14 章的 spring 形变)在包围盒内伸展/收缩;内容层按 4.5 的行高与间距排布。
  3. 只有下列情形才 setBounds:显示器/DPI 变化、行数超出包围盒上限、进入/退出编辑模式、窗口偏移/缩放/停靠位置设置变化。这类变更必须节流 ≥100ms(同一窗口内多次合并为一次),且不得与弹簧/高度动画同帧竞争。
  4. 视觉结果必须与 ClassIsland 一致:用户看到的可见岛高度 = 行高之和 + 行间距之和(行高恒 40,间距 MainWindowLineVerticalMargin),可见岛宽度 = 内容自然宽 + 两侧各 12px 内边距。窗口比岛大只是透明余量,用户不可见。
  5. 禁止:把窗口高度做成"刚好等于内容高"再逐帧改;用 requestAnimationFrame 每帧 setBounds;在行内容变化(秒数、课程名长度、提醒)时改窗口尺寸。以上都会导致 1px 级抖动。

与 ClassIsland 的差异及理由(必须写进实现注释):差异 = 窗口高度固定为包围盒 vs 随内容自适应;理由 = Electron 透明窗口 resize 抖动/闪烁,且 ClassHelper 的既有实现已证明"固定包围盒 + 窗内画布形变"在开合过程中零抖动(见该文件 :64-70 的设计说明)。视觉结果不做任何妥协:可见岛的行高、间距、圆角、阴影与 4.5 完全一致。

4.3.2 可见内容盒(视觉基准公式)

下面的 ClassIsland 原生公式只用于计算"可见岛"的内容尺寸、安全区裁剪与停靠对齐,不直接作为 BrowserWindow.setBounds 的 height 参数(setBounds 规则见 4.3.1):

  1. contentW、contentH:由渲染进程上报的岛内容尺寸(CSS px)。内容盒 = 所有行高度之和 + 行间距之和(见 4.5)。
  2. safe = 主题竖直安全区:Fluent 主题 20(repos/ClassIsland/ClassIsland/App.Services.xaml.cs:314),Classic 主题 0(ThemeManifest._verticalSafeAreaPx 默认 0,repos/ClassIsland/ClassIsland.Core/Models/XamlTheme/ThemeManifest.cs:20)。若正在播放教程且教程吸附在本窗口,则 safe = max(safe, 150)(MainWindow.axaml.cs:899-902)。
  3. dockingTop = docking ∈ {0,1,2}。
  4. safeT = Math.max(dockingTop ? Math.min(safe, oy) : safe, 0) * scale。
  5. safeB = Math.max(dockingTop ? safe : Math.min(safe, -oy), 0) * scale。
  6. 岛锚点 X = screen.workArea.x + ox;岛锚点 Y = dockingTop ? top + oy - safeT : bottom - contentH + oy + safeB,其中 top = ignoreArea ? screen.bounds.y : screen.workArea.y,bottom = ignoreArea ? screen.bounds.bottom : screen.workArea.bottom。
  7. layoutW = screen.bounds.width / dpiX(布局容器宽 = 整屏宽,用于左/中/右对齐计算,MainWindow.axaml.cs:915)。
  8. layoutH = contentH + safeT + safeB(岛的可布局高度;这只是内容画布高,不是窗口 height)。
  9. 岛在画布内的偏移:translateY = safeT、translateX = 0;水平对齐:docking ∈ {0,3} → 左、{1,4} → 居中、{2,5} → 右(对应 align-items: flex-start / center / flex-end,与 WidthDoubleToRectConverter 的 X 偏移逻辑一致,repos/ClassIsland/ClassIsland.Core/Converters/WidthDoubleToRectConverter.cs:66-77)。
  10. 窗口自身的包围盒位置必须让"岛锚点"落在公式 6 的位置:winX = 岛锚点X、winY = 岛锚点Y - safePadding(上停靠)/ 岛锚点Y - contentMaxHeight - safePadding(下停靠,包围盒底部贴锚点)。

4.3.3 编辑模式窗口

进入编辑模式时窗口必须扩展为全屏编辑画布(这一步允许 setBounds,因为是模式切换、不是内容变化):

  1. winX = screen.bounds.x、winY = screen.bounds.y。
  2. winW = screen.bounds.width / dpiX。
  3. winH = (screen.bounds.height - 1) / dpiY——必须减 1px,防止 Windows 把窗口判定成全屏并触发全屏优化(MainWindow.axaml.cs:970)。
  4. 岛内容在窗口内四边各留 60px 安全边距:padding = 60px(来源 WindowedEditModeScreenMargin = 60,MainWindow.axaml.cs:134),即编辑画布的可用区为 winW - 120 × winH - 120,岛在其中垂直居中、水平按停靠位置对齐。
  5. 内容偏移:translateX = ox / dpiX、translateY = oy / dpiY(编辑模式偏移是逻辑像素,MainWindow.axaml.cs:973-974);clientRect = ignoreArea ? screen.bounds : screen.workArea。
  6. 鼠标事件必须开启:win.setIgnoreMouseEvents(false)(见 4.2 第 2 条)。
  7. 编辑模式 + 自由窗口(windowed):窗口改为取 screen.workArea 四边各内缩 60px(winX = workArea.x + 60、winY = workArea.y + 60、winW = (workArea.width - 120) / dpiX、winH = (workArea.height - 120) / dpiY),切回系统装饰(SystemDecorations=Full、可缩放/最大化/最小化,MainWindow.axaml:182-191),标题 编辑主界面 | ClassIsland(MainWindow.axaml:180)。
  8. 退出编辑模式:恢复 4.3.1 的包围盒(位置 + 尺寸),win.setIgnoreMouseEvents(true, { forward: true }),恢复 skipTaskbar。

4.3.4 触发重算的时机与节流

必须接上这些时机,但全部走"标记脏 + ≥100ms 节流"的通道,不得每次直接 setBounds:

  • 包围盒(4.3.1):显示器/DPI/显示器插拔变化、行数超出 contentMaxHeight、编辑模式进出、偏移/缩放/停靠设置变化;
  • 岛内容布局(4.3.2,纯 CSS,不改窗口):主计时器每 tick(LessonsServiceOnPreMainTimerTicked,MainWindow.axaml.cs:475)、内容容器尺寸变化(StackPanelRootContainer_OnSizeChanged,MainWindow.axaml.cs:786-789)、任何 Settings 属性变化(MainWindow.axaml.cs:488-501, 730-782)。

节流与稳定:用 requestAnimationFrame 合并同一帧内的多次 setBounds;同一窗口尺寸在 100ms 内多次变化时只保留最后一次;高度目标值四舍五入到整数 CSS px。禁止在主计时器 tick 里直接改窗口尺寸。

恢复与兜底:WindowDockingMonitorIndex 超出显示器数量时回退主屏(MainWindow.axaml.cs:806-812);contentH 为 0(无行/全部隐藏)时岛整体 opacity:0(窗口保留包围盒、不改尺寸),避免窗口在 0 高附近抖动。

4.4 DOM 层级与 CSS 变量

必须严格复刻下面这棵树(左侧为 MainWindow.axaml 的控件,右侧为实现用的标签/类名):

Window                                  → html > body > #app
└ Grid#WindowRoot                       → .window-root(透明,pointer-events:none)
  └ ZoomBorder                          → .zoom-layer(缩放/平移宿主,overflow:visible)
    └ Canvas#LayoutContainerGrid        → .layout-container(position:relative; width:100%;height:100%)
      └ Panel#WorkingRoot               → .working-root(position:absolute; left:0; top:safeT; width:窗口宽; height:contentH)
        └ StackPanel#StackPanelRootContainer → .lines-root(display:flex; flex-direction:column; align-items:center|flex-start|flex-end)
          └ LayoutTransformControl      → .lines-transform(transform: translate(ox,oy) scale(scale))
            └ Grid#GridRoot             → .grid-root(每行背景/遮罩的公共祖先)
              └ Border#ResourceLoaderBorder → .resource-loader(font-size:16px,字体族/字重继承)
                └ Grid                  → .lines-host
                  ├ StackPanel          → .lines-stack(display:flex; flex-direction:column; row-gap:5px)
                  │ └ ItemsControl#RootMainWindowLinesItemsControl → .lines-list
                  │   └ (每行) Grid ColumnDefinitions="Auto,*,Auto" → .line-row(display:grid; grid-template-columns:auto 1fr auto)
                  └ ItemsControl#ContainerComponentsEditHost → .container-edit-host(仅编辑模式,position:absolute)

再加两个编辑模式专用兄弟节点:.operate-area(底部操作栏)、.edit-mode-view(抽屉宿主)。

CSS 根变量必须在 #app 或 .grid-root 上集中声明(对齐 ClassHelper 的 --island-* 命名习惯,repos/ClassHelper-master/packages/desktop-client/src/island/IslandApp.vue:522-528):

:root {
  /* 尺寸 token(ClassIsland 事实来源) */
  --row-height: 40px;          /* IslandContainerHeight,MainWindowLine.axaml:16 / FluentTheme/Styles.axaml:65 / ClassicTheme/Styles.axaml:16 */
  --row-padding-x: 12px;       /* GridContentRoot Margin="12 0",FluentTheme/Styles.axaml:110 */
  --row-min-width: 20px;       /* GridContentRoot MinWidth="20",同上 */
  --row-gap: 5px;              /* MainWindowLineVerticalMargin 默认 5,Models/Settings.cs:219 */
  --component-margin-x: 6px;   /* ComponentPresenter Margin="6 0",MainWindow.axaml:396 */
  --island-radius: 8px;        /* RadiusX/RadiusY 默认 8,Models/Settings.cs:183-184 */
  --island-opacity: 0.5;       /* Opacity 默认 0.5,Models/Settings.cs:64 */
  --island-scale: 1;           /* Scale 默认 1.0,Models/Settings.cs:86 */
  --line-border-width: 1px;    /* line-background BorderThickness=1,FluentTheme/Styles.axaml:47 */
  --line-faded-opacity: 0.05;  /* MainWindowLine.axaml:74 */
  --safe-top: 20px;            /* Fluent 主题安全区,App.Services.xaml.cs:314 */
  --safe-bottom: 20px;
  /* 字号 token */
  --font-secondary: 14px;      /* Models/Settings.cs:197 */
  --font-body: 16px;           /* Models/Settings.cs:198 */
  --font-emphasized: 18px;     /* Models/Settings.cs:199 */
  --font-large: 20px;          /* Models/Settings.cs:200 */
  /* 字体 */
  --font-family: 'HarmonyOS Sans SC', 'Segoe UI Variable Display', 'Segoe UI', 'Microsoft YaHei', sans-serif;
  --font-weight: 500;          /* MainWindowFontWeight2 默认 FontWeight.Medium,Models/Settings.cs:101 */
}

与设计 token 的命名对齐(强制):本文为便于阅读使用语义化局部名(--row-height、--font-body 等),落地时必须改用 part 3 定义的 --ci-* 规范 token,局部名仅作为别名。对应关系:

本文局部名 规范 token(part 3)
--row-height --ci-island-height(=40px)
--row-padding-x / --row-min-width --ci-island-pad-x(=12px)/ --ci-island-min-w(=20px)
--row-gap --ci-line-gap(=5px)
--component-margin-x calc(var(--ci-component-gap) / 2)(=6px)
--island-radius --ci-radius-island(=8px)
--island-opacity(背景层不透明度,0.5) --ci-island-opacity(=0.5,作用于行背景)
行整体不透明度(MainWindowLineSettings.Opacity,默认 1.0) --ci-line-opacity(=1.0,作用于整行 opacity;与上面的背景不透明度是两个独立字段,不得合并)
--island-scale --ci-scale(=1)
--font-secondary/body/emphasized/large --ci-fs-secondary/body/emphasized/large(14/16/18/20)
--accent --ci-accent;强调填充用 --ci-accent-fill
行背景(Fluent) --ci-bg-secondary;描边 --ci-stroke-island + --ci-stroke-island-w
行阴影 --ci-shadow-island;遮罩阴影 --ci-shadow-mask;Classic 遮罩阴影 --ci-shadow-classic-mask
编辑模式遮罩层 --ci-smoke
编辑操作栏底距 --ci-editbar-bottom(=64px)
抽屉宽度 / 设置区最大宽 --ci-settings-max-w(=960px)
抽屉圆角 / 阴影 --ci-radius-drawer / --ci-shadow-drawer
行内强调底 --ci-accent-fill-tertiary
文本层级色 --ci-text-1/2/3(对应 primary/secondary/tertiary)
进度条高度 --ci-progress-height(固定 3px,所有进度条共用)
层级 --ci-z-mask / --ci-z-drawer / --ci-z-window

字号/字体注入链:--font-* 由设置写入 .resource-loader(对应 Border#ResourceLoaderBorder 上 TextElement.FontSize = MainWindowBodyFontSize,MainWindow.axaml:311-313 与 MainWindow.axaml.cs:766-778),所有子组件用 font-size: var(--font-body) 继承;字体族与字重写在 .zoom-layer(对应 ZoomBorder 上 TextElement.FontFamily/FontWeight 绑定,MainWindow.axaml:211-212)。行级/组件级"资源覆盖"(IsResourceOverridingEnabled)时用内联 CSS 变量覆盖同一批 token。

4.5 主界面行(MainWindowLine)

4.5.1 三种模板

每行按条件选模板,不得混用:

模板 触发条件 差异
Full(完整) IsNotificationEnabled = true 含背景、遮罩(Mask)、浮层(Overlay)、进度条
Min(精简) IsNotificationEnabled = false(FluentTheme/Styles.axaml:275-278、ClassicTheme/Styles.axaml:219-222) 只有背景 + 内容,不含遮罩/浮层/进度条
分体(Separated) MainWindowLineSettings.IslandSeparationMode 覆盖为"启用",或全局 Settings.IsIslandSeperated=true(MainWindowLine.cs:478-492) 不再画整行背景,改为每个组件各自一个背景胶囊(见 4.5.6)

IslandSeparationMode 的取值语义固定为 0=继承(用全局设置)、1=禁用、2=启用(repos/ClassIsland/ClassIsland.Core/Models/Components/MainWindowLineSettings.cs:47-50,UI 文案"继承/禁用/启用"见 EditModeView.axaml:319-329)。

4.5.2 尺寸与间距(精确值)

  • 行高固定 40px:IslandContainerHeight = 40(三处定义:Controls/MainWindowLine.axaml:16、XamlThemes/FluentTheme/Styles.axaml:65、XamlThemes/ClassicTheme/Styles.axaml:16),并同时用作 Border#BackgroundBorder.Height、Grid#PART_GridWrapper.Height、Grid#GridContentRoot.Height、遮罩高度、ListBoxItem.MaxHeight。
  • 内容盒左右内边距 12px:Grid#GridContentRoot Margin="12 0"(FluentTheme/Styles.axaml:110、ClassicTheme/Styles.axaml:100)。
  • 内容盒最小宽 20px:同上 MinWidth="20"。
  • 行与行之间垂直间距 5px:ItemsPanel StackPanel Spacing="{Binding Settings.MainWindowLineVerticalMargin}"(MainWindow.axaml:344),默认 5(Models/Settings.cs:219,设置面板范围 0–150、步进 5,EditModeView.axaml:114-120)。
  • 行内相邻组件水平间距 12px:每个组件宿主 Margin="6 0"(MainWindow.axaml:396),左右各 6 相加得 12;容器组件内部再用 Margin="-6 0" 抵消一次(StackComponent.axaml:14、GroupComponent.axaml:15、RollingComponent.axaml:16),因此嵌套容器不会累积边距。
  • 行背景宽度 = 内容实际宽度,不是窗口宽度:BackgroundWidth 由 PART_GridWrapper.SizeChanged 回写(MainWindowLine.cs:614-617),背景 Border 与遮罩都绑 Width="{Binding BackgroundWidth}"(FluentTheme/Styles.axaml:81, 94)。

4.5.3 背景(Fluent 主题,默认主题)

逐条写死(XamlThemes/FluentTheme/Styles.axaml:40-56):

.line-bg {
  position: absolute;
  height: var(--row-height);
  width: var(--line-width);        /* = BackgroundWidth,由内容决定 */
  background: var(--fill-secondary); /* FluentAvalonia: SolidBackgroundFillColorSecondaryBrush */
  border: var(--line-border-width) solid var(--control-elevation-border); /* ControlElevationBorderBrush */
  border-radius: 8px;             /* = Settings.RadiusX,见下方说明 */
  box-shadow: 0 4px 8px 2px #48000000;  /* "0 4 8 2 #48000000" */
  opacity: var(--island-opacity);  /* = Settings.Opacity,默认 0.5 */
}
  • 圆角 = Settings.RadiusX 直接作为四角统一半径(转换器 SizeDoubleToCornerRadiusConverter 以参数 1.0 相除,repos/ClassIsland/ClassIsland.Core/Converters/SizeDoubleToCornerRadiusConverter.cs:8-17,调用处 FluentTheme/Styles.axaml:48)。默认 8,设置范围 0–20、步进 0.5(Models/Settings.cs:183, EditModeView.axaml:98-104)。当半径 ≥ 行高一半(20)时左右边缘为半圆。
  • 自定义背景色开启时(IsCustomBackgroundColorEnabled)背景色替换为 Settings.BackgroundColor(默认黑色,Models/Settings.cs:34),仍保留边框、阴影、透明度(FluentTheme/Styles.axaml:51-56)。
  • 背景不透明度只作用于背景层:Opacity 绑在 Border.line-background 上(FluentTheme/Styles.axaml:44),文字不跟着透明。这是硬性要求:文字必须始终 100% 不透明。

4.5.4 行裁剪(圆角裁切矩形)

行内容用几何裁剪切出圆角矩形,X 偏移随停靠位置变化(repos/ClassIsland/ClassIsland.Core/Converters/WidthDoubleToRectConverter.cs:55-92,应用处 FluentTheme/Styles.axaml:21-38):

clipRectWidth  = BackgroundWidth            // 内容宽
clipRectHeight = 行高(40) 或实际 Bounds.Height
clipX = docking ∈ {0,3} ? 0
      : docking ∈ {1,4} ? (行容器宽 - BackgroundWidth)/2
      :                    (行容器宽 - BackgroundWidth)       // {2,5}
clipRadius = (RadiusX, RadiusY) = (8, 8)

CSS 实现:行容器 .line-row 上 overflow: visible,内容层 .line-clip 用 clip-path: inset(0 round 8px) 并把 .line-clip 定位到 left: clipX、width: BackgroundWidth、height: 40px;或直接对内容层设置 clip-path: inset(0 calc(...) 0 ...) round 8px。分体模式关闭该裁剪(clip-host 只在 IsIslandSeperated=False 或 clip-enforcing 时生效,FluentTheme/Styles.axaml:21)。

4.5.5 行可见性(三态组合,必须逐条实现)

行的 IsVisible 是下面表达式的逻辑或(Controls/MainWindowLine.axaml:29-49):

visible = ( ( !IsAllComponentsHid || IsOverlayOpen )
            && ( Settings.HideOnRule ? Settings.IsVisible : true ) )
        || MainWindowInEditMode

其中:

  • IsAllComponentsHid = 该行所有组件都不可见(MainWindowLine.cs:408-413,判定 Settings.Children.Any(x => x.IsVisible) == false);注意"全部组件都被隐藏规则隐藏"时整行消失,而不是留一条空胶囊。
  • IsOverlayOpen = 该行正在播放提醒(遮罩/浮层打开);提醒期间即使组件全隐藏也要显示。
  • 行的规则隐藏(HideOnRule=true)由 HidingRules 规则集求值,规则满足时 IsVisibleInternal=false 并订阅 RulesetService.StatusUpdated 实时重算(MainWindowLine.cs:347-381)。
  • 编辑模式下永远可见(否则无法编辑隐藏行)。

行的 Opacity = Settings.Opacity(行级不透明度,默认 1.0,范围 0.1–1、步进 0.05,MainWindowLine.axaml:51、MainWindowLineSettings.cs:42、EditModeView.axaml:340-346)。

鼠标淡出("指针不在岛上就变淡"):

  • 行级:IsLineFaded = IsMouseInFadingEnabled && (mouseIn XOR IsMouseInFadingReversed),为 true 且非编辑模式时行 opacity = 0.05(MainWindowLine.cs:503-509、MainWindowLine.axaml:73-75)。默认 IsMouseInFadingEnabled=true、IsMouseInFadingReversed=false(Models/Settings.cs:193-194)。
  • 判定鼠标是否在行内:把行内容矩形转换为屏幕物理坐标后与指针比较,宽度/高度都要乘 dpi * scale(MainWindowLine.cs:619-637)。
  • 触摸屏:TouchInFadingDurationMs > 0 时,触摸命中后延迟该毫秒数再把"鼠标在内"置否(MainWindowLine.cs:520-550,默认 0,Models/Settings.cs:195)。
  • 平台差异:靠 pointerover 判定而不是原始输入。

4.5.6 分体主界面(Separated)

全局 Settings.IsIslandSeperated = true 或行级 IslandSeparationMode = 2 时:

  • 行不再画整行背景(BackgroundBorderWrapper.IsVisible = !IsIslandSeperated,FluentTheme/Styles.axaml:78)。
  • 每个顶层组件各自渲染一个胶囊(FluentTheme/Styles.axaml:245-259):
.component-island {
  position: relative;
  margin: -2px 0;                 /* Panel Margin="-2 0" */
  display: flex;
  align-items: center;
}
.component-island > .bg {
  position: absolute; inset: 0;
  background: 行背景色; opacity: var(--island-opacity);
  border: 1px solid 边框色;
  border-radius: 8px;
  box-shadow: 0 4px 8px 2px #48000000;
}
.component-island > .content {
  position: relative;
  min-width: 40px;                /* Border MinWidth="40" */
  padding: 0 16px;                /* ContentPresenter Margin="16 0" */
  border-radius: 8px;
  overflow: hidden;
}
  • 分体模式下窗口内容仍按整行排布(MainWindow.axaml:213-217 的 WorkingRoot 宽度 = 客户端宽),但每行的高度仍 40,行内组件按原顺序横排,胶囊之间保留 12px 间隔(每个组件 Margin="6 0")。
  • 分体模式下行的 clip-host 不再参与裁剪(FluentTheme/Styles.axaml:21),阴影由组件胶囊自己承担。

4.5.7 行的过渡动画

  • 行内容透明度:opacity 过渡 100ms,线性(DoubleTransition Property="Opacity" Duration="0:0:0.100",FluentTheme/Styles.axaml:268、ClassicTheme/Styles.axaml:206)。
  • 背景宽度:Fluent 300ms cubic-bezier(0.65, 0, 0.35, 1.0)(FluentTheme/Styles.axaml:269);Classic 550ms cubic-bezier(0, 1, 0.5, 1.10)(ClassicTheme/Styles.axaml:207,注意控制点 y>1 会产生一次轻微回弹,必须照写)。
  • 行首次挂载淡入:AnimationLevel ≥ 2 时用组合层做 opacity 0→1、250ms、cubic-bezier(0.25, 1, 0.5, 1)(MainWindowLine.cs:382-401)。AnimationLevel 默认 1(Models/Settings.cs:1851),即默认不播。
  • 行宽变化(组件增删、内容变化)由"自适应面板重排动画"驱动:WrapPanelResizingAnimationAssist.Duration = 0.300s、Easing = (0.65, 0, 0.35, 1.0),挂在 Grid#WindowRoot 上(MainWindow.axaml:197-198),并把 animated-resize 类加给行的外层 Panel(FluentTheme/Styles.axaml:103)。编辑模式下该动画关闭(MainWindow.axaml:345 的 Classes.animated-resize="{Binding !MainWindowInEditMode}")。

4.5.8 行设置项(MainWindowLineSettings)与默认值

必须实现为一个 JSON 结构 + 抽屉表单。字段与默认值一律照抄(repos/ClassIsland/ClassIsland.Core/Models/Components/MainWindowLineSettings.cs:17-50):

字段 类型 默认 说明 / UI
Children 数组 [] 行内组件
IsMainLine bool false 主要行;全局唯一
IsNotificationEnabled bool true 是否允许显示提醒;关掉后该行用 Min 模板
IsResourceOverridingEnabled bool false 覆盖字号资源
MainWindowSecondaryFontSize number 14 次级字号,范围 1–40
MainWindowBodyFontSize number 16 正文字号
MainWindowEmphasizedFontSize number 18 强调字号
MainWindowLargeFontSize number 20 大号字号
IsCustomForegroundColorEnabled bool false 自定义字体颜色
ForegroundColor color DodgerBlue 默认字体色
BackgroundOpacity number 0.5 自定义背景不透明度
IsCustomBackgroundOpacityEnabled bool false
BackgroundColor color Black
IsCustomBackgroundColorEnabled bool false
CustomCornerRadius number 0 自定义圆角,范围 0–20
IsCustomCornerRadiusEnabled bool false
Opacity number 1.0 整行不透明度,范围 0.1–1
IsVisible bool true 运行时态(规则求值结果)
HideOnRule bool false 按规则隐藏
HidingRules Ruleset {} 规则集
IslandSeparationMode int 0 0 继承 / 1 禁用 / 2 启用

行为约束:

  • 主要行唯一:勾选某行为主要行时,其余行必须自动取消(MainWindow.axaml.cs:1569-1588);取消唯一的主要行时,自动把第一行设为主要行并弹 toast"已将第一行设置为主要行。"
  • 主要行是提醒注册的优先消费者(注册序号 -1,非主要行用行号,MainWindowLine.cs:441)。
  • 本部分只负责"提醒宿主"与 IsNotificationEnabled 标记:行模板的遮罩/浮层/进度条容器(4.7)、行的可见性联动、IsNotificationEnabled 决定走 Full 还是 Min 模板。多行之间谁显示提醒的分发规则归属第 7 章(提醒系统),本文不重复、不得在此臆造优先级。
  • 删除行:只剩 1 行时禁止删除并弹警告"至少需要保留 1 个主界面行。"(MainWindow.axaml.cs:1598-1612)。
  • 关闭所有行的提醒显示时弹警告"您已经禁用了所有主界面行的提醒显示功能……"(MainWindow.axaml.cs:1590-1596)。

4.5.9 行内编辑控件(仅在编辑模式显示)

行模板右侧第三列(MainWindow.axaml:401-444)在编辑模式显示一组按钮,容器属性:

.line-actions {                    /* StackPanel Classes="items-valign-center" */
  display: flex;
  align-items: center;
  gap: 2px;                        /* Spacing="2" */
  font-size: 14px;                 /* TextElement.FontSize="14" */
}
.line-actions .btn { background: transparent; border: none; }
.line-actions .btn-delete { color: IndianRed; }

四个按钮(自左至右,全部 VerticalAlignment=Center、Background=Transparent、BorderThickness=0):

  1. 行设置:图标 \uEF27,ToolTip "设置…",打开行设置抽屉。
  2. 主要行开关(ToggleButton):图标 \uE994,ToolTip "此行是主要行"。
  3. 提醒开关(ToggleButton):图标 \uE02B,ToolTip "此行可以显示提醒"。
  4. 删除(Button,Foreground=IndianRed,Theme=TransparentButton):图标 \uE61D,ToolTip "删除"。

行选择框(第一列的虚线矩形,MainWindow.axaml:353-363):

.line-select-box {
  position: absolute; inset: 0;
  border: 1.5px dashed rgba(控制描边色);
  border-radius: 0;                /* 无圆角 */
  opacity: 0.75;
  /* StrokeDashArray="1, 1.5" → dash 1px、gap 1.5px */
  background: none;
}

拖动把手(第一列,MainWindow.axaml:364-367):TouchDragThumb,仅在编辑模式可见,垂直方向拖动改变行顺序;拖动阈值由 AdvancedItemDragBehavior 决定,其 Orientation="Vertical"、HorizontalDragThreshold="33550336"(即水平拖拽被彻底禁用)、AllowDragFromDragThumbOnly="True"(MainWindow.axaml:332-335)。

"新主界面行"按钮(MainWindow.axaml:449-455):水平居中、font-size:14px、内容为图标 \uE00D + 文本"新主界面行";仅在编辑模式可见;点击追加一行默认配置并在该行下方插入。

4.6 主界面的缩放(裁决:只做缩放,不做平移)

  • ZoomBorder(MainWindow.axaml:199-212)必须实现为 .zoom-layer,参数:Stretch=None、ZoomSpeed=1.2(每格缩放倍率)、EnableConstrains=True、MaxZoomX=MaxZoomY=3、MinZoomX=MinZoomY=0.5、ShowZoomIndicator=true、EnableGestureRotation=False、Focusable=true、Background=Transparent。
    • 缩放手势固定为 Ctrl + 滚轮:以指针位置为锚点缩放,每格倍率按 ZoomSpeed = 1.2 指数累乘,范围钳制在 [0.5, 3.0](= MinZoomX/Y 与 MaxZoomX/Y)。不带 Ctrl 的滚轮不得缩放(避免与页面/抽屉滚动冲突)。
    • 双击画布空白处重置为 100%(等价 ResetMatrix);底部操作栏的"重置视图"按钮同样恢复 100%。
    • 缩放指示器:非 100% 时在窗口左下角显示当前倍率,font-size:12px,background:#77222222、color:#fff【推断:样式对齐 MainWindow.axaml:643-649 的调试文本样式】。
    • P2 / 不做平移:首版只实现 Ctrl+滚轮 缩放与双击重置;Space + 拖拽 平移标 P2 /【推断】(ClassIsland 的 ZoomBorder 支持拖拽平移,但控件源码未定位,且非编辑态整窗鼠标穿透用不到),不实现。
  • 设置里的"界面缩放"是另一件事:Settings.Scale 默认 1.0,范围 0.75–2.5、步进 0.05(Models/Settings.cs:86、EditModeView.axaml:50-57),应用在 .lines-transform 的 transform: scale(),transform-origin 需按停靠位置设置:上停靠 center top、下停靠 center bottom(对齐 LayoutTransformControl 的布局缩放语义,MainWindow.axaml:257-265)。
  • 两者叠加:总视觉倍率 = 缩放设置 × ZoomBorder 倍率;窗口包围盒只由"未缩放的内容尺寸"决定(见 4.3.1),缩放只改渲染层 transform,不改窗口尺寸。
  • 不做平移:见上条 P2 说明;EnableConstrains=true 仅用于把缩放后的内容约束在窗口内。

4.7 提醒宿主、遮罩与进度条(行内)

Full 模板的行内部结构(XamlThemes/FluentTheme/Styles.axaml:70-202)必须按下面层级实现:

.line (Grid#GridRoot)
├ .line-bg-wrap  → Border#BackgroundBorderWrapper        (整行背景层,分体模式隐藏)
│ └ .line-bg     → Border#BackgroundBorder  (Classes: docking line-background)
├ .line-bg-mask  → Border#BackgroundBorderOverlayMask    (强调色遮罩,Opacity 0→1,仅提醒时)
│    · opacity 过渡只对 BoxShadow 做 0.1s
├ .line-clip     → Grid#ContentClipBorder (Classes: clip-host)
│ └ .line-panel  → Panel (Classes: animated-resize)
│   └ .grid-wrapper → Grid#PART_GridWrapper (height 40, Classes: docking)
│     ├ .content-root → Grid#GridContentRoot (height 40, min-width 20, margin 0 12)
│     │   └ .components  (组件列表)
│     ├ .overlay → Grid#GridOverlay (opacity 0)
│     │   ├ .overlay-bg → Border (line-background,分体模式才可见)
│     │   └ .overlay-clip → Border (clip, corner-radius 8)
│     │     └ Panel
│     │       ├ Canvas#ProgressContainer (bottom, height = progressBar 高度)
│     │       │   └ ProgressBar#OverlayTimeProgressBar (Maximum 1, Value = LeftProgress, MinWidth 0)
│     │       └ ContentPresenter(OverlayContent,MaxHeight 40)
│     └ .mask-shadow → Border#OverlayMaskShadowRoot (仅非编辑模式)
│       └ .mask  → Border#OverlayMask (背景 AccentFillColorDefaultBrush, 前景 TextOnAccentFillColorPrimaryBrush, clip, height 40)
│         └ Panel
│           ├ SlantedMaskControl (Classes: mask-bg, RegionDurationMs=360, StageStaggerMs=120, Fill=强调色)
│           └ Border (Padding 0 12)
│             └ .mask-content → ContentPresenter#OverlayMaskContent (opacity 0, scale)

4.7.1 进度条(提醒剩余时间)

  • 容器 Canvas 贴底、水平拉伸,高度绑定进度条自身高度(FluentTheme/Styles.axaml:127-147)。
  • ProgressBar:Maximum=1、Value = 当前提醒的 LeftProgress(0–1,MainWindowLine.axaml:77)、MinWidth=0、Width = 容器宽、贴底。
  • 值变化过渡:50ms、线性(DoubleTransition Property="Value" Duration="0:0:0.05" + LinearEasing,FluentTheme/Styles.axaml:137-145)。
  • 实现:进度条高度固定 3px(token --ci-progress-height: 3px,border-radius: 1.5px,贴底;三条进度条——行提醒进度条、课程进度条、倒计时进度条——共用同一 token),颜色取 AccentFillColorDefaultBrush,transform: scaleX(value) + transform-origin:left 比改 width 更省重排;但不能有过渡抖动,transition: transform 50ms linear。
  • ReduceProgressAccuracy=true(默认,Models/Settings.cs:1853)时进度按步长量化:progressTick = max(10, totalSeconds/500),value = round(seconds/progressTick)*progressTick(倒计时组件同款算法,Controls/Components/CountDownComponent.axaml.cs:288-295)。主界面行进度条同样应做降频更新(100ms 级),false 时按 1 秒精度。

4.7.2 遮罩(Mask)动画

  • SlantedMaskControl:5 个区段宽度权重 [0.0, 0.1, 0.2, 0.4, 0.2, 0.1] 的累计边界,斜面角度固定 60°(Controls/SlantedMaskControl.cs:284, 298-308),填充色 AccentFillColorDefaultBrush。
  • 参数:RegionDurationMs = 360、StageStaggerMs = 120(行模板里显式赋值,FluentTheme/Styles.axaml:178-179);控件默认值 300/120(SlantedMaskControl.cs:29-33)——以行模板的 360/120 为准。
  • 打开(mask-in)顺序:区段 0 与 4 立即开始(delay 0),区段 1 与 3 延迟 stag*0.75 = 90ms,区段 2 延迟 stag*1 = 120ms;缓动 QuarticEaseOut;每段时长为独立的关键帧(SlantedMaskControl.cs:167-188)。总时长 = 120 + 360 = 480ms。
  • 关闭(mask-out)顺序反向:区段 2 先(delay 0),1/3 延迟 90ms,0/4 延迟 120ms;缓动 QuadraticEaseIn(SlantedMaskControl.cs:189-200)。
  • CSS 实现建议:遮罩层用 5 个 clip-path 平行四边形(skewX(-30deg) 等价于 60° 斜面),每个用 animation: mask-region 360ms <ease> <delay> both,--scale-x: 0 → 1,transform-origin 取各自区段中心。
  • 遮罩背后的内容:Grid#GridContentRoot.Opacity 在 mask-in 时置 0,:mask-out 时也置 0,:overlay-in/out 控制恢复(FluentTheme/Styles.axaml:293-305, 368-370, 411-413, 438-450)。
  • 遮罩投影:mask-in 期间背景层与遮罩层加 BoxShadow = 0 8 32 0 #66000000(FluentTheme/Styles.axaml:323-328),mask-out 时淡掉。
  • 遮罩内容入场:delay 260ms、duration 250ms、ease cubic-bezier(0.25, 1, 0.5, 1)、opacity 0→1(FluentTheme/Styles.axaml:329-340);同时 scale 1.1→1.0、duration 750ms、delay 260ms、同缓动(:341-352)。
  • 遮罩内容容器:Padding="12 0"、前景色 TextOnAccentFillColorPrimaryBrush(FluentTheme/Styles.axaml:181-184)。
  • 强调色底:Border#BackgroundBorderOverlayMask 背景 = AccentFillColorDefaultBrush(StylesBase.axaml:90-93),Opacity 0→1、delay 600ms、duration 100ms、FillMode=Forward(FluentTheme/Styles.axaml:355-367)。

Classic 主题的等价实现(必须两套都做,用 CSS 变量或 data-theme 切换;XamlThemes/ClassicTheme/Styles.axaml):

  • 遮罩与强调层用 TranslateTransform Y=-50 起手;mask-in:duration 200ms、QuadraticEaseIn、Y: -50 → 0(:265-281)。
  • mask-out:delay 100ms、duration 200ms、QuadraticEaseOut、Y: 0 → 50(:305-318)。
  • 遮罩内容淡入:delay 200ms、duration 100ms、opacity 0→1(:288-300);淡出 100ms、opacity 1→0(:319-331)。
  • 首次出现时给 Grid#GridRoot 挂 DropShadowEffect(OffsetX=0, OffsetY=0, Opacity=1, BlurRadius=8),Effect 过渡 100ms 线性(:34-45, 283-287)。

4.7.3 浮层(Overlay)

  • Grid#GridOverlay 初始 Opacity=0,带 DoubleTransition Opacity 200ms SineEaseIn(FluentTheme/Styles.axaml:419-432)。
  • :overlay-in → 浮层 opacity 1、内容层 opacity 0;:overlay-out → 浮层 opacity 0、内容层 opacity 1(:434-451)。
  • 浮层内容 MaxHeight = 40(FluentTheme/Styles.axaml:149);浮层里的进度条只显示"当前提醒剩余时间"。

4.8 编辑模式

4.8.1 进入与退出

  • 入口:原生菜单"编辑主界面…"(MainWindow.axaml:108-109)、URI classisland://app/edit(MainWindow.axaml.cs:295)。
  • 进入前必须通过权限校验(ManagementService.AuthorizeByLevel(EditSettingsAuthorizeLevel),MainWindow.axaml.cs:1357-1362);被策略禁用时直接返回。
  • 进入步骤(按序):IsEditMode = true → 窗口 Activate() → 挂载编辑视图 → ZoomBorder.ResetMatrix() 在退出时调用 → TopmostEffectWindow.ViewModel.IsEditMode 同步 → 更新窗口层级 → 重新加载主题(MainWindow.axaml.cs:510-528)。
  • 退出步骤:关闭两级抽屉 → IsEditMode=false → 清空教程提示 → 保存组件配置(ComponentsService.SaveConfig(),MainWindow.axaml.cs:1391-1403)。保存必须幂等且原子(写临时文件再替换)。
  • 编辑模式的窗口态:ClassIsland 用 Background = SmokeFillColorDefaultBrush + TransparencyLevelHint = "AcrylicBlur, Transparent" + 标题 编辑主界面 | ClassIsland(MainWindow.axaml:177-181)。本实现的裁决:
    • 禁止调用 Electron 的 setBackgroundMaterial() / vibrancy / backgroundMaterial 等任何系统材质 API(原因见 4.1 第 2 条与 repos/ClassHelper-master/packages/desktop-client/src/main/island.ts:523-524)。
    • 替代方案:一层全屏半透明深色遮罩 div,background: rgba(0, 0, 0, 0.30)(token --ci-smoke;深色主题可下调至 0.24、浅色主题可上调至 0.36,但默认必须是 0.30),窗口保持 transparent: true 逐像素透明。
    • 不得使用 backdrop-filter: blur():透明窗口下 Chromium 的背板采样不可靠,会出现整块灰底/边缘发白。
    • 【推断 / 差异说明】该方案与 FluentAvalonia 的 Acrylic 是近似而非等价:Acrylic 带轻微模糊与噪声,本实现只有纯色压暗。这是有意接受的视觉差异,不得在实现说明里写成"与 Acrylic 完全一致";验收只要求"编辑态背景明显压暗、岛与抽屉可读、退出后恢复逐像素透明"。
  • 编辑模式下窗口必须接收鼠标:win.setIgnoreMouseEvents(false);同时 Transparent 窗口特性关闭(MainWindow.axaml.cs:839)。
  • 平台兜底:Windows 10 1803 及更早版本不支持 windowed 透明,需给窗口加标记类 no-windowed-transparent,此时 windowed+edit 模式用不透明底色 SolidBackgroundFillColorBaseBrush、TransparencyLevelHint=None(MainWindow.axaml:187-190, MainWindow.axaml.cs:257-260)。

4.8.2 编辑画布

  • 画布 = 全屏(宽 = 屏宽,高 = 屏高 −1px),岛内容在画布内四边各留 60px 安全边距,再按 (ox, oy) 偏移(见 4.3.3 编辑模式窗口公式)。
  • 行容器对齐:dock-top → align-items:flex-start,dock-bottom → align-items:flex-end,编辑模式 → justify-content:center(对应 StackPanelRootContainer 的 VerticalAlignment 覆写,MainWindow.axaml:240-248)。编辑模式下内容整体垂直居中,方便拖拽。
  • 底部操作栏占用画布底部 64px 区域,安全边距必须保证岛内容不被操作栏遮挡(岛最高可到画布高 −120px)。
  • 空白处左键点击:清除当前选中的组件并关闭组件设置抽屉(MainWindow.axaml.cs:1511-1535,只有点在 ZoomBorder 自身(非命中子控件)时触发)。

4.8.3 底部操作栏(OperateArea)

精确规格(MainWindow.axaml:595-634):

.operate-area {
  position: fixed;
  left: 50%; transform: translateX(-50%);
  bottom: 64px;                  /* Margin="0 0 0 64" */
  background: var(--commandbar-overflow-bg);   /* CommandBarOverflowPresenterBackground */
  border: 1px solid var(--commandbar-overflow-border);
  border-radius: 6px;
  padding: 4px;
  display: flex;                 /* 内部 StackPanel Spacing="6" */
  gap: 6px;
  z-index: 10;
  pointer-events: auto;
}
.operate-area .divider { width: 1px; height: 25px; background: var(--divider-stroke); align-self: center; }

内部按钮(自左至右,除"完成"外均为 TransparentButton 主题):

  1. "重置视图"——图标 \uE12F,命令 = 缩放重置。
  2. 竖分隔线:StartPoint 0,0 → EndPoint 0,25、StrokeThickness=1、颜色 DividerStrokeColorDefaultBrush。
  3. "外观"——图标 \uE51E,打开外观抽屉;DisableSettingsEditing 策略下禁用。
  4. "添加组件"——图标 \uE071,打开组件库抽屉;同策略禁用。
  5. 组件配置按钮——图标 \uE699,文本 = 当前配置名(Settings.CurrentComponentConfig,默认 "Default",Models/Settings.cs:159),MaxWidth=150;打开配置管理抽屉。
  6. "完成"——图标 \uE424,Classes="accent"(强调按钮),退出编辑模式。

4.8.4 抽屉(VerticalDrawer)

  • 主抽屉:RequestedWidth = 960(SettingsContainerWidth,ThemeBase.axaml:37;EditModeView.axaml:600-605),AllowCollapse=True,SmokeBackground=False。
  • 次抽屉:SmokeBackground=True、AllowCollapse=False、ContentHeightRatio=0.5(EditModeView.axaml:606-611)。
  • 外壳规格(repos/ClassIsland/ClassIsland.Core/Controls/VerticalDrawer.axaml:8-155):
.drawer-host { position:absolute; inset:0; pointer-events:none; }
.drawer-smoke { position:absolute; inset:0; background: var(--smoke-fill); opacity:0; transition: opacity 250ms cubic-bezier(0,0,0,1); }
.drawer-smoke.open { opacity:1; pointer-events:auto; }
.drawer-panel {
  position:absolute; bottom:0; left:50%; transform: translateX(-50%);
  width: 960px; max-width: 100%;
  max-height: 80vh;                /* 【推断:ContainerHeight 由内容/宿主决定,见下 */
  background: var(--content-dialog-bg);
  border: 1px solid var(--content-dialog-border);
  border-top-left-radius: 8px; border-top-right-radius: 8px;  /* "8 8 0 0" */
  box-shadow: 0 8px 32px 0 #66000000;
  overflow: hidden;
  pointer-events: auto;
}
.drawer-inner { padding: 8px 8px 0; }         /* Padding="8 8 8 0" */
.drawer-head  { height: 32px; display:flex; align-items:center; justify-content:space-between; }
.drawer-body  { min-height: 0; overflow:auto; }
  • 抽屉升降过渡:Canvas.Bottom 与 Height 各 250ms、cubic-bezier(0,0,0,1)(VerticalDrawer.axaml:29-36)。
  • 标题栏右侧三个按钮:收起 \uEC5C(仅 AllowCollapse 且已展开时可见)、展开 \uEC5E(AllowCollapse 且非展开时)、关闭 \uE671(VerticalDrawer.axaml:64-99)。
  • 抽屉内容宽度上限 = 960px;设置项容器 .settings-container 的 Margin = 12 12 18 12、Spacing = 4(StylesBase.axaml:67-71)。
  • 抽屉打开时挂载"逐项入场"动画类 animated-intro(EditModeView.axaml:43, 317),对应 ClassIsland.Core/Themes/StackPanelIntroAnimation.axaml(时长/曲线见第 14 章)。
  • 拖拽组件时主抽屉自动临时收起(Collapsed),拖拽结束自动展开(EditModeView.axaml.cs:143-163)。

4.8.5 组件库抽屉

  • 标题"组件库"(EditModeView.axaml.cs:130)。
  • 结构:按组件分组(ComponentRegistryService.GetGroupedSortedComponents())→ 每组一个 group-name(font-weight:SemiBold、margin-bottom:4px)+ 一个 WrapPanel(目标宽 225px,WrapPanelAutoResizeBehavior,EditModeView.axaml:167-218)。
  • 每个组件项:高 50px、Margin 0 8、四列 Auto,Auto,*,Auto:
.component-lib-item { display:grid; grid-template-columns:auto auto 1fr auto; height:50px; margin:8px 0; }
.component-lib-item .drag-thumb { width:24px; align-self:center; }        /* TouchDragThumb Width=24 */
.component-lib-item .icon { width:32px; height:32px; font-size:32px; margin:0 -4px 4px 0; }
.component-lib-item .name { font-size:14px; overflow:hidden; text-overflow:ellipsis; }
.component-lib-item .desc { grid-column:2 / span 2; font-size:12px; opacity:0.75; white-space:normal; }
  • 底部提示文案(按是否已选目标列表切换):"将组件拖动到主界面上即可添加组件。" / "选择要添加的组件,或将组件拖动到主界面上即可添加组件。",样式类 l-instructive,margin:8px 0(EditModeView.axaml:226-235)。
  • 已选目标时右下角出现"添加"按钮:padding:16px 6px、Classes="accent"、margin:4px 0 8px(EditModeView.axaml:236-240)。

4.8.6 组件设置抽屉 / 行设置抽屉

  • 标题栏(组件):左侧图标 16×16 + 组件名 + TabStrip.compact 两个页签"组件设置""高级设置"(仅当组件有 SettingsType 时显示第一个页签);右侧"按规则隐藏"复选框 + 编辑规则按钮(图标 \uF17E,仅勾选时启用),Margin 0 0 4 0、Spacing 4(EditModeView.axaml:243-283)。
  • 标题栏(行):文本"主界面行设置" + 同样的"按规则隐藏"组合(EditModeView.axaml:298-313)。
  • 抽屉内容宽度上限 960,内部 .settings-container(Margin 12 12 18 12、Spacing 4)。
  • 选择组件时其它所有列表的选择必须清空(MainWindow.axaml.cs:1500-1504)。

4.8.7 容器组件浮动编辑框(ContainerComponents)

在编辑模式中,点"查看子组件"把某个容器组件的子组件以浮动小窗形式加到画布上(MainWindow.axaml:457-582):

  • 宿主:ItemsControl + Canvas(CanvasDragBehavior),每项 Canvas.Left/Top 绑定 EditModeContainerComponentInfo.X/Y。
  • 初始位置:被点击组件的屏幕点转换到画布坐标,再 Y + 48(MainWindow.axaml.cs:1559-1567)。
  • 外框:Border,HorizontalAlignment=Center、VerticalAlignment=Bottom、Background=CommandBarOverflowPresenterBackground、CornerRadius=6、Padding="4 0"、BorderBrush=CommandBarOverflowPresenterBorderBrush、BorderThickness=1。
  • 内容:横排 Spacing 4;
    • 拖动把手 TouchDragThumb,Width=20,IsExplicitVisible=true;
    • 关闭按钮(图标 \uE671,ToolTip"关闭")、组件图标 16×16、组件名文本,这一组 font-size:14、Spacing 4、Margin "4 0 0 0";
    • 子组件列表 EditableComponentsListBox,Height=40。
  • 拖拽中的项:opacity: 0.5、z-index: 1(MainWindow.axaml:478-481)。
  • 关闭时移除对应 info(EditModeView.axaml.cs:240-249)。

4.8.8 编辑模式的组件外观

  • 被隐藏的组件仍然占位并显示"斜条纹"提示:OpacityMask 为 45° 线性渐变、Background 起止 #afffffff(0–0.5)、#50ffffff(0.5–1)、Repeat(repos/ClassIsland/ClassIsland.Core/Controls/ComponentPresenter.axaml:19-33)。CSS:
.comps-hidden {                       /* 仅编辑模式 */
  -webkit-mask-image: repeating-linear-gradient(45deg, #afffffff 0 4px, #50ffffff 4px 8px);
          mask-image: repeating-linear-gradient(45deg, #afffffff 0 4px, #50ffffff 4px 8px);
}
  • 容器组件未选中时显示虚线边框(SurfaceStrokeColorDefaultBrush、1.5px、dash 1/1.5、opacity 0.75、margin:0 2px);选中时改为强调色虚线边框(AccentFillColorDefaultBrush,EditableComponentsListBox.axaml:217-252)。
  • 选中组件的操作条(Adorner):出现在组件正下方,Background=CommandBarOverflowPresenterBackground、CornerRadius=6、Padding=4、BorderBrush=CommandBarOverflowPresenterBorderBrush、BorderThickness=1、Spacing 6(EditableComponentsListBox.axaml:100-184)。按钮:组件设置 \uEF27、"查看子组件" \uEC30(仅容器组件)、删除 \uE61D(IndianRed)、25px 竖分隔线、"更多选项…" \uEBAC(菜单:包裹到新容器 / 创建副本)。
  • 每个组件的"添加组件"按钮:height:32px、图标 \uE00D、font-size:20px,位于组件列表末尾,只在编辑模式可见(EditableComponentsListBox.axaml:55-60)。
  • 拖拽插入指示:拖拽中的列表在 AdornerLayer 上画一个强调色 1px 边框 + BoxShadow,并做 1s 交替无限动画(0%: opacity .1 → 100%: opacity .6,SineEaseInOut,PlaybackDirection=Alternate,EditableComponentsListBox.axaml:307-336)。
  • 拖拽开始阈值:水平/垂直各 3px,拖拽预览 opacity 0.5(EditableComponentsListBox.axaml:286-288)。
  • 拖拽预览浮标:Background=AcrylicBackgroundFillColorDefaultBrush、CornerRadius=6、Padding=4、BorderBrush=SurfaceStrokeColorDefaultBrush、BorderThickness=1、图标 16 + 名称、Spacing 4(EditableComponentsListBox.axaml:22-39)。该资源名虽含 Acrylic,但实现必须用不透明降级色 --ci-bg-tertiary,不得调用系统材质 API。

4.9 隐藏与显示规则(完整判定)

hideMode(Settings.HideMode,默认 0,Models/Settings.cs:185):

  • 0 = 基本:由三个独立开关控制,全部在 GridRoot 上以伪类组合判断(MainWindow.axaml:150-160):
    • HideOnClass=true 且当前时间状态为"上课" → 隐藏(默认 false,Models/Settings.cs:45);
    • HideOnMaxWindow=true 且前台窗口最大化 → 隐藏(默认 false,Models/Settings.cs:63);
    • HideOnFullscreen=true 且前台窗口全屏 → 隐藏(默认 false,Models/Settings.cs:54)。
  • 1 = 高级:用 Settings.HideRules(规则集,JSON 字段名 HiedRules,Models/Settings.cs:462-472)求值,满足则隐藏(MainWindow.axaml:161-163、MainWindow.axaml.cs:444-449)。

检测数据源与频率:

  • 全屏/最大化状态在每个主计时器 tick 检测:PlatformServices.WindowPlatformService.IsForegroundWindowFullscreen(screen) / IsForegroundWindowMaximized(screen)(MainWindow.axaml.cs:451-456)。
  • 排除进程名单 ExcludedFullscreenWindow,默认 ["explorer"](Models/Settings.cs:56-59):这些进程处于前台时不算全屏/最大化,不触发隐藏。
  • 判断基准屏幕必须是"本窗口所在屏幕"(GetSelectedScreenSafe())。

强制显示(覆盖隐藏):

  • ExplicitShowed = 正在播放提醒或处于编辑模式(MainWindow.axaml:271、MainWindow.axaml.cs:1056-1092 的 Acquire/ReleaseTopmostLock)。此时即使命中隐藏条件也必须显示,且窗口临时置顶。
  • IsMainWindowVisible(用户手动"隐藏主界面",默认 true,Models/Settings.cs:67):GridRoot 的可见性 = IsMainWindowVisible || IsEditMode(MainWindow.axaml:135-141)。手动隐藏时同时不接受新提醒(行级 AcceptsNotificationRequests 要求 IsNotificationEnabled && !IsAllComponentsHid && !_isOverlayOpen && !_isUnloading,MainWindowLine.cs:814)。
  • 所有行都隐藏时(IsAllComponentsHid 对所有行成立):可见岛内容 opacity:0,窗口包围盒保持不变(不改尺寸、不隐藏窗口),但不得销毁窗口(提醒到来时要能立刻显示)。

4.10 右键菜单(原生菜单)与托盘

菜单必须是系统原生菜单(Electron Menu.buildFromTemplate + tray.setContextMenu),不得用 HTML 菜单。逐项规格(MainWindow.axaml:51-132):

# 文本 图标 glyph 可见条件 操作
1 ClassIsland AppLogo 总是 打开"关于"设置页
2 教学… \uE959 IsWelcomeWindowShowed 打开教程中心
3 帮助文档… \uE23C IsWelcomeWindowShowed 打开 https://docs.classisland.tech/app/
4 分隔符
5 显示主界面 \uE813 !IsMainWindowVisible && IsWelcomeWindowShowed 切 IsMainWindowVisible
6 隐藏主界面 \uE817 IsMainWindowVisible && IsWelcomeWindowShowed 切 IsMainWindowVisible
7 清除全部提醒 \uE029 IsWelcomeWindowShowed && IsNotificationsPlaying 取消全部提醒
8 更多选项… \uEBAC 存在插件注入项 子菜单
9 分隔符
10 编辑档案… \uE699 总是 打开档案编辑窗口
11 加载临时课表… \uE6A1 总是 打开档案窗口并展开"临时课表"抽屉
12 换课… \uE15F 总是 打开换课窗口(需权限)
13 分隔符
14 编辑主界面… \uE7C9 总是 进入编辑模式
15 应用设置… \uEF27 总是 打开设置窗口
16 分隔符
17 重启 \uE0BD 总是 重启应用
18 退出 \uE0DF 总是 退出(需权限)
19 分隔符 IsDebugEnabled
20 dev__Debug \uE2C8 IsDebugEnabled 子菜单:DevTools / DevPortal / 截图工具 / 教程编辑器 / 启用课表 / 显示崩溃窗口 / WelcomeWindow / WelcomeWindow(full)

托盘左键点击行为 TaskBarIconClickBehavior:0=打开主菜单(仅 Windows 有效,其他平台自动改为 4)、1=打开档案编辑窗口、2=显示/隐藏主界面、3=打开换课窗口、4=打开设置窗口;默认 Windows 平台 0、其他平台 4(Models/Settings.cs:102, :540-561,实现 MainWindow.axaml.cs:617-662)。

4.11 本部分动画总表(照此实现,不得自创)

动画 触发 属性 时长 缓动
行背景色过渡 主题/背景色变化 Background 200ms 线性(MainWindow.axaml:40-44 BrushTransition)
行内容透明度 行显示/隐藏 opacity 100ms 线性(FluentTheme/Styles.axaml:268)
行背景宽度 内容宽变化 width 300ms(Fluent)/ 550ms(Classic) cubic-bezier(0.65,0,0.35,1.0) / cubic-bezier(0,1,0.5,1.1)
行挂载淡入 首次可见 opacity 0→1 250ms cubic-bezier(0.25,1,0.5,1);AnimationLevel≥2 才播
内容层交错重排 组件变化 位置/宽 300ms cubic-bezier(0.65,0,0.35,1.0)(MainWindow.axaml:197-198)
遮罩斜掠(开) 提醒开始 5 区段 scaleX 0→1 每段 360ms,错峰 0/90/90/120/120ms cubic-bezier(0.165,0.84,0.44,1)(QuarticEaseOut)
遮罩斜掠(关) 提醒结束 5 区段 scaleX 1→0 每段 360ms,反向错峰 cubic-bezier(0.55,0.085,0.68,0.53)(QuadraticEaseIn)
强调色底淡入 提醒开始 opacity 0→1 100ms,delay 600ms 线性
遮罩文字淡入 提醒开始 opacity 0→1 250ms,delay 260ms cubic-bezier(0.25,1,0.5,1)
遮罩文字缩放 提醒开始 scale 1.1→1.0 750ms,delay 260ms cubic-bezier(0.25,1,0.5,1)
遮罩文字淡出 提醒结束 opacity 1→0 200ms 线性
遮罩文字缩小 提醒结束 scale 1.0→1.1 200ms cubic-bezier(0.5,0,0.75,0)
浮层淡入淡出 Overlay 开始/结束 opacity 200ms SineEaseIn
提醒进度条值 剩余时间更新 value 50ms 线性
行变淡 指针离开 opacity →0.05 100ms 线性
编辑抽屉升降 打开/关闭抽屉 bottom/height 250ms cubic-bezier(0,0,0,1)
遮罩层(smoke) 抽屉打开 opacity 250ms cubic-bezier(0,0,0,1)
拖拽插入高亮 拖动中 opacity 0.1↔0.6 1000ms 交替无限 SineEaseInOut
斜条纹隐藏提示 隐藏组件 — 静态 —

4.12 验收标准

实现者必须逐条自查,全部通过才算完成本节:

  1. 非编辑模式下用 Spy++/AutoHotkey 之类工具点击岛的任何位置,点击都穿透到桌面应用;编辑模式下岛内的按钮、拖拽把手、抽屉都能被点到。
  2. 主界面窗口在 4K(150% 缩放)与 1080p(100% 缩放)两种 DPI 下都不出现 1px 白边、黑边、圆角锯齿;圆角在 RadiusX=0 时是直角,在 20 时左右边缘为半圆。
  3. 6 个停靠位置逐个验证:0/1/2 贴工作区上边、3/4/5 贴下边;左/中/右的横向对齐分别贴左、居中、贴右,且窗口本身始终横跨整屏。
  4. 修改"窗口向右/向下偏移"后岛整体平移,正值向下时上停靠要把偏移吞进安全区(不得被屏幕边缘裁掉)。
  5. 行数从 1 增到 5、再减到 1:可见岛高度严格等于"行高之和 + 行间距之和"(误差 ≤1px),岛在窗口包围盒内平滑形变,不出现窗口级闪烁;包围盒只在行数超出 contentMaxHeight 时变更一次,且变更被节流 ≥100ms。 5.1 用 DevTools 的 Performance 面板录 10 秒:setBounds 调用次数为 0(无设置/行数变化时);置顶复核轮询间隔 ≥1000ms,无 1ms/10ms 级高频定时器。
  6. 单行内容宽度变化(如时钟秒数出现/消失、课程名变长)时,背景宽度在 300ms 内以 cubic-bezier(0.65,0,0.35,1.0) 平滑跟随,文字不重排抖动,且不触发窗口 setBounds。
  7. 行间距设为 0、5、150 时,相邻行间距精确等于设置值(用像素标尺截图量)。
  8. 关闭某行"可以显示提醒"后,该行不再出现遮罩与进度条(走 Min 模板),其余行不受影响;多行同时具备提醒能力时,具体由哪一行显示由第 7 章的分发规则决定,本节只验证宿主与 IsNotificationEnabled 标记生效。 8.1 三条进度条(行提醒、课程、倒计时)高度均为 3px,圆角 1.5px,贴底且不越出行圆角裁切。
  9. 删除到只剩 1 行时再点删除,出现"至少需要保留 1 个主界面行。"提示且行数不变。
  10. 勾选 A 行为主要行后 B 行自动取消;取消 A 行后第一行自动成为主要行并出现提示。
  11. 触发一次提醒:遮罩从左、右两端同时向中间斜掠,总时长约 480ms,文字在 260ms 后淡入并伴随 1.1→1.0 缩放;遮罩期间行内容消失,结束后恢复。
  12. 阈值条件全开(上课隐藏、最大化隐藏、全屏隐藏),在前台最大化窗口时岛消失、切回桌面 1 个计时器周期内恢复;ExcludedFullscreenWindow 含 explorer 时桌面(资源管理器)前台不隐藏。
  13. 鼠标移出岛 0.5 秒内行的整体不透明度降到 0.05,移回立即恢复;勾选"反向"后行为相反。
  14. 编辑模式:进入后窗口扩展为"屏宽 ×(屏高−1px)",岛内容四边留 60px 安全边距并垂直居中,任务栏出现图标,鼠标可交互;退出后恢复包围盒尺寸与穿透状态,并写盘保存组件配置;编辑态背景为 rgba(0,0,0,0.30) 遮罩(无 backdrop-filter、无系统材质),退出后恢复逐像素透明。
  15. 编辑模式下拖拽组件到另一行的插入位置时,出现强调色发光插入线(1s 交替闪烁),松手后组件落到目标位置且行宽平滑重排。
  16. 容器组件"查看子组件"浮动框出现在被点组件下方 48px 处,可拖动、可关闭,关闭后 ContainerComponents 中对应项被移除。
  17. 主界面完全不可见(手动隐藏 / 全部隐藏规则命中)时收到提醒,岛必须临时显示、置顶,提醒结束后回到隐藏状态。
  18. 托盘图标左键行为的 5 种取值(0–4)逐个验证正确,其中非 Windows 平台 0 自动降级为 4。

5. 组件系统:全部内置组件逐个规格

本节规定主界面上所有组件的实现规格。组件是"岛"的内容单元:每个组件的实例由一份 JSON(ComponentSettings)描述,含「组件 ID + 组件自己的设置对象 + 一系列外观/布局/隐藏覆盖项」。实现必须做成可插拔的注册表(原生 11 个组件 + 后续插件组件走同一接口),不得把组件硬编码进主界面。

5.1 组件系统总则

5.1.1 组件注册表(唯一事实来源)

必须内置下面 11 个组件,ID、名称、图标 glyph、描述、是否容器、设置类型逐字照抄([ComponentInfo] 特性):

组件 ID(GUID) 名称 图标 glyph 描述 容器 设置类型 证据
日期 DF3F8295-21F6-482E-BADA-FA0E5F14BB66 日期 \ue304 显示今天的日期和星期。 否 无 Controls/Components/DateComponent.axaml.cs:14
时钟 9E1AF71D-8F77-4B21-A342-448787104DD9 时钟 \ue31c 显示现在的时间,支持精确到秒。 否 ClockComponentSettings ClockComponent.axaml.cs:17
倒计时 7C645D35-8151-48BA-B4AC-15017460D994 倒计时 \uf361 显示距离某一天的倒计时。 否 CountDownComponentSettings CountDownComponent.axaml.cs:29
课程表 1DB2017D-E374-4BC6-9D57-0B4ADF03A6B8 课程表 \ue751 显示当前的课程表信息。 否 LessonControlSettings ScheduleComponent.axaml.cs:30
天气简报 CA495086-E297-4BEB-9603-C5C1C1A8551E 天气简报 \uf465 显示当前的天气概况和气象预警。 否 WeatherComponentSettings WeatherComponent.axaml.cs:16
文本 EE8F66BD-C423-4E7C-AB46-AA9976B00E08 文本 \uf323 显示自定义文本。 否 TextComponentSettings TextComponent.axaml.cs:16
滚动容器 70FCD5EA-3FAE-4E06-ACA2-4F4DF47F9ACD 滚动容器 \uf279 滚动显示组件内的内容。 是 RollingComponentSettings RollingComponent.axaml.cs:33
轮播容器 7E19A113-D281-4F33-970A-834A0B78B5AD 轮播容器 \uefc9 轮播多个组件。 是 SlideComponentSettings SlideComponent.axaml.cs:24
分组容器 C911D762-107F-40C6-84CC-0146AB3C86B1 分组容器 \ue92f 将多个组件组合到一个组件中显示。 是 GroupComponentSettings GroupComponent.axaml.cs:11
堆叠容器 2D849ECE-9F21-4C78-9434-415CFC283294 堆叠容器 \uf04f 将多个容器堆叠显示。 是 StackComponentSettings StackComponent.axaml.cs:11
分割线 AB0F26D5-9DF6-4575-B844-73B04D0907C1 分割线 \uf023 显示一个分割线,视觉上对组件进行分组。 否 无 SeparatorComponent.axaml.cs:9

图标字体为 FluentSystemIcons-Resizable(repos/ClassIsland/ClassIsland.Core/ThemeBase.axaml:46),FontFamily 绑到 ci|FluentIcon 全局样式(StylesBase.axaml:79-81)。实现时用同名 SVG 图标表 + mask-image 或内联 SVG,不得贴无对应关系的图标。

课程表 组件有一个历史 ID 需在迁移时映射:E7831603-61A0-4180-B51B-54AD75B1A4D3 → 现 ID([MigrateFrom],ScheduleComponent.axaml.cs:29)。

5.1.2 默认组件布局(首次启动的样子)

首次启动必须生成 1 个主界面行,行内依次为「日期」「课程表」,其余字段取默认值(repos/ClassIsland/ClassIsland/Services/ComponentsService.cs:33-52)。实现即:lines = [{ children: [dateId, scheduleId], ...默认行设置 }]。

5.1.3 组件宿主(ComponentPresenter):所有组件共用的外壳

每个组件都必须包在统一的宿主里,宿主负责下面这些语义(repos/ClassIsland/ClassIsland.Core/Controls/ComponentPresenter.axaml):

  1. 可见性三态:visible = IsVisibleInternal || 主界面处于编辑模式(:13-18)。隐藏的组件在编辑模式仍占位。
  2. 透明度:宿主 opacity = 组件设置.Opacity(默认 1.0,范围 0–1,:34-36)。
  3. 字号继承:宿主根元素的 font-size = var(--font-body)(16px),即所有组件默认正文字号;组件内部需要别的字号时必须显式引用 --font-secondary/emphasized/large(:37-38)。
  4. 宽度三态(互斥,优先级:固定宽 > 最大宽/最小宽):
    • IsFixedWidthEnabled → width = FixedWidth(默认 200px,:45-47);
    • IsMaxWidthEnabled → max-width = MaxWidth(默认 300px,:39-41);
    • IsMinWidthEnabled → min-width = MinWidth(默认 100px,:42-44)。 默认三者全 false(repos/ClassIsland/ClassIsland.Core/Models/Components/ComponentSettings.cs:30-35)。
  5. 水平对齐:宿主内容的 justify-content 取 Settings.HorizontalAlignment,默认 stretch(枚举 Stretch/Left/Center/Right,ComponentSettings.cs:36,选择器 :49-51)。
  6. 内边距:宿主内部 Shimmer 容器 padding: 8px 0(:55-57),但组件自身的横向间距由外层 margin: 0 6px 提供(行内模板 Margin="6 0",MainWindow.axaml:396),两者都要保留,不得合并。
  7. 加载占位:首次布局或配置切换时显示 Shimmer 占位,占位宽度取上次测量宽度 LastWidthCache(默认 100,ComponentSettings.cs:39,ComponentPresenter.axaml:69-70)。占位态不得让行高变化。
  8. 隐藏规则:HideOnRule + HidingRules 满足时组件不可见但不卸载、不释放资源(ComponentSettings.cs:100-123)。
  9. 裁剪:宿主自身 overflow: visible(不对组件内部做裁剪),裁剪交给行的 clip-path(见 4.5.4);但轮播容器内部有 40px 高度钳制(见 5.9)。

5.1.4 组件通用设置项("高级设置"页,所有组件都有)

以下字段存在于每一个 ComponentSettings 上,默认值固定(repos/ClassIsland/ClassIsland.Core/Models/Components/ComponentSettings.cs:23-51),UI 顺序照 ComponentAdvancedSettingsControl.axaml:

字段 默认 UI 控件 范围/步进
IsResourceOverridingEnabled false 开关 + 4 个字号输入 字号 1–40
MainWindowSecondaryFontSize 14 NumberBox 1–40
MainWindowBodyFontSize 16 NumberBox 1–40
MainWindowEmphasizedFontSize 18 NumberBox 1–40
MainWindowLargeFontSize 20 NumberBox 1–40
IsCustomForegroundColorEnabled false 开关 + ColorPicker —
ForegroundColor DodgerBlue ColorPicker —
Opacity 1.0 Slider 0–1,步进 0.05
IsCustomBackgroundColorEnabled false 开关 + ColorPicker —
BackgroundColor Black ColorPicker —
IsCustomBackgroundOpacityEnabled false 开关 + Slider 0–1,步进 0.05
BackgroundOpacity 0.5 Slider 0–1
IsCustomCornerRadiusEnabled false 开关 + Slider 0–20,步进 0.5
CustomCornerRadius 8.0 Slider 0–20
HorizontalAlignment Stretch 4 个图标按钮 左/居中/拉伸/右
IsFixedWidthEnabled / FixedWidth false / 200 开关 + NumberBox —
IsMinWidthEnabled / MinWidth false / 100 开关 + NumberBox —
IsMaxWidthEnabled / MaxWidth false / 300 开关 + NumberBox —
IsCustomMarginEnabled false 开关 —
MarginLeft / MarginTop / MarginRight / MarginBottom 0 / 0 / 0 / 0 4 个 NumberBox —
HideOnRule / HidingRules false / {} 开关 + 规则集编辑器 —
RelativeLineNumber 0 内部字段 —

对齐按钮图标(ComponentAdvancedSettingsControl.axaml:223-232):左=\uE03E、居中=\uE03A、拉伸=\uE034、右=\uE03C。

自定义背景/圆角只有在"组件位于独立岛"(分体主界面)语义下才画得出来;普通行内背景由行承担,此时组件的背景设置不生效——实现必须在 UI 上如实表现(禁用或不渲染),不得偷偷画。

5.1.5 容器组件通用约定

  • 注册时带 [ContainerComponent],宿主 ComponentInfo.IsComponentContainer = true。
  • 子组件集合 = 组件设置对象实现 IComponentContainerSettings.Children(ComponentSettings.cs:143-144);因此 GroupComponentSettings、StackComponentSettings 只有 Children 一个字段(Models/ComponentSettings/GroupComponentSettings.cs、StackComponentSettings.cs)。
  • 容器内部为子组件再套一层 ComponentPresenter,IsOnMainWindow=true、Margin="6 0",并透传 HidingRules/HideOnRule(GroupComponent.axaml:25-29、StackComponent.axaml:24-28、RollingComponent.axaml:36-41、SlideComponent.axaml:114-120)。
  • 容器的子列表外层容器统一 Margin="-6 0"(抵消一次边距),循环嵌套时视觉间距保持 12px 恒定(GroupComponent.axaml:15、StackComponent.axaml:14、RollingComponent.axaml:16、SlideComponent.axaml:22)。
  • 容器内部的子组件条目使用 "components-panel" 面板类:WrapPanel/StackPanel 需要"重排动画"标记;StackPanel 固定 Orientation=Horizontal(repos/ClassIsland/ClassIsland.Core/Themes/ComponentsPanelStyles.axaml:11-16)。

5.1.6 组件高度与"岛"的约束

  • 组件可用高度恒为 40px(行高,见 4.5.2)。所有组件必须在这个高度内垂直居中显示。
  • 宿主的 MaxHeight 受容器约束:轮播容器显式 MaxHeight = IslandContainerHeight(SlideComponent.axaml:119)、编辑态列表项 MaxHeight = IslandContainerHeight(EditableComponentsListBox.axaml:70)。
  • 组件内部若出现高度超过 40px 的内容(如两行文字),必须自行 clamp 或按设计折行;不得撑高行。

5.1.7 时间驱动

除文本/分割线外,所有组件在主计时器 tick 时刷新(ClassIsland 用 ILessonsService.PostMainTimerTicked,DateComponent.axaml.cs:39-40、ClockComponent.axaml.cs:61、CountDownComponent.axaml.cs:142、ScheduleComponent.axaml.cs:119)。实现:

  • 用单一 1 秒节拍(主进程 setInterval(1000) 对齐到整秒边界)+ 事件广播;
  • 需要亚秒/更快的(滚动、轮播、提醒进度)各自用 requestAnimationFrame 或独立定时器,不得把 1 秒节拍改成 16ms 全局广播;
  • 页面不可见或被系统节流时,靠"重新挂载时立即刷新一次"兜底(对应 AttachedToVisualTree 时 UpdateContent())。

5.2 日期(DateComponent)

  • 注册:见 5.1.1。
  • 视觉:一个 TextBlock,垂直居中;文本格式 ddd MM/dd(DateComponent.axaml:11,StringFormat=\{0:ddd MM/dd\})。
    • 中文(zh-CN)渲染示例:周三 11/12;星期用短名(不补"星期"二字),月/日补零两位,斜杠分隔,无空格之外的分隔符(ddd 与 MM/dd 之间有一个空格)。
    • 字号:继承 --font-body = 16px;颜色:继承前景色;字重:继承 --font-weight(默认 500)。
  • 数据:Today = ExactTimeService.GetCurrentLocalDateTime(),每次 tick 刷新(DateComponent.axaml.cs:43-46)。必须使用"精确时间服务"(含 NTP 校正与时间偏移),不得直接用 new Date()。
  • 设置:无。
  • 边界:
    • 跨日 00:00 必须自动更新(不能只在启动时取一次)。
    • 系统时间被手动修改后 1 个 tick 内反映。
    • 语言/区域变化时按 Intl.DateTimeFormat('zh-CN', { weekday:'short' }) 重新渲染,且宽度变化要触发行宽动画(见 4.5.7)。
  • Vue 结构建议:<span class="node node-date">{{ dateText }}</span>,white-space: nowrap; font-variant-numeric: tabular-nums;。

5.3 时钟(ClockComponent)

  • 视觉结构(ClockComponent.axaml:13-41):外层网格水平/垂直居中,font-size = var(--font-emphasized)(18px)。
<div class="node node-clock" data-sep="1" data-seconds="0">
  <span class="clock-full">09:41</span>            <!-- TextBlock1,分隔符闪动用 -->
  <span class="clock-hour">09</span>               <!-- 左对齐 -->
  <span class="clock-minute">41</span>             <!-- 右对齐 -->
</div>
  • 三种显示态由两个类控制(:19-32):
    • TimeSeparatorShowing:clock-full 可见(opacity:1),clock-hour/clock-minute 隐藏;
    • 非 TimeSeparatorShowing:clock-full 隐藏(opacity:0),clock-hour 左对齐 + clock-minute 右对齐显示,二者之间留出分隔符的宽度(用等宽字符宽的占位,或给两个 span 之间固定 gap: <一个字符宽>;Avalonia 里两个 TextBlock 在同一 Grid 单元格内用 Left/Right 对齐实现,中间自然留空)。
    • ShowSeconds:文本格式变为 HH:mm:ss(:28-30)。
  • 分隔符闪动逻辑(ClockComponent.axaml.cs:71-76):
IsTimeSeparatorShowing = !FlashTimeSeparator || ShowSeconds || (当前秒 % 2 == 1)

即:开启闪动且不显示秒时,冒号在奇数秒显示、偶数秒消失(每次消失 1 秒);显示秒时冒号常亮。

  • 数字排版:必须 font-variant-numeric: tabular-nums(等宽数字),否则冒号闪动与秒数变化会让整块宽度跳动(ClassHelper 对时钟也做了同样处理:repos/ClassHelper-master/packages/desktop-client/src/island/ComponentNode.vue:305-308)。
  • 时间源:CurrentTime = Settings.ShowRealTime ? DateTime.Now : ExactTimeService.GetCurrentLocalDateTime()(ClockComponent.axaml.cs:73)。
  • 设置项与默认值(Models/ComponentSettings/ClockComponentSettings.cs):
字段 默认 UI 文案 说明
ShowRealTime false 使用实际时间 显示不经过时间偏移的系统时间
ShowSeconds false 显示秒数 精确到秒
FlashTimeSeparator true 闪动时间分隔符 模拟电子时钟分隔符闪动
  • 边界:
    • 24 小时制,不显示 AM/PM(格式串 HH:mm)。
    • 显示秒时每秒重绘一次;不显示秒时也需每秒 tick 以驱动闪动。
    • 若系统时间不可用(精确时间服务失败)必须回退本地时间,不得显示空白。
  • 验收要点:开启闪动后,肉眼观察冒号 1 秒亮 1 秒灭;开启秒数后冒号常亮且每秒变化;整块数字在切换时不发生水平位移。

5.4 倒计时(CountDownComponent)

最复杂的非容器组件。结构(CountDownComponent.axaml:64-114):

<div class="node node-countdown" data-progress="ring|bar" data-compact="0">
  <!-- 进度环(ProgressBarMode=0 且 ShowProgress 时可见) -->
  <div class="ring">
    <svg width="22" height="22" viewBox="0 0 22 22">
      <circle cx="11" cy="11" r="9.5" stroke="var(--control-strong-stroke-disabled)" stroke-width="2.6" fill="none"/>
      <circle cx="11" cy="11" r="9.5" stroke="var(--accent)" stroke-width="3.1" stroke-linecap="round"
              stroke-dasharray="59.69" stroke-dashoffset="计算值" transform="rotate(-90 11 11)"/>
    </svg>
  </div>
  <span class="connector-prefix">距离</span>        <!-- 非紧凑模式 -->
  <span class="name">倒计时</span>                  <!-- 颜色=FontColor,字号=FontSize -->
  <span class="connector">还有</span>               <!-- 非紧凑模式 -->
  <span class="value">30</span>                     <!-- 颜色=FontColor,字号=FontSize -->
  <!-- 进度条(ProgressBarMode=1 且 ShowProgress 时可见) -->
  <div class="bar-wrap"><div class="bar" style="width: Percent%"></div></div>
</div>

尺寸与样式(逐条,CountDownComponent.axaml:66-112):

  • 外层:横排、gap: 2px、垂直居中。
  • 进度环容器:margin-right: 4px、垂直居中;底环 Ellipse 宽高 21.4px、描边 2.6px、颜色 ControlStrongStrokeColorDisabledBrush;前景弧 Path 宽高 22px、stroke-linecap: round、stroke-width: 3.1、颜色默认 AccentFillColorDefaultBrush,开启"进度条使用强调色"后改为 Settings.FontColor。
  • 弧半径计算必须照抄:控件尺寸 22×22、描边 3、radius = 22/2 - 3/2 = 9.5,起点在正上方(center.Y - radius),顺时针扫过 percent/100*360 度,percent >= 100 时按 99.9999 处理以避免闭合渲染问题,percent <= 0 时不画(CountDownComponent.axaml.cs:32-85)。
  • 文本:"距离" + 名称 + 连接词 + 数值;"距离"与连接词用 connector l-instructive 类(次要色);名称与数值用 Settings.FontColor + Settings.FontSize(默认 #FFFF0000、16px)。
  • 紧凑模式(IsCompactModeEnabled):隐藏"距离"与连接词,只留名称 + 数值(:85, :95)。
  • 底部进度条:Maximum=100、Minimum=0、Value=Percent、贴底、min-width:0、宽度撑满行、高度固定 3px(token --ci-progress-height: 3px,border-radius: 1.5px);实现用 width: Percent% 或 scaleX(:102-112)。
  • 设置项与默认值(Models/ComponentSettings/CountDownComponentSettings.cs,逐字段):
字段 默认 说明
CountDownName "倒计时" 事件名称
CountdownSource 0 0 固定时间 / 1 周期 / 2 今天 / 3 本周
StartTime 今天 0 点 固定时间模式的起点
OverTime 今天 0 点 固定时间模式的终点(日为 0 的那天)
CycleStartTime 当前时刻 周期模式起点
CycleBeforeDuration TimeSpan.Zero 周期前阶段
CycleDuration 1 天 周期长度
CycleAfterDuration TimeSpan.Zero 周期后阶段
IsCycleCountLimited false 限制循环次数
CycleCountLimit 2 循环次数上限(UI 范围 1–33550336)
IsAdvancedCycleTimingEnabled false 启用前后阶段
NatureTimeUseMode 0 0 默认 / 1 强制自然时间 / 2 强制课程时间
WeekCountdownStartDay Monday 本周模式的第一天
IsCustomWeekCountdownStartDayEnabled false 自定义每周第一天(否则取学期开始日的星期)
FontColor #FFFF0000 名称与数值颜色(UI 文案"强调色")
FontSize 16 范围 16–30,步进 1
IsCompactModeEnabled false 紧凑模式
CountDownConnector "还有" 连接词
IsConnectorColorEmphasized false 连接词用强调色
ShowProgress false 显示进度
ProgressBarMode 0 0 进度环 / 1 进度条
UseAccentOnProgressBar true 进度使用强调色(否则用应用主题色)
IsProgressInverted false 进度反相(用"剩余"而非"已过"计算)
CustomStringFormat "%D天" 数值格式化模板
  • 格式化模板替换规则(必须逐 token 实现,CountDownComponent.axaml.cs:268-287):
Token 含义 取整
%D 总天数 向上取整
%H 总小时数 向上取整
%M 总分钟数 向上取整
%S 总秒数 向上取整
%X 总毫秒数 向上取整
%P 已过百分比(P0,如 30%) 四舍五入到 2 位小数后按 P0
%p 已过百分比(P2,如 30.00%) 保留 2 位
%L 剩余百分比(P0) 四舍五入到 2 位
%d 天(元素值) 不取整
%h 小时(元素值) 不取整
%m 分钟(元素值) 补两位
%s 秒(元素值) 补两位
%x 毫秒(元素值) 补三位

中国区必须用不变文化(InvariantCulture)格式化数字,避免千分位。delta < 0 时钳制为 0(:260-264)。

  • 四种时间源的时间区间计算(必须照抄,CountDownComponent.axaml.cs:166-247):
    1. 固定时间:(StartTime, OverTime)。
    2. 周期:total = before + duration + after;cycles = floor((now - cycleStart)/total)(启用次数限制时再取 min(cycles, limit));start = cycleStart + cycles*total + before;end = start + duration。total <= 0 时退化为 (cycleStart, cycleStart)。
    3. 今天:NatureTimeUseMode==1 → (今天0点, 明天0点);否则取当天时间表中第一个"上课/课间"时间点的开始 → 最后一个"上课/课间"时间点的结束;课程不可用时按 NatureTimeUseMode==2 退化为 (DateTime.MinValue, DateTime.MinValue)(不显示倒计时),否则退化为自然日。
    4. 本周:按 WeekCountdownStartDay(或学期开始日的星期)求出本周起点;NatureTimeUseMode==1 → 周起点到下周起点+1 天;否则遍历 7 天找第一个有课的日子与最后一个有课的日子,取其首/末时间点拼成区间;课程全无时按模式 2 退化或自然周。
  • 进度百分比(:288-295):value = (IsProgressInverted ? end - now : now - start).TotalSeconds;totalSeconds <= 0 时 Percent = 0;开启进度精度降低(主界面全局设置 ReduceProgressAccuracy 默认 true)时 progressTick = max(10, totalSeconds/500),Percent = round(value/progressTick)*progressTick / totalSeconds * 100;否则 progressTick = 1。
  • 边界:
    • 终点在过去 → 数值全 0、Percent=0、进度环不画。
    • 起点在未来(固定时间模式配错)→ Percent 为负,必须钳到 0。
    • 名称/连接词为空字符串时不得留出空 span 的间距(用 display:none 而非空文本)。
    • 字号变化(16–30)时进度环/进度条不跟随放大,只放大文本。
  • Vue 结构建议:CountDownNode.vue props = { settings, now },内部 computed 出 remaining、percent、text;进度环用 SVG circle + stroke-dashoffset = C * (1 - percent/100),C = 2π*9.5 ≈ 59.69。

5.5 课程表(ScheduleComponent)

课程表是"一行多条课程"的核心组件,内部由 LessonsListBox(repos/ClassIsland/ClassIsland.Core/Controls/LessonsControls/LessonsListBox.axaml)渲染一组课程控件。

5.5.1 外层结构

<div class="node node-schedule" data-tomorrow="0" data-empty="none">
  <div class="tomorrow-badge">明天</div>     <!-- 条件可见 -->
  <ul class="lessons">                       <!-- LessonsListBox#MainLessonsListBox -->
    <li class="lesson-item">…课程控件…</li>
    <li class="lesson-item">…</li>
  </ul>
  <ul class="lessons lessons-tomorrow">…</ul> <!-- LessonsListBox#TomorrowLessonsListBox -->
  <span class="empty-placeholder">今天没有课程。</span>
</div>
  • 外层 Grid 两列 Auto, *(ScheduleComponent.axaml:78)。
  • "明天"标记:margin-right: 2px、垂直居中、背景 AccentFillColorDefaultBrush、内边距 8px 2px、border-radius: 16px(药丸)、文本"明天"、前景 TextOnAccentFillColorPrimaryBrush、文本 margin: 0 1px -1px(微调基线)(:79-89)。
  • 课程列表容器:height: 40px、水平 StackPanel、ScrollViewer.VerticalScrollBarVisibility=Disabled(LessonsListBox.axaml:41-50)。
  • 每个列表项容器(LessonsListBox.axaml:100-136):background: ListViewItemBackground、border-radius: var(--control-corner-radius)(FluentAvalonia 控制圆角,默认 4【推断:FluentAvalonia ControlCornerRadius 默认 4】)、padding: 0、max-height: 40px、min-width: 0、内容纵横都 stretch;:disabled 时 opacity: 0.5;:pointerover/:pressed/:selected 各自换背景色(默认鼠标穿透,实际见不到,但编辑/演示态仍要正确)。
  • 已结束课程淡化:opacity: 0.6,transition: opacity 150ms cubic-bezier(0.645, 0.045, 0.355, 1)(CubicEaseInOut,LessonsListBox.axaml:56-77)。开关 FadeCompletedClasses(默认 true)。

5.5.2 三种课程控件

(a) 展开型 LessonControlExpanded(时间点为"上课"时显示,LessonControlExpanded.axaml):

<div class="lesson lesson-expanded">
  <span class="spacer" style="width:16px * ScheduleSpacing"></span>
  <span class="subject"><em>语文</em></span>          <!-- HighlightBox 包裹 -->
  <span class="extra">                              <!-- margin-left:6px,l-secondary -->
    <span class="detail">08:00-08:40</span>          <!-- 依 ExtraInfoType 切换 -->
  </span>
  <span class="spacer" style="width:16px * ScheduleSpacing"></span>
  <div class="progress"><i style="transform:scaleX(ProgressPercent)"></i></div>
</div>
  • 左右各一个 16px 间隔,宽度按 ScheduleSpacing 水平缩放(ScaleTransform ScaleX,:51-56, 219-224);ScheduleSpacing 默认 1,UI 范围 0.2–1(ScheduleComponentSettingsControl.axaml:156),因此左右各 3.2–16px。
  • 科目名:font-size = var(--font-emphasized)(18px)、font-weight: bold,包在 HighlightBox 里(:65-68)。
  • 附加信息区:margin: 6px 0 0 0、类 l-secondary(次要色、font-size = var(--font-secondary) = 14px)、贴底(:72-84)。
  • ExtraInfoType(ShowExtraInfoOnTimePoint=true 时生效)6 种(:85-171):
    • 0:开始时间-结束时间,格式 hh:mm(如 08:00-08:40);
    • 1:已过时间,格式 -mm:ss(秒数向上/向下取整见转换器);
    • 2:剩余时间,格式 -mm:ss;
    • 3:进度百分比;
    • 4:模糊倒计时,-<1min 形式(用 CountdownSeconds 向上取整);
    • 5:精确倒计时,-mm:ss。
  • 当前课程的"剩余时间药丸"(IsCountdownEnabled 且 MasterTabIndex==1 时):背景 AccentFillColorTertiaryBrush + opacity: 0.3 的底 + 1px 边框 AccentFillColorDefaultBrush + padding: 8px 2px + 圆角 = 高度的一半(药丸);文本 font-size = var(--font-body)(16px),普通模式 -mm:ss,非精确倒计时模式 <<countdownSeconds(:173-214)。
  • 课程进度条:贴底、Maximum=1、Value=ProgressPercent、min-width:0、宽度撑满、高度固定 3px(--ci-progress-height)(:227-236)。
  • 换课高亮:ChangedClass && HighlightChangedClass(设置项 HighlightChangedClass 默认 false)时 HighlightBox.IsHighlight = true(:30-32),即科目名后出现强调色下划色块。

(b) 最小型 LessonControlMinimized(TimeLayoutItem.IsHideDefault 或"上课时仅显示当前课程"等场合):只显示科目名首字(Subject.Initial),font-size = var(--font-emphasized),左右各 10px * ScheduleSpacing 间隔(LessonControlMinimized.axaml:30-64)。

(c) 分割线 LessonControlSeparator(时间点为"分割线/Action"时):一条 2px 竖线、高 25px、颜色 TextFillColorSecondaryBrush、opacity: 0.8,左右各 10px * ScheduleSpacing(LessonControlSeperator.axaml:8-26)。注意:分割线是每个时间点自己的一项,不合并。

HighlightBox 的高亮条规格:background: var(--accent)、opacity: 0.4、高 12px、垂直居中、margin: -2px 8px -2px 0、border-radius: 2px(repos/ClassIsland/ClassIsland.Core/Controls/HighlightBox.axaml:15-18)。

5.5.3 哪些时间点显示(必须逐条实现)

对每个 TimeLayoutItem(ScheduleComponent.axaml.cs:243-251):

  1. TimeType ∈ {2 分割线, 3 行动} 不显示【注意:分割线类型由专门的 LessonControlSeparator 渲染,判定顺序以源码为准】;
  2. HideFinishedClass=true 且 EndTime < now → 不显示;
  3. IsHideDefault=true 且不是当前选中项 → 不显示;
  4. TimeType==1(课间)且不是当前选中项 → 不显示;
  5. ShowCurrentLessonOnlyOnClass=true 且当前选中项是"上课"类型且该项不是当前项 → 不显示。

ShowCurrentLessonOnlyOnClass 的取值来自"附加的课程控件设置"(LessonControlAttachedSettings,优先级链:附加设置 → 组件设置)或组件设置本身(ScheduleComponent.axaml.cs:207-215)。

5.5.4 空状态、明天课表与"放学后"

  • 空状态占位符(ScheduleComponent.axaml:26-57, 105-107):
    • 今天没有课表(LessonsService.CurrentClassPlan == null)或(显示明天课表时明天也没有)→ 显示 PlaceholderTextNoClass,默认 "今天没有课程。";
    • 今日课程全部结束且 HideFinishedClass=true(AfterSchool 状态)→ 显示 PlaceholderTextAllClassEnded,默认 "今日课程已全部结束。";
    • 两者都受 ShowPlaceholderOnEmptyClassPlan(默认 true)控制;垂直居中、font-size = var(--font-body)。
    • 占位符与课程列表互斥显示(不重叠)。
  • 明天课表(TomorrowScheduleShowMode,默认 1,:166-172, 59-76):
    • 0 不显示;
    • 1 放学后显示;
    • 2 总是显示;
    • 3 今天无可显示课程时显示。 显示时:左侧出现"明天"药丸、主列表隐藏、明天列表显示;明天课表无数据时退回占位符逻辑。
  • 放学后(AfterSchool)= 当前时间状态为"放学后"或今天没有课表(:188-193)。

5.5.5 课程表设置项与默认值(LessonControlSettings)

(repos/ClassIsland/ClassIsland/Models/ComponentSettings/LessonControlSettings.cs:8-23,与全局设置同名项默认值一致:Models/Settings.cs:103-108, 208-209, 221)

字段 默认 UI 文案 说明
ShowExtraInfoOnTimePoint true 显示时间点附加信息
ExtraInfoType 0 要显示的附加信息 0–5,见 5.5.2
IsCountdownEnabled true 时间点结束倒计时
CountdownSeconds 60 时间点结束倒计时时长 秒
IsNonExactCountdownEnabled false 使用模糊倒计时 显示 <<数值
ExtraInfo4ShowSecondsSeconds 0 — 【预留】保留 schema、不给 UI
ScheduleSpacing 1 课程表文本间距 0.2–1,影响左右间隔
ShowCurrentLessonOnlyOnClass false 上课时仅显示当前课程
HideFinishedClass false 不显示已结束的课程
FadeCompletedClasses true 淡化上过的课程 透明度 0.6
HighlightChangedClass false 标记换课课程
ShowPlaceholderOnEmptyClassPlan true 无课程时显示占位符
PlaceholderTextNoClass "今天没有课程。" 当日没有课程的占位符内容
PlaceholderTextAllClassEnded "今日课程已全部结束。" 当日课程结束的占位符内容
ShowTomorrowSchedules false — 【预留】保留 schema、不给 UI(语义已被 TomorrowScheduleShowMode 取代)
TomorrowScheduleShowMode 1 显示明天课表 0–3

"时间点结束倒计时"只在"当前时间点剩余时间 ≤ CountdownSeconds"时出现(药丸),否则不显示。

5.5.6 边界

  • 课表里有 20 节课且全部展开时,一行放不下:课程表组件不换行、不滚动(ScrollViewer 禁用),超出部分被行裁切。必须在设置里引导用户启用"不显示已结束的课程"或"仅显示当前课程"。
  • 时间点为 null(无课表)时不得抛异常,走占位符分支。
  • ScheduleSpacing 为 0.2 时左右间隔各 3.2px,视觉上仍要能区分相邻课程。
  • 课程名过长:科目名 text-overflow: ellipsis(ClassHelper 同样处理,ComponentNode.vue:322-327),但不要截断到只剩 1 个字符。

5.6 天气简报(WeatherComponent)

结构(WeatherComponent.axaml:50-313):横排 StackPanel、Spacing 6、垂直居中、子项居中。

5.6.1 主天气信息(MainWeatherInfoKind,默认 0,共 6 种)

值 内容 图标/尺寸 文本
0 天气图标 + 温度 WeatherPackIconControl 高 22px 温度值 + 单位,margin-left:6px
1 湿度 FluentIcon \uE794,22×22 湿度值 + 单位
2 风 FluentIcon \uF43B 22×22 + FluentIcon \uE196 22×22(按风向角旋转,transform-origin: center,margin-left:6px) 风速值 + 单位
3 AQI FluentIcon \uEA7D 22×22(颜色随 AQI 等级 1–6 变) AQI 数值
4 气压 FluentIcon \uE8E0 22×22 气压值 + 单位
5 体感温度 FluentIcon \uF1B3 22×22 体感值 + 单位
  • 所有 ci|FluentIcon 统一 margin: 0 -2px 0 0(WeatherComponent.axaml:42-44),用于视觉居中补偿。
  • 温度/数值文本继承 --font-body(16px)。
  • AQI 等级配色:AqiLevel 1–6 分别用 AqiLevel1Brush … AqiLevel6Brush(主题资源,:22-41),等级来自 Settings.LastWeatherInfo.Aqi.AqiLevel(WeatherComponent.axaml.cs:47-50)。实现时把 6 个颜色做成 CSS 变量 --aqi-1…--aqi-6,按等级切 data-aqi。

5.6.2 气象预警(ShowAlerts,默认 true)

  • 容器:WrapPanel(带重排动画)、vertical-align:center、overflow:visible;visible = ShowAlerts && 预警数 > 0(:186-198)。
  • 每种预警有两种形态,由 AlertsTitleShowMode 决定(默认 1):
    • 0 = 总是图标:只显示 22×22 图标;
    • 1 = 默认:有自定义图标的预警显示图标,没有的显示全文药丸(IsDefaultIcon 判定);
    • 2 = 总是全文。
  • 图标形态:<img width="22" height="22" style="margin-left:2px">(:246-251)。
  • 全文形态(:252-263):
.alert-pill {
  display: grid; grid-template-columns: 1fr 1fr;
  margin-left: 2px;
}
.alert-pill .pill-bg {
  grid-column: 1 / span 2;
  background: var(--control-fill-default);
  border-radius: 11px;          /* 高度 22 的整圆 */
}
.alert-pill img { width: 22px; height: 22px; }
.alert-pill .type {
  margin: 4px 8px 0 4px;        /* Margin="4 0 8 0" 水平方向 */
  font-size: var(--font-secondary);  /* 14px */
  align-self: center;
}
  • 预警图标由后端 Images[icon] 提供(可能是 GIF/PNG),必须支持动态图与失败兜底(失败时显示感叹号图标,不得整块消失)。

5.6.3 降水提示(ShowRainTime,默认 true)

  • 仅当 雨剩余分钟数 > 0 时显示(:268-275)。
  • 双层药丸(:276-311):
.rain { position: relative; display: flex; align-items: center; padding: 0 8px; height: 22px; }
.rain .bg { position: absolute; inset: 0; background: var(--rainy-color); opacity: 0.15; border-radius: 11px; }
.rain .ring { position: relative; border: 1px solid var(--rainy-color); border-radius: 11px; padding: 0 8px;
              display: flex; align-items: center; }
.rain .icon { font-size: var(--font-secondary); margin-right: 4px; }   /* LucideIcon \uE092 */
.rain .text { font-size: var(--font-secondary); font-weight: 400; }    /* 14px */
  • 文本两态:IsFuzzyLongRainCountdownEnabled=true(默认)→ 用"约 X 分钟/约 X 小时"的模糊文案(MinutesToApproxTimeConverter);false → 精确 {n}min(:296-308)。
  • 药丸圆角 = 自身高度的一半(CornerRadius = SizeDoubleToCornerRadiusConverter(Bounds.Height))。

5.6.4 设置项与默认值(WeatherComponentSettings)

字段 默认 UI 文案
ShowMainWeatherInfo true 显示天气信息
MainWeatherInfoKind 0 主信息类型(6 选 1)
ShowAlerts true 显示预警信号
AlertsTitleShowMode 1 显示预警类型(0 总是图标/1 默认/2 总是全文)
ShowRainTime true 显示降水提示
IsFuzzyLongRainCountdownEnabled true 模糊长时间降雨倒计时时间显示精度

5.6.5 天气图标模板

全局设置 Settings.WeatherIconId,默认 "classisland.weatherIcons.lucide"(Models/Settings.cs:1849)。内置 4 套(App.Services.xaml.cs:287-290):

ID 名称
classisland.weatherIcons.lucide Lucide(默认)
classisland.weatherIcons.fluentDesign Fluent Design
classisland.weatherIcons.sfSymbols SF Symbols
classisland.weatherIcons.simpleText 纯文本
  • 纯文本模板的渲染规则(Controls/WeatherIcons/SimpleTextWeatherIconTemplate.axaml):若该图标用于"到"(Tag='To'),前置一个 ~ 分隔符,再显示 WeatherName;否则只显示 WeatherName,水平居中。
  • 实现:把这 4 套模板做成可注册的"图标渲染器"(返回 SVG 或文本节点),weatherPackIcon 组件按 WeatherIconId 分发;未知 ID 必须回退到 lucide。

5.6.6 边界

  • 天气数据为空/加载失败:不显示温度与图标(不得显示 0° 这种假数据);预警与降水区整体隐藏;组件不得撑高。
  • 天气数据过期(stale)时仍显示旧数据,但可加 opacity: 0.7【推断:ClassIsland 未做此处理,属可选增强】。
  • 预警可能同时 3 条以上,WrapPanel 在 40px 内只放得下一行,多余项被行裁切;必须按"最多显示 N 条 + 更多"策略实现【推断:ClassIsland 未裁剪,依赖用户自行配置】。

5.7 文本(TextComponent)

  • 视觉:单个 TextBlock,垂直居中,font-size = Settings.FontSize(:18-28)。
  • 设置项与默认值(Models/ComponentSettings/TextComponentSettings.cs):
字段 默认 UI 文案
TextContent "" 文本内容
UseCustomFontColor true 自定义字体颜色
FontColor #FFFFFF 颜色选择器
FontSize 16 字体大小,范围 16–30、步进 1
  • 颜色仅当 UseCustomFontColor=true 时生效,否则继承前景色(宿主/行/全局)。
  • 边界:内容为空时不占宽度(display:none 或空 span),避免行内留出 12px 空隙;内容超长时按 max-width 约束(如果启用了最大/固定宽度)并 ellipsis。
  • 支持多行文本?ClassIsland 只有一个 TextBlock 且行高 40,因此必须换行时用 \n 也只会被裁切;实现上禁止插入换行,输入框遇到换行符替换为空格。

5.8 滚动容器(RollingComponent)

  • 语义:内容宽于容器时,内容横向匀速滚动;可定时暂停、可按规则停止。
  • 结构(RollingComponent.axaml:14-48):外层 Border(overflow: visible、margin: 0 -6px)→ Canvas(宽 = 内容 ItemsControl 宽、左对齐)→ Grid(grid-template-columns: auto 16px auto)→ ItemsControl(子组件横排)。
  • 滚动动画(关键公式,RollingComponent.axaml.cs:85-135):
innerW  = 内容自然宽度
outerW  = 容器宽度
仅当 outerW < innerW 才滚动

pausePos    = (!IsPauseEnabled || PauseOffsetX > innerW) ? 0 : PauseOffsetX
durSeconds  = innerW / clamp(SpeedPixelPerSecond, 1, +∞)
pauseSeconds= IsPauseEnabled ? PauseSeconds : 0
backSeconds = outerW / clamp(SpeedPixelPerSecond, 1, +∞)
totalSeconds= durSeconds + pauseSeconds + backSeconds

关键帧(全部 LinearEasing,IterationBehavior=Forever):
  t=0                                    X = 0
  t=pause/total                          X = -pausePos
  t=(pause+dur)/total                    X = -(innerW + pausePos)
  t=(pause+dur)/total (同一时刻跳切)    X = outerW
  t=1                                    X = 0

即:先停 pauseSeconds(若 PauseOffsetX>0 则先移到 -PauseOffsetX),再匀速把内容推到完全离开,瞬间跳回右侧,再匀速推回原位。Duration = pauseSeconds + durSeconds(:108)——注意 Aughor 原实现里 anim.Duration 只含前两段,靠 t=1 关键帧兜住回程;实现时必须按"总时长 = pause+dur+back"重新归一化时间轴,否则回程速度会与去程不一致(这是源码中的已知近似,实现时应修正以保证视觉匀速)。

  • 规则停止:StopOnRule && 规则满足 → 停止动画并立刻回到初始位置(visual.Offset.X = -pausePos),并写一个 1 秒的"空动画"把 Offset 固定住防止被其他动画拖走(:137-157, 171-180)。
  • 设置项与默认值(Models/ComponentSettings/RollingComponentSettings.cs):
字段 默认 UI 文案 范围
SpeedPixelPerSecond 40.0 滚动速度 ≥0.1 px/s
IsPauseEnabled true 定时暂停
PauseSeconds 10.0 暂停时长 0–最大秒数
PauseOffsetX 0.0 暂停位置 ≥0 px
PauseOnRule false — 【预留】保留 schema、不给 UI(暂停由 5.9 轮播容器的规则承担)
PauseRule {} — 【预留】保留 schema、不给 UI
StopOnRule false 满足规则时停止
StopRule {} 规则集
  • 提示 InfoBar(首次使用显示):"滚动组件仅在组件内容超出滚动组件宽度时才会开始滚动。您可能需要在【高级设置】中为本组件设置【最大宽度】或【固定宽度】……"(RollingComponentSettingsControl.axaml:22-24,IsRollingComponentWarningVisible 默认 true,Models/Settings.cs:1850)。
  • 边界:内容未超出时完全不滚动(不得出现 1px 抖动);SpeedPixelPerSecond 极小时按 ≥1 px/s 计算以免除零;子组件全部隐藏时宽度为 0,应停止滚动;窗口失焦/系统节流导致帧不连续时不得跳帧。

5.9 轮播容器(SlideComponent)

  • 语义:多个子组件共用一块区域,按间隔轮换显示;支持动画过渡。
  • 结构(SlideComponent.axaml:17-125):ListBox(margin: 0 -6px、overflow: visible、水平滚动禁用、ItemsPanel=Grid(所有项叠在同一格))。
  • 未启用动画(IsAnimationEnabled=false,默认):非选中项 display:none(:51-53),只显示当前项。
  • 启用动画(:54-108):
.slide-item { position: relative; }
/* 进入 */
.slide-item.is-selected .content {
  animation: slide-in 250ms cubic-bezier(0.65, 0, 0.35, 1) both;
}
@keyframes slide-in {
  0%   { opacity: 0; transform: translateY(-40px); visibility: visible; }
  100% { opacity: 1; transform: translateY(0); }
}
/* 离开 */
.slide-item:not(.is-selected) .content {
  animation: slide-out 250ms cubic-bezier(0.65, 0, 0.35, 1) both;
}
@keyframes slide-out {
  0%   { opacity: 1; transform: translateY(0); visibility: visible; }
  100% { opacity: 0; transform: translateY(40px); visibility: hidden; }
}

源码里两段动画都是 Duration=250ms、KeySpline="0.65, 0, 0.35, 1"(:72-107),进入位移 -40 → 0、离开位移 0 → +40,透明度 0↔1 同步;离开结束后 IsVisible=false。

  • 轮播调度(SlideComponent.axaml.cs:51-121):
    • 间隔 = Settings.SlideSeconds(默认 15,UI 最小 0.1);
    • 子组件数 ≤ 1 时不轮播,恒显示第 0 个;
    • SlideMode:0 循环(+1 取模)、1 随机(预生成打乱队列,用尽再洗牌,保证一轮内不重复)、2 往复(到边界反向);
    • 跳过被隐藏规则的子组件(循环查找可见项,全部隐藏时停止);
    • 规则:IsPauseOnRuleEnabled 满足 → 暂停定时器;IsStopOnRuleEnabled 满足 → 停止定时器并把索引归 0;两者同时满足时"停止"优先。
  • 每项 MaxHeight = 40px(:119),ComponentPresenter Margin="6 0"、VerticalAlignment=Stretch。
  • 设置项与默认值(Models/ComponentSettings/SlideComponentSettings.cs):
字段 默认 UI
SlideSeconds 15 轮换间隔(秒,≥0.1)
SlideMode 0 轮换模式(循环/随机/往复)
IsPauseOnRuleEnabled false 满足规则时暂停
PauseRule {} 规则集
IsStopOnRuleEnabled false 满足规则时停止
StopRule {} 规则集
UseOldPresentingBehavior false 【预留】保留 schema、不给 UI(旧版呈现行为的兼容开关)
IsAnimationEnabled false 启用动画
  • 【预留】字段处置:UseOldPresentingBehavior、滚动容器的 PauseOnRule/PauseRule、课程表的 ExtraInfo4ShowSecondsSeconds/ShowTomorrowSchedules 均保留 JSON schema 与默认值、迁移时原样写回,但不提供 UI 开关。实现不得删除这些字段(会破坏旧配置反序列化),也不得为其臆造设置项。

  • 边界:切换时不能让行高变化(所有项都叠在同一格,容器高度取最大者,且被 MaxHeight 钳制);只有一个子组件时不产生任何过渡动画。

5.10 分组容器(GroupComponent)

  • 语义:把多个组件横向组合成一个整体(用于让它们共享一个"宽度固定/对齐"的约束)。
  • 结构(GroupComponent.axaml:13-33):ItemsControl + StackPanel(Orientation=Horizontal,类 components-panel),margin: 0 -6px,子项 ComponentPresenter Margin="6 0"。
  • 与"行直接放组件"的唯一区别:分组容器可以整体设置宽度/对齐/不透明度/背景/圆角,内部子组件单独再设各自的覆盖项。
  • 设置:只有 Children(GroupComponentSettings.cs)。
  • 边界:空分组不占宽度(自动塌缩为 0,不留 12px 空隙)。

5.11 堆叠容器(StackComponent)

  • 语义:多个子组件重叠在同一位置显示(用于"层叠"效果,例如文字 + 背景图标)。
  • 结构(StackComponent.axaml:12-32):ItemsControl + Panel(类 components-panel),margin: 0 -6px;Panel 的布局语义是"所有子元素占据同一矩形,各自按自身对齐方式摆放"。
  • CSS 实现:
.stack-panel { position: relative; display: block; }
.stack-panel > .component-host {
  position: absolute; inset: 0;             /* 所有子项同一矩形 */
  display: flex;                            /* 由子项自身 justify/align 决定落点 */
  align-items: center;
  margin: 0 6px;
}
  • 尺寸:容器宽高取所有子项测量值的最大者(Avalonia Panel 的行为);行高仍是 40px。
  • 设置:只有 Children。
  • 边界:子项之间不得互相拦截命中(普通模式全窗穿透,无影响);子项数量多时不要改变容器宽度以外的布局。
  • 与轮播容器的区别要写进文档与 UI 提示:轮播是"不同时刻显示不同子项",堆叠是"同一时刻全部显示"。

5.12 分割线(SeparatorComponent)

  • 视觉(SeparatorComponent.axaml:10-14):
.node-separator { width: 2px; display: flex; align-items: center; justify-content: center; height: 100%; }
.node-separator .line { width: 2px; height: 25px; background: var(--text-fill-secondary); opacity: 0.8; }
  • 源码用 Grid Width="2" 包一条 Line:StrokeThickness=2、StartPoint 0,0 → EndPoint 0,25、Stroke=TextFillColorSecondaryBrush、Opacity=0.8、垂直/水平居中。
  • 设置:无(只有通用设置)。
  • 边界:位于行首/行尾时仍然占 2px + 两侧各 6px = 14px,不做特殊收边(与 ClassIsland 一致)。

5.13 组件设置 UI(组件设置页 + 高级设置页)

抽屉标题栏与页签见 4.8.6。两页内容:

页 1「组件设置」:渲染组件自己的 SettingsType 控件(ComponentPresenter 以 IsPresentingSettings=true 渲染,EditModeView.axaml:287-290),即本文每个组件的"设置项"清单。

页 2「高级设置」:按下面顺序渲染(repos/ClassIsland/ClassIsland.Core/Controls/ComponentAdvancedSettingsControl.axaml):

  1. 按规则隐藏(开关 + 说明"注意此处的隐藏不会释放组件资源");
  2. 不透明度(Slider 0–1,步进 0.05);
  3. 自定义背景颜色(开关 + ColorPicker);
  4. 自定义背景不透明度(开关 + Slider);
  5. 自定义背景圆角半径(开关 + Slider 0–20,步进 0.5,文案"调至最大数值时左右边缘为半圆");
  6. 字体大小(总开关 IsResourceOverridingEnabled + 四个 NumberBox:次级/正文/强调/大号,范围 1–40,ci:Field 宽 175、后缀 px);
  7. 字体颜色(开关 + ColorPicker);
  8. 对齐方式(4 个图标按钮:左 \uE03E、居中 \uE03A、拉伸 \uE034、右 \uE03C);
  9. 固定宽度(开关 + NumberBox);
  10. 宽度限制(两个开关 + 两个 NumberBox:最大/最小);
  11. 自定义边距(开关 + 4 个 NumberBox:左/上/右/下)。

ci:Field 后缀单位固定为 px;字号输入框的 SpinButtonPlacementMode=Inline、Minimum=1、Maximum=40(EditModeView.axaml:418-427 的等价写法)。

5.14 空数据与异常兜底(逐组件)

场景 必须表现
课表为空 / 无今日课表 课程表显示 PlaceholderTextNoClass(默认"今天没有课程。");不得显示空白胶囊
今日课程全部结束 显示 PlaceholderTextAllClassEnded(默认"今日课程已全部结束。"),或按设置隐藏(HideFinishedClass)
天气数据缺失 天气组件隐藏温度/图标;只显示可用的预警/降水;不显示假值
天气图标加载失败 该预警显示"图裂"占位(红色背景 + 警告图标),不得整块消失
倒计时区间非法(end<start) Percent=0、数值全 0、进度环不画
倒计时区间为 0 长度 同上,不除零
计时服务不可用 回退本地时间(时钟、日期、倒计时全部回退)
滚动内容未溢出 不滚动、不抖动
轮播只有 1 个子项 不轮播、不播过渡动画
子组件全部被规则隐藏 容器塌缩为 0 宽,可行则整行隐藏(IsAllComponentsHid)
设置对象为 null / 字段缺失 回退到该字段默认值(JSON 反序列化时补默认,不得抛异常)
组件 ID 在注册表中找不到 渲染"缺失组件"占位:显示 NameCache(缓存名)或 ID,图标用警告图标,宽度 100px;不得崩溃、不得吞掉整行

5.15 Vue 组件结构与 CSS 变量

必须采用下面这套文件结构(与 ClassHelper 的 ComponentNode.vue 思路一致,repos/ClassHelper-master/packages/desktop-client/src/island/ComponentNode.vue:212-293):

src/island/components/
  ComponentHost.vue        // 宿主:宽度/对齐/透明度/隐藏/占位
  registry.ts              // ID → { name, icon, description, container, component, settingsSchema, defaults }
  nodes/
    DateNode.vue
    ClockNode.vue
    CountDownNode.vue
    ScheduleNode.vue  (+ LessonNode.vue / LessonSeparatorNode.vue)
    WeatherNode.vue
    TextNode.vue
    RollingNode.vue
    SlideNode.vue
    GroupNode.vue
    StackNode.vue
    SeparatorNode.vue
  ContainerNode.vue        // 通用容器包装,按 kind 分发

统一 props 约定:{ node: ComponentSettings, state: IslandState, now: number, schedules, timePoints };所有节点根元素类名固定为 component-node node-<kind>(与 ClassHelper 一致,便于主题覆盖与自动化断言)。

必须暴露的 CSS 变量(在 :root 与组件节点上均可覆盖)。规范 token 名以 part 3 的 --ci-* 为准,下表左侧为本文别名,落地时按右侧映射:

本文别名 规范 token 值
--font-secondary --ci-fs-secondary 14px
--font-body --ci-fs-body 16px
--font-emphasized --ci-fs-emphasized 18px
--font-large --ci-fs-large 20px
--text-fill-primary --ci-text-1 0.92 白
--text-fill-secondary --ci-text-2 0.72 白
--accent --ci-accent / 填充 --ci-accent-fill 默认 #0a84ff(用户强调色 DodgerBlue)
--control-fill-default --ci-fill-secondary 0.10 白
--control-strong-stroke-disabled --ci-stroke-control 0.30 白
--rainy-color --ci-weather-rain 降雨色
--aqi-1 … --aqi-6 --ci-aqi-1 … --ci-aqi-6 AQI 六级色
--node-gap --ci-component-gap 12px
进度条高度 --ci-progress-height 3px(倒计时进度条、课程进度条共用)
高亮条 --ci-accent-fill + opacity 0.4 —
:root {
  --font-secondary: 14px;
  --font-body: 16px;
  --font-emphasized: 18px;
  --font-large: 20px;
  --text-fill-primary: rgba(255,255,255,0.92);
  --text-fill-secondary: rgba(255,255,255,0.72);
  --accent: #0a84ff;                 /* 用户强调色,默认 DodgerBlue/Apple 蓝 */
  --control-fill-default: rgba(255,255,255,0.10);
  --control-strong-stroke-disabled: rgba(255,255,255,0.30);
  --rainy-color: #4a9eff;
  --aqi-1: #6fdd8b; --aqi-2: #ffd666; --aqi-3: #ff9f0a; --aqi-4: #ff6961; --aqi-5: #c77dff; --aqi-6: #b23a48;
  --node-gap: 12px;                  /* = 两个 6px 外边距之和 */
}

组件节点通用样式:

.component-node {
  display: flex;
  align-items: center;      /* 所有组件在 40px 行内垂直居中 */
  min-width: 0;
  flex-shrink: 0;
  white-space: nowrap;
}
.component-node.text, .component-node.schedule { white-space: normal; }

5.16 验收标准

  1. 首次启动生成 1 行 2 组件(日期 + 课程表),日期显示形如 周三 11/12,课程表显示当天课程或占位符。
  2. 11 个内置组件全部能从组件库拖入主界面,拖入后立刻可编辑、可删除,删除后无残留占位。
  3. 时钟:显示 HH:mm;开启闪动后冒号 1 秒亮 1 秒灭;开启秒数后变 HH:mm:ss 且冒号常亮;数字等宽,切换时不水平抖动。
  4. 倒计时:4 种时间源(固定时间/周期/今天/本周)各配一遍,数值、进度环、进度条、连接词显示正确;%D/%H/%M/%S/%X/%P/%p/%L/%d/%h/%m/%s/%x 全部 token 输出与源码规则一致(尤其 %m/%s/%x 的补零与向上取整语义)。
  5. 倒计时进度环从 12 点方向顺时针填充,满 100% 时不出现缺口;紧凑模式只显示"名称 + 数值"。
  6. 课程表:当前课程高亮、已结束课程淡化到 0.6、剩余时间药丸在阈值内出现;HideFinishedClass / ShowCurrentLessonOnlyOnClass / FadeCompletedClasses / HighlightChangedClass 各自开关验证;4 种"显示明天课表"模式行为正确。
  7. 课程表空状态:无课表显示"今天没有课程。";全部结束显示"今日课程已全部结束。";ShowPlaceholderOnEmptyClassPlan=false 时两者都不显示。
  8. 天气:6 种主信息逐一切换显示正确;有预警时按"总是图标/默认/总是全文"三种模式渲染;降水药丸在 RainRemainingMinutes>0 时出现、=0 时消失;模糊模式显示"约 X 分钟"、精确模式显示"Xmin"。
  9. 文本:字号 16 与 30 时高度都不超过 40px;自定义颜色生效/关闭后回退;空文本不占宽。
  10. 滚动容器:内容超出时按 SpeedPixelPerSecond 匀速滚动,PauseSeconds 停顿位置等于 PauseOffsetX;内容未超出时静止;命中"停止规则"后回到初始位置。
  11. 轮播容器:SlideSeconds 到点切换;循环/随机/往复三种模式各验证一轮;开启动画时切换有 250ms 上下位移淡入淡出,关闭时无过渡;只有 1 个子项时不轮播。
  12. 分组容器的子项横向排列,间距恒定 12px;堆叠容器的子项完全重叠在同一矩形且各自按对齐方式摆放;分割线为 2×25px、透明度 0.8。
  13. 所有组件的"高级设置"项生效:固定宽度、最大/最小宽度、对齐、不透明度、自定义边距、字号覆盖、字体颜色覆盖;隐藏规则勾选后组件消失但在编辑模式显示斜条纹占位。
  14. 组件 ID 不存在时显示"缺失组件"占位而不是白屏或崩溃。
  15. 全部组件在 40px 行高内垂直居中,无任何组件把行撑高超过 40px。