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, requireLogin, 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'], 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], ['admission-settings', '录取设置', 'check'], ['admission-accounts', '招生账户', 'users'], ['admission-plans', '招生计划', 'exam'], ['admission-reporting', '报到与补录', 'bell'], ['admission-supervision', '投档监督', 'check'], ['notices', '通知发布', 'bell'], ['centers', '考场信息', 'exam'], ['flows', '流程监督', 'check'], ['flow-design', '流程设计', 'exam'], ['number-rules', '报名号规则', 'ticket'], security]; } function portalShell(role, page, content, title, description) { const nav = role === 'admin' ? adminNavForUser() : candidateNav; const roleName = role === 'admin' ? '管理后台' : '考生中心'; const adminTitle = statusLabels[state.user?.adminLevel] || '管理员'; const groupFor = id => role === 'candidate' ? ({ dashboard: '个人总览', profile: '账户与档案', security: '账户与档案', exams: '考试服务', registrations: '考试服务', admit: '考试服务', results: '考试服务', admissions: '招生录取', notices: '招生录取' }[id] || '其他') : ({ dashboard: '运行总览', schools: '组织与账户', organization: '组织与账户', admins: '组织与账户', 'account-batches': '组织与账户', candidates: '报名考务', registrations: '报名考务', payments: '报名考务', admit: '报名考务', exams: '考试与成绩', results: '考试与成绩', admissions: '招生录取', 'admission-settings': '招生录取', 'admission-accounts': '招生录取', 'admission-plans': '招生录取', 'admission-reporting': '招生录取', 'admission-supervision': '招生录取', 'indicator-qualifications': '招生录取', notices: '招生录取', centers: '场所与流程', flows: '场所与流程', 'flow-design': '系统配置', 'number-rules': '系统配置', security: '系统配置' }[id] || '其他'); const groups = [...new Set(nav.map(([id]) => groupFor(id)))]; const navHtml = groups.map(group => `
${h(group)}${nav.filter(([id]) => groupFor(id) === group).map(([id, label, icon]) => ``).join('')}
`).join(''); 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', `
04

中考招生资格

特长资格按大类和小类登记,填志愿时系统只显示与本人资格相符的招生类别。

`); } function onboardingShell(stage, content) { const passwordDone = stage !== 'password'; return `
FIRST SIGN-IN

${stage === 'password' ? '先保护你的账户' : '建立完整考生档案'}

${stage === 'password' ? '初始密码只用于第一次登录。修改成功后才可填写个人信息。' : '带 * 的信息会用于身份核验、学校管理范围和考试联系。'}

${content}
`; } function passwordOnboardingForm() { return `
新密码要求至少 8 位,且不能与初始密码相同。
`; } async function renderCandidate(page) { if (state.user?.role !== 'candidate') return requireLogin(); 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' ? await api('/api/candidate/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 `
${workflow ? `
当前审批${h(step?.name || statusLabels[workflow.status])}${workflow.assignee ? `由 ${h(workflow.assignee.displayName)} 处理` : '流程已结束'}
` : ''}
01

身份信息

姓名和证件号码须与有效证件完全一致。

02

学校与班级

学校和班级决定资料审批范围。

03

家庭与联系信息

用于考试通知、身份复核和紧急联系。

${profile?.reviewNote ? `
审核意见

${h(profile.reviewNote)}

` : ''}

${onboarding ? '提交后进入资料审批,审核通过即可报名考试。' : '保存后资料将按当前流程重新审批。'}

`; } function candidateExams(data) { return `
${data.exams.map(exam => `
${h(exam.code)}${badge(exam.registrationState)}
${exam.registrationCount || 0} 人已报名

${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)}
选择报考科目可多选
${exam.subjects.map(subject => ``).join('') || '

科目安排尚未发布

'}
${exam.registration ? `
${icons.check}已提交报名 · ${exam.registration.subjectIds.length} 个科目${badge(exam.registration.status)}
` : `
已选 0满分 0 · ¥0.00
`}
`).join('')}
`; } function candidateRegistrations(registrations) { if (!registrations.length) return emptyState('还没有考试报名', '资料审核通过后,即可在“考试报名”中选择考试与科目。', 'candidate/exams', '去考试报名'); const card = reg => `
${h(reg.exam.code)}

${h(reg.exam.name)}

${badge(reg.exam.archivedAt ? 'archived' : reg.status)}
账户报名号
${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(items[0].examCode)}

${h(examName)}

${items[0].archivedAt ? `${formatDate(items[0].archivedAt, true)} 归档并锁定` : `最近发布 ${formatDate([...items].sort((a,b) => new Date(b.publishedAt) - new Date(a.publishedAt))[0].publishedAt, true)}`}
当前总分${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}

${items[0].archivedAt ? '本场所有成绩已永久锁定,以下内容仅保留历史查阅。' : '等级按同场同科已发布成绩排名计算;特征分单独登记,不计入文化课总分。'}

`; 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: '招生学校审核中', reporting: '考生报到中', supplementary: '补录填报中', completed: '录取结束' }; if (!data.admissions?.length) return emptyState('暂无志愿填报安排', '只有启用志愿功能且成绩已经发布的考试会显示在这里。', 'candidate/results', '查看成绩'); const notificationCards = (data.notifications || []).map(notification => { const invalid = ['withdrawn', 'forfeited'].includes(notification.placementStatus); return `
ADMISSION${invalid ? '失' : '录'}
${invalid ? '录取状态已更新' : '录取结果已发布'}

${h(notification.payload?.title || '录取结果通知')}

${h(notification.payload?.message || '录取结果已经发布,请核对以下信息。')}

