"use client"; import { useEffect, useMemo, useRef, useState } from "react"; import Link from "next/link"; import { CircleDollarSign, Clock, Eye, Handshake, ImageIcon, Inbox, Megaphone, Plus, Trash2, Type, Upload, Wallet, XCircle, } from "lucide-react"; import { apiAdminApproveAd, apiAdminDeleteAd, apiAdminListAds, apiAdminRejectAd, apiAdminSaveAdsConfig, apiAdminSaveSponsorsConfig, apiAdminUploadAdQR, type AdConfig, type AdPaymentOption, type AdRecord, type SponsorItem, type SponsorsConfig, } from "@/lib/api"; import { AdminEmpty, AdminPageHeader, AdminSegmented, AdminSettingsPage, AdminSettingsWork, AdminStatCard, AdminStatusChip, AdminSwitch, } from "@/components/admin"; import ConfirmDialog from "@/components/ConfirmDialog"; import Modal from "@/components/Modal"; import { AdText } from "@/components/AdText"; import { formatRelative } from "@/lib/format"; import { toast } from "@/lib/toast"; import { densitySrcSet, isOwnImage, pickVariantWidth, variantURL } from "@/lib/responsiveImage"; type StatusFilter = "" | "pending" | "active" | "rejected" | "expired"; type SectionId = "overview" | "queue" | "promote" | "sponsors"; const statusLabel: Record = { pending: "待审", active: "展示中", rejected: "已拒绝", expired: "已到期", }; const SECTIONS: { id: SectionId; label: string; icon: typeof Inbox }[] = [ { id: "overview", label: "概览", icon: Eye }, { id: "queue", label: "推广申购", icon: Inbox }, { id: "promote", label: "自助推广", icon: Megaphone }, { id: "sponsors", label: "赞助商", icon: Handshake }, ]; const emptySponsors = (): SponsorsConfig => ({ enabled: true, panel_title: "赞助商", items: [], }); export default function AdsAdmin({ initialAds, initialConfig, initialSponsors, initError, }: { initialAds: AdRecord[]; initialConfig: AdConfig | null; initialSponsors: SponsorsConfig | null; initError: string; }) { const [status, setStatus] = useState(""); const [ads, setAds] = useState(initialAds); const [cfg, setCfg] = useState( initialConfig || { enabled: true, panel_title: "自助推广", durations: [ { days: 30, label: "1 个月", price_hint: "", price_hint_text: "" }, { days: 60, label: "2 个月", price_hint: "", price_hint_text: "" }, { days: 90, label: "3 个月", price_hint: "", price_hint_text: "" }, { days: 180, label: "6 个月", price_hint: "", price_hint_text: "" }, { days: 365, label: "12 个月", price_hint: "", price_hint_text: "" }, ], payments: [], } ); const [sponsors, setSponsors] = useState(initialSponsors || emptySponsors()); const [busy, setBusy] = useState(false); const [deleteId, setDeleteId] = useState(null); const [rejectId, setRejectId] = useState(null); const [rejectReason, setRejectReason] = useState(""); const [activeSection, setActiveSection] = useState("overview"); const counts = useMemo(() => { const c = { pending: 0, active: 0, rejected: 0, expired: 0 }; for (const a of ads) { if (a.status in c) (c as Record)[a.status] += 1; } return c; }, [ads]); const visibleAds = useMemo(() => { if (!status) return ads; return ads.filter((a) => a.status === status); }, [ads, status]); const reload = async (st: StatusFilter = status) => { try { const res = await apiAdminListAds(st); setAds(res.ads ?? []); } catch (e) { toast(e instanceof Error ? e.message : "刷新失败", "error"); } }; const onApprove = async (id: number) => { setBusy(true); try { await apiAdminApproveAd(id); toast("已上架", "ok"); await reload(); } catch (e) { toast(e instanceof Error ? e.message : "操作失败", "error"); } finally { setBusy(false); } }; const onRejectConfirm = async () => { if (rejectId == null) return; setBusy(true); try { await apiAdminRejectAd(rejectId, rejectReason.trim()); toast("已拒绝", "ok"); setRejectId(null); setRejectReason(""); await reload(); } catch (e) { toast(e instanceof Error ? e.message : "操作失败", "error"); } finally { setBusy(false); } }; const onDelete = async () => { if (deleteId == null) return; setBusy(true); try { await apiAdminDeleteAd(deleteId); toast("已删除", "ok"); setDeleteId(null); await reload(); } catch (e) { toast(e instanceof Error ? e.message : "删除失败", "error"); } finally { setBusy(false); } }; const saveConfig = async () => { setBusy(true); try { const next = await apiAdminSaveAdsConfig(cfg); setCfg(next); toast("配置已保存", "ok"); } catch (e) { toast(e instanceof Error ? e.message : "保存失败", "error"); } finally { setBusy(false); } }; const saveSponsors = async () => { setBusy(true); try { const next = await apiAdminSaveSponsorsConfig(sponsors); setSponsors(next); toast("赞助商已保存", "ok"); } catch (e) { toast(e instanceof Error ? e.message : "保存失败", "error"); } finally { setBusy(false); } }; const uploadQR = async (index: number, file: File) => { try { const { url } = await apiAdminUploadAdQR(file); setCfg((c) => { const payments = [...c.payments]; payments[index] = { ...payments[index], qr_url: url }; return { ...c, payments }; }); toast("收款码已上传", "ok"); } catch (e) { toast(e instanceof Error ? e.message : "上传失败", "error"); } }; const uploadSponsorLogo = async (index: number, file: File) => { try { const { url } = await apiAdminUploadAdQR(file); setSponsors((c) => { const items = [...c.items]; items[index] = { ...items[index], logo_url: url }; return { ...c, items }; }); toast("Logo 已上传", "ok"); } catch (e) { toast(e instanceof Error ? e.message : "上传失败", "error"); } }; const addPayment = () => { const id = `pay_${Date.now().toString(36)}`; setCfg((c) => ({ ...c, payments: [ ...c.payments, { id, name: "支付宝", kind: "alipay", qr_url: "", hint: "付款请备注邮箱", enabled: true }, ], })); }; const addSponsor = () => { const id = `sp_${Date.now().toString(36)}`; setSponsors((c) => ({ ...c, items: [ ...c.items, { id, name: "", logo_url: "", link_url: "", hover_text: "", enabled: true, sort_order: c.items.length, }, ], })); }; // 粘性分区导航:滚动高亮当前区块 const sectionRefs = useRef>({ overview: null, queue: null, promote: null, sponsors: null, }); useEffect(() => { const els = SECTIONS.map((s) => sectionRefs.current[s.id]).filter( (el): el is HTMLElement => !!el ); if (els.length === 0) return; const observer = new IntersectionObserver( (entries) => { const visible = entries .filter((e) => e.isIntersecting) .sort((a, b) => b.intersectionRatio - a.intersectionRatio); if (visible[0]) { const id = (visible[0].target as HTMLElement).dataset.section as SectionId; if (id) setActiveSection(id); } }, { rootMargin: "-30% 0px -55% 0px", threshold: [0, 0.25, 0.5, 1] } ); els.forEach((el) => observer.observe(el)); return () => observer.disconnect(); }, []); const scrollTo = (id: SectionId) => { const el = sectionRefs.current[id]; if (el) el.scrollIntoView({ behavior: "smooth", block: "start" }); }; if (initError && !initialConfig) { return

{initError}

; } const leftNav = ( ); return ( 预览推广页 } />
{/* ── 概览 KPI ─────────────────────────────── */}
{ sectionRefs.current.overview = el; }} >
} color="var(--gold)" soft="var(--gold-soft)" hint="需尽快处理" /> } color="var(--ok)" soft="color-mix(in srgb, var(--ok) 14%, var(--panel))" hint="当前在线广告" /> } color="var(--ink-3)" soft="var(--panel-2)" hint="可重新上架" /> } color="var(--accent)" soft="var(--accent-soft)" hint={cfg.enabled ? "自助推广已开放" : "自助推广已关闭"} /> } color="var(--accent)" soft="var(--accent-soft)" hint={sponsors.enabled ? "板块已展示" : "板块已隐藏"} />
{/* ── 推广申购 ─────────────────────────────── */}
{ sectionRefs.current.queue = el; }} className="panel overflow-hidden" > } title="推广申购" desc="游客提交的推广申请,审核通过后展示在首页侧栏" extra={ ariaLabel="广告状态筛选" size="sm" role="group" value={status} onChange={(v) => { setStatus(v); void reload(v); }} options={[ { key: "", label: "全部" }, { key: "pending", label: "待审", count: counts.pending }, { key: "active", label: "展示中", count: counts.active }, { key: "rejected", label: "已拒绝" }, { key: "expired", label: "已到期" }, ]} /> } />
{visibleAds.length === 0 ? ( } title="暂无广告记录" description={status ? "该状态下没有记录" : "游客提交后会出现在此列表"} /> ) : (
    {visibleAds.map((ad) => ( void onApprove(ad.id)} onReject={() => { setRejectId(ad.id); setRejectReason(""); }} onDelete={() => setDeleteId(ad.id)} /> ))}
)}
{/* ── 自助推广配置 ─────────────────────────── */}
{ sectionRefs.current.promote = el; }} className="panel overflow-hidden" > } title="自助推广" desc="配置购买页的时长档位与收款方式" extra={
{cfg.enabled ? "已开放" : "已关闭"} setCfg({ ...cfg, enabled: !cfg.enabled })} />
} />
侧栏 / 页面标题 setCfg({ ...cfg, panel_title: e.target.value })} maxLength={16} />
{/* 时长档位 */} setCfg({ ...cfg, durations: [ ...cfg.durations, { days: 14, label: "14 天", price_hint: "", price_hint_text: "" }, ], }) } > 添加 } > {cfg.durations.length === 0 ? (

至少保留一个时长档位。

) : (
{cfg.durations.map((d, i) => ( ))}
天数 标签 图片价格 文字价格 操作
{ const durations = [...cfg.durations]; durations[i] = { ...d, days: Number(e.target.value) || 1 }; setCfg({ ...cfg, durations }); }} aria-label="天数" /> { const durations = [...cfg.durations]; durations[i] = { ...d, label: e.target.value }; setCfg({ ...cfg, durations }); }} /> { const durations = [...cfg.durations]; durations[i] = { ...d, price_hint: e.target.value }; setCfg({ ...cfg, durations }); }} aria-label="图片广告价格" /> { const durations = [...cfg.durations]; durations[i] = { ...d, price_hint_text: e.target.value }; setCfg({ ...cfg, durations }); }} aria-label="文字广告价格" />
)}
{/* 收款方式 */} 添加 } > {cfg.payments.length === 0 ? (
添加支付宝 / 微信收款码后,游客才能提交申购。
) : (
{cfg.payments.map((p, i) => ( { const payments = [...cfg.payments]; payments[i] = next; setCfg({ ...cfg, payments }); }} onUpload={(file) => void uploadQR(i, file)} onRemove={() => setCfg({ ...cfg, payments: cfg.payments.filter((_, j) => j !== i) }) } /> ))}
)}
{/* ── 赞助商(占满内容栏全宽)────────────── */}
{ sectionRefs.current.sponsors = el; }} className="panel overflow-hidden" > } title="赞助商" desc="展示在首页侧栏的赞助商 logo 墙" extra={
{sponsors.enabled ? "已展示" : "已隐藏"} setSponsors({ ...sponsors, enabled: !sponsors.enabled })} />
} />
侧栏标题 setSponsors({ ...sponsors, panel_title: e.target.value })} maxLength={16} />
添加 } > {sponsors.items.length === 0 ? ( } title="暂无赞助商" description="添加 logo 后,悬停或聚焦可显示赞助说明" /> ) : (
{sponsors.items.map((sp, i) => ( { const items = [...sponsors.items]; items[i] = next; setSponsors({ ...sponsors, items }); }} onUpload={(file) => void uploadSponsorLogo(i, file)} onRemove={() => setSponsors({ ...sponsors, items: sponsors.items.filter((_, j) => j !== i), }) } /> ))}
)}
void onDelete()} onCancel={() => setDeleteId(null)} /> { if (!busy) { setRejectId(null); setRejectReason(""); } }} busy={busy} maxWidthClass="max-w-sm" footer={ <> } >