264 lines
7.3 KiB
TypeScript
264 lines
7.3 KiB
TypeScript
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;
|