Files
ClassHelper/docs/app/island.md
T
LoveChengke 0ea6bd887f feat(website,docs,build): 官网加一个下载页(挑版本装机),文档站的照片换成与 README 同一份
下载页(`website/download.html` + `assets/download.js`)

- 官网首页的「立即下载」、顶栏与页脚的「取安装包」全部指向新页 `/download.html`。
  那一页是「先挑一个版本,再挑要装的那一份」:一个版本分段控件 + 四张卡
  (班级端 / 服务端 Windows / 服务端 Linux / ClassIsland 插件),每张卡列出这一版里
  真实存在的文件、体积与直链;
- 版本与文件清单**不在站点里存副本**,每次打开都由 `assets/download.js` 当场读 GitHub
  Releases —— 文件名、体积、链接照抄发布页,不靠命名约定去猜;
- 读不到时(离线,或匿名接口 60 次/小时按出口 IP 被限流)退回页面上那份
  `<meta name="ch-version">`,按发布命名约定拼直链,并在页面上**明说这是拼出来的**;
- 老版本缺哪个包就显示「这一版没有发布…」(`v0.1.0-fix` 只有插件包,实测正确);
  Linux 那一行安装命令跟着选中版本钉上 `--version`(tag 不是 `x.y.z` 时不带);
- 动态建出来的卡片与文件行派发 `ch:content`,由 `main.js` 补挂按压反馈
  —— 这些元素是拉到清单之后才出现的,启动时还看不到它们。

文档站的照片(`docs/images/`)

- 页面里的图一律从 `docs/screenshots/**` 换成 `docs/images/**`:后者就是 README 与官网
  吃的那一份(字节相同),前者是 9-26 的冒烟留档,比 README 落后一个版本;
- 展示图与回归留档这两套图的分工写进 `docs/dev/testing.md`「截图留档」,含重新采集命令
  (冒烟会把「岛外留白对照图」落在同一个目录,所以要先落临时目录再挑);
- 顺带修 README 手机截图那张标错的图注(`mobile-dashboard.jpg` 是仪表盘,不是抽屉菜单);
- `docs/reference.md` 验收表里那张 ClassIsland「真机实测截图」不再嵌图:它是连着桌面、
  任务栏和另一个窗口整屏截下来的,当文档配图不能用。断言与用例名都保留,另外注明了抓图位置。

版本落点

- `scripts/version.mjs` 增加 `website/download.html` 的兜底版本号,落点 13 → 14 处
  (`AGENTS.md` §4.4 与 `docs/dev/release.md` 同步);
- README「开始使用」表里那三个文件名改成**发布页上真实的名字**
  (`-x64-client-setup` / `-x64-client-portable` / `-x64-server-setup`)—— 原来写的是构建
  产物的名字;「上传前把产物改成 ASCII 名」这一步也一并写进发布流程,
  它是官网下载页离线兜底与 README 表格的共同依据。
2026-10-06 18:37:42 +08:00

