hookehuyr

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

将PDF预览从弹窗改为独立页面,提升大文件加载体验
添加返回课程详情页功能并保持学习资料弹窗状态
优化PDF组件关闭逻辑和错误处理
<!--
* @Date: 2025-01-21
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-10-22 09:57:57
* @LastEditTime: 2025-10-22 10:49:55
* @FilePath: /mlaj/src/components/ui/PdfViewer.vue
* @Description: PDF预览组件 - 使用pdf-vue3库
-->
......@@ -11,30 +11,30 @@
<div class="pdf-viewer-container">
<!-- PDF内容区域 -->
<div class="pdf-content" :class="{ 'pdf-no-select': preventSave }">
<PDF v-if="url && show && !loadingError"
<PDF v-if="url && show && !loadingError"
ref="pdfRef"
:src="url"
:showProgress="true"
:src="url"
:showProgress="true"
:progressColor="'#1989fa'"
:showPageTooltip="true"
:showBackToTopBtn="true"
:showPageTooltip="true"
:showBackToTopBtn="true"
:scrollThreshold="300"
:pdfWidth="`${Math.round(100 * zoomLevel)}%`"
:rowGap="8"
:page="1"
:rowGap="8"
:page="1"
:cMapUrl="'https://unpkg.com/pdfjs-dist@3.7.107/cmaps/'"
:withCredentials="false"
:useSystemFonts="true"
:stopAtErrors="false"
:withCredentials="false"
:useSystemFonts="true"
:stopAtErrors="false"
:disableFontFace="false"
:disableRange="false"
:disableStream="false"
:disableAutoFetch="false"
:disableRange="false"
:disableStream="false"
:disableAutoFetch="false"
@onProgress="handleProgress"
@onComplete="handleComplete"
@onScroll="handleScroll"
@onComplete="handleComplete"
@onScroll="handleScroll"
@onPageChange="handlePageChange"
@onPdfInit="handlePdfInit"
@onPdfInit="handlePdfInit"
@onError="handlePdfError" />
<!-- 错误状态显示 -->
......@@ -151,7 +151,7 @@ watch(() => props.show, (newVal) => {
} else if (!newVal) {
// 弹窗关闭时重置状态并清理资源
clearAllTimers(); // 清除计时器
// 延迟清理,确保PDF组件有时间完成内部清理
setTimeout(() => {
loading.value = false;
......@@ -160,7 +160,7 @@ watch(() => props.show, (newVal) => {
errorMessage.value = '';
zoomLevel.value = 1; // 重置缩放级别
scrollPosition.value = { x: 0, y: 0 }; // 重置滚动位置
// 触发关闭销毁事件,通知父组件清理引用
emit('onClose');
}, 100); // 100ms延迟,给PDF组件时间清理
......@@ -333,7 +333,7 @@ const retryLoad = () => {
const handleClose = () => {
// 先清理所有计时器
clearAllTimers();
// 如果PDF组件存在,尝试清理其内部状态
if (pdfRef.value && typeof pdfRef.value.destroy === 'function') {
try {
......@@ -342,10 +342,10 @@ const handleClose = () => {
console.warn('PDF组件销毁时出现警告:', error);
}
}
// 使用nextTick确保在DOM更新前完成清理
nextTick(() => {
emit('update:show', false);
emit('onClose', true);
});
};
......
......@@ -226,6 +226,14 @@ export const routes = [
}
},
{
path: '/pdfPreview',
name: 'PdfPreview',
component: () => import('@/views/study/PdfPreviewPage.vue'),
meta: {
title: 'PDF预览',
}
},
{
path: '/profile/studyCourse/:id',
component: () => import('@/views/profile/StudyCoursePage.vue'),
meta: {
......
<!--
* @Date: 2025-10-22 10:45:51
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-10-22 10:54:10
* @FilePath: /mlaj/src/views/study/PdfPreviewPage.vue
* @Description: 文件描述
-->
<template>
<div class="pdf-preview-page">
<PdfViewer :show="true" :url="pdfUrl" :title="pdfTitle" @onClose="handleClose" />
</div>
</template>
<script setup>
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import PdfViewer from '@/components/ui/PdfViewer.vue'
const route = useRoute()
const router = useRouter()
// 从路由查询参数获取PDF的URL和标题
const pdfUrl = computed(() => {
const url = route.query.url || ''
// 兼容可能存在的编码
return typeof url === 'string' ? decodeURIComponent(url) : ''
})
const pdfTitle = computed(() => {
const title = route.query.title || ''
return typeof title === 'string' ? decodeURIComponent(title) : ''
})
const handleClose = () => {
const returnId = route.query.returnId
const openMaterials = route.query.openMaterials
if (returnId) {
// 使用 replace 跳回学习详情页并打开学习资料弹框,避免历史中保留PDF预览
router.replace({ path: `/studyDetail/${returnId}`, query: { openMaterials: openMaterials || '1' } })
} else {
// 无返回ID时,使用 replace 导航到学习页,避免返回进入PDF预览
router.replace({ path: '/study' })
}
}
</script>
<style scoped>
.pdf-preview-page {
height: 100vh;
width: 100vw;
background: #fff;
display: flex;
flex-direction: column;
}
</style>
......@@ -260,8 +260,7 @@
</div>
</van-popup>
<!-- PDF预览 -->
<PdfViewer v-model:show="pdfShow" :url="pdfUrl" :title="pdfTitle" @onLoad="onPdfLoad" @onProgress="onPdfProgress" @onComplete="onPdfComplete" />
<!-- PDF预览改为独立页面,点击资源时跳转到 /pdfPreview -->
<!-- Office 文档预览弹窗 -->
<van-popup
......@@ -877,7 +876,10 @@ const popupVideoPlayerRef = ref(null); // 弹窗视频播放器引用
const showPdf = ({ title, url, meta_id }) => {
pdfTitle.value = title;
pdfUrl.value = url;
pdfShow.value = true;
// 跳转到PDF预览页面,并带上返回的课程ID和打开资料弹框的标记
const encodedUrl = encodeURIComponent(url);
const encodedTitle = encodeURIComponent(title);
router.replace({ name: 'PdfPreview', query: { url: encodedUrl, title: encodedTitle, returnId: courseId.value, openMaterials: '1' } });
// 新增记录
let paramsObj = {
schedule_id: courseId.value,
......@@ -1270,6 +1272,22 @@ const downloadFailInfo = ref({
// 学习资料弹窗状态
const showMaterialsPopup = ref(false);
// 路由参数监听:如果openMaterials=1,则打开学习资料弹框
watch(() => route.query.openMaterials, (val) => {
if (val === '1' || val === 1 || val === true) {
showMaterialsPopup.value = true;
}
}, { immediate: true });
// 监听弹框关闭:关闭时移除URL中的openMaterials参数,防止刷新再次打开
watch(showMaterialsPopup, (val, oldVal) => {
if (oldVal && !val) {
const newQuery = { ...route.query };
delete newQuery.openMaterials;
router.replace({ path: route.path, query: newQuery });
}
});
/**
* 复制文件地址到剪贴板
* @param {string} url - 要复制的文件地址
......