全站大数据台账补齐跨页搜索、筛选和分页,包括投档监督、报到审批、成绩复议、公开录取名单等。

后台重构为一级业务域,并将招生录取拆为 5 个二级菜单。
修复成绩等级排名遮挡科目。
成绩单、录取通知书均支持带防伪码和二维码的 PDF 下载。
录取通知书移除“录取专用章”,支持学校自定义模板。
通知书编号采用“学校代码 + 考试代码 + 学校独立流水号”。
完成招生学校报到台账:Y/N/P 状态、暂存、Excel 导入导出、二维码扫描确认。
完成报到提交、计划完成率、补录决定、超级管理员审批和自动公开闭环。
自动公告现已进入公开首页、考生通知及招生学校工作台。
验证结果:
完整 npm test 全部通过。
浏览器端三个角色工作台验收通过。
Excel 模板已实际导出并渲染验证。
两类 PDF 已实际生成、转图检查,二维码、编号和无印章版式正常。
This commit is contained in:
2026-07-22 09:50:52 +08:00 Unverified
parent d029931c31
commit 3e1bced990
22 changed files with 813 additions and 94 deletions
+77 -2
View File
@@ -345,13 +345,13 @@ document.addEventListener('click', async event => {
const results = (state.pageData?.results || []).filter(item => item.examId === examId);
const summary = (state.pageData?.summaries || []).find(item => item.examId === examId);
if (!results.length || !summary?.verificationCode) return toast('成绩单暂不可下载', '请刷新页面后重试');
downloadScoreReport({ organization: state.publicData.organization, candidate: state.pageData.candidate || { name: state.user.displayName, candidateNumber: state.user.candidateNumber }, exam: { id: examId, name: results[0].examName, code: results[0].examCode }, results, summary: { ...summary, publishedAt: [...results].sort((a,b) => new Date(b.publishedAt) - new Date(a.publishedAt))[0]?.publishedAt }, verificationCode: summary.verificationCode, verificationUrl: `${location.origin}/#verify/${summary.verificationCode}` });
await downloadScoreReport({ organization: state.publicData.organization, candidate: state.pageData.candidate || { name: state.user.displayName, candidateNumber: state.user.candidateNumber }, exam: { id: examId, name: results[0].examName, code: results[0].examCode }, results, summary: { ...summary, publishedAt: [...results].sort((a,b) => new Date(b.publishedAt) - new Date(a.publishedAt))[0]?.publishedAt }, verificationCode: summary.verificationCode, verificationQr: summary.verificationQr, verificationUrl: `${location.origin}/#verify/${summary.verificationCode}` });
return toast('PDF 成绩单已生成', '文件包含防伪查询码');
}
if (action === 'download-admission-notice') {
const item = (state.pageData?.admissions || []).find(entry => entry.examId === target.dataset.examId);
if (!item?.placement || item.placement.status !== 'final' || !item.noticeVerificationCode) return toast('录取通知书暂不可下载', '只有正式录取后才能生成');
downloadAdmissionNotice({ organization: state.publicData.organization, candidate: { name: state.profile?.name || state.user.displayName }, exam: item.exam, placement: item.placement, school: item.placementSchool || { name: item.placement.schoolName || '招生学校' }, template: item.noticeTemplate || {}, verificationCode: item.noticeVerificationCode, verificationUrl: `${location.origin}/#verify/${item.noticeVerificationCode}` });
await downloadAdmissionNotice({ organization: state.publicData.organization, candidate: { name: state.profile?.name || state.user.displayName }, exam: item.exam, placement: item.placement, school: item.placementSchool || { name: item.placement.schoolName || '招生学校' }, template: item.noticeTemplate || {}, verificationCode: item.noticeVerificationCode, verificationQr: item.noticeVerificationQr, noticeNumber: item.noticeNumber, verificationUrl: `${location.origin}/#verify/${item.noticeVerificationCode}` });
return toast('录取通知书已生成', '请核对学校和录取类别');
}
if (action === 'download-admitted-candidates') {
@@ -405,6 +405,30 @@ document.addEventListener('click', async event => {
updateQualificationSelection(container);
return;
}
if (action === 'download-admission-reporting') {
window.location.href = `/api/admission/reporting/export?examId=${encodeURIComponent(target.dataset.examId)}`;
return;
}
if (action === 'submit-admission-reporting') {
if (!window.confirm('确认提交本轮全部报到情况?提交后需先完成补录决定,才能交由超级管理员审批。')) return;
await api('/api/admission/reporting/submit', { method: 'POST', body: { examId: target.dataset.examId } });
toast('报到情况已提交', '请根据计划完成率确认是否申请补录');
return renderRoute();
}
if (action === 'review-admission-reporting') {
const approved = target.dataset.approved === 'true';
const supplement = target.dataset.supplement === 'true';
const approvalNote = window.prompt(approved ? '请输入审批意见(可选)' : '请输入退回原因', '') ?? '';
if (!approved && approvalNote.trim().length < 2) return toast('需要退回原因', '请说明招生学校应修改的内容');
let preferenceEnd = '';
if (approved && supplement) {
preferenceEnd = window.prompt('请输入补录志愿截止时间(例如 2026-07-30T18:00', '') ?? '';
if (!preferenceEnd) return;
}
await api(`/api/admin/admission-reporting/${encodeURIComponent(target.dataset.id)}`, { method: 'PATCH', body: { approved, approvalNote, preferenceEnd } });
toast(approved ? '审批完成并自动公开' : '已退回招生学校', approved ? (supplement ? '全部学校审批完成后将自动开启补录' : '报到统计已进入公开通知') : '招生学校可修改暂存数据后重新提交');
return renderRoute();
}
if (action === 'clear-table-filters') {
const tableId = target.dataset.target;
state.tableFilters[tableId] = { query: '', status: 'all', filters: {} };
@@ -1014,6 +1038,24 @@ document.addEventListener('submit', async event => {
} else if (kind === 'admission-notice-template') {
await api('/api/admission/notice-template', { method: 'PUT', body: formObject(form) });
toast('录取通知书模板已保存', '正式录取考生将使用该模板生成 PDF'); renderRoute();
} else if (kind === 'admission-reporting-draft') {
const rows = [...form.querySelectorAll('[data-reporting-status]')].map(select => ({
placementId: select.dataset.placementId,
status: select.value,
note: form.querySelector(`[data-reporting-note][data-placement-id="${CSS.escape(select.dataset.placementId)}"]`)?.value || ''
}));
if (!rows.length) return toast('当前页没有可暂存记录');
await api('/api/admission/reporting/draft', { method: 'PUT', body: { examId: form.dataset.examId, rows } });
toast('当前页已暂存', `${rows.length} 名考生的状态已保存,尚未正式提交`); renderRoute();
} else if (kind === 'admission-reporting-scan') {
const body = formObject(form);
const result = await api('/api/admission/reporting/scan', { method: 'POST', body });
toast('防伪二维码核验通过', `${result.row.name}已暂存为“已报到”`); renderRoute();
} else if (kind === 'admission-reporting-decision') {
const body = formObject(form);
body.supplement = body.supplement === 'true';
await api('/api/admission/reporting/decision', { method: 'POST', body });
toast('学校决定已提交', '等待超级管理员审批;审批后报到统计将自动公开'); renderRoute();
} else if (kind === 'admission-plan' || kind === 'school-admission-plan') {
const body = formObject(form);
body.categories = [...form.querySelectorAll('.admission-category-editor')].map((editor, index) => {
@@ -1422,6 +1464,39 @@ window.addEventListener('keydown', event => {
form.requestSubmit(form.querySelector('[data-result-mode="draft"]'));
}
}
if (event.target.matches('[data-admission-reporting-file]')) {
const input = event.target;
const file = input.files?.[0];
if (!file) return;
const examId = input.dataset.examId;
input.value = '';
(async () => {
try {
toast('正在导入报到状态', `${file.name} · 导入结果只会暂存`);
const result = await api(`/api/admission/reporting/import?examId=${encodeURIComponent(examId)}`, { method: 'POST', headers: { 'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }, body: await file.arrayBuffer() });
toast('Excel 已导入并暂存', `已更新 ${result.count} 名考生,提交前仍可修改`);
renderRoute();
} catch (error) { toast('Excel 导入失败', error.message); }
})();
}
if (event.target.matches('[data-reporting-qr-file]')) {
const input = event.target;
const file = input.files?.[0];
if (!file) return;
input.value = '';
(async () => {
try {
if (!('BarcodeDetector' in window)) throw new Error('当前浏览器不支持图片二维码识别,请粘贴二维码中的核验链接');
const detector = new BarcodeDetector({ formats: ['qr_code'] });
const codes = await detector.detect(file);
const code = codes[0]?.rawValue || '';
if (!code) throw new Error('图片中没有识别到二维码');
const result = await api('/api/admission/reporting/scan', { method: 'POST', body: { code } });
toast('二维码核验通过', `${result.row.name}已暂存为“已报到”`);
renderRoute();
} catch (error) { toast('二维码识别失败', error.message); }
})();
}
});
try {