init datamate

This commit is contained in:
Dallas98
2025-10-21 23:00:48 +08:00
commit 1c97afed7d
692 changed files with 135442 additions and 0 deletions

View File

@@ -0,0 +1,227 @@
import { useEffect, useMemo, useState } from "react";
import { Breadcrumb, App, Tabs } from "antd";
import {
ReloadOutlined,
DownloadOutlined,
UploadOutlined,
EditOutlined,
} 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, useParams } from "react-router";
import { useFilesOperation } from "../hooks";
import {
createDatasetTagUsingPost,
downloadFile,
queryDatasetByIdUsingGet,
queryDatasetTagsUsingGet,
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 tabList = [
{
key: "overview",
label: "概览",
},
{
key: "lineage",
label: "数据血缘",
},
{
key: "quality",
label: "数据质量",
},
];
export default function DatasetDetail() {
const { id } = useParams(); // 获取动态路由参数
const [activeTab, setActiveTab] = useState("overview");
const { message } = App.useApp();
const [showEditDialog, setShowEditDialog] = useState(false);
const [dataset, setDataset] = useState<Dataset>({} as Dataset);
const filesOperation = useFilesOperation(dataset);
const [showUploadDialog, setShowUploadDialog] = useState(false);
const navigateItems = useMemo(
() => [
{
title: <Link to="/data/management"></Link>,
},
{
title: dataset.name || "数据集详情",
},
],
[dataset]
);
const fetchDataset = async () => {
const { data } = await queryDatasetByIdUsingGet(id as unknown as number);
setDataset(mapDataset(data));
};
useEffect(() => {
fetchDataset();
filesOperation.fetchFiles();
}, []);
const handleRefresh = async (showMessage = true) => {
fetchDataset();
filesOperation.fetchFiles();
if (showMessage) message.success({ content: "数据刷新成功" });
};
const handleExportFormat = async ({ type }) => {
await downloadFile(dataset.id, type, `${dataset.name}-${type}.zip`);
message.success("文件下载成功");
};
useEffect(() => {
const refreshDataset = () => {
fetchDataset();
};
const refreshData = () => {
handleRefresh(false);
};
window.addEventListener("update:dataset", refreshData);
window.addEventListener("update:dataset-status", () => refreshDataset());
return () => {
window.removeEventListener("update:dataset", refreshData);
window.removeEventListener("update:dataset-status", refreshDataset);
};
}, []);
// 基本信息描述项
const statistics = [
{
icon: <File className="text-blue-400 w-4 h-4" />,
key: "file",
value: dataset?.fileCount || 0,
},
{
icon: <Activity className="text-blue-400 w-4 h-4" />,
key: "size",
value: dataset?.size || "0 B",
},
{
icon: <FileType className="text-blue-400 w-4 h-4" />,
key: "type",
value:
datasetTypeMap[dataset?.datasetType as keyof typeof datasetTypeMap]
?.label ||
dataset?.type ||
"未知",
},
{
icon: <Clock className="text-blue-400 w-4 h-4" />,
key: "time",
value: dataset?.createdAt,
},
];
// 数据集操作列表
const operations = [
{
key: "edit",
label: "编辑",
icon: <EditOutlined />,
onClick: () => {
setShowEditDialog(true);
},
},
{
key: "upload",
label: "导入数据",
icon: <UploadOutlined />,
onClick: () => setShowUploadDialog(true),
},
{
key: "export",
label: "导出",
icon: <DownloadOutlined />,
// isDropdown: true,
// items: [
// { key: "alpaca", label: "Alpaca 格式", icon: <FileTextOutlined /> },
// { key: "jsonl", label: "JSONL 格式", icon: <DatabaseOutlined /> },
// { key: "csv", label: "CSV 格式", icon: <FileTextOutlined /> },
// { key: "coco", label: "COCO 格式", icon: <FileImageOutlined /> },
// ],
onMenuClick: handleExportFormat,
},
{
key: "refresh",
label: "刷新",
icon: <ReloadOutlined />,
onClick: handleRefresh,
},
];
return (
<div className="h-full flex flex-col gap-4">
<Breadcrumb items={navigateItems} />
{/* Header */}
<DetailHeader
data={dataset}
statistics={statistics}
operations={operations}
tagConfig={{
showAdd: true,
tags: dataset.tags || [],
onFetchTags: async () => {
const res = await queryDatasetTagsUsingGet({
page: 0,
pageSize: 1000,
});
return res.data || [];
},
onCreateAndTag: async (tagName) => {
const res = await createDatasetTagUsingPost({ name: tagName });
if (res.data) {
await updateDatasetByIdUsingPut(dataset.id, {
tags: [...dataset.tags.map((tag) => tag.name), res.data.name],
});
handleRefresh();
}
},
onAddTag: async (tag) => {
const res = await updateDatasetByIdUsingPut(dataset.id, {
tags: [...dataset.tags.map((tag) => tag.name), tag],
});
if (res.data) {
handleRefresh();
}
},
}}
/>
<div className="h-full flex flex-col flex-1 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} />
)}
{activeTab === "lineage" && <DataLineageFlow dataset={dataset} />}
{activeTab === "quality" && <DataQuality />}
</div>
</div>
<ImportConfiguration
data={dataset}
open={showUploadDialog}
onClose={() => setShowUploadDialog(false)}
onRefresh={handleRefresh}
/>
<EditDataset
data={dataset}
open={showEditDialog}
onClose={() => setShowEditDialog(false)}
onRefresh={handleRefresh}
/>
</div>
);
}

