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:
2026-06-03 14:53:23 +08:00
parent 5b09a16bc3
commit 21b557c255
29 changed files with 1629 additions and 116 deletions

View 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],
});
}