hookehuyr

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

引入vue-pdf-embed库,新增PDF预览页面,支持在课程详情页中直接预览PDF文件。添加了新的路由和组件,并更新了相关依赖。
...@@ -22,6 +22,7 @@ ...@@ -22,6 +22,7 @@
22 "vconsole": "^3.15.1", 22 "vconsole": "^3.15.1",
23 "video.js": "^7.21.7", 23 "video.js": "^7.21.7",
24 "vue": "^3.5.13", 24 "vue": "^3.5.13",
25 + "vue-pdf-embed": "^2.1.2",
25 "vue-router": "^4.5.0", 26 "vue-router": "^4.5.0",
26 "weixin-js-sdk": "^1.6.5" 27 "weixin-js-sdk": "^1.6.5"
27 }, 28 },
...@@ -947,6 +948,177 @@ ...@@ -947,6 +948,177 @@
947 "@jridgewell/sourcemap-codec": "^1.4.14" 948 "@jridgewell/sourcemap-codec": "^1.4.14"
948 } 949 }
949 }, 950 },
951 + "node_modules/@napi-rs/canvas": {
952 + "version": "0.1.70",
953 + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.70.tgz",
954 + "integrity": "sha512-nD6NGa4JbNYSZYsTnLGrqe9Kn/lCkA4ybXt8sx5ojDqZjr2i0TWAHxx/vhgfjX+i3hCdKWufxYwi7CfXqtITSA==",
955 + "optional": true,
956 + "engines": {
957 + "node": ">= 10"
958 + },
959 + "optionalDependencies": {
960 + "@napi-rs/canvas-android-arm64": "0.1.70",
961 + "@napi-rs/canvas-darwin-arm64": "0.1.70",
962 + "@napi-rs/canvas-darwin-x64": "0.1.70",
963 + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.70",
964 + "@napi-rs/canvas-linux-arm64-gnu": "0.1.70",
965 + "@napi-rs/canvas-linux-arm64-musl": "0.1.70",
966 + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.70",
967 + "@napi-rs/canvas-linux-x64-gnu": "0.1.70",
968 + "@napi-rs/canvas-linux-x64-musl": "0.1.70",
969 + "@napi-rs/canvas-win32-x64-msvc": "0.1.70"
970 + }
971 + },
972 + "node_modules/@napi-rs/canvas-android-arm64": {
973 + "version": "0.1.70",
974 + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.70.tgz",
975 + "integrity": "sha512-I/YOuQ0wbkVYxVaYtCgN42WKTYxNqFA0gTcTrHIGG1jfpDSyZWII/uHcjOo4nzd19io6Y4+/BqP8E5hJgf9OmQ==",
976 + "cpu": [
977 + "arm64"
978 + ],
979 + "optional": true,
980 + "os": [
981 + "android"
982 + ],
983 + "engines": {
984 + "node": ">= 10"
985 + }
986 + },
987 + "node_modules/@napi-rs/canvas-darwin-arm64": {
988 + "version": "0.1.70",
989 + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.70.tgz",
990 + "integrity": "sha512-4pPGyXetHIHkw2TOJHujt3mkCP8LdDu8+CT15ld9Id39c752RcI0amDHSuMLMQfAjvusA9B5kKxazwjMGjEJpQ==",
991 + "cpu": [
992 + "arm64"
993 + ],
994 + "optional": true,
995 + "os": [
996 + "darwin"
997 + ],
998 + "engines": {
999 + "node": ">= 10"
1000 + }
1001 + },
1002 + "node_modules/@napi-rs/canvas-darwin-x64": {
1003 + "version": "0.1.70",
1004 + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.70.tgz",
1005 + "integrity": "sha512-+2N6Os9LbkmDMHL+raknrUcLQhsXzc5CSXRbXws9C3pv/mjHRVszQ9dhFUUe9FjfPhCJznO6USVdwOtu7pOrzQ==",
1006 + "cpu": [
1007 + "x64"
1008 + ],
1009 + "optional": true,
1010 + "os": [
1011 + "darwin"
1012 + ],
1013 + "engines": {
1014 + "node": ">= 10"
1015 + }
1016 + },
1017 + "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
1018 + "version": "0.1.70",
1019 + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.70.tgz",
1020 + "integrity": "sha512-QjscX9OaKq/990sVhSMj581xuqLgiaPVMjjYvWaCmAJRkNQ004QfoSMEm3FoTqM4DRoquP8jvuEXScVJsc1rqQ==",
1021 + "cpu": [
1022 + "arm"
1023 + ],
1024 + "optional": true,
1025 + "os": [
1026 + "linux"
1027 + ],
1028 + "engines": {
1029 + "node": ">= 10"
1030 + }
1031 + },
1032 + "node_modules/@napi-rs/canvas-linux-arm64-gnu": {
1033 + "version": "0.1.70",
1034 + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.70.tgz",
1035 + "integrity": "sha512-LNakMOwwqwiHIwMpnMAbFRczQMQ7TkkMyATqFCOtUJNlE6LPP/QiUj/mlFrNbUn/hctqShJ60gWEb52ZTALbVw==",
1036 + "cpu": [
1037 + "arm64"
1038 + ],
1039 + "optional": true,
1040 + "os": [
1041 + "linux"
1042 + ],
1043 + "engines": {
1044 + "node": ">= 10"
1045 + }
1046 + },
1047 + "node_modules/@napi-rs/canvas-linux-arm64-musl": {
1048 + "version": "0.1.70",
1049 + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.70.tgz",
1050 + "integrity": "sha512-wBTOllEYNfJCHOdZj9v8gLzZ4oY3oyPX8MSRvaxPm/s7RfEXxCyZ8OhJ5xAyicsDdbE5YBZqdmaaeP5+xKxvtg==",
1051 + "cpu": [
1052 + "arm64"
1053 + ],
1054 + "optional": true,
1055 + "os": [
1056 + "linux"
1057 + ],
1058 + "engines": {
1059 + "node": ">= 10"
1060 + }
1061 + },
1062 + "node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
1063 + "version": "0.1.70",
1064 + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.70.tgz",
1065 + "integrity": "sha512-GVUUPC8TuuFqHip0rxHkUqArQnlzmlXmTEBuXAWdgCv85zTCFH8nOHk/YCF5yo0Z2eOm8nOi90aWs0leJ4OE5Q==",
1066 + "cpu": [
1067 + "riscv64"
1068 + ],
1069 + "optional": true,
1070 + "os": [
1071 + "linux"
1072 + ],
1073 + "engines": {
1074 + "node": ">= 10"
1075 + }
1076 + },
1077 + "node_modules/@napi-rs/canvas-linux-x64-gnu": {
1078 + "version": "0.1.70",
1079 + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.70.tgz",
1080 + "integrity": "sha512-/kvUa2lZRwGNyfznSn5t1ShWJnr/m5acSlhTV3eXECafObjl0VBuA1HJw0QrilLpb4Fe0VLywkpD1NsMoVDROQ==",
1081 + "cpu": [
1082 + "x64"
1083 + ],
1084 + "optional": true,
1085 + "os": [
1086 + "linux"
1087 + ],
1088 + "engines": {
1089 + "node": ">= 10"
1090 + }
1091 + },
1092 + "node_modules/@napi-rs/canvas-linux-x64-musl": {
1093 + "version": "0.1.70",
1094 + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.70.tgz",
1095 + "integrity": "sha512-aqlv8MLpycoMKRmds7JWCfVwNf1fiZxaU7JwJs9/ExjTD8lX2KjsO7CTeAj5Cl4aEuzxUWbJPUUE2Qu9cZ1vfg==",
1096 + "cpu": [
1097 + "x64"
1098 + ],
1099 + "optional": true,
1100 + "os": [
1101 + "linux"
1102 + ],
1103 + "engines": {
1104 + "node": ">= 10"
1105 + }
1106 + },
1107 + "node_modules/@napi-rs/canvas-win32-x64-msvc": {
1108 + "version": "0.1.70",
1109 + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.70.tgz",
1110 + "integrity": "sha512-Q9QU3WIpwBTVHk4cPfBjGHGU4U0llQYRXgJtFtYqqGNEOKVN4OT6PQ+ve63xwIPODMpZ0HHyj/KLGc9CWc3EtQ==",
1111 + "cpu": [
1112 + "x64"
1113 + ],
1114 + "optional": true,
1115 + "os": [
1116 + "win32"
1117 + ],
1118 + "engines": {
1119 + "node": ">= 10"
1120 + }
1121 + },
950 "node_modules/@nodelib/fs.scandir": { 1122 "node_modules/@nodelib/fs.scandir": {
951 "version": "2.1.5", 1123 "version": "2.1.5",
952 "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", 1124 "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
...@@ -3098,6 +3270,17 @@ ...@@ -3098,6 +3270,17 @@
3098 "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", 3270 "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
3099 "dev": true 3271 "dev": true
3100 }, 3272 },
3273 + "node_modules/pdfjs-dist": {
3274 + "version": "4.10.38",
3275 + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-4.10.38.tgz",
3276 + "integrity": "sha512-/Y3fcFrXEAsMjJXeL9J8+ZG9U01LbuWaYypvDW2ycW1jL269L3js3DVBjDJ0Up9Np1uqDXsDrRihHANhZOlwdQ==",
3277 + "engines": {
3278 + "node": ">=20"
3279 + },
3280 + "optionalDependencies": {
3281 + "@napi-rs/canvas": "^0.1.65"
3282 + }
3283 + },
3101 "node_modules/picocolors": { 3284 "node_modules/picocolors": {
3102 "version": "1.1.1", 3285 "version": "1.1.1",
3103 "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", 3286 "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
...@@ -4327,6 +4510,17 @@ ...@@ -4327,6 +4510,17 @@
4327 } 4510 }
4328 } 4511 }
4329 }, 4512 },
4513 + "node_modules/vue-pdf-embed": {
4514 + "version": "2.1.2",
4515 + "resolved": "https://registry.npmjs.org/vue-pdf-embed/-/vue-pdf-embed-2.1.2.tgz",
4516 + "integrity": "sha512-/j++oknFBY9x/MgEFBo9tSuOXS0Z9COlywwLhMREhiGfmuQqpnGy5T+SwVIXxR1tmdzM/lHog8JL7HOAgXT1aw==",
4517 + "dependencies": {
4518 + "pdfjs-dist": "^4.10.38"
4519 + },
4520 + "peerDependencies": {
4521 + "vue": "^3.3.0"
4522 + }
4523 + },
4330 "node_modules/vue-router": { 4524 "node_modules/vue-router": {
4331 "version": "4.5.0", 4525 "version": "4.5.0",
4332 "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.0.tgz", 4526 "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.0.tgz",
......
...@@ -29,6 +29,7 @@ ...@@ -29,6 +29,7 @@
29 "vconsole": "^3.15.1", 29 "vconsole": "^3.15.1",
30 "video.js": "^7.21.7", 30 "video.js": "^7.21.7",
31 "vue": "^3.5.13", 31 "vue": "^3.5.13",
32 + "vue-pdf-embed": "^2.1.2",
32 "vue-router": "^4.5.0", 33 "vue-router": "^4.5.0",
33 "weixin-js-sdk": "^1.6.5" 34 "weixin-js-sdk": "^1.6.5"
34 }, 35 },
......
...@@ -20,6 +20,7 @@ declare module 'vue' { ...@@ -20,6 +20,7 @@ declare module 'vue' {
20 GradientHeader: typeof import('./components/ui/GradientHeader.vue')['default'] 20 GradientHeader: typeof import('./components/ui/GradientHeader.vue')['default']
21 LiveStreamCard: typeof import('./components/ui/LiveStreamCard.vue')['default'] 21 LiveStreamCard: typeof import('./components/ui/LiveStreamCard.vue')['default']
22 MenuItem: typeof import('./components/ui/MenuItem.vue')['default'] 22 MenuItem: typeof import('./components/ui/MenuItem.vue')['default']
23 + PdfViewer: typeof import('./components/ui/PdfViewer.vue')['default']
23 ReviewPopup: typeof import('./components/courses/ReviewPopup.vue')['default'] 24 ReviewPopup: typeof import('./components/courses/ReviewPopup.vue')['default']
24 RouterLink: typeof import('vue-router')['RouterLink'] 25 RouterLink: typeof import('vue-router')['RouterLink']
25 RouterView: typeof import('vue-router')['RouterView'] 26 RouterView: typeof import('vue-router')['RouterView']
......
1 /* 1 /*
2 * @Date: 2025-03-20 20:36:36 2 * @Date: 2025-03-20 20:36:36
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-05-08 10:56:28 4 + * @LastEditTime: 2025-05-08 11:14:46
5 * @FilePath: /mlaj/src/main.js 5 * @FilePath: /mlaj/src/main.js
6 * @Description: 文件描述 6 * @Description: 文件描述
7 */ 7 */
...@@ -18,10 +18,10 @@ import { library } from '@fortawesome/fontawesome-svg-core' ...@@ -18,10 +18,10 @@ import { library } from '@fortawesome/fontawesome-svg-core'
18 /* import font awesome icon component */ 18 /* import font awesome icon component */
19 import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' 19 import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
20 /* import specific icons */ 20 /* import specific icons */
21 -import { faCirclePause, faCirclePlay, faPlay, faPause, faBackwardStep, faForwardStep, faVolumeUp, faRedo, faRepeat, faList, faChevronDown, faVolumeOff, faXmark, faFileAlt } from '@fortawesome/free-solid-svg-icons' 21 +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'
22 22
23 /* add icons to the library */ 23 /* add icons to the library */
24 -library.add(faCirclePause, faCirclePlay, faPlay, faPause, faBackwardStep, faForwardStep, faVolumeUp, faRedo, faRepeat, faList, faChevronDown, faVolumeOff, faXmark, faFileAlt) 24 +library.add(faCirclePause, faCirclePlay, faPlay, faPause, faBackwardStep, faForwardStep, faVolumeUp, faRedo, faRepeat, faList, faChevronDown, faVolumeOff, faXmark, faFileAlt, faTimes, faEye, faFilePdf, faExternalLinkAlt, faSpinner, faExclamationCircle)
25 25
26 const app = createApp(App) 26 const app = createApp(App)
27 // 屏蔽警告信息 27 // 屏蔽警告信息
......
...@@ -219,5 +219,11 @@ export const routes = [ ...@@ -219,5 +219,11 @@ export const routes = [
219 title: '课程集合页面', 219 title: '课程集合页面',
220 } 220 }
221 }, 221 },
222 + {
223 + path: '/pdf-preview',
224 + name: 'pdf-preview',
225 + component: () => import('../views/study/PdfPreviewPage.vue'),
226 + meta: { title: 'PDF预览' },
227 + },
222 ...checkinRoutes, 228 ...checkinRoutes,
223 ] 229 ]
......
1 +<!--
2 + * @Date: 2024-01-17
3 + * @Description: PDF预览页面
4 +-->
5 +<template>
6 + <div class="pdf-preview-page bg-white min-h-screen flex flex-col">
7 + <!-- 顶部导航栏 -->
8 + <div class="flex items-center justify-between p-4 bg-white border-b sticky top-0 z-10">
9 + <div class="flex items-center space-x-2">
10 + <font-awesome-icon icon="file-pdf" class="text-red-500 text-xl" />
11 + <span class="text-gray-900 font-medium">{{ title }}</span>
12 + </div>
13 + <div class="flex items-center space-x-4">
14 + <a :href="pdfUrl" target="_blank" class="text-blue-600 hover:text-blue-800">
15 + <font-awesome-icon icon="external-link-alt" class="mr-1" />
16 + 新窗口打开
17 + </a>
18 + <button @click="goBack" class="text-gray-500 hover:text-gray-700">
19 + <font-awesome-icon icon="times" />
20 + </button>
21 + </div>
22 + </div>
23 +
24 + <!-- PDF内容区域 -->
25 + <div class="flex-1 overflow-y-auto bg-gray-100 p-4">
26 + <div v-for="pageNum in pageNums" :key="pageNum" class="mb-4" ref="pageRefs">
27 + <VuePdfEmbed
28 + v-if="pageVisibility[pageNum]"
29 + :source="{ url: pdfUrl }"
30 + :page="pageNum"
31 + :scale="1.5"
32 + :render-text="true"
33 + style="width: 100%;"
34 + />
35 + </div>
36 + </div>
37 + </div>
38 +</template>
39 +
40 +<script setup>
41 +import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue';
42 +import { useRoute, useRouter } from 'vue-router';
43 +import VuePdfEmbed, { useVuePdfEmbed } from 'vue-pdf-embed';
44 +
45 +const route = useRoute();
46 +const router = useRouter();
47 +
48 +// 获取路由参数
49 +const title = ref(route.query.title || 'PDF预览');
50 +const pdfUrl = ref(route.query.url || '');
51 +
52 +// PDF页面相关
53 +const pageRefs = ref([]);
54 +const pageVisibility = ref({});
55 +let pageIntersectionObserver;
56 +
57 +// 使用PDF嵌入组件
58 +const { doc } = useVuePdfEmbed({
59 + source: { url: pdfUrl.value },
60 +});
61 +
62 +// 计算总页数
63 +const pageNums = computed(() =>
64 + doc.value
65 + ? [...Array(doc.value.numPages + 1).keys()].slice(1)
66 + : []
67 +);
68 +
69 +// 重置页面交叉观察器
70 +const resetPageIntersectionObserver = () => {
71 + pageIntersectionObserver?.disconnect();
72 + pageIntersectionObserver = new IntersectionObserver((entries) => {
73 + entries.forEach((entry) => {
74 + if (entry.isIntersecting) {
75 + const index = pageRefs.value.indexOf(entry.target);
76 + const pageNum = pageNums.value[index];
77 + pageVisibility.value[pageNum] = true;
78 + }
79 + });
80 + });
81 + pageRefs.value.forEach((element) => {
82 + if (element) {
83 + pageIntersectionObserver.observe(element);
84 + }
85 + });
86 +};
87 +
88 +// 返回上一页
89 +const goBack = () => {
90 + router.back();
91 +};
92 +
93 +// 监听页数变化
94 +watch(pageNums, (newPageNums) => {
95 + if (newPageNums.length > 0) {
96 + pageVisibility.value = { [newPageNums[0]]: true };
97 + nextTick(resetPageIntersectionObserver);
98 + }
99 +});
100 +
101 +// 组件卸载前清理
102 +onBeforeUnmount(() => {
103 + pageIntersectionObserver?.disconnect();
104 +});
105 +</script>
106 +
107 +<style lang="less" scoped>
108 +.pdf-preview-page {
109 + .pdf-content {
110 + background-color: #f3f4f6;
111 + }
112 +}
113 +</style>
...@@ -56,7 +56,16 @@ ...@@ -56,7 +56,16 @@
56 </div> 56 </div>
57 <div class="flex-1 min-w-0"> 57 <div class="flex-1 min-w-0">
58 <h3 class="text-sm font-medium text-gray-900 truncate">{{ item.title }}</h3> 58 <h3 class="text-sm font-medium text-gray-900 truncate">{{ item.title }}</h3>
59 - <a :href="item.url" target="_blank" class="text-sm text-blue-600 hover:text-blue-800 hover:underline truncate block mt-1">打开文件</a> 59 + <!-- PDF文件预览 -->
60 + <template v-if="item.url.toLowerCase().endsWith('.pdf')">
61 + <button @click="openPdfViewer(item)" class="mt-2 w-full text-left text-blue-600 hover:text-blue-800 hover:underline">
62 + <font-awesome-icon icon="eye" class="mr-1" />
63 + 预览PDF
64 + </button>
65 + </template>
66 + <template v-else>
67 + <a :href="item.url" target="_blank" class="text-sm text-blue-600 hover:text-blue-800 hover:underline truncate block mt-1">打开文件</a>
68 + </template>
60 </div> 69 </div>
61 </div> 70 </div>
62 </div> 71 </div>
...@@ -339,6 +348,17 @@ const commentList = ref([]); ...@@ -339,6 +348,17 @@ const commentList = ref([]);
339 const courseFile = ref({}); 348 const courseFile = ref({});
340 349
341 // 处理课程切换 350 // 处理课程切换
351 +// 打开PDF预览
352 +const openPdfViewer = (item) => {
353 + router.push({
354 + name: 'pdf-preview',
355 + query: {
356 + url: item.url,
357 + title: item.title
358 + }
359 + });
360 +};
361 +
342 const handleLessonClick = async (lesson) => { 362 const handleLessonClick = async (lesson) => {
343 showCatalog.value = false; // 关闭目录弹窗 363 showCatalog.value = false; // 关闭目录弹窗
344 isPlaying.value = false; // 重置播放状态 364 isPlaying.value = false; // 重置播放状态
......