Add independent subject pass rules and atomic score imports
This commit is contained in:
@@ -177,6 +177,7 @@ document.addEventListener('click', async event => {
|
||||
const query = new URLSearchParams();
|
||||
if (target.dataset.template === '1') query.set('template', '1');
|
||||
if (target.dataset.batchId) query.set('batchId', target.dataset.batchId);
|
||||
if (target.dataset.examId) query.set('examId', target.dataset.examId);
|
||||
window.location.href = `/api/admin/excel/${target.dataset.resource}?${query}`;
|
||||
return;
|
||||
}
|
||||
@@ -184,6 +185,14 @@ document.addEventListener('click', async event => {
|
||||
document.querySelector(`[data-excel-file="${target.dataset.resource}"]`)?.click();
|
||||
return;
|
||||
}
|
||||
if (action === 'result-exam-filter') {
|
||||
state.resultExamFilter = target.dataset.id;
|
||||
return renderRoute();
|
||||
}
|
||||
if (action === 'cancel-result-import') {
|
||||
state.resultImportPreview = null;
|
||||
return renderRoute();
|
||||
}
|
||||
if (action === 'generate-admit') {
|
||||
const registration = state.pageData.registrations.find(item => item.id === target.dataset.id);
|
||||
if (registration.admitCard) return openAdmitPreview(registration);
|
||||
@@ -215,7 +224,7 @@ document.addEventListener('click', async event => {
|
||||
}
|
||||
if (action === 'status-filter') {
|
||||
target.parentElement.querySelectorAll('button').forEach(button => button.classList.toggle('active', button === target));
|
||||
document.querySelectorAll(`#${target.dataset.target} tbody tr`).forEach(row => row.hidden = target.dataset.status !== 'all' && row.dataset.status !== target.dataset.status);
|
||||
document.querySelectorAll(`#${target.dataset.target} tbody tr`).forEach(row => row.hidden = target.dataset.status !== 'all' && !String(row.dataset.status || '').split(/\s+/).includes(target.dataset.status));
|
||||
}
|
||||
} catch (error) { toast('操作未完成', error.message); }
|
||||
});
|
||||
@@ -246,11 +255,15 @@ document.addEventListener('change', event => {
|
||||
input.value = '';
|
||||
(async () => {
|
||||
try {
|
||||
toast('正在导入 Excel', `${file.name} · 逐行校验并写入,错误会标出具体行`);
|
||||
toast(resource === 'results' ? '正在生成导入预览' : '正在导入 Excel', resource === 'results' ? `${file.name} · 当前不会写入数据库` : `${file.name} · 逐行校验并写入,错误会标出具体行`);
|
||||
const result = await api(`/api/admin/excel/${resource}`, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }, body: await file.arrayBuffer()
|
||||
});
|
||||
if (resource === 'account_quotas') {
|
||||
if (resource === 'results') {
|
||||
state.resultImportPreview = { ...result, fileName: file.name };
|
||||
toast(result.summary.invalid ? '预览完成,发现错误' : '预览校验通过', `${result.summary.valid}/${result.summary.total} 行可提交,数据库尚未修改`);
|
||||
renderRoute();
|
||||
} else if (resource === 'account_quotas') {
|
||||
document.querySelectorAll('[data-class-id]').forEach(field => { field.value = '0'; });
|
||||
result.quotas.forEach(item => { const field = document.querySelector(`[data-class-id="${item.classId}"]`); if (field) field.value = item.count; });
|
||||
toast('班级配额已填入', `已读取 ${result.quotas.length} 个班级,请核对后提交审批`);
|
||||
@@ -275,25 +288,44 @@ document.addEventListener('change', event => {
|
||||
form?.querySelector('[data-admin-school]')?.classList.toggle('hidden', event.target.value === 'super');
|
||||
form?.querySelector('[data-admin-class]')?.classList.toggle('hidden', event.target.value !== 'class');
|
||||
}
|
||||
if (event.target.matches('[data-action="result-registration"]')) {
|
||||
const option = event.target.selectedOptions[0];
|
||||
const select = document.querySelector('#resultSubject');
|
||||
const subjects = option?.dataset.subjects ? JSON.parse(option.dataset.subjects) : [];
|
||||
select.innerHTML = `<option value="">请选择科目</option>${subjects.map(subject => `<option value="${h(subject.id)}" data-full-score="${h(subject.fullScore)}" data-pass-score="${h(subject.passScore)}">${h(subject.name)} · 满分 ${h(subject.fullScore)}</option>`).join('')}`;
|
||||
select.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
if (event.target.matches('[data-action="result-exam"]')) {
|
||||
const form = event.target.closest('form');
|
||||
const exam = state.pageData?.exams?.find(item => item.id === event.target.value);
|
||||
const subjectSelect = form?.querySelector('[name="subjectId"]');
|
||||
const candidateSelect = form?.querySelector('[name="registrationId"]');
|
||||
if (subjectSelect) subjectSelect.innerHTML = `<option value="">请选择科目</option>${(exam?.subjects || []).map(subject => `<option value="${h(subject.id)}" data-full-score="${h(subject.fullScore)}" data-pass-score="${h(subject.passScore ?? '')}" data-pass-text="${h(subject.passRule === 'none' ? '不设单科线' : subject.passRule === 'rank_percent' ? `本科排名前 ${subject.passValue}%` : `固定 ${subject.passScore} 分`)}">${h(subject.name)} · 满分 ${h(subject.fullScore)}</option>`).join('')}`;
|
||||
if (candidateSelect) { candidateSelect.innerHTML = '<option value="">请先选择科目</option>'; candidateSelect.disabled = true; }
|
||||
subjectSelect?.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
if (event.target.matches('[data-action="result-subject"]')) {
|
||||
const form = event.target.closest('form');
|
||||
const option = event.target.selectedOptions[0];
|
||||
const examId = form?.querySelector('[name="examId"]')?.value;
|
||||
const subjectId = event.target.value;
|
||||
const fullScore = Number(option?.dataset.fullScore || 0);
|
||||
const passScore = Number(option?.dataset.passScore || 0);
|
||||
const passText = option?.dataset.passText || '';
|
||||
const scoreInput = form?.querySelector('[name="score"]');
|
||||
if (scoreInput) scoreInput.max = fullScore || '';
|
||||
const candidateSelect = form?.querySelector('[name="registrationId"]');
|
||||
const registrations = (state.pageData?.registrations || []).filter(item => item.examId === examId && item.subjectIds.includes(subjectId));
|
||||
if (candidateSelect) {
|
||||
candidateSelect.disabled = !subjectId;
|
||||
candidateSelect.innerHTML = `<option value="">${subjectId ? '请选择考生' : '请先选择科目'}</option>${registrations.map(item => `<option value="${h(item.id)}">${h(item.candidateName)} · ${h(item.candidateNumber)} · ${h(item.className)}</option>`).join('')}`;
|
||||
}
|
||||
const label = form?.querySelector('[data-score-label]');
|
||||
const hint = form?.querySelector('[data-score-hint]');
|
||||
if (label) label.textContent = fullScore ? `成绩(0—${fullScore})` : '成绩';
|
||||
if (hint) hint.textContent = fullScore ? `本科满分 ${fullScore} 分,单科合格线 ${passScore} 分。` : '选择科目后显示满分与单科合格线。';
|
||||
if (hint) hint.textContent = fullScore ? `本科满分 ${fullScore} 分;独立及格规则:${passText}。` : '选择科目后显示其独立及格规则。';
|
||||
}
|
||||
if (event.target.matches('[data-action="result-candidate"]')) {
|
||||
const form = event.target.closest('form');
|
||||
const result = state.pageData?.results?.find(item => item.registrationId === event.target.value && item.subjectId === form?.subjectId?.value);
|
||||
if (form?.score) form.score.value = result?.score ?? '';
|
||||
if (form?.published) form.published.checked = result ? result.published : true;
|
||||
const button = form?.querySelector('button[type="submit"]');
|
||||
if (button) button.textContent = result ? '更新这条成绩' : '保存成绩';
|
||||
}
|
||||
if (event.target.matches('[name="subjectPassRule"]')) refreshSubjectPassRuleRow(event.target.closest('.exam-subject-editor'));
|
||||
if (event.target.matches('[name="passPolicy"]')) {
|
||||
refreshExamScoringForm(event.target.closest('form'));
|
||||
}
|
||||
@@ -409,7 +441,7 @@ document.addEventListener('submit', async event => {
|
||||
: body.businessType === 'candidate_account_batch'
|
||||
? `/api/admin/candidate-account-batches/${body.businessId}`
|
||||
: `/api/admin/score-appeals/${body.businessId}`;
|
||||
await api(path, { method: 'PATCH', body: { status: body.status, reviewNote: body.reviewNote } });
|
||||
await api(path, { method: 'PATCH', body: { status: body.status, reviewNote: body.reviewNote, ...(body.reviewedScore == null || body.reviewedScore === '' ? {} : { reviewedScore: Number(body.reviewedScore) }) } });
|
||||
closeModal(); toast(body.status === 'approved' ? '流程已处理' : '流程已退回', '操作已写入流程轨迹'); renderRoute();
|
||||
} else if (kind === 'flow-transfer') {
|
||||
const body = formObject(form);
|
||||
@@ -428,7 +460,8 @@ document.addEventListener('submit', async event => {
|
||||
body.subjects = [...form.querySelectorAll('.exam-subject-editor')].map(row => ({
|
||||
name: row.querySelector('[name="subjectName"]').value.trim(),
|
||||
fullScore: Number(row.querySelector('[name="subjectFullScore"]').value),
|
||||
passScore: Number(row.querySelector('[name="subjectPassScore"]').value),
|
||||
passRule: row.querySelector('[name="subjectPassRule"]').value,
|
||||
passValue: Number(row.querySelector('[name="subjectPassValue"]').value || 0),
|
||||
date: row.querySelector('[name="subjectDate"]').value,
|
||||
start: row.querySelector('[name="subjectStart"]').value,
|
||||
end: row.querySelector('[name="subjectEnd"]').value,
|
||||
@@ -443,6 +476,12 @@ document.addEventListener('submit', async event => {
|
||||
const body = formObject(form); body.published = form.published.checked;
|
||||
await api('/api/admin/results', { method: 'POST', body });
|
||||
toast(body.published ? '成绩已发布' : '成绩已保存', '考生端可见状态已更新'); renderRoute();
|
||||
} else if (kind === 'result-import-commit') {
|
||||
const rows = state.resultImportPreview?.rows || [];
|
||||
if (!rows.length) throw new Error('没有可提交的成绩预览');
|
||||
const result = await api('/api/admin/results/import', { method: 'POST', body: { rows } });
|
||||
state.resultImportPreview = null;
|
||||
toast('批量成绩已写入', `${result.count} 条成绩已在同一事务中提交`); renderRoute();
|
||||
}
|
||||
} catch (error) { toast('操作未完成', error.message); }
|
||||
finally { if (submit && submit.isConnected) { submit.disabled = false; submit.innerHTML = original; } }
|
||||
@@ -488,13 +527,25 @@ function openFlowDetail(id) {
|
||||
const isBatch = instance.businessType === 'candidate_account_batch';
|
||||
const isAppeal = instance.businessType === 'score_appeal';
|
||||
const finalBatchStep = isBatch && instance.currentStep >= instance.steps.length;
|
||||
const finalAppealStep = isAppeal && instance.currentStep >= instance.steps.length;
|
||||
const subject = isCenter ? instance.centerName : isBatch ? `${instance.schoolName} · ${instance.batchTotalCount} 个报名号` : isAppeal ? `${instance.candidateName} · ${instance.appealResult?.subjectName || '成绩复议'}` : instance.candidateName;
|
||||
const subjectDetail = isCenter ? `${instance.requestType === 'create' ? '新增考点' : '修改档案'} · ${instance.schoolName}` : isBatch ? '按班级批量申领 · 批准后生成账号' : isAppeal ? `${instance.appealResult?.examName || ''} · 原成绩 ${instance.appealResult?.score ?? '—'}` : `${instance.examName ? `${instance.examName} · ` : ''}${instance.schoolName}`;
|
||||
const change = instance.centerChange ? { ...instance.centerChange, address: formatRegionAddress(instance.centerChange) } : null;
|
||||
const changeSnapshot = change ? `<section class="flow-center-snapshot"><header><div><span>申请快照</span><h3>${h(change.name)} · ${h(change.code)}</h3></div><b>${change.rooms.length} 个考场</b></header><dl><div><dt>地址</dt><dd>${h(change.address)}</dd></div><div><dt>负责人</dt><dd>${h(change.managerName || '未填写')} · ${h(change.managerPhone || '未填写')}</dd></div><div><dt>开放时间</dt><dd>${h(change.gateOpenTime || '未填写')}</dd></div><div><dt>档案状态</dt><dd>${change.centerStatus === 'active' ? '启用' : '停用'}</dd></div></dl><div>${change.rooms.map(room => `<span><strong>${h(room.name)}</strong><small>${h(room.building)} · ${h(room.capacity)} 席 · ${h(room.seatPlan || '按现场座次表编排')}</small></span>`).join('')}</div></section>` : '';
|
||||
const batch = instance.accountBatch;
|
||||
const batchSnapshot = batch ? `<section class="flow-batch-snapshot"><header><div><span>班级配额</span><h3>${batch.totalCount} 个待建账户</h3></div><b>${batch.quotas.length} 个班级</b></header><div>${batch.quotas.map(item => `<span><strong>${h(item.className)}</strong><small>${item.count} 人</small></span>`).join('')}</div><p>最终批准时才生成固定报名号和随机初始密码。</p></section>` : '';
|
||||
const appeal = instance.appealResult;
|
||||
const appealSnapshot = appeal ? `<section class="flow-appeal-snapshot"><header><div><span>复议成绩快照</span><h3>${h(appeal.examName)} · ${h(appeal.subjectName)}</h3></div><b>${h(appeal.score)} / ${h(appeal.fullScore)}</b></header><dl><div><dt>当前等级</dt><dd>${h(appeal.grade)}</dd></div><div><dt>本科排名</dt><dd>第 ${h(appeal.rank)} / ${h(appeal.cohortSize)} 名(前 ${h(appeal.rankPercent)}%)</dd></div><div><dt>单科规则</dt><dd>${h(appeal.passText)}</dd></div><div><dt>当前结论</dt><dd>${appeal.qualified == null ? '不判定' : appeal.qualified ? '达线' : '未达线'}</dd></div></dl></section>` : '';
|
||||
const reviewedScoreField = finalAppealStep ? `<label><span>复核后分数(批准时必填)</span><input name="reviewedScore" type="number" min="0" max="${h(appeal?.fullScore || 0)}" step="0.5" value="${h(appeal?.score ?? '')}"></label>` : '';
|
||||
setModal(`<div class="modal-head"><div><span>WORKFLOW TRACE</span><h2>${h(instance.workflowName)}</h2><p>${h(subject)} · ${h(subjectDetail)}</p></div><button data-action="close-modal">×</button></div>${changeSnapshot}${batchSnapshot}<div class="flow-detail-track">${instance.steps.map(step => `<div class="${step.position < instance.currentStep || instance.status === 'approved' ? 'done' : step.position === instance.currentStep && instance.status === 'pending' ? 'current' : ''}"><i>${step.position}</i><span><strong>${h(step.name)}</strong><small>${h(statusLabels[step.adminLevel])}</small></span></div>`).join('')}</div><section class="flow-history"><h3>流程轨迹</h3>${history || '<p>暂无操作</p>'}</section>${canProcess ? `<form class="modal-form compact-flow-form" data-form="flow-process"><input type="hidden" name="businessType" value="${h(instance.businessType)}"><input type="hidden" name="businessId" value="${h(instance.businessId)}"><div class="field-row"><label><span>处理结论</span><select name="status"><option value="approved">通过当前步骤</option><option value="rejected">退回申请</option></select></label><label><span>处理意见</span><input name="reviewNote" placeholder="填写核验说明"></label></div><button class="solid-button" type="submit">${finalBatchStep ? '最终批准并生成账号' : '确认处理'}</button></form><form class="modal-form compact-flow-form" data-form="flow-transfer"><input type="hidden" name="id" value="${h(instance.id)}"><div class="field-row"><label><span>转交给同级管理员</span><select name="assigneeId" required>${available.filter(item => item.id !== instance.assignee?.id).map(item => `<option value="${h(item.id)}">${h(item.displayName)}</option>`).join('')}</select></label><label><span>转交说明</span><input name="note"></label></div><button class="ghost-button" type="submit" ${available.filter(item => item.id !== instance.assignee?.id).length ? '' : 'disabled'}>转交流程</button></form>` : '<div class="read-only-callout">当前流程未分配给你,只能查看轨迹。</div>'}${state.pageData.canSupervise ? `<form class="modal-form supervisor-form" data-form="flow-supervise"><input type="hidden" name="id" value="${h(instance.id)}"><h3>超级管理员监督调整</h3><div class="field-row"><label><span>调整到步骤</span><select name="currentStep">${instance.steps.map(step => `<option value="${step.position}" ${step.position === instance.currentStep ? 'selected' : ''}>${step.position}. ${h(step.name)}</option>`).join('')}</select></label><label><span>指定责任人(可留空自动选择)</span><select name="assigneeId"><option value="">自动选择</option>${state.pageData.availableAdmins.map(item => `<option value="${h(item.id)}">${h(item.displayName)} · ${h(statusLabels[item.adminLevel])}</option>`).join('')}</select></label></div><label><span>监督说明</span><input name="note" placeholder="说明修改或退回原因"></label><button class="ghost-button" type="submit">监督调整</button></form>` : ''}`);
|
||||
if (appealSnapshot) modalRoot.querySelector('.modal-head')?.insertAdjacentHTML('afterend', appealSnapshot);
|
||||
if (finalAppealStep) {
|
||||
const processForm = modalRoot.querySelector('[data-form="flow-process"]');
|
||||
const fieldRow = processForm?.querySelector('.field-row');
|
||||
if (fieldRow) fieldRow.insertAdjacentHTML('beforeend', reviewedScoreField);
|
||||
const processButton = processForm?.querySelector('button[type="submit"]');
|
||||
if (processButton) processButton.textContent = '批准并更新成绩';
|
||||
}
|
||||
}
|
||||
|
||||
function openCandidateReview(id) {
|
||||
@@ -523,18 +574,47 @@ function dateTimeLocal(value) {
|
||||
}
|
||||
|
||||
function examSubjectEditor(subject = {}) {
|
||||
return `<article class="exam-subject-editor"><header><span>科目明细</span><button type="button" data-action="remove-exam-subject">移除</button></header><div class="exam-subject-grid"><label class="subject-name-field"><span>科目名称 *</span><input name="subjectName" required value="${h(subject.name || '')}" placeholder="例如:语文"></label><label><span>满分 *</span><input name="subjectFullScore" type="number" min="0.5" max="1000" step="0.5" required value="${h(subject.fullScore ?? 100)}"></label><label><span>单科合格分 *</span><input name="subjectPassScore" type="number" min="0" max="1000" step="0.5" required value="${h(subject.passScore ?? 60)}"></label><label><span>考试日期 *</span><input name="subjectDate" type="date" required value="${h(subject.date || '')}"></label><label><span>开始 *</span><input name="subjectStart" type="time" required value="${h(subject.start || '09:00')}"></label><label><span>结束 *</span><input name="subjectEnd" type="time" required value="${h(subject.end || '11:00')}"></label><label><span>费用</span><input name="subjectFee" type="number" min="0" step="0.01" value="${h(subject.fee ?? 0)}"></label></div></article>`;
|
||||
const passRule = subject.passRule === 'score_ratio' ? 'rank_percent' : (subject.passRule || 'fixed_score');
|
||||
const passValue = subject.passValue ?? subject.passScore ?? 60;
|
||||
return `<article class="exam-subject-editor"><header><span>科目明细</span><button type="button" data-action="remove-exam-subject">移除</button></header><div class="exam-subject-grid"><label class="subject-name-field"><span>科目名称 *</span><input name="subjectName" required value="${h(subject.name || '')}" placeholder="例如:语文"></label><label><span>满分 *</span><input name="subjectFullScore" type="number" min="0.5" max="1000" step="0.5" required value="${h(subject.fullScore ?? 100)}"></label><label><span>单科及格线方式 *</span><select name="subjectPassRule"><option value="fixed_score" ${passRule === 'fixed_score' ? 'selected' : ''}>固定分</option><option value="rank_percent" ${passRule === 'rank_percent' ? 'selected' : ''}>按排名比例</option><option value="none" ${passRule === 'none' ? 'selected' : ''}>不设单科线</option></select></label><label data-subject-pass-value><span data-subject-pass-label>规则数值 *</span><span class="unit-input"><input name="subjectPassValue" type="number" min="0" max="1000" step="0.1" required value="${h(passValue)}"><em data-subject-pass-unit>分</em></span></label><label><span>考试日期 *</span><input name="subjectDate" type="date" required value="${h(subject.date || '')}"></label><label><span>开始 *</span><input name="subjectStart" type="time" required value="${h(subject.start || '09:00')}"></label><label><span>结束 *</span><input name="subjectEnd" type="time" required value="${h(subject.end || '11:00')}"></label><label><span>费用</span><input name="subjectFee" type="number" min="0" step="0.01" value="${h(subject.fee ?? 0)}"></label></div><p class="subject-pass-preview" data-subject-pass-preview></p></article>`;
|
||||
}
|
||||
|
||||
function refreshSubjectPassRuleRow(row) {
|
||||
if (!row) return;
|
||||
const ruleSelect = row.querySelector('[name="subjectPassRule"]');
|
||||
const legacyOption = ruleSelect?.querySelector('option[value="score_ratio"]');
|
||||
if (legacyOption) { legacyOption.value = 'rank_percent'; legacyOption.textContent = '按排名比例'; }
|
||||
const rule = ruleSelect?.value || 'fixed_score';
|
||||
const fullScore = Number(row.querySelector('[name="subjectFullScore"]')?.value || 0);
|
||||
const valueField = row.querySelector('[data-subject-pass-value]');
|
||||
const input = row.querySelector('[name="subjectPassValue"]');
|
||||
const label = row.querySelector('[data-subject-pass-label]');
|
||||
const unit = row.querySelector('[data-subject-pass-unit]');
|
||||
const preview = row.querySelector('[data-subject-pass-preview]');
|
||||
valueField?.classList.toggle('hidden', rule === 'none');
|
||||
if (input) {
|
||||
input.disabled = rule === 'none';
|
||||
input.max = rule === 'rank_percent' ? '100' : String(fullScore || 1000);
|
||||
input.min = rule === 'rank_percent' ? '0.1' : '0';
|
||||
}
|
||||
if (label) label.textContent = rule === 'rank_percent' ? '排名比例 *' : '固定及格分 *';
|
||||
if (unit) unit.textContent = rule === 'rank_percent' ? '%' : '分';
|
||||
const value = Number(input?.value || 0);
|
||||
if (preview) preview.textContent = rule === 'none' ? '本科只展示成绩,不单独判定达线。' : rule === 'rank_percent' ? `本科排名前 ${value}% 达线;分数边界随本次已发布成绩队列变化。` : `本科达到 ${value} 分视为单科达线。`;
|
||||
}
|
||||
|
||||
function refreshExamScoringForm(form) {
|
||||
if (!form?.matches('[data-form="exam-form"]')) return;
|
||||
const rows = [...form.querySelectorAll('.exam-subject-editor')];
|
||||
rows.forEach(refreshSubjectPassRuleRow);
|
||||
const total = rows.reduce((sum, row) => sum + Number(row.querySelector('[name="subjectFullScore"]')?.value || 0), 0);
|
||||
const totalElement = form.querySelector('[data-exam-total]');
|
||||
const countElement = form.querySelector('[data-exam-subject-count]');
|
||||
if (totalElement) totalElement.textContent = total;
|
||||
if (countElement) countElement.textContent = rows.length;
|
||||
const policy = form.querySelector('[name="passPolicy"]')?.value || 'score_ratio';
|
||||
const policySelect = form.querySelector('[name="passPolicy"]');
|
||||
policySelect?.querySelector('option[value="score_ratio"]')?.remove();
|
||||
const policy = policySelect?.value || 'rank_percent';
|
||||
const valueField = form.querySelector('[data-pass-value-field]');
|
||||
const valueInput = form.querySelector('[name="passValue"]');
|
||||
const unit = form.querySelector('[data-pass-unit]');
|
||||
@@ -549,10 +629,9 @@ function refreshExamScoringForm(form) {
|
||||
if (unit) unit.textContent = policy === 'fixed_score' ? '分' : '%';
|
||||
const hints = {
|
||||
fixed_score: '按报考科目的成绩总和判断;适合所有考生科目组合一致的考试。',
|
||||
score_ratio: '按“实得总分 ÷ 所报科目满分”判断,适合允许选科的考试。',
|
||||
rank_percent: '在相同报考科目组合且成绩完整的考生中排名,同分并列。',
|
||||
subject_scores: '每个科目都必须达到上方配置的单科合格分。',
|
||||
none: '只展示成绩、总分和得分率,不显示合格或未合格。'
|
||||
none: '只展示成绩、总分和排名,不显示合格或未合格。'
|
||||
};
|
||||
if (hint) hint.textContent = hints[policy];
|
||||
}
|
||||
@@ -561,7 +640,7 @@ function openExamForm(exam = null) {
|
||||
const editing = Boolean(exam);
|
||||
if (editing && exam.status !== 'draft') return toast('无法编辑', '请先将已发布考试撤回为草稿');
|
||||
const subjects = exam?.subjects?.length ? exam.subjects : [{ date: String(exam?.examStart || '').slice(0, 10) }];
|
||||
const passPolicy = exam?.passPolicy || 'score_ratio';
|
||||
const passPolicy = exam?.passPolicy === 'score_ratio' ? 'rank_percent' : (exam?.passPolicy || 'rank_percent');
|
||||
setModal(`<div class="modal-head"><div><span>${editing ? 'EDIT EXAM DRAFT' : 'NEW EXAM'}</span><h2>${editing ? '编辑考试草稿' : '创建考试与科目'}</h2><p>先定义科目计分,再选择整场考试的合格判定方式。</p></div><button type="button" data-action="close-modal" aria-label="关闭弹窗">×</button></div><form class="modal-form exam-config-form" data-form="exam-form">${editing ? `<input type="hidden" name="id" value="${h(exam.id)}">` : ''}<section class="exam-form-section"><header><span>01</span><div><h3>基本安排</h3><p>考试名称、开放窗口与考点信息</p></div></header><div class="field-row"><label><span>考试名称 *</span><input name="name" required value="${h(exam?.name)}" placeholder="例如:2027 年春季学业考试"></label><label><span>考试代码</span><input name="code" value="${h(exam?.code)}" placeholder="自动生成或手动填写"></label></div><label><span>考试说明</span><textarea name="description" rows="2" placeholder="报名对象、考试范围等">${h(exam?.description)}</textarea></label><div class="field-row"><label><span>报名开始 *</span><input name="registrationStart" type="datetime-local" value="${dateTimeLocal(exam?.registrationStart)}" required></label><label><span>报名结束 *</span><input name="registrationEnd" type="datetime-local" value="${dateTimeLocal(exam?.registrationEnd)}" required></label></div><div class="field-row"><label><span>考试开始 *</span><input name="examStart" type="datetime-local" value="${dateTimeLocal(exam?.examStart)}" required></label><label><span>考试结束 *</span><input name="examEnd" type="datetime-local" value="${dateTimeLocal(exam?.examEnd)}" required></label></div><div class="field-row"><label><span>准考证下载开始 *</span><input name="admitDownloadStart" type="datetime-local" value="${dateTimeLocal(exam?.admitDownloadStart)}" required></label><label><span>准考证下载结束 *</span><input name="admitDownloadEnd" type="datetime-local" value="${dateTimeLocal(exam?.admitDownloadEnd)}" required></label></div></section><section class="exam-form-section subject-config-section"><header><span>02</span><div><h3>科目与总分</h3><p>每科独立设置满分、合格分、时间和费用</p></div><aside><small><b data-exam-subject-count>${subjects.length}</b> 科</small><strong><b data-exam-total>0</b> 分</strong></aside></header><div data-exam-subjects>${subjects.map(examSubjectEditor).join('')}</div><button class="add-subject-button" type="button" data-action="add-exam-subject">${icons.plus} 添加科目</button></section><section class="exam-form-section pass-policy-section"><header><span>03</span><div><h3>合格线策略</h3><p>系统会在所有报考科目成绩发布后自动判定</p></div></header><div class="pass-policy-grid"><label><span>判定方式 *</span><select name="passPolicy"><option value="score_ratio" ${passPolicy === 'score_ratio' ? 'selected' : ''}>按总分得分率</option><option value="fixed_score" ${passPolicy === 'fixed_score' ? 'selected' : ''}>固定总分线</option><option value="rank_percent" ${passPolicy === 'rank_percent' ? 'selected' : ''}>总成绩排名前百分比</option><option value="subject_scores" ${passPolicy === 'subject_scores' ? 'selected' : ''}>所有单科均达线</option><option value="none" ${passPolicy === 'none' ? 'selected' : ''}>不判定合格</option></select></label><label data-pass-value-field><span>策略数值 *</span><span class="unit-input"><input name="passValue" type="number" min="0" max="100" step="0.1" value="${h(exam?.passValue ?? 60)}" required><em data-pass-unit>%</em></span></label></div><p class="pass-policy-hint" data-pass-hint></p></section><section class="exam-form-section"><header><span>04</span><div><h3>发布设置</h3><p>草稿可继续维护,发布后对考生开放</p></div></header><div class="field-row"><label><span>考点说明</span><input name="location" value="${h(exam?.location)}" placeholder="例如:全市指定考点"></label><label><span>${editing ? '保存状态' : '创建状态'}</span><select name="status"><option value="draft">保存为草稿</option><option value="published">立即发布</option></select></label></div></section><div class="modal-foot"><button type="button" class="ghost-button" data-action="close-modal">取消</button><button type="submit" class="solid-button">${editing ? '保存考试' : '创建考试'}</button></div></form>`);
|
||||
refreshExamScoringForm(modalRoot.querySelector('[data-form="exam-form"]'));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user