528 lines
16 KiB
TypeScript
528 lines
16 KiB
TypeScript
// HTTP 请求封装文件
|
||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
||
import { useUserStore } from '@/stores/user'
|
||
import router from '@/router'
|
||
import type { ApiResponse } from './types'
|
||
|
||
// 消息提示函数 - 使用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: 10000,
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
})
|
||
|
||
// 请求拦截器
|
||
request.interceptors.request.use(
|
||
(config: AxiosRequestConfig) => {
|
||
// 添加认证token
|
||
const userStore = useUserStore()
|
||
if (userStore.token) {
|
||
config.headers = {
|
||
...config.headers,
|
||
Authorization: `Bearer ${userStore.token}`,
|
||
}
|
||
}
|
||
|
||
// 添加请求时间戳
|
||
config.headers = {
|
||
...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<ApiResponse>) => {
|
||
const { data } = response
|
||
|
||
// 开发环境下打印响应信息
|
||
if (import.meta.env.DEV) {
|
||
console.log('✅ Response:', {
|
||
url: response.config.url,
|
||
status: response.status,
|
||
data: data,
|
||
})
|
||
}
|
||
|
||
// 检查业务状态码
|
||
if (data.code === 200 || data.code === 0) {
|
||
return data
|
||
}
|
||
|
||
// 处理业务错误
|
||
const errorMessage = data.message || '请求失败'
|
||
// 不在这里显示错误消息,让组件自己处理
|
||
// showMessage(errorMessage, 'error')
|
||
|
||
// 创建一个包含完整响应信息的错误对象
|
||
const error = new Error(errorMessage)
|
||
;(error as any).response = {
|
||
data: data,
|
||
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:
|
||
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数据处理
|
||
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 === '/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') {
|
||
// 模拟课程列表数据
|
||
const mockCourses = [
|
||
{
|
||
id: 1,
|
||
name: "暑期名师领学,提高班级教学质量!高效冲分指南",
|
||
cover: "https://images.unsplash.com/photo-1516321318423-f06f85e504b3?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80",
|
||
categoryId: 1,
|
||
price: "99.00",
|
||
school: "名师工作室",
|
||
description: "本课程深度聚焦问题,让每一位教师了解并学习使用DeepSeek,结合办公自动化职业岗位标准,以实际工作任务为引导,强调课程内容的易用性和岗位要求的匹配性。",
|
||
teacherId: 1,
|
||
outline: "课程大纲详细内容...",
|
||
prerequisite: "具备基本的计算机操作能力",
|
||
target: "掌握核心技能,能够在实际工作中熟练应用",
|
||
arrangement: "理论与实践相结合,循序渐进的学习方式",
|
||
startTime: "2025-01-26 10:13:17",
|
||
endTime: "2025-03-26 10:13:17",
|
||
revision: 1,
|
||
position: "高级讲师",
|
||
createdAt: 1737944724,
|
||
updatedAt: 1737944724,
|
||
updatedTime: null
|
||
},
|
||
{
|
||
id: 2,
|
||
name: "计算机二级考前冲刺班",
|
||
cover: "https://images.unsplash.com/photo-1517077304055-6e89abbf09b0?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80",
|
||
categoryId: 2,
|
||
price: "199.00",
|
||
school: "计算机学院",
|
||
description: "备考计算机二级,名师带你高效复习,掌握考试重点,轻松通过考试。",
|
||
teacherId: 2,
|
||
outline: "考试大纲详细解析...",
|
||
prerequisite: "具备基本的计算机基础知识",
|
||
target: "顺利通过计算机二级考试",
|
||
arrangement: "考点精讲+真题演练+模拟考试",
|
||
startTime: "2025-02-01 09:00:00",
|
||
endTime: "2025-02-28 18:00:00",
|
||
revision: 1,
|
||
position: "副教授",
|
||
createdAt: 1737944724,
|
||
updatedAt: 1737944724,
|
||
updatedTime: null
|
||
},
|
||
{
|
||
id: 3,
|
||
name: "摆脱哑巴英语,流利口语训练营",
|
||
cover: "https://images.unsplash.com/photo-1434030216411-0b793f4b4173?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80",
|
||
categoryId: 3,
|
||
price: "299.00",
|
||
school: "外语学院",
|
||
description: "专业外教授课,情景式教学,让你在短时间内突破口语障碍,自信开口说英语。",
|
||
teacherId: 3,
|
||
outline: "口语训练系统课程...",
|
||
prerequisite: "具备基本的英语基础",
|
||
target: "能够流利进行日常英语对话",
|
||
arrangement: "外教一对一+小班练习+实战演练",
|
||
startTime: "2025-02-15 19:00:00",
|
||
endTime: "2025-04-15 21:00:00",
|
||
revision: 1,
|
||
position: "外籍教师",
|
||
createdAt: 1737944724,
|
||
updatedAt: 1737944724,
|
||
updatedTime: null
|
||
}
|
||
]
|
||
|
||
return {
|
||
code: 0,
|
||
message: '查询课程列表成功',
|
||
data: {
|
||
list: mockCourses,
|
||
total: mockCourses.length
|
||
}
|
||
} as ApiResponse<T>
|
||
}
|
||
|
||
|
||
|
||
// 默认404响应
|
||
return {
|
||
code: 404,
|
||
message: '接口不存在',
|
||
data: null
|
||
} as ApiResponse<T>
|
||
}
|
||
|
||
// 请求方法封装
|
||
export class ApiRequest {
|
||
// GET 请求
|
||
static 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)
|
||
}
|
||
return request.get(url, { params, ...config })
|
||
}
|
||
|
||
// POST 请求
|
||
static 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)
|
||
}
|
||
return request.post(url, data, config)
|
||
}
|
||
|
||
// 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)
|
||
})
|
||
}
|
||
}
|
||
|
||
export default request
|