fix(attach): 附件改回浏览器下载通道,仅按钮触发且不再误报已开始
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<span className="j13-att-type" data-kind={fileKind(a.name, a.mime)} aria-hidden>
|
||||
@@ -123,11 +117,6 @@ function AttachmentFace({
|
||||
<span className="j13-att-price-unit">免费</span>
|
||||
)}
|
||||
</span>
|
||||
<span className={`j13-att-cta${paid && !unlocked ? " is-primary" : ""}`}>
|
||||
{busy ? <Loader2 size={14} className="animate-spin" aria-hidden /> : null}
|
||||
{!busy && unlocked && paid ? <Check size={14} aria-hidden /> : null}
|
||||
{cta}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 = (
|
||||
<AttachmentFace a={a} unlocked={unlocked} busy={busy} guestPaid={guestPaid} />
|
||||
);
|
||||
const ctaClass = `j13-att-cta${paidLocked || guestPaid ? " is-primary" : ""}`;
|
||||
const nativeHref = postAttachmentDownloadURL(postId, a.id);
|
||||
let cta: ReactNode;
|
||||
if (guestPaid) {
|
||||
cta = (
|
||||
<Link href={loginHref} className={ctaClass} aria-label={label}>
|
||||
{ctaText}
|
||||
</Link>
|
||||
);
|
||||
} else if (paidLocked) {
|
||||
cta = (
|
||||
<button
|
||||
type="button"
|
||||
className={ctaClass}
|
||||
aria-label={label}
|
||||
aria-busy={busy || undefined}
|
||||
disabled={busyId != null}
|
||||
onClick={() => onPayCta(a)}
|
||||
>
|
||||
{busy ? <Loader2 size={14} className="animate-spin" aria-hidden /> : null}
|
||||
{ctaText}
|
||||
</button>
|
||||
);
|
||||
} else {
|
||||
cta = (
|
||||
<a href={nativeHref} className={ctaClass} aria-label={label} download={a.name}>
|
||||
{unlocked && a.price_points > 0 ? <Check size={14} aria-hidden /> : null}
|
||||
{ctaText}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<li key={a.id}>
|
||||
{guestPaid ? (
|
||||
<Link href={loginHref} className={className} aria-label={label}>
|
||||
{face}
|
||||
</Link>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className={className}
|
||||
aria-label={label}
|
||||
aria-busy={busy || undefined}
|
||||
disabled={busyId != null}
|
||||
onClick={() => onActivate(a)}
|
||||
>
|
||||
{face}
|
||||
</button>
|
||||
)}
|
||||
<article className={className}>
|
||||
<AttachmentMeta a={a} unlocked={unlocked} />
|
||||
{cta}
|
||||
</article>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
@@ -287,7 +289,7 @@ export default function PostAttachments({
|
||||
disabled={busyId != null || !pending}
|
||||
onClick={() => {
|
||||
if (!pending) return;
|
||||
void download(pending);
|
||||
void downloadPaid(pending);
|
||||
}}
|
||||
>
|
||||
{busyId != null ? (
|
||||
|
||||
Reference in New Issue
Block a user