feat(docs,scripts,build): 用 docfx 搭文档站并发布到 GitHub Pages,版本号收口成脚本迭代
文档站(`docs/` 就是 docfx 项目,发布在 https://lovechengke.github.io/ClassHelper/): - 四章 + 常见问题 + 完整参考,共 44 页:快速上手(8 页)/ 应用帮助(10 页)/ 管理运维(6 页)/ 开发文档(11 页)。信息架构照 ClassIsland 文档站来: 顶栏分章、左侧章节导航、右侧本页目录、上一页 / 下一页、站内搜索、深色主题跟随系统; - 自定义模板覆盖(`templates/classhelper/`):把 docfx modern 模板那套英文 UI 文案 (In this article / Improve this doc / Next…)换成中文,补 GitHub 入口与版本徽标, 并加一层只调排版与配色的 `public/main.css`(不动 DOM 结构); - 站点顶栏的版本徽标取自根 `package.json`(`docs/build.mjs` 用 `--metadata` 注入), 不在文档里写第二份 —— 否则一定漂; - 404 页单独生成:GitHub Pages 的项目页挂在 `/<仓库名>/` 子路径下,而浏览器是按**被请求的路径** 解析页面里的相对链接的,所以 `<base>` 必须钉到站点根,由 `build.mjs`(本地 `./`)与 workflow(CI 传 `/<仓库名>/`)分别注入; - 构建前清空 `_site`:docfx 只覆盖不清理,删掉或改名的页面会以旧文件留在目录里, 而 Pages 上传的是整个目录。 版本迭代(**每一次 releases,不是每一次提交**): - 新增 `scripts/version.mjs`。这个仓库里同一个版本号散在 **10 个文件**里(5 份 `package.json`、 `deploy/package.runtime.json`、两个 compose 的镜像 tag、插件 `manifest.yml`、以及插件 C# 里的 `PluginVersion` 常量),手工改必然漏一处,而漏掉的那处**不会报错**,只在某个不常走的分支上 表现为"版本号对不上"。现在收口成两条命令:`pnpm version:check`(逐处比对,不一致 exit 1) 与 `pnpm version:bump patch|minor|major`(一次改写全部 10 处,任何一处匹配不到或匹配到多处 就直接失败退出,不做尽力而为的部分改写); - `AGENTS.md` 新增 §4.4「版本迭代(每一次 releases,不是每一次提交)」:写清版本号落在哪 10 处、 完整发布命令序列(体检 → 验收 → `dist:all` → 哈希清单 → 推 master → `gh release create` → Linux 包由 CI 合并哈希行),以及"已发布的版本号不要复用"的理由。 其它: - `.github/workflows/docs.yml`:推 master(`docs/**` / `package.json` / workflow 自身有变)即构建并 部署到 Pages;构建带 `--check`,把 docfx 的警告当错误 —— 最常见的警告是"某个 `.md` 没被任何 `toc.yml` 引用",那种页面有内容但没有侧边栏,属于必须修掉的疏漏; - docs 目录重组后的引用同步:`production` / `mysql` / `linux-deploy` / `winisland-design-tokens` 移到新位置后,AGENTS.md、`deploy/`、`scripts/`、客户端源码注释里的路径引用一并更新; - 文档站只发布 `docs/` 这一棵树,页面里指向树外的相对链接(`../AGENTS.md` 等)改成 GitHub 绝对 URL, 否则上线全是 404; - 图标派生清单补上文档站模板里的 `logo.png` 与 `favicon.png`(`pnpm icons` 一并生成, 仍然只有一个图形源 `build/classhelper.{png,ico}`); - `docs/_site` 进 `.gitignore` / `.prettierignore` 与 ESLint ignores; - 根脚本补 `version:check` / `version:bump` / `docs:build` / `docs:serve` / `docs:check`。
This commit is contained in:
1 parent
5b002a4f0b
commit
19c2aa07b2
61 files changed
+4761
-12
No files matched your search
@@ -0,0 +1,97 @@
|
||||
---
|
||||
title: 外观、主题与动效
|
||||
description: 深浅色主题、界面风格、设计令牌与动效语言,以及降低动态效果的降级行为。
|
||||
---
|
||||
|
||||
# 外观、主题与动效
|
||||
|
||||
## 深浅色主题
|
||||
|
||||
**两端都有深色主题,机制完全一致**:在 `<html>` 上挂 `.dark` 类,由各自的样式表覆盖**语义色令牌**,
|
||||
Element Plus 的深色变量提供组件层的默认值。两端的 `main.ts` 都在挂载之前就把主题落好,
|
||||
所以深色用户打开时不会"先白后黑"闪一下。
|
||||
|
||||
| 行为 | 说明 |
|
||||
| ------------ | -------------------------------------------------------- |
|
||||
| 默认 | **跟随系统**(`prefers-color-scheme`,系统改了就跟着变) |
|
||||
| 手动切换之后 | 以用户的选择为准(系统的偏好是默认值,不是命令) |
|
||||
| 切换入口 | 客户端在顶栏(日月按钮),Web 管理端也在顶栏 |
|
||||
| 切换效果 | 从点击位置**扩散的圆**铺满整页换肤 |
|
||||
|
||||
**刻意不随主题的区域**:登录页的深色渐变底、桌面端的启动过渡页与全屏作业看板、灵动岛自身、
|
||||
Web 管理端的侧边栏(本来就是深色),以及深色底上的白色文字。
|
||||
|
||||
Web 端还会同步 `<meta name="theme-color">`,并设 `color-scheme` 让**原生控件**
|
||||
(滚动条、日期选择器弹层、自动填充底色)也跟着走 —— 只改 CSS 变量的话它们会仍是浅色。
|
||||
|
||||
## 语义色令牌
|
||||
|
||||
组件里**一律引用令牌,不写死颜色**:
|
||||
|
||||
| 令牌 | 用途 |
|
||||
| ------------------------------------------------------ | -------- |
|
||||
| `--ch-surface` / `-alt` / `-sunken` / `-hover` | 各种表面 |
|
||||
| `--ch-text` / `-secondary` / `-muted` / `-placeholder` | 文字层级 |
|
||||
| `--ch-radius-xl / lg / md / sm / pill` | 圆角 |
|
||||
| `--ch-shadow-sm` / `-md` | 投影 |
|
||||
|
||||
`html.dark` **只覆盖颜色,绝不覆盖圆角与动效令牌** —— 两套主题的几何必须完全一致,
|
||||
否则切换时会看到布局跳一下。
|
||||
|
||||
> 改令牌时**只改值**,不要对样式表跑批量替换。"把 `#303133` 全量替换成 `var(--ch-text)`"
|
||||
> 会把令牌**定义本身**也换掉,自定义属性一旦自引用就整条失效、解析成空串,
|
||||
> 全站文字颜色静默回落成继承色。
|
||||
|
||||
## 界面风格
|
||||
|
||||
- 浅色底 `#f3f3f3` + 半透明卡片(`rgba(255,255,255,.7)` + `backdrop-filter: blur(20px)`);
|
||||
- 强调色 `#0F6CBD`(悬停 `#115EA3`);选中的导航项 = 强调色淡底 + 左侧 3px 强调条;
|
||||
- 圆角统一:卡片 8px / 控件 4px;描边用半透明黑代替灰线;阴影极轻;
|
||||
- 字体 `Segoe UI Variable / 微软雅黑 UI`;Element Plus 的变量整体对齐这套规范。
|
||||
|
||||
Web 管理端走的是另一套(1Panel 风格:深色侧边栏 + 白色圆角卡片),
|
||||
圆角与投影集中定义在它的 `:root` 令牌里。
|
||||
|
||||
## 动效语言
|
||||
|
||||
两端的动画与过渡统一走一套自研的动效语言,**移植自 [beUI](https://beui.dev) 的 `lib/ease.ts`**。
|
||||
beUI 的组件本身是 React,用不到这里,但它的动效语言与框架无关。
|
||||
|
||||
- **令牌单一来源**:`packages/shared/src/motion.ts`。三组缓动曲线 + 六组具名弹簧;
|
||||
- **弹簧由数值积分算出落定时间**,再生成 CSS `linear()` 缓动字面量 ——
|
||||
于是弹簧可以纯 CSS 驱动,不必给每个组件挂一个 rAF 循环;
|
||||
- **运行时注入**:两端在挂载前把令牌写进 `:root` 的自定义属性。CSS 里只引用变量,不抄数值。
|
||||
|
||||
落地到界面上的效果:
|
||||
|
||||
| 位置 | 动效 |
|
||||
| --------------------- | ------------------------------------------------------------------ |
|
||||
| Element Plus 全站过渡 | 时长收紧到 200 / 140ms,缓动换成 EASE_OUT |
|
||||
| 弹窗 / 抽屉 / 浮层 | 走弹簧;**退出一律比入场快**(用户已经决定关掉它了) |
|
||||
| 按钮 | 按下 90ms 直落(跟手)、抬起弹回 |
|
||||
| 侧栏激活项 | 单个"底片"在菜单项之间滑过去,取代"每项各画一块底色" |
|
||||
| 页面内容 | 按块错峰浮现(30ms 一档,封顶 180ms) |
|
||||
| 统计数字 | 0 → 实际值滚动 |
|
||||
| 灵动岛 | **刻意只有单调缓动,不用弹簧**(任何过冲都会让"开合"看起来在震动) |
|
||||
|
||||
### 降低动态效果
|
||||
|
||||
两端各有一个 `prefers-reduced-motion` 的全局降级块,把动画压到 1ms ——
|
||||
**是压短而不是删掉**(`transition: none` 会让依赖 `transitionend` 的逻辑失灵)。
|
||||
|
||||
唯一的例外是**加载指示旋**:它是放慢而不是停,停掉会让界面看起来像卡死。
|
||||
|
||||
## 客户端设置页
|
||||
|
||||
「设置」是一个可展开的分组,六个子页各有自己的路由:
|
||||
|
||||
| 子页 | 内容 |
|
||||
| ------ | ----------------------------------------------------------------------------- |
|
||||
| 通用 | 服务器连接、离线缓存、作业录入短语、使用引导 |
|
||||
| 外观 | 浅色 / 深色 |
|
||||
| 灵动岛 | 尺寸、字号、主题色、视觉风格、停靠、边距、空闲细缝(详见[灵动岛](island.md)) |
|
||||
| 提醒 | 通知显示在哪一端 |
|
||||
| 账号 | 当前班级信息、退出登录 |
|
||||
| 关于 | 版本 / Electron / Chromium / Node、诊断信息、检查更新、鸣谢 |
|
||||
|
||||
`/settings` 会自动跳到「通用」。设置页的改动会**立刻生效并写进本机配置**,重启后仍保留。
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
title: 叫人
|
||||
description: 老师点名让学生过来:普通与紧急两档、快捷短语、谁能叫谁,以及它在教室里的表现。
|
||||
---
|
||||
|
||||
# 叫人
|
||||
|
||||
「叫人」是老师点名让某位同学来一趟 —— 它**不是**一条普通通知,因为它有一个明确的收件人和一个明确的动作
|
||||
(学生会点「收到」)。
|
||||
|
||||
## 在哪发
|
||||
|
||||
| 入口 | 谁能用 |
|
||||
| --------------- | ------------ |
|
||||
| 通知发布 → 叫人 | 教师、管理员 |
|
||||
| 学生管理 → 叫人 | 管理员 |
|
||||
|
||||
学生管理页是管理员专属,但班主任与科任老师同样需要叫人,所以给老师留的入口在「通知发布 → 叫人」。
|
||||
|
||||
## 两档
|
||||
|
||||
| 级别 | 落库级别 | 学生端行为 |
|
||||
| ------------ | -------- | ----------------------------------------------------------------------------------------------------- |
|
||||
| **普通叫人** | `HIGH` | 课间先显示「叫人」胶囊,点击展开;**上课时段只进队列、不打断课堂**,下课后自动弹出详情(90 秒后收起) |
|
||||
| **紧急叫人** | `URGENT` | **无视上课时段立即展开**,上课时被收起后仍保留胶囊、可再次点开 |
|
||||
|
||||
两档的卡片都是「请 XXX 同学找 XXX 老师」+ 具体事由,徽标是「叫人」,按钮是「收到」
|
||||
(复用通知的已读机制)。
|
||||
|
||||
> **是否立即展开只由 `priority` 决定**,与普通通知共用同一条排队 / 隐藏 / 下课弹出路径。
|
||||
|
||||
## 怎么说
|
||||
|
||||
两种输入方式,**自定义消息优先于快捷短语**:
|
||||
|
||||
- **快捷短语**(8 条,点击选中、再点取消):请到办公室找我 / 请到讲台找我 / 请到实验室找我 /
|
||||
请到门卫处找我 / 请带上作业本找我 / 请带上试卷找我 / 请到教室门口等我 / 请马上来一趟;
|
||||
- **自定义消息**:最长 200 字,例如「带上昨天的数学作业到办公室」。
|
||||
|
||||
## 谁能叫谁
|
||||
|
||||
| 角色 | 限制 |
|
||||
| ------ | ---------------------------------------------------------- |
|
||||
| 学生 | 调用被拒(403) |
|
||||
| 教师 | 只能叫**自己班级**的学生;跨班、或学生不在本班 → 400 / 403 |
|
||||
| 管理员 | 任意班级 |
|
||||
|
||||
## 服务端做了什么
|
||||
|
||||
`POST /api/calls`(教师 / 管理员)→ `urgent: true` 落库成 `URGENT`,否则 `HIGH`,
|
||||
然后广播两个事件:
|
||||
|
||||
- `notification:new` 到班级房间(通知中心与灵动岛都靠它);
|
||||
- `call:new` 定向到被叫学生 / 班级设备。
|
||||
|
||||
如果教室的「通知显示位置」不是"只在 ClassHelper",这条叫人也会推给 ClassIsland,
|
||||
教室大屏一并播报。
|
||||
@@ -0,0 +1,98 @@
|
||||
---
|
||||
title: 班级、学生与教师
|
||||
description: 班级账号、学生名单、班主任与协作教师,以及"谁能看什么、谁能改什么"。
|
||||
---
|
||||
|
||||
# 班级、学生与教师
|
||||
|
||||
## 班级账号:学生端的身份
|
||||
|
||||
学生**没有个人账号**。一台教室机器用一个**班级账号**登录:
|
||||
|
||||
```
|
||||
班级码(例如 G203) + 班级密码 → 这台机器代表"高二(3)班"
|
||||
```
|
||||
|
||||
登录之后,这台机器上的「已读 / 完成 / 未交」都是**代全班**操作的;它看到的是本班的数据。
|
||||
|
||||
班级码是管理员在「班级管理」里设的(新建时自动生成,可以改成好记的)。
|
||||
校验时会统一按大写比对(码存大写)。
|
||||
|
||||
> 班级密码在新建班级时由 `.env` 的 `DEFAULT_CLASS_PASSWORD` 决定初始值,
|
||||
> 之后由管理员在「班级管理 → 修改班级账号」维护。
|
||||
>
|
||||
> **客户端没有改密码入口**,这是刻意移除的:教室机器上误改班级密码会让别的机器登录不了。
|
||||
> 教师 / 管理员本人改密码在 Web 端顶栏头像菜单里。
|
||||
|
||||
## 学生
|
||||
|
||||
**学生管理**(仅管理员)→ 新建 / 导入名单。
|
||||
|
||||
学生记录**只是名单**:成绩、未交名单、叫人、已读记录都按名单记录,所以名单不全这几项就不准。
|
||||
|
||||
| 项目 | 说明 |
|
||||
| -------- | ------------------------------------------------------------------------------------ |
|
||||
| 用户名 | `^[A-Za-z0-9_.-]{3,32}$`;单个新建时自动生成 |
|
||||
| 密码 | **没有密码概念**。学生的 `passwordHash` 是空串占位,不能登录,也没有任何重置密码接口 |
|
||||
| 所属班级 | 一个学生属于一个主班级 |
|
||||
|
||||
> 用学生用户名去登录会被拒。就算拿教师 / 管理员账号去试,服务端也会在**密码校验之前**先判角色 ——
|
||||
> 否则"密码错(401)"和"密码对但学生账号已停用(403)"的差异会把学生账号密码变成可离线验证的预言机。
|
||||
|
||||
## 教师
|
||||
|
||||
| 能力 | 在哪 | 谁能做 |
|
||||
| -------------------- | ------------------------------------------------ | ------------- |
|
||||
| 新建 / 编辑 / 删除 | 「教师管理」 | **仅管理员** |
|
||||
| 导入名单(xlsx/csv) | 「教师管理 → 导入名单」 | **仅管理员** |
|
||||
| 重置密码 | 「教师管理」(可指定新密码,留空用默认初始密码) | **仅管理员** |
|
||||
| 加进某个班 | 班级详情 → 协作教师 | 管理员 / 教师 |
|
||||
| 换班主任 | 班级详情顶部「更换班主任」 | **仅管理员** |
|
||||
|
||||
> **教师录入只有管理员能做**:`/api/teachers` 整组接口都是 `requireRole('ADMIN')`,教师调用返回 403。
|
||||
> 前端也只在管理员登录时显示这个菜单项。
|
||||
|
||||
### 删除教师有护栏
|
||||
|
||||
下面四种情况一律拒绝(409),因为删账号会级联删掉他在各班发过的内容:
|
||||
|
||||
1. 他还是某个班的**班主任**;
|
||||
2. 名下有**课程**;
|
||||
3. 有**班级分配**(协作教师);
|
||||
4. **发布过作业或通知**。
|
||||
|
||||
(后两项曾经漏检,会静默删掉该教师发过的作业与通知。)
|
||||
|
||||
## 角色与可见范围
|
||||
|
||||
班级内的角色由「账号角色 + 与该班级的关系」共同决定:
|
||||
|
||||
| 角色 | 怎么判定 | 可见范围 |
|
||||
| ------------------ | ------------------------------ | -------------------------- |
|
||||
| `ADMIN` 管理员 | `User.role = ADMIN` | 全部班级 |
|
||||
| 班主任 `HEAD` | `Class.teacherId === 当前用户` | 本班 |
|
||||
| 科任老师 `SUBJECT` | `ClassTeacher` 里有当前用户 | 被分配的班级 |
|
||||
| 学生 `STUDENT` | `User.classId` / 班级账号 | 自己的班级(只读业务数据) |
|
||||
|
||||
### 权限矩阵(摘要)
|
||||
|
||||
| 操作 | 管理员 | 班主任 | 科任老师 |
|
||||
| ------------------------------------ | :----: | :----------: | :------: |
|
||||
| 班级创建 / 修改 / 删除 | ✅ | ❌ | ❌ |
|
||||
| 分配班主任与科任老师 | ✅ | ❌ | ❌ |
|
||||
| 学生名单管理(增删 / 导入) | ✅ | ❌ | ❌ |
|
||||
| 教师录入(新建 / 导入 / 改密码) | ✅ | ❌ | ❌ |
|
||||
| 数据库管理(备份 / 导入导出 / 切换) | ✅ | ❌ | ❌ |
|
||||
| 课表管理(增删改 / 导入) | ✅ | ✅(仅本班) | ❌ |
|
||||
| 布置作业 | ✅ | ✅ | ✅ |
|
||||
| 叫人 | ✅ | ✅ | ✅ |
|
||||
| 发布通知 | ✅ | ✅ | ✅ |
|
||||
| 成绩录入 / 修改 / 导入(仅本班) | ✅ | ✅(仅本班) | ❌ |
|
||||
| 维护班级密码 | ✅ | ❌ | ❌ |
|
||||
|
||||
完整矩阵与实现见[角色与权限](../dev/permissions.md)。
|
||||
|
||||
## 学期周数
|
||||
|
||||
每个班有自己的学期周数(`Class.termWeeks`,默认 20,上限 30),**只有管理员**能改。
|
||||
它决定"现在是第几教学周",见[课表](schedules.md)。
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
title: 成绩
|
||||
description: 成绩录入与导入、等级与得分率、学生端看到的全班总览。
|
||||
---
|
||||
|
||||
# 成绩
|
||||
|
||||
## 三种录入方式
|
||||
|
||||
Web 管理端「成绩录入」页:
|
||||
|
||||
| 方式 | 适合 | 说明 |
|
||||
| -------- | --------------------------------- | ---------------------------------------------------------------- |
|
||||
| 单条录入 | 个别缺漏补录 | 选学生、科目、考试名称、分数、总分 |
|
||||
| 批量录入 | 一次录一个班的某一科 | 一屏列出全班学生,逐个填分 |
|
||||
| 导入表格 | 已经有 Excel 的(期中期末最常见) | 支持列名同义词映射、先预览再提交,见[表格与课表导入](imports.md) |
|
||||
|
||||
**科目**下拉是全校统一的 18 科目录;选中一个该班还没有的科目时,前端会自动为该班建同名课程,
|
||||
不用先去把科目建一遍。
|
||||
|
||||
## 谁能录
|
||||
|
||||
| 角色 | 权限 |
|
||||
| --------------- | -------------------------------------- |
|
||||
| 管理员 | ✅ 全部班级 |
|
||||
| **本班班主任** | ✅ 仅本班(录入 / 修改 / 删除 / 导入) |
|
||||
| 科任老师 | ❌ 403 |
|
||||
| 学生 / 班级账号 | ❌ 只能看 |
|
||||
|
||||
权限在**服务层**兜底,前端只是按同一份矩阵隐藏按钮。
|
||||
|
||||
## 学生看到的是什么
|
||||
|
||||
学生端(教室机器)用的是**班级账号**,所以它看到的不是"我的成绩",而是**全班成绩总览**:
|
||||
|
||||
- 表格:分数 / **得分率** / **等级**;
|
||||
- 柱状图;
|
||||
- 等级分布;
|
||||
- 最近更新。
|
||||
|
||||
图表是自研的轻量组件(柱状图是纯 HTML/CSS,折线图是 SVG),项目里**不引图表库**。
|
||||
|
||||
> 学生没有个人账号,所以系统里不存在"某个学生登录后看自己的成绩"这条路径。
|
||||
> 成绩记录本身是按学生名单存的(`Grade.userId`),只是入口统一在班级账号上。
|
||||
|
||||
## 接口
|
||||
|
||||
| 方法 | 路径 | 权限 |
|
||||
| -------------- | ---------------------------------------------- | --------------------------- |
|
||||
| GET | `/grades/my` | 登录(班级会话 → 全班总览) |
|
||||
| GET | `/grades?classId=&courseId=&userId=&examName=` | 教师 / 管理员 |
|
||||
| GET | `/grades/stats?classId=&courseId=&examName=` | 教师 / 管理员 |
|
||||
| POST | `/grades`、`/grades/bulk` | 班主任 / 管理员 |
|
||||
| PATCH / DELETE | `/grades/:id` | 班主任 / 管理员 |
|
||||
|
||||
写入后会广播 `grade:updated`,客户端立刻刷新。
|
||||
@@ -0,0 +1,92 @@
|
||||
---
|
||||
title: 作业
|
||||
description: 作业按所属日期归类、看板与全屏看板、教室机器录入作业、未交名单与附件。
|
||||
---
|
||||
|
||||
# 作业
|
||||
|
||||
## 按「所属日期」归类,不是按录入时间
|
||||
|
||||
每份作业有一个**所属日期**(`Homework.assignDate`,形如 `2026-10-06` 的本地日期,不带时区)。
|
||||
按天查看、日期高亮、客户端的「只看今天」都看它。
|
||||
|
||||
> 用录入时间(`createdAt`)当"哪一天的作业"是错的:它是 UTC,晚上 8 点留的作业会被算到第二天。
|
||||
> 系统里统一用「本地日期」,新增按天查询的接口也一样走 `?date=YYYY-MM-DD`。
|
||||
|
||||
所以老师可以给今天、明天、甚至补录上周某一天 —— 客户端按日期查得到。
|
||||
|
||||
## 两端各自能做什么
|
||||
|
||||
| 能力 | Web 管理端(老师) | 桌面客户端(教室机器) |
|
||||
| ------------------ | ---------------------------------------- | ----------------------------------------------- |
|
||||
| 按天查看 | 工具栏日期选择器;有作业的日期下面带圆点 | 日期选择器 + 前 / 后一天,支持 `?date=` 深链 |
|
||||
| 发布 / 编辑 / 删除 | ✅(教师 / 管理员) | ✅ **录入**(班级账号也能发,归属到该班班主任) |
|
||||
| 看板模式 | — | ✅ 默认视图 |
|
||||
| 全屏看板 | — | ✅ |
|
||||
| 完成情况 | ✅ 按名单统计完成人数 | ❌ **客户端不显示"已完成 / 待完成"** |
|
||||
| 未交名单 | ✅ 作业详情抽屉里可看可改 | ✅ |
|
||||
|
||||
> 客户端刻意不显示完成状态:完成与否是**老师端与通知中心**的概念;
|
||||
> 教室机器上只做"看作业 + 录作业"两件事。服务端的完成数据与统计接口都还在,Web 端照旧可用。
|
||||
|
||||
## 看板模式
|
||||
|
||||
客户端作业页默认按**科目**分卡片,每张卡列出该科的所有作业。工具栏可以:
|
||||
|
||||
- **看板 / 列表** 切换视图(列表模式有一列"作业要求"全文);
|
||||
- **只看今天** 开关(默认开);
|
||||
- **显示时间** 开关;
|
||||
- **字号**滑块(11–28px 即时生效)。
|
||||
|
||||
这些偏好存在客户端配置里,重启后仍生效。
|
||||
|
||||
卡片里同时渲染**标题 + 作业要求全文**(不截断)。
|
||||
|
||||
### 全屏看板
|
||||
|
||||
点卡片或「全屏看板」进入。全屏下的排版不是简单地放大:
|
||||
|
||||
1. 按"可用宽 × 可用高"枚举 1..N 列,取**单卡面积最大、宽高比最接近 1.15** 的方案;
|
||||
2. 再按「内容高度 / 可用高度」整体 `transform: scale()` 缩到刚好铺满 —— 所以**不用上下翻页**;
|
||||
3. 标题栏中间绝对居中显示当天日期时间(受「显示时间」开关控制,每分钟刷新)。
|
||||
|
||||
> 字号仍由工具栏的滑块决定,缩放只负责"放得下"。工具栏上会显示当前缩放比例。
|
||||
|
||||
## 教室机器录入作业
|
||||
|
||||
教室里那台机器用**班级账号**登录,可以直接发作业。两个细节:
|
||||
|
||||
- 服务端拿到的是班级账号的会话(`sub` 是班级 id),写 `createdBy` 前会换成该班班主任,
|
||||
否则会撞外键。
|
||||
- **本机刚录入的那条不会弹到灵动岛上**——自己通知自己没意义。这条判断是在发请求**之前**
|
||||
按内容登记指纹做的(服务端是先广播后回响应,等拿到 id 再登记就晚了)。
|
||||
|
||||
录入时可以点**快捷短语**(默认 `P` / `大本` / `小本` / `卷子` / `背诵` / `默写` / `听写` / `预习` /
|
||||
`订正` / `读书`),点一下追加到标题或内容。短语可以在客户端「设置 → 通用」里增删,存在本机。
|
||||
|
||||
## 未交名单
|
||||
|
||||
在作业列表或详情里点「未交名单」→ 勾选**没交作业**的同学 → 保存。
|
||||
|
||||
其余同学会被自动标记为已完成(接口是 `PATCH /api/homeworks/:id/submissions`)。
|
||||
Web 端作业详情抽屉里同样可看可改。
|
||||
|
||||
> 名单来自班级的学生名单 —— 名单不全,这项就不准。见[班级、学生与教师](classes.md)。
|
||||
|
||||
## 附件
|
||||
|
||||
作业可以带一个附件链接。**只接受 `http(s)://` 或站内相对路径**,其他 scheme(`javascript:`、
|
||||
`data:text/html` 等)服务端会直接拒;Web 端渲染前还会再兜一层。
|
||||
放行这些 scheme 等于给了一条存储型 XSS。
|
||||
|
||||
## 实时推送
|
||||
|
||||
发布 / 修改 / 删除作业会分别广播:
|
||||
|
||||
| 事件 | 客户端反应 |
|
||||
| ------------------ | ------------------------------------- |
|
||||
| `homework:new` | 作业列表刷新 + 灵动岛弹「新作业」胶囊 |
|
||||
| `homework:updated` | 刷新;带 `deleted: true` 表示删除 |
|
||||
| `homework:status` | 完成状态变化时刷新统计 |
|
||||
|
||||
课表、作业、通知、成绩都是同一个套路:写库 → 按班级房间广播 → 客户端更新内存状态并写回 IndexedDB。
|
||||
@@ -0,0 +1,115 @@
|
||||
---
|
||||
title: 表格与课表导入
|
||||
description: 成绩、学生名单、教师名单的表格导入,以及从 ClassIsland 档案导入节次时间与课表。
|
||||
---
|
||||
|
||||
# 表格与课表导入
|
||||
|
||||
五条导入链路**都在服务端完成解析、校验与去重**,前端只负责收集文件与展示结果。
|
||||
所以 Web 端、客户端与直接调接口拿到的结果是同一套规则、同一份统计。
|
||||
|
||||
## 通用流程
|
||||
|
||||
表格类导入都是三段式(成绩 / 学生 / 教师都一样):
|
||||
|
||||
1. **下载模板** —— `GET /imports/template?kind=grades&format=xlsx|csv`。
|
||||
CSV 带 BOM,Excel 直接双击打开不乱码。
|
||||
2. **上传预览** —— 服务端返回列名、前 20 行、总行数与**建议的字段映射**,以及校验问题。
|
||||
列名按同义词自动映射(「学号→学生用户名」「得分→分数」「满分→总分」……),也可以手动改。
|
||||
**缺必填列不会静默通过**,会直接给出「缺少必填列「…」」。
|
||||
3. **确认导入** —— 按映射与写入模式提交:
|
||||
|
||||
| 模式 | 已存在的记录 |
|
||||
| -------- | ------------ |
|
||||
| `upsert` | 更新(默认) |
|
||||
| `append` | 跳过 |
|
||||
|
||||
返回 `{ total, inserted, updated, skipped, failed, errors[], warnings[] }`。
|
||||
`errors[].row` 是 **Excel 视角的行号(含表头)**,方便老师改完再导。
|
||||
|
||||
**体积限制**:文件上限 **8MB**,请求体上限 12MB(文件以 base64 放进 JSON,约 4/3 倍)。
|
||||
超限返回 413 `IMPORT_TOO_LARGE` 与中文提示 —— 如果你看到一句英文的「request entity too large」,
|
||||
那说明这个异常没被映射,属于 bug。
|
||||
|
||||
## 1. 成绩表格导入
|
||||
|
||||
- 入口:成绩录入 → 「导入表格」
|
||||
- 模板列:`学生用户名 / 学生姓名 / 考试名称 / 分数 / 总分 / 课程`
|
||||
- 支持 `.xlsx / .xls / .csv / .tsv`(自动识别分隔符)
|
||||
- **重复判定**:同班级 + 同学生 + 同考试 + 同课程
|
||||
- 学生按「用户名」优先、「姓名」兜底匹配
|
||||
|
||||
## 2. 学生名单导入
|
||||
|
||||
- 入口:学生管理 → 「导入名单」(仅管理员)
|
||||
- 模板列:`用户名 / 姓名` —— 学生没有账号属性,**没有密码列**
|
||||
- 用户名必须匹配 `^[A-Za-z0-9_.-]{3,32}$`
|
||||
- 用户名已经被某个教师 / 管理员账号占用时,**这一行进错误列表**,不会覆盖他人账号
|
||||
|
||||
## 3. 教师名单导入
|
||||
|
||||
- 入口:教师管理 → 「导入名单」(仅管理员)
|
||||
- 模板列:`用户名 / 姓名 / 初始密码 / 角色`
|
||||
- 角色取 `ADMIN` / `TEACHER`,留空按 `TEACHER`
|
||||
- 与新建教师同一套校验,导入完可以直接登录
|
||||
|
||||
## 4. 从 ClassIsland 导入课程表
|
||||
|
||||
**这是最省事的一条路**:如果教室里已经在用 ClassIsland,直接导入它的档案 JSON,
|
||||
课表、节次时间、单双周会一起进来,不用排两遍。
|
||||
|
||||
- 入口:课表管理 → 「导入 ClassIsland 课程表」
|
||||
- 输入:同一份档案 JSON(`ClassPlans` / `TimeLayouts` / `Subjects` 三个字典),
|
||||
粘贴或选择 `.json` 文件
|
||||
|
||||
### 真实档案的形态
|
||||
|
||||
ClassIsland 2.x 的档案在 `Profiles/<档案名>.json`,字段是 PascalCase:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"TimeLayouts": {
|
||||
"<guid>": { "Name": "…", "Layouts": [{ "StartTime": "08:00:00", "EndTime": "08:45:00", "TimeType": 0 }] },
|
||||
},
|
||||
"ClassPlans": { "<guid>": { "Name": "…", "Classes": [/* 与上课时间点逐位对齐 */] } },
|
||||
"Subjects": { "<guid>": { "Name": "语文" } },
|
||||
}
|
||||
```
|
||||
|
||||
解析器同时兼容顶层数组、`{ TimeLayouts: [...] }`、`{ items | Items | Layouts | TimeLayout: [...] }`
|
||||
与单个时间表对象。
|
||||
|
||||
| 细节 | 说明 |
|
||||
| ------------ | ----------------------------------------------------------------------------------------------------------------------- |
|
||||
| 时间字段 | `StartTime` / `EndTime`(`"08:00:00"`);旧版 `StartSecond` / `EndSecond`(ISO 字符串或秒数);`"8:0"` / `"08:00"` 都认 |
|
||||
| `TimeType` | `0=上课`、`1=课间`、`2=分割线`、`3=行动`,与 ClassIsland 一致 |
|
||||
| **节次对齐** | `ClassPlan.Classes[i]` 对应 `Layouts` 里**第 i 个 `TimeType === 0` 的时间点**(课间、分割线不占位) |
|
||||
| 单双周 | `TimeRule.WeekCountDiv`(`0=每周`、`1=单周`、`2=双周`)+ `WeekCountDivTotal`(默认 2) |
|
||||
| 星期换算 | ClassIsland 是 `0=周日 … 6=周六`,导入时换算成 `1=周一 … 7=周日` |
|
||||
| 科目映射 | `Subjects[<guid>].Name`;该班还没有的科目**自动补建课程**(任课老师取该班班主任),预览里会提前列出 |
|
||||
| 写入模式 | `replace` 清空该班课表后写入(事务内完成)/ `merge` 按「星期 + 开始时间 + 单双周」去重 |
|
||||
|
||||
**解析出 `errors` 时返回 400 且数据库完全不写入**(`IMPORT_INVALID` / `IMPORT_EMPTY`)——
|
||||
先保证不写坏,再谈导入。
|
||||
|
||||
时间重叠、分割线没有时间这类问题只作 `warnings`,不阻断导入。
|
||||
|
||||
> **3 周及以上的轮换**没法用单双周表达,会给出 warning 并按"每周"处理。
|
||||
|
||||
## 5. 只导入节次时间(旧入口)
|
||||
|
||||
「导入时间配置」按钮**已经下线** —— 因为同一份档案 JSON 里的 `TimeLayouts` 已经跟着课表一起进来了。
|
||||
组件与接口(`/imports/time-layout/*`)仍然保留给旧脚本调用。
|
||||
|
||||
它的写入模式也是 `replace`(整体替换该班同名配置)与 `merge`(以**开始时间**为槽位标识合并,
|
||||
同槽位被导入项覆盖,其它旧节次保留,不静默丢数据)。
|
||||
|
||||
## 相关接口
|
||||
|
||||
| 路径 | 说明 |
|
||||
| ------------------------------------------------------ | ------------------------------------------ |
|
||||
| `/imports/template?kind=&format=` | 下载模板(`csv` 走 JSON,`xlsx` 走二进制) |
|
||||
| `/imports/table/preview` | 上传解析预览(base64) |
|
||||
| `/imports/table/commit` | 按映射与模式导入 |
|
||||
| `/imports/class-plan/preview`、`/imports/class-plan` | ClassIsland 课程表预览 / 导入 |
|
||||
| `/imports/time-layout/preview`、`/imports/time-layout` | 时间配置预览 / 导入(旧入口) |
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
title: 应用帮助
|
||||
description: 班级小助手每一项功能怎么用:课表、作业、通知、叫人、成绩、班级、灵动岛、外观与导入。
|
||||
---
|
||||
|
||||
# 应用帮助
|
||||
|
||||
这一章按功能讲,重点是**行为上的约定**——什么时段会弹、谁看得见、什么情况下不动。
|
||||
很多"看起来像故障"的现象其实是设计行为,下面每一页都会点出来。
|
||||
|
||||
| 页面 | 讲什么 |
|
||||
| --------------------------------- | ------------------------------------------------------ |
|
||||
| [课表](schedules.md) | 周视图与「今天」时间轴、单双周、学期周数、课表来源 |
|
||||
| [作业](homeworks.md) | 按所属日期归类、看板与全屏看板、教室机器录入、未交名单 |
|
||||
| [通知与提醒](notifications.md) | 三档优先级、上课时段策略、二次确认、已读与红点 |
|
||||
| [叫人](calls.md) | 普通叫人与紧急叫人的差别、快捷短语、谁能叫谁 |
|
||||
| [成绩](grades.md) | 录入方式、导入、等级分布、学生看到的是什么 |
|
||||
| [班级、学生与教师](classes.md) | 班级账号、名单、班主任与协作教师、谁看得见什么 |
|
||||
| [灵动岛](island.md) | 胶囊与卡片、多条通知的竖排列表、停靠与外观、触摸屏 |
|
||||
| [外观、主题与动效](appearance.md) | 深浅色、设计令牌、动效语言、降低动态效果 |
|
||||
| [表格与课表导入](imports.md) | 成绩 / 名单 / 教师表格导入与 ClassIsland 档案导入 |
|
||||
|
||||
## 贯穿所有功能的三条规则
|
||||
|
||||
**一、角色决定可见范围。** 同一个页面,管理员、班主任、科任老师看到的数据与能点的按钮都不一样。
|
||||
前端会按权限隐藏入口,但**真正拦人的是服务端**——直接调接口一样会被拒。完整矩阵在
|
||||
[角色与权限](../dev/permissions.md)。
|
||||
|
||||
**二、上课时段不打扰课堂。** 「上课时段」由课表实时判定,普通通知与普通叫人只进队列、下课补弹;
|
||||
紧急通知与紧急叫人才会立刻打断。规则只有一份,客户端与 ClassIsland 都遵守它。
|
||||
细节见[通知与提醒](notifications.md)。
|
||||
|
||||
**三、实时推送只走班级房间。** 老师一发内容,服务端写库后按班级房间广播,客户端立刻更新,
|
||||
不靠轮询。教室机器断线重连后会自动补上漏掉的数据,灵动岛也会把上课期间暂存的通知补弹出来。
|
||||
|
||||
## 两端的差异
|
||||
|
||||
同一件事在 Web 管理端和桌面客户端上不一定完全一样,这是有意的:
|
||||
|
||||
| 事情 | Web 管理端(老师) | 桌面客户端(教室机器) |
|
||||
| ---------- | ------------------------------- | --------------------------------------------------------- |
|
||||
| 作业 | 发布 / 编辑 / 删除 / 看未交名单 | 看作业 + **录入作业** + 勾未交名单,不显示"完成 / 待完成" |
|
||||
| 成绩 | 录入 / 导入 / 改 / 删,带统计 | 只看全班总览 |
|
||||
| 课表 | 排课、导入、改 | 只看(周视图 + 今天时间轴) |
|
||||
| 通知 | 发布 / 删除 / 看已读 | 看、标为已读 |
|
||||
| 班级与人员 | 管理员可管 | 只能看本班 |
|
||||
|
||||
> 客户端没有"修改密码"入口,是**刻意移除的**:防止教室机器上误改班级密码后,
|
||||
> 别的教室机器被锁在门外。班级密码由管理员在 Web 端维护。
|
||||
@@ -0,0 +1,121 @@
|
||||
---
|
||||
title: 灵动岛
|
||||
description: 桌面客户端的通知浮窗:胶囊与卡片、多条通知的竖排列表、停靠与外观、触摸屏下的行为。
|
||||
---
|
||||
|
||||
# 灵动岛
|
||||
|
||||
灵动岛是桌面客户端上那个独立于主窗口的浮窗:无边框、透明、置顶、不占任务栏。
|
||||
它是「消息到达的第一现场」—— 老师发的内容先在岛上出现,学生点开才进主界面。
|
||||
|
||||
视觉参照 [WinIsland](https://github.com/WinIslandProject/WinIsland):
|
||||
连续圆角(超椭圆)而不是 `border-radius`、纯黑 / 毛玻璃 / 主题色三种底、Apple 系统色强调色、
|
||||
6 个停靠位、可选的「空闲细缝」。逐项设计令牌见[灵动岛设计令牌](../dev/winisland-design-tokens.md)。
|
||||
|
||||
## 各形态长什么样
|
||||
|
||||
| 形态 | 截图 | 说明 |
|
||||
| ---------------------- | ----------------------------------------------------------------- | ------------------------------------------------- |
|
||||
| 非上课时段的「新消息」 |  | 268×44,直接出现(无入场动画),点击后展开 |
|
||||
| 点开后的详情卡 |  | 标题 / 内容 / 时间 + 打开应用 / 标为已读 / 知道了 |
|
||||
| 下课后自动弹出 |  | 上课期间暂存的通知,下课后自动弹出 |
|
||||
| 上课期间的紧急通知 |  | 卡片**内部**红色光晕 + 紧急角标,无需点击 |
|
||||
| 新作业 |  | 青蓝描边 + 书本图标 |
|
||||
| 叫人 |  | 琥珀金卡片 + 「叫人」徽标 + 「收到」按钮 |
|
||||
|
||||
## 什么时候弹、弹成什么样
|
||||
|
||||
| 场景 | 灵动岛行为 |
|
||||
| --------------------------------- | --------------------------------------------------------------------------------------------- |
|
||||
| 收到通知(非上课时段) | 直接出现胶囊:`新消息 · 共 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 重建渲染表面,首帧可能被当成空内容画出去。
|
||||
- **收回一瞬间闪一下**:多半是**自动化验证实例的叠影**,不是产品的问题 ——
|
||||
冒烟会真的在屏幕上放一个灵动岛,如果它和你自己的客户端叠在一起,你看到的是两个窗口。
|
||||
(验证实例的岛是洋红主题色,一眼可辨。)
|
||||
@@ -0,0 +1,91 @@
|
||||
---
|
||||
title: 通知与提醒
|
||||
description: 通知的三档优先级、上课时段策略、紧急通知的二次确认、提醒弹在哪个端、已读与红点。
|
||||
---
|
||||
|
||||
# 通知与提醒
|
||||
|
||||
通知是这套系统里规则最多的一项 —— 因为它要在"老师想说"和"别打断上课"之间做取舍。
|
||||
这一页把规则一次讲清楚。
|
||||
|
||||
## 三档优先级
|
||||
|
||||
| 优先级 | 学生端表现 |
|
||||
| ------ | -------------------------------------------------------------------------------------------- |
|
||||
| 普通 | 课间弹「新消息」胶囊,点击展开详情;**上课时段完全不显示**(窗口隐藏)并进队列,下课自动补弹 |
|
||||
| 重要 | 同上,卡片上有优先级标签 |
|
||||
| 紧急 | **无论是否上课都立即展开**,卡片内部红色呼吸光晕 + 「紧急」角标,无需点击,45 秒后收起 |
|
||||
|
||||
紧急通知的光晕画在**卡片内部**(`inset` 阴影),因为窗口比卡片只大 2~4px,向外的阴影会被裁成硬边。
|
||||
|
||||
## 上课时段策略
|
||||
|
||||
「上课时段」由课表实时判定(见[课表](schedules.md))。判定之后:
|
||||
|
||||
| 学生端行为 | 上课时段 | 课间 / 放学 |
|
||||
| ---------- | ---------------------------------------- | ----------- |
|
||||
| 普通通知 | 收着不弹(窗口隐藏),进待发队列 | 直接弹 |
|
||||
| 紧急通知 | **立刻弹** | 立刻弹 |
|
||||
| 任何点击 | 一律不响应(不会展开、也不会回缩出胶囊) | 正常 |
|
||||
|
||||
ClassIsland 那一端遵守同一套规则,否则会出现"学生在客户端看不到、却在教室大屏上被打断"。
|
||||
|
||||
## 紧急通知的二次确认
|
||||
|
||||
上课时段发紧急通知,**服务端会拦一次**(`409 URGENT_DURING_CLASS`):
|
||||
|
||||
```
|
||||
POST /api/notifications { classId, title, content, priority: "URGENT" }
|
||||
→ 409 URGENT_DURING_CLASS(响应体里带着正在上的那节课)
|
||||
|
||||
POST /api/notifications { …, priority: "URGENT", confirmDuringClass: true }
|
||||
→ 201 发出去
|
||||
```
|
||||
|
||||
Web 端在提交前会先查一次上课状态,命中就弹一个**全屏二次确认**:全屏遮罩 + 强警示配色,
|
||||
文案是"现在为上课时间段……" + 正在上的课时段 + 待发布的标题。确认按钮带 **3 秒倒计时**
|
||||
(圆环进度),倒计时结束前点不动,防误触。
|
||||
|
||||
**这个拦截在服务端,不是前端提醒。** 绕过界面直接调接口一样会被拦 —— 这是产品行为,不是可以关掉的提示。
|
||||
|
||||
## 提醒弹在哪个端
|
||||
|
||||
教室机器上的客户端「设置 → 提醒」里选,选择会同步到班级记录:
|
||||
|
||||
| 选择 | ClassHelper 客户端 | ClassIsland |
|
||||
| ----------------------------------------- | ------------------------------ | ----------- |
|
||||
| `ClassHelper 与 ClassIsland 都弹`(默认) | 弹窗 + 灵动岛 | 全屏提醒 |
|
||||
| `只在 ClassHelper 客户端弹` | 弹窗 + 灵动岛 | 不推送 |
|
||||
| `只在 ClassIsland 上弹` | 只进通知中心(不弹窗、不上岛) | 全屏提醒 |
|
||||
|
||||
这条规则**任何入口都遵守**,包括 ClassIsland 联动页的「下发提醒」——
|
||||
那种情况下会回一句"该班教室已设置为只在 ClassHelper 客户端显示"。
|
||||
|
||||
管理员与教师也可以用 `PATCH /api/classes/:id/notification-channel` 改,但默认由教室机器自己定。
|
||||
|
||||
## 已读、红点与删除
|
||||
|
||||
| 能力 | 说明 |
|
||||
| -------- | ------------------------------------------------------------------------------ |
|
||||
| 未读红点 | 客户端侧边栏「通知」项右上角的徽标;数字来自 `GET /notifications/unread-count` |
|
||||
| 标为已读 | 点开通知自动标为已读;也有「全部已读」 |
|
||||
| 班级设备 | 教室机器的"已读"是**代全班**操作的(它用的是班级账号) |
|
||||
| 删除 | 只有教师 / 管理员能删 |
|
||||
|
||||
## 发布入口只有一个实现
|
||||
|
||||
「通知发布」页、叫人、ClassIsland 联动页的「下发提醒」三个入口,
|
||||
**共用同一份 ClassIsland 推送实现**。所以不存在"某个页面发的通知不联动"这种事 ——
|
||||
以前有过,现在是硬约定:新增发通知的入口必须复用同一份代码。
|
||||
|
||||
## 接口
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| ------ | -------------------------------------------------------- | ------------------------------- |
|
||||
| GET | `/notifications?classId=&priority=&unreadOnly=&keyword=` | 列表(带已读状态) |
|
||||
| GET | `/notifications/unread-count` | 未读数 |
|
||||
| POST | `/notifications` | 发布(广播 `notification:new`) |
|
||||
| POST | `/notifications/:id/read`、`/notifications/read-all` | 标记已读 |
|
||||
| DELETE | `/notifications/:id` | 删除 |
|
||||
|
||||
完整清单见 [REST API](../dev/api.md)。
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
title: 课表
|
||||
description: 周课表与「今天」时间轴、单双周、学期周数、课表来源,以及上课时段是怎么判定的。
|
||||
---
|
||||
|
||||
# 课表
|
||||
|
||||
课表不只是"给学生们看看"的表格 —— **「上课时段」的判定完全基于它**。
|
||||
课表排错,提醒就会在该弹的时候不弹。所以这一页值得读完。
|
||||
|
||||
## 两个视图
|
||||
|
||||
| 视图 | 在哪 | 内容 |
|
||||
| -------------- | ----------------------------------------- | -------------------------------------------------------- |
|
||||
| **周视图** | Web 端「课表管理」、客户端「课表 → 本周」 | 节次 × 星期 的 7 列表格 |
|
||||
| **今天时间轴** | 客户端「课表 → 今天」 | 大时钟 + 当前状态卡 + 时间轴(已结束 / 正在上 / 下一节) |
|
||||
|
||||
「今天」时间轴是客户端默认打开的那一屏:
|
||||
|
||||
- **大时钟**:时:分(大字)+ 秒(小字),下面一行是日期 / 星期 / 第 N 教学周。
|
||||
- **当前状态卡**:「正在上 XXX」+ 时段地点 + **距下课 X 分钟**;没课时显示"今天的课都上完了"或"今天没有课程安排"。
|
||||
- **时间轴**:左侧时间栏(开始 / 结束)+ 竖向轨道与节点 + 右侧课卡。已结束的置灰、
|
||||
正在上的用强调色实底 + 进度条 + 距下课倒计时、下一节用橙色标记 + "还有多久开始"。
|
||||
|
||||
## 单双周
|
||||
|
||||
一门课可以设成**每周**(默认)、**单周**或**双周**。判定口径是 **第 1 周 = 单周**。
|
||||
|
||||
从 ClassIsland 导入时,`WeekCountDiv`(`0=每周 / 1=单周 / 2=双周`)会换算成这套 `ALL / ODD / EVEN`。
|
||||
**3 周及以上的轮换没法用单双周表达**,导入时会给出 warning 并按"每周"处理 ——
|
||||
如果学校里真的是三周轮换,目前只能用两份课表 + 手工切换的方式绕过。
|
||||
|
||||
## 学期周数
|
||||
|
||||
每个班有自己的**学期周数**(`Class.termWeeks`,默认 20,**上限 30**),只有管理员能改。
|
||||
|
||||
它决定三件事:课表的周次下拉能选到第几周、`weekEnd` 的默认值、以及"现在是第几教学周"。
|
||||
周次换算的起点是服务端 `.env` 里的 `TERM_START_DATE`(当学期第 1 教学周的周一)。
|
||||
|
||||
> **`TERM_START_DATE` 设错,整套周次都错。** 它不在界面上改,要改 `.env` 后重启服务端。
|
||||
> 上限三处必须一致(`MAX_TERM_WEEK`、周次校验、班级设置),曾经出现过"班级能设到 40 周、
|
||||
> 而课表只到 30 周"的错位。
|
||||
|
||||
## 上课时段是怎么判定的
|
||||
|
||||
服务端有一个接口专门回答"这个班现在是不是在上课":
|
||||
|
||||
```
|
||||
GET /api/schedules/current?classId=<班级 id>[&at=<ISO 时间>]
|
||||
```
|
||||
|
||||
返回 `{ inClass, current, next, week, serverTime }`。`at` 参数可以覆盖判定时刻,用来联调与测试。
|
||||
|
||||
判定结果被用在四处,规则完全一致:
|
||||
|
||||
1. 客户端灵动岛:上课时段普通通知**不显示**(窗口直接隐藏)并进队列,下课自动补弹;
|
||||
2. ClassIsland 插件:同样暂存,下课后补弹;
|
||||
3. 服务端发布接口:上课时段发紧急通知会被拦(`409 URGENT_DURING_CLASS`),确认后放行;
|
||||
4. Web 端:提交紧急通知前先查一次状态,命中就弹全屏二次确认。
|
||||
|
||||
## 课表的"来源"字段
|
||||
|
||||
每一行课表都记着它是从哪来的:
|
||||
|
||||
| 来源 | 什么时候产生 | 会被自动清理吗 |
|
||||
| ------------- | ---------------------------------------------- | ---------------------------- |
|
||||
| `manual` | Web 端手工录入、从 ClassIsland JSON 一次性导入 | **不会**,永远保留 |
|
||||
| `classisland` | 教室插件上报同步过来的 | 会——本次没再上报的行会被删掉 |
|
||||
|
||||
这条规则解决了"老师在 ClassIsland 里删掉一节,小助手这边却永远留着"的幽灵课问题,
|
||||
同时保证老师在小助手这边手排的课不会被插件覆盖掉。
|
||||
|
||||
> 去重键是「星期 + 开始时间 + 单双周」。如果老师手排的某一节恰好和上报的同一时间槽重合,
|
||||
> 那一行会被上报"收养"(保留原本的 `manual` 来源),下一次清理不会删它。
|
||||
|
||||
## 排课 / 改课 / 导入
|
||||
|
||||
- 手工排课在 **课表管理**(管理员 / 本班班主任)。科目下拉是全校统一的 18 科目录,
|
||||
选中一个该班还没有的科目会自动建课。
|
||||
- 从 ClassIsland 档案导入最省事,课表、节次时间、单双周会一起进来,
|
||||
见[表格与课表导入](imports.md)。
|
||||
- 课表变更后会广播 `schedule:updated`,客户端与灵动岛立刻刷新。
|
||||
@@ -0,0 +1,20 @@
|
||||
- name: 应用帮助
|
||||
href: index.md
|
||||
- name: 课表
|
||||
href: schedules.md
|
||||
- name: 作业
|
||||
href: homeworks.md
|
||||
- name: 通知与提醒
|
||||
href: notifications.md
|
||||
- name: 叫人
|
||||
href: calls.md
|
||||
- name: 成绩
|
||||
href: grades.md
|
||||
- name: 班级、学生与教师
|
||||
href: classes.md
|
||||
- name: 灵动岛
|
||||
href: island.md
|
||||
- name: 外观、主题与动效
|
||||
href: appearance.md
|
||||
- name: 表格与课表导入
|
||||
href: imports.md
|
||||
Reference in new issue
Block a user