This commit is contained in:
biss committed 2026-07-21 15:07:19 +08:00
1 parent 6be1ff227a
commit 0b423e565b
8 files changed
+222 -36

No files matched your search

+62 -5
View File
@@ -59,7 +59,7 @@ function renderError(error) {
}
const baseViewContext = { state, app, h, formatDate, dateRange, badge, money, passPolicyText, statusLabels, icons, api, renderError, emptyState };
const { brand, renderHome, renderAnnouncements, renderAuth } = createPublicViews(baseViewContext);
const { brand, renderHome, renderNoticeCenter, renderAuth } = createPublicViews(baseViewContext);
const { adminNavForUser, portalShell, loadingPanel, renderCandidate, accountSecurity } = createCandidateViews({ ...baseViewContext, brand });
const { renderAdmin, workflowStepEditor } = createAdminViews({ ...baseViewContext, brand, portalShell, loadingPanel, adminNavForUser, accountSecurity });
const { renderAdmission } = createAdmissionViews({ ...baseViewContext, brand });
@@ -75,7 +75,8 @@ async function renderRoute() {
const route = location.hash.slice(1) || 'home';
const [section, page = 'dashboard'] = route.split('/');
if (section === 'home') renderHome();
else if (section === 'announcements') { state.publicAnnouncements = await api('/api/public/announcements'); renderAnnouncements(state.publicAnnouncements); }
else if (section === 'notices' || section === 'announcements') { state.publicAnnouncements = await api('/api/public/announcements'); renderNoticeCenter(state.publicAnnouncements); }
else if (section === 'notice') { state.publicAnnouncements = await api('/api/public/announcements'); renderNoticeCenter(state.publicAnnouncements, page); }
else if (section === 'login' || section === 'register') renderAuth(section);
else if (section === 'candidate') await renderCandidate(page);
else if (section === 'admin') await renderAdmin(page);
@@ -103,6 +104,19 @@ function updateRegistrationSelection() {
document.querySelectorAll('[data-action="bulk-registration-review"]').forEach(button => { button.disabled = selected.length === 0; });
}
function updateQualificationSelection(container) {
if (!container) return;
const rows = [...container.querySelectorAll('[data-qualification-select]')];
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;
}
const count = container.querySelector('[data-qualification-selected-count]');
if (count) count.textContent = `已选 ${selected.length} 人`;
}
function applyTableFilters(tableId) {
const table = document.getElementById(tableId);
if (!table) return;
@@ -182,9 +196,20 @@ document.addEventListener('click', async event => {
await refreshPublic(); navigate('home'); toast('已安全退出', '期待下次见面'); return;
}
if (action === 'open-notice') {
const notice = state.publicData.notices.find(item => item.id === target.dataset.id) || (await api(`/api/public/notices/${target.dataset.id}`)).notice;
const contentHtml = notice.contentHtml || `<p>${h(notice.content).replace(/\r?\n/g, '</p><p>')}</p>`;
setModal(`<div class="modal-head"><div><span>${h(notice.category)}</span><h2>${h(notice.title)}</h2><p>${formatDate(notice.publishAt,true)} · ${h(notice.author)}</p></div><button data-action="close-modal">×</button></div><article class="notice-content">${contentHtml}</article><div class="modal-foot"><button class="ghost-button" data-action="close-modal">关闭</button></div>`); return;
return navigate(`notice/${target.dataset.id}`);
}
if (action === 'notice-category') {
state.noticeCategory = target.dataset.category;
state.noticePage = 1;
renderNoticeCenter(state.publicAnnouncements);
return;
}
if (action === 'notice-page') {
if (target.disabled) return;
state.noticePage = Number(target.dataset.page || 1);
renderNoticeCenter(state.publicAnnouncements);
window.scrollTo({ top: 0, behavior: 'smooth' });
return;
}
if (action === 'download-admit') { window.location.href = `/api/candidate/registrations/${target.dataset.id}/admit-card`; return; }
if (action === 'download-admitted-candidates') {
@@ -230,6 +255,37 @@ document.addEventListener('click', async event => {
toast(data.published ? '资格已确认并自动公示' : '资格已确认', data.published ? '本校全部考生已确认完成' : '继续核对其他考生');
return renderRoute();
}
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; });
updateQualificationSelection(container);
return;
}
if (action === 'bulk-indicator-qualification') {
const toolbar = target.closest('[data-qualification-bulk]');
const ledger = toolbar?.closest('.qualification-ledger');
const userIds = [...ledger.querySelectorAll('[data-qualification-select]:checked')].map(input => input.closest('tr').dataset.userId);
const value = toolbar.querySelector('[data-bulk-eligible]').value;
if (!userIds.length) return toast('请先选择考生', '可使用左侧复选框或全选');
if (!value) return toast('请选择批量资格结论');
if (!window.confirm(`确认将所选 ${userIds.length} 名考生批量设为“${value === 'true' ? '有' : '无'}指标分配资格”?`)) return;
const data = await api(`/api/admin/indicator-qualifications/${toolbar.dataset.examId}/bulk`, { method: 'PUT', body: { userIds, eligible: value === 'true' } });
toast(data.published ? '批量确认完成并自动公示' : '批量确认完成', `已更新 ${data.count} 名考生`);
return renderRoute();
}
if (action === 'toggle-admin-account') {
const active = target.dataset.active === 'true';
if (!active && !window.confirm('确认停用该管理员账户?其现有登录会话会立即失效,历史审批记录将保留。')) return;
await api(`/api/admin/admins/${target.dataset.id}`, { method: 'PATCH', body: { active } });
toast(active ? '管理员账户已启用' : '管理员账户已停用', active ? '该账号可以重新登录' : '现有会话已结束,历史记录仍保留');
return renderRoute();
}
if (action === 'reset-admin-password') {
if (!window.confirm('确认重置该管理员密码?其现有登录会话会立即失效。')) return;
const data = await api(`/api/admin/admins/${target.dataset.id}/reset-password`, { method: 'POST' });
setModal(`<div class="modal-head"><div><span>ADMIN PASSWORD RESET</span><h2>管理员临时密码已生成</h2><p>请通过线下安全渠道交给管理员,本页面关闭后不再展示。</p></div><button data-action="close-modal">×</button></div><div class="issued-number"><span>登录账号</span><strong>${h(data.username)}</strong><span>临时密码</span><strong>${h(data.temporaryPassword)}</strong><p>旧密码和现有登录会话均已失效。</p></div><div class="modal-foot"><button class="solid-button" data-action="close-modal">我已保存</button></div>`);
return;
}
if (action === 'add-admission-category') {
const sources = state.pageData?.sourceSchools || [];
target.closest('form')?.querySelector('[data-admission-categories]')?.insertAdjacentHTML('beforeend', admissionCategoryEditor(h, sources));
@@ -451,6 +507,7 @@ document.addEventListener('input', event => {
});
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-registration-select-all]')) {
+7 -2
View File
@@ -177,7 +177,12 @@ 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 => { const status = item.qualificationStatus; 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>'}<div class="table-scroll"><table><thead><tr><th>报名号 / 姓名</th><th>特长类型</th><th>指标分配资格</th><th>确认时间</th><th>保存</th></tr></thead><tbody>${status.rows.map(row => `<tr data-exam-id="${h(item.examId)}" data-user-id="${h(row.userId)}"><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('') || '<tr><td colspan="5" class="empty-state">本校暂无资料已完善的在册考生</td></tr>'}</tbody></table></div></section>`; }).join('')}`;
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 => {
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>`;
}).join('')}`;
}
function adminNotices(notices) {
@@ -232,7 +237,7 @@ export function createAdminViews(context) {
}
function adminUsers(data) {
return `<section class="panel registration-policy"><div><span>SELF REGISTRATION</span><h2>考生自主注册</h2><p>${data.selfRegistrationEnabled ? '公开入口已开放,考生可以自主申请固定报名号。' : '当前由学校统一创建账户、下发报名号和初始密码。'}</p></div><form data-form="self-registration-setting"><input type="hidden" name="enabled" value="${data.selfRegistrationEnabled ? 'false' : 'true'}"><span class="policy-state ${data.selfRegistrationEnabled ? 'open' : ''}">${data.selfRegistrationEnabled ? '已开放' : '已关闭'}</span><button class="${data.selfRegistrationEnabled ? 'ghost-button' : 'solid-button'}" type="submit">${data.selfRegistrationEnabled ? '关闭自主注册' : '开启自主注册'}</button></form></section><section class="panel data-panel"><div class="data-toolbar"><p>管理员层级决定可见范围和可执行操作;同一级可创建多名账号。</p></div><div class="table-scroll"><table><thead><tr><th>管理员</th><th>登录账号</th><th>层级</th><th>绑定范围</th><th>状态</th></tr></thead><tbody>${data.admins.map(item => `<tr><td><div class="person-cell"><span>${h(item.displayName.slice(0, 1))}</span><div><strong>${h(item.displayName)}</strong><small>${h(item.id)}</small></div></div></td><td class="mono">${h(item.username)}</td><td><span class="admin-level level-${h(item.adminLevel)}">${h(item.levelName)}</span></td><td><strong>${h(item.schoolName || '全局')}</strong><small>${h(item.className || '')}</small></td><td>${item.active ? badge('approved') : badge('closed')}</td></tr>`).join('')}</tbody></table></div></section>`;
return `<section class="panel registration-policy"><div><span>SELF REGISTRATION</span><h2>考生自主注册</h2><p>${data.selfRegistrationEnabled ? '公开入口已开放,考生可以自主申请固定报名号。' : '当前由学校统一创建账户、下发报名号和初始密码。'}</p></div><form data-form="self-registration-setting"><input type="hidden" name="enabled" value="${data.selfRegistrationEnabled ? 'false' : 'true'}"><span class="policy-state ${data.selfRegistrationEnabled ? 'open' : ''}">${data.selfRegistrationEnabled ? '已开放' : '已关闭'}</span><button class="${data.selfRegistrationEnabled ? 'ghost-button' : 'solid-button'}" type="submit">${data.selfRegistrationEnabled ? '关闭自主注册' : '开启自主注册'}</button></form></section><section class="panel data-panel admin-account-ledger"><div class="data-toolbar"><p>管理员账户不物理删除;停用会立即结束现有会话,并完整保留审批和审计记录。</p></div><div class="table-scroll"><table><thead><tr><th>管理员</th><th>登录账号</th><th>层级</th><th>绑定范围</th><th>状态</th><th>账户操作</th></tr></thead><tbody>${data.admins.map(item => { const self = item.id === state.user.id; return `<tr><td><div class="person-cell"><span>${h(item.displayName.slice(0, 1))}</span><div><strong>${h(item.displayName)}</strong><small>${h(item.id)}</small></div></div></td><td class="mono">${h(item.username)}</td><td><span class="admin-level level-${h(item.adminLevel)}">${h(item.levelName)}</span></td><td><strong>${h(item.schoolName || '全局')}</strong><small>${h(item.className || '')}</small></td><td>${item.active ? badge('approved') : badge('closed')}</td><td><div class="admin-account-actions"><button class="row-action" data-action="reset-admin-password" data-id="${h(item.id)}" ${self ? 'disabled title="当前账号请到账户安全修改密码"' : ''}>重置密码</button><button class="row-action ${item.active ? 'danger' : 'primary'}" data-action="toggle-admin-account" data-id="${h(item.id)}" data-active="${item.active ? 'false' : 'true'}" ${self ? 'disabled title="不能停用当前账号"' : ''}>${item.active ? '停用账户' : '重新启用'}</button></div></td></tr>`; }).join('')}</tbody></table></div></section>`;
}
function adminCenters(data) {
+3 -4
View File
@@ -28,10 +28,9 @@ export function createCandidateViews(context) {
const level = state.user?.adminLevel || 'super';
const core = [['dashboard', '工作台', 'home'], ['candidates', level === 'class' ? '本班考生' : '考生信息', 'users'], ['registrations', level === 'class' ? '报名状态' : '报名审核', 'check'], ['payments', level === 'class' ? '缴费确认' : '缴费名单', 'ticket'], ['results', level === 'super' ? '成绩发布' : '成绩查看', 'chart']];
const security = ['security', '账户安全', 'user'];
if (level === 'class') return [core[0], core[1], core[2], core[3], ['admit', '本班准考证', 'ticket'], ['flows', '流程中心', 'check'], core[4], security];
const operations = [['flows', '流程中心', 'check'], ['centers', '考场信息', 'exam']];
if (level === 'school') return [core[0], ['organization', '本校组织', 'users'], ['account-batches', '批量建号', 'ticket'], core[1], ['indicator-qualifications', '指标资格确认', 'check'], core[2], core[3], ['admit', '校内准考证', 'ticket'], ...operations, core[4], security];
return [core[0], ['schools', '学校管理', 'exam'], ['admins', '管理员', 'users'], core[1], core[2], core[3], ['flows', '流程监督', 'check'], ['flow-design', '流程设计', 'exam'], ['number-rules', '报名号规则', 'ticket'], ['centers', '考场信息', 'exam'], ['exams', '考试与科目', 'exam'], ['admissions', '招生录取', 'check'], ['notices', '通知发布', 'bell'], ['admit', '准考证编排', 'ticket'], core[4], security];
if (level === 'class') return [core[0], core[1], core[2], core[3], ['admit', '本班准考证', 'ticket'], core[4], ['flows', '流程中心', 'check'], security];
if (level === 'school') return [core[0], ['organization', '本校组织', 'users'], ['account-batches', '批量建号', 'ticket'], core[1], ['indicator-qualifications', '指标资格确认', 'check'], core[2], core[3], ['admit', '校内准考证', 'ticket'], core[4], ['centers', '考场信息', 'exam'], ['flows', '流程中心', 'check'], security];
return [core[0], ['schools', '学校管理', 'exam'], ['admins', '管理员', 'users'], core[1], ['exams', '考试与科目', 'exam'], core[2], core[3], ['admit', '准考证编排', 'ticket'], core[4], ['admissions', '招生录取', 'check'], ['notices', '通知发布', 'bell'], ['centers', '考场信息', 'exam'], ['flows', '流程监督', 'check'], ['flow-design', '流程设计', 'exam'], ['number-rules', '报名号规则', 'ticket'], security];
}
function portalShell(role, page, content, title, description) {
Loaded 3 of 8 files, more files were not shown because too many files have changed in this diff. Show more