merge: 合并远程dev分支,解决冲突
This commit is contained in:
commit
d6e76b7c73
@ -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'
|
||||
@ -138,6 +138,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>
|
||||
|
||||
<!-- 标签页内容区域 -->
|
||||
@ -185,37 +185,6 @@
|
||||
<!-- 评论内容 -->
|
||||
<div v-if="activeTab === 'comments'" class="tab-pane">
|
||||
<div class="comments-content">
|
||||
<!-- 发布评论区域 -->
|
||||
<div class="post-comment-section">
|
||||
<div class="comment-input-wrapper">
|
||||
<div class="user-avatar">
|
||||
<img src="/images/activity/5.png" alt="用户头像" />
|
||||
</div>
|
||||
<div class="comment-input-area">
|
||||
<textarea v-model="newComment" placeholder="写下你的评论..." rows="3" class="comment-textarea"
|
||||
@input="adjustTextareaHeight" @click="handleTextareaClick"></textarea>
|
||||
<div class="comment-toolbar">
|
||||
<div class="toolbar-left">
|
||||
<button class="toolbar-btn">
|
||||
<img src="/images/courses/expression.png" alt="表情" class="toolbar-icon" />
|
||||
</button>
|
||||
<button class="toolbar-btn">
|
||||
<img src="/images/courses/@.png" alt="@用户" class="toolbar-icon" />
|
||||
</button>
|
||||
<button class="toolbar-btn">
|
||||
<img src="/images/courses/Image.png" alt="图片" class="toolbar-icon" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<button class="btn-submit" @click="submitComment">
|
||||
发布
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="comment-stats">
|
||||
<span class="total-comments">共1251条评论</span>
|
||||
<div class="comment-filters">
|
||||
@ -226,22 +195,49 @@
|
||||
</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" @click="startReply(comment.id, comment.username)">回复</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" @click="startReply(comment.id, comment.userName)">回复</button>
|
||||
</div>
|
||||
|
||||
<!-- 回复输入区域 -->
|
||||
@ -275,7 +271,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 回复区域示例 -->
|
||||
<div class="comment-replies" v-if="comment.id === 1">
|
||||
<div class="comment-replies" v-if="comment.id === '1'">
|
||||
<!-- 讲师回复 -->
|
||||
<div class="reply-item instructor-reply">
|
||||
<div class="reply-avatar">
|
||||
@ -292,7 +288,7 @@
|
||||
<div class="reply-footer">
|
||||
<span class="reply-time">2025.07.23 17:30</span>
|
||||
<div class="reply-actions">
|
||||
<button class="reply-action-btn" @click="startReply(1, '张老师')">回复</button>
|
||||
<button class="reply-action-btn" @click="startReply('1', '张老师')">回复</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -314,7 +310,7 @@
|
||||
<div class="reply-footer">
|
||||
<span class="reply-time">2025.07.23 18:15</span>
|
||||
<div class="reply-actions">
|
||||
<button class="reply-action-btn" @click="startReply(1, '李同学')">回复</button>
|
||||
<button class="reply-action-btn" @click="startReply('1', '李同学')">回复</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -351,11 +347,11 @@
|
||||
<span>笔记</span>
|
||||
</div>
|
||||
<span class="comment-time">2025.07.23 16:28</span>
|
||||
<button class="action-btn" @click="startReply(1, '张老师')">回复</button>
|
||||
<button class="action-btn" @click="startReply('1', '张老师')">回复</button>
|
||||
</div>
|
||||
|
||||
<!-- 回复输入区域 -->
|
||||
<div v-if="replyingTo === 1" class="reply-input-section">
|
||||
<div v-if="replyingTo === '1'" class="reply-input-section">
|
||||
<div class="reply-input-header">
|
||||
<span class="reply-to-text">回复 @{{ replyToUsername }}</span>
|
||||
<button class="cancel-reply-btn" @click="cancelReply">取消</button>
|
||||
@ -382,6 +378,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -460,7 +457,7 @@
|
||||
</div>
|
||||
<div class="lesson-info">
|
||||
<span class="lesson-title" :class="{ 'disabled': !isUserEnrolled }">{{ section.name
|
||||
}}</span>
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="lesson-meta">
|
||||
<span v-if="isVideoLesson(section)" class="lesson-duration"
|
||||
@ -634,7 +631,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'
|
||||
@ -663,6 +660,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) // 报名加载状态
|
||||
@ -699,6 +701,11 @@ const isUserEnrolled = computed(() => {
|
||||
const enrollConfirmVisible = ref(false)
|
||||
const enrollSuccessVisible = ref(false)
|
||||
|
||||
// 评论回复相关状态
|
||||
const replyingTo = ref<string | null>(null)
|
||||
const replyToUsername = ref('')
|
||||
const replyText = ref('')
|
||||
|
||||
// 章节分组数据
|
||||
interface ChapterGroup {
|
||||
title: string
|
||||
@ -734,7 +741,7 @@ const groupSectionsByChapter = (sections: CourseSection[]) => {
|
||||
const chapterSections = sections.slice(sectionIndex, sectionIndex + sectionsPerChapter[i])
|
||||
if (chapterSections.length > 0) {
|
||||
groups.push({
|
||||
title: `第${i + 1}章 ${chapterTitles[i]}`,
|
||||
title: `第${i+1}章 ${chapterTitles[i]}`,
|
||||
sections: chapterSections,
|
||||
expanded: i === 0 // 默认展开第一章
|
||||
})
|
||||
@ -817,7 +824,7 @@ const totalSections = computed(() => {
|
||||
const formatTotalDuration = () => {
|
||||
// 计算总时长
|
||||
let totalMinutes = 0
|
||||
courseSections.value.forEach((section: CourseSection) => {
|
||||
courseSections.value.forEach(section => {
|
||||
if (section.duration) {
|
||||
const parts = section.duration.split(':')
|
||||
if (parts.length === 3) {
|
||||
@ -833,122 +840,8 @@ const formatTotalDuration = () => {
|
||||
return `${hours}小时${minutes}分钟`
|
||||
}
|
||||
|
||||
// 新评论内容
|
||||
const newComment = ref('')
|
||||
|
||||
// 自动调整textarea高度
|
||||
const adjustTextareaHeight = (event: Event) => {
|
||||
const textarea = event.target as HTMLTextAreaElement
|
||||
textarea.style.height = 'auto'
|
||||
textarea.style.height = textarea.scrollHeight + 'px'
|
||||
}
|
||||
|
||||
// 点击textarea时调整高度
|
||||
const handleTextareaClick = (event: MouseEvent) => {
|
||||
const textarea = event.target as HTMLTextAreaElement
|
||||
// 如果当前高度是40px,则调整到60px
|
||||
if (textarea.style.height === '40px' || textarea.style.height === '') {
|
||||
textarea.style.height = '60px'
|
||||
}
|
||||
}
|
||||
|
||||
// 提交评论函数
|
||||
const submitComment = () => {
|
||||
if (newComment.value.trim()) {
|
||||
const newCommentObj = {
|
||||
id: Date.now(),
|
||||
username: '当前用户',
|
||||
avatar: 'https://via.placeholder.com/40x40/1890ff/ffffff?text=我',
|
||||
time: '刚刚',
|
||||
content: newComment.value,
|
||||
likes: 0
|
||||
}
|
||||
displayComments.value.unshift(newCommentObj)
|
||||
newComment.value = ''
|
||||
// 这里可以调用API提交评论
|
||||
console.log('评论已提交:', newCommentObj)
|
||||
}
|
||||
}
|
||||
|
||||
// 回复相关函数
|
||||
const startReply = (commentId: number, 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() && replyingTo.value) {
|
||||
const newReplyObj = {
|
||||
id: Date.now(),
|
||||
username: '当前用户',
|
||||
avatar: 'https://via.placeholder.com/40x40/1890ff/ffffff?text=我',
|
||||
time: '刚刚',
|
||||
content: replyText.value,
|
||||
likes: 0
|
||||
}
|
||||
|
||||
// 这里可以调用API提交回复
|
||||
console.log('回复已提交:', newReplyObj)
|
||||
console.log('回复给评论ID:', replyingTo.value)
|
||||
console.log('回复给用户:', replyToUsername.value)
|
||||
|
||||
// 清空回复状态
|
||||
cancelReply()
|
||||
}
|
||||
}
|
||||
|
||||
// 回复文本框高度调整
|
||||
const adjustReplyTextareaHeight = (event: Event) => {
|
||||
const textarea = event.target as HTMLTextAreaElement
|
||||
textarea.style.height = '40px'
|
||||
textarea.style.height = textarea.scrollHeight + 'px'
|
||||
}
|
||||
|
||||
const handleReplyTextareaClick = (event: MouseEvent) => {
|
||||
const textarea = event.target as HTMLTextAreaElement
|
||||
if (textarea.style.height === '40px' || !textarea.style.height) {
|
||||
textarea.style.height = '60px'
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
])
|
||||
|
||||
// 回复相关数据
|
||||
const replyingTo = ref<number | null>(null)
|
||||
const replyText = ref('')
|
||||
const replyToUsername = ref('')
|
||||
// displayComments 现在使用真实的API数据 (comments状态)
|
||||
// const displayComments = ref([]) // 已替换为comments状态
|
||||
|
||||
// 加载课程详情
|
||||
const loadCourseDetail = async () => {
|
||||
@ -1089,6 +982,93 @@ 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 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)
|
||||
@ -1404,6 +1384,7 @@ onMounted(() => {
|
||||
loadCourseDetail()
|
||||
loadCourseSections() // 启用章节接口调用
|
||||
loadCourseInstructors() // 启用讲师接口调用
|
||||
loadCourseComments() // 启用评论接口调用
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -3306,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;
|
||||
@ -3564,7 +3541,6 @@ onMounted(() => {
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 1399px) and (min-width: 1200px) {
|
||||
.container {
|
||||
|
@ -184,168 +184,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>
|
||||
</div>
|
||||
<div class="comment-text">{{ comment.content }}</div>
|
||||
<div class="comment-actions">
|
||||
<button class="action-btn">
|
||||
<span>2025.07.23 16:28</span>
|
||||
</button>
|
||||
<button v-if="comment.type === 'note'" class="action-btn">
|
||||
<span class="top">置顶评论</span>
|
||||
</button>
|
||||
<button v-else class="action-btn"
|
||||
@click="startReply(comment.id, comment.username)">回复</button>
|
||||
</div>
|
||||
|
||||
<!-- 回复输入区域 -->
|
||||
<div v-if="replyingTo === comment.id" class="reply-input-section">
|
||||
<div class="reply-input-header">
|
||||
<span class="reply-to-text">回复 @{{ replyToUsername }}</span>
|
||||
<button class="cancel-reply-btn" @click="cancelReply">取消</button>
|
||||
</div>
|
||||
<div class="reply-input-container">
|
||||
<textarea v-model="replyText" placeholder="写下你的回复..." class="reply-textarea"
|
||||
@input="adjustReplyTextareaHeight" @click="handleReplyTextareaClick"></textarea>
|
||||
<div class="reply-toolbar">
|
||||
<div class="toolbar-left">
|
||||
<button class="toolbar-btn">
|
||||
<img src="/images/courses/expression.png" alt="表情" class="toolbar-icon" />
|
||||
</button>
|
||||
<button class="toolbar-btn">
|
||||
<img src="/images/courses/@.png" alt="@用户" class="toolbar-icon" />
|
||||
</button>
|
||||
<button class="toolbar-btn">
|
||||
<img src="/images/courses/Image.png" alt="图片" class="toolbar-icon" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<button class="btn-submit" @click="submitReply" :disabled="!replyText.trim()">
|
||||
发布
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 回复区域示例 -->
|
||||
<div class="comment-replies" v-if="comment.id === 1">
|
||||
<!-- 讲师回复 -->
|
||||
<div class="reply-item instructor-reply">
|
||||
<div class="reply-avatar">
|
||||
<img src="/images/activity/6.png" alt="讲师头像" />
|
||||
</div>
|
||||
<div class="reply-content">
|
||||
<div class="reply-main">
|
||||
<div class="reply-header">
|
||||
<span class="reply-username">张老师</span>
|
||||
<span class="reply-badge instructor">讲师</span>
|
||||
</div>
|
||||
<div class="reply-text">感谢您的反馈!我们会继续优化课程内容,让学习体验更好。</div>
|
||||
</div>
|
||||
<div class="reply-footer">
|
||||
<span class="reply-time">2025.07.23 17:30</span>
|
||||
<div class="reply-actions">
|
||||
<button class="reply-action-btn" @click="startReply(1, '张老师')">回复</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户回复 -->
|
||||
<div class="reply-item user-reply">
|
||||
<div class="reply-avatar">
|
||||
<img src="/images/activity/7.png" alt="用户头像" />
|
||||
</div>
|
||||
<div class="reply-content">
|
||||
<div class="reply-main">
|
||||
<div class="reply-header">
|
||||
<span class="reply-username">李同学</span>
|
||||
<span class="reply-badge user">学员</span>
|
||||
</div>
|
||||
<div class="reply-text">同意楼上的观点,这个课程确实很有帮助!</div>
|
||||
</div>
|
||||
<div class="reply-footer">
|
||||
<span class="reply-time">2025.07.23 18:15</span>
|
||||
<div class="reply-actions">
|
||||
<button class="reply-action-btn" @click="startReply(1, '李同学')">回复</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="commentsLoading" class="comments-loading">
|
||||
<p>正在加载评论...</p>
|
||||
</div>
|
||||
|
||||
<div class="comment-item">
|
||||
<div class="comment-avatar">
|
||||
<img
|
||||
src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-4.0.3&auto=format&fit=crop&w=50&q=80"
|
||||
alt="张老师" />
|
||||
</div>
|
||||
<div class="comment-content">
|
||||
<div class="comment-header">
|
||||
<span class="comment-username">张老师</span>
|
||||
</div>
|
||||
<div class="comment-text">这个课程内容很实用,讲解得很清楚,对初学者很有帮助!111</div>
|
||||
<div class="comment-image-container">
|
||||
<img src="/images/courses/course1.png" alt="课程图片" class="comment-image">
|
||||
<img src="/images/courses/course1.png" alt="课程图片" class="comment-image">
|
||||
<img src="/images/courses/course1.png" alt="课程图片" class="comment-image">
|
||||
<img src="/images/courses/course1.png" alt="课程图片" class="comment-image">
|
||||
<img src="/images/courses/course1.png" alt="课程图片" class="comment-image">
|
||||
<img src="/images/courses/course1.png" alt="课程图片" class="comment-image">
|
||||
<div class="image-overlay">
|
||||
<span class="more-images-text">+6</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="comment-actions">
|
||||
<div class="note-icon-container">
|
||||
<img src="/images/courses/comments-note.png" alt="笔记" class="note-icon">
|
||||
<span>笔记</span>
|
||||
</div>
|
||||
<span class="comment-time">2025.07.23 16:28</span>
|
||||
<button class="action-btn" @click="startReply(1, '张老师')">回复</button>
|
||||
</div>
|
||||
<!-- 错误状态 -->
|
||||
<div v-else-if="commentsError" class="comments-error">
|
||||
<p>{{ commentsError }}</p>
|
||||
<button @click="loadCourseComments" class="retry-btn">重试</button>
|
||||
</div>
|
||||
|
||||
<!-- 回复输入区域 -->
|
||||
<div v-if="replyingTo === 1" class="reply-input-section">
|
||||
<div class="reply-input-header">
|
||||
<span class="reply-to-text">回复 @{{ replyToUsername }}</span>
|
||||
<button class="cancel-reply-btn" @click="cancelReply">取消</button>
|
||||
<!-- 评论列表 -->
|
||||
<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-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="reply-input-container">
|
||||
<textarea v-model="replyText" placeholder="写下你的回复..." class="reply-textarea"
|
||||
@input="adjustReplyTextareaHeight" @click="handleReplyTextareaClick"></textarea>
|
||||
<div class="reply-toolbar">
|
||||
<div class="toolbar-left">
|
||||
<button class="toolbar-btn">
|
||||
<img src="/images/courses/expression.png" alt="表情" class="toolbar-icon" />
|
||||
</button>
|
||||
<button class="toolbar-btn">
|
||||
<img src="/images/courses/@.png" alt="@用户" class="toolbar-icon" />
|
||||
</button>
|
||||
<button class="toolbar-btn">
|
||||
<img src="/images/courses/Image.png" alt="图片" class="toolbar-icon" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<button class="btn-submit" @click="submitReply" :disabled="!replyText.trim()">
|
||||
发布
|
||||
</button>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<!-- <div class="load-more">
|
||||
<button class="btn-load-more">加载更多评论</button>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -527,7 +417,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'
|
||||
@ -555,6 +445,19 @@ 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 newComment = ref('')
|
||||
|
||||
// 视频源配置
|
||||
const VIDEO_CONFIG = {
|
||||
// 本地视频(当前使用)
|
||||
@ -640,27 +543,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(() => {
|
||||
@ -690,126 +573,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,
|
||||
type: 'comment'
|
||||
},
|
||||
{
|
||||
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,
|
||||
type: 'comment'
|
||||
},
|
||||
{
|
||||
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,
|
||||
type: 'comment'
|
||||
}
|
||||
])
|
||||
|
||||
// 新评论内容
|
||||
const newComment = ref('')
|
||||
|
||||
// 自动调整textarea高度
|
||||
const adjustTextareaHeight = (event: Event) => {
|
||||
const textarea = event.target as HTMLTextAreaElement
|
||||
textarea.style.height = 'auto'
|
||||
textarea.style.height = textarea.scrollHeight + 'px'
|
||||
}
|
||||
|
||||
// 点击textarea时调整高度
|
||||
const handleTextareaClick = (event: MouseEvent) => {
|
||||
const textarea = event.target as HTMLTextAreaElement
|
||||
// 如果当前高度是40px,则调整到60px
|
||||
if (textarea.style.height === '40px' || textarea.style.height === '') {
|
||||
textarea.style.height = '60px'
|
||||
}
|
||||
}
|
||||
|
||||
// 提交评论函数
|
||||
const submitComment = () => {
|
||||
if (newComment.value.trim()) {
|
||||
const newCommentObj = {
|
||||
id: Date.now(),
|
||||
username: '当前用户',
|
||||
avatar: 'https://via.placeholder.com/40x40/1890ff/ffffff?text=我',
|
||||
time: '刚刚',
|
||||
content: newComment.value,
|
||||
likes: 0,
|
||||
type: 'comment'
|
||||
}
|
||||
displayComments.value.unshift(newCommentObj)
|
||||
newComment.value = ''
|
||||
// 这里可以调用API提交评论
|
||||
console.log('评论已提交:', newCommentObj)
|
||||
}
|
||||
}
|
||||
|
||||
// 回复相关函数
|
||||
const startReply = (commentId: number, 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() && replyingTo.value) {
|
||||
const newReplyObj = {
|
||||
id: Date.now(),
|
||||
username: '当前用户',
|
||||
avatar: 'https://via.placeholder.com/40x40/1890ff/ffffff?text=我',
|
||||
time: '刚刚',
|
||||
content: replyText.value,
|
||||
likes: 0
|
||||
}
|
||||
|
||||
// 这里可以调用API提交回复
|
||||
console.log('回复已提交:', newReplyObj)
|
||||
console.log('回复给评论ID:', replyingTo.value)
|
||||
console.log('回复给用户:', replyToUsername.value)
|
||||
|
||||
// 清空回复状态
|
||||
cancelReply()
|
||||
}
|
||||
}
|
||||
|
||||
// 回复文本框高度调整
|
||||
const adjustReplyTextareaHeight = (event: Event) => {
|
||||
const textarea = event.target as HTMLTextAreaElement
|
||||
textarea.style.height = '40px'
|
||||
textarea.style.height = textarea.scrollHeight + 'px'
|
||||
}
|
||||
|
||||
const handleReplyTextareaClick = (event: MouseEvent) => {
|
||||
const textarea = event.target as HTMLTextAreaElement
|
||||
if (textarea.style.height === '40px' || !textarea.style.height) {
|
||||
textarea.style.height = '60px'
|
||||
}
|
||||
}
|
||||
|
||||
// 回复相关数据
|
||||
const replyingTo = ref<number | null>(null)
|
||||
const replyText = ref('')
|
||||
const replyToUsername = ref('')
|
||||
// displayComments 现在使用真实的API数据 (comments状态)
|
||||
// const displayComments = ref([]) // 已替换为comments状态
|
||||
|
||||
// 生成模拟章节数据(暂时禁用)
|
||||
const generateMockSections = (): CourseSection[] => {
|
||||
@ -1277,6 +1042,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() // 初始化已报名状态
|
||||
@ -1287,6 +1073,8 @@ onMounted(async () => {
|
||||
}
|
||||
loadCourseDetail()
|
||||
loadCourseSections()
|
||||
loadCourseComments() // 启用评论接口调用
|
||||
loadCourseInstructors() // 启用讲师接口调用
|
||||
})
|
||||
|
||||
// 组件卸载时清理播放器实例
|
||||
@ -1356,6 +1144,7 @@ onUnmounted(() => {
|
||||
.video-player {
|
||||
background: #000;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.video-container {
|
||||
@ -1558,6 +1347,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;
|
||||
@ -1679,7 +1496,7 @@ onUnmounted(() => {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 0 10px 0;
|
||||
/* background: #F5F7FA; */
|
||||
background: #F5F7FA;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
@ -3343,4 +3160,68 @@ onUnmounted(() => {
|
||||
.reply-action-btn:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
/* 评论加载和错误状态 */
|
||||
.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>
|
||||
|
@ -107,7 +107,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>
|
||||
@ -385,6 +385,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('登录成功!')
|
||||
|
||||
// 登录成功后跳转到首页或之前的页面
|
||||
|
@ -12,9 +12,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?.nickname || userStore.user?.username || '用户名' }}</span>
|
||||
<span class="text_72">{{ userStore.user?.profile?.realName || userStore.user?.nickname || userStore.user?.username || '用户名' }}</span>
|
||||
|
||||
<!-- 菜单项容器 -->
|
||||
<div class="box_22">
|
||||
@ -22,81 +22,72 @@
|
||||
<div class="menu-divider"></div>
|
||||
|
||||
<!-- 我的课程 -->
|
||||
<div :class="['image-text_19', { active: activeTab === 'courses' }]" @click="handleMenuSelect('courses')"
|
||||
@mouseenter="hoveredTab = 'courses'" @mouseleave="hoveredTab = null">
|
||||
<img class="image_8" referrerpolicy="no-referrer" :src="(activeTab === 'courses' || hoveredTab === 'courses')
|
||||
<div :class="['image-text_19', { active: activeTab === 'courses' }]" @click="handleMenuSelect('courses')">
|
||||
<img class="image_8" referrerpolicy="no-referrer" :src="activeTab === 'courses'
|
||||
? '/images/profile/course-active.png'
|
||||
: '/images/profile/course.png'" />
|
||||
<span class="text-group_19">我的课程</span>
|
||||
</div>
|
||||
|
||||
<!-- 我的作业 -->
|
||||
<div :class="['image-text_20', { active: activeTab === 'homework' }]" @click="handleMenuSelect('homework')"
|
||||
@mouseenter="hoveredTab = 'homework'" @mouseleave="hoveredTab = null">
|
||||
<img class="label_4" referrerpolicy="no-referrer" :src="(activeTab === 'homework' || hoveredTab === 'homework')
|
||||
<div :class="['image-text_20', { active: activeTab === 'homework' }]" @click="handleMenuSelect('homework')">
|
||||
<img class="label_4" referrerpolicy="no-referrer" :src="activeTab === 'homework'
|
||||
? '/images/profile/grade-active.png'
|
||||
: '/images/profile/grade.png'" />
|
||||
<span class="text-group_20">我的作业</span>
|
||||
</div>
|
||||
|
||||
<!-- 我的考试 -->
|
||||
<div :class="['image-text_21', { active: activeTab === 'exam' }]" @click="handleMenuSelect('exam')"
|
||||
@mouseenter="hoveredTab = 'exam'" @mouseleave="hoveredTab = null">
|
||||
<img class="label_5" referrerpolicy="no-referrer" :src="(activeTab === 'exam' || hoveredTab === 'exam')
|
||||
<div :class="['image-text_21', { active: activeTab === 'exam' }]" @click="handleMenuSelect('exam')">
|
||||
<img class="label_5" referrerpolicy="no-referrer" :src="activeTab === 'exam'
|
||||
? '/images/profile/checklist-active.png'
|
||||
: '/images/profile/checklist.png'" />
|
||||
<span class="text-group_21">我的考试</span>
|
||||
</div>
|
||||
|
||||
<!-- 我的练习 -->
|
||||
<div :class="['image-text_22', { active: activeTab === 'practice' }]" @click="handleMenuSelect('practice')"
|
||||
@mouseenter="hoveredTab = 'practice'" @mouseleave="hoveredTab = null">
|
||||
<img class="label_6" referrerpolicy="no-referrer" :src="(activeTab === 'practice' || hoveredTab === 'practice')
|
||||
<div :class="['image-text_22', { active: activeTab === 'practice' }]" @click="handleMenuSelect('practice')">
|
||||
<img class="label_6" referrerpolicy="no-referrer" :src="activeTab === 'practice'
|
||||
? '/images/profile/bookmark-active.png'
|
||||
: '/images/profile/bookmark.png'" />
|
||||
<span class="text-group_22">我的练习</span>
|
||||
</div>
|
||||
|
||||
<!-- 我的活动 -->
|
||||
<div :class="['image-text_23', { active: activeTab === 'activity' }]" @click="handleMenuSelect('activity')"
|
||||
@mouseenter="hoveredTab = 'activity'" @mouseleave="hoveredTab = null">
|
||||
<img class="thumbnail_40" referrerpolicy="no-referrer" :src="(activeTab === 'activity' || hoveredTab === 'activity')
|
||||
<div :class="['image-text_23', { active: activeTab === 'activity' }]" @click="handleMenuSelect('activity')">
|
||||
<img class="thumbnail_40" referrerpolicy="no-referrer" :src="activeTab === 'activity'
|
||||
? '/images/profile/gift-active.png'
|
||||
: '/images/profile/gift.png'" />
|
||||
<span class="text-group_23">我的活动</span>
|
||||
</div>
|
||||
|
||||
<!-- 我的关注 -->
|
||||
<div :class="['image-text_27', { active: activeTab === 'follows' }]" @click="handleMenuSelect('follows')"
|
||||
@mouseenter="hoveredTab = 'follows'" @mouseleave="hoveredTab = null">
|
||||
<img class="thumbnail_42" referrerpolicy="no-referrer" :src="(activeTab === 'follows' || hoveredTab === 'follows')
|
||||
<div :class="['image-text_27', { active: activeTab === 'follows' }]" @click="handleMenuSelect('follows')">
|
||||
<img class="thumbnail_42" referrerpolicy="no-referrer" :src="activeTab === 'follows'
|
||||
? '/images/profile/concern-active.png'
|
||||
: '/images/profile/concern.png'" />
|
||||
<span class="text-group_27">我的关注</span>
|
||||
</div>
|
||||
|
||||
<!-- 我的消息 -->
|
||||
<div :class="['image-text_24', { active: activeTab === 'message' }]" @click="handleMenuSelect('message')"
|
||||
@mouseenter="hoveredTab = 'message'" @mouseleave="hoveredTab = null">
|
||||
<img class="label_7" referrerpolicy="no-referrer" :src="(activeTab === 'message' || hoveredTab === 'message')
|
||||
<div :class="['image-text_24', { active: activeTab === 'message' }]" @click="handleMenuSelect('message')">
|
||||
<img class="label_7" referrerpolicy="no-referrer" :src="activeTab === 'message'
|
||||
? '/images/profile/message-active.png'
|
||||
: '/images/profile/message.png'" />
|
||||
<span class="text-group_24">我的消息</span>
|
||||
</div>
|
||||
|
||||
<!-- 我的资料 -->
|
||||
<div :class="['image-text_25', { active: activeTab === 'materials' }]" @click="handleMenuSelect('materials')"
|
||||
@mouseenter="hoveredTab = 'materials'" @mouseleave="hoveredTab = null">
|
||||
<img class="image_9" referrerpolicy="no-referrer" :src="(activeTab === 'materials' || hoveredTab === 'materials')
|
||||
<div :class="['image-text_25', { active: activeTab === 'materials' }]" @click="handleMenuSelect('materials')">
|
||||
<img class="image_9" referrerpolicy="no-referrer" :src="activeTab === 'materials'
|
||||
? '/images/profile/profile-active.png'
|
||||
: '/images/profile/profile.png'" />
|
||||
<span class="text-group_25">我的资料</span>
|
||||
</div>
|
||||
|
||||
<!-- 我的下载 -->
|
||||
<div :class="['image-text_26', { active: activeTab === 'download' }]" @click="handleMenuSelect('download')"
|
||||
@mouseenter="hoveredTab = 'download'" @mouseleave="hoveredTab = null">
|
||||
<img class="thumbnail_41" referrerpolicy="no-referrer" :src="(activeTab === 'download' || hoveredTab === 'download')
|
||||
<div :class="['image-text_26', { active: activeTab === 'download' }]" @click="handleMenuSelect('download')">
|
||||
<img class="thumbnail_41" referrerpolicy="no-referrer" :src="activeTab === 'download'
|
||||
? '/images/profile/download-active.png'
|
||||
: '/images/profile/download.png'" />
|
||||
<span class="text-group_26">我的下载</span>
|
||||
@ -247,7 +238,7 @@
|
||||
fontSize: '14px'
|
||||
}">
|
||||
{{ detailAssignment.status === '未完成' || detailAssignment.status === '待提交' ? '未完成' :
|
||||
(detailAssignment.status === '已完成' ? '已完成' : '541人已完成') }}
|
||||
(detailAssignment.status === '已完成' ? '已完成' : '541人已完成') }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
@ -370,7 +361,7 @@
|
||||
<span class="text_36">上传作业</span>
|
||||
</div>
|
||||
<div class="text-wrapper_8 anew-button" @click="reEditDraft">
|
||||
<span class="">重新编辑</span>
|
||||
<span class="text_36">重新编辑</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -676,7 +667,7 @@
|
||||
<div class="activity-status-left">
|
||||
<span :class="['activity-status-text', activity.status]">{{ activity.status === 'ongoing' ? '进行中' :
|
||||
'已结束'
|
||||
}}</span>
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -863,7 +854,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
|
||||
@ -966,7 +957,8 @@
|
||||
<!-- 面包屑导航或筛选和操作区域 -->
|
||||
<div v-if="isInSubDirectory" class="breadcrumb-controls">
|
||||
<div class="breadcrumb-nav">
|
||||
<span class="breadcrumb-text" @click="goBack">{{ currentPath.join(' > ') }}</span>
|
||||
<span class="breadcrumb-text" @click="goBack">课件>图片></span>
|
||||
<span class="breadcrumb-current">风景图片</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -986,7 +978,9 @@
|
||||
<div class="search-input-container">
|
||||
<input v-model="downloadFilter.keyword" type="text" class="search-input" placeholder="请输入文件名称" />
|
||||
<button class="search-btn">
|
||||
<img src="/images/profile/search.png" alt="搜索图标" class="search-icon" />
|
||||
<img
|
||||
src="https://lanhu-oss-2537-2.lanhuapp.com/SketchPng870a86da8af58a60f35fcb27ef4822e645d2ad5aaabe6416e4179342a53a5a60"
|
||||
alt="搜索图标" class="search-icon" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -1250,7 +1244,6 @@ const userStore = useUserStore()
|
||||
type TabType = 'courses' | 'homework' | 'exam' | 'practice' | 'activity' | 'follows' | 'message' | 'materials' | 'download'
|
||||
|
||||
const activeTab = ref<TabType>('courses')
|
||||
const hoveredTab = ref<TabType | null>(null)
|
||||
const activeCourseTab = ref('all')
|
||||
|
||||
// 作业筛选状态
|
||||
@ -2493,11 +2486,6 @@ const handleDownloadTabChange = (tab: string) => {
|
||||
|
||||
// 筛选后的下载文件
|
||||
const filteredDownloadFiles = computed(() => {
|
||||
// 如果在子目录中,显示空内容(因为目前没有子目录的文件数据)
|
||||
if (isInSubDirectory.value) {
|
||||
return []
|
||||
}
|
||||
|
||||
let files = downloadFiles.filter(file => file.category === activeDownloadTab.value)
|
||||
|
||||
if (downloadFilter.type !== 'all') {
|
||||
@ -2519,14 +2507,10 @@ const toggleFileMenu = (fileId: number) => {
|
||||
}
|
||||
|
||||
const handleFileClick = (file: any) => {
|
||||
if (file.type === 'folder') {
|
||||
// 点击任何文件夹,进入子目录
|
||||
if (file.type === 'folder' && file.name === '图片') {
|
||||
// 点击图片文件夹,进入子目录
|
||||
isInSubDirectory.value = true
|
||||
currentPath.value = ['课件', file.name]
|
||||
message.info(`进入文件夹:${file.name}`)
|
||||
} else {
|
||||
// 点击文件,显示文件详情或下载
|
||||
message.info(`打开文件:${file.name}`)
|
||||
currentPath.value = ['课件', '图片']
|
||||
}
|
||||
}
|
||||
|
||||
@ -2552,9 +2536,12 @@ const getFileIcon = (fileId?: number) => {
|
||||
]
|
||||
const index = (fileId || 0) % homeworkImages.length
|
||||
return homeworkImages[index]
|
||||
} else if (isInSubDirectory.value) {
|
||||
// 子目录使用子目录图标
|
||||
return 'https://lanhu-oss-2537-2.lanhuapp.com/SketchPngf45333052202c303acc2c06223c26b820d330459ce2d452a21a3132fbbeab442'
|
||||
} else {
|
||||
// 默认文件夹图标
|
||||
return '/images/profile/folder.png'
|
||||
return 'https://lanhu-oss-2537-2.lanhuapp.com/SketchPng5548891b00234027dbe6dadafbd83596d616261421c0587a85652dc194b2d5ef'
|
||||
}
|
||||
}
|
||||
|
||||
@ -2943,8 +2930,9 @@ onActivated(() => {
|
||||
/* 去掉背景色 */
|
||||
border-radius: 0.6vw;
|
||||
/* 12px转换为vw */
|
||||
margin: 2.55vh 0 0 0;
|
||||
margin: 2.55vh 0;
|
||||
/* 去掉左右边距,因为父容器已经居中 */
|
||||
padding: 1.04vh 0;
|
||||
/* 20px 0转换 */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -2979,7 +2967,7 @@ onActivated(() => {
|
||||
/* 自适应高度 */
|
||||
min-height: 3vh;
|
||||
/* 设置最小高度,让盒子更大 */
|
||||
margin: 1.5vh 0 0 0;
|
||||
margin: 1.5vh;
|
||||
/* 减小间距:从2.34vh减少到1.5vh */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -4198,9 +4186,9 @@ onActivated(() => {
|
||||
}
|
||||
|
||||
.course-name {
|
||||
/* margin-top: 10px; */
|
||||
margin-left: 5px;
|
||||
color: #497087;
|
||||
margin-top: 10px;
|
||||
margin-left: 83px;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.course-name span {
|
||||
@ -5756,7 +5744,7 @@ onActivated(() => {
|
||||
/* 0 0 16px 0转换 */
|
||||
line-height: 1.4;
|
||||
padding: 0 1.04vw;
|
||||
font-weight: 700;
|
||||
/* 添加左右内边距 */
|
||||
}
|
||||
|
||||
.activity-details {
|
||||
@ -6184,7 +6172,7 @@ onActivated(() => {
|
||||
/* height: 5.21vh; */
|
||||
padding: 0.52vh 0.57vw;
|
||||
/* 100px转换为vh,进一步增加高度 */
|
||||
background: #F5F8FB;
|
||||
background: url('https://lanhu-oss-2537-2.lanhuapp.com/SketchPng9491a7fe5bdac8e8a88de63907163bd6b8a259824f56a3c76784ba6cdc7bc32b') 100% no-repeat;
|
||||
background-size: 100% 100%;
|
||||
margin-top: 0.26vh;
|
||||
/* 5px转换为vh */
|
||||
@ -7070,7 +7058,6 @@ onActivated(() => {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.password-form-input {
|
||||
@ -7622,7 +7609,7 @@ onActivated(() => {
|
||||
/* 4px转换为vw,减小图标和文字间距 */
|
||||
padding: 0.52vh 0.73vw;
|
||||
/* 10px 14px转换 */
|
||||
font-size: 12px;
|
||||
font-size: 10px;
|
||||
/* 14px转换为vw */
|
||||
font-family: Helvetica, 'Microsoft YaHei', Arial, sans-serif;
|
||||
color: #000;
|
||||
@ -8079,7 +8066,7 @@ onActivated(() => {
|
||||
width: 80px;
|
||||
height: 23px;
|
||||
border: none;
|
||||
font-size: 12px;
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
white-space: nowrap;
|
||||
|
Loading…
x
Reference in New Issue
Block a user