feat(课程详情页): 添加课程评论功能
在课程详情页中新增评论功能,用户可以对已购买的课程进行评分和评论。新增了ReviewPopup组件用于显示评论弹窗,并在mockData中添加了isPurchased和isReviewed字段以模拟用户购买和评论状态。
Showing
4 changed files
with
154 additions
and
5 deletions
| ... | @@ -18,6 +18,7 @@ declare module 'vue' { | ... | @@ -18,6 +18,7 @@ declare module 'vue' { |
| 18 | GradientHeader: typeof import('./components/ui/GradientHeader.vue')['default'] | 18 | GradientHeader: typeof import('./components/ui/GradientHeader.vue')['default'] |
| 19 | LiveStreamCard: typeof import('./components/ui/LiveStreamCard.vue')['default'] | 19 | LiveStreamCard: typeof import('./components/ui/LiveStreamCard.vue')['default'] |
| 20 | MenuItem: typeof import('./components/ui/MenuItem.vue')['default'] | 20 | MenuItem: typeof import('./components/ui/MenuItem.vue')['default'] |
| 21 | + ReviewPopup: typeof import('./components/ui/ReviewPopup.vue')['default'] | ||
| 21 | RouterLink: typeof import('vue-router')['RouterLink'] | 22 | RouterLink: typeof import('vue-router')['RouterLink'] |
| 22 | RouterView: typeof import('vue-router')['RouterView'] | 23 | RouterView: typeof import('vue-router')['RouterView'] |
| 23 | SearchBar: typeof import('./components/ui/SearchBar.vue')['default'] | 24 | SearchBar: typeof import('./components/ui/SearchBar.vue')['default'] |
| ... | @@ -38,6 +39,7 @@ declare module 'vue' { | ... | @@ -38,6 +39,7 @@ declare module 'vue' { |
| 38 | VanRate: typeof import('vant/es')['Rate'] | 39 | VanRate: typeof import('vant/es')['Rate'] |
| 39 | VanTab: typeof import('vant/es')['Tab'] | 40 | VanTab: typeof import('vant/es')['Tab'] |
| 40 | VanTabs: typeof import('vant/es')['Tabs'] | 41 | VanTabs: typeof import('vant/es')['Tabs'] |
| 42 | + VanToast: typeof import('vant/es')['Toast'] | ||
| 41 | VanUploader: typeof import('vant/es')['Uploader'] | 43 | VanUploader: typeof import('vant/es')['Uploader'] |
| 42 | VideoPlayer: typeof import('./components/ui/VideoPlayer.vue')['default'] | 44 | VideoPlayer: typeof import('./components/ui/VideoPlayer.vue')['default'] |
| 43 | } | 45 | } | ... | ... |
src/components/ui/ReviewPopup.vue
0 → 100644
| 1 | +<!-- | ||
| 2 | + * @Date: 2025-03-24 16:57:55 | ||
| 3 | + * @LastEditors: hookehuyr hookehuyr@gmail.com | ||
| 4 | + * @LastEditTime: 2025-03-24 17:17:44 | ||
| 5 | + * @FilePath: /mlaj/src/components/ui/ReviewPopup.vue | ||
| 6 | + * @Description: 文件描述 | ||
| 7 | +--> | ||
| 8 | +<template> | ||
| 9 | + <van-popup | ||
| 10 | + :show="show" | ||
| 11 | + @update:show="emit('update:show', $event)" | ||
| 12 | + position="bottom" | ||
| 13 | + round | ||
| 14 | + > | ||
| 15 | + <div class="p-4"> | ||
| 16 | + <div class="text-lg font-bold text-center mb-4">课程评价</div> | ||
| 17 | + <div class="flex justify-center mb-4"> | ||
| 18 | + <van-rate | ||
| 19 | + v-model="rating" | ||
| 20 | + :size="24" | ||
| 21 | + color="#ffd21e" | ||
| 22 | + void-icon="star" | ||
| 23 | + void-color="#eee" | ||
| 24 | + /> | ||
| 25 | + </div> | ||
| 26 | + <van-field | ||
| 27 | + v-model="content" | ||
| 28 | + rows="3" | ||
| 29 | + type="textarea" | ||
| 30 | + placeholder="请输入您的评价内容" | ||
| 31 | + class="mb-4" | ||
| 32 | + /> | ||
| 33 | + <div class="flex justify-end"> | ||
| 34 | + <van-button | ||
| 35 | + round | ||
| 36 | + type="default" | ||
| 37 | + style="margin-right: 0.5rem" | ||
| 38 | + @click="handleCancel" | ||
| 39 | + >取消</van-button | ||
| 40 | + > | ||
| 41 | + <van-button round type="primary" color="#4CAF50" @click="handleSubmit" | ||
| 42 | + >提交评价</van-button | ||
| 43 | + > | ||
| 44 | + </div> | ||
| 45 | + </div> | ||
| 46 | + </van-popup> | ||
| 47 | + | ||
| 48 | + <van-toast v-model:show="show_toast"> | ||
| 49 | + <template #message> | ||
| 50 | + {{ message }} | ||
| 51 | + </template> | ||
| 52 | + </van-toast> | ||
| 53 | +</template> | ||
| 54 | + | ||
| 55 | +<script setup> | ||
| 56 | +import { ref } from "vue"; | ||
| 57 | + | ||
| 58 | +const props = defineProps({ | ||
| 59 | + show: { | ||
| 60 | + type: Boolean, | ||
| 61 | + default: false, | ||
| 62 | + }, | ||
| 63 | +}); | ||
| 64 | + | ||
| 65 | +const emit = defineEmits(["update:show", "submit"]); | ||
| 66 | + | ||
| 67 | +const rating = ref(5); | ||
| 68 | +const content = ref(""); | ||
| 69 | + | ||
| 70 | +const handleCancel = () => { | ||
| 71 | + emit("update:show", false); | ||
| 72 | + rating.value = 5; | ||
| 73 | + content.value = ""; | ||
| 74 | +}; | ||
| 75 | + | ||
| 76 | +const show_toast = ref(false); | ||
| 77 | +const message = ref(""); | ||
| 78 | + | ||
| 79 | +const handleSubmit = () => { | ||
| 80 | + if (rating.value === 0) { | ||
| 81 | + show_toast.value = true; | ||
| 82 | + message.value = "请选择评分"; | ||
| 83 | + return; | ||
| 84 | + } | ||
| 85 | + if (!content.value.trim()) { | ||
| 86 | + show_toast.value = true; | ||
| 87 | + message.value = "请输入评论内容"; | ||
| 88 | + return; | ||
| 89 | + } | ||
| 90 | + emit("submit", { | ||
| 91 | + rating: rating.value, | ||
| 92 | + content: content.value.trim(), | ||
| 93 | + }); | ||
| 94 | + show_toast.value = true; | ||
| 95 | + message.value = "评论提交成功"; | ||
| 96 | + handleCancel(); | ||
| 97 | +}; | ||
| 98 | +</script> |
| ... | @@ -50,6 +50,8 @@ export const courses = [ | ... | @@ -50,6 +50,8 @@ export const courses = [ |
| 50 | price: 365, | 50 | price: 365, |
| 51 | updatedLessons: 16, | 51 | updatedLessons: 16, |
| 52 | subscribers: 1140, | 52 | subscribers: 1140, |
| 53 | + isPurchased: true, | ||
| 54 | + isReviewed: true, | ||
| 53 | expireDate: '2025-04-17 00:00:00', | 55 | expireDate: '2025-04-17 00:00:00', |
| 54 | description: `【美乐考前赋能营】 | 56 | description: `【美乐考前赋能营】 |
| 55 | 结合平台多年亲子教育的实践经验, | 57 | 结合平台多年亲子教育的实践经验, |
| ... | @@ -77,7 +79,9 @@ export const courses = [ | ... | @@ -77,7 +79,9 @@ export const courses = [ |
| 77 | imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/GGCP6vshpPY.jpg', // Updated with new image | 79 | imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/GGCP6vshpPY.jpg', // Updated with new image |
| 78 | price: 3665, | 80 | price: 3665, |
| 79 | updatedLessons: 16, | 81 | updatedLessons: 16, |
| 80 | - subscribers: 1140 | 82 | + subscribers: 1140, |
| 83 | + isPurchased: true, | ||
| 84 | + isReviewed: false | ||
| 81 | }, | 85 | }, |
| 82 | { | 86 | { |
| 83 | id: 'course-3', | 87 | id: 'course-3', |
| ... | @@ -86,7 +90,9 @@ export const courses = [ | ... | @@ -86,7 +90,9 @@ export const courses = [ |
| 86 | imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/2Juj2cXWB7U.jpg', // Updated with new image | 90 | imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/2Juj2cXWB7U.jpg', // Updated with new image |
| 87 | price: 1280, | 91 | price: 1280, |
| 88 | updatedLessons: 16, | 92 | updatedLessons: 16, |
| 89 | - subscribers: 1140 | 93 | + subscribers: 1140, |
| 94 | + isPurchased: false, | ||
| 95 | + isReviewed: false | ||
| 90 | } | 96 | } |
| 91 | ]; | 97 | ]; |
| 92 | 98 | ... | ... |
| ... | @@ -244,15 +244,45 @@ | ... | @@ -244,15 +244,45 @@ |
| 244 | ¥{{ Math.round(course?.price * 1.2) }} | 244 | ¥{{ Math.round(course?.price * 1.2) }} |
| 245 | </div> | 245 | </div> |
| 246 | </div> | 246 | </div> |
| 247 | - <button | 247 | + <van-button |
| 248 | + v-if="!isPurchased" | ||
| 248 | @click="handlePurchase" | 249 | @click="handlePurchase" |
| 249 | - class="bg-gradient-to-r from-green-500 to-green-600 text-white px-6 py-2 rounded-full text-sm font-medium shadow-md" | 250 | + round |
| 251 | + block | ||
| 252 | + color="linear-gradient(to right, #22c55e, #16a34a)" | ||
| 253 | + class="shadow-md" | ||
| 250 | > | 254 | > |
| 251 | 立即购买 | 255 | 立即购买 |
| 252 | - </button> | 256 | + </van-button> |
| 257 | + <van-button | ||
| 258 | + v-else-if="!isReviewed" | ||
| 259 | + @click="showReviewPopup = true" | ||
| 260 | + round | ||
| 261 | + block | ||
| 262 | + color="linear-gradient(to right, #3b82f6, #2563eb)" | ||
| 263 | + class="shadow-md" | ||
| 264 | + > | ||
| 265 | + 立即评论 | ||
| 266 | + </van-button> | ||
| 267 | + <van-button | ||
| 268 | + v-else | ||
| 269 | + @click="router.push(`/courses/${course?.id}/reviews`)" | ||
| 270 | + round | ||
| 271 | + block | ||
| 272 | + color="linear-gradient(to right, #6b7280, #4b5563)" | ||
| 273 | + class="shadow-md" | ||
| 274 | + > | ||
| 275 | + 查看评论 | ||
| 276 | + </van-button> | ||
| 253 | </div> | 277 | </div> |
| 254 | </div> | 278 | </div> |
| 255 | </div> | 279 | </div> |
| 280 | + | ||
| 281 | + <!-- Review Popup --> | ||
| 282 | + <ReviewPopup | ||
| 283 | + v-model:show="showReviewPopup" | ||
| 284 | + @submit="handleReviewSubmit" | ||
| 285 | + /> | ||
| 256 | </AppLayout> | 286 | </AppLayout> |
| 257 | </template> | 287 | </template> |
| 258 | 288 | ||
| ... | @@ -261,6 +291,7 @@ import { ref, onMounted, defineComponent, h } from 'vue' | ... | @@ -261,6 +291,7 @@ import { ref, onMounted, defineComponent, h } from 'vue' |
| 261 | import { useRoute, useRouter } from 'vue-router' | 291 | import { useRoute, useRouter } from 'vue-router' |
| 262 | import AppLayout from '@/components/layout/AppLayout.vue' | 292 | import AppLayout from '@/components/layout/AppLayout.vue' |
| 263 | import FrostedGlass from '@/components/ui/FrostedGlass.vue' | 293 | import FrostedGlass from '@/components/ui/FrostedGlass.vue' |
| 294 | +import ReviewPopup from '@/components/ui/ReviewPopup.vue' | ||
| 264 | import { courses } from '@/utils/mockData' | 295 | import { courses } from '@/utils/mockData' |
| 265 | import { useCart } from '@/contexts/cart' | 296 | import { useCart } from '@/contexts/cart' |
| 266 | import { useTitle } from '@vueuse/core'; | 297 | import { useTitle } from '@vueuse/core'; |
| ... | @@ -272,6 +303,9 @@ const router = useRouter() | ... | @@ -272,6 +303,9 @@ const router = useRouter() |
| 272 | const course = ref(null) | 303 | const course = ref(null) |
| 273 | const activeTab = ref('课程特色') | 304 | const activeTab = ref('课程特色') |
| 274 | const isFavorite = ref(false) | 305 | const isFavorite = ref(false) |
| 306 | +const isPurchased = ref(false) | ||
| 307 | +const isReviewed = ref(false) | ||
| 308 | +const showReviewPopup = ref(false) | ||
| 275 | const { addToCart, proceedToCheckout } = useCart() | 309 | const { addToCart, proceedToCheckout } = useCart() |
| 276 | 310 | ||
| 277 | // Handle favorite toggle | 311 | // Handle favorite toggle |
| ... | @@ -333,12 +367,21 @@ const handlePurchase = () => { | ... | @@ -333,12 +367,21 @@ const handlePurchase = () => { |
| 333 | } | 367 | } |
| 334 | } | 368 | } |
| 335 | 369 | ||
| 370 | +// Handle review submit | ||
| 371 | +const handleReviewSubmit = (review) => { | ||
| 372 | + // TODO: 对接评论提交接口 | ||
| 373 | + console.log('Review submitted:', review) | ||
| 374 | + isReviewed.value = true | ||
| 375 | +} | ||
| 376 | + | ||
| 336 | // Fetch course data | 377 | // Fetch course data |
| 337 | onMounted(() => { | 378 | onMounted(() => { |
| 338 | const id = route.params.id | 379 | const id = route.params.id |
| 339 | const foundCourse = courses.find(c => c.id === id) | 380 | const foundCourse = courses.find(c => c.id === id) |
| 340 | if (foundCourse) { | 381 | if (foundCourse) { |
| 341 | course.value = foundCourse | 382 | course.value = foundCourse |
| 383 | + isPurchased.value = foundCourse.isPurchased | ||
| 384 | + isReviewed.value = foundCourse.isReviewed | ||
| 342 | } else { | 385 | } else { |
| 343 | // Course not found, redirect to courses page | 386 | // Course not found, redirect to courses page |
| 344 | router.push('/courses') | 387 | router.push('/courses') | ... | ... |
-
Please register or login to post a comment