Files
lingxi-web/src/pages/risks/RisksPage.tsx
T

144 lines
7.2 KiB
TypeScript

import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { toast, errorMessage } from '@/app/store/toast'
import { PageLoading, ErrorState } from '@/components/ui/feedback'
import { PageHeader } from '@/components/common/PageHeader'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Card, CardHeader } from '@/components/ui/card'
import { StatCard } from '@/components/common/StatCard'
import { Select } from '@/components/ui/select'
import { Drawer } from '@/components/ui/drawer'
import { Textarea } from '@/components/ui/input'
import { PermissionGate } from '@/components/common/PermissionGate'
import { CountryFlag } from '@/components/common/CountryFlag'
import { getRisks, updateRisk } from '@/services/api/crm'
import { riskLevelMeta, riskTypeLabel, RISK_TYPES, RISK_LEVELS } from '@/config/constants'
import { formatRelative } from '@/utils/format'
import { cn } from '@/utils/cn'
import { AlertTriangle, Settings } from 'lucide-react'
export function RisksPage() {
const queryClient = useQueryClient()
const navigate = useNavigate()
const [level, setLevel] = useState('')
const [type, setType] = useState('')
const [status, setStatus] = useState('')
const [detailId, setDetailId] = useState<string | null>(null)
const [noteDraft, setNoteDraft] = useState('')
const { data, isLoading, isError, refetch } = useQuery({
queryKey: ['risks', level, type, status],
queryFn: () => getRisks({ level, type, status }),
})
const updateMut = useMutation({
mutationFn: ({ id, data }: { id: string; data: any }) => updateRisk(id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['risks'] })
toast.success('风险状态已更新')
},
onError: (e) => toast.error(errorMessage(e)),
})
const list = data?.list ?? []
const openCount = list.filter((r) => r.status === 'open').length
const confirmedCount = list.filter((r) => r.status === 'confirmed').length
const urgentCount = list.filter((r) => r.level === 'urgent').length
const detail = list.find((r) => r.id === detailId)
if (isLoading) return <PageLoading />
if (isError) return <ErrorState onRetry={() => refetch()} />
return (
<div className="p-4 md:p-6">
<PageHeader
title="风险预警中心"
description="查看交易风险、客户流失信号和敏感表达"
actions={
<PermissionGate permission="settings:manage">
<Button size="sm" variant="outline" onClick={() => navigate('/settings')}><Settings className="h-4 w-4" />风险识别设置</Button>
</PermissionGate>
}
/>
<div className="grid grid-cols-3 gap-3 mb-4">
<StatCard title="待处理" value={openCount} icon={AlertTriangle} onClick={() => setStatus('open')} />
<StatCard title="已确认" value={confirmedCount} icon={AlertTriangle} onClick={() => setStatus('confirmed')} />
<StatCard title="紧急" value={urgentCount} icon={AlertTriangle} onClick={() => setLevel('urgent')} />
</div>
<div className="mb-3 flex flex-wrap gap-2">
<Select placeholder="风险等级" value={level} onChange={(e) => setLevel(e.target.value)} options={RISK_LEVELS.map((l) => ({ value: l.value, label: l.label }))} className="w-28" />
<Select placeholder="风险类型" value={type} onChange={(e) => setType(e.target.value)} options={RISK_TYPES.map((t) => ({ value: t.value, label: t.label }))} className="w-36" />
<Select placeholder="处理状态" value={status} onChange={(e) => setStatus(e.target.value)} options={[{value:'open',label:'未处理'},{value:'confirmed',label:'已确认'},{value:'resolved',label:'已解决'},{value:'ignored',label:'已忽略'}]} className="w-28" />
</div>
<div className="space-y-2">
{list.length === 0 && <Card className="p-8 text-center text-sm text-muted-foreground">暂无匹配的风险预警</Card>}
{list.map((r) => {
const meta = riskLevelMeta(r.level)
return (
<Card key={r.id} className="cursor-pointer p-4 hover:bg-muted/40 transition-colors" onClick={() => {
setDetailId(r.id)
setNoteDraft(r.note || '')
}}>
<div className="flex items-start justify-between gap-3">
<div className="flex items-start gap-2 min-w-0">
<span className={cn('mt-0.5 h-2 w-2 shrink-0 rounded-full', meta.dot)} />
<div>
<div className="flex flex-wrap items-center gap-2">
<Badge className={meta.color}>{meta.label}风险</Badge>
<Badge variant="outline">{riskTypeLabel(r.type)}</Badge>
<span className="text-sm font-medium truncate">{r.reason}</span>
</div>
<p className="mt-1 text-xs text-muted-foreground">{r.customerName} · {formatRelative(r.createdAt)} · {r.status === 'open' ? '待处理' : r.status === 'confirmed' ? '已确认' : r.status === 'resolved' ? '已解决' : '已忽略'}</p>
</div>
</div>
<div className="flex gap-1 shrink-0" onClick={(e) => e.stopPropagation()}>
{r.status === 'open' && <Button size="sm" variant="outline" onClick={() => updateMut.mutate({ id: r.id, data: { status: 'confirmed' } })}>确认</Button>}
<Button size="sm" variant="ghost" onClick={() => updateMut.mutate({ id: r.id, data: { status: 'resolved' } })}>已处理</Button>
</div>
</div>
</Card>
)
})}
</div>
{/* 风险详情抽屉 */}
<Drawer open={!!detailId} onOpenChange={() => setDetailId(null)} title={detail?.reason ?? '风险详情'} width="w-[480px]">
{detail && (
<div className="space-y-4 p-4">
<div className="flex items-center gap-2">
<Badge variant="danger">{riskLevelMeta(detail.level).label}</Badge>
<Badge variant="outline">{riskTypeLabel(detail.type)}</Badge>
<span className="text-xs text-muted-foreground">{formatRelative(detail.createdAt)}</span>
</div>
<div>
<h4 className="text-sm font-semibold mb-1">判断依据</h4>
<p className="text-sm text-muted-foreground leading-relaxed">{detail.aiExplanation}</p>
</div>
<div>
<h4 className="text-sm font-semibold mb-1">推荐处理方案</h4>
<p className="text-sm text-muted-foreground">{detail.suggestion}</p>
</div>
<div>
<h4 className="text-sm font-semibold mb-1">添加备注</h4>
<Textarea rows={3} placeholder="添加处理备注…" value={noteDraft} onChange={(event) => setNoteDraft(event.target.value)} />
<Button
size="sm"
className="mt-2"
disabled={!noteDraft.trim()}
onClick={() => updateMut.mutate({ id: detail.id, data: { note: noteDraft.trim() } })}
>
保存备注
</Button>
</div>
</div>
)}
</Drawer>
</div>
)
}