Implement fixed candidate account onboarding
This commit is contained in:
@@ -103,13 +103,13 @@ function renderHome() {
|
||||
app.innerHTML = `${publicHeader()}<main class="public-main">
|
||||
<section class="hero">
|
||||
<div class="hero-grid">
|
||||
<div class="hero-copy"><div class="notice-ticker"><span>最新</span><button data-action="open-notice" data-id="${h(topNotice?.id)}">${h(topNotice?.title || '欢迎使用衡准考试服务平台')}</button></div><p class="overline">HAIZHOU EXAMINATION SERVICE</p><h1>每一次应考,<br><em>都有清晰的下一步。</em></h1><p class="hero-lead">从身份核验、选科报名到准考证与成绩查询,所有考试事项集中办理,进度实时可见。</p><div class="hero-actions">${state.user?.role === 'candidate' ? `<button class="solid-button large" data-route="candidate/exams">继续报名 ${icons.arrow}</button>` : `<button class="solid-button large" data-route="register">创建考生账号 ${icons.arrow}</button>`}<button class="ghost-button large" data-action="scroll-to" data-target="home-exams">查看开放考试</button></div><div class="hero-stats"><div><strong>${h(stats.candidates || 0)}</strong><span>在册考生</span></div><div><strong>${h(stats.registrations || 0)}</strong><span>报名记录</span></div><div><strong>${h(stats.exams || 0)}</strong><span>开放考试</span></div></div></div>
|
||||
<div class="hero-copy"><div class="notice-ticker"><span>最新</span><button data-action="open-notice" data-id="${h(topNotice?.id)}">${h(topNotice?.title || '欢迎使用衡准考试服务平台')}</button></div><p class="overline">HAIZHOU EXAMINATION SERVICE</p><h1>一个报名号,<br><em>贯穿每一次考试。</em></h1><p class="hero-lead">使用学校下发的报名号登录,完成密码更新和个人信息核验后,即可办理所有考试事项。</p><div class="hero-actions">${state.user?.role === 'candidate' ? `<button class="solid-button large" data-route="candidate/dashboard">进入考生中心 ${icons.arrow}</button>` : state.publicData.selfRegistrationEnabled ? `<button class="solid-button large" data-route="register">申请报名号 ${icons.arrow}</button>` : `<button class="solid-button large" data-route="login">使用报名号登录 ${icons.arrow}</button>`}<button class="ghost-button large" data-action="scroll-to" data-target="home-exams">查看开放考试</button></div><div class="hero-stats"><div><strong>${h(stats.candidates || 0)}</strong><span>在册考生</span></div><div><strong>${h(stats.registrations || 0)}</strong><span>报名记录</span></div><div><strong>${h(stats.exams || 0)}</strong><span>开放考试</span></div></div></div>
|
||||
${featured ? renderHeroTicket(featured) : '<div class="hero-ticket empty-state">暂无开放考试</div>'}
|
||||
</div>
|
||||
</section>
|
||||
<section class="content-section" id="home-notices"><div class="section-heading"><div><p class="overline">NOTICE BOARD</p><h2>通知公告</h2></div><p>报名、考试、准考证与成绩发布的重要时间,请以平台通知为准。</p></div><div class="notice-layout"><article class="featured-notice">${topNotice ? `<span>${h(topNotice.category)}</span><h3>${h(topNotice.title)}</h3><p>${h(topNotice.summary)}</p><footer><time>${formatDate(topNotice.publishAt)}</time><button data-action="open-notice" data-id="${h(topNotice.id)}">阅读通知 ${icons.arrow}</button></footer>` : '<p>暂无通知</p>'}</article><div class="notice-list">${notices.slice(1, 5).map(renderNoticeRow).join('') || '<div class="empty-state">暂无更多通知</div>'}</div></div></section>
|
||||
<section class="content-section exam-section" id="home-exams"><div class="section-heading"><div><p class="overline">OPEN EXAMINATIONS</p><h2>考试报名</h2></div><p>登录后选择考试,并按实际需要勾选报考科目。</p></div><div class="public-exam-grid">${exams.map(renderPublicExam).join('') || '<div class="empty-state">当前没有已发布的考试</div>'}</div></section>
|
||||
<section class="service-flow" id="service-flow"><div class="section-heading light"><div><p class="overline">SERVICE FLOW</p><h2>从注册到查分,只需五步</h2></div><p>每一步都会在考生中心显示当前状态和下一项操作。</p></div><div class="flow-track">${[['01','注册并填写资料','使用手机号注册,完善实名信息。'],['02','等待资料审核','管理员核验身份与学籍信息。'],['03','选择考试科目','在报名期内自主勾选报考科目。'],['04','下载准考证','管理员生成后,在开放期内下载。'],['05','查询考试成绩','成绩发布后登录平台查询。']].map(item => `<article><span>${item[0]}</span><h3>${item[1]}</h3><p>${item[2]}</p></article>`).join('')}</div></section>
|
||||
<section class="service-flow" id="service-flow"><div class="section-heading light"><div><p class="overline">SERVICE FLOW</p><h2>报名号是唯一账户</h2></div><p>报名号不会随考试改变,每场考试只新增一条报名记录。</p></div><div class="flow-track">${[['01','领取报名号','学校创建账户并下发初始密码。'],['02','修改初始密码','首次登录必须设置自己的新密码。'],['03','补全个人信息','填写籍贯、住址、手机、邮箱和班级等资料。'],['04','选择考试科目','资料审核通过后自主选择考试。'],['05','下载准考证与查分','继续使用同一报名号办理后续事项。']].map(item => `<article><span>${item[0]}</span><h3>${item[1]}</h3><p>${item[2]}</p></article>`).join('')}</div></section>
|
||||
</main><footer class="public-footer"><div>${brand()}<p>${h(organization.name || '海州市教育考试中心')} · ${h(organization.phone || '')}</p></div><span>本平台展示数据仅用于系统演示</span></footer>`;
|
||||
}
|
||||
|
||||
@@ -128,16 +128,17 @@ function renderPublicExam(exam) {
|
||||
|
||||
function renderAuth(kind) {
|
||||
const login = kind === 'login';
|
||||
app.innerHTML = `<main class="auth-page"><section class="auth-story"><div>${brand()}<p class="overline">CANDIDATE SERVICE</p><h1>${login ? '欢迎回来,' : '从这里,'}<br><em>${login ? '继续你的考试进程。' : '开始你的应考旅程。'}</em></h1><p>统一管理报名、审核、准考证与成绩,不错过每一个关键节点。</p></div><div class="auth-quote"><span>考试服务承诺</span><p>资料有状态、报名有回执、下载有时间、成绩有依据。</p></div></section><section class="auth-panel"><button class="back-link" data-route="home">← 返回首页</button><div class="auth-card"><p class="overline">${login ? 'ACCOUNT LOGIN' : 'CREATE ACCOUNT'}</p><h2>${login ? '登录衡准' : '考生自主注册'}</h2><p>${login ? '使用考生账号或管理员账号进入系统。' : '请填写真实身份信息,注册后由管理员审核。'}</p>${login ? loginForm() : registerForm()}<div class="auth-switch">${login ? '还没有考生账号?' : '已经注册过?'}<button data-route="${login ? 'register' : 'login'}">${login ? '立即注册' : '返回登录'}</button></div>${login ? `<div class="demo-accounts"><strong>演示账号</strong><button data-action="fill-demo" data-type="candidate">考生:13800138000 / Candidate123!</button><button data-action="fill-demo" data-type="admin">超级管理员:admin / Admin123!</button><button data-action="fill-demo" data-type="school">校级管理员:school_admin / School123!</button><button data-action="fill-demo" data-type="class">班级管理员:class_admin / Class123!</button></div>` : ''}</div></section></main>`;
|
||||
const selfRegistration = state.publicData.selfRegistrationEnabled;
|
||||
app.innerHTML = `<main class="auth-page"><section class="auth-story"><div>${brand()}<p class="overline">CANDIDATE SERVICE</p><h1>${login ? '凭一个号码,' : '自主申请,'}<br><em>${login ? '办理每一次考试。' : '领取固定报名号。'}</em></h1><p>报名号就是考生账户,不因考试、科目或年度报名而改变。</p></div><div class="auth-quote"><span>首次登录顺序</span><p>修改初始密码 → 补全个人信息 → 等待资料审核。</p></div></section><section class="auth-panel"><button class="back-link" data-route="home">← 返回首页</button><div class="auth-card"><p class="overline">${login ? 'ACCOUNT LOGIN' : 'CANDIDATE NUMBER'}</p><h2>${login ? '报名号登录' : '自主申请报名号'}</h2><p>${login ? '考生填写报名号和密码;管理员继续使用管理账号。' : selfRegistration ? '提交基础学籍范围后,系统生成一个长期使用的报名号。' : '当前未开放自主注册,请联系学校领取报名号和初始密码。'}</p>${login ? loginForm() : selfRegistration ? registerForm() : '<div class="registration-closed"><strong>自主注册已关闭</strong><span>学校管理员会为考生创建账户并下发初始密码。</span><button class="solid-button" data-route="login">返回报名号登录</button></div>'}${login && selfRegistration ? `<div class="auth-switch">还没有报名号?<button data-route="register">自主申请</button></div>` : !login ? '<div class="auth-switch">已经有报名号?<button data-route="login">返回登录</button></div>' : ''}${login ? `<div class="demo-accounts"><strong>演示账号</strong><button data-action="fill-demo" data-type="candidate">考生:2026-HZ01-F-0001 / Candidate123!</button><button data-action="fill-demo" data-type="admin">超级管理员:admin / Admin123!</button><button data-action="fill-demo" data-type="school">校级管理员:school_admin / School123!</button><button data-action="fill-demo" data-type="class">班级管理员:class_admin / Class123!</button></div>` : ''}</div></section></main>`;
|
||||
}
|
||||
|
||||
function loginForm() {
|
||||
return `<form class="stack-form" data-form="login"><label><span>账号</span><input name="username" autocomplete="username" required placeholder="手机号或管理员账号"></label><label><span>密码</span><input name="password" type="password" autocomplete="current-password" required placeholder="请输入登录密码"></label><button class="solid-button large" type="submit">登录系统 ${icons.arrow}</button></form>`;
|
||||
return `<form class="stack-form" data-form="login"><label><span>报名号 / 管理员账号</span><input name="username" autocomplete="username" required placeholder="例如 2026-HZ01-F-0001"></label><label><span>密码</span><input name="password" type="password" autocomplete="current-password" required placeholder="首次登录请输入学校下发的初始密码"></label><button class="solid-button large" type="submit">登录系统 ${icons.arrow}</button></form>`;
|
||||
}
|
||||
|
||||
function registerForm() {
|
||||
const schools = state.publicData.schools || [];
|
||||
return `<form class="stack-form register-form" data-form="register"><div class="field-row"><label><span>考生姓名 *</span><input name="name" required placeholder="与证件一致"></label><label><span>性别</span><select name="gender"><option value="">请选择</option><option>男</option><option>女</option></select></label></div><label><span>证件号码 *</span><input name="idNumber" required placeholder="居民身份证号码"></label><div class="field-row"><label><span>手机号 *</span><input name="phone" required placeholder="用于接收通知"></label><label><span>登录账号 *</span><input name="username" required placeholder="建议使用手机号"></label></div><div class="field-row"><label><span>就读学校 *</span><select name="schoolId" data-action="school-select" required><option value="">请选择学校</option>${schools.map(item => `<option value="${h(item.id)}">${h(item.name)}</option>`).join('')}</select></label><label><span>班级 *</span><select name="classId" required><option value="">请先选择学校</option></select></label></div><label><span>登录密码 *</span><input name="password" type="password" required minlength="8" placeholder="至少 8 位字符"></label><label class="agreement"><input type="checkbox" required><span>我确认以上信息真实有效,并同意用于考试报名与身份核验。</span></label><button class="solid-button large" type="submit">注册考生账号 ${icons.arrow}</button></form>`;
|
||||
return `<form class="stack-form register-form" data-form="register"><div class="field-row"><label><span>考生姓名 *</span><input name="name" required placeholder="与证件一致"></label><label><span>性别 *</span><select name="gender" required><option value="">请选择</option><option>男</option><option>女</option></select></label></div><div class="field-row"><label><span>就读学校 *</span><select name="schoolId" data-action="school-select" required><option value="">请选择学校</option>${schools.map(item => `<option value="${h(item.id)}">${h(item.name)}</option>`).join('')}</select></label><label><span>班级 *</span><select name="classId" required><option value="">请先选择学校</option></select></label></div><label><span>设置登录密码 *</span><input name="password" type="password" required minlength="8" placeholder="至少 8 位字符"></label><label class="agreement"><input type="checkbox" required><span>我会妥善保存系统生成的报名号,并在登录后补全真实个人信息。</span></label><button class="solid-button large" type="submit">生成我的报名号 ${icons.arrow}</button></form>`;
|
||||
}
|
||||
|
||||
const candidateNav = [
|
||||
@@ -164,6 +165,7 @@ function portalHeadingAction(role, page) {
|
||||
if (role === 'admin' && page === 'notices') return `<button class="solid-button" data-action="new-notice">${icons.plus} 发布通知</button>`;
|
||||
if (role === 'admin' && page === 'exams') return `<button class="solid-button" data-action="new-exam">${icons.plus} 创建考试</button>`;
|
||||
if (role === 'admin' && page === 'admins') return `<button class="solid-button" data-action="new-admin">${icons.plus} 添加管理员</button>`;
|
||||
if (role === 'admin' && page === 'candidates' && state.user.adminLevel !== 'class') return `<button class="solid-button" data-action="new-candidate-account">${icons.plus} 创建考生账户</button>`;
|
||||
if (role === 'admin' && page === 'centers') return `<button class="solid-button" data-action="new-center">${icons.plus} 提交新考点</button>`;
|
||||
if (role === 'candidate' && page === 'profile') return `<span class="heading-status">当前状态 ${badge(state.profile?.status || 'pending')}</span>`;
|
||||
return '';
|
||||
@@ -173,8 +175,29 @@ function loadingPanel() {
|
||||
return `<div class="loading-panel"><i></i><span>正在读取数据</span></div>`;
|
||||
}
|
||||
|
||||
function onboardingShell(stage, content) {
|
||||
const passwordDone = stage !== 'password';
|
||||
return `<main class="onboarding-page"><aside class="onboarding-identity">${brand()}<span>固定报名号</span><strong>${h(state.user.candidateNumber)}</strong><p>这个号码就是你的考生账户。以后参加不同考试,仍然使用同一个报名号。</p><div class="onboarding-steps"><div class="${stage === 'password' ? 'current' : 'done'}"><i>${passwordDone ? '✓' : '1'}</i><span><b>修改初始密码</b><small>设置仅本人知道的新密码</small></span></div><div class="${stage === 'profile' ? 'current' : passwordDone ? '' : ''}"><i>2</i><span><b>补全个人信息</b><small>实名、籍贯、住址和学籍信息</small></span></div><div><i>3</i><span><b>等待资料审核</b><small>审核通过后开始考试报名</small></span></div></div><button data-action="logout">退出当前账户</button></aside><section class="onboarding-work"><div class="onboarding-work-head"><span>FIRST SIGN-IN</span><h1>${stage === 'password' ? '先保护你的账户' : '建立完整考生档案'}</h1><p>${stage === 'password' ? '初始密码只用于第一次登录。修改成功后才可填写个人信息。' : '带 * 的信息会用于身份核验、学校管理范围和考试联系。'}</p></div>${content}</section></main>`;
|
||||
}
|
||||
|
||||
function passwordOnboardingForm() {
|
||||
return `<section class="panel password-onboarding"><div class="password-rule"><b>新密码要求</b><span>至少 8 位,且不能与初始密码相同。</span></div><form class="stack-form" data-form="candidate-password"><label><span>当前初始密码</span><input name="currentPassword" type="password" autocomplete="current-password" required></label><label><span>设置新密码</span><input name="newPassword" type="password" autocomplete="new-password" minlength="8" required></label><label><span>再次输入新密码</span><input name="confirmPassword" type="password" autocomplete="new-password" minlength="8" required></label><button class="solid-button large" type="submit">保存新密码并继续 ${icons.arrow}</button></form></section>`;
|
||||
}
|
||||
|
||||
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: ['个人资料', '维护实名认证与联系方式;修改后需要重新审核。'],
|
||||
@@ -212,10 +235,11 @@ function candidateDashboard(data) {
|
||||
return `<section class="candidate-welcome"><div><span>${new Date().getHours() < 12 ? '上午好' : '下午好'}</span><h2>${h(data.profile?.name || state.user.displayName)},下一步已为你标出。</h2><p>${data.profile?.status === 'approved' ? (registration ? '报名已进入考务流程,请留意准考证下载时间。' : '个人资料已通过审核,现在可以选择考试和报考科目。') : '个人资料正在审核中,通过后即可进行考试报名。'}</p></div><div class="welcome-seal">准<br>考</div></section><div class="summary-grid"><article><span class="summary-icon">${icons.user}</span><div><small>个人资料</small><strong>${statusLabels[data.profile?.status] || '未填写'}</strong></div>${badge(data.profile?.status || 'pending')}</article><article><span class="summary-icon">${icons.exam}</span><div><small>已报名考试</small><strong>${data.registrations.length} 场</strong></div><button data-route="candidate/exams">去报名</button></article><article><span class="summary-icon">${icons.ticket}</span><div><small>可下载准考证</small><strong>${data.registrations.filter(item => item.admitCard).length} 份</strong></div><button data-route="candidate/admit">查看</button></article><article><span class="summary-icon">${icons.chart}</span><div><small>已发布成绩</small><strong>${data.results.length} 科</strong></div><button data-route="candidate/results">查分</button></article></div><div class="candidate-grid"><section class="panel progress-panel"><div class="panel-title"><h2>我的应考进度</h2><span>自动更新</span></div><div class="candidate-progress">${steps.map((step, index) => `<div class="progress-step ${step[1] ? 'done' : index === steps.findIndex(item => !item[1]) ? 'current' : ''}"><i>${step[1] ? '✓' : index + 1}</i><div><strong>${step[0]}</strong><small>${step[2]}</small></div></div>`).join('')}</div></section><section class="panel compact-notices"><div class="panel-title"><h2>最近通知</h2><button data-route="candidate/notices">全部通知</button></div>${data.notices.map(notice => `<button data-action="open-notice" data-id="${h(notice.id)}"><time>${formatDate(notice.publishAt)}</time><span>${h(notice.title)}</span></button>`).join('')}</section></div>`;
|
||||
}
|
||||
|
||||
function candidateProfile(data) {
|
||||
function candidateProfile(data, onboarding = false) {
|
||||
const { profile, schools = [], classes = [], workflow } = data;
|
||||
const step = workflow?.currentStepDetail;
|
||||
return `<section class="panel form-panel">${workflow ? `<div class="candidate-flow-note"><span>当前审批</span><strong>${h(step?.name || statusLabels[workflow.status])}</strong><small>${workflow.assignee ? `由 ${h(workflow.assignee.displayName)} 处理` : '流程已结束'}</small></div>` : ''}<div class="form-section-title"><span>01</span><div><h2>实名信息</h2><p>姓名与证件号码须与有效证件完全一致。</p></div></div><form class="profile-form" data-form="candidate-profile"><div class="form-grid"><label><span>考生姓名 *</span><input name="name" required value="${h(profile?.name)}"></label><label><span>性别</span><select name="gender"><option value="">请选择</option><option ${profile?.gender === '男' ? 'selected' : ''}>男</option><option ${profile?.gender === '女' ? 'selected' : ''}>女</option></select></label><label><span>证件号码 *</span><input name="idNumber" required value="${h(profile?.idNumber)}"></label><label><span>手机号 *</span><input name="phone" required value="${h(profile?.phone)}"></label></div><div class="form-section-title"><span>02</span><div><h2>学籍与联系信息</h2><p>学校和班级决定资料的管理范围。</p></div></div><div class="form-grid"><label><span>就读学校 *</span><select name="schoolId" data-action="school-select" required><option value="">请选择学校</option>${schools.map(item => `<option value="${h(item.id)}" ${profile?.schoolId === item.id ? 'selected' : ''}>${h(item.name)}</option>`).join('')}</select></label><label><span>班级 *</span><select name="classId" required>${classes.filter(item => item.schoolId === profile?.schoolId).map(item => `<option value="${h(item.id)}" ${profile?.classId === item.id ? 'selected' : ''}>${h(item.name)}</option>`).join('')}</select></label><label><span>电子邮箱</span><input name="email" type="email" value="${h(profile?.email)}"></label><label><span>常住地址</span><input name="address" value="${h(profile?.address)}"></label><label><span>紧急联系人</span><input name="emergencyContact" value="${h(profile?.emergencyContact)}"></label><label><span>紧急联系电话</span><input name="emergencyPhone" value="${h(profile?.emergencyPhone)}"></label></div>${profile?.reviewNote ? `<div class="review-note ${profile.status}"><strong>审核意见</strong><p>${h(profile.reviewNote)}</p></div>` : ''}<div class="form-actions"><p>保存后资料将按当前流程重新审批。</p><button class="solid-button" type="submit">保存并提交审批</button></div></form></section>`;
|
||||
const idNumber = profile?.idNumber?.startsWith('PENDING-') ? '' : profile?.idNumber;
|
||||
return `<section class="panel form-panel ${onboarding ? 'onboarding-profile' : ''}">${workflow ? `<div class="candidate-flow-note"><span>当前审批</span><strong>${h(step?.name || statusLabels[workflow.status])}</strong><small>${workflow.assignee ? `由 ${h(workflow.assignee.displayName)} 处理` : '流程已结束'}</small></div>` : ''}<form class="profile-form" data-form="candidate-profile"><div class="form-section-title"><span>01</span><div><h2>身份信息</h2><p>姓名和证件号码须与有效证件完全一致。</p></div></div><div class="form-grid"><label><span>考生姓名 *</span><input name="name" required value="${h(profile?.name)}"></label><label><span>性别 *</span><select name="gender" required><option value="">请选择</option><option ${profile?.gender === '男' ? 'selected' : ''}>男</option><option ${profile?.gender === '女' ? 'selected' : ''}>女</option></select></label><label><span>证件号码 *</span><input name="idNumber" required value="${h(idNumber)}"></label><label><span>出生日期</span><input name="birthDate" type="date" value="${h(profile?.birthDate)}"></label><label><span>籍贯 *</span><input name="nativePlace" required value="${h(profile?.nativePlace)}" placeholder="例如:江苏海州"></label><label><span>民族</span><input name="ethnicity" value="${h(profile?.ethnicity)}" placeholder="例如:汉族"></label></div><div class="form-section-title"><span>02</span><div><h2>学校与班级</h2><p>学校和班级决定资料审批范围。</p></div></div><div class="form-grid"><label><span>就读学校 *</span><select name="schoolId" data-action="school-select" required><option value="">请选择学校</option>${schools.map(item => `<option value="${h(item.id)}" ${profile?.schoolId === item.id ? 'selected' : ''}>${h(item.name)}</option>`).join('')}</select></label><label><span>班级 *</span><select name="classId" required><option value="">请选择班级</option>${classes.filter(item => item.schoolId === profile?.schoolId).map(item => `<option value="${h(item.id)}" ${profile?.classId === item.id ? 'selected' : ''}>${h(item.name)}</option>`).join('')}</select></label></div><div class="form-section-title"><span>03</span><div><h2>家庭与联系信息</h2><p>用于考试通知、身份复核和紧急联系。</p></div></div><div class="form-grid"><label><span>手机号 *</span><input name="phone" required value="${h(profile?.phone)}"></label><label><span>电子邮箱 *</span><input name="email" type="email" required value="${h(profile?.email)}"></label><label class="wide-field"><span>家庭住址 *</span><input name="address" required value="${h(profile?.address)}" placeholder="请填写省、市、区及详细门牌"></label><label><span>邮政编码</span><input name="postalCode" value="${h(profile?.postalCode)}"></label><label><span>监护人姓名</span><input name="guardianName" value="${h(profile?.guardianName)}"></label><label><span>监护人电话</span><input name="guardianPhone" value="${h(profile?.guardianPhone)}"></label><label><span>紧急联系人</span><input name="emergencyContact" value="${h(profile?.emergencyContact)}"></label><label><span>紧急联系电话</span><input name="emergencyPhone" value="${h(profile?.emergencyPhone)}"></label></div>${profile?.reviewNote ? `<div class="review-note ${profile.status}"><strong>审核意见</strong><p>${h(profile.reviewNote)}</p></div>` : ''}<div class="form-actions"><p>${onboarding ? '提交后进入资料审批,审核通过即可报名考试。' : '保存后资料将按当前流程重新审批。'}</p><button class="solid-button" type="submit">${onboarding ? '提交个人信息' : '保存并提交审批'}</button></div></form></section>`;
|
||||
}
|
||||
|
||||
function candidateExams(data) {
|
||||
@@ -223,7 +247,7 @@ function candidateExams(data) {
|
||||
}
|
||||
|
||||
function candidateRegistrations(registrations) {
|
||||
return registrations.length ? `<div class="registration-cards">${registrations.map(reg => `<article class="registration-card"><header><div><span class="exam-code">${h(reg.exam.code)}</span><h2>${h(reg.exam.name)}</h2></div>${badge(reg.status)}</header><div class="registration-info"><dl><div><dt>报名号</dt><dd class="mono">${h(reg.registrationNumber || '审批通过后生成')}</dd></div><div><dt>当前审批</dt><dd>${h(reg.workflow?.currentStepDetail?.name || statusLabels[reg.workflow?.status] || '待提交')}</dd></div><div><dt>责任人</dt><dd>${h(reg.workflow?.assignee?.displayName || '—')}</dd></div><div><dt>缴费状态</dt><dd>${badge(reg.paymentStatus)}</dd></div></dl><div class="selected-subjects"><strong>已选科目</strong><div>${reg.subjects.map(subject => `<span>${h(subject.name)}<small>${h(subject.date)} ${h(subject.start)}</small></span>`).join('')}</div></div></div><footer><p>${reg.reviewNote ? `审核意见:${h(reg.reviewNote)}` : reg.status === 'pending' ? '报名已进入审批流程,请留意当前步骤。' : '报名已经确认,请留意准考证下载通知。'}</p>${reg.admitCard ? `<button class="text-button" data-route="candidate/admit">查看准考证 →</button>` : ''}</footer></article>`).join('')}</div>` : emptyState('还没有考试报名', '资料审核通过后,即可在“考试报名”中选择考试与科目。', 'candidate/exams', '去考试报名');
|
||||
return registrations.length ? `<div class="registration-cards">${registrations.map(reg => `<article class="registration-card"><header><div><span class="exam-code">${h(reg.exam.code)}</span><h2>${h(reg.exam.name)}</h2></div>${badge(reg.status)}</header><div class="registration-info"><dl><div><dt>账户报名号</dt><dd class="mono">${h(reg.registrationNumber || state.user.candidateNumber)}</dd></div><div><dt>当前审批</dt><dd>${h(reg.workflow?.currentStepDetail?.name || statusLabels[reg.workflow?.status] || '待提交')}</dd></div><div><dt>责任人</dt><dd>${h(reg.workflow?.assignee?.displayName || '—')}</dd></div><div><dt>缴费状态</dt><dd>${badge(reg.paymentStatus)}</dd></div></dl><div class="selected-subjects"><strong>已选科目</strong><div>${reg.subjects.map(subject => `<span>${h(subject.name)}<small>${h(subject.date)} ${h(subject.start)}</small></span>`).join('')}</div></div></div><footer><p>${reg.reviewNote ? `审核意见:${h(reg.reviewNote)}` : reg.status === 'pending' ? '本次考试报名已进入审批,账户报名号不会改变。' : '本次报名已经确认,请留意准考证下载通知。'}</p>${reg.admitCard ? `<button class="text-button" data-route="candidate/admit">查看准考证 →</button>` : ''}</footer></article>`).join('')}</div>` : emptyState('还没有考试报名', '资料审核通过后,即可在“考试报名”中选择考试与科目。', 'candidate/exams', '去考试报名');
|
||||
}
|
||||
|
||||
function candidateAdmit(registrations) {
|
||||
@@ -252,7 +276,7 @@ async function renderAdmin(page) {
|
||||
centers: ['考务场所档案', state.user.adminLevel === 'school' ? '查看本校考点与结构化考场,所有变更提交后进入审批。' : '管理各校考点、考场容量与变更审批台账。'],
|
||||
flows: [state.user.adminLevel === 'super' ? '流程监督' : '流程中心', state.user.adminLevel === 'super' ? '查看全部流程,监督转交、修改和退回节点。' : '处理分配给你的流程,并可转交给本校同级管理员。'],
|
||||
'flow-design': ['流程设计', '配置考生信息、报名审核与考点考场变更的审批步骤。'],
|
||||
'number-rules': ['报名号规则', '设计号码组成,并为缺失报名号的已审核记录批量生成。']
|
||||
'number-rules': ['报名号规则', '设计新账户号码组成,并为旧考试记录批量同步固定报名号。']
|
||||
};
|
||||
const allowedPages = adminNavForUser().map(item => item[0]);
|
||||
if (!meta[page] || !allowedPages.includes(page)) page = 'dashboard';
|
||||
@@ -279,12 +303,12 @@ function adminDashboard(data) {
|
||||
|
||||
function adminCandidates(candidates) {
|
||||
const readOnly = state.user.adminLevel === 'class';
|
||||
return `<section class="panel data-panel"><div class="data-toolbar"><label class="search-box">${icons.search}<input data-action="table-search" data-target="candidateTable" placeholder="搜索姓名、证件号、学校"></label><div class="filter-pills"><button class="active" data-action="status-filter" data-target="candidateTable" data-status="all">全部</button><button data-action="status-filter" data-target="candidateTable" data-status="pending">待审核</button><button data-action="status-filter" data-target="candidateTable" data-status="approved">已通过</button><button data-action="status-filter" data-target="candidateTable" data-status="rejected">需修改</button></div></div><div class="table-scroll"><table id="candidateTable"><thead><tr><th>考生</th><th>证件号码</th><th>学校 / 班级</th><th>当前流程</th><th>更新时间</th><th>状态</th><th>操作</th></tr></thead><tbody>${candidates.map(item => `<tr data-status="${h(item.status)}"><td><div class="person-cell"><span>${h(item.name.slice(0,1))}</span><div><strong>${h(item.name)}</strong><small>${h(item.gender || '未填写')} · ${h(item.phone)}</small></div></div></td><td class="mono">${h(item.idNumberMasked)}</td><td><strong>${h(item.school || '未填写')}</strong><small>${h(item.grade || '')}</small></td><td><strong>${h(item.workflow?.currentStepDetail?.name || '流程已结束')}</strong><small>${h(item.workflow?.assignee?.displayName || '')}</small></td><td>${formatDate(item.updatedAt,true)}</td><td>${badge(item.status)}</td><td><button class="row-action" data-action="review-candidate" data-id="${h(item.id)}">${readOnly ? '查看' : '查看流程'}</button></td></tr>`).join('')}</tbody></table></div></section>`;
|
||||
return `<section class="panel data-panel"><div class="data-toolbar"><label class="search-box">${icons.search}<input data-action="table-search" data-target="candidateTable" placeholder="搜索报名号、姓名、证件号或学校"></label><div class="filter-pills"><button class="active" data-action="status-filter" data-target="candidateTable" data-status="all">全部</button><button data-action="status-filter" data-target="candidateTable" data-status="pending">待审核</button><button data-action="status-filter" data-target="candidateTable" data-status="approved">已通过</button><button data-action="status-filter" data-target="candidateTable" data-status="rejected">需修改</button></div></div><div class="table-scroll"><table id="candidateTable"><thead><tr><th>报名号 / 考生</th><th>证件号码</th><th>学校 / 班级</th><th>账户进度</th><th>更新时间</th><th>资料状态</th><th>操作</th></tr></thead><tbody>${candidates.map(item => `<tr data-status="${h(item.status)}"><td><div class="person-cell"><span>${h(item.name.slice(0,1))}</span><div><strong>${h(item.name)}</strong><small class="mono">${h(item.candidateNumber || '待分配')}</small></div></div></td><td class="mono">${h(item.idNumberMasked)}</td><td><strong>${h(item.school || '未填写')}</strong><small>${h(item.grade || '')}</small></td><td><strong>${item.mustChangePassword ? '待首次改密' : item.profileCompleted ? h(item.workflow?.currentStepDetail?.name || '资料已提交') : '待补全资料'}</strong><small>${h(item.workflow?.assignee?.displayName || '')}</small></td><td>${formatDate(item.updatedAt,true)}</td><td>${item.profileCompleted ? badge(item.status) : '<span class="onboarding-badge">未完成</span>'}</td><td><button class="row-action" data-action="review-candidate" data-id="${h(item.id)}" ${item.profileCompleted ? '' : 'disabled'}>${item.profileCompleted ? (readOnly ? '查看' : '查看流程') : '等待考生'}</button></td></tr>`).join('')}</tbody></table></div></section>`;
|
||||
}
|
||||
|
||||
function adminRegistrations(registrations) {
|
||||
const readOnly = state.user.adminLevel === 'class';
|
||||
return `<section class="panel data-panel"><div class="data-toolbar"><label class="search-box">${icons.search}<input data-action="table-search" data-target="registrationTable" placeholder="搜索考生、考试、报名号"></label><div class="filter-pills"><button class="active" data-action="status-filter" data-target="registrationTable" data-status="all">全部</button><button data-action="status-filter" data-target="registrationTable" data-status="pending">待审核</button><button data-action="status-filter" data-target="registrationTable" data-status="approved">已通过</button><button data-action="status-filter" data-target="registrationTable" data-status="rejected">已退回</button></div></div><div class="table-scroll"><table id="registrationTable"><thead><tr><th>考生</th><th>考试 / 科目</th><th>报名号</th><th>当前流程</th><th>缴费</th><th>状态</th><th>操作</th></tr></thead><tbody>${registrations.map(reg => `<tr data-status="${h(reg.status)}"><td><div class="person-cell"><span>${h((reg.candidate?.name || '?').slice(0,1))}</span><div><strong>${h(reg.candidate?.name)}</strong><small>${h(reg.candidate?.grade || '')}</small></div></div></td><td><strong>${h(reg.exam.name)}</strong><small>${reg.subjects.map(subject => h(subject.name)).join('、')}</small></td><td class="mono"><strong>${h(reg.registrationNumber || '审批通过后生成')}</strong><small>${formatDate(reg.createdAt,true)}</small></td><td><strong>${h(reg.workflow?.currentStepDetail?.name || '流程已结束')}</strong><small>${h(reg.workflow?.assignee?.displayName || '')}</small></td><td>${badge(reg.paymentStatus)}</td><td>${badge(reg.status)}</td><td><button class="row-action" data-action="review-registration" data-id="${h(reg.id)}">${readOnly ? '查看' : '查看流程'}</button></td></tr>`).join('')}</tbody></table></div></section>`;
|
||||
return `<section class="panel data-panel"><div class="data-toolbar"><label class="search-box">${icons.search}<input data-action="table-search" data-target="registrationTable" placeholder="搜索考生、考试、固定报名号"></label><div class="filter-pills"><button class="active" data-action="status-filter" data-target="registrationTable" data-status="all">全部</button><button data-action="status-filter" data-target="registrationTable" data-status="pending">待审核</button><button data-action="status-filter" data-target="registrationTable" data-status="approved">已通过</button><button data-action="status-filter" data-target="registrationTable" data-status="rejected">已退回</button></div></div><div class="table-scroll"><table id="registrationTable"><thead><tr><th>考生</th><th>考试 / 科目</th><th>账户报名号</th><th>当前流程</th><th>缴费</th><th>状态</th><th>操作</th></tr></thead><tbody>${registrations.map(reg => `<tr data-status="${h(reg.status)}"><td><div class="person-cell"><span>${h((reg.candidate?.name || '?').slice(0,1))}</span><div><strong>${h(reg.candidate?.name)}</strong><small>${h(reg.candidate?.grade || '')}</small></div></div></td><td><strong>${h(reg.exam.name)}</strong><small>${reg.subjects.map(subject => h(subject.name)).join('、')}</small></td><td class="mono"><strong>${h(reg.registrationNumber || '待同步账户号码')}</strong><small>各次考试保持一致</small></td><td><strong>${h(reg.workflow?.currentStepDetail?.name || '流程已结束')}</strong><small>${h(reg.workflow?.assignee?.displayName || '')}</small></td><td>${badge(reg.paymentStatus)}</td><td>${badge(reg.status)}</td><td><button class="row-action" data-action="review-registration" data-id="${h(reg.id)}">${readOnly ? '查看' : '查看流程'}</button></td></tr>`).join('')}</tbody></table></div></section>`;
|
||||
}
|
||||
|
||||
function adminExams(exams) {
|
||||
@@ -306,7 +330,7 @@ function adminResults(data) {
|
||||
}
|
||||
|
||||
function adminUsers(data) {
|
||||
return `<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"><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>`;
|
||||
}
|
||||
|
||||
function adminCenters(data) {
|
||||
@@ -326,7 +350,7 @@ function adminNumberRules(data) {
|
||||
const byType = Object.fromEntries(rule.segments.map(item => [item.type, item]));
|
||||
const types = Object.keys(numberSegmentMeta);
|
||||
const candidates = data.batchCandidates || [];
|
||||
return `<div class="number-rule-layout"><section class="panel rule-builder"><div class="panel-title"><div><h2>组合报名号</h2><p>勾选字段并填写顺序;流水号为必选字段。</p></div><span>实时规则</span></div><form data-form="number-rule"><input type="hidden" name="id" value="${h(rule.id)}"><div class="field-row"><label><span>规则名称</span><input name="name" required value="${h(rule.name)}"></label><label><span>字段分隔符</span><input name="separator" maxlength="3" value="${h(rule.separator)}" placeholder="留空表示直接拼接"></label></div><div class="segment-builder">${types.map((type, index) => { const segment = byType[type]; const checked = Boolean(segment) || type === 'sequence'; return `<label class="segment-option ${checked ? 'selected' : ''}"><input type="checkbox" name="include_${type}" ${checked ? 'checked' : ''} ${type === 'sequence' ? 'disabled' : ''}><span class="segment-order"><small>顺序</small><input type="number" name="position_${type}" min="1" max="9" value="${segment?.position || index + 1}"></span><span class="segment-copy"><strong>${numberSegmentMeta[type][0]}</strong><small>${numberSegmentMeta[type][1]}</small></span>${type === 'literal' ? `<input class="segment-value" name="value_${type}" value="${h(segment?.value)}" placeholder="例如 HZ">` : ''}${['year','sequence'].includes(type) ? `<input class="segment-width" type="number" name="width_${type}" min="1" max="12" value="${segment?.width || 4}" title="位数">` : ''}</label>`; }).join('')}</div><button class="solid-button" type="submit">保存并启用规则</button></form></section><aside class="rule-preview"><span>报名号样例</span><strong>${h(data.preview || '2026-HZ01-F-0001')}</strong><p>${rule.segments.map(item => numberSegmentMeta[item.type]?.[0] || item.type).join(' + ')}</p><small>历史报名号不会因规则修改而变化。</small></aside></div><section class="panel batch-number-panel"><div class="batch-number-intro"><span>BATCH GENERATION</span><h2>批量生成报名号</h2><p>只处理“审核通过且报名号为空”的记录,可按考试和学校缩小范围;批次一次提交、整体写入。</p></div><form data-form="batch-registration-numbers"><label><span>考试范围</span><select name="examId"><option value="">全部考试</option>${(data.exams || []).map(item => `<option value="${h(item.id)}">${h(item.name)}</option>`).join('')}</select></label><label><span>学校范围</span><select name="schoolId"><option value="">全部学校</option>${(data.schools || []).map(item => `<option value="${h(item.id)}">${h(item.name)}</option>`).join('')}</select></label><div class="batch-ready"><small>当前可生成</small><strong>${candidates.length}</strong><span>条</span></div><button class="solid-button" type="submit" ${candidates.length ? '' : 'disabled'}>按筛选条件批量生成</button></form><div class="batch-candidate-strip">${candidates.slice(0, 8).map(item => `<span><b>${h(item.candidateName)}</b><small>${h(item.schoolName)} · ${h(item.examName)}</small></span>`).join('') || '<p>当前没有缺失报名号的已审核记录。</p>'}${candidates.length > 8 ? `<em>另有 ${candidates.length - 8} 条</em>` : ''}</div></section>`;
|
||||
return `<section class="account-number-principle"><span>ONE CANDIDATE · ONE NUMBER</span><strong>报名号属于考生账户</strong><p>创建账户时生成一次,参加不同考试时只复制到报名记录,不再重新编号。</p></section><div class="number-rule-layout"><section class="panel rule-builder"><div class="panel-title"><div><h2>账户报名号组成</h2><p>规则用于新建考生账户;流水号为必选字段。</p></div><span>当前规则</span></div><form data-form="number-rule"><input type="hidden" name="id" value="${h(rule.id)}"><div class="field-row"><label><span>规则名称</span><input name="name" required value="${h(rule.name)}"></label><label><span>字段分隔符</span><input name="separator" maxlength="3" value="${h(rule.separator)}" placeholder="留空表示直接拼接"></label></div><div class="segment-builder">${types.map((type, index) => { const segment = byType[type]; const checked = Boolean(segment) || type === 'sequence'; return `<label class="segment-option ${checked ? 'selected' : ''}"><input type="checkbox" name="include_${type}" ${checked ? 'checked' : ''} ${type === 'sequence' ? 'disabled' : ''}><span class="segment-order"><small>顺序</small><input type="number" name="position_${type}" min="1" max="9" value="${segment?.position || index + 1}"></span><span class="segment-copy"><strong>${numberSegmentMeta[type][0]}</strong><small>${numberSegmentMeta[type][1]}</small></span>${type === 'literal' ? `<input class="segment-value" name="value_${type}" value="${h(segment?.value)}" placeholder="例如 HZ">` : ''}${['year','sequence'].includes(type) ? `<input class="segment-width" type="number" name="width_${type}" min="1" max="12" value="${segment?.width || 4}" title="位数">` : ''}</label>`; }).join('')}</div><button class="solid-button" type="submit">保存并用于新账户</button></form></section><aside class="rule-preview"><span>新账户样例</span><strong>${h(data.preview || '2026-HZ01-F-0001')}</strong><p>${rule.segments.map(item => numberSegmentMeta[item.type]?.[0] || item.type).join(' + ')}</p><small>已有考生账户的报名号不会因规则修改而变化。</small></aside></div><section class="panel batch-number-panel"><div class="batch-number-intro"><span>LEGACY SYNCHRONIZATION</span><h2>批量同步账户报名号</h2><p>用于旧数据或导入记录:缺号的考试报名会复制考生账户的固定报名号,不会为每场考试另建号码。</p></div><form data-form="batch-registration-numbers"><label><span>考试范围</span><select name="examId"><option value="">全部考试</option>${(data.exams || []).map(item => `<option value="${h(item.id)}">${h(item.name)}</option>`).join('')}</select></label><label><span>学校范围</span><select name="schoolId"><option value="">全部学校</option>${(data.schools || []).map(item => `<option value="${h(item.id)}">${h(item.name)}</option>`).join('')}</select></label><div class="batch-ready"><small>待同步</small><strong>${candidates.length}</strong><span>条</span></div><button class="solid-button" type="submit" ${candidates.length ? '' : 'disabled'}>同步账户报名号</button></form><div class="batch-candidate-strip">${candidates.slice(0, 8).map(item => `<span><b>${h(item.candidateName)}</b><small>${h(item.schoolName)} · ${h(item.examName)}</small></span>`).join('') || '<p>当前所有考试报名都已关联固定账户报名号。</p>'}${candidates.length > 8 ? `<em>另有 ${candidates.length - 8} 条</em>` : ''}</div></section>`;
|
||||
}
|
||||
|
||||
function adminFlowDesign(workflows) {
|
||||
@@ -414,7 +438,7 @@ document.addEventListener('click', async event => {
|
||||
}
|
||||
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: ['13800138000', 'Candidate123!'] };
|
||||
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;
|
||||
}
|
||||
@@ -426,6 +450,7 @@ document.addEventListener('click', async event => {
|
||||
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') {
|
||||
@@ -453,7 +478,7 @@ document.addEventListener('click', async event => {
|
||||
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();
|
||||
toast('账户报名号已同步', '考试记录已关联考生的固定号码'); return renderRoute();
|
||||
}
|
||||
if (action === 'generate-admit') {
|
||||
const registration = state.pageData.registrations.find(item => item.id === target.dataset.id);
|
||||
@@ -527,13 +552,18 @@ document.addEventListener('submit', async event => {
|
||||
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}/dashboard`);
|
||||
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') {
|
||||
await api('/api/auth/register', { method: 'POST', body: formObject(form) });
|
||||
toast('注册成功', '请登录后继续完善资料'); navigate('login');
|
||||
const data = await api('/api/auth/register', { method: 'POST', body: formObject(form) });
|
||||
setModal(`<div class="modal-head"><div><span>CANDIDATE NUMBER</span><h2>请保存你的报名号</h2><p>该号码就是长期使用的考生账户。</p></div><button data-action="close-modal">×</button></div><div class="issued-number"><span>固定报名号</span><strong>${h(data.registrationNumber)}</strong><p>以后报名不同考试仍使用这个号码。关闭窗口前请抄写或截图保存。</p></div><div class="modal-foot"><button class="solid-button" data-route="login">前往登录</button></div>`);
|
||||
} 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; toast('资料已提交', '管理员审核后会更新状态'); renderRoute();
|
||||
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('请至少选择一个报考科目');
|
||||
@@ -551,6 +581,13 @@ document.addEventListener('submit', async event => {
|
||||
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);
|
||||
@@ -576,10 +613,10 @@ document.addEventListener('submit', async event => {
|
||||
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();
|
||||
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();
|
||||
toast('报名号同步完成', `已为 ${result.count} 条考试报名关联固定账户号码`); renderRoute();
|
||||
} else if (kind === 'workflow-design') {
|
||||
const names = [...form.querySelectorAll('[name="stepName"]')];
|
||||
const levels = [...form.querySelectorAll('[name="stepLevel"]')];
|
||||
@@ -627,6 +664,11 @@ function openAdminForm() {
|
||||
setModal(`<div class="modal-head"><div><span>ADMIN SCOPE</span><h2>添加分级管理员</h2><p>同一级可以创建多个账号;校级和班级管理员必须绑定数据范围。</p></div><button data-action="close-modal">×</button></div><form class="modal-form" data-form="admin-form"><div class="field-row"><label><span>姓名 *</span><input name="displayName" required></label><label><span>管理员层级 *</span><select name="adminLevel" data-action="admin-level"><option value="school">校级管理员</option><option value="class">班级管理员</option><option value="super">超级管理员</option></select></label></div><div class="field-row"><label><span>登录账号 *</span><input name="username" required></label><label><span>初始密码 *</span><input name="password" type="password" minlength="8" required></label></div><label data-admin-school><span>绑定学校</span><select name="schoolId" data-action="school-select"><option value="">请选择学校</option>${schools.map(item => `<option value="${h(item.id)}">${h(item.name)}</option>`).join('')}</select></label><label class="hidden" data-admin-class><span>绑定班级</span><select name="classId"><option value="">请先选择学校</option>${classes.map(item => `<option value="${h(item.id)}">${h(item.name)}</option>`).join('')}</select></label><div class="modal-foot"><button type="button" class="ghost-button" data-action="close-modal">取消</button><button type="submit" class="solid-button">创建管理员</button></div></form>`);
|
||||
}
|
||||
|
||||
function openCandidateAccountForm() {
|
||||
const { schools = [], classes = [] } = state.pageData;
|
||||
setModal(`<div class="modal-head"><div><span>ISSUE CANDIDATE NUMBER</span><h2>创建考生账户</h2><p>系统按当前报名号规则生成固定账户;考生首次登录必须修改初始密码。</p></div><button data-action="close-modal">×</button></div><form class="modal-form" data-form="candidate-account"><div class="field-row"><label><span>考生姓名 *</span><input name="name" required placeholder="与学籍一致"></label><label><span>性别 *</span><select name="gender" required><option value="">请选择</option><option>男</option><option>女</option></select></label></div>${state.user.adminLevel === 'super' ? `<label><span>所属学校 *</span><select name="schoolId" data-action="school-select" required><option value="">请选择学校</option>${schools.map(item => `<option value="${h(item.id)}">${h(item.name)}</option>`).join('')}</select></label>` : `<input type="hidden" name="schoolId" value="${h(state.user.schoolId)}">`}<label><span>班级 *</span><select name="classId" required><option value="">请选择班级</option>${state.user.adminLevel === 'school' ? classes.map(item => `<option value="${h(item.id)}">${h(item.name)}</option>`).join('') : ''}</select></label><label><span>初始密码 *</span><input name="initialPassword" type="password" minlength="8" required placeholder="至少 8 位,线下安全告知考生"></label><div class="account-issue-note"><strong>下发给考生</strong><span>生成的报名号 + 本次设置的初始密码</span></div><div class="modal-foot"><button type="button" class="ghost-button" data-action="close-modal">取消</button><button type="submit" class="solid-button">生成报名号并创建账户</button></div></form>`);
|
||||
}
|
||||
|
||||
function centerRoomEditor(room = {}) {
|
||||
return `<section class="center-room-editor"><input type="hidden" name="roomId" value="${h(room.id || '')}"><header><span>结构化考场</span><button type="button" data-action="remove-center-room">移除</button></header><div class="room-editor-grid"><label><span>考场代码 *</span><input name="roomCode" required value="${h(room.code || '')}" placeholder="如 001"></label><label><span>考场名称 *</span><input name="roomName" required value="${h(room.name || '')}" placeholder="如 第 001 考场"></label><label><span>楼栋 *</span><input name="roomBuilding" required value="${h(room.building || '')}" placeholder="如 教学楼 A"></label><label><span>楼层</span><input name="roomFloor" value="${h(room.floor || '')}" placeholder="如 2 层"></label><label><span>容量 *</span><input name="roomCapacity" type="number" min="1" required value="${h(room.capacity || 30)}"></label><label><span>座位起号 *</span><input name="roomSeatStart" type="number" min="1" required value="${h(room.seatStart || 1)}"></label><label><span>座位止号 *</span><input name="roomSeatEnd" type="number" min="1" required value="${h(room.seatEnd || 30)}"></label><label><span>考场类型</span><select name="roomType"><option value="standard" ${room.roomType === 'standard' ? 'selected' : ''}>标准考场</option><option value="computer" ${room.roomType === 'computer' ? 'selected' : ''}>机考考场</option><option value="accessible" ${room.roomType === 'accessible' ? 'selected' : ''}>无障碍考场</option><option value="spare" ${room.roomType === 'spare' ? 'selected' : ''}>备用考场</option></select></label><label><span>状态</span><select name="roomStatus"><option value="active" ${room.status !== 'inactive' ? 'selected' : ''}>启用</option><option value="inactive" ${room.status === 'inactive' ? 'selected' : ''}>停用</option></select></label><label class="room-notes"><span>考场备注</span><input name="roomNotes" value="${h(room.notes || '')}" placeholder="设备、无障碍设施或备用安排"></label></div></section>`;
|
||||
}
|
||||
@@ -653,13 +695,13 @@ function openFlowDetail(id) {
|
||||
function openCandidateReview(id) {
|
||||
const item = state.pageData.candidates.find(candidate => candidate.id === id);
|
||||
const canReview = state.user.adminLevel !== 'class' && item.status === 'pending';
|
||||
setModal(`<div class="modal-head"><div><span>CANDIDATE REVIEW</span><h2>${canReview ? '处理' : '查看'} ${h(item.name)} 的资料</h2><p>最后更新:${formatDate(item.updatedAt,true)}</p></div><button data-action="close-modal">×</button></div><div class="review-profile"><dl><div><dt>证件号码</dt><dd class="mono">${h(item.idNumberMasked)}</dd></div><div><dt>联系电话</dt><dd>${h(item.phone)}</dd></div><div><dt>就读学校</dt><dd>${h(item.school)}</dd></div><div><dt>年级班级</dt><dd>${h(item.grade)}</dd></div><div><dt>当前步骤</dt><dd>${h(item.workflow?.currentStepDetail?.name || '流程已结束')}</dd></div><div><dt>当前责任人</dt><dd>${h(item.workflow?.assignee?.displayName || '—')}</dd></div></dl></div>${canReview ? `<form class="modal-form" data-form="candidate-review"><input type="hidden" name="id" value="${h(item.id)}"><label><span>审核结论</span><select name="status"><option value="approved">通过当前步骤</option><option value="rejected">退回考生修改</option></select></label><label><span>审核意见</span><textarea name="reviewNote" rows="3" placeholder="填写核验说明或需要补充的资料">${h(item.reviewNote)}</textarea></label><div class="modal-foot"><button type="button" class="ghost-button" data-action="close-modal">取消</button><button type="submit" class="solid-button">确认处理</button></div></form>` : '<div class="modal-foot"><button class="solid-button" data-action="close-modal">关闭</button></div>'}`);
|
||||
setModal(`<div class="modal-head"><div><span>CANDIDATE REVIEW</span><h2>${canReview ? '处理' : '查看'} ${h(item.name)} 的资料</h2><p class="mono">报名号 ${h(item.candidateNumber)} · 更新于 ${formatDate(item.updatedAt,true)}</p></div><button data-action="close-modal">×</button></div><div class="review-profile"><dl><div><dt>证件号码</dt><dd class="mono">${h(item.idNumberMasked)}</dd></div><div><dt>性别 / 籍贯</dt><dd>${h(item.gender)} · ${h(item.nativePlace)}</dd></div><div><dt>联系电话</dt><dd>${h(item.phone)}</dd></div><div><dt>电子邮箱</dt><dd>${h(item.email)}</dd></div><div><dt>就读学校</dt><dd>${h(item.school)}</dd></div><div><dt>年级班级</dt><dd>${h(item.grade)}</dd></div><div><dt>家庭住址</dt><dd>${h(item.address)}</dd></div><div><dt>监护人</dt><dd>${h(item.guardianName || item.emergencyContact)} · ${h(item.guardianPhone || item.emergencyPhone)}</dd></div><div><dt>当前步骤</dt><dd>${h(item.workflow?.currentStepDetail?.name || '流程已结束')}</dd></div><div><dt>当前责任人</dt><dd>${h(item.workflow?.assignee?.displayName || '—')}</dd></div></dl></div>${canReview ? `<form class="modal-form" data-form="candidate-review"><input type="hidden" name="id" value="${h(item.id)}"><label><span>审核结论</span><select name="status"><option value="approved">通过当前步骤</option><option value="rejected">退回考生修改</option></select></label><label><span>审核意见</span><textarea name="reviewNote" rows="3" placeholder="填写核验说明或需要补充的资料">${h(item.reviewNote)}</textarea></label><div class="modal-foot"><button type="button" class="ghost-button" data-action="close-modal">取消</button><button type="submit" class="solid-button">确认处理</button></div></form>` : '<div class="modal-foot"><button class="solid-button" data-action="close-modal">关闭</button></div>'}`);
|
||||
}
|
||||
|
||||
function openRegistrationReview(id) {
|
||||
const reg = state.pageData.registrations.find(item => item.id === id);
|
||||
const canReview = state.user.adminLevel !== 'class' && reg.status === 'pending';
|
||||
setModal(`<div class="modal-head"><div><span>REGISTRATION REVIEW</span><h2>${canReview ? '处理' : '查看'}考试报名</h2><p>${h(reg.candidate?.name)} · ${h(reg.exam.name)}</p></div><button data-action="close-modal">×</button></div><div class="registration-review"><div><span>报考科目</span><p>${reg.subjects.map(subject => `<b>${h(subject.name)}</b>`).join('')}</p></div><dl><div><dt>报名号</dt><dd class="mono">${h(reg.registrationNumber || '审批通过后生成')}</dd></div><div><dt>当前步骤</dt><dd>${h(reg.workflow?.currentStepDetail?.name || '流程已结束')}</dd></div><div><dt>责任人</dt><dd>${h(reg.workflow?.assignee?.displayName || '—')}</dd></div><div><dt>缴费状态</dt><dd>${badge(reg.paymentStatus)}</dd></div></dl></div>${canReview ? `<form class="modal-form" data-form="registration-review"><input type="hidden" name="id" value="${h(reg.id)}"><label><span>审核结论</span><select name="status"><option value="approved">通过当前步骤</option><option value="rejected">退回报名</option></select></label><label><span>审核意见</span><textarea name="reviewNote" rows="3" placeholder="可填写审核说明">${h(reg.reviewNote || '')}</textarea></label><div class="modal-foot"><button type="button" class="ghost-button" data-action="close-modal">取消</button><button type="submit" class="solid-button">确认处理</button></div></form>` : `<div class="modal-foot">${state.user.adminLevel === 'super' && reg.status === 'approved' && !reg.registrationNumber ? `<button class="ghost-button" data-action="generate-registration-number" data-id="${h(reg.id)}">生成报名号</button>` : ''}<button class="solid-button" data-action="close-modal">关闭</button></div>`}`);
|
||||
setModal(`<div class="modal-head"><div><span>REGISTRATION REVIEW</span><h2>${canReview ? '处理' : '查看'}考试报名</h2><p>${h(reg.candidate?.name)} · ${h(reg.exam.name)}</p></div><button data-action="close-modal">×</button></div><div class="registration-review"><div><span>报考科目</span><p>${reg.subjects.map(subject => `<b>${h(subject.name)}</b>`).join('')}</p></div><dl><div><dt>账户报名号</dt><dd class="mono">${h(reg.registrationNumber || '待同步账户号码')}</dd></div><div><dt>当前步骤</dt><dd>${h(reg.workflow?.currentStepDetail?.name || '流程已结束')}</dd></div><div><dt>责任人</dt><dd>${h(reg.workflow?.assignee?.displayName || '—')}</dd></div><div><dt>缴费状态</dt><dd>${badge(reg.paymentStatus)}</dd></div></dl></div>${canReview ? `<form class="modal-form" data-form="registration-review"><input type="hidden" name="id" value="${h(reg.id)}"><label><span>审核结论</span><select name="status"><option value="approved">通过当前步骤</option><option value="rejected">退回报名</option></select></label><label><span>审核意见</span><textarea name="reviewNote" rows="3" placeholder="可填写审核说明">${h(reg.reviewNote || '')}</textarea></label><div class="modal-foot"><button type="button" class="ghost-button" data-action="close-modal">取消</button><button type="submit" class="solid-button">确认处理</button></div></form>` : `<div class="modal-foot">${state.user.adminLevel === 'super' && reg.status === 'approved' && !reg.registrationNumber ? `<button class="ghost-button" data-action="generate-registration-number" data-id="${h(reg.id)}">同步账户报名号</button>` : ''}<button class="solid-button" data-action="close-modal">关闭</button></div>`}`);
|
||||
}
|
||||
|
||||
function openNoticeForm() {
|
||||
|
||||
Reference in New Issue
Block a user