Laravel 导出报价单 PDF:从 Blade 模板到文件下载

AI摘要
【总结】本文属知识分享,介绍Laravel结合ComPDF实现报价单HTML转PDF的接入流程,强调数据与模板分离、中文输出验证、下载响应与权限校验,并给出同步调用示例及生产环境注意事项。

Laravel报价单从Blade模板到PDF下载示意

报价单导出看似只是把一组字段放进 PDF,真正容易卡住的是三件事:模板和数据没有分开、中文输出不符合预期,以及下载响应、访问权限和文件版本没有接好。

本文用一张教学报价单说明接入顺序。

报价单生成与核对流程

ComPDF负责把HTML或模板转换为PDF;报价数据、权限、文件命名和下载响应仍由Laravel应用负责。

先把报价数据和模板分开

不要在控制器里拼一整段HTML。先准备结构化数据,再交给Blade模板渲染:

$quote = [
    'number' => 'Q-2026-271',
    'customer' => '教学供应商丙',
    'currency' => 'CNY',
    'items' => [
        ['name' => '可调支架', 'quantity' => 7, 'unit_price' => 45.00],
    ],
];

return view('quotes.pdf', compact('quote'));

在 resources/views/quotes/pdf.blade.php 中,模板只负责展示已经校验过的数据:

<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <style>body{font-family: sans-serif} table{width:100%;border-collapse:collapse} th,td{border:1px solid #bbb;padding:8px} .total{text-align:right}</style>
</head>
<body>
  <h1>报价单 {{ $quote['number'] }}</h1>
  <p>客户:{{ $quote['customer'] }} 币种:{{ $quote['currency'] }}</p>
  <table>
    <thead><tr><th>项目</th><th>数量</th><th>单价</th><th>小计</th></tr></thead>
    <tbody>
    @foreach ($quote['items'] as $item)
      <tr>
        <td>{{ $item['name'] }}</td>
        <td>{{ $item['quantity'] }}</td>
        <td>{{ number_format($item['unit_price'], 2) }}</td>
        <td>{{ number_format($item['quantity'] * $item['unit_price'], 2) }}</td>
      </tr>
    @endforeach
    </tbody>
  </table>
</body>
</html>

这个模板示例只演示字段和明细循环。生产环境还要按项目需要处理税额、舍入、页眉页脚和多页表格。

金额、数量和币种由应用校验。模板只负责展示,不能把“总价正确”交给视图自行猜测。

用Blade输出HTML,再调用HTML转PDF

Blade文档默认会转义变量,适合输出报价客户名称和明细。循环生成明细行时,仍应在保存报价前完成字段校验。

渲染好的HTML可交给ComPDF HTML转PDF接口。同步请求返回结果地址,文档列出的下载有效期为24小时,因此应及时下载。下面使用单份、小文件的同步调用;批量任务另按异步模式组织。

先在 .env 中配置 COMPDF_API_KEY,再在 config/services.php 的返回数组中加入配置。不要把密钥写进Blade或浏览器代码。

'compdf' => ['key' => env('COMPDF_API_KEY')],

以下片段放在已完成登录和报价查看权限检查的控制器方法里。方法中的 $quote 使用前面已经校验过的数据,HTTP请求使用Laravel提供的 Http 门面:

use Illuminate\Support\Facades\Http;

// 放入控制器方法内部:
$html = view('quotes.pdf', compact('quote'))->render();
$filename = 'Q-2026-271.pdf'; // 教学文件名;实际项目用已校验的编号
$key = config('services.compdf.key');
abort_unless(is_string($key) && $key !== '', 503, '尚未配置PDF服务');

$response = Http::timeout(120)
    ->withHeaders(['x-api-key' => $key])
    ->attach('file', $html, 'quote.html')
    ->post('https://api-server.compdf.com/server/v2/process/html/pdf', [
        'language' => 2,
    ])->throw()->json();

$data = $response['data'] ?? [];
$url = $data['downloadUrl'] ?? null;
abort_unless(
    (string) ($response['code'] ?? '') === '200'
    && ($data['status'] ?? '') === 'success'
    && is_string($url) && $url !== '',
    502, '未取得PDF结果,请检查转换状态'
);

// 本例只下载本次转换响应中的HTTPS地址,不接受浏览器传入任意URL。
abort_unless(parse_url($url, PHP_URL_SCHEME) === 'https', 502, '结果地址无效');
$pdf = Http::timeout(60)->get($url)->throw()->body();
abort_unless(strncmp($pdf, '%PDF-', 5) === 0, 502, '结果不是PDF文件');

return response($pdf, 200, [
    'Content-Type' => 'application/pdf',
    'Content-Disposition' => 'attachment; filename="'.$filename.'"',
]);

调用失败或超时会抛出异常,应由项目统一处理并向用户显示“生成失败,请重试”,不要在异常分支里返回“成功”。%PDF-只检查文件头,不能证明版式和金额正确。大文件应下载到受控临时文件并返回文件响应,避免把整个文件读入内存;这不属于本例的单份小文件路径。

中文输出先在云转换结果里确认

本例调用云API,Laravel服务器安装字体并不意味着云端生成服务能读取它;Java/.NET SDK的本地字体注册方法也不能直接套进这个请求。HTML转PDF接口当前没有列出上传字体的参数,不应在代码中擅自添加。

先把一条中文名称、一条数字编号和金额放入模板,生成后重新打开PDF,检查中文显示、搜索和复制。若需要固定品牌字体、特殊字形或严格排版,先向服务方确认当前云端支持与资源加载方式,再决定是否改用能够自行配置字体资源的生成SDK。不要仅因开发机浏览器能显示,就认定云端输出一定一致。

下载前再做一次业务检查

生成PDF后、返回下载响应前检查:

  • 报价编号与当前记录一致;
  • 客户、币种、数量和金额来自同一版本数据;
  • 生成结果确实是PDF;
  • 当前用户有权下载这份报价;
  • 生成失败时返回可理解的错误,不显示“已完成”。

如果需要保留历史报价,不要用同一个文件名覆盖旧文件;把报价版本和生成参数写入自己的记录。ComPDF处理文档生成,版本、权限、存储和下载链接仍由Laravel应用决定。

适合怎样继续接入

文件量少、模板固定时,可以先用一份教学报价验证HTML布局、中文字体和下载响应;模板多、字段经常变化时,再把模板版本、数据校验和失败重试独立出来。先用一份报价完成“生成→下载→重新打开→核对编号、金额和中文”这四步,再考虑批量生成或存储策略。

先用接口验证一份报价模板能否得到预期PDF;若需要更细的版式和字体控制,再了解PDF生成方式。确认布局和下载都符合要求后,再接入项目自己的报价版本与权限规则。

本作品采用《CC 协议》,转载必须注明作者和本文链接
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!
未填写
文章
40
粉丝
2
喜欢
7
收藏
9
排名:1392
访问:2788
私信
所有博文
社区赞助商