This commit is contained in:
2026-07-21 15:43:51 +08:00 Unverified
parent bca22a3a9e
commit 4929858ec2
5 changed files with 268 additions and 23 deletions
+43 -15
View File
@@ -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>`;
}
+5
View File
@@ -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)
};
});