feat: 完善学生生命周期管理与权限范围并发布 1.4.2

This commit is contained in:
LoveChengke committed 2026-10-23 22:55:21 +08:00
1 parent 6347d1e5aa
commit 24e8dd902b
92 files changed
+6801 -2255

No files matched your search

+98 -42
View File
@@ -36,6 +36,9 @@ const RESULT_FILE = process.env.UI_SMOKE_RESULT ?? '';
*/
const MENU_ITEMS = [
{ label: '仪表盘', path: '/dashboard' },
{ label: '班级管理', path: '/classes' },
{ label: '学生管理', path: '/students' },
{ label: '学期周次', path: '/term-weeks' },
{ label: '课表档案', path: '/plan-editor' },
{ label: '作业发布', path: '/homeworks' },
{ label: '通知发布', path: '/notifications' },
@@ -46,7 +49,7 @@ const MENU_ITEMS = [
];
/** 教师端必须隐藏的入口(前端隐藏 + 后端 403,双重保障) */
const HIDDEN_MENU_LABELS = ['班级管理', '学生管理', '教师管理', '数据库管理'];
const HIDDEN_MENU_LABELS = ['教师管理', '数据库管理'];
/** 管理员账号(教师录入用例):与种子/安装初始化账号一致 */
const ADMIN_USERNAME = process.env.UI_SMOKE_ADMIN ?? 'admin';
@@ -558,10 +561,10 @@ async function main() {
// 等视图渲染
const viewDeadline = Date.now() + 3000;
while (Date.now() < viewDeadline) {
if (document.querySelector('.page-title')) break;
if (document.querySelector('.page-title, .archive-titlebar > span')) break;
await new Promise((resolve) => setTimeout(resolve, 60));
}
const title = (document.querySelector('.page-title')?.textContent ?? '').trim();
const title = (document.querySelector('.page-title, .archive-titlebar > span')?.textContent ?? '').trim();
return { ok: location.pathname === expected && Boolean(title), path: location.pathname, title, expected };
})()`);
@@ -796,7 +799,7 @@ async function main() {
return { labels, leaked };
})()`);
record(
'教师端隐藏无权入口(班级管理/学生管理/教师管理)',
'教师端隐藏未授权教师管理与数据库入口',
(hiddenCheck?.leaked ?? ['?']).length === 0,
`可见菜单=[${(hiddenCheck?.labels ?? []).join(',')}] 越权入口=[${(hiddenCheck?.leaked ?? []).join(',')}]`,
);
@@ -1013,7 +1016,10 @@ async function main() {
await sleep(600);
const buttons = Array.from(document.querySelectorAll('button'));
const openButton = buttons.find((node) => (node.textContent ?? '').trim() === '导入档案');
const transfer = buttons.find((node) => (node.textContent ?? '').trim() === '导入/导出');
transfer?.click();
await sleep(200);
const openButton = Array.from(document.querySelectorAll('button')).find((node) => (node.textContent ?? '').trim() === '导入 ClassIsland 档案');
if (!openButton) return { ok: false, reason: '课表页没有"导入 ClassIsland 课程表"按钮(班主任权限未生效?)' };
// 旧入口必须已经下线(需求 ⑩)
const legacy = buttons.some((node) => (node.textContent ?? '').trim() === '导入时间配置');
@@ -1357,57 +1363,107 @@ async function runPlanEditorChecks(win) {
await win.loadURL(new URL('/plan-editor', TARGET_URL).href);
const ready = await waitFor(
win,
`document.body.innerText.includes('课表编辑') && Boolean(document.querySelector('.course-row'))`,
);
record('统一课表档案工作区与课程时段编辑可用', ready);
const save = await win.webContents.executeJavaScript(
`(() => {const b=[...document.querySelectorAll('button')].find(e=>e.textContent.trim()==='保存课表');b?.click();return Boolean(b&&!b.disabled);})()`,
);
const saved = save && (await waitFor(win, `document.body.innerText.includes('整张日课表已保存')`));
record('日课表整表保存可以从页面提交', saved);
await win.webContents.executeJavaScript(
`(() => {[...document.querySelectorAll('.editor-nav button')].find(e=>e.textContent.trim()==='时间表')?.click();})()`,
`Boolean(document.querySelector('.schedule-grid thead')) && document.querySelectorAll('.schedule-grid thead th').length === 8`,
);
record(
'时间表页四种时间点与管理员保存入口可见',
'ClassIsland 风格档案工作区默认展示周课表和选科侧栏',
ready &&
(await win.webContents.executeJavaScript(
`Boolean(document.querySelector('.subject-palette')) && document.querySelectorAll('.archive-tabs button').length === 4`,
)),
);
const assignment = await win.webContents.executeJavaScript(`(() => {
const cell = document.querySelector('.lesson-cell');
if (!cell) return null;
const original = cell.querySelector('strong')?.textContent.trim();
cell.click();
const button = [...document.querySelectorAll('.subject-palette button')].find(b => !b.disabled && b.title !== original);
button?.click();
return { original, editable: Boolean(button) };
})()`);
const dirty =
assignment?.editable && (await waitFor(win, `document.body.innerText.includes('有未保存的修改')`));
record('点击周课表并从右侧选科生成可保存的课程草稿', dirty);
await win.webContents.executeJavaScript(
`(() => {[...document.querySelectorAll('.archive-commands button')].find(b => b.textContent.trim() === '保存')?.click();})()`,
);
record(
'课表草稿可通过现有接口保存',
dirty &&
(await waitFor(
win,
`document.body.innerText.includes('档案已保存') && !document.querySelector('.draft-label')`,
)),
);
// 恢复隔离验收班级的原科目。
if (assignment?.original) {
await win.webContents.executeJavaScript(`(() => {
document.querySelector('.lesson-cell')?.click();
const original = ${JSON.stringify(assignment.original)};
[...document.querySelectorAll('.subject-palette button')].find(b => b.title === original)?.click();
})()`);
await sleep(150);
await win.webContents.executeJavaScript(
`(() => {[...document.querySelectorAll('.archive-commands button')].find(b => b.textContent.trim() === '保存')?.click();})()`,
);
await waitFor(win, `!document.querySelector('.draft-label')`);
}
await win.webContents.executeJavaScript(`document.querySelector('[aria-label="列表视图"]').click()`);
record(
'列表视图保留课时编辑和批量操作',
await waitFor(
win,
`document.body.innerText.includes('新建课时') && document.body.innerText.includes('保存时间表')`,
`Boolean(document.querySelector('.course-row')) && document.body.innerText.includes('批量排课')`,
),
);
await win.webContents.executeJavaScript(
`document.querySelector('.archive-tabs button:nth-child(2)').click()`,
);
record(
'时间表页四种时间点与保存入口可见',
await waitFor(
win,
`document.body.innerText.includes('新建课时') && document.body.innerText.includes('保存时间表') && document.body.innerText.includes('分割线') && document.body.innerText.includes('行动')`,
),
);
const quickAdd = await win.webContents.executeJavaScript(`(async () => {
const rows = () => document.querySelectorAll('.time-row');
const before = rows().length;
[...document.querySelectorAll('button')].find(b=>b.textContent.trim()==='+ 新建课时')?.click();
await new Promise((resolve) => setTimeout(resolve, 180));
const after = rows().length;
const last = rows()[after - 1];
const values = last ? [...last.querySelectorAll('.el-date-editor input')].map((input) => input.value) : [];
const [start, end] = values;
const diff = (() => {
if (!/^\\d{2}:\\d{2}$/.test(start ?? '') || !/^\\d{2}:\\d{2}$/.test(end ?? '')) return -1;
const [h1, m1] = start.split(':').map(Number), [h2, m2] = end.split(':').map(Number);
return h2 * 60 + m2 - (h1 * 60 + m1);
})();
return { before, after, start, end, diff };
const before = document.querySelectorAll('.time-row').length;
[...document.querySelectorAll('button')].find(b => b.textContent.trim() === '+ 新建课时')?.click();
await new Promise(resolve => setTimeout(resolve, 180));
const rows = document.querySelectorAll('.time-row');
const values = [...(rows[rows.length - 1]?.querySelectorAll('.el-date-editor input') || [])].map(input => input.value);
const minutes = (value) => { const [h, m] = (value || '').split(':').map(Number); return h * 60 + m; };
return { before, after: rows.length, diff: minutes(values[1]) - minutes(values[0]) };
})()`);
record(
'时间表新建课时按快速时长生成起止时间',
quickAdd.after === quickAdd.before + 1 && quickAdd.diff === 45,
`行数 ${quickAdd.before}→${quickAdd.after} 时间=${quickAdd.start ?? '-'}–${quickAdd.end ?? '-'} 分钟=${quickAdd.diff}`,
);
await win.webContents.executeJavaScript(
`document.querySelector('.editor-nav button:nth-child(5)').click()`,
`(() => {[...document.querySelectorAll('.archive-toolbar button')].find(b => b.textContent.trim() === '刷新')?.click();})()`,
);
await waitFor(win, `Boolean(document.querySelector('.el-message-box'))`);
await win.webContents.executeJavaScript(
`(() => {[...document.querySelectorAll('.el-message-box button')].find(b => b.textContent.trim() === '放弃修改')?.click();})()`,
);
await waitFor(win, `!document.querySelector('.draft-label')`);
await win.webContents.executeJavaScript(
`document.querySelector('.archive-tabs button:first-child').click()`,
);
await win.webContents.executeJavaScript(`document.querySelector('[aria-label="月视图"]').click()`);
record(
'整周预览与当前生效课表保持一致',
await waitFor(
win,
`document.body.innerText.includes('整周课表预览') && document.querySelector('.week-grid .el-table__body')`,
),
'月视图提供日期导航',
await waitFor(win, `document.querySelectorAll('.calendar-grid button').length === 42`),
);
await win.webContents.executeJavaScript(`document.querySelector('.editor-nav button:first-child').click()`);
await sleep(300);
await win.webContents.executeJavaScript(`document.querySelector('[aria-label="周视图"]').click()`);
record('底部视图切换可返回周课表', await waitFor(win, `Boolean(document.querySelector('.schedule-grid'))`));
fs.mkdirSync(path.join(__dirname, '../../docs/screenshots'), { recursive: true });
fs.writeFileSync(
path.join(__dirname, '../../docs/screenshots/plan-editor-live-api.png'),
(await win.webContents.capturePage()).toPNG(),
);
await win.webContents.executeJavaScript(`document.querySelector('.return-button').click()`);
await waitFor(win, `location.pathname === '/dashboard'`);
const collapsedBefore = await win.webContents.executeJavaScript(
`Boolean(document.querySelector('.layout-aside.is-collapsed'))`,
);
@@ -1425,7 +1481,7 @@ async function runPlanEditorChecks(win) {
await sleep(700);
fs.mkdirSync(path.join(__dirname, '../../docs/screenshots'), { recursive: true });
fs.writeFileSync(
path.join(__dirname, '../../docs/screenshots/plan-editor-workspace.png'),
path.join(__dirname, '../../docs/screenshots/plan-editor-shell.png'),
(await win.webContents.capturePage()).toPNG(),
);
await win.loadURL(new URL('/system-settings', TARGET_URL).href);
@@ -1441,7 +1497,7 @@ async function runPlanEditorChecks(win) {
'学年升级设置与待补跑提示可见且不执行升级',
await waitFor(
win,
`document.body.innerText.includes('每年自动学年升级') && document.body.innerText.includes('总周数')`,
`document.body.innerText.includes('学年升级计划') && document.body.innerText.includes('总周数')`,
),
);
await win.loadURL(new URL('/archives', TARGET_URL).href);