- 重构版主提示词(14 parts,面向 ClassHelper 灵动岛重构) - ClassIsland 全量规格提示词(20 parts,含分卷与逐章版本) - 投喂指南、分卷、逐节文件 - 调研记录、一致性基线、生成与质检脚本
159 KiB
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):
ColorSource == 0(自定义)→ 使用PrimaryColor;ColorSource == 1(壁纸主题色)或3(屏幕主题色)→ 使用SelectedPlatte(壁纸调色板第SelectedPlatteIndex项);ColorSource == 2(跟随系统)→ 传null,交给系统强调色;- 其余 → 兜底
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【推断:依据 ClassHelpertheme.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)。默认实现必须用 CSSborder-radius(与 AvaloniaCornerRadius的直角圆弧一致,保证与 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 落地顺序与常见错误
实现顺序必须严格如下,不得跳步:
- 先在
packages/shared/src/constants.ts建立常量对象(颜色、字号、圆角、间距、层级、DPI 基准),字段名与本节 token 名一一对应,导出为只读(as const)。 - 在
packages/ui/src/tokens.css的:root写入亮色值,在html.dark写入深色值;两处必须保持键集合完全一致,缺一个键都算不合格。 - 在
packages/desktop-client/src/renderer/styles/index.css引入--ci-*的桌面端覆盖(通知特效窗口、闪屏窗口等独立渲染入口要单独引入同一份 token)。 - 在
src/island/classic-island.css与custom-island.css里把--island-*映射到--ci-*,禁止在岛样式里重新定义颜色字面量。 - 最后才写组件;组件只允许消费 token。
常见错误(必须在实现时主动规避):
- 把「行级字号覆盖」写成给整个岛设置字号:这样改一行会连带改所有行,与源码作用域不符(源码把资源写在行控件自身,见
MainWindowCustomizableNodeHelper.cs:20-26)。 - 把 5 级前景色实现成「同一颜色的不同
opacity属性」:opacity会连带影响子元素与阴影,源码用的是颜色 alpha 通道(ControlColorHelper.cs:24-33),必须写进颜色值本身。 - 用
filter: drop-shadow()代替box-shadow实现岛投影:filter会新建渲染表面,开合时出现闪帧(ClassHelperclassic-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 验收标准
实现者必须逐条自查,全部通过才算完成本节:
- 颜色:全仓库(渲染进程 + 主进程 + shared)搜索,除 token 定义文件外不得出现
#开头的颜色字面量;grep -r "#[0-9a-fA-F]\{6\}"的命中只允许出现在 token 文件与主题包预览样式中。 - 强调色:修改
--ci-accent一处,岛背景描边、提醒遮罩、进度条、按钮强调态、天气图标强调色全部同步变化,无残留默认蓝。 - 5 级前景:开启自定义前景色后,5 个 alpha 分别为 100%/85.5%/77.3%/52.9%/36.5%;关闭后 5 个变量被移除并回落。
- 字号:把 4 档字号分别改成 10/20/30/40,岛内所有文字按对应档位变化且不溢出 40px 行高外的容器;行级覆盖可只影响单行。
- 圆角:把
--ci-radius-island从 8 改到 20,背景层与裁剪层同时变化,内容不溢出四角;其他卡片圆角不受影响。 - 间距:改
--ci-island-pad-x与--ci-component-gap,内容宽度变化后背景宽度动画仍正常(见 part 14)。 - 透明:非编辑态岛卡片外区域 100% 透明(用取色器/截图验证 alpha=0);编辑态出现亚克力底;全程不允许整窗材质导致卡片外出现色块。
- 层级:编辑态拖动项浮在其他项之上;提醒遮罩覆盖内容但低于右上角操作按钮(若有);
33550336哨兵元素永远最前。 - DPI:在 100%/125%/150%/200% 四档缩放下,岛高度逻辑值恒为 40、圆角逻辑值恒为 8、文字不模糊;跨显示器拖动后重新计算,无半个像素的缝。
- 深浅色:Light/Dark 各截图对照,两类主题的 AQI 色、雨天色、说明文字 alpha 与上表一致;深色下阴影仍为黑色。
- CSS 变量唯一来源:
--ci-*只在两处定义(:root与html.dark),组件内只引用不重定义(行级作用域覆盖除外)。 - 字符集:所有 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 |
强制约定:
- 不得把主界面做成"一个自适应大小的 div 窗口",也不得照搬 ClassIsland 的"窗口高度随内容自适应"。采用 4.3.1 的固定包围盒:窗口宽度 = 所选显示器整屏宽(DIP),高度 = 预设最大内容高 + 上下安全余量;可见岛在窗口内形变,其高度 = 行高之和 + 行间距之和。窗口大于岛的透明区域不会遮挡桌面(Windows 侧整窗被设为鼠标穿透,见 4.2)。
- 不得使用
win.setBackgroundMaterial()(亚克力/mica)。ClassIsland 的透明是逐像素 alpha +WS_EX_LAYERED,用系统材质会破坏像素级透明与圆角(repos/ClassHelper-master/packages/desktop-client/src/main/island.ts:523-524的既有结论)。 - 不得给主界面行加 CSS
box-shadow之外的重滤镜;Fluent 主题只有BoxShadow,Classic 主题只有提醒遮罩期才挂DropShadowEffect。 - 所有尺寸、时长、缓动必须按本文给的确切值写死为常量或 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,
},
});
逐条规则:
- 透明与无边框:
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)。 - 鼠标穿透(核心):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)。
- 非编辑模式:调用
- 不抢焦点:显示时用
win.showInactive();非编辑态focusable: false。若为了接收触摸事件必须保持可激活,则只允许在编辑模式内切换focusable,且退出编辑模式后立即恢复。 - 置顶/置底:
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、31ms 高频计时器(MainWindow.axaml.cs:420-436, 461-466, 687-704,默认WindowTopmostRecheckMode = 0见Models/Settings.cs:1860)。本实现的复核策略见 4.2.1,禁止照搬3的 1ms 轮询。
- 任务栏显示:
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)没有可见的切换按钮【推断:入口可能在教程步骤或后续版本】,实现者不得臆造按钮。
- P2 / 能力说明:
- 防截屏:
IsWindowCaptureBlockingEnabled=true时调用win.setContentProtection(true)(对应WDA_EXCLUDEFROMCAPTURE,WindowPlatformService.cs:169-172,MainWindow.axaml.cs:840)。 - 屏幕录制模式:
IsScreenRecordingModeEnabled=true时:不进 ToolWindow(任务栏可见)、不被置底。默认false(Models/Settings.cs:1861)。 - 窗口关闭拦截:主界面窗口不得被用户关闭。ClassIsland 在
Closing事件里e.Cancel = true,只有应用主动退出才放行(MainWindow.axaml.cs:577-590)。实现:win.on('close', (e) => { if (!appQuitting) e.preventDefault(); })。 - 禁止的窗口能力:不得
setAlwaysOnTop到'floating'以下级别;不得开启thickFrame;不得给窗口加系统菜单;不得设置roundedCorners(圆角由 CSS 画)。 - 单实例守卫:见第 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):
- 窗口创建时一次算好包围盒:
winW = 目标屏幕宽(DIP) = screen.bounds.width / dpiX;winH = contentMaxHeight + 2 * safePadding,其中contentMaxHeight= 预设最大内容高(默认按"当前可见行数 × (40 + 行间距 5) + 上下安全区"计算,并留出一行余量),safePadding ≥ 20(阴影 + 光晕余量,对齐ISLAND_SHADOW_PAD = 10的双侧口径)。
- 可见岛在窗口内形变:窗口不动,岛用 CSS
height过渡(或第 14 章的 spring 形变)在包围盒内伸展/收缩;内容层按 4.5 的行高与间距排布。 - 只有下列情形才
setBounds:显示器/DPI 变化、行数超出包围盒上限、进入/退出编辑模式、窗口偏移/缩放/停靠位置设置变化。这类变更必须节流 ≥100ms(同一窗口内多次合并为一次),且不得与弹簧/高度动画同帧竞争。 - 视觉结果必须与 ClassIsland 一致:用户看到的可见岛高度 = 行高之和 + 行间距之和(行高恒
40,间距MainWindowLineVerticalMargin),可见岛宽度 = 内容自然宽 + 两侧各 12px 内边距。窗口比岛大只是透明余量,用户不可见。 - 禁止:把窗口高度做成"刚好等于内容高"再逐帧改;用
requestAnimationFrame每帧setBounds;在行内容变化(秒数、课程名长度、提醒)时改窗口尺寸。以上都会导致 1px 级抖动。
与 ClassIsland 的差异及理由(必须写进实现注释):差异 = 窗口高度固定为包围盒 vs 随内容自适应;理由 = Electron 透明窗口 resize 抖动/闪烁,且 ClassHelper 的既有实现已证明"固定包围盒 + 窗内画布形变"在开合过程中零抖动(见该文件 :64-70 的设计说明)。视觉结果不做任何妥协:可见岛的行高、间距、圆角、阴影与 4.5 完全一致。
4.3.2 可见内容盒(视觉基准公式)
下面的 ClassIsland 原生公式只用于计算"可见岛"的内容尺寸、安全区裁剪与停靠对齐,不直接作为 BrowserWindow.setBounds 的 height 参数(setBounds 规则见 4.3.1):
contentW、contentH:由渲染进程上报的岛内容尺寸(CSS px)。内容盒 = 所有行高度之和 + 行间距之和(见 4.5)。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)。dockingTop = docking ∈ {0,1,2}。safeT = Math.max(dockingTop ? Math.min(safe, oy) : safe, 0) * scale。safeB = Math.max(dockingTop ? safe : Math.min(safe, -oy), 0) * scale。岛锚点 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。layoutW = screen.bounds.width / dpiX(布局容器宽 = 整屏宽,用于左/中/右对齐计算,MainWindow.axaml.cs:915)。layoutH = contentH + safeT + safeB(岛的可布局高度;这只是内容画布高,不是窗口height)。- 岛在画布内的偏移:
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)。 - 窗口自身的包围盒位置必须让"岛锚点"落在公式 6 的位置:
winX = 岛锚点X、winY = 岛锚点Y - safePadding(上停靠)/岛锚点Y - contentMaxHeight - safePadding(下停靠,包围盒底部贴锚点)。
4.3.3 编辑模式窗口
进入编辑模式时窗口必须扩展为全屏编辑画布(这一步允许 setBounds,因为是模式切换、不是内容变化):
winX = screen.bounds.x、winY = screen.bounds.y。winW = screen.bounds.width / dpiX。winH = (screen.bounds.height - 1) / dpiY——必须减 1px,防止 Windows 把窗口判定成全屏并触发全屏优化(MainWindow.axaml.cs:970)。- 岛内容在窗口内四边各留 60px 安全边距:
padding = 60px(来源WindowedEditModeScreenMargin = 60,MainWindow.axaml.cs:134),即编辑画布的可用区为winW - 120×winH - 120,岛在其中垂直居中、水平按停靠位置对齐。 - 内容偏移:
translateX = ox / dpiX、translateY = oy / dpiY(编辑模式偏移是逻辑像素,MainWindow.axaml.cs:973-974);clientRect = ignoreArea ? screen.bounds : screen.workArea。 - 鼠标事件必须开启:
win.setIgnoreMouseEvents(false)(见 4.2 第 2 条)。 - 编辑模式 + 自由窗口(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)。 - 退出编辑模式:恢复 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
300mscubic-bezier(0.65, 0, 0.35, 1.0)(FluentTheme/Styles.axaml:269);Classic550mscubic-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):
- 行设置:图标
\uEF27,ToolTip "设置…",打开行设置抽屉。 - 主要行开关(ToggleButton):图标
\uE994,ToolTip "此行是主要行"。 - 提醒开关(ToggleButton):图标
\uE02B,ToolTip "此行可以显示提醒"。 - 删除(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)、URIclassisland://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 完全一致";验收只要求"编辑态背景明显压暗、岛与抽屉可读、退出后恢复逐像素透明"。
- 禁止调用 Electron 的
- 编辑模式下窗口必须接收鼠标:
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 主题):
- "重置视图"——图标
\uE12F,命令 = 缩放重置。 - 竖分隔线:
StartPoint 0,0 → EndPoint 0,25、StrokeThickness=1、颜色DividerStrokeColorDefaultBrush。 - "外观"——图标
\uE51E,打开外观抽屉;DisableSettingsEditing策略下禁用。 - "添加组件"——图标
\uE071,打开组件库抽屉;同策略禁用。 - 组件配置按钮——图标
\uE699,文本 = 当前配置名(Settings.CurrentComponentConfig,默认"Default",Models/Settings.cs:159),MaxWidth=150;打开配置管理抽屉。 - "完成"——图标
\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 验收标准
实现者必须逐条自查,全部通过才算完成本节:
- 非编辑模式下用 Spy++/AutoHotkey 之类工具点击岛的任何位置,点击都穿透到桌面应用;编辑模式下岛内的按钮、拖拽把手、抽屉都能被点到。
- 主界面窗口在 4K(150% 缩放)与 1080p(100% 缩放)两种 DPI 下都不出现 1px 白边、黑边、圆角锯齿;圆角在
RadiusX=0时是直角,在20时左右边缘为半圆。 - 6 个停靠位置逐个验证:
0/1/2贴工作区上边、3/4/5贴下边;左/中/右的横向对齐分别贴左、居中、贴右,且窗口本身始终横跨整屏。 - 修改"窗口向右/向下偏移"后岛整体平移,正值向下时上停靠要把偏移吞进安全区(不得被屏幕边缘裁掉)。
- 行数从 1 增到 5、再减到 1:可见岛高度严格等于"行高之和 + 行间距之和"(误差 ≤1px),岛在窗口包围盒内平滑形变,不出现窗口级闪烁;包围盒只在行数超出
contentMaxHeight时变更一次,且变更被节流 ≥100ms。 5.1 用 DevTools 的 Performance 面板录 10 秒:setBounds调用次数为 0(无设置/行数变化时);置顶复核轮询间隔 ≥1000ms,无 1ms/10ms 级高频定时器。 - 单行内容宽度变化(如时钟秒数出现/消失、课程名变长)时,背景宽度在 300ms 内以
cubic-bezier(0.65,0,0.35,1.0)平滑跟随,文字不重排抖动,且不触发窗口setBounds。 - 行间距设为
0、5、150时,相邻行间距精确等于设置值(用像素标尺截图量)。 - 关闭某行"可以显示提醒"后,该行不再出现遮罩与进度条(走 Min 模板),其余行不受影响;多行同时具备提醒能力时,具体由哪一行显示由第 7 章的分发规则决定,本节只验证宿主与
IsNotificationEnabled标记生效。 8.1 三条进度条(行提醒、课程、倒计时)高度均为3px,圆角1.5px,贴底且不越出行圆角裁切。 - 删除到只剩 1 行时再点删除,出现"至少需要保留 1 个主界面行。"提示且行数不变。
- 勾选 A 行为主要行后 B 行自动取消;取消 A 行后第一行自动成为主要行并出现提示。
- 触发一次提醒:遮罩从左、右两端同时向中间斜掠,总时长约 480ms,文字在 260ms 后淡入并伴随 1.1→1.0 缩放;遮罩期间行内容消失,结束后恢复。
- 阈值条件全开(上课隐藏、最大化隐藏、全屏隐藏),在前台最大化窗口时岛消失、切回桌面 1 个计时器周期内恢复;
ExcludedFullscreenWindow含explorer时桌面(资源管理器)前台不隐藏。 - 鼠标移出岛 0.5 秒内行的整体不透明度降到 0.05,移回立即恢复;勾选"反向"后行为相反。
- 编辑模式:进入后窗口扩展为"屏宽 ×(屏高−1px)",岛内容四边留 60px 安全边距并垂直居中,任务栏出现图标,鼠标可交互;退出后恢复包围盒尺寸与穿透状态,并写盘保存组件配置;编辑态背景为
rgba(0,0,0,0.30)遮罩(无backdrop-filter、无系统材质),退出后恢复逐像素透明。 - 编辑模式下拖拽组件到另一行的插入位置时,出现强调色发光插入线(1s 交替闪烁),松手后组件落到目标位置且行宽平滑重排。
- 容器组件"查看子组件"浮动框出现在被点组件下方 48px 处,可拖动、可关闭,关闭后
ContainerComponents中对应项被移除。 - 主界面完全不可见(手动隐藏 / 全部隐藏规则命中)时收到提醒,岛必须临时显示、置顶,提醒结束后回到隐藏状态。
- 托盘图标左键行为的 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):
- 可见性三态:
visible = IsVisibleInternal || 主界面处于编辑模式(:13-18)。隐藏的组件在编辑模式仍占位。 - 透明度:宿主
opacity = 组件设置.Opacity(默认1.0,范围 0–1,:34-36)。 - 字号继承:宿主根元素的
font-size = var(--font-body)(16px),即所有组件默认正文字号;组件内部需要别的字号时必须显式引用--font-secondary/emphasized/large(:37-38)。 - 宽度三态(互斥,优先级:固定宽 > 最大宽/最小宽):
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)。
- 水平对齐:宿主内容的
justify-content取Settings.HorizontalAlignment,默认stretch(枚举Stretch/Left/Center/Right,ComponentSettings.cs:36,选择器:49-51)。 - 内边距:宿主内部
Shimmer容器padding: 8px 0(:55-57),但组件自身的横向间距由外层margin: 0 6px提供(行内模板Margin="6 0",MainWindow.axaml:396),两者都要保留,不得合并。 - 加载占位:首次布局或配置切换时显示 Shimmer 占位,占位宽度取上次测量宽度
LastWidthCache(默认100,ComponentSettings.cs:39,ComponentPresenter.axaml:69-70)。占位态不得让行高变化。 - 隐藏规则:
HideOnRule + HidingRules满足时组件不可见但不卸载、不释放资源(ComponentSettings.cs:100-123)。 - 裁剪:宿主自身
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 以驱动闪动。
- 若系统时间不可用(精确时间服务失败)必须回退本地时间,不得显示空白。
- 24 小时制,不显示 AM/PM(格式串
- 验收要点:开启闪动后,肉眼观察冒号 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):- 固定时间:
(StartTime, OverTime)。 - 周期:
total = before + duration + after;cycles = floor((now - cycleStart)/total)(启用次数限制时再取min(cycles, limit));start = cycleStart + cycles*total + before;end = start + duration。total <= 0时退化为(cycleStart, cycleStart)。 - 今天:
NatureTimeUseMode==1→(今天0点, 明天0点);否则取当天时间表中第一个"上课/课间"时间点的开始 → 最后一个"上课/课间"时间点的结束;课程不可用时按NatureTimeUseMode==2退化为(DateTime.MinValue, DateTime.MinValue)(不显示倒计时),否则退化为自然日。 - 本周:按
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)时进度环/进度条不跟随放大,只放大文本。
- 终点在过去 → 数值全 0、
- Vue 结构建议:
CountDownNode.vueprops ={ settings, now },内部computed出remaining、percent、text;进度环用 SVGcircle+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):
TimeType ∈ {2 分割线, 3 行动}不显示【注意:分割线类型由专门的LessonControlSeparator渲染,判定顺序以源码为准】;HideFinishedClass=true且EndTime < now→ 不显示;IsHideDefault=true且不是当前选中项 → 不显示;TimeType==1(课间)且不是当前选中项 → 不显示;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 等级配色:
AqiLevel1–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),ComponentPresenterMargin="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,子项ComponentPresenterMargin="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):
- 按规则隐藏(开关 + 说明"注意此处的隐藏不会释放组件资源");
- 不透明度(Slider 0–1,步进 0.05);
- 自定义背景颜色(开关 + ColorPicker);
- 自定义背景不透明度(开关 + Slider);
- 自定义背景圆角半径(开关 + Slider 0–20,步进 0.5,文案"调至最大数值时左右边缘为半圆");
- 字体大小(总开关
IsResourceOverridingEnabled+ 四个 NumberBox:次级/正文/强调/大号,范围 1–40,ci:Field宽 175、后缀px); - 字体颜色(开关 + ColorPicker);
- 对齐方式(4 个图标按钮:左
\uE03E、居中\uE03A、拉伸\uE034、右\uE03C); - 固定宽度(开关 + NumberBox);
- 宽度限制(两个开关 + 两个 NumberBox:最大/最小);
- 自定义边距(开关 + 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 行 2 组件(日期 + 课程表),日期显示形如
周三 11/12,课程表显示当天课程或占位符。 - 11 个内置组件全部能从组件库拖入主界面,拖入后立刻可编辑、可删除,删除后无残留占位。
- 时钟:显示
HH:mm;开启闪动后冒号 1 秒亮 1 秒灭;开启秒数后变HH:mm:ss且冒号常亮;数字等宽,切换时不水平抖动。 - 倒计时:4 种时间源(固定时间/周期/今天/本周)各配一遍,数值、进度环、进度条、连接词显示正确;
%D/%H/%M/%S/%X/%P/%p/%L/%d/%h/%m/%s/%x全部 token 输出与源码规则一致(尤其%m/%s/%x的补零与向上取整语义)。 - 倒计时进度环从 12 点方向顺时针填充,满 100% 时不出现缺口;紧凑模式只显示"名称 + 数值"。
- 课程表:当前课程高亮、已结束课程淡化到 0.6、剩余时间药丸在阈值内出现;
HideFinishedClass/ShowCurrentLessonOnlyOnClass/FadeCompletedClasses/HighlightChangedClass各自开关验证;4 种"显示明天课表"模式行为正确。 - 课程表空状态:无课表显示"今天没有课程。";全部结束显示"今日课程已全部结束。";
ShowPlaceholderOnEmptyClassPlan=false时两者都不显示。 - 天气:6 种主信息逐一切换显示正确;有预警时按"总是图标/默认/总是全文"三种模式渲染;降水药丸在
RainRemainingMinutes>0时出现、=0时消失;模糊模式显示"约 X 分钟"、精确模式显示"Xmin"。 - 文本:字号 16 与 30 时高度都不超过 40px;自定义颜色生效/关闭后回退;空文本不占宽。
- 滚动容器:内容超出时按
SpeedPixelPerSecond匀速滚动,PauseSeconds停顿位置等于PauseOffsetX;内容未超出时静止;命中"停止规则"后回到初始位置。 - 轮播容器:
SlideSeconds到点切换;循环/随机/往复三种模式各验证一轮;开启动画时切换有 250ms 上下位移淡入淡出,关闭时无过渡;只有 1 个子项时不轮播。 - 分组容器的子项横向排列,间距恒定 12px;堆叠容器的子项完全重叠在同一矩形且各自按对齐方式摆放;分割线为 2×25px、透明度 0.8。
- 所有组件的"高级设置"项生效:固定宽度、最大/最小宽度、对齐、不透明度、自定义边距、字号覆盖、字体颜色覆盖;隐藏规则勾选后组件消失但在编辑模式显示斜条纹占位。
- 组件 ID 不存在时显示"缺失组件"占位而不是白屏或崩溃。
- 全部组件在 40px 行高内垂直居中,无任何组件把行撑高超过 40px。