From bb116839ae9b830578da8a3163ba8d61ce49d58c Mon Sep 17 00:00:00 2001 From: chenghh-9609 <1159096025@qq.com> Date: Thu, 23 Oct 2025 15:37:22 +0800 Subject: [PATCH] feat: Enhance DatasetDetail component with delete functionality and improved download handling feat: Add automatic data refresh and improved user feedback in DatasetManagementPage fix: Update dataset API to streamline download functionality and improve error handling --- frontend/src/components/ActionDropdown.tsx | 115 +++++ frontend/src/components/AddTagPopover.tsx | 1 + frontend/src/components/CardView.tsx | 82 +++- frontend/src/components/DetailHeader.tsx | 44 +- frontend/src/hooks/useFetchData.ts | 41 +- .../pages/DataCleansing/Create/DragDrop.css | 411 ----------------- .../DataCleansing/Create/DragExample.tsx | 430 ------------------ .../DataManagement/Create/CreateDataset.tsx | 8 +- .../DataManagement/Detail/DatasetDetail.tsx | 29 +- .../Detail/components/ImportConfiguration.tsx | 95 +++- .../Detail/components/Overview.tsx | 5 - .../{hooks => Detail}/useFilesOperation.ts | 4 +- .../DataManagement/Home/DataManagement.tsx | 32 +- .../src/pages/DataManagement/dataset.api.ts | 25 +- .../pages/DataManagement/dataset.const.tsx | 3 +- .../src/pages/DataManagement/hooks/index.ts | 2 - .../DataManagement/hooks/useImportFile.tsx | 61 --- frontend/src/pages/Layout/Sidebar.tsx | 2 +- frontend/src/utils/request.ts | 14 +- 19 files changed, 397 insertions(+), 1007 deletions(-) create mode 100644 frontend/src/components/ActionDropdown.tsx delete mode 100644 frontend/src/pages/DataCleansing/Create/DragDrop.css delete mode 100644 frontend/src/pages/DataCleansing/Create/DragExample.tsx rename frontend/src/pages/DataManagement/{hooks => Detail}/useFilesOperation.ts (97%) delete mode 100644 frontend/src/pages/DataManagement/hooks/index.ts delete mode 100644 frontend/src/pages/DataManagement/hooks/useImportFile.tsx diff --git a/frontend/src/components/ActionDropdown.tsx b/frontend/src/components/ActionDropdown.tsx new file mode 100644 index 0000000..ae4e039 --- /dev/null +++ b/frontend/src/components/ActionDropdown.tsx @@ -0,0 +1,115 @@ +import { Dropdown, Popconfirm, Button, Space } from "antd"; +import { EllipsisOutlined } from "@ant-design/icons"; +import { useState } from "react"; + +interface ActionItem { + key: string; + label: string; + icon?: React.ReactNode; + danger?: boolean; + confirm?: { + title: string; + description?: string; + okText?: string; + cancelText?: string; + }; +} + +interface ActionDropdownProps { + actions?: ActionItem[]; + onAction?: (key: string, action: ActionItem) => void; + placement?: + | "bottomRight" + | "topLeft" + | "topCenter" + | "topRight" + | "bottomLeft" + | "bottomCenter" + | "top" + | "bottom"; +} + +const ActionDropdown = ({ + actions = [], + onAction, + placement = "bottomRight", +}: ActionDropdownProps) => { + const [open, setOpen] = useState(false); + const handleActionClick = (action: ActionItem) => { + if (action.confirm) { + // 如果有确认框,不立即执行,等待确认 + return; + } + // 执行操作 + onAction?.(action.key, action); + // 如果没有确认框,则立即关闭 Dropdown + setOpen(false); + }; + + const dropdownContent = ( +
+ + {actions.map((action) => { + if (action.confirm) { + return ( + { + onAction?.(action.key, action); + setOpen(false); + }} + okText={action.confirm.okText || "确定"} + cancelText={action.confirm.cancelText || "取消"} + okType={action.danger ? "danger" : "primary"} + styles={{ root: { zIndex: 9999 } }} + > + + + ); + } + + return ( + + ); + })} + +
+ ); + + return ( + + - )} - - -
- {rightItems.length === 0 ? ( -
-

📥 暂无进行中的任务

- 从左侧拖拽项目过来开始工作 -
- ) : ( - rightItems.map((item, index) => ( -
handleDragStart(e, item, "right")} - onDragEnd={handleDragEnd} - onDragOver={(e) => handleItemDragOver(e, item.id)} - onDragLeave={handleItemDragLeave} - onDrop={(e) => handleDropToRightItem(e, item.id)} - style={{ "--item-color": item.color }} - > -
- {index + 1} - {getTypeIcon(item.type)} -
- {item.title} - - {getPriorityLabel(item.priority).label} - -
-
-
- ⋮⋮ -
-
- )) - )} -
- - - -
-

