first commit
This commit is contained in:
335
frontend/src/pages/transactions/Transactions.tsx
Normal file
335
frontend/src/pages/transactions/Transactions.tsx
Normal file
@@ -0,0 +1,335 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
Card,
|
||||
Table,
|
||||
Tag,
|
||||
Typography,
|
||||
Space,
|
||||
Select,
|
||||
Tooltip,
|
||||
Badge,
|
||||
Drawer,
|
||||
Descriptions,
|
||||
Button,
|
||||
Alert,
|
||||
Row,
|
||||
Col,
|
||||
Statistic,
|
||||
} from 'antd';
|
||||
import {
|
||||
SwapOutlined,
|
||||
WarningOutlined,
|
||||
LinkOutlined,
|
||||
EyeOutlined,
|
||||
ArrowUpOutlined,
|
||||
ArrowDownOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import type { TransactionRecord, SourceApp } from '../../types';
|
||||
import { fetchTransactions } from '../../services/api';
|
||||
|
||||
const appTag: Record<SourceApp, { label: string; color: string }> = {
|
||||
wechat: { label: '微信', color: 'green' },
|
||||
alipay: { label: '支付宝', color: 'blue' },
|
||||
bank: { label: '银行', color: 'purple' },
|
||||
digital_wallet: { label: '数字钱包', color: 'orange' },
|
||||
other: { label: '其他', color: 'default' },
|
||||
};
|
||||
|
||||
const Transactions: React.FC = () => {
|
||||
const { id = '1' } = useParams();
|
||||
const [filterDuplicate, setFilterDuplicate] = useState<string>('all');
|
||||
const [detail, setDetail] = useState<TransactionRecord | null>(null);
|
||||
|
||||
const { data: txData } = useQuery({
|
||||
queryKey: ['transactions', id],
|
||||
queryFn: () => fetchTransactions(id),
|
||||
});
|
||||
const allTransactions = txData?.items ?? [];
|
||||
|
||||
const data =
|
||||
filterDuplicate === 'all'
|
||||
? allTransactions
|
||||
: filterDuplicate === 'unique'
|
||||
? allTransactions.filter((t) => !t.isDuplicate)
|
||||
: allTransactions.filter((t) => t.isDuplicate);
|
||||
|
||||
const totalOut = allTransactions
|
||||
.filter((t) => t.direction === 'out' && !t.isDuplicate)
|
||||
.reduce((s, t) => s + t.amount, 0);
|
||||
const totalIn = allTransactions
|
||||
.filter((t) => t.direction === 'in' && !t.isDuplicate)
|
||||
.reduce((s, t) => s + t.amount, 0);
|
||||
const duplicateCount = allTransactions.filter((t) => t.isDuplicate).length;
|
||||
const transitCount = allTransactions.filter((t) => t.isTransit).length;
|
||||
|
||||
const columns: ColumnsType<TransactionRecord> = [
|
||||
{
|
||||
title: '交易时间',
|
||||
dataIndex: 'tradeTime',
|
||||
width: 170,
|
||||
sorter: (a, b) => a.tradeTime.localeCompare(b.tradeTime),
|
||||
defaultSortOrder: 'ascend',
|
||||
},
|
||||
{
|
||||
title: '来源',
|
||||
dataIndex: 'sourceApp',
|
||||
width: 100,
|
||||
render: (app: SourceApp) => (
|
||||
<Tag color={appTag[app].color}>{appTag[app].label}</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '金额(元)',
|
||||
dataIndex: 'amount',
|
||||
width: 140,
|
||||
align: 'right',
|
||||
render: (v: number, r) => (
|
||||
<Typography.Text
|
||||
strong
|
||||
style={{ color: r.direction === 'out' ? '#cf1322' : '#389e0d' }}
|
||||
>
|
||||
{r.direction === 'out' ? '-' : '+'}¥{v.toLocaleString('zh-CN', { minimumFractionDigits: 2 })}
|
||||
</Typography.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '方向',
|
||||
dataIndex: 'direction',
|
||||
width: 70,
|
||||
align: 'center',
|
||||
render: (d: string) =>
|
||||
d === 'out' ? (
|
||||
<ArrowUpOutlined style={{ color: '#cf1322' }} />
|
||||
) : (
|
||||
<ArrowDownOutlined style={{ color: '#389e0d' }} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '对方',
|
||||
dataIndex: 'counterpartyName',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
dataIndex: 'remark',
|
||||
width: 120,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '标记',
|
||||
width: 130,
|
||||
render: (_, r) => (
|
||||
<Space size={4}>
|
||||
{r.isDuplicate && (
|
||||
<Tooltip title="该笔为重复展示记录,已与其他截图中的同笔交易合并">
|
||||
<Tag color="red">重复</Tag>
|
||||
</Tooltip>
|
||||
)}
|
||||
{r.isTransit && (
|
||||
<Tooltip title="该笔为本人账户间中转,不直接计入被骗金额">
|
||||
<Tag color="orange">中转</Tag>
|
||||
</Tooltip>
|
||||
)}
|
||||
{!r.isDuplicate && !r.isTransit && (
|
||||
<Tag color="green">有效</Tag>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '置信度',
|
||||
dataIndex: 'confidence',
|
||||
width: 80,
|
||||
align: 'center',
|
||||
render: (v: number) => (
|
||||
<Badge
|
||||
color={v >= 0.9 ? '#52c41a' : v >= 0.8 ? '#faad14' : '#ff4d4f'}
|
||||
text={`${(v * 100).toFixed(0)}%`}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 80,
|
||||
render: (_, r) => (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => setDetail(r)}
|
||||
>
|
||||
详情
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Row gutter={16} style={{ marginBottom: 24 }}>
|
||||
<Col span={6}>
|
||||
<Card variant="borderless">
|
||||
<Statistic
|
||||
title="总支出(去重后)"
|
||||
value={totalOut}
|
||||
precision={2}
|
||||
prefix="¥"
|
||||
valueStyle={{ color: '#cf1322' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card variant="borderless">
|
||||
<Statistic
|
||||
title="总收入(去重后)"
|
||||
value={totalIn}
|
||||
precision={2}
|
||||
prefix="¥"
|
||||
valueStyle={{ color: '#389e0d' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card variant="borderless">
|
||||
<Statistic
|
||||
title="重复记录"
|
||||
value={duplicateCount}
|
||||
suffix="笔"
|
||||
valueStyle={{ color: '#cf1322' }}
|
||||
prefix={<WarningOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card variant="borderless">
|
||||
<Statistic
|
||||
title="中转记录"
|
||||
value={transitCount}
|
||||
suffix="笔"
|
||||
valueStyle={{ color: '#fa8c16' }}
|
||||
prefix={<LinkOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{duplicateCount > 0 && (
|
||||
<Alert
|
||||
message={`系统识别出 ${duplicateCount} 笔重复展示记录`}
|
||||
description={'同一笔交易可能在列表页、详情页、短信通知中多次出现。标记为「重复」的记录已被合并,不会重复计入金额汇总。'}
|
||||
type="info"
|
||||
showIcon
|
||||
closable
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<SwapOutlined />
|
||||
<span>交易明细</span>
|
||||
<Tag>{allTransactions.length} 笔</Tag>
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<Select
|
||||
value={filterDuplicate}
|
||||
onChange={setFilterDuplicate}
|
||||
style={{ width: 160 }}
|
||||
options={[
|
||||
{ label: '全部交易', value: 'all' },
|
||||
{ label: '仅有效交易', value: 'unique' },
|
||||
{ label: '仅重复交易', value: 'duplicate' },
|
||||
]}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Table
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
pagination={false}
|
||||
rowClassName={(r) =>
|
||||
r.isDuplicate
|
||||
? 'row-duplicate'
|
||||
: r.isTransit
|
||||
? 'row-transit'
|
||||
: ''
|
||||
}
|
||||
size="middle"
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Drawer
|
||||
title="交易详情"
|
||||
placement="right"
|
||||
width={480}
|
||||
open={!!detail}
|
||||
onClose={() => setDetail(null)}
|
||||
>
|
||||
{detail && (
|
||||
<Descriptions column={1} bordered size="small">
|
||||
<Descriptions.Item label="交易时间">
|
||||
{detail.tradeTime}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="来源APP">
|
||||
<Tag color={appTag[detail.sourceApp].color}>
|
||||
{appTag[detail.sourceApp].label}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="金额">
|
||||
<Typography.Text
|
||||
strong
|
||||
style={{
|
||||
color: detail.direction === 'out' ? '#cf1322' : '#389e0d',
|
||||
}}
|
||||
>
|
||||
{detail.direction === 'out' ? '-' : '+'}¥
|
||||
{detail.amount.toLocaleString('zh-CN', { minimumFractionDigits: 2 })}
|
||||
</Typography.Text>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="对方">
|
||||
{detail.counterpartyName}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="对方账号">
|
||||
{detail.counterpartyAccount || '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="本方账户尾号">
|
||||
{detail.selfAccountTailNo || '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="订单号">
|
||||
{detail.orderNo}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="备注">{detail.remark}</Descriptions.Item>
|
||||
<Descriptions.Item label="置信度">
|
||||
{(detail.confidence * 100).toFixed(0)}%
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="证据截图">
|
||||
<Button type="link" size="small" icon={<EyeOutlined />}>
|
||||
查看原图 ({detail.evidenceImageId})
|
||||
</Button>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="归并簇">
|
||||
{detail.clusterId || '独立交易'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="标记">
|
||||
<Space>
|
||||
{detail.isDuplicate && <Tag color="red">重复</Tag>}
|
||||
{detail.isTransit && <Tag color="orange">中转</Tag>}
|
||||
{!detail.isDuplicate && !detail.isTransit && (
|
||||
<Tag color="green">有效</Tag>
|
||||
)}
|
||||
</Space>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
)}
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Transactions;
|
||||
Reference in New Issue
Block a user