feat(收藏): 实现课程收藏功能并优化相关页面
- 新增 `favorite.js` API 文件,提供收藏列表、新增和取消收藏的接口 - 在 `CourseDetailPage.vue` 中实现收藏和取消收藏的逻辑,并添加交互反馈 - 在 `MyFavoritesPage.vue` 中替换模拟数据为真实接口调用,优化收藏课程加载逻辑 - 调整 `jsconfig.json` 配置文件,包含新增的 API 文件路径
Showing
4 changed files
with
77 additions
and
39 deletions
| ... | @@ -24,6 +24,6 @@ | ... | @@ -24,6 +24,6 @@ |
| 24 | "esModuleInterop": true, | 24 | "esModuleInterop": true, |
| 25 | "skipLibCheck": true | 25 | "skipLibCheck": true |
| 26 | }, | 26 | }, |
| 27 | - "include": ["src/**/*", "build/**/*", "vite.config.js"], | 27 | + "include": ["src/**/*", "build/**/*", "vite.config.js", "src/api/.js"], |
| 28 | "exclude": ["node_modules", "dist"] | 28 | "exclude": ["node_modules", "dist"] |
| 29 | } | 29 | } | ... | ... |
src/api/favorite.js
0 → 100644
| 1 | +/* | ||
| 2 | + * @Date: 2025-04-16 16:21:37 | ||
| 3 | + * @LastEditors: hookehuyr hookehuyr@gmail.com | ||
| 4 | + * @LastEditTime: 2025-04-17 10:04:30 | ||
| 5 | + * @FilePath: /mlaj/src/api/favorite.js | ||
| 6 | + * @Description: 收藏相关接口 | ||
| 7 | + */ | ||
| 8 | +import { fn, fetch } from './fn' | ||
| 9 | + | ||
| 10 | +const Api = { | ||
| 11 | + GROUP_FAVORITE_LIST: '/srv/?a=group_favorite_list', | ||
| 12 | + FAVORITE_ADD: '/srv/?a=group_favorite', | ||
| 13 | + FAVORITE_CANCEL: '/srv/?a=group_unfavorite', | ||
| 14 | +} | ||
| 15 | + | ||
| 16 | +/** | ||
| 17 | + * @description: 获取课程收藏列表 | ||
| 18 | + * @param: page 页码 | ||
| 19 | + * @param: limit 每页数量 | ||
| 20 | + * @return: data: { id: 收藏ID, title: 课程名称, price: 优惠价格, original_price: 原价, feature: 课程特色, highlights: 课程亮点, learning_goal: 学习目标, count: 课程章节数, cover: 封面图 } | ||
| 21 | + */ | ||
| 22 | +export const getGroupFavoriteListAPI = (params) => fn(fetch.get(Api.GROUP_FAVORITE_LIST, params)) | ||
| 23 | + | ||
| 24 | +/** | ||
| 25 | + * @description: 新增收藏 | ||
| 26 | + * @param: group_id 课程ID | ||
| 27 | + * @return: data: { } | ||
| 28 | + */ | ||
| 29 | +export const addFavoriteAPI = (params) => fn(fetch.post(Api.FAVORITE_ADD, params)) | ||
| 30 | + | ||
| 31 | +/** | ||
| 32 | + * @description: 取消收藏 | ||
| 33 | + * @param: group_id 课程ID | ||
| 34 | + * @return: data: { } | ||
| 35 | + */ | ||
| 36 | +export const cancelFavoriteAPI = (params) => fn(fetch.post(Api.FAVORITE_CANCEL, params)) |
This diff is collapsed. Click to expand it.
| ... | @@ -2,7 +2,7 @@ | ... | @@ -2,7 +2,7 @@ |
| 2 | <div class="bg-gradient-to-b from-green-50/70 to-white/90 min-h-screen pb-20"> | 2 | <div class="bg-gradient-to-b from-green-50/70 to-white/90 min-h-screen pb-20"> |
| 3 | <!-- 分类切换 --> | 3 | <!-- 分类切换 --> |
| 4 | <div class="px-4 py-3"> | 4 | <div class="px-4 py-3"> |
| 5 | - <van-tabs v-model:active="activeTab" sticky swipeable title-active-color="#4caf50" color="#4caf50"> | 5 | + <van-tabs v-model:active="activeTab" @click-tab="onClickTab" sticky swipeable title-active-color="#4caf50" color="#4caf50"> |
| 6 | <van-tab title="课程" name="courses"> | 6 | <van-tab title="课程" name="courses"> |
| 7 | <van-list | 7 | <van-list |
| 8 | v-model:loading="coursesLoading" | 8 | v-model:loading="coursesLoading" |
| ... | @@ -24,7 +24,7 @@ | ... | @@ -24,7 +24,7 @@ |
| 24 | </van-tab> | 24 | </van-tab> |
| 25 | 25 | ||
| 26 | <van-tab title="活动" name="activities"> | 26 | <van-tab title="活动" name="activities"> |
| 27 | - <van-list | 27 | + <!--<van-list |
| 28 | v-model:loading="activitiesLoading" | 28 | v-model:loading="activitiesLoading" |
| 29 | :finished="activitiesFinished" | 29 | :finished="activitiesFinished" |
| 30 | finished-text="没有更多了" | 30 | finished-text="没有更多了" |
| ... | @@ -34,13 +34,13 @@ | ... | @@ -34,13 +34,13 @@ |
| 34 | <ActivityCard v-for="activity in favoriteActivities" :key="activity.id" :activity="activity" /> | 34 | <ActivityCard v-for="activity in favoriteActivities" :key="activity.id" :activity="activity" /> |
| 35 | </van-list> | 35 | </van-list> |
| 36 | 36 | ||
| 37 | - <!-- 无数据提示 --> | 37 | + <!~~ 无数据提示 ~~> |
| 38 | <div v-if="!activitiesLoading && favoriteActivities.length === 0" class="flex flex-col items-center justify-center py-12"> | 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"> | 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" /> | 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> | 41 | </svg> |
| 42 | <p class="mt-4 text-gray-500">暂无收藏活动</p> | 42 | <p class="mt-4 text-gray-500">暂无收藏活动</p> |
| 43 | - </div> | 43 | + </div>--> |
| 44 | </van-tab> | 44 | </van-tab> |
| 45 | </van-tabs> | 45 | </van-tabs> |
| 46 | </div> | 46 | </div> |
| ... | @@ -55,6 +55,9 @@ import ActivityCard from '@/components/ui/ActivityCard.vue'; | ... | @@ -55,6 +55,9 @@ import ActivityCard from '@/components/ui/ActivityCard.vue'; |
| 55 | import { courses as mockCourses, activities as mockActivities } from '@/utils/mockData'; | 55 | import { courses as mockCourses, activities as mockActivities } from '@/utils/mockData'; |
| 56 | import { useTitle } from '@vueuse/core'; | 56 | import { useTitle } from '@vueuse/core'; |
| 57 | 57 | ||
| 58 | +// 导入接口 | ||
| 59 | +import { getGroupFavoriteListAPI } from '@/api/favorite'; | ||
| 60 | + | ||
| 58 | const $route = useRoute(); | 61 | const $route = useRoute(); |
| 59 | const $router = useRouter(); | 62 | const $router = useRouter(); |
| 60 | useTitle($route.meta.title); | 63 | useTitle($route.meta.title); |
| ... | @@ -67,8 +70,8 @@ const favoriteActivities = ref([]); | ... | @@ -67,8 +70,8 @@ const favoriteActivities = ref([]); |
| 67 | // 课程列表加载状态 | 70 | // 课程列表加载状态 |
| 68 | const coursesLoading = ref(false); | 71 | const coursesLoading = ref(false); |
| 69 | const coursesFinished = ref(false); | 72 | const coursesFinished = ref(false); |
| 70 | -const coursePage = ref(1); | 73 | +const coursePage = ref(0); |
| 71 | -const coursePageSize = 10; | 74 | +const courseLimit = ref(5); |
| 72 | 75 | ||
| 73 | // 活动列表加载状态 | 76 | // 活动列表加载状态 |
| 74 | const activitiesLoading = ref(false); | 77 | const activitiesLoading = ref(false); |
| ... | @@ -77,42 +80,41 @@ const activitiesPage = ref(1); | ... | @@ -77,42 +80,41 @@ const activitiesPage = ref(1); |
| 77 | const activitiesPageSize = 10; | 80 | const activitiesPageSize = 10; |
| 78 | 81 | ||
| 79 | // 加载收藏课程 | 82 | // 加载收藏课程 |
| 80 | -const onCoursesLoad = () => { | 83 | +const onCoursesLoad = async () => { |
| 81 | - coursesLoading.value = true; | 84 | + const nextPage = coursePage.value; |
| 82 | - // 模拟异步加载 | 85 | + const res = await getGroupFavoriteListAPI({ limit: courseLimit.value, page: nextPage }); |
| 83 | - setTimeout(() => { | 86 | + if (res.code) { |
| 84 | - const start = (coursePage.value - 1) * coursePageSize; | 87 | + favoriteCourses.value = [...favoriteCourses.value, ...res.data]; |
| 85 | - const end = start + coursePageSize; | 88 | + coursesFinished.value = res.data.length < courseLimit.value; |
| 86 | - const newCourses = mockCourses.slice(start, end); | 89 | + coursePage.value = nextPage + 1; |
| 87 | - | 90 | + } |
| 88 | - favoriteCourses.value.push(...newCourses); | 91 | + coursesLoading.value = false; |
| 89 | - coursesLoading.value = false; | ||
| 90 | - | ||
| 91 | - if (newCourses.length < coursePageSize) { | ||
| 92 | - coursesFinished.value = true; | ||
| 93 | - } else { | ||
| 94 | - coursePage.value += 1; | ||
| 95 | - } | ||
| 96 | - }, 1000); | ||
| 97 | }; | 92 | }; |
| 98 | 93 | ||
| 99 | // 加载收藏活动 | 94 | // 加载收藏活动 |
| 100 | -const onActivitiesLoad = () => { | 95 | +// const onActivitiesLoad = () => { |
| 101 | - activitiesLoading.value = true; | 96 | +// activitiesLoading.value = true; |
| 102 | - // 模拟异步加载 | 97 | +// // 模拟异步加载 |
| 103 | - setTimeout(() => { | 98 | +// setTimeout(() => { |
| 104 | - const start = (activitiesPage.value - 1) * activitiesPageSize; | 99 | +// const start = (activitiesPage.value - 1) * activitiesPageSize; |
| 105 | - const end = start + activitiesPageSize; | 100 | +// const end = start + activitiesPageSize; |
| 106 | - const newActivities = mockActivities.slice(start, end); | 101 | +// const newActivities = mockActivities.slice(start, end); |
| 102 | + | ||
| 103 | +// favoriteActivities.value.push(...newActivities); | ||
| 104 | +// activitiesLoading.value = false; | ||
| 107 | 105 | ||
| 108 | - favoriteActivities.value.push(...newActivities); | 106 | +// if (newActivities.length < activitiesPageSize) { |
| 109 | - activitiesLoading.value = false; | 107 | +// activitiesFinished.value = true; |
| 108 | +// } else { | ||
| 109 | +// activitiesPage.value += 1; | ||
| 110 | +// } | ||
| 111 | +// }, 1000); | ||
| 112 | +// }; | ||
| 110 | 113 | ||
| 111 | - if (newActivities.length < activitiesPageSize) { | 114 | +// 切换标签页 |
| 112 | - activitiesFinished.value = true; | 115 | +const onClickTab = ({ name, title }) => { |
| 113 | - } else { | 116 | + if (name === 'activities') { |
| 114 | - activitiesPage.value += 1; | 117 | + location.href = 'http://www.baidu.com' |
| 115 | - } | 118 | + } |
| 116 | - }, 1000); | ||
| 117 | }; | 119 | }; |
| 118 | </script> | 120 | </script> | ... | ... |
-
Please register or login to post a comment