hookehuyr

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

引入@sunsetglow/vue-pdf-viewer库,实现在学习详情页面中直接预览PDF文件的功能,提升用户体验。移除原有的PDF文件下载链接,改为点击“查看文件”按钮即可在页面内直接预览PDF内容。
......@@ -19,6 +19,7 @@
"@fortawesome/free-solid-svg-icons": "^6.5.1",
"@fortawesome/vue-fontawesome": "^3.0.5",
"@heroicons/vue": "^2.2.0",
"@sunsetglow/vue-pdf-viewer": "^0.3.0",
"@vant/touch-emulator": "^1.4.0",
"@vant/use": "^1.6.0",
"@videojs-player/vue": "^1.0.0",
......
<!--
* @Date: 2025-04-07
* @Description: 学习详情页面
-->
-->
<template>
<div class="study-detail-page bg-gradient-to-b from-green-50/70 to-white/90 min-h-screen pb-20">
<div v-if="course" class="flex flex-col h-screen">
......@@ -22,23 +22,21 @@
</div>
</div>
<!-- 视频播放器 -->
<VideoPlayer v-show="isPlaying" ref="videoPlayerRef" :video-url="courseFile.list.length ? courseFile['list'][0]['url'] : ''" :autoplay="false"
<VideoPlayer v-show="isPlaying" ref="videoPlayerRef"
:video-url="courseFile.list.length ? courseFile['list'][0]['url'] : ''" :autoplay="false"
@onPlay="handleVideoPlay" @onPause="handleVideoPause" />
</div>
<div v-if="course.course_type === 'audio'" class="w-full relative" style="border-bottom: 1px solid #F3F4F6;">
<div v-if="course.course_type === 'audio'" class="w-full relative"
style="border-bottom: 1px solid #F3F4F6;">
<!-- 音频播放器 -->
<AudioPlayer :songs="audioList" />
</div>
<!-- 图片列表展示区域 -->
<div v-if="course.course_type === 'image'" class="w-full relative">
<van-swipe class="w-full" :autoplay="0" :show-indicators="true">
<van-swipe-item v-for="(item, index) in courseFile?.list" :key="index" @click.native="showImagePreview(index)">
<van-image
:src="item.url"
class="w-full"
fit="cover"
style="aspect-ratio: 16/9;"
>
<van-swipe-item v-for="(item, index) in courseFile?.list" :key="index"
@click.native="showImagePreview(index)">
<van-image :src="item.url" class="w-full" fit="cover" style="aspect-ratio: 16/9;">
<template #image>
<img :src="item.url" class="w-full h-full object-cover" />
</template>
......@@ -49,20 +47,14 @@
<!-- 文件列表展示区域 -->
<div v-if="course.course_type === 'file'" class="w-full relative bg-white rounded-lg shadow-sm">
<div class="p-4 space-y-3">
<div v-for="(item, index) in courseFile?.list" :key="index" class="group hover:bg-gray-50 transition-colors rounded-lg p-3">
<div v-for="(item, index) in courseFile?.list" :key="index"
class="group hover:bg-gray-50 transition-colors rounded-lg p-3">
<div class="flex items-center justify-between space-x-3 px-2">
<div class="flex items-center space-x-3 flex-1 min-w-0">
<font-awesome-icon icon="file-alt" class="text-gray-400 text-lg flex-shrink-0" />
<h3 class="text-xs font-medium text-gray-900 truncate">{{ item.title }}</h3>
<h3 class="text-x font-medium text-gray-900 truncate">{{ item.title }}</h3>
</div>
<template v-if="item.url.toLowerCase().endsWith('.pdf')">
<a :href="item.url" download class="text-xs text-blue-600 hover:text-blue-800 hover:underline whitespace-nowrap">
查看文件
</a>
</template>
<template v-else>
<a :href="item.url" target="_blank" class="text-xs text-blue-600 hover:text-blue-800 hover:underline whitespace-nowrap">打开文件</a>
</template>
<div class="text-x text-blue-600 hover:text-blue-800 hover:underline whitespace-nowrap" @click="showPdf(item.url)">查看文件</div>
</div>
</div>
</div>
......@@ -102,8 +94,8 @@
<div v-for="comment in commentList" :key="comment.id"
class="border-b border-gray-100 last:border-b-0 py-4">
<div class="flex">
<img :src="comment.avatar || 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'" class="w-10 h-10 rounded-full flex-shrink-0"
style="margin-right: 0.5rem;" />
<img :src="comment.avatar || 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'"
class="w-10 h-10 rounded-full flex-shrink-0" style="margin-right: 0.5rem;" />
<div class="flex-1 ml-3">
<div class="flex justify-between items-center mb-1">
<span class="font-medium text-gray-900">{{ comment.name }}</span>
......@@ -129,23 +121,20 @@
<!-- 可滚动的评论列表 -->
<div class="flex-1 overflow-y-auto">
<van-list
v-model:loading="popupLoading"
:finished="popupFinished"
finished-text="没有更多评论了"
@load="onPopupLoad"
class="px-4 py-2 pb-16"
>
<van-list v-model:loading="popupLoading" :finished="popupFinished"
finished-text="没有更多评论了" @load="onPopupLoad" class="px-4 py-2 pb-16">
<div v-for="comment in popupCommentList" :key="comment.id"
class="border-b border-gray-100 last:border-b-0 py-4">
<div class="flex">
<img :src="comment.avatar || 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'" class="w-10 h-10 rounded-full flex-shrink-0"
<img :src="comment.avatar || 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'"
class="w-10 h-10 rounded-full flex-shrink-0"
style="margin-right: 0.5rem;" />
<div class="flex-1 ml-3">
<div class="flex justify-between items-center mb-1">
<span class="font-medium text-gray-900">{{ comment.name }}</span>
<div class="flex items-center space-x-1">
<span class="text-sm text-gray-500">{{ comment.like_count }}</span>
<span class="text-sm text-gray-500">{{ comment.like_count
}}</span>
&nbsp;
<van-icon :name="comment.is_like ? 'like' : 'like-o'"
:class="{ 'text-red-500': comment.is_like, 'text-gray-400': !comment.is_like }"
......@@ -154,7 +143,8 @@
</div>
</div>
<p class="text-gray-700 text-sm mb-1">{{ comment.note }}</p>
<div class="text-gray-400 text-xs">{{ formatDate(comment.updated_time) }}</div>
<div class="text-gray-400 text-xs">{{ formatDate(comment.updated_time)
}}</div>
</div>
</div>
</div>
......@@ -196,11 +186,7 @@
</div>
<!-- 图片预览组件 -->
<van-image-preview
v-model:show="showPreview"
:images="previewImages"
:close-on-click-image="false"
>
<van-image-preview v-model:show="showPreview" :images="previewImages" :close-on-click-image="false">
<template #image="{ src, style, onLoad }">
<img :src="src" :style="[{ width: '100%' }, style]" @load="onLoad" />
</template>
......@@ -218,8 +204,7 @@
<!-- 可滚动的目录列表 -->
<div class="flex-1 overflow-y-auto px-4 py-2">
<div v-if="course_lessons.length" class="space-y-4">
<div v-for="(lesson, index) in course_lessons" :key="index"
@click="handleLessonClick(lesson)"
<div v-for="(lesson, index) in course_lessons" :key="index" @click="handleLessonClick(lesson)"
class="bg-white p-4 cursor-pointer hover:bg-gray-50 transition-colors border-b border-gray-200 relative">
<div v-if="lesson.progress > 0 && lesson.progress < 100"
class="absolute top-2 right-2 px-2 py-1 bg-green-100 text-green-600 text-xs rounded">
......@@ -238,6 +223,11 @@
</div>
</div>
</van-popup>
<!-- PDF预览 -->
<van-popup v-model:show="pdfShow" position="right" closeable :style="{ height: '100%', width: '100%' }">
<div id="pdf-container"></div>
</van-popup>
</div>
</template>
......@@ -250,6 +240,9 @@ import AudioPlayer from '@/components/ui/AudioPlayer.vue';
import dayjs from 'dayjs';
import { formatDate } from '@/utils/tools'
import { initPdfView, configPdfApiOptions, configOption } from "@sunsetglow/vue-pdf-viewer";
import "@sunsetglow/vue-pdf-viewer/dist/style.css";
// 导入接口
import { getScheduleCourseAPI, getGroupCommentListAPI, addGroupCommentAPI, addGroupCommentLikeAPI, delGroupCommentLikeAPI, getCourseDetailAPI } from '@/api/course';
......@@ -343,18 +336,6 @@ const commentCount = ref(0);
const commentList = ref([]);
const courseFile = ref({});
// 处理课程切换
// 打开PDF预览
const openPdfViewer = (item) => {
router.push({
name: 'pdf-preview',
query: {
url: item.url,
title: item.title
}
});
};
const handleLessonClick = async (lesson) => {
showCatalog.value = false; // 关闭目录弹窗
isPlaying.value = false; // 重置播放状态
......@@ -377,6 +358,70 @@ const handleLessonClick = async (lesson) => {
}
};
const loading = ref(false);
const pdfPath = new URL("@sunsetglow/vue-pdf-viewer/dist/libs/pdf.worker.min.js", import.meta.url).href;
const pdfShow = ref(false);
const showPdf = (url) => {
pdfShow.value = true;
// 初始化pdf
setTimeout(() => {
nextTick(() => {
loading.value = true;
initPdfView(document.querySelector("#pdf-container"), {
loadFileUrl: url, //文件路径
pdfPath: pdfPath, // pdf.js 里需要指定的文件路径
loading: (load, fileInfo) => {
loading.value = load;
console.log(`pdf 文件总数:${fileInfo.totalPage}`);
//加载完成会返回 false
configPdfApiOptions.onSearch("", false);
},
pdfOption: {
// search: true, // 搜索 开启搜索必须开启textLayer 为true
scale: true, //缩放
pdfImageView: false, //pdf 是否可以单片点击预览
page: true, //分页查看
navShow: true, //左侧导航
navigationShow: false, // 左侧导航是否开启
pdfViewResize: true, // 是否开启resize 函数 确保pdf 根据可视窗口缩放大小
toolShow: true, // 是否开启顶部导航
download: true, //下载
clearScale: 1.5, // 清晰度 默认1.5 感觉不清晰调大 ,当然清晰度越高pdf生成性能有影响
fileName: "preview.pdf", // pdf 下载文件名称
lang: "en", //字典语言
print: true, //打印功能
customPdfOption: {
// 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。可解决字体加载错误
},
textLayer: true, //文本是否可复制 , 文本复制和点击查看大图冲突建议把 pdfImageView 改为false
pageOption: {
current: 1, //当前页码
},
renderTotalPage: 5, //是否渲染指定页面总数,-1 则默认渲染文件总数,如果传5 则渲染前五页
// 不传默认是 0.5
visibleWindowPageRatio: 0.5, //当前pdf页面在可视窗口多少比例触发分页 传入0.5 就是 (pdf下一页滚动到容器高度一半的时候 更新当前页码)
containerWidthScale: 0.97, //pdf 文件占父元素容器width的比例 默认是0.8
pdfItemBackgroundColor: "#fff", //pdf 加载时背景颜色 默认#ebebeb
watermarkOptions: {
//水印功能
columns: 3, //列数量
rows: 4, // 行数量
color: "#2f7a54", //字体颜色
rotation: 25, //旋转角度
fontSize: 40, //字体大小
opacity: 0.4, //调整透明度
// watermarkTextList: ["第一行", "第二行", "第三行"], //水印文字和 watermarkLink 冲突,只能展示一个水印内容
// watermarkLink: "https://xxx.png", //水印可以支持公司logo(图片路径)
}, // 不展示水印传 undefined即可
},
})
});
}, 1000);
}
onMounted(async () => {
// 延迟设置topWrapper和bottomWrapper的高度
setTimeout(() => {
......@@ -424,6 +469,8 @@ onMounted(async () => {
}
}
})
// 提交评论
......@@ -599,4 +646,12 @@ watch(showCommentPopup, async (newVal) => {
:deep(.van-cell.van-field) {
background-color: rgb(244, 244, 244);
}
#pdf-container {
margin-top: 3rem;
// 高度100%-3rem
height: calc(100% - 3rem);
width: 100%;
padding: 0px;
}
</style>
......
This diff could not be displayed because it is too large.