Commit f2badcc8 authored by 375242562@qq.com's avatar 375242562@qq.com

feat: 医生工作站匹配列表新增删除操作

- 后端:新增 DELETE /matching/results/{id} 接口(需登录权限)
- 前端:matchingService 增加 delete() 方法
- 前端:WorkStationPage 操作栏加删除图标按钮,点击弹出
  MUI Dialog 确认弹窗(含警告提示、患者/试验详情展示)
  风格与系统其他删除确认框保持一致
Co-Authored-By: default avatarClaude Sonnet 4.6 <noreply@anthropic.com>
parent fd295ed7
......@@ -109,6 +109,19 @@ async def get_result(result_id: str, db: AsyncSession = Depends(get_db)):
return _result_to_response(r)
@router.delete("/results/{result_id}", status_code=204)
async def delete_result(
result_id: str,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
r = await db.get(MatchingResult, result_id)
if not r:
raise HTTPException(status_code=404, detail="Matching result not found")
await db.delete(r)
await db.commit()
@router.put("/results/{result_id}/review", response_model=MatchingResultResponse)
async def review_result(
result_id: str,
......
import { useState, useEffect, useCallback, useRef } from 'react'
import {
Box, Typography, Card, CardContent, Stack, Button,
Chip, Divider, Badge, IconButton, FormControl, InputLabel, Select, MenuItem
Chip, Divider, Badge, IconButton, FormControl, InputLabel, Select, MenuItem,
Dialog, DialogTitle, DialogContent, DialogActions, Tooltip,
} from '@mui/material'
import { DataGrid, GridColDef } from '@mui/x-data-grid'
import NotificationsIcon from '@mui/icons-material/Notifications'
import MonitorHeartIcon from '@mui/icons-material/MonitorHeart'
import VisibilityIcon from '@mui/icons-material/Visibility'
import FilterListIcon from '@mui/icons-material/FilterList'
import DeleteIcon from '@mui/icons-material/Delete'
import WarningAmberRoundedIcon from '@mui/icons-material/WarningAmberRounded'
import { RecommendationDrawer } from '../components/workstation/RecommendationDrawer'
import { RecommendationSnackbar } from '../components/workstation/RecommendationSnackbar'
import { MatchingDetailDrawer } from '../components/matching/MatchingDetailDrawer'
......@@ -38,6 +41,7 @@ export function WorkStationPage() {
const [selectedResult, setSelectedResult] = useState<MatchingResult | null>(null)
const [detailOpen, setDetailOpen] = useState(false)
const [deleteTarget, setDeleteTarget] = useState<MatchingResult | null>(null)
const shownNotificationIds = useRef<Set<string>>(new Set())
const resultColumns: GridColDef[] = [
......@@ -67,18 +71,32 @@ export function WorkStationPage() {
) : <Typography variant="caption" color="text.disabled">未审核</Typography>,
},
{
field: 'actions', headerName: '操作', width: 80, sortable: false,
field: 'actions', headerName: '操作', width: 110, sortable: false,
renderCell: (params) => (
<Stack direction="row" sx={{ height: '100%', alignItems: 'center' }}>
<Stack direction="row" sx={{ height: '100%', alignItems: 'center' }} spacing={0.5}>
<Tooltip title="查看详情">
<IconButton
size="small" color="primary"
onClick={() => {
onClick={(e) => {
e.stopPropagation()
setSelectedResult(params.row as MatchingResult)
setDetailOpen(true)
}}
>
<VisibilityIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="删除记录">
<IconButton
size="small" color="error"
onClick={(e) => {
e.stopPropagation()
setDeleteTarget(params.row as MatchingResult)
}}
>
<DeleteIcon fontSize="small" />
</IconButton>
</Tooltip>
</Stack>
)
}
......@@ -127,6 +145,17 @@ export function WorkStationPage() {
loadNotifications()
}
const handleDeleteConfirm = async () => {
if (!deleteTarget) return
try {
await matchingService.delete(deleteTarget.id)
setDeleteTarget(null)
loadResults()
} catch {
// 静默失败,保持弹窗打开
}
}
return (
<Box>
<Stack direction="row" alignItems="center" spacing={2} mb={3}>
......@@ -246,6 +275,56 @@ export function WorkStationPage() {
onOpenDrawer={() => { setDrawerOpen(true); setSnackNotification(null) }}
onDismiss={() => setSnackNotification(null)}
/>
{/* 删除匹配记录确认弹窗 */}
<Dialog
open={!!deleteTarget}
onClose={() => setDeleteTarget(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}>
删除后将无法恢复,AI 推理依据也将一并清除
</Typography>
</Box>
<Stack spacing={1}>
<Box>
<Typography variant="caption" color="text.secondary">患者</Typography>
<Typography variant="body2" fontWeight={500}>{deleteTarget?.patient_name}</Typography>
</Box>
<Box>
<Typography variant="caption" color="text.secondary">临床试验</Typography>
<Typography variant="body2">{deleteTarget?.trial_title}</Typography>
</Box>
<Box>
<Typography variant="caption" color="text.secondary">匹配状态</Typography>
<Typography variant="body2">{deleteTarget?.status}</Typography>
</Box>
</Stack>
</Stack>
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2, gap: 1 }}>
<Button onClick={() => setDeleteTarget(null)} variant="outlined" color="inherit">
取消
</Button>
<Button
variant="contained"
color="error"
startIcon={<DeleteIcon />}
onClick={handleDeleteConfirm}
>
确认删除
</Button>
</DialogActions>
</Dialog>
</Box>
)
}
......@@ -13,4 +13,7 @@ export const matchingService = {
review: (id: string, data: { decision: string; notes?: string }) =>
api.put<MatchingResult>(`/matching/results/${id}/review`, data).then(r => r.data),
delete: (id: string) =>
api.delete(`/matching/results/${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