ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

为 Refine + Strapi 发票生成器接入 PDF 渲染器:用 @react-pdf/renderer 在 Modal 中预览发票

为 Refine + Strapi 发票生成器接入 PDF 渲染器:用 @react-pdf/renderer 在 Modal 中预览发票 为 Refine Strapi 发票生成器接入 PDF 渲染器用 react-pdf/renderer 在 Modal 中预览发票【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine本文是 RefineWeekft. Strapi系列教程的第五天收官篇核心任务是为前四天搭建的PDF Invoice Generator发票生成器应用添加 PDF 渲染能力使用react-pdf/renderer将invoices资源中的单条记录渲染成 A4 版式的 PDF 文档并通过 Refine 的useModal()钩子将其嵌入到发票列表页/invoices的弹窗中。读完本文你将掌握以 React 组件方式声明 PDF 版式的完整套路、Refine 中useModal()与meta.populateStrapi 关联数据填充的实战用法并收获一个可直接运行的全栈发票应用示例。系列回顾与本篇目标在系列的前四天里我们已经为发票应用实现了多个资源的 CRUD 操作过程中探索了 Refine 的dataProvider、authProvider、resources、routerProvider以及与之对应的各类 hooks并熟悉了useSimpleList()、useModalForm()、useDrawerForm()等高阶钩子。系列各篇在仓库中均有完整博客记录可对照阅读Day 1Pilot 与 Refine 架构documentation/blog/2023-04-10-refine-invoicer-1.mdDay 2搭建 Invoicer 应用documentation/blog/2023-04-11-refine-invoicer-2.mdDay 3添加 CRUD 操作与视图documentation/blog/2023-04-12-refine-invoicer-3.mdDay 4创建 Mission 与 Invoices 页面documentation/blog/2023-04-13-refine-invoicer-4.md本篇将越过这些既有能力为应用加上 PDF 渲染器在发票列表页/invoices的每一行增加一个 PDF 按钮点击后在弹窗Modal中渲染出该条发票记录的 PDF 文档。安装 react-pdf/renderer我们选用react-pdf/renderer作为 PDF 渲染库。在项目根目录执行npm install react-pdf --save在当前仓库的示例中该依赖的声明可见于 examples/refine-week-invoice-generator/package.jsonreact-pdf/renderer: ^3.1.8。整个示例基于 Vite TypeScript 构建react/react-dom版本为^19.1.0Refine 核心为refinedev/core^5.0.12UI 层为refinedev/antd^6.0.3后端通信由refinedev/strapi-v4^7.0.1提供。构建 React-PDF 版式组件 PdfLayout安装完成后我们创建一个 PDF 版式组件。它接收一条IInvoice记录将其内容渲染进一个 A4 页面。完整实现位于 examples/refine-week-invoice-generator/src/components/pdf/pdfLayout.tsx并经 components/pdf/index.ts 统一导出export * from ./pdfLayout因此外部可以直接从components/pdf导入。import { Document, Image, Page, StyleSheet, View, Text, PDFViewer, } from react-pdf/renderer; import type { IInvoice } from ../../interfaces; import { API_URL } from ../../constants; type PdfProps { record: IInvoice | undefined; }; export const PdfLayout: React.FCPdfProps ({ record }) { const subtotal record?.missions.reduce((prev, cur) { return prev cur?.day * cur?.daily_rate; }, 0) ?? 0; return ( PDFViewer style{styles.viewer} Document Page style{styles.page} sizeA4 View Image src{API_URL record?.company?.logo?.url} style{{ width: 120px, height: auto }} / View style{styles.invoiceTextNumberContainer} Text style{styles.invoiceText} {Invoice: Invoice_#${record?.id}${record?.name}}/Text Text style{styles.invoiceId} {Invoice ID: INVOICE_#${record?.id}}/Text /View /View View style{styles.dividerLG} / View style{styles.invoiceForFromContainer} View style{styles.invoiceFor} Text style{styles.invoiceForFromTitle}invoice For:/Text View Text style{styles.invoiceForFromText} {record?.contact?.client?.name} /Text Text style{styles.invoiceForFromText} {record?.contact?.first_name} /Text Text style{styles.invoiceForFromText} {record?.contact?.last_name} /Text Text style{styles.invoiceForFromText} {record?.contact?.email} /Text /View /View View style{styles.invoiceFrom} Text style{styles.invoiceForFromTitle}From:/Text View Text style{styles.invoiceForFromText} {record?.company.name} /Text Text style{styles.invoiceForFromText} {record?.company.city} /Text Text style{styles.invoiceForFromText} {record?.company.address}, {record?.company.country} /Text /View View style{styles.dividerSM} / View Text style{styles.invoiceForFromText} {Invoice ID: ${record?.id}}/Text Text style{styles.invoiceForFromText} {Invoice Custom ID: ${record?.custom_id}}/Text Text style{styles.invoiceForFromText} {Invoice Date: ${record?.date}}/Text /View /View /View View style{styles.table} View style{styles.tableHeader} Text style{[styles.tableHeaderItem, { width: 40% }]} Mission /Text Text style{[styles.tableHeaderItem, { width: 20% }]} Day /Text Text style{[styles.tableHeaderItem, { width: 20% }]} Day Rate /Text Text style{[styles.tableHeaderItem, { width: 20% }]} Total /Text /View {record?.missions.map((item) { return ( View key{item.id} style{styles.tableRow} Text style{[styles.tableCol, { width: 40% }]} {item.mission} /Text Text style{[styles.tableCol, { width: 20% }]} {item?.day} /Text Text style{[styles.tableCol, { width: 20% }]} {item?.daily_rate} /Text Text style{[styles.tableCol, { width: 20% }]} {item?.daily_rate * item?.day} /Text /View ); })} /View View style{styles.signatureTotalContainer} View style{styles.signatureContainer} Text style{styles.signatureText} Signature: ________________ /Text Text style{styles.signatureText} Date: {record?.date.toString()} /Text /View View style{styles.totalContainer} Text style{styles.totalText}SUBTOTAL: {subtotal}/Text Text style{styles.totalText} Discount(%): {record?.discount} /Text Text style{styles.totalText}Tax(%): {record?.tax}/Text Text style{styles.totalText} Total($): {subtotal (subtotal * (record?.tax as number)) / 100 - (subtotal * (record?.discount as number)) / 100} /Text /View /View View style{styles.footer} Text style{styles.footerText}{record?.company.city}/Text Text style{styles.footerText} {record?.company.address}, {record?.company.country} /Text /View /Page /Document /PDFViewer ); }; const styles StyleSheet.create({ viewer: { paddingTop: 32, width: 100%, height: 80vh, border: none, }, page: { display: flex, padding: 0.4in 0.4in, fontSize: 12, color: #333, backgroundColor: #fff, }, invoiceTextNumberContainer: { display: flex, flexDirection: row, alignItems: center, justifyContent: space-between, }, invoiceText: { color: #3aabf0, }, invoiceId: { textAlign: center, }, invoiceForFromContainer: { display: flex, flexDirection: row, justifyContent: space-between, }, invoiceForFromTitle: { marginBottom: 24, }, invoiceFor: { flex: 1.5, }, invoiceFrom: { flex: 1, }, invoiceForFromText: { color: #787878, lineHeight: 1.5, }, dividerSM: { width: 100%, height: 1, marginTop: 12, marginBottom: 12, backgroundColor: #e5e5e5, }, dividerLG: { width: 100%, height: 1, marginTop: 40, marginBottom: 40, backgroundColor: #e5e5e5, }, table: { marginTop: 32, }, tableHeader: { display: flex, flexDirection: row, textAlign: center, }, tableHeaderItem: { paddingVertical: 8, border: 1px solid #000, borderBottom: none, }, tableRow: { display: flex, flexDirection: row, }, tableCol: { paddingVertical: 8, paddingHorizontal: 4, border: 1px solid #000, }, signatureTotalContainer: { display: flex, flexDirection: row, justifyContent: space-between, marginTop: 32, }, signatureContainer: {}, totalContainer: {}, signatureText: { marginTop: 32, }, totalText: { marginTop: 16, }, footer: { borderTop: 1px solid #e5e5e5, paddingTop: 8, marginTop: auto, }, footerText: { color: #787878, lineHeight: 1.5, }, });版式结构解读整个组件全部由react-pdf/renderer提供的声明式组件构成无需引入任何传统 PDF 生成库PDFViewer浏览器内嵌的 PDF 预览容器样式里设为width: 100%、height: 80vh保证在弹窗中占据足够空间Document/PagePDF 文档与页面骨架Page通过sizeA4指定纸张规格padding: 0.4in 0.4in、fontSize: 12控制版心与正文字号View相当于 CSS 的div配合flexDirection、justifyContent、flex等属性完成页面的区块布局抬头区、收/发件人区、明细表、签名与小计区、页脚Text文本节点发票编号、自定义编号、日期、金额等字段均通过模板字符串拼入Image加载公司 Logosrc由API_URL record?.company?.logo?.url拼接而成其中API_URL定义在 examples/refine-week-invoice-generator/src/constants.ts当前示例指向https://api.strapi-invoice.refine.dev金额计算小计subtotal通过missions.reduce累加每条任务的day * daily_rate最终Total($)按小计 小计 × 税率% − 小计 × 折扣%计算样式与 React Native 类似的StyleSheet.create定义支持flex、border、marginTop: auto页脚吸底等布局能力。这些组件的具体行为属于 React-PDF 库本身的范畴本文不再逐层展开如果希望深入探索各组件的能力可直接查阅react-pdf/renderer的官方组件文档。在 Refine Modal 中展示 PDF如 Day 4 所实现invoices的list页面渲染InvoiceList /组件发票数据以表格行形式展示。现在我们要在每一行增加一个按钮点击后打开弹窗弹窗内显示该条发票记录生成的 PDF 文档。更新后的InvoiceList /完整代码如下对应仓库 examples/refine-week-invoice-generator/src/pages/invoices/list.tsximport { useState } from react; import { useModal } from refinedev/core; import { List, useTable, DateField, TagField, EmailField, DeleteButton, EditButton, } from refinedev/antd; // It is recommended to use explicit import as seen below to reduce bundle size. // import { IconName } from ant-design/icons; import * as Icons from ant-design/icons; import { Table, Space, Button, Modal } from antd; import type { IInvoice, IMission } from ../../interfaces; import { PdfLayout } from ../../components/pdf; const { FilePdfOutlined } Icons; export const InvoiceList: React.FC () { const [record, setRecord] useStateIInvoice(); const { tableProps } useTableIInvoice({ sorters: { initial: [{ field: id, order: desc }] }, meta: { populate: { contact: { populate: [client] }, company: { populate: [logo] }, missions: *, }, }, }); const { show, visible, close } useModal(); return ( List Table {...tableProps} rowKeyid Table.Column dataIndexid titleID / Table.ColumnIInvoice dataIndexname titleInvoice Name render{(_, record) { return Invoice_#${record.id}${record?.name}; }} / Table.ColumnIInvoice dataIndexdate titleInvoice Date render{(value) DateField formatLL value{value} /} / Table.Column dataIndex{[company, name]} titleCompany / Table.Column dataIndex{missions} titleMissions render{(value) { return value.map((item: IMission) { return ( TagField key{item?.id} colorblue value{item?.mission} / ); }); }} / Table.Column dataIndexdiscount titleDiscount(%) render{(value) TagField colorblue value{value} /} / Table.Column dataIndextax titleTax(%) render{(value) TagField colorcyan value{value} /} / Table.Column dataIndexcustom_id titleCustom Invoice ID / Table.Column dataIndex{[contact, email]} titleContact render{(value) EmailField value{value} /} / Table.ColumnIInvoice titleActions dataIndexactions render{(_, record) { return ( Space EditButton hideText sizesmall recordItemId{record?.id} / DeleteButton hideText sizesmall recordItemId{record?.id} / {record.company ( Button sizesmall icon{FilePdfOutlined /} onClick{() { setRecord(record); show(); }} / )} /Space ); }} / /Table /List Modal visible{visible} onCancel{close} width80% footer{null} PdfLayout record{record} / /Modal / ); };关键改动点拆解1. 弹窗状态管理useModal()useModal()来自refinedev/core在 Refine 核心包中与弹窗/抽屉类 UI 状态管理配套提供解构出show、visible、close三个能力点击 PDF 按钮时先setRecord(record)暂存当前行数据再调用show()打开弹窗弹窗关闭时通过onCancel{close}回调。Modal本身使用 Ant Design 组件width80%给 PDF 预览留出充足宽度footer{null}去掉默认底部按钮区。2. 行内操作按钮FilePdfOutlined /在Actions列中除原有的EditButton、DeleteButton之外新增一个带FilePdfOutlined图标的按钮。注意这里有一个防御性判断{record.company ...}仅当该发票关联了公司信息也就意味着有可用的公司 Logo 与地址时才渲染 PDF 按钮避免渲染出残缺版式。代码采用import * as Icons from ant-design/icons的按需解构方式注释中也提示了更推荐显式导入以减小打包体积。3. 关联数据填充meta.populateuseTable()的入参中meta.populate是本次接入能否成功的关键。它负责告诉 Strapi 后端在返回invoices记录时一并展开populate其所有关联集合contact: { populate: [client] }—— 填充联系人并继续填充联系人的client两级嵌套company: { populate: [logo] }—— 填充公司及其logoPDF 页眉的 Logo 图片正来源于此missions: *—— 填充全部任务明细PDF 表格中的 Mission/Day/Day Rate/Total 数据。嵌套关系可以层层深入例如contact内再嵌套其client。这种深填充正是发票这类聚合型实体在列表页直接生成 PDF 的数据前提没有meta.populaterecord.company.logo、record.contact.client.name、record.missions等字段将全部为空。数据模型对照IInvoice 接口PdfLayout与InvoiceList都依赖 TypeScript 接口类型约束数据形状定义于 examples/refine-week-invoice-generator/src/interfaces/index.d.tsexport interface ICompany { id: string; name: string; address: string; country: string; city: string; email: string; website: string; logo?: null | { url: string }; } export interface IClient { id: string; name: string; contacts: IContact[]; } export interface IContact { id: string; first_name: string; last_name: string; client: IClient; email: string; } export interface IMission { id: string; mission: string; mission_description: string; day: number; daily_rate: number; } export interface IInvoice { id: string; name: string; date: Date; company: ICompany; discount: number; tax: number; custom_id: string; comments: string; contact: IContact; missions: IMission[]; status: Status; }对照可见PDF 版式中使用的每个字段company.logo.url、contact.client.name、missions[].day / daily_rate、discount、tax等都能在此接口中找到对应定义。missions是数组类型因此小计计算可以直接reduce累加。运行与验证从仓库本地体验该示例应用有两种方式直接运行示例项目cd examples/refine-week-invoice-generator npm install npm run dev应用的脚本定义在 package.jsondev对应refine devbuild对应tsc refine build要求 Node20。启动后访问/invoices页面登录表单默认预填demorefine.dev/demodemo见 App.tsx。通过脚手架拉取示例npm create refine-applatest -- --example refine-week-invoice-generator进入/invoices后在任意一行Actions列点击 PDF 图标弹窗即会打开内嵌的 PDF 预览器展示基于该条记录实时生成的发票文档——包括公司 Logo、收/发件人信息、任务明细表格、小计/折扣/税额/总计以及页脚地址。列表页本身依然保有 Refine Ant Design 的表格能力ID、发票名称、日期DateField、公司、任务标签TagField、折扣/税率、联系人邮箱EmailField以及编辑、删除操作。系列总结五天构建一个发票生成器至此五天的 RefineWeek 系列收官。我们构建的 PDF 发票生成器允许用户创建公司、添加客户与联系人、创建带日薪与工期天数的任务并基于这些任务开具发票发票可以按需渲染成 PDF 预览。整个应用串联了 Refine 的多项核心能力Provider 与 Hooks 的协作应用通过Refine组件的dataProvider、authProvider、resources、routerProvider四个关键属性完成能力装配见 examples/refine-week-invoice-generator/src/App.tsx。其中dataProvider{DataProvider(${API_URL}/api, axiosInstance)}来自refinedev/strapi-v4支持包该包针对 Strapi 后端提供了dataProvider与authProvider的通信支持authProvider的实现在 src/authProvider.ts。这体现了 Refine 的模块化设计核心模块负责数据获取、认证等通用逻辑而支持包如refinedev/strapi-v4、refinedev/antd负责与具体后端或 UI 体系对接。Refine v4 起的新式资源与路由定义在资源定义中每个 resource 项只声明页面路径如list: /invoices、create: /invoices/create、edit: /invoices/:id/edit而不是直接指定要渲染的组件组件到路由的映射在Routes路由定义中完成App.tsx。这种资源声明路径 路由决定组件的模式让我们对路由系统拥有更强的控制力与灵活性。Ant Design 的一等公民支持Refine 对 Ant Design 组件开箱即用同时通过refinedev/antd提供自己的封装库。本系列用到的useSimpleList()、useModalForm()、useDrawerForm()等钩子替我们承担了大量数据获取与状态管理的重活本篇的useModal()meta.populate则是又一处少写代码、多做实事的体现——仅需几行配置即可让后端返回深嵌套的聚合数据并驱动 PDF 渲染。后续可扩展的方向五天内容之外Refine 还有广阔的能力空间可以通过accessControlProvider实现基于角色的访问控制通过auditLogProvider接入审计日志也可以自定义布局、顶栏Header、认证页面等。示例中其实已经预置了部分此类设施如Authenticated路由守卫、UnsavedChangesNotifier、DocumentTitleHandler、RefineKbar命令面板可作为进一步探索的起点。结语本篇为发票生成器补齐了最后一公里用react-pdf/renderer以声明式组件构建 PDF 版式再用 Refine 的useModal()将其嵌入 Ant Design 弹窗配合 Strapi 的meta.populate深填充保证数据完整最终在列表页一键预览任意发票。这一组合Refine 数据层 Ant Design UI React-PDF 渲染思路同样适用于合同、报价单、报表等各类文档生成场景是数据密集型业务应用的常用样板。【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表