feat(courses): 实现课程模块接口集成与功能优化
- 集成课程列表、详情、热门课程等接口 - 优化课程卡片、课程详情页的数据展示 - 添加课程跳转功能 - 移除模拟数据,使用真实接口数据
Showing
7 changed files
with
215 additions
and
131 deletions
src/api/course.js
0 → 100644
| 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('课程详情'); | ... | ... |
-
Please register or login to post a comment