"use client"; import { useState, useMemo } from "react"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Button } from "@/components/ui/button"; import { PLATFORM_MAP } from "@/lib/platforms"; import { Check, X, Quote, Filter, TrendingUp, MapPin, Hash } from "lucide-react"; import { useApi } from "@/lib/hooks/use-api"; import { LoadingState } from "@/components/ui/api-states"; import { type CitationStats } from "@/lib/api/citations"; import { PieChart, Pie, Cell, Tooltip as RechartsTooltip, ResponsiveContainer, LineChart, Line, XAxis, YAxis, CartesianGrid, } from "recharts"; interface CitationItem { id: string; query_id: string; platform: string; cited: boolean; citation_position: number | null; citation_text: string | null; competitor_brands: string[]; queried_at: string; } interface QueryOption { id: string; keyword: string; } const PIE_FALLBACK_COLORS = [ "#3b82f6", "#10b981", "#f59e0b", "#ef4444", "#8b5cf6", ]; function getChartColor(index: number): string { if (typeof document !== "undefined") { const style = getComputedStyle(document.documentElement); const cssVar = style.getPropertyValue(`--chart-${index + 1}`).trim(); if (cssVar) return `hsl(var(--chart-${index + 1}))`; } return PIE_FALLBACK_COLORS[index % PIE_FALLBACK_COLORS.length]; } function StatCard({ title, value, icon: Icon, suffix, }: { title: string; value: string | number; icon: React.ElementType; suffix?: string; }) { return ( {title}
{value} {suffix && {suffix}}
); } function PlatformPieChart({ data }: { data: { platform: string; count: number }[] }) { const chartData = useMemo( () => data.map((d) => ({ name: PLATFORM_MAP[d.platform] || d.platform, value: d.count, })), [data] ); return ( {chartData.map((_, index) => ( ))} [value, "引用数"]} /> ); } function TrendLineChart({ data }: { data: { date: string; count: number }[] }) { 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 CitationsPage() { const [selectedQuery, setSelectedQuery] = useState("all"); const [selectedPlatform, setSelectedPlatform] = useState("all"); const [startDate, setStartDate] = useState(""); const [endDate, setEndDate] = useState(""); const [filterKey, setFilterKey] = useState(0); const citationsUrl = (() => { const params = new URLSearchParams(); if (selectedQuery && selectedQuery !== "all") params.append("query_id", selectedQuery); if (selectedPlatform && selectedPlatform !== "all") params.append("platform", selectedPlatform); if (startDate) params.append("start_date", startDate); if (endDate) params.append("end_date", endDate); const qs = params.toString(); return `/api/v1/citations/${qs ? `?${qs}&_k=${filterKey}` : `?_k=${filterKey}`}`; })(); const statsUrl = useMemo(() => { const params = new URLSearchParams(); if (selectedQuery && selectedQuery !== "all") params.append("query_id", selectedQuery); if (startDate) params.append("start_date", startDate); if (endDate) params.append("end_date", endDate); const qs = params.toString(); return `/api/v1/citations/stats${qs ? `?${qs}&_k=${filterKey}` : `?_k=${filterKey}`}`; }, [selectedQuery, startDate, endDate, filterKey]); const { data: citationsData, isLoading, error: citationsError, refresh: refreshCitations, } = useApi<{ items: CitationItem[] }>( citationsUrl, { dedupingInterval: 0 } ); const { data: queriesData, } = useApi<{ items: QueryOption[] }>("/api/v1/queries/"); const { data: statsData, isLoading: statsLoading, error: statsError, } = useApi(statsUrl, { dedupingInterval: 0 }); const citations: CitationItem[] = citationsData?.items ?? []; const queries: QueryOption[] = queriesData?.items ?? []; function handleFilter() { setFilterKey((k) => k + 1); } function handleReset() { setSelectedQuery("all"); setSelectedPlatform("all"); setStartDate(""); setEndDate(""); setFilterKey((k) => k + 1); } if (isLoading && citations.length === 0) { return (

引用记录

查看各平台的引用检测结果

); } return (

引用记录

查看各平台的引用检测结果

{!statsError && (
{statsLoading ? ( ) : statsData ? ( <>
平台分布 {statsData.platform_distribution?.length > 0 ? ( ) : (
暂无平台分布数据
)}
30天趋势 {statsData.trend?.length > 0 ? ( ) : (
暂无趋势数据
)}
) : null}
)} 筛选条件
setStartDate(e.target.value)} />
setEndDate(e.target.value)} />
{citationsError && (
{citationsError.message}
)} 引用记录列表 {citations.length === 0 ? (

暂无引用记录

添加查询词并执行查询后将在此显示结果

) : (
平台 是否引用 引用位置 引用文本片段 竞争品牌 查询时间 {citations.map((item) => ( {PLATFORM_MAP[item.platform] || item.platform} {item.cited ? (
已引用
) : (
未引用
)}
{item.citation_position !== null ? `第 ${item.citation_position} 位` : "—"} {item.citation_text || "—"}
{item.competitor_brands?.length > 0 ? ( item.competitor_brands.map((brand) => ( {brand} )) ) : ( )}
{new Date(item.queried_at).toLocaleString("zh-CN")}
))}
)}
); }