1621 lines
40 KiB
Vue
1621 lines
40 KiB
Vue
<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> |