OL-LearnPlatform/src/api/request.ts
2025-08-04 02:13:12 +08:00

583 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// HTTP 请求封装文件
import axios, { AxiosInstance, InternalAxiosRequestConfig, AxiosRequestConfig, AxiosResponse } from 'axios'
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库的消息组件
// 例如naive-ui的 useMessage()
console.log(`[${type.toUpperCase()}] ${message}`)
// 临时使用alert实际项目中应该替换为UI库的消息组件
if (type === 'error') {
alert(`错误: ${message}`)
}
}
// 创建axios实例
const request: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api',
timeout: 30000, // 增加到30秒
headers: {
'Content-Type': 'application/json',
},
})
// 请求拦截器
request.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
// 添加认证token
const userStore = useUserStore()
if (userStore.token) {
config.headers.Authorization = `Bearer ${userStore.token}`
}
// 添加请求时间戳
config.headers['X-Request-Time'] = Date.now().toString()
// 开发环境下打印请求信息
if (import.meta.env.DEV) {
console.log('🚀 Request:', {
url: config.url,
method: config.method,
params: config.params,
data: config.data,
})
}
return config
},
(error) => {
console.error('❌ Request Error:', error)
return Promise.reject(error)
}
)
// 响应拦截器
request.interceptors.response.use(
(response: AxiosResponse<any>) => {
const { data } = response
// 开发环境下打印响应信息
if (import.meta.env.DEV) {
console.log('✅ Response:', {
url: response.config.url,
status: response.status,
data: data,
})
}
// 处理不同的响应格式
let normalizedData: ApiResponse
// 如果响应已经是标准格式
if (data && typeof data === 'object' && 'code' in data && 'message' in data) {
normalizedData = data
} else {
// 如果响应不是标准格式,包装成标准格式
normalizedData = {
code: 200,
message: '请求成功',
data: data
}
}
// 检查业务状态码
if (normalizedData.code === 200 || normalizedData.code === 0) {
// 返回标准化后的响应
response.data = normalizedData
return response
}
// 处理业务错误
const errorMessage = normalizedData.message || '请求失败'
// 不在这里显示错误消息,让组件自己处理
// showMessage(errorMessage, 'error')
// 创建一个包含完整响应信息的错误对象
const error = new Error(errorMessage)
;(error as any).response = {
data: normalizedData,
status: 200 // HTTP状态码是200但业务状态码不是成功
}
return Promise.reject(error)
},
(error) => {
console.error('❌ Response Error:', error)
// 处理HTTP状态码错误
const { response } = error
let errorMessage = '网络错误,请稍后重试'
if (response) {
switch (response.status) {
case 400:
errorMessage = '请求参数错误'
break
case 401:
errorMessage = '登录已过期,请重新登录'
// 清除用户信息,不跳转页面(使用模态框登录)
const userStore = useUserStore()
userStore.logout()
break
case 403:
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:
errorMessage = '数据验证失败'
break
case 429:
errorMessage = '请求过于频繁,请稍后重试'
break
case 500:
errorMessage = '服务器内部错误'
break
case 502:
errorMessage = '网关错误'
break
case 503:
errorMessage = '服务暂时不可用'
break
case 504:
errorMessage = '网关超时'
break
default:
errorMessage = `请求失败 (${response.status})`
}
} else if (error.code === 'ECONNABORTED') {
errorMessage = '请求超时,请检查网络连接'
} else if (error.message === 'Network Error') {
errorMessage = '网络连接失败,请检查网络设置'
}
showMessage(errorMessage, 'error')
return Promise.reject(error)
}
)
// 导入Mock数据
import { mockCourses, getMockCourseSections } from './mock/courses'
// getMockCourseDetail 暂时注释,后续需要时再启用
// Mock数据处理
const handleMockRequest = async <T = any>(url: string, method: string, data?: any): Promise<ApiResponse<T>> => {
console.log('🚀 Mock Request:', { url, method, data })
// 模拟网络延迟
await new Promise(resolve => setTimeout(resolve, 500))
// 登录Mock
if (url === '/users/login' && method === 'POST') {
const { email, phone, password } = data || {}
const loginField = phone || email
// 模拟登录验证
if (loginField && password) {
return {
code: 200,
message: '登录成功',
data: {
user: {
id: 1,
email: email || `${phone}@example.com`,
phone: phone || '123456789',
username: phone || email?.split('@')[0] || 'user',
nickname: '测试用户',
avatar: 'https://via.placeholder.com/100',
role: 'student',
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z'
},
token: 'mock_jwt_token_' + Date.now(),
refreshToken: 'mock_refresh_token_' + Date.now(),
expiresIn: 3600
}
} as ApiResponse<T>
} else {
return {
code: 400,
message: '手机号/邮箱或密码不能为空',
data: null
} as ApiResponse<T>
}
}
// 注册Mock
if (url === '/auth/register' && method === 'POST') {
const { email, password, verificationCode } = data || {}
if (!email || !password) {
return {
code: 400,
message: '邮箱和密码不能为空',
data: null
} as ApiResponse<T>
}
if (!verificationCode) {
return {
code: 400,
message: '验证码不能为空',
data: null
} as ApiResponse<T>
}
return {
code: 200,
message: '注册成功',
data: {
id: 2,
email: email,
username: email.split('@')[0],
nickname: '新用户',
avatar: '',
role: 'student',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
}
} as ApiResponse<T>
}
// 发送验证码Mock
if (url === '/auth/send-verification' && method === 'POST') {
return {
code: 200,
message: '验证码已发送',
data: null
} as ApiResponse<T>
}
// 获取当前用户信息Mock
if (url === '/auth/me' && method === 'GET') {
return {
code: 200,
message: '获取成功',
data: {
id: 1,
email: 'test@example.com',
username: 'test',
nickname: '测试用户',
avatar: '',
role: 'student',
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z'
}
} as ApiResponse<T>
}
// 用户登出Mock
if (url === '/auth/logout' && method === 'POST') {
return {
code: 200,
message: '登出成功',
data: null
} as ApiResponse<T>
}
// 课程详情Mock
if (url === '/lesson/detail' && method === 'GET') {
// 对于GET请求参数直接在data中data就是params对象
const id = data?.id
console.log('课程详情Mock - 获取到的ID:', id, '原始data:', data)
if (!id) {
return {
code: 400,
message: '课程ID必填',
data: null
} as ApiResponse<T>
}
// 根据课程ID提供不同的模拟数据
const courseData = {
1: {
name: 'DeepSeek大语言模型实战应用',
cover: 'https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=400&h=300&fit=crop',
price: '299.00',
school: 'DeepSeek技术学院',
description: '本课程深度聚焦DeepSeek大语言模型的实际应用让每一位学员了解并学习使用DeepSeek结合办公自动化职业岗位标准以实际工作任务为引导强调课程内容的易用性和岗位要求的匹配性。',
position: 'AI技术专家 / 高级讲师'
},
2: {
name: 'Python编程基础与实战',
cover: 'https://images.unsplash.com/photo-1526379095098-d400fd0bf935?w=400&h=300&fit=crop',
price: '199.00',
school: '编程技术学院',
description: '从零开始学习Python编程涵盖基础语法、数据结构、面向对象编程等核心概念通过实际项目练习掌握Python开发技能。',
position: 'Python开发专家 / 资深讲师'
},
3: {
name: 'Web前端开发全栈课程',
cover: 'https://images.unsplash.com/photo-1461749280684-dccba630e2f6?w=400&h=300&fit=crop',
price: '399.00',
school: '前端技术学院',
description: '全面学习现代Web前端开发技术包括HTML5、CSS3、JavaScript、Vue.js、React等主流框架培养全栈开发能力。',
position: '前端架构师 / 技术总监'
}
}
const currentCourse = courseData[id as keyof typeof courseData] || courseData[1]
// 模拟课程详情数据
return {
code: 0,
message: '查询课程详情成功',
data: {
id: parseInt(id),
name: currentCourse.name,
cover: currentCourse.cover,
categoryId: 1,
price: currentCourse.price,
school: currentCourse.school,
description: currentCourse.description,
teacherId: 1,
outline: '<div><h4>课程大纲:</h4><ul><li><strong>第一章:基础入门</strong><br/>- 环境搭建与配置<br/>- 基本概念理解<br/>- 实践操作演示</li><li><strong>第二章:核心技能</strong><br/>- 核心功能详解<br/>- 实际应用场景<br/>- 案例分析讲解</li><li><strong>第三章:高级应用</strong><br/>- 进阶技巧掌握<br/>- 项目实战演练<br/>- 问题解决方案</li></ul></div>',
prerequisite: '具备基本的计算机操作能力',
target: '掌握核心技能,能够在实际工作中熟练应用',
arrangement: '理论与实践相结合,循序渐进的学习方式',
startTime: '2025-01-26 10:13:17',
endTime: '2025-03-26 10:13:17',
revision: 1,
position: currentCourse.position,
createdAt: 1737944724,
updatedAt: 1737944724,
updatedTime: null
}
} as ApiResponse<T>
}
// 课程列表Mock
if (url === '/lesson/list' && method === 'GET') {
return {
code: 0,
message: '查询课程列表成功',
data: {
list: mockCourses,
total: mockCourses.length
}
} as ApiResponse<T>
}
// 课程章节列表Mock
if (url === '/lesson/section/list' && method === 'GET') {
const lessonId = data?.lesson_id
console.log('课程章节Mock - 获取到的lesson_id:', lessonId, '原始data:', data)
if (!lessonId) {
return {
code: 400,
message: '课程ID必填',
data: null
} as ApiResponse<T>
}
const mockSections = getMockCourseSections(parseInt(lessonId))
return {
code: 200,
message: '获取成功',
data: {
list: mockSections,
total: mockSections.length
},
timestamp: new Date().toISOString()
} as ApiResponse<T>
}
// 默认404响应
return {
code: 404,
message: '接口不存在',
data: null
} as ApiResponse<T>
}
// 重试机制
const retryRequest = async <T = any>(
requestFn: () => Promise<T>,
maxRetries: number = 2,
delay: number = 1000
): Promise<T> => {
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 async get<T = any>(
url: string,
params?: any,
config?: AxiosRequestConfig
): Promise<ApiResponse<T>> {
// 检查是否启用Mock
if (import.meta.env.VITE_ENABLE_MOCK === 'true') {
return handleMockRequest<T>(url, 'GET', params)
}
try {
return await retryRequest(() => request.get(url, { params, ...config }))
} catch (error) {
console.warn('API请求失败降级到Mock数据:', error)
// 如果真实API失败降级到Mock数据
return handleMockRequest<T>(url, 'GET', params)
}
}
// POST 请求
static async post<T = any>(
url: string,
data?: any,
config?: AxiosRequestConfig
): Promise<ApiResponse<T>> {
// 检查是否启用Mock
if (import.meta.env.VITE_ENABLE_MOCK === 'true') {
return handleMockRequest<T>(url, 'POST', data)
}
try {
return await retryRequest(() => request.post(url, data, config))
} catch (error) {
console.warn('API请求失败降级到Mock数据:', error)
// 如果真实API失败降级到Mock数据
return handleMockRequest<T>(url, 'POST', data)
}
}
// PUT 请求
static put<T = any>(
url: string,
data?: any,
config?: AxiosRequestConfig
): Promise<ApiResponse<T>> {
return request.put(url, data, config)
}
// PATCH 请求
static patch<T = any>(
url: string,
data?: any,
config?: AxiosRequestConfig
): Promise<ApiResponse<T>> {
return request.patch(url, data, config)
}
// DELETE 请求
static delete<T = any>(
url: string,
params?: any,
config?: AxiosRequestConfig
): Promise<ApiResponse<T>> {
return request.delete(url, { params, ...config })
}
// 文件上传
static upload<T = any>(
url: string,
file: File,
onProgress?: (progress: number) => void,
config?: AxiosRequestConfig
): Promise<ApiResponse<T>> {
const formData = new FormData()
formData.append('file', file)
return request.post(url, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
onUploadProgress: (progressEvent) => {
if (onProgress && progressEvent.total) {
const progress = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
)
onProgress(progress)
}
},
...config,
})
}
// 文件下载
static download(
url: string,
filename?: string,
params?: any,
config?: AxiosRequestConfig
): Promise<void> {
return request
.get(url, {
params,
responseType: 'blob',
...config,
})
.then((response: any) => {
const blob = new Blob([response.data])
const downloadUrl = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = downloadUrl
link.download = filename || 'download'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(downloadUrl)
})
}
// API健康检查
static async healthCheck(): Promise<boolean> {
try {
const response = await request.get('/health', { timeout: 5000 })
return response.status === 200
} catch (error) {
console.warn('API健康检查失败:', error)
return false
}
}
}
export default request