const state = {
user: null,
profile: null,
publicData: { organization: {}, notices: [], exams: [], stats: {} },
permissions: [],
scopeLabel: '',
pageData: null,
loading: false
};
const app = document.querySelector('#app');
const modalRoot = document.querySelector('#modalRoot');
const statusLabels = {
pending: '待审核', approved: '已通过', rejected: '需修改',
published: '已发布', draft: '草稿', closed: '已结束',
open: '报名中', upcoming: '即将开始', paid: '已缴费', unpaid: '待缴费',
super: '超级管理员', school: '校级管理员', class: '班级管理员'
};
const icons = {
home: ' ',
user: ' ',
exam: ' ',
ticket: ' ',
chart: ' ',
bell: ' ',
users: ' ',
check: ' ',
plus: ' ',
logout: ' ',
menu: ' ',
search: ' ',
arrow: ' '
};
function h(value) {
return String(value ?? '').replace(/[&<>'"]/g, char => ({ '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' }[char]));
}
function formatDate(value, withTime = false) {
if (!value) return '待定';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return h(value);
return new Intl.DateTimeFormat('zh-CN', withTime ? { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' } : { year: 'numeric', month: '2-digit', day: '2-digit' }).format(date);
}
function dateRange(start, end) {
return `${formatDate(start)} — ${formatDate(end)}`;
}
function badge(status) {
return `${h(statusLabels[status] || status)} `;
}
function money(value) {
return `¥${Number(value || 0).toFixed(2)}`;
}
async function api(path, options = {}) {
const response = await fetch(path, {
credentials: 'same-origin',
headers: { ...(options.body ? { 'Content-Type': 'application/json' } : {}), ...options.headers },
...options,
body: options.body && typeof options.body !== 'string' ? JSON.stringify(options.body) : options.body
});
const type = response.headers.get('content-type') || '';
const data = type.includes('application/json') ? await response.json() : await response.text();
if (!response.ok) throw new Error(data?.message || '操作未完成,请稍后重试');
return data;
}
let toastTimer;
function toast(title, message = '') {
const element = document.querySelector('#toast');
element.querySelector('strong').textContent = title;
element.querySelector('small').textContent = message;
element.classList.add('show');
clearTimeout(toastTimer);
toastTimer = setTimeout(() => element.classList.remove('show'), 2800);
}
function setModal(content) {
modalRoot.innerHTML = `
`;
setTimeout(() => modalRoot.querySelector('input,textarea,button')?.focus(), 30);
}
function closeModal() {
modalRoot.innerHTML = '';
}
function brand() {
return `衡准 EXAM SERVICE `;
}
function publicHeader() {
return ``;
}
function renderHome() {
const { notices, exams, stats, organization } = state.publicData;
const featured = exams.find(exam => exam.registrationState === 'open') || exams[0];
const topNotice = notices[0];
app.innerHTML = `${publicHeader()}
最新 ${h(topNotice?.title || '欢迎使用衡准考试服务平台')}
HAIZHOU EXAMINATION SERVICE
一个报名号,贯穿每一次考试。 使用学校下发的报名号登录,完成密码更新和个人信息核验后,即可办理所有考试事项。
${state.user?.role === 'candidate' ? `进入考生中心 ${icons.arrow} ` : state.publicData.selfRegistrationEnabled ? `申请报名号 ${icons.arrow} ` : `使用报名号登录 ${icons.arrow} `}查看开放考试
${h(stats.candidates || 0)} 在册考生
${h(stats.registrations || 0)} 报名记录
${h(stats.exams || 0)} 开放考试
${featured ? renderHeroTicket(featured) : '
暂无开放考试
'}
报名、考试、准考证与成绩发布的重要时间,请以平台通知为准。
${topNotice ? `${h(topNotice.category)} ${h(topNotice.title)} ${h(topNotice.summary)}
${formatDate(topNotice.publishAt)} 阅读通知 ${icons.arrow} ` : '暂无通知
'} ${notices.slice(1, 5).map(renderNoticeRow).join('') || '
暂无更多通知
'}
${exams.map(renderPublicExam).join('') || '
当前没有已发布的考试
'}
报名号不会随考试改变,每场考试只新增一条报名记录。
${[['01','领取报名号','学校创建账户并下发初始密码。'],['02','修改初始密码','首次登录必须设置自己的新密码。'],['03','补全个人信息','填写籍贯、住址、手机、邮箱和班级等资料。'],['04','选择考试科目','资料审核通过后自主选择考试。'],['05','下载准考证与查分','继续使用同一报名号办理后续事项。']].map(item => `
${item[0]} ${item[1]} ${item[2]}
`).join('')}
`;
}
function renderHeroTicket(exam) {
const status = exam.registrationState;
return `${badge(status)} ${h(exam.code)} UPCOMING EXAM
${h(exam.name)}
报名时间 ${dateRange(exam.registrationStart, exam.registrationEnd)}
考试时间 ${dateRange(exam.examStart, exam.examEnd)}
考试地点 ${h(exam.location)} ${exam.subjects.slice(0, 5).map(subject => `${h(subject.name)} `).join('')}${exam.subjects.length > 5 ? `+${exam.subjects.length - 5} ` : ''}
报名人数 ${h(exam.registrationCount || 0)} ${status === 'open' ? '立即报名' : '查看详情'}
`;
}
function renderNoticeRow(notice) {
return `${formatDate(notice.publishAt)} ${h(notice.category)} ${h(notice.title)} ${h(notice.summary)} ${icons.arrow} `;
}
function renderPublicExam(exam) {
return `${h(exam.code)} ${badge(exam.registrationState)}${h(exam.name)} ${h(exam.description)}
报名 ${dateRange(exam.registrationStart, exam.registrationEnd)}考试 ${dateRange(exam.examStart, exam.examEnd)}
${exam.subjects.length} 个科目 · ${exam.registrationCount || 0} 人已报名 ${exam.registrationState === 'open' ? '选择科目' : '查看考试'} ${icons.arrow} `;
}
function renderAuth(kind) {
const login = kind === 'login';
const selfRegistration = state.publicData.selfRegistrationEnabled;
app.innerHTML = `${brand()}
CANDIDATE SERVICE
${login ? '凭一个号码,' : '自主申请,'}${login ? '办理每一次考试。' : '领取固定报名号。'} 报名号就是考生账户,不因考试、科目或年度报名而改变。
首次登录顺序 修改初始密码 → 补全个人信息 → 等待资料审核。
← 返回首页 ${login ? 'ACCOUNT LOGIN' : 'CANDIDATE NUMBER'}
${login ? '报名号登录' : '自主申请报名号'} ${login ? '考生填写报名号和密码;管理员继续使用管理账号。' : selfRegistration ? '提交基础学籍范围后,系统生成一个长期使用的报名号。' : '当前未开放自主注册,请联系学校领取报名号和初始密码。'}
${login ? loginForm() : selfRegistration ? registerForm() : '
自主注册已关闭 学校管理员会为考生创建账户并下发初始密码。 返回报名号登录
'}${login && selfRegistration ? `
还没有报名号?自主申请
` : !login ? '
已经有报名号?返回登录
' : ''}${login ? `
演示账号 考生:2026-HZ01-F-0001 / Candidate123! 超级管理员:admin / Admin123! 校级管理员:school_admin / School123! 班级管理员:class_admin / Class123!
` : ''}
`;
}
function loginForm() {
return ``;
}
function registerForm() {
const schools = state.publicData.schools || [];
return ``;
}
const candidateNav = [
['dashboard', '总览', 'home'], ['profile', '个人资料', 'user'], ['exams', '考试报名', 'exam'],
['registrations', '我的报名', 'check'], ['admit', '准考证', 'ticket'], ['results', '成绩查询', 'chart'], ['notices', '通知公告', 'bell']
];
function adminNavForUser() {
const level = state.user?.adminLevel || 'super';
const core = [['dashboard', '工作台', 'home'], ['candidates', level === 'class' ? '本班考生' : '考生信息', 'users'], ['registrations', level === 'class' ? '报名状态' : '报名审核', 'check'], ['results', level === 'super' ? '成绩发布' : '成绩查看', 'chart']];
if (level === 'class') return core;
const operations = [['flows', '流程中心', 'check'], ['centers', '考场信息', 'exam']];
if (level === 'school') return [core[0], core[1], core[2], ...operations, core[3]];
return [core[0], ['admins', '管理员', 'users'], core[1], core[2], ['flows', '流程监督', 'check'], ['flow-design', '流程设计', 'exam'], ['number-rules', '报名号规则', 'ticket'], ['centers', '考场信息', 'exam'], ['exams', '考试与科目', 'exam'], ['notices', '通知发布', 'bell'], ['admit', '准考证生成', 'ticket'], core[3]];
}
function portalShell(role, page, content, title, description) {
const nav = role === 'admin' ? adminNavForUser() : candidateNav;
const roleName = role === 'admin' ? '管理后台' : '考生中心';
const adminTitle = statusLabels[state.user?.adminLevel] || '管理员';
return `${roleName} / ${h(title)}
${role === 'admin' && state.user?.adminLevel !== 'class' ? `${icons.bell} ` : ''}${h((state.user?.displayName || '用').slice(0, 1))} ${h(state.user?.displayName)} ${role === 'admin' ? adminTitle : `资料${statusLabels[state.profile?.status] || '未完善'}`} ${icons.logout}
${role === 'admin' ? 'EXAM OPERATIONS' : 'CANDIDATE SERVICE'}
${h(title)} ${h(description)}
${portalHeadingAction(role, page)}
${content} `;
}
function portalHeadingAction(role, page) {
if (role === 'admin' && page === 'notices') return `${icons.plus} 发布通知 `;
if (role === 'admin' && page === 'exams') return `${icons.plus} 创建考试 `;
if (role === 'admin' && page === 'admins') return `${icons.plus} 添加管理员 `;
if (role === 'admin' && page === 'candidates' && state.user.adminLevel !== 'class') return `${icons.plus} 创建考生账户 `;
if (role === 'admin' && page === 'centers') return `${icons.plus} 提交新考点 `;
if (role === 'candidate' && page === 'profile') return `当前状态 ${badge(state.profile?.status || 'pending')} `;
return '';
}
function loadingPanel() {
return `正在读取数据
`;
}
function onboardingShell(stage, content) {
const passwordDone = stage !== 'password';
return `${brand()}固定报名号 ${h(state.user.candidateNumber)} 这个号码就是你的考生账户。以后参加不同考试,仍然使用同一个报名号。
${passwordDone ? '✓' : '1'} 修改初始密码 设置仅本人知道的新密码
2 补全个人信息 实名、籍贯、住址和学籍信息
3 等待资料审核 审核通过后开始考试报名
退出当前账户 FIRST SIGN-IN ${stage === 'password' ? '先保护你的账户' : '建立完整考生档案'} ${stage === 'password' ? '初始密码只用于第一次登录。修改成功后才可填写个人信息。' : '带 * 的信息会用于身份核验、学校管理范围和考试联系。'}
${content} `;
}
function passwordOnboardingForm() {
return ``;
}
async function renderCandidate(page) {
if (state.user?.role !== 'candidate') return navigate('login');
if (state.user.mustChangePassword) {
app.innerHTML = onboardingShell('password', passwordOnboardingForm());
return;
}
if (!state.profile?.profileCompleted) {
try {
const data = await api('/api/candidate/profile');
state.pageData = data; state.profile = data.profile;
app.innerHTML = onboardingShell('profile', candidateProfile(data, true));
} catch (error) { renderError(error); }
return;
}
const meta = {
dashboard: ['总览', '查看你的资料、报名、准考证与成绩状态。'],
profile: ['个人资料', '维护实名认证与联系方式;修改后需要重新审核。'],
exams: ['考试报名', '在开放时间内选择考试,并自主勾选报考科目。'],
registrations: ['我的报名', '查看已提交的考试、科目与审核进度。'],
admit: ['准考证', '管理员生成后,可在规定下载时间内保存准考证。'],
results: ['成绩查询', '仅显示考试中心已经正式发布的成绩。'],
notices: ['通知公告', '查看与报名、考试和成绩相关的最新消息。']
};
if (!meta[page]) page = 'dashboard';
app.innerHTML = portalShell('candidate', page, loadingPanel(), ...meta[page]);
try {
const endpoint = page === 'dashboard' ? 'dashboard' : page === 'profile' ? 'profile' : page === 'exams' ? 'exams' : page === 'results' ? 'results' : 'registrations';
const data = page === 'notices' ? { notices: state.publicData.notices } : await api(`/api/candidate/${endpoint}`);
state.pageData = data;
if (data.profile) state.profile = data.profile;
const content = {
dashboard: () => candidateDashboard(data), profile: () => candidateProfile(data), exams: () => candidateExams(data),
registrations: () => candidateRegistrations(data.registrations), admit: () => candidateAdmit(data.registrations),
results: () => candidateResults(data.results), notices: () => candidateNotices(data.notices)
}[page]();
app.innerHTML = portalShell('candidate', page, content, ...meta[page]);
} catch (error) { renderError(error); }
}
function candidateDashboard(data) {
const registration = data.registrations[0];
const steps = [
['资料填写', Boolean(data.profile?.name), data.profile?.status === 'rejected' ? '请修改' : '已提交'],
['资料审核', data.profile?.status === 'approved', statusLabels[data.profile?.status] || '待审核'],
['考试报名', Boolean(registration), registration ? '已报名' : '未报名'],
['准考证', Boolean(registration?.admitCard), registration?.admitCard ? '已生成' : '待生成'],
['成绩发布', Boolean(data.results?.length), data.results?.length ? `已发布 ${data.results.length} 科` : '待发布']
];
return `${new Date().getHours() < 12 ? '上午好' : '下午好'} ${h(data.profile?.name || state.user.displayName)},下一步已为你标出。 ${data.profile?.status === 'approved' ? (registration ? '报名已进入考务流程,请留意准考证下载时间。' : '个人资料已通过审核,现在可以选择考试和报考科目。') : '个人资料正在审核中,通过后即可进行考试报名。'}
准 考
${icons.user} 个人资料 ${statusLabels[data.profile?.status] || '未填写'}
${badge(data.profile?.status || 'pending')}${icons.exam} 已报名考试 ${data.registrations.length} 场
去报名 ${icons.ticket} 可下载准考证 ${data.registrations.filter(item => item.admitCard).length} 份
查看 ${icons.chart} 已发布成绩 ${data.results.length} 科
查分
我的应考进度 自动更新 ${steps.map((step, index) => `
${step[1] ? '✓' : index + 1} ${step[0]} ${step[2]}
`).join('')}
最近通知 全部通知 ${data.notices.map(notice => `${formatDate(notice.publishAt)} ${h(notice.title)} `).join('')} `;
}
function candidateProfile(data, onboarding = false) {
const { profile, schools = [], classes = [], workflow } = data;
const step = workflow?.currentStepDetail;
const idNumber = profile?.idNumber?.startsWith('PENDING-') ? '' : profile?.idNumber;
return ``;
}
function candidateExams(data) {
return `${data.exams.map(exam => `
${h(exam.name)} ${h(exam.description)}
报名期限 ${dateRange(exam.registrationStart, exam.registrationEnd)}
考试时间 ${dateRange(exam.examStart, exam.examEnd)}
考点安排 ${h(exam.location)} `).join('')}
`;
}
function candidateRegistrations(registrations) {
return registrations.length ? `${registrations.map(reg => `
账户报名号 ${h(reg.registrationNumber || state.user.candidateNumber)}
当前审批 ${h(reg.workflow?.currentStepDetail?.name || statusLabels[reg.workflow?.status] || '待提交')}
责任人 ${h(reg.workflow?.assignee?.displayName || '—')}
缴费状态 ${badge(reg.paymentStatus)} 已选科目 ${reg.subjects.map(subject => `${h(subject.name)}${h(subject.date)} ${h(subject.start)} `).join('')}
`).join('')}
` : emptyState('还没有考试报名', '资料审核通过后,即可在“考试报名”中选择考试与科目。', 'candidate/exams', '去考试报名');
}
function candidateAdmit(registrations) {
const cards = registrations.filter(reg => reg.admitCard);
return cards.length ? `${cards.map(reg => { const now = Date.now(); const open = now >= new Date(reg.exam.admitDownloadStart).getTime() && now <= new Date(reg.exam.admitDownloadEnd).getTime(); return `
${h(reg.exam.code)} ${badge(open ? 'open' : now < new Date(reg.exam.admitDownloadStart) ? 'upcoming' : 'closed')}${h(reg.exam.name)} 准考证号 ${h(reg.admitCard.number)}
考点 ${h(reg.admitCard.testCenter)}
考场 / 座位 ${h(reg.admitCard.room)} / ${h(reg.admitCard.seat)}
下载时间 ${dateRange(reg.exam.admitDownloadStart, reg.exam.admitDownloadEnd)} ADMISSION CARD ${open ? '下载准考证' : now < new Date(reg.exam.admitDownloadStart) ? '尚未开放' : '下载已结束'} 下载后请使用 A4 纸打印
`; }).join('')}
` : emptyState('准考证尚未生成', '考试报名审核通过后,由管理员统一生成准考证。', 'candidate/registrations', '查看报名状态');
}
function candidateResults(results) {
if (!results.length) return emptyState('暂时没有已发布成绩', '成绩发布后会在这里显示,同时首页会发布查分通知。', 'candidate/notices', '查看通知');
const grouped = Object.groupBy ? Object.groupBy(results, item => item.examName) : results.reduce((acc, item) => ((acc[item.examName] ||= []).push(item), acc), {});
return `${Object.entries(grouped).map(([examName, items]) => `
${items.map(item => `
${h(item.subjectName)} ${h(item.score)} ${h(item.grade)} 满分 150 `).join('')}
`).join('')}
`;
}
function candidateNotices(notices) {
return `${notices.map(notice => `${new Date(notice.publishAt).getDate()} ${new Date(notice.publishAt).toLocaleString('zh-CN',{month:'short'})} ${h(notice.category)} ${h(notice.title)} ${h(notice.summary)} ${notice.pinned ? '置顶 ' : ''}${icons.arrow} `).join('')}
`;
}
async function renderAdmin(page) {
if (state.user?.role !== 'admin') return navigate('login');
const meta = {
dashboard: ['考务工作台', '掌握当前报名、审核和发布任务。'], candidates: ['考生资料审核', '核验考生实名、学籍与联系信息。'],
registrations: ['考试报名审核', '确认考生所报考试、科目与缴费状态。'], exams: ['考试与科目', '创建考试、配置报名时间与考试科目。'],
notices: ['通知发布', '发布后立即展示在公开首页和考生中心。'], admit: ['准考证生成', '为已审核报名分配考点、考场与座位。'],
results: [state.user.adminLevel === 'super' ? '成绩发布' : '成绩查看', state.user.adminLevel === 'super' ? '录入单科成绩并控制是否对考生公开。' : '按数据范围查看已录入成绩。'],
admins: ['分级管理员', '同一级可以配置多名管理员,并分别绑定学校或班级。'],
centers: ['考务场所档案', state.user.adminLevel === 'school' ? '查看本校考点与结构化考场,所有变更提交后进入审批。' : '管理各校考点、考场容量与变更审批台账。'],
flows: [state.user.adminLevel === 'super' ? '流程监督' : '流程中心', state.user.adminLevel === 'super' ? '查看全部流程,监督转交、修改和退回节点。' : '处理分配给你的流程,并可转交给本校同级管理员。'],
'flow-design': ['流程设计', '配置考生信息、报名审核与考点考场变更的审批步骤。'],
'number-rules': ['报名号规则', '设计新账户号码组成,并为旧考试记录批量同步固定报名号。']
};
const allowedPages = adminNavForUser().map(item => item[0]);
if (!meta[page] || !allowedPages.includes(page)) page = 'dashboard';
app.innerHTML = portalShell('admin', page, loadingPanel(), ...meta[page]);
try {
const endpoint = page === 'admit' ? 'registrations' : page === 'flows' ? 'workflow-instances' : page === 'flow-design' ? 'workflows' : page;
const data = await api(`/api/admin/${endpoint}`);
state.pageData = data;
const content = {
dashboard: () => adminDashboard(data), candidates: () => adminCandidates(data.candidates), registrations: () => adminRegistrations(data.registrations),
exams: () => adminExams(data.exams), notices: () => adminNotices(data.notices), admit: () => adminAdmit(data.registrations), results: () => adminResults(data),
admins: () => adminUsers(data), centers: () => adminCenters(data), flows: () => adminFlows(data),
'flow-design': () => adminFlowDesign(data.workflows), 'number-rules': () => adminNumberRules(data)
}[page]();
app.innerHTML = portalShell('admin', page, content, ...meta[page]);
} catch (error) { renderError(error); }
}
function adminDashboard(data) {
const m = data.metrics;
const canFlow = state.user.adminLevel !== 'class';
return `${statusLabels[state.user.adminLevel]} ${h(data.scopeLabel)} 所有指标均已按当前管理员的数据范围过滤
${icons.users} 范围内考生 ${m.candidates} ${m.pendingCandidates} 人待审核
${icons.check} 考试报名 ${m.registrations} ${m.pendingRegistrations} 条待审核
${icons.exam} 待处理流程 ${m.pendingFlows ?? 0} ${canFlow ? '进入流程中心办理' : '班级账号只读'}
${icons.chart} 已发布考试 ${m.publishedExams} 全平台考试计划
${canFlow ? '当前工作入口' : '本班查询入口'} ${h(data.scopeLabel)} ${m.pendingCandidates} ${state.user.adminLevel === 'class' ? '查看本班考生' : '考生资料流程'} 身份、学籍与联系方式 ${icons.arrow}${m.pendingRegistrations} ${state.user.adminLevel === 'class' ? '查看报名状态' : '考试报名流程'} 考试、科目和报名号 ${icons.arrow} ${canFlow ? `${m.pendingFlows ?? 0} 流程中心 处理、转交与监督审批 ${icons.arrow} ` : ''}成 ${state.user.adminLevel === 'super' ? '录入与发布成绩' : '查看范围内成绩'} 成绩可见范围由权限控制 ${icons.arrow}
最近操作 系统审计日志 ${data.logs.map(log => `${h((log.actorName || '系').slice(0,1))} ${h(log.actorName || '系统')} · ${h(log.action)} ${h(log.detail)}
${formatDate(log.createdAt,true)} `).join('') || '当前账号暂无操作记录
'} `;
}
function adminCandidates(candidates) {
const readOnly = state.user.adminLevel === 'class';
return ``;
}
function adminRegistrations(registrations) {
const readOnly = state.user.adminLevel === 'class';
return ``;
}
function adminExams(exams) {
return `${exams.map(exam => `
${h(exam.code)} ${badge(exam.status)}${h(exam.name)} ${h(exam.description)}
报名时间 ${dateRange(exam.registrationStart, exam.registrationEnd)}
考试时间 ${dateRange(exam.examStart, exam.examEnd)}
考点 ${h(exam.location)} ${exam.subjects.map(subject => `${h(subject.name)} ${h(subject.date)} ${h(subject.start)} `).join('') || '科目待配置 '}
`).join('')}
`;
}
function adminNotices(notices) {
return ``;
}
function adminAdmit(registrations) {
const approved = registrations.filter(reg => reg.status === 'approved');
return ``;
}
function adminResults(data) {
const entry = state.user.adminLevel === 'super' ? `` : '';
return `${entry}
${state.user.adminLevel === 'super' ? '最近成绩' : '范围内成绩'} ${data.results.length} 条记录 ${data.results.slice(0, 50).map(result => `${h((result.candidateName || '?').slice(0,1))} ${h(result.candidateName)} · ${h(result.subjectName)} ${h(result.examName)}
${h(result.score)} ${badge(result.published ? 'published' : 'draft')}
`).join('') || '还没有成绩记录
'} `;
}
function adminUsers(data) {
return `SELF REGISTRATION 考生自主注册 ${data.selfRegistrationEnabled ? '公开入口已开放,考生可以自主申请固定报名号。' : '当前由学校统一创建账户、下发报名号和初始密码。'}
`;
}
function adminCenters(data) {
const roomTypeNames = { standard: '标准考场', computer: '机考考场', accessible: '无障碍考场', spare: '备用考场' };
const cards = data.centers.map(center => `${h(center.schoolName)} · ${h(center.code)}
${h(center.name)} ${center.pendingChange ? '变更审批中 ' : ''}${badge(center.status === 'active' ? 'approved' : 'closed')}提交变更
结构化考场 ${center.rooms.length} 个
启用席位 ${center.totalCapacity} 席
开放时间 ${h(center.gateOpenTime || '未设')}
详细地址 ${h(center.address)}
考点负责人 ${h(center.managerName || '未填写')} · ${h(center.managerPhone || center.contact || '未填写')}
应急电话 ${h(center.emergencyPhone || '未填写')}
交通提示 ${h(center.transport || '未填写')} 考场 位置 类型 容量 座位号 状态 ${center.rooms.map(room => `${h(room.name)} ${h(room.code)} ${h(room.building)} · ${h(room.floor || '楼层未填')} ${h(roomTypeNames[room.roomType] || room.roomType)} ${h(room.capacity)} 席 ${h(room.seatStart)}—${h(room.seatEnd)} ${badge(room.status === 'active' ? 'approved' : 'closed')} `).join('')}
${h(center.notes || '无补充说明')} 更新于 ${formatDate(center.updatedAt, true)} `).join('');
const requests = data.changeRequests || [];
return `正式考点 ${data.centers.length}
结构化考场 ${data.centers.reduce((sum, item) => sum + item.rooms.length, 0)}
待审批变更 ${requests.filter(item => item.status === 'pending').length}
${cards || emptyState('还没有正式考点', '提交考点和考场档案,经流程审批后会显示在这里。')}
考点变更台账 新增和修改均保留申请快照,审批通过后才更新正式档案。
进入流程中心 `;
}
const numberSegmentMeta = {
year: ['年份', '4 位考试年份'], school_code: ['学校代码', '使用学校档案代码'], gender: ['考生性别', '男 M / 女 F / 未知 X'],
sequence: ['流水号', '按规则前缀连续编号'], literal: ['固定值', '自定义固定字母或数字']
};
function adminNumberRules(data) {
const rule = data.activeRule || { name: '自定义报名号规则', separator: '-', segments: [] };
const byType = Object.fromEntries(rule.segments.map(item => [item.type, item]));
const types = Object.keys(numberSegmentMeta);
const candidates = data.batchCandidates || [];
return `ONE CANDIDATE · ONE NUMBER 报名号属于考生账户 创建账户时生成一次,参加不同考试时只复制到报名记录,不再重新编号。
账户报名号组成 规则用于新建考生账户;流水号为必选字段。
当前规则 LEGACY SYNCHRONIZATION 批量同步账户报名号 用于旧数据或导入记录:缺号的考试报名会复制考生账户的固定报名号,不会为每场考试另建号码。
${candidates.slice(0, 8).map(item => `
${h(item.candidateName)} ${h(item.schoolName)} · ${h(item.examName)} `).join('') || '
当前所有考试报名都已关联固定账户报名号。
'}${candidates.length > 8 ? `
另有 ${candidates.length - 8} 条 ` : ''}
`;
}
function adminFlowDesign(workflows) {
const codes = { profile_change: 'PROFILE CHANGE', registration_review: 'REGISTRATION', center_change: 'CENTER & ROOM CHANGE' };
return `${workflows.map(workflow => `
`).join('')}
`;
}
function workflowStepEditor(step = {}) {
return `校级管理员 超级管理员 ×
`;
}
function adminFlows(data) {
const actionNames = { submit: '提交', approve: '通过', reject: '退回考生', transfer: '转交', return: '退回节点', supervise: '监督调整' };
const typeNames = { profile_change: '考生信息修改', registration_review: '考试报名', center_change: '考点考场变更' };
return `${data.instances.map(instance => { const title = instance.businessType === 'center_change' ? instance.centerName : instance.candidateName; const sub = instance.businessType === 'center_change' ? `${instance.requestType === 'create' ? '新增考点' : '修改档案'} · ${instance.schoolName}` : `${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)}` : '尚无操作记录'} 查看与处理 `; }).join('') || emptyState('暂无审批流程', '考生资料、考试报名或考点档案提交后,流程会显示在这里。')}
`;
}
function emptyState(title, description, route, action) {
return `${icons.ticket} ${h(title)} ${h(description)}
${route ? `${h(action)} ` : ''} `;
}
function renderError(error) {
app.innerHTML = `! 页面暂时无法加载 ${h(error.message)}
重新加载 `;
}
function navigate(route) {
location.hash = route;
if (location.hash.slice(1) === route) renderRoute();
}
async function renderRoute() {
closeModal();
window.scrollTo({ top: 0, behavior: 'instant' });
const route = location.hash.slice(1) || 'home';
const [section, page = 'dashboard'] = route.split('/');
if (section === 'home') renderHome();
else if (section === 'login' || section === 'register') renderAuth(section);
else if (section === 'candidate') await renderCandidate(page);
else if (section === 'admin') await renderAdmin(page);
else navigate('home');
}
function formObject(form) {
return Object.fromEntries(new FormData(form).entries());
}
async function refreshPublic() {
state.publicData = await api('/api/public/home');
}
async function refreshSession() {
const session = await api('/api/auth/me');
state.user = session.user;
state.profile = session.profile;
state.permissions = session.permissions || [];
state.scopeLabel = session.scopeLabel || '';
}
document.addEventListener('click', async event => {
const routeTarget = event.target.closest('[data-route]');
if (routeTarget) {
event.preventDefault();
return navigate(routeTarget.dataset.route);
}
if (event.target.matches('[data-modal-backdrop]')) return closeModal();
const target = event.target.closest('[data-action]');
if (!target) return;
const action = target.dataset.action;
try {
if (action === 'close-modal') return closeModal();
if (action === 'retry') return renderRoute();
if (action === 'open-sidebar') return document.querySelector('#portalSidebar')?.classList.add('open');
if (action === 'close-sidebar') return document.querySelector('#portalSidebar')?.classList.remove('open');
if (action === 'toggle-public-nav') return document.querySelector('.public-header nav')?.classList.toggle('open');
if (action === 'scroll-to') {
event.preventDefault();
if (!document.querySelector(`#${target.dataset.target}`)) { navigate('home'); setTimeout(() => document.querySelector(`#${target.dataset.target}`)?.scrollIntoView({ behavior: 'smooth' }), 80); }
else document.querySelector(`#${target.dataset.target}`).scrollIntoView({ behavior: 'smooth' });
return;
}
if (action === 'logout') {
await api('/api/auth/logout', { method: 'POST' });
state.user = null; state.profile = null; state.pageData = null; state.permissions = []; state.scopeLabel = '';
await refreshPublic(); navigate('home'); toast('已安全退出', '期待下次见面'); return;
}
if (action === 'fill-demo') {
const form = document.querySelector('[data-form="login"]');
const accounts = { admin: ['admin', 'Admin123!'], school: ['school_admin', 'School123!'], class: ['class_admin', 'Class123!'], candidate: ['2026-HZ01-F-0001', 'Candidate123!'] };
[form.username.value, form.password.value] = accounts[target.dataset.type] || accounts.candidate;
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;
setModal(`${h(notice.category)} ${h(notice.title)} ${formatDate(notice.publishAt,true)} · ${h(notice.author)}
× ${h(notice.content).replace(/\n/g, '
')}
`); return;
}
if (action === 'download-admit') { window.location.href = `/api/candidate/registrations/${target.dataset.id}/admit-card`; return; }
if (action === 'new-notice') return openNoticeForm();
if (action === 'new-exam') return openExamForm();
if (action === 'new-admin') return openAdminForm();
if (action === 'new-candidate-account') return openCandidateAccountForm();
if (action === 'new-center') return openCenterForm();
if (action === 'edit-center') return openCenterForm(state.pageData.centers.find(item => item.id === target.dataset.id));
if (action === 'add-center-room') {
document.querySelector('[data-center-rooms]')?.insertAdjacentHTML('beforeend', centerRoomEditor());
return;
}
if (action === 'remove-center-room') {
const list = target.closest('[data-center-rooms]');
if (list.children.length <= 1) return toast('至少保留一个考场', '考点档案必须包含结构化考场');
target.closest('.center-room-editor').remove(); return;
}
if (action === 'open-flow') return openFlowDetail(target.dataset.id);
if (action === 'add-workflow-step') {
const form = document.querySelector(`[data-form="workflow-design"][data-type="${target.dataset.type}"]`);
form?.querySelector('[data-workflow-steps]')?.insertAdjacentHTML('beforeend', workflowStepEditor());
return;
}
if (action === 'remove-workflow-step') {
const list = target.closest('[data-workflow-steps]');
if (list.children.length <= 1) return toast('至少保留一步', '审批流程不能为空');
target.closest('.workflow-step-row').remove(); return;
}
if (action === 'edit-exam') return openExamForm(state.pageData.exams.find(exam => exam.id === target.dataset.id));
if (action === 'review-candidate') return openCandidateReview(target.dataset.id);
if (action === 'review-registration') return openRegistrationReview(target.dataset.id);
if (action === 'generate-registration-number') {
await api(`/api/admin/registrations/${target.dataset.id}/registration-number`, { method: 'POST' });
toast('账户报名号已同步', '考试记录已关联考生的固定号码'); return renderRoute();
}
if (action === 'generate-admit') {
const registration = state.pageData.registrations.find(item => item.id === target.dataset.id);
if (registration.admitCard) return openAdmitPreview(registration);
const testCenter = prompt('请输入考点名称:', '海州市第一中学');
if (!testCenter) return;
await api(`/api/admin/registrations/${target.dataset.id}/admit-card`, { method: 'POST', body: { testCenter } });
toast('准考证已生成', '考生可在开放时间内下载'); return renderRoute();
}
if (action === 'toggle-exam') {
await api(`/api/admin/exams/${target.dataset.id}`, { method: 'PATCH', body: { status: target.dataset.status } });
toast(target.dataset.status === 'published' ? '考试已发布' : '考试已撤回', '公开页面状态已同步'); return refreshPublic().then(renderRoute);
}
if (action === 'toggle-notice') {
await api(`/api/admin/notices/${target.dataset.id}`, { method: 'PATCH', body: { status: target.dataset.status } });
toast(target.dataset.status === 'published' ? '通知已发布' : '通知已撤回', '首页展示状态已更新'); return refreshPublic().then(renderRoute);
}
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' && row.dataset.status !== target.dataset.status);
}
} 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));
}
if (event.target.matches('.subject-options input')) {
const form = event.target.closest('form');
const checked = [...form.querySelectorAll('.subject-options input:checked')];
form.querySelector('[data-subject-count]').textContent = checked.length;
const exam = state.pageData.exams.find(item => item.id === form.examId.value);
const fee = checked.reduce((sum, input) => sum + Number(exam.subjects.find(subject => subject.id === input.value)?.fee || 0), 0);
form.querySelector('[data-subject-fee]').textContent = `合计 ${money(fee)}`;
}
});
document.addEventListener('change', event => {
if (event.target.matches('[data-action="school-select"]')) {
const form = event.target.closest('form');
const classSelect = form?.querySelector('select[name="classId"]');
if (classSelect) {
const classes = state.pageData?.classes || state.publicData.classes || [];
classSelect.innerHTML = `请选择班级 ${classes.filter(item => item.schoolId === event.target.value).map(item => `${h(item.name)} `).join('')}`;
}
}
if (event.target.matches('[data-action="admin-level"]')) {
const form = event.target.closest('form');
form?.querySelector('[data-admin-school]')?.classList.toggle('hidden', event.target.value === 'super');
form?.querySelector('[data-admin-class]')?.classList.toggle('hidden', event.target.value !== 'class');
}
if (event.target.matches('[data-action="result-registration"]')) {
const option = event.target.selectedOptions[0];
const select = document.querySelector('#resultSubject');
const subjects = option?.dataset.subjects ? JSON.parse(option.dataset.subjects) : [];
select.innerHTML = `请选择科目 ${subjects.map(subject => `${h(subject.name)} `).join('')}`;
}
});
document.addEventListener('submit', async event => {
const form = event.target.closest('form[data-form]');
if (!form) return;
event.preventDefault();
const submit = form.querySelector('button[type="submit"]');
const original = submit?.innerHTML;
if (submit) { submit.disabled = true; submit.textContent = '正在处理…'; }
try {
const kind = form.dataset.form;
if (kind === 'login') {
const data = await api('/api/auth/login', { method: 'POST', body: formObject(form) });
state.user = data.user; await refreshSession();
toast('登录成功', `欢迎,${data.user.displayName}`); navigate(data.user.role === 'candidate' && (state.user.mustChangePassword || !state.profile?.profileCompleted) ? 'candidate/onboarding' : `${data.user.role}/dashboard`);
} else if (kind === 'register') {
const data = await api('/api/auth/register', { method: 'POST', body: formObject(form) });
setModal(`CANDIDATE NUMBER 请保存你的报名号 该号码就是长期使用的考生账户。
× 固定报名号 ${h(data.registrationNumber)} 以后报名不同考试仍使用这个号码。关闭窗口前请抄写或截图保存。
`);
} else if (kind === 'candidate-password') {
const body = formObject(form);
if (body.newPassword !== body.confirmPassword) throw new Error('两次输入的新密码不一致');
await api('/api/auth/change-password', { method: 'POST', body });
await refreshSession(); toast('密码修改成功', '下一步请补全个人信息'); navigate('candidate/onboarding');
} else if (kind === 'candidate-profile') {
const data = await api('/api/candidate/profile', { method: 'PUT', body: formObject(form) });
state.profile = data.profile; await refreshSession(); toast('资料已提交', '管理员审核后会更新状态'); navigate('candidate/dashboard');
} else if (kind === 'exam-registration') {
const body = { examId: form.examId.value, subjectIds: [...form.querySelectorAll('input[name="subjectIds"]:checked')].map(input => input.value) };
if (!body.subjectIds.length) throw new Error('请至少选择一个报考科目');
await api('/api/candidate/registrations', { method: 'POST', body });
toast('报名已提交', `已选择 ${body.subjectIds.length} 个科目`); renderRoute();
} else if (kind === 'candidate-review') {
const body = formObject(form);
await api(`/api/admin/candidates/${body.id}`, { method: 'PATCH', body });
closeModal(); toast(body.status === 'approved' ? '资料审核通过' : '资料已退回', '考生端状态已同步'); renderRoute();
} else if (kind === 'registration-review') {
const body = formObject(form);
await api(`/api/admin/registrations/${body.id}`, { method: 'PATCH', body });
closeModal(); toast(body.status === 'approved' ? '报名审核通过' : '报名已退回', '报名状态已更新'); renderRoute();
} else if (kind === 'admin-form') {
const body = formObject(form);
await api('/api/admin/admins', { method: 'POST', body });
closeModal(); toast('管理员已创建', '权限范围已按层级绑定'); renderRoute();
} else if (kind === 'candidate-account') {
const data = await api('/api/admin/candidate-accounts', { method: 'POST', body: formObject(form) });
closeModal(); toast('考生账户已创建', `报名号:${data.candidate.candidateNumber}`); renderRoute();
} else if (kind === 'self-registration-setting') {
const enabled = form.enabled.value === 'true';
await api('/api/admin/settings/self-registration', { method: 'PUT', body: { enabled } });
await refreshPublic(); toast(enabled ? '自主注册已开放' : '自主注册已关闭', enabled ? '公开入口现在可以申请报名号' : '仅保留学校下发账户流程'); renderRoute();
} else if (kind === 'center-form') {
const body = formObject(form);
const editing = Boolean(body.id);
body.rooms = [...form.querySelectorAll('.center-room-editor')].map(row => ({
id: row.querySelector('[name="roomId"]').value || null,
code: row.querySelector('[name="roomCode"]').value,
name: row.querySelector('[name="roomName"]').value,
building: row.querySelector('[name="roomBuilding"]').value,
floor: row.querySelector('[name="roomFloor"]').value,
capacity: Number(row.querySelector('[name="roomCapacity"]').value),
seatStart: Number(row.querySelector('[name="roomSeatStart"]').value),
seatEnd: Number(row.querySelector('[name="roomSeatEnd"]').value),
roomType: row.querySelector('[name="roomType"]').value,
status: row.querySelector('[name="roomStatus"]').value,
notes: row.querySelector('[name="roomNotes"]').value
}));
await api(editing ? `/api/admin/centers/${body.id}` : '/api/admin/centers', { method: editing ? 'PATCH' : 'POST', body });
closeModal(); toast(editing ? '考点变更已提交' : '新考点已提交', '审批通过后才会更新正式档案'); renderRoute();
} else if (kind === 'number-rule') {
const raw = formObject(form);
const types = Object.keys(numberSegmentMeta);
const segments = types.filter(type => type === 'sequence' || form.querySelector(`[name="include_${type}"]`)?.checked).map(type => ({
type, position: Number(raw[`position_${type}`] || 99), value: raw[`value_${type}`] || '', width: Number(raw[`width_${type}`] || 0)
})).sort((a, b) => a.position - b.position);
await api('/api/admin/number-rules', { method: 'POST', body: { id: raw.id, name: raw.name, separator: raw.separator, segments } });
toast('报名号规则已启用', '后续创建的考生账户将按此规则生成固定号码'); renderRoute();
} else if (kind === 'batch-registration-numbers') {
const result = await api('/api/admin/registration-numbers/batch', { method: 'POST', body: formObject(form) });
toast('报名号同步完成', `已为 ${result.count} 条考试报名关联固定账户号码`); renderRoute();
} else if (kind === 'workflow-design') {
const names = [...form.querySelectorAll('[name="stepName"]')];
const levels = [...form.querySelectorAll('[name="stepLevel"]')];
const steps = names.map((input, index) => ({ name: input.value, adminLevel: levels[index].value }));
await api(`/api/admin/workflows/${form.dataset.type}`, { method: 'PUT', body: { name: form.name.value, steps } });
toast('审批流程已保存', `${steps.length} 个步骤已启用`); renderRoute();
} else if (kind === 'flow-process') {
const body = formObject(form);
const path = body.businessType === 'profile_change'
? `/api/admin/candidates/${body.businessId}`
: body.businessType === 'registration_review'
? `/api/admin/registrations/${body.businessId}`
: `/api/admin/center-change-requests/${body.businessId}`;
await api(path, { method: 'PATCH', body: { status: body.status, reviewNote: body.reviewNote } });
closeModal(); toast(body.status === 'approved' ? '流程已处理' : '流程已退回', '操作已写入流程轨迹'); renderRoute();
} else if (kind === 'flow-transfer') {
const body = formObject(form);
await api(`/api/admin/workflow-instances/${body.id}/transfer`, { method: 'PATCH', body });
closeModal(); toast('流程已转交', '新责任人已收到待办'); renderRoute();
} else if (kind === 'flow-supervise') {
const body = formObject(form);
await api(`/api/admin/workflow-instances/${body.id}/supervise`, { method: 'PATCH', body });
closeModal(); toast('流程已监督调整', '节点与责任人已更新并记录'); renderRoute();
} else if (kind === 'notice-form') {
const body = formObject(form); body.pinned = form.pinned.checked;
await api('/api/admin/notices', { method: 'POST', body });
closeModal(); await refreshPublic(); toast(body.status === 'published' ? '通知已发布' : '草稿已保存', '公开首页状态已同步'); renderRoute();
} else if (kind === 'exam-form') {
const body = formObject(form); body.subjects = body.subjects.split(/[,,]/).map(item => item.trim()).filter(Boolean);
['registrationStart','registrationEnd','examStart','examEnd','admitDownloadStart','admitDownloadEnd'].forEach(field => body[field] = new Date(body[field]).toISOString());
const editing = Boolean(body.id);
await api(editing ? `/api/admin/exams/${body.id}` : '/api/admin/exams', { method: editing ? 'PATCH' : 'POST', body });
closeModal(); await refreshPublic(); toast(editing ? '考试草稿已更新' : '考试计划已创建', `${body.subjects.length} 个科目已配置`); renderRoute();
} else if (kind === 'result-entry') {
const body = formObject(form); body.published = form.published.checked;
await api('/api/admin/results', { method: 'POST', body });
toast(body.published ? '成绩已发布' : '成绩已保存', '考生端可见状态已更新'); renderRoute();
}
} catch (error) { toast('操作未完成', error.message); }
finally { if (submit && submit.isConnected) { submit.disabled = false; submit.innerHTML = original; } }
});
function openAdminForm() {
const { schools = [], classes = [] } = state.pageData;
setModal(`ADMIN SCOPE 添加分级管理员 同一级可以创建多个账号;校级和班级管理员必须绑定数据范围。
× `);
}
function openCandidateAccountForm() {
const { schools = [], classes = [] } = state.pageData;
setModal(`ISSUE CANDIDATE NUMBER 创建考生账户 系统按当前报名号规则生成固定账户;考生首次登录必须修改初始密码。
× `);
}
function centerRoomEditor(room = {}) {
return ``;
}
function openCenterForm(center = null) {
const schools = state.pageData.schools || [];
const rooms = center?.rooms?.length ? center.rooms : [{}];
setModal(`CONTROLLED DOSSIER ${center ? '提交考点档案变更' : '提交新考点档案'} 表单将进入“考点考场变更审批”,通过前不会改动正式数据。
× `);
}
function openFlowDetail(id) {
const instance = state.pageData.instances.find(item => item.id === id);
const currentLevel = instance.currentStepDetail?.adminLevel;
const available = state.pageData.availableAdmins.filter(item => item.adminLevel === currentLevel && (currentLevel === 'super' || item.schoolId === instance.assignee?.schoolId));
const canProcess = instance.status === 'pending' && (state.user.adminLevel === 'super' || instance.assignee?.id === state.user.id);
const history = instance.actions.map(action => `${h(action.actorName)} · ${h({submit:'提交',approve:'通过',reject:'退回',transfer:'转交',return:'退回节点',supervise:'监督调整'}[action.action] || action.action)} ${h(action.note || '')}${action.toAssigneeName ? ` → ${h(action.toAssigneeName)}` : ''} ${formatDate(action.createdAt, true)}
`).join('');
const subject = instance.businessType === 'center_change' ? instance.centerName : instance.candidateName;
const subjectDetail = instance.businessType === 'center_change' ? `${instance.requestType === 'create' ? '新增考点' : '修改档案'} · ${instance.schoolName}` : `${instance.examName ? `${instance.examName} · ` : ''}${instance.schoolName}`;
const change = instance.centerChange;
const changeSnapshot = change ? `
地址 ${h(change.address)}
负责人 ${h(change.managerName || '未填写')} · ${h(change.managerPhone || '未填写')}
开放时间 ${h(change.gateOpenTime || '未填写')}
档案状态 ${change.centerStatus === 'active' ? '启用' : '停用'} ${change.rooms.map(room => `${h(room.name)} ${h(room.building)} · ${h(room.capacity)} 席 · ${h(room.seatStart)}—${h(room.seatEnd)} `).join('')}
` : '';
setModal(`WORKFLOW TRACE ${h(instance.workflowName)} ${h(subject)} · ${h(subjectDetail)}
× ${changeSnapshot}${instance.steps.map(step => `
${step.position} ${h(step.name)} ${h(statusLabels[step.adminLevel])}
`).join('')}
${canProcess ? `处理结论 通过当前步骤 退回申请 处理意见
确认处理 转交给同级管理员 ${available.filter(item => item.id !== instance.assignee?.id).map(item => `${h(item.displayName)} `).join('')} 转交说明
item.id !== instance.assignee?.id).length ? '' : 'disabled'}>转交流程 ` : '当前流程未分配给你,只能查看轨迹。
'}${state.pageData.canSupervise ? `超级管理员监督调整 调整到步骤 ${instance.steps.map(step => `${step.position}. ${h(step.name)} `).join('')} 指定责任人(可留空自动选择) 自动选择 ${state.pageData.availableAdmins.map(item => `${h(item.displayName)} · ${h(statusLabels[item.adminLevel])} `).join('')}
监督说明 监督调整 ` : ''}`);
}
function openCandidateReview(id) {
const item = state.pageData.candidates.find(candidate => candidate.id === id);
const canReview = state.user.adminLevel !== 'class' && item.status === 'pending';
setModal(`CANDIDATE REVIEW ${canReview ? '处理' : '查看'} ${h(item.name)} 的资料 报名号 ${h(item.candidateNumber)} · 更新于 ${formatDate(item.updatedAt,true)}
×
证件号码 ${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 ? `审核结论 通过当前步骤 退回考生修改 审核意见 ${h(item.reviewNote)} ` : ''}`);
}
function openRegistrationReview(id) {
const reg = state.pageData.registrations.find(item => item.id === id);
const canReview = state.user.adminLevel !== 'class' && reg.status === 'pending';
setModal(`REGISTRATION REVIEW ${canReview ? '处理' : '查看'}考试报名 ${h(reg.candidate?.name)} · ${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)} ${canReview ? `审核结论 通过当前步骤 退回报名 审核意见 ${h(reg.reviewNote || '')} ` : ``}`);
}
function openNoticeForm() {
setModal(`NEW NOTICE 发布通知公告 发布后将立即显示在公开首页和考生中心。
× 通知分类 报名通知 考试须知 考点公告 成绩通知 系统公告 发布方式 立即发布 保存草稿
通知标题 * 首页摘要 通知正文 * 在公开首页置顶展示 `);
}
function dateTimeLocal(value) {
if (!value) return '';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return '';
const pad = number => String(number).padStart(2, '0');
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
}
function openExamForm(exam = null) {
const editing = Boolean(exam);
if (editing && exam.status !== 'draft') return toast('无法编辑', '请先将已发布考试撤回为草稿');
setModal(`${editing ? 'EDIT EXAM DRAFT' : 'NEW EXAM'} ${editing ? '编辑考试草稿' : '创建考试与科目'} ${editing ? '补全考试信息和科目后,可保存草稿或直接发布。' : '创建后可选择直接发布或先保存为草稿。'}
× ${editing ? ` ` : ''}考试名称 * 考试代码
考试说明 ${h(exam?.description)} 报名开始 * 报名结束 *
考试开始 * 考试结束 *
准考证下载开始 * 准考证下载结束 *
考试科目 * 考点说明 ${editing ? '保存状态' : '创建状态'} 保存为草稿 立即发布
`);
}
function openAdmitPreview(reg) {
setModal(`ADMISSION CARD 准考证已生成 ${h(reg.candidate?.name)} · ${h(reg.exam.name)}
× ${h(reg.admitCard.number)}
考点 ${h(reg.admitCard.testCenter)}
考场 ${h(reg.admitCard.room)}
座位号 ${h(reg.admitCard.seat)}
生成时间 ${formatDate(reg.admitCard.generatedAt,true)} 考生可在 ${dateRange(reg.exam.admitDownloadStart, reg.exam.admitDownloadEnd)} 下载。
`);
}
window.addEventListener('hashchange', renderRoute);
window.addEventListener('keydown', event => { if (event.key === 'Escape') closeModal(); });
try {
await Promise.all([refreshPublic(), refreshSession()]);
await renderRoute();
} catch (error) {
renderError(error);
}