c
This commit is contained in:
@@ -24,14 +24,22 @@ function toast(title, message = '') {
|
||||
}
|
||||
|
||||
function setModal(content) {
|
||||
document.body.classList.remove('review-subpage-open');
|
||||
modalRoot.innerHTML = `<div class="modal-layer" data-modal-backdrop><div class="modal-card" role="dialog" aria-modal="true">${content}</div></div>`;
|
||||
setTimeout(() => modalRoot.querySelector('input,textarea,button')?.focus(), 30);
|
||||
}
|
||||
|
||||
function setReviewSubpage(content) {
|
||||
document.body.classList.add('review-subpage-open');
|
||||
modalRoot.innerHTML = `<div class="review-subpage-layer"><main class="review-subpage" role="dialog" aria-modal="true">${content}</main></div>`;
|
||||
setTimeout(() => modalRoot.querySelector('button,input,textarea,select')?.focus(), 30);
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
const editor = noticeEditor;
|
||||
noticeEditor = null;
|
||||
if (editor) editor.destroy().catch(error => console.error('CKEditor cleanup failed', error));
|
||||
document.body.classList.remove('review-subpage-open');
|
||||
modalRoot.innerHTML = '';
|
||||
}
|
||||
|
||||
@@ -104,14 +112,47 @@ function updateRegistrationSelection() {
|
||||
document.querySelectorAll('[data-action="bulk-registration-review"]').forEach(button => { button.disabled = selected.length === 0; });
|
||||
}
|
||||
|
||||
function updateCandidateSelection() {
|
||||
const table = document.querySelector('#candidateTable');
|
||||
if (!table) return;
|
||||
const selectable = [...table.querySelectorAll('[data-candidate-select]:not(:disabled)')];
|
||||
const visible = selectable.filter(input => !input.closest('tr').hidden);
|
||||
const selected = selectable.filter(input => input.checked);
|
||||
const selectAll = table.querySelector('[data-candidate-select-all]');
|
||||
if (selectAll) {
|
||||
selectAll.checked = visible.length > 0 && visible.every(input => input.checked);
|
||||
selectAll.indeterminate = visible.some(input => input.checked) && !selectAll.checked;
|
||||
}
|
||||
const count = document.querySelector('[data-candidate-selection-count]');
|
||||
if (count) count.textContent = selected.length;
|
||||
document.querySelectorAll('[data-action="bulk-candidate-review"]').forEach(button => { button.disabled = selected.length === 0; });
|
||||
}
|
||||
|
||||
function updatePaymentSelection() {
|
||||
const table = document.querySelector('#paymentTable');
|
||||
if (!table) return;
|
||||
const selectable = [...table.querySelectorAll('[data-payment-select]:not(:disabled)')];
|
||||
const visible = selectable.filter(input => !input.closest('tr').hidden);
|
||||
const selected = selectable.filter(input => input.checked);
|
||||
const selectAll = table.querySelector('[data-payment-select-all]');
|
||||
if (selectAll) {
|
||||
selectAll.checked = visible.length > 0 && visible.every(input => input.checked);
|
||||
selectAll.indeterminate = visible.some(input => input.checked) && !selectAll.checked;
|
||||
}
|
||||
const count = document.querySelector('[data-payment-selection-count]');
|
||||
if (count) count.textContent = selected.length;
|
||||
document.querySelectorAll('[data-action="bulk-payment-update"]').forEach(button => { button.disabled = selected.length === 0; });
|
||||
}
|
||||
|
||||
function updateQualificationSelection(container) {
|
||||
if (!container) return;
|
||||
const rows = [...container.querySelectorAll('[data-qualification-select]')];
|
||||
const visible = rows.filter(input => !input.closest('tr').hidden);
|
||||
const selected = rows.filter(input => input.checked);
|
||||
const selectAll = container.querySelector('[data-action="qualification-select-all"]');
|
||||
if (selectAll) {
|
||||
selectAll.checked = rows.length > 0 && selected.length === rows.length;
|
||||
selectAll.indeterminate = selected.length > 0 && selected.length < rows.length;
|
||||
selectAll.checked = visible.length > 0 && visible.every(input => input.checked);
|
||||
selectAll.indeterminate = visible.some(input => input.checked) && !selectAll.checked;
|
||||
}
|
||||
const count = container.querySelector('[data-qualification-selected-count]');
|
||||
if (count) count.textContent = `已选 ${selected.length} 人`;
|
||||
@@ -124,13 +165,16 @@ function applyTableFilters(tableId) {
|
||||
const query = search?.value.trim().toLowerCase() || '';
|
||||
const status = [...document.querySelectorAll('[data-action="status-filter"]')].find(button => button.dataset.target === tableId && button.classList.contains('active'))?.dataset.status || 'all';
|
||||
const filters = [...document.querySelectorAll('[data-table-filter]')].filter(select => select.dataset.target === tableId && select.value);
|
||||
table.querySelectorAll('tbody tr[data-filter-row], tbody tr[data-status]').forEach(row => {
|
||||
table.querySelectorAll('[data-filter-row], tbody tr[data-status]').forEach(row => {
|
||||
const matchesSearch = !query || row.textContent.toLowerCase().includes(query);
|
||||
const matchesStatus = status === 'all' || String(row.dataset.status || '').split(/\s+/).includes(status);
|
||||
const matchesFilters = filters.every(select => String(row.dataset[select.dataset.tableFilter] || '') === select.value);
|
||||
const matchesFilters = filters.every(select => String(row.dataset[select.dataset.tableFilter] || '').split('|').includes(select.value));
|
||||
row.hidden = !(matchesSearch && matchesStatus && matchesFilters);
|
||||
});
|
||||
if (tableId === 'registrationTable') updateRegistrationSelection();
|
||||
if (tableId === 'candidateTable') updateCandidateSelection();
|
||||
if (tableId === 'paymentTable') updatePaymentSelection();
|
||||
if (table.closest('.qualification-ledger')) updateQualificationSelection(table.closest('.qualification-ledger'));
|
||||
}
|
||||
|
||||
async function refreshPublic() {
|
||||
@@ -257,10 +301,20 @@ document.addEventListener('click', async event => {
|
||||
}
|
||||
if (action === 'qualification-select-all') {
|
||||
const container = target.closest('[data-qualification-bulk]')?.closest('.qualification-ledger');
|
||||
container?.querySelectorAll('[data-qualification-select]').forEach(input => { input.checked = target.checked; });
|
||||
container?.querySelectorAll('[data-qualification-select]').forEach(input => {
|
||||
if (!input.closest('tr').hidden) input.checked = target.checked;
|
||||
});
|
||||
updateQualificationSelection(container);
|
||||
return;
|
||||
}
|
||||
if (action === 'clear-table-filters') {
|
||||
const tableId = target.dataset.target;
|
||||
document.querySelectorAll(`[data-table-filter][data-target="${tableId}"]`).forEach(select => { select.value = ''; });
|
||||
document.querySelectorAll(`[data-action="table-search"][data-target="${tableId}"]`).forEach(input => { input.value = ''; });
|
||||
document.querySelectorAll(`[data-action="status-filter"][data-target="${tableId}"]`).forEach((button, index) => button.classList.toggle('active', index === 0));
|
||||
applyTableFilters(tableId);
|
||||
return;
|
||||
}
|
||||
if (action === 'bulk-indicator-qualification') {
|
||||
const toolbar = target.closest('[data-qualification-bulk]');
|
||||
const ledger = toolbar?.closest('.qualification-ledger');
|
||||
@@ -405,6 +459,24 @@ document.addEventListener('click', async event => {
|
||||
toast(status === 'rejected' ? `已退回 ${completed} 条报名` : `已处理 ${completed} 条报名`, failures.length ? `${failures.length} 条未完成,请刷新后重试` : '报名状态与流程轨迹已同步更新');
|
||||
return renderRoute();
|
||||
}
|
||||
if (action === 'bulk-candidate-review') {
|
||||
const ids = [...document.querySelectorAll('#candidateTable [data-candidate-select]:checked')].map(input => input.value);
|
||||
if (!ids.length) return toast('请先选择考生', '仅当前步骤可由你处理的资料可以勾选');
|
||||
const status = target.dataset.status;
|
||||
const reviewNote = window.prompt(status === 'rejected' ? '请填写批量退回原因(必填)' : '填写批量审核意见(可留空)', '');
|
||||
if (reviewNote === null) return;
|
||||
if (status === 'rejected' && !reviewNote.trim()) return toast('请填写退回原因', '考生需要根据原因补充或修改资料');
|
||||
let completed = 0;
|
||||
const failures = [];
|
||||
for (const id of ids) {
|
||||
try {
|
||||
await api(`/api/admin/candidates/${id}`, { method: 'PATCH', body: { status, reviewNote } });
|
||||
completed += 1;
|
||||
} catch (error) { failures.push(error.message); }
|
||||
}
|
||||
toast(status === 'rejected' ? `已退回 ${completed} 名考生资料` : `已处理 ${completed} 名考生资料`, failures.length ? `${failures.length} 名未完成,请检查其流程责任人` : '资料状态与流程轨迹已同步更新');
|
||||
return renderRoute();
|
||||
}
|
||||
if (action === 'confirm-payment') {
|
||||
if (!window.confirm(`确认已收到 ${target.dataset.name} 的“${target.dataset.exam}”考试费用吗?`)) return;
|
||||
await api(`/api/admin/payments/${target.dataset.id}`, { method: 'PATCH' });
|
||||
@@ -421,6 +493,25 @@ document.addEventListener('click', async event => {
|
||||
toast(paid ? '已标记为已缴费' : '已改为待缴费', paid ? '确认人和确认时间已同步记录' : '原确认人和确认时间已清除');
|
||||
return renderRoute();
|
||||
}
|
||||
if (action === 'bulk-payment-update') {
|
||||
const selected = [...document.querySelectorAll('#paymentTable [data-payment-select]:checked')];
|
||||
if (!selected.length) return toast('请先选择缴费记录');
|
||||
const status = target.dataset.status;
|
||||
const changed = selected.filter(input => input.dataset.status !== status);
|
||||
const skipped = selected.length - changed.length;
|
||||
if (!changed.length) return toast('所选记录无需更改', status === 'paid' ? '所选考生都已缴费' : '所选考生都处于待缴费状态');
|
||||
if (!window.confirm(`确认将 ${changed.length} 条缴费记录批量改为“${status === 'paid' ? '已缴费' : '待缴费'}”吗?`)) return;
|
||||
let completed = 0;
|
||||
const failures = [];
|
||||
for (const input of changed) {
|
||||
try {
|
||||
await api(`/api/admin/payments/${input.value}`, { method: 'PATCH', body: { status } });
|
||||
completed += 1;
|
||||
} catch (error) { failures.push(error.message); }
|
||||
}
|
||||
toast(`已更新 ${completed} 条缴费记录`, [skipped ? `${skipped} 条状态相同已跳过` : '', failures.length ? `${failures.length} 条未完成` : ''].filter(Boolean).join(';'));
|
||||
return renderRoute();
|
||||
}
|
||||
if (action === 'excel-download') {
|
||||
const query = new URLSearchParams();
|
||||
if (target.dataset.template === '1') query.set('template', '1');
|
||||
@@ -510,6 +601,8 @@ document.addEventListener('change', event => {
|
||||
if (event.target.matches('[data-qualification-select]')) updateQualificationSelection(event.target.closest('.qualification-ledger'));
|
||||
if (event.target.matches('[data-table-filter]')) applyTableFilters(event.target.dataset.target);
|
||||
if (event.target.matches('[data-registration-select]')) updateRegistrationSelection();
|
||||
if (event.target.matches('[data-candidate-select]')) updateCandidateSelection();
|
||||
if (event.target.matches('[data-payment-select]')) updatePaymentSelection();
|
||||
if (event.target.matches('[data-registration-select-all]')) {
|
||||
const table = document.getElementById(event.target.dataset.target);
|
||||
table?.querySelectorAll('[data-registration-select]:not(:disabled)').forEach(input => {
|
||||
@@ -517,6 +610,20 @@ document.addEventListener('change', event => {
|
||||
});
|
||||
updateRegistrationSelection();
|
||||
}
|
||||
if (event.target.matches('[data-candidate-select-all]')) {
|
||||
const table = document.getElementById(event.target.dataset.target);
|
||||
table?.querySelectorAll('[data-candidate-select]:not(:disabled)').forEach(input => {
|
||||
if (!input.closest('tr').hidden) input.checked = event.target.checked;
|
||||
});
|
||||
updateCandidateSelection();
|
||||
}
|
||||
if (event.target.matches('[data-payment-select-all]')) {
|
||||
const table = document.getElementById(event.target.dataset.target);
|
||||
table?.querySelectorAll('[data-payment-select]:not(:disabled)').forEach(input => {
|
||||
if (!input.closest('tr').hidden) input.checked = event.target.checked;
|
||||
});
|
||||
updatePaymentSelection();
|
||||
}
|
||||
if (event.target.matches('[data-excel-file]')) {
|
||||
const input = event.target;
|
||||
const file = input.files?.[0];
|
||||
@@ -919,8 +1026,11 @@ function openFlowDetail(id) {
|
||||
function openCandidateReview(id) {
|
||||
const source = state.pageData.candidates.find(candidate => candidate.id === id);
|
||||
const item = { ...source, address: formatRegionAddress(source) };
|
||||
const canReview = item.status === 'pending' && item.workflow?.assignee?.id === state.user.id;
|
||||
setModal(`<div class="modal-head"><div><span>CANDIDATE REVIEW</span><h2>${canReview ? '处理' : '查看'} ${h(item.name)} 的资料</h2><p class="mono">报名号 ${h(item.candidateNumber)} · 更新于 ${formatDate(item.updatedAt,true)}</p></div><button data-action="close-modal">×</button></div><div class="review-profile"><dl><div><dt>证件号码</dt><dd class="mono">${h(item.idNumberMasked)}</dd></div><div><dt>性别 / 籍贯</dt><dd>${h(item.gender)} · ${h(item.nativePlace)}</dd></div><div><dt>联系电话</dt><dd>${h(item.phone)}</dd></div><div><dt>电子邮箱</dt><dd>${h(item.email)}</dd></div><div><dt>就读学校</dt><dd>${h(item.school)}</dd></div><div><dt>年级班级</dt><dd>${h(item.grade)}</dd></div><div><dt>家庭住址</dt><dd>${h(item.address)}</dd></div><div><dt>监护人</dt><dd>${h(item.guardianName || item.emergencyContact)} · ${h(item.guardianPhone || item.emergencyPhone)}</dd></div><div><dt>当前步骤</dt><dd>${h(item.workflow?.currentStepDetail?.name || '流程已结束')}</dd></div><div><dt>当前责任人</dt><dd>${h(item.workflow?.assignee?.displayName || '—')}</dd></div></dl></div>${canReview ? `<form class="modal-form" data-form="candidate-review"><input type="hidden" name="id" value="${h(item.id)}"><label><span>审核结论</span><select name="status"><option value="approved">通过当前步骤</option><option value="rejected">退回考生修改</option></select></label><label><span>审核意见</span><textarea name="reviewNote" rows="3" placeholder="填写核验说明或需要补充的资料">${h(item.reviewNote)}</textarea></label><div class="modal-foot"><button type="button" class="ghost-button" data-action="close-modal">取消</button><button type="submit" class="solid-button">确认处理</button></div></form>` : '<div class="modal-foot"><button class="solid-button" data-action="close-modal">关闭</button></div>'}`);
|
||||
const canReview = item.status === 'pending' && (item.workflow?.assignee?.id === state.user.id || state.user.adminLevel === 'super');
|
||||
const registrations = item.registrations || [];
|
||||
const examCards = registrations.map(registration => `<article class="review-exam-card"><header><div><span class="exam-code">${h(registration.exam?.code || '')}</span><h3>${h(registration.exam?.name || '考试信息异常')}</h3></div>${badge(registration.exam?.archivedAt ? 'archived' : registration.status)}</header><dl><div><dt>考试时间</dt><dd>${dateRange(registration.exam?.examStart, registration.exam?.examEnd)}</dd></div><div><dt>考试地点</dt><dd>${h(registration.exam?.location || '待公布')}</dd></div><div><dt>报名科目</dt><dd><div class="review-subject-list">${registration.subjects?.map(subject => `<span><strong>${h(subject.name)}</strong><small>${h(subject.date || '')} ${h(subject.start || '')} · 满分 ${h(subject.fullScore)} · ${money(subject.fee || 0)}</small></span>`).join('') || '<em>未选择科目</em>'}</div></dd></div><div><dt>报名 / 缴费</dt><dd>${badge(registration.status)} ${badge(registration.paymentStatus)} · 应缴 ${money(registration.amountDue || 0)}</dd></div></dl></article>`).join('');
|
||||
const decision = canReview ? `<form class="review-decision-form" data-form="candidate-review"><input type="hidden" name="id" value="${h(item.id)}"><div><span>当前审核步骤</span><strong>${h(item.workflow?.currentStepDetail?.name || '流程已结束')}</strong><small>责任人:${h(item.workflow?.assignee?.displayName || '—')}</small></div><label><span>审核结论</span><select name="status"><option value="approved">通过当前步骤</option><option value="rejected">退回考生修改</option></select></label><label><span>审核意见</span><textarea name="reviewNote" rows="5" placeholder="填写核验说明;退回时请明确指出需要补充或修改的资料">${h(item.reviewNote || '')}</textarea></label><button type="submit" class="solid-button">确认处理</button></form>` : `<aside class="review-readonly"><strong>当前为只读查看</strong><p>${item.status === 'pending' ? `本步骤由 ${h(item.workflow?.assignee?.displayName || '其他管理员')} 处理。` : `资料状态:${h(statusLabels[item.status] || item.status)}`}</p><button class="ghost-button" data-action="close-modal">返回列表</button></aside>`;
|
||||
setReviewSubpage(`<header class="review-subpage-header"><button class="review-back" data-action="close-modal">← 返回考生资料审核</button><div><span>CANDIDATE DOSSIER</span><h1>${h(item.name)} · 资料审核</h1><p><b class="mono">${h(item.candidateNumber)}</b><i></i>${h(item.school || '学校未填写')} · ${h(item.grade || '班级未填写')}<i></i>更新于 ${formatDate(item.updatedAt, true)}</p></div>${badge(item.status)}</header><div class="review-subpage-layout"><div class="review-subpage-main"><section class="review-section"><header><span>01</span><div><h2>身份与学籍信息</h2><p>核对实名、学籍范围以及联系方式。</p></div></header><dl class="review-detail-grid"><div><dt>证件号码</dt><dd class="mono">${h(item.idNumberMasked)}</dd></div><div><dt>性别 / 出生日期</dt><dd>${h(item.gender || '未填写')} · ${h(item.birthDate || '未填写')}</dd></div><div><dt>籍贯 / 民族</dt><dd>${h(item.nativePlace || '未填写')} · ${h(item.ethnicity || '未填写')}</dd></div><div><dt>就读学校 / 班级</dt><dd>${h(item.school || '未填写')} · ${h(item.grade || '未填写')}</dd></div><div><dt>联系电话</dt><dd>${h(item.phone || '未填写')}</dd></div><div><dt>电子邮箱</dt><dd>${h(item.email || '未填写')}</dd></div><div class="wide"><dt>家庭住址</dt><dd>${h(item.address || '未填写')}</dd></div><div><dt>监护人</dt><dd>${h(item.guardianName || '未填写')} · ${h(item.guardianPhone || '电话未填写')}</dd></div><div><dt>紧急联系人</dt><dd>${h(item.emergencyContact || '未填写')} · ${h(item.emergencyPhone || '电话未填写')}</dd></div></dl></section><section class="review-section"><header><span>02</span><div><h2>关联考试与报名科目</h2><p>审核资料时同时查看该考生历次报名上下文。</p></div><strong>${registrations.length} 场</strong></header><div class="review-exam-list">${examCards || '<div class="review-empty-context"><strong>暂无考试报名</strong><p>该考生当前尚未提交考试报名;资料审核通过后才能选择考试与科目。</p></div>'}</div></section></div>${decision}</div>`);
|
||||
}
|
||||
|
||||
function openCandidatePasswordReset(id) {
|
||||
@@ -932,7 +1042,10 @@ function openCandidatePasswordReset(id) {
|
||||
function openRegistrationReview(id) {
|
||||
const reg = state.pageData.registrations.find(item => item.id === id);
|
||||
const canReview = reg.status === 'pending' && (reg.workflow?.assignee?.id === state.user.id || state.user.adminLevel === 'super');
|
||||
setModal(`<div class="modal-head"><div><span>REGISTRATION REVIEW</span><h2>${canReview ? '处理' : '查看'}考试报名</h2><p>${h(reg.candidate?.name)} · ${h(reg.exam.name)}</p></div><button data-action="close-modal">×</button></div><div class="registration-review"><div><span>报考科目</span><p>${reg.subjects.map(subject => `<b>${h(subject.name)}</b>`).join('')}</p></div><dl><div><dt>账户报名号</dt><dd class="mono">${h(reg.registrationNumber || reg.candidate?.candidateNumber || '账户号码异常')}</dd></div><div><dt>当前步骤</dt><dd>${h(reg.workflow?.currentStepDetail?.name || '流程已结束')}</dd></div><div><dt>责任人</dt><dd>${h(reg.workflow?.assignee?.displayName || '—')}</dd></div><div><dt>缴费状态</dt><dd>${badge(reg.paymentStatus)}</dd></div></dl></div>${canReview ? `<form class="modal-form" data-form="registration-review"><input type="hidden" name="id" value="${h(reg.id)}"><label><span>审核结论</span><select name="status"><option value="approved">通过当前步骤</option><option value="rejected">退回报名</option></select></label><label><span>审核意见</span><textarea name="reviewNote" rows="3" placeholder="可填写审核说明">${h(reg.reviewNote || '')}</textarea></label><div class="modal-foot"><button type="button" class="ghost-button" data-action="close-modal">取消</button><button type="submit" class="solid-button">确认处理</button></div></form>` : '<div class="modal-foot"><button class="solid-button" data-action="close-modal">关闭</button></div>'}`);
|
||||
const subjects = reg.subjects || [];
|
||||
const subjectCards = subjects.map((subject, index) => `<article><i>${index + 1}</i><div><strong>${h(subject.name)}</strong><span>${h(subject.date || '日期待定')} ${h(subject.start || '')}${subject.end ? `—${h(subject.end)}` : ''}</span></div><dl><div><dt>满分</dt><dd>${h(subject.fullScore)} 分</dd></div><div><dt>报名费</dt><dd>${money(subject.fee || 0)}</dd></div></dl></article>`).join('');
|
||||
const decision = canReview ? `<form class="review-decision-form" data-form="registration-review"><input type="hidden" name="id" value="${h(reg.id)}"><div><span>当前审核步骤</span><strong>${h(reg.workflow?.currentStepDetail?.name || '流程已结束')}</strong><small>责任人:${h(reg.workflow?.assignee?.displayName || '—')}</small></div><label><span>审核结论</span><select name="status"><option value="approved">通过当前步骤</option><option value="rejected">退回本次报名</option></select></label><label><span>审核意见</span><textarea name="reviewNote" rows="5" placeholder="填写审核依据;退回时请说明考试或科目问题">${h(reg.reviewNote || '')}</textarea></label><button type="submit" class="solid-button">确认处理</button></form>` : `<aside class="review-readonly"><strong>当前为只读查看</strong><p>${reg.status === 'pending' ? `本步骤由 ${h(reg.workflow?.assignee?.displayName || '其他管理员')} 处理。` : `报名状态:${h(statusLabels[reg.status] || reg.status)}`}</p><button class="ghost-button" data-action="close-modal">返回列表</button></aside>`;
|
||||
setReviewSubpage(`<header class="review-subpage-header"><button class="review-back" data-action="close-modal">← 返回报名审核</button><div><span>REGISTRATION DOSSIER</span><h1>${h(reg.candidate?.name)} · 报名审核</h1><p><b class="mono">${h(reg.registrationNumber || reg.candidate?.candidateNumber || '号码待同步')}</b><i></i>${h(reg.schoolName || reg.candidate?.school || '')} · ${h([reg.gradeName, reg.className].filter(Boolean).join(' · '))}</p></div>${badge(reg.status)}</header><div class="review-subpage-layout"><div class="review-subpage-main"><section class="review-section exam-context-hero"><header><span>考试</span><div><p class="exam-code">${h(reg.exam.code)}</p><h2>${h(reg.exam.name)}</h2></div></header><dl class="review-detail-grid"><div><dt>报名时间</dt><dd>${dateRange(reg.exam.registrationStart, reg.exam.registrationEnd)}</dd></div><div><dt>考试时间</dt><dd>${dateRange(reg.exam.examStart, reg.exam.examEnd)}</dd></div><div><dt>考试地点</dt><dd>${h(reg.exam.location || '待公布')}</dd></div><div><dt>本次报名</dt><dd><strong>${subjects.length} 个科目 · 应缴 ${money(reg.amountDue || 0)}</strong></dd></div><div><dt>缴费状态</dt><dd>${badge(reg.paymentStatus)}</dd></div><div><dt>账户报名号</dt><dd class="mono">${h(reg.registrationNumber || reg.candidate?.candidateNumber || '待同步')}</dd></div></dl></section><section class="review-section subject-review-section"><header><span>科目</span><div><h2>本次所报科目</h2><p>逐科核对日期、时间、满分和报名费用。</p></div><strong>${subjects.length} 科</strong></header><div class="review-subject-cards">${subjectCards || '<div class="review-empty-context"><strong>未选择任何科目</strong><p>该报名记录数据异常,不应通过审核。</p></div>'}</div><footer><span>费用合计</span><strong>${money(reg.amountDue || 0)}</strong></footer></section><section class="review-section"><header><span>考生</span><div><h2>考生资料摘要</h2><p>报名审核同时核验身份与学校范围。</p></div></header><dl class="review-detail-grid"><div><dt>姓名 / 性别</dt><dd>${h(reg.candidate?.name || '未填写')} · ${h(reg.candidate?.gender || '未填写')}</dd></div><div><dt>证件号码</dt><dd class="mono">${h(reg.candidate?.idNumber || '未填写')}</dd></div><div><dt>学校 / 班级</dt><dd>${h(reg.schoolName || '')} · ${h([reg.gradeName, reg.className].filter(Boolean).join(' · '))}</dd></div><div><dt>联系电话</dt><dd>${h(reg.candidate?.phone || '未填写')}</dd></div></dl></section></div>${decision}</div>`);
|
||||
}
|
||||
|
||||
async function openNoticeForm() {
|
||||
|
||||
+43
-15
@@ -106,29 +106,51 @@ export function createAdminViews(context) {
|
||||
|
||||
function adminCandidates(data) {
|
||||
const candidates = data.candidates || [];
|
||||
const readOnly = false;
|
||||
const canReview = state.user.adminLevel === 'super' || state.permissions?.includes('candidates.review');
|
||||
const classById = new Map((data.classes || []).map(item => [item.id, item]));
|
||||
const optionList = getter => [...new Set(candidates.map(getter).filter(Boolean))]
|
||||
.sort((left, right) => left.localeCompare(right, 'zh-CN'))
|
||||
.map(value => `<option value="${h(value)}">${h(value)}</option>`).join('');
|
||||
const archiveConsole = state.user.adminLevel === 'school' ? (() => {
|
||||
const classes = data.classes || [];
|
||||
const grades = [...new Set(classes.map(item => item.grade))];
|
||||
return `<section class="panel candidate-archive-console"><div><span>SCHOOL ACCOUNT ARCHIVE</span><h2>按班级或年级归档账户</h2><p>归档只冻结登录,不删除考生、报名、准考证、成绩和审计记录;可随时按相同范围恢复。</p></div><div class="archive-controls"><select data-archive-scope><option value="">请选择范围</option><optgroup label="按班级">${classes.map(item => `<option value="class:${h(item.id)}">${h(item.grade)} · ${h(item.name)}</option>`).join('')}</optgroup><optgroup label="按年级">${grades.map(grade => `<option value="grade:${h(grade)}">${h(grade)}全部班级</option>`).join('')}</optgroup></select><button class="ghost-button" data-action="candidate-archive" data-archived="false">恢复账户</button><button class="solid-button archive-button" data-action="candidate-archive" data-archived="true">归档账户</button></div></section>`;
|
||||
})() : '';
|
||||
return `${archiveConsole}${excelToolbar('candidates', { importable: !readOnly, label: '考生资料' })}<section class="panel data-panel"><div class="data-toolbar"><label class="search-box">${icons.search}<input data-action="table-search" data-target="candidateTable" placeholder="搜索报名号、姓名、证件号或学校"></label><div class="filter-pills"><button class="active" data-action="status-filter" data-target="candidateTable" data-status="all">全部</button><button data-action="status-filter" data-target="candidateTable" data-status="pending">待审核</button><button data-action="status-filter" data-target="candidateTable" data-status="approved">已通过</button><button data-action="status-filter" data-target="candidateTable" data-status="rejected">需修改</button><button data-action="status-filter" data-target="candidateTable" data-status="archived">已归档</button></div></div><div class="table-scroll"><table id="candidateTable"><thead><tr><th>报名号 / 考生</th><th>证件号码</th><th>学校 / 班级</th><th>账户状态</th><th>更新时间</th><th>资料状态</th><th>操作</th></tr></thead><tbody>${candidates.map(item => `<tr class="${item.accountArchived ? 'account-archived-row' : ''}" data-status="${h(item.status)} ${item.accountArchived ? 'archived' : 'active'}"><td><div class="person-cell"><span>${h(item.name.slice(0,1))}</span><div><strong>${h(item.name)}</strong><small class="mono">${h(item.candidateNumber || '待分配')}</small></div></div></td><td class="mono">${h(item.idNumberMasked)}</td><td><strong>${h(item.school || '未填写')}</strong><small>${h(item.grade || '')}</small></td><td><strong>${item.accountArchived ? '已归档' : item.mustChangePassword ? '待首次改密' : item.profileCompleted ? '正常' : '待补全资料'}</strong><small>${item.accountArchived ? `${formatDate(item.archivedAt, true)} · ${h(item.archivedByName || '校方')}` : h(item.workflow?.assignee?.displayName || '')}</small></td><td>${formatDate(item.updatedAt,true)}</td><td>${item.profileCompleted ? badge(item.status) : '<span class="onboarding-badge">未完成</span>'}</td><td><div class="candidate-account-actions"><button class="row-action" data-action="review-candidate" data-id="${h(item.id)}" ${item.profileCompleted && !item.accountArchived ? '' : 'disabled'}>${item.profileCompleted ? (readOnly ? '查看' : '查看流程') : '等待考生'}</button>${state.user.adminLevel === 'super' ? `<button class="row-action primary" data-action="reset-candidate-password" data-id="${h(item.id)}" ${item.accountArchived ? 'disabled' : ''}>重置密码</button>` : ''}</div></td></tr>`).join('')}</tbody></table></div></section>`;
|
||||
const rows = candidates.map(item => {
|
||||
const schoolClass = classById.get(item.classId);
|
||||
const grade = schoolClass?.grade || '';
|
||||
const className = schoolClass?.name || item.grade || '';
|
||||
const exams = item.registrations || [];
|
||||
const canSelect = canReview && item.profileCompleted && !item.accountArchived && item.status === 'pending' && item.workflow?.status === 'pending'
|
||||
&& (state.user.adminLevel === 'super' || item.workflow?.assignee?.id === state.user.id);
|
||||
const examNames = exams.map(registration => registration.exam?.name).filter(Boolean);
|
||||
return `<tr class="${item.accountArchived ? 'account-archived-row' : ''}" data-filter-row data-status="${h(item.status)} ${item.accountArchived ? 'archived' : 'active'}" data-school="${h(item.school || '')}" data-grade="${h(grade)}" data-class="${h(className)}" data-exam="${h(examNames.join('|'))}"><td class="selection-cell"><input type="checkbox" data-candidate-select value="${h(item.id)}" aria-label="选择 ${h(item.name)}" ${canSelect ? '' : 'disabled'}></td><td><div class="person-cell"><span>${h(item.name.slice(0,1))}</span><div><strong>${h(item.name)}</strong><small class="mono">${h(item.candidateNumber || '待分配')}</small></div></div></td><td class="mono">${h(item.idNumberMasked)}</td><td><strong>${h(item.school || '未填写')}</strong><small>${h([grade, className].filter(Boolean).join(' · '))}</small></td><td><div class="candidate-exam-summary">${exams.length ? `<strong>${h(examNames.slice(0, 2).join('、'))}${exams.length > 2 ? ` 等 ${exams.length} 场` : ''}</strong><small>${h(exams.flatMap(registration => registration.subjects || []).map(subject => subject.name).slice(0, 5).join('、') || '尚未选择科目')}</small>` : '<span>暂无考试报名</span>'}</div></td><td><strong>${item.accountArchived ? '已归档' : item.mustChangePassword ? '待首次改密' : item.profileCompleted ? '正常' : '待补全资料'}</strong><small>${item.accountArchived ? `${formatDate(item.archivedAt, true)} · ${h(item.archivedByName || '校方')}` : h(item.workflow?.assignee?.displayName || '')}</small></td><td>${item.profileCompleted ? badge(item.status) : '<span class="onboarding-badge">未完成</span>'}</td><td><div class="candidate-account-actions"><button class="row-action primary" data-action="review-candidate" data-id="${h(item.id)}" ${item.profileCompleted && !item.accountArchived ? '' : 'disabled'}>${item.profileCompleted ? '进入审核页' : '等待考生'}</button>${state.user.adminLevel === 'super' ? `<button class="row-action" data-action="reset-candidate-password" data-id="${h(item.id)}" ${item.accountArchived ? 'disabled' : ''}>重置密码</button>` : ''}</div></td></tr>`;
|
||||
}).join('');
|
||||
const bulkToolbar = canReview ? `<div class="registration-bulk-bar candidate-bulk-bar"><span>已选择 <strong data-candidate-selection-count>0</strong> 名当前可处理考生</span><div><button class="row-action" data-action="bulk-candidate-review" data-status="rejected" disabled>批量退回修改</button><button class="row-action primary" data-action="bulk-candidate-review" data-status="approved" disabled>批量通过当前步骤</button></div></div>` : '';
|
||||
const examOptions = [...new Set(candidates.flatMap(item => (item.registrations || []).map(registration => registration.exam?.name).filter(Boolean)))]
|
||||
.sort((left, right) => left.localeCompare(right, 'zh-CN'))
|
||||
.map(value => `<option value="${h(value)}">${h(value)}</option>`).join('');
|
||||
const filters = `<div class="table-filter-selects"><select data-table-filter="school" data-target="candidateTable"><option value="">全部学校</option>${optionList(item => item.school)}</select><select data-table-filter="grade" data-target="candidateTable"><option value="">全部年级</option>${optionList(item => classById.get(item.classId)?.grade)}</select><select data-table-filter="class" data-target="candidateTable"><option value="">全部班级</option>${optionList(item => classById.get(item.classId)?.name || item.grade)}</select><select data-table-filter="exam" data-target="candidateTable"><option value="">全部关联考试</option>${examOptions}</select></div>`;
|
||||
return `${archiveConsole}${excelToolbar('candidates', { importable: true, label: '考生资料' })}<section class="panel data-panel candidate-ledger">${bulkToolbar}<div class="data-toolbar candidate-toolbar"><label class="search-box">${icons.search}<input data-action="table-search" data-target="candidateTable" placeholder="搜索报名号、姓名、证件号、考试或学校"></label><div class="filter-pills"><button class="active" data-action="status-filter" data-target="candidateTable" data-status="all">全部</button><button data-action="status-filter" data-target="candidateTable" data-status="pending">待审核</button><button data-action="status-filter" data-target="candidateTable" data-status="approved">已通过</button><button data-action="status-filter" data-target="candidateTable" data-status="rejected">需修改</button><button data-action="status-filter" data-target="candidateTable" data-status="archived">已归档</button></div>${filters}</div><div class="table-scroll"><table id="candidateTable"><thead><tr><th class="selection-cell"><input type="checkbox" data-candidate-select-all data-target="candidateTable" aria-label="全选当前筛选结果"></th><th>报名号 / 考生</th><th>证件号码</th><th>学校 / 年级 / 班级</th><th>关联考试 / 科目</th><th>账户状态</th><th>资料状态</th><th>操作</th></tr></thead><tbody>${rows || '<tr><td colspan="8" class="empty-state">当前范围暂无考生</td></tr>'}</tbody></table></div></section>`;
|
||||
}
|
||||
|
||||
function adminRegistrations(registrations) {
|
||||
const isSuper = state.user.adminLevel === 'super';
|
||||
const canReview = state.user.adminLevel === 'super' || state.permissions?.includes('registrations.review');
|
||||
const optionList = (getter) => [...new Set(registrations.map(getter).filter(Boolean))]
|
||||
.sort((left, right) => left.localeCompare(right, 'zh-CN'))
|
||||
.map(value => `<option value="${h(value)}">${h(value)}</option>`).join('');
|
||||
const rows = (items, selectable = false) => items.map(reg => {
|
||||
const canSelect = selectable && reg.status === 'pending' && reg.workflow?.status === 'pending';
|
||||
return `<tr data-filter-row data-status="${h(reg.status)}" data-exam="${h(reg.exam?.name || '')}" data-school="${h(reg.schoolName || '')}" data-grade="${h(reg.gradeName || '')}" data-class="${h(reg.className || '')}">${selectable ? `<td class="selection-cell"><input type="checkbox" data-registration-select value="${h(reg.id)}" aria-label="选择 ${h(reg.candidate?.name || '考生')}" ${canSelect ? '' : 'disabled'}></td>` : ''}<td><div class="person-cell"><span>${h((reg.candidate?.name || '?').slice(0,1))}</span><div><strong>${h(reg.candidate?.name)}</strong><small>${h([reg.schoolName, reg.gradeName, reg.className].filter(Boolean).join(' · '))}</small></div></div></td><td><strong>${h(reg.exam.name)}</strong><small>${reg.subjects.map(subject => h(subject.name)).join('、')}</small></td><td class="mono"><strong>${h(reg.registrationNumber || '待同步账户号码')}</strong><small>各次考试保持一致</small></td><td><strong>${h(reg.workflow?.currentStepDetail?.name || '流程已结束')}</strong><small>${h(reg.workflow?.assignee?.displayName || '')}</small></td><td>${badge(reg.paymentStatus)}</td><td>${badge(reg.exam.archivedAt ? 'archived' : reg.status)}</td><td>${reg.exam.archivedAt ? '<span class="archive-readonly-label">只读封存</span>' : `<button class="row-action" data-action="review-registration" data-id="${h(reg.id)}">查看流程</button>`}</td></tr>`;
|
||||
const canSelect = selectable && reg.status === 'pending' && reg.workflow?.status === 'pending'
|
||||
&& (isSuper || reg.workflow?.assignee?.id === state.user.id);
|
||||
return `<tr data-filter-row data-status="${h(reg.status)}" data-exam="${h(reg.exam?.name || '')}" data-subject="${h(reg.subjects.map(subject => subject.name).join('|'))}" data-school="${h(reg.schoolName || '')}" data-grade="${h(reg.gradeName || '')}" data-class="${h(reg.className || '')}">${selectable ? `<td class="selection-cell"><input type="checkbox" data-registration-select value="${h(reg.id)}" aria-label="选择 ${h(reg.candidate?.name || '考生')}" ${canSelect ? '' : 'disabled'}></td>` : ''}<td><div class="person-cell"><span>${h((reg.candidate?.name || '?').slice(0,1))}</span><div><strong>${h(reg.candidate?.name)}</strong><small>${h([reg.schoolName, reg.gradeName, reg.className].filter(Boolean).join(' · '))}</small></div></div></td><td><div class="registration-exam-cell"><strong>${h(reg.exam.name)}</strong><div class="subject-summary">${reg.subjects.map(subject => `<span>${h(subject.name)}</span>`).join('') || '<em>未选择科目</em>'}</div><small>${h(reg.subjects.length)} 科 · ${money(reg.amountDue || 0)}</small></div></td><td class="mono"><strong>${h(reg.registrationNumber || '待同步账户号码')}</strong><small>各次考试保持一致</small></td><td><strong>${h(reg.workflow?.currentStepDetail?.name || '流程已结束')}</strong><small>${h(reg.workflow?.assignee?.displayName || '')}</small></td><td>${badge(reg.paymentStatus)}</td><td>${badge(reg.exam.archivedAt ? 'archived' : reg.status)}</td><td>${reg.exam.archivedAt ? '<span class="archive-readonly-label">只读封存</span>' : `<button class="row-action primary" data-action="review-registration" data-id="${h(reg.id)}">进入审核页</button>`}</td></tr>`;
|
||||
}).join('');
|
||||
const table = (items, id, selectable = false) => `<div class="table-scroll"><table id="${id}"><thead><tr>${selectable ? '<th class="selection-cell"><input type="checkbox" data-registration-select-all data-target="registrationTable" aria-label="全选当前筛选结果"></th>' : ''}<th>考生</th><th>考试 / 科目</th><th>账户报名号</th><th>当前流程</th><th>缴费</th><th>状态</th><th>操作</th></tr></thead><tbody>${rows(items, selectable) || `<tr><td colspan="${selectable ? 8 : 7}" class="empty-state">当前范围暂无报名记录</td></tr>`}</tbody></table></div>`;
|
||||
const current = registrations.filter(reg => !reg.exam.archivedAt);
|
||||
const archived = registrations.filter(reg => reg.exam.archivedAt);
|
||||
const bulkToolbar = isSuper ? `<div class="registration-bulk-bar"><span>已选择 <strong data-registration-selection-count>0</strong> 条待审核报名</span><div><button class="row-action" data-action="bulk-registration-review" data-status="rejected" disabled>批量退回</button><button class="row-action primary" data-action="bulk-registration-review" data-status="approved" disabled>批量通过当前步骤</button></div></div>` : '';
|
||||
return `<section class="panel data-panel registration-ledger">${bulkToolbar}<div class="data-toolbar registration-toolbar"><label class="search-box">${icons.search}<input data-action="table-search" data-target="registrationTable" placeholder="搜索考生、考试、固定报名号"></label><div class="filter-pills"><button class="active" data-action="status-filter" data-target="registrationTable" data-status="all">全部</button><button data-action="status-filter" data-target="registrationTable" data-status="pending">待审核</button><button data-action="status-filter" data-target="registrationTable" data-status="approved">已通过</button><button data-action="status-filter" data-target="registrationTable" data-status="rejected">已退回</button></div><div class="table-filter-selects"><select data-table-filter="exam" data-target="registrationTable"><option value="">全部考试</option>${optionList(item => item.exam?.name)}</select><select data-table-filter="school" data-target="registrationTable"><option value="">全部学校</option>${optionList(item => item.schoolName)}</select><select data-table-filter="grade" data-target="registrationTable"><option value="">全部年级</option>${optionList(item => item.gradeName)}</select><select data-table-filter="class" data-target="registrationTable"><option value="">全部班级</option>${optionList(item => item.className)}</select></div></div>${table(current, 'registrationTable', isSuper)}</section>${archived.length ? `<details class="candidate-archive-fold admin-registration-archive"><summary><span><strong>归档考试报名记录</strong><small>${archived.length} 条 · 流程与报名信息已冻结</small></span><b>${archived.length}</b></summary>${table(archived, 'archivedRegistrationTable')}</details>` : ''}`;
|
||||
const subjectOptions = [...new Set(registrations.flatMap(item => item.subjects.map(subject => subject.name)))].sort((left, right) => left.localeCompare(right, 'zh-CN')).map(value => `<option value="${h(value)}">${h(value)}</option>`).join('');
|
||||
const bulkToolbar = canReview ? `<div class="registration-bulk-bar"><span>已选择 <strong data-registration-selection-count>0</strong> 条当前可处理报名</span><div><button class="row-action" data-action="bulk-registration-review" data-status="rejected" disabled>批量退回</button><button class="row-action primary" data-action="bulk-registration-review" data-status="approved" disabled>批量通过当前步骤</button></div></div>` : '';
|
||||
return `<section class="panel data-panel registration-ledger">${bulkToolbar}<div class="data-toolbar registration-toolbar"><label class="search-box">${icons.search}<input data-action="table-search" data-target="registrationTable" placeholder="搜索考生、考试、科目、固定报名号"></label><div class="filter-pills"><button class="active" data-action="status-filter" data-target="registrationTable" data-status="all">全部</button><button data-action="status-filter" data-target="registrationTable" data-status="pending">待审核</button><button data-action="status-filter" data-target="registrationTable" data-status="approved">已通过</button><button data-action="status-filter" data-target="registrationTable" data-status="rejected">已退回</button></div><div class="table-filter-selects five-columns"><select data-table-filter="exam" data-target="registrationTable"><option value="">全部考试</option>${optionList(item => item.exam?.name)}</select><select data-table-filter="subject" data-target="registrationTable"><option value="">全部科目</option>${subjectOptions}</select><select data-table-filter="school" data-target="registrationTable"><option value="">全部学校</option>${optionList(item => item.schoolName)}</select><select data-table-filter="grade" data-target="registrationTable"><option value="">全部年级</option>${optionList(item => item.gradeName)}</select><select data-table-filter="class" data-target="registrationTable"><option value="">全部班级</option>${optionList(item => item.className)}</select></div></div>${table(current, 'registrationTable', Boolean(canReview))}</section>${archived.length ? `<details class="candidate-archive-fold admin-registration-archive"><summary><span><strong>归档考试报名记录</strong><small>${archived.length} 条 · 流程与报名信息已冻结</small></span><b>${archived.length}</b></summary>${table(archived, 'archivedRegistrationTable')}</details>` : ''}`;
|
||||
}
|
||||
|
||||
function adminPayments(data) {
|
||||
@@ -140,9 +162,10 @@ export function createAdminViews(context) {
|
||||
const optionList = (getter) => [...new Set(registrations.map(getter).filter(Boolean))]
|
||||
.sort((left, right) => left.localeCompare(right, 'zh-CN'))
|
||||
.map(value => `<option value="${h(value)}">${h(value)}</option>`).join('');
|
||||
const rows = registrations.map(item => `<tr data-filter-row data-status="${h(item.paymentStatus)}" data-exam="${h(item.exam?.name || '')}" data-school="${h(item.schoolName || '')}" data-grade="${h(item.gradeName || '')}" data-class="${h(item.className || '')}"><td><div class="person-cell"><span>${h((item.candidate?.name || '?').slice(0, 1))}</span><div><strong>${h(item.candidate?.name || '未知考生')}</strong><small class="mono">${h(item.registrationNumber || '')}</small></div></div></td><td><strong>${h(item.schoolName)}</strong><small>${h([item.gradeName, item.className].filter(Boolean).join(' · '))}</small></td><td><strong>${h(item.exam?.name || '')}</strong><small>${item.subjects.map(subject => h(subject.name)).join('、')}</small></td><td><strong>${money(item.amountDue || 0)}</strong></td><td>${badge(item.paymentStatus)}</td><td>${item.paidAt ? `<strong>${formatDate(item.paidAt, true)}</strong><small>${h(item.paidByName || '管理员')}</small>` : '<span>—</span>'}</td><td>${data.canUpdatePayment && !item.exam?.archivedAt ? `<button class="row-action ${item.paymentStatus === 'unpaid' ? 'primary' : 'danger'}" data-action="update-payment" data-id="${h(item.id)}" data-name="${h(item.candidate?.name || '')}" data-exam="${h(item.exam?.name || '')}" data-status="${item.paymentStatus === 'paid' ? 'unpaid' : 'paid'}">${item.paymentStatus === 'paid' ? '改为待缴费' : '标记已缴费'}</button>` : item.exam?.archivedAt ? '<span class="archive-readonly-label">只读封存</span>' : '<span>—</span>'}</td></tr>`).join('');
|
||||
const rows = registrations.map(item => `<tr data-filter-row data-status="${h(item.paymentStatus)}" data-exam="${h(item.exam?.name || '')}" data-school="${h(item.schoolName || '')}" data-grade="${h(item.gradeName || '')}" data-class="${h(item.className || '')}">${data.canUpdatePayment ? `<td class="selection-cell"><input type="checkbox" data-payment-select value="${h(item.id)}" data-status="${h(item.paymentStatus)}" aria-label="选择 ${h(item.candidate?.name || '考生')}" ${item.exam?.archivedAt ? 'disabled' : ''}></td>` : ''}<td><div class="person-cell"><span>${h((item.candidate?.name || '?').slice(0, 1))}</span><div><strong>${h(item.candidate?.name || '未知考生')}</strong><small class="mono">${h(item.registrationNumber || '')}</small></div></div></td><td><strong>${h(item.schoolName)}</strong><small>${h([item.gradeName, item.className].filter(Boolean).join(' · '))}</small></td><td><strong>${h(item.exam?.name || '')}</strong><div class="subject-summary">${item.subjects.map(subject => `<span>${h(subject.name)}</span>`).join('')}</div></td><td><strong>${money(item.amountDue || 0)}</strong></td><td>${badge(item.paymentStatus)}</td><td>${item.paidAt ? `<strong>${formatDate(item.paidAt, true)}</strong><small>${h(item.paidByName || '管理员')}</small>` : '<span>—</span>'}</td><td>${data.canUpdatePayment && !item.exam?.archivedAt ? `<button class="row-action ${item.paymentStatus === 'unpaid' ? 'primary' : 'danger'}" data-action="update-payment" data-id="${h(item.id)}" data-name="${h(item.candidate?.name || '')}" data-exam="${h(item.exam?.name || '')}" data-status="${item.paymentStatus === 'paid' ? 'unpaid' : 'paid'}">${item.paymentStatus === 'paid' ? '改为待缴费' : '标记已缴费'}</button>` : item.exam?.archivedAt ? '<span class="archive-readonly-label">只读封存</span>' : '<span>—</span>'}</td></tr>`).join('');
|
||||
const filters = `<div class="table-filter-selects"><select data-table-filter="exam" data-target="paymentTable"><option value="">全部考试</option>${optionList(item => item.exam?.name)}</select><select data-table-filter="school" data-target="paymentTable"><option value="">全部学校</option>${optionList(item => item.schoolName)}</select><select data-table-filter="grade" data-target="paymentTable"><option value="">全部年级</option>${optionList(item => item.gradeName)}</select><select data-table-filter="class" data-target="paymentTable"><option value="">全部班级</option>${optionList(item => item.className)}</select></div>`;
|
||||
return `${excelToolbar('payments', { importable: false, template: false, label: '缴费名单' })}<section class="center-summary payment-summary"><div><span>报名人数</span><strong>${registrations.length}</strong></div><div><span>待确认</span><strong>${unpaid.length}</strong></div><div><span>已缴费</span><strong>${paid.length}</strong></div><div><span>应缴合计</span><strong>${money(totalDue)}</strong></div><div><span>已缴合计</span><strong>${money(totalPaid)}</strong></div></section><section class="panel data-panel"><div class="data-toolbar payment-toolbar"><label class="search-box">${icons.search}<input data-action="table-search" data-target="paymentTable" placeholder="搜索考生、报名号、考试、学校或班级"></label><div class="filter-pills"><button class="active" data-action="status-filter" data-target="paymentTable" data-status="all">全部</button><button data-action="status-filter" data-target="paymentTable" data-status="unpaid">待缴费</button><button data-action="status-filter" data-target="paymentTable" data-status="paid">已缴费</button></div>${filters}</div><div class="table-scroll"><table id="paymentTable"><thead><tr><th>考生 / 报名号</th><th>学校 / 年级 / 班级</th><th>考试 / 科目</th><th>应缴金额</th><th>缴费状态</th><th>确认记录</th><th>操作</th></tr></thead><tbody>${rows || '<tr><td colspan="7" class="empty-state">当前范围暂无已审核通过的报名</td></tr>'}</tbody></table></div></section>`;
|
||||
const bulkToolbar = data.canUpdatePayment ? `<div class="registration-bulk-bar payment-bulk-bar"><span>已选择 <strong data-payment-selection-count>0</strong> 条缴费记录</span><div><button class="row-action" data-action="bulk-payment-update" data-status="unpaid" disabled>批量改为待缴费</button><button class="row-action primary" data-action="bulk-payment-update" data-status="paid" disabled>批量标记已缴费</button></div></div>` : '';
|
||||
return `${excelToolbar('payments', { importable: false, template: false, label: '缴费名单' })}<section class="center-summary payment-summary"><div><span>报名人数</span><strong>${registrations.length}</strong></div><div><span>待确认</span><strong>${unpaid.length}</strong></div><div><span>已缴费</span><strong>${paid.length}</strong></div><div><span>应缴合计</span><strong>${money(totalDue)}</strong></div><div><span>已缴合计</span><strong>${money(totalPaid)}</strong></div></section><section class="panel data-panel payment-ledger">${bulkToolbar}<div class="data-toolbar payment-toolbar"><label class="search-box">${icons.search}<input data-action="table-search" data-target="paymentTable" placeholder="搜索考生、报名号、考试、科目、学校或班级"></label><div class="filter-pills"><button class="active" data-action="status-filter" data-target="paymentTable" data-status="all">全部</button><button data-action="status-filter" data-target="paymentTable" data-status="unpaid">待缴费</button><button data-action="status-filter" data-target="paymentTable" data-status="paid">已缴费</button></div>${filters}</div><div class="table-scroll"><table id="paymentTable"><thead><tr>${data.canUpdatePayment ? '<th class="selection-cell"><input type="checkbox" data-payment-select-all data-target="paymentTable" aria-label="全选当前筛选结果"></th>' : ''}<th>考生 / 报名号</th><th>学校 / 年级 / 班级</th><th>考试 / 科目</th><th>应缴金额</th><th>缴费状态</th><th>确认记录</th><th>操作</th></tr></thead><tbody>${rows || `<tr><td colspan="${data.canUpdatePayment ? 8 : 7}" class="empty-state">当前范围暂无已审核通过的报名</td></tr>`}</tbody></table></div></section>`;
|
||||
}
|
||||
|
||||
function adminExams(exams) {
|
||||
@@ -177,16 +200,19 @@ export function createAdminViews(context) {
|
||||
|
||||
function adminIndicatorQualifications(data) {
|
||||
if (!data.exams?.length) return emptyState('暂无需要确认的考试', '超级管理员启用中考志愿填报后,本校资格名单会出现在这里。');
|
||||
return `<section class="qualification-ledger-intro"><div><span>SOURCE SCHOOL CERTIFICATION</span><h2>${h(data.school?.name)}资格确认簿</h2><p>逐人确认或多选批量设置。每场考试全部确认后立即自动公示,后续修改也会同步更新公示。</p></div></section>${data.exams.map(item => {
|
||||
return `<section class="qualification-ledger-intro"><div><span>SOURCE SCHOOL CERTIFICATION</span><h2>${h(data.school?.name)}资格确认簿</h2><p>先按姓名、报名号、确认状态或特长类型筛选,再逐人确认或多选批量设置。每场考试全部确认后立即自动公示。</p></div></section>${data.exams.map((item, index) => {
|
||||
const status = item.qualificationStatus;
|
||||
const bulk = `<div class="qualification-bulk-toolbar" data-qualification-bulk data-exam-id="${h(item.examId)}"><label><input type="checkbox" data-action="qualification-select-all"><span>全选本页 ${h(status.total)} 名考生</span></label><div><strong data-qualification-selected-count>已选 0 人</strong><select data-bulk-eligible><option value="">批量设置资格</option><option value="true">设为有指标分配资格</option><option value="false">设为无指标分配资格</option></select><button class="solid-button" data-action="bulk-indicator-qualification">应用到所选考生</button></div></div>`;
|
||||
const rows = status.rows.map(row => `<tr data-exam-id="${h(item.examId)}" data-user-id="${h(row.userId)}"><td><input type="checkbox" data-qualification-select aria-label="选择 ${h(row.name)}"></td><td><strong>${h(row.name)}</strong><small class="mono">${h(row.registrationNumber)}</small></td><td>${h(row.specialtyLabel)}</td><td><select data-indicator-eligible><option value="" ${row.confirmed ? '' : 'selected'}>请选择</option><option value="true" ${row.confirmed && row.eligible ? 'selected' : ''}>有指标分配资格</option><option value="false" ${row.confirmed && !row.eligible ? 'selected' : ''}>无指标分配资格</option></select></td><td>${row.confirmedAt ? formatDate(row.confirmedAt, true) : '待确认'}</td><td><button class="row-action primary" data-action="save-indicator-qualification">确认</button></td></tr>`).join('');
|
||||
return `<section class="panel qualification-ledger"><header><div><span>${h(item.exam.code)}</span><h2>${h(item.exam.name)}</h2></div><strong class="${status.complete ? 'complete' : ''}">${h(status.confirmed)} / ${h(status.total)} 已确认</strong></header><div class="qualification-progress"><i style="width:${status.total ? Math.round(status.confirmed / status.total * 100) : 0}%"></i></div>${status.complete ? '<div class="qualification-publication-state">✓ 本校资格已全部确认,公开公示已自动发布</div>' : '<div class="qualification-publication-state pending">未全部确认前不会公开,请逐项核对。</div>'}${bulk}<div class="table-scroll"><table><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>`;
|
||||
const tableId = `qualificationTable-${index}`;
|
||||
const specialties = [...new Set(status.rows.map(row => row.specialtyLabel).filter(Boolean))].sort((left, right) => left.localeCompare(right, 'zh-CN'));
|
||||
const filters = `<div class="data-toolbar qualification-filter-toolbar"><label class="search-box">${icons.search}<input data-action="table-search" data-target="${tableId}" placeholder="搜索报名号、姓名或特长类型"></label><div class="filter-pills"><button class="active" data-action="status-filter" data-target="${tableId}" data-status="all">全部</button><button data-action="status-filter" data-target="${tableId}" data-status="unconfirmed">待确认</button><button data-action="status-filter" data-target="${tableId}" data-status="eligible">有资格</button><button data-action="status-filter" data-target="${tableId}" data-status="ineligible">无资格</button></div><div class="qualification-specialty-filter"><select data-table-filter="specialty" data-target="${tableId}"><option value="">全部特长类型</option>${specialties.map(value => `<option value="${h(value)}">${h(value)}</option>`).join('')}</select><button class="row-action" data-action="clear-table-filters" data-target="${tableId}">清除筛选</button></div></div>`;
|
||||
const bulk = `<div class="qualification-bulk-toolbar" data-qualification-bulk data-exam-id="${h(item.examId)}"><label><input type="checkbox" data-action="qualification-select-all"><span>全选当前筛选结果</span></label><div><strong data-qualification-selected-count>已选 0 人</strong><select data-bulk-eligible><option value="">批量设置资格</option><option value="true">设为有指标分配资格</option><option value="false">设为无指标分配资格</option></select><button class="solid-button" data-action="bulk-indicator-qualification">应用到所选考生</button></div></div>`;
|
||||
const rows = status.rows.map(row => `<tr data-filter-row data-status="${row.confirmed ? `${row.eligible ? 'eligible' : 'ineligible'} confirmed` : 'unconfirmed'}" data-specialty="${h(row.specialtyLabel || '')}" data-exam-id="${h(item.examId)}" data-user-id="${h(row.userId)}"><td><input type="checkbox" data-qualification-select aria-label="选择 ${h(row.name)}"></td><td><strong>${h(row.name)}</strong><small class="mono">${h(row.registrationNumber)}</small></td><td>${h(row.specialtyLabel)}</td><td><select data-indicator-eligible><option value="" ${row.confirmed ? '' : 'selected'}>请选择</option><option value="true" ${row.confirmed && row.eligible ? 'selected' : ''}>有指标分配资格</option><option value="false" ${row.confirmed && !row.eligible ? 'selected' : ''}>无指标分配资格</option></select></td><td>${row.confirmedAt ? formatDate(row.confirmedAt, true) : '待确认'}</td><td><button class="row-action primary" data-action="save-indicator-qualification">确认</button></td></tr>`).join('');
|
||||
return `<section class="panel qualification-ledger"><header><div><span>${h(item.exam.code)}</span><h2>${h(item.exam.name)}</h2></div><strong class="${status.complete ? 'complete' : ''}">${h(status.confirmed)} / ${h(status.total)} 已确认</strong></header><div class="qualification-progress"><i style="width:${status.total ? Math.round(status.confirmed / status.total * 100) : 0}%"></i></div>${status.complete ? '<div class="qualification-publication-state">✓ 本校资格已全部确认,公开公示已自动发布</div>' : '<div class="qualification-publication-state pending">未全部确认前不会公开,请逐项核对。</div>'}${filters}${bulk}<div class="table-scroll"><table id="${tableId}"><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>`;
|
||||
}).join('')}`;
|
||||
}
|
||||
|
||||
function adminNotices(notices) {
|
||||
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>`;
|
||||
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><div class="filter-pills"><button class="active" data-action="status-filter" data-target="noticeTable" data-status="all">全部</button><button data-action="status-filter" data-target="noticeTable" data-status="published">已发布</button><button data-action="status-filter" data-target="noticeTable" data-status="draft">草稿</button></div><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-filter-row 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(data) {
|
||||
@@ -280,13 +306,15 @@ export function createAdminViews(context) {
|
||||
function adminFlows(data) {
|
||||
const actionNames = { submit: '提交', approve: '通过', reject: '退回考生', transfer: '转交', return: '退回节点', supervise: '监督调整' };
|
||||
const typeNames = { profile_change: '考生信息修改', registration_review: '考试报名', center_change: '考点考场变更', candidate_account_batch: '批量报名号申领', score_appeal: '考生成绩复议' };
|
||||
return `<div class="workflow-board">${data.instances.map(instance => {
|
||||
const visibleTypes = [...new Set(data.instances.map(instance => instance.businessType))];
|
||||
const toolbar = `<section class="panel data-toolbar workflow-filter-toolbar"><label class="search-box">${icons.search}<input data-action="table-search" data-target="workflowBoard" placeholder="搜索考生、学校、考试、责任人或流程"></label><div class="filter-pills"><button class="active" data-action="status-filter" data-target="workflowBoard" data-status="all">全部</button><button data-action="status-filter" data-target="workflowBoard" data-status="pending">待处理</button><button data-action="status-filter" data-target="workflowBoard" data-status="approved">已通过</button><button data-action="status-filter" data-target="workflowBoard" data-status="rejected">已退回</button></div><div class="workflow-type-filter"><select data-table-filter="type" data-target="workflowBoard"><option value="">全部流程类型</option>${visibleTypes.map(type => `<option value="${h(type)}">${h(typeNames[type] || type)}</option>`).join('')}</select></div></section>`;
|
||||
return `${toolbar}<div class="workflow-board" id="workflowBoard">${data.instances.map(instance => {
|
||||
const isCenter = instance.businessType === 'center_change';
|
||||
const isBatch = instance.businessType === 'candidate_account_batch';
|
||||
const isAppeal = instance.businessType === 'score_appeal';
|
||||
const title = isCenter ? instance.centerName : isBatch ? `${instance.schoolName} · ${instance.batchTotalCount} 个账户` : isAppeal ? `${instance.candidateName} · ${instance.appealResult?.subjectName || '成绩复议'}` : instance.candidateName;
|
||||
const sub = isCenter ? `${instance.requestType === 'create' ? '新增考点' : '修改档案'} · ${instance.schoolName}` : isBatch ? (instance.accountBatch?.quotas || []).map(item => `${item.className} ${item.count} 人`).join(' · ') : isAppeal ? `${instance.appealResult?.examName || ''} · 原成绩 ${instance.appealResult?.score ?? '—'}` : `${instance.examName ? `${instance.examName} · ` : ''}${instance.schoolName} · ${instance.className}`;
|
||||
return `<article class="panel workflow-card ${instance.status}"><header><div><span>${h(typeNames[instance.businessType] || instance.businessType)}</span><h2>${h(title)}</h2><p>${h(sub)}</p></div>${badge(instance.status)}</header><div class="workflow-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 < instance.currentStep || instance.status === 'approved' ? '✓' : step.position}</i><span><strong>${h(step.name)}</strong><small>${h(statusLabels[step.adminLevel])}</small></span></div>`).join('')}</div><div class="workflow-owner"><span>当前责任人</span><strong>${h(instance.assignee?.displayName || '流程已结束')}</strong><small>${h(instance.currentStepDetail?.name || statusLabels[instance.status])}</small></div><footer><span>${instance.actions.length ? `${h(actionNames[instance.actions.at(-1).action] || instance.actions.at(-1).action)} · ${h(instance.actions.at(-1).actorName)}` : '尚无操作记录'}</span><button class="row-action primary" data-action="open-flow" data-id="${h(instance.id)}">查看与处理</button></footer></article>`;
|
||||
return `<article class="panel workflow-card ${instance.status}" data-filter-row data-status="${h(instance.status)}" data-type="${h(instance.businessType)}"><header><div><span>${h(typeNames[instance.businessType] || instance.businessType)}</span><h2>${h(title)}</h2><p>${h(sub)}</p></div>${badge(instance.status)}</header><div class="workflow-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 < instance.currentStep || instance.status === 'approved' ? '✓' : step.position}</i><span><strong>${h(step.name)}</strong><small>${h(statusLabels[step.adminLevel])}</small></span></div>`).join('')}</div><div class="workflow-owner"><span>当前责任人</span><strong>${h(instance.assignee?.displayName || '流程已结束')}</strong><small>${h(instance.currentStepDetail?.name || statusLabels[instance.status])}</small></div><footer><span>${instance.actions.length ? `${h(actionNames[instance.actions.at(-1).action] || instance.actions.at(-1).action)} · ${h(instance.actions.at(-1).actorName)}` : '尚无操作记录'}</span><button class="row-action primary" data-action="open-flow" data-id="${h(instance.id)}">查看与处理</button></footer></article>`;
|
||||
}).join('') || emptyState('暂无审批流程', '考生资料、考试报名、考点档案或批量建号提交后,流程会显示在这里。')}</div>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -861,6 +861,10 @@ export function createAdminRoutes(context) {
|
||||
const candidates = db.candidateProfiles.filter(profile => profileInScope(user, profile)).map(profile => {
|
||||
const instance = pendingWorkflow(db, 'profile_change', profile.id) || db.workflowInstances.filter(item => item.businessType === 'profile_change' && item.businessId === profile.id)[0];
|
||||
const account = db.users.find(item => item.id === profile.userId);
|
||||
const registrations = db.registrations
|
||||
.filter(item => item.userId === profile.userId)
|
||||
.map(item => examRegistrationView(db, item))
|
||||
.sort((left, right) => new Date(right.createdAt || 0) - new Date(left.createdAt || 0));
|
||||
return {
|
||||
...profile,
|
||||
idNumberMasked: profile.idNumber.startsWith('PENDING-') ? '待考生补充' : maskId(profile.idNumber),
|
||||
@@ -870,6 +874,7 @@ export function createAdminRoutes(context) {
|
||||
accountArchived: Boolean(account?.archivedAt),
|
||||
archivedAt: account?.archivedAt || null,
|
||||
archivedByName: db.users.find(item => item.id === account?.archivedBy)?.displayName || '',
|
||||
registrations,
|
||||
workflow: workflowView(db, instance)
|
||||
};
|
||||
});
|
||||
|
||||
+95
@@ -682,3 +682,98 @@ button:disabled { cursor: not-allowed; opacity: .5; }
|
||||
.notice-pagination { display:flex; justify-content:center; gap:7px; padding:22px; }.notice-pagination button { min-width:36px; height:36px; padding:0 10px; border:1px solid #d7dee7; color:#526176; background:#fff; cursor:pointer; }.notice-pagination button.active { border-color:#17375f; color:#fff; background:#17375f; }.notice-pagination button:disabled { opacity:.42; cursor:not-allowed; }
|
||||
.notice-breadcrumb { width:min(980px,calc(100% - 48px)); display:flex; gap:10px; margin:0 auto; padding:38px 0 18px; color:#8792a1; }.notice-breadcrumb button { border:0; color:#245783; background:transparent; cursor:pointer; }.notice-document { width:min(980px,calc(100% - 48px)); margin:0 auto; border-top:5px solid #245783; background:#fff; box-shadow:0 14px 42px rgba(22,46,75,.08); }.notice-document > header { padding:46px 56px 34px; border-bottom:1px solid #dfe5eb; }.notice-document > header span { color:#287486; font-size:12px; font-weight:700; letter-spacing:.1em; }.notice-document > header h1 { margin:15px 0 18px; color:#1e3047; font:400 clamp(30px,4vw,44px)/1.25 STKaiti,KaiTi,serif; }.notice-document > header p { margin:0; color:#8994a2; }.notice-document > section { padding:40px 56px 50px; }.notice-document > footer { padding:20px 56px; border-top:1px solid #e1e6ec; background:#f8fafb; }.notice-document-content { color:#334257; font-size:15px; line-height:1.9; }.document-lead { margin:0 0 25px; padding:14px 17px; border-left:3px solid #287486; color:#526176; background:#f2f7f8; line-height:1.7; }.notice-document table { font-size:13px; }
|
||||
@media (max-width:800px) { .admission-settings-panel form,.admission-account-panel form,.admission-plan-console form { padding:18px; }.admission-settings-panel form > .solid-button,.admission-account-panel form > .solid-button,.admission-plan-console form > .solid-button { width:100%; }.qualification-bulk-toolbar { align-items:stretch; flex-direction:column; }.qualification-bulk-toolbar > div { align-items:stretch; flex-direction:column; }.notice-center-shell { width:calc(100% - 28px); grid-template-columns:1fr; }.notice-category-nav { display:flex; overflow-x:auto; padding:8px; border-right:0; border-bottom:1px solid #e1e6ec; scrollbar-width:none; }.notice-category-nav::-webkit-scrollbar { display:none; }.notice-category-nav button { width:auto; min-width:max-content; border-left:0; border-bottom:3px solid transparent; }.notice-category-nav button.active { border-bottom-color:#287486; }.notice-directory-list > button { grid-template-columns:58px minmax(0,1fr) 18px; gap:12px; padding:15px; }.notice-directory-copy small { white-space:normal; }.notice-center-hero { display:block; }.notice-center-hero > strong { display:none; }.notice-document > header,.notice-document > section,.notice-document > footer { padding-left:22px; padding-right:22px; } }
|
||||
|
||||
/* 审核工作台:以完整子页面承载资料、考试和处理结论 */
|
||||
body.review-subpage-open { overflow:hidden; }
|
||||
.review-subpage-layer { position:fixed; inset:0; z-index:120; overflow:auto; background:#eef2f6; animation:fadeIn .16s ease; }
|
||||
.review-subpage { min-height:100vh; color:#27364a; background:linear-gradient(90deg,#f8fafc 0 72%,#edf2f5 72%); }
|
||||
.review-subpage-header { min-height:190px; display:grid; grid-template-columns:minmax(190px,1fr) minmax(420px,2.2fr) minmax(140px,1fr); align-items:center; gap:28px; padding:34px clamp(28px,5vw,76px); color:#fff; background:linear-gradient(112deg,#132f52 0 62%,#216b79); box-shadow:0 12px 35px rgba(23,55,95,.16); }
|
||||
.review-back { justify-self:start; align-self:start; padding:10px 0; border:0; color:#c5dbe4; background:transparent; font-size:14px; font-weight:700; cursor:pointer; }
|
||||
.review-back:hover { color:#fff; }
|
||||
.review-subpage-header > div > span { color:#9bd0d6; font:700 12px/1.3 Consolas,monospace; letter-spacing:.16em; }
|
||||
.review-subpage-header h1 { margin:9px 0 12px; font:500 clamp(28px,3vw,42px)/1.18 STKaiti,KaiTi,serif; }
|
||||
.review-subpage-header p { display:flex; flex-wrap:wrap; align-items:center; gap:10px; margin:0; color:#d2dce7; font-size:14px; }
|
||||
.review-subpage-header p i { width:4px; height:4px; border-radius:50%; background:#7fa8b8; }
|
||||
.review-subpage-header > .status { justify-self:end; min-width:82px; justify-content:center; padding:9px 14px; font-size:13px; }
|
||||
.review-subpage-layout { width:min(1500px,100%); display:grid; grid-template-columns:minmax(0,1fr) 360px; gap:28px; margin:0 auto; padding:32px clamp(24px,4vw,58px) 70px; }
|
||||
.review-subpage-main { min-width:0; display:grid; align-content:start; gap:24px; }
|
||||
.review-section { overflow:hidden; border:1px solid #dbe3ea; border-radius:14px; background:#fff; box-shadow:0 9px 28px rgba(28,52,76,.05); }
|
||||
.review-section > header { min-height:86px; display:flex; align-items:center; gap:16px; padding:20px 24px; border-bottom:1px solid #e3e9ee; background:linear-gradient(90deg,#fff,#f6f9fa); }
|
||||
.review-section > header > span { min-width:45px; height:34px; display:grid; place-items:center; border-radius:6px; color:#fff; background:#245783; font:700 12px Consolas,monospace; }
|
||||
.review-section > header > div { min-width:0; margin-right:auto; }
|
||||
.review-section > header h2 { margin:0; color:#21364e; font-size:21px; }
|
||||
.review-section > header p { margin:5px 0 0; color:#748396; font-size:14px; }
|
||||
.review-section > header > strong { color:#287486; font-size:15px; }
|
||||
.review-detail-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:1px; margin:0; background:#e5eaef; }
|
||||
.review-detail-grid > div { min-width:0; padding:17px 20px; background:#fff; }
|
||||
.review-detail-grid > div.wide { grid-column:1/-1; }
|
||||
.review-detail-grid dt { margin-bottom:7px; color:#798799; font-size:13px; }
|
||||
.review-detail-grid dd { margin:0; overflow-wrap:anywhere; color:#293b51; font-size:15px; line-height:1.65; }
|
||||
.review-exam-list { display:grid; gap:14px; padding:20px 22px 24px; }
|
||||
.review-exam-card { overflow:hidden; border:1px solid #dfe6eb; border-radius:11px; background:#fbfcfd; }
|
||||
.review-exam-card > header { display:flex; align-items:center; justify-content:space-between; gap:20px; padding:16px 18px; border-bottom:1px solid #e4e9ed; background:#fff; }
|
||||
.review-exam-card h3 { margin:5px 0 0; font-size:18px; }
|
||||
.review-exam-card dl { display:grid; grid-template-columns:1fr 1fr; gap:0; margin:0; }
|
||||
.review-exam-card dl > div { padding:15px 18px; border-top:1px solid #e8edf0; }
|
||||
.review-exam-card dl > div:nth-child(-n+2) { border-top:0; }
|
||||
.review-exam-card dt { color:#7d8997; font-size:13px; }
|
||||
.review-exam-card dd { margin:6px 0 0; color:#33465a; font-size:14px; line-height:1.6; }
|
||||
.review-subject-list { display:flex; flex-wrap:wrap; gap:8px; }
|
||||
.review-subject-list > span { display:grid; gap:3px; padding:9px 11px; border:1px solid #d9e3e7; border-radius:8px; background:#fff; }
|
||||
.review-subject-list small { color:#738395; font-size:12px; }
|
||||
.review-empty-context { padding:25px; border:1px dashed #cdd8df; border-radius:10px; color:#67788b; background:#f7fafb; text-align:center; }
|
||||
.review-empty-context strong { color:#33485e; font-size:16px; }
|
||||
.review-empty-context p { margin:7px 0 0; font-size:14px; }
|
||||
.review-decision-form,.review-readonly { position:sticky; top:24px; align-self:start; display:grid; gap:18px; padding:24px; border-top:5px solid #287486; border-radius:12px; background:#fff; box-shadow:0 14px 38px rgba(26,52,76,.12); }
|
||||
.review-decision-form > div { display:grid; gap:5px; padding-bottom:18px; border-bottom:1px solid #e1e7eb; }
|
||||
.review-decision-form > div span,.review-decision-form label > span { color:#708093; font-size:13px; font-weight:700; }
|
||||
.review-decision-form > div strong { color:#1f3853; font-size:18px; }
|
||||
.review-decision-form > div small { color:#7e8a99; font-size:13px; }
|
||||
.review-decision-form label { display:grid; gap:7px; }
|
||||
.review-decision-form select,.review-decision-form textarea { width:100%; padding:11px 12px; border:1px solid #cbd6df; border-radius:8px; color:#293b50; background:#fff; font-size:14px; line-height:1.6; }
|
||||
.review-decision-form select:focus,.review-decision-form textarea:focus { border-color:#287486; outline:3px solid rgba(40,116,134,.12); }
|
||||
.review-decision-form .solid-button { min-height:46px; font-size:14px; }
|
||||
.review-readonly strong { color:#233c56; font-size:18px; }.review-readonly p { margin:0; color:#697b8e; font-size:14px; line-height:1.7; }
|
||||
.exam-context-hero { border-top:5px solid #245783; }
|
||||
.subject-review-section > footer { display:flex; align-items:baseline; justify-content:flex-end; gap:14px; padding:18px 24px; border-top:1px solid #e1e7eb; background:#f8fafb; }
|
||||
.subject-review-section > footer span { color:#6d7e91; font-size:14px; }.subject-review-section > footer strong { color:#17375f; font-size:24px; }
|
||||
.review-subject-cards { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:13px; padding:20px 22px; }
|
||||
.review-subject-cards > article { display:grid; grid-template-columns:34px minmax(0,1fr) auto; align-items:center; gap:12px; padding:16px; border:1px solid #dbe4ea; border-radius:10px; background:#fff; }
|
||||
.review-subject-cards > article > i { width:32px; height:32px; display:grid; place-items:center; border-radius:50%; color:#fff; background:#287486; font-size:13px; font-style:normal; }
|
||||
.review-subject-cards > article > div { display:grid; gap:4px; min-width:0; }.review-subject-cards > article > div strong { font-size:16px; }.review-subject-cards > article > div span { color:#718194; font-size:13px; }
|
||||
.review-subject-cards dl { display:flex; gap:16px; margin:0; }.review-subject-cards dl div { display:grid; gap:4px; }.review-subject-cards dt { color:#8290a0; font-size:12px; }.review-subject-cards dd { margin:0; font-size:14px; font-weight:700; }
|
||||
|
||||
/* 高频台账筛选与批量操作 */
|
||||
.candidate-toolbar { flex-wrap:wrap; }.table-filter-selects.five-columns { grid-template-columns:repeat(5,minmax(120px,1fr)); }
|
||||
.candidate-exam-summary { min-width:190px; }.candidate-exam-summary strong,.candidate-exam-summary small { display:block; }.candidate-exam-summary span { color:#8793a2; }
|
||||
.registration-exam-cell { min-width:220px; }.subject-summary { display:flex; flex-wrap:wrap; gap:5px; margin:7px 0 4px; }.subject-summary span { padding:4px 7px; border-radius:5px; color:#36576e; background:#e9f1f4; font-size:12px; font-weight:700; }.subject-summary em { color:#a14c43; font-style:normal; }
|
||||
.qualification-filter-toolbar { flex-wrap:wrap; margin-top:14px; border-top:1px solid #e4e9ed; }.qualification-specialty-filter { flex:1 0 100%; display:flex; gap:9px; }.qualification-specialty-filter select { flex:1; min-height:40px; padding:8px 11px; border:1px solid #ccd7df; border-radius:7px; color:#45576b; background:#fff; font-size:14px; }.qualification-specialty-filter .row-action { padding:0 12px; border:1px solid #d6dfe5; border-radius:7px; background:#fff; }
|
||||
.workflow-filter-toolbar { flex-wrap:wrap; margin-bottom:16px; }.workflow-type-filter { flex:1 0 100%; }.workflow-type-filter select { width:100%; min-height:40px; padding:8px 11px; border:1px solid #ccd7df; border-radius:7px; color:#45576b; background:#fff; font-size:14px; }
|
||||
|
||||
/* 全站可读性下限:正文 14px,辅助信息不低于 12px */
|
||||
.admin-readable .portal-sidebar nav button { font-size:14px; }.admin-readable .portal-sidebar nav button em { font-size:12px; }
|
||||
.admin-readable .portal-topbar > div:first-of-type,.admin-readable .portal-user > span strong { font-size:14px; }
|
||||
.admin-readable .portal-user > span small,.admin-readable .sidebar-help span,.admin-readable .sidebar-help small { font-size:12px; }
|
||||
.admin-readable .portal-content :where(p,time,dt,dd,label > span,th,td,button,input,select,textarea,em) { font-size:14px !important; line-height:1.55; }
|
||||
.admin-readable .portal-content small { font-size:12px !important; line-height:1.55; }
|
||||
.admin-readable .portal-content span:not(:has(svg)):not(.status):not(.user-avatar) { font-size:14px !important; }
|
||||
.admin-readable .portal-content :where(td strong,button strong,p strong) { font-size:14px !important; }
|
||||
.admin-readable .portal-content .status,.admin-readable .portal-content .overline,.admin-readable .portal-content .exam-code { font-size:12px !important; }
|
||||
.admin-readable .portal-content :where(.solid-button,.ghost-button,.row-action) { min-height:38px; }
|
||||
.portal-content :where(p,time,dt,dd,label > span,th,td,button,input,select,textarea,em) { font-size:14px !important; line-height:1.55; }
|
||||
.portal-content small { font-size:12px !important; line-height:1.55; }.portal-content span:not(:has(svg)):not(.status):not(.user-avatar) { font-size:14px !important; }.portal-content .status,.portal-content .overline,.portal-content .exam-code { font-size:12px !important; }
|
||||
.portal-sidebar nav button { font-size:14px; }.portal-sidebar nav button em,.portal-user small,.sidebar-help small { font-size:12px; }.portal-topbar button { font-size:13px; }
|
||||
.auth-page :where(p,label > span,button,input,select,textarea) { font-size:14px; line-height:1.65; }.auth-page span:not(:has(svg)) { font-size:14px; }.auth-page small { font-size:12px; }
|
||||
.onboarding-page :where(p,label > span,button,input,select,textarea) { font-size:14px; line-height:1.65; }.onboarding-page span:not(:has(svg)) { font-size:14px; }.onboarding-page small { font-size:12px; }
|
||||
.modal-card :where(p,dt,dd,label > span,button,input,select,textarea,li) { font-size:14px; line-height:1.65; }.modal-card span:not(:has(svg)) { font-size:14px; }.modal-card small,.modal-head span { font-size:12px; }
|
||||
.public-footer p,.public-footer > span { font-size:13px; line-height:1.65; }
|
||||
.notice-document table th,.notice-document table td { padding:15px 17px; font-size:15px; line-height:1.55; }
|
||||
.notice-document .document-lead { font-size:15px; }.notice-document .qualification-result { min-width:46px; padding:6px 11px; font-size:14px; }
|
||||
.public-main :where(button,input,select,textarea) { font-size:14px; }.public-main :where(p,dt,dd) { font-size:14px; line-height:1.7; }.public-main span:not(:has(svg)):not(.status) { font-size:14px; }.public-main small { font-size:12px; line-height:1.55; }
|
||||
|
||||
@media (max-width:1000px) {
|
||||
.review-subpage { background:#f4f7f9; }.review-subpage-header { grid-template-columns:1fr auto; min-height:0; }.review-subpage-header > div { grid-column:1/-1; grid-row:2; }.review-subpage-header > .status { grid-column:2; grid-row:1; }.review-subpage-layout { grid-template-columns:1fr; }.review-decision-form,.review-readonly { position:static; }.table-filter-selects.five-columns { grid-template-columns:repeat(2,minmax(130px,1fr)); }
|
||||
}
|
||||
@media (max-width:650px) {
|
||||
.review-subpage-header { padding:24px 20px; }.review-subpage-layout { padding:20px 14px 45px; }.review-detail-grid,.review-exam-card dl { grid-template-columns:1fr; }.review-detail-grid > div.wide { grid-column:auto; }.review-exam-card dl > div:nth-child(2) { border-top:1px solid #e8edf0; }.review-subject-cards { grid-template-columns:1fr; padding:14px; }.review-subject-cards > article { grid-template-columns:32px minmax(0,1fr); }.review-subject-cards dl { grid-column:2; }.qualification-specialty-filter { flex-direction:column; }.table-filter-selects.five-columns { grid-template-columns:1fr; }
|
||||
}
|
||||
|
||||
@@ -689,6 +689,10 @@ try {
|
||||
const adminRegistration = adminRegistrations.data.registrations.find(item => item.id === registrationId);
|
||||
assert.equal(adminRegistration.status, 'pending', '新报名应进入管理员审核队列');
|
||||
assert.ok(adminRegistration.exam?.name && adminRegistration.schoolName && adminRegistration.gradeName && adminRegistration.className, '报名审核列表应提供考试、学校、年级和班级筛选字段');
|
||||
assert.deepEqual(adminRegistration.subjects.map(item => item.id), [exam.subjects[0].id, exam.subjects[2].id], '报名审核详情应完整提供考生所报科目');
|
||||
const candidateReviewList = await admin.request('/api/admin/candidates');
|
||||
const candidateReviewItem = candidateReviewList.data.candidates.find(item => item.id === profile.id);
|
||||
assert.ok(candidateReviewItem.registrations.some(item => item.id === registrationId && item.exam?.id === exam.id && item.subjects.length === 2), '考生资料审核应同时提供关联考试和所报科目上下文');
|
||||
|
||||
const schoolFlows = await schoolAdmin.request('/api/admin/workflow-instances');
|
||||
const registrationFlow = schoolFlows.data.instances.find(item => item.businessId === registrationId && item.status === 'pending');
|
||||
|
||||
Reference in New Issue
Block a user