239 lines
6.4 KiB
TypeScript
239 lines
6.4 KiB
TypeScript
|
|
/**
|
|||
|
|
* 网络请求封装 - 基于 axios-miniprogram
|
|||
|
|
*
|
|||
|
|
* 功能特性:
|
|||
|
|
* - 自动添加 Token
|
|||
|
|
* - 统一错误处理
|
|||
|
|
* - 智能加载动画管理
|
|||
|
|
* - 完整的 TypeScript 类型支持
|
|||
|
|
* - 文件上传功能
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios-miniprogram'
|
|||
|
|
import type { StandardResponse } from '../api/types'
|
|||
|
|
|
|||
|
|
// ==================== 类型定义 ====================
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 自定义请求配置接口
|
|||
|
|
* 扩展 axios 的请求配置,添加自定义参数
|
|||
|
|
*/
|
|||
|
|
interface CustomRequestConfig extends AxiosRequestConfig {
|
|||
|
|
loading?: boolean // 是否显示加载提示,默认 true
|
|||
|
|
upload?: boolean // 是否为上传请求
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 文件上传配置接口
|
|||
|
|
*/
|
|||
|
|
interface UploadOptions {
|
|||
|
|
apiUrl: string // 接口路径
|
|||
|
|
filePath: string // 文件本地路径
|
|||
|
|
name?: string // 文件对应的 key,默认 'file'
|
|||
|
|
formData?: Record<string, unknown> // 额外参数
|
|||
|
|
loading?: boolean // 是否显示加载提示,默认 true
|
|||
|
|
onUploadProgress?: (event: unknown) => void // 上传进度回调
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ==================== 基础配置 ====================
|
|||
|
|
|
|||
|
|
// 配置 baseURL
|
|||
|
|
axios.defaults.baseURL = '你的后端域名'
|
|||
|
|
|
|||
|
|
// 配置超时时间(30秒)
|
|||
|
|
axios.defaults.timeout = 30000
|
|||
|
|
|
|||
|
|
// 配置默认请求头
|
|||
|
|
axios.defaults.headers.common['Content-Type'] = 'application/json'
|
|||
|
|
|
|||
|
|
// ==================== 请求白名单 ====================
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 请求白名单
|
|||
|
|
* 白名单内的接口不需要添加 Authorization 请求头
|
|||
|
|
* 例如:登录接口、注册接口等
|
|||
|
|
*/
|
|||
|
|
const requestWhiteList: string[] = []
|
|||
|
|
|
|||
|
|
// ==================== 加载动画管理 ====================
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 网络请求计数器
|
|||
|
|
* 用于管理多个并发请求的加载状态
|
|||
|
|
*/
|
|||
|
|
let netWorkNum = 0
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 开启加载动画
|
|||
|
|
* 使用计数器机制,防止多个请求导致的 loading 闪烁
|
|||
|
|
* @param title 加载提示文字
|
|||
|
|
*/
|
|||
|
|
function startLoading(title: string) {
|
|||
|
|
if (netWorkNum === 0) {
|
|||
|
|
wx.showLoading({
|
|||
|
|
title: title,
|
|||
|
|
mask: true,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
netWorkNum++
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 关闭加载动画
|
|||
|
|
* 延迟 500ms 关闭,确保用户体验
|
|||
|
|
*/
|
|||
|
|
function endLoading() {
|
|||
|
|
netWorkNum--
|
|||
|
|
setTimeout(() => {
|
|||
|
|
if (netWorkNum === 0) {
|
|||
|
|
wx.hideLoading()
|
|||
|
|
wx.stopPullDownRefresh() // 停止当前页面下拉刷新
|
|||
|
|
}
|
|||
|
|
}, 500)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ==================== 请求拦截器 ====================
|
|||
|
|
|
|||
|
|
axios.interceptors.request.use(
|
|||
|
|
(config: CustomRequestConfig) => {
|
|||
|
|
// 加载提示
|
|||
|
|
if (config.loading ?? true) {
|
|||
|
|
startLoading('请稍等...')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Content-Type 设置
|
|||
|
|
// 注意:上传文件时 axios 会自动设置正确的 Content-Type
|
|||
|
|
if (config.headers && !config.headers['Content-Type']) {
|
|||
|
|
config.headers['Content-Type'] = 'application/json'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Token 自动注入(非白名单接口)
|
|||
|
|
if (!requestWhiteList.includes(config.url || '')) {
|
|||
|
|
const app = getApp<IAppOption>()
|
|||
|
|
if (app.globalData.token && config.headers) {
|
|||
|
|
config.headers['Authorization'] = app.globalData.token
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 详细日志记录
|
|||
|
|
/* eslint-disable no-console */
|
|||
|
|
console.log('============发送请求 start============')
|
|||
|
|
console.log(`[${new Date().toISOString()}] [INFO] 请求地址: ${config.baseURL}${config.url}`)
|
|||
|
|
console.log(`[${new Date().toISOString()}] [INFO] 请求方式: ${config.method?.toUpperCase()}`)
|
|||
|
|
console.log(`[${new Date().toISOString()}] [INFO] 上传模式: ${config.upload ? '是' : '否'}`)
|
|||
|
|
console.log(`[${new Date().toISOString()}] [INFO] params参数: ${JSON.stringify(config.params)}`)
|
|||
|
|
console.log(`[${new Date().toISOString()}] [INFO] data参数: ${JSON.stringify(config.data)}`)
|
|||
|
|
console.log('============发送请求 end============')
|
|||
|
|
/* eslint-enable no-console */
|
|||
|
|
|
|||
|
|
return config
|
|||
|
|
},
|
|||
|
|
(error) => {
|
|||
|
|
// 请求错误处理
|
|||
|
|
endLoading()
|
|||
|
|
return Promise.reject(error)
|
|||
|
|
},
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// ==================== 响应拦截器 ====================
|
|||
|
|
|
|||
|
|
axios.interceptors.response.use(
|
|||
|
|
(response: AxiosResponse) => {
|
|||
|
|
// 关闭加载动画
|
|||
|
|
const config = response.config as CustomRequestConfig
|
|||
|
|
if (config.loading ?? true) {
|
|||
|
|
endLoading()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 标准格式:{ code, data, message }
|
|||
|
|
const result = response.data as StandardResponse
|
|||
|
|
|
|||
|
|
if (result.code === 200) {
|
|||
|
|
// 只返回 data 字段(修改 response.data 为提取后的 data)
|
|||
|
|
return { ...response, data: result.data } as unknown as AxiosResponse
|
|||
|
|
} else {
|
|||
|
|
// 业务错误处理
|
|||
|
|
wx.showToast({
|
|||
|
|
title: result.message || '请求失败',
|
|||
|
|
icon: 'none',
|
|||
|
|
})
|
|||
|
|
return Promise.reject(new Error(result.message))
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
(error: unknown) => {
|
|||
|
|
// 关闭加载动画
|
|||
|
|
const axiosError = error as {
|
|||
|
|
config?: CustomRequestConfig
|
|||
|
|
response?: AxiosResponse
|
|||
|
|
message?: string
|
|||
|
|
}
|
|||
|
|
const config = axiosError.config
|
|||
|
|
if (config?.loading ?? true) {
|
|||
|
|
endLoading()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 401 未授权,跳转登录页
|
|||
|
|
if (axiosError.response?.status === 401) {
|
|||
|
|
wx.showToast({
|
|||
|
|
title: '未授权,请重新登录',
|
|||
|
|
icon: 'none',
|
|||
|
|
})
|
|||
|
|
wx.reLaunch({
|
|||
|
|
url: '/pages/login/login',
|
|||
|
|
})
|
|||
|
|
return Promise.reject(error)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 统一错误提示
|
|||
|
|
const errorData = axiosError.response?.data as StandardResponse | undefined
|
|||
|
|
const message = errorData?.message || errorData?.data || axiosError.message || '网络请求失败'
|
|||
|
|
|
|||
|
|
wx.showToast({
|
|||
|
|
title: message as string,
|
|||
|
|
icon: 'none',
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
return Promise.reject(error)
|
|||
|
|
},
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// ==================== 文件上传封装 ====================
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 文件上传函数
|
|||
|
|
* 基于 axios-miniprogram 内置的 upload 功能
|
|||
|
|
* @param options 上传配置
|
|||
|
|
* @returns Promise<T> 返回 data 字段的类型
|
|||
|
|
*/
|
|||
|
|
function upload<T = unknown>(options: UploadOptions): Promise<T> {
|
|||
|
|
const {
|
|||
|
|
apiUrl,
|
|||
|
|
filePath,
|
|||
|
|
name = 'file',
|
|||
|
|
formData = {},
|
|||
|
|
loading = true,
|
|||
|
|
onUploadProgress,
|
|||
|
|
} = options
|
|||
|
|
|
|||
|
|
return axios({
|
|||
|
|
url: apiUrl,
|
|||
|
|
method: 'POST',
|
|||
|
|
upload: true, // 启用上传模式
|
|||
|
|
data: {
|
|||
|
|
name, // 文件对应的 key
|
|||
|
|
filePath, // 要上传文件资源的路径
|
|||
|
|
...formData, // 额外的 formData 属性
|
|||
|
|
},
|
|||
|
|
loading, // 自定义 loading 参数(会被拦截器识别)
|
|||
|
|
onUploadProgress, // 上传进度回调
|
|||
|
|
} as CustomRequestConfig).then((res) => res.data as T)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ==================== 导出接口 ====================
|
|||
|
|
|
|||
|
|
// 默认导出 axios 实例(支持完整的泛型类型推导)
|
|||
|
|
export default axios
|
|||
|
|
|
|||
|
|
// 命名导出 upload 函数(支持泛型)
|
|||
|
|
export { upload }
|