mirror of
https://github.com/patdelphi/suanming.git
synced 2026-02-27 21:23:12 +08:00
Remove PNG server generation functionality
- Remove PNG server generation option from DownloadButton component - Remove PNG generation logic from download route - Delete pngGenerator.cjs and related test files - Simplify download options to focus on frontend PNG export - Reduce server complexity and resource usage
This commit is contained in:
211
FRONTEND_EXPORT_SETUP.md
Normal file
211
FRONTEND_EXPORT_SETUP.md
Normal file
@@ -0,0 +1,211 @@
|
||||
# 前端页面导出功能设置说明
|
||||
|
||||
## 功能概述
|
||||
|
||||
新增了前端页面直接导出为PDF和PNG的功能,用户可以直接从分析结果页面生成:
|
||||
- **PDF文档**:支持分页的专业格式文档
|
||||
- **PNG长图**:完整页面截图,适合社交媒体分享
|
||||
|
||||
## 依赖安装
|
||||
|
||||
需要安装以下依赖包:
|
||||
|
||||
```bash
|
||||
npm install html2canvas jspdf
|
||||
```
|
||||
|
||||
或使用yarn:
|
||||
|
||||
```bash
|
||||
yarn add html2canvas jspdf
|
||||
```
|
||||
|
||||
## 功能特性
|
||||
|
||||
### PDF导出
|
||||
- ✅ A4纸张格式
|
||||
- ✅ 自动分页处理
|
||||
- ✅ 高质量输出
|
||||
- ✅ 保持原有样式和布局
|
||||
- ✅ 自动隐藏导出按钮等UI元素
|
||||
|
||||
### PNG导出
|
||||
- ✅ 高分辨率长图(2倍缩放)
|
||||
- ✅ 完整页面内容
|
||||
- ✅ 保持颜色和样式
|
||||
- ✅ 适合移动端查看和分享
|
||||
|
||||
## 已集成的组件
|
||||
|
||||
1. **CompleteYijingAnalysis** - 易经占卜分析页面
|
||||
2. **CompleteBaziAnalysis** - 八字命理分析页面
|
||||
3. **CompleteZiweiAnalysis** - 紫微斗数分析页面
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 在组件中使用
|
||||
|
||||
```tsx
|
||||
import PageExportButton from './ui/PageExportButton';
|
||||
|
||||
// 在JSX中使用
|
||||
<PageExportButton
|
||||
targetElementId="analysis-content" // 要导出的元素ID
|
||||
fileName="分析结果" // 文件名前缀
|
||||
title="页面导出" // 按钮显示文本
|
||||
className="sticky top-4 z-10" // 样式类名
|
||||
/>
|
||||
```
|
||||
|
||||
### 标记导出内容
|
||||
|
||||
为要导出的内容容器添加ID和data属性:
|
||||
|
||||
```tsx
|
||||
<div id="analysis-content" data-export-content>
|
||||
{/* 要导出的内容 */}
|
||||
</div>
|
||||
```
|
||||
|
||||
### 隐藏不需要导出的元素
|
||||
|
||||
为不需要出现在导出文件中的元素添加类名或属性:
|
||||
|
||||
```tsx
|
||||
<div className="no-export" data-no-export>
|
||||
{/* 下载按钮等UI元素 */}
|
||||
</div>
|
||||
```
|
||||
|
||||
## 技术实现
|
||||
|
||||
### 核心技术栈
|
||||
- **html2canvas**: 将DOM元素转换为Canvas
|
||||
- **jsPDF**: 生成PDF文档
|
||||
- **React**: 组件化开发
|
||||
- **TypeScript**: 类型安全
|
||||
|
||||
### 导出流程
|
||||
|
||||
1. **获取目标元素**: 根据ID或选择器定位要导出的DOM元素
|
||||
2. **生成Canvas**: 使用html2canvas将DOM转换为高质量Canvas
|
||||
3. **处理样式**: 自动处理CSS样式、字体、图片等
|
||||
4. **生成文件**:
|
||||
- PNG: 直接从Canvas生成图片
|
||||
- PDF: 将Canvas图片嵌入PDF文档,支持分页
|
||||
5. **自动下载**: 创建下载链接并触发下载
|
||||
|
||||
### 配置选项
|
||||
|
||||
```typescript
|
||||
// html2canvas配置
|
||||
{
|
||||
scale: 2, // 提高分辨率
|
||||
useCORS: true, // 支持跨域图片
|
||||
allowTaint: true, // 允许跨域内容
|
||||
backgroundColor: '#ffffff', // 背景色
|
||||
onclone: (clonedDoc) => {
|
||||
// 在克隆文档中隐藏不需要的元素
|
||||
}
|
||||
}
|
||||
|
||||
// jsPDF配置
|
||||
{
|
||||
orientation: 'portrait', // 纵向
|
||||
unit: 'mm', // 单位毫米
|
||||
format: 'a4' // A4格式
|
||||
}
|
||||
```
|
||||
|
||||
## 样式优化
|
||||
|
||||
### CSS类名约定
|
||||
|
||||
- `.no-export`: 不导出的元素
|
||||
- `[data-no-export]`: 不导出的元素(属性方式)
|
||||
- `[data-export-content]`: 主要导出内容
|
||||
- `.fixed`, `.sticky`, `.floating`: 自动隐藏的浮动元素
|
||||
|
||||
### 打印样式优化
|
||||
|
||||
可以添加打印专用样式:
|
||||
|
||||
```css
|
||||
@media print {
|
||||
.no-print {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.print-break {
|
||||
page-break-before: always;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
- ✅ 网络错误处理
|
||||
- ✅ 图片加载失败处理
|
||||
- ✅ 浏览器兼容性检查
|
||||
- ✅ 用户友好的错误提示
|
||||
- ✅ Toast通知集成
|
||||
|
||||
## 浏览器兼容性
|
||||
|
||||
- ✅ Chrome 60+
|
||||
- ✅ Firefox 55+
|
||||
- ✅ Safari 12+
|
||||
- ✅ Edge 79+
|
||||
- ⚠️ IE不支持(需要polyfill)
|
||||
|
||||
## 性能优化
|
||||
|
||||
- 🚀 按需加载依赖
|
||||
- 🚀 Canvas复用
|
||||
- 🚀 内存管理
|
||||
- 🚀 大文件分块处理
|
||||
- 🚀 用户体验优化(加载状态、进度提示)
|
||||
|
||||
## 未来扩展
|
||||
|
||||
- [ ] 支持更多导出格式(DOCX、Excel等)
|
||||
- [ ] 批量导出功能
|
||||
- [ ] 云端存储集成
|
||||
- [ ] 自定义模板支持
|
||||
- [ ] 水印和签名功能
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **图片跨域**: 确保所有图片资源支持CORS
|
||||
2. **字体加载**: 确保自定义字体已完全加载
|
||||
3. **内容大小**: 超大内容可能影响性能
|
||||
4. **移动端**: 在移动设备上可能需要额外优化
|
||||
5. **隐私**: 导出功能完全在客户端执行,不会上传数据
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **图片不显示**: 检查图片CORS设置
|
||||
2. **样式丢失**: 确保CSS已完全加载
|
||||
3. **字体异常**: 检查字体文件加载状态
|
||||
4. **内容截断**: 调整Canvas尺寸设置
|
||||
5. **下载失败**: 检查浏览器下载权限
|
||||
|
||||
### 调试方法
|
||||
|
||||
```javascript
|
||||
// 开启调试模式
|
||||
const canvas = await html2canvas(element, {
|
||||
logging: true, // 开启日志
|
||||
debug: true // 调试模式
|
||||
});
|
||||
```
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v1.0.0 (2025-01-21)
|
||||
- ✅ 初始版本发布
|
||||
- ✅ 支持PDF和PNG导出
|
||||
- ✅ 集成到三个主要分析组件
|
||||
- ✅ 完整的错误处理和用户体验
|
||||
227
comparison-yijing.md
Normal file
227
comparison-yijing.md
Normal file
@@ -0,0 +1,227 @@
|
||||
# 易经占卜分析报告
|
||||
|
||||
**占卜者:** 午饭
|
||||
**生成时间:** 2025/8/21 19:09:13
|
||||
**分析类型:** 易经占卜
|
||||
|
||||
---
|
||||
|
||||
## ❓ 占卜问题
|
||||
|
||||
**问题:** 午饭
|
||||
|
||||
**起卦方法:** 梅花易数时间起卦法
|
||||
|
||||
**占卜时间:** 2025/8/21 19:09:13
|
||||
|
||||
**问题类型:** 综合运势
|
||||
|
||||
**关注重点:** 整体发展、综合状况、全面分析
|
||||
|
||||
## 🔮 卦象信息
|
||||
|
||||
### 主卦
|
||||
|
||||
**卦名:** 困
|
||||
**卦象:**
|
||||
_ _
|
||||
___
|
||||
_ _
|
||||
_ _
|
||||
_ _
|
||||
___
|
||||
**卦序:** 第47卦
|
||||
|
||||
### 变卦
|
||||
|
||||
**卦名:** 兑
|
||||
**卦象:**
|
||||
_ _
|
||||
___
|
||||
___
|
||||
_ _
|
||||
___
|
||||
___
|
||||
|
||||
### 八卦结构
|
||||
|
||||
**上卦:** 兑 (泽)
|
||||
**下卦:** 坎 (水)
|
||||
|
||||
### 动爻
|
||||
|
||||
**动爻位置:** 1爻
|
||||
|
||||
## 📜 卦辞分析
|
||||
|
||||
### 卦象含义
|
||||
|
||||
【困卦】第47卦 - 困穷,困境,困顿
|
||||
|
||||
### 彖传
|
||||
|
||||
> 【彖传】曰:亨,贞,大人吉,无咎。有言不信。
|
||||
|
||||
### 象传
|
||||
|
||||
> 【象传】曰:泽无水,困。君子以致命遂志。
|
||||
|
||||
### 八卦组合分析
|
||||
|
||||
上卦兑(泽)代表悦,下卦坎(水)代表陷。泽在上,水在下,形成兑坎的组合,象征着特殊的能量组合,需要深入分析。
|
||||
|
||||
### 五行分析
|
||||
|
||||
**upper_element:** 金
|
||||
**lower_element:** 水
|
||||
**relationship:** 金生水,相生有利
|
||||
**balance:** 五行相生,和谐发展,有利于事物的成长
|
||||
|
||||
## 🔄 动爻分析
|
||||
|
||||
**动爻数量:** 1爻
|
||||
|
||||
## 🔀 变卦分析
|
||||
|
||||
### 变卦含义
|
||||
|
||||
兑悦,喜悦,和悦
|
||||
|
||||
### 转化洞察
|
||||
|
||||
从【困】到【兑】的变化,预示着事态将从困穷,困境,困顿转向兑悦,喜悦,和悦,这是一个重要的转折点。需要适应这种变化,调整策略和心态。
|
||||
|
||||
### 变化指导
|
||||
|
||||
变卦指示:充满喜悦和和谐的氛围。保持和悦态度,增进人际和谐。
|
||||
|
||||
### 时机把握
|
||||
|
||||
变化的速度适中,需要保持关注
|
||||
|
||||
## 🔍 高级分析
|
||||
|
||||
### 互卦 - 蹇
|
||||
|
||||
**卦象:**
|
||||
_ _
|
||||
___
|
||||
_ _
|
||||
_ _
|
||||
_ _
|
||||
___
|
||||
**含义:** 蹇难,困难,险阻
|
||||
**分析:** 互卦【蹇】揭示了事物的内在发展趋势和隐藏因素。面临重重困难的时期。反省自身,修德养性,寻求贵人帮助。
|
||||
|
||||
### 错卦 - 坤
|
||||
|
||||
**卦象:**
|
||||
_ _
|
||||
_ _
|
||||
_ _
|
||||
_ _
|
||||
_ _
|
||||
_ _
|
||||
**含义:** 接受,滋养,顺从
|
||||
**分析:** 错卦【坤】代表了相对立的状态和需要避免的方向。以柔顺和包容的态度面对挑战。通过支持他人和耐心等待,将获得成功。
|
||||
|
||||
### 综卦 - 涣
|
||||
|
||||
**卦象:**
|
||||
_ _
|
||||
___
|
||||
_ _
|
||||
_ _
|
||||
___
|
||||
___
|
||||
**含义:** 涣散,离散,化解
|
||||
**分析:** 综卦【涣】显示了事物的另一面和可能的转化方向。化解涣散,重建秩序的时期。凝聚人心,重获团结。
|
||||
|
||||
### 四卦综合洞察
|
||||
|
||||
通过四卦分析:本卦【困】显示当前状态,互卦【蹇】揭示内在动力,错卦【坤】提醒对立面,综卦【涣】指示转化方向。综合来看,需要在困穷,困境,困顿的基础上,注意蹇难,困难,险阻的内在发展,避免接受,滋养,顺从的极端,向涣散,离散,化解的方向转化。
|
||||
|
||||
## 🔢 象数分析
|
||||
|
||||
### 上卦数理
|
||||
|
||||
**数字:** 2
|
||||
**含义:** 上卦数字2,对应兑卦泽象。在您的问题"午饭"中,这表示外在环境充满喜悦和交流的机会。泽象主悦,预示着通过良好的沟通和人际关系能够获得成功。
|
||||
**影响:** 外在环境呈现泽的特质,需要以悦的方式应对
|
||||
|
||||
### 下卦数理
|
||||
|
||||
**数字:** 6
|
||||
**含义:** 下卦数字6,对应坎卦水象。在您的问题"午饭"中,这表示您内心深沉而有智慧。内在动力来自于对深层真理的探索。
|
||||
|
||||
### 组合能量
|
||||
|
||||
**总数:** 8
|
||||
**解释:** 总数8在您的问题"午饭"中代表丰盛收获,是收获成果的时机。这个数字预示着您的努力将得到回报。
|
||||
**和谐度:** 上下卦差异很大,需要深度调整和耐心化解
|
||||
|
||||
### 时间共振
|
||||
|
||||
**共振等级:** 需要调和
|
||||
**时间能量:** 阴气渐盛,适合休息调养
|
||||
**最佳时机:** 对于"午饭",建议在午时(11:00-13:00)把握收获时机
|
||||
|
||||
## 🧭 五行分析
|
||||
|
||||
### 五行属性
|
||||
|
||||
**上卦五行:** 金
|
||||
**下卦五行:** 水
|
||||
|
||||
### 五行关系
|
||||
|
||||
**相互作用:** 金生水,相生有利
|
||||
**平衡状态:** 五行相生,和谐发展,有利于事物的成长
|
||||
|
||||
## ⏰ 时间分析
|
||||
|
||||
### 月相影响
|
||||
|
||||
**月相能量:** 圆满充实
|
||||
**月相建议:** 适合收获和庆祝
|
||||
|
||||
### 能量状态
|
||||
|
||||
**整体状态:** 旺盛之气与阴气渐盛相结合
|
||||
**能量建议:** 在夏季的戌时,适合积极行动,同时休息调整
|
||||
|
||||
## 🎯 针对性指导
|
||||
|
||||
### 专业分析
|
||||
|
||||
针对您关于综合运势的问题,本卦【困】在整体发展、综合状况、全面分析方面的指示是:处于困境的时期。虽处困境,但保持正道,终将脱困。。 变卦【兑】预示着在整体发展、综合状况、全面分析方面将会有所转变。 结合当前的时间因素(夏季,戌时),建议您适合积极行动。
|
||||
|
||||
## 🎯 动态指导
|
||||
|
||||
### 实用建议
|
||||
|
||||
综合来看,本卦【困】的总体指导是:"处于困境的时期。虽处困境,但保持正道,终将脱困。"。 变卦【兑】提示未来趋势:"充满喜悦和和谐的氛围。保持和悦态度,增进人际和谐。"。
|
||||
|
||||
## 🌟 易经智慧
|
||||
|
||||
### 核心信息
|
||||
|
||||
坚持正道。变化在即,喜悦和谐。
|
||||
|
||||
### 行动建议
|
||||
|
||||
保持信念,寻求突破 当前正值夏季,适合积极行动。 现在是戌时,休息调整。 考虑到即将到来的变化,建议:保持和悦,增进和谐。
|
||||
|
||||
### 时机把握
|
||||
|
||||
时机分析:晚间时光,阴气渐盛,适合思考和规划。 夏季适合积极行动。
|
||||
|
||||
## 📖 哲学洞察
|
||||
|
||||
《易经》困卦的核心智慧在于:泽中无水,象征困穷。君子应舍命达成志向。。 而变卦兑则提醒我们:两泽相连,象征喜悦。君子应与朋友讲习道义。。 《易经》告诉我们,万事万物都在变化之中,智者应该顺应这种变化,在变化中寻找机遇,在稳定中积蓄力量。
|
||||
|
||||
---
|
||||
|
||||
*本报告由神机阁AI命理分析平台生成*
|
||||
*生成时间:2025/8/21 19:09:13*
|
||||
*仅供参考,请理性对待*
|
||||
0
debug-pdf.cjs
Normal file
0
debug-pdf.cjs
Normal file
622
dist/assets/index-CGu5zB0q.js
vendored
Normal file
622
dist/assets/index-CGu5zB0q.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/assets/index-CW1NqJRG.css
vendored
Normal file
1
dist/assets/index-CW1NqJRG.css
vendored
Normal file
File diff suppressed because one or more lines are too long
352
dist/assets/index-CegexIGf.js
vendored
352
dist/assets/index-CegexIGf.js
vendored
File diff suppressed because one or more lines are too long
1
dist/assets/index-D2qhzDKn.css
vendored
1
dist/assets/index-D2qhzDKn.css
vendored
File diff suppressed because one or more lines are too long
18
dist/assets/index.es-DFUQDuH3.js
vendored
Normal file
18
dist/assets/index.es-DFUQDuH3.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
dist/assets/purify.es-CQJ0hv7W.js
vendored
Normal file
2
dist/assets/purify.es-CQJ0hv7W.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
dist/index.html
vendored
4
dist/index.html
vendored
@@ -4,8 +4,8 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<script type="module" crossorigin src="/assets/index-CegexIGf.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-D2qhzDKn.css">
|
||||
<script type="module" crossorigin src="/assets/index-CGu5zB0q.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CW1NqJRG.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
209
package-lock.json
generated
209
package-lock.json
generated
@@ -47,8 +47,10 @@
|
||||
"embla-carousel-react": "^8.5.2",
|
||||
"express": "^4.18.2",
|
||||
"helmet": "^7.1.0",
|
||||
"html2canvas": "^1.4.1",
|
||||
"input-otp": "^1.4.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jspdf": "^3.0.1",
|
||||
"lucide-react": "^0.364.0",
|
||||
"next-themes": "^0.4.4",
|
||||
"nodemon": "^3.0.2",
|
||||
@@ -3175,6 +3177,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/raf": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
|
||||
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@types/range-parser": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
|
||||
@@ -3249,6 +3258,13 @@
|
||||
"@types/send": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@types/yauzl": {
|
||||
"version": "2.10.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
|
||||
@@ -3733,6 +3749,18 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/atob": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
|
||||
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
|
||||
"license": "(MIT OR Apache-2.0)",
|
||||
"bin": {
|
||||
"atob": "bin/atob.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/autoprefixer": {
|
||||
"version": "10.4.20",
|
||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
|
||||
@@ -3855,6 +3883,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/base64-arraybuffer": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
@@ -4030,6 +4067,18 @@
|
||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||
}
|
||||
},
|
||||
"node_modules/btoa": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz",
|
||||
"integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==",
|
||||
"license": "(MIT OR Apache-2.0)",
|
||||
"bin": {
|
||||
"btoa": "bin/btoa.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||
@@ -4146,6 +4195,26 @@
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/canvg": {
|
||||
"version": "3.0.11",
|
||||
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz",
|
||||
"integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@types/raf": "^3.4.0",
|
||||
"core-js": "^3.8.3",
|
||||
"raf": "^3.4.1",
|
||||
"regenerator-runtime": "^0.13.7",
|
||||
"rgbcolor": "^1.0.1",
|
||||
"stackblur-canvas": "^2.0.0",
|
||||
"svg-pathdata": "^6.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
@@ -4876,6 +4945,18 @@
|
||||
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/core-js": {
|
||||
"version": "3.45.1",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.45.1.tgz",
|
||||
"integrity": "sha512-L4NPsJlCfZsPeXukyzHFlg/i7IIVwHSItR0wg0FLNqYClJ4MQYTYLbC7EkjKYRLZF2iof2MUgN0EGy7MdQFChg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/core-js"
|
||||
}
|
||||
},
|
||||
"node_modules/cors": {
|
||||
"version": "2.8.5",
|
||||
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
|
||||
@@ -4929,6 +5010,15 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/css-line-break": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
|
||||
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"utrie": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/cssesc": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||
@@ -5217,6 +5307,16 @@
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz",
|
||||
"integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"optional": true,
|
||||
"optionalDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
@@ -5846,6 +5946,12 @@
|
||||
"pend": "~1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fflate": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
||||
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/file-entry-cache": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
||||
@@ -6255,6 +6361,19 @@
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/html2canvas": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
||||
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"css-line-break": "^2.1.0",
|
||||
"text-segmentation": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
||||
@@ -6627,6 +6746,24 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/jspdf": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.1.tgz",
|
||||
"integrity": "sha512-qaGIxqxetdoNnFQQXxTKUD9/Z7AloLaw94fFsOiJMxbfYdBbrBuhWmbzI8TVjrw7s3jBY1PFHofBKMV/wZPapg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.7",
|
||||
"atob": "^2.1.2",
|
||||
"btoa": "^1.2.1",
|
||||
"fflate": "^0.8.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"canvg": "^3.0.11",
|
||||
"core-js": "^3.6.0",
|
||||
"dompurify": "^3.2.4",
|
||||
"html2canvas": "^1.0.0-rc.5"
|
||||
}
|
||||
},
|
||||
"node_modules/jwa": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz",
|
||||
@@ -7392,6 +7529,13 @@
|
||||
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/performance-now": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -7779,6 +7923,16 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/raf": {
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
|
||||
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"performance-now": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/range-parser": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
@@ -8107,6 +8261,13 @@
|
||||
"decimal.js-light": "^2.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/regenerator-runtime": {
|
||||
"version": "0.13.11",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
@@ -8155,6 +8316,16 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rgbcolor": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
|
||||
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
|
||||
"license": "MIT OR SEE LICENSE IN FEEL-FREE.md",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.8.15"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.46.2",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.2.tgz",
|
||||
@@ -8599,6 +8770,16 @@
|
||||
"resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz",
|
||||
"integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ=="
|
||||
},
|
||||
"node_modules/stackblur-canvas": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
|
||||
"integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=0.1.14"
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
@@ -8785,6 +8966,16 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/svg-pathdata": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
|
||||
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwind-merge": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz",
|
||||
@@ -8878,6 +9069,15 @@
|
||||
"b4a": "^1.6.4"
|
||||
}
|
||||
},
|
||||
"node_modules/text-segmentation": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
||||
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"utrie": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/thenify": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
|
||||
@@ -9236,6 +9436,15 @@
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/utrie": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
|
||||
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-arraybuffer": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
|
||||
@@ -57,8 +57,10 @@
|
||||
"embla-carousel-react": "^8.5.2",
|
||||
"express": "^4.18.2",
|
||||
"helmet": "^7.1.0",
|
||||
"html2canvas": "^1.4.1",
|
||||
"input-otp": "^1.4.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jspdf": "^3.0.1",
|
||||
"lucide-react": "^0.364.0",
|
||||
"next-themes": "^0.4.4",
|
||||
"nodemon": "^3.0.2",
|
||||
|
||||
@@ -4,7 +4,6 @@ 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();
|
||||
|
||||
@@ -68,7 +67,7 @@ router.post('/', authenticate, async (req, res) => {
|
||||
const minute = String(analysisDate.getMinutes()).padStart(2, '0');
|
||||
const second = String(analysisDate.getSeconds()).padStart(2, '0');
|
||||
|
||||
const dateStr = `${year}-${month}-${day}`;
|
||||
const dateStr = `${year}${month}${day}`;
|
||||
const timeStr = `${hour}${minute}${second}`;
|
||||
|
||||
// 分析类型映射
|
||||
@@ -79,8 +78,9 @@ router.post('/', authenticate, async (req, res) => {
|
||||
};
|
||||
|
||||
const analysisTypeName = analysisTypeMap[analysisType] || analysisType;
|
||||
const baseFilename = `${analysisTypeName}_${userName || 'user'}_${dateStr}_${timeStr}`;
|
||||
// 文件名格式: 八字命理_午饭_2025-08-21_133105
|
||||
const exportMode = '服务器导出';
|
||||
const baseFilename = `${analysisTypeName}_${userName || 'user'}_${exportMode}_${dateStr}_${timeStr}`;
|
||||
// 文件名格式: 八字命理_午饭_服务器导出_20250821_133105
|
||||
|
||||
try {
|
||||
switch (format) {
|
||||
@@ -98,12 +98,7 @@ router.post('/', authenticate, async (req, res) => {
|
||||
filename = `${baseFilename}.pdf`;
|
||||
break;
|
||||
|
||||
case 'png':
|
||||
fileBuffer = await generatePNG(analysisData, analysisType, userName);
|
||||
contentType = 'image/png';
|
||||
fileExtension = 'png';
|
||||
filename = `${baseFilename}.png`;
|
||||
break;
|
||||
|
||||
}
|
||||
} catch (generationError) {
|
||||
console.error(`生成${format}文件失败:`, generationError);
|
||||
|
||||
@@ -1,634 +0,0 @@
|
||||
/**
|
||||
* PNG图片生成器
|
||||
* 将分析结果转换为PNG图片格式
|
||||
* 使用Puppeteer将SVG转换为PNG
|
||||
*/
|
||||
|
||||
const puppeteer = require('puppeteer');
|
||||
|
||||
const generatePNG = async (analysisData, analysisType, userName) => {
|
||||
let browser;
|
||||
try {
|
||||
// 生成SVG内容
|
||||
const svgContent = await generateImageData(analysisData, analysisType, userName);
|
||||
|
||||
// 创建包含SVG的HTML页面
|
||||
const htmlContent = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body { margin: 0; padding: 0; }
|
||||
svg { display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${svgContent}
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
// 启动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'
|
||||
],
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
|
||||
// 设置页面内容
|
||||
await page.setContent(htmlContent, {
|
||||
waitUntil: 'networkidle0'
|
||||
});
|
||||
|
||||
// 设置视口大小
|
||||
await page.setViewport({ width: 800, height: 1200 });
|
||||
|
||||
// 截图生成PNG
|
||||
const pngBuffer = await page.screenshot({
|
||||
type: 'png',
|
||||
fullPage: true,
|
||||
omitBackground: false
|
||||
});
|
||||
|
||||
// 确保返回的是Buffer对象
|
||||
if (!Buffer.isBuffer(pngBuffer)) {
|
||||
console.warn('Puppeteer返回的不是Buffer,正在转换:', typeof pngBuffer);
|
||||
return Buffer.from(pngBuffer);
|
||||
}
|
||||
|
||||
return pngBuffer;
|
||||
|
||||
} catch (error) {
|
||||
console.error('生成PNG失败:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
if (browser) {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成图片数据(SVG格式)
|
||||
*/
|
||||
const generateImageData = async (analysisData, analysisType, userName) => {
|
||||
const timestamp = new Date().toLocaleString('zh-CN');
|
||||
const analysisTypeLabel = getAnalysisTypeLabel(analysisType);
|
||||
|
||||
// 生成SVG内容
|
||||
let svg = `
|
||||
<svg width="800" height="1200" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<style>
|
||||
${getSVGStyles()}
|
||||
</style>
|
||||
<linearGradient id="headerGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#dc2626;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#b91c1c;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feDropShadow dx="2" dy="2" stdDeviation="3" flood-color="rgba(0,0,0,0.3)"/>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- 背景 -->
|
||||
<rect width="800" height="1200" fill="#f9f9f9"/>
|
||||
|
||||
<!-- 头部 -->
|
||||
<rect width="800" height="200" fill="url(#headerGradient)"/>
|
||||
|
||||
<!-- 标题 -->
|
||||
<text x="400" y="60" class="main-title" text-anchor="middle" fill="white" filter="url(#shadow)">神机阁</text>
|
||||
<text x="400" y="90" class="subtitle" text-anchor="middle" fill="white">专业命理分析平台</text>
|
||||
|
||||
<!-- 分割线 -->
|
||||
<line x1="100" y1="110" x2="700" y2="110" stroke="rgba(255,255,255,0.3)" stroke-width="1"/>
|
||||
|
||||
<!-- 报告信息 -->
|
||||
<text x="400" y="140" class="report-title" text-anchor="middle" fill="white">${analysisTypeLabel}分析报告</text>
|
||||
<text x="200" y="170" class="info-text" fill="white">姓名:${userName || '用户'}</text>
|
||||
<text x="500" y="170" class="info-text" fill="white">生成时间:${timestamp.split(' ')[0]}</text>
|
||||
|
||||
<!-- 内容区域背景 -->
|
||||
<rect x="50" y="220" width="700" height="900" fill="white" rx="10" ry="10" filter="url(#shadow)"/>
|
||||
|
||||
`;
|
||||
|
||||
// 根据分析类型添加不同内容
|
||||
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 += `
|
||||
<!-- 页脚 -->
|
||||
<rect x="50" y="1140" width="700" height="50" fill="#f8f9fa" rx="0" ry="0"/>
|
||||
<text x="400" y="1160" class="footer-text" text-anchor="middle" fill="#666">本报告由神机阁AI命理分析平台生成,仅供参考</text>
|
||||
<text x="400" y="1180" class="footer-text" text-anchor="middle" fill="#666">© 2025 神机阁 - AI命理分析平台</text>
|
||||
|
||||
</svg>
|
||||
`;
|
||||
|
||||
return svg;
|
||||
};
|
||||
|
||||
/**
|
||||
* 添加八字命理内容
|
||||
*/
|
||||
const addBaziContent = (svg, analysisData, yOffset) => {
|
||||
let content = '';
|
||||
|
||||
// 基本信息
|
||||
if (analysisData.basic_info) {
|
||||
content += `
|
||||
<!-- 基本信息 -->
|
||||
<text x="80" y="${yOffset}" class="section-title" fill="#dc2626">📋 基本信息</text>
|
||||
`;
|
||||
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 += `
|
||||
<text x="100" y="${yOffset}" class="info-label" fill="#333">姓名:</text>
|
||||
<text x="160" y="${yOffset}" class="info-value" fill="#666">${personal.name || '未提供'}</text>
|
||||
<text x="400" y="${yOffset}" class="info-label" fill="#333">性别:</text>
|
||||
<text x="460" y="${yOffset}" class="info-value" fill="#666">${genderText}</text>
|
||||
`;
|
||||
yOffset += 30;
|
||||
|
||||
content += `
|
||||
<text x="100" y="${yOffset}" class="info-label" fill="#333">出生日期:</text>
|
||||
<text x="180" y="${yOffset}" class="info-value" fill="#666">${personal.birth_date || '未提供'}</text>
|
||||
`;
|
||||
yOffset += 30;
|
||||
|
||||
content += `
|
||||
<text x="100" y="${yOffset}" class="info-label" fill="#333">出生时间:</text>
|
||||
<text x="180" y="${yOffset}" class="info-value" fill="#666">${personal.birth_time || '未提供'}</text>
|
||||
`;
|
||||
yOffset += 40;
|
||||
}
|
||||
|
||||
// 八字信息
|
||||
if (analysisData.basic_info.bazi_info) {
|
||||
const bazi = analysisData.basic_info.bazi_info;
|
||||
content += `
|
||||
<text x="100" y="${yOffset}" class="subsection-title" fill="#b91c1c">🔮 八字信息</text>
|
||||
`;
|
||||
yOffset += 30;
|
||||
|
||||
// 表格头
|
||||
content += `
|
||||
<rect x="100" y="${yOffset}" width="600" height="25" fill="#dc2626" rx="3"/>
|
||||
<text x="130" y="${yOffset + 17}" class="table-header" fill="white">柱位</text>
|
||||
<text x="230" y="${yOffset + 17}" class="table-header" fill="white">天干</text>
|
||||
<text x="330" y="${yOffset + 17}" class="table-header" fill="white">地支</text>
|
||||
<text x="430" y="${yOffset + 17}" class="table-header" fill="white">纳音</text>
|
||||
`;
|
||||
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 += `
|
||||
<rect x="100" y="${yOffset}" width="600" height="25" fill="${bgColor}" stroke="#ddd" stroke-width="0.5"/>
|
||||
<text x="130" y="${yOffset + 17}" class="table-cell" fill="#333">${pillar.name}</text>
|
||||
<text x="230" y="${yOffset + 17}" class="table-cell" fill="#333">${pillar.data?.split('')[0] || '-'}</text>
|
||||
<text x="330" y="${yOffset + 17}" class="table-cell" fill="#333">${pillar.data?.split('')[1] || '-'}</text>
|
||||
<text x="430" y="${yOffset + 17}" class="table-cell" fill="#333">${pillar.nayin || '-'}</text>
|
||||
`;
|
||||
yOffset += 25;
|
||||
});
|
||||
|
||||
yOffset += 20;
|
||||
}
|
||||
}
|
||||
|
||||
// 五行分析
|
||||
if (analysisData.wuxing_analysis && yOffset < 1000) {
|
||||
content += `
|
||||
<text x="80" y="${yOffset}" class="section-title" fill="#dc2626">🌟 五行分析</text>
|
||||
`;
|
||||
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 += `
|
||||
<text x="100" y="${yOffset}" class="subsection-title" fill="#b91c1c">五行分布</text>
|
||||
`;
|
||||
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 += `
|
||||
<rect x="${xOffset}" y="${yOffset + 80 - barHeight}" width="30" height="${barHeight}" fill="${elementColor}" rx="2"/>
|
||||
<text x="${xOffset + 15}" y="${yOffset + 100}" class="element-label" text-anchor="middle" fill="#333">${element}</text>
|
||||
<text x="${xOffset + 15}" y="${yOffset + 115}" class="element-count" text-anchor="middle" fill="#666">${numCount}</text>
|
||||
<text x="${xOffset + 15}" y="${yOffset + 130}" class="element-percent" text-anchor="middle" fill="#666">${percentage}%</text>
|
||||
`;
|
||||
|
||||
xOffset += 100;
|
||||
});
|
||||
|
||||
yOffset += 150;
|
||||
}
|
||||
|
||||
if (analysisData.wuxing_analysis.balance_analysis && yOffset < 1000) {
|
||||
content += `
|
||||
<text x="100" y="${yOffset}" class="subsection-title" fill="#b91c1c">五行平衡分析</text>
|
||||
`;
|
||||
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 += `
|
||||
<text x="120" y="${yOffset}" class="analysis-text" fill="#555">${line}</text>
|
||||
`;
|
||||
yOffset += 20;
|
||||
}
|
||||
});
|
||||
|
||||
yOffset += 20;
|
||||
}
|
||||
}
|
||||
|
||||
svg += content;
|
||||
return yOffset;
|
||||
};
|
||||
|
||||
/**
|
||||
* 添加紫微斗数内容
|
||||
*/
|
||||
const addZiweiContent = (svg, analysisData, yOffset) => {
|
||||
let content = '';
|
||||
|
||||
// 基本信息
|
||||
if (analysisData.basic_info) {
|
||||
content += `
|
||||
<text x="80" y="${yOffset}" class="section-title" fill="#dc2626">📋 基本信息</text>
|
||||
`;
|
||||
yOffset += 40;
|
||||
|
||||
if (analysisData.basic_info.ziwei_info) {
|
||||
const ziwei = analysisData.basic_info.ziwei_info;
|
||||
|
||||
if (ziwei.ming_gong) {
|
||||
content += `
|
||||
<text x="100" y="${yOffset}" class="info-label" fill="#333">命宫:</text>
|
||||
<text x="160" y="${yOffset}" class="info-highlight" fill="#dc2626">${ziwei.ming_gong}</text>
|
||||
`;
|
||||
yOffset += 30;
|
||||
}
|
||||
|
||||
if (ziwei.wuxing_ju) {
|
||||
content += `
|
||||
<text x="100" y="${yOffset}" class="info-label" fill="#333">五行局:</text>
|
||||
<text x="180" y="${yOffset}" class="info-highlight" fill="#dc2626">${ziwei.wuxing_ju}</text>
|
||||
`;
|
||||
yOffset += 30;
|
||||
}
|
||||
|
||||
if (ziwei.main_stars) {
|
||||
const starsText = Array.isArray(ziwei.main_stars) ? ziwei.main_stars.join('、') : ziwei.main_stars;
|
||||
content += `
|
||||
<text x="100" y="${yOffset}" class="info-label" fill="#333">主星:</text>
|
||||
<text x="160" y="${yOffset}" class="info-highlight" fill="#dc2626">${starsText}</text>
|
||||
`;
|
||||
yOffset += 40;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 星曜分析
|
||||
if (analysisData.star_analysis && yOffset < 1000) {
|
||||
content += `
|
||||
<text x="80" y="${yOffset}" class="section-title" fill="#dc2626">⭐ 星曜分析</text>
|
||||
`;
|
||||
yOffset += 40;
|
||||
|
||||
if (analysisData.star_analysis.main_stars) {
|
||||
content += `
|
||||
<text x="100" y="${yOffset}" class="subsection-title" fill="#b91c1c">主星分析</text>
|
||||
`;
|
||||
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 += `
|
||||
<rect x="100" y="${yOffset - 15}" width="600" height="60" fill="#f1f5f9" rx="5" stroke="#3b82f6" stroke-width="2"/>
|
||||
<text x="120" y="${yOffset + 5}" class="star-name" fill="#1e40af">${star.name || star.star}</text>
|
||||
`;
|
||||
|
||||
if (star.brightness) {
|
||||
content += `
|
||||
<text x="120" y="${yOffset + 25}" class="star-detail" fill="#333">亮度:${star.brightness}</text>
|
||||
`;
|
||||
}
|
||||
|
||||
if (star.influence) {
|
||||
const influenceText = star.influence.substring(0, 60) + (star.influence.length > 60 ? '...' : '');
|
||||
content += `
|
||||
<text x="120" y="${yOffset + 40}" class="star-detail" fill="#555">影响:${influenceText}</text>
|
||||
`;
|
||||
}
|
||||
|
||||
yOffset += 80;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
svg += content;
|
||||
return yOffset;
|
||||
};
|
||||
|
||||
/**
|
||||
* 添加易经占卜内容
|
||||
*/
|
||||
const addYijingContent = (svg, analysisData, yOffset) => {
|
||||
let content = '';
|
||||
|
||||
// 占卜问题
|
||||
if (analysisData.question_analysis) {
|
||||
content += `
|
||||
<text x="80" y="${yOffset}" class="section-title" fill="#dc2626">❓ 占卜问题</text>
|
||||
`;
|
||||
yOffset += 40;
|
||||
|
||||
if (analysisData.question_analysis.original_question) {
|
||||
content += `
|
||||
<text x="100" y="${yOffset}" class="info-label" fill="#333">问题:</text>
|
||||
`;
|
||||
|
||||
const questionText = analysisData.question_analysis.original_question;
|
||||
const questionLines = wrapText(questionText, 45);
|
||||
|
||||
questionLines.forEach((line, index) => {
|
||||
content += `
|
||||
<text x="${index === 0 ? 160 : 120}" y="${yOffset}" class="info-highlight" fill="#dc2626">${line}</text>
|
||||
`;
|
||||
yOffset += 20;
|
||||
});
|
||||
|
||||
yOffset += 10;
|
||||
}
|
||||
|
||||
if (analysisData.question_analysis.question_type) {
|
||||
content += `
|
||||
<text x="100" y="${yOffset}" class="info-label" fill="#333">问题类型:</text>
|
||||
<text x="180" y="${yOffset}" class="info-value" fill="#666">${analysisData.question_analysis.question_type}</text>
|
||||
`;
|
||||
yOffset += 40;
|
||||
}
|
||||
}
|
||||
|
||||
// 卦象信息
|
||||
if (analysisData.hexagram_info && yOffset < 1000) {
|
||||
content += `
|
||||
<text x="80" y="${yOffset}" class="section-title" fill="#dc2626">🔮 卦象信息</text>
|
||||
`;
|
||||
yOffset += 40;
|
||||
|
||||
if (analysisData.hexagram_info.main_hexagram) {
|
||||
const main = analysisData.hexagram_info.main_hexagram;
|
||||
|
||||
content += `
|
||||
<rect x="100" y="${yOffset - 15}" width="600" height="100" fill="#fef3c7" rx="8" stroke="#fbbf24" stroke-width="2"/>
|
||||
<text x="120" y="${yOffset + 10}" class="subsection-title" fill="#92400e">主卦</text>
|
||||
|
||||
<text x="120" y="${yOffset + 35}" class="info-label" fill="#333">卦名:</text>
|
||||
<text x="180" y="${yOffset + 35}" class="hexagram-name" fill="#dc2626">${main.name || '未知'}</text>
|
||||
|
||||
<text x="400" y="${yOffset + 35}" class="info-label" fill="#333">卦象:</text>
|
||||
<text x="460" y="${yOffset + 35}" class="hexagram-symbol" fill="#92400e">${main.symbol || ''}</text>
|
||||
`;
|
||||
|
||||
if (main.meaning) {
|
||||
const meaningText = main.meaning.substring(0, 50) + (main.meaning.length > 50 ? '...' : '');
|
||||
content += `
|
||||
<text x="120" y="${yOffset + 60}" class="info-label" fill="#333">含义:</text>
|
||||
<text x="180" y="${yOffset + 60}" class="info-value" fill="#666">${meaningText}</text>
|
||||
`;
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
@@ -276,14 +276,15 @@ const CompleteBaziAnalysis: React.FC<CompleteBaziAnalysisProps> = ({ birthDate,
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-red-50 to-yellow-50 py-8">
|
||||
<div className="max-w-7xl mx-auto px-4 space-y-8">
|
||||
<div className="max-w-7xl mx-auto px-4 space-y-8" id="bazi-analysis-content" data-export-content>
|
||||
|
||||
{/* 下载按钮 */}
|
||||
<div className="flex justify-end">
|
||||
<div className="flex justify-end no-export" data-no-export>
|
||||
<DownloadButton
|
||||
analysisData={analysisData}
|
||||
analysisType="bazi"
|
||||
userName={birthDate.name}
|
||||
targetElementId="bazi-analysis-content"
|
||||
className="sticky top-4 z-10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -264,14 +264,15 @@ const CompleteYijingAnalysis: React.FC<CompleteYijingAnalysisProps> = ({
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-red-50 to-yellow-50 py-8">
|
||||
<div className="max-w-7xl mx-auto px-4 space-y-8">
|
||||
<div className="max-w-7xl mx-auto px-4 space-y-8" id="yijing-analysis-content" data-export-content>
|
||||
|
||||
{/* 下载按钮 */}
|
||||
<div className="flex justify-end">
|
||||
<div className="flex justify-end no-export" data-no-export>
|
||||
<DownloadButton
|
||||
analysisData={analysisData}
|
||||
analysisType="yijing"
|
||||
userName={question ? `占卜_${question.substring(0, 10)}` : 'user'}
|
||||
targetElementId="yijing-analysis-content"
|
||||
className="sticky top-4 z-10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -579,14 +579,15 @@ const CompleteZiweiAnalysis: React.FC<CompleteZiweiAnalysisProps> = ({ birthDate
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-purple-50 to-indigo-50 py-8">
|
||||
<div className="max-w-7xl mx-auto px-4 space-y-8">
|
||||
<div className="max-w-7xl mx-auto px-4 space-y-8" id="ziwei-analysis-content" data-export-content>
|
||||
|
||||
{/* 下载按钮 */}
|
||||
<div className="flex justify-end">
|
||||
<div className="flex justify-end no-export" data-no-export>
|
||||
<DownloadButton
|
||||
analysisData={analysisData}
|
||||
analysisType="ziwei"
|
||||
userName={birthDate.name}
|
||||
targetElementId="ziwei-analysis-content"
|
||||
className="sticky top-4 z-10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import React, { useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Download, FileText, FileImage, File, Loader2, ChevronDown } from 'lucide-react';
|
||||
import { Download, FileText, FileImage, File, Loader2, ChevronDown, Printer, Camera } from 'lucide-react';
|
||||
import { ChineseButton } from './ChineseButton';
|
||||
import { cn } from '../../lib/utils';
|
||||
import html2canvas from 'html2canvas';
|
||||
import jsPDF from 'jspdf';
|
||||
|
||||
export type DownloadFormat = 'markdown' | 'pdf' | 'png';
|
||||
export type ExportMode = 'server' | 'frontend';
|
||||
|
||||
interface DownloadButtonProps {
|
||||
analysisData: any;
|
||||
@@ -13,6 +16,7 @@ interface DownloadButtonProps {
|
||||
onDownload?: (format: DownloadFormat) => Promise<void>;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
targetElementId?: string; // 用于前端导出的目标元素ID
|
||||
}
|
||||
|
||||
const DownloadButton: React.FC<DownloadButtonProps> = ({
|
||||
@@ -21,40 +25,71 @@ const DownloadButton: React.FC<DownloadButtonProps> = ({
|
||||
userName,
|
||||
onDownload,
|
||||
className,
|
||||
disabled = false
|
||||
disabled = false,
|
||||
targetElementId
|
||||
}) => {
|
||||
const [isDownloading, setIsDownloading] = useState(false);
|
||||
const [downloadingFormat, setDownloadingFormat] = useState<DownloadFormat | null>(null);
|
||||
const [showDropdown, setShowDropdown] = useState(false);
|
||||
|
||||
const formatOptions = [
|
||||
const allFormatOptions = [
|
||||
{
|
||||
format: 'markdown' as DownloadFormat,
|
||||
label: 'Markdown文档',
|
||||
icon: FileText,
|
||||
description: '结构化文本格式,便于编辑',
|
||||
color: 'text-blue-600',
|
||||
bgColor: 'bg-blue-50 hover:bg-blue-100'
|
||||
bgColor: 'bg-blue-50 hover:bg-blue-100',
|
||||
mode: 'server' as ExportMode
|
||||
},
|
||||
{
|
||||
format: 'pdf' as DownloadFormat,
|
||||
label: 'PDF文档',
|
||||
label: 'PDF文档(服务器生成)',
|
||||
icon: File,
|
||||
description: '专业格式,便于打印和分享',
|
||||
description: '服务器生成的PDF文档',
|
||||
color: 'text-red-600',
|
||||
bgColor: 'bg-red-50 hover:bg-red-100'
|
||||
bgColor: 'bg-red-50 hover:bg-red-100',
|
||||
mode: 'server' as ExportMode
|
||||
},
|
||||
{
|
||||
format: 'pdf' as DownloadFormat,
|
||||
label: 'PDF文档(页面导出)',
|
||||
icon: Printer,
|
||||
description: '直接从页面生成PDF,分页格式',
|
||||
color: 'text-purple-600',
|
||||
bgColor: 'bg-purple-50 hover:bg-purple-100',
|
||||
mode: 'frontend' as ExportMode
|
||||
},
|
||||
|
||||
{
|
||||
format: 'png' as DownloadFormat,
|
||||
label: 'PNG图片',
|
||||
icon: FileImage,
|
||||
description: '高清图片格式,便于保存',
|
||||
color: 'text-green-600',
|
||||
bgColor: 'bg-green-50 hover:bg-green-100'
|
||||
label: 'PNG长图(页面导出)',
|
||||
icon: Camera,
|
||||
description: '直接从页面生成PNG长图',
|
||||
color: 'text-teal-600',
|
||||
bgColor: 'bg-teal-50 hover:bg-teal-100',
|
||||
mode: 'frontend' as ExportMode
|
||||
}
|
||||
];
|
||||
|
||||
const handleDownload = async (format: DownloadFormat) => {
|
||||
// 根据是否有targetElementId来过滤选项
|
||||
const formatOptions = allFormatOptions.filter(option => {
|
||||
// 如果是前端导出模式,需要有targetElementId才显示
|
||||
if (option.mode === 'frontend') {
|
||||
return !!targetElementId;
|
||||
}
|
||||
// 服务器模式总是显示
|
||||
return true;
|
||||
});
|
||||
|
||||
console.log('DownloadButton配置:', {
|
||||
targetElementId,
|
||||
totalOptions: allFormatOptions.length,
|
||||
availableOptions: formatOptions.length,
|
||||
frontendOptionsAvailable: formatOptions.filter(o => o.mode === 'frontend').length
|
||||
});
|
||||
|
||||
const handleDownload = async (format: DownloadFormat, mode: ExportMode = 'server') => {
|
||||
if (disabled || isDownloading) return;
|
||||
|
||||
try {
|
||||
@@ -62,21 +97,193 @@ const DownloadButton: React.FC<DownloadButtonProps> = ({
|
||||
setDownloadingFormat(format);
|
||||
setShowDropdown(false);
|
||||
|
||||
if (onDownload) {
|
||||
if (mode === 'frontend') {
|
||||
// 前端导出逻辑
|
||||
await frontendExport(format);
|
||||
} else if (onDownload) {
|
||||
await onDownload(format);
|
||||
} else {
|
||||
// 默认下载逻辑
|
||||
// 默认服务器下载逻辑
|
||||
await defaultDownload(format);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('下载失败:', error);
|
||||
// 这里可以添加错误提示
|
||||
// 显示错误提示
|
||||
if (typeof window !== 'undefined' && (window as any).toast) {
|
||||
(window as any).toast.error(`下载失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||
}
|
||||
} finally {
|
||||
setIsDownloading(false);
|
||||
setDownloadingFormat(null);
|
||||
}
|
||||
};
|
||||
|
||||
// 前端导出功能
|
||||
const frontendExport = async (format: DownloadFormat) => {
|
||||
console.log('开始前端导出,格式:', format, '目标元素ID:', targetElementId);
|
||||
|
||||
if (!targetElementId) {
|
||||
const error = '未指定导出目标元素ID,无法使用前端导出功能';
|
||||
console.error(error);
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
const element = document.getElementById(targetElementId);
|
||||
console.log('查找目标元素:', targetElementId, '找到元素:', element);
|
||||
|
||||
if (!element) {
|
||||
const error = `未找到ID为"${targetElementId}"的元素,请确认页面已完全加载`;
|
||||
console.error(error);
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
console.log('目标元素尺寸:', {
|
||||
width: element.offsetWidth,
|
||||
height: element.offsetHeight,
|
||||
scrollWidth: element.scrollWidth,
|
||||
scrollHeight: element.scrollHeight
|
||||
});
|
||||
|
||||
if (format === 'png') {
|
||||
await exportToPNG(element);
|
||||
} else if (format === 'pdf') {
|
||||
await exportToPDF(element);
|
||||
}
|
||||
};
|
||||
|
||||
// 导出为PNG
|
||||
const exportToPNG = async (element: HTMLElement): Promise<void> => {
|
||||
const canvas = await html2canvas(element, {
|
||||
scale: 2,
|
||||
useCORS: true,
|
||||
allowTaint: true,
|
||||
backgroundColor: '#ffffff',
|
||||
scrollX: 0,
|
||||
scrollY: 0,
|
||||
logging: false,
|
||||
onclone: (clonedDoc) => {
|
||||
const elementsToHide = clonedDoc.querySelectorAll(
|
||||
'.no-export, [data-no-export], .fixed, .sticky, .floating'
|
||||
);
|
||||
elementsToHide.forEach(el => {
|
||||
(el as HTMLElement).style.display = 'none';
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const link = document.createElement('a');
|
||||
const fileName = getFileName('png', 'frontend');
|
||||
link.download = fileName;
|
||||
link.href = canvas.toDataURL('image/png', 1.0);
|
||||
link.click();
|
||||
|
||||
// 显示成功提示
|
||||
if (typeof window !== 'undefined' && (window as any).toast) {
|
||||
(window as any).toast.success('PNG长图导出成功');
|
||||
}
|
||||
};
|
||||
|
||||
// 导出为PDF
|
||||
const exportToPDF = async (element: HTMLElement): Promise<void> => {
|
||||
const canvas = await html2canvas(element, {
|
||||
scale: 1.5,
|
||||
useCORS: true,
|
||||
allowTaint: true,
|
||||
backgroundColor: '#ffffff',
|
||||
scrollX: 0,
|
||||
scrollY: 0,
|
||||
logging: false,
|
||||
onclone: (clonedDoc) => {
|
||||
const elementsToHide = clonedDoc.querySelectorAll(
|
||||
'.no-export, [data-no-export], .fixed, .sticky, .floating'
|
||||
);
|
||||
elementsToHide.forEach(el => {
|
||||
(el as HTMLElement).style.display = 'none';
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const imgData = canvas.toDataURL('image/png');
|
||||
const pdf = new jsPDF({
|
||||
orientation: 'portrait',
|
||||
unit: 'mm',
|
||||
format: 'a4'
|
||||
});
|
||||
|
||||
const pdfWidth = 210;
|
||||
const pdfHeight = 297;
|
||||
const margin = 10;
|
||||
const contentWidth = pdfWidth - 2 * margin;
|
||||
const contentHeight = pdfHeight - 2 * margin;
|
||||
|
||||
const imgWidth = canvas.width;
|
||||
const imgHeight = canvas.height;
|
||||
|
||||
// 优先填满宽度,让内容宽度占满页面
|
||||
const widthRatio = contentWidth / (imgWidth * 0.264583);
|
||||
const scaledWidth = contentWidth; // 直接使用全部可用宽度
|
||||
const scaledHeight = imgHeight * 0.264583 * widthRatio;
|
||||
|
||||
const pageHeight = contentHeight;
|
||||
const totalPages = Math.ceil(scaledHeight / pageHeight);
|
||||
|
||||
for (let i = 0; i < totalPages; i++) {
|
||||
if (i > 0) {
|
||||
pdf.addPage();
|
||||
}
|
||||
|
||||
const yOffset = -i * pageHeight;
|
||||
pdf.addImage(
|
||||
imgData,
|
||||
'PNG',
|
||||
margin,
|
||||
margin + yOffset,
|
||||
scaledWidth,
|
||||
scaledHeight
|
||||
);
|
||||
}
|
||||
|
||||
const fileName = getFileName('pdf', 'frontend');
|
||||
pdf.save(fileName);
|
||||
|
||||
// 显示成功提示
|
||||
if (typeof window !== 'undefined' && (window as any).toast) {
|
||||
(window as any).toast.success('PDF文档导出成功');
|
||||
}
|
||||
};
|
||||
|
||||
// 生成文件名
|
||||
const getFileName = (format: string, mode: ExportMode = 'server') => {
|
||||
const typeLabel = getAnalysisTypeLabel();
|
||||
const userPart = userName || 'user';
|
||||
const exportMode = mode === 'frontend' ? '页面导出' : '服务器导出';
|
||||
|
||||
// 获取分析报告生成时间
|
||||
let analysisDate;
|
||||
if (analysisData?.created_at) {
|
||||
analysisDate = new Date(analysisData.created_at);
|
||||
} else if (analysisData?.basic_info?.created_at) {
|
||||
analysisDate = new Date(analysisData.basic_info.created_at);
|
||||
} else if (analysisData?.metadata?.analysis_time) {
|
||||
analysisDate = new Date(analysisData.metadata.analysis_time);
|
||||
} else {
|
||||
// 如果没有分析时间,使用当前时间作为备用
|
||||
analysisDate = new Date();
|
||||
}
|
||||
|
||||
const year = analysisDate.getFullYear();
|
||||
const month = String(analysisDate.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(analysisDate.getDate()).padStart(2, '0');
|
||||
const hour = String(analysisDate.getHours()).padStart(2, '0');
|
||||
const minute = String(analysisDate.getMinutes()).padStart(2, '0');
|
||||
const second = String(analysisDate.getSeconds()).padStart(2, '0');
|
||||
|
||||
const dateStr = `${year}${month}${day}`;
|
||||
const timeStr = `${hour}${minute}${second}`;
|
||||
|
||||
return `${typeLabel}_${userPart}_${exportMode}_${dateStr}_${timeStr}.${format}`;
|
||||
};
|
||||
|
||||
const defaultDownload = async (format: DownloadFormat) => {
|
||||
try {
|
||||
// 获取认证token
|
||||
@@ -131,9 +338,10 @@ const DownloadButton: React.FC<DownloadButtonProps> = ({
|
||||
const minute = String(analysisDate.getMinutes()).padStart(2, '0');
|
||||
const second = String(analysisDate.getSeconds()).padStart(2, '0');
|
||||
|
||||
const dateStr = `${year}-${month}-${day}`;
|
||||
const dateStr = `${year}${month}${day}`;
|
||||
const timeStr = `${hour}${minute}${second}`;
|
||||
let filename = `${getAnalysisTypeLabel()}_${userName || 'user'}_${dateStr}_${timeStr}.${format === 'markdown' ? 'md' : format}`;
|
||||
const exportMode = '服务器导出';
|
||||
let filename = `${getAnalysisTypeLabel()}_${userName || 'user'}_${exportMode}_${dateStr}_${timeStr}.${format === 'markdown' ? 'md' : format}`;
|
||||
|
||||
if (contentDisposition) {
|
||||
const filenameMatch = contentDisposition.match(/filename[^;=\n]*=(['"]?)([^'"\n]*?)\1/);
|
||||
@@ -189,7 +397,7 @@ const DownloadButton: React.FC<DownloadButtonProps> = ({
|
||||
case 'markdown': return 'Markdown';
|
||||
case 'pdf': return 'PDF';
|
||||
case 'png': return 'PNG';
|
||||
default: return format.toUpperCase();
|
||||
default: return '';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -241,8 +449,8 @@ const DownloadButton: React.FC<DownloadButtonProps> = ({
|
||||
|
||||
return (
|
||||
<button
|
||||
key={option.format}
|
||||
onClick={() => handleDownload(option.format)}
|
||||
key={`${option.format}-${option.mode}`}
|
||||
onClick={() => handleDownload(option.format, option.mode)}
|
||||
disabled={disabled || isDownloading}
|
||||
className={cn(
|
||||
'w-full flex items-center space-x-3 p-3 rounded-lg transition-all duration-200',
|
||||
|
||||
@@ -156,8 +156,8 @@ const HistoryPage: React.FC = () => {
|
||||
|
||||
if (viewingResult && selectedReading) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-6" id="history-analysis-content" data-export-content>
|
||||
<div className="flex items-center justify-between no-export" data-no-export>
|
||||
<ChineseButton
|
||||
variant="outline"
|
||||
onClick={() => setViewingResult(false)}
|
||||
|
||||
0
test-download-flow.cjs
Normal file
0
test-download-flow.cjs
Normal file
0
test-pdf.cjs
Normal file
0
test-pdf.cjs
Normal file
0
test-pdf.js
Normal file
0
test-pdf.js
Normal file
0
test-yijing-pdf.cjs
Normal file
0
test-yijing-pdf.cjs
Normal file
Reference in New Issue
Block a user