merge: 合并远程dev分支,解决冲突

This commit is contained in:
Wxp 2025-08-19 19:07:54 +08:00
commit d6e76b7c73
12 changed files with 782 additions and 560 deletions

View File

@ -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',

View File

@ -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)

View File

@ -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

View File

@ -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[]

View File

@ -121,21 +121,48 @@ const handleLogin = async () => {
if (response.code === 200 || response.code === 0) {
const { user, token, refreshToken } = response.data
// tokenstore
userStore.user = user
// tokenstore
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()

View File

@ -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)

View File

@ -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
}

View File

@ -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
// 40px60px
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 {

View File

@ -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
// 40px60px
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>

View File

@ -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) {
// sortOrdersortOrder
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
}
// teacherListinstructor
if (course.instructor && course.instructor.name) {
return course.instructor.name
}
//
return '暂无讲师信息'
}
//
const goToCourseDetail = (course: Course) => {
router.push({

View File

@ -187,20 +187,48 @@ const handleSubmit = async () => {
if (response.code === 200) {
const { user, token, refreshToken } = response.data
// tokenstore
userStore.user = user
// tokenstore
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('登录成功!')
//

View File

@ -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">课件&gt;图片&gt;</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;