feat:个人中心对接
This commit is contained in:
parent
6e1ad5ea07
commit
9201cc44e5
@ -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',
|
||||
|
@ -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<UserInfoResponse> {
|
||||
const response = await ApiRequest.get<any>('/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<ApiResponse<User>> {
|
||||
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<UserProfile>): Promise<ApiResponse<User>> {
|
||||
return ApiRequest.put('/auth/profile', data)
|
||||
|
@ -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<ApiResponse<CourseComment[]>> {
|
||||
try {
|
||||
console.log('🔍 获取课程评论数据,课程ID:', courseId)
|
||||
console.log('🔍 API请求URL: /biz/comment/course/' + courseId + '/list')
|
||||
|
||||
const response = await ApiRequest.get<any>(`/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
|
||||
|
@ -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[]
|
||||
|
@ -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()
|
||||
|
@ -101,8 +101,8 @@
|
||||
<div v-else class="user-menu">
|
||||
<n-dropdown :options="userMenuOptions" @select="handleUserMenuSelect">
|
||||
<div class="user-info">
|
||||
<SafeAvatar :src="userStore.user?.avatar" :name="userStore.user?.username" :size="32" />
|
||||
<span class="username">{{ userStore.user?.username }}</span>
|
||||
<SafeAvatar :src="userStore.user?.avatar" :name="userStore.user?.profile?.realName || userStore.user?.nickname || userStore.user?.username" :size="32" />
|
||||
<span class="username">{{ userStore.user?.profile?.realName || userStore.user?.nickname || userStore.user?.username }}</span>
|
||||
</div>
|
||||
</n-dropdown>
|
||||
</div>
|
||||
@ -120,7 +120,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, h, onMounted, onUnmounted } from 'vue'
|
||||
import { ref, computed, h, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
@ -140,6 +140,20 @@ const router = useRouter()
|
||||
const { t, locale } = useI18n()
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 调试用户信息
|
||||
console.log('🔍 AppHeader - 当前用户信息:', userStore.user)
|
||||
console.log('🔍 AppHeader - 用户真实姓名:', userStore.user?.profile?.realName)
|
||||
console.log('🔍 AppHeader - 用户头像:', userStore.user?.avatar)
|
||||
|
||||
// 监听用户信息变化
|
||||
watch(() => userStore.user, (newUser) => {
|
||||
console.log('🔄 AppHeader - 用户信息已更新:', newUser)
|
||||
if (newUser) {
|
||||
console.log('🔄 AppHeader - 新的真实姓名:', newUser.profile?.realName)
|
||||
console.log('🔄 AppHeader - 新的头像:', newUser.avatar)
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
|
||||
// 移动端菜单状态
|
||||
const mobileMenuOpen = ref(false)
|
||||
|
@ -58,27 +58,60 @@ export const useUserStore = defineStore('user', () => {
|
||||
}
|
||||
|
||||
// 获取当前用户信息
|
||||
const getCurrentUser = async () => {
|
||||
const getCurrentUser = async (forceRefresh = false) => {
|
||||
if (!token.value) {
|
||||
return { success: false, message: '未登录' }
|
||||
}
|
||||
|
||||
// 如果已经有用户信息,直接返回成功
|
||||
if (user.value) {
|
||||
// 如果已经有用户信息且不强制刷新,直接返回成功
|
||||
if (user.value && !forceRefresh) {
|
||||
return { success: true, message: '用户信息已存在' }
|
||||
}
|
||||
|
||||
// 尝试从localStorage恢复用户信息
|
||||
const savedUser = localStorage.getItem('user')
|
||||
if (savedUser) {
|
||||
try {
|
||||
user.value = JSON.parse(savedUser)
|
||||
return { success: true, message: '用户信息已恢复' }
|
||||
} catch (error) {
|
||||
console.error('解析用户信息失败:', error)
|
||||
// 如果不强制刷新,尝试从localStorage恢复用户信息
|
||||
if (!forceRefresh) {
|
||||
const savedUser = localStorage.getItem('user')
|
||||
if (savedUser) {
|
||||
try {
|
||||
user.value = JSON.parse(savedUser)
|
||||
return { success: true, message: '用户信息已恢复' }
|
||||
} catch (error) {
|
||||
console.error('解析用户信息失败:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试从服务器获取用户信息
|
||||
try {
|
||||
console.log('🔍 从服务器获取用户信息...')
|
||||
const userInfoResponse = await AuthApi.getUserInfo()
|
||||
|
||||
if (userInfoResponse.success && userInfoResponse.result) {
|
||||
// 将后端用户信息转换为前端格式
|
||||
const convertedUser = AuthApi.convertBackendUserToUser(userInfoResponse.result)
|
||||
|
||||
// 保存转换后的用户信息
|
||||
user.value = convertedUser
|
||||
localStorage.setItem('user', JSON.stringify(convertedUser))
|
||||
|
||||
console.log('✅ 从服务器获取用户信息成功:', convertedUser)
|
||||
return { success: true, message: '用户信息获取成功' }
|
||||
} else {
|
||||
console.warn('⚠️ 服务器返回的用户信息格式不正确')
|
||||
return { success: false, message: '用户信息格式错误' }
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('❌ 获取用户信息失败:', error)
|
||||
|
||||
// 如果是401错误,说明token已过期
|
||||
if (error.response?.status === 401) {
|
||||
await logout()
|
||||
return { success: false, message: '登录已过期,请重新登录' }
|
||||
}
|
||||
|
||||
return { success: false, message: '获取用户信息失败' }
|
||||
}
|
||||
|
||||
// 暂时注释掉API调用,因为后端可能没有这个接口
|
||||
// isLoading.value = true
|
||||
// try {
|
||||
@ -108,6 +141,11 @@ export const useUserStore = defineStore('user', () => {
|
||||
return { success: false, message: '无法获取用户信息' }
|
||||
}
|
||||
|
||||
// 强制刷新用户信息
|
||||
const refreshUserInfo = async () => {
|
||||
return await getCurrentUser(true)
|
||||
}
|
||||
|
||||
const updateProfile = async (profileData: any) => {
|
||||
isLoading.value = true
|
||||
try {
|
||||
@ -143,8 +181,8 @@ export const useUserStore = defineStore('user', () => {
|
||||
user.value = JSON.parse(savedUser)
|
||||
token.value = savedToken
|
||||
|
||||
// 验证token是否仍然有效
|
||||
await getCurrentUser()
|
||||
// 验证token是否仍然有效,并强制刷新用户信息
|
||||
await getCurrentUser(true)
|
||||
} catch (error) {
|
||||
console.error('Failed to parse saved user data or token expired:', error)
|
||||
await logout()
|
||||
@ -169,6 +207,7 @@ export const useUserStore = defineStore('user', () => {
|
||||
register,
|
||||
logout,
|
||||
getCurrentUser,
|
||||
refreshUserInfo,
|
||||
updateProfile,
|
||||
initializeAuth
|
||||
}
|
||||
|
@ -170,7 +170,7 @@
|
||||
<button class="tab-btn" :class="{ active: activeTab === 'intro' }"
|
||||
@click="activeTab = 'intro'">课程介绍</button>
|
||||
<button class="tab-btn" :class="{ active: activeTab === 'comments' }"
|
||||
@click="activeTab = 'comments'">评论(1251)</button>
|
||||
@click="activeTab = 'comments'">评论({{ comments.length }})</button>
|
||||
</div>
|
||||
|
||||
<!-- 标签页内容区域 -->
|
||||
@ -195,25 +195,58 @@
|
||||
</div> -->
|
||||
|
||||
<div class="comment-list">
|
||||
<div class="comment-item" v-for="comment in displayComments" :key="comment.id">
|
||||
<div class="comment-avatar">
|
||||
<img :src="comment.avatar" :alt="comment.username" />
|
||||
</div>
|
||||
<div class="comment-content">
|
||||
<div class="comment-header">
|
||||
<span class="comment-username">{{ comment.username }}</span>
|
||||
<!-- <span class="comment-time">{{ comment.time }}</span> -->
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="commentsLoading" class="comments-loading">
|
||||
<p>正在加载评论...</p>
|
||||
</div>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<div v-else-if="commentsError" class="comments-error">
|
||||
<p>{{ commentsError }}</p>
|
||||
<button @click="loadCourseComments" class="retry-btn">重试</button>
|
||||
</div>
|
||||
|
||||
<!-- 评论列表 -->
|
||||
<div v-else-if="comments.length > 0">
|
||||
<div class="comment-item" v-for="comment in comments" :key="comment.id">
|
||||
<div class="comment-avatar">
|
||||
<SafeAvatar :src="comment.userAvatar" :name="comment.userName" :size="40" />
|
||||
</div>
|
||||
<div class="comment-text">{{ comment.content }}</div>
|
||||
<div class="comment-actions">
|
||||
<button class="action-btn">
|
||||
<span class="top">置顶评论</span>
|
||||
<span>2025.07.23 16:28</span>
|
||||
</button>
|
||||
<button class="action-btn">回复</button>
|
||||
<div class="comment-content">
|
||||
<div class="comment-header">
|
||||
<span class="comment-username">{{ comment.userName }}</span>
|
||||
<span v-if="comment.userTag" class="comment-tag">{{ comment.userTag }}</span>
|
||||
<span class="comment-time">{{ comment.timeAgo }}</span>
|
||||
</div>
|
||||
<div class="comment-text">{{ comment.content }}</div>
|
||||
|
||||
<!-- 评论图片 -->
|
||||
<div v-if="comment.images.length > 0" class="comment-images">
|
||||
<img v-for="(image, index) in comment.images" :key="index"
|
||||
:src="image" :alt="`评论图片${index + 1}`" />
|
||||
</div>
|
||||
|
||||
<div class="comment-actions">
|
||||
<button v-if="comment.isTop" class="action-btn top-comment">
|
||||
<span class="top">置顶评论</span>
|
||||
<span>{{ comment.createTime }}</span>
|
||||
</button>
|
||||
<button class="action-btn like-btn">
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" class="like-icon">
|
||||
<path d="M7 12.5L6.125 11.75C3.5 9.375 1.75 7.75 1.75 5.75C1.75 4.25 2.875 3.125 4.375 3.125C5.25 3.125 6.125 3.5 7 4.25C7.875 3.5 8.75 3.125 9.625 3.125C11.125 3.125 12.25 4.25 12.25 5.75C12.25 7.75 10.5 9.375 7.875 11.75L7 12.5Z" fill="currentColor"/>
|
||||
</svg>
|
||||
{{ comment.likeCount }}
|
||||
</button>
|
||||
<button class="action-btn">回复</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 无评论状态 -->
|
||||
<div v-else class="no-comments">
|
||||
<p>暂无评论,快来发表第一条评论吧!</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="load-more">
|
||||
@ -463,7 +496,7 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { CourseApi } from '@/api/modules/course'
|
||||
import type { Course, CourseSection } from '@/api/types'
|
||||
import type { Course, CourseSection, CourseComment } from '@/api/types'
|
||||
import SafeAvatar from '@/components/common/SafeAvatar.vue'
|
||||
import LoginModal from '@/components/auth/LoginModal.vue'
|
||||
import RegisterModal from '@/components/auth/RegisterModal.vue'
|
||||
@ -492,6 +525,11 @@ const sectionsError = ref('')
|
||||
const instructorsLoading = ref(false)
|
||||
const instructorsError = ref('')
|
||||
|
||||
// 评论相关状态
|
||||
const comments = ref<CourseComment[]>([])
|
||||
const commentsLoading = ref(false)
|
||||
const commentsError = ref('')
|
||||
|
||||
// 报名状态管理
|
||||
const isEnrolled = ref(false) // 用户是否已报名该课程
|
||||
const enrollmentLoading = ref(false) // 报名加载状态
|
||||
@ -662,32 +700,8 @@ const formatTotalDuration = () => {
|
||||
return `${hours}小时${minutes}分钟`
|
||||
}
|
||||
|
||||
const displayComments = ref([
|
||||
{
|
||||
id: 1,
|
||||
username: '学习者小王',
|
||||
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-4.0.3&auto=format&fit=crop&w=50&q=80',
|
||||
time: '2天前',
|
||||
content: '老师讲得很详细,从零基础到实际应用都有涉及,非常适合初学者!',
|
||||
likes: 23
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
username: 'AI爱好者',
|
||||
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-4.0.3&auto=format&fit=crop&w=50&q=80',
|
||||
time: '5天前',
|
||||
content: '课程内容很实用,跟着做了几个项目,收获很大。推荐给想学AI的朋友们!',
|
||||
likes: 18
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
username: '程序员小李',
|
||||
avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?ixlib=rb-4.0.3&auto=format&fit=crop&w=50&q=80',
|
||||
time: '1周前',
|
||||
content: 'DeepSeek确实是个很强大的工具,通过这个课程学会了很多实用技巧。',
|
||||
likes: 31
|
||||
}
|
||||
])
|
||||
// displayComments 现在使用真实的API数据 (comments状态)
|
||||
// const displayComments = ref([]) // 已替换为comments状态
|
||||
|
||||
// 加载课程详情
|
||||
const loadCourseDetail = async () => {
|
||||
@ -828,6 +842,54 @@ const loadCourseInstructors = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 加载课程评论列表
|
||||
const loadCourseComments = async () => {
|
||||
if (!courseId.value || courseId.value.trim() === '') {
|
||||
commentsError.value = '课程ID无效'
|
||||
console.error('课程ID无效:', courseId.value)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
commentsLoading.value = true
|
||||
commentsError.value = ''
|
||||
|
||||
console.log('调用API获取课程评论...')
|
||||
const response = await CourseApi.getCourseComments(courseId.value)
|
||||
console.log('评论API响应:', response)
|
||||
|
||||
if (response.code === 0 || response.code === 200) {
|
||||
if (response.data && Array.isArray(response.data)) {
|
||||
// 按置顶状态和时间排序:置顶评论在前,然后按时间倒序
|
||||
const sortedComments = response.data.sort((a, b) => {
|
||||
// 先按置顶状态排序
|
||||
if (a.isTop !== b.isTop) {
|
||||
return a.isTop ? -1 : 1 // 置顶的在前
|
||||
}
|
||||
// 再按创建时间倒序排序
|
||||
return new Date(b.createTime).getTime() - new Date(a.createTime).getTime()
|
||||
})
|
||||
|
||||
comments.value = sortedComments
|
||||
console.log('✅ 评论数据设置成功:', comments.value)
|
||||
} else {
|
||||
console.log('⚠️ API返回的评论数据为空')
|
||||
comments.value = []
|
||||
}
|
||||
} else {
|
||||
console.log('⚠️ API返回错误')
|
||||
commentsError.value = response.message || '获取评论失败'
|
||||
comments.value = []
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载课程评论失败:', err)
|
||||
commentsError.value = '获取评论失败'
|
||||
comments.value = []
|
||||
} finally {
|
||||
commentsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 切换章节展开/折叠
|
||||
const toggleChapter = (chapterIndex: number) => {
|
||||
console.log('点击切换章节,章节索引:', chapterIndex)
|
||||
@ -1143,6 +1205,7 @@ onMounted(() => {
|
||||
loadCourseDetail()
|
||||
loadCourseSections() // 启用章节接口调用
|
||||
loadCourseInstructors() // 启用讲师接口调用
|
||||
loadCourseComments() // 启用评论接口调用
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
</style>
|
@ -148,7 +148,15 @@
|
||||
<!-- 讲师信息 -->
|
||||
<div class="instructors-section">
|
||||
<h3 class="section-title">讲师</h3>
|
||||
<div class="instructors-list">
|
||||
|
||||
<div v-if="instructorsLoading" class="instructors-loading">
|
||||
<p>正在加载讲师信息...</p>
|
||||
</div>
|
||||
<div v-else-if="instructorsError" class="instructors-error">
|
||||
<p>{{ instructorsError }}</p>
|
||||
<button @click="loadCourseInstructors" class="retry-btn">重试</button>
|
||||
</div>
|
||||
<div v-else class="instructors-list">
|
||||
<div class="instructor-item" v-for="instructor in instructors" :key="instructor.id">
|
||||
<div class="instructor-avatar">
|
||||
<SafeAvatar :src="instructor.avatar" :name="instructor.name" :size="50" />
|
||||
@ -170,7 +178,7 @@
|
||||
<button class="tab-btn" :class="{ active: activeTab === 'intro' }"
|
||||
@click="activeTab = 'intro'">课程介绍</button>
|
||||
<button class="tab-btn" :class="{ active: activeTab === 'comments' }"
|
||||
@click="activeTab = 'comments'">评论(1251)</button>
|
||||
@click="activeTab = 'comments'">评论({{ comments.length }})</button>
|
||||
</div>
|
||||
|
||||
<!-- 标签页内容区域 -->
|
||||
@ -195,25 +203,58 @@
|
||||
</div> -->
|
||||
|
||||
<div class="comment-list">
|
||||
<div class="comment-item" v-for="comment in displayComments" :key="comment.id">
|
||||
<div class="comment-avatar">
|
||||
<img :src="comment.avatar" :alt="comment.username" />
|
||||
</div>
|
||||
<div class="comment-content">
|
||||
<div class="comment-header">
|
||||
<span class="comment-username">{{ comment.username }}</span>
|
||||
<span class="comment-time">{{ comment.time }}</span>
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="commentsLoading" class="comments-loading">
|
||||
<p>正在加载评论...</p>
|
||||
</div>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<div v-else-if="commentsError" class="comments-error">
|
||||
<p>{{ commentsError }}</p>
|
||||
<button @click="loadCourseComments" class="retry-btn">重试</button>
|
||||
</div>
|
||||
|
||||
<!-- 评论列表 -->
|
||||
<div v-else-if="comments.length > 0">
|
||||
<div class="comment-item" v-for="comment in comments" :key="comment.id">
|
||||
<div class="comment-avatar">
|
||||
<SafeAvatar :src="comment.userAvatar" :name="comment.userName" :size="40" />
|
||||
</div>
|
||||
<div class="comment-text">{{ comment.content }}</div>
|
||||
<div class="comment-actions">
|
||||
<button class="action-btn">
|
||||
<span class="top">置顶评论</span>
|
||||
<span>2025.07.23 16:28</span>
|
||||
</button>
|
||||
<button class="action-btn">回复</button>
|
||||
<div class="comment-content">
|
||||
<div class="comment-header">
|
||||
<span class="comment-username">{{ comment.userName }}</span>
|
||||
<span v-if="comment.userTag" class="comment-tag">{{ comment.userTag }}</span>
|
||||
<span class="comment-time">{{ comment.timeAgo }}</span>
|
||||
</div>
|
||||
<div class="comment-text">{{ comment.content }}</div>
|
||||
|
||||
<!-- 评论图片 -->
|
||||
<div v-if="comment.images.length > 0" class="comment-images">
|
||||
<img v-for="(image, index) in comment.images" :key="index"
|
||||
:src="image" :alt="`评论图片${index + 1}`" />
|
||||
</div>
|
||||
|
||||
<div class="comment-actions">
|
||||
<button v-if="comment.isTop" class="action-btn top-comment">
|
||||
<span class="top">置顶评论</span>
|
||||
<span>{{ comment.createTime }}</span>
|
||||
</button>
|
||||
<button class="action-btn like-btn">
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" class="like-icon">
|
||||
<path d="M7 12.5L6.125 11.75C3.5 9.375 1.75 7.75 1.75 5.75C1.75 4.25 2.875 3.125 4.375 3.125C5.25 3.125 6.125 3.5 7 4.25C7.875 3.5 8.75 3.125 9.625 3.125C11.125 3.125 12.25 4.25 12.25 5.75C12.25 7.75 10.5 9.375 7.875 11.75L7 12.5Z" fill="currentColor"/>
|
||||
</svg>
|
||||
{{ comment.likeCount }}
|
||||
</button>
|
||||
<button class="action-btn">回复</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 无评论状态 -->
|
||||
<div v-else class="no-comments">
|
||||
<p>暂无评论,快来发表第一条评论吧!</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="load-more">
|
||||
@ -400,7 +441,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'
|
||||
@ -435,6 +476,16 @@ const currentQuality = ref<string>('360') // 默认360p
|
||||
const videoLoading = ref(false)
|
||||
const showQualityMenu = ref(false)
|
||||
|
||||
// 评论相关状态
|
||||
const comments = ref<CourseComment[]>([])
|
||||
const commentsLoading = ref(false)
|
||||
const commentsError = ref('')
|
||||
|
||||
// 讲师相关状态
|
||||
const instructors = ref<Instructor[]>([])
|
||||
const instructorsLoading = ref(false)
|
||||
const instructorsError = ref('')
|
||||
|
||||
// 视频源配置
|
||||
const VIDEO_CONFIG = {
|
||||
// 本地视频(当前使用)
|
||||
@ -487,27 +538,7 @@ const activeTab = ref('intro')
|
||||
// 笔记弹窗相关
|
||||
const showNotesModal = ref(false)
|
||||
|
||||
// 讲师数据
|
||||
const instructors = ref([
|
||||
{
|
||||
id: 1,
|
||||
name: '汪波',
|
||||
title: '教授',
|
||||
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-4.0.3&auto=format&fit=crop&w=60&q=80'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '汪波',
|
||||
title: '教授',
|
||||
avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?ixlib=rb-4.0.3&auto=format&fit=crop&w=60&q=80'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '汪波',
|
||||
title: '教授',
|
||||
avatar: 'https://images.unsplash.com/photo-1494790108755-2616b612b786?ixlib=rb-4.0.3&auto=format&fit=crop&w=60&q=80'
|
||||
}
|
||||
])
|
||||
// 讲师数据现在使用API数据 (instructors状态在上面已声明)
|
||||
|
||||
// 计算属性
|
||||
const totalLessons = computed(() => {
|
||||
@ -537,32 +568,8 @@ const formatTotalDuration = () => {
|
||||
return `${hours}小时${minutes}分钟`
|
||||
}
|
||||
|
||||
const displayComments = ref([
|
||||
{
|
||||
id: 1,
|
||||
username: '学习者小王',
|
||||
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-4.0.3&auto=format&fit=crop&w=50&q=80',
|
||||
time: '2天前',
|
||||
content: '老师讲得很详细,从零基础到实际应用都有涉及,非常适合初学者!',
|
||||
likes: 23
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
username: 'AI爱好者',
|
||||
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-4.0.3&auto=format&fit=crop&w=50&q=80',
|
||||
time: '5天前',
|
||||
content: '课程内容很实用,跟着做了几个项目,收获很大。推荐给想学AI的朋友们!',
|
||||
likes: 18
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
username: '程序员小李',
|
||||
avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?ixlib=rb-4.0.3&auto=format&fit=crop&w=50&q=80',
|
||||
time: '1周前',
|
||||
content: 'DeepSeek确实是个很强大的工具,通过这个课程学会了很多实用技巧。',
|
||||
likes: 31
|
||||
}
|
||||
])
|
||||
// displayComments 现在使用真实的API数据 (comments状态)
|
||||
// const displayComments = ref([]) // 已替换为comments状态
|
||||
|
||||
// 生成模拟章节数据(暂时禁用)
|
||||
const generateMockSections = (): CourseSection[] => {
|
||||
@ -721,6 +728,91 @@ const loadMockData = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 加载课程评论列表
|
||||
const loadCourseComments = async () => {
|
||||
if (!courseId.value || courseId.value.trim() === '') {
|
||||
commentsError.value = '课程ID无效'
|
||||
console.error('课程ID无效:', courseId.value)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
commentsLoading.value = true
|
||||
commentsError.value = ''
|
||||
|
||||
console.log('调用API获取课程评论...')
|
||||
const response = await CourseApi.getCourseComments(courseId.value)
|
||||
console.log('评论API响应:', response)
|
||||
|
||||
if (response.code === 0 || response.code === 200) {
|
||||
if (response.data && Array.isArray(response.data)) {
|
||||
// 按置顶状态和时间排序:置顶评论在前,然后按时间倒序
|
||||
const sortedComments = response.data.sort((a, b) => {
|
||||
// 先按置顶状态排序
|
||||
if (a.isTop !== b.isTop) {
|
||||
return a.isTop ? -1 : 1 // 置顶的在前
|
||||
}
|
||||
// 再按创建时间倒序排序
|
||||
return new Date(b.createTime).getTime() - new Date(a.createTime).getTime()
|
||||
})
|
||||
|
||||
comments.value = sortedComments
|
||||
console.log('✅ 评论数据设置成功:', comments.value)
|
||||
} else {
|
||||
console.log('⚠️ API返回的评论数据为空')
|
||||
comments.value = []
|
||||
}
|
||||
} else {
|
||||
console.log('⚠️ API返回错误')
|
||||
commentsError.value = response.message || '获取评论失败'
|
||||
comments.value = []
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载课程评论失败:', err)
|
||||
commentsError.value = '获取评论失败'
|
||||
comments.value = []
|
||||
} finally {
|
||||
commentsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载课程讲师列表
|
||||
const loadCourseInstructors = async () => {
|
||||
if (!courseId.value || courseId.value.trim() === '') {
|
||||
instructorsError.value = '课程ID无效'
|
||||
console.error('课程ID无效:', courseId.value)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
instructorsLoading.value = true
|
||||
instructorsError.value = ''
|
||||
|
||||
console.log('调用API获取课程讲师...')
|
||||
const response = await CourseApi.getCourseInstructors(courseId.value)
|
||||
console.log('讲师API响应:', response)
|
||||
|
||||
if (response.code === 0 || response.code === 200) {
|
||||
if (response.data && Array.isArray(response.data)) {
|
||||
instructors.value = response.data
|
||||
console.log('✅ 讲师数据设置成功:', instructors.value)
|
||||
} else {
|
||||
console.log('⚠️ API返回的讲师数据为空,使用默认数据')
|
||||
// 保持默认的mock数据
|
||||
}
|
||||
} else {
|
||||
console.log('⚠️ API返回错误,使用默认数据')
|
||||
instructorsError.value = response.message || '获取讲师信息失败'
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载课程讲师失败:', err)
|
||||
instructorsError.value = '获取讲师信息失败'
|
||||
// 保持默认的mock数据
|
||||
} finally {
|
||||
instructorsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 切换章节展开/收起
|
||||
const toggleChapter = (chapterIndex: number) => {
|
||||
console.log('切换章节展开/收起:', chapterIndex)
|
||||
@ -1158,6 +1250,8 @@ onMounted(async () => {
|
||||
}
|
||||
loadCourseDetail()
|
||||
loadCourseSections()
|
||||
loadCourseComments() // 启用评论接口调用
|
||||
loadCourseInstructors() // 启用讲师接口调用
|
||||
})
|
||||
|
||||
// 组件卸载时清理CKPlayer实例
|
||||
@ -1555,6 +1649,34 @@ onUnmounted(() => {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 讲师加载状态 */
|
||||
.instructors-loading {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.instructors-error {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.instructors-error .retry-btn {
|
||||
margin-top: 10px;
|
||||
padding: 6px 12px;
|
||||
background: #1890ff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.instructors-error .retry-btn:hover {
|
||||
background: #40a9ff;
|
||||
}
|
||||
|
||||
/* 分隔线样式 */
|
||||
.course-info-divider {
|
||||
height: 1px;
|
||||
@ -2905,4 +3027,68 @@ onUnmounted(() => {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 评论加载和错误状态 */
|
||||
.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;
|
||||
}
|
||||
</style>
|
||||
|
@ -121,7 +121,7 @@
|
||||
</div>
|
||||
<div class="course-footer">
|
||||
<div class="course-stats">
|
||||
<span class="course-students">讲师: 刘莹</span>
|
||||
<span class="course-students">讲师: {{ getCourseInstructors(course) }}</span>
|
||||
</div>
|
||||
<button class="enroll-btn" @click="goToCourseDetail(course)">去学习</button>
|
||||
</div>
|
||||
@ -389,6 +389,39 @@ const getCourseTitle = (course: Course) => {
|
||||
return course.title
|
||||
}
|
||||
|
||||
// 获取课程讲师名称的函数
|
||||
const getCourseInstructors = (course: Course) => {
|
||||
// 检查是否有teacherList字段(从后端数据适配而来)
|
||||
if (course.teacherList && Array.isArray(course.teacherList) && course.teacherList.length > 0) {
|
||||
// 按sortOrder降序排列讲师(sortOrder越大越靠前)
|
||||
const sortedTeachers = [...course.teacherList].sort((a, b) => {
|
||||
const sortOrderA = a.sortOrder || 0
|
||||
const sortOrderB = b.sortOrder || 0
|
||||
return sortOrderB - sortOrderA // 降序排列
|
||||
})
|
||||
|
||||
// 提取所有讲师的名字,用逗号分隔
|
||||
const teacherNames = sortedTeachers.map(teacher => teacher.name).join('、')
|
||||
|
||||
console.log('🔍 课程讲师信息:', {
|
||||
courseTitle: course.title,
|
||||
originalTeachers: course.teacherList,
|
||||
sortedTeachers: sortedTeachers,
|
||||
teacherNames: teacherNames
|
||||
})
|
||||
|
||||
return teacherNames
|
||||
}
|
||||
|
||||
// 如果没有teacherList,检查instructor字段(兼容旧数据)
|
||||
if (course.instructor && course.instructor.name) {
|
||||
return course.instructor.name
|
||||
}
|
||||
|
||||
// 默认值
|
||||
return '暂无讲师信息'
|
||||
}
|
||||
|
||||
// 跳转到课程详情页
|
||||
const goToCourseDetail = (course: Course) => {
|
||||
router.push({
|
||||
|
@ -187,20 +187,48 @@ const handleSubmit = async () => {
|
||||
if (response.code === 200) {
|
||||
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 (rememberMe.value) {
|
||||
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('登录成功!')
|
||||
|
||||
// 登录成功后跳转到首页或之前的页面
|
||||
|
@ -5,9 +5,9 @@
|
||||
<!-- 左侧侧边栏 -->
|
||||
<div class="block_14">
|
||||
<!-- 用户头像和姓名 -->
|
||||
<SafeAvatar class="image_7" :src="userStore.user?.avatar" :name="userStore.user?.username || '用户'" :size="96"
|
||||
<SafeAvatar class="image_7" :src="userStore.user?.avatar" :name="userStore.user?.profile?.realName || userStore.user?.nickname || userStore.user?.username || '用户'" :size="96"
|
||||
alt="用户头像" />
|
||||
<span class="text_72">{{ userStore.user?.username || '用户名' }}</span>
|
||||
<span class="text_72">{{ userStore.user?.profile?.realName || userStore.user?.nickname || userStore.user?.username || '用户名' }}</span>
|
||||
|
||||
<!-- 菜单项容器 -->
|
||||
<div class="box_22">
|
||||
@ -847,7 +847,7 @@
|
||||
<div class="avatar-section">
|
||||
<div class="avatar-container">
|
||||
<SafeAvatar :src="userStore.user?.avatar"
|
||||
:name="userStore.user?.username || userInfo.nickname || '用户'" :size="68" alt="用户头像"
|
||||
:name="userStore.user?.profile?.realName || userStore.user?.nickname || userStore.user?.username || '用户'" :size="68" alt="用户头像"
|
||||
class="user-avatar-large" />
|
||||
<div class="avatar-edit-btn">
|
||||
<img
|
||||
|
Loading…
x
Reference in New Issue
Block a user