前端上传 pdf 文件 ,前端自己解析出来 生成界面 然后支持编辑

发布于:2025-07-17 ⋅ 阅读:(23) ⋅ 点赞:(0)

要在前端解析 PDF 文件并生成可编辑界面,我们可以使用 PDF.js 库来解析 PDF 内容,然后将其转换为可编辑的 HTML 元素。
主要特点和工作原理如下:

  1. PDF 解析:
    使用 Mozilla 的 PDF.js 库解析 PDF 文件内容,提取文本信息。这是前端处理 PDF 最常用的库之一,功能强大且稳定。
  2. 用户界面:
    简洁的上传区域,支持点击上传和拖拽上传
    解析进度显示,提升用户体验
    分页导航,方便浏览多页 PDF
    编辑 / 预览模式切换
  3. 编辑功能:
    将 PDF 文本转换为可编辑的 HTML 段落(使用 contenteditable 属性)
    支持基本的文本编辑操作
    保存编辑内容(前端暂存,实际应用中可发送到服务器)
  4. 实现流程:
    用户上传 PDF 文件
    使用 PDF.js 加载并解析 PDF
    将解析出的文本内容转换为可编辑的 HTML 元素
    提供编辑工具和导航功能
    支持保存更改和导出(导出功能在实际应用中需要额外库支持)

要注意的是,前端 PDF 编辑有一些局限性:

  • 复杂的 PDF 布局(如多列、表格)可能无法完美转换
  • 包含图片或复杂图形的 PDF 处理起来比较困难
  • 前端生成 PDF 需要额外的库(如 jsPDF)支持

界面
在这里插入图片描述
在这里插入图片描述
代码

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>PDF解析与编辑工具</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.4.120/pdf.min.js"></script>
    <script>
        tailwind.config = {
            theme: {
                extend: {
                    colors: {
                        primary: '#3B82F6',
                        secondary: '#10B981',
                        neutral: '#6B7280',
                        light: '#F3F4F6',
                        dark: '#1F2937'
                    },
                    fontFamily: {
                        sans: ['Inter', 'system-ui', 'sans-serif'],
                    },
                }
            }
        }
    </script>
    <style type="text/tailwindcss">
        @layer utilities {
            .content-auto {
                content-visibility: auto;
            }
            .transition-height {
                transition: max-height 0.3s ease-out;
            }
            .editable-content [contenteditable="true"]:focus {
                outline: 2px solid #3B82F6;
                border-radius: 2px;
                background-color: rgba(59, 130, 246, 0.05);
            }
        }
    </style>
