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 已重新打包并部署到本机安装目录。
This commit is contained in:
LoveChengke committed 2026-09-13 08:39:11 +08:00
1 parent b8ef5d8dbb
commit 11008928a1
34 files changed
+1421 -340

No files matched your search

+38 -15
View File
@@ -498,6 +498,9 @@ async function main() {
await sleep(300);
// 7.7 ClassIsland 时间配置导入弹窗(班主任可见:粘贴 JSON → 解析预览 → 确认导入)
// 7.7 ClassIsland 课程表导入弹窗(班主任可见:粘贴 JSON → 解析预览 → 单双周识别)
// 注:需求调整后课表页只保留「导入 ClassIsland 课程表」入口("导入时间配置"按钮已下线,
// 时间表由课程表 JSON 里的 TimeLayouts 一起带进来)。
const layoutDialog = await win.webContents.executeJavaScript(`(async () => {
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const menu = Array.from(document.querySelectorAll('.el-menu-item')).find((node) =>
@@ -509,17 +512,18 @@ async function main() {
while (Date.now() < deadline && location.pathname !== '/schedules') await sleep(80);
await sleep(600);
const openButton = Array.from(document.querySelectorAll('button')).find((node) =>
(node.textContent ?? '').trim() === '导入时间配置',
);
if (!openButton) return { ok: false, reason: '课表页没有"导入时间配置"按钮(班主任权限未生效?)' };
const buttons = Array.from(document.querySelectorAll('button'));
const openButton = buttons.find((node) => (node.textContent ?? '').trim() === '导入 ClassIsland 课程表');
if (!openButton) return { ok: false, reason: '课表页没有"导入 ClassIsland 课程表"按钮(班主任权限未生效?)' };
// 旧入口必须已经下线(需求 ⑩)
const legacy = buttons.some((node) => (node.textContent ?? '').trim() === '导入时间配置');
openButton.click();
await sleep(700);
const dialog = Array.from(document.querySelectorAll('.el-dialog')).find((node) =>
(node.querySelector('.el-dialog__title')?.textContent ?? '').includes('ClassIsland'),
);
if (!dialog) return { ok: false, reason: '时间配置导入弹窗未打开' };
if (!dialog) return { ok: false, reason: '课程表导入弹窗未打开' };
const sampleButton = Array.from(dialog.querySelectorAll('button')).find((node) =>
(node.textContent ?? '').trim() === '填入示例',
@@ -534,35 +538,54 @@ async function main() {
if (!previewButton) return { ok: false, reason: '缺少"解析预览"按钮' };
previewButton.click();
const previewDeadline = Date.now() + 8000;
const previewDeadline = Date.now() + 9000;
let rows = 0;
let importEnabled = false;
let errors = 0;
let parityTags = 0;
let content = '';
while (Date.now() < previewDeadline) {
rows = dialog.querySelectorAll('.el-table__body tbody tr').length;
errors = dialog.querySelectorAll('.el-alert--error').length;
const confirm = Array.from(dialog.querySelectorAll('.el-dialog__footer button')).find((node) =>
(node.textContent ?? '').trim() === '确认导入',
const tags = Array.from(dialog.querySelectorAll('.el-tag')).map((node) => (node.textContent ?? '').trim());
parityTags = tags.filter((text) => text === '单周' || text === '双周').length;
content = (dialog.textContent ?? '').replace(/\s+/g, ' ');
const confirm = Array.from(dialog.querySelectorAll('.el-dialog__footer button')).find(
(node) => (node.textContent ?? '').trim() === '开始导入',
);
importEnabled = Boolean(confirm) && !confirm.disabled;
if (rows >= 2) break;
await sleep(150);
}
return { ok: true, rows, importEnabled, errors, hasFileInput: Boolean(dialog.querySelector('input[type=file]')) };
return {
ok: true,
rows,
importEnabled,
errors,
parityTags,
legacyGone: !legacy,
hasFileInput: Boolean(dialog.querySelector('input[type=file]')) || content.includes('选择 .json 文件'),
};
})()`);
record(
'ClassIsland 时间配置导入弹窗(示例 → 解析预览 → 可导入)',
'ClassIsland 课程表导入弹窗(示例 → 解析预览 → 识别单双周,旧"导入时间配置"入口已下线)',
Boolean(layoutDialog?.ok) &&
(layoutDialog?.rows ?? 0) >= 2 &&
layoutDialog?.importEnabled === true &&
layoutDialog?.errors === 0,
`节次行=${layoutDialog?.rows ?? 0} 可导入=${layoutDialog?.importEnabled} 校验错误=${layoutDialog?.errors ?? '-'} 文件选择=${layoutDialog?.hasFileInput}` +
layoutDialog?.errors === 0 &&
(layoutDialog?.parityTags ?? 0) >= 1 &&
layoutDialog?.legacyGone === true,
`解析行=${layoutDialog?.rows ?? 0} 可导入=${layoutDialog?.importEnabled} 校验错误=${layoutDialog?.errors ?? '-'} ` +
`单双周标签=${layoutDialog?.parityTags ?? 0} 旧入口已下线=${layoutDialog?.legacyGone} 文件选择=${layoutDialog?.hasFileInput}` +
`${layoutDialog?.reason ? ` 原因=${layoutDialog.reason}` : ''}`,
);
// 关闭导入弹窗(用标题栏关闭按钮,兼容不同对话框的底部按钮文案)
await win.webContents.executeJavaScript(`(() => {
const buttons = Array.from(document.querySelectorAll('.el-dialog__footer button'));
const close = buttons.find((node) => (node.textContent ?? '').trim() === '关闭');
if (close) close.click();
const dialog = Array.from(document.querySelectorAll('.el-dialog')).find((node) =>
(node.querySelector('.el-dialog__title')?.textContent ?? '').includes('ClassIsland'),
);
const closeBtn = dialog?.querySelector('.el-dialog__headerbtn');
if (closeBtn) closeBtn.click();
return true;
})()`);
await sleep(300);