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

752 lines
54 KiB
Markdown
Raw Blame History

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