Commit 6b2df3e7 authored by 375242562@qq.com's avatar 375242562@qq.com

feat: 批量匹配操作日志非进行中任务支持删除

后端:新增 DELETE /matching/batch/jobs/{id},运行中/待启动状态拒绝删除
前端:
- batchMatchingService 新增 deleteJob() 方法
- 操作日志操作列:运行中/待启动显示停止按钮,其余状态显示删除按钮
- 删除前弹出 MUI Dialog 确认,展示试验名称、状态和进度信息
- 删除成功后自动刷新当前页,末页无数据时自动回退一页
Co-Authored-By: default avatarClaude Sonnet 4.6 <noreply@anthropic.com>
parent 2ee97d65
...@@ -100,6 +100,22 @@ async def get_batch_job( ...@@ -100,6 +100,22 @@ async def get_batch_job(
return _job_to_response(job, trial.title if trial else None) return _job_to_response(job, trial.title if trial else None)
@router.delete("/jobs/{job_id}", status_code=204)
async def delete_batch_job(
job_id: str,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Delete a finished batch job log (running/pending jobs cannot be deleted)."""
job = await db.get(BatchMatchingJob, job_id)
if not job:
raise HTTPException(status_code=404, detail="Job not found")
if job.status in (BatchJobStatus.running, BatchJobStatus.pending):
raise HTTPException(status_code=400, detail="运行中或待启动的任务不能删除,请先停止")
await db.delete(job)
await db.commit()
@router.post("/jobs/{job_id}/cancel", response_model=BatchJobResponse) @router.post("/jobs/{job_id}/cancel", response_model=BatchJobResponse)
async def cancel_batch_job( async def cancel_batch_job(
job_id: str, job_id: str,
......
...@@ -5,6 +5,7 @@ import { ...@@ -5,6 +5,7 @@ import {
Tabs, Tab, Alert, Divider, LinearProgress, Chip, Button, CircularProgress, Tabs, Tab, Alert, Divider, LinearProgress, Chip, Button, CircularProgress,
Table, TableBody, TableCell, TableHead, TableRow, Table, TableBody, TableCell, TableHead, TableRow,
Collapse, IconButton, Tooltip, TablePagination, Collapse, IconButton, Tooltip, TablePagination,
Dialog, DialogTitle, DialogContent, DialogActions,
} from '@mui/material' } from '@mui/material'
import HistoryIcon from '@mui/icons-material/History' import HistoryIcon from '@mui/icons-material/History'
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline' import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'
...@@ -15,6 +16,8 @@ import PsychologyIcon from '@mui/icons-material/Psychology' ...@@ -15,6 +16,8 @@ import PsychologyIcon from '@mui/icons-material/Psychology'
import BatchPredictionIcon from '@mui/icons-material/BatchPrediction' import BatchPredictionIcon from '@mui/icons-material/BatchPrediction'
import StopIcon from '@mui/icons-material/Stop' import StopIcon from '@mui/icons-material/Stop'
import StopCircleIcon from '@mui/icons-material/StopCircle' import StopCircleIcon from '@mui/icons-material/StopCircle'
import DeleteIcon from '@mui/icons-material/Delete'
import WarningAmberRoundedIcon from '@mui/icons-material/WarningAmberRounded'
import { MatchingStepper } from '../components/matching/MatchingStepper' import { MatchingStepper } from '../components/matching/MatchingStepper'
import { MatchingResultCard } from '../components/matching/MatchingResultCard' import { MatchingResultCard } from '../components/matching/MatchingResultCard'
import { ReasoningTimeline } from '../components/matching/ReasoningTimeline' import { ReasoningTimeline } from '../components/matching/ReasoningTimeline'
...@@ -76,6 +79,8 @@ export function MatchingPage() { ...@@ -76,6 +79,8 @@ export function MatchingPage() {
const [logPage, setLogPage] = useState(0) const [logPage, setLogPage] = useState(0)
const [expandedErrorId, setExpandedErrorId] = useState<string | null>(null) const [expandedErrorId, setExpandedErrorId] = useState<string | null>(null)
const [cancellingJobId, setCancellingJobId] = useState<string | null>(null) const [cancellingJobId, setCancellingJobId] = useState<string | null>(null)
const [deleteJobTarget, setDeleteJobTarget] = useState<BatchJob | null>(null)
const [deletingJobId, setDeletingJobId] = useState<string | null>(null)
const loadJobHistory = useCallback(async (page = 0) => { const loadJobHistory = useCallback(async (page = 0) => {
// fetch PAGE_SIZE+1 to detect if a next page exists // fetch PAGE_SIZE+1 to detect if a next page exists
...@@ -180,6 +185,26 @@ export function MatchingPage() { ...@@ -180,6 +185,26 @@ export function MatchingPage() {
} }
} }
const handleDeleteJob = async () => {
if (!deleteJobTarget) return
setDeletingJobId(deleteJobTarget.id)
try {
await batchMatchingService.deleteJob(deleteJobTarget.id)
setDeleteJobTarget(null)
if (logPage > 0 && jobHistory.length === 1) {
const newPage = logPage - 1
setLogPage(newPage)
loadJobHistory(newPage)
} else {
loadJobHistory(logPage)
}
} catch {
setDeleteJobTarget(null)
} finally {
setDeletingJobId(null)
}
}
const handleLogPageChange = (_: unknown, newPage: number) => { const handleLogPageChange = (_: unknown, newPage: number) => {
setLogPage(newPage) setLogPage(newPage)
loadJobHistory(newPage) loadJobHistory(newPage)
...@@ -406,7 +431,7 @@ export function MatchingPage() { ...@@ -406,7 +431,7 @@ export function MatchingPage() {
</Typography> </Typography>
</TableCell> </TableCell>
<TableCell align="center"> <TableCell align="center">
{(job.status === 'running' || job.status === 'pending') && ( {(job.status === 'running' || job.status === 'pending') ? (
<Tooltip title={job.cancel_requested ? '停止中...' : '停止该任务'}> <Tooltip title={job.cancel_requested ? '停止中...' : '停止该任务'}>
<span> <span>
<IconButton <IconButton
...@@ -421,6 +446,21 @@ export function MatchingPage() { ...@@ -421,6 +446,21 @@ export function MatchingPage() {
</IconButton> </IconButton>
</span> </span>
</Tooltip> </Tooltip>
) : (
<Tooltip title="删除记录">
<span>
<IconButton
size="small"
color="error"
disabled={deletingJobId === job.id}
onClick={() => setDeleteJobTarget(job)}
>
{deletingJobId === job.id
? <CircularProgress size={14} color="error" />
: <DeleteIcon fontSize="small" />}
</IconButton>
</span>
</Tooltip>
)} )}
</TableCell> </TableCell>
</TableRow> </TableRow>
...@@ -601,6 +641,47 @@ export function MatchingPage() { ...@@ -601,6 +641,47 @@ export function MatchingPage() {
)} )}
</> </>
)} )}
{/* 删除操作日志确认弹窗 */}
<Dialog open={!!deleteJobTarget} onClose={() => setDeleteJobTarget(null)} maxWidth="xs" fullWidth>
<DialogTitle sx={{ display: 'flex', alignItems: 'center', gap: 1, pb: 1 }}>
<DeleteIcon color="error" fontSize="small" />
<Typography variant="subtitle1" fontWeight={600}>删除操作记录</Typography>
</DialogTitle>
<DialogContent sx={{ pt: 1 }}>
<Stack spacing={2}>
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1.5, p: 1.5, bgcolor: 'error.50', borderRadius: 1, border: '1px solid', borderColor: 'error.200' }}>
<WarningAmberRoundedIcon color="error" sx={{ mt: 0.25, flexShrink: 0 }} />
<Typography variant="body2" color="error.main" fontWeight={600}>
删除后将无法恢复
</Typography>
</Box>
{deleteJobTarget && (
<Stack spacing={1}>
<Box>
<Typography variant="caption" color="text.secondary">试验项目</Typography>
<Typography variant="body2" fontWeight={500}>{deleteJobTarget.trial_title ?? deleteJobTarget.trial_id ?? ''}</Typography>
</Box>
<Stack direction="row" spacing={3}>
<Box>
<Typography variant="caption" color="text.secondary">状态</Typography>
<Typography variant="body2">{JOB_STATUS_MAP[deleteJobTarget.status]?.label ?? deleteJobTarget.status}</Typography>
</Box>
<Box>
<Typography variant="caption" color="text.secondary">进度</Typography>
<Typography variant="body2">{deleteJobTarget.completed_pairs} / {deleteJobTarget.total_pairs}</Typography>
</Box>
</Stack>
</Stack>
)}
</Stack>
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2, gap: 1 }}>
<Button onClick={() => setDeleteJobTarget(null)} variant="outlined" color="inherit">取消</Button>
<Button variant="contained" color="error" startIcon={<DeleteIcon />} onClick={handleDeleteJob}>
确认删除
</Button>
</DialogActions>
</Dialog>
</Box> </Box>
) )
} }
...@@ -29,4 +29,7 @@ export const batchMatchingService = { ...@@ -29,4 +29,7 @@ export const batchMatchingService = {
cancelJob: (id: string) => cancelJob: (id: string) =>
api.post<BatchJob>(`/matching/batch/jobs/${id}/cancel`).then(r => r.data), api.post<BatchJob>(`/matching/batch/jobs/${id}/cancel`).then(r => r.data),
deleteJob: (id: string) =>
api.delete(`/matching/batch/jobs/${id}`),
} }
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment