feat(export): 多考试对比导出(横向交叉表)
新增 export_multi 接口与多考试选择弹窗: - 第一行显示考试名称,前两列固定为学号/姓名,默认按学号排序 - 每场考试成绩可选「最新成绩」或「最高成绩」(latest 跳过 temp_save 草稿,highest 取跨全部尝试最高分) - 学生取并集(任一考试有提交即纳入),未考的考试单元格留空 - 纯逻辑抽离到 lib/multi_export.php,新增 26 项回归测试(tests/test-multi-export.php)
This commit is contained in:
@@ -1373,6 +1373,79 @@
|
||||
showAlert(exportTip('缺考名单', cls, grp), 'info');
|
||||
});
|
||||
|
||||
/* ---------------- 多考试对比导出 ---------------- */
|
||||
// 仅列出真正的"考试"(排除文件夹型项目,folder 或被其他项目作为父级引用)
|
||||
function nonFolderProjects() {
|
||||
const isFolder = p => (p.mode || '').toLowerCase().includes('folder');
|
||||
const parentIds = new Set();
|
||||
state.projects.forEach(p => {
|
||||
const pid = (p.parent_id || '0');
|
||||
if (pid && pid !== '0' && pid !== '') parentIds.add(pid);
|
||||
});
|
||||
const folderSet = new Set();
|
||||
state.projects.forEach(p => { if (isFolder(p) || parentIds.has(p.id)) folderSet.add(p.id); });
|
||||
return state.projects.filter(p => !folderSet.has(p.id));
|
||||
}
|
||||
function renderMultiExportModal() {
|
||||
const body = $('#meBody');
|
||||
const list = nonFolderProjects();
|
||||
body.innerHTML = '';
|
||||
if (!list.length) {
|
||||
body.innerHTML = '<tr><td colspan="3" class="empty">(暂无可导出的考试,请先在右上角刷新问卷列表)</td></tr>';
|
||||
} else {
|
||||
const cur = $('#projectSel').value;
|
||||
list.forEach((p, i) => {
|
||||
const tr = document.createElement('tr');
|
||||
const checked = (p.id === cur) ? ' checked' : '';
|
||||
tr.innerHTML =
|
||||
'<td><input type="checkbox" class="me-chk" value="' + esc(p.id) + '"' + checked + '></td>' +
|
||||
'<td>' + (i + 1) + '</td>' +
|
||||
'<td>' + esc(p.name) + '</td>';
|
||||
body.appendChild(tr);
|
||||
});
|
||||
}
|
||||
updMeMeta();
|
||||
}
|
||||
function updMeMeta() {
|
||||
const n = $$('#meBody .me-chk').filter(c => c.checked).length;
|
||||
$('#meMeta').textContent = '已选 ' + n + ' 场';
|
||||
const all = $$('#meBody .me-chk');
|
||||
$('#meCheckAll').checked = all.length > 0 && all.every(c => c.checked);
|
||||
}
|
||||
$('#btnExportMulti').addEventListener('click', () => {
|
||||
if (!state.projects.length) return showAlert('请先刷新问卷列表');
|
||||
renderMultiExportModal();
|
||||
$('#multiExportModal').classList.add('open');
|
||||
});
|
||||
$('#meCheckAll').addEventListener('change', () => {
|
||||
$$('#meBody .me-chk').forEach(c => { c.checked = $('#meCheckAll').checked; });
|
||||
updMeMeta();
|
||||
});
|
||||
$('#btnMeSelAll').addEventListener('click', () => {
|
||||
$$('#meBody .me-chk').forEach(c => { c.checked = true; });
|
||||
$('#meCheckAll').checked = true;
|
||||
updMeMeta();
|
||||
});
|
||||
$('#btnMeClear').addEventListener('click', () => {
|
||||
$$('#meBody .me-chk').forEach(c => { c.checked = false; });
|
||||
$('#meCheckAll').checked = false;
|
||||
updMeMeta();
|
||||
});
|
||||
$('#meBody').addEventListener('change', e => {
|
||||
if (e.target && e.target.classList.contains('me-chk')) updMeMeta();
|
||||
});
|
||||
$('#btnMeExport').addEventListener('click', () => {
|
||||
const ids = $$('#meBody .me-chk').filter(c => c.checked).map(c => c.value);
|
||||
if (!ids.length) return showAlert('请至少勾选一场考试');
|
||||
const scoreMode = (document.querySelector('input[name=meScoreMode]:checked') || {}).value || 'latest';
|
||||
const qs = 'api.php?action=export_multi&projectIds=' + encodeURIComponent(ids.join(',')) +
|
||||
'&nameQid=' + encodeURIComponent($('#nameFieldSel').value) +
|
||||
'&scoreMode=' + encodeURIComponent(scoreMode) + '&sort=sid';
|
||||
$('#multiExportModal').classList.remove('open');
|
||||
location.href = qs;
|
||||
showAlert('正在导出 ' + ids.length + ' 场考试成绩对比(' + (scoreMode === 'highest' ? '最高成绩' : '最新成绩') + ',按学号排序)', 'info');
|
||||
});
|
||||
|
||||
/* 弹窗关闭:只关最上层那一个(新增学生等弹窗是叠在名单管理之上的,
|
||||
关全部会连带把底层弹窗一起关掉,不符合预期) */
|
||||
function closeTopModal() {
|
||||
|
||||
@@ -431,6 +431,9 @@ table.grid tbody tr.new.clickable:hover td { background: #d6f2e2; }
|
||||
/* 弹窗内的表格不做吸顶:弹窗自身是滚动容器,吸顶表头会与内容错层(且贴边观感差) */
|
||||
.modal-body table.grid th { position: static; }
|
||||
.modal-body table.grid { box-shadow: inset 0 0 0 1px var(--border-2); border-radius: 8px; }
|
||||
/* 多考试对比导出:顶部工具条 */
|
||||
.me-bar { display: flex; align-items: center; gap: 16px; margin: 0 0 10px; }
|
||||
.me-bar .chk { margin: 0; white-space: nowrap; }
|
||||
.modal-foot {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
padding: 12px 18px; border-top: 1px solid var(--border); background: #fbfcfe;
|
||||
|
||||
Reference in New Issue
Block a user