OL-LearnPlatform/src/views/CourseDetail.vue

2150 lines
60 KiB
Vue
Raw Normal View History

2025-07-22 14:39:45 +08:00
<template>
<div class="course-detail-page">
<!-- 面包屑导航 -->
<div class="breadcrumb">
<div class="container">
2025-07-28 09:51:21 +08:00
<span class="breadcrumb-text">首页 > 课程库 > {{ course?.title || '课程详情' }}</span>
2025-07-22 14:39:45 +08:00
</div>
</div>
<!-- 主要内容区域 -->
<div class="main-content">
<div class="container">
<div class="content-layout">
2025-07-28 09:51:21 +08:00
<!-- 加载状态 -->
<div v-if="loading" class="loading-container">
<div class="loading-content">
<p>正在加载课程详情...</p>
</div>
</div>
2025-07-28 09:51:21 +08:00
<!-- 错误状态 -->
<div v-else-if="error" class="error-container">
<div class="error-content">
<p>{{ error }}</p>
<button @click="loadCourseDetail" class="retry-btn">重试</button>
</div>
</div>
2025-07-28 09:51:21 +08:00
<!-- 课程内容 -->
<div v-else-if="course" class="course-content">
<!-- 左侧主要内容 -->
<div class="main-column">
<!-- 视频播放器区域 - 未报名状态 -->
<div class="video-player-section">
<div class="video-player unregistered">
<div class="video-background" :style="{ backgroundImage: `url(${course.coverImage})` }">
<div class="video-content">
<!-- 课程标题 -->
<h1 class="course-main-title">{{ course.title }}</h1>
<!-- 课程信息 -->
<div class="course-meta-info">
<span class="meta-item">讲师{{ course.instructor?.name }}</span>
<span class="meta-separator">|</span>
<span class="meta-item">时长{{ course.duration }}</span>
</div>
2025-07-28 09:51:21 +08:00
<!-- 报名按钮 -->
<button class="enroll-button" @click="handleEnrollCourse">
立即报名
</button>
2025-07-22 14:39:45 +08:00
</div>
</div>
</div>
</div>
2025-07-28 09:51:21 +08:00
<!-- 课程信息区域 -->
<div class="course-info-section">
<!-- 课程标题和分类 -->
<div class="course-header">
<h1 class="course-title">{{ course.title }}</h1>
<div class="course-meta">
<span class="course-category">分类<span class="category-tag">{{ course.category?.name || '未分类' }}</span></span>
<span class="course-price">时长{{ course.price || 0 }}</span>
<button class="btn-notes">记笔记</button>
2025-07-22 14:39:45 +08:00
</div>
</div>
2025-07-28 09:51:21 +08:00
<!-- 课程描述 -->
<div class="course-description">
<p>{{ course.description }}</p>
<div v-if="course.content" class="course-content-detail">
<h4>课程大纲</h4>
<div v-html="course.content"></div>
</div>
</div>
2025-07-28 09:51:21 +08:00
<!-- 讲师信息 -->
<div class="instructors-section" v-if="course.instructor">
<h3 class="section-title">讲师</h3>
<div class="instructors-list">
<div class="instructor-item">
<div class="instructor-avatar">
<SafeAvatar
:src="course.instructor.avatar"
:name="course.instructor.name"
:size="60"
/>
</div>
2025-07-28 09:51:21 +08:00
<div class="instructor-info">
<div class="instructor-name">{{ course.instructor.name }}</div>
<div class="instructor-title">{{ course.instructor.title }}</div>
<div v-if="course.instructor.bio" class="instructor-bio">{{ course.instructor.bio }}</div>
<div v-if="course.instructor.experience" class="instructor-experience">{{ course.instructor.experience }}</div>
</div>
</div>
2025-07-28 09:51:21 +08:00
</div>
</div>
2025-07-28 09:51:21 +08:00
<!-- 课程标签页 -->
<div class="course-tabs">
<div class="tab-nav">
<button class="tab-btn" :class="{ active: activeTab === 'intro' }" @click="activeTab = 'intro'">课程介绍</button>
<button class="tab-btn" :class="{ active: activeTab === 'comments' }" @click="activeTab = 'comments'">评论(1251)</button>
</div>
2025-07-28 09:51:21 +08:00
<!-- 标签页内容区域 -->
<div class="tab-content">
<!-- 课程介绍内容 -->
<div v-if="activeTab === 'intro'" class="tab-pane">
<div class="intro-content">
<h4>课程详情</h4>
<p>本课程深度聚焦问题让每一位教师了解并学习使用DeepSeek结合办公自动化职业岗位标准以实际工作任务为引导强调课程内容的易用性和岗位要求的匹配性</p>
<p>课程内容与全国计算机等级考试"1+X"WPS办公应用职业技能等级证书技能大赛紧密结合课程设置紧密对应实际全面共享可为职业工作人员在校学生创行教师提供服务与学习支持</p>
<h4>学习目标</h4>
<ul>
<li>掌握DeepSeek的基本使用方法</li>
<li>了解办公自动化职业岗位标准</li>
<li>提高教学质量和效率</li>
<li>获得实际工作技能</li>
</ul>
<h4>适用人群</h4>
<p>本课程适合职业工作人员在校学生教师等群体学习</p>
<h4>课程大纲</h4>
<div class="course-outline-content">
<ul class="outline-list">
<li>
<strong>第一章基础入门</strong>
<ul>
<li>- 环境搭建与配置</li>
<li>- 基本概念理解</li>
<li>- 实践操作演示</li>
</ul>
</li>
<li>
<strong>第二章核心技能</strong>
<ul>
<li>- 核心功能详解</li>
<li>- 实际应用场景</li>
<li>- 案例分析讲解</li>
</ul>
</li>
<li>
<strong>第三章高级应用</strong>
<ul>
<li>- 进阶技巧掌握</li>
<li>- 项目实战演练</li>
<li>- 问题解决方案</li>
</ul>
</li>
</ul>
</div>
</div>
2025-07-28 09:51:21 +08:00
</div>
2025-07-28 09:51:21 +08:00
<!-- 评论内容 -->
<div v-if="activeTab === 'comments'" class="tab-pane">
<div class="comments-content">
<div class="comment-stats">
<span class="total-comments">共1251条评论</span>
<div class="comment-filters">
<button class="filter-btn active">全部</button>
<button class="filter-btn">最新</button>
<button class="filter-btn">最热</button>
</div>
2025-07-28 09:51:21 +08:00
</div>
<div class="comment-list">
<div class="comment-item" v-for="comment in displayComments" :key="comment.id">
<div class="comment-avatar">
<img :src="comment.avatar" :alt="comment.username" />
</div>
2025-07-28 09:51:21 +08:00
<div class="comment-content">
<div class="comment-header">
<span class="comment-username">{{ comment.username }}</span>
<span class="comment-time">{{ comment.time }}</span>
</div>
<div class="comment-text">{{ comment.content }}</div>
<div class="comment-actions">
<button class="action-btn">
<i class="icon-like"></i>
{{ comment.likes }}
</button>
<button class="action-btn">回复</button>
</div>
</div>
</div>
</div>
2025-07-28 09:51:21 +08:00
<div class="load-more">
<button class="btn-load-more">加载更多评论</button>
</div>
2025-07-22 14:39:45 +08:00
</div>
</div>
</div>
</div>
</div>
</div>
2025-07-28 09:51:21 +08:00
<!-- 右侧边栏 -->
<div class="sidebar">
<!-- 报名学习按钮 -->
<div class="enroll-section">
<button class="btn-enroll" @click="handleEnrollCourse">报名学习</button>
</div>
2025-07-28 09:51:21 +08:00
<!-- 课程章节列表 -->
<div class="course-sections">
<div class="sections-header">
<h3>课程章节</h3>
<div class="sections-actions">
<button class="sort-btn">
<svg width="14" height="14" viewBox="0 0 16 16">
<path d="M3 3h10M3 8h7M3 13h4" stroke="currentColor" stroke-width="1.5" fill="none"/>
</svg>
正序
</button>
<button @click="loadCourseSections" class="refresh-btn" style="margin-left: 10px;">
刷新章节
</button>
<button @click="testDirectApiCall" class="test-btn" style="margin-left: 10px;">
测试API
</button>
2025-08-01 01:22:09 +08:00
<button @click="loadMockData" class="mock-btn" style="margin-left: 10px;">
加载模拟数据
</button>
2025-07-28 09:51:21 +08:00
</div>
2025-07-22 14:39:45 +08:00
</div>
2025-07-28 09:51:21 +08:00
<div class="sections-content">
<!-- 调试信息 -->
<div class="debug-info" style="background: #f0f0f0; padding: 10px; margin-bottom: 10px; font-size: 12px;">
<p>课程ID: {{ courseId }}</p>
<p>章节数量: {{ courseSections.length }}</p>
<p>分组数量: {{ groupedSections.length }}</p>
<p>加载状态: {{ sectionsLoading ? '加载中' : '已完成' }}</p>
<p>错误信息: {{ sectionsError || '无' }}</p>
</div>
<div v-if="sectionsLoading" class="sections-loading">
<p>正在加载章节列表...</p>
</div>
<div v-else-if="sectionsError" class="sections-error">
<p>{{ sectionsError }}</p>
<button @click="loadCourseSections" class="retry-btn">重试</button>
</div>
<div v-else-if="courseSections.length > 0" class="sections-list">
<!-- 按章节分组显示 -->
<div v-for="(chapter, chapterIndex) in groupedSections" :key="chapterIndex" class="chapter-section">
<div class="chapter-header" @click="toggleChapter(chapterIndex)">
<div class="chapter-info">
<span class="chapter-number">{{ chapterIndex + 1 }}</span>
<span class="chapter-title">{{ chapter.title }}</span>
</div>
2025-07-28 09:51:21 +08:00
<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">
2025-08-01 01:22:09 +08:00
<div class="lesson-content" @click="handleSectionClick(section)">
<div class="lesson-type-badge" :class="getLessonTypeBadgeClass(section)">
2025-07-28 09:51:21 +08:00
{{ getLessonTypeText(section) }}
2025-08-01 01:22:09 +08:00
</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="handleSectionClick(section)">
<svg width="12" height="12" viewBox="0 0 16 16">
<circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="1.5" fill="none"/>
<path d="M6 5l6 3-6 3V5z" fill="currentColor"/>
</svg>
</button>
<!-- 下载图标 -->
<button v-else-if="isResourceLesson(section)" class="lesson-action-btn download-btn" @click.stop="handleDownload(section)">
<svg width="12" height="12" viewBox="0 0 16 16">
<path d="M8 1v10M4 7l4 4 4-4M2 14h12" stroke="currentColor" stroke-width="1.5" fill="none"/>
</svg>
</button>
<!-- 编辑图标作业 -->
<button v-else-if="isHomeworkLesson(section)" class="lesson-action-btn edit-btn" @click.stop="handleHomework(section)">
<svg width="12" height="12" viewBox="0 0 16 16">
<path d="M12 1l3 3-8 8-4 1 1-4 8-8z" stroke="currentColor" stroke-width="1.5" fill="none"/>
</svg>
</button>
<!-- 考试图标 -->
<button v-else-if="isExamLesson(section)" class="lesson-action-btn exam-btn" @click.stop="handleExam(section)">
<svg width="12" height="12" viewBox="0 0 16 16">
<rect x="2" y="2" width="12" height="12" rx="2" stroke="currentColor" stroke-width="1.5" fill="none"/>
<path d="M6 6h4M6 8h4M6 10h2" stroke="currentColor" stroke-width="1.5"/>
</svg>
</button>
<!-- 完成状态图标 -->
<span v-if="section.completed" class="completion-icon">
<svg width="14" height="14" viewBox="0 0 16 16">
<circle cx="8" cy="8" r="7" fill="#52c41a"/>
<path d="M5 8l2 2 4-4" stroke="white" stroke-width="2" fill="none"/>
</svg>
</span>
</div>
</div>
2025-07-28 09:51:21 +08:00
</div>
</div>
</div>
</div>
</div>
2025-07-28 09:51:21 +08:00
<div v-else class="no-sections">
<p>暂无课程章节</p>
</div>
</div>
2025-07-22 14:39:45 +08:00
</div>
2025-07-28 09:51:21 +08:00
<!-- 推荐课程 -->
<div class="recommended-courses">
<div class="recommend-header">
<h3>推荐课程</h3>
</div>
<div class="recommend-list">
<!-- 计算机二级课程 -->
<div class="recommend-item">
<div class="recommend-image">
<div class="recommend-placeholder computer-bg">
<div class="placeholder-text">计算机二级</div>
</div>
<div class="recommend-badge">热门</div>
</div>
2025-07-28 09:51:21 +08:00
<div class="recommend-content">
<h4 class="recommend-title">计算机二级考前冲刺</h4>
<div class="recommend-tags">
<span class="tag">考试必备</span>
<span class="tag">名师授课</span>
<span class="tag">高通过率</span>
</div>
<p class="recommend-desc">备考计算机二级名师带你高效复习掌握考试重点轻松通过考试</p>
<div class="recommend-meta">
<span class="recommend-price">¥99</span>
<button class="recommend-btn">立即学习</button>
</div>
</div>
</div>
2025-07-28 09:51:21 +08:00
<!-- 英语课程 -->
<div class="recommend-item">
<div class="recommend-image">
<div class="recommend-placeholder english-bg">
<div class="placeholder-text">摆脱哑巴英语</div>
</div>
</div>
2025-07-28 09:51:21 +08:00
<div class="recommend-content">
<h4 class="recommend-title">摆脱哑巴英语</h4>
<p class="recommend-desc">备考计算机二级名师带你高效复习掌握考试重点轻松通过考试</p>
<div class="recommend-meta">
<span class="recommend-price">¥99</span>
<button class="recommend-btn">立即学习</button>
</div>
</div>
2025-07-22 14:39:45 +08:00
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
2025-07-28 09:51:21 +08:00
<!-- 登录模态框 -->
<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>
2025-07-22 14:39:45 +08:00
</div>
</template>
<script setup lang="ts">
2025-07-28 09:51:21 +08:00
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useAuth } from '@/composables/useAuth'
import { CourseApi } from '@/api/modules/course'
import type { Course, CourseSection } from '@/api/types'
import SafeAvatar from '@/components/common/SafeAvatar.vue'
import LoginModal from '@/components/auth/LoginModal.vue'
import RegisterModal from '@/components/auth/RegisterModal.vue'
2025-07-22 14:39:45 +08:00
const route = useRoute()
2025-07-28 09:51:21 +08:00
const router = useRouter()
const courseId = ref(Number(route.params.id))
const { loginModalVisible, registerModalVisible, enrollCourse, handleAuthSuccess } = useAuth()
2025-07-28 09:51:21 +08:00
// 当前选中的章节
const currentSection = ref<CourseSection | null>(null)
2025-07-28 09:51:21 +08:00
// 课程数据相关状态
const course = ref<Course | null>(null)
const loading = ref(false)
const error = ref('')
2025-07-28 09:51:21 +08:00
// 课程章节数据
const courseSections = ref<CourseSection[]>([])
const sectionsLoading = ref(false)
const sectionsError = ref('')
2025-07-28 09:51:21 +08:00
// 章节分组数据
interface ChapterGroup {
title: string
sections: CourseSection[]
expanded: boolean
}
2025-07-28 09:51:21 +08:00
const groupedSections = ref<ChapterGroup[]>([])
2025-08-01 01:22:09 +08:00
// 生成模拟章节数据(用于演示)
const generateMockSections = (): CourseSection[] => {
return [
// 第一章 - 课前准备 (4个)
{ id: 1, lessonId: courseId.value, name: '开课彩蛋:新开始新征程', outline: 'https://example.com/video1.m3u8', parentId: 0, sort: 1, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: true, duration: '01:03:56' },
{ id: 2, lessonId: courseId.value, name: '课程定位与目标', outline: 'https://example.com/video2.m3u8', parentId: 0, sort: 2, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: true, duration: '00:44:05' },
{ id: 3, lessonId: courseId.value, name: '教学安排及学习建议', outline: 'https://example.com/video3.m3u8', parentId: 0, sort: 3, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: true, duration: '00:52:22' },
{ id: 4, lessonId: courseId.value, name: '课前准备PPT', outline: 'https://example.com/ppt1.ppt', parentId: 0, sort: 4, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined },
// 第二章 - 程序设计基础知识 (5个)
{ id: 5, lessonId: courseId.value, name: '第一课 程序设计入门', outline: 'https://example.com/video4.m3u8', parentId: 0, sort: 5, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: true, duration: '00:52:22' },
{ id: 6, lessonId: courseId.value, name: '操作PPT', outline: 'https://example.com/ppt2.ppt', parentId: 0, sort: 6, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined },
{ id: 7, lessonId: courseId.value, name: '第二课 循环结构', outline: 'https://example.com/video5.m3u8', parentId: 0, sort: 7, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: true, duration: '01:03:56' },
{ id: 8, lessonId: courseId.value, name: '函数&循环', outline: '', parentId: 0, sort: 8, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined },
{ id: 9, lessonId: courseId.value, name: '练习题目', outline: '', parentId: 0, sort: 9, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined },
// 第三章 - 程序的控制结构 (6个)
{ id: 10, lessonId: courseId.value, name: '条件语句详解', outline: 'https://example.com/video6.m3u8', parentId: 0, sort: 10, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '00:45:30' },
{ id: 11, lessonId: courseId.value, name: '循环语句应用', outline: 'https://example.com/video7.m3u8', parentId: 0, sort: 11, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '00:38:15' },
{ id: 12, lessonId: courseId.value, name: '控制结构参考资料', outline: 'https://example.com/ppt3.ppt', parentId: 0, sort: 12, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined },
{ id: 13, lessonId: courseId.value, name: '条件判断练习', outline: '', parentId: 0, sort: 13, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined },
{ id: 14, lessonId: courseId.value, name: '循环结构作业', outline: '', parentId: 0, sort: 14, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined },
{ id: 15, lessonId: courseId.value, name: '控制结构测试', outline: '', parentId: 0, sort: 15, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined },
// 第四章 - 大语言模型介绍 (5个)
{ id: 16, lessonId: courseId.value, name: 'AI发展历程', outline: 'https://example.com/video8.m3u8', parentId: 0, sort: 16, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '01:12:45' },
{ id: 17, lessonId: courseId.value, name: '大语言模型原理', outline: 'https://example.com/video9.m3u8', parentId: 0, sort: 17, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '00:58:20' },
{ id: 18, lessonId: courseId.value, name: 'AI模型对比资料', outline: 'https://example.com/ppt4.ppt', parentId: 0, sort: 18, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined },
{ id: 19, lessonId: courseId.value, name: 'AI应用场景分析', outline: '', parentId: 0, sort: 19, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined },
{ id: 20, lessonId: courseId.value, name: '大语言模型考试', outline: '', parentId: 0, sort: 20, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined },
// 第五章 - DeepSeek实际应用 (6个)
{ id: 21, lessonId: courseId.value, name: 'DeepSeek平台介绍', outline: 'https://example.com/video10.m3u8', parentId: 0, sort: 21, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '00:42:10' },
{ id: 22, lessonId: courseId.value, name: 'API接口使用', outline: 'https://example.com/video11.m3u8', parentId: 0, sort: 22, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '00:55:35' },
{ id: 23, lessonId: courseId.value, name: '实战项目演示', outline: 'https://example.com/video12.m3u8', parentId: 0, sort: 23, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '01:25:18' },
{ id: 24, lessonId: courseId.value, name: 'DeepSeek开发文档', outline: 'https://example.com/ppt5.ppt', parentId: 0, sort: 24, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined },
{ id: 25, lessonId: courseId.value, name: '项目实战作业', outline: '', parentId: 0, sort: 25, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined },
{ id: 26, lessonId: courseId.value, name: 'DeepSeek应用考试', outline: '', parentId: 0, sort: 26, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined },
// 第六章 - 综合项目实战 (5个)
{ id: 27, lessonId: courseId.value, name: '项目需求分析', outline: 'https://example.com/video13.m3u8', parentId: 0, sort: 27, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '00:35:45' },
{ id: 28, lessonId: courseId.value, name: '系统架构设计', outline: 'https://example.com/video14.m3u8', parentId: 0, sort: 28, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '00:48:22' },
{ id: 29, lessonId: courseId.value, name: '项目开发指南', outline: 'https://example.com/ppt6.ppt', parentId: 0, sort: 29, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined },
{ id: 30, lessonId: courseId.value, name: '综合项目作业', outline: '', parentId: 0, sort: 30, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined },
{ id: 31, lessonId: courseId.value, name: '期末综合考试', outline: '', parentId: 0, sort: 31, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined }
]
}
2025-07-28 09:51:21 +08:00
// 根据章节数据生成分组
const generateChapterGroups = () => {
2025-08-01 01:22:09 +08:00
// 确保有章节数据
2025-07-28 09:51:21 +08:00
if (courseSections.value.length === 0) {
2025-08-01 01:22:09 +08:00
console.log('没有章节数据,生成模拟数据')
courseSections.value = generateMockSections()
2025-07-28 09:51:21 +08:00
}
2025-07-28 09:51:21 +08:00
console.log('开始生成章节分组,原始数据:', courseSections.value)
2025-08-01 01:22:09 +08:00
console.log('章节数据数量:', courseSections.value.length)
2025-07-28 09:51:21 +08:00
2025-08-01 01:22:09 +08:00
// 手动创建章节分组,符合图片中的结构
const groups: ChapterGroup[] = [
{
title: '第一章 课前准备',
sections: courseSections.value.slice(0, 4), // 前4个项目
2025-07-28 09:51:21 +08:00
expanded: true
2025-08-01 01:22:09 +08:00
},
{
title: '第二章 程序设计基础知识',
sections: courseSections.value.slice(4, 9), // 5个项目
expanded: true
},
{
title: '第三章 程序的控制结构',
sections: courseSections.value.slice(9, 15), // 6个项目
expanded: false
},
{
title: '第四章 大语言模型介绍',
sections: courseSections.value.slice(15, 20), // 5个项目
expanded: false
},
{
title: '第五章 DeepSeek实际应用',
sections: courseSections.value.slice(20, 26), // 6个项目
expanded: false
},
{
title: '第六章 综合项目实战',
sections: courseSections.value.slice(26, 31), // 5个项目
expanded: false
}
]
2025-07-28 09:51:21 +08:00
console.log('生成的章节分组:', groups)
2025-08-01 01:22:09 +08:00
console.log('第一章节数:', groups[0].sections.length)
console.log('第二章节数:', groups[1].sections.length)
2025-07-28 09:51:21 +08:00
groupedSections.value = groups
}
2025-07-22 14:39:45 +08:00
2025-07-28 09:51:21 +08:00
// 获取章节标题
// const getChapterTitle = (chapterIndex: number): string => {
// const titles = [
// '课前准备',
// '程序设计基础知识',
// '程序的控制结构',
// '大话吉模型介绍',
// 'DeepSeek实际应用',
// 'DeepSeek实际应用'
// ]
// return titles[chapterIndex - 1] || '课程内容'
// }
2025-07-22 16:23:15 +08:00
2025-07-28 09:51:21 +08:00
// 预览模态框相关数据
const previewModalVisible = ref(false)
const previewModalTitle = ref('')
const previewModalContent = ref('')
const previewModalType = ref('')
2025-07-22 14:39:45 +08:00
2025-07-28 09:51:21 +08:00
// 新增的响应式数据
const activeTab = ref('intro')
const displayComments = ref([
2025-07-22 14:39:45 +08:00
{
id: 1,
username: '学习者小王',
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-4.0.3&auto=format&fit=crop&w=50&q=80',
time: '2天前',
content: '老师讲得很详细,从零基础到实际应用都有涉及,非常适合初学者!',
likes: 23
2025-07-22 14:39:45 +08:00
},
{
id: 2,
username: 'AI爱好者',
avatar: 'https://images.unsplash.com/photo-1494790108755-2616b612b786?ixlib=rb-4.0.3&auto=format&fit=crop&w=50&q=80',
time: '5天前',
content: '课程内容很实用跟着做了几个项目收获很大。推荐给想学AI的朋友们',
likes: 18
2025-07-22 14:39:45 +08:00
},
{
id: 3,
username: '程序员小李',
avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?ixlib=rb-4.0.3&auto=format&fit=crop&w=50&q=80',
time: '1周前',
content: 'DeepSeek确实是个很强大的工具通过这个课程学会了很多实用技巧。',
likes: 31
}
])
2025-07-28 09:51:21 +08:00
// 加载课程详情
const loadCourseDetail = async () => {
console.log('开始加载课程详情课程ID:', courseId.value)
if (!courseId.value || isNaN(courseId.value)) {
error.value = '课程ID无效'
console.error('课程ID无效:', courseId.value)
return
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
try {
loading.value = true
error.value = ''
console.log('调用API获取课程详情...')
const response = await CourseApi.getCourseById(courseId.value)
console.log('API响应:', response)
if (response.code === 0 || response.code === 200) {
course.value = response.data
console.log('课程数据设置成功:', course.value)
} else {
error.value = response.message || '获取课程详情失败'
console.error('API返回错误:', response)
}
} catch (err) {
console.error('加载课程详情失败:', err)
error.value = '网络错误,请稍后重试'
} finally {
loading.value = false
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
}
2025-07-22 14:39:45 +08:00
2025-07-28 09:51:21 +08:00
// 加载课程章节列表
const loadCourseSections = async () => {
if (!courseId.value || isNaN(courseId.value)) {
sectionsError.value = '课程ID无效'
console.error('课程ID无效:', courseId.value)
return
}
try {
sectionsLoading.value = true
sectionsError.value = ''
console.log('开始加载课程章节课程ID:', courseId.value)
console.log('调用API: CourseApi.getCourseSections')
const response = await CourseApi.getCourseSections(courseId.value)
console.log('章节API响应:', response)
console.log('响应状态码:', response.code)
console.log('响应数据:', response.data)
if (response.code === 0 || response.code === 200) {
courseSections.value = response.data.list || []
console.log('章节数据设置成功,数量:', courseSections.value.length)
console.log('章节详细数据:', courseSections.value)
2025-08-01 01:22:09 +08:00
// 如果API返回的数据为空使用模拟数据
if (courseSections.value.length === 0) {
console.log('API返回数据为空使用模拟数据')
courseSections.value = generateMockSections()
}
2025-07-28 09:51:21 +08:00
// 生成章节分组
generateChapterGroups()
} else {
2025-08-01 01:22:09 +08:00
console.log('API调用失败使用模拟数据')
courseSections.value = generateMockSections()
generateChapterGroups()
sectionsError.value = '' // 清除错误,因为我们有模拟数据
2025-07-28 09:51:21 +08:00
}
} catch (err) {
console.error('加载课程章节失败:', err)
2025-08-01 01:22:09 +08:00
console.log('网络错误,使用模拟数据')
courseSections.value = generateMockSections()
generateChapterGroups()
sectionsError.value = '' // 清除错误,因为我们有模拟数据
2025-07-28 09:51:21 +08:00
} finally {
sectionsLoading.value = false
}
}
2025-08-01 01:22:09 +08:00
// 强制加载模拟数据
const loadMockData = () => {
console.log('强制加载模拟数据')
courseSections.value = generateMockSections()
generateChapterGroups()
sectionsError.value = ''
console.log('模拟数据加载完成,章节数量:', courseSections.value.length)
console.log('分组数量:', groupedSections.value.length)
}
2025-07-28 09:51:21 +08:00
// 切换章节展开/折叠
const toggleChapter = (chapterIndex: number) => {
if (groupedSections.value[chapterIndex]) {
groupedSections.value[chapterIndex].expanded = !groupedSections.value[chapterIndex].expanded
}
}
2025-07-28 09:51:21 +08:00
// 格式化时长
// 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')}`
// }
2025-07-28 09:51:21 +08:00
2025-08-01 01:22:09 +08:00
2025-07-22 14:39:45 +08:00
2025-07-28 09:51:21 +08:00
// 获取课时类型文本
const getLessonTypeText = (section: CourseSection): string => {
if (section.outline && section.outline.includes('ppt')) {
return '资料'
} else if (section.name.includes('作业') || section.name.includes('练习')) {
return '作业'
} else if (section.name.includes('考试') || section.name.includes('测试')) {
return '考试'
}
return '视频' // 默认为视频
}
2025-07-28 09:51:21 +08:00
// 格式化课时时长
const formatLessonDuration = (section: CourseSection): string => {
// 根据课时名称和类型生成合适的时长
const durations = [
'01:03:56', '00:44:05', '00:52:22', '', // 第一章时长
'00:52:22', '', '01:03:56', '', '' // 第二章时长
]
// 根据section.id获取对应时长
const durationIndex = section.id - 1
if (durationIndex >= 0 && durationIndex < durations.length) {
return durations[durationIndex] || ''
}
2025-07-28 09:51:21 +08:00
// 默认时长生成
if (isVideoLesson(section)) {
const minutes = Math.floor(Math.random() * 60) + 10 // 10-70分钟
const seconds = Math.floor(Math.random() * 60)
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
}
return '' // 非视频课时不显示时长
}
// 判断是否为视频课时
const isVideoLesson = (section: CourseSection): boolean => {
return !!(section.outline && section.outline.includes('.m3u8'))
}
2025-08-01 01:22:09 +08:00
// 判断是否为资料课时
const isResourceLesson = (section: CourseSection): boolean => {
return !!(section.outline && section.outline.includes('ppt')) || section.name.includes('PPT')
}
// 判断是否为作业课时
const isHomeworkLesson = (section: CourseSection): boolean => {
return section.name.includes('作业') || section.name.includes('练习') || section.name.includes('题目') || section.name.includes('分析')
}
// 判断是否为考试课时
const isExamLesson = (section: CourseSection): boolean => {
return section.name.includes('考试') || section.name.includes('测试') || section.name.includes('函数&循环')
}
// 获取课时类型徽章样式类
const getLessonTypeBadgeClass = (section: CourseSection): string => {
2025-07-28 09:51:21 +08:00
if (isVideoLesson(section)) {
2025-08-01 01:22:09 +08:00
return 'badge-video'
} else if (isResourceLesson(section)) {
return 'badge-resource'
} else if (isHomeworkLesson(section)) {
return 'badge-homework'
} else if (isExamLesson(section)) {
return 'badge-exam'
2025-07-28 09:51:21 +08:00
}
2025-08-01 01:22:09 +08:00
return 'badge-video' // 默认为视频
}
// 处理下载操作
const handleDownload = (section: CourseSection) => {
console.log('下载资料:', section)
// 这里可以实现下载逻辑
alert(`下载资料: ${section.name}`)
2025-07-28 09:51:21 +08:00
}
2025-08-01 01:22:09 +08:00
// 处理作业操作
const handleHomework = (section: CourseSection) => {
console.log('打开作业:', section)
// 这里可以跳转到作业页面
alert(`打开作业: ${section.name}`)
}
// 处理考试操作
const handleExam = (section: CourseSection) => {
console.log('开始考试:', section)
// 跳转到考前须知页面
router.push({
name: 'ExamNotice',
params: {
courseId: courseId.value,
sectionId: section.id
},
query: {
courseName: course.value?.title || '课程名称',
examName: section.name
}
})
}
2025-07-28 09:51:21 +08:00
// 点击课程章节标题
const handleSectionClick = (section: CourseSection) => {
console.log('点击课程章节:', section)
// 设置当前选中的章节
currentSection.value = section
// 检查是否有视频链接
if (section.outline && section.outline.includes('.m3u8')) {
console.log('获取到视频链接:', section.outline)
// 跳转到已报名区域并播放视频
navigateToEnrolledArea(section.outline, section.name)
} else {
// 如果不是视频,显示预览
previewSection(section)
}
}
// 跳转到已报名区域
const navigateToEnrolledArea = (videoUrl: string, sectionName: string) => {
console.log('跳转到已报名区域,播放视频:', videoUrl)
console.log('章节名称:', sectionName)
console.log('当前章节:', currentSection.value)
// 使用路由跳转到学习页面
router.push({
name: 'CourseStudy',
params: { id: courseId.value },
query: {
videoUrl: encodeURIComponent(videoUrl),
sectionName: encodeURIComponent(sectionName),
sectionId: currentSection.value?.id
}
})
}
// 更新视频播放器(备用方案)
// const updateVideoPlayer = (videoUrl: string, sectionName: string) => {
// console.log('更新视频播放器:', { videoUrl, sectionName })
// // 如果在同一页面内更新视频播放器
// // 可以通过事件总线或状态管理来实现
// // 这里先显示确认信息
// const confirmed = confirm(`即将播放视频: ${sectionName}\n是否继续`)
// if (confirmed) {
// navigateToEnrolledArea(videoUrl, sectionName)
// }
// }
2025-07-28 09:51:21 +08:00
// 预览章节(非视频内容)
const previewSection = (section: CourseSection) => {
console.log('预览章节:', section)
previewModalTitle.value = section.name
previewModalContent.value = `章节ID: ${section.id}\n章节名称: ${section.name}\n内容类型: ${getLessonTypeText(section)}`
previewModalType.value = 'section'
previewModalVisible.value = true
}
// 关闭预览模态框
const closePreviewModal = () => {
previewModalVisible.value = false
previewModalTitle.value = ''
previewModalContent.value = ''
previewModalType.value = ''
}
// 处理课程报名
const handleEnrollCourse = () => {
enrollCourse(courseId.value)
}
// 测试直接API调用
const testDirectApiCall = async () => {
console.log('=== 开始测试直接API调用 ===')
console.log('课程ID:', courseId.value)
try {
// 使用axios直接调用API
const axios = (await import('axios')).default
const url = `http://110.42.96.65:55510/api/lesson/section/list?lesson_id=${courseId.value}`
console.log('请求URL:', url)
const response = await axios.get(url)
console.log('直接API调用成功:', response.data)
alert('API调用成功请查看控制台')
} catch (error) {
console.error('直接API调用失败:', error)
alert('API调用失败请查看控制台')
}
}
2025-07-22 14:39:45 +08:00
onMounted(() => {
console.log('课程详情页加载完成课程ID:', courseId.value)
2025-07-28 09:51:21 +08:00
loadCourseDetail()
loadCourseSections()
2025-07-22 14:39:45 +08:00
})
</script>
<style scoped>
.course-detail-page {
min-height: 100vh;
background: #f5f7fa;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.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;
}
2025-07-22 14:39:45 +08:00
.container {
2025-07-28 09:51:21 +08:00
max-width: 1600px;
2025-07-22 14:39:45 +08:00
margin: 0 auto;
2025-07-28 09:51:21 +08:00
padding: 0 142px;
2025-07-22 14:39:45 +08:00
}
.breadcrumb {
background: white;
2025-07-22 14:39:45 +08:00
padding: 12px 0;
border-bottom: 1px solid #e8e8e8;
2025-07-22 14:39:45 +08:00
}
.breadcrumb-text {
color: #666;
font-size: 14px;
}
.main-content {
padding: 20px 0;
2025-07-22 14:39:45 +08:00
}
.content-layout {
display: flex;
2025-07-22 14:39:45 +08:00
gap: 40px;
align-items: flex-start;
}
2025-07-28 09:51:21 +08:00
.course-content {
display: flex;
gap: 30px;
width: 100%;
}
2025-07-28 09:51:21 +08:00
.main-column {
flex: 1;
background: white;
border-radius: 8px;
overflow: hidden;
2025-07-28 09:51:21 +08:00
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.sidebar {
width: 320px;
flex-shrink: 0;
}
/* 视频播放器区域 */
.video-player-section {
position: relative;
height: 400px;
background: #000;
2025-07-22 14:39:45 +08:00
}
.video-player.unregistered {
2025-07-28 09:51:21 +08:00
height: 100%;
2025-07-22 14:39:45 +08:00
position: relative;
}
.video-background {
2025-07-22 14:39:45 +08:00
width: 100%;
2025-07-28 09:51:21 +08:00
height: 100%;
background-size: cover;
background-position: center;
2025-07-22 14:39:45 +08:00
position: relative;
}
.video-background::before {
content: '';
2025-07-22 14:39:45 +08:00
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
2025-07-28 09:51:21 +08:00
background: rgba(0, 0, 0, 0.5);
2025-07-22 14:39:45 +08:00
}
.video-content {
2025-07-28 09:51:21 +08:00
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
color: white;
2025-07-28 09:51:21 +08:00
z-index: 2;
}
.course-main-title {
font-size: 28px;
font-weight: 600;
margin-bottom: 16px;
2025-07-28 09:51:21 +08:00
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
}
.course-meta-info {
margin-bottom: 24px;
2025-07-28 09:51:21 +08:00
font-size: 16px;
}
.meta-item {
2025-07-28 09:51:21 +08:00
color: rgba(255, 255, 255, 0.9);
2025-07-22 14:39:45 +08:00
}
.meta-separator {
2025-07-28 09:51:21 +08:00
margin: 0 12px;
color: rgba(255, 255, 255, 0.6);
2025-07-22 14:39:45 +08:00
}
.enroll-button {
2025-07-28 09:51:21 +08:00
background: #1890ff;
2025-07-22 14:39:45 +08:00
color: white;
border: none;
padding: 12px 32px;
border-radius: 6px;
2025-07-22 14:39:45 +08:00
font-size: 16px;
font-weight: 600;
cursor: pointer;
2025-07-28 09:51:21 +08:00
transition: all 0.3s;
2025-07-22 14:39:45 +08:00
}
.enroll-button:hover {
2025-07-28 09:51:21 +08:00
background: #40a9ff;
transform: translateY(-2px);
2025-07-28 09:51:21 +08:00
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
}
2025-07-22 14:39:45 +08:00
2025-07-28 09:51:21 +08:00
/* 课程信息区域 */
.course-info-section {
padding: 24px;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.course-header {
margin-bottom: 24px;
padding-bottom: 20px;
border-bottom: 1px solid #f0f0f0;
2025-07-22 14:39:45 +08:00
}
.course-title {
font-size: 24px;
font-weight: 600;
color: #333;
2025-07-28 09:51:21 +08:00
margin-bottom: 12px;
line-height: 1.4;
2025-07-22 14:39:45 +08:00
}
.course-meta {
display: flex;
align-items: center;
2025-07-28 09:51:21 +08:00
gap: 20px;
flex-wrap: wrap;
2025-07-22 14:39:45 +08:00
}
.course-category {
font-size: 14px;
color: #666;
}
2025-07-22 14:39:45 +08:00
.category-tag {
2025-07-28 09:51:21 +08:00
background: #e6f7ff;
color: #1890ff;
2025-07-28 09:51:21 +08:00
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
}
.course-price {
font-size: 16px;
font-weight: 600;
color: #f5222d;
}
2025-07-22 14:39:45 +08:00
.btn-notes {
2025-07-28 09:51:21 +08:00
background: #f0f0f0;
border: none;
padding: 6px 12px;
border-radius: 4px;
font-size: 14px;
2025-07-28 09:51:21 +08:00
cursor: pointer;
transition: background-color 0.3s;
2025-07-22 14:39:45 +08:00
}
.btn-notes:hover {
2025-07-28 09:51:21 +08:00
background: #d9d9d9;
2025-07-22 14:39:45 +08:00
}
/* 课程描述 */
.course-description {
2025-07-28 09:51:21 +08:00
margin-bottom: 24px;
line-height: 1.6;
2025-07-22 14:39:45 +08:00
color: #666;
2025-07-28 09:51:21 +08:00
}
.course-content-detail {
margin-top: 16px;
padding: 16px;
background: #f8f9fa;
border-radius: 6px;
}
.course-content-detail h4 {
margin-bottom: 12px;
color: #333;
2025-07-22 14:39:45 +08:00
}
/* 讲师信息 */
.instructors-section {
2025-07-28 09:51:21 +08:00
margin-bottom: 24px;
padding-bottom: 20px;
border-bottom: 1px solid #f0f0f0;
2025-07-22 14:39:45 +08:00
}
.section-title {
2025-07-28 09:51:21 +08:00
font-size: 18px;
font-weight: 600;
2025-07-22 14:39:45 +08:00
color: #333;
2025-07-28 09:51:21 +08:00
margin-bottom: 16px;
2025-07-22 14:39:45 +08:00
}
.instructor-item {
display: flex;
2025-07-28 09:51:21 +08:00
gap: 16px;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.instructor-info {
flex: 1;
2025-07-22 14:39:45 +08:00
}
.instructor-name {
2025-07-28 09:51:21 +08:00
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 4px;
2025-07-22 14:39:45 +08:00
}
.instructor-title {
2025-07-28 09:51:21 +08:00
font-size: 14px;
color: #666;
margin-bottom: 8px;
}
.instructor-bio,
.instructor-experience {
font-size: 14px;
color: #666;
line-height: 1.5;
margin-bottom: 4px;
2025-07-22 14:39:45 +08:00
}
/* 课程标签页 */
.course-tabs {
2025-07-28 09:51:21 +08:00
margin-top: 24px;
2025-07-22 14:39:45 +08:00
}
.tab-nav {
2025-07-22 14:39:45 +08:00
display: flex;
2025-07-28 09:51:21 +08:00
border-bottom: 1px solid #f0f0f0;
margin-bottom: 24px;
2025-07-22 14:39:45 +08:00
}
.tab-btn {
background: none;
border: none;
2025-07-28 09:51:21 +08:00
padding: 12px 24px;
font-size: 16px;
color: #666;
cursor: pointer;
position: relative;
transition: color 0.3s;
2025-07-22 14:39:45 +08:00
}
.tab-btn.active {
color: #1890ff;
2025-07-28 09:51:21 +08:00
font-weight: 600;
2025-07-22 14:39:45 +08:00
}
.tab-btn.active::after {
content: '';
position: absolute;
2025-07-28 09:51:21 +08:00
bottom: -1px;
left: 0;
right: 0;
height: 2px;
background: #1890ff;
2025-07-22 14:39:45 +08:00
}
.tab-btn:hover {
color: #1890ff;
2025-07-22 14:39:45 +08:00
}
.tab-content {
2025-07-28 09:51:21 +08:00
min-height: 300px;
2025-07-22 14:39:45 +08:00
}
.intro-content h4 {
font-size: 16px;
font-weight: 600;
color: #333;
2025-07-28 09:51:21 +08:00
margin: 20px 0 12px 0;
2025-07-22 14:39:45 +08:00
}
.intro-content p {
line-height: 1.6;
color: #666;
margin-bottom: 16px;
2025-07-22 14:39:45 +08:00
}
.intro-content ul {
padding-left: 20px;
2025-07-28 09:51:21 +08:00
margin-bottom: 16px;
2025-07-22 14:39:45 +08:00
}
.intro-content li {
line-height: 1.6;
color: #666;
margin-bottom: 8px;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
/* 右侧边栏课程章节 */
.sidebar .course-sections {
background: white;
border-radius: 8px;
padding: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.sections-header {
2025-07-22 14:39:45 +08:00
display: flex;
align-items: center;
2025-07-28 09:51:21 +08:00
justify-content: space-between;
padding: 16px 20px;
background: #fafafa;
border-bottom: 1px solid #e8e8e8;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.sections-header h3 {
font-size: 16px;
font-weight: 600;
color: #333;
margin: 0;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.sections-actions {
2025-07-22 14:39:45 +08:00
display: flex;
2025-07-28 09:51:21 +08:00
align-items: center;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.sort-btn {
background: none;
border: none;
color: #666;
2025-07-28 09:51:21 +08:00
padding: 4px 8px;
border-radius: 4px;
2025-07-28 09:51:21 +08:00
font-size: 12px;
cursor: pointer;
transition: all 0.3s;
2025-07-28 09:51:21 +08:00
display: flex;
align-items: center;
gap: 4px;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.sort-btn:hover {
background: #f0f0f0;
color: #333;
2025-07-22 14:39:45 +08:00
}
2025-08-01 01:22:09 +08:00
.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;
}
2025-07-28 09:51:21 +08:00
.sections-loading,
.sections-error,
.no-sections {
text-align: center;
padding: 16px;
color: #666;
font-size: 14px;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.sections-error .retry-btn {
margin-top: 8px;
background: #1890ff;
color: white;
2025-07-28 09:51:21 +08:00
border: none;
padding: 6px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.3s;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.sections-error .retry-btn:hover {
background: #40a9ff;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
/* 章节列表样式 */
.sections-content {
background: white;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.sections-list {
max-height: 600px;
overflow-y: auto;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.sections-list::-webkit-scrollbar {
width: 4px;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.sections-list::-webkit-scrollbar-track {
background: transparent;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.sections-list::-webkit-scrollbar-thumb {
background: #d9d9d9;
border-radius: 2px;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.sections-list::-webkit-scrollbar-thumb:hover {
background: #bfbfbf;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.chapter-section {
border-bottom: 1px solid #f0f0f0;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.chapter-section:last-child {
border-bottom: none;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.chapter-header {
2025-07-22 14:39:45 +08:00
display: flex;
2025-07-28 09:51:21 +08:00
align-items: center;
justify-content: space-between;
padding: 16px 20px;
background: white;
cursor: pointer;
transition: background-color 0.2s;
border-bottom: 1px solid #f5f5f5;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.chapter-header:hover {
background: #fafafa;
}
.chapter-info {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
}
.chapter-number {
font-size: 14px;
font-weight: 600;
color: #333;
min-width: 60px;
}
.chapter-title {
font-size: 14px;
font-weight: 500;
color: #333;
flex: 1;
}
.chapter-toggle {
color: #999;
2025-07-28 09:51:21 +08:00
transition: transform 0.2s ease;
display: flex;
align-items: center;
2025-07-28 09:51:21 +08:00
padding: 4px;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.chapter-toggle.expanded {
transform: rotate(90deg);
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.chapter-lessons {
background: white;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.lesson-item {
2025-08-01 01:22:09 +08:00
border-bottom: 1px solid #f0f0f0;
2025-07-28 09:51:21 +08:00
transition: background-color 0.2s;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.lesson-item:last-child {
border-bottom: none;
}
.lesson-item:hover {
background: #f9f9f9;
}
2025-08-01 01:22:09 +08:00
.lesson-content {
2025-07-28 09:51:21 +08:00
display: flex;
align-items: center;
2025-08-01 01:22:09 +08:00
padding: 12px 20px 12px 40px;
cursor: pointer;
2025-07-28 09:51:21 +08:00
gap: 12px;
2025-08-01 01:22:09 +08:00
}
.lesson-type-badge {
font-size: 12px;
padding: 4px 8px;
border-radius: 4px;
font-weight: 500;
min-width: 40px;
text-align: center;
line-height: 1;
flex-shrink: 0;
}
.lesson-info {
2025-07-28 09:51:21 +08:00
flex: 1;
2025-08-01 01:22:09 +08:00
min-width: 0;
2025-07-22 14:39:45 +08:00
}
2025-08-01 01:22:09 +08:00
.lesson-title {
font-size: 14px;
color: #333;
transition: color 0.2s;
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
2025-07-22 14:39:45 +08:00
}
2025-08-01 01:22:09 +08:00
.lesson-content:hover .lesson-title {
2025-07-28 09:51:21 +08:00
color: #1890ff;
2025-07-22 14:39:45 +08:00
}
2025-08-01 01:22:09 +08:00
.lesson-meta {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
2025-07-22 14:39:45 +08:00
}
2025-08-01 01:22:09 +08:00
.lesson-duration {
font-size: 12px;
color: #666;
min-width: 60px;
text-align: right;
}
.lesson-actions {
display: flex;
align-items: center;
gap: 8px;
}
/* 课时类型徽章样式 */
.badge-video {
background: #1890ff;
color: white;
}
.badge-resource {
background: #f5f5f5;
color: #666;
}
.badge-homework {
background: #1890ff;
color: white;
}
.badge-exam {
background: #1890ff;
color: white;
}
/* 课时操作按钮样式 */
.lesson-action-btn {
background: none;
border: none;
cursor: pointer;
padding: 4px;
border-radius: 4px;
transition: background-color 0.2s;
display: flex;
align-items: center;
justify-content: center;
2025-07-28 09:51:21 +08:00
}
2025-08-01 01:22:09 +08:00
.lesson-action-btn:hover {
background: #f0f0f0;
}
.video-btn svg {
color: #1890ff;
2025-07-28 09:51:21 +08:00
}
2025-08-01 01:22:09 +08:00
.download-btn svg {
2025-07-28 09:51:21 +08:00
color: #52c41a;
}
2025-08-01 01:22:09 +08:00
.edit-btn svg {
color: #1890ff;
}
.exam-btn svg {
color: #1890ff;
}
/* 完成状态图标 */
.completion-icon {
display: flex;
align-items: center;
justify-content: center;
2025-07-28 09:51:21 +08:00
}
.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;
2025-07-28 09:51:21 +08:00
padding: 4px;
border-radius: 50%;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.lesson-action-btn:hover {
background: #f6ffed;
transform: scale(1.1);
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.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;
2025-07-28 09:51:21 +08:00
border-radius: 8px;
width: 90%;
max-width: 600px;
max-height: 80vh;
overflow: hidden;
2025-07-28 09:51:21 +08:00
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.preview-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
2025-07-28 09:51:21 +08:00
padding: 20px 24px;
border-bottom: 1px solid #f0f0f0;
background: #fafafa;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.preview-modal-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.close-btn {
background: none;
border: none;
font-size: 24px;
color: #999;
cursor: pointer;
2025-07-28 09:51:21 +08:00
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.3s;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.close-btn:hover {
background: #f0f0f0;
color: #666;
}
2025-07-22 14:39:45 +08:00
2025-07-28 09:51:21 +08:00
.preview-modal-content {
padding: 24px;
max-height: 60vh;
overflow-y: auto;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.preview-text {
font-size: 16px;
line-height: 1.6;
color: #666;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.preview-goals ul {
margin: 0;
padding-left: 20px;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.preview-goals li {
font-size: 16px;
line-height: 1.8;
color: #666;
margin-bottom: 8px;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.preview-content {
font-size: 16px;
line-height: 1.6;
color: #666;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.preview-content h4 {
font-size: 18px;
font-weight: 600;
color: #333;
margin: 16px 0 12px 0;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.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;
2025-07-22 14:39:45 +08:00
font-weight: 600;
color: #333;
2025-07-28 09:51:21 +08:00
margin-bottom: 12px;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.outline-list > li > ul {
list-style: none;
padding-left: 0;
margin: 0;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.outline-list > li > ul > li {
margin-bottom: 6px;
padding-left: 16px;
color: #666;
2025-07-28 09:51:21 +08:00
font-size: 14px;
line-height: 1.5;
position: relative;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.outline-list > li > ul > li:before {
content: "•";
color: #1890ff;
font-weight: bold;
position: absolute;
left: 0;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
/* 评论区 */
.comments-content {
padding: 0;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.comment-stats {
display: flex;
justify-content: space-between;
2025-07-28 09:51:21 +08:00
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;
2025-07-28 09:51:21 +08:00
border-radius: 4px;
transition: all 0.3s;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.filter-btn.active,
.filter-btn:hover {
background: #e6f7ff;
color: #1890ff;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.comment-list {
display: flex;
flex-direction: column;
gap: 20px;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.comment-item {
2025-07-22 14:39:45 +08:00
display: flex;
gap: 12px;
}
2025-07-28 09:51:21 +08:00
.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;
2025-07-28 09:51:21 +08:00
margin-bottom: 12px;
}
.comment-actions {
display: flex;
gap: 16px;
}
.action-btn {
background: none;
border: none;
font-size: 12px;
2025-07-28 09:51:21 +08:00
color: #999;
cursor: pointer;
display: flex;
align-items: center;
gap: 4px;
transition: color 0.3s;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.action-btn:hover {
color: #1890ff;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.load-more {
text-align: center;
margin-top: 24px;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.btn-load-more {
background: #f0f0f0;
border: none;
padding: 8px 24px;
border-radius: 4px;
2025-07-22 14:39:45 +08:00
font-size: 14px;
2025-07-28 09:51:21 +08:00
cursor: pointer;
transition: background-color 0.3s;
}
.btn-load-more:hover {
background: #d9d9d9;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
/* 右侧边栏 */
.sidebar {
display: flex;
2025-07-28 09:51:21 +08:00
flex-direction: column;
gap: 20px;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.enroll-section {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
text-align: center;
}
2025-07-28 09:51:21 +08:00
.btn-enroll {
width: 100%;
background: #1890ff;
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
2025-07-28 09:51:21 +08:00
font-size: 16px;
font-weight: 600;
cursor: pointer;
2025-07-22 14:39:45 +08:00
transition: all 0.3s;
}
2025-07-28 09:51:21 +08:00
.btn-enroll:hover {
background: #40a9ff;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
2025-07-22 14:39:45 +08:00
}
/* 推荐课程 */
.recommended-courses {
2025-07-22 14:39:45 +08:00
background: white;
2025-07-28 09:51:21 +08:00
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.recommend-header h3 {
2025-07-28 09:51:21 +08:00
font-size: 18px;
2025-07-22 14:39:45 +08:00
font-weight: 600;
color: #333;
2025-07-28 09:51:21 +08:00
margin-bottom: 16px;
2025-07-22 14:39:45 +08:00
}
.recommend-list {
2025-07-28 09:51:21 +08:00
display: flex;
flex-direction: column;
gap: 16px;
2025-07-22 14:39:45 +08:00
}
.recommend-item {
2025-07-28 09:51:21 +08:00
border: 1px solid #f0f0f0;
border-radius: 8px;
overflow: hidden;
transition: box-shadow 0.3s;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.recommend-item:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
2025-07-22 14:39:45 +08:00
}
.recommend-image {
position: relative;
2025-07-28 09:51:21 +08:00
height: 120px;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.recommend-placeholder {
2025-07-22 14:39:45 +08:00
width: 100%;
height: 100%;
2025-07-28 09:51:21 +08:00
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 600;
font-size: 16px;
}
.computer-bg {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.english-bg {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
2025-07-22 14:39:45 +08:00
}
.recommend-badge {
position: absolute;
top: 8px;
2025-07-28 09:51:21 +08:00
right: 8px;
background: #ff4d4f;
color: white;
2025-07-28 09:51:21 +08:00
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
}
.recommend-content {
2025-07-28 09:51:21 +08:00
padding: 16px;
}
.recommend-title {
2025-07-28 09:51:21 +08:00
font-size: 16px;
font-weight: 600;
color: #333;
2025-07-28 09:51:21 +08:00
margin-bottom: 8px;
}
.recommend-tags {
2025-07-22 14:39:45 +08:00
display: flex;
2025-07-28 09:51:21 +08:00
gap: 6px;
margin-bottom: 8px;
flex-wrap: wrap;
2025-07-22 14:39:45 +08:00
}
.tag {
2025-07-28 09:51:21 +08:00
background: #f0f0f0;
color: #666;
padding: 2px 6px;
2025-07-28 09:51:21 +08:00
border-radius: 4px;
font-size: 12px;
2025-07-22 14:39:45 +08:00
}
.recommend-desc {
2025-07-28 09:51:21 +08:00
font-size: 14px;
color: #666;
2025-07-28 09:51:21 +08:00
line-height: 1.5;
margin-bottom: 12px;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
2025-07-22 14:39:45 +08:00
}
.recommend-meta {
2025-07-22 14:39:45 +08:00
display: flex;
justify-content: space-between;
align-items: center;
}
.recommend-price {
font-size: 16px;
font-weight: 600;
2025-07-28 09:51:21 +08:00
color: #f5222d;
}
2025-07-28 09:51:21 +08:00
.recommend-btn {
background: #1890ff;
2025-07-22 14:39:45 +08:00
color: white;
2025-07-28 09:51:21 +08:00
border: none;
padding: 6px 16px;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: background-color 0.3s;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.recommend-btn:hover {
background: #40a9ff;
}
2025-07-28 09:51:21 +08:00
/* 响应式设计 */
@media (max-width: 1200px) {
.container {
2025-07-28 09:51:21 +08:00
padding: 0 20px;
}
2025-07-28 09:51:21 +08:00
.course-content {
gap: 20px;
}
2025-07-28 09:51:21 +08:00
.sidebar {
width: 280px;
2025-07-22 14:39:45 +08:00
}
}
@media (max-width: 992px) {
2025-07-28 09:51:21 +08:00
.course-content {
flex-direction: column;
2025-07-22 14:39:45 +08:00
}
.sidebar {
2025-07-22 14:39:45 +08:00
width: 100%;
2025-07-28 09:51:21 +08:00
order: -1;
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
.video-player-section {
height: 300px;
2025-07-22 14:39:45 +08:00
}
}
2025-07-28 09:51:21 +08:00
@media (max-width: 768px) {
.container {
padding: 0 16px;
2025-07-22 14:39:45 +08:00
}
.course-main-title {
2025-07-28 09:51:21 +08:00
font-size: 24px;
}
2025-07-22 14:39:45 +08:00
2025-07-28 09:51:21 +08:00
.course-title {
font-size: 20px;
}
2025-07-22 14:39:45 +08:00
2025-07-28 09:51:21 +08:00
.course-meta {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
2025-07-28 09:51:21 +08:00
.tab-nav {
overflow-x: auto;
}
2025-07-28 09:51:21 +08:00
.tab-btn {
white-space: nowrap;
padding: 12px 16px;
}
2025-07-22 14:39:45 +08:00
}
2025-07-28 09:51:21 +08:00
</style>