"use client"; import { useState, useEffect, useCallback } from "react"; import { useRouter } from "next/navigation"; import { ArrowLeft, Plus, Trash2, Eye, EyeOff, Edit, Wifi, WifiOff, Loader2, X } from "lucide-react"; interface Link { name: string; url: string; description?: string; image?: string; hidden?: boolean; rss?: string; addedAt?: string; } export default function LinksPage() { const router = useRouter(); const [links, setLinks] = useState([]); const [loading, setLoading] = useState(true); const [showAll, setShowAll] = useState(false); const [message, setMessage] = useState<{ type: "success" | "error"; text: string } | null>(null); // Add form const [showAdd, setShowAdd] = useState(false); const [addForm, setAddForm] = useState({ name: "", url: "", description: "" }); const [adding, setAdding] = useState(false); // Edit form const [editTarget, setEditTarget] = useState(null); const [editForm, setEditForm] = useState({ name: "", url: "", description: "" }); const [editing, setEditing] = useState(false); // Health const [health, setHealth] = useState>(new Map()); const [checkingAll, setCheckingAll] = useState(false); const [checkingMap, setCheckingMap] = useState>(new Map()); const fetchLinks = useCallback(async () => { setLoading(true); try { const res = await fetch(`/api/rss/links?all=1`); // always get all for admin const data = await res.json(); const all = data.links || []; // Sort visible first, then hidden all.sort((a: Link, b: Link) => (a.hidden ? 1 : 0) - (b.hidden ? 1 : 0)); setLinks(all); } catch { setMessage({ type: "error", text: "加载失败" }); } finally { setLoading(false); } }, []); useEffect(() => { fetchLinks(); }, [fetchLinks]); const handleAdd = async (e: React.FormEvent) => { e.preventDefault(); if (!addForm.name || !addForm.url) return; setAdding(true); try { const res = await fetch("/api/rss/links", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(addForm), }); if (!res.ok) throw new Error("添加失败"); setShowAdd(false); setAddForm({ name: "", url: "", description: "" }); fetchLinks(); } catch (err: unknown) { setMessage({ type: "error", text: err instanceof Error ? err.message : "添加失败" }); } finally { setAdding(false); } }; const startEdit = (l: Link) => { setEditTarget(l.url); setEditForm({ name: l.name, url: l.url, description: l.description || "" }); }; const handleEdit = async () => { if (!editTarget) return; setEditing(true); try { const body = { oldUrl: editTarget, name: editForm.name, url: editForm.url, description: editForm.description, }; const res = await fetch("/api/rss/links", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); if (!res.ok) throw new Error("保存失败"); setEditTarget(null); fetchLinks(); } catch (err: unknown) { setMessage({ type: "error", text: err instanceof Error ? err.message : "保存失败" }); } finally { setEditing(false); } }; const toggleHidden = async (l: Link) => { try { const res = await fetch("/api/rss/links", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ oldUrl: l.url, name: l.name, description: l.description || "", hidden: !l.hidden, }), }); if (!res.ok) throw new Error("操作失败"); setLinks((prev) => prev.map((x) => x.url === l.url ? { ...x, hidden: !x.hidden } : x)); } catch (err: unknown) { setMessage({ type: "error", text: err instanceof Error ? err.message : "操作失败" }); } }; const handleDelete = async (url: string, name: string) => { if (!confirm(`确定删除「${name}」?`)) return; try { await fetch(`/api/rss/links?url=${encodeURIComponent(url)}`, { method: "DELETE" }); setLinks((prev) => prev.filter((l) => l.url !== url)); } catch { setMessage({ type: "error", text: "删除失败" }); } }; const checkOne = async (url: string) => { setCheckingMap((prev) => new Map(prev).set(url, true)); try { const res = await fetch(`/api/rss/links?health=${encodeURIComponent(url)}`); const data = await res.json(); setHealth((prev) => new Map(prev).set(url, data)); } catch {} finally { setCheckingMap((prev) => { const m = new Map(prev); m.delete(url); return m; }); } }; const checkAll = async () => { setCheckingAll(true); const visible = links.filter((l) => !l.hidden); for (let i = 0; i < visible.length; i += 3) { const batch = visible.slice(i, i + 3); await Promise.all(batch.map((l) => checkOne(l.url))); } setCheckingAll(false); }; // Counts for display const visibleLinks = showAll ? links : links.filter((l) => !l.hidden); const hiddenCount = links.filter((l) => l.hidden).length; return (

友链管理

{links.length} 个{hiddenCount > 0 && `(${hiddenCount} 隐藏)`}
{message && (
{message.text}
)} {/* Add form */} {showAdd && (
setAddForm({ ...addForm, name: e.target.value })} className="w-full bg-bg border border-line rounded-md px-3 py-2 text-[13px] focus:outline-none focus:border-accent/30" required />
setAddForm({ ...addForm, url: e.target.value })} className="w-full bg-bg border border-line rounded-md px-3 py-2 text-[13px] focus:outline-none focus:border-accent/30" required />
setAddForm({ ...addForm, description: e.target.value })} className="w-full bg-bg border border-line rounded-md px-3 py-2 text-[13px] focus:outline-none focus:border-accent/30" />
)} {/* Loading */} {loading ? (
加载中...
) : visibleLinks.length === 0 ? (
暂无友链
) : (
{visibleLinks.map((l) => { const h = health.get(l.url); const ck = checkingMap.get(l.url); return (
{/* Health indicator */} {ck ? : h ? (h.ok ? : ) : null} {/* Favicon */} {l.image && { (e.target as HTMLImageElement).style.display = "none"; }} />} {/* Info or Edit form */} {editTarget === l.url ? (
setEditForm({ ...editForm, name: e.target.value })} className="w-full bg-bg border border-line rounded-md px-2 py-1.5 text-[13px] focus:outline-none focus:border-accent/30" title="名称" placeholder="名称" />
setEditForm({ ...editForm, url: e.target.value })} className="w-full bg-bg border border-line rounded-md px-2 py-1.5 text-[13px] focus:outline-none focus:border-accent/30" title="URL" placeholder="URL" />
setEditForm({ ...editForm, description: e.target.value })} className="w-full bg-bg border border-line rounded-md px-2 py-1.5 text-[13px] focus:outline-none focus:border-accent/30" placeholder="描述" title="描述" />
) : (
{l.name} {l.hidden && 隐藏} {h && !h.ok && 失联}
{l.url} {l.description && ·} {l.description && {l.description}}
)} {/* Actions */}
{editTarget !== l.url && ( <> )}
); })}
)}
); }