hookehuyr

feat(课程评论): 实现课程评论的点赞、取消点赞及分页加载功能

添加了课程评论的点赞和取消点赞功能,并优化了评论列表的分页加载逻辑。同时,修复了评论提交后的列表刷新问题,确保数据一致性。
1 /* 1 /*
2 * @Date: 2025-04-15 09:32:07 2 * @Date: 2025-04-15 09:32:07
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-04-18 14:43:46 4 + * @LastEditTime: 2025-04-18 17:15:52
5 * @FilePath: /mlaj/src/api/course.js 5 * @FilePath: /mlaj/src/api/course.js
6 * @Description: 课程模块相关接口 6 * @Description: 课程模块相关接口
7 */ 7 */
...@@ -15,6 +15,8 @@ const Api = { ...@@ -15,6 +15,8 @@ const Api = {
15 GROUP_COMMENT_ADD: '/srv/?a=group_comment_add', 15 GROUP_COMMENT_ADD: '/srv/?a=group_comment_add',
16 GROUP_COMMENT_EDIT: '/srv/?a=group_comment_edit', 16 GROUP_COMMENT_EDIT: '/srv/?a=group_comment_edit',
17 GROUP_COMMENT_DEL: '/srv/?a=group_comment_del', 17 GROUP_COMMENT_DEL: '/srv/?a=group_comment_del',
18 + GROUP_COMMENT_LIKE: '/srv/?a=group_comment_like',
19 + GROUP_COMMENT_DISLIKE: '/srv/?a=group_comment_dislike',
18 } 20 }
19 21
20 /** 22 /**
...@@ -44,6 +46,7 @@ export const getScheduleCourseAPI = (params) => fn(fetch.get(Api.GET_SCHEDULE_CO ...@@ -44,6 +46,7 @@ export const getScheduleCourseAPI = (params) => fn(fetch.get(Api.GET_SCHEDULE_CO
44 /** 46 /**
45 * @description: 获取课程评论列表 47 * @description: 获取课程评论列表
46 * @param: i 课程 ID 48 * @param: i 课程 ID
49 + * @param: schedule_id 章节ID,非必须,在课程章节内查询时需要
47 * @param: limit 每页数量 默认10 50 * @param: limit 每页数量 默认10
48 * @param: page 页码 51 * @param: page 页码
49 * @return: data: { comment_score 课程评论分数, comment_count 评论数量, comment_list [{ id 评论id, created_by 评论人ID, name 评论人姓名, note 评论内容, score 分数, create_time 评论时间}] 评论列表} 52 * @return: data: { comment_score 课程评论分数, comment_count 评论数量, comment_list [{ id 评论id, created_by 评论人ID, name 评论人姓名, note 评论内容, score 分数, create_time 评论时间}] 评论列表}
...@@ -53,6 +56,7 @@ export const getGroupCommentListAPI = (params) => fn(fetch.get(Api.GET_GROUP_COM ...@@ -53,6 +56,7 @@ export const getGroupCommentListAPI = (params) => fn(fetch.get(Api.GET_GROUP_COM
53 /** 56 /**
54 * @description: 添加课程评论 57 * @description: 添加课程评论
55 * @param: i 课程 ID 58 * @param: i 课程 ID
59 + * @param: schedule_id 章节ID,非必须,在课程章节添加时需要
56 * @param: note 评论内容 60 * @param: note 评论内容
57 * @param: score 分数 61 * @param: score 分数
58 * @return: data: '' 62 * @return: data: ''
...@@ -70,7 +74,21 @@ export const editGroupCommentAPI = (params) => fn(fetch.post(Api.GROUP_COMMENT_E ...@@ -70,7 +74,21 @@ export const editGroupCommentAPI = (params) => fn(fetch.post(Api.GROUP_COMMENT_E
70 74
71 /** 75 /**
72 * @description: 删除课程评论 76 * @description: 删除课程评论
73 - * @param: i 课程 ID 77 + * @param: i 课程ID
74 * @return: data: '' 78 * @return: data: ''
75 */ 79 */
76 export const delGroupCommentAPI = (params) => fn(fetch.post(Api.GROUP_COMMENT_DEL, params)) 80 export const delGroupCommentAPI = (params) => fn(fetch.post(Api.GROUP_COMMENT_DEL, params))
81 +
82 +/**
83 + * @description: 点赞章节评论
84 + * @param: i 评论ID
85 + * @return: data: ''
86 + */
87 +export const addGroupCommentLikeAPI = (params) => fn(fetch.post(Api.GROUP_COMMENT_LIKE, params))
88 +
89 +/**
90 + * @description: 取消点赞章节评论
91 + * @param: i 评论ID
92 + * @return: data: ''
93 + */
94 +export const delGroupCommentLikeAPI = (params) => fn(fetch.post(Api.GROUP_COMMENT_DISLIKE, params))
......
...@@ -20,7 +20,7 @@ declare module 'vue' { ...@@ -20,7 +20,7 @@ declare module 'vue' {
20 GradientHeader: typeof import('./components/ui/GradientHeader.vue')['default'] 20 GradientHeader: typeof import('./components/ui/GradientHeader.vue')['default']
21 LiveStreamCard: typeof import('./components/ui/LiveStreamCard.vue')['default'] 21 LiveStreamCard: typeof import('./components/ui/LiveStreamCard.vue')['default']
22 MenuItem: typeof import('./components/ui/MenuItem.vue')['default'] 22 MenuItem: typeof import('./components/ui/MenuItem.vue')['default']
23 - ReviewPopup: typeof import('./components/ui/ReviewPopup.vue')['default'] 23 + ReviewPopup: typeof import('./components/courses/ReviewPopup.vue')['default']
24 RouterLink: typeof import('vue-router')['RouterLink'] 24 RouterLink: typeof import('vue-router')['RouterLink']
25 RouterView: typeof import('vue-router')['RouterView'] 25 RouterView: typeof import('vue-router')['RouterView']
26 SearchBar: typeof import('./components/ui/SearchBar.vue')['default'] 26 SearchBar: typeof import('./components/ui/SearchBar.vue')['default']
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
11 <div v-if="course.course_type === 'video'" class="w-full bg-black relative"> 11 <div v-if="course.course_type === 'video'" class="w-full bg-black relative">
12 <!-- 视频封面和播放按钮 --> 12 <!-- 视频封面和播放按钮 -->
13 <div v-if="!isPlaying" class="relative w-full" style="aspect-ratio: 16/9;"> 13 <div v-if="!isPlaying" class="relative w-full" style="aspect-ratio: 16/9;">
14 - <img :src="course.cover" :alt="course.title" class="w-full h-full object-cover" /> 14 + <img :src="courseFile.cover" :alt="course.title" class="w-full h-full object-cover" />
15 <div class="absolute inset-0 flex items-center justify-center cursor-pointer" 15 <div class="absolute inset-0 flex items-center justify-center cursor-pointer"
16 @click="startPlay"> 16 @click="startPlay">
17 <div 17 <div
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
22 </div> 22 </div>
23 </div> 23 </div>
24 <!-- 视频播放器 --> 24 <!-- 视频播放器 -->
25 - <VideoPlayer v-show="isPlaying" ref="videoPlayerRef" :video-url="course.file" :autoplay="false" 25 + <VideoPlayer v-show="isPlaying" ref="videoPlayerRef" :video-url="courseFile.url" :autoplay="false"
26 @onPlay="handleVideoPlay" @onPause="handleVideoPause" /> 26 @onPlay="handleVideoPlay" @onPause="handleVideoPause" />
27 </div> 27 </div>
28 <div v-if="course.course_type === 'audio'" class="w-full relative" style="border-bottom: 1px solid #F3F4F6;"> 28 <div v-if="course.course_type === 'audio'" class="w-full relative" style="border-bottom: 1px solid #F3F4F6;">
...@@ -35,7 +35,7 @@ ...@@ -35,7 +35,7 @@
35 <van-tab title="介绍" name="intro"> 35 <van-tab title="介绍" name="intro">
36 </van-tab> 36 </van-tab>
37 <van-tab :title-style="{ 'min-width': '50%' }" name="comments"> 37 <van-tab :title-style="{ 'min-width': '50%' }" name="comments">
38 - <template #title>评论(999)</template> 38 + <template #title>评论({{ commentCount }})</template>
39 </van-tab> 39 </van-tab>
40 </van-tabs> 40 </van-tabs>
41 </div> 41 </div>
...@@ -57,27 +57,27 @@ ...@@ -57,27 +57,27 @@
57 57
58 <div id="comment" class="py-4 px-4 space-y-4" :style="{ paddingBottom: bottomWrapperHeight }"> 58 <div id="comment" class="py-4 px-4 space-y-4" :style="{ paddingBottom: bottomWrapperHeight }">
59 <div class="flex justify-between items-center mb-4"> 59 <div class="flex justify-between items-center mb-4">
60 - <div class="text-gray-900 font-medium text-sm">评论 ({{ comments.length }})</div> 60 + <div class="text-gray-900 font-medium text-sm">评论 ({{ commentCount }})</div>
61 <div class="text-gray-500 cursor-pointer text-sm" @click="showCommentPopup = true">查看更多</div> 61 <div class="text-gray-500 cursor-pointer text-sm" @click="showCommentPopup = true">查看更多</div>
62 </div> 62 </div>
63 - <!-- 显示前三条评论 --> 63 + <!-- 显示条评论 -->
64 - <div v-for="comment in comments.slice(0, 3)" :key="comment.id" 64 + <div v-for="comment in commentList" :key="comment.id"
65 class="border-b border-gray-100 last:border-b-0 py-4"> 65 class="border-b border-gray-100 last:border-b-0 py-4">
66 <div class="flex"> 66 <div class="flex">
67 - <img :src="comment.avatar" class="w-10 h-10 rounded-full flex-shrink-0" 67 + <!-- <img :src="comment.avatar" class="w-10 h-10 rounded-full flex-shrink-0"
68 - style="margin-right: 0.5rem;" /> 68 + style="margin-right: 0.5rem;" /> -->
69 <div class="flex-1 ml-3"> 69 <div class="flex-1 ml-3">
70 <div class="flex justify-between items-center mb-1"> 70 <div class="flex justify-between items-center mb-1">
71 - <span class="font-medium text-gray-900">{{ comment.username }}</span> 71 + <span class="font-medium text-gray-900">{{ comment.name }}</span>
72 <div class="flex items-center space-x-1"> 72 <div class="flex items-center space-x-1">
73 - <span class="text-sm text-gray-500">{{ comment.likes }}</span> &nbsp; 73 + <span class="text-sm text-gray-500">{{ comment.like_count }}</span> &nbsp;
74 - <van-icon :name="comment.isLiked ? 'like' : 'like-o'" 74 + <van-icon :name="comment.is_like ? 'like' : 'like-o'"
75 - :class="{ 'text-red-500': comment.isLiked, 'text-gray-400': !comment.isLiked }" 75 + :class="{ 'text-red-500': comment.is_like, 'text-gray-400': !comment.is_like }"
76 @click="toggleLike(comment)" class="text-lg cursor-pointer" /> 76 @click="toggleLike(comment)" class="text-lg cursor-pointer" />
77 </div> 77 </div>
78 </div> 78 </div>
79 - <p class="text-gray-700 text-sm mb-1">{{ comment.content }}</p> 79 + <p class="text-gray-700 text-sm mb-1">{{ comment.note }}</p>
80 - <div class="text-gray-400 text-xs">{{ comment.time }}</div> 80 + <div class="text-gray-400 text-xs">{{ formatDate(comment.updated_time) }}</div>
81 </div> 81 </div>
82 </div> 82 </div>
83 </div> 83 </div>
...@@ -86,36 +86,39 @@ ...@@ -86,36 +86,39 @@
86 <div class="flex flex-col h-full"> 86 <div class="flex flex-col h-full">
87 <!-- 固定头部 --> 87 <!-- 固定头部 -->
88 <div class="flex-none px-4 py-3 border-b bg-white sticky top-0 z-10"> 88 <div class="flex-none px-4 py-3 border-b bg-white sticky top-0 z-10">
89 - <div class="text-lg font-medium">全部评论 ({{ comments.length }})</div> 89 + <div class="text-lg font-medium">全部评论 ({{ popupCommentList.length }})</div>
90 </div> 90 </div>
91 91
92 <!-- 可滚动的评论列表 --> 92 <!-- 可滚动的评论列表 -->
93 <div class="flex-1 overflow-y-auto"> 93 <div class="flex-1 overflow-y-auto">
94 - <div class="px-4 py-2 pb-16"> 94 + <van-list
95 - <div v-for="comment in comments" :key="comment.id" 95 + v-model:loading="popupLoading"
96 + :finished="popupFinished"
97 + finished-text="没有更多评论了"
98 + @load="onPopupLoad"
99 + class="px-4 py-2 pb-16"
100 + >
101 + <div v-for="comment in popupCommentList" :key="comment.id"
96 class="border-b border-gray-100 last:border-b-0 py-4"> 102 class="border-b border-gray-100 last:border-b-0 py-4">
97 <div class="flex"> 103 <div class="flex">
98 - <img :src="comment.avatar" class="w-10 h-10 rounded-full flex-shrink-0"
99 - style="margin-right: 0.5rem;" />
100 <div class="flex-1 ml-3"> 104 <div class="flex-1 ml-3">
101 <div class="flex justify-between items-center mb-1"> 105 <div class="flex justify-between items-center mb-1">
102 - <span class="font-medium text-gray-900">{{ comment.username 106 + <span class="font-medium text-gray-900">{{ comment.name }}</span>
103 - }}</span>
104 <div class="flex items-center space-x-1"> 107 <div class="flex items-center space-x-1">
105 - <span class="text-sm text-gray-500">{{ comment.likes }}</span> 108 + <span class="text-sm text-gray-500">{{ comment.like_count }}</span>
106 &nbsp; 109 &nbsp;
107 - <van-icon :name="comment.isLiked ? 'like' : 'like-o'" 110 + <van-icon :name="comment.is_like ? 'like' : 'like-o'"
108 - :class="{ 'text-red-500': comment.isLiked, 'text-gray-400': !comment.isLiked }" 111 + :class="{ 'text-red-500': comment.is_like, 'text-gray-400': !comment.is_like }"
109 @click="toggleLike(comment)" 112 @click="toggleLike(comment)"
110 class="text-lg cursor-pointer" /> 113 class="text-lg cursor-pointer" />
111 </div> 114 </div>
112 </div> 115 </div>
113 - <p class="text-gray-700 text-sm mb-1">{{ comment.content }}</p> 116 + <p class="text-gray-700 text-sm mb-1">{{ comment.note }}</p>
114 - <div class="text-gray-400 text-xs">{{ comment.time }}</div> 117 + <div class="text-gray-400 text-xs">{{ formatDate(comment.updated_time) }}</div>
115 </div> 118 </div>
116 </div> 119 </div>
117 </div> 120 </div>
118 - </div> 121 + </van-list>
119 </div> 122 </div>
120 123
121 <!-- 固定底部输入框 --> 124 <!-- 固定底部输入框 -->
...@@ -161,9 +164,10 @@ import { useTitle } from '@vueuse/core'; ...@@ -161,9 +164,10 @@ import { useTitle } from '@vueuse/core';
161 import VideoPlayer from '@/components/ui/VideoPlayer.vue'; 164 import VideoPlayer from '@/components/ui/VideoPlayer.vue';
162 import AudioPlayer from '@/components/ui/AudioPlayer.vue'; 165 import AudioPlayer from '@/components/ui/AudioPlayer.vue';
163 import dayjs from 'dayjs'; 166 import dayjs from 'dayjs';
167 +import { formatDate } from '@/utils/tools'
164 168
165 // 导入接口 169 // 导入接口
166 -import { getScheduleCourseAPI } from '@/api/course'; 170 +import { getScheduleCourseAPI, getGroupCommentListAPI, addGroupCommentAPI, addGroupCommentLikeAPI, delGroupCommentLikeAPI } from '@/api/course';
167 171
168 const route = useRoute(); 172 const route = useRoute();
169 const course = ref(null); 173 const course = ref(null);
...@@ -193,123 +197,73 @@ const handleVideoPause = () => { ...@@ -193,123 +197,73 @@ const handleVideoPause = () => {
193 // 保持视频播放器可见,只在初始状态显示封面 197 // 保持视频播放器可见,只在初始状态显示封面
194 }; 198 };
195 199
196 -// 评论列表 200 +// 评论列表分页参数
197 -const comments = ref([ 201 +const popupCommentList = ref([]);
198 - { 202 +const popupLoading = ref(false);
199 - id: 1, 203 +const popupFinished = ref(false);
200 - username: '欢乐马', 204 +const popupLimit = ref(5);
201 - avatar: 'https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg', 205 +const popupPage = ref(0);
202 - content: '教育的顶级传承,是你用什么样的心,传承智慧',
203 - time: '2024-12-04 18:51',
204 - likes: 12,
205 - isLiked: false
206 - },
207 - {
208 - id: 2,
209 - username: '欢乐马',
210 - avatar: 'https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg',
211 - content: '不要用战术上的勤奋,掩盖战略上的懒惰',
212 - time: '2024-12-04 08:01',
213 - likes: 8,
214 - isLiked: true
215 - },
216 - {
217 - id: 3,
218 - username: '欢乐马',
219 - avatar: 'https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg',
220 - content: '和老师积极互动,整个课堂像为你而讲',
221 - time: '2024-12-04 07:54',
222 - likes: 5,
223 - isLiked: false
224 - },
225 - {
226 - id: 1,
227 - username: '欢乐马',
228 - avatar: 'https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg',
229 - content: '教育的顶级传承,是你用什么样的心,传承智慧',
230 - time: '2024-12-04 18:51',
231 - likes: 12,
232 - isLiked: false
233 - },
234 - {
235 - id: 2,
236 - username: '欢乐马',
237 - avatar: 'https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg',
238 - content: '不要用战术上的勤奋,掩盖战略上的懒惰',
239 - time: '2024-12-04 08:01',
240 - likes: 8,
241 - isLiked: true
242 - },
243 - {
244 - id: 3,
245 - username: '欢乐马',
246 - avatar: 'https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg',
247 - content: '和老师积极互动,整个课堂像为你而讲',
248 - time: '2024-12-04 07:54',
249 - likes: 5,
250 - isLiked: false
251 - }
252 -]);
253 206
254 // 测试音频数据 207 // 测试音频数据
255 -const audioList = ref([ 208 +// const audioList = ref([
256 - { 209 +// {
257 - id: 1, 210 +// id: 1,
258 - title: '示例音频 1', 211 +// title: '示例音频 1',
259 - artist: '演唱者 1', 212 +// artist: '演唱者 1',
260 - cover: 'https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg', 213 +// cover: 'https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg',
261 - url: 'https://img.tukuppt.com/newpreview_music/09/03/95/5c8af46b01eb138909.mp3' 214 +// url: 'https://img.tukuppt.com/newpreview_music/09/03/95/5c8af46b01eb138909.mp3'
262 - }, 215 +// },
263 - { 216 +// {
264 - id: 2, 217 +// id: 2,
265 - title: '示例音频 2', 218 +// title: '示例音频 2',
266 - artist: '演唱者 2', 219 +// artist: '演唱者 2',
267 - cover: 'https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg', 220 +// cover: 'https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg',
268 - url: 'https://img.tukuppt.com/newpreview_music/08/99/00/5c88d4a8d1f5745026.mp3' 221 +// url: 'https://img.tukuppt.com/newpreview_music/08/99/00/5c88d4a8d1f5745026.mp3'
269 - }, 222 +// },
270 - { 223 +// {
271 - id: 3, 224 +// id: 3,
272 - title: '示例音频 3', 225 +// title: '示例音频 3',
273 - artist: '演唱者 3', 226 +// artist: '演唱者 3',
274 - cover: 'https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg', 227 +// cover: 'https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg',
275 - url: 'https://img.tukuppt.com/newpreview_music/09/03/95/5c8af46b01eb138909.mp3' 228 +// url: 'https://img.tukuppt.com/newpreview_music/09/03/95/5c8af46b01eb138909.mp3'
276 - }, 229 +// },
277 - { 230 +// {
278 - id: 4, 231 +// id: 4,
279 - title: '示例音频 4', 232 +// title: '示例音频 4',
280 - artist: '演唱者 4', 233 +// artist: '演唱者 4',
281 - cover: 'https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg', 234 +// cover: 'https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg',
282 - url: 'https://img.tukuppt.com/newpreview_music/09/03/72/5c8ad96fbf47328809.mp3' 235 +// url: 'https://img.tukuppt.com/newpreview_music/09/03/72/5c8ad96fbf47328809.mp3'
283 - }, 236 +// },
284 - { 237 +// {
285 - id: 5, 238 +// id: 5,
286 - title: '示例音频 5', 239 +// title: '示例音频 5',
287 - artist: '演唱者 5', 240 +// artist: '演唱者 5',
288 - cover: 'https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg', 241 +// cover: 'https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg',
289 - url: 'https://img.tukuppt.com/newpreview_music/09/03/95/5c8af46b01eb138909.mp3' 242 +// url: 'https://img.tukuppt.com/newpreview_music/09/03/95/5c8af46b01eb138909.mp3'
290 - }, 243 +// },
291 - { 244 +// {
292 - id: 6, 245 +// id: 6,
293 - title: '示例音频 6', 246 +// title: '示例音频 6',
294 - artist: '演唱者 6', 247 +// artist: '演唱者 6',
295 - cover: 'https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg', 248 +// cover: 'https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg',
296 - url: 'https://img.tukuppt.com/newpreview_music/09/03/72/5c8ad96fbf47328809.mp3' 249 +// url: 'https://img.tukuppt.com/newpreview_music/09/03/72/5c8ad96fbf47328809.mp3'
297 - }, 250 +// },
298 - { 251 +// {
299 - id: 7, 252 +// id: 7,
300 - title: '示例音频 7', 253 +// title: '示例音频 7',
301 - artist: '演唱者 7', 254 +// artist: '演唱者 7',
302 - cover: 'https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg', 255 +// cover: 'https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg',
303 - url: 'https://img.tukuppt.com/newpreview_music/09/03/95/5c8af46b01eb138909.mp3' 256 +// url: 'https://img.tukuppt.com/newpreview_music/09/03/95/5c8af46b01eb138909.mp3'
304 - }, 257 +// },
305 - { 258 +// {
306 - id: 8, 259 +// id: 8,
307 - title: '示例音频 8', 260 +// title: '示例音频 8',
308 - artist: '演唱者 8', 261 +// artist: '演唱者 8',
309 - cover: 'https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg', 262 +// cover: 'https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg',
310 - url: 'https://img.tukuppt.com/newpreview_music/09/03/72/5c8ad96fbf47328809.mp3' 263 +// url: 'https://img.tukuppt.com/newpreview_music/09/03/72/5c8ad96fbf47328809.mp3'
311 - }, 264 +// },
312 -]); 265 +// ]);
266 +const audioList = ref([]);
313 267
314 // 设置页面标题 268 // 设置页面标题
315 useTitle('学习详情'); 269 useTitle('学习详情');
...@@ -333,6 +287,10 @@ const handleScroll = () => { ...@@ -333,6 +287,10 @@ const handleScroll = () => {
333 } 287 }
334 }; 288 };
335 289
290 +const commentCount = ref(0);
291 +const commentList = ref([]);
292 +const courseFile = ref({});
293 +
336 onMounted(async () => { 294 onMounted(async () => {
337 // 延迟设置topWrapper和bottomWrapper的高度 295 // 延迟设置topWrapper和bottomWrapper的高度
338 setTimeout(() => { 296 setTimeout(() => {
...@@ -356,37 +314,70 @@ onMounted(async () => { ...@@ -356,37 +314,70 @@ onMounted(async () => {
356 const { code, data } = await getScheduleCourseAPI({ i: courseId }); 314 const { code, data } = await getScheduleCourseAPI({ i: courseId });
357 if (code) { 315 if (code) {
358 course.value = data; 316 course.value = data;
359 - // TODO: 测试数据 317 + courseFile.value = data.file;
360 - course.value.cover = 'https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg';
361 // 音频列表处理 318 // 音频列表处理
362 - // if (data.course_type === 'audio') { 319 + if (data.course_type === 'audio') {
363 - // audioList.value = [course.value.file]; 320 + audioList.value = [data.file];
364 - // } 321 + }
322 +
323 + // 获取评论列表
324 + const comment = await getGroupCommentListAPI({ group_id: course.value.group_id, schedule_id: course.value.id });
325 + if (comment.code) {
326 + commentList.value = comment.data.comment_list;
327 + commentCount.value = comment.data.comment_count;
328 + }
365 } 329 }
366 } 330 }
367 }) 331 })
368 332
369 // 提交评论 333 // 提交评论
370 // 切换点赞状态 334 // 切换点赞状态
371 -const toggleLike = (comment) => { 335 +const toggleLike = async (comment) => {
372 - comment.isLiked = !comment.isLiked; 336 + try {
373 - comment.likes += comment.isLiked ? 1 : -1; 337 + if (!comment.is_like) {
338 + const { code } = await addGroupCommentLikeAPI({ i: comment.id });
339 + if (code) {
340 + comment.is_like = true;
341 + comment.like_count += 1;
342 + }
343 + } else {
344 + const { code } = await delGroupCommentLikeAPI({ i: comment.id });
345 + if (code) {
346 + comment.is_like = false;
347 + comment.like_count -= 1;
348 + }
349 + }
350 + } catch (error) {
351 + console.error('点赞操作失败:', error);
352 + }
374 }; 353 };
375 354
376 -const submitComment = () => { 355 +// 发送按钮,提交评论
356 +const submitComment = async () => {
377 if (!newComment.value.trim()) return; 357 if (!newComment.value.trim()) return;
378 358
379 - comments.value.unshift({ 359 + try {
380 - id: Date.now(), 360 + const { code, data } = await addGroupCommentAPI({
381 - username: '当前用户', 361 + group_id: course.value.group_id,
382 - avatar: 'https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg', 362 + schedule_id: course.value.id,
383 - content: newComment.value, 363 + note: newComment.value
384 - time: new Date().toLocaleString(), 364 + });
385 - likes: 0, 365 +
386 - isLiked: false 366 + if (code) {
387 - }); 367 + // 刷新评论列表
388 - 368 + const comment = await getGroupCommentListAPI({
389 - newComment.value = ''; 369 + group_id: course.value.group_id,
370 + schedule_id: course.value.id,
371 + });
372 + if (comment.code) {
373 + commentList.value = comment.data.comment_list;
374 + commentCount.value = comment.data.comment_count;
375 + }
376 + newComment.value = '';
377 + }
378 + } catch (error) {
379 + console.error('提交评论失败:', error);
380 + }
390 }; 381 };
391 382
392 // 处理标签页切换 383 // 处理标签页切换
...@@ -404,28 +395,74 @@ const handleTabChange = (name) => { ...@@ -404,28 +395,74 @@ const handleTabChange = (name) => {
404 }; 395 };
405 396
406 397
398 +// 加载更多弹框评论
399 +const onPopupLoad = async () => {
400 + const nextPage = popupPage.value;
401 + try {
402 + const res = await getGroupCommentListAPI({
403 + group_id: course.value.group_id,
404 + schedule_id: course.value.id,
405 + limit: popupLimit.value,
406 + page: nextPage
407 + });
408 + if (res.code) {
409 + // 使用Set进行去重处理
410 + const newComments = res.data.comment_list;
411 + const existingIds = new Set(popupCommentList.value.map(comment => comment.id));
412 + const uniqueNewComments = newComments.filter(comment => !existingIds.has(comment.id));
413 + popupCommentList.value = [...popupCommentList.value, ...uniqueNewComments];
414 + popupFinished.value = res.data.comment_list.length < popupLimit.value;
415 + popupPage.value = nextPage + 1;
416 + }
417 + } catch (error) {
418 + console.error('加载评论失败:', error);
419 + }
420 + popupLoading.value = false;
421 +};
422 +
407 // 在组件卸载时移除滚动监听 423 // 在组件卸载时移除滚动监听
408 onUnmounted(() => { 424 onUnmounted(() => {
409 window.removeEventListener('scroll', handleScroll); 425 window.removeEventListener('scroll', handleScroll);
410 }); 426 });
411 427
412 // 提交弹窗中的评论 428 // 提交弹窗中的评论
413 -const submitPopupComment = () => { 429 +const submitPopupComment = async () => {
414 if (!popupComment.value.trim()) return; 430 if (!popupComment.value.trim()) return;
415 431
416 - comments.value.unshift({ 432 + try {
417 - id: Date.now(), 433 + const { code, data } = await addGroupCommentAPI({
418 - username: '当前用户', 434 + group_id: course.value.group_id,
419 - avatar: 'https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg', 435 + schedule_id: course.value.id,
420 - content: popupComment.value, 436 + note: popupComment.value
421 - time: new Date().toLocaleString(), 437 + });
422 - likes: 0, 438 +
423 - isLiked: false 439 + if (code) {
424 - }); 440 + // 重置弹框评论列表并重新加载
425 - 441 + popupCommentList.value = [];
426 - popupComment.value = ''; 442 + popupPage.value = 0;
427 - showCommentPopup.value = false; 443 + popupFinished.value = false;
444 + await onPopupLoad();
445 +
446 + // 更新评论数量和清空输入
447 + commentCount.value = data.comment_count;
448 + popupComment.value = '';
449 + }
450 + } catch (error) {
451 + console.error('提交评论失败:', error);
452 + }
428 }; 453 };
454 +// 监听弹窗显示状态变化
455 +watch(showCommentPopup, (newVal) => {
456 + if (newVal) {
457 + // 打开弹窗时重置状态
458 + popupCommentList.value = [];
459 + popupPage.value = 0;
460 + popupFinished.value = false;
461 + popupLoading.value = true;
462 + // 加载第一页数据
463 + onPopupLoad();
464 + }
465 +});
429 </script> 466 </script>
430 467
431 <style lang="less" scoped> 468 <style lang="less" scoped>
......