From 9201cc44e526c13fa4d1bec8ff61a004d69ac3d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E5=BC=A0?= <2091066548@qq.com> Date: Tue, 19 Aug 2025 01:50:27 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat=EF=BC=9A=E4=B8=AA=E4=BA=BA=E4=B8=AD?= =?UTF-8?q?=E5=BF=83=E5=AF=B9=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/index.ts | 1 + src/api/modules/auth.ts | 75 ++++++- src/api/modules/course.ts | 84 ++++++++ src/api/types.ts | 79 +++++++ src/components/auth/LoginModal.vue | 37 +++- src/components/layout/AppHeader.vue | 20 +- src/stores/user.ts | 65 ++++-- src/views/CourseDetail.vue | 213 +++++++++++++++---- src/views/CourseDetailEnrolled.vue | 316 ++++++++++++++++++++++------ src/views/Courses.vue | 35 ++- src/views/Login.vue | 38 +++- src/views/Profile.vue | 6 +- 12 files changed, 830 insertions(+), 139 deletions(-) diff --git a/src/api/index.ts b/src/api/index.ts index 41502a8..d030f0f 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -19,6 +19,7 @@ export const API_ENDPOINTS = { // 认证相关 AUTH: { LOGIN: '/biz/user/login', + USER_INFO: '/biz/user/info', REGISTER: '/auth/register', LOGOUT: '/auth/logout', REFRESH: '/auth/refresh', diff --git a/src/api/modules/auth.ts b/src/api/modules/auth.ts index 8c16a8b..b6b9a03 100644 --- a/src/api/modules/auth.ts +++ b/src/api/modules/auth.ts @@ -8,6 +8,8 @@ import type { // BackendLoginResponse, RegisterRequest, UserProfile, + BackendUserInfo, + UserInfoResponse, } from '../types' /** @@ -227,11 +229,82 @@ export class AuthApi { return ApiRequest.post('/auth/refresh', { refreshToken }) } - // 获取当前用户信息 + // 获取当前用户信息 - 使用后端实际接口 + static async getUserInfo(): Promise { + const response = await ApiRequest.get('/biz/user/info') + + console.log('🔍 getUserInfo - 原始响应:', response) + + // 后端返回的格式是 { success, message, code, result: BackendUserInfo, timestamp } + // 而不是标准的 ApiResponse 格式 + if (response.data && response.data.result) { + return { + success: response.data.success || (response.data.code === 200 || response.data.code === 0), + message: response.data.message || '', + code: response.data.code, + result: response.data.result, + timestamp: response.data.timestamp || Date.now() + } + } else { + // 如果是标准ApiResponse格式,直接转换 + return { + success: response.code === 200 || response.code === 0, + message: response.message || '', + code: response.code, + result: response.data, + timestamp: Date.now() + } + } + } + + // 获取当前用户信息 - 兼容旧接口 static getCurrentUser(): Promise> { return ApiRequest.get('/users/info') } + // 将后端用户信息转换为前端User格式 + static convertBackendUserToUser(backendUser: BackendUserInfo): User { + const { baseInfo, roles, extendedInfo } = backendUser + + // 转换性别 + let gender: 'male' | 'female' | 'other' = 'other' + if (baseInfo.sex === 1) gender = 'male' + else if (baseInfo.sex === 2) gender = 'female' + + // 转换状态 (1表示正常/激活,0表示禁用) + const status = baseInfo.status === 1 ? 'active' : 'inactive' + + // 确定角色 + let role: 'student' | 'teacher' | 'admin' = 'student' + if (roles.includes('admin') || roles.includes('ADMIN')) { + role = 'admin' + } else if (roles.includes('teacher') || roles.includes('TEACHER')) { + role = 'teacher' + } + + return { + id: parseInt(baseInfo.id) || 0, + username: baseInfo.username, + email: baseInfo.email, + phone: baseInfo.phone, + nickname: baseInfo.realname || baseInfo.username, + avatar: baseInfo.avatar, + role, + status: status as 'active' | 'inactive' | 'banned', + createdAt: new Date().toISOString(), // 后端没有提供,使用当前时间 + updatedAt: new Date().toISOString(), // 后端没有提供,使用当前时间 + profile: { + realName: baseInfo.realname, + gender, + birthday: baseInfo.birthday, + bio: extendedInfo.tag, + location: extendedInfo.college, + website: '', + socialLinks: {} + } + } + } + // 更新用户资料 static updateProfile(data: Partial): Promise> { return ApiRequest.put('/auth/profile', data) diff --git a/src/api/modules/course.ts b/src/api/modules/course.ts index efc2396..c226973 100644 --- a/src/api/modules/course.ts +++ b/src/api/modules/course.ts @@ -17,8 +17,10 @@ import type { BackendCourseSection, BackendInstructor, BackendSectionVideo, + BackendComment, SectionVideo, VideoQuality, + CourseComment, Quiz, LearningProgress, SearchRequest, @@ -118,6 +120,7 @@ export class CourseApi { title: item.name || '', description: item.description || '', instructor: item.school || '未知讲师', + teacherList: item.teacherList || [], // 新增:传递讲师列表 duration: item.arrangement || '待定', level: this.mapDifficultyToLevel(item.difficulty), category: item.subject || '其他', @@ -948,6 +951,87 @@ export class CourseApi { return qualities } + // 获取课程评论列表 + static async getCourseComments(courseId: string): Promise> { + try { + console.log('🔍 获取课程评论数据,课程ID:', courseId) + console.log('🔍 API请求URL: /biz/comment/course/' + courseId + '/list') + + const response = await ApiRequest.get(`/biz/comment/course/${courseId}/list`) + console.log('🔍 评论API响应:', response) + + // 处理后端响应格式 + if (response.data && response.data.success && response.data.result) { + console.log('✅ 响应状态码:', response.data.code) + console.log('✅ 响应消息:', response.data.message) + console.log('✅ 原始评论数据:', response.data.result) + console.log('✅ 评论数据数量:', response.data.result.length || 0) + + // 适配数据格式 + const adaptedComments: CourseComment[] = response.data.result.map((comment: BackendComment) => ({ + id: comment.id, + userId: comment.userId, + userName: comment.userName || '匿名用户', + userAvatar: comment.userAvatar || '', + userTag: comment.userTag || '', + content: comment.content || '', + images: comment.imgs ? comment.imgs.split(',').filter(img => img.trim()) : [], // 图片URL逗号分隔 + isTop: comment.izTop === 1, // 1=置顶,0=普通 + likeCount: comment.likeCount || 0, + createTime: comment.createTime || '', + timeAgo: this.formatTimeAgo(comment.createTime) // 计算相对时间 + })) + + console.log('✅ 适配后的评论数据:', adaptedComments) + + return { + code: response.data.code, + message: response.data.message, + data: adaptedComments + } + } else { + console.warn('⚠️ API返回的数据结构不正确:', response.data) + return { + code: 500, + message: '数据格式错误', + data: [] + } + } + } catch (error) { + console.error('❌ 评论API调用失败:', error) + throw error + } + } + + // 格式化时间为相对时间显示 + private static formatTimeAgo(createTime: string): string { + if (!createTime) return '未知时间' + + try { + const now = new Date() + const commentTime = new Date(createTime) + const diffMs = now.getTime() - commentTime.getTime() + + const diffMinutes = Math.floor(diffMs / (1000 * 60)) + const diffHours = Math.floor(diffMs / (1000 * 60 * 60)) + const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)) + const diffWeeks = Math.floor(diffDays / 7) + const diffMonths = Math.floor(diffDays / 30) + + if (diffMinutes < 1) return '刚刚' + if (diffMinutes < 60) return `${diffMinutes}分钟前` + if (diffHours < 24) return `${diffHours}小时前` + if (diffDays < 7) return `${diffDays}天前` + if (diffWeeks < 4) return `${diffWeeks}周前` + if (diffMonths < 12) return `${diffMonths}个月前` + + return commentTime.toLocaleDateString() + } catch (error) { + console.warn('时间格式化失败:', error) + return createTime + } + } + } export default CourseApi diff --git a/src/api/types.ts b/src/api/types.ts index 7f4de62..97ad735 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -46,6 +46,39 @@ export interface UserProfile { } } +// 后端用户信息接口返回的数据结构 +export interface BackendUserInfo { + baseInfo: { + id: string + username: string + realname: string + avatar: string + phone: string + email: string + sex: number // 0: 未知, 1: 男, 2: 女 + birthday: string + status: number // 0: 正常, 1: 禁用 + } + roles: string[] + extendedInfo: { + major: string + college: string + education: string + title: string + tag: string + sortOrder: number + } +} + +// 用户信息接口响应类型 +export interface UserInfoResponse { + success: boolean + message: string + code: number + result: BackendUserInfo + timestamp: number +} + // 登录注册类型 export interface LoginRequest { email?: string @@ -107,6 +140,7 @@ export interface Course { requirements: string[] objectives: string[] instructor: Instructor + teacherList?: BackendInstructor[] // 新增讲师列表字段(从后端适配) status: 'draft' | 'published' | 'archived' isEnrolled?: boolean progress?: number @@ -279,6 +313,7 @@ export interface BackendCourseItem { createTime: string updateBy: string updateTime: string + teacherList: BackendInstructor[] // 新增讲师列表字段 } // 后端课程列表响应格式 @@ -415,6 +450,7 @@ export interface BackendInstructor { avatar: string title: string tag: string + sortOrder?: number // 排序字段,用于多讲师排序 } // 后端讲师列表响应格式 @@ -475,6 +511,49 @@ export interface SectionVideo { currentQuality: string // 当前选中的质量 } +// 后端评论数据结构 +export interface BackendComment { + id: string + userId: string + targetType: string + targetId: string + content: string + imgs: string + izTop: number // 是否置顶:0=否,1=是 + likeCount: number + createBy: string + createTime: string + updateBy: string + updateTime: string + userName: string + userAvatar: string + userTag: string +} + +// 后端评论列表响应格式 +export interface BackendCommentListResponse { + success: boolean + message: string + code: number + result: BackendComment[] + timestamp: number +} + +// 前端评论类型 +export interface CourseComment { + id: string + userId: string + userName: string + userAvatar: string + userTag: string + content: string + images: string[] // 评论图片列表 + isTop: boolean // 是否置顶 + likeCount: number + createTime: string + timeAgo: string // 相对时间显示(如"2天前") +} + // 前端章节列表响应格式 export interface CourseSectionListResponse { list: CourseSection[] diff --git a/src/components/auth/LoginModal.vue b/src/components/auth/LoginModal.vue index e0a91e4..43ef76d 100644 --- a/src/components/auth/LoginModal.vue +++ b/src/components/auth/LoginModal.vue @@ -121,21 +121,48 @@ const handleLogin = async () => { if (response.code === 200 || response.code === 0) { const { user, token, refreshToken } = response.data - // 保存用户信息和token到store - userStore.user = user + // 保存token到store和本地存储 userStore.token = token - - // 保存到本地存储 localStorage.setItem('X-Access-Token', token) localStorage.setItem('token', token) localStorage.setItem('refreshToken', refreshToken || '') - localStorage.setItem('user', JSON.stringify(user)) // 如果选择了记住我,设置更长的过期时间 if (loginForm.remember) { localStorage.setItem('rememberMe', 'true') } + try { + // 登录成功后立即调用用户信息接口获取完整的用户信息 + console.log('🔍 登录成功,正在获取用户信息...') + const userInfoResponse = await AuthApi.getUserInfo() + + if (userInfoResponse.success && userInfoResponse.result) { + // 将后端用户信息转换为前端格式 + const convertedUser = AuthApi.convertBackendUserToUser(userInfoResponse.result) + + console.log('🔍 转换后的用户信息:', convertedUser) + console.log('🔍 用户真实姓名:', convertedUser.profile?.realName) + console.log('🔍 用户头像:', convertedUser.avatar) + + // 保存转换后的用户信息 + userStore.user = convertedUser + localStorage.setItem('user', JSON.stringify(convertedUser)) + + console.log('✅ 用户信息获取成功并保存到store:', userStore.user) + } else { + // 如果获取用户信息失败,使用登录接口返回的基本用户信息 + console.warn('⚠️ 获取用户信息失败,使用登录返回的基本信息') + userStore.user = user + localStorage.setItem('user', JSON.stringify(user)) + } + } catch (userInfoError) { + // 如果获取用户信息失败,使用登录接口返回的基本用户信息 + console.warn('⚠️ 获取用户信息异常,使用登录返回的基本信息:', userInfoError) + userStore.user = user + localStorage.setItem('user', JSON.stringify(user)) + } + message.success('登录成功!') emit('success') closeModal() diff --git a/src/components/layout/AppHeader.vue b/src/components/layout/AppHeader.vue index 80d77fb..eef0459 100644 --- a/src/components/layout/AppHeader.vue +++ b/src/components/layout/AppHeader.vue @@ -101,8 +101,8 @@
@@ -120,7 +120,7 @@ @@ -3289,4 +3352,68 @@ onMounted(() => { gap: 8px; } } + +/* 评论加载和错误状态 */ +.comments-loading, .comments-error, .no-comments { + padding: 40px 20px; + text-align: center; + color: #666; +} + +.comments-error { + color: #ff4d4f; +} + +.comments-error .retry-btn { + margin-top: 10px; + padding: 8px 16px; + background: #1890ff; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 14px; +} + +.comments-error .retry-btn:hover { + background: #40a9ff; +} + +/* 评论用户标签 */ +.comment-tag { + display: inline-block; + padding: 2px 6px; + background: #f0f0f0; + color: #666; + font-size: 10px; + border-radius: 2px; + margin-left: 8px; +} + +/* 置顶评论样式 */ +.top-comment { + background: #fff7e6 !important; + color: #fa8c16 !important; + border: 1px solid #ffd591 !important; +} + +.top-comment .top { + font-weight: 500; +} + +/* 点赞按钮 */ +.like-btn { + display: flex; + align-items: center; + gap: 4px; +} + +.like-icon { + color: #999; + transition: color 0.2s; +} + +.like-btn:hover .like-icon { + color: #ff4d4f; +} \ No newline at end of file diff --git a/src/views/CourseDetailEnrolled.vue b/src/views/CourseDetailEnrolled.vue index 5463a22..802d04a 100644 --- a/src/views/CourseDetailEnrolled.vue +++ b/src/views/CourseDetailEnrolled.vue @@ -148,7 +148,15 @@

讲师

-
+ +
+

正在加载讲师信息...

+
+
+

{{ instructorsError }}

+ +
+
@@ -170,7 +178,7 @@ + @click="activeTab = 'comments'">评论({{ comments.length }})
@@ -195,25 +203,58 @@
-->
-
-
- -
-
-
- {{ comment.username }} - {{ comment.time }} + +
+

正在加载评论...

+
+ + +
+

{{ commentsError }}

+ +
+ + +
+
+
+
-
{{ comment.content }}
-
- - +
+
+ {{ comment.userName }} + {{ comment.userTag }} + {{ comment.timeAgo }} +
+
{{ comment.content }}
+ + +
+ +
+ +
+ + + +
+ + +
+

暂无评论,快来发表第一条评论吧!

+
- - {{ userStore.user?.username || '用户名' }} + {{ userStore.user?.profile?.realName || userStore.user?.nickname || userStore.user?.username || '用户名' }}
@@ -847,7 +847,7 @@
Date: Tue, 19 Aug 2025 02:06:33 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix=EF=BC=9A=E8=AF=84=E8=AE=BA=E8=B0=83?= =?UTF-8?q?=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/CourseDetail.vue | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/views/CourseDetail.vue b/src/views/CourseDetail.vue index fc2fd78..a298754 100644 --- a/src/views/CourseDetail.vue +++ b/src/views/CourseDetail.vue @@ -660,6 +660,11 @@ const sectionsError = ref('') const instructorsLoading = ref(false) const instructorsError = ref('') +// 评论数据 +const comments = ref([]) +const commentsLoading = ref(false) +const commentsError = ref('') + // 报名状态管理 const isEnrolled = ref(false) // 用户是否已报名该课程 const enrollmentLoading = ref(false) // 报名加载状态 From b37cdd3ccc04b7bca2593938e7bb83c72692e732 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E5=BC=A0?= <2091066548@qq.com> Date: Tue, 19 Aug 2025 02:21:57 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix=EF=BC=9A=E6=89=93=E5=8C=85=E6=8A=A5?= =?UTF-8?q?=E9=94=99=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/CourseDetail.vue | 62 ++++++++++++++++++++++++------ src/views/CourseDetailEnrolled.vue | 26 ++++++++++++- 2 files changed, 76 insertions(+), 12 deletions(-) diff --git a/src/views/CourseDetail.vue b/src/views/CourseDetail.vue index a298754..e7b0214 100644 --- a/src/views/CourseDetail.vue +++ b/src/views/CourseDetail.vue @@ -237,7 +237,7 @@ {{ comment.likeCount }} - +
@@ -271,7 +271,7 @@
-
+
@@ -288,7 +288,7 @@
@@ -310,7 +310,7 @@
@@ -347,11 +347,11 @@ 笔记
2025.07.23 16:28 - +
-
+
回复 @{{ replyToUsername }} @@ -661,7 +661,7 @@ const instructorsLoading = ref(false) const instructorsError = ref('') // 评论数据 -const comments = ref([]) +const comments = ref([]) const commentsLoading = ref(false) const commentsError = ref('') @@ -701,6 +701,11 @@ const isUserEnrolled = computed(() => { const enrollConfirmVisible = ref(false) const enrollSuccessVisible = ref(false) +// 评论回复相关状态 +const replyingTo = ref(null) +const replyToUsername = ref('') +const replyText = ref('') + // 章节分组数据 interface ChapterGroup { title: string @@ -1025,6 +1030,45 @@ const loadCourseComments = async () => { } } +// 评论相关函数 +const startReply = (commentId: string, username: string) => { + replyingTo.value = commentId + replyToUsername.value = username + replyText.value = '' +} + +const cancelReply = () => { + replyingTo.value = null + replyToUsername.value = '' + replyText.value = '' +} + +const submitReply = () => { + if (!replyText.value.trim()) return + + console.log('提交回复:', { + commentId: replyingTo.value, + username: replyToUsername.value, + content: replyText.value + }) + + // 这里可以调用API提交回复 + // 提交成功后清空状态 + cancelReply() +} + +const adjustReplyTextareaHeight = (event: Event) => { + const textarea = event.target as HTMLTextAreaElement + textarea.style.height = 'auto' + textarea.style.height = textarea.scrollHeight + 'px' +} + +const handleReplyTextareaClick = () => { + if (!userStore.isLoggedIn) { + showLoginModal() + } +} + // 切换章节展开/折叠 const toggleChapter = (chapterIndex: number) => { console.log('点击切换章节,章节索引:', chapterIndex) @@ -3243,16 +3287,12 @@ onMounted(() => { } /* 讲师评论特殊样式 */ -.reply-item.instructor-reply {} - .reply-item.instructor-reply .reply-username { color: #666666; font-weight: 500; } /* 用户评论样式 */ -.reply-item.user-reply {} - .reply-item.user-reply .reply-username { color: #666666; font-weight: 500; diff --git a/src/views/CourseDetailEnrolled.vue b/src/views/CourseDetailEnrolled.vue index 2ba1657..ba14338 100644 --- a/src/views/CourseDetailEnrolled.vue +++ b/src/views/CourseDetailEnrolled.vue @@ -450,7 +450,7 @@ import { ref, onMounted, onUnmounted, computed, nextTick } from 'vue' import { useRoute, useRouter } from 'vue-router' import { useUserStore } from '@/stores/user' import { CourseApi } from '@/api/modules/course' -import type { Course, CourseSection, SectionVideo, VideoQuality } from '@/api/types' +import type { Course, CourseSection, SectionVideo, VideoQuality, CourseComment, Instructor } from '@/api/types' import SafeAvatar from '@/components/common/SafeAvatar.vue' import LearningProgressStats from '@/components/common/LearningProgressStats.vue' import NotesModal from '@/components/common/NotesModal.vue' @@ -495,6 +495,9 @@ const instructors = ref([]) const instructorsLoading = ref(false) const instructorsError = ref('') +// 评论输入相关状态 +const newComment = ref('') + // 视频源配置 const VIDEO_CONFIG = { // 本地视频(当前使用) @@ -1247,6 +1250,27 @@ const saveNote = (content: string) => { // 这里可以添加保存笔记到服务器的逻辑 } +// 评论相关函数 +const adjustTextareaHeight = (event: Event) => { + const textarea = event.target as HTMLTextAreaElement + textarea.style.height = 'auto' + textarea.style.height = textarea.scrollHeight + 'px' +} + +const handleTextareaClick = () => { + // 处理评论输入框点击事件 + console.log('评论输入框被点击') +} + +const submitComment = () => { + if (!newComment.value.trim()) return + + console.log('提交评论:', newComment.value) + // 这里可以调用API提交评论 + // 提交成功后清空输入框 + newComment.value = '' +} + onMounted(async () => { console.log('已报名课程详情页加载完成,课程ID:', courseId.value) initializeEnrolledState() // 初始化已报名状态