diff --git a/frontend/src/pages/DataManagement/Detail/DatasetDetail.tsx b/frontend/src/pages/DataManagement/Detail/DatasetDetail.tsx index aa3b729..b3d106a 100644 --- a/frontend/src/pages/DataManagement/Detail/DatasetDetail.tsx +++ b/frontend/src/pages/DataManagement/Detail/DatasetDetail.tsx @@ -1,195 +1,210 @@ -import { useEffect, useMemo, useRef, useState } from "react"; -import { Breadcrumb, App, Tabs, Table, Tag } from "antd"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { Breadcrumb, App, Tabs, Table, Tag } from "antd"; import { - ReloadOutlined, - DownloadOutlined, - EditOutlined, - DeleteOutlined, - PlusOutlined, -} from "@ant-design/icons"; + ReloadOutlined, + DownloadOutlined, + EditOutlined, + DeleteOutlined, + PlusOutlined, +} from "@ant-design/icons"; import DetailHeader from "@/components/DetailHeader"; import { mapDataset, datasetTypeMap } from "../dataset.const"; import type { Dataset } from "@/pages/DataManagement/dataset.model"; import { Link, useNavigate, useParams } from "react-router"; import { useFilesOperation } from "./useFilesOperation"; -import { - createDatasetTagUsingPost, - deleteDatasetByIdUsingDelete, - downloadDatasetUsingGet, - queryDatasetByIdUsingGet, - queryDatasetsUsingGet, - queryDatasetTagsUsingGet, - querySimilarDatasetsUsingGet, - updateDatasetByIdUsingPut, -} from "../dataset.api"; +import { + createDatasetTagUsingPost, + deleteDatasetByIdUsingDelete, + downloadDatasetUsingGet, + queryDatasetByIdUsingGet, + queryDatasetsUsingGet, + queryDatasetTagsUsingGet, + querySimilarDatasetsUsingGet, + updateDatasetByIdUsingPut, +} from "../dataset.api"; import DataQuality from "./components/DataQuality"; import DataLineageFlow from "./components/DataLineageFlow"; import Overview from "./components/Overview"; import { Activity, Clock, File, FileType } from "lucide-react"; import EditDataset from "../Create/EditDataset"; -import ImportConfiguration from "./components/ImportConfiguration"; - -const SIMILAR_DATASET_LIMIT = 4; - -export default function DatasetDetail() { - const { id } = useParams(); // 获取动态路由参数 - const navigate = useNavigate(); - const [activeTab, setActiveTab] = useState("overview"); - const { message } = App.useApp(); - const [showEditDialog, setShowEditDialog] = useState(false); - - const [dataset, setDataset] = useState({} as Dataset); - const [parentDataset, setParentDataset] = useState(null); - const [childDatasets, setChildDatasets] = useState([]); - const [childDatasetsLoading, setChildDatasetsLoading] = useState(false); - const [similarDatasets, setSimilarDatasets] = useState([]); - const [similarDatasetsLoading, setSimilarDatasetsLoading] = useState(false); - const [similarTagNames, setSimilarTagNames] = useState([]); - const similarRequestRef = useRef(0); - const filesOperation = useFilesOperation(dataset); +import ImportConfiguration from "./components/ImportConfiguration"; +import CardView from "@/components/CardView"; - const [showUploadDialog, setShowUploadDialog] = useState(false); - const normalizeTagNames = ( - tags?: Array - ) => { - if (!tags || tags.length === 0) { - return []; - } - const names = tags - .map((tag) => (typeof tag === "string" ? tag : tag?.name)) - .filter((name): name is string => !!name && name.trim().length > 0) - .map((name) => name.trim()); - return Array.from(new Set(names)); - }; - const fetchSimilarDatasets = async (currentDataset: Dataset) => { - const requestId = similarRequestRef.current + 1; - similarRequestRef.current = requestId; - if (!currentDataset?.id) { - setSimilarDatasets([]); - setSimilarTagNames([]); - setSimilarDatasetsLoading(false); - return; - } - const tagNames = normalizeTagNames( - currentDataset.tags as Array - ); - setSimilarTagNames(tagNames); - setSimilarDatasets([]); - if (tagNames.length === 0) { - setSimilarDatasetsLoading(false); - return; - } - setSimilarDatasetsLoading(true); - try { - const { data } = await querySimilarDatasetsUsingGet(currentDataset.id, { - limit: SIMILAR_DATASET_LIMIT, - }); - if (similarRequestRef.current !== requestId) { - return; - } - const list = Array.isArray(data) ? data : []; - setSimilarDatasets(list.map((item) => mapDataset(item))); - } catch (error) { - console.error("Failed to fetch similar datasets:", error); - } finally { - if (similarRequestRef.current === requestId) { - setSimilarDatasetsLoading(false); - } - } - }; - const navigateItems = useMemo(() => { - const items = [ - { - title: 数据管理, - }, - ]; - if (parentDataset) { - items.push({ - title: ( - - {parentDataset.name} - - ), - }); - } - items.push({ - title: dataset.name || "数据集详情", - }); - return items; - }, [dataset, parentDataset]); - const tabList = useMemo(() => { - const items = [ - { - key: "overview", - label: "概览", - }, - ]; - if (!dataset?.parentDatasetId) { - items.push({ - key: "children", - label: "关联数据集", - }); - } - return items; - }, [dataset?.parentDatasetId]); - const handleCreateChildDataset = () => { - if (!dataset?.id) { - return; - } - navigate("/data/management/create", { - state: { parentDatasetId: dataset.id }, - }); - }; - const fetchChildDatasets = async (parentId?: string) => { - if (!parentId) { - setChildDatasets([]); - return; - } - setChildDatasetsLoading(true); - try { - const { data: res } = await queryDatasetsUsingGet({ - parentDatasetId: parentId, - page: 1, - size: 1000, - }); - const list = res?.content || res?.data || []; - setChildDatasets(list.map((item) => mapDataset(item))); - } finally { - setChildDatasetsLoading(false); - } - }; - const fetchDataset = async () => { - if (!id) { - return; - } - const { data } = await queryDatasetByIdUsingGet(id); - const mapped = mapDataset(data); - setDataset(mapped); - fetchSimilarDatasets(mapped); - if (data?.parentDatasetId) { - const { data: parentData } = await queryDatasetByIdUsingGet( - data.parentDatasetId - ); - setParentDataset(mapDataset(parentData)); - setChildDatasets([]); - } else { - setParentDataset(null); - await fetchChildDatasets(data?.id); - } - }; +const SIMILAR_DATASET_LIMIT = 4; +const SIMILAR_TAGS_PREVIEW_LIMIT = 3; - useEffect(() => { - if (!id) { - return; - } - fetchDataset(); - filesOperation.fetchFiles("", 1, 10); // 从根目录开始,第一页 - }, [id]); - useEffect(() => { - if (dataset?.parentDatasetId && activeTab === "children") { - setActiveTab("overview"); - } - }, [activeTab, dataset?.parentDatasetId]); +export default function DatasetDetail() { + const { id } = useParams(); // 获取动态路由参数 + const navigate = useNavigate(); + const [activeTab, setActiveTab] = useState("overview"); + const { message } = App.useApp(); + const [showEditDialog, setShowEditDialog] = useState(false); + + const [dataset, setDataset] = useState({} as Dataset); + const [parentDataset, setParentDataset] = useState(null); + const [childDatasets, setChildDatasets] = useState([]); + const [childDatasetsLoading, setChildDatasetsLoading] = useState(false); + const [similarDatasets, setSimilarDatasets] = useState([]); + const [similarDatasetsLoading, setSimilarDatasetsLoading] = useState(false); + const [similarTagNames, setSimilarTagNames] = useState([]); + const similarRequestRef = useRef(0); + const filesOperation = useFilesOperation(dataset); + + const [showUploadDialog, setShowUploadDialog] = useState(false); + const normalizeTagNames = ( + tags?: Array + ) => { + if (!tags || tags.length === 0) { + return []; + } + const names = tags + .map((tag) => (typeof tag === "string" ? tag : tag?.name)) + .filter((name): name is string => !!name && name.trim().length > 0) + .map((name) => name.trim()); + return Array.from(new Set(names)); + }; + const fetchSimilarDatasets = async (currentDataset: Dataset) => { + const requestId = similarRequestRef.current + 1; + similarRequestRef.current = requestId; + if (!currentDataset?.id) { + setSimilarDatasets([]); + setSimilarTagNames([]); + setSimilarDatasetsLoading(false); + return; + } + const tagNames = normalizeTagNames( + currentDataset.tags as Array + ); + setSimilarTagNames(tagNames); + setSimilarDatasets([]); + if (tagNames.length === 0) { + setSimilarDatasetsLoading(false); + return; + } + setSimilarDatasetsLoading(true); + try { + const { data } = await querySimilarDatasetsUsingGet(currentDataset.id, { + limit: SIMILAR_DATASET_LIMIT, + }); + if (similarRequestRef.current !== requestId) { + return; + } + const list = Array.isArray(data) ? data : []; + setSimilarDatasets(list.map((item) => mapDataset(item))); + } catch (error) { + console.error("Failed to fetch similar datasets:", error); + } finally { + if (similarRequestRef.current === requestId) { + setSimilarDatasetsLoading(false); + } + } + }; + + const similarTagsSummary = useMemo(() => { + if (!similarTagNames || similarTagNames.length === 0) { + return ""; + } + const visibleTags = similarTagNames.slice(0, SIMILAR_TAGS_PREVIEW_LIMIT); + const hiddenCount = similarTagNames.length - visibleTags.length; + if (hiddenCount > 0) { + return `${visibleTags.join("、")} 等 ${similarTagNames.length} 个`; + } + return visibleTags.join("、"); + }, [similarTagNames]); + + const navigateItems = useMemo(() => { + const items = [ + { + title: 数据管理, + }, + ]; + if (parentDataset) { + items.push({ + title: ( + + {parentDataset.name} + + ), + }); + } + items.push({ + title: dataset.name || "数据集详情", + }); + return items; + }, [dataset, parentDataset]); + const tabList = useMemo(() => { + const items = [ + { + key: "overview", + label: "概览", + }, + ]; + if (!dataset?.parentDatasetId) { + items.push({ + key: "children", + label: "关联数据集", + }); + } + return items; + }, [dataset?.parentDatasetId]); + const handleCreateChildDataset = () => { + if (!dataset?.id) { + return; + } + navigate("/data/management/create", { + state: { parentDatasetId: dataset.id }, + }); + }; + const fetchChildDatasets = async (parentId?: string) => { + if (!parentId) { + setChildDatasets([]); + return; + } + setChildDatasetsLoading(true); + try { + const { data: res } = await queryDatasetsUsingGet({ + parentDatasetId: parentId, + page: 1, + size: 1000, + }); + const list = res?.content || res?.data || []; + setChildDatasets(list.map((item) => mapDataset(item))); + } finally { + setChildDatasetsLoading(false); + } + }; + const fetchDataset = async () => { + if (!id) { + return; + } + const { data } = await queryDatasetByIdUsingGet(id); + const mapped = mapDataset(data); + setDataset(mapped); + fetchSimilarDatasets(mapped); + if (data?.parentDatasetId) { + const { data: parentData } = await queryDatasetByIdUsingGet( + data.parentDatasetId + ); + setParentDataset(mapDataset(parentData)); + setChildDatasets([]); + } else { + setParentDataset(null); + await fetchChildDatasets(data?.id); + } + }; + + useEffect(() => { + if (!id) { + return; + } + fetchDataset(); + filesOperation.fetchFiles("", 1, 10); // 从根目录开始,第一页 + }, [id]); + useEffect(() => { + if (dataset?.parentDatasetId && activeTab === "children") { + setActiveTab("overview"); + } + }, [activeTab, dataset?.parentDatasetId]); const handleRefresh = async (showMessage = true, prefixOverride?: string) => { fetchDataset(); @@ -261,22 +276,22 @@ export default function DatasetDetail() { ]; // 数据集操作列表 - const operations = [ - ...(dataset?.id && !dataset.parentDatasetId - ? [ - { - key: "create-child", - label: "创建关联数据集", - icon: , - onClick: handleCreateChildDataset, - }, - ] - : []), - { - key: "edit", - label: "编辑", - icon: , - onClick: () => { + const operations = [ + ...(dataset?.id && !dataset.parentDatasetId + ? [ + { + key: "create-child", + label: "创建关联数据集", + icon: , + onClick: handleCreateChildDataset, + }, + ] + : []), + { + key: "edit", + label: "编辑", + icon: , + onClick: () => { setShowEditDialog(true); }, }, @@ -314,55 +329,55 @@ export default function DatasetDetail() { icon: , onClick: handleDeleteDataset, }, - ]; - const childColumns = [ - { - title: "名称", - dataIndex: "name", - key: "name", - render: (_: string, record: Dataset) => ( - {record.name} - ), - }, - { - title: "类型", - dataIndex: "datasetType", - key: "datasetType", - width: 120, - render: (value: string) => datasetTypeMap[value]?.label || "未知", - }, - { - title: "状态", - dataIndex: "status", - key: "status", - width: 120, - render: (status) => - status ? {status.label} : "-", - }, - { - title: "文件数", - dataIndex: "fileCount", - key: "fileCount", - width: 120, - render: (value?: number) => value ?? 0, - }, - { - title: "大小", - dataIndex: "size", - key: "size", - width: 140, - render: (value?: string) => value || "0 B", - }, - { - title: "更新时间", - dataIndex: "updatedAt", - key: "updatedAt", - width: 180, - }, - ]; + ]; + const childColumns = [ + { + title: "名称", + dataIndex: "name", + key: "name", + render: (_: string, record: Dataset) => ( + {record.name} + ), + }, + { + title: "类型", + dataIndex: "datasetType", + key: "datasetType", + width: 120, + render: (value: string) => datasetTypeMap[value]?.label || "未知", + }, + { + title: "状态", + dataIndex: "status", + key: "status", + width: 120, + render: (status) => + status ? {status.label} : "-", + }, + { + title: "文件数", + dataIndex: "fileCount", + key: "fileCount", + width: 120, + render: (value?: number) => value ?? 0, + }, + { + title: "大小", + dataIndex: "size", + key: "size", + width: 140, + render: (value?: string) => value || "0 B", + }, + { + title: "更新时间", + dataIndex: "updatedAt", + key: "updatedAt", + width: 180, + }, + ]; return ( -
+
{/* Header */} -
- -
- {activeTab === "overview" && ( - setShowUploadDialog(true)} - similarDatasets={similarDatasets} - similarDatasetsLoading={similarDatasetsLoading} - similarTags={similarTagNames} - /> - )} - {activeTab === "children" && ( -
-
-

关联数据集

- - 共 {childDatasets.length} 个 - -
- - - )} - {activeTab === "lineage" && } - {activeTab === "quality" && } - - +
+
+ +
+ {activeTab === "overview" && ( + setShowUploadDialog(true)} + /> + )} + {activeTab === "children" && ( +
+
+

关联数据集

+ + 共 {childDatasets.length} 个 + +
+
+ + )} + {activeTab === "lineage" && } + {activeTab === "quality" && } + + + + {/* 相似数据集 */} +
+
+

