chore: 重写初始提交(清空历史,整理后全量提交)
This commit is contained in:
@@ -0,0 +1,620 @@
|
||||
// simple-http.uts
|
||||
// 简单强大的 UniappX UTS 网络请求封装
|
||||
// 支持 token 管理、无感刷新、多方法兼容
|
||||
// ================ 类型定义 ================
|
||||
// 响应接口
|
||||
import HttpError, {
|
||||
IResponse, IRequestOption, IHttpError, IFileInfo, INetworkInfo, IDownRespone, IUploadInfo, IUploadOption, IUploadResult, IResponseToken
|
||||
} from "@/types/http.uts"
|
||||
|
||||
import userState, { getUserToken, setUserToken, clearToken } from '@/stores/user.uts'
|
||||
|
||||
// ================ 简单 HTTP 请求类 ================
|
||||
class SimpleHttp {
|
||||
|
||||
// 配置
|
||||
private baseOptions : UTSJSONObject = {
|
||||
baseURL: 'http://localhost:8000',
|
||||
timeout: 15000,
|
||||
tokenKey: 'access_token',
|
||||
refreshTokenKey: 'refresh_token',
|
||||
tokenExpireKey: 'token_expire',
|
||||
tokenType: 'Bearer',
|
||||
enableRefreshToken: true,
|
||||
maxRetryCount: 3,
|
||||
retryDelay: 1000,
|
||||
debug: true
|
||||
} as UTSJSONObject
|
||||
// 状态
|
||||
private isRefreshing = false
|
||||
private refreshSubscribers : Array<() => void> = []
|
||||
private requestQueue : Array<() => Promise<any>> = []
|
||||
private isProcessingQueue = false
|
||||
constructor(options : UTSJSONObject = {}) {
|
||||
this.baseOptions = { ...this.baseOptions, ...options }
|
||||
this.setupNetworkListener()
|
||||
}
|
||||
// ================ 核心请求方法 ================
|
||||
/**
|
||||
* 发送请求
|
||||
*/
|
||||
async request(options : IRequestOption) : Promise<IResponse> {
|
||||
try {
|
||||
//await this.checkNetwork() // 检查网络
|
||||
if (options.showLoading != null && options.showLoading == true) {
|
||||
uni.showLoading({
|
||||
title: options.loadingText ?? '加载中...',
|
||||
mask: true
|
||||
})
|
||||
}
|
||||
const requestConfig = this.buildRequestConfig(options)
|
||||
const response = await this.sendRequest(requestConfig)
|
||||
|
||||
const { statusCode, data } = response
|
||||
if (statusCode >= 200 && statusCode < 300 && data != null) {
|
||||
if (data?.access_token != null) {
|
||||
const access_token = data.access_token ?? ''
|
||||
const access_expired = data.access_expired ?? 0
|
||||
const refresh_token = data.refresh_token ?? ''
|
||||
const refresh_expired = data.refresh_expired ?? 0
|
||||
setUserToken(access_token, access_expired, refresh_token, refresh_expired)
|
||||
}
|
||||
return Promise.resolve(data)
|
||||
}
|
||||
else if (statusCode == 401) {
|
||||
return this.handleTokenExpired(options)
|
||||
} else {
|
||||
throw HttpError({
|
||||
errCode: statusCode,
|
||||
errMsg: `HTTP 错误: ${statusCode}`,
|
||||
options: options
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
throw HttpError({
|
||||
errCode: 5000,
|
||||
options,
|
||||
cause: error as Error
|
||||
})
|
||||
} finally {
|
||||
if (options.showLoading != null && options.showLoading == true) {
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET 请求
|
||||
*/
|
||||
async get(url : string, data : any = {}, config : IRequestOption = { url: '' }) : Promise<IResponse> {
|
||||
//const mergedConfig = config != null ? config : { url: '' }
|
||||
return this.request({
|
||||
...(config),
|
||||
url,
|
||||
method: 'GET',
|
||||
data
|
||||
} as IRequestOption)
|
||||
}
|
||||
|
||||
/**
|
||||
* POST 请求
|
||||
*/
|
||||
async post(url : string, data : any = {}, config : IRequestOption = { url: '' }) : Promise<IResponse> {
|
||||
//const mergedConfig = config != null ? config : { url: '' }
|
||||
return this.request({
|
||||
...(config),
|
||||
url,
|
||||
method: 'POST',
|
||||
data
|
||||
} as IRequestOption)
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT 请求
|
||||
*/
|
||||
async put(url : string, data : any = {}, config : IRequestOption = { url: '' }) : Promise<IResponse> {
|
||||
//const mergedConfig = config != null ? config : { url: '' }
|
||||
return this.request({
|
||||
...(config),
|
||||
url,
|
||||
method: 'PUT',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE 请求
|
||||
*/
|
||||
async delete(url : string, data ?: any, config : IRequestOption = { url: '' }) : Promise<IResponse> {
|
||||
//const mergedConfig = config != null ? config : { url: '' }
|
||||
return this.request({
|
||||
...(config),
|
||||
url,
|
||||
method: 'DELETE'
|
||||
} as IRequestOption)
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH 请求
|
||||
*/
|
||||
async patch(url : string, data ?: any, config : IRequestOption = { url: '' }) : Promise<IResponse> {
|
||||
//const mergedConfig = config != null ? config : { url: '' }
|
||||
return this.request({
|
||||
...(config),
|
||||
url,
|
||||
method: 'PATCH',
|
||||
data
|
||||
} as IRequestOption)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 检查 Token 是否有效
|
||||
*/
|
||||
isTokenValid() : boolean {
|
||||
const token = getUserToken()
|
||||
return (token != null)
|
||||
}
|
||||
|
||||
|
||||
// ================ Token 无感刷新 ================
|
||||
|
||||
/**
|
||||
* 刷新 Token
|
||||
*/
|
||||
private async refreshToken() : Promise<boolean> {
|
||||
if (this.isRefreshing) {
|
||||
return new Promise((resolve) => {
|
||||
this.refreshSubscribers.push(() => resolve(true))
|
||||
})
|
||||
}
|
||||
this.isRefreshing = true
|
||||
try {
|
||||
const tokenResult = getUserToken()
|
||||
if (tokenResult == null) {
|
||||
clearToken()
|
||||
this.goToLogin()
|
||||
return false
|
||||
}
|
||||
const { refreshToken } = tokenResult
|
||||
if (refreshToken == null) {
|
||||
this.log('无刷新 Token')
|
||||
clearToken()
|
||||
this.goToLogin()
|
||||
return false
|
||||
}
|
||||
// 调用刷新接口(需后端实现 Login::refresh 并返回 { access_token, refresh_token, ... })
|
||||
const response = await this.post('/wxchat/api/login/refresh', {
|
||||
refresh_token: refreshToken
|
||||
}, {
|
||||
url: "",
|
||||
needToken: false,
|
||||
showLoading: false
|
||||
})
|
||||
if (response.code == 0) {
|
||||
const { access_token, access_expired, refresh_token, refresh_expired } = response
|
||||
setUserToken(access_token ?? '', access_expired ?? 0, refresh_token ?? '', refresh_expired ?? 0)
|
||||
// 通知所有等待的请求
|
||||
this.refreshSubscribers.forEach(callback => callback())
|
||||
this.refreshSubscribers = []
|
||||
return true
|
||||
} else {
|
||||
this.log('刷新 Token 失败:', response.message)
|
||||
clearToken()
|
||||
this.goToLogin()
|
||||
return false
|
||||
}
|
||||
} catch (error) {
|
||||
this.log('刷新 Token 异常:', error)
|
||||
clearToken()
|
||||
this.goToLogin()
|
||||
return false
|
||||
} finally {
|
||||
this.isRefreshing = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 Token 过期
|
||||
*/
|
||||
private async handleTokenExpired(options : IRequestOption) : Promise<IResponse> {
|
||||
if (this.baseOptions.enableRefreshToken == null) {
|
||||
throw HttpError({
|
||||
errCode: 401,
|
||||
errMsg: 'Token 已过期',
|
||||
options: options
|
||||
})
|
||||
}
|
||||
const success = await this.refreshToken()
|
||||
if (success) {
|
||||
return this.request(options)
|
||||
} else {
|
||||
throw HttpError({
|
||||
errCode: 401,
|
||||
errMsg: '登录已过期,请重新登录',
|
||||
options: options
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转到登录页
|
||||
*/
|
||||
private goToLogin() : void {
|
||||
uni.showModal({
|
||||
title: '登录提示',
|
||||
content: '登录已过期,请重新登录',
|
||||
showCancel: false,
|
||||
confirmText: '去登录',
|
||||
success: () => {
|
||||
uni.reLaunch({
|
||||
url: '/pages/login/index'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ================ 请求处理 ================
|
||||
|
||||
/**
|
||||
* 构建请求配置
|
||||
*/
|
||||
private buildRequestConfig(options : IRequestOption) : IRequestOption {
|
||||
const { url, contentType } = options
|
||||
options.url = url?.startsWith('http') == true ? url! : `${this.baseOptions.baseURL}${url?.startsWith('/') == true ? url! : '/' + (url ?? '')}`
|
||||
if (options.header == null) {
|
||||
options.header = {} as UTSJSONObject
|
||||
}
|
||||
// #ifdef APP
|
||||
const sysInfo = uni.getSystemInfoSync()
|
||||
options.header['App-Platform'] = sysInfo.osName
|
||||
options.header['App-Version-Code'] = sysInfo.appVersionCode
|
||||
options.header['App-Version-Name'] = sysInfo.appVersion
|
||||
options.header['App-Channel'] = sysInfo.osName
|
||||
options.header['App-Device-ID'] = sysInfo.deviceId
|
||||
options.header['App-Device-Model'] = sysInfo.deviceModel
|
||||
options.header['App-OS-Version'] = sysInfo.osVersion
|
||||
// #endif
|
||||
switch (contentType) {
|
||||
case 'json':
|
||||
options.header['Content-Type'] = 'application/json'
|
||||
break
|
||||
case 'form':
|
||||
options.header['Content-Type'] = 'application/x-www-form-urlencoded'
|
||||
break
|
||||
case 'multipart':
|
||||
options.header['Content-Type'] = 'multipart/form-data'
|
||||
break
|
||||
}
|
||||
if (options.needToken != null && options.needToken) {
|
||||
const tokenResult = getUserToken()
|
||||
if (tokenResult != null) {
|
||||
const { accessToken } = tokenResult
|
||||
if (accessToken != null) {
|
||||
options.header['Authorization'] = `${this.baseOptions.tokenType} ${accessToken}`
|
||||
}
|
||||
}
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送请求
|
||||
*/
|
||||
private async sendRequest(options : IRequestOption) : Promise<RequestSuccess<IResponse>> {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request<IResponse>({
|
||||
url: options.url,
|
||||
data: options.data,
|
||||
header: options.header,
|
||||
method: options.method,
|
||||
timeout: options.timeout,
|
||||
enableChunked: true,
|
||||
success: (res : RequestSuccess<IResponse>) => {
|
||||
resolve(res)
|
||||
},
|
||||
fail: (err : RequestFail) => {
|
||||
reject(HttpError({
|
||||
errCode: err.errCode,
|
||||
errMsg: err.errMsg,
|
||||
options: options,
|
||||
}))
|
||||
},
|
||||
complete: (option : any) => {
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ================ 网络状态 ================
|
||||
|
||||
/**
|
||||
* 检查网络状态
|
||||
*/
|
||||
private async checkNetwork() : Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.getNetworkType({
|
||||
success: (res) => {
|
||||
if (res.networkType === 'none') {
|
||||
reject(new Error('网络连接已断开'))
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
resolve() // 网络检查失败不阻止请求
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取网络信息
|
||||
*/
|
||||
async getNetworkInfo() : Promise<INetworkInfo> {
|
||||
return new Promise((resolve) => {
|
||||
uni.getNetworkType({
|
||||
success: (res) => {
|
||||
resolve({
|
||||
isConnected: res.networkType !== 'none',
|
||||
networkType: res.networkType,
|
||||
isWifi: res.networkType == 'wifi',
|
||||
isCellular: ['2g', '3g', '4g', '5g'].includes(res.networkType)
|
||||
})
|
||||
},
|
||||
fail: () => {
|
||||
resolve({
|
||||
isConnected: false,
|
||||
networkType: 'unknown',
|
||||
isWifi: false,
|
||||
isCellular: false
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置网络监听
|
||||
*/
|
||||
private setupNetworkListener() : void {
|
||||
uni.onNetworkStatusChange((res) => {
|
||||
if (!res.isConnected) {
|
||||
uni.showToast({
|
||||
title: '网络连接已断开',
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ================ 工具方法 ================
|
||||
/**
|
||||
* 日志记录
|
||||
*/
|
||||
private log(...args : any[]) : void {
|
||||
if (this.baseOptions.debug != null && this.baseOptions.debug == true) {
|
||||
console.log('[SimpleHttp]', ...args)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置基础 URL
|
||||
*/
|
||||
setBaseURL(baseURL : string) : void {
|
||||
this.baseOptions.baseURL = baseURL
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置超时时间
|
||||
*/
|
||||
setTimeout(timeout : number) : void {
|
||||
this.baseOptions.timeout = timeout
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置调试模式
|
||||
*/
|
||||
setDebug(debug : boolean) : void {
|
||||
this.baseOptions.debug = debug
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 Token 刷新开关
|
||||
*/
|
||||
setEnableRefreshToken(enable : boolean) : void {
|
||||
this.baseOptions.enableRefreshToken = enable
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置
|
||||
*/
|
||||
getConfig() : UTSJSONObject {
|
||||
return { ...this.baseOptions }
|
||||
}
|
||||
// ================ 上传和下载 ================
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
*/
|
||||
async upload(options : IUploadOption, onProgress ?: (current : number, total : number, percent : number) => void) : Promise<IResponse> {
|
||||
let { url, filePath, name = 'file', formData, header } = options
|
||||
const headers : UTSJSONObject = {
|
||||
...(header ?? {} as UTSJSONObject)
|
||||
}
|
||||
const tokenResult = getUserToken()
|
||||
if (tokenResult != null) {
|
||||
const { accessToken } = tokenResult
|
||||
if (accessToken != null) {
|
||||
{
|
||||
headers['Authorization'] = `${this.baseOptions.tokenType} ${accessToken}`
|
||||
}
|
||||
}
|
||||
}
|
||||
return await new Promise((resolve, reject) => {
|
||||
const uploadTask = uni.uploadFile({
|
||||
url: url.startsWith('http') ? url : `${this.baseOptions.baseURL}${url}`,
|
||||
filePath,
|
||||
name,
|
||||
formData: formData ?? {},
|
||||
header: headers,
|
||||
success: (response : UploadFileSuccess) => {
|
||||
const { statusCode, data } = response
|
||||
// 处理 HTTP 204 等无内容响应
|
||||
if (statusCode == 204 || data == null) {
|
||||
//resolve({ code: 0, message: 'success', data: null } as IResponse)
|
||||
reject(HttpError({ errCode: -1, errMsg: `响应解析失败,原始数据>: ${data.substring(0, 200)}`, options }))
|
||||
}
|
||||
if (statusCode >= 200 && statusCode < 300) {
|
||||
try {
|
||||
const obj = JSON.parse(data) as UTSJSONObject;
|
||||
let resInfo = obj.parse<IResponse>()
|
||||
|
||||
const { access_token, access_expired, refresh_token, refresh_expired } = resInfo!!
|
||||
if (resInfo?.access_token != null) {
|
||||
setUserToken(access_token ?? '', access_expired ?? 0, refresh_token ?? '', refresh_expired ?? 0)
|
||||
}
|
||||
if (resInfo != null) {
|
||||
resolve(resInfo)
|
||||
}
|
||||
// // 业务状态码判断(根据你的业务逻辑调整)
|
||||
if (resInfo?.code != 0 && resInfo?.code != 200) {
|
||||
reject(HttpError({ errCode: resInfo?.code ?? 500, errMsg: resInfo?.message ?? '业务错误', options }))
|
||||
}
|
||||
} catch (parseError) {
|
||||
console.log(parseError)
|
||||
// 返回的不是 JSON,可能是之前遇到的响应头问题
|
||||
reject(HttpError({ errCode: -1, errMsg: `响应解析失败,原始数据: ${data.substring(0, 200)}`, options }))
|
||||
}
|
||||
return
|
||||
}
|
||||
// 401 未授权处理
|
||||
if (statusCode == 401) {
|
||||
// if (this.baseOptions.enableRefreshToken) {
|
||||
// try {
|
||||
// // const refreshSuccess = await this.refreshToken()
|
||||
// // if (refreshSuccess) {
|
||||
// // // 重试上传
|
||||
// // const retryResult = await this.upload<T>(options)
|
||||
// // resolve(retryResult)
|
||||
// // return
|
||||
// // }
|
||||
// } catch (refreshError) {
|
||||
// // refreshToken 失败,继续走登录过期逻辑
|
||||
// }
|
||||
// }
|
||||
reject(HttpError({ errCode: 401, errMsg: '登录已过期,请重新登录', options }))
|
||||
|
||||
}
|
||||
// 其他 HTTP 错误
|
||||
reject(HttpError({ errCode: statusCode, errMsg: `HTTP 错误: ${statusCode}`, options }))
|
||||
},
|
||||
fail: (error : UploadFileFail) => {
|
||||
console.log(error)
|
||||
reject(HttpError({ errCode: error.errCode ?? -1, errMsg: error.errMsg ?? '上传失败', options }))
|
||||
}
|
||||
})
|
||||
uploadTask.onProgressUpdate((res) => {
|
||||
console.log('上传进度', res.progress)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 并行上传多个文件
|
||||
*/
|
||||
async uploadMultipleFilesParallel(options : IUploadOption, files : IFileInfo[]) : Promise<IFileInfo[]> {
|
||||
// 显式指定 map 回调的返回类型为 Promise<IFileInfo>
|
||||
const uploadPromises = files.map((file) : Promise<IFileInfo> => {
|
||||
// 显式指定 upload 的泛型参数,并添加 return 返回 Promise
|
||||
return this.upload({
|
||||
url: options.url,
|
||||
header: options.header,
|
||||
timeout: options.timeout,
|
||||
filePath: file.path,
|
||||
//name: file.name ?? 'file',
|
||||
}, null).then((response : IResponse) : IFileInfo => { // 显式指定 then 的返回类型
|
||||
let { code, data } = response
|
||||
if (code == 0 && data != null) {
|
||||
const result = (data as UTSJSONObject).parse<IUploadInfo>()
|
||||
file.fid = result?.fid ?? 0
|
||||
file.name = result?.name ?? file.name
|
||||
file.path = result?.url ?? file.path
|
||||
}
|
||||
return file // 直接返回 file,不要用 Promise.resolve
|
||||
}).catch((error) : IFileInfo => {
|
||||
console.log(error);
|
||||
return file
|
||||
})
|
||||
})
|
||||
return Promise.all(uploadPromises)
|
||||
}
|
||||
// /**
|
||||
// * 带整体进度回调的并行上传
|
||||
// */
|
||||
// async uploadMultipleFilesWithProgress<T = any>(options : IUploadOption, files : IFileInfo[], onProgress ?: (completed : number, total : number, results : IUploadResult<T>[]) => void) : Promise<IUploadResult<T>[]> {
|
||||
// const results : IUploadResult<T>[] = new Array()
|
||||
// let completedCount = 0
|
||||
// const uploadPromises = files.map((file) => {
|
||||
// return this.upload<T>({
|
||||
// url: options.url,
|
||||
// header: options.header,
|
||||
// timeout: options.timeout,
|
||||
// filePath: file.path,
|
||||
// name: file.name ?? 'file',
|
||||
// formData: {
|
||||
// ...(options.formData ?? {}),
|
||||
// fileIndex: file.index.toString(),
|
||||
// fileCount: files.length.toString()
|
||||
// }
|
||||
// }).then((result) => {
|
||||
// results[file.index] = {
|
||||
// index: file.index,
|
||||
// success: true,
|
||||
// data: result
|
||||
// }
|
||||
// completedCount++
|
||||
// onProgress?.(completedCount, files.length, [...results])
|
||||
// return results[file.index]
|
||||
// }).catch((error) => {
|
||||
// results[file.index] = {
|
||||
// index: file.index,
|
||||
// success: false,
|
||||
// error: error
|
||||
// }
|
||||
// completedCount++
|
||||
// onProgress?.(completedCount, files.length, [...results])
|
||||
// return results[file.index]
|
||||
// })
|
||||
// })
|
||||
// await Promise.all(uploadPromises)
|
||||
// return results
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
|
||||
const httpApi = new SimpleHttp()
|
||||
|
||||
// 导出
|
||||
export default httpApi
|
||||
export type { IResponse, IRequestOption, INetworkInfo, IDownRespone, IUploadOption, IFileInfo }
|
||||
|
||||
// import http, { SimpleHttp, IResponse } from './simple-http'
|
||||
|
||||
// // 使用默认配置
|
||||
// const api = http
|
||||
|
||||
// // 或者创建自定义实例
|
||||
// const customApi = new SimpleHttp('https://api.yourservice.com', {
|
||||
// timeout: 20000,
|
||||
// debug: true,
|
||||
// tokenType: 'Bearer',
|
||||
// enableRefreshToken: true
|
||||
// })
|
||||
Reference in New Issue
Block a user