fix: mock
This commit is contained in:
@@ -175,6 +175,40 @@ const Analysis: React.FC = () => {
|
||||
a.tradeTime.localeCompare(b.tradeTime),
|
||||
);
|
||||
|
||||
const payeeAgg = useMemo(() => {
|
||||
const targetTx = validTx.filter((t) => t.direction === 'out' && !t.isTransit);
|
||||
const map = new Map<string, { name: string; amount: number; count: number; hasKeyword: boolean }>();
|
||||
const riskKeywords = ['刷单', '客服', '店铺', '商家', '返利', '任务', '垫付', '佣金', '淘宝'];
|
||||
for (const tx of targetTx) {
|
||||
const name = (tx.counterpartyName || '未知收款方').trim() || '未知收款方';
|
||||
const key = name.toLowerCase();
|
||||
const cur = map.get(key) ?? {
|
||||
name,
|
||||
amount: 0,
|
||||
count: 0,
|
||||
hasKeyword: false,
|
||||
};
|
||||
cur.amount += tx.amount;
|
||||
cur.count += 1;
|
||||
if (!cur.hasKeyword) {
|
||||
cur.hasKeyword = riskKeywords.some((kw) => name.includes(kw) || (tx.remark || '').includes(kw));
|
||||
}
|
||||
map.set(key, cur);
|
||||
}
|
||||
return Array.from(map.values())
|
||||
.map((item) => {
|
||||
const risk =
|
||||
item.hasKeyword || item.amount >= 50000 || item.count >= 3
|
||||
? 'high'
|
||||
: item.amount >= 20000 || item.count >= 2
|
||||
? 'medium'
|
||||
: 'low';
|
||||
return { ...item, risk };
|
||||
})
|
||||
.sort((a, b) => b.amount - a.amount)
|
||||
.slice(0, 10);
|
||||
}, [validTx]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card style={{ marginBottom: 16 }}>
|
||||
@@ -221,7 +255,7 @@ const Analysis: React.FC = () => {
|
||||
<Card variant="borderless">
|
||||
<Statistic
|
||||
title="涉诈对手方"
|
||||
value={3}
|
||||
value={payeeAgg.length}
|
||||
suffix="个"
|
||||
/>
|
||||
</Card>
|
||||
@@ -280,11 +314,10 @@ const Analysis: React.FC = () => {
|
||||
|
||||
<Card title="收款方聚合" style={{ marginTop: 24 }}>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
{[
|
||||
{ name: '李*华 (138****5678)', amount: 70000, count: 2, risk: 'high' },
|
||||
{ name: 'USDT地址 T9yD...Xk3m', amount: 86500, count: 1, risk: 'medium' },
|
||||
{ name: '财富管家-客服', amount: 30000, count: 1, risk: 'high' },
|
||||
].map((item, idx) => (
|
||||
{payeeAgg.length === 0 && (
|
||||
<Typography.Text type="secondary">暂无可聚合的收款方数据</Typography.Text>
|
||||
)}
|
||||
{payeeAgg.map((item, idx) => (
|
||||
<Card key={idx} size="small" variant="borderless" style={{ background: '#fafafa' }}>
|
||||
<Row justify="space-between" align="middle">
|
||||
<Col>
|
||||
@@ -299,8 +332,8 @@ const Analysis: React.FC = () => {
|
||||
<Typography.Text strong style={{ color: '#cf1322' }}>
|
||||
¥{item.amount.toLocaleString()}
|
||||
</Typography.Text>
|
||||
<Tag color={item.risk === 'high' ? 'red' : 'orange'}>
|
||||
{item.risk === 'high' ? '高风险' : '中风险'}
|
||||
<Tag color={item.risk === 'high' ? 'red' : item.risk === 'medium' ? 'orange' : 'default'}>
|
||||
{item.risk === 'high' ? '高风险' : item.risk === 'medium' ? '中风险' : '一般'}
|
||||
</Tag>
|
||||
</Space>
|
||||
</Col>
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
Segmented,
|
||||
Divider,
|
||||
Dropdown,
|
||||
Modal,
|
||||
} from 'antd';
|
||||
import {
|
||||
AuditOutlined,
|
||||
@@ -154,6 +155,8 @@ const Review: React.FC = () => {
|
||||
const [editableTx, setEditableTx] = useState<FraudAssessment['transaction'] | null>(null);
|
||||
const [reviewAction, setReviewAction] = useState<ReviewAction>('confirmed');
|
||||
const [reviewNote, setReviewNote] = useState('');
|
||||
const [supplementModal, setSupplementModal] = useState<FraudAssessment | null>(null);
|
||||
const [supplementNote, setSupplementNote] = useState('');
|
||||
|
||||
const { data: assessData } = useQuery({
|
||||
queryKey: ['assessments', id],
|
||||
@@ -181,6 +184,8 @@ const Review: React.FC = () => {
|
||||
message.success('复核结果已保存');
|
||||
qc.invalidateQueries({ queryKey: ['assessments', id] });
|
||||
setReviewModal(null);
|
||||
setSupplementModal(null);
|
||||
setSupplementNote('');
|
||||
},
|
||||
});
|
||||
|
||||
@@ -232,6 +237,11 @@ const Review: React.FC = () => {
|
||||
(a) => a.reviewStatus === 'confirmed',
|
||||
).length;
|
||||
|
||||
const openSupplementDialog = (assessment: FraudAssessment, initialNote?: string) => {
|
||||
setSupplementModal(assessment);
|
||||
setSupplementNote(initialNote ?? assessment.reviewNote ?? '');
|
||||
};
|
||||
|
||||
const columns: ColumnsType<FraudAssessment> = [
|
||||
{
|
||||
title: '交易时间',
|
||||
@@ -303,6 +313,8 @@ const Review: React.FC = () => {
|
||||
render: (_, r) => {
|
||||
const aiAction = getAiSuggestedAction(r, allAssessments);
|
||||
const isPending = r.reviewStatus === 'pending';
|
||||
const isSupplemented =
|
||||
r.reviewStatus === 'needs_info' && !!(r.reviewNote || '').trim();
|
||||
const status = isPending ? aiAction : r.reviewStatus;
|
||||
|
||||
const pendingStyle: Record<string, { bg: string; border: string; color: string }> = {
|
||||
@@ -322,9 +334,18 @@ const Review: React.FC = () => {
|
||||
const doneLabel: Record<string, string> = {
|
||||
confirmed: '已确认', rejected: '已排除', needs_info: '已补充',
|
||||
};
|
||||
const label = isPending ? (pendingLabel[aiAction] || '待确认') : (doneLabel[status] || status);
|
||||
|
||||
const label = isPending
|
||||
? (pendingLabel[aiAction] || '待确认')
|
||||
: (status === 'needs_info'
|
||||
? (isSupplemented ? doneLabel.needs_info : pendingLabel.needs_info)
|
||||
: (doneLabel[status] || status));
|
||||
|
||||
const submitReviewAction = (action: ReviewAction) => {
|
||||
if (action === 'needs_info') {
|
||||
openSupplementDialog(r);
|
||||
return;
|
||||
}
|
||||
reviewMutation.mutate({
|
||||
assessmentId: r.id,
|
||||
body: {
|
||||
@@ -343,11 +364,18 @@ const Review: React.FC = () => {
|
||||
<span style={{
|
||||
color: v === 'confirmed' ? '#389e0d' : v === 'rejected' ? '#cf1322' : '#1677ff',
|
||||
}}>
|
||||
{v === 'confirmed' ? '确认' : v === 'rejected' ? '排除' : '需补充'}
|
||||
{v === 'confirmed' ? '确认' : v === 'rejected' ? '排除' : '补充信息'}
|
||||
</span>
|
||||
),
|
||||
}));
|
||||
|
||||
if (!isPending && r.reviewStatus === 'needs_info') {
|
||||
otherOptions.push({
|
||||
key: 'needs_info',
|
||||
label: <span style={{ color: '#1677ff' }}>更新补充</span>,
|
||||
});
|
||||
}
|
||||
|
||||
if (isPending) {
|
||||
const ps = pendingStyle[aiAction] || pendingStyle.confirmed;
|
||||
return (
|
||||
@@ -445,7 +473,7 @@ const Review: React.FC = () => {
|
||||
? getAiSuggestedAction(r, allAssessments)
|
||||
: (r.reviewStatus as ReviewAction),
|
||||
);
|
||||
setReviewNote('');
|
||||
setReviewNote(r.reviewNote || '');
|
||||
}}
|
||||
>
|
||||
详情
|
||||
@@ -807,7 +835,7 @@ const Review: React.FC = () => {
|
||||
options={[
|
||||
{ label: '确认 - 该笔计入被骗金额', value: 'confirmed' },
|
||||
{ label: '排除 - 该笔不计入被骗金额', value: 'rejected' },
|
||||
{ label: '需补充 - 需进一步调查确认', value: 'needs_info' },
|
||||
{ label: '待补充 - 需录入补充说明', value: 'needs_info' },
|
||||
]}
|
||||
/>
|
||||
<Typography.Text strong>备注说明:</Typography.Text>
|
||||
@@ -815,7 +843,7 @@ const Review: React.FC = () => {
|
||||
rows={3}
|
||||
value={reviewNote}
|
||||
onChange={(e) => setReviewNote(e.target.value)}
|
||||
placeholder="请输入复核意见或备注..."
|
||||
placeholder={reviewAction === 'needs_info' ? '请填写已补充的证据/线索说明(必填)' : '请输入复核意见或备注...'}
|
||||
/>
|
||||
</Space>
|
||||
|
||||
@@ -845,6 +873,10 @@ const Review: React.FC = () => {
|
||||
type="primary"
|
||||
loading={reviewMutation.isPending}
|
||||
onClick={() => {
|
||||
if (reviewAction === 'needs_info') {
|
||||
openSupplementDialog(reviewModal, reviewNote);
|
||||
return;
|
||||
}
|
||||
reviewMutation.mutate({
|
||||
assessmentId: reviewModal.id,
|
||||
body: {
|
||||
@@ -861,6 +893,44 @@ const Review: React.FC = () => {
|
||||
</>
|
||||
)}
|
||||
</Drawer>
|
||||
|
||||
<Modal
|
||||
title="补充信息"
|
||||
open={!!supplementModal}
|
||||
zIndex={1400}
|
||||
onCancel={() => {
|
||||
setSupplementModal(null);
|
||||
setSupplementNote('');
|
||||
}}
|
||||
onOk={() => {
|
||||
if (!supplementModal) return;
|
||||
if (!supplementNote.trim()) {
|
||||
message.warning('请填写补充说明后再提交');
|
||||
return;
|
||||
}
|
||||
reviewMutation.mutate({
|
||||
assessmentId: supplementModal.id,
|
||||
body: {
|
||||
review_status: 'needs_info',
|
||||
review_note: supplementNote.trim(),
|
||||
reviewed_by: 'demo_user',
|
||||
},
|
||||
});
|
||||
}}
|
||||
confirmLoading={reviewMutation.isPending}
|
||||
okText="提交补充"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Typography.Paragraph type="secondary" style={{ marginBottom: 8 }}>
|
||||
该操作将状态更新为“已补充”,并记录补充说明以便后续复核。
|
||||
</Typography.Paragraph>
|
||||
<Input.TextArea
|
||||
rows={4}
|
||||
value={supplementNote}
|
||||
onChange={(e) => setSupplementNote(e.target.value)}
|
||||
placeholder="请输入补充的证据、线索、核验结果等内容..."
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user