前端根据用户输入生成PDF发票并支持下载的详细实现
在现代Web应用中,自动生成并下载PDF发票是提升用户体验的关键功能之一。本文将详细介绍如何使用前端技术,根据用户输入的信息生成对应的PDF发票,并提供下载功能。我们将使用HTML、CSS和JavaScript,结合jsPDF库来实现这一功能。
项目结构
我们的项目将包含以下文件:
pdf-invoice-generator/
├── index.html
├── styles.css
└── app.js
index.html:主HTML文件,包含用户输入表单和布局。styles.css:样式表,用于美化界面。app.js:JavaScript文件,处理表单数据、生成PDF和下载功能。
环境搭建
-
创建项目文件夹:
在你的开发环境中,创建一个名为
pdf-invoice-generator的文件夹。 -
初始化文件:
在该文件夹内创建
index.html、styles.css和app.js三个文件。 -
引入jsPDF库:
我们将使用 jsPDF 库来生成PDF文件。可以通过CDN引入,也可以通过npm安装。本文将使用CDN方式。
前端界面设计
HTML
我们将创建一个简单的用户输入表单,用户可以输入发票相关信息,如公司信息、客户信息、项目明细等。
CSS
使用CSS进行基本的样式美化,确保界面友好、清晰。
JavaScript逻辑实现
引入jsPDF库
在index.html中引入jsPDF库,以便在app.js中使用。
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
表单数据收集与验证
在用户填写表单后,我们需要收集这些数据,并进行必要的验证,确保生成的PDF内容完整、准确。
PDF发票生成
使用jsPDF库,根据收集到的表单数据,构建发票的布局和内容。
下载功能实现
提供一个按钮,当用户点击时,将生成的PDF文件自动下载到用户的设备中。
完整代码讲解
以下是项目中各个文件的详细代码及其讲解。
文件路径:index.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>PDF发票生成器</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="styles.css">
<!-- jsPDF库 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
</head>
<body>
<div class="container">
<h1>PDF发票生成器</h1>
<form id="invoice-form">
<h2>公司信息</h2>
<div class="form-group">
<label for="company-name">公司名称:</label>
<input type="text" id="company-name" name="companyName" required>
</div>
<div class="form-group">
<label for="company-address">公司地址:</label>
<input type="text" id="company-address" name="companyAddress" required>
</div>
<div class="form-group">
<label for="company-phone">公司电话:</label>
<input type="text" id="company-phone" name="companyPhone" required>
</div>
<h2>客户信息</h2>
<div class="form-group">
<label for="client-name">客户名称:</label>
<input type="text" id="client-name" name="clientName" required>
</div>
<div class="form-group">
<label for="client-address">客户地址:</label>
<input type="text" id="client-address" name="clientAddress" required>
</div>
<div class="form-group">
<label for="client-phone">客户电话:</label>
<input type="text" id="client-phone" name="clientPhone" required>
</div>
<h2>发票详情</h2>
<div class="form-group">
<label for="invoice-number">发票号码:</label>
<input type="text" id="invoice-number" name="invoiceNumber" required>
</div>
<div class="form-group">
<label for="invoice-date">发票日期:</label>
<input type="date" id="invoice-date" name="invoiceDate" required>
</div>
<h2>项目明细</h2>
<table id="items-table">
<thead>
<tr>
<th>项目描述</th>
<th>数量</th>
<th>单价(¥)</th>
<th>总价(¥)</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr>
<td><input type="text" name="itemDescription[]" required></td>
<td><input type="number" name="itemQuantity[]" min="1" value="1" required></td>
<td><input type="number" name="itemPrice[]" min="0" step="0.01" value="0.00" required></td>
<td class="item-total">0.00</td>
<td><button type="button" class="remove-item-btn">删除</button></td>
</tr>
</tbody>
</table>
<button type="button" id="add-item-btn">添加项目</button>
<h2>总计</h2>
<div class="form-group">
<label for="subtotal">小计(¥):</label>
<input type="text" id="subtotal" name="subtotal" readonly value="0.00">
</div>
<div class="form-group">
<label for="tax">税额(¥):</label>
<input type="text" id="tax" name="tax" readonly value="0.00">
</div>
<div class="form-group">
<label for="total">总计(¥):</label>
<input type="text" id="total" name="total" readonly value="0.00">
</div>
<button type="submit" id="generate-pdf-btn">生成PDF发票</button>
</form>
</div>
<script src="app.js"></script>
</body>
</html>
代码解析:
-
头部信息:
- 设置页面字符集为UTF-8,确保中文显示正常。
- 引入
styles.css进行样式美化。 - 通过CDN引入
jsPDF库,方便后续生成PDF。
-
主体部分:
- 使用一个包含多个部分的表单,用户可以输入公司信息、客户信息、发票详情和项目明细。
- 项目明细部分使用一个可动态增删行的表格,用户可以根据需要添加或删除项目。
- 总计部分显示小计、税额和总计,这些字段将根据项目明细动态计算。
-
按钮:
- "添加项目"按钮用于在项目明细表格中添加新的行。
- "生成PDF发票"按钮用于触发表单提交,生成PDF文件。
文件路径:styles.css
/* styles.css */
/* 通用样式 */
body {
font-family: Arial, sans-serif;
background-color: #f5f5f5;
margin: 0;
padding: 0;
}
.container {
width: 90%;
max-width: 800px;
margin: 50px auto;
background-color: #ffffff;
padding: 30px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
h1, h2 {
text-align: center;
color: #333333;
}
form {
margin-top: 20px;
}
.form-group {
display: flex;
justify-content: space-between;
margin-bottom: 15px;
}
.form-group label {
flex: 1;
margin-right: 10px;
align-self: center;
}
.form-group input {
flex: 2;
padding: 8px;
border: 1px solid #cccccc;
border-radius: 4px;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 15px;
}
table th, table td {
border: 1px solid #dddddd;
padding: 8px;
text-align: center;
}
#add-item-btn, #generate-pdf-btn {
display: block;
width: 100%;
padding: 10px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
#add-item-btn:hover, #generate-pdf-btn:hover {
background-color: #45a049;
}
.remove-item-btn {
padding: 5px 10px;
background-color: #f44336;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.remove-item-btn:hover {
background-color: #da190b;
}
input[readonly] {
background-color: #e9e9e9;
}
代码解析:
-
页面布局:
- 使用
.container类来定义内容的最大宽度和居中显示。 - 设置
body的背景色为浅灰色,表单背景为白色,增加对比度。
- 使用
-
标题样式:
h1和h2居中显示,使用深灰色字体增加可读性。
-
表单样式:
.form-group通过Flex布局实现标签和输入框的对齐。- 输入框设置边框、内边距和圆角,提高用户体验。
-
表格样式:
- 使用边框和Padding,使表格内容清晰可见。
- 表头与表格内容居中对齐。
-
按钮样式:
- 主要按钮(添加项目和生成PDF)设置为绿色背景,悬停时颜色加深。
- 删除按钮设置为红色背景,悬停时颜色加深,增强操作的直观性。
文件路径:app.js
// app.js
// 确保jsPDF库已加载
if (typeof window.jspdf === 'undefined') {
alert('jsPDF库未加载,请检查CDN链接。');
} else {
const { jsPDF } = window.jspdf;
}
// 获取表单元素
const invoiceForm = document.getElementById('invoice-form');
const addItemBtn = document.getElementById('add-item-btn');
const itemsTableBody = document.querySelector('#items-table tbody');
// 函数:添加新的项目行
function addItemRow() {
const newRow = document.createElement('tr');
newRow.innerHTML = `
<td><input type="text" name="itemDescription[]" required></td>
<td><input type="number" name="itemQuantity[]" min="1" value="1" required></td>
<td><input type="number" name="itemPrice[]" min="0" step="0.01" value="0.00" required></td>
<td class="item-total">0.00</td>
<td><button type="button" class="remove-item-btn">删除</button></td>
`;
// 添加事件监听器删除按钮
newRow.querySelector('.remove-item-btn').addEventListener('click', () => {
newRow.remove();
calculateTotals();
});
// 添加事件监听器计算总价
const descriptionInput = newRow.querySelector('input[name="itemDescription[]"]');
const quantityInput = newRow.querySelector('input[name="itemQuantity[]"]');
const priceInput = newRow.querySelector('input[name="itemPrice[]"]');
quantityInput.addEventListener('input', calculateTotalForRow);
priceInput.addEventListener('input', calculateTotalForRow);
itemsTableBody.appendChild(newRow);
}
// 函数:移除项目行
function removeItemRow(event) {
const button = event.target;
const row = button.closest('tr');
row.remove();
calculateTotals();
}
// 函数:计算单行总价
function calculateTotalForRow(event) {
const row = event.target.closest('tr');
const quantity = parseFloat(row.querySelector('input[name="itemQuantity[]"]').value) || 0;
const price = parseFloat(row.querySelector('input[name="itemPrice[]"]').value) || 0;
const total = quantity * price;
row.querySelector('.item-total').innerText = total.toFixed(2);
calculateTotals();
}
// 函数:计算所有项目的总计、税额和总金额
function calculateTotals() {
const itemTotals = document.querySelectorAll('.item-total');
let subtotal = 0;
itemTotals.forEach(totalTd => {
const total = parseFloat(totalTd.innerText) || 0;
subtotal += total;
});
const tax = subtotal * 0.1; // 假设税率为10%
const total = subtotal + tax;
document.getElementById('subtotal').value = subtotal.toFixed(2);
document.getElementById('tax').value = tax.toFixed(2);
document.getElementById('total').value = total.toFixed(2);
}
// 添加初始事件监听器
addItemBtn.addEventListener('click', addItemRow);
// 初始化删除和计算事件监听器
document.querySelectorAll('.remove-item-btn').forEach(btn => {
btn.addEventListener('click', removeItemRow);
});
document.querySelectorAll('input[name="itemQuantity[]"], input[name="itemPrice[]"]').forEach(input => {
input.addEventListener('input', calculateTotalForRow);
});
// 表单提交事件监听器:生成PDF
invoiceForm.addEventListener('submit', (event) => {
event.preventDefault();
generatePDF();
});
// 函数:生成PDF发票
function generatePDF() {
const doc = new jsPDF();
// 获取表单数据
const companyName = document.getElementById('company-name').value;
const companyAddress = document.getElementById('company-address').value;
const companyPhone = document.getElementById('company-phone').value;
const clientName = document.getElementById('client-name').value;
const clientAddress = document.getElementById('client-address').value;
const clientPhone = document.getElementById('client-phone').value;
const invoiceNumber = document.getElementById('invoice-number').value;
const invoiceDate = document.getElementById('invoice-date').value;
const itemDescriptions = document.getElementsByName('itemDescription[]');
const itemQuantities = document.getElementsByName('itemQuantity[]');
const itemPrices = document.getElementsByName('itemPrice[]');
const subtotal = document.getElementById('subtotal').value;
const tax = document.getElementById('tax').value;
const total = document.getElementById('total').value;
// 设置标题
doc.setFontSize(20);
doc.text('发票', 105, 20, null, null, 'center');
// 公司信息
doc.setFontSize(12);
doc.text(`公司名称: ${companyName}`, 10, 30);
doc.text(`地址: ${companyAddress}`, 10, 36);
doc.text(`电话: ${companyPhone}`, 10, 42);
// 客户信息
doc.text(`客户名称: ${clientName}`, 110, 30);
doc.text(`地址: ${clientAddress}`, 110, 36);
doc.text(`电话: ${clientPhone}`, 110, 42);
// 发票详情
doc.text(`发票号码: ${invoiceNumber}`, 10, 50);
doc.text(`发票日期: ${invoiceDate}`, 110, 50);
// 项目明细表格
const startY = 60;
doc.autoTable({
startY: startY,
head: [['项目描述', '数量', '单价(¥)', '总价(¥)']],
body: Array.from(itemDescriptions).map((desc, index) => [
desc.value,
itemQuantities[index].value,
parseFloat(itemPrices[index].value).toFixed(2),
parseFloat(itemQuantities[index].value * itemPrices[index].value).toFixed(2)
]),
theme: 'grid',
styles: { halign: 'center' },
headStyles: { fillColor: [41, 128, 185] },
});
// 总计
const finalY = doc.previousAutoTable.finalY + 10;
doc.text(`小计(¥): ${subtotal}`, 120, finalY);
doc.text(`税额(¥): ${tax}`, 120, finalY + 6);
doc.text(`总计(¥): ${total}`, 120, finalY + 12);
// 保存PDF
doc.save(`${invoiceNumber}.pdf`);
}
// 引入jsPDF-autotable插件
// 需要确保在生成表格前引入该插件
const script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.5.25/jspdf.plugin.autotable.min.js';
document.head.appendChild(script);
代码解析:
-
引入jsPDF库:
- 首先检查
jsPDF是否正确加载,如果未加载,则显示警告。
- 首先检查
-
获取表单元素:
- 使用
document.getElementById和document.querySelector获取表单、添加按钮和项目明细表格的引用。
- 使用
-
添加项目行:
addItemRow函数用于在项目明细表格中添加新的行。- 每行包含项目描述、数量、单价、总价和删除按钮。
- 为新添加的输入框绑定事件监听器,确保在输入数量或单价时自动计算总价。
- 为删除按钮绑定事件监听器,允许用户删除不需要的项目。
-
删除项目行:
removeItemRow函数用于删除指定的项目行,并重新计算总计。
-
计算单行总价:
calculateTotalForRow函数根据用户输入的数量和单价计算该行的总价,并显示在相应的单元格中。- 同时调用
calculateTotals函数,更新小计、税额和总计。
-
计算总计:
calculateTotals函数遍历所有项目的总价,计算出小计。- 假设税率为10%,计算税额和总计。
- 更新小计、税额和总计的输入框值。
-
生成PDF发票:
- 表单提交时触发
generatePDF函数,阻止默认提交行为。 - 收集所有表单数据,包括公司信息、客户信息、发票详情和项目明细。
- 使用
jsPDF创建一个新的PDF文档。 - 设置标题、公司信息、客户信息和发票详情。
- 使用
jsPDF-autotable插件生成项目明细表格。 - 添加总计信息。
- 调用
doc.save方法,将生成的PDF文件下载到用户设备中,文件名为发票号码。
- 表单提交时触发
-
引入jsPDF-autotable插件:
jsPDF本身不支持自动生成表格,因此需要引入jsPDF-autotable插件,增强其表格生成能力。
运行效果
完成上述文件的编写后,打开index.html,你将看到一个包含公司信息、客户信息、发票详情和项目明细的表单。用户可以:
-
填写发票信息:
- 输入公司和客户的相关信息。
- 填写发票号码和日期。
- 在项目明细部分添加或删除项目,输入每个项目的描述、数量和单价。
-
实时计算总计:
- 当用户输入或修改项目的数量和单价时,总价、小计、税额和总计会自动更新。
-
生成并下载PDF发票:
- 点击“生成PDF发票”按钮后,浏览器将自动生成一个PDF文件,内容包括用户输入的所有信息,并启动下载。
总结与最佳实践
总结
本文详细介绍了如何在前端使用HTML、CSS和JavaScript,结合jsPDF库,实现根据用户输入生成PDF发票并支持下载的功能。主要步骤包括:
- 设计用户友好的输入表单:确保用户可以轻松输入发票所需的所有信息。
- 动态计算总计:根据用户输入的项目明细,实时计算小计、税额和总计。
- 生成PDF发票:使用jsPDF库,根据收集到的数据生成格式化的PDF发票。
- 提供下载功能:允许用户一键下载生成的PDF发票。
最佳实践
-
数据验证:
- 确保用户输入的数据是有效的,如数量和单价为正数,必要字段不为空等。
- 可以使用前端验证库如
validate.js或原生HTML5验证。
-
提高用户体验:
- 提供实时预览功能,让用户在生成PDF之前可以查看发票的样式和内容。
- 使用加载动画提示用户生成PDF的过程,尤其是在处理大量数据时。
-
安全性考虑:
- 尽管PDF生成在前端进行,但确保敏感信息的输入和处理符合安全标准。
- 不要在前端存储或传输敏感数据,必要时结合后端进行处理。
-
响应式设计:
- 确保发票在不同设备和屏幕尺寸下都能正确显示。
- 使用CSS媒体查询和灵活的布局技术,如Flexbox或Grid。
-
优化PDF样式:
- 根据需求设计专业的发票模板,使用统一的字体、颜色和布局。
- 可以结合CSS和HTML的灵活布局,提升PDF的美观性和可读性。
-
兼容性测试:
- 测试生成的PDF在不同浏览器和设备上的显示效果,确保一致性。
- 确保jsPDF库和插件在目标环境中的兼容性。
-
扩展功能:
- 添加签名、二维码等高级功能,提升发票的专业性和功能性。
- 提供多种导出格式,如DOCX、XLSX等,满足不同用户需求。