hookehuyr

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

引入vue-pdf-embed库,新增PDF预览页面,支持在课程详情页中直接预览PDF文件。添加了新的路由和组件,并更新了相关依赖。
......@@ -22,6 +22,7 @@
"vconsole": "^3.15.1",
"video.js": "^7.21.7",
"vue": "^3.5.13",
"vue-pdf-embed": "^2.1.2",
"vue-router": "^4.5.0",
"weixin-js-sdk": "^1.6.5"
},
......@@ -947,6 +948,177 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@napi-rs/canvas": {
"version": "0.1.70",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.70.tgz",
"integrity": "sha512-nD6NGa4JbNYSZYsTnLGrqe9Kn/lCkA4ybXt8sx5ojDqZjr2i0TWAHxx/vhgfjX+i3hCdKWufxYwi7CfXqtITSA==",
"optional": true,
"engines": {
"node": ">= 10"
},
"optionalDependencies": {
"@napi-rs/canvas-android-arm64": "0.1.70",
"@napi-rs/canvas-darwin-arm64": "0.1.70",
"@napi-rs/canvas-darwin-x64": "0.1.70",
"@napi-rs/canvas-linux-arm-gnueabihf": "0.1.70",
"@napi-rs/canvas-linux-arm64-gnu": "0.1.70",
"@napi-rs/canvas-linux-arm64-musl": "0.1.70",
"@napi-rs/canvas-linux-riscv64-gnu": "0.1.70",
"@napi-rs/canvas-linux-x64-gnu": "0.1.70",
"@napi-rs/canvas-linux-x64-musl": "0.1.70",
"@napi-rs/canvas-win32-x64-msvc": "0.1.70"
}
},
"node_modules/@napi-rs/canvas-android-arm64": {
"version": "0.1.70",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.70.tgz",
"integrity": "sha512-I/YOuQ0wbkVYxVaYtCgN42WKTYxNqFA0gTcTrHIGG1jfpDSyZWII/uHcjOo4nzd19io6Y4+/BqP8E5hJgf9OmQ==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-darwin-arm64": {
"version": "0.1.70",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.70.tgz",
"integrity": "sha512-4pPGyXetHIHkw2TOJHujt3mkCP8LdDu8+CT15ld9Id39c752RcI0amDHSuMLMQfAjvusA9B5kKxazwjMGjEJpQ==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-darwin-x64": {
"version": "0.1.70",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.70.tgz",
"integrity": "sha512-+2N6Os9LbkmDMHL+raknrUcLQhsXzc5CSXRbXws9C3pv/mjHRVszQ9dhFUUe9FjfPhCJznO6USVdwOtu7pOrzQ==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
"version": "0.1.70",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.70.tgz",
"integrity": "sha512-QjscX9OaKq/990sVhSMj581xuqLgiaPVMjjYvWaCmAJRkNQ004QfoSMEm3FoTqM4DRoquP8jvuEXScVJsc1rqQ==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-linux-arm64-gnu": {
"version": "0.1.70",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.70.tgz",
"integrity": "sha512-LNakMOwwqwiHIwMpnMAbFRczQMQ7TkkMyATqFCOtUJNlE6LPP/QiUj/mlFrNbUn/hctqShJ60gWEb52ZTALbVw==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-linux-arm64-musl": {
"version": "0.1.70",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.70.tgz",
"integrity": "sha512-wBTOllEYNfJCHOdZj9v8gLzZ4oY3oyPX8MSRvaxPm/s7RfEXxCyZ8OhJ5xAyicsDdbE5YBZqdmaaeP5+xKxvtg==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
"version": "0.1.70",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.70.tgz",
"integrity": "sha512-GVUUPC8TuuFqHip0rxHkUqArQnlzmlXmTEBuXAWdgCv85zTCFH8nOHk/YCF5yo0Z2eOm8nOi90aWs0leJ4OE5Q==",
"cpu": [
"riscv64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-linux-x64-gnu": {
"version": "0.1.70",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.70.tgz",
"integrity": "sha512-/kvUa2lZRwGNyfznSn5t1ShWJnr/m5acSlhTV3eXECafObjl0VBuA1HJw0QrilLpb4Fe0VLywkpD1NsMoVDROQ==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-linux-x64-musl": {
"version": "0.1.70",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.70.tgz",
"integrity": "sha512-aqlv8MLpycoMKRmds7JWCfVwNf1fiZxaU7JwJs9/ExjTD8lX2KjsO7CTeAj5Cl4aEuzxUWbJPUUE2Qu9cZ1vfg==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-win32-x64-msvc": {
"version": "0.1.70",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.70.tgz",
"integrity": "sha512-Q9QU3WIpwBTVHk4cPfBjGHGU4U0llQYRXgJtFtYqqGNEOKVN4OT6PQ+ve63xwIPODMpZ0HHyj/KLGc9CWc3EtQ==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
......@@ -3098,6 +3270,17 @@
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
"dev": true
},
"node_modules/pdfjs-dist": {
"version": "4.10.38",
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-4.10.38.tgz",
"integrity": "sha512-/Y3fcFrXEAsMjJXeL9J8+ZG9U01LbuWaYypvDW2ycW1jL269L3js3DVBjDJ0Up9Np1uqDXsDrRihHANhZOlwdQ==",
"engines": {
"node": ">=20"
},
"optionalDependencies": {
"@napi-rs/canvas": "^0.1.65"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
......@@ -4327,6 +4510,17 @@
}
}
},
"node_modules/vue-pdf-embed": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/vue-pdf-embed/-/vue-pdf-embed-2.1.2.tgz",
"integrity": "sha512-/j++oknFBY9x/MgEFBo9tSuOXS0Z9COlywwLhMREhiGfmuQqpnGy5T+SwVIXxR1tmdzM/lHog8JL7HOAgXT1aw==",
"dependencies": {
"pdfjs-dist": "^4.10.38"
},
"peerDependencies": {
"vue": "^3.3.0"
}
},
"node_modules/vue-router": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.0.tgz",
......
......@@ -29,6 +29,7 @@
"vconsole": "^3.15.1",
"video.js": "^7.21.7",
"vue": "^3.5.13",
"vue-pdf-embed": "^2.1.2",
"vue-router": "^4.5.0",
"weixin-js-sdk": "^1.6.5"
},
......
......@@ -20,6 +20,7 @@ declare module 'vue' {
GradientHeader: typeof import('./components/ui/GradientHeader.vue')['default']
LiveStreamCard: typeof import('./components/ui/LiveStreamCard.vue')['default']
MenuItem: typeof import('./components/ui/MenuItem.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']
......
/*
* @Date: 2025-03-20 20:36:36
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-05-08 10:56:28
* @LastEditTime: 2025-05-08 11:14:46
* @FilePath: /mlaj/src/main.js
* @Description: 文件描述
*/
......@@ -18,10 +18,10 @@ import { library } from '@fortawesome/fontawesome-svg-core'
/* import font awesome icon component */
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
/* import specific icons */
import { faCirclePause, faCirclePlay, faPlay, faPause, faBackwardStep, faForwardStep, faVolumeUp, faRedo, faRepeat, faList, faChevronDown, faVolumeOff, faXmark, faFileAlt } from '@fortawesome/free-solid-svg-icons'
import { faCirclePause, faCirclePlay, faPlay, faPause, faBackwardStep, faForwardStep, faVolumeUp, faRedo, faRepeat, faList, faChevronDown, faVolumeOff, faXmark, faFileAlt, faTimes, faEye, faFilePdf, faExternalLinkAlt, faSpinner, faExclamationCircle } from '@fortawesome/free-solid-svg-icons'
/* add icons to the library */
library.add(faCirclePause, faCirclePlay, faPlay, faPause, faBackwardStep, faForwardStep, faVolumeUp, faRedo, faRepeat, faList, faChevronDown, faVolumeOff, faXmark, faFileAlt)
library.add(faCirclePause, faCirclePlay, faPlay, faPause, faBackwardStep, faForwardStep, faVolumeUp, faRedo, faRepeat, faList, faChevronDown, faVolumeOff, faXmark, faFileAlt, faTimes, faEye, faFilePdf, faExternalLinkAlt, faSpinner, faExclamationCircle)
const app = createApp(App)
// 屏蔽警告信息
......
......@@ -219,5 +219,11 @@ export const routes = [
title: '课程集合页面',
}
},
{
path: '/pdf-preview',
name: 'pdf-preview',
component: () => import('../views/study/PdfPreviewPage.vue'),
meta: { title: 'PDF预览' },
},
...checkinRoutes,
]
......
<!--
* @Date: 2024-01-17
* @Description: PDF预览页面
-->
<template>
<div class="pdf-preview-page bg-white min-h-screen flex flex-col">
<!-- 顶部导航栏 -->
<div class="flex items-center justify-between p-4 bg-white border-b sticky top-0 z-10">
<div class="flex items-center space-x-2">
<font-awesome-icon icon="file-pdf" class="text-red-500 text-xl" />
<span class="text-gray-900 font-medium">{{ title }}</span>
</div>
<div class="flex items-center space-x-4">
<a :href="pdfUrl" target="_blank" class="text-blue-600 hover:text-blue-800">
<font-awesome-icon icon="external-link-alt" class="mr-1" />
新窗口打开
</a>
<button @click="goBack" class="text-gray-500 hover:text-gray-700">
<font-awesome-icon icon="times" />
</button>
</div>
</div>
<!-- PDF内容区域 -->
<div class="flex-1 overflow-y-auto bg-gray-100 p-4">
<div v-for="pageNum in pageNums" :key="pageNum" class="mb-4" ref="pageRefs">
<VuePdfEmbed
v-if="pageVisibility[pageNum]"
:source="{ url: pdfUrl }"
:page="pageNum"
:scale="1.5"
:render-text="true"
style="width: 100%;"
/>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import VuePdfEmbed, { useVuePdfEmbed } from 'vue-pdf-embed';
const route = useRoute();
const router = useRouter();
// 获取路由参数
const title = ref(route.query.title || 'PDF预览');
const pdfUrl = ref(route.query.url || '');
// PDF页面相关
const pageRefs = ref([]);
const pageVisibility = ref({});
let pageIntersectionObserver;
// 使用PDF嵌入组件
const { doc } = useVuePdfEmbed({
source: { url: pdfUrl.value },
});
// 计算总页数
const pageNums = computed(() =>
doc.value
? [...Array(doc.value.numPages + 1).keys()].slice(1)
: []
);
// 重置页面交叉观察器
const resetPageIntersectionObserver = () => {
pageIntersectionObserver?.disconnect();
pageIntersectionObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const index = pageRefs.value.indexOf(entry.target);
const pageNum = pageNums.value[index];
pageVisibility.value[pageNum] = true;
}
});
});
pageRefs.value.forEach((element) => {
if (element) {
pageIntersectionObserver.observe(element);
}
});
};
// 返回上一页
const goBack = () => {
router.back();
};
// 监听页数变化
watch(pageNums, (newPageNums) => {
if (newPageNums.length > 0) {
pageVisibility.value = { [newPageNums[0]]: true };
nextTick(resetPageIntersectionObserver);
}
});
// 组件卸载前清理
onBeforeUnmount(() => {
pageIntersectionObserver?.disconnect();
});
</script>
<style lang="less" scoped>
.pdf-preview-page {
.pdf-content {
background-color: #f3f4f6;
}
}
</style>
......@@ -56,7 +56,16 @@
</div>
<div class="flex-1 min-w-0">
<h3 class="text-sm font-medium text-gray-900 truncate">{{ item.title }}</h3>
<a :href="item.url" target="_blank" class="text-sm text-blue-600 hover:text-blue-800 hover:underline truncate block mt-1">打开文件</a>
<!-- PDF文件预览 -->
<template v-if="item.url.toLowerCase().endsWith('.pdf')">
<button @click="openPdfViewer(item)" class="mt-2 w-full text-left text-blue-600 hover:text-blue-800 hover:underline">
<font-awesome-icon icon="eye" class="mr-1" />
预览PDF
</button>
</template>
<template v-else>
<a :href="item.url" target="_blank" class="text-sm text-blue-600 hover:text-blue-800 hover:underline truncate block mt-1">打开文件</a>
</template>
</div>
</div>
</div>
......@@ -339,6 +348,17 @@ 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; // 重置播放状态
......