diff --git a/src/components/course/DPlayerVideo.vue b/src/components/course/DPlayerVideo.vue index f832b37..30f13af 100644 --- a/src/components/course/DPlayerVideo.vue +++ b/src/components/course/DPlayerVideo.vue @@ -16,24 +16,53 @@ - -
-
- -
-
- {{ quality.label }} + +
+ +
+ +
+ +
+
+ {{ quality.label }} +
+ + + + + + + + +
+ +
@@ -72,12 +101,19 @@ const emit = defineEmits<{ ended: [] error: [error: any] qualityChange: [quality: string] + screenshot: [dataUrl: string] + danmakuSend: [text: string] }>() const dplayerContainer = ref() let player: any = null const playerInitialized = ref(false) const isPlaying = ref(false) + +// 新增功能状态 +const danmakuEnabled = ref(true) +const danmakuText = ref('') +const isPictureInPicture = ref(false) const showQualityMenu = ref(false) // 加载 DPlayer @@ -145,7 +181,20 @@ const initializePlayer = async (videoUrl?: string) => { await nextTick() if (!dplayerContainer.value) return - + + // 检查容器尺寸 + const containerRect = dplayerContainer.value.getBoundingClientRect() + console.log('🔍 DPlayer容器尺寸:', { + width: containerRect.width, + height: containerRect.height, + top: containerRect.top, + left: containerRect.left + }) + + if (containerRect.height === 0) { + console.warn('⚠️ DPlayer容器高度为0,可能影响播放') + } + const DPlayer = (window as any).DPlayer const url = videoUrl || props.videoUrl @@ -159,7 +208,8 @@ const initializePlayer = async (videoUrl?: string) => { player = null } - player = new DPlayer({ + // 构建DPlayer配置 + const dplayerConfig: any = { container: dplayerContainer.value, video: { url: url, @@ -173,12 +223,50 @@ const initializePlayer = async (videoUrl?: string) => { volume: 0.8, playbackSpeed: [0.5, 0.75, 1, 1.25, 1.5, 2], loop: false, + screenshot: true, // 启用截屏功能 + danmaku: { + id: 'course-video-' + Date.now(), + api: '/api/danmaku/', // 弹幕API地址 + token: 'demo-token', + maximum: 1000, + // 移除有问题的addition API + // addition: ['https://api.prprpr.me/dplayer/'], + user: 'student', + bottom: '15%', + unlimited: true + }, contextmenu: [ + { + text: '截屏', + click: () => takeScreenshot() + }, { text: '关于 DPlayer', link: 'https://github.com/DIYGod/DPlayer' } ] + } + + // 如果有多个清晰度,启用DPlayer原生清晰度切换 + console.log('🔍 检查清晰度配置:', { + videoQualities: props.videoQualities, + currentQuality: props.currentQuality, + qualitiesLength: props.videoQualities?.length + }) + + // 使用自定义清晰度选择器,不依赖DPlayer原生quality功能 + console.log('✅ 使用自定义清晰度选择器:', { + mainVideoUrl: url, + availableQualities: props.videoQualities?.length || 0 + }) + + player = new DPlayer(dplayerConfig) + + // 检查DPlayer是否正确加载了quality配置 + console.log('🔍 DPlayer实例创建完成:', { + hasQuality: !!player.quality, + qualityOptions: player.quality?.options, + playerConfig: dplayerConfig }) // 事件监听 @@ -198,10 +286,42 @@ const initializePlayer = async (videoUrl?: string) => { }) player.on('error', (error: any) => { - console.error('DPlayer error:', error) + console.error('DPlayer 播放错误:', error) + console.error('错误详情:', { + type: error.type, + message: error.message, + url: url + }) emit('error', error) }) + player.on('loadstart', () => { + console.log('🔍 视频开始加载:', url) + }) + + player.on('canplay', () => { + console.log('✅ 视频可以播放:', url) + }) + + player.on('loadeddata', () => { + console.log('✅ 视频数据加载完成:', url) + + // 在视频加载完成后,尝试手动设置清晰度选项 + if (props.videoQualities && props.videoQualities.length > 1) { + setTimeout(() => { + console.log('🔍 尝试手动设置清晰度选项') + if (player && player.quality) { + console.log('✅ DPlayer quality对象存在:', player.quality) + } else { + console.log('❌ DPlayer quality对象不存在,尝试其他方法') + // 尝试直接在DOM中查找清晰度按钮 + const qualityBtn = dplayerContainer.value?.querySelector('.dplayer-quality-button') + console.log('🔍 查找清晰度按钮:', qualityBtn) + } + }, 1000) + } + }) + playerInitialized.value = true console.log('DPlayer 初始化成功:', url) } catch (err) { @@ -210,30 +330,7 @@ const initializePlayer = async (videoUrl?: string) => { } } -// 切换视频清晰度 -const changeVideoQuality = (quality: string) => { - const qualityVideo = props.videoQualities.find(q => q.value === quality) - if (qualityVideo && player) { - try { - // 尝试使用 DPlayer 的 switchVideo 方法 - if (typeof player.switchVideo === 'function') { - player.switchVideo({ - url: qualityVideo.url, - type: 'auto' - }) - } else { - // 如果没有 switchVideo 方法,重新初始化播放器 - initializePlayer(qualityVideo.url) - } - emit('qualityChange', quality) - console.log('切换清晰度到:', quality) - } catch (error) { - console.error('切换清晰度失败:', error) - // 重新初始化播放器作为后备方案 - initializePlayer(qualityVideo.url) - } - } -} + // 播放控制方法 const play = () => { @@ -254,6 +351,144 @@ const seek = (time: number) => { } } +// 截屏功能 +const takeScreenshot = () => { + if (player && player.video) { + try { + const canvas = document.createElement('canvas') + const ctx = canvas.getContext('2d') + const video = player.video + + canvas.width = video.videoWidth + canvas.height = video.videoHeight + + if (ctx) { + ctx.drawImage(video, 0, 0, canvas.width, canvas.height) + const dataUrl = canvas.toDataURL('image/png') + + // 创建下载链接 + const link = document.createElement('a') + link.download = `screenshot-${Date.now()}.png` + link.href = dataUrl + link.click() + + emit('screenshot', dataUrl) + console.log('截屏成功') + } + } catch (error) { + console.error('截屏失败:', error) + } + } +} + +// 画中画功能 +const togglePictureInPicture = async () => { + if (player && player.video) { + try { + if (document.pictureInPictureElement) { + await document.exitPictureInPicture() + isPictureInPicture.value = false + } else { + await player.video.requestPictureInPicture() + isPictureInPicture.value = true + } + } catch (error) { + console.error('画中画切换失败:', error) + } + } +} + +// 弹幕功能 +const toggleDanmaku = () => { + danmakuEnabled.value = !danmakuEnabled.value + if (player && player.danmaku) { + if (danmakuEnabled.value) { + player.danmaku.show() + } else { + player.danmaku.hide() + } + } +} + +const sendDanmaku = () => { + if (danmakuText.value.trim() && player && player.danmaku) { + const danmaku = { + text: danmakuText.value.trim(), + color: '#ffffff', + type: 'right' + } + + player.danmaku.send(danmaku) + emit('danmakuSend', danmakuText.value.trim()) + danmakuText.value = '' + console.log('发送弹幕:', danmaku.text) + } +} + +// 清晰度切换相关函数 +const getCurrentQualityLabel = () => { + const current = props.videoQualities.find(q => q.value === props.currentQuality) + return current ? current.label : '360p' +} + +const switchQuality = (quality: any) => { + if (player && quality.url) { + try { + // 记录当前播放状态 + const currentTime = player.video?.currentTime || 0 + const wasPlaying = !player.video?.paused + + console.log('🔄 开始切换清晰度:', { + from: getCurrentQualityLabel(), + to: quality.label, + currentTime: currentTime, + wasPlaying: wasPlaying + }) + + // 暂停播放 + if (wasPlaying) { + player.pause() + } + + // 切换视频源 + if (typeof player.switchVideo === 'function') { + player.switchVideo({ + url: quality.url, + type: 'auto' + }) + + // 恢复播放状态 + setTimeout(() => { + if (player && player.video) { + player.seek(currentTime) + if (wasPlaying) { + player.play() + } + } + }, 1000) + } else { + // 重新初始化播放器 + initializePlayer(quality.url).then(() => { + // 恢复播放时间 + setTimeout(() => { + if (player && player.video) { + player.seek(currentTime) + if (wasPlaying) { + player.play() + } + } + }, 500) + }) + } + + emit('qualityChange', quality.value) + console.log('✅ 切换清晰度到:', quality.label) + } catch (error) { + console.error('❌ 切换清晰度失败:', error) + } + } +} + const setVolume = (volume: number) => { if (player) { player.volume(volume / 100) @@ -286,7 +521,12 @@ defineExpose({ seek, setVolume, destroy, - initializePlayer + initializePlayer, + takeScreenshot, + togglePictureInPicture, + toggleDanmaku, + sendDanmaku, + switchQuality }) onMounted(() => { @@ -299,6 +539,8 @@ onMounted(() => { onUnmounted(() => { destroy() }) + + diff --git a/src/router/index.ts b/src/router/index.ts index fee3ab7..8c57ae8 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -6,6 +6,7 @@ import Home from '@/views/Home.vue' import Courses from '@/views/Courses.vue' import CourseDetail from '@/views/CourseDetail.vue' import CourseDetailEnrolled from '@/views/CourseDetailEnrolled.vue' +import CourseExchanged from '@/views/CourseExchanged.vue' import CourseStudy from '@/views/CourseStudy.vue' import Learning from '@/views/Learning.vue' import Profile from '@/views/Profile.vue' @@ -26,6 +27,7 @@ import SpecialTraining from '@/views/SpecialTraining.vue' import SpecialTrainingDetail from '@/views/SpecialTrainingDetail.vue' import HelpCenter from '@/views/HelpCenter.vue' import LearningCenter from '@/views/LearningCenter.vue' +import AICompanion from '@/views/AICompanion.vue' // ========== 管理员后台组件 ========== import AdminDashboard from '@/views/teacher/AdminDashboard.vue' @@ -311,6 +313,14 @@ const routes: RouteRecordRaw[] = [ meta: { title: '积分中心', requiresAuth: true } }, + // AI伴学 + { + path: '/ai-companion', + name: 'AICompanion', + component: AICompanion, + meta: { title: 'AI伴学' } + }, + // 首页与课程 { path: '/service-agreement', @@ -344,6 +354,12 @@ const routes: RouteRecordRaw[] = [ component: CourseDetailEnrolled, meta: { title: '课程详情 - 已报名' } }, + { + path: '/course/:id/exchanged', + name: 'CourseExchanged', + component: CourseExchanged, + meta: { title: '课程详情 - 已兑换', requiresAuth: true } + }, { path: '/course/study/:id', name: 'CourseStudy', diff --git a/src/views/AICompanion.vue b/src/views/AICompanion.vue index a7bf09c..c535edc 100644 --- a/src/views/AICompanion.vue +++ b/src/views/AICompanion.vue @@ -522,12 +522,12 @@
- +
- 第{{ chapterIndex + 1 }}章 - {{ chapter.title }} + 第{{ getChapterNumber(chapterIndex + 1) }}章 {{ chapter.title + }}
@@ -537,46 +537,39 @@
-
-
+ +
+
{{ getLessonTypeText(section) }}
- {{ section.name - }} + {{ section.name }}
- {{ formatLessonDuration(section) }} + {{ + formatLessonDuration(section) }}
- - - - - - - - -
@@ -714,7 +707,7 @@