hookehuyr

feat(pdf): 添加pdf-vue3依赖并实现新版PDF查看组件

添加pdf-vue3依赖包用于PDF文件预览
创建PdfViewer组件替换原有PdfPreview组件
优化PDF加载性能并添加进度显示和错误处理
在StudyDetailPage中使用新组件并添加文件大小显示
......@@ -35,6 +35,7 @@
"browser-md5-file": "^1.1.1",
"dayjs": "^1.11.13",
"lodash": "^4.17.21",
"pdf-vue3": "^1.0.12",
"swiper": "^11.2.6",
"uuid": "^11.1.0",
"vant": "^4.9.19",
......
......@@ -18,6 +18,7 @@ declare module 'vue' {
CourseCard: typeof import('./components/ui/CourseCard.vue')['default']
CourseImageCard: typeof import('./components/ui/CourseImageCard.vue')['default']
CourseList: typeof import('./components/courses/CourseList.vue')['default']
FilePreviewPopup: typeof import('./components/ui/FilePreviewPopup.vue')['default']
FormPage: typeof import('./components/infoEntry/formPage.vue')['default']
FrostedGlass: typeof import('./components/ui/FrostedGlass.vue')['default']
GradientHeader: typeof import('./components/ui/GradientHeader.vue')['default']
......@@ -25,6 +26,7 @@ declare module 'vue' {
MenuItem: typeof import('./components/ui/MenuItem.vue')['default']
OfficeViewer: typeof import('./components/ui/OfficeViewer.vue')['default']
PdfPreview: typeof import('./components/ui/PdfPreview.vue')['default']
PdfViewer: typeof import('./components/ui/PdfViewer.vue')['default']
ReviewPopup: typeof import('./components/courses/ReviewPopup.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
......
......@@ -79,12 +79,29 @@ const initPdfViewer = () => {
// customPdfOption是 pdfjs getDocument 函数中一些配置参数 具体可参考 https://mozilla.github.io/pdf.js/api/draft/module-pdfjsLib.html#~DocumentInitParameters
cMapPacked: true, //指定 CMap 是否是二进制打包的
cMapUrl: "https://cdn.jsdelivr.net/npm/pdfjs-dist@2.2.228/cmaps/", //预定义 Adob​​e CMaps 所在的 URL。可解决字体加载错误
// 内存优化配置
disableAutoFetch: true, // 启用自动获取,提升加载速度
disableStream: true, // 启用流式加载
disableRange: true, // 启用范围请求,支持分片下载
// maxImageSize: 1 * 1024 * 1024, // 限制图片最大尺寸为1MB
isEvalSupported: false, // 禁用eval,提升安全性和性能
// 启用懒加载,减少内存占用
enableXfa: true, // 禁用XFA表单支持,减少内存占用
// 添加更多性能优化配置
useOnlyCssZoom: true, // 仅使用CSS缩放,减少重新渲染
verbosity: 0, // 减少日志输出,提升性能
// 微信浏览器优化配置
useWorkerFetch: true, // 在微信浏览器中禁用Worker fetch,避免兼容性问题
disableFontFace: true, // 保持字体渲染,但可能需要根据情况调整
// 内存管理配置
pdfBug: true, // 禁用调试模式,减少内存占用
stopAtErrors: true, // 不要在错误时停止,让错误回调处理
},
// 禁用右键菜单和文本选择
selectConfig: undefined, // 禁用文本选择功能
}
});
// 添加额外的事件监听器来防止长按保存
const pdfContainer = document.querySelector("#pdf-container");
if (pdfContainer) {
......@@ -93,13 +110,13 @@ const initPdfViewer = () => {
e.preventDefault();
return false;
});
// 防止长按选择
pdfContainer.addEventListener('selectstart', (e) => {
e.preventDefault();
return false;
});
// 防止拖拽
pdfContainer.addEventListener('dragstart', (e) => {
e.preventDefault();
......
<!--
* @Date: 2025-01-21
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-10-21 14:33:07
* @FilePath: /mlaj/src/components/ui/PdfViewer.vue
* @Description: PDF预览组件 - 使用pdf-vue3库
-->
<template>
<van-popup v-if="show" :show="show" @update:show="emit('update:show', $event)" position="right"
:style="{ height: '100%', width: '100%' }">
<div class="pdf-viewer-container">
<!-- PDF内容区域 -->
<div class="pdf-content" :class="{ 'pdf-no-select': preventSave }">
<PDF v-if="url && show && !loadingError" :src="url" :showProgress="true" :progressColor="'#1989fa'"
:showPageTooltip="true" :showBackToTopBtn="true" :scrollThreshold="300" :pdfWidth="'100%'"
:rowGap="8" :page="1" :cMapUrl="'https://unpkg.com/pdfjs-dist@3.7.107/cmaps/'"
:withCredentials="false" :useSystemFonts="true" :stopAtErrors="false" :disableFontFace="false"
:disableRange="false" :disableStream="false" :disableAutoFetch="false" @onProgress="handleProgress"
@onComplete="handleComplete" @onScroll="handleScroll" @onPageChange="handlePageChange"
@onPdfInit="handlePdfInit" @onError="handlePdfError" />
<!-- 错误状态显示 -->
<div v-if="loadingError" class="error-container">
<div class="error-content">
<van-icon name="warning-o" size="48" color="#ff4444" />
<div class="error-title">加载失败</div>
<div class="error-message">{{ errorMessage }}</div>
<van-button type="primary" size="small" @click="retryLoad" class="retry-btn">
重新加载
</van-button>
</div>
</div>
</div>
<!-- 关闭按钮 -->
<van-button class="close-btn" type="default" icon="cross" round @click="emit('update:show', false)" />
<!-- 加载遮罩 -->
<van-overlay :show="loading && !loadingError">
<div class="wrapper" @click.stop>
<div class="loading-content">
<!-- 进度环形图 -->
<div class="progress-circle">
<div class="progress-percent">{{ loadingProgress }}%</div>
</div>
<!-- 加载文字 -->
<div class="loading-text">
<div class="loading-title">正在加载PDF文档</div>
<div class="loading-subtitle">
{{ loadingProgress < 10 ? '正在连接服务器...' : loadingProgress < 50 ? '正在下载文件...' :
loadingProgress < 90 ? '正在解析文档...' : '即将完成...' }} </div>
</div>
</div>
</div>
</van-overlay>
</div>
</van-popup>
</template>
<script setup>
import { ref, watch, nextTick } from 'vue';
import PDF from "pdf-vue3";
/**
* 组件属性定义
*/
const props = defineProps({
// 是否显示弹窗
show: {
type: Boolean,
default: false
},
// PDF文件URL
url: {
type: String,
default: ''
},
// PDF标题
title: {
type: String,
default: ''
},
// 是否防止保存(禁用右键、长按等)
preventSave: {
type: Boolean,
default: true
}
});
/**
* 事件定义
*/
const emit = defineEmits(['update:show', 'onLoad', 'onProgress', 'onComplete']);
// 响应式数据
const loading = ref(true);
const loadingProgress = ref(0);
const loadingError = ref(false);
const errorMessage = ref('');
/**
* 监听show属性变化,控制PDF加载
*/
watch(() => props.show, (newVal) => {
if (newVal && props.url) {
// 重置所有状态
loading.value = true;
loadingError.value = false;
loadingProgress.value = 0;
errorMessage.value = '';
} else if (!newVal) {
// 弹窗关闭时重置状态
loading.value = true;
loadingError.value = false;
loadingProgress.value = 0;
errorMessage.value = '';
}
});
/**
* 监听URL变化,重新加载PDF
*/
watch(() => props.url, (newUrl) => {
if (newUrl && props.show) {
// URL变化时重置状态
loading.value = true;
loadingError.value = false;
loadingProgress.value = 0;
errorMessage.value = '';
}
});
/**
* 处理PDF下载进度
* @param {number} loadRatio - 加载进度 0-100
*/
const handleProgress = (loadRatio) => {
loadingProgress.value = Math.round(loadRatio);
emit('onProgress', loadRatio);
};
/**
* 处理PDF下载完成
*/
const handleComplete = () => {
loading.value = false;
loadingError.value = false;
loadingProgress.value = 100;
emit('onLoad', false);
emit('onComplete');
};
/**
* 处理PDF滚动事件
* @param {number} scrollOffset - 滚动偏移量
*/
const handleScroll = (scrollOffset) => {
// 可以在这里处理滚动事件
};
/**
* 处理页面变化事件
* @param {number} page - 当前页码
*/
const handlePageChange = (page) => {
// 可以在这里处理页面变化事件
};
/**
* 处理PDF初始化完成
* @param {Object} pdf - PDF文档对象
*/
const handlePdfInit = (pdf) => {
// PDF初始化完成后的处理
loadingError.value = false;
nextTick(() => {
if (props.preventSave) {
// 添加防止保存的事件监听器
addPreventSaveListeners();
}
});
};
/**
* 处理PDF加载错误
* @param {Error} error - 错误对象
*/
const handlePdfError = (error) => {
console.error('PDF加载失败:', error);
loading.value = false;
loadingError.value = true;
loadingProgress.value = 0;
// 根据错误类型设置不同的错误信息
if (error.name === 'NetworkError' || error.message.includes('network')) {
errorMessage.value = '网络连接失败,请检查网络后重试';
} else if (error.name === 'InvalidPDFException' || error.message.includes('Invalid PDF')) {
errorMessage.value = 'PDF文件格式错误或已损坏';
} else if (error.message.includes('404') || error.message.includes('Not Found')) {
errorMessage.value = '文件不存在或已被删除';
} else if (error.message.includes('403') || error.message.includes('Forbidden')) {
errorMessage.value = '没有权限访问此文件';
} else {
errorMessage.value = '文件加载失败,请重试';
}
};
/**
* 重试加载PDF
*/
const retryLoad = () => {
loadingError.value = false;
loading.value = true;
loadingProgress.value = 0;
errorMessage.value = '';
};
/**
* 添加防止保存的事件监听器
*/
const addPreventSaveListeners = () => {
const pdfContainer = document.querySelector('.pdf-viewer-container .pdf-content');
if (pdfContainer) {
// 防止上下文菜单(右键菜单)
pdfContainer.addEventListener('contextmenu', (e) => {
e.preventDefault();
return false;
});
// 防止长按选择
pdfContainer.addEventListener('selectstart', (e) => {
e.preventDefault();
return false;
});
// 防止拖拽
pdfContainer.addEventListener('dragstart', (e) => {
e.preventDefault();
return false;
});
// 防止触摸回调(移动端长按)
pdfContainer.addEventListener('touchstart', (e) => {
if (e.touches.length > 1) {
e.preventDefault();
}
});
// 防止双指缩放等手势
pdfContainer.addEventListener('gesturestart', (e) => {
e.preventDefault();
});
}
};
</script>
<style scoped>
.pdf-viewer-container {
width: 100%;
height: 100%;
position: relative;
background-color: #f5f5f5;
}
.pdf-content {
width: 100%;
height: 100%;
overflow: auto;
}
/* 防止PDF内容被选择和保存的样式 */
.pdf-no-select {
-webkit-user-select: none;
/* Safari */
-moz-user-select: none;
/* Firefox */
-ms-user-select: none;
/* IE10+/Edge */
user-select: none;
/* Standard */
-webkit-touch-callout: none;
/* iOS Safari */
-webkit-tap-highlight-color: transparent;
/* 移除点击高亮 */
pointer-events: auto;
/* 保持可点击 */
}
/* 深度选择器,防止PDF内部元素被保存 */
:deep(.pdf-no-select img) {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
pointer-events: none;
/* 禁用图片的所有交互 */
-webkit-user-drag: none;
/* 禁止拖拽 */
-khtml-user-drag: none;
-moz-user-drag: none;
-o-user-drag: none;
user-drag: none;
}
/* 防止Canvas元素被保存 */
:deep(.pdf-no-select canvas) {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
pointer-events: none;
-webkit-user-drag: none;
-khtml-user-drag: none;
-moz-user-drag: none;
-o-user-drag: none;
user-drag: none;
}
/* 关闭按钮样式 */
.close-btn {
position: fixed;
right: 20px;
top: 20px;
z-index: 100;
width: 40px;
height: 40px;
padding: 0;
border-radius: 50%;
background: rgba(0, 0, 0, 0.6);
color: #fff;
}
/* 加载遮罩样式 */
.wrapper {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(4px);
}
/* 加载内容样式 */
.loading-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
color: #fff;
}
.progress-circle {
width: 80px;
height: 80px;
background-color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.progress-percent {
font-size: 18px;
font-weight: bold;
color: #1989fa;
}
.loading-text {
max-width: 200px;
}
.loading-title {
font-size: 16px;
font-weight: 500;
margin-bottom: 8px;
color: #fff;
}
.loading-subtitle {
font-size: 14px;
color: rgba(255, 255, 255, 0.8);
line-height: 1.4;
}
/* 错误状态样式 */
.error-container {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
background: #f8f9fa;
}
.error-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 40px 20px;
max-width: 300px;
}
.error-title {
font-size: 18px;
font-weight: 600;
color: #333;
margin: 16px 0 8px 0;
}
.error-message {
font-size: 14px;
color: #666;
line-height: 1.5;
margin-bottom: 20px;
}
.retry-btn {
min-width: 100px;
}
</style>
......@@ -261,7 +261,7 @@
</van-popup>
<!-- PDF预览 -->
<PdfPreview v-model:show="pdfShow" :url="pdfUrl" :title="pdfTitle" @onLoad="onPdfLoad" />
<PdfViewer v-model:show="pdfShow" :url="pdfUrl" :title="pdfTitle" @onLoad="onPdfLoad" @onProgress="onPdfProgress" @onComplete="onPdfComplete" />
<!-- Office 文档预览弹窗 -->
<van-popup
......@@ -536,6 +536,7 @@
<div class="flex items-center gap-1">
<van-icon name="label-o" size="12" style="margin-right: 0.25rem;"/>
<span>{{ getFileType(file.title || file.name) }}</span>
<span class="ml-2">{{ file.size ? (file.size / 1024 / 1024).toFixed(2) + 'MB' : '' }}</span>
</div>
<!-- 复制地址按钮 -->
<!-- <button
......@@ -656,7 +657,7 @@ import { formatDate, wxInfo } from '@/utils/tools'
import axios from 'axios';
import { v4 as uuidv4 } from "uuid";
import { useIntersectionObserver } from '@vueuse/core';
import PdfPreview from '@/components/ui/PdfPreview.vue';
import PdfViewer from '@/components/ui/PdfViewer.vue';
import { showToast } from 'vant';
// 导入接口
......@@ -986,6 +987,21 @@ const onPdfLoad = (load) => {
};
/**
* PDF下载进度回调
* @param {number} progress - 下载进度 0-100
*/
const onPdfProgress = (progress) => {
// console.log('PDF下载进度:', progress);
};
/**
* PDF下载完成回调
*/
const onPdfComplete = () => {
// console.log('PDF下载完成');
};
/**
* Office 文档渲染完成回调
*/
const onOfficeRendered = () => {
......
......@@ -2124,6 +2124,11 @@ pathe@^2.0.1, pathe@^2.0.2, pathe@^2.0.3:
resolved "https://registry.yarnpkg.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716"
integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==
pdf-vue3@^1.0.12:
version "1.0.12"
resolved "https://registry.npmjs.org/pdf-vue3/-/pdf-vue3-1.0.12.tgz#8b2c4346cd75a7d3307aa6117c591c047549f4ed"
integrity sha512-7SMTx1RfRwdc+2WPniDzqM8MxJLqTNNzdyV0SeQTxeRLJGndb5Wv/fz5afO13oBSIvvaqcbZ/S3gF+XjqkSb9g==
pdfjs-dist@3.4.120:
version "3.4.120"
resolved "https://registry.yarnpkg.com/pdfjs-dist/-/pdfjs-dist-3.4.120.tgz#6f4222117157498f179c95dc4569fad6336a8fdd"
......