Add independent subject pass rules and atomic score imports
This commit is contained in:
@@ -27,7 +27,7 @@ export function createAdminViews(context) {
|
||||
dashboard: ['考务工作台', '掌握当前报名、审核和发布任务。'], candidates: ['考生资料审核', '核验考生实名、学籍与联系信息。'],
|
||||
registrations: ['考试报名审核', '确认考生所报考试、科目与缴费状态。'], exams: ['考试与科目', '创建考试、配置报名时间与考试科目。'],
|
||||
notices: ['通知发布', '发布后立即展示在公开首页和考生中心。'], admit: [state.user.adminLevel === 'super' ? '准考证编排' : state.user.adminLevel === 'school' ? '校内准考证' : '本班准考证', state.user.adminLevel === 'super' ? '按整场考试预检容量,并批量分配固定考点、分科考场与准考证号。' : '按当前管理范围批量下载准考证,并导出逐科准考证信息。'],
|
||||
results: [state.user.adminLevel === 'super' ? '成绩发布' : '成绩查看', state.user.adminLevel === 'super' ? '录入单科成绩并控制是否对考生公开。' : '按数据范围查看已录入成绩。'],
|
||||
results: [state.user.adminLevel === 'super' ? '成绩管理中心' : '成绩分析', state.user.adminLevel === 'super' ? '按考试录入、预览导入、发布并跟踪各科达线情况。' : '按考试、科目和发布状态分析管理范围内成绩。'],
|
||||
admins: ['分级管理员', '同一级可以配置多名管理员,并分别绑定学校或班级。'],
|
||||
centers: ['考务场所档案', state.user.adminLevel === 'school' ? '查看本校考点与结构化考场,所有变更提交后进入审批。' : '管理各校考点、考场容量与变更审批台账。'],
|
||||
organization: ['本校组织与权限', '维护本校班级,并为每个班级配置一个或多个班级管理员。'],
|
||||
@@ -80,7 +80,7 @@ export function createAdminViews(context) {
|
||||
}
|
||||
|
||||
function adminExams(exams) {
|
||||
return `<div class="admin-exam-grid">${exams.map(exam => `<article class="admin-exam-card ${exam.status}${exam.status === 'draft' ? ' editable' : ''}" ${exam.status === 'draft' ? `data-action="edit-exam" data-id="${h(exam.id)}" title="点击编辑草稿"` : ''}><header><span class="exam-code">${h(exam.code)}</span>${badge(exam.status)}</header><h2>${h(exam.name)}</h2><p>${h(exam.description)}</p><div class="exam-score-band"><span><small>考试总分</small><strong>${h(exam.totalScore)}</strong><em>分</em></span><span><small>合格规则</small><b>${h(passPolicyText(exam))}</b></span></div><dl><div><dt>报名时间</dt><dd>${dateRange(exam.registrationStart, exam.registrationEnd)}</dd></div><div><dt>考试时间</dt><dd>${dateRange(exam.examStart, exam.examEnd)}</dd></div><div><dt>考点</dt><dd>${h(exam.location)}</dd></div></dl><div class="admin-subjects">${exam.subjects.map(subject => `<span><b>${h(subject.name)}</b><small>${h(subject.date)} ${h(subject.start)} · ${h(subject.fullScore)} 分</small></span>`).join('') || '<span>科目待配置</span>'}</div><footer><span>${exam.registrationCount} 人报名 · ${exam.subjects.length} 科</span><div class="exam-card-actions">${exam.status === 'draft' ? `<button class="row-action" data-action="edit-exam" data-id="${h(exam.id)}">编辑草稿</button>` : ''}<button class="row-action" data-action="toggle-exam" data-id="${h(exam.id)}" data-status="${exam.status === 'published' ? 'draft' : 'published'}">${exam.status === 'published' ? '撤回为草稿' : '发布考试'}</button></div></footer></article>`).join('')}</div>`;
|
||||
return `<div class="admin-exam-grid">${exams.map(exam => `<article class="admin-exam-card ${exam.status}${exam.status === 'draft' ? ' editable' : ''}" ${exam.status === 'draft' ? `data-action="edit-exam" data-id="${h(exam.id)}" title="点击编辑草稿"` : ''}><header><span class="exam-code">${h(exam.code)}</span>${badge(exam.status)}</header><h2>${h(exam.name)}</h2><p>${h(exam.description)}</p><div class="exam-score-band"><span><small>考试总分</small><strong>${h(exam.totalScore)}</strong><em>分</em></span><span><small>合格规则</small><b>${h(passPolicyText(exam))}</b></span></div><dl><div><dt>报名时间</dt><dd>${dateRange(exam.registrationStart, exam.registrationEnd)}</dd></div><div><dt>考试时间</dt><dd>${dateRange(exam.examStart, exam.examEnd)}</dd></div><div><dt>考点</dt><dd>${h(exam.location)}</dd></div></dl><div class="admin-subjects">${exam.subjects.map(subject => `<span><b>${h(subject.name)}</b><small>${h(subject.date)} ${h(subject.start)} · 满分 ${h(subject.fullScore)} · ${subject.passRule === 'none' ? '不设单科线' : subject.passRule === 'rank_percent' ? `本科排名前 ${h(subject.passValue)}%` : `固定 ${h(subject.passScore)} 分`}</small></span>`).join('') || '<span>科目待配置</span>'}</div><footer><span>${exam.registrationCount} 人报名 · ${exam.subjects.length} 科</span><div class="exam-card-actions">${exam.status === 'draft' ? `<button class="row-action" data-action="edit-exam" data-id="${h(exam.id)}">编辑草稿</button>` : ''}<button class="row-action" data-action="toggle-exam" data-id="${h(exam.id)}" data-status="${exam.status === 'published' ? 'draft' : 'published'}">${exam.status === 'published' ? '撤回为草稿' : '发布考试'}</button></div></footer></article>`).join('')}</div>`;
|
||||
}
|
||||
|
||||
function adminNotices(notices) {
|
||||
@@ -109,9 +109,20 @@ export function createAdminViews(context) {
|
||||
}
|
||||
|
||||
function adminResults(data) {
|
||||
const entry = state.user.adminLevel === 'super' ? `<section class="panel result-entry"><div class="panel-title"><h2>录入单科成绩</h2><span>按科目满分校验</span></div><form data-form="result-entry"><label><span>报名记录</span><select name="registrationId" data-action="result-registration" required><option value="">请选择考生和考试</option>${data.registrations.map(reg => `<option value="${h(reg.id)}" data-subjects='${h(JSON.stringify(reg.subjects.map(subject => ({id:subject.id,name:subject.name,fullScore:subject.fullScore,passScore:subject.passScore}))))}'>${h(reg.exam.name)} · ${h(reg.registrationNumber || reg.id)}</option>`).join('')}</select></label><label><span>考试科目</span><select name="subjectId" id="resultSubject" data-action="result-subject" required><option value="">请先选择报名记录</option></select></label><div class="field-row"><label><span data-score-label>成绩</span><input type="number" name="score" min="0" step="0.5" required></label><label><span>等级</span><input name="grade" placeholder="留空将按得分率计算"></label></div><p class="score-rule-hint" data-score-hint>选择科目后显示满分与单科合格线。</p><label class="agreement publish-switch"><input type="checkbox" name="published" checked><span>保存后立即向考生发布</span></label><button class="solid-button" type="submit">保存成绩</button></form></section>` : '';
|
||||
const appealLedger = `<section class="panel data-panel"><div class="panel-title"><div><h2>成绩复议台账</h2><p>复议按考生所属班级和学校自动分配,可在流程中心办理。</p></div><button class="row-action" data-route="admin/flows">进入流程中心</button></div><div class="table-scroll"><table><thead><tr><th>考生 / 科目</th><th>考试</th><th>原成绩</th><th>复议理由</th><th>当前步骤</th><th>责任人</th><th>状态</th></tr></thead><tbody>${(data.appeals || []).map(appeal => `<tr><td><strong>${h(appeal.result?.candidateName)} · ${h(appeal.result?.subjectName)}</strong></td><td>${h(appeal.result?.examName)}</td><td><strong>${h(appeal.result?.score)}</strong></td><td><small>${h(appeal.reason)}</small></td><td>${h(appeal.currentStepDetail?.name || '流程已结束')}</td><td>${h(appeal.assignee?.displayName || '—')}</td><td>${badge(appeal.status)}</td></tr>`).join('') || '<tr><td colspan="7" class="empty-state">暂无成绩复议申请</td></tr>'}</tbody></table></div></section>`;
|
||||
return `${excelToolbar('results', { importable: state.user.adminLevel === 'super', label: '成绩台账' })}<div class="results-admin-grid ${state.user.adminLevel === 'super' ? '' : 'read-only'}">${entry}<section class="panel published-results"><div class="panel-title"><h2>${state.user.adminLevel === 'super' ? '最近成绩' : '范围内成绩'}</h2><span>${data.results.length} 条记录</span></div>${data.results.slice(0, 50).map(result => `<div><span class="user-avatar">${h((result.candidateName || '?').slice(0,1))}</span><p><strong>${h(result.candidateName)} · ${h(result.subjectName)}</strong><small>${h(result.examName)}</small></p><b>${h(result.score)}</b>${badge(result.published ? 'published' : 'draft')}</div>`).join('') || '<p class="empty-state">还没有成绩记录</p>'}</section></div>${appealLedger}`;
|
||||
const preview = state.resultImportPreview;
|
||||
const activeExam = data.exams.find(item => item.id === state.resultExamFilter) || data.exams.find(item => item.registrationCount) || data.exams[0];
|
||||
if (activeExam) state.resultExamFilter = activeExam.id;
|
||||
const examResults = activeExam ? data.results.filter(item => item.examId === activeExam.id) : data.results;
|
||||
const examAppeals = activeExam ? (data.appeals || []).filter(item => item.result?.examId === activeExam.id) : (data.appeals || []);
|
||||
const passRate = activeExam?.complete ? Math.round(activeExam.qualified / activeExam.complete * 100) : null;
|
||||
const entry = state.user.adminLevel === 'super' ? `<section class="panel result-entry result-entry-console"><div class="panel-title"><div><h2>单科成绩录入</h2><p>按考试和科目缩小候选范围,已有成绩会自动回填。</p></div><span>实时校验</span></div><form data-form="result-entry"><div class="result-entry-fields"><label><span>考试 *</span><select name="examId" data-action="result-exam" required><option value="">请选择考试</option>${data.exams.map(exam => `<option value="${h(exam.id)}" ${exam.id === activeExam?.id ? 'selected' : ''}>${h(exam.name)} · ${h(exam.code)}</option>`).join('')}</select></label><label><span>考试科目 *</span><select name="subjectId" id="resultSubject" data-action="result-subject" required><option value="">请选择科目</option>${(activeExam?.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('')}</select></label><label><span>考生 *</span><select name="registrationId" data-action="result-candidate" required disabled><option value="">请先选择科目</option></select></label><label><span data-score-label>成绩 *</span><input type="number" name="score" min="0" step="0.5" required></label><label><span>等级规则</span><input value="按同场同科排名自动计算" disabled></label></div><p class="score-rule-hint" data-score-hint>选择科目后显示其独立及格规则。</p><div class="result-entry-submit"><label class="agreement publish-switch"><input type="checkbox" name="published" checked><span>保存后立即向考生发布</span></label><button class="solid-button" type="submit">保存成绩</button></div></form></section>` : '';
|
||||
const importPreview = preview ? `<section class="panel result-import-preview"><header><div><span>EXCEL STAGING AREA</span><h2>${h(preview.fileName || '成绩导入预览')}</h2><p>此处数据尚未写入数据库。请检查错误、更新覆盖项和发布状态后再提交。</p></div><button class="row-action" data-action="cancel-result-import">取消预览</button></header><div class="import-preview-metrics"><span><small>总行数</small><strong>${preview.summary.total}</strong></span><span class="valid"><small>可提交</small><strong>${preview.summary.valid}</strong></span><span class="invalid"><small>有错误</small><strong>${preview.summary.invalid}</strong></span><span><small>新增 / 更新</small><strong>${preview.summary.create} / ${preview.summary.update}</strong></span><span><small>将发布</small><strong>${preview.summary.publish}</strong></span></div><div class="table-scroll"><table class="import-preview-table"><thead><tr><th>Excel 行</th><th>考生</th><th>考试 / 科目</th><th>成绩</th><th>独立及格线</th><th>发布</th><th>写入方式 / 校验</th></tr></thead><tbody>${preview.rows.map(row => `<tr class="${row.errors.length ? 'row-invalid' : ''}"><td class="mono">${h(row.sourceRow)}</td><td><strong>${h(row.candidateName || '未匹配')}</strong><small class="mono">${h(row.candidateNumber)}</small></td><td><strong>${h(row.examName || row.examCode)}</strong><small>${h(row.subjectName)}</small></td><td><strong>${Number.isFinite(row.score) ? h(row.score) : '—'}</strong><small>${row.scoreRate == null ? '' : `${h(row.scoreRate)}% · ${h(row.grade)}`}</small></td><td><strong>${h(row.passText || '—')}</strong><small>${row.qualified == null ? '不判定' : row.qualified ? '达到单科线' : '未达到单科线'}</small></td><td>${badge(row.published ? 'published' : 'draft')}</td><td>${row.errors.length ? `<ul>${row.errors.map(error => `<li>${h(error)}</li>`).join('')}</ul>` : `<strong>${row.mode === 'update' ? '覆盖已有成绩' : '新增成绩'}</strong><small>校验通过</small>`}</td></tr>`).join('')}</tbody></table></div><form data-form="result-import-commit"><div class="import-commit-bar"><p>${preview.summary.invalid ? `有 ${preview.summary.invalid} 行错误,修正 Excel 后请重新选择文件。` : `确认后将以一个事务写入 ${preview.summary.valid} 条成绩,失败时不会留下部分数据。`}</p><button class="solid-button" type="submit" ${preview.summary.invalid ? 'disabled' : ''}>确认提交到数据库</button></div></form></section>` : '';
|
||||
const appealLedger = `<section class="panel data-panel"><div class="panel-title"><div><h2>本场成绩复议</h2><p>复议按考生所属班级和学校自动分配,可在流程中心办理。</p></div><button class="row-action" data-route="admin/flows">进入流程中心</button></div><div class="table-scroll"><table><thead><tr><th>考生 / 科目</th><th>考试</th><th>原成绩</th><th>复议理由</th><th>当前步骤</th><th>责任人</th><th>状态</th></tr></thead><tbody>${examAppeals.map(appeal => `<tr><td><strong>${h(appeal.result?.candidateName)} · ${h(appeal.result?.subjectName)}</strong></td><td>${h(appeal.result?.examName)}</td><td><strong>${h(appeal.result?.score)}</strong></td><td><small>${h(appeal.reason)}</small></td><td>${h(appeal.currentStepDetail?.name || '流程已结束')}</td><td>${h(appeal.assignee?.displayName || '—')}</td><td>${badge(appeal.status)}</td></tr>`).join('') || '<tr><td colspan="7" class="empty-state">本场暂无成绩复议申请</td></tr>'}</tbody></table></div></section>`;
|
||||
const examStrip = `<section class="result-exam-strip">${data.exams.map(exam => `<button class="${exam.id === activeExam?.id ? 'active' : ''}" data-action="result-exam-filter" data-id="${h(exam.id)}"><span>${h(exam.code)}</span><strong>${h(exam.name)}</strong><small>${exam.registrationCount} 人 · 已录 ${exam.scored}/${exam.enrolledSubjects} 科次</small><i style="--progress:${exam.enrolledSubjects ? Math.round(exam.scored / exam.enrolledSubjects * 100) : 0}%"></i></button>`).join('')}</section>`;
|
||||
const toolbar = `<div class="excel-toolbar result-excel-toolbar"><span><strong>成绩 Excel 工作区</strong><small>模板导入会先暂存预览,确认后才原子写入数据库</small></span><div><button class="row-action" data-action="excel-download" data-resource="results" data-template="1">下载模板</button><button class="row-action" data-action="excel-download" data-resource="results" data-exam-id="${h(activeExam?.id || '')}">导出本场成绩</button>${state.user.adminLevel === 'super' ? '<button class="row-action primary" data-action="excel-import" data-resource="results">选择 Excel 预览</button><input type="file" accept=".xlsx" hidden data-excel-file="results">' : ''}</div></div>`;
|
||||
const metrics = `<section class="result-metric-grid"><article><small>报名考生</small><strong>${activeExam?.registrationCount ?? 0}</strong><span>本场已通过报名</span></article><article><small>录入进度</small><strong>${activeExam?.scored ?? 0}<em> / ${activeExam?.enrolledSubjects ?? 0}</em></strong><span>剩余 ${activeExam?.missing ?? 0} 科次</span></article><article><small>已发布</small><strong>${activeExam?.published ?? 0}</strong><span>草稿 ${Math.max(0, (activeExam?.scored || 0) - (activeExam?.published || 0))} 条</span></article><article><small>成绩已出齐</small><strong>${activeExam?.complete ?? 0}</strong><span>人</span></article><article><small>整场合格率</small><strong>${passRate == null ? '—' : `${passRate}%`}</strong><span>按本场排名或所设规则判定</span></article><article><small>成绩复议</small><strong>${examAppeals.length}</strong><span>当前考试累计</span></article></section>`;
|
||||
const ledger = `<section class="panel data-panel result-ledger"><div class="data-toolbar"><label class="search-box">${icons.search}<input data-action="table-search" data-target="resultTable" placeholder="搜索报名号、姓名、学校或科目"></label><div class="filter-pills"><button class="active" data-action="status-filter" data-target="resultTable" data-status="all">全部</button><button data-action="status-filter" data-target="resultTable" data-status="published">已发布</button><button data-action="status-filter" data-target="resultTable" data-status="draft">草稿</button><button data-action="status-filter" data-target="resultTable" data-status="qualified">单科达线</button><button data-action="status-filter" data-target="resultTable" data-status="unqualified">单科未达线</button></div></div><div class="table-scroll"><table id="resultTable"><thead><tr><th>考生</th><th>考试 / 科目</th><th>成绩</th><th>排名 / 等级</th><th>单科及格规则</th><th>达线</th><th>发布</th><th>更新时间</th></tr></thead><tbody>${examResults.map(result => `<tr data-status="${result.published ? 'published' : 'draft'} ${result.qualified === true ? 'qualified' : result.qualified === false ? 'unqualified' : ''}"><td><div class="person-cell"><span>${h((result.candidateName || '?').slice(0,1))}</span><div><strong>${h(result.candidateName)}</strong><small class="mono">${h(result.candidateNumber)}</small><small>${h(result.schoolName)} · ${h(result.className)}</small></div></div></td><td><strong>${h(result.subjectName)}</strong><small>${h(result.examCode)}</small></td><td><strong class="result-score">${h(result.score)}<em> / ${h(result.fullScore)}</em></strong></td><td><strong>第 ${h(result.rank)} / ${h(result.cohortSize)} 名</strong><small>${h(result.grade)} · 前 ${h(result.rankPercent)}%</small></td><td><strong>${h(result.passText)}</strong></td><td>${result.qualified == null ? '<span class="result-neutral">不判定</span>' : result.qualified ? '<span class="result-qualified">达线</span>' : '<span class="result-unqualified">未达线</span>'}</td><td>${badge(result.published ? 'published' : 'draft')}</td><td>${formatDate(result.updatedAt || result.publishedAt, true)}</td></tr>`).join('') || '<tr><td colspan="8" class="empty-state">本场考试还没有成绩记录</td></tr>'}</tbody></table></div></section>`;
|
||||
return `${examStrip}${metrics}${toolbar}${importPreview}${entry}${ledger}${appealLedger}`;
|
||||
}
|
||||
|
||||
function adminUsers(data) {
|
||||
|
||||
Reference in New Issue
Block a user