OL-LearnPlatform/src/views/CourseDetail.vue
username 8067376d43 www
2025-07-28 09:51:21 +08:00

1915 lines
47 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 unregistered">
<div class="video-background" :style="{ backgroundImage: `url(${course.coverImage})` }">
<div class="video-content">
<!-- 课程标题 -->
<h1 class="course-main-title">{{ course.title }}</h1>
<!-- 课程信息 -->
<div class="course-meta-info">
<span class="meta-item">讲师{{ course.instructor?.name }}</span>
<span class="meta-separator">|</span>
<span class="meta-item">时长{{ course.duration }}</span>
</div>
<!-- 报名按钮 -->
<button class="enroll-button" @click="handleEnrollCourse">
立即报名
</button>
</div>
</div>
</div>
</div>
<!-- 课程信息区域 -->
<div class="course-info-section">
<!-- 课程标题和分类 -->
<div class="course-header">
<h1 class="course-title">{{ course.title }}</h1>
<div class="course-meta">
<span class="course-category">分类<span class="category-tag">{{ course.category?.name || '未分类' }}</span></span>
<span class="course-price">时长{{ course.price || 0 }}</span>
<button class="btn-notes">记笔记</button>
</div>
</div>
<!-- 课程描述 -->
<div class="course-description">
<p>{{ course.description }}</p>
<div v-if="course.content" class="course-content-detail">
<h4>课程大纲</h4>
<div v-html="course.content"></div>
</div>
</div>
<!-- 讲师信息 -->
<div class="instructors-section" v-if="course.instructor">
<h3 class="section-title">讲师</h3>
<div class="instructors-list">
<div class="instructor-item">
<div class="instructor-avatar">
<SafeAvatar
:src="course.instructor.avatar"
:name="course.instructor.name"
:size="60"
/>
</div>
<div class="instructor-info">
<div class="instructor-name">{{ course.instructor.name }}</div>
<div class="instructor-title">{{ course.instructor.title }}</div>
<div v-if="course.instructor.bio" class="instructor-bio">{{ course.instructor.bio }}</div>
<div v-if="course.instructor.experience" class="instructor-experience">{{ course.instructor.experience }}</div>
</div>
</div>
</div>
</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">
<h4>课程详情</h4>
<p>本课程深度聚焦问题让每一位教师了解并学习使用DeepSeek结合办公自动化职业岗位标准以实际工作任务为引导强调课程内容的易用性和岗位要求的匹配性</p>
<p>课程内容与全国计算机等级考试"1+X"WPS办公应用职业技能等级证书技能大赛紧密结合课程设置紧密对应实际全面共享可为职业工作人员在校学生创行教师提供服务与学习支持</p>
<h4>学习目标</h4>
<ul>
<li>掌握DeepSeek的基本使用方法</li>
<li>了解办公自动化职业岗位标准</li>
<li>提高教学质量和效率</li>
<li>获得实际工作技能</li>
</ul>
<h4>适用人群</h4>
<p>本课程适合职业工作人员在校学生教师等群体学习</p>
<h4>课程大纲</h4>
<div class="course-outline-content">
<ul class="outline-list">
<li>
<strong>第一章基础入门</strong>
<ul>
<li>- 环境搭建与配置</li>
<li>- 基本概念理解</li>
<li>- 实践操作演示</li>
</ul>
</li>
<li>
<strong>第二章核心技能</strong>
<ul>
<li>- 核心功能详解</li>
<li>- 实际应用场景</li>
<li>- 案例分析讲解</li>
</ul>
</li>
<li>
<strong>第三章高级应用</strong>
<ul>
<li>- 进阶技巧掌握</li>
<li>- 项目实战演练</li>
<li>- 问题解决方案</li>
</ul>
</li>
</ul>
</div>
</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">
<i class="icon-like"></i>
{{ comment.likes }}
</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="enroll-section">
<button class="btn-enroll" @click="handleEnrollCourse">报名学习</button>
</div>
<!-- 课程章节列表 -->
<div class="course-sections">
<div class="sections-header">
<h3>课程章节</h3>
<div class="sections-actions">
<button class="sort-btn">
<svg width="14" height="14" viewBox="0 0 16 16">
<path d="M3 3h10M3 8h7M3 13h4" stroke="currentColor" stroke-width="1.5" fill="none"/>
</svg>
正序
</button>
<button @click="loadCourseSections" class="refresh-btn" style="margin-left: 10px;">
刷新章节
</button>
<button @click="testDirectApiCall" class="test-btn" style="margin-left: 10px;">
测试API
</button>
</div>
</div>
<div class="sections-content">
<!-- 调试信息 -->
<div class="debug-info" style="background: #f0f0f0; padding: 10px; margin-bottom: 10px; font-size: 12px;">
<p>课程ID: {{ courseId }}</p>
<p>章节数量: {{ courseSections.length }}</p>
<p>分组数量: {{ groupedSections.length }}</p>
<p>加载状态: {{ sectionsLoading ? '加载中' : '已完成' }}</p>
<p>错误信息: {{ sectionsError || '无' }}</p>
</div>
<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-number">第{{ chapterIndex + 1 }}章</span>
<span class="chapter-title">{{ 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, sectionIndex) in chapter.sections" :key="section.id" class="lesson-item">
<div class="lesson-info" @click="handleSectionClick(section)">
<span class="lesson-type" :class="getLessonTypeClass(section)">
{{ getLessonTypeText(section) }}
</span>
<span class="lesson-title">{{ section.name }}</span>
</div>
<div class="lesson-actions">
<span class="lesson-duration">{{ formatLessonDuration(section) }}</span>
<button class="lesson-action-btn" @click="handleSectionClick(section)" :class="getLessonActionClass(section)">
<svg v-if="isVideoLesson(section)" width="14" height="14" viewBox="0 0 16 16">
<circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="1.5" fill="none"/>
<path d="M6 5l6 3-6 3V5z" fill="currentColor"/>
</svg>
<svg v-else width="14" height="14" viewBox="0 0 16 16">
<path d="M8 2l3 6-3 6-3-6 3-6z" stroke="currentColor" stroke-width="1.5" fill="none"/>
<path d="M5 8h6" stroke="currentColor" stroke-width="1.5"/>
</svg>
</button>
</div>
</div>
</div>
</div>
</div>
<div v-else class="no-sections">
<p>暂无课程章节</p>
</div>
</div>
</div>
<!-- 推荐课程 -->
<div class="recommended-courses">
<div class="recommend-header">
<h3>推荐课程</h3>
</div>
<div class="recommend-list">
<!-- 计算机二级课程 -->
<div class="recommend-item">
<div class="recommend-image">
<div class="recommend-placeholder computer-bg">
<div class="placeholder-text">计算机二级</div>
</div>
<div class="recommend-badge">热门</div>
</div>
<div class="recommend-content">
<h4 class="recommend-title">计算机二级考前冲刺</h4>
<div class="recommend-tags">
<span class="tag">考试必备</span>
<span class="tag">名师授课</span>
<span class="tag">高通过率</span>
</div>
<p class="recommend-desc">备考计算机二级,名师带你高效复习,掌握考试重点,轻松通过考试</p>
<div class="recommend-meta">
<span class="recommend-price">¥99</span>
<button class="recommend-btn">立即学习</button>
</div>
</div>
</div>
<!-- 英语课程 -->
<div class="recommend-item">
<div class="recommend-image">
<div class="recommend-placeholder english-bg">
<div class="placeholder-text">摆脱哑巴英语</div>
</div>
</div>
<div class="recommend-content">
<h4 class="recommend-title">摆脱哑巴英语</h4>
<p class="recommend-desc">备考计算机二级,名师带你高效复习,掌握考试重点,轻松通过考试</p>
<div class="recommend-meta">
<span class="recommend-price">¥99</span>
<button class="recommend-btn">立即学习</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 登录模态框 -->
<LoginModal
v-model:show="loginModalVisible"
@success="handleAuthSuccess"
/>
<!-- 注册模态框 -->
<RegisterModal
v-model:show="registerModalVisible"
@success="handleAuthSuccess"
/>
<!-- 章节预览模态框 -->
<div v-if="previewModalVisible" class="preview-modal-overlay" @click="closePreviewModal">
<div class="preview-modal" @click.stop>
<div class="preview-modal-header">
<h3>{{ previewModalTitle }}</h3>
<button class="close-btn" @click="closePreviewModal">×</button>
</div>
<div class="preview-modal-content">
<div v-if="previewModalType === 'goals'" class="preview-goals">
<ul>
<li>掌握DeepSeek的基本使用方法</li>
<li>了解办公自动化职业岗位标准</li>
<li>提高教学质量和效率</li>
<li>获得实际工作技能</li>
</ul>
</div>
<div v-else-if="previewModalType === 'content'" class="preview-content" v-html="course?.content"></div>
<div v-else class="preview-text">{{ previewModalContent }}</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useAuth } from '@/composables/useAuth'
import { CourseApi } from '@/api/modules/course'
import type { Course, CourseSection } from '@/api/types'
import SafeAvatar from '@/components/common/SafeAvatar.vue'
import LoginModal from '@/components/auth/LoginModal.vue'
import RegisterModal from '@/components/auth/RegisterModal.vue'
const route = useRoute()
const router = useRouter()
const courseId = ref(Number(route.params.id))
const { loginModalVisible, registerModalVisible, enrollCourse, handleAuthSuccess } = useAuth()
// 当前选中的章节
const currentSection = ref<CourseSection | null>(null)
// 课程数据相关状态
const course = ref<Course | null>(null)
const loading = ref(false)
const error = ref('')
// 课程章节数据
const courseSections = ref<CourseSection[]>([])
const sectionsLoading = ref(false)
const sectionsError = ref('')
// 章节分组数据
interface ChapterGroup {
title: string
sections: CourseSection[]
expanded: boolean
}
const groupedSections = ref<ChapterGroup[]>([])
// 根据章节数据生成分组
const generateChapterGroups = () => {
if (courseSections.value.length === 0) {
groupedSections.value = []
return
}
console.log('开始生成章节分组,原始数据:', courseSections.value)
// 根据level字段分组章节
const groups: ChapterGroup[] = []
// 获取所有level=0的章节作为父标题
const parentSections = courseSections.value.filter(section => section.level === 0)
console.log('父级章节:', parentSections)
if (parentSections.length === 0) {
// 如果没有父级章节,将所有章节作为一个默认分组
groups.push({
title: '课程内容',
sections: courseSections.value,
expanded: true
})
} else {
parentSections.forEach((parentSection, index) => {
// 获取该父标题下的所有子标题 (level=1 且 parentId 匹配)
const childSections = courseSections.value.filter(section =>
section.level === 1 && section.parentId === parentSection.id
)
console.log(`父章节 ${parentSection.name} 的子章节:`, childSections)
groups.push({
title: parentSection.name, // 使用API返回的name作为标题
sections: childSections.length > 0 ? childSections : [parentSection], // 如果没有子章节,显示父章节本身
expanded: index === 0 // 默认展开第一章
})
})
}
console.log('生成的章节分组:', groups)
groupedSections.value = groups
}
// 获取章节标题
const getChapterTitle = (chapterIndex: number): string => {
const titles = [
'课前准备',
'程序设计基础知识',
'程序的控制结构',
'大话吉模型介绍',
'DeepSeek实际应用',
'DeepSeek实际应用'
]
return titles[chapterIndex - 1] || '课程内容'
}
// 预览模态框相关数据
const previewModalVisible = ref(false)
const previewModalTitle = ref('')
const previewModalContent = ref('')
const previewModalType = ref('')
// 新增的响应式数据
const activeTab = ref('intro')
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-1494790108755-2616b612b786?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 loadCourseDetail = async () => {
console.log('开始加载课程详情课程ID:', courseId.value)
if (!courseId.value || isNaN(courseId.value)) {
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 || isNaN(courseId.value)) {
sectionsError.value = '课程ID无效'
console.error('课程ID无效:', courseId.value)
return
}
try {
sectionsLoading.value = true
sectionsError.value = ''
console.log('开始加载课程章节课程ID:', courseId.value)
console.log('调用API: CourseApi.getCourseSections')
const response = await CourseApi.getCourseSections(courseId.value)
console.log('章节API响应:', response)
console.log('响应状态码:', response.code)
console.log('响应数据:', response.data)
if (response.code === 0 || response.code === 200) {
courseSections.value = response.data.list || []
console.log('章节数据设置成功,数量:', courseSections.value.length)
console.log('章节详细数据:', courseSections.value)
// 生成章节分组
generateChapterGroups()
} else {
sectionsError.value = response.message || '获取课程章节失败'
console.error('章节API返回错误:', response)
}
} catch (err) {
console.error('加载课程章节失败:', err)
sectionsError.value = '网络错误,请稍后重试'
} finally {
sectionsLoading.value = false
}
}
// 切换章节展开/折叠
const toggleChapter = (chapterIndex: number) => {
if (groupedSections.value[chapterIndex]) {
groupedSections.value[chapterIndex].expanded = !groupedSections.value[chapterIndex].expanded
}
}
// 格式化时长
const formatDuration = (sortOrder: number): string => {
// 根据章节序号模拟时长
const baseDuration = 5 + (sortOrder * 3) // 基础5分钟 + 序号*3分钟
const minutes = baseDuration % 60
const seconds = (sortOrder * 17) % 60 // 模拟秒数
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
}
// 获取课时类型样式类
const getLessonTypeClass = (section: CourseSection): string => {
// 根据章节内容判断类型
if (section.outline && section.outline.includes('video')) {
return 'lesson-type-video'
} else if (section.outline && section.outline.includes('ppt')) {
return 'lesson-type-document'
} else if (section.name.includes('作业') || section.name.includes('练习')) {
return 'lesson-type-homework'
} else if (section.name.includes('考试') || section.name.includes('测试')) {
return 'lesson-type-exam'
}
return 'lesson-type-video' // 默认为视频
}
// 获取课时类型文本
const getLessonTypeText = (section: CourseSection): string => {
if (section.outline && section.outline.includes('ppt')) {
return '资料'
} else if (section.name.includes('作业') || section.name.includes('练习')) {
return '作业'
} else if (section.name.includes('考试') || section.name.includes('测试')) {
return '考试'
}
return '视频' // 默认为视频
}
// 格式化课时时长
const formatLessonDuration = (section: CourseSection): string => {
// 根据课时名称和类型生成合适的时长
const durations = [
'01:03:56', '00:44:05', '00:52:22', '', // 第一章时长
'00:52:22', '', '01:03:56', '', '' // 第二章时长
]
// 根据section.id获取对应时长
const durationIndex = section.id - 1
if (durationIndex >= 0 && durationIndex < durations.length) {
return durations[durationIndex] || ''
}
// 默认时长生成
if (isVideoLesson(section)) {
const minutes = Math.floor(Math.random() * 60) + 10 // 10-70分钟
const seconds = Math.floor(Math.random() * 60)
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
}
return '' // 非视频课时不显示时长
}
// 判断是否为视频课时
const isVideoLesson = (section: CourseSection): boolean => {
return !!(section.outline && section.outline.includes('.m3u8'))
}
// 获取课时操作按钮样式类
const getLessonActionClass = (section: CourseSection): string => {
if (isVideoLesson(section)) {
return 'action-play'
} else {
return 'action-download'
}
}
// 点击课程章节标题
const handleSectionClick = (section: CourseSection) => {
console.log('点击课程章节:', section)
// 设置当前选中的章节
currentSection.value = section
// 检查是否有视频链接
if (section.outline && section.outline.includes('.m3u8')) {
console.log('获取到视频链接:', section.outline)
// 跳转到已报名区域并播放视频
navigateToEnrolledArea(section.outline, section.name)
} else {
// 如果不是视频,显示预览
previewSection(section)
}
}
// 跳转到已报名区域
const navigateToEnrolledArea = (videoUrl: string, sectionName: string) => {
console.log('跳转到已报名区域,播放视频:', videoUrl)
console.log('章节名称:', sectionName)
console.log('当前章节:', currentSection.value)
// 使用路由跳转到学习页面
router.push({
name: 'CourseStudy',
params: { id: courseId.value },
query: {
videoUrl: encodeURIComponent(videoUrl),
sectionName: encodeURIComponent(sectionName),
sectionId: currentSection.value?.id
}
})
}
// 更新视频播放器(备用方案)
const updateVideoPlayer = (videoUrl: string, sectionName: string) => {
console.log('更新视频播放器:', { videoUrl, sectionName })
// 如果在同一页面内更新视频播放器
// 可以通过事件总线或状态管理来实现
// 这里先显示确认信息
const confirmed = confirm(`即将播放视频: ${sectionName}\n是否继续`)
if (confirmed) {
navigateToEnrolledArea(videoUrl, sectionName)
}
}
// 预览章节(非视频内容)
const previewSection = (section: CourseSection) => {
console.log('预览章节:', section)
previewModalTitle.value = section.name
previewModalContent.value = `章节ID: ${section.id}\n章节名称: ${section.name}\n内容类型: ${getLessonTypeText(section)}`
previewModalType.value = 'section'
previewModalVisible.value = true
}
// 关闭预览模态框
const closePreviewModal = () => {
previewModalVisible.value = false
previewModalTitle.value = ''
previewModalContent.value = ''
previewModalType.value = ''
}
// 处理课程报名
const handleEnrollCourse = () => {
enrollCourse(courseId.value)
}
// 测试直接API调用
const testDirectApiCall = async () => {
console.log('=== 开始测试直接API调用 ===')
console.log('课程ID:', courseId.value)
try {
// 使用axios直接调用API
const axios = (await import('axios')).default
const url = `http://110.42.96.65:55510/api/lesson/section/list?lesson_id=${courseId.value}`
console.log('请求URL:', url)
const response = await axios.get(url)
console.log('直接API调用成功:', response.data)
alert('API调用成功请查看控制台')
} catch (error) {
console.error('直接API调用失败:', error)
alert('API调用失败请查看控制台')
}
}
onMounted(() => {
console.log('课程详情页加载完成课程ID:', courseId.value)
loadCourseDetail()
loadCourseSections()
})
</script>
<style scoped>
.course-detail-page {
min-height: 100vh;
background: #f5f7fa;
}
.loading-container,
.error-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
text-align: center;
}
.loading-content p,
.error-content p {
font-size: 16px;
color: #666;
margin-bottom: 20px;
}
.retry-btn {
background: #1890ff;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.retry-btn:hover {
background: #40a9ff;
}
.container {
max-width: 1600px;
margin: 0 auto;
padding: 0 142px;
}
.breadcrumb {
background: white;
padding: 12px 0;
border-bottom: 1px solid #e8e8e8;
}
.breadcrumb-text {
color: #666;
font-size: 14px;
}
.main-content {
padding: 20px 0;
}
.content-layout {
display: flex;
gap: 40px;
align-items: flex-start;
}
.course-content {
display: flex;
gap: 30px;
width: 100%;
}
.main-column {
flex: 1;
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.sidebar {
width: 320px;
flex-shrink: 0;
}
/* 视频播放器区域 */
.video-player-section {
position: relative;
height: 400px;
background: #000;
}
.video-player.unregistered {
height: 100%;
position: relative;
}
.video-background {
width: 100%;
height: 100%;
background-size: cover;
background-position: center;
position: relative;
}
.video-background::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
}
.video-content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
color: white;
z-index: 2;
}
.course-main-title {
font-size: 28px;
font-weight: 600;
margin-bottom: 16px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
}
.course-meta-info {
margin-bottom: 24px;
font-size: 16px;
}
.meta-item {
color: rgba(255, 255, 255, 0.9);
}
.meta-separator {
margin: 0 12px;
color: rgba(255, 255, 255, 0.6);
}
.enroll-button {
background: #1890ff;
color: white;
border: none;
padding: 12px 32px;
border-radius: 6px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.enroll-button:hover {
background: #40a9ff;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
}
/* 课程信息区域 */
.course-info-section {
padding: 24px;
}
.course-header {
margin-bottom: 24px;
padding-bottom: 20px;
border-bottom: 1px solid #f0f0f0;
}
.course-title {
font-size: 24px;
font-weight: 600;
color: #333;
margin-bottom: 12px;
line-height: 1.4;
}
.course-meta {
display: flex;
align-items: center;
gap: 20px;
flex-wrap: wrap;
}
.course-category {
font-size: 14px;
color: #666;
}
.category-tag {
background: #e6f7ff;
color: #1890ff;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
}
.course-price {
font-size: 16px;
font-weight: 600;
color: #f5222d;
}
.btn-notes {
background: #f0f0f0;
border: none;
padding: 6px 12px;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: background-color 0.3s;
}
.btn-notes:hover {
background: #d9d9d9;
}
/* 课程描述 */
.course-description {
margin-bottom: 24px;
line-height: 1.6;
color: #666;
}
.course-content-detail {
margin-top: 16px;
padding: 16px;
background: #f8f9fa;
border-radius: 6px;
}
.course-content-detail h4 {
margin-bottom: 12px;
color: #333;
}
/* 讲师信息 */
.instructors-section {
margin-bottom: 24px;
padding-bottom: 20px;
border-bottom: 1px solid #f0f0f0;
}
.section-title {
font-size: 18px;
font-weight: 600;
color: #333;
margin-bottom: 16px;
}
.instructor-item {
display: flex;
gap: 16px;
}
.instructor-info {
flex: 1;
}
.instructor-name {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 4px;
}
.instructor-title {
font-size: 14px;
color: #666;
margin-bottom: 8px;
}
.instructor-bio,
.instructor-experience {
font-size: 14px;
color: #666;
line-height: 1.5;
margin-bottom: 4px;
}
/* 课程标签页 */
.course-tabs {
margin-top: 24px;
}
.tab-nav {
display: flex;
border-bottom: 1px solid #f0f0f0;
margin-bottom: 24px;
}
.tab-btn {
background: none;
border: none;
padding: 12px 24px;
font-size: 16px;
color: #666;
cursor: pointer;
position: relative;
transition: color 0.3s;
}
.tab-btn.active {
color: #1890ff;
font-weight: 600;
}
.tab-btn.active::after {
content: '';
position: absolute;
bottom: -1px;
left: 0;
right: 0;
height: 2px;
background: #1890ff;
}
.tab-btn:hover {
color: #1890ff;
}
.tab-content {
min-height: 300px;
}
.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;
}
/* 右侧边栏课程章节 */
.sidebar .course-sections {
background: white;
border-radius: 8px;
padding: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.sections-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
background: #fafafa;
border-bottom: 1px solid #e8e8e8;
}
.sections-header h3 {
font-size: 16px;
font-weight: 600;
color: #333;
margin: 0;
}
.sections-actions {
display: flex;
align-items: center;
}
.sort-btn {
background: none;
border: none;
color: #666;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
transition: all 0.3s;
display: flex;
align-items: center;
gap: 4px;
}
.sort-btn:hover {
background: #f0f0f0;
color: #333;
}
.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;
}
.sections-list {
max-height: 600px;
overflow-y: auto;
}
.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: 16px 20px;
background: white;
cursor: pointer;
transition: background-color 0.2s;
border-bottom: 1px solid #f5f5f5;
}
.chapter-header:hover {
background: #fafafa;
}
.chapter-info {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
}
.chapter-number {
font-size: 14px;
font-weight: 600;
color: #333;
min-width: 60px;
}
.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 {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 20px 12px 40px;
border-bottom: 1px solid #f8f8f8;
transition: background-color 0.2s;
position: relative;
}
.lesson-item:last-child {
border-bottom: none;
}
.lesson-item:hover {
background: #f9f9f9;
}
.lesson-info {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
cursor: pointer;
transition: all 0.2s;
}
.lesson-info:hover {
color: #1890ff;
}
.lesson-info:hover .lesson-title {
color: #1890ff;
}
.lesson-type {
font-size: 11px;
padding: 3px 6px;
border-radius: 3px;
font-weight: 500;
min-width: 32px;
text-align: center;
line-height: 1;
}
.lesson-type-video {
background: #e6f7ff;
color: #1890ff;
border: 1px solid #b3d8ff;
}
.lesson-type-document {
background: #fff7e6;
color: #fa8c16;
border: 1px solid #ffd591;
}
.lesson-type-homework {
background: #f6ffed;
color: #52c41a;
border: 1px solid #b7eb8f;
}
.lesson-type-exam {
background: #fff1f0;
color: #ff4d4f;
border: 1px solid #ffb3b3;
}
.lesson-title {
font-size: 13px;
color: #333;
line-height: 1.4;
flex: 1;
font-weight: 400;
}
.lesson-actions {
display: flex;
align-items: center;
gap: 16px;
}
.lesson-duration {
font-size: 12px;
color: #999;
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
min-width: 50px;
text-align: right;
font-weight: 400;
}
.lesson-action-btn {
background: none;
border: none;
color: #52c41a;
cursor: pointer;
padding: 4px;
border-radius: 50%;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
}
.lesson-action-btn:hover {
background: #f6ffed;
transform: scale(1.1);
}
.lesson-action-btn.action-play {
color: #52c41a;
}
.lesson-action-btn.action-play:hover {
background: #f6ffed;
color: #52c41a;
}
.lesson-action-btn.action-download {
color: #52c41a;
}
.lesson-action-btn.action-download:hover {
background: #f6ffed;
color: #52c41a;
}
/* 预览模态框 */
.preview-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.preview-modal {
background: white;
border-radius: 8px;
width: 90%;
max-width: 600px;
max-height: 80vh;
overflow: hidden;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
}
.preview-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px;
border-bottom: 1px solid #f0f0f0;
background: #fafafa;
}
.preview-modal-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
.close-btn {
background: none;
border: none;
font-size: 24px;
color: #999;
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.3s;
}
.close-btn:hover {
background: #f0f0f0;
color: #666;
}
.preview-modal-content {
padding: 24px;
max-height: 60vh;
overflow-y: auto;
}
.preview-text {
font-size: 16px;
line-height: 1.6;
color: #666;
}
.preview-goals ul {
margin: 0;
padding-left: 20px;
}
.preview-goals li {
font-size: 16px;
line-height: 1.8;
color: #666;
margin-bottom: 8px;
}
.preview-content {
font-size: 16px;
line-height: 1.6;
color: #666;
}
.preview-content h4 {
font-size: 18px;
font-weight: 600;
color: #333;
margin: 16px 0 12px 0;
}
.preview-content ul {
margin: 12px 0;
padding-left: 20px;
}
.preview-content li {
margin-bottom: 8px;
line-height: 1.6;
}
/* 课程大纲样式 */
.course-outline-content {
margin-top: 16px;
}
.outline-list {
list-style: none;
padding-left: 0;
}
.outline-list > li {
margin-bottom: 20px;
padding: 16px;
background: #f8f9fa;
border-radius: 8px;
border-left: 4px solid #1890ff;
}
.outline-list > li > strong {
display: block;
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 12px;
}
.outline-list > li > ul {
list-style: none;
padding-left: 0;
margin: 0;
}
.outline-list > li > ul > li {
margin-bottom: 6px;
padding-left: 16px;
color: #666;
font-size: 14px;
line-height: 1.5;
position: relative;
}
.outline-list > li > ul > li:before {
content: "";
color: #1890ff;
font-weight: bold;
position: absolute;
left: 0;
}
/* 评论区 */
.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,
.filter-btn:hover {
background: #e6f7ff;
color: #1890ff;
}
.comment-list {
display: flex;
flex-direction: column;
gap: 20px;
}
.comment-item {
display: flex;
gap: 12px;
}
.comment-avatar img {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
}
.comment-content {
flex: 1;
}
.comment-header {
display: flex;
justify-content: space-between;
align-items: center;
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.6;
color: #666;
margin-bottom: 12px;
}
.comment-actions {
display: flex;
gap: 16px;
}
.action-btn {
background: none;
border: none;
font-size: 12px;
color: #999;
cursor: pointer;
display: flex;
align-items: center;
gap: 4px;
transition: color 0.3s;
}
.action-btn:hover {
color: #1890ff;
}
.load-more {
text-align: center;
margin-top: 24px;
}
.btn-load-more {
background: #f0f0f0;
border: none;
padding: 8px 24px;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: background-color 0.3s;
}
.btn-load-more:hover {
background: #d9d9d9;
}
/* 右侧边栏 */
.sidebar {
display: flex;
flex-direction: column;
gap: 20px;
}
.enroll-section {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
text-align: center;
}
.btn-enroll {
width: 100%;
background: #1890ff;
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.btn-enroll:hover {
background: #40a9ff;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
}
/* 推荐课程 */
.recommended-courses {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.recommend-header h3 {
font-size: 18px;
font-weight: 600;
color: #333;
margin-bottom: 16px;
}
.recommend-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.recommend-item {
border: 1px solid #f0f0f0;
border-radius: 8px;
overflow: hidden;
transition: box-shadow 0.3s;
}
.recommend-item:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.recommend-image {
position: relative;
height: 120px;
}
.recommend-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 600;
font-size: 16px;
}
.computer-bg {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.english-bg {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.recommend-badge {
position: absolute;
top: 8px;
right: 8px;
background: #ff4d4f;
color: white;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
}
.recommend-content {
padding: 16px;
}
.recommend-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.recommend-tags {
display: flex;
gap: 6px;
margin-bottom: 8px;
flex-wrap: wrap;
}
.tag {
background: #f0f0f0;
color: #666;
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
}
.recommend-desc {
font-size: 14px;
color: #666;
line-height: 1.5;
margin-bottom: 12px;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.recommend-meta {
display: flex;
justify-content: space-between;
align-items: center;
}
.recommend-price {
font-size: 16px;
font-weight: 600;
color: #f5222d;
}
.recommend-btn {
background: #1890ff;
color: white;
border: none;
padding: 6px 16px;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: background-color 0.3s;
}
.recommend-btn:hover {
background: #40a9ff;
}
/* 响应式设计 */
@media (max-width: 1200px) {
.container {
padding: 0 20px;
}
.course-content {
gap: 20px;
}
.sidebar {
width: 280px;
}
}
@media (max-width: 992px) {
.course-content {
flex-direction: column;
}
.sidebar {
width: 100%;
order: -1;
}
.video-player-section {
height: 300px;
}
}
@media (max-width: 768px) {
.container {
padding: 0 16px;
}
.course-main-title {
font-size: 24px;
}
.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;
}
}
</style>