|
@@ -1,11 +1,3 @@
|
|
|
-import axios, {
|
|
|
|
|
- type AxiosError,
|
|
|
|
|
- type AxiosInstance,
|
|
|
|
|
- type AxiosRequestConfig,
|
|
|
|
|
- type AxiosResponse,
|
|
|
|
|
- type InternalAxiosRequestConfig,
|
|
|
|
|
-} from "axios"
|
|
|
|
|
-
|
|
|
|
|
// 定义通用的 API 响应接口
|
|
// 定义通用的 API 响应接口
|
|
|
interface ApiResponse<T = any> {
|
|
interface ApiResponse<T = any> {
|
|
|
data: T
|
|
data: T
|
|
@@ -13,174 +5,230 @@ interface ApiResponse<T = any> {
|
|
|
msg: string
|
|
msg: string
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 扩展 Axios 请求配置
|
|
|
|
|
-interface RequestConfig extends AxiosRequestConfig {
|
|
|
|
|
|
|
+interface RequestConfig {
|
|
|
timeout?: number
|
|
timeout?: number
|
|
|
headers?: Record<string, string>
|
|
headers?: Record<string, string>
|
|
|
|
|
+ signal?: AbortSignal
|
|
|
|
|
+ // Next.js 缓存选项
|
|
|
|
|
+ cache?: "force-cache" | "no-store" | "no-cache" | "default"
|
|
|
|
|
+ next?: {
|
|
|
|
|
+ revalidate?: number | false
|
|
|
|
|
+ tags?: string[]
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 定义可能的错误类型
|
|
// 定义可能的错误类型
|
|
|
-type HttpClientError = AxiosError | Error
|
|
|
|
|
|
|
+type HttpClientError = Error
|
|
|
|
|
|
|
|
class ServerHttpClient {
|
|
class ServerHttpClient {
|
|
|
- private instance: AxiosInstance
|
|
|
|
|
|
|
+ private baseURL: string
|
|
|
|
|
+ private defaultTimeout: number
|
|
|
|
|
+ private defaultHeaders: Record<string, string>
|
|
|
|
|
|
|
|
constructor() {
|
|
constructor() {
|
|
|
- // 创建axios实例
|
|
|
|
|
- this.instance = axios.create({
|
|
|
|
|
- baseURL: process.env.NEXT_PUBLIC_API_BASE ?? "http://localhost:8080",
|
|
|
|
|
- timeout: 10000,
|
|
|
|
|
- headers: {
|
|
|
|
|
- "Content-Type": "application/json",
|
|
|
|
|
- },
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
- this.setupRequestInterceptors()
|
|
|
|
|
- this.setupResponseInterceptors()
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private setupRequestInterceptors() {
|
|
|
|
|
- this.instance.interceptors.request.use(
|
|
|
|
|
- (config: InternalAxiosRequestConfig) => {
|
|
|
|
|
- return config
|
|
|
|
|
- },
|
|
|
|
|
- (error: any) => {
|
|
|
|
|
- return Promise.reject(error)
|
|
|
|
|
- },
|
|
|
|
|
- )
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private setupResponseInterceptors() {
|
|
|
|
|
- this.instance.interceptors.response.use(
|
|
|
|
|
- (response: AxiosResponse<ApiResponse<any>>) => {
|
|
|
|
|
- return response
|
|
|
|
|
- },
|
|
|
|
|
- (error: AxiosError) => {
|
|
|
|
|
- // 服务端只返回错误,不做 UI 提示
|
|
|
|
|
- return Promise.reject(error)
|
|
|
|
|
- },
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ this.baseURL = process.env.NEXT_PUBLIC_API_BASE ?? "http://localhost:8080"
|
|
|
|
|
+ this.defaultTimeout = 10000
|
|
|
|
|
+ this.defaultHeaders = {
|
|
|
|
|
+ "Content-Type": "application/json",
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // GET 请求
|
|
|
|
|
async serverGet<TResponse = any, TParams extends Record<string, any> | null = Record<string, any> | null>(
|
|
async serverGet<TResponse = any, TParams extends Record<string, any> | null = Record<string, any> | null>(
|
|
|
url: string,
|
|
url: string,
|
|
|
params?: TParams,
|
|
params?: TParams,
|
|
|
config?: RequestConfig & { arrayFormat?: "indices" | "repeat" | "comma" },
|
|
config?: RequestConfig & { arrayFormat?: "indices" | "repeat" | "comma" },
|
|
|
): Promise<ApiResponse<TResponse>> {
|
|
): Promise<ApiResponse<TResponse>> {
|
|
|
try {
|
|
try {
|
|
|
- const response = await this.instance.get<ApiResponse<TResponse>>(url, {
|
|
|
|
|
- params,
|
|
|
|
|
- paramsSerializer: (p) => {
|
|
|
|
|
- const parts: string[] = []
|
|
|
|
|
- for (const key in p) {
|
|
|
|
|
- const value = (p as any)[key]
|
|
|
|
|
- if (Array.isArray(value)) {
|
|
|
|
|
- switch (config?.arrayFormat ?? "indices") {
|
|
|
|
|
- case "repeat":
|
|
|
|
|
- value.forEach((v) => parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(v)}`))
|
|
|
|
|
- break
|
|
|
|
|
- case "comma":
|
|
|
|
|
- parts.push(`${encodeURIComponent(key)}=${value.map((v) => encodeURIComponent(v)).join(",")}`)
|
|
|
|
|
- break
|
|
|
|
|
- case "indices":
|
|
|
|
|
- default:
|
|
|
|
|
- value.forEach((v, i) => parts.push(`${encodeURIComponent(key)}[${i}]=${encodeURIComponent(v)}`))
|
|
|
|
|
- break
|
|
|
|
|
- }
|
|
|
|
|
- } else if (value !== undefined && value !== null) {
|
|
|
|
|
- parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- return parts.join("&")
|
|
|
|
|
|
|
+ const fullUrl = this.buildUrl(url, params, config?.arrayFormat)
|
|
|
|
|
+ const controller = new AbortController()
|
|
|
|
|
+ const timeoutId = setTimeout(() => controller.abort(), config?.timeout ?? this.defaultTimeout)
|
|
|
|
|
+
|
|
|
|
|
+ const response = await fetch(fullUrl, {
|
|
|
|
|
+ method: "GET",
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ ...this.defaultHeaders,
|
|
|
|
|
+ ...config?.headers,
|
|
|
},
|
|
},
|
|
|
- ...config,
|
|
|
|
|
|
|
+ signal: config?.signal ?? controller.signal,
|
|
|
|
|
+ cache: config?.cache,
|
|
|
|
|
+ next: config?.next,
|
|
|
})
|
|
})
|
|
|
- return response.data
|
|
|
|
|
|
|
+
|
|
|
|
|
+ clearTimeout(timeoutId)
|
|
|
|
|
+
|
|
|
|
|
+ if (!response.ok) {
|
|
|
|
|
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const data: ApiResponse<TResponse> = await response.json()
|
|
|
|
|
+ return data
|
|
|
} catch (error: any) {
|
|
} catch (error: any) {
|
|
|
throw this.handleError(error)
|
|
throw this.handleError(error)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // POST 请求
|
|
|
|
|
async serverPost<TResponse = any, TData extends Record<string, any> | null = Record<string, any> | null>(
|
|
async serverPost<TResponse = any, TData extends Record<string, any> | null = Record<string, any> | null>(
|
|
|
url: string,
|
|
url: string,
|
|
|
data?: TData,
|
|
data?: TData,
|
|
|
config?: RequestConfig,
|
|
config?: RequestConfig,
|
|
|
): Promise<ApiResponse<TResponse>> {
|
|
): Promise<ApiResponse<TResponse>> {
|
|
|
try {
|
|
try {
|
|
|
- const response = await this.instance.post<ApiResponse<TResponse>>(url, JSON.stringify(data), config)
|
|
|
|
|
- return response.data
|
|
|
|
|
|
|
+ const fullUrl = `${this.baseURL}${url}`
|
|
|
|
|
+ const controller = new AbortController()
|
|
|
|
|
+ const timeoutId = setTimeout(() => controller.abort(), config?.timeout ?? this.defaultTimeout)
|
|
|
|
|
+
|
|
|
|
|
+ const response = await fetch(fullUrl, {
|
|
|
|
|
+ method: "POST",
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ ...this.defaultHeaders,
|
|
|
|
|
+ ...config?.headers,
|
|
|
|
|
+ },
|
|
|
|
|
+ body: data ? JSON.stringify(data) : undefined,
|
|
|
|
|
+ signal: config?.signal ?? controller.signal,
|
|
|
|
|
+ cache: config?.cache,
|
|
|
|
|
+ next: config?.next,
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ clearTimeout(timeoutId)
|
|
|
|
|
+
|
|
|
|
|
+ if (!response.ok) {
|
|
|
|
|
+ const errorData = await response.json().catch(() => ({}))
|
|
|
|
|
+ throw new Error((errorData as any)?.msg || `HTTP ${response.status}: ${response.statusText}`)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const responseData: ApiResponse<TResponse> = await response.json()
|
|
|
|
|
+ return responseData
|
|
|
} catch (error: any) {
|
|
} catch (error: any) {
|
|
|
throw this.handleError(error)
|
|
throw this.handleError(error)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // PUT 请求
|
|
|
|
|
async serverPut<TResponse = any, TData extends Record<string, any> | null = Record<string, any> | null>(
|
|
async serverPut<TResponse = any, TData extends Record<string, any> | null = Record<string, any> | null>(
|
|
|
url: string,
|
|
url: string,
|
|
|
data?: TData,
|
|
data?: TData,
|
|
|
config?: RequestConfig,
|
|
config?: RequestConfig,
|
|
|
): Promise<ApiResponse<TResponse>> {
|
|
): Promise<ApiResponse<TResponse>> {
|
|
|
try {
|
|
try {
|
|
|
- const response = await this.instance.put<ApiResponse<TResponse>>(url, JSON.stringify(data), config)
|
|
|
|
|
- return response.data
|
|
|
|
|
|
|
+ const fullUrl = `${this.baseURL}${url}`
|
|
|
|
|
+ const controller = new AbortController()
|
|
|
|
|
+ const timeoutId = setTimeout(() => controller.abort(), config?.timeout ?? this.defaultTimeout)
|
|
|
|
|
+
|
|
|
|
|
+ const response = await fetch(fullUrl, {
|
|
|
|
|
+ method: "PUT",
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ ...this.defaultHeaders,
|
|
|
|
|
+ ...config?.headers,
|
|
|
|
|
+ },
|
|
|
|
|
+ body: data ? JSON.stringify(data) : undefined,
|
|
|
|
|
+ signal: config?.signal ?? controller.signal,
|
|
|
|
|
+ cache: config?.cache,
|
|
|
|
|
+ next: config?.next,
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ clearTimeout(timeoutId)
|
|
|
|
|
+
|
|
|
|
|
+ if (!response.ok) {
|
|
|
|
|
+ const errorData = await response.json().catch(() => ({}))
|
|
|
|
|
+ throw new Error((errorData as any)?.msg || `HTTP ${response.status}: ${response.statusText}`)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const responseData: ApiResponse<TResponse> = await response.json()
|
|
|
|
|
+ return responseData
|
|
|
} catch (error: any) {
|
|
} catch (error: any) {
|
|
|
throw this.handleError(error)
|
|
throw this.handleError(error)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // DELETE 请求
|
|
|
|
|
async serverDelete<TResponse = any, TParams extends Record<string, any> | null = Record<string, any> | null>(
|
|
async serverDelete<TResponse = any, TParams extends Record<string, any> | null = Record<string, any> | null>(
|
|
|
url: string,
|
|
url: string,
|
|
|
params?: TParams,
|
|
params?: TParams,
|
|
|
config?: RequestConfig & { arrayFormat?: "indices" | "repeat" | "comma" },
|
|
config?: RequestConfig & { arrayFormat?: "indices" | "repeat" | "comma" },
|
|
|
): Promise<ApiResponse<TResponse>> {
|
|
): Promise<ApiResponse<TResponse>> {
|
|
|
try {
|
|
try {
|
|
|
- const response = await this.instance.delete<ApiResponse<TResponse>>(url, {
|
|
|
|
|
- params,
|
|
|
|
|
- paramsSerializer: (p) => {
|
|
|
|
|
- const parts: string[] = []
|
|
|
|
|
- for (const key in p) {
|
|
|
|
|
- const value = (p as any)[key]
|
|
|
|
|
- if (Array.isArray(value)) {
|
|
|
|
|
- switch (config?.arrayFormat ?? "indices") {
|
|
|
|
|
- case "repeat":
|
|
|
|
|
- value.forEach((v) => parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(v)}`))
|
|
|
|
|
- break
|
|
|
|
|
- case "comma":
|
|
|
|
|
- parts.push(`${encodeURIComponent(key)}=${value.map((v) => encodeURIComponent(v)).join(",")}`)
|
|
|
|
|
- break
|
|
|
|
|
- case "indices":
|
|
|
|
|
- default:
|
|
|
|
|
- value.forEach((v, i) => parts.push(`${encodeURIComponent(key)}[${i}]=${encodeURIComponent(v)}`))
|
|
|
|
|
- break
|
|
|
|
|
- }
|
|
|
|
|
- } else if (value !== undefined && value !== null) {
|
|
|
|
|
- parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- return parts.join("&")
|
|
|
|
|
|
|
+ const fullUrl = this.buildUrl(url, params, config?.arrayFormat)
|
|
|
|
|
+ const controller = new AbortController()
|
|
|
|
|
+ const timeoutId = setTimeout(() => controller.abort(), config?.timeout ?? this.defaultTimeout)
|
|
|
|
|
+
|
|
|
|
|
+ const response = await fetch(fullUrl, {
|
|
|
|
|
+ method: "DELETE",
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ ...this.defaultHeaders,
|
|
|
|
|
+ ...config?.headers,
|
|
|
},
|
|
},
|
|
|
- ...config,
|
|
|
|
|
|
|
+ signal: config?.signal ?? controller.signal,
|
|
|
|
|
+ cache: config?.cache,
|
|
|
|
|
+ next: config?.next,
|
|
|
})
|
|
})
|
|
|
- return response.data
|
|
|
|
|
|
|
+
|
|
|
|
|
+ clearTimeout(timeoutId)
|
|
|
|
|
+
|
|
|
|
|
+ if (!response.ok) {
|
|
|
|
|
+ const errorData = await response.json().catch(() => ({}))
|
|
|
|
|
+ throw new Error((errorData as any)?.msg || `HTTP ${response.status}: ${response.statusText}`)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const responseData: ApiResponse<TResponse> = await response.json()
|
|
|
|
|
+ return responseData
|
|
|
} catch (error: any) {
|
|
} catch (error: any) {
|
|
|
throw this.handleError(error)
|
|
throw this.handleError(error)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 错误处理
|
|
|
|
|
|
|
+ private buildUrl(
|
|
|
|
|
+ url: string,
|
|
|
|
|
+ params?: Record<string, any> | null,
|
|
|
|
|
+ arrayFormat: "indices" | "repeat" | "comma" = "indices",
|
|
|
|
|
+ ): string {
|
|
|
|
|
+ const fullUrl = `${this.baseURL}${url}`
|
|
|
|
|
+
|
|
|
|
|
+ if (!params) {
|
|
|
|
|
+ return fullUrl
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const searchParams = new URLSearchParams()
|
|
|
|
|
+
|
|
|
|
|
+ for (const key in params) {
|
|
|
|
|
+ const value = params[key]
|
|
|
|
|
+ if (Array.isArray(value)) {
|
|
|
|
|
+ switch (arrayFormat) {
|
|
|
|
|
+ case "repeat":
|
|
|
|
|
+ value.forEach((v) => searchParams.append(key, String(v)))
|
|
|
|
|
+ break
|
|
|
|
|
+ case "comma":
|
|
|
|
|
+ searchParams.set(key, value.map((v) => String(v)).join(","))
|
|
|
|
|
+ break
|
|
|
|
|
+ case "indices":
|
|
|
|
|
+ default:
|
|
|
|
|
+ value.forEach((v, i) => searchParams.set(`${key}[${i}]`, String(v)))
|
|
|
|
|
+ break
|
|
|
|
|
+ }
|
|
|
|
|
+ } else if (value !== undefined && value !== null) {
|
|
|
|
|
+ searchParams.set(key, String(value))
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const queryString = searchParams.toString()
|
|
|
|
|
+ return queryString ? `${fullUrl}?${queryString}` : fullUrl
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private handleError(error: any): HttpClientError {
|
|
private handleError(error: any): HttpClientError {
|
|
|
- if (axios.isAxiosError(error)) {
|
|
|
|
|
- const message = (error.response?.data as any)?.msg || error.message || "请求失败"
|
|
|
|
|
- return new Error(message)
|
|
|
|
|
|
|
+ if (error.name === "AbortError") {
|
|
|
|
|
+ return new Error("请求超时")
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ if (error instanceof TypeError && error.message.includes("fetch")) {
|
|
|
|
|
+ return new Error("网络连接失败")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
return error instanceof Error ? error : new Error("未知错误")
|
|
return error instanceof Error ? error : new Error("未知错误")
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- getInstance(): AxiosInstance {
|
|
|
|
|
- return this.instance
|
|
|
|
|
|
|
+ getInstance() {
|
|
|
|
|
+ return {
|
|
|
|
|
+ baseURL: this.baseURL,
|
|
|
|
|
+ timeout: this.defaultTimeout,
|
|
|
|
|
+ headers: this.defaultHeaders,
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|