"use client"; import { useState, useMemo } from "react"; import { useSession } from "next-auth/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, FileDown, FileText, Loader2 } 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 { reportsApi } from "@/lib/api/reports"; 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 { data: session } = useSession(); const token = (session as { accessToken?: string })?.accessToken; const [selectedQuery, setSelectedQuery] = useState("all"); const [selectedPlatform, setSelectedPlatform] = useState("all"); const [startDate, setStartDate] = useState(""); const [endDate, setEndDate] = useState(""); const [filterKey, setFilterKey] = useState(0); const [exporting, setExporting] = useState(false); 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); } async function handleExport(format: "csv" | "pdf") { if (!token) return; const queryId = selectedQuery !== "all" ? selectedQuery : undefined; try { setExporting(true); let blob: Blob; let filename: string; if (format === "csv") { blob = await reportsApi.exportCSV(token, queryId) as unknown as Blob; filename = `citations_${new Date().toISOString().split("T")[0]}.csv`; } else { blob = await reportsApi.exportPDF(token, queryId) as unknown as Blob; filename = `citations_${new Date().toISOString().split("T")[0]}.pdf`; } const url = window.URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); window.URL.revokeObjectURL(url); } catch (err) { console.error("导出失败:", err); } finally { setExporting(false); } } 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")}
))}
)}
); }