From a83de861dd5649ac14342f380c6964eb6bef51cb Mon Sep 17 00:00:00 2001 From: Developer Date: Thu, 21 May 2026 16:30:36 +0800 Subject: [PATCH] fix: replace PDF with HTML report (fontkit unavailable) --- .../src/assessment/assessment.controller.ts | 4 +- .../src/assessment/services/export.service.ts | 91 +++++++------------ .../src/assessment/services/pdf-generator.ts | 4 +- web/components/views/AssessmentView.tsx | 8 +- 4 files changed, 41 insertions(+), 66 deletions(-) diff --git a/server/src/assessment/assessment.controller.ts b/server/src/assessment/assessment.controller.ts index 827d0bc..59f51f4 100644 --- a/server/src/assessment/assessment.controller.ts +++ b/server/src/assessment/assessment.controller.ts @@ -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'), }; } diff --git a/server/src/assessment/services/export.service.ts b/server/src/assessment/services/export.service.ts index 8c7a62b..1e3adae 100644 --- a/server/src/assessment/services/export.service.ts +++ b/server/src/assessment/services/export.service.ts @@ -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 += `${dim}${score}/10`; + } + + let qRows = ''; + questions.forEach((q: any, i: number) => { + qRows += `${i + 1}${(q.questionText || '').substring(0, 80)}${q.questionType || '-'}${q.dimension || '-'}`; }); + + const html = `Assessment Report + +

Assessment Report

+

${userName} — ${new Date(session.createdAt).toLocaleDateString()}

+

Template: ${templateName}

+

Result

+

${session.finalScore ?? '-'}/10

+

${(session as any).passed ? 'PASSED' : 'FAILED'}

+${cert ? `

Level: ${cert.level}

` : ''} +

Dimension Scores

+${dimRows}
+

Questions

+${qRows}
#QuestionTypeDimension
+${session.finalReport ? `

Mastery Report

${session.finalReport}
` : ''} +`; + + return Buffer.from(html, 'utf-8'); } } \ No newline at end of file diff --git a/server/src/assessment/services/pdf-generator.ts b/server/src/assessment/services/pdf-generator.ts index fef869e..f1bbcca 100644 --- a/server/src/assessment/services/pdf-generator.ts +++ b/server/src/assessment/services/pdf-generator.ts @@ -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', ]; diff --git a/web/components/views/AssessmentView.tsx b/web/components/views/AssessmentView.tsx index f3516d9..11d4ae3 100644 --- a/web/components/views/AssessmentView.tsx +++ b/web/components/views/AssessmentView.tsx @@ -923,13 +923,9 @@ export const AssessmentView: React.FC = ({ 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')); }