录取学校
${h(notification.schoolName || '招生学校')}
招生类别
${h(notification.categoryName || '以录取通知书为准')}
所属考试
${h(notification.examName || '—')}
${notification.noticeNumber ? `
通知书编号
${h(notification.noticeNumber)}
` : ''}
${invalid ? '资格已失效' : '正式录取'}
`; }).join(''); return `${notificationCards ? `
${notificationCards}
` : ''}
${data.admissions.map(item => { const choices = item.preference?.payload?.choices || []; const canFill = ['filling', 'supplementary'].includes(item.status) && item.totalScore != null && !item.preferenceLocked && item.supplementEligible !== false; const placementSchool = item.placementSchool?.name || item.plans.find(plan => plan.schoolId === item.placement?.schoolId)?.schoolName || '招生学校'; const progressSteps = ['filling', 'closed', 'school_review', 'completed']; const progressIndex = item.status === 'supplementary' ? 1 : item.status === 'reporting' ? 3 : Math.max(0, progressSteps.indexOf(item.status)); const indicatorChoice = choices.find(choice => choice.preferenceType === 'indicator') || {}; const generalChoices = choices.filter(choice => choice.preferenceType !== 'indicator'); const indicatorEligible = item.indicatorQualification?.payload?.eligible === true; const choiceRow = (choice, preferenceType, index) => { const eligiblePlans = item.plans.filter(plan => plan.categories.some(category => category.preferenceTypes?.includes(preferenceType))); const plan = eligiblePlans.find(entry => entry.schoolId === choice.schoolId); const categoryOptions = (plan?.categories || []).filter(category => category.preferenceTypes?.includes(preferenceType) && ((preferenceType === 'indicator' ? category.indicatorRemaining : category.generalRemaining) > 0 || category.code === choice.categoryCode)); const disabled = preferenceType === 'indicator' && !indicatorEligible; return `
${preferenceType === 'indicator' ? '指标' : index + 1}
`; }; const choiceRows = choiceRow(indicatorChoice, 'indicator', 0) + Array.from({ length: Number(item.payload.maxChoices || 5) }, (_, index) => choiceRow(generalChoices[index] || {}, 'general', index)).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); const schoolCode = choice.schoolCode || plan?.schoolCode || ''; const schoolName = choice.schoolName || plan?.schoolName || choice.schoolId; const categoryName = choice.categoryName || category?.name || choice.categoryCode; return `${choice.preferenceType === 'indicator' ? '指标' : index + 1}${h(schoolName)}${h([schoolCode, categoryName].filter(Boolean).join(' · '))}`; }).join(''); const qualification = specialtyLabel(item.specialtyQualification?.category, item.specialtyQualification?.type) || '普通生'; const indicatorText = !item.indicatorQualification ? '待生源校确认' : indicatorEligible ? '有指标分配资格' : '无指标分配资格'; return `
${h(item.exam.code)} · 第 ${h(item.payload.round || 1)} 轮

${h(item.exam.name)}

${badge(item.status)}
${['填报志愿','志愿锁定','投档审核','录取结束'].map((label, index) => `
${index < progressIndex ? '✓' : index + 1}${label}
`).join('')}
本场总成绩${item.totalScore == null ? '成绩尚未完整发布' : `${h(item.totalScore)} 分`}特征分 ${h(item.featureScore || 0)}特长类型 ${h(qualification)}指标资格 ${h(indicatorText)}${h(phaseLabels[item.status] || item.status)}

${h(item.payload.progress || '等待录取工作更新')}

${item.placement ? `
当前结果${h(placementSchool)} · ${h(item.placement.payload.categoryName)}${item.noticeNumber ? `录取通知书编号:${h(item.noticeNumber)}` : ''}${item.placement.status === 'final' ? '已正式录取,可下载带防伪二维码的正式录取通知书' : item.placement.status === 'withdrawal_pending' ? '招生学校申请退档,等待超级管理员审核' : '材料已发送招生学校审核'}${item.placement.status === 'final' ? `` : ''}
` : ''}${canFill ? `
1 个指标分配志愿 + ${h(item.payload.maxChoices)} 个普通志愿指标栏仅在生源校确认有资格时开放;每次保存计为一次提交。
已提交 ${h(item.submissionCount)} / ${h(item.maxSubmissions)} 次
${choiceRows}
` : choices.length ? `
${item.preferenceLocked ? `达到 ${h(item.maxSubmissions)} 次上限,志愿已自动锁定` : '已锁定志愿顺序'}${lockedRows}
` : `
${h(item.supplementIneligibilityReason || (item.preferenceLocked ? '志愿提交次数已用完,系统已自动锁定。' : '当前不能填报:请等待成绩完整发布或志愿填报窗口开放。'))}
`}
`; }).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 = `
LOGIN PASSWORD

修改登录密码

密码修改成功后立即生效。请使用至少 8 位、且与当前密码不同的新密码。

当前账号
${account}
账户类型
${type}
`; const totpPanel = totp.enabled ? `
TWO-STEP VERIFICATION

TOTP 二次验证已开启

登录密码验证通过后,还需要输入验证器应用生成的 6 位动态验证码。

保护中剩余 ${h(totp.recoveryCodesRemaining)} 个恢复码
重新生成恢复码
关闭二次验证
` : `
TWO-STEP VERIFICATION

添加 TOTP 二次验证

使用 Microsoft Authenticator、Google Authenticator、1Password 等验证器应用扫码。即使密码泄露,没有动态验证码也无法登录。

绑定时会显示二维码和手动密钥;验证成功后请立即保存恢复码。

`; return `
${password}${totpPanel}
`; } return { adminNavForUser, portalShell, loadingPanel, renderCandidate, accountSecurity }; }