update: del
This commit is contained in:
@@ -1,12 +1,21 @@
|
|||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Query
|
from fastapi import APIRouter, Depends, Query
|
||||||
|
from sqlalchemy import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.core.database import get_db
|
from app.core.database import get_db
|
||||||
from app.models.case import Case, CaseStatus
|
from app.models.case import Case, CaseStatus
|
||||||
from app.repositories.case_repo import CaseRepository
|
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()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -41,7 +50,7 @@ async def list_cases(
|
|||||||
async def get_case(case_id: UUID, db: AsyncSession = Depends(get_db)):
|
async def get_case(case_id: UUID, db: AsyncSession = Depends(get_db)):
|
||||||
repo = CaseRepository(db)
|
repo = CaseRepository(db)
|
||||||
case = await repo.get(case_id)
|
case = await repo.get(case_id)
|
||||||
if not case:
|
if not case or case.deleted_at is not None:
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
raise HTTPException(status_code=404, detail="案件不存在")
|
raise HTTPException(status_code=404, detail="案件不存在")
|
||||||
return case
|
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)):
|
async def update_case(case_id: UUID, body: CaseUpdate, db: AsyncSession = Depends(get_db)):
|
||||||
repo = CaseRepository(db)
|
repo = CaseRepository(db)
|
||||||
case = await repo.get(case_id)
|
case = await repo.get(case_id)
|
||||||
if not case:
|
if not case or case.deleted_at is not None:
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
raise HTTPException(status_code=404, detail="案件不存在")
|
raise HTTPException(status_code=404, detail="案件不存在")
|
||||||
case = await repo.update(case, body.model_dump(exclude_unset=True))
|
case = await repo.update(case, body.model_dump(exclude_unset=True))
|
||||||
return case
|
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)} 个案件")
|
||||||
|
|||||||
@@ -35,3 +35,12 @@ class CaseOut(CamelModel):
|
|||||||
class CaseListOut(CamelModel):
|
class CaseListOut(CamelModel):
|
||||||
items: list[CaseOut]
|
items: list[CaseOut]
|
||||||
total: int
|
total: int
|
||||||
|
|
||||||
|
|
||||||
|
class CaseDeleteIn(CamelModel):
|
||||||
|
case_ids: list[UUID]
|
||||||
|
|
||||||
|
|
||||||
|
class CaseDeleteOut(CamelModel):
|
||||||
|
deleted: int
|
||||||
|
message: str
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { useState } from 'react';
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import {
|
import {
|
||||||
|
App,
|
||||||
Card,
|
Card,
|
||||||
Table,
|
Table,
|
||||||
Button,
|
Button,
|
||||||
@@ -18,6 +19,7 @@ import {
|
|||||||
} from 'antd';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
SearchOutlined,
|
SearchOutlined,
|
||||||
FolderOpenOutlined,
|
FolderOpenOutlined,
|
||||||
ClockCircleOutlined,
|
ClockCircleOutlined,
|
||||||
@@ -26,7 +28,7 @@ import {
|
|||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import type { ColumnsType } from 'antd/es/table';
|
import type { ColumnsType } from 'antd/es/table';
|
||||||
import type { CaseRecord, CaseStatus } from '../../types';
|
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 }> = {
|
const statusConfig: Record<CaseStatus, { color: string; label: string }> = {
|
||||||
pending: { color: 'default', label: '待处理' },
|
pending: { color: 'default', label: '待处理' },
|
||||||
@@ -48,9 +50,11 @@ const splitDateTime = (raw: string): { date: string; time: string } => {
|
|||||||
const CaseList: React.FC = () => {
|
const CaseList: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
|
const { modal } = App.useApp();
|
||||||
const [createOpen, setCreateOpen] = useState(false);
|
const [createOpen, setCreateOpen] = useState(false);
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
|
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||||
|
|
||||||
const { data, isLoading } = useQuery({
|
const { data, isLoading } = useQuery({
|
||||||
queryKey: ['cases', search],
|
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 totalCases = cases.length;
|
||||||
const pendingReview = cases.filter((c) => c.status === 'reviewing').length;
|
const pendingReview = cases.filter((c) => c.status === 'reviewing').length;
|
||||||
const completedCount = cases.filter((c) => c.status === 'completed').length;
|
const completedCount = cases.filter((c) => c.status === 'completed').length;
|
||||||
@@ -221,11 +237,36 @@ const CaseList: React.FC = () => {
|
|||||||
>
|
>
|
||||||
新建案件
|
新建案件
|
||||||
</Button>
|
</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>
|
</Space>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Table
|
<Table
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
|
rowSelection={{
|
||||||
|
selectedRowKeys,
|
||||||
|
onChange: (keys) => setSelectedRowKeys(keys),
|
||||||
|
}}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={cases}
|
dataSource={cases}
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
|
|||||||
@@ -97,6 +97,19 @@ export async function fetchCase(id: string): Promise<CaseRecord> {
|
|||||||
return request(`${BASE}/cases/${id}`);
|
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 ──
|
// ── Images ──
|
||||||
|
|
||||||
export async function fetchImages(
|
export async function fetchImages(
|
||||||
|
|||||||
Reference in New Issue
Block a user