html2pdf.js使用与配置详解

一、概述

html2pdf.js 是一个纯前端的 JavaScript 库,用于将 HTML 元素转换为可下载的 PDF 文件。它基于两个底层库:

  • html2canvas:将 DOM 元素渲染为 Canvas 图像12。

  • jsPDF :将图像或文本生成 PDF 文件26。
    特点:完全在浏览器端运行,无需服务器支持,适合生成报告、发票等场景17。


二、工作原理

转换过程分为两步:

  1. HTML → Canvas

    • 使用 html2canvas 捕获目标 DOM 的快照,生成 Canvas 图像(本质是位图)26。
  2. Canvas → PDF

    • 将 Canvas 图像插入 jsPDF 实例,按配置(如页面尺寸、方向)生成 PDF28。

图表

代码

下载

HTML元素

html2canvas生成Canvas图像

jsPDF创建PDF文档

保存或下载PDF文件


三、基本用法

1. 安装

bash

复制

下载

复制代码
npm install html2pdf.js  # 或通过 CDN 引入:cite[3]:cite[7]
2. 核心代码

html

复制

下载

运行

复制代码
<button οnclick="generatePDF()">导出 PDF</button>
<div id="element-to-print">
  <h1>标题</h1>
  <p>待转换内容</p>
</div>

<script>
  function generatePDF() {
    const element = document.getElementById('element-to-print');
    html2pdf()
      .from(element)
      .save('document.pdf');  // 保存文件:cite[1]:cite[8]
  }
</script>

四、配置选项

通过链式调用自定义输出效果:

