import { mountRegionSelects } from './region-select.mjs';
import { resolveProfileSpecialty, specialtyCatalog, specialtyLabel } from '../data/specialty-types.mjs';
export function createCandidateViews(context) {
const {
state,
app,
h,
formatDate,
dateRange,
badge,
money,
passPolicyText,
statusLabels,
icons,
api,
renderError,
emptyState,
brand
} = context;
const candidateNav = [
['dashboard', '总览', 'home'], ['profile', '个人资料', 'user'], ['exams', '考试报名', 'exam'],
['registrations', '我的报名', 'check'], ['admit', '准考证', 'ticket'], ['results', '成绩查询', 'chart'], ['admissions', '志愿与录取', 'check'], ['notices', '通知公告', 'bell'],
['security', '账户安全', 'user']
];
function adminNavForUser() {
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], 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];
}
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' ? `` : ''}${h((state.user?.displayName || '用').slice(0, 1))}${h(state.user?.displayName)}${role === 'admin' ? adminTitle : `资料${statusLabels[state.profile?.status] || '未完善'}`}
${role === 'admin' ? 'EXAM OPERATIONS' : 'CANDIDATE SERVICE'}
${h(title)}
${h(description)}
${portalHeadingAction(role, page)}
${content} `;
}
function portalHeadingAction(role, page) {
if (role === 'admin' && page === 'notices') return ``;
if (role === 'admin' && page === 'exams') return ``;
if (role === 'admin' && page === 'schools') return ``;
if (role === 'admin' && page === 'admins') return ``;
if (role === 'admin' && page === 'centers') return ``;
if (role === 'admin' && page === 'organization') return ``;
if (role === 'candidate' && page === 'profile') return `当前状态 ${badge(state.profile?.status || 'pending')}`;
return '';
}
function loadingPanel() {
return `正在读取数据
`;
}
function mountAdmissionProfileFields(profile = {}) {
const actions = app.querySelector('.profile-form .form-actions');
if (!actions || app.querySelector('[data-admission-profile-fields]')) return;
const qualification = resolveProfileSpecialty(profile);
const selectedCategory = specialtyCatalog.find(item => item.code === qualification.category);
actions.insertAdjacentHTML('beforebegin', ``);
}
function onboardingShell(stage, content) {
const passwordDone = stage !== 'password';
return `FIRST SIGN-IN${stage === 'password' ? '先保护你的账户' : '建立完整考生档案'}
${stage === 'password' ? '初始密码只用于第一次登录。修改成功后才可填写个人信息。' : '带 * 的信息会用于身份核验、学校管理范围和考试联系。'}
${content}`;
}
function passwordOnboardingForm() {
return ``;
}
async function renderCandidate(page) {
if (state.user?.role !== 'candidate') return navigate('login');
app.classList.remove('admin-readable');
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));
mountAdmissionProfileFields(data.profile);
mountRegionSelects(app, data.profile, { className: 'region-selects wide-field' });
} catch (error) { renderError(error); }
return;
}
const meta = {
dashboard: ['总览', '查看你的资料、报名、准考证与成绩状态。'],
profile: ['个人资料', '维护实名认证与联系方式;修改后需要重新审核。'],
exams: ['考试报名', '在开放时间内选择考试,并自主勾选报考科目。'],
registrations: ['我的报名', '查看已提交的考试、科目与审核进度。'],
admit: ['准考证', '管理员生成后,可在规定下载时间内保存准考证。'],
results: ['成绩查询', '仅显示考试中心已经正式发布的成绩。'],
admissions: ['志愿填报与录取', '成绩发布后由本人填报志愿,并在这里查看投档与录取进度。'],
notices: ['通知公告', '查看与报名、考试和成绩相关的最新消息。'],
security: ['账户安全', '使用当前密码设置新的登录密码。']
};
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' : page === 'admissions' ? 'admissions' : 'registrations';
const data = page === 'notices' ? { notices: state.publicData.notices } : page === 'security' ? await api('/api/auth/totp') : 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), admissions: () => candidateAdmissions(data), notices: () => candidateNotices(data.notices), security: () => accountSecurity(data)
}[page]();
app.innerHTML = portalShell('candidate', page, content, ...meta[page]);
if (page === 'profile') { mountAdmissionProfileFields(data.profile); mountRegionSelects(app, data.profile, { className: 'region-selects wide-field' }); }
} 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 => ``).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.totalScore)} · ${h(passPolicyText(exam))}
- 考点安排
- ${h(exam.location)}
`).join('')}
`;
}
function candidateRegistrations(registrations) {
if (!registrations.length) return emptyState('还没有考试报名', '资料审核通过后,即可在“考试报名”中选择考试与科目。', 'candidate/exams', '去考试报名');
const card = reg => `- 账户报名号
- ${h(reg.registrationNumber || state.user.candidateNumber)}
- 当前审批
- ${h(reg.workflow?.currentStepDetail?.name || statusLabels[reg.workflow?.status] || '待提交')}
- 应缴金额
- ${money(reg.amountDue || 0)}
- 缴费状态
- ${badge(reg.paymentStatus)}${reg.paidAt ? `${formatDate(reg.paidAt, true)} · ${h(reg.paidByName || '班级负责人')}` : ''}
已选科目${reg.subjects.map(subject => `${h(subject.name)}${h(subject.date)} ${h(subject.start)}`).join('')}
`;
const current = registrations.filter(reg => !reg.exam.archivedAt);
const archived = registrations.filter(reg => reg.exam.archivedAt);
return `${current.map(card).join('')}
${archived.length ? `历史报名记录${archived.length} 场归档考试 · 点击查阅${archived.length}
${archived.map(card).join('')}
` : ''}`;
}
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();
const assignments = new Map((reg.admitCard.assignments || []).map(item => [item.subjectId, item]));
const subjectRows = reg.subjects.map(subject => {
const assignment = assignments.get(subject.id) || {};
return `
${h(subject.name)}${h(subject.date)} ${h(subject.start)} · 考试考场序号 ${h(assignment.examRoomCode || '待定')} · ${h(assignment.roomName || assignment.room || '场地待定')}(${h(assignment.roomCode || '—')})· ${h(assignment.building || '楼栋待定')} ${h(assignment.floor || '')} · 座位 ${h(assignment.seat || '—')}`;
}).join('');
const ticket = `
${h(reg.exam.code)}${badge(reg.exam.archivedAt ? 'archived' : 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.centerCode || '')} · ${h(reg.admitCard.centerAddress || '详细地址待公布')}
- 逐科详细安排
- ${subjectRows}
- 下载时间
- ${dateRange(reg.exam.admitDownloadStart, reg.exam.admitDownloadEnd)}
ADMISSION
CARD${reg.exam.archivedAt ? '历史准考证仅供查阅' : '下载后请使用 A4 纸横向打印'}
`;
return reg.exam.archivedAt ? `
${h(reg.exam.name)}${h(reg.exam.code)} · ${formatDate(reg.exam.archivedAt, true)} 归档查看历史准考证
${ticket}` : ticket;
}).join('')}
` : emptyState('准考证尚未生成', '考试报名审核通过后,由管理员统一编排准考证。', 'candidate/registrations', '查看报名状态');
}
function candidateResults(data) {
const { results, summaries = [] } = data;
if (!results.length) return emptyState('暂时没有已发布成绩', '成绩发布后会在这里显示,同时首页会发布查分通知。', 'candidate/notices', '查看通知');
const grouped = Object.groupBy ? Object.groupBy(results, item => item.examId) : results.reduce((acc, item) => ((acc[item.examId] ||= []).push(item), acc), {});
const completeSummaries = summaries.filter(item => item.complete);
const overview = `已发布考试${Object.keys(grouped).length}场已发布科目${results.length}科整场已合格${completeSummaries.filter(item => item.qualified === true).length}场复议处理中${results.filter(item => item.appeal?.status === 'pending').length}项`;
return `${overview}${Object.entries(grouped).sort(([, left], [, right]) => new Date(right[0]?.examStart || 0) - new Date(left[0]?.examStart || 0)).map(([, items]) => {
const examName = items[0].examName;
const summary = summaries.find(item => item.examId === items[0].examId);
const stateText = !summary?.complete ? '等待全部科目发布' : summary.qualified == null ? '本考试不判定合格' : summary.qualified ? '合格' : '未达合格线';
const detail = summary?.passPolicy === 'rank_percent' && summary.complete ? `第 ${summary.rank} / ${summary.cohortSize} 名` : summary ? passPolicyText(summary) : '';
const scores = items.map(item => {
const appeal = item.appeal;
const latestAction = appeal?.actions?.at(-1);
const appealPanel = item.archivedAt
? `
${badge('archived')}本场成绩已永久锁定,复议入口已关闭
`
: appeal?.status === 'pending'
? `
${badge('pending')}${h(appeal.currentStepDetail?.name || '等待处理')} · ${h(appeal.assignee?.displayName || '待分配')}
`
: appeal?.status === 'approved'
? `
${badge('approved')}${h(latestAction?.note || '复议流程已完成')}
`
: `${appeal ? `
${badge('rejected')}${h(latestAction?.note || '可补充理由后重新提交')}
` : ''}
`;
const lineState = item.qualified == null ? 'neutral' : item.qualified ? 'qualified' : 'unqualified';
return `
${h(item.subjectName)}${item.qualified == null ? '不判定单科' : item.qualified ? '单科达线' : '单科未达线'}
${h(item.score)} / ${h(item.fullScore)}${h(item.grade)} · 第 ${h(item.rank)} / ${h(item.cohortSize)} 名 · 前 ${h(item.rankPercent)}%本科排名${h(item.passText || '不设单科线')}
${appealPanel}`;
}).join('');
const panel = `
当前总分${h(summary?.total ?? '—')} / ${h(summary?.fullScore ?? '—')}科目等级按排名特征分${h(summary?.featureScore ?? 0)}独立于考试科目整场合格判定${h(stateText)}${h(detail)}发布进度${h(summary?.publishedSubjects ?? items.length)} / ${h(summary?.subjectCount ?? items.length)} 科${summary?.complete ? '成绩已出齐' : '持续发布中'}
${scores}
`;
return items[0].archivedAt ? `
${h(examName)}${h(items[0].examCode)} · ${items.length} 科成绩 · 已永久锁定历史成绩
${panel}` : panel;
}).join('')}
`;
}
function candidateAdmissions(data) {
const phaseLabels = { draft: '尚未开放', filling: '志愿填报中', closed: '填报已截止', matching: '正在投档', school_review: '招生学校审核中', supplementary: '补录填报中', completed: '录取结束' };
if (!data.admissions?.length) return emptyState('暂无志愿填报安排', '只有启用志愿功能且成绩已经发布的考试会显示在这里。', 'candidate/results', '查看成绩');
return `${data.notifications?.length ? `${h(data.notifications[0].payload.title)}${h(data.notifications[0].payload.message)}
${formatDate(data.notifications[0].createdAt, true)}` : ''}${data.admissions.map(item => {
const choices = item.preference?.payload?.choices || [];
const canFill = ['filling', 'supplementary'].includes(item.status) && item.totalScore != null;
const placementSchool = item.plans.find(plan => plan.schoolId === item.placement?.schoolId)?.schoolName || '';
const progressSteps = ['filling', 'closed', 'school_review', 'completed'];
const progressIndex = item.status === 'supplementary' ? 1 : Math.max(0, progressSteps.indexOf(item.status));
const choiceRows = Array.from({ length: Number(item.payload.maxChoices || 5) }, (_, index) => {
const choice = choices[index] || {};
const plan = item.plans.find(entry => entry.schoolId === choice.schoolId);
const categoryOptions = (plan?.categories || []).filter(category => category.remaining > 0 || category.code === choice.categoryCode);
return `
${index + 1}
`;
}).join('');
const lockedRows = choices.map((choice, index) => { const plan = item.plans.find(entry => entry.schoolId === choice.schoolId); const category = plan?.categories.find(entry => entry.code === choice.categoryCode); return `
${index + 1}${h(plan ? `${plan.schoolCode} · ${plan.schoolName} · ${category?.name || choice.categoryCode}` : `${choice.schoolId} · ${choice.categoryCode}`)}`; }).join('');
const qualification = specialtyLabel(item.specialtyQualification?.category, item.specialtyQualification?.type) || '普通生';
return `
${['填报志愿','志愿锁定','投档审核','录取结束'].map((label, index) => `
${index < progressIndex ? '✓' : index + 1}${label}
`).join('')}
本场总成绩${item.totalScore == null ? '成绩尚未完整发布' : `${h(item.totalScore)} 分`}特征分 ${h(item.featureScore || 0)}资格 ${h(qualification)}${h(phaseLabels[item.status] || item.status)}
${h(item.payload.progress || '等待录取工作更新')}
${item.placement ? `当前结果${h(placementSchool)} · ${h(item.placement.payload.categoryName)}${item.placement.status === 'final' ? '已正式录取,通知已发送' : item.placement.status === 'withdrawal_pending' ? '招生学校申请退档,等待超级管理员审核' : '材料已发送招生学校审核'}
` : ''}${canFill ? `` : choices.length ? `已锁定志愿顺序${lockedRows}
` : '当前不能填报:请等待成绩完整发布或志愿填报窗口开放。
'}`;
}).join('')}
`;
}
function candidateNotices(notices) {
return `${notices.map(notice => ``).join('')}
`;
}
function accountSecurity(totp = {}) {
const account = h(state.user?.candidateNumber || state.user?.username);
const type = state.user?.role === 'candidate' ? '考生账户' : statusLabels[state.user?.adminLevel] || '管理员账户';
const password = ``;
const totpPanel = totp.enabled
? `TWO-STEP VERIFICATIONTOTP 二次验证已开启
登录密码验证通过后,还需要输入验证器应用生成的 6 位动态验证码。
保护中剩余 ${h(totp.recoveryCodesRemaining)} 个恢复码
`
: ``;
return `${password}${totpPanel}
`;
}
return { adminNavForUser, portalShell, loadingPanel, renderCandidate, accountSecurity };
}