feat(inbox): 素材持久化 CRUD — 数据库表 + API + 前端接入
- 新增 materials 表(id/projectId/description/associatedDate/status/createdAt/updatedAt) - 新增 4 个后端 API 路由(list/create/get/delete)+ 13 个测试 - 新增 use-materials hooks(TanStack Query) - 收集箱页面重构为三层架构(InboxPage + MaterialSidebar + MaterialDetailPanel) - MaterialCard: Popconfirm 删除确认 + 粗粒度时间格式 - MaterialContent: 展示状态标签 + createdAt - 更新开发文档 backend.md / frontend.md
This commit is contained in:
95
src/web/shared/hooks/use-materials.ts
Normal file
95
src/web/shared/hooks/use-materials.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import type {
|
||||
CreateMaterialRequest,
|
||||
Material,
|
||||
MaterialListResponse,
|
||||
MaterialResponse,
|
||||
MaterialStatus,
|
||||
} from "../../../shared/api";
|
||||
|
||||
import { handleResponse, handleVoidResponse } from "../utils/api";
|
||||
import { createConsoleLogger } from "../utils/logger";
|
||||
|
||||
const MATERIALS_KEY = ["materials"] as const;
|
||||
const logger = createConsoleLogger();
|
||||
|
||||
export function createMaterial(args: { body: CreateMaterialRequest; projectId: string }): Promise<Material> {
|
||||
const response = fetch(`/api/projects/${args.projectId}/materials`, {
|
||||
body: JSON.stringify(args.body),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "POST",
|
||||
});
|
||||
return response.then((r) => handleResponse(r, (data) => (data as MaterialResponse).material));
|
||||
}
|
||||
|
||||
export function deleteMaterial(args: { materialId: string; projectId: string }): Promise<void> {
|
||||
const response = fetch(`/api/projects/${args.projectId}/materials/${args.materialId}`, { method: "DELETE" });
|
||||
return response.then(handleVoidResponse);
|
||||
}
|
||||
|
||||
export async function fetchMaterial(args: { materialId: string; projectId: string }): Promise<Material> {
|
||||
const response = await fetch(`/api/projects/${args.projectId}/materials/${args.materialId}`);
|
||||
return handleResponse(response, (data) => (data as MaterialResponse).material);
|
||||
}
|
||||
|
||||
export function fetchMaterials(
|
||||
projectId: string,
|
||||
params?: { page?: number; pageSize?: number; status?: MaterialStatus },
|
||||
): Promise<MaterialListResponse> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.page) searchParams.set("page", String(params.page));
|
||||
if (params?.pageSize) searchParams.set("pageSize", String(params.pageSize));
|
||||
if (params?.status) searchParams.set("status", params.status);
|
||||
const qs = searchParams.toString();
|
||||
const url = `/api/projects/${projectId}/materials${qs ? `?${qs}` : ""}`;
|
||||
const response = fetch(url);
|
||||
return response.then((r) => {
|
||||
if (!r.ok) {
|
||||
return r.json().then((body: null | { error?: string }) => {
|
||||
throw new Error(body?.error ?? `HTTP ${r.status}`);
|
||||
});
|
||||
}
|
||||
return r.json() as Promise<MaterialListResponse>;
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateMaterial(projectId: string) {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: createMaterial,
|
||||
onSuccess: (data) => {
|
||||
logger.info("素材创建成功", { materialId: data.id, projectId });
|
||||
void queryClient.invalidateQueries({ queryKey: [...MATERIALS_KEY, "list", projectId] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteMaterial(projectId: string) {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: deleteMaterial,
|
||||
onSuccess: (_data, variables) => {
|
||||
logger.info("素材删除成功", { materialId: variables.materialId, projectId });
|
||||
void queryClient.invalidateQueries({ queryKey: [...MATERIALS_KEY, "list", projectId] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useMaterial(args: { materialId: null | string; projectId: string }) {
|
||||
return useQuery({
|
||||
enabled: !!args.materialId,
|
||||
queryFn: () => fetchMaterial({ materialId: args.materialId!, projectId: args.projectId }),
|
||||
queryKey: [...MATERIALS_KEY, "detail", args.projectId, args.materialId],
|
||||
});
|
||||
}
|
||||
|
||||
export function useMaterialList(
|
||||
projectId: string,
|
||||
params?: { page?: number; pageSize?: number; status?: MaterialStatus },
|
||||
) {
|
||||
return useQuery({
|
||||
queryFn: () => fetchMaterials(projectId, params),
|
||||
queryKey: [...MATERIALS_KEY, "list", projectId, params],
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user