Compare commits

..
Author SHA1 Message Date
ji ruo 542d7e9ca6 fix: preserve UTF-8 filenames for exports 2026-10-30 15:06:37 +08:00
RuoJi6 35a33b20f9 Merge pull request #98 from RuoJi6/codex/fix-image-drag-ghost
修复截图拖动时的原生拖拽重影
2026-10-29 22:13:34 -07:00
2 changed files with 26 additions and 4 deletions

No files matched your search

+4 -1
View File
@@ -2332,7 +2332,10 @@ func (s *Server) findingsExport(w http.ResponseWriter, r *http.Request) {
stamp := now.Format("20060102-150405")
setDownload := func(contentType, filename string) {
w.Header().Set("Content-Type", contentType)
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))
// Keep an ASCII fallback for older clients and provide the real UTF-8
// name through RFC 5987. Browsers otherwise commonly decode Chinese
// names using the platform legacy code page.
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"; filename*=UTF-8''%s`, sanitizeFilename(filename), url.PathEscape(filename)))
}
switch format {
+22 -3
View File
@@ -119,6 +119,24 @@ import type {
SystemMetrics,
} from "@/lib/types";
/**
* Read a download name from Content-Disposition. RFC 5987's filename* is
* preferred because it preserves UTF-8 names; older filename remains the
* fallback for clients that do not understand it.
*/
export function downloadFilename(contentDisposition: string, fallback: string): string {
const encoded = contentDisposition.match(/(?:^|;)\s*filename\*\s*=\s*UTF-8''([^;]+)/i)?.[1];
if (encoded) {
try {
return decodeURIComponent(encoded.replace(/^"|"$/g, ""));
} catch {
// Fall through to the legacy token when a malformed header is received.
}
}
const legacy = contentDisposition.match(/(?:^|;)\s*filename\s*=\s*(?:"([^"]*)"|([^;\s]+))/i);
return (legacy?.[1] ?? legacy?.[2] ?? fallback).trim() || fallback;
}
function getToken(): string | null {
if (typeof window === "undefined") return null;
return localStorage.getItem("artex_token");
@@ -534,6 +552,7 @@ export const api = {
},
workspaceDownload: async (path: string) => {
let blob: Blob;
let contentDisposition = "";
if (MOCK) {
blob = new Blob([`(demo)${path} 的下载内容示例。`], { type: "text/plain" });
} else {
@@ -542,12 +561,13 @@ export const api = {
headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}) },
});
if (!r.ok) throw new Error(`下载失败: ${r.status}`);
contentDisposition = r.headers.get("Content-Disposition") ?? "";
blob = await r.blob();
}
const objUrl = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = objUrl;
a.download = path.split("/").pop() || "download";
a.download = downloadFilename(contentDisposition, path.split("/").pop() || "download");
document.body.appendChild(a);
a.click();
a.remove();
@@ -703,8 +723,7 @@ export const api = {
const blob = await r.blob();
// 文件名优先取后端 Content-Disposition,取不到则用默认名。
const disp = r.headers.get("Content-Disposition") ?? "";
const m = disp.match(/filename="?([^"]+)"?/);
const filename = m?.[1] ?? `findings-export`;
const filename = downloadFilename(disp, `findings-export`);
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;