hookehuyr

feat(StudyDetailPage): 添加PDF预览功能

引入@sunsetglow/vue-pdf-viewer库,实现在学习详情页面中直接预览PDF文件的功能,提升用户体验。移除原有的PDF文件下载链接,改为点击“查看文件”按钮即可在页面内直接预览PDF内容。
...@@ -19,6 +19,7 @@ ...@@ -19,6 +19,7 @@
19 "@fortawesome/free-solid-svg-icons": "^6.5.1", 19 "@fortawesome/free-solid-svg-icons": "^6.5.1",
20 "@fortawesome/vue-fontawesome": "^3.0.5", 20 "@fortawesome/vue-fontawesome": "^3.0.5",
21 "@heroicons/vue": "^2.2.0", 21 "@heroicons/vue": "^2.2.0",
22 + "@sunsetglow/vue-pdf-viewer": "^0.3.0",
22 "@vant/touch-emulator": "^1.4.0", 23 "@vant/touch-emulator": "^1.4.0",
23 "@vant/use": "^1.6.0", 24 "@vant/use": "^1.6.0",
24 "@videojs-player/vue": "^1.0.0", 25 "@videojs-player/vue": "^1.0.0",
......
1 <!-- 1 <!--
2 - * @Date: 2025-04-07 2 + * @Date: 2025-04-07
3 - * @Description: 学习详情页面 3 + * @Description: 学习详情页面
4 ---> 4 + -->
5 <template> 5 <template>
6 <div class="study-detail-page bg-gradient-to-b from-green-50/70 to-white/90 min-h-screen pb-20"> 6 <div class="study-detail-page bg-gradient-to-b from-green-50/70 to-white/90 min-h-screen pb-20">
7 <div v-if="course" class="flex flex-col h-screen"> 7 <div v-if="course" class="flex flex-col h-screen">
...@@ -22,23 +22,21 @@ ...@@ -22,23 +22,21 @@
22 </div> 22 </div>
23 </div> 23 </div>
24 <!-- 视频播放器 --> 24 <!-- 视频播放器 -->
25 - <VideoPlayer v-show="isPlaying" ref="videoPlayerRef" :video-url="courseFile.list.length ? courseFile['list'][0]['url'] : ''" :autoplay="false" 25 + <VideoPlayer v-show="isPlaying" ref="videoPlayerRef"
26 + :video-url="courseFile.list.length ? courseFile['list'][0]['url'] : ''" :autoplay="false"
26 @onPlay="handleVideoPlay" @onPause="handleVideoPause" /> 27 @onPlay="handleVideoPlay" @onPause="handleVideoPause" />
27 </div> 28 </div>
28 - <div v-if="course.course_type === 'audio'" class="w-full relative" style="border-bottom: 1px solid #F3F4F6;"> 29 + <div v-if="course.course_type === 'audio'" class="w-full relative"
30 + style="border-bottom: 1px solid #F3F4F6;">
29 <!-- 音频播放器 --> 31 <!-- 音频播放器 -->
30 <AudioPlayer :songs="audioList" /> 32 <AudioPlayer :songs="audioList" />
31 </div> 33 </div>
32 <!-- 图片列表展示区域 --> 34 <!-- 图片列表展示区域 -->
33 <div v-if="course.course_type === 'image'" class="w-full relative"> 35 <div v-if="course.course_type === 'image'" class="w-full relative">
34 <van-swipe class="w-full" :autoplay="0" :show-indicators="true"> 36 <van-swipe class="w-full" :autoplay="0" :show-indicators="true">
35 - <van-swipe-item v-for="(item, index) in courseFile?.list" :key="index" @click.native="showImagePreview(index)"> 37 + <van-swipe-item v-for="(item, index) in courseFile?.list" :key="index"
36 - <van-image 38 + @click.native="showImagePreview(index)">
37 - :src="item.url" 39 + <van-image :src="item.url" class="w-full" fit="cover" style="aspect-ratio: 16/9;">
38 - class="w-full"
39 - fit="cover"
40 - style="aspect-ratio: 16/9;"
41 - >
42 <template #image> 40 <template #image>
43 <img :src="item.url" class="w-full h-full object-cover" /> 41 <img :src="item.url" class="w-full h-full object-cover" />
44 </template> 42 </template>
...@@ -49,20 +47,14 @@ ...@@ -49,20 +47,14 @@
49 <!-- 文件列表展示区域 --> 47 <!-- 文件列表展示区域 -->
50 <div v-if="course.course_type === 'file'" class="w-full relative bg-white rounded-lg shadow-sm"> 48 <div v-if="course.course_type === 'file'" class="w-full relative bg-white rounded-lg shadow-sm">
51 <div class="p-4 space-y-3"> 49 <div class="p-4 space-y-3">
52 - <div v-for="(item, index) in courseFile?.list" :key="index" class="group hover:bg-gray-50 transition-colors rounded-lg p-3"> 50 + <div v-for="(item, index) in courseFile?.list" :key="index"
51 + class="group hover:bg-gray-50 transition-colors rounded-lg p-3">
53 <div class="flex items-center justify-between space-x-3 px-2"> 52 <div class="flex items-center justify-between space-x-3 px-2">
54 <div class="flex items-center space-x-3 flex-1 min-w-0"> 53 <div class="flex items-center space-x-3 flex-1 min-w-0">
55 <font-awesome-icon icon="file-alt" class="text-gray-400 text-lg flex-shrink-0" /> 54 <font-awesome-icon icon="file-alt" class="text-gray-400 text-lg flex-shrink-0" />
56 - <h3 class="text-xs font-medium text-gray-900 truncate">{{ item.title }}</h3> 55 + <h3 class="text-x font-medium text-gray-900 truncate">{{ item.title }}</h3>
57 </div> 56 </div>
58 - <template v-if="item.url.toLowerCase().endsWith('.pdf')"> 57 + <div class="text-x text-blue-600 hover:text-blue-800 hover:underline whitespace-nowrap" @click="showPdf(item.url)">查看文件</div>
59 - <a :href="item.url" download class="text-xs text-blue-600 hover:text-blue-800 hover:underline whitespace-nowrap">
60 - 查看文件
61 - </a>
62 - </template>
63 - <template v-else>
64 - <a :href="item.url" target="_blank" class="text-xs text-blue-600 hover:text-blue-800 hover:underline whitespace-nowrap">打开文件</a>
65 - </template>
66 </div> 58 </div>
67 </div> 59 </div>
68 </div> 60 </div>
...@@ -102,8 +94,8 @@ ...@@ -102,8 +94,8 @@
102 <div v-for="comment in commentList" :key="comment.id" 94 <div v-for="comment in commentList" :key="comment.id"
103 class="border-b border-gray-100 last:border-b-0 py-4"> 95 class="border-b border-gray-100 last:border-b-0 py-4">
104 <div class="flex"> 96 <div class="flex">
105 - <img :src="comment.avatar || 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'" class="w-10 h-10 rounded-full flex-shrink-0" 97 + <img :src="comment.avatar || 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'"
106 - style="margin-right: 0.5rem;" /> 98 + class="w-10 h-10 rounded-full flex-shrink-0" style="margin-right: 0.5rem;" />
107 <div class="flex-1 ml-3"> 99 <div class="flex-1 ml-3">
108 <div class="flex justify-between items-center mb-1"> 100 <div class="flex justify-between items-center mb-1">
109 <span class="font-medium text-gray-900">{{ comment.name }}</span> 101 <span class="font-medium text-gray-900">{{ comment.name }}</span>
...@@ -129,23 +121,20 @@ ...@@ -129,23 +121,20 @@
129 121
130 <!-- 可滚动的评论列表 --> 122 <!-- 可滚动的评论列表 -->
131 <div class="flex-1 overflow-y-auto"> 123 <div class="flex-1 overflow-y-auto">
132 - <van-list 124 + <van-list v-model:loading="popupLoading" :finished="popupFinished"
133 - v-model:loading="popupLoading" 125 + finished-text="没有更多评论了" @load="onPopupLoad" class="px-4 py-2 pb-16">
134 - :finished="popupFinished"
135 - finished-text="没有更多评论了"
136 - @load="onPopupLoad"
137 - class="px-4 py-2 pb-16"
138 - >
139 <div v-for="comment in popupCommentList" :key="comment.id" 126 <div v-for="comment in popupCommentList" :key="comment.id"
140 class="border-b border-gray-100 last:border-b-0 py-4"> 127 class="border-b border-gray-100 last:border-b-0 py-4">
141 <div class="flex"> 128 <div class="flex">
142 - <img :src="comment.avatar || 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'" class="w-10 h-10 rounded-full flex-shrink-0" 129 + <img :src="comment.avatar || 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'"
143 - style="margin-right: 0.5rem;" /> 130 + class="w-10 h-10 rounded-full flex-shrink-0"
131 + style="margin-right: 0.5rem;" />
144 <div class="flex-1 ml-3"> 132 <div class="flex-1 ml-3">
145 <div class="flex justify-between items-center mb-1"> 133 <div class="flex justify-between items-center mb-1">
146 <span class="font-medium text-gray-900">{{ comment.name }}</span> 134 <span class="font-medium text-gray-900">{{ comment.name }}</span>
147 <div class="flex items-center space-x-1"> 135 <div class="flex items-center space-x-1">
148 - <span class="text-sm text-gray-500">{{ comment.like_count }}</span> 136 + <span class="text-sm text-gray-500">{{ comment.like_count
137 + }}</span>
149 &nbsp; 138 &nbsp;
150 <van-icon :name="comment.is_like ? 'like' : 'like-o'" 139 <van-icon :name="comment.is_like ? 'like' : 'like-o'"
151 :class="{ 'text-red-500': comment.is_like, 'text-gray-400': !comment.is_like }" 140 :class="{ 'text-red-500': comment.is_like, 'text-gray-400': !comment.is_like }"
...@@ -154,7 +143,8 @@ ...@@ -154,7 +143,8 @@
154 </div> 143 </div>
155 </div> 144 </div>
156 <p class="text-gray-700 text-sm mb-1">{{ comment.note }}</p> 145 <p class="text-gray-700 text-sm mb-1">{{ comment.note }}</p>
157 - <div class="text-gray-400 text-xs">{{ formatDate(comment.updated_time) }}</div> 146 + <div class="text-gray-400 text-xs">{{ formatDate(comment.updated_time)
147 + }}</div>
158 </div> 148 </div>
159 </div> 149 </div>
160 </div> 150 </div>
...@@ -196,11 +186,7 @@ ...@@ -196,11 +186,7 @@
196 </div> 186 </div>
197 187
198 <!-- 图片预览组件 --> 188 <!-- 图片预览组件 -->
199 - <van-image-preview 189 + <van-image-preview v-model:show="showPreview" :images="previewImages" :close-on-click-image="false">
200 - v-model:show="showPreview"
201 - :images="previewImages"
202 - :close-on-click-image="false"
203 - >
204 <template #image="{ src, style, onLoad }"> 190 <template #image="{ src, style, onLoad }">
205 <img :src="src" :style="[{ width: '100%' }, style]" @load="onLoad" /> 191 <img :src="src" :style="[{ width: '100%' }, style]" @load="onLoad" />
206 </template> 192 </template>
...@@ -218,8 +204,7 @@ ...@@ -218,8 +204,7 @@
218 <!-- 可滚动的目录列表 --> 204 <!-- 可滚动的目录列表 -->
219 <div class="flex-1 overflow-y-auto px-4 py-2"> 205 <div class="flex-1 overflow-y-auto px-4 py-2">
220 <div v-if="course_lessons.length" class="space-y-4"> 206 <div v-if="course_lessons.length" class="space-y-4">
221 - <div v-for="(lesson, index) in course_lessons" :key="index" 207 + <div v-for="(lesson, index) in course_lessons" :key="index" @click="handleLessonClick(lesson)"
222 - @click="handleLessonClick(lesson)"
223 class="bg-white p-4 cursor-pointer hover:bg-gray-50 transition-colors border-b border-gray-200 relative"> 208 class="bg-white p-4 cursor-pointer hover:bg-gray-50 transition-colors border-b border-gray-200 relative">
224 <div v-if="lesson.progress > 0 && lesson.progress < 100" 209 <div v-if="lesson.progress > 0 && lesson.progress < 100"
225 class="absolute top-2 right-2 px-2 py-1 bg-green-100 text-green-600 text-xs rounded"> 210 class="absolute top-2 right-2 px-2 py-1 bg-green-100 text-green-600 text-xs rounded">
...@@ -238,6 +223,11 @@ ...@@ -238,6 +223,11 @@
238 </div> 223 </div>
239 </div> 224 </div>
240 </van-popup> 225 </van-popup>
226 +
227 + <!-- PDF预览 -->
228 + <van-popup v-model:show="pdfShow" position="right" closeable :style="{ height: '100%', width: '100%' }">
229 + <div id="pdf-container"></div>
230 + </van-popup>
241 </div> 231 </div>
242 </template> 232 </template>
243 233
...@@ -250,6 +240,9 @@ import AudioPlayer from '@/components/ui/AudioPlayer.vue'; ...@@ -250,6 +240,9 @@ import AudioPlayer from '@/components/ui/AudioPlayer.vue';
250 import dayjs from 'dayjs'; 240 import dayjs from 'dayjs';
251 import { formatDate } from '@/utils/tools' 241 import { formatDate } from '@/utils/tools'
252 242
243 +import { initPdfView, configPdfApiOptions, configOption } from "@sunsetglow/vue-pdf-viewer";
244 +import "@sunsetglow/vue-pdf-viewer/dist/style.css";
245 +
253 // 导入接口 246 // 导入接口
254 import { getScheduleCourseAPI, getGroupCommentListAPI, addGroupCommentAPI, addGroupCommentLikeAPI, delGroupCommentLikeAPI, getCourseDetailAPI } from '@/api/course'; 247 import { getScheduleCourseAPI, getGroupCommentListAPI, addGroupCommentAPI, addGroupCommentLikeAPI, delGroupCommentLikeAPI, getCourseDetailAPI } from '@/api/course';
255 248
...@@ -343,18 +336,6 @@ const commentCount = ref(0); ...@@ -343,18 +336,6 @@ const commentCount = ref(0);
343 const commentList = ref([]); 336 const commentList = ref([]);
344 const courseFile = ref({}); 337 const courseFile = ref({});
345 338
346 -// 处理课程切换
347 -// 打开PDF预览
348 -const openPdfViewer = (item) => {
349 - router.push({
350 - name: 'pdf-preview',
351 - query: {
352 - url: item.url,
353 - title: item.title
354 - }
355 - });
356 -};
357 -
358 const handleLessonClick = async (lesson) => { 339 const handleLessonClick = async (lesson) => {
359 showCatalog.value = false; // 关闭目录弹窗 340 showCatalog.value = false; // 关闭目录弹窗
360 isPlaying.value = false; // 重置播放状态 341 isPlaying.value = false; // 重置播放状态
...@@ -377,6 +358,70 @@ const handleLessonClick = async (lesson) => { ...@@ -377,6 +358,70 @@ const handleLessonClick = async (lesson) => {
377 } 358 }
378 }; 359 };
379 360
361 +const loading = ref(false);
362 +const pdfPath = new URL("@sunsetglow/vue-pdf-viewer/dist/libs/pdf.worker.min.js", import.meta.url).href;
363 +const pdfShow = ref(false);
364 +
365 +const showPdf = (url) => {
366 + pdfShow.value = true;
367 + // 初始化pdf
368 + setTimeout(() => {
369 + nextTick(() => {
370 + loading.value = true;
371 + initPdfView(document.querySelector("#pdf-container"), {
372 + loadFileUrl: url, //文件路径
373 + pdfPath: pdfPath, // pdf.js 里需要指定的文件路径
374 + loading: (load, fileInfo) => {
375 + loading.value = load;
376 + console.log(`pdf 文件总数:${fileInfo.totalPage}`);
377 + //加载完成会返回 false
378 + configPdfApiOptions.onSearch("", false);
379 + },
380 + pdfOption: {
381 + // search: true, // 搜索 开启搜索必须开启textLayer 为true
382 + scale: true, //缩放
383 + pdfImageView: false, //pdf 是否可以单片点击预览
384 + page: true, //分页查看
385 + navShow: true, //左侧导航
386 + navigationShow: false, // 左侧导航是否开启
387 + pdfViewResize: true, // 是否开启resize 函数 确保pdf 根据可视窗口缩放大小
388 + toolShow: true, // 是否开启顶部导航
389 + download: true, //下载
390 + clearScale: 1.5, // 清晰度 默认1.5 感觉不清晰调大 ,当然清晰度越高pdf生成性能有影响
391 + fileName: "preview.pdf", // pdf 下载文件名称
392 + lang: "en", //字典语言
393 + print: true, //打印功能
394 + customPdfOption: {
395 + // customPdfOption是 pdfjs getDocument 函数中一些配置参数 具体可参考 https://mozilla.github.io/pdf.js/api/draft/module-pdfjsLib.html#~DocumentInitParameters
396 + cMapPacked: true, //指定 CMap 是否是二进制打包的
397 + cMapUrl: "https://cdn.jsdelivr.net/npm/pdfjs-dist@2.2.228/cmaps/", //预定义 Adob​​e CMaps 所在的 URL。可解决字体加载错误
398 + },
399 + textLayer: true, //文本是否可复制 , 文本复制和点击查看大图冲突建议把 pdfImageView 改为false
400 + pageOption: {
401 + current: 1, //当前页码
402 + },
403 + renderTotalPage: 5, //是否渲染指定页面总数,-1 则默认渲染文件总数,如果传5 则渲染前五页
404 + // 不传默认是 0.5
405 + visibleWindowPageRatio: 0.5, //当前pdf页面在可视窗口多少比例触发分页 传入0.5 就是 (pdf下一页滚动到容器高度一半的时候 更新当前页码)
406 + containerWidthScale: 0.97, //pdf 文件占父元素容器width的比例 默认是0.8
407 + pdfItemBackgroundColor: "#fff", //pdf 加载时背景颜色 默认#ebebeb
408 + watermarkOptions: {
409 + //水印功能
410 + columns: 3, //列数量
411 + rows: 4, // 行数量
412 + color: "#2f7a54", //字体颜色
413 + rotation: 25, //旋转角度
414 + fontSize: 40, //字体大小
415 + opacity: 0.4, //调整透明度
416 + // watermarkTextList: ["第一行", "第二行", "第三行"], //水印文字和 watermarkLink 冲突,只能展示一个水印内容
417 + // watermarkLink: "https://xxx.png", //水印可以支持公司logo(图片路径)
418 + }, // 不展示水印传 undefined即可
419 + },
420 + })
421 + });
422 + }, 1000);
423 +}
424 +
380 onMounted(async () => { 425 onMounted(async () => {
381 // 延迟设置topWrapper和bottomWrapper的高度 426 // 延迟设置topWrapper和bottomWrapper的高度
382 setTimeout(() => { 427 setTimeout(() => {
...@@ -424,6 +469,8 @@ onMounted(async () => { ...@@ -424,6 +469,8 @@ onMounted(async () => {
424 } 469 }
425 470
426 } 471 }
472 +
473 +
427 }) 474 })
428 475
429 // 提交评论 476 // 提交评论
...@@ -599,4 +646,12 @@ watch(showCommentPopup, async (newVal) => { ...@@ -599,4 +646,12 @@ watch(showCommentPopup, async (newVal) => {
599 :deep(.van-cell.van-field) { 646 :deep(.van-cell.van-field) {
600 background-color: rgb(244, 244, 244); 647 background-color: rgb(244, 244, 244);
601 } 648 }
649 +
650 +#pdf-container {
651 + margin-top: 3rem;
652 + // 高度100%-3rem
653 + height: calc(100% - 3rem);
654 + width: 100%;
655 + padding: 0px;
656 +}
602 </style> 657 </style>
......
This diff could not be displayed because it is too large.