feat:章节对应的视频,视频url切割,可切换清晰度
This commit is contained in:
parent
f4a5f6f782
commit
6bdd7d6999
@ -16,6 +16,9 @@ import type {
|
|||||||
CourseSectionListResponse,
|
CourseSectionListResponse,
|
||||||
BackendCourseSection,
|
BackendCourseSection,
|
||||||
BackendInstructor,
|
BackendInstructor,
|
||||||
|
BackendSectionVideo,
|
||||||
|
SectionVideo,
|
||||||
|
VideoQuality,
|
||||||
Quiz,
|
Quiz,
|
||||||
LearningProgress,
|
LearningProgress,
|
||||||
SearchRequest,
|
SearchRequest,
|
||||||
@ -572,8 +575,8 @@ export class CourseApi {
|
|||||||
lessonId: section.courseId, // 使用courseId作为lessonId
|
lessonId: section.courseId, // 使用courseId作为lessonId
|
||||||
outline: '', // 暂时为空,根据type可以设置不同的内容
|
outline: '', // 暂时为空,根据type可以设置不同的内容
|
||||||
name: section.name,
|
name: section.name,
|
||||||
type: section.type,
|
type: section.type, // 保持原值,可能为null
|
||||||
parentId: section.parentId,
|
parentId: section.parentId || '', // 如果parentId为空字符串,保持为空字符串
|
||||||
sort: section.sortOrder,
|
sort: section.sortOrder,
|
||||||
level: section.level,
|
level: section.level,
|
||||||
revision: 1, // 默认版本号
|
revision: 1, // 默认版本号
|
||||||
@ -823,6 +826,128 @@ export class CourseApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取章节视频列表
|
||||||
|
static async getSectionVideos(courseId: string, sectionId: string): Promise<ApiResponse<SectionVideo[]>> {
|
||||||
|
try {
|
||||||
|
console.log('🔍 获取章节视频数据,课程ID:', courseId, '章节ID:', sectionId)
|
||||||
|
console.log('🔍 API请求URL: /biz/course/' + courseId + '/section_video/' + sectionId)
|
||||||
|
|
||||||
|
const response = await ApiRequest.get<any>(`/biz/course/${courseId}/section_video/${sectionId}`)
|
||||||
|
console.log('🔍 章节视频API响应:', response)
|
||||||
|
|
||||||
|
// 处理后端响应格式
|
||||||
|
if (response.data && response.data.success && response.data.result) {
|
||||||
|
console.log('✅ 响应状态码:', response.data.code)
|
||||||
|
console.log('✅ 响应消息:', response.data.message)
|
||||||
|
console.log('✅ 原始视频数据:', response.data.result)
|
||||||
|
console.log('✅ 视频数据数量:', response.data.result.length || 0)
|
||||||
|
|
||||||
|
// 适配数据格式
|
||||||
|
const adaptedVideos: SectionVideo[] = response.data.result.map((video: BackendSectionVideo) => {
|
||||||
|
// 解析fileUrl中的多个清晰度URL
|
||||||
|
const qualities = this.parseVideoQualities(video.fileUrl)
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: video.id,
|
||||||
|
name: video.name,
|
||||||
|
description: video.description,
|
||||||
|
type: video.type,
|
||||||
|
thumbnailUrl: video.thumbnailUrl,
|
||||||
|
duration: video.duration,
|
||||||
|
fileSize: video.fileSize,
|
||||||
|
qualities: qualities,
|
||||||
|
defaultQuality: '360', // 默认360p
|
||||||
|
currentQuality: '360' // 当前选中360p
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('✅ 适配后的视频数据:', adaptedVideos)
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: response.data.code,
|
||||||
|
message: response.data.message,
|
||||||
|
data: adaptedVideos
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('⚠️ API返回的数据结构不正确:', response.data)
|
||||||
|
return {
|
||||||
|
code: 500,
|
||||||
|
message: '数据格式错误',
|
||||||
|
data: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 章节视频API调用失败:', error)
|
||||||
|
console.error('❌ 错误详情:', {
|
||||||
|
message: (error as Error).message,
|
||||||
|
stack: (error as Error).stack,
|
||||||
|
response: (error as any).response?.data,
|
||||||
|
status: (error as any).response?.status,
|
||||||
|
statusText: (error as any).response?.statusText
|
||||||
|
})
|
||||||
|
|
||||||
|
// 重新抛出错误,不使用模拟数据
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析视频URL(逗号分隔的多个清晰度URL)
|
||||||
|
private static parseVideoQualities(fileUrl: string): VideoQuality[] {
|
||||||
|
const qualities: VideoQuality[] = []
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!fileUrl || fileUrl.trim() === '') {
|
||||||
|
console.warn('视频URL为空')
|
||||||
|
return qualities
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按逗号分割URL
|
||||||
|
const urls = fileUrl.split(',').map(url => url.trim()).filter(url => url.length > 0)
|
||||||
|
console.log('🔍 分割后的视频URL:', urls)
|
||||||
|
|
||||||
|
// 支持的清晰度列表(按优先级排序)
|
||||||
|
const supportedQualities = [
|
||||||
|
{ value: '1080', label: '1080p' },
|
||||||
|
{ value: '720', label: '720p' },
|
||||||
|
{ value: '480', label: '480p' },
|
||||||
|
{ value: '360', label: '360p' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// 根据URL数量分配清晰度
|
||||||
|
// 假设URL按清晰度从高到低排列:1080p, 720p, 480p, 360p
|
||||||
|
urls.forEach((url, index) => {
|
||||||
|
if (index < supportedQualities.length) {
|
||||||
|
qualities.push({
|
||||||
|
label: supportedQualities[index].label,
|
||||||
|
value: supportedQualities[index].value,
|
||||||
|
url: url
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 如果没有解析到任何清晰度,使用第一个URL作为360p
|
||||||
|
if (qualities.length === 0 && urls.length > 0) {
|
||||||
|
qualities.push({
|
||||||
|
label: '360p',
|
||||||
|
value: '360',
|
||||||
|
url: urls[0]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ 解析后的视频清晰度:', qualities)
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('解析视频清晰度失败,使用原始URL:', error)
|
||||||
|
// 如果解析失败,将整个fileUrl作为360p使用
|
||||||
|
qualities.push({
|
||||||
|
label: '360p',
|
||||||
|
value: '360',
|
||||||
|
url: fileUrl
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return qualities
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CourseApi
|
export default CourseApi
|
||||||
|
@ -371,7 +371,7 @@ export interface BackendCourseSection {
|
|||||||
id: string
|
id: string
|
||||||
courseId: string
|
courseId: string
|
||||||
name: string // 章节名称
|
name: string // 章节名称
|
||||||
type: number // 章节类型:0=视频、1=资料、2=考试、3=作业
|
type: number | null // 章节类型:0=视频、1=资料、2=考试、3=作业,null=未设置
|
||||||
sortOrder: number // 排序
|
sortOrder: number // 排序
|
||||||
parentId: string // 父章节ID
|
parentId: string // 父章节ID
|
||||||
level: number // 章节层级:0=一级章节、1=二级章节
|
level: number // 章节层级:0=一级章节、1=二级章节
|
||||||
@ -387,7 +387,7 @@ export interface CourseSection {
|
|||||||
lessonId: string // 改为string类型,与Course.id保持一致
|
lessonId: string // 改为string类型,与Course.id保持一致
|
||||||
outline: string // 章节大纲/内容链接
|
outline: string // 章节大纲/内容链接
|
||||||
name: string // 章节名称
|
name: string // 章节名称
|
||||||
type: number // 章节类型:0=视频、1=资料、2=考试、3=作业
|
type: number | null // 章节类型:0=视频、1=资料、2=考试、3=作业,null=未设置
|
||||||
parentId: string // 父章节ID,改为string类型
|
parentId: string // 父章节ID,改为string类型
|
||||||
sort: number // 排序(从sortOrder适配)
|
sort: number // 排序(从sortOrder适配)
|
||||||
level: number // 层级:0=一级章节、1=二级章节
|
level: number // 层级:0=一级章节、1=二级章节
|
||||||
@ -426,6 +426,55 @@ export interface BackendInstructorListResponse {
|
|||||||
timestamp: number
|
timestamp: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 后端章节视频数据结构
|
||||||
|
export interface BackendSectionVideo {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
type: number
|
||||||
|
fileUrl: string // 视频文件URL,包含多个清晰度
|
||||||
|
thumbnailUrl: string
|
||||||
|
duration: number
|
||||||
|
fileSize: number
|
||||||
|
metadata: string
|
||||||
|
izFeatured: number
|
||||||
|
status: number
|
||||||
|
createdBy: string
|
||||||
|
createdTime: string
|
||||||
|
updatedBy: string
|
||||||
|
updatedTime: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 后端章节视频列表响应格式
|
||||||
|
export interface BackendSectionVideoListResponse {
|
||||||
|
success: boolean
|
||||||
|
message: string
|
||||||
|
code: number
|
||||||
|
result: BackendSectionVideo[]
|
||||||
|
timestamp: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// 前端视频质量选项
|
||||||
|
export interface VideoQuality {
|
||||||
|
label: string // 显示名称:1080p, 720p, 480p, 360p
|
||||||
|
value: string // 实际值:1080, 720, 480, 360
|
||||||
|
url: string // 对应的视频URL
|
||||||
|
}
|
||||||
|
|
||||||
|
// 前端章节视频类型
|
||||||
|
export interface SectionVideo {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
type: number
|
||||||
|
thumbnailUrl: string
|
||||||
|
duration: number
|
||||||
|
fileSize: number
|
||||||
|
qualities: VideoQuality[] // 可用的视频质量选项
|
||||||
|
defaultQuality: string // 默认质量(360p)
|
||||||
|
currentQuality: string // 当前选中的质量
|
||||||
|
}
|
||||||
|
|
||||||
// 前端章节列表响应格式
|
// 前端章节列表响应格式
|
||||||
export interface CourseSectionListResponse {
|
export interface CourseSectionListResponse {
|
||||||
list: CourseSection[]
|
list: CourseSection[]
|
||||||
|
@ -49,6 +49,29 @@
|
|||||||
<p>请选择要播放的视频课程</p>
|
<p>请选择要播放的视频课程</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 清晰度选择器 -->
|
||||||
|
<div v-if="videoQualities.length > 1" class="video-quality-selector">
|
||||||
|
<div class="quality-dropdown">
|
||||||
|
<button class="quality-btn" @click="showQualityMenu = !showQualityMenu">
|
||||||
|
{{ currentQuality }}p
|
||||||
|
<svg width="12" height="12" viewBox="0 0 12 12" class="dropdown-icon">
|
||||||
|
<path d="M3 4.5l3 3 3-3" stroke="currentColor" stroke-width="1.5" fill="none" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div v-if="showQualityMenu" class="quality-menu">
|
||||||
|
<div
|
||||||
|
v-for="quality in videoQualities"
|
||||||
|
:key="quality.value"
|
||||||
|
class="quality-option"
|
||||||
|
:class="{ active: quality.value === currentQuality }"
|
||||||
|
@click="changeVideoQuality(quality.value); showQualityMenu = false"
|
||||||
|
>
|
||||||
|
{{ quality.label }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 底部交互区域 -->
|
<!-- 底部交互区域 -->
|
||||||
@ -377,7 +400,7 @@ import { ref, onMounted, onUnmounted, computed, nextTick } from 'vue'
|
|||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import { CourseApi } from '@/api/modules/course'
|
import { CourseApi } from '@/api/modules/course'
|
||||||
import type { Course, CourseSection } from '@/api/types'
|
import type { Course, CourseSection, SectionVideo, VideoQuality } from '@/api/types'
|
||||||
import SafeAvatar from '@/components/common/SafeAvatar.vue'
|
import SafeAvatar from '@/components/common/SafeAvatar.vue'
|
||||||
import LearningProgressStats from '@/components/common/LearningProgressStats.vue'
|
import LearningProgressStats from '@/components/common/LearningProgressStats.vue'
|
||||||
import NotesModal from '@/components/common/NotesModal.vue'
|
import NotesModal from '@/components/common/NotesModal.vue'
|
||||||
@ -405,6 +428,13 @@ const currentSection = ref<CourseSection | null>(null)
|
|||||||
const currentVideoUrl = ref<string>('')
|
const currentVideoUrl = ref<string>('')
|
||||||
const ckplayer = ref<any>(null)
|
const ckplayer = ref<any>(null)
|
||||||
|
|
||||||
|
// 视频相关状态
|
||||||
|
const currentVideo = ref<SectionVideo | null>(null)
|
||||||
|
const videoQualities = ref<VideoQuality[]>([])
|
||||||
|
const currentQuality = ref<string>('360') // 默认360p
|
||||||
|
const videoLoading = ref(false)
|
||||||
|
const showQualityMenu = ref(false)
|
||||||
|
|
||||||
// 视频源配置
|
// 视频源配置
|
||||||
const VIDEO_CONFIG = {
|
const VIDEO_CONFIG = {
|
||||||
// 本地视频(当前使用)
|
// 本地视频(当前使用)
|
||||||
@ -540,33 +570,44 @@ const generateMockSections = (): CourseSection[] => {
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将章节按章分组
|
// 将章节按章分组 - 根据后端数据结构重新实现
|
||||||
const groupSectionsByChapter = (sections: CourseSection[]) => {
|
const groupSectionsByChapter = (sections: CourseSection[]) => {
|
||||||
const chapterTitles = [
|
console.log('🔍 开始分组章节数据:', sections)
|
||||||
'课前准备',
|
|
||||||
'程序设计基础知识',
|
|
||||||
'实战项目',
|
|
||||||
'高级应用',
|
|
||||||
'拓展学习',
|
|
||||||
'答疑与交流'
|
|
||||||
]
|
|
||||||
|
|
||||||
const groups: ChapterGroup[] = []
|
const groups: ChapterGroup[] = []
|
||||||
let sectionsPerChapter = [4, 5, 6, 4, 3, 2] // 每章的课程数量
|
|
||||||
let sectionIndex = 0
|
|
||||||
|
|
||||||
for (let i = 0; i < chapterTitles.length; i++) {
|
// 找出所有一级章节(level=1,这些是父章节)
|
||||||
const chapterSections = sections.slice(sectionIndex, sectionIndex + sectionsPerChapter[i])
|
const parentChapters = sections.filter(section => section.level === 1)
|
||||||
if (chapterSections.length > 0) {
|
console.log('🔍 找到一级章节:', parentChapters)
|
||||||
groups.push({
|
|
||||||
title: chapterTitles[i],
|
// 为每个一级章节创建分组
|
||||||
sections: chapterSections,
|
parentChapters.forEach((parentChapter, index) => {
|
||||||
expanded: i === 0 // 默认展开第一章
|
// 找出该章节下的所有子章节(level=2,parentId匹配)
|
||||||
})
|
const childSections = sections.filter(section =>
|
||||||
}
|
section.level === 2 && section.parentId === parentChapter.id
|
||||||
sectionIndex += sectionsPerChapter[i]
|
)
|
||||||
|
|
||||||
|
console.log(`🔍 章节"${parentChapter.name}"的子章节:`, childSections)
|
||||||
|
|
||||||
|
// 创建章节分组
|
||||||
|
groups.push({
|
||||||
|
title: parentChapter.name, // 使用后端返回的章节名称
|
||||||
|
sections: childSections.length > 0 ? childSections : [parentChapter], // 如果有子章节就用子章节,否则用父章节本身
|
||||||
|
expanded: index === 0 // 默认展开第一章
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 如果没有找到一级章节,可能所有章节都是同级的,直接作为一个组
|
||||||
|
if (groups.length === 0 && sections.length > 0) {
|
||||||
|
console.log('🔍 没有找到层级结构,将所有章节作为一组')
|
||||||
|
groups.push({
|
||||||
|
title: '课程章节',
|
||||||
|
sections: sections,
|
||||||
|
expanded: true
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('✅ 章节分组完成:', groups)
|
||||||
return groups
|
return groups
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -620,11 +661,11 @@ const loadCourseSections = async () => {
|
|||||||
console.log('章节API响应:', response)
|
console.log('章节API响应:', response)
|
||||||
|
|
||||||
if (response.code === 0 || response.code === 200) {
|
if (response.code === 0 || response.code === 200) {
|
||||||
if (response.data && Array.isArray(response.data)) {
|
if (response.data && response.data.list && Array.isArray(response.data.list)) {
|
||||||
courseSections.value = response.data
|
courseSections.value = response.data.list
|
||||||
groupedSections.value = groupSectionsByChapter(response.data)
|
groupedSections.value = groupSectionsByChapter(response.data.list)
|
||||||
console.log('章节数据设置成功:', courseSections.value)
|
console.log('✅ 章节数据设置成功:', courseSections.value)
|
||||||
console.log('分组数据:', groupedSections.value)
|
console.log('✅ 分组数据:', groupedSections.value)
|
||||||
// 默认播放右侧第一个视频章节(当未强制使用本地视频时)
|
// 默认播放右侧第一个视频章节(当未强制使用本地视频时)
|
||||||
if (!FORCE_LOCAL_VIDEO) {
|
if (!FORCE_LOCAL_VIDEO) {
|
||||||
const firstVideo = courseSections.value.find(s => s.outline && (s.outline.includes('.m3u8') || s.outline.includes('.mp4')))
|
const firstVideo = courseSections.value.find(s => s.outline && (s.outline.includes('.m3u8') || s.outline.includes('.mp4')))
|
||||||
@ -686,6 +727,103 @@ const toggleChapter = (chapterIndex: number) => {
|
|||||||
groupedSections.value[chapterIndex].expanded = !groupedSections.value[chapterIndex].expanded
|
groupedSections.value[chapterIndex].expanded = !groupedSections.value[chapterIndex].expanded
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 加载章节视频
|
||||||
|
const loadSectionVideo = async (section: CourseSection) => {
|
||||||
|
try {
|
||||||
|
videoLoading.value = true
|
||||||
|
console.log('🔍 加载章节视频,章节ID:', section.id)
|
||||||
|
|
||||||
|
const response = await CourseApi.getSectionVideos(courseId.value, section.id)
|
||||||
|
console.log('🔍 视频API响应:', response)
|
||||||
|
|
||||||
|
if (response.code === 0 || response.code === 200) {
|
||||||
|
if (response.data && response.data.length > 0) {
|
||||||
|
const video = response.data[0] // 取第一个视频
|
||||||
|
currentVideo.value = video
|
||||||
|
videoQualities.value = video.qualities
|
||||||
|
currentQuality.value = video.defaultQuality
|
||||||
|
|
||||||
|
// 获取默认清晰度的URL
|
||||||
|
const defaultQualityVideo = video.qualities.find(q => q.value === video.defaultQuality)
|
||||||
|
if (defaultQualityVideo) {
|
||||||
|
currentVideoUrl.value = defaultQualityVideo.url
|
||||||
|
console.log('✅ 设置视频URL:', currentVideoUrl.value)
|
||||||
|
|
||||||
|
// 更新播放器
|
||||||
|
await updateVideoPlayer()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('⚠️ 没有找到视频数据')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('❌ 获取视频失败:', response.message)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 加载章节视频失败:', error)
|
||||||
|
} finally {
|
||||||
|
videoLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换视频清晰度
|
||||||
|
const changeVideoQuality = async (quality: string) => {
|
||||||
|
if (!currentVideo.value) return
|
||||||
|
|
||||||
|
const qualityVideo = currentVideo.value.qualities.find(q => q.value === quality)
|
||||||
|
if (qualityVideo) {
|
||||||
|
currentQuality.value = quality
|
||||||
|
currentVideoUrl.value = qualityVideo.url
|
||||||
|
console.log('🔍 切换清晰度到:', quality, 'URL:', qualityVideo.url)
|
||||||
|
|
||||||
|
// 更新播放器
|
||||||
|
await updateVideoPlayer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新视频播放器
|
||||||
|
const updateVideoPlayer = async () => {
|
||||||
|
if (!currentVideoUrl.value) {
|
||||||
|
console.warn('⚠️ 视频URL为空,无法更新播放器')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('🔍 更新播放器视频源:', currentVideoUrl.value)
|
||||||
|
|
||||||
|
if (ckplayer.value) {
|
||||||
|
// 尝试不同的CKPlayer API方法
|
||||||
|
if (typeof ckplayer.value.newVideo === 'function') {
|
||||||
|
console.log('✅ 使用newVideo方法更新视频源')
|
||||||
|
ckplayer.value.newVideo(currentVideoUrl.value)
|
||||||
|
} else if (typeof ckplayer.value.changeVideo === 'function') {
|
||||||
|
console.log('✅ 使用changeVideo方法更新视频源')
|
||||||
|
ckplayer.value.changeVideo(currentVideoUrl.value)
|
||||||
|
} else if (typeof ckplayer.value.videoSrc === 'function') {
|
||||||
|
console.log('✅ 使用videoSrc方法更新视频源')
|
||||||
|
ckplayer.value.videoSrc(currentVideoUrl.value)
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ 未找到合适的更新方法,重新初始化播放器')
|
||||||
|
// 如果没有找到合适的方法,重新初始化播放器
|
||||||
|
await nextTick()
|
||||||
|
initCKPlayer(currentVideoUrl.value)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('🔍 播放器未初始化,开始初始化')
|
||||||
|
await nextTick()
|
||||||
|
initCKPlayer(currentVideoUrl.value)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 更新播放器失败:', error)
|
||||||
|
// 如果更新失败,尝试重新初始化
|
||||||
|
try {
|
||||||
|
await nextTick()
|
||||||
|
initCKPlayer(currentVideoUrl.value)
|
||||||
|
} catch (initError) {
|
||||||
|
console.error('❌ 重新初始化播放器也失败:', initError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 获取章节编号
|
// 获取章节编号
|
||||||
const getChapterNumber = (num: number) => {
|
const getChapterNumber = (num: number) => {
|
||||||
const numbers = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十']
|
const numbers = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十']
|
||||||
@ -694,19 +832,39 @@ const getChapterNumber = (num: number) => {
|
|||||||
|
|
||||||
// 课程类型判断函数
|
// 课程类型判断函数
|
||||||
const isVideoLesson = (section: CourseSection) => {
|
const isVideoLesson = (section: CourseSection) => {
|
||||||
console.log(section.outline)
|
console.log('检查章节类型:', section.name, 'type:', section.type, 'outline:', section.outline)
|
||||||
|
// 优先根据type字段判断:0=视频
|
||||||
|
if (section.type === 0) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// 如果type为null,则根据outline判断
|
||||||
return section.outline && (section.outline.includes('.m3u8') || section.outline.includes('.mp4'))
|
return section.outline && (section.outline.includes('.m3u8') || section.outline.includes('.mp4'))
|
||||||
}
|
}
|
||||||
|
|
||||||
const isResourceLesson = (section: CourseSection) => {
|
const isResourceLesson = (section: CourseSection) => {
|
||||||
|
// 优先根据type字段判断:1=资料
|
||||||
|
if (section.type === 1) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// 如果type为null,则根据outline或名称判断
|
||||||
return section.outline && (section.outline.includes('.pdf') || section.outline.includes('.ppt') || section.outline.includes('.zip'))
|
return section.outline && (section.outline.includes('.pdf') || section.outline.includes('.ppt') || section.outline.includes('.zip'))
|
||||||
}
|
}
|
||||||
|
|
||||||
const isHomeworkLesson = (section: CourseSection) => {
|
const isHomeworkLesson = (section: CourseSection) => {
|
||||||
|
// 优先根据type字段判断:3=作业
|
||||||
|
if (section.type === 3) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// 如果type为null,则根据名称判断
|
||||||
return section.name.includes('作业') || section.name.includes('练习')
|
return section.name.includes('作业') || section.name.includes('练习')
|
||||||
}
|
}
|
||||||
|
|
||||||
const isExamLesson = (section: CourseSection) => {
|
const isExamLesson = (section: CourseSection) => {
|
||||||
|
// 优先根据type字段判断:2=考试
|
||||||
|
if (section.type === 2) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// 如果type为null,则根据名称判断
|
||||||
return section.name.includes('考试') || section.name.includes('测试')
|
return section.name.includes('考试') || section.name.includes('测试')
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -736,18 +894,39 @@ const formatLessonDuration = (section: CourseSection) => {
|
|||||||
|
|
||||||
// 处理章节点击 - 已报名状态,可以正常点击
|
// 处理章节点击 - 已报名状态,可以正常点击
|
||||||
const handleSectionClick = (section: CourseSection) => {
|
const handleSectionClick = (section: CourseSection) => {
|
||||||
console.log('点击课程章节:', section.name)
|
console.log('🔍 点击课程章节:', section.name, section)
|
||||||
currentSection.value = section
|
currentSection.value = section
|
||||||
|
|
||||||
// 如果是视频课程,直接播放
|
// 检查章节类型
|
||||||
if (isVideoLesson(section)) {
|
const isVideo = isVideoLesson(section)
|
||||||
handleVideoPlay(section)
|
const isResource = isResourceLesson(section)
|
||||||
} else if (isResourceLesson(section)) {
|
const isHomework = isHomeworkLesson(section)
|
||||||
|
const isExam = isExamLesson(section)
|
||||||
|
|
||||||
|
console.log('🔍 章节类型判断结果:', {
|
||||||
|
isVideo,
|
||||||
|
isResource,
|
||||||
|
isHomework,
|
||||||
|
isExam,
|
||||||
|
type: section.type
|
||||||
|
})
|
||||||
|
|
||||||
|
// 如果是视频课程,加载视频数据
|
||||||
|
if (isVideo) {
|
||||||
|
console.log('✅ 识别为视频课程,开始加载视频数据')
|
||||||
|
loadSectionVideo(section)
|
||||||
|
} else if (isResource) {
|
||||||
|
console.log('✅ 识别为资料课程')
|
||||||
handleDownload(section)
|
handleDownload(section)
|
||||||
} else if (isHomeworkLesson(section)) {
|
} else if (isHomework) {
|
||||||
|
console.log('✅ 识别为作业课程')
|
||||||
handleHomework(section)
|
handleHomework(section)
|
||||||
} else if (isExamLesson(section)) {
|
} else if (isExam) {
|
||||||
|
console.log('✅ 识别为考试课程')
|
||||||
handleExam(section)
|
handleExam(section)
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ 未识别的课程类型,默认当作视频处理')
|
||||||
|
loadSectionVideo(section)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1111,6 +1290,73 @@ onUnmounted(() => {
|
|||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 清晰度选择器 */
|
||||||
|
.video-quality-selector {
|
||||||
|
position: absolute;
|
||||||
|
top: 15px;
|
||||||
|
right: 15px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quality-dropdown {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quality-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quality-btn:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-icon {
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quality-btn:hover .dropdown-icon {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quality-menu {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
right: 0;
|
||||||
|
margin-top: 4px;
|
||||||
|
background: rgba(0, 0, 0, 0.9);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
min-width: 80px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quality-option {
|
||||||
|
padding: 8px 12px;
|
||||||
|
color: white;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quality-option:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quality-option.active {
|
||||||
|
background: #1890ff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
/* 课程信息区域 */
|
/* 课程信息区域 */
|
||||||
.course-info-section {
|
.course-info-section {
|
||||||
/* padding: 24px 0; */
|
/* padding: 24px 0; */
|
||||||
|
Loading…
x
Reference in New Issue
Block a user