You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
11 KiB
11 KiB
API 层设计
文档版本:v1.1
最后更新:2025-01-18
目录
1. API 客户端配置
1.1 Axios 实例配置
// 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 请求/响应类型
// src/types/api.ts
export interface ApiResponse<T> {
data: T;
message?: string;
code?: number;
}
export interface PaginatedResponse<T> {
data: T[];
total: number;
page: number;
pageSize: number;
}
export interface ApiError {
message: string;
code: string;
details?: Record<string, any>;
}
2. API 服务模块
2.1 分镜 API 服务
// 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<Storyboard[]> => {
const response = await apiClient.get<ApiResponse<Storyboard[]>>(
`/projects/${projectId}/storyboards`,
);
return response.data;
},
// 获取单个分镜
getById: async (id: string): Promise<Storyboard> => {
const response = await apiClient.get<ApiResponse<Storyboard>>(
`/storyboards/${id}`,
);
return response.data;
},
// 创建分镜
create: async (data: CreateStoryboardDto): Promise<Storyboard> => {
const response = await apiClient.post<ApiResponse<Storyboard>>(
`/projects/${data.projectId}/storyboards`,
data,
);
return response.data;
},
// 更新分镜
update: async (
id: string,
data: UpdateStoryboardDto,
): Promise<Storyboard> => {
const response = await apiClient.patch<ApiResponse<Storyboard>>(
`/storyboards/${id}`,
data,
);
return response.data;
},
// 删除分镜
delete: async (id: string): Promise<void> => {
await apiClient.delete(`/storyboards/${id}`);
},
// 调整分镜顺序
reorder: async (
projectId: string,
storyboardIds: string[],
): Promise<void> => {
await apiClient.put(`/projects/${projectId}/storyboards/reorder`, {
storyboardIds,
});
},
};
2.2 项目 API 服务
// 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<PaginatedResponse<Project>> => {
const response = await apiClient.get<
ApiResponse<PaginatedResponse<Project>>
>("/projects", { params });
return response.data;
},
// 获取单个项目
getById: async (id: string): Promise<Project> => {
const response = await apiClient.get<ApiResponse<Project>>(
`/projects/${id}`,
);
return response.data;
},
// 创建项目
create: async (data: CreateProjectDto): Promise<Project> => {
const response = await apiClient.post<ApiResponse<Project>>(
"/projects",
data,
);
return response.data;
},
// 更新项目
update: async (id: string, data: UpdateProjectDto): Promise<Project> => {
const response = await apiClient.patch<ApiResponse<Project>>(
`/projects/${id}`,
data,
);
return response.data;
},
// 删除项目
delete: async (id: string): Promise<void> => {
await apiClient.delete(`/projects/${id}`);
},
};
2.3 文件上传服务
// 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<UploadResponse> => {
const formData = new FormData();
formData.append("file", file);
const response = await apiClient.post<ApiResponse<UploadResponse>>(
"/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<UploadResponse[]> => {
const formData = new FormData();
files.forEach((file) => {
formData.append("files", file);
});
const response = await apiClient.post<ApiResponse<UploadResponse[]>>(
"/upload/batch",
formData,
{
headers: {
"Content-Type": "multipart/form-data",
},
},
);
return response.data;
},
};
3. Mock API 服务
3.1 Mock 数据
// 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 客户端
// 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<Storyboard[]> => {
await delay(300);
return mockData.storyboards.filter((s) => s.projectId === projectId);
},
getById: async (id: string): Promise<Storyboard> => {
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<Storyboard> => {
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<Storyboard> => {
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<void> => {
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<void> => {
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 统一导出
// 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 使用方式
// 在 Hook 中使用
import { api } from "@services/api";
export function useStoryboards(projectId: string) {
return useQuery({
queryKey: ["storyboards", projectId],
queryFn: () => api.storyboards.getList(projectId),
});
}
5. 错误处理
5.1 错误类型定义
// src/types/api.ts
export class ApiError extends Error {
constructor(
public message: string,
public code: string,
public status?: number,
public details?: Record<string, any>,
) {
super(message);
this.name = "ApiError";
}
}
5.2 错误处理 Hook
// 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 };
}
相关文档
最后更新:2025-01-18 | 版本:v1.1