Files
gym-manage/gym-manage-uniapp/pages/searchCourse/searchCourse.vue
T

1621 lines
40 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>
<view class="search-page-container">
<!-- 固定头部容器 -->
<view class="fixed-header">
<!-- 搜索栏 -->
<view class="search-bar-wrapper">
<view class="search-bar">
<uni-icons type="search" size="28" color="#94a3b8"></uni-icons>
<input
class="search-input"
type="text"
placeholder="搜索课程名称..."
v-model="keyword"
@confirm="handleSearch"
:focus="isFocus"
@focus="isFocus = true"
@blur="isFocus = false"
/>
<view class="search-actions">
<view class="clear-btn" v-if="keyword" @tap="clearKeyword">
<uni-icons type="clear" size="24" color="#94a3b8"></uni-icons>
</view>
<text class="search-btn" @tap="handleSearch">搜索</text>
</view>
</view>
</view>
<!-- 筛选容器 -->
<view class="filter-container">
<!-- 快捷筛选标签默认显示 -->
<scroll-view scroll-x class="filter-scroll" :show-scrollbar="false">
<view class="filter-tags">
<view
v-for="tag in filterTags"
:key="tag.value"
:class="['filter-tag', { active: activeFilter === tag.value }]"
@tap="activeFilter = tag.value; handleSearch()"
>
{{ tag.label }}
</view>
</view>
</scroll-view>
<!-- 下拉展开箭头 -->
<view class="filter-expand" @tap="toggleExpand">
<view class="expand-content">
<text class="expand-text">{{ showAdvancedFilter ? '收起筛选' : '展开筛选' }}</text>
<uni-icons
type="chevron-down"
size="24"
color="#f97316"
:class="{ expanded: showAdvancedFilter }"
></uni-icons>
</view>
</view>
<!-- 高级筛选内容可折叠 -->
<view class="advanced-content" :class="{ expanded: showAdvancedFilter }">
<!-- 课程级别 -->
<view class="filter-group">
<text class="filter-label">课程级别</text>
<view class="filter-options">
<view
v-for="level in levelOptions"
:key="level.value"
:class="['filter-option', { active: selectedLevelValue === level.value }]"
@tap="toggleLevel(level.value)"
>
{{ level.label }}
</view>
</view>
</view>
<!-- 课程时长 -->
<view class="filter-group">
<text class="filter-label">课程时长</text>
<view class="filter-options">
<view
v-for="duration in durationOptions"
:key="duration.value"
:class="['filter-option', { active: selectedDurationValue === duration.value }]"
@tap="toggleDuration(duration.value)"
>
{{ duration.label }}
</view>
</view>
</view>
<!-- 排序方式 -->
<view class="filter-group">
<text class="filter-label">排序方式</text>
<view class="filter-options">
<view
v-for="sort in sortOptions"
:key="sort.value"
:class="['filter-option', { active: selectedSortValue === sort.value }]"
@tap="setSort(sort.value)"
>
{{ sort.label }}
</view>
</view>
</view>
<!-- 筛选操作按钮 -->
<view class="filter-actions">
<view class="btn-reset" @tap="resetFilters">重置</view>
</view>
</view>
</view>
</view>
<!-- 搜索结果列表 -->
<scroll-view
scroll-y
class="course-scroll"
:scroll-with-animation="true"
@scroll="handleScroll"
@scrolltolower="loadMore"
>
<!-- 刷新状态提示 -->
<view class="refresh-hint" v-if="isRefreshing">
<view class="refresh-content">
<view class="refresh-spinner">
<view class="spinner"></view>
</view>
<text class="refresh-text">刷新中...</text>
</view>
</view>
<view class="course-list">
<view
class="course-card"
v-for="course in courses"
:key="course.id"
@tap="handleCourseClick(course)"
>
<view class="card-image-wrapper">
<image
class="card-image"
:src="course.image"
mode="aspectFill"
lazy-load
></image>
<view class="image-overlay"></view>
<view :class="['course-tag', course.tagType]">{{ course.tag }}</view>
</view>
<view class="card-content">
<text class="course-name">{{ course.name }}</text>
<view class="course-meta">
<view class="meta-item duration">
<uni-icons type="clock" size="20" color="#64748b"></uni-icons>
<text>{{ course.duration }}</text>
</view>
<view class="meta-item level">
<text>{{ course.level }}</text>
</view>
<view class="meta-item course-type">
<text>{{ course.courseType }}</text>
</view>
</view>
<view class="course-footer">
<view class="participants">
<uni-icons type="users" size="20" color="#94a3b8"></uni-icons>
<text class="count">{{ course.participants }}</text>
<text class="label">人已报名</text>
</view>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-if="courses.length === 0 && !loading">
<view class="empty-icon">
<uni-icons type="search" size="80" color="#cbd5e1"></uni-icons>
</view>
<text class="empty-title">未找到相关课程</text>
<text class="empty-desc">试试其他关键词或筛选条件</text>
<view class="empty-action" @tap="resetSearch">重新搜索</view>
</view>
<!-- 加载状态 -->
<view class="loading-state" v-if="loading">
<view class="loading-spinner">
<view class="spinner"></view>
</view>
<text class="loading-text">加载中...</text>
</view>
<!-- 加载更多 -->
<view class="load-more" v-if="hasMore && !loading && !loadingMore">
<text>上拉加载更多</text>
</view>
<!-- 加载更多中 -->
<view class="loading-more-state" v-if="loadingMore">
<view class="loading-spinner small">
<view class="spinner"></view>
</view>
<text class="loading-text">加载更多中...</text>
</view>
<!-- 没有更多数据 -->
<view class="no-more" v-if="!hasMore && !loading && !loadingMore">
<text>- 已加载全部课程 -</text>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { getGroupCoursePage } from '@/api/main.js'
import { onPullDownRefresh } from '@dcloudio/uni-app'
// 测试模式:true 使用假数据,false 使用网络请求
const TEST_MODE = false
// 搜索关键词
const keyword = ref('')
// 当前页
const currentPage = ref(0)
// 每页数量
const pageSize = 10
// 是否有更多数据
const hasMore = ref(true)
// 是否正在加载更多
const loadingMore = ref(false)
// 活动筛选标签
const activeFilter = ref('all')
// 课程列表
const courses = ref([])
// 加载状态
const loading = ref(false)
// 是否已加载过默认数据
const loadedDefaultData = ref(false)
// 搜索框焦点状态
const isFocus = ref(false)
// 是否显示高级筛选
const showAdvancedFilter = ref(false)
// 是否正在下拉刷新
const isRefreshing = ref(false)
// 是否正在滚动(用于节流收起操作)
const isScrolling = ref(false)
// 滚动节流定时器
let scrollTimer = null
// 处理滚动事件
const handleScroll = (e) => {
// 如果高级筛选展开,自动收起
if (showAdvancedFilter.value && !isScrolling.value) {
isScrolling.value = true
showAdvancedFilter.value = false
// 清除之前的定时器
if (scrollTimer) {
clearTimeout(scrollTimer)
}
// 300ms 后重置滚动状态
scrollTimer = setTimeout(() => {
isScrolling.value = false
}, 300)
}
}
// 高级筛选选项(使用简单变量名方便模板绑定)
const selectedLevelValue = ref('')
const selectedDurationValue = ref('')
const selectedSortValue = ref('id')
// 快捷筛选标签
const filterTags = [
{ label: '全部', value: 'all' },
{ label: '瑜伽', value: 'yoga' },
{ label: '力量', value: 'strength' },
{ label: '有氧', value: 'cardio' },
{ label: '舞蹈', value: 'dance' },
{ label: '普拉提', value: 'pilates' }
]
// 课程级别选项
const levelOptions = [
{ label: '初级', value: '初级' },
{ label: '中级', value: '中级' },
{ label: '高级', value: '高级' }
]
// 课程时长选项
const durationOptions = [
{ label: '30分钟内', value: 'short' },
{ label: '30-60分钟', value: 'medium' },
{ label: '60分钟以上', value: 'long' }
]
// 排序选项
const sortOptions = [
{ label: '默认', value: 'id' },
{ label: '最热门', value: 'participants' },
{ label: '最新开课', value: 'startTime' },
{ label: '时长最短', value: 'duration' }
]
// 切换课程级别筛选(选择后立即搜索)
const toggleLevel = (level) => {
selectedLevelValue.value = selectedLevelValue.value === level ? '' : level
handleSearch()
}
// 切换课程时长筛选(选择后立即搜索)
const toggleDuration = (duration) => {
selectedDurationValue.value = selectedDurationValue.value === duration ? '' : duration
handleSearch()
}
// 设置排序方式(选择后立即搜索)
const setSort = (sort) => {
selectedSortValue.value = sort
handleSearch()
}
// 下拉刷新处理
const onRefresh = () => {
if (isRefreshing.value) return
isRefreshing.value = true
currentPage.value = 0
loadedDefaultData.value = false
// 重新获取数据
fetchCourses(keyword.value, activeFilter.value, 0, pageSize, false)
.finally(() => {
isRefreshing.value = false
// 停止下拉刷新动画
uni.stopPullDownRefresh()
})
}
// 监听下拉刷新(页面级别)
onPullDownRefresh(() => {
onRefresh()
})
// 获取课程列表
const fetchCourses = async (searchKeyword = '', filter = 'all', page = 0, size = 10, append = false) => {
if (loading.value) return
loading.value = true
try {
let res
// 测试模式:使用假数据
if (TEST_MODE) {
// 模拟网络延迟
await new Promise(resolve => setTimeout(resolve, 500))
// 生成假数据(传递所有筛选参数)
console.log('筛选参数:', { keyword: searchKeyword, filter, level: selectedLevelValue.value, duration: selectedDurationValue.value, sort: selectedSortValue.value })
res = generateMockData(page, size, filter, selectedLevelValue.value, selectedDurationValue.value, selectedSortValue.value, searchKeyword)
} else {
// 正常模式:使用网络请求
const params = {
page: page,
size: size,
sort: selectedSortValue.value,
order: selectedSortValue.value === 'duration' ? 'asc' : 'desc'
}
// 添加关键词搜索参数
if (searchKeyword && searchKeyword.trim()) {
params.keyword = searchKeyword.trim()
}
res = await getGroupCoursePage(params, { cache: true, cacheTime: 5 * 60 * 1000 })
}
if (res && res.content) {
let filteredCourses = res.content
// 非测试模式下,在客户端应用筛选条件(测试模式下假数据已过滤)
if (!TEST_MODE) {
// 0. 过滤已结束和人满的团课
filteredCourses = filteredCourses.filter(course => {
// 过滤已结束的课程
if (course.status === '2') {
return false
}
// 过滤人满的课程(报名人数 >= 最大人数)
if (course.currentMembers && course.maxMembers && course.currentMembers >= course.maxMembers) {
return false
}
return true
})
// 1. 应用关键词搜索
if (searchKeyword && searchKeyword.trim()) {
const keyword = searchKeyword.trim().toLowerCase()
filteredCourses = filteredCourses.filter(course => {
const courseName = (course.courseName || '').toLowerCase()
return courseName.includes(keyword)
})
}
// 应用快捷筛选
if (filter !== 'all') {
filteredCourses = filteredCourses.filter(course => {
const courseName = course.courseName || ''
switch (filter) {
case 'yoga':
return courseName.includes('瑜伽') || course.courseType === '1'
case 'strength':
return courseName.includes('力量') || courseName.includes('器械') || course.courseType === '3'
case 'cardio':
return courseName.includes('有氧') || courseName.includes('动感') || course.courseType === '2'
case 'dance':
return courseName.includes('舞蹈') || course.courseType === '4'
case 'pilates':
return courseName.includes('普拉提') || course.courseType === '5'
default:
return true
}
})
}
// 应用课程级别筛选
if (selectedLevelValue.value) {
filteredCourses = filteredCourses.filter(course => course.level === selectedLevelValue.value)
}
// 应用时长筛选
if (selectedDurationValue.value) {
filteredCourses = filteredCourses.filter(course => {
const duration = course.duration || 60
switch (selectedDurationValue.value) {
case 'short':
return duration <= 30
case 'medium':
return duration > 30 && duration <= 60
case 'long':
return duration > 60
default:
return true
}
})
}
}
// 转换数据格式
const formattedCourses = filteredCourses.map(course => ({
id: course.id,
image: course.coverImage || 'https://images.unsplash.com/photo-1534438327276-14e5300c3a48?w=400&q=80',
tag: getTag(course),
tagType: getTagType(course),
courseType: getCourseTypeName(course),
name: course.courseName || '未知课程',
duration: course.duration ? `${course.duration}分钟` : '60分钟',
level: getLevel(course),
participants: course.currentMembers || 0,
rawData: course
}))
if (append) {
courses.value = [...courses.value, ...formattedCourses]
} else {
courses.value = formattedCourses
}
// 判断是否还有更多数据
hasMore.value = res.content.length >= size
}
} catch (err) {
console.error('获取课程失败:', err)
if (!append) {
useDefaultData()
}
} finally {
loading.value = false
}
}
// 获取课程标签
const getTag = (course) => {
if (course.status === '2') {
return '已结束'
}
if (course.currentMembers && course.maxMembers && course.currentMembers >= course.maxMembers * 0.8) {
return '热门'
}
if (course.isNew) {
return '新课'
}
return '推荐'
}
// 获取课程类型名称
const getCourseTypeName = (course) => {
const typeMap = {
'1': '瑜伽',
'2': '有氧',
'3': '力量',
'4': '舞蹈',
'5': '普拉提'
}
return typeMap[course.courseType] || '其他'
}
// 获取标签类型
const getTagType = (course) => {
if (course.status === '2') {
return 'disabled'
}
if (course.currentMembers && course.maxMembers && course.currentMembers >= course.maxMembers * 0.8) {
return 'hot'
}
if (course.isNew) {
return 'new'
}
return 'default'
}
// 获取课程级别
const getLevel = (course) => {
const level = course.level || '初级'
return level
}
// 生成测试假数据
const generateMockData = (page, size, filter, level = '', duration = '', sort = 'id', keyword = '') => {
const allCourses = [
{
id: 1,
courseName: 'HIIT高强度燃脂',
coverImage: 'https://images.unsplash.com/photo-1534438327276-14e5300c3a48?w=400&q=80',
duration: 30,
level: '中级',
currentMembers: 4587,
maxMembers: 5000,
courseType: '2',
status: '1',
isNew: false
},
{
id: 2,
courseName: '力量进阶训练',
coverImage: 'https://images.unsplash.com/photo-1583454110551-21f2fa2afe61?w=400&q=80',
duration: 45,
level: '高级',
currentMembers: 6231,
maxMembers: 8000,
courseType: '3',
status: '1',
isNew: false
},
{
id: 3,
courseName: '瑜伽·身心平衡',
coverImage: 'https://images.unsplash.com/photo-1544367567-0f2fcb009e0b?w=400&q=80',
duration: 60,
level: '初级',
currentMembers: 3210,
maxMembers: 4000,
courseType: '1',
status: '1',
isNew: true
},
{
id: 4,
courseName: '动感单车',
coverImage: 'https://images.unsplash.com/photo-1549880338-65ddcdfd017b?w=400&q=80',
duration: 45,
level: '中级',
currentMembers: 2156,
maxMembers: 3000,
courseType: '2',
status: '1',
isNew: false
},
{
id: 5,
courseName: '普拉提核心',
coverImage: 'https://images.unsplash.com/photo-1517841905240-472988babdf9?w=400&q=80',
duration: 50,
level: '中级',
currentMembers: 1876,
maxMembers: 2500,
courseType: '5',
status: '1',
isNew: true
},
{
id: 6,
courseName: '有氧舞蹈',
coverImage: 'https://images.unsplash.com/photo-1504384308090-c894fdcc538d?w=400&q=80',
duration: 40,
level: '初级',
currentMembers: 2987,
maxMembers: 3500,
courseType: '4',
status: '1',
isNew: false
},
{
id: 7,
courseName: '核心训练',
coverImage: 'https://images.unsplash.com/photo-1534438327276-14e5300c3a48?w=400&q=80',
duration: 25,
level: '高级',
currentMembers: 3567,
maxMembers: 4000,
courseType: '3',
status: '1',
isNew: false
},
{
id: 8,
courseName: '冥想放松',
coverImage: 'https://images.unsplash.com/photo-1544367567-0f2fcb009e0b?w=400&q=80',
duration: 60,
level: '初级',
currentMembers: 987,
maxMembers: 1500,
courseType: '1',
status: '1',
isNew: true
},
{
id: 9,
courseName: '搏击操',
coverImage: 'https://images.unsplash.com/photo-1517841905240-472988babdf9?w=400&q=80',
duration: 45,
level: '中级',
currentMembers: 1890,
maxMembers: 2500,
courseType: '4',
status: '1',
isNew: false
},
{
id: 10,
courseName: '柔韧性训练',
coverImage: 'https://images.unsplash.com/photo-1583454110551-21f2fa2afe61?w=400&q=80',
duration: 40,
level: '初级',
currentMembers: 1567,
maxMembers: 2000,
courseType: '1',
status: '1',
isNew: false
},
{
id: 11,
courseName: '高强度间歇',
coverImage: 'https://images.unsplash.com/photo-1534438327276-14e5300c3a48?w=400&q=80',
duration: 30,
level: '高级',
currentMembers: 4234,
maxMembers: 5000,
courseType: '2',
status: '1',
isNew: false
},
{
id: 12,
courseName: '器械力量',
coverImage: 'https://images.unsplash.com/photo-1583454110551-21f2fa2afe61?w=400&q=80',
duration: 55,
level: '高级',
currentMembers: 2765,
maxMembers: 3500,
courseType: '3',
status: '1',
isNew: true
},
{
id: 13,
courseName: '流瑜伽',
coverImage: 'https://images.unsplash.com/photo-1544367567-0f2fcb009e0b?w=400&q=80',
duration: 70,
level: '中级',
currentMembers: 2345,
maxMembers: 3000,
courseType: '1',
status: '1',
isNew: false
},
{
id: 14,
courseName: '拉丁舞',
coverImage: 'https://images.unsplash.com/photo-1504384308090-c894fdcc538d?w=400&q=80',
duration: 50,
level: '初级',
currentMembers: 1876,
maxMembers: 2500,
courseType: '4',
status: '1',
isNew: false
},
{
id: 15,
courseName: '普拉提进阶',
coverImage: 'https://images.unsplash.com/photo-1517841905240-472988babdf9?w=400&q=80',
duration: 60,
level: '高级',
currentMembers: 1432,
maxMembers: 2000,
courseType: '5',
status: '1',
isNew: true
}
]
// 根据筛选条件过滤
let filteredCourses = allCourses
// 0. 过滤已结束和人满的团课
filteredCourses = filteredCourses.filter(course => {
// 过滤已结束的课程
if (course.status === '2') {
return false
}
// 过滤人满的课程(报名人数 >= 最大人数)
if (course.currentMembers && course.maxMembers && course.currentMembers >= course.maxMembers) {
return false
}
return true
})
// 1. 关键词搜索
if (keyword && keyword.trim()) {
const searchKeyword = keyword.trim().toLowerCase()
filteredCourses = filteredCourses.filter(course => {
const courseName = (course.courseName || '').toLowerCase()
return courseName.includes(searchKeyword)
})
}
// 2. 快捷筛选(课程类型)
if (filter !== 'all') {
filteredCourses = filteredCourses.filter(course => {
const courseName = course.courseName || ''
switch (filter) {
case 'yoga':
return courseName.includes('瑜伽') || course.courseType === '1'
case 'strength':
return courseName.includes('力量') || courseName.includes('器械') || course.courseType === '3'
case 'cardio':
return courseName.includes('有氧') || courseName.includes('动感') || course.courseType === '2'
case 'dance':
return courseName.includes('舞蹈') || courseName.includes('拉丁') || course.courseType === '4'
case 'pilates':
return courseName.includes('普拉提') || course.courseType === '5'
default:
return true
}
})
}
// 2. 课程级别过滤
if (level) {
filteredCourses = filteredCourses.filter(course => {
return course.level === level
})
}
// 3. 课程时长过滤
if (duration) {
filteredCourses = filteredCourses.filter(course => {
const courseDuration = course.duration || 0
switch (duration) {
case 'short':
return courseDuration <= 30
case 'medium':
return courseDuration > 30 && courseDuration <= 60
case 'long':
return courseDuration > 60
default:
return true
}
})
}
// 4. 排序处理
filteredCourses.sort((a, b) => {
switch (sort) {
case 'participants':
// 最热门(按参与人数降序)
return b.currentMembers - a.currentMembers
case 'startTime':
// 最新开课(按id降序模拟)
return b.id - a.id
case 'duration':
// 时长最短(按时长升序)
return a.duration - b.duration
case 'id':
default:
// 默认排序(按id升序)
return a.id - b.id
}
})
// 分页处理
const start = page * size
const end = start + size
const paginatedCourses = filteredCourses.slice(start, end)
// 模拟分页响应结构
return {
content: paginatedCourses,
totalPages: Math.ceil(filteredCourses.length / size),
totalElements: filteredCourses.length,
currentPage: page,
pageSize: size,
first: page === 0,
last: end >= filteredCourses.length
}
}
// 使用默认数据
const useDefaultData = () => {
courses.value = [
{
id: 1,
image: 'https://images.unsplash.com/photo-1534438327276-14e5300c3a48?w=400&q=80',
tag: '热门',
tagType: 'hot',
name: 'HIIT高强度燃脂',
duration: '30分钟',
level: '中级',
participants: 4587
},
{
id: 2,
image: 'https://images.unsplash.com/photo-1583454110551-21f2fa2afe61?w=400&q=80',
tag: '推荐',
tagType: 'default',
name: '力量进阶训练',
duration: '45分钟',
level: '高级',
participants: 6231
},
{
id: 3,
image: 'https://images.unsplash.com/photo-1544367567-0f2fcb009e0b?w=400&q=80',
tag: '新课',
tagType: 'new',
name: '瑜伽·身心平衡',
duration: '60分钟',
level: '初级',
participants: 3210
},
{
id: 4,
image: 'https://images.unsplash.com/photo-1549880338-65ddcdfd017b?w=400&q=80',
tag: '推荐',
tagType: 'default',
name: '动感单车',
duration: '45分钟',
level: '中级',
participants: 2156
},
{
id: 5,
image: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=400&q=80',
tag: '推荐',
tagType: 'default',
name: '普拉提核心',
duration: '50分钟',
level: '初级',
participants: 1890
},
{
id: 6,
image: 'https://images.unsplash.com/photo-1534438327276-14e5300c3a48?w=400&q=80',
tag: '热门',
tagType: 'hot',
name: '搏击训练',
duration: '60分钟',
level: '高级',
participants: 3456
},
{
id: 7,
image: 'https://images.unsplash.com/photo-1501426026826-31c667bdf23d?w=400&q=80',
tag: '新课',
tagType: 'new',
name: '爵士舞基础',
duration: '55分钟',
level: '初级',
participants: 1234
},
{
id: 8,
image: 'https://images.unsplash.com/photo-1544025162-d76694265947?w=400&q=80',
tag: '推荐',
tagType: 'default',
name: '核心力量训练',
duration: '40分钟',
level: '中级',
participants: 2890
}
]
}
// 搜索处理
const handleSearch = () => {
currentPage.value = 0
hasMore.value = true
fetchCourses(keyword.value, activeFilter.value, 0, pageSize, false)
}
// 加载更多
const loadMore = () => {
if (hasMore.value && !loading.value && !loadingMore.value) {
loadingMore.value = true
currentPage.value++
fetchCourses(keyword.value, activeFilter.value, currentPage.value, pageSize, true)
.catch(() => {
// 加载失败时添加默认数据
if (!loadedDefaultData.value) {
appendDefaultData()
}
})
.finally(() => {
loadingMore.value = false
})
}
}
// 追加默认数据(当后端数据不足时使用)
const appendDefaultData = () => {
if (loadedDefaultData.value) return
loadedDefaultData.value = true
const defaultCourses = [
{
id: 1001,
image: 'https://images.unsplash.com/photo-1517841905240-472988babdf9?w=400&q=80',
tag: '推荐',
tagType: 'default',
name: '搏击操',
duration: '45分钟',
level: '中级',
participants: 1890
},
{
id: 1002,
image: 'https://images.unsplash.com/photo-1504384308090-c894fdcc538d?w=400&q=80',
tag: '新课',
tagType: 'new',
name: '核心训练',
duration: '30分钟',
level: '初级',
participants: 1245
},
{
id: 1003,
image: 'https://images.unsplash.com/photo-1534438327276-14e5300c3a48?w=400&q=80',
tag: '热门',
tagType: 'hot',
name: 'HIIT全身燃脂',
duration: '25分钟',
level: '高级',
participants: 3567
},
{
id: 1004,
image: 'https://images.unsplash.com/photo-1544367567-0f2fcb009e0b?w=400&q=80',
tag: '推荐',
tagType: 'default',
name: '冥想放松',
duration: '60分钟',
level: '初级',
participants: 987
},
{
id: 1005,
image: 'https://images.unsplash.com/photo-1549880338-65ddcdfd017b?w=400&q=80',
tag: '热门',
tagType: 'hot',
name: '有氧舞蹈',
duration: '50分钟',
level: '中级',
participants: 2876
},
{
id: 1006,
image: 'https://images.unsplash.com/photo-1583454110551-21f2fa2afe61?w=400&q=80',
tag: '推荐',
tagType: 'default',
name: '柔韧性训练',
duration: '40分钟',
level: '初级',
participants: 1567
}
]
// 根据当前课程数量决定添加多少默认数据
const neededCount = pageSize - (courses.value.length % pageSize)
const addCount = Math.min(neededCount, defaultCourses.length)
if (addCount > 0) {
courses.value = [...courses.value, ...defaultCourses.slice(0, addCount)]
}
// 如果添加了默认数据,标记为没有更多
hasMore.value = false
}
// 清除关键词
const clearKeyword = () => {
keyword.value = ''
handleSearch()
}
// 重置所有筛选
const resetSearch = () => {
keyword.value = ''
activeFilter.value = 'all'
showAdvancedFilter.value = false
resetFilters()
}
// 重置高级筛选
const resetFilters = () => {
selectedLevelValue.value = ''
selectedDurationValue.value = ''
selectedSortValue.value = 'id'
handleSearch()
}
// 切换展开/收起
const toggleExpand = () => {
showAdvancedFilter.value = !showAdvancedFilter.value
}
// 课程点击
const handleCourseClick = (course) => {
uni.navigateTo({
url: `/pages/course/detail?id=${course.id}`
})
}
// 组件挂载时获取数据
onMounted(() => {
fetchCourses()
})
</script>
<style lang="scss">
/* 页面容器 - 禁止整体滚动 */
.search-page-container {
height: 100vh;
background-color: #f0f4f8;
display: flex;
flex-direction: column;
overflow: hidden;
/* 阻止默认触摸滚动 */
touch-action: none;
-webkit-overflow-scrolling: touch;
}
/* 固定头部容器 */
.fixed-header {
flex-shrink: 0;
background: #ffffff;
/* 阻止触摸滚动穿透 */
touch-action: none;
}
/* 搜索栏区域 */
.search-bar-wrapper {
padding: 24rpx;
background: #ffffff;
}
/* 筛选容器 */
.filter-container {
width: 100%;
background: #ffffff;
border-radius: 0;
overflow: hidden;
}
/* 搜索栏 */
.search-bar {
display: flex;
align-items: center;
background: #f8fafc;
border-radius: 40rpx;
padding: 0 24rpx;
height: 80rpx;
border: 2rpx solid #e2e8f0;
&:active, &:focus-within {
border-color: #f97316;
box-shadow: 0 0 0 4rpx rgba(249, 115, 22, 0.1);
}
.search-input {
flex: 1;
height: 100%;
font-size: 28rpx;
color: #1a202c;
background: transparent;
margin-left: 16rpx;
}
.search-actions {
display: flex;
align-items: center;
gap: 16rpx;
margin-left: 16rpx;
}
.clear-btn {
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
}
.search-btn {
font-size: 28rpx;
color: #f97316;
font-weight: 600;
padding: 8rpx 20rpx;
}
}
/* 下拉刷新提示(列表顶部) */
.refresh-hint {
display: flex;
justify-content: center;
align-items: center;
padding: 30rpx 0;
}
/* 下拉刷新内容 */
.refresh-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 12rpx;
}
/* 刷新旋转动画 */
.refresh-spinner {
width: 40rpx;
height: 40rpx;
.spinner {
width: 100%;
height: 100%;
border: 4rpx solid #f3f3f3;
border-top: 4rpx solid #f97316;
border-radius: 50%;
animation: spin 1s linear infinite;
}
}
/* 刷新文字 */
.refresh-text {
font-size: 24rpx;
color: #94a3b8;
}
/* 快捷筛选滚动区域 */
.filter-scroll {
white-space: nowrap;
padding: 20rpx 0;
.filter-tags {
display: inline-flex;
gap: 16rpx;
padding: 0 24rpx;
}
.filter-tag {
display: inline-block;
padding: 16rpx 32rpx;
background: #f8fafc;
border-radius: 40rpx;
font-size: 26rpx;
color: #64748b;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
&.active {
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
color: #ffffff;
font-weight: 600;
box-shadow: 0 4rpx 12rpx rgba(249, 115, 22, 0.3);
}
&:active {
transform: scale(0.96);
}
}
}
/* 下拉展开按钮 */
.filter-expand {
padding: 16rpx 24rpx;
border-top: 1rpx solid #e2e8f0;
.expand-content {
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
.expand-text {
font-size: 26rpx;
color: #64748b;
}
uni-icons {
transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
&.expanded {
transform: rotate(180deg);
}
}
}
&:active {
background: #f8fafc;
}
}
/* 高级筛选内容 */
.advanced-content {
height: 0;
overflow: hidden;
opacity: 0;
transition: height 0.25s ease-out, opacity 0.15s ease-out;
background: #ffffff;
will-change: height, opacity;
transform: translateZ(0);
backface-visibility: hidden;
&.expanded {
height: 520rpx;
opacity: 1;
}
/* 筛选组 */
.filter-group {
padding: 0 24rpx;
margin-top: 20rpx;
.filter-label {
font-size: 26rpx;
color: #64748b;
margin-bottom: 12rpx;
display: block;
}
.filter-options {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
}
.filter-option {
padding: 12rpx 24rpx;
background: #f8fafc;
border-radius: 24rpx;
font-size: 24rpx;
color: #64748b;
border: 2rpx solid transparent;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
&.active {
background: #fff7ed;
color: #f97316;
border-color: #f97316;
}
&:active {
transform: scale(0.96);
}
}
}
/* 筛选操作按钮 */
.filter-actions {
display: flex;
gap: 24rpx;
padding: 0 24rpx;
margin-top: 24rpx;
.btn-reset, .btn-confirm {
flex: 1;
padding: 20rpx;
border-radius: 24rpx;
text-align: center;
font-size: 28rpx;
font-weight: 600;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.btn-reset {
background: #f8fafc;
color: #64748b;
&:active {
background: #f1f5f9;
}
}
.btn-confirm {
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
color: #ffffff;
&:active {
transform: scale(0.98);
}
}
}
}
/* 课程滚动区域 */
.course-scroll {
flex: 1;
height: 0;
/* 确保可以滚动 */
touch-action: pan-y;
-webkit-overflow-scrolling: touch;
}
/* 课程列表 */
.course-list {
display: flex;
flex-wrap: wrap;
padding: 24rpx;
margin: 0;
width: 100%;
box-sizing: border-box;
}
/* 课程卡片 */
.course-card {
width: calc(50% - 13rpx);
margin-right: 24rpx;
margin-bottom: 24rpx;
background: #ffffff;
border-radius: 24rpx;
overflow: hidden;
box-sizing: border-box;
/* 偶数个卡片移除右边距 */
&:nth-child(2n) {
margin-right: 0;
}
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
&:active {
transform: scale(0.97);
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.12);
}
.card-image-wrapper {
position: relative;
width: 100%;
padding-top: 70%;
overflow: hidden;
}
.card-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
.course-card:active & {
transform: scale(1.03);
}
}
.image-overlay {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: linear-gradient(to top, rgba(0,0,0,0.6) 0%, transparent 50%);
}
.course-tag {
position: absolute;
top: 16rpx;
right: 16rpx;
padding: 8rpx 16rpx;
border-radius: 12rpx;
font-size: 20rpx;
font-weight: 600;
color: #ffffff;
z-index: 2;
&.default {
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
}
&.hot {
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
}
&.new {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
}
&.disabled {
background: #94a3b8;
}
}
.card-content {
padding: 20rpx;
.course-name {
font-size: 30rpx;
font-weight: 700;
color: #1a202c;
margin-bottom: 12rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: block;
}
.course-meta {
display: flex;
gap: 16rpx;
margin-bottom: 16rpx;
.meta-item {
display: flex;
align-items: center;
gap: 6rpx;
padding: 6rpx 12rpx;
border-radius: 8rpx;
&.duration {
background: #f1f5f9;
font-size: 22rpx;
color: #64748b;
}
&.level {
background: #fff7ed;
font-size: 22rpx;
color: #f97316;
font-weight: 600;
}
&.course-type {
background: #ecfdf5;
font-size: 22rpx;
color: #059669;
font-weight: 500;
}
}
}
.course-footer {
.participants {
display: flex;
align-items: center;
gap: 8rpx;
.count {
font-size: 26rpx;
font-weight: 600;
color: #f97316;
}
.label {
font-size: 22rpx;
color: #94a3b8;
}
}
}
}
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 48rpx;
.empty-icon {
width: 160rpx;
height: 160rpx;
background: #f1f5f9;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 24rpx;
}
.empty-title {
font-size: 32rpx;
color: #64748b;
font-weight: 600;
margin-bottom: 12rpx;
}
.empty-desc {
font-size: 26rpx;
color: #94a3b8;
margin-bottom: 32rpx;
}
.empty-action {
padding: 20rpx 48rpx;
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
color: #ffffff;
font-size: 28rpx;
font-weight: 600;
border-radius: 40rpx;
&:active {
transform: scale(0.96);
}
}
}
/* 加载状态 */
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40rpx;
.loading-spinner {
width: 48rpx;
height: 48rpx;
.spinner {
width: 100%;
height: 100%;
border: 4rpx solid #f3f3f3;
border-top: 4rpx solid #f97316;
border-radius: 50%;
animation: spin 1s linear infinite;
}
}
.loading-text {
font-size: 26rpx;
color: #94a3b8;
margin-top: 16rpx;
}
}
/* 加载更多 */
.load-more {
padding: 32rpx;
text-align: center;
font-size: 26rpx;
color: #94a3b8;
}
/* 加载更多中 */
.loading-more-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24rpx;
.loading-spinner {
&.small {
width: 32rpx;
height: 32rpx;
.spinner {
width: 100%;
height: 100%;
border: 3rpx solid #f3f3f3;
border-top: 3rpx solid #f97316;
border-radius: 50%;
animation: spin 1s linear infinite;
}
}
}
.loading-text {
font-size: 24rpx;
color: #94a3b8;
margin-top: 12rpx;
}
}
/* 没有更多数据 */
.no-more {
padding: 32rpx;
text-align: center;
font-size: 24rpx;
color: #cbd5e1;
}
/* 旋转动画 */
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>