update: del

This commit is contained in:
2026-03-13 23:53:26 +08:00
parent c72fbc9a14
commit bb543eb350
4 changed files with 93 additions and 4 deletions

View File

@@ -1,12 +1,21 @@
from uuid import UUID
from datetime import datetime, timezone
from fastapi import APIRouter, Depends, Query
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.models.case import Case, CaseStatus
from app.repositories.case_repo import CaseRepository
from app.schemas.case import CaseCreate, CaseUpdate, CaseOut, CaseListOut
from app.schemas.case import (
CaseCreate,
CaseUpdate,
CaseOut,
CaseListOut,
CaseDeleteIn,
CaseDeleteOut,
)
router = APIRouter()
@@ -41,7 +50,7 @@ async def list_cases(
async def get_case(case_id: UUID, db: AsyncSession = Depends(get_db)):
repo = CaseRepository(db)
case = await repo.get(case_id)
if not case:
if not case or case.deleted_at is not None:
from fastapi import HTTPException
raise HTTPException(status_code=404, detail="案件不存在")
return case
@@ -51,8 +60,25 @@ async def get_case(case_id: UUID, db: AsyncSession = Depends(get_db)):
async def update_case(case_id: UUID, body: CaseUpdate, db: AsyncSession = Depends(get_db)):
repo = CaseRepository(db)
case = await repo.get(case_id)
if not case:
if not case or case.deleted_at is not None:
from fastapi import HTTPException
raise HTTPException(status_code=404, detail="案件不存在")
case = await repo.update(case, body.model_dump(exclude_unset=True))
return case
@router.delete("", response_model=CaseDeleteOut)
async def delete_cases(body: CaseDeleteIn, db: AsyncSession = Depends(get_db)):
if not body.case_ids:
return CaseDeleteOut(deleted=0, message="未选择需要删除的案件")
result = await db.execute(
select(Case).where(Case.id.in_(body.case_ids), Case.deleted_at.is_(None))
)
cases = list(result.scalars().all())
now = datetime.now(timezone.utc)
for c in cases:
c.deleted_at = now
await db.flush()
return CaseDeleteOut(deleted=len(cases), message=f"已删除 {len(cases)} 个案件")

View File

@@ -35,3 +35,12 @@ class CaseOut(CamelModel):
class CaseListOut(CamelModel):
items: list[CaseOut]
total: int
class CaseDeleteIn(CamelModel):
case_ids: list[UUID]
class CaseDeleteOut(CamelModel):
deleted: int
message: str

View File

@@ -2,6 +2,7 @@ import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import {
App,
Card,
Table,
Button,
@@ -18,6 +19,7 @@ import {
} from 'antd';
import {
PlusOutlined,
DeleteOutlined,
SearchOutlined,
FolderOpenOutlined,
ClockCircleOutlined,
@@ -26,7 +28,7 @@ import {
} from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import type { CaseRecord, CaseStatus } from '../../types';
import { fetchCases, createCase } from '../../services/api';
import { fetchCases, createCase, deleteCases } from '../../services/api';
const statusConfig: Record<CaseStatus, { color: string; label: string }> = {
pending: { color: 'default', label: '待处理' },
@@ -48,9 +50,11 @@ const splitDateTime = (raw: string): { date: string; time: string } => {
const CaseList: React.FC = () => {
const navigate = useNavigate();
const qc = useQueryClient();
const { modal } = App.useApp();
const [createOpen, setCreateOpen] = useState(false);
const [form] = Form.useForm();
const [search, setSearch] = useState('');
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const { data, isLoading } = useQuery({
queryKey: ['cases', search],
@@ -69,6 +73,18 @@ const CaseList: React.FC = () => {
},
});
const deleteMutation = useMutation({
mutationFn: (caseIds: string[]) => deleteCases(caseIds),
onSuccess: (res) => {
message.success(res.message || `已删除 ${res.deleted} 个案件`);
setSelectedRowKeys([]);
qc.invalidateQueries({ queryKey: ['cases'] });
},
onError: () => {
message.error('删除案件失败');
},
});
const totalCases = cases.length;
const pendingReview = cases.filter((c) => c.status === 'reviewing').length;
const completedCount = cases.filter((c) => c.status === 'completed').length;
@@ -221,11 +237,36 @@ const CaseList: React.FC = () => {
>
</Button>
<Button
danger
icon={<DeleteOutlined />}
disabled={selectedRowKeys.length === 0}
loading={deleteMutation.isPending}
onClick={() => {
const selectedCases = cases.filter((c) => selectedRowKeys.includes(c.id));
modal.confirm({
title: '确认删除选中案件?',
content: `已选择 ${selectedCases.length} 个案件,删除后将不再显示在案件列表中。`,
okText: '确认删除',
okButtonProps: { danger: true },
cancelText: '取消',
onOk: () => {
deleteMutation.mutate(selectedRowKeys as string[]);
},
});
}}
>
</Button>
</Space>
}
>
<Table
rowKey="id"
rowSelection={{
selectedRowKeys,
onChange: (keys) => setSelectedRowKeys(keys),
}}
columns={columns}
dataSource={cases}
loading={isLoading}

View File

@@ -97,6 +97,19 @@ export async function fetchCase(id: string): Promise<CaseRecord> {
return request(`${BASE}/cases/${id}`);
}
export async function deleteCases(caseIds: string[]): Promise<{ deleted: number; message: string }> {
if (!(await isBackendUp())) {
return {
deleted: caseIds.length,
message: `Mock 模式:模拟删除 ${caseIds.length} 个案件`,
};
}
return request(`${BASE}/cases`, {
method: 'DELETE',
body: JSON.stringify({ case_ids: caseIds }),
});
}
// ── Images ──
export async function fetchImages(