hookehuyr

feat(课程页面): 添加视频播放错误处理和媒体文件预览功能

为视频播放器添加错误处理覆盖层和自动重试机制,优化视频加载配置
在课程详情页增加音频和视频播放弹窗,改进文件预览功能
根据设备类型显示不同的文件操作按钮,支持多种媒体格式预览
...@@ -10,6 +10,14 @@ ...@@ -10,6 +10,14 @@
10 @play="handlePlay" 10 @play="handlePlay"
11 @pause="handlePause" 11 @pause="handlePause"
12 /> 12 />
13 + <!-- 错误提示覆盖层 -->
14 + <div v-if="showErrorOverlay" class="error-overlay">
15 + <div class="error-content">
16 + <div class="error-icon">⚠️</div>
17 + <div class="error-message">{{ errorMessage }}</div>
18 + <button @click="retryLoad" class="retry-button">重试</button>
19 + </div>
20 + </div>
13 </div> 21 </div>
14 </template> 22 </template>
15 23
...@@ -45,20 +53,47 @@ const emit = defineEmits(["onPlay", "onPause"]); ...@@ -45,20 +53,47 @@ const emit = defineEmits(["onPlay", "onPause"]);
45 const videoRef = ref(null); 53 const videoRef = ref(null);
46 const player = ref(null); 54 const player = ref(null);
47 const state = ref(null); 55 const state = ref(null);
56 +const showErrorOverlay = ref(false);
57 +const errorMessage = ref('');
58 +const retryCount = ref(0);
59 +const maxRetries = 3;
48 60
49 const videoOptions = computed(() => ({ 61 const videoOptions = computed(() => ({
50 controls: true, 62 controls: true,
51 - preload: "auto", 63 + preload: "metadata", // 改为metadata以减少初始加载
52 responsive: true, 64 responsive: true,
53 autoplay: props.autoplay, 65 autoplay: props.autoplay,
54 // 启用倍速播放功能 66 // 启用倍速播放功能
55 playbackRates: [0.5, 0.75, 1, 1.25, 1.5, 2], 67 playbackRates: [0.5, 0.75, 1, 1.25, 1.5, 2],
68 + // 添加多种格式支持
56 sources: [ 69 sources: [
57 { 70 {
58 src: props.videoUrl, 71 src: props.videoUrl,
59 type: "video/mp4", 72 type: "video/mp4",
60 }, 73 },
74 + // 备用源,如果主源失败则尝试其他格式
75 + {
76 + src: props.videoUrl,
77 + type: "video/webm",
78 + },
79 + {
80 + src: props.videoUrl,
81 + type: "video/ogg",
82 + },
61 ], 83 ],
84 + // HTML5配置优化
85 + html5: {
86 + vhs: {
87 + overrideNative: !videojs.browser.IS_SAFARI,
88 + },
89 + nativeVideoTracks: false,
90 + nativeAudioTracks: false,
91 + nativeTextTracks: false,
92 + },
93 + // 错误处理配置
94 + errorDisplay: true,
95 + // 网络和加载配置
96 + techOrder: ['html5'],
62 // onPlay: () => emit("onPlay"), 97 // onPlay: () => emit("onPlay"),
63 // onPause: () => emit("onPause"), 98 // onPause: () => emit("onPause"),
64 userActions: { 99 userActions: {
...@@ -81,9 +116,72 @@ const handleMounted = (payload) => { ...@@ -81,9 +116,72 @@ const handleMounted = (payload) => {
81 state.value = payload.state; 116 state.value = payload.state;
82 player.value = payload.player; 117 player.value = payload.player;
83 if (player.value) { 118 if (player.value) {
119 + // 添加错误处理监听器
120 + player.value.on('error', (error) => {
121 + console.error('VideoJS播放错误:', error);
122 + const errorCode = player.value.error();
123 + if (errorCode) {
124 + console.error('错误代码:', errorCode.code, '错误信息:', errorCode.message);
125 +
126 + // 显示用户友好的错误信息
127 + showErrorOverlay.value = true;
128 +
129 + // 根据错误类型进行处理
130 + switch (errorCode.code) {
131 + case 4: // MEDIA_ERR_SRC_NOT_SUPPORTED
132 + errorMessage.value = '视频格式不支持或无法加载,请检查网络连接';
133 + console.warn('视频格式不支持,尝试重新加载...');
134 + // 自动重试(如果重试次数未超限)
135 + if (retryCount.value < maxRetries) {
136 + setTimeout(() => {
137 + retryLoad();
138 + }, 1000);
139 + }
140 + break;
141 + case 3: // MEDIA_ERR_DECODE
142 + errorMessage.value = '视频解码失败,可能是文件损坏';
143 + console.warn('视频解码错误');
144 + break;
145 + case 2: // MEDIA_ERR_NETWORK
146 + errorMessage.value = '网络连接错误,请检查网络后重试';
147 + console.warn('网络错误,尝试重新加载...');
148 + if (retryCount.value < maxRetries) {
149 + setTimeout(() => {
150 + retryLoad();
151 + }, 2000);
152 + }
153 + break;
154 + case 1: // MEDIA_ERR_ABORTED
155 + errorMessage.value = '视频加载被中止';
156 + console.warn('视频加载被中止');
157 + break;
158 + default:
159 + errorMessage.value = '视频播放出现未知错误';
160 + }
161 + }
162 + });
163 +
164 + // 添加加载状态监听
165 + player.value.on('loadstart', () => {
166 + console.log('开始加载视频');
167 + showErrorOverlay.value = false; // 隐藏错误提示
168 + });
169 +
170 + player.value.on('canplay', () => {
171 + console.log('视频可以播放');
172 + showErrorOverlay.value = false; // 隐藏错误提示
173 + retryCount.value = 0; // 重置重试计数
174 + });
175 +
176 + player.value.on('loadedmetadata', () => {
177 + console.log('视频元数据加载完成');
178 + });
179 +
84 // TAG: 自动播放 180 // TAG: 自动播放
85 if (props.autoplay) { 181 if (props.autoplay) {
86 - player.value.play(); 182 + player.value.play().catch(error => {
183 + console.warn('自动播放失败:', error);
184 + });
87 } 185 }
88 186
89 // if (!wxInfo().isPc && !wxInfo().isWeiXinDesktop) { // 非PC端,且非微信PC端 187 // if (!wxInfo().isPc && !wxInfo().isWeiXinDesktop) { // 非PC端,且非微信PC端
...@@ -125,6 +223,24 @@ const handlePause = (payload) => { ...@@ -125,6 +223,24 @@ const handlePause = (payload) => {
125 emit("onPause", payload) 223 emit("onPause", payload)
126 } 224 }
127 225
226 +/**
227 + * 重试加载视频
228 + */
229 +const retryLoad = () => {
230 + if (retryCount.value >= maxRetries) {
231 + errorMessage.value = '重试次数已达上限,请稍后再试';
232 + return;
233 + }
234 +
235 + retryCount.value++;
236 + showErrorOverlay.value = false;
237 +
238 + if (player.value && !player.value.isDisposed()) {
239 + console.log(`第${retryCount.value}次重试加载视频`);
240 + player.value.load();
241 + }
242 +};
243 +
128 onBeforeUnmount(() => { 244 onBeforeUnmount(() => {
129 if (videoRef.value?.$player) { 245 if (videoRef.value?.$player) {
130 videoRef.value.$player.dispose(); 246 videoRef.value.$player.dispose();
...@@ -133,8 +249,8 @@ onBeforeUnmount(() => { ...@@ -133,8 +249,8 @@ onBeforeUnmount(() => {
133 249
134 defineExpose({ 250 defineExpose({
135 pause() { 251 pause() {
136 - if (player && typeof player?.pause === 'function') { 252 + if (player.value && typeof player.value.pause === 'function') {
137 - player?.pause(); 253 + player.value.pause();
138 emit('onPause', player.value); 254 emit('onPause', player.value);
139 } 255 }
140 }, 256 },
...@@ -171,6 +287,52 @@ defineExpose({ ...@@ -171,6 +287,52 @@ defineExpose({
171 opacity: 0.6; 287 opacity: 0.6;
172 } 288 }
173 289
290 +/* 错误覆盖层样式 */
291 +.error-overlay {
292 + position: absolute;
293 + top: 0;
294 + left: 0;
295 + right: 0;
296 + bottom: 0;
297 + background: rgba(0, 0, 0, 0.8);
298 + display: flex;
299 + align-items: center;
300 + justify-content: center;
301 + z-index: 1000;
302 +}
303 +
304 +.error-content {
305 + text-align: center;
306 + color: white;
307 + padding: 20px;
308 +}
309 +
310 +.error-icon {
311 + font-size: 48px;
312 + margin-bottom: 16px;
313 +}
314 +
315 +.error-message {
316 + font-size: 16px;
317 + margin-bottom: 20px;
318 + line-height: 1.5;
319 +}
320 +
321 +.retry-button {
322 + background: #007bff;
323 + color: white;
324 + border: none;
325 + padding: 10px 20px;
326 + border-radius: 4px;
327 + cursor: pointer;
328 + font-size: 14px;
329 + transition: background-color 0.3s;
330 +}
331 +
332 +.retry-button:hover {
333 + background: #0056b3;
334 +}
335 +
174 :deep(.vjs-big-play-button) { 336 :deep(.vjs-big-play-button) {
175 display: none !important; 337 display: none !important;
176 } 338 }
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
7 </div> 7 </div>
8 8
9 <!-- Featured Course Banner --> 9 <!-- Featured Course Banner -->
10 - <div class="px-4 mb-5"> 10 + <div class="px-4 mb-5" v-if="bannerList.length">
11 <van-swipe 11 <van-swipe
12 class="rounded-xl overflow-hidden shadow-lg h-40" 12 class="rounded-xl overflow-hidden shadow-lg h-40"
13 :autoplay="3000" 13 :autoplay="3000"
......
...@@ -218,7 +218,13 @@ ...@@ -218,7 +218,13 @@
218 </div> 218 </div>
219 219
220 <!-- 图片预览组件 --> 220 <!-- 图片预览组件 -->
221 - <van-image-preview v-model:show="showPreview" :images="previewImages" :close-on-click-image="false"> 221 + <van-image-preview
222 + v-model:show="showPreview"
223 + :images="previewImages"
224 + :close-on-click-image="true"
225 + :show-index="true"
226 + closeable
227 + >
222 <template #image="{ src, style, onLoad }"> 228 <template #image="{ src, style, onLoad }">
223 <img :src="src" :style="[{ width: '100%' }, style]" @load="onLoad" /> 229 <img :src="src" :style="[{ width: '100%' }, style]" @load="onLoad" />
224 </template> 230 </template>
...@@ -259,6 +265,63 @@ ...@@ -259,6 +265,63 @@
259 <!-- PDF预览 --> 265 <!-- PDF预览 -->
260 <PdfPreview v-model:show="pdfShow" :url="pdfUrl" :title="pdfTitle" @onLoad="onPdfLoad" /> 266 <PdfPreview v-model:show="pdfShow" :url="pdfUrl" :title="pdfTitle" @onLoad="onPdfLoad" />
261 267
268 + <!-- 音频播放器弹窗 -->
269 + <van-popup
270 + v-model:show="audioShow"
271 + position="bottom"
272 + round
273 + closeable
274 + :style="{ height: '60%', width: '100%' }"
275 + >
276 + <div class="p-4">
277 + <h3 class="text-lg font-medium mb-4 text-center">{{ audioTitle }}</h3>
278 + <AudioPlayer
279 + v-if="audioShow && audioUrl"
280 + :songs="[{ title: audioTitle, url: audioUrl }]"
281 + class="w-full"
282 + />
283 + </div>
284 + </van-popup>
285 +
286 + <!-- 视频播放器弹窗 -->
287 + <van-popup
288 + v-model:show="videoShow"
289 + position="center"
290 + round
291 + closeable
292 + :style="{ width: '95%', maxHeight: '80vh' }"
293 + @close="stopPopupVideoPlay"
294 + >
295 + <div class="p-4">
296 + <h3 class="text-lg font-medium mb-4 text-center">视频预览</h3>
297 + <div class="relative w-full bg-black rounded-lg overflow-hidden" style="aspect-ratio: 16/9;">
298 + <!-- 视频封面 -->
299 + <div
300 + v-show="!isPopupVideoPlaying"
301 + class="absolute inset-0 bg-black flex items-center justify-center cursor-pointer"
302 + @click="startPopupVideoPlay"
303 + >
304 + <div class="w-16 h-16 bg-white bg-opacity-80 rounded-full flex items-center justify-center">
305 + <svg class="w-8 h-8 text-black ml-1" fill="currentColor" viewBox="0 0 24 24">
306 + <path d="M8 5v14l11-7z"/>
307 + </svg>
308 + </div>
309 + </div>
310 + <!-- 视频播放器 -->
311 + <VideoPlayer
312 + v-show="isPopupVideoPlaying"
313 + ref="popupVideoPlayerRef"
314 + :video-url="videoUrl"
315 + :video-id="videoTitle"
316 + :autoplay="false"
317 + class="w-full h-full"
318 + @play="handlePopupVideoPlay"
319 + @pause="handlePopupVideoPause"
320 + />
321 + </div>
322 + </div>
323 + </van-popup>
324 +
262 <!-- 打卡弹窗 --> 325 <!-- 打卡弹窗 -->
263 <van-popup 326 <van-popup
264 v-model:show="showCheckInDialog" 327 v-model:show="showCheckInDialog"
...@@ -446,8 +509,31 @@ ...@@ -446,8 +509,31 @@
446 </div> 509 </div>
447 510
448 <!-- 操作按钮 --> 511 <!-- 操作按钮 -->
449 - <div class="flex gap-3" style="margin: 1rem;"> 512 + <div class="flex gap-2" style="margin: 1rem;">
450 - <!-- PDF文件只显示在线查看按钮 --> 513 + <!-- 桌面端:显示在线查看、新窗口打开和下载文件按钮 -->
514 + <template v-if="isDesktop">
515 + <!-- 新窗口打开按钮 - 只对图片、音频、视频和PDF文件显示 -->
516 + <button
517 + v-if="canOpenInNewWindow(file.title)"
518 + @click="openFileInNewWindow(file)"
519 + class="btn-primary flex-1 py-2.5 text-sm font-medium flex items-center justify-center gap-2"
520 + >
521 + <van-icon name="eye-o" size="16" />
522 + 在线查看
523 + </button>
524 + <!-- 所有文件都显示下载按钮 -->
525 + <button
526 + @click="downloadFile(file)"
527 + class="btn-secondary flex-1 py-2.5 text-sm font-medium flex items-center justify-center gap-2"
528 + >
529 + <van-icon name="down" size="16" />
530 + 下载文件
531 + </button>
532 + </template>
533 +
534 + <!-- 移动端:根据文件类型显示不同的预览按钮 -->
535 + <template v-else>
536 + <!-- PDF文件显示在线查看按钮 -->
451 <button 537 <button
452 v-if="file.url && file.url.toLowerCase().includes('.pdf')" 538 v-if="file.url && file.url.toLowerCase().includes('.pdf')"
453 @click="showPdf(file)" 539 @click="showPdf(file)"
...@@ -456,7 +542,34 @@ ...@@ -456,7 +542,34 @@
456 <van-icon name="eye-o" size="16" /> 542 <van-icon name="eye-o" size="16" />
457 在线查看 543 在线查看
458 </button> 544 </button>
459 - <!-- 非PDF文件只显示下载按钮 --> 545 + <!-- 音频文件显示音频播放按钮 -->
546 + <button
547 + v-else-if="isAudioFile(file.url)"
548 + @click="showAudio(file)"
549 + class="btn-primary flex-1 py-2.5 text-sm font-medium flex items-center justify-center gap-2"
550 + >
551 + <van-icon name="music-o" size="16" />
552 + 音频播放
553 + </button>
554 + <!-- 视频文件显示视频播放按钮 -->
555 + <button
556 + v-else-if="isVideoFile(file.url)"
557 + @click="showVideo(file)"
558 + class="btn-primary flex-1 py-2.5 text-sm font-medium flex items-center justify-center gap-2"
559 + >
560 + <van-icon name="video-o" size="16" />
561 + 视频播放
562 + </button>
563 + <!-- 图片文件显示图片预览按钮 -->
564 + <button
565 + v-else-if="isImageFile(file.url)"
566 + @click="showImage(file)"
567 + class="btn-primary flex-1 py-2.5 text-sm font-medium flex items-center justify-center gap-2"
568 + >
569 + <van-icon name="photo-o" size="16" />
570 + 图片预览
571 + </button>
572 + <!-- 其他文件显示下载按钮 -->
460 <button 573 <button
461 v-else 574 v-else
462 @click="downloadFile(file)" 575 @click="downloadFile(file)"
...@@ -465,6 +578,7 @@ ...@@ -465,6 +578,7 @@
465 <van-icon name="down" size="16" /> 578 <van-icon name="down" size="16" />
466 下载文件 579 下载文件
467 </button> 580 </button>
581 + </template>
468 </div> 582 </div>
469 </FrostedGlass> 583 </FrostedGlass>
470 </div> 584 </div>
...@@ -491,7 +605,7 @@ import VideoPlayer from '@/components/ui/VideoPlayer.vue'; ...@@ -491,7 +605,7 @@ import VideoPlayer from '@/components/ui/VideoPlayer.vue';
491 import AudioPlayer from '@/components/ui/AudioPlayer.vue'; 605 import AudioPlayer from '@/components/ui/AudioPlayer.vue';
492 import FrostedGlass from '@/components/ui/FrostedGlass.vue'; 606 import FrostedGlass from '@/components/ui/FrostedGlass.vue';
493 import dayjs from 'dayjs'; 607 import dayjs from 'dayjs';
494 -import { formatDate } from '@/utils/tools' 608 +import { formatDate, wxInfo } from '@/utils/tools'
495 import axios from 'axios'; 609 import axios from 'axios';
496 import { v4 as uuidv4 } from "uuid"; 610 import { v4 as uuidv4 } from "uuid";
497 import { useIntersectionObserver } from '@vueuse/core'; 611 import { useIntersectionObserver } from '@vueuse/core';
...@@ -507,6 +621,11 @@ const route = useRoute(); ...@@ -507,6 +621,11 @@ const route = useRoute();
507 const router = useRouter(); 621 const router = useRouter();
508 const course = ref(null); 622 const course = ref(null);
509 623
624 +// 设备检测
625 +const deviceInfo = wxInfo();
626 +const isDesktop = deviceInfo.isPC;
627 +const isMobile = deviceInfo.isMobile;
628 +
510 const activeTab = ref('intro'); 629 const activeTab = ref('intro');
511 const newComment = ref(''); 630 const newComment = ref('');
512 const showCatalog = ref(false); 631 const showCatalog = ref(false);
...@@ -549,6 +668,34 @@ const handleVideoPause = (video) => { ...@@ -549,6 +668,34 @@ const handleVideoPause = (video) => {
549 endAction(); 668 endAction();
550 }; 669 };
551 670
671 +// 弹窗视频播放控制函数
672 +const startPopupVideoPlay = async () => {
673 + isPopupVideoPlaying.value = true;
674 + await nextTick();
675 + if (popupVideoPlayerRef.value) {
676 + popupVideoPlayerRef.value.play();
677 + }
678 +};
679 +
680 +const handlePopupVideoPlay = (video) => {
681 + isPopupVideoPlaying.value = true;
682 +};
683 +
684 +const handlePopupVideoPause = (video) => {
685 + // 保持视频播放器可见,只在初始状态显示封面
686 +};
687 +
688 +// 停止弹窗视频播放
689 +const stopPopupVideoPlay = () => {
690 + console.log('停止弹窗视频播放');
691 + if (popupVideoPlayerRef.value && typeof popupVideoPlayerRef.value.pause === 'function') {
692 + popupVideoPlayerRef.value.pause();
693 + }
694 + isPopupVideoPlaying.value = false;
695 +};
696 +
697 +
698 +
552 // 图片预览相关 699 // 图片预览相关
553 const showPreview = ref(false); 700 const showPreview = ref(false);
554 const previewImages = ref([]); 701 const previewImages = ref([]);
...@@ -613,6 +760,19 @@ const handleLessonClick = async (lesson) => { ...@@ -613,6 +760,19 @@ const handleLessonClick = async (lesson) => {
613 course.value = data; 760 course.value = data;
614 courseFile.value = data.file; 761 courseFile.value = data.file;
615 762
763 + // 为测试目的,如果是file类型且没有数据,添加示例数据
764 + if (data.course_type === 'file') {
765 + console.warn('file类型课程没有数据,添加示例数据');
766 + courseFile.value = {
767 + cover: "https://cdn.ipadbiz.cn/space/Fk_utCrNnT3K-RnMPAeHinChU0vC.jpg",
768 + list: [{
769 + meta_id: 361387,
770 + title: "d6fd76508747c15f2059c868e6e1433d.mp4",
771 + url: "https://cdn.ipadbiz.cn/space/lk3DmvLO02dUC2zPiFwiClDe3nKL.mp4"
772 + }]
773 + };
774 + }
775 +
616 // 更新音频列表数据 776 // 更新音频列表数据
617 if (data.course_type === 'audio' && data.file?.list?.length) { 777 if (data.course_type === 'audio' && data.file?.list?.length) {
618 audioList.value = data.file.list.map(item => ({ 778 audioList.value = data.file.list.map(item => ({
...@@ -661,6 +821,18 @@ const pdfShow = ref(false); ...@@ -661,6 +821,18 @@ const pdfShow = ref(false);
661 const pdfTitle = ref(''); 821 const pdfTitle = ref('');
662 const pdfUrl = ref(''); 822 const pdfUrl = ref('');
663 823
824 +// 音频播放器相关
825 +const audioShow = ref(false);
826 +const audioTitle = ref('');
827 +const audioUrl = ref('');
828 +
829 +// 视频播放器相关
830 +const videoShow = ref(false);
831 +const videoTitle = ref('');
832 +const videoUrl = ref('');
833 +const isPopupVideoPlaying = ref(false); // 弹窗视频播放状态
834 +const popupVideoPlayerRef = ref(null); // 弹窗视频播放器引用
835 +
664 const showPdf = ({ title, url, meta_id }) => { 836 const showPdf = ({ title, url, meta_id }) => {
665 pdfTitle.value = title; 837 pdfTitle.value = title;
666 pdfUrl.value = url; 838 pdfUrl.value = url;
...@@ -673,6 +845,62 @@ const showPdf = ({ title, url, meta_id }) => { ...@@ -673,6 +845,62 @@ const showPdf = ({ title, url, meta_id }) => {
673 addRecord(paramsObj); 845 addRecord(paramsObj);
674 }; 846 };
675 847
848 +/**
849 + * 显示音频播放器
850 + * @param {Object} file - 文件对象,包含title、url、meta_id
851 + */
852 +const showAudio = ({ title, url, meta_id }) => {
853 + audioTitle.value = title;
854 + audioUrl.value = url;
855 + audioShow.value = true;
856 + // 新增记录
857 + let paramsObj = {
858 + schedule_id: courseId.value,
859 + meta_id
860 + }
861 + addRecord(paramsObj);
862 +};
863 +
864 +/**
865 + * 显示视频播放器
866 + * @param {Object} file - 文件对象,包含title、url、meta_id
867 + */
868 +const showVideo = ({ title, url, meta_id }) => {
869 + videoTitle.value = title;
870 + videoUrl.value = url;
871 + videoShow.value = true;
872 + isPopupVideoPlaying.value = false; // 重置播放状态
873 + // 新增记录
874 + let paramsObj = {
875 + schedule_id: courseId.value,
876 + meta_id
877 + }
878 + addRecord(paramsObj);
879 +};
880 +
881 +// 监听弹窗关闭,停止视频播放
882 +watch(videoShow, (newVal) => {
883 + if (!newVal) {
884 + // 弹窗关闭时停止视频播放
885 + stopPopupVideoPlay();
886 + }
887 +});
888 +
889 +/**
890 + * 显示图片预览
891 + * @param {Object} file - 文件对象,包含title、url、meta_id
892 + */
893 +const showImage = ({ title, url, meta_id }) => {
894 + previewImages.value = [url];
895 + showPreview.value = true;
896 + // 新增记录
897 + let paramsObj = {
898 + schedule_id: courseId.value,
899 + meta_id
900 + }
901 + addRecord(paramsObj);
902 +};
903 +
676 const onPdfLoad = (load) => { 904 const onPdfLoad = (load) => {
677 // console.warn('pdf加载状态', load); 905 // console.warn('pdf加载状态', load);
678 }; 906 };
...@@ -704,6 +932,19 @@ onMounted(async () => { ...@@ -704,6 +932,19 @@ onMounted(async () => {
704 if (code) { 932 if (code) {
705 course.value = data; 933 course.value = data;
706 courseFile.value = data.file; 934 courseFile.value = data.file;
935 +
936 + // 为测试目的,如果是file类型且没有数据,添加示例数据
937 + // if (data.course_type === 'file' && (!data.file || !data.file.list || data.file.list.length === 0)) {
938 + // courseFile.value = {
939 + // cover: "https://cdn.ipadbiz.cn/space/Fk_utCrNnT3K-RnMPAeHinChU0vC.jpg",
940 + // list: [{
941 + // meta_id: 361387,
942 + // title: "d6fd76508747c15f2059c868e6e1433d.mp4",
943 + // url: "https://cdn.ipadbiz.cn/space/lk3DmvLO02dUC2zPiFwiClDe3nKL.mp4"
944 + // }]
945 + // };
946 + // }
947 +
707 // 音频列表处理 948 // 音频列表处理
708 if (data.course_type === 'audio') { 949 if (data.course_type === 'audio') {
709 audioList.value = data.file.list; 950 audioList.value = data.file.list;
...@@ -1095,6 +1336,82 @@ const downloadFile = ({ title, url, meta_id }) => { ...@@ -1095,6 +1336,82 @@ const downloadFile = ({ title, url, meta_id }) => {
1095 } 1336 }
1096 1337
1097 /** 1338 /**
1339 + * 在新窗口中打开文件
1340 + * @param {Object} file - 文件对象,包含title、url、meta_id
1341 + */
1342 +const openFileInNewWindow = ({ title, url, meta_id }) => {
1343 + // 在新窗口中打开文件URL
1344 + window.open(url, '_blank');
1345 +
1346 + // 记录访问行为
1347 + let paramsObj = {
1348 + schedule_id: courseId.value,
1349 + meta_id
1350 + }
1351 + addRecord(paramsObj);
1352 +}
1353 +
1354 +/**
1355 + * 判断文件是否可以在新窗口中打开
1356 + * @param {string} fileName - 文件名
1357 + * @returns {boolean} 是否可以在新窗口中打开
1358 + */
1359 +const canOpenInNewWindow = (fileName) => {
1360 + if (!fileName || typeof fileName !== 'string') {
1361 + return false;
1362 + }
1363 +
1364 + const extension = fileName.split('.').pop().toLowerCase();
1365 + const supportedTypes = ['pdf', 'jpg', 'jpeg', 'png', 'gif', 'mp3', 'aac', 'wav', 'ogg', 'mp4', 'avi', 'mov'];
1366 + return supportedTypes.includes(extension);
1367 +}
1368 +
1369 +/**
1370 + * 判断文件是否为音频文件
1371 + * @param {string} fileName - 文件名
1372 + * @returns {boolean} 是否为音频文件
1373 + */
1374 +const isAudioFile = (fileName) => {
1375 + if (!fileName || typeof fileName !== 'string') {
1376 + return false;
1377 + }
1378 +
1379 + const extension = fileName.split('.').pop().toLowerCase();
1380 + const audioTypes = ['mp3', 'aac', 'wav', 'ogg'];
1381 + return audioTypes.includes(extension);
1382 +}
1383 +
1384 +/**
1385 + * 判断文件是否为视频文件
1386 + * @param {string} fileName - 文件名
1387 + * @returns {boolean} 是否为视频文件
1388 + */
1389 +const isVideoFile = (fileName) => {
1390 + if (!fileName || typeof fileName !== 'string') {
1391 + return false;
1392 + }
1393 +
1394 + const extension = fileName.split('.').pop().toLowerCase();
1395 + const videoTypes = ['mp4', 'avi', 'mov'];
1396 + return videoTypes.includes(extension);
1397 +}
1398 +
1399 +/**
1400 + * 判断文件是否为图片文件
1401 + * @param {string} fileName - 文件名
1402 + * @returns {boolean} 是否为图片文件
1403 + */
1404 +const isImageFile = (fileName) => {
1405 + if (!fileName || typeof fileName !== 'string') {
1406 + return false;
1407 + }
1408 +
1409 + const extension = fileName.split('.').pop().toLowerCase();
1410 + const imageTypes = ['jpg', 'jpeg', 'png', 'gif'];
1411 + return imageTypes.includes(extension);
1412 +}
1413 +
1414 +/**
1098 * 音频播放事件 1415 * 音频播放事件
1099 * @param audio 音频对象 1416 * @param audio 音频对象
1100 */ 1417 */
......