- 重构版主提示词(14 parts,面向 ClassHelper 灵动岛重构) - ClassIsland 全量规格提示词(20 parts,含分卷与逐章版本) - 投喂指南、分卷、逐节文件 - 调研记录、一致性基线、生成与质检脚本
69 KiB
4. 主界面(岛):窗口、行、背景、进度、遮罩、编辑模式
本节是「主界面(MainWindow,俗称岛)」的完整实现规格。实现技术栈固定为 ClassHelper 的 Electron + Vue 3 + TypeScript:主进程用 BrowserWindow 复刻 Avalonia Window 的窗口级行为,渲染进程用 Vue 单文件组件 + CSS 复刻 MainWindow.axaml 的控件树与全部像素参数。所有 Avalonia 的"逻辑像素(DIP,96 DPI 基准)"一律直接对应 CSS px;Settings.Scale 用 transform: scale() 实现。
4.1 术语与实现映射总则
必须先建立下面这张映射表,后文所有规则都按它落地:
| ClassIsland 概念 | 源码实体 | 本实现替代物 |
|---|---|---|
| 主界面窗口 | MainWindow : Window(repos/ClassIsland/ClassIsland/MainWindow.axaml) |
独立 island.html 渲染入口 + 主进程 BrowserWindow(透明、无边框、置顶、跳过任务栏) |
| 主界面行 | MainWindowLine(repos/ClassIsland/ClassIsland/Controls/MainWindowLine.cs) |
IslandLine.vue(Vue 组件),一行一个 |
| 组件宿主 | ComponentPresenter(repos/ClassIsland/ClassIsland.Core/Controls/ComponentPresenter.axaml) |
ComponentHost.vue(统一承载宽度约束/透明度/隐藏规则/裁剪) |
| 组件 | DateComponent / ClockComponent / … |
components/DateNode.vue 等,见第 5 章 |
| 主题样式 | XamlThemes/FluentTheme/Styles.axaml、ClassicTheme/Styles.axaml |
island.css + CSS 变量主题包(--island-*) |
| 编辑模式视图 | Controls/EditMode/EditModeView.axaml |
IslandEditMode.vue(抽屉 + 拖动 + 选择) |
| 提醒宿主 | 行模板内的 Overlay / Mask 层 | 行内 .line-mask / .line-overlay |
强制约定:
- 不得把主界面做成"一个自适应大小的 div 窗口",也不得照搬 ClassIsland 的"窗口高度随内容自适应"。采用 4.3.1 的固定包围盒:窗口宽度 = 所选显示器整屏宽(DIP),高度 = 预设最大内容高 + 上下安全余量;可见岛在窗口内形变,其高度 = 行高之和 + 行间距之和。窗口大于岛的透明区域不会遮挡桌面(Windows 侧整窗被设为鼠标穿透,见 4.2)。
- 不得使用
win.setBackgroundMaterial()(亚克力/mica)。ClassIsland 的透明是逐像素 alpha +WS_EX_LAYERED,用系统材质会破坏像素级透明与圆角(repos/ClassHelper-master/packages/desktop-client/src/main/island.ts:523-524的既有结论)。 - 不得给主界面行加 CSS
box-shadow之外的重滤镜;Fluent 主题只有BoxShadow,Classic 主题只有提醒遮罩期才挂DropShadowEffect。 - 所有尺寸、时长、缓动必须按本文给的确切值写死为常量或 CSS 变量,不得凭观感微调。
4.2 窗口(BrowserWindow)逐项规格
按下面顺序创建窗口(默认非编辑模式):
const win = new BrowserWindow({
...geometry, // 见 4.3,x / y / width / height
show: false,
frame: false, // Avalonia: SystemDecorations=None(MainWindow.axaml:171)
transparent: true, // Background=Transparent + TransparencyLevelHint=Transparent(:169-170)
resizable: false, // CanResize=False(:172)
movable: false,
minimizable: false, // CanMinimize=False(:174)
maximizable: false, // CanMaximize=False(:173)
fullscreenable: false,
skipTaskbar: true, // 默认不进任务栏;例外见下
hasShadow: false, // 阴影由岛自己的 CSS box-shadow 画
focusable: false, // 普通态不抢焦点(配合 showInactive)
alwaysOnTop: true, // WindowLayer 默认 1=置顶(Settings.cs:52)
type: 'toolbar', // 对应 WS_EX_TOOLWINDOW(MainWindow.axaml.cs:838)
backgroundColor: '#00000000',
webPreferences: {
preload: /* island preload 白名单 */,
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
backgroundThrottling: false,
},
});
逐条规则:
- 透明与无边框:
transparent: true+frame: false+hasShadow: false+backgroundColor: '#00000000'。HTML 根节点(html, body, #app)必须margin:0;padding:0;width:100%;height:100%;background:transparent;overflow:hidden;-webkit-user-select:none;user-select:none(对齐repos/ClassHelper-master/packages/desktop-client/src/island/classic-island.css:8-19)。 - 鼠标穿透(核心):ClassIsland 在 Windows 上给主窗口加
WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_NOACTIVATE并调用SetLayeredWindowAttributes(repos/ClassIsland/platforms/ClassIsland.Platforms.Windows/Services/WindowPlatformService.cs:130-150),因此非编辑模式下整个窗口对鼠标完全透明,点击直接落到桌面。由此得:- 非编辑模式:调用
win.setIgnoreMouseEvents(true, { forward: true }),且不得注册任何需要点击的交互(行、组件都不可点)。 - 编辑模式:调用
win.setIgnoreMouseEvents(false)恢复命中,并win.focus()。 - 不得实现"点岛展开/收起"这类交互;ClassIsland 的主界面是纯展示层。旧设置项
IsMouseClickingEnabled已被废弃并被强制关闭(repos/ClassIsland/ClassIsland/MainWindow.axaml.cs:733-737,默认值false见Models/Settings.cs:53)。
- 非编辑模式:调用
- 不抢焦点:显示时用
win.showInactive();非编辑态focusable: false。若为了接收触摸事件必须保持可激活,则只允许在编辑模式内切换focusable,且退出编辑模式后立即恢复。 - 置顶/置底:
Settings.WindowLayer取值0=置底、1=置顶,默认1(Models/Settings.cs:52,:1961-1978)。置顶时win.setAlwaysOnTop(true, 'screen-saver')并win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });置底时win.setAlwaysOnTop(false)并用setAlwaysOnTop(false)+ 必要时win.moveBottom()(Windows 上等价于HWND_BOTTOM,见WindowPlatformService.cs:151-159)。置顶优先级还要被三个条件覆盖(MainWindow.axaml.cs:791-803, 843-874):- 处于自由窗口模式(编辑模式的 windowed)或编辑模式时为普通窗口;
IsNotificationWindowExplicitShowed=true(有提醒/编辑)时无论WindowLayer都置顶;WindowTopmostRecheckMode决定重查时机:0由窗口 Z 序变化事件驱动、1前台窗口变化时、2主计时器每 tick、31ms 高频计时器(MainWindow.axaml.cs:420-436, 461-466, 687-704,默认WindowTopmostRecheckMode = 0见Models/Settings.cs:1860)。本实现的复核策略见 4.2.1,禁止照搬3的 1ms 轮询。
- 任务栏显示:
ShowInTaskbar是IsWindowMode || IsScreenRecordingModeEnabled的逻辑或(MainWindow.axaml:34-39)。即只有"编辑模式 + 自由窗口"或"屏幕录制模式"才进任务栏。实现用win.setSkipTaskbar(!(isWindowedEditMode || settings.isScreenRecordingModeEnabled))。- P2 / 能力说明:
IsWindowMode(=EditModeIsWindowMode && IsEditMode,ViewModels/MainViewModel.cs:363)本任务不实现具体 UI 入口,只保留设置字段与"编辑模式可切成自由窗口"的能力说明。依据:ClassIsland 2.1 中该设置字段存在,但编辑页(Controls/EditMode/EditModeView.axaml)没有可见的切换按钮【推断:入口可能在教程步骤或后续版本】,实现者不得臆造按钮。
- P2 / 能力说明:
- 防截屏:
IsWindowCaptureBlockingEnabled=true时调用win.setContentProtection(true)(对应WDA_EXCLUDEFROMCAPTURE,WindowPlatformService.cs:169-172,MainWindow.axaml.cs:840)。 - 屏幕录制模式:
IsScreenRecordingModeEnabled=true时:不进 ToolWindow(任务栏可见)、不被置底。默认false(Models/Settings.cs:1861)。 - 窗口关闭拦截:主界面窗口不得被用户关闭。ClassIsland 在
Closing事件里e.Cancel = true,只有应用主动退出才放行(MainWindow.axaml.cs:577-590)。实现:win.on('close', (e) => { if (!appQuitting) e.preventDefault(); })。 - 禁止的窗口能力:不得
setAlwaysOnTop到'floating'以下级别;不得开启thickFrame;不得给窗口加系统菜单;不得设置roundedCorners(圆角由 CSS 画)。 - 单实例守卫:见第 12 章(本部分不重复),但主界面窗口必须在单实例锁之后创建。
4.2.1 置顶复核与性能底线
必须事件驱动 + 兜底轮询,禁止任何 1ms/10ms 级高频轮询。
- 事件源(全部接上):
win.on('focus'/'blur')、screen的display-added/removed/metrics-changed、编辑模式进出、setVisibleOnAllWorkspaces变化、提醒开始/结束(对应TopmostLock的Acquire/Release,MainWindow.axaml.cs:1056-1092)。 - 兜底轮询:仅在"当前是置顶态且窗口可见"时启用,间隔 ≥1000ms,每次只做一次轻量
setAlwaysOnTop(true, 'screen-saver')复核(对抗系统抢 Z 序)。窗口隐藏/置底时停止轮询。 - 禁止实现
WindowTopmostRecheckMode === 3的 1ms 高频计时器(MainWindow.axaml.cs:136-139):那是原生SetWindowPos成本极低的平台红利,Electron 侧照搬会持续占用 CPU 并让渲染进程掉帧。 - 同类频率约束(本部分涉及的所有动画/刷新):
- 时钟/日期/倒计时/课程表:1s 节拍(整秒对齐);
- 提醒进度条:100ms(
ReduceProgressAccuracy=true时 250ms 即可); - 行淡出/透明度过渡:由 CSS
transition驱动,不得用 JS 每帧写样式; - 鼠标位置命中判定:只在
pointermove(编辑模式)或 200ms 兜底轮询(非编辑模式不需要,因为整窗穿透)。
4.3 几何:包围盒、可见内容盒与编辑画布
所有坐标计算以「所选显示器」为基准,显示器由 Settings.WindowDockingMonitorIndex 选定;越界或负数时回退主显示器(MainWindow.axaml.cs:806-812,默认 0 见 Models/Settings.cs:51)。设:
screen:所选显示器;screen.bounds为整屏物理像素,screen.workArea为工作区物理像素;dpiX = screen.scaleFactor、dpiY = screen.scaleFactor(Avalonia 取screen.Scaling,MainWindow.axaml.cs:1019-1037);ox = Settings.WindowDockingOffsetX、oy = Settings.WindowDockingOffsetY(单位:物理像素,默认0、0,Models/Settings.cs:49-50);ignoreArea = Settings.IsIgnoreWorkAreaEnabled(默认false,Models/Settings.cs:1835);docking = Settings.WindowDockingLocation(默认1,Models/Settings.cs:48),取值为:
###############
# 0 1 2 #
# #
# 3 4 5 #
###############
即 0/1/2 = 上(左/中/右)、3/4/5 = 下(左/中/右)(Models/Settings.cs:1810-1823)。
4.3.1 实现策略裁决:固定包围盒 + 窗内形变
ClassIsland 的原生模型是"窗口高度随内容自适应"(SizeToContent = Height,MainWindow.axaml.cs:781;窗口高 = 内容高 + 安全区,:916)。本实现不得照搬这一条:Electron 的逐像素透明窗口在频繁 setBounds 时会触发 DWM 重排与透明表面重建,产生整块闪烁/掉帧,而行增删、提醒展开、时钟秒数出现都会触发高度变化。因此采用 ClassHelper 已验证的「固定包围盒 + 窗内形变」方案(repos/ClassHelper-master/packages/desktop-client/src/main/island.ts:480-512,注释 :78-90):
- 窗口创建时一次算好包围盒:
winW = 目标屏幕宽(DIP) = screen.bounds.width / dpiX;winH = contentMaxHeight + 2 * safePadding,其中contentMaxHeight= 预设最大内容高(默认按"当前可见行数 × (40 + 行间距 5) + 上下安全区"计算,并留出一行余量),safePadding ≥ 20(阴影 + 光晕余量,对齐ISLAND_SHADOW_PAD = 10的双侧口径)。
- 可见岛在窗口内形变:窗口不动,岛用 CSS
height过渡(或第 14 章的 spring 形变)在包围盒内伸展/收缩;内容层按 4.5 的行高与间距排布。 - 只有下列情形才
setBounds:显示器/DPI 变化、行数超出包围盒上限、进入/退出编辑模式、窗口偏移/缩放/停靠位置设置变化。这类变更必须节流 ≥100ms(同一窗口内多次合并为一次),且不得与弹簧/高度动画同帧竞争。 - 视觉结果必须与 ClassIsland 一致:用户看到的可见岛高度 = 行高之和 + 行间距之和(行高恒
40,间距MainWindowLineVerticalMargin),可见岛宽度 = 内容自然宽 + 两侧各 12px 内边距。窗口比岛大只是透明余量,用户不可见。 - 禁止:把窗口高度做成"刚好等于内容高"再逐帧改;用
requestAnimationFrame每帧setBounds;在行内容变化(秒数、课程名长度、提醒)时改窗口尺寸。以上都会导致 1px 级抖动。
与 ClassIsland 的差异及理由(必须写进实现注释):差异 = 窗口高度固定为包围盒 vs 随内容自适应;理由 = Electron 透明窗口 resize 抖动/闪烁,且 ClassHelper 的既有实现已证明"固定包围盒 + 窗内画布形变"在开合过程中零抖动(见该文件 :64-70 的设计说明)。视觉结果不做任何妥协:可见岛的行高、间距、圆角、阴影与 4.5 完全一致。
4.3.2 可见内容盒(视觉基准公式)
下面的 ClassIsland 原生公式只用于计算"可见岛"的内容尺寸、安全区裁剪与停靠对齐,不直接作为 BrowserWindow.setBounds 的 height 参数(setBounds 规则见 4.3.1):
contentW、contentH:由渲染进程上报的岛内容尺寸(CSS px)。内容盒 = 所有行高度之和 + 行间距之和(见 4.5)。safe = 主题竖直安全区:Fluent 主题20(repos/ClassIsland/ClassIsland/App.Services.xaml.cs:314),Classic 主题0(ThemeManifest._verticalSafeAreaPx默认 0,repos/ClassIsland/ClassIsland.Core/Models/XamlTheme/ThemeManifest.cs:20)。若正在播放教程且教程吸附在本窗口,则safe = max(safe, 150)(MainWindow.axaml.cs:899-902)。dockingTop = docking ∈ {0,1,2}。safeT = Math.max(dockingTop ? Math.min(safe, oy) : safe, 0) * scale。safeB = Math.max(dockingTop ? safe : Math.min(safe, -oy), 0) * scale。岛锚点 X = screen.workArea.x + ox;岛锚点 Y = dockingTop ? top + oy - safeT : bottom - contentH + oy + safeB,其中top = ignoreArea ? screen.bounds.y : screen.workArea.y,bottom = ignoreArea ? screen.bounds.bottom : screen.workArea.bottom。layoutW = screen.bounds.width / dpiX(布局容器宽 = 整屏宽,用于左/中/右对齐计算,MainWindow.axaml.cs:915)。layoutH = contentH + safeT + safeB(岛的可布局高度;这只是内容画布高,不是窗口height)。- 岛在画布内的偏移:
translateY = safeT、translateX = 0;水平对齐:docking ∈ {0,3}→ 左、{1,4}→ 居中、{2,5}→ 右(对应align-items: flex-start / center / flex-end,与WidthDoubleToRectConverter的 X 偏移逻辑一致,repos/ClassIsland/ClassIsland.Core/Converters/WidthDoubleToRectConverter.cs:66-77)。 - 窗口自身的包围盒位置必须让"岛锚点"落在公式 6 的位置:
winX = 岛锚点X、winY = 岛锚点Y - safePadding(上停靠)/岛锚点Y - contentMaxHeight - safePadding(下停靠,包围盒底部贴锚点)。
4.3.3 编辑模式窗口
进入编辑模式时窗口必须扩展为全屏编辑画布(这一步允许 setBounds,因为是模式切换、不是内容变化):
winX = screen.bounds.x、winY = screen.bounds.y。winW = screen.bounds.width / dpiX。winH = (screen.bounds.height - 1) / dpiY——必须减 1px,防止 Windows 把窗口判定成全屏并触发全屏优化(MainWindow.axaml.cs:970)。- 岛内容在窗口内四边各留 60px 安全边距:
padding = 60px(来源WindowedEditModeScreenMargin = 60,MainWindow.axaml.cs:134),即编辑画布的可用区为winW - 120×winH - 120,岛在其中垂直居中、水平按停靠位置对齐。 - 内容偏移:
translateX = ox / dpiX、translateY = oy / dpiY(编辑模式偏移是逻辑像素,MainWindow.axaml.cs:973-974);clientRect = ignoreArea ? screen.bounds : screen.workArea。 - 鼠标事件必须开启:
win.setIgnoreMouseEvents(false)(见 4.2 第 2 条)。 - 编辑模式 + 自由窗口(windowed):窗口改为取
screen.workArea四边各内缩 60px(winX = workArea.x + 60、winY = workArea.y + 60、winW = (workArea.width - 120) / dpiX、winH = (workArea.height - 120) / dpiY),切回系统装饰(SystemDecorations=Full、可缩放/最大化/最小化,MainWindow.axaml:182-191),标题编辑主界面 | ClassIsland(MainWindow.axaml:180)。 - 退出编辑模式:恢复 4.3.1 的包围盒(位置 + 尺寸),
win.setIgnoreMouseEvents(true, { forward: true }),恢复skipTaskbar。
4.3.4 触发重算的时机与节流
必须接上这些时机,但全部走"标记脏 + ≥100ms 节流"的通道,不得每次直接 setBounds:
- 包围盒(4.3.1):显示器/DPI/显示器插拔变化、行数超出
contentMaxHeight、编辑模式进出、偏移/缩放/停靠设置变化; - 岛内容布局(4.3.2,纯 CSS,不改窗口):主计时器每 tick(
LessonsServiceOnPreMainTimerTicked,MainWindow.axaml.cs:475)、内容容器尺寸变化(StackPanelRootContainer_OnSizeChanged,MainWindow.axaml.cs:786-789)、任何Settings属性变化(MainWindow.axaml.cs:488-501, 730-782)。
节流与稳定:用 requestAnimationFrame 合并同一帧内的多次 setBounds;同一窗口尺寸在 100ms 内多次变化时只保留最后一次;高度目标值四舍五入到整数 CSS px。禁止在主计时器 tick 里直接改窗口尺寸。
恢复与兜底:WindowDockingMonitorIndex 超出显示器数量时回退主屏(MainWindow.axaml.cs:806-812);contentH 为 0(无行/全部隐藏)时岛整体 opacity:0(窗口保留包围盒、不改尺寸),避免窗口在 0 高附近抖动。
4.4 DOM 层级与 CSS 变量
必须严格复刻下面这棵树(左侧为 MainWindow.axaml 的控件,右侧为实现用的标签/类名):
Window → html > body > #app
└ Grid#WindowRoot → .window-root(透明,pointer-events:none)
└ ZoomBorder → .zoom-layer(缩放/平移宿主,overflow:visible)
└ Canvas#LayoutContainerGrid → .layout-container(position:relative; width:100%;height:100%)
└ Panel#WorkingRoot → .working-root(position:absolute; left:0; top:safeT; width:窗口宽; height:contentH)
└ StackPanel#StackPanelRootContainer → .lines-root(display:flex; flex-direction:column; align-items:center|flex-start|flex-end)
└ LayoutTransformControl → .lines-transform(transform: translate(ox,oy) scale(scale))
└ Grid#GridRoot → .grid-root(每行背景/遮罩的公共祖先)
└ Border#ResourceLoaderBorder → .resource-loader(font-size:16px,字体族/字重继承)
└ Grid → .lines-host
├ StackPanel → .lines-stack(display:flex; flex-direction:column; row-gap:5px)
│ └ ItemsControl#RootMainWindowLinesItemsControl → .lines-list
│ └ (每行) Grid ColumnDefinitions="Auto,*,Auto" → .line-row(display:grid; grid-template-columns:auto 1fr auto)
└ ItemsControl#ContainerComponentsEditHost → .container-edit-host(仅编辑模式,position:absolute)
再加两个编辑模式专用兄弟节点:.operate-area(底部操作栏)、.edit-mode-view(抽屉宿主)。
CSS 根变量必须在 #app 或 .grid-root 上集中声明(对齐 ClassHelper 的 --island-* 命名习惯,repos/ClassHelper-master/packages/desktop-client/src/island/IslandApp.vue:522-528):
:root {
/* 尺寸 token(ClassIsland 事实来源) */
--row-height: 40px; /* IslandContainerHeight,MainWindowLine.axaml:16 / FluentTheme/Styles.axaml:65 / ClassicTheme/Styles.axaml:16 */
--row-padding-x: 12px; /* GridContentRoot Margin="12 0",FluentTheme/Styles.axaml:110 */
--row-min-width: 20px; /* GridContentRoot MinWidth="20",同上 */
--row-gap: 5px; /* MainWindowLineVerticalMargin 默认 5,Models/Settings.cs:219 */
--component-margin-x: 6px; /* ComponentPresenter Margin="6 0",MainWindow.axaml:396 */
--island-radius: 8px; /* RadiusX/RadiusY 默认 8,Models/Settings.cs:183-184 */
--island-opacity: 0.5; /* Opacity 默认 0.5,Models/Settings.cs:64 */
--island-scale: 1; /* Scale 默认 1.0,Models/Settings.cs:86 */
--line-border-width: 1px; /* line-background BorderThickness=1,FluentTheme/Styles.axaml:47 */
--line-faded-opacity: 0.05; /* MainWindowLine.axaml:74 */
--safe-top: 20px; /* Fluent 主题安全区,App.Services.xaml.cs:314 */
--safe-bottom: 20px;
/* 字号 token */
--font-secondary: 14px; /* Models/Settings.cs:197 */
--font-body: 16px; /* Models/Settings.cs:198 */
--font-emphasized: 18px; /* Models/Settings.cs:199 */
--font-large: 20px; /* Models/Settings.cs:200 */
/* 字体 */
--font-family: 'HarmonyOS Sans SC', 'Segoe UI Variable Display', 'Segoe UI', 'Microsoft YaHei', sans-serif;
--font-weight: 500; /* MainWindowFontWeight2 默认 FontWeight.Medium,Models/Settings.cs:101 */
}
与设计 token 的命名对齐(强制):本文为便于阅读使用语义化局部名(--row-height、--font-body 等),落地时必须改用 part 3 定义的 --ci-* 规范 token,局部名仅作为别名。对应关系:
| 本文局部名 | 规范 token(part 3) |
|---|---|
--row-height |
--ci-island-height(=40px) |
--row-padding-x / --row-min-width |
--ci-island-pad-x(=12px)/ --ci-island-min-w(=20px) |
--row-gap |
--ci-line-gap(=5px) |
--component-margin-x |
calc(var(--ci-component-gap) / 2)(=6px) |
--island-radius |
--ci-radius-island(=8px) |
--island-opacity(背景层不透明度,0.5) |
--ci-island-opacity(=0.5,作用于行背景) |
行整体不透明度(MainWindowLineSettings.Opacity,默认 1.0) |
--ci-line-opacity(=1.0,作用于整行 opacity;与上面的背景不透明度是两个独立字段,不得合并) |
--island-scale |
--ci-scale(=1) |
--font-secondary/body/emphasized/large |
--ci-fs-secondary/body/emphasized/large(14/16/18/20) |
--accent |
--ci-accent;强调填充用 --ci-accent-fill |
| 行背景(Fluent) | --ci-bg-secondary;描边 --ci-stroke-island + --ci-stroke-island-w |
| 行阴影 | --ci-shadow-island;遮罩阴影 --ci-shadow-mask;Classic 遮罩阴影 --ci-shadow-classic-mask |
| 编辑模式遮罩层 | --ci-smoke |
| 编辑操作栏底距 | --ci-editbar-bottom(=64px) |
| 抽屉宽度 / 设置区最大宽 | --ci-settings-max-w(=960px) |
| 抽屉圆角 / 阴影 | --ci-radius-drawer / --ci-shadow-drawer |
| 行内强调底 | --ci-accent-fill-tertiary |
| 文本层级色 | --ci-text-1/2/3(对应 primary/secondary/tertiary) |
| 进度条高度 | --ci-progress-height(固定 3px,所有进度条共用) |
| 层级 | --ci-z-mask / --ci-z-drawer / --ci-z-window |
字号/字体注入链:--font-* 由设置写入 .resource-loader(对应 Border#ResourceLoaderBorder 上 TextElement.FontSize = MainWindowBodyFontSize,MainWindow.axaml:311-313 与 MainWindow.axaml.cs:766-778),所有子组件用 font-size: var(--font-body) 继承;字体族与字重写在 .zoom-layer(对应 ZoomBorder 上 TextElement.FontFamily/FontWeight 绑定,MainWindow.axaml:211-212)。行级/组件级"资源覆盖"(IsResourceOverridingEnabled)时用内联 CSS 变量覆盖同一批 token。
4.5 主界面行(MainWindowLine)
4.5.1 三种模板
每行按条件选模板,不得混用:
| 模板 | 触发条件 | 差异 |
|---|---|---|
| Full(完整) | IsNotificationEnabled = true |
含背景、遮罩(Mask)、浮层(Overlay)、进度条 |
| Min(精简) | IsNotificationEnabled = false(FluentTheme/Styles.axaml:275-278、ClassicTheme/Styles.axaml:219-222) |
只有背景 + 内容,不含遮罩/浮层/进度条 |
| 分体(Separated) | MainWindowLineSettings.IslandSeparationMode 覆盖为"启用",或全局 Settings.IsIslandSeperated=true(MainWindowLine.cs:478-492) |
不再画整行背景,改为每个组件各自一个背景胶囊(见 4.5.6) |
IslandSeparationMode 的取值语义固定为 0=继承(用全局设置)、1=禁用、2=启用(repos/ClassIsland/ClassIsland.Core/Models/Components/MainWindowLineSettings.cs:47-50,UI 文案"继承/禁用/启用"见 EditModeView.axaml:319-329)。
4.5.2 尺寸与间距(精确值)
- 行高固定
40px:IslandContainerHeight = 40(三处定义:Controls/MainWindowLine.axaml:16、XamlThemes/FluentTheme/Styles.axaml:65、XamlThemes/ClassicTheme/Styles.axaml:16),并同时用作Border#BackgroundBorder.Height、Grid#PART_GridWrapper.Height、Grid#GridContentRoot.Height、遮罩高度、ListBoxItem.MaxHeight。 - 内容盒左右内边距
12px:Grid#GridContentRoot Margin="12 0"(FluentTheme/Styles.axaml:110、ClassicTheme/Styles.axaml:100)。 - 内容盒最小宽
20px:同上MinWidth="20"。 - 行与行之间垂直间距
5px:ItemsPanel StackPanel Spacing="{Binding Settings.MainWindowLineVerticalMargin}"(MainWindow.axaml:344),默认5(Models/Settings.cs:219,设置面板范围0–150、步进5,EditModeView.axaml:114-120)。 - 行内相邻组件水平间距
12px:每个组件宿主Margin="6 0"(MainWindow.axaml:396),左右各 6 相加得 12;容器组件内部再用Margin="-6 0"抵消一次(StackComponent.axaml:14、GroupComponent.axaml:15、RollingComponent.axaml:16),因此嵌套容器不会累积边距。 - 行背景宽度 = 内容实际宽度,不是窗口宽度:
BackgroundWidth由PART_GridWrapper.SizeChanged回写(MainWindowLine.cs:614-617),背景 Border 与遮罩都绑Width="{Binding BackgroundWidth}"(FluentTheme/Styles.axaml:81, 94)。
4.5.3 背景(Fluent 主题,默认主题)
逐条写死(XamlThemes/FluentTheme/Styles.axaml:40-56):
.line-bg {
position: absolute;
height: var(--row-height);
width: var(--line-width); /* = BackgroundWidth,由内容决定 */
background: var(--fill-secondary); /* FluentAvalonia: SolidBackgroundFillColorSecondaryBrush */
border: var(--line-border-width) solid var(--control-elevation-border); /* ControlElevationBorderBrush */
border-radius: 8px; /* = Settings.RadiusX,见下方说明 */
box-shadow: 0 4px 8px 2px #48000000; /* "0 4 8 2 #48000000" */
opacity: var(--island-opacity); /* = Settings.Opacity,默认 0.5 */
}
- 圆角 =
Settings.RadiusX直接作为四角统一半径(转换器SizeDoubleToCornerRadiusConverter以参数1.0相除,repos/ClassIsland/ClassIsland.Core/Converters/SizeDoubleToCornerRadiusConverter.cs:8-17,调用处FluentTheme/Styles.axaml:48)。默认8,设置范围0–20、步进0.5(Models/Settings.cs:183,EditModeView.axaml:98-104)。当半径 ≥ 行高一半(20)时左右边缘为半圆。 - 自定义背景色开启时(
IsCustomBackgroundColorEnabled)背景色替换为Settings.BackgroundColor(默认黑色,Models/Settings.cs:34),仍保留边框、阴影、透明度(FluentTheme/Styles.axaml:51-56)。 - 背景不透明度只作用于背景层:
Opacity绑在Border.line-background上(FluentTheme/Styles.axaml:44),文字不跟着透明。这是硬性要求:文字必须始终 100% 不透明。
4.5.4 行裁剪(圆角裁切矩形)
行内容用几何裁剪切出圆角矩形,X 偏移随停靠位置变化(repos/ClassIsland/ClassIsland.Core/Converters/WidthDoubleToRectConverter.cs:55-92,应用处 FluentTheme/Styles.axaml:21-38):
clipRectWidth = BackgroundWidth // 内容宽
clipRectHeight = 行高(40) 或实际 Bounds.Height
clipX = docking ∈ {0,3} ? 0
: docking ∈ {1,4} ? (行容器宽 - BackgroundWidth)/2
: (行容器宽 - BackgroundWidth) // {2,5}
clipRadius = (RadiusX, RadiusY) = (8, 8)
CSS 实现:行容器 .line-row 上 overflow: visible,内容层 .line-clip 用 clip-path: inset(0 round 8px) 并把 .line-clip 定位到 left: clipX、width: BackgroundWidth、height: 40px;或直接对内容层设置 clip-path: inset(0 calc(...) 0 ...) round 8px。分体模式关闭该裁剪(clip-host 只在 IsIslandSeperated=False 或 clip-enforcing 时生效,FluentTheme/Styles.axaml:21)。
4.5.5 行可见性(三态组合,必须逐条实现)
行的 IsVisible 是下面表达式的逻辑或(Controls/MainWindowLine.axaml:29-49):
visible = ( ( !IsAllComponentsHid || IsOverlayOpen )
&& ( Settings.HideOnRule ? Settings.IsVisible : true ) )
|| MainWindowInEditMode
其中:
IsAllComponentsHid= 该行所有组件都不可见(MainWindowLine.cs:408-413,判定Settings.Children.Any(x => x.IsVisible) == false);注意"全部组件都被隐藏规则隐藏"时整行消失,而不是留一条空胶囊。IsOverlayOpen= 该行正在播放提醒(遮罩/浮层打开);提醒期间即使组件全隐藏也要显示。- 行的规则隐藏(
HideOnRule=true)由HidingRules规则集求值,规则满足时IsVisibleInternal=false并订阅RulesetService.StatusUpdated实时重算(MainWindowLine.cs:347-381)。 - 编辑模式下永远可见(否则无法编辑隐藏行)。
行的 Opacity = Settings.Opacity(行级不透明度,默认 1.0,范围 0.1–1、步进 0.05,MainWindowLine.axaml:51、MainWindowLineSettings.cs:42、EditModeView.axaml:340-346)。
鼠标淡出("指针不在岛上就变淡"):
- 行级:
IsLineFaded = IsMouseInFadingEnabled && (mouseIn XOR IsMouseInFadingReversed),为true且非编辑模式时行opacity = 0.05(MainWindowLine.cs:503-509、MainWindowLine.axaml:73-75)。默认IsMouseInFadingEnabled=true、IsMouseInFadingReversed=false(Models/Settings.cs:193-194)。 - 判定鼠标是否在行内:把行内容矩形转换为屏幕物理坐标后与指针比较,宽度/高度都要乘
dpi * scale(MainWindowLine.cs:619-637)。 - 触摸屏:
TouchInFadingDurationMs > 0时,触摸命中后延迟该毫秒数再把"鼠标在内"置否(MainWindowLine.cs:520-550,默认0,Models/Settings.cs:195)。 - 平台差异:靠
pointerover判定而不是原始输入。
4.5.6 分体主界面(Separated)
全局 Settings.IsIslandSeperated = true 或行级 IslandSeparationMode = 2 时:
- 行不再画整行背景(
BackgroundBorderWrapper.IsVisible = !IsIslandSeperated,FluentTheme/Styles.axaml:78)。 - 每个顶层组件各自渲染一个胶囊(
FluentTheme/Styles.axaml:245-259):
.component-island {
position: relative;
margin: -2px 0; /* Panel Margin="-2 0" */
display: flex;
align-items: center;
}
.component-island > .bg {
position: absolute; inset: 0;
background: 行背景色; opacity: var(--island-opacity);
border: 1px solid 边框色;
border-radius: 8px;
box-shadow: 0 4px 8px 2px #48000000;
}
.component-island > .content {
position: relative;
min-width: 40px; /* Border MinWidth="40" */
padding: 0 16px; /* ContentPresenter Margin="16 0" */
border-radius: 8px;
overflow: hidden;
}
- 分体模式下窗口内容仍按整行排布(
MainWindow.axaml:213-217的WorkingRoot宽度 = 客户端宽),但每行的高度仍40,行内组件按原顺序横排,胶囊之间保留12px间隔(每个组件Margin="6 0")。 - 分体模式下行的
clip-host不再参与裁剪(FluentTheme/Styles.axaml:21),阴影由组件胶囊自己承担。
4.5.7 行的过渡动画
- 行内容透明度:
opacity过渡100ms,线性(DoubleTransition Property="Opacity" Duration="0:0:0.100",FluentTheme/Styles.axaml:268、ClassicTheme/Styles.axaml:206)。 - 背景宽度:Fluent
300mscubic-bezier(0.65, 0, 0.35, 1.0)(FluentTheme/Styles.axaml:269);Classic550mscubic-bezier(0, 1, 0.5, 1.10)(ClassicTheme/Styles.axaml:207,注意控制点y>1会产生一次轻微回弹,必须照写)。 - 行首次挂载淡入:
AnimationLevel ≥ 2时用组合层做opacity 0→1、250ms、cubic-bezier(0.25, 1, 0.5, 1)(MainWindowLine.cs:382-401)。AnimationLevel默认1(Models/Settings.cs:1851),即默认不播。 - 行宽变化(组件增删、内容变化)由"自适应面板重排动画"驱动:
WrapPanelResizingAnimationAssist.Duration = 0.300s、Easing = (0.65, 0, 0.35, 1.0),挂在Grid#WindowRoot上(MainWindow.axaml:197-198),并把animated-resize类加给行的外层Panel(FluentTheme/Styles.axaml:103)。编辑模式下该动画关闭(MainWindow.axaml:345的Classes.animated-resize="{Binding !MainWindowInEditMode}")。
4.5.8 行设置项(MainWindowLineSettings)与默认值
必须实现为一个 JSON 结构 + 抽屉表单。字段与默认值一律照抄(repos/ClassIsland/ClassIsland.Core/Models/Components/MainWindowLineSettings.cs:17-50):
| 字段 | 类型 | 默认 | 说明 / UI |
|---|---|---|---|
Children |
数组 | [] |
行内组件 |
IsMainLine |
bool | false |
主要行;全局唯一 |
IsNotificationEnabled |
bool | true |
是否允许显示提醒;关掉后该行用 Min 模板 |
IsResourceOverridingEnabled |
bool | false |
覆盖字号资源 |
MainWindowSecondaryFontSize |
number | 14 |
次级字号,范围 1–40 |
MainWindowBodyFontSize |
number | 16 |
正文字号 |
MainWindowEmphasizedFontSize |
number | 18 |
强调字号 |
MainWindowLargeFontSize |
number | 20 |
大号字号 |
IsCustomForegroundColorEnabled |
bool | false |
自定义字体颜色 |
ForegroundColor |
color | DodgerBlue |
默认字体色 |
BackgroundOpacity |
number | 0.5 |
自定义背景不透明度 |
IsCustomBackgroundOpacityEnabled |
bool | false |
|
BackgroundColor |
color | Black |
|
IsCustomBackgroundColorEnabled |
bool | false |
|
CustomCornerRadius |
number | 0 |
自定义圆角,范围 0–20 |
IsCustomCornerRadiusEnabled |
bool | false |
|
Opacity |
number | 1.0 |
整行不透明度,范围 0.1–1 |
IsVisible |
bool | true |
运行时态(规则求值结果) |
HideOnRule |
bool | false |
按规则隐藏 |
HidingRules |
Ruleset | {} |
规则集 |
IslandSeparationMode |
int | 0 |
0 继承 / 1 禁用 / 2 启用 |
行为约束:
- 主要行唯一:勾选某行为主要行时,其余行必须自动取消(
MainWindow.axaml.cs:1569-1588);取消唯一的主要行时,自动把第一行设为主要行并弹 toast"已将第一行设置为主要行。" - 主要行是提醒注册的优先消费者(注册序号
-1,非主要行用行号,MainWindowLine.cs:441)。 - 本部分只负责"提醒宿主"与
IsNotificationEnabled标记:行模板的遮罩/浮层/进度条容器(4.7)、行的可见性联动、IsNotificationEnabled决定走 Full 还是 Min 模板。多行之间谁显示提醒的分发规则归属第 7 章(提醒系统),本文不重复、不得在此臆造优先级。 - 删除行:只剩 1 行时禁止删除并弹警告"至少需要保留 1 个主界面行。"(
MainWindow.axaml.cs:1598-1612)。 - 关闭所有行的提醒显示时弹警告"您已经禁用了所有主界面行的提醒显示功能……"(
MainWindow.axaml.cs:1590-1596)。
4.5.9 行内编辑控件(仅在编辑模式显示)
行模板右侧第三列(MainWindow.axaml:401-444)在编辑模式显示一组按钮,容器属性:
.line-actions { /* StackPanel Classes="items-valign-center" */
display: flex;
align-items: center;
gap: 2px; /* Spacing="2" */
font-size: 14px; /* TextElement.FontSize="14" */
}
.line-actions .btn { background: transparent; border: none; }
.line-actions .btn-delete { color: IndianRed; }
四个按钮(自左至右,全部 VerticalAlignment=Center、Background=Transparent、BorderThickness=0):
- 行设置:图标
\uEF27,ToolTip "设置…",打开行设置抽屉。 - 主要行开关(ToggleButton):图标
\uE994,ToolTip "此行是主要行"。 - 提醒开关(ToggleButton):图标
\uE02B,ToolTip "此行可以显示提醒"。 - 删除(Button,
Foreground=IndianRed,Theme=TransparentButton):图标\uE61D,ToolTip "删除"。
行选择框(第一列的虚线矩形,MainWindow.axaml:353-363):
.line-select-box {
position: absolute; inset: 0;
border: 1.5px dashed rgba(控制描边色);
border-radius: 0; /* 无圆角 */
opacity: 0.75;
/* StrokeDashArray="1, 1.5" → dash 1px、gap 1.5px */
background: none;
}
拖动把手(第一列,MainWindow.axaml:364-367):TouchDragThumb,仅在编辑模式可见,垂直方向拖动改变行顺序;拖动阈值由 AdvancedItemDragBehavior 决定,其 Orientation="Vertical"、HorizontalDragThreshold="33550336"(即水平拖拽被彻底禁用)、AllowDragFromDragThumbOnly="True"(MainWindow.axaml:332-335)。
"新主界面行"按钮(MainWindow.axaml:449-455):水平居中、font-size:14px、内容为图标 \uE00D + 文本"新主界面行";仅在编辑模式可见;点击追加一行默认配置并在该行下方插入。
4.6 主界面的缩放(裁决:只做缩放,不做平移)
ZoomBorder(MainWindow.axaml:199-212)必须实现为.zoom-layer,参数:Stretch=None、ZoomSpeed=1.2(每格缩放倍率)、EnableConstrains=True、MaxZoomX=MaxZoomY=3、MinZoomX=MinZoomY=0.5、ShowZoomIndicator=true、EnableGestureRotation=False、Focusable=true、Background=Transparent。- 缩放手势固定为
Ctrl + 滚轮:以指针位置为锚点缩放,每格倍率按ZoomSpeed = 1.2指数累乘,范围钳制在[0.5, 3.0](=MinZoomX/Y与MaxZoomX/Y)。不带Ctrl的滚轮不得缩放(避免与页面/抽屉滚动冲突)。 - 双击画布空白处重置为 100%(等价
ResetMatrix);底部操作栏的"重置视图"按钮同样恢复 100%。 - 缩放指示器:非 100% 时在窗口左下角显示当前倍率,
font-size:12px,background:#77222222、color:#fff【推断:样式对齐MainWindow.axaml:643-649的调试文本样式】。 - P2 / 不做平移:首版只实现
Ctrl+滚轮缩放与双击重置;Space + 拖拽平移标 P2 /【推断】(ClassIsland 的ZoomBorder支持拖拽平移,但控件源码未定位,且非编辑态整窗鼠标穿透用不到),不实现。
- 缩放手势固定为
- 设置里的"界面缩放"是另一件事:
Settings.Scale默认1.0,范围0.75–2.5、步进0.05(Models/Settings.cs:86、EditModeView.axaml:50-57),应用在.lines-transform的transform: scale(),transform-origin需按停靠位置设置:上停靠center top、下停靠center bottom(对齐LayoutTransformControl的布局缩放语义,MainWindow.axaml:257-265)。 - 两者叠加:总视觉倍率 = 缩放设置 × ZoomBorder 倍率;窗口包围盒只由"未缩放的内容尺寸"决定(见 4.3.1),缩放只改渲染层
transform,不改窗口尺寸。 - 不做平移:见上条 P2 说明;
EnableConstrains=true仅用于把缩放后的内容约束在窗口内。
4.7 提醒宿主、遮罩与进度条(行内)
Full 模板的行内部结构(XamlThemes/FluentTheme/Styles.axaml:70-202)必须按下面层级实现:
.line (Grid#GridRoot)
├ .line-bg-wrap → Border#BackgroundBorderWrapper (整行背景层,分体模式隐藏)
│ └ .line-bg → Border#BackgroundBorder (Classes: docking line-background)
├ .line-bg-mask → Border#BackgroundBorderOverlayMask (强调色遮罩,Opacity 0→1,仅提醒时)
│ · opacity 过渡只对 BoxShadow 做 0.1s
├ .line-clip → Grid#ContentClipBorder (Classes: clip-host)
│ └ .line-panel → Panel (Classes: animated-resize)
│ └ .grid-wrapper → Grid#PART_GridWrapper (height 40, Classes: docking)
│ ├ .content-root → Grid#GridContentRoot (height 40, min-width 20, margin 0 12)
│ │ └ .components (组件列表)
│ ├ .overlay → Grid#GridOverlay (opacity 0)
│ │ ├ .overlay-bg → Border (line-background,分体模式才可见)
│ │ └ .overlay-clip → Border (clip, corner-radius 8)
│ │ └ Panel
│ │ ├ Canvas#ProgressContainer (bottom, height = progressBar 高度)
│ │ │ └ ProgressBar#OverlayTimeProgressBar (Maximum 1, Value = LeftProgress, MinWidth 0)
│ │ └ ContentPresenter(OverlayContent,MaxHeight 40)
│ └ .mask-shadow → Border#OverlayMaskShadowRoot (仅非编辑模式)
│ └ .mask → Border#OverlayMask (背景 AccentFillColorDefaultBrush, 前景 TextOnAccentFillColorPrimaryBrush, clip, height 40)
│ └ Panel
│ ├ SlantedMaskControl (Classes: mask-bg, RegionDurationMs=360, StageStaggerMs=120, Fill=强调色)
│ └ Border (Padding 0 12)
│ └ .mask-content → ContentPresenter#OverlayMaskContent (opacity 0, scale)
4.7.1 进度条(提醒剩余时间)
- 容器
Canvas贴底、水平拉伸,高度绑定进度条自身高度(FluentTheme/Styles.axaml:127-147)。 ProgressBar:Maximum=1、Value = 当前提醒的 LeftProgress(0–1,MainWindowLine.axaml:77)、MinWidth=0、Width = 容器宽、贴底。- 值变化过渡:
50ms、线性(DoubleTransition Property="Value" Duration="0:0:0.05"+LinearEasing,FluentTheme/Styles.axaml:137-145)。 - 实现:进度条高度固定
3px(token--ci-progress-height: 3px,border-radius: 1.5px,贴底;三条进度条——行提醒进度条、课程进度条、倒计时进度条——共用同一 token),颜色取AccentFillColorDefaultBrush,transform: scaleX(value)+transform-origin:left比改 width 更省重排;但不能有过渡抖动,transition: transform 50ms linear。 ReduceProgressAccuracy=true(默认,Models/Settings.cs:1853)时进度按步长量化:progressTick = max(10, totalSeconds/500),value = round(seconds/progressTick)*progressTick(倒计时组件同款算法,Controls/Components/CountDownComponent.axaml.cs:288-295)。主界面行进度条同样应做降频更新(100ms 级),false时按 1 秒精度。
4.7.2 遮罩(Mask)动画
SlantedMaskControl:5 个区段宽度权重[0.0, 0.1, 0.2, 0.4, 0.2, 0.1]的累计边界,斜面角度固定60°(Controls/SlantedMaskControl.cs:284, 298-308),填充色AccentFillColorDefaultBrush。- 参数:
RegionDurationMs = 360、StageStaggerMs = 120(行模板里显式赋值,FluentTheme/Styles.axaml:178-179);控件默认值300/120(SlantedMaskControl.cs:29-33)——以行模板的360/120为准。 - 打开(
mask-in)顺序:区段 0 与 4 立即开始(delay 0),区段 1 与 3 延迟stag*0.75 = 90ms,区段 2 延迟stag*1 = 120ms;缓动QuarticEaseOut;每段时长为独立的关键帧(SlantedMaskControl.cs:167-188)。总时长 =120 + 360 = 480ms。 - 关闭(
mask-out)顺序反向:区段 2 先(delay 0),1/3 延迟 90ms,0/4 延迟 120ms;缓动QuadraticEaseIn(SlantedMaskControl.cs:189-200)。 - CSS 实现建议:遮罩层用 5 个
clip-path平行四边形(skewX(-30deg)等价于 60° 斜面),每个用animation: mask-region 360ms <ease> <delay> both,--scale-x: 0 → 1,transform-origin取各自区段中心。 - 遮罩背后的内容:
Grid#GridContentRoot.Opacity在mask-in时置 0,:mask-out时也置 0,:overlay-in/out控制恢复(FluentTheme/Styles.axaml:293-305, 368-370, 411-413, 438-450)。 - 遮罩投影:
mask-in期间背景层与遮罩层加BoxShadow = 0 8 32 0 #66000000(FluentTheme/Styles.axaml:323-328),mask-out时淡掉。 - 遮罩内容入场:
delay 260ms、duration 250ms、ease cubic-bezier(0.25, 1, 0.5, 1)、opacity 0→1(FluentTheme/Styles.axaml:329-340);同时scale 1.1→1.0、duration 750ms、delay 260ms、同缓动(:341-352)。 - 遮罩内容容器:
Padding="12 0"、前景色TextOnAccentFillColorPrimaryBrush(FluentTheme/Styles.axaml:181-184)。 - 强调色底:
Border#BackgroundBorderOverlayMask背景 =AccentFillColorDefaultBrush(StylesBase.axaml:90-93),Opacity 0→1、delay 600ms、duration 100ms、FillMode=Forward(FluentTheme/Styles.axaml:355-367)。
Classic 主题的等价实现(必须两套都做,用 CSS 变量或 data-theme 切换;XamlThemes/ClassicTheme/Styles.axaml):
- 遮罩与强调层用
TranslateTransform Y=-50起手;mask-in:duration 200ms、QuadraticEaseIn、Y: -50 → 0(:265-281)。 mask-out:delay 100ms、duration 200ms、QuadraticEaseOut、Y: 0 → 50(:305-318)。- 遮罩内容淡入:
delay 200ms、duration 100ms、opacity 0→1(:288-300);淡出100ms、opacity 1→0(:319-331)。 - 首次出现时给
Grid#GridRoot挂DropShadowEffect(OffsetX=0, OffsetY=0, Opacity=1, BlurRadius=8),Effect过渡100ms线性(:34-45, 283-287)。
4.7.3 浮层(Overlay)
Grid#GridOverlay初始Opacity=0,带DoubleTransition Opacity 200ms SineEaseIn(FluentTheme/Styles.axaml:419-432)。:overlay-in→ 浮层opacity 1、内容层opacity 0;:overlay-out→ 浮层opacity 0、内容层opacity 1(:434-451)。- 浮层内容
MaxHeight = 40(FluentTheme/Styles.axaml:149);浮层里的进度条只显示"当前提醒剩余时间"。
4.8 编辑模式
4.8.1 进入与退出
- 入口:原生菜单"编辑主界面…"(
MainWindow.axaml:108-109)、URIclassisland://app/edit(MainWindow.axaml.cs:295)。 - 进入前必须通过权限校验(
ManagementService.AuthorizeByLevel(EditSettingsAuthorizeLevel),MainWindow.axaml.cs:1357-1362);被策略禁用时直接返回。 - 进入步骤(按序):
IsEditMode = true→ 窗口Activate()→ 挂载编辑视图 →ZoomBorder.ResetMatrix()在退出时调用 →TopmostEffectWindow.ViewModel.IsEditMode同步 → 更新窗口层级 → 重新加载主题(MainWindow.axaml.cs:510-528)。 - 退出步骤:关闭两级抽屉 →
IsEditMode=false→ 清空教程提示 → 保存组件配置(ComponentsService.SaveConfig(),MainWindow.axaml.cs:1391-1403)。保存必须幂等且原子(写临时文件再替换)。 - 编辑模式的窗口态:ClassIsland 用
Background = SmokeFillColorDefaultBrush+TransparencyLevelHint = "AcrylicBlur, Transparent"+ 标题编辑主界面 | ClassIsland(MainWindow.axaml:177-181)。本实现的裁决:- 禁止调用 Electron 的
setBackgroundMaterial()/vibrancy/backgroundMaterial等任何系统材质 API(原因见 4.1 第 2 条与repos/ClassHelper-master/packages/desktop-client/src/main/island.ts:523-524)。 - 替代方案:一层全屏半透明深色遮罩 div,
background: rgba(0, 0, 0, 0.30)(token--ci-smoke;深色主题可下调至0.24、浅色主题可上调至0.36,但默认必须是0.30),窗口保持transparent: true逐像素透明。 - 不得使用
backdrop-filter: blur():透明窗口下 Chromium 的背板采样不可靠,会出现整块灰底/边缘发白。 - 【推断 / 差异说明】该方案与 FluentAvalonia 的 Acrylic 是近似而非等价:Acrylic 带轻微模糊与噪声,本实现只有纯色压暗。这是有意接受的视觉差异,不得在实现说明里写成"与 Acrylic 完全一致";验收只要求"编辑态背景明显压暗、岛与抽屉可读、退出后恢复逐像素透明"。
- 禁止调用 Electron 的
- 编辑模式下窗口必须接收鼠标:
win.setIgnoreMouseEvents(false);同时Transparent窗口特性关闭(MainWindow.axaml.cs:839)。 - 平台兜底:Windows 10 1803 及更早版本不支持 windowed 透明,需给窗口加标记类
no-windowed-transparent,此时 windowed+edit 模式用不透明底色SolidBackgroundFillColorBaseBrush、TransparencyLevelHint=None(MainWindow.axaml:187-190,MainWindow.axaml.cs:257-260)。
4.8.2 编辑画布
- 画布 = 全屏(宽 = 屏宽,高 = 屏高 −1px),岛内容在画布内四边各留 60px 安全边距,再按
(ox, oy)偏移(见 4.3.3 编辑模式窗口公式)。 - 行容器对齐:
dock-top→align-items:flex-start,dock-bottom→align-items:flex-end,编辑模式 →justify-content:center(对应StackPanelRootContainer的VerticalAlignment覆写,MainWindow.axaml:240-248)。编辑模式下内容整体垂直居中,方便拖拽。 - 底部操作栏占用画布底部 64px 区域,安全边距必须保证岛内容不被操作栏遮挡(岛最高可到画布高 −120px)。
- 空白处左键点击:清除当前选中的组件并关闭组件设置抽屉(
MainWindow.axaml.cs:1511-1535,只有点在ZoomBorder自身(非命中子控件)时触发)。
4.8.3 底部操作栏(OperateArea)
精确规格(MainWindow.axaml:595-634):
.operate-area {
position: fixed;
left: 50%; transform: translateX(-50%);
bottom: 64px; /* Margin="0 0 0 64" */
background: var(--commandbar-overflow-bg); /* CommandBarOverflowPresenterBackground */
border: 1px solid var(--commandbar-overflow-border);
border-radius: 6px;
padding: 4px;
display: flex; /* 内部 StackPanel Spacing="6" */
gap: 6px;
z-index: 10;
pointer-events: auto;
}
.operate-area .divider { width: 1px; height: 25px; background: var(--divider-stroke); align-self: center; }
内部按钮(自左至右,除"完成"外均为 TransparentButton 主题):
- "重置视图"——图标
\uE12F,命令 = 缩放重置。 - 竖分隔线:
StartPoint 0,0 → EndPoint 0,25、StrokeThickness=1、颜色DividerStrokeColorDefaultBrush。 - "外观"——图标
\uE51E,打开外观抽屉;DisableSettingsEditing策略下禁用。 - "添加组件"——图标
\uE071,打开组件库抽屉;同策略禁用。 - 组件配置按钮——图标
\uE699,文本 = 当前配置名(Settings.CurrentComponentConfig,默认"Default",Models/Settings.cs:159),MaxWidth=150;打开配置管理抽屉。 - "完成"——图标
\uE424,Classes="accent"(强调按钮),退出编辑模式。
4.8.4 抽屉(VerticalDrawer)
- 主抽屉:
RequestedWidth = 960(SettingsContainerWidth,ThemeBase.axaml:37;EditModeView.axaml:600-605),AllowCollapse=True,SmokeBackground=False。 - 次抽屉:
SmokeBackground=True、AllowCollapse=False、ContentHeightRatio=0.5(EditModeView.axaml:606-611)。 - 外壳规格(
repos/ClassIsland/ClassIsland.Core/Controls/VerticalDrawer.axaml:8-155):
.drawer-host { position:absolute; inset:0; pointer-events:none; }
.drawer-smoke { position:absolute; inset:0; background: var(--smoke-fill); opacity:0; transition: opacity 250ms cubic-bezier(0,0,0,1); }
.drawer-smoke.open { opacity:1; pointer-events:auto; }
.drawer-panel {
position:absolute; bottom:0; left:50%; transform: translateX(-50%);
width: 960px; max-width: 100%;
max-height: 80vh; /* 【推断:ContainerHeight 由内容/宿主决定,见下 */
background: var(--content-dialog-bg);
border: 1px solid var(--content-dialog-border);
border-top-left-radius: 8px; border-top-right-radius: 8px; /* "8 8 0 0" */
box-shadow: 0 8px 32px 0 #66000000;
overflow: hidden;
pointer-events: auto;
}
.drawer-inner { padding: 8px 8px 0; } /* Padding="8 8 8 0" */
.drawer-head { height: 32px; display:flex; align-items:center; justify-content:space-between; }
.drawer-body { min-height: 0; overflow:auto; }
- 抽屉升降过渡:
Canvas.Bottom与Height各250ms、cubic-bezier(0,0,0,1)(VerticalDrawer.axaml:29-36)。 - 标题栏右侧三个按钮:收起
\uEC5C(仅AllowCollapse且已展开时可见)、展开\uEC5E(AllowCollapse且非展开时)、关闭\uE671(VerticalDrawer.axaml:64-99)。 - 抽屉内容宽度上限 =
960px;设置项容器.settings-container的Margin = 12 12 18 12、Spacing = 4(StylesBase.axaml:67-71)。 - 抽屉打开时挂载"逐项入场"动画类
animated-intro(EditModeView.axaml:43, 317),对应ClassIsland.Core/Themes/StackPanelIntroAnimation.axaml(时长/曲线见第 14 章)。 - 拖拽组件时主抽屉自动临时收起(
Collapsed),拖拽结束自动展开(EditModeView.axaml.cs:143-163)。
4.8.5 组件库抽屉
- 标题"组件库"(
EditModeView.axaml.cs:130)。 - 结构:按组件分组(
ComponentRegistryService.GetGroupedSortedComponents())→ 每组一个group-name(font-weight:SemiBold、margin-bottom:4px)+ 一个WrapPanel(目标宽225px,WrapPanelAutoResizeBehavior,EditModeView.axaml:167-218)。 - 每个组件项:高
50px、Margin 0 8、四列Auto,Auto,*,Auto:
.component-lib-item { display:grid; grid-template-columns:auto auto 1fr auto; height:50px; margin:8px 0; }
.component-lib-item .drag-thumb { width:24px; align-self:center; } /* TouchDragThumb Width=24 */
.component-lib-item .icon { width:32px; height:32px; font-size:32px; margin:0 -4px 4px 0; }
.component-lib-item .name { font-size:14px; overflow:hidden; text-overflow:ellipsis; }
.component-lib-item .desc { grid-column:2 / span 2; font-size:12px; opacity:0.75; white-space:normal; }
- 底部提示文案(按是否已选目标列表切换):
"将组件拖动到主界面上即可添加组件。"/"选择要添加的组件,或将组件拖动到主界面上即可添加组件。",样式类l-instructive,margin:8px 0(EditModeView.axaml:226-235)。 - 已选目标时右下角出现"添加"按钮:
padding:16px 6px、Classes="accent"、margin:4px 0 8px(EditModeView.axaml:236-240)。
4.8.6 组件设置抽屉 / 行设置抽屉
- 标题栏(组件):左侧图标 16×16 + 组件名 +
TabStrip.compact两个页签"组件设置""高级设置"(仅当组件有SettingsType时显示第一个页签);右侧"按规则隐藏"复选框 + 编辑规则按钮(图标\uF17E,仅勾选时启用),Margin 0 0 4 0、Spacing 4(EditModeView.axaml:243-283)。 - 标题栏(行):文本"主界面行设置" + 同样的"按规则隐藏"组合(
EditModeView.axaml:298-313)。 - 抽屉内容宽度上限
960,内部.settings-container(Margin 12 12 18 12、Spacing 4)。 - 选择组件时其它所有列表的选择必须清空(
MainWindow.axaml.cs:1500-1504)。
4.8.7 容器组件浮动编辑框(ContainerComponents)
在编辑模式中,点"查看子组件"把某个容器组件的子组件以浮动小窗形式加到画布上(MainWindow.axaml:457-582):
- 宿主:
ItemsControl+Canvas(CanvasDragBehavior),每项Canvas.Left/Top绑定EditModeContainerComponentInfo.X/Y。 - 初始位置:被点击组件的屏幕点转换到画布坐标,再
Y + 48(MainWindow.axaml.cs:1559-1567)。 - 外框:
Border,HorizontalAlignment=Center、VerticalAlignment=Bottom、Background=CommandBarOverflowPresenterBackground、CornerRadius=6、Padding="4 0"、BorderBrush=CommandBarOverflowPresenterBorderBrush、BorderThickness=1。 - 内容:横排
Spacing 4;- 拖动把手
TouchDragThumb,Width=20,IsExplicitVisible=true; - 关闭按钮(图标
\uE671,ToolTip"关闭")、组件图标 16×16、组件名文本,这一组font-size:14、Spacing 4、Margin "4 0 0 0"; - 子组件列表
EditableComponentsListBox,Height=40。
- 拖动把手
- 拖拽中的项:
opacity: 0.5、z-index: 1(MainWindow.axaml:478-481)。 - 关闭时移除对应 info(
EditModeView.axaml.cs:240-249)。
4.8.8 编辑模式的组件外观
- 被隐藏的组件仍然占位并显示"斜条纹"提示:
OpacityMask为45°线性渐变、Background起止#afffffff(0–0.5)、#50ffffff(0.5–1)、Repeat(repos/ClassIsland/ClassIsland.Core/Controls/ComponentPresenter.axaml:19-33)。CSS:
.comps-hidden { /* 仅编辑模式 */
-webkit-mask-image: repeating-linear-gradient(45deg, #afffffff 0 4px, #50ffffff 4px 8px);
mask-image: repeating-linear-gradient(45deg, #afffffff 0 4px, #50ffffff 4px 8px);
}
- 容器组件未选中时显示虚线边框(
SurfaceStrokeColorDefaultBrush、1.5px、dash 1/1.5、opacity 0.75、margin:0 2px);选中时改为强调色虚线边框(AccentFillColorDefaultBrush,EditableComponentsListBox.axaml:217-252)。 - 选中组件的操作条(Adorner):出现在组件正下方,
Background=CommandBarOverflowPresenterBackground、CornerRadius=6、Padding=4、BorderBrush=CommandBarOverflowPresenterBorderBrush、BorderThickness=1、Spacing 6(EditableComponentsListBox.axaml:100-184)。按钮:组件设置\uEF27、"查看子组件"\uEC30(仅容器组件)、删除\uE61D(IndianRed)、25px竖分隔线、"更多选项…"\uEBAC(菜单:包裹到新容器 / 创建副本)。 - 每个组件的"添加组件"按钮:
height:32px、图标\uE00D、font-size:20px,位于组件列表末尾,只在编辑模式可见(EditableComponentsListBox.axaml:55-60)。 - 拖拽插入指示:拖拽中的列表在
AdornerLayer上画一个强调色 1px 边框 + BoxShadow,并做1s交替无限动画(0%: opacity .1→100%: opacity .6,SineEaseInOut,PlaybackDirection=Alternate,EditableComponentsListBox.axaml:307-336)。 - 拖拽开始阈值:水平/垂直各
3px,拖拽预览opacity 0.5(EditableComponentsListBox.axaml:286-288)。 - 拖拽预览浮标:
Background=AcrylicBackgroundFillColorDefaultBrush、CornerRadius=6、Padding=4、BorderBrush=SurfaceStrokeColorDefaultBrush、BorderThickness=1、图标 16 + 名称、Spacing 4(EditableComponentsListBox.axaml:22-39)。该资源名虽含 Acrylic,但实现必须用不透明降级色--ci-bg-tertiary,不得调用系统材质 API。
4.9 隐藏与显示规则(完整判定)
hideMode(Settings.HideMode,默认 0,Models/Settings.cs:185):
0 = 基本:由三个独立开关控制,全部在GridRoot上以伪类组合判断(MainWindow.axaml:150-160):HideOnClass=true且当前时间状态为"上课" → 隐藏(默认false,Models/Settings.cs:45);HideOnMaxWindow=true且前台窗口最大化 → 隐藏(默认false,Models/Settings.cs:63);HideOnFullscreen=true且前台窗口全屏 → 隐藏(默认false,Models/Settings.cs:54)。
1 = 高级:用Settings.HideRules(规则集,JSON 字段名HiedRules,Models/Settings.cs:462-472)求值,满足则隐藏(MainWindow.axaml:161-163、MainWindow.axaml.cs:444-449)。
检测数据源与频率:
- 全屏/最大化状态在每个主计时器 tick 检测:
PlatformServices.WindowPlatformService.IsForegroundWindowFullscreen(screen)/IsForegroundWindowMaximized(screen)(MainWindow.axaml.cs:451-456)。 - 排除进程名单
ExcludedFullscreenWindow,默认["explorer"](Models/Settings.cs:56-59):这些进程处于前台时不算全屏/最大化,不触发隐藏。 - 判断基准屏幕必须是"本窗口所在屏幕"(
GetSelectedScreenSafe())。
强制显示(覆盖隐藏):
ExplicitShowed= 正在播放提醒或处于编辑模式(MainWindow.axaml:271、MainWindow.axaml.cs:1056-1092的Acquire/ReleaseTopmostLock)。此时即使命中隐藏条件也必须显示,且窗口临时置顶。IsMainWindowVisible(用户手动"隐藏主界面",默认true,Models/Settings.cs:67):GridRoot的可见性 =IsMainWindowVisible || IsEditMode(MainWindow.axaml:135-141)。手动隐藏时同时不接受新提醒(行级AcceptsNotificationRequests要求IsNotificationEnabled && !IsAllComponentsHid && !_isOverlayOpen && !_isUnloading,MainWindowLine.cs:814)。- 所有行都隐藏时(
IsAllComponentsHid对所有行成立):可见岛内容opacity:0,窗口包围盒保持不变(不改尺寸、不隐藏窗口),但不得销毁窗口(提醒到来时要能立刻显示)。
4.10 右键菜单(原生菜单)与托盘
菜单必须是系统原生菜单(Electron Menu.buildFromTemplate + tray.setContextMenu),不得用 HTML 菜单。逐项规格(MainWindow.axaml:51-132):
| # | 文本 | 图标 glyph | 可见条件 | 操作 |
|---|---|---|---|---|
| 1 | ClassIsland |
AppLogo | 总是 | 打开"关于"设置页 |
| 2 | 教学… |
\uE959 |
IsWelcomeWindowShowed |
打开教程中心 |
| 3 | 帮助文档… |
\uE23C |
IsWelcomeWindowShowed |
打开 https://docs.classisland.tech/app/ |
| 4 | 分隔符 | |||
| 5 | 显示主界面 |
\uE813 |
!IsMainWindowVisible && IsWelcomeWindowShowed |
切 IsMainWindowVisible |
| 6 | 隐藏主界面 |
\uE817 |
IsMainWindowVisible && IsWelcomeWindowShowed |
切 IsMainWindowVisible |
| 7 | 清除全部提醒 |
\uE029 |
IsWelcomeWindowShowed && IsNotificationsPlaying |
取消全部提醒 |
| 8 | 更多选项… |
\uEBAC |
存在插件注入项 | 子菜单 |
| 9 | 分隔符 | |||
| 10 | 编辑档案… |
\uE699 |
总是 | 打开档案编辑窗口 |
| 11 | 加载临时课表… |
\uE6A1 |
总是 | 打开档案窗口并展开"临时课表"抽屉 |
| 12 | 换课… |
\uE15F |
总是 | 打开换课窗口(需权限) |
| 13 | 分隔符 | |||
| 14 | 编辑主界面… |
\uE7C9 |
总是 | 进入编辑模式 |
| 15 | 应用设置… |
\uEF27 |
总是 | 打开设置窗口 |
| 16 | 分隔符 | |||
| 17 | 重启 |
\uE0BD |
总是 | 重启应用 |
| 18 | 退出 |
\uE0DF |
总是 | 退出(需权限) |
| 19 | 分隔符 | IsDebugEnabled |
||
| 20 | dev__Debug |
\uE2C8 |
IsDebugEnabled |
子菜单:DevTools / DevPortal / 截图工具 / 教程编辑器 / 启用课表 / 显示崩溃窗口 / WelcomeWindow / WelcomeWindow(full) |
托盘左键点击行为 TaskBarIconClickBehavior:0=打开主菜单(仅 Windows 有效,其他平台自动改为 4)、1=打开档案编辑窗口、2=显示/隐藏主界面、3=打开换课窗口、4=打开设置窗口;默认 Windows 平台 0、其他平台 4(Models/Settings.cs:102, :540-561,实现 MainWindow.axaml.cs:617-662)。
4.11 本部分动画总表(照此实现,不得自创)
| 动画 | 触发 | 属性 | 时长 | 缓动 |
|---|---|---|---|---|
| 行背景色过渡 | 主题/背景色变化 | Background |
200ms | 线性(MainWindow.axaml:40-44 BrushTransition) |
| 行内容透明度 | 行显示/隐藏 | opacity |
100ms | 线性(FluentTheme/Styles.axaml:268) |
| 行背景宽度 | 内容宽变化 | width |
300ms(Fluent)/ 550ms(Classic) | cubic-bezier(0.65,0,0.35,1.0) / cubic-bezier(0,1,0.5,1.1) |
| 行挂载淡入 | 首次可见 | opacity 0→1 |
250ms | cubic-bezier(0.25,1,0.5,1);AnimationLevel≥2 才播 |
| 内容层交错重排 | 组件变化 | 位置/宽 | 300ms | cubic-bezier(0.65,0,0.35,1.0)(MainWindow.axaml:197-198) |
| 遮罩斜掠(开) | 提醒开始 | 5 区段 scaleX 0→1 | 每段 360ms,错峰 0/90/90/120/120ms | cubic-bezier(0.165,0.84,0.44,1)(QuarticEaseOut) |
| 遮罩斜掠(关) | 提醒结束 | 5 区段 scaleX 1→0 | 每段 360ms,反向错峰 | cubic-bezier(0.55,0.085,0.68,0.53)(QuadraticEaseIn) |
| 强调色底淡入 | 提醒开始 | opacity 0→1 |
100ms,delay 600ms | 线性 |
| 遮罩文字淡入 | 提醒开始 | opacity 0→1 |
250ms,delay 260ms | cubic-bezier(0.25,1,0.5,1) |
| 遮罩文字缩放 | 提醒开始 | scale 1.1→1.0 |
750ms,delay 260ms | cubic-bezier(0.25,1,0.5,1) |
| 遮罩文字淡出 | 提醒结束 | opacity 1→0 |
200ms | 线性 |
| 遮罩文字缩小 | 提醒结束 | scale 1.0→1.1 |
200ms | cubic-bezier(0.5,0,0.75,0) |
| 浮层淡入淡出 | Overlay 开始/结束 | opacity |
200ms | SineEaseIn |
| 提醒进度条值 | 剩余时间更新 | value |
50ms | 线性 |
| 行变淡 | 指针离开 | opacity →0.05 |
100ms | 线性 |
| 编辑抽屉升降 | 打开/关闭抽屉 | bottom/height |
250ms | cubic-bezier(0,0,0,1) |
| 遮罩层(smoke) | 抽屉打开 | opacity |
250ms | cubic-bezier(0,0,0,1) |
| 拖拽插入高亮 | 拖动中 | opacity 0.1↔0.6 |
1000ms 交替无限 | SineEaseInOut |
| 斜条纹隐藏提示 | 隐藏组件 | — | 静态 | — |
4.12 验收标准
实现者必须逐条自查,全部通过才算完成本节:
- 非编辑模式下用 Spy++/AutoHotkey 之类工具点击岛的任何位置,点击都穿透到桌面应用;编辑模式下岛内的按钮、拖拽把手、抽屉都能被点到。
- 主界面窗口在 4K(150% 缩放)与 1080p(100% 缩放)两种 DPI 下都不出现 1px 白边、黑边、圆角锯齿;圆角在
RadiusX=0时是直角,在20时左右边缘为半圆。 - 6 个停靠位置逐个验证:
0/1/2贴工作区上边、3/4/5贴下边;左/中/右的横向对齐分别贴左、居中、贴右,且窗口本身始终横跨整屏。 - 修改"窗口向右/向下偏移"后岛整体平移,正值向下时上停靠要把偏移吞进安全区(不得被屏幕边缘裁掉)。
- 行数从 1 增到 5、再减到 1:可见岛高度严格等于"行高之和 + 行间距之和"(误差 ≤1px),岛在窗口包围盒内平滑形变,不出现窗口级闪烁;包围盒只在行数超出
contentMaxHeight时变更一次,且变更被节流 ≥100ms。 5.1 用 DevTools 的 Performance 面板录 10 秒:setBounds调用次数为 0(无设置/行数变化时);置顶复核轮询间隔 ≥1000ms,无 1ms/10ms 级高频定时器。 - 单行内容宽度变化(如时钟秒数出现/消失、课程名变长)时,背景宽度在 300ms 内以
cubic-bezier(0.65,0,0.35,1.0)平滑跟随,文字不重排抖动,且不触发窗口setBounds。 - 行间距设为
0、5、150时,相邻行间距精确等于设置值(用像素标尺截图量)。 - 关闭某行"可以显示提醒"后,该行不再出现遮罩与进度条(走 Min 模板),其余行不受影响;多行同时具备提醒能力时,具体由哪一行显示由第 7 章的分发规则决定,本节只验证宿主与
IsNotificationEnabled标记生效。 8.1 三条进度条(行提醒、课程、倒计时)高度均为3px,圆角1.5px,贴底且不越出行圆角裁切。 - 删除到只剩 1 行时再点删除,出现"至少需要保留 1 个主界面行。"提示且行数不变。
- 勾选 A 行为主要行后 B 行自动取消;取消 A 行后第一行自动成为主要行并出现提示。
- 触发一次提醒:遮罩从左、右两端同时向中间斜掠,总时长约 480ms,文字在 260ms 后淡入并伴随 1.1→1.0 缩放;遮罩期间行内容消失,结束后恢复。
- 阈值条件全开(上课隐藏、最大化隐藏、全屏隐藏),在前台最大化窗口时岛消失、切回桌面 1 个计时器周期内恢复;
ExcludedFullscreenWindow含explorer时桌面(资源管理器)前台不隐藏。 - 鼠标移出岛 0.5 秒内行的整体不透明度降到 0.05,移回立即恢复;勾选"反向"后行为相反。
- 编辑模式:进入后窗口扩展为"屏宽 ×(屏高−1px)",岛内容四边留 60px 安全边距并垂直居中,任务栏出现图标,鼠标可交互;退出后恢复包围盒尺寸与穿透状态,并写盘保存组件配置;编辑态背景为
rgba(0,0,0,0.30)遮罩(无backdrop-filter、无系统材质),退出后恢复逐像素透明。 - 编辑模式下拖拽组件到另一行的插入位置时,出现强调色发光插入线(1s 交替闪烁),松手后组件落到目标位置且行宽平滑重排。
- 容器组件"查看子组件"浮动框出现在被点组件下方 48px 处,可拖动、可关闭,关闭后
ContainerComponents中对应项被移除。 - 主界面完全不可见(手动隐藏 / 全部隐藏规则命中)时收到提醒,岛必须临时显示、置顶,提醒结束后回到隐藏状态。
- 托盘图标左键行为的 5 种取值(
0–4)逐个验证正确,其中非 Windows 平台0自动降级为4。