-
使用Element UI和PDF.js实现无服务器上传的PDF预览功能
image.png
image.png
image.png
使用的组件
- npm install element-ui --save
- vue
如果不自定义,就不用 npm install --save pdfjs-dist
用viewer.html 下载压缩包,在public下解压,页面中使用就可以了
<template>
<div id="app">
<div class="container">
<header>
<h1>PDF文件本地预览解决方案</h1>
<p>使用Element UI和PDF.js实现无服务器上传的PDF预览功能</p>
</header>
<div class="preview-container">
<div class="card">
<h2>PDF文件上传</h2>
<el-upload
class="upload-area"
drag
action="#"
:auto-upload="false"
:on-change="handleFileChange"
:show-file-list="false"
accept="application/pdf"
>
<i class="el-icon-upload upload-icon"></i>
<div class="el-upload__text">
将PDF文件拖到此处,或<em>点击上传</em>
</div>
<div class="el-upload__tip" slot="tip">
仅支持PDF文件,且不超过10MB
</div>
</el-upload>
<div v-if="file" class="status-bar">
<div class="file-info">
<i class="el-icon-document file-icon"></i>
<span>{{ file.name }} ({{ formatFileSize(file.size) }})</span>
</div>
<el-button type="success" plain @click="previewPDF"
>预览文件</el-button
>
</div>
</div>
<div class="card">
<h2>PDF预览</h2>
<div v-if="previewUrl" class="preview-frame">
<iframe
:src="previewUrl"
width="100%"
height="100%"
frameborder="0"
></iframe>
</div>
<div v-else class="preview-frame">
<div
style="
display: flex;
justify-content: center;
align-items: center;
height: 100%;
"
>
<div style="text-align: center">
<i
class="el-icon-document"
style="font-size: 60px; color: #dcdfe6; margin-bottom: 20px"
></i>
<p style="color: #909399; font-size: 18px">等待上传PDF文件</p>
</div>
</div>
</div>
</div>
</div>
<!--
<div class="instructions">
<h3>技术实现说明</h3>
<ul>
<li>使用Element UI的Upload组件实现文件选择功能</li>
<li>通过URL.createObjectURL生成本地Blob URL实现预览</li>
<li>采用PDF.js作为PDF渲染引擎,无需服务器支持</li>
<li>完全本地处理,避免网络请求和代理错误</li>
<li>支持文件验证(类型、大小)和错误处理</li>
</ul>
</div> -->
<!-- <div class="features">
<div class="feature-card">
<i class="el-icon-lock"></i>
<h3>安全可靠</h3>
<p>文件仅在浏览器中处理,不会上传到任何服务器</p>
</div>
<div class="feature-card">
<i class="el-icon-thunderbolt"></i>
<h3>快速预览</h3>
<p>无需等待上传过程,即时预览PDF文件</p>
</div>
<div class="feature-card">
<i class="el-icon-cpu"></i>
<h3>零服务器依赖</h3>
<p>完全在浏览器端实现,无需后端支持</p>
</div>
</div> -->
<footer>
<p>PDF本地预览解决方案 | 基于Vue.js和Element UI | 无服务器上传</p>
</footer>
</div>
</div>
</template>
<script>
export default {
components: {
},
data() {
return {
file: null,
previewUrl: null,
previousBlobUrl: null,
};
},
methods: {
handleFileChange(file) {
// 验证文件类型
if (file.raw.type !== "application/pdf") {
this.$message.error("请上传PDF格式的文件");
return;
}
// 验证文件大小 (10MB)
if (file.raw.size > 20 * 1024 * 1024) {
this.$message.error("文件大小不能超过10MB");
return;
}
// 清理之前的文件引用
this.revokePreviousBlob();
this.file = file.raw;
this.$message.success("文件已选择,点击预览按钮查看");
},
previewPDF() {
if (!this.file) {
this.$message.warning("请先选择PDF文件");
return;
}
this.revokePreviousBlob();
try {
// 生成Blob URL
const blobUrl = URL.createObjectURL(this.file);
this.previousBlobUrl = blobUrl;
// 创建PDF.js预览URL
const viewerPath = "/pdfjs2/web/viewer.html";
const encodedUrl = encodeURIComponent(blobUrl);
this.previewUrl = `${viewerPath}?file=${encodedUrl}`;
this.$message.success("PDF预览已加载");
} catch (error) {
console.error("预览失败:", error);
this.$message.error("预览失败: " + error.message);
}
},
revokePreviousBlob() {
if (this.previousBlobUrl) {
URL.revokeObjectURL(this.previousBlobUrl);
this.previousBlobUrl = null;
}
},
formatFileSize(bytes) {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
},
},
beforeDestroy() {
this.revokePreviousBlob();
},
};
</script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: "Helvetica Neue", Arial, sans-serif;
}
body {
background: linear-gradient(135deg, #f5f7fa 0%, #e4edf5 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 30px;
background-color: white;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
}
header {
text-align: center;
margin-bottom: 40px;
}
header h1 {
color: #2c3e50;
font-size: 2.5rem;
margin-bottom: 10px;
}
header p {
color: #7f8c8d;
font-size: 1.1rem;
}
.card {
background: white;
border-radius: 12px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08);
padding: 25px;
margin-bottom: 30px;
transition: transform 0.3s ease;
}
.card:hover {
transform: translateY(-5px);
}
.card h2 {
color: #3498db;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #f0f5ff;
}
.preview-container {
display: flex;
flex-direction: column;
gap: 30px;
}
.upload-area {
border: 2px dashed #dcdfe6;
border-radius: 10px;
padding: 40px 20px;
text-align: center;
background-color: #fafafa;
cursor: pointer;
transition: all 0.3s;
}
.upload-area:hover {
border-color: #409eff;
background-color: #f0f7ff;
}
.upload-icon {
font-size: 50px;
color: #409eff;
margin-bottom: 15px;
}
.preview-frame {
height: 70vh;
width: 100%;
border: 1px solid #ebeef5;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
background-color: #f8f9fa;
}
.instructions {
background-color: #f8f9fa;
border-left: 4px solid #3498db;
padding: 15px;
margin-top: 20px;
border-radius: 0 8px 8px 0;
}
.instructions h3 {
color: #2c3e50;
margin-bottom: 10px;
}
.instructions ul {
padding-left: 20px;
color: #555;
}
.instructions li {
margin-bottom: 8px;
line-height: 1.5;
}
footer {
text-align: center;
margin-top: 40px;
color: #7f8c8d;
font-size: 0.9rem;
}
.status-bar {
display: flex;
justify-content: space-between;
align-items: center;
background: #eaf6ff;
padding: 10px 15px;
border-radius: 6px;
margin-top: 15px;
font-size: 14px;
}
.file-info {
display: flex;
align-items: center;
gap: 10px;
}
.file-icon {
color: #e74c3c;
font-size: 20px;
}
.features {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-top: 30px;
}
.feature-card {
background: #f8f9fa;
border-radius: 10px;
padding: 20px;
text-align: center;
border: 1px solid #eaeaea;
}
.feature-card i {
font-size: 36px;
color: #3498db;
margin-bottom: 15px;
}
.feature-card h3 {
margin-bottom: 10px;
color: #2c3e50;
}
/* 浅色主题覆盖样式 */
::v-ddep :root {
--primary-color: #f0f0f0;
--secondary-color: #ffffff;
--text-color: #333333;
--border-color: #e0e0e0;
--hover-color: #e6f7ff;
}
/* PDF.js v1.10.100 浅色主题样式 */
/* 基础样式 */
/* body {
background-color: #f5f7fa !important;
color: #333 !important;
font-family: Arial, sans-serif !important;
} */
::v-deep #toolbarSideba{
background-color: #ffffff !important;
border-bottom: 1px solid #e1e4e8 !important;
}
::v-deep #sidebarContainer{
background-color: #ffffff !important;
border-bottom: 1px solid #e1e4e8 !important;
}
/* 工具栏 */
#toolbar {
background-color: #ffffff !important;
border-bottom: 1px solid #e1e4e8 !important;
}
.toolbarButton {
color: #586069 !important;
background-color: transparent !important;
border: none !important;
}
.toolbarButton:hover {
background-color: #f1f5f9 !important;
color: #2d3748 !important;
border-radius: 4px !important;
}
.toolbarButton[disabled] {
color: #d1d5da !important;
}
/* 缩放控制 */
#scaleSelectContainer .dropdown {
background-color: #ffffff !important;
border: 1px solid #e1e4e8 !important;
color: #333 !important;
}
/* 侧边栏 */
#sidebar {
background-color: #ffffff !important;
border-right: 1px solid #e1e4e8 !important;
}
#sidebarContent {
background-color: #ffffff !important;
}
/* 缩略图视图 */
#thumbs {
background-color: #ffffff !important;
}
.thumb {
border: 1px solid transparent !important;
}
.thumb.selected {
border-color: #4299e1 !important;
background-color: #ebf8ff !important;
}
/* 大纲视图 */
#outline {
background-color: #ffffff !important;
}
.outlineItem {
color: #333 !important;
}
.outlineItem:hover {
background-color: #f1f5f9 !important;
}
.outlineItem.active {
color: #2b6cb0 !important;
background-color: #ebf8ff !important;
}
/* 文档容器 */
#viewer {
background-color: #e9ecef !important;
}
.page {
background-color: #ffffff !important;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06) !important;
margin: 10px auto !important;
}
.page:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important;
}
/* 搜索面板 */
#findbar {
background-color: #ffffff !important;
border-top: 1px solid #e1e4e8 !important;
}
#findInput {
background-color: #f9fafb !important;
border: 1px solid #e1e4e8 !important;
color: #333 !important;
}
.findbarButton {
color: #586069 !important;
background-color: transparent !important;
}
.findbarButton:hover {
background-color: #f1f5f9 !important;
}
/* 上下文菜单 */
.contextMenu {
background-color: #ffffff !important;
border: 1px solid #e1e4e8 !important;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1) !important;
}
.contextMenuItem {
color: #333 !important;
}
.contextMenuItem:hover {
background-color: #f1f5f9 !important;
color: #2d3748 !important;
}
/* 加载指示器 */
#loadingBox {
background-color: rgba(255, 255, 255, 0.9) !important;
}
#loadingIndicator {
color: #4a5568 !important;
}
/* 错误消息 */
#errorMessage {
background-color: #fff5f5 !important;
color: #742a2a !important;
border: 1px solid #ffe3e3 !important;
}
/* 打印对话框 */
.printDialog {
background-color: #ffffff !important;
border: 1px solid #e1e4e8 !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08) !important;
}
.printDialogTitle {
color: #333 !important;
border-bottom: 1px solid #e1e4e8 !important;
}
.printDialogButton {
background-color: #f5f5f5 !important;
color: #333 !important;
border: 1px solid #ddd !important;
}
.printDialogButton:hover {
background-color: #e9e9e9 !important;
}
/* 文本层样式 */
.textLayer {
color: #1a202c !important;
}
/* 链接样式 */
a {
color: #2b6cb0 !important;
}
a:hover {
color: #1a365d !important;
}
</style>