diff --git a/frontend/src/pages/DataManagement/Detail/DatasetDetail.tsx b/frontend/src/pages/DataManagement/Detail/DatasetDetail.tsx
index ea18d74e..82c89951 100644
--- a/frontend/src/pages/DataManagement/Detail/DatasetDetail.tsx
+++ b/frontend/src/pages/DataManagement/Detail/DatasetDetail.tsx
@@ -1,13 +1,13 @@
import { useEffect, useMemo, useState } from "react";
import { Breadcrumb, App, Tabs, Drawer, Descriptions } from "antd";
import {
- ReloadOutlined,
- DownloadOutlined,
- UploadOutlined,
- EditOutlined,
- DeleteOutlined,
- InfoCircleOutlined,
-} from "@ant-design/icons";
+ Info,
+ Edit,
+ Upload,
+ Download,
+ RefreshCw,
+ Trash2,
+} from "lucide-react";
import DetailHeader from "@/components/DetailHeader";
import { getDatasetTypeMap, mapDataset } from "../dataset.const";
import type { Dataset } from "@/pages/DataManagement/dataset.model";
@@ -140,7 +140,7 @@ export default function DatasetDetail() {
{
key: "detail",
label: t("dataManagement.actions.detail"),
- icon: ,
+ icon: ,
onClick: () => {
setShowDetailDrawer(true);
},
@@ -148,7 +148,7 @@ export default function DatasetDetail() {
{
key: "edit",
label: t("dataManagement.actions.edit"),
- icon: ,
+ icon: ,
onClick: () => {
setShowEditDialog(true);
},
@@ -157,26 +157,19 @@ export default function DatasetDetail() {
{
key: "upload",
label: t("dataManagement.actions.importData"),
- icon: ,
+ icon: ,
onClick: () => setShowUploadDialog(true),
},
{
key: "export",
label: t("dataManagement.actions.export"),
- icon: ,
- // isDropdown: true,
- // items: [
- // { key: "alpaca", label: "Alpaca 格式", icon: },
- // { key: "jsonl", label: "JSONL 格式", icon: },
- // { key: "csv", label: "CSV 格式", icon: },
- // { key: "coco", label: "COCO 格式", icon: },
- // ],
+ icon: ,
onClick: () => handleDownload(),
},
{
key: "refresh",
label: t("dataManagement.actions.refresh"),
- icon: ,
+ icon: ,
onClick: handleRefresh,
},
{
@@ -190,7 +183,7 @@ export default function DatasetDetail() {
cancelText: t("dataManagement.confirm.deleteCancel"),
okType: "danger",
},
- icon: ,
+ icon: ,
onClick: handleDeleteDataset,
},
];
diff --git a/frontend/src/utils/request.ts b/frontend/src/utils/request.ts
index 5e5aff21..74371466 100644
--- a/frontend/src/utils/request.ts
+++ b/frontend/src/utils/request.ts
@@ -388,6 +388,46 @@ class Request {
return this.request(fullURL, config);
}
+ /**
+ * 从 Content-Disposition 头中解析文件名
+ */
+ parseContentDisposition(contentDisposition) {
+ if (!contentDisposition) return null;
+
+ // 匹配 filename="..." 或 filename=...
+ const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
+ const matches = filenameRegex.exec(contentDisposition);
+
+ if (matches && matches[1]) {
+ let fileName = matches[1].replace(/['"]/g, '');
+ // 处理 URL 编码的文件名(如中文文件名)
+ try {
+ fileName = decodeURIComponent(fileName);
+ } catch {
+ // 如果解码失败,使用原始文件名
+ }
+ return fileName;
+ }
+
+ return null;
+ }
+
+ /**
+ * 获取当前认证 token
+ */
+ getAuthToken() {
+ const session = localStorage.getItem("session");
+ if (session) {
+ try {
+ const sessionData = JSON.parse(session);
+ return sessionData.token;
+ } catch {
+ return null;
+ }
+ }
+ return null;
+ }
+
/**
* 下载文件
* @param {string} url - 请求URL
@@ -397,85 +437,56 @@ class Request {
* @param {object} options - 额外的fetch选项,包括showLoading, onDownloadProgress
*/
async download(url, params = null, filename = "", action = "download", options = {}) {
- const fullURL = this.buildURL(url, params);
-
- const config = {
- method: "GET",
- responseType: "blob",
- ...options,
- };
-
- // 执行请求拦截器
- const processedConfig = await this.executeRequestInterceptors(config);
-
- let blob;
- let name = filename;
-
- // 如果需要下载进度监听,使用XMLHttpRequest
- if (config.onDownloadProgress) {
- const xhrResponse = await this.createXHRWithProgress(
- fullURL,
- { ...processedConfig, responseType: "blob" },
- null,
- config.onDownloadProgress
- );
-
- if (xhrResponse.status < 200 || xhrResponse.status >= 300) {
- throw new Error(`HTTP error! status: ${xhrResponse.status}`);
- }
+ // 对于预览,使用 fetch + blob 方式
+ if (action === "preview") {
+ const fullURL = this.buildURL(url, params);
+ const config = {
+ method: "GET",
+ ...options,
+ };
- blob = xhrResponse.xhr.response;
- name =
- name ||
- xhrResponse.headers.get("Content-Disposition")?.split("filename=")[1] ||
- "download";
- } else {
- // 使用fetch
+ const processedConfig = await this.executeRequestInterceptors(config);
const response = await fetch(fullURL, processedConfig);
- // 文件下载不需要执行响应拦截器(因为响应是二进制数据,不是JSON)
+ // 文件预览不需要执行响应拦截器(因为响应是二进制数据,不是JSON)
// 直接检查响应状态
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
- blob = await response.blob();
- name =
- name ||
- response.headers.get("Content-Disposition")?.split("filename=")[1] ||
- `download_${Date.now()}`;
- }
-
- if (action === "download") {
- // 创建下载链接
- const downloadUrl = window.URL.createObjectURL(blob);
- const link = document.createElement("a");
- link.href = downloadUrl;
- link.download = filename ?? name;
-
- // 添加到DOM并触发下载
- document.body.appendChild(link);
- link.click();
- document.body.removeChild(link);
-
- // 清理URL对象
- window.URL.revokeObjectURL(downloadUrl);
- } else if (action === "preview") {
- // 预览逻辑 - 返回Blob URL和相关信息
+ const blob = await response.blob();
const blobUrl = window.URL.createObjectURL(blob);
+ const name = filename || this.parseContentDisposition(response.headers.get("Content-Disposition")) || `download_${Date.now()}`;
- // 可以返回更多信息用于预览
return {
blob,
blobUrl,
filename: name,
size: blob.size,
- // 自动清理的钩子
revoke: () => window.URL.revokeObjectURL(blobUrl)
};
}
- return blob;
+ // 对于下载,使用原生 标签 + token 参数
+ if (action === "download") {
+ // 获取 token 并添加到 URL 参数中
+ const token = this.getAuthToken();
+ const downloadParams = token ? { ...params, token } : params;
+ const fullURL = this.buildURL(url, downloadParams);
+
+ // 创建隐藏的 标签触发下载
+ const link = document.createElement("a");
+ link.href = fullURL;
+ link.download = filename || `download_${Date.now()}`;
+ link.style.display = "none";
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+
+ return { success: true };
+ }
+
+ return {};
}
/**