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

54 KiB
Raw Blame History

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)。
<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):

<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 外层结构

<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):

<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):
.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):
.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):
.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 实现:
.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):
.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 —
: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 外边距之和 */
}

组件节点通用样式:

.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。