本文档提供具体的代码示例,展示如何将前端 Mock 数据替换为真实后端 API。
.env 文件cp .env.example .env
编辑 .env:
VITE_API_BASE_URL=http://localhost:3000/api
在 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();
}
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等函数可以删除, 因为搜索/筛选/排序逻辑已由后端处理。
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;
}
rm src/data/mock.ts
全局搜索 @/data/mock,删除所有相关 import:
# 检查哪些文件还在引用 mock 数据
grep -r "@/data/mock" src/
src/api/products.ts 中的以下函数在对接后端后可以删除:
matchesQuery()matchesFilters()sortProducts()productSimilarity()| 功能 | 测试步骤 | 预期结果 |
|---|---|---|
| 搜索 | 首页输入关键词,点击搜索 | 跳转结果页,显示匹配产品 |
| 筛选 | 左侧勾选筛选条件 | 产品列表实时更新 |
| 排序 | 切换排序方式 | 产品列表重新排序 |
| 详情 | 点击产品卡片 | 跳转详情页,显示完整信息 |
| 图片切换 | 详情页点击左右箭头 | 图片正常切换 |
| 收藏 | 详情页点击收藏按钮 | 心形图标切换状态 |
| 导出 | 结果页选择产品 → 批量导出 | 弹出导出弹窗,进度条正常 |
| 导出记录 | 导航到"我的导出" | 显示导出记录列表 |
| 404 | 访问 /product/999 |
显示"未找到该产品" |
| 登录 | (如已实现登录页) | 登录后显示用户名 |
后端需要配置允许跨域,详见 README.md 中的"CORS 跨域"章节。
检查后端返回的 images 字段是否为可访问的完整 URL。如果图片需要鉴权,需要后端提供图片代理接口。
后端 checkExportStatus 返回的 downloadUrl 应为可直接下载的链接。前端会在导出完成后展示"下载文件"按钮。
当前前端使用 pageSize: 1000 一次获取全部数据做客户端分页。后端对接后建议改为真实分页:
ResultsPage.tsx 中的 fetchProducts 调用,传入实际 page 和 pageSize