refactor(DataManagement): 移除相似数据集表格并改用卡片视图显示

- 移除了 Overview 组件中的相似数据集表格相关代码
- 移除了 Tag 组件和相关依赖的导入
- 在 DatasetDetail 中添加 CardView 组件用于显示相似数据集
- 将相似数据集的展示从表格改为卡片布局
- 移除了 Overview 组件中的相似数据集参数传递
- 更新了页面布局以
This commit is contained in:
2026-01-31 09:40:06 +08:00
parent 790385bd80
commit 85d7141a91
2 changed files with 525 additions and 596 deletions

View File

@@ -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<Dataset>({} as Dataset);
const [parentDataset, setParentDataset] = useState<Dataset | null>(null);
const [childDatasets, setChildDatasets] = useState<Dataset[]>([]);
const [childDatasetsLoading, setChildDatasetsLoading] = useState(false);
const [similarDatasets, setSimilarDatasets] = useState<Dataset[]>([]);
const [similarDatasetsLoading, setSimilarDatasetsLoading] = useState(false);
const [similarTagNames, setSimilarTagNames] = useState<string[]>([]);
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<string | { name?: string | null } | null>
) => {
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<string | { name?: string }>
);
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: <Link to="/data/management"></Link>,
},
];
if (parentDataset) {
items.push({
title: (
<Link to={`/data/management/detail/${parentDataset.id}`}>
{parentDataset.name}
</Link>
),
});
}
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<Dataset>({} as Dataset);
const [parentDataset, setParentDataset] = useState<Dataset | null>(null);
const [childDatasets, setChildDatasets] = useState<Dataset[]>([]);
const [childDatasetsLoading, setChildDatasetsLoading] = useState(false);
const [similarDatasets, setSimilarDatasets] = useState<Dataset[]>([]);
const [similarDatasetsLoading, setSimilarDatasetsLoading] = useState(false);
const [similarTagNames, setSimilarTagNames] = useState<string[]>([]);
const similarRequestRef = useRef(0);
const filesOperation = useFilesOperation(dataset);
const [showUploadDialog, setShowUploadDialog] = useState(false);
const normalizeTagNames = (
tags?: Array<string | { name?: string | null } | null>
) => {
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<string | { name?: string }>
);
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: <Link to="/data/management"></Link>,
},
];
if (parentDataset) {
items.push({
title: (
<Link to={`/data/management/detail/${parentDataset.id}`}>
{parentDataset.name}
</Link>
),
});
}
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: <PlusOutlined />,
onClick: handleCreateChildDataset,
},
]
: []),
{
key: "edit",
label: "编辑",
icon: <EditOutlined />,
onClick: () => {
const operations = [
...(dataset?.id && !dataset.parentDatasetId
? [
{
key: "create-child",
label: "创建关联数据集",
icon: <PlusOutlined />,
onClick: handleCreateChildDataset,
},
]
: []),
{
key: "edit",
label: "编辑",
icon: <EditOutlined />,
onClick: () => {
setShowEditDialog(true);
},
},
@@ -314,55 +329,55 @@ export default function DatasetDetail() {
icon: <DeleteOutlined />,
onClick: handleDeleteDataset,
},
];
const childColumns = [
{
title: "名称",
dataIndex: "name",
key: "name",
render: (_: string, record: Dataset) => (
<Link to={`/data/management/detail/${record.id}`}>{record.name}</Link>
),
},
{
title: "类型",
dataIndex: "datasetType",
key: "datasetType",
width: 120,
render: (value: string) => datasetTypeMap[value]?.label || "未知",
},
{
title: "状态",
dataIndex: "status",
key: "status",
width: 120,
render: (status) =>
status ? <Tag color={status.color}>{status.label}</Tag> : "-",
},
{
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) => (
<Link to={`/data/management/detail/${record.id}`}>{record.name}</Link>
),
},
{
title: "类型",
dataIndex: "datasetType",
key: "datasetType",
width: 120,
render: (value: string) => datasetTypeMap[value]?.label || "未知",
},
{
title: "状态",
dataIndex: "status",
key: "status",
width: 120,
render: (status) =>
status ? <Tag color={status.color}>{status.label}</Tag> : "-",
},
{
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 (
<div className="h-full flex flex-col gap-4">
<div className="h-full flex flex-col gap-4 overflow-hidden">
<Breadcrumb items={navigateItems} />
{/* Header */}
<DetailHeader
@@ -398,42 +413,67 @@ export default function DatasetDetail() {
},
}}
/>
<div className="flex-overflow-auto p-6 pt-2 bg-white rounded-md shadow">
<Tabs activeKey={activeTab} items={tabList} onChange={setActiveTab} />
<div className="h-full overflow-auto">
{activeTab === "overview" && (
<Overview
dataset={dataset}
filesOperation={filesOperation}
fetchDataset={fetchDataset}
onUpload={() => setShowUploadDialog(true)}
similarDatasets={similarDatasets}
similarDatasetsLoading={similarDatasetsLoading}
similarTags={similarTagNames}
/>
)}
{activeTab === "children" && (
<div className="pt-4">
<div className="flex items-center justify-between mb-3">
<h2 className="text-base font-semibold"></h2>
<span className="text-xs text-gray-500">
{childDatasets.length}
</span>
</div>
<Table
rowKey="id"
columns={childColumns}
dataSource={childDatasets}
loading={childDatasetsLoading}
pagination={false}
locale={{ emptyText: "暂无关联数据集" }}
/>
</div>
)}
{activeTab === "lineage" && <DataLineageFlow dataset={dataset} />}
{activeTab === "quality" && <DataQuality />}
</div>
</div>
<div className="flex-1 overflow-auto">
<div className="p-6 pt-2 bg-white rounded-md shadow mb-4">
<Tabs activeKey={activeTab} items={tabList} onChange={setActiveTab} />
<div className="">
{activeTab === "overview" && (
<Overview
dataset={dataset}
filesOperation={filesOperation}
fetchDataset={fetchDataset}
onUpload={() => setShowUploadDialog(true)}
/>
)}
{activeTab === "children" && (
<div className="pt-4">
<div className="flex items-center justify-between mb-3">
<h2 className="text-base font-semibold"></h2>
<span className="text-xs text-gray-500">
{childDatasets.length}
</span>
</div>
<Table
rowKey="id"
columns={childColumns}
dataSource={childDatasets}
loading={childDatasetsLoading}
pagination={false}
locale={{ emptyText: "暂无关联数据集" }}
/>
</div>
)}
{activeTab === "lineage" && <DataLineageFlow dataset={dataset} />}
{activeTab === "quality" && <DataQuality />}
</div>
</div>
{/* 相似数据集 */}
<div className="bg-white rounded-md shadow p-6 mb-4">
<div className="flex items-center justify-between mb-3">
<h2 className="text-base font-semibold"></h2>
{similarTagsSummary && (
<span className="text-xs text-gray-500">
{similarTagsSummary}
</span>
)}
</div>
<CardView
data={similarDatasets}
loading={similarDatasetsLoading}
operations={[]}
pagination={{
current: 1,
pageSize: similarDatasets.length || 10,
total: similarDatasets.length || 0,
style: { display: "none" },
}}
onView={(item) => {
navigate(`/data/management/detail/${item.id}`);
}}
/>
</div>
</div>
<ImportConfiguration
data={dataset}
open={showUploadDialog}