122 lines
9.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: 灵动岛
description: 桌面客户端的通知浮窗:胶囊与卡片、多条通知的竖排列表、停靠与外观、触摸屏下的行为。
---
# 灵动岛
灵动岛是桌面客户端上那个独立于主窗口的浮窗:无边框、透明、置顶、不占任务栏。
它是「消息到达的第一现场」—— 老师发的内容先在岛上出现,学生点开才进主界面。
视觉参照 [WinIsland](https://github.com/WinIslandProject/WinIsland):
连续圆角(超椭圆)而不是 `border-radius`、纯黑 / 毛玻璃 / 主题色三种底、Apple 系统色强调色、
6 个停靠位、可选的「空闲细缝」。逐项设计令牌见[灵动岛设计令牌](../dev/winisland-design-tokens.md)。
## 各形态长什么样
| 形态 | 截图 | 说明 |
| ---------------------- | ----------------------------------------------------- | ------------------------------------------------- |
| 非上课时段的「新消息」 | ![新消息胶囊](../images/island-4-pill.png) | 268×44,直接出现(无入场动画),点击后展开 |
| 点开后的详情卡 | ![点击后展开](../images/island-5-clicked.png) | 标题 / 内容 / 时间 + 打开应用 / 标为已读 / 知道了 |
| 下课后自动弹出 | ![下课后自动弹出](../images/island-2-after-class.png) | 上课期间暂存的通知,下课后自动弹出 |
| 上课期间的紧急通知 | ![紧急通知立即展开](../images/island-3-urgent.png) | 卡片**内部**红色光晕 + 紧急角标,无需点击 |
| 新作业 | ![新作业](../images/island-7-homework.png) | 青蓝描边 + 书本图标 |
| 叫人 | ![叫人](../images/island-8-call.png) | 琥珀金卡片 + 「叫人」徽标 + 「收到」按钮 |
## 什么时候弹、弹成什么样
| 场景 | 灵动岛行为 |
| --------------------------------- | --------------------------------------------------------------------------------------------- |
| 收到通知(非上课时段) | 直接出现胶囊:`新消息 · 共 N 条` + 发送人 + `点击查看` |
| 多条未处理消息 | 胶囊标题汇总类型:`新消息:叫人/作业/通知(共 3 条)`,右侧带类型小标签 |
| 点击胶囊(只有一条) | 展开为**详情卡**:标题、内容、时间 + 三个按钮 |
| **点击胶囊(多条)** | 展开为**竖排列表**,按重要程度排序(紧急 > 重要 > 普通 > 低,同级按时间新的在前) |
| **点「展开更多」** | 把放得下的都铺出来;铺到工作区下沿还放不下时改成「更多请前往应用内操作」 |
| **多条时的「知道了 / 标为已读」** | 都是**整批**操作 |
| 上课时段的**普通叫人** | **不自动展开**(不打断课堂),但保留胶囊并**允许学生主动点开**;下课自动展开 |
| **紧急叫人** | 上课时段也立即展开,卡片是琥珀金 + 「叫人」徽标,按钮为「收到」 |
| 上课时段的普通通知 | **完全不显示**(窗口隐藏)并进队列;下课自动弹出详情 → 收起为胶囊 |
| 上课时段的紧急通知 | **无论是否上课立刻展开**,45 秒后收起 |
| 点击卡片空白处 / 屏幕任意位置 | 回缩为**胶囊**(不直接消失,仍可再次点开) |
| **收起后胶囊常驻** | 只要还有未处理通知,胶囊就不会消失;彻底消失只在「知道了 / 标为已读」、队列清空或进入上课时段 |
> **本机刚录入的作业不会上岛** —— 自己通知自己没意义。
## 多条通知时的排版
这是最容易出问题的地方:卡片要长高,还得刚好放得下。规则是**主进程与渲染进程各算一次同一个数**
(`islandListLayout()`,在 `@classhelper/shared` 里):
- 默认只显示前 `defaultVisibleRows`(3)条,下面是「展开更多(还有 N 条)」;
- 点开后铺到 `maxCardHeight` 为止,仍放不下的改成「更多请前往应用内操作」;
- `maxCardHeight` 是主进程按工作区 / 任务栏算出的**可用高度**,随状态下发。
窗口的包围盒会**在卡片形变之前**先长高(这一步用户看不到),否则底部的按钮会被裁掉。
## 外观设置
客户端「设置 → 灵动岛」:
| 设置项 | 区间 / 取值 | 默认 |
| --------------- | -------------------------------------- | ------------------------- |
| 高度 | 36–72px | 44 |
| 宽度 | 220–420px | 268 |
| 圆角 | 8–32px | 20 |
| 不透明度 | 40%–100% | 100% |
| **字号** | 11–20px | 13 |
| 动画速度 | 0.5×–2× | 1× |
| 主题色 | 取色器 | `#0a84ff`(Apple 系统蓝) |
| **视觉风格** | 纯黑 / 毛玻璃 / 主题色渐变 | 纯黑 |
| **显示位置** | 顶部 / 底部 × 左 / 中 / 右(6 个锚点) | 顶部居中 |
| 左右 / 上下边距 | 0–200px / 0–160px | 都是 8px |
| 跟随鼠标屏幕 | 开 / 关 | 关 |
| 动画开关 | 开 / 关 | 开 |
| 始终置顶 | 开 / 关 | 开 |
| **空闲细缝** | 开 / 关 | 关 |
几个容易困惑的点:
- **字号是"基础字号"**:岛内所有文本 = 基础字号 × 各自的排版系数(标题 1.08 / 正文 0.95 / 次要 0.78 /
徽标 0.76 / 胶囊标题 0.92 ……),所以调一个滑块就能整体放大,比例不会乱。
- **左右边距只在「左 / 右停靠」时生效**:默认是「顶部居中」,水平锚点就是屏幕中线,
这时调左右边距不会有任何变化 —— 设置页会把那个滑块置灰并写明原因。
- **「毛玻璃」不是窗口材质**,是 CSS 半透明深色卡。用系统级背景材质(`setBackgroundMaterial`)
会在卡片外那一圈垫出一块浅色"白底面板",而且**调用过一次就再也回不去逐像素透明**。
- **「预览效果」**会本地推一条通知,立刻看到当前尺寸 / 配色 / 动画。预览是专用通道:
直接展开、失焦不收起、30 秒后自行消失。
越界的值在**主进程**统一夹紧,配置填坏了也不会让布局错乱。
## 触摸屏(希沃白板等)
触摸屏上灵动岛走的是**另一条命中链路**。
原因:常规的命中判定有两条路径 ——渲染进程转发鼠标移动、主进程读系统光标位置 ——
**两条都以"鼠标指针移动"为前提**。而手指触摸既不产生 `mousemove`、也不移动系统光标,
窗口就会永远停在"穿透"状态,**手指点不开岛**。
修法是:客户端检测到 `navigator.maxTouchPoints > 0` 后进入触摸模式,
把窗口**贴合岛体**并**始终接收输入**(Windows 上 Electron 没有窗口区域能力,这是唯一可行的办法)。
触摸模式下有三条额外的护栏,都是为了不闪、不裁:
- **窗口可见期间只缩不放**(需要变小一律等窗口隐藏之后再做);
- 命中矩形按**窗口实际尺寸**算偏移;
- 失焦宽限期放宽到 1200ms(触摸没有光标,"假失焦"会让刚展开的岛被缩回胶囊)。
非触摸屏的机器**完全不走这条路**。
## 鼠标穿透
窗口是按最大形态创建的固定包围盒,比卡片大一圈。默认整块穿透,渲染层做命中测试,
只在指针进入岛体时打开命中 —— 所以这个"大窗口"不会吞掉桌面上的点击。
另有一层保险:主进程每 120ms 用系统光标位置校正一次命中。
## 排查"闪一下"
- **开合时底座闪一下**:卡片投影是常驻的 CSS `filter`,形态切换只改参数。
给元素增删 `filter` 会让 Chromium 重建渲染表面,首帧可能被当成空内容画出去。
- **收回一瞬间闪一下**:多半是**自动化验证实例的叠影**,不是产品的问题 ——
冒烟会真的在屏幕上放一个灵动岛,如果它和你自己的客户端叠在一起,你看到的是两个窗口。
(验证实例的岛是洋红主题色,一眼可辨。)