"use client"; import * as React from "react"; import { useSession } from "next-auth/react"; import { useApi } from "@/lib/hooks/use-api"; import { scoringApi, BrandScore, BrandCompare, ScoreHistory } from "@/lib/api/scoring"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; import { LoadingState, ErrorState, EmptyState } from "@/components/ui/api-states"; import { CompetitorRadarChart } from "@/components/charts/CompetitorRadarChart"; import { round, getStatusColor, getProgressBg } from "@/lib/utils/health-score"; import { PieChart, Pie, Cell, ResponsiveContainer, LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, } from "recharts"; import { Heart } from "lucide-react"; interface BrandsResponse { items: { id: string; name: string }[]; } const DIMENSION_LABELS: Record = { mention_rate: "提及率", recommendation_rank: "推荐排名", sentiment: "情感倾向", citation_quality: "引用质量", competitor_comparison: "竞品对比", }; function ScoreGauge({ score }: { score: number }) { const percentage = Math.min(Math.max(score, 0), 100); const colorClass = getStatusColor(percentage); const colorMap: Record = { "text-green-500": "#22c55e", "text-yellow-500": "#eab308", "text-red-500": "#ef4444", }; const fillColor = colorMap[colorClass] || "#3b82f6"; const data = [ { name: "score", value: percentage }, { name: "remaining", value: 100 - percentage }, ]; return (
{round(percentage, 1)} /100
); } function DimensionCards({ dimensions }: { dimensions: BrandScore["dimensions"] }) { return (
{dimensions.map((dim) => { const percentage = dim.max_score > 0 ? (dim.score / dim.max_score) * 100 : 0; const label = DIMENSION_LABELS[dim.name] || dim.name; return (
{label} {round(percentage, 1)}%

{dim.description}

); })}
); } function CompetitorTab({ token, brandId }: { token: string; brandId: string }) { const [data, setData] = React.useState(null); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); React.useEffect(() => { let cancelled = false; setLoading(true); setError(null); scoringApi .getCompare(token, brandId) .then((res) => { if (!cancelled) setData(res); }) .catch((err) => { if (!cancelled) setError(err instanceof Error ? err.message : "加载失败"); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [token, brandId]); if (loading) return ; if (error) return ; if (!data) return ; const radarData = data.dimensions.map((dim) => { const item: Record = { label: DIMENSION_LABELS[dim] || dim, dimension: dim, }; item[data.brand_name] = 0; data.competitors.forEach((c) => { item[c.name] = c.scores[dim] ?? 0; }); return item; }); const brandScoreEntry = data.dimensions.reduce>( (acc, dim) => { acc[dim] = 0; return acc; }, {} ); radarData.forEach((item) => { const dim = item.dimension as string; brandScoreEntry[dim] = (item[data.brand_name] as number) || 0; }); radarData.forEach((item) => { const dim = item.dimension as string; item[data.brand_name] = brandScoreEntry[dim]; }); const competitors = data.competitors.map((c, i) => ({ name: c.name, color: [ "hsl(346.8 77.2% 49.8%)", "hsl(24.6 95% 53.1%)", "hsl(142.1 76.2% 36.3%)", "hsl(262.1 83.3% 57.8%)", ][i % 4], })); return ( 竞品对比雷达图 ); } function HistoryTab({ token, brandId }: { token: string; brandId: string }) { const [data, setData] = React.useState(null); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); React.useEffect(() => { let cancelled = false; setLoading(true); setError(null); scoringApi .getHistory(token, brandId) .then((res) => { if (!cancelled) setData(res); }) .catch((err) => { if (!cancelled) setError(err instanceof Error ? err.message : "加载失败"); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [token, brandId]); if (loading) return ; if (error) return ; if (!data || data.scores.length === 0) return ; const chartData = data.scores.map((entry) => ({ date: entry.date, score: entry.overall_score, })); return ( 历史趋势 { const date = new Date(value); return `${date.getMonth() + 1}/${date.getDate()}`; }} /> { const date = new Date(value); return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`; }} formatter={(value: number) => [`评分: ${value}`, ""]} /> ); } export default function HealthScorePage() { const { data: session } = useSession(); const token = session?.accessToken || ""; const { data: brandsData } = useApi("/api/v1/brands/?limit=1"); const brandId = brandsData?.items?.[0]?.id ?? null; const [scoreData, setScoreData] = React.useState(null); const [scoreLoading, setScoreLoading] = React.useState(true); const [scoreError, setScoreError] = React.useState(null); React.useEffect(() => { if (!token || !brandId) return; let cancelled = false; setScoreLoading(true); setScoreError(null); scoringApi .getScore(token, brandId) .then((res) => { if (!cancelled) setScoreData(res); }) .catch((err) => { if (!cancelled) setScoreError(err instanceof Error ? err.message : "加载失败"); }) .finally(() => { if (!cancelled) setScoreLoading(false); }); return () => { cancelled = true; }; }, [token, brandId]); return (

健康评分

品牌在AI搜索中的综合健康表现

{!token || !brandId ? ( ) : scoreLoading ? ( ) : scoreError ? ( ) : !scoreData ? ( } message="暂无健康评分数据" description="请先完成品牌评分检测" /> ) : ( <>

综合健康评分

竞品对比 历史趋势 )}
); }