From a3577401b47a656963dc9a3ca68d59917c4eae77 Mon Sep 17 00:00:00 2001 From: linkeyu <798648091@qq.com> Date: Sat, 25 Jul 2026 15:59:30 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E8=A1=A5=E9=BD=90?= =?UTF-8?q?=E9=A3=8E=E6=8E=A7=E8=A7=A3=E9=99=A4=E5=B0=81=E7=A6=81=E6=93=8D?= =?UTF-8?q?=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(main)/risk-monitor/page.module.css | 33 ++++ src/app/(main)/risk-monitor/page.tsx | 172 +++++++++++++++++--- src/lib/types.ts | 1 + 3 files changed, 182 insertions(+), 24 deletions(-) diff --git a/src/app/(main)/risk-monitor/page.module.css b/src/app/(main)/risk-monitor/page.module.css index 444487a..bc55438 100644 --- a/src/app/(main)/risk-monitor/page.module.css +++ b/src/app/(main)/risk-monitor/page.module.css @@ -87,6 +87,34 @@ margin-bottom: 16px; } +.statusToolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + margin-bottom: 2px; + padding: 12px 16px; + border: 1px solid #ecece8; + border-radius: 12px; + background: #fff; +} + +.statusToolbar > div { + display: flex; + flex-direction: column; + gap: 2px; +} + +.statusToolbar strong { + color: #222; + font-size: 14px; +} + +.statusToolbar span { + color: #888; + font-size: 12px; +} + .summaryCard { min-height: 148px; padding: 26px 24px 20px; @@ -320,4 +348,9 @@ .updatedMeta { display: none; } + + .statusToolbar { + align-items: flex-start; + flex-direction: column; + } } diff --git a/src/app/(main)/risk-monitor/page.tsx b/src/app/(main)/risk-monitor/page.tsx index 1d8ac96..3d46201 100644 --- a/src/app/(main)/risk-monitor/page.tsx +++ b/src/app/(main)/risk-monitor/page.tsx @@ -7,6 +7,7 @@ import { Empty, InputNumber, Modal, + Segmented, Skeleton, Table, Tag, @@ -64,6 +65,7 @@ const KIND_META: Record< const integer = (value: number) => new Intl.NumberFormat('zh-CN').format(value); const LIST_PAGE_SIZE = 20; const RISK_KINDS: RiskKind[] = ['sms', 'oneclick', 'compare']; +type IncidentStatus = 'open' | 'blocked'; const RULE_LIMITS: Record = { sms: { min: 1, @@ -277,6 +279,7 @@ export default function RiskMonitorPage() { oneclick: 1, compare: 1, }); + const [incidentStatus, setIncidentStatus] = useState('open'); const listPageRef = useRef(listPage); const [loading, setLoading] = useState(true); const [expanded, setExpanded] = useState>(new Set()); @@ -300,13 +303,25 @@ export default function RiskMonitorPage() { const [summaryRes, smsRes, oneclickRes, compareRes] = await Promise.all([ api.get('/admin/api/risk-monitor/summary'), api.get('/admin/api/risk-monitor/incidents/sms', { - params: { cursor: (pages.sms - 1) * LIST_PAGE_SIZE, limit: LIST_PAGE_SIZE }, + params: { + status: incidentStatus, + cursor: (pages.sms - 1) * LIST_PAGE_SIZE, + limit: LIST_PAGE_SIZE, + }, }), api.get('/admin/api/risk-monitor/incidents/oneclick', { - params: { cursor: (pages.oneclick - 1) * LIST_PAGE_SIZE, limit: LIST_PAGE_SIZE }, + params: { + status: incidentStatus, + cursor: (pages.oneclick - 1) * LIST_PAGE_SIZE, + limit: LIST_PAGE_SIZE, + }, }), api.get('/admin/api/risk-monitor/incidents/compare', { - params: { cursor: (pages.compare - 1) * LIST_PAGE_SIZE, limit: LIST_PAGE_SIZE }, + params: { + status: incidentStatus, + cursor: (pages.compare - 1) * LIST_PAGE_SIZE, + limit: LIST_PAGE_SIZE, + }, }), ]); setSummary(summaryRes.data); @@ -320,7 +335,7 @@ export default function RiskMonitorPage() { } finally { setLoading(false); } - }, [message]); + }, [incidentStatus, message]); const changeListPage = useCallback( async (kind: RiskKind, page: number) => { @@ -331,7 +346,11 @@ export default function RiskMonitorPage() { const { data } = await api.get( `/admin/api/risk-monitor/incidents/${kind}`, { - params: { cursor: (page - 1) * LIST_PAGE_SIZE, limit: LIST_PAGE_SIZE }, + params: { + status: incidentStatus, + cursor: (page - 1) * LIST_PAGE_SIZE, + limit: LIST_PAGE_SIZE, + }, }, ); setLists((current) => ({ ...current, [kind]: data })); @@ -339,9 +358,21 @@ export default function RiskMonitorPage() { void message.error(errMsg(error, '风险列表加载失败')); } }, - [message], + [incidentStatus, message], ); + const changeIncidentStatus = useCallback((status: IncidentStatus) => { + const firstPage = { sms: 1, oneclick: 1, compare: 1 }; + listPageRef.current = firstPage; + setListPage(firstPage); + setLists({}); + setExpanded(new Set()); + setDetails({}); + setDetailLoading(new Set()); + setDetailPage({}); + setIncidentStatus(status); + }, []); + useEffect(() => { void loadAll(); const timer = window.setInterval(() => void loadAll(true), 60 * 60 * 1000); @@ -516,6 +547,56 @@ export default function RiskMonitorPage() { [lists, loadAll, message, modal], ); + const revokeRestriction = useCallback( + (row: RiskIncidentItem) => { + if (!row.restriction_id) { + void message.error('未找到有效的封禁记录,请刷新后重试'); + return; + } + modal.confirm({ + title: `确认解除该${row.subject_type === 'device' ? '设备' : '账户'}的封禁?`, + content: + row.subject_type === 'device' + ? '解除后,该设备可以重新发送短信验证码和使用一键登录。' + : '解除后,该账户可以重新发起比价、领取任务奖励和提现。', + okText: '解除封禁', + cancelText: '取消', + async onOk() { + setActing(row.incident_id); + try { + await api.post( + `/admin/api/risk-monitor/restrictions/${row.restriction_id}/revoke`, + ); + void message.success('解除封禁成功'); + setExpanded((current) => { + const next = new Set(current); + next.delete(row.incident_id); + return next; + }); + if ( + (lists[row.kind]?.items.length ?? 0) === 1 + && listPageRef.current[row.kind] > 1 + ) { + const nextPages = { + ...listPageRef.current, + [row.kind]: listPageRef.current[row.kind] - 1, + }; + listPageRef.current = nextPages; + setListPage(nextPages); + } + await loadAll(true); + } catch (error) { + void message.error(errMsg(error, '解除封禁失败')); + throw error; + } finally { + setActing(null); + } + }, + }); + }, + [lists, loadAll, message, modal], + ); + const columns = useCallback( (kind: RiskKind): ColumnsType => { const actionColumn = { @@ -537,21 +618,35 @@ export default function RiskMonitorPage() { > {open ? '收起' : '展开'} - - + {row.status === 'open' && ( + <> + + + + )} + {row.status === 'blocked' && ( + + )} ); }, @@ -605,7 +700,7 @@ export default function RiskMonitorPage() { actionColumn, ]; }, - [acting, copy, detailLoading, expanded, runAction, toggle], + [acting, copy, detailLoading, expanded, revokeRestriction, runAction, toggle], ); const cards = useMemo(() => { @@ -688,6 +783,25 @@ export default function RiskMonitorPage() { })} +
+
+ 风险处置 + + {incidentStatus === 'open' + ? '查看待处理报警,可执行封禁或忽略' + : '查看当前已封禁主体,可恢复其业务权限'} + +
+ changeIncidentStatus(value as IncidentStatus)} + /> +
+ {RISK_KINDS.map((kind) => { const data = lists[kind]?.items ?? []; return ( @@ -700,7 +814,17 @@ export default function RiskMonitorPage() { dataSource={data} tableLayout="fixed" scroll={{ x: kind === 'compare' ? 900 : 890 }} - locale={{ emptyText: }} + locale={{ + emptyText: ( + + ), + }} pagination={{ current: listPage[kind], pageSize: LIST_PAGE_SIZE, @@ -728,7 +852,7 @@ export default function RiskMonitorPage() { )} diff --git a/src/lib/types.ts b/src/lib/types.ts index 28058ed..26d8d95 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -268,6 +268,7 @@ export interface RiskIncidentItem { event_count: number; status: 'open' | 'ignored' | 'blocked' | 'resolved'; restricted: boolean; + restriction_id: number | null; } export interface RiskIncidentPage {