chore: 重写初始提交(清空历史,整理后全量提交)

This commit is contained in:
ywxapp
2026-08-16 16:54:14 +08:00
commit 6c1a106bc1
1808 changed files with 238144 additions and 0 deletions
@@ -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
// })