441 lines
9.8 KiB
TypeScript
441 lines
9.8 KiB
TypeScript
import config from "../config/index";
|
||
import { Response, QueryPage, QueryPageResult, TempFileResponse } from "../types/Model";
|
||
|
||
/** 微信媒体项(用于上传) */
|
||
export interface WechatMediaItem {
|
||
|
||
/** 媒体路径或 URL */
|
||
path?: string;
|
||
|
||
url?: string;
|
||
|
||
/** 文件大小 */
|
||
size?: number;
|
||
|
||
/** 媒体类型 */
|
||
type?: string;
|
||
}
|
||
|
||
/** 请求选项 */
|
||
export interface RequestOptions<T = any>
|
||
|
||
extends Omit<WechatMiniprogram.RequestOption, "url" | "success" | "fail" | "complete"> {
|
||
|
||
/** 接口路径(相对于 baseURL) */
|
||
url: string;
|
||
|
||
/** 请求数据 */
|
||
data?: T;
|
||
|
||
/** 是否显示加载提示 */
|
||
showLoading?: boolean;
|
||
|
||
/** 加载提示文字 */
|
||
loadingText?: string;
|
||
|
||
/** 是否自动处理错误提示 */
|
||
autoHandleError?: boolean;
|
||
}
|
||
|
||
/** 上传进度信息 */
|
||
export interface UploadProgress {
|
||
|
||
/** 总大小(字节) */
|
||
total: number;
|
||
|
||
/** 已上传大小(字节) */
|
||
uploaded: number;
|
||
|
||
/** 当前每秒上传大小(字节/秒) */
|
||
speed: number;
|
||
|
||
/** 上传进度百分比 (0-100) */
|
||
percent: number;
|
||
}
|
||
|
||
/** 上传选项 */
|
||
export interface UploadOptions {
|
||
|
||
/** 文件路径 */
|
||
filePath: string;
|
||
|
||
/** 文件字段名 */
|
||
name?: string;
|
||
|
||
/** 上传进度回调 */
|
||
onProgress?: (progress: UploadProgress) => void;
|
||
}
|
||
|
||
/** 批量上传选项 */
|
||
export interface UploadFilesOptions {
|
||
|
||
/** 媒体文件列表 */
|
||
mediaList: WechatMediaItem[];
|
||
|
||
/** 上传进度回调 */
|
||
onProgress?: (progress: UploadProgress) => void;
|
||
|
||
/** 是否显示加载提示 */
|
||
showLoading?: boolean;
|
||
}
|
||
|
||
/**
|
||
* 网络请求工具类
|
||
*
|
||
* 设计原则:
|
||
* 1. 简单直接 - 不过度封装,保持 API 清晰
|
||
* 2. 类型安全 - 充分利用 TypeScript 泛型
|
||
* 3. 统一处理 - 自动添加 header、统一错误处理
|
||
* 4. Promise 化 - 提供现代化的异步 API
|
||
*/
|
||
export class Network {
|
||
/** 基础 URL */
|
||
private static baseURL = config.url;
|
||
|
||
/** 获取通用请求头 */
|
||
private static getHeaders(): Record<string, string> {
|
||
return {
|
||
Key: wx.getStorageSync("key") || ""
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 通用请求方法
|
||
*
|
||
* @template T - 响应数据类型
|
||
* @param options - 请求选项
|
||
* @returns Promise<T> - 返回业务数据
|
||
*/
|
||
static request<T = any>(options: RequestOptions): Promise<T> {
|
||
const {
|
||
url,
|
||
method = "GET",
|
||
data,
|
||
header = {},
|
||
showLoading = false,
|
||
loadingText = "加载中...",
|
||
autoHandleError = true,
|
||
...restOptions
|
||
} = options;
|
||
// 显示加载提示
|
||
if (showLoading) {
|
||
wx.showLoading({ title: loadingText, mask: true });
|
||
}
|
||
return new Promise<T>((resolve, reject) => {
|
||
wx.request({
|
||
url: `${this.baseURL}${url}`,
|
||
method: method as any,
|
||
data,
|
||
header: {
|
||
...this.getHeaders(),
|
||
...header
|
||
},
|
||
...restOptions,
|
||
success: (res: WechatMiniprogram.RequestSuccessCallbackResult) => {
|
||
if (showLoading) {
|
||
wx.hideLoading();
|
||
}
|
||
const response = res.data as Response<T>;
|
||
// 业务成功
|
||
if (response.code === 20000) {
|
||
resolve(response.data as T);
|
||
} else {
|
||
// 业务失败
|
||
const error = new Error(response.msg || "请求失败");
|
||
if (autoHandleError) {
|
||
wx.showToast({
|
||
title: response.msg || "请求失败",
|
||
icon: "error"
|
||
});
|
||
}
|
||
reject(error);
|
||
}
|
||
},
|
||
fail: (err) => {
|
||
if (showLoading) {
|
||
wx.hideLoading();
|
||
}
|
||
if (autoHandleError) {
|
||
wx.showToast({
|
||
title: "网络请求失败",
|
||
icon: "error"
|
||
});
|
||
}
|
||
reject(err);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* GET 请求
|
||
*
|
||
* @template T - 响应数据类型
|
||
* @param url - 接口路径
|
||
* @param data - 请求参数
|
||
* @param options - 其他选项
|
||
*/
|
||
static get<T = any>(url: string, data?: any, options?: Partial<RequestOptions>): Promise<T> {
|
||
return this.request<T>({
|
||
url,
|
||
method: "GET",
|
||
data,
|
||
...options
|
||
});
|
||
}
|
||
|
||
/**
|
||
* POST 请求
|
||
*
|
||
* @template T - 响应数据类型
|
||
* @param url - 接口路径
|
||
* @param data - 请求数据
|
||
* @param options - 其他选项
|
||
*/
|
||
static post<T = any>(url: string, data?: any, options?: Partial<RequestOptions>): Promise<T> {
|
||
return this.request<T>({
|
||
url,
|
||
method: "POST",
|
||
data,
|
||
...options
|
||
});
|
||
}
|
||
|
||
/**
|
||
* DELETE 请求
|
||
*
|
||
* @template T - 响应数据类型
|
||
* @param url - 接口路径
|
||
* @param data - 请求数据
|
||
* @param options - 其他选项
|
||
*/
|
||
static delete<T = any>(url: string, data?: any, options?: Partial<RequestOptions>): Promise<T> {
|
||
return this.request<T>({
|
||
url,
|
||
method: "DELETE",
|
||
data,
|
||
...options
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 分页查询请求
|
||
*
|
||
* @template T - 列表项类型
|
||
* @param url - 接口路径
|
||
* @param pageParams - 分页参数
|
||
* @param options - 其他选项
|
||
*/
|
||
static page<T = any>(
|
||
url: string,
|
||
pageParams: QueryPage,
|
||
options?: Partial<RequestOptions>
|
||
): Promise<QueryPageResult<T>> {
|
||
return this.post<QueryPageResult<T>>(url, pageParams, options);
|
||
}
|
||
|
||
/**
|
||
* 上传单个文件
|
||
*
|
||
* @param options - 上传选项
|
||
* @returns Promise<string> - 返回临时文件 ID
|
||
*/
|
||
static uploadFile(options: UploadOptions): Promise<string> {
|
||
const { filePath, name = "file", onProgress } = options;
|
||
|
||
return new Promise<string>((resolve, reject) => {
|
||
// 先获取文件大小
|
||
wx.getFileSystemManager().getFileInfo({
|
||
filePath,
|
||
success: (fileInfo) => {
|
||
const total = fileInfo.size;
|
||
let uploaded = 0;
|
||
let lastuploaded = 0;
|
||
let speed = 0;
|
||
|
||
// 每秒计算一次上传速度
|
||
const speedUpdateInterval = setInterval(() => {
|
||
const chunkSize = uploaded - lastuploaded;
|
||
speed = chunkSize;
|
||
lastuploaded = uploaded;
|
||
|
||
if (onProgress) {
|
||
onProgress({
|
||
total,
|
||
uploaded,
|
||
speed,
|
||
percent: Math.round((uploaded / total) * 10000) / 100
|
||
});
|
||
}
|
||
}, 1000);
|
||
|
||
const uploadTask = wx.uploadFile({
|
||
url: `${this.baseURL}/temp/file/upload`,
|
||
filePath,
|
||
name,
|
||
header: this.getHeaders(),
|
||
success: (res) => {
|
||
// 清除定时器
|
||
clearInterval(speedUpdateInterval);
|
||
try {
|
||
const response = JSON.parse(res.data) as Response<TempFileResponse[]>;
|
||
if (response.code === 20000) {
|
||
resolve(response.data[0].id);
|
||
} else {
|
||
reject(new Error(response.msg || "文件上传失败"));
|
||
}
|
||
} catch (error) {
|
||
reject(new Error("解析上传响应失败"));
|
||
}
|
||
},
|
||
fail: (err) => {
|
||
// 清除定时器
|
||
clearInterval(speedUpdateInterval);
|
||
reject(err);
|
||
}
|
||
});
|
||
|
||
// 监听上传进度
|
||
uploadTask.onProgressUpdate((res) => {
|
||
uploaded = Math.floor((res.totalBytesExpectedToSend * res.progress) / 100);
|
||
|
||
if (onProgress) {
|
||
onProgress({
|
||
total,
|
||
uploaded,
|
||
speed,
|
||
percent: res.progress
|
||
});
|
||
}
|
||
});
|
||
},
|
||
fail: (err) => {
|
||
reject(new Error(`获取文件信息失败: ${err.errMsg}`));
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 批量上传文件
|
||
*
|
||
* @param options - 批量上传选项
|
||
* @returns Promise<string[]> - 返回临时文件 ID 列表
|
||
*/
|
||
static uploadFiles(options: UploadFilesOptions): Promise<string[]> {
|
||
const { mediaList, onProgress, showLoading = true } = options;
|
||
|
||
return new Promise<string[]>((resolve, reject) => {
|
||
// 空列表直接返回
|
||
if (mediaList.length === 0) {
|
||
resolve([]);
|
||
return;
|
||
}
|
||
|
||
if (showLoading) {
|
||
wx.showLoading({ title: "正在上传..", mask: true });
|
||
}
|
||
|
||
// 先获取所有文件的大小
|
||
const sizePromises = mediaList.map(item => {
|
||
const filePath = item.path || item.url || "";
|
||
return new Promise<number>((sizeResolve, sizeReject) => {
|
||
wx.getFileSystemManager().getFileInfo({
|
||
filePath,
|
||
success: (res) => sizeResolve(res.size),
|
||
fail: (err) => sizeReject(err)
|
||
});
|
||
});
|
||
});
|
||
|
||
Promise.all(sizePromises).then(fileSizes => {
|
||
const total = fileSizes.reduce((acc, size) => acc + size, 0);
|
||
const tempFileIds: string[] = [];
|
||
let uploaded = 0;
|
||
let lastuploaded = 0;
|
||
let speed = 0;
|
||
|
||
// 每秒计算一次上传速度
|
||
const speedUpdateInterval = setInterval(() => {
|
||
const chunkSize = uploaded - lastuploaded;
|
||
speed = chunkSize;
|
||
lastuploaded = uploaded;
|
||
|
||
if (onProgress) {
|
||
onProgress({
|
||
total,
|
||
uploaded,
|
||
speed,
|
||
percent: Math.round((uploaded / total) * 10000) / 100
|
||
});
|
||
}
|
||
}, 1000);
|
||
|
||
// 串行上传(避免并发过多)
|
||
const uploadNext = (index: number) => {
|
||
if (index >= mediaList.length) {
|
||
// 清除定时器
|
||
clearInterval(speedUpdateInterval);
|
||
if (showLoading) {
|
||
wx.hideLoading();
|
||
}
|
||
resolve(tempFileIds);
|
||
return;
|
||
}
|
||
|
||
const media = mediaList[index];
|
||
const filePath = media.path || media.url || "";
|
||
let prevProgress = 0;
|
||
|
||
this.uploadFile({
|
||
filePath,
|
||
onProgress: (progressResult) => {
|
||
// 计算当前文件的增量上传大小
|
||
const fileUploaded = (progressResult.total * progressResult.percent) / 100;
|
||
const delta = fileUploaded - prevProgress;
|
||
uploaded += delta;
|
||
prevProgress = fileUploaded;
|
||
|
||
if (onProgress) {
|
||
onProgress({
|
||
total,
|
||
uploaded,
|
||
speed,
|
||
percent: Math.round((uploaded / total) * 10000) / 100
|
||
});
|
||
}
|
||
}
|
||
}).then((tempFileId) => {
|
||
tempFileIds.push(tempFileId);
|
||
// 继续上传下一个
|
||
uploadNext(index + 1);
|
||
}).catch((err) => {
|
||
// 清除定时器
|
||
clearInterval(speedUpdateInterval);
|
||
if (showLoading) {
|
||
wx.hideLoading();
|
||
}
|
||
wx.showToast({
|
||
title: "文件上传失败",
|
||
icon: "error"
|
||
});
|
||
reject(err);
|
||
});
|
||
};
|
||
|
||
// 开始上传第一个文件
|
||
uploadNext(0);
|
||
}).catch((err) => {
|
||
if (showLoading) {
|
||
wx.hideLoading();
|
||
}
|
||
wx.showToast({
|
||
title: "获取文件信息失败",
|
||
icon: "error"
|
||
});
|
||
reject(err);
|
||
});
|
||
});
|
||
}
|
||
}
|