Commit Graph
18 Commits
Author SHA1 Message Date
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 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 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 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 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 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 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 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