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
+75 -4
View File
@@ -81,6 +81,38 @@ function formObject(form) {
return Object.fromEntries(new FormData(form).entries());
}
function updateRegistrationSelection() {
const table = document.querySelector('#registrationTable');
if (!table) return;
const selectable = [...table.querySelectorAll('[data-registration-select]:not(:disabled)')];
const visible = selectable.filter(input => !input.closest('tr').hidden);
const selected = selectable.filter(input => input.checked);
const selectAll = table.querySelector('[data-registration-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-registration-selection-count]');
if (count) count.textContent = selected.length;
document.querySelectorAll('[data-action="bulk-registration-review"]').forEach(button => { button.disabled = selected.length === 0; });
}
function applyTableFilters(tableId) {
const table = document.getElementById(tableId);
if (!table) return;
const search = [...document.querySelectorAll('[data-action="table-search"]')].find(input => input.dataset.target === 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 => {
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);
row.hidden = !(matchesSearch && matchesStatus && matchesFilters);
});
if (tableId === 'registrationTable') updateRegistrationSelection();
}
async function refreshPublic() {
state.publicData = await api('/api/public/home');
}
@@ -227,12 +259,43 @@ document.addEventListener('click', async event => {
toast(archived ? '账户已归档' : '账户已恢复', `${data.scopeLabel} · ${data.count} 个账户状态已更新`); return renderRoute();
}
if (action === 'review-registration') return openRegistrationReview(target.dataset.id);
if (action === 'bulk-registration-review') {
const ids = [...document.querySelectorAll('#registrationTable [data-registration-select]:checked')].map(input => input.value);
if (!ids.length) return toast('请先选择报名记录', '仅待审核记录可批量处理');
const status = target.dataset.status;
const promptText = status === 'rejected' ? '请填写批量退回原因(必填)' : '填写批量审核意见(可留空)';
const reviewNote = window.prompt(promptText, '');
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/registrations/${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' });
toast('缴费已确认', '考生端状态、确认人和确认时间已同步记录');
return renderRoute();
}
if (action === 'update-payment') {
const paid = target.dataset.status === 'paid';
const message = paid
? `确认将 ${target.dataset.name} 的“${target.dataset.exam}”标记为已缴费吗?`
: `确认撤销 ${target.dataset.name} 的“${target.dataset.exam}”缴费记录,并改为待缴费吗?`;
if (!window.confirm(message)) return;
await api(`/api/admin/payments/${target.dataset.id}`, { method: 'PATCH', body: { status: target.dataset.status } });
toast(paid ? '已标记为已缴费' : '已改为待缴费', paid ? '确认人和确认时间已同步记录' : '原确认人和确认时间已清除');
return renderRoute();
}
if (action === 'excel-download') {
const query = new URLSearchParams();
if (target.dataset.template === '1') query.set('template', '1');
@@ -297,15 +360,14 @@ document.addEventListener('click', async event => {
}
if (action === 'status-filter') {
target.parentElement.querySelectorAll('button').forEach(button => button.classList.toggle('active', button === target));
document.querySelectorAll(`#${target.dataset.target} tbody tr`).forEach(row => row.hidden = target.dataset.status !== 'all' && !String(row.dataset.status || '').split(/\s+/).includes(target.dataset.status));
applyTableFilters(target.dataset.target);
}
} catch (error) { toast('操作未完成', error.message); }
});
document.addEventListener('input', event => {
if (event.target.matches('[data-action="table-search"]')) {
const query = event.target.value.trim().toLowerCase();
document.querySelectorAll(`#${event.target.dataset.target} tbody tr`).forEach(row => row.hidden = !row.textContent.toLowerCase().includes(query));
applyTableFilters(event.target.dataset.target);
}
if (event.target.matches('.subject-options input')) {
const form = event.target.closest('form');
@@ -320,6 +382,15 @@ document.addEventListener('input', event => {
});
document.addEventListener('change', event => {
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-registration-select-all]')) {
const table = document.getElementById(event.target.dataset.target);
table?.querySelectorAll('[data-registration-select]:not(:disabled)').forEach(input => {
if (!input.closest('tr').hidden) input.checked = event.target.checked;
});
updateRegistrationSelection();
}
if (event.target.matches('[data-excel-file]')) {
const input = event.target;
const file = input.files?.[0];
@@ -674,7 +745,7 @@ 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;
const canReview = reg.status === 'pending' && (reg.workflow?.assignee?.id === state.user.id || state.user.adminLevel === 'super');
setModal(`<div class="modal-head"><div><span>REGISTRATION REVIEW</span><h2>${canReview ? '处理' : '查看'}考试报名</h2><p>${h(reg.candidate?.name)} · ${h(reg.exam.name)}</p></div><button data-action="close-modal">×</button></div><div class="registration-review"><div><span>报考科目</span><p>${reg.subjects.map(subject => `<b>${h(subject.name)}</b>`).join('')}</p></div><dl><div><dt>账户报名号</dt><dd class="mono">${h(reg.registrationNumber || reg.candidate?.candidateNumber || '账户号码异常')}</dd></div><div><dt>当前步骤</dt><dd>${h(reg.workflow?.currentStepDetail?.name || '流程已结束')}</dd></div><div><dt>责任人</dt><dd>${h(reg.workflow?.assignee?.displayName || '—')}</dd></div><div><dt>缴费状态</dt><dd>${badge(reg.paymentStatus)}</dd></div></dl></div>${canReview ? `<form class="modal-form" data-form="registration-review"><input type="hidden" name="id" value="${h(reg.id)}"><label><span>审核结论</span><select name="status"><option value="approved">通过当前步骤</option><option value="rejected">退回报名</option></select></label><label><span>审核意见</span><textarea name="reviewNote" rows="3" placeholder="可填写审核说明">${h(reg.reviewNote || '')}</textarea></label><div class="modal-foot"><button type="button" class="ghost-button" data-action="close-modal">取消</button><button type="submit" class="solid-button">确认处理</button></div></form>` : '<div class="modal-foot"><button class="solid-button" data-action="close-modal">关闭</button></div>'}`);
}