- 重构版主提示词(14 parts,面向 ClassHelper 灵动岛重构) - ClassIsland 全量规格提示词(20 parts,含分卷与逐章版本) - 投喂指南、分卷、逐节文件 - 调研记录、一致性基线、生成与质检脚本
54 KiB
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):
- 可见性三态:
visible = IsVisibleInternal || 主界面处于编辑模式(:13-18)。隐藏的组件在编辑模式仍占位。 - 透明度:宿主
opacity = 组件设置.Opacity(默认1.0,范围 0–1,:34-36)。 - 字号继承:宿主根元素的
font-size = var(--font-body)(16px),即所有组件默认正文字号;组件内部需要别的字号时必须显式引用--font-secondary/emphasized/large(:37-38)。 - 宽度三态(互斥,优先级:固定宽 > 最大宽/最小宽):
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)。
- 水平对齐:宿主内容的
justify-content取Settings.HorizontalAlignment,默认stretch(枚举Stretch/Left/Center/Right,ComponentSettings.cs:36,选择器:49-51)。 - 内边距:宿主内部
Shimmer容器padding: 8px 0(:55-57),但组件自身的横向间距由外层margin: 0 6px提供(行内模板Margin="6 0",MainWindow.axaml:396),两者都要保留,不得合并。 - 加载占位:首次布局或配置切换时显示 Shimmer 占位,占位宽度取上次测量宽度
LastWidthCache(默认100,ComponentSettings.cs:39,ComponentPresenter.axaml:69-70)。占位态不得让行高变化。 - 隐藏规则:
HideOnRule + HidingRules满足时组件不可见但不卸载、不释放资源(ComponentSettings.cs:100-123)。 - 裁剪:宿主自身
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 以驱动闪动。
- 若系统时间不可用(精确时间服务失败)必须回退本地时间,不得显示空白。
- 24 小时制,不显示 AM/PM(格式串
- 验收要点:开启闪动后,肉眼观察冒号 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):- 固定时间:
(StartTime, OverTime)。 - 周期:
total = before + duration + after;cycles = floor((now - cycleStart)/total)(启用次数限制时再取min(cycles, limit));start = cycleStart + cycles*total + before;end = start + duration。total <= 0时退化为(cycleStart, cycleStart)。 - 今天:
NatureTimeUseMode==1→(今天0点, 明天0点);否则取当天时间表中第一个"上课/课间"时间点的开始 → 最后一个"上课/课间"时间点的结束;课程不可用时按NatureTimeUseMode==2退化为(DateTime.MinValue, DateTime.MinValue)(不显示倒计时),否则退化为自然日。 - 本周:按
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)时进度环/进度条不跟随放大,只放大文本。
- 终点在过去 → 数值全 0、
- Vue 结构建议:
CountDownNode.vueprops ={ settings, now },内部computed出remaining、percent、text;进度环用 SVGcircle+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):
TimeType ∈ {2 分割线, 3 行动}不显示【注意:分割线类型由专门的LessonControlSeparator渲染,判定顺序以源码为准】;HideFinishedClass=true且EndTime < now→ 不显示;IsHideDefault=true且不是当前选中项 → 不显示;TimeType==1(课间)且不是当前选中项 → 不显示;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 等级配色:
AqiLevel1–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),ComponentPresenterMargin="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,子项ComponentPresenterMargin="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):
- 按规则隐藏(开关 + 说明"注意此处的隐藏不会释放组件资源");
- 不透明度(Slider 0–1,步进 0.05);
- 自定义背景颜色(开关 + ColorPicker);
- 自定义背景不透明度(开关 + Slider);
- 自定义背景圆角半径(开关 + Slider 0–20,步进 0.5,文案"调至最大数值时左右边缘为半圆");
- 字体大小(总开关
IsResourceOverridingEnabled+ 四个 NumberBox:次级/正文/强调/大号,范围 1–40,ci:Field宽 175、后缀px); - 字体颜色(开关 + ColorPicker);
- 对齐方式(4 个图标按钮:左
\uE03E、居中\uE03A、拉伸\uE034、右\uE03C); - 固定宽度(开关 + NumberBox);
- 宽度限制(两个开关 + 两个 NumberBox:最大/最小);
- 自定义边距(开关 + 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 行 2 组件(日期 + 课程表),日期显示形如
周三 11/12,课程表显示当天课程或占位符。 - 11 个内置组件全部能从组件库拖入主界面,拖入后立刻可编辑、可删除,删除后无残留占位。
- 时钟:显示
HH:mm;开启闪动后冒号 1 秒亮 1 秒灭;开启秒数后变HH:mm:ss且冒号常亮;数字等宽,切换时不水平抖动。 - 倒计时:4 种时间源(固定时间/周期/今天/本周)各配一遍,数值、进度环、进度条、连接词显示正确;
%D/%H/%M/%S/%X/%P/%p/%L/%d/%h/%m/%s/%x全部 token 输出与源码规则一致(尤其%m/%s/%x的补零与向上取整语义)。 - 倒计时进度环从 12 点方向顺时针填充,满 100% 时不出现缺口;紧凑模式只显示"名称 + 数值"。
- 课程表:当前课程高亮、已结束课程淡化到 0.6、剩余时间药丸在阈值内出现;
HideFinishedClass/ShowCurrentLessonOnlyOnClass/FadeCompletedClasses/HighlightChangedClass各自开关验证;4 种"显示明天课表"模式行为正确。 - 课程表空状态:无课表显示"今天没有课程。";全部结束显示"今日课程已全部结束。";
ShowPlaceholderOnEmptyClassPlan=false时两者都不显示。 - 天气:6 种主信息逐一切换显示正确;有预警时按"总是图标/默认/总是全文"三种模式渲染;降水药丸在
RainRemainingMinutes>0时出现、=0时消失;模糊模式显示"约 X 分钟"、精确模式显示"Xmin"。 - 文本:字号 16 与 30 时高度都不超过 40px;自定义颜色生效/关闭后回退;空文本不占宽。
- 滚动容器:内容超出时按
SpeedPixelPerSecond匀速滚动,PauseSeconds停顿位置等于PauseOffsetX;内容未超出时静止;命中"停止规则"后回到初始位置。 - 轮播容器:
SlideSeconds到点切换;循环/随机/往复三种模式各验证一轮;开启动画时切换有 250ms 上下位移淡入淡出,关闭时无过渡;只有 1 个子项时不轮播。 - 分组容器的子项横向排列,间距恒定 12px;堆叠容器的子项完全重叠在同一矩形且各自按对齐方式摆放;分割线为 2×25px、透明度 0.8。
- 所有组件的"高级设置"项生效:固定宽度、最大/最小宽度、对齐、不透明度、自定义边距、字号覆盖、字体颜色覆盖;隐藏规则勾选后组件消失但在编辑模式显示斜条纹占位。
- 组件 ID 不存在时显示"缺失组件"占位而不是白屏或崩溃。
- 全部组件在 40px 行高内垂直居中,无任何组件把行撑高超过 40px。