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

@@ -1,39 +1,43 @@
import { useState } from "react";
import type { Material } from "./types";
import type { CreateMaterialRequest, Material } from "./types";
import { useCurrentProject } from "../../shared/hooks/use-current-project";
import { useCreateMaterial } from "../../shared/hooks/use-materials";
import { AddMaterialModal } from "./components/AddMaterialModal";
import { MaterialContent } from "./components/MaterialContent";
import { MaterialList } from "./components/MaterialList";
import { MaterialDetailPanel } from "./components/MaterialDetailPanel";
import { MaterialSidebar } from "./components/MaterialSidebar";
export function InboxPage() {
const [materials, setMaterials] = useState<Material[]>([]);
const project = useCurrentProject();
const [modalOpen, setModalOpen] = useState(false);
const [selectedId, setSelectedId] = useState<null | string>(null);
const selectedMaterial = materials.find((m) => m.id === selectedId) ?? null;
const createMutation = useCreateMaterial(project.id);
const handleAdd = (material: Material) => {
setMaterials((prev) => [...prev, material].sort((a, b) => b.associatedDate.localeCompare(a.associatedDate)));
const handleCreate = async (body: CreateMaterialRequest): Promise<Material> => {
const material = await createMutation.mutateAsync({ body, projectId: project.id });
setSelectedId(material.id);
return material;
};
const handleDelete = (id: string) => {
setMaterials((prev) => prev.filter((m) => m.id !== id));
if (selectedId === id) setSelectedId(null);
const handleDelete = (_id: string) => {
if (selectedId === _id) {
setSelectedId(null);
}
};
return (
<div className="app-inbox-page">
<MaterialList
materials={materials}
<MaterialSidebar
onAddClick={() => setModalOpen(true)}
onDelete={handleDelete}
onSelect={setSelectedId}
projectId={project.id}
selectedId={selectedId}
/>
<MaterialContent material={selectedMaterial} />
<AddMaterialModal onAdd={handleAdd} onOpenChange={setModalOpen} open={modalOpen} />
<MaterialDetailPanel materialId={selectedId} projectId={project.id} />
<AddMaterialModal onAdd={handleCreate} onOpenChange={setModalOpen} open={modalOpen} />
</div>
);
}