Commit Graph
56 Commits
Author SHA1 Message Date
LoveChengke aad22df4fd docs: 重写 README —— 按 1.0.0 现状重排为「产品说明书 + 验收对照表」
结构重排:组成部分 → 交付产物与部署形态 → 快速开始 → 功能说明(Web 端 / 客户端 / 灵动岛 / 作业 /
叫人 / 上课时段 / 导入 / ClassIsland 联动 / 数据库管理)→ 技术参考(目录结构 / 技术栈 / REST API /
WebSocket / RBAC / 模块化 / 数据库与迁移)→ 验收 → 打包与交付 → 常见问题 → 后续增强。
1594 行 → 1236 行,去掉的是与 AGENTS.md 重复的踩坑叙述与「按阶段的交付清单」文件流水(后者改由目录结构
与 git 历史承载)。

修正过期内容:
- 迁移数 9 → 10(补回 add_schedule_source),迁移清单同步
- verify:e2e 173 项 → 186 项;verify:web 30/31、verify:desktop 99/102、插件静态契约 68/68 按最近一次实测重写
- 删掉两条与现状相反的验收行(「客户端设置页提供修改密码」),改为「客户端有意不提供改密入口」,
  与 AGENTS.md §5 第 31 条、冒烟断言「改密入口存在=false」一致
- 服务端安装包体积口径统一为 ~71MB(切库依赖内置后);插件版本 1.0.0.0;正文标注当前版本 1.0.0

新增与加强:
- 补 1.0.0 的内容:数据库管理(仅管理员)、新手引导(Web 端 el-tour + 客户端六步向导)、
  设置拆 6 个子路由、深色模式、客户端主窗口与 ClassIsland 对齐(1242x582)、
  课表 source 字段与「幽灵行清理」口径
- 6.3「已知的时序 / 环境相关用例」:写明 verify:desktop 那 3 项在「当前正在上课」时必然失败的原因,
  并给出各验证脚本覆盖凭据的方式(ELECTRON_SMOKE_* / UI_SMOKE_* 支持环境变量,verify:e2e 不支持)

