1915 lines
47 KiB
Vue
1915 lines
47 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 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> |