| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274 |
- // src/types/request.ts
- /**
- * API 通用响应格式
- */
- export interface ApiResponse<T = any> {
- data: T
- code: number
- msg: string
- success?: boolean // 兼容部分后端返回格式
- }
- /**
- * 客户端请求配置
- * 剔除服务端专属的 cache/next 配置
- */
- export interface ClientRequestConfig {
- /** 请求超时时间(毫秒) */
- timeout?: number
- /** 请求头 */
- headers?: Record<string, string>
- /** 取消请求信号 */
- 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<string, string> = {
- '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<TResponse = any>(
- method: 'GET' | 'POST' | 'PUT' | 'DELETE',
- url: string,
- options: {
- params?: Record<string, any> | null // GET/DELETE 参数
- data?: Record<string, any> | null // POST/PUT 数据
- config?: ClientRequestConfig
- }
- ): Promise<ApiResponse<TResponse>> {
- 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<TResponse>
- // 兼容不同的后端响应格式
- 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<string, any>,
- 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<TResponse = any>(
- url: string,
- params?: Record<string, any> | null,
- config?: ClientGetDeleteConfig
- ): Promise<ApiResponse<TResponse>> {
- return this.request<TResponse>('GET', url, { params, config })
- }
- /**
- * 客户端 POST 请求
- */
- async post<TResponse = any>(
- url: string,
- data?: Record<string, any> | null,
- config?: ClientRequestConfig
- ): Promise<ApiResponse<TResponse>> {
- return this.request<TResponse>('POST', url, { data, config })
- }
- /**
- * 客户端 PUT 请求
- */
- async put<TResponse = any>(
- url: string,
- data?: Record<string, any> | null,
- config?: ClientRequestConfig
- ): Promise<ApiResponse<TResponse>> {
- return this.request<TResponse>('PUT', url, { data, config })
- }
- /**
- * 客户端 DELETE 请求
- */
- async delete<TResponse = any>(
- url: string,
- params?: Record<string, any> | null,
- config?: ClientGetDeleteConfig
- ): Promise<ApiResponse<TResponse>> {
- return this.request<TResponse>('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 }
|