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

6494 lines
154 KiB
Vue
Raw Normal View History

2025-08-16 19:51:39 +08:00
<template>
<div class="course-detail-page">
2025-08-18 12:03:30 +08:00
<!-- 刷新遮罩 -->
<div v-if="isRefreshing" class="refresh-mask">
<div class="refresh-content">
<div class="refresh-spinner"></div>
<p>正在刷新...</p>
</div>
</div>
2025-08-16 19:51:39 +08:00
<!-- 主要内容区域 -->
<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>
</div>
<!-- 提示 -->
<div class="tip-section">
<img src="/images/aiCompanion/ii.jpg" alt="">
<span>此视频请在2025.10.23 23:59前完成学习快进拖拽或逾期学习不计入观看进度和成绩</span>
<div class="tip-section-box">
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 1L9 9M9 1L1 9" stroke="#999999" stroke-width="1.5" stroke-linecap="round" />
</svg>
</div>
</div>
<!-- 视频播放器区域 - 已兑换状态 -->
<div class="video-player-section">
<div class="video-player enrolled">
<div class="video-container">
<!-- DPlayer 播放器 -->
<DPlayerVideo
v-if="currentVideoUrl"
ref="videoPlayerRef"
:video-url="currentVideoUrl"
:poster="course?.coverImage || course?.thumbnail"
:title="currentVideoSection?.name || '课程视频'"
:autoplay="false"
:video-qualities="videoQualities"
:current-quality="currentQuality"
@play="onVideoPlay"
@pause="onVideoPause"
@ended="onVideoEnded"
@error="onVideoError"
@screenshot="onScreenshot"
@danmaku-send="onDanmakuSend"
2025-08-29 18:28:49 +08:00
@qualityChange="onQualityChange"
/>
2025-08-16 19:51:39 +08:00
<div v-else class="video-placeholder"
:style="{ backgroundImage: course?.coverImage || course?.thumbnail ? `url(${course.coverImage || course.thumbnail})` : '' }">
<div class="placeholder-content">
<div class="play-icon">
<svg width="60" height="60" viewBox="0 0 60 60">
<circle cx="30" cy="30" r="30" fill="rgba(255,255,255,0.9)" />
<path d="M23 18l20 12-20 12V18z" fill="#1890ff" />
</svg>
</div>
<p>请选择要播放的视频课程</p>
</div>
</div>
</div>
</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-description">
<div>本课程中的部分图片音频和视频素材来源于网络仅供教学使用如有问题请点击 <span @click="openComplaintModal('feedback')">这里</span> 反馈</div>
<span @click="openComplaintModal('complaint')">稿件投诉</span>
2025-08-16 19:51:39 +08:00
</div>
<!-- 分隔线 -->
<div class="course-info-divider"></div>
<!-- 课程标签页 -->
<div class="course-tabs">
<div class="tab-nav">
<button class="tab-btn" :class="{ active: courseActiveTab === 'summary' }"
@click="courseActiveTab = 'summary'">课程总结</button>
<button class="tab-btn" :class="{ active: courseActiveTab === 'subtitles' }"
@click="courseActiveTab = 'subtitles'">字幕列表</button>
<button class="tab-btn" :class="{ active: courseActiveTab === 'comments' }"
2025-08-30 03:57:24 +08:00
@click="courseActiveTab = 'comments'">评论({{ commentsCount }})</button>
2025-08-16 19:51:39 +08:00
</div>
<!-- 标签页内容区域 -->
<div class="tab-content">
<!-- 课程总结内容 -->
<div v-if="courseActiveTab === 'summary'" class="tab-pane">
<div class="summary-content">
<div class="summary-item">
<div class="summary-header">
<span class="timestamp">00:23</span>
<div class="timestamp-icon"><span></span></div>
<h3 class="summary-title">职业探索与选择:追求卓越与实现自我价值</h3>
</div>
2025-08-18 12:03:30 +08:00
<p class="summary-description">
本次课程旨在引导学生探索自身的职业目标及价值观,强调了根据个人兴趣和优势做出职业选择的重要性通过分享不同领域的职场榜样,如一位对C罗的远见和执行力表示赞赏的学生另一位崇拜Elon
Musk的学习能力和创新精神以及第三位钦佩俞敏洪的社会洞察力和团队合作精神,课程鼓励学生思考并追求自己理想中的职业生涯这些榜样人物不仅在各自领域取得了巨大成功,而且还展现了持续学习勇于创新和积极影响社会的价值观
</p>
2025-08-16 19:51:39 +08:00
</div>
<div class="summary-item">
<div class="summary-header">
<span class="timestamp">00:45</span>
<div class="timestamp-icon"><span></span></div>
<h3 class="summary-title">职业探索与选择:追求卓越与实现自我价值</h3>
</div>
2025-08-18 12:03:30 +08:00
<p class="summary-description">
本次课程旨在引导学生探索自身的职业目标及价值观,强调了根据个人兴趣和优势做出职业选择的重要性通过分享不同领域的职场榜样,如一位对C罗的远见和执行力表示赞赏的学生另一位崇拜Elon
Musk的学习能力和创新精神以及第三位钦佩俞敏洪的社会洞察力和团队合作精神,课程鼓励学生思考并追求自己理想中的职业生涯这些榜样人物不仅在各自领域取得了巨大成功,而且还展现了持续学习勇于创新和积极影响社会的价值观
</p>
2025-08-16 19:51:39 +08:00
</div>
<div class="summary-item">
<div class="summary-header">
<span class="timestamp">01:12</span>
<div class="timestamp-icon"><span></span></div>
<h3 class="summary-title">职业探索与选择:追求卓越与实现自我价值</h3>
</div>
2025-08-18 12:03:30 +08:00
<p class="summary-description">
本次课程旨在引导学生探索自身的职业目标及价值观,强调了根据个人兴趣和优势做出职业选择的重要性通过分享不同领域的职场榜样,如一位对C罗的远见和执行力表示赞赏的学生另一位崇拜Elon
Musk的学习能力和创新精神以及第三位钦佩俞敏洪的社会洞察力和团队合作精神,课程鼓励学生思考并追求自己理想中的职业生涯这些榜样人物不仅在各自领域取得了巨大成功,而且还展现了持续学习勇于创新和积极影响社会的价值观
</p>
2025-08-16 19:51:39 +08:00
</div>
<div class="summary-item">
<div class="summary-header">
<span class="timestamp">01:35</span>
<div class="timestamp-icon"><span></span></div>
<h3 class="summary-title">职业探索与选择:追求卓越与实现自我价值</h3>
</div>
2025-08-18 12:03:30 +08:00
<p class="summary-description">
本次课程旨在引导学生探索自身的职业目标及价值观,强调了根据个人兴趣和优势做出职业选择的重要性通过分享不同领域的职场榜样,如一位对C罗的远见和执行力表示赞赏的学生另一位崇拜Elon
Musk的学习能力和创新精神以及第三位钦佩俞敏洪的社会洞察力和团队合作精神,课程鼓励学生思考并追求自己理想中的职业生涯这些榜样人物不仅在各自领域取得了巨大成功,而且还展现了持续学习勇于创新和积极影响社会的价值观
</p>
2025-08-16 19:51:39 +08:00
</div>
</div>
</div>
<!-- 字幕列表内容 -->
<div v-if="courseActiveTab === 'subtitles'" class="tab-pane">
<div class="subtitles-content">
<div class="subtitle-item">
<span class="subtitle-time">00:00</span>
<span class="subtitle-text">欢迎来到本课程</span>
</div>
<div class="subtitle-item">
<span class="subtitle-time">00:05</span>
<span class="subtitle-text">今天我们将学习职业探索与选择</span>
</div>
<div class="subtitle-item">
<span class="subtitle-time">00:10</span>
<span class="subtitle-text">首先让我们了解一下课程的目标</span>
</div>
<div class="subtitle-item">
<span class="subtitle-time">00:15</span>
<span class="subtitle-text">通过分享不同领域的职场榜样</span>
</div>
<div class="subtitle-item">
<span class="subtitle-time">00:20</span>
<span class="subtitle-text">我们将学习如何追求卓越与实现自我价值</span>
</div>
</div>
</div>
<!-- 评论内容 -->
<div v-if="courseActiveTab === 'comments'" class="tab-pane">
<div class="comments-content">
2025-08-18 12:03:30 +08:00
<!-- 发布评论区域 -->
<div class="post-comment-section">
<div class="comment-input-wrapper">
<div class="user-avatar">
<img src="/images/activity/6.png" alt="用户头像" />
</div>
<div class="comment-input-area">
<textarea v-model="newComment" placeholder="写下你的评论..." class="comment-textarea"
@input="adjustTextareaHeight" @click="handleTextareaClick"></textarea>
<div class="comment-toolbar">
<div class="toolbar-left">
<button class="toolbar-btn">
<img src="/images/courses/expression.png" alt="表情" class="toolbar-icon" />
</button>
<button class="toolbar-btn">
<img src="/images/courses/@.png" alt="@用户" class="toolbar-icon" />
</button>
<button class="toolbar-btn">
<img src="/images/courses/Image.png" alt="图片" class="toolbar-icon" />
</button>
</div>
<div class="toolbar-right">
<button class="btn-submit" @click="submitComment">
发布
</button>
</div>
</div>
</div>
2025-08-16 19:51:39 +08:00
</div>
2025-08-18 12:03:30 +08:00
</div>
2025-08-16 19:51:39 +08:00
<div class="comment-list">
2025-08-30 03:57:24 +08:00
<!-- 加载状态 -->
<div v-if="commentsLoading" class="comments-loading">
<p>正在加载评论...</p>
</div>
<!-- 错误状态 -->
<div v-else-if="commentsError" class="comments-error">
<p>{{ commentsError }}</p>
<button @click="loadCourseComments" class="retry-btn">重试</button>
</div>
<!-- 无评论状态 -->
<div v-else-if="displayComments.length === 0" class="no-comments">
<p>暂无评论快来发表第一条评论吧</p>
</div>
<!-- 评论列表 -->
<div v-else class="comment-item" v-for="comment in displayComments" :key="comment.id">
2025-08-16 19:51:39 +08:00
<div class="comment-avatar">
2025-08-30 03:57:24 +08:00
<SafeAvatar :src="comment.avatar" :name="comment.username" :size="40" />
2025-08-16 19:51:39 +08:00
</div>
<div class="comment-content">
<div class="comment-header">
<span class="comment-username">{{ comment.username }}</span>
</div>
<div class="comment-text">{{ comment.content }}</div>
<div class="comment-actions">
<button class="action-btn">
<span>2025.07.23 16:28</span>
</button>
2025-08-18 12:03:30 +08:00
<button v-if="comment.type === 'note'" class="action-btn">
<span class="top">置顶评论</span>
</button>
<button v-else class="action-btn"
@click="startReply(comment.id, comment.username)">回复</button>
</div>
<!-- 回复输入区域 -->
<div v-if="replyingTo === comment.id" class="reply-input-section">
<div class="reply-input-header">
<span class="reply-to-text">回复 @{{ replyToUsername }}</span>
<button class="cancel-reply-btn" @click="cancelReply">取消</button>
</div>
<div class="reply-input-container">
<textarea v-model="replyText" placeholder="写下你的回复..." class="reply-textarea"
@input="adjustReplyTextareaHeight" @click="handleReplyTextareaClick"></textarea>
<div class="reply-toolbar">
<div class="toolbar-left">
<button class="toolbar-btn">
<img src="/images/courses/expression.png" alt="表情" class="toolbar-icon" />
</button>
<button class="toolbar-btn">
<img src="/images/courses/@.png" alt="@用户" class="toolbar-icon" />
</button>
<button class="toolbar-btn">
<img src="/images/courses/Image.png" alt="图片" class="toolbar-icon" />
</button>
</div>
<div class="toolbar-right">
<button class="btn-submit" @click="submitReply" :disabled="!replyText.trim()">
发布
</button>
</div>
</div>
</div>
</div>
<!-- 回复区域示例 -->
2025-08-30 14:05:01 +08:00
<div class="comment-replies" v-if="String(comment.id) === '1'">
2025-08-18 12:03:30 +08:00
<!-- 讲师回复 -->
<div class="reply-item instructor-reply">
<div class="reply-avatar">
<img src="/images/activity/6.png" alt="讲师头像" />
</div>
<div class="reply-content">
<div class="reply-main">
<div class="reply-header">
<span class="reply-username">张老师</span>
<span class="reply-badge instructor">讲师</span>
</div>
<div class="reply-text">感谢您的反馈我们会继续优化课程内容让学习体验更好</div>
</div>
<div class="reply-footer">
<span class="reply-time">2025.07.23 17:30</span>
<div class="reply-actions">
<button class="reply-action-btn" @click="startReply(1, '张老师')">回复</button>
</div>
</div>
</div>
</div>
<!-- 用户回复 -->
<div class="reply-item user-reply">
<div class="reply-avatar">
<img src="/images/activity/7.png" alt="用户头像" />
</div>
<div class="reply-content">
<div class="reply-main">
<div class="reply-header">
<span class="reply-username">李同学</span>
<span class="reply-badge user">学员</span>
</div>
<div class="reply-text">同意楼上的观点这个课程确实很有帮助</div>
</div>
<div class="reply-footer">
<span class="reply-time">2025.07.23 18:15</span>
<div class="reply-actions">
<button class="reply-action-btn" @click="startReply(1, '李同学')">回复</button>
</div>
</div>
</div>
</div>
2025-08-16 19:51:39 +08:00
</div>
</div>
</div>
2025-08-18 12:03:30 +08:00
<div class="comment-item">
<div class="comment-avatar">
<img
src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-4.0.3&auto=format&fit=crop&w=50&q=80"
alt="张老师" />
</div>
<div class="comment-content">
<div class="comment-header">
<span class="comment-username">张老师</span>
</div>
<div class="comment-text">这个课程内容很实用讲解得很清楚对初学者很有帮助111</div>
<div class="comment-image-container">
<img src="/images/courses/course1.png" alt="课程图片" class="comment-image">
<img src="/images/courses/course1.png" alt="课程图片" class="comment-image">
<img src="/images/courses/course1.png" alt="课程图片" class="comment-image">
<img src="/images/courses/course1.png" alt="课程图片" class="comment-image">
<img src="/images/courses/course1.png" alt="课程图片" class="comment-image">
<img src="/images/courses/course1.png" alt="课程图片" class="comment-image">
<div class="image-overlay">
<span class="more-images-text">+6</span>
</div>
</div>
<div class="comment-actions">
<div class="note-icon-container">
<img src="/images/courses/comments-note.png" alt="笔记" class="note-icon">
<span>笔记</span>
</div>
<span class="comment-time">2025.07.23 16:28</span>
<button class="action-btn" @click="startReply(1, '张老师')">回复</button>
</div>
<!-- 回复输入区域 -->
<div v-if="replyingTo === 1" class="reply-input-section">
<div class="reply-input-header">
<span class="reply-to-text">回复 @{{ replyToUsername }}</span>
<button class="cancel-reply-btn" @click="cancelReply">取消</button>
</div>
<div class="reply-input-container">
<textarea v-model="replyText" placeholder="写下你的回复..." class="reply-textarea"
@input="adjustReplyTextareaHeight" @click="handleReplyTextareaClick"></textarea>
<div class="reply-toolbar">
<div class="toolbar-left">
<button class="toolbar-btn">
<img src="/images/courses/expression.png" alt="表情" class="toolbar-icon" />
</button>
<button class="toolbar-btn">
<img src="/images/courses/@.png" alt="@用户" class="toolbar-icon" />
</button>
<button class="toolbar-btn">
<img src="/images/courses/Image.png" alt="图片" class="toolbar-icon" />
</button>
</div>
<div class="toolbar-right">
<button class="btn-submit" @click="submitReply" :disabled="!replyText.trim()">
发布
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
2025-08-16 19:51:39 +08:00
</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">
<!-- 按章节分组显示 - 已报名状态彩色可点击 -->
2025-08-16 19:51:39 +08:00
<div v-for="(chapter, chapterIndex) in groupedSections" :key="chapterIndex" class="chapter-section">
<div class="chapter-header" @click="toggleChapter(chapterIndex)">
<div class="chapter-info">
<span class="chapter-title">{{ getChapterNumber(chapterIndex + 1) }} {{ chapter.title
}}</span>
2025-08-16 19:51:39 +08:00
</div>
<span class="chapter-toggle" :class="{ 'expanded': chapter.expanded }">
<svg width="12" height="12" viewBox="0 0 12 12">
<path d="M4 3l4 3-4 3" stroke="currentColor" stroke-width="1.5" fill="none" />
</svg>
</span>
</div>
<div v-if="chapter.expanded" class="chapter-lessons">
<div v-for="section in chapter.sections" :key="section.id" class="lesson-item">
<!-- 已报名状态彩色可点击 -->
<div class="lesson-content enrolled" @click="handleSectionClick(section)">
<div class="lesson-type-badge" :class="getLessonTypeBadgeClass(section)">
2025-08-16 19:51:39 +08:00
{{ getLessonTypeText(section) }}
</div>
<div class="lesson-info">
<span class="lesson-title">{{ section.name }}</span>
2025-08-16 19:51:39 +08:00
</div>
<div class="lesson-meta">
<span v-if="isVideoLesson(section)" class="lesson-duration">{{
formatLessonDuration(section) }}</span>
2025-08-16 19:51:39 +08:00
<div class="lesson-actions">
<!-- 视频播放图标 - 可点击 -->
2025-08-16 19:51:39 +08:00
<button v-if="isVideoLesson(section)" class="lesson-action-btn video-btn"
@click.stop="handleVideoPlay(section)">
<img src="/public/images/courses/video-enroll.png" alt="视频" width="14" height="14">
2025-08-16 19:51:39 +08:00
</button>
<!-- 下载图标 - 可点击 -->
2025-08-16 19:51:39 +08:00
<button v-else-if="isResourceLesson(section)" class="lesson-action-btn download-btn"
@click.stop="handleDownload(section)">
<img src="/public/images/courses/download-enroll.png" alt="资料" width="14" height="14">
2025-08-16 19:51:39 +08:00
</button>
<!-- 编辑图标作业 - 可点击 -->
2025-08-16 19:51:39 +08:00
<button v-else-if="isHomeworkLesson(section)" class="lesson-action-btn edit-btn"
@click.stop="handleHomework(section)">
<img src="/public/images/courses/homework-enroll.png" alt="作业" width="14" height="14">
2025-08-16 19:51:39 +08:00
</button>
<!-- 考试图标 - 可点击 -->
2025-08-16 19:51:39 +08:00
<button v-else-if="isExamLesson(section)" class="lesson-action-btn exam-btn"
@click.stop="handleExam(section)">
<img src="/public/images/courses/examination-enroll.png" alt="考试" width="14"
height="14">
2025-08-16 19:51:39 +08:00
</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 class="ai-assistant-interface">
<!-- <div class="banner-button">
2025-08-16 19:51:39 +08:00
<img src="/images/aiCompanion/切换@2x.png" alt="切换" class="button-icon-image">
<span class="button-text">普通</span>
</div> -->
2025-08-16 19:51:39 +08:00
<!-- AI主要内容区域 -->
<div class="ai-main-content">
<!-- AI头部栏 -->
<div class="ai-header-bar">
<div class="ai-header-left">
<img src="/images/aiCompanion/AI小助手@2x.png" alt="AI小助手" class="ai-avatar">
<h3 class="ai-title">AI小助手</h3>
</div>
<button class="save-button">
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 1L9 9M9 1L1 9" stroke="#999999" stroke-width="1.5" stroke-linecap="round" />
</svg>
</button>
</div>
<!-- 切换笔记和助手 -->
<div class="ai-tab">
<div class="ai-tab-item" :class="{ active: aiActiveTab === 'assistant' }"
@click="switchTab('assistant')">
AI小助手
</div>
<div class="ai-tab-item" :class="{ active: aiActiveTab === 'notes' }" @click="switchTab('notes')">
我的笔记
</div>
</div>
<!-- AI工具栏 -->
<div v-if="aiActiveTab !== 'assistant'" class="ai-toolbar">
</div>
<!-- AI内容区域 -->
<div class="ai-content-area">
<!-- AI小助手聊天界面 -->
<div v-if="aiActiveTab === 'assistant'" class="ai-chat-interface">
<!-- 聊天消息列表 -->
<div class="chat-messages">
<!-- AI欢迎消息 -->
<div class="message ai-message">
<div class="message-avatar">
<img src="/images/aiCompanion/AI小助手@2x.png" alt="AI小助手">
</div>
<div class="message-content">
<div class="message-bubble">
<p>您好我是您的AI学习助手很高兴为您服务</p>
<p>我可以帮助您</p>
<ul>
<li>解答课程相关问题</li>
<li>提供学习建议</li>
<li>协助完成作业</li>
<li>生成学习笔记</li>
</ul>
<p>请随时向我提问</p>
</div>
<div class="message-time">刚刚</div>
</div>
</div>
<!-- 用户消息示例 -->
<div class="message user-message">
<div class="message-avatar">
<img
src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-4.0.3&auto=format&fit=crop&w=50&q=80"
alt="用户">
</div>
<div class="message-content">
<div class="message-bubble">
<p>你好我想了解一下这个课程的主要内容</p>
</div>
<div class="message-time">2分钟前</div>
</div>
</div>
<!-- AI回复消息 -->
<div class="message ai-message">
<div class="message-avatar">
<img src="/images/aiCompanion/AI小助手@2x.png" alt="AI小助手">
</div>
<div class="message-content">
<div class="message-bubble">
<p>这个课程主要包含以下几个部分</p>
<ol>
<li><strong>DeepSeek大语言模型介绍</strong> - 了解模型的基本原理和特点</li>
<li><strong>模型应用场景分析</strong> - 学习如何在实际工作中应用</li>
<li><strong>实战应用技巧</strong> - 掌握具体的操作方法和技巧</li>
</ol>
<p>课程总时长约12小时包含54节课程内容您想从哪个部分开始学习呢</p>
</div>
<div class="message-time">1分钟前</div>
</div>
</div>
2025-08-18 12:03:30 +08:00
<div class="ai-suggestion">
<div class="ai-suggestion-title">你可以尝试与AI进行以下对话:</div>
<div class="ai-suggestion-item">
<div class="ai-suggestion-header">
<img src="/images/aiCompanion/做作业@2x.png" alt="做作业" class="ai-icon" />
<span class="ai-category">做作业</span>
</div>
<div class="ai-prompt-bubble" @click="sendPrompt('解决这个数学问题: 2^(4)+3^(3)')">
解决这个数学问题: 2^(4)+3^(3)
</div>
</div>
<div class="ai-suggestion-item">
<div class="ai-suggestion-header">
<img src="/images/aiCompanion/智能问答@2x.png" alt="智能问答" class="ai-icon" />
<span class="ai-category">智能问答</span>
</div>
<div class="ai-prompt-bubble" @click="sendPrompt('我想研究量子力学,可以从哪些方面入手?')">
我想研究量子力学,可以从哪些方面入手?
</div>
</div>
<div class="ai-suggestion-item">
<div class="ai-suggestion-header">
<img src="/images/aiCompanion/随便聊聊@2x.png" alt="随便聊聊" class="ai-icon" />
<span class="ai-category">随便聊聊</span>
</div>
<div class="ai-prompt-bubble" @click="sendPrompt('帮我提炼本节课程的核心知识点')">
帮我提炼本节课程的核心知识点
</div>
</div>
</div>
2025-08-16 19:51:39 +08:00
</div>
<!-- 工具栏 -->
<div class="toolbar">
<img src="/images/aiCompanion/删除@2x.png" alt="工具栏">
<img src="/images/aiCompanion/记录@2x.png" alt="工具栏">
</div>
<!-- 聊天输入区域 -->
<div class="chat-input-area">
<div class="input-container">
<textarea type="text" v-model="chatMessage" placeholder="请输入您的问题..." class="chat-input"
@keyup.enter="sendMessage">
</textarea>
<button class="send-button" @click="sendMessage">
<img src="/images/aiCompanion/发送@2x.png" alt="发送" class="send-icon">
</button>
</div>
</div>
</div>
<!-- 我的笔记界面 -->
<div v-if="aiActiveTab === 'notes'" class="notes-interface">
<div class="notes-header">
<h4>我的笔记</h4>
2025-08-18 12:03:30 +08:00
<button class="add-note-btn" @click="showNoteEditor = true">
2025-08-16 19:51:39 +08:00
<img src="/images/aiCompanion/记录@2x.png" alt="添加笔记" class="add-icon">
<span>添加笔记</span>
</button>
</div>
2025-08-18 12:03:30 +08:00
<!-- 富文本编辑器 -->
<div v-if="showNoteEditor" class="note-editor-container">
<div class="note-editor-header">
<input v-model="noteTitle" type="text" class="note-title-input" placeholder="请输入笔记标题..." />
<div class="note-editor-actions">
<button class="save-note-btn" @click="saveNote">保存</button>
<button class="cancel-note-btn" @click="cancelNote">取消</button>
2025-08-16 19:51:39 +08:00
</div>
</div>
2025-08-18 12:03:30 +08:00
<QuillEditor v-model="noteContent" placeholder="请输入笔记内容..." height="300px" />
</div>
<!-- 笔记列表 -->
<div v-else class="notes-list">
<div class="note-item" v-for="(note, index) in notesList" :key="index">
2025-08-16 19:51:39 +08:00
<div class="note-header">
2025-08-18 12:03:30 +08:00
<span class="note-title">{{ note.title }}</span>
<span class="note-date">{{ note.date }}</span>
2025-08-16 19:51:39 +08:00
</div>
2025-08-18 12:03:30 +08:00
<div class="note-content" v-html="note.content"></div>
2025-08-16 19:51:39 +08:00
<div class="note-actions">
2025-08-18 12:03:30 +08:00
<button class="note-action-btn" @click="editNote(index)">编辑</button>
<button class="note-action-btn" @click="deleteNote(index)">删除</button>
2025-08-16 19:51:39 +08:00
</div>
</div>
</div>
</div>
</div>
<!-- AI底部按钮 -->
<div v-if="aiActiveTab !== 'assistant'" class="ai-bottom-button">
<button class="public-notes-btn">
<img src="/images/aiCompanion/笔记-蓝@2x.png" alt="公开笔记" class="notes-icon">
<span>公开笔记</span>
</button>
</div>
</div>
<!-- 知识图谱横幅 -->
<div class="knowledge-graph-banner">
<img src="/images/aiCompanion/知识图谱@2x.png" alt="知识图谱" class="graph-image">
</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>
<!-- 投诉/反馈弹窗 -->
<n-modal v-model:show="complaintModalVisible" style="width: 600px;">
<div class="complaint-modal">
<!-- 自定义标题 -->
<div class="complaint-modal-header">
<div class="complaint-modal-title">投诉/反馈</div>
<div class="complaint-modal-divider"></div>
</div>
<div class="complaint-modal-content">
<!-- 投诉内容 -->
<div class="complaint-section">
<div class="complaint-label">投诉内容</div>
<div class="complaint-input-wrapper">
<n-input
v-model:value="complaintContent"
type="textarea"
placeholder="请输入您想要投诉或反馈的内容"
:rows="8"
:maxlength="500"
show-count
class="complaint-textarea"
/>
</div>
</div>
<!-- 上传图片 -->
<div class="upload-section">
<div class="upload-label">上传图片</div>
<div class="upload-wrapper">
<n-upload
v-model:file-list="uploadFileList"
:max="5"
list-type="image-card"
accept="image/*"
:custom-request="handleUpload"
>
<div class="upload-area">
<div class="upload-plus">+</div>
</div>
</n-upload>
</div>
</div>
</div>
<!-- 底部按钮 -->
<div class="modal-actions">
<n-button @click="cancelComplaint" class="cancel-btn">取消</n-button>
<n-button type="primary" @click="submitComplaint" class="submit-btn">提交</n-button>
</div>
</div>
</n-modal>
2025-08-16 19:51:39 +08:00
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onActivated } from 'vue'
2025-08-16 19:51:39 +08:00
import { useRoute, useRouter } from 'vue-router'
import { useMessage } from 'naive-ui'
2025-08-16 19:51:39 +08:00
// import { useAuth } from '@/composables/useAuth'
2025-08-26 19:07:48 +08:00
// import { useUserStore } from '@/stores/user'
2025-08-16 19:51:39 +08:00
import { CourseApi } from '@/api/modules/course'
2025-08-30 03:57:24 +08:00
import type { Course, CourseSection, CourseComment } from '@/api/types'
2025-08-18 12:03:30 +08:00
import QuillEditor from '@/components/common/QuillEditor.vue'
import DPlayerVideo from '@/components/course/DPlayerVideo.vue'
2025-08-30 03:57:24 +08:00
import SafeAvatar from '@/components/common/SafeAvatar.vue'
2025-08-16 19:51:39 +08:00
// import LoginModal from '@/components/auth/LoginModal.vue'
// import RegisterModal from '@/components/auth/RegisterModal.vue'
const route = useRoute()
const router = useRouter()
2025-08-26 19:07:48 +08:00
// const userStore = useUserStore()
const message = useMessage()
const courseId = ref(String(route.params.id))
2025-08-16 19:51:39 +08:00
// 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()
// }
// }
// 计算用户是否已报名
2025-08-26 19:07:48 +08:00
// const isUserEnrolled = computed(() => {
// // 必须同时满足:用户已登录 AND 已报名该课程
// return userStore.isLoggedIn && isEnrolled.value
2025-08-16 19:51:39 +08:00
2025-08-26 19:07:48 +08:00
// // 临时测试不同状态:
// // return false // 强制显示未报名状态(灰色不可点击)
// // return true // 强制显示已报名状态(彩色可点击)
// })
2025-08-16 19:51:39 +08:00
// 报名确认弹窗
const enrollConfirmVisible = ref(false)
const enrollSuccessVisible = ref(false)
2025-08-18 12:03:30 +08:00
// 刷新状态
const isRefreshing = ref(false)
// AI建议相关
const sendPrompt = (prompt: string) => {
// 这里可以添加发送提示词到AI的逻辑
console.log('发送AI提示词:', prompt)
// 可以将提示词设置到输入框中
// 或者直接发送给AI处理
}
2025-08-16 19:51:39 +08:00
// 章节分组数据
interface ChapterGroup {
title: string
sections: CourseSection[]
expanded: boolean
}
const groupedSections = ref<ChapterGroup[]>([])
// 将章节按章分组 - 根据后端数据结构重新实现
2025-08-16 19:51:39 +08:00
const groupSectionsByChapter = (sections: CourseSection[]) => {
console.log('🔍 开始分组章节数据:', sections)
2025-08-16 19:51:39 +08:00
const groups: ChapterGroup[] = []
// 找出所有一级章节level=1这些是父章节
const parentChapters = sections.filter(section => section.level === 1)
console.log('🔍 找到一级章节:', parentChapters)
// 按sortOrder降序排序一级章节sortOrder越大越靠前
parentChapters.sort((a, b) => b.sort - a.sort)
// 为每个一级章节创建分组
parentChapters.forEach((parentChapter, index) => {
// 找出该章节下的所有子章节level=2parentId匹配
const childSections = sections.filter(section =>
section.level === 2 && section.parentId === parentChapter.id
)
// 按sortOrder降序排序子章节sortOrder越大越靠前
childSections.sort((a, b) => b.sort - a.sort)
console.log(`🔍 章节"${parentChapter.name}"的子章节:`, childSections)
// 创建章节分组
groups.push({
title: parentChapter.name, // 使用后端返回的章节名称
sections: childSections.length > 0 ? childSections : [parentChapter], // 如果有子章节就用子章节,否则用父章节本身
expanded: index === 0 // 默认展开第一章
})
})
// 如果没有找到一级章节,可能所有章节都是同级的,直接作为一个组
if (groups.length === 0 && sections.length > 0) {
console.log('🔍 没有找到层级结构,将所有章节作为一组')
// 按sortOrder降序排序sortOrder越大越靠前
const sortedSections = [...sections].sort((a, b) => b.sort - a.sort)
groups.push({
title: '课程章节',
sections: sortedSections,
expanded: true
})
2025-08-16 19:51:39 +08:00
}
console.log('✅ 章节分组完成:', groups)
2025-08-16 19:51:39 +08:00
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('')
// 视频播放相关
2025-08-26 19:07:48 +08:00
// const VIDEO_CONFIG = {
// // 本地视频(当前使用)
// LOCAL: '/video/first.mp4',
// // HLS流服务器准备好后使用
// HLS: 'http://110.42.96.65:55513/learn/index.m3u8'
// }
2025-08-16 19:51:39 +08:00
// 获取视频URL的函数
2025-08-26 19:07:48 +08:00
// const getVideoUrl = (section?: CourseSection) => {
// const outline = section?.outline?.trim()
// if (outline && (outline.endsWith('.mp4') || outline.endsWith('.m3u8'))) {
// return outline
// }
// // 当前使用本地视频,将来可以通过环境变量或配置切换
// return VIDEO_CONFIG.LOCAL
// }
2025-08-16 19:51:39 +08:00
// 视频播放相关状态
const currentVideoUrl = ref<string>('')
const currentVideoSection = ref<CourseSection | null>(null)
const currentVideo = ref<any>(null)
const videoQualities = ref<any[]>([])
const currentQuality = ref<string>('360')
const videoLoading = ref<boolean>(false)
const videoPlayerRef = ref<any>(null)
2025-08-16 19:51:39 +08:00
// 新增的响应式数据
const aiActiveTab = ref('assistant')
const courseActiveTab = ref('summary')
// 讲师数据
// 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}分钟`
// }
2025-08-30 03:57:24 +08:00
// 真实评论数据
const comments = ref<CourseComment[]>([])
const commentsLoading = ref(false)
const commentsError = ref('')
// 为了保持兼容性,将真实评论数据转换为显示格式
const displayComments = computed(() => {
return comments.value.map(comment => ({
id: comment.id,
username: comment.userName,
avatar: comment.userAvatar,
time: comment.timeAgo,
content: comment.content,
likes: comment.likeCount || 0,
type: comment.isTop ? 'note' : 'comment'
}))
})
// 评论数量计算属性
const commentsCount = computed(() => {
return comments.value.length
})
2025-08-16 19:51:39 +08:00
2025-08-18 12:03:30 +08:00
// 新评论内容
const newComment = ref('')
// 自动调整textarea高度
const adjustTextareaHeight = (event: Event) => {
const textarea = event.target as HTMLTextAreaElement
textarea.style.height = 'auto'
textarea.style.height = textarea.scrollHeight + 'px'
}
// 点击textarea时调整高度
const handleTextareaClick = (event: MouseEvent) => {
const textarea = event.target as HTMLTextAreaElement
// 如果当前高度是40px则调整到60px
if (textarea.style.height === '40px' || textarea.style.height === '') {
textarea.style.height = '60px'
}
}
2025-08-30 03:57:24 +08:00
// 加载课程评论列表
const loadCourseComments = async () => {
if (!courseId.value || courseId.value.trim() === '') {
commentsError.value = '课程ID无效'
console.error('课程ID无效:', courseId.value)
return
}
try {
commentsLoading.value = true
commentsError.value = ''
console.log('调用API获取课程评论...')
const response = await CourseApi.getCourseComments(courseId.value)
console.log('评论API响应:', response)
if (response.code === 0 || response.code === 200) {
if (response.data && Array.isArray(response.data)) {
// 按置顶状态和时间排序:置顶评论在前,然后按时间倒序
const sortedComments = response.data.sort((a, b) => {
// 先按置顶状态排序
if (a.isTop !== b.isTop) {
return a.isTop ? -1 : 1 // 置顶的在前
}
// 再按创建时间倒序排序
return new Date(b.createTime).getTime() - new Date(a.createTime).getTime()
})
comments.value = sortedComments
console.log('✅ 评论数据设置成功:', comments.value)
} else {
console.log('⚠️ API返回的评论数据为空')
comments.value = []
}
} else {
console.log('⚠️ API返回错误')
commentsError.value = response.message || '获取评论失败'
comments.value = []
}
} catch (err) {
console.error('加载课程评论失败:', err)
commentsError.value = '获取评论失败'
comments.value = []
} finally {
commentsLoading.value = false
}
}
2025-08-18 12:03:30 +08:00
// 提交评论函数
const submitComment = () => {
if (newComment.value.trim()) {
// 这里可以调用API提交评论
2025-08-30 03:57:24 +08:00
console.log('提交评论:', newComment.value)
// 提交成功后重新加载评论列表
// await CourseApi.submitComment(courseId.value, newComment.value)
// loadCourseComments()
newComment.value = ''
2025-08-18 12:03:30 +08:00
}
}
// 回复相关函数
2025-08-30 14:05:01 +08:00
const startReply = (commentId: string | number, username: string) => {
2025-08-18 12:03:30 +08:00
replyingTo.value = commentId
replyToUsername.value = username
replyText.value = ''
}
const cancelReply = () => {
replyingTo.value = null
replyToUsername.value = ''
replyText.value = ''
}
const submitReply = () => {
if (replyText.value.trim() && replyingTo.value) {
const newReplyObj = {
id: Date.now(),
username: '当前用户',
avatar: 'https://via.placeholder.com/40x40/1890ff/ffffff?text=我',
time: '刚刚',
content: replyText.value,
likes: 0
}
// 这里可以调用API提交回复
console.log('回复已提交:', newReplyObj)
console.log('回复给评论ID:', replyingTo.value)
console.log('回复给用户:', replyToUsername.value)
// 清空回复状态
cancelReply()
}
}
// 回复文本框高度调整
const adjustReplyTextareaHeight = (event: Event) => {
const textarea = event.target as HTMLTextAreaElement
textarea.style.height = '40px'
textarea.style.height = textarea.scrollHeight + 'px'
}
const handleReplyTextareaClick = (event: MouseEvent) => {
const textarea = event.target as HTMLTextAreaElement
if (textarea.style.height === '40px' || !textarea.style.height) {
textarea.style.height = '60px'
}
}
// 回复相关数据
2025-08-30 14:05:01 +08:00
const replyingTo = ref<string | number | null>(null)
2025-08-18 12:03:30 +08:00
const replyText = ref('')
const replyToUsername = ref('')
2025-08-16 19:51:39 +08:00
// 处理视频播放
const handleVideoPlay = async (section: CourseSection) => {
console.log('播放视频:', section.name)
// 加载章节视频数据
await loadSectionVideo(section)
2025-08-16 19:51:39 +08:00
// 标记为已完成
if (!section.completed) {
section.completed = true
// 重新计算进度
const completed = courseSections.value.filter((s: CourseSection) => s.completed).length
completedLessons.value = completed
// 更新各个进度
const videoSections = courseSections.value.filter((section: CourseSection) => isVideoLesson(section))
const exerciseSections = courseSections.value.filter((section: CourseSection) => isHomeworkLesson(section))
const examSections = courseSections.value.filter((section: CourseSection) => isExamLesson(section))
videoProgress.value = videoSections.length > 0 ? Math.round((videoSections.filter((s: CourseSection) => s.completed).length / videoSections.length) * 100) : 0
exerciseProgress.value = exerciseSections.length > 0 ? Math.round((exerciseSections.filter((s: CourseSection) => s.completed).length / exerciseSections.length) * 100) : 0
examProgress.value = examSections.length > 0 ? Math.round((examSections.filter((s: CourseSection) => s.completed).length / examSections.length) * 100) : 0
}
}
// 加载章节视频
const loadSectionVideo = async (section: CourseSection) => {
try {
videoLoading.value = true
console.log('🔍 加载章节视频章节ID:', section.id)
const response = await CourseApi.getSectionVideos(courseId.value, section.id)
console.log('🔍 视频API响应:', response)
if (response.code === 0 || response.code === 200) {
if (response.data && response.data.length > 0) {
const video = response.data[0] // 取第一个视频
currentVideo.value = video
videoQualities.value = video.qualities
currentQuality.value = video.defaultQuality
// 设置视频清晰度选项
videoQualities.value = video.qualities || []
currentQuality.value = video.defaultQuality || '360'
// 获取默认清晰度的URL
const defaultQualityVideo = video.qualities.find((q: any) => q.value === video.defaultQuality)
if (defaultQualityVideo) {
currentVideoUrl.value = defaultQualityVideo.url
currentVideoSection.value = section
console.log('✅ 设置视频URL:', currentVideoUrl.value)
console.log('✅ 可用清晰度:', video.qualities)
console.log('✅ 传递给DPlayer的清晰度:', videoQualities.value)
}
} else {
console.warn('⚠️ 没有找到视频数据')
// 如果没有视频数据,使用默认视频
currentVideoUrl.value = 'https://example.com/default-video.mp4'
currentVideoSection.value = section
}
} else {
console.error('❌ 获取视频失败:', response.message)
// 使用默认视频
currentVideoUrl.value = 'https://example.com/default-video.mp4'
currentVideoSection.value = section
2025-08-16 19:51:39 +08:00
}
} catch (error) {
console.error('❌ 加载章节视频失败:', error)
// 使用默认视频
currentVideoUrl.value = 'https://example.com/default-video.mp4'
currentVideoSection.value = section
} finally {
videoLoading.value = false
2025-08-16 19:51:39 +08:00
}
}
2025-08-16 19:51:39 +08:00
// DPlayer 事件处理函数
const onVideoPlay = () => {
console.log('视频开始播放')
}
2025-08-16 19:51:39 +08:00
const onVideoPause = () => {
console.log('视频暂停')
}
2025-08-16 19:51:39 +08:00
const onVideoEnded = () => {
console.log('视频播放结束')
}
2025-08-16 19:51:39 +08:00
const onVideoError = (error: Event) => {
console.error('视频播放出错:', error)
2025-08-16 19:51:39 +08:00
}
// 新增的事件处理函数
const onScreenshot = (dataUrl: string) => {
console.log('截屏成功:', dataUrl)
// 可以在这里添加截屏成功的提示
message.success('截屏成功!')
}
const onDanmakuSend = (text: string) => {
console.log('发送弹幕:', text)
// 可以在这里添加弹幕发送的逻辑,比如保存到数据库
2025-08-16 20:39:56 +08:00
}
2025-08-29 18:28:49 +08:00
// 清晰度切换事件处理
const onQualityChange = (newQuality: string) => {
console.log('🔄 清晰度已切换到:', newQuality)
currentQuality.value = newQuality
}
2025-08-16 19:51:39 +08:00
// 加载课程详情
const loadCourseDetail = async () => {
console.log('开始加载课程详情课程ID:', courseId.value)
if (!courseId.value) {
courseId.value = '1'
}
2025-08-16 19:51:39 +08:00
if (!courseId.value || isNaN(Number(courseId.value))) {
console.log('课程ID无效')
error.value = '课程ID无效'
2025-08-16 19:51:39 +08:00
return
}
try {
loading.value = true
error.value = ''
console.log('调用API获取课程详情课程ID:', courseId.value)
const response = await CourseApi.getCourseById(courseId.value)
2025-08-16 19:51:39 +08:00
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返回错误')
error.value = response.message || '获取课程详情失败'
2025-08-16 19:51:39 +08:00
}
} catch (err) {
console.error('加载课程详情失败:', err)
error.value = '网络错误,请稍后重试'
2025-08-16 19:51:39 +08:00
} finally {
loading.value = false
}
}
// 加载课程章节列表
const loadCourseSections = async () => {
if (!courseId.value || isNaN(Number(courseId.value))) {
2025-08-16 19:51:39 +08:00
sectionsError.value = '课程ID无效'
console.error('课程ID无效:', courseId.value)
return
}
try {
sectionsLoading.value = true
sectionsError.value = ''
console.log('调用API获取课程章节...')
const response = await CourseApi.getCourseSections(courseId.value)
2025-08-16 19:51:39 +08:00
console.log('章节API响应:', response)
if (response.code === 0 || response.code === 200) {
if (response.data && response.data.list && Array.isArray(response.data.list)) {
console.log('✅ API返回的原始章节数据:', response.data.list)
console.log('✅ 章节数据数量:', response.data.list.length)
courseSections.value = response.data.list
groupedSections.value = groupSectionsByChapter(response.data.list)
console.log('✅ 设置后的courseSections:', courseSections.value)
console.log('✅ 设置后的groupedSections:', groupedSections.value)
console.log('✅ groupedSections长度:', groupedSections.value.length)
2025-08-16 19:51:39 +08:00
} else {
console.log('❌ API返回的章节数据为空或格式错误')
console.log('❌ response.data:', response.data)
sectionsError.value = '暂无课程章节数据'
2025-08-16 19:51:39 +08:00
}
} else {
console.log('API返回错误')
sectionsError.value = response.message || '获取课程章节失败'
2025-08-16 19:51:39 +08:00
}
} catch (err) {
console.error('加载课程章节失败:', err)
sectionsError.value = '网络错误,请稍后重试'
2025-08-16 19:51:39 +08:00
} finally {
sectionsLoading.value = false
}
}
// 切换章节展开/折叠
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) => {
if (isVideoLesson(section)) return '视频'
if (isResourceLesson(section)) return '资料'
if (isHomeworkLesson(section)) return '作业'
if (isExamLesson(section)) return '考试'
return '视频'
2025-08-16 19:51:39 +08:00
}
// 获取章节编号
const getChapterNumber = (num: number) => {
const numbers = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十']
return numbers[num - 1] || num.toString()
}
2025-08-16 19:51:39 +08:00
// 格式化课程时长
const formatLessonDuration = (section: CourseSection) => {
if (!section.duration) return ''
return section.duration
}
2025-08-16 19:51:39 +08:00
// 课程类型判断函数 - 与CourseDetailEnrolled保持一致
const isVideoLesson = (section: CourseSection) => {
console.log('检查章节类型:', section.name, 'type:', section.type, 'outline:', section.outline)
// 优先根据type字段判断0=视频
if (section.type === 0) {
return true
2025-08-16 19:51:39 +08:00
}
// 如果type为null则根据outline判断
return section.outline && (section.outline.includes('.m3u8') || section.outline.includes('.mp4'))
2025-08-16 19:51:39 +08:00
}
const isResourceLesson = (section: CourseSection) => {
// 优先根据type字段判断1=资料
if (section.type === 1) {
return true
}
// 如果type为null则根据outline或名称判断
return section.outline && (section.outline.includes('.pdf') || section.outline.includes('.ppt') || section.outline.includes('.zip'))
}
2025-08-16 19:51:39 +08:00
const isHomeworkLesson = (section: CourseSection) => {
// 优先根据type字段判断3=作业
if (section.type === 3) {
2025-08-16 19:51:39 +08:00
return true
}
// 如果type为null则根据名称判断
return section.name.includes('作业') || section.name.includes('练习')
}
const isExamLesson = (section: CourseSection) => {
// 优先根据type字段判断2=考试
if (section.type === 2) {
return true
2025-08-16 19:51:39 +08:00
}
// 如果type为null则根据名称判断
return section.name.includes('考试') || section.name.includes('测试')
}
// 获取课程类型样式类
const getLessonTypeBadgeClass = (section: CourseSection) => {
if (isVideoLesson(section)) return 'badge-video'
if (isResourceLesson(section)) return 'badge-resource'
if (isHomeworkLesson(section)) return 'badge-homework'
if (isExamLesson(section)) return 'badge-exam'
return 'badge-default'
2025-08-16 19:51:39 +08:00
}
// 处理下载操作
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
}
})
}
// 处理章节点击 - 已报名状态,可以正常点击
2025-08-16 19:51:39 +08:00
const handleSectionClick = (section: CourseSection) => {
console.log('🔍 点击课程章节:', section.name, section)
2025-08-16 19:51:39 +08:00
currentSection.value = section
// 检查章节类型
const isVideo = isVideoLesson(section)
const isResource = isResourceLesson(section)
const isHomework = isHomeworkLesson(section)
const isExam = isExamLesson(section)
console.log('🔍 章节类型判断结果:', {
isVideo,
isResource,
isHomework,
isExam,
type: section.type
})
// 如果是视频课程,加载视频数据
if (isVideo) {
console.log('✅ 识别为视频课程,开始加载视频数据')
loadSectionVideo(section)
} else if (isResource) {
console.log('✅ 识别为资料课程')
2025-08-16 19:51:39 +08:00
handleDownload(section)
} else if (isHomework) {
console.log('✅ 识别为作业课程')
2025-08-16 19:51:39 +08:00
handleHomework(section)
} else if (isExam) {
console.log('✅ 识别为考试课程')
2025-08-16 19:51:39 +08:00
handleExam(section)
} else {
console.log('⚠️ 未识别的课程类型,默认当作视频处理')
loadSectionVideo(section)
2025-08-16 19:51:39 +08:00
}
}
// 跳转到已报名区域
// 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)
// }
// }
// 预览章节(非视频内容)
2025-08-26 19:07:48 +08:00
// const previewSection = (section: CourseSection) => {
// console.log('预览章节:', section)
2025-08-16 19:51:39 +08:00
2025-08-26 19:07:48 +08:00
// previewModalTitle.value = section.name
// previewModalContent.value = `章节ID: ${section.id}\n章节名称: ${section.name}\n内容类型: ${getLessonTypeText(section)}`
// previewModalType.value = 'section'
// previewModalVisible.value = true
// }
2025-08-16 19:51:39 +08:00
// 关闭预览模态框
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
// }
// 切换AI助手和笔记标签页
const switchTab = (tab: string) => {
aiActiveTab.value = tab
console.log('切换到标签页:', tab)
}
// AI聊天相关状态
const chatMessage = ref('')
2025-08-18 12:03:30 +08:00
// 笔记相关状态
const showNoteEditor = ref(false)
const noteTitle = ref('')
const noteContent = ref('')
const notesList = ref([
{
title: 'DeepSeek模型特点总结',
content: '<p>1. 强大的自然语言处理能力</p><p>2. 支持多种编程语言</p><p>3. 高效的代码生成功能</p>',
date: '2025.01.23'
},
{
title: '应用场景笔记',
content: '<p>• 代码审查和优化</p><p>• 文档自动生成</p><p>• 问题诊断和解决</p>',
date: '2025.01.22'
}
])
const editingNoteIndex = ref(-1)
// 投诉/反馈弹窗相关状态
const complaintModalVisible = ref(false)
const complaintContent = ref('')
const complaintType = ref<'feedback' | 'complaint'>('feedback')
const uploadFileList = ref([])
// 打开投诉弹窗
const openComplaintModal = (type: 'feedback' | 'complaint') => {
complaintType.value = type
complaintModalVisible.value = true
complaintContent.value = ''
uploadFileList.value = []
}
// 取消投诉
const cancelComplaint = () => {
complaintModalVisible.value = false
complaintContent.value = ''
uploadFileList.value = []
}
// 提交投诉
const submitComplaint = async () => {
if (!complaintContent.value.trim()) {
message.warning('请输入投诉内容')
return
}
try {
// 这里可以调用API提交投诉
console.log('提交投诉:', {
type: complaintType.value,
content: complaintContent.value,
files: uploadFileList.value
})
message.success('提交成功,我们会尽快处理您的反馈')
complaintModalVisible.value = false
complaintContent.value = ''
uploadFileList.value = []
} catch (error) {
console.error('提交投诉失败:', error)
message.error('提交失败,请稍后重试')
}
}
// 处理文件上传
2025-09-01 21:55:29 +08:00
const handleUpload = ({ file, onFinish }: any) => {
// 这里可以实现真实的文件上传逻辑
console.log('上传文件:', file)
// 模拟上传成功
setTimeout(() => {
onFinish()
}, 1000)
}
2025-08-16 19:51:39 +08:00
// 发送聊天消息
const sendMessage = () => {
if (chatMessage.value.trim()) {
console.log('发送消息:', chatMessage.value)
// 这里可以添加发送消息的逻辑
chatMessage.value = ''
}
}
2025-08-18 12:03:30 +08:00
// 笔记相关方法
const saveNote = () => {
if (noteTitle.value.trim() && noteContent.value.trim()) {
const currentDate = new Date().toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
}).replace(/\//g, '.')
if (editingNoteIndex.value >= 0) {
// 编辑现有笔记
notesList.value[editingNoteIndex.value] = {
title: noteTitle.value,
content: noteContent.value,
date: currentDate
}
} else {
// 添加新笔记
notesList.value.unshift({
title: noteTitle.value,
content: noteContent.value,
date: currentDate
})
}
// 重置状态
cancelNote()
}
}
const cancelNote = () => {
showNoteEditor.value = false
noteTitle.value = ''
noteContent.value = ''
editingNoteIndex.value = -1
}
const editNote = (index: number) => {
const note = notesList.value[index]
noteTitle.value = note.title
noteContent.value = note.content
editingNoteIndex.value = index
showNoteEditor.value = true
}
const deleteNote = (index: number) => {
if (confirm('确定要删除这条笔记吗?')) {
notesList.value.splice(index, 1)
}
}
2025-08-16 19:51:39 +08:00
// 发送快捷消息
// const sendQuickMessage = (message: string) => {
// chatMessage.value = message
// sendMessage()
// }
// 测试直接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()
2025-08-30 03:57:24 +08:00
loadCourseComments() // 加载评论
2025-08-18 12:03:30 +08:00
// 检查是否需要刷新
const shouldRefresh = sessionStorage.getItem('refreshCourseExchanged')
if (shouldRefresh === 'true') {
isRefreshing.value = true
sessionStorage.removeItem('refreshCourseExchanged')
setTimeout(() => {
window.location.reload()
}, 300)
}
})
onActivated(() => {
// 检查是否需要刷新
const shouldRefresh = sessionStorage.getItem('refreshCourseExchanged')
if (shouldRefresh === 'true') {
isRefreshing.value = true
sessionStorage.removeItem('refreshCourseExchanged')
setTimeout(() => {
window.location.reload()
}, 300)
}
2025-08-16 19:51:39 +08:00
})
</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: none;
margin: 0;
padding-left: 120px;
padding-right: 120px;
2025-08-16 19:51:39 +08:00
}
.breadcrumb {
padding: 12px 0;
}
.breadcrumb-text {
color: #666;
font-size: 14px;
}
.main-content {
padding-bottom: 20px;
}
.content-layout {
margin: auto;
padding: 0 2%;
width: 100%;
display: flex;
gap: 20px;
align-items: flex-start;
}
.course-content {
display: flex;
gap: 32px;
2025-08-16 19:51:39 +08:00
width: 100%;
flex-direction: row-reverse;
}
.main-column {
min-width: 800px;
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;
}
.tip-section {
margin-top: 10px;
margin-bottom: 20px;
width: 100%;
height: 40px;
font-size: 14px;
color: #999;
background-color: rgba(255, 255, 255, 0.5);
padding: 10px 25px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.tip-section img {
width: 14px;
height: 14px;
background: #078BD2;
}
.tip-section .tip-section-box {
flex: 1;
text-align: right;
}
/* 视频播放器区域 */
.video-player-section {
position: relative;
background: #fff;
overflow: visible; /* 改为visible确保底部交互区域不被裁剪 */
2025-08-16 19:51:39 +08:00
margin-bottom: 20px;
display: flex;
flex-direction: column;
2025-08-16 19:51:39 +08:00
}
.video-player.unregistered {
height: 578px;
position: relative;
}
.video-player.enrolled {
background: #000;
/* 移除固定高度,让内容自适应 */
2025-08-16 19:51:39 +08:00
}
.video-container {
position: relative;
width: 100%;
height: 450px; /* 使用固定高度,确保播放器能正常工作 */
background: #000;
2025-08-16 19:51:39 +08:00
}
/* DPlayer 容器样式 */
.video-container :deep(.dplayer) {
width: 100% !important;
height: 100% !important;
2025-08-16 19:51:39 +08:00
}
.video-placeholder {
position: absolute;
top: 0;
left: 0;
2025-08-16 19:51:39 +08:00
width: 100%;
height: 100%;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
display: flex;
align-items: center;
justify-content: center;
background-color: #f5f5f5;
}
.placeholder-content {
text-align: center;
color: #666;
}
.play-icon {
margin-bottom: 16px;
}
.placeholder-content p {
font-size: 16px;
margin: 0;
}
.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;
flex-shrink: 0; /* 防止被压缩 */
position: relative;
z-index: 10; /* 确保在最上层 */
2025-08-16 19:51:39 +08:00
}
.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/aiCompanion/弹幕(开).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 {
padding: 0 !important;
line-height: 1.8;
color: #999999;
font-size: 14px;
display: flex;
justify-content: space-between;
align-items: center;
}
.course-description span {
color: #0088D1;
}
2025-08-30 03:57:24 +08:00
/* 课程描述内容样式 */
.course-description-content {
padding: 0;
line-height: 1.6;
color: #333;
width: 100%;
}
/* 课程描述中的图片样式 */
.course-description-content img {
max-width: 100%;
height: auto;
display: block;
margin: 10px auto;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
/* 课程描述中的段落样式 */
.course-description-content p {
margin: 16px 0;
font-size: 14px;
line-height: 1.6;
color: #666;
}
/* 课程描述中的标题样式 */
.course-description-content h1,
.course-description-content h2,
.course-description-content h3,
.course-description-content h4,
.course-description-content h5,
.course-description-content h6 {
margin: 20px 0 10px 0;
color: #333;
font-weight: 600;
}
/* 课程描述中的列表样式 */
.course-description-content ul,
.course-description-content ol {
margin: 16px 0;
padding-left: 20px;
}
.course-description-content li {
margin: 8px 0;
line-height: 1.6;
}
/* 默认描述样式 */
.default-description {
color: #666;
display: flex;
justify-content: space-between;
align-items: center;
}
2025-08-16 19:51:39 +08:00
.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;
}
/* 课程总结样式 */
.summary-content {
padding: 20px 0;
}
.summary-item {
position: relative;
margin-bottom: 30px;
padding-bottom: 20px;
/* border-bottom: 1px solid #f0f0f0; */
}
.summary-item:last-child {
border-bottom: none;
margin-bottom: 0;
}
.summary-item:last-child .summary-description::after {
display: none;
}
.summary-header {
display: flex;
align-items: center;
margin-bottom: 12px;
}
.timestamp {
font-size: 14px;
color: #333;
margin-right: 8px;
}
.timestamp-icon {
width: 8px;
height: 8px;
background: #0088D1;
border-radius: 50%;
margin-right: 12px;
border: 1px solid #0088D1;
position: relative;
}
.timestamp-icon span {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 12px;
height: 12px;
border: 1px solid #0088D1;
border-radius: 50%;
}
.summary-title {
font-size: 14px;
color: #333;
margin: 0;
line-height: 1.4;
}
.summary-description {
font-size: 14px;
line-height: 1.6;
color: #666;
margin: 12px 0 0 44px;
padding-left: 20px;
position: relative;
}
.summary-description::before {
content: '';
position: absolute;
left: 0;
top: -12px;
bottom: -20px;
width: 2px;
border-left: 1.5px dashed #B6E7FC;
}
/* 字幕列表样式 */
.subtitles-content {
padding: 20px 0;
}
.subtitle-item {
display: flex;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f5f5f5;
cursor: pointer;
transition: background-color 0.2s;
}
.subtitle-item:hover {
background-color: #f8f9fa;
}
.subtitle-item:last-child {
border-bottom: none;
}
.subtitle-time {
font-size: 14px;
color: #008BD7;
font-weight: 500;
min-width: 60px;
margin-right: 20px;
}
.subtitle-text {
font-size: 14px;
color: #333;
line-height: 1.5;
flex: 1;
}
/* 右侧边栏课程章节 */
.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;
}
/* AI助手界面样式 */
.ai-assistant-interface {
position: relative;
width: 360px; /* 固定宽度,不再自适应 */
flex-shrink: 0; /* 防止收缩 */
2025-08-16 19:51:39 +08:00
/* background: white; */
padding-top: 12px;
margin-top: 30px;
}
/* 顶部控制栏 */
.top-controls {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background-color: transparent;
}
.mode-button {
background: none;
border: none;
cursor: pointer;
padding: 4px;
border-radius: 4px;
transition: background-color 0.2s;
}
.mode-button:hover {
background: #e9ecef;
}
.mode-icon {
width: 16px;
height: 16px;
}
.close-button {
background: none;
border: none;
cursor: pointer;
font-size: 18px;
color: #6c757d;
padding: 4px 8px;
border-radius: 4px;
transition: all 0.2s;
}
.close-button:hover {
background: #e9ecef;
color: #495057;
}
/* AI主要内容区域 */
.ai-main-content {
background-color: rgba(255, 255, 255, 0.5);
padding: 15px 20px;
border: 1px solid #fff;
}
/* AI头部栏 */
.ai-header-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.ai-header-left {
display: flex;
align-items: center;
gap: 12px;
}
.ai-avatar {
width: 40px;
height: 40px;
}
.ai-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.ai-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
background: linear-gradient(to right, #1EA4FF, #3066FF);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.ai-tabs {
display: flex;
gap: 8px;
}
.tab {
padding: 4px 8px;
font-size: 12px;
color: #6c757d;
cursor: pointer;
border-radius: 4px;
transition: all 0.2s;
}
.tab.active {
background: #007bff;
color: white;
}
.tab:hover:not(.active) {
background: #f8f9fa;
}
.save-button {
background: none;
border: none;
cursor: pointer;
padding: 8px;
border-radius: 4px;
transition: background-color 0.2s;
}
.save-button:hover {
background: #f8f9fa;
}
.save-icon {
width: 20px;
height: 20px;
}
/* AI工具栏 */
.ai-toolbar {
height: 32px;
display: flex;
gap: 8px;
margin-bottom: 20px;
background: #fff;
}
.toolbar {
2025-08-18 12:03:30 +08:00
margin-top: 15px;
2025-08-16 19:51:39 +08:00
margin-left: 16px;
padding: 0;
}
.toolbar img {
margin-right: 10px;
width: 18px;
height: 18px;
}
.tool-btn {
background: none;
border: none;
cursor: pointer;
padding: 8px;
border-radius: 4px;
transition: all 0.2s;
}
.tool-btn:hover {
background: #e9ecef;
}
.tool-icon {
width: 20px;
height: 20px;
}
/* AI内容区域 */
.ai-content-area {
2025-08-18 12:03:30 +08:00
/* background-color: #fff; */
2025-08-16 19:51:39 +08:00
margin-bottom: 20px;
}
/* AI聊天界面样式 */
.ai-chat-interface {
display: flex;
flex-direction: column;
2025-08-18 12:03:30 +08:00
height: 600px;
2025-08-16 19:51:39 +08:00
}
.chat-messages {
flex: 1;
overflow-y: auto;
2025-08-18 12:03:30 +08:00
padding: 10px;
2025-08-16 19:51:39 +08:00
display: flex;
flex-direction: column;
gap: 16px;
/* 隐藏滚动条但保持功能 */
scrollbar-width: none;
/* Firefox */
-ms-overflow-style: none;
2025-08-18 12:03:30 +08:00
background-color: #fff;
2025-08-16 19:51:39 +08:00
/* IE and Edge */
}
.chat-messages::-webkit-scrollbar {
display: none;
/* Chrome, Safari and Opera */
}
.message {
display: flex;
align-items: flex-start;
gap: 16px;
}
.message.user-message {
flex-direction: row-reverse;
/* 用户头像在右边 */
}
.message-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
overflow: hidden;
flex-shrink: 0;
border: 2px solid #EBF9FF;
}
.message-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.message-content {
flex: 1;
max-width: 70%;
}
.message-bubble {
background: #EAF8FF;
padding: 12px 16px;
margin-bottom: 4px;
}
.message.user-message .message-bubble {
background: linear-gradient(135deg, #FFFBD9 0%, #EAF8FF 100%);
color: #323232;
}
.message-bubble p {
margin: 0 0 8px 0;
font-size: 14px;
line-height: 1.4;
}
.message-bubble p:last-child {
margin-bottom: 0;
}
.message-bubble ul,
.message-bubble ol {
margin: 8px 0;
padding-left: 20px;
}
.message-bubble li {
margin-bottom: 4px;
font-size: 14px;
line-height: 1.4;
}
.message-time {
font-size: 12px;
color: #999;
text-align: right;
}
.message.user-message .message-time {
text-align: left;
}
.chat-input-area {
height: 100%;
display: flex;
background: #fff;
margin: 0 16px 16px 16px;
height: 96px;
2025-08-18 22:09:42 +08:00
border: 1.5px solid #D5D5D5;
2025-08-16 19:51:39 +08:00
position: relative;
}
.input-container {
width: 100%;
height: 100%;
display: flex;
gap: 12px;
margin-bottom: 16px;
align-items: center;
}
.input-container input {
width: 100%;
height: 100%;
border: none;
outline: none;
background: transparent;
font-size: 14px;
color: #333;
padding: 12px 20px;
}
.chat-input {
height: 100%;
flex: 1;
padding: 12px 20px;
font-size: 14px;
outline: none;
background: white;
resize: none;
position: relative;
2025-08-18 22:09:42 +08:00
border: none;
2025-08-16 19:51:39 +08:00
}
.send-button {
position: absolute;
right: 10px;
bottom: 10px;
width: 34px;
height: 22px;
border-radius: 1px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border: none;
z-index: 1000;
}
.send-icon {
width: 100%;
height: 100%;
}
.quick-actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
justify-content: center;
}
.quick-action-btn {
padding: 8px 16px;
border: 1px solid #e8e8e8;
border-radius: 20px;
background: white;
color: #666;
font-size: 13px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.quick-action-btn:hover {
border-color: #667eea;
color: #667eea;
background: #f8f9ff;
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(102, 126, 234, 0.15);
}
/* 我的笔记界面样式 */
.notes-interface {
display: flex;
flex-direction: column;
gap: 16px;
}
.notes-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 12px;
border-bottom: 1px solid #e8e8e8;
}
.notes-header h4 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
.add-note-btn {
display: flex;
align-items: center;
gap: 6px;
background: #1890ff;
color: white;
border: none;
padding: 6px 12px;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
transition: background-color 0.3s;
}
.add-note-btn:hover {
background: #40a9ff;
}
.add-icon {
width: 12px;
height: 12px;
}
.notes-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.note-item {
background: #f8f9fa;
border-radius: 8px;
padding: 12px;
border: 1px solid #e8e8e8;
}
.note-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.note-title {
font-size: 14px;
font-weight: 500;
color: #333;
}
.note-date {
font-size: 12px;
color: #999;
}
.note-content {
margin-bottom: 12px;
}
.note-content p {
margin: 0 0 4px 0;
font-size: 13px;
color: #666;
line-height: 1.4;
}
.note-actions {
display: flex;
gap: 8px;
}
.note-action-btn {
padding: 4px 8px;
border: 1px solid #d9d9d9;
border-radius: 4px;
background: white;
color: #666;
font-size: 12px;
cursor: pointer;
transition: all 0.3s;
}
.note-action-btn:hover {
border-color: #1890ff;
color: #1890ff;
}
2025-08-18 12:03:30 +08:00
/* 笔记编辑器样式 */
.note-editor-container {
background: white;
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
border: 1px solid #e8e8e8;
}
.note-editor-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
gap: 12px;
}
.note-title-input {
flex: 1;
padding: 8px 12px;
border: 1px solid #e0e0e0;
border-radius: 4px;
font-size: 14px;
outline: none;
transition: border-color 0.2s;
}
.note-title-input:focus {
border-color: #1890ff;
}
.note-editor-actions {
display: flex;
gap: 8px;
}
.save-note-btn {
background: #1890ff;
color: white;
border: none;
padding: 6px 12px;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
transition: background-color 0.3s;
}
.save-note-btn:hover {
background: #40a9ff;
}
.cancel-note-btn {
background: #f5f5f5;
color: #666;
border: 1px solid #d9d9d9;
padding: 6px 12px;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
transition: all 0.3s;
}
.cancel-note-btn:hover {
background: #e6e6e6;
border-color: #bfbfbf;
}
2025-08-16 19:51:39 +08:00
.ai-text-content {
margin-bottom: 16px;
}
.ai-text-content p {
margin: 0 0 12px 0;
color: #333;
font-size: 14px;
line-height: 1.5;
}
.ai-text-content ul {
margin: 0;
padding-left: 20px;
color: #666;
font-size: 14px;
line-height: 1.6;
}
.ai-text-content li {
margin-bottom: 4px;
}
/* 图片横幅 */
.image-banner {
margin-bottom: 16px;
}
.banner-content {
position: relative;
border-radius: 8px;
overflow: hidden;
/* height: 120px; */
}
.banner-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.banner-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
justify-content: center;
}
.overlay-image {
width: 60px;
height: 60px;
opacity: 0.8;
}
/* 大纲区域 */
.outline-section {
background: #f8f9fa;
border-radius: 8px;
padding: 16px;
}
.outline-section h4 {
margin: 0 0 12px 0;
font-size: 14px;
font-weight: 600;
color: #333;
}
.outline-content {
display: flex;
flex-direction: column;
gap: 8px;
}
.outline-item {
display: flex;
align-items: center;
gap: 8px;
}
.outline-number {
width: 20px;
height: 20px;
background: #007bff;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 600;
}
.outline-text {
font-size: 13px;
color: #666;
}
/* AI底部按钮 */
.ai-bottom-button {
text-align: center;
}
.public-notes-btn {
display: inline-flex;
justify-content: center;
align-items: center;
gap: 8px;
background: #EEF9FF;
color: #0088D1;
border: none;
width: 96px;
height: 24px;
border-radius: 30px;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
border: 1px solid #0088D1;
}
.public-notes-btn:hover {
background: #0056b3;
}
.notes-icon {
width: 10px;
height: 10px;
}
/* 知识图谱横幅 */
.knowledge-graph-banner {
/* width: 444px; */
height: 148px;
margin-top: 30px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.knowledge-graph-banner img {
width: 100%;
height: 100%;
object-fit: cover;
}
.graph-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.graph-text h4 {
margin: 0 0 4px 0;
font-size: 16px;
font-weight: 600;
}
.graph-text p {
margin: 0;
font-size: 14px;
opacity: 0.9;
}
.graph-icon {
display: flex;
align-items: center;
}
.graph-image {
width: 40px;
height: 40px;
opacity: 0.9;
}
/* 更多课程 */
.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;
}
/* 响应式设计 */
/* 大屏幕 - 使用120px左右边距 */
@media (min-width: 1400px) {
.container {
padding-left: 120px;
padding-right: 120px;
max-width: none;
margin: 0;
}
.course-content {
gap: 32px;
}
}
2025-08-16 19:51:39 +08:00
@media (max-width: 1399px) and (min-width: 1200px) {
.container {
padding: 0 24px;
max-width: 1200px;
margin: 0 auto;
}
.course-content {
gap: 20px;
2025-08-16 19:51:39 +08:00
}
.sidebar {
width: 350px;
}
}
@media (max-width: 1199px) and (min-width: 992px) {
.container {
padding: 0 20px;
max-width: 992px;
margin: 0 auto;
2025-08-16 19:51:39 +08:00
}
.course-content {
gap: 20px;
}
.sidebar {
width: 320px;
}
}
/* 平板横屏 */
@media (max-width: 1023px) and (min-width: 768px) {
.container {
padding: 0 16px;
max-width: 768px;
margin: 0 auto;
2025-08-16 19:51:39 +08:00
}
.course-content {
gap: 16px;
}
.sidebar {
width: 280px;
}
}
/* 平板竖屏及以下 */
@media (max-width: 767px) {
.container {
padding: 0 16px;
max-width: 576px;
margin: 0 auto;
2025-08-16 19:51:39 +08:00
}
.course-content {
flex-direction: column;
gap: 16px;
}
.sidebar {
width: 100%;
order: -1;
}
.video-player-section {
/* 移除固定高度,让内容自适应 */
2025-08-16 19:51:39 +08:00
}
}
@media (max-width: 767px) {
.container {
padding: 0 16px;
max-width: 576px;
margin: 0 auto;
2025-08-16 19:51:39 +08:00
}
.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;
margin: 0 auto;
max-width: 480px;
2025-08-16 19:51:39 +08:00
}
.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;
margin: 0 auto;
max-width: 360px;
2025-08-16 19:51:39 +08:00
}
.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 {
position: absolute;
top: -30px;
right: 0;
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;
}
/* AI Tab 切换样式 */
.ai-tab {
width: 292px;
height: 36px;
background: #EAF2F5;
border-radius: 11px;
display: flex;
margin: 15px auto;
padding: 4px;
transform: scale(0.9);
transform-origin: center;
align-items: center;
justify-content: center;
}
.ai-tab-item {
flex: 1;
text-align: center;
background: transparent;
border: none;
cursor: pointer;
font-size: 16px; /* 调整字体大小从12px到16px */
2025-08-16 19:51:39 +08:00
color: #666;
border-radius: 4px;
transition: all 0.2s ease;
position: relative;
display: flex;
align-items: center;
justify-content: center;
height: 28px;
padding: 0 12px;
}
.ai-tab-item.active {
background: #FFFFFF;
color: #000;
font-weight: 500;
border-radius: 8px;
}
.ai-tab-item:not(.active):hover {
background: rgba(0, 0, 0, 0.05);
}
2025-08-18 12:03:30 +08:00
/* 评论区样式 */
.comments-content {
padding: 0;
}
/* 发布评论区域 */
.post-comment-section {
margin-bottom: 10px;
}
.comment-input-wrapper {
display: flex;
gap: 12px;
}
.user-avatar img {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
flex-shrink: 0;
}
.comment-input-area {
flex: 1;
}
.comment-textarea {
width: 100%;
border: 1px solid #E6E6E6;
padding: 10px;
font-size: 14px;
resize: none;
font-family: inherit;
height: 40px;
min-height: 40px;
box-sizing: border-box;
overflow: hidden;
transition: height 0.3s ease;
}
.comment-textarea:focus {
outline: none;
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
.comment-textarea::placeholder {
color: #999;
}
.comment-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
}
.toolbar-left {
display: flex;
gap: 8px;
}
.toolbar-btn {
background: none;
border: none;
width: 24px;
height: 20px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border: 1.5px solid #E6E6E6;
}
.toolbar-btn:hover {
background: #f5f5f5;
}
.toolbar-icon {
width: 12px;
height: 12px;
object-fit: contain;
}
.toolbar-right {
display: flex;
align-items: center;
}
.btn-submit {
background: #0088D1;
color: white;
border: none;
padding: 3px 10px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.3s;
font-weight: 500;
}
.btn-submit:hover:not(:disabled) {
background: #40a9ff;
}
.btn-submit:disabled {
opacity: 0.5;
cursor: not-allowed;
background: #f5f5f5;
color: #bfbfbf;
}
/* 回复输入区域样式 */
.reply-input-section {
margin-top: 16px;
padding: 16px;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.reply-input-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.reply-to-text {
font-size: 14px;
color: #666;
font-weight: 500;
}
.cancel-reply-btn {
background: none;
border: none;
color: #999;
font-size: 14px;
cursor: pointer;
padding: 4px 8px;
border-radius: 4px;
transition: all 0.3s ease;
}
.cancel-reply-btn:hover {
background: #f0f0f0;
color: #666;
}
.reply-input-container {
background: white;
border: 1px solid #E6E6E6;
border-radius: 6px;
overflow: hidden;
}
.reply-textarea {
width: 100%;
border: none;
padding: 10px;
font-size: 14px;
resize: none;
font-family: inherit;
height: 40px;
min-height: 40px;
box-sizing: border-box;
overflow: hidden;
transition: height 0.3s ease;
background: transparent;
}
.reply-textarea:focus {
outline: none;
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
.reply-textarea::placeholder {
color: #999;
}
.reply-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 10px;
border-top: 1px solid #f0f0f0;
background: #fafafa;
}
.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;
}
.comment-username {
font-size: 14px;
color: #666666;
}
.comment-time {
font-size: 12px;
color: #999;
}
.comment-text {
font-size: 14px;
line-height: 1.6;
color: #333;
margin-bottom: 12px;
}
.comment-image-container {
width: 100%;
margin-bottom: 10px;
display: flex;
flex-wrap: wrap;
gap: 16px;
position: relative;
}
.comment-image-container img {
width: 84px;
height: 84px;
object-fit: cover;
border-radius: 4px;
}
.image-overlay {
position: absolute;
top: 0;
left: calc(6 * (84px + 16px));
width: 84px;
height: 84px;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
cursor: pointer;
}
.more-images-text {
color: white;
font-size: 16px;
font-weight: 500;
}
.comment-actions {
display: flex;
align-items: center;
gap: 16px;
}
.note-icon-container {
width: 50px;
height: 20px;
background: #EDEDED;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
}
.note-icon-container img {
width: 10px;
height: 10px;
}
.note-icon-container span {
color: #666666;
font-size: 10px;
}
.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;
}
/* 回复和二级评论样式 */
.comment-replies {
margin-top: 12px;
}
.reply-item {
display: flex;
gap: 12px;
margin-bottom: 16px;
position: relative;
}
.reply-item:last-child {
margin-bottom: 0;
}
.reply-avatar {
flex-shrink: 0;
}
.reply-avatar img {
width: 24px;
height: 24px;
border-radius: 50%;
object-fit: cover;
}
.reply-content {
flex: 1;
}
.reply-main {
display: flex;
align-items: flex-start;
gap: 12px;
margin-bottom: 8px;
}
.reply-header {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.reply-username {
font-size: 14px;
color: #666;
}
.reply-badge {
width: 32px;
height: 20px;
text-align: center;
line-height: 20px;
font-size: 10px;
font-weight: 500;
font-size: 10px;
}
.reply-badge.instructor {
background: #EEF9FF;
color: #008BD7;
}
.reply-badge.user {
background: #f6ffed;
color: #52c41a;
}
.reply-time {
font-size: 12px;
color: #999;
}
.reply-text {
font-size: 14px;
line-height: 1.5;
color: #333;
flex: 1;
}
.reply-footer {
display: flex;
justify-content: left;
align-items: center;
margin-top: 8px;
gap: 15px;
}
.reply-actions {
display: flex;
align-items: center;
gap: 12px;
}
.reply-action-btn {
background: none;
border: none;
font-size: 12px;
color: #999;
cursor: pointer;
transition: color 0.3s;
}
.reply-action-btn:hover {
color: #1890ff;
}
/* 刷新遮罩样式 */
.refresh-mask {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.9);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
backdrop-filter: blur(2px);
}
.refresh-content {
text-align: center;
color: #666;
}
.refresh-spinner {
width: 40px;
height: 40px;
border: 3px solid #f3f3f3;
border-top: 3px solid #1890ff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 16px;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* AI建议样式 */
.ai-suggestion {
margin-top: 20px;
padding: 20px;
background: white;
border-radius: 8px;
}
.ai-suggestion-title {
font-size: 14px;
color: #323232;
font-weight: 500;
margin-bottom: 20px;
}
.ai-suggestion-item {
margin-bottom: 30px;
}
.ai-suggestion-header {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin-bottom: 8px;
}
.ai-icon {
width: 18px;
height: 18px;
}
.ai-category {
font-size: 16px;
color: #323232;
font-weight: 500;
}
.ai-prompt-bubble {
height: 36px;
line-height: 36px;
background: linear-gradient(90deg, #FFFBD9 0%, #EAF8FF 100%);
border-radius: 2px;
padding: 0 16px;
font-size: 14px;
color: #323232;
cursor: pointer;
transition: all 0.2s;
}
.ai-prompt-bubble:hover {
background: #e0f2fe;
border-color: #0284c7;
}
.refresh-content p {
font-size: 16px;
margin: 0;
}
2025-08-30 03:57:24 +08:00
/* 评论状态样式 */
.comments-loading, .comments-error, .no-comments {
padding: 40px 20px;
text-align: center;
color: #666;
font-size: 14px;
}
.comments-error {
color: #ff4d4f;
}
.comments-error .retry-btn {
margin-top: 10px;
padding: 8px 16px;
background: #1890ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background 0.2s;
}
.comments-error .retry-btn:hover {
background: #40a9ff;
}
.no-comments {
color: #999;
}
/* 投诉/反馈弹窗样式 */
.complaint-modal {
background: white;
border-radius: 2px;
padding: 24px;
}
.complaint-modal-header {
margin-bottom: 24px;
}
.complaint-modal-title {
width: 72px;
height: 22px;
font-family: PingFangSC, PingFang SC;
font-weight: 500;
font-size: 16px;
color: #000000;
line-height: 22px;
text-align: left;
font-style: normal;
margin-bottom: 16px;
}
.complaint-modal-divider {
width: 506px;
height: 1px;
background: #E6E6E6;
}
.complaint-modal-content {
padding: 20px 0;
}
.complaint-section {
display: flex;
margin-bottom: 24px;
align-items: flex-start;
}
.complaint-label {
font-size: 14px;
color: #333;
font-weight: 500;
margin-right: 16px;
white-space: nowrap;
padding-top: 8px;
}
.complaint-input-wrapper {
flex: 1;
}
.complaint-textarea {
width: 428px;
height: 214px;
background: #F5F8FB;
border-radius: 2px;
}
.upload-section {
display: flex;
margin-bottom: 24px;
align-items: flex-start;
}
.upload-label {
font-size: 14px;
color: #333;
font-weight: 500;
margin-right: 16px;
white-space: nowrap;
padding-top: 8px;
}
.upload-wrapper {
flex: 1;
}
.upload-area {
width: 100px;
height: 100px;
background: white;
border: 1px dashed #d9d9d9;
border-radius: 1px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: border-color 0.3s;
}
.upload-area:hover {
border-color: #40a9ff;
}
.upload-plus {
width: 20px;
height: 20px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.upload-plus::before,
.upload-plus::after {
content: '';
position: absolute;
background: #666666;
}
.upload-plus::before {
width: 10px;
height: 1px;
}
.upload-plus::after {
width: 1px;
height: 10px;
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
padding-top: 24px;
}
.cancel-btn {
background: #f5f5f5;
border: 1px solid #d9d9d9;
color: #666;
}
.cancel-btn:hover {
background: #e6f7ff;
border-color: #40a9ff;
color: #40a9ff;
}
.submit-btn {
background: #1890ff;
border: 1px solid #1890ff;
}
.submit-btn:hover {
background: #40a9ff;
border-color: #40a9ff;
}
/* 课程描述中的可点击元素样式 */
.course-description span {
color: #1890ff;
cursor: pointer;
text-decoration: underline;
}
.course-description span:hover {
color: #40a9ff;
}
2025-08-16 19:51:39 +08:00
</style>