feat(client): 灵动岛个性化设置(需求 2)、形变抖动修复(需求 4)、关闭到系统托盘与退出清理(需求 5)

## 需求 2:设置页个性化(实时生效 + 持久化)

- 新增 `IslandAppearance`(shared 类型)+ `DEFAULT_ISLAND_APPEARANCE` / `ISLAND_APPEARANCE_RANGES` /
  `ISLAND_POSITIONS`:高度 36–72、宽度 220–420、圆角 8–32、不透明度 0.4–1、字号 11–18、
  动画速度 0.5–2、主题色、显示位置(顶部居中/左上/右上/底部居中)、动画开关、始终置顶。
- 客户端 `config.json` 增加 `island` 字段(`normalizeIslandAppearance` 夹紧越界值;
  旧配置无该字段时自动补默认值,向后兼容);`SettingsView` 新增「灵动岛 · 个性化」面板:
  滑块/取色器/开关/位置选择 + 「保存设置 / 预览效果 / 恢复默认」,拖动即实时预览。
- 主进程 `island.setAppearance()`:窗口尺寸按外观联动(展开卡按差值同步放大,内容用 flex + 溢出滚动承载)、
  透明度、置顶、动画开关与速度、停靠位置实时生效,并把外观通过 `island:appearance` 下发给灵动岛渲染进程;
  渲染进程用 CSS 变量(`--island-w/-h/-radius/-font/-accent`)驱动卡片尺寸、圆角、字号与强调色。

## 需求 4:形变抖动修复

- 窗口尺寸动画改为**单条帧循环**(优先 `requestAnimationFrame`,主进程无该全局时退化为 ~60fps 定时器);
  新动画取消上一条并作废令牌;结束对齐最终尺寸(消除 1px 累积误差);**展开动画结束后才取焦点**。
- 卡片保持固定尺寸 + 顶部居中锚定(此前已修),只让窗口露出/裁切,形变时内容不参与重排。
- 冒烟新增断言:收回过程窗口宽度**单调收缩**(实测 362→330→305→288→274→270→268…)、
  卡片上边缘波动 **0px**、卡片布局宽度只取 `396/260` 两个固定值;连续 5 次快速开合后状态与尺寸稳定。

## 需求 5:关闭到系统托盘 + 退出清理

- 新增 `main/tray.ts`:托盘图标(`public/tray.png`,`pnpm icons` 会自动同步)、右键菜单
  「显示主窗口 / 隐藏到托盘 / 退出班级小助手」、左键单击/双击恢复窗口。
- `main/index.ts`:`close` → `preventDefault + hide`(关闭窗口不退出,后台继续接收通知,日志留痕);
  `window-all-closed` 不再退出;`quitApp()` 与 `before-quit`/`will-quit` 统一走
  `shutdownResources()`:销毁托盘 → 销毁灵动岛(帧循环/定时器)→ 销毁全部窗口
  (渲染进程随之释放 Socket.IO 连接与 IndexedDB 句柄)→ `app.quit()` + `app.exit(0)` 兜底。
- 冒烟新增断言:关闭后 `visible=false / destroyed=false / 进程存活=true`;清理后 `托盘=false / 灵动岛=false`。

## 验证

- `pnpm verify:desktop` **40/40** ✅(新增 6 项:个性化外观生效、调整尺寸后内容不溢出、收回不抖动、
  快速开合稳定、关闭到托盘、退出前资源清理)
- `pnpm verify:e2e` **82/82** ✅、`pnpm verify:web` **17/17** ✅(回归无破坏)
- `pnpm lint` / `format:check` / `typecheck` 全绿
- 顺带修复:托盘/关闭处理器此前挂在冒烟验证之后,导致冒烟模式关闭窗口会真的销毁窗口
  (现已调整为在冒烟前挂载);`smoke.mjs` 配置目录按进程号隔离,避免上次异常退出的目录占用导致启动失败。
This commit is contained in:
LoveChengke committed 2026-09-12 15:19:13 +08:00
1 parent 30e4c36809
commit 0dcab6f369
15 files changed
+923 -56

No files matched your search

