import jsPDF from 'jspdf'; import 'jspdf-autotable'; import html2canvas from 'html2canvas'; export interface PDFReportData { projectTitle: string; overallScore: number; totalPossible: number; grade: string; analysisDate: string; criteria: Array<{ name: string; score: number; maxScore: number; weight: number; weightedScore: number; percentage: number; feedback: string; strengths: string[]; improvements: string[]; }>; overview: { excellentItems: number; improvementItems: number; overallPerformance: number; }; improvementSuggestions: { overallSuggestions: string; maintainStrengths: Array<{ title: string; description: string; }>; keyImprovements: Array<{ title: string; subtitle?: string; description?: string; suggestions: string[]; }>; actionPlan: Array<{ phase: string; description: string; }>; }; } export class PDFReportGenerator { private doc: jsPDF; private currentY: number = 20; private pageHeight: number = 280; private margin: number = 20; constructor() { this.doc = new jsPDF('p', 'mm', 'a4'); // 設置支援中文的默認字體 this.doc.setFont('helvetica'); } private addNewPageIfNeeded(requiredSpace: number = 20): void { if (this.currentY + requiredSpace > this.pageHeight) { this.doc.addPage(); this.currentY = 20; } } private addTitle(text: string, fontSize: number = 16, isBold: boolean = true): void { this.addNewPageIfNeeded(10); this.doc.setFontSize(fontSize); this.doc.setFont('helvetica', isBold ? 'bold' : 'normal'); this.doc.text(text, this.margin, this.currentY); this.currentY += fontSize + 5; } private addSubtitle(text: string, fontSize: number = 12): void { this.addNewPageIfNeeded(8); this.doc.setFontSize(fontSize); this.doc.setFont('helvetica', 'bold'); this.doc.text(text, this.margin, this.currentY); this.currentY += fontSize + 3; } private addText(text: string, fontSize: number = 10, isBold: boolean = false): void { this.addNewPageIfNeeded(6); this.doc.setFontSize(fontSize); this.doc.setFont('helvetica', isBold ? 'bold' : 'normal'); // 處理長文本換行 const maxWidth = 170; // A4 寬度減去邊距 const lines = this.doc.splitTextToSize(text, maxWidth); for (const line of lines) { this.addNewPageIfNeeded(6); this.doc.text(line, this.margin, this.currentY); this.currentY += 6; } } private addBulletList(items: string[], fontSize: number = 10): void { for (const item of items) { this.addNewPageIfNeeded(6); this.doc.setFontSize(fontSize); this.doc.setFont('helvetica', 'normal'); this.doc.text(`• ${item}`, this.margin + 5, this.currentY); this.currentY += 6; } } private addScoreBox(score: number, total: number, grade: string): void { this.addNewPageIfNeeded(25); // 繪製分數框 this.doc.setFillColor(240, 240, 240); this.doc.rect(this.margin, this.currentY, 50, 20, 'F'); // 分數文字 this.doc.setFontSize(18); this.doc.setFont('helvetica', 'bold'); this.doc.text(`${score}/${total}`, this.margin + 5, this.currentY + 12); // 等級 this.doc.setFontSize(12); this.doc.text(grade, this.margin + 5, this.currentY + 18); this.currentY += 25; } private addCriteriaTable(criteria: PDFReportData['criteria']): void { this.addNewPageIfNeeded(30); // 表頭 this.doc.setFontSize(10); this.doc.setFont('helvetica', 'bold'); this.doc.text('評分項目', this.margin, this.currentY); this.doc.text('得分', this.margin + 100, this.currentY); this.doc.text('權重', this.margin + 130, this.currentY); this.doc.text('加權分', this.margin + 150, this.currentY); this.currentY += 8; // 分隔線 this.doc.line(this.margin, this.currentY, this.margin + 170, this.currentY); this.currentY += 5; // 數據行 for (const item of criteria) { this.addNewPageIfNeeded(15); this.doc.setFont('helvetica', 'normal'); this.doc.text(item.name, this.margin, this.currentY); this.doc.text(`${item.score}/${item.maxScore}`, this.margin + 100, this.currentY); this.doc.text(`${item.weight}%`, this.margin + 130, this.currentY); this.doc.text(item.weightedScore.toFixed(1), this.margin + 150, this.currentY); this.currentY += 6; } this.currentY += 10; } public generateReport(data: PDFReportData): Promise { return new Promise((resolve, reject) => { try { // 標題頁 this.addTitle('AI 智能評審報告', 20); this.addText(`專案名稱:${data.projectTitle}`, 12, true); this.addText(`分析日期:${data.analysisDate}`, 12); this.currentY += 10; // 總分顯示 this.addSubtitle('總評結果'); this.addScoreBox(data.overallScore, data.totalPossible, data.grade); // 統計概覽 this.addSubtitle('統計概覽'); this.addText(`優秀項目:${data.overview.excellentItems} 項`); this.addText(`待改進項目:${data.overview.improvementItems} 項`); this.addText(`整體表現:${data.overview.overallPerformance}%`); this.currentY += 10; // 評分明細 this.addSubtitle('評分明細'); this.addCriteriaTable(data.criteria); // 詳細分析 this.addSubtitle('詳細分析'); for (const item of data.criteria) { this.addNewPageIfNeeded(20); this.doc.setFontSize(11); this.doc.setFont('helvetica', 'bold'); this.doc.text(item.name, this.margin, this.currentY); this.currentY += 8; this.addText(`得分:${item.score}/${item.maxScore} (${item.percentage.toFixed(1)}%)`, 10); this.addText(`AI 評語:${item.feedback}`, 10); if (item.strengths.length > 0) { this.addText('優點:', 10, true); this.addBulletList(item.strengths, 9); } if (item.improvements.length > 0) { this.addText('改進建議:', 10, true); this.addBulletList(item.improvements, 9); } this.currentY += 10; } // 改進建議 this.addSubtitle('整體改進建議'); this.addText(data.improvementSuggestions.overallSuggestions, 10); this.currentY += 10; // 保持的優勢 if (data.improvementSuggestions.maintainStrengths.length > 0) { this.addSubtitle('繼續保持的優勢'); for (const strength of data.improvementSuggestions.maintainStrengths) { this.addText(strength.title, 10, true); this.addText(strength.description, 9); this.currentY += 5; } } // 重點改進方向 if (data.improvementSuggestions.keyImprovements.length > 0) { this.addSubtitle('重點改進方向'); for (const improvement of data.improvementSuggestions.keyImprovements) { this.addText(improvement.title, 10, true); if (improvement.description) { this.addText(improvement.description, 9); } if (improvement.suggestions.length > 0) { this.addBulletList(improvement.suggestions, 9); } this.currentY += 5; } } // 行動計劃 if (data.improvementSuggestions.actionPlan.length > 0) { this.addSubtitle('下一步行動計劃'); for (let i = 0; i < data.improvementSuggestions.actionPlan.length; i++) { const action = data.improvementSuggestions.actionPlan[i]; this.addText(`${i + 1}. ${action.phase}`, 10, true); this.addText(action.description, 9); this.currentY += 5; } } // 頁腳 this.doc.setFontSize(8); this.doc.setFont('helvetica', 'normal'); this.doc.text('本報告由 AI 智能評審系統生成', this.margin, this.pageHeight - 10); this.doc.text(`生成時間:${new Date().toLocaleString('zh-TW')}`, this.margin + 100, this.pageHeight - 10); // 生成 PDF Blob const pdfBlob = this.doc.output('blob'); resolve(pdfBlob); } catch (error) { reject(error); } }); } } // 便捷函數 export async function generatePDFReport(data: PDFReportData): Promise { const generator = new PDFReportGenerator(); return generator.generateReport(data); }