跳轉到

HTML 報告範例

需求套件nextpdf/artisan 難度:中級

完整程式碼

<?php

declare(strict_types=1);

use NextPDF\Artisan\ArtisanRenderer;
use NextPDF\Artisan\ValueObjects\RenderOptions;
use NextPDF\Core\ValueObjects\PageSize;
use NextPDF\Core\ValueObjects\Margin;

// ─── HTML 範本 ────────────────────────────────────────────────
$reportData = [
    'title'    => '2024 Q4 業績報告',
    'company'  => 'NextPDF 科技有限公司',
    'period'   => '2024 年 10 月 — 12 月',
    'revenue'  => 'NT$ 12,580,000',
    'growth'   => '+23.5%',
    'items'    => [
        ['month' => '10 月', 'value' => 3_820_000],
        ['month' => '11 月', 'value' => 4_150_000],
        ['month' => '12 月', 'value' => 4_610_000],
    ],
];

$html = buildReportHtml($reportData);

// ─── 渲染為 PDF ───────────────────────────────────────────────
$renderer = ArtisanRenderer::create(
    chromePath: $_ENV['CHROME_PATH'] ?? '/usr/bin/google-chrome',
);

$pdf = $renderer->renderHtml(
    html:    $html,
    options: RenderOptions::create(
        pageSize:         PageSize::A4,
        margin:           Margin::uniform(0.0),  // HTML 自行處理邊距
        waitUntil:        'networkidle0',         // 等待圖表渲染完成
        timeout:          30_000,                 // 30 秒超時
        printBackground:  true,                   // 列印背景色
    )->withHeaderTemplate(
        html: '<div style="font-size:9px;color:#6B7280;text-align:right;
                           width:100%;padding-right:15mm;">
                   <span class="pageNumber"></span> / <span class="totalPages"></span>
               </div>',
    ),
);

$pdf->save('/output/q4-report.pdf');

// ─── HTML 建構函式 ────────────────────────────────────────────
function buildReportHtml(array $data): string
{
    $chartData = json_encode(array_column($data['items'], 'value'));
    $chartLabels = json_encode(array_column($data['items'], 'month'));

    return <<<HTML
    <!DOCTYPE html>
    <html lang="zh-TW">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>{$data['title']}</title>
        <link rel="preconnect" href="https://fonts.googleapis.com">
        <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@400;700&display=swap"
              rel="stylesheet">
        <script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
        <style>
            * { box-sizing: border-box; margin: 0; padding: 0; }

            body {
                font-family: 'Noto Sans TC', sans-serif;
                font-size: 10pt;
                color: #111827;
                background: #FFFFFF;
            }

            .page {
                width: 210mm;
                min-height: 297mm;
                padding: 20mm 20mm 25mm;
            }

            /* 頁首 */
            .header {
                display: grid;
                grid-template-columns: 1fr auto;
                align-items: center;
                padding-bottom: 12px;
                border-bottom: 2px solid #1E3A8A;
                margin-bottom: 24px;
            }

            .header-title { font-size: 20pt; font-weight: 700; color: #1E3A8A; }
            .header-meta  { text-align: right; color: #6B7280; font-size: 9pt; line-height: 1.6; }

            /* KPI 卡片 */
            .kpi-grid {
                display: grid;
                grid-template-columns: repeat(3, 1fr);
                gap: 16px;
                margin-bottom: 28px;
            }

            .kpi-card {
                background: #F0F9FF;
                border: 1px solid #BAE6FD;
                border-radius: 8px;
                padding: 16px;
            }

            .kpi-label { font-size: 8pt; color: #0369A1; text-transform: uppercase; letter-spacing: 0.05em; }
            .kpi-value { font-size: 22pt; font-weight: 700; color: #0C4A6E; margin-top: 4px; }
            .kpi-sub   { font-size: 9pt;  color: #0369A1; margin-top: 2px; }

            /* 圖表區域 */
            .chart-section { margin-bottom: 28px; }
            .section-title {
                font-size: 12pt;
                font-weight: 700;
                color: #1E3A8A;
                margin-bottom: 12px;
                padding-bottom: 6px;
                border-bottom: 1px solid #BFDBFE;
            }

            /* 資料表格 */
            table { width: 100%; border-collapse: collapse; }
            thead th {
                background: #1E3A8A;
                color: #FFFFFF;
                padding: 8px 12px;
                text-align: left;
                font-size: 9pt;
            }
            tbody tr:nth-child(even) { background: #F9FAFB; }
            tbody td { padding: 8px 12px; font-size: 9pt; border-bottom: 1px solid #F3F4F6; }

            @page { margin: 15mm 15mm 20mm; }
        </style>
    </head>
    <body>
        <div class="page">
            <!-- 頁首 -->
            <div class="header">
                <div>
                    <div class="header-title">{$data['title']}</div>
                    <div style="color:#6B7280;margin-top:4px;">{$data['company']}</div>
                </div>
                <div class="header-meta">
                    <div>{$data['period']}</div>
                    <div>生成日期:2025-03-04</div>
                </div>
            </div>

            <!-- KPI 卡片 -->
            <div class="kpi-grid">
                <div class="kpi-card">
                    <div class="kpi-label">季度總營收</div>
                    <div class="kpi-value">{$data['revenue']}</div>
                    <div class="kpi-sub">較上季成長 {$data['growth']}</div>
                </div>
                <div class="kpi-card">
                    <div class="kpi-label">月均營收</div>
                    <div class="kpi-value">NT$ 4.19M</div>
                    <div class="kpi-sub">目標達成率 104.8%</div>
                </div>
                <div class="kpi-card">
                    <div class="kpi-label">新增客戶</div>
                    <div class="kpi-value">48</div>
                    <div class="kpi-sub">企業客戶佔 62.5%</div>
                </div>
            </div>

            <!-- 長條圖 -->
            <div class="chart-section">
                <div class="section-title">月度營收趨勢</div>
                <canvas id="revenueChart" height="120"></canvas>
            </div>

            <!-- 明細表格 -->
            <div class="chart-section">
                <div class="section-title">月度明細</div>
                <table>
                    <thead>
                        <tr>
                            <th>月份</th>
                            <th>營收(NTD)</th>
                            <th>環比成長</th>
                            <th>目標達成</th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr>
                            <td>十月</td>
                            <td>3,820,000</td>
                            <td>+12.3%</td>
                            <td>95.5%</td>
                        </tr>
                        <tr>
                            <td>十一月</td>
                            <td>4,150,000</td>
                            <td>+8.6%</td>
                            <td>103.8%</td>
                        </tr>
                        <tr>
                            <td>十二月</td>
                            <td>4,610,000</td>
                            <td>+11.1%</td>
                            <td>115.3%</td>
                        </tr>
                    </tbody>
                </table>
            </div>
        </div>

        <script>
            const ctx = document.getElementById('revenueChart').getContext('2d');
            new Chart(ctx, {
                type: 'bar',
                data: {
                    labels: {$chartLabels},
                    datasets: [{
                        label: '月度營收(NTD)',
                        data: {$chartData},
                        backgroundColor: ['#3B82F6', '#1D4ED8', '#1E3A8A'],
                        borderRadius: 6,
                    }]
                },
                options: {
                    responsive: true,
                    plugins: {
                        legend: { display: false },
                    },
                    scales: {
                        y: {
                            beginAtZero: false,
                            ticks: {
                                callback: v => 'NT$ ' + (v/1000000).toFixed(1) + 'M'
                            }
                        }
                    }
                }
            });
        </script>
    </body>
    </html>
    HTML;
}

程式碼說明

延伸閱讀