1. 环境配置与依赖安装
1.1 安装依赖
javascript
# 安装 PDF.js 库(推荐稳定版)
npm install pdfjs-dist@2.16.105 @types/pdfjs-dist@2.16.105
# 或使用最新版(需自定义类型声明)
npm install pdfjs-dist@3.4.120
1.2 配置 Worker 路径
在组件或服务中初始化 PDF.js:
javascript
import * as pdfjsLib from 'pdfjs-dist';
// 方案1:使用 CDN(需联网)
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.16.105/pdf.worker.min.js';
// 方案2:使用本地文件(推荐,更可靠)
// 1. 下载 worker 文件到 `assets/` 目录
// 2. 配置路径
pdfjsLib.GlobalWorkerOptions.workerSrc = 'assets/pdf.worker.min.js';
1.3 代码变化
1.3.1 angular.jsin

1.3.2 package.json

1.3.3 package-lock.json

2. 核心功能实现
2.1 HTML
html
<div class="app-container">
<!-- 顶部导航栏 -->
<header class="app-header">
<div class="header-container">
<div class="logo-container">
<i class="fa fa-file-pdf-o header-icon"></i>
<h1 class="app-title">PDF解析与编辑工具</h1>
</div>
<div class="header-buttons">
<button #saveBtn class="btn btn-save" disabled>
<i class="fa fa-save button-icon"></i>保存
</button>
<button #downloadBtn class="btn btn-download" disabled>
<i class="fa fa-download button-icon"></i>导出PDF
</button>
</div>
</div>
</header>
<div class="main-content">
<!-- 文件上传区域 -->
<section #uploadSection class="upload-section">
<div class="upload-card">
<label for="fileInput" class="file-upload-label">
<div class="upload-area"
(dragover)="onDragOver($event)"
(dragleave)="onDragLeave($event)"
(drop)="onFileDrop($event)"
[class.upload-area-drag-over]="isDragOver">
<i class="fa fa-cloud-upload upload-icon"></i>
<h2 class="upload-title">上传PDF文件</h2>
<p class="upload-text">点击或拖拽文件到此处上传</p>
<p class="upload-hint">支持的格式: PDF</p>
<input #fileInput id="fileInput" type="file" accept=".pdf" class="file-input" (change)="onFileSelected($event)">
</div>
</label>
<div #fileInfo class="file-info" [class.hidden]="!selectedFile">
<div class="file-info-card">
<i class="fa fa-file-pdf-o file-icon"></i>
<span #fileName class="file-name">{{ selectedFile?.name }}</span>
<button (click)="removeSelectedFile()" class="file-remove-btn">
<i class="fa fa-times"></i>
</button>
</div>
</div>
</div>
</section>
<!-- 解析进度 -->
<section #progressSection class="progress-section" [class.hidden]="!isProcessing">
<div class="progress-card">
<h2 class="progress-title">正在解析PDF文件...</h2>
<div class="progress-bar">
<div #progressBar class="progress-value" [style.width.%]="progressValue"></div>
</div>
<p #progressText class="progress-text">{{ progressMessage }}</p>
</div>
</section>
<!-- 编辑区域 -->
<section #editorSection class="editor-section" [class.hidden]="!isEditorVisible">
<div class="editor-panel">
<div class="editor-header">
<h2 class="editor-title">PDF内容编辑</h2>
<div class="editor-actions">
<button #editModeBtn class="btn btn-edit" (click)="enableEditMode()">
<i class="fa fa-pencil button-icon"></i>编辑模式
</button>
<button #previewModeBtn class="btn btn-preview" (click)="enablePreviewMode()">
<i class="fa fa-eye button-icon"></i>预览模式
</button>
</div>
</div>
<div #pdfEditor class="editor-content">
<!-- PDF内容将在这里渲染 -->
</div>
</div>
</section>
<!-- 页面导航 -->
<div #pageNavigation class="page-navigation" [class.hidden]="!isNavigationVisible">
<div class="navigation-container">
<button #prevPage class="nav-button" (click)="goToPreviousPage()" [disabled]="currentPage <= 1">
<i class="fa fa-chevron-left"></i>
</button>
<div #pageIndicator class="page-indicator">第 {{ currentPage }} 页 / 共 {{ totalPages }} 页</div>
<button #nextPage class="nav-button" (click)="goToNextPage()" [disabled]="currentPage >= totalPages">
<i class="fa fa-chevron-right"></i>
</button>
</div>
</div>
</div>
<footer class="app-footer">
<div class="footer-container">
<p>PDF解析与编辑工具 © {{ currentYear }}</p>
</div>
</footer>
</div>
2.2 SCSS
css
/* 基础变量 */
$primary-color: #3b82f6;
$secondary-color: #10b981;
$neutral-color: #737373;
$light-color: #f5f5f5;
$dark-color: #1a202c;
$danger-color: #ef4444;
/* 全局样式 */
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #f9fafb;
margin: 0;
padding: 0;
}
.app-container {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.hidden {
display: none !important;
}
/* 顶部导航 */
.app-header {
background-color: white;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
position: sticky;
top: 0;
z-index: 50;
padding: 1rem 0;
.header-container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo-container {
display: flex;
align-items: center;
gap: 0.5rem;
}
.header-icon {
color: $danger-color;
font-size: 1.5rem;
}
.app-title {
font-size: 1.25rem;
font-weight: bold;
color: $dark-color;
margin: 0;
}
.header-buttons {
display: flex;
gap: 0.75rem;
}
}
/* 主内容区 */
.main-content {
flex: 1;
max-width: 1200px;
width: 100%;
margin: 0 auto;
padding: 2rem 1rem;
}
/* 上传区域 */
.upload-section {
margin-bottom: 2rem;
.upload-card {
background-color: white;
border-radius: 0.5rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
padding: 2rem;
text-align: center;
}
.file-upload-label {
cursor: pointer;
}
.upload-area {
border: 2px dashed #d4d4d4;
border-radius: 0.5rem;
padding: 2.5rem;
transition: all 0.2s ease;
&:hover {
border-color: $primary-color;
}
&-drag-over {
border-color: $primary-color;
background-color: rgba(59, 130, 246, 0.05);
}
}
.upload-icon {
font-size: 3rem;
color: $primary-color;
margin-bottom: 1rem;
}
.upload-title {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 0.5rem;
color: $dark-color;
}
.upload-text, .upload-hint {
color: $neutral-color;
margin-bottom: 1rem;
}
.upload-hint {
font-size: 0.875rem;
}
.file-input {
display: none;
}
.file-info {
margin-top: 1rem;
}
.file-info-card {
display: flex;
align-items: center;
justify-content: center;
padding: 0.75rem;
background-color: $light-color;
border-radius: 0.375rem;
}
.file-icon {
color: $danger-color;
margin-right: 0.5rem;
}
.file-name {
margin-right: 0.5rem;
color: $dark-color;
}
.file-remove-btn {
color: $neutral-color;
background: none;
border: none;
cursor: pointer;
transition: color 0.2s;
&:hover {
color: $danger-color;
}
}
}
/* 进度条 */
.progress-section {
margin-bottom: 2rem;
.progress-card {
background-color: white;
border-radius: 0.5rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
padding: 1.5rem;
}
.progress-title {
font-size: 1.125rem;
font-weight: 600;
margin-bottom: 1rem;
color: $dark-color;
}
.progress-bar {
width: 100%;
height: 0.5rem;
background-color: #e5e7eb;
border-radius: 0.25rem;
overflow: hidden;
}
.progress-value {
height: 100%;
background-color: $primary-color;
transition: width 0.3s ease;
}
.progress-text {
font-size: 0.875rem;
color: $neutral-color;
margin-top: 0.5rem;
}
}
/* 编辑区域 */
.editor-section {
margin-bottom: 2rem;
.editor-panel {
background-color: white;
border-radius: 0.5rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
padding: 1.5rem;
}
.editor-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.editor-title {
font-size: 1.25rem;
font-weight: 600;
color: $dark-color;
}
.editor-actions {
display: flex;
gap: 0.5rem;
}
.editor-content {
min-height: 500px;
border: 1px solid #e5e7eb;
border-radius: 0.5rem;
padding: 1rem;
.pdf-page {
padding: 1.5rem;
border: 1px solid #e5e7eb;
border-radius: 0.375rem;
margin-bottom: 1rem;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
p {
margin-bottom: 1rem;
line-height: 1.6;
}
}
}
}
/* 页面导航 */
.page-navigation {
margin-top: 1.5rem;
.navigation-container {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
}
.nav-button {
background-color: white;
border: 1px solid #d4d4d4;
border-radius: 0.375rem;
padding: 0.25rem 0.75rem;
cursor: pointer;
transition: background-color 0.2s;
&:hover:not(:disabled) {
background-color: $light-color;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
.page-indicator {
color: $neutral-color;
}
}
/* 按钮通用样式 */
.btn {
display: flex;
align-items: center;
padding: 0.5rem 1rem;
border-radius: 0.375rem;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
border: none;
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.button-icon {
margin-right: 0.5rem;
}
&-save {
background-color: $secondary-color;
color: white;
&:hover:not(:disabled) {
background-color: darken($secondary-color, 10%);
}
}
&-download {
background-color: $primary-color;
color: white;
&:hover:not(:disabled) {
background-color: darken($primary-color, 10%);
}
}
&-edit {
background-color: $primary-color;
color: white;
}
&-preview {
background-color: $neutral-color;
color: white;
}
}
/* 页脚 */
.app-footer {
background-color: $dark-color;
color: white;
padding: 1.5rem 0;
margin-top: 3rem;
.footer-container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
text-align: center;
}
}
2.3 ts
javascript
import { Component, ElementRef, ViewChild, AfterViewInit, OnDestroy } from '@angular/core';
import * as pdfjsLib from 'pdfjs-dist';
@Component({
selector: 'app-pdf-editor',
templateUrl: './pdf-editor.component.html',
styleUrls: ['./pdf-editor.component.scss']
})
export class PdfEditorComponent implements AfterViewInit, OnDestroy {
// 视图引用
@ViewChild('fileInput') fileInput!: ElementRef<HTMLInputElement>;
@ViewChild('fileInfo') fileInfo!: ElementRef<HTMLDivElement>;
@ViewChild('progressBar') progressBar!: ElementRef<HTMLDivElement>;
@ViewChild('progressText') progressText!: ElementRef<HTMLParagraphElement>;
@ViewChild('editorSection') editorSection!: ElementRef<HTMLElement>;
@ViewChild('pdfEditor') pdfEditor!: ElementRef<HTMLDivElement>;
@ViewChild('pageNavigation') pageNavigation!: ElementRef<HTMLElement>;
@ViewChild('editModeBtn') editModeBtn!: ElementRef<HTMLButtonElement>;
@ViewChild('previewModeBtn') previewModeBtn!: ElementRef<HTMLButtonElement>;
@ViewChild('saveBtn') saveBtn!: ElementRef<HTMLButtonElement>;
@ViewChild('downloadBtn') downloadBtn!: ElementRef<HTMLButtonElement>;
@ViewChild('prevPage') prevPage!: ElementRef<HTMLButtonElement>;
@ViewChild('nextPage') nextPage!: ElementRef<HTMLButtonElement>;
// PDF相关状态
pdfDoc: any = null;
currentPage = 1;
totalPages = 0;
isEditMode = true;
pdfData: Uint8Array | null = null;
currentYear = new Date().getFullYear();
isDragOver = false;
progressValue = 0;
progressMessage = '准备中...';
selectedFile: File | null = null;
// 状态标志
isProcessing = false;
isEditorVisible = false;
isNavigationVisible = false;
ngAfterViewInit() {
this.initPdfJs();
}
ngOnDestroy() {}
private initPdfJs() {
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.4.120/pdf.worker.min.js';
}
// 文件选择处理
onFileSelected(event: Event) {
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
if (!file) return;
this.handleFileSelection(file);
}
// 文件拖放处理
onDragOver(event: DragEvent) {
event.preventDefault();
this.isDragOver = true;
}
onDragLeave(event: DragEvent) {
event.preventDefault();
this.isDragOver = false;
}
onFileDrop(event: DragEvent) {
event.preventDefault();
this.isDragOver = false;
const files = event.dataTransfer?.files;
if (files && files.length > 0) {
const file = files[0];
if (file.type === 'application/pdf') {
// 创建新的FileList对象
const dataTransfer = new DataTransfer();
dataTransfer.items.add(file);
this.fileInput.nativeElement.files = dataTransfer.files;
// 触发change事件
const changeEvent = new Event('change');
this.fileInput.nativeElement.dispatchEvent(changeEvent);
} else {
alert('请上传PDF格式的文件');
}
}
}
private arrayBufferToBase64(buffer: ArrayBuffer): string {
let binary = '';
const bytes = new Uint8Array(buffer);
const len = bytes.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
}
private handleFileSelection(file: File) {
this.selectedFile = file;
this.isProcessing = true;
this.progressValue = 0;
this.progressMessage = '正在加载PDF...';
const fileReader = new FileReader();
fileReader.onload = () => {
this.pdfData = new Uint8Array(fileReader.result as ArrayBuffer);
// 转换为 base64
const pdfBase64 = this.arrayBufferToBase64(this.pdfData.buffer);
console.log('PDF base64:', pdfBase64);
const dataUrl = this.getDataUrlFromArrayBuffer(pdfBase64);
console.log('dataUrl:', dataUrl);
this.loadPDF(this.pdfData);
};
fileReader.readAsArrayBuffer(file);
}
getDataUrlFromArrayBuffer(buffer): Promise<string> {
return new Promise((resolve) => {
const blob = new Blob([buffer], { type: 'application/pdf' });
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.readAsDataURL(blob);
});
}
removeSelectedFile() {
this.selectedFile = null;
this.fileInput.nativeElement.value = '';
this.resetPDFState();
}
resetPDFState() {
this.pdfDoc = null;
this.currentPage = 1;
this.totalPages = 0;
this.pdfData = null;
this.isProcessing = false;
this.isEditorVisible = false;
this.isNavigationVisible = false;
if (this.saveBtn) this.saveBtn.nativeElement.disabled = true;
if (this.downloadBtn) this.downloadBtn.nativeElement.disabled = true;
}
loadPDF(data: Uint8Array) {
this.progressValue = 10;
this.progressMessage = '正在加载PDF...';
console.log(data);
pdfjsLib.getDocument(data).promise.then((pdf: any) => {
this.pdfDoc = pdf;
this.totalPages = pdf.numPages;
this.progressValue = 30;
this.progressMessage = '解析PDF内容...';
this.renderPage(this.currentPage);
this.isEditorVisible = true;
this.isNavigationVisible = true;
if (this.saveBtn) this.saveBtn.nativeElement.disabled = false;
if (this.downloadBtn) this.downloadBtn.nativeElement.disabled = false;
}).catch((error: any) => {
console.error('加载PDF时出错:', error);
this.progressMessage = `加载失败: ${error.message}`;
});
}
renderPage(pageNum: number) {
if (!this.pdfDoc) return;
this.pdfDoc.getPage(pageNum).then((page: any) => {
return page.getTextContent().then((textContent: any) => {
// 更新进度
const progress = 30 + Math.round((pageNum / this.totalPages) * 70);
this.progressValue = progress;
this.progressMessage = `正在处理第 ${pageNum} 页 / 共 ${this.totalPages} 页`;
// 清空编辑器内容
this.pdfEditor.nativeElement.innerHTML = '';
const pageContainer = document.createElement('div');
pageContainer.className = 'pdf-page';
pageContainer.dataset.page = pageNum.toString();
let lastY: number | null = null;
let paragraph = document.createElement('p');
paragraph.contentEditable = this.isEditMode.toString();
textContent.items.forEach((item: any) => {
if (lastY !== null && Math.abs(item.transform[5] - lastY) > 15) {
pageContainer.appendChild(paragraph);
paragraph = document.createElement('p');
paragraph.contentEditable = this.isEditMode.toString();
}
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);
}
this.pdfEditor.nativeElement.appendChild(pageContainer);
// 如果是最后一页,完成处理
if (pageNum === this.totalPages) {
setTimeout(() => {
this.isProcessing = false;
}, 500);
}
});
}).catch((error: any) => {
console.error('渲染页面时出错:', error);
this.pdfEditor.nativeElement.innerHTML = `<p class="error">渲染页面时出错: ${error.message}</p>`;
});
}
goToPreviousPage() {
if (this.currentPage > 1) {
this.currentPage--;
this.renderPage(this.currentPage);
}
}
goToNextPage() {
if (this.currentPage < this.totalPages) {
this.currentPage++;
this.renderPage(this.currentPage);
}
}
enableEditMode() {
this.isEditMode = true;
// 更新按钮样式
if (this.editModeBtn) {
this.editModeBtn.nativeElement.classList.add('btn-edit');
this.editModeBtn.nativeElement.classList.remove('btn-preview');
}
if (this.previewModeBtn) {
this.previewModeBtn.nativeElement.classList.add('btn-preview');
this.previewModeBtn.nativeElement.classList.remove('btn-edit');
}
// 启用内容编辑
const editableElements = this.pdfEditor.nativeElement.querySelectorAll('[contenteditable]');
editableElements.forEach((el: any) => {
el.contentEditable = 'true';
});
}
enablePreviewMode() {
this.isEditMode = false;
// 更新按钮样式
if (this.previewModeBtn) {
this.previewModeBtn.nativeElement.classList.add('btn-edit');
this.previewModeBtn.nativeElement.classList.remove('btn-preview');
}
if (this.editModeBtn) {
this.editModeBtn.nativeElement.classList.add('btn-preview');
this.editModeBtn.nativeElement.classList.remove('btn-edit');
}
// 禁用内容编辑
const editableElements = this.pdfEditor.nativeElement.querySelectorAll('[contenteditable]');
editableElements.forEach((el: any) => {
el.contentEditable = 'false';
});
}
saveChanges() {
if (!this.saveBtn) return;
const originalText = this.saveBtn.nativeElement.innerHTML;
this.saveBtn.nativeElement.innerHTML = '<i class="fa fa-check button-icon"></i>已保存';
this.saveBtn.nativeElement.classList.add('saved');
// 收集所有页面内容
const pagesContent: Array<{ page: number, content: string }> = [];
const pageElements = this.pdfEditor.nativeElement.querySelectorAll('.pdf-page');
pageElements.forEach((pageEl: any) => {
const pageNum = parseInt(pageEl.dataset.page);
const textContent = pageEl.innerText;
pagesContent.push({
page: pageNum,
content: textContent
});
});
// 实际应用中,这里可以调用服务将数据发送到后端
console.log('保存的PDF内容:', pagesContent);
// 重置按钮状态
setTimeout(() => {
if (this.saveBtn) {
this.saveBtn.nativeElement.innerHTML = originalText;
this.saveBtn.nativeElement.classList.remove('saved');
}
}, 2000);
}
downloadAsPDF() {
if (!this.downloadBtn) return;
const originalText = this.downloadBtn.nativeElement.innerHTML;
this.downloadBtn.nativeElement.innerHTML = '<i class="fa fa-spinner fa-spin button-icon"></i>处理中...';
this.downloadBtn.nativeElement.disabled = true;
// 模拟导出处理
setTimeout(() => {
alert('PDF导出功能在实际应用中需要额外的库或后端支持。');
if (this.downloadBtn) {
this.downloadBtn.nativeElement.innerHTML = originalText;
this.downloadBtn.nativeElement.disabled = false;
}
}, 1500);
}
}
3 效果截图
3.1 总览

3.2 可修改

3.3可保存传后台
该部分功能没有写通。