hookehuyr

feat(profile): 新增我的收藏页面并集成Vant组件

添加我的收藏页面,支持展示收藏的课程和活动。使用Vant的Tab和Tabs组件实现分类切换,并通过VanList实现分页加载功能。
...@@ -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({
......
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>