From 9b6ad60913cb86cb453048a884645587a806bfe9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E5=BC=A0?= <2091066548@qq.com> Date: Tue, 23 Sep 2025 14:31:39 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E6=92=AD=E6=94=BE=E5=99=A8?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=B8=85=E6=99=B0=E5=BA=A6=EF=BC=8C=E7=84=B6?= =?UTF-8?q?=E5=90=8E=E8=AE=A8=E8=AE=BA=E6=B7=BB=E5=8A=A0icon?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/modules/course.ts | 30 ++ src/components/course/DPlayerVideo.vue | 645 ++++++++++++++++++------- src/views/CourseExchanged.vue | 233 +++++---- 3 files changed, 618 insertions(+), 290 deletions(-) diff --git a/src/api/modules/course.ts b/src/api/modules/course.ts index 4b1f4a4..085d7d5 100644 --- a/src/api/modules/course.ts +++ b/src/api/modules/course.ts @@ -901,6 +901,36 @@ export class CourseApi { } } + // 获取章节练习 + static async getSectionExercise(courseId: string, sectionId: string): Promise> { + try { + console.log('🚀 调用章节练习API,课程ID:', courseId, '章节ID:', sectionId) + + const response = await ApiRequest.get(`/aiol/aiolCourse/${courseId}/section_exercise/${sectionId}`) + console.log('🔍 章节练习API响应:', response) + + return response + } catch (error) { + console.error('❌ 获取章节练习失败:', error) + throw error + } + } + + // 获取章节讨论 + static async getSectionDiscussion(courseId: string, sectionId: string): Promise> { + try { + console.log('🚀 调用章节讨论API,课程ID:', courseId, '章节ID:', sectionId) + + const response = await ApiRequest.get(`/aiol/aiolCourse/${courseId}/section_discussion/${sectionId}`) + console.log('🔍 章节讨论API响应:', response) + + return response + } catch (error) { + console.error('❌ 获取章节讨论失败:', error) + throw error + } + } + // 获取章节资料 static async getSectionDocument(courseId: string, sectionId: string): Promise> { try { diff --git a/src/components/course/DPlayerVideo.vue b/src/components/course/DPlayerVideo.vue index b2d0f42..e228149 100644 --- a/src/components/course/DPlayerVideo.vue +++ b/src/components/course/DPlayerVideo.vue @@ -18,28 +18,7 @@ - -
-
- 清晰度: -
- -
-
- {{ quality.label }} -
-
-
-
-
+ @@ -89,7 +68,14 @@ const isPlaying = ref(false) // 功能状态 const isPictureInPicture = ref(false) -const showQualityMenu = ref(false) +const qualityButtonCreated = ref(false) // 跟踪清晰度按钮是否已创建 + + +// HLS.js已在index.html中加载,无需动态加载函数 + + + + // 加载 DPlayer const loadDPlayer = (): Promise => { @@ -153,8 +139,24 @@ const initializePlayer = async (videoUrl?: string) => { try { await loadDPlayer() + + const url = videoUrl || props.videoUrl + + // 确保url是字符串 + if (!url || typeof url !== 'string') { + console.error('❌ 视频URL无效:', url) + throw new Error('Invalid video URL') + } + + const isHLS = url.includes('.m3u8') + + // HLS.js已在index.html中加载,无需额外处理 + if (isHLS) { + console.log('🔧 检测到HLS流,使用index.html中的HLS.js') + } + await nextTick() - + if (!dplayerContainer.value) return // 检查容器尺寸 @@ -171,7 +173,6 @@ const initializePlayer = async (videoUrl?: string) => { } const DPlayer = (window as any).DPlayer - const url = videoUrl || props.videoUrl // 清理之前的播放器实例 if (player) { @@ -183,16 +184,30 @@ const initializePlayer = async (videoUrl?: string) => { player = null } - // 检查是否为HLS流 - const isHLS = url.includes('.m3u8') console.log('🔍 视频类型检测:', { url, isHLS }) + // 测试HLS URL的可访问性 + if (isHLS) { + console.log('🔍 测试HLS URL可访问性...') + fetch(url, { method: 'HEAD' }) + .then(response => { + console.log('✅ HLS URL可访问:', { + status: response.status, + statusText: response.statusText, + headers: Object.fromEntries(response.headers.entries()) + }) + }) + .catch(error => { + console.error('❌ HLS URL不可访问:', error) + }) + } + // 构建DPlayer配置 const dplayerConfig: any = { container: dplayerContainer.value, video: { url: url, - type: isHLS ? 'hls' : 'auto' + type: 'auto' // 统一使用auto,让DPlayer自动检测 }, autoplay: props.autoplay, theme: '#007bff', @@ -203,6 +218,9 @@ const initializePlayer = async (videoUrl?: string) => { playbackSpeed: [0.5, 0.75, 1, 1.25, 1.5, 2], loop: false, screenshot: true, // 启用截屏功能 + mutex: true, // 互斥播放 + airplay: true, // 启用AirPlay + chromecast: true, // 启用Chromecast contextmenu: [ { text: '截屏', @@ -215,40 +233,62 @@ const initializePlayer = async (videoUrl?: string) => { ] } - // 如果有多个清晰度,添加清晰度切换功能 + // 暂时禁用DPlayer原生清晰度功能,避免配置错误 if (props.videoQualities && props.videoQualities.length > 1) { - console.log('🔧 配置多清晰度支持:', props.videoQualities) - dplayerConfig.quality = { - default: props.currentQuality || props.videoQualities[0]?.value, - options: props.videoQualities.map(q => ({ - name: q.label, - url: q.url, - type: isHLS ? 'hls' : 'auto' - })) + console.log('🔧 检测到多清晰度,但暂时禁用DPlayer原生清晰度功能以避免错误') + console.log('🔧 清晰度数据:', props.videoQualities) + + // 验证清晰度数据的完整性 + const validQualities = props.videoQualities.filter(q => + q && q.url && typeof q.url === 'string' && q.label + ) + + console.log('🔧 有效的清晰度数据:', validQualities) + + if (validQualities.length !== props.videoQualities.length) { + console.warn('⚠️ 部分清晰度数据无效,已过滤') } + + // 暂时不配置DPlayer原生清晰度,使用手动按钮 + // dplayerConfig.quality = { ... } + + } else { + console.log('🔧 单一清晰度或无清晰度数据') } - // 如果是HLS流,添加HLS.js支持 - if (isHLS) { - console.log('🔧 配置HLS支持') + // 如果是HLS流,配置HLS.js支持 + if (isHLS && (window as any).Hls) { + console.log('🔧 配置HLS.js支持') + + // DPlayer使用'hls'类型来启用HLS.js + dplayerConfig.video.type = 'hls' + + // 添加HLS.js配置 dplayerConfig.pluginOptions = { hls: { - // HLS.js配置选项 + enableWorker: true, + lowLatencyMode: false, + backBufferLength: 90 } } + + console.log('✅ HLS.js配置完成:', { + hlsAvailable: !!(window as any).Hls, + hlsSupported: (window as any).Hls?.isSupported(), + videoType: dplayerConfig.video.type + }) + } else if (isHLS) { + console.warn('⚠️ HLS流但HLS.js未加载,尝试原生播放') + dplayerConfig.video.type = 'auto' } - // 如果有多个清晰度,启用DPlayer原生清晰度切换 - console.log('🔍 检查清晰度配置:', { + // 最终配置检查 + console.log('🔍 最终DPlayer配置检查:', { videoQualities: props.videoQualities, currentQuality: props.currentQuality, - qualitiesLength: props.videoQualities?.length - }) - - // 使用自定义清晰度选择器,不依赖DPlayer原生quality功能 - console.log('✅ 使用自定义清晰度选择器:', { - mainVideoUrl: url, - availableQualities: props.videoQualities?.length || 0 + qualitiesLength: props.videoQualities?.length, + hasQualityConfig: !!dplayerConfig.quality, + qualityConfig: dplayerConfig.quality }) console.log('🔨 创建DPlayer实例,配置:', { @@ -257,14 +297,67 @@ const initializePlayer = async (videoUrl?: string) => { }) player = new DPlayer(dplayerConfig) + // 让DPlayer自动处理HLS,不手动集成 + if (isHLS) { + console.log('🔧 检测到HLS流,让DPlayer自动处理') + } + + // 强制设置视频样式,确保填满容器 + setTimeout(() => { + const videoElement = player.video + if (videoElement) { + console.log('🔧 强制设置视频样式以填满容器') + videoElement.style.width = '100%' + videoElement.style.height = '100%' + videoElement.style.objectFit = 'cover' // 裁剪黑边 + + // 也设置视频包装器 + const videoWrap = dplayerContainer.value?.querySelector('.dplayer-video-wrap') + if (videoWrap) { + ;(videoWrap as HTMLElement).style.width = '100%' + ;(videoWrap as HTMLElement).style.height = '100%' + } + + // 设置DPlayer容器 + const dplayerElement = dplayerContainer.value?.querySelector('.dplayer') + if (dplayerElement) { + ;(dplayerElement as HTMLElement).style.width = '100%' + ;(dplayerElement as HTMLElement).style.height = '100%' + } + } + }, 100) + // 检查DPlayer是否正确加载了quality配置 console.log('🔍 DPlayer实例创建完成:', { hasQuality: !!player.quality, qualityOptions: player.quality?.options, qualityDefault: player.quality?.default, - videoElement: player.video + videoElement: player.video, + playerMethods: Object.keys(player), + dplayerConfigQuality: dplayerConfig.quality }) + // 延迟检查,因为DPlayer可能需要时间来初始化quality功能 + setTimeout(() => { + console.log('🔍 延迟检查DPlayer质量对象:', { + hasQuality: !!player.quality, + qualityObject: player.quality, + containerHTML: dplayerContainer.value?.innerHTML?.substring(0, 200) + '...' + }) + + // 检查DOM中是否有质量相关的元素 + const qualityElements = dplayerContainer.value?.querySelectorAll('[class*="quality"]') + console.log('🔍 DOM中的质量相关元素:', qualityElements) + + // 如果有多个清晰度,创建手动清晰度按钮 + if (props.videoQualities && props.videoQualities.length > 1) { + console.log('🔧 创建手动清晰度按钮(多清晰度支持)') + createManualQualityButton() + } else { + console.log('🔧 单一清晰度,不需要清晰度按钮') + } + }, 2000) + // 事件监听 player.on('play', () => { console.log('🎬 视频开始播放') @@ -344,17 +437,46 @@ const initializePlayer = async (videoUrl?: string) => { player.on('loadeddata', () => { console.log('✅ 视频数据加载完成:', url) - // 在视频加载完成后,尝试手动设置清晰度选项 + // 再次强制设置视频样式,确保填满容器 + const videoElement = player.video + if (videoElement) { + console.log('🔧 视频加载完成后强制设置样式以填满容器') + videoElement.style.width = '100%' + videoElement.style.height = '100%' + videoElement.style.objectFit = 'fill' + } + + // 在视频加载完成后,检查DPlayer原生清晰度功能 if (props.videoQualities && props.videoQualities.length > 1) { setTimeout(() => { - console.log('🔍 尝试手动设置清晰度选项') + console.log('🔍 检查DPlayer原生清晰度功能') if (player && player.quality) { - console.log('✅ DPlayer quality对象存在:', player.quality) + console.log('✅ DPlayer quality对象存在:', { + quality: player.quality, + options: player.quality.options, + current: player.quality.current + }) + + // 监听DPlayer原生清晰度切换事件 + player.on('quality_start', (quality: any) => { + console.log('🔄 DPlayer原生清晰度切换开始:', quality) + }) + + player.on('quality_end', (quality: any) => { + console.log('✅ DPlayer原生清晰度切换完成:', quality) + // 通知父组件清晰度已切换 + const qualityValue = props.videoQualities.find(q => q.label === quality.name)?.value + if (qualityValue) { + emit('qualityChange', qualityValue) + } + }) + } else { - console.log('❌ DPlayer quality对象不存在,尝试其他方法') + console.log('❌ DPlayer quality对象不存在,检查DOM结构') // 尝试直接在DOM中查找清晰度按钮 const qualityBtn = dplayerContainer.value?.querySelector('.dplayer-quality-button') - console.log('🔍 查找清晰度按钮:', qualityBtn) + const qualityList = dplayerContainer.value?.querySelector('.dplayer-quality-list') + console.log('🔍 DOM中的清晰度元素:', { qualityBtn, qualityList }) } }, 1000) } @@ -438,6 +560,147 @@ const togglePictureInPicture = async () => { +// 手动创建清晰度按钮 +const createManualQualityButton = () => { + if (!dplayerContainer.value || !player) return + + // 如果按钮已经创建过,只更新文字 + if (qualityButtonCreated.value) { + const existingButton = dplayerContainer.value.querySelector('.manual-quality .dplayer-quality-button') + if (existingButton) { + existingButton.textContent = getCurrentQualityLabel() + console.log('✅ 更新已存在按钮文字为:', getCurrentQualityLabel()) + } + return + } + + console.log('🔧 首次创建清晰度按钮') + + // 检查DOM中是否已存在按钮 + const existingButton = dplayerContainer.value.querySelector('.manual-quality') + if (existingButton) { + console.log('🔧 DOM中已存在按钮,标记为已创建') + qualityButtonCreated.value = true + return + } + + // 查找DPlayer控制栏 + const controlBar = dplayerContainer.value.querySelector('.dplayer-controller') + if (!controlBar) { + console.error('❌ 找不到DPlayer控制栏') + return + } + + // 创建清晰度按钮容器 + const qualityContainer = document.createElement('div') + qualityContainer.className = 'dplayer-quality manual-quality' + qualityContainer.style.cssText = ` + position: absolute !important; + right: 180px !important; + top: 50% !important; + transform: translateY(-50%) !important; + z-index: 1000 !important; + display: inline-block; + cursor: pointer; + min-width: 50px; + text-align: center; + background: none; + ` + + // 创建清晰度按钮 + const qualityButton = document.createElement('button') + qualityButton.className = 'dplayer-quality-button' + qualityButton.textContent = getCurrentQualityLabel() + qualityButton.style.cssText = ` + background: none; + border: none; + color: white; + padding: 4px 8px; + font-size: 12px; + cursor: pointer; + user-select: none; + white-space: nowrap; + min-width: 50px; + text-align: center; + transition: none; + ` + + // 创建清晰度菜单 + const qualityMenu = document.createElement('div') + qualityMenu.className = 'dplayer-quality-list' + qualityMenu.style.cssText = ` + position: absolute; + bottom: 100%; + right: 0; + background: rgba(0, 0, 0, 0.9); + border-radius: 3px; + margin-bottom: 8px; + display: none; + min-width: 80px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + ` + + // 添加清晰度选项 + props.videoQualities.forEach(quality => { + const option = document.createElement('div') + option.className = 'dplayer-quality-item' + option.textContent = quality.label + option.style.cssText = ` + padding: 8px 12px; + color: #fff; + font-size: 13px; + cursor: pointer; + transition: background-color 0.2s; + text-align: center; + ${quality.value === props.currentQuality ? 'background: #007bff;' : ''} + ` + + option.addEventListener('mouseenter', () => { + if (quality.value !== props.currentQuality) { + option.style.backgroundColor = 'rgba(255, 255, 255, 0.1)' + } + }) + + option.addEventListener('mouseleave', () => { + if (quality.value !== props.currentQuality) { + option.style.backgroundColor = 'transparent' + } + }) + + option.addEventListener('click', () => { + console.log('🔄 手动切换清晰度到:', quality.label) + switchQuality(quality) + qualityMenu.style.display = 'none' + qualityButton.textContent = quality.label + }) + + qualityMenu.appendChild(option) + }) + + // 按钮点击事件 + qualityButton.addEventListener('click', (e) => { + e.stopPropagation() + const isVisible = qualityMenu.style.display === 'block' + qualityMenu.style.display = isVisible ? 'none' : 'block' + }) + + // 点击其他地方关闭菜单 + document.addEventListener('click', () => { + qualityMenu.style.display = 'none' + }) + + qualityContainer.appendChild(qualityButton) + qualityContainer.appendChild(qualityMenu) + + // 直接插入到控制栏,使用绝对定位 + controlBar.appendChild(qualityContainer) + + // 标记按钮已创建 + qualityButtonCreated.value = true + + console.log('✅ 手动清晰度按钮创建完成') +} + // 清晰度切换相关函数 const getCurrentQualityLabel = () => { const current = props.videoQualities.find(q => q.value === props.currentQuality) @@ -445,13 +708,38 @@ const getCurrentQualityLabel = () => { } const switchQuality = (quality: any) => { + console.log('🔄 开始切换清晰度:', { + quality: quality, + hasPlayer: !!player, + hasUrl: !!quality.url, + qualityValue: quality.value, + qualityLabel: quality.label + }) + + if (!quality || !quality.url) { + console.error('❌ 清晰度数据无效:', quality) + return + } + + // 设置切换清晰度标志,防止URL变化触发播放器重新初始化 + isSwitchingQuality.value = true + console.log('🔧 设置切换清晰度标志,防止播放器重新初始化') + + // 添加超时保护,防止标志永远不被重置 + setTimeout(() => { + if (isSwitchingQuality.value) { + console.log('⚠️ 切换清晰度超时,强制重置标志') + isSwitchingQuality.value = false + } + }, 10000) // 10秒超时 + if (player && quality.url) { try { // 记录当前播放状态 const currentTime = player.video?.currentTime || 0 const wasPlaying = !player.video?.paused - console.log('🔄 开始切换清晰度:', { + console.log('🔄 切换清晰度详情:', { from: getCurrentQualityLabel(), to: quality.label, currentTime: currentTime, @@ -464,35 +752,85 @@ const switchQuality = (quality: any) => { player.pause() } - // 销毁当前播放器 - if (player) { - player.destroy() - player = null - } + // 不销毁播放器,直接切换视频源 + console.log('🔄 直接切换视频源,不重建播放器') - // 重新初始化播放器使用新的URL - initializePlayer(quality.url).then(() => { - console.log('✅ 播放器重新初始化完成,新URL:', quality.url) - // 恢复播放时间 - setTimeout(() => { - if (player && player.video) { - player.seek(currentTime) + if (player && player.video) { + console.log('🔧 当前视频源:', player.video.src) + console.log('🔧 切换到新视频源:', quality.url) + + // 使用DPlayer的switchVideo方法来切换视频源 + if (typeof player.switchVideo === 'function') { + console.log('🔧 使用DPlayer的switchVideo方法') + player.switchVideo({ + url: quality.url, + type: quality.url.includes('.m3u8') ? 'hls' : 'auto' + }) + + // 监听视频加载完成 + const handleCanPlay = () => { + console.log('✅ 视频切换完成,恢复播放状态') + player.video.currentTime = currentTime if (wasPlaying) { player.play() } - console.log('✅ 恢复播放状态:', { currentTime, wasPlaying }) + + // 重置切换清晰度标志 + setTimeout(() => { + isSwitchingQuality.value = false + console.log('🔄 清晰度切换完成,重置标志') + }, 500) + + player.video.removeEventListener('canplay', handleCanPlay) } - }, 500) - }).catch(error => { - console.error('❌ 重新初始化播放器失败:', error) - }) + + player.video.addEventListener('canplay', handleCanPlay, { once: true }) + } else { + console.log('🔧 DPlayer不支持switchVideo,使用原生方法') + // 直接更换视频源 + player.video.src = quality.url + player.video.load() + + const handleLoadedMetadata = () => { + player.video.currentTime = currentTime + if (wasPlaying) { + player.play() + } + console.log('✅ 视频源切换完成,播放状态已恢复') + + // 重置切换清晰度标志 + setTimeout(() => { + isSwitchingQuality.value = false + console.log('🔄 清晰度切换完成,重置标志') + }, 500) + } + + player.video.addEventListener('loadedmetadata', handleLoadedMetadata, { once: true }) + } + + // 立即更新按钮文字,不等待视频加载 + const existingButton = dplayerContainer.value?.querySelector('.manual-quality .dplayer-quality-button') + if (existingButton) { + existingButton.textContent = quality.label + console.log('✅ 清晰度按钮文字已立即更新为:', quality.label) + } + } + + // 通知父组件清晰度已切换 emit('qualityChange', quality.value) + + + console.log('✅ 切换清晰度到:', quality.label) } catch (error) { console.error('❌ 切换清晰度失败:', error) + // 出错时也要重置标志 + isSwitchingQuality.value = false } + } else { + console.error('❌ 播放器未初始化或清晰度URL无效') } } @@ -505,18 +843,36 @@ const setVolume = (volume: number) => { const destroy = () => { if (player) { try { + // 清理手动创建的清晰度按钮 + const manualQuality = dplayerContainer.value?.querySelector('.manual-quality') + if (manualQuality) { + console.log('🧹 清理手动创建的清晰度按钮') + manualQuality.remove() + } + player.destroy() } catch (e) { console.log('销毁播放器时出错:', e) } player = null playerInitialized.value = false + qualityButtonCreated.value = false // 重置按钮创建标志 } } +// 添加标志来跟踪是否正在切换清晰度 +const isSwitchingQuality = ref(false) + // 监听视频URL变化 watch(() => props.videoUrl, (newUrl) => { + // 如果正在切换清晰度,不重新初始化播放器 + if (isSwitchingQuality.value) { + console.log('🔄 正在切换清晰度,忽略URL变化') + return + } + if (newUrl && playerInitialized.value) { + console.log('🔄 视频URL变化,重新初始化播放器:', newUrl) initializePlayer(newUrl) } }) @@ -550,23 +906,58 @@ onUnmounted(() => { \ No newline at end of file +