From 4b500f6169d3c8cea449d84ffa0eeab12636f991 Mon Sep 17 00:00:00 2001 From: BTMuli Date: Fri, 3 Apr 2026 23:35:34 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=8C=B1=20=E5=88=9D=E6=AD=A5=E5=88=9B?= =?UTF-8?q?=E5=BB=BA=E6=96=B0=E8=AF=B7=E6=B1=82=E5=B0=81=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/enum/app.ts | 20 ++++ src/types/App/Response.d.ts | 98 ++++++++++++++++ src/utils/TGHttps.ts | 216 ++++++++++++++++++++++++++++++++++++ 3 files changed, 334 insertions(+) create mode 100644 src/enum/app.ts create mode 100644 src/types/App/Response.d.ts create mode 100644 src/utils/TGHttps.ts diff --git a/src/enum/app.ts b/src/enum/app.ts new file mode 100644 index 00000000..31e02959 --- /dev/null +++ b/src/enum/app.ts @@ -0,0 +1,20 @@ +/** + * 应用枚举类 + * @since Beta v0.10.0 + */ + +/** + * 请求方法类型枚举 + * @since Beta v0.10.0 + * @see TGApp.App.Response.ReqMethodEnum + */ +const ReqMethodEnum: typeof TGApp.App.Response.ReqMethod = { + GET: "GET", + POST: "POST", +}; + +const appEnum = { + req: ReqMethodEnum, +}; + +export default appEnum; diff --git a/src/types/App/Response.d.ts b/src/types/App/Response.d.ts new file mode 100644 index 00000000..9c1889ea --- /dev/null +++ b/src/types/App/Response.d.ts @@ -0,0 +1,98 @@ +/** + * 应用请求封装相关类型定义 + * @since Beta v0.10.0 + */ + +declare namespace TGApp.App.Response { + /** + * 请求方法枚举 + * @since Beta v0.10.0 + * @remarks 只定义了用到的部分 + */ + const ReqMethod = { + /** GET */ + GET: "GET", + /** POST */ + POST: "POST", + }; + + /** + * 请求方法枚举类型 + * @since Beta v0.10.0 + */ + type ReqMethodEnum = (typeof ReqMethod)[keyof typeof ReqMethod]; + + /** + * 请求配置 + * @since Beta v0.10.0 + * @remarks 只写了用到的部分 + */ + type ReqConf = { + /** 请求方法 */ + method?: ReqMethodEnum; + /** 请求头 */ + headers?: Record; + /** URL 查询参数 */ + query?: Record; + /** 请求体 */ + body?: string | Record; + /** 是否返回 Blob 数据 */ + isBlob?: boolean; + /** 是否包含 BigInt 数据 */ + hasBigInt?: boolean; + /** 请求超时时间(毫秒) */ + timeout?: number; + /** AbortSignal 用于取消请求 */ + signal?: AbortSignal; + /** 基础 URL */ + baseURL?: string; + }; + + /** + * 请求配置参数 + * @since Beta v0.10.0 + */ + type ReqConfParams = Omit; + + /** + * 响应类型 + * @since Beta v0.10.0 + */ + type Resp = { + /** 响应数据 */ + data: T; + /** HTTP 状态码 */ + status: number; + /** 状态文本 */ + statusText: string; + /** 响应头 */ + headers: Headers; + /** 原始 Response 对象 */ + raw: Response; + /** 请求配置 */ + config: ReqConf; + }; + + /** + * HTTP错误响应 + * @since Beta v0.10.0 + */ + type HttpErr = { + /** 错误消息 */ + message: string; + /** HTTP 状态码 */ + status?: number; + /** 状态文本 */ + statusText?: string; + /** 响应数据 */ + data?: unknown; + /** 原始错误 */ + cause?: unknown; + }; + + /** + * Http错误构建参数 + * @since Beta v0.10.0 + */ + type HttpErrParams = Omit; +} diff --git a/src/utils/TGHttps.ts b/src/utils/TGHttps.ts new file mode 100644 index 00000000..f3a26345 --- /dev/null +++ b/src/utils/TGHttps.ts @@ -0,0 +1,216 @@ +/** + * 应用请求客户端封装 + * @since Beta v0.10.0 + */ + +import { type ClientOptions, fetch } from "@tauri-apps/plugin-http"; +import JSONBig from "json-bigint"; + +import TGLogger from "./TGLogger.js"; + +/** + * 构建 URL 查询字符串 + * @since Beta v0.10.0 + * @param params - 查询参数 + * @returns 查询字符串 + */ +function buildQueryString(params: Record): string { + const searchParams = new URLSearchParams(); + for (const [key, value] of Object.entries(params)) { + searchParams.append(key, String(value)); + } + return searchParams.toString(); +} + +/** + * 创建 HTTP 错误对象 + * @since Beta v0.10.0 + * @param message - 错误消息 + * @param options - 错误选项 + * @returns HTTP 错误对象 + */ +function createHttpError( + message: string, + options: TGApp.App.Response.HttpErrParams, +): TGApp.App.Response.HttpErr { + return { + message, + status: options.status, + statusText: options.statusText, + data: options.data, + cause: options.cause, + }; +} + +/** + * 解析响应数据 + * @since Beta v0.10.0 + * @param response - 原始响应 + * @param config - 请求配置 + * @returns 解析后的数据 + */ +async function parseResponse( + response: Response, + config: TGApp.App.Response.ReqConf, +): Promise { + if (config.isBlob) { + return await response.arrayBuffer(); + } + if (config.hasBigInt) { + return JSONBig.parse(await response.text()); + } + return await response.json(); +} + +/** + * 执行 HTTP 请求 + * @since Beta v0.10.0 + * @param method - 请求方法 + * @param url - 请求地址 + * @param config - 请求配置 + * @returns 响应对象 + */ +async function request( + method: TGApp.App.Response.ReqMethodEnum, + url: string, + config: TGApp.App.Response.ReqConf = {}, +): Promise> { + const timeout = config.timeout ?? 30000; + + // 构建完整 URL + let finalUrl = url; + if (config.query) { + const queryString = buildQueryString(config.query); + if (queryString) { + finalUrl += `?${queryString}`; + } + } + + // 构建请求头 + const httpHeaders = new Headers(); + if (config.headers) { + for (const [key, value] of Object.entries(config.headers)) { + httpHeaders.append(key, value); + } + } + + // 构建请求选项 + const fetchOptions: RequestInit & ClientOptions = { + method, + headers: httpHeaders, + }; + + // 添加请求体 + if (config.body !== undefined) { + fetchOptions.body = typeof config.body === "string" ? config.body : JSON.stringify(config.body); + } + + // 调试日志 + if (config.isBlob) { + console.debug(`[TGHttps] Fetch Blob: ${finalUrl}`); + } else { + console.debug(`[TGHttps] ${method} ${finalUrl}`); + } + + // 创建超时控制器 + const timeoutController = new AbortController(); + const timeoutId = setTimeout(() => timeoutController.abort(), timeout); + + // 合并 AbortSignal + let combinedSignal: AbortSignal; + if (config.signal) { + combinedSignal = AbortSignal.any([config.signal, timeoutController.signal]); + } else { + combinedSignal = timeoutController.signal; + } + fetchOptions.signal = combinedSignal; + + try { + const rawResponse = await fetch(finalUrl, fetchOptions); + + // 清除超时定时器 + clearTimeout(timeoutId); + + // 检查 HTTP 状态 + if (!rawResponse.ok) { + const errorText = await rawResponse.text().catch(() => "Unknown error"); + throw createHttpError(`HTTP Error: ${rawResponse.status} ${rawResponse.statusText}`, { + status: rawResponse.status, + statusText: rawResponse.statusText, + data: errorText, + }); + } + + // 解析响应 + const data = await parseResponse(rawResponse, config); + + return { + data: data, + status: rawResponse.status, + statusText: rawResponse.statusText, + headers: rawResponse.headers, + raw: rawResponse, + config: config, + }; + } catch (error) { + // 清除超时定时器 + clearTimeout(timeoutId); + + let httpError: TGApp.App.Response.HttpErr; + + if (typeof error === "object" && error !== null && "message" in error) { + httpError = error; + } else if (error instanceof Error) { + httpError = createHttpError(error.message, { cause: error }); + } else { + httpError = createHttpError(String(error), { cause: error }); + } + + // 记录错误日志 + await TGLogger.Error(`[TGHttps] Request failed: ${httpError.message}`); + + throw httpError; + } +} + +const TGHttps = { + /** + * GET 请求 + * @since Beta v0.10.0 + * @param url - 请求地址 + * @param config - 请求配置 + * @returns 响应对象 + */ + get: ( + url: string, + config?: TGApp.App.Response.ReqConfParams, + ): Promise> => request("GET", url, config), + + /** + * POST 请求 + * @since Beta v0.10.0 + * @param url - 请求地址 + * @param config - 请求配置 + * @returns 响应对象 + */ + post: ( + url: string, + config?: TGApp.App.Response.ReqConfParams, + ): Promise> => request("POST", url, config), + + /** + * 通用请求方法 + * @since Beta v0.10.0 + * @param method - 请求方法 + * @param url - 请求地址 + * @param config - 请求配置 + * @returns 响应对象 + */ + request: ( + method: TGApp.App.Response.ReqMethodEnum, + url: string, + config?: TGApp.App.Response.ReqConfParams, + ): Promise> => request(method, url, config), +}; + +export default TGHttps;