This commit is contained in:
2026-07-21 15:07:19 +08:00 Unverified
parent 6be1ff227a
commit 0b423e565b
8 changed files with 222 additions and 36 deletions
+7 -2
View File
@@ -177,7 +177,12 @@ export function createAdminViews(context) {
function adminIndicatorQualifications(data) {
if (!data.exams?.length) return emptyState('暂无需要确认的考试', '超级管理员启用中考志愿填报后,本校资格名单会出现在这里。');
return `<section class="qualification-ledger-intro"><div><span>SOURCE SCHOOL CERTIFICATION</span><h2>${h(data.school?.name)}资格确认簿</h2><p>确认对象为本校在册且个人资料已完善的考生。每场考试全部确认后立即自动公示,后续修改也会同步更新公示。</p></div></section>${data.exams.map(item => { const status = item.qualificationStatus; return `<section class="panel qualification-ledger"><header><div><span>${h(item.exam.code)}</span><h2>${h(item.exam.name)}</h2></div><strong class="${status.complete ? 'complete' : ''}">${h(status.confirmed)} / ${h(status.total)} 已确认</strong></header><div class="qualification-progress"><i style="width:${status.total ? Math.round(status.confirmed / status.total * 100) : 0}%"></i></div>${status.complete ? '<div class="qualification-publication-state">✓ 本校资格已全部确认,公开公示已自动发布</div>' : '<div class="qualification-publication-state pending">未全部确认前不会公开,请逐项核对。</div>'}<div class="table-scroll"><table><thead><tr><th>报名号 / 姓名</th><th>特长类型</th><th>指标分配资格</th><th>确认时间</th><th>保存</th></tr></thead><tbody>${status.rows.map(row => `<tr data-exam-id="${h(item.examId)}" data-user-id="${h(row.userId)}"><td><strong>${h(row.name)}</strong><small class="mono">${h(row.registrationNumber)}</small></td><td>${h(row.specialtyLabel)}</td><td><select data-indicator-eligible><option value="" ${row.confirmed ? '' : 'selected'}>请选择</option><option value="true" ${row.confirmed && row.eligible ? 'selected' : ''}>有指标分配资格</option><option value="false" ${row.confirmed && !row.eligible ? 'selected' : ''}>无指标分配资格</option></select></td><td>${row.confirmedAt ? formatDate(row.confirmedAt, true) : '待确认'}</td><td><button class="row-action primary" data-action="save-indicator-qualification">确认</button></td></tr>`).join('') || '<tr><td colspan="5" class="empty-state">本校暂无资料已完善的在册考生</td></tr>'}</tbody></table></div></section>`; }).join('')}`;
return `<section class="qualification-ledger-intro"><div><span>SOURCE SCHOOL CERTIFICATION</span><h2>${h(data.school?.name)}资格确认簿</h2><p>逐人确认或多选批量设置。每场考试全部确认后立即自动公示,后续修改也会同步更新公示。</p></div></section>${data.exams.map(item => {
const status = item.qualificationStatus;
const bulk = `<div class="qualification-bulk-toolbar" data-qualification-bulk data-exam-id="${h(item.examId)}"><label><input type="checkbox" data-action="qualification-select-all"><span>全选本页 ${h(status.total)} 名考生</span></label><div><strong data-qualification-selected-count>已选 0 人</strong><select data-bulk-eligible><option value="">批量设置资格</option><option value="true">设为有指标分配资格</option><option value="false">设为无指标分配资格</option></select><button class="solid-button" data-action="bulk-indicator-qualification">应用到所选考生</button></div></div>`;
const rows = status.rows.map(row => `<tr data-exam-id="${h(item.examId)}" data-user-id="${h(row.userId)}"><td><input type="checkbox" data-qualification-select aria-label="选择 ${h(row.name)}"></td><td><strong>${h(row.name)}</strong><small class="mono">${h(row.registrationNumber)}</small></td><td>${h(row.specialtyLabel)}</td><td><select data-indicator-eligible><option value="" ${row.confirmed ? '' : 'selected'}>请选择</option><option value="true" ${row.confirmed && row.eligible ? 'selected' : ''}>有指标分配资格</option><option value="false" ${row.confirmed && !row.eligible ? 'selected' : ''}>无指标分配资格</option></select></td><td>${row.confirmedAt ? formatDate(row.confirmedAt, true) : '待确认'}</td><td><button class="row-action primary" data-action="save-indicator-qualification">确认</button></td></tr>`).join('');
return `<section class="panel qualification-ledger"><header><div><span>${h(item.exam.code)}</span><h2>${h(item.exam.name)}</h2></div><strong class="${status.complete ? 'complete' : ''}">${h(status.confirmed)} / ${h(status.total)} 已确认</strong></header><div class="qualification-progress"><i style="width:${status.total ? Math.round(status.confirmed / status.total * 100) : 0}%"></i></div>${status.complete ? '<div class="qualification-publication-state">✓ 本校资格已全部确认,公开公示已自动发布</div>' : '<div class="qualification-publication-state pending">未全部确认前不会公开,请逐项核对。</div>'}${bulk}<div class="table-scroll"><table><thead><tr><th>选择</th><th>报名号 / 姓名</th><th>特长类型</th><th>指标分配资格</th><th>确认时间</th><th>保存</th></tr></thead><tbody>${rows || '<tr><td colspan="6" class="empty-state">本校暂无资料已完善的在册考生</td></tr>'}</tbody></table></div></section>`;
}).join('')}`;
}
function adminNotices(notices) {
@@ -232,7 +237,7 @@ export function createAdminViews(context) {
}
function adminUsers(data) {
return `<section class="panel registration-policy"><div><span>SELF REGISTRATION</span><h2>考生自主注册</h2><p>${data.selfRegistrationEnabled ? '公开入口已开放,考生可以自主申请固定报名号。' : '当前由学校统一创建账户、下发报名号和初始密码。'}</p></div><form data-form="self-registration-setting"><input type="hidden" name="enabled" value="${data.selfRegistrationEnabled ? 'false' : 'true'}"><span class="policy-state ${data.selfRegistrationEnabled ? 'open' : ''}">${data.selfRegistrationEnabled ? '已开放' : '已关闭'}</span><button class="${data.selfRegistrationEnabled ? 'ghost-button' : 'solid-button'}" type="submit">${data.selfRegistrationEnabled ? '关闭自主注册' : '开启自主注册'}</button></form></section><section class="panel data-panel"><div class="data-toolbar"><p>管理员层级决定可见范围和可执行操作;同一级可创建多名账号。</p></div><div class="table-scroll"><table><thead><tr><th>管理员</th><th>登录账号</th><th>层级</th><th>绑定范围</th><th>状态</th></tr></thead><tbody>${data.admins.map(item => `<tr><td><div class="person-cell"><span>${h(item.displayName.slice(0, 1))}</span><div><strong>${h(item.displayName)}</strong><small>${h(item.id)}</small></div></div></td><td class="mono">${h(item.username)}</td><td><span class="admin-level level-${h(item.adminLevel)}">${h(item.levelName)}</span></td><td><strong>${h(item.schoolName || '全局')}</strong><small>${h(item.className || '')}</small></td><td>${item.active ? badge('approved') : badge('closed')}</td></tr>`).join('')}</tbody></table></div></section>`;
return `<section class="panel registration-policy"><div><span>SELF REGISTRATION</span><h2>考生自主注册</h2><p>${data.selfRegistrationEnabled ? '公开入口已开放,考生可以自主申请固定报名号。' : '当前由学校统一创建账户、下发报名号和初始密码。'}</p></div><form data-form="self-registration-setting"><input type="hidden" name="enabled" value="${data.selfRegistrationEnabled ? 'false' : 'true'}"><span class="policy-state ${data.selfRegistrationEnabled ? 'open' : ''}">${data.selfRegistrationEnabled ? '已开放' : '已关闭'}</span><button class="${data.selfRegistrationEnabled ? 'ghost-button' : 'solid-button'}" type="submit">${data.selfRegistrationEnabled ? '关闭自主注册' : '开启自主注册'}</button></form></section><section class="panel data-panel admin-account-ledger"><div class="data-toolbar"><p>管理员账户不物理删除;停用会立即结束现有会话,并完整保留审批和审计记录。</p></div><div class="table-scroll"><table><thead><tr><th>管理员</th><th>登录账号</th><th>层级</th><th>绑定范围</th><th>状态</th><th>账户操作</th></tr></thead><tbody>${data.admins.map(item => { const self = item.id === state.user.id; return `<tr><td><div class="person-cell"><span>${h(item.displayName.slice(0, 1))}</span><div><strong>${h(item.displayName)}</strong><small>${h(item.id)}</small></div></div></td><td class="mono">${h(item.username)}</td><td><span class="admin-level level-${h(item.adminLevel)}">${h(item.levelName)}</span></td><td><strong>${h(item.schoolName || '全局')}</strong><small>${h(item.className || '')}</small></td><td>${item.active ? badge('approved') : badge('closed')}</td><td><div class="admin-account-actions"><button class="row-action" data-action="reset-admin-password" data-id="${h(item.id)}" ${self ? 'disabled title="当前账号请到账户安全修改密码"' : ''}>重置密码</button><button class="row-action ${item.active ? 'danger' : 'primary'}" data-action="toggle-admin-account" data-id="${h(item.id)}" data-active="${item.active ? 'false' : 'true'}" ${self ? 'disabled title="不能停用当前账号"' : ''}>${item.active ? '停用账户' : '重新启用'}</button></div></td></tr>`; }).join('')}</tbody></table></div></section>`;
}
function adminCenters(data) {
+3 -4
View File
@@ -28,10 +28,9 @@ export function createCandidateViews(context) {
const level = state.user?.adminLevel || 'super';
const core = [['dashboard', '工作台', 'home'], ['candidates', level === 'class' ? '本班考生' : '考生信息', 'users'], ['registrations', level === 'class' ? '报名状态' : '报名审核', 'check'], ['payments', level === 'class' ? '缴费确认' : '缴费名单', 'ticket'], ['results', level === 'super' ? '成绩发布' : '成绩查看', 'chart']];
const security = ['security', '账户安全', 'user'];
if (level === 'class') return [core[0], core[1], core[2], core[3], ['admit', '本班准考证', 'ticket'], ['flows', '流程中心', 'check'], core[4], security];
const operations = [['flows', '流程中心', 'check'], ['centers', '考场信息', 'exam']];
if (level === 'school') return [core[0], ['organization', '本校组织', 'users'], ['account-batches', '批量建号', 'ticket'], core[1], ['indicator-qualifications', '指标资格确认', 'check'], core[2], core[3], ['admit', '校内准考证', 'ticket'], ...operations, core[4], security];
return [core[0], ['schools', '学校管理', 'exam'], ['admins', '管理员', 'users'], core[1], core[2], core[3], ['flows', '流程监督', 'check'], ['flow-design', '流程设计', 'exam'], ['number-rules', '报名号规则', 'ticket'], ['centers', '考场信息', 'exam'], ['exams', '考试与科目', 'exam'], ['admissions', '招生录取', 'check'], ['notices', '通知发布', 'bell'], ['admit', '准考证编排', 'ticket'], core[4], security];
if (level === 'class') return [core[0], core[1], core[2], core[3], ['admit', '本班准考证', 'ticket'], core[4], ['flows', '流程中心', 'check'], security];
if (level === 'school') return [core[0], ['organization', '本校组织', 'users'], ['account-batches', '批量建号', 'ticket'], core[1], ['indicator-qualifications', '指标资格确认', 'check'], core[2], core[3], ['admit', '校内准考证', 'ticket'], core[4], ['centers', '考场信息', 'exam'], ['flows', '流程中心', 'check'], security];
return [core[0], ['schools', '学校管理', 'exam'], ['admins', '管理员', 'users'], core[1], ['exams', '考试与科目', 'exam'], core[2], core[3], ['admit', '准考证编排', 'ticket'], core[4], ['admissions', '招生录取', 'check'], ['notices', '通知发布', 'bell'], ['centers', '考场信息', 'exam'], ['flows', '流程监督', 'check'], ['flow-design', '流程设计', 'exam'], ['number-rules', '报名号规则', 'ticket'], security];
}
function portalShell(role, page, content, title, description) {
+36 -14
View File
@@ -20,7 +20,7 @@ export function createPublicViews(context) {
}
function publicHeader() {
return `<header class="public-header"><div class="public-nav">${brand()}<nav><a href="#home" data-route="home">首页</a><a href="#home-exams" data-action="scroll-to" data-target="home-exams">考试报名</a><a href="#home-notices" data-action="scroll-to" data-target="home-notices">通知公告</a><a href="#announcements" data-route="announcements">招生公示</a><a href="#service-flow" data-action="scroll-to" data-target="service-flow">办事指南</a></nav><div class="nav-actions">${state.user ? `<button class="text-button" data-route="${state.user.role}/dashboard">进入${state.user.role === 'admin' ? '管理后台' : state.user.role === 'admission_school' ? '招生学校' : '考生中心'}</button><button class="solid-button" data-action="logout">退出</button>` : `<button class="text-button" data-route="login">登录</button><button class="solid-button" data-route="register">考生注册</button>`}<button class="mobile-menu" data-action="toggle-public-nav" aria-label="打开导航">${icons.menu}</button></div></div></header>`;
return `<header class="public-header"><div class="public-nav">${brand()}<nav><a href="#home" data-route="home">首页</a><a href="#home-exams" data-action="scroll-to" data-target="home-exams">考试报名</a><a href="#notices" data-route="notices">通知公告</a><a href="#service-flow" data-action="scroll-to" data-target="service-flow">办事指南</a></nav><div class="nav-actions">${state.user ? `<button class="text-button" data-route="${state.user.role}/dashboard">进入${state.user.role === 'admin' ? '管理后台' : state.user.role === 'admission_school' ? '招生学校' : '考生中心'}</button><button class="solid-button" data-action="logout">退出</button>` : `<button class="text-button" data-route="login">登录</button><button class="solid-button" data-route="register">考生注册</button>`}<button class="mobile-menu" data-action="toggle-public-nav" aria-label="打开导航">${icons.menu}</button></div></div></header>`;
}
function renderHome() {
@@ -32,27 +32,49 @@ export function createPublicViews(context) {
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">${h(siteCopy.heroEyebrow || 'EXAMINATION SERVICE')}</p><h1>${h(siteCopy.heroTitle || '一个报名号,')}<br><em>${h(siteCopy.heroHighlight || '贯穿每一次考试。')}</em></h1><p class="hero-lead">${h(siteCopy.heroDescription || '')}</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>
<div class="hero-copy"><div class="notice-ticker"><span>最新</span><button data-route="notice/${h(topNotice?.id)}">${h(topNotice?.title || '欢迎使用衡准考试服务平台')}</button></div><p class="overline">${h(siteCopy.heroEyebrow || 'EXAMINATION SERVICE')}</p><h1>${h(siteCopy.heroTitle || '一个报名号,')}<br><em>${h(siteCopy.heroHighlight || '贯穿每一次考试。')}</em></h1><p class="hero-lead">${h(siteCopy.heroDescription || '')}</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 disclosure-entry"><div><p class="overline">ADMISSION DISCLOSURE</p><h2>招生录取公开卷宗</h2><p>指标分配资格、最终录取名单和录取分数线集中公开,所有重要身份信息均按规则脱敏。</p></div><button class="solid-button large" data-route="announcements">进入招生公示页 ${icons.arrow}</button></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-route="notice/${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><button class="notice-archive-link" data-route="notices">查看全部通知与录取公示 ${icons.arrow}</button></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>
</main><footer class="public-footer"><div>${brand()}<p>${[organization.name, organization.phone].filter(Boolean).map(h).join(' · ')}</p>${organization.address || organization.email ? `<p class="public-contact-detail">${[organization.address, organization.email].filter(Boolean).map(h).join(' · ')}</p>` : ''}</div><span>${h(siteCopy.footerNotice || '')}</span></footer>`;
}
function renderAnnouncements(data = state.publicAnnouncements) {
function noticeDocuments(data = state.publicAnnouncements) {
const ordinary = (state.publicData.notices || []).map(item => ({ ...item, documentId: item.id, documentType: 'notice', subtype: item.category || '通知公告', publishedAt: item.publishAt }));
const qualifications = (data.qualifications || []).map(item => ({ ...item, documentId: `qualification-${item.id}`, documentType: 'qualification', category: '录取公示', subtype: '指标资格', title: `${item.examName} · ${item.schoolName}指标分配资格公示`, summary: `本次公开 ${item.rows.length} 名考生的指标分配资格及特长类型。` }));
const admissions = (data.admissions || []).map(item => ({ ...item, documentId: `admission-${item.id}`, documentType: 'admission', category: '录取公示', subtype: '录取名单', title: `${item.examName}最终录取名单`, summary: `${item.rows.length} 名考生正式录取,公开报名号、姓名、总成绩和录取学校。` }));
const cutoffs = (data.cutoffs || []).map(item => ({ ...item, documentId: `cutoff-${item.id}`, documentType: 'cutoff', category: '录取公示', subtype: '录取分数线', title: `${item.examName}录取分数线`, summary: `按招生学校和招生类别公布 ${item.rows.length} 条最低录取分数线。` }));
return [...ordinary, ...qualifications, ...admissions, ...cutoffs].sort((left, right) => new Date(right.publishedAt) - new Date(left.publishedAt));
}
function renderDocumentBody(document) {
if (document.documentType === 'notice') return `<article class="notice-document-content">${document.contentHtml || `<p>${h(document.content || '').replace(/\r?\n/g, '</p><p>')}</p>`}</article>`;
if (document.documentType === 'qualification') return `<p class="document-lead">本公示由生源校完成全部考生资格确认后自动生成。</p><div class="table-scroll"><table><thead><tr><th>报名号</th><th>姓名</th><th>指标分配资格</th><th>特长类型</th></tr></thead><tbody>${document.rows.map(row => `<tr><td class="mono">${h(row.registrationNumber)}</td><td><strong>${h(row.name)}</strong></td><td><span class="qualification-result ${row.eligible ? 'eligible' : ''}">${row.eligible ? '有' : '无'}</span></td><td>${h(row.specialtyLabel || '普通生')}</td></tr>`).join('')}</tbody></table></div>`;
if (document.documentType === 'admission') return `<p class="document-lead">报名号、姓名、考生总成绩与录取学校公开透明;证件号和联系方式不在本页展示。</p><div class="table-scroll"><table><thead><tr><th>报名号</th><th>姓名</th><th>总成绩</th><th>录取学校</th><th>录取类别</th></tr></thead><tbody>${document.rows.map(row => `<tr><td class="mono">${h(row.registrationNumber)}</td><td><strong>${h(row.name)}</strong></td><td>${h(row.totalScore)}</td><td>${h(row.admittedSchool)}</td><td>${h(row.categoryName)}</td></tr>`).join('')}</tbody></table></div>`;
return `<p class="document-lead">录取分数线为对应学校、招生类别最终录取考生的最低总成绩。</p><div class="table-scroll"><table><thead><tr><th>招生学校</th><th>招生类别</th><th>计划数</th><th>录取数</th><th>最高分</th><th>录取分数线</th></tr></thead><tbody>${document.rows.map(row => `<tr><td><strong>${h(row.schoolName)}</strong></td><td>${h(row.categoryName)}</td><td>${h(row.planQuota)}</td><td>${h(row.admittedCount)}</td><td>${h(row.highestScore)}</td><td><strong class="cutoff-score">${h(row.cutoffScore)}</strong></td></tr>`).join('')}</tbody></table></div>`;
}
function renderNoticeCenter(data = state.publicAnnouncements, selectedId = '') {
app.classList.remove('admin-readable');
const documents = noticeDocuments(data);
const selected = documents.find(item => item.documentId === selectedId);
const organization = state.publicData.organization || {};
const sections = [data.qualifications?.length, data.admissions?.length, data.cutoffs?.length].filter(Boolean).length;
app.innerHTML = `${publicHeader()}<main class="public-main announcement-page"><section class="announcement-hero"><div><p class="overline">PUBLIC ADMISSION LEDGER</p><h1>招生录取<br><em>公开卷宗</em></h1><p>按考试留存资格确认、录取结果和分数线。报名号、姓名、总成绩与录取学校透明公开,证件及联系方式不在本页展示。</p></div><dl><div><dt>公开类别</dt><dd>${sections}</dd></div><div><dt>资格公示</dt><dd>${h(data.qualifications?.length || 0)}</dd></div><div><dt>录取公告</dt><dd>${h(data.admissions?.length || 0)}</dd></div></dl></section>
<nav class="announcement-index"><a href="#qualification-publications">01 指标资格</a><a href="#admission-publications">02 录取名单</a><a href="#cutoff-publications">03 录取分数线</a></nav>
<section class="content-section announcement-register" id="qualification-publications"><div class="section-heading"><div><p class="overline">QUALIFICATION REGISTER</p><h2>指标分配资格公示</h2></div><p>仅在生源校全部考生完成资格确认后自动发布。</p></div>${(data.qualifications || []).map(item => `<article class="panel announcement-sheet"><header><div><span>${formatDate(item.publishedAt, true)} · ${h(item.schoolName)}</span><h3>${h(item.examName)}</h3></div><strong>${h(item.rows.length)} 人</strong></header><div class="table-scroll"><table><thead><tr><th>报名号</th><th>姓名</th><th>指标分配资格</th><th>特长类型</th></tr></thead><tbody>${item.rows.map(row => `<tr><td class="mono">${h(row.registrationNumber)}</td><td><strong>${h(row.name)}</strong></td><td><span class="qualification-result ${row.eligible ? 'eligible' : ''}">${row.eligible ? '有' : '无'}</span></td><td>${h(row.specialtyLabel || '普通生')}</td></tr>`).join('')}</tbody></table></div></article>`).join('') || '<div class="empty-state">暂无已完成全校确认的资格公示</div>'}</section>
<section class="content-section announcement-register" id="admission-publications"><div class="section-heading"><div><p class="overline">ADMISSION RESULTS</p><h2>最终录取名单</h2></div><p>录取完成后自动公告,报名号、姓名、总成绩与录取学校公开透明。</p></div>${(data.admissions || []).map(item => `<article class="panel announcement-sheet"><header><div><span>${formatDate(item.publishedAt, true)}</span><h3>${h(item.examName)}</h3></div><strong>${h(item.rows.length)} 人录取</strong></header><div class="table-scroll"><table><thead><tr><th>报名号</th><th>姓名</th><th>总成绩</th><th>录取学校</th><th>录取类别</th></tr></thead><tbody>${item.rows.map(row => `<tr><td class="mono">${h(row.registrationNumber)}</td><td><strong>${h(row.name)}</strong></td><td>${h(row.totalScore)}</td><td>${h(row.admittedSchool)}</td><td>${h(row.categoryName)}</td></tr>`).join('')}</tbody></table></div></article>`).join('') || '<div class="empty-state">暂无已完成的录取公告</div>'}</section>
<section class="content-section announcement-register" id="cutoff-publications"><div class="section-heading"><div><p class="overline">ADMISSION CUTOFFS</p><h2>录取分数线统计</h2></div><p>分数线为对应学校、招生类别最终录取考生的最低总成绩。</p></div>${(data.cutoffs || []).map(item => `<article class="panel announcement-sheet cutoff-sheet"><header><div><span>${formatDate(item.publishedAt, true)}</span><h3>${h(item.examName)}</h3></div><strong>${h(item.rows.length)} 条分数线</strong></header><div class="table-scroll"><table><thead><tr><th>招生学校</th><th>招生类别</th><th>计划数</th><th>录取数</th><th>最高分</th><th>录取分数线</th></tr></thead><tbody>${item.rows.map(row => `<tr><td><strong>${h(row.schoolName)}</strong></td><td>${h(row.categoryName)}</td><td>${h(row.planQuota)}</td><td>${h(row.admittedCount)}</td><td>${h(row.highestScore)}</td><td><strong class="cutoff-score">${h(row.cutoffScore)}</strong></td></tr>`).join('')}</tbody></table></div></article>`).join('') || '<div class="empty-state">暂无已发布的录取分数线</div>'}</section>
</main><footer class="public-footer"><div>${brand()}<p>${[organization.name, organization.phone].filter(Boolean).map(h).join(' · ')}</p></div><span>招生公示数据由系统按业务状态自动生成</span></footer>`;
if (selected) {
app.innerHTML = `${publicHeader()}<main class="public-main notice-document-page"><div class="notice-breadcrumb"><button data-route="notices">通知公告</button><span>/</span><strong>${h(selected.subtype)}</strong></div><article class="notice-document"><header><span>${h(selected.category)} · ${h(selected.subtype)}</span><h1>${h(selected.title)}</h1><p>${formatDate(selected.publishedAt, true)}${selected.author ? ` · ${h(selected.author)}` : ''}</p></header><section>${renderDocumentBody(selected)}</section><footer><button class="ghost-button" data-route="notices">返回通知公告列表</button></footer></article></main><footer class="public-footer"><div>${brand()}<p>${[organization.name, organization.phone].filter(Boolean).map(h).join(' · ')}</p></div><span>公开信息以本页面正式发布内容为准</span></footer>`;
return;
}
const categories = ['全部', ...new Set(documents.map(item => item.category || '通知公告'))];
const category = categories.includes(state.noticeCategory) ? state.noticeCategory : '全部';
const filtered = category === '全部' ? documents : documents.filter(item => item.category === category);
const pageSize = 8;
const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize));
const page = Math.min(totalPages, Math.max(1, Number(state.noticePage || 1)));
state.noticeCategory = category; state.noticePage = page;
const pageRows = filtered.slice((page - 1) * pageSize, page * pageSize);
app.innerHTML = `${publicHeader()}<main class="public-main notice-center-page"><section class="notice-center-hero"><div><p class="overline">PUBLIC NOTICE ARCHIVE</p><h1>通知公告</h1><p>考试通知、成绩发布与招生录取公示统一归档,按发布时间倒序公开。</p></div><strong>${h(documents.length)}<small>份公开文件</small></strong></section><section class="notice-center-shell"><nav class="notice-category-nav">${categories.map(item => `<button class="${item === category ? 'active' : ''}" data-action="notice-category" data-category="${h(item)}">${h(item)}<span>${item === '全部' ? documents.length : documents.filter(document => document.category === item).length}</span></button>`).join('')}</nav><div class="notice-directory"><header><div><strong>${h(category)}</strong><span>第 ${h(page)} / ${h(totalPages)} 页</span></div><small>共 ${h(filtered.length)} 条</small></header><div class="notice-directory-list">${pageRows.map(item => `<button data-route="notice/${h(item.documentId)}"><time><strong>${String(new Date(item.publishedAt).getDate()).padStart(2,'0')}</strong><span>${new Date(item.publishedAt).toLocaleDateString('zh-CN',{year:'numeric',month:'2-digit'}).replace('/','.')}</span></time><span class="notice-directory-copy"><em>${h(item.subtype)}</em><strong>${h(item.title)}</strong><small>${h(item.summary || '')}</small></span><span class="notice-directory-arrow">${icons.arrow}</span></button>`).join('') || '<div class="empty-state">当前分类暂无公开信息</div>'}</div><footer class="notice-pagination"><button data-action="notice-page" data-page="${page - 1}" ${page <= 1 ? 'disabled' : ''}>上一页</button>${Array.from({length:totalPages},(_,index) => index + 1).map(value => `<button class="${value === page ? 'active' : ''}" data-action="notice-page" data-page="${value}">${value}</button>`).join('')}<button data-action="notice-page" data-page="${page + 1}" ${page >= totalPages ? 'disabled' : ''}></button></footer></div></section></main><footer class="public-footer"><div>${brand()}<p>${[organization.name, organization.phone].filter(Boolean).map(h).join(' · ')}</p></div><span></span></footer>`;
}
function renderHeroTicket(exam) {
@@ -61,7 +83,7 @@ export function createPublicViews(context) {
}
function renderNoticeRow(notice) {
return `<button class="notice-row" data-action="open-notice" data-id="${h(notice.id)}"><time>${formatDate(notice.publishAt)}</time><span><em>${h(notice.category)}</em><strong>${h(notice.title)}</strong><small>${h(notice.summary)}</small></span>${icons.arrow}</button>`;
return `<button class="notice-row" data-route="notice/${h(notice.id)}"><time>${formatDate(notice.publishAt)}</time><span><em>${h(notice.category)}</em><strong>${h(notice.title)}</strong><small>${h(notice.summary)}</small></span>${icons.arrow}</button>`;
}
function renderPublicExam(exam) {
@@ -84,5 +106,5 @@ export function createPublicViews(context) {
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>`;
}
return { brand, renderHome, renderAnnouncements, renderAuth };
return { brand, renderHome, renderNoticeCenter, renderAuth };
}
+2
View File
@@ -3,6 +3,8 @@ export const state = {
profile: null,
publicData: { organization: {}, notices: [], exams: [], stats: {} },
publicAnnouncements: { qualifications: [], admissions: [], cutoffs: [] },
noticeCategory: '全部',
noticePage: 1,
permissions: [],
scopeLabel: '',
pageData: null,
+53 -7
View File
@@ -151,6 +151,37 @@ export function createAdminRoutes(context) {
}).filter(item => item.exam);
return sendJson(response, 200, { ok: true, school, exams });
}
const qualificationBulkMatch = pathname.match(/^\/api\/admin\/indicator-qualifications\/([^/]+)\/bulk$/);
if (qualificationBulkMatch && request.method === 'PUT') {
if (user.adminLevel !== 'school' || !user.schoolId) return sendError(response, 403, '只有生源校学校管理员可以批量确认指标分配资格');
const setting = admissionSetting(db, qualificationBulkMatch[1]);
if (!setting?.payload?.enabled) return sendError(response, 404, '该考试未启用志愿填报');
const body = await readJson(request);
if (typeof body.eligible !== 'boolean') return sendError(response, 400, '请选择批量设置为有资格或无资格');
const userIds = [...new Set((Array.isArray(body.userIds) ? body.userIds : []).map(value => cleanText(value, 64)).filter(Boolean))];
if (!userIds.length) return sendError(response, 400, '请至少选择一名考生');
const profiles = userIds.map(userId => db.candidateProfiles.find(item => item.userId === userId && item.schoolId === user.schoolId && item.profileCompleted));
if (profiles.some(item => !item) || profiles.some(profile => !db.users.some(item => item.id === profile.userId && item.role === 'candidate' && item.active))) return sendError(response, 403, '批量名单中包含不属于本校的有效考生');
const now = nowIso();
const existing = new Map(admissionRecords(db, 'indicator_qualification', setting.examId).map(item => [item.userId, item]));
const qualifications = profiles.map(profile => {
const qualification = existing.get(profile.userId) || { id: uid('indicator_qualification'), kind: 'indicator_qualification', examId: setting.examId, userId: profile.userId, schoolId: user.schoolId, createdAt: now };
Object.assign(qualification, { status: 'confirmed', updatedAt: now, payload: { eligible: body.eligible, confirmedBy: user.displayName, confirmedAt: now } });
return qualification;
});
const replacementIds = new Set(qualifications.map(item => item.id));
const nextDb = { ...db, admissionRecords: [...db.admissionRecords.filter(item => !replacementIds.has(item.id)), ...qualifications] };
const status = sourceSchoolQualificationStatus(nextDb, setting.examId, user.schoolId);
const records = [...qualifications];
if (status.complete) {
const published = admissionRecords(nextDb, 'qualification_publication', setting.examId).find(item => item.schoolId === user.schoolId);
const publication = published || { id: uid('qualification_publication'), kind: 'qualification_publication', examId: setting.examId, userId: user.id, schoolId: user.schoolId, createdAt: now };
Object.assign(publication, { status: 'published', updatedAt: now, payload: { publishedAt: now, rows: status.rows } });
records.push(publication);
}
await database.saveAdmissionRecords(records, logAction(db, user, '批量确认指标分配资格', `${qualifications.length} 人 · ${body.eligible ? '有资格' : '无资格'}${status.complete ? ' · 全校已自动公示' : ''}`));
return sendJson(response, 200, { ok: true, count: qualifications.length, qualificationStatus: status, published: status.complete });
}
const qualificationMatch = pathname.match(/^\/api\/admin\/indicator-qualifications\/([^/]+)\/([^/]+)$/);
if (qualificationMatch && request.method === 'PUT') {
if (user.adminLevel !== 'school' || !user.schoolId) return sendError(response, 403, '只有生源校学校管理员可以确认指标分配资格');
@@ -451,19 +482,34 @@ export function createAdminRoutes(context) {
}
const adminMatch = pathname.match(/^\/api\/admin\/admins\/([^/]+)$/);
if (adminMatch && request.method === 'PATCH') {
if (user.adminLevel !== 'school') return sendError(response, 403, '只有校级管理员可以维护本校班级管理员');
if (!['super', 'school'].includes(user.adminLevel)) return sendError(response, 403, '当前账号不能维护管理员');
const body = await readJson(request);
const target = db.users.find(item => item.id === adminMatch[1] && item.role === 'admin' && item.adminLevel === 'class' && item.schoolId === user.schoolId);
if (!target) return sendError(response, 404, '班级管理员不存在');
const schoolClass = db.classes.find(item => item.id === cleanText(body.classId || target.classId, 64) && item.schoolId === user.schoolId);
if (!schoolClass) return sendError(response, 400, '请选择本校有效班级');
const target = db.users.find(item => item.id === adminMatch[1] && item.role === 'admin' && (user.adminLevel === 'super' || (item.adminLevel === 'class' && item.schoolId === user.schoolId)));
if (!target) return sendError(response, 404, '管理员账户不存在或不在当前管理范围');
if (target.id === user.id && body.active === false) return sendError(response, 409, '不能停用当前正在使用的管理员账户');
const schoolClass = target.adminLevel === 'class' ? db.classes.find(item => item.id === cleanText(body.classId || target.classId, 64) && item.schoolId === target.schoolId) : null;
if (target.adminLevel === 'class' && !schoolClass) return sendError(response, 400, '请选择该管理员所属学校的有效班级');
const password = String(body.password || '');
if (password && password.length < 8) return sendError(response, 400, '重置密码至少 8 位');
Object.assign(target, { displayName: cleanText(body.displayName || target.displayName, 50), classId: schoolClass.id, active: body.active == null ? target.active : Boolean(body.active) });
Object.assign(target, { displayName: cleanText(body.displayName || target.displayName, 50), classId: schoolClass?.id || target.classId || null, active: body.active == null ? target.active : Boolean(body.active) });
if (password) target.passwordHash = hashPassword(password);
await database.updateAdmin(target, Boolean(password), logAction(db, user, '维护班级管理员', `${target.displayName} · ${schoolClass.name}`));
await database.updateAdmin(target, Boolean(password), logAction(db, user, '维护管理员账户', `${target.displayName} · ${adminLevelNames[target.adminLevel]} · ${target.active ? '启用' : '停用'}`));
if (!target.active || password) for (const [token, session] of sessions) if (session.userId === target.id) sessions.delete(token);
return sendJson(response, 200, { ok: true, admin: safeUser(target) });
}
const adminPasswordResetMatch = pathname.match(/^\/api\/admin\/admins\/([^/]+)\/reset-password$/);
if (adminPasswordResetMatch && request.method === 'POST') {
if (!['super', 'school'].includes(user.adminLevel)) return sendError(response, 403, '当前账号不能重置管理员密码');
const target = db.users.find(item => item.id === adminPasswordResetMatch[1] && item.role === 'admin' && (user.adminLevel === 'super' || (item.adminLevel === 'class' && item.schoolId === user.schoolId)));
if (!target) return sendError(response, 404, '管理员账户不存在或不在当前管理范围');
if (target.id === user.id) return sendError(response, 409, '当前账号请在“账户安全”中修改自己的密码');
const temporaryPassword = `Reset-${randomBytes(7).toString('base64url')}`;
target.passwordHash = hashPassword(temporaryPassword);
target.active = true;
await database.updateAdmin(target, true, logAction(db, user, '重置管理员密码', `${target.displayName} · ${target.username}`));
for (const [token, session] of sessions) if (session.userId === target.id) sessions.delete(token);
return sendJson(response, 200, { ok: true, username: target.username, temporaryPassword });
}
if (pathname === '/api/admin/settings/self-registration' && request.method === 'PUT') {
if (!requirePermission(user, response, '*')) return true;
const body = await readJson(request);