- 重构版主提示词(14 parts,面向 ClassHelper 灵动岛重构) - ClassIsland 全量规格提示词(20 parts,含分卷与逐章版本) - 投喂指南、分卷、逐节文件 - 调研记录、一致性基线、生成与质检脚本
1869 lines
159 KiB
Markdown
1869 lines
159 KiB
Markdown
## 3. 设计系统 Token 总表(颜色 / 字体 / 圆角 / 间距 / 阴影 / 层级 / DPI / 深浅色)
|
||
|
||
本节是整份提示词的「地基」。实现者必须先落地本节的 token(CSS 自定义属性 + TypeScript 常量),再开始写任何界面。任何界面代码**不得**出现裸色值、裸字号、裸圆角,一律引用本节 token。
|
||
|
||
实现技术栈为 ClassHelper 的 Electron + Vue 3 栈,宿主已有 `packages/ui/src/tokens.css` 的 `--ch-ui-*` 与桌面端 `renderer/styles/index.css` 的 `--ch-*`。**保留并复用**这些既有 token 作为「应用外壳层」,新增 `--ci-*` 命名空间承载 ClassIsland 主界面(岛)层 token。两层必须分开:`--ch-*` 管设置中心/管理页,`--ci-*` 管岛与课程信息。
|
||
|
||
### 3.1 颜色 Token
|
||
|
||
#### 3.1.1 强调色与派生色
|
||
|
||
| Token 名 | 默认值 | 用途 | 建议 CSS 变量 | 源码证据 |
|
||
|---|---|---|---|---|
|
||
| `PrimaryColor`(用户强调色) | `#00BFFF`(Colors.DeepSkyBlue) | 设置项「强调色」的默认值;`ColorSource=0` 时作为全站 accent | `--ci-accent` | `ClassIsland/Models/Settings.cs:35`、`Settings.cs:992` |
|
||
| `SecondaryColor` | `#7FFFD4`(Colors.Aquamarine) | 备用强调色(组件/图表次级色) | `--ci-accent-2` | `Settings.cs:36`、`Settings.cs:1003` |
|
||
| FluentAvalonia 初始 accent | `DodgerBlue`=`#1E90FF` | 主题服务尚未介入时的启动色 | `--ci-accent-fallback` | `ClassIsland/App.axaml:17` |
|
||
| `SelectedPlatte` 兜底 | `DodgerBlue`=`#1E90FF` | 壁纸调色板取不到颜色时的兜底 | `--ci-accent-palette-fallback` | `Settings.cs:1036-1039`、`Settings.cs:80` |
|
||
| `AccentFillColorDefaultBrush` | 由 accent 派生(Fluent 资源) | 提醒遮罩填充、教程高亮框、进度条强调 | `--ci-accent-fill` | `ClassIsland.Core/StylesBase.axaml:90-93`、`XamlThemes/FluentTheme/Styles.axaml:180` |
|
||
| `AccentFillColorTertiaryBrush` | accent 低透明度变体 | 课程列表展开态底色 | `--ci-accent-fill-tertiary` | `ClassIsland.Core/Controls/LessonsControls/LessonControlExpanded.axaml:182` |
|
||
| `AccentTextFillColorPrimaryBrush` | accent 文字色 | 拖拽高亮描边、TeachingTip 指示箭头 | `--ci-accent-text` | `EditableComponentsListBox.axaml:310`、`MyTeachingTipStyles.axaml:213` |
|
||
| `TextOnAccentFillColorPrimaryBrush` | 白/黑(随 accent 对比度) | 遮罩上的文字 | `--ci-text-on-accent` | `FluentTheme/Styles.axaml:162,182`;`ClassicTheme/Styles.axaml:142` |
|
||
| `CustomizedAccentBarBackground1Brush` | 亮 `#FFDDE6F0` / 暗 `#26374C`;运行时被覆盖为 accent 的 10% 透明 | 「强调按钮」淡底、`Button.foreground-accent` | `--ci-accent-soft` | `Themes/LightTheme.axaml:18`、`Themes/DarkTheme.axaml:18`、`Services/ThemeService.cs:66-74` |
|
||
|
||
运行时强调色决策必须严格按以下分支实现(源码见 `ClassIsland/MainWindow.axaml.cs:743-764`):
|
||
|
||
1. `ColorSource == 0`(自定义)→ 使用 `PrimaryColor`;
|
||
2. `ColorSource == 1`(壁纸主题色)或 `3`(屏幕主题色)→ 使用 `SelectedPlatte`(壁纸调色板第 `SelectedPlatteIndex` 项);
|
||
3. `ColorSource == 2`(跟随系统)→ 传 `null`,交给系统强调色;
|
||
4. 其余 → 兜底 `DodgerBlue`。
|
||
|
||
`--ci-accent-soft` 必须以 `color-mix(in srgb, var(--ci-accent) 10%, transparent)` 实时推导,不得写死。ClassHelper 已有等价写法可复用:`packages/ui/src/theme.css:4-9` 用 `color-mix()` 派生 Element Plus 的 `--el-color-primary-light-*`。
|
||
|
||
#### 3.1.2 前景文字阶梯(前景色 5 级)
|
||
|
||
主界面自定义前景色时,前景色不是 1 个值,而是**由同一个基色派生出 5 个 alpha 版本**,必须按下表实现(源码:`ClassIsland.Core/Helpers/UI/ControlColorHelper.cs:20-52`):
|
||
|
||
| 层级 | 资源键 | alpha | 用途 | 建议 CSS 变量 |
|
||
|---|---|---|---|---|
|
||
| 1 | `TextFillColorPrimaryBrush` | 255(100%) | 主要文字、标题 | `--ci-text-1` |
|
||
| 2 | `TextFillColorInformativeSecondaryBrush` | 218(85.5%) | 次要说明(本项目覆盖值) | `--ci-text-2` |
|
||
| 3 | `TextFillColorSecondaryBrush` | 197(77.3%) | 辅助文字 | `--ci-text-3` |
|
||
| 4 | `TextFillColorTertiaryBrush` | 135(52.9%) | 装饰/弱提示 | `--ci-text-4` |
|
||
| 5 | `TextFillColorDisabledBrush` | 93(36.5%) | 禁用态 | `--ci-text-5` |
|
||
|
||
实现规则:
|
||
- 未启用自定义前景色时,上述 5 个 token 使用 Fluent 默认值:亮色 `#E4000000` / `#9E000000` / `#72000000` / `#5C000000`,深色 `#FFFFFFFF` 及对应 alpha 版本【推断:FluentAvalonia 资源值不在本仓库,按 Fluent 2 规范给出】。
|
||
- 启用自定义前景色时,5 个 token 全部按上表 alpha 从 `CustomForegroundColor` 派生,且**必须同时**重设 `TemplatedControl.Foreground` 与 `TextElement.Foreground`,否则模板内文字会漏改(`ControlColorHelper.cs:34-40`)。
|
||
- 关闭自定义前景色时,必须 `Remove` 这 5 个局部资源并 `SetValue(..., AvaloniaProperty.UnsetValue)` 回落(`ControlColorHelper.cs:44-50`)——映射到 CSS 即删除元素上的内联 `--ci-text-*`,回落 `:root`。
|
||
- 两个默认覆盖值:亮色 `TextFillColorInformativeSecondaryBrush = #c0000000`(75% 黑),深色 `#daffffff`(85.5% 白)(`Themes/LightTheme.axaml:20`、`Themes/DarkTheme.axaml:20`)。
|
||
- 语义层级类必须映射为 CSS 工具类:`.l-primary|.l1 → --ci-text-1`、`.l-secondary|.l2 → --ci-text-2`、`.l-instructive|.l3 → --ci-text-3`、`.l-decoration|.l4 → --ci-text-4`、`.l-inactive|.l5 → --ci-text-5`(`ClassIsland.Core/Themes/Layers.axaml:10-43`)。
|
||
|
||
#### 3.1.3 背景与表面色
|
||
|
||
| Token 名 | 亮色 | 深色 | 用途 | 建议 CSS 变量 | 源码证据 |
|
||
|---|---|---|---|---|---|
|
||
| `SolidBackgroundFillColorBaseBrush` | `#F3F3F3`【推断】 | `#202020`【推断】 | 经典主题岛背景、编辑模式窗口底、抽屉内容底、主题卡渐变底 | `--ci-bg-base` | `ClassicTheme/Styles.axaml:214`;`DrawerHost.axaml:29`;`MainWindow.axaml:188` |
|
||
| `SolidBackgroundFillColorSecondaryBrush` | `#EEEEEE`【推断】 | `#1C1C1C`【推断】 | Fluent 主题岛背景 | `--ci-bg-secondary` | `FluentTheme/Styles.axaml:45` |
|
||
| `SolidBackgroundFillColorTertiaryBrush` | `#F9F9F9`【推断】 | `#282828`【推断】 | 弹出层底(`.popup-bg`) | `--ci-bg-tertiary` | `StylesBase.axaml:291` |
|
||
| `ControlFillColorSecondaryBrush` | 半透明白/黑 | 同左 | 主题卡底色 | `--ci-fill-secondary` | `ThemesSettingsPage.axaml:149` |
|
||
| `SmokeFillColorDefaultBrush` | 半透明遮罩 | 同左 | 编辑模式窗口背景、抽屉忽略层 | `--ci-smoke` | `MainWindow.axaml:178`;`DrawerHost.axaml:20` |
|
||
| `Layer` 应用底色 | `#F3F3F3` | `#161719` | 设置中心/管理页页面底(沿用 ClassHelper) | `--ch-bg` | `ClassHelper/renderer/styles/index.css:27,114` |
|
||
| `Layer` 卡片底 | `rgba(255,255,255,.7)` | `rgba(32,33,36,.72)` | 应用外壳卡片 | `--ch-layer` | 同上 `:28,115` |
|
||
| `CustomBackgroundColor` | `#000000`(默认) | 同左 | 岛自定义背景色默认值 | `--ci-island-bg-custom` | `Settings.cs:34`、`Settings.cs:1129` |
|
||
|
||
主界面行的背景最终呈现必须叠加两个不透明度:
|
||
- 自定义背景色本身的不透明度:`--ci-island-bg-opacity`,默认 `0.5`(`Settings.cs:64`、`:1141`;Fluent 用 `MainWindowStylesAssist.BackgroundOpacity` 绑定到 `Border.Opacity`,见 `FluentTheme/Styles.axaml:44`);
|
||
- 整行不透明度:`MainWindowLineSettings.Opacity`,默认 `1.0`(`ClassIsland.Core/Models/Components/MainWindowLineSettings.cs:42`;`Controls/MainWindowLine.axaml:51`)。
|
||
- 鼠标移出淡化态:整行 `opacity = 0.05`(`Controls/MainWindowLine.axaml:73-75`)。
|
||
|
||
#### 3.1.4 描边与分隔线
|
||
|
||
| Token 名 | 值 | 用途 | 建议 CSS 变量 | 源码证据 |
|
||
|---|---|---|---|---|
|
||
| 岛描边 | 1px `ControlElevationBorderBrush` | 经典/Fluent 行背景描边 | `--ci-stroke-island` / 宽度 `--ci-stroke-island-w:1px` | `FluentTheme/Styles.axaml:46-47`;`ClassicTheme/Styles.axaml:61-65` |
|
||
| `ControlStrokeColorDefaultBrush` | 亮色半透明黑 | 编辑模式选中框描边 | `--ci-stroke-control` | `MainWindow.axaml:357` |
|
||
| `DividerStrokeColorDefaultBrush` | 亮色半透明黑 | 操作条分隔线(1px,长 25px) | `--ci-stroke-divider` | `MainWindow.axaml:610-612` |
|
||
| `SurfaceStrokeColorFlyoutBrush` | 半透明 | 弹窗描边 | `--ci-stroke-flyout` | `StylesBase.axaml:292` |
|
||
| `ControlStrongFillColorDefaultBrush` | 中灰 | OOBE 方块、Workflow 进度条底、Shimmer 底 | `--ci-fill-strong` | `OobeIntroAnimationControl.axaml:34`;`WorkflowProgressIndicatorItemControl.axaml:26`;`Shimmer.axaml:11` |
|
||
| 主题卡描边 | 1px `ControlElevationBorderBrush` | 主题网格卡片 | `--ci-stroke-card` | `ThemesSettingsPage.axaml:150-151` |
|
||
| 拖拽高亮描边 | 1px accent | 组件拖拽占位 | `--ci-stroke-drag` | `EditableComponentsListBox.axaml:310-311` |
|
||
| 教程高亮描边 | 2.5px(外框)/ 3px(四角) | 教程 spotlight | `--ci-stroke-tutorial` | `TutorialHighlightControl.axaml:96-118` |
|
||
|
||
#### 3.1.5 状态色
|
||
|
||
天气与空气质量状态色(浅色/深色两套,必须都实现):
|
||
|
||
| 语义 | 亮色 | 深色 | 建议 CSS 变量 | 源码证据 |
|
||
|---|---|---|---|---|
|
||
| AQI 1 优 | `ForestGreen`=`#228B22` | `MediumSeaGreen`=`#3CB371` | `--ci-aqi-1` | `LightTheme.axaml:10`;`DarkTheme.axaml:10` |
|
||
| AQI 2 良 | `#CCCC00` | `Yellow`=`#FFFF00` | `--ci-aqi-2` | `:11` |
|
||
| AQI 3 轻度 | `DarkOrange`=`#FF8C00` | `Orange`=`#FFA500` | `--ci-aqi-3` | `:12` |
|
||
| AQI 4 中度 | `IndianRed`=`#CD5C5C` | 同亮色 | `--ci-aqi-4` | `:13` |
|
||
| AQI 5 重度 | `DarkRed`=`#8B0000` | 同亮色 | `--ci-aqi-5` | `:14` |
|
||
| AQI 6 严重 | `Purple`=`#800080` | `MediumPurple`=`#9370DB` | `--ci-aqi-6` | `:15` |
|
||
| 雨天图标色 | `Blue`=`#0000FF` | `DeepSkyBlue`=`#00BFFF` | `--ci-weather-rain` | `LightTheme.axaml:3`;`DarkTheme.axaml:3` |
|
||
| 上课发光 | `DropShadowEffect` blur 6(亮)/2(暗),颜色 `#FFFF00` | 同左 | `--ci-glow-class` | `LightTheme.axaml:4-7`;`DarkTheme.axaml:4-7` |
|
||
| 危险/删除按钮 | `IndianRed`=`#CD5C5C` | 同左 | `--ci-danger` | `Themes/Generic.xaml:225-226,275-276`;`MainWindow.axaml:435` |
|
||
| 警告/错误图标 | `SystemFillColorCriticalBrush` / `SystemFillColorCautionBackgroundBrush` | 同左 | `--ci-warning-*` | `StylesBase.axaml:209,217`;`ThemesSettingsPage.axaml:343` |
|
||
|
||
#### 3.1.6 阴影与遮罩色(必须逐值实现)
|
||
|
||
| 用途 | 值 | 建议 CSS 变量 | 源码证据 |
|
||
|---|---|---|---|
|
||
| Fluent 岛投影 | `0 4 8 2 #48000000` | `--ci-shadow-island: 0 4px 8px 2px #48000000` | `FluentTheme/Styles.axaml:49` |
|
||
| 提醒遮罩时岛投影 | `0 8 32 0 #66000000` | `--ci-shadow-mask` | `FluentTheme/Styles.axaml:324,327` |
|
||
| 竖抽屉/遮罩卡投影 | `0 8 32 0 #66000000` | `--ci-shadow-drawer` | `VerticalDrawer.axaml:45` |
|
||
| 横抽屉内浮层投影 | `0 0 8 #33000000` | `--ci-shadow-drawer-x` | `DrawerHost.axaml:33` |
|
||
| 经典主题提醒时行投影 | `DropShadowEffect` BlurRadius=8, Opacity=1, Offset=0 | `--ci-shadow-classic-mask` | `ClassicTheme/Styles.axaml:283-287` |
|
||
| 时间线装饰器阴影 | `DropShadowEffect` opacity 0.4, blur 8 | `--ci-shadow-adorn` | `Themes/Generic.xaml:215-219` |
|
||
| 弹窗底渐变遮罩 | `#00ffffff`→(0.65)`#00ffffff`→(1)`#ffffffff` | `--ci-mask-fade` | `ThemesSettingsPage.axaml:162-169` |
|
||
| 教程聚光遮罩 | `#99000000`(60% 黑) | `--ci-scrim-tutorial` | `TutorialSpotlightControl.axaml:45-61` |
|
||
| 编辑模式调试叠加 | `#77222222` | `--ci-debug-bg` | `MainWindow.axaml:645` |
|
||
| 启动闪屏底 | `#88111111` + 1px `ControlStrokeColorOnAccentDefaultBrush` | `--ci-splash-bg` | `SplashWindow.axaml:24-28` |
|
||
| 主题卡渐变底 | 径向 `#ff0c0e1f`→`#ff0d1111`(关于页) | `--ci-brand-gradient-a/b` | `AboutSettingsPage.axaml:153-154` |
|
||
| 关于页正文底 | `#0d1111` | `--ci-brand-dark` | `AboutSettingsPage.axaml:131` |
|
||
| 通知涟漪默认填充 | `AccentFillColorDefaultBrush`(可被 `NotificationContent.Color` 覆盖) | `--ci-ripple` | `RippleEffect.axaml.cs:61`;`MainWindowLine.cs:750` |
|
||
| 通知涟漪描边宽 | 200(Ellipse StrokeThickness) | `--ci-ripple-stroke:200` | `RippleEffect.axaml:15` |
|
||
|
||
### 3.2 字体 Token
|
||
|
||
#### 3.2.1 字体族
|
||
|
||
| Token | 值 | 用途 | 建议 CSS 变量/写法 |
|
||
|---|---|---|---|
|
||
| `AppFont`(全局默认) | `HarmonyOS Sans SC, Microsoft YaHei UI, Noto Sans CJK SC, Noto Sans SC, PingFang SC, Source Han Sans SC, SimHei, WenQuanYi Micro Hei, sans-serif, FluentSystemIcons-Resizable` | 全应用默认字体 | `--ci-font: 'HarmonyOS Sans SC', 'Microsoft YaHei UI', 'Noto Sans CJK SC', 'Noto Sans SC', 'PingFang SC', 'Source Han Sans SC', SimHei, sans-serif` |
|
||
| `MainWindowFont`(岛字体,用户可改) | 默认等于 `DefaultFontFamilyKey`:`avares://ClassIsland/Assets/Fonts/#HarmonyOS Sans SC, ...FluentSystemIcons-Resizable` | 只作用于岛的 `ZoomBorder` 文本 | `--ci-font-island`(元素级覆盖) |
|
||
| `HarmonyOsSans`(旧键) | `HarmonyOS Sans SC, Microsoft YaHei UI, SimHei` | 旧样式/右键菜单 | 兼容别名 |
|
||
| `BackendFontFamily` | `Microsoft YaHei UI` | 后端渲染/图表兜底 | `--ci-font-backend` |
|
||
| 图标字体 1 | `FluentSystemIcons-Resizable`(`avares://ClassIsland.Core/Assets/Fonts/#FluentSystemIcons-Resizable`) | 全局 Fluent 图标 | `--ci-font-icon-fluent` |
|
||
| 图标字体 2 | `lucide`(`#lucide`) | Lucide 天气图标等 | `--ci-font-icon-lucide` |
|
||
| 图标字体 3 | `SF-Symbols`(`#SF-Symbols`) | SFSymbols 天气图标 | `--ci-font-icon-sf` |
|
||
|
||
证据:`ClassIsland/Assets/Fonts/Fonts.axaml:3`、`Assets/Fonts/Fonts.xaml:3`、`MainWindow.axaml.cs:185-189`、`App.axaml:48`、`ClassIsland.Core/AppBase.cs:142-152`、`ClassIsland.Core/ThemeBase.axaml:46-47`。
|
||
|
||
实现要求:
|
||
- 字体栈**必须按上表顺序**声明,任何一环缺失即回落下一位;不得只写 `HarmonyOS Sans SC`。
|
||
- 字体文件必须以本地资源打包,**禁止**运行时从网络加载字体(离线教室环境)。
|
||
- 图标字体与正文字体必须拆成两个 token,图标组件禁止继承正文字体族。
|
||
|
||
#### 3.2.2 字重
|
||
|
||
字重设置以整数保存,映射表必须逐值实现(`ClassIsland.Core/Converters/IntToFontWeightConverter.cs:16-28`):
|
||
|
||
| 数值 | 字重名 | CSS |
|
||
|---|---|---|
|
||
| 100 | Thin | 100 |
|
||
| 200 | ExtraLight | 200 |
|
||
| 300 | Light | 300 |
|
||
| 400 | Normal | 400 |
|
||
| 500 | Medium(**默认**) | 500 |
|
||
| 600 | SemiBold | 600 |
|
||
| 700 | Bold | 700 |
|
||
| 800 | ExtraBold | 800 |
|
||
| 900 | Black | 900 |
|
||
| 其他 | Normal | 400 |
|
||
|
||
默认字重来自 `Settings.cs:101`(`(int)FontWeight.Medium` = 500)。岛内绝大多数文字必须用 500(Medium),标题可用 700(Bold),说明文字用 400(Regular)。
|
||
|
||
4 档核心字号在设置页的输入范围均为 **1–40 px**(`AppearanceSettingsPage.axaml:482-533`,`NumberBox` 的 `Minimum=1`、`Maximum=40`)。实现时必须允许 1–40 的整数/小数输入,并在预览区用四行示例文字实时展示 4 档效果(源码「字号预览」tab:`AppearanceSettingsPage.axaml:570-591`)。
|
||
|
||
#### 3.2.3 字号阶梯(核心 4 档 + 扩展档)
|
||
|
||
核心 4 档是**用户可调**的「主界面字号」,默认值与用途如下(`Settings.cs:197-200`;`ClassIsland.Core/ThemeBase.axaml:32-35`;`Settings.cs:1209-1251`):
|
||
|
||
| Token | 默认 px | 用途 | 建议 CSS 变量 | 证据 |
|
||
|---|---|---|---|---|
|
||
| `MainWindowSecondaryFontSize` | 14 | 次要信息:天气副标题、日期星期、倒计时标签 | `--ci-fs-secondary` | `ThemeBase.axaml:32`;`WeatherComponent.axaml:261,294,296,305` |
|
||
| `MainWindowBodyFontSize` | 16 | 正文:课程名、时间、温度 | `--ci-fs-body` | `ThemeBase.axaml:33`;`MainWindow.axaml:313`;`CountDownComponent.axaml:65` |
|
||
| `MainWindowEmphasizedFontSize` | 18 | 强调:时钟、提醒主文案 | `--ci-fs-emphasized` | `ThemeBase.axaml:34`;`ClockComponent.axaml:14`;`TwoIconsMaskTemplate.axaml:14` |
|
||
| `MainWindowLargeFontSize` | 20 | 大号标题 | `--ci-fs-large` | `ThemeBase.axaml:35` |
|
||
|
||
扩展固定档(非用户设置,必须写死为 token 并说明来源):
|
||
|
||
| 字号 | 用途 | 建议 CSS 变量 | 证据 |
|
||
|---|---|---|---|
|
||
| 9 | 启动闪屏版本号(0.5 不透明度) | `--ci-fs-version` | `SplashWindow.axaml:86-87` |
|
||
| 12 | 主题卡作者名 | `--ci-fs-caption` | `ThemesSettingsPage.axaml:191` |
|
||
| 13 | 组件设置小字、ClassHelper 控件基准 | `--ci-fs-control-base` | `CountDownComponentSettingsControl.axaml:185-201`;`ClassHelper/renderer/styles/index.css:97` |
|
||
| 14 | 通用控件/次级标签 | `--ci-fs-label` | `MainWindow.axaml:406,452,497` |
|
||
| 16 | 主题卡名称、闪屏状态文字 | `--ci-fs-title` | `ThemesSettingsPage.axaml:188`;`SplashWindow.axaml:80` |
|
||
| 17 | 滚动提醒文字 | `--ci-fs-rolling` | `RollingTextTemplate.axaml:26` |
|
||
| 18 | OOBE 说明文字 | `--ci-fs-oobe-body` | `WelcomeWindowIntroControl.xaml:166` |
|
||
| 20 | 启动 logo 文字、导航标签 | `--ci-fs-brand` | `SplashWindow.axaml:56-58`;`StylesBase.axaml:118-123` |
|
||
| 22 | 天气温度/图标文字 | `--ci-fs-weather` | `WeatherComponent.axaml:86-175` |
|
||
| 32 | OOBE 大标题 | `--ci-fs-oobe-title` | `OobeIntroAnimationControl.axaml:69,87`;`WelcomePage.axaml:38` |
|
||
|
||
实现硬约束:
|
||
- 4 档核心字号必须由**用户设置驱动**,且必须能作用到「组件自身」与「单个主界面行」两个作用域:行级覆盖时把 4 个变量写到行根元素的 `style` 上;关闭行级覆盖时移除内联变量,回落 `:root`(`MainWindowCustomizableNodeHelper.cs:20-37`)。
|
||
- 缩放:所有字号乘以界面缩放 `--ci-scale`(默认 1.0),但**不得**用浏览器 zoom,必须用布局缩放以保持像素对齐(见 3.8)。
|
||
- 行高:源码未给出显式 `LineHeight`,建议正文 `line-height: 1.25`、说明文字 `1.6`【推断:依据 ClassHelper `theme.css:60-63` 的 `line-height:1.6` 与 ClassIsland 主界面 40px 行高/16px 字号的比值】。
|
||
|
||
### 3.3 圆角 Token
|
||
|
||
| Token | 默认值 | 用途 | 建议 CSS 变量 | 证据 |
|
||
|---|---|---|---|---|
|
||
| `RadiusX` / `RadiusY`(岛圆角) | `8`(UI 范围 `0–20`,步进 `0.5`,提示文案:调至最大时左右边缘为半圆) | 主界面行背景圆角;`SizeDoubleToCornerRadiusConverter` 以 `ConverterParameter=1.0` 直接取值(除以 1 即原值) | `--ci-radius-island: 8px` | `Settings.cs:183-184`、`:1187-1207`;`AppearanceSettingsPage.axaml:75-87`;`FluentTheme/Styles.axaml:48,91,124,160-168,252`;`SizeDoubleToCornerRadiusConverter.cs:12-18` |
|
||
| 行级自定义圆角 | `0`(关闭) | 单行覆盖圆角 | `--ci-radius-island-local` | `MainWindowLineSettings.cs:40-41` |
|
||
| 岛内容高度 | `40` | 所有行的容器高度(Fluent/Classic 同值) | `--ci-island-height: 40px` | `FluentTheme/Styles.axaml:65`;`ClassicTheme/Styles.axaml:16`;`MainWindowLine.axaml:16` |
|
||
| 弹窗圆角 | `6` | `.popup-bg`、编辑模式操作条 | `--ci-radius-popup: 6px` | `StylesBase.axaml:295`;`MainWindow.axaml:488,599` |
|
||
| 卡片圆角 | `4` | 设置项卡片、主题卡、Shimmer、时间线装饰器、OOBE 提示 | `--ci-radius-card: 4px` | `StylesBase.axaml:257-260`;`ThemesSettingsPage.axaml:152,161`;`Themes/Generic.xaml:214` |
|
||
| 控件圆角 | `4` | 按钮/输入框等 | `--ci-radius-control: 4px` | ClassHelper `index.css:52`;`theme.css:10` |
|
||
| 抽屉圆角 | `8 8 0 0` | 底部竖抽屉 | `--ci-radius-drawer: 8px 8px 0 0` | `VerticalDrawer.axaml:9` |
|
||
| 胶囊圆角 | `999` | 徽标、胶囊按钮 | `--ci-radius-pill: 999px` | `classic-island.css:308,454,541,694` |
|
||
| 圆形按钮 | `14`(28×28) | 主题卡安装按钮 | `--ci-radius-icon-btn: 14px` | `ThemesSettingsPage.axaml:246-249` |
|
||
| OOBE 大圆钮 | `24`(48×48) | 欢迎页「下一步」 | `--ci-radius-oobe-btn: 24px` | `WelcomePage.axaml:61-65` |
|
||
| 岛内小元素 | `0.5 × --ci-island-font` / `0.36 × --ci-island-font` | 岛内图标块 | 用 calc 派生 | `classic-island.css:388,403,476,502` |
|
||
|
||
实现硬约束:
|
||
- 岛圆角必须**四角同值**(源码把 `RadiusX` 同时传给宽高两个方向:`FluentTheme/Styles.axaml:31-34` 传两次 `RadiusX`),不得实现「椭圆圆角」。
|
||
- 圆角必须作用于「背景层」与「内容裁剪层」两层,且两层同值,否则内容会溢出圆角外(`FluentTheme/Styles.axaml:48,91,124,160-168`)。
|
||
- 若引入 ClassHelper 的连续圆角(squircle),必须保持 `SUPERELLIPSE_EXPONENT = 4.2`、每角 `CORNER_SEGMENTS = 14`、展开卡半径 `min(48*scale, w/2, h/2)` 不变(`ClassHelper/packages/desktop-client/src/island/squircle.ts:21,24,79-81`)。**默认实现必须用 CSS `border-radius`**(与 Avalonia `CornerRadius` 的直角圆弧一致,保证与 ClassIsland 逐像素相同);连续圆角只能由主题包显式启用(P2),不得作为默认岛外形。
|
||
|
||
### 3.4 间距 Token
|
||
|
||
| Token | 值 | 用途 | 建议 CSS 变量 | 证据 |
|
||
|---|---|---|---|---|
|
||
| 行内容左右内边距 | `12 0` | 岛内容区 `GridContentRoot.Margin` | `--ci-island-pad-x: 12px` | `FluentTheme/Styles.axaml:110`;`ClassicTheme/Styles.axaml:100` |
|
||
| 行内容最小宽度 | `20` | `MinWidth` | `--ci-island-min-w: 20px` | `FluentTheme/Styles.axaml:109` |
|
||
| 组件间距 | `6 0` | 每个 `ComponentPresenter.Margin`;**token 值取相邻间距 12px**(每个组件左右各 6px),供 `calc(var(--ci-component-gap) / 2)` 反推单侧 margin | `--ci-component-gap: 12px` | `MainWindow.axaml:396,546`;与 part 04 §4.4 对齐 |
|
||
| 行间距 | `5` | 多行堆叠 `StackPanel.Spacing`(用户设置) | `--ci-line-gap: 5px` | `Settings.cs:219`;`MainWindow.axaml:344` |
|
||
| 设置页容器 | `margin 12 12 18 12`;`max-width 960`;`spacing 4` | 设置页正文 | `--ci-settings-pad` / `--ci-settings-max-w: 960px` | `StylesBase.axaml:67-71`;`ThemeBase.axaml:37` |
|
||
| 组件设置容器 | `margin 4 0 4 12`;`max-width 960`;`spacing 4` | 子组件设置 | `--ci-component-settings-pad` | `StylesBase.axaml:73-77` |
|
||
| 弹窗内边距 | `12` | `.popup-bg` | `--ci-popup-pad: 12px` | `StylesBase.axaml:294` |
|
||
| 主题卡网格 | 单元高 `150`;目标宽 `328`;单元 `margin 2`;卡内 `padding 8` | 主题市场网格 | `--ci-theme-card-h: 150px`、`--ci-theme-card-w: 328px` | `ThemesSettingsPage.axaml:139-142,172` |
|
||
| 编辑模式操作条 | 底距 `64`;`padding 4`;按钮间距 `6` | 编辑态底部工具条 | `--ci-editbar-bottom: 64px` | `MainWindow.axaml:595-604` |
|
||
| 闪屏 | 尺寸 `400×100`;`padding 17`;logo 高 `24`;行距 `8`;底部进度条高 `4` | 启动闪屏 | `--ci-splash-w: 400px`、`--ci-splash-h: 100px` | `SplashWindow.axaml:16-17,24,29,42,92-96` |
|
||
| 岛内水平内边距(ClassHelper 风格) | `0.78 × 字号` | 岛胶囊左右内边距 | calc 派生 | `classic-island.css:215` |
|
||
| 进度条高度 | `3px` | 岛内倒计时/提醒进度条统一高度 | `--ci-progress-height: 3px` | 与 part 04 §4.4 对齐;源码进度条未显式设高(`FluentTheme/Styles.axaml:126-146`),取 FluentAvalonia ProgressBar 默认值【推断】 |
|
||
| 岛内元素间距(ClassHelper) | `0.62 × 字号` | 岛内行间距 | calc 派生 | `classic-island.css:212` |
|
||
|
||
### 3.5 透明与模糊 Token
|
||
|
||
| Token | 值 | 用途 | 建议实现 | 证据 |
|
||
|---|---|---|---|---|
|
||
| 主窗口透明级别 | `Transparent` | 非编辑态的岛窗口必须逐像素透明 | Electron `transparent: true` + `backgroundColor: '#00000000'` | `MainWindow.axaml:168-176` |
|
||
| 编辑模式透明级别 | `AcrylicBlur, Transparent`(回退 Transparent) | 编辑态窗口使用亚克力 | 只能用 CSS `backdrop-filter` 在自绘层模拟,**不得**用 `setBackgroundMaterial`(会破坏逐像素透明) | `MainWindow.axaml:177-181`;ClassHelper `island.ts:1726-1734` 明确禁止 |
|
||
| 普通窗口模式 | 若禁用透明则 `TransparencyLevelHint=None` | 窗口模式 | `transparent:false` | `MainWindow.axaml:187-190` |
|
||
| 闪屏透明 | `AcrylicBlur, Transparent` | 启动闪屏 | `backdrop-filter: blur(...)` + 半透明底 | `SplashWindow.axaml:21` |
|
||
| 岛背景不透明度 | `0.5`(默认范围 0–1,步进 0.05,滑块带数值气泡) | 背景层 opacity | `--ci-island-opacity`(主名;兼容别名 `--ci-island-bg-opacity`) | `Settings.cs:64,1141`;`AppearanceSettingsPage.axaml:61-72`;`FluentTheme/Styles.axaml:44` |
|
||
| 行整体不透明度 | `1.0`(行级设置,可被单行覆盖) | 行级 opacity | `--ci-line-opacity` | `MainWindowLineSettings.cs:42` |
|
||
| 鼠标移出淡化 | `0.05` | 悬停淡出模式 | `opacity:.05` | `MainWindowLine.axaml:73-75` |
|
||
| 提醒特效渲染缩放 | `1.0`;屏高 ≥1400 → `0.75`;≥2000 → `0.5`(自动时) | 强提醒涟漪窗口缩放 | `--ci-effect-scale` | `Settings.cs:150`;`MainWindow.axaml.cs:377-392` |
|
||
| 遮罩动画层级不透明度 | 0 → 1(0.1s) | 遮罩出现时附加投影 | `box-shadow` transition | `FluentTheme/Styles.axaml:355-367` |
|
||
|
||
硬规则:
|
||
- **不得**给岛卡片加 `backdrop-filter`(会把整块矩形模糊掉);模糊只允许出现在「编辑模式窗口背景」与「闪屏底」这两个整窗层。
|
||
- 岛必须是「透明窗口 + 自绘圆角卡片」,卡片外区域完全透明、可点击穿透(与 ClassHelper 一致,见 `island.ts:250-257`)。
|
||
|
||
### 3.6 层级(z-index)Token
|
||
|
||
| 层 | 数值 | 用途 | 建议 CSS 变量 | 证据 |
|
||
|---|---|---|---|---|
|
||
| 窗口层级-底层 | `WindowLayer=0` | 岛沉到桌面层(`Bottommost`),仅提醒/编辑时置顶 | `--ci-z-window:0` | `Settings.cs:52`;`MainWindow.axaml.cs:843-874` |
|
||
| 窗口层级-置顶 | `WindowLayer=1`(默认) | 岛始终置顶 | `--ci-z-window-top:1` | 同上;默认值 `Settings.cs:52` |
|
||
| 岛内容层 | 0 | 正常内容 | — | — |
|
||
| 遮罩/覆盖层 | 1 | 内容之上、背景之下 | `--ci-z-mask:1` | `FluentTheme/Styles.axaml:155-197` |
|
||
| 拖拽占位 | `1`(且 opacity .5) | 编辑态拖动项 | `--ci-z-drag:1` | `MainWindow.axaml:478-481` |
|
||
| 时间线装饰器 | `9999` | 分割线/行动装饰器必须压住时间块 | `--ci-z-timeline-adorn:9999` | `Themes/Generic.xaml:193,243` |
|
||
| 「无穷大」哨兵 | `33550336` | 必须永远最前的元素(OOBE 方块、时间线提示、拖拽阈值) | `--ci-z-sentinel:33550336` | `OobeIntroAnimationControl.axaml:45`;`TimeLineListControl.axaml:309`;`MainWindow.axaml:334` |
|
||
| 提醒特效窗口 | 独立 Topmost 窗口 | 全屏涟漪,高于一切 | 独立 BrowserWindow `alwaysOnTop` | `Views/TopmostEffectWindow.axaml.cs:120-128` |
|
||
| 教程聚光灯 | 独立置顶窗口 | 遮罩挖洞 | 独立窗口 | `Controls/Tutorial/TutorialSpotlightControl.axaml:41` |
|
||
| 抽屉遮罩 | 覆盖内容 | 横/竖抽屉 | `--ci-z-drawer:10`【推断】 | `DrawerHost.axaml`、`VerticalDrawer.axaml` |
|
||
| 应用外壳引导层 | `10000` | 全屏引导 | `--ch-z-guide:10000` | `ClassHelper/packages/ui/src/theme.css:155` |
|
||
| 主题切换快照层 | 旧 `1` / 新 `2` | View Transition 两帧 | `--ch-z-view-old/new` | `ClassHelper/renderer/styles/index.css:948-954` |
|
||
|
||
实现必须满足:
|
||
- 岛内容、遮罩、覆盖层在同一「行」内按上表叠放;多行之间不重叠(`MainWindowLine` 的 `ClipToBounds=False`,行本身不裁剪)。
|
||
- `33550336` 是源码里反复使用的「无穷大」哨兵(同时被用作拖拽阈值 `HorizontalDragThreshold="33550336"` 表达「禁止横向拖动」,见 `MainWindow.axaml:334`)。实现时可直接用 `z-index: 2147483647` 代替,但**必须**在常量注释里保留原值语义。
|
||
- 编辑器/设置中心内任何弹窗、Tooltip 都必须高于主内容;不得用负 z-index 实现遮罩。
|
||
|
||
### 3.7 DPI 与缩放规则
|
||
|
||
| 规则 | 数值/算法 | 证据 |
|
||
|---|---|---|
|
||
| 界面缩放用户设置 | `Scale`,默认 `1.0`,UI 范围 `0.75–2.5`,步进 `0.05`(滑块带数值气泡,格式 `F2`) | `Settings.cs:86,1153`;`Views/SettingPages/AppearanceSettingsPage.axaml:27-39` |
|
||
| 缩放实现 | 用 `LayoutTransform`(布局缩放)+ `ScaleTransform`,同时作用于所有行;**不得**用 `transform: scale()` 直接缩放窗口内容(会模糊) | `MainWindow.axaml:250-265` |
|
||
| 编码缩放补偿 | `GridRootLeft = Width/10*(Scale-1)`、`GridRootTop = Height/10*(Scale-1)` | `MainWindow.axaml.cs:881-882,938-939` |
|
||
| 编辑模式缩放边界 | `MinZoom 0.5` / `MaxZoom 3` / `ZoomSpeed 1.2`,`EnableConstrains=True`,显示缩放指示 | `MainWindow.axaml:199-209` |
|
||
| 窗口尺寸换算 | 逻辑宽 = 物理宽 / `dpiX`;逻辑高 = (物理高 − 1) / `dpiY`(−1 防被误判全屏) | `MainWindow.axaml.cs:969-971` |
|
||
| 窗口定位 | 使用**物理像素** `PixelPoint`,不做 dpi 换算 | `MainWindow.axaml.cs:905-909` |
|
||
| 安全区(主题声明) | 竖直安全区 = 当前启用主题 `VerticalSafeAreaPx` 的最大值;Fluent 主题为 `20` | `Services/XamlThemeService.cs:111-135`;`App.Services.xaml.cs:314` |
|
||
| 教程运行时的安全区 | 主题安全区与 `150` 取大者 | `MainWindow.axaml.cs:898-902` |
|
||
| 通知特效窗口缩放 | `1 / dpiX` | `MainWindow.axaml.cs:929` |
|
||
| 鼠标命中判定 | 屏幕坐标与 `Bounds * dpi * Scale` 比较 | `Controls/MainWindowLine.cs:625-636` |
|
||
| 字体缩放(关怀模式,应用外壳) | `--ch-font-scale: 1`;关怀模式 `1.15`,只放大字号不动几何 | `ClassHelper/renderer/styles/index.css:103,973-979` |
|
||
| 岛内字号缩放(ClassHelper 模型) | 所有岛内尺寸由 `--island-font` 用 `calc()` 派生 | `classic-island.css:2-7,57` |
|
||
|
||
实现要求:
|
||
- 所有视觉常量必须定义在 96 DPI 的 CSS px 基准上,再由 `--ci-scale` 统一放大;不得为不同 DPI 写两套值。
|
||
- 多显示器:每行/每窗口使用「所在显示器」的 DPI 与工作区;切换显示器必须重新计算窗口尺寸与位置(`GetSelectedScreenSafe`,`MainWindow.axaml.cs:806-812`)。
|
||
- `Scale` 改变时必须重算窗口宽高与安全区,不能只改 CSS transform。
|
||
|
||
### 3.8 深浅色规则
|
||
|
||
| 规则 | 要求 | 证据 |
|
||
|---|---|---|
|
||
| 主题模式枚举 | `0=跟随系统`、`1=明亮`、`2=黑暗`;默认 `2`(黑暗) | `Settings.cs:32,979-989`;`Services/ThemeService.cs:53-59` |
|
||
| 跟随系统实现 | `themeVariant=Default` + `preferSystemTheme=true`;系统切换要实时响应 | `ThemeService.cs:53-63` |
|
||
| 资源覆盖机制 | Light/Dark 两套 ThemeDictionary,同名 key 覆盖:`RainyColorBrush`、`LuminousClass`、6 个 AQI、`CustomizedAccentBarBackground1Brush`、`TextFillColorInformativeSecondaryBrush` | `App.axaml:35-38`;`Themes/LightTheme.axaml`、`Themes/DarkTheme.axaml` |
|
||
| 自定义前景色优先级 | 用户自定义前景色 > 主题前景色;关闭时回落主题 | `ControlColorHelper.cs:20-52` |
|
||
| 自定义背景色优先级 | 用户自定义背景色 > 主题背景;关闭时回落 `SolidBackgroundFillColor*` | `FluentTheme/Styles.axaml:51-56`;`ClassicTheme/Styles.axaml:216-218` |
|
||
| 深色下的阴影 | 阴影颜色不变(黑),只调不透明度;**不得**在深色下把投影改成白色 | Fluent `#48000000`/`#66000000`/`#33000000` 与 `VerticalDrawer.axaml:47` 注释「Even in WinUI, shadow is always black regardless of light/dark mode」 |
|
||
| ClassHelper 深色底色 | 应用壳亮 `#F3F3F3`/暗 `#161719`;卡片 `rgba(255,255,255,.7)` / `rgba(32,33,36,.72)` | `renderer/styles/index.css:27-48,114-137` |
|
||
| 主题切换过渡 | 窗口 `Background` 用 200ms 过渡;整页换肤用圆形扩散 View Transition(320×1.5=480ms) | `MainWindow.axaml:40-44`;`ClassHelper/renderer/composables/motion.ts:226-235` |
|
||
|
||
实现禁止项:
|
||
- 禁止在组件里写死 `#fff`/`#000`;必须走 `--ci-text-*`。
|
||
- 禁止只做深色、不做亮色(两套必须同步验收)。
|
||
- 禁止在深色模式使用纯黑 `#000000` 作为岛背景(默认自定义背景色是黑,但必须靠 `--ci-island-bg-opacity` 降为 50% 并叠加主题描边,否则与桌面融为一体)。
|
||
|
||
### 3.9 ClassHelper 侧落点(实现映射表)
|
||
|
||
| 内容 | 落点文件 | 要求 |
|
||
|---|---|---|
|
||
| 应用外壳 token | `packages/ui/src/tokens.css` | 保留 `--ch-ui-*`,新增 `--ci-*` 只读引用 |
|
||
| 应用外壳主题 | `packages/ui/src/theme.css` | 把 `--ci-accent` 注入 `--el-color-primary` 系列,`color-mix` 派生 |
|
||
| 桌面端全局样式 | `packages/desktop-client/src/renderer/styles/index.css` | 在 `:root` 与 `html.dark` 中定义 `--ci-*` 的亮/暗值 |
|
||
| 岛渲染进程样式 | `src/island/classic-island.css`、`custom-island.css` | 定义 `--island-*` 与 `--wn-*`(`classic-island.css:60-82`) |
|
||
| 动效常量 | `packages/shared/src/motion.ts` + 桌面端/管理端 `composables/motion.ts` | 新增 token 只加在 shared;不得两端各抄一份(`motion.ts:14-16` 明确约束) |
|
||
| 设计常量的 TS 形态 | `packages/shared/src/constants.ts` | 输出 `CLASSISLAND_TOKENS` 常量对象,供窗口几何计算复用 |
|
||
|
||
### 3.10 落地顺序与常见错误
|
||
|
||
实现顺序必须严格如下,不得跳步:
|
||
|
||
1. 先在 `packages/shared/src/constants.ts` 建立常量对象(颜色、字号、圆角、间距、层级、DPI 基准),字段名与本节 token 名一一对应,导出为只读(`as const`)。
|
||
2. 在 `packages/ui/src/tokens.css` 的 `:root` 写入亮色值,在 `html.dark` 写入深色值;两处必须保持键集合完全一致,缺一个键都算不合格。
|
||
3. 在 `packages/desktop-client/src/renderer/styles/index.css` 引入 `--ci-*` 的桌面端覆盖(通知特效窗口、闪屏窗口等独立渲染入口要单独引入同一份 token)。
|
||
4. 在 `src/island/classic-island.css` 与 `custom-island.css` 里把 `--island-*` 映射到 `--ci-*`,禁止在岛样式里重新定义颜色字面量。
|
||
5. 最后才写组件;组件只允许消费 token。
|
||
|
||
常见错误(必须在实现时主动规避):
|
||
|
||
- 把「行级字号覆盖」写成给整个岛设置字号:这样改一行会连带改所有行,与源码作用域不符(源码把资源写在行控件自身,见 `MainWindowCustomizableNodeHelper.cs:20-26`)。
|
||
- 把 5 级前景色实现成「同一颜色的不同 `opacity` 属性」:`opacity` 会连带影响子元素与阴影,源码用的是颜色 alpha 通道(`ControlColorHelper.cs:24-33`),必须写进颜色值本身。
|
||
- 用 `filter: drop-shadow()` 代替 `box-shadow` 实现岛投影:`filter` 会新建渲染表面,开合时出现闪帧(ClassHelper `classic-island.css:114-120` 已记录该问题)。
|
||
- 在深色主题下把阴影改成 `rgba(0,0,0,.6)` 以外的颜色,或直接删除阴影:源码在两种主题下都用黑色阴影,只改透明度。
|
||
- 忘记「安全区」:Fluent 主题声明了 `20px` 竖直安全区,岛贴边时会空出这段距离(`App.Services.xaml.cs:314`、`XamlThemeService.cs:111-135`),漏实现会导致岛压住任务栏或屏幕边缘。
|
||
- 忘记 `Scale` 变化时重算窗口:只改 CSS 会导致点击热区与可见区域错位(源码在 `MainWindow.axaml.cs:876-931` 每次都重算 `Bounds` 与 `ActualClientBound`)。
|
||
- 把「编辑模式操作条」的层级设在岛内容之下:它必须浮在行内容之上,且底距 64px(`MainWindow.axaml:595`)。
|
||
|
||
### 3.11 验收标准
|
||
|
||
实现者必须逐条自查,全部通过才算完成本节:
|
||
|
||
1. **颜色**:全仓库(渲染进程 + 主进程 + shared)搜索,除 token 定义文件外不得出现 `#` 开头的颜色字面量;`grep -r "#[0-9a-fA-F]\{6\}"` 的命中只允许出现在 token 文件与主题包预览样式中。
|
||
2. **强调色**:修改 `--ci-accent` 一处,岛背景描边、提醒遮罩、进度条、按钮强调态、天气图标强调色**全部**同步变化,无残留默认蓝。
|
||
3. **5 级前景**:开启自定义前景色后,5 个 alpha 分别为 100%/85.5%/77.3%/52.9%/36.5%;关闭后 5 个变量被移除并回落。
|
||
4. **字号**:把 4 档字号分别改成 10/20/30/40,岛内所有文字按对应档位变化且不溢出 40px 行高外的容器;行级覆盖可只影响单行。
|
||
5. **圆角**:把 `--ci-radius-island` 从 8 改到 20,背景层与裁剪层同时变化,内容不溢出四角;其他卡片圆角不受影响。
|
||
6. **间距**:改 `--ci-island-pad-x` 与 `--ci-component-gap`,内容宽度变化后背景宽度动画仍正常(见 part 14)。
|
||
7. **透明**:非编辑态岛卡片外区域 100% 透明(用取色器/截图验证 alpha=0);编辑态出现亚克力底;全程不允许整窗材质导致卡片外出现色块。
|
||
8. **层级**:编辑态拖动项浮在其他项之上;提醒遮罩覆盖内容但低于右上角操作按钮(若有);`33550336` 哨兵元素永远最前。
|
||
9. **DPI**:在 100%/125%/150%/200% 四档缩放下,岛高度逻辑值恒为 40、圆角逻辑值恒为 8、文字不模糊;跨显示器拖动后重新计算,无半个像素的缝。
|
||
10. **深浅色**:Light/Dark 各截图对照,两类主题的 AQI 色、雨天色、说明文字 alpha 与上表一致;深色下阴影仍为黑色。
|
||
11. **CSS 变量唯一来源**:`--ci-*` 只在两处定义(`:root` 与 `html.dark`),组件内只引用不重定义(行级作用域覆盖除外)。
|
||
12. **字符集**:所有 token 名使用 kebab-case,前缀统一为 `ci-`;禁止出现 `--ci-primaryColor` 这类混合命名。
|
||
|
||
---
|
||
|
||
## 4. 主界面(岛):窗口、行、背景、进度、遮罩、编辑模式
|
||
|
||
本节是「主界面(MainWindow,俗称岛)」的完整实现规格。实现技术栈固定为 ClassHelper 的 Electron + Vue 3 + TypeScript:主进程用 `BrowserWindow` 复刻 Avalonia `Window` 的窗口级行为,渲染进程用 Vue 单文件组件 + CSS 复刻 `MainWindow.axaml` 的控件树与全部像素参数。所有 Avalonia 的"逻辑像素(DIP,96 DPI 基准)"一律直接对应 CSS `px`;`Settings.Scale` 用 `transform: scale()` 实现。
|
||
|
||
### 4.1 术语与实现映射总则
|
||
|
||
必须先建立下面这张映射表,后文所有规则都按它落地:
|
||
|
||
| ClassIsland 概念 | 源码实体 | 本实现替代物 |
|
||
|---|---|---|
|
||
| 主界面窗口 | `MainWindow : Window`(`repos/ClassIsland/ClassIsland/MainWindow.axaml`) | 独立 `island.html` 渲染入口 + 主进程 `BrowserWindow`(透明、无边框、置顶、跳过任务栏) |
|
||
| 主界面行 | `MainWindowLine`(`repos/ClassIsland/ClassIsland/Controls/MainWindowLine.cs`) | `IslandLine.vue`(Vue 组件),一行一个 |
|
||
| 组件宿主 | `ComponentPresenter`(`repos/ClassIsland/ClassIsland.Core/Controls/ComponentPresenter.axaml`) | `ComponentHost.vue`(统一承载宽度约束/透明度/隐藏规则/裁剪) |
|
||
| 组件 | `DateComponent` / `ClockComponent` / … | `components/DateNode.vue` 等,见第 5 章 |
|
||
| 主题样式 | `XamlThemes/FluentTheme/Styles.axaml`、`ClassicTheme/Styles.axaml` | `island.css` + CSS 变量主题包(`--island-*`) |
|
||
| 编辑模式视图 | `Controls/EditMode/EditModeView.axaml` | `IslandEditMode.vue`(抽屉 + 拖动 + 选择) |
|
||
| 提醒宿主 | 行模板内的 Overlay / Mask 层 | 行内 `.line-mask` / `.line-overlay` |
|
||
|
||
强制约定:
|
||
|
||
1. 不得把主界面做成"一个自适应大小的 div 窗口",也不得照搬 ClassIsland 的"窗口高度随内容自适应"。采用 4.3.1 的**固定包围盒**:窗口宽度 = 所选显示器整屏宽(DIP),高度 = 预设最大内容高 + 上下安全余量;**可见岛**在窗口内形变,其高度 = 行高之和 + 行间距之和。窗口大于岛的透明区域不会遮挡桌面(Windows 侧整窗被设为鼠标穿透,见 4.2)。
|
||
2. 不得使用 `win.setBackgroundMaterial()`(亚克力/mica)。ClassIsland 的透明是逐像素 alpha + `WS_EX_LAYERED`,用系统材质会破坏像素级透明与圆角(`repos/ClassHelper-master/packages/desktop-client/src/main/island.ts:523-524` 的既有结论)。
|
||
3. 不得给主界面行加 CSS `box-shadow` 之外的重滤镜;Fluent 主题只有 `BoxShadow`,Classic 主题只有提醒遮罩期才挂 `DropShadowEffect`。
|
||
4. 所有尺寸、时长、缓动必须按本文给的确切值写死为常量或 CSS 变量,不得凭观感微调。
|
||
|
||
### 4.2 窗口(BrowserWindow)逐项规格
|
||
|
||
按下面顺序创建窗口(默认非编辑模式):
|
||
|
||
```ts
|
||
const win = new BrowserWindow({
|
||
...geometry, // 见 4.3,x / y / width / height
|
||
show: false,
|
||
frame: false, // Avalonia: SystemDecorations=None(MainWindow.axaml:171)
|
||
transparent: true, // Background=Transparent + TransparencyLevelHint=Transparent(:169-170)
|
||
resizable: false, // CanResize=False(:172)
|
||
movable: false,
|
||
minimizable: false, // CanMinimize=False(:174)
|
||
maximizable: false, // CanMaximize=False(:173)
|
||
fullscreenable: false,
|
||
skipTaskbar: true, // 默认不进任务栏;例外见下
|
||
hasShadow: false, // 阴影由岛自己的 CSS box-shadow 画
|
||
focusable: false, // 普通态不抢焦点(配合 showInactive)
|
||
alwaysOnTop: true, // WindowLayer 默认 1=置顶(Settings.cs:52)
|
||
type: 'toolbar', // 对应 WS_EX_TOOLWINDOW(MainWindow.axaml.cs:838)
|
||
backgroundColor: '#00000000',
|
||
webPreferences: {
|
||
preload: /* island preload 白名单 */,
|
||
contextIsolation: true,
|
||
nodeIntegration: false,
|
||
sandbox: true,
|
||
backgroundThrottling: false,
|
||
},
|
||
});
|
||
```
|
||
|
||
逐条规则:
|
||
|
||
1. **透明与无边框**:`transparent: true` + `frame: false` + `hasShadow: false` + `backgroundColor: '#00000000'`。HTML 根节点(`html, body, #app`)必须 `margin:0;padding:0;width:100%;height:100%;background:transparent;overflow:hidden;-webkit-user-select:none;user-select:none`(对齐 `repos/ClassHelper-master/packages/desktop-client/src/island/classic-island.css:8-19`)。
|
||
2. **鼠标穿透(核心)**:ClassIsland 在 Windows 上给主窗口加 `WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_NOACTIVATE` 并调用 `SetLayeredWindowAttributes`(`repos/ClassIsland/platforms/ClassIsland.Platforms.Windows/Services/WindowPlatformService.cs:130-150`),因此**非编辑模式下整个窗口对鼠标完全透明**,点击直接落到桌面。由此得:
|
||
- 非编辑模式:调用 `win.setIgnoreMouseEvents(true, { forward: true })`,且不得注册任何需要点击的交互(行、组件都不可点)。
|
||
- 编辑模式:调用 `win.setIgnoreMouseEvents(false)` 恢复命中,并 `win.focus()`。
|
||
- 不得实现"点岛展开/收起"这类交互;ClassIsland 的主界面是纯展示层。旧设置项 `IsMouseClickingEnabled` 已被废弃并被强制关闭(`repos/ClassIsland/ClassIsland/MainWindow.axaml.cs:733-737`,默认值 `false` 见 `Models/Settings.cs:53`)。
|
||
3. **不抢焦点**:显示时用 `win.showInactive()`;非编辑态 `focusable: false`。若为了接收触摸事件必须保持可激活,则只允许在编辑模式内切换 `focusable`,且退出编辑模式后立即恢复。
|
||
4. **置顶/置底**:`Settings.WindowLayer` 取值 `0=置底`、`1=置顶`,默认 `1`(`Models/Settings.cs:52`, `:1961-1978`)。置顶时 `win.setAlwaysOnTop(true, 'screen-saver')` 并 `win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true })`;置底时 `win.setAlwaysOnTop(false)` 并用 `setAlwaysOnTop(false)` + 必要时 `win.moveBottom()`(Windows 上等价于 `HWND_BOTTOM`,见 `WindowPlatformService.cs:151-159`)。置顶优先级还要被三个条件覆盖(`MainWindow.axaml.cs:791-803, 843-874`):
|
||
- 处于自由窗口模式(编辑模式的 windowed)或编辑模式时为普通窗口;
|
||
- `IsNotificationWindowExplicitShowed=true`(有提醒/编辑)时无论 `WindowLayer` 都置顶;
|
||
- `WindowTopmostRecheckMode` 决定重查时机:`0` 由窗口 Z 序变化事件驱动、`1` 前台窗口变化时、`2` 主计时器每 tick、`3` 1ms 高频计时器(`MainWindow.axaml.cs:420-436, 461-466, 687-704`,默认 `WindowTopmostRecheckMode = 0` 见 `Models/Settings.cs:1860`)。本实现的复核策略见 4.2.1,**禁止照搬 `3` 的 1ms 轮询**。
|
||
5. **任务栏显示**:`ShowInTaskbar` 是 `IsWindowMode || IsScreenRecordingModeEnabled` 的逻辑或(`MainWindow.axaml:34-39`)。即只有"编辑模式 + 自由窗口"或"屏幕录制模式"才进任务栏。实现用 `win.setSkipTaskbar(!(isWindowedEditMode || settings.isScreenRecordingModeEnabled))`。
|
||
- **P2 / 能力说明**:`IsWindowMode`(= `EditModeIsWindowMode && IsEditMode`,`ViewModels/MainViewModel.cs:363`)本任务**不实现具体 UI 入口**,只保留设置字段与"编辑模式可切成自由窗口"的能力说明。依据:ClassIsland 2.1 中该设置字段存在,但编辑页(`Controls/EditMode/EditModeView.axaml`)没有可见的切换按钮【推断:入口可能在教程步骤或后续版本】,实现者不得臆造按钮。
|
||
6. **防截屏**:`IsWindowCaptureBlockingEnabled=true` 时调用 `win.setContentProtection(true)`(对应 `WDA_EXCLUDEFROMCAPTURE`,`WindowPlatformService.cs:169-172`, `MainWindow.axaml.cs:840`)。
|
||
7. **屏幕录制模式**:`IsScreenRecordingModeEnabled=true` 时:不进 ToolWindow(任务栏可见)、不被置底。默认 `false`(`Models/Settings.cs:1861`)。
|
||
8. **窗口关闭拦截**:主界面窗口不得被用户关闭。ClassIsland 在 `Closing` 事件里 `e.Cancel = true`,只有应用主动退出才放行(`MainWindow.axaml.cs:577-590`)。实现:`win.on('close', (e) => { if (!appQuitting) e.preventDefault(); })`。
|
||
9. **禁止的窗口能力**:不得 `setAlwaysOnTop` 到 `'floating'` 以下级别;不得开启 `thickFrame`;不得给窗口加系统菜单;不得设置 `roundedCorners`(圆角由 CSS 画)。
|
||
10. **单实例守卫**:见第 12 章(本部分不重复),但主界面窗口必须在单实例锁之后创建。
|
||
|
||
#### 4.2.1 置顶复核与性能底线
|
||
|
||
**必须事件驱动 + 兜底轮询,禁止任何 1ms/10ms 级高频轮询。**
|
||
|
||
- 事件源(全部接上):`win.on('focus'/'blur')`、`screen` 的 `display-added/removed/metrics-changed`、编辑模式进出、`setVisibleOnAllWorkspaces` 变化、提醒开始/结束(对应 `TopmostLock` 的 `Acquire/Release`,`MainWindow.axaml.cs:1056-1092`)。
|
||
- 兜底轮询:仅在"当前是置顶态且窗口可见"时启用,间隔 **≥1000ms**,每次只做一次轻量 `setAlwaysOnTop(true, 'screen-saver')` 复核(对抗系统抢 Z 序)。窗口隐藏/置底时停止轮询。
|
||
- 禁止实现 `WindowTopmostRecheckMode === 3` 的 1ms 高频计时器(`MainWindow.axaml.cs:136-139`):那是原生 `SetWindowPos` 成本极低的平台红利,Electron 侧照搬会持续占用 CPU 并让渲染进程掉帧。
|
||
- 同类频率约束(本部分涉及的所有动画/刷新):
|
||
- 时钟/日期/倒计时/课程表:**1s 节拍**(整秒对齐);
|
||
- 提醒进度条:**100ms**(`ReduceProgressAccuracy=true` 时 250ms 即可);
|
||
- 行淡出/透明度过渡:由 CSS `transition` 驱动,**不得**用 JS 每帧写样式;
|
||
- 鼠标位置命中判定:只在 `pointermove`(编辑模式)或 200ms 兜底轮询(非编辑模式不需要,因为整窗穿透)。
|
||
|
||
### 4.3 几何:包围盒、可见内容盒与编辑画布
|
||
|
||
所有坐标计算以「所选显示器」为基准,显示器由 `Settings.WindowDockingMonitorIndex` 选定;越界或负数时回退主显示器(`MainWindow.axaml.cs:806-812`,默认 `0` 见 `Models/Settings.cs:51`)。设:
|
||
|
||
- `screen`:所选显示器;`screen.bounds` 为整屏物理像素,`screen.workArea` 为工作区物理像素;
|
||
- `dpiX = screen.scaleFactor`、`dpiY = screen.scaleFactor`(Avalonia 取 `screen.Scaling`,`MainWindow.axaml.cs:1019-1037`);
|
||
- `ox = Settings.WindowDockingOffsetX`、`oy = Settings.WindowDockingOffsetY`(单位:物理像素,默认 `0`、`0`,`Models/Settings.cs:49-50`);
|
||
- `ignoreArea = Settings.IsIgnoreWorkAreaEnabled`(默认 `false`,`Models/Settings.cs:1835`);
|
||
- `docking = Settings.WindowDockingLocation`(默认 `1`,`Models/Settings.cs:48`),取值为:
|
||
|
||
```
|
||
###############
|
||
# 0 1 2 #
|
||
# #
|
||
# 3 4 5 #
|
||
###############
|
||
```
|
||
|
||
即 `0/1/2 = 上(左/中/右)`、`3/4/5 = 下(左/中/右)`(`Models/Settings.cs:1810-1823`)。
|
||
|
||
#### 4.3.1 实现策略裁决:固定包围盒 + 窗内形变
|
||
|
||
ClassIsland 的原生模型是"窗口高度随内容自适应"(`SizeToContent = Height`,`MainWindow.axaml.cs:781`;窗口高 = 内容高 + 安全区,`:916`)。**本实现不得照搬这一条**:Electron 的逐像素透明窗口在频繁 `setBounds` 时会触发 DWM 重排与透明表面重建,产生整块闪烁/掉帧,而行增删、提醒展开、时钟秒数出现都会触发高度变化。因此采用 ClassHelper 已验证的「固定包围盒 + 窗内形变」方案(`repos/ClassHelper-master/packages/desktop-client/src/main/island.ts:480-512`,注释 `:78-90`):
|
||
|
||
1. **窗口创建时一次算好包围盒**:
|
||
- `winW = 目标屏幕宽(DIP) = screen.bounds.width / dpiX`;
|
||
- `winH = contentMaxHeight + 2 * safePadding`,其中 `contentMaxHeight` = 预设最大内容高(默认按"当前可见行数 × (40 + 行间距 5) + 上下安全区"计算,并留出一行余量),`safePadding ≥ 20`(阴影 + 光晕余量,对齐 `ISLAND_SHADOW_PAD = 10` 的双侧口径)。
|
||
2. **可见岛在窗口内形变**:窗口不动,岛用 CSS `height` 过渡(或第 14 章的 spring 形变)在包围盒内伸展/收缩;内容层按 4.5 的行高与间距排布。
|
||
3. **只有下列情形才 `setBounds`**:显示器/DPI 变化、行数超出包围盒上限、进入/退出编辑模式、窗口偏移/缩放/停靠位置设置变化。这类变更必须**节流 ≥100ms**(同一窗口内多次合并为一次),且不得与弹簧/高度动画同帧竞争。
|
||
4. **视觉结果必须与 ClassIsland 一致**:用户看到的**可见岛高度 = 行高之和 + 行间距之和**(行高恒 `40`,间距 `MainWindowLineVerticalMargin`),**可见岛宽度 = 内容自然宽 + 两侧各 12px 内边距**。窗口比岛大只是透明余量,用户不可见。
|
||
5. **禁止**:把窗口高度做成"刚好等于内容高"再逐帧改;用 `requestAnimationFrame` 每帧 `setBounds`;在行内容变化(秒数、课程名长度、提醒)时改窗口尺寸。以上都会导致 1px 级抖动。
|
||
|
||
**与 ClassIsland 的差异及理由(必须写进实现注释)**:差异 = 窗口高度固定为包围盒 vs 随内容自适应;理由 = Electron 透明窗口 resize 抖动/闪烁,且 ClassHelper 的既有实现已证明"固定包围盒 + 窗内画布形变"在开合过程中零抖动(见该文件 `:64-70` 的设计说明)。**视觉结果不做任何妥协**:可见岛的行高、间距、圆角、阴影与 4.5 完全一致。
|
||
|
||
#### 4.3.2 可见内容盒(视觉基准公式)
|
||
|
||
下面的 ClassIsland 原生公式**只用于计算"可见岛"的内容尺寸、安全区裁剪与停靠对齐**,不直接作为 `BrowserWindow.setBounds` 的 height 参数(`setBounds` 规则见 4.3.1):
|
||
|
||
1. `contentW`、`contentH`:由渲染进程上报的岛内容尺寸(CSS px)。内容盒 = 所有行高度之和 + 行间距之和(见 4.5)。
|
||
2. `safe = 主题竖直安全区`:Fluent 主题 `20`(`repos/ClassIsland/ClassIsland/App.Services.xaml.cs:314`),Classic 主题 `0`(`ThemeManifest._verticalSafeAreaPx` 默认 0,`repos/ClassIsland/ClassIsland.Core/Models/XamlTheme/ThemeManifest.cs:20`)。若正在播放教程且教程吸附在本窗口,则 `safe = max(safe, 150)`(`MainWindow.axaml.cs:899-902`)。
|
||
3. `dockingTop = docking ∈ {0,1,2}`。
|
||
4. `safeT = Math.max(dockingTop ? Math.min(safe, oy) : safe, 0) * scale`。
|
||
5. `safeB = Math.max(dockingTop ? safe : Math.min(safe, -oy), 0) * scale`。
|
||
6. `岛锚点 X = screen.workArea.x + ox`;`岛锚点 Y = dockingTop ? top + oy - safeT : bottom - contentH + oy + safeB`,其中 `top = ignoreArea ? screen.bounds.y : screen.workArea.y`,`bottom = ignoreArea ? screen.bounds.bottom : screen.workArea.bottom`。
|
||
7. `layoutW = screen.bounds.width / dpiX`(布局容器宽 = 整屏宽,用于左/中/右对齐计算,`MainWindow.axaml.cs:915`)。
|
||
8. `layoutH = contentH + safeT + safeB`(岛的可布局高度;这只是内容画布高,不是窗口 `height`)。
|
||
9. 岛在画布内的偏移:`translateY = safeT`、`translateX = 0`;水平对齐:`docking ∈ {0,3}` → 左、`{1,4}` → 居中、`{2,5}` → 右(对应 `align-items: flex-start / center / flex-end`,与 `WidthDoubleToRectConverter` 的 X 偏移逻辑一致,`repos/ClassIsland/ClassIsland.Core/Converters/WidthDoubleToRectConverter.cs:66-77`)。
|
||
10. 窗口自身的包围盒位置必须让"岛锚点"落在公式 6 的位置:`winX = 岛锚点X`、`winY = 岛锚点Y - safePadding`(上停靠)/ `岛锚点Y - contentMaxHeight - safePadding`(下停靠,包围盒底部贴锚点)。
|
||
|
||
#### 4.3.3 编辑模式窗口
|
||
|
||
进入编辑模式时**窗口必须扩展为全屏编辑画布**(这一步允许 `setBounds`,因为是模式切换、不是内容变化):
|
||
|
||
1. `winX = screen.bounds.x`、`winY = screen.bounds.y`。
|
||
2. `winW = screen.bounds.width / dpiX`。
|
||
3. `winH = (screen.bounds.height - 1) / dpiY`——**必须减 1px**,防止 Windows 把窗口判定成全屏并触发全屏优化(`MainWindow.axaml.cs:970`)。
|
||
4. **岛内容在窗口内四边各留 60px 安全边距**:`padding = 60px`(来源 `WindowedEditModeScreenMargin = 60`,`MainWindow.axaml.cs:134`),即编辑画布的可用区为 `winW - 120` × `winH - 120`,岛在其中垂直居中、水平按停靠位置对齐。
|
||
5. 内容偏移:`translateX = ox / dpiX`、`translateY = oy / dpiY`(编辑模式偏移是逻辑像素,`MainWindow.axaml.cs:973-974`);`clientRect = ignoreArea ? screen.bounds : screen.workArea`。
|
||
6. 鼠标事件**必须开启**:`win.setIgnoreMouseEvents(false)`(见 4.2 第 2 条)。
|
||
7. **编辑模式 + 自由窗口(windowed)**:窗口改为取 `screen.workArea` 四边各内缩 60px(`winX = workArea.x + 60`、`winY = workArea.y + 60`、`winW = (workArea.width - 120) / dpiX`、`winH = (workArea.height - 120) / dpiY`),切回系统装饰(`SystemDecorations=Full`、可缩放/最大化/最小化,`MainWindow.axaml:182-191`),标题 `编辑主界面 | ClassIsland`(`MainWindow.axaml:180`)。
|
||
8. **退出编辑模式**:恢复 4.3.1 的包围盒(位置 + 尺寸),`win.setIgnoreMouseEvents(true, { forward: true })`,恢复 `skipTaskbar`。
|
||
|
||
#### 4.3.4 触发重算的时机与节流
|
||
|
||
必须接上这些时机,但全部走"标记脏 + ≥100ms 节流"的通道,不得每次直接 `setBounds`:
|
||
|
||
- **包围盒**(4.3.1):显示器/DPI/显示器插拔变化、行数超出 `contentMaxHeight`、编辑模式进出、偏移/缩放/停靠设置变化;
|
||
- **岛内容布局**(4.3.2,纯 CSS,不改窗口):主计时器每 tick(`LessonsServiceOnPreMainTimerTicked`,`MainWindow.axaml.cs:475`)、内容容器尺寸变化(`StackPanelRootContainer_OnSizeChanged`,`MainWindow.axaml.cs:786-789`)、任何 `Settings` 属性变化(`MainWindow.axaml.cs:488-501, 730-782`)。
|
||
|
||
**节流与稳定**:用 `requestAnimationFrame` 合并同一帧内的多次 `setBounds`;同一窗口尺寸在 100ms 内多次变化时只保留最后一次;高度目标值四舍五入到整数 CSS px。禁止在主计时器 tick 里直接改窗口尺寸。
|
||
|
||
**恢复与兜底**:`WindowDockingMonitorIndex` 超出显示器数量时回退主屏(`MainWindow.axaml.cs:806-812`);`contentH` 为 0(无行/全部隐藏)时岛整体 `opacity:0`(窗口保留包围盒、不改尺寸),避免窗口在 0 高附近抖动。
|
||
|
||
### 4.4 DOM 层级与 CSS 变量
|
||
|
||
必须严格复刻下面这棵树(左侧为 `MainWindow.axaml` 的控件,右侧为实现用的标签/类名):
|
||
|
||
```
|
||
Window → html > body > #app
|
||
└ Grid#WindowRoot → .window-root(透明,pointer-events:none)
|
||
└ ZoomBorder → .zoom-layer(缩放/平移宿主,overflow:visible)
|
||
└ Canvas#LayoutContainerGrid → .layout-container(position:relative; width:100%;height:100%)
|
||
└ Panel#WorkingRoot → .working-root(position:absolute; left:0; top:safeT; width:窗口宽; height:contentH)
|
||
└ StackPanel#StackPanelRootContainer → .lines-root(display:flex; flex-direction:column; align-items:center|flex-start|flex-end)
|
||
└ LayoutTransformControl → .lines-transform(transform: translate(ox,oy) scale(scale))
|
||
└ Grid#GridRoot → .grid-root(每行背景/遮罩的公共祖先)
|
||
└ Border#ResourceLoaderBorder → .resource-loader(font-size:16px,字体族/字重继承)
|
||
└ Grid → .lines-host
|
||
├ StackPanel → .lines-stack(display:flex; flex-direction:column; row-gap:5px)
|
||
│ └ ItemsControl#RootMainWindowLinesItemsControl → .lines-list
|
||
│ └ (每行) Grid ColumnDefinitions="Auto,*,Auto" → .line-row(display:grid; grid-template-columns:auto 1fr auto)
|
||
└ ItemsControl#ContainerComponentsEditHost → .container-edit-host(仅编辑模式,position:absolute)
|
||
```
|
||
|
||
再加两个编辑模式专用兄弟节点:`.operate-area`(底部操作栏)、`.edit-mode-view`(抽屉宿主)。
|
||
|
||
CSS 根变量必须在 `#app` 或 `.grid-root` 上集中声明(对齐 ClassHelper 的 `--island-*` 命名习惯,`repos/ClassHelper-master/packages/desktop-client/src/island/IslandApp.vue:522-528`):
|
||
|
||
```css
|
||
: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`):
|
||
|
||
```css
|
||
.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`):
|
||
|
||
```css
|
||
.component-island {
|
||
position: relative;
|
||
margin: -2px 0; /* Panel Margin="-2 0" */
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
.component-island > .bg {
|
||
position: absolute; inset: 0;
|
||
background: 行背景色; opacity: var(--island-opacity);
|
||
border: 1px solid 边框色;
|
||
border-radius: 8px;
|
||
box-shadow: 0 4px 8px 2px #48000000;
|
||
}
|
||
.component-island > .content {
|
||
position: relative;
|
||
min-width: 40px; /* Border MinWidth="40" */
|
||
padding: 0 16px; /* ContentPresenter Margin="16 0" */
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
}
|
||
```
|
||
|
||
- 分体模式下窗口内容仍按整行排布(`MainWindow.axaml:213-217` 的 `WorkingRoot` 宽度 = 客户端宽),但每行的高度仍 `40`,行内组件按原顺序横排,胶囊之间保留 `12px` 间隔(每个组件 `Margin="6 0"`)。
|
||
- 分体模式下行的 `clip-host` 不再参与裁剪(`FluentTheme/Styles.axaml:21`),阴影由组件胶囊自己承担。
|
||
|
||
#### 4.5.7 行的过渡动画
|
||
|
||
- 行内容透明度:`opacity` 过渡 `100ms`,线性(`DoubleTransition Property="Opacity" Duration="0:0:0.100"`,`FluentTheme/Styles.axaml:268`、`ClassicTheme/Styles.axaml:206`)。
|
||
- 背景宽度:Fluent `300ms` `cubic-bezier(0.65, 0, 0.35, 1.0)`(`FluentTheme/Styles.axaml:269`);Classic `550ms` `cubic-bezier(0, 1, 0.5, 1.10)`(`ClassicTheme/Styles.axaml:207`,注意控制点 `y>1` 会产生一次轻微回弹,必须照写)。
|
||
- 行首次挂载淡入:`AnimationLevel ≥ 2` 时用组合层做 `opacity 0→1`、`250ms`、`cubic-bezier(0.25, 1, 0.5, 1)`(`MainWindowLine.cs:382-401`)。`AnimationLevel` 默认 `1`(`Models/Settings.cs:1851`),即默认不播。
|
||
- 行宽变化(组件增删、内容变化)由"自适应面板重排动画"驱动:`WrapPanelResizingAnimationAssist.Duration = 0.300s`、`Easing = (0.65, 0, 0.35, 1.0)`,挂在 `Grid#WindowRoot` 上(`MainWindow.axaml:197-198`),并把 `animated-resize` 类加给行的外层 `Panel`(`FluentTheme/Styles.axaml:103`)。编辑模式下该动画关闭(`MainWindow.axaml:345` 的 `Classes.animated-resize="{Binding !MainWindowInEditMode}"`)。
|
||
|
||
#### 4.5.8 行设置项(`MainWindowLineSettings`)与默认值
|
||
|
||
必须实现为一个 JSON 结构 + 抽屉表单。字段与默认值一律照抄(`repos/ClassIsland/ClassIsland.Core/Models/Components/MainWindowLineSettings.cs:17-50`):
|
||
|
||
| 字段 | 类型 | 默认 | 说明 / UI |
|
||
|---|---|---|---|
|
||
| `Children` | 数组 | `[]` | 行内组件 |
|
||
| `IsMainLine` | bool | `false` | 主要行;全局唯一 |
|
||
| `IsNotificationEnabled` | bool | `true` | 是否允许显示提醒;关掉后该行用 Min 模板 |
|
||
| `IsResourceOverridingEnabled` | bool | `false` | 覆盖字号资源 |
|
||
| `MainWindowSecondaryFontSize` | number | `14` | 次级字号,范围 1–40 |
|
||
| `MainWindowBodyFontSize` | number | `16` | 正文字号 |
|
||
| `MainWindowEmphasizedFontSize` | number | `18` | 强调字号 |
|
||
| `MainWindowLargeFontSize` | number | `20` | 大号字号 |
|
||
| `IsCustomForegroundColorEnabled` | bool | `false` | 自定义字体颜色 |
|
||
| `ForegroundColor` | color | `DodgerBlue` | 默认字体色 |
|
||
| `BackgroundOpacity` | number | `0.5` | 自定义背景不透明度 |
|
||
| `IsCustomBackgroundOpacityEnabled` | bool | `false` | |
|
||
| `BackgroundColor` | color | `Black` | |
|
||
| `IsCustomBackgroundColorEnabled` | bool | `false` | |
|
||
| `CustomCornerRadius` | number | `0` | 自定义圆角,范围 0–20 |
|
||
| `IsCustomCornerRadiusEnabled` | bool | `false` | |
|
||
| `Opacity` | number | `1.0` | 整行不透明度,范围 0.1–1 |
|
||
| `IsVisible` | bool | `true` | 运行时态(规则求值结果) |
|
||
| `HideOnRule` | bool | `false` | 按规则隐藏 |
|
||
| `HidingRules` | Ruleset | `{}` | 规则集 |
|
||
| `IslandSeparationMode` | int | `0` | 0 继承 / 1 禁用 / 2 启用 |
|
||
|
||
行为约束:
|
||
|
||
- 主要行唯一:勾选某行为主要行时,其余行必须自动取消(`MainWindow.axaml.cs:1569-1588`);取消唯一的主要行时,自动把第一行设为主要行并弹 toast"已将第一行设置为主要行。"
|
||
- 主要行是提醒注册的优先消费者(注册序号 `-1`,非主要行用行号,`MainWindowLine.cs:441`)。
|
||
- **本部分只负责"提醒宿主"与 `IsNotificationEnabled` 标记**:行模板的遮罩/浮层/进度条容器(4.7)、行的可见性联动、`IsNotificationEnabled` 决定走 Full 还是 Min 模板。**多行之间谁显示提醒的分发规则归属第 7 章(提醒系统)**,本文不重复、不得在此臆造优先级。
|
||
- 删除行:只剩 1 行时禁止删除并弹警告"至少需要保留 1 个主界面行。"(`MainWindow.axaml.cs:1598-1612`)。
|
||
- 关闭所有行的提醒显示时弹警告"您已经禁用了所有主界面行的提醒显示功能……"(`MainWindow.axaml.cs:1590-1596`)。
|
||
|
||
#### 4.5.9 行内编辑控件(仅在编辑模式显示)
|
||
|
||
行模板右侧第三列(`MainWindow.axaml:401-444`)在编辑模式显示一组按钮,容器属性:
|
||
|
||
```css
|
||
.line-actions { /* StackPanel Classes="items-valign-center" */
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 2px; /* Spacing="2" */
|
||
font-size: 14px; /* TextElement.FontSize="14" */
|
||
}
|
||
.line-actions .btn { background: transparent; border: none; }
|
||
.line-actions .btn-delete { color: IndianRed; }
|
||
```
|
||
|
||
四个按钮(自左至右,全部 `VerticalAlignment=Center`、`Background=Transparent`、`BorderThickness=0`):
|
||
|
||
1. 行设置:图标 `\uEF27`,ToolTip "设置…",打开行设置抽屉。
|
||
2. 主要行开关(ToggleButton):图标 `\uE994`,ToolTip "此行是主要行"。
|
||
3. 提醒开关(ToggleButton):图标 `\uE02B`,ToolTip "此行可以显示提醒"。
|
||
4. 删除(Button,`Foreground=IndianRed`,`Theme=TransparentButton`):图标 `\uE61D`,ToolTip "删除"。
|
||
|
||
行选择框(第一列的虚线矩形,`MainWindow.axaml:353-363`):
|
||
|
||
```css
|
||
.line-select-box {
|
||
position: absolute; inset: 0;
|
||
border: 1.5px dashed rgba(控制描边色);
|
||
border-radius: 0; /* 无圆角 */
|
||
opacity: 0.75;
|
||
/* StrokeDashArray="1, 1.5" → dash 1px、gap 1.5px */
|
||
background: none;
|
||
}
|
||
```
|
||
|
||
拖动把手(第一列,`MainWindow.axaml:364-367`):`TouchDragThumb`,仅在编辑模式可见,垂直方向拖动改变行顺序;拖动阈值由 `AdvancedItemDragBehavior` 决定,其 `Orientation="Vertical"`、`HorizontalDragThreshold="33550336"`(即水平拖拽被彻底禁用)、`AllowDragFromDragThumbOnly="True"`(`MainWindow.axaml:332-335`)。
|
||
|
||
"新主界面行"按钮(`MainWindow.axaml:449-455`):水平居中、`font-size:14px`、内容为图标 `\uE00D` + 文本"新主界面行";仅在编辑模式可见;点击追加一行默认配置并在该行下方插入。
|
||
|
||
### 4.6 主界面的缩放(裁决:只做缩放,不做平移)
|
||
|
||
- `ZoomBorder`(`MainWindow.axaml:199-212`)必须实现为 `.zoom-layer`,参数:`Stretch=None`、`ZoomSpeed=1.2`(每格缩放倍率)、`EnableConstrains=True`、`MaxZoomX=MaxZoomY=3`、`MinZoomX=MinZoomY=0.5`、`ShowZoomIndicator=true`、`EnableGestureRotation=False`、`Focusable=true`、`Background=Transparent`。
|
||
- **缩放手势固定为 `Ctrl + 滚轮`**:以指针位置为锚点缩放,每格倍率按 `ZoomSpeed = 1.2` 指数累乘,范围钳制在 `[0.5, 3.0]`(= `MinZoomX/Y` 与 `MaxZoomX/Y`)。不带 `Ctrl` 的滚轮不得缩放(避免与页面/抽屉滚动冲突)。
|
||
- **双击画布空白处重置为 100%**(等价 `ResetMatrix`);底部操作栏的"重置视图"按钮同样恢复 100%。
|
||
- 缩放指示器:非 100% 时在窗口左下角显示当前倍率,`font-size:12px`,`background:#77222222`、`color:#fff`【推断:样式对齐 `MainWindow.axaml:643-649` 的调试文本样式】。
|
||
- **P2 / 不做平移**:首版只实现 `Ctrl+滚轮` 缩放与双击重置;`Space + 拖拽` 平移标 **P2 /【推断】**(ClassIsland 的 `ZoomBorder` 支持拖拽平移,但控件源码未定位,且非编辑态整窗鼠标穿透用不到),不实现。
|
||
- **设置里的"界面缩放"是另一件事**:`Settings.Scale` 默认 `1.0`,范围 `0.75–2.5`、步进 `0.05`(`Models/Settings.cs:86`、`EditModeView.axaml:50-57`),应用在 `.lines-transform` 的 `transform: scale()`,`transform-origin` 需按停靠位置设置:上停靠 `center top`、下停靠 `center bottom`(对齐 `LayoutTransformControl` 的布局缩放语义,`MainWindow.axaml:257-265`)。
|
||
- 两者叠加:总视觉倍率 = 缩放设置 × ZoomBorder 倍率;**窗口包围盒只由"未缩放的内容尺寸"决定**(见 4.3.1),缩放只改渲染层 `transform`,不改窗口尺寸。
|
||
- **不做平移**:见上条 P2 说明;`EnableConstrains=true` 仅用于把缩放后的内容约束在窗口内。
|
||
|
||
### 4.7 提醒宿主、遮罩与进度条(行内)
|
||
|
||
Full 模板的行内部结构(`XamlThemes/FluentTheme/Styles.axaml:70-202`)必须按下面层级实现:
|
||
|
||
```
|
||
.line (Grid#GridRoot)
|
||
├ .line-bg-wrap → Border#BackgroundBorderWrapper (整行背景层,分体模式隐藏)
|
||
│ └ .line-bg → Border#BackgroundBorder (Classes: docking line-background)
|
||
├ .line-bg-mask → Border#BackgroundBorderOverlayMask (强调色遮罩,Opacity 0→1,仅提醒时)
|
||
│ · opacity 过渡只对 BoxShadow 做 0.1s
|
||
├ .line-clip → Grid#ContentClipBorder (Classes: clip-host)
|
||
│ └ .line-panel → Panel (Classes: animated-resize)
|
||
│ └ .grid-wrapper → Grid#PART_GridWrapper (height 40, Classes: docking)
|
||
│ ├ .content-root → Grid#GridContentRoot (height 40, min-width 20, margin 0 12)
|
||
│ │ └ .components (组件列表)
|
||
│ ├ .overlay → Grid#GridOverlay (opacity 0)
|
||
│ │ ├ .overlay-bg → Border (line-background,分体模式才可见)
|
||
│ │ └ .overlay-clip → Border (clip, corner-radius 8)
|
||
│ │ └ Panel
|
||
│ │ ├ Canvas#ProgressContainer (bottom, height = progressBar 高度)
|
||
│ │ │ └ ProgressBar#OverlayTimeProgressBar (Maximum 1, Value = LeftProgress, MinWidth 0)
|
||
│ │ └ ContentPresenter(OverlayContent,MaxHeight 40)
|
||
│ └ .mask-shadow → Border#OverlayMaskShadowRoot (仅非编辑模式)
|
||
│ └ .mask → Border#OverlayMask (背景 AccentFillColorDefaultBrush, 前景 TextOnAccentFillColorPrimaryBrush, clip, height 40)
|
||
│ └ Panel
|
||
│ ├ SlantedMaskControl (Classes: mask-bg, RegionDurationMs=360, StageStaggerMs=120, Fill=强调色)
|
||
│ └ Border (Padding 0 12)
|
||
│ └ .mask-content → ContentPresenter#OverlayMaskContent (opacity 0, scale)
|
||
```
|
||
|
||
#### 4.7.1 进度条(提醒剩余时间)
|
||
|
||
- 容器 `Canvas` 贴底、水平拉伸,高度绑定进度条自身高度(`FluentTheme/Styles.axaml:127-147`)。
|
||
- `ProgressBar`:`Maximum=1`、`Value = 当前提醒的 LeftProgress`(0–1,`MainWindowLine.axaml:77`)、`MinWidth=0`、`Width = 容器宽`、贴底。
|
||
- 值变化过渡:`50ms`、线性(`DoubleTransition Property="Value" Duration="0:0:0.05"` + `LinearEasing`,`FluentTheme/Styles.axaml:137-145`)。
|
||
- 实现:进度条高度**固定 `3px`**(token `--ci-progress-height: 3px`,`border-radius: 1.5px`,贴底;三条进度条——行提醒进度条、课程进度条、倒计时进度条——共用同一 token),颜色取 `AccentFillColorDefaultBrush`,`transform: scaleX(value)` + `transform-origin:left` 比改 width 更省重排;但不能有过渡抖动,`transition: transform 50ms linear`。
|
||
- `ReduceProgressAccuracy=true`(默认,`Models/Settings.cs:1853`)时进度按步长量化:`progressTick = max(10, totalSeconds/500)`,`value = round(seconds/progressTick)*progressTick`(倒计时组件同款算法,`Controls/Components/CountDownComponent.axaml.cs:288-295`)。主界面行进度条同样应做降频更新(100ms 级),`false` 时按 1 秒精度。
|
||
|
||
#### 4.7.2 遮罩(Mask)动画
|
||
|
||
- `SlantedMaskControl`:5 个区段宽度权重 `[0.0, 0.1, 0.2, 0.4, 0.2, 0.1]` 的累计边界,斜面角度固定 `60°`(`Controls/SlantedMaskControl.cs:284, 298-308`),填充色 `AccentFillColorDefaultBrush`。
|
||
- 参数:`RegionDurationMs = 360`、`StageStaggerMs = 120`(行模板里显式赋值,`FluentTheme/Styles.axaml:178-179`);控件默认值 `300`/`120`(`SlantedMaskControl.cs:29-33`)——以行模板的 `360/120` 为准。
|
||
- 打开(`mask-in`)顺序:区段 0 与 4 立即开始(delay 0),区段 1 与 3 延迟 `stag*0.75 = 90ms`,区段 2 延迟 `stag*1 = 120ms`;缓动 `QuarticEaseOut`;每段时长为独立的关键帧(`SlantedMaskControl.cs:167-188`)。总时长 = `120 + 360 = 480ms`。
|
||
- 关闭(`mask-out`)顺序反向:区段 2 先(delay 0),1/3 延迟 90ms,0/4 延迟 120ms;缓动 `QuadraticEaseIn`(`SlantedMaskControl.cs:189-200`)。
|
||
- CSS 实现建议:遮罩层用 5 个 `clip-path` 平行四边形(`skewX(-30deg)` 等价于 60° 斜面),每个用 `animation: mask-region 360ms <ease> <delay> both`,`--scale-x: 0 → 1`,`transform-origin` 取各自区段中心。
|
||
- 遮罩背后的内容:`Grid#GridContentRoot.Opacity` 在 `mask-in` 时置 0,`:mask-out` 时也置 0,`:overlay-in/out` 控制恢复(`FluentTheme/Styles.axaml:293-305, 368-370, 411-413, 438-450`)。
|
||
- 遮罩投影:`mask-in` 期间背景层与遮罩层加 `BoxShadow = 0 8 32 0 #66000000`(`FluentTheme/Styles.axaml:323-328`),`mask-out` 时淡掉。
|
||
- 遮罩内容入场:`delay 260ms`、`duration 250ms`、`ease cubic-bezier(0.25, 1, 0.5, 1)`、`opacity 0→1`(`FluentTheme/Styles.axaml:329-340`);同时 `scale 1.1→1.0`、`duration 750ms`、`delay 260ms`、同缓动(`:341-352`)。
|
||
- 遮罩内容容器:`Padding="12 0"`、前景色 `TextOnAccentFillColorPrimaryBrush`(`FluentTheme/Styles.axaml:181-184`)。
|
||
- 强调色底:`Border#BackgroundBorderOverlayMask` 背景 = `AccentFillColorDefaultBrush`(`StylesBase.axaml:90-93`),`Opacity 0→1`、`delay 600ms`、`duration 100ms`、`FillMode=Forward`(`FluentTheme/Styles.axaml:355-367`)。
|
||
|
||
**Classic 主题的等价实现**(必须两套都做,用 CSS 变量或 `data-theme` 切换;`XamlThemes/ClassicTheme/Styles.axaml`):
|
||
|
||
- 遮罩与强调层用 `TranslateTransform Y=-50` 起手;`mask-in`:`duration 200ms`、`QuadraticEaseIn`、`Y: -50 → 0`(`:265-281`)。
|
||
- `mask-out`:`delay 100ms`、`duration 200ms`、`QuadraticEaseOut`、`Y: 0 → 50`(`:305-318`)。
|
||
- 遮罩内容淡入:`delay 200ms`、`duration 100ms`、`opacity 0→1`(`:288-300`);淡出 `100ms`、`opacity 1→0`(`:319-331`)。
|
||
- 首次出现时给 `Grid#GridRoot` 挂 `DropShadowEffect(OffsetX=0, OffsetY=0, Opacity=1, BlurRadius=8)`,`Effect` 过渡 `100ms` 线性(`:34-45, 283-287`)。
|
||
|
||
#### 4.7.3 浮层(Overlay)
|
||
|
||
- `Grid#GridOverlay` 初始 `Opacity=0`,带 `DoubleTransition Opacity 200ms SineEaseIn`(`FluentTheme/Styles.axaml:419-432`)。
|
||
- `:overlay-in` → 浮层 `opacity 1`、内容层 `opacity 0`;`:overlay-out` → 浮层 `opacity 0`、内容层 `opacity 1`(`:434-451`)。
|
||
- 浮层内容 `MaxHeight = 40`(`FluentTheme/Styles.axaml:149`);浮层里的进度条只显示"当前提醒剩余时间"。
|
||
|
||
### 4.8 编辑模式
|
||
|
||
#### 4.8.1 进入与退出
|
||
|
||
- 入口:原生菜单"编辑主界面…"(`MainWindow.axaml:108-109`)、URI `classisland://app/edit`(`MainWindow.axaml.cs:295`)。
|
||
- 进入前必须通过权限校验(`ManagementService.AuthorizeByLevel(EditSettingsAuthorizeLevel)`,`MainWindow.axaml.cs:1357-1362`);被策略禁用时直接返回。
|
||
- 进入步骤(按序):`IsEditMode = true` → 窗口 `Activate()` → 挂载编辑视图 → `ZoomBorder.ResetMatrix()` 在退出时调用 → `TopmostEffectWindow.ViewModel.IsEditMode` 同步 → 更新窗口层级 → 重新加载主题(`MainWindow.axaml.cs:510-528`)。
|
||
- 退出步骤:关闭两级抽屉 → `IsEditMode=false` → 清空教程提示 → **保存组件配置**(`ComponentsService.SaveConfig()`,`MainWindow.axaml.cs:1391-1403`)。保存必须幂等且原子(写临时文件再替换)。
|
||
- 编辑模式的窗口态:ClassIsland 用 `Background = SmokeFillColorDefaultBrush` + `TransparencyLevelHint = "AcrylicBlur, Transparent"` + 标题 `编辑主界面 | ClassIsland`(`MainWindow.axaml:177-181`)。**本实现的裁决**:
|
||
- **禁止**调用 Electron 的 `setBackgroundMaterial()` / `vibrancy` / `backgroundMaterial` 等任何系统材质 API(原因见 4.1 第 2 条与 `repos/ClassHelper-master/packages/desktop-client/src/main/island.ts:523-524`)。
|
||
- 替代方案:一层全屏半透明深色遮罩 div,`background: rgba(0, 0, 0, 0.30)`(token `--ci-smoke`;深色主题可下调至 `0.24`、浅色主题可上调至 `0.36`,但**默认必须是 `0.30`**),窗口保持 `transparent: true` 逐像素透明。
|
||
- **不得使用 `backdrop-filter: blur()`**:透明窗口下 Chromium 的背板采样不可靠,会出现整块灰底/边缘发白。
|
||
- 【推断 / 差异说明】该方案与 FluentAvalonia 的 Acrylic 是**近似而非等价**:Acrylic 带轻微模糊与噪声,本实现只有纯色压暗。这是**有意接受的视觉差异**,不得在实现说明里写成"与 Acrylic 完全一致";验收只要求"编辑态背景明显压暗、岛与抽屉可读、退出后恢复逐像素透明"。
|
||
- 编辑模式下窗口必须接收鼠标:`win.setIgnoreMouseEvents(false)`;同时 `Transparent` 窗口特性关闭(`MainWindow.axaml.cs:839`)。
|
||
- 平台兜底:Windows 10 1803 及更早版本不支持 windowed 透明,需给窗口加标记类 `no-windowed-transparent`,此时 windowed+edit 模式用不透明底色 `SolidBackgroundFillColorBaseBrush`、`TransparencyLevelHint=None`(`MainWindow.axaml:187-190`, `MainWindow.axaml.cs:257-260`)。
|
||
|
||
#### 4.8.2 编辑画布
|
||
|
||
- 画布 = 全屏(宽 = 屏宽,高 = 屏高 −1px),岛内容在画布内**四边各留 60px 安全边距**,再按 `(ox, oy)` 偏移(见 4.3.3 编辑模式窗口公式)。
|
||
- 行容器对齐:`dock-top` → `align-items:flex-start`,`dock-bottom` → `align-items:flex-end`,编辑模式 → `justify-content:center`(对应 `StackPanelRootContainer` 的 `VerticalAlignment` 覆写,`MainWindow.axaml:240-248`)。编辑模式下内容整体垂直居中,方便拖拽。
|
||
- 底部操作栏占用画布底部 64px 区域,安全边距必须保证岛内容不被操作栏遮挡(岛最高可到画布高 −120px)。
|
||
- 空白处左键点击:清除当前选中的组件并关闭组件设置抽屉(`MainWindow.axaml.cs:1511-1535`,只有点在 `ZoomBorder` 自身(非命中子控件)时触发)。
|
||
|
||
#### 4.8.3 底部操作栏(OperateArea)
|
||
|
||
精确规格(`MainWindow.axaml:595-634`):
|
||
|
||
```css
|
||
.operate-area {
|
||
position: fixed;
|
||
left: 50%; transform: translateX(-50%);
|
||
bottom: 64px; /* Margin="0 0 0 64" */
|
||
background: var(--commandbar-overflow-bg); /* CommandBarOverflowPresenterBackground */
|
||
border: 1px solid var(--commandbar-overflow-border);
|
||
border-radius: 6px;
|
||
padding: 4px;
|
||
display: flex; /* 内部 StackPanel Spacing="6" */
|
||
gap: 6px;
|
||
z-index: 10;
|
||
pointer-events: auto;
|
||
}
|
||
.operate-area .divider { width: 1px; height: 25px; background: var(--divider-stroke); align-self: center; }
|
||
```
|
||
|
||
内部按钮(自左至右,除"完成"外均为 `TransparentButton` 主题):
|
||
|
||
1. "重置视图"——图标 `\uE12F`,命令 = 缩放重置。
|
||
2. 竖分隔线:`StartPoint 0,0 → EndPoint 0,25`、`StrokeThickness=1`、颜色 `DividerStrokeColorDefaultBrush`。
|
||
3. "外观"——图标 `\uE51E`,打开外观抽屉;`DisableSettingsEditing` 策略下禁用。
|
||
4. "添加组件"——图标 `\uE071`,打开组件库抽屉;同策略禁用。
|
||
5. 组件配置按钮——图标 `\uE699`,文本 = 当前配置名(`Settings.CurrentComponentConfig`,默认 `"Default"`,`Models/Settings.cs:159`),`MaxWidth=150`;打开配置管理抽屉。
|
||
6. "完成"——图标 `\uE424`,`Classes="accent"`(强调按钮),退出编辑模式。
|
||
|
||
#### 4.8.4 抽屉(VerticalDrawer)
|
||
|
||
- 主抽屉:`RequestedWidth = 960`(`SettingsContainerWidth`,`ThemeBase.axaml:37`;`EditModeView.axaml:600-605`),`AllowCollapse=True`,`SmokeBackground=False`。
|
||
- 次抽屉:`SmokeBackground=True`、`AllowCollapse=False`、`ContentHeightRatio=0.5`(`EditModeView.axaml:606-611`)。
|
||
- 外壳规格(`repos/ClassIsland/ClassIsland.Core/Controls/VerticalDrawer.axaml:8-155`):
|
||
|
||
```css
|
||
.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`:
|
||
|
||
```css
|
||
.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:
|
||
|
||
```css
|
||
.comps-hidden { /* 仅编辑模式 */
|
||
-webkit-mask-image: repeating-linear-gradient(45deg, #afffffff 0 4px, #50ffffff 4px 8px);
|
||
mask-image: repeating-linear-gradient(45deg, #afffffff 0 4px, #50ffffff 4px 8px);
|
||
}
|
||
```
|
||
|
||
- 容器组件未选中时显示虚线边框(`SurfaceStrokeColorDefaultBrush`、`1.5px`、`dash 1/1.5`、`opacity 0.75`、`margin:0 2px`);选中时改为强调色虚线边框(`AccentFillColorDefaultBrush`,`EditableComponentsListBox.axaml:217-252`)。
|
||
- 选中组件的操作条(Adorner):出现在组件正下方,`Background=CommandBarOverflowPresenterBackground`、`CornerRadius=6`、`Padding=4`、`BorderBrush=CommandBarOverflowPresenterBorderBrush`、`BorderThickness=1`、`Spacing 6`(`EditableComponentsListBox.axaml:100-184`)。按钮:组件设置 `\uEF27`、"查看子组件" `\uEC30`(仅容器组件)、删除 `\uE61D`(`IndianRed`)、`25px` 竖分隔线、"更多选项…" `\uEBAC`(菜单:包裹到新容器 / 创建副本)。
|
||
- 每个组件的"添加组件"按钮:`height:32px`、图标 `\uE00D`、`font-size:20px`,位于组件列表末尾,只在编辑模式可见(`EditableComponentsListBox.axaml:55-60`)。
|
||
- 拖拽插入指示:拖拽中的列表在 `AdornerLayer` 上画一个强调色 1px 边框 + BoxShadow,并做 `1s` 交替无限动画(`0%: opacity .1` → `100%: opacity .6`,`SineEaseInOut`,`PlaybackDirection=Alternate`,`EditableComponentsListBox.axaml:307-336`)。
|
||
- 拖拽开始阈值:水平/垂直各 `3px`,拖拽预览 `opacity 0.5`(`EditableComponentsListBox.axaml:286-288`)。
|
||
- 拖拽预览浮标:`Background=AcrylicBackgroundFillColorDefaultBrush`、`CornerRadius=6`、`Padding=4`、`BorderBrush=SurfaceStrokeColorDefaultBrush`、`BorderThickness=1`、图标 16 + 名称、`Spacing 4`(`EditableComponentsListBox.axaml:22-39`)。该资源名虽含 Acrylic,但**实现必须用不透明降级色 `--ci-bg-tertiary`**,不得调用系统材质 API。
|
||
|
||
### 4.9 隐藏与显示规则(完整判定)
|
||
|
||
`hideMode`(`Settings.HideMode`,默认 `0`,`Models/Settings.cs:185`):
|
||
|
||
- `0 = 基本`:由三个独立开关控制,全部在 `GridRoot` 上以伪类组合判断(`MainWindow.axaml:150-160`):
|
||
- `HideOnClass=true` 且当前时间状态为"上课" → 隐藏(默认 `false`,`Models/Settings.cs:45`);
|
||
- `HideOnMaxWindow=true` 且前台窗口最大化 → 隐藏(默认 `false`,`Models/Settings.cs:63`);
|
||
- `HideOnFullscreen=true` 且前台窗口全屏 → 隐藏(默认 `false`,`Models/Settings.cs:54`)。
|
||
- `1 = 高级`:用 `Settings.HideRules`(规则集,JSON 字段名 `HiedRules`,`Models/Settings.cs:462-472`)求值,满足则隐藏(`MainWindow.axaml:161-163`、`MainWindow.axaml.cs:444-449`)。
|
||
|
||
检测数据源与频率:
|
||
|
||
- 全屏/最大化状态在每个主计时器 tick 检测:`PlatformServices.WindowPlatformService.IsForegroundWindowFullscreen(screen)` / `IsForegroundWindowMaximized(screen)`(`MainWindow.axaml.cs:451-456`)。
|
||
- 排除进程名单 `ExcludedFullscreenWindow`,默认 `["explorer"]`(`Models/Settings.cs:56-59`):这些进程处于前台时不算全屏/最大化,不触发隐藏。
|
||
- 判断基准屏幕必须是"本窗口所在屏幕"(`GetSelectedScreenSafe()`)。
|
||
|
||
强制显示(覆盖隐藏):
|
||
|
||
- `ExplicitShowed` = 正在播放提醒或处于编辑模式(`MainWindow.axaml:271`、`MainWindow.axaml.cs:1056-1092` 的 `Acquire/ReleaseTopmostLock`)。此时即使命中隐藏条件也必须显示,且窗口临时置顶。
|
||
- `IsMainWindowVisible`(用户手动"隐藏主界面",默认 `true`,`Models/Settings.cs:67`):`GridRoot` 的可见性 = `IsMainWindowVisible || IsEditMode`(`MainWindow.axaml:135-141`)。手动隐藏时同时不接受新提醒(行级 `AcceptsNotificationRequests` 要求 `IsNotificationEnabled && !IsAllComponentsHid && !_isOverlayOpen && !_isUnloading`,`MainWindowLine.cs:814`)。
|
||
- 所有行都隐藏时(`IsAllComponentsHid` 对所有行成立):**可见岛内容 `opacity:0`,窗口包围盒保持不变**(不改尺寸、不隐藏窗口),但不得销毁窗口(提醒到来时要能立刻显示)。
|
||
|
||
### 4.10 右键菜单(原生菜单)与托盘
|
||
|
||
菜单必须是系统原生菜单(Electron `Menu.buildFromTemplate` + `tray.setContextMenu`),不得用 HTML 菜单。逐项规格(`MainWindow.axaml:51-132`):
|
||
|
||
| # | 文本 | 图标 glyph | 可见条件 | 操作 |
|
||
|---|---|---|---|---|
|
||
| 1 | `ClassIsland` | AppLogo | 总是 | 打开"关于"设置页 |
|
||
| 2 | `教学…` | `\uE959` | `IsWelcomeWindowShowed` | 打开教程中心 |
|
||
| 3 | `帮助文档…` | `\uE23C` | `IsWelcomeWindowShowed` | 打开 `https://docs.classisland.tech/app/` |
|
||
| 4 | 分隔符 | | | |
|
||
| 5 | `显示主界面` | `\uE813` | `!IsMainWindowVisible && IsWelcomeWindowShowed` | 切 `IsMainWindowVisible` |
|
||
| 6 | `隐藏主界面` | `\uE817` | `IsMainWindowVisible && IsWelcomeWindowShowed` | 切 `IsMainWindowVisible` |
|
||
| 7 | `清除全部提醒` | `\uE029` | `IsWelcomeWindowShowed && IsNotificationsPlaying` | 取消全部提醒 |
|
||
| 8 | `更多选项…` | `\uEBAC` | 存在插件注入项 | 子菜单 |
|
||
| 9 | 分隔符 | | | |
|
||
| 10 | `编辑档案…` | `\uE699` | 总是 | 打开档案编辑窗口 |
|
||
| 11 | `加载临时课表…` | `\uE6A1` | 总是 | 打开档案窗口并展开"临时课表"抽屉 |
|
||
| 12 | `换课…` | `\uE15F` | 总是 | 打开换课窗口(需权限) |
|
||
| 13 | 分隔符 | | | |
|
||
| 14 | `编辑主界面…` | `\uE7C9` | 总是 | 进入编辑模式 |
|
||
| 15 | `应用设置…` | `\uEF27` | 总是 | 打开设置窗口 |
|
||
| 16 | 分隔符 | | | |
|
||
| 17 | `重启` | `\uE0BD` | 总是 | 重启应用 |
|
||
| 18 | `退出` | `\uE0DF` | 总是 | 退出(需权限) |
|
||
| 19 | 分隔符 | | `IsDebugEnabled` | |
|
||
| 20 | `dev__Debug` | `\uE2C8` | `IsDebugEnabled` | 子菜单:DevTools / DevPortal / 截图工具 / 教程编辑器 / 启用课表 / 显示崩溃窗口 / WelcomeWindow / WelcomeWindow(full) |
|
||
|
||
托盘左键点击行为 `TaskBarIconClickBehavior`:`0=打开主菜单`(仅 Windows 有效,其他平台自动改为 4)、`1=打开档案编辑窗口`、`2=显示/隐藏主界面`、`3=打开换课窗口`、`4=打开设置窗口`;默认 Windows 平台 `0`、其他平台 `4`(`Models/Settings.cs:102`, `:540-561`,实现 `MainWindow.axaml.cs:617-662`)。
|
||
|
||
### 4.11 本部分动画总表(照此实现,不得自创)
|
||
|
||
| 动画 | 触发 | 属性 | 时长 | 缓动 |
|
||
|---|---|---|---|---|
|
||
| 行背景色过渡 | 主题/背景色变化 | `Background` | 200ms | 线性(`MainWindow.axaml:40-44` BrushTransition) |
|
||
| 行内容透明度 | 行显示/隐藏 | `opacity` | 100ms | 线性(`FluentTheme/Styles.axaml:268`) |
|
||
| 行背景宽度 | 内容宽变化 | `width` | 300ms(Fluent)/ 550ms(Classic) | `cubic-bezier(0.65,0,0.35,1.0)` / `cubic-bezier(0,1,0.5,1.1)` |
|
||
| 行挂载淡入 | 首次可见 | `opacity 0→1` | 250ms | `cubic-bezier(0.25,1,0.5,1)`;`AnimationLevel≥2` 才播 |
|
||
| 内容层交错重排 | 组件变化 | 位置/宽 | 300ms | `cubic-bezier(0.65,0,0.35,1.0)`(`MainWindow.axaml:197-198`) |
|
||
| 遮罩斜掠(开) | 提醒开始 | 5 区段 scaleX 0→1 | 每段 360ms,错峰 0/90/90/120/120ms | `cubic-bezier(0.165,0.84,0.44,1)`(QuarticEaseOut) |
|
||
| 遮罩斜掠(关) | 提醒结束 | 5 区段 scaleX 1→0 | 每段 360ms,反向错峰 | `cubic-bezier(0.55,0.085,0.68,0.53)`(QuadraticEaseIn) |
|
||
| 强调色底淡入 | 提醒开始 | `opacity 0→1` | 100ms,delay 600ms | 线性 |
|
||
| 遮罩文字淡入 | 提醒开始 | `opacity 0→1` | 250ms,delay 260ms | `cubic-bezier(0.25,1,0.5,1)` |
|
||
| 遮罩文字缩放 | 提醒开始 | `scale 1.1→1.0` | 750ms,delay 260ms | `cubic-bezier(0.25,1,0.5,1)` |
|
||
| 遮罩文字淡出 | 提醒结束 | `opacity 1→0` | 200ms | 线性 |
|
||
| 遮罩文字缩小 | 提醒结束 | `scale 1.0→1.1` | 200ms | `cubic-bezier(0.5,0,0.75,0)` |
|
||
| 浮层淡入淡出 | Overlay 开始/结束 | `opacity` | 200ms | SineEaseIn |
|
||
| 提醒进度条值 | 剩余时间更新 | `value` | 50ms | 线性 |
|
||
| 行变淡 | 指针离开 | `opacity →0.05` | 100ms | 线性 |
|
||
| 编辑抽屉升降 | 打开/关闭抽屉 | `bottom`/`height` | 250ms | `cubic-bezier(0,0,0,1)` |
|
||
| 遮罩层(smoke) | 抽屉打开 | `opacity` | 250ms | `cubic-bezier(0,0,0,1)` |
|
||
| 拖拽插入高亮 | 拖动中 | `opacity 0.1↔0.6` | 1000ms 交替无限 | SineEaseInOut |
|
||
| 斜条纹隐藏提示 | 隐藏组件 | — | 静态 | — |
|
||
|
||
### 4.12 验收标准
|
||
|
||
实现者必须逐条自查,全部通过才算完成本节:
|
||
|
||
1. 非编辑模式下用 Spy++/AutoHotkey 之类工具点击岛的任何位置,点击都穿透到桌面应用;编辑模式下岛内的按钮、拖拽把手、抽屉都能被点到。
|
||
2. 主界面窗口在 4K(150% 缩放)与 1080p(100% 缩放)两种 DPI 下都不出现 1px 白边、黑边、圆角锯齿;圆角在 `RadiusX=0` 时是直角,在 `20` 时左右边缘为半圆。
|
||
3. 6 个停靠位置逐个验证:`0/1/2` 贴工作区上边、`3/4/5` 贴下边;左/中/右的横向对齐分别贴左、居中、贴右,且窗口本身始终横跨整屏。
|
||
4. 修改"窗口向右/向下偏移"后岛整体平移,正值向下时上停靠要把偏移吞进安全区(不得被屏幕边缘裁掉)。
|
||
5. 行数从 1 增到 5、再减到 1:**可见岛高度严格等于"行高之和 + 行间距之和"**(误差 ≤1px),岛在窗口包围盒内平滑形变,**不出现窗口级闪烁**;包围盒只在行数超出 `contentMaxHeight` 时变更一次,且变更被节流 ≥100ms。
|
||
5.1 用 DevTools 的 Performance 面板录 10 秒:`setBounds` 调用次数为 0(无设置/行数变化时);置顶复核轮询间隔 ≥1000ms,无 1ms/10ms 级高频定时器。
|
||
6. 单行内容宽度变化(如时钟秒数出现/消失、课程名变长)时,背景宽度在 300ms 内以 `cubic-bezier(0.65,0,0.35,1.0)` 平滑跟随,文字不重排抖动,且**不触发窗口 `setBounds`**。
|
||
7. 行间距设为 `0`、`5`、`150` 时,相邻行间距精确等于设置值(用像素标尺截图量)。
|
||
8. 关闭某行"可以显示提醒"后,该行不再出现遮罩与进度条(走 Min 模板),其余行不受影响;多行同时具备提醒能力时,**具体由哪一行显示由第 7 章的分发规则决定**,本节只验证宿主与 `IsNotificationEnabled` 标记生效。
|
||
8.1 三条进度条(行提醒、课程、倒计时)高度均为 `3px`,圆角 `1.5px`,贴底且不越出行圆角裁切。
|
||
9. 删除到只剩 1 行时再点删除,出现"至少需要保留 1 个主界面行。"提示且行数不变。
|
||
10. 勾选 A 行为主要行后 B 行自动取消;取消 A 行后第一行自动成为主要行并出现提示。
|
||
11. 触发一次提醒:遮罩从左、右两端同时向中间斜掠,总时长约 480ms,文字在 260ms 后淡入并伴随 1.1→1.0 缩放;遮罩期间行内容消失,结束后恢复。
|
||
12. 阈值条件全开(上课隐藏、最大化隐藏、全屏隐藏),在前台最大化窗口时岛消失、切回桌面 1 个计时器周期内恢复;`ExcludedFullscreenWindow` 含 `explorer` 时桌面(资源管理器)前台不隐藏。
|
||
13. 鼠标移出岛 0.5 秒内行的整体不透明度降到 0.05,移回立即恢复;勾选"反向"后行为相反。
|
||
14. 编辑模式:进入后窗口扩展为"屏宽 ×(屏高−1px)",岛内容四边留 60px 安全边距并垂直居中,任务栏出现图标,鼠标可交互;退出后恢复包围盒尺寸与穿透状态,并写盘保存组件配置;编辑态背景为 `rgba(0,0,0,0.30)` 遮罩(无 `backdrop-filter`、无系统材质),退出后恢复逐像素透明。
|
||
15. 编辑模式下拖拽组件到另一行的插入位置时,出现强调色发光插入线(1s 交替闪烁),松手后组件落到目标位置且行宽平滑重排。
|
||
16. 容器组件"查看子组件"浮动框出现在被点组件下方 48px 处,可拖动、可关闭,关闭后 `ContainerComponents` 中对应项被移除。
|
||
17. 主界面完全不可见(手动隐藏 / 全部隐藏规则命中)时收到提醒,岛必须临时显示、置顶,提醒结束后回到隐藏状态。
|
||
18. 托盘图标左键行为的 5 种取值(`0–4`)逐个验证正确,其中非 Windows 平台 `0` 自动降级为 `4`。
|
||
|
||
---
|
||
|
||
## 5. 组件系统:全部内置组件逐个规格
|
||
|
||
本节规定主界面上所有组件的实现规格。组件是"岛"的内容单元:每个组件的实例由一份 JSON(`ComponentSettings`)描述,含「组件 ID + 组件自己的设置对象 + 一系列外观/布局/隐藏覆盖项」。实现必须做成可插拔的注册表(原生 11 个组件 + 后续插件组件走同一接口),不得把组件硬编码进主界面。
|
||
|
||
### 5.1 组件系统总则
|
||
|
||
#### 5.1.1 组件注册表(唯一事实来源)
|
||
|
||
必须内置下面 11 个组件,ID、名称、图标 glyph、描述、是否容器、设置类型逐字照抄(`[ComponentInfo]` 特性):
|
||
|
||
| 组件 | ID(GUID) | 名称 | 图标 glyph | 描述 | 容器 | 设置类型 | 证据 |
|
||
|---|---|---|---|---|---|---|---|
|
||
| 日期 | `DF3F8295-21F6-482E-BADA-FA0E5F14BB66` | 日期 | `\ue304` | 显示今天的日期和星期。 | 否 | 无 | `Controls/Components/DateComponent.axaml.cs:14` |
|
||
| 时钟 | `9E1AF71D-8F77-4B21-A342-448787104DD9` | 时钟 | `\ue31c` | 显示现在的时间,支持精确到秒。 | 否 | `ClockComponentSettings` | `ClockComponent.axaml.cs:17` |
|
||
| 倒计时 | `7C645D35-8151-48BA-B4AC-15017460D994` | 倒计时 | `\uf361` | 显示距离某一天的倒计时。 | 否 | `CountDownComponentSettings` | `CountDownComponent.axaml.cs:29` |
|
||
| 课程表 | `1DB2017D-E374-4BC6-9D57-0B4ADF03A6B8` | 课程表 | `\ue751` | 显示当前的课程表信息。 | 否 | `LessonControlSettings` | `ScheduleComponent.axaml.cs:30` |
|
||
| 天气简报 | `CA495086-E297-4BEB-9603-C5C1C1A8551E` | 天气简报 | `\uf465` | 显示当前的天气概况和气象预警。 | 否 | `WeatherComponentSettings` | `WeatherComponent.axaml.cs:16` |
|
||
| 文本 | `EE8F66BD-C423-4E7C-AB46-AA9976B00E08` | 文本 | `\uf323` | 显示自定义文本。 | 否 | `TextComponentSettings` | `TextComponent.axaml.cs:16` |
|
||
| 滚动容器 | `70FCD5EA-3FAE-4E06-ACA2-4F4DF47F9ACD` | 滚动容器 | `\uf279` | 滚动显示组件内的内容。 | 是 | `RollingComponentSettings` | `RollingComponent.axaml.cs:33` |
|
||
| 轮播容器 | `7E19A113-D281-4F33-970A-834A0B78B5AD` | 轮播容器 | `\uefc9` | 轮播多个组件。 | 是 | `SlideComponentSettings` | `SlideComponent.axaml.cs:24` |
|
||
| 分组容器 | `C911D762-107F-40C6-84CC-0146AB3C86B1` | 分组容器 | `\ue92f` | 将多个组件组合到一个组件中显示。 | 是 | `GroupComponentSettings` | `GroupComponent.axaml.cs:11` |
|
||
| 堆叠容器 | `2D849ECE-9F21-4C78-9434-415CFC283294` | 堆叠容器 | `\uf04f` | 将多个容器堆叠显示。 | 是 | `StackComponentSettings` | `StackComponent.axaml.cs:11` |
|
||
| 分割线 | `AB0F26D5-9DF6-4575-B844-73B04D0907C1` | 分割线 | `\uf023` | 显示一个分割线,视觉上对组件进行分组。 | 否 | 无 | `SeparatorComponent.axaml.cs:9` |
|
||
|
||
图标字体为 `FluentSystemIcons-Resizable`(`repos/ClassIsland/ClassIsland.Core/ThemeBase.axaml:46`),`FontFamily` 绑到 `ci|FluentIcon` 全局样式(`StylesBase.axaml:79-81`)。实现时用同名 SVG 图标表 + `mask-image` 或内联 SVG,不得贴无对应关系的图标。
|
||
|
||
`课程表` 组件有一个历史 ID 需在迁移时映射:`E7831603-61A0-4180-B51B-54AD75B1A4D3` → 现 ID(`[MigrateFrom]`,`ScheduleComponent.axaml.cs:29`)。
|
||
|
||
#### 5.1.2 默认组件布局(首次启动的样子)
|
||
|
||
首次启动必须生成 1 个主界面行,行内依次为「日期」「课程表」,其余字段取默认值(`repos/ClassIsland/ClassIsland/Services/ComponentsService.cs:33-52`)。实现即:`lines = [{ children: [dateId, scheduleId], ...默认行设置 }]`。
|
||
|
||
#### 5.1.3 组件宿主(ComponentPresenter):所有组件共用的外壳
|
||
|
||
每个组件都必须包在统一的宿主里,宿主负责下面这些语义(`repos/ClassIsland/ClassIsland.Core/Controls/ComponentPresenter.axaml`):
|
||
|
||
1. **可见性三态**:`visible = IsVisibleInternal || 主界面处于编辑模式`(`:13-18`)。隐藏的组件在编辑模式仍占位。
|
||
2. **透明度**:宿主 `opacity = 组件设置.Opacity`(默认 `1.0`,范围 0–1,`:34-36`)。
|
||
3. **字号继承**:宿主根元素的 `font-size = var(--font-body)`(16px),即所有组件默认正文字号;组件内部需要别的字号时必须显式引用 `--font-secondary/emphasized/large`(`:37-38`)。
|
||
4. **宽度三态**(互斥,优先级:固定宽 > 最大宽/最小宽):
|
||
- `IsFixedWidthEnabled` → `width = FixedWidth`(默认 `200px`,`:45-47`);
|
||
- `IsMaxWidthEnabled` → `max-width = MaxWidth`(默认 `300px`,`:39-41`);
|
||
- `IsMinWidthEnabled` → `min-width = MinWidth`(默认 `100px`,`:42-44`)。
|
||
默认三者全 `false`(`repos/ClassIsland/ClassIsland.Core/Models/Components/ComponentSettings.cs:30-35`)。
|
||
5. **水平对齐**:宿主内容的 `justify-content` 取 `Settings.HorizontalAlignment`,默认 `stretch`(枚举 `Stretch/Left/Center/Right`,`ComponentSettings.cs:36`,选择器 `:49-51`)。
|
||
6. **内边距**:宿主内部 `Shimmer` 容器 `padding: 8px 0`(`:55-57`),但组件自身的横向间距由外层 `margin: 0 6px` 提供(行内模板 `Margin="6 0"`,`MainWindow.axaml:396`),两者都要保留,不得合并。
|
||
7. **加载占位**:首次布局或配置切换时显示 Shimmer 占位,占位宽度取上次测量宽度 `LastWidthCache`(默认 `100`,`ComponentSettings.cs:39`,`ComponentPresenter.axaml:69-70`)。占位态不得让行高变化。
|
||
8. **隐藏规则**:`HideOnRule + HidingRules` 满足时组件不可见但不卸载、不释放资源(`ComponentSettings.cs:100-123`)。
|
||
9. **裁剪**:宿主自身 `overflow: visible`(不对组件内部做裁剪),裁剪交给行的 `clip-path`(见 4.5.4);但轮播容器内部有 40px 高度钳制(见 5.9)。
|
||
|
||
#### 5.1.4 组件通用设置项("高级设置"页,所有组件都有)
|
||
|
||
以下字段存在于每一个 `ComponentSettings` 上,默认值固定(`repos/ClassIsland/ClassIsland.Core/Models/Components/ComponentSettings.cs:23-51`),UI 顺序照 `ComponentAdvancedSettingsControl.axaml`:
|
||
|
||
| 字段 | 默认 | UI 控件 | 范围/步进 |
|
||
|---|---|---|---|
|
||
| `IsResourceOverridingEnabled` | `false` | 开关 + 4 个字号输入 | 字号 1–40 |
|
||
| `MainWindowSecondaryFontSize` | `14` | NumberBox | 1–40 |
|
||
| `MainWindowBodyFontSize` | `16` | NumberBox | 1–40 |
|
||
| `MainWindowEmphasizedFontSize` | `18` | NumberBox | 1–40 |
|
||
| `MainWindowLargeFontSize` | `20` | NumberBox | 1–40 |
|
||
| `IsCustomForegroundColorEnabled` | `false` | 开关 + ColorPicker | — |
|
||
| `ForegroundColor` | `DodgerBlue` | ColorPicker | — |
|
||
| `Opacity` | `1.0` | Slider | 0–1,步进 0.05 |
|
||
| `IsCustomBackgroundColorEnabled` | `false` | 开关 + ColorPicker | — |
|
||
| `BackgroundColor` | `Black` | ColorPicker | — |
|
||
| `IsCustomBackgroundOpacityEnabled` | `false` | 开关 + Slider | 0–1,步进 0.05 |
|
||
| `BackgroundOpacity` | `0.5` | Slider | 0–1 |
|
||
| `IsCustomCornerRadiusEnabled` | `false` | 开关 + Slider | 0–20,步进 0.5 |
|
||
| `CustomCornerRadius` | `8.0` | Slider | 0–20 |
|
||
| `HorizontalAlignment` | `Stretch` | 4 个图标按钮 | 左/居中/拉伸/右 |
|
||
| `IsFixedWidthEnabled` / `FixedWidth` | `false` / `200` | 开关 + NumberBox | — |
|
||
| `IsMinWidthEnabled` / `MinWidth` | `false` / `100` | 开关 + NumberBox | — |
|
||
| `IsMaxWidthEnabled` / `MaxWidth` | `false` / `300` | 开关 + NumberBox | — |
|
||
| `IsCustomMarginEnabled` | `false` | 开关 | — |
|
||
| `MarginLeft` / `MarginTop` / `MarginRight` / `MarginBottom` | `0` / `0` / `0` / `0` | 4 个 NumberBox | — |
|
||
| `HideOnRule` / `HidingRules` | `false` / `{}` | 开关 + 规则集编辑器 | — |
|
||
| `RelativeLineNumber` | `0` | 内部字段 | — |
|
||
|
||
对齐按钮图标(`ComponentAdvancedSettingsControl.axaml:223-232`):左=`\uE03E`、居中=`\uE03A`、拉伸=`\uE034`、右=`\uE03C`。
|
||
|
||
自定义背景/圆角只有在"组件位于独立岛"(分体主界面)语义下才画得出来;普通行内背景由行承担,此时组件的背景设置不生效——实现必须在 UI 上如实表现(禁用或不渲染),不得偷偷画。
|
||
|
||
#### 5.1.5 容器组件通用约定
|
||
|
||
- 注册时带 `[ContainerComponent]`,宿主 `ComponentInfo.IsComponentContainer = true`。
|
||
- 子组件集合 = 组件设置对象实现 `IComponentContainerSettings.Children`(`ComponentSettings.cs:143-144`);因此 `GroupComponentSettings`、`StackComponentSettings` 只有 `Children` 一个字段(`Models/ComponentSettings/GroupComponentSettings.cs`、`StackComponentSettings.cs`)。
|
||
- 容器内部为子组件再套一层 `ComponentPresenter`,`IsOnMainWindow=true`、`Margin="6 0"`,并透传 `HidingRules/HideOnRule`(`GroupComponent.axaml:25-29`、`StackComponent.axaml:24-28`、`RollingComponent.axaml:36-41`、`SlideComponent.axaml:114-120`)。
|
||
- 容器的子列表外层容器统一 `Margin="-6 0"`(抵消一次边距),循环嵌套时视觉间距保持 12px 恒定(`GroupComponent.axaml:15`、`StackComponent.axaml:14`、`RollingComponent.axaml:16`、`SlideComponent.axaml:22`)。
|
||
- 容器内部的子组件条目使用 "components-panel" 面板类:`WrapPanel`/`StackPanel` 需要"重排动画"标记;`StackPanel` 固定 `Orientation=Horizontal`(`repos/ClassIsland/ClassIsland.Core/Themes/ComponentsPanelStyles.axaml:11-16`)。
|
||
|
||
#### 5.1.6 组件高度与"岛"的约束
|
||
|
||
- 组件可用高度恒为 `40px`(行高,见 4.5.2)。所有组件必须在这个高度内垂直居中显示。
|
||
- 宿主的 `MaxHeight` 受容器约束:轮播容器显式 `MaxHeight = IslandContainerHeight`(`SlideComponent.axaml:119`)、编辑态列表项 `MaxHeight = IslandContainerHeight`(`EditableComponentsListBox.axaml:70`)。
|
||
- 组件内部若出现高度超过 40px 的内容(如两行文字),必须自行 clamp 或按设计折行;不得撑高行。
|
||
|
||
#### 5.1.7 时间驱动
|
||
|
||
除文本/分割线外,所有组件在主计时器 tick 时刷新(ClassIsland 用 `ILessonsService.PostMainTimerTicked`,`DateComponent.axaml.cs:39-40`、`ClockComponent.axaml.cs:61`、`CountDownComponent.axaml.cs:142`、`ScheduleComponent.axaml.cs:119`)。实现:
|
||
|
||
- 用单一 1 秒节拍(主进程 `setInterval(1000)` 对齐到整秒边界)+ 事件广播;
|
||
- 需要亚秒/更快的(滚动、轮播、提醒进度)各自用 `requestAnimationFrame` 或独立定时器,不得把 1 秒节拍改成 16ms 全局广播;
|
||
- 页面不可见或被系统节流时,靠"重新挂载时立即刷新一次"兜底(对应 `AttachedToVisualTree` 时 `UpdateContent()`)。
|
||
|
||
### 5.2 日期(DateComponent)
|
||
|
||
- 注册:见 5.1.1。
|
||
- 视觉:一个 `TextBlock`,垂直居中;文本格式 `ddd MM/dd`(`DateComponent.axaml:11`,`StringFormat=\{0:ddd MM/dd\}`)。
|
||
- 中文(`zh-CN`)渲染示例:`周三 11/12`;星期用短名(不补"星期"二字),月/日补零两位,斜杠分隔,无空格之外的分隔符(`ddd` 与 `MM/dd` 之间有一个空格)。
|
||
- 字号:继承 `--font-body` = 16px;颜色:继承前景色;字重:继承 `--font-weight`(默认 500)。
|
||
- 数据:`Today = ExactTimeService.GetCurrentLocalDateTime()`,每次 tick 刷新(`DateComponent.axaml.cs:43-46`)。必须使用"精确时间服务"(含 NTP 校正与时间偏移),不得直接用 `new Date()`。
|
||
- 设置:无。
|
||
- 边界:
|
||
- 跨日 00:00 必须自动更新(不能只在启动时取一次)。
|
||
- 系统时间被手动修改后 1 个 tick 内反映。
|
||
- 语言/区域变化时按 `Intl.DateTimeFormat('zh-CN', { weekday:'short' })` 重新渲染,且宽度变化要触发行宽动画(见 4.5.7)。
|
||
- Vue 结构建议:`<span class="node node-date">{{ dateText }}</span>`,`white-space: nowrap; font-variant-numeric: tabular-nums;`。
|
||
|
||
### 5.3 时钟(ClockComponent)
|
||
|
||
- 视觉结构(`ClockComponent.axaml:13-41`):外层网格水平/垂直居中,`font-size = var(--font-emphasized)`(18px)。
|
||
|
||
```html
|
||
<div class="node node-clock" data-sep="1" data-seconds="0">
|
||
<span class="clock-full">09:41</span> <!-- TextBlock1,分隔符闪动用 -->
|
||
<span class="clock-hour">09</span> <!-- 左对齐 -->
|
||
<span class="clock-minute">41</span> <!-- 右对齐 -->
|
||
</div>
|
||
```
|
||
|
||
- 三种显示态由两个类控制(`:19-32`):
|
||
- `TimeSeparatorShowing`:`clock-full` 可见(`opacity:1`),`clock-hour/clock-minute` 隐藏;
|
||
- 非 `TimeSeparatorShowing`:`clock-full` 隐藏(`opacity:0`),`clock-hour` 左对齐 + `clock-minute` 右对齐显示,二者之间留出分隔符的宽度(用等宽字符宽的占位,或给两个 span 之间固定 `gap: <一个字符宽>`;Avalonia 里两个 TextBlock 在同一 Grid 单元格内用 Left/Right 对齐实现,中间自然留空)。
|
||
- `ShowSeconds`:文本格式变为 `HH:mm:ss`(`:28-30`)。
|
||
- 分隔符闪动逻辑(`ClockComponent.axaml.cs:71-76`):
|
||
|
||
```
|
||
IsTimeSeparatorShowing = !FlashTimeSeparator || ShowSeconds || (当前秒 % 2 == 1)
|
||
```
|
||
|
||
即:开启闪动且不显示秒时,冒号在奇数秒显示、偶数秒消失(每次消失 1 秒);显示秒时冒号常亮。
|
||
|
||
- 数字排版:必须 `font-variant-numeric: tabular-nums`(等宽数字),否则冒号闪动与秒数变化会让整块宽度跳动(ClassHelper 对时钟也做了同样处理:`repos/ClassHelper-master/packages/desktop-client/src/island/ComponentNode.vue:305-308`)。
|
||
- 时间源:`CurrentTime = Settings.ShowRealTime ? DateTime.Now : ExactTimeService.GetCurrentLocalDateTime()`(`ClockComponent.axaml.cs:73`)。
|
||
- 设置项与默认值(`Models/ComponentSettings/ClockComponentSettings.cs`):
|
||
|
||
| 字段 | 默认 | UI 文案 | 说明 |
|
||
|---|---|---|---|
|
||
| `ShowRealTime` | `false` | 使用实际时间 | 显示不经过时间偏移的系统时间 |
|
||
| `ShowSeconds` | `false` | 显示秒数 | 精确到秒 |
|
||
| `FlashTimeSeparator` | `true` | 闪动时间分隔符 | 模拟电子时钟分隔符闪动 |
|
||
|
||
- 边界:
|
||
- 24 小时制,不显示 AM/PM(格式串 `HH:mm`)。
|
||
- 显示秒时每秒重绘一次;不显示秒时也需每秒 tick 以驱动闪动。
|
||
- 若系统时间不可用(精确时间服务失败)必须回退本地时间,不得显示空白。
|
||
- 验收要点:开启闪动后,肉眼观察冒号 1 秒亮 1 秒灭;开启秒数后冒号常亮且每秒变化;整块数字在切换时不发生水平位移。
|
||
|
||
### 5.4 倒计时(CountDownComponent)
|
||
|
||
最复杂的非容器组件。结构(`CountDownComponent.axaml:64-114`):
|
||
|
||
```html
|
||
<div class="node node-countdown" data-progress="ring|bar" data-compact="0">
|
||
<!-- 进度环(ProgressBarMode=0 且 ShowProgress 时可见) -->
|
||
<div class="ring">
|
||
<svg width="22" height="22" viewBox="0 0 22 22">
|
||
<circle cx="11" cy="11" r="9.5" stroke="var(--control-strong-stroke-disabled)" stroke-width="2.6" fill="none"/>
|
||
<circle cx="11" cy="11" r="9.5" stroke="var(--accent)" stroke-width="3.1" stroke-linecap="round"
|
||
stroke-dasharray="59.69" stroke-dashoffset="计算值" transform="rotate(-90 11 11)"/>
|
||
</svg>
|
||
</div>
|
||
<span class="connector-prefix">距离</span> <!-- 非紧凑模式 -->
|
||
<span class="name">倒计时</span> <!-- 颜色=FontColor,字号=FontSize -->
|
||
<span class="connector">还有</span> <!-- 非紧凑模式 -->
|
||
<span class="value">30</span> <!-- 颜色=FontColor,字号=FontSize -->
|
||
<!-- 进度条(ProgressBarMode=1 且 ShowProgress 时可见) -->
|
||
<div class="bar-wrap"><div class="bar" style="width: Percent%"></div></div>
|
||
</div>
|
||
```
|
||
|
||
尺寸与样式(逐条,`CountDownComponent.axaml:66-112`):
|
||
|
||
- 外层:横排、`gap: 2px`、垂直居中。
|
||
- 进度环容器:`margin-right: 4px`、垂直居中;底环 `Ellipse` 宽高 `21.4px`、描边 `2.6px`、颜色 `ControlStrongStrokeColorDisabledBrush`;前景弧 `Path` 宽高 `22px`、`stroke-linecap: round`、`stroke-width: 3.1`、颜色默认 `AccentFillColorDefaultBrush`,开启"进度条使用强调色"后改为 `Settings.FontColor`。
|
||
- 弧半径计算必须照抄:控件尺寸 22×22、描边 3、`radius = 22/2 - 3/2 = 9.5`,起点在正上方(`center.Y - radius`),顺时针扫过 `percent/100*360` 度,`percent >= 100` 时按 `99.9999` 处理以避免闭合渲染问题,`percent <= 0` 时不画(`CountDownComponent.axaml.cs:32-85`)。
|
||
- 文本:"距离" + 名称 + 连接词 + 数值;"距离"与连接词用 `connector l-instructive` 类(次要色);名称与数值用 `Settings.FontColor` + `Settings.FontSize`(默认 `#FFFF0000`、16px)。
|
||
- 紧凑模式(`IsCompactModeEnabled`):隐藏"距离"与连接词,只留名称 + 数值(`:85, :95`)。
|
||
- 底部进度条:`Maximum=100`、`Minimum=0`、`Value=Percent`、贴底、`min-width:0`、宽度撑满行、**高度固定 `3px`(token `--ci-progress-height: 3px`,`border-radius: 1.5px`)**;实现用 `width: Percent%` 或 `scaleX`(`:102-112`)。
|
||
- 设置项与默认值(`Models/ComponentSettings/CountDownComponentSettings.cs`,逐字段):
|
||
|
||
| 字段 | 默认 | 说明 |
|
||
|---|---|---|
|
||
| `CountDownName` | `"倒计时"` | 事件名称 |
|
||
| `CountdownSource` | `0` | `0 固定时间 / 1 周期 / 2 今天 / 3 本周` |
|
||
| `StartTime` | 今天 0 点 | 固定时间模式的起点 |
|
||
| `OverTime` | 今天 0 点 | 固定时间模式的终点(日为 0 的那天) |
|
||
| `CycleStartTime` | 当前时刻 | 周期模式起点 |
|
||
| `CycleBeforeDuration` | `TimeSpan.Zero` | 周期前阶段 |
|
||
| `CycleDuration` | `1 天` | 周期长度 |
|
||
| `CycleAfterDuration` | `TimeSpan.Zero` | 周期后阶段 |
|
||
| `IsCycleCountLimited` | `false` | 限制循环次数 |
|
||
| `CycleCountLimit` | `2` | 循环次数上限(UI 范围 1–33550336) |
|
||
| `IsAdvancedCycleTimingEnabled` | `false` | 启用前后阶段 |
|
||
| `NatureTimeUseMode` | `0` | `0 默认 / 1 强制自然时间 / 2 强制课程时间` |
|
||
| `WeekCountdownStartDay` | `Monday` | 本周模式的第一天 |
|
||
| `IsCustomWeekCountdownStartDayEnabled` | `false` | 自定义每周第一天(否则取学期开始日的星期) |
|
||
| `FontColor` | `#FFFF0000` | 名称与数值颜色(UI 文案"强调色") |
|
||
| `FontSize` | `16` | 范围 16–30,步进 1 |
|
||
| `IsCompactModeEnabled` | `false` | 紧凑模式 |
|
||
| `CountDownConnector` | `"还有"` | 连接词 |
|
||
| `IsConnectorColorEmphasized` | `false` | 连接词用强调色 |
|
||
| `ShowProgress` | `false` | 显示进度 |
|
||
| `ProgressBarMode` | `0` | `0 进度环 / 1 进度条` |
|
||
| `UseAccentOnProgressBar` | `true` | 进度使用强调色(否则用应用主题色) |
|
||
| `IsProgressInverted` | `false` | 进度反相(用"剩余"而非"已过"计算) |
|
||
| `CustomStringFormat` | `"%D天"` | 数值格式化模板 |
|
||
|
||
- 格式化模板替换规则(必须逐 token 实现,`CountDownComponent.axaml.cs:268-287`):
|
||
|
||
| Token | 含义 | 取整 |
|
||
|---|---|---|
|
||
| `%D` | 总天数 | 向上取整 |
|
||
| `%H` | 总小时数 | 向上取整 |
|
||
| `%M` | 总分钟数 | 向上取整 |
|
||
| `%S` | 总秒数 | 向上取整 |
|
||
| `%X` | 总毫秒数 | 向上取整 |
|
||
| `%P` | 已过百分比(`P0`,如 `30%`) | 四舍五入到 2 位小数后按 P0 |
|
||
| `%p` | 已过百分比(`P2`,如 `30.00%`) | 保留 2 位 |
|
||
| `%L` | 剩余百分比(`P0`) | 四舍五入到 2 位 |
|
||
| `%d` | 天(元素值) | 不取整 |
|
||
| `%h` | 小时(元素值) | 不取整 |
|
||
| `%m` | 分钟(元素值) | 补两位 |
|
||
| `%s` | 秒(元素值) | 补两位 |
|
||
| `%x` | 毫秒(元素值) | 补三位 |
|
||
|
||
中国区必须用不变文化(`InvariantCulture`)格式化数字,避免千分位。`delta < 0` 时钳制为 0(`:260-264`)。
|
||
|
||
- 四种时间源的时间区间计算(必须照抄,`CountDownComponent.axaml.cs:166-247`):
|
||
1. **固定时间**:`(StartTime, OverTime)`。
|
||
2. **周期**:`total = before + duration + after`;`cycles = floor((now - cycleStart)/total)`(启用次数限制时再取 `min(cycles, limit)`);`start = cycleStart + cycles*total + before`;`end = start + duration`。`total <= 0` 时退化为 `(cycleStart, cycleStart)`。
|
||
3. **今天**:`NatureTimeUseMode==1` → `(今天0点, 明天0点)`;否则取当天时间表中第一个"上课/课间"时间点的开始 → 最后一个"上课/课间"时间点的结束;课程不可用时按 `NatureTimeUseMode==2` 退化为 `(DateTime.MinValue, DateTime.MinValue)`(不显示倒计时),否则退化为自然日。
|
||
4. **本周**:按 `WeekCountdownStartDay`(或学期开始日的星期)求出本周起点;`NatureTimeUseMode==1` → 周起点到下周起点+1 天;否则遍历 7 天找第一个有课的日子与最后一个有课的日子,取其首/末时间点拼成区间;课程全无时按模式 2 退化或自然周。
|
||
- 进度百分比(`:288-295`):`value = (IsProgressInverted ? end - now : now - start).TotalSeconds`;`totalSeconds <= 0` 时 `Percent = 0`;开启进度精度降低(主界面全局设置 `ReduceProgressAccuracy` 默认 `true`)时 `progressTick = max(10, totalSeconds/500)`,`Percent = round(value/progressTick)*progressTick / totalSeconds * 100`;否则 `progressTick = 1`。
|
||
- 边界:
|
||
- 终点在过去 → 数值全 0、`Percent=0`、进度环不画。
|
||
- 起点在未来(固定时间模式配错)→ `Percent` 为负,必须钳到 0。
|
||
- 名称/连接词为空字符串时不得留出空 span 的间距(用 `display:none` 而非空文本)。
|
||
- 字号变化(16–30)时进度环/进度条不跟随放大,只放大文本。
|
||
- Vue 结构建议:`CountDownNode.vue` props = `{ settings, now }`,内部 `computed` 出 `remaining`、`percent`、`text`;进度环用 SVG `circle` + `stroke-dashoffset = C * (1 - percent/100)`,`C = 2π*9.5 ≈ 59.69`。
|
||
|
||
### 5.5 课程表(ScheduleComponent)
|
||
|
||
课程表是"一行多条课程"的核心组件,内部由 `LessonsListBox`(`repos/ClassIsland/ClassIsland.Core/Controls/LessonsControls/LessonsListBox.axaml`)渲染一组课程控件。
|
||
|
||
#### 5.5.1 外层结构
|
||
|
||
```html
|
||
<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`):
|
||
|
||
```html
|
||
<div class="lesson lesson-expanded">
|
||
<span class="spacer" style="width:16px * ScheduleSpacing"></span>
|
||
<span class="subject"><em>语文</em></span> <!-- HighlightBox 包裹 -->
|
||
<span class="extra"> <!-- margin-left:6px,l-secondary -->
|
||
<span class="detail">08:00-08:40</span> <!-- 依 ExtraInfoType 切换 -->
|
||
</span>
|
||
<span class="spacer" style="width:16px * ScheduleSpacing"></span>
|
||
<div class="progress"><i style="transform:scaleX(ProgressPercent)"></i></div>
|
||
</div>
|
||
```
|
||
|
||
- 左右各一个 `16px` 间隔,宽度按 `ScheduleSpacing` 水平缩放(`ScaleTransform ScaleX`,`:51-56, 219-224`);`ScheduleSpacing` 默认 `1`,UI 范围 `0.2–1`(`ScheduleComponentSettingsControl.axaml:156`),因此左右各 3.2–16px。
|
||
- 科目名:`font-size = var(--font-emphasized)`(18px)、`font-weight: bold`,包在 `HighlightBox` 里(`:65-68`)。
|
||
- 附加信息区:`margin: 6px 0 0 0`、类 `l-secondary`(次要色、`font-size = var(--font-secondary)` = 14px)、贴底(`:72-84`)。
|
||
- `ExtraInfoType`(`ShowExtraInfoOnTimePoint=true` 时生效)6 种(`:85-171`):
|
||
- `0`:`开始时间-结束时间`,格式 `hh:mm`(如 `08:00-08:40`);
|
||
- `1`:已过时间,格式 `-mm:ss`(秒数向上/向下取整见转换器);
|
||
- `2`:剩余时间,格式 `-mm:ss`;
|
||
- `3`:进度百分比;
|
||
- `4`:模糊倒计时,`-<1min` 形式(用 `CountdownSeconds` 向上取整);
|
||
- `5`:精确倒计时,`-mm:ss`。
|
||
- 当前课程的"剩余时间药丸"(`IsCountdownEnabled` 且 `MasterTabIndex==1` 时):背景 `AccentFillColorTertiaryBrush` + `opacity: 0.3` 的底 + `1px` 边框 `AccentFillColorDefaultBrush` + `padding: 8px 2px` + 圆角 = 高度的一半(药丸);文本 `font-size = var(--font-body)`(16px),普通模式 `-mm:ss`,非精确倒计时模式 `<<countdownSeconds`(`:173-214`)。
|
||
- 课程进度条:贴底、`Maximum=1`、`Value=ProgressPercent`、`min-width:0`、宽度撑满、**高度固定 `3px`(`--ci-progress-height`)**(`:227-236`)。
|
||
- 换课高亮:`ChangedClass && HighlightChangedClass`(设置项 `HighlightChangedClass` 默认 `false`)时 `HighlightBox.IsHighlight = true`(`:30-32`),即科目名后出现强调色下划色块。
|
||
|
||
**(b) 最小型 `LessonControlMinimized`**(`TimeLayoutItem.IsHideDefault` 或"上课时仅显示当前课程"等场合):只显示科目名首字(`Subject.Initial`),`font-size = var(--font-emphasized)`,左右各 `10px * ScheduleSpacing` 间隔(`LessonControlMinimized.axaml:30-64`)。
|
||
|
||
**(c) 分割线 `LessonControlSeparator`**(时间点为"分割线/Action"时):一条 `2px` 竖线、高 `25px`、颜色 `TextFillColorSecondaryBrush`、`opacity: 0.8`,左右各 `10px * ScheduleSpacing`(`LessonControlSeperator.axaml:8-26`)。注意:分割线是**每个时间点自己的一项**,不合并。
|
||
|
||
`HighlightBox` 的高亮条规格:`background: var(--accent)`、`opacity: 0.4`、高 `12px`、垂直居中、`margin: -2px 8px -2px 0`、`border-radius: 2px`(`repos/ClassIsland/ClassIsland.Core/Controls/HighlightBox.axaml:15-18`)。
|
||
|
||
#### 5.5.3 哪些时间点显示(必须逐条实现)
|
||
|
||
对每个 `TimeLayoutItem`(`ScheduleComponent.axaml.cs:243-251`):
|
||
|
||
1. `TimeType ∈ {2 分割线, 3 行动}` 不显示【注意:分割线类型由专门的 `LessonControlSeparator` 渲染,判定顺序以源码为准】;
|
||
2. `HideFinishedClass=true` 且 `EndTime < now` → 不显示;
|
||
3. `IsHideDefault=true` 且不是当前选中项 → 不显示;
|
||
4. `TimeType==1`(课间)且不是当前选中项 → 不显示;
|
||
5. `ShowCurrentLessonOnlyOnClass=true` 且当前选中项是"上课"类型且该项不是当前项 → 不显示。
|
||
|
||
`ShowCurrentLessonOnlyOnClass` 的取值来自"附加的课程控件设置"(`LessonControlAttachedSettings`,优先级链:附加设置 → 组件设置)或组件设置本身(`ScheduleComponent.axaml.cs:207-215`)。
|
||
|
||
#### 5.5.4 空状态、明天课表与"放学后"
|
||
|
||
- 空状态占位符(`ScheduleComponent.axaml:26-57, 105-107`):
|
||
- 今天没有课表(`LessonsService.CurrentClassPlan == null`)或(显示明天课表时明天也没有)→ 显示 `PlaceholderTextNoClass`,默认 `"今天没有课程。"`;
|
||
- 今日课程全部结束且 `HideFinishedClass=true`(`AfterSchool` 状态)→ 显示 `PlaceholderTextAllClassEnded`,默认 `"今日课程已全部结束。"`;
|
||
- 两者都受 `ShowPlaceholderOnEmptyClassPlan`(默认 `true`)控制;垂直居中、`font-size = var(--font-body)`。
|
||
- 占位符与课程列表互斥显示(不重叠)。
|
||
- 明天课表(`TomorrowScheduleShowMode`,默认 `1`,`:166-172, 59-76`):
|
||
- `0` 不显示;
|
||
- `1` 放学后显示;
|
||
- `2` 总是显示;
|
||
- `3` 今天无可显示课程时显示。
|
||
显示时:左侧出现"明天"药丸、主列表隐藏、明天列表显示;明天课表无数据时退回占位符逻辑。
|
||
- 放学后(`AfterSchool`)= 当前时间状态为"放学后"或今天没有课表(`:188-193`)。
|
||
|
||
#### 5.5.5 课程表设置项与默认值(`LessonControlSettings`)
|
||
|
||
(`repos/ClassIsland/ClassIsland/Models/ComponentSettings/LessonControlSettings.cs:8-23`,与全局设置同名项默认值一致:`Models/Settings.cs:103-108, 208-209, 221`)
|
||
|
||
| 字段 | 默认 | UI 文案 | 说明 |
|
||
|---|---|---|---|
|
||
| `ShowExtraInfoOnTimePoint` | `true` | 显示时间点附加信息 | |
|
||
| `ExtraInfoType` | `0` | 要显示的附加信息 | 0–5,见 5.5.2 |
|
||
| `IsCountdownEnabled` | `true` | 时间点结束倒计时 | |
|
||
| `CountdownSeconds` | `60` | 时间点结束倒计时时长 | 秒 |
|
||
| `IsNonExactCountdownEnabled` | `false` | 使用模糊倒计时 | 显示 `<<数值` |
|
||
| `ExtraInfo4ShowSecondsSeconds` | `0` | — | 【预留】保留 schema、不给 UI |
|
||
| `ScheduleSpacing` | `1` | 课程表文本间距 | 0.2–1,影响左右间隔 |
|
||
| `ShowCurrentLessonOnlyOnClass` | `false` | 上课时仅显示当前课程 | |
|
||
| `HideFinishedClass` | `false` | 不显示已结束的课程 | |
|
||
| `FadeCompletedClasses` | `true` | 淡化上过的课程 | 透明度 0.6 |
|
||
| `HighlightChangedClass` | `false` | 标记换课课程 | |
|
||
| `ShowPlaceholderOnEmptyClassPlan` | `true` | 无课程时显示占位符 | |
|
||
| `PlaceholderTextNoClass` | `"今天没有课程。"` | 当日没有课程的占位符内容 | |
|
||
| `PlaceholderTextAllClassEnded` | `"今日课程已全部结束。"` | 当日课程结束的占位符内容 | |
|
||
| `ShowTomorrowSchedules` | `false` | — | 【预留】保留 schema、不给 UI(语义已被 `TomorrowScheduleShowMode` 取代) |
|
||
| `TomorrowScheduleShowMode` | `1` | 显示明天课表 | 0–3 |
|
||
|
||
"时间点结束倒计时"只在"当前时间点剩余时间 ≤ CountdownSeconds"时出现(药丸),否则不显示。
|
||
|
||
#### 5.5.6 边界
|
||
|
||
- 课表里有 20 节课且全部展开时,一行放不下:课程表组件不换行、不滚动(`ScrollViewer` 禁用),超出部分被行裁切。必须在设置里引导用户启用"不显示已结束的课程"或"仅显示当前课程"。
|
||
- 时间点为 `null`(无课表)时不得抛异常,走占位符分支。
|
||
- `ScheduleSpacing` 为 `0.2` 时左右间隔各 3.2px,视觉上仍要能区分相邻课程。
|
||
- 课程名过长:科目名 `text-overflow: ellipsis`(ClassHelper 同样处理,`ComponentNode.vue:322-327`),但不要截断到只剩 1 个字符。
|
||
|
||
### 5.6 天气简报(WeatherComponent)
|
||
|
||
结构(`WeatherComponent.axaml:50-313`):横排 `StackPanel`、`Spacing 6`、垂直居中、子项居中。
|
||
|
||
#### 5.6.1 主天气信息(`MainWeatherInfoKind`,默认 `0`,共 6 种)
|
||
|
||
| 值 | 内容 | 图标/尺寸 | 文本 |
|
||
|---|---|---|---|
|
||
| `0` | 天气图标 + 温度 | `WeatherPackIconControl` 高 `22px` | 温度值 + 单位,`margin-left:6px` |
|
||
| `1` | 湿度 | FluentIcon `\uE794`,22×22 | 湿度值 + 单位 |
|
||
| `2` | 风 | FluentIcon `\uF43B` 22×22 + FluentIcon `\uE196` 22×22(按风向角旋转,`transform-origin: center`,`margin-left:6px`) | 风速值 + 单位 |
|
||
| `3` | AQI | FluentIcon `\uEA7D` 22×22(颜色随 AQI 等级 1–6 变) | AQI 数值 |
|
||
| `4` | 气压 | FluentIcon `\uE8E0` 22×22 | 气压值 + 单位 |
|
||
| `5` | 体感温度 | FluentIcon `\uF1B3` 22×22 | 体感值 + 单位 |
|
||
|
||
- 所有 `ci|FluentIcon` 统一 `margin: 0 -2px 0 0`(`WeatherComponent.axaml:42-44`),用于视觉居中补偿。
|
||
- 温度/数值文本继承 `--font-body`(16px)。
|
||
- AQI 等级配色:`AqiLevel` 1–6 分别用 `AqiLevel1Brush` … `AqiLevel6Brush`(主题资源,`:22-41`),等级来自 `Settings.LastWeatherInfo.Aqi.AqiLevel`(`WeatherComponent.axaml.cs:47-50`)。实现时把 6 个颜色做成 CSS 变量 `--aqi-1`…`--aqi-6`,按等级切 `data-aqi`。
|
||
|
||
#### 5.6.2 气象预警(`ShowAlerts`,默认 `true`)
|
||
|
||
- 容器:`WrapPanel`(带重排动画)、`vertical-align:center`、`overflow:visible`;`visible = ShowAlerts && 预警数 > 0`(`:186-198`)。
|
||
- 每种预警有两种形态,由 `AlertsTitleShowMode` 决定(默认 `1`):
|
||
- `0 = 总是图标`:只显示 22×22 图标;
|
||
- `1 = 默认`:**有自定义图标**的预警显示图标,没有的显示全文药丸(`IsDefaultIcon` 判定);
|
||
- `2 = 总是全文`。
|
||
- 图标形态:`<img width="22" height="22" style="margin-left:2px">`(`:246-251`)。
|
||
- 全文形态(`:252-263`):
|
||
|
||
```css
|
||
.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`):
|
||
|
||
```css
|
||
.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`):
|
||
|
||
```css
|
||
.slide-item { position: relative; }
|
||
/* 进入 */
|
||
.slide-item.is-selected .content {
|
||
animation: slide-in 250ms cubic-bezier(0.65, 0, 0.35, 1) both;
|
||
}
|
||
@keyframes slide-in {
|
||
0% { opacity: 0; transform: translateY(-40px); visibility: visible; }
|
||
100% { opacity: 1; transform: translateY(0); }
|
||
}
|
||
/* 离开 */
|
||
.slide-item:not(.is-selected) .content {
|
||
animation: slide-out 250ms cubic-bezier(0.65, 0, 0.35, 1) both;
|
||
}
|
||
@keyframes slide-out {
|
||
0% { opacity: 1; transform: translateY(0); visibility: visible; }
|
||
100% { opacity: 0; transform: translateY(40px); visibility: hidden; }
|
||
}
|
||
```
|
||
|
||
源码里两段动画都是 `Duration=250ms`、`KeySpline="0.65, 0, 0.35, 1"`(`:72-107`),进入位移 `-40 → 0`、离开位移 `0 → +40`,透明度 0↔1 同步;离开结束后 `IsVisible=false`。
|
||
- 轮播调度(`SlideComponent.axaml.cs:51-121`):
|
||
- 间隔 = `Settings.SlideSeconds`(默认 `15`,UI 最小 `0.1`);
|
||
- 子组件数 ≤ 1 时不轮播,恒显示第 0 个;
|
||
- `SlideMode`:`0 循环`(+1 取模)、`1 随机`(预生成打乱队列,用尽再洗牌,保证一轮内不重复)、`2 往复`(到边界反向);
|
||
- 跳过被隐藏规则的子组件(循环查找可见项,全部隐藏时停止);
|
||
- 规则:`IsPauseOnRuleEnabled` 满足 → 暂停定时器;`IsStopOnRuleEnabled` 满足 → 停止定时器并把索引归 0;两者同时满足时"停止"优先。
|
||
- 每项 `MaxHeight = 40px`(`:119`),`ComponentPresenter` `Margin="6 0"`、`VerticalAlignment=Stretch`。
|
||
- 设置项与默认值(`Models/ComponentSettings/SlideComponentSettings.cs`):
|
||
|
||
| 字段 | 默认 | UI |
|
||
|---|---|---|
|
||
| `SlideSeconds` | `15` | 轮换间隔(秒,≥0.1) |
|
||
| `SlideMode` | `0` | 轮换模式(循环/随机/往复) |
|
||
| `IsPauseOnRuleEnabled` | `false` | 满足规则时暂停 |
|
||
| `PauseRule` | `{}` | 规则集 |
|
||
| `IsStopOnRuleEnabled` | `false` | 满足规则时停止 |
|
||
| `StopRule` | `{}` | 规则集 |
|
||
| `UseOldPresentingBehavior` | `false` | 【预留】保留 schema、不给 UI(旧版呈现行为的兼容开关) |
|
||
| `IsAnimationEnabled` | `false` | 启用动画 |
|
||
|
||
- **【预留】字段处置**:`UseOldPresentingBehavior`、滚动容器的 `PauseOnRule`/`PauseRule`、课程表的 `ExtraInfo4ShowSecondsSeconds`/`ShowTomorrowSchedules` 均**保留 JSON schema 与默认值、迁移时原样写回,但不提供 UI 开关**。实现不得删除这些字段(会破坏旧配置反序列化),也不得为其臆造设置项。
|
||
|
||
- 边界:切换时不能让行高变化(所有项都叠在同一格,容器高度取最大者,且被 `MaxHeight` 钳制);只有一个子组件时不产生任何过渡动画。
|
||
|
||
### 5.10 分组容器(GroupComponent)
|
||
|
||
- 语义:把多个组件横向组合成一个整体(用于让它们共享一个"宽度固定/对齐"的约束)。
|
||
- 结构(`GroupComponent.axaml:13-33`):`ItemsControl` + `StackPanel`(`Orientation=Horizontal`,类 `components-panel`),`margin: 0 -6px`,子项 `ComponentPresenter` `Margin="6 0"`。
|
||
- 与"行直接放组件"的唯一区别:分组容器可以整体设置宽度/对齐/不透明度/背景/圆角,内部子组件单独再设各自的覆盖项。
|
||
- 设置:只有 `Children`(`GroupComponentSettings.cs`)。
|
||
- 边界:空分组不占宽度(自动塌缩为 0,不留 12px 空隙)。
|
||
|
||
### 5.11 堆叠容器(StackComponent)
|
||
|
||
- 语义:多个子组件**重叠**在同一位置显示(用于"层叠"效果,例如文字 + 背景图标)。
|
||
- 结构(`StackComponent.axaml:12-32`):`ItemsControl` + `Panel`(类 `components-panel`),`margin: 0 -6px`;`Panel` 的布局语义是"所有子元素占据同一矩形,各自按自身对齐方式摆放"。
|
||
- CSS 实现:
|
||
|
||
```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`):
|
||
|
||
```css
|
||
.node-separator { width: 2px; display: flex; align-items: center; justify-content: center; height: 100%; }
|
||
.node-separator .line { width: 2px; height: 25px; background: var(--text-fill-secondary); opacity: 0.8; }
|
||
```
|
||
|
||
- 源码用 `Grid Width="2"` 包一条 `Line`:`StrokeThickness=2`、`StartPoint 0,0` → `EndPoint 0,25`、`Stroke=TextFillColorSecondaryBrush`、`Opacity=0.8`、垂直/水平居中。
|
||
- 设置:无(只有通用设置)。
|
||
- 边界:位于行首/行尾时仍然占 2px + 两侧各 6px = 14px,不做特殊收边(与 ClassIsland 一致)。
|
||
|
||
### 5.13 组件设置 UI(组件设置页 + 高级设置页)
|
||
|
||
抽屉标题栏与页签见 4.8.6。两页内容:
|
||
|
||
**页 1「组件设置」**:渲染组件自己的 `SettingsType` 控件(`ComponentPresenter` 以 `IsPresentingSettings=true` 渲染,`EditModeView.axaml:287-290`),即本文每个组件的"设置项"清单。
|
||
|
||
**页 2「高级设置」**:按下面顺序渲染(`repos/ClassIsland/ClassIsland.Core/Controls/ComponentAdvancedSettingsControl.axaml`):
|
||
|
||
1. 按规则隐藏(开关 + 说明"注意此处的隐藏不会释放组件资源");
|
||
2. 不透明度(Slider 0–1,步进 0.05);
|
||
3. 自定义背景颜色(开关 + ColorPicker);
|
||
4. 自定义背景不透明度(开关 + Slider);
|
||
5. 自定义背景圆角半径(开关 + Slider 0–20,步进 0.5,文案"调至最大数值时左右边缘为半圆");
|
||
6. 字体大小(总开关 `IsResourceOverridingEnabled` + 四个 NumberBox:次级/正文/强调/大号,范围 1–40,`ci:Field` 宽 175、后缀 `px`);
|
||
7. 字体颜色(开关 + ColorPicker);
|
||
8. 对齐方式(4 个图标按钮:左 `\uE03E`、居中 `\uE03A`、拉伸 `\uE034`、右 `\uE03C`);
|
||
9. 固定宽度(开关 + NumberBox);
|
||
10. 宽度限制(两个开关 + 两个 NumberBox:最大/最小);
|
||
11. 自定义边距(开关 + 4 个 NumberBox:左/上/右/下)。
|
||
|
||
`ci:Field` 后缀单位固定为 `px`;字号输入框的 `SpinButtonPlacementMode=Inline`、`Minimum=1`、`Maximum=40`(`EditModeView.axaml:418-427` 的等价写法)。
|
||
|
||
### 5.14 空数据与异常兜底(逐组件)
|
||
|
||
| 场景 | 必须表现 |
|
||
|---|---|
|
||
| 课表为空 / 无今日课表 | 课程表显示 `PlaceholderTextNoClass`(默认"今天没有课程。");不得显示空白胶囊 |
|
||
| 今日课程全部结束 | 显示 `PlaceholderTextAllClassEnded`(默认"今日课程已全部结束。"),或按设置隐藏(`HideFinishedClass`) |
|
||
| 天气数据缺失 | 天气组件隐藏温度/图标;只显示可用的预警/降水;不显示假值 |
|
||
| 天气图标加载失败 | 该预警显示"图裂"占位(红色背景 + 警告图标),不得整块消失 |
|
||
| 倒计时区间非法(end<start) | `Percent=0`、数值全 0、进度环不画 |
|
||
| 倒计时区间为 0 长度 | 同上,不除零 |
|
||
| 计时服务不可用 | 回退本地时间(时钟、日期、倒计时全部回退) |
|
||
| 滚动内容未溢出 | 不滚动、不抖动 |
|
||
| 轮播只有 1 个子项 | 不轮播、不播过渡动画 |
|
||
| 子组件全部被规则隐藏 | 容器塌缩为 0 宽,可行则整行隐藏(`IsAllComponentsHid`) |
|
||
| 设置对象为 `null` / 字段缺失 | 回退到该字段默认值(JSON 反序列化时补默认,不得抛异常) |
|
||
| 组件 ID 在注册表中找不到 | 渲染"缺失组件"占位:显示 `NameCache`(缓存名)或 ID,图标用警告图标,宽度 100px;不得崩溃、不得吞掉整行 |
|
||
|
||
### 5.15 Vue 组件结构与 CSS 变量
|
||
|
||
必须采用下面这套文件结构(与 ClassHelper 的 `ComponentNode.vue` 思路一致,`repos/ClassHelper-master/packages/desktop-client/src/island/ComponentNode.vue:212-293`):
|
||
|
||
```
|
||
src/island/components/
|
||
ComponentHost.vue // 宿主:宽度/对齐/透明度/隐藏/占位
|
||
registry.ts // ID → { name, icon, description, container, component, settingsSchema, defaults }
|
||
nodes/
|
||
DateNode.vue
|
||
ClockNode.vue
|
||
CountDownNode.vue
|
||
ScheduleNode.vue (+ LessonNode.vue / LessonSeparatorNode.vue)
|
||
WeatherNode.vue
|
||
TextNode.vue
|
||
RollingNode.vue
|
||
SlideNode.vue
|
||
GroupNode.vue
|
||
StackNode.vue
|
||
SeparatorNode.vue
|
||
ContainerNode.vue // 通用容器包装,按 kind 分发
|
||
```
|
||
|
||
统一 props 约定:`{ node: ComponentSettings, state: IslandState, now: number, schedules, timePoints }`;所有节点根元素类名固定为 `component-node node-<kind>`(与 ClassHelper 一致,便于主题覆盖与自动化断言)。
|
||
|
||
必须暴露的 CSS 变量(在 `:root` 与组件节点上均可覆盖)。**规范 token 名以 part 3 的 `--ci-*` 为准**,下表左侧为本文别名,落地时按右侧映射:
|
||
|
||
| 本文别名 | 规范 token | 值 |
|
||
|---|---|---|
|
||
| `--font-secondary` | `--ci-fs-secondary` | 14px |
|
||
| `--font-body` | `--ci-fs-body` | 16px |
|
||
| `--font-emphasized` | `--ci-fs-emphasized` | 18px |
|
||
| `--font-large` | `--ci-fs-large` | 20px |
|
||
| `--text-fill-primary` | `--ci-text-1` | 0.92 白 |
|
||
| `--text-fill-secondary` | `--ci-text-2` | 0.72 白 |
|
||
| `--accent` | `--ci-accent` / 填充 `--ci-accent-fill` | 默认 `#0a84ff`(用户强调色 DodgerBlue) |
|
||
| `--control-fill-default` | `--ci-fill-secondary` | 0.10 白 |
|
||
| `--control-strong-stroke-disabled` | `--ci-stroke-control` | 0.30 白 |
|
||
| `--rainy-color` | `--ci-weather-rain` | 降雨色 |
|
||
| `--aqi-1` … `--aqi-6` | `--ci-aqi-1` … `--ci-aqi-6` | AQI 六级色 |
|
||
| `--node-gap` | `--ci-component-gap` | 12px |
|
||
| 进度条高度 | `--ci-progress-height` | **3px**(倒计时进度条、课程进度条共用) |
|
||
| 高亮条 | `--ci-accent-fill` + opacity 0.4 | — |
|
||
|
||
```css
|
||
: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 外边距之和 */
|
||
}
|
||
```
|
||
|
||
组件节点通用样式:
|
||
|
||
```css
|
||
.component-node {
|
||
display: flex;
|
||
align-items: center; /* 所有组件在 40px 行内垂直居中 */
|
||
min-width: 0;
|
||
flex-shrink: 0;
|
||
white-space: nowrap;
|
||
}
|
||
.component-node.text, .component-node.schedule { white-space: normal; }
|
||
```
|
||
|
||
### 5.16 验收标准
|
||
|
||
1. 首次启动生成 1 行 2 组件(日期 + 课程表),日期显示形如 `周三 11/12`,课程表显示当天课程或占位符。
|
||
2. 11 个内置组件全部能从组件库拖入主界面,拖入后立刻可编辑、可删除,删除后无残留占位。
|
||
3. 时钟:显示 `HH:mm`;开启闪动后冒号 1 秒亮 1 秒灭;开启秒数后变 `HH:mm:ss` 且冒号常亮;数字等宽,切换时不水平抖动。
|
||
4. 倒计时:4 种时间源(固定时间/周期/今天/本周)各配一遍,数值、进度环、进度条、连接词显示正确;`%D/%H/%M/%S/%X/%P/%p/%L/%d/%h/%m/%s/%x` 全部 token 输出与源码规则一致(尤其 `%m/%s/%x` 的补零与向上取整语义)。
|
||
5. 倒计时进度环从 12 点方向顺时针填充,满 100% 时不出现缺口;紧凑模式只显示"名称 + 数值"。
|
||
6. 课程表:当前课程高亮、已结束课程淡化到 0.6、剩余时间药丸在阈值内出现;`HideFinishedClass` / `ShowCurrentLessonOnlyOnClass` / `FadeCompletedClasses` / `HighlightChangedClass` 各自开关验证;4 种"显示明天课表"模式行为正确。
|
||
7. 课程表空状态:无课表显示"今天没有课程。";全部结束显示"今日课程已全部结束。";`ShowPlaceholderOnEmptyClassPlan=false` 时两者都不显示。
|
||
8. 天气:6 种主信息逐一切换显示正确;有预警时按"总是图标/默认/总是全文"三种模式渲染;降水药丸在 `RainRemainingMinutes>0` 时出现、`=0` 时消失;模糊模式显示"约 X 分钟"、精确模式显示"Xmin"。
|
||
9. 文本:字号 16 与 30 时高度都不超过 40px;自定义颜色生效/关闭后回退;空文本不占宽。
|
||
10. 滚动容器:内容超出时按 `SpeedPixelPerSecond` 匀速滚动,`PauseSeconds` 停顿位置等于 `PauseOffsetX`;内容未超出时静止;命中"停止规则"后回到初始位置。
|
||
11. 轮播容器:`SlideSeconds` 到点切换;循环/随机/往复三种模式各验证一轮;开启动画时切换有 250ms 上下位移淡入淡出,关闭时无过渡;只有 1 个子项时不轮播。
|
||
12. 分组容器的子项横向排列,间距恒定 12px;堆叠容器的子项完全重叠在同一矩形且各自按对齐方式摆放;分割线为 2×25px、透明度 0.8。
|
||
13. 所有组件的"高级设置"项生效:固定宽度、最大/最小宽度、对齐、不透明度、自定义边距、字号覆盖、字体颜色覆盖;隐藏规则勾选后组件消失但在编辑模式显示斜条纹占位。
|
||
14. 组件 ID 不存在时显示"缺失组件"占位而不是白屏或崩溃。
|
||
15. 全部组件在 40px 行高内垂直居中,无任何组件把行撑高超过 40px。
|