防伪+成绩单+录取通知书
This commit is contained in:
@@ -8,12 +8,15 @@ import { state } from './src/client/state.mjs';
|
||||
import { badge, dateRange, formatDate, h, icons, money, passPolicyText, statusLabels } from './src/client/ui.mjs';
|
||||
import { formatRegionAddress, mountRegionSelects, updateRegionSelects } from './src/client/region-select.mjs';
|
||||
import { specialtyCatalog } from './src/data/specialty-types.mjs';
|
||||
import { getTableControl, setTableControl } from './src/client/table-state.mjs';
|
||||
import { downloadAdmissionNotice, downloadScoreReport } from './src/client/pdf-export.mjs';
|
||||
|
||||
const app = document.querySelector('#app');
|
||||
const modalRoot = document.querySelector('#modalRoot');
|
||||
let toastTimer;
|
||||
let noticeEditor;
|
||||
let ckeditorModulePromise;
|
||||
let tableSearchTimer;
|
||||
function toast(title, message = '') {
|
||||
const element = document.querySelector('#toast');
|
||||
element.querySelector('strong').textContent = title;
|
||||
@@ -83,7 +86,7 @@ function requireLogin() {
|
||||
}
|
||||
|
||||
const baseViewContext = { state, app, h, formatDate, dateRange, badge, money, passPolicyText, statusLabels, icons, api, renderError, requireLogin, emptyState };
|
||||
const { brand, renderHome, renderNoticeCenter, renderAuth } = createPublicViews(baseViewContext);
|
||||
const { brand, renderHome, renderNoticeCenter, renderAuth, renderVerification } = createPublicViews(baseViewContext);
|
||||
const { adminNavForUser, portalShell, loadingPanel, renderCandidate, accountSecurity } = createCandidateViews({ ...baseViewContext, brand });
|
||||
const { renderAdmin, workflowStepEditor } = createAdminViews({ ...baseViewContext, brand, portalShell, loadingPanel, adminNavForUser, accountSecurity });
|
||||
const { renderAdmission } = createAdmissionViews({ ...baseViewContext, brand });
|
||||
@@ -100,6 +103,13 @@ async function renderRoute() {
|
||||
const [section, page = 'dashboard'] = route.split('/');
|
||||
if (section !== 'login') state.authNotice = '';
|
||||
if (section === 'home') renderHome();
|
||||
else if (section === 'verify') {
|
||||
if (!page || page === 'dashboard') renderVerification();
|
||||
else {
|
||||
try { renderVerification(page, await api(`/api/public/verifications/${encodeURIComponent(page)}`)); }
|
||||
catch (error) { renderVerification(page, null, error.message); }
|
||||
}
|
||||
}
|
||||
else if (section === 'notices' || section === 'announcements') { state.publicAnnouncements = await api('/api/public/announcements'); renderNoticeCenter(state.publicAnnouncements); }
|
||||
else if (section === 'notice') { state.publicAnnouncements = await api('/api/public/announcements'); renderNoticeCenter(state.publicAnnouncements, page); }
|
||||
else if (section === 'login' || section === 'register') renderAuth(section);
|
||||
@@ -107,6 +117,13 @@ async function renderRoute() {
|
||||
else if (section === 'admin') await renderAdmin(page);
|
||||
else if (section === 'admission_school') await renderAdmission(page);
|
||||
else navigate('home');
|
||||
restoreTableControls();
|
||||
}
|
||||
|
||||
function restoreTableControls() {
|
||||
document.querySelectorAll('[data-action="table-search"][data-target]').forEach(input => { input.value = getTableControl(state, input.dataset.target).query || ''; });
|
||||
document.querySelectorAll('[data-action="status-filter"][data-target]').forEach(button => button.classList.toggle('active', (getTableControl(state, button.dataset.target).status || 'all') === button.dataset.status));
|
||||
document.querySelectorAll('[data-table-filter][data-target]').forEach(select => { select.value = getTableControl(state, select.dataset.target).filters?.[select.dataset.tableFilter] || ''; });
|
||||
}
|
||||
|
||||
function formObject(form) {
|
||||
@@ -323,6 +340,20 @@ document.addEventListener('click', async event => {
|
||||
return;
|
||||
}
|
||||
if (action === 'download-admit') { window.location.href = `/api/candidate/registrations/${target.dataset.id}/admit-card`; return; }
|
||||
if (action === 'download-score-report') {
|
||||
const examId = target.dataset.examId;
|
||||
const results = (state.pageData?.results || []).filter(item => item.examId === examId);
|
||||
const summary = (state.pageData?.summaries || []).find(item => item.examId === examId);
|
||||
if (!results.length || !summary?.verificationCode) return toast('成绩单暂不可下载', '请刷新页面后重试');
|
||||
downloadScoreReport({ organization: state.publicData.organization, candidate: state.pageData.candidate || { name: state.user.displayName, candidateNumber: state.user.candidateNumber }, exam: { id: examId, name: results[0].examName, code: results[0].examCode }, results, summary: { ...summary, publishedAt: [...results].sort((a,b) => new Date(b.publishedAt) - new Date(a.publishedAt))[0]?.publishedAt }, verificationCode: summary.verificationCode, verificationUrl: `${location.origin}/#verify/${summary.verificationCode}` });
|
||||
return toast('PDF 成绩单已生成', '文件包含防伪查询码');
|
||||
}
|
||||
if (action === 'download-admission-notice') {
|
||||
const item = (state.pageData?.admissions || []).find(entry => entry.examId === target.dataset.examId);
|
||||
if (!item?.placement || item.placement.status !== 'final' || !item.noticeVerificationCode) return toast('录取通知书暂不可下载', '只有正式录取后才能生成');
|
||||
downloadAdmissionNotice({ organization: state.publicData.organization, candidate: { name: state.profile?.name || state.user.displayName }, exam: item.exam, placement: item.placement, school: item.placementSchool || { name: item.placement.schoolName || '招生学校' }, template: item.noticeTemplate || {}, verificationCode: item.noticeVerificationCode, verificationUrl: `${location.origin}/#verify/${item.noticeVerificationCode}` });
|
||||
return toast('录取通知书已生成', '请核对学校和录取类别');
|
||||
}
|
||||
if (action === 'download-admitted-candidates') {
|
||||
const examId = target.closest('.admission-export-bar')?.querySelector('[name="exportExamId"]')?.value;
|
||||
if (!examId) return toast('请选择考试', '仅录取工作结束的考试可以下载');
|
||||
@@ -376,11 +407,11 @@ document.addEventListener('click', async event => {
|
||||
}
|
||||
if (action === 'clear-table-filters') {
|
||||
const tableId = target.dataset.target;
|
||||
state.tableFilters[tableId] = { query: '', status: 'all', filters: {} };
|
||||
document.querySelectorAll(`[data-table-filter][data-target="${tableId}"]`).forEach(select => { select.value = ''; });
|
||||
document.querySelectorAll(`[data-action="table-search"][data-target="${tableId}"]`).forEach(input => { input.value = ''; });
|
||||
document.querySelectorAll(`[data-action="status-filter"][data-target="${tableId}"]`).forEach((button, index) => button.classList.toggle('active', index === 0));
|
||||
applyTableFilters(tableId);
|
||||
return;
|
||||
return renderRoute();
|
||||
}
|
||||
if (action === 'bulk-indicator-qualification') {
|
||||
const toolbar = target.closest('[data-qualification-bulk]');
|
||||
@@ -686,15 +717,26 @@ document.addEventListener('click', async event => {
|
||||
toast(visible ? '系统公示已显示' : '系统公示已隐藏', '只调整公开目录展示,不修改公示内容'); return refreshPublic().then(renderRoute);
|
||||
}
|
||||
if (action === 'status-filter') {
|
||||
target.parentElement.querySelectorAll('button').forEach(button => button.classList.toggle('active', button === target));
|
||||
applyTableFilters(target.dataset.target);
|
||||
setTableControl(state, target.dataset.target, { status: target.dataset.status });
|
||||
return renderRoute();
|
||||
}
|
||||
} catch (error) { toast('操作未完成', error.message); }
|
||||
});
|
||||
|
||||
document.addEventListener('input', event => {
|
||||
if (event.target.matches('[data-action="table-search"]')) {
|
||||
setTableControl(state, event.target.dataset.target, { query: event.target.value });
|
||||
applyTableFilters(event.target.dataset.target);
|
||||
clearTimeout(tableSearchTimer);
|
||||
tableSearchTimer = setTimeout(() => renderRoute(), 260);
|
||||
}
|
||||
if (event.target.closest('[data-notice-template]')) {
|
||||
const studio = event.target.closest('[data-notice-template]');
|
||||
const preview = studio.querySelector('.notice-template-preview');
|
||||
if (event.target.name === 'primaryColor') preview.style.setProperty('--template-primary', event.target.value);
|
||||
if (event.target.name === 'accentColor') preview.style.setProperty('--template-accent', event.target.value);
|
||||
const output = studio.querySelector(`[data-template-preview="${event.target.name}"]`);
|
||||
if (output) output.textContent = event.target.value.replaceAll('{{考生姓名}}','张同学').replaceAll('{{考试名称}}','示例考试').replaceAll('{{录取学校}}', state.pageData?.school?.name || '本校').replaceAll('{{录取类别}}','普通生');
|
||||
}
|
||||
if (event.target.matches('[data-result-score]')) updateResultScoreInput(event.target);
|
||||
if (event.target.matches('[data-feature-score]')) updateFeatureScoreInput(event.target);
|
||||
@@ -739,7 +781,10 @@ document.addEventListener('change', event => {
|
||||
renderRoute();
|
||||
}
|
||||
if (event.target.matches('[data-qualification-select]')) updateQualificationSelection(event.target.closest('.qualification-ledger'));
|
||||
if (event.target.matches('[data-table-filter]')) applyTableFilters(event.target.dataset.target);
|
||||
if (event.target.matches('[data-table-filter]')) {
|
||||
setTableControl(state, event.target.dataset.target, { filters: { [event.target.dataset.tableFilter]: event.target.value } });
|
||||
renderRoute();
|
||||
}
|
||||
if (event.target.matches('[data-registration-select]')) updateRegistrationSelection();
|
||||
if (event.target.matches('[data-candidate-select]')) updateCandidateSelection();
|
||||
if (event.target.matches('[data-payment-select]')) updatePaymentSelection();
|
||||
@@ -898,6 +943,10 @@ document.addEventListener('submit', async event => {
|
||||
} else if (kind === 'register') {
|
||||
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 === 'document-verification') {
|
||||
const code = form.code.value.trim().toUpperCase();
|
||||
if (!code) throw new Error('请输入防伪查询码');
|
||||
navigate(`verify/${encodeURIComponent(code)}`);
|
||||
} else if (kind === 'candidate-password') {
|
||||
const body = formObject(form);
|
||||
if (body.newPassword !== body.confirmPassword) throw new Error('两次输入的新密码不一致');
|
||||
@@ -962,6 +1011,9 @@ document.addEventListener('submit', async event => {
|
||||
} else if (kind === 'admission-account') {
|
||||
await api('/api/admin/admission-school-accounts', { method: 'POST', body: formObject(form) });
|
||||
form.reset(); toast('招生学校账号已创建'); renderRoute();
|
||||
} else if (kind === 'admission-notice-template') {
|
||||
await api('/api/admission/notice-template', { method: 'PUT', body: formObject(form) });
|
||||
toast('录取通知书模板已保存', '正式录取考生将使用该模板生成 PDF'); renderRoute();
|
||||
} else if (kind === 'admission-plan' || kind === 'school-admission-plan') {
|
||||
const body = formObject(form);
|
||||
body.categories = [...form.querySelectorAll('.admission-category-editor')].map((editor, index) => {
|
||||
|
||||
Reference in New Issue
Block a user