2897 lines
68 KiB
Vue
2897 lines
68 KiB
Vue
<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=2,parentId匹配)
|
||
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>
|