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 ?