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)): ...@@ -109,6 +109,19 @@ async def get_result(result_id: str, db: AsyncSession = Depends(get_db)):
return _result_to_response(r) 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) @router.put("/results/{result_id}/review", response_model=MatchingResultResponse)
async def review_result( async def review_result(
result_id: str, result_id: str,
......
import { useState, useEffect, useCallback, useRef } from 'react' import { useState, useEffect, useCallback, useRef } from 'react'
import { import {
Box, Typography, Card, CardContent, Stack, Button, 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' } from '@mui/material'
import { DataGrid, GridColDef } from '@mui/x-data-grid' import { DataGrid, GridColDef } from '@mui/x-data-grid'
import NotificationsIcon from '@mui/icons-material/Notifications' import NotificationsIcon from '@mui/icons-material/Notifications'
import MonitorHeartIcon from '@mui/icons-material/MonitorHeart' import MonitorHeartIcon from '@mui/icons-material/MonitorHeart'
import VisibilityIcon from '@mui/icons-material/Visibility' import VisibilityIcon from '@mui/icons-material/Visibility'
import FilterListIcon from '@mui/icons-material/FilterList' 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 { RecommendationDrawer } from '../components/workstation/RecommendationDrawer'
import { RecommendationSnackbar } from '../components/workstation/RecommendationSnackbar' import { RecommendationSnackbar } from '../components/workstation/RecommendationSnackbar'
import { MatchingDetailDrawer } from '../components/matching/MatchingDetailDrawer' import { MatchingDetailDrawer } from '../components/matching/MatchingDetailDrawer'
...@@ -38,6 +41,7 @@ export function WorkStationPage() { ...@@ -38,6 +41,7 @@ export function WorkStationPage() {
const [selectedResult, setSelectedResult] = useState<MatchingResult | null>(null) const [selectedResult, setSelectedResult] = useState<MatchingResult | null>(null)
const [detailOpen, setDetailOpen] = useState(false) const [detailOpen, setDetailOpen] = useState(false)
const [deleteTarget, setDeleteTarget] = useState<MatchingResult | null>(null)
const shownNotificationIds = useRef<Set<string>>(new Set()) const shownNotificationIds = useRef<Set<string>>(new Set())
const resultColumns: GridColDef[] = [ const resultColumns: GridColDef[] = [
...@@ -67,18 +71,32 @@ export function WorkStationPage() { ...@@ -67,18 +71,32 @@ export function WorkStationPage() {
) : <Typography variant="caption" color="text.disabled">未审核</Typography>, ) : <Typography variant="caption" color="text.disabled">未审核</Typography>,
}, },
{ {
field: 'actions', headerName: '操作', width: 80, sortable: false, field: 'actions', headerName: '操作', width: 110, sortable: false,
renderCell: (params) => ( renderCell: (params) => (
<Stack direction="row" sx={{ height: '100%', alignItems: 'center' }}> <Stack direction="row" sx={{ height: '100%', alignItems: 'center' }} spacing={0.5}>
<IconButton <Tooltip title="查看详情">
size="small" color="primary" <IconButton
onClick={() => { size="small" color="primary"
setSelectedResult(params.row as MatchingResult) onClick={(e) => {
setDetailOpen(true) e.stopPropagation()
}} setSelectedResult(params.row as MatchingResult)
> setDetailOpen(true)
<VisibilityIcon fontSize="small" /> }}
</IconButton> >
<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> </Stack>
) )
} }
...@@ -127,6 +145,17 @@ export function WorkStationPage() { ...@@ -127,6 +145,17 @@ export function WorkStationPage() {
loadNotifications() loadNotifications()
} }
const handleDeleteConfirm = async () => {
if (!deleteTarget) return
try {
await matchingService.delete(deleteTarget.id)
setDeleteTarget(null)
loadResults()
} catch {
// 静默失败,保持弹窗打开
}
}
return ( return (
<Box> <Box>
<Stack direction="row" alignItems="center" spacing={2} mb={3}> <Stack direction="row" alignItems="center" spacing={2} mb={3}>
...@@ -246,6 +275,56 @@ export function WorkStationPage() { ...@@ -246,6 +275,56 @@ export function WorkStationPage() {
onOpenDrawer={() => { setDrawerOpen(true); setSnackNotification(null) }} onOpenDrawer={() => { setDrawerOpen(true); setSnackNotification(null) }}
onDismiss={() => 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> </Box>
) )
} }
...@@ -13,4 +13,7 @@ export const matchingService = { ...@@ -13,4 +13,7 @@ export const matchingService = {
review: (id: string, data: { decision: string; notes?: string }) => review: (id: string, data: { decision: string; notes?: string }) =>
api.put<MatchingResult>(`/matching/results/${id}/review`, data).then(r => r.data), 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