Files
gym-manage/gym-manage-uniapp/pages/recommendCourses/index.vue
T

252 lines
6.4 KiB
Vue

<!-- pages/recommendCourses/index.vue -->
<template>
<!-- 页面容器 -->
<view class="recommend-page">
<!-- 页面标题 -->
<PageHeader title="推荐课程" subtitle="精选热门团课,等你来练" :show-back="true" />
<!-- 骨架屏 -->
<view v-if="loading" class="skeleton-container">
<view class="skeleton-card" v-for="i in 3" :key="i">
<view class="skeleton-img"></view>
<view class="skeleton-text"></view>
<view class="skeleton-text-short"></view>
</view>
</view>
<!-- 课程列表 -->
<scroll-view v-else class="courses-container" scroll-y="true">
<view class="courses-grid">
<CourseCard
v-for="course in courses"
:key="course.id"
:course="course"
@join="handleJoinCourse"
/>
</view>
<!-- 空状态 -->
<view v-if="!loading && courses.length === 0" class="empty-state">
<text class="empty-text">暂无推荐课程</text>
</view>
<!-- 底部占位 -->
<view class="bottom-placeholder"></view>
</scroll-view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import CourseCard from '@/components/index/CourseCard.vue'
import PageHeader from '@/components/index/PageHeader.vue'
import { groupCourseMockApi } from '@/request_api/groupCourse.mock.js'
// 课程列表数据
const courses = ref([])
// 加载状态
const loading = ref(true)
// 课程类型映射(用于显示标签)
const getCourseTypeName = (type) => {
const typeMap = {
'1': '瑜伽',
'2': '搏击',
'3': '塑形'
}
return typeMap[type] || '课程'
}
// 根据课程信息获取标签文本
const getTag = (course) => {
if (course.currentMembers >= course.maxMembers) return '已满员'
if (course.status === '2') return '已结束'
if (course.status === '1') return '已取消'
if (course.currentMembers / course.maxMembers >= 0.8) return '热门'
return getCourseTypeName(course.courseType)
}
// 根据课程信息获取标签样式类型
const getTagType = (course) => {
if (course.currentMembers >= course.maxMembers) return 'full'
if (course.status === '2') return 'ended'
if (course.status === '1') return 'ended'
if (course.currentMembers / course.maxMembers >= 0.8) return 'hot'
return 'default'
}
// 计算课程时长
const calculateDuration = (startTime, endTime) => {
if (!startTime || !endTime) return '60分钟'
const start = new Date(startTime)
const end = new Date(endTime)
const durationMinutes = Math.floor((end - start) / (1000 * 60))
return `${durationMinutes}分钟`
}
// 获取课程难度
const getCourseLevel = (course) => {
if (course.courseType === '2') return '中级'
if (course.courseType === '3') return '高级'
if (course.courseType === '1') return '初级'
return '初级'
}
// 处理图片URL
const getImageUrl = (coverImage) => {
if (!coverImage) return 'https://images.unsplash.com/photo-1534438327276-14e5300c3a48?w=400&q=80'
if (coverImage.startsWith('http')) return coverImage
return `https://your-domain.com${coverImage}`
}
// 转换课程数据格式
const transformCourseData = (course) => {
return {
id: course.id,
image: getImageUrl(course.coverImage),
tag: getTag(course),
tagType: getTagType(course),
name: course.courseName || '未知课程',
duration: calculateDuration(course.startTime, course.endTime),
level: getCourseLevel(course),
participants: course.currentMembers || 0,
rawData: course
}
}
// 获取推荐课程
const fetchRecommendCourses = async () => {
loading.value = true
try {
// 使用 mock API 获取数据
const res = await groupCourseMockApi.getList({
pageNum: 1,
pageSize: 20
})
if (res && res.data && res.data.list) {
// 按参与人数排序,推荐热门课程
const sortedList = res.data.list.sort((a, b) => b.currentMembers - a.currentMembers)
courses.value = sortedList.map(course => transformCourseData(course))
}
} catch (err) {
console.error('获取推荐课程失败', err)
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
loading.value = false
}
}
// 处理参与课程点击
const handleJoinCourse = (course) => {
if (course.rawData.status === '2') {
uni.showToast({ title: '课程已结束', icon: 'none' })
return
}
if (course.rawData.status === '1') {
uni.showToast({ title: '课程已取消', icon: 'none' })
return
}
if (course.rawData.currentMembers >= course.rawData.maxMembers) {
uni.showToast({ title: '课程已满员', icon: 'none' })
return
}
// 跳转到课程详情页
uni.navigateTo({ url: `/pages/groupCourse/detail?id=${course.id}` })
}
onMounted(() => {
fetchRecommendCourses()
})
</script>
<style lang="scss" scoped>
.recommend-page {
min-height: 100vh;
background: linear-gradient(180deg, #E3F2FD 0%, #F5F5F5 100%);
}
.courses-container {
height: calc(100vh - 160rpx);
padding: 32rpx 0;
}
.courses-grid {
display: flex;
flex-wrap: wrap;
gap: 24rpx;
justify-content: center;
// 两列网格布局,适度缩小卡片宽度
> * {
flex: 0 0 calc(45% - 12rpx);
margin-bottom: 0;
}
}
.empty-state {
display: flex;
justify-content: center;
align-items: center;
padding: 120rpx 0;
}
.empty-text {
font-size: 28rpx;
color: #8CA0B0;
}
.bottom-placeholder {
height: 40rpx;
}
/* 骨架屏样式 */
.skeleton-container {
padding: 32rpx 24rpx;
display: flex;
flex-wrap: wrap;
gap: 20rpx;
}
.skeleton-card {
flex: 0 0 calc(50% - 10rpx);
background: #fff;
border-radius: 24rpx;
overflow: hidden;
box-shadow: 0 8rpx 28rpx rgba(45, 74, 90, 0.08);
}
.skeleton-img {
width: 100%;
height: 280rpx;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: skeleton-loading 1.5s infinite;
}
.skeleton-text {
height: 32rpx;
margin: 20rpx;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: skeleton-loading 1.5s infinite;
border-radius: 8rpx;
}
.skeleton-text-short {
height: 24rpx;
margin: 0 20rpx 20rpx;
width: 60%;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: skeleton-loading 1.5s infinite;
border-radius: 8rpx;
}
@keyframes skeleton-loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
</style>