+37 -3
View File
@@ -20,7 +20,7 @@
| 1 | pnpm monorepo 脚手架、TypeScript / ESLint / Prettier / 环境变量 | ✅ 已完成 |
| 2 | 后端:Prisma schema + 迁移 + 种子数据 + JWT 认证 + RBAC + 模块化 REST API + Socket.IO | ✅ 已完成 |
| 3 | Web 管理端:登录、主布局、仪表盘、班级/学生/课表/作业/通知/成绩页面、Axios 封装、实时提示 | ✅ 已完成 |
| 4 | EXE 客户端:Electron 主进程/preload/渲染进程、登录、课表/作业/通知/成绩/设置、实时推送、IndexedDB 离线缓存 | ✅ 已完成(冒烟验证 34/34) |
| 4 | EXE 客户端:Electron 主进程/preload/渲染进程、登录、课表/作业/通知/成绩/设置、实时推送、IndexedDB 离线缓存 | ✅ 已完成(冒烟验证 40/40) |
| 5 | 三端联调脚本、打包命令、完整 README | ✅ 已完成(三套安装包 + 两套 UI 回归测试) |
| 6 | 测试账号与种子数据说明 | ✅ 已完成(见下文) |
| 7 | 生产化:服务端安装程序(内置 Node)、Web 端 PWA 可安装、Docker + Nginx 部署、生产加固与运维文档 | ✅ 已完成(见 [`docs/production.md`](docs/production.md)) |
@@ -173,7 +173,7 @@ pnpm verify:desktop
# 另开一个终端先启动后端:pnpm dev:server
pnpm verify:e2e # 后端 + REST + Socket.IO + RBAC + 上课时段拦截 + 叫人:82 项
pnpm verify:web # Web 管理端真实点击(含权限入口隐藏、手机适配、叫人入口、紧急通知):17 项
pnpm verify:desktop # Electron 客户端(含灵动岛动画/收回/已读/作业/叫人、真实链路、课表时间轴):34 项
pnpm verify:desktop # Electron 客户端(含灵动岛动画/收回/已读/作业/叫人、真实链路、课表时间轴、个性化与托盘):40 项
```
后端脚本验证**实时推送时延、作业完成、成绩下发、权限隔离、上课时段紧急通知拦截**等 62 项,
@@ -383,6 +383,40 @@ pnpm verify:desktop # Electron 客户端(含灵动岛动画/收回/已读
> 后续可继续深化:迷你悬浮课表窗口(ClassIsland 的桌面课表条)、上课/下课提醒动画与铃声。
### 个性化设置与系统托盘
**设置 → 灵动岛 · 个性化**(改动即实时生效,保存后写入本地配置,重启仍生效):
| 设置项 | 区间/取值 | 说明 |
| ---------- | --------------------------------- | ------------------------------------------------------------------------------ |
| 灵动岛高度 | 36–72px | 胶囊高度;展开卡按同差值联动(`+264/+292/+308px`),内容用 flex + 溢出滚动承载 |
| 灵动岛宽度 | 220–420px | 同上,展开卡按差值联动 |
| 圆角 | 8–32px | 卡片圆角 |
| 不透明度 | 40%–100% | 窗口整体不透明度 |
| 字号 | 11–18px | 卡片基础字号 |
| 动画速度 | 0.5×–2× | 形变/淡入时长倍率 |
| 主题色 | 取色器 | 用于高亮、按钮、进度条(CSS 变量下发到灵动岛渲染进程) |
| 显示位置 | 顶部居中 / 左上 / 右上 / 底部居中 | 按住 `workArea` 计算,多屏跟随主屏 |
| 动画开关 | 开/关 | 关闭后展开/收起瞬时生效(不再逐帧插值) |
| 始终置顶 | 开/关 | 对应 `setAlwaysOnTop` |
- 越界值在**主进程**统一夹紧(`normalizeIslandAppearance`),用户填坏配置也不会让布局错乱。
- 「预览效果」按钮会本地推一条通知,立刻看到当前尺寸/配色/动画。
- 落盘位置:客户端 `config.json` 的 `island` 字段(旧配置没有该字段时自动补默认值,向后兼容)。
**形变动画(需求 4 抖动修复)**:窗口尺寸改为**单条帧循环**驱动(优先 `requestAnimationFrame`,
主进程没有该全局时退化为 ~60fps 定时器),新动画会取消上一条并作废令牌,结束时对齐最终尺寸;
卡片始终是**固定尺寸 + 顶部居中锚定**,只让窗口露出/裁切,形变过程中卡片上边缘不动;
展开动画结束后才取焦点。冒烟实测:收回过程窗口宽度序列 `362→330→305→288→274→270→268…`
单调收缩、卡片上边缘波动 **0px**、卡片宽度只取 `396/260` 两个固定值,连续 5 次快速开合后状态与尺寸稳定。
**系统托盘(需求 5)**:关闭主窗口不退出程序,而是隐藏到托盘后台继续接收通知;
托盘图标(左键单击/双击)恢复主窗口,右键菜单提供「显示主窗口 / 隐藏到托盘 / 退出班级小助手」。
退出时统一清理:销毁托盘 → 销毁灵动岛(含帧循环与定时器)→ 销毁全部窗口
(渲染进程随之结束,其中的 Socket.IO 连接、IndexedDB 句柄一并释放),并用 `app.exit(0)` 兜底,
任务管理器中不留残留进程。冒烟实测:关闭后 `visible=false / destroyed=false / 进程存活=true`,
清理后 `托盘=false / 灵动岛=false`。
### 离线缓存与自动同步
```
@@ -680,7 +714,7 @@ pnpm db:migrate && pnpm db:seed
| --------------------------------------------------------------------------------- | ----------------------------------- |
| `pnpm verify:e2e`(开发环境与**安装后的生产实例**各跑一次) | **82/82** ✅ |
| `pnpm verify:web`(Web 管理端真实点击 + 权限入口隐藏 + 手机适配 + 叫人 + PWA) | **17/17** ✅ |
| `pnpm verify:desktop`(客户端冒烟 + 灵动岛动画/收回/已读/作业/叫人 + 课表时间轴) | **34/34** ✅ |
| `pnpm verify:desktop`(客户端冒烟 + 灵动岛动画/收回/已读/作业/叫人 + 课表时间轴) | **40/40** ✅ |
| `pnpm typecheck` / `pnpm lint` / `pnpm format:check` | 全部通过 ✅ |
| 安装程序完整生命周期(静默安装 → 启停脚本 → 卸载) | 通过 ✅ |
| Docker / Nginx 部署样例 | 文件已提供,本机无 Docker 未实测 ⚠️ |