Quellcode durchsuchen

跟新post请求客户端

丁烨烨 vor 4 Monaten
Ursprung
Commit
9beaa6b13f
2 geänderte Dateien mit 327 neuen und 295 gelöschten Zeilen
  1. 164 151
      src/components/products/ProductGridClient.tsx
  2. 163 144
      src/utils/request.ts

+ 164 - 151
src/components/products/ProductGridClient.tsx

@@ -1,10 +1,10 @@
 "use client"
 
-import React, {useState} from "react"
+import React, { useState, useEffect } from "react"
 import Image from "next/image"
-import {Button, Form, Input, message, Modal, Pagination} from "antd"
+import { Button, Form, Input, message, Modal, Pagination, ConfigProvider } from "antd"
 import Link from "next/link";
-import {serverPost} from "@/utils/request";
+import { clientPost } from "@/utils/request";
 
 // 留言表单数据接口(移除quantity字段)
 interface QuoteFormValues {
@@ -22,11 +22,20 @@ interface SaveQuotePayload {
     phone: string;
 }
 
+// 补充 ProductItem 接口定义(避免类型报错)
+interface ProductItem {
+    productId: string;
+    productName: string;
+    productUrl?: string;
+    productIntroduction: string;
+    productModel: string;
+}
+
 interface ProductGridProps {
     products: ProductItem[]
 }
 
-export default function ProductGrid({products}: ProductGridProps) {
+export default function ProductGrid({ products }: ProductGridProps) {
     const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL as string
 
     // 分页控制
@@ -40,6 +49,8 @@ export default function ProductGrid({products}: ProductGridProps) {
     // 添加加载状态,防止重复提交
     const [submitting, setSubmitting] = useState(false)
 
+    const [messageApi, contextHolder] = message.useMessage()
+
     // 当前页数据
     const startIndex = (currentPage - 1) * pageSize
     const currentProducts = products.slice(startIndex, startIndex + pageSize)
@@ -53,17 +64,15 @@ export default function ProductGrid({products}: ProductGridProps) {
             form.setFieldsValue({
                 productId: product.productId,
                 productName: product.productName
-                // 移除quantity相关初始化
             })
         }, 0)
     }
 
-    // 关闭弹窗
+    // 关闭弹窗(优化:增加延迟,确保提示显示完成)
     const handleCancel = () => {
         setIsModalOpen(false)
         setCurrentProduct(null)
         form.resetFields()
-        // 重置提交状态
         setSubmitting(false)
     }
 
@@ -72,162 +81,166 @@ export default function ProductGrid({products}: ProductGridProps) {
         try {
             // 1. 设置提交中状态,禁用按钮
             setSubmitting(true)
-            const response = await serverPost<Page<QuoteFormValues>, SaveQuotePayload>(
-                "/webSite/save",
-                {
-                    content: values.message,
-                    deviceName: values.productName,
-                    name: values.name,
-                    phone: values.phone
-                }, {
-                    next: {
-                        revalidate: 30
-                    },
-                    cache: "force-cache"
+
+            const response = await clientPost('/webSite/save', {
+                content: values.message,
+                deviceName: values.productName,
+                name: values.name,
+                phone: values.phone
+            }, {
+                timeout: 15000,
+                headers: {
+                    'X-Client-Type': 'browser'
                 }
-            )
-            // 5. 提交成功提示
-            message.success("报价请求提交成功!我们会尽快联系你")
-            // 6. 关闭弹窗并重置表单
+            })
+
+            // 关键修复2:使用独立的 messageApi 显示提示
+            messageApi.success("报价请求提交成功!我们会尽快联系你")
             handleCancel()
         } catch (error) {
-            // 7. 异常处理
+            // 异常处理也使用 messageApi
             const errorMsg = error instanceof Error ? error.message : "提交失败,请稍后重试"
-            message.error(errorMsg)
-            console.error("提交报价失败:", error)
+            messageApi.error(errorMsg)
         } finally {
-            // 8. 无论成功失败,都重置提交状态
-            setSubmitting(false)
+            // 只有失败时才在这里重置状态(成功时在提示关闭后重置)
+            if (submitting) {
+                setSubmitting(false)
+            }
         }
     }
 
     return (
-        <div className="flex flex-col gap-6">
-            {/* 产品网格 */}
-            <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6">
-                {currentProducts.map((product) => (
-                    <div
-                        key={product.productId}
-                        className="bg-white rounded-lg overflow-hidden shadow-lg hover:shadow-xl transition-all duration-300 hover:scale-[1.02]"
+        // 关键修复3:添加 ConfigProvider 包裹组件,确保 AntD 上下文正确
+        <ConfigProvider>
+            {/* 必须添加 contextHolder 到组件中,否则 message 无法显示 */}
+            {contextHolder}
+
+            <div className="flex flex-col gap-6">
+                {/* 产品网格 */}
+                <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6">
+                    {currentProducts.map((product) => (
+                        <div
+                            key={product.productId}
+                            className="bg-white rounded-lg overflow-hidden shadow-lg hover:shadow-xl transition-all duration-300 hover:scale-[1.02]"
+                        >
+                            <div className="p-2">
+                                <Image
+                                    className={"h-50 object-contain"}
+                                    src={product.productUrl ? BASE_URL + product.productUrl : "/assets/productions/2.png"}
+                                    alt={product.productName}
+                                    width={1000}
+                                    height={1000}
+                                    priority={false}
+                                />
+                            </div>
+                            <div className="p-4 sm:py-6 text-center">
+                                <h3 className="font-bold text-lg mb-1">{product.productName}</h3>
+                                <p className="text-sm text-gray-500 mb-2 line-clamp-1">{product.productIntroduction}</p>
+                                <p className="text-sm mb-3">{product.productModel}</p>
+
+                                <div className="w-full flex justify-center">
+                                    <button
+                                        onClick={() => showModal(product)}
+                                        className="bg-red-600 text-white !text-white px-4 !px-4 py-1.5 !py-1.5 rounded hover:bg-red-700 transition-colors duration-200"
+                                    >
+                                        获取报价
+                                    </button>
+                                    <Link
+                                        href={`/products/${product.productId}`}
+                                        className="bg-blue-600 text-white !text-white px-4 !px-4 py-1.5 !py-1.5 rounded hover:bg-blue-700 transition-colors duration-200 ml-2"
+                                    >
+                                        了解详情
+                                    </Link>
+                                </div>
+                            </div>
+                        </div>
+                    ))}
+                </div>
+
+                {/* 分页器 */}
+                <div className="flex justify-center">
+                    <Pagination
+                        current={currentPage}
+                        pageSize={pageSize}
+                        total={products.length}
+                        onChange={(page) => setCurrentPage(page)}
+                        showSizeChanger={false}
+                    />
+                </div>
+
+                {/* 报价弹窗(移除采购数量相关内容) */}
+                <Modal
+                    title={`获取 "${currentProduct?.productName}" 报价`}
+                    open={isModalOpen}
+                    onCancel={handleCancel}
+                    footer={null}
+                    maskClosable={false}
+                    width={600}
+                >
+                    <Form
+                        form={form}
+                        layout="vertical"
+                        onFinish={handleSubmit}
                     >
-                        <div className="p-2">
-                            <Image
-                                className={"h-50 object-contain"}
-                                src={product.productUrl ? BASE_URL + product.productUrl : "/assets/productions/2.png"}
-                                alt={product.productName}
-                                width={1000}
-                                height={1000}
-                                priority={false}
+                        {/* 隐藏字段 */}
+                        <Form.Item name="productId" hidden>
+                            <Input />
+                        </Form.Item>
+                        <Form.Item name="productName" hidden>
+                            <Input />
+                        </Form.Item>
+
+                        {/* 联系人姓名 */}
+                        <Form.Item
+                            name="name"
+                            label="联系人姓名"
+                            rules={[{ required: true, message: "请输入您的姓名" }]}
+                        >
+                            <Input placeholder="请输入您的姓名" />
+                        </Form.Item>
+
+                        {/* 联系电话 */}
+                        <Form.Item
+                            name="phone"
+                            label="联系电话"
+                            rules={[
+                                { required: true, message: "请输入您的联系电话" },
+                                { pattern: /^1[3-9]\d{9}$/, message: "请输入有效的手机号码" }
+                            ]}
+                        >
+                            <Input placeholder="请输入您的手机号码" />
+                        </Form.Item>
+
+                        {/* 留言/备注 */}
+                        <Form.Item
+                            name="message"
+                            label="留言/备注"
+                        >
+                            <Input.TextArea
+                                rows={4}
+                                placeholder="请输入您的其他需求或备注信息(选填)"
                             />
-                        </div>
-                        <div className="p-4 sm:py-6 text-center">
-                            <h3 className="font-bold text-lg mb-1">{product.productName}</h3>
-                            <p className="text-sm text-gray-500 mb-2 line-clamp-1">{product.productIntroduction}</p>
-                            <p className="text-sm mb-3">{product.productModel}</p>
-
-                            <div className="w-full flex justify-center">
-                                <button
-                                    onClick={() => showModal(product)}
-                                    className="bg-red-600 text-white !text-white px-4 !px-4 py-1.5 !py-1.5 rounded hover:bg-red-700 transition-colors duration-200"
+                        </Form.Item>
+
+                        {/* 表单操作按钮(添加加载状态) */}
+                        <Form.Item>
+                            <div className="flex justify-end gap-2">
+                                <Button onClick={handleCancel} type="default" disabled={submitting}>
+                                    取消
+                                </Button>
+                                <Button
+                                    type="primary"
+                                    htmlType="submit"
+                                    loading={submitting}
+                                    disabled={submitting}
                                 >
-                                    获取报价
-                                </button>
-                                <Link
-                                    href={`/products/${product.productId}`}
-                                    className="bg-blue-600 text-white !text-white px-4 !px-4 py-1.5 !py-1.5 rounded hover:bg-blue-700 transition-colors duration-200 ml-2"
-                                >
-                                    了解详情
-                                </Link>
+                                    提交报价请求
+                                </Button>
                             </div>
-                        </div>
-                    </div>
-                ))}
-            </div>
-
-            {/* 分页器 */}
-            <div className="flex justify-center">
-                <Pagination
-                    current={currentPage}
-                    pageSize={pageSize}
-                    total={products.length}
-                    onChange={(page) => setCurrentPage(page)}
-                    showSizeChanger={false}
-                />
+                        </Form.Item>
+                    </Form>
+                </Modal>
             </div>
-
-            {/* 报价弹窗(移除采购数量相关内容) */}
-            <Modal
-                title={`获取 "${currentProduct?.productName}" 报价`}
-                open={isModalOpen}
-                onCancel={handleCancel}
-                footer={null}
-                destroyOnClose={true}
-                maskClosable={false}
-                width={600}
-            >
-                <Form
-                    form={form}
-                    layout="vertical"
-                    onFinish={handleSubmit}
-                >
-                    {/* 隐藏字段 */}
-                    <Form.Item name="productId" hidden>
-                        <Input/>
-                    </Form.Item>
-                    <Form.Item name="productName" hidden>
-                        <Input/>
-                    </Form.Item>
-
-                    {/* 联系人姓名 */}
-                    <Form.Item
-                        name="name"
-                        label="联系人姓名"
-                        rules={[{required: true, message: "请输入您的姓名"}]}
-                    >
-                        <Input placeholder="请输入您的姓名"/>
-                    </Form.Item>
-
-                    {/* 联系电话 */}
-                    <Form.Item
-                        name="phone"
-                        label="联系电话"
-                        rules={[
-                            {required: true, message: "请输入您的联系电话"},
-                            {pattern: /^1[3-9]\d{9}$/, message: "请输入有效的手机号码"}
-                        ]}
-                    >
-                        <Input placeholder="请输入您的手机号码"/>
-                    </Form.Item>
-
-                    {/* 留言/备注 */}
-                    <Form.Item
-                        name="message"
-                        label="留言/备注"
-                    >
-                        <Input.TextArea
-                            rows={4}
-                            placeholder="请输入您的其他需求或备注信息(选填)"
-                        />
-                    </Form.Item>
-
-                    {/* 表单操作按钮(添加加载状态) */}
-                    <Form.Item>
-                        <div className="flex justify-end gap-2">
-                            <Button onClick={handleCancel} type="default" disabled={submitting}>
-                                取消
-                            </Button>
-                            <Button
-                                type="primary"
-                                htmlType="submit"
-                                loading={submitting} // 提交中显示加载动画
-                                disabled={submitting} // 禁用按钮防止重复提交
-                            >
-                                提交报价请求
-                            </Button>
-                        </div>
-                    </Form.Item>
-                </Form>
-            </Modal>
-        </div>
+        </ConfigProvider>
     )
-}
+}

+ 163 - 144
src/utils/request.ts

@@ -9,7 +9,7 @@ interface RequestConfig {
   timeout?: number
   headers?: Record<string, string>
   signal?: AbortSignal
-  // Next.js 缓存选项
+  // Next.js 缓存选项(仅服务端有效)
   cache?: "force-cache" | "no-store" | "no-cache" | "default"
   next?: {
     revalidate?: number | false
@@ -17,13 +17,22 @@ interface RequestConfig {
   }
 }
 
+// 扩展 GET/DELETE 请求的配置(增加数组参数格式化)
+interface GetDeleteRequestConfig extends RequestConfig {
+  arrayFormat?: "indices" | "repeat" | "comma"
+}
+
 // 定义可能的错误类型
-type HttpClientError = Error
+type HttpClientError = Error & {
+  statusCode?: number // 新增:记录 HTTP 状态码
+  originalError?: Error // 新增:保留原始错误
+  isClientError?: boolean // 新增:标记是否为客户端错误
+}
 
 class ServerHttpClient {
-  private baseURL: string
-  private defaultTimeout: number
-  private defaultHeaders: Record<string, string>
+  private readonly baseURL: string
+  private readonly defaultTimeout: number
+  private readonly defaultHeaders: Record<string, string>
 
   constructor() {
     this.baseURL = process.env.NEXT_PUBLIC_API_BASE ?? "http://localhost:8080"
@@ -33,212 +42,222 @@ class ServerHttpClient {
     }
   }
 
-  async serverGet<TResponse = any, TParams extends Record<string, any> | null = Record<string, any> | null>(
-    url: string,
-    params?: TParams,
-    config?: RequestConfig & { arrayFormat?: "indices" | "repeat" | "comma" },
-  ): Promise<ApiResponse<TResponse>> {
-    try {
-      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,
-        },
-        signal: config?.signal ?? controller.signal,
-        cache: config?.cache,
-        next: config?.next,
-      })
-
-      clearTimeout(timeoutId)
-
-      if (!response.ok) {
-        throw new Error(`HTTP ${response.status}: ${response.statusText}`)
+  /**
+   * 通用请求方法(核心复用逻辑)
+   */
+  private async request<TResponse = any>(
+      method: "GET" | "POST" | "PUT" | "DELETE",
+      url: string,
+      options: {
+        params?: Record<string, any> | null
+        data?: Record<string, any> | null
+        config?: RequestConfig & { arrayFormat?: "indices" | "repeat" | "comma" }
+        isClientRequest?: boolean // 标记是否为客户端请求
       }
+  ): Promise<ApiResponse<TResponse>> {
+    const { params, data, config, isClientRequest = false } = options
+    let fullUrl = `${this.baseURL}${url}`
 
-      const data: ApiResponse<TResponse> = await response.json()
-      return data
-    } catch (error: any) {
-      throw this.handleError(error)
+    // 处理 GET/DELETE 的 URL 参数
+    if ((method === "GET" || method === "DELETE") && params) {
+      fullUrl = this.buildUrl(url, params, config?.arrayFormat)
     }
-  }
 
-  async serverPost<TResponse = any, TData extends Record<string, any> | null = Record<string, any> | null>(
-    url: string,
-    data?: TData,
-    config?: RequestConfig,
-  ): Promise<ApiResponse<TResponse>> {
+    // 创建请求控制器(处理超时)
+    const controller = new AbortController()
+    const timeoutId = setTimeout(() => controller.abort(), config?.timeout ?? this.defaultTimeout)
+
     try {
-      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,
-        },
+      // 客户端请求移除服务端缓存配置
+      const fetchOptions: RequestInit = {
+        method,
+        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) {
-      throw this.handleError(error)
-    }
-  }
+      // 仅服务端请求保留缓存配置
+      if (!isClientRequest) {
+        fetchOptions.cache = config?.cache as RequestCache
+        ;(fetchOptions as any).next = config?.next
+      }
 
-  async serverPut<TResponse = any, TData extends Record<string, any> | null = Record<string, any> | null>(
-    url: string,
-    data?: TData,
-    config?: RequestConfig,
-  ): Promise<ApiResponse<TResponse>> {
-    try {
-      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,
-      })
+      const response = await fetch(fullUrl, fetchOptions)
 
       clearTimeout(timeoutId)
 
+      // 处理 HTTP 错误状态
       if (!response.ok) {
-        const errorData = await response.json().catch(() => ({}))
-        throw new Error((errorData as any)?.msg || `HTTP ${response.status}: ${response.statusText}`)
+        let errorMsg = `HTTP ${response.status}: ${response.statusText}`
+        try {
+          const errorData = await response.json()
+          errorMsg = (errorData as ApiResponse)?.msg || errorMsg
+        } catch {
+          // 解析错误数据失败时使用默认提示
+        }
+
+        const error = new Error(errorMsg) as HttpClientError
+        error.statusCode = response.status
+        error.isClientError = isClientRequest
+        throw error
       }
 
+      // 解析响应数据
       const responseData: ApiResponse<TResponse> = await response.json()
       return responseData
-    } catch (error: any) {
-      throw this.handleError(error)
+    } catch (error) {
+      clearTimeout(timeoutId) // 确保超时器被清理
+      throw this.handleError(error as Error, isClientRequest)
     }
   }
 
-  async serverDelete<TResponse = any, TParams extends Record<string, any> | null = Record<string, any> | null>(
-    url: string,
-    params?: TParams,
-    config?: RequestConfig & { arrayFormat?: "indices" | "repeat" | "comma" },
+  /**
+   * 服务端 GET 请求方法
+   */
+  async serverGet<TResponse = any, TParams extends Record<string, any> | null = null>(
+      url: string,
+      params?: TParams,
+      config?: GetDeleteRequestConfig,
   ): Promise<ApiResponse<TResponse>> {
-    try {
-      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,
-        },
-        signal: config?.signal ?? controller.signal,
-        cache: config?.cache,
-        next: config?.next,
-      })
+    return this.request<TResponse>("GET", url, { params, config })
+  }
 
