# 前后端对接代码示例 本文档提供具体的代码示例,展示如何将前端 Mock 数据替换为真实后端 API。 --- ## 前置准备 ### 1. 创建 `.env` 文件 ```bash cp .env.example .env ``` 编辑 `.env`: ```env VITE_API_BASE_URL=http://localhost:3000/api ``` ### 2. 封装 HTTP 请求工具(可选但推荐) 在 `src/api/` 下新建 `request.ts`: ```typescript const API_BASE = import.meta.env.VITE_API_BASE_URL; interface RequestOptions { method?: "GET" | "POST" | "PUT" | "DELETE"; body?: unknown; headers?: Record; } export async function request(path: string, options: RequestOptions = {}): Promise { const { method = "GET", body, headers = {} } = options; const token = localStorage.getItem("token"); if (token) { headers["Authorization"] = `Bearer ${token}`; } if (body) { headers["Content-Type"] = "application/json"; } const res = await fetch(`${API_BASE}${path}`, { method, headers, body: body ? JSON.stringify(body) : undefined, }); if (!res.ok) { const error = await res.text(); throw new Error(`API Error ${res.status}: ${error}`); } return res.json(); } ``` --- ## 一、产品 API 对接 ### 修改文件:`src/api/products.ts` **当前代码(Mock):** ```typescript import { PRODUCTS } from "@/data/mock"; export async function fetchProducts(params: ProductListParams = {}): Promise> { const { query = "", filters = {}, sort = "相关度", page = 1, pageSize = 8 } = params; let result = PRODUCTS.filter(p => matchesQuery(p, query) && matchesFilters(p, filters)); result = sortProducts(result, sort); const total = result.length; const start = (page - 1) * pageSize; const items = result.slice(start, start + pageSize); return { items, total, page, pageSize }; } ``` **替换为(HTTP 请求):** ```typescript import { request } from "./request"; import type { Product } from "@/types"; import type { PaginatedResponse } from "./types"; export interface ProductListParams { query?: string; filters?: Record; sort?: string; page?: number; pageSize?: number; } // 搜索产品列表 export async function fetchProducts( params: ProductListParams = {} ): Promise> { return request>("/products/search", { method: "POST", body: params, }); } // 获取产品详情 export async function fetchProductById(id: string): Promise { try { return await request(`/products/${id}`); } catch { return null; } } // 获取相似产品 export async function fetchSimilarProducts(id: string, limit = 5): Promise { return request(`/products/${id}/similar?limit=${limit}`); } ``` > 注意:替换后 `matchesQuery`、`matchesFilters`、`sortProducts` 等函数可以删除, > 因为搜索/筛选/排序逻辑已由后端处理。 --- ## 二、导出 API 对接 ### 修改文件:`src/api/exports.ts` **替换为:** ```typescript import { request } from "./request"; import type { ExportRecord } from "@/types"; // 获取导出记录 export async function fetchExportRecords(): Promise { const res = await request<{ items: ExportRecord[] }>("/exports"); return res.items; } // 创建导出任务 export async function createExport(params: { productIds: string[]; imgFormat: string; metaFormat: string; filename: string; }): Promise<{ taskId: string }> { return request<{ taskId: string }>("/exports", { method: "POST", body: params, }); } // 查询导出进度 export async function checkExportStatus(taskId: string): Promise<{ progress: number; failed: boolean; downloadUrl?: string; }> { return request(`/exports/${taskId}/status`); } // 删除导出记录 export async function deleteExportRecord(id: string): Promise { await request(`/exports/${id}`, { method: "DELETE" }); } ``` --- ## 三、用户认证对接 ### 修改文件:`src/contexts/AuthContext.tsx` **当前代码(Mock):** ```typescript const MOCK_USER: User = { id: "u1", name: "设计师小王", role: "designer" }; export function AuthProvider({ children }: { children: ReactNode }) { const [user, setUser] = useState(MOCK_USER); // ... } ``` **替换为:** ```typescript import { createContext, useContext, useState, useEffect, type ReactNode } from "react"; import type { User } from "@/types"; const API_BASE = import.meta.env.VITE_API_BASE_URL; interface AuthContextType { user: User | null; isAuthenticated: boolean; login: (username: string, password: string) => Promise; logout: () => void; } const AuthContext = createContext(null); export function AuthProvider({ children }: { children: ReactNode }) { const [user, setUser] = useState(null); // 启动时检查 token useEffect(() => { const token = localStorage.getItem("token"); if (!token) return; fetch(`${API_BASE}/auth/me`, { headers: { Authorization: `Bearer ${token}` }, }) .then(res => { if (!res.ok) throw new Error("Unauthorized"); return res.json(); }) .then(data => setUser(data)) .catch(() => { localStorage.removeItem("token"); setUser(null); }); }, []); const login = async (username: string, password: string) => { const res = await fetch(`${API_BASE}/auth/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username, password }), }); if (!res.ok) throw new Error("登录失败"); const data = await res.json(); localStorage.setItem("token", data.token); setUser(data.user); }; const logout = () => { localStorage.removeItem("token"); setUser(null); }; return ( {children} ); } export function useAuth() { const ctx = useContext(AuthContext); if (!ctx) throw new Error("useAuth must be used within AuthProvider"); return ctx; } ``` --- ## 四、对接后清理 ### 1. 删除 Mock 数据 ```bash rm src/data/mock.ts ``` ### 2. 清理残留 import 全局搜索 `@/data/mock`,删除所有相关 import: ```bash # 检查哪些文件还在引用 mock 数据 grep -r "@/data/mock" src/ ``` ### 3. 删除不再需要的函数 `src/api/products.ts` 中的以下函数在对接后端后可以删除: - `matchesQuery()` - `matchesFilters()` - `sortProducts()` - `productSimilarity()` --- ## 五、联调测试清单 | 功能 | 测试步骤 | 预期结果 | |------|----------|----------| | 搜索 | 首页输入关键词,点击搜索 | 跳转结果页,显示匹配产品 | | 筛选 | 左侧勾选筛选条件 | 产品列表实时更新 | | 排序 | 切换排序方式 | 产品列表重新排序 | | 详情 | 点击产品卡片 | 跳转详情页,显示完整信息 | | 图片切换 | 详情页点击左右箭头 | 图片正常切换 | | 收藏 | 详情页点击收藏按钮 | 心形图标切换状态 | | 导出 | 结果页选择产品 → 批量导出 | 弹出导出弹窗,进度条正常 | | 导出记录 | 导航到"我的导出" | 显示导出记录列表 | | 404 | 访问 `/product/999` | 显示"未找到该产品" | | 登录 | (如已实现登录页) | 登录后显示用户名 | --- ## 六、常见问题 ### Q: 前端请求报 CORS 错误? 后端需要配置允许跨域,详见 [README.md](./README.md) 中的"CORS 跨域"章节。 ### Q: 图片加载失败? 检查后端返回的 `images` 字段是否为可访问的完整 URL。如果图片需要鉴权,需要后端提供图片代理接口。 ### Q: 导出文件如何下载? 后端 `checkExportStatus` 返回的 `downloadUrl` 应为可直接下载的链接。前端会在导出完成后展示"下载文件"按钮。 ### Q: 分页怎么对接? 当前前端使用 `pageSize: 1000` 一次获取全部数据做客户端分页。后端对接后建议改为真实分页: 1. 修改 `ResultsPage.tsx` 中的 `fetchProducts` 调用,传入实际 `page` 和 `pageSize` 2. 使用 IntersectionObserver 触发下一页加载