Expand application functionality

This commit is contained in:
2026-07-20 19:23:11 +08:00 Unverified
parent 6aeed6b880
commit 5b86cdaec2
10 changed files with 3037 additions and 22 deletions
+70 -5
View File
@@ -9,6 +9,8 @@ import { formatRegionAddress, mountRegionSelects, updateRegionSelects } from './
const app = document.querySelector('#app');
const modalRoot = document.querySelector('#modalRoot');
let toastTimer;
let noticeEditor;
let ckeditorModulePromise;
function toast(title, message = '') {
const element = document.querySelector('#toast');
element.querySelector('strong').textContent = title;
@@ -24,9 +26,27 @@ function setModal(content) {
}
function closeModal() {
const editor = noticeEditor;
noticeEditor = null;
if (editor) editor.destroy().catch(error => console.error('CKEditor cleanup failed', error));
modalRoot.innerHTML = '';
}
function loadCKEditor() {
if (!document.querySelector('link[data-ckeditor-styles]')) {
const stylesheet = document.createElement('link');
stylesheet.rel = 'stylesheet';
stylesheet.href = '/vendor/ckeditor5/ckeditor5.css';
stylesheet.dataset.ckeditorStyles = '';
document.head.append(stylesheet);
}
ckeditorModulePromise ||= Promise.all([
import('/vendor/ckeditor5/ckeditor5.js'),
import('/vendor/ckeditor5/translations/zh-cn.js')
]);
return ckeditorModulePromise;
}
function emptyState(title, description, route, action) {
return `<section class="panel empty-panel"><span>${icons.ticket}</span><h2>${h(title)}</h2><p>${h(description)}</p>${route ? `<button class="solid-button" data-route="${h(route)}">${h(action)}</button>` : ''}</section>`;
}
@@ -108,7 +128,8 @@ document.addEventListener('click', async event => {
}
if (action === 'open-notice') {
const notice = state.publicData.notices.find(item => item.id === target.dataset.id) || (await api(`/api/public/notices/${target.dataset.id}`)).notice;
setModal(`<div class="modal-head"><div><span>${h(notice.category)}</span><h2>${h(notice.title)}</h2><p>${formatDate(notice.publishAt,true)} · ${h(notice.author)}</p></div><button data-action="close-modal">×</button></div><article class="notice-content"><p>${h(notice.content).replace(/\n/g, '</p><p>')}</p></article><div class="modal-foot"><button class="ghost-button" data-action="close-modal">关闭</button></div>`); return;
const contentHtml = notice.contentHtml || `<p>${h(notice.content).replace(/\r?\n/g, '</p><p>')}</p>`;
setModal(`<div class="modal-head"><div><span>${h(notice.category)}</span><h2>${h(notice.title)}</h2><p>${formatDate(notice.publishAt,true)} · ${h(notice.author)}</p></div><button data-action="close-modal">×</button></div><article class="notice-content">${contentHtml}</article><div class="modal-foot"><button class="ghost-button" data-action="close-modal">关闭</button></div>`); return;
}
if (action === 'download-admit') { window.location.href = `/api/candidate/registrations/${target.dataset.id}/admit-card`; return; }
if (['batch-admit-download', 'admit-info-export', 'center-materials-export'].includes(action)) {
@@ -119,7 +140,7 @@ document.addEventListener('click', async event => {
window.location.href = `/api/admin/admission-exports/${type}?examId=${encodeURIComponent(examId)}`;
return;
}
if (action === 'new-notice') return openNoticeForm();
if (action === 'new-notice') { await openNoticeForm(); return; }
if (action === 'new-exam') return openExamForm();
if (action === 'new-admin') return openAdminForm();
if (action === 'new-school-class') return openSchoolClassForm();
@@ -484,7 +505,9 @@ document.addEventListener('submit', async event => {
await api(`/api/admin/workflow-instances/${body.id}/supervise`, { method: 'PATCH', body });
closeModal(); toast('流程已监督调整', '节点与责任人已更新并记录'); renderRoute();
} else if (kind === 'notice-form') {
const body = formObject(form); body.pinned = form.pinned.checked;
const body = formObject(form);
if (noticeEditor) body.content = noticeEditor.getData();
body.pinned = form.pinned.checked;
await api('/api/admin/notices', { method: 'POST', body });
closeModal(); await refreshPublic(); toast(body.status === 'published' ? '通知已发布' : '草稿已保存', '公开首页状态已同步'); renderRoute();
} else if (kind === 'exam-form') {
@@ -599,8 +622,50 @@ function openRegistrationReview(id) {
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 || reg.candidate?.candidateNumber || '账户号码异常')}</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"><button class="solid-button" data-action="close-modal">关闭</button></div>'}`);
}
function openNoticeForm() {
setModal(`<div class="modal-head"><div><span>NEW NOTICE</span><h2>发布通知公告</h2><p>发布后将立即显示在公开首页和考生中心。</p></div><button data-action="close-modal">×</button></div><form class="modal-form" data-form="notice-form"><div class="field-row"><label><span>通知分类</span><select name="category"><option>报名通知</option><option>考试须知</option><option>考点公告</option><option>成绩通知</option><option>系统公告</option></select></label><label><span>发布方式</span><select name="status"><option value="published">立即发布</option><option value="draft">保存草稿</option></select></label></div><label><span>通知标题 *</span><input name="title" required placeholder="清楚说明通知事项"></label><label><span>首页摘要</span><input name="summary" placeholder="用于首页列表展示,建议 60 字以内"></label><label><span>通知正文 *</span><textarea name="content" rows="7" required placeholder="请输入完整通知内容"></textarea></label><label class="agreement"><input type="checkbox" name="pinned"><span>在公开首页置顶展示</span></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>`);
async function openNoticeForm() {
setModal(`<div class="modal-head"><div><span>NEW NOTICE</span><h2>发布通知公告</h2><p>发布后将立即显示在公开首页和考生中心。</p></div><button data-action="close-modal">×</button></div><form class="modal-form notice-editor-form" data-form="notice-form"><div class="field-row"><label><span>通知分类</span><select name="category"><option>报名通知</option><option>考试须知</option><option>考点公告</option><option>成绩通知</option><option>系统公告</option></select></label><label><span>发布方式</span><select name="status"><option value="published">立即发布</option><option value="draft">保存草稿</option></select></label></div><label><span>通知标题 *</span><input name="title" required placeholder="清楚说明通知事项"></label><label><span>首页摘要</span><input name="summary" placeholder="用于首页列表展示;留空将从正文自动提取"></label><div class="notice-editor-field"><span>通知正文 *</span><textarea name="content" rows="10" data-notice-editor placeholder="请输入完整通知内容"></textarea><small>图片请点工具栏“通过 URL 插入图片”并填写专用服务器地址;普通文件仍使用超链接。</small></div><label class="agreement"><input type="checkbox" name="pinned"><span>在公开首页置顶展示</span></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>`);
const source = modalRoot.querySelector('[data-notice-editor]');
try {
const [ckeditor, translation] = await loadCKEditor();
const {
ClassicEditor, AutoImage, AutoLink, BlockQuote, Bold, Essentials, Heading,
Image, ImageCaption, ImageInsertViaUrl, ImageStyle, ImageTextAlternative, ImageToolbar,
Italic, Link, List, Paragraph, Table, TableCaption, TableToolbar, Underline
} = ckeditor;
if (!source?.isConnected) return;
noticeEditor = await ClassicEditor.create(source, {
licenseKey: 'GPL',
language: 'zh-cn',
translations: [translation.default],
plugins: [
Essentials, Paragraph, Heading, Bold, Italic, Underline, Link, AutoLink, List, BlockQuote,
Image, ImageCaption, ImageInsertViaUrl, ImageStyle, ImageTextAlternative, ImageToolbar, AutoImage,
Table, TableToolbar, TableCaption
],
toolbar: {
items: ['heading', '|', 'bold', 'italic', 'underline', '|', 'bulletedList', 'numberedList', 'blockQuote', '|', 'link', 'insertImageViaUrl', 'insertTable', '|', 'undo', 'redo'],
shouldNotGroupWhenFull: true
},
heading: {
options: [
{ model: 'paragraph', title: '正文', class: 'ck-heading_paragraph' },
{ model: 'heading2', view: 'h2', title: '二级标题', class: 'ck-heading_heading2' },
{ model: 'heading3', view: 'h3', title: '三级标题', class: 'ck-heading_heading3' }
]
},
link: { defaultProtocol: 'https://', addTargetToExternalLinks: true },
image: {
toolbar: ['imageStyle:inline', 'imageStyle:block', 'imageStyle:side', '|', 'toggleImageCaption', 'imageTextAlternative']
},
table: {
contentToolbar: ['tableColumn', 'tableRow', 'mergeTableCells', '|', 'toggleTableCaption']
},
placeholder: '请输入完整通知内容;图片使用 URL 插入,文件使用超链接'
});
} catch (error) {
console.error('CKEditor failed to load', error);
toast('富文本编辑器加载失败', '已保留普通文本输入,可检查网络后重试');
}
}
function dateTimeLocal(value) {