fix: mock

This commit is contained in:
2026-03-13 23:29:55 +08:00
parent b7e973e2b6
commit c72fbc9a14
7 changed files with 165 additions and 21 deletions

View File

@@ -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>

View File

@@ -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>
);
};