技术问询:能否在Flask中渲染图表模板并保存为PDF?
无需新开页面,将Chart.js图表与表格导出为PDF的实现方案
完全可以实现你的需求,不需要通过新开页面打印的方式生成PDF。我们可以用jsPDF结合html2canvas,直接在当前页面捕获已渲染的图表和表格,生成并下载PDF文件。
实现步骤
- 引入必要依赖:在页面头部添加
jsPDF和html2canvas脚本,用于处理DOM转Canvas及PDF生成 - 添加导出按钮:在页面中添加触发导出操作的按钮
- 编写导出逻辑:编写函数捕获页面中的图表、表格等内容,转换后生成PDF
修改后的完整代码
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Local request</title> <meta name="viewport" content="width=297mm, initial-scale=1.0"> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <!-- 引入jsPDF和html2canvas依赖 --> <script src="https://cdn.jsdelivr.net/npm/jspdf@2.5.1/dist/jspdf.umd.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script> <style> .canvas { max-width: 297mm; max-height: 297mm; margin-left: auto; margin-right: auto; display: block; break-inside: avoid; page-break-inside: avoid; } table, th, td { text-align: center; border-collapse: collapse; border: 1px solid black; } th { background-color: grey; } .export-btn { display: block; margin: 20px auto; padding: 10px 20px; font-size: 16px; cursor: pointer; } </style> </head> <body> <h1 style="text-align: center">XPF REPORT</h1> <!-- 添加导出按钮 --> <button class="export-btn" id="exportPdfBtn">导出为PDF</button> <p><br></p> <p><br></p> <div class="canvas"> <h2 style="text-align: left;">PRI par IC et par etape</h2> <canvas id="PRIParICEtParEtape"></canvas> </div> <p><br></p> <div class="canvas"> <center> <h2 style="text-align: left;">PRI par IC et par etape / table</h2> <table width="300px"> <thead> <tr> <th>IC</th> <th>Total</th> </tr> </thead> <tbody id="icTableBody"> </tbody> <tfoot> <tr> <td>Total:</td> <td id="totalSum"></td> </tr> </tfoot> </table> </center> </div> <div><p><br></p></div> <div class="canvas"> <h2 style="text-align: left;">PRI par DSP et par etape</h2> <canvas id="PRIParDSPEtParEtape"></canvas> </div> <script> // 声明全局图表实例,确保导出时已渲染完成 let chart1, chart2; document.addEventListener('DOMContentLoaded', function (event) { fetchData('http://localhost:8069/xpf_report') .then(data => { console.log(data); chart2 = PRIParDSPEtParEtape(data); chart1 = PRIParICEtParEtape(data); createICTable(data); // 图表渲染完成后绑定导出事件 bindExportEvent(); }) .catch(error => console.error('Error fetching data:', error)); }); function bindExportEvent() { const exportBtn = document.getElementById('exportPdfBtn'); exportBtn.addEventListener('click', async () => { // 等待图表完全渲染 await Promise.all([chart1.rendered, chart2.rendered]); const { jsPDF } = window.jspdf; const pdf = new jsPDF('p', 'mm', 'a4'); const pageWidth = pdf.internal.pageSize.getWidth(); // 遍历所有需要导出的内容容器 const contentElements = document.querySelectorAll('.canvas, h1'); let yPosition = 20; // 页面起始Y坐标 for (const element of contentElements) { // 将DOM元素转为Canvas const canvas = await html2canvas(element, { scale: 2, // 提高分辨率,避免PDF模糊 useCORS: true }); const imgData = canvas.toDataURL('image/png'); const imgWidth = pageWidth - 40; // 左右留20mm边距 const imgHeight = (canvas.height * imgWidth) / canvas.width; // 如果当前内容超出页面剩余空间,添加新页面 if (yPosition + imgHeight > 297 - 20) { // A4页面高度297mm,底部留20mm边距 pdf.addPage(); yPosition = 20; } // 将图片添加到PDF pdf.addImage(imgData, 'PNG', 20, yPosition, imgWidth, imgHeight); yPosition += imgHeight + 15; // 内容间留15mm间距 } // 下载PDF pdf.save('XPF_REPORT.pdf'); }); } function createICTable(data) { const icTableBody = document.getElementById('icTableBody'); const totalSumElement = document.getElementById('totalSum'); const icTotalSum = {}; data.forEach(lead => { const icName = lead.user.name; const expectedRevenue = lead.expected_revenue; if (!icTotalSum[icName]) { icTotalSum[icName] = 0 } icTotalSum[icName] += expectedRevenue; }); let totalSum = 0; Object.keys(icTotalSum).forEach(icName => { const row = document.createElement('tr'); const icCell = document.createElement('td'); const totalCell = document.createElement('td'); icCell.textContent = icName; totalCell.textContent = icTotalSum[icName].toLocaleString('fr', {useGrouping: true}); row.append(icCell); row.append(totalCell); icTableBody.appendChild(row); totalSum += icTotalSum[icName]; }); totalSumElement.textContent = totalSum.toLocaleString('fr', {useGrouping: true}) } function PRIParICEtParEtape(data) { const userStageSum = {}; data.forEach(lead => { const userName = lead.user.name; const stage = lead.stage.stage; const expectedRevenue = lead.expected_revenue; if (!userStageSum[userName]) { userStageSum[userName] = {}; } if (!userStageSum[userName][stage]) { userStageSum[userName][stage] = 0; } userStageSum[userName][stage] += expectedRevenue; }); const ctx = document.getElementById('PRIParICEtParEtape').getContext('2d'); const userNames = Object.keys(userStageSum); const stages = [...new Set(data.map(lead => lead.stage.stage))]; const datasets = stages.map(stage => ({ label: stage, data: userNames.map(userName => userStageSum[userName][stage] || 0), borderWidth: 1 })); return new Chart(ctx, { type: 'bar', data: { labels: userNames, datasets: datasets }, options: { scales: { x: { type: 'category', stacked: true, ticks: { color: 'black', order: [] } }, y: { stacked: true, beginAtZero: true, ticks: {} } } } }); } function PRIParDSPEtParEtape(data) { const dspStageSum = {}; data.forEach(lead => { const dspName = lead.dsp?.name || 'Unknown DSP'; const stage = lead.stage.stage; const expectedRevenue = lead.expected_revenue; if (!dspStageSum[dspName]) { dspStageSum[dspName] = {}; } if (!dspStageSum[dspName][stage]) { dspStageSum[dspName][stage] = 0; } dspStageSum[dspName][stage] += expectedRevenue; }); const ctx = document.getElementById('PRIParDSPEtParEtape').getContext('2d'); const dspNames = Object.keys(dspStageSum); const stages = [...new Set(data.map(lead => lead.stage.stage))]; const datasets = stages.map(stage => ({ label: stage, data: dspNames.map(dspName => dspStageSum[dspName][stage] || 0), borderWidth: 1 })); return new Chart(ctx, { type: 'bar', data: { labels: dspNames, datasets: datasets }, options: { scales: { x: { type: 'category', stacked: true, ticks: { color: 'black', order: [] } }, y: { stacked: true, beginAtZero: true, ticks: {} } } } }); } async function fetchData(apiUrl) { const response = await fetch(apiUrl); return await response.json(); } </script> </body> </html>
关键说明
- 依赖作用:
html2canvas负责将DOM元素(包括Chart.js生成的canvas)转换为图片,jsPDF负责将图片拼接成PDF并下载 - 渲染等待:通过保存Chart实例并等待其
renderedPromise,确保图表完全绘制后再捕获,避免导出空白图表 - 分页处理:自动检测内容高度,超出当前页面时自动添加新页面,保证排版整齐
- 分辨率优化:设置
scale:2提高Canvas分辨率,避免PDF中的图表和文字模糊
内容的提问来源于stack exchange,提问作者Claudiu
相关产品推荐
相关产品推荐

