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
|