## 5. 组件系统:全部内置组件逐个规格 本节规定主界面上所有组件的实现规格。组件是"岛"的内容单元:每个组件的实例由一份 JSON(`ComponentSettings`)描述,含「组件 ID + 组件自己的设置对象 + 一系列外观/布局/隐藏覆盖项」。实现必须做成可插拔的注册表(原生 11 个组件 + 后续插件组件走同一接口),不得把组件硬编码进主界面。 ### 5.1 组件系统总则 #### 5.1.1 组件注册表(唯一事实来源) 必须内置下面 11 个组件,ID、名称、图标 glyph、描述、是否容器、设置类型逐字照抄(`[ComponentInfo]` 特性): | 组件 | ID(GUID) | 名称 | 图标 glyph | 描述 | 容器 | 设置类型 | 证据 | |---|---|---|---|---|---|---|---| | 日期 | `DF3F8295-21F6-482E-BADA-FA0E5F14BB66` | 日期 | `\ue304` | 显示今天的日期和星期。 | 否 | 无 | `Controls/Components/DateComponent.axaml.cs:14` | | 时钟 | `9E1AF71D-8F77-4B21-A342-448787104DD9` | 时钟 | `\ue31c` | 显示现在的时间,支持精确到秒。 | 否 | `ClockComponentSettings` | `ClockComponent.axaml.cs:17` | | 倒计时 | `7C645D35-8151-48BA-B4AC-15017460D994` | 倒计时 | `\uf361` | 显示距离某一天的倒计时。 | 否 | `CountDownComponentSettings` | `CountDownComponent.axaml.cs:29` | | 课程表 | `1DB2017D-E374-4BC6-9D57-0B4ADF03A6B8` | 课程表 | `\ue751` | 显示当前的课程表信息。 | 否 | `LessonControlSettings` | `ScheduleComponent.axaml.cs:30` | | 天气简报 | `CA495086-E297-4BEB-9603-C5C1C1A8551E` | 天气简报 | `\uf465` | 显示当前的天气概况和气象预警。 | 否 | `WeatherComponentSettings` | `WeatherComponent.axaml.cs:16` | | 文本 | `EE8F66BD-C423-4E7C-AB46-AA9976B00E08` | 文本 | `\uf323` | 显示自定义文本。 | 否 | `TextComponentSettings` | `TextComponent.axaml.cs:16` | | 滚动容器 | `70FCD5EA-3FAE-4E06-ACA2-4F4DF47F9ACD` | 滚动容器 | `\uf279` | 滚动显示组件内的内容。 | 是 | `RollingComponentSettings` | `RollingComponent.axaml.cs:33` | | 轮播容器 | `7E19A113-D281-4F33-970A-834A0B78B5AD` | 轮播容器 | `\uefc9` | 轮播多个组件。 | 是 | `SlideComponentSettings` | `SlideComponent.axaml.cs:24` | | 分组容器 | `C911D762-107F-40C6-84CC-0146AB3C86B1` | 分组容器 | `\ue92f` | 将多个组件组合到一个组件中显示。 | 是 | `GroupComponentSettings` | `GroupComponent.axaml.cs:11` | | 堆叠容器 | `2D849ECE-9F21-4C78-9434-415CFC283294` | 堆叠容器 | `\uf04f` | 将多个容器堆叠显示。 | 是 | `StackComponentSettings` | `StackComponent.axaml.cs:11` | | 分割线 | `AB0F26D5-9DF6-4575-B844-73B04D0907C1` | 分割线 | `\uf023` | 显示一个分割线,视觉上对组件进行分组。 | 否 | 无 | `SeparatorComponent.axaml.cs:9` | 图标字体为 `FluentSystemIcons-Resizable`(`repos/ClassIsland/ClassIsland.Core/ThemeBase.axaml:46`),`FontFamily` 绑到 `ci|FluentIcon` 全局样式(`StylesBase.axaml:79-81`)。实现时用同名 SVG 图标表 + `mask-image` 或内联 SVG,不得贴无对应关系的图标。 `课程表` 组件有一个历史 ID 需在迁移时映射:`E7831603-61A0-4180-B51B-54AD75B1A4D3` → 现 ID(`[MigrateFrom]`,`ScheduleComponent.axaml.cs:29`)。 #### 5.1.2 默认组件布局(首次启动的样子) 首次启动必须生成 1 个主界面行,行内依次为「日期」「课程表」,其余字段取默认值(`repos/ClassIsland/ClassIsland/Services/ComponentsService.cs:33-52`)。实现即:`lines = [{ children: [dateId, scheduleId], ...默认行设置 }]`。 #### 5.1.3 组件宿主(ComponentPresenter):所有组件共用的外壳 每个组件都必须包在统一的宿主里,宿主负责下面这些语义(`repos/ClassIsland/ClassIsland.Core/Controls/ComponentPresenter.axaml`): 1. **可见性三态**:`visible = IsVisibleInternal || 主界面处于编辑模式`(`:13-18`)。隐藏的组件在编辑模式仍占位。 2. **透明度**:宿主 `opacity = 组件设置.Opacity`(默认 `1.0`,范围 0–1,`:34-36`)。 3. **字号继承**:宿主根元素的 `font-size = var(--font-body)`(16px),即所有组件默认正文字号;组件内部需要别的字号时必须显式引用 `--font-secondary/emphasized/large`(`:37-38`)。 4. **宽度三态**(互斥,优先级:固定宽 > 最大宽/最小宽): - `IsFixedWidthEnabled` → `width = FixedWidth`(默认 `200px`,`:45-47`); - `IsMaxWidthEnabled` → `max-width = MaxWidth`(默认 `300px`,`:39-41`); - `IsMinWidthEnabled` → `min-width = MinWidth`(默认 `100px`,`:42-44`)。 默认三者全 `false`(`repos/ClassIsland/ClassIsland.Core/Models/Components/ComponentSettings.cs:30-35`)。 5. **水平对齐**:宿主内容的 `justify-content` 取 `Settings.HorizontalAlignment`,默认 `stretch`(枚举 `Stretch/Left/Center/Right`,`ComponentSettings.cs:36`,选择器 `:49-51`)。 6. **内边距**:宿主内部 `Shimmer` 容器 `padding: 8px 0`(`:55-57`),但组件自身的横向间距由外层 `margin: 0 6px` 提供(行内模板 `Margin="6 0"`,`MainWindow.axaml:396`),两者都要保留,不得合并。 7. **加载占位**:首次布局或配置切换时显示 Shimmer 占位,占位宽度取上次测量宽度 `LastWidthCache`(默认 `100`,`ComponentSettings.cs:39`,`ComponentPresenter.axaml:69-70`)。占位态不得让行高变化。 8. **隐藏规则**:`HideOnRule + HidingRules` 满足时组件不可见但不卸载、不释放资源(`ComponentSettings.cs:100-123`)。 9. **裁剪**:宿主自身 `overflow: visible`(不对组件内部做裁剪),裁剪交给行的 `clip-path`(见 4.5.4);但轮播容器内部有 40px 高度钳制(见 5.9)。 #### 5.1.4 组件通用设置项("高级设置"页,所有组件都有) 以下字段存在于每一个 `ComponentSettings` 上,默认值固定(`repos/ClassIsland/ClassIsland.Core/Models/Components/ComponentSettings.cs:23-51`),UI 顺序照 `ComponentAdvancedSettingsControl.axaml`: | 字段 | 默认 | UI 控件 | 范围/步进 | |---|---|---|---| | `IsResourceOverridingEnabled` | `false` | 开关 + 4 个字号输入 | 字号 1–40 | | `MainWindowSecondaryFontSize` | `14` | NumberBox | 1–40 | | `MainWindowBodyFontSize` | `16` | NumberBox | 1–40 | | `MainWindowEmphasizedFontSize` | `18` | NumberBox | 1–40 | | `MainWindowLargeFontSize` | `20` | NumberBox | 1–40 | | `IsCustomForegroundColorEnabled` | `false` | 开关 + ColorPicker | — | | `ForegroundColor` | `DodgerBlue` | ColorPicker | — | | `Opacity` | `1.0` | Slider | 0–1,步进 0.05 | | `IsCustomBackgroundColorEnabled` | `false` | 开关 + ColorPicker | — | | `BackgroundColor` | `Black` | ColorPicker | — | | `IsCustomBackgroundOpacityEnabled` | `false` | 开关 + Slider | 0–1,步进 0.05 | | `BackgroundOpacity` | `0.5` | Slider | 0–1 | | `IsCustomCornerRadiusEnabled` | `false` | 开关 + Slider | 0–20,步进 0.5 | | `CustomCornerRadius` | `8.0` | Slider | 0–20 | | `HorizontalAlignment` | `Stretch` | 4 个图标按钮 | 左/居中/拉伸/右 | | `IsFixedWidthEnabled` / `FixedWidth` | `false` / `200` | 开关 + NumberBox | — | | `IsMinWidthEnabled` / `MinWidth` | `false` / `100` | 开关 + NumberBox | — | | `IsMaxWidthEnabled` / `MaxWidth` | `false` / `300` | 开关 + NumberBox | — | | `IsCustomMarginEnabled` | `false` | 开关 | — | | `MarginLeft` / `MarginTop` / `MarginRight` / `MarginBottom` | `0` / `0` / `0` / `0` | 4 个 NumberBox | — | | `HideOnRule` / `HidingRules` | `false` / `{}` | 开关 + 规则集编辑器 | — | | `RelativeLineNumber` | `0` | 内部字段 | — | 对齐按钮图标(`ComponentAdvancedSettingsControl.axaml:223-232`):左=`\uE03E`、居中=`\uE03A`、拉伸=`\uE034`、右=`\uE03C`。 自定义背景/圆角只有在"组件位于独立岛"(分体主界面)语义下才画得出来;普通行内背景由行承担,此时组件的背景设置不生效——实现必须在 UI 上如实表现(禁用或不渲染),不得偷偷画。 #### 5.1.5 容器组件通用约定 - 注册时带 `[ContainerComponent]`,宿主 `ComponentInfo.IsComponentContainer = true`。 - 子组件集合 = 组件设置对象实现 `IComponentContainerSettings.Children`(`ComponentSettings.cs:143-144`);因此 `GroupComponentSettings`、`StackComponentSettings` 只有 `Children` 一个字段(`Models/ComponentSettings/GroupComponentSettings.cs`、`StackComponentSettings.cs`)。 - 容器内部为子组件再套一层 `ComponentPresenter`,`IsOnMainWindow=true`、`Margin="6 0"`,并透传 `HidingRules/HideOnRule`(`GroupComponent.axaml:25-29`、`StackComponent.axaml:24-28`、`RollingComponent.axaml:36-41`、`SlideComponent.axaml:114-120`)。 - 容器的子列表外层容器统一 `Margin="-6 0"`(抵消一次边距),循环嵌套时视觉间距保持 12px 恒定(`GroupComponent.axaml:15`、`StackComponent.axaml:14`、`RollingComponent.axaml:16`、`SlideComponent.axaml:22`)。 - 容器内部的子组件条目使用 "components-panel" 面板类:`WrapPanel`/`StackPanel` 需要"重排动画"标记;`StackPanel` 固定 `Orientation=Horizontal`(`repos/ClassIsland/ClassIsland.Core/Themes/ComponentsPanelStyles.axaml:11-16`)。 #### 5.1.6 组件高度与"岛"的约束 - 组件可用高度恒为 `40px`(行高,见 4.5.2)。所有组件必须在这个高度内垂直居中显示。 - 宿主的 `MaxHeight` 受容器约束:轮播容器显式 `MaxHeight = IslandContainerHeight`(`SlideComponent.axaml:119`)、编辑态列表项 `MaxHeight = IslandContainerHeight`(`EditableComponentsListBox.axaml:70`)。 - 组件内部若出现高度超过 40px 的内容(如两行文字),必须自行 clamp 或按设计折行;不得撑高行。 #### 5.1.7 时间驱动 除文本/分割线外,所有组件在主计时器 tick 时刷新(ClassIsland 用 `ILessonsService.PostMainTimerTicked`,`DateComponent.axaml.cs:39-40`、`ClockComponent.axaml.cs:61`、`CountDownComponent.axaml.cs:142`、`ScheduleComponent.axaml.cs:119`)。实现: - 用单一 1 秒节拍(主进程 `setInterval(1000)` 对齐到整秒边界)+ 事件广播; - 需要亚秒/更快的(滚动、轮播、提醒进度)各自用 `requestAnimationFrame` 或独立定时器,不得把 1 秒节拍改成 16ms 全局广播; - 页面不可见或被系统节流时,靠"重新挂载时立即刷新一次"兜底(对应 `AttachedToVisualTree` 时 `UpdateContent()`)。 ### 5.2 日期(DateComponent) - 注册:见 5.1.1。 - 视觉:一个 `TextBlock`,垂直居中;文本格式 `ddd MM/dd`(`DateComponent.axaml:11`,`StringFormat=\{0:ddd MM/dd\}`)。 - 中文(`zh-CN`)渲染示例:`周三 11/12`;星期用短名(不补"星期"二字),月/日补零两位,斜杠分隔,无空格之外的分隔符(`ddd` 与 `MM/dd` 之间有一个空格)。 - 字号:继承 `--font-body` = 16px;颜色:继承前景色;字重:继承 `--font-weight`(默认 500)。 - 数据:`Today = ExactTimeService.GetCurrentLocalDateTime()`,每次 tick 刷新(`DateComponent.axaml.cs:43-46`)。必须使用"精确时间服务"(含 NTP 校正与时间偏移),不得直接用 `new Date()`。 - 设置:无。 - 边界: - 跨日 00:00 必须自动更新(不能只在启动时取一次)。 - 系统时间被手动修改后 1 个 tick 内反映。 - 语言/区域变化时按 `Intl.DateTimeFormat('zh-CN', { weekday:'short' })` 重新渲染,且宽度变化要触发行宽动画(见 4.5.7)。 - Vue 结构建议:`{{ dateText }}`,`white-space: nowrap; font-variant-numeric: tabular-nums;`。 ### 5.3 时钟(ClockComponent) - 视觉结构(`ClockComponent.axaml:13-41`):外层网格水平/垂直居中,`font-size = var(--font-emphasized)`(18px)。 ```html
09:41 09 41
``` - 三种显示态由两个类控制(`:19-32`): - `TimeSeparatorShowing`:`clock-full` 可见(`opacity:1`),`clock-hour/clock-minute` 隐藏; - 非 `TimeSeparatorShowing`:`clock-full` 隐藏(`opacity:0`),`clock-hour` 左对齐 + `clock-minute` 右对齐显示,二者之间留出分隔符的宽度(用等宽字符宽的占位,或给两个 span 之间固定 `gap: <一个字符宽>`;Avalonia 里两个 TextBlock 在同一 Grid 单元格内用 Left/Right 对齐实现,中间自然留空)。 - `ShowSeconds`:文本格式变为 `HH:mm:ss`(`:28-30`)。 - 分隔符闪动逻辑(`ClockComponent.axaml.cs:71-76`): ``` IsTimeSeparatorShowing = !FlashTimeSeparator || ShowSeconds || (当前秒 % 2 == 1) ``` 即:开启闪动且不显示秒时,冒号在奇数秒显示、偶数秒消失(每次消失 1 秒);显示秒时冒号常亮。 - 数字排版:必须 `font-variant-numeric: tabular-nums`(等宽数字),否则冒号闪动与秒数变化会让整块宽度跳动(ClassHelper 对时钟也做了同样处理:`repos/ClassHelper-master/packages/desktop-client/src/island/ComponentNode.vue:305-308`)。 - 时间源:`CurrentTime = Settings.ShowRealTime ? DateTime.Now : ExactTimeService.GetCurrentLocalDateTime()`(`ClockComponent.axaml.cs:73`)。 - 设置项与默认值(`Models/ComponentSettings/ClockComponentSettings.cs`): | 字段 | 默认 | UI 文案 | 说明 | |---|---|---|---| | `ShowRealTime` | `false` | 使用实际时间 | 显示不经过时间偏移的系统时间 | | `ShowSeconds` | `false` | 显示秒数 | 精确到秒 | | `FlashTimeSeparator` | `true` | 闪动时间分隔符 | 模拟电子时钟分隔符闪动 | - 边界: - 24 小时制,不显示 AM/PM(格式串 `HH:mm`)。 - 显示秒时每秒重绘一次;不显示秒时也需每秒 tick 以驱动闪动。 - 若系统时间不可用(精确时间服务失败)必须回退本地时间,不得显示空白。 - 验收要点:开启闪动后,肉眼观察冒号 1 秒亮 1 秒灭;开启秒数后冒号常亮且每秒变化;整块数字在切换时不发生水平位移。 ### 5.4 倒计时(CountDownComponent) 最复杂的非容器组件。结构(`CountDownComponent.axaml:64-114`): ```html
距离 倒计时 还有 30
``` 尺寸与样式(逐条,`CountDownComponent.axaml:66-112`): - 外层:横排、`gap: 2px`、垂直居中。 - 进度环容器:`margin-right: 4px`、垂直居中;底环 `Ellipse` 宽高 `21.4px`、描边 `2.6px`、颜色 `ControlStrongStrokeColorDisabledBrush`;前景弧 `Path` 宽高 `22px`、`stroke-linecap: round`、`stroke-width: 3.1`、颜色默认 `AccentFillColorDefaultBrush`,开启"进度条使用强调色"后改为 `Settings.FontColor`。 - 弧半径计算必须照抄:控件尺寸 22×22、描边 3、`radius = 22/2 - 3/2 = 9.5`,起点在正上方(`center.Y - radius`),顺时针扫过 `percent/100*360` 度,`percent >= 100` 时按 `99.9999` 处理以避免闭合渲染问题,`percent <= 0` 时不画(`CountDownComponent.axaml.cs:32-85`)。 - 文本:"距离" + 名称 + 连接词 + 数值;"距离"与连接词用 `connector l-instructive` 类(次要色);名称与数值用 `Settings.FontColor` + `Settings.FontSize`(默认 `#FFFF0000`、16px)。 - 紧凑模式(`IsCompactModeEnabled`):隐藏"距离"与连接词,只留名称 + 数值(`:85, :95`)。 - 底部进度条:`Maximum=100`、`Minimum=0`、`Value=Percent`、贴底、`min-width:0`、宽度撑满行、**高度固定 `3px`(token `--ci-progress-height: 3px`,`border-radius: 1.5px`)**;实现用 `width: Percent%` 或 `scaleX`(`:102-112`)。 - 设置项与默认值(`Models/ComponentSettings/CountDownComponentSettings.cs`,逐字段): | 字段 | 默认 | 说明 | |---|---|---| | `CountDownName` | `"倒计时"` | 事件名称 | | `CountdownSource` | `0` | `0 固定时间 / 1 周期 / 2 今天 / 3 本周` | | `StartTime` | 今天 0 点 | 固定时间模式的起点 | | `OverTime` | 今天 0 点 | 固定时间模式的终点(日为 0 的那天) | | `CycleStartTime` | 当前时刻 | 周期模式起点 | | `CycleBeforeDuration` | `TimeSpan.Zero` | 周期前阶段 | | `CycleDuration` | `1 天` | 周期长度 | | `CycleAfterDuration` | `TimeSpan.Zero` | 周期后阶段 | | `IsCycleCountLimited` | `false` | 限制循环次数 | | `CycleCountLimit` | `2` | 循环次数上限(UI 范围 1–33550336) | | `IsAdvancedCycleTimingEnabled` | `false` | 启用前后阶段 | | `NatureTimeUseMode` | `0` | `0 默认 / 1 强制自然时间 / 2 强制课程时间` | | `WeekCountdownStartDay` | `Monday` | 本周模式的第一天 | | `IsCustomWeekCountdownStartDayEnabled` | `false` | 自定义每周第一天(否则取学期开始日的星期) | | `FontColor` | `#FFFF0000` | 名称与数值颜色(UI 文案"强调色") | | `FontSize` | `16` | 范围 16–30,步进 1 | | `IsCompactModeEnabled` | `false` | 紧凑模式 | | `CountDownConnector` | `"还有"` | 连接词 | | `IsConnectorColorEmphasized` | `false` | 连接词用强调色 | | `ShowProgress` | `false` | 显示进度 | | `ProgressBarMode` | `0` | `0 进度环 / 1 进度条` | | `UseAccentOnProgressBar` | `true` | 进度使用强调色(否则用应用主题色) | | `IsProgressInverted` | `false` | 进度反相(用"剩余"而非"已过"计算) | | `CustomStringFormat` | `"%D天"` | 数值格式化模板 | - 格式化模板替换规则(必须逐 token 实现,`CountDownComponent.axaml.cs:268-287`): | Token | 含义 | 取整 | |---|---|---| | `%D` | 总天数 | 向上取整 | | `%H` | 总小时数 | 向上取整 | | `%M` | 总分钟数 | 向上取整 | | `%S` | 总秒数 | 向上取整 | | `%X` | 总毫秒数 | 向上取整 | | `%P` | 已过百分比(`P0`,如 `30%`) | 四舍五入到 2 位小数后按 P0 | | `%p` | 已过百分比(`P2`,如 `30.00%`) | 保留 2 位 | | `%L` | 剩余百分比(`P0`) | 四舍五入到 2 位 | | `%d` | 天(元素值) | 不取整 | | `%h` | 小时(元素值) | 不取整 | | `%m` | 分钟(元素值) | 补两位 | | `%s` | 秒(元素值) | 补两位 | | `%x` | 毫秒(元素值) | 补三位 | 中国区必须用不变文化(`InvariantCulture`)格式化数字,避免千分位。`delta < 0` 时钳制为 0(`:260-264`)。 - 四种时间源的时间区间计算(必须照抄,`CountDownComponent.axaml.cs:166-247`): 1. **固定时间**:`(StartTime, OverTime)`。 2. **周期**:`total = before + duration + after`;`cycles = floor((now - cycleStart)/total)`(启用次数限制时再取 `min(cycles, limit)`);`start = cycleStart + cycles*total + before`;`end = start + duration`。`total <= 0` 时退化为 `(cycleStart, cycleStart)`。 3. **今天**:`NatureTimeUseMode==1` → `(今天0点, 明天0点)`;否则取当天时间表中第一个"上课/课间"时间点的开始 → 最后一个"上课/课间"时间点的结束;课程不可用时按 `NatureTimeUseMode==2` 退化为 `(DateTime.MinValue, DateTime.MinValue)`(不显示倒计时),否则退化为自然日。 4. **本周**:按 `WeekCountdownStartDay`(或学期开始日的星期)求出本周起点;`NatureTimeUseMode==1` → 周起点到下周起点+1 天;否则遍历 7 天找第一个有课的日子与最后一个有课的日子,取其首/末时间点拼成区间;课程全无时按模式 2 退化或自然周。 - 进度百分比(`:288-295`):`value = (IsProgressInverted ? end - now : now - start).TotalSeconds`;`totalSeconds <= 0` 时 `Percent = 0`;开启进度精度降低(主界面全局设置 `ReduceProgressAccuracy` 默认 `true`)时 `progressTick = max(10, totalSeconds/500)`,`Percent = round(value/progressTick)*progressTick / totalSeconds * 100`;否则 `progressTick = 1`。 - 边界: - 终点在过去 → 数值全 0、`Percent=0`、进度环不画。 - 起点在未来(固定时间模式配错)→ `Percent` 为负,必须钳到 0。 - 名称/连接词为空字符串时不得留出空 span 的间距(用 `display:none` 而非空文本)。 - 字号变化(16–30)时进度环/进度条不跟随放大,只放大文本。 - Vue 结构建议:`CountDownNode.vue` props = `{ settings, now }`,内部 `computed` 出 `remaining`、`percent`、`text`;进度环用 SVG `circle` + `stroke-dashoffset = C * (1 - percent/100)`,`C = 2π*9.5 ≈ 59.69`。 ### 5.5 课程表(ScheduleComponent) 课程表是"一行多条课程"的核心组件,内部由 `LessonsListBox`(`repos/ClassIsland/ClassIsland.Core/Controls/LessonsControls/LessonsListBox.axaml`)渲染一组课程控件。 #### 5.5.1 外层结构 ```html
明天
今天没有课程。
``` - 外层 Grid 两列 `Auto, *`(`ScheduleComponent.axaml:78`)。 - "明天"标记:`margin-right: 2px`、垂直居中、背景 `AccentFillColorDefaultBrush`、内边距 `8px 2px`、`border-radius: 16px`(药丸)、文本"明天"、前景 `TextOnAccentFillColorPrimaryBrush`、文本 `margin: 0 1px -1px`(微调基线)(`:79-89`)。 - 课程列表容器:`height: 40px`、水平 `StackPanel`、`ScrollViewer.VerticalScrollBarVisibility=Disabled`(`LessonsListBox.axaml:41-50`)。 - 每个列表项容器(`LessonsListBox.axaml:100-136`):`background: ListViewItemBackground`、`border-radius: var(--control-corner-radius)`(FluentAvalonia 控制圆角,默认 4【推断:FluentAvalonia ControlCornerRadius 默认 4】)、`padding: 0`、`max-height: 40px`、`min-width: 0`、内容纵横都 `stretch`;`:disabled` 时 `opacity: 0.5`;`:pointerover/:pressed/:selected` 各自换背景色(默认鼠标穿透,实际见不到,但编辑/演示态仍要正确)。 - 已结束课程淡化:`opacity: 0.6`,`transition: opacity 150ms cubic-bezier(0.645, 0.045, 0.355, 1)`(`CubicEaseInOut`,`LessonsListBox.axaml:56-77`)。开关 `FadeCompletedClasses`(默认 `true`)。 #### 5.5.2 三种课程控件 **(a) 展开型 `LessonControlExpanded`**(时间点为"上课"时显示,`LessonControlExpanded.axaml`): ```html
语文 08:00-08:40
``` - 左右各一个 `16px` 间隔,宽度按 `ScheduleSpacing` 水平缩放(`ScaleTransform ScaleX`,`:51-56, 219-224`);`ScheduleSpacing` 默认 `1`,UI 范围 `0.2–1`(`ScheduleComponentSettingsControl.axaml:156`),因此左右各 3.2–16px。 - 科目名:`font-size = var(--font-emphasized)`(18px)、`font-weight: bold`,包在 `HighlightBox` 里(`:65-68`)。 - 附加信息区:`margin: 6px 0 0 0`、类 `l-secondary`(次要色、`font-size = var(--font-secondary)` = 14px)、贴底(`:72-84`)。 - `ExtraInfoType`(`ShowExtraInfoOnTimePoint=true` 时生效)6 种(`:85-171`): - `0`:`开始时间-结束时间`,格式 `hh:mm`(如 `08:00-08:40`); - `1`:已过时间,格式 `-mm:ss`(秒数向上/向下取整见转换器); - `2`:剩余时间,格式 `-mm:ss`; - `3`:进度百分比; - `4`:模糊倒计时,`-<1min` 形式(用 `CountdownSeconds` 向上取整); - `5`:精确倒计时,`-mm:ss`。 - 当前课程的"剩余时间药丸"(`IsCountdownEnabled` 且 `MasterTabIndex==1` 时):背景 `AccentFillColorTertiaryBrush` + `opacity: 0.3` 的底 + `1px` 边框 `AccentFillColorDefaultBrush` + `padding: 8px 2px` + 圆角 = 高度的一半(药丸);文本 `font-size = var(--font-body)`(16px),普通模式 `-mm:ss`,非精确倒计时模式 `< 0`(`:186-198`)。 - 每种预警有两种形态,由 `AlertsTitleShowMode` 决定(默认 `1`): - `0 = 总是图标`:只显示 22×22 图标; - `1 = 默认`:**有自定义图标**的预警显示图标,没有的显示全文药丸(`IsDefaultIcon` 判定); - `2 = 总是全文`。 - 图标形态:``(`:246-251`)。 - 全文形态(`:252-263`): ```css .alert-pill { display: grid; grid-template-columns: 1fr 1fr; margin-left: 2px; } .alert-pill .pill-bg { grid-column: 1 / span 2; background: var(--control-fill-default); border-radius: 11px; /* 高度 22 的整圆 */ } .alert-pill img { width: 22px; height: 22px; } .alert-pill .type { margin: 4px 8px 0 4px; /* Margin="4 0 8 0" 水平方向 */ font-size: var(--font-secondary); /* 14px */ align-self: center; } ``` - 预警图标由后端 `Images[icon]` 提供(可能是 GIF/PNG),必须支持动态图与失败兜底(失败时显示感叹号图标,不得整块消失)。 #### 5.6.3 降水提示(`ShowRainTime`,默认 `true`) - 仅当 `雨剩余分钟数 > 0` 时显示(`:268-275`)。 - 双层药丸(`:276-311`): ```css .rain { position: relative; display: flex; align-items: center; padding: 0 8px; height: 22px; } .rain .bg { position: absolute; inset: 0; background: var(--rainy-color); opacity: 0.15; border-radius: 11px; } .rain .ring { position: relative; border: 1px solid var(--rainy-color); border-radius: 11px; padding: 0 8px; display: flex; align-items: center; } .rain .icon { font-size: var(--font-secondary); margin-right: 4px; } /* LucideIcon \uE092 */ .rain .text { font-size: var(--font-secondary); font-weight: 400; } /* 14px */ ``` - 文本两态:`IsFuzzyLongRainCountdownEnabled=true`(默认)→ 用"约 X 分钟/约 X 小时"的模糊文案(`MinutesToApproxTimeConverter`);`false` → 精确 `{n}min`(`:296-308`)。 - 药丸圆角 = 自身高度的一半(`CornerRadius = SizeDoubleToCornerRadiusConverter(Bounds.Height)`)。 #### 5.6.4 设置项与默认值(`WeatherComponentSettings`) | 字段 | 默认 | UI 文案 | |---|---|---| | `ShowMainWeatherInfo` | `true` | 显示天气信息 | | `MainWeatherInfoKind` | `0` | 主信息类型(6 选 1) | | `ShowAlerts` | `true` | 显示预警信号 | | `AlertsTitleShowMode` | `1` | 显示预警类型(0 总是图标/1 默认/2 总是全文) | | `ShowRainTime` | `true` | 显示降水提示 | | `IsFuzzyLongRainCountdownEnabled` | `true` | 模糊长时间降雨倒计时时间显示精度 | #### 5.6.5 天气图标模板 全局设置 `Settings.WeatherIconId`,默认 `"classisland.weatherIcons.lucide"`(`Models/Settings.cs:1849`)。内置 4 套(`App.Services.xaml.cs:287-290`): | ID | 名称 | |---|---| | `classisland.weatherIcons.lucide` | Lucide(默认) | | `classisland.weatherIcons.fluentDesign` | Fluent Design | | `classisland.weatherIcons.sfSymbols` | SF Symbols | | `classisland.weatherIcons.simpleText` | 纯文本 | - 纯文本模板的渲染规则(`Controls/WeatherIcons/SimpleTextWeatherIconTemplate.axaml`):若该图标用于"到"(`Tag='To'`),前置一个 `~` 分隔符,再显示 `WeatherName`;否则只显示 `WeatherName`,水平居中。 - 实现:把这 4 套模板做成可注册的"图标渲染器"(返回 SVG 或文本节点),`weatherPackIcon` 组件按 `WeatherIconId` 分发;未知 ID 必须回退到 lucide。 #### 5.6.6 边界 - 天气数据为空/加载失败:不显示温度与图标(不得显示 `0°` 这种假数据);预警与降水区整体隐藏;组件不得撑高。 - 天气数据过期(`stale`)时仍显示旧数据,但可加 `opacity: 0.7`【推断:ClassIsland 未做此处理,属可选增强】。 - 预警可能同时 3 条以上,`WrapPanel` 在 40px 内只放得下一行,多余项被行裁切;必须按"最多显示 N 条 + 更多"策略实现【推断:ClassIsland 未裁剪,依赖用户自行配置】。 ### 5.7 文本(TextComponent) - 视觉:单个 `TextBlock`,垂直居中,`font-size = Settings.FontSize`(`:18-28`)。 - 设置项与默认值(`Models/ComponentSettings/TextComponentSettings.cs`): | 字段 | 默认 | UI 文案 | |---|---|---| | `TextContent` | `""` | 文本内容 | | `UseCustomFontColor` | `true` | 自定义字体颜色 | | `FontColor` | `#FFFFFF` | 颜色选择器 | | `FontSize` | `16` | 字体大小,范围 16–30、步进 1 | - 颜色仅当 `UseCustomFontColor=true` 时生效,否则继承前景色(宿主/行/全局)。 - 边界:内容为空时不占宽度(`display:none` 或空 span),避免行内留出 12px 空隙;内容超长时按 `max-width` 约束(如果启用了最大/固定宽度)并 `ellipsis`。 - 支持多行文本?ClassIsland 只有一个 `TextBlock` 且行高 40,因此**必须换行时用 `\n` 也只会被裁切**;实现上禁止插入换行,输入框遇到换行符替换为空格。 ### 5.8 滚动容器(RollingComponent) - 语义:内容宽于容器时,内容横向匀速滚动;可定时暂停、可按规则停止。 - 结构(`RollingComponent.axaml:14-48`):外层 `Border`(`overflow: visible`、`margin: 0 -6px`)→ `Canvas`(宽 = 内容 `ItemsControl` 宽、左对齐)→ `Grid`(`grid-template-columns: auto 16px auto`)→ `ItemsControl`(子组件横排)。 - 滚动动画(关键公式,`RollingComponent.axaml.cs:85-135`): ``` innerW = 内容自然宽度 outerW = 容器宽度 仅当 outerW < innerW 才滚动 pausePos = (!IsPauseEnabled || PauseOffsetX > innerW) ? 0 : PauseOffsetX durSeconds = innerW / clamp(SpeedPixelPerSecond, 1, +∞) pauseSeconds= IsPauseEnabled ? PauseSeconds : 0 backSeconds = outerW / clamp(SpeedPixelPerSecond, 1, +∞) totalSeconds= durSeconds + pauseSeconds + backSeconds 关键帧(全部 LinearEasing,IterationBehavior=Forever): t=0 X = 0 t=pause/total X = -pausePos t=(pause+dur)/total X = -(innerW + pausePos) t=(pause+dur)/total (同一时刻跳切) X = outerW t=1 X = 0 ``` 即:先停 `pauseSeconds`(若 `PauseOffsetX>0` 则先移到 `-PauseOffsetX`),再匀速把内容推到完全离开,瞬间跳回右侧,再匀速推回原位。`Duration = pauseSeconds + durSeconds`(`:108`)——注意 Aughor 原实现里 `anim.Duration` 只含前两段,靠 `t=1` 关键帧兜住回程;实现时必须按"总时长 = pause+dur+back"重新归一化时间轴,否则回程速度会与去程不一致(这是源码中的已知近似,实现时应修正以保证视觉匀速)。 - 规则停止:`StopOnRule && 规则满足` → 停止动画并立刻回到初始位置(`visual.Offset.X = -pausePos`),并写一个 1 秒的"空动画"把 Offset 固定住防止被其他动画拖走(`:137-157, 171-180`)。 - 设置项与默认值(`Models/ComponentSettings/RollingComponentSettings.cs`): | 字段 | 默认 | UI 文案 | 范围 | |---|---|---|---| | `SpeedPixelPerSecond` | `40.0` | 滚动速度 | ≥0.1 px/s | | `IsPauseEnabled` | `true` | 定时暂停 | | | `PauseSeconds` | `10.0` | 暂停时长 | 0–最大秒数 | | `PauseOffsetX` | `0.0` | 暂停位置 | ≥0 px | | `PauseOnRule` | `false` | — | 【预留】保留 schema、不给 UI(暂停由 5.9 轮播容器的规则承担) | | `PauseRule` | `{}` | — | 【预留】保留 schema、不给 UI | | `StopOnRule` | `false` | 满足规则时停止 | | | `StopRule` | `{}` | 规则集 | | - 提示 InfoBar(首次使用显示):"滚动组件仅在组件内容超出滚动组件宽度时才会开始滚动。您可能需要在【高级设置】中为本组件设置【最大宽度】或【固定宽度】……"(`RollingComponentSettingsControl.axaml:22-24`,`IsRollingComponentWarningVisible` 默认 `true`,`Models/Settings.cs:1850`)。 - 边界:内容未超出时完全不滚动(不得出现 1px 抖动);`SpeedPixelPerSecond` 极小时按 ≥1 px/s 计算以免除零;子组件全部隐藏时宽度为 0,应停止滚动;窗口失焦/系统节流导致帧不连续时不得跳帧。 ### 5.9 轮播容器(SlideComponent) - 语义:多个子组件共用一块区域,按间隔轮换显示;支持动画过渡。 - 结构(`SlideComponent.axaml:17-125`):`ListBox`(`margin: 0 -6px`、`overflow: visible`、水平滚动禁用、`ItemsPanel=Grid`(所有项叠在同一格))。 - 未启用动画(`IsAnimationEnabled=false`,默认):非选中项 `display:none`(`:51-53`),只显示当前项。 - 启用动画(`:54-108`): ```css .slide-item { position: relative; } /* 进入 */ .slide-item.is-selected .content { animation: slide-in 250ms cubic-bezier(0.65, 0, 0.35, 1) both; } @keyframes slide-in { 0% { opacity: 0; transform: translateY(-40px); visibility: visible; } 100% { opacity: 1; transform: translateY(0); } } /* 离开 */ .slide-item:not(.is-selected) .content { animation: slide-out 250ms cubic-bezier(0.65, 0, 0.35, 1) both; } @keyframes slide-out { 0% { opacity: 1; transform: translateY(0); visibility: visible; } 100% { opacity: 0; transform: translateY(40px); visibility: hidden; } } ``` 源码里两段动画都是 `Duration=250ms`、`KeySpline="0.65, 0, 0.35, 1"`(`:72-107`),进入位移 `-40 → 0`、离开位移 `0 → +40`,透明度 0↔1 同步;离开结束后 `IsVisible=false`。 - 轮播调度(`SlideComponent.axaml.cs:51-121`): - 间隔 = `Settings.SlideSeconds`(默认 `15`,UI 最小 `0.1`); - 子组件数 ≤ 1 时不轮播,恒显示第 0 个; - `SlideMode`:`0 循环`(+1 取模)、`1 随机`(预生成打乱队列,用尽再洗牌,保证一轮内不重复)、`2 往复`(到边界反向); - 跳过被隐藏规则的子组件(循环查找可见项,全部隐藏时停止); - 规则:`IsPauseOnRuleEnabled` 满足 → 暂停定时器;`IsStopOnRuleEnabled` 满足 → 停止定时器并把索引归 0;两者同时满足时"停止"优先。 - 每项 `MaxHeight = 40px`(`:119`),`ComponentPresenter` `Margin="6 0"`、`VerticalAlignment=Stretch`。 - 设置项与默认值(`Models/ComponentSettings/SlideComponentSettings.cs`): | 字段 | 默认 | UI | |---|---|---| | `SlideSeconds` | `15` | 轮换间隔(秒,≥0.1) | | `SlideMode` | `0` | 轮换模式(循环/随机/往复) | | `IsPauseOnRuleEnabled` | `false` | 满足规则时暂停 | | `PauseRule` | `{}` | 规则集 | | `IsStopOnRuleEnabled` | `false` | 满足规则时停止 | | `StopRule` | `{}` | 规则集 | | `UseOldPresentingBehavior` | `false` | 【预留】保留 schema、不给 UI(旧版呈现行为的兼容开关) | | `IsAnimationEnabled` | `false` | 启用动画 | - **【预留】字段处置**:`UseOldPresentingBehavior`、滚动容器的 `PauseOnRule`/`PauseRule`、课程表的 `ExtraInfo4ShowSecondsSeconds`/`ShowTomorrowSchedules` 均**保留 JSON schema 与默认值、迁移时原样写回,但不提供 UI 开关**。实现不得删除这些字段(会破坏旧配置反序列化),也不得为其臆造设置项。 - 边界:切换时不能让行高变化(所有项都叠在同一格,容器高度取最大者,且被 `MaxHeight` 钳制);只有一个子组件时不产生任何过渡动画。 ### 5.10 分组容器(GroupComponent) - 语义:把多个组件横向组合成一个整体(用于让它们共享一个"宽度固定/对齐"的约束)。 - 结构(`GroupComponent.axaml:13-33`):`ItemsControl` + `StackPanel`(`Orientation=Horizontal`,类 `components-panel`),`margin: 0 -6px`,子项 `ComponentPresenter` `Margin="6 0"`。 - 与"行直接放组件"的唯一区别:分组容器可以整体设置宽度/对齐/不透明度/背景/圆角,内部子组件单独再设各自的覆盖项。 - 设置:只有 `Children`(`GroupComponentSettings.cs`)。 - 边界:空分组不占宽度(自动塌缩为 0,不留 12px 空隙)。 ### 5.11 堆叠容器(StackComponent) - 语义:多个子组件**重叠**在同一位置显示(用于"层叠"效果,例如文字 + 背景图标)。 - 结构(`StackComponent.axaml:12-32`):`ItemsControl` + `Panel`(类 `components-panel`),`margin: 0 -6px`;`Panel` 的布局语义是"所有子元素占据同一矩形,各自按自身对齐方式摆放"。 - CSS 实现: ```css .stack-panel { position: relative; display: block; } .stack-panel > .component-host { position: absolute; inset: 0; /* 所有子项同一矩形 */ display: flex; /* 由子项自身 justify/align 决定落点 */ align-items: center; margin: 0 6px; } ``` - 尺寸:容器宽高取所有子项测量值的最大者(Avalonia `Panel` 的行为);行高仍是 40px。 - 设置:只有 `Children`。 - 边界:子项之间不得互相拦截命中(普通模式全窗穿透,无影响);子项数量多时不要改变容器宽度以外的布局。 - 与轮播容器的区别要写进文档与 UI 提示:**轮播是"不同时刻显示不同子项",堆叠是"同一时刻全部显示"**。 ### 5.12 分割线(SeparatorComponent) - 视觉(`SeparatorComponent.axaml:10-14`): ```css .node-separator { width: 2px; display: flex; align-items: center; justify-content: center; height: 100%; } .node-separator .line { width: 2px; height: 25px; background: var(--text-fill-secondary); opacity: 0.8; } ``` - 源码用 `Grid Width="2"` 包一条 `Line`:`StrokeThickness=2`、`StartPoint 0,0` → `EndPoint 0,25`、`Stroke=TextFillColorSecondaryBrush`、`Opacity=0.8`、垂直/水平居中。 - 设置:无(只有通用设置)。 - 边界:位于行首/行尾时仍然占 2px + 两侧各 6px = 14px,不做特殊收边(与 ClassIsland 一致)。 ### 5.13 组件设置 UI(组件设置页 + 高级设置页) 抽屉标题栏与页签见 4.8.6。两页内容: **页 1「组件设置」**:渲染组件自己的 `SettingsType` 控件(`ComponentPresenter` 以 `IsPresentingSettings=true` 渲染,`EditModeView.axaml:287-290`),即本文每个组件的"设置项"清单。 **页 2「高级设置」**:按下面顺序渲染(`repos/ClassIsland/ClassIsland.Core/Controls/ComponentAdvancedSettingsControl.axaml`): 1. 按规则隐藏(开关 + 说明"注意此处的隐藏不会释放组件资源"); 2. 不透明度(Slider 0–1,步进 0.05); 3. 自定义背景颜色(开关 + ColorPicker); 4. 自定义背景不透明度(开关 + Slider); 5. 自定义背景圆角半径(开关 + Slider 0–20,步进 0.5,文案"调至最大数值时左右边缘为半圆"); 6. 字体大小(总开关 `IsResourceOverridingEnabled` + 四个 NumberBox:次级/正文/强调/大号,范围 1–40,`ci:Field` 宽 175、后缀 `px`); 7. 字体颜色(开关 + ColorPicker); 8. 对齐方式(4 个图标按钮:左 `\uE03E`、居中 `\uE03A`、拉伸 `\uE034`、右 `\uE03C`); 9. 固定宽度(开关 + NumberBox); 10. 宽度限制(两个开关 + 两个 NumberBox:最大/最小); 11. 自定义边距(开关 + 4 个 NumberBox:左/上/右/下)。 `ci:Field` 后缀单位固定为 `px`;字号输入框的 `SpinButtonPlacementMode=Inline`、`Minimum=1`、`Maximum=40`(`EditModeView.axaml:418-427` 的等价写法)。 ### 5.14 空数据与异常兜底(逐组件) | 场景 | 必须表现 | |---|---| | 课表为空 / 无今日课表 | 课程表显示 `PlaceholderTextNoClass`(默认"今天没有课程。");不得显示空白胶囊 | | 今日课程全部结束 | 显示 `PlaceholderTextAllClassEnded`(默认"今日课程已全部结束。"),或按设置隐藏(`HideFinishedClass`) | | 天气数据缺失 | 天气组件隐藏温度/图标;只显示可用的预警/降水;不显示假值 | | 天气图标加载失败 | 该预警显示"图裂"占位(红色背景 + 警告图标),不得整块消失 | | 倒计时区间非法(end`(与 ClassHelper 一致,便于主题覆盖与自动化断言)。 必须暴露的 CSS 变量(在 `:root` 与组件节点上均可覆盖)。**规范 token 名以 part 3 的 `--ci-*` 为准**,下表左侧为本文别名,落地时按右侧映射: | 本文别名 | 规范 token | 值 | |---|---|---| | `--font-secondary` | `--ci-fs-secondary` | 14px | | `--font-body` | `--ci-fs-body` | 16px | | `--font-emphasized` | `--ci-fs-emphasized` | 18px | | `--font-large` | `--ci-fs-large` | 20px | | `--text-fill-primary` | `--ci-text-1` | 0.92 白 | | `--text-fill-secondary` | `--ci-text-2` | 0.72 白 | | `--accent` | `--ci-accent` / 填充 `--ci-accent-fill` | 默认 `#0a84ff`(用户强调色 DodgerBlue) | | `--control-fill-default` | `--ci-fill-secondary` | 0.10 白 | | `--control-strong-stroke-disabled` | `--ci-stroke-control` | 0.30 白 | | `--rainy-color` | `--ci-weather-rain` | 降雨色 | | `--aqi-1` … `--aqi-6` | `--ci-aqi-1` … `--ci-aqi-6` | AQI 六级色 | | `--node-gap` | `--ci-component-gap` | 12px | | 进度条高度 | `--ci-progress-height` | **3px**(倒计时进度条、课程进度条共用) | | 高亮条 | `--ci-accent-fill` + opacity 0.4 | — | ```css :root { --font-secondary: 14px; --font-body: 16px; --font-emphasized: 18px; --font-large: 20px; --text-fill-primary: rgba(255,255,255,0.92); --text-fill-secondary: rgba(255,255,255,0.72); --accent: #0a84ff; /* 用户强调色,默认 DodgerBlue/Apple 蓝 */ --control-fill-default: rgba(255,255,255,0.10); --control-strong-stroke-disabled: rgba(255,255,255,0.30); --rainy-color: #4a9eff; --aqi-1: #6fdd8b; --aqi-2: #ffd666; --aqi-3: #ff9f0a; --aqi-4: #ff6961; --aqi-5: #c77dff; --aqi-6: #b23a48; --node-gap: 12px; /* = 两个 6px 外边距之和 */ } ``` 组件节点通用样式: ```css .component-node { display: flex; align-items: center; /* 所有组件在 40px 行内垂直居中 */ min-width: 0; flex-shrink: 0; white-space: nowrap; } .component-node.text, .component-node.schedule { white-space: normal; } ``` ### 5.16 验收标准 1. 首次启动生成 1 行 2 组件(日期 + 课程表),日期显示形如 `周三 11/12`,课程表显示当天课程或占位符。 2. 11 个内置组件全部能从组件库拖入主界面,拖入后立刻可编辑、可删除,删除后无残留占位。 3. 时钟:显示 `HH:mm`;开启闪动后冒号 1 秒亮 1 秒灭;开启秒数后变 `HH:mm:ss` 且冒号常亮;数字等宽,切换时不水平抖动。 4. 倒计时:4 种时间源(固定时间/周期/今天/本周)各配一遍,数值、进度环、进度条、连接词显示正确;`%D/%H/%M/%S/%X/%P/%p/%L/%d/%h/%m/%s/%x` 全部 token 输出与源码规则一致(尤其 `%m/%s/%x` 的补零与向上取整语义)。 5. 倒计时进度环从 12 点方向顺时针填充,满 100% 时不出现缺口;紧凑模式只显示"名称 + 数值"。 6. 课程表:当前课程高亮、已结束课程淡化到 0.6、剩余时间药丸在阈值内出现;`HideFinishedClass` / `ShowCurrentLessonOnlyOnClass` / `FadeCompletedClasses` / `HighlightChangedClass` 各自开关验证;4 种"显示明天课表"模式行为正确。 7. 课程表空状态:无课表显示"今天没有课程。";全部结束显示"今日课程已全部结束。";`ShowPlaceholderOnEmptyClassPlan=false` 时两者都不显示。 8. 天气:6 种主信息逐一切换显示正确;有预警时按"总是图标/默认/总是全文"三种模式渲染;降水药丸在 `RainRemainingMinutes>0` 时出现、`=0` 时消失;模糊模式显示"约 X 分钟"、精确模式显示"Xmin"。 9. 文本:字号 16 与 30 时高度都不超过 40px;自定义颜色生效/关闭后回退;空文本不占宽。 10. 滚动容器:内容超出时按 `SpeedPixelPerSecond` 匀速滚动,`PauseSeconds` 停顿位置等于 `PauseOffsetX`;内容未超出时静止;命中"停止规则"后回到初始位置。 11. 轮播容器:`SlideSeconds` 到点切换;循环/随机/往复三种模式各验证一轮;开启动画时切换有 250ms 上下位移淡入淡出,关闭时无过渡;只有 1 个子项时不轮播。 12. 分组容器的子项横向排列,间距恒定 12px;堆叠容器的子项完全重叠在同一矩形且各自按对齐方式摆放;分割线为 2×25px、透明度 0.8。 13. 所有组件的"高级设置"项生效:固定宽度、最大/最小宽度、对齐、不透明度、自定义边距、字号覆盖、字体颜色覆盖;隐藏规则勾选后组件消失但在编辑模式显示斜条纹占位。 14. 组件 ID 不存在时显示"缺失组件"占位而不是白屏或崩溃。 15. 全部组件在 40px 行高内垂直居中,无任何组件把行撑高超过 40px。