فهرست منبع

feat(components/about): 添加招聘页面组件

- 新增 Recruitment 组件,用于展示招聘信息
- 添加 JobCard 组件,用于展示单个职位信息
- 实现职位列表渲染和更多岗位按钮
- 新增请求工具类 ServerHttpClient,用于处理 API 请求
- 添加 serverGet、serverPost、serverPut、serverDelete 方法,用于不同类型的 HTTP 请求
nahida 8 ماه پیش
والد
کامیت
33fccf059f
2فایلهای تغییر یافته به همراه299 افزوده شده و 0 حذف شده
  1. 103 0
      src/components/about/Recruitment.tsx
  2. 196 0
      src/utils/request.ts

+ 103 - 0
src/components/about/Recruitment.tsx

@@ -0,0 +1,103 @@
+import {ChevronRight} from "lucide-react";
+import Link from "next/link";
+
+interface JobCardProps {
+  id: string
+  title: string
+  location: string
+  positions: number
+  requirements: string
+}
+function Recruitment({list}:{list:RecruitmentInfo[]}) {
+  function JobCard({ id, title, location, positions, requirements }: JobCardProps) {
+    return (
+      <div className="w-80 bg-[url('/assets/about/21.png')] bg-[length:103%_100%] bg-no-repeat rounded-2xl border border-blue-200 overflow-hidden shadow-sm hover:shadow-md transition-shadow">
+        {/* Header */}
+        <div className="bg-blue-500 text-white px-6 py-4 rounded-t-2xl">
+          <h3 className="text-lg font-medium">{title}</h3>
+        </div>
+
+        {/* Content */}
+        <div className="p-6 space-y-4">
+          {/* Work Location */}
+          <div className="flex items-start gap-3">
+            <div className="w-2 h-2 bg-blue-400 rounded-full mt-2 flex-shrink-0"></div>
+            <div>
+              <span className="text-gray-700 font-medium">工作地址:</span>
+              <span className="text-gray-600">{location}</span>
+            </div>
+          </div>
+
+          {/* Number of Positions */}
+          <div className="flex items-start gap-3">
+            <div className="w-2 h-2 bg-blue-400 rounded-full mt-2 flex-shrink-0"></div>
+            <div>
+              <span className="text-gray-700 font-medium">招聘人数:</span>
+              <span className="text-gray-600">{positions}</span>
+            </div>
+          </div>
+
+          {/* Job Requirements */}
+          <div className="flex items-start gap-3">
+            <div className="w-2 h-2 bg-blue-400 rounded-full mt-2 flex-shrink-0"></div>
+            <div>
+              <span className="text-gray-700 font-medium">岗位要求:</span>
+              <p className="text-gray-600 mt-1 leading-relaxed line-clamp-1">{requirements}</p>
+            </div>
+          </div>
+
+          {/* Learn More Button */}
+          <div className="pt-4">
+            <Link href={`/about/recruitList/${id}`}>
+              <button
+                className="flex items-center gap-2 text-orange-500 hover:text-orange-600 transition-colors cursor-pointer">
+                <span className="font-medium">了解更多</span>
+                <div className="w-6 h-6 bg-orange-500 rounded-full flex items-center justify-center">
+                  <ChevronRight className="w-3 h-3 text-white"/>
+                </div>
+              </button>
+            </Link>
+          </div>
+        </div>
+      </div>
+    )
+  }
+
+  return (
+    <>
+      <div className="max-w-4/5 mx-auto py-8">
+        <div className="px-4">
+          <div className="flex justify-around gap-12">
+            {list && list.length > 0 ? (
+              list.map((job) => (
+                <JobCard
+                  key={job.id}
+                  title={job.jobOpenings}
+                  location={job.workLocation}
+                  positions={job.numberRecruits}
+                  requirements={job.jobRequirements}
+                  id={job.id}
+                />
+              ))
+            ) : (
+              <p className="text-gray-500">暂无招聘信息</p>
+            )}
+          </div>
+
+          {/* More Positions Button */}
+          <div className="flex justify-center mt-8">
+            <Link href={'/about/recruitList'} className="font-medium">
+            <button
+              className="flex items-center gap-2 px-6 py-3 bg-white border border-blue-300 rounded-lg text-blue-600 hover:bg-blue-50 transition-colors cursor-pointer">
+              更多岗位
+              <ChevronRight className="w-4 h-4"/>
+            </button>
+            </Link>
+          </div>
+        </div>
+      </div>
+    </>
+  );
+}
+
+export default Recruitment;

+ 196 - 0
src/utils/request.ts

@@ -0,0 +1,196 @@
+import axios, {
+  type AxiosError,
+  type AxiosInstance,
+  type AxiosRequestConfig,
+  type AxiosResponse,
+  type InternalAxiosRequestConfig,
+} from "axios"
+
+// 定义通用的 API 响应接口
+interface ApiResponse<T = any> {
+  data: T
+  code: number
+  msg: string
+}
+
+// 扩展 Axios 请求配置
+interface RequestConfig extends AxiosRequestConfig {
+  timeout?: number
+  headers?: Record<string, string>
+}
+
+// 定义可能的错误类型
+type HttpClientError = AxiosError | Error
+
+class ServerHttpClient {
+  private instance: AxiosInstance
+
+  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)
+      },
+    )
+  }
+
+  // GET 请求
+  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 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("&")
+        },
+        ...config,
+      })
+      return response.data
+    } catch (error: any) {
+      throw this.handleError(error)
+    }
+  }
+
+  // POST 请求
+  async serverPost<TResponse = any, TData extends Record<string, any> | null = Record<string, any> | null>(
+    url: string,
+    data?: TData,
+    config?: RequestConfig,
+  ): Promise<ApiResponse<TResponse>> {
+    try {
+      const response = await this.instance.post<ApiResponse<TResponse>>(url, JSON.stringify(data), config)
+      return response.data
+    } catch (error: any) {
+      throw this.handleError(error)
+    }
+  }
+
+  // PUT 请求
+  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 response = await this.instance.put<ApiResponse<TResponse>>(url, JSON.stringify(data), config)
+      return response.data
+    } catch (error: any) {
+      throw this.handleError(error)
+    }
+  }
+
+  // DELETE 请求
+  async serverDelete<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 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("&")
+        },
+        ...config,
+      })
+      return response.data
+    } catch (error: any) {
+      throw this.handleError(error)
+    }
+  }
+
+  // 错误处理
+  private handleError(error: any): HttpClientError {
+    if (axios.isAxiosError(error)) {
+      const message = (error.response?.data as any)?.msg || error.message || "请求失败"
+      return new Error(message)
+    }
+    return error instanceof Error ? error : new Error("未知错误")
+  }
+
+  getInstance(): AxiosInstance {
+    return this.instance
+  }
+}
+
+// 创建服务端 HTTP 客户端实例
+const serverHttpClient = new ServerHttpClient()
+
+export const serverGet = serverHttpClient.serverGet.bind(serverHttpClient)
+export const serverPost = serverHttpClient.serverPost.bind(serverHttpClient)
+export const serverPut = serverHttpClient.serverPut.bind(serverHttpClient)
+export const serverDelete = serverHttpClient.serverDelete.bind(serverHttpClient)
+
+export type { ApiResponse, RequestConfig, HttpClientError }
+export { serverHttpClient }