-      clearTimeout(timeoutId)
+  /**
+   * 服务端 POST 请求方法
+   */
+  async serverPost<TResponse = any, TData extends Record<string, any> | null = null>(
+      url: string,
+      data?: TData,
+      config?: RequestConfig,
+  ): Promise<ApiResponse<TResponse>> {
+    return this.request<TResponse>("POST", url, { data, config })
+  }
 
-      if (!response.ok) {
-        const errorData = await response.json().catch(() => ({}))
-        throw new Error((errorData as any)?.msg || `HTTP ${response.status}: ${response.statusText}`)
-      }
+  /**
+   * 客户端 POST 请求方法(新增)
+   * 适配浏览器环境,自动移除服务端缓存配置,优化错误提示
+   */
+  async clientPost<TResponse = any, TData extends Record<string, any> | null = null>(
+      url: string,
+      data?: TData,
+      config?: Omit<RequestConfig, "cache" | "next">, // 客户端禁用服务端缓存配置
+  ): Promise<ApiResponse<TResponse>> {
+    return this.request<TResponse>("POST", url, {
+      data,
+      config,
+      isClientRequest: true // 标记为客户端请求
+    })
+  }
 
-      const responseData: ApiResponse<TResponse> = await response.json()
-      return responseData
-    } catch (error: any) {
-      throw this.handleError(error)
-    }
+  /**
+   * 服务端 PUT 请求方法
+   */
+  async serverPut<TResponse = any, TData extends Record<string, any> | null = null>(
+      url: string,
+      data?: TData,
+      config?: RequestConfig,
+  ): Promise<ApiResponse<TResponse>> {
+    return this.request<TResponse>("PUT", url, { data, config })
+  }
+
+  /**
+   * 服务端 DELETE 请求方法
+   */
+  async serverDelete<TResponse = any, TParams extends Record<string, any> | null = null>(
+      url: string,
+      params?: TParams,
+      config?: GetDeleteRequestConfig,
+  ): Promise<ApiResponse<TResponse>> {
+    return this.request<TResponse>("DELETE", url, { params, config })
   }
 
