OL-LearnPlatform-Frontend/src/views/CourseDetailEnrolled.vue

2897 lines
68 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="course-detail-page">
<!-- 面包屑导航 -->
<div class="breadcrumb">
<div class="container">
<span class="breadcrumb-text">首页 > 课程库 > {{ course?.title || '课程详情' }}</span>
</div>
</div>
<!-- 主要内容区域 -->
<div class="main-content">
<div class="container">
<div class="content-layout">
<!-- 加载状态 -->
<div v-if="loading" class="loading-container">
<div class="loading-content">
<p>正在加载课程详情...</p>
</div>
</div>
<!-- 错误状态 -->
<div v-else-if="error" class="error-container">
<div class="error-content">
<p>{{ error }}</p>
<button @click="loadCourseDetail" class="retry-btn">重试</button>
</div>
</div>
<!-- 课程内容 -->
<div v-else-if="course" class="course-content">
<!-- 左侧主要内容 -->
<div class="main-column">
<!-- 视频播放器区域 - 已报名状态 -->
<div class="video-player-section">
<div class="video-player enrolled">
<div class="video-container">
<!-- CKPlayer 容器 -->
<div v-if="currentVideoUrl" id="ckplayer_container" class="ckplayer-container">
</div>
<div v-else class="video-placeholder"
:style="{ backgroundImage: course?.coverImage || course?.thumbnail ? `url(${course.coverImage || course.thumbnail})` : '' }">
<div class="placeholder-content">
<div class="play-icon">
<svg width="60" height="60" viewBox="0 0 60 60">
<circle cx="30" cy="30" r="30" fill="rgba(255,255,255,0.9)" />
<path d="M23 18l20 12-20 12V18z" fill="#1890ff" />
</svg>
</div>
<p>请选择要播放的视频课程</p>
</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 class="video-interaction-bar">
<div class="interaction-left">
<button class="interaction-btn">
<span class="icon-like"></span>
<span>541</span>
</button>
<!-- 分割线 -->
<div class="split-line"></div>
<button class="interaction-btn">
<span class="icon-share"></span>
<span class="share-text">2377</span>
</button>
<button class="interaction-btn">
<span class="icon-notes"></span>
</button>
<button class="interaction-btn">
<span class="icon-download"></span>
</button>
</div>
<div class="interaction-right">
<div class="comment-input">
<input type="text" placeholder="成功报名学习才能发送弹幕哦~" />
<button class="send-btn">发送</button>
</div>
</div>
</div>
</div>
</div>
<!-- 课程信息区域 -->
<div class="course-info-section">
<!-- 课程标题 -->
<div class="course-header">
<h1 class="course-title">{{ course.title }}</h1>
<!-- 课程元信息 -->
<div class="course-meta">
<div class="meta-row">
<span class="meta-item">
分类<span class="category-link">{{ course.category?.name || '信息技术' }}</span>
</span>
<div class="meta-right">
<button class="btn-notes" @click="openNotesModal">
<i class="icon-note"></i>
记笔记
</button>
</div>
</div>
<div class="meta-row">
<span class="meta-item">
<i class="icon-time"></i>
{{ totalLessons }}{{ totalSections }}
</span>
<span class="meta-separator"></span>
<span class="meta-item">
<i class="icon-duration"></i>
{{ formatTotalDuration() }}
</span>
</div>
</div>
</div>
<!-- 课程描述 -->
<div class="course-description">
<p>{{ course.description ||
'本课程深度聚焦问题让每一位教师了解并学习使用DeepSeek结合办公自动化职业岗位标准以实际工作任务为引导强调课程内容的易用性和岗位要求的匹配性。课程内容与全国计算机等级考试、"1+X"WPS办公应用职业技能等级证书技能大赛紧密结合课程设置紧密对应实际全面共享可为职业工作人员、在校学生、创行教师提供服务与学习支持。'
}}</p>
</div>
<!-- 讲师信息 -->
<div class="instructors-section">
<h3 class="section-title">讲师</h3>
<div class="instructors-list">
<div class="instructor-item" v-for="instructor in instructors" :key="instructor.id">
<div class="instructor-avatar">
<SafeAvatar :src="instructor.avatar" :name="instructor.name" :size="50" />
</div>
<div class="instructor-info">
<div class="instructor-name">{{ instructor.name }}</div>
<div class="instructor-title">{{ instructor.title }}</div>
</div>
</div>
</div>
</div>
<!-- 分隔线 -->
<div class="course-info-divider"></div>
<!-- 课程标签页 -->
<div class="course-tabs">
<div class="tab-nav">
<button class="tab-btn" :class="{ active: activeTab === 'intro' }"
@click="activeTab = 'intro'">课程介绍</button>
<button class="tab-btn" :class="{ active: activeTab === 'comments' }"
@click="activeTab = 'comments'">评论(1251)</button>
</div>
<!-- 标签页内容区域 -->
<div class="tab-content">
<!-- 课程介绍内容 -->
<div v-if="activeTab === 'intro'" class="tab-pane">
<div class="intro-content">
<img src="/images/courses/课程介绍区.png" alt="课程介绍" class="course-intro-image" />
</div>
</div>
<!-- 评论内容 -->
<div v-if="activeTab === 'comments'" class="tab-pane">
<div class="comments-content">
<!-- <div class="comment-stats">
<span class="total-comments">共1251条评论</span>
<div class="comment-filters">
<button class="filter-btn active">全部</button>
<button class="filter-btn">最新</button>
<button class="filter-btn">最热</button>
</div>
</div> -->
<div class="comment-list">
<div class="comment-item" v-for="comment in displayComments" :key="comment.id">
<div class="comment-avatar">
<img :src="comment.avatar" :alt="comment.username" />
</div>
<div class="comment-content">
<div class="comment-header">
<span class="comment-username">{{ comment.username }}</span>
<span class="comment-time">{{ comment.time }}</span>
</div>
<div class="comment-text">{{ comment.content }}</div>
<div class="comment-actions">
<button class="action-btn">
<span class="top">置顶评论</span>
<span>2025.07.23 16:28</span>
</button>
<button class="action-btn">回复</button>
</div>
</div>
</div>
</div>
<!-- <div class="load-more">
<button class="btn-load-more">加载更多评论</button>
</div> -->
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 右侧边栏 -->
<div class="sidebar">
<!-- 学习进度 -->
<div class="progress-section">
<LearningProgressStats :course-progress="videoProgress" :homework-progress="exerciseProgress"
:exam-progress="examProgress" :completed-items="completedLessons" :total-items="totalSections" />
</div>
<!-- 课程章节列表 -->
<div class="course-sections">
<div class="sections-header">
<div class="header-left">
<h3 class="sections-title">课程章节</h3>
</div>
<div class="header-right">
<button class="sort-btn">
<svg width="16" height="16" viewBox="0 0 16 16" class="sort-icon">
<path d="M3 3h10M3 8h7M3 13h4" stroke="currentColor" stroke-width="1.5" fill="none" />
</svg>
<span class="sort-text">正序</span>
</button>
</div>
</div>
<div class="sections-content">
<div v-if="sectionsLoading" class="sections-loading">
<p>正在加载章节列表...</p>
</div>
<div v-else-if="sectionsError" class="sections-error">
<p>{{ sectionsError }}</p>
<button @click="loadCourseSections" class="retry-btn">重试</button>
</div>
<div v-else-if="courseSections.length > 0" class="sections-list">
<!-- 按章节分组显示 - 已报名状态,彩色可点击 -->
<div v-for="(chapter, chapterIndex) in groupedSections" :key="chapterIndex" class="chapter-section">
<div class="chapter-header" @click="toggleChapter(chapterIndex)">
<div class="chapter-info">
<span class="chapter-title">第{{ getChapterNumber(chapterIndex + 1) }}章 {{ chapter.title
}}</span>
</div>
<span class="chapter-toggle" :class="{ 'expanded': chapter.expanded }">
<svg width="12" height="12" viewBox="0 0 12 12">
<path d="M4 3l4 3-4 3" stroke="currentColor" stroke-width="1.5" fill="none" />
</svg>
</span>
</div>
<div v-if="chapter.expanded" class="chapter-lessons">
<div v-for="section in chapter.sections" :key="section.id" class="lesson-item">
<!-- 已报名状态:彩色可点击 -->
<div class="lesson-content enrolled" @click="handleSectionClick(section)">
<div class="lesson-type-badge" :class="getLessonTypeBadgeClass(section)">
{{ getLessonTypeText(section) }}
</div>
<div class="lesson-info">
<span class="lesson-title">{{ section.name }}</span>
</div>
<div class="lesson-meta">
<span v-if="isVideoLesson(section)" class="lesson-duration">{{
formatLessonDuration(section) }}</span>
<div class="lesson-actions">
<!-- 视频播放图标 - 可点击 -->
<button v-if="isVideoLesson(section)" class="lesson-action-btn video-btn"
@click.stop="handleVideoPlay(section)">
<img src="/public/images/courses/video-enroll.png" alt="视频" width="14" height="14">
</button>
<!-- 下载图标 - 可点击 -->
<button v-else-if="isResourceLesson(section)" class="lesson-action-btn download-btn"
@click.stop="handleDownload(section)">
<img src="/public/images/courses/download-enroll.png" alt="资料" width="14" height="14">
</button>
<!-- 编辑图标(作业) - 可点击 -->
<button v-else-if="isHomeworkLesson(section)" class="lesson-action-btn edit-btn"
@click.stop="handleHomework(section)">
<img src="/public/images/courses/homework-enroll.png" alt="作业" width="14" height="14">
</button>
<!-- 考试图标 - 可点击 -->
<button v-else-if="isExamLesson(section)" class="lesson-action-btn exam-btn"
@click.stop="handleExam(section)">
<img src="/public/images/courses/examination-enroll.png" alt="考试" width="14"
height="14">
</button>
<!-- 完成状态图标 - 彩色显示 -->
<!-- <span v-if="section.completed" class="completion-icon">
<svg width="14" height="14" viewBox="0 0 16 16">
<circle cx="8" cy="8" r="7" fill="#52c41a"/>
<path d="M5 8l2 2 4-4" stroke="white" stroke-width="2" fill="none"/>
</svg>
</span> -->
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div v-else class="no-sections">
<p>暂无课程章节</p>
</div>
</div>
</div>
<!-- 更多课程 -->
<div class="more-courses">
<div class="more-courses-header">
<h3>更多课程</h3>
</div>
<div class="more-courses-list">
<div class="course-card">
<div class="course-cover">
<div class="course-image computer-bg">
<img src="/images/courses/course-activities1.png" alt="">
</div>
</div>
<div class="course-info">
<div class="course-desc">暑期名师领学,提高班级教学质量!高效冲分指南</div>
<div class="course-stats">
<span class="stats-item">
<i class="icon-chapters"></i>
共9章54节
</span>
<span class="stats-item">
<i class="icon-duration"></i>
12小时43分钟
</span>
</div>
<div class="course-footer">
<span class="enrolled-count">324人已报名</span>
<button class="btn-enroll-course">去报名</button>
</div>
</div>
</div>
<div class="course-card">
<div class="course-cover">
<div class="course-image computer-bg">
<img src="/images/courses/course-activities2.png" alt="">
</div>
</div>
<div class="course-info">
<div class="course-desc">暑期名师领学,提高班级教学质量!高效冲分指南</div>
<div class="course-stats">
<span class="stats-item">
<i class="icon-chapters"></i>
共9章54节
</span>
<span class="stats-item">
<i class="icon-duration"></i>
12小时43分钟
</span>
</div>
<div class="course-footer">
<span class="enrolled-count">324人已报名</span>
<button class="btn-enroll-course">去报名</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 笔记弹窗 -->
<NotesModal :visible="showNotesModal" @close="closeNotesModal" @save="saveNote" />
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed, nextTick } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user'
import { CourseApi } from '@/api/modules/course'
import type { Course, CourseSection, SectionVideo, VideoQuality } from '@/api/types'
import SafeAvatar from '@/components/common/SafeAvatar.vue'
import LearningProgressStats from '@/components/common/LearningProgressStats.vue'
import NotesModal from '@/components/common/NotesModal.vue'
// 声明全局CKPlayer类型
declare global {
interface Window {
ckplayer: any
loadedHandler: () => void
endedHandler: () => void
errorHandler: (error: any) => void
}
}
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
const courseId = ref(route.params.id as string)
// 强制仅播放本地视频(如需关闭,置为 false
const FORCE_LOCAL_VIDEO = true
// 当前选中的章节和视频
const currentSection = ref<CourseSection | null>(null)
const currentVideoUrl = ref<string>('')
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 = {
// 本地视频(当前使用)
LOCAL: '/video/first.mp4',
// HLS流服务器准备好后使用
HLS: 'http://110.42.96.65:55513/learn/index.m3u8'
}
// 获取视频URL的函数
const getVideoUrl = (section?: CourseSection) => {
const outline = section?.outline?.trim()
if (outline && (outline.endsWith('.mp4') || outline.endsWith('.m3u8'))) {
return outline
}
// 当前使用本地视频,将来可以通过环境变量或配置切换
return VIDEO_CONFIG.LOCAL
// 服务器准备好后,可以改为:
// return VIDEO_CONFIG.HLS
}
// 课程数据相关状态
const course = ref<Course | null>(null)
const loading = ref(false)
const error = ref('')
// 课程章节数据
const courseSections = ref<CourseSection[]>([])
const sectionsLoading = ref(false)
const sectionsError = ref('')
// 学习进度数据
const progress = ref(24.6)
const completedLessons = ref(13)
const videoProgress = ref(31.7)
const exerciseProgress = ref(22.5)
const examProgress = ref(9.6)
// 章节分组数据
interface ChapterGroup {
title: string
sections: CourseSection[]
expanded: boolean
}
const groupedSections = ref<ChapterGroup[]>([])
// 新增的响应式数据
const activeTab = ref('intro')
// 笔记弹窗相关
const showNotesModal = ref(false)
// 讲师数据
const instructors = ref([
{
id: 1,
name: '汪波',
title: '教授',
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-4.0.3&auto=format&fit=crop&w=60&q=80'
},
{
id: 2,
name: '汪波',
title: '教授',
avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?ixlib=rb-4.0.3&auto=format&fit=crop&w=60&q=80'
},
{
id: 3,
name: '汪波',
title: '教授',
avatar: 'https://images.unsplash.com/photo-1494790108755-2616b612b786?ixlib=rb-4.0.3&auto=format&fit=crop&w=60&q=80'
}
])
// 计算属性
const totalLessons = computed(() => {
return groupedSections.value.length
})
const totalSections = computed(() => {
return 54 // 固定显示54节课程
})
const formatTotalDuration = () => {
// 计算总时长
let totalMinutes = 0
courseSections.value.forEach(section => {
if (section.duration) {
const parts = section.duration.split(':')
if (parts.length === 3) {
const hours = parseInt(parts[0])
const minutes = parseInt(parts[1])
totalMinutes += hours * 60 + minutes
}
}
})
const hours = Math.floor(totalMinutes / 60)
const minutes = totalMinutes % 60
return `${hours}小时${minutes}分钟`
}
const displayComments = ref([
{
id: 1,
username: '学习者小王',
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-4.0.3&auto=format&fit=crop&w=50&q=80',
time: '2天前',
content: '老师讲得很详细,从零基础到实际应用都有涉及,非常适合初学者!',
likes: 23
},
{
id: 2,
username: 'AI爱好者',
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-4.0.3&auto=format&fit=crop&w=50&q=80',
time: '5天前',
content: '课程内容很实用跟着做了几个项目收获很大。推荐给想学AI的朋友们',
likes: 18
},
{
id: 3,
username: '程序员小李',
avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?ixlib=rb-4.0.3&auto=format&fit=crop&w=50&q=80',
time: '1周前',
content: 'DeepSeek确实是个很强大的工具通过这个课程学会了很多实用技巧。',
likes: 31
}
])
// 生成模拟章节数据(暂时禁用)
const generateMockSections = (): CourseSection[] => {
// 暂时返回空数组等待API修复
return []
}
// 将章节按章分组 - 根据后端数据结构重新实现
const groupSectionsByChapter = (sections: CourseSection[]) => {
console.log('🔍 开始分组章节数据:', sections)
const groups: ChapterGroup[] = []
// 找出所有一级章节level=1这些是父章节
const parentChapters = sections.filter(section => section.level === 1)
console.log('🔍 找到一级章节:', parentChapters)
// 为每个一级章节创建分组
parentChapters.forEach((parentChapter, index) => {
// 找出该章节下的所有子章节level=2parentId匹配
const childSections = sections.filter(section =>
section.level === 2 && section.parentId === parentChapter.id
)
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
}
// 加载课程详情
const loadCourseDetail = async () => {
console.log('开始加载课程详情课程ID:', courseId.value)
if (!courseId.value || courseId.value.trim() === '') {
error.value = '课程ID无效'
console.error('课程ID无效:', courseId.value)
return
}
try {
loading.value = true
error.value = ''
console.log('调用API获取课程详情...')
const response = await CourseApi.getCourseById(courseId.value)
console.log('API响应:', response)
if (response.code === 0 || response.code === 200) {
course.value = response.data
console.log('课程数据设置成功:', course.value)
} else {
error.value = response.message || '获取课程详情失败'
console.error('API返回错误:', response)
}
} catch (err) {
console.error('加载课程详情失败:', err)
error.value = '网络错误,请稍后重试'
} finally {
loading.value = false
}
}
// 加载课程章节列表
const loadCourseSections = async () => {
if (!courseId.value || courseId.value.trim() === '') {
sectionsError.value = '课程ID无效'
console.error('课程ID无效:', courseId.value)
return
}
try {
sectionsLoading.value = true
sectionsError.value = ''
console.log('调用API获取课程章节...')
const response = await CourseApi.getCourseSections(courseId.value)
console.log('章节API响应:', response)
if (response.code === 0 || response.code === 200) {
if (response.data && response.data.list && Array.isArray(response.data.list)) {
courseSections.value = response.data.list
groupedSections.value = groupSectionsByChapter(response.data.list)
console.log('✅ 章节数据设置成功:', courseSections.value)
console.log('✅ 分组数据:', groupedSections.value)
// 默认播放右侧第一个视频章节(当未强制使用本地视频时)
if (!FORCE_LOCAL_VIDEO) {
const firstVideo = courseSections.value.find(s => s.outline && (s.outline.includes('.m3u8') || s.outline.includes('.mp4')))
if (firstVideo) {
currentSection.value = firstVideo
currentVideoUrl.value = getVideoUrl(firstVideo)
await nextTick()
initCKPlayer(currentVideoUrl.value)
}
}
} else {
console.log('API返回的章节数据为空使用模拟数据')
loadMockData()
}
} else {
console.log('API返回错误使用模拟数据')
loadMockData()
}
} catch (err) {
console.error('加载课程章节失败:', err)
console.log('API调用失败使用模拟数据')
loadMockData()
} finally {
sectionsLoading.value = false
}
}
// 加载模拟数据
const loadMockData = () => {
console.log('加载模拟章节数据')
const mockSections = generateMockSections()
courseSections.value = mockSections
groupedSections.value = groupSectionsByChapter(mockSections)
// 计算学习进度
const completed = mockSections.filter(section => section.completed).length
completedLessons.value = completed
progress.value = Math.round((completed / mockSections.length) * 100)
console.log('模拟数据加载完成:', {
total: mockSections.length,
completed: completed,
progress: progress.value
})
// 默认播放右侧第一个视频章节(模拟数据,未强制使用本地视频时)
if (!FORCE_LOCAL_VIDEO) {
const firstVideo = mockSections.find(s => s.outline && (s.outline.includes('.m3u8') || s.outline.includes('.mp4')))
if (firstVideo) {
currentSection.value = firstVideo
currentVideoUrl.value = getVideoUrl(firstVideo)
setTimeout(() => initCKPlayer(currentVideoUrl.value), 0)
}
}
}
// 切换章节展开/收起
const toggleChapter = (chapterIndex: number) => {
console.log('切换章节展开/收起:', chapterIndex)
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 numbers = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十']
return numbers[num - 1] || num.toString()
}
// 课程类型判断函数
const isVideoLesson = (section: CourseSection) => {
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'))
}
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'))
}
const isHomeworkLesson = (section: CourseSection) => {
// 优先根据type字段判断3=作业
if (section.type === 3) {
return true
}
// 如果type为null则根据名称判断
return section.name.includes('作业') || section.name.includes('练习')
}
const isExamLesson = (section: CourseSection) => {
// 优先根据type字段判断2=考试
if (section.type === 2) {
return true
}
// 如果type为null则根据名称判断
return section.name.includes('考试') || section.name.includes('测试')
}
// 获取课程类型样式类
const getLessonTypeBadgeClass = (section: CourseSection) => {
if (isVideoLesson(section)) return 'badge-video'
if (isResourceLesson(section)) return 'badge-resource'
if (isHomeworkLesson(section)) return 'badge-homework'
if (isExamLesson(section)) return 'badge-exam'
return 'badge-default'
}
// 获取课程类型文本
const getLessonTypeText = (section: CourseSection) => {
if (isVideoLesson(section)) return '视频'
if (isResourceLesson(section)) return '资料'
if (isHomeworkLesson(section)) return '作业'
if (isExamLesson(section)) return '考试'
return '视频'
}
// 格式化课程时长
const formatLessonDuration = (section: CourseSection) => {
if (!section.duration) return ''
return section.duration
}
// 处理章节点击 - 已报名状态,可以正常点击
const handleSectionClick = (section: CourseSection) => {
console.log('🔍 点击课程章节:', section.name, section)
currentSection.value = section
// 检查章节类型
const isVideo = isVideoLesson(section)
const isResource = 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)
} else if (isHomework) {
console.log('✅ 识别为作业课程')
handleHomework(section)
} else if (isExam) {
console.log('✅ 识别为考试课程')
handleExam(section)
} else {
console.log('⚠️ 未识别的课程类型,默认当作视频处理')
loadSectionVideo(section)
}
}
// 处理视频播放 - 已报名状态,可以正常播放
const handleVideoPlay = async (section: CourseSection) => {
console.log('播放视频:', section.name)
// 获取视频URL
const videoUrl = getVideoUrl(section)
currentVideoUrl.value = videoUrl
currentSection.value = section
console.log('使用视频源:', videoUrl)
// 等待DOM更新
await nextTick()
// 初始化CKPlayer播放器
initCKPlayer(videoUrl)
// 标记为已完成
if (!section.completed) {
section.completed = true
// 重新计算进度
const completed = courseSections.value.filter(s => s.completed).length
completedLessons.value = completed
progress.value = Math.round((completed / courseSections.value.length) * 100)
}
}
// 初始化CKPlayer播放器
const initCKPlayer = (url: string) => {
// 清理之前的播放器实例
if (ckplayer.value) {
try {
ckplayer.value.remove()
} catch (e) {
console.log('清理播放器实例时出错:', e)
}
ckplayer.value = null
}
// 检查CKPlayer是否已加载
if (typeof window.ckplayer === 'undefined') {
console.error('CKPlayer not loaded')
return
}
// 若容器暂未挂载,延迟重试一次,避免"未找到放置视频的容器"
const containerEl = document.querySelector('#ckplayer_container') as HTMLElement | null
if (!containerEl) {
console.warn('Player container not found, retrying init...')
setTimeout(() => initCKPlayer(url), 50)
return
}
// 判断视频类型
const isMP4 = url.endsWith('.mp4')
const isHLS = url.endsWith('.m3u8')
// CKPlayer配置
const videoObject = {
container: '#ckplayer_container', // 容器ID
autoplay: false, // 自动播放
video: url, // 视频地址
volume: 0.8, // 音量
poster: course.value?.coverImage || course.value?.thumbnail || '', // 封面图
live: false, // 是否直播
// 根据视频格式选择插件
plug: isHLS ? 'hls.js' : '', // HLS使用hls.js插件MP4不需要插件
playbackrateOpen: true, // 开启倍速选项
playbackrateList: [0.5, 0.75, 1, 1.25, 1.5, 2], // 播放速度选项
seek: 0, // 默认跳转秒数
loaded: 'loadedHandler', // 播放器加载完成回调
ended: 'endedHandler', // 播放结束回调
error: 'errorHandler', // 播放错误回调
title: currentSection.value?.name || '课程视频', // 视频标题
controls: true, // 显示控制栏
webFull: true, // 启用页面全屏
screenshot: true, // 启用截图功能
timeScheduleAdjust: 1, // 允许调节播放进度
// MP4视频的额外配置
...(isMP4 && {
type: 'mp4', // 明确指定视频类型
crossOrigin: 'anonymous' // 跨域设置
})
}
try {
// 创建播放器实例
ckplayer.value = new window.ckplayer(videoObject)
console.log('CKPlayer initialized successfully for:', isMP4 ? 'MP4' : 'HLS')
} catch (error) {
console.error('Failed to initialize CKPlayer:', error)
}
}
// CKPlayer回调函数
window.loadedHandler = () => {
console.log('CKPlayer loaded successfully')
}
window.endedHandler = () => {
console.log('Video playback ended')
}
window.errorHandler = (error: any) => {
console.error('CKPlayer error:', error)
// 自动回退到本地视频,避免一直缓冲
if (currentVideoUrl.value !== VIDEO_CONFIG.LOCAL) {
try {
currentVideoUrl.value = VIDEO_CONFIG.LOCAL
// 重新初始化播放器播放本地视频
initCKPlayer(VIDEO_CONFIG.LOCAL)
} catch (e) {
console.error('Fallback to local video failed:', e)
}
}
}
// 处理资源下载
const handleDownload = (section: CourseSection) => {
console.log('下载资源:', section.name, section.outline)
if (section.outline) {
// 模拟下载
alert(`开始下载:${section.name}`)
// 标记为已完成
if (!section.completed) {
section.completed = true
const completed = courseSections.value.filter(s => s.completed).length
completedLessons.value = completed
progress.value = Math.round((completed / courseSections.value.length) * 100)
}
}
}
// 处理作业
const handleHomework = (section: CourseSection) => {
console.log('打开作业:', section.name)
alert(`打开作业:${section.name}`)
}
// 处理考试
const handleExam = (section: CourseSection) => {
console.log('开始考试:', section.name)
// 跳转到考前须知页面
router.push({
name: 'ExamNotice',
params: {
courseId: courseId.value.toString(),
sectionId: section.id.toString()
},
query: {
courseName: course.value?.title || '课程名称',
examName: section.name
}
})
}
// 视频事件处理
// const handleVideoLoadStart = () => {
// console.log('视频开始加载')
// }
// const handleVideoCanPlay = () => {
// console.log('视频可以播放')
// }
// const handleVideoError = (event: Event) => {
// console.error('视频播放错误:', event)
// }
// 初始化模拟状态(已报名状态)
const initializeEnrolledState = () => {
// 模拟用户已登录
if (!userStore.isLoggedIn) {
userStore.user = {
id: 1,
username: 'testuser',
email: 'test@example.com',
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-4.0.3&auto=format&fit=crop&w=50&q=80',
role: 'student',
status: 'active',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
}
userStore.token = 'mock-token'
}
console.log('已报名状态页面初始化完成')
}
// 笔记弹窗相关方法
const openNotesModal = () => {
showNotesModal.value = true
}
const closeNotesModal = () => {
showNotesModal.value = false
}
const saveNote = (content: string) => {
console.log('保存笔记:', content)
// 这里可以添加保存笔记到服务器的逻辑
}
onMounted(async () => {
console.log('已报名课程详情页加载完成课程ID:', courseId.value)
initializeEnrolledState() // 初始化已报名状态
// 若强制播放本地视频,优先初始化本地源
if (FORCE_LOCAL_VIDEO) {
currentSection.value = null
currentVideoUrl.value = VIDEO_CONFIG.LOCAL
await nextTick()
initCKPlayer(currentVideoUrl.value)
}
loadCourseDetail()
loadCourseSections()
})
// 组件卸载时清理CKPlayer实例
onUnmounted(() => {
if (ckplayer.value) {
try {
ckplayer.value.remove()
} catch (e) {
console.log('清理播放器实例时出错:', e)
}
ckplayer.value = null
}
})
</script>
<style scoped>
/* 基础样式 */
.course-detail-page {
min-height: 100vh;
background: #f5f5f5;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 0 32px;
}
/* 面包屑导航 */
.breadcrumb {
padding: 12px 0;
}
.breadcrumb-text {
font-size: 14px;
color: #666;
}
/* 主要内容区域 */
.main-content {
padding: 0 0 20px 0;
}
.content-layout {
display: flex;
gap: 40px;
align-items: flex-start;
}
.course-content {
display: flex;
gap: 30px;
align-items: flex-start;
}
.main-column {
flex: 1;
min-width: 0;
}
/* 右侧边栏 */
.sidebar {
width: 370px;
flex-shrink: 0;
}
/* 视频播放器区域 */
.video-player-section {
margin-bottom: 30px;
}
.video-player {
background: #000;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.video-container {
position: relative;
width: 100%;
height: 578px;
}
.video-element {
width: 100%;
height: 100%;
object-fit: cover;
}
.ckplayer-container {
width: 100%;
height: 100%;
background: #000;
}
.video-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
/* 背景图片设置 */
background-size: cover;
background-position: center;
background-repeat: no-repeat;
/* 如果没有背景图片,使用默认渐变背景 */
background-image: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
position: relative;
}
.video-placeholder::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.4);
z-index: 1;
}
.placeholder-content {
text-align: center;
position: relative;
z-index: 2;
}
.play-icon {
margin-bottom: 16px;
cursor: pointer;
transition: transform 0.3s;
}
.play-icon:hover {
transform: scale(1.1);
}
.placeholder-content p {
margin: 0;
font-size: 16px;
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 {
/* padding: 24px 0; */
}
.course-header {
padding-top: 18px;
background: white;
}
.course-title {
font-size: 28px;
font-weight: 700;
color: #333;
margin-bottom: 16px;
line-height: 1.3;
}
.course-meta {
display: flex;
flex-direction: column;
}
.meta-row {
display: flex;
align-items: center;
margin-bottom: 8px;
width: 100%;
}
.meta-row:first-child {
justify-content: space-between;
}
.meta-right {
display: flex;
justify-content: flex-end;
width: auto;
}
.meta-item {
font-size: 14px;
color: #999999;
display: flex;
align-items: center;
gap: 3px;
}
.meta-separator {
color: #d9d9d9;
width: 20px;
}
.category-link {
color: #0088D1;
text-decoration: none;
cursor: pointer;
}
.category-link:hover {
text-decoration: underline;
}
.icon-time {
width: 16px;
height: 16px;
background-image: url('/images/courses/课程总章数.png');
background-size: contain;
background-repeat: no-repeat;
background-position: center;
display: inline-block;
vertical-align: middle;
margin-right: 4px;
flex-shrink: 0;
}
.icon-time,
.icon-duration,
.icon-note {
width: 16px;
height: 16px;
display: inline-block;
}
/* 这些图标样式已被替换为背景图片 */
.btn-notes {
background: #fff;
border: none;
padding: 6px 12px;
border-radius: 4px;
font-size: 16px;
color: #000;
cursor: pointer;
transition: all 0.3s;
display: flex;
align-items: center;
gap: 2px;
}
.btn-notes:hover {
background: #e9ecef;
border-color: #dee2e6;
}
/* 课程描述 */
.course-description {
line-height: 1.8;
color: #999;
font-size: 14px;
background-color: #fff;
}
.course-content-detail {
margin-top: 16px;
padding: 16px;
background: #f8f9fa;
border-radius: 6px;
}
.course-content-detail h4 {
margin-bottom: 12px;
color: #333;
}
/* 讲师信息 */
.instructors-section {
padding-bottom: 24px;
border-bottom: 1px solid #f0f0f0;
background-color: #fff;
}
.section-title {
font-weight: 600;
font-size: 16px;
font-style: normal;
color: #000;
padding-top: 12px;
margin-bottom: 12px;
line-height: 22px;
}
.instructors-list {
display: flex;
gap: 30px;
flex-wrap: wrap;
}
.instructor-item {
display: flex;
flex-direction: row;
align-items: center;
gap: 12px;
}
.instructor-avatar {
width: 50px;
height: 50px;
border-radius: 50%;
overflow: hidden;
border: 2px solid #f0f0f0;
}
.instructor-info {
display: flex;
flex-direction: column;
justify-content: center;
gap: 2px;
}
.instructor-info {
text-align: center;
}
.instructor-name {
font-size: 14px;
font-weight: 500;
color: #000;
margin-bottom: 1px;
}
.instructor-title {
font-size: 11px;
color: #999;
}
/* 分隔线样式 */
.course-info-divider {
height: 1px;
margin: 10px 0;
}
/* 课程标签页 */
.course-tabs {
padding: 14px 24px;
background-color: #ffffff;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
margin-bottom: 40px;
}
.tab-nav {
display: flex;
border-bottom: 2px solid #E6E6E6;
margin-bottom: 24px;
}
.tab-btn {
background: none;
border: none;
padding: 12px 0 12px 0;
margin-right: 54px;
font-size: 14px;
color: #333;
cursor: pointer;
position: relative;
transition: color 0.3s;
}
.tab-btn.active {
color: #008BD7;
font-weight: 600;
}
.tab-btn.active::after {
content: '';
position: absolute;
bottom: -1px;
left: 0;
right: 0;
height: 2px;
background: #008BD7;
}
.tab-btn:hover {
color: #008BD7;
}
.tab-content {
min-height: 300px;
}
.intro-content {
text-align: center;
}
.course-intro-image {
width: 100%;
max-width: 100%;
height: auto;
/* border-radius: 8px; */
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.intro-content h4 {
font-size: 16px;
font-weight: 600;
color: #333;
margin: 20px 0 12px 0;
}
.intro-content p {
line-height: 1.6;
color: #666;
margin-bottom: 16px;
}
.intro-content ul {
padding-left: 20px;
margin-bottom: 16px;
}
.intro-content li {
line-height: 1.6;
color: #666;
margin-bottom: 8px;
}
.instructors-section,
.course-description,
.course-header {
padding-left: 24px;
padding-right: 24px;
}
/* 学习进度 */
.progress-section {
background: white;
border-radius: 3px;
padding: 16px;
margin-bottom: 20px;
}
/* 学习进度组件样式由 LearningProgressStats 组件内部处理 */
/* 右侧边栏课程章节 */
.sidebar .course-sections {
/* background: white; */
border-radius: 5px;
padding: 0;
overflow: hidden;
}
.sections-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 0 10px 0;
background: #F5F7FA;
border-bottom: 1px solid #f0f0f0;
}
.header-left {
display: flex;
align-items: center;
}
.sections-title {
font-size: 18px;
font-weight: 600;
color: #333;
margin: 0;
}
.header-right {
display: flex;
align-items: center;
}
.sections-actions {
display: flex;
align-items: center;
}
.sort-btn {
background: none;
border: none;
color: #999;
padding: 6px 0;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
transition: all 0.3s;
display: flex;
align-items: center;
gap: 6px;
}
.sort-btn:hover {
background: #f5f5f5;
color: #666;
}
.sort-icon {
width: 16px;
height: 16px;
}
.sort-text {
font-size: 14px;
}
.refresh-btn,
.test-btn,
.mock-btn {
background: #1890ff;
color: white;
border: none;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: background-color 0.3s;
}
.refresh-btn:hover,
.test-btn:hover,
.mock-btn:hover {
background: #40a9ff;
}
.mock-btn {
background: #52c41a;
}
.mock-btn:hover {
background: #73d13d;
}
.sections-loading,
.sections-error,
.no-sections {
text-align: center;
padding: 16px;
color: #666;
font-size: 14px;
}
.sections-error .retry-btn {
margin-top: 8px;
background: #1890ff;
color: white;
border: none;
padding: 6px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.3s;
}
.sections-error .retry-btn:hover {
background: #40a9ff;
}
/* 章节列表样式 */
.sections-content {
background: white;
margin-bottom: 30px;
}
.sections-list {
max-height: 600px;
overflow-y: auto;
padding: 12px 20px 20px 20px;
}
.sections-list::-webkit-scrollbar {
width: 4px;
}
.sections-list::-webkit-scrollbar-track {
background: transparent;
}
.sections-list::-webkit-scrollbar-thumb {
background: #d9d9d9;
border-radius: 2px;
}
.sections-list::-webkit-scrollbar-thumb:hover {
background: #bfbfbf;
}
.chapter-section {
/* border-bottom: 1px solid #f0f0f0; */
}
.chapter-section:last-child {
border-bottom: none;
}
.chapter-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 16px;
background: #F5F8FB;
cursor: pointer;
transition: background-color 0.2s;
margin-top: 8px;
margin-bottom: 8px;
border-radius: 5px;
}
.chapter-header:hover {
background: #fafafa;
}
.chapter-info {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
}
.chapter-number {
font-size: 14px;
color: #333;
min-width: 20px;
}
.chapter-title {
font-size: 14px;
font-weight: 500;
color: #333;
flex: 1;
}
.chapter-toggle {
color: #999;
transition: transform 0.2s ease;
display: flex;
align-items: center;
/* padding: 4px; */
}
.chapter-toggle.expanded {
transform: rotate(90deg);
}
.chapter-lessons {
background: white;
}
.lesson-item {
/* border-bottom: 1px solid #f0f0f0; */
transition: background-color 0.2s;
}
.lesson-item:last-child {
border-bottom: none;
}
.lesson-item:hover {
background: #f9f9f9;
}
.lesson-content {
display: flex;
align-items: center;
padding: 3px 0 3px 0;
cursor: pointer;
gap: 12px;
}
.lesson-type-badge {
font-size: 12px;
padding: 3px 0;
border-radius: 2px;
font-weight: 500;
min-width: 32px;
text-align: center;
line-height: 1;
flex-shrink: 0;
}
.lesson-info {
flex: 1;
min-width: 0;
}
.lesson-title {
font-size: 14px;
color: #333;
transition: color 0.2s;
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.lesson-content:hover .lesson-title {
color: #1890ff;
}
/* 未报名状态的灰色样式 */
.lesson-content.unregistered {
cursor: not-allowed;
}
.lesson-title.disabled {
color: #666;
}
.lesson-duration.disabled {
color: #E1E1E1;
font-size: 12px;
}
.lesson-type-badge.disabled {
background: #fff !important;
color: #C0C0C0 !important;
border: 1px solid #E1E1E1;
}
.lesson-action-btn.disabled {
cursor: not-allowed;
opacity: 0.5;
}
.lesson-action-btn.disabled:hover {
background: none;
}
.lesson-action-btn.disabled svg {
color: #d9d9d9 !important;
}
.completion-icon.disabled {
opacity: 0.5;
}
.lesson-meta {
display: flex;
justify-content: flex-end;
align-items: center;
flex-shrink: 0;
}
.lesson-duration {
font-size: 12px;
color: #666;
min-width: 60px;
text-align: right;
}
.lesson-actions {
display: flex;
align-items: center;
gap: 8px;
}
/* 课程类型徽章彩色样式 */
.badge-video {
background: #fff !important;
color: #C0C0C0 !important;
border: 1px solid #E1E1E1;
}
.badge-resource {
background: #fff !important;
color: #C0C0C0 !important;
border: 1px solid #E1E1E1;
}
.badge-homework {
background: #fff !important;
color: #C0C0C0 !important;
border: 1px solid #E1E1E1;
}
.badge-exam {
background: #fff !important;
color: #C0C0C0 !important;
border: 1px solid #E1E1E1;
}
.badge-default {
background: #fff;
color: #C0C0C0;
border: 1px solid #E1E1E1;
}
.lesson-info {
flex: 1;
min-width: 0;
}
.lesson-title {
font-size: 14px;
color: #666;
transition: color 0.2s;
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.lesson-content:hover .lesson-title {
color: #1890ff;
}
.lesson-meta {
display: flex;
align-items: center;
justify-content: flex-end;
}
.lesson-duration {
font-size: 12px;
color: #666;
flex-shrink: 0;
}
.lesson-actions {
display: flex;
align-items: center;
gap: 8px;
}
.lesson-action-btn {
background: none;
border: none;
padding: 4px;
border-radius: 3px;
cursor: pointer;
transition: all 0.3s;
display: flex;
align-items: center;
justify-content: center;
}
.lesson-action-btn:hover {
background: #f0f0f0;
}
/* 操作按钮彩色样式 */
.video-btn {
color: #1890ff;
}
.download-btn {
color: #52c41a;
}
.edit-btn {
color: #fa8c16;
}
.exam-btn {
color: #f5222d;
}
.completion-icon {
display: flex;
align-items: center;
justify-content: center;
}
/* 课程标签页 */
.course-tabs {
/* margin-top: 32px; */
}
.tab-nav {
display: flex;
border-bottom: 1px solid #e8e8e8;
margin-bottom: 24px;
}
.tab-btn {
background: none;
border: none;
padding: 12px 24px;
font-size: 16px;
color: #666;
cursor: pointer;
border-bottom: 2px solid transparent;
transition: all 0.3s;
}
.tab-btn.active {
color: #1890ff;
border-bottom-color: #1890ff;
}
.tab-btn:hover {
color: #1890ff;
}
.tab-content {
min-height: 300px;
}
.intro-content {
text-align: center;
}
.course-intro-image {
width: 100%;
max-width: 100%;
height: auto;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.intro-content h4 {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 12px;
margin-top: 24px;
}
.intro-content h4:first-child {
margin-top: 0;
}
.intro-content p {
line-height: 1.6;
color: #666;
margin-bottom: 16px;
}
.intro-content ul {
padding-left: 20px;
margin-bottom: 16px;
}
.intro-content li {
line-height: 1.6;
color: #666;
margin-bottom: 8px;
}
/* 评论区域 */
.comments-content {
padding: 0;
}
.comment-stats {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid #f0f0f0;
}
.total-comments {
font-size: 16px;
font-weight: 600;
color: #333;
}
.comment-filters {
display: flex;
gap: 16px;
}
.filter-btn {
background: none;
border: none;
padding: 6px 12px;
font-size: 14px;
color: #666;
cursor: pointer;
border-radius: 4px;
transition: all 0.3s;
}
.filter-btn.active {
background: #1890ff;
color: white;
}
.filter-btn:hover:not(.active) {
background: #f0f0f0;
}
.comment-list {
margin-bottom: 24px;
}
.comment-item {
display: flex;
gap: 12px;
padding: 16px 0;
border-bottom: 1px solid #f8f8f8;
}
.comment-item:last-child {
border-bottom: none;
}
.comment-avatar {
flex-shrink: 0;
}
.comment-avatar img {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
}
.comment-content {
flex: 1;
min-width: 0;
}
.comment-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
}
.comment-username {
font-size: 14px;
font-weight: 600;
color: #333;
}
.comment-time {
font-size: 12px;
color: #999;
}
.comment-text {
font-size: 14px;
line-height: 1.5;
color: #666;
margin-bottom: 8px;
}
.comment-actions {
display: flex;
align-items: center;
gap: 16px;
}
.action-btn {
background: none;
border: none;
font-size: 12px;
color: #999;
cursor: pointer;
display: flex;
align-items: center;
gap: 14px;
transition: color 0.3s;
}
.action-btn:hover {
color: #1890ff;
}
.action-btn span {
font-size: 12px;
color: #999;
}
.action-btn .top {
padding: 4px 8px;
font-size: 10px;
color: #FF304B;
background-color: #FFF4F4;
border-radius: 30px;
}
.load-more {
text-align: center;
padding: 20px 0;
}
.btn-load-more {
background: #f0f0f0;
border: none;
padding: 10px 20px;
border-radius: 4px;
font-size: 14px;
color: #666;
cursor: pointer;
transition: all 0.3s;
}
.btn-load-more:hover {
background: #d9d9d9;
}
/* 更多课程 */
.more-courses {
background: white;
border-radius: 8px;
padding: 30px 45px;
}
.more-courses-header h3 {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 20px;
text-align: center;
position: relative;
padding: 0 20px;
}
.more-courses-header h3::before,
.more-courses-header h3::after {
content: "";
position: absolute;
top: 50%;
width: 34%;
height: 2px;
background-color: #E1E1E1;
;
}
.more-courses-header h3::before {
left: 0;
}
.more-courses-header h3::after {
right: 0;
}
.more-courses-list {
display: flex;
flex-direction: column;
gap: 20px;
}
.course-card {
border-radius: 8px;
overflow: hidden;
transition: all 0.3s ease;
background: white;
border: 1px solid #E1E1E1;
}
.course-cover {
position: relative;
height: 200px;
}
.course-image {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: space-between;
color: white;
font-weight: 600;
position: relative;
box-sizing: border-box;
}
.computer-bg {
background: linear-gradient(135deg, #87CEEB 0%, #4682B4 100%);
}
.computer-bg img {
width: 100%;
height: 100%;
object-fit: cover;
}
.english-bg {
background: linear-gradient(135deg, #4A5568 0%, #2D3748 100%);
}
.course-title-overlay {
font-size: 28px;
font-weight: bold;
text-align: left;
line-height: 1.2;
margin-bottom: auto;
}
.course-tags {
display: flex;
gap: 8px;
margin-bottom: 12px;
flex-wrap: wrap;
}
.tag {
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
background: #FA8C16;
color: white;
}
.live-time {
font-size: 14px;
color: white;
font-weight: 500;
margin-bottom: 0;
}
.course-subtitle {
font-size: 16px;
color: #FFD700;
margin-bottom: 8px;
font-weight: 600;
}
.course-english {
font-size: 12px;
opacity: 0.9;
text-transform: uppercase;
letter-spacing: 1px;
margin-top: auto;
}
.course-info {
padding: 16px 20px 16px 20px;
}
.course-desc {
font-size: 14px;
color: #333;
line-height: 1.5;
margin-bottom: 12px;
font-weight: 700;
}
.course-stats {
display: flex;
gap: 16px;
margin-bottom: 16px;
}
.stats-item {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: #999;
}
.icon-duration {
width: 16px;
height: 16px;
background-image: url('/images/courses/课程总时长.png');
background-size: contain;
background-repeat: no-repeat;
background-position: center;
display: inline-block;
vertical-align: middle;
margin-right: 4px;
flex-shrink: 0;
}
.course-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.enrolled-count {
font-size: 14px;
color: #999;
}
.btn-enroll-course {
background: #0088D1;
color: white;
border: none;
padding: 6px 10px;
border-radius: 20px;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-enroll-course:hover {
background: #40a9ff;
transform: translateY(-1px);
}
.loading-content,
.error-content {
text-align: center;
color: #666;
}
.retry-btn {
background: #1890ff;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
margin-top: 10px;
transition: background-color 0.3s;
}
.retry-btn:hover {
background: #40a9ff;
}
.icon-chapters {
width: 16px;
height: 16px;
background-image: url('/images/courses/课程总章数.png');
background-size: contain;
background-repeat: no-repeat;
background-position: center;
display: inline-block;
vertical-align: middle;
margin-right: 4px;
flex-shrink: 0;
}
/* 响应式设计 */
@media (max-width: 1399px) and (min-width: 1200px) {
.container {
padding: 0 24px;
max-width: 1200px;
}
.sidebar {
width: 350px;
}
}
@media (max-width: 1199px) and (min-width: 992px) {
.container {
padding: 0 20px;
max-width: 992px;
}
.course-content {
gap: 20px;
}
.sidebar {
width: 320px;
}
}
/* 平板横屏 */
@media (max-width: 1023px) and (min-width: 768px) {
.container {
padding: 0 16px;
max-width: 768px;
}
.course-content {
gap: 16px;
}
.sidebar {
width: 280px;
}
}
/* 平板竖屏及以下 */
@media (max-width: 767px) {
.container {
padding: 0 16px;
max-width: 576px;
}
.course-content {
flex-direction: column;
gap: 16px;
}
.sidebar {
width: 100%;
order: -1;
}
.video-player-section {
height: 400px;
}
}
@media (max-width: 767px) {
.container {
padding: 0 16px;
max-width: 576px;
}
.course-title {
font-size: 20px;
}
.course-meta {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.tab-nav {
overflow-x: auto;
}
.tab-btn {
white-space: nowrap;
padding: 12px 16px;
}
.instructors-list {
flex-wrap: wrap;
gap: 16px;
}
.video-container {
height: 400px;
}
.video-interaction-bar {
flex-direction: column;
gap: 16px;
align-items: stretch;
}
}
/* 底部交互区域 */
.video-interaction-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 24px;
background: #ffffff;
border-top: 1px solid #e5e7eb;
min-height: 60px;
}
.interaction-left {
display: flex;
align-items: center;
gap: 10px;
}
.interaction-btn {
display: flex;
align-items: center;
gap: 6px;
background: none;
border: none;
color: #9ca3af;
font-size: 12px;
cursor: pointer;
padding: 8px 2px;
border-radius: 4px;
transition: all 0.2s;
font-weight: 400;
}
.interaction-btn:hover {
background: #f9fafb;
color: #6b7280;
}
.split-line {
height: 12px;
width: 1px;
background: #f3f3f3;
}
.share-text {
margin-right: 35px;
}
/* 交互按钮图标样式已在各自的图标类中定义 */
.interaction-right {
flex: 1;
max-width: 650px;
margin-left: 40px;
}
.comment-input {
position: relative;
display: flex;
align-items: center;
width: 100%;
}
.comment-input input {
width: 100%;
padding: 12px 80px 12px 20px;
border: none;
border-radius: 10px;
font-size: 14px;
background: #EEF9FF;
outline: none;
transition: all 0.2s;
height: 44px;
color: #374151;
box-sizing: border-box;
}
.comment-input input:focus {
border-color: #d1d5db;
background: #ffffff;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.comment-input input::placeholder {
color: #9ca3af;
font-size: 14px;
}
.send-btn {
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
background: #0088D1;
color: white;
border: none;
padding: 8px 16px;
border-radius: 0 10px 10px 0;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
height: 42px;
min-width: 70px;
font-weight: 400;
z-index: 1;
}
.send-btn:hover {
background: #6b7280;
}
/* 交互区图标样式 */
.icon-like {
width: 18px;
height: 18px;
background-image: url('/images/courses/底部交互区1.png');
background-size: contain;
background-repeat: no-repeat;
background-position: center;
display: inline-block;
vertical-align: middle;
flex-shrink: 0;
opacity: 0.7;
}
.icon-share {
width: 18px;
height: 18px;
background-image: url('/images/courses/底部交互区2.png');
background-size: contain;
background-repeat: no-repeat;
background-position: center;
display: inline-block;
vertical-align: middle;
flex-shrink: 0;
opacity: 0.7;
}
.icon-note {
width: 18px;
height: 18px;
background-image: url('/images/courses/note.png');
background-size: contain;
background-repeat: no-repeat;
background-position: center;
display: inline-block;
vertical-align: middle;
flex-shrink: 0;
opacity: 0.7;
}
.icon-notes {
width: 32px;
height: 32px;
background-image: url('/images/courses/底部交互区4.png');
background-size: contain;
background-repeat: no-repeat;
background-position: center;
display: inline-block;
vertical-align: middle;
flex-shrink: 0;
opacity: 0.7;
}
.icon-download {
width: 32px;
height: 32px;
background-image: url('/images/courses/底部交互区4.png');
background-size: contain;
background-repeat: no-repeat;
background-position: center;
display: inline-block;
vertical-align: middle;
flex-shrink: 0;
opacity: 0.7;
}
/* 手机小屏优化 */
@media (max-width: 480px) {
.container {
padding: 0 12px;
}
.video-container {
height: 350px;
}
.interaction-left {
gap: 12px;
}
.interaction-btn {
font-size: 12px;
padding: 6px 4px;
}
.video-interaction-bar {
padding: 12px 16px;
gap: 12px;
}
.progress-section,
.course-sections {
margin-bottom: 16px;
}
}
/* 超小屏幕优化 */
@media (max-width: 360px) {
.container {
padding: 0 8px;
}
.video-container {
height: 300px;
}
.interaction-btn {
font-size: 11px;
padding: 4px 2px;
}
.interaction-left {
gap: 8px;
}
.progress-circles {
gap: 10px;
}
.circle-progress {
width: 50px;
height: 50px;
}
.progress-ring {
width: 50px;
height: 50px;
}
.progress-icon {
width: 16px;
height: 16px;
}
.section-header h4 {
font-size: 14px;
}
}
</style>