'use client'; import { useEffect, useState, type ReactNode } from 'react'; import Link from 'next/link'; import Sidebar from '@/components/Sidebar'; import { Calendar, ExternalLink, Newspaper, RefreshCw, Wrench } from 'lucide-react'; type LinkInsight = { kind: 'article' | 'tool'; url: string; canonical_url: string; title: string; domain: string; count: number; group_count: number; first_seen: string; last_seen: string; sources: Array<{ chatroom_id: string; chat_name: string; sender: string; time: string; local_id: number; snippet: string; source: string; }>; }; type LinkInsightResp = { ok: boolean; date: string; articles: LinkInsight[]; tools: LinkInsight[]; }; type RawWechatLink = { chatroom_id: string; chat_name: string; local_id: number; sender: string; time: string; url: string; canonical_url: string; title: string | null; domain: string; source: string; raw_kind: string; }; function localToday(): string { const d = new Date(); const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); return `${y}-${m}-${day}`; } export default function LinksPage() { const [date, setDate] = useState(() => localToday()); const [links, setLinks] = useState(null); const [rawLinks, setRawLinks] = useState([]); const [refreshing, setRefreshing] = useState(false); useEffect(() => { let cancelled = false; (async () => { try { const r = await fetch(`/api/topics/links?date=${date}`); const j = (await r.json()) as LinkInsightResp; if (!cancelled && j.ok) setLinks(j); } catch {} try { const r = await fetch(`/api/message-links/raw?date=${date}`, { cache: 'no-store' }); const j = (await r.json()) as { ok: boolean; links?: RawWechatLink[] }; if (!cancelled && j.ok) setRawLinks(j.links ?? []); } catch {} })(); return () => { cancelled = true; }; }, [date]); const loading = links?.date !== date; async function refreshLinks() { setRefreshing(true); try { const r = await fetch(`/api/topics/links?date=${date}&refresh=1`, { cache: 'no-store' }); const j = (await r.json()) as LinkInsightResp; if (j.ok) setLinks(j); } finally { setRefreshing(false); } } return (
Link Intelligence
链接情报 · 文章与工具
{loading ? `${date} · 加载中…` : `${date} · ${links.articles.length} 篇文章 · ${links.tools.length} 个工具/资源`}
setDate(e.target.value)} className="theme-date-input bg-transparent text-[12px] outline-none" />
} items={loading ? [] : links.articles} date={date} loading={loading} empty="当天还没有文章链接" /> } items={loading ? [] : links.tools} date={date} loading={loading} empty="当天还没有工具链接" />
); } function RawWechatPanel({ date, items, loading, }: { date: string; items: RawWechatLink[]; loading: boolean; }) { return (
微信文章链接
{loading ? '加载中' : `${items.length} 条`}
{loading ? (
加载中…
) : items.length === 0 ? (
当天没有解析到微信文章链接
) : (
{items.map((item) => (
{item.title || item.domain || item.raw_kind}
{item.url}
{item.chat_name} · {item.sender} {item.raw_kind}
))}
)}
); } function LinkInsightPanel({ title, icon, items, date, loading, empty, }: { title: string; icon: ReactNode; items: LinkInsight[]; date: string; loading: boolean; empty: string; }) { return (
{icon} {title}
{loading ? '加载中' : `${items.length} 条`}
{loading ? (
加载中…
) : items.length === 0 ? (
{empty}
) : (
{items.map((item) => ( ))}
)}
); } function LinkInsightRow({ item, date }: { item: LinkInsight; date: string }) { const first = item.sources[0]; return (
{item.title} {sourceLabel(first?.source)} {item.domain}
{first.chat_name} · {first.sender} {item.count > 1 ? `${item.count} 次 · ` : ''} {item.last_seen?.slice(11) ?? ''}
{first.snippet && (
{first.snippet}
)}
); } function sourceLabel(source?: string): string { if (source === 'wechat_raw') return '微信原文'; if (source === 'public_search') return '公开补链'; if (source === 'manual') return '手动补链'; return '网页链接'; } function sourceClass(source?: string): string { const base = 'shrink-0 rounded px-1.5 py-0.5'; if (source === 'wechat_raw') return `${base} bg-[var(--accent-soft)] text-[var(--accent)]`; if (source === 'public_search' || source === 'manual') return `${base} bg-[var(--warn-soft)] text-[var(--warn)]`; return `${base} bg-[var(--surface-2)] text-[var(--text-3)]`; }