diff --git a/src/api/modules/activity.ts b/src/api/modules/activity.ts new file mode 100644 index 0000000..e3068f6 --- /dev/null +++ b/src/api/modules/activity.ts @@ -0,0 +1,228 @@ +import request from '../request' +import type { ApiResponse } from '../types' + +// 活动相关的类型定义 +export interface Activity { + id: number + title: string + subtitle: string + description: string + startDate: string + endDate: string + status: 'upcoming' | 'ongoing' | 'ended' + registrationCount: number + likeCount: number + categories: string[] + timeline: ActivityTimelineItem[] + content: ActivityContentItem[] + images: string[] + organizer: string + coOrganizer?: string +} + +export interface ActivityTimelineItem { + date: string + title: string + description: string +} + +export interface ActivityContentItem { + text: string + downloadLink?: string +} + +export interface ActivityListItem { + id: number + title: string + subtitle: string + courseTitle: string + schedule: string + duration: string + students: string + price: string + status: string + image: string +} + +// 活动API类 +export class ActivityApi { + // 获取活动列表 + static async getActivities(): Promise> { + try { + // 模拟API调用 - 实际项目中这里应该调用真实的API + const mockData: ActivityListItem[] = [ + { + id: 1, + title: '计算机二级', + subtitle: 'C语言讲练综合班', + courseTitle: '计算机二级C语言程序设计证书', + schedule: '开课时间:2025.07.26-2025.09.28', + duration: '适合年级:高校本科生', + students: '已报名:1468/2000', + price: '免费', + status: '进行中', + image: '/images/activity/活动图1.png' + }, + { + id: 2, + title: 'AI创新实践', + subtitle: '全国青少年人工智能创新实践活动', + courseTitle: '与AI共创未来 - 2025年全国青少年人工智能创新实践活动', + schedule: '开课时间:2025.01.26-2025.09.22', + duration: '适合年级:中小学生', + students: '已报名:541/1000', + price: '免费', + status: '报名中', + image: '/images/activity/活动图2.png' + } + ] + + return { + code: 0, + message: '获取活动列表成功', + data: mockData + } + } catch (error) { + console.error('获取活动列表失败:', error) + return { + code: -1, + message: '获取活动列表失败', + data: [] + } + } + } + + // 根据ID获取活动详情 + static async getActivityById(id: number): Promise> { + try { + // 模拟API调用 - 实际项目中这里应该调用真实的API + const mockActivity: Activity = { + id: id, + title: '"与AI共创未来"', + subtitle: '2025年全国青少年人工智能创新实践活动', + description: '本活动是面向全国,以青少年为主体的人工智能DeepSeek,该活动以公司自主研发的创新成果,以实践工作作为引导,向青少年群体宣传推广人工智能技术,让青少年了解人工智能技术的发展历程,体验人工智能技术带来的便利,培养青少年对人工智能技术的兴趣,提升青少年的科学素养,为我国人工智能技术的发展培养后备人才。在此基础上,通过实践活动的开展,让青少年在实践中学习,在学习中实践。', + startDate: '2025-01-26', + endDate: '2025-09-22', + status: 'ongoing', + registrationCount: 541, + likeCount: 2377, + categories: [ + 'AI少年明日程', + 'AI实习程序员', + 'AI工程实践营', + 'AI智能未来营', + 'AI编程教学创新实验' + ], + timeline: [ + { + date: '7月1日-7月24日', + title: '青少年编程技能实践活动报名参赛', + description: '' + }, + { + date: '7月25日-8月31日', + title: '青少年编程技能竞赛活动、开发实践活动', + description: '' + }, + { + date: '8月31日-9月5日', + title: '实践作品提交和技能竞赛活动开展', + description: '' + }, + { + date: '9月6日-9月20日', + title: '优秀作品公示期', + description: '' + }, + { + date: '9月21日-9月27日', + title: '人工智能实践活动总结活动举办', + description: '' + } + ], + content: [ + { + text: '中国科协青少年科技中心、中国青少年科技教育工作者协会等', + downloadLink: undefined + }, + { + text: '作品提交规则说明', + downloadLink: '#' + }, + { + text: '中小学教师人工智能素养提升在线学习平台(网址)', + downloadLink: '#' + }, + { + text: '作品提交 报名表', + downloadLink: '#' + } + ], + images: [ + '/images/activity/活动图1.png', + '/images/activity/活动图2.png', + '/images/activity/活动图3.png' + ], + organizer: '中国科协青少年科技中心', + coOrganizer: '中国青少年科技教育工作者协会、上海人工智能实验室' + } + + return { + code: 0, + message: '获取活动详情成功', + data: mockActivity + } + } catch (error) { + console.error('获取活动详情失败:', error) + return { + code: -1, + message: '获取活动详情失败', + data: {} as Activity + } + } + } + + // 报名活动 + static async registerActivity(activityId: number): Promise> { + try { + // 模拟API调用 + console.log('报名活动:', activityId) + + return { + code: 0, + message: '报名成功', + data: { success: true } + } + } catch (error) { + console.error('报名活动失败:', error) + return { + code: -1, + message: '报名失败', + data: { success: false } + } + } + } + + // 提交作品 + static async submitWork(activityId: number, workData: any): Promise> { + try { + // 模拟API调用 + console.log('提交作品:', activityId, workData) + + return { + code: 0, + message: '作品提交成功', + data: { success: true } + } + } catch (error) { + console.error('提交作品失败:', error) + return { + code: -1, + message: '作品提交失败', + data: { success: false } + } + } + } +} + +export default ActivityApi diff --git a/src/api/modules/auth.ts b/src/api/modules/auth.ts index 8a54e77..f7dec1d 100644 --- a/src/api/modules/auth.ts +++ b/src/api/modules/auth.ts @@ -17,34 +17,167 @@ export class AuthApi { // 用户登录 static async login(data: LoginRequest): Promise> { try { - // 调用后端API - const response = await ApiRequest.post('/users/login', data) + console.log('🚀 发送登录请求:', { url: '/users/login', data: { ...data, password: '***' } }) - // 适配后端响应格式为前端期望的格式 - const adaptedResponse: ApiResponse = { - code: response.code, - message: response.message, - data: { - user: { - id: response.data.id, // 使用后端返回的用户ID - email: data.email || '', - phone: data.phone || '', - username: data.phone || data.email?.split('@')[0] || 'user', - nickname: '用户', - avatar: '', - role: 'student', - status: 'active', - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString() - }, - token: response.data.token, - refreshToken: '', // 后端没有返回,使用空字符串 - expiresIn: 3600 // 默认1小时,可以根据expires字段计算 + // 调用后端API + const response = await ApiRequest.post('/users/login', data) + + console.log('🔍 Login API Response:', response) + console.log('🔍 Response Code:', response.code) + console.log('🔍 Response Data:', response.data) + console.log('🔍 Response Data Type:', typeof response.data) + + // 处理响应格式问题 - 如果code和message是undefined,可能是响应拦截器的问题 + let actualCode = response.code + let actualMessage = response.message + let actualData = response.data + + // 如果response.code是undefined,检查response.data是否包含完整的API响应 + if (actualCode === undefined && actualData && typeof actualData === 'object') { + if ('code' in actualData && 'message' in actualData && 'data' in actualData) { + // 这种情况下,真正的API响应被包装在了response.data中 + actualCode = actualData.code + actualMessage = actualData.message + actualData = actualData.data + console.log('🔧 修正后的响应:', { code: actualCode, message: actualMessage, data: actualData }) } } - return adaptedResponse + // 检查响应格式并适配 + if (actualCode === 200 || actualCode === 0) { + // 如果后端返回的是完整的用户信息格式(mock数据格式) + if (actualData && actualData.user && actualData.token) { + return { + code: actualCode, + message: actualMessage, + data: actualData + } as ApiResponse + } + + // 如果后端返回的是真实API格式(包含token, timestamp, expires) + if (actualData && actualData.token && actualData.timestamp) { + const adaptedResponse: ApiResponse = { + code: actualCode, + message: actualMessage || '登录成功', + data: { + user: { + id: 1, // 真实API没有返回用户ID,使用默认值 + email: data.email || '', + phone: data.phone || '', + username: data.phone || data.email?.split('@')[0] || 'user', + nickname: '用户', + avatar: '', + role: 'student', + status: 'active', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString() + }, + token: actualData.token, + refreshToken: '', // 真实API没有返回refreshToken + expiresIn: 3600 // 默认1小时过期 + } + } + return adaptedResponse + } + + // 如果后端返回的是简化格式(BackendLoginResponse) + if (actualData && actualData.token) { + const adaptedResponse: ApiResponse = { + code: actualCode, + message: actualMessage || '登录成功', + data: { + user: { + id: actualData.id || 1, + email: data.email || '', + phone: data.phone || '', + username: data.phone || data.email?.split('@')[0] || 'user', + nickname: '用户', + avatar: '', + role: 'student', + status: 'active', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString() + }, + token: actualData.token, + refreshToken: actualData.refreshToken || '', + expiresIn: actualData.expiresIn || 3600 + } + } + return adaptedResponse + } + + // 如果只有token字段,直接使用token + if (typeof actualData === 'string') { + const adaptedResponse: ApiResponse = { + code: actualCode, + message: actualMessage || '登录成功', + data: { + user: { + id: 1, + email: data.email || '', + phone: data.phone || '', + username: data.phone || data.email?.split('@')[0] || 'user', + nickname: '用户', + avatar: '', + role: 'student', + status: 'active', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString() + }, + token: actualData, + refreshToken: '', + expiresIn: 3600 + } + } + return adaptedResponse + } + + // 如果data是null或undefined,但响应成功,可能是某些API的特殊情况 + if (!actualData) { + console.warn('⚠️ API返回成功但data为空,可能需要检查API实现') + throw new Error('登录成功但未返回用户信息') + } + + // 尝试处理其他可能的响应格式 + console.warn('⚠️ 未知的响应格式,尝试通用处理:', actualData) + + // 如果data是对象但不包含预期字段,尝试创建默认响应 + if (typeof actualData === 'object') { + const adaptedResponse: ApiResponse = { + code: actualCode, + message: actualMessage || '登录成功', + data: { + user: { + id: actualData.id || actualData.userId || 1, + email: data.email || '', + phone: data.phone || '', + username: data.phone || data.email?.split('@')[0] || 'user', + nickname: actualData.nickname || actualData.name || '用户', + avatar: actualData.avatar || '', + role: actualData.role || 'student', + status: 'active', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString() + }, + token: actualData.accessToken || actualData.access_token || 'temp_token_' + Date.now(), + refreshToken: actualData.refreshToken || actualData.refresh_token || '', + expiresIn: actualData.expiresIn || actualData.expires_in || 3600 + } + } + return adaptedResponse + } + } + + // 如果响应格式不符合预期,抛出错误 + console.error('🚨 Unexpected response format:', { + code: actualCode, + message: actualMessage, + data: actualData, + dataType: typeof actualData + }) + throw new Error(actualMessage || '登录响应格式错误') } catch (error) { + console.error('🚨 Login API Error:', error) throw error } } @@ -56,7 +189,24 @@ export class AuthApi { // 用户登出 static logout(): Promise> { - return ApiRequest.post('/auth/logout') + // 尝试多个可能的登出端点 + return ApiRequest.post('/auth/logout').catch(async (error) => { + // 如果 /auth/logout 失败,尝试其他可能的端点 + if (error.response?.status === 404) { + try { + return await ApiRequest.post('/users/logout') + } catch (secondError) { + // 如果所有端点都失败,返回成功(客户端登出) + console.log('所有登出端点都不存在,执行客户端登出') + return { + code: 200, + message: '登出成功', + data: null + } + } + } + throw error + }) } // 刷新Token diff --git a/src/api/modules/course.ts b/src/api/modules/course.ts index 6b8ed79..b488f74 100644 --- a/src/api/modules/course.ts +++ b/src/api/modules/course.ts @@ -198,13 +198,22 @@ export class CourseApi { } return adaptedResponse - } catch (error) { + } catch (error: any) { console.error('课程API调用失败:', error) + let errorMessage = '获取课程列表失败' + if (error.code === 'ECONNABORTED') { + errorMessage = '请求超时,请检查网络连接' + } else if (error.message === 'Network Error') { + errorMessage = '网络连接失败,请检查网络设置' + } else if (error.message?.includes('网络')) { + errorMessage = error.message + } + // 返回空数据而不是抛出错误,确保应用不会崩溃 return { code: 500, - message: '获取课程列表失败', + message: errorMessage, data: { list: [], total: 0, diff --git a/src/api/request.ts b/src/api/request.ts index 181c4ff..257d7dd 100644 --- a/src/api/request.ts +++ b/src/api/request.ts @@ -4,6 +4,14 @@ import { useUserStore } from '@/stores/user' // import router from '@/router' import type { ApiResponse } from './types' +// 网络状态检测 +const checkNetworkStatus = (): boolean => { + if (typeof navigator !== 'undefined' && 'onLine' in navigator) { + return navigator.onLine + } + return true // 默认认为网络可用 +} + // 消息提示函数 - 使用window.alert作为fallback,实际项目中应该使用UI库的消息组件 const showMessage = (message: string, type: 'success' | 'error' | 'warning' | 'info' = 'info') => { // 这里可以替换为你使用的UI库的消息组件 @@ -19,7 +27,7 @@ const showMessage = (message: string, type: 'success' | 'error' | 'warning' | 'i // 创建axios实例 const request: AxiosInstance = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api', - timeout: 10000, + timeout: 30000, // 增加到30秒 headers: { 'Content-Type': 'application/json', }, @@ -57,7 +65,7 @@ request.interceptors.request.use( // 响应拦截器 request.interceptors.response.use( - (response: AxiosResponse) => { + (response: AxiosResponse) => { const { data } = response // 开发环境下打印响应信息 @@ -69,20 +77,37 @@ request.interceptors.response.use( }) } + // 处理不同的响应格式 + let normalizedData: ApiResponse + + // 如果响应已经是标准格式 + if (data && typeof data === 'object' && 'code' in data && 'message' in data) { + normalizedData = data + } else { + // 如果响应不是标准格式,包装成标准格式 + normalizedData = { + code: 200, + message: '请求成功', + data: data + } + } + // 检查业务状态码 - if (data.code === 200 || data.code === 0) { + if (normalizedData.code === 200 || normalizedData.code === 0) { + // 返回标准化后的响应 + response.data = normalizedData return response } // 处理业务错误 - const errorMessage = data.message || '请求失败' + const errorMessage = normalizedData.message || '请求失败' // 不在这里显示错误消息,让组件自己处理 // showMessage(errorMessage, 'error') // 创建一个包含完整响应信息的错误对象 const error = new Error(errorMessage) ;(error as any).response = { - data: data, + data: normalizedData, status: 200 // HTTP状态码是200,但业务状态码不是成功 } return Promise.reject(error) @@ -109,6 +134,13 @@ request.interceptors.response.use( errorMessage = '没有权限访问' break case 404: + // 对于登出接口的404错误,不显示错误消息 + if (error.config?.url?.includes('/logout')) { + console.log('登出接口不存在,这是正常的') + return Promise.resolve({ + data: { code: 200, message: '登出成功', data: null } + }) + } errorMessage = '请求的资源不存在' break case 422: @@ -249,6 +281,15 @@ const handleMockRequest = async (url: string, method: string, data?: an } as ApiResponse } + // 用户登出Mock + if (url === '/auth/logout' && method === 'POST') { + return { + code: 200, + message: '登出成功', + data: null + } as ApiResponse + } + // 课程详情Mock if (url === '/lesson/detail' && method === 'GET') { // 对于GET请求,参数直接在data中(data就是params对象) @@ -410,10 +451,44 @@ const handleMockRequest = async (url: string, method: string, data?: an } as ApiResponse } +// 重试机制 +const retryRequest = async ( + requestFn: () => Promise, + maxRetries: number = 2, + delay: number = 1000 +): Promise => { + let lastError: any + + for (let i = 0; i <= maxRetries; i++) { + try { + // 检查网络状态 + if (!checkNetworkStatus()) { + throw new Error('网络连接不可用,请检查网络设置') + } + + return await requestFn() + } catch (error: any) { + lastError = error + + // 如果是最后一次重试,或者不是网络错误,直接抛出 + if (i === maxRetries || (error.code !== 'ECONNABORTED' && error.message !== 'Network Error' && !error.message?.includes('网络'))) { + throw error + } + + console.log(`请求失败,${delay}ms后进行第${i + 1}次重试...`) + console.log('错误详情:', error.message) + await new Promise(resolve => setTimeout(resolve, delay)) + delay *= 2 // 指数退避 + } + } + + throw lastError +} + // 请求方法封装 export class ApiRequest { // GET 请求 - static get( + static async get( url: string, params?: any, config?: AxiosRequestConfig @@ -422,11 +497,12 @@ export class ApiRequest { if (import.meta.env.VITE_ENABLE_MOCK === 'true') { return handleMockRequest(url, 'GET', params) } - return request.get(url, { params, ...config }) + + return retryRequest(() => request.get(url, { params, ...config })) } // POST 请求 - static post( + static async post( url: string, data?: any, config?: AxiosRequestConfig @@ -435,7 +511,8 @@ export class ApiRequest { if (import.meta.env.VITE_ENABLE_MOCK === 'true') { return handleMockRequest(url, 'POST', data) } - return request.post(url, data, config) + + return retryRequest(() => request.post(url, data, config)) } // PUT 请求 @@ -516,6 +593,17 @@ export class ApiRequest { window.URL.revokeObjectURL(downloadUrl) }) } + + // API健康检查 + static async healthCheck(): Promise { + try { + const response = await request.get('/health', { timeout: 5000 }) + return response.status === 200 + } catch (error) { + console.warn('API健康检查失败:', error) + return false + } + } } export default request diff --git a/src/components/auth/LoginModal.vue b/src/components/auth/LoginModal.vue index b38cc90..6d2b47a 100644 --- a/src/components/auth/LoginModal.vue +++ b/src/components/auth/LoginModal.vue @@ -123,6 +123,8 @@ const handleLogin = async () => { isLoading.value = true try { + console.log('🚀 开始登录:', { account: loginForm.account, password: '***' }) + // 判断输入的是手机号还是邮箱 const isPhone = /^[0-9]+$/.test(loginForm.account) @@ -132,6 +134,8 @@ const handleLogin = async () => { password: loginForm.password }) + console.log('✅ 登录响应:', response) + if (response.code === 200 || response.code === 0) { const { user, token, refreshToken } = response.data @@ -141,7 +145,7 @@ const handleLogin = async () => { // 保存到本地存储 localStorage.setItem('token', token) - localStorage.setItem('refreshToken', refreshToken) + localStorage.setItem('refreshToken', refreshToken || '') localStorage.setItem('user', JSON.stringify(user)) // 如果选择了记住我,设置更长的过期时间 @@ -158,11 +162,12 @@ const handleLogin = async () => { loginForm.password = '' loginForm.remember = false } else { + console.error('❌ 登录失败 - 响应码错误:', response) message.error(response.message || '登录失败') } } catch (error: any) { - console.error('登录失败:', error) + console.error('❌ 登录异常:', error) // 处理不同类型的错误 if (error.response?.status === 401) { diff --git a/src/composables/useNetworkStatus.ts b/src/composables/useNetworkStatus.ts new file mode 100644 index 0000000..bd6dec0 --- /dev/null +++ b/src/composables/useNetworkStatus.ts @@ -0,0 +1,55 @@ +import { ref, onMounted, onUnmounted } from 'vue' + +export function useNetworkStatus() { + const isOnline = ref(navigator.onLine) + const isSlowConnection = ref(false) + + const updateOnlineStatus = () => { + isOnline.value = navigator.onLine + console.log('网络状态变化:', isOnline.value ? '在线' : '离线') + } + + const checkConnectionSpeed = async () => { + if (!isOnline.value) return + + try { + const startTime = Date.now() + const response = await fetch('/favicon.ico', { + method: 'HEAD', + cache: 'no-cache' + }) + const endTime = Date.now() + const duration = endTime - startTime + + // 如果请求时间超过3秒,认为是慢连接 + isSlowConnection.value = duration > 3000 + console.log('连接速度检测:', duration + 'ms', isSlowConnection.value ? '慢连接' : '正常') + } catch (error) { + console.warn('连接速度检测失败:', error) + isSlowConnection.value = true + } + } + + onMounted(() => { + window.addEventListener('online', updateOnlineStatus) + window.addEventListener('offline', updateOnlineStatus) + + // 初始检测连接速度 + checkConnectionSpeed() + + // 每30秒检测一次连接速度 + const speedCheckInterval = setInterval(checkConnectionSpeed, 30000) + + onUnmounted(() => { + window.removeEventListener('online', updateOnlineStatus) + window.removeEventListener('offline', updateOnlineStatus) + clearInterval(speedCheckInterval) + }) + }) + + return { + isOnline, + isSlowConnection, + checkConnectionSpeed + } +} diff --git a/src/router/index.ts b/src/router/index.ts index f904aa4..ba6ace0 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -13,6 +13,7 @@ import LearningPaths from '@/views/LearningPaths.vue' import Faculty from '@/views/Faculty.vue' import Resources from '@/views/Resources.vue' import Activities from '@/views/Activities.vue' +import ActivityDetail from '@/views/ActivityDetail.vue' import TestSections from '@/views/TestSections.vue' import VideoTest from '@/views/VideoTest.vue' @@ -101,6 +102,14 @@ const routes: RouteRecordRaw[] = [ title: '全部活动' } }, + { + path: '/activity/:id', + name: 'ActivityDetail', + component: ActivityDetail, + meta: { + title: '活动详情' + } + }, { path: '/test-sections', name: 'TestSections', diff --git a/src/stores/user.ts b/src/stores/user.ts index 75d725a..64ab0aa 100644 --- a/src/stores/user.ts +++ b/src/stores/user.ts @@ -30,10 +30,21 @@ export const useUserStore = defineStore('user', () => { const logout = async () => { try { - // 调用登出API - await AuthApi.logout() - } catch (error) { - console.error('登出API调用失败:', error) + // 尝试调用登出API(如果存在的话) + // 但不让API失败阻止登出过程 + if (token.value) { + try { + await AuthApi.logout() + console.log('服务器端登出成功') + } catch (error: any) { + // 如果是404错误,说明后端没有登出接口,这是正常的 + if (error.response?.status === 404) { + console.log('后端无登出接口,仅执行客户端登出') + } else { + console.warn('登出API调用失败,但继续执行客户端登出:', error.message) + } + } + } } finally { // 无论API调用是否成功,都清除本地数据 user.value = null @@ -42,6 +53,7 @@ export const useUserStore = defineStore('user', () => { localStorage.removeItem('refreshToken') localStorage.removeItem('user') localStorage.removeItem('rememberMe') + console.log('用户已登出') } } diff --git a/src/views/Activities.vue b/src/views/Activities.vue index 8a578d3..8bb31d5 100644 --- a/src/views/Activities.vue +++ b/src/views/Activities.vue @@ -96,6 +96,9 @@ + +