- 重构版主提示词(14 parts,面向 ClassHelper 灵动岛重构) - ClassIsland 全量规格提示词(20 parts,含分卷与逐章版本) - 投喂指南、分卷、逐节文件 - 调研记录、一致性基线、生成与质检脚本
756 lines
69 KiB
Markdown
756 lines
69 KiB
Markdown
## 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`。
|