+  /**
+   * 构建带参数的 URL
+   */
   private buildUrl(
-    url: string,
-    params?: Record<string, any> | null,
-    arrayFormat: "indices" | "repeat" | "comma" = "indices",
+      url: string,
+      params?: Record<string, any> | null,
+      arrayFormat: "indices" | "repeat" | "comma" = "indices",
   ): string {
-    const fullUrl = `${this.baseURL}${url}`
-
-    if (!params) {
-      return fullUrl
-    }
+    if (!params) return `${this.baseURL}${url}`
 
     const searchParams = new URLSearchParams()
 
-    for (const key in params) {
-      const value = params[key]
+    for (const [key, value] of Object.entries(params)) {
+      if (value === undefined || value === null) continue
+
       if (Array.isArray(value)) {
         switch (arrayFormat) {
           case "repeat":
-            value.forEach((v) => searchParams.append(key, String(v)))
+            value.forEach(v => searchParams.append(key, String(v)))
             break
           case "comma":
-            searchParams.set(key, value.map((v) => String(v)).join(","))
+            searchParams.set(key, value.map(String).join(","))
             break
           case "indices":
           default:
             value.forEach((v, i) => searchParams.set(`${key}[${i}]`, String(v)))
             break
         }
-      } else if (value !== undefined && value !== null) {
+      } else {
         searchParams.set(key, String(value))
       }
     }
 
     const queryString = searchParams.toString()
-    return queryString ? `${fullUrl}?${queryString}` : fullUrl
+    return queryString ? `${this.baseURL}${url}?${queryString}` : `${this.baseURL}${url}`
   }
 
-  private handleError(error: any): HttpClientError {
-    if (error.name === "AbortError") {
-      return new Error("请求超时")
-    }
+  /**
+   * 统一错误处理(区分客户端/服务端错误)
+   */
+  private handleError(error: Error, isClientRequest = false): HttpClientError {
+    const httpError = error as HttpClientError
+    httpError.originalError = { ...error } // 保留原始错误信息
+    httpError.isClientError = isClientRequest // 标记客户端错误
 
-    if (error instanceof TypeError && error.message.includes("fetch")) {
-      return new Error("网络连接失败")
+    if (error.name === "AbortError") {
+      httpError.message = isClientRequest
+          ? "请求超时,请检查网络或稍后重试" // 客户端更友好的提示
+          : "请求超时"
+      httpError.statusCode = 408
+    } else if (error instanceof TypeError && error.message.includes("fetch")) {
+      httpError.message = isClientRequest
+          ? "网络连接失败,请检查您的网络设置" // 客户端更友好的提示
+          : "网络连接失败"
+      httpError.statusCode = 0
+    } else if (!httpError.statusCode) {
+      httpError.message = httpError.message || (isClientRequest
+          ? "请求失败,请联系客服"
+          : "未知错误,请联系管理员")
+      httpError.statusCode = 500
     }
 
-    return error instanceof Error ? error : new Error("未知错误")
+    return httpError
   }
 
+  /**
+   * 获取客户端配置信息
+   */
   getInstance() {
     return {
       baseURL: this.baseURL,
       timeout: this.defaultTimeout,
-      headers: this.defaultHeaders,
+      headers: { ...this.defaultHeaders }, // 返回拷贝,避免外部修改内部配置
     }
   }
 }
 
-// 创建服务端 HTTP 客户端实例
+// 创建服务端 HTTP 客户端实例(单例)
 const serverHttpClient = new ServerHttpClient()
 
+// 导出请求方法(绑定实例上下文)
 export const serverGet = serverHttpClient.serverGet.bind(serverHttpClient)
 export const serverPost = serverHttpClient.serverPost.bind(serverHttpClient)
+export const clientPost = serverHttpClient.clientPost.bind(serverHttpClient) // 导出新增的clientPost
 export const serverPut = serverHttpClient.serverPut.bind(serverHttpClient)
 export const serverDelete = serverHttpClient.serverDelete.bind(serverHttpClient)
 
-export type { ApiResponse, RequestConfig, HttpClientError }
-export { serverHttpClient }
+// 导出类型和实例
+export type { ApiResponse, RequestConfig, GetDeleteRequestConfig, HttpClientError }
+export { serverHttpClient }