1146 lines
26 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="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>