Files
fund-tracer/frontend/src/pages/cases/CaseList.tsx
2026-03-12 20:04:27 +08:00

264 lines
7.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import {
Card,
Table,
Button,
Tag,
Space,
Input,
Typography,
Modal,
Form,
Row,
Col,
Statistic,
message,
} from 'antd';
import {
PlusOutlined,
SearchOutlined,
FolderOpenOutlined,
ClockCircleOutlined,
CheckCircleOutlined,
ExclamationCircleOutlined,
} from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import type { CaseRecord, CaseStatus } from '../../types';
import { fetchCases, createCase } from '../../services/api';
const statusConfig: Record<CaseStatus, { color: string; label: string }> = {
pending: { color: 'default', label: '待处理' },
uploading: { color: 'processing', label: '上传中' },
analyzing: { color: 'blue', label: '分析中' },
reviewing: { color: 'orange', label: '待复核' },
completed: { color: 'green', label: '已完成' },
};
const CaseList: React.FC = () => {
const navigate = useNavigate();
const qc = useQueryClient();
const [createOpen, setCreateOpen] = useState(false);
const [form] = Form.useForm();
const [search, setSearch] = useState('');
const { data, isLoading } = useQuery({
queryKey: ['cases', search],
queryFn: () => fetchCases({ search: search || undefined }),
});
const cases = data?.items ?? [];
const createMutation = useMutation({
mutationFn: createCase,
onSuccess: () => {
message.success('案件创建成功');
qc.invalidateQueries({ queryKey: ['cases'] });
setCreateOpen(false);
form.resetFields();
},
});
const totalCases = cases.length;
const pendingReview = cases.filter((c) => c.status === 'reviewing').length;
const completedCount = cases.filter((c) => c.status === 'completed').length;
const analyzingCount = cases.filter(
(c) => c.status === 'analyzing' || c.status === 'uploading',
).length;
const columns: ColumnsType<CaseRecord> = [
{
title: '案件编号',
dataIndex: 'caseNo',
width: 180,
render: (text, record) => (
<a onClick={() => navigate(`/cases/${record.id}/workspace`)}>{text}</a>
),
},
{ title: '案件名称', dataIndex: 'title', ellipsis: true },
{ title: '受害人', dataIndex: 'victimName', width: 100 },
{ title: '承办人', dataIndex: 'handler', width: 100 },
{
title: '状态',
dataIndex: 'status',
width: 100,
render: (s: CaseStatus) => (
<Tag color={statusConfig[s].color}>{statusConfig[s].label}</Tag>
),
},
{
title: '截图数',
dataIndex: 'imageCount',
width: 80,
align: 'center',
},
{
title: '识别金额(元)',
dataIndex: 'totalAmount',
width: 140,
align: 'right',
render: (v: number) =>
v > 0 ? (
<Typography.Text strong style={{ color: '#cf1322' }}>
¥{v.toLocaleString('zh-CN', { minimumFractionDigits: 2 })}
</Typography.Text>
) : (
<Typography.Text type="secondary">-</Typography.Text>
),
},
{
title: '更新时间',
dataIndex: 'updatedAt',
width: 170,
},
{
title: '操作',
width: 160,
render: (_, record) => (
<Space>
<Button
type="default"
size="small"
style={{
background: '#e6f4ff',
borderColor: '#91caff',
color: '#0958d9',
fontWeight: 600,
boxShadow: '0 1px 2px rgba(22, 119, 255, 0.12)',
}}
onClick={() => navigate(`/cases/${record.id}/workspace`)}
>
</Button>
</Space>
),
},
];
return (
<div>
<Row gutter={16} style={{ marginBottom: 24 }}>
<Col span={6}>
<Card variant="borderless">
<Statistic
title="全部案件"
value={totalCases}
prefix={<FolderOpenOutlined />}
/>
</Card>
</Col>
<Col span={6}>
<Card variant="borderless">
<Statistic
title="处理中"
value={analyzingCount}
prefix={<ClockCircleOutlined />}
valueStyle={{ color: '#1677ff' }}
/>
</Card>
</Col>
<Col span={6}>
<Card variant="borderless">
<Statistic
title="待复核"
value={pendingReview}
prefix={<ExclamationCircleOutlined />}
valueStyle={{ color: '#fa8c16' }}
/>
</Card>
</Col>
<Col span={6}>
<Card variant="borderless">
<Statistic
title="已完成"
value={completedCount}
prefix={<CheckCircleOutlined />}
valueStyle={{ color: '#52c41a' }}
/>
</Card>
</Col>
</Row>
<Card
title="案件列表"
extra={
<Space>
<Input
placeholder="搜索案件编号、名称"
prefix={<SearchOutlined />}
style={{ width: 240 }}
allowClear
onPressEnter={(e) => setSearch((e.target as HTMLInputElement).value)}
onChange={(e) => !e.target.value && setSearch('')}
/>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => setCreateOpen(true)}
>
</Button>
</Space>
}
>
<Table
rowKey="id"
columns={columns}
dataSource={cases}
loading={isLoading}
pagination={{ pageSize: 10, showSizeChanger: true, showTotal: (t) => `${t}` }}
/>
</Card>
<Modal
title="新建案件"
open={createOpen}
onCancel={() => setCreateOpen(false)}
onOk={() => {
form.validateFields().then((values) => createMutation.mutate(values));
}}
confirmLoading={createMutation.isPending}
okText="创建"
cancelText="取消"
destroyOnClose
>
<Form form={form} layout="vertical" style={{ marginTop: 16 }}>
<Form.Item
label="案件编号"
name="caseNo"
rules={[{ required: true, message: '请输入案件编号' }]}
>
<Input placeholder="如ZA-2026-001XXX" />
</Form.Item>
<Form.Item
label="案件名称"
name="title"
rules={[{ required: true, message: '请输入案件名称' }]}
>
<Input placeholder="如:张某被电信诈骗案" />
</Form.Item>
<Row gutter={16}>
<Col span={12}>
<Form.Item
label="受害人姓名"
name="victimName"
rules={[{ required: true, message: '请输入受害人姓名' }]}
>
<Input />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="承办人" name="handler">
<Input />
</Form.Item>
</Col>
</Row>
</Form>
</Modal>
</div>
);
};
export default CaseList;