Files
gaoYuJournal/miniprogram/utils/Network.ts
2025-12-17 16:16:10 +08:00

442 lines
9.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import config from "../config/index";
import { Response, QueryPage, QueryPageResult, TempFileResponse } from "../types/Model";
import { MediaItemType } from "../types/UI";
/** 微信媒体项(用于上传) */
export interface WechatMediaItem {
/** 媒体路径或 URL */
path?: string;
url?: string;
/** 文件大小 */
size?: number;
/** 媒体类型 */
type?: MediaItemType;
}
/** 请求选项 */
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);
});
});
}
}