/** 帖子附件客户端校验(与后端 setting / fileallow 对齐;安全边界仍在 Go) */ export const FILE_MAX_MB_DEFAULT = 20; export const FILE_MAX_COUNT_DEFAULT = 10; export function normalizeExt(nameOrExt: string): string { const base = nameOrExt.includes("/") ? nameOrExt.split(/[/\\]/).pop() || "" : nameOrExt; const i = base.lastIndexOf("."); const ext = (i >= 0 ? base.slice(i + 1) : base).trim().toLowerCase(); return ext.replace(/[^a-z0-9]/g, "").slice(0, 16); } export function buildAcceptAttr(exts: string[], limit: boolean): string | undefined { if (!limit) return undefined; if (!exts.length) return undefined; return exts.map((e) => `.${e}`).join(","); } export function isAllowedPostFileName( name: string, opts: { limit: boolean; exts: string[] } ): boolean { if (!opts.limit) return true; const ext = normalizeExt(name); if (!ext) return false; return opts.exts.includes(ext); } export function formatMB(n: number): string { return `${n}MB`; } export type PostFileRejectReason = "size" | "type" | "count" | null; export function rejectPostFile( file: File, opts: { limit: boolean; exts: string[]; maxMB: number; maxCount: number; currentCount: number; } ): { reason: PostFileRejectReason; message?: string } { if (opts.currentCount >= opts.maxCount) { return { reason: "count", message: `最多 ${opts.maxCount} 个附件` }; } const maxBytes = opts.maxMB * 1024 * 1024; if (file.size > maxBytes) { return { reason: "size", message: `附件不能超过 ${opts.maxMB}MB` }; } if (!isAllowedPostFileName(file.name, opts)) { return { reason: "type", message: "不支持该附件格式" }; } return { reason: null }; }