feat(课程评论): 实现课程评论的点赞、取消点赞及分页加载功能
添加了课程评论的点赞和取消点赞功能,并优化了评论列表的分页加载逻辑。同时,修复了评论提交后的列表刷新问题,确保数据一致性。
Showing
3 changed files
with
234 additions
and
179 deletions
| 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> | 73 | + <span class="text-sm text-gray-500">{{ comment.like_count }}</span> |
| 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 | | 109 | |
| 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> | ... | ... |
-
Please register or login to post a comment