From fc6dc52f72d6619b08554297c5c02c1793ad63d2 Mon Sep 17 00:00:00 2001 From: repair-agent Date: Fri, 13 Mar 2026 10:44:29 +0800 Subject: [PATCH] add del project --- app/main.py | 13 ++++ web/src/api.ts | 3 + web/src/pages/ProjectList.tsx | 135 ++++++++++++++++++++++++++++++++-- 3 files changed, 143 insertions(+), 8 deletions(-) diff --git a/app/main.py b/app/main.py index fa36442..9bfbfb3 100644 --- a/app/main.py +++ b/app/main.py @@ -604,6 +604,19 @@ async def update_project(project_id: str, data: ProjectUpdate, session: AsyncSes await session.refresh(project) return project +@app.delete("/api/v1/projects/{project_id}", tags=["Projects"]) +async def delete_project(project_id: str, session: AsyncSession = Depends(get_session)): + """Delete a project by project_id""" + statement = select(Project).where(Project.project_id == project_id) + results = await session.exec(statement) + project = results.first() + if not project: + raise HTTPException(status_code=404, detail="Project not found") + + await session.delete(project) + await session.commit() + return {"message": "Project deleted"} + @app.get("/", tags=["Health"]) async def health_check(): return {"status": "ok"} diff --git a/web/src/api.ts b/web/src/api.ts index 3ccfdd4..9d91b50 100644 --- a/web/src/api.ts +++ b/web/src/api.ts @@ -160,6 +160,9 @@ export const getProjectDetail = (projectId: string) => export const updateProject = (projectId: string, data: Partial) => api.put(`/api/v1/projects/${projectId}`, data); +export const deleteProject = (projectId: string) => + api.delete(`/api/v1/projects/${projectId}`); + export const updateTaskStatus = (taskId: number, status: string, message?: string) => api.put(`/api/v1/tasks/${taskId}/status`, { status, message }); diff --git a/web/src/pages/ProjectList.tsx b/web/src/pages/ProjectList.tsx index b6ba6e1..517f757 100644 --- a/web/src/pages/ProjectList.tsx +++ b/web/src/pages/ProjectList.tsx @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react'; -import { getProjects, updateProject, type Project } from '../api'; -import { Save, X, Pencil } from 'lucide-react'; +import { getProjects, updateProject, deleteProject, type Project } from '../api'; +import { Save, X, Pencil, Trash2 } from 'lucide-react'; export default function ProjectList() { const [projects, setProjects] = useState([]); @@ -9,6 +9,11 @@ export default function ProjectList() { const [editForm, setEditForm] = useState({ name: '', repo_url: '', local_path: '', description: '' }); const [saving, setSaving] = useState(false); + // Delete confirmation state + const [deleteTarget, setDeleteTarget] = useState(null); + const [deleteConfirmName, setDeleteConfirmName] = useState(''); + const [deleting, setDeleting] = useState(false); + const fetchProjects = async () => { setLoading(true); try { @@ -50,6 +55,32 @@ export default function ProjectList() { } }; + const openDeleteConfirm = (p: Project) => { + setDeleteTarget(p); + setDeleteConfirmName(''); + }; + + const closeDeleteConfirm = () => { + setDeleteTarget(null); + setDeleteConfirmName(''); + }; + + const confirmDelete = async () => { + if (!deleteTarget) return; + setDeleting(true); + try { + await deleteProject(deleteTarget.project_id); + closeDeleteConfirm(); + await fetchProjects(); + } catch (err) { + console.error('删除失败:', err); + } finally { + setDeleting(false); + } + }; + + const isDeleteConfirmed = deleteTarget && deleteConfirmName === deleteTarget.project_id; + const ConfigBadge = ({ value }: { value: string | null }) => ( value ? ( {value} @@ -164,9 +195,18 @@ export default function ProjectList() { {new Date(p.updated_at).toLocaleString()} - +
+ + +
)} @@ -233,9 +273,18 @@ export default function ProjectList() { <>
{p.project_id} - +
+ + +
{p.name &&
{p.name}
}
@@ -253,6 +302,76 @@ export default function ProjectList() {
)} + + {/* Delete confirmation modal */} + {deleteTarget && ( +
+
e.stopPropagation()} + > +

删除项目

+

+ 此操作不可撤销。请输入项目 ID {deleteTarget.project_id} 以确认删除。 +

+ setDeleteConfirmName(e.target.value)} + placeholder={deleteTarget.project_id} + className="edit-input" + style={{ width: '100%', marginBottom: '16px', boxSizing: 'border-box' }} + autoFocus + /> +
+ + +
+
+
+ )} ); }