diff --git a/server/database/schema.sql b/server/database/schema.sql index 3a6009f..a05aaa4 100644 --- a/server/database/schema.sql +++ b/server/database/schema.sql @@ -45,6 +45,18 @@ CREATE TABLE IF NOT EXISTS numerology_readings ( FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); +-- 下载历史表 +CREATE TABLE IF NOT EXISTS download_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + analysis_type TEXT NOT NULL CHECK (analysis_type IN ('bazi', 'ziwei', 'yijing')), + format TEXT NOT NULL CHECK (format IN ('markdown', 'pdf', 'png')), + filename TEXT NOT NULL, + file_size INTEGER, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +); + -- 会话表 (用于JWT token管理) CREATE TABLE IF NOT EXISTS user_sessions ( id INTEGER PRIMARY KEY AUTOINCREMENT, diff --git a/server/index.cjs b/server/index.cjs index ae1d403..3120589 100644 --- a/server/index.cjs +++ b/server/index.cjs @@ -9,6 +9,7 @@ const authRoutes = require('./routes/auth.cjs'); const analysisRoutes = require('./routes/analysis.cjs'); const historyRoutes = require('./routes/history.cjs'); const profileRoutes = require('./routes/profile.cjs'); +const downloadRoutes = require('./routes/download.cjs'); // 导入中间件 const { errorHandler } = require('./middleware/errorHandler.cjs'); @@ -92,6 +93,7 @@ app.use('/api/auth', authRoutes); app.use('/api/analysis', analysisRoutes); app.use('/api/history', historyRoutes); app.use('/api/profile', profileRoutes); +app.use('/api/download', downloadRoutes); // 静态文件服务 (用于生产环境) // 强制在 Koyeb 部署时启用静态文件服务 diff --git a/server/routes/download.cjs b/server/routes/download.cjs new file mode 100644 index 0000000..6904b8e --- /dev/null +++ b/server/routes/download.cjs @@ -0,0 +1,226 @@ +const express = require('express'); +const { authenticate } = require('../middleware/auth.cjs'); +const { dbManager } = require('../database/index.cjs'); + +// 临时注释生成器导入,先测试路由基本功能 +// const { generateMarkdown } = require('../services/generators/markdownGenerator.cjs'); +// const { generatePDF } = require('../services/generators/pdfGenerator.cjs'); +// const { generatePNG } = require('../services/generators/pngGenerator.cjs'); + +const router = express.Router(); + +/** + * 下载分析结果 + * POST /api/download + * 支持格式:markdown, pdf, png + */ +router.post('/', authenticate, async (req, res) => { + try { + const { analysisData, analysisType, format, userName } = req.body; + const userId = req.user.id; + + // 验证必需参数 + if (!analysisData || !analysisType || !format) { + return res.status(400).json({ + error: '缺少必需参数', + details: 'analysisData, analysisType, format 都是必需的' + }); + } + + // 验证格式类型 + const supportedFormats = ['markdown', 'pdf', 'png']; + if (!supportedFormats.includes(format)) { + return res.status(400).json({ + error: '不支持的格式', + supportedFormats + }); + } + + // 验证分析类型 + const supportedAnalysisTypes = ['bazi', 'ziwei', 'yijing']; + if (!supportedAnalysisTypes.includes(analysisType)) { + return res.status(400).json({ + error: '不支持的分析类型', + supportedAnalysisTypes + }); + } + + let fileBuffer; + let contentType; + let fileExtension; + let filename; + + // 生成文件名 + const timestamp = new Date().toISOString().slice(0, 19).replace(/[:-]/g, ''); + const analysisTypeLabel = { + 'bazi': '八字命理', + 'ziwei': '紫微斗数', + 'yijing': '易经占卜' + }[analysisType]; + + const baseFilename = `${analysisTypeLabel}_${userName || 'user'}_${timestamp}`; + + try { + switch (format) { + case 'markdown': + // 临时简单实现 + const markdownContent = `# ${analysisTypeLabel}分析报告\n\n**姓名:** ${userName || '用户'}\n**生成时间:** ${new Date().toLocaleString('zh-CN')}\n\n## 分析结果\n\n这是一个测试文件。\n\n---\n\n*本报告由神机阁AI命理分析平台生成*`; + fileBuffer = Buffer.from(markdownContent, 'utf8'); + contentType = 'text/markdown'; + fileExtension = 'md'; + filename = `${baseFilename}.md`; + break; + + case 'pdf': + // 临时返回HTML内容 + const htmlContent = `${analysisTypeLabel}分析报告

${analysisTypeLabel}分析报告

姓名:${userName || '用户'}

生成时间:${new Date().toLocaleString('zh-CN')}

分析结果

这是一个测试文件。

`; + fileBuffer = Buffer.from(htmlContent, 'utf8'); + contentType = 'text/html'; + fileExtension = 'html'; + filename = `${baseFilename}.html`; + break; + + case 'png': + // 临时返回SVG内容 + const svgContent = `${analysisTypeLabel}分析报告姓名:${userName || '用户'}生成时间:${new Date().toLocaleString('zh-CN')}这是一个测试文件`; + fileBuffer = Buffer.from(svgContent, 'utf8'); + contentType = 'image/svg+xml'; + fileExtension = 'svg'; + filename = `${baseFilename}.svg`; + break; + } + } catch (generationError) { + console.error(`生成${format}文件失败:`, generationError); + return res.status(500).json({ + error: `生成${format}文件失败`, + details: generationError.message + }); + } + + // 记录下载历史(可选) + try { + const db = dbManager.getDb(); + const stmt = db.prepare(` + INSERT INTO download_history (user_id, analysis_type, format, filename, created_at) + VALUES (?, ?, ?, ?, datetime('now')) + `); + stmt.run(userId, analysisType, format, filename); + } catch (dbError) { + // 下载历史记录失败不影响文件下载 + console.warn('记录下载历史失败:', dbError); + } + + // 设置响应头 + res.setHeader('Content-Type', contentType); + res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(filename)}"`); + res.setHeader('Content-Length', fileBuffer.length); + res.setHeader('Cache-Control', 'no-cache'); + + // 发送文件 + res.send(fileBuffer); + + } catch (error) { + console.error('下载API错误:', error); + res.status(500).json({ + error: '服务器内部错误', + details: error.message + }); + } +}); + +/** + * 获取用户下载历史 + * GET /api/download/history + */ +router.get('/history', authenticate, async (req, res) => { + try { + const userId = req.user.id; + const { page = 1, limit = 20 } = req.query; + + const db = dbManager.getDb(); + + // 获取总数 + const countStmt = db.prepare('SELECT COUNT(*) as total FROM download_history WHERE user_id = ?'); + const { total } = countStmt.get(userId); + + // 获取分页数据 + const offset = (page - 1) * limit; + const stmt = db.prepare(` + SELECT analysis_type, format, filename, created_at + FROM download_history + WHERE user_id = ? + ORDER BY created_at DESC + LIMIT ? OFFSET ? + `); + + const downloads = stmt.all(userId, limit, offset); + + res.json({ + downloads, + pagination: { + page: parseInt(page), + limit: parseInt(limit), + total, + pages: Math.ceil(total / limit) + } + }); + + } catch (error) { + console.error('获取下载历史失败:', error); + res.status(500).json({ + error: '获取下载历史失败', + details: error.message + }); + } +}); + +/** + * 获取支持的格式和分析类型 + * GET /api/download/formats + */ +router.get('/formats', (req, res) => { + res.json({ + supportedFormats: [ + { + format: 'markdown', + label: 'Markdown文档', + description: '结构化文本格式,便于编辑', + mimeType: 'text/markdown', + extension: 'md' + }, + { + format: 'pdf', + label: 'PDF文档', + description: '专业格式,便于打印和分享', + mimeType: 'application/pdf', + extension: 'pdf' + }, + { + format: 'png', + label: 'PNG图片', + description: '高清图片格式,便于保存', + mimeType: 'image/png', + extension: 'png' + } + ], + supportedAnalysisTypes: [ + { + type: 'bazi', + label: '八字命理', + description: '基于传统八字学说的命理分析' + }, + { + type: 'ziwei', + label: '紫微斗数', + description: '通过星曜排布分析命运走向' + }, + { + type: 'yijing', + label: '易经占卜', + description: '运用梅花易数解读卦象含义' + } + ] + }); +}); + +module.exports = router; \ No newline at end of file diff --git a/server/services/generators/markdownGenerator.cjs b/server/services/generators/markdownGenerator.cjs new file mode 100644 index 0000000..feb2de8 --- /dev/null +++ b/server/services/generators/markdownGenerator.cjs @@ -0,0 +1,622 @@ +/** + * Markdown格式生成器 + * 将分析结果转换为结构化的Markdown文档 + */ + +const generateMarkdown = async (analysisData, analysisType, userName) => { + try { + let markdown = ''; + + // 根据分析类型生成不同的Markdown内容 + switch (analysisType) { + case 'bazi': + markdown = generateBaziMarkdown(analysisData, userName); + break; + case 'ziwei': + markdown = generateZiweiMarkdown(analysisData, userName); + break; + case 'yijing': + markdown = generateYijingMarkdown(analysisData, userName); + break; + default: + throw new Error(`不支持的分析类型: ${analysisType}`); + } + + return Buffer.from(markdown, 'utf8'); + } catch (error) { + console.error('生成Markdown失败:', error); + throw error; + } +}; + +/** + * 生成八字命理Markdown文档 + */ +const generateBaziMarkdown = (analysisData, userName) => { + const timestamp = new Date().toLocaleString('zh-CN'); + + let markdown = `# 八字命理分析报告\n\n`; + markdown += `**姓名:** ${userName || '用户'}\n`; + markdown += `**生成时间:** ${timestamp}\n`; + markdown += `**分析类型:** 八字命理\n\n`; + + markdown += `---\n\n`; + + // 基本信息 + if (analysisData.basic_info) { + markdown += `## 📋 基本信息\n\n`; + + if (analysisData.basic_info.personal_data) { + const personal = analysisData.basic_info.personal_data; + markdown += `- **姓名:** ${personal.name || '未提供'}\n`; + markdown += `- **性别:** ${personal.gender === 'male' ? '男' : personal.gender === 'female' ? '女' : personal.gender || '未提供'}\n`; + markdown += `- **出生日期:** ${personal.birth_date || '未提供'}\n`; + markdown += `- **出生时间:** ${personal.birth_time || '未提供'}\n`; + if (personal.birth_place) { + markdown += `- **出生地点:** ${personal.birth_place}\n`; + } + } + + // 八字信息 + if (analysisData.basic_info.bazi_info) { + const bazi = analysisData.basic_info.bazi_info; + markdown += `\n### 🔮 八字信息\n\n`; + markdown += `| 柱位 | 天干 | 地支 | 纳音 |\n`; + markdown += `|------|------|------|------|\n`; + markdown += `| 年柱 | ${bazi.year?.split('')[0] || '-'} | ${bazi.year?.split('')[1] || '-'} | ${bazi.year_nayin || '-'} |\n`; + markdown += `| 月柱 | ${bazi.month?.split('')[0] || '-'} | ${bazi.month?.split('')[1] || '-'} | ${bazi.month_nayin || '-'} |\n`; + markdown += `| 日柱 | ${bazi.day?.split('')[0] || '-'} | ${bazi.day?.split('')[1] || '-'} | ${bazi.day_nayin || '-'} |\n`; + markdown += `| 时柱 | ${bazi.hour?.split('')[0] || '-'} | ${bazi.hour?.split('')[1] || '-'} | ${bazi.hour_nayin || '-'} |\n\n`; + } + } + + // 五行分析 + if (analysisData.wuxing_analysis) { + markdown += `## 🌟 五行分析\n\n`; + + if (analysisData.wuxing_analysis.element_distribution) { + markdown += `### 五行分布\n\n`; + const elements = analysisData.wuxing_analysis.element_distribution; + const total = Object.values(elements).reduce((sum, count) => sum + (typeof count === 'number' ? count : 0), 0); + + markdown += `| 五行 | 数量 | 占比 | 强度 |\n`; + markdown += `|------|------|------|------|\n`; + + 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 ? '中' : '弱'; + markdown += `| ${element} | ${numCount} | ${percentage}% | ${strength} |\n`; + }); + + markdown += `\n`; + } + + if (analysisData.wuxing_analysis.balance_analysis) { + markdown += `### 五行平衡分析\n\n`; + markdown += `${analysisData.wuxing_analysis.balance_analysis}\n\n`; + } + + if (analysisData.wuxing_analysis.suggestions) { + markdown += `### 调和建议\n\n`; + markdown += `${analysisData.wuxing_analysis.suggestions}\n\n`; + } + } + + // 十神分析 + if (analysisData.ten_gods_analysis) { + markdown += `## ⚡ 十神分析\n\n`; + + if (analysisData.ten_gods_analysis.distribution) { + markdown += `### 十神分布\n\n`; + Object.entries(analysisData.ten_gods_analysis.distribution).forEach(([god, info]) => { + markdown += `#### ${god}\n`; + if (typeof info === 'object' && info.count !== undefined) { + markdown += `- **数量:** ${info.count}\n`; + if (info.description) { + markdown += `- **含义:** ${info.description}\n`; + } + } else { + markdown += `- **数量:** ${info}\n`; + } + markdown += `\n`; + }); + } + + if (analysisData.ten_gods_analysis.analysis) { + markdown += `### 十神综合分析\n\n`; + markdown += `${analysisData.ten_gods_analysis.analysis}\n\n`; + } + } + + // 格局分析 + if (analysisData.pattern_analysis) { + markdown += `## 🎯 格局分析\n\n`; + + if (analysisData.pattern_analysis.main_pattern) { + markdown += `### 主要格局\n\n`; + markdown += `**格局类型:** ${analysisData.pattern_analysis.main_pattern}\n\n`; + } + + if (analysisData.pattern_analysis.pattern_strength) { + const strength = analysisData.pattern_analysis.pattern_strength; + const strengthLabel = strength === 'strong' ? '强' : strength === 'moderate' ? '中等' : strength === 'fair' ? '一般' : '较弱'; + markdown += `**格局强度:** ${strengthLabel}\n\n`; + } + + if (analysisData.pattern_analysis.analysis) { + markdown += `### 格局详解\n\n`; + markdown += `${analysisData.pattern_analysis.analysis}\n\n`; + } + } + + // 运势分析 + if (analysisData.fortune_analysis) { + markdown += `## 🔮 运势分析\n\n`; + + ['career', 'wealth', 'relationship', 'health'].forEach(aspect => { + if (analysisData.fortune_analysis[aspect]) { + const aspectNames = { + career: '事业运势', + wealth: '财运分析', + relationship: '感情运势', + health: '健康运势' + }; + + markdown += `### ${aspectNames[aspect]}\n\n`; + markdown += `${analysisData.fortune_analysis[aspect]}\n\n`; + } + }); + } + + // 人生指导 + if (analysisData.life_guidance) { + markdown += `## 🌟 人生指导\n\n`; + + if (analysisData.life_guidance.strengths) { + markdown += `### 优势特质\n\n`; + if (Array.isArray(analysisData.life_guidance.strengths)) { + analysisData.life_guidance.strengths.forEach(strength => { + markdown += `- ${strength}\n`; + }); + } else { + markdown += `${analysisData.life_guidance.strengths}\n`; + } + markdown += `\n`; + } + + if (analysisData.life_guidance.challenges) { + markdown += `### 需要注意\n\n`; + if (Array.isArray(analysisData.life_guidance.challenges)) { + analysisData.life_guidance.challenges.forEach(challenge => { + markdown += `- ${challenge}\n`; + }); + } else { + markdown += `${analysisData.life_guidance.challenges}\n`; + } + markdown += `\n`; + } + + if (analysisData.life_guidance.suggestions) { + markdown += `### 发展建议\n\n`; + if (Array.isArray(analysisData.life_guidance.suggestions)) { + analysisData.life_guidance.suggestions.forEach(suggestion => { + markdown += `- ${suggestion}\n`; + }); + } else { + markdown += `${analysisData.life_guidance.suggestions}\n`; + } + markdown += `\n`; + } + + if (analysisData.life_guidance.overall_summary) { + markdown += `### 综合总结\n\n`; + markdown += `${analysisData.life_guidance.overall_summary}\n\n`; + } + } + + // 现代应用建议 + if (analysisData.modern_applications) { + markdown += `## 💡 现代应用建议\n\n`; + + Object.entries(analysisData.modern_applications).forEach(([key, value]) => { + const keyNames = { + lifestyle: '生活方式建议', + career_development: '职业发展建议', + relationship_advice: '人际关系建议', + health_maintenance: '健康养生建议', + financial_planning: '理财规划建议' + }; + + if (keyNames[key] && value) { + markdown += `### ${keyNames[key]}\n\n`; + markdown += `${value}\n\n`; + } + }); + } + + // 页脚 + markdown += `---\n\n`; + markdown += `*本报告由神机阁AI命理分析平台生成*\n`; + markdown += `*生成时间:${timestamp}*\n`; + markdown += `*仅供参考,请理性对待*\n`; + + return markdown; +}; + +/** + * 生成紫微斗数Markdown文档 + */ +const generateZiweiMarkdown = (analysisData, userName) => { + const timestamp = new Date().toLocaleString('zh-CN'); + + let markdown = `# 紫微斗数分析报告\n\n`; + markdown += `**姓名:** ${userName || '用户'}\n`; + markdown += `**生成时间:** ${timestamp}\n`; + markdown += `**分析类型:** 紫微斗数\n\n`; + + markdown += `---\n\n`; + + // 基本信息 + if (analysisData.basic_info) { + markdown += `## 📋 基本信息\n\n`; + + if (analysisData.basic_info.personal_data) { + const personal = analysisData.basic_info.personal_data; + markdown += `- **姓名:** ${personal.name || '未提供'}\n`; + markdown += `- **性别:** ${personal.gender === 'male' ? '男' : personal.gender === 'female' ? '女' : personal.gender || '未提供'}\n`; + markdown += `- **出生日期:** ${personal.birth_date || '未提供'}\n`; + markdown += `- **出生时间:** ${personal.birth_time || '未提供'}\n`; + } + + // 紫微基本信息 + if (analysisData.basic_info.ziwei_info) { + const ziwei = analysisData.basic_info.ziwei_info; + markdown += `\n### 🌟 紫微基本信息\n\n`; + if (ziwei.ming_gong) { + markdown += `- **命宫:** ${ziwei.ming_gong}\n`; + } + if (ziwei.wuxing_ju) { + markdown += `- **五行局:** ${ziwei.wuxing_ju}\n`; + } + if (ziwei.main_stars) { + markdown += `- **主星:** ${Array.isArray(ziwei.main_stars) ? ziwei.main_stars.join('、') : ziwei.main_stars}\n`; + } + } + } + + // 星曜分析 + if (analysisData.star_analysis) { + markdown += `\n## ⭐ 星曜分析\n\n`; + + if (analysisData.star_analysis.main_stars) { + markdown += `### 主星分析\n\n`; + if (Array.isArray(analysisData.star_analysis.main_stars)) { + analysisData.star_analysis.main_stars.forEach(star => { + if (typeof star === 'object') { + markdown += `#### ${star.name || star.star}\n`; + if (star.brightness) { + markdown += `- **亮度:** ${star.brightness}\n`; + } + if (star.influence) { + markdown += `- **影响:** ${star.influence}\n`; + } + if (star.description) { + markdown += `- **特质:** ${star.description}\n`; + } + markdown += `\n`; + } + }); + } else { + markdown += `${analysisData.star_analysis.main_stars}\n\n`; + } + } + + if (analysisData.star_analysis.auxiliary_stars) { + markdown += `### 辅星分析\n\n`; + markdown += `${analysisData.star_analysis.auxiliary_stars}\n\n`; + } + } + + // 十二宫位分析 + if (analysisData.palace_analysis) { + markdown += `## 🏛️ 十二宫位分析\n\n`; + + const palaceNames = { + ming: '命宫', + xiong: '兄弟宫', + fu: '夫妻宫', + zi: '子女宫', + cai: '财帛宫', + ji: '疾厄宫', + qian: '迁移宫', + nu: '奴仆宫', + guan: '官禄宫', + tian: '田宅宫', + fu_de: '福德宫', + fu_mu: '父母宫' + }; + + Object.entries(analysisData.palace_analysis).forEach(([palace, analysis]) => { + const palaceName = palaceNames[palace] || palace; + markdown += `### ${palaceName}\n\n`; + if (typeof analysis === 'object') { + if (analysis.stars) { + markdown += `**星曜:** ${Array.isArray(analysis.stars) ? analysis.stars.join('、') : analysis.stars}\n`; + } + if (analysis.analysis) { + markdown += `**分析:** ${analysis.analysis}\n`; + } + if (analysis.fortune) { + markdown += `**运势:** ${analysis.fortune}\n`; + } + } else { + markdown += `${analysis}\n`; + } + markdown += `\n`; + }); + } + + // 四化分析 + if (analysisData.sihua_analysis) { + markdown += `## 🔄 四化分析\n\n`; + + const sihuaNames = { + lu: '化禄', + quan: '化权', + ke: '化科', + ji: '化忌' + }; + + Object.entries(analysisData.sihua_analysis).forEach(([sihua, analysis]) => { + const sihuaName = sihuaNames[sihua] || sihua; + markdown += `### ${sihuaName}\n\n`; + markdown += `${analysis}\n\n`; + }); + } + + // 大运分析 + if (analysisData.major_periods) { + markdown += `## 📅 大运分析\n\n`; + + if (Array.isArray(analysisData.major_periods)) { + analysisData.major_periods.forEach((period, index) => { + markdown += `### 第${index + 1}大运 (${period.age_range || period.years || '年龄段'})\n\n`; + if (period.main_star) { + markdown += `**主星:** ${period.main_star}\n`; + } + if (period.fortune) { + markdown += `**运势:** ${period.fortune}\n`; + } + if (period.analysis) { + markdown += `**分析:** ${period.analysis}\n`; + } + if (period.advice) { + markdown += `**建议:** ${period.advice}\n`; + } + markdown += `\n`; + }); + } + } + + // 综合分析 + if (analysisData.comprehensive_analysis) { + markdown += `## 🎯 综合分析\n\n`; + + ['personality', 'career', 'wealth', 'relationship', 'health'].forEach(aspect => { + if (analysisData.comprehensive_analysis[aspect]) { + const aspectNames = { + personality: '性格特质', + career: '事业发展', + wealth: '财运分析', + relationship: '感情婚姻', + health: '健康状况' + }; + + markdown += `### ${aspectNames[aspect]}\n\n`; + markdown += `${analysisData.comprehensive_analysis[aspect]}\n\n`; + } + }); + } + + // 页脚 + markdown += `---\n\n`; + markdown += `*本报告由神机阁AI命理分析平台生成*\n`; + markdown += `*生成时间:${timestamp}*\n`; + markdown += `*仅供参考,请理性对待*\n`; + + return markdown; +}; + +/** + * 生成易经占卜Markdown文档 + */ +const generateYijingMarkdown = (analysisData, userName) => { + const timestamp = new Date().toLocaleString('zh-CN'); + + let markdown = `# 易经占卜分析报告\n\n`; + markdown += `**占卜者:** ${userName || '用户'}\n`; + markdown += `**生成时间:** ${timestamp}\n`; + markdown += `**分析类型:** 易经占卜\n\n`; + + markdown += `---\n\n`; + + // 占卜问题 + if (analysisData.question_analysis) { + markdown += `## ❓ 占卜问题\n\n`; + if (analysisData.question_analysis.original_question) { + markdown += `**问题:** ${analysisData.question_analysis.original_question}\n\n`; + } + if (analysisData.question_analysis.question_type) { + markdown += `**问题类型:** ${analysisData.question_analysis.question_type}\n\n`; + } + if (analysisData.question_analysis.analysis_focus) { + markdown += `**分析重点:** ${analysisData.question_analysis.analysis_focus}\n\n`; + } + } + + // 卦象信息 + if (analysisData.hexagram_info) { + markdown += `## 🔮 卦象信息\n\n`; + + if (analysisData.hexagram_info.main_hexagram) { + const main = analysisData.hexagram_info.main_hexagram; + markdown += `### 主卦\n\n`; + markdown += `**卦名:** ${main.name || '未知'}\n`; + markdown += `**卦象:** ${main.symbol || ''}\n`; + if (main.number) { + markdown += `**卦序:** 第${main.number}卦\n`; + } + if (main.element) { + markdown += `**五行:** ${main.element}\n`; + } + if (main.meaning) { + markdown += `**含义:** ${main.meaning}\n`; + } + markdown += `\n`; + } + + if (analysisData.hexagram_info.changing_hexagram) { + const changing = analysisData.hexagram_info.changing_hexagram; + markdown += `### 变卦\n\n`; + markdown += `**卦名:** ${changing.name || '未知'}\n`; + markdown += `**卦象:** ${changing.symbol || ''}\n`; + if (changing.meaning) { + markdown += `**含义:** ${changing.meaning}\n`; + } + markdown += `\n`; + } + } + + // 卦辞分析 + if (analysisData.hexagram_analysis) { + markdown += `## 📜 卦辞分析\n\n`; + + if (analysisData.hexagram_analysis.gua_ci) { + markdown += `### 卦辞\n\n`; + markdown += `> ${analysisData.hexagram_analysis.gua_ci}\n\n`; + } + + if (analysisData.hexagram_analysis.gua_ci_interpretation) { + markdown += `### 卦辞解释\n\n`; + markdown += `${analysisData.hexagram_analysis.gua_ci_interpretation}\n\n`; + } + + if (analysisData.hexagram_analysis.yao_ci) { + markdown += `### 爻辞分析\n\n`; + if (Array.isArray(analysisData.hexagram_analysis.yao_ci)) { + analysisData.hexagram_analysis.yao_ci.forEach((yao, index) => { + markdown += `#### ${yao.position || `第${index + 1}爻`}\n`; + if (yao.text) { + markdown += `**爻辞:** ${yao.text}\n`; + } + if (yao.interpretation) { + markdown += `**解释:** ${yao.interpretation}\n`; + } + markdown += `\n`; + }); + } + } + } + + // 象数分析 + if (analysisData.numerology_analysis) { + markdown += `## 🔢 象数分析\n\n`; + + if (analysisData.numerology_analysis.upper_trigram_number) { + markdown += `### 上卦数理\n\n`; + const upper = analysisData.numerology_analysis.upper_trigram_number; + markdown += `**数字:** ${upper.number || upper}\n`; + if (upper.meaning) { + markdown += `**含义:** ${upper.meaning}\n`; + } + if (upper.influence) { + markdown += `**影响:** ${upper.influence}\n`; + } + markdown += `\n`; + } + + if (analysisData.numerology_analysis.lower_trigram_number) { + markdown += `### 下卦数理\n\n`; + const lower = analysisData.numerology_analysis.lower_trigram_number; + markdown += `**数字:** ${lower.number || lower}\n`; + if (lower.meaning) { + markdown += `**含义:** ${lower.meaning}\n`; + } + if (lower.influence) { + markdown += `**影响:** ${lower.influence}\n`; + } + markdown += `\n`; + } + + if (analysisData.numerology_analysis.combined_energy) { + markdown += `### 组合能量\n\n`; + const combined = analysisData.numerology_analysis.combined_energy; + markdown += `**总数:** ${combined.total_number || combined.total || combined}\n`; + if (combined.interpretation) { + markdown += `**解释:** ${combined.interpretation}\n`; + } + if (combined.harmony) { + markdown += `**和谐度:** ${combined.harmony}\n`; + } + markdown += `\n`; + } + } + + // 综合解读 + if (analysisData.comprehensive_interpretation) { + markdown += `## 🎯 综合解读\n\n`; + + if (analysisData.comprehensive_interpretation.current_situation) { + markdown += `### 当前状况\n\n`; + markdown += `${analysisData.comprehensive_interpretation.current_situation}\n\n`; + } + + if (analysisData.comprehensive_interpretation.development_trend) { + markdown += `### 发展趋势\n\n`; + markdown += `${analysisData.comprehensive_interpretation.development_trend}\n\n`; + } + + if (analysisData.comprehensive_interpretation.action_advice) { + markdown += `### 行动建议\n\n`; + if (Array.isArray(analysisData.comprehensive_interpretation.action_advice)) { + analysisData.comprehensive_interpretation.action_advice.forEach(advice => { + markdown += `- ${advice}\n`; + }); + } else { + markdown += `${analysisData.comprehensive_interpretation.action_advice}\n`; + } + markdown += `\n`; + } + + if (analysisData.comprehensive_interpretation.timing_guidance) { + markdown += `### 时机指导\n\n`; + markdown += `${analysisData.comprehensive_interpretation.timing_guidance}\n\n`; + } + } + + // 注意事项 + if (analysisData.precautions) { + markdown += `## ⚠️ 注意事项\n\n`; + if (Array.isArray(analysisData.precautions)) { + analysisData.precautions.forEach(precaution => { + markdown += `- ${precaution}\n`; + }); + } else { + markdown += `${analysisData.precautions}\n`; + } + markdown += `\n`; + } + + // 页脚 + markdown += `---\n\n`; + markdown += `*本报告由神机阁AI命理分析平台生成*\n`; + markdown += `*生成时间:${timestamp}*\n`; + markdown += `*仅供参考,请理性对待*\n`; + + return markdown; +}; + +module.exports = { + generateMarkdown +}; \ No newline at end of file diff --git a/server/services/generators/pdfGenerator.cjs b/server/services/generators/pdfGenerator.cjs new file mode 100644 index 0000000..63ceafc --- /dev/null +++ b/server/services/generators/pdfGenerator.cjs @@ -0,0 +1,866 @@ +/** + * PDF格式生成器 + * 将分析结果转换为PDF文档 + * 使用html-pdf库进行转换 + */ + +const generatePDF = async (analysisData, analysisType, userName) => { + try { + // 生成HTML内容 + const htmlContent = generateHTML(analysisData, analysisType, userName); + + // 由于html-pdf库需要额外安装,这里先返回HTML转PDF的占位符 + // 在实际部署时需要安装 html-pdf 或 puppeteer + + // 临时解决方案:返回HTML内容作为PDF(实际应该转换为PDF) + const Buffer = require('buffer').Buffer; + return Buffer.from(htmlContent, 'utf8'); + + // 正式实现应该是: + // const pdf = require('html-pdf'); + // return new Promise((resolve, reject) => { + // pdf.create(htmlContent, { + // format: 'A4', + // border: { + // top: '0.5in', + // right: '0.5in', + // bottom: '0.5in', + // left: '0.5in' + // } + // }).toBuffer((err, buffer) => { + // if (err) reject(err); + // else resolve(buffer); + // }); + // }); + + } catch (error) { + console.error('生成PDF失败:', error); + throw error; + } +}; + +/** + * 生成HTML内容 + */ +const generateHTML = (analysisData, analysisType, userName) => { + const timestamp = new Date().toLocaleString('zh-CN'); + + let html = ` + + + + + + ${getAnalysisTypeLabel(analysisType)}分析报告 + + + +
+
+ +
+

