性能
This commit is contained in:
@@ -3,7 +3,7 @@ import { createAdminViews } from './src/client/admin-views.mjs';
|
||||
import { createCandidateViews } from './src/client/candidate-views.mjs';
|
||||
import { createPublicViews } from './src/client/public-views.mjs';
|
||||
import { state } from './src/client/state.mjs';
|
||||
import { badge, dateRange, formatDate, h, icons, money, statusLabels } from './src/client/ui.mjs';
|
||||
import { badge, dateRange, formatDate, h, icons, money, passPolicyText, statusLabels } from './src/client/ui.mjs';
|
||||
|
||||
const app = document.querySelector('#app');
|
||||
const modalRoot = document.querySelector('#modalRoot');
|
||||
@@ -34,7 +34,7 @@ function renderError(error) {
|
||||
app.innerHTML = `<section class="fatal-error"><span>!</span><h1>页面暂时无法加载</h1><p>${h(error.message)}</p><button class="solid-button" data-action="retry">重新加载</button></section>`;
|
||||
}
|
||||
|
||||
const baseViewContext = { state, app, h, formatDate, dateRange, badge, money, statusLabels, icons, api, renderError, emptyState };
|
||||
const baseViewContext = { state, app, h, formatDate, dateRange, badge, money, passPolicyText, statusLabels, icons, api, renderError, emptyState };
|
||||
const { brand, renderHome, renderAuth } = createPublicViews(baseViewContext);
|
||||
const { adminNavForUser, portalShell, loadingPanel, renderCandidate } = createCandidateViews({ ...baseViewContext, brand });
|
||||
const { renderAdmin } = createAdminViews({ ...baseViewContext, brand, portalShell, loadingPanel, adminNavForUser });
|
||||
@@ -146,6 +146,21 @@ document.addEventListener('click', async event => {
|
||||
if (list.children.length <= 1) return toast('至少保留一步', '审批流程不能为空');
|
||||
target.closest('.workflow-step-row').remove(); return;
|
||||
}
|
||||
if (action === 'add-exam-subject') {
|
||||
const form = target.closest('form');
|
||||
const date = form?.examStart?.value?.slice(0, 10) || '';
|
||||
form?.querySelector('[data-exam-subjects]')?.insertAdjacentHTML('beforeend', examSubjectEditor({ date }));
|
||||
refreshExamScoringForm(form);
|
||||
return;
|
||||
}
|
||||
if (action === 'remove-exam-subject') {
|
||||
const form = target.closest('form');
|
||||
const list = target.closest('[data-exam-subjects]');
|
||||
if (list.children.length <= 1) return toast('至少保留一个科目', '考试计划必须包含科目');
|
||||
target.closest('.exam-subject-editor').remove();
|
||||
refreshExamScoringForm(form);
|
||||
return;
|
||||
}
|
||||
if (action === 'edit-exam') return openExamForm(state.pageData.exams.find(exam => exam.id === target.dataset.id));
|
||||
if (action === 'review-candidate') return openCandidateReview(target.dataset.id);
|
||||
if (action === 'review-registration') return openRegistrationReview(target.dataset.id);
|
||||
@@ -194,8 +209,10 @@ document.addEventListener('input', event => {
|
||||
form.querySelector('[data-subject-count]').textContent = checked.length;
|
||||
const exam = state.pageData.exams.find(item => item.id === form.examId.value);
|
||||
const fee = checked.reduce((sum, input) => sum + Number(exam.subjects.find(subject => subject.id === input.value)?.fee || 0), 0);
|
||||
form.querySelector('[data-subject-fee]').textContent = `合计 ${money(fee)}`;
|
||||
const fullScore = checked.reduce((sum, input) => sum + Number(exam.subjects.find(subject => subject.id === input.value)?.fullScore || 0), 0);
|
||||
form.querySelector('[data-subject-fee]').textContent = `满分 ${fullScore} · ${money(fee)}`;
|
||||
}
|
||||
if (event.target.closest('[data-exam-subjects]') || event.target.matches('[name="passValue"]')) refreshExamScoringForm(event.target.closest('form'));
|
||||
});
|
||||
|
||||
document.addEventListener('change', event => {
|
||||
@@ -239,7 +256,23 @@ document.addEventListener('change', event => {
|
||||
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)}">${h(subject.name)}</option>`).join('')}`;
|
||||
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-subject"]')) {
|
||||
const form = event.target.closest('form');
|
||||
const option = event.target.selectedOptions[0];
|
||||
const fullScore = Number(option?.dataset.fullScore || 0);
|
||||
const passScore = Number(option?.dataset.passScore || 0);
|
||||
const scoreInput = form?.querySelector('[name="score"]');
|
||||
if (scoreInput) scoreInput.max = fullScore || '';
|
||||
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 (event.target.matches('[name="passPolicy"]')) {
|
||||
refreshExamScoringForm(event.target.closest('form'));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -358,7 +391,17 @@ document.addEventListener('submit', async event => {
|
||||
await api('/api/admin/notices', { method: 'POST', body });
|
||||
closeModal(); await refreshPublic(); toast(body.status === 'published' ? '通知已发布' : '草稿已保存', '公开首页状态已同步'); renderRoute();
|
||||
} else if (kind === 'exam-form') {
|
||||
const body = formObject(form); body.subjects = body.subjects.split(/[,,]/).map(item => item.trim()).filter(Boolean);
|
||||
const body = formObject(form);
|
||||
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),
|
||||
date: row.querySelector('[name="subjectDate"]').value,
|
||||
start: row.querySelector('[name="subjectStart"]').value,
|
||||
end: row.querySelector('[name="subjectEnd"]').value,
|
||||
fee: Number(row.querySelector('[name="subjectFee"]').value || 0)
|
||||
}));
|
||||
body.passValue = ['subject_scores', 'none'].includes(body.passPolicy) ? 0 : Number(body.passValue);
|
||||
['registrationStart','registrationEnd','examStart','examEnd','admitDownloadStart','admitDownloadEnd'].forEach(field => body[field] = new Date(body[field]).toISOString());
|
||||
const editing = Boolean(body.id);
|
||||
await api(editing ? `/api/admin/exams/${body.id}` : '/api/admin/exams', { method: editing ? 'PATCH' : 'POST', body });
|
||||
@@ -438,10 +481,48 @@ function dateTimeLocal(value) {
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
||||
}
|
||||
|
||||
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>`;
|
||||
}
|
||||
|
||||
function refreshExamScoringForm(form) {
|
||||
if (!form?.matches('[data-form="exam-form"]')) return;
|
||||
const rows = [...form.querySelectorAll('.exam-subject-editor')];
|
||||
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 valueField = form.querySelector('[data-pass-value-field]');
|
||||
const valueInput = form.querySelector('[name="passValue"]');
|
||||
const unit = form.querySelector('[data-pass-unit]');
|
||||
const hint = form.querySelector('[data-pass-hint]');
|
||||
const hiddenValue = ['subject_scores', 'none'].includes(policy);
|
||||
if (valueField) valueField.classList.toggle('hidden', hiddenValue);
|
||||
if (valueInput) {
|
||||
valueInput.disabled = hiddenValue;
|
||||
valueInput.max = policy === 'fixed_score' ? String(total || 1000) : '100';
|
||||
valueInput.min = policy === 'fixed_score' ? '0' : '0.1';
|
||||
}
|
||||
if (unit) unit.textContent = policy === 'fixed_score' ? '分' : '%';
|
||||
const hints = {
|
||||
fixed_score: '按报考科目的成绩总和判断;适合所有考生科目组合一致的考试。',
|
||||
score_ratio: '按“实得总分 ÷ 所报科目满分”判断,适合允许选科的考试。',
|
||||
rank_percent: '在相同报考科目组合且成绩完整的考生中排名,同分并列。',
|
||||
subject_scores: '每个科目都必须达到上方配置的单科合格分。',
|
||||
none: '只展示成绩、总分和得分率,不显示合格或未合格。'
|
||||
};
|
||||
if (hint) hint.textContent = hints[policy];
|
||||
}
|
||||
|
||||
function openExamForm(exam = null) {
|
||||
const editing = Boolean(exam);
|
||||
if (editing && exam.status !== 'draft') return toast('无法编辑', '请先将已发布考试撤回为草稿');
|
||||
setModal(`<div class="modal-head"><div><span>${editing ? 'EDIT EXAM DRAFT' : 'NEW EXAM'}</span><h2>${editing ? '编辑考试草稿' : '创建考试与科目'}</h2><p>${editing ? '补全考试信息和科目后,可保存草稿或直接发布。' : '创建后可选择直接发布或先保存为草稿。'}</p></div><button type="button" data-action="close-modal" aria-label="关闭弹窗">×</button></div><form class="modal-form wide-form" data-form="exam-form">${editing ? `<input type="hidden" name="id" value="${h(exam.id)}">` : ''}<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><label><span>考试科目 *</span><input name="subjects" required value="${h(exam?.subjects?.map(subject => subject.name).join(','))}" placeholder="用逗号分隔,例如:语文,数学,英语"></label><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><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>`);
|
||||
const subjects = exam?.subjects?.length ? exam.subjects : [{ date: String(exam?.examStart || '').slice(0, 10) }];
|
||||
const passPolicy = exam?.passPolicy || 'score_ratio';
|
||||
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"]'));
|
||||
}
|
||||
|
||||
function openAdmitPreview(reg) {
|
||||
|
||||
Reference in New Issue
Block a user