当前位置: 首页 > news >正文

【vue3-element-admin 项目实战】:基于vue-pdf-embed 构建专业级PDF预览组件

🚀 作者主页: 有来技术
🔥 开源项目: youlai-mall ︱vue3-element-admin︱youlai-boot︱vue-uniapp-template
🌺 仓库主页: GitCode︱ Gitee ︱ Github
💖 欢迎点赞 👍 收藏 ⭐评论 📝 如有错误敬请纠正!

为什么选择vue-pdf-embed

在企业级Web应用中,PDF在线预览是一个常见且高频的需求,特别是在管理后台、合同平台或文档中心等场景。经过评估多种PDF预览解决方案后,我选择了vue-pdf-embed库,原因有三:

  1. 与Vue3生态完美兼容,支持TypeScript
  2. 基于PDF.js,渲染质量高且功能丰富
  3. 轻量级设计,API简洁易用,易于扩展

本文将基于开源项目 vue3-element-admin ,提供一套精简、可直接使用的PDF预览实现方案,技术栈为Vue3 + TypeScript + Element Plus + Vite。

效果预览:
在这里插入图片描述

环境准备与依赖安装

首先,确保您的项目已经搭建好Vue3环境。如果您使用的是vue3-element-admin框架,可以直接在此基础上进行开发。

安装vue-pdf-embed依赖:

# 使用pnpm
pnpm add vue-pdf-embed# 或使用npm
npm install vue-pdf-embed# 或使用yarn
yarn add vue-pdf-embed

组件设计与实现

组件布局结构

我们的PDF预览组件采用三栏布局设计:

  • 左侧控制面板:提供PDF选择、自定义链接输入、缩放控制、页面导航等功能
  • 中间预览区域:展示PDF内容,支持加载状态和错误处理
  • 右侧缩略图区域:可切换显示/隐藏,支持缩略图导航

这种布局既符合用户习惯,又能充分利用大屏幕空间,提供良好的阅读体验。

核心功能实现

将以下代码保存为src/views/pdf-preview.vue,可直接在项目中复制拷贝使用:

