From 4929858ec26c56dcb3acf5d54149d957918281cd Mon Sep 17 00:00:00 2001 From: biss Date: Tue, 21 Jul 2026 15:43:51 +0800 Subject: [PATCH] c --- app.js | 129 +++++++++++++++++++++++++++++++++--- src/client/admin-views.mjs | 58 +++++++++++----- src/routes/admin.routes.mjs | 5 ++ styles.css | 95 ++++++++++++++++++++++++++ tests/system.test.mjs | 4 ++ 5 files changed, 268 insertions(+), 23 deletions(-) diff --git a/app.js b/app.js index 471d033..5230b24 100644 --- a/app.js +++ b/app.js @@ -24,14 +24,22 @@ function toast(title, message = '') { } function setModal(content) { + document.body.classList.remove('review-subpage-open'); modalRoot.innerHTML = ``; setTimeout(() => modalRoot.querySelector('input,textarea,button')?.focus(), 30); } +function setReviewSubpage(content) { + document.body.classList.add('review-subpage-open'); + modalRoot.innerHTML = `
${content}
`; + 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(`
证件号码
${h(item.idNumberMasked)}
性别 / 籍贯
${h(item.gender)} · ${h(item.nativePlace)}
联系电话
${h(item.phone)}
电子邮箱
${h(item.email)}
就读学校
${h(item.school)}
年级班级
${h(item.grade)}
家庭住址
${h(item.address)}
监护人
${h(item.guardianName || item.emergencyContact)} · ${h(item.guardianPhone || item.emergencyPhone)}
当前步骤
${h(item.workflow?.currentStepDetail?.name || '流程已结束')}
当前责任人
${h(item.workflow?.assignee?.displayName || '—')}
${canReview ? `` : ''}`); + 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 => `
${h(registration.exam?.code || '')}

${h(registration.exam?.name || '考试信息异常')}

${badge(registration.exam?.archivedAt ? 'archived' : registration.status)}
考试时间
${dateRange(registration.exam?.examStart, registration.exam?.examEnd)}
考试地点
${h(registration.exam?.location || '待公布')}
报名科目
${registration.subjects?.map(subject => `${h(subject.name)}${h(subject.date || '')} ${h(subject.start || '')} · 满分 ${h(subject.fullScore)} · ${money(subject.fee || 0)}`).join('') || '未选择科目'}
报名 / 缴费
${badge(registration.status)} ${badge(registration.paymentStatus)} · 应缴 ${money(registration.amountDue || 0)}
`).join(''); + const decision = canReview ? `
当前审核步骤${h(item.workflow?.currentStepDetail?.name || '流程已结束')}责任人:${h(item.workflow?.assignee?.displayName || '—')}
` : ``; + setReviewSubpage(`
CANDIDATE DOSSIER

${h(item.name)} · 资料审核

${h(item.candidateNumber)}${h(item.school || '学校未填写')} · ${h(item.grade || '班级未填写')}更新于 ${formatDate(item.updatedAt, true)}

${badge(item.status)}
01

身份与学籍信息

核对实名、学籍范围以及联系方式。

证件号码
${h(item.idNumberMasked)}
性别 / 出生日期
${h(item.gender || '未填写')} · ${h(item.birthDate || '未填写')}
籍贯 / 民族
${h(item.nativePlace || '未填写')} · ${h(item.ethnicity || '未填写')}
就读学校 / 班级
${h(item.school || '未填写')} · ${h(item.grade || '未填写')}
联系电话
${h(item.phone || '未填写')}
电子邮箱
${h(item.email || '未填写')}
家庭住址
${h(item.address || '未填写')}
监护人
${h(item.guardianName || '未填写')} · ${h(item.guardianPhone || '电话未填写')}
紧急联系人
${h(item.emergencyContact || '未填写')} · ${h(item.emergencyPhone || '电话未填写')}
02

关联考试与报名科目

审核资料时同时查看该考生历次报名上下文。

${registrations.length} 场
${examCards || '
暂无考试报名

该考生当前尚未提交考试报名;资料审核通过后才能选择考试与科目。

'}
${decision}
`); } 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(`
报考科目

${reg.subjects.map(subject => `${h(subject.name)}`).join('')}

账户报名号
${h(reg.registrationNumber || reg.candidate?.candidateNumber || '账户号码异常')}
当前步骤
${h(reg.workflow?.currentStepDetail?.name || '流程已结束')}
责任人
${h(reg.workflow?.assignee?.displayName || '—')}
缴费状态
${badge(reg.paymentStatus)}
${canReview ? `` : ''}`); + const subjects = reg.subjects || []; + const subjectCards = subjects.map((subject, index) => `
${index + 1}
${h(subject.name)}${h(subject.date || '日期待定')} ${h(subject.start || '')}${subject.end ? `—${h(subject.end)}` : ''}
满分
${h(subject.fullScore)} 分
报名费
${money(subject.fee || 0)}
`).join(''); + const decision = canReview ? `
当前审核步骤${h(reg.workflow?.currentStepDetail?.name || '流程已结束')}责任人:${h(reg.workflow?.assignee?.displayName || '—')}
` : ``; + setReviewSubpage(`
REGISTRATION DOSSIER

${h(reg.candidate?.name)} · 报名审核

${h(reg.registrationNumber || reg.candidate?.candidateNumber || '号码待同步')}${h(reg.schoolName || reg.candidate?.school || '')} · ${h([reg.gradeName, reg.className].filter(Boolean).join(' · '))}

${badge(reg.status)}
考试

${h(reg.exam.code)}

${h(reg.exam.name)}

报名时间
${dateRange(reg.exam.registrationStart, reg.exam.registrationEnd)}
考试时间
${dateRange(reg.exam.examStart, reg.exam.examEnd)}
考试地点
${h(reg.exam.location || '待公布')}
本次报名
${subjects.length} 个科目 · 应缴 ${money(reg.amountDue || 0)}
缴费状态
${badge(reg.paymentStatus)}
账户报名号
${h(reg.registrationNumber || reg.candidate?.candidateNumber || '待同步')}
科目

本次所报科目

逐科核对日期、时间、满分和报名费用。

${subjects.length} 科
${subjectCards || '
未选择任何科目

该报名记录数据异常,不应通过审核。

'}
费用合计${money(reg.amountDue || 0)}
考生

考生资料摘要

报名审核同时核验身份与学校范围。

姓名 / 性别
${h(reg.candidate?.name || '未填写')} · ${h(reg.candidate?.gender || '未填写')}
证件号码
${h(reg.candidate?.idNumber || '未填写')}
学校 / 班级
${h(reg.schoolName || '')} · ${h([reg.gradeName, reg.className].filter(Boolean).join(' · '))}
联系电话
${h(reg.candidate?.phone || '未填写')}
${decision}
`); } async function openNoticeForm() { diff --git a/src/client/admin-views.mjs b/src/client/admin-views.mjs index 8055568..207e3b2 100644 --- a/src/client/admin-views.mjs +++ b/src/client/admin-views.mjs @@ -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 => ``).join(''); const archiveConsole = state.user.adminLevel === 'school' ? (() => { const classes = data.classes || []; const grades = [...new Set(classes.map(item => item.grade))]; return `
SCHOOL ACCOUNT ARCHIVE

按班级或年级归档账户

归档只冻结登录,不删除考生、报名、准考证、成绩和审计记录;可随时按相同范围恢复。

`; })() : ''; - return `${archiveConsole}${excelToolbar('candidates', { importable: !readOnly, label: '考生资料' })}
${candidates.map(item => ``).join('')}
报名号 / 考生证件号码学校 / 班级账户状态更新时间资料状态操作
${h(item.name.slice(0,1))}
${h(item.name)}${h(item.candidateNumber || '待分配')}
${h(item.idNumberMasked)}${h(item.school || '未填写')}${h(item.grade || '')}${item.accountArchived ? '已归档' : item.mustChangePassword ? '待首次改密' : item.profileCompleted ? '正常' : '待补全资料'}${item.accountArchived ? `${formatDate(item.archivedAt, true)} · ${h(item.archivedByName || '校方')}` : h(item.workflow?.assignee?.displayName || '')}${formatDate(item.updatedAt,true)}${item.profileCompleted ? badge(item.status) : '未完成'}
`; + 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 `
${h(item.name.slice(0,1))}
${h(item.name)}${h(item.candidateNumber || '待分配')}
${h(item.idNumberMasked)}${h(item.school || '未填写')}${h([grade, className].filter(Boolean).join(' · '))}
${exams.length ? `${h(examNames.slice(0, 2).join('、'))}${exams.length > 2 ? ` 等 ${exams.length} 场` : ''}${h(exams.flatMap(registration => registration.subjects || []).map(subject => subject.name).slice(0, 5).join('、') || '尚未选择科目')}` : '暂无考试报名'}
${item.accountArchived ? '已归档' : item.mustChangePassword ? '待首次改密' : item.profileCompleted ? '正常' : '待补全资料'}${item.accountArchived ? `${formatDate(item.archivedAt, true)} · ${h(item.archivedByName || '校方')}` : h(item.workflow?.assignee?.displayName || '')}${item.profileCompleted ? badge(item.status) : '未完成'}
${state.user.adminLevel === 'super' ? `` : ''}
`; + }).join(''); + const bulkToolbar = canReview ? `
已选择 0 名当前可处理考生
` : ''; + 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 => ``).join(''); + const filters = `
`; + return `${archiveConsole}${excelToolbar('candidates', { importable: true, label: '考生资料' })}
${bulkToolbar}
${filters}
${rows || ''}
报名号 / 考生证件号码学校 / 年级 / 班级关联考试 / 科目账户状态资料状态操作
当前范围暂无考生
`; } 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 => ``).join(''); const rows = (items, selectable = false) => items.map(reg => { - const canSelect = selectable && reg.status === 'pending' && reg.workflow?.status === 'pending'; - return `${selectable ? `` : ''}
${h((reg.candidate?.name || '?').slice(0,1))}
${h(reg.candidate?.name)}${h([reg.schoolName, reg.gradeName, reg.className].filter(Boolean).join(' · '))}
${h(reg.exam.name)}${reg.subjects.map(subject => h(subject.name)).join('、')}${h(reg.registrationNumber || '待同步账户号码')}各次考试保持一致${h(reg.workflow?.currentStepDetail?.name || '流程已结束')}${h(reg.workflow?.assignee?.displayName || '')}${badge(reg.paymentStatus)}${badge(reg.exam.archivedAt ? 'archived' : reg.status)}${reg.exam.archivedAt ? '只读封存' : ``}`; + const canSelect = selectable && reg.status === 'pending' && reg.workflow?.status === 'pending' + && (isSuper || reg.workflow?.assignee?.id === state.user.id); + return `${selectable ? `` : ''}
${h((reg.candidate?.name || '?').slice(0,1))}
${h(reg.candidate?.name)}${h([reg.schoolName, reg.gradeName, reg.className].filter(Boolean).join(' · '))}
${h(reg.exam.name)}
${reg.subjects.map(subject => `${h(subject.name)}`).join('') || '未选择科目'}
${h(reg.subjects.length)} 科 · ${money(reg.amountDue || 0)}
${h(reg.registrationNumber || '待同步账户号码')}各次考试保持一致${h(reg.workflow?.currentStepDetail?.name || '流程已结束')}${h(reg.workflow?.assignee?.displayName || '')}${badge(reg.paymentStatus)}${badge(reg.exam.archivedAt ? 'archived' : reg.status)}${reg.exam.archivedAt ? '只读封存' : ``}`; }).join(''); const table = (items, id, selectable = false) => `
${selectable ? '' : ''}${rows(items, selectable) || ``}
考生考试 / 科目账户报名号当前流程缴费状态操作
当前范围暂无报名记录
`; const current = registrations.filter(reg => !reg.exam.archivedAt); const archived = registrations.filter(reg => reg.exam.archivedAt); - const bulkToolbar = isSuper ? `
已选择 0 条待审核报名
` : ''; - return `
${bulkToolbar}
${table(current, 'registrationTable', isSuper)}
${archived.length ? `
归档考试报名记录${archived.length} 条 · 流程与报名信息已冻结${archived.length}${table(archived, 'archivedRegistrationTable')}
` : ''}`; + const subjectOptions = [...new Set(registrations.flatMap(item => item.subjects.map(subject => subject.name)))].sort((left, right) => left.localeCompare(right, 'zh-CN')).map(value => ``).join(''); + const bulkToolbar = canReview ? `
已选择 0 条当前可处理报名
` : ''; + return `
${bulkToolbar}
${table(current, 'registrationTable', Boolean(canReview))}
${archived.length ? `
归档考试报名记录${archived.length} 条 · 流程与报名信息已冻结${archived.length}${table(archived, 'archivedRegistrationTable')}
` : ''}`; } 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 => ``).join(''); - const rows = registrations.map(item => `
${h((item.candidate?.name || '?').slice(0, 1))}
${h(item.candidate?.name || '未知考生')}${h(item.registrationNumber || '')}
${h(item.schoolName)}${h([item.gradeName, item.className].filter(Boolean).join(' · '))}${h(item.exam?.name || '')}${item.subjects.map(subject => h(subject.name)).join('、')}${money(item.amountDue || 0)}${badge(item.paymentStatus)}${item.paidAt ? `${formatDate(item.paidAt, true)}${h(item.paidByName || '管理员')}` : ''}${data.canUpdatePayment && !item.exam?.archivedAt ? `` : item.exam?.archivedAt ? '只读封存' : ''}`).join(''); + const rows = registrations.map(item => `${data.canUpdatePayment ? `` : ''}
${h((item.candidate?.name || '?').slice(0, 1))}
${h(item.candidate?.name || '未知考生')}${h(item.registrationNumber || '')}
${h(item.schoolName)}${h([item.gradeName, item.className].filter(Boolean).join(' · '))}${h(item.exam?.name || '')}
${item.subjects.map(subject => `${h(subject.name)}`).join('')}
${money(item.amountDue || 0)}${badge(item.paymentStatus)}${item.paidAt ? `${formatDate(item.paidAt, true)}${h(item.paidByName || '管理员')}` : ''}${data.canUpdatePayment && !item.exam?.archivedAt ? `` : item.exam?.archivedAt ? '只读封存' : ''}`).join(''); const filters = `
`; - return `${excelToolbar('payments', { importable: false, template: false, label: '缴费名单' })}
报名人数${registrations.length}
待确认${unpaid.length}
已缴费${paid.length}
应缴合计${money(totalDue)}
已缴合计${money(totalPaid)}
${filters}
${rows || ''}
考生 / 报名号学校 / 年级 / 班级考试 / 科目应缴金额缴费状态确认记录操作
当前范围暂无已审核通过的报名
`; + const bulkToolbar = data.canUpdatePayment ? `
已选择 0 条缴费记录
` : ''; + return `${excelToolbar('payments', { importable: false, template: false, label: '缴费名单' })}
报名人数${registrations.length}
待确认${unpaid.length}
已缴费${paid.length}
应缴合计${money(totalDue)}
已缴合计${money(totalPaid)}
${bulkToolbar}
${filters}
${data.canUpdatePayment ? '' : ''}${rows || ``}
考生 / 报名号学校 / 年级 / 班级考试 / 科目应缴金额缴费状态确认记录操作
当前范围暂无已审核通过的报名
`; } function adminExams(exams) { @@ -177,16 +200,19 @@ export function createAdminViews(context) { function adminIndicatorQualifications(data) { if (!data.exams?.length) return emptyState('暂无需要确认的考试', '超级管理员启用中考志愿填报后,本校资格名单会出现在这里。'); - return `
SOURCE SCHOOL CERTIFICATION

${h(data.school?.name)}资格确认簿

逐人确认或多选批量设置。每场考试全部确认后立即自动公示,后续修改也会同步更新公示。

${data.exams.map(item => { + return `
SOURCE SCHOOL CERTIFICATION

${h(data.school?.name)}资格确认簿

先按姓名、报名号、确认状态或特长类型筛选,再逐人确认或多选批量设置。每场考试全部确认后立即自动公示。

${data.exams.map((item, index) => { const status = item.qualificationStatus; - const bulk = `
已选 0 人
`; - const rows = status.rows.map(row => `${h(row.name)}${h(row.registrationNumber)}${h(row.specialtyLabel)}${row.confirmedAt ? formatDate(row.confirmedAt, true) : '待确认'}`).join(''); - return `
${h(item.exam.code)}

${h(item.exam.name)}

${h(status.confirmed)} / ${h(status.total)} 已确认
${status.complete ? '
✓ 本校资格已全部确认,公开公示已自动发布
' : '
未全部确认前不会公开,请逐项核对。
'}${bulk}
${rows || ''}
选择报名号 / 姓名特长类型指标分配资格确认时间保存
本校暂无资料已完善的在册考生
`; + 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 = `
`; + const bulk = `
已选 0 人
`; + const rows = status.rows.map(row => `${h(row.name)}${h(row.registrationNumber)}${h(row.specialtyLabel)}${row.confirmedAt ? formatDate(row.confirmedAt, true) : '待确认'}`).join(''); + return `
${h(item.exam.code)}

${h(item.exam.name)}

${h(status.confirmed)} / ${h(status.total)} 已确认
${status.complete ? '
✓ 本校资格已全部确认,公开公示已自动发布
' : '
未全部确认前不会公开,请逐项核对。
'}${filters}${bulk}
${rows || ''}
选择报名号 / 姓名特长类型指标分配资格确认时间保存
本校暂无资料已完善的在册考生
`; }).join('')}`; } function adminNotices(notices) { - return `

发布状态会实时同步到公开首页和考生中心。

${notices.map(notice => ``).join('')}
通知标题分类作者发布时间展示状态操作
${h(notice.title)}${h(notice.summary)}${h(notice.category)}${h(notice.author)}${formatDate(notice.publishAt || notice.createdAt,true)}${notice.pinned ? '首页置顶' : '普通'}${badge(notice.status)}
`; + return `

发布状态会实时同步到公开首页和考生中心。

${notices.map(notice => ``).join('')}
通知标题分类作者发布时间展示状态操作
${h(notice.title)}${h(notice.summary)}${h(notice.category)}${h(notice.author)}${formatDate(notice.publishAt || notice.createdAt,true)}${notice.pinned ? '首页置顶' : '普通'}${badge(notice.status)}
`; } 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 `
${data.instances.map(instance => { + const visibleTypes = [...new Set(data.instances.map(instance => instance.businessType))]; + const toolbar = `
`; + return `${toolbar}
${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 `
${h(typeNames[instance.businessType] || instance.businessType)}

${h(title)}

${h(sub)}

${badge(instance.status)}
${instance.steps.map(step => `
${step.position < instance.currentStep || instance.status === 'approved' ? '✓' : step.position}${h(step.name)}${h(statusLabels[step.adminLevel])}
`).join('')}
当前责任人${h(instance.assignee?.displayName || '流程已结束')}${h(instance.currentStepDetail?.name || statusLabels[instance.status])}
${instance.actions.length ? `${h(actionNames[instance.actions.at(-1).action] || instance.actions.at(-1).action)} · ${h(instance.actions.at(-1).actorName)}` : '尚无操作记录'}
`; + return `
${h(typeNames[instance.businessType] || instance.businessType)}

${h(title)}

${h(sub)}

${badge(instance.status)}
${instance.steps.map(step => `
${step.position < instance.currentStep || instance.status === 'approved' ? '✓' : step.position}${h(step.name)}${h(statusLabels[step.adminLevel])}
`).join('')}
当前责任人${h(instance.assignee?.displayName || '流程已结束')}${h(instance.currentStepDetail?.name || statusLabels[instance.status])}
`; }).join('') || emptyState('暂无审批流程', '考生资料、考试报名、考点档案或批量建号提交后,流程会显示在这里。')}
`; } diff --git a/src/routes/admin.routes.mjs b/src/routes/admin.routes.mjs index 1675041..6037d58 100644 --- a/src/routes/admin.routes.mjs +++ b/src/routes/admin.routes.mjs @@ -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) }; }); diff --git a/styles.css b/styles.css index 5d3afb0..7249b6a 100644 --- a/styles.css +++ b/styles.css @@ -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; } +} diff --git a/tests/system.test.mjs b/tests/system.test.mjs index e3a5d33..b223c87 100644 --- a/tests/system.test.mjs +++ b/tests/system.test.mjs @@ -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');