验证(2026-10-02 本机):verify:e2e 186/186;verify:web 30/31(1 项为既有脆弱用例);
verify:desktop 99/102(3 项时序相关);verify:classisland-plugin 68/68;typecheck 与 lint 通过。
2026-10-02 11:24:31 +08:00
LoveChengke 572295c297 feat(server,web,desktop,shared,deploy,plugin): 数据库管理模块 + 客户端深色模式/设置拆子路由/首启引导,版本统一 1.0.0
- 服务端新增 database 模块(/api/database/*,仅 ADMIN):全库 JSON 快照(备份 / 恢复 / 导入导出 / 定时备份
  共用同一格式,14 张表按拓扑序导出、恢复时关外键检查)、连接测试与目标库校验、一键 SQLite ⇄ MySQL 切换
  (备份 → 导出 → 测试连接 → 校验空库 → 改 schema → 子进程 generate / tsc / db push → 写数据,全部成功才改写
  .env,中途失败回滚 schema);XML 快照与 SQLite 数据文件均可下载;打包形态随包带上 prisma CLI 与 typescript
  (prisma-client 生成物是 .ts,需再编进 dist/generated,安装包因之变大)
- Web 管理端新增「数据库管理」页(仅管理员),并加首次登录新手引导(el-tour,可跳过、可随时重看,版本号写
  localStorage,改版后老用户会再收到一次)
- 客户端设置改版:侧边栏改为分组折叠菜单,设置拆成 6 个子路由
  (/settings/{general,appearance,island,reminder,account,about}),新增深色模式(主题持久化到主进程配置,
  主窗口底色跟主题走,避免"先白后黑"闪一下),新增「关于」页(应用信息 / 诊断信息)
- 客户端新增首启六步引导(OnboardingWelcome,状态存主进程配置 onboardingDone)
- 客户端主窗口初始尺寸与 ClassIsland 对齐(1242x582 / minHeight 540 / 居中)
- 版本号统一提升到 1.0.0:5 个 package.json、部署配置与镜像标签(.env.example / docker-compose / Dockerfile)、
  服务端 /health 与首页、插件 manifest.yml 与 BridgeService.PluginVersion、文档里的安装包名
- 文档与验收同步:AGENTS.md 补第 37~51 条约定(深色主题实现边界、设置子路由、数据库管理三件事、侧栏与窗口
  尺寸、引导在自动化里的判据等),README 补「数据库管理 / 新手引导 / 设置改版」三节;verify:e2e 增补 13 条
  数据库管理用例,客户端冒烟与 Web UI 冒烟跟进子路由、深色主题、侧栏与引导

验证:pnpm lint 0 error;pnpm typecheck 全过;verify:e2e 186/186;verify:classisland-plugin 68/68;
build:desktop 通过(含产物凭据门禁);verify:desktop 99/102(3 项为上课时段下的"真实通知链路"用例,
客户端按产品规则暂存普通通知所致,见下);插件 dotnet 编译 0 错误。

注:verify:desktop 的「真实通知链路 / 标记已读」三项要求当前不在上课时段——本次运行时种子课表恰有
「周五 10:55-11:40 生物」正在进行,客户端正确地暂存了普通通知(AGENTS.md 第 18 条),属时间相关用例,
与本次改动无关。
2026-10-02 11:18:47 +08:00
LoveChengke 391e2c1591 docs(desktop,island): 修正被写成既成事实的"收回握手"机制描述
AGENTS.md 第 34 条把一个从未落地的设计当成了已实现的规则,还配上了"踩过的坑"与
"两条配套铁律",读起来像历史教训。实际核对:`island:closing`、`island:closing-done`、
`sendClosingDone`、`noteClosingDone`、`closingTimer`、`finishClosing` 六个符号在全部
源码中零命中,在 git 全部历史里也从未出现过。

真实实现是主进程单向的:handleAction() → dismissActive() / clearPendingForClose()
→ hide() → fadeOut(),状态同一帧写成 hidden,不等渲染进程任何回执。渲染侧只有本地
弹簧形变(spring.ts),是纯视觉动画,不参与淡出时机。

另,早先那层自研的"卡片缩回胶囊再淡出"快照动画(closing / closingSnapshot)确实存在过,
因在触摸屏上会露出旧帧/重影,已按用户要求整体删除 —— IslandApp.vue 顶部注释有记载。

改动:重写 AGENTS.md 第 34 条为真实机制,并注明该自研动画已移除、别再照着不存在的
握手机制改代码;README 三处同样表述一并修正;smoke.ts 里那句"渲染进程收敛上报"的
过时注释改为描述真实的淡出轮询。
2026-10-02 00:32:48 +08:00
LoveChengke 20fcca0388 feat(server,web,shared,desktop): 上传超限正确报错 + 两个登录入口都限流 + 作业按天深链,文档与验证脚本同步
服务端加固:
- express.json 上限 2mb → 12mb:表格导入把文件以 base64 放进 JSON(约 4/3 倍),
  8MB 的声明上限原先到 1.5MB 就被拦掉;超限现在由 error 中间件映射成
  413 IMPORT_TOO_LARGE(中文提示"请拆分后分批导入"),不再落进 500 兜底分支抛英文原文;
- loginRateLimiter 同时挂 /auth/login 与 /auth/class-login(班级密码默认 123456,
  班级登录曾是唯一没有暴力破解防护的入口),两个路径共享同一计数器;
- CORS_ORIGIN=* 时启动告警(不拦截启动,避免挡住教室机 file:// 的合法部署);
- 删除已无用的 DEFAULT_STUDENT_PASSWORD;班级 termWeeks 上限 40 → 30,
  与 MAX_TERM_WEEK / weekNumberSchema 对齐(原先对外承诺了系统兑现不了的学期长度)。

Web 端:
- 作业页 ?date= 深链在同路由再推 query 时也生效(watch route.query.date);
- 表格导入对话框适配 8MB 上限与 413 提示。

冒烟与文档:
- 新增 scripts/lib/smoke-class.mjs:验证用临时班级,避免探针通知弹到用户自己的客户端与灵动岛;
- README 与 AGENTS.md 同步本轮全部行为变更(含 schedule source、学生账号清理、凭据门禁)。
2026-10-02 00:25:21 +08:00
LoveChengke 3880eee08e feat(desktop,island,shared): 灵动岛支持多条通知竖排列表 + 触摸模式 + 收回动画,产物加凭据门禁
灵动岛列表:多条通知时展开为竖排列表,默认显示 3 条,点「展开更多」铺到屏幕容纳
上限,放不下的收进「更多请前往应用内操作」。卡片高度由主进程与渲染进程各算一次
同一个数 —— 度量表 ISLAND_LIST_METRICS 与排序权重 PRIORITY_RANK 放在 shared,
窗口包围盒随之长高(不够高会把底部按钮裁掉)。

触摸模式:触摸屏上手指既不产生 mousemove、也不移动系统光标,原先两条命中链路都会
让窗口永远停在穿透态、手指点不开岛。现按 navigator.maxTouchPoints 上报后,主进程把
窗口贴合岛体并始终接收输入;非触摸屏机器完全不走这条路。

收回动画:main 先发 island:closing,渲染进程形变收敛后回 closing-done 才淡出,
不再靠猜时长(rAF 被节流时会把卡片缩到一半窗口就没了)。

安全门禁:新增 scripts/check-bundle-secrets.mjs 并接进 build:desktop ——
dist 产物会被打进 app.asar 发到每台学生机、且可解包,冒烟凭据一律改由环境变量注入,
产物里出现种子口令即构建失败。
2026-10-02 00:25:10 +08:00
LoveChengke d27f9a4b9e feat(students,teachers,classes,auth,web): 学生个人账号彻底清理,学生只是名单记录
学生不再有登录能力:User.passwordHash 以空串占位,role=STUDENT 在密码校验之前
一律 403 并提示改用班级登录(否则「密码错 401 / 密码对但已停用 403」的差异会把
学生账号密码变成可离线验证的预言机)。成绩、未交名单、叫人、已读全部按名单记。

配套改动:
- 删除 POST /students/:id/reset-password(调用返回 404,e2e 有负断言);
- 新建学生不设密码,StudentsView 移除密码输入与「重置密码」入口,
  列表标识仍自动生成为学号式(student01…);
- class-account.ts / auth.service.ts 收紧班级会话语义;
- 经管「修改密码」入口收敛到 Web 端:教师改本人密码走顶栏下拉,
  班级密码由管理员在班级管理里维护,教师可用 POST /teachers/:id/reset-password 设初始密码。
2026-10-02 00:25:04 +08:00
LoveChengke 7ea0e0f7c9 feat(schedules,classisland,server,plugin,shared): 课表行加来源字段,插件上报可真正同步删除
课表行新增 Schedule.source(manual / classisland):插件每次全量上报时,服务端
只清理「source=classisland 且本次未再上报」的行。原先 merge 只做 upsert,老师在
ClassIsland 里删掉或挪走一节,服务端会永远留着那行幽灵课;现在能真正同步下来,
而老师在小助手这边手排的课(manual)一律不删。

配套改动:
- 插件上报时带回 machineKey,服务端回填到设备记录(避开 [classId, deviceKey] 唯一约束);
- 新增迁移 20260930120000_add_schedule_source,存量数据保守地按 manual 处理;
- verify:classisland 补「幽灵行清理」「手排课不被误删」用例。

另:.gitignore 补上 .zcode/ 与 .zcodeignore(AI 会话产物,不属于产品代码)。
2026-10-02 00:24:58 +08:00
LoveChengke 9c678b3730 fix(deploy,docs): 修好 Docker 构建(缺 prisma generate)+ 新增 SQLite 单容器 compose
- Dockerfile:构建阶段补 `pnpm --filter @classhelper/server run db:generate`。生成产物 packages/server/src/generated/prisma 是 gitignored 的,而 pnpm install 用了 --ignore-scripts 跳过 postinstall,少了这一步 tsc 会报 "Cannot find module ../generated/prisma/client.js"(从 git clone 构建必踩)
- 新增 deploy/docker-compose.sqlite.yml + deploy/.env.sqlite.example:只跑服务端一个容器(DATABASE_URL=file:/app/data/classhelper.db + 数据卷),首次启动靠 AUTO_MIGRATE=true 自动建表与建管理员。适合内存小的机器或与别的服务共用一台机器(MySQL 变体会多一个数据库进程)
- docs/production.md:补 2.1 单容器变体说明(用法、适用场景、端口/生产自检注意事项、备份命令)
2026-09-27 18:34:19 +08:00
LoveChengke 5c27586af9 fix(island,classisland,server,desktop,plugin): 灵动岛去掉整窗背景材质(修卡片外圈白底面板)+ 左右边距只在左/右停靠生效 + 镜像课表补课间
- 灵动岛:移除 win.setBackgroundMaterial() 与 applyBackgroundMaterial/getBackgroundMaterial。它把系统材质刷在整个窗口矩形上(岛是"固定大包围盒窗口",卡片外还有留白),且明暗跟随应用的浅色主题 → 卡片周围出现一圈浅色面板;更糟的是调用过一次(含 none)后窗口就回不到逐像素透明,从 glass 切到 black/tinted 会永久留一圈纯白。glass 因此改为纯 CSS 半透明深色卡(WinIsland 拿不到 host backdrop 时的降级色 rgba(32,32,36,.804))
- 回归用例:新增「个性设置:卡片外圈透出桌面(三种风格都没有白底面板)」——整屏截图取样,并在岛底下垫一块已知颜色底板,让"环 = 底板色"可被精确断言(webContents.capturePage() 拍不到窗口之后的材质,是当初漏掉这个 bug 的原因)
- 边距:左右边距只在"左/右停靠"时生效(默认顶部居中时水平锚点就是屏幕中线),设置页把该滑块置灰并说明,避免被当成"调了不生效"
- 课间:镜像回 ClassIsland 的课表改为每两节之间补一段课间、最后一节之后不补(放学);服务端 synthesizeTimeLayout 同口径,不再依赖导入的时间表里有没有课间点
- 文档:README / AGENTS.md / docs/winisland-design-tokens.md / 插件 README 同步

门禁:pnpm lint 0 error、pnpm typecheck 四包通过。未跑:verify:desktop(需先 build:desktop)、verify:e2e / verify:classisland(需后端启动)。
2026-09-27 18:08:51 +08:00
LoveChengke 7536abfc95 feat(homeworks,island,classisland,desktop,web,server): 作业按天查看与教室机器录入 + 提醒上课时段暂存规则 + 灵动岛边距/跟随鼠标屏幕
- 作业:新增 Homework.assignDate(本地日期 YYYY-MM-DD;UTC 切片在晚上会把今天的作业算成明天),列表按天过滤与排序,新增 GET /homeworks?date= 与 GET /homeworks/days(日期高亮)供客户端与 Web 共用;客户端作业页改为"看作业 + 录作业"(不再显示完成状态),支持快捷短语(存本机配置,「设置 → 作业录入」可增删/恢复默认)
- 教室机器录入:POST /homeworks 去掉 requireRole,权限下沉到 homeworks.service(staff 或本班班级账号);班级账号的 sub 是班级 id,写 createdBy 前换成该班班主任,避免撞 User 外键
- 提醒类型:ClassIslandPush.kind(notification / call)。通知类上课时段暂存、下课补弹,叫人/紧急立即弹;客户端灵动岛与 ClassIsland 插件共用同一套规则
- 灵动岛:新增 marginX / marginY(默认仍 8px,不动老外观)与「跟随鼠标屏幕」;文案统一成"灵动岛 / ClassIsland"
- 设置页:新增「ClassIsland 联动」(联动状态 + 通知模式)与「作业录入」分区
- 校验:verify:e2e 增补 assignDate 默认当天 / ?date= 过滤 / days / 非法日期 422 / 班级账号录入 201 与跨班 403;verify:classisland 与插件静态契约增补上课时段暂存断言(57/57)
- 文档:README 与 AGENTS.md 同步(作业按天、上课时段策略、两个方向的开关表、新增踩坑)

门禁:pnpm lint 0 error、pnpm typecheck 四包通过、verify:classisland-plugin 57/57 通过。
未跑:verify:e2e / verify:classisland(需后端启动)、verify:web / verify:desktop(需先构建产物)。
2026-09-27 16:34:01 +08:00
LoveChengke d0830f6830 feat(integrations,classisland,server,web,desktop): ClassIsland 联动插件——上报课表与上课状态、下发提醒并回执、教室端推送通道
教室机器上的 ClassIsland 现在可以直接和班级小助手互通:插件走 /api/integrations/classisland/*,用设备令牌(X-ClassIsland-Token,库里只存 sha256)独立鉴权,不占用 JWT 通道。

- 服务端:新增 integrations 模块(上报 / 待弹出 / 回执 / 课表镜像 / 回写),两个迁移(设备令牌表、Class.notificationChannel);通知、叫人、联动页下发统一复用 pushToClassIsland,避免"某个页面发的通知不联动"
- 插件:packages/classisland-plugin(.NET 8,独立于 pnpm workspace)——BridgeService 上报与提醒轮询、ScheduleMapper/ClassPlanWriter 课表镜像、Avalonia 设置页、ClassHelperNotificationProvider 在 ClassIsland 上弹出提醒;本地/内网地址强制直连,躲开系统代理返回的假 404
- 客户端:设置页可选提醒通道(both / client / classisland),并订阅相应实时事件
- Web 端:新增「ClassIsland 联动」页(设备管理与提醒下发)
- 校验与打包:verify:classisland(需后端)、verify:classisland-plugin(静态契约 54 项)、build/dist:classisland-plugin 产出 .cipx
- 文档:README 与 AGENTS.md 同步新增联动说明,docs/screenshots/classisland 留三张真机截图

门禁:pnpm lint 0 error、pnpm typecheck 四包通过、verify:classisland-plugin 54/54 通过。运行期仍需在装了 ClassIsland 的机器上实测(步骤见 AGENTS.md §8)。
2026-09-27 14:59:12 +08:00
LoveChengke fe41ebb7d6 docs: 新增 AGENTS.md(面向 AI 编码代理的仓库说明与踩坑约定) 2026-09-27 10:44:00 +08:00
LoveChengke 14ac16ff29 fix(desktop,island): 设置页灵动岛外观改动即生效——IPC 只接受纯数据 + 真实 UI 冒烟回归
用户反馈"灵动岛设置无法生效并且无法实时预览"。根因是设置页把 ref 的响应式 Proxy 直接交给 contextBridge,结构化克隆在进入 preload 函数体之前就抛 `An object could not be cloned.`,界面数字照常变化却半静默失败。

- SettingsView:应用/保存外观时展开成纯对象({ ...island.value })
- desktop.d.ts / preload:显式写明"必须传纯数据",记录该失败模式
- smoke.ts:新增「拖拽设置页真实滑块」用例,同时断言页面数值与主进程外观都变化
2026-09-27 10:43:57 +08:00
LoveChengke d900280760 fix(island): 胶囊"点不动"的根因——窗口必须始终可激活(否则 Windows 丢掉 mousedown)
真机复现(打包版 + 真实 OS 点击 + 渲染进程鼠标事件计数):
- 胶囊态:渲染进程只收到 mouseup,**收不到 mousedown,因此永远不产生 click** → 点了没反应;
- 展开态:完整 mousedown + mouseup + click → 正常。
根因:窗口创建时 `focusable: false`(WS_EX_NOACTIVATE),Windows 在窗口非活动时会丢掉鼠标
**按下**事件。结果是"岛只有在已经展开时才能被点开",即胶囊态/重启后/切到别的窗口后点不动
(用户反馈的"点不动 / 要点两下")。这也解释了为什么此前冒烟全绿 —— 冒烟用 DOM `.click()`
和直接调动作,绕过了 OS 输入层。

修复:
- 窗口改为**始终可激活**(`focusable: true`);"不抢焦点"由 `showInactive()`(显示时不激活)保证;
- `applyFocusable`:只有展开态才 `focus()`,收起/隐藏时 `blur()`——不占用户键盘焦点,
  但保持可激活,点击才能送达;
- `hideImmediately` 不再自己 `setFocusable(false)`,可激活性统一由 `applyFocusable` 维护。

自建穷举真机审计(`.cache/island-probe/audit.mjs`,真实 OS 点击,非合成事件):23 个场景 ——
普通/紧急通知、作业、普通/紧急叫人 × 上课/非上课、6 个停靠位置、展开→收起→再点、
课中收起后再点、下课补发、联合通知、已读、知道了、胶囊超时、关闭动画、大尺寸/半透明/
毛玻璃/主题色、细缝态、预览态、运行中改位置尺寸、真实"点别处"失焦后再点、双广播、20s 回缩。
结果:dev 构建连续 3 轮 23/23,打包版 23/23。

回归:pnpm verify:desktop 82/82(新增回归用例「胶囊态窗口仍可激活(否则 Windows 丢掉
mousedown,胶囊"点不动")」,并把「点击屏幕任意位置回缩为胶囊」的断言改为"窗口始终可激活");
打包版 verify:packaged 82/82、packaged=true、二次启动 74/74,全绿。
2026-09-26 22:56:45 +08:00
LoveChengke 337d6eae53 fix(island): 幽灵胶囊自愈 + 叫人双广播不降级 + 淡出/帧限流兜底(继续修"点不动")
用户反馈"叫人点不动"复盘:状态机层面没问题(真机点击日志能看到"收到用户动作 expand"),
问题在窗口/渲染层面。本轮修掉三个真实缺陷:

1) 幽灵胶囊:岛渲染进程一旦崩溃/卡死,Windows 会把最后一帧留在屏幕上 —— 岛还挂在最上方、
   窗口也还在,但点击全部丢失,而客户端没有任何自愈机制(实测:崩掉渲染进程后点击不再产生
   任何动作)。现在渲染进程每 5s 上报心跳,主进程 20s 收不到、或收到 render-process-gone /
   unresponsive 时,**先隐藏死窗口**再重建并重新下发状态与外观;10 分钟内最多重建 3 次,
   超出则隐藏并告警,避免持续崩坏时无限重建。
2) 叫人双广播降级:服务端 createCall 同时发 notification:new(班级房间)与 call:new(user 房间),
   两条 DTO 同 id,后到的那条会把"叫人"覆盖成普通通知 —— 胶囊显示成
   "新消息 / 系统管理员 · 点击查看"(用户截图正是如此),紧急叫人还会被降级成"课中只排队、
   不立即展开"。现在按 id 合并:类型 call 优先、优先级取更高(URGENT > HIGH > NORMAL)。
3) 淡出竞态:岛重新变为可见时没有取消进行中的淡出,那次淡出会继续把透明度降到 0 并 hide()
   窗口,出现"状态是胶囊/展开、窗口却不可见"的错位(冒烟"岛外鼠标穿透"用例就是被它打红的)。
   现在可见路径总是 cancelFade + 恢复透明度。

另加两条兜底:
- 命中矩形兜底:渲染进程还没上报岛体矩形时,主进程按当前形态/停靠位置自己算一个,
  杜绝"岛可见却一直不接收鼠标"。
- 形变兜底:实测打包版某些时刻(细缝态、刚启动)rAF 会被 Chromium 压到约 1 帧/秒,
  此时"点击后形变"看起来就是没反应;现在形变开始 1.2s 后仍未收敛就直接 snap 到目标几何。

验证:pnpm verify:desktop 81/81(新增 3 条回归:双广播不降级 / 紧急叫人双广播 /
渲染进程崩溃后自愈;离线二次启动 73/73);打包版 verify:packaged 81/81、packaged=true、
二次启动 73/73,全绿。
顺带:细缝用例改为轮询等几何收敛(帧被限流时固定 sleep 会误判为"细缝尺寸不对"),
课表时间轴失败时打印各卡片状态(便于定位),并清理了库里 3 条遗留测试探针课表
(它们会污染客户端"今天"的课表)。
2026-09-26 20:47:01 +08:00
LoveChengke a7b0e9c2b3 fix(island): 修好"收起后再次点击灵动岛没反应"(假失焦缩回 + 命中状态互相覆盖)
根因一(假失焦):展开时 setFocusable(true)+focus() 会引发一次与用户无关的失焦
(实测 70~520ms 后到达)。原来的失焦处理器在 500ms 宽限期之后就把任何 blur 当成
"用户点了别处",于是刚展开的岛立刻缩回胶囊 —— 看起来就是"点了没反应"。
现在只在**指针确实离开岛体**时才认为用户点了别处(新增 isCursorOnIsland)。

根因二(命中抖动):渲染进程与主进程各自下发 setInteractive(true/false),而两边几何
逐帧不一致(弹簧形变 + 岛体矩形上报限流),窗口就在"接收鼠标 / 穿透"之间几毫秒翻转一次,
点击撞上穿透态即"点不动"(日志实测 true/false 成对翻转)。现在命中判定单一权威:
渲染进程只负责"立刻打开命中",关闭一律由主进程按真实光标轮询(120ms→60ms)决定;
同时去掉"窗口一可见就整块 476×282 包围盒接收鼠标"(会在屏幕上方留一块看不见却点不动的
区域)、淡出期间不接收鼠标、岛体矩形上报限流 100→50ms。

验证(真机真实点击,非合成事件):SetCursorPos + mouse_event 连续 4 轮
"点开 → 点卡片收起 → 再点胶囊"全部正常,展开后保持展开不再被假失焦缩回
(修复前 0.5~2s 内必缩回);pnpm verify:desktop 78/78(新增回归用例
「假失焦(指针仍在岛上)不回缩:展开后必须保持展开」)。

顺带修掉既有测试缺陷:通知菜单项带未读徽标时整项 textContent 变成 "2通知",
用 startsWith 匹配会误报"菜单项缺失",改为按文案层 span 精确匹配。
2026-09-26 19:32:47 +08:00
LoveChengke 70250151d0 feat(deploy,docs): Linux 原生部署(systemd 一键脚本 + unit 模板)+ 生产文档扩写
- deploy/install-linux.sh:Linux 一键部署(Node 版本检查 → 铺 /opt/classhelper → 生成 .env 与随机
  JWT_SECRET → 安装 systemd 服务 → 就绪探针;支持 --check/--port/--dir/--no-service/--uninstall)
- deploy/classhelper.service:systemd 单元模板(__NODE__/__DIR__/__USER__ 由脚本替换后安装)
- deploy/{.env.example,docker-compose.yml}:补充 DEFAULT_STUDENT/CLASS/TEACHER_PASSWORD 默认密码环境变量
- docs/production.md:新增形态 C(Linux systemd + SQLite)、Nginx + HTTPS、升级/备份/排障、探针与安全清单
- README.md:生产部署四形态对照表、目录树与验证表更新
2026-09-26 19:32:37 +08:00
LoveChengke 845fbdd863 feat(teachers,classes,schedules,web): 教师录入(仅管理员)+ 班主任可设置/更改 + 授课科目全校统一 + 登录页去示例
① 支持录入教师(与学生录入对齐),权限只有管理员
- 新增 Web「教师管理」页(`Views/TeachersView.vue`):单个录入(姓名/用户名/初始密码/角色)、
  导入名单(xlsx/csv:用户名、姓名、初始密码、角色)、编辑(改名/改角色)、重置密码、删除
- 服务端 `teachers` 模块补齐:GET / POST / PATCH /:id / POST /:id/reset-password / DELETE /:id,
  全部 `requireRole('ADMIN')`;表格导入新增 kind=teachers(`IMPORT_FIELDS.teachers`、`commitTeachers`、
  教师名单模板),教师调用预览/提交一律 403
- 删除护栏:还是班主任(`Class.teacherId`)或持有课程/班级分配的账号拒绝删除(409,提示先更换班主任)——
  教师是数据创建者,级联删除会连带删掉班级数据
- 前端路由加 `meta.roles`:`/classes`、`/students`、`/teachers` 仅 ADMIN,教师直接输网址会被挡回仪表盘
  (菜单隐藏只是"看不见",这次补上真正的守卫);新增环境变量 `DEFAULT_TEACHER_PASSWORD`

② 创建班级仅管理员 + 设置 / 更改班主任
- 新建班级时可直接指定班主任(不填则创建者本人),班级详情顶部「更换班主任」下拉随时改:
  新增 `PATCH /api/classes/:id/head-teacher`(仅管理员,校验目标账号是教师/管理员,广播 class:updated)
- 原班主任保留在协作教师名单里,避免换人后丢掉历史协作关系

③ 授课科目全校统一(用户澄清:"统一"指的是授课科目固定,不是各班课表相同)
- `SUBJECT_CATALOG` 改为用户给定的 18 科:语文/数学/英语/政治/历史/物理/化学/地理/生物/信息技术/
  体育/美术/音乐/班会/早读/晚读/通用技术/听力
- 课表「新增课表」与成绩「单条/批量录入」的科目下拉直接列出统一目录(该班还没有的进
  「统一科目(选中后自动建课)」分组),选中后自动为该班建同名课程再落库 —— 不必按班级一个个录科目
- 各班课表仍然各自独立(各班的课表本来就不一样)

④ 登录页去掉所有示例内容
- Web 登录页删除「演示账号(点击填充)」标签列表与"种子数据见 README"提示;客户端班级码占位符
  由「例如 G101(由老师在班级管理中提供)」改为「请输入班级码」
- `verify:web` 与 `verify:desktop` 各自断言登录页正文/占位符不含 `演示/示例/admin123/teacher123/G101`

⑤ 验证脚本不再依赖演示数据(并修掉自身残留)
- 清空服务端作业/通知后,联网自检改为**自建**一条作业/通知并在收尾删除(`seedSmokeContent`);
  权限用例需要"其它教师的班级/科任班级"时现建临时班级并在收尾删除
- e2e 新增清理:实时推送用例造的通知/作业、上课时段用例造的两条通知、教师名单/临时班级
  (此前这几条会留在库里);班级详情相关断言不再依赖种子班级布局
- ui-smoke 新增 7 项:登录页无示例、教师端访问 /teachers 被挡回、统一科目下拉 + 自动建课 + 收尾删除
  课表与课程、管理员真人录入教师 → 列表出现 → 删除
- 冒烟里"岛就绪"与"截图留档"改为轮询等待(偶发丢一张留档图会误判"形态尺寸递增")

实测:verify:e2e 163/163(安装实例)、verify:web 27/27、verify:desktop 77/77(离线复跑 69/69)、
verify:packaged 对 release/win-unpacked 与 D:\class\@classhelperdesktop-client 均 77/77 且 packaged=true;
typecheck / lint / format:check 全绿。服务端安装实例与客户端安装目录已就地更新。
2026-09-13 10:34:46 +08:00
LoveChengke 0ff2dc228b fix(homework,island,verify): 全屏作业看板自适应 + 已安装副本覆盖升级 + 新增打包版冒烟
① 全屏作业看板自适应(用户反馈:全屏模式没有自适应,卡片挤在左上角)
- 全屏看板按「可用宽 × 可用高」枚举 1..N 列,取**单卡面积最大且宽高比最接近 1.15** 的方案,
  注入 `--board-cols`(不再靠 CSS auto-fit 猜);列数变了再量一次缩放系数,
  `transform-origin: top center` 保证整体居中、不溢出屏幕
- 两个全屏弹窗各自持有 ref(原来共用同名 ref,后挂载的会覆盖前一个,量到的是另一个弹窗);
  列表型「单个科目全屏」补上 `transform: scale()`(该规则原先只作用于 `.board`,列表全屏从未缩放)
- 弹窗 `@opened` 后轮询等待布局就绪(最多 20×60ms)再计算;窗口 resize 时自动重算;
  另加一条全局 `.board-fullscreen .el-dialog__body` 兜底,保证内容区铺满整屏
- 实测:6 个科目 → 3 列、排版宽 1155px(视口 1187px)、可视底 678px(视口高 765px)

② 「灵动岛设置没办法生效」的真正原因:用户机器上的副本一直没被更新
- 学生端实际运行的是安装目录 `D:\class\@classhelperdesktop-client` 里的 `resources/app.asar`,
  而 `pnpm dist:win` 只更新 `release/`;只发新版 release 不动安装目录,用户看到的还是旧界面
- 已就地覆盖升级该目录,并用同一套冒烟实测**那个副本**:`packaged: true`、76/76 全绿
- README 补充「把修复交付到已安装的客户端」步骤(robocopy 覆盖 + verify:packaged 实测)

③ 新增/加固自动化验证
- `verify:desktop` 新增「作业看板全屏自适应(铺满屏幕、内容不溢出)」:进作业页 → 切看板 →
  打开全屏 → 断言排版宽铺满视口、可视底不超出屏幕;再把卡片补到 6 张触发 resize,
  要求列数自动变多(1 → 3)且仍不溢出
- 点击/命中这类异步链路由"固定 sleep 后读一次"改为**轮询等待**(曾偶发误判为"点不开"),
  失败时把渲染进程当前类名、窗口可见性、岛体矩形写进结论,便于区分真 bug 与竞态
- 新增 `pnpm verify:packaged`:对**打包后/已安装**的 EXE 跑同一套冒烟(含二次启动校验文件锁),
  避免"开发产物全绿但交付给学生的 EXE 是旧的"这类问题
- 实测:`verify:desktop` 76/76(离线复跑 68/68);`verify:packaged` 对
  `release/win-unpacked` 与 `D:\class\@classhelperdesktop-client` 均 76/76、`packaged: true`;
  `verify:e2e` 149/149(安装实例);`typecheck` / `lint` / `format:check` 全绿
2026-09-13 09:26:52 +08:00
LoveChengke 11008928a1 fix(island,homework,schedule,web): 16 项反馈修复(联合通知点不开 / 设置不生效 / 红点位置 / 看板当天且铺满 / 单双周与周数可调 / Web 手机端与批量通知等)
一、灵动岛
- ①「紧急通知后来了新作业,收起再点开不了新作业」根因有二,都已修:
  (a) active 仍是紧急那条,点击只会反复打开它 —— 现在点击胶囊优先打开**最新**那条未处理消息(队列里更晚的);
  (b) 上课时段 syncWindow 会把普通通知立刻隐藏(表现为点了没反应)—— 新增 explicitShowId,
      "用户主动点开"的那条守卫放行(岛完全隐藏时仍严格不打扰)。
  冒烟新增「紧急通知后来了新作业:收起再点击打开的是新作业(联合通知回归)」。
- ② 设置实时预览/保存:补「设置页真实链路」断言(window.desktop.islandSetAppearance → IPC → 主进程 →
  渲染进程 CSS 变量 + 窗口尺寸),并断言"岛显示中改尺寸立即生效"(实测均通过)。
- ③ 未读红点移到「通知」图标右上角(此前在文字后面),并加 DOM 几何断言(dx>0 且 dy<0,
  没有未读时自动造一条未读通知再断言、断言后删除)。
- 冒烟稳定性:新增 island.setBlurCollapseEnabled()(默认开),测试期间关掉"失焦收起",
  避免系统抢焦点导致展开态被收回的随机失败;专门验证失焦的用例仍临时打开。

二、作业看板(学生端)
- ⑥ 只看当天:新增「只看今天」开关(默认开),按 createdAt 的本地日期过滤,昨天的过期作业不再混入。
- ⑧ 完整显示:卡片同时渲染标题 + 作业要求全文(列表模式也加「作业要求」列)。
- ⑨ 铺满不滚动:看板外层高度固定 + overflow hidden,按内容/可用高度算缩放系数用 transform: scale 适配
  (工具栏显示当前缩放比例),页面本身不出现滚动条。
- ⑦ 全屏看板标题栏用 #header 插槽:标题左对齐、当天日期时间绝对居中(受「显示时间」开关控制,每分钟刷新)。

三、课表与周数
- ⑤ 学期周数可调:新增 Class.termWeeks(默认 20,迁移 + 维护脚本),班主任在班级管理里改;
  GET /api/dashboard/term?classId= 返回该班 maxWeek,课表周次下拉与新建课表的默认 weekEnd 都按它来
  (不再固定 30 周)。
- ⑩ 课表管理只保留「导入 ClassIsland 课程表」入口(时间表由课程表 JSON 的 TimeLayouts 一并带入),
  「导入时间配置」按钮下线;ui-smoke 第 7.7 段改为断言新弹窗(解析 4 行 + 单双周标签 + 旧入口已下线)。

四、以班级为单位(⑬)与 Web 端
- 个人学生账号登录停用:POST /api/auth/login 对 role=STUDENT 返回 403(改用班级码 + 班级密码);
  学生行仍作为个人数据记录键由班级会话代全班读写;e2e 全面切换到班级会话并新增该断言。
- ④ 作业科目过少:新增共享 SUBJECT_CATALOG(16 个常见科目)+ 种子课程扩到全科目;
  Web 作业发布弹窗可选「其他科目(新建)」,提交前自动建课(同名复用)。
- ⑪ 通知支持多班批量发布(多选 + 逐班发布 + 汇总结果 + 二次确认覆盖所有选中班);
  ⑫ 单班只显示已读情况、跨班才显示已读率;⑭ 手机端课表可横向滚动看全 7 天;
  ⑮ 手机端已连接不再显示孤立绿点(仅未连接时显示文字标签);⑯ 手机端「叫人/发布通知」按钮同宽左对齐;
  ⑬ Web 学生管理去掉「用户名」列(新建自动生成学号式用户名,导入仍支持显式指定)。

验证:verify:e2e 149/149、verify:web 20/20、verify:desktop 75/75(复跑 68/68,打包便携版 75/75 packaged=true)、
typecheck / lint / format 全绿;服务端与 Web 已重新打包并部署到本机安装目录。
2026-09-13 08:39:11 +08:00
LoveChengke b8ef5d8dbb feat(island,homework,schedule): 灵动岛点开修复+胶囊类型汇总 · 作业看板与未交名单 · 单双周课表与 ClassIsland 课程表导入
一、灵动岛(用户反馈的三处"点不开"全部修复)
- 非紧急叫人:上课时段仍以胶囊保持可见、可点开(不再只进队列导致屏幕上什么都没有);
  新增 isOpenable():紧急消息 + 任何"叫人"都允许点开,但普通叫人不会自动展开打断课堂。
- 紧急叫人收起后再点:命中改为"主进程按光标位置兜底校正"(上一轮的 island:set-hit-rect 轮询),
  本轮补齐叫人类专项回归(展开 → 收起 → 再次点开)。
- 设置页预览:新增 preview 通道 —— 示例岛直接展开、失焦不收起、30 秒自行消失、关掉不残留。
- 收起态胶囊汇总消息类型:多条时显示「新消息:叫人/作业/通知(共 3 条)」+「点击查看」+ 类型小标签。
- 顺带修掉两个真问题:胶囊态收到新消息会把上一条静默顶掉(现在一律回队);
  syncWindow 的上课时段守卫只认"紧急"导致普通叫人被藏掉。
- 性能:岛体矩形上报限流(~10 次/秒 + 尾随补发),修掉每帧 IPC 导致的开合采样骤降。

二、作业(学生端 + Web 教师端)
- 新增**看板模式**:按科目分卡片、点卡片放大全屏、工具栏可切「看板/列表」、
  开关「显示时间」、拖「看板字号」(11–28px),偏好持久化到客户端配置 homeworkBoard。
- 去掉「全班标记完成」,改为**未交名单**:勾选没交的同学,其余自动标记已交;
  新增 GET/PATCH /api/homeworks/:id/submissions(教师与班级设备可用,普通学生 403)。

三、课表单双周 + ClassIsland 课程表导入
- Schedule 新增 weekParity(ALL/ODD/EVEN)+ 迁移;周视图与上课判定按「第 1 周=单周」过滤,
  历史数据默认 ALL 行为不变;Web 手动新增/编辑课表可选单双周,学生端课表打「单周/双周」标签。
- 修复既有 P0 bug:ClassIsland 真实档案里 TimeLayouts 是 "Guid → { Name, Layouts }" 字典,
  旧解析器只认数组/.items,导致**真实导出完全导不进来**;现已按真实形态解析(并兼容旧写法),
  同时补上 TimeType=3(行动)映射。
- 新增 ClassIsland 课程表(ClassPlan)导入:解析 TimeRule.WeekCountDiv/WeekCountDivTotal → 单双周,
  Classes[i] 对齐第 i 个 TimeType=0 的时间点,Subjects 映射科目(缺失自动补建课程),
  星期 0=周日 换算为 1=周一 … 7=周日;replace/merge 两种模式,预览只解析不写库。
- 新增维护脚本 packages/server/scripts/apply-parity-column.cjs(手工覆盖 dist 部署时补列用)。

验证:verify:e2e 148/148(新增单双周 12 项 + 课程表导入 5 项)、verify:desktop 71/71(复跑 65/65)、
verify:web 20/20、typecheck / lint / format 全绿;服务端与 Web 已重新打包并部署到本机安装目录。
2026-09-12 23:48:59 +08:00
LoveChengke 86430c5f4a fix(island): 修好"非紧急叫人 / 紧急叫人收起 / 设置页预览"三处点不开,并让胶囊汇总通知类型
用户反馈的三个场景:
1) 非紧急叫人(HIGH + kind=call)—— 上课时段原先只进队列、屏幕上看不到东西,
   自然"无法点开"。现在:上课时段仍以**胶囊**保持可见(不自动展开、不打断课堂),
   点一下即可展开详情;下课自动展开。新增 isOpenable():紧急消息与"任何叫人"都允许点开。
2) 紧急叫人点击缩回后点不开 —— 归因于固定包围盒窗口的命中丢失(上一轮已改为光标兜底轮询),
   本轮补上"展开 → 收起 → 再次展开"的叫人类专项回归。
3) 设置页"预览效果"示例岛无法正常展开 —— 预览通知现在走 preview 通道:直接展开、
   失焦不收起(用户拖滑块时主窗口一直是焦点)、30 秒后自行消失、点"知道了"不留残影。

顺带修掉两个真问题:
- pushNotification 在"胶囊态"收到新消息时会把上一条静默顶掉(只有展开态才回队);
  现在只要换了当前展示的消息,上一条一律回队列 —— 否则类型汇总与"还有 N 条"都会漏。
- syncWindow 的上课时段守卫只认"紧急",导致普通叫人被 hideImmediately 藏掉;改用 isOpenable。

胶囊类型汇总(用户要求):多条未处理消息时收起态显示
「新消息:叫人/作业/通知(共 3 条)」+「点击查看」,并把三种类型做成小标签;
单条时仍显示类型名(新作业 / 老师叫你 / 新消息)。

性能:岛体矩形上报加限流(~10 次/秒,尾随补发最终形态),修掉"每帧 IPC + 每帧读光标"
导致的渲染变慢(实测会让开合采样从 57 帧掉到 4 帧)。

验证:verify:desktop 71/71 + 离线复跑 65/65;typecheck/lint 全绿。
2026-09-12 22:36:37 +08:00
LoveChengke d8b0eb05c2 fix(island): 命中改为"光标位置兜底轮询",修复"点开再收起就再也点不开"
用户复现路径:点开灵动岛(展开)→ 收起为胶囊 → 再点就毫无反应。
根因:固定包围盒窗口默认整块穿透,命中只在渲染进程收到 mousemove 时才打开。
Windows 下 setIgnoreMouseEvents(true, { forward: true }) 的 mousemove 转发并不总是可靠,
一旦"展开 → 收起"后转发丢失,窗口就永远停在穿透状态 —— 看得到胶囊,却怎么点都没反应。

修法(双保险,命中不再依赖"必须先收到一次 mousemove"):
- 渲染进程把**岛体矩形**上报主进程(新 IPC island:set-hit-rect,随状态/外观/形变更新,
  彻底隐藏时上报 null);
- 主进程每 120ms 用 screen.getCursorScreenPoint() 与矩形比对校正命中(窗口不可见或无矩形
  则强制穿透),并在收到矩形时立即校正一次;
- 保留渲染进程 mousemove 的快速路径(即时响应),两者使用同一矩形,结论一致;
- 渲染进程仍记住最近指针位置,状态变化时按新几何重算命中。

冒烟新增/改造(新增可注入光标位置 setHitTestCursor,避免去挪用户物理鼠标):
- 「展开 → 收起 → 再次展开(用户复现路径)」:真实点击往返,并断言每个形态都上报了命中框;
- 「命中兜底:岛体矩形随形态更新」(展开 424x230 → 收起 268x44);
- 「彻底隐藏时撤销命中框(不吞桌面点击)」;
- 「空闲细缝态命中跟随光标」(细缝上可点、移开即穿透);
- 「岛外鼠标穿透」与「收起态点击可再次展开」改为注入光标位置做确定性断言。

验证:verify:desktop 65/65 + 离线复跑 59/59;打包便携版 65/65(packaged=true);
typecheck / lint / format 全绿。
2026-09-12 21:49:14 +08:00
LoveChengke 80a0efbcfe fix(smoke): 冒烟不再悄悄改掉演示班级密码(先试已知密码,必要时才重置并恢复)
- 现状问题:pnpm verify:desktop 会用管理员接口把被测班级密码改成 smoke123456 且不恢复,
  导致演示实例的「班级码 G101 / 123456」在跑完冒烟后登录失败(本次验证时实际踩到)。
- 现在先按 [冒烟密码, 种子默认 123456] 依次探测:命中就直接用,**完全不改数据**;
  只有当两者都不通时才临时重置,并在脚本收尾(含失败分支)恢复为 123456 并打印日志。
- README 冒烟章节补充说明;演示实例的 G101 密码已恢复为 123456。

验证:verify:desktop 62/62(日志显示「班级账号就绪:G101」即未改动数据)、离线复跑 56/56;
打包便携版 62/62(packaged=true)且跑完后 G101/123456 仍可登录。
2026-09-12 21:06:58 +08:00
LoveChengke b69f708aa2 feat(web,desktop,server): 灵动岛收起后可再次打开 · 下线作业截止时间 · Web 端圆角设计
灵动岛"收起后部分情况无法再次打开"(用户反馈):
- 收起不再等于消失:只要有未处理通知(当前展示的或队列里的),超时后保持胶囊常驻,
  只有"知道了 / 标为已读"、队列清空或进入上课时段才彻底隐藏。
- 收起态点击兜底:active 为空但队列还有未看通知时,点击胶囊会把它取出并展开;
  上课时段仍严格不显示(该兜底放在上课守卫之后,不影响"上课不打扰")。
- 命中测试自愈:渲染进程记住最近指针位置,状态/外观变化与形变每一帧都按新几何重算命中;
  岛可见时主进程也会重新下发可交互,避免"窗口还处于穿透 → 点了没反应"。
- 新增 3 条回归断言:收起后胶囊常驻、收起态点击可再次展开(命中恢复)、空闲细缝也可点开。

作业"截止时间"功能下线(用户要求):
- 接口/类型:HomeworkDto 与创建请求去掉 dueAt,homeworks schema/service 不再读写;
  仪表盘去掉"即将截止的作业"卡片与待截止统计口径(改为"未完成/待完成")。
- Web:作业发布表单移除截止时间选择、列表与详情移除截止时间列;
  学生端作业列表/详情移除截止时间与"已逾期"状态(只保留完成状态)。
- 灵动岛:作业卡不再显示"截止时间:…"副标题,改为只展示作业要求。
- DB 中的 Homework.dueAt 列**保留**(已在 schema 注明为遗留字段),避免对已安装库做破坏性迁移;
  种子数据不再写入该列。

Web 管理端圆角设计(用户要求):
- 新增设计令牌 --ch-radius-xl/lg/md/sm/pill 与两级柔和投影,并让 Element Plus 的
  --el-border-radius-* 对齐同一套令牌;卡片、弹窗、抽屉、按钮、输入、标签、分页、
  侧栏菜单项、表格表头统一改造,卡片去硬边框改投影 + 悬停微浮起,顶栏/侧栏做圆角收口。
- 未改组件源码,全部为 CSS 变量 + 类选择器覆盖;手机端媒体查询保持生效(截图已更新)。

验证:verify:desktop 62/62(含 3 条收起态回归)与离线复跑 56/56、打包便携版 62/62(packaged=true)、
verify:e2e 136/136(新增"作业接口不再返回 dueAt")、verify:web 20/20、typecheck/lint/format 全绿;
服务端与 Web 已重新打包并部署到本机安装目录。
2026-09-12 21:01:31 +08:00
LoveChengke 0690546307 fix(island,server): 连续圆角四角修正 + 叫人分紧急/普通两级
灵动岛圆角(用户反馈"左上角和右下角缺一块 / 圆角边缘割裂"):
- squircle.ts 改为"每个角独立圆心 + 角度参数"的超椭圆采样(原本四角共用同一个偏移向量,
  左上/右下被切掉一块);四角几何与 WinIsland 的连续超椭圆一致,r 与 h/2、w/2 自动收敛。
- verify:desktop 新增像素级断言「连续圆角四角一致」:从四角沿对角线找岛体边界,
  四角步进必须两两一致(展开 15/15/15/15、紧急 14/14/14/14、胶囊 5/5/5/5),
  并保留路径绕行 |winding|≈2π 断言(抓"角上往回折")。该断言即本次回归的守卫。
- 截图留档同时写入裁剪图与整窗图,便于人工复核角形。

叫人分紧急 / 普通两级(用户要求"叫人也分为紧急和普通"):
- shared:CreateCallRequest 新增 urgent;紧急叫人 → URGENT(无视上课时段立即展开),
  普通叫人(默认)→ HIGH(按普通通知排队)。
- desktop:isImmediate 只认 priority=URGENT,不再把 kind=call 一律视为立即消息;
  普通叫人回到与普通通知相同的排队 / 隐藏 / 下课弹出路径。
- web:通知发布 → 叫人、学生管理 → 叫人 两个弹窗新增"级别"单选(默认普通)+ 联动提示文案。

验证:verify:e2e 135/135(新增普通 / 紧急叫人级别断言)、verify:web 20/20(新增级别单选断言)、
verify:desktop 59/59 + 离线复跑 53/53、typecheck / lint / format 全绿。
2026-09-12 19:54:00 +08:00
LoveChengke 1b4dd14e4a docs(island): README 与设计文档补齐 WinIsland 架构对照(第 1 轮收尾)
- README 客户端冒烟验证表:新增两条断言(窗口全程恒定 + 形状与卡片框一致、岛外鼠标穿透),
  并把"开合不震动"改成按"岛"描述(窗口冻结 / 岛 0.00px / 无过冲);空闲细缝更新为 6x22
- README 个性化设置表与交付清单:去掉已失效的"Windows 36px 最小窗口高度"说明
- docs/winisland-design-tokens.md:新增"实际采纳了哪些"对照表
  (固定包围盒窗口、弹簧参数、连续圆角、交叉淡入、描边/投影、亚克力、命中控制、6 锚点、字号模型,
  以及明确未采纳的音乐/歌词/小组件部分)

验证:verify:desktop 54/54(离线复跑 48/48)、打包产物 portable 54/54、verify:e2e 133/133、verify:web 19/19
      lint / format:check / typecheck 全通过;服务端安装包同步重打(18:53)
2026-09-12 18:53:32 +08:00
LoveChengke e1e7e6ef5d docs(screenshots): 更新 WinIsland 架构下的灵动岛留档截图 2026-09-12 18:48:34 +08:00
LoveChengke 7a4668cb1b feat(island): 彻底照搬 WinIsland 架构重写灵动岛(固定包围盒窗口 + 窗口内弹簧形变)
背景:上一版仍沿用"每帧移动/缩放透明窗口"的做法,实机上会出现形变抖动与残影
(用户反馈"越改越烂、开合震动还在")。本次按 WinIsland 的真实做法重做架构。

主进程(packages/desktop-client/src/main/island.ts):
- 窗口一次创建为**固定包围盒**(最大形态 + ISLAND_SHADOW_PAD 阴影留白),
  开合过程中不再调用 setBounds;只在"尺寸/位置设置变化"时重排一次窗口
- 新增 setInteractive():默认 setIgnoreMouseEvents(true,{forward:true}) 整块穿透,
  由渲染层命中测试回调切换(照搬 WinIsland set_cursor_hittest),保证不吞桌面点击
- 移除逐帧几何代码(animateBounds/boundsFor/computeBounds/resolveAnchor 用于窗口定位的简化版保留)
- 空闲细缝不再受 Windows 36px 最小窗口高度限制(窗口固定,岛可真正缩到 6px 宽)

渲染层(IslandApp.vue + 新增 spring.ts):
- 岛是**单一元素**,宽/高/圆角由 WinIsland 弹簧逐帧逼近:
  force=(target-value)*0.10*dt → velocity=(velocity+force)*0.68^dt → value+=velocity*dt
  (dt 以帧计;换向保留 35% 动量、单帧位移 ≤ 剩余距离 20%),并加零过冲钳制
- 胶囊层与展开层常驻并交叉淡入(展开 progress²、胶囊 1-1.5×progress),形变全程无缩放回弹
- 形状层改为显式像素宽高 + preserveAspectRatio="none"(避免 viewBox 缩放导致形状错位/残留角)
- 修两个实机可见缺陷:① 展开层漏了 flex-direction: column(展开卡内容横排、元素散落);
  ② 隐藏且无细缝时不再把岛缩成细缝(否则下次弹出会"从细缝长大")
- 尺寸增量 ISLAND_SIZE_DELTA / 阴影留白移入 @classhelper/shared,主/渲染两端共用一份

验证(verify:desktop 53 → 54 项,全绿):
- 新增「开合过程中窗口全程恒定」(逐帧窗口签名恒为 476x282@482,-2,岛 386x230→…→268x44 单调收缩,
  SVG 形状 bbox 与卡片框一致 ±2px)
- 新增「岛外鼠标穿透」(岛内 interactive=true / 岛外 false)
- 开合不震动改为按"岛"判定:窗口冻结 + 岛锚点 0.00px + 无过冲;截图按岛矩形裁剪并统计岛外红像素=0
- 其余能力(通知/作业/叫人/紧急、标为已读、上课隐藏、6 锚点、字号可调、设置立即生效、空闲细缝)全部保留
2026-09-12 18:46:20 +08:00
LoveChengke 93447922dc fix(island): 失焦收起加 500ms 宽限期 + 冒烟不再改写班级码(打包产物验证稳定)
- 灵动岛窗口 blur 处理增加 BLUR_GRACE_MS = 500:激活后 500ms 内的失焦一律忽略。
  实机/打包产物上偶发"刚展开就被系统换前台造成的 blur 立刻收起",
  表现为"下课后自动弹出通知详情"变成 pill(截图比例也随之异常);
  宽限期后该用例稳定,且"点击屏幕任意位置收起"用例(展开后等 700ms 再触发 blur)仍正常。
- 桌面冒烟准备班级账号时**永不修改班级码**(只按需重置密码):
  班级码是给学生用的凭据,不应被测试工具改写;实测产物验证不再依赖固定班级码。

验证: verify:desktop 53/53(首轮)与 47/47(二次启动)全绿
2026-09-12 17:48:32 +08:00
LoveChengke a5c717e33e chore: 不再跟踪第三方参考包 WinIsland-master.zip(已提取设计 token 入库) 2026-09-12 17:41:12 +08:00
LoveChengke 3f2b135211 feat(island): 参考 WinIsland 重写灵动岛(squircle + 三种底 + 字号系数排版 + 空闲细缝)
参考: 工作目录 WinIsland-master.zip(Rust/Direct2D 的 Windows 动态岛),
     逐项提取的设计 token 归档在 docs/winisland-design-tokens.md。

视觉与造型:
- 连续圆角 squircle(新增 island/squircle.ts,超椭圆角,对齐 WinIsland utils/shape.rs),
  胶囊半径 = min(圆角, h/2)(满圆角),展开卡 = min(48*圆角/20, w/2, h/2)(WinIsland expanded_island_radius)
- 三种底: 纯黑(WinIsland default)/ 毛玻璃(win.setBackgroundMaterial('acrylic'),等价 HostBackdropBrush)/
  主题色渐变;描边 1px 白 alpha .118(纯黑)/.157(玻璃),展开投影 0 2px 3px rgba(0,0,0,.11)
- Apple 系统色: 强调蓝 #0a84ff、警告橙 #ff9f0a、危险红 #ff453a;白字 alpha 分级 .92/.72/.58
- 状态色只做"内层微染 + 描边 + 徽标",不外溢(保持外圈偏红 = 0)

排版(本次重点):
- 基础字号 11–20px,岛内**所有文本** = 基础字号 × 排版系数
  (标题 1.08 / 正文 0.95 / 次要 0.78 / 徽标与按钮 0.76 / 胶囊标题 0.92 / 胶囊副题 0.74),
  与 WinIsland 的 font_size 模型一致;实测 13→19px 全部文本倍率 1.462(=19/13)
- 图标、按钮高度、间距也由字号推导,改字号不会溢出

形态与设置:
- 尺寸基准对齐 WinIsland 比例(胶囊 216x34 基准、展开 +156/+186 等),卡片 = 窗口 - 2*5px 且水平居中
- 6 个停靠位(顶/底 × 左/中/右,对齐 WinIsland DockPosition)
- 新增"空闲细缝"(WinIsland hidden_width):空闲留一条 6px 宽竖条,来消息自动展开
- 所有设置依旧"改即生效";顺带修掉 hide() 用 skipWindow 绕过 syncWindow 导致细缝不生效的问题

验证(verify:desktop 49 → 53 项,全绿):
- 字号即时生效: 13px→19px 五项文本倍率全部 1.462
- 风格即时生效: 纯黑 fill=rgb(0,0,0)/材质 none;毛玻璃 fill=rgba(10,10,14,.59)/材质 acrylic;主题色 fill=rgb(11,11,15)
- 空闲细缝: 16x36 → 来消息 268x44;关闭后完全隐藏
- 6 锚点、开合不震动(0.00px / 无过冲)、截图 6 张(比例偏差 0.000、外圈偏红 0)全部保持
- verify:e2e 133/133、verify:web 19/19 不受影响;截图归档到 docs/screenshots/island/

工程发现: Windows 对透明窗口有约 36px 最小高度(请求 32px 实际 36px),
空闲细缝高度据此设下限并写入注释与 README。
2026-09-12 17:41:05 +08:00
LoveChengke 9c9ac69ac6 fix(island): 开合不再震动——去掉弹簧过冲 + 锚点整数化
问题(用户反馈"灵动岛开合的时候不要震动"):
之前虽然有「收回不抖动」断言,但它只看窗口宽度单调收缩,漏掉了两处真实回弹。
逐帧采样卡片真实屏幕矩形后复现:
- 展开时窗口冲到 411x329(目标 404x316)→ 主进程用了 easeOutBack 弹簧缓动,尺寸超过目标再弹回
- 卡片被放大到 400.6x311.6(目标 396x310)→ 卡片入场用 cubic-bezier(0.34,1.56,0.64,1) 缩放,先超过 1 再回落
- 卡片中心左右波动 1.00px、上边缘波动 2.88px → 逐帧 Math.round 在 0.5 边界跳变 + transform 位移

修复:
1. 缓动单调化: 删除 easeOutBack,展开/收回统一 easeOutCubic(窗口与卡片都不再过冲)
2. 锚点整数化且与尺寸解耦: 新增 resolveAnchor(),停靠位置决定不动的那条边/中心,动画前算一次
3. 卡片宽度取偶数 + boundsFor 统一推导窗口矩形(消除逐帧 0.5px 抖动)
4. 卡片入场去掉 scale/translateY,只做 180ms 淡入,形变完全交给窗口尺寸
5. 顺带修掉新探针暴露的真实缺陷: 隐藏后再弹出胶囊时透明度被硬编码为 1,忽略了个性化设置

验证(verify:desktop 49/49,修复前后对比):
- 展开: 卡片中心波动 0.00px / 上边缘 0.00px,最大窗口 404x316(=目标)、最大卡片 396x308,单调=true
- 收回: 0.00px / 0.00px,最小窗口 268,宽度序列单调无回跳
- 透明度 0.62 生效;四个停靠位置、置顶、动画开关/速度、持久化全部通过
- verify:e2e 133/133、verify:web 19/19 不受影响
文档: README 形变动画章节更新为「单调不过冲 + 整数锚点」,并记录修复前后实测数据
2026-09-12 16:41:01 +08:00
LoveChengke 97a89de944 feat(rbac): 成绩录入/导入放开到本班班主任(需求 6 与需求 7 口径统一)
依据: 需求 6 明确要求'成绩与表格导入……老师端与学生端班级账号均可用且不越权',
      需求 7 的权限清单并未把成绩收归管理员专属(只限制班级增删改、人员分配与科任权限)。

变更:
- shared/permissions: canManageGrades(classRole) = ADMIN | HEAD;canImport 按 kind 分发
  (成绩=ADMIN|HEAD、名单=仅管理员、课表时间=ADMIN|HEAD);PERMISSION_MATRIX 同步
- lib/access: assertCanManageGrades(user, classId) 异步按班级角色判定,科任/跨班给出针对性提示
- grades 服务与路由、imports 的模板/预览/提交路由放宽为 ADMIN|TEACHER,
  服务层按 kind + 班级二次校验(名单仍仅管理员,科任仍 403)
- Web 管理端: 成绩录入菜单对教师可见;写入/导入按钮仅对'管理员或所选班级班主任'显示

验证:
- verify:e2e 133/133(新增: 班主任录入本班成绩 201 + 跨班 403、班主任模板/预览/导入本班成绩可用、
  科任导入 403、班主任名单模板与导入 403、append 重复导入跳过)
- verify:web 19/19(新增: 班主任可见成绩导入入口并打开弹窗,越权入口仍隐藏、菜单 5 项)
- verify:desktop 47/47(未受影响)
文档: README 权限矩阵与'需求 6/7 口径统一'对照表、各项计数更新
2026-09-12 16:21:39 +08:00
LoveChengke 21485ea294 style: README 表格格式化 2026-09-12 16:14:14 +08:00
LoveChengke 910e520d09 fix(verify): e2e 班级码用后还原,避免把演示实例改成随机码
问题: verify:e2e 的班级账号段会临时把班级码改成 E2E####,结束时只还原了密码,
      导致演示实例(以及随后跑的打包产物冒烟)拿着文档默认码 G101 登录被 401。

修复:
- e2e 开始前记录该班原本的班级码,结束时连码带密码一起还原
- 新增断言'清理:班级码恢复为验证前的原值(不污染演示数据)',并断言用原码仍可登录
- README 权限矩阵段落补充需求 6/7 的成绩导入取舍说明与放开范围(6 处改动清单)

验收: verify:e2e 126/126(结束后实例仍为 G101/123456)
      打包产物(portable.exe) 47/47, packaged=true, login=高一(1)班 classSession=true
2026-09-12 16:13:58 +08:00
LoveChengke b07ee4f8d7 test(desktop): 个性化全参数 + 退出无残留的自动化验证(verify:desktop 40 → 47 项)
新增 7 项真实断言(全部读真实窗口属性,而非界面显示):
- 透明度 → win.getOpacity 实际 0.62
- 主题色 → 渲染进程 CSS 变量 --island-accent = #ff7043
- 停靠位置 → 左上 8,8 / 右上 1164,8 / 底部居中 586,800 / 顶部居中 586,8
- 置顶开关 → isAlwaysOnTop 关/开
- 动画开关 → 关闭后 60ms 内即到位(404x316),无逐帧过渡
- 动画速度 → speed=0.5 收回 435ms vs speed=2 收回 128ms
- 持久化 → saveConfig/getConfig 全字段往返一致

需求 5 的'退出无残留'改为进程退出后实测:
- 删除 userData 目录(SingletonLock/IndexedDB 文件锁)成功 → 无句柄残留
- 用同一配置目录二次启动成功并再次全绿 → 单实例锁与进程均已释放

验收: verify:desktop 47/47(二次启动 41/41), verify:e2e 125/125, verify:web 18/18
文档: README 客户端冒烟验证表刷新为最新实测值 + 新增退出无残留说明
2026-09-12 16:09:56 +08:00
LoveChengke d11f8e43a1 chore(release): 客户端班级账号收尾 + README/安装包产物更新
- 客户端设置页展示班级码与登录方式(班级账号 / 个人学生账号)
- 冒烟脚本复用已有班级凭据(现成的班级码 + 密码可直接登录时不再改密码,避免污染实例)
- README: 学生端班级账号章节、功能页面/API/验收/命令计数(verify:e2e 125)同步
- 安装包 README 默认账号补充班级码 G101/G102/G203 + 班级密码 123456

验收: pnpm verify:e2e 125/125, verify:web 18/18, verify:desktop 40/40,
      打包产物实测(portable.exe): 40/40, login=高一(1)班 classSession=true
2026-09-12 16:02:37 +08:00
LoveChengke 1073436ad6 feat(class-account): 学生端主体改为班级(班级码 + 班级密码)+ 数据按班级隔离
服务端:
- Class 新增 code(唯一)/passwordHash 与迁移(老数据回填 C00001 形式, 未设密码禁止登录)
- POST /auth/class-login: 班级码 + 班级密码 -> classSession 会话(sub=classId)
- PATCH /classes/:id/class-account: 管理员改班级码/重置密码(唯一性、格式校验)
- auth 中间件/socket 握手支持班级会话(sub 为 classId, 回查 Class 表, 加入 user:{classId} 房间)
- lib/session.ts: 班级设备代全班操作(通知已读/作业完成/成绩总览按全班读写), 普通学生账号行为不变
- 叫人消息同时广播到 user:{classId}, 班级设备立即上岛
- 班级列表/详情按权限返回班级码与密码状态(哈希不外泄)

客户端:
- 登录页改为班级码 + 班级密码(个人学生入口移除, /auth/login 接口保留兼容)
- 作业按钮/成绩标题按班级会话显示(全班标记完成 / 本班成绩)
- 冒烟脚本自动准备班级账号凭据(兼容全新与升级安装)

Web 管理端:
- 班级管理新增「班级账号」列与设置弹窗(改班级码/重置密码/随机生成)
- 新建班级可选自定义班级码, 默认由服务端生成

验证: verify:e2e 125/125(新增班级账号 18 项) verify:web 18/18 verify:desktop 40/40
      (login=高一(1)班 classSession=true)
文档: README 新增「学生端班级账号」章节、API/迁移/验收/默认账号更新; 安装包 README 同步
2026-09-12 15:51:08 +08:00
LoveChengke 8b85079d81 feat(imports): 成绩/名单表格导入 + ClassIsland 时间配置导入 + 安装版增量迁移
服务端:
- 新增 imports 模块: 模板下载(csv/xlsx)、表格解析预览、字段映射导入(append/upsert)、
  错误行号与结果统计; ClassIsland 时间配置容错解析 + replace/merge 导入 + 失败回滚(400 不写库)
- TimeLayout 模型 + 迁移; 修复 contentBase64 空值被 422 拦截, 改由 service 返回 IMPORT_EMPTY_FILE
- merge 语义改为按开始时间槽位覆盖, 同开始时间的旧节次不再被静默丢弃
- db-bootstrap 增加 _ch_migrations 账本: 覆盖安装升级也能自动补跑新增迁移(旧库实测通过)

Web 管理端:
- TableImportDialog(成绩/名单): 模板下载、预览、字段映射、重复处理模式、结果统计与错误行
- TimeLayoutImportDialog: 粘贴/选 .json、解析预览、覆盖/合并、已导入列表与删除
- 成绩录入/学生管理/课表管理接入入口(按权限矩阵显示)

验证: verify:e2e 107/107(新增导入 22 项) verify:web 18/18(新增时间配置导入弹窗真实点击)
      verify:desktop 40/40
文档: README 新增导入章节、增量迁移说明、API 表与验收对照更新
2026-09-12 15:35:01 +08:00
LoveChengke 0dcab6f369 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` 配置目录按进程号隔离,避免上次异常退出的目录占用导致启动失败。
2026-09-12 15:19:13 +08:00
LoveChengke 30e4c36809 feat(rbac): 落地角色权限模型(需求 7)——班级/人员/名单/成绩收紧到管理员,班主任管本班课表,科任仅作业·叫人·通知
## 权限矩阵(前后端共用一份)

- 新增 `packages/shared/src/permissions.ts`:`ClassRole`(ADMIN/HEAD/SUBJECT/STUDENT/NONE)、
  `resolveClassRole()`、`canManageClasses/AssignTeachers/Roster/Schedule/Courses/PublishContent/ManageGrades/Import`
  与 `PERMISSION_MATRIX`(README 与验收脚本据此展开)。
- 后端 `lib/access.ts`:新增 `resolveUserClassRole()`(`Class.teacherId` → 班主任,`ClassTeacher` → 科任)
  与 `assertCanManageClasses/AssignTeachers/Roster/Schedule/PublishContent/ManageGrades`;
  各 service 在**服务层**断言(路由仅做角色过滤),越权统一 403。
- 收紧项:班级增删改、班主任/科任分配、学生名单与账号 → 仅 ADMIN;
  课表/课程增删改 → ADMIN 或本班班主任;成绩录入/修改/批量 → 仅 ADMIN(按需求清单);
  作业/通知/叫人 → ADMIN、班主任、科任皆可。
- 读权限保持不变:教师仍可查看自己班级的课表/作业/通知/成绩列表与统计。

## 前端按同一矩阵隐藏入口

- 侧边栏:教师端不再显示「班级管理 / 学生管理 / 成绩录入」(管理员仍是 7 项)。
- 班级页:新建/编辑/删除按钮仅管理员可见;课表页:增删改按钮仅"管理员或本班班主任"可见;
  成绩页:录入/批量/删除按钮仅管理员可见。
- **叫人入口调整**:教师需要叫人,而「学生管理」是管理员专属,因此教师入口移到
  「通知发布 → 叫人」(选班级 → 选学生 → 快捷短语/自定义消息;名单走 `/classes/:id/students`,
  不使用管理员专属的 `/students`);管理员在学生管理页仍保留逐行「叫人」。

## 顺手修掉的两个真实缺陷

- `NotificationsView.openCreate()`:班级列表异步加载期间点「发布通知」会导致「目标班级」为空、
  校验失败且**静默不发布**;现在打开弹窗前会确保班级已加载(冒烟同步等待下拉就绪)。
- `desktop-client/scripts/smoke.mjs`:上一次冒烟被强杀后配置目录仍被句柄占用,`rmSync` 报 ENOTEMPTY
  直接让冒烟启动失败;改为按进程号使用独立目录并容错清理。

## 验证

- `pnpm verify:e2e` **82/82** ✅:新增 14 项角色权限断言(班主任/科任建班被拒、班主任删班被拒、
  班主任/科任分配人员被拒、管理员分配与取消成功、科任管理课表被拒、科任可布置作业、
  科任/班主任录入成绩被拒、管理员录入成功),并把历史用例中"仅管理员可执行"的操作切到管理员令牌。
- `pnpm verify:web` **17/17** ✅:新增「教师端隐藏无权入口」断言(可见菜单=仪表盘/课表/作业/通知,
  越权入口=[]),叫人入口改到通知页后仍验证弹窗、8 条快捷短语、自定义输入与可选中态。
- `pnpm verify:desktop` **34/34** ✅;`lint` / `format:check` / `typecheck` 全绿。
- 数据清理:清掉历史遗留的测试班级与 4 条探针课表,演示数据恢复为 3 个班级。
2026-09-12 15:02:02 +08:00
LoveChengke 71f43042ca docs: 客户端三套产物(setup / portable / win-unpacked)均按最新代码重打且打包后冒烟 28/28,同步 README 与灵动岛归档截图 2026-09-12 14:19:41 +08:00
LoveChengke 6b8563d0d3 feat(desktop-client): 课表改为 ClassIsland 风格今天时间轴(大时钟 + 当前课强调色高亮 + 进度条与倒计时 + 已结束/下一节标记),补充时间轴自动化校验与实拍 2026-09-12 14:15:00 +08:00
LoveChengke 219f6ae83a feat(desktop+web+server): ClassIsland 风格 UI、灵动岛动画/交互修复、作业上岛与"叫人"功能
## 灵动岛修复与动画优化(用户反馈)

- **收回不再出现"方框"闪烁**:卡片改为**固定逻辑尺寸 + 顶部居中锚定**,
  窗口只负责露出/裁切透明区域。此前卡片随窗口拉伸,形变过程中会看到被拉扁的圆角方块,
  胶囊里的铃铛/文字还会被垂直居中而"从上跳到下";两条问题同源,现已消除。
  冒烟用 15 次采样断言卡片尺寸恒为固定值(`396x308` / `260x38`),一旦出现中间值即失败。
- **点击屏幕任意位置收回**:展开态临时 `setFocusable(true)` 并取焦,点到别处即失焦 → 自动回缩为胶囊;
  收起后立即释放可聚焦性。断言覆盖 `focusable=true → 失焦后 mode=pill、focusable=false`。
- 动画继续打磨:危险/叫人形态改为**内部光晕**(卡片四周再留 4px 透明边,彻底避免窗口边界裁切出硬边)、
  尖角与圆角统一、形态尺寸各异(胶囊 268×44 / 详情 404×316 / 紧急 424×344 / 叫人 440×360)。

## 灵动岛"标为已读"真正同步通知中心

- 新增链路:灵动岛 `mark-read` → 主进程 `setMarkReadHandler` → 主窗口渲染进程 →
  `notificationApi.markRead` + 刷新 store(未读红点即时减少)。
- 真机链路断言:点击"标为已读"后 `read=false → true`、未读数 `1 → 0`(此前只从灵动岛队列移除)。

## 作业发布也上岛

- `homework:new` → 灵动岛 `kind=homework`:胶囊显示"新作业"(青蓝描边 + 书本图标),
  展开显示作业说明与"截止时间:…"。

## 新增"叫人"功能

- 服务端:新模块 `modules/calls`(`POST /api/calls`)——校验班级可写 + 学生属于该班,
  落库一条 HIGH 级通知(进通知中心、可用已读状态),广播 `notification:new` 到班级房间,
  并定向 `call:new` 给被叫学生。
- 共享层:`CALL_QUICK_PHRASES`(8 条快捷短语)、`buildCallTitle()`("请 XXX 同学找 XXX 老师",老师名已含"老师"不重复)、
  `ServerToClientEvents['call:new']`、`CreateCallRequest`。
- Web 管理端:学生管理列表新增 **叫人** 按钮 → 弹窗显示对象、快捷短语(点选/取消)、自定义消息(优先),发送即通知。
- 客户端:`call:new` → 灵动岛 `kind=call`,**上课时段也立即展开**(老师正在等这名学生),
  琥珀金卡片 +"叫人"徽标 +"收到"按钮,90 秒后自动收起。

## 客户端界面改为 ClassIsland 同款 Fluent 风格

- 主题令牌:Mica 浅底 `#f3f3f3`、亚克力卡片 `rgba(255,255,255,.7)` + `backdrop-filter`、
  强调色 `#0F6CBD`、半透明描边、卡片 8px / 控件 4px 圆角、极轻阴影。
- Element Plus 变量整体对齐(`--el-color-primary`、圆角、表头/行分隔、滚动条等)。
- 侧边导航改为 Fluent NavigationView 观感:白色半透明底、选中项强调色淡底 + 左侧 3px 强调条;
  品牌标改为强调色渐变圆角方块。附客户端五个页面实拍(`docs/screenshots/client/`)。

## 验证

- `pnpm verify:e2e` **68/68**(新增 6 项叫人:201 + 标题模板、自定义消息优先、上课时段允许、学生 403、通知中心可见、清理)
- `pnpm verify:desktop` **33/33**(新增:卡片固定尺寸、点击任意处收回、标为已读同步、作业上岛、叫人上课中立即展开、真实通知链路)
- `pnpm verify:web` **16/16**(新增叫人入口:弹窗、8 条快捷短语、自定义输入、短语可选中)
- `pnpm lint` / `format:check` / `typecheck` 全部通过;服务端安装包已重打并安装验证
2026-09-12 13:57:46 +08:00
LoveChengke f759aac0a2 feat(web-admin): 手机小屏适配(1Panel 风格抽屉导航)并补齐移动端自动化验证
## 布局与交互(沿用 1Panel 风格)

- 新增 `src/composables/useResponsive.ts`:断点 768 / 992px + resize/orientationchange 监听
- `AdminLayout.vue`:
  - >992px 桌面保持原样(210px 深色侧栏 + 顶栏连接状态 + "已接收 N 条实时事件")
  - <=768px 手机:侧边栏不再渲染,顶栏出现汉堡按钮(`aria-expanded` 标记状态),
    点击唤出 240px 深色抽屉菜单,点菜单项自动收起;顶栏显示当前页标题,
    用户菜单收成头像图标(身份信息移入下拉菜单首行)
  - 复用同一套 `.ch-sidebar` 样式,抽屉内容与桌面侧栏视觉一致
- `styles/index.css` 增加全局响应式规则:工具栏控件铺满整行、`.table-card` 表格卡片内横向滚动、
  弹窗 `calc(100vw - 24px)`、消息框/抽屉自适应、`.stat-grid` 小屏两列、分页居中、标签页可横滑
- 各视图:7 个表格卡片加 `.table-card`;班级详情/作业详情的 `el-descriptions` 小屏降为单列;
  登录页卡片自适应宽度并加安全内边距

## 自动化验证(scripts/ui-smoke)

- 新增 `runMobileChecks`:把窗口真实缩到 390×844,用真实点击验证 7 项——
  侧边栏收起 + 汉堡出现、抽屉滑出且菜单完整、抽屉内点击"通知发布"跳转并收起、
  页面无横向溢出(表格自身滚动不计入)、表格在卡片内可滚动、工具栏控件铺满、弹窗宽度自适应
- 移动端截图留档到 `docs/screenshots/web-mobile/`(`UI_SMOKE_SHOTS_DIR` 可覆盖)
- 踩坑修复:隐藏窗口下 Chromium 会冻结 CSS transition,Vue `<Transition>` 收不到 transitionend,
  Element Plus 抽屉会卡在 `enter-from`(表现为"点了没反应")——移动端检查改为 `showInactive()` 并断言
  "抽屉滑到位/收起到位",而不是只看元素是否存在

## 安装包升级健壮性

- `scripts/nsis/server-installer.nsi`:覆盖安装前先 `RMDir /r web|server|node_modules`,
  避免旧 hash 分片、旧 dist 模块、旧依赖残留导致"新旧版本混装"
  (此前 web/assets 里累积了 4 份 AdminLayout 分片)

## 验证结果(均在安装后的生产实例上)

- `pnpm verify:web` 15/15 ✅(8 项原有 + 7 项移动端)
- `pnpm verify:e2e` 62/62 ✅
- 覆盖安装:`.env` 与数据库保持不变,`web/assets` 只剩当前版本分片,服务 `healthz` 正常
- `pnpm lint` / `format:check` / `typecheck` 全部通过
2026-09-12 10:09:11 +08:00
LoveChengke e434032b6e docs: 更新客户端安装包实测数据(portable 106.9MB / setup 107.2MB,均 23/23 通过) 2026-09-12 09:45:47 +08:00
LoveChengke b037131e46 docs: 补充 portable/setup 安装包冒烟结果(各 23/23,packaged: true)并刷新灵动岛归档截图 2026-09-12 09:45:35 +08:00
LoveChengke 38c7660dd9 fix(desktop-client): 灵动岛反馈修正——红晕改为卡片内部 inset(窗口边缘零外溢)、紧急通知补展开动画、普通通知去掉上岛入场动画、点空白处/右上角按钮回缩为胶囊、上课时段严格不显示;冒烟用独立 userData 以免与运行中的客户端抢单实例锁 2026-09-12 09:39:33 +08:00
LoveChengke 3ab3cb1eb1 test(desktop-client): 灵动岛各状态截图做像素级留档与断言(尺寸/宽高比/绘制内容/紧急红光),截图归档到 docs/screenshots/island,打包后 EXE 16/16 通过 2026-09-12 08:54:33 +08:00
LoveChengke d42b0b2a1b docs: 补充安装包故障排查(shared 旧构建 / Electron 宿主下的 node.exe)并更新校验项口径 2026-09-12 08:49:41 +08:00
LoveChengke 3a9f8812fc feat: 客户端灵动岛 + 上课时段紧急通知二次确认,并修复打包/校验脚本在 Electron 宿主下的多个缺陷
## 客户端灵动岛(动态岛)

- 新增独立透明置顶窗口:桌面中上方常驻、不占任务栏、不抢焦点(268x44 胶囊 / 404x316 详情 / 424x344 紧急)
- 三种形态与动画:隐藏 ↔ 胶囊 ↔ 详情,尺寸用 easeOutCubic 260ms 逐帧缓动、透明度单独淡入淡出
  (尺寸与淡入使用各自动画令牌,避免互相打断导致窗口卡在胶囊尺寸)
- 行为:普通通知变「新消息 · 共 N 条」胶囊,点击展开详情;上课时段自动隐藏并进入待发队列,
  下课后自动弹出;紧急通知无论是否上课立即展开(红色呼吸光晕 + 紧急角标,无需点击)
- 上课状态来自 GET /api/schedules/current(10s 心跳 + 下课时长定时器),
  新增 src/main/island.ts(控制器/IPC)、src/preload/island.ts、src/island/IslandApp.vue、
  src/renderer/island/bridge.ts、island.html 与多页 Vite 构建

## 上课时段发布紧急通知的二次确认

- 服务端:priority=URGENT 且当前处于上课时间段且未带 confirmDuringClass 时返回
  409 URGENT_DURING_CLASS(携带 current/next/week/serverTime),绕过界面调接口同样被拦截
- 新增 GET /api/schedules/current(inClass/current/next,at 参数用于诊断与测试)
- Web 管理端:全屏警告遮罩(UrgentClassWarning.vue),显示正在上的课时段与待发布标题,
  确认按钮带 3 秒倒计时(圆环进度,倒计时结束前禁用),取消即不发;并发命中 409 同样兜底弹窗

## 打包与验证脚本修复(Electron 宿主环境)

- scripts/lib/node-runtime.mjs:定位真正的 node.exe。此前经 pnpm 运行时 process.execPath 指向
  Electron,dist:server 会把 Electron 复制成安装包内的 node.exe,dist:win 会让 electron-builder 报
  Unknown argument: .../cli.js(改为显式使用真实 Node)
- scripts/lib/electron-env.mjs:启动 Electron 前清理继承的 ELECTRON_RUN_AS_NODE(否则 Electron 退化为
  纯 Node,require('electron') 直接 MODULE_NOT_FOUND),并从 path.txt 定位 electron.exe
  (process.versions.electron 存在时 import 'electron' 返回的是 API 对象而非路径)
- scripts/dist-server.mjs:新增构建产物新鲜度校验(曾出现 server 已重建但打进安装包的 shared/dist 是旧的,
  安装后服务启动即报 does not provide an export named MAX_TERM_WEEK)
- scripts/ui-smoke:新增 live-probe.mjs 真实制造/清理上课时段,Electron 驱动真实点击断言全屏警告文案、
  按钮禁用与 3 秒倒计时时长(实测 2966ms),测试后自动删除探针课表与测试通知

## 验证结果

- pnpm verify:e2e 62/62(新增上课时段拦截、二次确认后发布、普通通知不受限等 8 项)
- pnpm verify:web 8/8(新增上课时段紧急通知全屏警告 + 倒计时)
- pnpm verify:desktop 18/18(新增灵动岛窗口/上课隐藏/下课弹出/紧急立即展开/胶囊点击展开 + 截图)
- 打包后 EXE 冒烟 15/15(packaged: true,退出码 0)
- 服务端安装程序升级安装:.env 与数据库保持不变,/api/schedules/current 正常
- pnpm lint / format:check / typecheck 全部通过
2026-09-12 08:41:08 +08:00
LoveChengke 5545ef6f55 feat: 生产化改造(服务端安装程序/PWA/Docker+HTTPS)并修复侧边栏点击无反应、SPA 静态托管 dotfiles 两个缺陷
- 修复 web-admin 与 desktop-client 侧边栏点击无反应:el-menu 的 index 是路径,不能当路由名使用
- 修复 Web 产物目录含点号目录(如 .cache)时 res.sendFile 因 dotfiles:ignore 返回 500
- 后端生产模式:静态托管 Web 端 + SPA 回退、helmet、限流(通用/登录)、trust proxy、
  /healthz 与 /readyz、请求耗时日志、生产环境变量强校验、首启动自动迁移与创建管理员、PID 文件
- Web 管理端 PWA:manifest + service worker + 图标,可在浏览器安装为应用
- 新增服务端 + Web 管理端 Windows 安装程序(内置 Node 运行时、启停与服务脚本、开机自启、卸载保留数据)
- 新增 Docker/Compose/Nginx 部署样例与 docs/production.md
- 新增 UI 回归测试:Web 端 Electron 驱动真实点击 7 项、客户端冒烟 11 项(含侧边栏点击与离线回退)
2026-09-12 00:20:21 +08:00
LoveChengke 6e4c7c1050 feat(desktop-client): 阶段4-6 Electron EXE 客户端(登录/课表/作业/通知/成绩/设置 + Socket.IO 实时 + IndexedDB 离线缓存与自动同步 + electron-builder 打包) 2026-09-11 22:46:31 +08:00
LoveChengke 4566d7cd5e feat(server): 增加根路径服务首页,完善文档中的访问地址说明 2026-09-11 22:01:32 +08:00
LoveChengke 84ab49de10 feat: 班级小助手 阶段1-3(monorepo + 后端 REST/Socket.IO + Web 管理端) 2026-09-11 21:57:38 +08:00