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 表格的共同依据。
This commit is contained in:
1 parent
ded10118f5
commit
0ea6bd887f
15 files changed
+1235
-167
No files matched your search
@@ -1,4 +1,4 @@
|
||||
# AGENTS.md — 班级小助手(Class Helper)
|
||||
# AGENTS.md — ClassHelper(Class Helper)
|
||||
|
||||
本文件写给在本仓库里干活的 AI 编码代理。先读这里,再动代码。
|
||||
文中「本机环境」「已知坑」两节按**实测**写成;若与本机现状不符,以本机实测为准并顺手改掉这里。
|
||||
@@ -28,7 +28,7 @@
|
||||
▼
|
||||
Socket.IO ──► class:{classId} 房间 ──► 学生桌面客户端实时更新 UI
|
||||
|
||||
ClassIsland(教室机器)──► 班级小助手联动插件 ──► /api/integrations/classisland/*(设备令牌鉴权)
|
||||
ClassIsland(教室机器)──► ClassHelper联动插件 ──► /api/integrations/classisland/*(设备令牌鉴权)
|
||||
▲ │
|
||||
└──── 老师下发的提醒(上报返回值里顺带带回)◄┘
|
||||
```
|
||||
@@ -73,18 +73,24 @@ docs/ **文档站**(docfx 项目;不进 pnpm workspac
|
||||
docfx.json / toc.yml(顶栏)+ 各章节自己的 toc.yml(侧栏)
|
||||
get-started/ app/ management/ dev/(四章内容)
|
||||
management/{production,linux-deploy,mysql}.md(部署与换库)
|
||||
dev/winisland-design-tokens.md / screenshots/(回归留档的截图)
|
||||
dev/winisland-design-tokens.md
|
||||
images/(**展示图**:文档站 / README / 官网共用的同一份,1× 逻辑像素)
|
||||
screenshots/(回归留档的截图:island / client / web-mobile / classisland,
|
||||
页面不展示;两者分工见 docs/dev/testing.md「截图留档」)
|
||||
templates/classhelper/(模板覆盖:_master.tmpl + public/main.css + 图标)
|
||||
build.mjs(构建,注入版本号与 404 的 base)/ serve.mjs(本地预览 :5181)
|
||||
_site/(构建产物,gitignored)
|
||||
⚠ 站点的顶栏版本徽标取自根 package.json(build.mjs 用 --metadata 注入),
|
||||
不要在文档里再写一份版本号。
|
||||
⚠ 页面里的照片一律指向 images/ —— 它就是 README 与官网吃的那一份。
|
||||
别再往 screenshots/ 引图:那是冒烟脚本滚动的留档,随时被下一次验证覆盖。
|
||||
⚠ 模板目录里的 logo.png / favicon.png 由 `pnpm icons` 派生(见 §5 第 62 条),
|
||||
别手工替换;改图标只改 build/classhelper.png 再跑 pnpm icons。
|
||||
⚠ 文档站只发布 docs/ 这一棵树,页面里**不要**写指向 docs/ 之外的相对链接
|
||||
(如 ../AGENTS.md)—— 发布出去全是 404,要用 GitHub 的绝对 URL。
|
||||
website/ 产品官网(**纯静态、无构建步骤**,不进 pnpm workspace)
|
||||
index.html / assets/{styles.css,motion.js,main.js,icon.png,shots/}
|
||||
index.html(门面)/ download.html(下载页,首页的「立即下载」跳到它)
|
||||
assets/{styles.css,motion.js,main.js,download.js,icon.png,shots/}
|
||||
serve.mjs(本地预览)/ tools/capture-shots.mjs(实机图采集)
|
||||
404.html(**站点根** 404,含 {{BASE}} 占位符,由 build-pages.mjs 注入)
|
||||
⚠ serve.mjs 把 ../docs/_site 挂在 /docs/ 下,为的是让本地与线上同形 ——
|
||||
@@ -93,11 +99,19 @@ website/ 产品官网(**纯静态、无构建步骤**,
|
||||
(main.js 会跳过它)。拆成 per-char 的 inline-block 后每个字各自建一层合成,
|
||||
祖先的 background-clip: text 就穿不过去,渐变会整行失效。
|
||||
⚠ assets/motion.js 是动效层:曲线与弹簧 token 逐条取自 beUI
|
||||
(beui.dev/components/motion,见 README §7「官网」)。
|
||||
(beui.dev/components/motion,见 docs/reference.md 的「官网」一节)。
|
||||
改官网动效请改 token 表,不要在组件里另写 duration / cubic-bezier。
|
||||
⚠ assets/shots/ 里全是**跑起来截的**实机图,重采集步骤见 README
|
||||
§7「采集实机图」;客户端那两张要配合冒烟的
|
||||
⚠ assets/shots/ 里全是**跑起来截的**实机图,重采集步骤见
|
||||
docs/dev/testing.md 的「截图留档」;客户端那两张要配合冒烟的
|
||||
ELECTRON_CLIENT_SHOTS_DIR / ELECTRON_SMOKE_ISLAND_* 开关。
|
||||
⚠ download.html 的版本清单是**当场**读 GitHub Releases 的
|
||||
(assets/download.js)—— 渲染出的卡片与文件行都是拉到数据之后才建的,
|
||||
建完会派发一次 ch:content,由 main.js 补挂按压反馈(新增动态内容照这个来)。
|
||||
读不到时退回 <meta name="ch-version"> 并按发布命名约定拼直链 ——
|
||||
那个 meta 是 version.mjs 的落点之一,发版必须一起改。
|
||||
命名约定与 README「开始使用」表里那三个名字要对齐
|
||||
(ClassHelper-<版本>-x64-client-setup.exe / -client-portable.exe /
|
||||
-x64-server-setup.exe),它们是离线兜底链接的唯一依据。
|
||||
packages/shared/src/ types.ts / constants.ts / permissions.ts / utils.ts / index.ts
|
||||
packages/server/
|
||||
prisma/schema.prisma 数据模型(14 个 model,无 enum、无 @db.*;连接串在 prisma.config.ts)
|
||||
@@ -122,7 +136,7 @@ packages/desktop-client/
|
||||
src/main/ 主进程:index / config(含作业短语与看板偏好)/ ipc / island / tray / update / logger / smoke
|
||||
src/preload/ contextBridge 白名单桥(index.ts + island.ts,**不暴露 ipcRenderer 本体**)
|
||||
src/island/ 灵动岛渲染进程:IslandApp.vue / spring.ts / squircle.ts / main.ts
|
||||
src/renderer/ 学生端渲染进程:api / cache(IndexedDB) / stores / views / island / router
|
||||
src/renderer/ ClassHelper 班级端渲染进程:api / cache(IndexedDB) / stores / views / island / router
|
||||
src/types/desktop.d.ts 主进程 ↔ 渲染进程契约
|
||||
scripts/ build-main / dev / smoke / dist-win
|
||||
packages/classisland-plugin/ .NET 8 插件(独立于 pnpm workspace,不参与 pnpm install)
|
||||
@@ -187,12 +201,12 @@ pnpm build:classisland-plugin
|
||||
| -------------- | ------------------------------- | ------------ |
|
||||
| 管理员 | `admin` | `admin123` |
|
||||
| 教师 | `teacher1` / `teacher2` | `teacher123` |
|
||||
| 学生端班级账号 | 班级码 `G101` / `G102` / `G203` | `123456` |
|
||||
| ClassHelper 班级端班级账号 | 班级码 `G101` / `G102` / `G203` | `123456` |
|
||||
|
||||
> 密码是**种子脚本里硬编码**的(`admin123` / `teacher123` / 班级 `123456`);
|
||||
> `.env` 的 `DEFAULT_CLASS_PASSWORD` 只影响**新建**班级时的初始班级密码。
|
||||
> **学生没有个人账号**(2026-10-01 清理):`student01…15` 只是名单记录(成绩/未交/叫人/已读按名单),
|
||||
> `User.passwordHash` 为空串、不能登录,也没有任何密码接口;学生端统一用班级码 + 班级密码。
|
||||
> `User.passwordHash` 为空串、不能登录,也没有任何密码接口;ClassHelper 班级端统一用班级码 + 班级密码。
|
||||
> 班级码 `G203` 是「高二(3)班」,不是 `G103`。
|
||||
|
||||
---
|
||||
@@ -228,8 +242,8 @@ pnpm build:classisland-plugin
|
||||
| `pnpm dist:server` / `dist:win` / `dist:dir` / `dist:all` | 打包服务端安装程序 / 客户端安装包 / 免安装目录 / 全部 |
|
||||
| `pnpm dist:server:linux` | 打包 **Linux 服务端安装包**(`classhelper-server-linux-x64-<版本>.tar.gz` + `.sha256`)—— 见 §5 第 59 条 |
|
||||
| `pnpm icons` | 从 `build/classhelper.png` 派生全部图标(PWA / favicon / 托盘 / 安装包 / 插件 / 官网 / 文档站 / 界面内品牌标) |
|
||||
| `pnpm version:check` | 逐处列出并比对 13 个落点的版本号,不一致即 exit 1(见 §4.4「版本迭代」) |
|
||||
| `pnpm version:bump patch\|minor\|major` | **改版本号的唯一入口**:一次改写全部 13 处(含插件清单、C# 里的常量,以及官网上手写的三处版本号) |
|
||||
| `pnpm version:check` | 逐处列出并比对 14 个落点的版本号,不一致即 exit 1(见 §4.4「版本迭代」) |
|
||||
| `pnpm version:bump patch\|minor\|major` | **改版本号的唯一入口**:一次改写全部 14 处(含插件清单、C# 里的常量、官网上手写的三处,以及下载页的兜底值) |
|
||||
| `pnpm docs:build` | 构建文档站到 `docs/_site`(需要 .NET SDK 8 + `dotnet tool install -g docfx`) |
|
||||
| `pnpm docs:serve` | 构建文档站并起本地预览 → http://127.0.0.1:5181 |
|
||||
| `pnpm docs:check` | 构建文档站并把 docfx 的警告当错误(发布前用;最常见的警告是"某个 .md 没被任何 toc.yml 引用") |
|
||||
@@ -254,11 +268,11 @@ pnpm build:classisland-plugin
|
||||
```
|
||||
releases/
|
||||
client/<版本>/
|
||||
安装包/ 班级小助手-<版本>-x64-setup.exe / -portable.exe(+ .blockmap)
|
||||
安装包/ ClassHelper-<版本>-x64-setup.exe / -portable.exe(+ .blockmap)
|
||||
免安装/ electron-builder 的 win-unpacked 内容
|
||||
构建中间/ builder-*.yml、*.nsis.7z
|
||||
server/<版本>/
|
||||
安装包/ 班级小助手服务端-<版本>-x64-setup.exe
|
||||
安装包/ ClassHelper服务端-<版本>-x64-setup.exe
|
||||
免安装/ classhelper-server 免安装目录(含 .env、node.exe、data/、logs/)
|
||||
linux-x64/ classhelper-server-linux-x64-<版本>.tar.gz + .sha256 + 同内容解包目录
|
||||
构建中间/ server-installer.generated.nsi
|
||||
@@ -296,9 +310,11 @@ pnpm dist:classisland-plugin
|
||||
> 算一次发布:攒够一批功能 / 修好一个用户能感知的 bug / 要给别人一份新安装包。
|
||||
> 不算:改注释、改文档、重构、加测试。
|
||||
|
||||
**同一个版本号写在 13 处(11 个文件)里**(5 份 `package.json`、`deploy/package.runtime.json`、
|
||||
两个 `docker-compose*.yml` 的镜像 tag、插件 `manifest.yml`、以及插件 C# 里的 `PluginVersion` 常量)。
|
||||
手工改必然漏一处,而漏掉的那处**不会报错** —— 只会在某个不常走的分支上表现为"版本号对不上"。
|
||||
**同一个版本号写在 14 处(12 个文件)里**(5 份 `package.json`、`deploy/package.runtime.json`、
|
||||
两个 `docker-compose*.yml` 的镜像 tag、插件 `manifest.yml`、插件 C# 里的 `PluginVersion` 常量、
|
||||
官网首页上三处手写值,以及下载页 `<meta name="ch-version">` 那个兜底值)。
|
||||
手工改必然漏一处,而漏掉的那处**不会报错** —— 只会在某个不常走的分支上表现为"版本号对不上"
|
||||
(官网那三处的后果是首页一直挂着旧版本号;下载页那一处的后果是离线访客点到一串不存在的直链)。
|
||||
所以**改版本号的唯一入口是脚本**:
|
||||
|
||||
```bash
|
||||
@@ -331,7 +347,7 @@ pnpm build && pnpm verify:web
|
||||
|
||||
# ④ 打安装包(产物落在 releases/<组件>/<版本>/)
|
||||
pnpm dist:all
|
||||
pnpm verify:packaged --exe "releases/client/<版本>/免安装/班级小助手.exe"
|
||||
pnpm verify:packaged --exe "releases/client/<版本>/免安装/ClassHelper.exe"
|
||||
|
||||
# ⑤ 整理跨组件哈希清单 releases/SHA256SUMS-<版本>.txt(人读;Linux 那一行由 CI 合并进来)
|
||||
# ⑥ 提交并推上去
|
||||
@@ -346,6 +362,29 @@ gh release upload v<版本> \
|
||||
releases/SHA256SUMS-<版本>.txt --clobber
|
||||
```
|
||||
|
||||
> **上传前把三个 EXE 改成 ASCII 名。** 构建产物是中文名(客户端 `ClassHelper-<版本>-x64-setup.exe`
|
||||
> / `-x64-portable.exe`,服务端 `ClassHelper 服务端-<版本>-x64-setup.exe`),而 Release 上的名字
|
||||
> 是给别人看、要贴进脚本的 —— 一律用 ASCII 形式,与构建产物一一对应(内容相同、sha256 相同):
|
||||
>
|
||||
> | 构建产物 | Release 上的名字 |
|
||||
> | --------------------------- | ------------------------------------------- |
|
||||
> | `<客户端>-x64-setup.exe` | `ClassHelper-<版本>-x64-client-setup.exe` |
|
||||
> | `<客户端>-x64-portable.exe` | `ClassHelper-<版本>-x64-client-portable.exe` |
|
||||
> | `<服务端>-x64-setup.exe` | `ClassHelper-<版本>-x64-server-setup.exe` |
|
||||
>
|
||||
> **这三个名字是外部契约**:README 的「开始使用」表、官网下载页的离线兜底直链都按它们拼 ——
|
||||
> 改名要三处一起改。上传前改名比上传后 `gh release edit` 省事:
|
||||
>
|
||||
> ```bash
|
||||
> V=<版本>
|
||||
> mkdir -p .cache/release-staging
|
||||
> cp "releases/client/$V/安装包/"*-x64-setup.exe ".cache/release-staging/ClassHelper-$V-x64-client-setup.exe"
|
||||
> cp "releases/client/$V/安装包/"*-x64-portable.exe ".cache/release-staging/ClassHelper-$V-x64-client-portable.exe"
|
||||
> cp "releases/server/$V/安装包/"*-x64-setup.exe ".cache/release-staging/ClassHelper-$V-x64-server-setup.exe"
|
||||
> cp "releases/classisland-plugin/$V/安装包/ClassHelper.ClassIslandPlugin.cipx" .cache/release-staging/
|
||||
> gh release upload v$V .cache/release-staging/* "releases/SHA256SUMS-$V.txt" --clobber
|
||||
> ```
|
||||
|
||||
**第 ⑦ 步的顺序有讲究**:`.github/workflows/release-linux-server.yml` 由 tag 推送触发,
|
||||
它在**最后一步**才把自己的那一行**合并进** Release 上已有的 `SHA256SUMS-<版本>.txt`
|
||||
(`grep -v` 掉旧的 linux 行再追加,Windows / 插件那几行原样保留)。
|
||||
@@ -368,6 +407,7 @@ gh workflow run release-linux-server.yml -f tag=v<版本> -f attach=true
|
||||
|
||||
```
|
||||
https://lovechengke.github.io/ClassHelper/ ← 官网(website/,纯静态无构建)
|
||||
https://lovechengke.github.io/ClassHelper/download.html ← 下载页(同一棵树里的一个页面)
|
||||
https://lovechengke.github.io/ClassHelper/docs/ ← 文档站(docfx 构建 docs/)
|
||||
```
|
||||
|
||||
@@ -378,7 +418,11 @@ https://lovechengke.github.io/ClassHelper/docs/ ← 文档站(docfx 构建
|
||||
> 以及文档站模板 `docs/templates/classhelper/layout/_master.tmpl` 里那个 `{{_rel}}../` 的「官网」入口。
|
||||
|
||||
- 顶栏的版本徽标取自根 `package.json`(`docs/build.mjs` 用 `--metadata _chVersion=…` 注入),
|
||||
**不要在文档里再写一份版本号**;官网首屏那排芯片里的 `v1.1.2` 是手写的,发版时要一起改;
|
||||
**不要在文档里再写一份版本号**;官网首页那排芯片里的 `v1.1.2` 与下载页的
|
||||
`<meta name="ch-version">` 是手写的,发版时要一起改(都在 `pnpm version:bump` 的 14 个落点里);
|
||||
- 下载页的版本清单**不在站点里存副本**:每次打开都由 `assets/download.js` 当场读
|
||||
GitHub Releases(匿名接口 60 次/小时/IP),读不到才退回上面那个 meta 按发布命名约定拼直链。
|
||||
所以发完 Release 不用重新部署这一页,但**兜底值必须跟着发版改**;
|
||||
- 首次启用需要在仓库 Settings → Pages 把 Source 选成「GitHub Actions」(只需一次);
|
||||
- 404 页的 `<base>` 由 `scripts/build-pages.mjs`(本地 `./`)与 workflow(CI 传 `/<仓库名>/`)注入 ——
|
||||
项目页挂在 `/<仓库名>/` 子路径下,而浏览器是按**被请求的路径**解析 404 页里的相对链接的;
|
||||
@@ -400,7 +444,7 @@ database / update`。
|
||||
`@classhelper/shared` 的 `resolveClassRole` → `ADMIN / HEAD(班主任)/ SUBJECT(科任)/ STUDENT / NONE`,
|
||||
权限判 `canManageSchedule` / `canPublishContent` / `canManageRoster` / `canManageGrades` 等
|
||||
(人类可读矩阵是 `PERMISSION_MATRIX`,README 直接引用它),**不要在页面里另写一套规则**。
|
||||
4. **学生端主体是「班级账号」。** 班级码 + 班级密码登录后,`classId` 即身份;该设备的「已读 / 完成」
|
||||
4. **ClassHelper 班级端主体是「班级账号」。** 班级码 + 班级密码登录后,`classId` 即身份;该设备的「已读 / 完成」
|
||||
代全班操作,个人学生登录已停用。API 里对此有专门分支,别按个人账号语义改。
|
||||
5. **实时推送只走房间。** 服务端 `src/realtime/`(`SOCKET_ROOMS`:`class:{classId}`、`user:{id}`、
|
||||
`teacher:{id}`、`role:{role}`、`students`、`teachers`;`bus.ts` 事件总线),客户端
|
||||
@@ -1129,7 +1173,7 @@ database / update`。
|
||||
|
||||
```bash
|
||||
# 判据:看交付物(以及 node_modules/electron/dist)有没有 Low 标签
|
||||
icacls packages\desktop-client\release\win-unpacked\班级小助手.exe | findstr Mandatory
|
||||
icacls packages\desktop-client\release\win-unpacked\ClassHelper.exe | findstr Mandatory
|
||||
# 修法:把交付物改成 Medium。dist-win.mjs 收尾会自动做,手工补救用:
|
||||
icacls packages\desktop-client\release /setintegritylevel Medium /T /C
|
||||
# 目录自身要还原成继承,否则下次构建时自带 Low 标签的 7za / makensis 写不进去
|
||||
@@ -1229,8 +1273,8 @@ database / update`。
|
||||
同理,断言岛体尺寸别写死"胶囊尺寸 + 留白":那一刻是胶囊还是卡片取决于前面用例留下的状态,
|
||||
应改成**与形态无关的不变量**(如"窗口 = 卡片实测尺寸 + 2×阴影留白")。
|
||||
|
||||
23. **客户端安装包有硬底:`班级小助手.exe` 单独压缩后就有 85~90MB,"压到 60MB"做不到 —— 别再为这个反复折腾。**
|
||||
实测(2026-10-05,Electron 44.3.0):`win-unpacked` 372MB,其中 `班级小助手.exe`(就是 electron.exe,
|
||||
23. **客户端安装包有硬底:`ClassHelper.exe` 单独压缩后就有 85~90MB,"压到 60MB"做不到 —— 别再为这个反复折腾。**
|
||||
实测(2026-10-05,Electron 44.3.0):`win-unpacked` 372MB,其中 `ClassHelper.exe`(就是 electron.exe,
|
||||
rcedit 只加了 74KB)**234.7MB**;brotli-9 抽样压缩后估算它单独就有 85~90MB。也就是说把渲染产物
|
||||
从 3.2MB 压到 2.1MB、把图标从 293 个减到 42 个,对安装包只影响约 1MB。
|
||||
剩下能动的只有这些(合计约 12MB,风险不成比例,**当前一律不做**):
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
<div align="center">
|
||||
|
||||
# <img src="build/classhelper.png" width="30" height="30" alt="" /> 班级小助手
|
||||
# <img src="build/classhelper.png" width="30" height="30" alt="" /> ClassHelper
|
||||
|
||||
<img src="docs/images/web-dashboard.jpg" alt="班级小助手 · Web 管理端仪表盘" />
|
||||
<img src="docs/images/web-dashboard.jpg" alt="ClassHelper · Web 管理端仪表盘" />
|
||||
|
||||
[](https://github.com/LoveChengke/ClassHelper/releases/latest)
|
||||
[](https://github.com/LoveChengke/ClassHelper/releases)
|
||||
@@ -16,7 +16,7 @@
|
||||

|
||||
[](https://github.com/LoveChengke/ClassHelper)
|
||||
|
||||
班级小助手是给中小学班级用的一套信息管理系统。<br/>
|
||||
ClassHelper是给中小学班级用的一套信息管理系统。<br/>
|
||||
**老师发一条通知,教室那台机器上的灵动岛和 [ClassIsland](https://classisland.tech) 会同时弹出来**;<br/>
|
||||
学生打开客户端就能看到当天的课表和作业。
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
|
||||
- [x] 教室机器上报课表与上课状态,Web 端能看到「现在在上什么」
|
||||
- [x] 老师发的提醒在教室大屏上全屏弹出,支持语音朗读,播完回执不重复弹
|
||||
- [x] 可以把班级小助手排好的课表镜像回 ClassIsland,不影响老师原有的课表
|
||||
- [x] 可以把ClassHelper排好的课表镜像回 ClassIsland,不影响老师原有的课表
|
||||
- [x] 提醒弹在客户端还是 ClassIsland,由教室那台机器自己选
|
||||
|
||||
### 部署与运维
|
||||
@@ -150,18 +150,22 @@
|
||||
|
||||
| 场景 | 截图 |
|
||||
| --- | --- |
|
||||
| 抽屉菜单 | <img src="docs/images/mobile-dashboard.jpg" alt="手机 · 仪表盘" /> |
|
||||
| 仪表盘 | <img src="docs/images/mobile-dashboard.jpg" alt="手机 · 仪表盘" /> |
|
||||
| 通知发布 | <img src="docs/images/mobile-notifications.jpg" alt="手机 · 通知发布" /> |
|
||||
|
||||
</details>
|
||||
|
||||
## 开始使用
|
||||
|
||||
| 装在哪 | 装什么 | 从 Releases 里拿 |
|
||||
| ----------------------- | ------------------------------------------------- | --------------------------------------------------------- |
|
||||
| 教师的电脑 / 学校服务器 | **服务端**(内含 Web 管理端与 Node 运行时) | `班级小助手服务端-<版本>-x64-setup.exe` |
|
||||
| 教室机器 / 学生电脑 | **学生客户端** | `班级小助手-<版本>-x64-setup.exe`,或 `-portable.exe` 免安装 |
|
||||
| 教室机器的 ClassIsland | **联动插件** | `ClassHelper.ClassIslandPlugin.cipx` |
|
||||
| 装在哪 | 装什么 | 从 Releases 里拿 |
|
||||
| ----------------------- | ------------------------------------------------- | --------------------------------------------------------------------------- |
|
||||
| 教师的电脑 / 学校服务器 | **服务端**(内含 Web 管理端与 Node 运行时) | `ClassHelper-<版本>-x64-server-setup.exe` |
|
||||
| 教室机器 / 学生电脑 | **学生客户端** | `ClassHelper-<版本>-x64-client-setup.exe`,或 `-client-portable.exe` 免安装 |
|
||||
| 教室机器的 ClassIsland | **联动插件** | `ClassHelper.ClassIslandPlugin.cipx` |
|
||||
|
||||
也可以在官网的[下载页](https://lovechengke.github.io/ClassHelper/download.html)上挑:
|
||||
选一个版本,四个包排成四张卡,文件名、体积与 SHA256 清单都在那儿 ——
|
||||
那一页的清单是**当场**读 GitHub Releases 的,显示什么就能下什么。
|
||||
|
||||
服务端**装上就算跑起来了**:双击安装程序,建库、建号、开机自启都由它自己办,
|
||||
装完在浏览器打开 `http://<服务器地址>:4000` 就能登录。教师的电脑、学校的服务器、
|
||||
@@ -208,7 +212,7 @@ pnpm dev # 后端 4000 + Web 端 5
|
||||
| -------------- | --------------- | ------------ |
|
||||
| 管理员 | `admin` | `admin123` |
|
||||
| 教师 | `teacher1` | `teacher123` |
|
||||
| 学生端(班级) | 班级码 `G101` | `123456` |
|
||||
| ClassHelper 班级端(班级) | 班级码 `G101` | `123456` |
|
||||
|
||||
- [AGENTS.md](AGENTS.md) —— 工程约定与本机踩坑,**改代码前先读它**
|
||||
- [docs/reference.md](docs/reference.md) —— 完整参考(功能行为、接口、验收记录)
|
||||
|
||||
+8
-8
@@ -14,14 +14,14 @@ description: 桌面客户端的通知浮窗:胶囊与卡片、多条通知的
|
||||
|
||||
## 各形态长什么样
|
||||
|
||||
| 形态 | 截图 | 说明 |
|
||||
| ---------------------- | ----------------------------------------------------------------- | ------------------------------------------------- |
|
||||
| 非上课时段的「新消息」 |  | 268×44,直接出现(无入场动画),点击后展开 |
|
||||
| 点开后的详情卡 |  | 标题 / 内容 / 时间 + 打开应用 / 标为已读 / 知道了 |
|
||||
| 下课后自动弹出 |  | 上课期间暂存的通知,下课后自动弹出 |
|
||||
| 上课期间的紧急通知 |  | 卡片**内部**红色光晕 + 紧急角标,无需点击 |
|
||||
| 新作业 |  | 青蓝描边 + 书本图标 |
|
||||
| 叫人 |  | 琥珀金卡片 + 「叫人」徽标 + 「收到」按钮 |
|
||||
| 形态 | 截图 | 说明 |
|
||||
| ---------------------- | ----------------------------------------------------- | ------------------------------------------------- |
|
||||
| 非上课时段的「新消息」 |  | 268×44,直接出现(无入场动画),点击后展开 |
|
||||
| 点开后的详情卡 |  | 标题 / 内容 / 时间 + 打开应用 / 标为已读 / 知道了 |
|
||||
| 下课后自动弹出 |  | 上课期间暂存的通知,下课后自动弹出 |
|
||||
| 上课期间的紧急通知 |  | 卡片**内部**红色光晕 + 紧急角标,无需点击 |
|
||||
| 新作业 |  | 青蓝描边 + 书本图标 |
|
||||
| 叫人 |  | 琥珀金卡片 + 「叫人」徽标 + 「收到」按钮 |
|
||||
|
||||
## 什么时候弹、弹成什么样
|
||||
|
||||
|
||||
@@ -55,7 +55,8 @@ class-helper/
|
||||
│ ├── index.md # 首页
|
||||
│ ├── get-started/ app/ management/ dev/ # 四个章节
|
||||
│ ├── templates/classhelper/ # docfx 模板覆盖(_master.tmpl + public/main.css + 图标)
|
||||
│ ├── screenshots/ # 回归测试自动留档的截图(island / client / web-mobile / classisland)
|
||||
│ ├── images/ # 展示图:文档站 / README / 官网共用的同一份(1× 逻辑像素)
|
||||
│ ├── screenshots/ # 回归测试留档的截图(island / client / web-mobile / classisland),页面不展示
|
||||
│ ├── build.mjs # 构建入口(注入版本号与 404 的 base)
|
||||
│ ├── serve.mjs # 本地预览(:5181)
|
||||
│ └── _site/ # 构建产物(gitignore)
|
||||
|
||||
+42
-23
@@ -19,22 +19,24 @@ description: 改版本号、跑验收、打安装包、发 GitHub Release、发
|
||||
| 要给别人一份新安装包 | 只在自己机器上跑 |
|
||||
| 插件单独发补丁(只动插件第 4 段版本号) | — |
|
||||
|
||||
## 版本号写在哪(13 处 / 11 个文件)
|
||||
## 版本号写在哪(14 处 / 12 个文件)
|
||||
|
||||
同一个版本号分散在 13 个落点上,**手工改必然漏一处**,而漏掉的那处不会报错:
|
||||
同一个版本号分散在 14 个落点上,**手工改必然漏一处**,而漏掉的那处不会报错:
|
||||
|
||||
| 文件 | 内容 |
|
||||
| ---------------------------------------------------------------- | ------------------------------- |
|
||||
| `package.json` | 单一来源(服务端运行时读它) |
|
||||
| `packages/{shared,server,web-admin,desktop-client}/package.json` | 各包的版本 |
|
||||
| `deploy/package.runtime.json` | 安装包 / 容器里的运行时依赖清单 |
|
||||
| `deploy/docker-compose.yml`、`docker-compose.sqlite.yml` | 镜像 tag 的默认值 |
|
||||
| `packages/classisland-plugin/manifest.yml` | 插件清单(四段:`x.y.z.0`) |
|
||||
| `packages/classisland-plugin/src/Services/BridgeService.cs` | 插件上报的 `PluginVersion` 常量 |
|
||||
| `website/index.html` × 3 | 官网顶栏徽标 / 首屏芯片 / 页脚 |
|
||||
| 文件 | 内容 |
|
||||
| ---------------------------------------------------------------- | ---------------------------------- |
|
||||
| `package.json` | 单一来源(服务端运行时读它) |
|
||||
| `packages/{shared,server,web-admin,desktop-client}/package.json` | 各包的版本 |
|
||||
| `deploy/package.runtime.json` | 安装包 / 容器里的运行时依赖清单 |
|
||||
| `deploy/docker-compose.yml`、`docker-compose.sqlite.yml` | 镜像 tag 的默认值 |
|
||||
| `packages/classisland-plugin/manifest.yml` | 插件清单(四段:`x.y.z.0`) |
|
||||
| `packages/classisland-plugin/src/Services/BridgeService.cs` | 插件上报的 `PluginVersion` 常量 |
|
||||
| `website/index.html` × 3 | 官网顶栏徽标 / 首屏芯片 / 页脚 |
|
||||
| `website/download.html`(`<meta name="ch-version">`) | 下载页读不到版本清单时的兜底版本号 |
|
||||
|
||||
最后一行值得单独说:官网是**纯静态站、没有构建步骤**,没有哪段代码会去读 `package.json`,
|
||||
所以那三处只能靠脚本一起改 —— 漏了的话首页会一直挂着旧版本号,而那是别人看到的第一眼。
|
||||
最后两行值得单独说:官网是**纯静态站、没有构建步骤**,没有哪段代码会去读 `package.json`,
|
||||
所以那几处只能靠脚本一起改 —— 漏了的话首页会一直挂着旧版本号(而那是别人看到的第一眼),
|
||||
下载页则会给离线访客一串指向**不存在版本**的直链。
|
||||
|
||||
所以**改版本号的唯一入口是脚本**:
|
||||
|
||||
@@ -97,7 +99,7 @@ pnpm dist:all # = dist:server + dist:win + dist:classisland-pl
|
||||
打完之后**对打包副本再验一次**(开发产物通过 ≠ 用户机器上的副本通过):
|
||||
|
||||
```bash
|
||||
pnpm verify:packaged --exe "releases/client/<版本>/免安装/班级小助手.exe"
|
||||
pnpm verify:packaged --exe "releases/client/<版本>/免安装/ClassHelper.exe"
|
||||
```
|
||||
|
||||
> Linux 包**只能在 Linux 上构建**(依赖是平台相关的),所以不在这条命令里 ——
|
||||
@@ -127,16 +129,29 @@ git push origin master
|
||||
|
||||
### 7. 开 Release
|
||||
|
||||
```bash
|
||||
gh release create v1.1.3 --title "v1.1.3" --generate-notes
|
||||
上传前先把产物改成 ASCII 名 —— 构建产物是中文名,Release 上的名字是给别人看、要贴进脚本的:
|
||||
|
||||
gh release upload v1.1.3 \
|
||||
releases/client/<版本>/安装包/*.exe \
|
||||
releases/classisland-plugin/<版本>/安装包/*.cipx \
|
||||
releases/server/<版本>/安装包/*.exe \
|
||||
releases/SHA256SUMS-<版本>.txt --clobber
|
||||
```bash
|
||||
V=1.1.3
|
||||
mkdir -p .cache/release-staging
|
||||
cp "releases/client/$V/安装包/"*-x64-setup.exe ".cache/release-staging/ClassHelper-$V-x64-client-setup.exe"
|
||||
cp "releases/client/$V/安装包/"*-x64-portable.exe ".cache/release-staging/ClassHelper-$V-x64-client-portable.exe"
|
||||
cp "releases/server/$V/安装包/"*-x64-setup.exe ".cache/release-staging/ClassHelper-$V-x64-server-setup.exe"
|
||||
cp "releases/classisland-plugin/$V/安装包/ClassHelper.ClassIslandPlugin.cipx" .cache/release-staging/
|
||||
|
||||
gh release create v$V --title "v$V" --generate-notes
|
||||
gh release upload v$V .cache/release-staging/* "releases/SHA256SUMS-$V.txt" --clobber
|
||||
```
|
||||
|
||||
| 构建产物 | Release 上的名字 |
|
||||
| --------------------------- | -------------------------------------------- |
|
||||
| `<客户端>-x64-setup.exe` | `ClassHelper-<版本>-x64-client-setup.exe` |
|
||||
| `<客户端>-x64-portable.exe` | `ClassHelper-<版本>-x64-client-portable.exe` |
|
||||
| `<服务端>-x64-setup.exe` | `ClassHelper-<版本>-x64-server-setup.exe` |
|
||||
|
||||
这三个名字是**外部契约**:README 的「开始使用」表、官网下载页读不到版本清单时拼的兜底直链,
|
||||
都按它们写;改名要三处一起改。
|
||||
|
||||
`gh release create` 会**创建 tag 并推送**,这一步会自动触发
|
||||
[`release-linux-server.yml`](https://github.com/LoveChengke/classhelper/blob/master/.github/workflows/release-linux-server.yml)
|
||||
构建 Linux 包(ubuntu runner)。它跑完之后会:
|
||||
@@ -169,8 +184,12 @@ https://lovechengke.github.io/ClassHelper/docs/ ← 文档站
|
||||
(一个仓库在 Pages 上只有一个站点,所以是拼在一起发的 —— 官网在根、文档站在 `/docs/` 下。)
|
||||
|
||||
文档站顶栏的版本徽标取自根 `package.json`,所以第 1 步改完就跟着变了;
|
||||
官网首屏那排芯片、顶栏徽标与页脚里的版本号也是**由 `pnpm version:bump` 一起改的**
|
||||
(它们是那 13 个落点里的三个)。
|
||||
官网首屏那排芯片、顶栏徽标与页脚里的版本号,以及下载页那个兜底版本号,
|
||||
也都是**由 `pnpm version:bump` 一起改的**(它们是那 14 个落点里的四个)。
|
||||
|
||||
> 官网的「立即下载」会跳到 `/download.html`,那一页的版本清单是**当场**从 GitHub Releases 读的
|
||||
> (读不到才退回上面那个兜底版本号),所以发完 Release 不用重新部署那一页 —— 但**兜底值要跟着发版一起改**,
|
||||
> 不然离线访客点到的就是旧版本的直链。
|
||||
|
||||
### 9. 检查
|
||||
|
||||
|
||||
+29
-6
@@ -80,20 +80,43 @@ pnpm build && pnpm verify:web
|
||||
|
||||
## 截图留档
|
||||
|
||||
回归测试顺手把截图存到 `docs/screenshots/`(island / client / web-mobile / classisland 四组),
|
||||
文档站里用的就是这些图。要重新采集:
|
||||
截图分两套,各管一件事:
|
||||
|
||||
| 目录 | 谁在用 | 谁产出 |
|
||||
| ------------------- | ----------------------------------------------------------- | --------------------------------------------------------------------- |
|
||||
| `docs/images/` | **文档站、README、官网用的是同一份图**,页面上展示的都是它 | `capture-shots.mjs`(Web / 手机)+ 客户端冒烟(客户端 / 灵动岛) |
|
||||
| `docs/screenshots/` | 回归留档,页面不展示;`verify:*` 的像素级断言比对的就是它们 | 各冒烟脚本自动写入(island / client / web-mobile / classisland 四组) |
|
||||
|
||||
**展示图重新采集**(保证文档站与 README、官网用的是同一份图):
|
||||
|
||||
```bash
|
||||
pnpm dev:server # 后端同时托管 Web 管理端
|
||||
CAPTURE_OUT=docs/images,website/assets/shots node website/tools/capture-shots.mjs # → web-*.jpg / mobile-*.jpg
|
||||
|
||||
# 客户端与灵动岛走的是冒烟脚本。它除了正片,还会顺手落一份「岛外留白对照图」
|
||||
# (*-window.png / *-backdrop.png),所以先落到临时目录,再挑要用的拷进去。
|
||||
ISLAND_SHOTS_DIR="$PWD/.cache/shots" ELECTRON_CLIENT_SHOTS_DIR="$PWD/.cache/shots" \
|
||||
ELECTRON_SMOKE_ISLAND_ACCENT='#0a84ff' ELECTRON_SMOKE_ISLAND_STYLE=black \
|
||||
ELECTRON_SMOKE_USER=teacher1 ELECTRON_SMOKE_PASSWORD=123456 \
|
||||
node packages/desktop-client/scripts/smoke.mjs
|
||||
cp .cache/shots/{island-*,client-*}.png docs/images/
|
||||
rm -f docs/images/*-window.png docs/images/*-backdrop.png
|
||||
cp docs/images/{island-*,client-*}.png website/assets/shots/ # 官网那份保持同步
|
||||
```
|
||||
|
||||
**回归留档重新采集**(同一套冒烟,把输出指到留档目录):
|
||||
|
||||
```bash
|
||||
pnpm dev:server
|
||||
ISLAND_SHOTS_DIR=F:/ClassHelper/.cache/island-shots \
|
||||
ELECTRON_CLIENT_SHOTS_DIR=F:/ClassHelper/.cache/client-shots \
|
||||
ISLAND_SHOTS_DIR="$PWD/.cache/island-shots" \
|
||||
ELECTRON_CLIENT_SHOTS_DIR="$PWD/.cache/client-shots" \
|
||||
ELECTRON_SMOKE_ISLAND_ACCENT='#0a84ff' ELECTRON_SMOKE_ISLAND_STYLE=black \
|
||||
ELECTRON_SMOKE_USER=teacher1 ELECTRON_SMOKE_PASSWORD=123456 \
|
||||
node packages/desktop-client/scripts/smoke.mjs
|
||||
```
|
||||
|
||||
官网的实机图则用 `website/tools/capture-shots.mjs`(采集步骤见 `README.md` §7)。
|
||||
|
||||
> 展示图都是**逻辑像素 1×**(灵动岛的 268×44、424×230 就是这个数,不再乘 2),
|
||||
> 页面按原始尺寸显示即可 —— 换图时别顺手放大,否则整页的尺寸对不上。
|
||||
> 采集脚本会把窗口真的弹在屏幕上。客户端留档会**临时停掉 CSS 动画**再截图 ——
|
||||
> 窗口被遮挡时 Chromium 冻结动画,而页面入场动画的起始帧是 `opacity: 0`,
|
||||
> 不停掉的话截出来是**整片空白**(DOM 里文本齐全,只有图是空的)。
|
||||
+8
-8
@@ -1,12 +1,12 @@
|
||||
---
|
||||
title: 首页
|
||||
description: 班级小助手(Class Helper)的官方文档:安装部署、功能说明、运维手册与开发参考。
|
||||
description: ClassHelper(Class Helper)的官方文档:安装部署、功能说明、运维手册与开发参考。
|
||||
---
|
||||
|
||||
# 班级小助手文档
|
||||
# ClassHelper文档
|
||||
|
||||
班级小助手(Class Helper)是给中小学班级用的一套信息管理系统。**一份代码,四个交付物**:
|
||||
装在服务器上的后端、给老师用的 Web 管理端、装在教室机器的桌面客户端(学生端 / 教室大屏),
|
||||
ClassHelper(Class Helper)是给中小学班级用的一套信息管理系统。**一份代码,四个交付物**:
|
||||
装在服务器上的后端、给老师用的 Web 管理端、装在教室机器的桌面客户端(ClassHelper 班级端 / 教室大屏),
|
||||
以及装在 [ClassIsland](https://classisland.tech) 上的联动插件。
|
||||
|
||||
<p class="ch-tagline">
|
||||
@@ -16,7 +16,7 @@ description: 班级小助手(Class Helper)的官方文档:安装部署、
|
||||
<div class="ch-cards">
|
||||
<a class="ch-card" href="get-started/index.md">
|
||||
<span class="ch-card-title">第一次装</span>
|
||||
<span class="ch-card-desc">从零把服务端、管理端、学生端和 ClassIsland 插件跑起来,大约半小时。</span>
|
||||
<span class="ch-card-desc">从零把服务端、管理端、ClassHelper 班级端和 ClassIsland 插件跑起来,大约半小时。</span>
|
||||
</a>
|
||||
<a class="ch-card" href="app/index.md">
|
||||
<span class="ch-card-title">已经在用了</span>
|
||||
@@ -35,9 +35,9 @@ description: 班级小助手(Class Helper)的官方文档:安装部署、
|
||||
## 它长什么样
|
||||
|
||||
<div class="ch-shots">
|
||||
<img src="screenshots/client/01-schedule.png" alt="桌面客户端 · 课表「今天」时间轴" />
|
||||
<img src="screenshots/island/island-5-clicked.png" alt="灵动岛 · 点开通知后的详情卡" />
|
||||
<img src="screenshots/web-mobile/mobile-2-notifications.png" alt="Web 管理端 · 手机上的通知发布页" />
|
||||
<img src="images/client-schedule.png" alt="桌面客户端 · 课表「今天」时间轴" />
|
||||
<img src="images/island-5-clicked.png" alt="灵动岛 · 点开通知后的详情卡" />
|
||||
<img src="images/mobile-notifications.jpg" alt="Web 管理端 · 手机上的抽屉导航与通知发布页" />
|
||||
</div>
|
||||
|
||||
> 这些图都是脚本把程序真的跑起来截的,不是示意图。采集方式见 [验收与测试](dev/testing.md)。
|
||||
|
||||
+66
-71
@@ -1,7 +1,7 @@
|
||||
# 班级小助手 · 完整参考
|
||||
# ClassHelper · 完整参考
|
||||
|
||||
班级信息管理系统。一个 pnpm monorepo,一份代码产出四个交付物:**后端服务**、**Web 管理端**(教师 / 管理员)、
|
||||
**桌面客户端**(学生端 / 教室机器)与 **ClassIsland 联动插件**(装在教室的 ClassIsland 上)。**当前版本 1.1.2**。
|
||||
**桌面客户端**(ClassHelper 班级端 / 教室机器)与 **ClassIsland 联动插件**(装在教室的 ClassIsland 上)。**当前版本 1.1.2**。
|
||||
|
||||
核心链路:
|
||||
|
||||
@@ -19,7 +19,7 @@ ClassIsland(教室机器)──► 联动插件 ──► /api/integrations/
|
||||
└──── 老师下发的提醒 ◄────────┘
|
||||
```
|
||||
|
||||
> 本文是班级小助手的**完整参考**:功能行为、接口契约、验收证据与已知问题都在这里,是功能口径的权威来源。
|
||||
> 本文是ClassHelper的**完整参考**:功能行为、接口契约、验收证据与已知问题都在这里,是功能口径的权威来源。
|
||||
> 面向「第一次打开仓库」的展示型介绍在 [README](https://github.com/LoveChengke/classhelper/blob/master/README.md);按读者分组的引导式文档在文档站(本目录)。
|
||||
> 工程约定(改代码前必读)与本机踩坑在 [AGENTS.md](https://github.com/LoveChengke/classhelper/blob/master/AGENTS.md);生产部署在
|
||||
> [production.md](management/production.md);切换数据库在 [mysql.md](management/mysql.md)。
|
||||
@@ -62,7 +62,7 @@ ClassIsland(教室机器)──► 联动插件 ──► /api/integrations/
|
||||
| -------------------- | ----------------------------- | -------------------------------------------------------------------- | --------------------------------- |
|
||||
| 后端服务 | `packages/server` | Express 5 + Prisma 7 + Socket.IO,接口前缀 `/api` | 其它三端 |
|
||||
| Web 管理端 | `packages/web-admin` | Vue 3 + Vite + Element Plus(构建后由后端托管,PWA 可安装) | 教师、管理员(手机可用) |
|
||||
| 桌面客户端 | `packages/desktop-client` | Electron + Vue 3,含「灵动岛」浮窗、托盘、离线缓存 | 学生端 / 教室机器 |
|
||||
| 桌面客户端 | `packages/desktop-client` | Electron + Vue 3,含「灵动岛」浮窗、托盘、离线缓存 | ClassHelper 班级端 / 教室机器 |
|
||||
| 共享契约 | `packages/shared` | 三端共用的类型 / 常量 / 权限矩阵 / 工具函数 | 上面三个 + 插件 |
|
||||
| ClassIsland 联动插件 | `packages/classisland-plugin` | .NET 8 / C#,装在教室的 ClassIsland 上(上报课表与上课状态、弹提醒) | 教室机器(独立于 pnpm workspace) |
|
||||
| 官网 | `website/` | 纯 HTML / CSS / JS 的静态页面,**没有构建步骤** | 展示与说明(`node website/serve.mjs` 本地预览) |
|
||||
@@ -78,10 +78,10 @@ ClassIsland(教室机器)──► 联动插件 ──► /api/integrations/
|
||||
|
||||
| 产物 | 产物文件位置(打包后) | 用途 |
|
||||
| ------------------------------- | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
|
||||
| **服务端 + Web 管理端安装程序** | `releases/server/<版本>/安装包/班级小助手服务端-<版本>-x64-setup.exe` | 装到教师电脑 / 校服务器即完整系统,**内置 Node 运行时**,双击安装、开机自启、自动建库建号 |
|
||||
| **服务端 + Web 管理端安装程序** | `releases/server/<版本>/安装包/ClassHelper服务端-<版本>-x64-setup.exe` | 装到教师电脑 / 校服务器即完整系统,**内置 Node 运行时**,双击安装、开机自启、自动建库建号 |
|
||||
| **服务端 Linux 安装包** | `releases/server/<版本>/linux-x64/classhelper-server-linux-x64-<版本>.tar.gz` | 给 `deploy/install.sh` 用(另见同名 `.sha256`);由 Actions 在 Linux 上构建后挂 Release |
|
||||
| **学生客户端安装程序** | `releases/client/<版本>/安装包/班级小助手-<版本>-x64-setup.exe` | 学生机安装(NSIS) |
|
||||
| **学生客户端单文件版** | `releases/client/<版本>/安装包/班级小助手-<版本>-x64-portable.exe` | 免安装直接运行(U 盘分发) |
|
||||
| **学生客户端安装程序** | `releases/client/<版本>/安装包/ClassHelper-<版本>-x64-setup.exe` | 学生机安装(NSIS) |
|
||||
| **学生客户端单文件版** | `releases/client/<版本>/安装包/ClassHelper-<版本>-x64-portable.exe` | 免安装直接运行(U 盘分发) |
|
||||
| Web 管理端(PWA) | 由服务端在 `/` 直接托管 | 浏览器打开即用,可在 Edge/Chrome 中「安装为应用」;已适配手机小屏 |
|
||||
| **ClassIsland 联动插件** | `releases/classisland-plugin/<版本>/安装包/ClassHelper.ClassIslandPlugin.cipx` | 装在教室机器的 ClassIsland 上(也可把 `ClassHelper.ClassIslandPlugin/` 目录丢进 Plugins) |
|
||||
|
||||
@@ -94,7 +94,7 @@ ClassIsland(教室机器)──► 联动插件 ──► /api/integrations/
|
||||
|
||||
| 场景 | 形态 | 入口 |
|
||||
| ----------------------------------- | ----------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
||||
| 学校机房 / 教师电脑(Windows 单机) | Windows 服务端安装程序(内置 Node,双击即用) | `releases/server/<版本>/安装包/班级小助手服务端-<版本>-x64-setup.exe` |
|
||||
| 学校机房 / 教师电脑(Windows 单机) | Windows 服务端安装程序(内置 Node,双击即用) | `releases/server/<版本>/安装包/ClassHelper服务端-<版本>-x64-setup.exe` |
|
||||
| 云服务器 / 多终端共享(推荐长期) | Docker Compose + MySQL(另有 SQLite 单容器版) | `deploy/Dockerfile`、`deploy/docker-compose{,.sqlite}.yml` |
|
||||
| 已有 Linux 服务器 | **一键安装器 + `classhelper` 运维命令**(systemd / docker) | `sudo bash deploy/install.sh`(先 `--check` 体检);运维见 `classhelper help` |
|
||||
| 自定义 / 已有 Node 环境 | 手动部署免安装目录 | `pnpm dist:server` → `releases/server/<版本>/免安装/` |
|
||||
@@ -147,7 +147,7 @@ pnpm dev
|
||||
> `{ "success": false, "code": "NOT_FOUND" }`。Web 管理端开发地址是 **5173**,构建后由后端在 4000 托管。
|
||||
> 也可以分开启动:`pnpm dev:server`(仅后端)/ `pnpm dev:web`(仅 Web 端)。
|
||||
|
||||
### 启动客户端(学生端)
|
||||
### 启动客户端(ClassHelper 班级端)
|
||||
|
||||
```bash
|
||||
# 开发模式:自动构建主进程 + 启动 Vite 渲染进程(5174) + 打开 Electron 窗口(热更新)
|
||||
@@ -183,14 +183,14 @@ pnpm verify:desktop
|
||||
| 管理员 | `admin` | `admin123` | 可访问全部班级 |
|
||||
| 教师 | `teacher1` | `teacher123` | 张老师:高一(1)班、高二(3)班班主任 |
|
||||
| 教师 | `teacher2` | `teacher123` | 李老师:高一(2)班班主任,高二(3)班协作教师 |
|
||||
| 班级 | 班级码 `G101` | `123456` | 高一(1)班(**学生端唯一登录方式**) |
|
||||
| 班级 | 班级码 `G101` | `123456` | 高一(1)班(**ClassHelper 班级端唯一登录方式**) |
|
||||
| 班级 | 班级码 `G102` | `123456` | 高一(2)班 |
|
||||
| 班级 | 班级码 `G203` | `123456` | 高二(3)班(不是 `G103`) |
|
||||
|
||||
- 密码由**种子脚本硬编码**(`admin123` / `teacher123` / 班级 `123456`);`.env` 的
|
||||
`DEFAULT_CLASS_PASSWORD` 只决定**新建**班级时的初始班级密码。
|
||||
- **学生没有个人账号**(2026-10-01 起彻底清理):学生只是名单记录(成绩 / 未交名单 / 已读人数 / 叫人都按名单),
|
||||
没有用户名密码、不能登录;学生端统一用班级码 + 班级密码。
|
||||
没有用户名密码、不能登录;ClassHelper 班级端统一用班级码 + 班级密码。
|
||||
- 教学周由 `TERM_START_DATE`(第 1 教学周的周一)换算,当前周次用于课表默认视图。
|
||||
|
||||
### 常用命令
|
||||
@@ -270,13 +270,12 @@ Element Plus 也通过对齐它的 `--el-border-radius-*` 一并改造:
|
||||
详情抽屉宽 86vw 且 `el-descriptions` 由 2~3 列降为单列;统计卡片两列。
|
||||
实现位置:`src/composables/useResponsive.ts`、`src/layouts/AdminLayout.vue`、`src/styles/index.css`。
|
||||
|
||||
实拍(`pnpm verify:web` 自动留档到 `docs/screenshots/web-mobile/`):
|
||||
实拍(`node website/tools/capture-shots.mjs` 采集,落在 `docs/images/` —— 与 README、官网用的是同一份图):
|
||||
|
||||
| 场景 | 截图 |
|
||||
| ----------------------------------------- | --------------------------------------------------------------------- |
|
||||
| 手机抽屉菜单 |  |
|
||||
| 手机通知页(工具栏铺满 + 表格卡片内滚动) |  |
|
||||
| 手机弹窗自适应 |  |
|
||||
| 场景 | 截图 |
|
||||
| ---------------------------------------------- | ------------------------------------------------ |
|
||||
| 手机抽屉菜单(叠在「通知发布」页上) |  |
|
||||
| 手机仪表盘(统计卡两列,表格在卡片内横向滚动) |  |
|
||||
|
||||
> 手机访问:与电脑同一局域网时用 `http://<主机IP>:4000` 打开管理端;支持「添加到主屏幕」(PWA)。
|
||||
|
||||
@@ -289,7 +288,7 @@ Element Plus 也通过对齐它的 `--el-border-radius-*` 一并改造:
|
||||
| 删除教师有护栏 | 四种情况一律 409:还是**班主任**、名下有课程 / 班级分配、**发布过作业或通知**(后两项也是级联删除,删账号会连带删掉他在各班发过的内容) |
|
||||
| 创建班级仅管理员 | `POST /api/classes` = `requireRole('ADMIN')`,教师(含班主任)403 |
|
||||
| **设置 / 更改班主任** | 新建班级时可选班主任(默认自己);班级详情顶部「更换班主任」下拉随时改(`PATCH /api/classes/:id/head-teacher`,仅管理员),原班主任仍留在协作教师名单 |
|
||||
| 学生录入 | 「学生管理」:单个新建(登录名自动生成)+导入名单;学生端主体是班级账号 |
|
||||
| 学生录入 | 「学生管理」:单个新建(登录名自动生成)+导入名单;ClassHelper 班级端主体是班级账号 |
|
||||
|
||||
> 学生名单接口(`/api/students`)与教师全量接口(`/api/teachers`)都是 **`requireRole('ADMIN')`**;
|
||||
> 「班级 → 学生名单」(`/api/classes/:id/students`)读接口对班级可见,写接口为 ADMIN/TEACHER(服务层再按班级角色兜底)。
|
||||
@@ -371,7 +370,7 @@ Element Plus 也通过对齐它的 `--el-border-radius-*` 一并改造:
|
||||
(`renderer/components/OnboardingWelcome.vue`),关闭即从 DOM 卸载(不用 el-dialog,避免「关闭后 DOM 残留」
|
||||
的断言陷阱);显隐由 `stores/onboarding.ts` 承载,登录页 / 设置页 / App.vue 三处共用一个开关。
|
||||
|
||||
#### 作业看板(学生端)
|
||||
#### 作业看板(ClassHelper 班级端)
|
||||
|
||||
默认按科目分卡片(每张卡列该科作业,条目可点开详情),工具栏可切「看板 / 列表」、开关「显示时间」、
|
||||
拖「字号」(11–28px 即时生效),点卡片或「全屏看板」进入全屏放大视图;这些偏好写入客户端配置
|
||||
@@ -403,15 +402,16 @@ Element Plus 也通过对齐它的 `--el-border-radius-*` 一并改造:
|
||||
| 时间轴 | 左侧时间栏(开始 / 结束)+ 竖向轨道与节点 + 右侧课卡:**已结束**(置灰)、**正在上课**(强调色实底 + 进度条 + 距下课倒计时)、**下一节**(橙色标记 + 还有多久开始) |
|
||||
| 周视图 | 保留「节次 × 星期」表格(`本周` 标签) |
|
||||
|
||||
实拍(`docs/screenshots/client/`):
|
||||
实拍(`node website/tools/capture-shots.mjs` 采集,落在 `docs/images/` —— 与 README、官网用的是同一份图):
|
||||
|
||||
| 页面 | 截图 |
|
||||
| --------------------- | ------------------------------------------------------------ |
|
||||
| 课表 · 今天(时间轴) |  |
|
||||
| 作业 |  |
|
||||
| 通知 |  |
|
||||
| 成绩 |  |
|
||||
| 设置 |  |
|
||||
| 页面 | 截图 |
|
||||
| --------------------- | ---------------------------------------------------- |
|
||||
| 课表 · 今天(时间轴) |  |
|
||||
| 作业看板 |  |
|
||||
| 通知 |  |
|
||||
| 成绩 |  |
|
||||
| 设置 · 灵动岛 |  |
|
||||
| 设置 · 外观(深色) |  |
|
||||
|
||||
#### 个性化设置与系统托盘
|
||||
|
||||
@@ -440,7 +440,7 @@ Element Plus 也通过对齐它的 `--el-border-radius-*` 一并改造:
|
||||
教师 / 管理员本人密码在 Web 管理端顶栏下拉修改 —— 防止教室机器上误改班级密码后其他机器被锁在门外。
|
||||
|
||||
**系统托盘**:关闭主窗口不退出程序,而是隐藏到托盘后台继续接收通知;托盘图标(左键单击 / 双击)恢复主窗口,
|
||||
右键菜单提供「显示主窗口 / 隐藏到托盘 / 退出班级小助手」。退出时统一清理:销毁托盘 → 销毁灵动岛
|
||||
右键菜单提供「显示主窗口 / 隐藏到托盘 / 退出ClassHelper」。退出时统一清理:销毁托盘 → 销毁灵动岛
|
||||
(含帧循环与定时器)→ 销毁全部窗口,并用 `app.exit(0)` 兜底,任务管理器中不留残留进程。
|
||||
|
||||
#### 离线缓存与自动同步
|
||||
@@ -518,15 +518,18 @@ Element Plus 也通过对齐它的 `--el-border-radius-*` 一并改造:
|
||||
- 相关文件:`src/main/island.ts`(控制器 + IPC)、`src/preload/island.ts`、`src/island/{IslandApp.vue,spring.ts,squircle.ts}`、
|
||||
`src/renderer/island/bridge.ts`。
|
||||
|
||||
各形态实拍(由 `pnpm verify:desktop` 自动截取并归档到 `docs/screenshots/island/`,并做像素级校验):
|
||||
各形态实拍(显示的是 `docs/images/` —— 与 README、官网同一份,按**逻辑像素 1×** 采集;
|
||||
`pnpm verify:desktop` 另有一套逐像素校验过的留档在 `docs/screenshots/island/`,那是回归产物,不在这页展示):
|
||||
|
||||
| 形态 | 截图 | 尺寸(逻辑 / 图片) | 说明 |
|
||||
| ------------------------ | ------------------------------------------------------------------- | ------------------- | ------------------------------------------------- |
|
||||
| 下课后自动弹出详情 |  | 424×230 / 848×460 | 上课期间暂存的通知,下课后自动弹出 |
|
||||
| 上课期间紧急通知 |  | 440×246 / 880×492 | 卡片**内部**红色光晕 + 紧急角标,无需点击 |
|
||||
| 非上课时段「新消息」胶囊 |  | 268×44 / 536×88 | 直接出现(无入场动画),点击后展开 |
|
||||
| 点击胶囊后的详情 |  | 424×230 / 848×460 | 标题 / 内容 / 时间 + 打开应用 / 标为已读 / 知道了 |
|
||||
| 新作业胶囊 / 叫人卡片 | `island-7-homework.png` / `island-8-call.png` | 424×230 / 456×262 | 作业与叫人各自的配色与徽标 |
|
||||
| 形态 | 截图 | 尺寸(逻辑 px) | 说明 |
|
||||
| ------------------------ | ---------------------------------------------------- | --------------- | ------------------------------------------------- |
|
||||
| 下课后自动弹出详情 |  | 424×230 | 上课期间暂存的通知,下课后自动弹出 |
|
||||
| 上课期间紧急通知 |  | 440×246 | 卡片**内部**红色光晕 + 紧急角标,无需点击 |
|
||||
| 非上课时段「新消息」胶囊 |  | 268×44 | 直接出现(无入场动画),点击后展开 |
|
||||
| 点击胶囊后的详情 |  | 424×230 | 标题 / 内容 / 时间 + 打开应用 / 标为已读 / 知道了 |
|
||||
| 多条通知竖向排列 |  | 424×303 | 按重要程度排序,默认显示 3 条 +「展开更多」 |
|
||||
| 新作业胶囊 |  | 424×230 | 青蓝描边 + 书本图标 |
|
||||
| 叫人卡片 |  | 456×262 | 琥珀金卡片 +「叫人」徽标 +「收到」按钮 |
|
||||
|
||||
> 截图路径可用 `ISLAND_SHOTS_DIR` 覆盖(默认 `.cache/island-shots/`)。
|
||||
|
||||
@@ -556,7 +559,7 @@ Element Plus 也通过对齐它的 `--el-border-radius-*` 一并改造:
|
||||
老师在 **Web 管理端 → 学生管理 → 叫人**(或 **通知发布 → 叫人**)选中学生,配合**快捷短语**或**自定义消息**发送。
|
||||
学生管理页是管理员专属,而班主任与科任老师都需要叫人,因此教师的叫人入口在「通知发布 → 叫人」。
|
||||
|
||||
| 级别 | 落库级别 | 学生端行为 |
|
||||
| 级别 | 落库级别 | ClassHelper 班级端行为 |
|
||||
| ------------ | -------- | --------------------------------------------------------------------------------------------------------- |
|
||||
| **普通叫人** | `HIGH` | 课间:先显示「叫人」胶囊,点击展开;**上课时段:只进队列、不打断课堂**,下课后自动弹出详情(90 秒后收起) |
|
||||
| **紧急叫人** | `URGENT` | **无视上课时段立即展开**(与紧急通知同待遇),上课时被收起后仍保留胶囊、可再次点开 |
|
||||
@@ -680,15 +683,15 @@ POST /api/notifications { …, priority: "NORMAL" }(上课时段)
|
||||
|
||||
| 方向 | 做什么 | 老师在哪看 |
|
||||
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------ | -------------------------------------- |
|
||||
| ClassIsland → 班级小助手 | 上报**课表 + 节次时间 + 上课状态**(现在上什么 / 下一节 / 本节起止时间),写入本班课表 | Web 端「ClassIsland 联动」实时状态卡片 |
|
||||
| 班级小助手 → ClassIsland | 老师在 Web 端发一条**提醒** → 落库 → 教室机器的插件取回 → ClassIsland 上**全屏弹出**(可语音朗读),播完回执、不会重复弹 | Web 端「ClassIsland 联动 → 下发提醒」 |
|
||||
| (可选)班级 → ClassIsland | 把班级小助手上排好的课表**镜像**回 ClassIsland:新建一份 `班级小助手-<班级名>` 档案课表,不动老师原有的课表 | 同上页面的「开镜像」开关 |
|
||||
| ClassIsland → ClassHelper | 上报**课表 + 节次时间 + 上课状态**(现在上什么 / 下一节 / 本节起止时间),写入本班课表 | Web 端「ClassIsland 联动」实时状态卡片 |
|
||||
| ClassHelper → ClassIsland | 老师在 Web 端发一条**提醒** → 落库 → 教室机器的插件取回 → ClassIsland 上**全屏弹出**(可语音朗读),播完回执、不会重复弹 | Web 端「ClassIsland 联动 → 下发提醒」 |
|
||||
| (可选)班级 → ClassIsland | 把ClassHelper上排好的课表**镜像**回 ClassIsland:新建一份 `ClassHelper-<班级名>` 档案课表,不动老师原有的课表 | 同上页面的「开镜像」开关 |
|
||||
|
||||
#### 接入流程(三步)
|
||||
|
||||
1. **Web 端签发设备令牌**:管理员 / 本班班主任在「ClassIsland 联动」页点「接入新设备」,选择班级并生成令牌
|
||||
(形如 `chci_xxxxxxxx…`,**明文只显示这一次**,服务端只存 sha256);
|
||||
2. **教室机器填令牌**:在 ClassIsland 的「设置 → 班级小助手联动」里填入后端地址 + 令牌,点「立即上报」;
|
||||
2. **教室机器填令牌**:在 ClassIsland 的「设置 → ClassHelper联动」里填入后端地址 + 令牌,点「立即上报」;
|
||||
3. **验证**:页面顶部出现设备卡片与「在线」标记,说明链路通了 —— 之后课表与上课状态自动同步,
|
||||
老师发的提醒也会在这台机器上弹出来。
|
||||
|
||||
@@ -718,8 +721,8 @@ POST /api/notifications { …, priority: "NORMAL" }(上课时段)
|
||||
| 要同步的内容 | 需要打开的开关 |
|
||||
| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 服务端 → ClassIsland(镜像课表) | ① 插件设置「把班级课表镜像回 ClassIsland」+ ② Web 端该设备的「镜像课表」 |
|
||||
| ClassIsland → 服务端(上报课表) | 插件设置「上报课表到班级小助手」(关着时日志会明说「仅状态(开关已关闭)」);同步是**双向对齐**的:老师在 ClassIsland 里删掉 / 挪动的节次会一并清理,Web 端手排的课不会被删 |
|
||||
| 提醒下发 | ① 教室客户端「通知显示位置」不是「只在 ClassHelper」+ ② 插件「接收班级小助手提醒」 |
|
||||
| ClassIsland → 服务端(上报课表) | 插件设置「上报课表到ClassHelper」(关着时日志会明说「仅状态(开关已关闭)」);同步是**双向对齐**的:老师在 ClassIsland 里删掉 / 挪动的节次会一并清理,Web 端手排的课不会被删 |
|
||||
| 提醒下发 | ① 教室客户端「通知显示位置」不是「只在 ClassHelper」+ ② 插件「接收ClassHelper提醒」 |
|
||||
|
||||
#### 提醒弹在哪个端:由教室客户端自己选
|
||||
|
||||
@@ -852,10 +855,11 @@ class-helper/
|
||||
│ ├── production.md # 生产部署指南(四种形态 + Linux systemd + 运维 + 安全清单)
|
||||
│ ├── mysql.md # MySQL 切换指南
|
||||
│ ├── winisland-design-tokens.md # 灵动岛照 WinIsland 提取的设计 token
|
||||
│ └── screenshots/ # 回归测试自动留档的截图(island / client / web-mobile / classisland)
|
||||
│ ├── images/ # 展示图:文档站 / README / 官网共用的同一份(1× 逻辑像素)
|
||||
│ └── screenshots/ # 回归测试留档的截图(island / client / web-mobile / classisland),页面不展示
|
||||
├── website/ # 产品官网(纯静态,无构建;node serve.mjs 本地预览)
|
||||
│ ├── index.html
|
||||
│ └── assets/{styles.css,motion.js,main.js,icon.png,shots/} # motion.js = beUI 动效 token 与弹簧引擎
|
||||
│ ├── index.html / download.html
|
||||
│ └── assets/{styles.css,motion.js,main.js,download.js,icon.png,shots/} # motion.js = beUI 动效 token 与弹簧引擎
|
||||
└── packages/
|
||||
├── shared/ # 三端共享:类型契约、常量、权限矩阵、工具函数
|
||||
│ └── src/{types,constants,permissions,utils,index}.ts
|
||||
@@ -890,7 +894,7 @@ class-helper/
|
||||
│ ├── src/Services/ScheduleMapper.cs # 课表与单双周映射
|
||||
│ ├── src/Services/ClassPlanWriter.cs # 镜像回 ClassIsland(含课间时间点)
|
||||
│ ├── src/Services/ClassHelperNotificationProvider.cs # 把老师发的提醒显示到 ClassIsland 上
|
||||
│ ├── src/Views/BridgeSettingsPage.axaml(.cs) # 「班级小助手联动」设置页(Avalonia)
|
||||
│ ├── src/Views/BridgeSettingsPage.axaml(.cs) # 「ClassHelper联动」设置页(Avalonia)
|
||||
│ ├── src/Interop/ClassHelperClient.cs # 后端 HTTP 客户端 + 对齐服务端 zod 的 DTO
|
||||
│ └── scripts/{build,verify}.mjs # 构建打包(.cipx)与静态契约校验
|
||||
├── web-admin/ # Web 管理端(Vue 3 + Vite + Element Plus + PWA)
|
||||
@@ -1191,7 +1195,7 @@ pnpm db:generate && pnpm --filter @classhelper/server db:deploy && pnpm db:seed
|
||||
|
||||
| 验收项 | 结果 | 证据 |
|
||||
| ------------------------------------------------------------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 教师 Web 端发布通知,学生端 5 秒内收到 | ✅ | `verify:e2e`:`notification:new` 实测 30–50ms 到达(要求 < 5 秒) |
|
||||
| 教师 Web 端发布通知,ClassHelper 班级端 5 秒内收到 | ✅ | `verify:e2e`:`notification:new` 实测 30–50ms 到达(要求 < 5 秒) |
|
||||
| 教师发布作业,学生能查看并标记完成 | ✅ | `homework:new` 实时到达,`PATCH /homeworks/:id/status` 200 且列表回显 `completed=true` |
|
||||
| 教师录入成绩,学生能查看个人成绩 | ✅ | `grade:updated` 实时到达,`/grades/my` 返回记录;批量录入与统计接口通过 |
|
||||
| 学生能查看课表,支持按周切换 | ✅ | `/schedules/grid?week=1` 返回周视图条目,`week` 过滤 `weekStart ≤ week ≤ weekEnd`,单双周按第 1 周 = 单周过滤 |
|
||||
@@ -1211,7 +1215,7 @@ pnpm db:generate && pnpm --filter @classhelper/server db:deploy && pnpm db:seed
|
||||
| 成绩 / 名单表格导入(xlsx·xls·csv) | ✅ | 模板下载 + 预览映射 + 重复处理 + 行号级错误:`verify:e2e` 覆盖 20 余项,`verify:web` 弹窗实测 |
|
||||
| ClassIsland 时间配置导入(覆盖 / 合并 / 回滚) | ✅ | 合法 200、非法 400 `IMPORT_INVALID` 且原配置仍在、merge 覆盖与保留行为符合预期 |
|
||||
| 安装版覆盖升级自动补迁移 | ✅ | 真实旧库升级日志:`升级安装:已应用 N 个迁移文件,跳过 M 个已存在对象`,新表自动建好 |
|
||||
| 学生端主体 = 班级(班级码 + 班级密码登录) | ✅ | `verify:e2e` 班级账号 19 项:登录 / 错误密码 401 / 跨班 403 / 发布 403 / 班级码重复 400 / 密码重置 / 班级码用后还原 |
|
||||
| ClassHelper 班级端主体 = 班级(班级码 + 班级密码登录) | ✅ | `verify:e2e` 班级账号 19 项:登录 / 错误密码 401 / 跨班 403 / 发布 403 / 班级码重复 400 / 密码重置 / 班级码用后还原 |
|
||||
| 班级设备代全班操作(已读 · 完成 · 成绩总览) | ✅ | 标记已读写入全班、教师端 `readCount` = 班级学生数、`completedCount` 一致、`/grades/my` 返回全班成绩 |
|
||||
| 客户端只保留班级登录入口 | ✅ | `verify:desktop`:导航「成绩」标题变为「本班成绩」;个人学生登录 403 |
|
||||
| **教师录入(仅管理员)** | ✅ | `verify:e2e`:教师读 / 建教师 403、管理员建 201 → 编辑 → 重置密码(新密码 200 / 旧密码 401)→ 删除(有职责 409 / 无职责 200);`verify:web`:管理员真人点「教师管理 → 新建教师 → 保存 → 列表出现 → 删除」 |
|
||||
@@ -1225,12 +1229,12 @@ pnpm db:generate && pnpm --filter @classhelper/server db:deploy && pnpm db:seed
|
||||
| **客户端设置改版(分组侧栏 / 深色模式 / 关于页)** | ✅ | `verify:desktop`:「侧边栏点击导航」适配「学习 / 设置」分组;「主题切换与侧边栏折叠(真实点击、写回配置)」——`html.dark` 翻转 + 折叠宽度 200→64 + 双双落盘;「关于页渲染」含 configPath |
|
||||
| **数据库管理(仅管理员:状态 / 备份 / 导入导出 / 一键切换)** | ✅ | `verify:e2e` 13 项:状态与 14 表行数、教师全端点 403、连接测试(当前库可达 / 不可达 MySQL ok=false)、**Redis 主库 422**、切换校验分支(同库 400)、备份 → 快照导入 → 备份恢复 roundtrip(临时数据被清除)、定时配置校验、SQLite 文件下载;**切换子进程链路真机实测**:快照 728 行 → generate → db push → apply-snapshot 逐表一致;`verify:web`:数据库管理页(仅管理员)状态卡与备份表渲染 |
|
||||
| **更新检查(服务端 / Web 端 / 客户端)** | ✅ | `verify:e2e` 4 项:未登录 401、管理员结构完整且本机版本取自 package.json、教师可查但 `force` 仅管理员生效、健康检查暴露版本号与 15 个模块;`verify:desktop` 4 项:IPC 返回结构正确(无外网时降级 `ok=false` 且带原因)、结果带缓存、冒烟模式跳过启动自动检查、关于页出现「检查更新」;`verify:web`:「顶栏用户菜单 → 弹窗给出结论并可关闭」。**真机联网实测**:`GET /api/update/check` → `ok=true`、`latestVersion=1.0.0`、5 个附件、更新说明 1358 字 |
|
||||
| **其他页面发通知也会联动 ClassIsland** | ✅ | `verify:classisland`:「通知发布页发的通知也会推送」「叫人也会推送」;真机实测截图 [03-from-notify-page](screenshots/classisland/03-from-notify-page.png) |
|
||||
| **其他页面发通知也会联动 ClassIsland** | ✅ | `verify:classisland`:「通知发布页发的通知也会推送」「叫人也会推送」;2026-09-27 在真机(ClassIsland 2.1.0.1 + 本插件)上实测过,当时的抓图留在 `docs/screenshots/classisland/` |
|
||||
| **作业按天查看(日期选择器 + 有作业日期高亮)** | ✅ | `verify:e2e`:`assignDate` 默认服务器当天、`?date=` 只返回该天、`/homeworks/days` 返回有作业日期、非法日期 422 |
|
||||
| **支持在客户端录入作业(含自定义快捷短语)** | ✅ | `verify:e2e`:班级账号录入 201(归属班主任)、跨班 403;客户端录入弹窗 + 「设置 → 通用」管理短语 |
|
||||
| **提醒弹在哪个端由客户端自行选择** | ✅ | 客户端「设置 → 提醒」三选一(`verify:desktop`:设置页真实点击 → 写回本地配置);服务端按班级设置决定是否推送(`verify:classisland`:`client` 时通知与联动页下发都被跳过) |
|
||||
| **班级小助手能读取 ClassIsland 的课表** | ✅ | `verify:classisland`:插件上报课表 → 本班课表新增 / 更新(重复上报幂等)、节次时间写入、设备状态快照可在 Web 端展示;`verify:web`:教师在「ClassIsland 联动」页真实签发设备令牌 |
|
||||
| **用户可选择通知是否在 ClassIsland 上显示** | ✅ | 插件设置页开关「接收班级小助手提醒」(本机总闸)+ Web 端「下发提醒」(标题 / 内容 / 时长 / 优先级 / 语音朗读 / 是否同步通知中心);`verify:classisland`:提醒落库 → 待提醒 → 回执 → 确认后不补发 |
|
||||
| **ClassHelper能读取 ClassIsland 的课表** | ✅ | `verify:classisland`:插件上报课表 → 本班课表新增 / 更新(重复上报幂等)、节次时间写入、设备状态快照可在 Web 端展示;`verify:web`:教师在「ClassIsland 联动」页真实签发设备令牌 |
|
||||
| **用户可选择通知是否在 ClassIsland 上显示** | ✅ | 插件设置页开关「接收ClassHelper提醒」(本机总闸)+ Web 端「下发提醒」(标题 / 内容 / 时长 / 优先级 / 语音朗读 / 是否同步通知中心);`verify:classisland`:提醒落库 → 待提醒 → 回执 → 确认后不补发 |
|
||||
| 能成功打包 Windows EXE | ✅ | `-x64-setup.exe` / `-x64-portable.exe` / `win-unpacked/*.exe`(见 [7. 打包与交付](#7-打包与交付)) |
|
||||
| 提供完整 README(启动、构建、打包、默认账号) | ✅ | 本文档含快速开始、命令表、API、WebSocket、RBAC、模块化、数据库、打包与常见问题 |
|
||||
|
||||
@@ -1287,7 +1291,7 @@ pnpm db:generate && pnpm --filter @classhelper/server db:deploy && pnpm db:seed
|
||||
| 客户端渲染产物 `dist/renderer` | 3.2MB | **2.1MB** | 成绩页图表从 echarts 全量(1.12MB,占 35%)换成内置 HTML/SVG 组件;Element Plus 图标从全量注册 293 个改为按需 42/33 个 |
|
||||
| 客户端主进程产物体积 | 242KB | 242KB | 未变(冒烟代码拆分会重复 `island`/`config` 单例,esbuild 的 CJS 打包无法代码分割,故保留) |
|
||||
| 客户端 `win-unpacked` | 372MB | **323MB** | `electronLanguages: [zh-CN]`,只留中文语言包(其余 54 个 `locales/*.pak` 共 48MB) |
|
||||
| 客户端 `setup.exe` / `portable.exe` | 107MB | **98MB** | 同上。**安装包有硬底**:`班级小助手.exe`(electron.exe)压缩后本身就有 85~90MB,60MB 不可达 |
|
||||
| 客户端 `setup.exe` / `portable.exe` | 107MB | **98MB** | 同上。**安装包有硬底**:`ClassHelper.exe`(electron.exe)压缩后本身就有 85~90MB,60MB 不可达 |
|
||||
| 服务端 `dist` | 3.8MB | **0.6MB** | `tsconfig.build.json` 关掉 `declaration`/`declarationMap`/`sourceMap`(占原体积 82%) |
|
||||
| 服务端免安装目录 | 436MB | **335MB** | 打包末尾的运行时裁剪:删掉 Prisma 为 postgresql/cockroachdb/sqlserver 准备的查询编译器副本、`*.map`、文档与测试夹具(共 2329 项 / 101MB) |
|
||||
| 服务端安装程序 | 71MB | **62MB** | 同上 |
|
||||
@@ -1302,7 +1306,7 @@ pnpm db:generate && pnpm --filter @classhelper/server db:deploy && pnpm db:seed
|
||||
### 客户端(Windows EXE)
|
||||
|
||||
```bash
|
||||
pnpm dist:dir # 免安装目录:releases/client/<版本>/免安装/班级小助手.exe
|
||||
pnpm dist:dir # 免安装目录:releases/client/<版本>/免安装/ClassHelper.exe
|
||||
pnpm dist:win # nsis 安装包 + portable 单文件(需联网下载 NSIS 工具,可用镜像)
|
||||
```
|
||||
|
||||
@@ -1313,7 +1317,7 @@ pnpm dist:win # nsis 安装包 + portable 单文件(需联网下载 NSIS
|
||||
electron-builder 的原始布局整理成 `安装包/`、`免安装/`、`构建中间/`;
|
||||
收尾把交付物完整性标签改成 Medium(Low 标签会导致装不上 / 双击没反应,见 [8. 常见问题](#8-常见问题))。
|
||||
|
||||
产物结构:`免安装/班级小助手.exe`(Electron 运行时)+ `resources/app.asar`
|
||||
产物结构:`免安装/ClassHelper.exe`(Electron 运行时)+ `resources/app.asar`
|
||||
(仅含 `dist/` 与 `package.json`,运行时不需要 node_modules)。
|
||||
|
||||
> **把修复交付到「已安装的客户端」**:`pnpm dist:win` 只产出 `releases/client/<版本>/`。
|
||||
@@ -1325,7 +1329,7 @@ electron-builder 的原始布局整理成 `安装包/`、`免安装/`、`构建
|
||||
> # 2) 用新产物覆盖安装目录(不删 Uninstall 等安装器文件,所以不加 /MIR)
|
||||
> robocopy "packages\desktop-client\release\win-unpacked" "D:\class\@classhelperdesktop-client" /E /NFL /NDL /NJH /NJS /R:2 /W:1
|
||||
> # 3) 实测**那个副本**真的带上了修复(packaged=true)
|
||||
> pnpm verify:packaged --exe "D:\class\@classhelperdesktop-client\班级小助手.exe"
|
||||
> pnpm verify:packaged --exe "D:\class\@classhelperdesktop-client\ClassHelper.exe"
|
||||
> ```
|
||||
|
||||
最近一次打包实测(产物不入库):
|
||||
@@ -1333,7 +1337,7 @@ electron-builder 的原始布局整理成 `安装包/`、`免安装/`、`构建
|
||||
| 产物 | 大小 | 冒烟结果 |
|
||||
| -------------------------------------- | ------ | -------------------------------------------------------------------- |
|
||||
| 服务端安装程序(内置 Node + Web 端) | ~62MB | ✅ 静默安装(升级保留 `.env` 与数据库)→ 自动建库建号 → 服务就绪 |
|
||||
| `releases/client/<版本>/免安装/班级小助手.exe` | ~235MB | ✅ 打包版冒烟全绿,`packaged: true`,退出码 0 |
|
||||
| `releases/client/<版本>/免安装/ClassHelper.exe` | ~235MB | ✅ 打包版冒烟全绿,`packaged: true`,退出码 0 |
|
||||
| `releases/client/<版本>/安装包/…-portable.exe` | ~98MB | ✅ 打包后冒烟全绿,`packaged: true`,退出码 0 |
|
||||
| `releases/client/<版本>/安装包/…-setup.exe` | ~98MB | ✅ 构建成功,已嵌入自定义图标 |
|
||||
| `releases/classisland-plugin/<版本>/…cipx` | ~88KB | ✅ 全量编译通过(1 警告 0 错误),包内 DLL 哈希与 `bin/Release` 一致 |
|
||||
@@ -1404,25 +1408,16 @@ node website/serve.mjs # http://127.0.0.1:5180,只是本地预览用
|
||||
> 标题与引文用的是楷体(优先加载霞鹜文楷 Screen,失败回落到系统 `KaiTi` / `STKaiti`),
|
||||
> 正文用产品自己那套 UI 字体。字体是 CDN 引入的,**断网时会自动回落,不影响阅读**。
|
||||
|
||||
#### 采集实机图(`website/assets/shots/`)
|
||||
#### 采集实机图
|
||||
|
||||
官网上那一栏实机截图**不是画的示意**,是脚本真的把程序跑起来截的。要重新采集:
|
||||
官网上那一栏实机截图**不是画的示意**,是脚本真的把程序跑起来截的:Web / 手机走
|
||||
`website/tools/capture-shots.mjs`,客户端页面与灵动岛走客户端冒烟。
|
||||
|
||||
```bash
|
||||
pnpm dev:server # 后端(同时托管 Web 管理端)
|
||||
node website/tools/capture-shots.mjs # → web-*.jpg / mobile-*.jpg
|
||||
|
||||
# 客户端页面 + 灵动岛各形态(跑一遍客户端冒烟,顺便留档)
|
||||
ISLAND_SHOTS_DIR=F:/ClassHelper/.cache/island-shots \
|
||||
ELECTRON_CLIENT_SHOTS_DIR=F:/ClassHelper/.cache/client-shots \
|
||||
ELECTRON_SMOKE_ISLAND_ACCENT='#0a84ff' ELECTRON_SMOKE_ISLAND_STYLE=black \
|
||||
ELECTRON_SMOKE_USER=teacher1 ELECTRON_SMOKE_PASSWORD=123456 \
|
||||
node packages/desktop-client/scripts/smoke.mjs
|
||||
```
|
||||
展示图统一放在 `docs/images/` —— 文档站、README 与官网吃的是**同一份文件**,
|
||||
重新采集的完整命令(含目录与清理)写在[验收与测试](dev/testing.md)的「截图留档」一节。
|
||||
|
||||
两个环境变量在 `smoke.ts` 里是**默认值为洋红**的开关:冒烟平时故意用洋红主题色把验证实例的岛
|
||||
和用户自己的岛区分开(见 AGENTS.md §5 第 38 条),采集留档图时才换成产品默认外观。
|
||||
指向 `website/assets/shots/` 的那两个目录变量不在 `website/`,要自己拷过去。
|
||||
|
||||
> 采集脚本会把窗口**真的弹在屏幕上**(和 `verify:web` / `verify:desktop` 一样),跑的时候别动键鼠。
|
||||
> 客户端留档会**临时停掉 CSS 动画**再截图:窗口被遮挡时 Chromium 冻结动画,页面入场动画的起始帧是
|
||||
|
||||
+13
-5
@@ -4,8 +4,9 @@
|
||||
*
|
||||
* ## 为什么要这个脚本
|
||||
*
|
||||
* 这个仓库里**同一个版本号写在 13 处(11 个文件)**:5 份 package.json、运行时常量、两个 compose 文件、
|
||||
* 插件清单、插件 C# 源码里的一个 const,以及官网页面上三处手写的版本号。手工改必然漏一处,
|
||||
* 这个仓库里**同一个版本号写在 14 处(12 个文件)**:5 份 package.json、运行时常量、两个 compose 文件、
|
||||
* 插件清单、插件 C# 源码里的一个 const,以及官网页面上三处手写的版本号、下载页那一处兜底值。
|
||||
* 手工改必然漏一处,
|
||||
* 而漏掉的那处**不会报错**,只会在某个不常走的分支上表现为"版本号对不上":
|
||||
*
|
||||
* - `deploy/docker-compose*.yml` 镜像 tag 落后 → 服务器上跑的还是旧镜像;
|
||||
@@ -14,7 +15,9 @@
|
||||
* (清单里的 version 与实际上报的 PluginVersion 不一致时,
|
||||
* Web 端「ClassIsland 联动」页显示的版本会对不上插件市场);
|
||||
* - `website/index.html` 的那三处 官网是纯静态站、无构建步骤,没有哪段代码会去读
|
||||
* `package.json`,漏改的后果是**首页上一直挂着旧版本号** —— 那是别人看到的第一眼。
|
||||
* `package.json`,漏改的后果是**首页上一直挂着旧版本号** —— 那是别人看到的第一眼;
|
||||
* - `website/download.html` 的 `<meta name="ch-version">` 下载页拿它当"读不到版本清单"时的兜底,
|
||||
* 漏改的后果是**离线访客点到的下载链接指向一个不存在的版本**。
|
||||
*
|
||||
* 因此:**改版本号的唯一入口是这个脚本**。它按表逐个改写,任何一处没匹配到(或匹配到多处)
|
||||
* 就直接失败退出,不做"尽力而为"的部分改写。
|
||||
@@ -74,8 +77,8 @@ const PLUGIN_TARGETS = [
|
||||
];
|
||||
|
||||
/**
|
||||
* 官网首屏与页脚那三处手写的版本号。官网是纯静态站,没有构建步骤、也没有哪段代码会去读
|
||||
* `package.json`,所以这三处只能靠脚本一起改 —— 漏了的话官网会一直显示旧版本号,
|
||||
* 官网首页与下载页上那几处手写的版本号。官网是纯静态站,没有构建步骤、也没有哪段代码会去读
|
||||
* `package.json`,所以这几处只能靠脚本一起改 —— 漏了的话官网会一直显示旧版本号,
|
||||
* 而它是别人看到的第一眼。
|
||||
*/
|
||||
const SITE_TARGETS = [
|
||||
@@ -90,6 +93,11 @@ const SITE_TARGETS = [
|
||||
label: '(页脚)',
|
||||
pattern: /(<p class="foot-ver">版本 )(\d+\.\d+\.\d+)( ·)/,
|
||||
},
|
||||
{
|
||||
file: 'website/download.html',
|
||||
label: '(版本清单的兜底值)',
|
||||
pattern: /(<meta name="ch-version" content=")(\d+\.\d+\.\d+)(" \/>)/,
|
||||
},
|
||||
];
|
||||
|
||||
const ALL_TARGETS = [...PRODUCT_TARGETS, ...PLUGIN_TARGETS, ...SITE_TARGETS];
|
||||
|
||||
+2
-1
@@ -13,7 +13,7 @@
|
||||
<base href="{{BASE}}" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="robots" content="noindex" />
|
||||
<title>页面不存在 | 班级小助手</title>
|
||||
<title>页面不存在 | ClassHelper</title>
|
||||
<style>
|
||||
:root {
|
||||
--board: #16231e;
|
||||
@@ -126,6 +126,7 @@
|
||||
<p>链接可能过期了,或者这一页已经被重写进了别的章节。</p>
|
||||
<div class="links">
|
||||
<a class="primary" href="index.html">回到官网首页</a>
|
||||
<a href="download.html">下载</a>
|
||||
<a href="docs/index.html">文档站</a>
|
||||
<a href="docs/get-started/index.html">快速上手</a>
|
||||
<a href="docs/faq.html">常见问题</a>
|
||||
|
||||
@@ -0,0 +1,434 @@
|
||||
/**
|
||||
* ClassHelper · 下载页(download.html)
|
||||
*
|
||||
* 这一页只做一件事:**挑一个版本,挑一份文件,点下去就下**。
|
||||
* 版本与文件清单有两种来路,按可靠性兜底:
|
||||
*
|
||||
* ① 当场读 GitHub Releases 接口 —— 拿到的就是发布页上真实存在的资源,
|
||||
* 文件名与体积照抄,不靠命名约定去猜(这是正常路径);
|
||||
* ② 读不到时(离线、被限流、被墙)退回 HTML 里那份 `<meta name="ch-version">`,
|
||||
* 按**发布的命名约定**拼直链,并在页面上明说"这是拼出来的"。
|
||||
*
|
||||
* 为什么不把清单预生成成 JSON 放进站点:那样只在部署那一刻是新的,
|
||||
* 发了新版本得等下一次推 master 才更新。而这一页几乎每一步都指回 github.com,
|
||||
* 真离线时它本来也用不了 —— 所以真正要兜的只有"接口被限流",
|
||||
* 那由 ② 兜住就够了。匿名接口每小时 60 次、按出口 IP 算,机房出口很容易撞到。
|
||||
*
|
||||
* 动效沿用 assets/motion.js 那套 beUI token(版本分段控件的指示器与首页实机截图那组同源)。
|
||||
* 建完卡片会派发一次 `ch:content`,让 main.js 把按压反馈补挂上去
|
||||
* —— 这些元素是拉到清单之后才出现的,main.js 启动时还看不到它们。
|
||||
*/
|
||||
|
||||
(() => {
|
||||
'use strict';
|
||||
|
||||
const REPO = 'LoveChengke/ClassHelper';
|
||||
const RELEASES_API = `https://api.github.com/repos/${REPO}/releases?per_page=30`;
|
||||
const RELEASES_PAGE = `https://github.com/${REPO}/releases`;
|
||||
|
||||
/** 版本选择器最多列几版;更早的去 Releases 页看 */
|
||||
const MAX_VERSIONS = 12;
|
||||
|
||||
const motion = window.CHMotion ?? null;
|
||||
|
||||
const $ = (selector, root = document) => root.querySelector(selector);
|
||||
const $$ = (selector, root = document) => [...root.querySelectorAll(selector)];
|
||||
|
||||
/** 页面上那份兜底版本号,由 `pnpm version:bump` 与根 package.json 一起维护 */
|
||||
const FALLBACK_VERSION = ($('meta[name="ch-version"]')?.content ?? '').trim();
|
||||
|
||||
/**
|
||||
* 四个交付物 = 四张卡;卡里每一行 = Release 上的一个文件。
|
||||
*
|
||||
* `match` 用来在真实资源里认领这个文件,`expect` 在没有接口时按命名约定拼直链的名字。
|
||||
* **两个都要跟着发布流程一起改** —— 名字口径与 README「开始使用」那张表、
|
||||
* `.cache/release-notes-*.md` 里那份清单是同一套。
|
||||
*/
|
||||
const CARDS = [
|
||||
{
|
||||
key: 'client',
|
||||
who: '教室机器 / 学生电脑',
|
||||
name: 'ClassHelper 班级端',
|
||||
desc: '第一次打开时填服务器地址和班级码,之后开机就能看课表、作业、通知与成绩;断网时读本地缓存,恢复后自己刷新。',
|
||||
files: [
|
||||
{
|
||||
role: '安装版',
|
||||
note: 'NSIS 安装程序,装完在开始菜单里',
|
||||
match: /-client-setup\.exe$/i,
|
||||
expect: (version) => `ClassHelper-${version}-x64-client-setup.exe`,
|
||||
},
|
||||
{
|
||||
role: '单文件版',
|
||||
note: '不用安装,放 U 盘里就能分发',
|
||||
match: /-client-portable\.exe$/i,
|
||||
expect: (version) => `ClassHelper-${version}-x64-client-portable.exe`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'server',
|
||||
who: '教师电脑 / 学校服务器 · Windows',
|
||||
name: 'ClassHelper 服务端',
|
||||
desc: '内含 Web 管理端与 Node 运行时,装上就算跑起来:建库、建号、开机自启都由它自己办。',
|
||||
files: [
|
||||
{
|
||||
role: '安装程序',
|
||||
note: '装完浏览器打开 http://<服务器地址>:4000',
|
||||
match: /-server-setup\.exe$/i,
|
||||
expect: (version) => `ClassHelper-${version}-x64-server-setup.exe`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'linux',
|
||||
who: 'Linux 服务器',
|
||||
name: '服务端 · systemd',
|
||||
desc: '一行命令装完,附带 classhelper 运维命令(status / upgrade / backup / doctor / password);也可以下压缩包自己解。',
|
||||
files: [
|
||||
{
|
||||
role: '安装包',
|
||||
note: '解压即用,含内置 Node 运行时',
|
||||
match: /^classhelper-server-linux-x64-.*\.tar\.gz$/i,
|
||||
expect: (version) => `classhelper-server-linux-x64-${version}.tar.gz`,
|
||||
},
|
||||
{
|
||||
role: '校验值',
|
||||
note: '安装脚本会自动取它核对',
|
||||
match: /^classhelper-server-linux-x64-.*\.tar\.gz\.sha256$/i,
|
||||
expect: (version) => `classhelper-server-linux-x64-${version}.tar.gz.sha256`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'plugin',
|
||||
who: '教室大屏 · 装在 ClassIsland 上',
|
||||
name: 'ClassIsland 联动插件',
|
||||
desc: '把课表上报给服务端,把老师发的提醒弹到 ClassIsland 的全屏提醒上。上课时段的普通通知先收着,打下课铃补弹。',
|
||||
files: [
|
||||
{
|
||||
role: '插件包',
|
||||
note: '在 ClassIsland 的插件页里选它安装',
|
||||
match: /classislandplugin\.cipx$/i,
|
||||
expect: () => 'ClassHelper.ClassIslandPlugin.cipx',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const tabs = $('#ver-tabs');
|
||||
const indicator = $('#ver-indicator');
|
||||
const note = $('#ver-note');
|
||||
const grid = $('#dl-grid');
|
||||
const gridNote = $('#dl-note');
|
||||
const primary = $('#ver-primary');
|
||||
const command = $('#cmd-linux code');
|
||||
|
||||
if (!tabs || !indicator || !note || !grid || !gridNote || !primary || !command) return;
|
||||
|
||||
/** 当前选中的版本清单来自哪儿:live(读到的) / fallback(拼出来的) */
|
||||
let source = 'live';
|
||||
let releases = [];
|
||||
let current = null;
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
小工具
|
||||
══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
function element(tag, className, text) {
|
||||
const node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (text != null) node.textContent = text;
|
||||
return node;
|
||||
}
|
||||
|
||||
/** 体积口径与 GitHub 发布页一致(1024 进制,标签写 MB) */
|
||||
function formatSize(bytes) {
|
||||
if (!bytes) return '';
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
const mb = bytes / 1024 / 1024;
|
||||
if (mb >= 1) return `${mb.toFixed(1)} MB`;
|
||||
return `${Math.round(bytes / 1024)} KB`;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
取清单
|
||||
══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
async function fetchReleases() {
|
||||
const response = await fetch(RELEASES_API, { headers: { accept: 'application/vnd.github+json' } });
|
||||
if (!response.ok) throw new Error(`GitHub 返回 ${response.status}`);
|
||||
const list = await response.json();
|
||||
if (!Array.isArray(list)) throw new Error('返回的不是发布列表');
|
||||
|
||||
return list
|
||||
.filter((item) => item && item.tag_name && !item.draft)
|
||||
.map((item) => ({
|
||||
tag: String(item.tag_name),
|
||||
version: String(item.tag_name).replace(/^v/i, ''),
|
||||
prerelease: Boolean(item.prerelease),
|
||||
publishedAt: String(item.published_at ?? '').slice(0, 10),
|
||||
notesUrl: item.html_url ?? `${RELEASES_PAGE}/tag/${item.tag_name}`,
|
||||
assets: (item.assets ?? []).map((asset) => ({
|
||||
name: String(asset.name ?? ''),
|
||||
url: String(asset.browser_download_url ?? ''),
|
||||
size: Number(asset.size) || 0,
|
||||
})),
|
||||
}))
|
||||
.slice(0, MAX_VERSIONS);
|
||||
}
|
||||
|
||||
/** 最新的一版 = 第一个正式版(预发布不占这个位置) */
|
||||
function markLatest(list) {
|
||||
const index = list.findIndex((item) => !item.prerelease);
|
||||
if (index >= 0) list[index].latest = true;
|
||||
return list;
|
||||
}
|
||||
|
||||
/** 接口读不到时的替身:版本号取自页面上的 meta,文件名按发布命名约定拼 */
|
||||
function fallbackRelease() {
|
||||
const version = FALLBACK_VERSION || '0.0.0';
|
||||
const tag = `v${version}`;
|
||||
const assets = [];
|
||||
for (const card of CARDS) {
|
||||
for (const file of card.files) {
|
||||
const name = file.expect(version);
|
||||
assets.push({ name, url: `${RELEASES_PAGE}/download/${tag}/${name}`, size: 0 });
|
||||
}
|
||||
}
|
||||
return {
|
||||
tag,
|
||||
version,
|
||||
latest: true,
|
||||
prerelease: false,
|
||||
publishedAt: '',
|
||||
notesUrl: `${RELEASES_PAGE}/tag/${tag}`,
|
||||
assets,
|
||||
};
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
版本分段控件 —— beUI Tabs:一个指示器在选项之间滑移
|
||||
══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
let setIndicator = null;
|
||||
if (motion) {
|
||||
const x = new motion.Spring(0, motion.SPRING_LAYOUT, (value) => {
|
||||
indicator.style.transform = `translateX(${motion.round2(value)}px)`;
|
||||
});
|
||||
const width = new motion.Spring(0, motion.SPRING_LAYOUT, (value) => {
|
||||
indicator.style.width = `${motion.round2(value)}px`;
|
||||
});
|
||||
setIndicator = (left, size, animate) => {
|
||||
if (!animate || motion.reduceMotion()) {
|
||||
x.jump(left);
|
||||
width.jump(size);
|
||||
return;
|
||||
}
|
||||
// 首次落位时宽度弹簧还在 0,直接跳过去,免得从 0 撑开
|
||||
if (width.value === 0) width.jump(size);
|
||||
x.set(left);
|
||||
width.set(size);
|
||||
};
|
||||
} else {
|
||||
// 动效层没加载出来也要能用:直接落位
|
||||
setIndicator = (left, size) => {
|
||||
indicator.style.transform = `translateX(${left}px)`;
|
||||
indicator.style.width = `${size}px`;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 位置用 offsetLeft / offsetWidth 量:指示器是绝对定位在 `.dl-tabs`(position: relative)里的,
|
||||
* 两者的参照边都是容器的内边距边 —— 而且这样**与容器的横向滚动无关**
|
||||
* (版本多了以后这排是可以横滚的,用 getBoundingClientRect 会在滚动后对不上)。
|
||||
*/
|
||||
function moveIndicator(tag, animate) {
|
||||
const button = $$('.dl-tab', tabs).find((node) => node.dataset.tag === tag);
|
||||
if (!button) return;
|
||||
setIndicator(button.offsetLeft, button.offsetWidth, animate);
|
||||
}
|
||||
|
||||
function renderTabs() {
|
||||
$$('.dl-tab', tabs).forEach((node) => node.remove());
|
||||
|
||||
for (const release of releases) {
|
||||
const button = element('button', 'dl-tab');
|
||||
button.type = 'button';
|
||||
button.setAttribute('role', 'tab');
|
||||
button.dataset.tag = release.tag;
|
||||
button.dataset.press = '0.96';
|
||||
button.setAttribute('aria-selected', 'false');
|
||||
button.append(element('span', null, release.tag));
|
||||
|
||||
if (release.latest) button.append(element('span', 'dl-tab-flag', '最新'));
|
||||
else if (release.prerelease) button.append(element('span', 'dl-tab-flag dl-tab-flag-quiet', '预发布'));
|
||||
|
||||
button.addEventListener('click', () => select(release.tag));
|
||||
tabs.append(button);
|
||||
}
|
||||
|
||||
// 首帧之后重量一次:楷体是 CDN 来的,字体落定前量出来的宽度是错的
|
||||
moveIndicator(current?.tag ?? releases[0]?.tag, false);
|
||||
if (motion) {
|
||||
motion.nextFrame(() => moveIndicator(current?.tag ?? releases[0]?.tag, false));
|
||||
}
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
卡片
|
||||
══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
function buildFileRow(file, asset) {
|
||||
const link = element('a', 'dl-file');
|
||||
link.href = asset.url;
|
||||
link.dataset.press = '0.98';
|
||||
|
||||
const top = element('span', 'dl-file-top');
|
||||
top.append(element('span', 'dl-file-role', file.role));
|
||||
const size = formatSize(asset.size);
|
||||
if (size) top.append(element('span', 'dl-file-size', size));
|
||||
|
||||
link.append(top, element('span', 'dl-file-name', asset.name), element('span', 'dl-file-note', file.note));
|
||||
|
||||
const row = element('li');
|
||||
row.append(link);
|
||||
return row;
|
||||
}
|
||||
|
||||
function buildCard(card, release) {
|
||||
const node = element('div', 'dl-card');
|
||||
node.append(
|
||||
element('p', 'dl-card-who', card.who),
|
||||
element('p', 'dl-card-name', card.name),
|
||||
element('p', 'dl-card-desc', card.desc),
|
||||
);
|
||||
|
||||
const rows = card.files
|
||||
.map((file) => ({ file, asset: release.assets.find((item) => file.match.test(item.name)) }))
|
||||
.filter((row) => row.asset);
|
||||
|
||||
if (rows.length) {
|
||||
const list = element('ul', 'dl-files');
|
||||
for (const row of rows) list.append(buildFileRow(row.file, row.asset));
|
||||
node.append(list);
|
||||
} else {
|
||||
node.append(element('p', 'dl-missing', `这一版没有发布「${card.name}」。`));
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/** 卡片下方那段「这一版还有什么」,顺便说明清单是从哪儿来的 */
|
||||
function renderNote(release) {
|
||||
gridNote.textContent = '';
|
||||
const say = (text) => gridNote.append(document.createTextNode(text));
|
||||
const link = (text, href) => {
|
||||
const anchor = element('a', null, text);
|
||||
anchor.href = href;
|
||||
anchor.target = '_blank';
|
||||
anchor.rel = 'noreferrer';
|
||||
gridNote.append(anchor);
|
||||
};
|
||||
|
||||
if (source !== 'live') {
|
||||
say(
|
||||
'现在读不到版本列表 —— 多半是离线,或者 GitHub 那个匿名接口被限流了(每小时 60 次,按出口 IP 算)。',
|
||||
);
|
||||
say(`下面只列了当前版本 ${release.tag},文件名是按发布命名约定拼出来的直链;打不开就去 `);
|
||||
link('Releases 页', RELEASES_PAGE);
|
||||
say(' 自己挑一份。');
|
||||
return;
|
||||
}
|
||||
|
||||
const sums = release.assets.find((asset) => /^SHA256SUMS-.*\.txt$/i.test(asset.name));
|
||||
say(
|
||||
`这一版${release.publishedAt ? `(${release.publishedAt} 发布)` : ''}在发布页上有 ${release.assets.length} 个文件。`,
|
||||
);
|
||||
if (sums) {
|
||||
say('全部文件的哈希在 ');
|
||||
link(sums.name, sums.url);
|
||||
say(' 里,装之前可以先核对;');
|
||||
}
|
||||
say('发布说明与更早的版本见 ');
|
||||
link('Releases', release.notesUrl);
|
||||
say('。这份清单每打开一次就重新读一次,站点里不留副本。');
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
选中某一版
|
||||
══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/** 版本选择器下面那行状态 —— 它要说清"现在选中的是哪一版" */
|
||||
function renderStatus(release) {
|
||||
if (source !== 'live') {
|
||||
note.textContent =
|
||||
'版本列表暂时取不到(离线,或 GitHub 的匿名接口被限流),下面是按发布命名约定拼出来的直链。';
|
||||
return;
|
||||
}
|
||||
const marks = [];
|
||||
if (release.latest) marks.push('最新版');
|
||||
else if (release.prerelease) marks.push('预发布');
|
||||
if (release.publishedAt) marks.push(`${release.publishedAt} 发布`);
|
||||
marks.push(`${release.assets.length} 个文件`);
|
||||
note.textContent = `已选 ${release.tag}(${marks.join(' · ')})· 列表读自本仓库的 GitHub Releases`;
|
||||
}
|
||||
|
||||
function select(tag, animate = true) {
|
||||
const release = releases.find((item) => item.tag === tag);
|
||||
if (!release) return;
|
||||
current = release;
|
||||
|
||||
for (const button of $$('.dl-tab', tabs)) {
|
||||
button.setAttribute('aria-selected', String(button.dataset.tag === tag));
|
||||
}
|
||||
moveIndicator(tag, animate);
|
||||
|
||||
grid.textContent = '';
|
||||
for (const card of CARDS) grid.append(buildCard(card, release));
|
||||
renderStatus(release);
|
||||
renderNote(release);
|
||||
|
||||
// 首屏那颗按钮:直接下这一版的服务端(其余三个包在下面自己挑)
|
||||
const serverCard = CARDS.find((card) => card.key === 'server');
|
||||
const server = release.assets.find((asset) => serverCard.files[0].match.test(asset.name));
|
||||
if (server) {
|
||||
primary.href = server.url;
|
||||
primary.textContent = `下 ${release.tag} 服务端`;
|
||||
} else {
|
||||
primary.href = release.notesUrl;
|
||||
primary.textContent = '去 Releases 挑一份';
|
||||
}
|
||||
|
||||
// Linux 那一行:钉到当前选中的版本(老版本 tag 不是 x.y.z 形式时不带 --version)
|
||||
const pinned = /^\d+\.\d+\.\d+$/.test(release.version) ? ` --version ${release.version}` : '';
|
||||
command.textContent = `${command.dataset.base}${pinned}`;
|
||||
|
||||
// 卡片与文件行是刚建出来的,让 main.js 把按压反馈补挂上
|
||||
document.dispatchEvent(new CustomEvent('ch:content'));
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
启动
|
||||
══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
async function boot() {
|
||||
// 命令原文先记下来:后面每次都从它拼(版本选择会改写这段文本)
|
||||
command.dataset.base = command.textContent.trim();
|
||||
|
||||
try {
|
||||
releases = markLatest(await fetchReleases());
|
||||
if (!releases.length) throw new Error('发布列表是空的');
|
||||
} catch (error) {
|
||||
console.warn('[download] 取版本列表失败,改用按命名约定拼出来的直链:', error.message);
|
||||
source = 'fallback';
|
||||
releases = [fallbackRelease()];
|
||||
}
|
||||
|
||||
renderTabs();
|
||||
select(releases[0].tag, false);
|
||||
|
||||
window.addEventListener('resize', () => moveIndicator(current?.tag, false), { passive: true });
|
||||
}
|
||||
|
||||
boot();
|
||||
})();
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* 班级小助手 · 官网交互
|
||||
* ClassHelper · 官网交互
|
||||
*
|
||||
* 动效全部走 assets/motion.js 里那套 beUI token(曲线、弹簧、时长口径)。
|
||||
* 这里只做页面逻辑,逐块对着 beUI 的组件写:
|
||||
@@ -1353,4 +1353,9 @@
|
||||
setupNumberTickers();
|
||||
setupCopyButtons();
|
||||
setupShots();
|
||||
|
||||
// 下载页(download.html)的版本卡片是拉到 GitHub 清单之后才建出来的,建完会派发一次
|
||||
// `ch:content` —— 那时候启动已经跑过了,按压反馈得在这儿补挂一次
|
||||
// (bindPressable 对挂过的元素会自己跳过,重复派发不会重复绑定)。
|
||||
document.addEventListener('ch:content', setupPressable);
|
||||
})();
|
||||
+269
-1
@@ -1,5 +1,5 @@
|
||||
/* ============================================================================
|
||||
班级小助手 · 官网样式
|
||||
ClassHelper · 官网样式
|
||||
----------------------------------------------------------------------------
|
||||
两套表面,对应教室里的两样东西:
|
||||
|
||||
@@ -313,6 +313,22 @@ table {
|
||||
background: var(--board-raise);
|
||||
}
|
||||
|
||||
/* .btn-line 的颜色是给黑板调的(粉笔白),落在浅色纸面上会看不见 —— 纸面另给一套 */
|
||||
.band-paper .btn-line {
|
||||
border-color: var(--paper-rule);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.band-paper .btn-line:hover {
|
||||
border-color: var(--blue);
|
||||
background: var(--paper-raise);
|
||||
}
|
||||
|
||||
/* 纸面上的按钮行跟在正文后面,自己带一段上边距(黑板那边靠 .lead 的下边距撑开) */
|
||||
.band-paper .actions {
|
||||
margin-top: 26px;
|
||||
}
|
||||
|
||||
.btn-quiet {
|
||||
margin-left: auto;
|
||||
border-color: var(--board-line);
|
||||
@@ -2069,3 +2085,255 @@ html:not(.reveal-pending) .display .reveal-grad {
|
||||
transition-delay: 0s !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ════════════════════════════════ 下载页 ═══════════════════════════════
|
||||
download.html 专用。页面骨架与官网首页同一套(同样的色带、按钮、页脚),
|
||||
这里只补两样只有这一页才有的东西:版本分段控件、每个交付物的文件卡。
|
||||
|
||||
两条口径和首页一致:
|
||||
蓝(--blue-deep) 产品自己的强调色 —— 这里表示「这是一份真的能下的文件」
|
||||
卡片一律纸面(--paper-raise)——「谁装什么」是要照着一项项核对的,跟名单一样 */
|
||||
|
||||
/* ── 版本选择 ────────────────────────────────────────────────────────────
|
||||
与首页实机截图那组分段控件同一套做法(beUI Tabs:一个指示器在选项之间滑移),
|
||||
只是它管的是版本而不是图组,所以另起一套类名,不去动 .shots-*。 */
|
||||
|
||||
.dl-pick {
|
||||
margin: 34px 0 30px;
|
||||
}
|
||||
|
||||
.dl-pick-label {
|
||||
font-size: var(--fs-xs);
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--chalk-3);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.dl-tabs {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
padding: 4px;
|
||||
border: 1px solid var(--board-line);
|
||||
border-radius: 12px;
|
||||
background: var(--board-deep);
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.dl-tabs::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dl-tab {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
font: inherit;
|
||||
font-size: var(--fs-sm);
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--chalk-2);
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 9px;
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: color 0.18s var(--ease-out);
|
||||
}
|
||||
|
||||
.dl-tab:hover,
|
||||
.dl-tab[aria-selected='true'] {
|
||||
color: var(--chalk);
|
||||
}
|
||||
|
||||
/* 旗标:最新版 / 预发布。整排版本里只有它有,是这一行唯一的信息栅格 */
|
||||
.dl-tab-flag {
|
||||
font-size: 0.68rem;
|
||||
line-height: 1;
|
||||
padding: 3px 6px;
|
||||
border-radius: 999px;
|
||||
color: #cfe6fb;
|
||||
background: rgba(47, 143, 230, 0.18);
|
||||
border: 1px solid rgba(47, 143, 230, 0.4);
|
||||
}
|
||||
|
||||
.dl-tab-flag-quiet {
|
||||
color: var(--chalk-2);
|
||||
background: rgba(232, 238, 233, 0.07);
|
||||
border-color: var(--board-line);
|
||||
}
|
||||
|
||||
.dl-tabs-indicator {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 0;
|
||||
height: calc(100% - 8px);
|
||||
width: 0;
|
||||
border-radius: 9px;
|
||||
background: var(--board-raise);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 版本清单的来路 —— 这段话是这一页的"数据来源声明",取不到时要在这里说清楚 */
|
||||
.dl-pick-note {
|
||||
margin-top: 12px;
|
||||
font-size: var(--fs-xs);
|
||||
line-height: 1.85;
|
||||
color: var(--chalk-3);
|
||||
max-width: 48em;
|
||||
}
|
||||
|
||||
.dl-pick-note a {
|
||||
color: var(--chalk-2);
|
||||
}
|
||||
|
||||
.dl-pick-note code {
|
||||
color: var(--chalk-2);
|
||||
}
|
||||
|
||||
/* ── 文件卡(纸面) ───────────────────────────────────────────────────── */
|
||||
|
||||
/* 正好四张卡:宽度取 400px 起,桌面上一律两列(2×2 比 3+1 那种"落单"好读)。
|
||||
`min(400px, 100%)` 那层是必要的:只写 400px 的话,窄屏(比 400 还窄)上轨道会硬撑到 400,
|
||||
卡片就顶出视口右侧 —— 这是 auto-fit + minmax 的经典坑。 */
|
||||
.dl-grid {
|
||||
margin-top: 36px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(400px, 100%), 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.dl-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--paper-raise);
|
||||
border: 1px solid var(--paper-rule);
|
||||
border-radius: 6px;
|
||||
padding: 22px 22px 18px;
|
||||
}
|
||||
|
||||
/* 「谁在用」那行 —— 与首页特性块的 #标签同一个位置,用蓝色承担 */
|
||||
.dl-card-who {
|
||||
font-size: var(--fs-xs);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
color: var(--blue-deep);
|
||||
}
|
||||
|
||||
.dl-card-name {
|
||||
margin-top: 5px;
|
||||
font-size: 1.18rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.45;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.dl-card-desc {
|
||||
margin-top: 8px;
|
||||
font-size: var(--fs-sm);
|
||||
line-height: 1.85;
|
||||
color: var(--ink-2);
|
||||
}
|
||||
|
||||
.dl-files {
|
||||
margin-top: 16px;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* 一行 = 一个文件,整行可点(GitHub 的发布资源带 Content-Disposition,点了就下) */
|
||||
.dl-file {
|
||||
display: block;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--paper-rule);
|
||||
border-radius: var(--radius);
|
||||
background: var(--paper);
|
||||
text-decoration: none;
|
||||
transition:
|
||||
border-color 0.16s var(--ease-out),
|
||||
background-color 0.16s var(--ease-out);
|
||||
}
|
||||
|
||||
.dl-file:hover,
|
||||
.dl-file:focus-visible {
|
||||
border-color: var(--blue);
|
||||
background: #eaf3fd;
|
||||
}
|
||||
|
||||
.dl-file-top {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.dl-file-role {
|
||||
font-size: var(--fs-sm);
|
||||
font-weight: 600;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.dl-file-size {
|
||||
margin-left: auto;
|
||||
font-size: var(--fs-xs);
|
||||
color: var(--ink-3);
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dl-file-name {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
font-family: var(--mono);
|
||||
font-size: 0.72rem;
|
||||
line-height: 1.6;
|
||||
color: var(--ink-2);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.dl-file-note {
|
||||
display: block;
|
||||
margin-top: 3px;
|
||||
font-size: var(--fs-xs);
|
||||
color: var(--ink-3);
|
||||
}
|
||||
|
||||
/* 这一版没打这个包(老版本里真的会缺) */
|
||||
.dl-missing {
|
||||
margin-top: 16px;
|
||||
padding: 12px;
|
||||
border: 1px dashed var(--paper-rule);
|
||||
border-radius: var(--radius);
|
||||
font-size: var(--fs-xs);
|
||||
color: var(--ink-3);
|
||||
}
|
||||
|
||||
.dl-note {
|
||||
margin-top: 24px;
|
||||
font-size: var(--fs-xs);
|
||||
line-height: 1.9;
|
||||
color: var(--ink-3);
|
||||
max-width: 54em;
|
||||
}
|
||||
|
||||
.dl-note code {
|
||||
background: rgba(23, 35, 31, 0.06);
|
||||
padding: 1px 5px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.dl-card {
|
||||
padding: 18px 16px 14px;
|
||||
}
|
||||
|
||||
.dl-file-name {
|
||||
font-size: 0.68rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>下载 ClassHelper —— 挑版本、挑平台</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="从 GitHub Releases 直接下载 ClassHelper:教室机器要的班级端、服务器要的服务端(Windows 安装程序 / Linux 压缩包)、教室大屏要的 ClassIsland 联动插件。版本与文件清单取自本仓库的发布页,显示什么就能下什么。"
|
||||
/>
|
||||
<meta name="theme-color" content="#16231e" />
|
||||
<meta property="og:title" content="下载 ClassHelper" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="班级端、服务端(Windows / Linux)、ClassIsland 联动插件 —— 选好版本,下面显示什么就能下什么。"
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<!-- 页面上所有版本号与文件名都从这一行起步:脚本会去 GitHub Releases 取真实清单,
|
||||
取不到时(离线、被限流)就按这份版本号 + 发布命名约定拼直链。
|
||||
它和根 package.json 一起由 `pnpm version:bump` 维护 —— 别在这儿单独改。 -->
|
||||
<meta name="ch-version" content="1.1.2" />
|
||||
<link rel="icon" href="assets/icon.png" />
|
||||
<link rel="apple-touch-icon" href="assets/icon.png" />
|
||||
|
||||
<!-- 标题与引文用的楷体(霞鹜文楷 Screen)。加载失败时自动回落到系统的楷体 / 衬线 -->
|
||||
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/lxgw-wenkai-screen-webfont@1.7.0/style.css"
|
||||
media="print"
|
||||
onload="this.media = 'all'"
|
||||
/>
|
||||
<link rel="stylesheet" href="assets/styles.css" />
|
||||
<!-- 脚本在,先把标题藏起来准备逐字上浮;4 秒兜底,脚本坏在半路也不会把标题永久藏住 -->
|
||||
<script>
|
||||
document.documentElement.classList.add('reveal-pending');
|
||||
setTimeout(function () {
|
||||
document.documentElement.classList.remove('reveal-pending');
|
||||
}, 4000);
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<a class="skip" href="#main">跳到正文</a>
|
||||
|
||||
<header class="topbar">
|
||||
<div class="wrap topbar-inner">
|
||||
<a class="brand" href="index.html" data-press="0.96">
|
||||
<img class="brand-mark" src="assets/icon.png" alt="" width="32" height="32" />
|
||||
<span class="brand-name">ClassHelper</span>
|
||||
</a>
|
||||
<nav class="topnav" aria-label="站内导航">
|
||||
<span class="nav-pill" aria-hidden="true"></span>
|
||||
<a href="index.html">官网首页</a>
|
||||
<a href="index.html#parts">组成部分</a>
|
||||
<a href="index.html#shots">实机</a>
|
||||
<a href="docs/index.html">文档站</a>
|
||||
</nav>
|
||||
<a
|
||||
class="btn btn-quiet"
|
||||
href="https://github.com/LoveChengke/ClassHelper/releases"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
data-press="0.95"
|
||||
>
|
||||
全部版本
|
||||
</a>
|
||||
</div>
|
||||
<!-- 阅读进度:beUI ScrollProgressBar(2px,origin-left,弹簧缓跟) -->
|
||||
<div class="scroll-progress" aria-hidden="true"></div>
|
||||
</header>
|
||||
|
||||
<main id="main">
|
||||
<!-- ═══════════════════════ 首屏:挑版本、挑平台 ═══════════════════════ -->
|
||||
<section class="band band-board hero" id="top">
|
||||
<div class="wrap">
|
||||
<div class="hero-intro">
|
||||
<h1 class="display" data-reveal>
|
||||
<span class="reveal-line">先挑一个版本,</span>
|
||||
<span class="reveal-line reveal-grad">再挑要装的那一份</span>
|
||||
</h1>
|
||||
|
||||
<p class="hero-sub intro-seq" style="--intro-delay: 150ms">班级端 · 服务端 · ClassIsland 插件</p>
|
||||
|
||||
<p class="lead intro-seq" style="--intro-delay: 200ms">
|
||||
下面的版本与文件清单是当场从本仓库的 GitHub Releases 读出来的:文件名、体积都照抄发布页,
|
||||
页面显示什么,就真能下到什么。
|
||||
</p>
|
||||
|
||||
<!-- 版本选择:Tab 由 assets/download.js 按发布列表建出来 -->
|
||||
<div class="dl-pick intro-seq" style="--intro-delay: 250ms">
|
||||
<p class="dl-pick-label" id="ver-label">版本</p>
|
||||
<div class="dl-tabs" role="tablist" aria-labelledby="ver-label" id="ver-tabs">
|
||||
<span class="dl-tabs-indicator" id="ver-indicator" aria-hidden="true"></span>
|
||||
</div>
|
||||
<p class="dl-pick-note" id="ver-note" role="status">正在读取版本列表……</p>
|
||||
</div>
|
||||
|
||||
<noscript>
|
||||
<p class="dl-pick-note">
|
||||
这一页的版本清单要脚本才能读。直接去
|
||||
<a href="https://github.com/LoveChengke/ClassHelper/releases">GitHub Releases</a>
|
||||
挑一份 —— 文件名与体积那儿都有。
|
||||
</p>
|
||||
</noscript>
|
||||
|
||||
<div class="actions intro-seq" style="--intro-delay: 300ms">
|
||||
<a
|
||||
class="btn btn-solid"
|
||||
id="ver-primary"
|
||||
href="https://github.com/LoveChengke/ClassHelper/releases/latest"
|
||||
data-press="0.93"
|
||||
>
|
||||
下最新版服务端
|
||||
</a>
|
||||
<a
|
||||
class="btn btn-line"
|
||||
href="https://github.com/LoveChengke/ClassHelper/releases"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
data-press="0.93"
|
||||
>
|
||||
全部版本与更新日志
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<ul class="hero-badges intro-seq" style="--intro-delay: 350ms">
|
||||
<li>Windows 10/11 x64</li>
|
||||
<li>Linux x64 · Docker</li>
|
||||
<li>ClassIsland 2.x 插件</li>
|
||||
<li>包内自带 Node 运行时</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═══════════════════════ 这一版里有什么 ═══════════════════════ -->
|
||||
<section class="band band-paper" id="pick">
|
||||
<div class="wrap">
|
||||
<h2 class="h2">这一版里有这几个包</h2>
|
||||
<p class="lead narrow">
|
||||
按「装在哪台机器上」挑:教室机器装班级端,服务器装服务端,教室大屏再给 ClassIsland 加一个插件。
|
||||
四个包互不依赖顺序,但
|
||||
<strong>服务端要先有一台机器跑着</strong>
|
||||
,别的才连得上。
|
||||
</p>
|
||||
|
||||
<!-- 卡片由 assets/download.js 按当前选中的版本渲染 -->
|
||||
<div class="dl-grid" id="dl-grid" role="tabpanel" tabindex="0" aria-live="polite"></div>
|
||||
|
||||
<p class="dl-note" id="dl-note"></p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═══════════════════════ Linux 与 Docker ═══════════════════════ -->
|
||||
<section class="band band-board" id="server-linux">
|
||||
<div class="wrap">
|
||||
<h2 class="h2">Linux 服务器上,一行命令装完</h2>
|
||||
<p class="lead narrow">
|
||||
脚本自己去 Releases 取这一版的压缩包、核对旁边的
|
||||
<code>.sha256</code>
|
||||
,再装成 systemd 服务,顺手把
|
||||
<code>classhelper</code>
|
||||
运维命令放到
|
||||
<code>/usr/local/bin</code>
|
||||
里。
|
||||
</p>
|
||||
|
||||
<div class="snippet">
|
||||
<div class="snippet-head">
|
||||
<span class="snippet-label">在服务器上跑这一条</span>
|
||||
<button
|
||||
class="copy"
|
||||
type="button"
|
||||
data-press="0.97"
|
||||
data-copy="#cmd-linux"
|
||||
data-copy-done="已复制"
|
||||
>
|
||||
<span class="copy-label">复制</span>
|
||||
</button>
|
||||
</div>
|
||||
<pre
|
||||
id="cmd-linux"
|
||||
><code>curl -fsSL -o /tmp/classhelper-install.sh https://raw.githubusercontent.com/LoveChengke/ClassHelper/master/deploy/install.sh && sudo bash /tmp/classhelper-install.sh</code></pre>
|
||||
</div>
|
||||
|
||||
<p class="lead narrow">
|
||||
想先不改动系统,在末尾加
|
||||
<code>--check</code>
|
||||
就只体检。装完之后
|
||||
<code>classhelper upgrade</code>
|
||||
、
|
||||
<code>backup</code>
|
||||
、
|
||||
<code>doctor</code>
|
||||
、
|
||||
<code>password</code>
|
||||
这些命令都在等着,升级与回滚也走它们。
|
||||
</p>
|
||||
|
||||
<p class="lead narrow">
|
||||
云服务器、多终端共享的场合可以走 Docker Compose —— MySQL 与 SQLite 两种编排都在仓库的
|
||||
<code>deploy/</code>
|
||||
里,镜像 tag 与版本号对齐。完整步骤见
|
||||
<a href="docs/management/production.html">生产部署</a>
|
||||
与
|
||||
<a href="docs/management/linux-deploy.html">Linux 运维</a>
|
||||
。
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 收尾 CTA:装完去哪儿 -->
|
||||
<section class="band cta" aria-label="装完之后">
|
||||
<div class="wrap">
|
||||
<!-- prettier-ignore -->
|
||||
<h2 class="cta-title">装完,<span class="act-hl">从这三页开始</span></h2>
|
||||
<p class="cta-lead">
|
||||
一台机器装完就能跑起来,学生机和教室大屏接上同一个地址就行。第一次部署大约半小时,
|
||||
每一步都对着截图写好了。
|
||||
</p>
|
||||
<div class="cta-actions">
|
||||
<a class="btn btn-solid" href="docs/get-started/index.html" data-press="0.93">第一次装</a>
|
||||
<a class="btn btn-line" href="docs/management/production.html" data-press="0.93">
|
||||
部署形态怎么选
|
||||
</a>
|
||||
<a class="btn btn-line" href="docs/faq.html" data-press="0.93">装不上怎么办</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- ═══════════════════════ 页脚 ═══════════════════════ -->
|
||||
<footer class="foot">
|
||||
<div class="wrap foot-inner">
|
||||
<div class="foot-brand">
|
||||
<img src="assets/icon.png" alt="" width="36" height="36" />
|
||||
<div>
|
||||
<p class="foot-name">
|
||||
ClassHelper
|
||||
<span>Class Helper</span>
|
||||
</p>
|
||||
<p class="foot-ver">版本以 GitHub Releases 为准 · 源码公开 · 装在你自己的机器上</p>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="foot-nav" aria-label="相关链接">
|
||||
<a href="index.html">官网首页</a>
|
||||
<a href="docs/index.html">文档站</a>
|
||||
<a href="https://github.com/LoveChengke/ClassHelper/releases" target="_blank" rel="noreferrer">
|
||||
全部版本
|
||||
</a>
|
||||
<a href="https://github.com/LoveChengke/ClassHelper" target="_blank" rel="noreferrer">源码仓库</a>
|
||||
<a href="https://classisland.tech" target="_blank" rel="noreferrer">ClassIsland</a>
|
||||
</nav>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="assets/motion.js" defer></script>
|
||||
<script src="assets/main.js" defer></script>
|
||||
<script src="assets/download.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
+12
-8
@@ -3,13 +3,13 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>班级小助手 Class Helper — 知道现在是第几节课的班级信息系统</title>
|
||||
<title>ClassHelper Class Helper — 知道现在是第几节课的班级信息系统</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="班级小助手是一套自托管的班级信息管理系统:课表、作业、通知、成绩、叫人合在一个后端,教室大屏与每台学生机实时收到,上课时段自动不打扰课堂。支持 Windows 单机、Linux 服务器与 Docker 部署。"
|
||||
content="ClassHelper是一套自托管的班级信息管理系统:课表、作业、通知、成绩、叫人合在一个后端,教室大屏与每台学生机实时收到,上课时段自动不打扰课堂。支持 Windows 单机、Linux 服务器与 Docker 部署。"
|
||||
/>
|
||||
<meta name="theme-color" content="#16231e" />
|
||||
<meta property="og:title" content="班级小助手 Class Helper" />
|
||||
<meta property="og:title" content="ClassHelper Class Helper" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="课表、作业、通知、成绩、叫人合在一个后端,教室大屏与每台学生机实时收到,上课时段自动不打扰课堂。"
|
||||
@@ -43,7 +43,7 @@
|
||||
<div class="wrap topbar-inner">
|
||||
<a class="brand" href="#top" data-press="0.96">
|
||||
<img class="brand-mark" src="assets/icon.png" alt="" width="32" height="32" />
|
||||
<span class="brand-name">班级小助手</span>
|
||||
<span class="brand-name">ClassHelper</span>
|
||||
<span class="brand-ver">1.1.2</span>
|
||||
</a>
|
||||
<nav class="topnav" aria-label="页内导航">
|
||||
@@ -56,7 +56,7 @@
|
||||
<a href="#verify">验收</a>
|
||||
<a href="docs/index.html">文档站</a>
|
||||
</nav>
|
||||
<a class="btn btn-quiet" href="#deploy" data-press="0.95">取安装包</a>
|
||||
<a class="btn btn-quiet" href="download.html" data-press="0.95">下载</a>
|
||||
</div>
|
||||
<!-- 阅读进度:beUI ScrollProgressBar(2px,origin-left,弹簧缓跟) -->
|
||||
<div class="scroll-progress" aria-hidden="true"></div>
|
||||
@@ -80,7 +80,7 @@
|
||||
</p>
|
||||
|
||||
<div class="actions intro-seq" style="--intro-delay: 250ms">
|
||||
<a class="btn btn-solid" href="#deploy" data-press="0.93">立即下载</a>
|
||||
<a class="btn btn-solid" href="download.html" data-press="0.93">立即下载</a>
|
||||
<a
|
||||
class="btn btn-line"
|
||||
href="https://github.com/LoveChengke/ClassHelper"
|
||||
@@ -575,6 +575,10 @@
|
||||
学生机这边是安装包和单文件版两种:安装包走 NSIS,单文件版不用装、放 U 盘里就能分发。
|
||||
两个都不写死服务器地址 —— 第一次打开时填教室的地址和班级码就行。
|
||||
</p>
|
||||
|
||||
<div class="actions">
|
||||
<a class="btn btn-line" href="download.html" data-press="0.93">去下载页挑一份</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -635,7 +639,7 @@
|
||||
不用先读一遍文档。
|
||||
</p>
|
||||
<div class="cta-actions">
|
||||
<a class="btn btn-solid" href="#deploy" data-press="0.93">取安装包</a>
|
||||
<a class="btn btn-solid" href="download.html" data-press="0.93">取安装包</a>
|
||||
<a
|
||||
class="btn btn-line"
|
||||
href="https://github.com/LoveChengke/ClassHelper"
|
||||
@@ -657,7 +661,7 @@
|
||||
<img src="assets/icon.png" alt="" width="36" height="36" />
|
||||
<div>
|
||||
<p class="foot-name">
|
||||
班级小助手
|
||||
ClassHelper
|
||||
<span>Class Helper</span>
|
||||
</p>
|
||||
<p class="foot-ver">版本 1.1.2 · 源码公开 · 装在你自己的机器上</p>
|
||||
|
||||
Reference in new issue
Block a user