import jsPDF from 'jspdf'; import 'jspdf-autotable'; 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 ChinesePDFReportGenerator { private doc: jsPDF; private currentY: number = 20; private pageHeight: number = 280; private margin: number = 20; constructor() { this.doc = new jsPDF('p', 'mm', 'a4'); } 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'); // 將中文轉換為可顯示的格式 const displayText = this.convertChineseText(text); this.doc.text(displayText, 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'); const displayText = this.convertChineseText(text); this.doc.text(displayText, 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 displayText = this.convertChineseText(text); // 處理長文本換行 const maxWidth = 170; const lines = this.doc.splitTextToSize(displayText, 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'); const displayText = this.convertChineseText(item); this.doc.text(`• ${displayText}`, 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); // 準備表格數據 const tableData = criteria.map(item => [ this.convertChineseText(item.name), `${item.score}/${item.maxScore}`, `${item.weight}%`, item.weightedScore.toFixed(1) ]); // 使用 autoTable 插件創建表格 (this.doc as any).autoTable({ head: [['評分項目', '得分', '權重', '加權分']], body: tableData, startY: this.currentY, margin: { left: this.margin, right: this.margin }, styles: { fontSize: 10 }, headStyles: { fillColor: [66, 139, 202] }, alternateRowStyles: { fillColor: [245, 245, 245] } }); // 更新當前 Y 位置 this.currentY = (this.doc as any).lastAutoTable.finalY + 10; } // 將中文字符轉換為可顯示的格式 private convertChineseText(text: string): string { // 簡單的字符替換,將常見的中文字符替換為英文描述 const chineseMap: { [key: string]: string } = { '評審結果': 'Evaluation Results', 'AI 智能評審報告': 'AI Intelligent Evaluation Report', '專案名稱': 'Project Name', '分析日期': 'Analysis Date', '總評結果': 'Overall Results', '統計概覽': 'Statistical Overview', '優秀項目': 'Excellent Items', '待改進項目': 'Items to Improve', '整體表現': 'Overall Performance', '評分明細': 'Score Details', '評分項目': 'Evaluation Items', '得分': 'Score', '權重': 'Weight', '加權分': 'Weighted Score', '詳細分析': 'Detailed Analysis', 'AI 評語': 'AI Comments', '優點': 'Strengths', '改進建議': 'Improvement Suggestions', '整體改進建議': 'Overall Improvement Suggestions', '繼續保持的優勢': 'Maintain Strengths', '重點改進方向': 'Key Improvement Areas', '下一步行動計劃': 'Next Action Plan', '本報告由 AI 智能評審系統生成': 'Generated by AI Intelligent Evaluation System', '生成時間': 'Generated Time', '內容品質': 'Content Quality', '視覺設計': 'Visual Design', '邏輯結構': 'Logical Structure', '創新性': 'Innovation', '實用性': 'Practicality' }; let result = text; // 替換已知的中文詞彙 for (const [chinese, english] of Object.entries(chineseMap)) { result = result.replace(new RegExp(chinese, 'g'), english); } // 對於其他中文字符,使用 Unicode 轉換 result = result.replace(/[\u4e00-\u9fff]/g, (char) => { const code = char.charCodeAt(0); return `[U+${code.toString(16).toUpperCase()}]`; }); return result; } public generateReport(data: PDFReportData): Promise { return new Promise((resolve, reject) => { try { // 標題頁 this.addTitle('AI Intelligent Evaluation Report', 20); this.addText(`Project Name: ${data.projectTitle}`, 12, true); this.addText(`Analysis Date: ${data.analysisDate}`, 12); this.currentY += 10; // 總分顯示 this.addSubtitle('Overall Results'); this.addScoreBox(data.overallScore, data.totalPossible, data.grade); // 統計概覽 this.addSubtitle('Statistical Overview'); this.addText(`Excellent Items: ${data.overview.excellentItems} items`); this.addText(`Items to Improve: ${data.overview.improvementItems} items`); this.addText(`Overall Performance: ${data.overview.overallPerformance}%`); this.currentY += 10; // 評分明細 this.addSubtitle('Score Details'); this.addCriteriaTable(data.criteria); // 詳細分析 this.addSubtitle('Detailed Analysis'); for (const item of data.criteria) { this.addNewPageIfNeeded(20); this.doc.setFontSize(11); this.doc.setFont('helvetica', 'bold'); this.doc.text(this.convertChineseText(item.name), this.margin, this.currentY); this.currentY += 8; this.addText(`Score: ${item.score}/${item.maxScore} (${item.percentage.toFixed(1)}%)`, 10); this.addText(`AI Comments: ${item.feedback}`, 10); if (item.strengths.length > 0) { this.addText('Strengths:', 10, true); this.addBulletList(item.strengths, 9); } if (item.improvements.length > 0) { this.addText('Improvement Suggestions:', 10, true); this.addBulletList(item.improvements, 9); } this.currentY += 10; } // 改進建議 this.addSubtitle('Overall Improvement Suggestions'); this.addText(data.improvementSuggestions.overallSuggestions, 10); this.currentY += 10; // 保持的優勢 if (data.improvementSuggestions.maintainStrengths.length > 0) { this.addSubtitle('Maintain Strengths'); 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('Key Improvement Areas'); 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('Next Action Plan'); 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('Generated by AI Intelligent Evaluation System', this.margin, this.pageHeight - 10); this.doc.text(`Generated Time: ${new Date().toLocaleString('en-US')}`, this.margin + 100, this.pageHeight - 10); // 生成 PDF Blob const pdfBlob = this.doc.output('blob'); resolve(pdfBlob); } catch (error) { reject(error); } }); } } // 便捷函數 export async function generateChinesePDFReport(data: PDFReportData): Promise { const generator = new ChinesePDFReportGenerator(); return generator.generateReport(data); }