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:
107
src/server/db/materials.ts
Normal file
107
src/server/db/materials.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
|
||||
import type { CreateMaterialRequest, Material, MaterialStatus } from "../../shared/api";
|
||||
import type { Logger } from "../logger";
|
||||
|
||||
import { paginateQuery, wrap } from "./connection";
|
||||
import { materials, projects } from "./schema";
|
||||
|
||||
export function createMaterial(
|
||||
raw: Database,
|
||||
projectId: string,
|
||||
request: CreateMaterialRequest,
|
||||
_logger: Logger,
|
||||
): { error: string; status: number } | { material: Material } {
|
||||
const db = wrap(raw);
|
||||
const project = db.select().from(projects).where(eq(projects.id, projectId)).get();
|
||||
if (!project) return { error: "项目不存在", status: 404 };
|
||||
if (project.status === "archived") return { error: "已归档项目不可操作", status: 409 };
|
||||
|
||||
const description = request.description.trim();
|
||||
if (!description) return { error: "描述不能为空", status: 400 };
|
||||
|
||||
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
|
||||
if (!dateRegex.test(request.associatedDate)) {
|
||||
return { error: "associatedDate 格式错误,要求 YYYY-MM-DD", status: 400 };
|
||||
}
|
||||
|
||||
const id = crypto.randomUUID();
|
||||
const now = new Date().toISOString();
|
||||
|
||||
db.insert(materials)
|
||||
.values({
|
||||
associatedDate: request.associatedDate,
|
||||
createdAt: now,
|
||||
description,
|
||||
id,
|
||||
projectId,
|
||||
status: "pending",
|
||||
updatedAt: now,
|
||||
})
|
||||
.run();
|
||||
|
||||
const row = db.select().from(materials).where(eq(materials.id, id)).get();
|
||||
return { material: toMaterial(row!) };
|
||||
}
|
||||
|
||||
export function deleteMaterial(
|
||||
raw: Database,
|
||||
projectId: string,
|
||||
materialId: string,
|
||||
_logger: Logger,
|
||||
): { error: string; status: number } | { success: true } {
|
||||
const db = wrap(raw);
|
||||
const row = db.select().from(materials).where(eq(materials.id, materialId)).get();
|
||||
if (!row) return { error: "素材不存在", status: 404 };
|
||||
if (row.projectId !== projectId) return { error: "素材不属于该项目", status: 403 };
|
||||
|
||||
db.delete(materials).where(eq(materials.id, materialId)).run();
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
export function getMaterial(
|
||||
raw: Database,
|
||||
projectId: string,
|
||||
materialId: string,
|
||||
): { error: string; status: number } | { material: Material } {
|
||||
const db = wrap(raw);
|
||||
const row = db.select().from(materials).where(eq(materials.id, materialId)).get();
|
||||
if (!row) return { error: "素材不存在", status: 404 };
|
||||
if (row.projectId !== projectId) return { error: "素材不属于该项目", status: 403 };
|
||||
|
||||
return { material: toMaterial(row) };
|
||||
}
|
||||
|
||||
export function listMaterials(
|
||||
raw: Database,
|
||||
projectId: string,
|
||||
options: { page: number; pageSize: number; status?: MaterialStatus },
|
||||
): { items: Material[]; page: number; pageSize: number; total: number } {
|
||||
const conditions = [eq(materials.projectId, projectId)];
|
||||
|
||||
if (options.status) {
|
||||
conditions.push(eq(materials.status, options.status));
|
||||
}
|
||||
|
||||
return paginateQuery(raw, materials, {
|
||||
conditions,
|
||||
mapRow: toMaterial,
|
||||
orderBy: () => desc(materials.createdAt),
|
||||
page: options.page,
|
||||
pageSize: options.pageSize,
|
||||
});
|
||||
}
|
||||
|
||||
function toMaterial(row: typeof materials.$inferSelect): Material {
|
||||
return {
|
||||
associatedDate: row.associatedDate,
|
||||
createdAt: row.createdAt,
|
||||
description: row.description,
|
||||
id: row.id,
|
||||
projectId: row.projectId,
|
||||
status: row.status,
|
||||
updatedAt: row.updatedAt,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user