1146 lines
26 KiB
Vue
1146 lines
26 KiB
Vue
<template>
|
||
<div class="courses-page">
|
||
<!-- 页面头部横幅 -->
|
||
<div class="page-header">
|
||
<div class="header-content">
|
||
<h1 class="page-title">课程</h1>
|
||
<p class="page-subtitle">已收录617门视频课程</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 主要内容区域 -->
|
||
<div class="main-content">
|
||
<div class="container">
|
||
<!-- 分类筛选区域 -->
|
||
<div class="filter-section">
|
||
<!-- 学科分类 -->
|
||
<div class="filter-group">
|
||
<span class="filter-label">类型:</span>
|
||
<div class="filter-tags">
|
||
<span class="filter-tag" :class="{ active: selectedMajor === '全部' }" @click="selectMajor('全部')">全部</span>
|
||
<span v-for="category in categories" :key="category.id" class="filter-tag"
|
||
:class="{ active: selectedMajor === category.name }" @click="selectMajor(category.name)">
|
||
{{ category.name }}
|
||
</span>
|
||
<!-- 加载状态 -->
|
||
<span v-if="categoriesLoading" class="filter-tag loading">加载中...</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 专题分类 -->
|
||
<div class="filter-group">
|
||
<span class="filter-label">专题:</span>
|
||
<div class="filter-tags">
|
||
<span class="filter-tag" :class="{ active: selectedSubject === '全部' }"
|
||
@click="selectSubject('全部')">全部</span>
|
||
<span v-for="subject in subjects" :key="subject.id" class="filter-tag"
|
||
:class="{ active: selectedSubject === subject.name }" @click="selectSubject(subject.name)">
|
||
{{ subject.name }}
|
||
</span>
|
||
<!-- 加载状态 -->
|
||
<span v-if="subjectsLoading" class="filter-tag loading">加载中...</span>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
<!-- 难度分类 -->
|
||
<div class="filter-group">
|
||
<span class="filter-label">难度:</span>
|
||
<div class="filter-tags">
|
||
<span class="filter-tag" :class="{ active: selectedDifficulty === '全部' }"
|
||
@click="selectDifficulty('全部')">全部</span>
|
||
<span v-for="difficulty in difficulties" :key="difficulty.id" class="filter-tag"
|
||
:class="{ active: selectedDifficulty === difficulty.name }" @click="selectDifficulty(difficulty.name)">
|
||
{{ difficulty.name }}
|
||
</span>
|
||
<!-- 加载状态 -->
|
||
<span v-if="difficultiesLoading" class="filter-tag loading">加载中...</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 分隔线 -->
|
||
<div class="divider"></div>
|
||
|
||
<!-- 广告 -->
|
||
<section class="advertisement-section" v-if="showAdvertisement">
|
||
<div class="container">
|
||
<div class="ad-container">
|
||
<button class="close-btn" @click="closeAdvertisement">关闭</button>
|
||
<img src="/images/advertising/advertising1.png" alt="广告图片" class="ad-image" />
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 排序标签 -->
|
||
<div class="sort-tabs">
|
||
<span class="sort-tab" :class="{ active: selectedSort === 'latest' }" @click="selectSort('latest')">最新</span>
|
||
<span class="sort-tab" :class="{ active: selectedSort === 'hot' }" @click="selectSort('hot')">最热</span>
|
||
<span class="sort-tab" :class="{ active: selectedSort === 'recommended' }"
|
||
@click="selectSort('recommended')">推荐</span>
|
||
</div>
|
||
|
||
<!-- 加载状态 -->
|
||
<div class="loading-state" v-if="loading">
|
||
<div class="loading-content">
|
||
<p>正在加载课程...</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 课程网格 -->
|
||
<div class="courses-grid" v-else-if="allCourses.length > 0">
|
||
<div class="course-card" v-for="course in allCourses" :key="course.id">
|
||
<div class="course-image">
|
||
<img :src="course.thumbnail" :alt="course.title" />
|
||
</div>
|
||
<div class="course-info">
|
||
<h3 class="course-title">{{ getCourseTitle(course) }}</h3>
|
||
<div class="course-meta">
|
||
<div class="course-chapters">
|
||
<img src="/images/courses/课程总章数.png" alt="课程章节" class="meta-icon">
|
||
<span>共9章54节</span>
|
||
</div>
|
||
<div class="course-duration">
|
||
<img src="/images/courses/课程总时长.png" alt="课程时长" class="meta-icon">
|
||
<span>12小时43分钟</span>
|
||
</div>
|
||
</div>
|
||
<div class="course-footer">
|
||
<div class="course-stats">
|
||
<span class="course-students">讲师: {{ getCourseInstructors(course) }}</span>
|
||
</div>
|
||
<button class="enroll-btn" @click="goToCourseDetail(course)">去学习</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 空状态 -->
|
||
<div class="empty-state" v-else>
|
||
<div class="empty-content">
|
||
<p>暂无符合条件的课程</p>
|
||
<button class="clear-filters-btn" @click="clearAllFilters">清除筛选条件</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 分页组件 -->
|
||
<div class="pagination">
|
||
<span class="pagination-info">{{ currentPageText }}</span>
|
||
<span class="pagination-link prev" @click="goToPrevPage" :class="{ disabled: currentPage === 1 }">上一页</span>
|
||
|
||
<!-- 第一页 -->
|
||
<button v-if="totalPages > 0" class="pagination-btn page-num" :class="{ active: currentPage === 1 }"
|
||
@click="goToPage(1)">1</button>
|
||
|
||
<!-- 左侧省略号 -->
|
||
<span v-if="currentPage > 4" class="pagination-dots">...</span>
|
||
|
||
<!-- 当前页附近的页码 -->
|
||
<template v-for="page in visiblePages" :key="page">
|
||
<button v-if="page !== 1 && page !== totalPages" class="pagination-btn page-num"
|
||
:class="{ active: currentPage === page }" @click="goToPage(page)">{{ page }}</button>
|
||
</template>
|
||
|
||
<!-- 右侧省略号 -->
|
||
<span v-if="currentPage < totalPages - 3" class="pagination-dots">...</span>
|
||
|
||
<!-- 最后一页 -->
|
||
<button v-if="totalPages > 1" class="pagination-btn page-num" :class="{ active: currentPage === totalPages }"
|
||
@click="goToPage(totalPages)">{{ totalPages }}</button>
|
||
|
||
<span class="pagination-link next" @click="goToNextPage"
|
||
:class="{ disabled: currentPage === totalPages }">下一页</span>
|
||
<span class="pagination-link last" @click="goToPage(totalPages)">尾页</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, onMounted } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import type { Course, CourseCategory, CourseSubject, CourseDifficulty } from '@/api/types'
|
||
import { CourseApi } from '@/api'
|
||
|
||
const router = useRouter()
|
||
|
||
// 课程数据和加载状态
|
||
const courses = ref<Course[]>([])
|
||
const loading = ref(false)
|
||
const total = ref(0)
|
||
|
||
// 分类数据和加载状态
|
||
const categories = ref<CourseCategory[]>([])
|
||
const categoriesLoading = ref(false)
|
||
|
||
// 专题数据和加载状态
|
||
const subjects = ref<CourseSubject[]>([])
|
||
const subjectsLoading = ref(false)
|
||
|
||
// 难度数据和加载状态
|
||
const difficulties = ref<CourseDifficulty[]>([])
|
||
const difficultiesLoading = ref(false)
|
||
|
||
// 筛选状态
|
||
const selectedSubject = ref('全部')
|
||
const selectedMajor = ref('全部')
|
||
const selectedDifficulty = ref('全部')
|
||
|
||
// 排序状态
|
||
const selectedSort = ref('recommended')
|
||
|
||
// 分页相关状态
|
||
const currentPage = ref(1)
|
||
const itemsPerPage = 20
|
||
const totalItems = computed(() => total.value)
|
||
const totalPages = computed(() => Math.ceil(totalItems.value / itemsPerPage))
|
||
|
||
// 控制广告显示状态
|
||
const showAdvertisement = ref(true)
|
||
|
||
// 关闭广告函数
|
||
const closeAdvertisement = () => {
|
||
showAdvertisement.value = false
|
||
}
|
||
|
||
// 数字转中文
|
||
const numberToChinese = (num: number): string => {
|
||
const chineseNumbers = ['', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十']
|
||
if (num <= 10) {
|
||
return chineseNumbers[num]
|
||
} else if (num < 20) {
|
||
return '十' + chineseNumbers[num - 10]
|
||
} else if (num < 100) {
|
||
const tens = Math.floor(num / 10)
|
||
const ones = num % 10
|
||
return chineseNumbers[tens] + '十' + (ones > 0 ? chineseNumbers[ones] : '')
|
||
}
|
||
return num.toString()
|
||
}
|
||
|
||
// 当前页面文本
|
||
const currentPageText = computed(() => {
|
||
if (currentPage.value === 1) {
|
||
return '首页'
|
||
} else {
|
||
return `第${numberToChinese(currentPage.value)}页`
|
||
}
|
||
})
|
||
|
||
// 可见的页码范围
|
||
const visiblePages = computed(() => {
|
||
const pages = []
|
||
const current = currentPage.value
|
||
const total = totalPages.value
|
||
|
||
// 显示当前页前后2页
|
||
const start = Math.max(2, current - 2)
|
||
const end = Math.min(total - 1, current + 2)
|
||
|
||
for (let i = start; i <= end; i++) {
|
||
pages.push(i)
|
||
}
|
||
|
||
return pages
|
||
})
|
||
|
||
// 加载课程数据(使用真实API)
|
||
const loadCourses = async () => {
|
||
try {
|
||
loading.value = true
|
||
console.log('🚀 加载课程数据...')
|
||
|
||
// 构建查询参数
|
||
const queryParams: any = {}
|
||
|
||
// 根据选择的分类添加categoryId参数(分类接口返回的是{id, name}格式,传递id字段)
|
||
if (selectedMajor.value !== '全部') {
|
||
const selectedCategory = categories.value.find(cat => cat.name === selectedMajor.value)
|
||
if (selectedCategory) {
|
||
queryParams.categoryId = selectedCategory.id.toString()
|
||
console.log('🏷️ 选择的分类:', selectedCategory.name, 'ID:', selectedCategory.id)
|
||
}
|
||
}
|
||
|
||
// 根据选择的难度添加difficulty参数(难度接口返回的是{value, label}格式,传递value字段)
|
||
if (selectedDifficulty.value !== '全部') {
|
||
const selectedDiff = difficulties.value.find(diff => diff.name === selectedDifficulty.value)
|
||
if (selectedDiff) {
|
||
queryParams.difficulty = selectedDiff.id // 直接使用字符串值,不需要toString()
|
||
console.log('📊 选择的难度:', selectedDiff.name, 'Value:', selectedDiff.id)
|
||
}
|
||
}
|
||
|
||
// 根据选择的专题添加subject参数(专题接口返回的是{value, label}格式,传递value字段)
|
||
if (selectedSubject.value !== '全部') {
|
||
const selectedSubj = subjects.value.find(subj => subj.name === selectedSubject.value)
|
||
if (selectedSubj) {
|
||
queryParams.subject = selectedSubj.id // 直接使用字符串值,不需要toString()
|
||
console.log('🎯 选择的专题:', selectedSubj.name, 'Value:', selectedSubj.id)
|
||
}
|
||
}
|
||
|
||
// 根据选择的排序方式添加sort参数
|
||
if (selectedSort.value) {
|
||
queryParams.sort = selectedSort.value
|
||
console.log('📊 选择的排序方式:', selectedSort.value)
|
||
}
|
||
|
||
console.log('🔍 查询参数:', queryParams)
|
||
|
||
// 调用API
|
||
const response = await CourseApi.getCourses(queryParams)
|
||
console.log('✅ 课程API响应:', response)
|
||
|
||
if (response.code === 200 && response.data) {
|
||
courses.value = response.data
|
||
total.value = response.data.length
|
||
console.log('✅ 课程数据加载成功:', courses.value.length, '条课程')
|
||
} else {
|
||
console.warn('⚠️ 课程数据加载失败:', response.message)
|
||
courses.value = []
|
||
total.value = 0
|
||
}
|
||
} catch (error) {
|
||
console.error('❌ 加载课程失败:', error)
|
||
courses.value = []
|
||
total.value = 0
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
// 根据学科获取分类ID(这里需要根据实际后端分类来映射)
|
||
// const getCategoryIdBySubject = (subject: string): number | undefined => {
|
||
// const categoryMap: Record<string, number> = {
|
||
// '必修课': 1,
|
||
// '高分课': 2,
|
||
// '名师课堂': 3,
|
||
// '训练营': 4,
|
||
// '无考试': 5,
|
||
// '专题讲座': 6
|
||
// }
|
||
// return categoryMap[subject]
|
||
// }
|
||
|
||
// 跳转到指定页面
|
||
const goToPage = (page: number) => {
|
||
if (page >= 1 && page <= totalPages.value) {
|
||
currentPage.value = page
|
||
loadCourses()
|
||
}
|
||
}
|
||
|
||
// 上一页
|
||
const goToPrevPage = () => {
|
||
if (currentPage.value > 1) {
|
||
goToPage(currentPage.value - 1)
|
||
}
|
||
}
|
||
|
||
// 下一页
|
||
const goToNextPage = () => {
|
||
if (currentPage.value < totalPages.value) {
|
||
goToPage(currentPage.value + 1)
|
||
}
|
||
}
|
||
|
||
// 清除所有筛选条件
|
||
const clearAllFilters = () => {
|
||
selectedSubject.value = '全部'
|
||
selectedMajor.value = '全部'
|
||
selectedDifficulty.value = '全部'
|
||
selectedSort.value = 'recommended' // 重置排序为推荐
|
||
currentPage.value = 1
|
||
loadCourses()
|
||
}
|
||
|
||
// 筛选功能
|
||
const selectSubject = (subject: string) => {
|
||
selectedSubject.value = subject
|
||
currentPage.value = 1 // 重置到第一页
|
||
loadCourses()
|
||
}
|
||
|
||
const selectMajor = (major: string) => {
|
||
selectedMajor.value = major
|
||
currentPage.value = 1 // 重置到第一页
|
||
loadCourses()
|
||
}
|
||
|
||
const selectDifficulty = (difficulty: string) => {
|
||
selectedDifficulty.value = difficulty
|
||
currentPage.value = 1 // 重置到第一页
|
||
loadCourses()
|
||
}
|
||
|
||
// 当前页显示的课程数据(直接使用从API获取的数据)
|
||
const allCourses = computed(() => {
|
||
return courses.value
|
||
})
|
||
|
||
// 获取课程标题的函数
|
||
const getCourseTitle = (course: Course) => {
|
||
return course.title
|
||
}
|
||
|
||
// 获取课程讲师名称的函数
|
||
const getCourseInstructors = (course: Course) => {
|
||
// 检查是否有teacherList字段(从后端数据适配而来)
|
||
if (course.teacherList && Array.isArray(course.teacherList) && course.teacherList.length > 0) {
|
||
// 按sortOrder降序排列讲师(sortOrder越大越靠前)
|
||
const sortedTeachers = [...course.teacherList].sort((a, b) => {
|
||
const sortOrderA = a.sortOrder || 0
|
||
const sortOrderB = b.sortOrder || 0
|
||
return sortOrderB - sortOrderA // 降序排列
|
||
})
|
||
|
||
// 提取所有讲师的名字,用逗号分隔
|
||
const teacherNames = sortedTeachers.map(teacher => teacher.name).join('、')
|
||
|
||
console.log('🔍 课程讲师信息:', {
|
||
courseTitle: course.title,
|
||
originalTeachers: course.teacherList,
|
||
sortedTeachers: sortedTeachers,
|
||
teacherNames: teacherNames
|
||
})
|
||
|
||
return teacherNames
|
||
}
|
||
|
||
// 如果没有teacherList,检查instructor字段(兼容旧数据)
|
||
if (course.instructor && course.instructor.name) {
|
||
return course.instructor.name
|
||
}
|
||
|
||
// 默认值
|
||
return '暂无讲师信息'
|
||
}
|
||
|
||
// 跳转到课程详情页
|
||
const goToCourseDetail = (course: Course) => {
|
||
router.push({
|
||
name: 'CourseDetail',
|
||
params: { id: course.id }
|
||
})
|
||
}
|
||
|
||
// 选择排序方式
|
||
const selectSort = (sortType: string) => {
|
||
selectedSort.value = sortType
|
||
currentPage.value = 1 // 重置到第一页
|
||
loadCourses() // 重新加载课程数据
|
||
}
|
||
|
||
// 加载课程分类数据
|
||
const loadCategories = async () => {
|
||
try {
|
||
categoriesLoading.value = true
|
||
console.log('🚀 加载课程分类...')
|
||
|
||
const response = await CourseApi.getCategories()
|
||
console.log('✅ 分类API响应:', response)
|
||
|
||
if (response.code === 200 && response.data) {
|
||
categories.value = response.data
|
||
console.log('✅ 分类数据加载成功:', categories.value)
|
||
} else {
|
||
console.warn('⚠️ 分类数据加载失败:', response.message)
|
||
}
|
||
} catch (error) {
|
||
console.error('❌ 加载分类数据失败:', error)
|
||
} finally {
|
||
categoriesLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 加载课程专题数据
|
||
const loadSubjects = async () => {
|
||
try {
|
||
subjectsLoading.value = true
|
||
console.log('🚀 加载课程专题...')
|
||
|
||
const response = await CourseApi.getSubjects()
|
||
console.log('✅ 专题API响应:', response)
|
||
|
||
if (response.code === 200 && response.data) {
|
||
subjects.value = response.data
|
||
console.log('✅ 专题数据加载成功:', subjects.value)
|
||
} else {
|
||
console.warn('⚠️ 专题数据加载失败:', response.message)
|
||
}
|
||
} catch (error) {
|
||
console.error('❌ 加载专题数据失败:', error)
|
||
} finally {
|
||
subjectsLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 加载课程难度数据
|
||
const loadDifficulties = async () => {
|
||
try {
|
||
difficultiesLoading.value = true
|
||
console.log('🚀 加载课程难度...')
|
||
|
||
const response = await CourseApi.getDifficulties()
|
||
console.log('✅ 难度API响应:', response)
|
||
|
||
if (response.code === 200 && response.data) {
|
||
difficulties.value = response.data
|
||
console.log('✅ 难度数据加载成功:', difficulties.value)
|
||
} else {
|
||
console.warn('⚠️ 难度数据加载失败:', response.message)
|
||
}
|
||
} catch (error) {
|
||
console.error('❌ 加载难度数据失败:', error)
|
||
} finally {
|
||
difficultiesLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 组件挂载时加载数据
|
||
onMounted(() => {
|
||
loadCourses()
|
||
loadCategories()
|
||
loadSubjects()
|
||
loadDifficulties()
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
@font-face {
|
||
font-family: 'AlimamaShuHeiTiBold';
|
||
src: url('/fonts/AlimamaShuHeiTiBold.ttf') format('truetype');
|
||
}
|
||
|
||
|
||
.courses-page {
|
||
min-height: 100vh;
|
||
background: #fff;
|
||
}
|
||
|
||
.page-header {
|
||
background-image: url('/images/courses/course-bg.png');
|
||
background-size: cover;
|
||
background-position: center;
|
||
padding: 0;
|
||
text-align: center;
|
||
color: #000;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.page-header::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0;
|
||
right: 0;
|
||
width: 300px;
|
||
height: 100%;
|
||
background: url('/images/header-decoration.png') no-repeat center right;
|
||
background-size: contain;
|
||
opacity: 0.3;
|
||
}
|
||
|
||
.header-content {
|
||
max-width: 1420px;
|
||
margin: 0 auto;
|
||
padding: 0 20px;
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
.page-title {
|
||
/* 数黑体 */
|
||
font-family: 'AlimamaShuHeiTiBold';
|
||
font-size: 28px;
|
||
margin: 35px 0 5px 0;
|
||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.page-subtitle {
|
||
font-size: 14px;
|
||
opacity: 0.9;
|
||
margin-bottom: 35px;
|
||
}
|
||
|
||
.main-content {
|
||
padding: 40px 0;
|
||
}
|
||
|
||
.container {
|
||
width: 1420px;
|
||
margin: 0 auto;
|
||
padding: 0 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.filter-section {
|
||
background: transparent;
|
||
padding: 24px;
|
||
border-radius: 8px;
|
||
margin-bottom: 20px;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.divider {
|
||
width: 100%;
|
||
height: 1px;
|
||
background-color: rgba(128, 128, 128, 0.2);
|
||
}
|
||
|
||
|
||
|
||
.loading-state {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
min-height: 300px;
|
||
text-align: center;
|
||
}
|
||
|
||
.loading-content p {
|
||
color: #666;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.empty-state {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
min-height: 300px;
|
||
text-align: center;
|
||
}
|
||
|
||
.empty-content p {
|
||
color: #999;
|
||
font-size: 16px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.clear-filters-btn {
|
||
background: #1890ff;
|
||
color: white;
|
||
border: none;
|
||
padding: 10px 20px;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
.clear-filters-btn:hover {
|
||
background: #40a9ff;
|
||
}
|
||
|
||
.filter-group {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
margin-bottom: 16px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.filter-group:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.filter-label {
|
||
font-weight: 500;
|
||
color: #333;
|
||
margin-right: 16px;
|
||
min-width: 60px;
|
||
line-height: 32px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.filter-tags {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
flex: 1;
|
||
}
|
||
|
||
.filter-tag {
|
||
padding: 4px 8px;
|
||
color: #000;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
font-size: 14px;
|
||
border: 1px solid transparent;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.filter-tag:hover {
|
||
background: #e6f7ff;
|
||
color: #1890ff;
|
||
border-color: #1890ff;
|
||
}
|
||
|
||
.filter-tag.active {
|
||
background: #e6f7ff;
|
||
color: #1890ff;
|
||
border-color: transparent;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.filter-tag.loading {
|
||
background: #f0f0f0;
|
||
color: #999;
|
||
cursor: not-allowed;
|
||
opacity: 0.7;
|
||
}
|
||
|
||
.sort-tabs {
|
||
display: flex;
|
||
gap: 20px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.sort-tab {
|
||
color: #666;
|
||
font-size: 14px;
|
||
cursor: pointer;
|
||
padding: 4px 0;
|
||
border-bottom: 2px solid transparent;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.sort-tab:hover {
|
||
color: #1890ff;
|
||
}
|
||
|
||
.sort-tab.active {
|
||
color: #1890ff;
|
||
border-bottom-color: #1890ff;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.courses-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: 20px;
|
||
margin-bottom: 40px;
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.course-card {
|
||
background: white;
|
||
border-radius: 3px;
|
||
overflow: hidden;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||
transition: transform 0.2s, box-shadow 0.2s;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.course-card:hover {
|
||
transform: translateY(-4px);
|
||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||
}
|
||
|
||
.course-image {
|
||
width: 100%;
|
||
height: 208px;
|
||
overflow: hidden;
|
||
border-radius: 8px 8px 0 0;
|
||
}
|
||
|
||
.course-image img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
|
||
.course-info {
|
||
padding: 16px;
|
||
}
|
||
|
||
|
||
|
||
.course-title {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: #333;
|
||
margin: 0 0 12px 0;
|
||
line-height: 1.4;
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 2;
|
||
line-clamp: 2;
|
||
-webkit-box-orient: vertical;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.course-meta {
|
||
display: flex;
|
||
justify-content: left;
|
||
gap: 10px;
|
||
font-size: 12px;
|
||
color: #999;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.course-duration {
|
||
color: #666;
|
||
}
|
||
|
||
.course-price {
|
||
color: #ff4d4f;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.course-stats {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.course-students {
|
||
color: #999;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.course-footer {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.course-chapters,
|
||
.course-duration {
|
||
display: flex;
|
||
align-items: center;
|
||
font-size: 12px;
|
||
color: #666;
|
||
}
|
||
|
||
.meta-icon {
|
||
width: 16px;
|
||
height: 16px;
|
||
margin-right: 4px;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.course-teacher {
|
||
font-size: 12px;
|
||
color: #999;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.course-tags {
|
||
display: flex;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.course-tag {
|
||
background: transparent;
|
||
color: #999;
|
||
padding: 0;
|
||
border-radius: 0;
|
||
font-size: 12px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.enroll-btn {
|
||
background: #0286D5;
|
||
color: white;
|
||
border: none;
|
||
padding: 6px 10px;
|
||
border-radius: 20px;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
transition: background 0.2s;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.enroll-btn:hover {
|
||
background: #40a9ff;
|
||
}
|
||
|
||
.pagination {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
padding: 20px 0;
|
||
margin-top: 40px;
|
||
}
|
||
|
||
.pagination-info {
|
||
color: #666;
|
||
font-size: 14px;
|
||
margin-right: 20px;
|
||
}
|
||
|
||
.pagination-btn {
|
||
padding: 8px 12px;
|
||
border: 1px solid #d9d9d9;
|
||
background: white;
|
||
color: #666;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
transition: all 0.2s;
|
||
min-width: 40px;
|
||
text-align: center;
|
||
margin: 0 6px;
|
||
}
|
||
|
||
.pagination-btn:hover {
|
||
border-color: #1890ff;
|
||
color: #1890ff;
|
||
}
|
||
|
||
.pagination-btn.active {
|
||
background: #1890ff;
|
||
border-color: #1890ff;
|
||
color: white;
|
||
}
|
||
|
||
/* 移除了 .pagination-btn.last 样式,因为现在尾页是文本链接 */
|
||
|
||
.pagination-btn:disabled {
|
||
background: #f5f5f5;
|
||
color: #ccc;
|
||
border-color: #e6e6e6;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.pagination-btn:disabled:hover {
|
||
background: #f5f5f5;
|
||
color: #ccc;
|
||
border-color: #e6e6e6;
|
||
}
|
||
|
||
.pagination-link {
|
||
color: #666;
|
||
font-size: 14px;
|
||
cursor: pointer;
|
||
padding: 8px 12px;
|
||
margin: 0 6px;
|
||
transition: color 0.2s;
|
||
user-select: none;
|
||
}
|
||
|
||
.pagination-link:hover {
|
||
color: #1890ff;
|
||
}
|
||
|
||
.pagination-link.disabled {
|
||
color: #ccc;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.pagination-link.disabled:hover {
|
||
color: #ccc;
|
||
}
|
||
|
||
.pagination-dots {
|
||
color: #999;
|
||
padding: 0 8px;
|
||
font-size: 14px;
|
||
margin: 0 6px;
|
||
}
|
||
|
||
/* 广告区域样式 */
|
||
.advertisement-section {
|
||
padding: 40px 0;
|
||
background: white;
|
||
}
|
||
|
||
.ad-container {
|
||
width: 100%;
|
||
max-width: 1420px;
|
||
margin: 0 auto;
|
||
text-align: center;
|
||
}
|
||
|
||
.ad-image {
|
||
width: 100%;
|
||
max-width: 1420px;
|
||
height: auto;
|
||
border-radius: 8px;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.ad-container {
|
||
position: relative;
|
||
}
|
||
|
||
.close-btn {
|
||
position: absolute;
|
||
top: 0;
|
||
right: 0;
|
||
background: #CFD5D9;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 0 5px 0 0;
|
||
width: 25px;
|
||
height: 15px;
|
||
font-size: 10px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 10;
|
||
transition: background 0.3s ease;
|
||
}
|
||
|
||
.close-btn:hover {
|
||
background: rgba(0, 0, 0, 0.7);
|
||
}
|
||
|
||
/* 广告悬停效果 */
|
||
/* .ad-image:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12);
|
||
} */
|
||
|
||
.stats-grid {
|
||
display: flex;
|
||
align-items: center;
|
||
width: 100%;
|
||
margin: 0 auto;
|
||
padding: 0 247px;
|
||
}
|
||
|
||
.stat-item {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 20px;
|
||
padding: 0 45px 0 25px;
|
||
}
|
||
|
||
.stat-item:last-child {
|
||
border-right: none;
|
||
}
|
||
|
||
.stat-item:last-child {
|
||
border-right: none;
|
||
}
|
||
|
||
.line {
|
||
width: 1px;
|
||
height: 50px;
|
||
background: #D0E8F5;
|
||
}
|
||
|
||
.stat-icon {
|
||
flex-shrink: 0;
|
||
width: 74px;
|
||
height: 74px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.stat-icon img {
|
||
width: 74px;
|
||
height: 74px;
|
||
object-fit: contain;
|
||
display: block;
|
||
}
|
||
|
||
/* 响应式设计 */
|
||
@media (min-width: 1600px) {
|
||
.courses-grid {
|
||
grid-template-columns: repeat(5, 1fr);
|
||
}
|
||
}
|
||
|
||
@media (min-width: 1400px) {
|
||
.container {
|
||
max-width: 1420px;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 1400px) {
|
||
.courses-grid {
|
||
grid-template-columns: repeat(3, 1fr);
|
||
}
|
||
}
|
||
|
||
@media (max-width: 1420px) {
|
||
.container {
|
||
max-width: calc(100vw - 160px);
|
||
padding: 0 20px;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 992px) {
|
||
.courses-grid {
|
||
grid-template-columns: repeat(3, 1fr);
|
||
}
|
||
|
||
.page-title {
|
||
font-size: 36px;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.container {
|
||
max-width: calc(100vw - 40px);
|
||
padding: 0 20px;
|
||
}
|
||
|
||
.page-title {
|
||
font-size: 28px;
|
||
}
|
||
|
||
.filter-group {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 8px;
|
||
}
|
||
|
||
.filter-label {
|
||
min-width: auto;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.courses-grid {
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 16px;
|
||
}
|
||
|
||
.pagination {
|
||
flex-wrap: wrap;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.pagination-info {
|
||
margin-right: 0;
|
||
margin-bottom: 8px;
|
||
width: 100%;
|
||
text-align: center;
|
||
}
|
||
|
||
.pagination-btn,
|
||
.pagination-link {
|
||
margin: 4px;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.container {
|
||
margin: 0 20px;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
.courses-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.container {
|
||
margin: 0 16px;
|
||
}
|
||
|
||
.filter-section {
|
||
padding: 16px;
|
||
}
|
||
}
|
||
</style>
|