type RequestConfig = { url: string method: string headers: Record timeout?: number body?: BodyInit | null } type ResponseWrapper = { data: T status: number statusText: string headers: Headers } type RequestInterceptor = { onFulfilled?: (config: RequestConfig) => RequestConfig | Promise onRejected?: (error: unknown) => RequestConfig | Promise } type ResponseInterceptor = { onFulfilled?: ( response: ResponseWrapper ) => ResponseWrapper | Promise> onRejected?: (error: unknown) => unknown | Promise } export class RequestExtend { private baseURL: string private timeout: number private headers: Record private static defaultBaseURL = '' private static defaultTimeout = 30000 private static defaultHeaders: Record = { 'Content-Type': 'application/json' } private static requestInterceptors: RequestInterceptor[] = [] private static responseInterceptors: ResponseInterceptor[] = [] constructor(config?: { baseURL?: string timeout?: number headers?: Record }) { this.baseURL = config?.baseURL || RequestExtend.defaultBaseURL this.timeout = config?.timeout || RequestExtend.defaultTimeout this.headers = { ...RequestExtend.defaultHeaders, ...config?.headers } } static setDefaultConfig(config: { baseURL?: string timeout?: number headers?: Record }) { if (config.baseURL) { this.defaultBaseURL = config.baseURL } if (config.timeout) { this.defaultTimeout = config.timeout } if (config.headers) { this.defaultHeaders = { ...this.defaultHeaders, ...config.headers } } } static addRequestInterceptor(interceptor: RequestInterceptor) { this.requestInterceptors.push(interceptor) } static addResponseInterceptor(interceptor: ResponseInterceptor) { this.responseInterceptors.push(interceptor) } private async executeRequestInterceptors(config: RequestConfig): Promise { let result = config for (const interceptor of RequestExtend.requestInterceptors) { try { if (interceptor.onFulfilled) { result = await interceptor.onFulfilled(result) } } catch (error) { if (interceptor.onRejected) { result = await interceptor.onRejected(error) } else { throw error } } } return result } private async executeResponseInterceptors( response: ResponseWrapper ): Promise> { let result = response for (const interceptor of RequestExtend.responseInterceptors) { try { if (interceptor.onFulfilled) { result = await interceptor.onFulfilled(result) } } catch (error) { if (interceptor.onRejected) { const interceptedError = await interceptor.onRejected(error) throw interceptedError } throw error } } return result } private async executeResponseErrorInterceptors(error: unknown): Promise { let result = error for (const interceptor of RequestExtend.responseInterceptors) { if (!interceptor.onRejected) { continue } try { result = await interceptor.onRejected(result) } catch (interceptorError) { result = interceptorError } } return result } private buildURL(url: string, params?: Record): string { let fullURL = url if (!url.startsWith('http://') && !url.startsWith('https://')) { fullURL = this.baseURL + url } if (params && Object.keys(params).length > 0) { const searchParams = new URLSearchParams() for (const key in params) { const value = params[key] if (value !== undefined && value !== null) { searchParams.append(key, String(value)) } } const queryString = searchParams.toString() if (queryString) { fullURL += (fullURL.includes('?') ? '&' : '?') + queryString } } return fullURL } private async parseResponseBody(response: Response): Promise { const contentType = response.headers.get('content-type') || '' if (contentType.includes('application/json')) { return response.json() } if (contentType.includes('application/octet-stream')) { return response.blob() } const text = await response.text() if (!text) { return text } try { return JSON.parse(text) } catch { return text } } private getErrorMessage(result: unknown, fallback: string): string { if (typeof result === 'string' && result.trim()) { return result } if (result && typeof result === 'object') { const payload = result as { msg?: string; message?: string } return payload.msg || payload.message || fallback } return fallback } private async request( method: string, url: string, options: { params?: Record data?: unknown headers?: Record } = {} ): Promise { const { params, data, headers } = options const config: RequestConfig = { method: method.toUpperCase(), headers: { ...this.headers, ...headers }, timeout: this.timeout, url: params && method.toUpperCase() === 'GET' ? this.buildURL(url, params) : this.buildURL(url) } if (data && method.toUpperCase() !== 'GET' && method.toUpperCase() !== 'HEAD') { if (data instanceof FormData) { config.body = data delete config.headers['Content-Type'] } else { config.body = JSON.stringify(data) } } const interceptedConfig = await this.executeRequestInterceptors(config) try { const response = await fetch(interceptedConfig.url, { method: interceptedConfig.method, headers: interceptedConfig.headers, body: interceptedConfig.body, signal: interceptedConfig.timeout ? AbortSignal.timeout(interceptedConfig.timeout) : undefined }) const result = await this.parseResponseBody(response) const wrappedResponse: ResponseWrapper = { data: result, status: response.status, statusText: response.statusText, headers: response.headers } if (!response.ok) { const handledError = await this.executeResponseErrorInterceptors({ ...wrappedResponse, message: this.getErrorMessage(result, response.statusText || 'Request failed') }) throw handledError } const interceptedResponse = await this.executeResponseInterceptors(wrappedResponse) return interceptedResponse.data as T } catch (error) { if (error && typeof error === 'object' && ('status' in error || 'code' in error)) { throw error } const handledError = await this.executeResponseErrorInterceptors({ message: error instanceof Error ? error.message : '网络请求失败', code: error && typeof error === 'object' && 'code' in error ? (error as { code?: string }).code || 'NETWORK_ERROR' : 'NETWORK_ERROR', status: error && typeof error === 'object' && 'status' in error ? Number((error as { status?: number }).status || 0) : 0 }) throw handledError } } async get(url: string, options?: { params?: Record headers?: Record }): Promise { return this.request('GET', url, options) } async post(url: string, data?: unknown, options?: { params?: Record headers?: Record }): Promise { return this.request('POST', url, { ...options, data }) } async put(url: string, data?: unknown, options?: { params?: Record headers?: Record }): Promise { return this.request('PUT', url, { ...options, data }) } async delete(url: string, options?: { params?: Record headers?: Record }): Promise { return this.request('DELETE', url, options) } async patch(url: string, data?: unknown, options?: { params?: Record headers?: Record }): Promise { return this.request('PATCH', url, { ...options, data }) } async upload(url: string, file: File | FormData, options?: { params?: Record headers?: Record }): Promise { const formData = file instanceof FormData ? file : new FormData() if (file instanceof File) { formData.append('file', file) } return this.request('POST', url, { ...options, data: formData }) } async download(url: string, filename?: string): Promise { const response = await fetch(this.buildURL(url), { method: 'GET', headers: this.headers }) if (!response.ok) { throw new Error('下载失败') } const blob = await response.blob() const downloadURL = window.URL.createObjectURL(blob) const link = document.createElement('a') link.href = downloadURL link.download = filename || 'download' document.body.appendChild(link) link.click() document.body.removeChild(link) window.URL.revokeObjectURL(downloadURL) } }