</head>
<body class="bg-gray-50 font-sans">
    <!-- 顶部导航栏 -->
    <header class="bg-white shadow-sm sticky top-0 z-50">
        <div class="container mx-auto px-4 py-4 flex justify-between items-center">
            <div class="flex items-center space-x-2">
                <i class="fa fa-file-pdf-o text-red-500 text-2xl"></i>
                <h1 class="text-xl font-bold text-dark">PDF解析与编辑工具</h1>
            </div>
            <div class="flex space-x-3">
                <button id="saveBtn" class="bg-secondary hover:bg-green-600 text-white px-4 py-2 rounded-md flex items-center transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed" disabled>
                    <i class="fa fa-save mr-2"></i>保存
                </button>
                <button id="downloadBtn" class="bg-primary hover:bg-blue-600 text-white px-4 py-2 rounded-md flex items-center transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed" disabled>
                    <i class="fa fa-download mr-2"></i>导出PDF
                </button>
            </div>
        </div>
    </header>

    <main class="container mx-auto px-4 py-8">
        <!-- 文件上传区域 -->
        <section id="uploadSection" class="mb-8">
            <div class="bg-white rounded-lg shadow-md p-8 text-center">
                <label for="fileInput" class="cursor-pointer">
                    <div class="border-2 border-dashed border-neutral rounded-lg p-10 transition-colors duration-200 hover:border-primary">
                        <i class="fa fa-cloud-upload text-5xl text-primary mb-4"></i>
                        <h2 class="text-xl font-semibold mb-2">上传PDF文件</h2>
                        <p class="text-neutral mb-4">点击或拖拽文件到此处上传</p>
                        <p class="text-sm text-neutral">支持的格式: PDF</p>
                        <input id="fileInput" type="file" accept=".pdf" class="hidden">
                    </div>
                </label>
                <div id="fileInfo" class="mt-4 hidden">
                    <div class="flex items-center justify-center p-3 bg-light rounded-md">
                        <i class="fa fa-file-pdf-o text-red-500 mr-2"></i>
                        <span id="fileName" class="mr-2"></span>
                        <button id="removeFile" class="text-neutral hover:text-red-500 transition-colors">
                            <i class="fa fa-times"></i>
                        </button>
                    </div>
                </div>
            </div>
        </section>

        <!-- 解析进度 -->
        <section id="progressSection" class="mb-8 hidden">
            <div class="bg-white rounded-lg shadow-md p-6">
                <h2 class="text-lg font-semibold mb-4">正在解析PDF文件...</h2>
                <div class="w-full bg-gray-200 rounded-full h-2.5">
                    <div id="progressBar" class="bg-primary h-2.5 rounded-full" style="width: 0%"></div>
                </div>
                <p id="progressText" class="text-sm text-neutral mt-2">准备中...</p>
            </div>
        </section>

        <!-- 编辑区域 -->
        <section id="editorSection" class="hidden">
            <div class="bg-white rounded-lg shadow-md p-6 mb-6">
                <div class="flex justify-between items-center mb-6">
                    <h2 class="text-xl font-semibold">PDF内容编辑</h2>
                    <div class="flex space-x-2">
                        <button id="editModeBtn" class="bg-primary hover:bg-blue-600 text-white px-3 py-1 rounded text-sm transition-colors">
                            <i class="fa fa-pencil mr-1"></i>编辑模式
                        </button>
                        <button id="previewModeBtn" class="bg-neutral hover:bg-gray-600 text-white px-3 py-1 rounded text-sm transition-colors">
                            <i class="fa fa-eye mr-1"></i>预览模式
                        </button>
                    </div>
                </div>
                
                <div id="pdfEditor" class="editable-content min-h-[500px]">
                    <!-- PDF内容将在这里显示 -->
                </div>
            </div>
        </section>

        <!-- 页面导航 -->
        <section id="pageNavigation" class="flex justify-center mt-6 hidden">
            <div class="flex items-center space-x-4">
                <button id="prevPage" class="bg-white border border-neutral rounded-md px-3 py-1 hover:bg-light transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
                    <i class="fa fa-chevron-left"></i>
                </button>
                <div id="pageIndicator" class="text-neutral">1/0</div>
                <button id="nextPage" class="bg-white border border-neutral rounded-md px-3 py-1 hover:bg-light transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
                    <i class="fa fa-chevron-right"></i>
                </button>
            </div>
        </section>
    </main>

    <footer class="bg-dark text-white py-6 mt-12">
        <div class="container mx-auto px-4 text-center">
            <p>PDF解析与编辑工具 &copy; 2025716</p>
            <p class="text-sm text-gray-400 mt-1">使用PDF.js和Tailwind CSS构建</p>
        </div>
    </footer>

    <script>
        // 全局变量
        let pdfDoc = null;
        let currentPage = 1;
        let totalPages = 0;
        let isEditMode = true;
        let pdfData = null;
        
        // DOM元素
        const fileInput = document.getElementById('fileInput');
        const fileInfo = document.getElementById('fileInfo');
        const fileName = document.getElementById('fileName');
        const removeFile = document.getElementById('removeFile');
        const uploadSection = document.getElementById('uploadSection');
        const progressSection = document.getElementById('progressSection');
        const progressBar = document.getElementById('progressBar');
        const progressText = document.getElementById('progressText');
        const editorSection = document.getElementById('editorSection');
        const pdfEditor = document.getElementById('pdfEditor');
        const pageNavigation = document.getElementById('pageNavigation');
        const pageIndicator = document.getElementById('pageIndicator');
        const prevPageBtn = document.getElementById('prevPage');
        const nextPageBtn = document.getElementById('nextPage');
        const editModeBtn = document.getElementById('editModeBtn');
        const previewModeBtn = document.getElementById('previewModeBtn');
        const saveBtn = document.getElementById('saveBtn');
        const downloadBtn = document.getElementById('downloadBtn');
        
        // 初始化PDF.js
        const pdfjsLib = window['pdfjs-dist/build/pdf'];
        pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.4.120/pdf.worker.min.js';
        
        // 事件监听
        fileInput.addEventListener('change', handleFileUpload);
        removeFile.addEventListener('click', removeSelectedFile);
        prevPageBtn.addEventListener('click', goToPreviousPage);
        nextPageBtn.addEventListener('click', goToNextPage);
        editModeBtn.addEventListener('click', enableEditMode);
        previewModeBtn.addEventListener('click', enablePreviewMode);
        saveBtn.addEventListener('click', saveChanges);
        downloadBtn.addEventListener('click', downloadAsPDF);
        
        // 处理文件上传
        function handleFileUpload(event) {
            const file = event.target.files[0];
            if (!file) return;
            
            // 显示文件信息
            fileName.textContent = file.name;
            fileInfo.classList.remove('hidden');
            uploadSection.classList.add('opacity-50');
            
            // 准备解析
            const fileReader = new FileReader();
            fileReader.onload = function() {
                pdfData = new Uint8Array(this.result);
                loadPDF(pdfData);
            };
            fileReader.readAsArrayBuffer(file);
        }
        
        // 移除选中的文件
        function removeSelectedFile() {
            fileInput.value = '';
            fileInfo.classList.add('hidden');
            uploadSection.classList.remove('opacity-50');
            resetPDFState();
        }
        
        // 重置PDF状态
        function resetPDFState() {
            pdfDoc = null;
            currentPage = 1;
            totalPages = 0;
            pdfData = null;
            progressSection.classList.add('hidden');
            editorSection.classList.add('hidden');
            pageNavigation.classList.add('hidden');
            saveBtn.disabled = true;
            downloadBtn.disabled = true;
        }
        
        // 加载PDF文件
        function loadPDF(data) {
            progressSection.classList.remove('hidden');
            progressBar.style.width = '0%';
            progressText.textContent = '正在加载PDF...';
            
            pdfjsLib.getDocument(data).promise.then(function(pdf) {
                pdfDoc = pdf;
                totalPages = pdf.numPages;
                
                progressBar.style.width = '30%';
                progressText.textContent = '解析PDF内容...';
                
                updatePageIndicator();
                renderPage(currentPage);
                
                // 显示编辑区域和导航
                editorSection.classList.remove('hidden');
                pageNavigation.classList.remove('hidden');
                saveBtn.disabled = false;
                downloadBtn.disabled = false;
            }).catch(function(error) {
                console.error('加载PDF时出错:', error);
                progressText.textContent = `加载失败: ${error.message}`;
            });
        }
        
        // 渲染指定页面
        function renderPage(pageNum) {
            if (!pdfDoc) return;
            
            pdfDoc.getPage(pageNum).then(function(page) {
                // 获取页面内容
                return page.getTextContent().then(function(textContent) {
                    // 更新进度
                    const progress = 30 + Math.round((pageNum / totalPages) * 70);
                    progressBar.style.width = `${progress}%`;
                    progressText.textContent = `正在处理第 ${pageNum} 页 / 共 ${totalPages}`;
                    
                    // 清空编辑器
                    pdfEditor.innerHTML = '';
                    
                    // 创建页面容器
                    const pageContainer = document.createElement('div');
                    pageContainer.className = 'pdf-page p-8 border border-gray-200 rounded-lg shadow-sm';
                    pageContainer.dataset.page = pageNum;
                    
                    // 处理文本内容
                    let lastY = null;
                    let paragraph = document.createElement('p');
                    paragraph.className = 'mb-4 leading-relaxed';
                    paragraph.contentEditable = isEditMode;
                    
                    textContent.items.forEach(function(item) {
                        // 当Y坐标变化较大时,创建新段落
                        if (lastY !== null && Math.abs(item.transform[5] - lastY) > 15) {
                            pageContainer.appendChild(paragraph);
                            paragraph = document.createElement('p');
                            paragraph.className = 'mb-4 leading-relaxed';
                            paragraph.contentEditable = isEditMode;
                        }
                        
                        const span = document.createElement('span');
                        span.textContent = item.str;
                        paragraph.appendChild(span);
                        
                        lastY = item.transform[5];
                    });
                    
                    // 添加最后一个段落
                    if (paragraph.children.length > 0) {
                        pageContainer.appendChild(paragraph);
                    }
                    
                    // 如果页面没有文本内容
                    if (textContent.items.length === 0) {
                        const emptyMsg = document.createElement('p');
                        emptyMsg.className = 'text-neutral italic text-center py-8';
                        emptyMsg.textContent = '此页面没有可编辑的文本内容。可能包含图像或其他非文本元素。';
                        pageContainer.appendChild(emptyMsg);
                    }
                    
                    // 添加到编辑器
                    pdfEditor.appendChild(pageContainer);
                    
                    // 更新按钮状态
                    updateNavigationButtons();
                    
                    // 如果是最后一页,隐藏进度
                    if (pageNum === totalPages) {
                        setTimeout(() => {
                            progressSection.classList.add('hidden');
                        }, 500);
                    }
                });
            }).catch(function(error) {
                console.error('渲染页面时出错:', error);
                pdfEditor.innerHTML = `<p class="text-red-500">渲染页面时出错: ${error.message}</p>`;
            });
        }
        
        // 更新页码指示器
        function updatePageIndicator() {
            pageIndicator.textContent = `${currentPage} 页 / 共 ${totalPages}`;
        }
        
        // 更新导航按钮状态
        function updateNavigationButtons() {
            prevPageBtn.disabled = currentPage <= 1;
            nextPageBtn.disabled = currentPage >= totalPages;
        }
        
        // 上一页
        function goToPreviousPage() {
            if (currentPage > 1) {
                currentPage--;
                renderPage(currentPage);
                updatePageIndicator();
            }
        }
        
        // 下一页
        function goToNextPage() {
            if (currentPage < totalPages) {
                currentPage++;
                renderPage(currentPage);
                updatePageIndicator();
            }
        }
        
        // 启用编辑模式
        function enableEditMode() {
            isEditMode = true;
            editModeBtn.classList.remove('bg-neutral', 'hover:bg-gray-600');
            editModeBtn.classList.add('bg-primary', 'hover:bg-blue-600');
            previewModeBtn.classList.remove('bg-primary', 'hover:bg-blue-600');
            previewModeBtn.classList.add('bg-neutral', 'hover:bg-gray-600');
            
            // 使所有段落可编辑
            document.querySelectorAll('#pdfEditor [contenteditable]').forEach(el => {
                el.contentEditable = true;
            });
        }
        
        // 启用预览模式
        function enablePreviewMode() {
            isEditMode = false;
            previewModeBtn.classList.remove('bg-neutral', 'hover:bg-gray-600');
            previewModeBtn.classList.add('bg-primary', 'hover:bg-blue-600');
            editModeBtn.classList.remove('bg-primary', 'hover:bg-blue-600');
            editModeBtn.classList.add('bg-neutral', 'hover:bg-gray-600');
            
            // 使所有段落不可编辑
            document.querySelectorAll('#pdfEditor [contenteditable]').forEach(el => {
                el.contentEditable = false;
            });
        }
        
        // 保存更改(在实际应用中,这里会将数据发送到服务器)
        function saveChanges() {
            // 获取所有页面的内容
            const pagesContent = [];
            document.querySelectorAll('.pdf-page').forEach(pageEl => {
                const pageNum = parseInt(pageEl.dataset.page);
                const textContent = pageEl.innerText;
                pagesContent.push({
                    page: pageNum,
                    content: textContent
                });
            });
            
            // 显示保存成功提示
            const originalText = saveBtn.innerHTML;
            saveBtn.innerHTML = '<i class="fa fa-check mr-2"></i>已保存';
            saveBtn.classList.remove('bg-secondary');
            saveBtn.classList.add('bg-green-600');
            
            setTimeout(() => {
                saveBtn.innerHTML = originalText;
                saveBtn.classList.remove('bg-green-600');
                saveBtn.classList.add('bg-secondary');
            }, 2000);
            
            // 在实际应用中,这里会发送数据到服务器
            console.log('保存的PDF内容:', pagesContent);
        }
        
        // 下载为PDF(实际应用中需要后端支持或使用jsPDF等库)
        function downloadAsPDF() {
            // 显示加载状态
            const originalText = downloadBtn.innerHTML;
            downloadBtn.innerHTML = '<i class="fa fa-spinner fa-spin mr-2"></i>处理中...';
            downloadBtn.disabled = true;
            
            // 模拟PDF生成过程
            setTimeout(() => {
                // 这里仅做演示,实际应用中需要使用专门的库如jsPDF或调用后端API
                alert('PDF导出功能在实际应用中需要额外的库或后端支持。');
                
                // 恢复按钮状态
                downloadBtn.innerHTML = originalText;
                downloadBtn.disabled = false;
            }, 1500);
        }
        
        // 支持拖拽上传
        const dropArea = document.querySelector('#uploadSection .border-dashed');
        
        ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
            dropArea.addEventListener(eventName, preventDefaults, false);
        });
        
        function preventDefaults(e) {
            e.preventDefault();
            e.stopPropagation();
        }
        
        ['dragenter', 'dragover'].forEach(eventName => {
            dropArea.addEventListener(eventName, highlight, false);
        });
        
        ['dragleave', 'drop'].forEach(eventName => {
            dropArea.addEventListener(eventName, unhighlight, false);
        });
        
        function highlight() {
            dropArea.classList.add('border-primary', 'bg-blue-50');
        }
        
        function unhighlight() {
            dropArea.classList.remove('border-primary', 'bg-blue-50');
        }
        
        dropArea.addEventListener('drop', handleDrop, false);
        
        function handleDrop(e) {
            const dt = e.dataTransfer;
            const file = dt.files[0];
            
            if (file && file.type === 'application/pdf') {
                // 将文件设置到fileInput
                const dataTransfer = new DataTransfer();
                dataTransfer.items.add(file);
                fileInput.files = dataTransfer.files;
                
                // 触发change事件
                const event = new Event('change', { bubbles: true });
                fileInput.dispatchEvent(event);
            } else {
                alert('请上传PDF格式的文件');
            }
        }
    </script>
</body>
</html>


网站公告

今日签到

点亮在社区的每一天
去签到