Expand application functionality
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user