build(release): 加固 v0.1.0 桌面发布链路 (#3)
Client Quality Gates / Shared client and Web (push) Has been cancelled
Client Quality Gates / macOS Desktop (push) Has been cancelled

This commit is contained in:
chengchengzhou7
2026-07-17 09:36:16 +08:00
committed by GitHub
parent 1837ceff58
commit 4863ade45b
16 changed files with 895 additions and 323 deletions
+14 -1
View File
@@ -1,5 +1,18 @@
import { describe, expect, it } from "vitest";
import { getContentDispositionFilename } from "./contentDisposition";
import { getContentDispositionFilename, getHttpHeaderString } from "./contentDisposition";
describe("getHttpHeaderString", () => {
it("keeps string header values and joins string arrays", () => {
expect(getHttpHeaderString("application/pdf")).toBe("application/pdf");
expect(getHttpHeaderString(["attachment", "filename=protocol.pdf"])).toBe("attachment, filename=protocol.pdf");
});
it("rejects non-string Axios header values", () => {
expect(getHttpHeaderString(200)).toBeUndefined();
expect(getHttpHeaderString(true)).toBeUndefined();
expect(getHttpHeaderString({ value: "application/pdf" })).toBeUndefined();
});
});
describe("getContentDispositionFilename", () => {
it("prefers the UTF-8 filename even when the ASCII fallback appears first", () => {
+8
View File
@@ -1,3 +1,11 @@
export const getHttpHeaderString = (value: unknown): string | undefined => {
if (typeof value === "string") return value;
if (Array.isArray(value) && value.every((item) => typeof item === "string")) {
return value.join(", ");
}
return undefined;
};
export const getContentDispositionFilename = (header?: string | null): string | null => {
if (!header) return null;
const encoded = /filename\*\s*=\s*UTF-8''([^;]+)/i.exec(header)?.[1];
@@ -381,7 +381,7 @@ import StateLoading from "../../components/StateLoading.vue";
import { onDesktopRefreshCurrentView } from "../../composables/useDesktopRefresh";
import { openFileWithFeedback, pickFilesWithFeedback, saveFileWithFeedback } from "../../utils/fileTaskFeedback";
import { getApiErrorMessage } from "../../utils/apiErrorMessage";
import { getContentDispositionFilename } from "../../utils/contentDisposition";
import { getContentDispositionFilename, getHttpHeaderString } from "../../utils/contentDisposition";
import { prepareSaveFile } from "../../runtime";
import { detectFilePreviewKind, ONLYOFFICE_FILE_EXTENSIONS } from "../../utils/officePreview";
@@ -803,7 +803,8 @@ const previewVersion = async (version: DocumentVersion) => {
previewLoading.value = true;
try {
const response = await downloadDocumentVersion(version.id);
const contentType = response.headers?.["content-type"] || version.mime_type || "application/octet-stream";
const contentType =
getHttpHeaderString(response.headers?.["content-type"]) || version.mime_type || "application/octet-stream";
const blob = new Blob([response.data], { type: contentType });
previewObjectUrl.value = URL.createObjectURL(blob);
previewUrl.value = previewObjectUrl.value;
@@ -823,8 +824,9 @@ const downloadVersion = async (version: DocumentVersion) => {
return;
}
const response = await downloadDocumentVersion(version.id);
const contentType = response.headers?.["content-type"] || "application/octet-stream";
const filename = getContentDispositionFilename(response.headers?.["content-disposition"]) || suggestedName;
const contentType = getHttpHeaderString(response.headers?.["content-type"]) || "application/octet-stream";
const filename =
getContentDispositionFilename(getHttpHeaderString(response.headers?.["content-disposition"])) || suggestedName;
const blob = new Blob([response.data], { type: contentType });
await saveFileWithFeedback({ suggestedName: filename, mimeType: contentType, data: blob }, {}, destination);
} catch (e: any) { ElMessage.error(await getApiErrorMessage(e, TEXT.common.messages.downloadFailed)); }
@@ -840,10 +842,11 @@ const openVersion = async (version: DocumentVersion) => {
}
try {
const contentType = response.headers?.["content-type"] || "application/octet-stream";
const filename = getContentDispositionFilename(response.headers?.["content-disposition"])
|| getVersionFileName(version)
|| `document-${version.version_no || version.id}.bin`;
const contentType = getHttpHeaderString(response.headers?.["content-type"]) || "application/octet-stream";
const filename =
getContentDispositionFilename(getHttpHeaderString(response.headers?.["content-disposition"])) ||
getVersionFileName(version) ||
`document-${version.version_no || version.id}.bin`;
await openFileWithFeedback({
suggestedName: filename,
mimeType: contentType,
@@ -474,6 +474,7 @@ import { isApiPermissionAllowed } from "../../utils/apiPermissionValue";
import { displayDateTime } from "../../utils/display";
import { getProjectRole, isSystemAdmin } from "../../utils/roles";
import { useDrawerDirtyGuard } from "../../utils/drawerDirtyGuard";
import { getContentDispositionFilename, getHttpHeaderString } from "../../utils/contentDisposition";
interface MonitoringIssueRow {
id: string;
@@ -937,13 +938,6 @@ const removeIssue = async (row: MonitoringIssueRow) => {
}
};
const getFilename = (header?: string | null) => {
if (!header) return null;
const match = /filename\*=UTF-8''([^;]+)|filename="?([^\";]+)"?/i.exec(header);
if (!match) return null;
return decodeURIComponent(match[1] || match[2] || "");
};
const handleExportExcel = async () => {
const studyId = study.currentStudy?.id;
if (!studyId || !canListIssues.value) return;
@@ -975,8 +969,9 @@ const handleExportExcel = async () => {
}
const response = await exportMonitoringVisitIssues(studyId, params);
const contentType = response.headers?.["content-type"] || "application/octet-stream";
const filename = getFilename(response.headers?.["content-disposition"]) || "监查访视问题.xlsx";
const contentType = getHttpHeaderString(response.headers?.["content-type"]) || "application/octet-stream";
const filename =
getContentDispositionFilename(getHttpHeaderString(response.headers?.["content-disposition"])) || "监查访视问题.xlsx";
const blob = new Blob([response.data], { type: contentType });
await saveFileWithFeedback(
{ suggestedName: filename, mimeType: contentType, data: blob },