2025-08-16 20:39:56 +08:00

3870 lines
95 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="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="banner-title-section">
<div class="banner-content">
<div class="banner-text">
<span class="main-text">暑期名师领学,提高班级教学质量!高效冲分指南</span>
<div class="ai-companion-tag">
<img src="/images/aiCompanion/AI伴学标签@2x.png" alt="AI伴学" class="tag-image">
</div>
</div>
<div class="banner-button">
<img src="/images/aiCompanion/切换@2x.png" alt="切换" class="button-icon-image">
<span class="button-text">普通</span>
</div>
</div>
</div>
<!-- 视频播放器区域 - 未报名状态 -->
<div class="video-player-section">
<div class="video-player unregistered">
<div class="video-background" style="background-image: url('/images/aiCompanion/背景色@2x.png');">
<div class="video-content">
<!-- 锁定图标 -->
<div class="lock-icon">
<img src="/images/aiCompanion/lock.png" alt="">
</div>
<!-- 主要信息 -->
<div class="wisdom-points-info">
<h2 class="main-message">完整课程内容,需使用智点兑换</h2>
<!-- 课程统计信息 -->
<div class="course-stats-info">
<span class="stats-item">
<span class="icon-chapters"></span>
共9章54节
</span>
<span class="stats-item">
<span class="icon-duration"></span>
12小时43分钟
</span>
</div>
</div>
<!-- 兑换按钮 -->
<div class="exchange-button-container">
<button class="exchange-button" @click="handleExchangeCourse">
消耗29智点 | 立即兑换
</button>
</div>
<!-- 用户智点信息 -->
<div class="user-points-info">
<div class="points-display">您的智点101.5</div>
<div class="get-more-points">获取更多智点 >></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 class="course-info-section">
<!-- 课程标题 -->
<div class="course-header">
<!-- 课程元信息 -->
<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">
</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">
<div class="safe-avatar" style="width: 50px; height: 50px;">
<img :src="instructor.avatar" :alt="instructor.name">
</div>
</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="sidebar-title">
<h2>学习进度</h2>
<img src="/images/aiCompanion/fold.png" alt="">
</div>
<!-- 学习进度 -->
<div class="progress-section">
<div class="progress-header">
<h3 class="progress-title">学习进度</h3>
<p class="progress-subtitle">实时跟踪您的学习状态和完成情况</p>
</div>
<!-- 三个圆形进度图表 -->
<div class="progress-circles">
<!-- 课程进度 -->
<div class="progress-item">
<div class="circle-container">
<svg class="progress-circle" width="80" height="80" viewBox="0 0 80 80">
<!-- 背景圆环 -->
<circle cx="40" cy="40" r="32" stroke="##E2F5FF" stroke-width="8" fill="none"
class="progress-bg" />
<!-- 进度圆环 -->
<circle cx="40" cy="40" r="32" stroke="#078BD2" stroke-width="8" fill="none"
stroke-linecap="round" class="progress-fill" :stroke-dasharray="circumference"
:stroke-dashoffset="circumference - (circumference * videoProgress / 100)" />
</svg>
<!-- 中心图标和文字 -->
<div class="circle-content">
<div class="progress-icon">
<img src="/images/courses/course-icon.png" alt="课程图标" class="course-icon" />
</div>
<div class="progress-label">课程</div>
</div>
</div>
<div class="progress-percentage">{{ videoProgress.toFixed(1) }}%</div>
</div>
<!-- 作业进度 -->
<div class="progress-item">
<div class="circle-container">
<svg class="progress-circle" width="80" height="80" viewBox="0 0 80 80">
<!-- 背景圆环 -->
<circle cx="40" cy="40" r="32" stroke="#d9ecff" stroke-width="8" fill="none"
class="progress-bg" />
<!-- 进度圆环 -->
<circle cx="40" cy="40" r="32" stroke="#078BD2" stroke-width="8" fill="none"
stroke-linecap="round" class="progress-fill" :stroke-dasharray="circumference"
:stroke-dashoffset="circumference - (circumference * exerciseProgress / 100)" />
</svg>
<!-- 中心图标和文字 -->
<div class="circle-content">
<div class="progress-icon">
<img src="/images/courses/homework-icon.png" alt="作业图标" class="homework-icon" />
</div>
<div class="progress-label">作业</div>
</div>
</div>
<div class="progress-percentage">{{ exerciseProgress.toFixed(1) }}%</div>
</div>
<!-- 考试进度 -->
<div class="progress-item">
<div class="circle-container">
<svg class="progress-circle" width="80" height="80" viewBox="0 0 80 80">
<!-- 背景圆环 -->
<circle cx="40" cy="40" r="32" stroke="#d9ecff" stroke-width="8" fill="none"
class="progress-bg" />
<!-- 进度圆环 -->
<circle cx="40" cy="40" r="32" stroke="#078BD2" stroke-width="8" fill="none"
stroke-linecap="round" class="progress-fill" :stroke-dasharray="circumference"
:stroke-dashoffset="circumference - (circumference * examProgress / 100)" />
</svg>
<!-- 中心图标和文字 -->
<div class="circle-content">
<div class="progress-icon">
<img src="/images/courses/examination-icon.png" alt="考试图标" class="exam-icon" />
</div>
<div class="progress-label">考试</div>
</div>
</div>
<div class="progress-percentage">{{ examProgress.toFixed(1) }}%</div>
</div>
</div>
<!-- 总体学习进度条 -->
<div class="overall-progress">
<div class="progress-bar-container">
<div class="progress-bar">
<div class="progress-bar-fill" :style="{ width: overallProgress + '%' }"></div>
</div>
</div>
<div class="progress-info">
<div class="progress-text">
<span class="progress-title">学习总进度</span>
<span class="progress-value">{{ overallProgress.toFixed(1) }}%</span>
</div>
<div class="progress-count">
<span class="current">{{ completedLessons }}</span>
<span class="separator">/</span>
<span class="total">{{ totalSections }}</span>
</div>
</div>
</div>
</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-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 in chapter.sections" :key="section.id" class="lesson-item">
<div class="lesson-content" :class="{ 'unregistered': !isUserEnrolled }"
@click="isUserEnrolled ? handleSectionClick(section) : handleUnregisteredClick(section)">
<div class="lesson-type-badge"
:class="[getLessonTypeBadgeClass(section), { 'disabled': !isUserEnrolled }]">
{{ getLessonTypeText(section) }}
</div>
<div class="lesson-info">
<span class="lesson-title" :class="{ 'disabled': !isUserEnrolled }">{{ section.name
}}</span>
</div>
<div class="lesson-meta">
<span v-if="isVideoLesson(section)" class="lesson-duration"
:class="{ 'disabled': !isUserEnrolled }">{{ formatLessonDuration(section) }}</span>
<div class="lesson-actions">
<!-- 视频播放图标 -->
<button v-if="isVideoLesson(section)" class="lesson-action-btn video-btn"
:class="{ 'disabled': !isUserEnrolled }" :disabled="!isUserEnrolled"
@click.stop="isUserEnrolled ? handleSectionClick(section) : handleUnregisteredClick(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"
:class="{ 'disabled': !isUserEnrolled }" :disabled="!isUserEnrolled"
@click.stop="isUserEnrolled ? handleDownload(section) : handleUnregisteredClick(section)">
<img src="/images/courses/download-enroll.png" alt="资料" width="14" height="14">
</button>
<!-- 作业图标 -->
<button v-else-if="isHomeworkLesson(section)" class="lesson-action-btn edit-btn"
:class="{ 'disabled': !isUserEnrolled }" :disabled="!isUserEnrolled"
@click.stop="isUserEnrolled ? handleHomework(section) : handleUnregisteredClick(section)">
<img src="/images/courses/homework-enroll.png" alt="作业" width="14" height="14">
</button>
<!-- 考试图标 -->
<button v-else-if="isExamLesson(section)" class="lesson-action-btn exam-btn"
:class="{ 'disabled': !isUserEnrolled }" :disabled="!isUserEnrolled"
@click.stop="isUserEnrolled ? handleExam(section) : handleUnregisteredClick(section)">
<img src="/images/courses/examination-enroll.png" alt="考试" width="14" height="14">
</button>
</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>
<!-- 登录模态框 -->
<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 v-if="enrollConfirmVisible" class="modal-overlay" @click="cancelEnrollment">
<div class="modal-content" @click.stop>
<div class="modal-header">
<h3>确认兑换</h3>
<button class="modal-close" @click="cancelEnrollment">×</button>
</div>
<div class="modal-body">
<p>确定要兑换《{{ course?.title }}》课程吗?</p>
<p class="modal-tip">兑换后将消耗29智点获得完整的学习权限</p>
</div>
<div class="modal-footer">
<button class="btn-cancel" @click="cancelEnrollment">取消</button>
<button class="btn-confirm" @click="confirmEnrollment" :disabled="enrollmentLoading">
{{ enrollmentLoading ? '兑换中...' : '立即兑换' }}
</button>
</div>
</div>
</div>
<!-- 报名成功弹窗 -->
<div v-if="enrollSuccessVisible" class="modal-overlay">
<div class="modal-content success-modal">
<div class="success-icon"></div>
<h3>报名成功</h3>
<p>正在跳转到已报名状态页面...</p>
<p class="success-tip">您将看到彩色可点击的课程章节</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useAuth } from '@/composables/useAuth'
import { useUserStore } from '@/stores/user'
import { CourseApi } from '@/api/modules/course'
import type { Course, CourseSection } from '@/api/types'
import LoginModal from '@/components/auth/LoginModal.vue'
import RegisterModal from '@/components/auth/RegisterModal.vue'
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
const courseId = ref(Number(route.params.id))
const { loginModalVisible, registerModalVisible, handleAuthSuccess, showLoginModal } = useAuth()
// enrollCourse 暂时未使用,后续需要时再启用
// 当前选中的章节
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('')
// 报名状态管理
const isEnrolled = ref(false) // 用户是否已报名该课程
const enrollmentLoading = ref(false) // 报名加载状态
// 报名状态
// const RegistrationStatus = ref(false)
// 学习进度相关数据
const completedLessons = ref(0)
const videoProgress = ref(0)
const exerciseProgress = ref(0)
const examProgress = ref(0)
// 处理记笔记点击事件
// const handleNotesClick = () => {
// if (isUserEnrolled.value) {
// // 已报名,执行记笔记逻辑
// console.log('开始记笔记')
// // 这里可以添加打开笔记模态框的逻辑
// } else if (userStore.isLoggedIn) {
// // 已登录但未报名,提示去报名
// enrollConfirmVisible.value = true
// } else {
// // 未登录,显示登录模态框
// showLoginModal()
// }
// }
// 计算用户是否已报名
const isUserEnrolled = computed(() => {
// 必须同时满足:用户已登录 AND 已报名该课程
return userStore.isLoggedIn && isEnrolled.value
// 临时测试不同状态:
// return false // 强制显示未报名状态(灰色不可点击)
// return true // 强制显示已报名状态(彩色可点击)
})
// 报名确认弹窗
const enrollConfirmVisible = ref(false)
const enrollSuccessVisible = ref(false)
// 章节分组数据
interface ChapterGroup {
title: string
sections: CourseSection[]
expanded: boolean
}
const groupedSections = ref<ChapterGroup[]>([])
// 生成模拟章节数据(用于演示)
const generateMockSections = (): CourseSection[] => {
return [
// 第一章 - 课前准备 (4个)
{ id: '1', lessonId: String(courseId.value), name: '开课彩蛋:新开始新征程', outline: 'https://example.com/video1.m3u8', type: 0, parentId: '0', sort: 1, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: true, duration: '01:03:56' },
{ id: '2', lessonId: String(courseId.value), name: '课程定位与目标', outline: 'https://example.com/video2.m3u8', type: 0, parentId: '0', sort: 2, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: true, duration: '00:44:05' },
{ id: '3', lessonId: String(courseId.value), name: '教学安排及学习建议', outline: 'https://example.com/video3.m3u8', type: 0, parentId: '0', sort: 3, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: true, duration: '00:52:22' },
{ id: '4', lessonId: String(courseId.value), name: '课前准备PPT', outline: 'https://example.com/ppt1.ppt', type: 1, parentId: '0', sort: 4, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined },
// 第二章 - 程序设计基础知识 (5个)
{ id: '5', lessonId: String(courseId.value), name: '第一课 程序设计入门', outline: 'https://example.com/video4.m3u8', type: 0, parentId: '0', sort: 5, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: true, duration: '00:52:22' },
{ id: '6', lessonId: String(courseId.value), name: '操作PPT', outline: 'https://example.com/ppt2.ppt', type: 1, parentId: '0', sort: 6, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined },
{ id: '7', lessonId: String(courseId.value), name: '第二课 循环结构', outline: 'https://example.com/video5.m3u8', type: 0, parentId: '0', sort: 7, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: true, duration: '01:03:56' },
{ id: '8', lessonId: String(courseId.value), name: '函数&循环', outline: 'https://example.com/video5.m3u8', type: 0, parentId: '0', sort: 8, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined },
{ id: '9', lessonId: String(courseId.value), name: '第三课 条件结构', outline: 'https://example.com/video6.m3u8', type: 0, parentId: '0', sort: 9, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '00:45:30' },
// 第三章 - 实战项目 (6个)
{ id: '10', lessonId: String(courseId.value), name: '项目一:计算器开发', outline: 'https://example.com/video7.m3u8', type: 0, parentId: '0', sort: 10, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '01:20:15' },
{ id: '11', lessonId: String(courseId.value), name: '项目源码下载', outline: 'https://example.com/source1.zip', type: 1, parentId: '0', sort: 11, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined },
{ id: '12', lessonId: String(courseId.value), name: '项目二:数据管理系统', outline: 'https://example.com/video8.m3u8', type: 0, parentId: '0', sort: 12, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '01:45:20' },
{ id: '13', lessonId: String(courseId.value), name: '作业:完成个人项目', outline: '', type: 0, parentId: '0', sort: 13, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined },
{ id: '14', lessonId: String(courseId.value), name: '项目三Web应用开发', outline: 'https://example.com/video9.m3u8', type: 0, parentId: '0', sort: 14, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '02:10:45' },
{ id: '15', lessonId: String(courseId.value), name: '期末考试', outline: '', type: 0, parentId: '0', sort: 15, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined },
// 第四章 - 高级应用 (4个)
{ id: '16', lessonId: String(courseId.value), name: '高级特性介绍', outline: 'https://example.com/video10.m3u8', type: 0, parentId: '0', sort: 16, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '00:55:30' },
{ id: '17', lessonId: String(courseId.value), name: '性能优化技巧', outline: 'https://example.com/video11.m3u8', type: 0, parentId: '0', sort: 17, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '01:15:20' },
{ id: '18', lessonId: String(courseId.value), name: '部署与发布', outline: 'https://example.com/video12.m3u8', type: 0, parentId: '0', sort: 18, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '00:40:15' },
{ id: '19', lessonId: String(courseId.value), name: '课程总结', outline: 'https://example.com/video13.m3u8', type: 0, parentId: '0', sort: 19, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '00:30:10' },
// 第五章 - 拓展学习 (3个)
{ id: '20', lessonId: String(courseId.value), name: '行业发展趋势', outline: 'https://example.com/video14.m3u8', type: 0, parentId: '0', sort: 20, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '00:35:45' },
{ id: '21', lessonId: String(courseId.value), name: '学习资源推荐', outline: 'https://example.com/resources.pdf', type: 1, parentId: '0', sort: 21, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined },
{ id: '22', lessonId: String(courseId.value), name: '结业证书申请', outline: '', type: 0, parentId: '0', sort: 22, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined },
// 第六章 - 答疑与交流 (2个)
{ id: '23', lessonId: String(courseId.value), name: '常见问题解答', outline: 'https://example.com/video15.m3u8', type: 0, parentId: '0', sort: 23, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '00:25:30' },
{ id: '24', lessonId: String(courseId.value), name: '在线答疑直播', outline: 'https://example.com/live1.m3u8', type: 0, parentId: '0', sort: 24, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '01:30:00' }
]
}
// 将章节按章分组
const groupSectionsByChapter = (sections: CourseSection[]) => {
const chapterTitles = [
'课前准备',
'程序设计基础知识',
'实战项目',
'高级应用',
'拓展学习',
'答疑与交流'
]
const groups: ChapterGroup[] = []
let sectionsPerChapter = [4, 5, 6, 4, 3, 2] // 每章的课程数量
let sectionIndex = 0
for (let i = 0; i < chapterTitles.length; i++) {
const chapterSections = sections.slice(sectionIndex, sectionIndex + sectionsPerChapter[i])
if (chapterSections.length > 0) {
groups.push({
title: `${i + 1}${chapterTitles[i]}`,
sections: chapterSections,
expanded: i === 0 // 默认展开第一章
})
}
sectionIndex += sectionsPerChapter[i]
}
return groups
}
// 根据章节数据生成分组
// const generateChapterGroups = () => {
// // 确保有章节数据
// if (courseSections.value.length === 0) {
// console.log('没有章节数据,生成模拟数据')
// courseSections.value = generateMockSections()
// }
// console.log('开始生成章节分组,原始数据:', courseSections.value)
// console.log('章节数据数量:', courseSections.value.length)
// // 使用统一的分组函数
// groupedSections.value = groupSectionsByChapter(courseSections.value)
// console.log('生成的章节分组:', groupedSections.value)
// }
// 获取章节标题已弃用使用groupSectionsByChapter替代
// 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 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-1507003211169-0a1dd7228f2d?ixlib=rb-4.0.3&auto=format&fit=crop&w=60&q=80'
}
])
// 计算属性
const totalLessons = computed(() => {
return groupedSections.value.length
})
const totalSections = computed(() => {
return courseSections.value.length
})
// 计算圆环周长
const circumference = computed(() => 2 * Math.PI * 32) // r=32
// 计算总体进度
const overallProgress = computed(() => {
if (totalSections.value === 0) return 0
return (completedLessons.value / totalSections.value) * 100
})
const formatTotalDuration = () => {
// 计算总时长
let totalMinutes = 0
courseSections.value.forEach((section: CourseSection) => {
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 loadCourseDetail = async () => {
console.log('开始加载课程详情课程ID:', courseId.value)
if (!courseId.value) courseId.value = 1
if (!courseId.value || isNaN(courseId.value)) {
console.log('课程ID无效使用模拟数据')
loadMockCourseData()
return
}
try {
loading.value = true
error.value = ''
console.log('调用API获取课程详情...')
const response = await CourseApi.getCourseById(String(courseId.value))
console.log('API响应:', response)
if (response.code === 0 || response.code === 200) {
course.value = response.data
console.log('课程数据设置成功:', course.value)
// 确保讲师和时长信息正确显示
if (course.value) {
if (!course.value.instructor?.name) {
course.value.instructor = {
id: 1,
name: 'DeepSeek技术学院',
title: '讲师',
bio: '',
avatar: '',
rating: 4.8,
studentsCount: 1000,
coursesCount: 10,
experience: '5年教学经验',
education: ['计算机科学硕士'],
certifications: ['高级讲师认证']
}
}
if (!course.value.duration || course.value.duration === '待定') {
course.value.duration = '59天'
}
}
} else {
console.log('API返回错误使用模拟数据')
loadMockCourseData()
}
} catch (err) {
console.error('加载课程详情失败:', err)
console.log('API调用失败使用模拟数据')
loadMockCourseData()
} finally {
loading.value = false
}
}
// 加载课程章节列表
const loadCourseSections = async () => {
if (!courseId.value || isNaN(courseId.value)) {
console.log('课程ID无效使用模拟章节数据')
loadMockData()
return
}
try {
sectionsLoading.value = true
sectionsError.value = ''
console.log('调用API获取课程章节...')
const response = await CourseApi.getCourseSections(String(courseId.value))
console.log('章节API响应:', response)
if (response.code === 0 || response.code === 200) {
if (response.data && Array.isArray(response.data)) {
courseSections.value = response.data
groupedSections.value = groupSectionsByChapter(response.data)
console.log('章节数据设置成功:', courseSections.value)
console.log('分组数据:', groupedSections.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 loadMockCourseData = () => {
console.log('加载模拟课程数据')
course.value = {
id: String(courseId.value) || '1',
title: 'DeepSeek办公自动化职业岗位标准课程',
description: '本课程将帮助您掌握DeepSeek的基本使用方法了解办公自动化职业岗位标准提高教学质量和效率获得实际工作技能。',
instructor: {
id: 1,
name: 'DeepSeek技术学院',
title: '讲师',
bio: '专注于AI技术应用与教学',
avatar: '/images/aiCompanion/AI小助手@2x.png',
rating: 4.8,
studentsCount: 1000,
coursesCount: 10,
experience: '5年教学经验',
education: ['计算机科学硕士'],
certifications: ['高级讲师认证']
},
duration: '12小时43分钟',
totalLessons: 54,
rating: 4.8,
studentsCount: 1000,
price: 0,
originalPrice: 299,
category: {
id: 1,
name: 'AI技术',
slug: 'ai-technology',
description: 'AI技术',
icon: 'https://example.com/ai-icon.png'
},
level: 'beginner',
tags: ['AI', '办公自动化', 'DeepSeek'],
thumbnail: '/images/aiCompanion/bg.png',
content: '课程内容详细介绍...',
objectives: ['掌握DeepSeek的基本使用方法', '了解办公自动化职业岗位标准', '提高教学质量和效率'],
requirements: ['基础计算机操作能力', '对AI技术感兴趣'],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
ratingCount: 0,
currency: 'CNY',
language: '中文',
skills: ['AI', '办公自动化', 'DeepSeek'],
status: 'published'
}
}
// 加载模拟数据
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
// 计算各类进度
const videoSections = mockSections.filter(section => isVideoLesson(section))
const exerciseSections = mockSections.filter(section => isHomeworkLesson(section))
const examSections = mockSections.filter(section => isExamLesson(section))
videoProgress.value = videoSections.length > 0 ? Math.round((videoSections.filter(s => s.completed).length / videoSections.length) * 100) : 0
exerciseProgress.value = exerciseSections.length > 0 ? Math.round((exerciseSections.filter(s => s.completed).length / exerciseSections.length) * 100) : 0
examProgress.value = examSections.length > 0 ? Math.round((examSections.filter(s => s.completed).length / examSections.length) * 100) : 0
}
// 切换章节展开/折叠
const toggleChapter = (chapterIndex: number) => {
console.log('点击切换章节,章节索引:', chapterIndex)
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 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 = Number(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 => {
if (!section.outline && getLessonTypeText(section) != '考试') {
return true
}
console.log(section.outline)
// 检查常见视频格式扩展名
return !!(section.outline && (
section.outline.includes('.m3u8') ||
section.outline.includes('.mp4') ||
section.outline.includes('.avi') ||
section.outline.includes('.mov') ||
section.outline.includes('.wmv')
)) || section.name.includes('视频')
}
// 判断是否为资料课时
const isResourceLesson = (section: CourseSection): boolean => {
return !!(section.outline && section.outline.includes('ppt')) || section.name.includes('PPT')
}
// 判断是否为作业课时
const isHomeworkLesson = (section: CourseSection): boolean => {
return section.name.includes('作业') || section.name.includes('练习') || section.name.includes('题目') || section.name.includes('分析')
}
// 判断是否为考试课时
const isExamLesson = (section: CourseSection): boolean => {
return section.name.includes('考试') || section.name.includes('测试') || section.name.includes('函数&循环')
}
// 获取课时类型徽章样式类
const getLessonTypeBadgeClass = (section: CourseSection): string => {
if (isVideoLesson(section)) {
return 'badge-video'
} else if (isResourceLesson(section)) {
return 'badge-resource'
} else if (isHomeworkLesson(section)) {
return 'badge-homework'
} else if (isExamLesson(section)) {
return 'badge-exam'
}
return 'badge-video' // 默认为视频
}
// 处理下载操作
const handleDownload = (section: CourseSection) => {
console.log('下载资料:', section)
// 这里可以实现下载逻辑
alert(`下载资料: ${section.name}`)
}
// 处理作业操作
const handleHomework = (section: CourseSection) => {
console.log('打开作业:', section)
// 跳转到练习页面
router.push({
name: 'Practice',
params: {
courseId: courseId.value,
sectionId: section.id
},
query: {
courseName: course.value?.title || '课程名称',
practiceName: section.name
}
})
}
// 处理考试操作
const handleExam = (section: CourseSection) => {
console.log('开始考试:', section)
// 跳转到考前须知页面
router.push({
name: 'ExamNotice',
params: {
courseId: courseId.value,
sectionId: section.id
},
query: {
courseName: course.value?.title || '课程名称',
examName: section.name
}
})
}
// 点击课程章节标题
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 = () => {
// if (!userStore.isLoggedIn) {
// // 未登录,显示登录弹窗
// showLoginModal()
// return
// }
// if (isEnrolled.value) {
// // 已报名,跳转到学习页面
// console.log('用户已报名,跳转到学习页面')
// router.push(`/course/${courseId.value}/study`)
// return
// }
// // 未报名,显示报名确认弹窗
// console.log('用户未报名,显示报名确认弹窗')
// enrollConfirmVisible.value = true
// }
// 确认报名
const confirmEnrollment = async () => {
try {
enrollmentLoading.value = true
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000))
// 报名成功
isEnrolled.value = true
enrollConfirmVisible.value = false
enrollSuccessVisible.value = true
// 2秒后跳转到已兑换课程页面
setTimeout(() => {
enrollSuccessVisible.value = false
// 跳转到已兑换课程页面
router.push(`/course/${courseId.value}/exchanged`)
}, 2000)
} catch (error) {
console.error('报名失败:', error)
} finally {
enrollmentLoading.value = false
}
}
// 取消报名
const cancelEnrollment = () => {
enrollConfirmVisible.value = false
}
// 处理未报名用户点击
const handleUnregisteredClick = (section: CourseSection) => {
console.log('未报名用户点击课程:', section.name)
// 显示报名提示
enrollConfirmVisible.value = true
}
// 处理课程兑换
const handleExchangeCourse = () => {
if (!userStore.isLoggedIn) {
// 未登录,显示登录弹窗
showLoginModal()
return
}
// 显示兑换确认弹窗
console.log('用户点击兑换课程')
enrollConfirmVisible.value = true
}
// 测试直接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调用失败请查看控制台')
// }
// }
// 初始化模拟状态(用于演示)
const initializeMockState = () => {
// 模拟用户已登录
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'
}
// 模拟用户未报名状态,可以测试完整的报名流程
isEnrolled.value = false // false=未报名状态true=已报名状态
}
onMounted(() => {
console.log('课程详情页加载完成课程ID:', courseId.value)
initializeMockState() // 初始化模拟状态
loadCourseDetail()
loadCourseSections()
})
</script>
<style scoped>
.course-detail-page {
padding-top: 30px;
min-height: 100vh;
background: #F6F6F6;
background-image: url('/images/aiCompanion/背景色@2x.png');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
.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: 1400px;
margin: 0 auto;
padding: 0 32px;
}
.breadcrumb {
padding: 12px 0;
}
.breadcrumb-text {
color: #666;
font-size: 14px;
}
.main-content {
padding-bottom: 20px;
}
.content-layout {
display: flex;
gap: 40px;
align-items: flex-start;
}
.course-content {
display: flex;
gap: 30px;
width: 100%;
flex-direction: row-reverse;
}
.main-column {
flex: 1;
overflow: hidden;
}
.sidebar {
width: 370px;
flex-shrink: 0;
padding-top: 0;
}
.sidebar-title {
display: flex;
justify-content: space-between;
align-items: center;
}
.sidebar-title h2 {
font-family: PingFangSC, PingFang SC;
font-weight: 600;
font-size: 16px;
color: #000000;
line-height: 26px;
text-align: justify;
font-style: normal;
}
.sidebar-title img {
width: 14px;
height: 14px;
/* background: #999999; */
}
/* 学习进度区域 */
.progress-section {
margin-top: 5px;
background: #ffffff90;
padding: 24px;
margin-bottom: 20px;
border: 1px solid white;
}
/* 进度头部样式 */
.progress-header {
margin-bottom: 20px;
text-align: center;
display: flex;
justify-content: space-between;
align-items: center;
}
.progress-title {
font-size: 20px;
font-weight: 600;
color: #1f2937;
/* margin: 0 0 8px 0; */
line-height: 1.4;
}
.progress-subtitle {
font-size: 14px;
color: #6b7280;
margin: 0;
line-height: 1.5;
}
/* 三个圆形进度图表 */
.progress-circles {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 22px;
gap: 40px;
}
.progress-item {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
flex: 0 0 auto;
}
.circle-container {
position: relative;
width: 80px;
height: 80px;
margin-bottom: 5px;
}
.progress-circle {
transform: rotate(-90deg);
width: 100%;
height: 100%;
}
.progress-bg {
opacity: 0.4;
stroke: #E2F5FF;
}
.progress-fill {
transition: stroke-dashoffset 0.8s ease-in-out;
}
.circle-content {
position: absolute;
top: 58%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.progress-icon {
/* margin-bottom: 4px; */
}
.progress-label {
margin-top: -5px;
font-size: 11px;
color: #999;
font-weight: 400;
margin-bottom: 8px;
}
.progress-percentage {
font-size: 14px;
font-weight: 400;
color: #999;
margin-top: 0;
}
/* 总体进度条 */
.overall-progress {
margin-top: 0;
}
.progress-bar-container {
margin-bottom: 6px;
}
.progress-bar {
width: 100%;
height: 8px;
background: #E2F5FF;
border-radius: 4px;
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
background: #078BD2;
border-radius: 4px;
transition: width 0.8s ease-in-out;
}
.progress-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.progress-text {
display: flex;
align-items: center;
gap: 6px;
}
.progress-title {
font-size: 14px;
color: #999;
font-weight: 400;
}
.progress-value {
font-size: 14px;
color: #999;
font-weight: 400;
}
.progress-count {
display: flex;
align-items: center;
font-size: 14px;
font-weight: 400;
}
.current {
color: #333;
font-size: 14px;
}
.separator {
color: #999;
margin: 0 2px;
font-size: 14px;
}
.total {
color: #999;
font-size: 14px;
}
/* 状态指示器 */
.status-indicator {
background: white;
border-radius: 8px;
padding: 16px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.status-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.status-item:last-child {
margin-bottom: 0;
}
.status-label {
font-size: 14px;
color: #666;
}
.status-value {
font-size: 14px;
font-weight: 600;
padding: 2px 8px;
border-radius: 4px;
}
.status-success {
background: #f6ffed;
color: #52c41a;
border: 1px solid #b7eb8f;
}
.status-error {
background: #fff2f0;
color: #ff4d4f;
border: 1px solid #ffb3b3;
}
/* 视频播放器区域 */
.video-player-section {
position: relative;
background: #fff;
overflow: hidden;
margin-bottom: 20px;
}
.video-player.unregistered {
height: 578px;
position: relative;
}
.video-background {
width: 100%;
height: 100%;
position: relative;
/* 背景图片设置 */
background-size: cover;
background-position: center;
background-repeat: no-repeat;
/* 如果没有背景图片,使用默认渐变背景 */
background-image:
radial-gradient(ellipse at 30% 40%, rgba(59, 130, 246, 0.4) 0%, transparent 50%),
radial-gradient(ellipse at 70% 60%, rgba(34, 197, 94, 0.3) 0%, transparent 50%),
radial-gradient(ellipse at 50% 80%, rgba(168, 85, 247, 0.2) 0%, transparent 50%),
linear-gradient(135deg, #1e3a8a 0%, #1e40af 30%, #1d4ed8 60%, #2563eb 100%);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.video-background::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image: url('/images/aiCompanion/bg.png');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
.video-background::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background:
radial-gradient(ellipse 800px 600px at 40% 50%, rgba(59, 130, 246, 0.08) 0%, transparent 70%),
radial-gradient(ellipse 600px 400px at 60% 30%, rgba(34, 197, 94, 0.06) 0%, transparent 70%);
pointer-events: none;
}
/* 按钮容器 - 中间偏下位置 */
.video-buttons-container {
position: absolute;
bottom: 20%;
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
z-index: 10;
}
.video-content {
position: relative;
z-index: 2;
text-align: center;
color: white;
padding: 42px 20px;
max-width: 800px;
width: 100%;
}
.course-main-title {
font-size: 26px;
font-weight: 500;
margin-bottom: 14px;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
line-height: 1.3;
color: #ffffff;
letter-spacing: 0.5px;
}
/* 课程统计信息样式 */
.course-stats-info {
font-size: 15px;
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
gap: 12px;
}
.course-stats-info .stats-item {
color: #999999;
display: flex;
align-items: center;
gap: 6px;
font-weight: 400;
}
.course-stats-info .stats-separator {
color: rgba(255, 255, 255, 0.5);
margin: 0 16px;
}
/* 图标透明度调整 */
.course-stats-info .icon-chapters,
.course-stats-info .icon-duration {
opacity: 0.9;
}
.course-meta-info .meta-separator {
margin: 0 12px;
color: rgba(255, 255, 255, 0.6);
white-space: nowrap;
}
.enroll-button {
background: #1890ff;
color: white;
border: none;
padding: 0;
border-radius: 4px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
width: 112px;
height: 42px;
letter-spacing: 0.2px;
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.3);
text-align: center;
line-height: 42px;
}
.enroll-button:hover {
background: #40a9ff;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.4);
}
.not-started-button {
background: #8c8c8c;
color: white;
border: none;
padding: 0;
border-radius: 4px;
font-size: 13px;
font-weight: 500;
cursor: default;
transition: all 0.3s ease;
width: 112px;
height: 42px;
letter-spacing: 0.2px;
box-shadow: 0 2px 8px rgba(140, 140, 140, 0.3);
text-align: center;
line-height: 42px;
}
.not-started-button:hover {
background: #595959;
}
/* 底部交互区域 */
.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: 2px;
background: #f3f3f3;
}
/* 交互按钮图标样式已在各自的图标类中定义 */
.interaction-right {
flex: 1;
max-width: 650px;
margin-left: 15px;
}
.comment-input {
position: relative;
display: flex;
align-items: center;
width: 100%;
}
.comment-input input {
width: 100%;
padding: 12px 80px 12px 20px;
border: 1px solid #F1F1F1;
border-radius: 10px;
font-size: 14px;
background: #F1F1F1;
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: #9A9A9A;
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-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;
}
.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;
}
.icon-like {
width: 16px;
height: 16px;
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: 16px;
height: 16px;
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-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;
}
.share-text {
margin-right: 35px;
}
.icon-note {
width: 18px !important;
height: 18px !important;
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/底部交互区3.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;
}
/* 课程信息区域 */
.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,
.icon-duration,
.icon-note {
width: 14px;
height: 14px;
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;
}
.safe-avatar {
width: 100%;
height: 100%;
border-radius: 50%;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background-color: #f0f0f0;
}
.safe-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 50%;
}
.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;
}
/* 右侧边栏课程章节 */
.sidebar .course-sections {
border-radius: 5px;
padding: 0;
overflow: hidden;
margin-bottom: 20px;
}
.sections-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 0 10px 0;
/* 透明 */
background: transparent;
}
.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;
}
.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;
}
.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: #1890ff;
color: white;
}
.badge-resource {
background: #f5f5f5;
color: #666;
}
.badge-homework {
background: #1890ff;
color: white;
}
.badge-exam {
background: #1890ff;
color: white;
}
/* 课时操作按钮样式 */
.lesson-action-btn {
background: none;
border: none;
cursor: pointer;
padding: 4px;
border-radius: 4px;
transition: background-color 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.lesson-action-btn:hover {
background: #f0f0f0;
}
.video-btn svg,
.video-btn img {
color: #1890ff;
display: inline-block;
vertical-align: middle;
}
.download-btn svg,
.download-btn img {
color: #52c41a;
display: inline-block;
vertical-align: middle;
}
.edit-btn svg,
.edit-btn img {
color: #1890ff;
display: inline-block;
vertical-align: middle;
}
.exam-btn svg,
.exam-btn img {
color: #1890ff;
display: inline-block;
vertical-align: middle;
}
/* 完成状态图标 */
.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;
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;
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: 10px;
}
.enroll-section {
border-radius: 8px;
text-align: center;
margin-top: -10px;
}
.btn-enroll {
width: 100%;
background: #E3F6FF;
color: #75AEC4;
border: none;
padding: 12px 24px;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
border: 1px solid #377E9F;
}
.btn-enroll:hover {
background: #40a9ff;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
color: white;
}
.btn-course-status {
width: 100%;
background: #EFEFEF;
color: #A1B2B2;
border: 1px solid #6c6a6a;
padding: 12px 24px;
border-radius: 4px;
font-size: 14px;
font-weight: 400;
cursor: not-allowed;
margin-bottom: 12px;
transition: all 0.3s;
}
.btn-course-status:disabled {
background: #EFEFEF;
color: #A1B2B2;
border: 1px solid #9FA6A6;
}
/* 更多课程 */
.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: 1px;
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;
}
/* 这些图标样式已被替换为背景图片 */
.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);
}
.instructors-section,
.course-description,
.course-header {
padding-left: 24px;
padding-right: 24px;
}
/* 响应式设计 */
@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;
}
.video-player.unregistered {
height: 400px;
}
.course-main-title {
font-size: 24px;
margin-bottom: 16px;
}
.video-content {
padding: 20px 16px;
}
.course-stats-info {
flex-direction: column;
gap: 8px;
margin-bottom: 24px;
}
.course-stats-info .stats-separator {
display: none;
}
.video-interaction-bar {
flex-direction: column;
gap: 16px;
align-items: stretch;
}
.interaction-left {
justify-content: center;
}
.interaction-right {
margin-left: 0;
max-width: none;
}
.comment-input {
flex-direction: column;
gap: 12px;
}
.comment-input input {
width: 100%;
}
.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;
}
/* 进度图表响应式 */
.progress-circles {
gap: 24px;
margin-bottom: 20px;
}
.circle-container {
width: 70px;
height: 70px;
}
.progress-circle {
width: 70px;
height: 70px;
}
.progress-label {
font-size: 12px;
}
.progress-percentage {
font-size: 16px;
}
.progress-title,
.progress-value {
font-size: 14px;
}
.progress-count {
font-size: 16px;
}
}
/* 报名弹窗样式 */
.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;
}
.modal-content {
background: white;
border-radius: 8px;
padding: 0;
max-width: 400px;
width: 90%;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 24px;
border-bottom: 1px solid #f0f0f0;
}
.modal-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
.modal-close {
background: none;
border: none;
font-size: 24px;
color: #999;
cursor: pointer;
padding: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.modal-close:hover {
color: #666;
}
.modal-body {
padding: 24px;
}
.modal-body p {
margin: 0 0 12px 0;
color: #333;
line-height: 1.5;
}
.modal-tip {
color: #666;
font-size: 14px;
}
.modal-footer {
display: flex;
gap: 12px;
padding: 20px 24px;
border-top: 1px solid #f0f0f0;
justify-content: flex-end;
}
.btn-cancel {
background: #f5f5f5;
border: 1px solid #d9d9d9;
color: #666;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
}
.btn-cancel:hover {
background: #e6e6e6;
border-color: #bfbfbf;
}
.btn-confirm {
background: #1890ff;
border: 1px solid #1890ff;
color: white;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
}
.btn-confirm:hover:not(:disabled) {
background: #40a9ff;
border-color: #40a9ff;
}
.btn-confirm:disabled {
background: #f5f5f5;
border-color: #d9d9d9;
color: #bfbfbf;
cursor: not-allowed;
}
.success-modal {
text-align: center;
padding: 40px 24px;
}
.success-icon {
width: 60px;
height: 60px;
background: #52c41a;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 20px;
font-size: 30px;
color: white;
font-weight: bold;
}
.success-modal h3 {
margin: 0 0 12px 0;
color: #333;
font-size: 18px;
}
.success-modal p {
margin: 0 0 8px 0;
color: #666;
font-size: 14px;
}
.success-modal p:last-child {
margin-bottom: 0;
}
.success-tip {
color: #52c41a !important;
font-weight: 500;
}
/* 手机小屏优化 */
@media (max-width: 480px) {
.container {
padding: 0 12px;
}
.video-player.unregistered {
height: 350px;
}
.course-main-title {
font-size: 20px;
margin-bottom: 12px;
}
.video-content {
padding: 16px 12px;
}
.interaction-left {
gap: 12px;
}
.interaction-btn {
font-size: 12px;
padding: 6px 4px;
}
.video-interaction-bar {
padding: 12px 16px;
gap: 12px;
}
/* 进度图表小屏响应式 */
.progress-circles {
gap: 16px;
margin-bottom: 16px;
}
.circle-container {
width: 60px;
height: 60px;
}
.progress-circle {
width: 60px;
height: 60px;
}
.progress-percentage {
font-size: 14px;
}
.progress-count {
font-size: 14px;
}
}
/* 超小屏幕优化 */
@media (max-width: 360px) {
.container {
padding: 0 8px;
}
.video-player.unregistered {
height: 300px;
}
.course-main-title {
font-size: 18px;
}
.interaction-btn {
font-size: 11px;
padding: 4px 2px;
}
.interaction-left {
gap: 8px;
}
}
/* 智点兑换界面样式 */
.lock-icon {
text-align: center;
margin-bottom: 24px;
}
.lock-icon img {
width: 30px;
height: 40px;
}
.wisdom-points-info {
text-align: center;
margin-bottom: 32px;
}
.main-message {
color: white;
font-size: 16px;
font-weight: 600;
margin-bottom: 20px;
line-height: 1.4;
}
.exchange-button-container {
text-align: center;
margin-bottom: 24px;
}
.exchange-button {
background: transparent;
border: 1px solid #FFE2B0;
color: #FFE2B0;
padding: 10px 20px;
border-radius: 6px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
}
.exchange-button:hover {
background: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.5);
}
.user-points-info {
text-align: center;
}
.points-display {
color: white;
font-size: 16px;
margin-bottom: 8px;
opacity: 0.9;
}
.get-more-points {
color: white;
font-size: 16px;
cursor: pointer;
}
.get-more-points:hover {
color: #81D4FA;
}
/* 横幅标题区域样式 */
.banner-title-section {
width: 100%;
margin-bottom: 10px;
}
.banner-content {
display: flex;
justify-content: space-between;
align-items: center;
color: #000;
}
.banner-text {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
}
.main-text {
color: #000;
font-size: 20px;
font-weight: 600;
}
.ai-companion-tag {
width: 64px;
height: 20px;
display: flex;
align-items: center;
}
.tag-image {
height: 100%;
width: auto;
}
.tag-text {
color: white;
font-size: 12px;
font-weight: 500;
}
.banner-button {
display: flex;
justify-content: center;
align-items: center;
gap: 6px;
background: transparent;
border: 1px solid #0088D1;
width: 70px;
height: 24px;
cursor: pointer;
}
.button-icon {
color: white;
font-size: 13px;
font-weight: bold;
}
.button-icon-image {
height: 8px;
width: auto;
}
.button-text {
color: #0088D1;
font-size: 12px;
font-weight: 500;
}
</style>