相似数据集

+ {similarTagsSummary && ( + + 匹配标签:{similarTagsSummary} + + )} +
+ { + navigate(`/data/management/detail/${item.id}`); + }} + /> +
+ ; - fetchDataset: () => void; - onUpload?: () => void; - similarDatasets: Dataset[]; - similarDatasetsLoading: boolean; - similarTags: string[]; -}; - -export default function Overview({ - dataset, - filesOperation, - fetchDataset, - onUpload, - similarDatasets, - similarDatasetsLoading, - similarTags, -}: OverviewProps) { - const { modal, message } = App.useApp(); - const { - fileList, - pagination, - selectedFiles, - previewVisible, - previewFileName, - previewContent, - previewFileType, - previewMediaUrl, - previewLoading, - closePreview, - handleDeleteFile, - handleDownloadFile, - handleBatchDeleteFiles, - handleBatchExport, - handleCreateDirectory, - handleDownloadDirectory, - handleDeleteDirectory, - handlePreviewFile, - } = filesOperation; - const similarTagsSummary = (() => { - if (!similarTags || similarTags.length === 0) { - return ""; - } - const visibleTags = similarTags.slice(0, SIMILAR_TAGS_PREVIEW_LIMIT); - const hiddenCount = similarTags.length - visibleTags.length; - if (hiddenCount > 0) { - return `${visibleTags.join("、")} 等 ${similarTags.length} 个`; - } - return visibleTags.join("、"); - })(); - const renderDatasetTags = ( - tags?: Array - ) => { - if (!tags || tags.length === 0) { - return "-"; - } - const visibleTags = tags.slice(0, SIMILAR_DATASET_TAG_PREVIEW_LIMIT); - const hiddenCount = tags.length - visibleTags.length; - return ( -
- {visibleTags.map((tag, index) => { - const tagName = typeof tag === "string" ? tag : tag?.name; - if (!tagName) { - return null; - } - const tagColor = typeof tag === "string" ? undefined : tag?.color; - return ( - - {tagName} - - ); - })} - {hiddenCount > 0 && +{hiddenCount}} -
- ); - }; - const similarColumns = [ - { - title: "名称", - dataIndex: "name", - key: "name", - render: (_: string, record: Dataset) => ( - {record.name} - ), - }, - { - title: "标签", - dataIndex: "tags", - key: "tags", - render: (tags: Array) => - renderDatasetTags(tags), - }, - { - title: "类型", - dataIndex: "datasetType", - key: "datasetType", - width: 120, - render: (_: string, record: Dataset) => - datasetTypeMap[record.datasetType as keyof typeof datasetTypeMap]?.label || - "未知", - }, - { - title: "文件数", - dataIndex: "fileCount", - key: "fileCount", - width: 120, - render: (value?: number) => value ?? 0, - }, - { - title: "更新时间", - dataIndex: "updatedAt", - key: "updatedAt", - width: 180, - }, - ]; +import { + App, + Button, + Descriptions, + DescriptionsProps, + Modal, + Table, + Input, +} from "antd"; +import { formatBytes, formatDateTime } from "@/utils/unit"; +import { Download, Trash2, Folder, File } from "lucide-react"; +import { datasetTypeMap } from "../../dataset.const"; +import type { Dataset, DatasetFile } from "@/pages/DataManagement/dataset.model"; +import type { useFilesOperation } from "../useFilesOperation"; - // 基本信息 +type DatasetFileRow = DatasetFile & { + fileSize?: number; + fileCount?: number; + uploadTime?: string; +}; + +const PREVIEW_MAX_HEIGHT = 500; +const PREVIEW_MODAL_WIDTH = { + text: 800, + media: 700, +}; +const PREVIEW_TEXT_FONT_SIZE = 12; +const PREVIEW_TEXT_PADDING = 12; +const PREVIEW_AUDIO_PADDING = 40; + +type OverviewProps = { + dataset: Dataset; + filesOperation: ReturnType; + fetchDataset: () => void; + onUpload?: () => void; +}; + +export default function Overview({ + dataset, + filesOperation, + fetchDataset, + onUpload, +}: OverviewProps) { + const { modal, message } = App.useApp(); + const { + fileList, + pagination, + selectedFiles, + previewVisible, + previewFileName, + previewContent, + previewFileType, + previewMediaUrl, + previewLoading, + closePreview, + handleDeleteFile, + handleDownloadFile, + handleBatchDeleteFiles, + handleBatchExport, + handleCreateDirectory, + handleDownloadDirectory, + handleDeleteDirectory, + handlePreviewFile, + } = filesOperation; + + // 基本信息 const items: DescriptionsProps["items"] = [ { key: "id", @@ -211,7 +125,7 @@ export default function Overview({ dataIndex: "fileName", key: "fileName", fixed: "left", - render: (text: string, record: DatasetFileRow) => { + render: (text: string, record: DatasetFileRow) => { const isDirectory = record.id.startsWith('directory-'); const iconSize = 16; @@ -230,35 +144,35 @@ export default function Overview({ return ( ); } - return ( - - ); - }, + return ( + + ); + }, }, { title: "大小", dataIndex: "fileSize", key: "fileSize", width: 150, - render: (text: number, record: DatasetFileRow) => { + render: (text: number, record: DatasetFileRow) => { const isDirectory = record.id.startsWith('directory-'); if (isDirectory) { return formatBytes(record.fileSize || 0); @@ -271,7 +185,7 @@ export default function Overview({ dataIndex: "fileCount", key: "fileCount", width: 120, - render: (text: number, record: DatasetFileRow) => { + render: (text: number, record: DatasetFileRow) => { const isDirectory = record.id.startsWith('directory-'); if (!isDirectory) { return "-"; @@ -291,7 +205,7 @@ export default function Overview({ key: "action", width: 180, fixed: "right", - render: (_, record: DatasetFileRow) => { + render: (_, record: DatasetFileRow) => { const isDirectory = record.id.startsWith('directory-'); if (isDirectory) { @@ -330,21 +244,21 @@ export default function Overview({ ); } - return ( -
- - +
- - - {/* 文件列表 */} -
-

文件列表

-
- - -
-
+ {/* 文件列表 */} +
+

文件列表

+
+ + +
+
{selectedFiles.length > 0 && (
@@ -519,70 +408,70 @@ export default function Overview({ />
- {/* 文件预览弹窗 */} - - 关闭 - , - ]} - width={previewFileType === "text" ? PREVIEW_MODAL_WIDTH.text : PREVIEW_MODAL_WIDTH.media} - > - {previewFileType === "text" && ( -
-            {previewContent}
-          
- )} - {previewFileType === "image" && ( -
- {previewFileName} -
- )} - {previewFileType === "pdf" && ( -