"use client"; import { useEffect, useState, type ReactNode } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { Check, Coins, Loader2, Paperclip } from "lucide-react"; import { apiDownloadPostAttachment, postAttachmentDownloadURL, type PostAttachment, } from "@/lib/api"; import { fileExtBadge, fileKind } from "@/lib/fileKind"; import { toast } from "@/lib/toast"; import Modal from "@/components/Modal"; function formatSize(n: number) { if (n < 1024) return `${n} B`; if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`; return `${(n / 1024 / 1024).toFixed(1)} MB`; } function formatPoints(n: number) { return n.toLocaleString("zh-CN"); } /** 把已拿到的文件交给浏览器下载栏(付费首次须先 fetch 才能处理 402) */ function triggerBrowserDownload(blob: Blob, filename: string) { const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; a.rel = "noopener"; document.body.appendChild(a); a.click(); a.remove(); window.setTimeout(() => URL.revokeObjectURL(url), 30_000); } function AttachmentMeta({ a, unlocked, }: { a: PostAttachment; unlocked: boolean; }) { const paid = a.price_points > 0; return ( <> {fileExtBadge(a.name)} {a.name} {formatSize(a.size)} {a.download_count > 0 ? ` · ${a.download_count} 次下载` : ""} {paid ? ( <> {formatPoints(a.price_points)} {unlocked ? "已解锁" : "积分"} > ) : ( 免费 )} > ); } export default function PostAttachments({ postId, items, loggedIn, }: { postId: number; items: PostAttachment[]; loggedIn: boolean; }) { const router = useRouter(); const loginHref = `/login?redirect=${encodeURIComponent(`/post/${postId}`)}`; const [busyId, setBusyId] = useState(null); const [pending, setPending] = useState(null); const [unlockedIds, setUnlockedIds] = useState([]); const [freshId, setFreshId] = useState(null); useEffect(() => { if (freshId == null) return; const t = window.setTimeout(() => setFreshId(null), 900); return () => window.clearTimeout(t); }, [freshId]); if (!items.length) return null; const isUnlocked = (a: PostAttachment) => a.unlocked || unlockedIds.includes(a.id); /** 付费未解锁:先确认再 fetch,避免积分不足时整页 JSON;成功后交给浏览器下载栏 */ const downloadPaid = async (a: PostAttachment) => { if (busyId != null) return; setBusyId(a.id); try { const res = await apiDownloadPostAttachment(postId, a.id, a.name); if (res.status === 401) { setPending(null); router.push(loginHref); return; } if (res.error || !res.blob) { toast(res.error || "下载失败"); return; } triggerBrowserDownload(res.blob, res.filename || a.name); setPending(null); setUnlockedIds((prev) => (prev.includes(a.id) ? prev : [...prev, a.id])); setFreshId(a.id); toast("已解锁", "ok"); router.refresh(); } catch { toast("下载失败,请稍后重试"); } finally { setBusyId(null); } }; const onPayCta = (a: PostAttachment) => { if (busyId != null) return; setPending(a); }; return ( 附件 {items.length} 个文件 {items.map((a) => { const guestPaid = !loggedIn && a.price_points > 0; const unlocked = isUnlocked(a); const busy = busyId === a.id; const paidLocked = a.price_points > 0 && !unlocked; const label = guestPaid ? `登录后下载 ${a.name},需 ${formatPoints(a.price_points)} 积分` : paidLocked ? `支付 ${formatPoints(a.price_points)} 积分下载 ${a.name}` : `下载 ${a.name}`; const ctaText = guestPaid ? "登录后下载" : busy ? "处理中…" : paidLocked ? "支付下载" : "下载"; const className = [ "j13-att-card", a.price_points > 0 ? "is-paid" : "", unlocked && a.price_points > 0 ? "is-unlocked" : "", busy ? "is-busy" : "", freshId === a.id ? "is-fresh" : "", ] .filter(Boolean) .join(" "); const ctaClass = `j13-att-cta${paidLocked || guestPaid ? " is-primary" : ""}`; const nativeHref = postAttachmentDownloadURL(postId, a.id); let cta: ReactNode; if (guestPaid) { cta = ( {ctaText} ); } else if (paidLocked) { cta = ( onPayCta(a)} > {busy ? : null} {ctaText} ); } else { cta = ( {unlocked && a.price_points > 0 ? : null} {ctaText} ); } return ( {cta} ); })} { if (busyId != null) return; setPending(null); }} footer={ <> { if (busyId != null) return; setPending(null); }} > 取消 { if (!pending) return; void downloadPaid(pending); }} > {busyId != null ? ( <> 处理中… > ) : ( `支付 ${pending ? formatPoints(pending.price_points) : ""} 积分` )} > } > {pending ? ( {fileExtBadge(pending.name)} {pending.name} {formatSize(pending.size)} {formatPoints(pending.price_points)} 积分 ) : null} ); }
{pending.name}
{formatSize(pending.size)}