Enable editing for draft exams
This commit is contained in:
1 parent
bb5d3da766
commit
1ed3b6722f
5 files changed
+79
-14
No files matched your search
@@ -77,7 +77,7 @@ function toast(title, message = '') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setModal(content) {
|
function setModal(content) {
|
||||||
modalRoot.innerHTML = `<div class="modal-layer" data-action="close-modal"><div class="modal-card" role="dialog" aria-modal="true" onclick="event.stopPropagation()">${content}</div></div>`;
|
modalRoot.innerHTML = `<div class="modal-layer" data-modal-backdrop><div class="modal-card" role="dialog" aria-modal="true">${content}</div></div>`;
|
||||||
setTimeout(() => modalRoot.querySelector('input,textarea,button')?.focus(), 30);
|
setTimeout(() => modalRoot.querySelector('input,textarea,button')?.focus(), 30);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,7 +264,7 @@ function adminRegistrations(registrations) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function adminExams(exams) {
|
function adminExams(exams) {
|
||||||
return `<div class="admin-exam-grid">${exams.map(exam => `<article class="admin-exam-card ${exam.status}"><header><span class="exam-code">${h(exam.code)}</span>${badge(exam.status)}</header><h2>${h(exam.name)}</h2><p>${h(exam.description)}</p><dl><div><dt>报名时间</dt><dd>${dateRange(exam.registrationStart, exam.registrationEnd)}</dd></div><div><dt>考试时间</dt><dd>${dateRange(exam.examStart, exam.examEnd)}</dd></div><div><dt>考点</dt><dd>${h(exam.location)}</dd></div></dl><div class="admin-subjects">${exam.subjects.map(subject => `<span><b>${h(subject.name)}</b><small>${h(subject.date)} ${h(subject.start)}</small></span>`).join('') || '<span>科目待配置</span>'}</div><footer><span>${exam.registrationCount} 人报名 · ${exam.subjects.length} 科</span><button class="row-action" data-action="toggle-exam" data-id="${h(exam.id)}" data-status="${exam.status === 'published' ? 'draft' : 'published'}">${exam.status === 'published' ? '撤回为草稿' : '发布考试'}</button></footer></article>`).join('')}</div>`;
|
return `<div class="admin-exam-grid">${exams.map(exam => `<article class="admin-exam-card ${exam.status}${exam.status === 'draft' ? ' editable' : ''}" ${exam.status === 'draft' ? `data-action="edit-exam" data-id="${h(exam.id)}" title="点击编辑草稿"` : ''}><header><span class="exam-code">${h(exam.code)}</span>${badge(exam.status)}</header><h2>${h(exam.name)}</h2><p>${h(exam.description)}</p><dl><div><dt>报名时间</dt><dd>${dateRange(exam.registrationStart, exam.registrationEnd)}</dd></div><div><dt>考试时间</dt><dd>${dateRange(exam.examStart, exam.examEnd)}</dd></div><div><dt>考点</dt><dd>${h(exam.location)}</dd></div></dl><div class="admin-subjects">${exam.subjects.map(subject => `<span><b>${h(subject.name)}</b><small>${h(subject.date)} ${h(subject.start)}</small></span>`).join('') || '<span>科目待配置</span>'}</div><footer><span>${exam.registrationCount} 人报名 · ${exam.subjects.length} 科</span><div class="exam-card-actions">${exam.status === 'draft' ? `<button class="row-action" data-action="edit-exam" data-id="${h(exam.id)}">编辑草稿</button>` : ''}<button class="row-action" data-action="toggle-exam" data-id="${h(exam.id)}" data-status="${exam.status === 'published' ? 'draft' : 'published'}">${exam.status === 'published' ? '撤回为草稿' : '发布考试'}</button></div></footer></article>`).join('')}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function adminNotices(notices) {
|
function adminNotices(notices) {
|
||||||
@@ -325,6 +325,7 @@ document.addEventListener('click', async event => {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
return navigate(routeTarget.dataset.route);
|
return navigate(routeTarget.dataset.route);
|
||||||
}
|
}
|
||||||
|
if (event.target.matches('[data-modal-backdrop]')) return closeModal();
|
||||||
const target = event.target.closest('[data-action]');
|
const target = event.target.closest('[data-action]');
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
const action = target.dataset.action;
|
const action = target.dataset.action;
|
||||||
@@ -358,6 +359,7 @@ document.addEventListener('click', async event => {
|
|||||||
if (action === 'download-admit') { window.location.href = `/api/candidate/registrations/${target.dataset.id}/admit-card`; return; }
|
if (action === 'download-admit') { window.location.href = `/api/candidate/registrations/${target.dataset.id}/admit-card`; return; }
|
||||||
if (action === 'new-notice') return openNoticeForm();
|
if (action === 'new-notice') return openNoticeForm();
|
||||||
if (action === 'new-exam') return openExamForm();
|
if (action === 'new-exam') return openExamForm();
|
||||||
|
if (action === 'edit-exam') return openExamForm(state.pageData.exams.find(exam => exam.id === target.dataset.id));
|
||||||
if (action === 'review-candidate') return openCandidateReview(target.dataset.id);
|
if (action === 'review-candidate') return openCandidateReview(target.dataset.id);
|
||||||
if (action === 'review-registration') return openRegistrationReview(target.dataset.id);
|
if (action === 'review-registration') return openRegistrationReview(target.dataset.id);
|
||||||
if (action === 'generate-admit') {
|
if (action === 'generate-admit') {
|
||||||
@@ -446,8 +448,9 @@ document.addEventListener('submit', async event => {
|
|||||||
} else if (kind === 'exam-form') {
|
} else if (kind === 'exam-form') {
|
||||||
const body = formObject(form); body.subjects = body.subjects.split(/[,,]/).map(item => item.trim()).filter(Boolean);
|
const body = formObject(form); body.subjects = body.subjects.split(/[,,]/).map(item => item.trim()).filter(Boolean);
|
||||||
['registrationStart','registrationEnd','examStart','examEnd','admitDownloadStart','admitDownloadEnd'].forEach(field => body[field] = new Date(body[field]).toISOString());
|
['registrationStart','registrationEnd','examStart','examEnd','admitDownloadStart','admitDownloadEnd'].forEach(field => body[field] = new Date(body[field]).toISOString());
|
||||||
await api('/api/admin/exams', { method: 'POST', body });
|
const editing = Boolean(body.id);
|
||||||
closeModal(); await refreshPublic(); toast('考试计划已创建', `${body.subjects.length} 个科目已加入`); renderRoute();
|
await api(editing ? `/api/admin/exams/${body.id}` : '/api/admin/exams', { method: editing ? 'PATCH' : 'POST', body });
|
||||||
|
closeModal(); await refreshPublic(); toast(editing ? '考试草稿已更新' : '考试计划已创建', `${body.subjects.length} 个科目已配置`); renderRoute();
|
||||||
} else if (kind === 'result-entry') {
|
} else if (kind === 'result-entry') {
|
||||||
const body = formObject(form); body.published = form.published.checked;
|
const body = formObject(form); body.published = form.published.checked;
|
||||||
await api('/api/admin/results', { method: 'POST', body });
|
await api('/api/admin/results', { method: 'POST', body });
|
||||||
@@ -471,8 +474,18 @@ 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>`);
|
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>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function openExamForm() {
|
function dateTimeLocal(value) {
|
||||||
setModal(`<div class="modal-head"><div><span>NEW EXAM</span><h2>创建考试与科目</h2><p>创建后可选择直接发布或先保存为草稿。</p></div><button data-action="close-modal">×</button></div><form class="modal-form wide-form" data-form="exam-form"><div class="field-row"><label><span>考试名称 *</span><input name="name" required placeholder="例如:2027 年春季学业考试"></label><label><span>考试代码</span><input name="code" placeholder="自动生成或手动填写"></label></div><label><span>考试说明</span><textarea name="description" rows="2" placeholder="报名对象、考试范围等"></textarea></label><div class="field-row"><label><span>报名开始 *</span><input name="registrationStart" type="datetime-local" required></label><label><span>报名结束 *</span><input name="registrationEnd" type="datetime-local" required></label></div><div class="field-row"><label><span>考试开始 *</span><input name="examStart" type="datetime-local" required></label><label><span>考试结束 *</span><input name="examEnd" type="datetime-local" required></label></div><div class="field-row"><label><span>准考证下载开始 *</span><input name="admitDownloadStart" type="datetime-local" required></label><label><span>准考证下载结束 *</span><input name="admitDownloadEnd" type="datetime-local" required></label></div><label><span>考试科目 *</span><input name="subjects" required placeholder="用逗号分隔,例如:语文,数学,英语"></label><div class="field-row"><label><span>考点说明</span><input name="location" placeholder="例如:全市指定考点"></label><label><span>创建状态</span><select name="status"><option value="draft">保存为草稿</option><option value="published">立即发布</option></select></label></div><div class="modal-foot"><button type="button" class="ghost-button" data-action="close-modal">取消</button><button type="submit" class="solid-button">创建考试</button></div></form>`);
|
if (!value) return '';
|
||||||
|
const date = new Date(value);
|
||||||
|
if (Number.isNaN(date.getTime())) return '';
|
||||||
|
const pad = number => String(number).padStart(2, '0');
|
||||||
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openExamForm(exam = null) {
|
||||||
|
const editing = Boolean(exam);
|
||||||
|
if (editing && exam.status !== 'draft') return toast('无法编辑', '请先将已发布考试撤回为草稿');
|
||||||
|
setModal(`<div class="modal-head"><div><span>${editing ? 'EDIT EXAM DRAFT' : 'NEW EXAM'}</span><h2>${editing ? '编辑考试草稿' : '创建考试与科目'}</h2><p>${editing ? '补全考试信息和科目后,可保存草稿或直接发布。' : '创建后可选择直接发布或先保存为草稿。'}</p></div><button type="button" data-action="close-modal" aria-label="关闭弹窗">×</button></div><form class="modal-form wide-form" data-form="exam-form">${editing ? `<input type="hidden" name="id" value="${h(exam.id)}">` : ''}<div class="field-row"><label><span>考试名称 *</span><input name="name" required value="${h(exam?.name)}" placeholder="例如:2027 年春季学业考试"></label><label><span>考试代码</span><input name="code" value="${h(exam?.code)}" placeholder="自动生成或手动填写"></label></div><label><span>考试说明</span><textarea name="description" rows="2" placeholder="报名对象、考试范围等">${h(exam?.description)}</textarea></label><div class="field-row"><label><span>报名开始 *</span><input name="registrationStart" type="datetime-local" value="${dateTimeLocal(exam?.registrationStart)}" required></label><label><span>报名结束 *</span><input name="registrationEnd" type="datetime-local" value="${dateTimeLocal(exam?.registrationEnd)}" required></label></div><div class="field-row"><label><span>考试开始 *</span><input name="examStart" type="datetime-local" value="${dateTimeLocal(exam?.examStart)}" required></label><label><span>考试结束 *</span><input name="examEnd" type="datetime-local" value="${dateTimeLocal(exam?.examEnd)}" required></label></div><div class="field-row"><label><span>准考证下载开始 *</span><input name="admitDownloadStart" type="datetime-local" value="${dateTimeLocal(exam?.admitDownloadStart)}" required></label><label><span>准考证下载结束 *</span><input name="admitDownloadEnd" type="datetime-local" value="${dateTimeLocal(exam?.admitDownloadEnd)}" required></label></div><label><span>考试科目 *</span><input name="subjects" required value="${h(exam?.subjects?.map(subject => subject.name).join(','))}" placeholder="用逗号分隔,例如:语文,数学,英语"></label><div class="field-row"><label><span>考点说明</span><input name="location" value="${h(exam?.location)}" placeholder="例如:全市指定考点"></label><label><span>${editing ? '保存状态' : '创建状态'}</span><select name="status"><option value="draft">保存为草稿</option><option value="published">立即发布</option></select></label></div><div class="modal-foot"><button type="button" class="ghost-button" data-action="close-modal">取消</button><button type="submit" class="solid-button">${editing ? '保存考试' : '创建考试'}</button></div></form>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function openAdmitPreview(reg) {
|
function openAdmitPreview(reg) {
|
||||||
|
|||||||
+16
-5
@@ -720,8 +720,8 @@ function createRepository({ client, location, read, transaction, close }) {
|
|||||||
operations.push(auditOperation(log));
|
operations.push(auditOperation(log));
|
||||||
await transaction(operations);
|
await transaction(operations);
|
||||||
},
|
},
|
||||||
async updateExam(exam, log) {
|
async updateExam(exam, log, replaceSubjects = false) {
|
||||||
await transaction([
|
const operations = [
|
||||||
operation(
|
operation(
|
||||||
`UPDATE exams SET
|
`UPDATE exams SET
|
||||||
code = ?, name = ?, description = ?, registration_start = ?, registration_end = ?,
|
code = ?, name = ?, description = ?, registration_start = ?, registration_end = ?,
|
||||||
@@ -730,9 +730,20 @@ function createRepository({ client, location, read, transaction, close }) {
|
|||||||
exam.code, exam.name, exam.description || '', exam.registrationStart, exam.registrationEnd,
|
exam.code, exam.name, exam.description || '', exam.registrationStart, exam.registrationEnd,
|
||||||
exam.examStart, exam.examEnd, exam.admitDownloadStart, exam.admitDownloadEnd,
|
exam.examStart, exam.examEnd, exam.admitDownloadStart, exam.admitDownloadEnd,
|
||||||
exam.location || '', exam.status, exam.id
|
exam.location || '', exam.status, exam.id
|
||||||
),
|
)
|
||||||
auditOperation(log)
|
];
|
||||||
]);
|
if (replaceSubjects) {
|
||||||
|
operations.push(operation('DELETE FROM exam_subjects WHERE exam_id = ?', exam.id));
|
||||||
|
exam.subjects.forEach((subject, index) => operations.push(operation(
|
||||||
|
`INSERT INTO exam_subjects (
|
||||||
|
id, exam_id, name, subject_date, start_time, end_time, fee, position
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
|
subject.id, exam.id, subject.name, subject.date || String(exam.examStart).slice(0, 10),
|
||||||
|
subject.start || '', subject.end || '', Number(subject.fee || 0), Number(subject.order || index + 1)
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
operations.push(auditOperation(log));
|
||||||
|
await transaction(operations);
|
||||||
},
|
},
|
||||||
async createNotice(notice, log) {
|
async createNotice(notice, log) {
|
||||||
await transaction([
|
await transaction([
|
||||||
|
|||||||
+17
-2
@@ -437,10 +437,25 @@ async function handleAdmin(request, response, pathname) {
|
|||||||
const body = await readJson(request);
|
const body = await readJson(request);
|
||||||
const exam = db.exams.find(item => item.id === examMatch[1]);
|
const exam = db.exams.find(item => item.id === examMatch[1]);
|
||||||
if (!exam) return sendError(response, 404, '考试不存在');
|
if (!exam) return sendError(response, 404, '考试不存在');
|
||||||
|
const originalStatus = exam.status;
|
||||||
|
const detailFields = ['code', 'name', 'description', 'location', 'registrationStart', 'registrationEnd', 'examStart', 'examEnd', 'admitDownloadStart', 'admitDownloadEnd'];
|
||||||
|
const editingDetails = detailFields.some(field => body[field] != null) || body.subjects != null;
|
||||||
|
if (editingDetails && originalStatus !== 'draft') return sendError(response, 409, '请先将考试撤回为草稿后再编辑');
|
||||||
if (body.status && ['draft', 'published', 'closed'].includes(body.status)) exam.status = body.status;
|
if (body.status && ['draft', 'published', 'closed'].includes(body.status)) exam.status = body.status;
|
||||||
['name', 'description', 'location', 'registrationStart', 'registrationEnd', 'examStart', 'examEnd', 'admitDownloadStart', 'admitDownloadEnd'].forEach(field => { if (body[field] != null) exam[field] = cleanText(body[field], 500); });
|
detailFields.forEach(field => { if (body[field] != null) exam[field] = cleanText(body[field], field === 'description' ? 500 : 100); });
|
||||||
|
let replaceSubjects = false;
|
||||||
|
if (body.subjects != null) {
|
||||||
|
if (db.registrations.some(registration => registration.examId === exam.id)) return sendError(response, 409, '已有报名记录,不能修改考试科目');
|
||||||
|
const subjectNames = Array.isArray(body.subjects) ? body.subjects : String(body.subjects || '').split(/[,,]/);
|
||||||
|
const names = subjectNames.map(item => cleanText(typeof item === 'string' ? item : item.name, 30)).filter(Boolean);
|
||||||
|
if (!names.length) return sendError(response, 400, '请至少添加一个考试科目');
|
||||||
|
exam.subjects = names.map((name, index) => ({ id: uid('sub'), name, date: String(exam.examStart).slice(0, 10), start: '09:00', end: '11:00', fee: 0, order: index + 1 }));
|
||||||
|
replaceSubjects = true;
|
||||||
|
}
|
||||||
|
if (!exam.name || !exam.registrationStart || !exam.registrationEnd || !exam.examStart || !exam.examEnd) return sendError(response, 400, '请完整填写考试名称和关键日期');
|
||||||
|
if (exam.status === 'published' && !exam.subjects.length) return sendError(response, 400, '请先配置考试科目再发布');
|
||||||
const log = logAction(db, user, '更新考试', `${exam.name} · 状态 ${exam.status}`);
|
const log = logAction(db, user, '更新考试', `${exam.name} · 状态 ${exam.status}`);
|
||||||
await database.updateExam(exam, log);
|
await database.updateExam(exam, log, replaceSubjects);
|
||||||
return sendJson(response, 200, { ok: true, exam });
|
return sendJson(response, 200, { ok: true, exam });
|
||||||
}
|
}
|
||||||
if (request.method === 'GET' && pathname === '/api/admin/notices') return sendJson(response, 200, { ok: true, notices: db.notices.sort((a, b) => new Date(b.publishAt || b.createdAt) - new Date(a.publishAt || a.createdAt)) });
|
if (request.method === 'GET' && pathname === '/api/admin/notices') return sendJson(response, 200, { ok: true, notices: db.notices.sort((a, b) => new Date(b.publishAt || b.createdAt) - new Date(a.publishAt || a.createdAt)) });
|
||||||
|
|||||||
Loaded 3 of 5 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user