在本文中,我们将学习如何通过接受用户的输入来生成动态 PDF。一些用例包括根据收到的数据生成invoices
、certificates
、resumes
、等。reports
为了启用 PDF 下载,我们将使用react-pdf
提供有用组件的包,如Document
、Page
、View
、Image
、Text
、PDFDownloadLink
等PDFViewer
。
让我们检查一下每个组件:
使用以下 cmd创建pdf-invoice React 应用程序:
npx create-react-app react-pdf-invoice
成功创建应用程序后,使用以下命令转到目录并启动项目 -
cd react-pdf-invoice
npm start
在react应用程序中安装react-pdf的命令:
npm install @react-pdf/renderer --save
yarn add @react-pdf/renderer
由于我们的 PDF 本质上是动态的,因此可以选择添加/删除项目、更改产品的价格/数量、根据提到的项目计算总金额。 因此,我们需要接受用户的输入并相应地显示数据。
src > 组件 > createInvoice > InvoiceForm.js
import React, { useState } from 'react';
import InvoicePDF from '../getPDF/InvoicePDF';
import { PDFDownloadLink } from '@react-pdf/renderer';
import './styles.css';
const InvoiceForm = () => {
// state for storing info about user creating Invoice
const [billFrom, setBillFrom] = useState({
name: '',
address: '',
invoiceNumber: '',
})
// state for capturing info of person who needs to pay
const [client, setClient] = useState({
clientName: '',
clientAddress: '',
})
// items description containing name, price and quantity
const [items, setItems] = useState([{ name: '', quantity: 0, price: 0}]);
const handleBillFromData = (e) => {
e.preventDefault();
const {name, value} = e.target;
setBillFrom({
...billFrom,
[name] : value
})
}
const handleClientData = (e) => {
e.preventDefault();
const {name, value} = e.target;
setClient({
...client,
[name] : value
})
}
const handleItemChange = (e, index, field, value) => {
e.preventDefault();
const updatedItems = [...items];
updatedItems[index][field] = value; // updating the item field (using index) according to user's input
setItems(updatedItems); // updating the items array
};
const handleAddItem = () => {
setItems([...items, { name: '', quantity: 0, price: 0}]); // adding new item to items array
};
const handleRemoveItem = (index) => {
const updatedItems = [...items];
updatedItems.splice(index, 1); // removing the selected item
setItems(updatedItems); // updating the items array
};
// to compute the items' total amount
const total = () => {
return items.map(({price, quantity}) => price * quantity).reduce((acc, currValue) => acc + currValue, 0);
}
return (
<div className="invoice">
<div>
<h1 className='title'>Invoice</h1>
<div className='firstRow'>
<div className='inputName'>
<label>Invoice Number:</label>
<input name="invoiceNumber" className="input" type="text" value={billFrom.invoiceNumber} onChange={handleBillFromData} />
</div>
</div>
<div className='firstRow'>
<div className='inputName'>
<label>Name:</label>
<input name="name" className="input" type="text" value={billFrom.name} onChange={handleBillFromData} />
</div>
<div className='inputName'>
<label>Address:</label>
<textarea name="address" className="textarea" type="text" value={billFrom.address} onChange={handleBillFromData} />
</div>
</div>
<hr/>
<h2>Bill To:</h2>
<div className='firstRow'>
<div className='inputName'>
<label>Client Name:</label>
<input name="clientName" className="input" type="text" value={client.clientName} onChange={handleClientData} />
</div>
<div className='inputName'>
<label>Address:</label>
<textarea name="clientAddress" className="textarea" type="text" value={client.clientAddress} onChange={handleClientData} />
</div>
</div>
<h2 className='title'>Add Details</h2>
<div className='subTitleSection'>
<h2 className='subTitle item'>Item</h2>
<h2 className='subTitle quantity'>Quantity</h2>
<h2 className='subTitle price'>Price</h2>
<h2 className='subTitle action'>Amount</h2>
</div>
{items?.map((item, index) => (
<div key={index} className='firstRow'>
<input className="input item"
type="text"
value={item.name}
onChange={(e) => handleItemChange(e, index, 'name', e.target.value)}
placeholder="Item Name"
/>
<input className="input quantity"
type="number"
value={item.quantity}
onChange={(e) => handleItemChange(e, index, 'quantity', e.target.value)}
placeholder="Quantity"
/>
<input className="input price"
type="number"
value={item.price}
onChange={(e) => handleItemChange(e, index, 'price', e.target.value)}
placeholder="Price"
/>
<p className='amount'>$ {item.quantity * item.price}</p>
<button className='button' onClick={() => handleRemoveItem(index)}>-</button>
</div>
))}
<button className='button' onClick={handleAddItem}>+</button>
<hr/>
<div className='total'>
<p>Total:</p>
<p>{total()}</p>
</div>
<hr/>
<PDFDownloadLink document={<InvoicePDF billFrom={billFrom} client={client} total={total} items={items} />} fileName={"Invoice.pdf"} >
{({ blob, url, loading, error }) =>
loading ? "Loading..." : <button className='button'>Print Invoice</button>
}
</PDFDownloadLink>
</div>
</div>
);
};
export default InvoiceForm;
const handleBillFromData = (e) => {
e.preventDefault();
const {name, value} = e.target;
setBillFrom({
...billFrom,
[name] : value
})
}
在上述代码中,我们存储了创建发票的人员的信息(姓名、发票编号、地址)。
const handleClientData = (e) => {
e.preventDefault();
const {name, value} = e.target;
setClient({
...client,
[name] : value
})
}
在上面的代码块中,该函数将根据用户的输入更新需要支付总金额的客户信息(名称、地址)。
const handleItemChange = (e, index, field, value) => {
e.preventDefault();
const updatedItems = [...items];
updatedItems[index][field] = value; // updating the item field (using index) according to user's input
setItems(updatedItems); // updating the items array
};
const handleAddItem = () => {
setItems([...items, { name: '', quantity: 0, price: 0}]); // adding new item to items array
};
const handleRemoveItem = (index) => {
const updatedItems = [...items];
updatedItems.splice(index, 1); // removing the selected item
setItems(updatedItems); // updating the items array
};
每当用户单击按钮时,handleAddItem()将添加一个具有itemName
, quantity
,作为输入字段的新项目。price+
当用户单击按钮时, handleRemoveItem()将删除所选项目-
。
handleItemChange()将通过获取特定项目的索引和值(由用户输入)来更新所选项目。
<PDFDownloadLink document={<InvoicePDF billFrom={billFrom} client={client} total={total} items={items} />} fileName={"Invoice.pdf"} >
{({ blob, url, loading, error }) =>
loading ? "Loading..." : <button className='button'>Print Invoice</button>
}
</PDFDownloadLink>
上面提到的代码片段负责使用用户输入的所有输入生成 PDF 文档。
src > 组件 > createInvoice > styles.css
.invoice {
display: flex;
padding: 10px;
margin: 20px;
border-radius: 12px;
justify-content: center;
width: 1200px;
box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px;
}
.title {
font-size: 30px;
}
.firstRow {
display: flex;
justify-content: space-between;
flex-grow: 1;
}
.inputName {
display: flex;
flex-direction: column;
}
.total {
display: flex;
justify-content: space-between;
font-size: 20px;
}
.total > p {
align-items: center;
justify-content: center;
font-weight: 700;
}
.inputName > label {
font-size: 20px;
font-weight: 600;
margin-right: 5px;
text-align: left;
}
.input, .textarea{
font-size: 20px;
border-radius: 5px;
padding-left: 10px;
margin: 10px 0px;
}
.subTitleSection {
background: bisque;
display: flex;
justify-content: space-between;
border-radius: 12px;
flex-grow: 1;
padding-right: 15px;
}
.subTitle {
font-size: 20px;
font-weight: 700;
margin: 10px 10px;
text-align: left;
}
.item {
width: 50%;
}
.quantity {
width: 15%;
}
.price {
width: 15%;
}
.action {
width: 10%;
}
.amount {
font-size: 18px;
font-weight: 700;
}
.remove {
border-radius: 12px;
height: 20px;
width: 20px;
}
.button {
background-color: #405cf5;
border-radius: 6px;
border-width: 0;
box-sizing: border-box;
color: #fff;
cursor: pointer;
font-size: 18px;
height: 44px;
padding: 0 25px;
}
一旦我们从用户端获得所需的数据,我们就将数据提供给负责生成 pdf 文档的组件。在我们的例子中,InvoicePDF 就是该组件。
src > 组件 > getPDF > InvoicePDF.js
import React from 'react';
import { Page, Text, View, Document, StyleSheet } from '@react-pdf/renderer';
import ItemsTable from './ItemsTable';
const styles = StyleSheet.create({
page: {
flexDirection: 'column',
padding: 20,
},
name: {
flexDirection: 'column',
justifyContent: 'flex-start',
fontSize: 22,
marginBottom: 5
},
invoiceNumber: {
flexDirection: 'column',
justifyContent: 'flex-start',
},
section: {
margin: 10,
padding: 10,
flexGrow: 1,
},
header: {
fontSize: 24,
marginBottom: 10,
textAlign: 'center'
},
label: {
fontSize: 12,
marginBottom: 5,
},
input: {
marginBottom: 10,
paddingBottom: 5,
},
client: {
borderTopWidth: 1,
marginTop: 20,
marginBottom: 10
},
});
const InvoicePDF = ({ billFrom, client, total, items }) => { // destructuring props
return (
<Document>
<Page size="A4" style={styles.page}>
<View>
<Text style={styles.header}>Invoice Form</Text>
<View>
<Text style={styles.name}>{billFrom.name}</Text>
</View>
<View style={styles.invoiceNumber}>
<Text style={styles.label}>INVOICE NO.</Text>
<Text style={styles.input}>{billFrom.invoiceNumber}</Text>
</View>
<View style={styles.invoiceNumber}>
<Text style={styles.label}>ADDRESS</Text>
<Text style={styles.input}>{billFrom.address}</Text>
</View>
<View style={styles.client}></View>
<Text style={styles.label}>BILL TO</Text>
<View>
<Text style={styles.name}>{client.clientName}</Text>
</View>
<View style={styles.invoiceNumber}>
<Text style={styles.label}>CLIENT ADDRESS</Text>
<Text style={styles.input}>{client.clientAddress}</Text>
</View>
<ItemsTable items={items} total={total} />
</View>
</Page>
</Document>
);
};
export default InvoicePDF;
src > 组件 > getPDF > ItemsTable.js
import React from 'react'
import { Text, View, StyleSheet } from '@react-pdf/renderer';
const styles = StyleSheet.create({
row: {
flexDirection: 'row',
borderBottomWidth: 1,
backgroundColor: '#D3D3D3',
borderTopColor: 'black',
borderTopWidth: 1,
borderBottomColor: 'black',
fontStyle: 'bold',
alignItems: 'center',
height: 22,
},
quantity: {
width: '10%',
borderRightWidth: 1,
textAlign: 'right',
borderRightColor: '#000000',
paddingRight: 10,
},
description: {
width: '60%',
borderRightColor: '#000000',
borderRightWidth: 1,
textAlign: 'left',
paddingLeft: 10,
},
price: {
width: '15%',
borderRightColor: '#000000',
borderRightWidth: 1,
textAlign: 'right',
paddingRight: 10,
},
amount: {
width: '15%',
textAlign: 'right',
paddingRight: 10,
},
total: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
borderBottomColor: 'black',
borderBottomWidth: 1,
}
})
const ItemsTable = ({ items, total }) => { // destructuring props
return (
<View>
<View style={styles.row}>
<Text style={styles.description}>Item Description</Text>
<Text style={styles.quantity}>Qty</Text>
<Text style={styles.price}>Price</Text>
<Text style={styles.amount}>Amount</Text>
</View>
{
items.map((item, index) => (
<View key={index} style={styles.row}>
<Text style={styles.description}>{item.name}</Text>
<Text style={styles.quantity}>{item.quantity}</Text>
<Text style={styles.price}>{item.price}</Text>
<Text style={styles.amount}>$ {item.quantity * item.price}</Text>
</View>
))}
<View style={styles.total}>
<Text>Total: </Text>
<Text>$ {total()} </Text>
</View>
</View>
)
}
export default ItemsTable;
src > App.js
import './App.css';
import InvoiceForm from './components/createInvoice/InvoiceForm';
function App() {
return (
<div className="App">
<InvoiceForm />
</div>
);
}
export default App;
单击“打印发票”后,将下载名为Invoice.pdf的 pdf 文档,其结构如下:
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。