583 lines
17 KiB
TypeScript
583 lines
17 KiB
TypeScript
// 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
|