add del project
All checks were successful
Build and Deploy Log Center / build-and-deploy (push) Successful in 1m42s

This commit is contained in:
repair-agent 2026-03-13 10:44:29 +08:00
parent 01f87e38aa
commit fc6dc52f72
3 changed files with 143 additions and 8 deletions

View File

@ -604,6 +604,19 @@ async def update_project(project_id: str, data: ProjectUpdate, session: AsyncSes
await session.refresh(project) await session.refresh(project)
return 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"]) @app.get("/", tags=["Health"])
async def health_check(): async def health_check():
return {"status": "ok"} return {"status": "ok"}

View File

@ -160,6 +160,9 @@ export const getProjectDetail = (projectId: string) =>
export const updateProject = (projectId: string, data: Partial<Project>) => export const updateProject = (projectId: string, data: Partial<Project>) =>
api.put<Project>(`/api/v1/projects/${projectId}`, data); api.put<Project>(`/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) => export const updateTaskStatus = (taskId: number, status: string, message?: string) =>
api.put(`/api/v1/tasks/${taskId}/status`, { status, message }); api.put(`/api/v1/tasks/${taskId}/status`, { status, message });

View File

@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { getProjects, updateProject, type Project } from '../api'; import { getProjects, updateProject, deleteProject, type Project } from '../api';
import { Save, X, Pencil } from 'lucide-react'; import { Save, X, Pencil, Trash2 } from 'lucide-react';
export default function ProjectList() { export default function ProjectList() {
const [projects, setProjects] = useState<Project[]>([]); const [projects, setProjects] = useState<Project[]>([]);
@ -9,6 +9,11 @@ export default function ProjectList() {
const [editForm, setEditForm] = useState({ name: '', repo_url: '', local_path: '', description: '' }); const [editForm, setEditForm] = useState({ name: '', repo_url: '', local_path: '', description: '' });
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
// Delete confirmation state
const [deleteTarget, setDeleteTarget] = useState<Project | null>(null);
const [deleteConfirmName, setDeleteConfirmName] = useState('');
const [deleting, setDeleting] = useState(false);
const fetchProjects = async () => { const fetchProjects = async () => {
setLoading(true); setLoading(true);
try { 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 }) => ( const ConfigBadge = ({ value }: { value: string | null }) => (
value ? ( value ? (
<span style={{ fontSize: '12px', color: 'var(--text-secondary)', wordBreak: 'break-all' }}>{value}</span> <span style={{ fontSize: '12px', color: 'var(--text-secondary)', wordBreak: 'break-all' }}>{value}</span>
@ -164,9 +195,18 @@ export default function ProjectList() {
{new Date(p.updated_at).toLocaleString()} {new Date(p.updated_at).toLocaleString()}
</td> </td>
<td> <td>
<button className="btn-link" onClick={() => startEdit(p)}> <div style={{ display: 'flex', gap: '6px' }}>
<Pencil size={14} /> <button className="btn-link" onClick={() => startEdit(p)}>
</button> <Pencil size={14} />
</button>
<button
className="btn-link"
onClick={() => openDeleteConfirm(p)}
style={{ color: 'var(--error)' }}
>
<Trash2 size={14} />
</button>
</div>
</td> </td>
</> </>
)} )}
@ -233,9 +273,18 @@ export default function ProjectList() {
<> <>
<div className="mobile-card-top"> <div className="mobile-card-top">
<strong>{p.project_id}</strong> <strong>{p.project_id}</strong>
<button className="btn-link" onClick={() => startEdit(p)}> <div style={{ display: 'flex', gap: '8px' }}>
<Pencil size={14} /> <button className="btn-link" onClick={() => startEdit(p)}>
</button> <Pencil size={14} />
</button>
<button
className="btn-link"
onClick={() => openDeleteConfirm(p)}
style={{ color: 'var(--error)' }}
>
<Trash2 size={14} />
</button>
</div>
</div> </div>
{p.name && <div style={{ fontSize: '13px' }}>{p.name}</div>} {p.name && <div style={{ fontSize: '13px' }}>{p.name}</div>}
<div className="mobile-card-meta" style={{ flexDirection: 'column', alignItems: 'flex-start', gap: '4px', marginTop: '6px' }}> <div className="mobile-card-meta" style={{ flexDirection: 'column', alignItems: 'flex-start', gap: '4px', marginTop: '6px' }}>
@ -253,6 +302,76 @@ export default function ProjectList() {
</div> </div>
</> </>
)} )}
{/* Delete confirmation modal */}
{deleteTarget && (
<div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'rgba(0,0,0,0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000,
}}
onClick={closeDeleteConfirm}
>
<div
style={{
background: 'var(--bg-secondary)',
borderRadius: 'var(--radius-md)',
padding: '24px',
maxWidth: '440px',
width: '90%',
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
border: '1px solid var(--border)',
}}
onClick={(e) => e.stopPropagation()}
>
<h3 style={{ margin: '0 0 8px 0', fontSize: '16px' }}></h3>
<p style={{ margin: '0 0 16px 0', fontSize: '13px', color: 'var(--text-secondary)' }}>
ID <strong style={{ color: 'var(--error)' }}>{deleteTarget.project_id}</strong>
</p>
<input
type="text"
value={deleteConfirmName}
onChange={(e) => setDeleteConfirmName(e.target.value)}
placeholder={deleteTarget.project_id}
className="edit-input"
style={{ width: '100%', marginBottom: '16px', boxSizing: 'border-box' }}
autoFocus
/>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '8px' }}>
<button
className="btn-link"
onClick={closeDeleteConfirm}
style={{ color: 'var(--text-tertiary)', padding: '6px 12px' }}
>
</button>
<button
onClick={confirmDelete}
disabled={!isDeleteConfirmed || deleting}
style={{
background: isDeleteConfirmed ? 'var(--error)' : 'var(--bg-tertiary)',
color: isDeleteConfirmed ? '#fff' : 'var(--text-tertiary)',
border: 'none',
borderRadius: 'var(--radius-sm, 4px)',
padding: '6px 16px',
fontSize: '13px',
cursor: isDeleteConfirmed ? 'pointer' : 'not-allowed',
}}
>
{deleting ? '删除中...' : '确认删除'}
</button>
</div>
</div>
</div>
)}
</div> </div>
); );
} }