配置分类 常用参数 作用
全局设置 margin PDF 页边距(单位:mm/pt/in)37
filename 输出文件名(默认:file.pdf
图像质量 image: { type: 'jpeg', quality: 0.98 } 图片格式与质量(0-1)3
html2canvas scale: 2 渲染缩放倍数(提高清晰度)17
jsPDF orientation: 'portrait' 页面方向(portrait/landscape
format: 'a4' 纸张尺寸(如 a4, letter)38

示例配置

javascript

复制

下载

复制代码
html2pdf().set({
  margin: [15, 10],       // 上下15mm,左右10mm
  filename: 'report.pdf',
  image: { type: 'jpeg', quality: 0.95 },
  html2canvas: { scale: 2, dpi: 192 },
  jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
}).from(element).save();

五、高级技巧

  1. 处理长内容分页

    • 问题:Canvas 有最大尺寸限制(如高度超 16384 像素会截断)16。

    • 方案:

      • 手动拆分内容为多个区块,分次渲染1。

      • 使用 pagebreak 参数自动分页:

        javascript

        复制

        下载

        复制代码
        pagebreak: { mode: 'avoid-all', before: '.page-break' }  // 避免元素截断,在指定类前分页:cite[7]
  2. 解决渲染不一致问题

    • 原因:依赖浏览器渲染引擎,不同平台效果可能差异。

    • 优化:

      • 使用 Web Safe Fonts(如 Arial, Helvetica)2。

      • 避免复杂 CSS 属性(如 position: fixed)3。

  3. 在 Vue/React 中的集成

    • 通过 ref 获取 DOM 元素,避免直接操作 DOM78:

      vue

      复制

      下载

      复制代码
      <template>
        <div ref="pdfContent">...</div>
        <button @click="exportPDF">导出</button>
      </template>
      <script>
      import html2pdf from 'html2pdf.js';
      export default {
        methods: {
          exportPDF() {
            html2pdf().from(this.$refs.pdfContent).save();
          }
        }
      };
      </script>

六、优缺点分析

优点 缺点
✅ 纯前端实现,无需后端支持1 ❌ 复杂页面渲染耗时长(低端设备卡顿)2
✅ 配置灵活,支持自定义样式 ❌ 文本为图像形式,无法复制/搜索23
✅ 开源免费,社区活跃 ❌ 超大内容需手动分页(Canvas 尺寸限制)6

七、适用场景

  • 生成电子发票或订单凭证2。

  • 导出数据报表(如图表、表格)7。

  • 博客/文章离线保存(一键下载为 PDF)28。


八、替代方案对比

库名 特点 适用场景
jsPDF + html2canvas 基础组合,需手动处理分页 简单内容导出
Print.js 直接调用浏览器打印,不支持复杂样式3 快速打印功能
jsPDF-AutoTable 仅支持表格导出3 纯表格数据报表

九、最佳实践建议

  1. 样式优化

    • 使用 @media print 定义打印专用样式3。

    • 避免背景图/渐变,防止渲染异常。

  2. 性能提升

    • 隐藏非必要元素(如按钮、广告)再生成 PDF。

    • 分块渲染超长内容(循环调用 html2pdf)1。

  3. 兼容性处理

    • 在 Safari 中测试字体嵌入效果。

    • 备用方案:服务端生成(如 Puppeteer)5。

💡 关键提示 :若需高质量可搜索文本 PDF,建议改用服务端方案(如 wkhtmltopdfPuppeteer)56。html2pdf.js 的核心价值在于客户端快速生成,适用于对文本可编辑性要求不高的场景。

通过合理配置与问题规避,html2pdf.js 仍是轻量级 PDF 导出需求的优选工具。完整示例参考 官方 GitHub

vue示例

html 复制代码
<template>
  <div class="pdf-exporter-container">
    <div class="header">
      <h1><i class="fas fa-file-pdf"></i> Vue3 PDF导出工具</h1>
      <p>使用html2pdf.js库将HTML内容转换为PDF文件</p>
    </div>

    <div class="content-container">
      <!-- 编辑器区域 -->
      <div class="editor-section">
        <div class="section-header">
          <h2><i class="fas fa-edit"></i> HTML内容</h2>
          <div class="controls">
            <button class="btn" @click="loadSample('report')">
              <i class="fas fa-file-alt"></i> 示例报告
            </button>
            <button class="btn" @click="loadSample('invoice')">
              <i class="fas fa-file-invoice"></i> 示例发票
            </button>
            <button class="btn" @click="resetContent">
              <i class="fas fa-trash-alt"></i> 清空
            </button>
          </div>
        </div>
        <textarea 
          v-model="htmlContent" 
          class="html-editor" 
          placeholder="在此输入HTML内容..."
        ></textarea>
      </div>

      <!-- 预览区域 -->
      <div class="preview-section">
        <div class="section-header">
          <h2><i class="fas fa-eye"></i> 预览</h2>
          <div class="controls">
            <button class="btn primary" @click="updatePreview">
              <i class="fas fa-sync-alt"></i> 刷新预览
            </button>
          </div>
        </div>
        <div class="preview-container">
          <div class="preview-content" v-html="previewContent"></div>
        </div>
      </div>

      <!-- 配置区域 -->
      <div class="config-section">
        <div class="section-header">
          <h2><i class="fas fa-cog"></i> PDF设置</h2>
        </div>
        
        <div class="config-grid">
          <div class="config-group">
            <h3><i class="fas fa-file"></i> 页面设置</h3>
            <div class="form-group">
              <label>页面方向:</label>
              <select v-model="pdfOptions.orientation">
                <option value="portrait">纵向</option>
                <option value="landscape">横向</option>
              </select>
            </div>
            <div class="form-group">
              <label>页面格式:</label>
              <select v-model="pdfOptions.format">
                <option value="a4">A4</option>
                <option value="letter">Letter</option>
                <option value="a3">A3</option>
                <option value="a5">A5</option>
              </select>
            </div>
            <div class="form-group">
              <label>页边距 (mm):</label>
              <input type="number" v-model.number="pdfOptions.margin" min="0" max="50">
            </div>
          </div>

          <div class="config-group">
            <h3><i class="fas fa-image"></i> 渲染设置</h3>
            <div class="form-group">
              <label>图像质量:</label>
              <select v-model="pdfOptions.imageQuality">
                <option :value="0.8">高 (80%)</option>
                <option :value="0.9">非常高 (90%)</option>
                <option :value="1.0">最高 (100%)</option>
              </select>
            </div>
            <div class="form-group">
              <label>缩放比例:</label>
              <select v-model="pdfOptions.scale">
                <option :value="1">1x</option>
                <option :value="2">2x (推荐)</option>
                <option :value="3">3x</option>
              </select>
            </div>
            <div class="form-group">
              <label>文件名:</label>
              <input type="text" v-model="pdfOptions.filename">
            </div>
          </div>
        </div>
      </div>
    </div>

    <!-- 操作按钮 -->
    <div class="actions">
      <button class="btn primary large" @click="exportPDF">
        <i class="fas fa-download"></i> 导出PDF
      </button>
      <button class="btn secondary large" @click="openInNewWindow">
        <i class="fas fa-external-link-alt"></i> 新窗口预览
      </button>
    </div>

    <!-- 状态提示 -->
    <div class="status" :class="status.type" v-if="status.show">
      <i class="icon" :class="status.icon"></i>
      <span>{{ status.message }}</span>
    </div>

    <div class="footer">
      <p>使用 <strong>html2pdf.js</strong> 和 <strong>Vue3</strong> 构建 | 基于TypeScript实现</p>
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref, reactive, onMounted } from 'vue';
import html2pdf from 'html2pdf.js';

interface PdfOptions {
  orientation: 'portrait' | 'landscape';
  format: string;
  margin: number;
  imageQuality: number;
  scale: number;
  filename: string;
}

interface Status {
  show: boolean;
  type: 'success' | 'error' | 'loading';
  message: string;
  icon: string;
}

export default defineComponent({
  name: 'PdfExporter',
  
  setup() {
    // HTML内容编辑器
    const htmlContent = ref<string>('');
    
    // 预览内容
    const previewContent = ref<string>('');
    
    // PDF配置选项
    const pdfOptions = reactive<PdfOptions>({
      orientation: 'portrait',
      format: 'a4',
      margin: 15,
      imageQuality: 0.9,
      scale: 2,
      filename: 'document.pdf'
    });
    
    // 状态管理
    const status = reactive<Status>({
      show: false,
      type: 'success',
      message: '',
      icon: 'fas fa-check-circle'
    });
    
    // 当前日期
    const currentDate = new Date().toLocaleDateString('zh-CN', {
      year: 'numeric',
      month: 'long',
      day: 'numeric'
    });
    
    // 更新预览内容
    const updatePreview = () => {
      // 替换模板变量
      let content = htmlContent.value.replace('{{ currentDate }}', currentDate);
      previewContent.value = content;
    };
    
    // 导出PDF
    const exportPDF = async () => {
      showStatus('loading', '正在生成PDF...', 'fas fa-spinner fa-spin');
      
      try {
        const element = document.querySelector('.preview-content') as HTMLElement;
        const opt = {
          margin: pdfOptions.margin,
          filename: pdfOptions.filename,
          image: { type: 'jpeg', quality: pdfOptions.imageQuality },
          html2canvas: { scale: pdfOptions.scale },
          jsPDF: { 
            unit: 'mm', 
            format: pdfOptions.format, 
            orientation: pdfOptions.orientation 
          }
        };
        
        await html2pdf().set(opt).from(element).save();
        
        showStatus('success', 'PDF已成功生成并下载!', 'fas fa-check-circle');
      } catch (error) {
        console.error('导出PDF失败:', error);
        showStatus('error', `导出失败: ${(error as Error).message}`, 'fas fa-exclamation-circle');
      }
    };
    
    // 在新窗口打开PDF
    const openInNewWindow = async () => {
      showStatus('loading', '正在生成预览...', 'fas fa-spinner fa-spin');
      
      try {
        const element = document.querySelector('.preview-content') as HTMLElement;
        const opt = {
          margin: pdfOptions.margin,
          image: { type: 'jpeg', quality: pdfOptions.imageQuality },
          html2canvas: { scale: pdfOptions.scale },
          jsPDF: { 
            unit: 'mm', 
            format: pdfOptions.format, 
            orientation: pdfOptions.orientation 
          }
        };
        
        const blob = await html2pdf().set(opt).from(element).outputPdf('blob');
        const url = URL.createObjectURL(blob);
        window.open(url, '_blank');
        
        showStatus('success', 'PDF已在新窗口打开', 'fas fa-check-circle');
      } catch (error) {
        console.error('预览PDF失败:', error);
        showStatus('error', `预览失败: ${(error as Error).message}`, 'fas fa-exclamation-circle');
      }
    };
    
    // 加载示例内容
    const loadSample = (type: 'report' | 'invoice') => {
      if (type === 'report') {
        htmlContent.value = `
<div>
  <h1>项目进度报告 - 2023年10月</h1>
  <p>报告日期: {{ currentDate }}</p>
  
  <h3>项目概览</h3>
  <p>项目目前进展顺利,整体完成度为 <strong>75%</strong>,预计将在11月底前完成。</p>
  
  <h3>任务完成情况</h3>
  <table>
    <thead>
      <tr>
        <th>任务名称</th>
        <th>负责人</th>
        <th>状态</th>
        <th>完成日期</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>用户界面设计</td>
        <td>张设计师</td>
        <td>已完成</td>
        <td>2023-09-15</td>
      </tr>
      <tr>
        <td>后端API开发</td>
        <td>李工程师</td>
        <td>进行中 (90%)</td>
        <td>2023-10-20</td>
      </tr>
      <tr>
        <td>数据库优化</td>
        <td>王工程师</td>
        <td>进行中 (70%)</td>
        <td>2023-10-25</td>
      </tr>
      <tr>
        <td>用户测试</td>
        <td>测试团队</td>
        <td>未开始</td>
        <td>2023-11-05</td>
      </tr>
    </tbody>
  </table>
  
  <h3>下月重点</h3>
  <ul>
    <li>完成所有核心功能开发</li>
    <li>启动第一阶段用户测试</li>
    <li>准备项目交付文档</li>
  </ul>
  
  <div style="margin-top: 30px; text-align: center; color: #7f8c8d; font-size: 0.9rem;">
    <p>项目负责人: 王经理 | 下次报告日期: 2023-11-05</p>
  </div>
</div>
        `.trim();
      } else if (type === 'invoice') {
        htmlContent.value = `
<div style="max-width: 800px; margin: 0 auto;">
  <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px;">
    <div>
      <h1 style="color: #2c3e50;">发票</h1>
      <p>发票号: INV-2023-00142</p>
      <p>日期: {{ currentDate }}</p>
    </div>
    <div style="text-align: right;">
      <h2>科技有限公司</h2>
      <p>北京市海淀区科技园88号</p>
      <p>电话: (010) 1234-5678</p>
    </div>
  </div>
  
  <div style="display: flex; justify-content: space-between; margin-bottom: 30px; background: #f9f9f9; padding: 20px; border-radius: 8px;">
    <div>
      <h3>账单至:</h3>
      <p>客户公司名称</p>
      <p>联系人: 张先生</p>
      <p>上海市浦东新区金融街100号</p>
    </div>
    <div style="text-align: right;">
      <h3>付款信息</h3>
      <p>金额: <strong style="font-size: 1.2rem;">¥18,750.00</strong></p>
      <p>付款方式: 银行转账</p>
      <p>付款期限: 30天</p>
    </div>
  </div>
  
  <h3>收费项目</h3>
  <table>
    <thead>
      <tr>
        <th>项目描述</th>
        <th>数量</th>
        <th>单价</th>
        <th>总价</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>企业版软件许可 (年)</td>
        <td>5</td>
        <td>¥2,500.00</td>
        <td>¥12,500.00</td>
      </tr>
      <tr>
        <td>技术咨询服务</td>
        <td>40小时</td>
        <td>¥500.00</td>
        <td>¥20,000.00</td>
      </tr>
      <tr>
        <td>折扣</td>
        <td>-</td>
        <td>-</td>
        <td>-¥13,750.00</td>
      </tr>
    </tbody>
    <tfoot>
      <tr style="font-weight: bold; background: #f1f2f6;">
        <td colspan="3" style="text-align: right;">总计:</td>
        <td>¥18,750.00</td>
      </tr>
    </tfoot>
  </table>
  
  <div style="margin-top: 40px; padding-top: 20px; border-top: 1px solid #eee; text-align: center; color: #7f8c8d;">
    <p>感谢您的惠顾!如有任何问题,请随时联系我们的财务部门。</p>
    <p>tech@company.com | (010) 1234-5678</p>
  </div>
</div>
        `.trim();
      }
      
      updatePreview();
      showStatus('success', '示例已加载', 'fas fa-check-circle');
    };
    
    // 重置内容
    const resetContent = () => {
      htmlContent.value = '';
      previewContent.value = '';
      showStatus('success', '内容已清空', 'fas fa-check-circle');
    };
    
    // 显示状态消息
    const showStatus = (type: 'success' | 'error' | 'loading', message: string, icon: string) => {
      status.type = type;
      status.message = message;
      status.icon = icon;
      status.show = true;
      
      // 自动隐藏消息
      setTimeout(() => {
        status.show = false;
      }, type === 'loading' ? 5000 : 3000);
    };
    
    // 初始化组件
    onMounted(() => {
      // 加载默认示例
      loadSample('report');
    });
    
    return {
      htmlContent,
      previewContent,
      pdfOptions,
      status,
      updatePreview,
      exportPDF,
      openInNewWindow,
      loadSample,
      resetContent
    };
  }
});
</script>

<style scoped>
.pdf-exporter-container {
  max-width: 1400px;
  margin: 0 auto;
  padding: 20px;
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  color: #2c3e50;
  background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
  min-height: 100vh;
}

.header {
  text-align: center;
  padding: 30px 0;
  margin-bottom: 20px;
}

.header h1 {
  font-size: 2.5rem;
  font-weight: 700;
  color: #2c3e50;
  margin-bottom: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 15px;
}

.header p {
  font-size: 1.1rem;
  color: #6c757d;
  max-width: 700px;
  margin: 0 auto;
  line-height: 1.6;
}

.content-container {
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-template-rows: auto auto;
  gap: 25px;
  margin-bottom: 30px;
}

.editor-section {
  grid-column: 1;
  grid-row: 1;
  background: white;
  border-radius: 12px;
  box-shadow: 0 5px 20px rgba(0, 0, 0, 0.08);
  overflow: hidden;
}

.preview-section {
  grid-column: 2;
  grid-row: 1 / span 2;
  background: white;
  border-radius: 12px;
  box-shadow: 0 5px 20px rgba(0, 0, 0, 0.08);
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

.config-section {
  grid-column: 1;
  grid-row: 2;
  background: white;
  border-radius: 12px;
  box-shadow: 0 5px 20px rgba(0, 0, 0, 0.08);
  overflow: hidden;
}

.section-header {
  background: linear-gradient(to right, #3498db, #2c3e50);
  color: white;
  padding: 18px 25px;
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.section-header h2 {
  font-size: 1.4rem;
  font-weight: 600;
  display: flex;
  align-items: center;
  gap: 10px;
}

.controls {
  display: flex;
  gap: 10px;
}

.html-editor {
  width: 100%;
  min-height: 300px;
  padding: 20px;
  font-family: 'Courier New', Courier, monospace;
  font-size: 0.95rem;
  line-height: 1.5;
  border: none;
  resize: vertical;
  background: #f8f9fa;
  border-radius: 0 0 12px 12px;
}

.html-editor:focus {
  outline: none;
  background: #fff;
}

.preview-container {
  flex: 1;
  padding: 20px;
  overflow: auto;
  background: #f8f9fa;
  border-radius: 0 0 12px 12px;
}

.preview-content {
  background: white;
  padding: 25px;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
  min-height: 100%;
}

.preview-content h1, .preview-content h2, .preview-content h3 {
  color: #2c3e50;
  margin-top: 0;
}

.preview-content table {
  width: 100%;
  border-collapse: collapse;
  margin: 20px 0;
}

.preview-content th {
  background-color: #3498db;
  color: white;
  text-align: left;
  padding: 12px 15px;
}

.preview-content td {
  padding: 10px 15px;
  border-bottom: 1px solid #e0e0e0;
}

.preview-content tr:nth-child(even) {
  background-color: #f8f9fa;
}

.config-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 25px;
  padding: 25px;
}

.config-group {
  background: #f8f9fa;
  padding: 20px;
  border-radius: 8px;
}

.config-group h3 {
  font-size: 1.2rem;
  margin-top: 0;
  margin-bottom: 20px;
  padding-bottom: 10px;
  border-bottom: 2px solid #3498db;
  display: flex;
  align-items: center;
  gap: 10px;
}

.form-group {
  margin-bottom: 18px;
}

.form-group label {
  display: block;
  margin-bottom: 8px;
  font-weight: 500;
  color: #495057;
}

.form-group select, .form-group input {
  width: 100%;
  padding: 12px;
  border: 1px solid #ced4da;
  border-radius: 6px;
  background: white;
  font-size: 1rem;
}

.actions {
  display: flex;
  justify-content: center;
  gap: 20px;
  margin: 30px 0;
}

.btn {
  padding: 12px 24px;
  border: none;
  border-radius: 8px;
  font-size: 1rem;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.3s ease;
  display: flex;
  align-items: center;
  gap: 10px;
  background: #e9ecef;
  color: #495057;
}

.btn.primary {
  background: linear-gradient(to right, #3498db, #2980b9);
  color: white;
  box-shadow: 0 4px 12px rgba(52, 152, 219, 0.3);
}

.btn.primary:hover {
  transform: translateY(-2px);
  box-shadow: 0 6px 15px rgba(52, 152, 219, 0.4);
}

.btn.secondary {
  background: linear-gradient(to right, #6c757d, #495057);
  color: white;
}

.btn.secondary:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(108, 117, 125, 0.3);
}

.btn.large {
  padding: 15px 30px;
  font-size: 1.1rem;
}

.status {
  padding: 15px 25px;
  border-radius: 8px;
  margin: 20px auto;
  max-width: 600px;
  display: flex;
  align-items: center;
  gap: 15px;
  font-weight: 500;
  box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1);
}

.status.success {
  background: rgba(46, 204, 113, 0.15);
  color: #27ae60;
  border-left: 4px solid #27ae60;
}

.status.error {
  background: rgba(231, 76, 60, 0.15);
  color: #c0392b;
  border-left: 4px solid #c0392b;
}

.status.loading {
  background: rgba(52, 152, 219, 0.15);
  color: #2980b9;
  border-left: 4px solid #2980b9;
}

.footer {
  text-align: center;
  padding: 30px 0 20px;
  color: #6c757d;
  font-size: 0.95rem;
}

/* 响应式设计 */
@media (max-width: 1200px) {
  .content-container {
    grid-template-columns: 1fr;
    grid-template-rows: auto auto auto;
  }
  
  .editor-section {
    grid-column: 1;
    grid-row: 1;
  }
  
  .preview-section {
    grid-column: 1;
    grid-row: 2;
    min-height: 500px;
  }
  
  .config-section {
    grid-column: 1;
    grid-row: 3;
  }
}

@media (max-width: 768px) {
  .config-grid {
    grid-template-columns: 1fr;
  }
  
  .actions {
    flex-direction: column;
    align-items: center;
  }
}
</style>

html示例

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Vue3 PDF导出工具</title>
  <script src="https://unpkg.com/vue@3.2.47/dist/vue.global.js"></script>
  <script src="https://unpkg.com/html2pdf.js@0.10.1/dist/html2pdf.bundle.min.js"></script>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
      font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
    }
    
    body {
      background: linear-gradient(135deg, #f5f7fa 0%, #e4edf5 100%);
      min-height: 100vh;
      padding: 20px;
      color: #2c3e50;
    }
    
    #app {
      max-width: 1200px;
      margin: 0 auto;
    }
    
    .header {
      text-align: center;
      padding: 40px 0;
    }
    
    .header h1 {
      font-size: 2.5rem;
      font-weight: 700;
      color: #2c3e50;
      margin-bottom: 10px;
    }
    
    .header p {
      font-size: 1.1rem;
      color: #7f8c8d;
      max-width: 700px;
      margin: 0 auto;
      line-height: 1.6;
    }
    
    .container {
      display: flex;
      gap: 30px;
      margin-bottom: 40px;
    }
    
    @media (max-width: 900px) {
      .container {
        flex-direction: column;
      }
    }
    
    .editor-section, .preview-section {
      flex: 1;
      background: white;
      border-radius: 16px;
      box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
      overflow: hidden;
    }
    
    .section-header {
      background: linear-gradient(to right, #3498db, #2c3e50);
      color: white;
      padding: 18px 25px;
      display: flex;
      align-items: center;
      justify-content: space-between;
    }
    
    .section-header h2 {
      font-size: 1.3rem;
      font-weight: 600;
      display: flex;
      align-items: center;
      gap: 10px;
    }
    
    .section-content {
      padding: 25px;
    }
    
    .editor {
      width: 100%;
      min-height: 300px;
      border: 1px solid #e0e0e0;
      border-radius: 8px;
      padding: 15px;
      font-size: 1rem;
      line-height: 1.6;
      resize: vertical;
      transition: border-color 0.3s;
    }
    
    .editor:focus {
      outline: none;
      border-color: #3498db;
      box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2);
    }
    
    .preview-container {
      background: #f9f9f9;
      border-radius: 8px;
      padding: 20px;
      min-height: 350px;
      border: 1px solid #eee;
    }
    
    .controls {
      display: flex;
      flex-wrap: wrap;
      gap: 15px;
      margin-top: 30px;
      padding: 20px;
      background: white;
      border-radius: 16px;
      box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
    }
    
    .control-group {
      flex: 1;
      min-width: 250px;
    }
    
    .control-group h3 {
      margin-bottom: 15px;
      color: #2c3e50;
      font-size: 1.1rem;
      display: flex;
      align-items: center;
      gap: 8px;
    }
    
    .control-row {
      display: flex;
      align-items: center;
      margin-bottom: 12px;
    }
    
    label {
      width: 140px;
      font-weight: 500;
      color: #555;
    }
    
    select, input {
      flex: 1;
      padding: 10px 12px;
      border: 1px solid #ddd;
      border-radius: 6px;
      background: white;
    }
    
    .buttons {
      display: flex;
      gap: 15px;
      flex-wrap: wrap;
      margin-top: 10px;
    }
    
    button {
      padding: 14px 24px;
      border: none;
      border-radius: 8px;
      font-weight: 600;
      font-size: 1rem;
      cursor: pointer;
      transition: all 0.3s ease;
      display: flex;
      align-items: center;
      gap: 8px;
    }
    
    .btn-primary {
      background: linear-gradient(to right, #3498db, #2980b9);
      color: white;
      box-shadow: 0 4px 10px rgba(52, 152, 219, 0.3);
    }
    
    .btn-primary:hover {
      transform: translateY(-2px);
      box-shadow: 0 6px 15px rgba(52, 152, 219, 0.4);
    }
    
    .btn-secondary {
      background: #f1f2f6;
      color: #2c3e50;
    }
    
    .btn-secondary:hover {
      background: #e0e3e9;
    }
    
    .btn-success {
      background: linear-gradient(to right, #2ecc71, #27ae60);
      color: white;
      box-shadow: 0 4px 10px rgba(46, 204, 113, 0.3);
    }
    
    .btn-success:hover {
      transform: translateY(-2px);
      box-shadow: 0 6px 15px rgba(46, 204, 113, 0.4);
    }
    
    .btn-icon {
      font-size: 1.2rem;
    }
    
    .footer {
      text-align: center;
      padding: 30px 0;
      color: #7f8c8d;
      font-size: 0.95rem;
    }
    
    .footer a {
      color: #3498db;
      text-decoration: none;
    }
    
    .footer a:hover {
      text-decoration: underline;
    }
    
    .preview-content {
      line-height: 1.7;
    }
    
    .preview-content h3 {
      color: #2c3e50;
      margin: 20px 0 10px;
      padding-bottom: 8px;
      border-bottom: 2px solid #3498db;
    }
    
    .preview-content p {
      margin-bottom: 15px;
    }
    
    .preview-content ul {
      padding-left: 25px;
      margin-bottom: 20px;
    }
    
    .preview-content li {
      margin-bottom: 8px;
    }
    
    .table {
      width: 100%;
      border-collapse: collapse;
      margin: 20px 0;
      background: white;
      box-shadow: 0 2px 5px rgba(0,0,0,0.05);
    }
    
    .table th {
      background: #3498db;
      color: white;
      text-align: left;
      padding: 12px 15px;
    }
    
    .table td {
      padding: 10px 15px;
      border-bottom: 1px solid #eee;
    }
    
    .table tr:nth-child(even) {
      background: #f9f9f9;
    }
    
    .table tr:hover {
      background: #f1f8ff;
    }
    
    .highlight {
      background: #f1f8ff;
      padding: 3px 5px;
      border-radius: 4px;
      color: #2980b9;
      font-weight: 500;
    }
    
    .status {
      padding: 15px;
      border-radius: 8px;
      margin-top: 20px;
      display: none;
      align-items: center;
      gap: 10px;
    }
    
    .status.visible {
      display: flex;
    }
    
    .status.success {
      background: rgba(46, 204, 113, 0.15);
      color: #27ae60;
    }
    
    .status.error {
      background: rgba(231, 76, 60, 0.15);
      color: #c0392b;
    }
    
    .spinner {
      width: 20px;
      height: 20px;
      border: 3px solid rgba(52, 152, 219, 0.3);
      border-top: 3px solid #3498db;
      border-radius: 50%;
      animation: spin 1s linear infinite;
    }
    
    @keyframes spin {
      0% { transform: rotate(0deg); }
      100% { transform: rotate(360deg); }
    }
  </style>
</head>
<body>
  <div id="app">
    <div class="header">
      <h1><i class="fas fa-file-pdf"></i> Vue3 PDF导出工具</h1>
      <p>使用html2pdf.js库将HTML内容转换为PDF文件 - 基于Vue3和TypeScript实现</p>
    </div>
    
    <div class="container">
      <div class="editor-section">
        <div class="section-header">
          <h2><i class="fas fa-edit"></i> HTML编辑器</h2>
          <div class="buttons">
            <button class="btn-secondary" @click="loadSample('report')">
              <i class="fas fa-file-alt"></i> 示例报告
            </button>
            <button class="btn-secondary" @click="loadSample('invoice')">
              <i class="fas fa-receipt"></i> 示例发票
            </button>
          </div>
        </div>
        <div class="section-content">
          <textarea class="editor" v-model="htmlContent" placeholder="在此输入HTML内容..."></textarea>
        </div>
      </div>
      
      <div class="preview-section">
        <div class="section-header">
          <h2><i class="fas fa-eye"></i> 实时预览</h2>
          <div class="buttons">
            <button class="btn-primary" @click="updatePreview">
              <i class="fas fa-sync-alt"></i> 刷新预览
            </button>
          </div>
        </div>
        <div class="section-content">
          <div class="preview-container">
            <div class="preview-content" v-html="previewContent"></div>
          </div>
        </div>
      </div>
    </div>
    
    <div class="controls">
      <div class="control-group">
        <h3><i class="fas fa-sliders-h"></i> PDF设置</h3>
        <div class="control-row">
          <label>页面方向:</label>
          <select v-model="pdfOptions.orientation">
            <option value="portrait">纵向</option>
            <option value="landscape">横向</option>
          </select>
        </div>
        <div class="control-row">
          <label>页面格式:</label>
          <select v-model="pdfOptions.format">
            <option value="a4">A4</option>
            <option value="letter">Letter</option>
            <option value="a3">A3</option>
            <option value="a5">A5</option>
          </select>
        </div>
        <div class="control-row">
          <label>页边距 (mm):</label>
          <input type="number" v-model.number="pdfOptions.margin" min="0" max="50">
        </div>
      </div>
      
      <div class="control-group">
        <h3><i class="fas fa-paint-brush"></i> 渲染设置</h3>
        <div class="control-row">
          <label>图像质量:</label>
          <select v-model="pdfOptions.imageQuality">
            <option :value="0.8">高 (80%)</option>
            <option :value="0.9">非常高 (90%)</option>
            <option :value="1.0">最高 (100%)</option>
          </select>
        </div>
        <div class="control-row">
          <label>缩放比例:</label>
          <select v-model="pdfOptions.scale">
            <option :value="1">1x</option>
            <option :value="2">2x (推荐)</option>
            <option :value="3">3x</option>
          </select>
        </div>
        <div class="control-row">
          <label>文件名:</label>
          <input type="text" v-model="pdfOptions.filename">
        </div>
      </div>
      
      <div class="control-group">
        <h3><i class="fas fa-file-export"></i> 导出操作</h3>
        <div class="buttons">
          <button class="btn-primary" @click="exportPDF">
            <i class="fas fa-download"></i> 导出PDF
          </button>
          <button class="btn-success" @click="openPDF">
            <i class="fas fa-external-link-alt"></i> 新窗口预览
          </button>
          <button class="btn-secondary" @click="reset">
            <i class="fas fa-redo"></i> 重置
          </button>
        </div>
        <div class="status" :class="{ visible: status.visible, success: status.type === 'success', error: status.type === 'error' }">
          <div v-if="status.loading" class="spinner"></div>
          <i v-else-if="status.type === 'success'" class="fas fa-check-circle"></i>
          <i v-else-if="status.type === 'error'" class="fas fa-exclamation-circle"></i>
          <span>{{ status.message }}</span>
        </div>
      </div>
    </div>
    
    <div class="footer">
      <p>使用 <span class="highlight">html2pdf.js</span> 和 <span class="highlight">Vue3</span> 构建 | 本工具完全在浏览器中运行,无需服务器处理</p>
      <p>提示:对于复杂布局,建议使用2x缩放比例以获得最佳效果</p>
    </div>
  </div>

  <script>
    const { createApp, ref, reactive, onMounted } = Vue;
    
    createApp({
      setup() {
        // 初始HTML内容
        const htmlContent = ref(`
<div>
  <h1>销售报告 - 2023年第三季度</h1>
  <p>报告生成日期: {{ currentDate }}</p>
  
  <h3>季度概览</h3>
  <p>本季度总收入为 <strong>$125,430</strong>,相比上季度增长 <strong>12.5%</strong>。</p>
  
  <h3>产品表现</h3>
  <table class="table">
    <thead>
      <tr>
        <th>产品名称</th>
        <th>销售量</th>
        <th>收入</th>
        <th>增长率</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>高级订阅</td>
        <td>1,240</td>
        <td>$84,200</td>
        <td>+15.2%</td>
      </tr>
      <tr>
        <td>企业套件</td>
        <td>480</td>
        <td>$32,150</td>
        <td>+8.7%</td>
      </tr>
      <tr>
        <td>基础版</td>
        <td>3,120</td>
        <td>$9,080</td>
        <td>+4.3%</td>
      </tr>
    </tbody>
  </table>
  
  <h3>下季度目标</h3>
  <ul>
    <li>总收入目标: $140,000</li>
    <li>新客户增长: 20%</li>
    <li>推出新产品线</li>
  </ul>
  
  <div style="margin-top: 30px; text-align: center; color: #7f8c8d; font-size: 0.9rem;">
    <p>此报告由销售部门生成 - 机密文件</p>
  </div>
</div>
        `.trim());
        
        const previewContent = ref('');
        const currentDate = new Date().toLocaleDateString('zh-CN', {
          year: 'numeric',
          month: 'long',
          day: 'numeric'
        });
        
        // PDF配置选项
        const pdfOptions = reactive({
          orientation: 'portrait',
          format: 'a4',
          margin: 15,
          imageQuality: 0.9,
          scale: 2,
          filename: 'document.pdf'
        });
        
        // 状态管理
        const status = reactive({
          visible: false,
          type: '',
          message: '',
          loading: false
        });
        
        // 更新预览内容
        const updatePreview = () => {
          // 替换模板变量
          let content = htmlContent.value.replace('{{ currentDate }}', currentDate);
          previewContent.value = content;
        };
        
        // 导出PDF
        const exportPDF = () => {
          status.visible = true;
          status.loading = true;
          status.type = '';
          status.message = '正在生成PDF...';
          
          try {
            const element = document.querySelector('.preview-content');
            const opt = {
              margin: pdfOptions.margin,
              filename: pdfOptions.filename,
              image: { type: 'jpeg', quality: pdfOptions.imageQuality },
              html2canvas: { scale: pdfOptions.scale },
              jsPDF: { 
                unit: 'mm', 
                format: pdfOptions.format, 
                orientation: pdfOptions.orientation 
              }
            };
            
            html2pdf()
              .set(opt)
              .from(element)
              .save()
              .then(() => {
                status.loading = false;
                status.type = 'success';
                status.message = 'PDF已成功生成并下载!';
                
                // 5秒后隐藏状态消息
                setTimeout(() => {
                  status.visible = false;
                }, 5000);
              });
          } catch (error) {
            status.loading = false;
            status.type = 'error';
            status.message = `导出失败: ${error.message}`;
            
            // 8秒后隐藏状态消息
            setTimeout(() => {
              status.visible = false;
            }, 8000);
          }
        };
        
        // 在新窗口打开PDF
        const openPDF = () => {
          status.visible = true;
          status.loading = true;
          status.message = '正在生成预览...';
          
          try {
            const element = document.querySelector('.preview-content');
            const opt = {
              margin: pdfOptions.margin,
              image: { type: 'jpeg', quality: pdfOptions.imageQuality },
              html2canvas: { scale: pdfOptions.scale },
              jsPDF: { 
                unit: 'mm', 
                format: pdfOptions.format, 
                orientation: pdfOptions.orientation 
              }
            };
            
            html2pdf()
              .set(opt)
              .from(element)
              .outputPdf('blob')
              .then(blob => {
                const url = URL.createObjectURL(blob);
                window.open(url, '_blank');
                
                status.loading = false;
                status.type = 'success';
                status.message = 'PDF已在新窗口打开';
                
                setTimeout(() => {
                  status.visible = false;
                }, 4000);
              });
          } catch (error) {
            status.loading = false;
            status.type = 'error';
            status.message = `预览失败: ${error.message}`;
            
            setTimeout(() => {
              status.visible = false;
            }, 8000);
          }
        };
        
        // 加载示例
        const loadSample = (type) => {
          if (type === 'report') {
            htmlContent.value = `
<div>
  <h1>项目进度报告 - 2023年10月</h1>
  <p>报告日期: ${currentDate}</p>
  
  <h3>项目概览</h3>
  <p>项目目前进展顺利,整体完成度为 <strong>75%</strong>,预计将在11月底前完成。</p>
  
  <h3>任务完成情况</h3>
  <table class="table">
    <thead>
      <tr>
        <th>任务名称</th>
        <th>负责人</th>
        <th>状态</th>
        <th>完成日期</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>用户界面设计</td>
        <td>张设计师</td>
        <td>已完成</td>
        <td>2023-09-15</td>
      </tr>
      <tr>
        <td>后端API开发</td>
        <td>李工程师</td>
        <td>进行中 (90%)</td>
        <td>2023-10-20</td>
      </tr>
      <tr>
        <td>数据库优化</td>
        <td>王工程师</td>
        <td>进行中 (70%)</td>
        <td>2023-10-25</td>
      </tr>
      <tr>
        <td>用户测试</td>
        <td>测试团队</td>
        <td>未开始</td>
        <td>2023-11-05</td>
      </tr>
    </tbody>
  </table>
  
  <h3>下月重点</h3>
  <ul>
    <li>完成所有核心功能开发</li>
    <li>启动第一阶段用户测试</li>
    <li>准备项目交付文档</li>
  </ul>
  
  <div style="margin-top: 30px; text-align: center; color: #7f8c8d; font-size: 0.9rem;">
    <p>项目负责人: 王经理 | 下次报告日期: 2023-11-05</p>
  </div>
</div>
            `.trim();
          } else if (type === 'invoice') {
            htmlContent.value = `
<div style="max-width: 800px; margin: 0 auto;">
  <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px;">
    <div>
      <h1 style="color: #2c3e50;">发票</h1>
      <p>发票号: INV-2023-00142</p>
      <p>日期: ${currentDate}</p>
    </div>
    <div style="text-align: right;">
      <h2>科技有限公司</h2>
      <p>北京市海淀区科技园88号</p>
      <p>电话: (010) 1234-5678</p>
    </div>
  </div>
  
  <div style="display: flex; justify-content: space-between; margin-bottom: 30px; background: #f9f9f9; padding: 20px; border-radius: 8px;">
    <div>
      <h3>账单至:</h3>
      <p>客户公司名称</p>
      <p>联系人: 张先生</p>
      <p>上海市浦东新区金融街100号</p>
    </div>
    <div style="text-align: right;">
      <h3>付款信息</h3>
      <p>金额: <strong style="font-size: 1.2rem;">¥18,750.00</strong></p>
      <p>付款方式: 银行转账</p>
      <p>付款期限: 30天</p>
    </div>
  </div>
  
  <h3>收费项目</h3>
  <table class="table">
    <thead>
      <tr>
        <th>项目描述</th>
        <th>数量</th>
        <th>单价</th>
        <th>总价</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>企业版软件许可 (年)</td>
        <td>5</td>
        <td>¥2,500.00</td>
        <td>¥12,500.00</td>
      </tr>
      <tr>
        <td>技术咨询服务</td>
        <td>40小时</td>
        <td>¥500.00</td>
        <td>¥20,000.00</td>
      </tr>
      <tr>
        <td>折扣</td>
        <td>-</td>
        <td>-</td>
        <td>-¥13,750.00</td>
      </tr>
    </tbody>
    <tfoot>
      <tr style="font-weight: bold; background: #f1f2f6;">
        <td colspan="3" style="text-align: right;">总计:</td>
        <td>¥18,750.00</td>
      </tr>
    </tfoot>
  </table>
  
  <div style="margin-top: 40px; padding-top: 20px; border-top: 1px solid #eee; text-align: center; color: #7f8c8d;">
    <p>感谢您的惠顾!如有任何问题,请随时联系我们的财务部门。</p>
    <p>tech@company.com | (010) 1234-5678</p>
  </div>
</div>
            `.trim();
          }
          
          updatePreview();
          showStatus('success', '示例已加载');
        };
        
        // 重置内容
        const reset = () => {
          htmlContent.value = '';
          previewContent.value = '';
          pdfOptions.orientation = 'portrait';
          pdfOptions.format = 'a4';
          pdfOptions.margin = 15;
          pdfOptions.imageQuality = 0.9;
          pdfOptions.scale = 2;
          pdfOptions.filename = 'document.pdf';
          
          showStatus('success', '已重置所有设置');
        };
        
        // 显示状态消息
        const showStatus = (type, message) => {
          status.visible = true;
          status.type = type;
          status.message = message;
          status.loading = false;
          
          setTimeout(() => {
            status.visible = false;
          }, type === 'success' ? 4000 : 6000);
        };
        
        // 初始化
        onMounted(() => {
          updatePreview();
        });
        
        return {
          htmlContent,
          previewContent,
          pdfOptions,
          status,
          updatePreview,
          exportPDF,
          openPDF,
          loadSample,
          reset
        };
      }
    }).mount('#app');
  </script>
</body>
</html>
相关推荐
无·糖1 小时前
大学生HTML期末大作业——HTML+CSS+JavaScript人物明星(周杰伦)
javascript·css·html·课程设计·大学生·大作业·web网页设计作业
n***s9091 小时前
ThinkPHP和PHP的区别
开发语言·php
code bean1 小时前
【C++】全局函数和全局变量
开发语言·c++·c#
safestar20121 小时前
Elasticsearch ILM实战:从数据热恋到冷静归档的自动化管理
java·开发语言·jvm·elasticsearch·es
霸王大陆1 小时前
《零基础学 PHP:从入门到实战》教程-模块四:数组与函数-2
android·开发语言·php
神仙别闹1 小时前
基于C++实现(控制台)应用二维矩阵完成矩阵运算
开发语言·c++·矩阵
yi碗汤园1 小时前
C#实现对UI元素的拖拽
开发语言·ui·unity·c#
lqwh53541 小时前
python控制修改comsol边界条件仿真方法
开发语言·python
似水এ᭄往昔1 小时前
【C++】--二叉搜索树
开发语言·数据结构·c++