hookehuyr

feat(课程详情页): 添加课程评论功能

在课程详情页中新增评论功能,用户可以对已购买的课程进行评分和评论。新增了ReviewPopup组件用于显示评论弹窗,并在mockData中添加了isPurchased和isReviewed字段以模拟用户购买和评论状态。
...@@ -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 }
......
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')
......