🎯 操作指南

-
-
- 🎯 -
- 精确插入 -

拖拽时悬停在项目上方或下方选择插入位置

-
-
-
- 🔄 -
- 重新排序 -

在右侧容器内拖拽调整任务顺序

-
-
-
- 📤 -
- 移回待办 -

从右侧拖拽任务回左侧容器

-
-
-
- 🧹 -
- 批量操作 -

使用"清空所有"按钮快速重置

-
-
-
-
- - ); -}; - -export default PreciseDragDrop; diff --git a/frontend/src/pages/DataManagement/Create/CreateDataset.tsx b/frontend/src/pages/DataManagement/Create/CreateDataset.tsx index 9121f83..cd0109d 100644 --- a/frontend/src/pages/DataManagement/Create/CreateDataset.tsx +++ b/frontend/src/pages/DataManagement/Create/CreateDataset.tsx @@ -4,7 +4,7 @@ import { ArrowLeft } from "lucide-react"; import { Button, Form, App } from "antd"; import { Link, useNavigate } from "react-router"; import { createDatasetUsingPost } from "../dataset.api"; -import { DatasetType, DataSource } from "../dataset.model"; +import { DatasetType } from "../dataset.model"; import BasicInformation from "./components/BasicInformation"; export default function DatasetCreate() { @@ -69,7 +69,11 @@ export default function DatasetCreate() {
-
diff --git a/frontend/src/pages/DataManagement/Detail/DatasetDetail.tsx b/frontend/src/pages/DataManagement/Detail/DatasetDetail.tsx index cc93a81..23546d2 100644 --- a/frontend/src/pages/DataManagement/Detail/DatasetDetail.tsx +++ b/frontend/src/pages/DataManagement/Detail/DatasetDetail.tsx @@ -10,11 +10,12 @@ import { import DetailHeader from "@/components/DetailHeader"; import { mapDataset, datasetTypeMap } from "../dataset.const"; import type { Dataset } from "@/pages/DataManagement/dataset.model"; -import { Link, useParams } from "react-router"; -import { useFilesOperation } from "../hooks"; +import { Link, useNavigate, useParams } from "react-router"; +import { useFilesOperation } from "./useFilesOperation"; import { createDatasetTagUsingPost, - downloadFile, + deleteDatasetByIdUsingDelete, + downloadDatasetUsingGet, queryDatasetByIdUsingGet, queryDatasetTagsUsingGet, updateDatasetByIdUsingPut, @@ -43,6 +44,7 @@ const tabList = [ export default function DatasetDetail() { const { id } = useParams(); // 获取动态路由参数 + const navigate = useNavigate(); const [activeTab, setActiveTab] = useState("overview"); const { message } = App.useApp(); const [showEditDialog, setShowEditDialog] = useState(false); @@ -79,10 +81,16 @@ export default function DatasetDetail() { }; const handleDownload = async () => { - await downloadFile(dataset.id); + await downloadDatasetUsingGet(dataset.id); message.success("文件下载成功"); }; + const handleDeleteDataset = async () => { + await deleteDatasetByIdUsingDelete(dataset.id); + navigate("/data/management"); + message.success("数据集删除成功"); + }; + useEffect(() => { const refreshDataset = () => { fetchDataset(); @@ -166,11 +174,16 @@ export default function DatasetDetail() { key: "delete", label: "删除", danger: true, - icon: , - onClick: () => { - console.log("delete dataset"); + confirm: { + title: "确认删除该数据集?", + description: "删除后该数据集将无法恢复,请谨慎操作。", + okText: "删除", + cancelText: "取消", + okType: "danger", }, - } + icon: , + onClick: handleDeleteDataset, + }, ]; return ( diff --git a/frontend/src/pages/DataManagement/Detail/components/ImportConfiguration.tsx b/frontend/src/pages/DataManagement/Detail/components/ImportConfiguration.tsx index a35deed..ebb710f 100644 --- a/frontend/src/pages/DataManagement/Detail/components/ImportConfiguration.tsx +++ b/frontend/src/pages/DataManagement/Detail/components/ImportConfiguration.tsx @@ -1,10 +1,21 @@ -import { Select, Input, Form, Radio, Modal, Button } from "antd"; +import { + Select, + Input, + Form, + Radio, + Modal, + Button, + App, + UploadFile, +} from "antd"; +import { InboxOutlined } from "@ant-design/icons"; import { dataSourceOptions } from "../../dataset.const"; import { Dataset, DataSource } from "../../dataset.model"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { queryTasksUsingGet } from "@/pages/DataCollection/collection.apis"; -import { useImportFile } from "../../hooks"; import { updateDatasetByIdUsingPut } from "../../dataset.api"; +import { sliceFile } from "@/utils/file.util"; +import Dragger from "antd/es/upload/Dragger"; export default function ImportConfiguration({ data, @@ -15,16 +26,52 @@ export default function ImportConfiguration({ data?: Dataset; open: boolean; onClose: () => void; - onRefresh?: () => void; + onRefresh?: (showMessage?: boolean) => void; }) { + const { message } = App.useApp(); const [form] = Form.useForm(); const [collectionOptions, setCollectionOptions] = useState([]); const [importConfig, setImportConfig] = useState({ source: DataSource.UPLOAD, }); - const { importFileRender, handleUpload } = useImportFile(); - // 获取归集任务列表 + const [fileList, setFileList] = useState([]); + const fileSliceList = useMemo(() => { + const sliceList = fileList.map((file) => { + const slices = sliceFile(file); + return { originFile: file, slices, name: file.name, size: file.size }; + }); + return sliceList; + }, [fileList]); + + // 本地上传文件相关逻辑 + + const resetFiles = () => { + setFileList([]); + }; + + const handleUpload = async (dataset: Dataset) => { + const formData = new FormData(); + fileList.forEach((file) => { + formData.append("file", file); + }); + window.dispatchEvent( + new CustomEvent("upload:dataset", { + detail: { dataset, files: fileSliceList }, + }) + ); + resetFiles(); + }; + + const handleBeforeUpload = (_, files: UploadFile[]) => { + setFileList([...fileList, ...files]); + return false; + }; + + const handleRemoveFile = (file: UploadFile) => { + setFileList((prev) => prev.filter((f) => f.uid !== file.uid)); + }; + const fetchCollectionTasks = async () => { try { const res = await queryTasksUsingGet({ page: 0, size: 100 }); @@ -40,6 +87,8 @@ export default function ImportConfiguration({ const resetState = () => { form.resetFields(); + setFileList([]); + form.setFieldsValue({ files: null }); setImportConfig({ source: DataSource.UPLOAD }); }; @@ -51,13 +100,16 @@ export default function ImportConfiguration({ ...importConfig, }); } - resetState(); - onRefresh?.(); + message.success("数据已更新"); + onRefresh?.(false); onClose(); }; useEffect(() => { - if (open) fetchCollectionTasks(); + if (open) { + resetState(); + fetchCollectionTasks(); + } }, [open]); return ( @@ -65,12 +117,19 @@ export default function ImportConfiguration({ title="导入数据" open={open} width={600} - onCancel={onClose} + onCancel={() => { + onClose(); + resetState(); + }} maskClosable={false} footer={ <> - @@ -132,6 +191,7 @@ export default function ImportConfiguration({ )} + {/* obs import */} {importConfig?.source === DataSource.OBS && (
@@ -185,7 +245,18 @@ export default function ImportConfiguration({ }, ]} > - {importFileRender()} + +

+ +

+

本地文件上传

+

拖拽文件到此处或点击选择文件

+
)} diff --git a/frontend/src/pages/DataManagement/Detail/components/Overview.tsx b/frontend/src/pages/DataManagement/Detail/components/Overview.tsx index ed3754b..f5466e7 100644 --- a/frontend/src/pages/DataManagement/Detail/components/Overview.tsx +++ b/frontend/src/pages/DataManagement/Detail/components/Overview.tsx @@ -82,11 +82,6 @@ export default function Overview({ dataset, filesOperation }) { label: "更新时间", children: dataset.updatedAt, }, - { - key: "dataSource", - label: "数据源", - children: dataset.dataSource || "未知", - }, { key: "description", label: "描述", diff --git a/frontend/src/pages/DataManagement/hooks/useFilesOperation.ts b/frontend/src/pages/DataManagement/Detail/useFilesOperation.ts similarity index 97% rename from frontend/src/pages/DataManagement/hooks/useFilesOperation.ts rename to frontend/src/pages/DataManagement/Detail/useFilesOperation.ts index 17ca811..1c58b19 100644 --- a/frontend/src/pages/DataManagement/hooks/useFilesOperation.ts +++ b/frontend/src/pages/DataManagement/Detail/useFilesOperation.ts @@ -6,7 +6,7 @@ import { App } from "antd"; import { useState } from "react"; import { deleteDatasetFileUsingDelete, - downloadFile, + downloadFileByIdUsingGet, exportDatasetUsingPost, queryDatasetFilesUsingGet, } from "../dataset.api"; @@ -51,7 +51,7 @@ export function useFilesOperation(dataset: Dataset) { const handleDownloadFile = async (file: DatasetFile) => { console.log("批量下载文件:", selectedFiles); // 实际导出逻辑 - await downloadFile(dataset.id, file.id, file.fileName); + await downloadFileByIdUsingGet(dataset.id, file.id, file.fileName); // 假设导出成功 message.success({ content: `已导出 1 个文件`, diff --git a/frontend/src/pages/DataManagement/Home/DataManagement.tsx b/frontend/src/pages/DataManagement/Home/DataManagement.tsx index 76430ed..ec81a64 100644 --- a/frontend/src/pages/DataManagement/Home/DataManagement.tsx +++ b/frontend/src/pages/DataManagement/Home/DataManagement.tsx @@ -119,7 +119,13 @@ export default function DatasetManagementPage() { fetchData, setSearchParams, handleFiltersChange, - } = useFetchData(queryDatasetsUsingGet, mapDataset); + } = useFetchData( + queryDatasetsUsingGet, + mapDataset, + 30000, // 30秒轮询间隔 + true, // 自动刷新 + [fetchStatistics] // 额外的轮询函数 + ); const handleDownloadDataset = async (dataset: Dataset) => { await downloadDatasetUsingGet(dataset.id, dataset.name); @@ -138,9 +144,12 @@ export default function DatasetManagementPage() { setShowUploadDialog(true); }; - useEffect(() => { - fetchStatistics(); - }, []); + const handleRefresh = async (showMessage = true) => { + await fetchData(); + if (showMessage) { + message.success("数据已刷新"); + } + }; const operations = [ { @@ -173,6 +182,13 @@ export default function DatasetManagementPage() { key: "delete", label: "删除", danger: true, + confirm: { + title: "确认删除该数据集?", + description: "删除后该数据集将无法恢复,请谨慎操作。", + okText: "删除", + cancelText: "取消", + okType: "danger", + }, icon: , onClick: (item: Dataset) => handleDeleteDataset(item.id), }, @@ -306,7 +322,7 @@ export default function DatasetManagementPage() { {/* Header */}

数据管理

-
+
{/* tasks */} {viewMode === "card" ? renderCardView() : renderListView()}
); diff --git a/frontend/src/pages/DataManagement/dataset.api.ts b/frontend/src/pages/DataManagement/dataset.api.ts index 59f8a0e..e2cb656 100644 --- a/frontend/src/pages/DataManagement/dataset.api.ts +++ b/frontend/src/pages/DataManagement/dataset.api.ts @@ -1,4 +1,4 @@ -import { get, post, put, del, download, upload } from "@/utils/request"; +import { get, post, put, del, download } from "@/utils/request"; // 数据集统计接口 export function getDatasetStatisticsUsingGet() { @@ -35,15 +35,8 @@ export function deleteDatasetByIdUsingDelete(id: string | number) { } // 下载数据集 -export function downloadDatasetUsingGet( - id: string | number, - filename?: string -) { - return download( - `/api/data-management/datasets/${id}/download`, - null, - filename - ); +export function downloadDatasetUsingGet(id: string | number) { + return download(`/api/data-management/datasets/${id}/files/download`); } // 验证数据集 @@ -61,8 +54,16 @@ export function uploadDatasetFileUsingPost(id: string | number, data: any) { return post(`/api/data-management/datasets/${id}/files`, data); } -export function downloadFile(id: string | number) { - return download(`/api/data-management/datasets/${id}/files/download`); +export function downloadFileByIdUsingGet( + id: string | number, + fileId: string | number, + fileName: string +) { + return download( + `/api/data-management/datasets/${id}/files/${fileId}/download`, + null, + fileName + ); } // 删除数据集文件 diff --git a/frontend/src/pages/DataManagement/dataset.const.tsx b/frontend/src/pages/DataManagement/dataset.const.tsx index 4ac6f63..8950506 100644 --- a/frontend/src/pages/DataManagement/dataset.const.tsx +++ b/frontend/src/pages/DataManagement/dataset.const.tsx @@ -12,6 +12,7 @@ import { CloseCircleOutlined, FileOutlined, } from "@ant-design/icons"; +import { AnyObject } from "antd/es/_util/type"; import { FileImage, FileText, @@ -194,7 +195,7 @@ export const dataSourceMap: Record = { export const dataSourceOptions = Object.values(dataSourceMap); -export function mapDataset(dataset: Dataset) { +export function mapDataset(dataset: AnyObject): Dataset { const { icon: IconComponent, iconColor } = datasetTypeMap[dataset?.datasetType] || {}; return { diff --git a/frontend/src/pages/DataManagement/hooks/index.ts b/frontend/src/pages/DataManagement/hooks/index.ts deleted file mode 100644 index dda61af..0000000 --- a/frontend/src/pages/DataManagement/hooks/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { useFilesOperation } from "./useFilesOperation"; -export { useImportFile } from "./useImportFile"; \ No newline at end of file diff --git a/frontend/src/pages/DataManagement/hooks/useImportFile.tsx b/frontend/src/pages/DataManagement/hooks/useImportFile.tsx deleted file mode 100644 index 8b8b7b9..0000000 --- a/frontend/src/pages/DataManagement/hooks/useImportFile.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { Upload, type UploadFile } from "antd"; -import { InboxOutlined } from "@ant-design/icons"; -import { useMemo, useState } from "react"; -import type { Dataset } from "@/pages/DataManagement/dataset.model"; -import { sliceFile } from "@/utils/file.util"; - -const { Dragger } = Upload; - -export const useImportFile = () => { - const [fileList, setFileList] = useState([]); - const fileSliceList = useMemo(() => { - const sliceList = fileList.map((file) => { - const slices = sliceFile(file); - return { originFile: file, slices, name: file.name, size: file.size }; - }); - return sliceList; - }, [fileList]); - - const resetFiles = () => { - setFileList([]); - }; - - const handleUpload = async (dataset: Dataset) => { - const formData = new FormData(); - fileList.forEach((file) => { - formData.append("file", file); - }); - window.dispatchEvent( - new CustomEvent("upload:dataset", { - detail: { dataset, files: fileSliceList }, - }) - ); - resetFiles(); - }; - - const handleBeforeUpload = (_, files: UploadFile[]) => { - setFileList([...fileList, ...files]); - return false; - }; - - const handleRemoveFile = (file: UploadFile) => { - setFileList((prev) => prev.filter((f) => f.uid !== file.uid)); - }; - - const importFileRender = () => ( - -

- -

-

本地文件上传

-

拖拽文件到此处或点击选择文件

-
- ); - - return { fileList, resetFiles, handleUpload, importFileRender }; -}; diff --git a/frontend/src/pages/Layout/Sidebar.tsx b/frontend/src/pages/Layout/Sidebar.tsx index a63a6ed..f881d80 100644 --- a/frontend/src/pages/Layout/Sidebar.tsx +++ b/frontend/src/pages/Layout/Sidebar.tsx @@ -71,7 +71,7 @@ const AsiderAndHeaderLayout = () => {
- ModelEngine + DataMate )}