准考证优化
This commit is contained in:
1 parent
9566542414
commit
c5b18d0b15
13 files changed
+447
-57
No files matched your search
@@ -111,6 +111,14 @@ document.addEventListener('click', async event => {
|
||||
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;
|
||||
}
|
||||
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)) {
|
||||
event.preventDefault();
|
||||
const examId = target.closest('.admission-export-panel')?.querySelector('[data-admission-export-exam]')?.value;
|
||||
if (!examId) return toast('请选择考试', '没有可导出的考试范围');
|
||||
const type = { 'batch-admit-download': 'admit-cards', 'admit-info-export': 'info', 'center-materials-export': 'center-materials' }[action];
|
||||
window.location.href = `/api/admin/admission-exports/${type}?examId=${encodeURIComponent(examId)}`;
|
||||
return;
|
||||
}
|
||||
if (action === 'new-notice') return openNoticeForm();
|
||||
if (action === 'new-exam') return openExamForm();
|
||||
if (action === 'new-admin') return openAdminForm();
|
||||
@@ -562,9 +570,9 @@ function openAdmitPreview(reg) {
|
||||
const assignments = new Map((reg.admitCard.assignments || []).map(item => [item.subjectId, item]));
|
||||
const rows = reg.subjects.map(subject => {
|
||||
const assignment = assignments.get(subject.id) || {};
|
||||
return `<tr><td>${h(subject.name)}</td><td>${h(subject.date)} ${h(subject.start)}</td><td>${h(assignment.roomName || assignment.room || '待定')}</td><td>${h(assignment.seat || '—')}</td></tr>`;
|
||||
return `<tr><td>${h(subject.name)}</td><td>${h(subject.date)} ${h(subject.start)}</td><td class="mono">${h(assignment.examRoomCode || '待定')}</td><td><strong>${h(assignment.roomName || assignment.room || '待定')}</strong><small>场地代码 ${h(assignment.roomCode || '—')}</small></td><td>${h(assignment.building || '楼栋待定')} · ${h(assignment.floor || '楼层待定')}</td><td>${h(assignment.seat || '—')}</td></tr>`;
|
||||
}).join('');
|
||||
setModal(`<div class="modal-head"><div><span>ADMISSION CARD</span><h2>准考证编排明细</h2><p>${h(reg.candidate?.name)} · ${h(reg.exam.name)}</p></div><button data-action="close-modal">×</button></div><div class="admit-preview"><strong>${h(reg.admitCard.number)}</strong><dl><div><dt>固定考点</dt><dd>${h(reg.admitCard.testCenter)}</dd></div><div><dt>生成时间</dt><dd>${formatDate(reg.admitCard.generatedAt,true)}</dd></div></dl><table><thead><tr><th>科目</th><th>时间</th><th>考场</th><th>座位</th></tr></thead><tbody>${rows}</tbody></table><p>考生可在 ${dateRange(reg.exam.admitDownloadStart, reg.exam.admitDownloadEnd)} 下载。</p></div><div class="modal-foot"><button class="solid-button" data-action="close-modal">完成</button></div>`);
|
||||
setModal(`<div class="modal-head"><div><span>ADMISSION CARD</span><h2>准考证编排明细</h2><p>${h(reg.candidate?.name)} · ${h(reg.exam.name)}</p></div><button data-action="close-modal">×</button></div><div class="admit-preview"><strong>${h(reg.admitCard.number)}</strong><dl><div><dt>固定考点</dt><dd><strong>${h(reg.admitCard.testCenter)}</strong><small>${h(reg.admitCard.centerCode || '')} · ${h(reg.admitCard.centerAddress || '详细地址待公布')}</small></dd></div><div><dt>生成时间</dt><dd>${formatDate(reg.admitCard.generatedAt,true)}</dd></div></dl><table><thead><tr><th>科目</th><th>时间</th><th>考试考场序号</th><th>考场通用名称 / 场地代码</th><th>楼栋 / 楼层</th><th>座位</th></tr></thead><tbody>${rows}</tbody></table><p>“考试考场序号”是本次考试编排编号,不等同于考场通用名称。考生可在 ${dateRange(reg.exam.admitDownloadStart, reg.exam.admitDownloadEnd)} 下载。</p></div><div class="modal-foot"><button class="solid-button" data-action="close-modal">完成</button></div>`);
|
||||
}
|
||||
|
||||
window.addEventListener('hashchange', renderRoute);
|
||||
|
||||
+19
-13
@@ -57,7 +57,7 @@ function buildSeedOperations(state) {
|
||||
const nullable = value => value == null || value === '' ? null : value;
|
||||
|
||||
add(
|
||||
'UPDATE schema_metadata SET schema_version = 9, app_version = ?, self_registration_enabled = ?, created_at = ? WHERE id = 1',
|
||||
'UPDATE schema_metadata SET schema_version = 10, app_version = ?, self_registration_enabled = ?, created_at = ? WHERE id = 1',
|
||||
Number(state.meta?.version || 1), state.settings?.selfRegistrationEnabled ? 1 : 0,
|
||||
state.meta?.createdAt || new Date().toISOString()
|
||||
);
|
||||
@@ -219,16 +219,17 @@ function buildSeedOperations(state) {
|
||||
const card = registration.admitCard;
|
||||
add(
|
||||
`INSERT INTO admit_cards (
|
||||
registration_id, plan_id, card_number, center_id, test_center, generated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
registration.id, card.planId, card.number, nullable(card.centerId), card.testCenter, card.generatedAt
|
||||
registration_id, plan_id, card_number, center_id, test_center, center_code, center_address, generated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
registration.id, card.planId, card.number, nullable(card.centerId), card.testCenter,
|
||||
card.centerCode || '', card.centerAddress || '', card.generatedAt
|
||||
);
|
||||
for (const assignment of card.assignments || []) add(
|
||||
`INSERT INTO admit_card_subjects (
|
||||
registration_id, subject_id, room_id, room, room_code, exam_room_code, seat, subject_signature
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
registration_id, subject_id, room_id, room, room_code, exam_room_code, building, floor, seat, subject_signature
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
registration.id, assignment.subjectId, nullable(assignment.roomId), assignment.roomName || assignment.room,
|
||||
assignment.roomCode, assignment.examRoomCode, assignment.seat, assignment.subjectSignature || ''
|
||||
assignment.roomCode, assignment.examRoomCode, assignment.building || '', assignment.floor || '', assignment.seat, assignment.subjectSignature || ''
|
||||
);
|
||||
}
|
||||
|
||||
@@ -374,6 +375,8 @@ function stateFromRows(rows) {
|
||||
room: row.room,
|
||||
roomCode: row.room_code,
|
||||
examRoomCode: row.exam_room_code,
|
||||
building: row.building || '',
|
||||
floor: row.floor || '',
|
||||
seat: row.seat,
|
||||
subjectSignature: row.subject_signature || ''
|
||||
});
|
||||
@@ -390,6 +393,8 @@ function stateFromRows(rows) {
|
||||
number: row.card_number,
|
||||
centerId: row.center_id,
|
||||
testCenter: row.test_center,
|
||||
centerCode: row.center_code || '',
|
||||
centerAddress: row.center_address || '',
|
||||
room: primary.roomName || '',
|
||||
seat: primary.seat || '',
|
||||
assignments,
|
||||
@@ -1265,16 +1270,17 @@ function createRepository({ client, location, read, transaction, close }) {
|
||||
for (const card of cards) {
|
||||
operations.push(operation(
|
||||
`INSERT INTO admit_cards (
|
||||
registration_id, plan_id, card_number, center_id, test_center, generated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
card.registrationId, plan.id, card.number, optional(card.centerId), card.testCenter, card.generatedAt
|
||||
registration_id, plan_id, card_number, center_id, test_center, center_code, center_address, generated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
card.registrationId, plan.id, card.number, optional(card.centerId), card.testCenter,
|
||||
card.centerCode || '', card.centerAddress || '', card.generatedAt
|
||||
));
|
||||
for (const assignment of card.assignments) operations.push(operation(
|
||||
`INSERT INTO admit_card_subjects (
|
||||
registration_id, subject_id, room_id, room, room_code, exam_room_code, seat, subject_signature
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
registration_id, subject_id, room_id, room, room_code, exam_room_code, building, floor, seat, subject_signature
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
card.registrationId, assignment.subjectId, optional(assignment.roomId), assignment.roomName,
|
||||
assignment.roomCode, assignment.examRoomCode, assignment.seat, assignment.subjectSignature
|
||||
assignment.roomCode, assignment.examRoomCode, assignment.building || '', assignment.floor || '', assignment.seat, assignment.subjectSignature
|
||||
));
|
||||
}
|
||||
operations.push(auditOperation(log));
|
||||
|
||||
@@ -78,6 +78,18 @@ const resourceSpecs = {
|
||||
],
|
||||
validations: { published: ['发布', '不发布'] },
|
||||
numberColumns: ['score']
|
||||
},
|
||||
admit_cards: {
|
||||
title: '准考证信息台账', sheet: '准考证信息',
|
||||
columns: [
|
||||
['schoolName', '考生学校', 24, ''], ['className', '班级', 20, ''],
|
||||
['candidateNumber', '报名号', 24, ''], ['candidateName', '姓名', 14, ''], ['idNumber', '证件号码', 22, ''],
|
||||
['examCode', '考试代码', 18, ''], ['examName', '考试名称', 28, ''], ['cardNumber', '准考证号', 22, ''],
|
||||
['centerCode', '考点代码', 16, ''], ['centerName', '考点名称', 26, ''], ['centerAddress', '考点详细地址', 38, ''],
|
||||
['subjectName', '科目', 14, ''], ['subjectDate', '日期', 14, ''], ['subjectTime', '时间', 16, ''],
|
||||
['examRoomCode', '考试考场序号', 16, ''], ['roomName', '考场通用名称', 20, ''], ['roomCode', '物理场地代码', 16, ''],
|
||||
['building', '楼栋', 16, ''], ['floor', '楼层', 12, ''], ['seat', '座位号', 12, '']
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -173,3 +185,133 @@ export async function buildWorkbook(resource, rows = [], { template = false, sub
|
||||
for (const col of requiredColumns) header.getCell(col).font = { name: '微软雅黑', bold: true, color: { argb: 'FFFFE7A3' } };
|
||||
return workbook.xlsx.writeBuffer();
|
||||
}
|
||||
|
||||
function addOperationalSheet(workbook, name, title, columns, rows, { landscape = true } = {}) {
|
||||
const sheet = workbook.addWorksheet(name, { views: [{ state: 'frozen', ySplit: 2, showGridLines: false }] });
|
||||
sheet.columns = columns.map(([key, , width]) => ({ key, width }));
|
||||
sheet.mergeCells(1, 1, 1, columns.length);
|
||||
const titleCell = sheet.getCell(1, 1);
|
||||
titleCell.value = title;
|
||||
titleCell.font = { name: '微软雅黑', size: 16, bold: true, color: { argb: 'FFFFFFFF' } };
|
||||
titleCell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF173F60' } };
|
||||
titleCell.alignment = { vertical: 'middle', horizontal: 'left' };
|
||||
sheet.getRow(1).height = 34;
|
||||
const header = sheet.getRow(2);
|
||||
header.values = columns.map(([, label]) => label);
|
||||
header.height = 28;
|
||||
header.font = { name: '微软雅黑', bold: true, color: { argb: 'FFFFFFFF' } };
|
||||
header.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF2C7080' } };
|
||||
header.alignment = { vertical: 'middle', horizontal: 'center', wrapText: true };
|
||||
rows.forEach((item, index) => {
|
||||
const row = sheet.addRow(Object.fromEntries(columns.map(([key]) => [key, item[key] ?? ''])));
|
||||
row.height = 28;
|
||||
row.font = { name: '微软雅黑', size: 10, color: { argb: 'FF243B4A' } };
|
||||
row.alignment = { vertical: 'middle', wrapText: true };
|
||||
if (index % 2 === 1) row.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF3F7F8' } };
|
||||
row.eachCell(cell => { cell.border = { bottom: { style: 'hair', color: { argb: 'FFD8E2E7' } } }; });
|
||||
});
|
||||
sheet.autoFilter = { from: { row: 2, column: 1 }, to: { row: Math.max(2, sheet.rowCount), column: columns.length } };
|
||||
sheet.pageSetup = { orientation: landscape ? 'landscape' : 'portrait', paperSize: 9, fitToPage: true, fitToWidth: 1, fitToHeight: 0, margins: { left: .25, right: .25, top: .4, bottom: .4, header: .2, footer: .2 } };
|
||||
sheet.printTitlesRow = '1:2';
|
||||
return sheet;
|
||||
}
|
||||
|
||||
function addDeskStickerSheet(workbook, rows) {
|
||||
const sheet = workbook.addWorksheet('桌贴', { views: [{ showGridLines: false, zoomScale: 85 }] });
|
||||
const labelColumns = [14.5, 14.5, 14.5, 3, 14.5, 14.5, 14.5];
|
||||
labelColumns.forEach((width, index) => { sheet.getColumn(index + 1).width = width; });
|
||||
sheet.pageSetup = {
|
||||
orientation: 'portrait', paperSize: 9, fitToPage: true, fitToWidth: 1, fitToHeight: 0,
|
||||
margins: { left: .25, right: .25, top: .25, bottom: .25, header: 0, footer: 0 },
|
||||
horizontalCentered: true, verticalCentered: false
|
||||
};
|
||||
sheet.headerFooter = { oddFooter: '&C第 &P / &N 页' };
|
||||
const rowsPerLabel = 8;
|
||||
const labelsPerPage = 6;
|
||||
const rowsPerPage = rowsPerLabel * 3;
|
||||
const borderColor = { argb: 'FF173F60' };
|
||||
const paleBlue = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFEAF1F5' } };
|
||||
const paleGold = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFFF1C7' } };
|
||||
const mergeLine = (row, startColumn) => {
|
||||
sheet.mergeCells(row, startColumn, row, startColumn + 2);
|
||||
return sheet.getCell(row, startColumn);
|
||||
};
|
||||
const styleLine = (cell, { size = 10, bold = false, color = 'FF243B4A', fill = null } = {}) => {
|
||||
cell.font = { name: '微软雅黑', size, bold, color: { argb: color } };
|
||||
cell.alignment = { vertical: 'middle', horizontal: 'center', wrapText: true, shrinkToFit: true };
|
||||
if (fill) cell.fill = fill;
|
||||
};
|
||||
|
||||
rows.forEach((item, index) => {
|
||||
const page = Math.floor(index / labelsPerPage);
|
||||
const slot = index % labelsPerPage;
|
||||
const startRow = page * rowsPerPage + Math.floor(slot / 2) * rowsPerLabel + 1;
|
||||
const startColumn = slot % 2 === 0 ? 1 : 5;
|
||||
for (let row = startRow; row < startRow + rowsPerLabel; row += 1) sheet.getRow(row).height = 32.5;
|
||||
const cells = Array.from({ length: rowsPerLabel }, (_, offset) => mergeLine(startRow + offset, startColumn));
|
||||
cells[0].value = item.examName;
|
||||
styleLine(cells[0], { size: 11, bold: true, color: 'FFFFFFFF', fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF173F60' } } });
|
||||
cells[1].value = `${item.subjectName}|${item.subjectDate} ${item.subjectTime}`;
|
||||
styleLine(cells[1], { size: 9, bold: true, fill: paleBlue });
|
||||
cells[2].value = '考试考场序号';
|
||||
styleLine(cells[2], { size: 9, bold: true, color: 'FF2C7080' });
|
||||
cells[3].value = { richText: [{ text: String(item.examRoomCode || '') }] };
|
||||
cells[3].numFmt = '@';
|
||||
styleLine(cells[3], { size: 25, bold: true, color: 'FF173F60' });
|
||||
cells[4].value = `座位号 ${item.seat}`;
|
||||
styleLine(cells[4], { size: 20, bold: true, color: 'FF8A4B08', fill: paleGold });
|
||||
cells[5].value = `${item.roomName}(场地 ${item.roomCode})|${item.building} ${item.floor}`;
|
||||
styleLine(cells[5], { size: 9, bold: true });
|
||||
cells[6].value = `${item.candidateName}|${item.schoolName} ${item.className}`;
|
||||
styleLine(cells[6], { size: 10, bold: true, fill: paleBlue });
|
||||
cells[7].value = `${item.centerName}|准考证号 ${item.cardNumber}`;
|
||||
styleLine(cells[7], { size: 8, color: 'FF526576' });
|
||||
|
||||
for (let row = startRow; row < startRow + rowsPerLabel; row += 1) {
|
||||
for (let column = startColumn; column < startColumn + 3; column += 1) {
|
||||
const cell = sheet.getCell(row, column);
|
||||
cell.border = {
|
||||
top: { style: row === startRow ? 'medium' : 'hair', color: borderColor },
|
||||
bottom: { style: row === startRow + rowsPerLabel - 1 ? 'medium' : 'hair', color: borderColor },
|
||||
left: { style: column === startColumn ? 'medium' : 'hair', color: borderColor },
|
||||
right: { style: column === startColumn + 2 ? 'medium' : 'hair', color: borderColor }
|
||||
};
|
||||
}
|
||||
}
|
||||
if ((index + 1) % labelsPerPage === 0 && index + 1 < rows.length) sheet.getRow(startRow + rowsPerLabel - 1).addPageBreak();
|
||||
});
|
||||
if (rows.length) sheet.pageSetup.printArea = `A1:G${Math.ceil(rows.length / labelsPerPage) * rowsPerPage}`;
|
||||
return sheet;
|
||||
}
|
||||
|
||||
export async function buildCenterMaterialsWorkbook(rows, subtitle = '') {
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
workbook.creator = '衡准考试信息管理系统';
|
||||
workbook.created = new Date();
|
||||
const sorted = [...rows].sort((a, b) => String(a.centerCode).localeCompare(String(b.centerCode))
|
||||
|| String(a.subjectDate).localeCompare(String(b.subjectDate)) || String(a.subjectTime).localeCompare(String(b.subjectTime))
|
||||
|| String(a.examRoomCode).localeCompare(String(b.examRoomCode)) || String(a.seat).localeCompare(String(b.seat)));
|
||||
const baseTitle = subtitle ? `|${subtitle}` : '';
|
||||
addDeskStickerSheet(workbook, sorted);
|
||||
|
||||
const doorMap = new Map();
|
||||
for (const item of sorted) {
|
||||
const key = [item.examCode, item.subjectName, item.centerCode, item.examRoomCode, item.roomCode].join('|');
|
||||
const current = doorMap.get(key) || { ...item, candidateCount: 0 };
|
||||
current.candidateCount += 1;
|
||||
doorMap.set(key, current);
|
||||
}
|
||||
addOperationalSheet(workbook, '门贴', `考场门贴${baseTitle}`, [
|
||||
['examName', '考试', 28], ['subjectName', '科目', 14], ['subjectDate', '日期', 14], ['subjectTime', '时间', 16],
|
||||
['centerName', '考点', 24], ['examRoomCode', '考试考场序号', 16], ['roomName', '考场通用名称', 20],
|
||||
['roomCode', '场地代码', 14], ['building', '楼栋', 16], ['floor', '楼层', 10], ['candidateCount', '人数', 10]
|
||||
], [...doorMap.values()]);
|
||||
|
||||
addOperationalSheet(workbook, '考场签名单', `考场考生签名单${baseTitle}`, [
|
||||
['examName', '考试', 26], ['subjectName', '科目', 14], ['subjectDate', '日期', 14], ['subjectTime', '时间', 16],
|
||||
['examRoomCode', '考试考场序号', 16], ['roomName', '考场通用名称', 20], ['building', '楼栋', 16], ['floor', '楼层', 10],
|
||||
['seat', '座位号', 10], ['cardNumber', '准考证号', 22], ['candidateName', '姓名', 14],
|
||||
['schoolName', '考生学校', 24], ['className', '班级', 18], ['signature', '考生签名', 18]
|
||||
], sorted.map(item => ({ ...item, signature: '' })));
|
||||
return workbook.xlsx.writeBuffer();
|
||||
}
|
||||
Loaded 3 of 13 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user