feat(attach): 付费附件游客跳登录,积分数独立价签并升级下载交互

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-17 06:04:29 +08:00
parent c44b0efa7d
commit dfc0beafe9
5 changed files with 653 additions and 39 deletions

View File

@@ -1066,6 +1066,45 @@ export function postAttachmentDownloadURL(postId: number | string, attId: number
return `/api/posts/${postId}/attachments/${attId}/download`;
}
function filenameFromDisposition(header: string | null, fallback: string): string {
if (!header) return fallback;
const star = /filename\*\s*=\s*UTF-8''([^;]+)/i.exec(header);
if (star?.[1]) {
try {
return decodeURIComponent(star[1]);
} catch {
return fallback;
}
}
const quoted = /filename\s*=\s*"([^"]+)"/i.exec(header);
if (quoted?.[1]) return quoted[1];
const plain = /filename\s*=\s*([^;]+)/i.exec(header);
if (plain?.[1]) return plain[1].trim();
return fallback;
}
/** 下载帖子附件:走 fetch 以便处理 401/402 JSON,避免整页打开错误文档 */
export async function apiDownloadPostAttachment(
postId: number | string,
attId: number,
fallbackName: string
): Promise<{ blob?: Blob; filename?: string; error?: string; status?: number }> {
const res = await fetchWithRefresh(postAttachmentDownloadURL(postId, attId), {
method: "GET",
cache: "no-store",
headers: clientHeaders(),
});
if (!res.ok) {
const data = (await res.json().catch(() => ({}))) as { error?: string };
return { error: data.error || "下载失败", status: res.status };
}
const blob = await res.blob();
return {
blob,
filename: filenameFromDisposition(res.headers.get("Content-Disposition"), fallbackName),
};
}
export async function apiVotePoll(postId: string, options: number[]) {
const res = await fetchWithRefresh(`/api/posts/${postId}/poll/vote`, {
method: "POST",