feat(profile): 新增我的收藏页面并集成Vant组件
添加我的收藏页面,支持展示收藏的课程和活动。使用Vant的Tab和Tabs组件实现分类切换,并通过VanList实现分页加载功能。
Showing
3 changed files
with
126 additions
and
0 deletions
| ... | @@ -23,5 +23,7 @@ declare module 'vue' { | ... | @@ -23,5 +23,7 @@ declare module 'vue' { |
| 23 | SummerCampCard: typeof import('./components/ui/SummerCampCard.vue')['default'] | 23 | SummerCampCard: typeof import('./components/ui/SummerCampCard.vue')['default'] |
| 24 | VanList: typeof import('vant/es')['List'] | 24 | VanList: typeof import('vant/es')['List'] |
| 25 | VanRate: typeof import('vant/es')['Rate'] | 25 | VanRate: typeof import('vant/es')['Rate'] |
| 26 | + VanTab: typeof import('vant/es')['Tab'] | ||
| 27 | + VanTabs: typeof import('vant/es')['Tabs'] | ||
| 26 | } | 28 | } |
| 27 | } | 29 | } | ... | ... |
| ... | @@ -96,6 +96,12 @@ const routes = [ | ... | @@ -96,6 +96,12 @@ const routes = [ |
| 96 | component: () => import('../views/courses/MyCoursesPage.vue'), | 96 | component: () => import('../views/courses/MyCoursesPage.vue'), |
| 97 | meta: { title: '我的课程' } | 97 | meta: { title: '我的课程' } |
| 98 | }, | 98 | }, |
| 99 | + { | ||
| 100 | + path: '/profile/favorites', | ||
| 101 | + name: 'MyFavorites', | ||
| 102 | + component: () => import('../views/profile/MyFavoritesPage.vue'), | ||
| 103 | + meta: { title: '我的收藏' } | ||
| 104 | + }, | ||
| 99 | ] | 105 | ] |
| 100 | 106 | ||
| 101 | const router = createRouter({ | 107 | const router = createRouter({ | ... | ... |
src/views/profile/MyFavoritesPage.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <div class="bg-gradient-to-b from-green-50/70 to-white/90 min-h-screen pb-20"> | ||
| 3 | + <!-- 分类切换 --> | ||
| 4 | + <div class="px-4 py-3"> | ||
| 5 | + <van-tabs v-model:active="activeTab" sticky swipeable title-active-color="#4caf50" color="#4caf50"> | ||
| 6 | + <van-tab title="课程" name="courses"> | ||
| 7 | + <van-list | ||
| 8 | + v-model:loading="coursesLoading" | ||
| 9 | + :finished="coursesFinished" | ||
| 10 | + finished-text="没有更多了" | ||
| 11 | + @load="onCoursesLoad" | ||
| 12 | + class="space-y-4 mt-4" | ||
| 13 | + > | ||
| 14 | + <CourseCard v-for="course in favoriteCourses" :key="course.id" :course="course" /> | ||
| 15 | + </van-list> | ||
| 16 | + | ||
| 17 | + <!-- 无数据提示 --> | ||
| 18 | + <div v-if="!coursesLoading && favoriteCourses.length === 0" class="flex flex-col items-center justify-center py-12"> | ||
| 19 | + <svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 text-gray-300" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | ||
| 20 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" /> | ||
| 21 | + </svg> | ||
| 22 | + <p class="mt-4 text-gray-500">暂无收藏课程</p> | ||
| 23 | + </div> | ||
| 24 | + </van-tab> | ||
| 25 | + | ||
| 26 | + <van-tab title="活动" name="activities"> | ||
| 27 | + <van-list | ||
| 28 | + v-model:loading="activitiesLoading" | ||
| 29 | + :finished="activitiesFinished" | ||
| 30 | + finished-text="没有更多了" | ||
| 31 | + @load="onActivitiesLoad" | ||
| 32 | + class="space-y-4 mt-4" | ||
| 33 | + > | ||
| 34 | + <ActivityCard v-for="activity in favoriteActivities" :key="activity.id" :activity="activity" /> | ||
| 35 | + </van-list> | ||
| 36 | + | ||
| 37 | + <!-- 无数据提示 --> | ||
| 38 | + <div v-if="!activitiesLoading && favoriteActivities.length === 0" class="flex flex-col items-center justify-center py-12"> | ||
| 39 | + <svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 text-gray-300" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | ||
| 40 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" /> | ||
| 41 | + </svg> | ||
| 42 | + <p class="mt-4 text-gray-500">暂无收藏活动</p> | ||
| 43 | + </div> | ||
| 44 | + </van-tab> | ||
| 45 | + </van-tabs> | ||
| 46 | + </div> | ||
| 47 | + </div> | ||
| 48 | +</template> | ||
| 49 | + | ||
| 50 | +<script setup> | ||
| 51 | +import { ref } from 'vue'; | ||
| 52 | +import { useRoute, useRouter } from 'vue-router'; | ||
| 53 | +import CourseCard from '@/components/ui/CourseCard.vue'; | ||
| 54 | +import ActivityCard from '@/components/ui/ActivityCard.vue'; | ||
| 55 | +import { courses as mockCourses, activities as mockActivities } from '@/utils/mockData'; | ||
| 56 | +import { useTitle } from '@vueuse/core'; | ||
| 57 | + | ||
| 58 | +const $route = useRoute(); | ||
| 59 | +const $router = useRouter(); | ||
| 60 | +useTitle($route.meta.title); | ||
| 61 | + | ||
| 62 | +// 页面状态 | ||
| 63 | +const activeTab = ref('courses'); | ||
| 64 | +const favoriteCourses = ref([]); | ||
| 65 | +const favoriteActivities = ref([]); | ||
| 66 | + | ||
| 67 | +// 课程列表加载状态 | ||
| 68 | +const coursesLoading = ref(false); | ||
| 69 | +const coursesFinished = ref(false); | ||
| 70 | +const coursePage = ref(1); | ||
| 71 | +const coursePageSize = 10; | ||
| 72 | + | ||
| 73 | +// 活动列表加载状态 | ||
| 74 | +const activitiesLoading = ref(false); | ||
| 75 | +const activitiesFinished = ref(false); | ||
| 76 | +const activitiesPage = ref(1); | ||
| 77 | +const activitiesPageSize = 10; | ||
| 78 | + | ||
| 79 | +// 加载收藏课程 | ||
| 80 | +const onCoursesLoad = () => { | ||
| 81 | + coursesLoading.value = true; | ||
| 82 | + // 模拟异步加载 | ||
| 83 | + setTimeout(() => { | ||
| 84 | + const start = (coursePage.value - 1) * coursePageSize; | ||
| 85 | + const end = start + coursePageSize; | ||
| 86 | + const newCourses = mockCourses.slice(start, end); | ||
| 87 | + | ||
| 88 | + favoriteCourses.value.push(...newCourses); | ||
| 89 | + coursesLoading.value = false; | ||
| 90 | + | ||
| 91 | + if (newCourses.length < coursePageSize) { | ||
| 92 | + coursesFinished.value = true; | ||
| 93 | + } else { | ||
| 94 | + coursePage.value += 1; | ||
| 95 | + } | ||
| 96 | + }, 1000); | ||
| 97 | +}; | ||
| 98 | + | ||
| 99 | +// 加载收藏活动 | ||
| 100 | +const onActivitiesLoad = () => { | ||
| 101 | + activitiesLoading.value = true; | ||
| 102 | + // 模拟异步加载 | ||
| 103 | + setTimeout(() => { | ||
| 104 | + const start = (activitiesPage.value - 1) * activitiesPageSize; | ||
| 105 | + const end = start + activitiesPageSize; | ||
| 106 | + const newActivities = mockActivities.slice(start, end); | ||
| 107 | + | ||
| 108 | + favoriteActivities.value.push(...newActivities); | ||
| 109 | + activitiesLoading.value = false; | ||
| 110 | + | ||
| 111 | + if (newActivities.length < activitiesPageSize) { | ||
| 112 | + activitiesFinished.value = true; | ||
| 113 | + } else { | ||
| 114 | + activitiesPage.value += 1; | ||
| 115 | + } | ||
| 116 | + }, 1000); | ||
| 117 | +}; | ||
| 118 | +</script> |
-
Please register or login to post a comment