Files
CIBank/src/sections/Dashboard.tsx
T
2026-07-20 19:49:27 +08:00

185 lines
7.9 KiB
TypeScript

import { useEffect, useState } from 'react'
import { ArrowUpRight, BookOpen, Flame, Newspaper, Siren, Store, Tags, Loader2 } from 'lucide-react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { api, type DashboardData, type OverviewStats } from '@/lib/api'
export interface PageKeyHintType {
onNavigate?: (page: 'intel' | 'qa' | 'analysis' | 'reports') => void
}
const statCards = (ovStats: OverviewStats) => [
{ label: '知识条目', value: ovStats.knowledgeItems.toLocaleString(), icon: BookOpen, sub: '持续结构化沉淀', color: 'text-orange-500 bg-orange-50' },
{ label: '今日新增情报', value: `+${ovStats.todayNew}`, icon: Newspaper, sub: `来自 ${ovStats.sourcesMp + ovStats.sourcesVideo} 个监控信源`, color: 'text-blue-500 bg-blue-50' },
{ label: '追踪品牌', value: ovStats.brandsTracked, icon: Store, sub: '覆盖 12 个赛道', color: 'text-emerald-500 bg-emerald-50' },
{ label: '已生成报告', value: ovStats.reportsTotal, icon: Tags, sub: '周报 / 专题报告', color: 'text-violet-500 bg-violet-50' },
]
const typeColor: Record<string, string> = {
: 'bg-red-50 text-red-600 border-red-200',
: 'bg-blue-50 text-blue-600 border-blue-200',
: 'bg-violet-50 text-violet-600 border-violet-200',
: 'bg-emerald-50 text-emerald-600 border-emerald-200',
: 'bg-amber-50 text-amber-600 border-amber-200',
: 'bg-cyan-50 text-cyan-600 border-cyan-200',
}
export default function Dashboard({ onNavigate }: PageKeyHintType) {
const [data, setData] = useState<DashboardData | null>(null)
const [stats, setStats] = useState<OverviewStats | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
Promise.allSettled([api.getDashboard(), api.getStats()]).then(([dRes, sRes]) => {
if (dRes.status === 'fulfilled') setData(dRes.value)
if (sRes.status === 'fulfilled') setStats(sRes.value)
setLoading(false)
})
}, [])
const topIntel = data?.topIntel ?? []
const policyAlerts = data?.policyAlerts?.map((p) => ({ ...p, type: '政策监管', sourceType: '公众号', category: '综合', keyPoints: [], entities: [], timeliness: '高', score: 0 })) ?? []
const hotKw = data?.hotKeywords ?? []
const ovStats: OverviewStats = {
sourcesMp: stats?.sourcesMp ?? 0,
sourcesVideo: stats?.sourcesVideo ?? 0,
todayNew: stats?.todayNew ?? 0,
knowledgeItems: stats?.knowledgeItems ?? 0,
brandsTracked: stats?.brandsTracked ?? 0,
reportsTotal: stats?.reportsTotal ?? 0,
}
if (loading) {
return (
<div className="flex h-full items-center justify-center">
<Loader2 className="h-6 w-6 animate-spin text-orange-500" />
</div>
)
}
return (
<div className="space-y-6">
<div>
<h1 className="text-xl font-bold text-slate-900"></h1>
<p className="mt-1 text-sm text-slate-500">
</p>
</div>
{/* 统计卡片 */}
<div className="grid grid-cols-2 gap-4 xl:grid-cols-4">
{(statCards(ovStats)).map((s) => {
const Icon = s.icon
return (
<Card key={s.label} className="border-slate-200">
<CardContent className="flex items-start justify-between p-5">
<div>
<div className="text-sm text-slate-500">{s.label}</div>
<div className="mt-1 text-2xl font-bold text-slate-900">{s.value}</div>
<div className="mt-1 text-xs text-slate-400">{s.sub}</div>
</div>
<div className={`rounded-lg p-2 ${s.color}`}>
<Icon className="h-5 w-5" />
</div>
</CardContent>
</Card>
)
})}
</div>
<div className="grid gap-4 xl:grid-cols-3">
{/* 高价值情报 TOP5 */}
<Card className="xl:col-span-2 border-slate-200">
<CardHeader className="flex-row items-center justify-between pb-3">
<CardTitle className="flex items-center gap-2 text-base">
<Flame className="h-4 w-4 text-orange-500" />
TOP 5
</CardTitle>
<button
onClick={() => onNavigate?.('intel')}
className="flex items-center gap-1 text-xs text-orange-500 hover:text-orange-600"
>
<ArrowUpRight className="h-3 w-3" />
</button>
</CardHeader>
<CardContent className="space-y-3">
{topIntel.length === 0 && (
<div className="py-8 text-center text-sm text-slate-400"></div>
)}
{topIntel.map((item, idx) => (
<div
key={item.id}
className="flex items-start gap-3 rounded-lg border border-slate-100 p-3 transition-colors hover:border-orange-200 hover:bg-orange-50/40"
>
<div className="flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-slate-900 text-xs font-bold text-white">
{idx + 1}
</div>
<div className="min-w-0 flex-1">
<div className="line-clamp-1 text-sm font-medium text-slate-800">{item.title}</div>
<div className="mt-1 flex flex-wrap items-center gap-2 text-xs text-slate-400">
<span>{item.source}</span>
<span>·</span>
<span>{item.date}</span>
<Badge variant="outline" className={`text-[10px] ${typeColor[item.type]}`}>{item.type}</Badge>
</div>
</div>
<div className="shrink-0 text-right">
<div className="text-sm font-bold text-orange-500">{item.score}</div>
<div className="text-[10px] text-slate-400"></div>
</div>
</div>
))}
</CardContent>
</Card>
<div className="space-y-4">
{/* 政策预警 */}
<Card className="border-red-100">
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-base">
<Siren className="h-4 w-4 text-red-500" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{policyAlerts.length === 0 && (
<div className="py-4 text-center text-xs text-slate-400"></div>
)}
{policyAlerts.map((p) => (
<div key={p.id} className="rounded-lg bg-red-50/60 p-3">
<div className="line-clamp-2 text-xs font-medium leading-5 text-slate-700">{p.title}</div>
<div className="mt-1.5 text-[11px] text-slate-400">{p.date} · {p.source}</div>
</div>
))}
</CardContent>
</Card>
{/* 热词榜 */}
<Card className="border-slate-200">
<CardHeader className="pb-3">
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent>
{hotKw.length === 0 ? (
<div className="py-4 text-center text-xs text-slate-400"></div>
) : (
<div className="flex flex-wrap gap-2">
{hotKw.map((k) => (
<span
key={k.word}
className="inline-flex items-center gap-1 rounded-full bg-slate-100 px-3 py-1 text-xs text-slate-700"
>
{k.word}
<span className="text-[10px] font-medium text-emerald-500">+{k.change}%</span>
</span>
))}
</div>
)}
</CardContent>
</Card>
</div>
</div>
</div>
)
}