200 lines
5.0 KiB
TypeScript
200 lines
5.0 KiB
TypeScript
import Taro from '@tarojs/taro'
|
||
import type { ApiResponse, RequestConfig } from '@/types/api'
|
||
|
||
/** 存储 key(与 AuthContext 保持一致) */
|
||
const STORAGE_TOKEN_KEY = 'auth_token'
|
||
|
||
/** 登录页路径 */
|
||
const LOGIN_PATH = '/pages/login/index'
|
||
|
||
/** 默认请求超时(ms) */
|
||
const DEFAULT_TIMEOUT = 15000
|
||
/** 接口根地址(uploadFile 等原生请求同样使用) */
|
||
export const BASE_URL = "http://localhost:8000/index.php"
|
||
|
||
/**
|
||
* HTTP 状态码 → 错误提示映射
|
||
*/
|
||
const HTTP_ERROR_MAP: Record<number, string> = {
|
||
400: '参数不正确',
|
||
401: '登录已过期,请重新登录',
|
||
403: '您没有权限操作',
|
||
404: '请求的资源不存在',
|
||
408: '请求超时',
|
||
500: '服务器内部错误',
|
||
502: '网关错误',
|
||
503: '服务暂时不可用',
|
||
504: '网关超时',
|
||
}
|
||
|
||
/** 业务状态码常量 */
|
||
const BIZ_CODE = {
|
||
SUCCESS: 0,
|
||
} as const
|
||
|
||
/**
|
||
* 获取本地存储的 token
|
||
*/
|
||
export function getToken(): string | null {
|
||
try {
|
||
return Taro.getStorageSync(STORAGE_TOKEN_KEY) || null
|
||
} catch {
|
||
return null
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 清除本地认证信息
|
||
*/
|
||
function clearAuth(): void {
|
||
try {
|
||
Taro.removeStorageSync(STORAGE_TOKEN_KEY)
|
||
Taro.removeStorageSync('auth_user')
|
||
} catch {
|
||
// noop
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理 HTTP 状态码错误
|
||
* @param statusCode - HTTP 状态码
|
||
*/
|
||
function handleHttpError(statusCode: number): void {
|
||
// 401 → 清除登录态并跳转登录页
|
||
if (statusCode === 401) {
|
||
clearAuth()
|
||
Taro.showToast({ title: '登录已过期,请重新登录', icon: 'none' })
|
||
// 避免在登录页重复跳转
|
||
const pages = Taro.getCurrentPages()
|
||
const currentPage = pages[pages.length - 1]
|
||
if (currentPage?.route !== 'pages/login/index') {
|
||
setTimeout(() => {
|
||
Taro.navigateTo({ url: LOGIN_PATH })
|
||
}, 800)
|
||
}
|
||
return
|
||
}
|
||
|
||
const message = HTTP_ERROR_MAP[statusCode] || `请求失败 (状态码: ${statusCode})`
|
||
Taro.showToast({ title: message, icon: 'none' })
|
||
}
|
||
|
||
/**
|
||
* 处理业务错误
|
||
* @param data - 接口返回数据
|
||
*/
|
||
function handleBusinessError(data: ApiResponse): void {
|
||
const { msg } = data
|
||
if (msg) {
|
||
Taro.showToast({ title: msg, icon: 'none' })
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 发起网络请求
|
||
*
|
||
* @example
|
||
* ```ts
|
||
* // GET 请求
|
||
* const res = await request({ url: '/api/user/info' })
|
||
*
|
||
* // POST 请求
|
||
* const res = await request({ url: '/api/order/create', method: 'POST', data: { id: 1 } })
|
||
*
|
||
* // 跳过 token(如登录接口)
|
||
* const res = await request({ url: '/api/auth/login', method: 'POST', skipToken: true })
|
||
* ```
|
||
*/
|
||
export function request<T = any>(config: RequestConfig): Promise<ApiResponse<T>> {
|
||
const { skipToken, skipErrorToast, ...restConfig } = config
|
||
|
||
// 构建请求头
|
||
const header: Record<string, string> = {
|
||
'Content-Type': 'application/json',
|
||
...((restConfig.header as Record<string, string>) || {}),
|
||
}
|
||
|
||
// 自动附加 token
|
||
if (!skipToken) {
|
||
const token = getToken()
|
||
if (token) {
|
||
header['Authorization'] = `Bearer ${token}`
|
||
}
|
||
}
|
||
|
||
return new Promise((resolve, reject) => {
|
||
Taro.request({
|
||
...restConfig,
|
||
url: BASE_URL + restConfig.url,
|
||
header,
|
||
timeout: restConfig.timeout || DEFAULT_TIMEOUT,
|
||
success(res) {
|
||
const { statusCode, data } = res
|
||
|
||
// HTTP 状态码异常
|
||
if (statusCode < 200 || statusCode >= 300) {
|
||
if (!skipErrorToast) {
|
||
handleHttpError(statusCode)
|
||
}
|
||
reject(res)
|
||
return
|
||
}
|
||
|
||
const responseData = data as ApiResponse<T>
|
||
|
||
// 业务成功
|
||
if (responseData.success) {
|
||
resolve(responseData)
|
||
return
|
||
}
|
||
|
||
// 业务失败
|
||
if (!skipErrorToast) {
|
||
handleBusinessError(responseData)
|
||
}
|
||
reject(responseData)
|
||
},
|
||
fail(err) {
|
||
// 网络错误 / 超时
|
||
const errMsg = err.errMsg || ''
|
||
if (errMsg.includes('timeout')) {
|
||
Taro.showToast({ title: '请求超时,请稍后重试', icon: 'none' })
|
||
} else if (errMsg.includes('fail')) {
|
||
Taro.showToast({ title: '网络连接失败,请检查网络', icon: 'none' })
|
||
} else {
|
||
Taro.showToast({ title: '网络错误,请稍后重试', icon: 'none' })
|
||
}
|
||
reject(err)
|
||
},
|
||
})
|
||
})
|
||
}
|
||
|
||
/**
|
||
* GET 请求快捷方法
|
||
*/
|
||
export function get<T = any>(url: string, config?: Omit<RequestConfig, 'url' | 'method'>) {
|
||
return request<T>({ ...config, url, method: 'GET' })
|
||
}
|
||
|
||
/**
|
||
* POST 请求快捷方法
|
||
*/
|
||
export function post<T = any>(url: string, data?: any, config?: Omit<RequestConfig, 'url' | 'method' | 'data'>) {
|
||
return request<T>({ ...config, url, method: 'POST', data })
|
||
}
|
||
|
||
/**
|
||
* PUT 请求快捷方法
|
||
*/
|
||
export function put<T = any>(url: string, data?: any, config?: Omit<RequestConfig, 'url' | 'method' | 'data'>) {
|
||
return request<T>({ ...config, url, method: 'PUT', data })
|
||
}
|
||
|
||
/**
|
||
* DELETE 请求快捷方法
|
||
*/
|
||
export function del<T = any>(url: string, config?: Omit<RequestConfig, 'url' | 'method'>) {
|
||
return request<T>({ ...config, url, method: 'DELETE' })
|
||
}
|