Initial commit: One Pipe System
完整的管理系统,包含账户管理、卡片管理、套餐管理、财务管理等功能模块。 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
195
src/utils/http/README.md
Normal file
195
src/utils/http/README.md
Normal file
@@ -0,0 +1,195 @@
|
||||
# HTTP 请求使用说明
|
||||
|
||||
## 🚀 基础用法
|
||||
|
||||
```typescript
|
||||
import request from '@/utils/http'
|
||||
import type { BaseResponse } from '@/types/api'
|
||||
|
||||
// 基础GET请求
|
||||
const response = await request.get<BaseResponse<User[]>>({
|
||||
url: '/api/users'
|
||||
})
|
||||
|
||||
// 基础POST请求
|
||||
const result = await request.post<BaseResponse<User>>({
|
||||
url: '/api/users',
|
||||
data: { name: 'John', email: 'john@example.com' }
|
||||
})
|
||||
```
|
||||
|
||||
## ⚙️ 使用自定义请求选项
|
||||
|
||||
```typescript
|
||||
// 不显示错误消息
|
||||
await request.get({
|
||||
url: '/api/data',
|
||||
requestOptions: {
|
||||
errorMessageMode: 'none'
|
||||
}
|
||||
})
|
||||
|
||||
// 不携带token的请求
|
||||
await request.get({
|
||||
url: '/api/public/data',
|
||||
requestOptions: {
|
||||
withToken: false
|
||||
}
|
||||
})
|
||||
|
||||
// 添加时间戳防止缓存
|
||||
await request.get({
|
||||
url: '/api/data',
|
||||
requestOptions: {
|
||||
joinTime: true // 会在URL后添加 ?_t=timestamp
|
||||
}
|
||||
})
|
||||
|
||||
// 使用不同的API地址
|
||||
await request.post({
|
||||
url: '/upload',
|
||||
data: formData,
|
||||
requestOptions: {
|
||||
apiUrl: 'https://upload.example.com'
|
||||
}
|
||||
})
|
||||
|
||||
// 组合使用
|
||||
await request.get({
|
||||
url: '/api/sensitive-data',
|
||||
requestOptions: {
|
||||
withToken: true,
|
||||
errorMessageMode: 'modal',
|
||||
joinTime: true
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## 📋 RequestOptions 配置项
|
||||
|
||||
| 配置项 | 类型 | 默认值 | 说明 |
|
||||
| ------------------------ | -------------------------------- | ----------- | ------------------- |
|
||||
| `errorMessageMode` | `'none' \| 'modal' \| 'message'` | `'message'` | 错误消息显示方式 |
|
||||
| `withToken` | `boolean` | `true` | 是否携带认证token |
|
||||
| `joinTime` | `boolean` | `false` | 是否添加时间戳 |
|
||||
| `apiUrl` | `string` | - | 自定义API基础地址 |
|
||||
| `joinParamsToUrl` | `boolean` | `false` | 是否将参数拼接到URL |
|
||||
| `formatDate` | `boolean` | `false` | 是否格式化日期 |
|
||||
| `isTransformResponse` | `boolean` | `true` | 是否转换响应数据 |
|
||||
| `isReturnNativeResponse` | `boolean` | `false` | 是否返回原生响应 |
|
||||
| `joinPrefix` | `boolean` | `true` | 是否添加前缀 |
|
||||
| `ignoreCancelToken` | `boolean` | `false` | 是否忽略取消令牌 |
|
||||
|
||||
## 🔄 迁移指南
|
||||
|
||||
### 从旧版本迁移
|
||||
|
||||
```typescript
|
||||
// 旧写法 ❌
|
||||
await request.get({
|
||||
url: '/api/data'
|
||||
})
|
||||
|
||||
// 新写法 ✅ (向后兼容)
|
||||
await request.get({
|
||||
url: '/api/data',
|
||||
requestOptions: {
|
||||
errorMessageMode: 'message' // 可选配置
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### 错误处理方式对比
|
||||
|
||||
```typescript
|
||||
// 默认错误处理 (ElMessage)
|
||||
await request.get({ url: '/api/data' })
|
||||
|
||||
// 静默请求 (不显示错误消息)
|
||||
await request.get({
|
||||
url: '/api/data',
|
||||
requestOptions: { errorMessageMode: 'none' }
|
||||
})
|
||||
|
||||
// 模态框错误 (TODO: 需要实现ElMessageBox)
|
||||
await request.get({
|
||||
url: '/api/data',
|
||||
requestOptions: { errorMessageMode: 'modal' }
|
||||
})
|
||||
```
|
||||
|
||||
## 📝 最佳实践
|
||||
|
||||
1. **API服务类中的使用**
|
||||
|
||||
```typescript
|
||||
export class UserService {
|
||||
// 公开接口,不需要token
|
||||
static getPublicInfo() {
|
||||
return request.get<BaseResponse<PublicInfo>>({
|
||||
url: '/api/public/info',
|
||||
requestOptions: {
|
||||
withToken: false,
|
||||
errorMessageMode: 'none' // 静默失败
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 需要认证的接口
|
||||
static getUserProfile() {
|
||||
return request.get<BaseResponse<UserProfile>>({
|
||||
url: '/api/user/profile',
|
||||
requestOptions: {
|
||||
withToken: true,
|
||||
joinTime: true // 防止缓存
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **文件上传场景**
|
||||
|
||||
```typescript
|
||||
const uploadFile = (file: File) => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
return request.post({
|
||||
url: '/api/upload',
|
||||
data: formData,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
},
|
||||
requestOptions: {
|
||||
withToken: true,
|
||||
errorMessageMode: 'modal' // 上传失败用模态框提示
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
3. **批量请求优化**
|
||||
|
||||
```typescript
|
||||
// 并发请求,但其中一个失败不影响其他
|
||||
const fetchDashboardData = async () => {
|
||||
const requests = [
|
||||
request.get({
|
||||
url: '/api/stats',
|
||||
requestOptions: { errorMessageMode: 'none' }
|
||||
}),
|
||||
request.get({
|
||||
url: '/api/charts',
|
||||
requestOptions: { errorMessageMode: 'none' }
|
||||
}),
|
||||
request.get({
|
||||
url: '/api/notifications',
|
||||
requestOptions: { errorMessageMode: 'none' }
|
||||
})
|
||||
]
|
||||
|
||||
const results = await Promise.allSettled(requests)
|
||||
return results
|
||||
}
|
||||
```
|
||||
262
src/utils/http/index.ts
Normal file
262
src/utils/http/index.ts
Normal file
@@ -0,0 +1,262 @@
|
||||
import axios, { InternalAxiosRequestConfig, AxiosRequestConfig, AxiosResponse } from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import EmojiText from '../ui/emojo'
|
||||
import { ApiStatus } from './status'
|
||||
import type { RequestOptions, ErrorMessageMode } from '@/types/api'
|
||||
|
||||
const axiosInstance = axios.create({
|
||||
timeout: 15000, // 请求超时时间(毫秒)
|
||||
baseURL: import.meta.env.MODE === 'development' ? '' : import.meta.env.VITE_API_URL, // API地址:开发环境使用代理,生产环境使用完整URL
|
||||
withCredentials: true, // 异步请求携带cookie
|
||||
transformRequest: [(data) => JSON.stringify(data)], // 请求数据转换为 JSON 字符串
|
||||
validateStatus: (status) => status >= 200 && status < 300, // 只接受 2xx 的状态码
|
||||
headers: {
|
||||
get: { 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8' },
|
||||
post: { 'Content-Type': 'application/json;charset=utf-8' }
|
||||
},
|
||||
transformResponse: [
|
||||
(data, headers) => {
|
||||
const contentType = headers['content-type']
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
try {
|
||||
return JSON.parse(data)
|
||||
} catch {
|
||||
return data
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
axiosInstance.interceptors.request.use(
|
||||
(request: InternalAxiosRequestConfig) => {
|
||||
const { accessToken } = useUserStore()
|
||||
|
||||
// 如果 token 存在,则设置请求头
|
||||
if (accessToken) {
|
||||
request.headers.set({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
})
|
||||
}
|
||||
|
||||
return request // 返回修改后的配置
|
||||
},
|
||||
(error) => {
|
||||
ElMessage.error(`服务器异常! ${EmojiText[500]}`) // 显示错误消息
|
||||
return Promise.reject(error) // 返回拒绝的 Promise
|
||||
}
|
||||
)
|
||||
|
||||
// Token 刷新相关
|
||||
let isRefreshing = false // 是否正在刷新 token
|
||||
let failedQueue: Array<{
|
||||
resolve: (value?: any) => void
|
||||
reject: (reason?: any) => void
|
||||
}> = [] // 失败队列,存储因 token 过期而失败的请求
|
||||
|
||||
// 处理失败队列
|
||||
const processQueue = (error: any, token: string | null = null) => {
|
||||
failedQueue.forEach((prom) => {
|
||||
if (error) {
|
||||
prom.reject(error)
|
||||
} else {
|
||||
prom.resolve(token)
|
||||
}
|
||||
})
|
||||
failedQueue = []
|
||||
}
|
||||
|
||||
// 响应拦截器
|
||||
axiosInstance.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
// 401 未授权 - 尝试刷新 token
|
||||
if (response.data.code === ApiStatus.unauthorized) {
|
||||
const userStore = useUserStore()
|
||||
const originalRequest = response.config as any
|
||||
|
||||
// 如果没有 refreshToken,直接退出登录
|
||||
if (!userStore.refreshToken) {
|
||||
logOut()
|
||||
return Promise.reject(response)
|
||||
}
|
||||
|
||||
// 如果正在刷新 token,将请求加入队列
|
||||
if (isRefreshing) {
|
||||
return new Promise((resolve, reject) => {
|
||||
failedQueue.push({ resolve, reject })
|
||||
})
|
||||
.then((token) => {
|
||||
originalRequest.headers['Authorization'] = `Bearer ${token}`
|
||||
return axiosInstance.request(originalRequest)
|
||||
})
|
||||
.catch((err) => {
|
||||
return Promise.reject(err)
|
||||
})
|
||||
}
|
||||
|
||||
// 标记正在刷新
|
||||
originalRequest._retry = true
|
||||
isRefreshing = true
|
||||
|
||||
// 调用刷新 token 接口
|
||||
return new Promise((resolve, reject) => {
|
||||
axiosInstance
|
||||
.post('/api/auth/refresh', { refreshToken: userStore.refreshToken })
|
||||
.then((res) => {
|
||||
const { data } = res
|
||||
|
||||
// 刷新成功
|
||||
if (data.code === ApiStatus.success && data.data?.access_token) {
|
||||
// 更新 token
|
||||
userStore.setToken(data.data.access_token, data.data.refresh_token)
|
||||
|
||||
// 更新请求头
|
||||
originalRequest.headers['Authorization'] = `Bearer ${data.data.access_token}`
|
||||
axiosInstance.defaults.headers.common['Authorization'] = `Bearer ${data.data.access_token}`
|
||||
|
||||
// 处理队列中的请求
|
||||
processQueue(null, data.data.access_token)
|
||||
|
||||
// 重试原请求
|
||||
resolve(axiosInstance.request(originalRequest))
|
||||
} else {
|
||||
// 刷新失败
|
||||
processQueue(new Error('Token refresh failed'), null)
|
||||
logOut()
|
||||
reject(res)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
// 刷新失败
|
||||
processQueue(err, null)
|
||||
logOut()
|
||||
reject(err)
|
||||
})
|
||||
.finally(() => {
|
||||
isRefreshing = false
|
||||
})
|
||||
})
|
||||
}
|
||||
return response
|
||||
},
|
||||
(error) => {
|
||||
if (axios.isCancel(error)) {
|
||||
console.log('repeated request: ' + error.message)
|
||||
}
|
||||
// 注意:错误处理现在在request函数中根据requestOptions处理
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 扩展的请求配置接口
|
||||
interface ExtendedRequestConfig extends AxiosRequestConfig {
|
||||
requestOptions?: RequestOptions
|
||||
}
|
||||
|
||||
// 处理请求配置
|
||||
function processRequestConfig(config: ExtendedRequestConfig): AxiosRequestConfig {
|
||||
const { requestOptions, ...axiosConfig } = config
|
||||
|
||||
// 应用自定义请求选项
|
||||
if (requestOptions) {
|
||||
// 处理是否携带token
|
||||
if (requestOptions.withToken === false) {
|
||||
axiosConfig.headers = { ...axiosConfig.headers }
|
||||
delete axiosConfig.headers?.Authorization
|
||||
}
|
||||
|
||||
// 处理是否添加时间戳
|
||||
if (requestOptions.joinTime) {
|
||||
const timestamp = Date.now()
|
||||
if (axiosConfig.method?.toUpperCase() === 'GET') {
|
||||
axiosConfig.params = { ...axiosConfig.params, _t: timestamp }
|
||||
}
|
||||
}
|
||||
|
||||
// 处理API URL
|
||||
if (requestOptions.apiUrl) {
|
||||
axiosConfig.baseURL = requestOptions.apiUrl
|
||||
}
|
||||
}
|
||||
|
||||
return axiosConfig
|
||||
}
|
||||
|
||||
// 处理错误消息
|
||||
function handleErrorMessage(error: any, mode: ErrorMessageMode = 'message') {
|
||||
if (mode === 'none') return
|
||||
|
||||
const errorMessage = error.response?.data.msg
|
||||
const message = errorMessage
|
||||
? `${errorMessage} ${EmojiText[500]}`
|
||||
: `请求超时或服务器异常!${EmojiText[500]}`
|
||||
|
||||
if (mode === 'modal') {
|
||||
// TODO: 可以使用 ElMessageBox 显示模态框
|
||||
ElMessage.error(message)
|
||||
} else {
|
||||
ElMessage.error(message)
|
||||
}
|
||||
}
|
||||
|
||||
// 请求
|
||||
async function request<T = any>(config: ExtendedRequestConfig): Promise<T> {
|
||||
const processedConfig = processRequestConfig(config)
|
||||
|
||||
// 对 POST | PUT 请求特殊处理
|
||||
if (
|
||||
processedConfig.method?.toUpperCase() === 'POST' ||
|
||||
processedConfig.method?.toUpperCase() === 'PUT'
|
||||
) {
|
||||
// 如果已经有 data,则保留原有的 data
|
||||
if (processedConfig.params && !processedConfig.data) {
|
||||
processedConfig.data = processedConfig.params
|
||||
processedConfig.params = undefined // 使用 undefined 而不是空对象
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await axiosInstance.request<T>(processedConfig)
|
||||
return res.data
|
||||
} catch (e) {
|
||||
if (axios.isAxiosError(e)) {
|
||||
// 处理错误消息
|
||||
const errorMode = config.requestOptions?.errorMessageMode || 'message'
|
||||
handleErrorMessage(e, errorMode)
|
||||
}
|
||||
return Promise.reject(e)
|
||||
}
|
||||
}
|
||||
|
||||
// API 方法集合
|
||||
const api = {
|
||||
get<T>(config: ExtendedRequestConfig): Promise<T> {
|
||||
return request({ ...config, method: 'GET' }) // GET 请求
|
||||
},
|
||||
post<T>(config: ExtendedRequestConfig): Promise<T> {
|
||||
return request({ ...config, method: 'POST' }) // POST 请求
|
||||
},
|
||||
put<T>(config: ExtendedRequestConfig): Promise<T> {
|
||||
return request({ ...config, method: 'PUT' }) // PUT 请求
|
||||
},
|
||||
del<T>(config: ExtendedRequestConfig): Promise<T> {
|
||||
return request({ ...config, method: 'DELETE' }) // DELETE 请求
|
||||
},
|
||||
request<T>(config: ExtendedRequestConfig): Promise<T> {
|
||||
return request({ ...config }) // 通用请求
|
||||
}
|
||||
}
|
||||
|
||||
// 退出登录
|
||||
const logOut = () => {
|
||||
ElMessage.error(`登录已过期,请重新登录 ${EmojiText[500]}`)
|
||||
setTimeout(() => {
|
||||
useUserStore().logOut()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
export default api
|
||||
12
src/utils/http/status.ts
Normal file
12
src/utils/http/status.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* 接口状态码
|
||||
*/
|
||||
export enum ApiStatus {
|
||||
success = 0, // 成功
|
||||
error = 400, // 错误
|
||||
unauthorized = 401, // 未授权
|
||||
forbidden = 403, // 禁止访问
|
||||
notFound = 404, // 未找到
|
||||
methodNotAllowed = 405, // 方法不允许
|
||||
internalServerError = 500 // 服务器错误
|
||||
}
|
||||
Reference in New Issue
Block a user