${getAnalysisTypeLabel(analysisType)}分析报告

+

姓名:${userName || '用户'}

+

生成时间:${timestamp}

+
+
+ +
+ `; + + // 根据分析类型生成不同的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 += ` + + + + + + + `; + }); + + html += ` + +
五行数量占比强度
${element}${numCount}${percentage}%${strength}
+ `; + } + + 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 '命理'; + } +}; + +/** + * 获取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: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + 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-left: 4px solid #dc2626; + border-radius: 0 5px 5px 0; + } + + .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: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + 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 +}; \ No newline at end of file diff --git a/server/services/generators/pngGenerator.cjs b/server/services/generators/pngGenerator.cjs new file mode 100644 index 0000000..fa71033 --- /dev/null +++ b/server/services/generators/pngGenerator.cjs @@ -0,0 +1,588 @@ +/** + * PNG图片生成器 + * 将分析结果转换为PNG图片格式 + * 使用canvas或html-to-image技术 + */ + +const generatePNG = async (analysisData, analysisType, userName) => { + try { + // 生成图片内容 + const imageData = await generateImageData(analysisData, analysisType, userName); + + // 由于canvas库需要额外安装,这里先返回占位符 + // 在实际部署时需要安装 canvas 或 puppeteer + + // 临时解决方案:返回SVG内容作为PNG(实际应该转换为PNG) + const Buffer = require('buffer').Buffer; + return Buffer.from(imageData, 'utf8'); + + // 正式实现应该是: + // const { createCanvas, loadImage } = require('canvas'); + // const canvas = createCanvas(800, 1200); + // const ctx = canvas.getContext('2d'); + // + // // 绘制内容到canvas + // drawContent(ctx, analysisData, analysisType, userName); + // + // return canvas.toBuffer('image/png'); + + } catch (error) { + console.error('生成PNG失败:', error); + throw error; + } +}; + +/** + * 生成图片数据(SVG格式) + */ +const generateImageData = async (analysisData, analysisType, userName) => { + const timestamp = new Date().toLocaleString('zh-CN'); + const analysisTypeLabel = getAnalysisTypeLabel(analysisType); + + // 生成SVG内容 + let svg = ` + + + + + + + + + + + + + + + + + + + + 神机阁 + 专业命理分析平台 + + + + + + ${analysisTypeLabel}分析报告 + 姓名:${userName || '用户'} + 生成时间:${timestamp.split(' ')[0]} + + + + + `; + + // 根据分析类型添加不同内容 + let yOffset = 260; + + switch (analysisType) { + case 'bazi': + yOffset = addBaziContent(svg, analysisData, yOffset); + break; + case 'ziwei': + yOffset = addZiweiContent(svg, analysisData, yOffset); + break; + case 'yijing': + yOffset = addYijingContent(svg, analysisData, yOffset); + break; + } + + // 页脚 + svg += ` + + + 本报告由神机阁AI命理分析平台生成,仅供参考 + © 2025 神机阁 - AI命理分析平台 + + + `; + + return svg; +}; + +/** + * 添加八字命理内容 + */ +const addBaziContent = (svg, analysisData, yOffset) => { + let content = ''; + + // 基本信息 + if (analysisData.basic_info) { + content += ` + + 📋 基本信息 + `; + yOffset += 40; + + if (analysisData.basic_info.personal_data) { + const personal = analysisData.basic_info.personal_data; + const genderText = personal.gender === 'male' ? '男' : personal.gender === 'female' ? '女' : personal.gender || '未提供'; + + content += ` + 姓名: + ${personal.name || '未提供'} + 性别: + ${genderText} + `; + yOffset += 30; + + content += ` + 出生日期: + ${personal.birth_date || '未提供'} + `; + yOffset += 30; + + content += ` + 出生时间: + ${personal.birth_time || '未提供'} + `; + yOffset += 40; + } + + // 八字信息 + if (analysisData.basic_info.bazi_info) { + const bazi = analysisData.basic_info.bazi_info; + content += ` + 🔮 八字信息 + `; + yOffset += 30; + + // 表格头 + content += ` + + 柱位 + 天干 + 地支 + 纳音 + `; + yOffset += 25; + + // 表格内容 + const pillars = [ + { name: '年柱', data: bazi.year, nayin: bazi.year_nayin }, + { name: '月柱', data: bazi.month, nayin: bazi.month_nayin }, + { name: '日柱', data: bazi.day, nayin: bazi.day_nayin }, + { name: '时柱', data: bazi.hour, nayin: bazi.hour_nayin } + ]; + + pillars.forEach((pillar, index) => { + const bgColor = index % 2 === 0 ? '#f8f9fa' : 'white'; + content += ` + + ${pillar.name} + ${pillar.data?.split('')[0] || '-'} + ${pillar.data?.split('')[1] || '-'} + ${pillar.nayin || '-'} + `; + yOffset += 25; + }); + + yOffset += 20; + } + } + + // 五行分析 + if (analysisData.wuxing_analysis && yOffset < 1000) { + content += ` + 🌟 五行分析 + `; + yOffset += 40; + + if (analysisData.wuxing_analysis.element_distribution) { + const elements = analysisData.wuxing_analysis.element_distribution; + const total = Object.values(elements).reduce((sum, count) => sum + (typeof count === 'number' ? count : 0), 0); + + content += ` + 五行分布 + `; + yOffset += 30; + + // 五行分布图表 + let xOffset = 120; + Object.entries(elements).forEach(([element, count]) => { + const numCount = typeof count === 'number' ? count : 0; + const percentage = total > 0 ? Math.round((numCount / total) * 100) : 0; + const barHeight = Math.max(numCount * 20, 5); + const elementColor = getElementColor(element); + + // 柱状图 + content += ` + + ${element} + ${numCount} + ${percentage}% + `; + + xOffset += 100; + }); + + yOffset += 150; + } + + if (analysisData.wuxing_analysis.balance_analysis && yOffset < 1000) { + content += ` + 五行平衡分析 + `; + yOffset += 25; + + // 分析内容(截取前200字符) + const analysisText = analysisData.wuxing_analysis.balance_analysis.substring(0, 200) + (analysisData.wuxing_analysis.balance_analysis.length > 200 ? '...' : ''); + const lines = wrapText(analysisText, 50); + + lines.forEach(line => { + if (yOffset < 1000) { + content += ` + ${line} + `; + yOffset += 20; + } + }); + + yOffset += 20; + } + } + + svg += content; + return yOffset; +}; + +/** + * 添加紫微斗数内容 + */ +const addZiweiContent = (svg, analysisData, yOffset) => { + let content = ''; + + // 基本信息 + if (analysisData.basic_info) { + content += ` + 📋 基本信息 + `; + yOffset += 40; + + if (analysisData.basic_info.ziwei_info) { + const ziwei = analysisData.basic_info.ziwei_info; + + if (ziwei.ming_gong) { + content += ` + 命宫: + ${ziwei.ming_gong} + `; + yOffset += 30; + } + + if (ziwei.wuxing_ju) { + content += ` + 五行局: + ${ziwei.wuxing_ju} + `; + yOffset += 30; + } + + if (ziwei.main_stars) { + const starsText = Array.isArray(ziwei.main_stars) ? ziwei.main_stars.join('、') : ziwei.main_stars; + content += ` + 主星: + ${starsText} + `; + yOffset += 40; + } + } + } + + // 星曜分析 + if (analysisData.star_analysis && yOffset < 1000) { + content += ` + ⭐ 星曜分析 + `; + yOffset += 40; + + if (analysisData.star_analysis.main_stars) { + content += ` + 主星分析 + `; + yOffset += 30; + + if (Array.isArray(analysisData.star_analysis.main_stars)) { + analysisData.star_analysis.main_stars.slice(0, 3).forEach(star => { + if (typeof star === 'object' && yOffset < 1000) { + content += ` + + ${star.name || star.star} + `; + + if (star.brightness) { + content += ` + 亮度:${star.brightness} + `; + } + + if (star.influence) { + const influenceText = star.influence.substring(0, 60) + (star.influence.length > 60 ? '...' : ''); + content += ` + 影响:${influenceText} + `; + } + + yOffset += 80; + } + }); + } + } + } + + svg += content; + return yOffset; +}; + +/** + * 添加易经占卜内容 + */ +const addYijingContent = (svg, analysisData, yOffset) => { + let content = ''; + + // 占卜问题 + if (analysisData.question_analysis) { + content += ` + ❓ 占卜问题 + `; + yOffset += 40; + + if (analysisData.question_analysis.original_question) { + content += ` + 问题: + `; + + const questionText = analysisData.question_analysis.original_question; + const questionLines = wrapText(questionText, 45); + + questionLines.forEach((line, index) => { + content += ` + ${line} + `; + yOffset += 20; + }); + + yOffset += 10; + } + + if (analysisData.question_analysis.question_type) { + content += ` + 问题类型: + ${analysisData.question_analysis.question_type} + `; + yOffset += 40; + } + } + + // 卦象信息 + if (analysisData.hexagram_info && yOffset < 1000) { + content += ` + 🔮 卦象信息 + `; + yOffset += 40; + + if (analysisData.hexagram_info.main_hexagram) { + const main = analysisData.hexagram_info.main_hexagram; + + content += ` + + 主卦 + + 卦名: + ${main.name || '未知'} + + 卦象: + ${main.symbol || ''} + `; + + if (main.meaning) { + const meaningText = main.meaning.substring(0, 50) + (main.meaning.length > 50 ? '...' : ''); + content += ` + 含义: + ${meaningText} + `; + } + + yOffset += 120; + } + } + + svg += content; + return yOffset; +}; + +/** + * 获取五行颜色 + */ +const getElementColor = (element) => { + const colors = { + '木': '#22c55e', + '火': '#ef4444', + '土': '#eab308', + '金': '#64748b', + '水': '#3b82f6' + }; + return colors[element] || '#666'; +}; + +/** + * 文本换行处理 + */ +const wrapText = (text, maxLength) => { + const lines = []; + let currentLine = ''; + + for (let i = 0; i < text.length; i++) { + currentLine += text[i]; + if (currentLine.length >= maxLength || text[i] === '\n') { + lines.push(currentLine.trim()); + currentLine = ''; + } + } + + if (currentLine.trim()) { + lines.push(currentLine.trim()); + } + + return lines; +}; + +/** + * 获取分析类型标签 + */ +const getAnalysisTypeLabel = (analysisType) => { + switch (analysisType) { + case 'bazi': return '八字命理'; + case 'ziwei': return '紫微斗数'; + case 'yijing': return '易经占卜'; + default: return '命理'; + } +}; + +/** + * 获取SVG样式 + */ +const getSVGStyles = () => { + return ` + .main-title { + font-family: 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; + font-size: 36px; + font-weight: bold; + } + + .subtitle { + font-family: 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; + font-size: 16px; + } + + .report-title { + font-family: 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; + font-size: 24px; + font-weight: bold; + } + + .info-text { + font-family: 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; + font-size: 14px; + } + + .section-title { + font-family: 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; + font-size: 20px; + font-weight: bold; + } + + .subsection-title { + font-family: 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; + font-size: 16px; + font-weight: bold; + } + + .info-label { + font-family: 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; + font-size: 14px; + font-weight: bold; + } + + .info-value { + font-family: 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; + font-size: 14px; + } + + .info-highlight { + font-family: 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; + font-size: 14px; + font-weight: bold; + } + + .table-header { + font-family: 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; + font-size: 12px; + font-weight: bold; + } + + .table-cell { + font-family: 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; + font-size: 12px; + } + + .element-label { + font-family: 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; + font-size: 12px; + font-weight: bold; + } + + .element-count { + font-family: 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; + font-size: 11px; + } + + .element-percent { + font-family: 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; + font-size: 10px; + } + + .analysis-text { + font-family: 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; + font-size: 12px; + } + + .star-name { + font-family: 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; + font-size: 14px; + font-weight: bold; + } + + .star-detail { + font-family: 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; + font-size: 11px; + } + + .hexagram-name { + font-family: 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; + font-size: 16px; + font-weight: bold; + } + + .hexagram-symbol { + font-family: monospace; + font-size: 14px; + font-weight: bold; + } + + .footer-text { + font-family: 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; + font-size: 10px; + } + `; +}; + +module.exports = { + generatePNG +}; \ No newline at end of file diff --git a/src/components/CompleteBaziAnalysis.tsx b/src/components/CompleteBaziAnalysis.tsx index c493449..103f4b2 100644 --- a/src/components/CompleteBaziAnalysis.tsx +++ b/src/components/CompleteBaziAnalysis.tsx @@ -3,6 +3,7 @@ import { Radar, RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis, Responsi import { Calendar, Star, BookOpen, Sparkles, User, BarChart3, Zap, TrendingUp, Loader2, Clock, Target, Heart, DollarSign, Activity } from 'lucide-react'; import { Card, CardContent, CardHeader, CardTitle } from './ui/Card'; import { BackToTop } from './ui/BackToTop'; +import DownloadButton from './ui/DownloadButton'; import { localApi } from '../lib/localApi'; interface CompleteBaziAnalysisProps { @@ -277,6 +278,16 @@ const CompleteBaziAnalysis: React.FC = ({ birthDate,
+ {/* 下载按钮 */} +
+ +
+ {/* 标题和基本信息 */} diff --git a/src/components/CompleteYijingAnalysis.tsx b/src/components/CompleteYijingAnalysis.tsx index c8066ab..03b0b00 100644 --- a/src/components/CompleteYijingAnalysis.tsx +++ b/src/components/CompleteYijingAnalysis.tsx @@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react'; import { Calendar, Star, BookOpen, Sparkles, User, BarChart3, Zap, TrendingUp, Loader2, Clock, Target, Heart, DollarSign, Activity, Crown, Compass, Moon, Sun, Hexagon, Layers, Eye, Shuffle } from 'lucide-react'; import { Card, CardContent, CardHeader, CardTitle } from './ui/Card'; import { BackToTop } from './ui/BackToTop'; +import DownloadButton from './ui/DownloadButton'; import { localApi } from '../lib/localApi'; interface CompleteYijingAnalysisProps { @@ -721,6 +722,16 @@ const CompleteYijingAnalysis: React.FC = ({ + {/* 下载按钮 */} +
+ +
+ {/* 免责声明 */} diff --git a/src/components/CompleteZiweiAnalysis.tsx b/src/components/CompleteZiweiAnalysis.tsx index b95cbae..8e62bef 100644 --- a/src/components/CompleteZiweiAnalysis.tsx +++ b/src/components/CompleteZiweiAnalysis.tsx @@ -5,6 +5,7 @@ import { Card, CardContent, CardHeader, CardTitle } from './ui/Card'; import { ChineseCard, ChineseCardContent, ChineseCardHeader, ChineseCardTitle } from './ui/ChineseCard'; import { ChineseLoading } from './ui/ChineseLoading'; import { BackToTop } from './ui/BackToTop'; +import DownloadButton from './ui/DownloadButton'; import { localApi } from '../lib/localApi'; import { cn } from '../lib/utils'; @@ -580,6 +581,16 @@ const CompleteZiweiAnalysis: React.FC = ({ birthDate
+ {/* 下载按钮 */} +
+ +
+ {/* 标题和基本信息 */} diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index e0b128b..f1f1c5c 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -1,7 +1,7 @@ import React, { ReactNode, useState } from 'react'; import { Link, useLocation } from 'react-router-dom'; import { useAuth } from '../contexts/AuthContext'; -import { Sparkles, User, History, LogOut, Home, Menu, X } from 'lucide-react'; +import { Sparkles, User, History, LogOut, Home, Menu, X, Github } from 'lucide-react'; import { ChineseButton } from './ui/ChineseButton'; import { toast } from 'sonner'; import { cn } from '../lib/utils'; @@ -89,6 +89,18 @@ const Layout: React.FC = ({ children }) => { ); })} + {/* GitHub链接 */} + + + GitHub + + {user ? ( = ({ children }) => { ); })} + {/* 移动端GitHub链接 */} + + + GitHub + +
{user ? ( Promise; + className?: string; + disabled?: boolean; +} + +const DownloadButton: React.FC = ({ + analysisData, + analysisType, + userName, + onDownload, + className, + disabled = false +}) => { + const [isDownloading, setIsDownloading] = useState(false); + const [downloadingFormat, setDownloadingFormat] = useState(null); + const [showDropdown, setShowDropdown] = useState(false); + + const formatOptions = [ + { + format: 'markdown' as DownloadFormat, + label: 'Markdown文档', + icon: FileText, + description: '结构化文本格式,便于编辑', + color: 'text-blue-600', + bgColor: 'bg-blue-50 hover:bg-blue-100' + }, + { + format: 'pdf' as DownloadFormat, + label: 'PDF文档', + icon: File, + description: '专业格式,便于打印和分享', + color: 'text-red-600', + bgColor: 'bg-red-50 hover:bg-red-100' + }, + { + format: 'png' as DownloadFormat, + label: 'PNG图片', + icon: FileImage, + description: '高清图片格式,便于保存', + color: 'text-green-600', + bgColor: 'bg-green-50 hover:bg-green-100' + } + ]; + + const handleDownload = async (format: DownloadFormat) => { + if (disabled || isDownloading) return; + + try { + setIsDownloading(true); + setDownloadingFormat(format); + setShowDropdown(false); + + if (onDownload) { + await onDownload(format); + } else { + // 默认下载逻辑 + await defaultDownload(format); + } + } catch (error) { + console.error('下载失败:', error); + // 这里可以添加错误提示 + } finally { + setIsDownloading(false); + setDownloadingFormat(null); + } + }; + + const defaultDownload = async (format: DownloadFormat) => { + try { + // 获取认证token + const token = localStorage.getItem('auth_token'); + if (!token) { + throw new Error('请先登录'); + } + + // 获取正确的API基础URL + const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || + (import.meta.env.DEV ? 'http://localhost:3001/api' : + (window.location.hostname.includes('koyeb.app') ? `${window.location.origin}/api` : `${window.location.origin}/api`)); + + // 调用后端下载API + const response = await fetch(`${API_BASE_URL}/download`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ + analysisData, + analysisType, + format, + userName + }) + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.error || `下载失败 (${response.status})`); + } + + // 获取文件名(从响应头或生成默认名称) + const contentDisposition = response.headers.get('Content-Disposition'); + let filename = `${getAnalysisTypeLabel()}_${userName || 'user'}_${new Date().toISOString().slice(0, 10)}.${format === 'markdown' ? 'md' : format}`; + + if (contentDisposition) { + const filenameMatch = contentDisposition.match(/filename[^;=\n]*=(['"]?)([^'"\n]*?)\1/); + if (filenameMatch && filenameMatch[2]) { + filename = decodeURIComponent(filenameMatch[2]); + } + } + + // 创建blob并下载 + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + a.style.display = 'none'; + document.body.appendChild(a); + a.click(); + + // 清理 + setTimeout(() => { + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + }, 100); + + // 显示成功提示 + if (typeof window !== 'undefined' && (window as any).toast) { + (window as any).toast.success(`${format.toUpperCase()}文件下载成功`); + } + + } catch (error) { + console.error('下载失败:', error); + + // 显示错误提示 + if (typeof window !== 'undefined' && (window as any).toast) { + (window as any).toast.error(error instanceof Error ? error.message : '下载失败,请重试'); + } + + throw error; + } + }; + + const getAnalysisTypeLabel = () => { + switch (analysisType) { + case 'bazi': return '八字命理'; + case 'ziwei': return '紫微斗数'; + case 'yijing': return '易经占卜'; + default: return '命理'; + } + }; + + const getFormatLabel = (format: DownloadFormat) => { + switch (format) { + case 'markdown': return 'Markdown'; + case 'pdf': return 'PDF'; + case 'png': return 'PNG'; + default: return format.toUpperCase(); + } + }; + + return ( +
+ {/* 主下载按钮 */} +
+ setShowDropdown(!showDropdown)} + disabled={disabled || isDownloading} + variant="secondary" + className="flex items-center space-x-2 bg-gradient-to-r from-yellow-500 to-yellow-600 hover:from-yellow-600 hover:to-yellow-700 text-white border-0 shadow-lg" + > + {isDownloading ? ( + + ) : ( + + )} + + {isDownloading ? `正在生成${getFormatLabel(downloadingFormat!)}...` : '下载分析结果'} + + + +
+ + {/* 下拉菜单 */} + {showDropdown && ( +
+
+

选择下载格式

+

{getAnalysisTypeLabel()}分析结果

+
+ +
+ {formatOptions.map((option) => { + const Icon = option.icon; + const isCurrentlyDownloading = isDownloading && downloadingFormat === option.format; + + return ( + + ); + })} +
+ +
+

+ 💡 提示:PDF和PNG格式包含完整的视觉设计,Markdown格式便于编辑 +

+
+
+ )} + + {/* 点击外部关闭下拉菜单 */} + {showDropdown && ( +
setShowDropdown(false)} + /> + )} +
+ ); +}; + +export default DownloadButton; \ No newline at end of file diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index 80b56c6..418dd57 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Link } from 'react-router-dom'; -import { Sparkles, Star, Compass, Heart, BarChart3, BookOpen, Shield, Zap, Users, Award, Brain, TrendingUp } from 'lucide-react'; +import { Sparkles, Star, Compass, Heart, BarChart3, BookOpen, Shield, Zap, Users, Award, Brain, TrendingUp, Github } from 'lucide-react'; import { ChineseButton } from '../components/ui/ChineseButton'; import { ChineseCard, ChineseCardContent, ChineseCardHeader, ChineseCardTitle } from '../components/ui/ChineseCard'; import { useAuth } from '../contexts/AuthContext'; @@ -333,6 +333,24 @@ const HomePage: React.FC = () => { )}
+ + {/* GitHub链接 */} +
+ +

+ 开源项目,欢迎贡献代码和建议 +

+