<template><div class="pdf-preview"><el-card><template #header><div class="header"><span>PDF 预览(vue-pdf-embed)</span><el-tag type="info">基于 vue3-element-admin</el-tag></div></template><div class="layout"><!-- 左侧控制 --><div class="side"><el-form label-position="top" size="small"><el-form-item label="选择示例PDF"><el-select v-model="selectedPdf" placeholder="请选择PDF"><el-optionv-for="item in pdfOptions":key="item.value":label="item.label":value="item.value"/></el-select></el-form-item><el-form-item label="自定义PDF链接"><el-input v-model="customPdfUrl" placeholder="输入 PDF 链接" @keyup.enter="loadCustomPdf"><template #append><el-button @click="loadCustomPdf" size="small">加载</el-button></template></el-input></el-form-item><el-divider /><div class="tools"><el-button @click="zoomIn" :disabled="!pdfSrc" size="small">放大</el-button><el-button @click="zoomOut" :disabled="!pdfSrc" size="small">缩小</el-button><el-button @click="resetZoom" :disabled="!pdfSrc" size="small">重置</el-button><div class="pager" style="margin-top:8px"><el-button @click="prevPage" :disabled="currentPage <= 1" size="small">上一页</el-button><el-input-numberv-model="currentPage":min="1":max="Math.max(totalPages, 1)"size="small"style="width: 100px"@change="handlePageNumberChange"/><span>/ {{ totalPages }}</span><el-button @click="nextPage" :disabled="currentPage >= totalPages" size="small">下一页</el-button></div><div style="margin-top:8px"><el-switch v-model="showThumbnails" active-text="显示缩略图" inactive-text="隐藏缩略图" /></div><div style="margin-top:8px"><el-button type="success" @click="downloadPdf" :disabled="!pdfSrc" size="small">下载 PDF</el-button></div></div></el-form></div><!-- 中间预览 --><div class="main" ref="previewWrapRef"><div v-loading="loading" element-loading-text="PDF加载中..." class="viewer-wrap"><div v-if="error"><el-result icon="error" title="加载失败" :sub-title="error"><template #extra><el-button type="primary" @click="retryLoad">重试</el-button></template></el-result></div><div v-else-if="!pdfSrc"><el-empty description="请选择或输入 PDF 链接" /></div><div v-else class="pdf-viewer"><VuePdfEmbedref="pdfRef":source="pdfSrc":page="currentPage":width="pageWidth":rotation="rotation"class="pdf-embed"@loaded="onPdfLoaded"@loading-failed="onLoadingFailed"@page-loaded="onPageLoaded"@password-requested="onPasswordRequested"/></div></div></div><!-- 右侧缩略图 --><div class="thumb" v-if="showThumbnails && pdfSrc"><div class="thumbs"><divv-for="p in totalPages":key="p":class="['thumb-item', { active: p === currentPage }]"@click="goToPage(p)"><VuePdfEmbed :source="pdfSrc" :page="p" :scale="0.18" class="thumb-mini" /><div class="label">{{ p }}</div></div></div></div></div></el-card></div>
</template><script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
import VuePdfEmbed from 'vue-pdf-embed';
import { ElMessage, ElMessageBox } from 'element-plus';/* state */
const pdfSrc = ref<string>('');
const selectedPdf = ref<string>('');
const customPdfUrl = ref<string>('');
const loading = ref(false);
const error = ref('');
const totalPages = ref(0);
const currentPage = ref(1);
const scale = ref(1.0);
const rotation = ref(0);
const showThumbnails = ref(false);const pdfOptions = [{label: '示例 PDF(Mozilla)',value: 'https://mozilla.github.io/pdf.js/web/compressed.tracemonkey-pldi-09.pdf',},
];const pdfRef = ref<any>(null);
const previewWrapRef = ref<HTMLElement | null>(null);
const pageWidth = ref<number | undefined>(undefined);const updatePageWidth = () => {const wrap = previewWrapRef.value;if (!wrap) return;const containerWidth = Math.max(200, wrap.clientWidth - 40);pageWidth.value = Math.round(containerWidth * scale.value);
};onMounted(() => {if (!selectedPdf.value && pdfOptions.length) selectedPdf.value = pdfOptions[0].value;nextTick(updatePageWidth);window.addEventListener('resize', updatePageWidth);
});onBeforeUnmount(() => {window.removeEventListener('resize', updatePageWidth);
});watch(selectedPdf, (v) => { if (v) loadPdf(v); });/* load */
const loadPdf = async (url: string) => {loading.value = true;error.value = '';pdfSrc.value = url;currentPage.value = 1;scale.value = 1.0;rotation.value = 0;nextTick(updatePageWidth);
};const loadCustomPdf = () => {if (!customPdfUrl.value.trim()) return ElMessage.warning('请输入 PDF 链接');if (!customPdfUrl.value.toLowerCase().endsWith('.pdf')) return ElMessage.warning('请输入有效的 PDF 链接');loadPdf(customPdfUrl.value.trim());
};/* events */
let lastLoadedSrc = ref('');
const onPdfLoaded = (pdf: any) => {loading.value = false;totalPages.value = pdf?.numPages || 0;if (pdfSrc.value !== lastLoadedSrc.value) {lastLoadedSrc.value = pdfSrc.value;ElMessage.success(`PDF 加载成功,共 ${totalPages.value}`);}
};
const onLoadingFailed = (e: any) => {loading.value = false;error.value = e?.message || 'PDF 加载失败';ElMessage.error(error.value);
};
const onPageLoaded = () => { /* 页面渲染完成可处理其它逻辑 */ };
const onPasswordRequested = async (callback: Function) => {try {const { value } = await ElMessageBox.prompt('此 PDF 受密码保护,请输入密码', '密码验证', {inputType: 'password',confirmButtonText: '确定',cancelButtonText: '取消',});callback(value);} catch {callback(null);}
};const retryLoad = () => { if (pdfSrc.value) loadPdf(pdfSrc.value); };/* controls */
const zoomIn = () => { scale.value = Math.min(scale.value + 0.25, 3.0); updatePageWidth(); };
const zoomOut = () => { scale.value = Math.max(scale.value - 0.25, 0.25); updatePageWidth(); };
const resetZoom = () => { scale.value = 1.0; updatePageWidth(); };const prevPage = () => { if (currentPage.value > 1) currentPage.value--; };
const nextPage = () => { if (currentPage.value < totalPages.value) currentPage.value++; };
const goToPage = (p: number) => { if (p >= 1 && p <= totalPages.value) currentPage.value = p; };
const handlePageNumberChange = (cur?: number) => { if (typeof cur === 'number') goToPage(cur); };/* download */
const downloadPdf = () => {if (!pdfSrc.value) return;const src = pdfSrc.value;if (/^https?:/i.test(src)) {(async () => {try {const resp = await fetch(src, { mode: 'cors' });const blob = await resp.blob();const cd = resp.headers.get('content-disposition') || '';const m = cd.match(/filename\*=UTF-8''([^;]+)|filename="?([^";]+)"?/i);let fileName = m ? decodeURIComponent(m[1] || m[2]) : '';if (!fileName) {try { fileName = new URL(src).pathname.split('/').pop() || `document-${Date.now()}.pdf`; }catch { fileName = `document-${Date.now()}.pdf`; }}const url = URL.createObjectURL(blob);const a = document.createElement('a');a.href = url;a.download = fileName.endsWith('.pdf') ? fileName : `${fileName}.pdf`;document.body.appendChild(a);a.click();a.remove();URL.revokeObjectURL(url);ElMessage.success('已开始下载');} catch {try { await pdfRef.value?.download?.(`document-${Date.now()}.pdf`); }catch { ElMessage.error('下载失败'); }}})();return;}try { pdfRef.value?.download?.(`document-${Date.now()}.pdf`); }catch {const link = document.createElement('a');link.href = src as string;link.download = `document-${Date.now()}.pdf`;document.body.appendChild(link);link.click();link.remove();}
};
</script><style scoped lang="scss">
.pdf-preview {.layout { display:flex; gap:16px; align-items:flex-start; }.side { width:300px; flex-shrink:0; }.main { flex:1; min-width:0; }.thumb { width:220px; flex-shrink:0; }.pdf-embed { border:1px solid #ddd; }.thumb-mini { width:100%; }.thumb-item { padding:8px; margin-bottom:8px; cursor:pointer; border:1px solid transparent; border-radius:4px; }.thumb-item.active { border-color:#409EFF; background:#f0f8ff; }
}
</style>

样式设计

我们采用了简洁而实用的样式设计:

  1. 使用Flex布局实现三栏结构,确保在不同屏幕尺寸下的自适应
  2. 为PDF预览区域添加边框,增强视觉分隔效果
  3. 缩略图区域采用高亮样式标记当前页,提升用户体验
  4. 整体风格与Element Plus组件库保持一致,确保UI的统一性

功能详解

PDF加载与错误处理

组件支持两种PDF加载方式:

  1. 从预设列表中选择PDF
  2. 输入自定义PDF链接并加载

为提升用户体验,我们实现了完善的加载状态和错误处理机制:

  • 加载中显示加载动画
  • 加载失败时展示错误信息和重试按钮
  • 未选择PDF时显示空状态提示

关键代码实现:

const loadPdf = async (url: string) => {loading.value = true;error.value = '';pdfSrc.value = url;currentPage.value = 1;scale.value = 1.0;rotation.value = 0;nextTick(updatePageWidth);
};const onLoadingFailed = (e: any) => {loading.value = false;error.value = e?.message || 'PDF 加载失败';ElMessage.error(error.value);
};

页面导航与缩放控制

组件提供了丰富的页面导航和缩放控制功能:

  • 上一页/下一页按钮
  • 页码输入框,支持直接跳转到指定页
  • 放大/缩小/重置缩放按钮

缩放功能通过动态计算PDF宽度实现,并结合窗口大小调整自动适配容器宽度:

const updatePageWidth = () => {const wrap = previewWrapRef.value;if (!wrap) return;const containerWidth = Math.max(200, wrap.clientWidth - 40);pageWidth.value = Math.round(containerWidth * scale.value);
};// 监听窗口大小变化
onMounted(() => {window.addEventListener('resize', updatePageWidth);
});

缩略图预览

缩略图功能是提升PDF浏览体验的重要部分,特别是对于多页PDF文档:

  • 支持通过开关控制缩略图区域的显示/隐藏
  • 缩略图使用小比例尺渲染PDF,优化性能
  • 当前页高亮显示,点击缩略图可快速跳转

实现方式是复用VuePdfEmbed组件,但使用较小的比例尺:

<VuePdfEmbed :source="pdfSrc" :page="p" :scale="0.18" class="thumb-mini" />

密码保护支持

对于受密码保护的PDF文件,组件提供了友好的密码输入界面:

const onPasswordRequested = async (callback: Function) => {try {const { value } = await ElMessageBox.prompt('此 PDF 受密码保护,请输入密码', '密码验证', {inputType: 'password',confirmButtonText: '确定',cancelButtonText: '取消',});callback(value);} catch {callback(null);}
};

这种实现方式保证了安全性,同时提供了良好的用户体验。

PDF下载实现

组件提供了PDF下载功能,支持多种下载场景:

  1. 远程URL的PDF下载,自动提取文件名
  2. 本地PDF的下载,使用时间戳作为默认文件名
  3. 优雅降级处理,确保在不同环境下都能正常下载

下载功能的实现考虑了各种边界情况,保证了功能的健壮性。

总结与实践建议

本文提供了一个专业级PDF预览组件的完整实现,具有以下特点:

  1. 功能完备:支持PDF加载、页面导航、缩放控制、缩略图预览、密码保护和文件下载
  2. 用户体验优先:良好的错误处理、加载状态反馈和交互设计
  3. 易于集成:可直接复制到vue3-element-admin项目中使用,无需额外配置

在实际项目中使用时,可以根据需求进一步扩展以下功能:

  • 添加全文搜索功能
  • 实现PDF标注和批注功能
  • 增加页面旋转和打印支持
  • 集成文档管理系统,实现更复杂的文档操作

通过这个组件,您可以在Vue3项目中快速实现专业级的PDF预览功能,提升用户体验和开发效率。


文章转载自:

http://WeBkSw1q.rkwjs.cn
http://iTToul6V.rkwjs.cn
http://4n9nZQam.rkwjs.cn
http://ADoN4c8u.rkwjs.cn
http://SGvrAf0S.rkwjs.cn
http://kZwK4jrb.rkwjs.cn
http://rBgA7gsf.rkwjs.cn
http://bu3PSRut.rkwjs.cn
http://t034lNGE.rkwjs.cn
http://aoSqCsja.rkwjs.cn
http://gWd3e4zO.rkwjs.cn
http://mwVwyJ5B.rkwjs.cn
http://N1A3Axwf.rkwjs.cn
http://UpArkRRX.rkwjs.cn
http://PyxcMk5z.rkwjs.cn
http://Wjd7b3J5.rkwjs.cn
http://bz7Vvo1t.rkwjs.cn
http://l5oripuW.rkwjs.cn
http://mMTRvf5r.rkwjs.cn
http://Wt5OFfNw.rkwjs.cn
http://fcHHM86G.rkwjs.cn
http://PyMYbbJG.rkwjs.cn
http://H0vsSe0Y.rkwjs.cn
http://aco7FN4E.rkwjs.cn
http://Birlsb6u.rkwjs.cn
http://MluVX8V4.rkwjs.cn
http://gRH7qt18.rkwjs.cn
http://ZhcJB7jD.rkwjs.cn
http://LjF43m4d.rkwjs.cn
http://yG2ysc9j.rkwjs.cn
http://www.dtcms.com/a/385764.html

相关文章:

  • QC七大工具与生活-控制图
  • ABP + Verify(快照) 驱动的 PDF/Excel 导出回归
  • 《探秘PCDN:破解数字生活的极速密码》
  • 佰力博检测与您探讨样品电晕极化在实际生活中的应用
  • JAVASCRIPT 前端数据库-V10 说明书--仙盟数据库架构-—仙盟创梦IDE
  • itext5生成pdf和合并pdf
  • 整体设计 之 绪 思维导图引擎 之 引 认知系统 之 引 认知系统 之 序 认知元架构 之 概要设计收官 之2 认知科学向度的 唯识学高阶重构(豆包助手)
  • 商务折叠屏市场洞察:从技术竞赛到生态重构
  • 【开题答辩全过程】以 hadoop企业信息管理系统为例,包含答辩的问题和答案
  • 大模型decoder中权重矩阵的理解
  • SpringBoot项目通过k8s集群发布与管理
  • Ubuntu20.04仿真 |iris四旋翼添加云台相机详述
  • 【K8s】什么是K8s?
  • kubernetes(k8s)核心之Pod速通
  • 1.8、机器学习-XGBoost模型(金融实战)
  • Nosana发布公共GPU市场,释放去中心化AI算力无限潜能
  • 图灵完备性:计算理论的基石与无限可能
  • Fiddler使用教程 代理设置、HTTPS抓包与接口调试全流程指南
  • 手写MyBatis第63弹:MyBatis SQL日志插件完整实现:专业级SQL监控与调试方案
  • CrowS-Pairs:衡量掩码语言模型中社会偏见的挑战数据集
  • 认知语义学意象图式对人工智能自然语言处理中隐喻分析的影响与启示
  • 中小企业 4G 专网部署:性能与成本的最佳平衡
  • 解决照片内存告急和无公网访问,用Piwigo+cpolar组合刚刚好
  • SQLAlchemy使用笔记(二)
  • Linux服务器日志管理与分析(以journalctl为例)
  • 即插即用,秒入虚拟:TouchDIVER Pro 触觉手套 赋能 AR/VR 高效交互
  • CentOS系统修改网卡命名的方法总结
  • 超越RGB:移动设备多光谱成像的真实世界数据集
  • 固高运动卡与 Blaster 相机协同的飞拍系统:技术实现与应用案例
  • 无法定位程序输入点于动态链接库 kernel32.dll?深度解析与5种修复方法