"use client"; import { useCallback, useEffect, useState } from "react"; import Link from "next/link"; import { RefreshCw, ImageIcon, Type, CheckCircle2, Mail, Link2, Shield, AlertCircle, } from "lucide-react"; import { apiAdsPublicConfig, apiFetchCaptcha, apiSubmitAd, type AdConfig, type AdPaymentOption, } from "@/lib/api"; import ImageLightbox, { type LightboxItem } from "@/components/markdown/ImageLightbox"; import { AdText } from "@/components/AdText"; import { toast } from "@/lib/toast"; import { AD_IMAGE_HEIGHT, AD_IMAGE_SIZE_LABEL, AD_IMAGE_SIZE_LABEL_2X, AD_IMAGE_WIDTH, AD_TEXT_COL_WIDTH, } from "@/lib/ads"; type Kind = "image" | "text"; export default function AdsBuyClient() { const [cfg, setCfg] = useState(null); const [loadErr, setLoadErr] = useState(""); const [kind, setKind] = useState("image"); const [email, setEmail] = useState(""); const [link, setLink] = useState(""); const [imageURL, setImageURL] = useState(""); const [title, setTitle] = useState(""); const [textColor, setTextColor] = useState("#1E293B"); const [bgColor, setBgColor] = useState("#F1F5F9"); const [days, setDays] = useState(0); const [paymentID, setPaymentID] = useState(""); const [note, setNote] = useState(""); const [captchaID, setCaptchaID] = useState(""); const [captchaImg, setCaptchaImg] = useState(""); const [captchaCode, setCaptchaCode] = useState(""); const [busy, setBusy] = useState(false); const [done, setDone] = useState(false); const [lightbox, setLightbox] = useState<{ items: LightboxItem[]; index: number } | null>(null); const refreshCaptcha = useCallback(async () => { try { const c = await apiFetchCaptcha(); setCaptchaID(c.id); setCaptchaImg(c.image); setCaptchaCode(""); } catch (e) { toast(e instanceof Error ? e.message : "验证码加载失败", "error"); } }, []); useEffect(() => { let alive = true; apiAdsPublicConfig() .then((c) => { if (!alive) return; setCfg(c); if (c.durations[0]) setDays(c.durations[0].days); const pay = c.payments.find((p) => p.enabled && p.qr_url); if (pay) setPaymentID(pay.id); }) .catch((e) => { if (alive) setLoadErr(e instanceof Error ? e.message : "加载失败"); }); void refreshCaptcha(); return () => { alive = false; }; }, [refreshCaptcha]); const selectedPay: AdPaymentOption | undefined = cfg?.payments.find((p) => p.id === paymentID); const selectedDuration = cfg?.durations.find((d) => d.days === days); const canSubmit = !!cfg && cfg.payments.length > 0 && !busy; const kindLabel = kind === "image" ? "图片广告" : "文字广告"; const openLightbox = (src: string, alt = "") => { setLightbox({ items: [{ src, alt }], index: 0 }); }; const onSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!canSubmit) return; setBusy(true); try { await apiSubmitAd({ kind, contact_email: email, link_url: link, image_url: kind === "image" ? imageURL : "", title: kind === "text" ? title : "", text_color: kind === "text" ? textColor : "", bg_color: kind === "text" ? bgColor : "", duration_days: days, payment_id: paymentID, buyer_note: note, captcha_id: captchaID, captcha_code: captchaCode, }); setDone(true); toast("提交成功,请完成付款并等待审核", "ok"); } catch (err) { toast(err instanceof Error ? err.message : "提交失败", "error"); void refreshCaptcha(); } finally { setBusy(false); } }; if (loadErr) { return (

{loadErr}

返回首页
); } if (!cfg) { return (

加载中…

); } if (!cfg.enabled) { return (

自助推广暂未开放

返回首页
); } if (done) { return (

申请已提交

请完成付款,管理员确认后将出现在首页侧栏。

{selectedPay?.qr_url ? ( ) : null}
返回首页
{lightbox ? ( setLightbox(null)} onIndexChange={(i) => setLightbox((prev) => (prev ? { ...prev, index: i } : prev))} /> ) : null}
); } return (

自助购买广告位

{kindLabel} {selectedDuration ? ` · ${selectedDuration.label}` : ""}

openLightbox(imageURL, "图片广告预览")} />
setKind("image")}> 图片广告 setKind("text")}> 文字广告
}> setEmail(e.target.value)} placeholder="联系邮箱" autoComplete="email" /> }> setLink(e.target.value)} placeholder="广告链接,带 https" /> {kind === "image" ? ( <> }> setImageURL(e.target.value)} placeholder={`图片地址,带 https,尺寸 ${AD_IMAGE_SIZE_LABEL}(高清 ${AD_IMAGE_SIZE_LABEL_2X})`} /> ) : ( <> }> setTitle(e.target.value)} placeholder="广告标题,最多 24 字" />
)}
{cfg.durations.map((d) => { const active = days === d.days; return ( setDays(d.days)} compact> {d.label.replace(/\s/g, "")} ); })}
{cfg.payments.length === 0 ? (
站长尚未配置收款码。
) : ( <>
{cfg.payments.map((p) => ( setPaymentID(p.id)}> {p.name} ))}
{selectedPay?.qr_url ? ( ) : null} {selectedPay?.hint ? (

{selectedPay.hint}

) : null} )} }> setNote(e.target.value)} placeholder="备注(可选)" />
} className="min-w-0 flex-1"> setCaptchaCode(e.target.value)} placeholder="图片验证码" autoComplete="off" maxLength={8} />

返回首页

{lightbox ? ( setLightbox(null)} onIndexChange={(i) => setLightbox((prev) => (prev ? { ...prev, index: i } : prev))} /> ) : null}
); } function Shell({ children }: { children: React.ReactNode }) { return
{children}
; } function ChipTab({ active, onClick, children, compact, }: { active: boolean; onClick: () => void; children: React.ReactNode; compact?: boolean; }) { return ( ); } function IconField({ icon, children, className = "", }: { icon: React.ReactNode; children: React.ReactNode; className?: string; }) { return ( ); } /** * 上线预览:复用首页侧栏 .j13-ad-promo 结构与样式, * 图片位 248×60、文字位两列网格,尺寸/圆角与侧栏完全一致。 */ function AdPreview({ kind, imageURL, title, textColor, bgColor, onImageClick, }: { kind: Kind; imageURL: string; title: string; textColor: string; bgColor: string; onImageClick: () => void; }) { const text = title.trim(); return (

上线预览 · 尺寸与首页侧栏一致

{kind === "image" ? ( ) : (
  • {text.length > 0 ? ( ) : ( 文字广告位 )}
)}
); } function normalizeHex(v: string): string { const s = v.trim(); if (/^#[0-9A-Fa-f]{6}$/.test(s)) return s; if (/^[0-9A-Fa-f]{6}$/.test(s)) return `#${s}`; return "#1E293B"; }