From 73fbbbafef42f608c2febcd690ad5c6b528502ba Mon Sep 17 00:00:00 2001 From: freefire Date: Thu, 17 Sep 2026 06:28:33 +0800 Subject: [PATCH] =?UTF-8?q?fix(attach):=20=E9=99=84=E4=BB=B6=E6=94=B9?= =?UTF-8?q?=E5=9B=9E=E6=B5=8F=E8=A7=88=E5=99=A8=E4=B8=8B=E8=BD=BD=E9=80=9A?= =?UTF-8?q?=E9=81=93=EF=BC=8C=E4=BB=85=E6=8C=89=E9=92=AE=E8=A7=A6=E5=8F=91?= =?UTF-8?q?=E4=B8=94=E4=B8=8D=E5=86=8D=E8=AF=AF=E6=8A=A5=E5=B7=B2=E5=BC=80?= =?UTF-8?q?=E5=A7=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- frontend/app/globals.css | 33 ++++--- frontend/components/PostAttachments.tsx | 124 ++++++++++++------------ 2 files changed, 84 insertions(+), 73 deletions(-) diff --git a/frontend/app/globals.css b/frontend/app/globals.css index f22ee30..655785a 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -1767,15 +1767,12 @@ body { text-decoration: none; color: inherit; font: inherit; - cursor: pointer; + cursor: default; border: 1px solid var(--line); background: var(--panel); box-shadow: var(--shadow-sm); transition: border-color 0.15s, box-shadow 0.15s, transform 0.15s, background 0.15s; } -a.j13-att-card:hover { - color: inherit; -} .j13-att-card:hover { transform: translateY(-1px); box-shadow: var(--shadow); @@ -1800,13 +1797,7 @@ a.j13-att-card:hover { border-color: color-mix(in srgb, var(--ok) 46%, var(--line)); } .j13-att-card.is-busy { - cursor: wait; -} -.j13-att-card:disabled { - opacity: 0.58; - cursor: not-allowed; - transform: none; - box-shadow: var(--shadow-sm); + opacity: 0.92; } .j13-att-card.is-fresh { animation: j13-att-unlock 0.7s ease-out; @@ -1923,11 +1914,25 @@ a.j13-att-card:hover { font-weight: 600; line-height: 1; flex-shrink: 0; - pointer-events: none; + cursor: pointer; border: 1px solid var(--line-2); background: var(--panel); color: var(--ink); white-space: nowrap; + font-family: inherit; + text-decoration: none; + appearance: none; +} +a.j13-att-cta:hover { + color: var(--ink); +} +.j13-att-cta:hover:not(:disabled):not(.is-primary) { + border-color: var(--accent); + color: var(--accent); +} +.j13-att-cta:disabled { + opacity: 0.5; + cursor: not-allowed; } .j13-att-cta.is-primary { border-color: transparent; @@ -1936,6 +1941,10 @@ a.j13-att-card:hover { box-shadow: 0 1px 2px rgba(16, 24, 40, 0.12), 0 2px 8px color-mix(in srgb, var(--accent) 26%, transparent); } +.j13-att-cta.is-primary:hover:not(:disabled) { + background: var(--accent-hi); + color: var(--accent-on); +} .j13-att-pay { display: flex; flex-direction: column; diff --git a/frontend/components/PostAttachments.tsx b/frontend/components/PostAttachments.tsx index 517accd..747775b 100644 --- a/frontend/components/PostAttachments.tsx +++ b/frontend/components/PostAttachments.tsx @@ -1,10 +1,14 @@ "use client"; -import { useEffect, useState } from "react"; +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, type PostAttachment } from "@/lib/api"; +import { + apiDownloadPostAttachment, + postAttachmentDownloadURL, + type PostAttachment, +} from "@/lib/api"; import { toast } from "@/lib/toast"; import Modal from "@/components/Modal"; @@ -64,7 +68,8 @@ function fileKind(name: string, mime: string): "image" | "archive" | "doc" | "me return "file"; } -function saveBlob(blob: Blob, filename: string) { +/** 把已拿到的文件交给浏览器下载栏(付费首次须先 fetch 才能处理 402) */ +function triggerBrowserDownload(blob: Blob, filename: string) { const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; @@ -73,28 +78,17 @@ function saveBlob(blob: Blob, filename: string) { document.body.appendChild(a); a.click(); a.remove(); - URL.revokeObjectURL(url); + window.setTimeout(() => URL.revokeObjectURL(url), 30_000); } -function AttachmentFace({ +function AttachmentMeta({ 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 ( <> @@ -123,11 +117,6 @@ function AttachmentFace({ 免费 )} - - {busy ? : null} - {!busy && unlocked && paid ? : null} - {cta} - ); } @@ -158,9 +147,9 @@ export default function PostAttachments({ const isUnlocked = (a: PostAttachment) => a.unlocked || unlockedIds.includes(a.id); - const download = async (a: PostAttachment) => { + /** 付费未解锁:先确认再 fetch,避免积分不足时整页 JSON;成功后交给浏览器下载栏 */ + const downloadPaid = 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); @@ -173,15 +162,11 @@ export default function PostAttachments({ toast(res.error || "下载失败"); return; } - saveBlob(res.blob, res.filename || a.name); + triggerBrowserDownload(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"); - } + setUnlockedIds((prev) => (prev.includes(a.id) ? prev : [...prev, a.id])); + setFreshId(a.id); + toast("已解锁", "ok"); router.refresh(); } catch { toast("下载失败,请稍后重试"); @@ -190,13 +175,9 @@ export default function PostAttachments({ } }; - const onActivate = (a: PostAttachment) => { + const onPayCta = (a: PostAttachment) => { if (busyId != null) return; - if (a.price_points > 0 && !isUnlocked(a)) { - setPending(a); - return; - } - void download(a); + setPending(a); }; return ( @@ -217,10 +198,15 @@ export default function PostAttachments({ const label = guestPaid ? `登录后下载 ${a.name},需 ${formatPoints(a.price_points)} 积分` : paidLocked - ? `下载 ${a.name},需 ${formatPoints(a.price_points)} 积分` - : a.price_points > 0 - ? `下载 ${a.name}(已解锁)` - : `下载 ${a.name}(免费)`; + ? `支付 ${formatPoints(a.price_points)} 积分下载 ${a.name}` + : `下载 ${a.name}`; + const ctaText = guestPaid + ? "登录后下载" + : busy + ? "处理中…" + : paidLocked + ? "支付下载" + : "下载"; const className = [ "j13-att-card", a.price_points > 0 ? "is-paid" : "", @@ -230,27 +216,43 @@ export default function PostAttachments({ ] .filter(Boolean) .join(" "); - const face = ( - - ); + 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 = ( + + ); + } else { + cta = ( + + {unlocked && a.price_points > 0 ? : null} + {ctaText} + + ); + } return (
  • - {guestPaid ? ( - - {face} - - ) : ( - - )} +
    + + {cta} +
  • ); })} @@ -287,7 +289,7 @@ export default function PostAttachments({ disabled={busyId != null || !pending} onClick={() => { if (!pending) return; - void download(pending); + void downloadPaid(pending); }} > {busyId != null ? (