"use client"; import { useEffect, useMemo, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { Info, Link2, Loader2, X } from "lucide-react"; import { apiMyFriendLinks, apiUpdateMyFriendLink, type FriendLink, type FriendLinkStatus, } from "@/lib/api"; import LinksClient from "./LinksClient"; interface Props { links: FriendLink[]; isLoggedIn: boolean; canManage: boolean; /** 本站信息(申请弹窗展示,供对方添加回链) */ siteName: string; siteUrl: string; siteLogo: string; } /** 申请须知(站点政策,可按需修改) */ const APPLY_NOTICE = "纯导链、无回链、带有营销类产品的网站申请将不予通过"; /** 弹窗内单条申请的可编辑字段 */ interface MineEdit { name: string; url: string; reciprocal_url: string; logo_url: string; } function toEdit(l: FriendLink): MineEdit { return { name: l.name, url: l.url, reciprocal_url: l.reciprocal_url || "", logo_url: l.logo_url || "", }; } /** 取 URL 域名用于搜索匹配(解析失败返回原文) */ function domainOf(url: string): string { try { return new URL(url).hostname.toLowerCase(); } catch { return url.toLowerCase(); } } const STATUS_META: Record = { pending: { label: "待审核", color: "var(--accent)" }, approved: { label: "已通过", color: "#16a34a" }, rejected: { label: "已拒绝", color: "var(--danger)" }, }; export default function LinksBoard({ links, isLoggedIn, canManage, siteName, siteUrl, siteLogo, }: Props) { const router = useRouter(); const [query, setQuery] = useState(""); const [mineOpen, setMineOpen] = useState(false); const [applyOpen, setApplyOpen] = useState(false); const [mine, setMine] = useState(null); const [mineLoading, setMineLoading] = useState(false); const [edits, setEdits] = useState>({}); const [savingId, setSavingId] = useState(null); const [editMsg, setEditMsg] = useState<{ id: number; ok: boolean; text: string } | null>(null); // 从其他入口带 #apply 锚点跳转过来时(如首页侧栏「申请」),自动打开申请弹窗 useEffect(() => { if (window.location.hash === "#apply") setApplyOpen(true); }, []); const filtered = useMemo(() => { const q = query.trim().toLowerCase(); if (!q) return links; return links.filter( (l) => l.name.toLowerCase().includes(q) || domainOf(l.url).includes(q) ); }, [links, query]); const openMine = async () => { setMineOpen(true); if (mine == null) { setMineLoading(true); try { const res = await apiMyFriendLinks(); const list = res.links ?? []; setMine(list); setEdits((e) => { const next = { ...e }; for (const l of list) if (!next[l.id]) next[l.id] = toEdit(l); return next; }); } catch { setMine([]); } finally { setMineLoading(false); } } }; const setEdit = (id: number, k: keyof MineEdit, v: string) => setEdits((e) => ({ ...e, [id]: { ...e[id], [k]: v } })); const saveMine = async (l: FriendLink) => { const edit = edits[l.id]; if (!edit || savingId != null) return; setSavingId(l.id); setEditMsg(null); try { await apiUpdateMyFriendLink(l.id, { name: edit.name.trim(), url: edit.url.trim(), reciprocal_url: edit.reciprocal_url.trim() || undefined, logo_url: edit.logo_url.trim() || undefined, }); // 重拉本人申请列表;已通过/已驳回的修改会回退待审,同时刷新 SSR 卡片墙 const res = await apiMyFriendLinks(); const list = res.links ?? []; setMine(list); setEdits((e) => { const next = { ...e }; for (const nl of list) next[nl.id] = toEdit(nl); return next; }); const updated = list.find((x) => x.id === l.id); setEditMsg({ id: l.id, ok: true, text: updated && updated.status === "pending" && l.status !== "pending" ? "已保存,链接重新进入审核队列" : "保存成功", }); router.refresh(); } catch (err) { setEditMsg({ id: l.id, ok: false, text: err instanceof Error ? err.message : "保存失败,请重试", }); } finally { setSavingId(null); } }; return ( <> {/* 搜索 */}
setQuery(e.target.value)} placeholder="搜索友链(域名或名称)" className="w-full rounded-full px-4 py-2 text-[14px] outline-none transition-colors" style={{ background: "var(--panel)", color: "var(--ink)", border: "1px solid var(--line)" }} />
{/* 操作按钮 */}
{/* 我的申请弹窗:列出本人提交的申请,可直接修改并保存 */} {mineOpen ? (
setMineOpen(false)} >
e.stopPropagation()} >

我的申请

{!isLoggedIn ? (

请先 登录 后查看申请记录

) : mineLoading ? (

加载中…

) : mine == null || mine.length === 0 ? (

暂无申请记录

) : (
    {mine.map((l) => { const edit = edits[l.id] ?? toEdit(l); return (
  • {/* 头部:LOGO + 名称 + 状态 */}
    {edit.logo_url ? ( // eslint-disable-next-line @next/next/no-img-element ) : ( )} {l.name} {STATUS_META[l.status]?.label ?? l.status}
    {l.status === "rejected" && l.reject_reason ? (

    拒绝理由:{l.reject_reason}

    ) : null} {/* 可编辑字段 */}
    {/* 底部:反馈 + 保存 */}
    {editMsg?.id === l.id ? ( {editMsg.text} ) : l.status !== "pending" ? ( 修改后将重新进入审核 ) : null}
  • ); })}
)}
) : null} {/* 申请友链弹窗:本站信息 + 申请表单 */} {applyOpen ? (
setApplyOpen(false)} >
e.stopPropagation()} >

申请友链

{/* 本站信息:供对方先添加我方链接 */}

本站信息(请添加本站链接后再申请)

名称:
{siteName}
{siteUrl ? (
地址:
{siteUrl}
) : null} {siteLogo ? (
LOGO:
{siteLogo} (固定地址,换图不失效)
) : null}
{/* 申请须知 */}

{APPLY_NOTICE}

setApplyOpen(false)} />
) : null} {/* 卡片墙 */} {filtered.length === 0 ? (

{links.length === 0 ? "暂无友链,欢迎申请收录" : "没有匹配的友链"}

) : (
{filtered.map((l) => ( {l.logo_url ? ( // eslint-disable-next-line @next/next/no-img-element ) : ( )} {l.name} ))}
)} ); }