// src/types/request.ts /** * API 通用响应格式 */ export interface ApiResponse { data: T code: number msg: string success?: boolean // 兼容部分后端返回格式 } /** * 客户端请求配置 * 剔除服务端专属的 cache/next 配置 */ export interface ClientRequestConfig { /** 请求超时时间(毫秒) */ timeout?: number /** 请求头 */ headers?: Record /** 取消请求信号 */ signal?: AbortSignal /** 是否携带凭证(cookie) */ credentials?: RequestCredentials } /** * GET/DELETE 请求专用配置 */ export interface ClientGetDeleteConfig extends ClientRequestConfig { /** 数组参数格式化方式 */ arrayFormat?: "indices" | "repeat" | "comma" } /** * 客户端请求错误类型 */ export type ClientRequestError = Error & { /** HTTP 状态码 */ statusCode?: number /** 原始错误信息 */ originalError?: Error /** 后端返回的错误信息 */ responseMsg?: string } /** * 客户端请求工具类 * 专用于 Next.js "use client" 组件 */ class ClientHttpClient { // 基础配置 private readonly baseURL: string private readonly defaultTimeout: number = 15000 // 默认超时 15 秒 private readonly defaultHeaders: Record = { 'Content-Type': 'application/json;charset=UTF-8', 'X-Client-Type': 'browser', // 标记客户端请求 } constructor() { // 从环境变量获取基础地址,兜底值保证代码不报错 this.baseURL = process.env.NEXT_PUBLIC_API_BASE ?? "http://localhost:8080" } /** * 通用请求核心方法 */ private async request( method: 'GET' | 'POST' | 'PUT' | 'DELETE', url: string, options: { params?: Record | null // GET/DELETE 参数 data?: Record | null // POST/PUT 数据 config?: ClientRequestConfig } ): Promise> { const { params, data, config = {} } = options let fullUrl = `${this.baseURL}${url}` // 处理 GET/DELETE 请求参数 if ((method === 'GET' || method === 'DELETE') && params) { fullUrl = this.buildUrl(url, params, (config as ClientGetDeleteConfig)?.arrayFormat) } // 创建超时控制器 const controller = new AbortController() const timeoutId = setTimeout(() => controller.abort(), config.timeout || this.defaultTimeout) try { // 构建请求选项 const fetchOptions: RequestInit = { method, headers: { ...this.defaultHeaders, ...config.headers }, signal: config.signal || controller.signal, credentials: config.credentials || 'same-origin', // 默认携带同域 cookie } // 处理 POST/PUT 请求体 if ((method === 'POST' || method === 'PUT') && data) { fetchOptions.body = JSON.stringify(data) } // 发送请求 const response = await fetch(fullUrl, fetchOptions) clearTimeout(timeoutId) // 处理 HTTP 错误状态 if (!response.ok) { const error = new Error(`请求失败:${response.statusText}`) as ClientRequestError error.statusCode = response.status // 尝试解析后端返回的错误信息 try { const errorData = await response.json() as ApiResponse // (确保类型安全) error.responseMsg = errorData.msg ?? errorData.data?.msg ?? error.message ?? '请求失败' error.message = error.responseMsg } catch { // 非 JSON 响应时使用默认提示 error.message = `HTTP ${response.status}: 请求失败` } throw error } // 解析响应数据 const responseData = await response.json() as ApiResponse // 兼容不同的后端响应格式 if (responseData.code !== undefined && responseData.code !== 200 && !responseData.success) { const error = new Error(responseData.msg || '业务处理失败') as ClientRequestError error.statusCode = responseData.code error.responseMsg = responseData.msg throw error } return responseData } catch (error) { clearTimeout(timeoutId) // 统一错误处理 throw this.handleError(error as Error) } } /** * 构建带参数的 URL */ private buildUrl( url: string, params: Record, arrayFormat: "indices" | "repeat" | "comma" = "repeat" ): string { if (!params || Object.keys(params).length === 0) { return `${this.baseURL}${url}` } const searchParams = new URLSearchParams() Object.entries(params).forEach(([key, value]) => { if (value === undefined || value === null) return if (Array.isArray(value)) { switch (arrayFormat) { case 'comma': searchParams.set(key, value.map(String).join(',')) break case 'repeat': value.forEach(v => searchParams.append(key, String(v))) break case 'indices': default: value.forEach((v, i) => searchParams.set(`${key}[${i}]`, String(v))) break } } else { searchParams.set(key, String(value)) } }) const queryString = searchParams.toString() return queryString ? `${this.baseURL}${url}?${queryString}` : `${this.baseURL}${url}` } /** * 统一错误处理(客户端友好提示) */ private handleError(error: Error): ClientRequestError { const clientError = error as ClientRequestError clientError.originalError = { ...error } // 超时错误 if (error.name === 'AbortError') { clientError.message = '请求超时,请检查网络或稍后重试' clientError.statusCode = 408 } // 网络错误 else if (error instanceof TypeError && error.message.includes('fetch')) { clientError.message = '网络连接失败,请检查您的网络设置' clientError.statusCode = 0 } // 未知错误 else if (!clientError.statusCode) { clientError.message = clientError.message || '请求失败,请联系客服' clientError.statusCode = 500 } return clientError } /** * 客户端 GET 请求 */ async get( url: string, params?: Record | null, config?: ClientGetDeleteConfig ): Promise> { return this.request('GET', url, { params, config }) } /** * 客户端 POST 请求 */ async post( url: string, data?: Record | null, config?: ClientRequestConfig ): Promise> { return this.request('POST', url, { data, config }) } /** * 客户端 PUT 请求 */ async put( url: string, data?: Record | null, config?: ClientRequestConfig ): Promise> { return this.request('PUT', url, { data, config }) } /** * 客户端 DELETE 请求 */ async delete( url: string, params?: Record | null, config?: ClientGetDeleteConfig ): Promise> { return this.request('DELETE', url, { params, config }) } /** * 获取当前配置信息(调试用) */ getConfig() { return { baseURL: this.baseURL, defaultTimeout: this.defaultTimeout, defaultHeaders: { ...this.defaultHeaders } } } } // 创建单例实例(避免重复创建) const clientHttpClient = new ClientHttpClient() // 导出简化的方法名(方便使用) export const clientGet = clientHttpClient.get.bind(clientHttpClient) export const clientPost = clientHttpClient.post.bind(clientHttpClient) export const clientPut = clientHttpClient.put.bind(clientHttpClient) export const clientDelete = clientHttpClient.delete.bind(clientHttpClient) export { clientHttpClient }