OL-LearnPlatform-Frontend/src/views/CourseExchanged.vue
2025-09-23 15:12:30 +08:00

9607 lines
234 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="course-detail-page">
<!-- 刷新遮罩 -->
<div v-if="isRefreshing" class="refresh-mask">
<div class="refresh-content">
<div class="refresh-spinner"></div>
<p>正在刷新...</p>
</div>
</div>
<!-- 主要内容区域 -->
<div class="main-content">
<div class="container">
<div class="content-layout">
<!-- 加载状态 -->
<div v-if="loading" class="loading-container">
<div class="loading-content">
<p>正在加载课程详情...</p>
</div>
</div>
<!-- 错误状态 -->
<div v-else-if="error" class="error-container">
<div class="error-content">
<p>{{ error }}</p>
<button @click="loadCourseDetail" class="retry-btn">重试</button>
</div>
</div>
<!-- 课程内容 -->
<div v-else-if="course" class="course-content">
<!-- 右侧主要内容 -->
<div class="main-column" :class="{ 'practice-mode': practiceMode }">
<!-- 横幅标题区域 - 练习模式和讨论模式下隐藏 -->
<div v-if="!practiceMode && !discussionMode" class="banner-title-section">
<div class="banner-content">
<div class="banner-text">
<span class="main-text">暑期名师领学,提高班级教学质量!高效冲分指南</span>
<div v-if="(course as any)?.izAi === 1" class="ai-companion-tag">
<img src="/images/aiCompanion/AI伴学标签@2x.png" alt="AI伴学" class="tag-image">
</div>
</div>
</div>
</div>
<!-- 提示 - 练习模式和讨论模式下显示不同内容 -->
<div v-if="!practiceMode && !discussionMode && showTipSection" class="tip-section">
<img src="/images/aiCompanion/ii.jpg" alt="">
<span>此视频请在2025.10.23 23:59前完成学习快进拖拽或逾期学习不计入观看进度和成绩。</span>
<div class="tip-section-box" @click="hideTipSection">
<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 v-if="practiceMode || discussionMode" class="practice-section">
<!-- 整体横向布局 -->
<div class="practice-overall-layout">
<!-- 左侧纵向盒子 -->
<div class="practice-left-container">
<!-- 面包屑导航 -->
<div class="breadcrumb-section">
<div class="breadcrumb">
<span class="breadcrumb-course clickable" @click="goBackToVideo">{{ course?.title || '课程' }}</span>
<span class="breadcrumb-separator"> > </span>
<span class="breadcrumb-current">{{ practiceMode ? currentPracticeSection?.name || '练习' : '讨论' }}</span>
</div>
</div>
<!-- 提示区域 -->
<div class="tip-section practice-tip">
<img src="/images/aiCompanion/ii.jpg" alt="">
<span>{{ practiceMode ? '此练习' : '此讨论' }}请在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 v-if="practiceStarted && !practiceFinished" class="practice-content">
<div class="question-card" v-if="currentPracticeQuestion">
<div class="question-header">
<span class="question-title-info">{{ String(currentQuestionIndex + 1).padStart(2, '0') }}【{{ getPracticeQuestionTypeShort(currentPracticeQuestion.type) }}】<span>{{ currentPracticeQuestion.score }}分</span></span>
</div>
<div class="question-content">
<div class="question-title">
{{ currentPracticeQuestion.title }}
</div>
<!-- 选择题选项 -->
<div v-if="currentPracticeQuestion.type === '单选题' || currentPracticeQuestion.type === '多选题' || currentPracticeQuestion.type === '判断题'" class="question-options">
<div v-for="(option, index) in currentPracticeQuestion.options" :key="index"
class="option-item"
:class="{ 'selected': isPracticeOptionSelected(index) }"
@click="selectPracticeOption(index)">
<div class="option-checkbox">
<input type="checkbox"
:name="`practice-question-${currentQuestionIndex}`"
:checked="isPracticeOptionSelected(index)"
@change="handlePracticeCheckboxClick(index, $event)"
@click="handlePracticeCheckboxClick(index, $event)">
</div>
<span class="option-label">{{ String.fromCharCode(65 + index) }}.</span>
<span class="option-text">{{ option }}</span>
</div>
</div>
<!-- 填空题输入框 -->
<div v-else-if="currentPracticeQuestion.type === '填空题'" class="fill-blank">
<div class="fill-item" v-for="(_, index) in currentPracticeQuestion.blanks || [1]" :key="index">
<span class="fill-number">{{ index + 1 }}.</span>
<input type="text"
:value="getPracticeFillAnswer(currentQuestionIndex, index)"
@input="setPracticeFillAnswer(currentQuestionIndex, index, $event.target.value)"
placeholder=""
class="fill-input" />
</div>
<div class="fill-hint">
*请在上方输入框内输入填空内容
</div>
</div>
<!-- 简答题文本域 -->
<div v-else-if="currentPracticeQuestion.type === '简答题'" class="essay-answer">
<div class="essay-container">
<textarea v-model="essayAnswers[currentQuestionIndex]"
placeholder=""
class="essay-textarea"
rows="8"
maxlength="500"></textarea>
</div>
<div class="essay-footer">
<div class="essay-hint">
*请在上方输入框内输入答案内容
</div>
<div class="essay-counter">
{{ getPracticeEssayLength(currentQuestionIndex) }}/500
</div>
</div>
</div>
</div>
<div class="question-navigation">
<button class="btn-nav btn-prev" @click="previousPracticeQuestion" :disabled="currentQuestionIndex === 0">
上一题
</button>
<button v-if="currentQuestionIndex < practiceQuestions.length - 1" class="btn-nav btn-next" @click="nextPracticeQuestion">
下一题
</button>
<button v-else class="btn-nav btn-return" @click="exitPracticeMode">
返回学习
</button>
</div>
</div>
</div>
</div>
<!-- 右侧答题卡 -->
<div v-if="practiceStarted && !practiceFinished" class="practice-answer-card">
<div class="answer-card-container">
<!-- 答题报告标题 -->
<div class="answer-card-header">
<div class="answer-card-title">答题报告</div>
</div>
<!-- 分割线 -->
<div class="divider-line"></div>
<!-- 当前得分圆环 -->
<div class="score-circle-container">
<SemiCircleProgress
:value="getCurrentPracticeScore()"
:max-value="getTotalPracticeScore()"
label="当前得分"
:size="190"
:stroke-width="16"
progress-color="#0288d1"
:background-colors="{
outer: '#e1edf2',
middle: '#cce2ed'
}"
/>
<!-- 难度标签移到圆环右上角 -->
<div class="difficulty-tag-circle">难度·4.8</div>
</div>
<!-- 答题信息 -->
<div class="answer-info">
<div class="info-item">
<span class="info-label">答题时间:</span>
<span class="info-value">2025.07.13 12:34</span>
</div>
<div class="info-item">
<span class="info-label">答题时长:</span>
<span class="info-value">1分23秒</span>
</div>
<div class="info-item">
<span class="info-label">答题总数:</span>
<span class="info-value">{{ getAnsweredCount() }}/{{ practiceQuestions.length }}</span>
</div>
</div>
<!-- 分割线 -->
<div class="divider-line"></div>
<!-- 得分情况 -->
<div class="score-breakdown">
<div class="score-breakdown-title">得分情况</div>
<div class="score-item">
<div class="score-item-content">
<span class="score-type">单选</span>
<div class="score-progress-bar">
<div class="score-progress-fill" :style="{ width: getSingleChoiceProgress() + '%' }"></div>
</div>
<span class="score-count">{{ getSingleChoiceScore() }}/{{ getSingleChoiceTotal() }}</span>
</div>
</div>
<div class="score-item">
<div class="score-item-content">
<span class="score-type">多选</span>
<div class="score-progress-bar">
<div class="score-progress-fill" :style="{ width: getMultiChoiceProgress() + '%' }"></div>
</div>
<span class="score-count">{{ getMultiChoiceScore() }}/{{ getMultiChoiceTotal() }}</span>
</div>
</div>
<div class="score-item">
<div class="score-item-content">
<span class="score-type">判断</span>
<div class="score-progress-bar">
<div class="score-progress-fill" :style="{ width: getJudgeProgress() + '%' }"></div>
</div>
<span class="score-count">{{ getJudgeScore() }}/{{ getJudgeTotal() }}</span>
</div>
</div>
</div>
<!-- 分割线 -->
<div class="divider-line"></div>
<!-- 排名信息 -->
<div class="ranking-info">
<div class="ranking-item ranking-item-left">
<div class="ranking-number">6</div>
<div class="ranking-label">最高分</div>
</div>
<div class="ranking-divider"></div>
<div class="ranking-item ranking-item-center">
<div class="ranking-number-with-text">
<span class="ranking-prefix">第</span>
<span class="ranking-number">30</span>
<span class="ranking-suffix">名</span>
</div>
<div class="ranking-label">练习人数3892人</div>
</div>
<div class="ranking-divider"></div>
<div class="ranking-item ranking-item-right">
<div class="ranking-number">90</div>
<div class="ranking-label">答错人数</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 视频播放器区域 - 已兑换状态,练习模式和讨论模式下隐藏 -->
<div v-else-if="!practiceMode && !discussionMode" 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"
@qualityChange="onQualityChange"
/>
<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" style="display: none;">
<div class="comment-input">
<input type="text" placeholder="成功报名学习才能发送弹幕哦~" />
<button class="send-btn">发送</button>
</div>
</div>
</div>
</div>
<!-- 讨论模式界面 -->
<div v-if="discussionMode" class="discussion-section">
<!-- 讨论主容器 -->
<div class="discussion-container">
<!-- 讨论标题行 -->
<div class="discussion-title-row">
<h2 class="discussion-title">讨论</h2>
<span class="participation-status">未参与</span>
</div>
<!-- 讨论描述 -->
<div class="discussion-description">
如何理解学风与科研诚信?如何理解优良学风与科研诚信之间的关系?各位同学大家好,欢迎加入本学期的课程学习,在学习过程有任何问题困惑和不同见解,都欢迎同学在讨论区积极交流,踊跃回复老师留在讨论区的问题。
</div>
<!-- 评论统计 -->
<div class="discussion-stats">
<span class="comment-count">评论 (1251)</span>
</div>
<!-- 评论输入区域 -->
<div class="comment-input-section">
<div class="user-avatar">
<img src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-4.0.3&auto=format&fit=crop&w=40&q=80" alt="用户头像">
</div>
<div class="input-wrapper">
<textarea
v-model="newComment"
placeholder="请把你的想法写下来~"
class="comment-input"
rows="3"
></textarea>
<div class="input-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/Image.png" alt="图片" class="toolbar-icon" />
</button>
</div>
<button @click="submitDiscussionComment" class="submit-btn" :disabled="!newComment.trim()">
发表
</button>
</div>
</div>
</div>
<!-- 评论列表 -->
<div class="discussion-list">
<div v-for="comment in discussionList" :key="comment.id" class="discussion-item">
<div class="comment-avatar">
<img src="/images/activity/1.png" :alt="comment.username" />
</div>
<div class="comment-content">
<div class="comment-header">
<span class="comment-username">{{ comment.username }}</span>
<span class="comment-badge" v-if="comment.isTeacher">讲师</span>
</div>
<div class="comment-text">{{ comment.content }}</div>
<div class="comment-images" v-if="comment.images">
<img v-for="(image, index) in comment.images" :key="index" :src="image" alt="评论图片" class="comment-image">
</div>
<div class="comment-footer">
<span class="comment-time">{{ comment.time }}</span>
<div class="discussion-comment-actions">
<button @click="likeComment(comment)" class="discussion-like-btn" :class="{ 'liked': comment.isLiked }">
<img :src="comment.isLiked ? '/opinion/赞_thumbs-up备份 2.png' : '/opinion/赞_thumbs-up.png'"
alt="点赞"
class="like-icon" />
{{ comment.likes || 0 }}
</button>
<span class="discussion-reply-text">回复</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 课程信息区域 - 练习模式和讨论模式下隐藏 -->
<div v-if="!practiceMode && !discussionMode" class="course-info-section">
<!-- 课程描述 -->
<div class="course-description">
<div class="course-description-text">本课程中的部分图片、音频和视频素材来源于网络,仅供教学使用。如有问题,请点击 <span @click="openComplaintModal('feedback')">这里</span> 反馈</div>
<span @click="openComplaintModal('complaint')" style="display: none;">稿件投诉</span>
</div>
<!-- 分隔线 -->
<div class="course-info-divider"></div>
<!-- 课程标签页 -->
<div class="course-tabs">
<div class="tab-nav">
<button class="tab-btn" :class="{ active: courseActiveTab === 'comments' }"
@click="courseActiveTab = 'comments'">评论({{ commentsCount }})</button>
<button class="tab-btn" :class="{ active: courseActiveTab === 'summary' }"
@click="courseActiveTab = 'summary'">课程总结</button>
<button class="tab-btn" :class="{ active: courseActiveTab === 'subtitles' }"
@click="courseActiveTab = 'subtitles'">字幕列表</button>
</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>
<p class="summary-description">
本次课程旨在引导学生探索自身的职业目标及价值观,强调了根据个人兴趣和优势做出职业选择的重要性。通过分享不同领域的职场榜样,如一位对C罗的远见和执行力表示赞赏的学生、另一位崇拜Elon
Musk的学习能力和创新精神、以及第三位钦佩俞敏洪的社会洞察力和团队合作精神,课程鼓励学生思考并追求自己理想中的职业生涯。这些榜样人物不仅在各自领域取得了巨大成功,而且还展现了持续学习、勇于创新和积极影响社会的价值观。
</p>
</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>
<p class="summary-description">
本次课程旨在引导学生探索自身的职业目标及价值观,强调了根据个人兴趣和优势做出职业选择的重要性。通过分享不同领域的职场榜样,如一位对C罗的远见和执行力表示赞赏的学生、另一位崇拜Elon
Musk的学习能力和创新精神、以及第三位钦佩俞敏洪的社会洞察力和团队合作精神,课程鼓励学生思考并追求自己理想中的职业生涯。这些榜样人物不仅在各自领域取得了巨大成功,而且还展现了持续学习、勇于创新和积极影响社会的价值观。
</p>
</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>
<p class="summary-description">
本次课程旨在引导学生探索自身的职业目标及价值观,强调了根据个人兴趣和优势做出职业选择的重要性。通过分享不同领域的职场榜样,如一位对C罗的远见和执行力表示赞赏的学生、另一位崇拜Elon
Musk的学习能力和创新精神、以及第三位钦佩俞敏洪的社会洞察力和团队合作精神,课程鼓励学生思考并追求自己理想中的职业生涯。这些榜样人物不仅在各自领域取得了巨大成功,而且还展现了持续学习、勇于创新和积极影响社会的价值观。
</p>
</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>
<p class="summary-description">
本次课程旨在引导学生探索自身的职业目标及价值观,强调了根据个人兴趣和优势做出职业选择的重要性。通过分享不同领域的职场榜样,如一位对C罗的远见和执行力表示赞赏的学生、另一位崇拜Elon
Musk的学习能力和创新精神、以及第三位钦佩俞敏洪的社会洞察力和团队合作精神,课程鼓励学生思考并追求自己理想中的职业生涯。这些榜样人物不仅在各自领域取得了巨大成功,而且还展现了持续学习、勇于创新和积极影响社会的价值观。
</p>
</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">
<!-- 发布评论区域 -->
<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>
</div>
</div>
<div class="comment-list">
<!-- 加载状态 -->
<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">
<div class="comment-avatar">
<SafeAvatar :src="comment.avatar" :name="comment.username" :size="40" />
</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-footer">
<span class="comment-time">2025.07.23 16:28</span>
<div class="discussion-comment-actions">
<span class="discussion-reply-text" @click="startReply(comment.id, comment.username)">回复</span>
</div>
</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>
<!-- 回复区域示例 -->
<div class="comment-replies" v-if="String(comment.id) === '1'">
<!-- 讲师回复 -->
<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>
</div>
</div>
</div>
<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-footer">
<span class="comment-time">2025.07.23 16:28</span>
<div class="discussion-comment-actions">
<button class="discussion-like-btn">
👍 0
</button>
<span class="discussion-reply-text" @click="startReply(1, '张老师')">回复</span>
</div>
</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>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 左侧边栏 -->
<div class="sidebar">
<!-- 学期显示 -->
<div class="semester-display">
<span class="semester-text">2025年上学期</span>
</div>
<!-- 开课时间 -->
<div class="course-time-info">
开课时间2025.09.01-2026.01.14
</div>
<div class="sidebar-title">
<h2>学习进度</h2>
<img src="/images/aiCompanion/fold.png" alt="">
</div>
<!-- 学习进度 -->
<div class="progress-section">
<div class="progress-header">
<h3 class="progress-title">学习进度</h3>
<p class="progress-subtitle">实时跟踪您的学习状态和完成情况</p>
</div>
<!-- 三个圆形进度图表 -->
<div class="progress-circles">
<!-- 课程进度 -->
<div class="progress-item">
<div class="circle-container">
<svg class="progress-circle" width="80" height="80" viewBox="0 0 80 80">
<!-- 背景圆环 -->
<circle cx="40" cy="40" r="32" stroke="##E2F5FF" stroke-width="8" fill="none"
class="progress-bg" />
<!-- 进度圆环 -->
<circle cx="40" cy="40" r="32" stroke="#078BD2" stroke-width="8" fill="none"
stroke-linecap="round" class="progress-fill" :stroke-dasharray="circumference"
:stroke-dashoffset="circumference - (circumference * videoProgress / 100)" />
</svg>
<!-- 中心图标和文字 -->
<div class="circle-content">
<div class="progress-icon">
<img src="/images/courses/course-icon.png" alt="课程图标" class="course-icon" />
</div>
<div class="progress-label">课程</div>
</div>
</div>
<div class="progress-percentage">{{ videoProgress.toFixed(1) }}%</div>
</div>
<!-- 作业进度 -->
<div class="progress-item">
<div class="circle-container">
<svg class="progress-circle" width="80" height="80" viewBox="0 0 80 80">
<!-- 背景圆环 -->
<circle cx="40" cy="40" r="32" stroke="#d9ecff" stroke-width="8" fill="none"
class="progress-bg" />
<!-- 进度圆环 -->
<circle cx="40" cy="40" r="32" stroke="#078BD2" stroke-width="8" fill="none"
stroke-linecap="round" class="progress-fill" :stroke-dasharray="circumference"
:stroke-dashoffset="circumference - (circumference * exerciseProgress / 100)" />
</svg>
<!-- 中心图标和文字 -->
<div class="circle-content">
<div class="progress-icon">
<img src="/images/courses/homework-icon.png" alt="作业图标" class="homework-icon" />
</div>
<div class="progress-label">作业</div>
</div>
</div>
<div class="progress-percentage">{{ exerciseProgress.toFixed(1) }}%</div>
</div>
<!-- 考试进度 -->
<div class="progress-item">
<div class="circle-container">
<svg class="progress-circle" width="80" height="80" viewBox="0 0 80 80">
<!-- 背景圆环 -->
<circle cx="40" cy="40" r="32" stroke="#d9ecff" stroke-width="8" fill="none"
class="progress-bg" />
<!-- 进度圆环 -->
<circle cx="40" cy="40" r="32" stroke="#078BD2" stroke-width="8" fill="none"
stroke-linecap="round" class="progress-fill" :stroke-dasharray="circumference"
:stroke-dashoffset="circumference - (circumference * examProgress / 100)" />
</svg>
<!-- 中心图标和文字 -->
<div class="circle-content">
<div class="progress-icon">
<img src="/images/courses/examination-icon.png" alt="考试图标" class="exam-icon" />
</div>
<div class="progress-label">考试</div>
</div>
</div>
<div class="progress-percentage">{{ examProgress.toFixed(1) }}%</div>
</div>
</div>
<!-- 总体学习进度条 -->
<div class="overall-progress">
<div class="progress-bar-container">
<div class="progress-bar">
<div class="progress-bar-fill" :style="{ width: overallProgress + '%' }"></div>
</div>
</div>
<div class="progress-info">
<div class="progress-text">
<span class="progress-title">学习总进度</span>
<span class="progress-value">{{ overallProgress.toFixed(1) }}%</span>
</div>
<div class="progress-count">
<span class="current">{{ completedLessons }}</span>
<span class="separator">/</span>
<span class="total">{{ totalSections }}</span>
</div>
</div>
</div>
</div>
<!-- 课程章节列表 -->
<div class="course-sections">
<div class="sections-header">
<div class="header-left">
<h3 class="sections-title">课程章节</h3>
</div>
<div class="header-right">
<button class="sort-btn">
<svg width="16" height="16" viewBox="0 0 16 16" class="sort-icon">
<path d="M3 3h10M3 8h7M3 13h4" stroke="currentColor" stroke-width="1.5" fill="none" />
</svg>
<span class="sort-text">正序</span>
</button>
</div>
</div>
<div class="sections-content">
<div v-if="sectionsLoading" class="sections-loading">
<p>正在加载章节列表...</p>
</div>
<div v-else-if="sectionsError" class="sections-error">
<p>{{ sectionsError }}</p>
<button @click="loadCourseSections" class="retry-btn">重试</button>
</div>
<div v-else-if="courseSections.length > 0" class="sections-list">
<!-- 按章节分组显示 - 已报名状态,彩色可点击 -->
<div v-for="(chapter, chapterIndex) in groupedSections" :key="chapterIndex" class="chapter-section">
<div class="chapter-header" @click="toggleChapter(chapterIndex)">
<div class="chapter-info">
<span class="chapter-title">第{{ getChapterNumber(chapterIndex + 1) }}章 {{ chapter.title
}}</span>
</div>
<span class="chapter-toggle" :class="{ 'expanded': chapter.expanded }">
<svg width="12" height="12" viewBox="0 0 12 12">
<path d="M4 3l4 3-4 3" stroke="currentColor" stroke-width="1.5" fill="none" />
</svg>
</span>
</div>
<div v-if="chapter.expanded" class="chapter-lessons">
<div v-for="section in chapter.sections" :key="section.id" class="lesson-item">
<!-- 已报名状态:彩色可点击 -->
<div class="lesson-content enrolled" @click="handleSectionClick(section)">
<div class="lesson-type-badge" :class="getLessonTypeBadgeClass(section)">
{{ getLessonTypeText(section) }}
</div>
<div class="lesson-info">
<span class="lesson-title">{{ section.name }}</span>
</div>
<div class="lesson-meta">
<span v-if="isVideoLesson(section)" class="lesson-duration">{{
formatLessonDuration(section) }}</span>
<div class="lesson-actions">
<!-- 视频播放图标 - 可点击 -->
<button v-if="isVideoLesson(section)" class="lesson-action-btn video-btn"
@click.stop="handleVideoPlay(section)">
<img src="/public/images/courses/video-enroll.png" alt="视频" width="14" height="14">
</button>
<!-- 下载图标 - 可点击 -->
<button v-else-if="isResourceLesson(section)" class="lesson-action-btn download-btn"
@click.stop="handleDownload(section)">
<img src="/public/images/courses/download-enroll.png" alt="资料" width="14" height="14">
</button>
<!-- 编辑图标(作业) - 可点击 -->
<button v-else-if="isHomeworkLesson(section)" class="lesson-action-btn edit-btn"
@click.stop="handleHomework(section)">
<img src="/public/images/courses/homework-enroll.png" alt="作业" width="14" height="14">
</button>
<!-- 练习图标 - 可点击 -->
<button v-else-if="isPracticeLesson(section)" class="lesson-action-btn practice-btn"
@click.stop="handlePractice(section)">
<img src="/public/images/courses/homework-enroll.png" alt="练习" width="14" height="14">
</button>
<!-- 考试图标 - 可点击 -->
<button v-else-if="isExamLesson(section)" class="lesson-action-btn exam-btn"
@click.stop="handleExam(section)">
<img src="/public/images/courses/examination-enroll.png" alt="考试" width="14"
height="14">
</button>
<!-- 讨论图标 - 可点击 -->
<button v-else-if="isDiscussionLesson(section)" class="lesson-action-btn discussion-btn"
@click.stop="handleDiscussion(section)">
<img src="/public/logo/discussion.png" alt="讨论" width="14" height="14">
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div v-else class="no-sections">
<p>暂无课程章节</p>
</div>
</div>
</div>
<!-- 更多课程 -->
<div class="more-courses">
<div class="more-courses-header">
<h3>更多课程</h3>
</div>
<div v-if="moreCoursesLoading" class="more-courses-loading">
<p>正在加载更多课程...</p>
</div>
<div v-else-if="moreCoursesError" class="more-courses-error">
<p>{{ moreCoursesError }}</p>
<button @click="loadMoreCourses" class="retry-btn">重试</button>
</div>
<div v-else-if="moreCourses.length > 0" class="more-courses-list">
<div v-for="course in moreCourses" :key="course.id" class="course-card">
<div class="course-cover">
<div class="course-image computer-bg">
<img :src="course.coverImage || course.thumbnail || '/images/courses/course-activities1.png'"
:alt="course.title"
@error="handleImageError">
</div>
</div>
<div class="course-info">
<div class="course-desc">{{ course.description || course.title }}</div>
<div class="course-stats">
<span class="stats-item">
<i class="icon-chapters"></i>
共{{ course.chaptersCount || 0 }}章{{ course.lessonsCount || 0 }}节
</span>
<span class="stats-item">
<i class="icon-duration"></i>
{{ formatDuration(course.duration) }}
</span>
</div>
<div class="course-footer">
<span class="enrolled-count">{{ course.enrolledCount || 0 }}人已报名</span>
<button class="btn-enroll-course" @click="handleEnrollCourse(course)">去报名</button>
</div>
</div>
</div>
</div>
<div v-else class="no-more-courses">
<p>暂无更多课程</p>
</div>
</div>
</div>
</div>
<!-- AI助手界面 - 仅在AI伴学模式下显示练习模式和讨论模式下隐藏 -->
<div v-if="(course as any)?.izAi === 1 && !practiceMode && !discussionMode" class="ai-assistant-interface">
<!-- <div class="banner-button">
<img src="/images/aiCompanion/切换@2x.png" alt="切换" class="button-icon-image">
<span class="button-text">普通</span>
</div> -->
<!-- AI主要内容区域 -->
<div v-if="showAiAssistant" 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" @click="hideAiAssistant">
<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" ref="chatMessagesContainer">
<!-- 动态聊天消息 -->
<div
v-for="msg in chatMessages"
:key="msg.id"
class="message"
:class="msg.type === 'ai' ? 'ai-message' : 'user-message'"
>
<div class="message-avatar">
<img
:src="msg.type === 'ai' ? '/images/aiCompanion/AI小助手@2x.png' : 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-4.0.3&auto=format&fit=crop&w=50&q=80'"
:alt="msg.type === 'ai' ? 'AI小助手' : '用户'"
>
</div>
<div class="message-content">
<div class="message-bubble">
<div v-if="msg.content" class="message-text" v-html="formatMessageContent(msg.content)"></div>
<div v-if="msg.isStreaming" class="typing-indicator">
<span></span>
<span></span>
<span></span>
</div>
</div>
<div class="message-time">{{ msg.timestamp }}</div>
</div>
</div>
<!-- 只在没有对话时显示AI建议 -->
<div v-if="chatMessages.length === 0" 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>
</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
v-model="chatMessage"
placeholder="请输入您的问题..."
class="chat-input"
:disabled="isAISending"
@keyup.enter="!isAISending && sendMessage()"
></textarea>
<button
class="send-button"
:disabled="isAISending || !chatMessage.trim()"
@click="sendMessage"
>
<img
src="/images/aiCompanion/发送@2x.png"
alt="发送"
class="send-icon"
:class="{ 'disabled': isAISending || !chatMessage.trim() }"
>
</button>
</div>
</div>
</div>
<!-- 我的笔记界面 -->
<div v-if="aiActiveTab === 'notes'" class="notes-interface">
<div class="notes-header">
<h4>我的笔记</h4>
<button class="add-note-btn" @click="showNoteEditor = true">
<img src="/images/aiCompanion/记录@2x.png" alt="添加笔记" class="add-icon">
<span>添加笔记</span>
</button>
</div>
<!-- 富文本编辑器 -->
<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>
</div>
</div>
<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">
<div class="note-header">
<span class="note-title">{{ note.title }}</span>
<span class="note-date">{{ note.date }}</span>
</div>
<div class="note-content" v-html="note.content"></div>
<div class="note-actions">
<button class="note-action-btn" @click="editNote(index)">编辑</button>
<button class="note-action-btn" @click="deleteNote(index)">删除</button>
</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>
<!-- 下载确认弹窗 -->
<n-modal v-model:show="downloadConfirmVisible" style="width: 400px;">
<div class="download-confirm-modal">
<div class="download-confirm-header">
<div class="download-confirm-title">确认下载</div>
</div>
<div class="download-confirm-content">
<p>确定要下载该资料吗</p>
<p v-if="pendingDownloadSection" class="download-section-name">
{{ pendingDownloadSection.name }}
</p>
</div>
<div class="download-confirm-actions">
<n-button @click="cancelDownload" style="margin-right: 12px;">
取消
</n-button>
<n-button type="primary" @click="confirmDownload">
确认下载
</n-button>
</div>
</div>
</n-modal>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onActivated } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useMessage } from 'naive-ui'
// import { useAuth } from '@/composables/useAuth'
// import { useUserStore } from '@/stores/user'
import { CourseApi } from '@/api/modules/course'
import { CommentApi } from '@/api/modules/comment'
import { AIApi } from '@/api/modules/ai'
import type { Course, CourseSection, CourseComment } from '@/api/types'
import QuillEditor from '@/components/common/QuillEditor.vue'
import DPlayerVideo from '@/components/course/DPlayerVideo.vue'
import SafeAvatar from '@/components/common/SafeAvatar.vue'
import SemiCircleProgress from '@/components/common/SemiCircleProgress.vue'
// import LoginModal from '@/components/auth/LoginModal.vue'
// import RegisterModal from '@/components/auth/RegisterModal.vue'
const route = useRoute()
const router = useRouter()
// const userStore = useUserStore()
const message = useMessage()
const courseId = ref(String(route.params.id))
// const { loginModalVisible, registerModalVisible, handleAuthSuccess, showLoginModal } = useAuth()
// enrollCourse 暂时未使用,后续需要时再启用
// 当前选中的章节
const currentSection = ref<CourseSection | null>(null)
// 课程数据相关状态
const course = ref<Course | null>(null)
const loading = ref(false)
const error = ref('')
// 课程章节数据
const courseSections = ref<CourseSection[]>([])
const sectionsLoading = ref(false)
const sectionsError = ref('')
// 报名状态管理
const isEnrolled = ref(false) // 用户是否已报名该课程
const enrollmentLoading = ref(false) // 报名加载状态
// 报名状态
// const RegistrationStatus = ref(false)
// 学习进度相关数据
const completedLessons = ref(0)
const videoProgress = ref(0)
const exerciseProgress = ref(0)
const examProgress = ref(0)
// 进度数据
const progressData = ref<any>(null)
// 处理记笔记点击事件
// const handleNotesClick = () => {
// if (isUserEnrolled.value) {
// // 已报名,执行记笔记逻辑
// console.log('开始记笔记')
// // 这里可以添加打开笔记模态框的逻辑
// } else if (userStore.isLoggedIn) {
// // 已登录但未报名,提示去报名
// enrollConfirmVisible.value = true
// } else {
// // 未登录,显示登录模态框
// showLoginModal()
// }
// }
// 计算用户是否已报名
// const isUserEnrolled = computed(() => {
// // 必须同时满足:用户已登录 AND 已报名该课程
// return userStore.isLoggedIn && isEnrolled.value
// // 临时测试不同状态:
// // return false // 强制显示未报名状态(灰色不可点击)
// // return true // 强制显示已报名状态(彩色可点击)
// })
// 报名确认弹窗
const enrollConfirmVisible = ref(false)
const enrollSuccessVisible = ref(false)
// 刷新状态
const isRefreshing = ref(false)
// AI建议相关
const sendPrompt = (prompt: string) => {
chatMessage.value = prompt
sendMessage()
}
// 隐藏区域的函数
const hideTipSection = () => {
showTipSection.value = false
console.log('隐藏提示区域')
}
const hideAiAssistant = () => {
showAiAssistant.value = false
console.log('隐藏AI助手')
}
// 章节分组数据
interface ChapterGroup {
title: string
sections: CourseSection[]
expanded: boolean
}
const groupedSections = ref<ChapterGroup[]>([])
// 将章节按章分组 - 根据后端数据结构重新实现
const groupSectionsByChapter = (sections: CourseSection[]) => {
console.log('🔍 开始分组章节数据:', sections)
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
})
}
console.log('✅ 章节分组完成:', groups)
return groups
}
// 根据章节数据生成分组
// const generateChapterGroups = () => {
// // 确保有章节数据
// if (courseSections.value.length === 0) {
// console.log('没有章节数据,生成模拟数据')
// courseSections.value = generateMockSections()
// }
// console.log('开始生成章节分组,原始数据:', courseSections.value)
// console.log('章节数据数量:', courseSections.value.length)
// // 使用统一的分组函数
// groupedSections.value = groupSectionsByChapter(courseSections.value)
// console.log('生成的章节分组:', groupedSections.value)
// }
// 获取章节标题已弃用使用groupSectionsByChapter替代
// const getChapterTitle = (chapterIndex: number): string => {
// const titles = [
// '课前准备',
// '程序设计基础知识',
// '程序的控制结构',
// '大话吉模型介绍',
// 'DeepSeek实际应用',
// 'DeepSeek实际应用'
// ]
// return titles[chapterIndex - 1] || '课程内容'
// }
// 预览模态框相关数据
const previewModalVisible = ref(false)
const previewModalTitle = ref('')
const previewModalContent = ref('')
const previewModalType = ref('')
// 视频播放相关
// const VIDEO_CONFIG = {
// // 本地视频(当前使用)
// LOCAL: '/video/first.mp4',
// // HLS流服务器准备好后使用
// HLS: 'http://110.42.96.65:55513/learn/index.m3u8'
// }
// 获取视频URL的函数
// const getVideoUrl = (section?: CourseSection) => {
// const outline = section?.outline?.trim()
// if (outline && (outline.endsWith('.mp4') || outline.endsWith('.m3u8'))) {
// return outline
// }
// // 当前使用本地视频,将来可以通过环境变量或配置切换
// return VIDEO_CONFIG.LOCAL
// }
// 视频播放相关状态
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)
// 练习模式状态
const practiceMode = ref(false)
const currentPracticeSection = ref<CourseSection | null>(null)
const practiceQuestions = ref<any[]>([])
const currentQuestionIndex = ref(0)
const practiceAnswers = ref<any[]>([])
const fillAnswers = ref<any[]>([])
const essayAnswers = ref<any[]>([])
const practiceStarted = ref(false)
const practiceFinished = ref(false)
// 讨论模式状态
const discussionMode = ref(false)
const currentDiscussionSection = ref<CourseSection | null>(null)
const discussionList = ref<any[]>([])
const newComment = ref('')
const replyingTo = ref<any>(null)
// 新增的响应式数据
const aiActiveTab = ref('assistant')
const courseActiveTab = ref('summary')
// 控制区域显示状态
const showTipSection = ref(true)
const showAiAssistant = ref(true)
// 更多课程相关状态
const moreCourses = ref<any[]>([])
const moreCoursesLoading = ref(false)
const moreCoursesError = ref('')
// 讲师数据
// const instructors = ref([
// {
// id: 1,
// name: '汪波',
// title: '教授',
// avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-4.0.3&auto=format&fit=crop&w=60&q=80'
// },
// {
// id: 2,
// name: '汪波',
// title: '教授',
// avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?ixlib=rb-4.0.3&auto=format&fit=crop&w=60&q=80'
// },
// {
// id: 3,
// name: '汪波',
// title: '教授',
// avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?ixlib=rb-4.0.3&auto=format&fit=crop&w=60&q=80'
// }
// ])
// 计算属性
// const totalLessons = computed(() => {
// return groupedSections.value.length
// })
const totalSections = computed(() => {
return courseSections.value.length
})
// 计算圆环周长
const circumference = computed(() => 2 * Math.PI * 32) // r=32
// 计算总体进度
const overallProgress = computed(() => {
if (totalSections.value === 0) return 0
return (completedLessons.value / totalSections.value) * 100
})
// const formatTotalDuration = () => {
// // 计算总时长
// let totalMinutes = 0
// courseSections.value.forEach((section: CourseSection) => {
// if (section.duration) {
// const parts = section.duration.split(':')
// if (parts.length === 3) {
// const hours = parseInt(parts[0])
// const minutes = parseInt(parts[1])
// totalMinutes += hours * 60 + minutes
// }
// }
// })
// const hours = Math.floor(totalMinutes / 60)
// const minutes = totalMinutes % 60
// return `${hours}小时${minutes}分钟`
// }
// 真实评论数据
const 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
})
// 自动调整textarea高度但不影响固定高度的textarea
const adjustTextareaHeight = (event: Event) => {
const textarea = event.target as HTMLTextAreaElement
// 如果textarea有comment-textarea类且在发布评论区域保持固定高度36px
if (textarea.classList.contains('comment-textarea') && textarea.closest('.post-comment-section')) {
textarea.style.height = '36px'
return
}
// 其他textarea正常调整高度
textarea.style.height = 'auto'
textarea.style.height = textarea.scrollHeight + 'px'
}
// 点击textarea时调整高度
const handleTextareaClick = (event: MouseEvent) => {
const textarea = event.target as HTMLTextAreaElement
// 如果textarea有comment-textarea类且在发布评论区域保持固定高度36px
if (textarea.classList.contains('comment-textarea') && textarea.closest('.post-comment-section')) {
textarea.style.height = '36px'
return
}
// 其他textarea正常调整高度
if (textarea.style.height === '40px' || textarea.style.height === '') {
textarea.style.height = '60px'
}
}
// 加载课程评论列表
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
}
}
// 提交评论函数
const submitComment = async () => {
if (!newComment.value.trim()) {
message.warning('请输入评论内容')
return
}
try {
console.log('🚀 开始提交评论:', newComment.value)
// 调用评论API
const response = await CommentApi.postCourseComment(courseId.value, {
content: newComment.value.trim(),
imgs: '' // 暂时不支持图片,可以后续扩展
})
console.log('✅ 评论提交成功:', response)
// 检查响应数据结构
if (response.data && response.data.code === 200 && response.data.success) {
message.success('评论发布成功')
newComment.value = ''
// 重新加载评论列表
await loadCourseComments()
} else {
message.error(response.data?.message || '评论发布失败')
}
} catch (error) {
console.error('❌ 提交评论失败:', error)
message.error('评论发布失败,请稍后重试')
}
}
// 回复相关函数
const startReply = (commentId: string | number, username: string) => {
replyingTo.value = commentId
replyToUsername.value = username
replyText.value = ''
}
const cancelReply = () => {
replyingTo.value = null
replyToUsername.value = ''
replyText.value = ''
}
// 返回到视频页面
const goBackToVideo = () => {
// 退出练习模式和讨论模式,返回到正常的课程视频页面
practiceMode.value = false
discussionMode.value = false
practiceStarted.value = false
practiceFinished.value = false
// 清除当前练习相关状态
currentPracticeSection.value = null
currentQuestionIndex.value = 0
console.log('🔙 返回到视频页面')
}
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'
}
}
// 回复相关数据
const replyText = ref('')
const replyToUsername = ref('')
// 处理视频播放
const handleVideoPlay = async (section: CourseSection) => {
console.log('🎬 点击视频播放按钮:', section.name)
console.log('🔍 当前状态:', {
practiceMode: practiceMode.value,
discussionMode: discussionMode.value,
sectionId: section.id
})
// 如果当前在练习模式或讨论模式,先退出这些模式
if (practiceMode.value) {
console.log('🔄 退出练习模式,切换到视频播放')
exitPractice()
}
if (discussionMode.value) {
console.log('🔄 退出讨论模式,切换到视频播放')
exitDiscussion()
}
console.log('🔍 模式切换后状态:', {
practiceMode: practiceMode.value,
discussionMode: discussionMode.value
})
// 加载章节视频数据
await loadSectionVideo(section)
// 标记为已完成
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
console.log('🔍 原始视频数据:', video)
console.log('🔍 原始清晰度数据:', video.qualities)
console.log('🔍 原始默认清晰度:', video.defaultQuality)
// 设置视频清晰度选项
videoQualities.value = video.qualities || []
currentQuality.value = video.defaultQuality || '360'
console.log('🔍 处理后的清晰度数据:', videoQualities.value)
console.log('🔍 处理后的当前清晰度:', currentQuality.value)
// 验证清晰度数据格式
if (videoQualities.value.length > 0) {
const firstQuality = videoQualities.value[0]
console.log('🔍 第一个清晰度对象结构:', firstQuality)
console.log('🔍 是否有必要字段:', {
hasValue: 'value' in firstQuality,
hasLabel: 'label' in firstQuality,
hasUrl: 'url' in firstQuality
})
// 检查所有清晰度数据
videoQualities.value.forEach((quality, index) => {
console.log(`🔍 清晰度 ${index}:`, {
value: quality.value,
label: quality.label,
url: quality.url,
urlValid: !!quality.url && quality.url.length > 0
})
})
}
console.log('🔍 即将传递给DPlayerVideo的props:', {
videoQualities: videoQualities.value,
currentQuality: currentQuality.value,
qualitiesCount: videoQualities.value.length
})
// 获取默认清晰度的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('✅ 默认清晰度:', video.defaultQuality)
console.log('✅ 传递给DPlayer的清晰度:', videoQualities.value)
} else {
// 如果找不到默认清晰度,使用第一个可用的清晰度
if (video.qualities && video.qualities.length > 0) {
currentVideoUrl.value = video.qualities[0].url
currentVideoSection.value = section
console.log('✅ 使用第一个可用清晰度:', video.qualities[0])
}
}
} 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
}
} catch (error) {
console.error('❌ 加载章节视频失败:', error)
// 使用默认视频
currentVideoUrl.value = 'https://example.com/default-video.mp4'
currentVideoSection.value = section
} finally {
videoLoading.value = false
}
}
// DPlayer 事件处理函数
const onVideoPlay = () => {
console.log('视频开始播放')
}
const onVideoPause = () => {
console.log('视频暂停')
}
const onVideoEnded = () => {
console.log('视频播放结束')
}
const onVideoError = (error: Event) => {
console.error('视频播放出错:', error)
}
// 新增的事件处理函数
const onScreenshot = (dataUrl: string) => {
console.log('截屏成功:', dataUrl)
// 可以在这里添加截屏成功的提示
message.success('截屏成功!')
}
const onDanmakuSend = (text: string) => {
console.log('发送弹幕:', text)
// 可以在这里添加弹幕发送的逻辑,比如保存到数据库
}
// 清晰度切换事件处理
const onQualityChange = (newQuality: string) => {
console.log('🔄 清晰度已切换到:', newQuality)
console.log('🔍 当前可用清晰度:', videoQualities.value)
// 更新当前清晰度
currentQuality.value = newQuality
// 查找新清晰度对应的URL
const newQualityVideo = videoQualities.value.find((q: any) => q.value === newQuality)
if (newQualityVideo) {
console.log('✅ 找到新清晰度视频:', newQualityVideo)
currentVideoUrl.value = newQualityVideo.url
} else {
console.warn('⚠️ 未找到对应清晰度的视频URL:', newQuality)
}
}
// 加载课程详情
const loadCourseDetail = async () => {
console.log('开始加载课程详情课程ID:', courseId.value)
if (!courseId.value) {
courseId.value = '1'
}
if (!courseId.value || isNaN(Number(courseId.value))) {
console.log('课程ID无效')
error.value = '课程ID无效'
return
}
try {
loading.value = true
error.value = ''
console.log('调用API获取课程详情课程ID:', courseId.value)
const response = await CourseApi.getCourseById(courseId.value)
console.log('API响应:', response)
if (response.code === 0 || response.code === 200) {
course.value = response.data
console.log('课程数据设置成功:', course.value)
console.log('课程AI模式:', (course.value as any)?.izAi)
// 加载课程学习进度
await loadCourseProgress()
// 确保讲师和时长信息正确显示
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 || '获取课程详情失败'
}
} catch (err) {
console.error('加载课程详情失败:', err)
error.value = '网络错误,请稍后重试'
} finally {
loading.value = false
}
}
// 加载课程章节列表
const loadCourseSections = async () => {
if (!courseId.value || isNaN(Number(courseId.value))) {
sectionsError.value = '课程ID无效'
console.error('课程ID无效:', courseId.value)
return
}
try {
sectionsLoading.value = true
sectionsError.value = ''
console.log('调用API获取课程章节...')
const response = await CourseApi.getCourseSections(courseId.value)
console.log('章节API响应:', response)
if (response.code === 0 || response.code === 200) {
if (response.data && response.data.list && Array.isArray(response.data.list)) {
console.log('✅ API返回的原始章节数据:', response.data.list)
console.log('✅ 章节数据数量:', response.data.list.length)
// 添加模拟练习章节
const sectionsWithPractice = [...response.data.list]
// 添加一个练习章节到第一章
const practiceSection: CourseSection = {
id: '999999', // 使用一个特殊的ID
lessonId: '999999',
name: 'JavaScript基础练习',
type: 5, // 练习类型
level: 2, // 二级章节
parentId: sectionsWithPractice.find(s => s.level === 1)?.id || '1', // 找到第一个父章节
duration: '30分钟',
completed: false,
outline: '',
sort: 999,
revision: 1,
createdAt: Date.now(),
updatedAt: Date.now(),
deletedAt: null
}
// 将练习章节插入到合适的位置(第一章的最后)
const firstChapterSections = sectionsWithPractice.filter(s => s.level === 2 && s.parentId === practiceSection.parentId)
if (firstChapterSections.length > 0) {
// 插入到第一章的最后一个章节后面
const insertIndex = sectionsWithPractice.findIndex(s => s.id === firstChapterSections[firstChapterSections.length - 1].id) + 1
sectionsWithPractice.splice(insertIndex, 0, practiceSection)
} else {
// 如果没有找到合适位置,就添加到最后
sectionsWithPractice.push(practiceSection)
}
// 添加一个讨论章节
const discussionSection: CourseSection = {
id: '999998', // 使用另一个特殊的ID
lessonId: '999998',
name: '机器学习与科研流程讨论',
type: 6, // 讨论类型(自定义)
level: 2, // 二级章节
parentId: sectionsWithPractice.find(s => s.level === 1)?.id || '1', // 找到第一个父章节
duration: '讨论',
completed: false,
outline: '',
sort: 998,
revision: 1,
createdAt: Date.now(),
updatedAt: Date.now(),
deletedAt: null
}
// 将讨论章节插入到练习章节后面
const practiceIndex = sectionsWithPractice.findIndex(s => s.id === practiceSection.id)
if (practiceIndex !== -1) {
sectionsWithPractice.splice(practiceIndex + 1, 0, discussionSection)
} else {
sectionsWithPractice.push(discussionSection)
}
courseSections.value = sectionsWithPractice
groupedSections.value = groupSectionsByChapter(sectionsWithPractice)
console.log('✅ 设置后的courseSections:', courseSections.value)
console.log('✅ 设置后的groupedSections:', groupedSections.value)
console.log('✅ groupedSections长度:', groupedSections.value.length)
console.log('✅ 已添加练习章节:', practiceSection)
} else {
console.log('❌ API返回的章节数据为空或格式错误')
console.log('❌ response.data:', response.data)
sectionsError.value = '暂无课程章节数据'
}
} else {
console.log('API返回错误')
sectionsError.value = response.message || '获取课程章节失败'
}
} catch (err) {
console.error('加载课程章节失败:', err)
sectionsError.value = '网络错误,请稍后重试'
} 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 isPracticeLesson = (section: CourseSection) => {
// 优先根据type字段判断4=练习
if (section.type === 4) {
return true
}
// 如果type为null则根据名称判断
return section.name.includes('练习')
}
// 获取课程类型文本
const getLessonTypeText = (section: CourseSection) => {
if (isVideoLesson(section)) return '视频'
if (isResourceLesson(section)) return '资料'
if (isPracticeLesson(section)) return '练习' // 练习判断放在作业前面
if (isHomeworkLesson(section)) return '作业'
if (isExamLesson(section)) return '考试'
if (isDiscussionLesson(section)) return '讨论'
return '视频'
}
// 获取章节编号
const getChapterNumber = (num: number) => {
const numbers = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十']
return numbers[num - 1] || num.toString()
}
// 格式化课程时长
const formatLessonDuration = (section: CourseSection) => {
if (!section.duration) return ''
return section.duration
}
// 课程类型判断函数 - 与CourseDetailEnrolled保持一致
const isVideoLesson = (section: CourseSection) => {
console.log('检查章节类型:', section.name, 'type:', section.type, 'outline:', section.outline)
// 优先根据type字段判断0=视频
if (section.type === 0) {
return true
}
// 如果type为null则根据outline判断
return section.outline && (section.outline.includes('.m3u8') || section.outline.includes('.mp4'))
}
const isResourceLesson = (section: CourseSection) => {
// 优先根据type字段判断1=资料
if (section.type === 1) {
return true
}
// 如果type为null则根据outline或名称判断
return section.outline && (section.outline.includes('.pdf') || section.outline.includes('.ppt') || section.outline.includes('.zip'))
}
const isHomeworkLesson = (section: CourseSection) => {
// 优先根据type字段判断3=作业
if (section.type === 3) {
return true
}
// 如果type为null则根据名称判断只判断作业不包括练习
return section.name.includes('作业')
}
const isExamLesson = (section: CourseSection) => {
// 优先根据type字段判断2=考试
if (section.type === 2) {
return true
}
// 如果type为null则根据名称判断
return section.name.includes('考试') || section.name.includes('测试')
}
const isDiscussionLesson = (section: CourseSection) => {
// 优先根据type字段判断5=讨论
if (section.type === 5) {
return true
}
// 如果type为null则根据名称判断
return section.name.includes('讨论')
}
// 获取课程类型样式类
const getLessonTypeBadgeClass = (section: CourseSection) => {
if (isVideoLesson(section)) return 'badge-video'
if (isResourceLesson(section)) return 'badge-resource'
if (isPracticeLesson(section)) return 'badge-practice' // 练习判断放在作业前面
if (isHomeworkLesson(section)) return 'badge-homework'
if (isExamLesson(section)) return 'badge-exam'
if (isDiscussionLesson(section)) return 'badge-discussion'
return 'badge-default'
}
// 处理下载操作
const handleDownload = async (section: CourseSection) => {
console.log('📄 点击下载按钮:', section)
// 显示确认弹窗
pendingDownloadSection.value = section
downloadConfirmVisible.value = true
}
// 确认下载
const confirmDownload = async () => {
if (!pendingDownloadSection.value) return
const section = pendingDownloadSection.value
console.log('📄 确认下载章节资料:', section)
// 关闭确认弹窗
downloadConfirmVisible.value = false
pendingDownloadSection.value = null
try {
// 调用章节资料API
const response = await CourseApi.getSectionDocument(courseId.value, section.id.toString())
if (response.data && (response.data.code === 200 || response.data.code === 0)) {
console.log('✅ 获取章节资料成功:', response.data)
const documents = response.data.result || []
if (documents.length === 0) {
message.warning('该章节暂无资料')
return
}
// 处理所有资料文件
documents.forEach((doc: any, index: number) => {
if (doc.fileUrl) {
setTimeout(() => {
const fileName = doc.name || `${section.name}_资料${index + 1}`
const fileExtension = getFileExtension(doc.fileUrl)
// 先在新标签页中打开预览
openFilePreview(doc.fileUrl, fileName)
// 对于某些文件类型,自动下载到本地
if (shouldAutoDownload(fileExtension)) {
setTimeout(() => {
downloadFile(doc.fileUrl, fileName)
}, 1000) // 延迟1秒下载让预览先打开
}
}, index * 500) // 延迟处理,避免浏览器阻止多个操作
}
})
const downloadableCount = documents.filter((doc: any) =>
shouldAutoDownload(getFileExtension(doc.fileUrl))
).length
if (downloadableCount > 0) {
message.success(`正在打开预览并下载 ${downloadableCount} 个资料文件`)
} else {
message.success(`正在打开 ${documents.length} 个资料文件预览`)
}
} else {
console.error('❌ 获取章节资料失败:', response.data?.message || response.message)
message.error(response.data?.message || response.message || '获取资料失败')
}
} catch (error) {
console.error('❌ 获取章节资料异常:', error)
message.error('获取资料失败,请稍后重试')
}
}
// 获取文件扩展名
const getFileExtension = (url: string): string => {
try {
const pathname = new URL(url).pathname
const extension = pathname.split('.').pop()?.toLowerCase() || ''
return extension
} catch (error) {
// 如果URL解析失败尝试从字符串中提取
const parts = url.split('.')
return parts.length > 1 ? parts.pop()?.toLowerCase() || '' : ''
}
}
// 判断是否需要自动下载
const shouldAutoDownload = (extension: string): boolean => {
// 对于这些文件类型,自动下载到本地
const downloadableTypes = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'zip', 'rar', '7z']
return downloadableTypes.includes(extension)
}
// 在新标签页中打开文件预览
const openFilePreview = (url: string, filename: string) => {
try {
console.log('👁️ 打开文件预览:', filename, url)
// 在新标签页中打开文件
window.open(url, '_blank')
console.log('✅ 文件预览已打开:', filename)
} catch (error) {
console.error('❌ 打开文件预览失败:', error)
message.error(`打开文件预览失败: ${filename}`)
}
}
// 下载文件的辅助函数
const downloadFile = (url: string, filename: string) => {
try {
console.log('📥 开始下载文件:', filename, url)
// 创建一个隐藏的a标签来触发下载
const link = document.createElement('a')
link.href = url
link.download = filename
link.target = '_blank'
link.style.display = 'none'
// 添加到DOM并点击
document.body.appendChild(link)
link.click()
// 清理
setTimeout(() => {
document.body.removeChild(link)
}, 100)
console.log('✅ 文件下载已触发:', filename)
} catch (error) {
console.error('❌ 下载文件失败:', error)
message.error(`下载文件失败: ${filename}`)
}
}
// 处理作业操作
const handleHomework = async (section: CourseSection) => {
console.log('📝 获取章节作业:', section)
try {
// 调用章节作业API
const response = await CourseApi.getSectionHomework(courseId.value, section.id.toString())
if (response.data && (response.data.code === 200 || response.data.code === 0)) {
console.log('✅ 获取章节作业成功:', response.data)
// 跳转到练习页面
router.push({
name: 'Practice',
params: {
courseId: courseId.value,
sectionId: section.id.toString()
},
query: {
courseName: course.value?.title || '课程名称',
practiceName: section.name,
homeworkData: JSON.stringify(response.data.result)
}
})
} else {
console.error('❌ 获取章节作业失败:', response.data?.message || response.message)
message.error(response.data?.message || response.message || '获取作业失败')
}
} catch (error) {
console.error('❌ 获取章节作业异常:', error)
message.error('获取作业失败,请稍后重试')
}
}
// 处理练习操作
const handlePractice = async (section: CourseSection) => {
console.log('📝 开始练习:', section)
// 退出讨论模式
if (discussionMode.value) {
exitDiscussion()
}
try {
// 调用章节练习API
const response = await CourseApi.getSectionExercise(courseId.value, section.id.toString())
if (response.data && (response.data.code === 200 || response.data.code === 0)) {
console.log('✅ 获取章节练习成功:', response.data)
// 处理练习数据
const exerciseData = response.data.result
if (exerciseData && Array.isArray(exerciseData)) {
// 设置练习数据
practiceQuestions.value = exerciseData
currentPracticeSection.value = section
practiceMode.value = true
practiceStarted.value = true // 直接开始练习,不需要点击开始按钮
practiceFinished.value = false
currentQuestionIndex.value = 0
// 初始化答案数组
practiceAnswers.value = new Array(exerciseData.length).fill(null).map(() => [])
fillAnswers.value = new Array(exerciseData.length).fill(null).map(() => [])
essayAnswers.value = new Array(exerciseData.length).fill('')
console.log('✅ 练习模式已启动,题目数量:', practiceQuestions.value.length)
} else {
console.warn('⚠️ 练习数据格式异常:', exerciseData)
message.warning('练习数据格式异常')
}
} else {
console.error('❌ 获取章节练习失败:', response.data?.message || response.message)
message.error(response.data?.message || response.message || '获取练习失败')
}
} catch (error) {
console.error('❌ 获取章节练习异常:', error)
message.error('获取练习失败,请稍后重试')
}
}
// 练习相关计算属性和方法
const currentPracticeQuestion = computed(() => {
if (practiceQuestions.value.length > 0 && currentQuestionIndex.value >= 0) {
return practiceQuestions.value[currentQuestionIndex.value]
}
return null
})
// 退出练习
const exitPractice = () => {
console.log('🚪 正在退出练习模式...')
practiceMode.value = false
practiceStarted.value = false
practiceFinished.value = false
currentPracticeSection.value = null
practiceQuestions.value = []
console.log('✅ 已退出练习模式practiceMode:', practiceMode.value)
}
// 获取练习题目类型简称
const getPracticeQuestionTypeShort = (type: string) => {
const typeMap: { [key: string]: string } = {
'单选题': '单选',
'多选题': '多选',
'判断题': '判断',
'填空题': '填空',
'简答题': '简答'
}
return typeMap[type] || type
}
// 判断练习选项是否被选中
const isPracticeOptionSelected = (optionIndex: number) => {
const answers = practiceAnswers.value[currentQuestionIndex.value] || []
return answers.includes(optionIndex)
}
// 选择练习选项
const selectPracticeOption = (optionIndex: number) => {
const questionType = currentPracticeQuestion.value?.type
if (!practiceAnswers.value[currentQuestionIndex.value]) {
practiceAnswers.value[currentQuestionIndex.value] = []
}
if (questionType === '单选题' || questionType === '判断题') {
// 单选题和判断题只能选一个
practiceAnswers.value[currentQuestionIndex.value] = [optionIndex]
} else if (questionType === '多选题') {
// 多选题可以选多个
const answers = practiceAnswers.value[currentQuestionIndex.value]
const index = answers.indexOf(optionIndex)
if (index > -1) {
answers.splice(index, 1)
} else {
answers.push(optionIndex)
}
}
}
// 处理练习复选框点击
const handlePracticeCheckboxClick = (optionIndex: number, event: Event) => {
event.stopPropagation()
selectPracticeOption(optionIndex)
}
// 获取练习填空答案
const getPracticeFillAnswer = (questionIndex: number, blankIndex: number) => {
if (!fillAnswers.value[questionIndex]) {
fillAnswers.value[questionIndex] = []
}
return fillAnswers.value[questionIndex][blankIndex] || ''
}
// 设置练习填空答案
const setPracticeFillAnswer = (questionIndex: number, blankIndex: number, value: string) => {
if (!fillAnswers.value[questionIndex]) {
fillAnswers.value[questionIndex] = []
}
fillAnswers.value[questionIndex][blankIndex] = value
}
// 获取练习简答题字数
const getPracticeEssayLength = (questionIndex: number) => {
return essayAnswers.value[questionIndex]?.length || 0
}
// 上一题
const previousPracticeQuestion = () => {
if (currentQuestionIndex.value > 0) {
currentQuestionIndex.value--
}
}
// 下一题
const nextPracticeQuestion = () => {
if (currentQuestionIndex.value < practiceQuestions.value.length - 1) {
currentQuestionIndex.value++
}
}
// 退出练习模式
const exitPracticeMode = () => {
console.log('🔙 退出练习模式,返回学习')
// 重置练习相关状态
practiceMode.value = false
practiceStarted.value = false
practiceFinished.value = false
currentQuestionIndex.value = 0
practiceQuestions.value = []
practiceAnswers.value = []
fillAnswers.value = []
essayAnswers.value = []
currentPracticeSection.value = null
message.success('已退出练习模式')
}
// 取消下载
const cancelDownload = () => {
downloadConfirmVisible.value = false
pendingDownloadSection.value = null
}
const getCurrentPracticeScore = () => {
// 这里可以根据实际答题情况计算得分
// 暂时返回0实际应用中需要根据正确答案计算
return 0
}
const getAnsweredCount = () => {
let count = 0
practiceQuestions.value.forEach((question, index) => {
if (question.type === '单选题' || question.type === '多选题' || question.type === '判断题') {
if (practiceAnswers.value[index] && practiceAnswers.value[index].length > 0) {
count++
}
} else if (question.type === '填空题') {
if (fillAnswers.value[index] && fillAnswers.value[index].some((answer: string) => answer.trim())) {
count++
}
} else if (question.type === '简答题') {
if (essayAnswers.value[index] && essayAnswers.value[index].trim()) {
count++
}
}
})
return count
}
const getSingleChoiceScore = () => {
// 计算单选题得分
return 0
}
const getSingleChoiceTotal = () => {
return practiceQuestions.value.filter(q => q.type === '单选题').length
}
const getMultiChoiceScore = () => {
// 计算多选题得分
return 0
}
const getMultiChoiceTotal = () => {
return practiceQuestions.value.filter(q => q.type === '多选题').length
}
const getJudgeScore = () => {
// 计算判断题得分
return 0
}
const getJudgeTotal = () => {
return practiceQuestions.value.filter(q => q.type === '判断题').length
}
// 计算各题型进度百分比
const getSingleChoiceProgress = () => {
const total = getSingleChoiceTotal()
if (total === 0) return 0
return Math.round((getSingleChoiceScore() / total) * 100)
}
const getMultiChoiceProgress = () => {
const total = getMultiChoiceTotal()
if (total === 0) return 0
return Math.round((getMultiChoiceScore() / total) * 100)
}
const getJudgeProgress = () => {
const total = getJudgeTotal()
if (total === 0) return 0
return Math.round((getJudgeScore() / total) * 100)
}
// 获取练习总分
const getTotalPracticeScore = () => {
return practiceQuestions.value.reduce((total, question) => {
return total + (question.score || 0)
}, 0)
}
// 处理考试操作
const handleExam = (section: CourseSection) => {
console.log('开始考试:', section)
// 跳转到考前须知页面
router.push({
name: 'ExamNotice',
params: {
courseId: courseId.value,
sectionId: section.id
},
query: {
courseName: course.value?.title || '课程名称',
examName: section.name
}
})
}
// 讨论相关方法
const handleDiscussion = (section: CourseSection) => {
console.log('进入讨论模式:', section.name)
// 退出练习模式
if (practiceMode.value) {
exitPractice()
}
discussionMode.value = true
currentDiscussionSection.value = section
loadDiscussionData(section)
}
const loadDiscussionData = async (section: CourseSection) => {
try {
console.log('🗣️ 加载讨论数据:', section.name)
// 调用章节讨论API
const response = await CourseApi.getSectionDiscussion(courseId.value, section.id.toString())
if (response.data && (response.data.code === 200 || response.data.code === 0)) {
console.log('✅ 获取章节讨论成功:', response.data)
// 处理讨论数据
const discussionData = response.data.result
if (discussionData && Array.isArray(discussionData)) {
// 为每个评论添加点赞状态字段
discussionList.value = discussionData.map(comment => ({
...comment,
isLiked: false, // 默认未点赞
likes: comment.likes || 0 // 确保有点赞数字段
}))
console.log('✅ 讨论数据加载完成,讨论数量:', discussionList.value.length)
} else {
console.warn('⚠️ 讨论数据格式异常:', discussionData)
discussionList.value = []
}
} else {
console.error('❌ 获取章节讨论失败:', response.data?.message || response.message)
message.error(response.data?.message || response.message || '获取讨论失败')
discussionList.value = []
}
} catch (error) {
console.error('❌ 获取章节讨论异常:', error)
message.error('获取讨论失败,请稍后重试')
discussionList.value = []
}
}
const exitDiscussion = () => {
console.log('🚪 正在退出讨论模式...')
discussionMode.value = false
currentDiscussionSection.value = null
discussionList.value = []
newComment.value = ''
replyingTo.value = null
console.log('✅ 已退出讨论模式discussionMode:', discussionMode.value)
}
const submitDiscussionComment = () => {
if (!newComment.value.trim()) return
const comment = {
id: Date.now(),
username: '当前用户',
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-4.0.3&auto=format&fit=crop&w=60&q=80',
content: newComment.value,
time: new Date().toLocaleString(),
likes: 0,
isLiked: false, // 新评论默认未点赞
replies: []
}
discussionList.value.unshift(comment)
newComment.value = ''
message.success('评论发布成功!')
}
const likeComment = (comment: any) => {
console.log('🔄 点赞操作:', {
commentId: comment.id,
currentLiked: comment.isLiked,
currentLikes: comment.likes
})
// 切换点赞状态
if (comment.isLiked) {
// 取消点赞
comment.isLiked = false
comment.likes = Math.max((comment.likes || 0) - 1, 0)
message.success('取消点赞!')
console.log('✅ 取消点赞成功:', { likes: comment.likes, isLiked: comment.isLiked })
} else {
// 点赞
comment.isLiked = true
comment.likes = (comment.likes || 0) + 1
message.success('点赞成功!')
console.log('✅ 点赞成功:', { likes: comment.likes, isLiked: comment.isLiked })
}
}
// 处理章节点击 - 已报名状态,可以正常点击
const handleSectionClick = (section: CourseSection) => {
console.log('🔍 点击课程章节:', section.name, section)
currentSection.value = section
// 检查章节类型
const isVideo = isVideoLesson(section)
const isResource = isResourceLesson(section)
const isPractice = isPracticeLesson(section) // 练习判断放在前面
const isHomework = isHomeworkLesson(section)
const isExam = isExamLesson(section)
const isDiscussion = isDiscussionLesson(section)
console.log('🔍 章节类型判断结果:', {
isVideo,
isResource,
isPractice,
isHomework,
isExam,
isDiscussion,
type: section.type,
name: section.name
})
// 如果是视频课程,调用视频播放处理方法
if (isVideo) {
console.log('✅ 识别为视频课程,调用视频播放方法')
handleVideoPlay(section)
} else if (isResource) {
console.log('✅ 识别为资料课程')
handleDownload(section)
} else if (isPractice) {
console.log('✅ 识别为练习课程')
handlePractice(section)
} else if (isHomework) {
console.log('✅ 识别为作业课程')
handleHomework(section)
} else if (isExam) {
console.log('✅ 识别为考试课程')
handleExam(section)
} else if (isDiscussion) {
console.log('✅ 识别为讨论课程')
handleDiscussion(section)
} else {
console.log('⚠️ 未识别的课程类型,默认当作视频处理')
loadSectionVideo(section)
}
}
// 跳转到已报名区域
// const navigateToEnrolledArea = (videoUrl: string, sectionName: string) => {
// console.log('跳转到已报名区域,播放视频:', videoUrl)
// console.log('章节名称:', sectionName)
// console.log('当前章节:', currentSection.value)
// // 使用路由跳转到学习页面
// router.push({
// name: 'CourseStudy',
// params: { id: courseId.value },
// query: {
// videoUrl: encodeURIComponent(videoUrl),
// sectionName: encodeURIComponent(sectionName),
// sectionId: currentSection.value?.id
// }
// })
// }
// 更新视频播放器(备用方案)
// const updateVideoPlayer = (videoUrl: string, sectionName: string) => {
// console.log('更新视频播放器:', { videoUrl, sectionName })
// // 如果在同一页面内更新视频播放器
// // 可以通过事件总线或状态管理来实现
// // 这里先显示确认信息
// const confirmed = confirm(`即将播放视频: ${sectionName}\n是否继续`)
// if (confirmed) {
// navigateToEnrolledArea(videoUrl, sectionName)
// }
// }
// 预览章节(非视频内容)
// const previewSection = (section: CourseSection) => {
// console.log('预览章节:', section)
// previewModalTitle.value = section.name
// previewModalContent.value = `章节ID: ${section.id}\n章节名称: ${section.name}\n内容类型: ${getLessonTypeText(section)}`
// previewModalType.value = 'section'
// previewModalVisible.value = true
// }
// 关闭预览模态框
const closePreviewModal = () => {
previewModalVisible.value = false
previewModalTitle.value = ''
previewModalContent.value = ''
previewModalType.value = ''
}
// 加载更多课程
const loadMoreCourses = async () => {
try {
moreCoursesLoading.value = true
moreCoursesError.value = ''
console.log('🔍 开始加载更多课程课程ID:', courseId.value)
const response = await CourseApi.getMoreCourses(courseId.value)
console.log('🔍 更多课程API响应:', response)
if (response.code === 0 || response.code === 200) {
moreCourses.value = response.data || []
console.log('✅ 更多课程加载成功,数量:', moreCourses.value.length)
} else {
moreCoursesError.value = response.message || '加载更多课程失败'
console.error('❌ 更多课程加载失败:', response.message)
}
} catch (error) {
console.error('❌ 加载更多课程失败:', error)
moreCoursesError.value = '加载更多课程失败,请稍后重试'
} finally {
moreCoursesLoading.value = false
}
}
// 格式化时长
const formatDuration = (duration: number): string => {
if (!duration || duration <= 0) return '0分钟'
const hours = Math.floor(duration / 3600)
const minutes = Math.floor((duration % 3600) / 60)
if (hours > 0) {
return `${hours}小时${minutes}分钟`
} else {
return `${minutes}分钟`
}
}
// 处理图片加载错误
const handleImageError = (event: Event) => {
const img = event.target as HTMLImageElement
img.src = '/images/courses/course-activities1.png'
}
// 处理课程报名
const handleEnrollCourse = (course: any) => {
console.log('点击报名课程:', course)
// 这里可以添加报名逻辑,比如跳转到课程详情页
window.open(`/course/${course.id}`, '_blank')
}
// 处理课程报名
// 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
console.log('🚀 开始报名课程课程ID:', courseId.value)
// 调用真实的报名API
const response = await CourseApi.enrollCourse(courseId.value)
console.log('📊 报名API响应:', response)
if (response.code === 200 || response.code === 0) {
console.log('✅ 报名成功:', response.data)
// 报名成功
isEnrolled.value = true
enrollConfirmVisible.value = false
enrollSuccessVisible.value = true
// 2秒后跳转到已兑换课程页面
setTimeout(() => {
enrollSuccessVisible.value = false
// 跳转到已兑换课程页面
router.push(`/course/${courseId.value}/exchanged`)
}, 2000)
} else {
console.error('❌ 报名失败:', response.message)
message.error(response.message || '报名失败,请稍后重试')
enrollConfirmVisible.value = false
}
} catch (error) {
console.error('❌ 报名失败:', error)
message.error('报名失败,请稍后重试')
enrollConfirmVisible.value = false
} 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('')
const chatMessages = ref<Array<{
id: string
type: 'user' | 'ai'
content: string
timestamp: string
isStreaming?: boolean
}>>([
// 移除初始化欢迎消息,让用户直接开始对话
])
const isAISending = ref(false)
// 笔记相关状态
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 downloadConfirmVisible = ref(false)
const pendingDownloadSection = ref<CourseSection | null>(null)
// 打开投诉弹窗
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('提交失败,请稍后重试')
}
}
// 处理文件上传
const handleUpload = ({ file, onFinish }: any) => {
// 这里可以实现真实的文件上传逻辑
console.log('上传文件:', file)
// 模拟上传成功
setTimeout(() => {
onFinish()
}, 1000)
}
// 加载课程学习进度
const loadCourseProgress = async () => {
try {
console.log('🚀 开始加载课程学习进度课程ID:', courseId.value)
const response = await CourseApi.getCourseProgress(courseId.value)
console.log('📊 课程学习进度响应:', response)
// 检查响应数据结构
if (response.data && response.data.code === 200 && response.data.result) {
console.log('✅ 课程学习进度加载成功:', response.data.result)
progressData.value = response.data.result
// 计算各项进度百分比
calculateProgress()
} else {
console.warn('⚠️ 课程学习进度API返回异常:', response)
}
} catch (error) {
console.error('❌ 加载课程学习进度失败:', error)
}
}
// 计算进度百分比
const calculateProgress = () => {
if (!progressData.value) {
console.log('⚠️ 进度数据为空,无法计算')
return
}
const data = progressData.value
console.log('🔍 原始进度数据:', data)
// 计算视频进度百分比
videoProgress.value = data.video?.total > 0 ?
(data.video.completed / data.video.total) * 100 : 0
// 计算作业进度百分比
exerciseProgress.value = data.homework?.total > 0 ?
(data.homework.completed / data.homework.total) * 100 : 0
// 计算考试进度百分比
examProgress.value = data.exam?.total > 0 ?
(data.exam.completed / data.exam.total) * 100 : 0
// 更新完成的课程数
completedLessons.value = data.total?.completed || 0
console.log('📊 进度计算结果:', {
原始数据: data,
视频进度: `${data.video?.completed}/${data.video?.total} = ${videoProgress.value.toFixed(1)}%`,
作业进度: `${data.homework?.completed}/${data.homework?.total} = ${exerciseProgress.value.toFixed(1)}%`,
考试进度: `${data.exam?.completed}/${data.exam?.total} = ${examProgress.value.toFixed(1)}%`,
总体进度: `${completedLessons.value}/${totalSections.value} = ${overallProgress.value.toFixed(1)}%`,
计算后的值: {
video: videoProgress.value,
exercise: exerciseProgress.value,
exam: examProgress.value,
overall: overallProgress.value,
completed: completedLessons.value,
total: totalSections.value
}
})
}
// 发送聊天消息
const sendMessage = async () => {
if (!chatMessage.value.trim() || isAISending.value) {
return
}
const userMessage = chatMessage.value.trim()
const messageId = Date.now().toString()
// 添加用户消息
chatMessages.value.push({
id: messageId,
type: 'user',
content: userMessage,
timestamp: new Date().toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
})
})
// 清空输入框
chatMessage.value = ''
// 滚动到底部
scrollToBottom()
// 添加AI消息占位符
const aiMessageId = (Date.now() + 1).toString()
chatMessages.value.push({
id: aiMessageId,
type: 'ai',
content: '',
timestamp: new Date().toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
}),
isStreaming: true
})
isAISending.value = true
try {
// 调用AI接口 - 流式响应
console.log('开始AI流式请求...')
await AIApi.sendChatMessageStream(
userMessage,
// 接收流式消息的回调
(chunk: string) => {
console.log('收到AI消息块:', chunk)
const aiMessage = chatMessages.value.find(msg => msg.id === aiMessageId)
if (aiMessage) {
aiMessage.content += chunk
scrollToBottom()
}
},
// 完成回调
() => {
console.log('AI消息流完成')
const aiMessage = chatMessages.value.find(msg => msg.id === aiMessageId)
if (aiMessage) {
aiMessage.isStreaming = false
console.log('最终AI消息内容:', aiMessage.content)
// 如果没有收到任何内容,显示默认消息
if (!aiMessage.content.trim()) {
aiMessage.content = '抱歉,我没有收到完整的回复,请重新提问。'
console.log('AI回复为空显示默认消息')
}
}
isAISending.value = false
},
// 错误回调
(error) => {
console.error('AI聊天失败:', error)
const aiMessage = chatMessages.value.find(msg => msg.id === aiMessageId)
if (aiMessage) {
aiMessage.content = `抱歉AI助手暂时无法回复${error.message || '网络连接异常'},请稍后再试。`
aiMessage.isStreaming = false
}
isAISending.value = false
message.error(`AI助手暂时无法回复${error.message || '请检查网络连接'}`)
}
)
} catch (error) {
console.error('发送消息失败:', error)
const aiMessage = chatMessages.value.find(msg => msg.id === aiMessageId)
if (aiMessage) {
aiMessage.content = '抱歉AI助手暂时无法回复请稍后再试。'
aiMessage.isStreaming = false
}
isAISending.value = false
message.error('发送消息失败,请检查网络连接')
}
}
// 笔记相关方法
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)
}
}
// 聊天消息容器引用
const chatMessagesContainer = ref<HTMLElement>()
// 格式化消息内容
const formatMessageContent = (content: string): string => {
if (!content) return ''
// 将换行符转换为HTML换行
let formatted = content.replace(/\n/g, '<br>')
// 处理列表项(以•开头的行)
formatted = formatted.replace(/^• (.+)$/gm, '<li>$1</li>')
// 如果有列表项包装在ul标签中
if (formatted.includes('<li>')) {
formatted = formatted.replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>')
}
return formatted
}
// 滚动到消息底部
const scrollToBottom = () => {
if (chatMessagesContainer.value) {
setTimeout(() => {
chatMessagesContainer.value!.scrollTop = chatMessagesContainer.value!.scrollHeight
}, 100)
}
}
// 发送快捷消息
// 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()
loadCourseComments() // 加载评论
loadMoreCourses() // 加载更多课程
// 检查是否需要刷新
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)
}
})
</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: 72px;
}
/* 练习/讨论模式整体布局 */
.practice-overall-layout {
display: flex;
gap: 20px;
align-items: flex-start;
}
.practice-left-container {
width: 70%;
flex-shrink: 0;
display: flex;
flex-direction: column;
gap: 0;
}
.practice-answer-card {
width: 300px;
flex-shrink: 0;
align-self: flex-start;
margin-top: 54px; /* 与广告区域齐平 */
}
/* 面包屑导航样式 */
.breadcrumb-section {
padding: 12px 0;
}
.breadcrumb {
display: flex;
align-items: center;
font-size: 14px;
line-height: 20px;
}
.breadcrumb-course {
height: 20px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 14px;
color: #333333;
line-height: 20px;
text-align: left;
font-style: normal;
cursor: pointer;
white-space: nowrap; /* 一行展示,不换行 */
/* 移除固定宽度,让内容完全展示 */
transition: color 0.3s ease;
}
.breadcrumb-course.clickable {
color: #333333; /* 保持原来的颜色 */
}
.breadcrumb-course.clickable:hover {
/* 悬停时保持原样,不改变颜色和样式 */
}
.breadcrumb-separator {
margin: 0 8px;
color: #333333;
}
.breadcrumb-current {
width: 154px;
height: 20px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 14px;
color: #999999;
line-height: 20px;
text-align: left;
font-style: normal;
}
/* 练习模式提示样式调整 */
.tip-section.practice-tip {
width: 100%; /* 占满左侧容器宽度 */
background: rgba(255, 255, 255, 0.5);
margin-bottom: 20px;
}
.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;
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;
}
/* 学期显示 */
.semester-display {
width: 100%;
height: 42px;
margin: 20px 0 -3px 0;
box-sizing: border-box;
}
.semester-text {
display: block;
width: 100%;
height: 100%;
background: #E2F5FF;
border: 1px solid #0088D1;
border-radius: 4px;
padding: 0 16px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 14px;
color: #0088D1;
line-height: 40px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
box-sizing: border-box;
}
/* 开课时间信息 */
.course-time-info {
width: 100%;
height: 20px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 14px;
color: #666666;
line-height: 20px;
text-align: left;
font-style: normal;
margin: 0 0 20px 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
box-sizing: border-box;
}
/* 进度头部样式 */
.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;
cursor: pointer;
}
/* 视频播放器区域 */
.video-player-section {
position: relative;
background: #fff;
overflow: visible; /* 改为visible确保底部交互区域不被裁剪 */
margin-bottom: 20px;
display: flex;
flex-direction: column;
}
.video-player.unregistered {
height: 578px;
position: relative;
}
.video-player.enrolled {
/* 移除固定高度,让内容自适应 */
}
.video-container {
position: relative;
width: 100%;
height: 450px; /* 使用固定高度,确保播放器能正常工作 */
}
/* DPlayer 容器样式 */
.video-container :deep(.dplayer) {
width: 100% !important;
height: 100% !important;
}
.video-placeholder {
position: absolute;
top: 0;
left: 0;
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; /* 确保在最上层 */
}
.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: flex-start;
align-items: center;
margin-left: 10px;
}
.course-description span {
color: #0088D1;
}
/* 课程描述内容样式 */
.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;
}
.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;
}
.badge-practice {
background: #9C27B0;
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; /* 防止收缩 */
/* 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 {
margin-top: 15px;
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 {
/* background-color: #fff; */
margin-bottom: 20px;
}
/* AI聊天界面样式 */
.ai-chat-interface {
display: flex;
flex-direction: column;
height: 600px;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 10px;
display: flex;
flex-direction: column;
gap: 16px;
/* 隐藏滚动条但保持功能 */
scrollbar-width: none;
/* Firefox */
-ms-overflow-style: none;
background-color: #fff;
/* 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;
}
/* 消息文本样式 */
.message-text {
font-size: 14px;
line-height: 1.6;
color: #333;
}
.message-text ul {
margin: 8px 0;
padding-left: 20px;
}
.message-text li {
margin-bottom: 4px;
}
/* 打字指示器样式 */
.typing-indicator {
display: flex;
align-items: center;
gap: 4px;
padding: 8px 0;
}
.typing-indicator span {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #0088D1;
animation: typing 1.4s infinite ease-in-out;
}
.typing-indicator span:nth-child(1) {
animation-delay: -0.32s;
}
.typing-indicator span:nth-child(2) {
animation-delay: -0.16s;
}
@keyframes typing {
0%, 80%, 100% {
transform: scale(0.8);
opacity: 0.5;
}
40% {
transform: scale(1);
opacity: 1;
}
}
.chat-input-area {
height: 100%;
display: flex;
background: #fff;
margin: 0 16px 16px 16px;
height: 96px;
border: 1.5px solid #D5D5D5;
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;
border: none;
}
.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;
transition: opacity 0.3s ease;
}
.send-button:disabled {
cursor: not-allowed;
opacity: 0.5;
}
.send-icon {
width: 100%;
height: 100%;
transition: opacity 0.3s ease;
}
.send-icon.disabled {
opacity: 0.5;
}
.chat-input:disabled {
background-color: #f5f5f5;
cursor: not-allowed;
}
.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;
}
/* 笔记编辑器样式 */
.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;
}
.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-header {
padding-left: 24px;
padding-right: 24px;
}
.course-description {
padding-left: 24px;
padding-right: 24px;
}
/* 响应式设计 */
/* 大屏幕 - 使用120px左右边距 */
@media (min-width: 1400px) {
.container {
padding-left: 120px;
padding-right: 72px;
max-width: none;
margin: 0;
}
.course-content {
gap: 32px;
}
}
@media (max-width: 1399px) and (min-width: 1200px) {
.container {
padding: 0 24px;
max-width: 1200px;
margin: 0 auto;
}
.course-content {
gap: 20px;
}
.sidebar {
width: 350px;
}
}
@media (max-width: 1199px) and (min-width: 992px) {
.container {
padding: 0 20px;
max-width: 992px;
margin: 0 auto;
}
.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;
}
.course-content {
gap: 16px;
}
.sidebar {
width: 280px;
}
}
/* 平板竖屏及以下 */
@media (max-width: 767px) {
.container {
padding: 0 16px;
max-width: 576px;
margin: 0 auto;
}
.course-content {
flex-direction: column;
gap: 16px;
}
.sidebar {
width: 100%;
order: -1;
}
.video-player-section {
/* 移除固定高度,让内容自适应 */
}
}
@media (max-width: 767px) {
.container {
padding: 0 16px;
max-width: 576px;
margin: 0 auto;
}
.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;
}
.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;
}
.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;
height: 60px;
}
.banner-content {
display: flex;
justify-content: space-between;
align-items: center;
color: #000;
height: 100%;
}
.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 */
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);
}
/* 评论区样式 */
.comments-content {
padding: 0;
}
/* 发布评论区域 */
.post-comment-section {
margin-bottom: 10px;
background: transparent; /* 无背景 */
border: none; /* 无边框 */
}
.comment-input-wrapper {
display: flex;
gap: 12px;
background: transparent; /* 无背景 */
border: none; /* 无边框 */
}
.user-avatar img {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
flex-shrink: 0;
}
.comment-input-area {
flex: 1;
background: transparent; /* 无背景 */
border: none; /* 无边框 */
}
.comment-textarea {
width: 100% !important;
border: 1px solid #E6E6E6 !important;
border-radius: 2px !important;
padding: 8px 12px !important;
font-size: 14px !important;
resize: none !important; /* 强制禁用调整大小功能 */
font-family: PingFangSC, PingFang SC !important;
height: 36px !important; /* 强制设置高度为36px */
min-height: 36px !important;
max-height: 36px !important;
box-sizing: border-box !important;
overflow: hidden !important;
background: transparent !important; /* 透明背景 */
outline: none !important;
line-height: 20px !important;
/* 完全移除 resize handle */
-webkit-appearance: none !important;
-moz-appearance: none !important;
appearance: none !important;
}
/* 针对外面评论区域的 textarea 强制移除 resize handle */
.comment-textarea::-webkit-resizer {
display: none !important;
visibility: hidden !important;
opacity: 0 !important;
width: 0 !important;
height: 0 !important;
}
.comment-textarea::-moz-resizer {
display: none !important;
visibility: hidden !important;
opacity: 0 !important;
width: 0 !important;
height: 0 !important;
}
.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;
padding-top: 1px; /* 与讨论区域一致的间距 */
}
.toolbar-left {
display: flex;
gap: 8px;
}
.toolbar-btn {
width: 24px;
height: 20px;
border-radius: 2px;
border: 1px solid #E6E6E6;
background: transparent; /* 透明背景 */
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border: 1.5px solid #E6E6E6;
}
.toolbar-btn:hover {
background: #f5f5f5;
}
.toolbar-icon {
width: 16px; /* 调整图标大小 */
height: 16px;
/* 移除蓝色边框 */
}
.toolbar-right {
display: flex;
align-items: center;
}
.btn-submit {
background: #0088D1;
color: white;
border: none;
border-radius: 2px; /* 添加圆角 */
padding: 3px 10px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.3s;
font-weight: 500;
width: 48px; /* 与讨论区域一致的宽度 */
height: 24px; /* 与讨论区域一致的高度 */
font-family: PingFangSC, PingFang SC;
text-align: center;
line-height: 20px;
}
.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 !important; /* 强制禁用调整大小功能 */
font-family: inherit;
height: 40px;
min-height: 40px;
box-sizing: border-box;
overflow: hidden;
transition: height 0.3s ease;
background: transparent;
/* 完全移除 resize handle */
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
.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;
}
/* 针对回复区域的 textarea 强制移除 resize handle */
.reply-textarea::-webkit-resizer {
display: none !important;
visibility: hidden !important;
opacity: 0 !important;
width: 0 !important;
height: 0 !important;
}
.reply-textarea::-moz-resizer {
display: none !important;
visibility: hidden !important;
opacity: 0 !important;
width: 0 !important;
height: 0 !important;
}
.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;
}
/* 评论状态样式 */
.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;
}
/* 练习模式样式 */
.practice-section {
background: transparent; /* 改为透明 */
border-radius: 0;
overflow: visible;
width: 100%;
max-width: none;
}
/* 练习模式布局调整 */
.course-layout.practice-layout-mode {
padding-left: 120px;
padding-right: 140px;
}
/* 练习模式下的主列样式调整 */
.main-column.practice-mode {
min-width: auto;
max-width: none;
width: 100%;
}
.practice-instructions {
padding: 40px;
text-align: center;
}
.instructions-card {
max-width: 600px;
margin: 0 auto;
background: #f8f9fa;
border-radius: 12px;
padding: 40px;
}
.instructions-card h3 {
font-size: 24px;
color: #333;
margin-bottom: 20px;
}
.instructions-card ul {
text-align: left;
margin: 20px 0;
padding-left: 20px;
}
.instructions-card li {
margin-bottom: 10px;
color: #666;
line-height: 1.6;
}
.practice-actions {
display: flex;
gap: 16px;
justify-content: center;
margin-top: 30px;
}
.btn-start-practice {
background: #1890ff;
color: white;
border: none;
padding: 12px 30px;
border-radius: 6px;
font-size: 16px;
cursor: pointer;
transition: background 0.3s;
}
.btn-start-practice:hover {
background: #40a9ff;
}
.btn-back-practice {
background: #f5f5f5;
color: #666;
border: none;
padding: 12px 30px;
border-radius: 6px;
font-size: 16px;
cursor: pointer;
transition: background 0.3s;
}
.btn-back-practice:hover {
background: #e6f7ff;
}
.practice-content {
width: 100%;
background: rgba(255, 255, 255, 0.5);
border-radius: 2px;
border: 1px solid #FFFFFF;
padding: 24px;
min-height: 560px;
}
.answer-sheet {
background: white;
border: 1px solid #e8e8e8;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.answer-sheet-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #f0f0f0;
}
.answer-sheet-header h4 {
margin: 0;
font-size: 16px;
color: #333;
}
.progress-text {
font-size: 14px;
color: #666;
}
.question-grid {
margin-bottom: 20px;
}
.question-numbers {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 8px;
}
.answer-card-number {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #d9d9d9;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
}
.answer-card-number.unanswered {
background: white;
color: #666;
}
.answer-card-number.answered {
background: #1890ff;
color: white;
border-color: #1890ff;
}
.answer-card-number:hover {
border-color: #40a9ff;
}
.answer-legend {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
}
.legend-icon {
width: 16px;
height: 16px;
border-radius: 2px;
}
.legend-icon.unanswered {
background: white;
border: 1px solid #d9d9d9;
}
.legend-icon.answered {
background: #1890ff;
}
.legend-text {
font-size: 12px;
color: #666;
}
.submit-section {
text-align: center;
}
.btn-submit-practice {
background: #52c41a;
color: white;
border: none;
padding: 12px 30px;
border-radius: 6px;
font-size: 16px;
cursor: pointer;
transition: background 0.3s;
width: 100%;
}
.btn-submit-practice:hover {
background: #73d13d;
}
.question-card {
height: 100%;
display: flex;
flex-direction: column;
background: transparent;
border: none;
border-radius: 0;
padding: 0;
}
.question-header {
margin-bottom: 12px;
}
.question-title-info {
color: #0088D1;
font-size: 16px;
font-weight: 500;
}
.question-title-info span {
background-color: #EEF9FF;
font-size: 12px;
padding: 2px 4px;
border-radius: 10px;
}
.question-content {
flex: 1;
margin-bottom: 24px;
}
.question-title {
font-size: 16px;
font-weight: 400;
color: #333;
margin: 0 0 24px 0;
line-height: 1.8;
}
.question-options {
margin-bottom: 24px;
}
.option-item {
display: flex;
align-items: center;
padding: 5px 20px;
border: none;
border-radius: 6px;
margin-bottom: 16px;
cursor: pointer;
transition: all 0.3s;
background: #EEF9FF;
min-height: 50px;
}
.option-item:hover {
background: #EEF9FF;
}
.option-item.selected {
background: #EEF9FF;
color: #333;
}
.option-checkbox {
margin-right: 12px;
position: relative;
}
.option-checkbox input[type="checkbox"] {
width: 16px;
height: 16px;
margin: 0;
cursor: pointer;
}
.option-label {
font-weight: 500;
color: #333;
margin-right: 8px;
min-width: 20px;
}
.option-text {
flex: 1;
color: #333;
line-height: 1.5;
}
.fill-blank {
margin-top: 20px;
}
.fill-item {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 15px;
}
.fill-number {
font-weight: 500;
color: #333;
min-width: 20px;
}
.fill-input {
flex: 1;
padding: 8px 12px;
border: 1px solid #d9d9d9;
border-radius: 4px;
font-size: 14px;
}
.fill-input:focus {
border-color: #1890ff;
outline: none;
}
.fill-hint {
font-size: 12px;
color: #999;
margin-top: 10px;
}
.essay-answer {
margin-top: 20px;
}
.essay-container {
margin-bottom: 10px;
}
.essay-textarea {
width: 100%;
padding: 12px;
border: 1px solid #d9d9d9;
border-radius: 4px;
font-size: 14px;
resize: vertical;
font-family: inherit;
}
.essay-textarea:focus {
border-color: #1890ff;
outline: none;
}
.essay-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.essay-hint {
font-size: 12px;
color: #999;
}
.essay-counter {
font-size: 12px;
color: #666;
}
.question-navigation {
display: flex;
justify-content: space-between;
gap: 16px;
}
.btn-nav {
padding: 10px 20px;
border: 1px solid #d9d9d9;
border-radius: 4px;
background: white;
color: #333;
cursor: pointer;
transition: all 0.3s;
}
.btn-nav:hover:not(:disabled) {
border-color: #1890ff;
color: #1890ff;
}
.btn-nav:disabled {
background: #f5f5f5;
color: #bfbfbf;
cursor: not-allowed;
}
.btn-next {
background: #1890ff;
color: white;
border-color: #1890ff;
}
.btn-next:hover {
background: #40a9ff;
border-color: #40a9ff;
}
.btn-return {
background: #52c41a;
color: white;
border-color: #52c41a;
}
.btn-return:hover {
background: #73d13d;
border-color: #73d13d;
}
@media (max-width: 768px) {
.practice-layout {
flex-direction: column;
}
.practice-sidebar {
width: 100%;
order: 2;
}
.practice-content {
order: 1;
}
.question-numbers {
grid-template-columns: repeat(8, 1fr);
}
.question-navigation {
flex-direction: column;
}
}
/* 答题卡容器样式 */
.answer-card-container {
width: 300px;
height: 566px;
background: rgba(255, 255, 255, 0.5);
border: 1px solid #FFFFFF;
border-radius: 0;
box-shadow: none;
padding: 20px;
}
/* 答题报告标题 */
.answer-card-title {
font-family: PingFangSC, PingFang SC;
font-weight: 500;
font-size: 16px;
color: #000000;
line-height: 22px;
text-align: center;
margin-bottom: 16px;
width: 100%;
display: flex;
justify-content: center;
}
/* 圆环右上角的难度标签 */
.difficulty-tag-circle {
position: absolute;
top: 0px;
right: 5px;
width: 56px;
height: 20px;
background: #0288D1;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
font-family: AppleSystemUIFont;
font-size: 10px;
color: #FFFFFF;
line-height: 14px;
text-align: left;
font-style: normal;
text-transform: none;
white-space: nowrap;
z-index: 10;
}
/* 分割线 */
.divider-line {
width: 252px;
height: 1px;
border: 1px solid #E6E6E6;
margin: 16px auto;
}
/* 得分圆环容器 */
.score-circle-container {
display: flex;
justify-content: center;
margin: 20px 0;
position: relative; /* 为难度标签提供定位基准 */
}
/* 使用新的SemiCircleProgress组件移除旧的圆环样式 */
.answer-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: -20px;
}
.answer-card-header h3 {
font-size: 18px;
font-weight: 600;
color: #333;
margin: 0;
}
.difficulty-tag {
background: #1890ff;
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
}
.score-circle-container {
display: flex;
justify-content: center;
margin-bottom: 20px;
}
.score-circle {
position: relative;
width: 120px;
height: 120px;
}
.score-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
}
.current-score {
font-size: 32px;
font-weight: bold;
color: #333;
line-height: 1;
}
.score-label {
font-size: 12px;
color: #999;
margin-top: 4px;
}
.answer-info {
margin-bottom: 20px;
}
.info-item {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-size: 14px;
}
.info-label {
color: #666;
}
.info-value {
color: #333;
}
.score-breakdown {
margin-bottom: 20px;
}
.score-breakdown h4 {
font-size: 16px;
font-weight: 600;
color: #333;
margin: 0 0 12px 0;
}
.score-item {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-size: 14px;
}
.score-type {
color: #666;
}
.score-count {
color: #1890ff;
font-weight: 500;
}
.ranking-info {
display: flex;
justify-content: space-between;
text-align: center;
}
.ranking-item {
flex: 1;
}
.ranking-number {
font-size: 24px;
font-weight: bold;
margin-bottom: 4px;
}
.ranking-item:nth-child(1) .ranking-number {
color: #ff6b35;
}
.ranking-item:nth-child(2) .ranking-number {
color: #666;
}
.ranking-item:nth-child(3) .ranking-number {
color: #1890ff;
}
.ranking-label {
font-size: 12px;
color: #999;
line-height: 1.2;
}
/* 讨论模式样式 - 按照原型图设计 */
.discussion-section {
width: 1246px;
min-height: 864px; /* 改为最小高度,支持自适应 */
height: auto; /* 自适应高度 */
background: rgba(255, 255, 255, 0.5);
border-radius: 2px;
border: 1px solid #E5E5E5;
padding: 24px;
margin: 0 auto;
}
.discussion-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 24px;
border-bottom: 1px solid #f0f0f0;
background: #fafafa;
}
.discussion-title h3 {
margin: 0 0 8px 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
.discussion-subtitle {
margin: 0;
font-size: 14px;
color: #666;
line-height: 1.6;
max-width: 800px;
}
.exit-discussion-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: white;
border: 1px solid #d9d9d9;
border-radius: 6px;
color: #666;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
}
.exit-discussion-btn:hover {
border-color: #1890ff;
color: #1890ff;
}
.discussion-content {
padding: 24px;
}
.discussion-stats {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding-bottom: 6px;
}
.comment-count {
font-size: 16px;
font-weight: 600;
color: #333;
}
.comment-input-section {
margin-bottom: 32px;
}
/* 移除重复的 comment-input-wrapper 样式,使用上面的定义 */
/* 移除重复的 comment-textarea 样式,使用上面的定义 */
.comment-actions {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: #fafafa;
border-top: 1px solid #f0f0f0;
}
.comment-tools {
display: flex;
gap: 8px;
}
.tool-btn {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background: white;
border: 1px solid #d9d9d9;
border-radius: 4px;
color: #666;
cursor: pointer;
transition: all 0.3s;
}
.tool-btn:hover {
border-color: #1890ff;
color: #1890ff;
}
.submit-comment-btn {
padding: 8px 24px;
background: #1890ff;
color: white;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
}
.submit-comment-btn:hover:not(:disabled) {
background: #40a9ff;
}
.submit-comment-btn:disabled {
background: #f5f5f5;
color: #bfbfbf;
cursor: not-allowed;
}
.discussion-list {
display: flex;
flex-direction: column;
gap: 24px;
}
.discussion-item {
display: flex;
gap: 12px;
padding: 20px;
background: #fafafa;
border-radius: 8px;
}
.comment-avatar {
flex-shrink: 0;
}
.comment-avatar img {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
}
.comment-content {
flex: 1;
}
.comment-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
}
.comment-username {
font-weight: 600;
color: #333;
font-size: 14px;
}
.comment-time {
color: #999;
font-size: 12px;
}
.comment-text {
color: #333;
font-size: 14px;
line-height: 1.6;
margin-bottom: 12px;
}
.comment-actions {
display: flex;
gap: 16px;
}
.action-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
background: none;
border: none;
color: #666;
font-size: 12px;
cursor: pointer;
border-radius: 4px;
transition: all 0.3s;
}
.action-btn:hover {
background: #f0f0f0;
color: #1890ff;
}
.like-btn.liked {
color: #ff4d4f;
}
/* 讨论模式下的徽章样式 */
.badge-discussion {
background: #52c41a;
color: white;
}
/* 讨论容器 */
.discussion-container {
width: 100%;
height: 100%;
padding-right: 48px; /* 增加右侧距离 */
}
/* 讨论标题行 */
.discussion-title-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
width: 100%; /* 确保占满容器宽度 */
}
.discussion-title {
font-family: PingFangSC, PingFang SC;
font-weight: 500;
font-size: 16px;
color: #0088D1;
line-height: 22px;
text-align: left;
font-style: normal;
margin: 0;
flex-shrink: 0; /* 防止标题被压缩 */
}
.participation-status {
font-family: PingFangSC, PingFang SC;
font-weight: 500;
font-size: 16px;
color: #0088D1;
line-height: 22px;
text-align: right;
font-style: normal;
flex-shrink: 0; /* 防止状态文字被压缩 */
}
/* 讨论描述 */
.discussion-description {
width: 100%;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #000000;
line-height: 22px;
text-align: justify;
font-style: normal;
margin-bottom: 32px;
}
/* 评论统计 */
.discussion-stats {
margin-bottom: 14px;
}
.comment-count {
width: 85px;
height: 20px;
font-family: PingFangSC, PingFang SC;
font-weight: 500;
font-size: 14px;
color: #008BD7;
line-height: 20px;
text-align: left;
font-style: normal;
text-transform: none;
}
/* 评论输入区域 */
.comment-input-section {
display: flex;
gap: 12px;
margin-bottom: 24px;
background: transparent; /* 无背景,无边框 */
}
.user-avatar img {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
}
.input-wrapper {
flex: 1;
background: transparent;
}
.comment-input {
width: 100% !important;
height: 36px !important;
padding: 8px 12px !important;
border: 1px solid #E6E6E6 !important;
border-radius: 2px !important;
outline: none !important;
resize: none !important; /* 强制禁用调整大小功能 */
font-family: PingFangSC, PingFang SC !important;
font-size: 14px !important;
color: #333 !important;
background: transparent !important; /* 透明背景 */
box-sizing: border-box !important;
/* 完全移除 resize handle */
-webkit-appearance: none !important;
-moz-appearance: none !important;
appearance: none !important;
/* 强制移除所有可能的 resize 样式 */
max-height: 36px !important;
min-height: 36px !important;
overflow: hidden !important;
}
/* 针对 webkit 浏览器强制移除 resize handle */
.comment-input::-webkit-resizer {
display: none !important;
visibility: hidden !important;
opacity: 0 !important;
width: 0 !important;
height: 0 !important;
}
/* 针对 Firefox 移除 resize handle */
.comment-input::-moz-resizer {
display: none !important;
visibility: hidden !important;
opacity: 0 !important;
width: 0 !important;
height: 0 !important;
}
/* 通用的 resizer 移除 */
.comment-input::resizer {
display: none !important;
}
.comment-input::placeholder {
color: #BFBFBF;
}
.input-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 8px; /* 减少到8px */
margin-top: 8px;
}
.toolbar-left {
display: flex;
gap: 8px;
}
.toolbar-btn {
width: 24px;
height: 20px;
border-radius: 2px;
border: 1px solid #E6E6E6;
background: transparent; /* 透明背景 */
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
}
.toolbar-icon {
width: 16px; /* 调整图标大小 */
height: 16px;
/* 移除蓝色边框 */
}
.submit-btn {
width: 48px; /* 确保宽度为48px */
height: 24px;
background: #0088D1;
border-radius: 2px;
border: none;
cursor: pointer;
transition: background-color 0.3s;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 14px;
color: #FFFFFF;
line-height: 20px;
text-align: center;
font-style: normal;
}
.submit-btn:hover {
background: #0077B8;
}
.submit-btn:disabled {
background: #D9D9D9;
cursor: not-allowed;
}
/* 评论列表 */
.discussion-list {
background: transparent;
}
.discussion-item {
display: flex;
gap: 12px;
margin-bottom: 16px; /* 减少间距 */
background: transparent;
padding: 0; /* 移除内边距 */
}
.discussion-item:last-child {
margin-bottom: 0;
}
.comment-avatar img {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
}
.comment-content {
flex: 1;
background: transparent;
}
.comment-header {
display: flex;
align-items: center;
gap: 12px; /* 用户名与讲师标签间距12px更紧凑 */
margin-bottom: 6px; /* 减少间距 */
justify-content: flex-start; /* 左对齐,让讲师标签紧跟用户名 */
}
.comment-username {
height: 20px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 14px;
color: #666666;
line-height: 20px;
text-align: left;
font-style: normal;
white-space: nowrap; /* 不换行 */
display: inline-block; /* 宽度自适应内容 */
}
.comment-badge {
background: #1890FF;
color: white;
padding: 2px 6px;
border-radius: 2px;
font-size: 10px;
line-height: 14px;
}
.comment-text {
width: 700px;
min-height: 20px; /* 改为最小高度,支持自适应 */
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 14px;
color: #333333;
line-height: 20px;
text-align: left;
font-style: normal;
margin-bottom: 8px; /* 减少间距 */
word-wrap: break-word; /* 支持换行 */
word-break: break-all; /* 强制换行 */
}
.comment-images {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
.comment-image {
width: 80px;
height: 60px;
border-radius: 4px;
object-fit: cover;
}
.comment-footer {
display: flex;
align-items: center;
margin-top: 4px; /* 减少顶部间距 */
gap: 24px; /* 时间与操作按钮间距24px */
}
.comment-time {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
color: #999999;
line-height: 20px;
text-align: left;
font-style: normal;
text-transform: none;
white-space: nowrap; /* 确保时间一行展示 */
}
.comment-actions {
display: flex;
align-items: center;
gap: 24px; /* 点赞与回复间距24px */
}
/* 讨论区域专用的评论操作样式 */
.discussion-comment-actions {
display: flex;
align-items: center;
gap: 24px; /* 点赞与回复间距24px */
}
.like-btn {
background: transparent; /* 透明背景 */
border: none;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
color: #999999;
line-height: 20px;
text-align: left;
font-style: normal;
text-transform: none;
cursor: pointer;
display: flex;
align-items: center;
gap: 4px;
padding: 0;
white-space: nowrap; /* 确保一行展示 */
}
.like-btn:hover {
color: #1890FF;
}
.reply-text {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
color: #999999;
line-height: 20px;
text-align: left;
font-style: normal;
text-transform: none;
cursor: pointer;
padding: 0;
background: transparent; /* 透明背景 */
white-space: nowrap; /* 确保一行展示 */
}
.reply-text:hover {
color: #1890FF;
}
/* 讨论区域专用的点赞和回复按钮样式 */
.discussion-like-btn {
background: transparent !important; /* 强制透明背景 */
border: none;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
color: #999999;
line-height: 20px;
text-align: left;
font-style: normal;
text-transform: none;
cursor: pointer;
display: flex;
align-items: center;
gap: 4px;
padding: 0;
white-space: nowrap; /* 确保一行展示 */
}
.discussion-like-btn:hover {
color: #1890FF;
}
.discussion-like-btn.liked {
color: #1890FF;
}
.discussion-like-btn .like-icon {
width: 16px;
height: 16px;
object-fit: contain;
}
.discussion-reply-text {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
color: #999999;
line-height: 20px;
text-align: left;
font-style: normal;
text-transform: none;
cursor: pointer;
padding: 0;
background: transparent !important; /* 强制透明背景 */
white-space: nowrap; /* 确保一行展示 */
}
.discussion-reply-text:hover {
color: #1890FF;
}
/* 新的答题卡样式 */
/* 答题信息样式 */
.answer-info {
margin: 20px 0;
}
.info-item {
display: flex;
justify-content: flex-start;
align-items: center;
margin-bottom: 8px;
padding-left: 20px;
gap: 2px; /* 非常小的间距 */
}
.info-label {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
color: #999999;
line-height: 17px;
text-align: left;
font-style: normal;
flex-shrink: 0;
}
.info-value {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
color: #999999;
line-height: 17px;
flex-shrink: 0;
}
/* 得分情况样式 */
.score-breakdown {
margin: 20px 0;
}
.score-breakdown-title {
width: 100%;
height: 22px;
font-family: PingFangSC, PingFang SC;
font-weight: 500;
font-size: 16px;
color: #000000;
line-height: 22px;
text-align: center;
font-style: normal;
margin-bottom: 16px;
display: flex;
justify-content: center;
}
.score-item {
margin-bottom: 12px; /* 减少高度 */
}
.score-item-content {
display: flex;
align-items: center;
gap: 8px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
color: #999999;
line-height: 17px;
}
.score-type {
width: 30px;
flex-shrink: 0;
}
.score-progress-bar {
width: 150px; /* 增加宽度,让进度条更长 */
height: 8px;
background: #E6E6E6;
border-radius: 5px;
overflow: hidden;
flex-shrink: 0;
}
.score-progress-fill {
height: 100%;
background: #0288D1;
border-radius: 5px;
transition: width 0.3s ease;
}
.score-count {
margin-left: 8px;
flex-shrink: 0;
}
/* 排名信息样式 */
.ranking-info {
display: flex;
align-items: center;
justify-content: space-between;
margin: 20px 0;
padding: 0 10px;
}
.ranking-item {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
}
.ranking-item-left {
margin-left: -20px; /* 左边项目往左移 */
}
.ranking-item-right {
margin-right: -20px; /* 右边项目往右移 */
}
.ranking-number {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 24px;
color: #F38505;
line-height: 33px;
text-align: center;
font-style: normal;
text-transform: none;
}
.ranking-number-with-text {
display: flex;
align-items: center;
justify-content: center;
height: 33px;
}
.ranking-prefix {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 10px;
color: #999999;
line-height: 14px;
text-align: left;
font-style: normal;
margin-right: 4px; /* 往左,增加与数字的间距 */
}
.ranking-suffix {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 10px;
color: #999999;
line-height: 14px;
text-align: left;
font-style: normal;
margin-left: 4px; /* 往右,增加与数字的间距 */
}
.ranking-label {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
color: #999999;
line-height: 17px;
text-align: center;
margin-top: 4px;
white-space: nowrap; /* 防止换行,保持一行显示 */
}
.ranking-divider {
width: 1px;
height: 40px;
background: #EDEDED;
}
/* 下载确认弹窗样式 */
.download-confirm-modal {
background: white;
border-radius: 8px;
padding: 24px;
}
.download-confirm-header {
margin-bottom: 20px;
}
.download-confirm-title {
font-size: 16px;
font-weight: 500;
color: #333;
text-align: center;
}
.download-confirm-content {
margin-bottom: 24px;
text-align: center;
}
.download-confirm-content p {
margin: 8px 0;
color: #666;
}
.download-section-name {
font-weight: 500;
color: #333 !important;
}
.download-confirm-actions {
display: flex;
justify-content: center;
align-items: center;
}
</style>