185 lines
7.9 KiB
TypeScript
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>
|
|
)
|
|
}
|