add del project
All checks were successful
Build and Deploy Log Center / build-and-deploy (push) Successful in 1m42s
All checks were successful
Build and Deploy Log Center / build-and-deploy (push) Successful in 1m42s
This commit is contained in:
parent
01f87e38aa
commit
fc6dc52f72
13
app/main.py
13
app/main.py
@ -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"}
|
||||||
|
|||||||
@ -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 });
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user