# API 层设计 > **文档版本**:v1.1 > **最后更新**:2025-01-18 --- ## 目录 1. [API 客户端配置](#1-api-客户端配置) 2. [API 服务模块](#2-api-服务模块) 3. [Mock API 服务](#3-mock-api-服务) 4. [API/Mock 自动切换](#4-apimock-自动切换) --- ## 1. API 客户端配置 ### 1.1 Axios 实例配置 ```typescript // src/services/api/client.ts import axios, { AxiosInstance, AxiosError, InternalAxiosRequestConfig, } from "axios"; import { useAppStore } from "@stores/appStore"; // 创建 axios 实例 const apiClient: AxiosInstance = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 30000, headers: { "Content-Type": "application/json", }, }); // 请求拦截器 apiClient.interceptors.request.use( (config: InternalAxiosRequestConfig) => { // 添加 token const token = localStorage.getItem("token"); if (token && config.headers) { config.headers.Authorization = `Bearer ${token}`; } return config; }, (error) => Promise.reject(error), ); // 响应拦截器 apiClient.interceptors.response.use( (response) => response.data, (error: AxiosError) => { // 统一错误处理 if (error.response?.status === 401) { // token 过期,清除用户状态 useAppStore.getState().setUser(null); localStorage.removeItem("token"); window.location.href = "/login"; } return Promise.reject(error); }, ); export default apiClient; ``` ### 1.2 请求/响应类型 ```typescript // src/types/api.ts export interface ApiResponse { data: T; message?: string; code?: number; } export interface PaginatedResponse { data: T[]; total: number; page: number; pageSize: number; } export interface ApiError { message: string; code: string; details?: Record; } ``` --- ## 2. API 服务模块 ### 2.1 分镜 API 服务 ```typescript // src/services/api/storyboards.ts import apiClient from "./client"; import type { Storyboard, CreateStoryboardDto, UpdateStoryboardDto, ApiResponse, } from "@types"; export const storyboardApi = { // 获取分镜列表 getList: async (projectId: string): Promise => { const response = await apiClient.get>( `/projects/${projectId}/storyboards`, ); return response.data; }, // 获取单个分镜 getById: async (id: string): Promise => { const response = await apiClient.get>( `/storyboards/${id}`, ); return response.data; }, // 创建分镜 create: async (data: CreateStoryboardDto): Promise => { const response = await apiClient.post>( `/projects/${data.projectId}/storyboards`, data, ); return response.data; }, // 更新分镜 update: async ( id: string, data: UpdateStoryboardDto, ): Promise => { const response = await apiClient.patch>( `/storyboards/${id}`, data, ); return response.data; }, // 删除分镜 delete: async (id: string): Promise => { await apiClient.delete(`/storyboards/${id}`); }, // 调整分镜顺序 reorder: async ( projectId: string, storyboardIds: string[], ): Promise => { await apiClient.put(`/projects/${projectId}/storyboards/reorder`, { storyboardIds, }); }, }; ``` ### 2.2 项目 API 服务 ```typescript // src/services/api/projects.ts import apiClient from "./client"; import type { Project, CreateProjectDto, UpdateProjectDto, ApiResponse, PaginatedResponse, } from "@types"; export const projectApi = { // 获取项目列表 getList: async (params?: { page?: number; pageSize?: number; search?: string; }): Promise> => { const response = await apiClient.get< ApiResponse> >("/projects", { params }); return response.data; }, // 获取单个项目 getById: async (id: string): Promise => { const response = await apiClient.get>( `/projects/${id}`, ); return response.data; }, // 创建项目 create: async (data: CreateProjectDto): Promise => { const response = await apiClient.post>( "/projects", data, ); return response.data; }, // 更新项目 update: async (id: string, data: UpdateProjectDto): Promise => { const response = await apiClient.patch>( `/projects/${id}`, data, ); return response.data; }, // 删除项目 delete: async (id: string): Promise => { await apiClient.delete(`/projects/${id}`); }, }; ``` ### 2.3 文件上传服务 ```typescript // src/services/api/upload.ts import apiClient from "./client"; import type { ApiResponse } from "@types"; export interface UploadResponse { url: string; filename: string; size: number; } export const uploadApi = { // 上传单个文件 uploadFile: async ( file: File, onProgress?: (progress: number) => void, ): Promise => { const formData = new FormData(); formData.append("file", file); const response = await apiClient.post>( "/upload", formData, { headers: { "Content-Type": "multipart/form-data", }, onUploadProgress: (progressEvent) => { if (onProgress && progressEvent.total) { const progress = Math.round( (progressEvent.loaded * 100) / progressEvent.total, ); onProgress(progress); } }, }, ); return response.data; }, // 批量上传文件 uploadFiles: async (files: File[]): Promise => { const formData = new FormData(); files.forEach((file) => { formData.append("files", file); }); const response = await apiClient.post>( "/upload/batch", formData, { headers: { "Content-Type": "multipart/form-data", }, }, ); return response.data; }, }; ``` --- ## 3. Mock API 服务 ### 3.1 Mock 数据 ```typescript // src/services/mock/mockData.ts import type { Storyboard, Project } from "@types"; export const mockData = { projects: [ { id: "project-1", name: "示例项目", description: "这是一个示例项目", createdAt: "2025-01-01T00:00:00Z", updatedAt: "2025-01-01T00:00:00Z", }, ] as Project[], storyboards: [ { id: "storyboard-1", projectId: "project-1", title: "开场镜头", description: "主角登场", order: 0, duration: 5, thumbnailUrl: "/images/storyboard-1.jpg", createdAt: "2025-01-01T00:00:00Z", updatedAt: "2025-01-01T00:00:00Z", }, ] as Storyboard[], }; ``` ### 3.2 Mock 客户端 ```typescript // src/services/mock/mockClient.ts import { mockData } from "./mockData"; import type { Storyboard, CreateStoryboardDto, UpdateStoryboardDto, } from "@types"; // 模拟延迟 const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); export const mockStoryboardApi = { getList: async (projectId: string): Promise => { await delay(300); return mockData.storyboards.filter((s) => s.projectId === projectId); }, getById: async (id: string): Promise => { await delay(200); const storyboard = mockData.storyboards.find((s) => s.id === id); if (!storyboard) throw new Error("Storyboard not found"); return storyboard; }, create: async (data: CreateStoryboardDto): Promise => { await delay(500); const newStoryboard: Storyboard = { id: `storyboard-${Date.now()}`, ...data, order: mockData.storyboards.filter((s) => s.projectId === data.projectId) .length, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; mockData.storyboards.push(newStoryboard); return newStoryboard; }, update: async ( id: string, data: UpdateStoryboardDto, ): Promise => { await delay(300); const index = mockData.storyboards.findIndex((s) => s.id === id); if (index === -1) throw new Error("Storyboard not found"); mockData.storyboards[index] = { ...mockData.storyboards[index], ...data, updatedAt: new Date().toISOString(), }; return mockData.storyboards[index]; }, delete: async (id: string): Promise => { await delay(300); const index = mockData.storyboards.findIndex((s) => s.id === id); if (index !== -1) { mockData.storyboards.splice(index, 1); } }, reorder: async ( projectId: string, storyboardIds: string[], ): Promise => { await delay(300); storyboardIds.forEach((id, index) => { const storyboard = mockData.storyboards.find((s) => s.id === id); if (storyboard) { storyboard.order = index; } }); }, }; ``` --- ## 4. API/Mock 自动切换 ### 4.1 统一导出 ```typescript // src/services/api/index.ts import { storyboardApi } from "./storyboards"; import { projectApi } from "./projects"; import { mockStoryboardApi } from "../mock/mockClient"; import { mockProjectApi } from "../mock/mockClient"; const useMock = import.meta.env.VITE_USE_MOCK === "true"; export const api = { storyboards: useMock ? mockStoryboardApi : storyboardApi, projects: useMock ? mockProjectApi : projectApi, // ... 其他模块 }; ``` ### 4.2 使用方式 ```typescript // 在 Hook 中使用 import { api } from "@services/api"; export function useStoryboards(projectId: string) { return useQuery({ queryKey: ["storyboards", projectId], queryFn: () => api.storyboards.getList(projectId), }); } ``` --- ## 5. 错误处理 ### 5.1 错误类型定义 ```typescript // src/types/api.ts export class ApiError extends Error { constructor( public message: string, public code: string, public status?: number, public details?: Record, ) { super(message); this.name = "ApiError"; } } ``` ### 5.2 错误处理 Hook ```typescript // src/hooks/useApiError.ts import { useCallback } from "react"; import { toast } from "@/hooks/use-toast"; import type { ApiError } from "@types"; export function useApiError() { const handleError = useCallback((error: unknown) => { if (error instanceof ApiError) { toast({ title: "操作失败", description: error.message, variant: "destructive", }); } else { toast({ title: "未知错误", description: "请稍后重试", variant: "destructive", }); } }, []); return { handleError }; } ``` --- ## 相关文档 - [状态管理](./04-state-management.md) - [构建与开发环境](./03-build-environment.md) - [类型定义](./02-project-structure.md#types) --- **最后更新**:2025-01-18 | **版本**:v1.1