Files
gym-manage/gym-manage-uniapp/pages/searchCourse/searchCourse.vue
T
2026-06-06 13:25:58 +08:00

1702 lines
42 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
enhanced
:show-scrollbar="false"
:scroll-with-animation="true"
class="course-scroll"
@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
v-for="course in courses"
:key="'course-' + course.id"
class="course-card"
@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
v-for="i in skeletonCount"
:key="'skeleton-' + i"
class="skeleton-course-item"
>
<view class="skeleton-card-image-wrapper">
<view class="skeleton-card-image"></view>
</view>
<view class="skeleton-card-content">
<view class="skeleton-card-tag"></view>
<view class="skeleton-card-title"></view>
<view class="skeleton-card-meta">
<view class="skeleton-meta-item duration"></view>
<view class="skeleton-meta-item level"></view>
<view class="skeleton-meta-item participants"></view>
</view>
</view>
</view>
<!-- 空状态加载完成后且无数据时显示 -->
<view class="empty-state" v-if="courses.length === 0 && skeletonCount === 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="no-more" v-if="!hasMore && courses.length > 0">
<text>已经到底啦~</text>
</view>
</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 skeletonCount = ref(5)
// 加载状态(控制加载提示)
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
// 首次加载或搜索时显示骨架屏(数量与每页条数一致)
if (page === 0 && !append) {
skeletonCount.value = pageSize
courses.value = []
} else if (append && hasMore.value) {
// 上拉加载时也添加骨架屏(数量与每页条数一致)
skeletonCount.value += pageSize
}
try {
let res
// 仅使用假数据,不请求后端
// 模拟网络延迟:骨架屏显示3秒后再显示数据
await new Promise(resolve => setTimeout(resolve, 3000))
// 生成假数据(传递所有筛选参数)
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)
if (res && res.content) {
let filteredCourses = res.content
// 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
}))
// 渐进式替换骨架屏:逐个添加数据,同时减少骨架屏数量
for (let i = 0; i < formattedCourses.length; i++) {
// 使用微任务而非宏任务,避免阻塞滚动
await Promise.resolve()
courses.value.push(formattedCourses[i])
skeletonCount.value = Math.max(0, skeletonCount.value - 1)
}
// 判断是否还有更多数据(支持无限滚动,最多100页)
hasMore.value = res.content.length >= size && page < 99
// 如果到达底部,清空骨架屏
if (!hasMore.value) {
skeletonCount.value = 0
}
} else {
// 没有数据时清空骨架屏
skeletonCount.value = 0
}
} catch (err) {
console.error('获取课程失败:', err)
skeletonCount.value = 0
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 baseCourses = [
{
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
},
{
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
},
{
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
},
{
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
},
{
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
},
{
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
},
{
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
},
{
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
},
{
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
},
{
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
},
{
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
},
{
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
},
{
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
},
{
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
},
{
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
}
]
// 生成无限数据:通过复制基础数据并添加页码后缀来模拟无限滚动
// 支持最多100页,每页10条,总共1000条假数据
const maxPages = 5
const pageOffset = page * size
const allCourses = []
for (let i = 0; i < maxPages * size; i++) {
const baseIndex = i % baseCourses.length
const pageNum = Math.floor(i / baseCourses.length)
const baseCourse = baseCourses[baseIndex]
allCourses.push({
...baseCourse,
id: i + 1,
courseName: `${baseCourse.courseName} ${pageNum + 1}` // 添加页码后缀区分不同页的数据
})
}
// 根据筛选条件过滤
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;
-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;
min-height: 120%; /* 确保骨架屏显示时可以滚动 */
}
/* 骨架屏课程项 - 与真实卡片样式一致 */
.skeleton-course-item {
width: calc(50% - 13rpx);
margin-right: 24rpx;
margin-bottom: 24rpx;
background: #ffffff;
border-radius: 24rpx;
overflow: hidden;
box-sizing: border-box;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
&:nth-child(2n) {
margin-right: 0;
}
.skeleton-card-image-wrapper {
position: relative;
width: 100%;
padding-top: 70%;
overflow: hidden;
.skeleton-card-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(90deg, #f0f0f0 25%, #e8e8e8 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
}
.skeleton-card-content {
padding: 20rpx;
.skeleton-card-tag {
display: inline-block;
padding: 6rpx 16rpx;
background: linear-gradient(90deg, #f0f0f0 25%, #e8e8e8 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 8rpx;
margin-bottom: 16rpx;
}
.skeleton-card-title {
height: 36rpx;
width: 90%;
background: linear-gradient(90deg, #f0f0f0 25%, #e8e8e8 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 8rpx;
margin-bottom: 16rpx;
}
.skeleton-card-meta {
display: flex;
align-items: center;
gap: 24rpx;
.skeleton-meta-item {
height: 28rpx;
background: linear-gradient(90deg, #f0f0f0 25%, #e8e8e8 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 8rpx;
&.duration {
width: 100rpx;
}
&.level {
width: 80rpx;
}
&.participants {
width: 120rpx;
}
}
}
}
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
/* 课程卡片 */
.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;
margin: auto;
}
/* 旋转动画 */
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>