feat(attach): 附件可配置限额、流式落盘与分阶段上传

管理端可配类型/体积;上传改流式避免整文件进内存;发帖先选再传并拦截未上传提交;提高 Next proxy 体积上限。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-17 07:33:16 +08:00
parent 6cc434f103
commit 61bf9fb48c
24 changed files with 2513 additions and 307 deletions

View File

@@ -9,6 +9,7 @@ import {
postAttachmentDownloadURL,
type PostAttachment,
} from "@/lib/api";
import { fileExtBadge, fileKind } from "@/lib/fileKind";
import { toast } from "@/lib/toast";
import Modal from "@/components/Modal";
@@ -22,52 +23,6 @@ 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";
}
/** 把已拿到的文件交给浏览器下载栏(付费首次须先 fetch 才能处理 402) */
function triggerBrowserDownload(blob: Blob, filename: string) {
const url = URL.createObjectURL(blob);
@@ -92,7 +47,7 @@ function AttachmentMeta({
return (
<>
<span className="j13-att-type" data-kind={fileKind(a.name, a.mime)} aria-hidden>
{fileExt(a.name)}
{fileExtBadge(a.name)}
</span>
<span className="j13-att-main">
<span className="j13-att-name">{a.name}</span>
@@ -312,7 +267,7 @@ export default function PostAttachments({
data-kind={fileKind(pending.name, pending.mime)}
aria-hidden
>
{fileExt(pending.name)}
{fileExtBadge(pending.name)}
</span>
<div className="min-w-0">
<p className="j13-att-pay-name">{pending.name}</p>