feat: 增加项目管理功能
引入 SQLite 数据库(Drizzle ORM + bun:sqlite),实现项目 CRUD 与归档/恢复/删除 生命周期管理,新增项目管理前端页面,migration 嵌入单文件构建产物保持部署体验。 - src/server/db: schema、connection、migration 执行器、项目数据访问层 - src/server/routes/projects: 7 个 API 端点(列表/创建/详情/更新/归档/恢复/删除) - src/web: 项目管理页面(TDesign Table/Tabs/Dialog/Form),TanStack Query hooks - scripts: 构建时嵌入 migration SQL,开发期独立 generate-migrations-data 脚本 - tests: 60 个后端测试 + 27 个前端测试,覆盖 DB/migration/API/路由/页面 - docs: 更新架构、后端、发布、配置、部署、使用文档
This commit is contained in:
158
src/web/hooks/use-projects.ts
Normal file
158
src/web/hooks/use-projects.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import type {
|
||||
CreateProjectRequest,
|
||||
Project,
|
||||
ProjectListResponse,
|
||||
ProjectStatus,
|
||||
UpdateProjectRequest,
|
||||
} from "../../shared/api";
|
||||
|
||||
const PROJECTS_KEY = ["projects"] as const;
|
||||
|
||||
export function useArchiveProject() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: archiveProject,
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({ queryKey: PROJECTS_KEY });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateProject() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: createProject,
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({ queryKey: PROJECTS_KEY });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteProject() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: deleteProject,
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({ queryKey: PROJECTS_KEY });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useProject(id: string) {
|
||||
return useQuery({
|
||||
enabled: !!id,
|
||||
queryFn: () => fetchProject(id),
|
||||
queryKey: [...PROJECTS_KEY, "detail", id],
|
||||
});
|
||||
}
|
||||
|
||||
export function useProjectList(params: { keyword?: string; page?: number; pageSize?: number; status?: ProjectStatus }) {
|
||||
return useQuery({
|
||||
queryFn: () => fetchProjectList(params),
|
||||
queryKey: [...PROJECTS_KEY, "list", params],
|
||||
});
|
||||
}
|
||||
|
||||
export function useRestoreProject() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: restoreProject,
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({ queryKey: PROJECTS_KEY });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateProject() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (args: { data: UpdateProjectRequest; id: string }) => updateProject(args.id, args.data),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({ queryKey: PROJECTS_KEY });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function archiveProject(id: string): Promise<Project> {
|
||||
const response = await fetch(`/api/projects/${id}/archive`, { method: "POST" });
|
||||
if (!response.ok) {
|
||||
const body = (await response.json().catch(() => null)) as null | { error?: string };
|
||||
throw new Error(body?.error ?? `HTTP ${response.status}`);
|
||||
}
|
||||
return response.json() as Promise<Project>;
|
||||
}
|
||||
|
||||
async function createProject(data: CreateProjectRequest): Promise<Project> {
|
||||
const response = await fetch("/api/projects", {
|
||||
body: JSON.stringify(data),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "POST",
|
||||
});
|
||||
if (!response.ok) {
|
||||
const body = (await response.json().catch(() => null)) as null | { error?: string };
|
||||
throw new Error(body?.error ?? `HTTP ${response.status}`);
|
||||
}
|
||||
return response.json() as Promise<Project>;
|
||||
}
|
||||
|
||||
async function deleteProject(id: string): Promise<void> {
|
||||
const response = await fetch(`/api/projects/${id}`, { method: "DELETE" });
|
||||
if (!response.ok) {
|
||||
const body = (await response.json().catch(() => null)) as null | { error?: string };
|
||||
throw new Error(body?.error ?? `HTTP ${response.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchProject(id: string): Promise<Project> {
|
||||
const response = await fetch(`/api/projects/${id}`);
|
||||
if (!response.ok) {
|
||||
const body = (await response.json().catch(() => null)) as null | { error?: string };
|
||||
throw new Error(body?.error ?? `HTTP ${response.status}`);
|
||||
}
|
||||
return response.json() as Promise<Project>;
|
||||
}
|
||||
|
||||
async function fetchProjectList(params: {
|
||||
keyword?: string;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
status?: ProjectStatus;
|
||||
}): Promise<ProjectListResponse> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params.page) searchParams.set("page", String(params.page));
|
||||
if (params.pageSize) searchParams.set("pageSize", String(params.pageSize));
|
||||
if (params.keyword) searchParams.set("keyword", params.keyword);
|
||||
if (params.status) searchParams.set("status", params.status);
|
||||
const qs = searchParams.toString();
|
||||
const url = `/api/projects${qs ? `?${qs}` : ""}`;
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
const body = (await response.json().catch(() => null)) as null | { error?: string };
|
||||
throw new Error(body?.error ?? `HTTP ${response.status}`);
|
||||
}
|
||||
return response.json() as Promise<ProjectListResponse>;
|
||||
}
|
||||
|
||||
async function restoreProject(id: string): Promise<Project> {
|
||||
const response = await fetch(`/api/projects/${id}/restore`, { method: "POST" });
|
||||
if (!response.ok) {
|
||||
const body = (await response.json().catch(() => null)) as null | { error?: string };
|
||||
throw new Error(body?.error ?? `HTTP ${response.status}`);
|
||||
}
|
||||
return response.json() as Promise<Project>;
|
||||
}
|
||||
|
||||
async function updateProject(id: string, data: UpdateProjectRequest): Promise<Project> {
|
||||
const response = await fetch(`/api/projects/${id}`, {
|
||||
body: JSON.stringify(data),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "PATCH",
|
||||
});
|
||||
if (!response.ok) {
|
||||
const body = (await response.json().catch(() => null)) as null | { error?: string };
|
||||
throw new Error(body?.error ?? `HTTP ${response.status}`);
|
||||
}
|
||||
return response.json() as Promise<Project>;
|
||||
}
|
||||
Reference in New Issue
Block a user