"use client"; import { useEffect, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { Check, Coins, Loader2, Paperclip } from "lucide-react"; import { apiDownloadPostAttachment, type PostAttachment } from "@/lib/api"; 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"); } function fileExt(name: string) { const i = name.lastIndexOf("."); if (i <= 0 || i === name.length - 1) return "FILE"; const ext = name.slice(i + 1).replace(/[^a-zA-Z0-9]/g, ""); if (!ext || ext.length > 5) return "FILE"; return ext.toUpperCase(); } function fileKind(name: string, mime: string): "image" | "archive" | "doc" | "media" | "file" { const m = (mime || "").toLowerCase(); const ext = (name.split(".").pop() || "").toLowerCase(); if ( m.startsWith("image/") || ["png", "jpg", "jpeg", "gif", "webp", "svg", "bmp", "avif", "ico", "heic"].includes(ext) ) { return "image"; } if ( m.includes("zip") || m.includes("rar") || m.includes("7z") || m.includes("compress") || ["zip", "rar", "7z", "tar", "gz", "bz2", "xz"].includes(ext) ) { return "archive"; } if ( m.startsWith("audio/") || m.startsWith("video/") || ["mp3", "wav", "flac", "aac", "ogg", "mp4", "mkv", "webm", "mov", "avi"].includes(ext) ) { return "media"; } if ( m.includes("pdf") || m.includes("word") || m.includes("text") || m.includes("sheet") || m.includes("excel") || ["pdf", "doc", "docx", "txt", "md", "xls", "xlsx", "ppt", "pptx", "csv"].includes(ext) ) { return "doc"; } return "file"; } function saveBlob(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(); URL.revokeObjectURL(url); } function AttachmentFace({ a, unlocked, busy, guestPaid, }: { a: PostAttachment; unlocked: boolean; busy: boolean; guestPaid: boolean; }) { const paid = a.price_points > 0; const cta = guestPaid ? "登录后下载" : busy ? "处理中…" : paid && !unlocked ? "支付下载" : "下载"; return ( <> {fileExt(a.name)} {a.name} {formatSize(a.size)} {a.download_count > 0 ? ` · ${a.download_count} 次下载` : ""} {paid ? ( <> {formatPoints(a.price_points)} {unlocked ? "已解锁" : "积分"} > ) : ( 免费 )} {busy ? : null} {!busy && unlocked && paid ? : null} {cta} > ); } 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); const download = async (a: PostAttachment) => { if (busyId != null) return; const firstPaid = a.price_points > 0 && !isUnlocked(a); 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; } saveBlob(res.blob, res.filename || a.name); setPending(null); if (firstPaid) { setUnlockedIds((prev) => (prev.includes(a.id) ? prev : [...prev, a.id])); setFreshId(a.id); toast("已解锁并开始下载", "ok"); } else { toast("已开始下载", "ok"); } router.refresh(); } catch { toast("下载失败,请稍后重试"); } finally { setBusyId(null); } }; const onActivate = (a: PostAttachment) => { if (busyId != null) return; if (a.price_points > 0 && !isUnlocked(a)) { setPending(a); return; } void download(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 ? `下载 ${a.name},需 ${formatPoints(a.price_points)} 积分` : a.price_points > 0 ? `下载 ${a.name}(已解锁)` : `下载 ${a.name}(免费)`; 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 face = ( ); return ( {guestPaid ? ( {face} ) : ( onActivate(a)} > {face} )} ); })} { if (busyId != null) return; setPending(null); }} footer={ <> { if (busyId != null) return; setPending(null); }} > 取消 { if (!pending) return; void download(pending); }} > {busyId != null ? ( <> 处理中… > ) : ( `支付 ${pending ? formatPoints(pending.price_points) : ""} 积分` )} > } > {pending ? ( {fileExt(pending.name)} {pending.name} {formatSize(pending.size)} {formatPoints(pending.price_points)} 积分 ) : null} ); }
{pending.name}
{formatSize(pending.size)}