Add structured admission plans and school role management

This commit is contained in:
2026-07-21 14:04:19 +08:00 Unverified
parent 1b6e860013
commit 5e0bee60e2
23 changed files with 483 additions and 82 deletions
+14 -4
View File
@@ -1,4 +1,5 @@
import { mountRegionSelects } from './region-select.mjs';
import { resolveProfileSpecialty, specialtyCatalog, specialtyLabel } from '../data/specialty-types.mjs';
export function createCandidateViews(context) {
const {
@@ -58,7 +59,9 @@ export function createCandidateViews(context) {
function mountAdmissionProfileFields(profile = {}) {
const actions = app.querySelector('.profile-form .form-actions');
if (!actions || app.querySelector('[data-admission-profile-fields]')) return;
actions.insertAdjacentHTML('beforebegin', `<div class="form-section-title" data-admission-profile-fields><span>04</span><div><h2>中考招生资格</h2><p>用于普通生、各类特长生和政策性计划资格校验;多个特长类型用逗号分隔。</p></div></div><div class="form-grid"><label><span>特长生类型</span><input name="specialtyTypes" value="${h((profile.specialtyTypes || []).join('、'))}" placeholder="例如:田径、声乐"></label><label><span>特长证明编号</span><input name="specialtyCertificate" value="${h(profile.specialtyCertificate || '')}" placeholder="证书或材料编号"></label><label class="wide-field"><span>政策资格说明</span><input name="policyEligibility" value="${h(profile.policyEligibility || '')}" placeholder="例如:指标生资格已核验"></label></div>`);
const qualification = resolveProfileSpecialty(profile);
const selectedCategory = specialtyCatalog.find(item => item.code === qualification.category);
actions.insertAdjacentHTML('beforebegin', `<div class="form-section-title" data-admission-profile-fields><span>04</span><div><h2>中考招生资格</h2><p>特长资格按大类和小类登记,填志愿时系统只显示与本人资格相符的招生类别。</p></div></div><div class="form-grid specialty-qualification-grid"><label><span>特长生大类</span><select name="specialtyCategory" data-action="specialty-category"><option value="">无特长生资格</option>${specialtyCatalog.map(item => `<option value="${h(item.code)}" ${item.code === qualification.category ? 'selected' : ''}>${h(item.name)}</option>`).join('')}</select></label><label><span>特长生小类</span><select name="specialtyType" data-specialty-type ${selectedCategory ? '' : 'disabled'}><option value="">${selectedCategory ? '请选择小类' : '请先选择大类'}</option>${(selectedCategory?.types || []).map(item => `<option value="${h(item.code)}" ${item.code === qualification.type ? 'selected' : ''}>${h(item.name)}</option>`).join('')}</select></label><label><span>特长证明编号</span><input name="specialtyCertificate" value="${h(profile.specialtyCertificate || '')}" placeholder="证书或统一测试材料编号"></label><label class="wide-field"><span>政策资格说明</span><input name="policyEligibility" value="${h(profile.policyEligibility || '')}" placeholder="例如:指标生资格已核验"></label></div>`);
}
function onboardingShell(stage, content) {
@@ -185,7 +188,7 @@ export function createCandidateViews(context) {
const lineState = item.qualified == null ? 'neutral' : item.qualified ? 'qualified' : 'unqualified';
return `<article class="${lineState}"><div class="score-subject-head"><span>${h(item.subjectName)}</span><i>${item.qualified == null ? '不判定单科' : item.qualified ? '单科达线' : '单科未达线'}</i></div><strong>${h(item.score)}<small> / ${h(item.fullScore)}</small></strong><em>${h(item.grade)} · 第 ${h(item.rank)} / ${h(item.cohortSize)} 名 · 前 ${h(item.rankPercent)}%</em><div class="rank-rule-line"><span>本科排名</span><b>${h(item.passText || '不设单科线')}</b></div>${appealPanel}</article>`;
}).join('');
const panel = `<section class="panel result-panel ${items[0].archivedAt ? 'archived' : ''}"><header><div><span>${h(items[0].examCode)}</span><h2>${h(examName)}</h2></div><small>${items[0].archivedAt ? `${formatDate(items[0].archivedAt, true)} ` : ` ${formatDate([...items].sort((a,b) => new Date(b.publishedAt) - new Date(a.publishedAt))[0].publishedAt, true)}`}</small></header><div class="result-summary ${summary?.qualified === true ? 'qualified' : summary?.qualified === false ? 'unqualified' : ''}"><span><small></small><strong>${h(summary?.total ?? '')}<em> / ${h(summary?.fullScore ?? '')}</em></strong><i></i></span><span><small></small><strong>${h(stateText)}</strong><em>${h(detail)}</em></span><span><small></small><strong>${h(summary?.publishedSubjects ?? items.length)}<em> / ${h(summary?.subjectCount ?? items.length)} </em></strong><i>${summary?.complete ? '' : ''}</i></span></div><div class="score-grid">${scores}</div><footer><p>${items[0].archivedAt ? '' : ''}</p><strong> ${items.length} </strong></footer></section>`;
const panel = `<section class="panel result-panel ${items[0].archivedAt ? 'archived' : ''}"><header><div><span>${h(items[0].examCode)}</span><h2>${h(examName)}</h2></div><small>${items[0].archivedAt ? `${formatDate(items[0].archivedAt, true)} ` : ` ${formatDate([...items].sort((a,b) => new Date(b.publishedAt) - new Date(a.publishedAt))[0].publishedAt, true)}`}</small></header><div class="result-summary ${summary?.qualified === true ? 'qualified' : summary?.qualified === false ? 'unqualified' : ''}"><span><small></small><strong>${h(summary?.total ?? '')}<em> / ${h(summary?.fullScore ?? '')}</em></strong><i></i></span><span><small></small><strong>${h(summary?.featureScore ?? 0)}</strong><i></i></span><span><small></small><strong>${h(stateText)}</strong><em>${h(detail)}</em></span><span><small></small><strong>${h(summary?.publishedSubjects ?? items.length)}<em> / ${h(summary?.subjectCount ?? items.length)} </em></strong><i>${summary?.complete ? '' : ''}</i></span></div><div class="score-grid">${scores}</div><footer><p>${items[0].archivedAt ? '' : ''}</p><strong> ${items.length} </strong></footer></section>`;
return items[0].archivedAt ? `<details class="candidate-archive-fold result-archive-fold"><summary><span><strong>${h(examName)}</strong><small>${h(items[0].examCode)} · ${items.length} 科成绩 · 已永久锁定</small></span><b>历史成绩</b></summary>${panel}</details>` : panel;
}).join('')}</div>`;
}
@@ -196,11 +199,18 @@ export function createCandidateViews(context) {
return `${data.notifications?.length ? `<section class="panel admission-notification"><strong>${h(data.notifications[0].payload.title)}</strong><p>${h(data.notifications[0].payload.message)}</p><small>${formatDate(data.notifications[0].createdAt, true)}</small></section>` : ''}<div class="admission-candidate-list">${data.admissions.map(item => {
const choices = item.preference?.payload?.choices || [];
const canFill = ['filling', 'supplementary'].includes(item.status) && item.totalScore != null;
const options = item.plans.flatMap(plan => plan.categories.filter(category => category.remaining > 0 || choices.some(choice => choice.schoolId === plan.schoolId && choice.categoryCode === category.code)).map(category => ({ value: `${plan.schoolId}|${category.code}`, label: `${plan.schoolName} · ${category.name}`, specialtyType: category.specialtyType, remaining: category.remaining })));
const placementSchool = item.plans.find(plan => plan.schoolId === item.placement?.schoolId)?.schoolName || '';
const progressSteps = ['filling', 'closed', 'school_review', 'completed'];
const progressIndex = item.status === 'supplementary' ? 1 : Math.max(0, progressSteps.indexOf(item.status));
return `<section class="panel admission-candidate-card"><header><div><span>${h(item.exam.code)} · ${h(item.payload.round || 1)} </span><h2>${h(item.exam.name)}</h2></div>${badge(item.status)}</header><div class="admission-progress-track">${['','','',''].map((label, index) => `<div class="${index < progressIndex ? 'done' : index === progressIndex ? 'current' : ''}"><i>${index < progressIndex ? '' : index + 1}</i><span>${label}</span></div>`).join('')}</div><div class="admission-score-strip"><span></span><strong>${item.totalScore == null ? '' : `${h(item.totalScore)} `}</strong><em>${h(phaseLabels[item.status] || item.status)}</em></div><p class="admission-progress-copy">${h(item.payload.progress || '')}</p>${item.placement ? `<div class="admission-result-banner ${h(item.placement.status)}"><span></span><strong>${h(placementSchool)} · ${h(item.placement.payload.categoryName)}</strong><small>${item.placement.status === 'final' ? '' : item.placement.status === 'withdrawal_pending' ? '退' : ''}</small></div>` : ''}${canFill ? `<form class="preference-form" data-form="volunteer-preference"><input type="hidden" name="examId" value="${h(item.examId)}"><div class="preference-form-head"><div><strong></strong><small></small></div><span> ${h(item.payload.maxChoices)} </span></div><div class="preference-choice-list">${Array.from({ length: Number(item.payload.maxChoices || 5) }, (_, index) => { const selected = choices[index] ? `${choices[index].schoolId}|${choices[index].categoryCode}` : ''; return `<label><b>${index + 1}</b><select name="choices"><option value="">${index ? '' : ''}</option>${options.map(option => `<option value="${h(option.value)}" ${option.value === selected ? 'selected' : ''}>${h(option.label)}${option.specialtyType ? ` ${h(option.specialtyType)}` : ''} · ${h(option.remaining)}</option>`).join('')}</select></label>`; }).join('')}</div><button class="solid-button" type="submit"></button></form>` : choices.length ? `<div class="locked-preferences"><strong></strong>${choices.map((choice, index) => { const option = options.find(entry => entry.value === `${choice.schoolId}|${choice.categoryCode}`); return `<span><b>${index + 1}</b>${h(option?.label || `${choice.schoolId} · ${choice.categoryCode}`)}</span>`; }).join('')}</div>` : '<div class="read-only-callout"></div>'}</section>`;
const choiceRows = Array.from({ length: Number(item.payload.maxChoices || 5) }, (_, index) => {
const choice = choices[index] || {};
const plan = item.plans.find(entry => entry.schoolId === choice.schoolId);
const categoryOptions = (plan?.categories || []).filter(category => category.remaining > 0 || category.code === choice.categoryCode);
return `<div class="preference-choice-row"><b>${index + 1}</b><label><span>招生学校代码 / 学校</span><select name="choiceSchool" data-action="preference-school" data-exam-id="${h(item.examId)}"><option value="">${index ? '' : ''}</option>${item.plans.map(entry => `<option value="${h(entry.schoolId)}" ${entry.schoolId === choice.schoolId ? 'selected' : ''}>${h(entry.schoolCode)} · ${h(entry.schoolName)}</option>`).join('')}</select></label><label><span></span><select name="choiceCategory" ${plan ? '' : 'disabled'}><option value="">${plan ? '' : ''}</option>${categoryOptions.map(category => `<option value="${h(category.code)}" ${category.code === choice.categoryCode ? 'selected' : ''}>${h(category.name)}${category.specialtyCategory ? `${h(specialtyLabel(category.specialtyCategory, category.specialtyType))}` : ''} · ${h(category.remaining)}</option>`).join('')}</select></label></div>`;
}).join('');
const lockedRows = choices.map((choice, index) => { const plan = item.plans.find(entry => entry.schoolId === choice.schoolId); const category = plan?.categories.find(entry => entry.code === choice.categoryCode); return `<span><b>${index + 1}</b>${h(plan ? `${plan.schoolCode} · ${plan.schoolName} · ${category?.name || choice.categoryCode}` : `${choice.schoolId} · ${choice.categoryCode}`)}</span>`; }).join('');
const qualification = specialtyLabel(item.specialtyQualification?.category, item.specialtyQualification?.type) || '普通生';
return `<section class="panel admission-candidate-card"><header><div><span>${h(item.exam.code)} · 第 ${h(item.payload.round || 1)} 轮</span><h2>${h(item.exam.name)}</h2></div>${badge(item.status)}</header><div class="admission-progress-track">${['填报志愿','志愿锁定','投档审核','录取结束'].map((label, index) => `<div class="${index < progressIndex ? 'done' : index === progressIndex ? 'current' : ''}"><i>${index < progressIndex ? '✓' : index + 1}</i><span>${label}</span></div>`).join('')}</div><div class="admission-score-strip"><span>本场总成绩</span><strong>${item.totalScore == null ? '成绩尚未完整发布' : `${h(item.totalScore)}`}</strong><span>特征分 <b>${h(item.featureScore || 0)}</b></span><span>资格 <b>${h(qualification)}</b></span><em>${h(phaseLabels[item.status] || item.status)}</em></div><p class="admission-progress-copy">${h(item.payload.progress || '等待录取工作更新')}</p>${item.placement ? `<div class="admission-result-banner ${h(item.placement.status)}"><span>当前结果</span><strong>${h(placementSchool)} · ${h(item.placement.payload.categoryName)}</strong><small>${item.placement.status === 'final' ? '已正式录取,通知已发送' : item.placement.status === 'withdrawal_pending' ? '招生学校申请退档,等待超级管理员审核' : '材料已发送招生学校审核'}</small></div>` : ''}${canFill ? `<form class="preference-form" data-form="volunteer-preference"><input type="hidden" name="examId" value="${h(item.examId)}"><div class="preference-form-head"><div><strong>按学校代码填写志愿</strong><small>先匹配招生学校,再选择该校对本人开放的招生类别;仅你本人可保存和修改。</small></div><span>最多 ${h(item.payload.maxChoices)} 个</span></div><div class="preference-choice-list">${choiceRows}</div><button class="solid-button" type="submit">保存本人志愿</button></form>` : choices.length ? `<div class="locked-preferences"><strong>已锁定志愿顺序</strong>${lockedRows}</div>` : '<div class="read-only-callout">当前不能填报:请等待成绩完整发布或志愿填报窗口开放。</div>'}</section>`;
}).join('')}</div>`;
}