准考证编排
This commit is contained in:
@@ -26,7 +26,7 @@ export function createAdminViews(context) {
|
||||
const meta = {
|
||||
dashboard: ['考务工作台', '掌握当前报名、审核和发布任务。'], candidates: ['考生资料审核', '核验考生实名、学籍与联系信息。'],
|
||||
registrations: ['考试报名审核', '确认考生所报考试、科目与缴费状态。'], exams: ['考试与科目', '创建考试、配置报名时间与考试科目。'],
|
||||
notices: ['通知发布', '发布后立即展示在公开首页和考生中心。'], admit: ['准考证生成', '为已审核报名分配考点、考场与座位。'],
|
||||
notices: ['通知发布', '发布后立即展示在公开首页和考生中心。'], admit: ['准考证编排', '按整场考试预检容量,并批量分配固定考点、分科考场与准考证号。'],
|
||||
results: [state.user.adminLevel === 'super' ? '成绩发布' : '成绩查看', state.user.adminLevel === 'super' ? '录入单科成绩并控制是否对考生公开。' : '按数据范围查看已录入成绩。'],
|
||||
admins: ['分级管理员', '同一级可以配置多名管理员,并分别绑定学校或班级。'],
|
||||
centers: ['考务场所档案', state.user.adminLevel === 'school' ? '查看本校考点与结构化考场,所有变更提交后进入审批。' : '管理各校考点、考场容量与变更审批台账。'],
|
||||
@@ -40,12 +40,12 @@ export function createAdminViews(context) {
|
||||
if (!meta[page] || !allowedPages.includes(page)) page = 'dashboard';
|
||||
app.innerHTML = portalShell('admin', page, loadingPanel(), ...meta[page]);
|
||||
try {
|
||||
const endpoint = page === 'admit' ? 'registrations' : page === 'flows' ? 'workflow-instances' : page === 'flow-design' ? 'workflows' : page === 'account-batches' ? 'candidate-account-batches' : page === 'organization' ? 'school-organization' : page;
|
||||
const endpoint = page === 'admit' ? 'admission-arrangements' : page === 'flows' ? 'workflow-instances' : page === 'flow-design' ? 'workflows' : page === 'account-batches' ? 'candidate-account-batches' : page === 'organization' ? 'school-organization' : page;
|
||||
const data = await api(`/api/admin/${endpoint}`);
|
||||
state.pageData = data;
|
||||
const content = {
|
||||
dashboard: () => adminDashboard(data), candidates: () => adminCandidates(data.candidates), registrations: () => adminRegistrations(data.registrations),
|
||||
exams: () => adminExams(data.exams), notices: () => adminNotices(data.notices), admit: () => adminAdmit(data.registrations), results: () => adminResults(data),
|
||||
exams: () => adminExams(data.exams), notices: () => adminNotices(data.notices), admit: () => adminAdmit(data), results: () => adminResults(data),
|
||||
admins: () => adminUsers(data), centers: () => adminCenters(data), flows: () => adminFlows(data), organization: () => adminSchoolOrganization(data), 'account-batches': () => adminAccountBatches(data),
|
||||
'flow-design': () => adminFlowDesign(data.workflows), 'number-rules': () => adminNumberRules(data)
|
||||
}[page]();
|
||||
@@ -87,9 +87,23 @@ export function createAdminViews(context) {
|
||||
return `<section class="panel data-panel"><div class="data-toolbar"><label class="search-box">${icons.search}<input data-action="table-search" data-target="noticeTable" placeholder="搜索通知标题或分类"></label><p>发布状态会实时同步到公开首页和考生中心。</p></div><div class="table-scroll"><table id="noticeTable"><thead><tr><th>通知标题</th><th>分类</th><th>作者</th><th>发布时间</th><th>展示</th><th>状态</th><th>操作</th></tr></thead><tbody>${notices.map(notice => `<tr data-status="${h(notice.status)}"><td><strong>${h(notice.title)}</strong><small>${h(notice.summary)}</small></td><td>${h(notice.category)}</td><td>${h(notice.author)}</td><td>${formatDate(notice.publishAt || notice.createdAt,true)}</td><td>${notice.pinned ? '<span class="pin-label">首页置顶</span>' : '普通'}</td><td>${badge(notice.status)}</td><td><button class="row-action" data-action="toggle-notice" data-id="${h(notice.id)}" data-status="${notice.status === 'published' ? 'draft' : 'published'}">${notice.status === 'published' ? '撤回' : '发布'}</button></td></tr>`).join('')}</tbody></table></div></section>`;
|
||||
}
|
||||
|
||||
function adminAdmit(registrations) {
|
||||
const approved = registrations.filter(reg => reg.status === 'approved');
|
||||
return `<section class="panel data-panel"><div class="data-toolbar"><label class="search-box">${icons.search}<input data-action="table-search" data-target="admitTable" placeholder="搜索考生、考试、准考证号"></label><p>仅已通过报名审核的考生可生成准考证。</p></div><div class="table-scroll"><table id="admitTable"><thead><tr><th>考生</th><th>考试</th><th>报考科目</th><th>准考证号</th><th>考点 / 考场</th><th>下载时间</th><th>操作</th></tr></thead><tbody>${approved.map(reg => `<tr><td><div class="person-cell"><span>${h((reg.candidate?.name || '?').slice(0,1))}</span><div><strong>${h(reg.candidate?.name)}</strong><small>${h(reg.candidate?.phone)}</small></div></div></td><td><strong>${h(reg.exam.name)}</strong><small>${h(reg.exam.code)}</small></td><td>${reg.subjects.map(subject => h(subject.name)).join('、')}</td><td class="mono">${h(reg.admitCard?.number || '尚未生成')}</td><td>${reg.admitCard ? `${h(reg.admitCard.testCenter)}<small>${h(reg.admitCard.room)} · ${h(reg.admitCard.seat)} 号</small>` : '—'}</td><td>${dateRange(reg.exam.admitDownloadStart, reg.exam.admitDownloadEnd)}</td><td><button class="row-action ${reg.admitCard ? '' : 'primary'}" data-action="generate-admit" data-id="${h(reg.id)}">${reg.admitCard ? '重新查看' : '生成准考证'}</button></td></tr>`).join('') || '<tr><td colspan="7" class="empty-state">暂无已通过的考试报名</td></tr>'}</tbody></table></div></section>`;
|
||||
function adminAdmit(data) {
|
||||
const approved = data.registrations || [];
|
||||
const selectedExam = data.exams.find(exam => exam.approvedCount > 0) || data.exams[0];
|
||||
const selectedPlan = selectedExam?.plan;
|
||||
const scopeName = code => data.mixingScopes.find(item => item.code === code)?.name || code;
|
||||
const form = selectedExam ? `<section class="panel arrangement-config"><div class="panel-title"><div><h2>整场编排控制台</h2><p>先预检容量与档案,再以一个事务生成或替换整套编排。</p></div><span>${data.centers.length} 个启用考点 · ${data.centers.reduce((sum, item) => sum + item.capacity, 0)} 个常规席位</span></div><form data-form="admission-arrangement"><div class="arrangement-fields"><label><span>考试 *</span><select name="examId" required>${data.exams.map(exam => `<option value="${h(exam.id)}" ${exam.id === selectedExam.id ? 'selected' : ''}>${h(exam.name)}(审核通过 ${exam.approvedCount} 人 / 已编排 ${exam.arrangedCount} 人)</option>`).join('')}</select></label><label><span>混编范围 *</span><select name="mixingScope" required>${data.mixingScopes.map(item => `<option value="${h(item.code)}" ${item.code === (selectedPlan?.mixingScope || 'school') ? 'selected' : ''}>${h(item.name)}</option>`).join('')}</select></label><label><span>准考证号规则 *</span><select name="numberRuleId" required>${data.rules.map(rule => `<option value="${h(rule.id)}" ${rule.id === selectedPlan?.numberRuleId ? 'selected' : ''}>${h(rule.name)}</option>`).join('')}</select></label><label><span>稳定编排种子</span><input name="seed" value="${h(selectedPlan?.randomSeed || selectedExam.code)}" maxlength="80"><small>相同数据与种子会得到相同顺序,便于复核。</small></label></div><div class="arrangement-actions"><button type="button" class="ghost-button" data-action="preview-arrangement">仅预检,不写入</button><button type="submit" class="solid-button">${selectedPlan ? '重新编排整场考试' : '生成整场编排'}</button></div></form><div data-arrangement-preview></div></section>` : emptyState('还没有可编排考试', '请先创建考试并完成报名审核。');
|
||||
const ruleCards = `<section class="arrangement-rules">${data.rules.map(rule => `<article><span>号码规则</span><h3>${h(rule.name)}</h3><p>${h(rule.description)}</p><code>${h(rule.example)}</code></article>`).join('')}</section>`;
|
||||
const planCards = data.plans.length ? `<section class="arrangement-plan-grid">${data.plans.map(plan => `<article class="panel"><header><div><span>${h(scopeName(plan.mixingScope))}</span><h3>${h(plan.examName)}</h3></div><time>${formatDate(plan.generatedAt, true)}</time></header><div><strong>${plan.candidateCount}</strong><small>名考生</small><strong>${plan.centerCount}</strong><small>个考点</small><strong>${plan.subjectCombinationCount}</strong><small>种科目组合</small></div><p>${h(plan.ruleName)} · 本校考点率 ${h(plan.sameSchoolCenterRate)}%</p>${plan.warnings.length ? `<ul>${plan.warnings.map(item => `<li>${h(item)}</li>`).join('')}</ul>` : '<p class="arrangement-ok">容量、档案与时间冲突检查通过</p>'}</article>`).join('')}</section>` : '';
|
||||
const rows = approved.map(reg => {
|
||||
const assignments = new Map((reg.admitCard?.assignments || []).map(item => [item.subjectId, item]));
|
||||
const subjects = reg.subjects.map(subject => {
|
||||
const assignment = assignments.get(subject.id);
|
||||
return `<span><b>${h(subject.name)}</b><small>${assignment ? `${h(assignment.roomName || assignment.room)} · ${h(assignment.seat)} 号` : '待编排'}</small></span>`;
|
||||
}).join('');
|
||||
return `<tr><td><div class="person-cell"><span>${h((reg.candidate?.name || '?').slice(0,1))}</span><div><strong>${h(reg.candidate?.name)}</strong><small>${h(reg.candidate?.school || '')} · ${h(reg.candidate?.grade || '')}</small></div></div></td><td><strong>${h(reg.exam.name)}</strong><small>${h(reg.exam.code)}</small></td><td class="mono">${h(reg.admitCard?.number || '待编排')}</td><td>${h(reg.admitCard?.testCenter || '—')}</td><td><div class="subject-room-list">${subjects}</div></td><td>${reg.admitCard ? `<button class="row-action" data-action="generate-admit" data-id="${h(reg.id)}">查看</button>` : '—'}</td></tr>`;
|
||||
}).join('');
|
||||
return `${form}${ruleCards}${planCards}<section class="panel data-panel"><div class="data-toolbar"><label class="search-box">${icons.search}<input data-action="table-search" data-target="admitTable" placeholder="搜索考生、考试、准考证号或考点"></label><p>同一考生所有科目固定在一个考点;考场和座位按科目分别展示。</p></div><div class="table-scroll"><table id="admitTable"><thead><tr><th>考生</th><th>考试</th><th>准考证号</th><th>固定考点</th><th>分科考场 / 座位</th><th>操作</th></tr></thead><tbody>${rows || '<tr><td colspan="6" class="empty-state">暂无已通过的考试报名</td></tr>'}</tbody></table></div></section>`;
|
||||
}
|
||||
|
||||
function adminResults(data) {
|
||||
|
||||
@@ -28,7 +28,7 @@ export function createCandidateViews(context) {
|
||||
if (level === 'class') return [core[0], core[1], core[2], ['flows', '流程中心', 'check'], core[3]];
|
||||
const operations = [['flows', '流程中心', 'check'], ['centers', '考场信息', 'exam']];
|
||||
if (level === 'school') return [core[0], ['organization', '本校组织', 'users'], ['account-batches', '批量建号', 'ticket'], core[1], core[2], ...operations, core[3]];
|
||||
return [core[0], ['admins', '管理员', 'users'], core[1], core[2], ['flows', '流程监督', 'check'], ['flow-design', '流程设计', 'exam'], ['number-rules', '报名号规则', 'ticket'], ['centers', '考场信息', 'exam'], ['exams', '考试与科目', 'exam'], ['notices', '通知发布', 'bell'], ['admit', '准考证生成', 'ticket'], core[3]];
|
||||
return [core[0], ['admins', '管理员', 'users'], core[1], core[2], ['flows', '流程监督', 'check'], ['flow-design', '流程设计', 'exam'], ['number-rules', '报名号规则', 'ticket'], ['centers', '考场信息', 'exam'], ['exams', '考试与科目', 'exam'], ['notices', '通知发布', 'bell'], ['admit', '准考证编排', 'ticket'], core[3]];
|
||||
}
|
||||
|
||||
function portalShell(role, page, content, title, description) {
|
||||
@@ -131,7 +131,16 @@ export function createCandidateViews(context) {
|
||||
|
||||
function candidateAdmit(registrations) {
|
||||
const cards = registrations.filter(reg => reg.admitCard);
|
||||
return cards.length ? `<div class="admit-list">${cards.map(reg => { const now = Date.now(); const open = now >= new Date(reg.exam.admitDownloadStart).getTime() && now <= new Date(reg.exam.admitDownloadEnd).getTime(); return `<article class="admit-ticket"><div class="admit-main"><header><span>${h(reg.exam.code)}</span>${badge(open ? 'open' : now < new Date(reg.exam.admitDownloadStart) ? 'upcoming' : 'closed')}</header><h2>${h(reg.exam.name)}</h2><div class="admit-number"><small>准考证号</small><strong>${h(reg.admitCard.number)}</strong></div><dl><div><dt>考点</dt><dd>${h(reg.admitCard.testCenter)}</dd></div><div><dt>考场 / 座位</dt><dd>${h(reg.admitCard.room)} / ${h(reg.admitCard.seat)}</dd></div><div><dt>下载时间</dt><dd>${dateRange(reg.exam.admitDownloadStart, reg.exam.admitDownloadEnd)}</dd></div></dl></div><div class="admit-stub"><span>ADMISSION<br>CARD</span><i></i><button class="solid-button" data-action="download-admit" data-id="${h(reg.id)}" ${open ? '' : 'disabled'}>${open ? '下载准考证' : now < new Date(reg.exam.admitDownloadStart) ? '尚未开放' : '下载已结束'}</button><small>下载后请使用 A4 纸打印</small></div></article>`; }).join('')}</div>` : emptyState('准考证尚未生成', '考试报名审核通过后,由管理员统一生成准考证。', 'candidate/registrations', '查看报名状态');
|
||||
return cards.length ? `<div class="admit-list">${cards.map(reg => {
|
||||
const now = Date.now();
|
||||
const open = now >= new Date(reg.exam.admitDownloadStart).getTime() && now <= new Date(reg.exam.admitDownloadEnd).getTime();
|
||||
const assignments = new Map((reg.admitCard.assignments || []).map(item => [item.subjectId, item]));
|
||||
const subjectRows = reg.subjects.map(subject => {
|
||||
const assignment = assignments.get(subject.id) || {};
|
||||
return `<span><b>${h(subject.name)}</b><small>${h(subject.date)} ${h(subject.start)} · ${h(assignment.roomName || assignment.room || '待定')} · ${h(assignment.seat || '—')} 号</small></span>`;
|
||||
}).join('');
|
||||
return `<article class="admit-ticket"><div class="admit-main"><header><span>${h(reg.exam.code)}</span>${badge(open ? 'open' : now < new Date(reg.exam.admitDownloadStart) ? 'upcoming' : 'closed')}</header><h2>${h(reg.exam.name)}</h2><div class="admit-number"><small>准考证号</small><strong>${h(reg.admitCard.number)}</strong></div><dl><div><dt>固定考点</dt><dd>${h(reg.admitCard.testCenter)}</dd></div><div><dt>逐科安排</dt><dd class="admit-subject-rooms">${subjectRows}</dd></div><div><dt>下载时间</dt><dd>${dateRange(reg.exam.admitDownloadStart, reg.exam.admitDownloadEnd)}</dd></div></dl></div><div class="admit-stub"><span>ADMISSION<br>CARD</span><i></i><button class="solid-button" data-action="download-admit" data-id="${h(reg.id)}" ${open ? '' : 'disabled'}>${open ? '下载准考证' : now < new Date(reg.exam.admitDownloadStart) ? '尚未开放' : '下载已结束'}</button><small>下载后请使用 A4 纸打印</small></div></article>`;
|
||||
}).join('')}</div>` : emptyState('准考证尚未生成', '考试报名审核通过后,由管理员统一编排准考证。', 'candidate/registrations', '查看报名状态');
|
||||
}
|
||||
|
||||
function candidateResults(data) {
|
||||
|
||||
Reference in New Issue
Block a user