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

69 KiB
Raw Blame History

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

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

4.1 术语与实现映射总则

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

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

强制约定:

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

4.2 窗口(BrowserWindow)逐项规格

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

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

逐条规则:

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

4.2.1 置顶复核与性能底线

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

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

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

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

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

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

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

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

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

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

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

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

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

4.3.3 编辑模式窗口

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

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

4.3.4 触发重算的时机与节流

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

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

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

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

4.4 DOM 层级与 CSS 变量

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

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

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

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

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

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

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

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

4.5 主界面行(MainWindowLine)

4.5.1 三种模板

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

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

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

4.5.2 尺寸与间距(精确值)

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

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

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

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

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

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

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

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

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

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

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

其中:

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

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

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

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

4.5.6 分体主界面(Separated)

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

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

4.5.7 行的过渡动画

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

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

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

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

行为约束:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

4.7.2 遮罩(Mask)动画

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

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

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

4.7.3 浮层(Overlay)

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

4.8 编辑模式

4.8.1 进入与退出

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

4.8.2 编辑画布

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

4.8.3 底部操作栏(OperateArea)

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

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

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

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

4.8.4 抽屉(VerticalDrawer)

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

4.8.5 组件库抽屉

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

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

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

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

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

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

4.8.8 编辑模式的组件外观

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

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

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

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

检测数据源与频率:

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

强制显示(覆盖隐藏):

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

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

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

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

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

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

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

4.12 验收标准

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

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