hookehuyr

feat(router): 新增PDF预览独立页面并优化预览体验

将PDF预览从弹窗改为独立页面,提升大文件加载体验
添加返回课程详情页功能并保持学习资料弹窗状态
优化PDF组件关闭逻辑和错误处理
1 <!-- 1 <!--
2 * @Date: 2025-01-21 2 * @Date: 2025-01-21
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-10-22 09:57:57 4 + * @LastEditTime: 2025-10-22 10:49:55
5 * @FilePath: /mlaj/src/components/ui/PdfViewer.vue 5 * @FilePath: /mlaj/src/components/ui/PdfViewer.vue
6 * @Description: PDF预览组件 - 使用pdf-vue3库 6 * @Description: PDF预览组件 - 使用pdf-vue3库
7 --> 7 -->
...@@ -11,30 +11,30 @@ ...@@ -11,30 +11,30 @@
11 <div class="pdf-viewer-container"> 11 <div class="pdf-viewer-container">
12 <!-- PDF内容区域 --> 12 <!-- PDF内容区域 -->
13 <div class="pdf-content" :class="{ 'pdf-no-select': preventSave }"> 13 <div class="pdf-content" :class="{ 'pdf-no-select': preventSave }">
14 - <PDF v-if="url && show && !loadingError" 14 + <PDF v-if="url && show && !loadingError"
15 ref="pdfRef" 15 ref="pdfRef"
16 - :src="url" 16 + :src="url"
17 - :showProgress="true" 17 + :showProgress="true"
18 :progressColor="'#1989fa'" 18 :progressColor="'#1989fa'"
19 - :showPageTooltip="true" 19 + :showPageTooltip="true"
20 - :showBackToTopBtn="true" 20 + :showBackToTopBtn="true"
21 :scrollThreshold="300" 21 :scrollThreshold="300"
22 :pdfWidth="`${Math.round(100 * zoomLevel)}%`" 22 :pdfWidth="`${Math.round(100 * zoomLevel)}%`"
23 - :rowGap="8" 23 + :rowGap="8"
24 - :page="1" 24 + :page="1"
25 :cMapUrl="'https://unpkg.com/pdfjs-dist@3.7.107/cmaps/'" 25 :cMapUrl="'https://unpkg.com/pdfjs-dist@3.7.107/cmaps/'"
26 - :withCredentials="false" 26 + :withCredentials="false"
27 - :useSystemFonts="true" 27 + :useSystemFonts="true"
28 - :stopAtErrors="false" 28 + :stopAtErrors="false"
29 :disableFontFace="false" 29 :disableFontFace="false"
30 - :disableRange="false" 30 + :disableRange="false"
31 - :disableStream="false" 31 + :disableStream="false"
32 - :disableAutoFetch="false" 32 + :disableAutoFetch="false"
33 @onProgress="handleProgress" 33 @onProgress="handleProgress"
34 - @onComplete="handleComplete" 34 + @onComplete="handleComplete"
35 - @onScroll="handleScroll" 35 + @onScroll="handleScroll"
36 @onPageChange="handlePageChange" 36 @onPageChange="handlePageChange"
37 - @onPdfInit="handlePdfInit" 37 + @onPdfInit="handlePdfInit"
38 @onError="handlePdfError" /> 38 @onError="handlePdfError" />
39 39
40 <!-- 错误状态显示 --> 40 <!-- 错误状态显示 -->
...@@ -151,7 +151,7 @@ watch(() => props.show, (newVal) => { ...@@ -151,7 +151,7 @@ watch(() => props.show, (newVal) => {
151 } else if (!newVal) { 151 } else if (!newVal) {
152 // 弹窗关闭时重置状态并清理资源 152 // 弹窗关闭时重置状态并清理资源
153 clearAllTimers(); // 清除计时器 153 clearAllTimers(); // 清除计时器
154 - 154 +
155 // 延迟清理,确保PDF组件有时间完成内部清理 155 // 延迟清理,确保PDF组件有时间完成内部清理
156 setTimeout(() => { 156 setTimeout(() => {
157 loading.value = false; 157 loading.value = false;
...@@ -160,7 +160,7 @@ watch(() => props.show, (newVal) => { ...@@ -160,7 +160,7 @@ watch(() => props.show, (newVal) => {
160 errorMessage.value = ''; 160 errorMessage.value = '';
161 zoomLevel.value = 1; // 重置缩放级别 161 zoomLevel.value = 1; // 重置缩放级别
162 scrollPosition.value = { x: 0, y: 0 }; // 重置滚动位置 162 scrollPosition.value = { x: 0, y: 0 }; // 重置滚动位置
163 - 163 +
164 // 触发关闭销毁事件,通知父组件清理引用 164 // 触发关闭销毁事件,通知父组件清理引用
165 emit('onClose'); 165 emit('onClose');
166 }, 100); // 100ms延迟,给PDF组件时间清理 166 }, 100); // 100ms延迟,给PDF组件时间清理
...@@ -333,7 +333,7 @@ const retryLoad = () => { ...@@ -333,7 +333,7 @@ const retryLoad = () => {
333 const handleClose = () => { 333 const handleClose = () => {
334 // 先清理所有计时器 334 // 先清理所有计时器
335 clearAllTimers(); 335 clearAllTimers();
336 - 336 +
337 // 如果PDF组件存在,尝试清理其内部状态 337 // 如果PDF组件存在,尝试清理其内部状态
338 if (pdfRef.value && typeof pdfRef.value.destroy === 'function') { 338 if (pdfRef.value && typeof pdfRef.value.destroy === 'function') {
339 try { 339 try {
...@@ -342,10 +342,10 @@ const handleClose = () => { ...@@ -342,10 +342,10 @@ const handleClose = () => {
342 console.warn('PDF组件销毁时出现警告:', error); 342 console.warn('PDF组件销毁时出现警告:', error);
343 } 343 }
344 } 344 }
345 - 345 +
346 // 使用nextTick确保在DOM更新前完成清理 346 // 使用nextTick确保在DOM更新前完成清理
347 nextTick(() => { 347 nextTick(() => {
348 - emit('update:show', false); 348 + emit('onClose', true);
349 }); 349 });
350 }; 350 };
351 351
......
...@@ -226,6 +226,14 @@ export const routes = [ ...@@ -226,6 +226,14 @@ export const routes = [
226 } 226 }
227 }, 227 },
228 { 228 {
229 + path: '/pdfPreview',
230 + name: 'PdfPreview',
231 + component: () => import('@/views/study/PdfPreviewPage.vue'),
232 + meta: {
233 + title: 'PDF预览',
234 + }
235 + },
236 + {
229 path: '/profile/studyCourse/:id', 237 path: '/profile/studyCourse/:id',
230 component: () => import('@/views/profile/StudyCoursePage.vue'), 238 component: () => import('@/views/profile/StudyCoursePage.vue'),
231 meta: { 239 meta: {
......
1 +<!--
2 + * @Date: 2025-10-22 10:45:51
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-10-22 10:54:10
5 + * @FilePath: /mlaj/src/views/study/PdfPreviewPage.vue
6 + * @Description: 文件描述
7 +-->
8 +<template>
9 + <div class="pdf-preview-page">
10 + <PdfViewer :show="true" :url="pdfUrl" :title="pdfTitle" @onClose="handleClose" />
11 + </div>
12 +</template>
13 +
14 +<script setup>
15 +import { computed } from 'vue'
16 +import { useRoute, useRouter } from 'vue-router'
17 +import PdfViewer from '@/components/ui/PdfViewer.vue'
18 +
19 +const route = useRoute()
20 +const router = useRouter()
21 +
22 +// 从路由查询参数获取PDF的URL和标题
23 +const pdfUrl = computed(() => {
24 + const url = route.query.url || ''
25 + // 兼容可能存在的编码
26 + return typeof url === 'string' ? decodeURIComponent(url) : ''
27 +})
28 +
29 +const pdfTitle = computed(() => {
30 + const title = route.query.title || ''
31 + return typeof title === 'string' ? decodeURIComponent(title) : ''
32 +})
33 +
34 +const handleClose = () => {
35 + const returnId = route.query.returnId
36 + const openMaterials = route.query.openMaterials
37 + if (returnId) {
38 + // 使用 replace 跳回学习详情页并打开学习资料弹框,避免历史中保留PDF预览
39 + router.replace({ path: `/studyDetail/${returnId}`, query: { openMaterials: openMaterials || '1' } })
40 + } else {
41 + // 无返回ID时,使用 replace 导航到学习页,避免返回进入PDF预览
42 + router.replace({ path: '/study' })
43 + }
44 +}
45 +</script>
46 +
47 +<style scoped>
48 +.pdf-preview-page {
49 + height: 100vh;
50 + width: 100vw;
51 + background: #fff;
52 + display: flex;
53 + flex-direction: column;
54 +}
55 +</style>
...@@ -260,8 +260,7 @@ ...@@ -260,8 +260,7 @@
260 </div> 260 </div>
261 </van-popup> 261 </van-popup>
262 262
263 - <!-- PDF预览 --> 263 + <!-- PDF预览改为独立页面,点击资源时跳转到 /pdfPreview -->
264 - <PdfViewer v-model:show="pdfShow" :url="pdfUrl" :title="pdfTitle" @onLoad="onPdfLoad" @onProgress="onPdfProgress" @onComplete="onPdfComplete" />
265 264
266 <!-- Office 文档预览弹窗 --> 265 <!-- Office 文档预览弹窗 -->
267 <van-popup 266 <van-popup
...@@ -877,7 +876,10 @@ const popupVideoPlayerRef = ref(null); // 弹窗视频播放器引用 ...@@ -877,7 +876,10 @@ const popupVideoPlayerRef = ref(null); // 弹窗视频播放器引用
877 const showPdf = ({ title, url, meta_id }) => { 876 const showPdf = ({ title, url, meta_id }) => {
878 pdfTitle.value = title; 877 pdfTitle.value = title;
879 pdfUrl.value = url; 878 pdfUrl.value = url;
880 - pdfShow.value = true; 879 + // 跳转到PDF预览页面,并带上返回的课程ID和打开资料弹框的标记
880 + const encodedUrl = encodeURIComponent(url);
881 + const encodedTitle = encodeURIComponent(title);
882 + router.replace({ name: 'PdfPreview', query: { url: encodedUrl, title: encodedTitle, returnId: courseId.value, openMaterials: '1' } });
881 // 新增记录 883 // 新增记录
882 let paramsObj = { 884 let paramsObj = {
883 schedule_id: courseId.value, 885 schedule_id: courseId.value,
...@@ -1270,6 +1272,22 @@ const downloadFailInfo = ref({ ...@@ -1270,6 +1272,22 @@ const downloadFailInfo = ref({
1270 // 学习资料弹窗状态 1272 // 学习资料弹窗状态
1271 const showMaterialsPopup = ref(false); 1273 const showMaterialsPopup = ref(false);
1272 1274
1275 +// 路由参数监听:如果openMaterials=1,则打开学习资料弹框
1276 +watch(() => route.query.openMaterials, (val) => {
1277 + if (val === '1' || val === 1 || val === true) {
1278 + showMaterialsPopup.value = true;
1279 + }
1280 +}, { immediate: true });
1281 +
1282 +// 监听弹框关闭:关闭时移除URL中的openMaterials参数,防止刷新再次打开
1283 +watch(showMaterialsPopup, (val, oldVal) => {
1284 + if (oldVal && !val) {
1285 + const newQuery = { ...route.query };
1286 + delete newQuery.openMaterials;
1287 + router.replace({ path: route.path, query: newQuery });
1288 + }
1289 +});
1290 +
1273 /** 1291 /**
1274 * 复制文件地址到剪贴板 1292 * 复制文件地址到剪贴板
1275 * @param {string} url - 要复制的文件地址 1293 * @param {string} url - 要复制的文件地址
......