feat:新增loading,

This commit is contained in:
小张 2025-09-23 18:09:49 +08:00
parent ce6a0d41eb
commit 978713b316
5 changed files with 952 additions and 18 deletions

View File

@ -0,0 +1,198 @@
<template>
<div class="loading-container" :class="containerClass">
<!-- 全屏遮罩模式 -->
<div v-if="overlay" class="loading-overlay" :class="overlayClass">
<div class="loading-content">
<n-spin :size="spinSize" :stroke="strokeColor">
<template #description>
<div class="loading-text" :style="textStyle">
{{ text }}
</div>
</template>
</n-spin>
</div>
</div>
<!-- 内联模式 -->
<div v-else class="loading-inline" :class="inlineClass">
<n-spin :size="spinSize" :stroke="strokeColor">
<template #description v-if="text">
<div class="loading-text" :style="textStyle">
{{ text }}
</div>
</template>
</n-spin>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { NSpin } from 'naive-ui'
interface Props {
//
loading?: boolean
//
text?: string
//
overlay?: boolean
// small | medium | large
size?: 'small' | 'medium' | 'large'
//
color?: string
//
textColor?: string
//
backgroundColor?: string
//
opacity?: number
//
customClass?: string
// z-index
zIndex?: number
}
const props = withDefaults(defineProps<Props>(), {
loading: true,
text: '加载中...',
overlay: false,
size: 'medium',
color: '#1890ff',
textColor: '#666666',
backgroundColor: 'rgba(255, 255, 255, 0.8)',
opacity: 0.8,
customClass: '',
zIndex: 1000
})
// Spin
const spinSize = computed(() => {
const sizeMap = {
small: 'small',
medium: 'medium',
large: 'large'
}
return sizeMap[props.size] as 'small' | 'medium' | 'large'
})
//
const strokeColor = computed(() => props.color)
//
const textStyle = computed(() => ({
color: props.textColor,
marginTop: '12px',
fontSize: props.size === 'small' ? '12px' : props.size === 'large' ? '16px' : '14px'
}))
//
const containerClass = computed(() => [
props.customClass,
{
'loading-container--overlay': props.overlay,
'loading-container--inline': !props.overlay
}
])
//
const overlayClass = computed(() => ({
'loading-overlay--visible': props.loading
}))
//
const inlineClass = computed(() => ({
'loading-inline--visible': props.loading
}))
</script>
<style scoped>
.loading-container {
position: relative;
}
/* 遮罩模式样式 */
.loading-container--overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: v-bind(zIndex);
pointer-events: none;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: v-bind(backgroundColor);
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease, visibility 0.3s ease;
pointer-events: none;
display: flex;
align-items: center;
justify-content: center;
}
.loading-overlay--visible {
opacity: v-bind(opacity);
visibility: visible;
pointer-events: all;
}
.loading-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24px;
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
min-width: 120px;
}
/* 内联模式样式 */
.loading-container--inline {
position: relative;
display: inline-block;
}
.loading-inline {
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease, visibility 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.loading-inline--visible {
opacity: 1;
visibility: visible;
}
.loading-text {
text-align: center;
font-weight: 400;
line-height: 1.4;
white-space: nowrap;
}
/* 响应式设计 */
@media (max-width: 768px) {
.loading-content {
padding: 20px;
min-width: 100px;
}
.loading-text {
font-size: 12px !important;
}
}
</style>

View File

@ -0,0 +1,337 @@
<template>
<div class="loading-example">
<h2>Loading 组件使用示例</h2>
<!-- 基础用法 -->
<div class="example-section">
<h3>1. 基础用法</h3>
<div class="example-content">
<n-button @click="showBasicLoading" type="primary">
显示基础 Loading
</n-button>
<n-button @click="hideBasicLoading" type="default">
隐藏 Loading
</n-button>
</div>
<!-- 内联 Loading 示例 -->
<div class="inline-loading-demo">
<Loading
:loading="basicLoading"
text="正在加载数据..."
:overlay="false"
/>
</div>
</div>
<!-- 不同尺寸 -->
<div class="example-section">
<h3>2. 不同尺寸</h3>
<div class="example-content">
<n-button @click="showSmallLoading" size="small">
小尺寸 Loading
</n-button>
<n-button @click="showMediumLoading">
中等尺寸 Loading
</n-button>
<n-button @click="showLargeLoading" size="large">
大尺寸 Loading
</n-button>
</div>
</div>
<!-- 自定义颜色 -->
<div class="example-section">
<h3>3. 自定义颜色</h3>
<div class="example-content">
<n-button @click="showRedLoading" type="error">
红色 Loading
</n-button>
<n-button @click="showGreenLoading" type="success">
绿色 Loading
</n-button>
<n-button @click="showPurpleLoading" type="info">
紫色 Loading
</n-button>
</div>
</div>
<!-- 异步操作包装 -->
<div class="example-section">
<h3>4. 异步操作包装</h3>
<div class="example-content">
<n-button @click="simulateApiCall" type="primary">
模拟 API 调用
</n-button>
<n-button @click="simulateApiError" type="error">
模拟 API 错误
</n-button>
<n-button @click="simulateProgressLoading" type="info">
模拟进度加载
</n-button>
</div>
</div>
<!-- 组件内 Loading -->
<div class="example-section">
<h3>5. 组件内 Loading Hook</h3>
<div class="example-content">
<n-button @click="toggleComponentLoading" :type="componentLoading ? 'default' : 'primary'">
{{ componentLoading ? '停止' : '开始' }} 组件加载
</n-button>
<div class="component-loading-demo">
<Loading
v-if="componentLoading"
:loading="componentLoading"
:text="componentLoadingText"
size="small"
color="#52c41a"
/>
<div v-else class="demo-content">
<p>这里是组件内容</p>
<p> Loading 显示时这些内容会被遮盖</p>
</div>
</div>
</div>
</div>
<!-- 结果显示 -->
<div v-if="result" class="result-section">
<h3>操作结果</h3>
<n-alert :type="result.type" :title="result.title">
{{ result.message }}
</n-alert>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { NButton, NAlert, useMessage } from 'naive-ui'
import Loading, { useLoading } from '@/composables/useLoading'
// 使 Loading Hook
const { loading: componentLoading, loadingText: componentLoadingText, showLoading: showComponentLoading, hideLoading: hideComponentLoading, updateLoadingText } = useLoading()
const message = useMessage()
const basicLoading = ref(false)
const result = ref<{ type: 'success' | 'error' | 'info' | 'warning', title: string, message: string } | null>(null)
//
const showBasicLoading = () => {
basicLoading.value = true
}
const hideBasicLoading = () => {
basicLoading.value = false
}
//
const showSmallLoading = () => {
Loading.show({
text: '小尺寸加载中...',
size: 'small'
})
setTimeout(() => Loading.hide(), 2000)
}
const showMediumLoading = () => {
Loading.show({
text: '中等尺寸加载中...',
size: 'medium'
})
setTimeout(() => Loading.hide(), 2000)
}
const showLargeLoading = () => {
Loading.show({
text: '大尺寸加载中...',
size: 'large'
})
setTimeout(() => Loading.hide(), 2000)
}
//
const showRedLoading = () => {
Loading.show({
text: '红色加载中...',
color: '#ff4d4f',
textColor: '#ff4d4f'
})
setTimeout(() => Loading.hide(), 2000)
}
const showGreenLoading = () => {
Loading.show({
text: '绿色加载中...',
color: '#52c41a',
textColor: '#52c41a'
})
setTimeout(() => Loading.hide(), 2000)
}
const showPurpleLoading = () => {
Loading.show({
text: '紫色加载中...',
color: '#722ed1',
textColor: '#722ed1'
})
setTimeout(() => Loading.hide(), 2000)
}
// API
const simulateApiCall = async () => {
try {
const data = await Loading.wrap(
() => new Promise(resolve => setTimeout(() => resolve('API 数据'), 3000)),
{
text: '正在获取数据...',
color: '#1890ff'
}
)
result.value = {
type: 'success',
title: '成功',
message: `获取到数据: ${data}`
}
message.success('API 调用成功!')
} catch (error) {
console.error('API 调用失败:', error)
}
}
// API
const simulateApiError = async () => {
try {
await Loading.wrap(
() => new Promise((_, reject) => setTimeout(() => reject(new Error('网络错误')), 2000)),
{
text: '正在处理请求...',
color: '#ff4d4f',
onError: (error) => {
result.value = {
type: 'error',
title: '错误',
message: error.message
}
message.error('API 调用失败!')
}
}
)
} catch (error) {
// onError
}
}
//
const simulateProgressLoading = () => {
Loading.show({
text: '准备中... 0%',
color: '#1890ff'
})
let progress = 0
const interval = setInterval(() => {
progress += 10
Loading.updateText(`加载中... ${progress}%`)
if (progress >= 100) {
clearInterval(interval)
setTimeout(() => {
Loading.hide()
result.value = {
type: 'success',
title: '完成',
message: '进度加载完成!'
}
message.success('加载完成!')
}, 500)
}
}, 300)
}
// Loading
const toggleComponentLoading = () => {
if (componentLoading.value) {
hideComponentLoading()
} else {
showComponentLoading('组件加载中...')
//
setTimeout(() => updateLoadingText('即将完成...'), 1500)
setTimeout(() => hideComponentLoading(), 3000)
}
}
</script>
<style scoped>
.loading-example {
padding: 24px;
max-width: 800px;
margin: 0 auto;
}
.example-section {
margin-bottom: 32px;
padding: 20px;
border: 1px solid #e8e8e8;
border-radius: 8px;
background: #fafafa;
}
.example-section h3 {
margin: 0 0 16px 0;
color: #333;
font-size: 16px;
font-weight: 600;
}
.example-content {
display: flex;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 16px;
}
.inline-loading-demo {
height: 100px;
border: 1px dashed #d9d9d9;
border-radius: 4px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
background: white;
}
.component-loading-demo {
height: 120px;
border: 1px dashed #d9d9d9;
border-radius: 4px;
position: relative;
background: white;
display: flex;
align-items: center;
justify-content: center;
}
.demo-content {
text-align: center;
color: #666;
}
.result-section {
margin-top: 24px;
padding: 16px;
background: white;
border-radius: 8px;
}
.result-section h3 {
margin: 0 0 12px 0;
color: #333;
font-size: 16px;
font-weight: 600;
}
</style>

View File

@ -0,0 +1,243 @@
import { ref } from 'vue'
import type {
LoadingOptions,
LoadingWrapOptions,
UseLoadingReturn
} from '@/types/loading'
// 全局 Loading 状态(保留以备后用)
// const globalLoading = ref(false)
// const globalLoadingText = ref('加载中...')
// const globalLoadingOptions = ref<LoadingOptions>({})
// 创建全局遮罩元素
let globalLoadingElement: HTMLElement | null = null
/**
* Loading
*/
function createGlobalLoadingElement() {
if (globalLoadingElement) return
globalLoadingElement = document.createElement('div')
globalLoadingElement.id = 'global-loading-overlay'
globalLoadingElement.style.cssText = `
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease, visibility 0.3s ease;
`
const content = document.createElement('div')
content.style.cssText = `
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24px;
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
min-width: 120px;
`
const spinner = document.createElement('div')
spinner.style.cssText = `
width: 32px;
height: 32px;
border: 3px solid #f3f3f3;
border-top: 3px solid #1890ff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 12px;
`
const text = document.createElement('div')
text.id = 'global-loading-text'
text.style.cssText = `
color: #666;
font-size: 14px;
text-align: center;
`
text.textContent = '加载中...'
// 添加旋转动画
const style = document.createElement('style')
style.textContent = `
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`
document.head.appendChild(style)
content.appendChild(spinner)
content.appendChild(text)
globalLoadingElement.appendChild(content)
document.body.appendChild(globalLoadingElement)
}
/**
* Loading
*/
function showGlobalLoading(options: LoadingOptions = {}) {
createGlobalLoadingElement()
if (globalLoadingElement) {
const textElement = globalLoadingElement.querySelector('#global-loading-text') as HTMLElement
if (textElement) {
textElement.textContent = options.text || '加载中...'
}
// 更新样式
if (options.color) {
const spinner = globalLoadingElement.querySelector('div > div') as HTMLElement
if (spinner) {
spinner.style.borderTopColor = options.color
}
}
if (options.backgroundColor) {
globalLoadingElement.style.background = options.backgroundColor
}
if (options.zIndex) {
globalLoadingElement.style.zIndex = options.zIndex.toString()
}
// 显示
globalLoadingElement.style.opacity = '1'
globalLoadingElement.style.visibility = 'visible'
}
}
/**
* Loading
*/
function hideGlobalLoading() {
if (globalLoadingElement) {
globalLoadingElement.style.opacity = '0'
globalLoadingElement.style.visibility = 'hidden'
}
}
/**
* Loading
*/
function updateGlobalLoadingText(text: string) {
if (globalLoadingElement) {
const textElement = globalLoadingElement.querySelector('#global-loading-text') as HTMLElement
if (textElement) {
textElement.textContent = text
}
}
}
/**
* Loading
*/
function destroyGlobalLoading() {
if (globalLoadingElement) {
document.body.removeChild(globalLoadingElement)
globalLoadingElement = null
}
}
/**
* 使 Loading Hook
*/
export function useLoading(initialLoading = false): UseLoadingReturn {
const loading = ref(initialLoading)
const loadingText = ref('加载中...')
const showLoading = (text = '加载中...') => {
loadingText.value = text
loading.value = true
}
const hideLoading = () => {
loading.value = false
}
const updateLoadingText = (text: string) => {
loadingText.value = text
}
return {
loading,
loadingText,
showLoading,
hideLoading,
updateLoadingText
}
}
/**
* Loading
*/
export const Loading = {
/**
* Loading
*/
show: (options?: LoadingOptions) => {
showGlobalLoading(options)
},
/**
* Loading
*/
hide: () => {
hideGlobalLoading()
},
/**
* Loading
*/
updateText: (text: string) => {
updateGlobalLoadingText(text)
},
/**
* Loading
*/
destroy: () => {
destroyGlobalLoading()
},
/**
*
*/
async wrap<T>(
asyncFn: () => Promise<T>,
options?: LoadingWrapOptions
): Promise<T> {
try {
Loading.show(options)
const result = await asyncFn()
return result
} catch (error) {
if (options?.onError) {
options.onError(error)
} else {
console.error('Loading.wrap error:', error)
}
throw error
} finally {
Loading.hide()
if (options?.finallyCallback) {
options.finallyCallback()
}
}
}
}
export default Loading

77
src/types/loading.ts Normal file
View File

@ -0,0 +1,77 @@
/**
* Loading
*/
export interface LoadingOptions {
/** 加载文本 */
text?: string
/** 尺寸small | medium | large */
size?: 'small' | 'medium' | 'large'
/** 自定义颜色 */
color?: string
/** 文本颜色 */
textColor?: string
/** 背景颜色(遮罩模式) */
backgroundColor?: string
/** 透明度(遮罩模式) */
opacity?: number
/** z-index */
zIndex?: number
}
export interface LoadingWrapOptions extends LoadingOptions {
/** 错误处理回调 */
onError?: (error: any) => void
/** 最终回调(无论成功失败都会执行) */
finallyCallback?: () => void
}
export interface LoadingInstance {
/** 显示 Loading */
show: (options?: LoadingOptions) => void
/** 隐藏 Loading */
hide: () => void
/** 更新文本 */
updateText: (text: string) => void
/** 销毁实例 */
destroy: () => void
}
export interface UseLoadingReturn {
/** 加载状态 */
loading: Ref<boolean>
/** 加载文本 */
loadingText: Ref<string>
/** 显示加载 */
showLoading: (text?: string) => void
/** 隐藏加载 */
hideLoading: () => void
/** 更新加载文本 */
updateLoadingText: (text: string) => void
}
export interface LoadingProps {
/** 是否显示加载状态 */
loading?: boolean
/** 加载文本 */
text?: string
/** 是否显示遮罩层 */
overlay?: boolean
/** 尺寸small | medium | large */
size?: 'small' | 'medium' | 'large'
/** 自定义颜色 */
color?: string
/** 文本颜色 */
textColor?: string
/** 背景颜色(遮罩模式) */
backgroundColor?: string
/** 透明度(遮罩模式) */
opacity?: number
/** 自定义类名 */
customClass?: string
/** z-index */
zIndex?: number
}
// 导入 Vue 的 Ref 类型
import type { Ref } from 'vue'

View File

@ -355,14 +355,14 @@
</div>
<!-- 评论统计 -->
<div class="discussion-stats">
<div class="discussion-stats" v-if="false">
<span class="comment-count">评论 (1251)</span>
</div>
<!-- 评论输入区域 -->
<div class="comment-input-section">
<div class="user-avatar">
<img src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-4.0.3&auto=format&fit=crop&w=40&q=80" alt="用户头像">
<img :src="userAvatar" :alt="userName + '的头像'">
</div>
<div class="input-wrapper">
<textarea
@ -391,7 +391,7 @@
<div class="discussion-list">
<div v-for="comment in discussionList" :key="comment.id" class="discussion-item">
<div class="comment-avatar">
<img src="/images/activity/1.png" :alt="comment.username" />
<img :src="comment.avatar || userAvatar" :alt="comment.username" />
</div>
<div class="comment-content">
<div class="comment-header">
@ -530,7 +530,7 @@
<div class="post-comment-section">
<div class="comment-input-wrapper">
<div class="user-avatar">
<img src="/images/activity/6.png" alt="用户头像" />
<img :src="userAvatar" :alt="userName + '的头像'" />
</div>
<div class="comment-input-area">
<textarea v-model="newComment" placeholder="写下你的评论..." class="comment-textarea"
@ -582,6 +582,7 @@
<div class="comment-content">
<div class="comment-header">
<span class="comment-username">{{ comment.username }}</span>
<span class="comment-badge instructor" v-if="comment.isTeacher">讲师</span>
</div>
<div class="comment-text">{{ comment.content }}</div>
<div class="comment-footer">
@ -649,20 +650,22 @@
<!-- 用户回复 -->
<div class="reply-item user-reply">
<div class="reply-avatar">
<img src="/images/activity/7.png" alt="用户头像" />
<img :src="userAvatar" :alt="userName + '的头像'" />
</div>
<div class="reply-content">
<div class="reply-main">
<div class="reply-header">
<span class="reply-username">李同学</span>
<span class="reply-badge user">学员</span>
<span class="reply-username">{{ userName }}</span>
<span class="reply-badge" :class="isTeacher ? 'instructor' : 'user'">
{{ isTeacher ? '讲师' : '学员' }}
</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>
<button class="reply-action-btn" @click="startReply(1, userName)">回复</button>
</div>
</div>
</div>
@ -1348,6 +1351,7 @@ import { CourseApi } from '@/api/modules/course'
import { CommentApi } from '@/api/modules/comment'
import { AIApi } from '@/api/modules/ai'
import { AuthApi } from '@/api/modules/auth'
import Loading from '@/composables/useLoading'
import type { Course, CourseSection, CourseComment } from '@/api/types'
import QuillEditor from '@/components/common/QuillEditor.vue'
import DPlayerVideo from '@/components/course/DPlayerVideo.vue'
@ -1383,6 +1387,13 @@ const sectionsError = ref('')
const isEnrolled = ref(false) //
const enrollmentLoading = ref(false) //
//
const userInfo = ref<any>(null)
const userAvatar = ref('https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-4.0.3&auto=format&fit=crop&w=40&q=80')
const userName = ref('用户')
const userRoles = ref<string[]>([])
const isTeacher = ref(false)
//
// const RegistrationStatus = ref(false)
@ -1672,7 +1683,8 @@ const displayComments = computed(() => {
time: comment.timeAgo,
content: comment.content,
likes: comment.likeCount || 0,
type: comment.isTop ? 'note' : 'comment'
type: comment.isTop ? 'note' : 'comment',
isTeacher: comment.userTag === 'teacher' || comment.userTag === '讲师' //
}))
})
@ -1823,11 +1835,12 @@ const submitReply = () => {
if (replyText.value.trim() && replyingTo.value) {
const newReplyObj = {
id: Date.now(),
username: '当前用户',
avatar: 'https://via.placeholder.com/40x40/1890ff/ffffff?text=我',
username: userName.value,
avatar: userAvatar.value,
time: '刚刚',
content: replyText.value,
likes: 0
likes: 0,
isTeacher: isTeacher.value
}
// API
@ -2443,13 +2456,23 @@ const handlePractice = async (section: CourseSection) => {
exitDiscussion()
}
// Loading
Loading.show({
text: '正在加载练习...',
size: 'medium',
color: '#1890ff'
})
try {
//
console.log('👤 获取用户信息...')
Loading.updateText('正在获取用户信息...')
const userInfoResponse = await AuthApi.getUserInfo()
if (!userInfoResponse.success || !userInfoResponse.result?.baseInfo?.id) {
console.error('❌ 获取用户信息失败:', userInfoResponse)
Loading.hide()
message.error('获取用户信息失败,请重新登录')
return
}
@ -2458,6 +2481,7 @@ const handlePractice = async (section: CourseSection) => {
console.log('✅ 获取用户信息成功学生ID:', studentId)
// API
Loading.updateText('正在获取练习信息...')
const response = await CourseApi.getSectionExercise(courseId.value, section.id.toString())
if (response.data && (response.data.code === 200 || response.data.code === 0)) {
@ -2474,6 +2498,7 @@ const handlePractice = async (section: CourseSection) => {
console.log('📋 开始获取考试题目考试ID:', examId, '学生ID:', studentId)
// IDID
Loading.updateText('正在获取题目列表...')
const questionsResponse = await CourseApi.getExamQuestions(examId, studentId)
if (questionsResponse.data && (questionsResponse.data.code === 200 || questionsResponse.data.code === 0)) {
@ -2485,10 +2510,13 @@ const handlePractice = async (section: CourseSection) => {
// ID
const detailedQuestions = []
const totalQuestions = questionsList.length
for (const questionItem of questionsList) {
for (let i = 0; i < questionsList.length; i++) {
const questionItem = questionsList[i]
try {
console.log('🔍 获取题目详情题目ID:', questionItem.id)
Loading.updateText(`正在获取题目详情... (${i + 1}/${totalQuestions})`)
const detailResponse = await CourseApi.getQuestionDetail(questionItem.id)
if (detailResponse.data && (detailResponse.data.code === 200 || detailResponse.data.code === 0)) {
@ -2567,16 +2595,22 @@ const handlePractice = async (section: CourseSection) => {
console.log('✅ 练习模式已启动,题目数量:', practiceQuestions.value.length)
console.log('✅ 处理后的题目列表:', practiceQuestions.value)
// Loading
Loading.hide()
} else {
console.warn('⚠️ 没有获取到有效的题目详情')
Loading.hide()
message.warning('没有获取到有效的题目详情')
}
} else {
console.warn('⚠️ 考试题目列表为空:', questionsList)
Loading.hide()
message.warning('考试题目列表为空')
}
} else {
console.error('❌ 获取考试题目失败:', questionsResponse.data?.message || questionsResponse.message)
Loading.hide()
message.error(questionsResponse.data?.message || questionsResponse.message || '获取考试题目失败')
}
} else {
@ -2924,12 +2958,13 @@ const submitDiscussionComment = () => {
const comment = {
id: Date.now(),
username: '当前用户',
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-4.0.3&auto=format&fit=crop&w=60&q=80',
username: userName.value,
avatar: userAvatar.value,
content: newComment.value,
time: new Date().toLocaleString(),
likes: 0,
isLiked: false, //
isTeacher: isTeacher.value, //
replies: []
}
@ -3106,10 +3141,26 @@ const handleImageError = (event: Event) => {
}
//
const handleEnrollCourse = (course: any) => {
const handleEnrollCourse = (course: any, _event?: Event) => {
console.log('点击报名课程:', course)
//
window.open(`/course/${course.id}`, '_blank')
// Loading
Loading.show({
text: '正在跳转...',
size: 'medium',
color: '#1890ff'
})
// AI Companion
const targetUrl = `http://localhost:3000/ai-companion?courseId=${course.id}`
console.log('跳转到报名页面:', targetUrl)
// Loading
setTimeout(() => {
window.open(targetUrl, '_blank')
// Loading
Loading.hide()
}, 500)
}
//
@ -3562,8 +3613,36 @@ const scrollToBottom = () => {
//
const loadUserInfo = async () => {
try {
console.log('🔍 获取用户信息...')
const response = await AuthApi.getUserInfo()
if (response.success && response.result?.baseInfo) {
userInfo.value = response.result
userAvatar.value = response.result.baseInfo.avatar || userAvatar.value
userName.value = response.result.baseInfo.realname || response.result.baseInfo.username || '用户'
userRoles.value = response.result.roles || []
isTeacher.value = userRoles.value.includes('teacher')
console.log('✅ 用户信息获取成功:', {
avatar: userAvatar.value,
name: userName.value,
roles: userRoles.value,
isTeacher: isTeacher.value
})
} else {
console.warn('⚠️ 获取用户信息失败:', response)
}
} catch (error) {
console.error('❌ 获取用户信息异常:', error)
}
}
onMounted(() => {
console.log('课程详情页加载完成课程ID:', courseId.value)
loadUserInfo() //
loadCourseDetail()
loadCourseSections()
loadCourseComments() //