import React, { useEffect, 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, Input, InputNumber, Dropdown, } 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 { fetchImageDetail, fetchTransactions } from '../../services/api'; const appTag: Record = { wechat: { label: '微信', color: 'green' }, alipay: { label: '支付宝', color: 'blue' }, bank: { label: '银行', color: 'purple' }, digital_wallet: { label: '数字钱包', color: 'orange' }, other: { label: '其他', color: 'default' }, }; const splitDateTime = (raw: string): { date: string; time: string } => { if (!raw) return { date: '-', time: '-' }; const normalized = raw.trim().replace(' ', 'T'); if (!normalized.includes('T')) return { date: normalized, time: '-' }; const [datePart, timePartRaw = ''] = normalized.split('T'); const cleanedTime = timePartRaw.replace('Z', '').split('.')[0] || '-'; return { date: datePart || '-', time: cleanedTime }; }; const Transactions: React.FC = () => { const { id = '1' } = useParams(); const [filterDuplicate, setFilterDuplicate] = useState('all'); const [detail, setDetail] = useState(null); const [editableDetail, setEditableDetail] = useState(null); const [markOverrides, setMarkOverrides] = useState>({}); const { data: txData } = useQuery({ queryKey: ['transactions', id], queryFn: () => fetchTransactions(id), }); const allTransactions = txData?.items ?? []; const { data: detailImage, isFetching: detailImageFetching } = useQuery({ queryKey: ['image-detail', detail?.evidenceImageId], queryFn: () => fetchImageDetail(detail!.evidenceImageId), enabled: !!detail?.evidenceImageId, }); useEffect(() => { setEditableDetail(detail ? { ...detail } : null); }, [detail]); const getEffectiveMark = (tx: TransactionRecord): 'duplicate' | 'transit' | 'valid' => { if (markOverrides[tx.id]) return markOverrides[tx.id]; if (tx.isDuplicate) return 'duplicate'; if (tx.isTransit) return 'transit'; return 'valid'; }; const data = filterDuplicate === 'all' ? allTransactions : filterDuplicate === 'unique' ? allTransactions.filter((t) => getEffectiveMark(t) !== 'duplicate') : allTransactions.filter((t) => getEffectiveMark(t) === 'duplicate'); const totalOut = allTransactions .filter((t) => t.direction === 'out' && getEffectiveMark(t) !== 'duplicate') .reduce((s, t) => s + t.amount, 0); const totalIn = allTransactions .filter((t) => t.direction === 'in' && getEffectiveMark(t) !== 'duplicate') .reduce((s, t) => s + t.amount, 0); const duplicateCount = allTransactions.filter((t) => getEffectiveMark(t) === 'duplicate').length; const transitCount = allTransactions.filter((t) => getEffectiveMark(t) === 'transit').length; const columns: ColumnsType = [ { title: '交易时间', dataIndex: 'tradeTime', width: 96, render: (raw: string) => { const { date, time } = splitDateTime(raw); return (
{date}
{time}
); }, sorter: (a, b) => a.tradeTime.localeCompare(b.tradeTime), defaultSortOrder: 'ascend', }, { title: '来源', dataIndex: 'sourceApp', width: 80, render: (app: SourceApp) => ( {appTag[app].label} ), }, { title: '金额(元)', dataIndex: 'amount', width: 140, align: 'right', render: (v: number, r) => ( {r.direction === 'out' ? '-' : '+'}¥{v.toLocaleString('zh-CN', { minimumFractionDigits: 2 })} ), }, { title: '方向', dataIndex: 'direction', width: 70, align: 'center', render: (d: string) => d === 'out' ? ( ) : ( ), }, { title: '对方', dataIndex: 'counterpartyName', width: 120, ellipsis: true, }, { title: '备注', dataIndex: 'remark', width: 120, ellipsis: true, }, { title: '标记', width: 73, render: (_, r) => ( (() => { const mark = getEffectiveMark(r); const styleByMark: Record<'duplicate' | 'transit' | 'valid', { bg: string; border: string; color: string; label: string }> = { duplicate: { bg: '#fff2e8', border: '#ffbb96', color: '#cf1322', label: '重复' }, transit: { bg: '#fff7e6', border: '#ffd591', color: '#d46b08', label: '中转' }, valid: { bg: '#f6ffed', border: '#b7eb8f', color: '#389e0d', label: '有效' }, }; const cfg = styleByMark[mark]; const options: Array<'duplicate' | 'transit' | 'valid'> = ['duplicate', 'transit', 'valid']; return ( v !== mark) .map((v) => ({ key: v, label: v === 'duplicate' ? '重复' : v === 'transit' ? '中转' : '有效', })), onClick: ({ key }) => setMarkOverrides((prev) => ({ ...prev, [r.id]: key as 'duplicate' | 'transit' | 'valid' })), }} trigger={['click']} > ); })() ), }, { title: '置信度', dataIndex: 'confidence', width: 80, align: 'center', render: (v: number) => ( = 0.9 ? '#52c41a' : v >= 0.8 ? '#faad14' : '#ff4d4f'} text={`${(v * 100).toFixed(0)}%`} /> ), }, { title: '操作', width: 80, render: (_, r) => ( ), }, ]; return (
} /> } /> {duplicateCount > 0 && ( )} 交易明细 {allTransactions.length} 笔 } extra={ setEditableDetail((prev) => (prev ? { ...prev, tradeTime: e.target.value } : prev)) } /> setEditableDetail((prev) => (prev ? { ...prev, direction: val } : prev)) } options={[ { label: '转入', value: 'in' }, { label: '转出', value: 'out' }, ]} /> setEditableDetail((prev) => (prev ? { ...prev, counterpartyName: e.target.value } : prev)) } /> setEditableDetail((prev) => (prev ? { ...prev, counterpartyAccount: e.target.value } : prev)) } /> setEditableDetail((prev) => (prev ? { ...prev, selfAccountTailNo: e.target.value } : prev)) } /> setEditableDetail((prev) => (prev ? { ...prev, orderNo: e.target.value } : prev)) } /> setEditableDetail((prev) => (prev ? { ...prev, remark: e.target.value } : prev)) } /> setEditableDetail((prev) => (prev ? { ...prev, confidence: Number(val ?? 0) } : prev)) } /> {getEffectiveMark(detail) === 'duplicate' && 重复} {getEffectiveMark(detail) === 'transit' && 中转} {getEffectiveMark(detail) === 'valid' && ( 有效 )} )}
); }; export default Transactions;