- 重构版主提示词(14 parts,面向 ClassHelper 灵动岛重构) - ClassIsland 全量规格提示词(20 parts,含分卷与逐章版本) - 投喂指南、分卷、逐节文件 - 调研记录、一致性基线、生成与质检脚本
474 lines
41 KiB
Markdown
474 lines
41 KiB
Markdown
## 8. 消息、提醒、课程提醒、强提醒特效、天气与课件覆盖(ClassHelper 落地规格)
|
||
|
||
本节是「消息与课程」这一部分在 **ClassHelper 现有仓库内** 的落地规格。前提口径(与 parts-r/01、parts-r/02 保持一致):
|
||
|
||
- 默认风格 `appearance.family = 'classisland'`(对应配置键 `islandAppearance.style`,落盘字段以 parts-r/05 为准):`fixed` **不再是胶囊+详情卡**,而是 **ClassIsland 风格信息行**——默认 1 行 = 日期 + 课程表,行高 40、圆角 8、背景不透明度 0.5、底部 3px 进度条;消息到来时**在同一行内**展开 ClassIsland 风格气泡。原「胶囊 + 详情卡」只在 `family='winisland'` 兼容形态保留(老用户升级默认 `winisland`,新安装默认 `classisland`)。
|
||
- `custom` 常驻:保留现有 `schemes/rows/nodes/expandedRows` 数据模型与编辑器,渲染与交互按 ClassIsland 规格重构;消息入口仍是铃铛 + 共用消息面板。
|
||
- 数据不迁移:通知/作业/叫人/课表/课件/天气全部沿用 ClassHelper 现有服务端与本地链路(`IslandNotification`、`ScheduleDto`、`IslandTimePointDto`、`CoursewareTransfer`、`IslandWeather`)。
|
||
- 裁决优先级:**ClassHelper 业务语义优先,ClassIsland 视觉表现优先**;逐项裁决见 8.12。
|
||
|
||
### 8.1 数据契约(复用为主,最小扩展)
|
||
|
||
#### 8.1.1 现有类型必须原样复用
|
||
|
||
| 类型 | 位置 | 用途 |
|
||
|---|---|---|
|
||
| `NotificationPriority = 'LOW'\|'NORMAL'\|'HIGH'\|'URGENT'` | `packages/shared/src/types.ts:30` | 业务优先级(四档) |
|
||
| `PRIORITY_RANK` | `packages/shared/src/constants.ts:203-208` | 排序与合并共用的权重(LOW 0 / NORMAL 1 / HIGH 2 / URGENT 3) |
|
||
| `IslandNotification` | `packages/shared/src/types.ts:1131-1143` | 岛消息载荷:`id/title/content/priority/createdAt/courseName/teacherName/kind/subtitle` |
|
||
| `IslandNotificationKind = 'notification'\|'homework'\|'call'` | `packages/shared/src/types.ts:1111` | 消息类型 |
|
||
| `IslandState` | `packages/shared/src/types.ts:1198-1230` | `coursewareTransfer/mode/lessonReminder/active/queued/inClass/currentPeriodEnd/reason/listExpanded/maxCardHeight/updatedAt` |
|
||
| `IslandLessonReminder` | `packages/shared/src/types.ts:1114-1128` | `kind: prepare/on-class/break/ending/in-class` 等字段 |
|
||
| `LessonReminderSettings` | `packages/shared/src/island-layout.ts:141-158` | 五类提醒开关 + `prepareSeconds/endingSeconds` |
|
||
| `ScheduleDto` | `packages/shared/src/types.ts:545-564` | `dayOfWeek`(1=周一…7=周日)/`startTime`/`endTime`/`weekParity`/`course` |
|
||
| `IslandTimePointDto` | `packages/shared/src/class-plan.ts:76-87` | `timeType/name/courseName/startTime/endTime/isHideDefault` |
|
||
| `CoursewareTransfer` | `packages/shared/src/courseware.ts:82-94` | `stage/receivedBytes/totalBytes/progress/error` |
|
||
| `IslandWeather` | `packages/shared/src/island-layout.ts:38-44` | `temperature/code/windSpeed/fetchedAt/stale` |
|
||
|
||
#### 8.1.2 必须新增/扩展的字段(只加不改,保证迁移可回滚)
|
||
|
||
1. `IslandState` 新增:
|
||
- `family: 'classisland' | 'winisland'`:由外观下发给渲染进程,决定渲染形态;
|
||
- `pendingStaged: number`:上课期间被暂存的消息数(用于行内角标/铃铛计数);
|
||
- `bubblePhase: 'hidden' | 'mask' | 'overlay' | 'closing'`:气泡阶段(仅 `fixed` 使用,见 8.4);
|
||
- `bubbleDeadline: number`:当前气泡阶段结束时间戳(毫秒),供渲染进程做进度条与自动收起(避免渲染进程自己计时导致主/渲染漂移)。
|
||
2. `IslandNotification` 新增可选 `level?: 'emergency'|'high'|'normal'|'low'`:由 `priority` 映射而来(URGENT→emergency、HIGH→high、NORMAL→normal、LOW→low),用于 ClassIsland 三级排序键的中间键;**不改变服务端 DTO**,只在主进程派生。
|
||
3. `IslandLessonReminder` 新增 `progress: number`(0–1,课程进度)与 `leftSeconds: number`(整数秒,供徽标直接渲染),由主进程计算下发,渲染进程不得自行 `Date.now()` 推算(防止跨进程时钟漂移与系统时间跳变)。
|
||
4. `islandLayout` 新增 `version` 与 `family`(见 parts-r/05);旧 `residentEnabled` 保留为兼容字段。
|
||
|
||
#### 8.1.3 通知 DTO → 岛载荷的映射(单一入口)
|
||
|
||
必须在主进程的推送链路(现有 `pushNotification` 的调用方)集中实现,禁止各处自行拼装:
|
||
|
||
```
|
||
toIslandNotification(dto: NotificationDto, kindHint?): IslandNotification {
|
||
id: dto.id,
|
||
title: dto.title,
|
||
content: dto.content,
|
||
priority: dto.priority,
|
||
level: MAP[dto.priority],
|
||
createdAt: dto.createdAt,
|
||
kind: kindHint ?? 'notification', // 作业=homework;叫人=call(保留现有双播合并)
|
||
courseName: dto.course?.name ?? null,
|
||
teacherName: dto.teacher?.name ?? null,
|
||
subtitle: dto.subtitle ?? null
|
||
}
|
||
```
|
||
|
||
- 作业消息 `kind='homework'`,`id` 为本地合成的 `homework-<id>`(`packages/desktop-client/src/main/island.ts:1061-1065`),**不得**拿它去调「标为已读」接口。
|
||
- 叫人消息 `kind='call'`,标题由服务端 `buildCallTitle` 生成(`packages/shared/src/constants.ts:571-575`:`请 XXX 同学找 XXX 老师`)。
|
||
|
||
### 8.2 去重、合并与排序(ClassHelper 业务规则,必须保留)
|
||
|
||
1. **按 id 去重**:同一 id 只保留一条,`pendingNotifications()` 以 Map 去重(`packages/desktop-client/src/main/island.ts:738-749`;渲染侧同口径 `src/island/IslandApp.vue:86-97`)。
|
||
2. **通知/叫人双播合并**:`createCall` 同时发 `notification:new` 与 `call:new`,同 id 两条到达时必须**合并而非覆盖**:`kind` 取更特殊的(call 优先),`priority` 取更高(`PRIORITY_RANK` 比较)(`island.ts:713-724`)。这是硬性要求,否则会出现「叫人胶囊显示成新消息、上课被当普通通知隐藏」。
|
||
3. **排序口径**:`PRIORITY_RANK` 降序 → `createdAt` 降序(新的在前)(`island.ts:744-748`)。**不得**改用 ClassIsland 的三级键打乱业务优先级——ClassIsland 的「提供方优先级」在 ClassHelper 中没有对应概念,只有服务端单通道消息,因此**本重构的排序键固定为(业务优先级 → 时间)**;若要引入 ClassIsland 的「通知等级」作为第二键,仅在与时间同序时作为稳定排序的次级键,不得跨优先级反转。
|
||
4. **未读与已读**:未读数以服务端为准(`GET /notifications/unread-count`,`docs/app/notifications.md:45-47`);岛内「标为已读 / 全部已读」必须调用既有 `markRead/markAllRead` 处理器,成功后通知中心列表同步(`island.ts:1044-1070`;`packages/desktop-client/src/renderer/stores/notifications.ts:102-122`)。
|
||
5. **实时撤回**:收到 `notification:deleted` 后必须从岛队列与当前展示中移除(`island.ts:1210-1216`),缓存与通知列表同步清理(`docs/app/notifications.md:34`)。
|
||
6. **本机作业指纹**:本机刚录的作业通过指纹避免重复提醒自己——这是既有规则,必须保留(`docs/app/island.md:25`);实现位置在推送入口,不在岛渲染层。
|
||
|
||
### 8.3 队列与展示状态机(三形态对照 + 伪代码)
|
||
|
||
`IslandState` 的状态字段固定为:`mode(hidden|pill|expanded)`、`active`、`queued`、`inClass`、`currentPeriodEnd`、`reason(new|after-class|urgent|call|closing)`、`listExpanded`、`maxCardHeight` + 新增 `bubblePhase/pendingStaged`。
|
||
|
||
#### 8.3.1 三种形态的展示规则对照
|
||
|
||
| 场景 | `fixed` + `classisland`(默认) | `fixed` + `winisland`(兼容) | `custom` |
|
||
|---|---|---|---|
|
||
| 空闲 | 信息行常驻(日期+课程表),无消息时不显示气泡 | 隐藏或细缝(现有规则) | 常驻组件行 |
|
||
| 课间普通通知/作业 | 行内气泡自动展开(遮罩→正文),可点开应用 | 胶囊,点击详情 | 铃铛 + 未处理数量,点击面板 |
|
||
| 课堂普通通知/作业 | **暂存**:不展开气泡,行内显示「N 条暂存」角标(muted),下课补弹 | 暂存,下课补弹(现有) | 铃铛提示,允许主动查看 |
|
||
| 普通叫人(课间) | 行内气泡自动展开(kind=call 样式) | 胶囊(可点) | 铃铛 |
|
||
| 普通叫人(课堂) | **保持可见的「叫人」chip,不自动展开**;点击后展开气泡 | 保留可点胶囊,不自动展开 | 铃铛,主动查看 |
|
||
| 紧急通知/紧急叫人 | **立即展开气泡**(无视上课),并触发强提醒特效 | 立即展开 | 铃铛 + 提示,用户主动展开(可自动提示) |
|
||
| 处理完成 | 气泡收回,信息行恢复 | 队列清空后隐藏/细缝 | 回常态行 |
|
||
| 课件传输 | 覆盖整岛内容(信息行也覆盖),见 8.10 | 同 | 同 |
|
||
|
||
#### 8.3.2 `pushNotification` 分支(必须逐条实现,以现有实现为基底)
|
||
|
||
现实现(`packages/desktop-client/src/main/island.ts:794-867`)分支顺序不得打乱,只在「展示方式」处按形态分流:
|
||
|
||
```
|
||
push(raw, ctx):
|
||
state.listExpanded = false
|
||
n = mergeNotification(findKnown(raw.id), raw)
|
||
inClass = ctx.inClass ?? state.inClass; state.inClass = inClass
|
||
if (ctx.currentPeriodEnd !== undefined) state.currentPeriodEnd = ...
|
||
if (ctx.preview) → 预览分支(30s 自收,失焦不收起)
|
||
if (isImmediate(n)): // priority === 'URGENT'(含紧急叫人)
|
||
正在展示的其它消息回队列
|
||
activate(n, n.kind==='call' ? 'call' : 'urgent', 'expanded')
|
||
→ classisland: bubblePhase='mask',并触发出特效
|
||
else if (inClass && n.kind==='call'):
|
||
activate(n, 'call', 'pill') // classisland: 只显示 chip,不展开气泡
|
||
else if (inClass):
|
||
queued = [..., n]; mode='hidden'(classisland: 保持行可见,pendingStaged++)
|
||
else:
|
||
上一条 active 回队列;queued 追加 n;activate(n, 'new', 'pill')
|
||
→ classisland: bubblePhase='mask',行内自动展开气泡
|
||
```
|
||
|
||
#### 8.3.3 消息动作表(渲染 → 主进程,`island.ts:929-1077`)
|
||
|
||
| 动作 | 语义 | 备注 |
|
||
|---|---|---|
|
||
| `expand` | 展开当前/最新消息 | `classisland`:进入气泡;`winisland`:展开详情卡;`custom`:打开消息面板 |
|
||
| `collapse` | 收起 | 上课时普通通知直接隐藏;紧急/叫人回缩为 chip/胶囊 |
|
||
| `expand-list` | 多条时「展开更多」 | 由 `islandListLayout` 决定行数与提示(见 8.6.3) |
|
||
| `dismiss` | 单条「知道了」 | 不改已读 |
|
||
| `dismiss-all` | 整批「知道了」 | 不改已读 |
|
||
| `mark-read` | 标为已读 | 调后端,失败要回滚 UI 状态 |
|
||
| `mark-all-read` | 整批已读 | 过滤掉 `kind==='homework'` 的本地 id |
|
||
| `open-app` | 打开应用内通知页 | 列表行点击也走这里 |
|
||
|
||
#### 8.3.4 自动收起超时(必须原值保留,`island.ts:124-132`)
|
||
|
||
| 形态 | 超时 |
|
||
|---|---|
|
||
| `pill`(胶囊/chip) | 15 000ms |
|
||
| 课后补发展开 | 14 000ms |
|
||
| 课后补发胶囊 | 25 000ms |
|
||
| 展开(普通) | 20 000ms |
|
||
| 紧急 | 45 000ms |
|
||
| 叫人 | 90 000ms |
|
||
| 收回动画 `CLOSE_ANIMATION_MS` | 360ms(随 `speed` 缩放,`island.ts:121`) |
|
||
|
||
**收起后的关键规则**(`island.ts:1266-1296`):只要还有未处理消息(`active` 或 `queued` 非空),收起后必须保持 chip/胶囊/行内角标,不得直接消失;彻底隐藏只在「知道了 / 已读 / 队列清空 / 进入上课」时发生。
|
||
|
||
### 8.4 ClassIsland 风格气泡(`fixed` 默认形态,两阶段)
|
||
|
||
#### 8.4.1 阶段机(必须由主进程状态驱动,渲染进程只渲染)
|
||
|
||
```
|
||
bubblePhase: hidden → mask →(有正文)overlay → closing → hidden
|
||
└(无正文/正文时长为 0)→ closing → hidden
|
||
```
|
||
|
||
- 进入 `mask`:行仍为 40 高,气泡在同一行内展开;遮罩背景为强调色,斜切遮罩扫入,遮罩文字(标题/徽标)延迟入场。
|
||
- `mask` 时长:默认 3 000ms;若该消息带正文且正文有明确 `EndTime`/时长,则按正文时长决定(不得把正文时长叠加到遮罩上导致总时长翻倍)。
|
||
- 进入 `overlay`:遮罩退出、正文淡入;底部显示**剩余时间进度条**(见 8.4.4)。
|
||
- `closing`:正文/遮罩退出(200ms),随后窗口/行恢复;`reason='closing'` 时行内不得显示任何「新消息」文案(防闪烁,沿用现有 `isClosing` 语义 `src/island/IslandApp.vue:73-80`)。
|
||
- 任一阶段都可被新消息打断:紧急消息直接覆盖当前气泡(当前消息回队列),普通消息按队列顺序等待。
|
||
|
||
#### 8.4.2 行内气泡几何(像素)
|
||
|
||
- 行高固定 **40**;行圆角 **8**(`--ci-radius`);行背景不透明度 **0.5**(只作用背景层)。
|
||
- 气泡内容内边距:水平 **12**(ClassIsland 行内容盒 `Margin="12 0"`);遮罩文字层内边距 **12 0**;遮罩文字图标 20×20、间距 8。
|
||
- 气泡最大高度 = 行高 40(正文单行/双行截断,超出省略);多条列表形态时高度由 `islandListLayout` 计算(见 8.6.3)。
|
||
- 气泡底部进度条:高 **3px**、贴底、圆角 1.5、`transform-origin:left`、`scaleX(progress)`、值过渡 **50ms linear**(ClassIsland 3px 规格;现有课程提醒条是 2px,两者不得混用,见 8.8)。
|
||
- 遮罩背景:强调色实底(`--island-accent`,默认 `#0a84ff`);文字色 `#fff`,标题 16px/600,副文本 12px/0.72 透明度。
|
||
|
||
#### 8.4.3 气泡动画(ClassIsland 参数 + ClassHelper 弹簧的裁决)
|
||
|
||
**裁决**:岛的**整体尺寸/圆角**继续用 ClassHelper 的 `SpringValue`(stiffness=0.10、damping=0.68、换向保留 35% 动量、单帧位移 ≤ 剩余 20%、snap 0.05px,`packages/desktop-client/src/island/spring.ts:26-114`);**气泡内部的遮罩扫入/文字/进度**用 ClassIsland 的 CSS/WAAPI 参数。两条动画路径不得互相接管。
|
||
|
||
| 动画 | 参数 | 实现 |
|
||
|---|---|---|
|
||
| 斜切遮罩扫入 | 5 区段,权重 `[0,0.1,0.2,0.4,0.2,0.1]`,每段 **360ms**,错峰 **120ms**(区 0/4 同时、1/3 延迟 90ms、2 延迟 120ms),缓动 **QuarticEaseOut** | SVG `polygon` 5 片,或 `clip-path` 5 段;`--ci-speed` 缩放 |
|
||
| 斜切遮罩退出 | 反向:中间先收,1/3 延迟 90ms,0/4 延迟 120ms,缓动 **QuadraticEaseIn**,每段 360ms | 同上,反向 |
|
||
| 遮罩文字淡入 | `delay 260ms`、`250ms`、`opacity 0→1`、`cubic-bezier(0.25,1,0.5,1)` | CSS animation `both` |
|
||
| 遮罩文字缩放 | `delay 260ms`、`750ms`、`scale 1.1→1.0`、同曲线 | CSS transform |
|
||
| 遮罩退出文字 | `200ms`、`opacity 1→0` + `scale 1.0→1.1`、`cubic-bezier(0.5,0,0.75,0)` | CSS |
|
||
| 正文淡入淡出 | `200ms`、`SineEaseIn` | CSS transition |
|
||
| 进度条值 | `50ms linear` | `transform: scaleX` |
|
||
| 行宽/行高变化 | 由弹簧决定(约 300–500ms 自然落定) | `SpringValue.step()` |
|
||
|
||
**速度与降级**:`appearance.speed`(0.5–2)必须同比缩放上述所有 `ms`:`duration = base / speed`,延迟同理;`animations=false` 时全部时长置 0(直接终态),但**不得**改变最终几何。系统「减少动态效果」时按 `speed=2` 且时长压到 1ms 处理。
|
||
|
||
#### 8.4.4 气泡内容(信息层次)
|
||
|
||
- 遮罩阶段:类型图标(通知/作业/叫人 三套 SVG,沿用 `src/island/ClassicNotifications.vue:115-132`)+ 类型徽标(`紧急/重要/普通/低` 或 `叫人/新作业`)+ 一行标题;紧急加「需立即查看」、叫人加「老师正在等你」、课后补发加「下课后补发」(同一套 `live-dot` 语义)。
|
||
- 正文阶段:标题(单行省略)+ 正文(最多 2 行省略)+ 元信息(`教师名 · 时间`)+ 可选 `subtitle`(叫人提示语,如「请到办公室找我」)+ 按钮行(`打开应用` / `标为已读` / `知道了`;叫人时为 `收到`)。
|
||
- 多条列表(`listMode`):标题 `"N 条待处理通知"`,每行「类型图标 + 标题 + `教师名 · 时间` + 类型徽标」,默认显示前 **3** 行,超出提示「展开更多(还有 N 条)」或「更多请前往应用内操作」(`packages/shared/src/constants.ts:379-402,445-474`)。
|
||
|
||
#### 8.4.5 `ClassicNotifications.vue` 的改造与 `winisland` 兼容形态
|
||
|
||
`packages/desktop-client/src/island/ClassicNotifications.vue` 不得整体废弃,必须**拆成「气泡内容」组件**并同时服务两种形态:
|
||
|
||
1. **两阶段复用**:`fixed` + `classisland` 下,`mask` 阶段只渲染它的 `header` 部分(类型图标 + 类型徽标 + 标题 + 紧急/叫人提示),`overlay` 阶段再渲染 `title / body / footer`(标题、正文、元信息、按钮行);列表形态(多条)只在 `overlay` 阶段出现。原组件的 `size/listLayout/pendingNotifications` 计算逻辑保留,改为由父组件传入。
|
||
2. **`winisland` 兼容**:整卡(header + title + body + footer 一次性渲染)继续用于详情卡,**交互与尺寸不变**;只把文案/徽标/按钮语义与新气泡对齐(`叫人` 时按钮「收到」等)。
|
||
3. **不得复制两份实现**:图标、徽标、行布局、按钮行必须共用同一组件与同一份 `classic-island.css`,避免两种形态文案不一致。
|
||
|
||
### 8.5 强提醒全屏特效(Topmost)
|
||
|
||
#### 8.5.1 行为
|
||
|
||
1. **触发条件**(全部满足):消息 `priority==='URGENT'`(紧急通知或紧急叫人);`appearance.animations===true` 且 `appearance.effectEnabled`(新增外观开关,默认 `true`);岛当前可见(不是编辑模式、不是预览、不是隐藏);本节特效与气泡可同时出现。
|
||
2. **表现**:以岛中心为原点,强调色涟漪扩散到覆盖主界面所在屏幕;扩散 **600ms**、同步淡出 **600ms**,**750ms** 后移除元素并隐藏特效窗口。
|
||
3. **独立窗口**:新建 `topmost-effect` 渲染入口与独立 BrowserWindow(`transparent:true`、`frame:false`、`skipTaskbar:true`、`alwaysOnTop:true`、`showInactive`、`setIgnoreMouseEvents(true)`、逐屏一个窗口);**禁止** `setBackgroundMaterial()`;窗口内元素清空即隐藏,不销毁。
|
||
4. **缩放与多屏**:`effectScale`(默认 1.0,复用现有 `ISLAND_SHADOW_PAD` 之外的独立字段)缩放窗口与圆心;屏幕/DPI 变化重算;4K@150% 下涟漪必须覆盖整屏无接缝。
|
||
5. **降级**:`animations=false`、系统减少动态、编辑模式、窗口重建中、系统休眠恢复后的 1s 内 → 不播特效(只播气泡),不得阻塞消息展示。
|
||
6. **非目标**:本轮**不做**音效与 TTS(ClassHelper 无音频通道);`docs/app/notifications.md:23` 的「约 45 秒收起」仍由队列超时实现,与特效无关。
|
||
|
||
#### 8.5.2 像素/动画参数
|
||
|
||
| 项 | 值 |
|
||
|---|---|
|
||
| 涟漪元素 | 全屏 SVG/CSS 圆(`border-radius:50%` 或 SVG circle),`pointer-events:none` |
|
||
| 扩散 | `scale 0→1`(半径 = 圆心到屏幕最远角),600ms,`QuadraticEaseIn`(≈`cubic-bezier(0.11,0,0.5,0)`) |
|
||
| 淡出 | `opacity 1→0`,600ms,`SineEaseIn`(≈`cubic-bezier(0.12,0,0.39,0)`) |
|
||
| 颜色 | `--island-accent`,默认 `#0a84ff`,透明度按 1→0 |
|
||
| 生命周期 | `setTimeout(750 / speed)` 后移除 |
|
||
| 窗口 | 覆盖 `display.workArea`;`scale` 后宽高乘系数 |
|
||
|
||
### 8.6 课程提醒五类(prepare / on-class / break / ending / progress)
|
||
|
||
> 命名对照:任务/文档口中的 `progress`(课中进度)在代码里对应 `IslandLessonReminder.kind === 'in-class'`,开关为 `classProgressEnabled`;其余四类同名。本节一律用代码枚举值,并在需要时注明「progress」。
|
||
|
||
#### 8.6.1 设置与默认值
|
||
|
||
`islandLayout.lessonReminders`(`packages/shared/src/island-layout.ts:141-158,390-398`):
|
||
|
||
| 字段 | 默认 | 范围 |
|
||
|---|---|---|
|
||
| `prepareEnabled` | `true` | — |
|
||
| `onClassEnabled` | `true` | — |
|
||
| `breakEnabled` | `true` | — |
|
||
| `endingEnabled` | `true` | — |
|
||
| `classProgressEnabled` | `true` | — |
|
||
| `prepareSeconds` | `60` | 5–1800 |
|
||
| `endingSeconds` | `60` | 5–600 |
|
||
|
||
#### 8.6.2 判定算法(`LessonReminderTracker.update`,必须逐行实现)
|
||
|
||
输入:`schedules`(当天过滤后按 `startTime` 升序)、`now`、`settings`。输出 `IslandLessonReminder | null`。证据:`packages/desktop-client/src/main/lesson-reminder.ts:17-109`。
|
||
|
||
1. 当天课表 = `dayOfWeek === (now.getDay() || 7)` 的条目,按开始时间升序(周日=7)。
|
||
2. `current` = 满足 `start ≤ now < end` 的条目;`next` = 第一个 `start > now`;`ended` = `end ≤ now` 且距 `end` < **12 000ms** 的最近一条。
|
||
3. **kind 判定顺序(自上而下,命中即止)**:
|
||
1. `current && now - current.start < 5 000 && onClassEnabled` → `on-class`;
|
||
2. `breakEnabled && !current && ended && now - ended.end < (next ? 12 000 : 5 000) && (now - ended.end < 5 000 || this.current?.id === keyOf(ended,'break'))` → `break`;
|
||
3. `current && endingEnabled && current.end - now ≤ endingSeconds*1000` → `ending`;
|
||
4. `!current && next && prepareEnabled && next.start - now ≤ prepareSeconds*1000` → `prepare`;
|
||
5. `current && classProgressEnabled` → `in-class`。
|
||
4. `maskUntil / endsAt / lessonStartsAt / lessonEndsAt` 必须按源码公式:
|
||
|
||
| kind | maskUntil | endsAt |
|
||
|---|---|---|
|
||
| ending / in-class | `entered`(无遮罩) | `end` |
|
||
| prepare | `min(start, entered + 3000)` | `start` |
|
||
| break 且有下一节 | `entered + 2000` | `end + 12000` |
|
||
| break 无下一节 | `entered + 5000` | `end + 5000` |
|
||
| on-class | `entered + 5000` | `start + 5000` |
|
||
|
||
5. `id = "${dayKey}:${scheduleId}:${start}:${end}:${kind}"`;`entered` 为同一 id 的连续保持时刻(避免同一提醒重复计时)。
|
||
6. 课程名 `course?.name ?? '课程'`;时间字符串截到 `HH:mm`(导入数据可能带秒,`lesson-reminder.ts:78-79`)。
|
||
7. **渲染进程不得自行推导 kind**,只消费主进程下发的 `lessonReminder`(以及新增的 `progress/leftSeconds`)。
|
||
|
||
#### 8.6.3 课程提醒条 UI(像素)
|
||
|
||
| 项 | 值 | 依据 |
|
||
|---|---|---|
|
||
| 容器 | 高 **40**、宽 `max-content`、`min-width 300`、`max-width 800`、圆角 **8**、背景 `#20202080` | `LessonReminderContent.vue:196-208` |
|
||
| 信息层 | 高 40、居中、`gap 12`、`padding 0 14`、`white-space:nowrap` | `:209-216` |
|
||
| 主字号 | 16(课程名 20、时间 14、遮罩标题 18) | `:204,235-245,262` |
|
||
| 进度条 | 高 **2px**、贴底、强调色、`scaleX(progress)`、`transition 50ms linear` | `:307-316` |
|
||
| 遮罩 | 5 片斜切 SVG、`tan(30°)*40` 偏移、区域边界 `[0,0.1,0.3,0.7,0.9,1]`、每片 360ms | `:62-72` |
|
||
| 遮罩时序 | 开:`delays [0,90,120,90,0]`;关:`[120,90,0,90,120]`;`eased = p^4`(开)/ `p²`(关) | `:91-99` |
|
||
| 兜底 | rAF 停摆时 `600/speed` ms 后强制到位 | `:83-89` |
|
||
| 文字动画 | 淡入 250ms/延迟 260ms、缩放 750ms/延迟 260ms,曲线 `(0.25,1,0.5,1)`;退出 200ms、`(0.5,0,0.75,0)` | `:126-138,273-306` |
|
||
|
||
**文案规则**(`LessonReminderContent.vue:14-23,143-169`):
|
||
|
||
| kind | 左侧 | 课程侧 |
|
||
|---|---|---|
|
||
| prepare | `距上课还剩 N 秒` | `下节课是:课程名` + 时间 |
|
||
| on-class | `开始上课` | `本节课:课程名` + 时间 |
|
||
| break | `课间休息` | `下节课是:下一节课程名` + 下一节时间 |
|
||
| ending | (无左侧倒计时文字,只显示课程侧) | `本节课:课程名` + 时间 + **右侧蓝色秒数徽标**(见 8.7) |
|
||
| in-class | `距下课还剩 N 秒` | `本节课:课程名` + 时间(无徽标) |
|
||
|
||
- 无课程时课程名显示 `"今日课程已结束"`。
|
||
- `leftSeconds = ceil((目标时间 - now)/1000)`,下限 0;目标时间:ending/in-class 用 `lessonEndsAt`,其余用 `lessonStartsAt`。
|
||
- `progress`:ending/in-class 用「已上课时间 / 总课时」,其余用「剩余时长 / 本提醒总时长」,钳制 0–1。
|
||
|
||
#### 8.6.4 三形态差异(必须实现)
|
||
|
||
| 形态 | 行为 |
|
||
|---|---|
|
||
| `fixed` + `classisland` | 课程提醒以**行内条**接管岛内容:信息行临时替换为提醒条(宽 = 行宽,高 40);`ending` 用徽标而非遮罩;`in-class`(进度)默认**不使用**提醒条(由课程表组件常驻显示进度),仅当用户开启 `classProgressEnabled` 且组件不可用时回退为提醒条【推断】 |
|
||
| `fixed` + `winisland` | 保留现有实现:`customLayout=false` 时 `ending` 不接管(`island.ts:173-185` 的 `lessonShowing` 已排除 custom 的 ending),`fixed` 的 `ending` 仍以提醒条显示并使用徽标(`docs/app/island.md:70`) |
|
||
| `custom` | `customLayout=true` 时:`kind==='in-class'` 不接管岛(由 `schedule` 组件常驻显示进度);`kind==='ending'` **不接管岛内容**,只在当前课程名右侧显示徽标(`island.ts:198-199`;`docs/app/island.md:65-68`) |
|
||
|
||
**必须遵守的三条硬规则**:
|
||
|
||
1. **没有「即将下课」遮罩**:`ending` 绝不显示 ClassIsland 式全行遮罩,只显示课程名右侧的蓝色秒数徽标(`docs/app/island.md:68`)。
|
||
2. **不替换自定义岛常态组件**:`custom` 模式下课程提醒(除 prepare/on-class/break 的临时接管)不得替换常态组件行。
|
||
3. **关闭开关即不显示**:`endingEnabled=false` 时不显示徽标;`endingSeconds` 只影响徽标出现时机,不影响其他提醒。
|
||
|
||
### 8.7 下课前蓝色秒数徽标(重点规格)
|
||
|
||
#### 8.7.1 视觉
|
||
|
||
| 项 | 值 |
|
||
|---|---|
|
||
| 位置 | 课程名 `<strong>` 右侧,与课程名同一行(`inline-flex`,`flex:0 0 auto`) |
|
||
| 形态 | **正圆 30×30**,`border-radius:50%` |
|
||
| 背景 | 蓝色 `#0a84ff`(与默认强调色同值;可随 `--island-accent` 同步【推断:现实现写死 #0a84ff】) |
|
||
| 文字 | `-N`,白色 `#fff`,**12px / 600**,`font-variant-numeric: tabular-nums`,`line-height:1`,水平垂直居中 |
|
||
| 无障碍 | `aria-label="距下课N秒"` |
|
||
| 证据 | `packages/desktop-client/src/island/lesson-ending.css:1-16`;`LessonReminderContent.vue:160-167` |
|
||
|
||
#### 8.7.2 行为
|
||
|
||
1. **显示条件**(全部满足):`kind==='ending'`(即 `current && endingEnabled && end - now ≤ endingSeconds*1000`);`leftSeconds ≥ 0`;课表数据存在 `endTime`。
|
||
2. **数值**:`leftSeconds = ceil((lessonEndsAt - now)/1000)`;显示为 `-${leftSeconds}`。
|
||
3. **剩余一秒显示 `-1`**:当真实剩余时间在 `(0, 1000]ms` 时 `ceil` 结果为 1,徽标显示 `-1`;**不得**显示 `-0`,也不得在剩余 0 时继续显示(到点后 `ending` 条件不再成立,徽标消失)。这是用户明确要求的行为,必须有单测用例(`now = end - 1ms` → `-1`)。
|
||
4. **阈值与开关**:`endingSeconds` 默认 60(5–600);`endingEnabled=false` 时完全不出现徽标;`animations=false` 也不影响徽标(徽标不是动画)。
|
||
5. **出现/消失动画**:徽标可淡入 100ms(`opacity 0→1`)并随课程提醒条一起退出;**不得**用缩放弹跳(避免课堂投影上的视觉干扰)。
|
||
6. **两种岛都要用**:`fixed`(两种 family)与 `custom` 都必须显示该徽标;`custom` 下徽标挂在 `schedule` 组件的当前课程名右侧,由组件内部渲染同一 `lesson-ending.css`,不得复制第二套样式。
|
||
7. **边界**:
|
||
- 无时间点数据、按 `ScheduleDto` 回退显示课程时,`leftSeconds` 同样由 `endTime` 计算,徽标照常显示;
|
||
- 跨天(课程结束时间 ≤ 开始时间)视为数据异常,不显示徽标并记一次告警;
|
||
- 系统时间跳变:以主进程下发的 `leftSeconds` 为准,渲染进程不得自行用 `Date.now()` 覆盖;
|
||
- 连续多节连堂:每节课结束时徽标各自独立计算,切换瞬间不得出现「-60 → -1」跳变(切换时应先隐藏上一节徽标再显示新一节)。
|
||
|
||
### 8.8 课程进度条(两处,不得混用)
|
||
|
||
| 位置 | 高度 | 颜色 | 过渡 | 取值 |
|
||
|---|---|---|---|---|
|
||
| 课程提醒条底部(ClassHelper 既有) | **2px** | `--island-accent` | `transform 50ms linear` | 见 8.6.3 的 progress 公式 |
|
||
| 课程表组件内「当前课程」进度(ClassIsland 规格) | **3px** | 强调色 | `transform 50ms linear` | 已上/总课时,钳 0–1 |
|
||
|
||
**裁决**:两者同时出现时(`fixed` + `classisland` 信息行同时含课程表与提醒条)**保留两条**——提醒条 2px 贴岛底,组件内进度 3px 贴课程项底;不得合并成一条,也不得把提醒条改成 3px(会与 ClassIsland 「行内进度条 3px」混淆)。渲染必须用 `scaleX` + `transform-origin:left`,不得对宽度做过渡。
|
||
|
||
### 8.9 天气简报(Open-Meteo,保留 ClassHelper 语义)
|
||
|
||
#### 8.9.1 数据链路(不得改)
|
||
|
||
渲染组件 `WeatherBrief.vue` → `window.island.getIslandWeather({latitude, longitude})`(preload 白名单)→ IPC `island:weather`(`packages/desktop-client/src/main/ipc.ts:23`)→ 主进程 `getIslandWeather`(`packages/desktop-client/src/main/island-weather.ts`)→ Open-Meteo。
|
||
|
||
#### 8.9.2 服务行为(必须原样保留)
|
||
|
||
1. 固定服务与参数,**不接受渲染层传 URL**:`https://api.open-meteo.com/v1/forecast` + `current=temperature_2m,weather_code,wind_speed_10m` + `forecast_days=1`(`island-weather.ts:35-41`)。
|
||
2. 坐标校验:`latitude/longitude` 必须是有限数字,纬度绝对值 ≤90、经度 ≤180,否则抛「请设置有效的经纬度」(`:14-22`)。
|
||
3. 缓存键 = 坐标定点 4 位(`:23`);成功缓存 **15 分钟**(`:60`);失败缓存 **60 秒**退避并保留旧数据(`:63`);同一坐标并发请求复用同一 Promise(`:29-30`);缓存上限 **32** 条(`:68`)。
|
||
4. 请求安全:`AbortSignal.timeout(8000)`、`redirect:'error'`、响应体 > **64 000** 字节拒收(`:42-45`);自动验收模式下不访问外网(`:33-34`)。
|
||
5. 数据校验:三个数值字段必须都是有限数字,否则「天气数据格式不正确」(`:47-52`)。
|
||
6. `stale`:成功返回时按 `fetchedAt` 距今 ≥ **15 分钟**标记;失败返回旧数据时强制 `stale:true`(`:26,64`)。
|
||
7. 渲染侧:每 **60 秒**刷新一次、坐标变化立即刷新、失败显示「天气暂不可用」、未设置坐标显示「请设置天气地点」、`stale || unavailable` 时追加 `· 缓存`(`WeatherBrief.vue:36-49,68-81`)。
|
||
|
||
#### 8.9.3 天气文案与建议(保留)
|
||
|
||
WMO 天气码映射(`WeatherBrief.vue:50-60`):`0 晴`、`1/2/3 少云/多云/阴`、`45/48 雾`、`51-57 毛毛雨`、`61-67/80-82 雨`、`71-77/85/86 雪`、`95-99 雷雨`、其他 `天气`。建议文案:`code ≥ 95 → 雷雨天气,注意避雷`;`windSpeed ≥ 40 → 风力较大,注意出行`(`:61-66`)。
|
||
|
||
#### 8.9.4 ClassIsland 视觉映射与范围
|
||
|
||
- ClassIsland 天气组件的「6 种主信息模式」本轮**只取默认一种**:`地名 + 天气文字 + 温度(取整)+ 可选建议`,以 ClassIsland 的次要/正文字号(14/16)呈现;不实现多模式切换。
|
||
- **不实现** ClassIsland 的气象预警、三天天气预报、逐小时预报(数据源无对应字段;`docs/app/island.md:45` 明确「不提供官方气象预警」)。
|
||
- 组件内刷新失败不得阻塞岛其他内容;天气与服务端无关,不参与消息队列。
|
||
|
||
### 8.10 课件传输覆盖(最高优先级内容)
|
||
|
||
#### 8.10.1 优先级规则(必须实现,`island.ts:649-676`)
|
||
|
||
1. 课件传输(`coursewareTransfer`)**覆盖整岛内容**:`fixed` 的两种 family 与 `custom` 都必须显示;`getState()` 时若「当前有正在展示的紧急消息或叫人(`mode!=='hidden' && active && (priority==='URGENT' || kind==='call')`)」则**课件让位**,状态里返回 `coursewareTransfer:null`。
|
||
2. 其余消息不因课件覆盖而丢弃:`active/queued` 保持原状,课件结束后按队列规则继续展示。
|
||
3. 课件传输期间不触发强提醒特效(课件不是紧急消息)。
|
||
4. 课件覆盖时窗口几何按 `COURSEWARE_ISLAND_SIZE`(**424×132**,`packages/shared/src/courseware.ts:4`)+ `ISLAND_SHADOW_PAD` 定位;`custom` 常驻也临时改为该尺寸,结束后恢复原方案尺寸。
|
||
|
||
#### 8.10.2 UI 与像素(保留现有值)
|
||
|
||
| 项 | 值 |
|
||
|---|---|
|
||
| 卡片 | 424×132,`padding 17 24`,`border-radius 24`,背景 `rgba(20,20,24,0.97)`,阴影 `0 2px 8px #0004` |
|
||
| 字体 | `13px/1.4 'Segoe UI','Microsoft YaHei',sans-serif` |
|
||
| 状态行 | 12px,`#ffffffb8`;百分比/「失败」用强调色、600 |
|
||
| 文件名 | 16px、600、单行省略、`margin 5px 0` |
|
||
| 元信息 | `类型 · 教师` + `已收/总量 MB`(保留 1 位小数) |
|
||
| 进度条 | 高 **5px**、圆角 8、`accent-color`,进度槽 `#ffffff30` |
|
||
| 错误 | 11px、`#ffb4ab`、单行省略 |
|
||
| 证据 | `CoursewareTransfer.vue:67-131` |
|
||
|
||
#### 8.10.3 阶段与失败
|
||
|
||
- 阶段文案(`CoursewareTransfer.vue:26-34`):`downloading 正在接收课件`、`verifying 正在校验文件`、`completed 课件接收完成`、`failed 课件接收失败`。
|
||
- 完成后由主进程/服务端链路校验 SHA256 并回执(`docs/app/courseware.md:28`);岛只负责展示,不执行文件。
|
||
- 失败必须显示 `error` 文案并允许用户点击卡片打开课件页(`pointer-events` 在课件卡上为 `none`,点击交给整岛 `open-app`);失败卡片停留至用户处理或下一个传输覆盖,建议停留 30s 后自动恢复常态【推断】。
|
||
- 传输中窗口重建/渲染进程崩溃:主进程重新下发 `coursewareTransfer` 状态,恢复展示。
|
||
|
||
### 8.11 教师端 / 班级端差异
|
||
|
||
1. **岛只在班级端运行**:教师/管理员使用 Web 管理端,不启动岛;客户端以 `CLASS_DEVICE` 登录时才有「本班」消息。
|
||
2. 班级端:接收本班通知/作业/叫人/课件;课程提醒基于服务端解析后的本班课表(`ScheduleDto`/`IslandTimePointDto`)。
|
||
3. 教师端(Web):无岛;发布 URGENT 时的二次确认是服务端规则(`docs/app/notifications.md:25-29`),与岛无关。
|
||
4. **未登录/离线**:不展示在线消息;已缓存消息可只读展示(不得伪造未读状态);课程提醒与天气可离线工作(课表来自本地缓存,天气有缓存)。
|
||
5. **权限**:已读/撤回等操作失败(403/401)时,岛必须回滚本地状态并提示「无权限或登录已失效」,不得假装成功。
|
||
|
||
### 8.12 冲突裁决表(ClassHelper 业务优先,ClassIsland 视觉优先)
|
||
|
||
| 主题 | ClassHelper 既有语义(胜出) | ClassIsland 视觉(采用) | 落地规则 |
|
||
|---|---|---|---|
|
||
| 消息排序 | `PRIORITY_RANK` → 时间 | 提供方优先级/通知等级 | 按 8.2 第 3 条:业务优先级优先,等级仅作稳定次级键 |
|
||
| 上课暂存 | 上课期间普通通知暂存,下课补弹 | ClassIsland 上课照常播提醒 | 暂存规则保留;信息行保持可见但**不展开气泡** |
|
||
| 紧急消息 | 立即展开、45s 收起 | ClassIsland 气泡两阶段 | 立即展开气泡 + 撒特效;45s 超时保留 |
|
||
| 普通叫人 | 上课也保留可点入口,不自动展开 | ClassIsland 无「叫人」概念 | 上课显示可点 chip;点击展开气泡 |
|
||
| 去重 | 按 id + 双播合并 | 同一 request 只入队一次 | 两者都实现(8.2) |
|
||
| 已读 | 服务端已读 + 本地回执 | ClassIsland 无 | 保留现有,失败回滚 |
|
||
| 收起动画 | 收回动画 360ms/窗口淡出 | 遮罩退出 200ms | 遮罩退出用 ClassIsland 参数;岛形变用弹簧;窗口淡出保留 |
|
||
| 多消息列表 | 前 3 条 + 展开更多 | ClassIsland 无列表形态 | 用 ClassIsland 行高/字号重排列表(共享 `islandListLayout`) |
|
||
| 课程提醒 kind | prepare/on-class/break/ending/in-class | ClassIsland 只有上下课/课前/放学 | 保留五类;`ending` 用徽标,**不做「即将下课」遮罩** |
|
||
| 课程提醒条高度 | 40 | 行高 40 | 一致,沿用 40 |
|
||
| 进度条 | 2px(提醒条) | 3px(行/组件) | 两处各自保留,见 8.8 |
|
||
| 天气 | Open-Meteo 15 分钟 + 缓存标记 | 小米天气 + 预警 | 保留 Open-Meteo;不实现预警/预报 |
|
||
| 课件覆盖 | 覆盖岛内容 | 无对应 | 保留 ClassHelper 规则;紧急/叫人时不覆盖 |
|
||
| 音效/TTS | 无 | 有 | **不实现**,不进入验收 |
|
||
| 自动化/插件/管理端 | 无 | 有 | **不实现** |
|
||
|
||
### 8.13 边界与失败降级
|
||
|
||
1. **无课表/课表为空**:`lessonReminder=null`;信息行显示日期 + 课程表占位(`今天没有课程。`);课程提醒全部不触发;不得因此隐藏整行。
|
||
2. **上课状态未知**(`inClass` 未下发/服务端断线):按「非上课」处理(消息正常展示),不得猜测;恢复后以下发值为准。
|
||
3. **时间跳变/时区变化**:课程提醒 id 含 `dayKey`,跨天自动重建;时间回退不得重复触发同一 id 的提醒(`entered` 逻辑),时间前跳不得补发多条。
|
||
4. **消息风暴**(一次 > 20 条):列表最多 `maxRows=20`,其余提示「更多请前往应用内操作」;不得逐条弹气泡;同一 id 重复到达按合并规则处理。
|
||
5. **重复广播 / 断线重连重放**:以 id 去重;重连后以服务端列表为准做一次全量对账(新增/删除/已读状态),不得凭队列恢复弹窗。
|
||
6. **已读请求失败**:`markRead/markAllRead` 失败时恢复本地未读状态并提示;不得把消息从队列删除。
|
||
7. **课件传输中断/断网**:显示 `failed + error`;恢复网络后由心跳补拉(`docs/app/courseware.md:26-28`);岛不得自行重试下载。
|
||
8. **渲染进程崩溃/窗口重建**:主进程保留 `state`,重建后一次性下发;课件/课程提醒/消息不丢;重建限流沿用现有(3 次/10 分钟)。
|
||
9. **系统睡眠/唤醒**:唤醒后立即重算课程提醒与上课状态;跨过的时间点不补弹(与 8.13 第 3 条一致);课件若在睡眠中完成,按最新状态展示。
|
||
10. **多屏切换/DPI 变化**:重算命中矩形、气泡与特效窗口;切换瞬间不得显示残影(先隐藏特效窗口再重算)。
|
||
11. **无网络启动**:消息/课表用离线缓存(按服务器与班级隔离),天气显示缓存或「暂不可用」;课程提醒只要课表缓存存在就必须工作。
|
||
12. **编辑模式/设置预览**:编辑模式不弹新气泡、不播特效、课程提醒条不接管编辑画布;预览模式停留 30s 自动结束(`PREVIEW_DURATION_MS`,`island.ts:2299-2300`),不写已读。
|
||
|
||
### 8.14 验收标准(可勾选,全部通过才算完成)
|
||
|
||
**消息与队列**
|
||
|
||
1. 同一 id 的通知与叫人双播到达:只出现一条,`kind='call'`、`priority` 取高者;不会降级成普通通知。
|
||
2. 课间收到普通通知:`fixed`+`classisland` 行内自动展开气泡(先遮罩后正文);`winisland` 为胶囊;`custom` 铃铛计数 +1。
|
||
3. 上课收到普通通知:不弹气泡,行内显示暂存角标;下课自动补弹最新一条,其余保留「还有 N 条」。
|
||
4. 上课收到普通叫人:显示可点 chip,不自动展开;点击后展开气泡并可「收到」。
|
||
5. 紧急通知/紧急叫人:无论上课与否立即展开气泡,并在 45s/90s 后按规则收起(有未处理时保持 chip/角标)。
|
||
6. 「知道了」与「标为已读」语义区分正确;整批已读会过滤 homework 本地 id,不报「通知不存在」。
|
||
7. 实时撤回后,岛内该消息从 active 与 queued 同时消失。
|
||
8. 消息列表默认 3 行、超出显示「展开更多(还有 N 条)」;展开后受屏幕高度限制改提示「更多请前往应用内操作」。
|
||
|
||
**气泡视觉**
|
||
|
||
9. 气泡遮罩 5 区段斜切扫入,区段 360ms、错峰 120ms;文字延迟 260ms 淡入 250ms + 缩放 750ms;退出 200ms 且缩放回 1.1;`speed` 改变后所有时长同比缩放。
|
||
10. 行高 40、圆角 8、背景不透明度 0.5、底部进度条 3px、值过渡 50ms 线性;文字始终不透明。
|
||
11. `animations=false` 或系统减少动态时,气泡直接终态,最终几何与有动画时一致。
|
||
|
||
**强提醒特效**
|
||
|
||
12. 紧急消息触发全屏涟漪:600ms 扩散 + 600ms 淡出,750ms 后移除;多屏各一窗口;`effectScale` 生效。
|
||
13. 编辑模式、隐藏状态、窗口重建中不播特效,但气泡照常;特效窗口不接收鼠标、不改变岛命中。
|
||
|
||
**课程提醒**
|
||
|
||
14. 五类提醒按 8.6.2 算法在构造的时间轴下逐一出现,且同一提醒不重复;关闭对应开关后不出现。
|
||
15. `custom` 下 `in-class` 与 `ending` 不接管岛;`fixed` 下 `ending` 显示徽标而非遮罩;任何形态都**没有「即将下课」遮罩**。
|
||
16. 蓝色秒数徽标:正圆 30×30、`#0a84ff`、12px/600、显示 `-N`;`now = end - 1ms` 时显示 `-1`;`endingEnabled=false` 不显示。
|
||
17. 课程进度:提醒条 2px、组件内 3px,两条同时存在且均贴底、均用 `scaleX`。
|
||
|
||
**天气与课件**
|
||
|
||
18. 天气 15 分钟缓存、失败 60s 退避并保留旧数据、断网显示「· 缓存」、坐标非法显示「请设置天气地点」;同一坐标并发请求只发一次网络请求。
|
||
19. 课件传输覆盖整岛(424×132),显示文件名/类型/教师/已收-总量/进度;紧急或叫人在播时课件让位且不丢消息;完成后 SHA256 校验通过才显示「完成」。
|
||
|
||
**边界**
|
||
|
||
20. 跨天、时间跳变、睡眠唤醒、渲染进程崩溃重建后,消息不丢、不重复弹、课程提醒状态正确。
|
||
21. 无课表/无网络/未登录时,岛显示合理占位且不崩溃;已读失败会回滚并提示。
|
||
22. 全程未出现音效/TTS、自动化、插件、主题市场等非目标功能;也未出现第二个岛窗口(课件与特效窗口除外,且均为独立用途)。
|