feat(课程页面): 添加视频播放错误处理和媒体文件预览功能
为视频播放器添加错误处理覆盖层和自动重试机制,优化视频加载配置 在课程详情页增加音频和视频播放弹窗,改进文件预览功能 根据设备类型显示不同的文件操作按钮,支持多种媒体格式预览
Showing
3 changed files
with
505 additions
and
26 deletions
| ... | @@ -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,25 +509,76 @@ | ... | @@ -446,25 +509,76 @@ |
| 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 | + <!-- 桌面端:显示在线查看、新窗口打开和下载文件按钮 --> |
| 451 | - <button | 514 | + <template v-if="isDesktop"> |
| 452 | - v-if="file.url && file.url.toLowerCase().includes('.pdf')" | 515 | + <!-- 新窗口打开按钮 - 只对图片、音频、视频和PDF文件显示 --> |
| 453 | - @click="showPdf(file)" | 516 | + <button |
| 454 | - class="btn-primary flex-1 py-2.5 text-sm font-medium flex items-center justify-center gap-2" | 517 | + v-if="canOpenInNewWindow(file.title)" |
| 455 | - > | 518 | + @click="openFileInNewWindow(file)" |
| 456 | - <van-icon name="eye-o" size="16" /> | 519 | + class="btn-primary flex-1 py-2.5 text-sm font-medium flex items-center justify-center gap-2" |
| 457 | - 在线查看 | 520 | + > |
| 458 | - </button> | 521 | + <van-icon name="eye-o" size="16" /> |
| 459 | - <!-- 非PDF文件只显示下载按钮 --> | 522 | + 在线查看 |
| 460 | - <button | 523 | + </button> |
| 461 | - v-else | 524 | + <!-- 所有文件都显示下载按钮 --> |
| 462 | - @click="downloadFile(file)" | 525 | + <button |
| 463 | - class="btn-secondary flex-1 py-2.5 text-sm font-medium flex items-center justify-center gap-2" | 526 | + @click="downloadFile(file)" |
| 464 | - > | 527 | + class="btn-secondary flex-1 py-2.5 text-sm font-medium flex items-center justify-center gap-2" |
| 465 | - <van-icon name="down" size="16" /> | 528 | + > |
| 466 | - 下载文件 | 529 | + <van-icon name="down" size="16" /> |
| 467 | - </button> | 530 | + 下载文件 |
| 531 | + </button> | ||
| 532 | + </template> | ||
| 533 | + | ||
| 534 | + <!-- 移动端:根据文件类型显示不同的预览按钮 --> | ||
| 535 | + <template v-else> | ||
| 536 | + <!-- PDF文件显示在线查看按钮 --> | ||
| 537 | + <button | ||
| 538 | + v-if="file.url && file.url.toLowerCase().includes('.pdf')" | ||
| 539 | + @click="showPdf(file)" | ||
| 540 | + class="btn-primary flex-1 py-2.5 text-sm font-medium flex items-center justify-center gap-2" | ||
| 541 | + > | ||
| 542 | + <van-icon name="eye-o" size="16" /> | ||
| 543 | + 在线查看 | ||
| 544 | + </button> | ||
| 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 | + <!-- 其他文件显示下载按钮 --> | ||
| 573 | + <button | ||
| 574 | + v-else | ||
| 575 | + @click="downloadFile(file)" | ||
| 576 | + class="btn-secondary flex-1 py-2.5 text-sm font-medium flex items-center justify-center gap-2" | ||
| 577 | + > | ||
| 578 | + <van-icon name="down" size="16" /> | ||
| 579 | + 下载文件 | ||
| 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 | */ | ... | ... |
-
Please register or login to post a comment