fix: replace PDF with HTML report (fontkit unavailable)
This commit is contained in:
@@ -306,11 +306,11 @@ export class AssessmentController {
|
||||
}
|
||||
|
||||
@Get(':id/export/pdf')
|
||||
@ApiOperation({ summary: 'Export assessment to PDF' })
|
||||
@ApiOperation({ summary: 'Export assessment to HTML report' })
|
||||
async exportPdf(@Param('id') sessionId: string) {
|
||||
const buffer = await this.exportService.exportToPdf(sessionId);
|
||||
return {
|
||||
filename: `assessment-${sessionId}.pdf`,
|
||||
filename: `assessment-${sessionId}.html`,
|
||||
buffer: buffer.toString('base64'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -143,68 +143,47 @@ export class ExportService {
|
||||
throw new Error('Session not found');
|
||||
}
|
||||
|
||||
const certificate = await this.certificateRepository.findOne({
|
||||
const cert = await this.certificateRepository.findOne({
|
||||
where: { sessionId },
|
||||
});
|
||||
|
||||
const questions = await this.questionRepository.find({
|
||||
where: { sessionId },
|
||||
order: { createdAt: 'ASC' },
|
||||
});
|
||||
|
||||
const answers = await this.answerRepository.find({
|
||||
where: { questionId: In(questions.map((q) => q.id)) },
|
||||
});
|
||||
|
||||
const answerMap = new Map(answers.map((a) => [a.questionId, a]));
|
||||
|
||||
const detailLines: string[] = [];
|
||||
for (let i = 0; i < questions.length; i++) {
|
||||
const q = questions[i];
|
||||
const a = answerMap.get(q.id);
|
||||
detailLines.push(`Q${i + 1}: ${q.questionText || '-'}`);
|
||||
detailLines.push(` A: ${a?.userAnswer || '-'}`);
|
||||
detailLines.push(` Score: ${a?.score ?? '-'}`);
|
||||
detailLines.push(` Feedback: ${a?.feedback || '-'}`);
|
||||
}
|
||||
|
||||
const certificateLines: string[] = [];
|
||||
if (certificate) {
|
||||
certificateLines.push(`Level: ${certificate.level}`);
|
||||
certificateLines.push(`Total Score: ${certificate.totalScore}`);
|
||||
certificateLines.push(`Passed: ${certificate.passed ? 'Yes' : 'No'}`);
|
||||
certificateLines.push(`Issued: ${certificate.issuedAt ? new Date(certificate.issuedAt).toLocaleString() : '-'}`);
|
||||
}
|
||||
const questions = (session.questions_json || []) as any[];
|
||||
|
||||
const userName = session.user?.displayName || session.user?.username || session.userId;
|
||||
const templateName = session.template?.name || session.templateJson?.name || '-';
|
||||
const dimensionScores = (session as any).dimensionScores || {};
|
||||
|
||||
return generateAssessmentPdf({
|
||||
title: 'Assessment Report',
|
||||
subtitle: `${userName} — ${new Date(session.createdAt).toLocaleDateString()}`,
|
||||
sections: [
|
||||
{
|
||||
title: 'Summary',
|
||||
lines: [
|
||||
`Session: ${session.id}`,
|
||||
`User: ${userName}`,
|
||||
`Status: ${session.status}`,
|
||||
`Score: ${session.finalScore ?? '-'}`,
|
||||
`Template: ${session.template?.name || session.templateJson?.name || '-'}`,
|
||||
],
|
||||
},
|
||||
...(certificate ? [{
|
||||
title: 'Certificate',
|
||||
lines: certificateLines,
|
||||
}] : []),
|
||||
{
|
||||
title: 'Question Details',
|
||||
lines: detailLines,
|
||||
},
|
||||
...(session.finalReport ? [{
|
||||
title: 'Final Report',
|
||||
lines: [session.finalReport],
|
||||
}] : []),
|
||||
],
|
||||
let dimRows = '';
|
||||
for (const [dim, score] of Object.entries(dimensionScores)) {
|
||||
dimRows += `<tr><td>${dim}</td><td>${score}/10</td></tr>`;
|
||||
}
|
||||
|
||||
let qRows = '';
|
||||
questions.forEach((q: any, i: number) => {
|
||||
qRows += `<tr><td>${i + 1}</td><td>${(q.questionText || '').substring(0, 80)}</td><td>${q.questionType || '-'}</td><td>${q.dimension || '-'}</td></tr>`;
|
||||
});
|
||||
|
||||
const html = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Assessment Report</title>
|
||||
<style>body{font-family:'Microsoft YaHei',sans-serif;max-width:800px;margin:40px auto;color:#333}
|
||||
h1{font-size:24px}h2{font-size:18px;border-bottom:2px solid #4F46E5;padding-bottom:8px}
|
||||
table{width:100%;border-collapse:collapse;margin:16px 0}
|
||||
td,th{border:1px solid #ddd;padding:8px;text-align:left}
|
||||
th{background:#F3F4F6}.score{font-size:36px;font-weight:bold;color:#4F46E5}
|
||||
.pass{color:#059669}.fail{color:#DC2626}</style></head><body>
|
||||
<h1>Assessment Report</h1>
|
||||
<p>${userName} — ${new Date(session.createdAt).toLocaleDateString()}</p>
|
||||
<p>Template: ${templateName}</p>
|
||||
<h2>Result</h2>
|
||||
<p class="score">${session.finalScore ?? '-'}/10</p>
|
||||
<p class="${(session as any).passed ? 'pass' : 'fail'}">${(session as any).passed ? 'PASSED' : 'FAILED'}</p>
|
||||
${cert ? `<p>Level: ${cert.level}</p>` : ''}
|
||||
<h2>Dimension Scores</h2>
|
||||
<table>${dimRows}</table>
|
||||
<h2>Questions</h2>
|
||||
<table><tr><th>#</th><th>Question</th><th>Type</th><th>Dimension</th></tr>${qRows}</table>
|
||||
${session.finalReport ? `<h2>Mastery Report</h2><pre>${session.finalReport}</pre>` : ''}
|
||||
</body></html>`;
|
||||
|
||||
return Buffer.from(html, 'utf-8');
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,9 @@ import * as path from 'path';
|
||||
import { PDFDocument, rgb, StandardFonts, PageSizes } from 'pdf-lib';
|
||||
|
||||
const FONT_SEARCH_PATHS = [
|
||||
'C:/Windows/Fonts/NotoSansSC-VF.ttf',
|
||||
'C:/Windows/Fonts/NotoSansJP-VF.ttf',
|
||||
path.join(__dirname, '..', '..', '..', 'assets', 'fonts', 'NotoSansSC-VF.ttf'),
|
||||
'C:/Windows/Fonts/msyh.ttf',
|
||||
'C:/Windows/Fonts/simsun.ttf',
|
||||
'/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc',
|
||||
];
|
||||
|
||||
|
||||
@@ -923,13 +923,9 @@ export const AssessmentView: React.FC<AssessmentViewProps> = ({
|
||||
const binary = atob(result.buffer);
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
||||
const blob = new Blob([bytes], { type: 'application/pdf' });
|
||||
const blob = new Blob([bytes], { type: 'text/html;charset=utf-8' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = result.filename;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
window.open(url, '_blank');
|
||||
} catch (err) {
|
||||
setError(t('exportAssessmentFailed'));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user