This commit is contained in:
2026-07-21 07:42:18 +08:00 Unverified
parent 1287544c41
commit e3a4ec81db
7 changed files with 139 additions and 32 deletions
+19 -8
View File
@@ -34,7 +34,7 @@ export function createAdminViews(context) {
dashboard: ['考务工作台', '掌握当前报名、审核和发布任务。'], candidates: ['考生资料审核', '核验考生实名、学籍与联系信息。'],
schools: ['学校管理', '创建和维护学校档案,控制学校在考生公开入口中的可选状态。'],
registrations: ['考试报名审核', '确认考生所报考试、科目与缴费状态。'], exams: ['考试与科目', '创建考试、配置报名时间与考试科目。'],
payments: [state.user.adminLevel === 'class' ? '考生缴费确认' : '缴费名单', state.user.adminLevel === 'class' ? '考生线下缴费后,由班级负责人确认并记录办理时间。' : '查看导出当前管理范围内的考试缴费名单。'],
payments: [state.user.adminLevel === 'class' ? '考生缴费确认' : '缴费名单', '查看导出并修改当前管理范围内考生的考试缴费状态。'],
notices: ['通知发布', '发布后立即展示在公开首页和考生中心。'], admit: [state.user.adminLevel === 'super' ? '准考证编排' : state.user.adminLevel === 'school' ? '校内准考证' : '本班准考证', state.user.adminLevel === 'super' ? '按整场考试预检容量,并批量分配固定考点、分科考场与准考证号。' : '按当前管理范围批量下载准考证,并导出逐科准考证信息。'],
results: [state.user.adminLevel === 'super' ? '成绩管理中心' : '成绩分析', state.user.adminLevel === 'super' ? '按考试录入、预览导入、发布并跟踪各科达线情况。' : '按考试、科目和发布状态分析管理范围内成绩。'],
admins: ['分级管理员', '同一级可以配置多名管理员,并分别绑定学校或班级。'],
@@ -66,7 +66,7 @@ export function createAdminViews(context) {
function adminDashboard(data) {
const m = data.metrics;
const canFlow = true;
return `<section class="scope-banner"><span>${statusLabels[state.user.adminLevel]}</span><div><strong>${h(data.scopeLabel)}</strong><small>所有指标均已按当前管理员的数据范围过滤</small></div></section><div class="admin-metrics"><article><span>${icons.users}</span><div><small>范围内考生</small><strong>${m.candidates}</strong><em>${m.pendingCandidates} 人待审核</em></div></article><article><span>${icons.check}</span><div><small>考试报名</small><strong>${m.registrations}</strong><em>${m.pendingRegistrations} 条待审核</em></div></article><article><span>${icons.ticket}</span><div><small>待确认缴费</small><strong>${m.pendingPayments ?? 0}</strong><em>${state.user.adminLevel === 'class' ? '由本班负责人办理' : '按管理范围统计'}</em></div></article><article><span>${icons.exam}</span><div><small>待处理流程</small><strong>${m.pendingFlows ?? 0}</strong><em>进入流程中心办理</em></div></article><article><span>${icons.chart}</span><div><small>已发布考试</small><strong>${m.publishedExams}</strong><em>全平台考试计划</em></div></article></div><div class="admin-dashboard-grid"><section class="panel admin-todos"><div class="panel-title"><h2>${canFlow ? '当前工作入口' : '本班查询入口'}</h2><span>${h(data.scopeLabel)}</span></div><button data-route="admin/candidates"><i class="urgent">${m.pendingCandidates}</i><span><strong>${state.user.adminLevel === 'class' ? '查看本班考生' : '考生资料流程'}</strong><small>身份、学籍与联系方式</small></span>${icons.arrow}</button><button data-route="admin/registrations"><i>${m.pendingRegistrations}</i><span><strong>${state.user.adminLevel === 'class' ? '查看报名状态' : '考试报名流程'}</strong><small>考试、科目和报名号</small></span>${icons.arrow}</button><button data-route="admin/payments"><i>${m.pendingPayments ?? 0}</i><span><strong>${state.user.adminLevel === 'class' ? '确认本班缴费' : '查看缴费名单'}</strong><small>线下缴费确认与名单导出</small></span>${icons.arrow}</button>${canFlow ? `<button data-route="admin/flows"><i>${m.pendingFlows ?? 0}</i><span><strong>流程中心</strong><small>处理、转交与监督审批</small></span>${icons.arrow}</button>` : ''}<button data-route="admin/results"><i>成</i><span><strong>${state.user.adminLevel === 'super' ? '录入与发布成绩' : '查看范围内成绩'}</strong><small>成绩可见范围由权限控制</small></span>${icons.arrow}</button></section><section class="panel audit-feed"><div class="panel-title"><h2>最近操作</h2><span>系统审计日志</span></div>${data.logs.map(log => `<div><span class="user-avatar">${h((log.actorName || '系').slice(0,1))}</span><p><strong>${h(log.actorName || '系统')} · ${h(log.action)}</strong><small>${h(log.detail)}</small></p><time>${formatDate(log.createdAt,true)}</time></div>`).join('') || '<p class="empty-state">当前账号暂无操作记录</p>'}</section></div>`;
return `<section class="scope-banner"><span>${statusLabels[state.user.adminLevel]}</span><div><strong>${h(data.scopeLabel)}</strong><small>所有指标均已按当前管理员的数据范围过滤</small></div></section><div class="admin-metrics"><article><span>${icons.users}</span><div><small>范围内考生</small><strong>${m.candidates}</strong><em>${m.pendingCandidates} 人待审核</em></div></article><article><span>${icons.check}</span><div><small>考试报名</small><strong>${m.registrations}</strong><em>${m.pendingRegistrations} 条待审核</em></div></article><article><span>${icons.ticket}</span><div><small>待确认缴费</small><strong>${m.pendingPayments ?? 0}</strong><em>可按当前管理范围办理</em></div></article><article><span>${icons.exam}</span><div><small>待处理流程</small><strong>${m.pendingFlows ?? 0}</strong><em>进入流程中心办理</em></div></article><article><span>${icons.chart}</span><div><small>已发布考试</small><strong>${m.publishedExams}</strong><em>全平台考试计划</em></div></article></div><div class="admin-dashboard-grid"><section class="panel admin-todos"><div class="panel-title"><h2>${canFlow ? '当前工作入口' : '本班查询入口'}</h2><span>${h(data.scopeLabel)}</span></div><button data-route="admin/candidates"><i class="urgent">${m.pendingCandidates}</i><span><strong>${state.user.adminLevel === 'class' ? '查看本班考生' : '考生资料流程'}</strong><small>身份、学籍与联系方式</small></span>${icons.arrow}</button><button data-route="admin/registrations"><i>${m.pendingRegistrations}</i><span><strong>${state.user.adminLevel === 'class' ? '查看报名状态' : '考试报名流程'}</strong><small>考试、科目和报名号</small></span>${icons.arrow}</button><button data-route="admin/payments"><i>${m.pendingPayments ?? 0}</i><span><strong>管理缴费名单</strong><small>线下缴费状态修改与名单导出</small></span>${icons.arrow}</button>${canFlow ? `<button data-route="admin/flows"><i>${m.pendingFlows ?? 0}</i><span><strong>流程中心</strong><small>处理、转交与监督审批</small></span>${icons.arrow}</button>` : ''}<button data-route="admin/results"><i>成</i><span><strong>${state.user.adminLevel === 'super' ? '录入与发布成绩' : '查看范围内成绩'}</strong><small>成绩可见范围由权限控制</small></span>${icons.arrow}</button></section><section class="panel audit-feed"><div class="panel-title"><h2>最近操作</h2><span>系统审计日志</span></div>${data.logs.map(log => `<div><span class="user-avatar">${h((log.actorName || '系').slice(0,1))}</span><p><strong>${h(log.actorName || '系统')} · ${h(log.action)}</strong><small>${h(log.detail)}</small></p><time>${formatDate(log.createdAt,true)}</time></div>`).join('') || '<p class="empty-state">当前账号暂无操作记录</p>'}</section></div>`;
}
function excelToolbar(resource, { importable = true, template = true, label = '数据' } = {}) {
@@ -97,12 +97,19 @@ export function createAdminViews(context) {
}
function adminRegistrations(registrations) {
const readOnly = false;
const rows = items => items.map(reg => `<tr data-status="${h(reg.status)}"><td><div class="person-cell"><span>${h((reg.candidate?.name || '?').slice(0,1))}</span><div><strong>${h(reg.candidate?.name)}</strong><small>${h(reg.candidate?.grade || '')}</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)}">${readOnly ? '查看' : '查看流程'}</button>`}</td></tr>`).join('');
const table = (items, id) => `<div class="table-scroll"><table id="${id}"><thead><tr><th>考生</th><th>考试 / 科目</th><th>账户报名号</th><th>当前流程</th><th>缴费</th><th>状态</th><th>操作</th></tr></thead><tbody>${rows(items)}</tbody></table></div>`;
const isSuper = state.user.adminLevel === 'super';
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>`;
}).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);
return `<section class="panel data-panel"><div class="data-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>${table(current, 'registrationTable')}</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 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>` : ''}`;
}
function adminPayments(data) {
@@ -111,8 +118,12 @@ export function createAdminViews(context) {
const unpaid = registrations.filter(item => item.paymentStatus === 'unpaid');
const totalDue = registrations.reduce((sum, item) => sum + Number(item.amountDue || 0), 0);
const totalPaid = paid.reduce((sum, item) => sum + Number(item.amountDue || 0), 0);
const rows = registrations.map(item => `<tr data-status="${h(item.paymentStatus)}"><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.className)}</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.canConfirmPayment && item.paymentStatus === 'unpaid' && !item.exam?.archivedAt ? `<button class="row-action primary" data-action="confirm-payment" data-id="${h(item.id)}" data-name="${h(item.candidate?.name || '')}" data-exam="${h(item.exam?.name || '')}">确认已缴费</button>` : item.exam?.archivedAt ? '<span class="archive-readonly-label">只读封存</span>' : '<span>—</span>'}</td></tr>`).join('');
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"><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></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 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 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>`;
}
function adminExams(exams) {