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