hookehuyr

feat(courses): 实现课程模块接口集成与功能优化

- 集成课程列表、详情、热门课程等接口
- 优化课程卡片、课程详情页的数据展示
- 添加课程跳转功能
- 移除模拟数据,使用真实接口数据
1 +/*
2 + * @Date: 2025-04-15 09:32:07
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-04-15 12:58:17
5 + * @FilePath: /mlaj/src/api/course.js
6 + * @Description: 课程模块相关接口
7 + */
8 +import { fn, fetch } from './fn'
9 +
10 +const Api = {
11 + GET_COURSE_LIST: '/srv/?a=schedule&t=list',
12 + GET_COURSE_DETAIL: '/srv/?a=schedule&t=detail',
13 + GET_SCHEDULE_COURSE_LIST: '/srv/?a=schedule&t=course',
14 +}
15 +
16 +/**
17 + * @description: 获取课程列表
18 + * @param: page 页码
19 + * @param: limit 每页数量 默认10
20 + * @param: sn 类型 RMKC=热门课程 JXKC=精选课程
21 + * @return: data: [{ id, title, price, original_price, feature, highlights, count, cover}]
22 + */
23 +export const getCourseListAPI = (params) => fn(fetch.get(Api.GET_COURSE_LIST, params))
24 +
25 +/**
26 + * @description: 获取课程详情
27 + * @param: i 课程 ID
28 + * @return: data: [{ id, title, price, original_price, feature, highlights, learning_goal, schedule}]
29 + * @return: schedule: [{ id, schedule_time, seq, title, duration}] 课程章节
30 + */
31 +export const getCourseDetailAPI = (params) => fn(fetch.get(Api.GET_COURSE_DETAIL, params))
32 +
33 +/**
34 + * @description: 获取特定学习课程的目录
35 + * @param: i 课程 ID
36 + * @return: data: [{ id, schedule_time, seq, title, duration, course_id, file}]
37 + */
38 +export const getScheduleCourseListAPI = (params) => fn(fetch.get(Api.GET_SCHEDULE_COURSE_LIST, params))
1 +<!--
2 + * @Date: 2025-03-20 20:36:36
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-04-15 12:52:27
5 + * @FilePath: /mlaj/src/components/ui/CourseCard.vue
6 + * @Description: 文件描述
7 +-->
1 <template> 8 <template>
2 <router-link :to="`/courses/${course.id}`" class="flex bg-white rounded-lg overflow-hidden shadow-sm"> 9 <router-link :to="`/courses/${course.id}`" class="flex bg-white rounded-lg overflow-hidden shadow-sm">
3 <div class="w-1/3 h-28"> 10 <div class="w-1/3 h-28">
4 <img 11 <img
5 - :src="course.imageUrl" 12 + :src="course.cover || 'https://cdn.ipadbiz.cn/mlaj/images/default_block.png'"
6 :alt="course.title" 13 :alt="course.title"
7 class="w-full h-full object-cover" 14 class="w-full h-full object-cover"
8 /> 15 />
...@@ -10,16 +17,16 @@ ...@@ -10,16 +17,16 @@
10 <div class="flex-1 p-3 flex flex-col justify-between"> 17 <div class="flex-1 p-3 flex flex-col justify-between">
11 <div> 18 <div>
12 <h3 class="font-medium text-sm mb-1 line-clamp-2">{{ course.title }}</h3> 19 <h3 class="font-medium text-sm mb-1 line-clamp-2">{{ course.title }}</h3>
13 - <div class="text-gray-500 text-xs">{{ course.subtitle }}</div> 20 + <div class="text-gray-500 text-xs">{{ course.subtitle || '空数据' }}</div>
14 </div> 21 </div>
15 <div class="flex justify-between items-end mt-1"> 22 <div class="flex justify-between items-end mt-1">
16 - <div class="text-orange-500 font-semibold">¥{{ course.price }}</div> 23 + <div class="text-orange-500 font-semibold">¥{{ course.price || '空数据' }}</div>
17 <div class="text-gray-400 text-xs"> 24 <div class="text-gray-400 text-xs">
18 - {{ course.subscribers }}人订阅 25 + {{ course.subscribers || '没字段' }}人订阅
19 </div> 26 </div>
20 </div> 27 </div>
21 <div class="text-gray-400 text-xs"> 28 <div class="text-gray-400 text-xs">
22 - 已更新{{ course.updatedLessons }}期 | {{ course.subscribers }}人订阅 29 + 已更新{{ course.count }}期 | {{ course.subscribers || '没字段' }}人订阅
23 </div> 30 </div>
24 </div> 31 </div>
25 </router-link> 32 </router-link>
......
1 <!-- 1 <!--
2 * @Date: 2025-03-20 19:55:21 2 * @Date: 2025-03-20 19:55:21
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-04-07 18:24:11 4 + * @LastEditTime: 2025-04-15 13:52:20
5 * @FilePath: /mlaj/src/views/HomePage.vue 5 * @FilePath: /mlaj/src/views/HomePage.vue
6 * @Description: 亲子学院首页组件 6 * @Description: 亲子学院首页组件
7 * 7 *
...@@ -305,13 +305,12 @@ ...@@ -305,13 +305,12 @@
305 :key="index" 305 :key="index"
306 class="p-3 rounded-xl" 306 class="p-3 rounded-xl"
307 > 307 >
308 - <div class="flex flex-col h-full"> 308 + <div class="flex flex-col h-full" @click="goToCourseDetail(item)">
309 <div class="h-28 mb-2 rounded-lg overflow-hidden"> 309 <div class="h-28 mb-2 rounded-lg overflow-hidden">
310 <img 310 <img
311 - :src="item.image" 311 + :src="item.cover || 'https://cdn.ipadbiz.cn/mlaj/images/default_block.png'"
312 :alt="item.title" 312 :alt="item.title"
313 class="w-full h-full object-cover" 313 class="w-full h-full object-cover"
314 - @error="handleImageError"
315 /> 314 />
316 </div> 315 </div>
317 <h4 class="font-medium text-sm mb-1 line-clamp-1">{{ item.title }}</h4> 316 <h4 class="font-medium text-sm mb-1 line-clamp-1">{{ item.title }}</h4>
...@@ -319,7 +318,9 @@ ...@@ -319,7 +318,9 @@
319 <svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor"> 318 <svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
320 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" /> 319 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
321 </svg> 320 </svg>
322 - {{ item.duration }} 321 + <!-- TODO: 后台没字段 -->
322 + <!-- {{ item.duration }} -->
323 + 后台没字段
323 </p> 324 </p>
324 </div> 325 </div>
325 </FrostedGlass> 326 </FrostedGlass>
...@@ -357,7 +358,7 @@ ...@@ -357,7 +358,7 @@
357 </div> 358 </div>
358 <div class="space-y-4"> 359 <div class="space-y-4">
359 <CourseCard 360 <CourseCard
360 - v-for="course in courses.slice(0, 3)" 361 + v-for="course in hotCourses"
361 :key="course.id" 362 :key="course.id"
362 :course="course" 363 :course="course"
363 /> 364 />
...@@ -529,11 +530,14 @@ import SummerCampCard from '@/components/ui/SummerCampCard.vue' ...@@ -529,11 +530,14 @@ import SummerCampCard from '@/components/ui/SummerCampCard.vue'
529 import VideoPlayer from '@/components/ui/VideoPlayer.vue' 530 import VideoPlayer from '@/components/ui/VideoPlayer.vue'
530 531
531 // 导入模拟数据和工具函数 532 // 导入模拟数据和工具函数
532 -import { courses, liveStreams, activities, checkInTypes, userRecommendations } from '@/utils/mockData' 533 +import { courses, liveStreams, activities, checkInTypes } from '@/utils/mockData'
533 import { useTitle } from '@vueuse/core' 534 import { useTitle } from '@vueuse/core'
534 import { useAuth } from '@/contexts/auth' 535 import { useAuth } from '@/contexts/auth'
535 import { showToast } from 'vant' 536 import { showToast } from 'vant'
536 537
538 +// 导入接口
539 +import { getCourseListAPI } from "@/api/course";
540 +
537 // 视频播放状态管理 541 // 视频播放状态管理
538 const activeVideoIndex = ref(null); // 当前播放的视频索引 542 const activeVideoIndex = ref(null); // 当前播放的视频索引
539 const videoPlayerRefs = ref([]); // 视频播放器组件引用数组 543 const videoPlayerRefs = ref([]); // 视频播放器组件引用数组
...@@ -546,6 +550,7 @@ const playVideo = (index, videoUrl) => { ...@@ -546,6 +550,7 @@ const playVideo = (index, videoUrl) => {
546 550
547 // 路由相关 551 // 路由相关
548 const $route = useRoute() 552 const $route = useRoute()
553 +const $router = useRouter()
549 useTitle($route.meta.title) // 设置页面标题 554 useTitle($route.meta.title) // 设置页面标题
550 555
551 // 获取用户认证状态 556 // 获取用户认证状态
...@@ -560,19 +565,52 @@ const isCheckingIn = ref(false) // 打卡提交状态 ...@@ -560,19 +565,52 @@ const isCheckingIn = ref(false) // 打卡提交状态
560 const checkInSuccess = ref(false) // 打卡成功状态 565 const checkInSuccess = ref(false) // 打卡成功状态
561 const displayedRecommendations = ref([]) // 当前显示的推荐内容 566 const displayedRecommendations = ref([]) // 当前显示的推荐内容
562 567
568 +//
569 +const userRecommendations = ref([])
570 +const hotCourses = ref([])
563 // 获取推荐内容 571 // 获取推荐内容
564 const getRecommendations = (random = false) => { 572 const getRecommendations = (random = false) => {
565 if (random) { 573 if (random) {
566 - const shuffled = [...userRecommendations].sort(() => 0.5 - Math.random()) 574 + const shuffled = [...userRecommendations.value].sort(() => 0.5 - Math.random())
567 return shuffled.slice(0, 4) 575 return shuffled.slice(0, 4)
568 } 576 }
569 - return userRecommendations.slice(0, 4) 577 + return userRecommendations.value.slice(0, 4)
570 } 578 }
571 579
572 -// 初始化显示推荐内容 580 +
573 -onMounted(() => { 581 +// 自动轮播
582 +let carouselInterval
583 +onMounted(async () => {
584 + carouselInterval = setInterval(() => {
585 + if (carouselRef.value) {
586 + const nextSlide = (currentSlide.value + 1) % courses.slice(0, 4).length
587 + scrollToSlide(nextSlide)
588 + }
589 + }, 5000)
590 + // TODO: 模拟获取用户推荐内容
591 + // 获取课程列表
592 + const res = await getCourseListAPI({ sn: 'RMKC' })
593 + if (res.code) {
594 + userRecommendations.value = res.data
595 + // 初始化显示推荐内容
574 displayedRecommendations.value = getRecommendations() 596 displayedRecommendations.value = getRecommendations()
597 + }
598 + // 获取热门课程
599 + const res2 = await getCourseListAPI({
600 + sn: 'RMKC',
601 + limit: 4
602 + })
603 + if (res2.code) {
604 + hotCourses.value = res2.data
605 + }
606 +})
607 +
608 +onUnmounted(() => {
609 + if (carouselInterval) {
610 + clearInterval(carouselInterval)
611 + }
575 }) 612 })
613 +
576 const carouselRef = ref(null) // 轮播图容器引用 614 const carouselRef = ref(null) // 轮播图容器引用
577 615
578 // 右侧导航组件:搜索和消息通知 616 // 右侧导航组件:搜索和消息通知
...@@ -658,22 +696,7 @@ const handleCheckInSubmit = () => { ...@@ -658,22 +696,7 @@ const handleCheckInSubmit = () => {
658 }, 1500) 696 }, 1500)
659 } 697 }
660 698
661 -// 自动轮播
662 -let carouselInterval
663 -onMounted(() => {
664 - carouselInterval = setInterval(() => {
665 - if (carouselRef.value) {
666 - const nextSlide = (currentSlide.value + 1) % courses.slice(0, 4).length
667 - scrollToSlide(nextSlide)
668 - }
669 - }, 5000)
670 -})
671 699
672 -onUnmounted(() => {
673 - if (carouselInterval) {
674 - clearInterval(carouselInterval)
675 - }
676 -})
677 700
678 const contentRef = ref(null) // 内容区域的ref引用 701 const contentRef = ref(null) // 内容区域的ref引用
679 702
...@@ -690,4 +713,9 @@ watch(activeTab, () => { ...@@ -690,4 +713,9 @@ watch(activeTab, () => {
690 } 713 }
691 }) 714 })
692 }) 715 })
716 +
717 +// 跳转到购买课程详情页
718 +const goToCourseDetail = ({id}) => {
719 + $router.push(`/courses/${id}`)
720 +}
693 </script> 721 </script>
......
...@@ -5,22 +5,22 @@ ...@@ -5,22 +5,22 @@
5 <div class="px-4"> 5 <div class="px-4">
6 <div class="bg-gradient-to-b from-red-500 to-red-600 p-4 mb-4 rounded-b-3xl shadow-lg"> 6 <div class="bg-gradient-to-b from-red-500 to-red-600 p-4 mb-4 rounded-b-3xl shadow-lg">
7 <div class="bg-white/10 backdrop-blur-sm rounded-lg p-3 mb-3 inline-block"> 7 <div class="bg-white/10 backdrop-blur-sm rounded-lg p-3 mb-3 inline-block">
8 - <div class="text-white font-semibold">{{ course?.subtitle?.split(' ')[0] }}</div> 8 + <div class="text-white font-semibold">{{ course?.subtitle?.split(' ')[0] || '没有字段' }}</div>
9 </div> 9 </div>
10 <h1 class="text-2xl text-white font-bold mb-1">{{ course?.title }}</h1> 10 <h1 class="text-2xl text-white font-bold mb-1">{{ course?.title }}</h1>
11 - <h2 class="text-lg text-white/90">{{ course?.subtitle }}</h2> 11 + <h2 class="text-lg text-white/90">{{ course?.subtitle || '没有字段' }}</h2>
12 <div class="mt-4 flex justify-between items-center"> 12 <div class="mt-4 flex justify-between items-center">
13 - <div class="text-orange-300 font-bold text-2xl">¥{{ course?.price }}</div> 13 + <div class="text-orange-300 font-bold text-2xl">¥{{ course?.price || '空数据' }}</div>
14 <div class="bg-orange-500/30 text-orange-100 text-xs px-3 py-1 rounded-full"> 14 <div class="bg-orange-500/30 text-orange-100 text-xs px-3 py-1 rounded-full">
15 限时优惠 15 限时优惠
16 </div> 16 </div>
17 </div> 17 </div>
18 <div class="flex justify-between text-xs text-white/80 mt-3"> 18 <div class="flex justify-between text-xs text-white/80 mt-3">
19 - <div>已更新{{ course?.updatedLessons }}期</div> 19 + <div>已更新{{ course?.count || '空数据' }}期</div>
20 - <div>{{ course?.subscribers }}人订阅</div> 20 + <div>{{ course?.subscribers || '没有字段' }}人订阅</div>
21 </div> 21 </div>
22 <div v-if="course?.expireDate" class="text-xs text-white/80 mt-1"> 22 <div v-if="course?.expireDate" class="text-xs text-white/80 mt-1">
23 - 有效期: {{ course?.expireDate }} 23 + 有效期: {{ course?.expireDate || '没有字段' }}
24 </div> 24 </div>
25 </div> 25 </div>
26 </div> 26 </div>
...@@ -31,14 +31,14 @@ ...@@ -31,14 +31,14 @@
31 <FrostedGlass class="mb-4 p-4 rounded-xl"> 31 <FrostedGlass class="mb-4 p-4 rounded-xl">
32 <h3 class="text-lg font-bold text-gray-800 mb-3">本课程介绍</h3> 32 <h3 class="text-lg font-bold text-gray-800 mb-3">本课程介绍</h3>
33 <p class="text-gray-700 whitespace-pre-line"> 33 <p class="text-gray-700 whitespace-pre-line">
34 - {{ course?.description }} 34 + {{ course?.description || '没有字段' }}
35 </p> 35 </p>
36 </FrostedGlass> 36 </FrostedGlass>
37 37
38 <!-- Course Image --> 38 <!-- Course Image -->
39 <div class="mb-6"> 39 <div class="mb-6">
40 <img 40 <img
41 - :src="course?.imageUrl" 41 + :src="course?.cover || 'https://cdn.ipadbiz.cn/mlaj/images/default_block.png'"
42 :alt="course?.title" 42 :alt="course?.title"
43 class="w-full h-auto rounded-xl shadow-md" 43 class="w-full h-auto rounded-xl shadow-md"
44 /> 44 />
...@@ -67,46 +67,33 @@ ...@@ -67,46 +67,33 @@
67 <!-- Tab Content --> 67 <!-- Tab Content -->
68 <div class="p-4"> 68 <div class="p-4">
69 <div v-if="activeTab === '课程特色'"> 69 <div v-if="activeTab === '课程特色'">
70 - <ul class="list-disc pl-5 space-y-2 text-gray-700"> 70 + <!-- <ul class="list-disc pl-5 space-y-2 text-gray-700">
71 <li>小班授课,更多关注</li> 71 <li>小班授课,更多关注</li>
72 <li>名师授课,经验丰富</li> 72 <li>名师授课,经验丰富</li>
73 <li>随堂练习,巩固知识</li> 73 <li>随堂练习,巩固知识</li>
74 <li>及时反馈,调整教学</li> 74 <li>及时反馈,调整教学</li>
75 - </ul> 75 + </ul> -->
76 + <div v-html="course?.feature || '空数据'"></div>
76 </div> 77 </div>
77 78
78 <div v-if="activeTab === '课程大纲'"> 79 <div v-if="activeTab === '课程大纲'">
79 <div class="space-y-4"> 80 <div class="space-y-4">
80 - <div class="border-l-2 border-green-500 pl-3"> 81 + <div v-for="(item, index) in course?.schedule" :key="index" class="border-l-2 border-green-500 pl-3">
81 - <h4 class="font-medium text-gray-800">第一章:心态准备</h4> 82 + <h4 class="font-medium text-gray-800">{{ item.title }}</h4>
82 - <p class="text-sm text-gray-600 mt-1">45分钟 · 3个小节</p> 83 + <p class="text-sm text-gray-600 mt-1">{{ item.duration }}分钟 · {{ item.schedule_time || '空数据' }}个小节</p>
83 - </div>
84 - <div class="border-l-2 border-gray-300 pl-3">
85 - <h4 class="font-medium text-gray-800">第二章:考前减压</h4>
86 - <p class="text-sm text-gray-600 mt-1">60分钟 · 4个小节</p>
87 - </div>
88 - <div class="border-l-2 border-gray-300 pl-3">
89 - <h4 class="font-medium text-gray-800">第三章:家庭支持</h4>
90 - <p class="text-sm text-gray-600 mt-1">50分钟 · 3个小节</p>
91 </div> 84 </div>
92 </div> 85 </div>
93 </div> 86 </div>
94 87
95 <div v-if="activeTab === '课程亮点'"> 88 <div v-if="activeTab === '课程亮点'">
96 <div class="space-y-3 text-gray-700"> 89 <div class="space-y-3 text-gray-700">
97 - <p>✓ 专业的心理辅导</p> 90 + <div v-html="course?.highlights || '空数据'"></div>
98 - <p>✓ 系统化的学习方法</p>
99 - <p>✓ 实用的家庭沟通技巧</p>
100 - <p>✓ 定制的减压活动推荐</p>
101 </div> 91 </div>
102 </div> 92 </div>
103 93
104 <div v-if="activeTab === '学习目标'"> 94 <div v-if="activeTab === '学习目标'">
105 <div class="space-y-3 text-gray-700"> 95 <div class="space-y-3 text-gray-700">
106 - <p>1. 帮助家长理解考前压力来源</p> 96 + <div v-html="course?.learning_goal || '空数据'"></div>
107 - <p>2. 掌握有效的情绪管理技巧</p>
108 - <p>3. 建立积极的家庭支持系统</p>
109 - <p>4. 培养考前良好的心态和信心</p>
110 </div> 97 </div>
111 </div> 98 </div>
112 </div> 99 </div>
...@@ -118,16 +105,16 @@ ...@@ -118,16 +105,16 @@
118 <div class="flex items-center"> 105 <div class="flex items-center">
119 <div class="w-16 h-16 rounded-full overflow-hidden mr-4"> 106 <div class="w-16 h-16 rounded-full overflow-hidden mr-4">
120 <img 107 <img
121 - src="https://cdn.ipadbiz.cn/mlaj/images/teacher-avatar.jpg" 108 + :src="teacher?.avatar || 'https://cdn.ipadbiz.cn/mlaj/images/default_block.png'"
122 alt="Teacher" 109 alt="Teacher"
123 class="w-full h-full object-cover" 110 class="w-full h-full object-cover"
124 @error="handleImageError" 111 @error="handleImageError"
125 /> 112 />
126 </div> 113 </div>
127 <div> 114 <div>
128 - <h4 class="font-bold text-gray-900">张明睿</h4> 115 + <h4 class="font-bold text-gray-900">{{ teacher?.name || '没字段' }}</h4>
129 - <p class="text-sm text-gray-600">教育心理学博士</p> 116 + <p class="text-sm text-gray-600">{{ teacher?.position || '没字段' }}</p>
130 - <p class="text-xs text-gray-500 mt-1">10年家庭教育培训经验</p> 117 + <p class="text-xs text-gray-500 mt-1">{{ teacher?.description || '没字段' }}</p>
131 </div> 118 </div>
132 </div> 119 </div>
133 </FrostedGlass> 120 </FrostedGlass>
...@@ -241,9 +228,9 @@ ...@@ -241,9 +228,9 @@
241 </div> 228 </div>
242 <div class="flex items-center"> 229 <div class="flex items-center">
243 <div class="mr-2"> 230 <div class="mr-2">
244 - <div class="text-red-500 font-bold">¥{{ course?.price }}</div> 231 + <div class="text-red-500 font-bold">¥{{ course?.price || 0 }}</div>
245 <div class="text-xs text-gray-400 line-through"> 232 <div class="text-xs text-gray-400 line-through">
246 - ¥{{ Math.round(course?.price * 1.2) }} 233 + ¥{{ Math.round((course?.price || 0) * 1.2) }}
247 </div> 234 </div>
248 </div> 235 </div>
249 <van-button 236 <van-button
...@@ -297,17 +284,29 @@ import ReviewPopup from '@/components/ui/ReviewPopup.vue' ...@@ -297,17 +284,29 @@ import ReviewPopup from '@/components/ui/ReviewPopup.vue'
297 import { courses } from '@/utils/mockData' 284 import { courses } from '@/utils/mockData'
298 import { useCart } from '@/contexts/cart' 285 import { useCart } from '@/contexts/cart'
299 import { useTitle } from '@vueuse/core'; 286 import { useTitle } from '@vueuse/core';
287 +import { showToast } from 'vant';
288 +
289 +// 导入接口
290 +import { getCourseDetailAPI } from "@/api/course";
291 +
300 const $route = useRoute(); 292 const $route = useRoute();
301 const $router = useRouter(); 293 const $router = useRouter();
302 useTitle($route.meta.title); 294 useTitle($route.meta.title);
295 +
303 const route = useRoute() 296 const route = useRoute()
304 const router = useRouter() 297 const router = useRouter()
298 +
305 const course = ref(null) 299 const course = ref(null)
300 +const teacher = ref(null)
306 const activeTab = ref('课程特色') 301 const activeTab = ref('课程特色')
302 +// 是否收藏状态
307 const isFavorite = ref(false) 303 const isFavorite = ref(false)
304 +// 是否已购买状态
308 const isPurchased = ref(false) 305 const isPurchased = ref(false)
306 +// 是否已评论状态
309 const isReviewed = ref(false) 307 const isReviewed = ref(false)
310 const showReviewPopup = ref(false) 308 const showReviewPopup = ref(false)
309 +
311 const { addToCart, proceedToCheckout } = useCart() 310 const { addToCart, proceedToCheckout } = useCart()
312 311
313 // Handle favorite toggle 312 // Handle favorite toggle
...@@ -377,17 +376,28 @@ const handleReviewSubmit = (review) => { ...@@ -377,17 +376,28 @@ const handleReviewSubmit = (review) => {
377 } 376 }
378 377
379 // Fetch course data 378 // Fetch course data
380 -onMounted(() => { 379 +onMounted(async () => {
381 const id = route.params.id 380 const id = route.params.id
382 - const foundCourse = courses.find(c => c.id === id) 381 + // 调用接口获取课程详情
382 + const res = await getCourseDetailAPI({i: id});
383 + if (res.code) {
384 + const foundCourse = res.data;
383 if (foundCourse) { 385 if (foundCourse) {
384 - course.value = foundCourse 386 + course.value = foundCourse;
385 - isPurchased.value = foundCourse.isPurchased 387 + teacher.value = {
386 - isReviewed.value = foundCourse.isReviewed 388 + name: '',
389 + avatar: '',
390 + position: '',
391 + description: '',
392 + }
393 + isPurchased.value = foundCourse.isPurchased;
394 + isReviewed.value = foundCourse.isReviewed;
387 } else { 395 } else {
388 // Course not found, redirect to courses page 396 // Course not found, redirect to courses page
397 + showToast('课程不存在')
389 router.push('/courses') 398 router.push('/courses')
390 } 399 }
400 + }
391 }) 401 })
392 </script> 402 </script>
393 403
......
1 <!-- 1 <!--
2 * @Date: 2025-03-21 14:31:21 2 * @Date: 2025-03-21 14:31:21
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-03-21 14:54:47 4 + * @LastEditTime: 2025-04-15 14:24:16
5 * @FilePath: /mlaj/src/views/courses/CourseListPage.vue 5 * @FilePath: /mlaj/src/views/courses/CourseListPage.vue
6 * @Description: 文件描述 6 * @Description: 文件描述
7 --> 7 -->
...@@ -10,29 +10,20 @@ ...@@ -10,29 +10,20 @@
10 <div class="pb-16"> 10 <div class="pb-16">
11 <!-- Search Bar --> 11 <!-- Search Bar -->
12 <div class="pb-2"> 12 <div class="pb-2">
13 - <SearchBar placeholder="搜索" v-model="keyword" @search="handleSearch" @blur="handleBlur" /> 13 + <SearchBar placeholder="搜索" v-model="keyword" @blur="handleBlur" />
14 </div> 14 </div>
15 15
16 <!-- Course List --> 16 <!-- Course List -->
17 <div class="px-4"> 17 <div class="px-4">
18 - <div class="space-y-4"> 18 + <van-list
19 - <CourseCard v-for="course in courses" :key="course.id" :course="course" /> 19 + v-model:loading="loading"
20 - </div> 20 + :finished="finished"
21 - 21 + finished-text="没有更多课程了"
22 - <!-- Load More --> 22 + @load="onLoad"
23 - <div 23 + class="space-y-4"
24 - v-if="hasMore"
25 - class="py-4 text-center text-gray-500 text-sm"
26 - @click="loadMore"
27 > 24 >
28 - 加载更多 25 + <CourseCard v-for="course in courses" :key="course.id" :course="course" />
29 - </div> 26 + </van-list>
30 - <div
31 - v-else
32 - class="py-4 text-center text-gray-400 text-sm"
33 - >
34 - 没有更多课程了
35 - </div>
36 </div> 27 </div>
37 </div> 28 </div>
38 </AppLayout> 29 </AppLayout>
...@@ -45,41 +36,42 @@ import AppLayout from '@/components/layout/AppLayout.vue'; ...@@ -45,41 +36,42 @@ import AppLayout from '@/components/layout/AppLayout.vue';
45 import SearchBar from '@/components/ui/SearchBar.vue'; 36 import SearchBar from '@/components/ui/SearchBar.vue';
46 import CourseCard from '@/components/ui/CourseCard.vue'; 37 import CourseCard from '@/components/ui/CourseCard.vue';
47 import { courses as mockCourses } from '@/utils/mockData'; 38 import { courses as mockCourses } from '@/utils/mockData';
39 +// 导入接口
40 +import { getCourseListAPI } from "@/api/course";
41 +import { List } from 'vant';
48 42
49 const $route = useRoute(); 43 const $route = useRoute();
50 const courses = ref([]); 44 const courses = ref([]);
51 -const hasMore = ref(true); 45 +const loading = ref(false);
52 -const page = ref(1); 46 +const finished = ref(false);
47 +const limit = ref(5);
48 +const page = ref(0);
53 const keyword = ref(''); 49 const keyword = ref('');
54 50
55 // 搜索课程列表 51 // 搜索课程列表
56 -const searchCourses = () => { 52 +const searchCourses = async () => {
57 - // 实际项目中这里会调用搜索API 53 + const res = await getCourseListAPI({ limit: limit.value, page: 0, keyword: keyword.value });
58 - const filteredCourses = keyword.value 54 + if (res.code) {
59 - ? mockCourses.filter(course => 55 + courses.value = res.data;
60 - (course.title?.toLowerCase().includes(keyword.value.toLowerCase()) || 56 + finished.value = res.data.length < limit.value;
61 - course.description?.toLowerCase().includes(keyword.value.toLowerCase())) ?? false
62 - )
63 - : [...mockCourses];
64 - courses.value = filteredCourses;
65 - hasMore.value = filteredCourses.length >= 10;
66 page.value = 1; 57 page.value = 1;
58 + }
67 }; 59 };
68 60
69 // 监听路由参数变化 61 // 监听路由参数变化
70 -watchEffect(() => { 62 +// watchEffect(() => {
71 - const queryKeyword = $route.query.keyword; 63 +// const queryKeyword = $route.query.keyword;
72 - if (keyword.value !== queryKeyword) { 64 +// if (keyword.value !== queryKeyword) {
73 - keyword.value = queryKeyword || ''; 65 +// keyword.value = queryKeyword || '';
74 - searchCourses(); 66 +// searchCourses();
75 - } 67 +// }
76 -}); 68 +// });
77 69
78 // Search handler 70 // Search handler
79 -const handleSearch = (query) => { 71 +// const handleSearch = (query) => {
80 - keyword.value = query; 72 + // keyword.value = query;
81 - searchCourses(); 73 + // searchCourses();
82 -}; 74 +// };
83 75
84 // Blur handler 76 // Blur handler
85 const handleBlur = (query) => { 77 const handleBlur = (query) => {
...@@ -88,22 +80,14 @@ const handleBlur = (query) => { ...@@ -88,22 +80,14 @@ const handleBlur = (query) => {
88 }; 80 };
89 81
90 // Load more courses 82 // Load more courses
91 -const loadMore = () => { 83 +const onLoad = async () => {
92 - // 实际项目中这里会调用分页API 84 + const nextPage = page.value;
93 - if (page.value < 3) { 85 + const res = await getCourseListAPI({ limit: limit.value, page: nextPage, keyword: keyword.value });
94 - const filteredCourses = keyword.value 86 + if (res.code) {
95 - ? mockCourses.filter(course => 87 + courses.value = [...courses.value, ...res.data];
96 - course.title.toLowerCase().includes(keyword.value.toLowerCase()) || 88 + finished.value = res.data.length < limit.value;
97 - course.description.toLowerCase().includes(keyword.value.toLowerCase()) 89 + page.value = nextPage + 1;
98 - )
99 - : [...mockCourses];
100 - courses.value = [...courses.value, ...filteredCourses];
101 - console.warn(courses.value);
102 -
103 - page.value += 1;
104 - hasMore.value = page.value < 3;
105 - } else {
106 - hasMore.value = false;
107 } 90 }
91 + loading.value = false;
108 }; 92 };
109 </script> 93 </script>
......
...@@ -88,11 +88,25 @@ import SearchBar from "@/components/ui/SearchBar.vue"; ...@@ -88,11 +88,25 @@ import SearchBar from "@/components/ui/SearchBar.vue";
88 import FrostedGlass from "@/components/ui/FrostedGlass.vue"; 88 import FrostedGlass from "@/components/ui/FrostedGlass.vue";
89 import CourseCard from "@/components/ui/CourseCard.vue"; 89 import CourseCard from "@/components/ui/CourseCard.vue";
90 import LiveStreamCard from "@/components/ui/LiveStreamCard.vue"; 90 import LiveStreamCard from "@/components/ui/LiveStreamCard.vue";
91 -import { featuredCourse, liveStreams, courses } from "@/utils/mockData"; 91 +import { featuredCourse, liveStreams } from "@/utils/mockData";
92 import { useTitle } from '@vueuse/core'; 92 import { useTitle } from '@vueuse/core';
93 +
94 +// 导入接口
95 +import { getCourseListAPI } from "@/api/course";
96 +
97 +const courses = ref([])
98 +
99 +onMounted(async () => {
100 + const res = await getCourseListAPI({ limit: 4 });
101 + if (res.code) {
102 + courses.value = res.data;
103 + }
104 +})
105 +
93 const $route = useRoute(); 106 const $route = useRoute();
94 const $router = useRouter(); 107 const $router = useRouter();
95 useTitle($route.meta.title); 108 useTitle($route.meta.title);
109 +
96 // Search handler 110 // Search handler
97 const handleSearch = (query) => { 111 const handleSearch = (query) => {
98 console.log("Searching for:", query); 112 console.log("Searching for:", query);
......
...@@ -50,7 +50,7 @@ ...@@ -50,7 +50,7 @@
50 <!-- 目录区域 --> 50 <!-- 目录区域 -->
51 <div id="catalog" class="py-4"> 51 <div id="catalog" class="py-4">
52 <div class="space-y-4"> 52 <div class="space-y-4">
53 - <div v-for="(lesson, index) in course?.lessons" :key="index" class="bg-white p-4 cursor-pointer hover:bg-gray-50 transition-colors border-b border-gray-200 relative"> 53 + <div v-for="(lesson, index) in course?.lessons" :key="index" @click="router.push(`/studyDetail/${lesson.id}`)" class="bg-white p-4 cursor-pointer hover:bg-gray-50 transition-colors border-b border-gray-200 relative">
54 <div v-if="lesson.progress > 0 && lesson.progress < 100" class="absolute top-2 right-2 px-2 py-1 bg-green-100 text-green-600 text-xs rounded">上次看到</div> 54 <div v-if="lesson.progress > 0 && lesson.progress < 100" class="absolute top-2 right-2 px-2 py-1 bg-green-100 text-green-600 text-xs rounded">上次看到</div>
55 <div class="text-black text-base font-medium mb-2">{{ lesson.title }}</div> 55 <div class="text-black text-base font-medium mb-2">{{ lesson.title }}</div>
56 <div class="flex items-center text-sm text-gray-500"> 56 <div class="flex items-center text-sm text-gray-500">
...@@ -92,6 +92,9 @@ ...@@ -92,6 +92,9 @@
92 <script setup> 92 <script setup>
93 import { ref, onMounted, nextTick, onUnmounted } from 'vue'; 93 import { ref, onMounted, nextTick, onUnmounted } from 'vue';
94 import { useTitle } from '@vueuse/core'; 94 import { useTitle } from '@vueuse/core';
95 +import { useRouter } from "vue-router";
96 +
97 +const router = useRouter();
95 98
96 // 页面标题 99 // 页面标题
97 useTitle('课程详情'); 100 useTitle('课程详情');
......