View File

@@ -0,0 +1,52 @@
import DevelopmentInProgress from "@/components/DevelopmentInProgress";
import { Dataset } from "../../dataset.model";
export default function DataLineageFlow(dataset: Dataset) {
return <DevelopmentInProgress showHome={false} />
const lineage = dataset.lineage;
if (!lineage) return null;
const steps = [
{ name: "数据源", value: lineage.source, icon: Database },
...lineage.processing.map((step, index) => ({
name: `处理${index + 1}`,
value: step,
icon: GitBranch,
})),
];
if (lineage.training) {
steps.push({
name: "模型训练",
value: `${lineage.training.model} (准确率: ${lineage.training.accuracy}%)`,
icon: Target,
});
}
return (
<div className="space-y-4">
<div className="relative">
{steps.map((step, index) => (
<div key={index} className="flex items-start gap-4 pb-8 last:pb-0">
<div className="flex flex-col items-center">
<div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-full flex items-center justify-center shadow-lg">
<step.icon className="w-6 h-6 text-white" />
</div>
{index < steps.length - 1 && (
<div className="w-0.5 h-12 bg-gradient-to-b from-blue-200 to-indigo-200 mt-2"></div>
)}
</div>
<div className="flex-1 pt-3">
<div className="bg-white rounded-lg border border-gray-200 p-4 shadow-sm hover:shadow-md transition-shadow">
<h5 className="font-semibold text-gray-900 mb-1">
{step.name}
</h5>
<p className="text-sm text-gray-600">{step.value}</p>
</div>
</div>
</div>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,77 @@
import DevelopmentInProgress from "@/components/DevelopmentInProgress";
import { Card } from "antd";
import { AlertTriangle } from "lucide-react";
export default function DataQuality() {
return <DevelopmentInProgress showHome={false} />
return (
<div className=" mt-0">
<div className="grid md:grid-cols-2 gap-6">
<Card title="质量分布">
{[
{ metric: "图像清晰度", value: 96.2, color: "bg-green-500" },
{ metric: "色彩一致性", value: 94.8, color: "bg-blue-500" },
{ metric: "标注完整性", value: 98.1, color: "bg-purple-500" },
].map((item, index) => (
<div key={index} className="space-y-2">
<div className="flex justify-between text-sm">
<span>{item.metric}</span>
<span className="font-semibold">{item.value}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-3">
<div
className={`${item.color} h-3 rounded-full transition-all duration-500`}
style={{ width: `${item.value}%` }}
></div>
</div>
</div>
))}
</Card>
<Card title="数据完整性">
{[
{ metric: "文件完整性", value: 99.7, color: "bg-green-500" },
{ metric: "元数据完整性", value: 97.3, color: "bg-blue-500" },
{ metric: "标签一致性", value: 95.6, color: "bg-purple-500" },
].map((item, index) => (
<div key={index} className="space-y-2">
<div className="flex justify-between text-sm">
<span>{item.metric}</span>
<span className="font-semibold">{item.value}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-3">
<div
className={`${item.color} h-3 rounded-full transition-all duration-500`}
style={{ width: `${item.value}%` }}
></div>
</div>
</div>
))}
</Card>
</div>
<Card className="bg-gradient-to-r from-yellow-50 to-orange-50 border-yellow-200">
<div className="flex items-start gap-4">
<AlertTriangle className="w-6 h-6 text-yellow-600 mt-1 flex-shrink-0" />
<div>
<h4 className="font-semibold text-yellow-800 mb-2"></h4>
<ul className="text-sm text-yellow-700 space-y-2">
<li className="flex items-start gap-2">
<span className="w-1.5 h-1.5 bg-yellow-600 rounded-full mt-2 flex-shrink-0"></span>
42
</li>
<li className="flex items-start gap-2">
<span className="w-1.5 h-1.5 bg-yellow-600 rounded-full mt-2 flex-shrink-0"></span>
</li>
<li className="flex items-start gap-2">
<span className="w-1.5 h-1.5 bg-yellow-600 rounded-full mt-2 flex-shrink-0"></span>
</li>
</ul>
</div>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,235 @@
import { Select, Input, Form, Radio, Modal, Button } from "antd";
import { dataSourceOptions } from "../../dataset.const";
import { Dataset, DataSource } from "../../dataset.model";
import { useEffect, useState } from "react";
import { queryTasksUsingGet } from "@/pages/DataCollection/collection.apis";
import { useImportFile } from "../../hooks";
import { updateDatasetByIdUsingPut } from "../../dataset.api";
export default function ImportConfiguration({
data,
open,
onClose,
onRefresh,
}: {
data?: Dataset;
open: boolean;
onClose: () => void;
onRefresh?: () => void;
}) {
const [form] = Form.useForm();
const [collectionOptions, setCollectionOptions] = useState([]);
const [importConfig, setImportConfig] = useState<any>({
source: DataSource.UPLOAD,
});
const { importFileRender, handleUpload } = useImportFile();
// 获取归集任务列表
const fetchCollectionTasks = async () => {
try {
const res = await queryTasksUsingGet({ page: 0, size: 100 });
const options = res.data.content.map((task: any) => ({
label: task.name,
value: task.id,
}));
setCollectionOptions(options);
} catch (error) {
console.error("Error fetching collection tasks:", error);
}
};
const resetState = () => {
form.resetFields();
setImportConfig({ source: DataSource.UPLOAD });
};
const handleImportData = async () => {
if (importConfig.source === DataSource.UPLOAD) {
await handleUpload(data);
} else if (importConfig.source === DataSource.COLLECTION) {
await updateDatasetByIdUsingPut(data?.id!, {
...importConfig,
});
}
resetState();
onRefresh?.();
onClose();
};
useEffect(() => {
if (open) fetchCollectionTasks();
}, [open]);
return (
<Modal
title="导入数据"
open={open}
width={600}
onCancel={onClose}
maskClosable={false}
footer={
<>
<Button onClick={onClose}></Button>
<Button type="primary" onClick={handleImportData}>
</Button>
</>
}
>
<Form
form={form}
layout="vertical"
initialValues={importConfig || {}}
onValuesChange={(_, allValues) => setImportConfig(allValues)}
>
<Form.Item
label="数据源"
name="source"
rules={[{ required: true, message: "请选择数据源" }]}
>
<Radio.Group
buttonStyle="solid"
options={dataSourceOptions}
optionType="button"
/>
</Form.Item>
{importConfig?.source === DataSource.COLLECTION && (
<Form.Item name="dataSource" label="归集任务" required>
<Select placeholder="请选择归集任务" options={collectionOptions} />
</Form.Item>
)}
{/* nas import */}
{importConfig?.source === DataSource.NAS && (
<div className="grid grid-cols-2 gap-3 p-4 bg-blue-50 rounded-lg">
<Form.Item
name="nasPath"
rules={[{ required: true }]}
label="NAS地址"
>
<Input placeholder="192.168.1.100" />
</Form.Item>
<Form.Item
name="sharePath"
rules={[{ required: true }]}
label="共享路径"
>
<Input placeholder="/share/importConfig" />
</Form.Item>
<Form.Item
name="username"
rules={[{ required: true }]}
label="用户名"
>
<Input placeholder="用户名" />
</Form.Item>
<Form.Item
name="password"
rules={[{ required: true }]}
label="密码"
>
<Input type="password" placeholder="密码" />
</Form.Item>
</div>
)}
{/* obs import */}
{importConfig?.source === DataSource.OBS && (
<div className="grid grid-cols-2 gap-3 p-4 bg-blue-50 rounded-lg">
<Form.Item
name="endpoint"
rules={[{ required: true }]}
label="Endpoint"
>
<Input
className="h-8 text-xs"
placeholder="obs.cn-north-4.myhuaweicloud.com"
/>
</Form.Item>
<Form.Item
name="bucket"
rules={[{ required: true }]}
label="Bucket"
>
<Input className="h-8 text-xs" placeholder="my-bucket" />
</Form.Item>
<Form.Item
name="accessKey"
rules={[{ required: true }]}
label="Access Key"
>
<Input className="h-8 text-xs" placeholder="Access Key" />
</Form.Item>
<Form.Item
name="secretKey"
rules={[{ required: true }]}
label="Secret Key"
>
<Input
type="password"
className="h-8 text-xs"
placeholder="Secret Key"
/>
</Form.Item>
</div>
)}
{/* Local Upload Component */}
{importConfig?.source === DataSource.UPLOAD && (
<Form.Item
label="上传文件"
name="files"
rules={[
{
required: true,
message: "请上传文件",
},
]}
>
{importFileRender()}
</Form.Item>
)}
{/* Target Configuration */}
{importConfig?.target && importConfig?.target !== DataSource.UPLOAD && (
<div className="space-y-3 p-4 bg-blue-50 rounded-lg">
{importConfig?.target === DataSource.DATABASE && (
<div className="grid grid-cols-2 gap-3">
<Form.Item
name="databaseType"
rules={[{ required: true }]}
label="数据库类型"
>
<Select
className="w-full"
options={[
{ label: "MySQL", value: "mysql" },
{ label: "PostgreSQL", value: "postgresql" },
{ label: "MongoDB", value: "mongodb" },
]}
></Select>
</Form.Item>
<Form.Item
name="tableName"
rules={[{ required: true }]}
label="表名"
>
<Input className="h-8 text-xs" placeholder="dataset_table" />
</Form.Item>
<Form.Item
name="connectionString"
rules={[{ required: true }]}
label="连接字符串"
>
<Input
className="h-8 text-xs col-span-2"
placeholder="数据库连接字符串"
/>
</Form.Item>
</div>
)}
</div>
)}
</Form>
</Modal>
);
}

View File

@@ -0,0 +1,211 @@
import { Button, Descriptions, DescriptionsProps, Modal, Table } from "antd";
import { formatBytes, formatDateTime } from "@/utils/unit";
import { Download, Trash2 } from "lucide-react";
import { datasetTypeMap } from "../../dataset.const";
export default function Overview({ dataset, filesOperation }) {
const {
fileList,
selectedFiles,
setSelectedFiles,
previewVisible,
previewFileName,
previewContent,
setPreviewVisible,
handleDeleteFile,
handleDownloadFile,
handleBatchDeleteFiles,
handleBatchExport,
} = filesOperation;
// 文件列表多选配置
const rowSelection = {
onChange: (selectedRowKeys: React.Key[], selectedRows: any[]) => {
setSelectedFiles(selectedRowKeys as number[]);
console.log(
`selectedRowKeys: ${selectedRowKeys}`,
"selectedRows: ",
selectedRows
);
},
};
// 基本信息
const items: DescriptionsProps["items"] = [
{
key: "id",
label: "ID",
children: dataset.id,
},
{
key: "name",
label: "名称",
children: dataset.name,
},
{
key: "fileCount",
label: "文件数",
children: dataset.fileCount || 0,
},
{
key: "size",
label: "数据大小",
children: dataset.size || "0 B",
},
{
key: "datasetType",
label: "类型",
children: datasetTypeMap[dataset?.datasetType]?.label || "未知",
},
{
key: "status",
label: "状态",
children: dataset?.status?.label || "未知",
},
{
key: "createdBy",
label: "创建者",
children: dataset.createdBy || "未知",
},
{
key: "targetLocation",
label: "存储路径",
children: dataset.targetLocation || "未知",
},
{
key: "createdAt",
label: "创建时间",
children: dataset.createdAt,
},
{
key: "updatedAt",
label: "更新时间",
children: dataset.updatedAt,
},
{
key: "dataSource",
label: "数据源",
children: dataset.dataSource || "未知",
},
{
key: "description",
label: "描述",
children: dataset.description || "无",
},
];
// 文件列表列定义
const columns = [
{
title: "文件名",
dataIndex: "fileName",
key: "fileName",
fixed: "left",
},
{
title: "大小",
dataIndex: "fileSize",
key: "fileSize",
width: 150,
render: (text) => formatBytes(text),
},
{
title: "上传时间",
dataIndex: "uploadTime",
key: "uploadTime",
width: 200,
render: (text) => formatDateTime(text),
},
{
title: "操作",
key: "action",
width: 180,
fixed: "right",
render: (_, record) => (
<div className="flex">
<Button
size="small"
type="link"
onClick={() => handleDownloadFile(record)}
>
</Button>
<Button
size="small"
type="link"
onClick={() => handleDeleteFile(record)}
>
</Button>
</div>
),
},
];
return (
<>
<div className=" flex flex-col gap-4">
{/* 基本信息 */}
<Descriptions
title="基本信息"
layout="vertical"
size="small"
items={items}
column={5}
/>
{/* 文件列表 */}
<h2 className="text-base font-semibold mt-8"></h2>
{selectedFiles.length > 0 && (
<div className="flex items-center gap-2 p-3 bg-blue-50 rounded-lg border border-blue-200">
<span className="text-sm text-blue-700 font-medium">
{selectedFiles.length}
</span>
<Button
onClick={handleBatchExport}
className="ml-auto bg-transparent"
>
<Download className="w-4 h-4 mr-2" />
</Button>
<Button
onClick={handleBatchDeleteFiles}
className="text-red-600 hover:text-red-700 hover:bg-red-50 bg-transparent"
>
<Trash2 className="w-4 h-4 mr-2" />
</Button>
</div>
)}
<div className="overflow-x-auto">
<Table
size="middle"
rowKey="id"
columns={columns}
dataSource={fileList}
// rowSelection={rowSelection}
scroll={{ x: "max-content", y: 600 }}
/>
</div>
</div>
{/* 文件预览弹窗 */}
<Modal
title={`文件预览:${previewFileName}`}
open={previewVisible}
onCancel={() => setPreviewVisible(false)}
footer={null}
width={700}
>
<pre
style={{
whiteSpace: "pre-wrap",
wordBreak: "break-all",
fontSize: 14,
color: "#222",
}}
>
{previewContent}
</pre>
</Modal>
</>
);
}