/**
* PDF格式生成器
* 将分析结果转换为PDF文档
* 使用puppeteer进行HTML到PDF的转换
*/
const puppeteer = require('puppeteer');
const { generateMarkdown } = require('./markdownGenerator.cjs');
const generatePDF = async (analysisData, analysisType, userName) => {
let browser;
try {
// 生成Markdown内容
const markdownBuffer = await generateMarkdown(analysisData, analysisType, userName);
// 将Buffer转换为字符串
const markdownString = Buffer.isBuffer(markdownBuffer) ? markdownBuffer.toString('utf8') : String(markdownBuffer);
// 将Markdown转换为HTML
const htmlContent = convertMarkdownToHTML(markdownString, analysisType, userName);
// 启动puppeteer浏览器
browser = await puppeteer.launch({
headless: 'new',
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-gpu',
'--no-first-run',
'--disable-extensions',
'--disable-plugins',
'--disable-images',
'--disable-javascript',
'--run-all-compositor-stages-before-draw',
'--disable-background-timer-throttling',
'--disable-renderer-backgrounding',
'--disable-backgrounding-occluded-windows',
'--disable-ipc-flooding-protection'
],
timeout: 30000
});
const page = await browser.newPage();
// 设置页面内容
await page.setContent(htmlContent, {
waitUntil: 'networkidle0'
});
// 生成PDF
const pdfBuffer = await page.pdf({
format: 'A4',
margin: {
top: '20mm',
right: '15mm',
bottom: '20mm',
left: '15mm'
},
printBackground: true,
preferCSSPageSize: true
});
// 确保返回的是Buffer对象
if (!Buffer.isBuffer(pdfBuffer)) {
console.warn('Puppeteer返回的不是Buffer,正在转换:', typeof pdfBuffer);
return Buffer.from(pdfBuffer);
}
return pdfBuffer;
} catch (error) {
console.error('生成PDF失败:', error);
throw error;
} finally {
if (browser) {
await browser.close();
}
}
};
/**
* 将Markdown内容转换为适合PDF的HTML
*/
const convertMarkdownToHTML = (markdownContent, analysisType, userName) => {
// 预处理:分离表格
const lines = markdownContent.split('\n');
let html = '';
let inTable = false;
let tableRows = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// 检测表格开始
if (line.includes('|') && line.includes('---')) {
inTable = true;
// 添加表格头(前一行)
if (i > 0 && lines[i-1].includes('|')) {
const headerCells = lines[i-1].split('|').map(cell => cell.trim()).filter(cell => cell);
tableRows.push('
' + headerCells.map(cell => `${cell} `).join('') + ' ');
}
continue;
}
// 处理表格行
if (inTable && line.includes('|')) {
const cells = line.split('|').map(cell => cell.trim()).filter(cell => cell);
if (cells.length > 0) {
tableRows.push('' + cells.map(cell => `${cell} `).join('') + ' ');
}
continue;
}
// 表格结束
if (inTable && !line.includes('|')) {
html += '' + tableRows.join('') + '
\n';
tableRows = [];
inTable = false;
}
// 处理非表格行
if (!inTable) {
html += line + '\n';
}
}
// 处理未结束的表格
if (tableRows.length > 0) {
html += '' + tableRows.join('') + '
\n';
}
// Markdown到HTML转换
html = html
// 标题转换
.replace(/^# (.+)$/gm, '$1 ')
.replace(/^## (.+)$/gm, '$1 ')
.replace(/^### (.+)$/gm, '$1 ')
.replace(/^#### (.+)$/gm, '$1 ')
// 加粗文本
.replace(/\*\*(.+?)\*\*/g, '$1 ')
// 处理列表
.replace(/^- (.+)$/gm, '$1 ')
// 将连续的li包装在ul中
.replace(/(.*<\/li>\s*)+/gs, (match) => {
return '';
})
// 水平分割线
.replace(/^---$/gm, ' ')
// 段落处理
.replace(/\n\s*\n/g, '')
.replace(/^(?!<[h1-6]|
$1')
// 清理多余的p标签
.replace(/<\/p>/g, '')
.replace(/
(<[^>]+>)/g, '$1')
.replace(/(<\/[^>]+>)<\/p>/g, '$1')
// 换行处理
.replace(/\n/g, '');
// 包装在完整的HTML文档中
return `
${getAnalysisTypeLabel(analysisType)}分析报告
${html}
`;
};
/**
* 生成HTML内容
*/
const generateHTML = (analysisData, analysisType, userName) => {
const timestamp = new Date().toLocaleString('zh-CN');
let html = `
${getAnalysisTypeLabel(analysisType)}分析报告
`;
// 根据分析类型生成不同的HTML内容
switch (analysisType) {
case 'bazi':
html += generateBaziHTML(analysisData);
break;
case 'ziwei':
html += generateZiweiHTML(analysisData);
break;
case 'yijing':
html += generateYijingHTML(analysisData);
break;
}
html += `
`;
return html;
};
/**
* 生成八字命理HTML内容
*/
const generateBaziHTML = (analysisData) => {
let html = '';
// 基本信息
if (analysisData.basic_info) {
html += `
📋 基本信息
`;
if (analysisData.basic_info.personal_data) {
const personal = analysisData.basic_info.personal_data;
html += `
姓名:
${personal.name || '未提供'}
性别:
${personal.gender === 'male' ? '男' : personal.gender === 'female' ? '女' : personal.gender || '未提供'}
出生日期:
${personal.birth_date || '未提供'}
出生时间:
${personal.birth_time || '未提供'}
`;
if (personal.birth_place) {
html += `
出生地点:
${personal.birth_place}
`;
}
}
html += `
`;
// 八字信息
if (analysisData.basic_info.bazi_info) {
const bazi = analysisData.basic_info.bazi_info;
html += `
🔮 八字信息
柱位
天干
地支
纳音
年柱
${bazi.year?.split('')[0] || '-'}
${bazi.year?.split('')[1] || '-'}
${bazi.year_nayin || '-'}
月柱
${bazi.month?.split('')[0] || '-'}
${bazi.month?.split('')[1] || '-'}
${bazi.month_nayin || '-'}
日柱
${bazi.day?.split('')[0] || '-'}
${bazi.day?.split('')[1] || '-'}
${bazi.day_nayin || '-'}
时柱
${bazi.hour?.split('')[0] || '-'}
${bazi.hour?.split('')[1] || '-'}
${bazi.hour_nayin || '-'}
`;
}
html += `
`;
}
// 五行分析
if (analysisData.wuxing_analysis) {
html += `
🌟 五行分析
`;
if (analysisData.wuxing_analysis.element_distribution) {
html += `
五行分布
五行
数量
占比
强度
`;
const elements = analysisData.wuxing_analysis.element_distribution;
const total = Object.values(elements).reduce((sum, count) => sum + (typeof count === 'number' ? count : 0), 0);
Object.entries(elements).forEach(([element, count]) => {
const numCount = typeof count === 'number' ? count : 0;
const percentage = total > 0 ? Math.round((numCount / total) * 100) : 0;
const strength = numCount >= 3 ? '旺' : numCount >= 2 ? '中' : '弱';
html += `
${element}
${numCount}
${percentage}%
${strength}
`;
});
html += `
`;
}
if (analysisData.wuxing_analysis.balance_analysis) {
html += `
五行平衡分析
${analysisData.wuxing_analysis.balance_analysis}
`;
}
if (analysisData.wuxing_analysis.suggestions) {
html += `
调和建议
${analysisData.wuxing_analysis.suggestions}
`;
}
html += `
`;
}
// 格局分析
if (analysisData.pattern_analysis) {
html += `
🎯 格局分析
`;
if (analysisData.pattern_analysis.main_pattern) {
html += `
主要格局:
${analysisData.pattern_analysis.main_pattern}
`;
}
if (analysisData.pattern_analysis.pattern_strength) {
const strength = analysisData.pattern_analysis.pattern_strength;
const strengthLabel = strength === 'strong' ? '强' : strength === 'moderate' ? '中等' : strength === 'fair' ? '一般' : '较弱';
html += `
格局强度:
${strengthLabel}
`;
}
if (analysisData.pattern_analysis.analysis) {
html += `
格局详解
${analysisData.pattern_analysis.analysis}
`;
}
html += `
`;
}
// 人生指导
if (analysisData.life_guidance) {
html += `
🌟 人生指导
`;
if (analysisData.life_guidance.strengths) {
html += `
优势特质
`;
if (Array.isArray(analysisData.life_guidance.strengths)) {
html += '
';
analysisData.life_guidance.strengths.forEach(strength => {
html += `${strength} `;
});
html += ' ';
} else {
html += `
${analysisData.life_guidance.strengths}
`;
}
html += `
`;
}
if (analysisData.life_guidance.challenges) {
html += `
需要注意
`;
if (Array.isArray(analysisData.life_guidance.challenges)) {
html += '
';
analysisData.life_guidance.challenges.forEach(challenge => {
html += `${challenge} `;
});
html += ' ';
} else {
html += `
${analysisData.life_guidance.challenges}
`;
}
html += `
`;
}
if (analysisData.life_guidance.overall_summary) {
html += `
综合总结
${analysisData.life_guidance.overall_summary}
`;
}
html += `
`;
}
return html;
};
/**
* 生成紫微斗数HTML内容
*/
const generateZiweiHTML = (analysisData) => {
let html = '';
// 基本信息
if (analysisData.basic_info) {
html += `
📋 基本信息
`;
if (analysisData.basic_info.personal_data) {
const personal = analysisData.basic_info.personal_data;
html += `
姓名:
${personal.name || '未提供'}
性别:
${personal.gender === 'male' ? '男' : personal.gender === 'female' ? '女' : personal.gender || '未提供'}
出生日期:
${personal.birth_date || '未提供'}
出生时间:
${personal.birth_time || '未提供'}
`;
}
// 紫微基本信息
if (analysisData.basic_info.ziwei_info) {
const ziwei = analysisData.basic_info.ziwei_info;
if (ziwei.ming_gong) {
html += `
命宫:
${ziwei.ming_gong}
`;
}
if (ziwei.wuxing_ju) {
html += `
五行局:
${ziwei.wuxing_ju}
`;
}
if (ziwei.main_stars) {
html += `
主星:
${Array.isArray(ziwei.main_stars) ? ziwei.main_stars.join('、') : ziwei.main_stars}
`;
}
}
html += `
`;
}
// 星曜分析
if (analysisData.star_analysis) {
html += `
⭐ 星曜分析
`;
if (analysisData.star_analysis.main_stars) {
html += `
主星分析
`;
if (Array.isArray(analysisData.star_analysis.main_stars)) {
analysisData.star_analysis.main_stars.forEach(star => {
if (typeof star === 'object') {
html += `
${star.name || star.star}
`;
if (star.brightness) {
html += `
亮度: ${star.brightness}
`;
}
if (star.influence) {
html += `
影响: ${star.influence}
`;
}
if (star.description) {
html += `
特质: ${star.description}
`;
}
html += `
`;
}
});
} else {
html += `
${analysisData.star_analysis.main_stars}
`;
}
html += `
`;
}
html += `
`;
}
return html;
};
/**
* 生成易经占卜HTML内容
*/
const generateYijingHTML = (analysisData) => {
let html = '';
// 占卜问题
if (analysisData.question_analysis) {
html += `
❓ 占卜问题
`;
if (analysisData.question_analysis.original_question) {
html += `
问题:
${analysisData.question_analysis.original_question}
`;
}
if (analysisData.question_analysis.question_type) {
html += `
问题类型:
${analysisData.question_analysis.question_type}
`;
}
html += `
`;
}
// 卦象信息
if (analysisData.hexagram_info) {
html += `
🔮 卦象信息
`;
if (analysisData.hexagram_info.main_hexagram) {
const main = analysisData.hexagram_info.main_hexagram;
html += `
主卦
卦名:
${main.name || '未知'}
卦象:
${main.symbol || ''}
`;
if (main.number) {
html += `
卦序:
第${main.number}卦
`;
}
if (main.meaning) {
html += `
含义:
${main.meaning}
`;
}
html += `
`;
}
html += `
`;
}
return html;
};
/**
* 获取分析类型标签
*/
const getAnalysisTypeLabel = (analysisType) => {
switch (analysisType) {
case 'bazi': return '八字命理';
case 'ziwei': return '紫微斗数';
case 'yijing': return '易经占卜';
default: return '命理';
}
};
/**
* 获取PDF专用CSS样式
*/
const getPDFCSS = () => {
return `
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', '微软雅黑', 'SimSun', '宋体', Arial, sans-serif;
line-height: 1.6;
color: #333;
background-color: white;
font-size: 16px;
}
.container {
max-width: 100%;
margin: 0;
padding: 0;
}
h1 {
font-size: 32px;
color: #2c3e50;
text-align: center;
margin: 20px 0;
padding: 15px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 8px;
}
h2 {
font-size: 24px;
color: #34495e;
margin: 20px 0 10px 0;
padding: 10px 0;
border-bottom: 2px solid #3498db;
page-break-after: avoid;
}
h3 {
font-size: 20px;
color: #2980b9;
margin: 15px 0 8px 0;
padding-left: 10px;
page-break-after: avoid;
}
h4 {
font-size: 18px;
color: #27ae60;
margin: 12px 0 6px 0;
page-break-after: avoid;
}
p {
margin: 8px 0;
line-height: 1.6;
text-align: justify;
}
strong {
color: #2c3e50;
font-weight: bold;
}
ul, ol {
margin: 10px 0;
padding-left: 20px;
}
li {
margin: 4px 0;
line-height: 1.5;
}
table {
width: 100%;
border-collapse: collapse;
margin: 15px 0;
font-size: 14px;
page-break-inside: avoid;
}
th, td {
border: 1px solid #bdc3c7;
padding: 8px;
text-align: left;
}
th {
background-color: #ecf0f1;
font-weight: bold;
color: #2c3e50;
}
tr:nth-child(even) {
background-color: #f8f9fa;
}
.section {
margin: 20px 0;
page-break-inside: avoid;
}
.page-break {
page-break-before: always;
}
.no-break {
page-break-inside: avoid;
}
/* 打印优化 */
@page {
margin: 20mm 15mm;
size: A4;
}
@media print {
body {
font-size: 11px;
}
h1 {
font-size: 20px;
}
h2 {
font-size: 16px;
}
h3 {
font-size: 14px;
}
.section {
break-inside: avoid;
}
}
`;
};
/**
* 获取原有CSS样式(保持兼容性)
*/
const getCSS = () => {
return `
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
line-height: 1.6;
color: #333;
background-color: #f9f9f9;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
box-shadow: 0 0 20px rgba(0,0,0,0.1);
}
.header {
background: linear-gradient(135deg, #dc2626, #b91c1c);
color: white;
padding: 30px;
text-align: center;
}
.header .logo h1 {
font-size: 2.5em;
margin-bottom: 5px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.header .logo p {
font-size: 1.1em;
opacity: 0.9;
}
.header .report-info {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid rgba(255,255,255,0.3);
}
.header .report-info h2 {
font-size: 1.8em;
margin-bottom: 10px;
}
.content {
padding: 30px;
}
.section {
margin-bottom: 40px;
padding-bottom: 30px;
border-bottom: 1px solid #eee;
}
.section:last-child {
border-bottom: none;
}
.section-title {
font-size: 1.5em;
color: #dc2626;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #dc2626;
}
.subsection-title {
font-size: 1.2em;
color: #b91c1c;
margin: 20px 0 10px 0;
}
.info-grid {
display: flex;
flex-direction: column;
gap: 15px;
margin-bottom: 20px;
}
.info-item {
display: flex;
align-items: center;
padding: 10px;
background: #f8f9fa;
border-radius: 5px;
}
.info-item label {
font-weight: bold;
margin-right: 10px;
min-width: 80px;
}
.highlight {
color: #dc2626;
font-weight: bold;
}
.bazi-table, .element-table {
width: 100%;
border-collapse: collapse;
margin: 15px 0;
}
.bazi-table th, .bazi-table td,
.element-table th, .element-table td {
border: 1px solid #ddd;
padding: 12px;
text-align: center;
}
.bazi-table th, .element-table th {
background: #dc2626;
color: white;
font-weight: bold;
}
.bazi-table tr:nth-child(even),
.element-table tr:nth-child(even) {
background: #f8f9fa;
}
.element-木 { color: #22c55e; font-weight: bold; }
.element-火 { color: #ef4444; font-weight: bold; }
.element-土 { color: #eab308; font-weight: bold; }
.element-金 { color: #64748b; font-weight: bold; }
.element-水 { color: #3b82f6; font-weight: bold; }
.strength-旺 { color: #22c55e; font-weight: bold; }
.strength-中 { color: #eab308; font-weight: bold; }
.strength-弱 { color: #ef4444; font-weight: bold; }
.strength-strong { color: #22c55e; font-weight: bold; }
.strength-moderate { color: #eab308; font-weight: bold; }
.strength-fair { color: #f97316; font-weight: bold; }
.strength-weak { color: #ef4444; font-weight: bold; }
.analysis-content {
margin: 20px 0;
padding: 20px;
background: #f8f9fa;
border-radius: 5px;
}
.guidance-item {
margin: 20px 0;
padding: 20px;
background: #fff7ed;
border-radius: 8px;
border: 1px solid #fed7aa;
}
.guidance-content ul {
margin-left: 20px;
}
.guidance-content li {
margin: 8px 0;
}
.star-analysis {
display: flex;
flex-direction: column;
gap: 20px;
}
.star-item {
padding: 15px;
background: #f1f5f9;
border-radius: 8px;
border-left: 4px solid #3b82f6;
}
.star-item h5 {
color: #1e40af;
margin-bottom: 10px;
font-size: 1.1em;
}
.hexagram-item {
margin: 20px 0;
padding: 20px;
background: #fef3c7;
border-radius: 8px;
border: 1px solid #fbbf24;
}
.hexagram-symbol {
font-family: monospace;
font-size: 1.2em;
font-weight: bold;
color: #92400e;
}
.footer {
background: #f8f9fa;
padding: 30px;
border-top: 1px solid #eee;
}
.disclaimer {
margin-bottom: 20px;
padding: 20px;
background: #fef2f2;
border: 1px solid #fecaca;
border-radius: 8px;
}
.disclaimer p {
margin: 5px 0;
}
.footer-info {
text-align: center;
color: #666;
font-size: 0.9em;
}
.footer-info p {
margin: 5px 0;
}
@media print {
body {
background: white;
}
.container {
box-shadow: none;
}
}
`;
};
module.exports = {
generatePDF
};