前端PDF发票

1,081 阅读10分钟

前端根据用户输入生成PDF发票并支持下载的详细实现

在现代Web应用中,自动生成并下载PDF发票是提升用户体验的关键功能之一。本文将详细介绍如何使用前端技术,根据用户输入的信息生成对应的PDF发票,并提供下载功能。我们将使用HTMLCSSJavaScript,结合jsPDF库来实现这一功能。

项目结构

我们的项目将包含以下文件:

pdf-invoice-generator/
├── index.html
├── styles.css
└── app.js
  • index.html:主HTML文件,包含用户输入表单和布局。
  • styles.css:样式表,用于美化界面。
  • app.js:JavaScript文件,处理表单数据、生成PDF和下载功能。

环境搭建

  1. 创建项目文件夹

    在你的开发环境中,创建一个名为 pdf-invoice-generator 的文件夹。

  2. 初始化文件

    在该文件夹内创建 index.htmlstyles.cssapp.js 三个文件。

  3. 引入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的背景色为浅灰色,表单背景为白色,增加对比度。
  • 标题样式

    • h1h2居中显示,使用深灰色字体增加可读性。
  • 表单样式

    • .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);

代码解析

  1. 引入jsPDF库

    • 首先检查jsPDF是否正确加载,如果未加载,则显示警告。
  2. 获取表单元素

    • 使用document.getElementByIddocument.querySelector获取表单、添加按钮和项目明细表格的引用。
  3. 添加项目行

    • addItemRow函数用于在项目明细表格中添加新的行。
    • 每行包含项目描述、数量、单价、总价和删除按钮。
    • 为新添加的输入框绑定事件监听器,确保在输入数量或单价时自动计算总价。
    • 为删除按钮绑定事件监听器,允许用户删除不需要的项目。
  4. 删除项目行

    • removeItemRow函数用于删除指定的项目行,并重新计算总计。
  5. 计算单行总价

    • calculateTotalForRow函数根据用户输入的数量和单价计算该行的总价,并显示在相应的单元格中。
    • 同时调用calculateTotals函数,更新小计、税额和总计。
  6. 计算总计

    • calculateTotals函数遍历所有项目的总价,计算出小计。
    • 假设税率为10%,计算税额和总计。
    • 更新小计、税额和总计的输入框值。
  7. 生成PDF发票

    • 表单提交时触发generatePDF函数,阻止默认提交行为。
    • 收集所有表单数据,包括公司信息、客户信息、发票详情和项目明细。
    • 使用jsPDF创建一个新的PDF文档。
    • 设置标题、公司信息、客户信息和发票详情。
    • 使用jsPDF-autotable插件生成项目明细表格。
    • 添加总计信息。
    • 调用doc.save方法,将生成的PDF文件下载到用户设备中,文件名为发票号码。
  8. 引入jsPDF-autotable插件

    • jsPDF本身不支持自动生成表格,因此需要引入jsPDF-autotable插件,增强其表格生成能力。

运行效果

完成上述文件的编写后,打开index.html,你将看到一个包含公司信息、客户信息、发票详情和项目明细的表单。用户可以:

  1. 填写发票信息

    • 输入公司和客户的相关信息。
    • 填写发票号码和日期。
    • 在项目明细部分添加或删除项目,输入每个项目的描述、数量和单价。
  2. 实时计算总计

    • 当用户输入或修改项目的数量和单价时,总价、小计、税额和总计会自动更新。
  3. 生成并下载PDF发票

    • 点击“生成PDF发票”按钮后,浏览器将自动生成一个PDF文件,内容包括用户输入的所有信息,并启动下载。

总结与最佳实践

总结

本文详细介绍了如何在前端使用HTML、CSS和JavaScript,结合jsPDF库,实现根据用户输入生成PDF发票并支持下载的功能。主要步骤包括:

  1. 设计用户友好的输入表单:确保用户可以轻松输入发票所需的所有信息。
  2. 动态计算总计:根据用户输入的项目明细,实时计算小计、税额和总计。
  3. 生成PDF发票:使用jsPDF库,根据收集到的数据生成格式化的PDF发票。
  4. 提供下载功能:允许用户一键下载生成的PDF发票。

最佳实践

  1. 数据验证

    • 确保用户输入的数据是有效的,如数量和单价为正数,必要字段不为空等。
    • 可以使用前端验证库如validate.js或原生HTML5验证。
  2. 提高用户体验

    • 提供实时预览功能,让用户在生成PDF之前可以查看发票的样式和内容。
    • 使用加载动画提示用户生成PDF的过程,尤其是在处理大量数据时。
  3. 安全性考虑

    • 尽管PDF生成在前端进行,但确保敏感信息的输入和处理符合安全标准。
    • 不要在前端存储或传输敏感数据,必要时结合后端进行处理。
  4. 响应式设计

    • 确保发票在不同设备和屏幕尺寸下都能正确显示。
    • 使用CSS媒体查询和灵活的布局技术,如Flexbox或Grid。
  5. 优化PDF样式

    • 根据需求设计专业的发票模板,使用统一的字体、颜色和布局。
    • 可以结合CSS和HTML的灵活布局,提升PDF的美观性和可读性。
  6. 兼容性测试

    • 测试生成的PDF在不同浏览器和设备上的显示效果,确保一致性。
    • 确保jsPDF库和插件在目标环境中的兼容性。
  7. 扩展功能

    • 添加签名、二维码等高级功能,提升发票的专业性和功能性。
    • 提供多种导出格式,如DOCX、XLSX等,满足不同用户需求。

参考资料

  1. jsPDF 官方文档
  2. jsPDF-autotable插件文档
  3. jsPDF 示例与教程
  4. HTML5表单验证
  5. CSS Flexbox指南
  6. CSS Grid指南
  7. 前端表单处理最佳实践
  8. 生成PDF的多种方法对比