You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

技术问询:能否在Flask中渲染图表模板并保存为PDF?

无需新开页面,将Chart.js图表与表格导出为PDF的实现方案

完全可以实现你的需求,不需要通过新开页面打印的方式生成PDF。我们可以用jsPDF结合html2canvas,直接在当前页面捕获已渲染的图表和表格,生成并下载PDF文件。

实现步骤

  1. 引入必要依赖:在页面头部添加jsPDF和html2canvas脚本,用于处理DOM转Canvas及PDF生成
  2. 添加导出按钮:在页面中添加触发导出操作的按钮
  3. 编写导出逻辑:编写函数捕获页面中的图表、表格等内容,转换后生成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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 09:45:54