INTEGRATION_GUIDE.md 8.1 KB

前后端对接代码示例

本文档提供具体的代码示例,展示如何将前端 Mock 数据替换为真实后端 API。


前置准备

1. 创建 .env 文件

cp .env.example .env

编辑 .env:

VITE_API_BASE_URL=http://localhost:3000/api

2. 封装 HTTP 请求工具(可选但推荐)

在 src/api/ 下新建 request.ts:

const API_BASE = import.meta.env.VITE_API_BASE_URL;

interface RequestOptions {
  method?: "GET" | "POST" | "PUT" | "DELETE";
  body?: unknown;
  headers?: Record<string, string>;
}

export async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {
  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):

import { PRODUCTS } from "@/data/mock";

export async function fetchProducts(params: ProductListParams = {}): Promise<PaginatedResponse<Product>> {
  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 请求):

import { request } from "./request";
import type { Product } from "@/types";
import type { PaginatedResponse } from "./types";

export interface ProductListParams {
  query?: string;
  filters?: Record<string, string[]>;
  sort?: string;
  page?: number;
  pageSize?: number;
}

// 搜索产品列表
export async function fetchProducts(
  params: ProductListParams = {}
): Promise<PaginatedResponse<Product>> {
  return request<PaginatedResponse<Product>>("/products/search", {
    method: "POST",
    body: params,
  });
}

// 获取产品详情
export async function fetchProductById(id: string): Promise<Product | null> {
  try {
    return await request<Product>(`/products/${id}`);
  } catch {
    return null;
  }
}

// 获取相似产品
export async function fetchSimilarProducts(id: string, limit = 5): Promise<Product[]> {
  return request<Product[]>(`/products/${id}/similar?limit=${limit}`);
}

注意:替换后 matchesQuery、matchesFilters、sortProducts 等函数可以删除, 因为搜索/筛选/排序逻辑已由后端处理。


二、导出 API 对接

修改文件:src/api/exports.ts

替换为:

import { request } from "./request";
import type { ExportRecord } from "@/types";

// 获取导出记录
export async function fetchExportRecords(): Promise<ExportRecord[]> {
  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<void> {
  await request(`/exports/${id}`, { method: "DELETE" });
}

三、用户认证对接

修改文件:src/contexts/AuthContext.tsx

当前代码(Mock):

const MOCK_USER: User = { id: "u1", name: "设计师小王", role: "designer" };

export function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<User | null>(MOCK_USER);
  // ...
}

替换为:

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<void>;
  logout: () => void;
}

const AuthContext = createContext<AuthContextType | null>(null);

export function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<User | null>(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 (
    <AuthContext.Provider
      value={{ user, isAuthenticated: !!user, login, logout }}
    >
      {children}
    </AuthContext.Provider>
  );
}

export function useAuth() {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error("useAuth must be used within AuthProvider");
  return ctx;
}

四、对接后清理

1. 删除 Mock 数据

rm src/data/mock.ts

2. 清理残留 import

全局搜索 @/data/mock,删除所有相关 import:

# 检查哪些文件还在引用 mock 数据
grep -r "@/data/mock" src/

3. 删除不再需要的函数

src/api/products.ts 中的以下函数在对接后端后可以删除:

  • matchesQuery()
  • matchesFilters()
  • sortProducts()
  • productSimilarity()

五、联调测试清单

功能 测试步骤 预期结果
搜索 首页输入关键词,点击搜索 跳转结果页,显示匹配产品
筛选 左侧勾选筛选条件 产品列表实时更新
排序 切换排序方式 产品列表重新排序
详情 点击产品卡片 跳转详情页,显示完整信息
图片切换 详情页点击左右箭头 图片正常切换
收藏 详情页点击收藏按钮 心形图标切换状态
导出 结果页选择产品 → 批量导出 弹出导出弹窗,进度条正常
导出记录 导航到"我的导出" 显示导出记录列表
404 访问 /product/999 显示"未找到该产品"
登录 (如已实现登录页) 登录后显示用户名

六、常见问题

Q: 前端请求报 CORS 错误?

后端需要配置允许跨域,详见 README.md 中的"CORS 跨域"章节。

Q: 图片加载失败?

检查后端返回的 images 字段是否为可访问的完整 URL。如果图片需要鉴权,需要后端提供图片代理接口。

Q: 导出文件如何下载?

后端 checkExportStatus 返回的 downloadUrl 应为可直接下载的链接。前端会在导出完成后展示"下载文件"按钮。

Q: 分页怎么对接?

当前前端使用 pageSize: 1000 一次获取全部数据做客户端分页。后端对接后建议改为真实分页:

  1. 修改 ResultsPage.tsx 中的 fetchProducts 调用,传入实际 page 和 pageSize
  2. 使用 IntersectionObserver 触发下一页加载