You've already forked DataMate
feat(auto-annotation): integrate YOLO auto-labeling and enhance data management (#223)
* feat(auto-annotation): initial setup * chore: remove package-lock.json * chore: 清理本地测试脚本与 Maven 设置 * chore: change package-lock.json
This commit is contained in:
@@ -0,0 +1,302 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Card, Button, Table, message, Modal, Tag, Progress, Space, Tooltip } from "antd";
|
||||
import {
|
||||
PlusOutlined,
|
||||
DeleteOutlined,
|
||||
DownloadOutlined,
|
||||
ReloadOutlined,
|
||||
EyeOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import type { ColumnType } from "antd/es/table";
|
||||
import type { AutoAnnotationTask, AutoAnnotationStatus } from "../annotation.model";
|
||||
import {
|
||||
queryAutoAnnotationTasksUsingGet,
|
||||
deleteAutoAnnotationTaskByIdUsingDelete,
|
||||
downloadAutoAnnotationResultUsingGet,
|
||||
} from "../annotation.api";
|
||||
import CreateAutoAnnotationDialog from "./components/CreateAutoAnnotationDialog";
|
||||
|
||||
const STATUS_COLORS: Record<AutoAnnotationStatus, string> = {
|
||||
pending: "default",
|
||||
running: "processing",
|
||||
completed: "success",
|
||||
failed: "error",
|
||||
cancelled: "default",
|
||||
};
|
||||
|
||||
const STATUS_LABELS: Record<AutoAnnotationStatus, string> = {
|
||||
pending: "等待中",
|
||||
running: "处理中",
|
||||
completed: "已完成",
|
||||
failed: "失败",
|
||||
cancelled: "已取消",
|
||||
};
|
||||
|
||||
const MODEL_SIZE_LABELS: Record<string, string> = {
|
||||
n: "YOLOv8n (最快)",
|
||||
s: "YOLOv8s",
|
||||
m: "YOLOv8m",
|
||||
l: "YOLOv8l (推荐)",
|
||||
x: "YOLOv8x (最精确)",
|
||||
};
|
||||
|
||||
export default function AutoAnnotation() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [tasks, setTasks] = useState<AutoAnnotationTask[]>([]);
|
||||
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchTasks();
|
||||
const interval = setInterval(() => {
|
||||
fetchTasks(true);
|
||||
}, 3000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const fetchTasks = async (silent = false) => {
|
||||
if (!silent) setLoading(true);
|
||||
try {
|
||||
const response = await queryAutoAnnotationTasksUsingGet();
|
||||
setTasks(response.data || response || []);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch auto annotation tasks:", error);
|
||||
if (!silent) message.error("获取任务列表失败");
|
||||
} finally {
|
||||
if (!silent) setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = (task: AutoAnnotationTask) => {
|
||||
Modal.confirm({
|
||||
title: `确认删除自动标注任务「${task.name}」吗?`,
|
||||
content: "删除任务后,已生成的标注结果不会被删除。",
|
||||
okText: "删除",
|
||||
okType: "danger",
|
||||
cancelText: "取消",
|
||||
onOk: async () => {
|
||||
try {
|
||||
await deleteAutoAnnotationTaskByIdUsingDelete(task.id);
|
||||
message.success("任务删除成功");
|
||||
fetchTasks();
|
||||
setSelectedRowKeys((keys) => keys.filter((k) => k !== task.id));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
message.error("删除失败,请稍后重试");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleDownload = async (task: AutoAnnotationTask) => {
|
||||
try {
|
||||
message.loading("正在准备下载...", 0);
|
||||
await downloadAutoAnnotationResultUsingGet(task.id);
|
||||
message.destroy();
|
||||
message.success("下载已开始");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
message.destroy();
|
||||
message.error("下载失败");
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewResult = (task: AutoAnnotationTask) => {
|
||||
if (task.outputPath) {
|
||||
Modal.info({
|
||||
title: "标注结果路径",
|
||||
content: (
|
||||
<div>
|
||||
<p>输出路径:{task.outputPath}</p>
|
||||
<p>检测对象数:{task.detectedObjects}</p>
|
||||
<p>
|
||||
处理图片数:{task.processedImages} / {task.totalImages}
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const columns: ColumnType<AutoAnnotationTask>[] = [
|
||||
{ title: "任务名称", dataIndex: "name", key: "name", width: 200 },
|
||||
{
|
||||
title: "数据集",
|
||||
dataIndex: "datasetName",
|
||||
key: "datasetName",
|
||||
width: 220,
|
||||
render: (_: any, record: AutoAnnotationTask) => {
|
||||
const list =
|
||||
record.sourceDatasets && record.sourceDatasets.length > 0
|
||||
? record.sourceDatasets
|
||||
: record.datasetName
|
||||
? [record.datasetName]
|
||||
: [];
|
||||
|
||||
if (list.length === 0) return "-";
|
||||
|
||||
const text = list.join(",");
|
||||
return (
|
||||
<Tooltip title={text}>
|
||||
<span>{text}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "模型",
|
||||
dataIndex: ["config", "modelSize"],
|
||||
key: "modelSize",
|
||||
width: 120,
|
||||
render: (size: string) => MODEL_SIZE_LABELS[size] || size,
|
||||
},
|
||||
{
|
||||
title: "置信度",
|
||||
dataIndex: ["config", "confThreshold"],
|
||||
key: "confThreshold",
|
||||
width: 100,
|
||||
render: (threshold: number) => `${(threshold * 100).toFixed(0)}%`,
|
||||
},
|
||||
{
|
||||
title: "目标类别",
|
||||
dataIndex: ["config", "targetClasses"],
|
||||
key: "targetClasses",
|
||||
width: 120,
|
||||
render: (classes: number[]) => (
|
||||
<Tooltip
|
||||
title={classes.length > 0 ? classes.join(", ") : "全部类别"}
|
||||
>
|
||||
<span>
|
||||
{classes.length > 0
|
||||
? `${classes.length} 个类别`
|
||||
: "全部类别"}
|
||||
</span>
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
width: 100,
|
||||
render: (status: AutoAnnotationStatus) => (
|
||||
<Tag color={STATUS_COLORS[status]}>{STATUS_LABELS[status]}</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "进度",
|
||||
dataIndex: "progress",
|
||||
key: "progress",
|
||||
width: 150,
|
||||
render: (progress: number, record: AutoAnnotationTask) => (
|
||||
<div>
|
||||
<Progress percent={progress} size="small" />
|
||||
<div style={{ fontSize: "12px", color: "#999" }}>
|
||||
{record.processedImages} / {record.totalImages}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "检测对象数",
|
||||
dataIndex: "detectedObjects",
|
||||
key: "detectedObjects",
|
||||
width: 100,
|
||||
render: (count: number) => count.toLocaleString(),
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
dataIndex: "createdAt",
|
||||
key: "createdAt",
|
||||
width: 150,
|
||||
render: (time: string) => new Date(time).toLocaleString(),
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "actions",
|
||||
width: 180,
|
||||
fixed: "right",
|
||||
render: (_: any, record: AutoAnnotationTask) => (
|
||||
<Space size="small">
|
||||
{record.status === "completed" && (
|
||||
<>
|
||||
<Tooltip title="查看结果">
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => handleViewResult(record)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="下载结果">
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={() => handleDownload(record)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
<Tooltip title="删除">
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => handleDelete(record)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card
|
||||
title="自动标注任务"
|
||||
extra={
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setShowCreateDialog(true)}
|
||||
>
|
||||
创建任务
|
||||
</Button>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
loading={loading}
|
||||
onClick={() => fetchTasks()}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Table
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={tasks}
|
||||
rowSelection={{
|
||||
selectedRowKeys,
|
||||
onChange: (keys) => setSelectedRowKeys(keys as string[]),
|
||||
}}
|
||||
pagination={{ pageSize: 10 }}
|
||||
scroll={{ x: 1000 }}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<CreateAutoAnnotationDialog
|
||||
visible={showCreateDialog}
|
||||
onCancel={() => setShowCreateDialog(false)}
|
||||
onSuccess={() => {
|
||||
setShowCreateDialog(false);
|
||||
fetchTasks();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Modal, Form, Input, Select, Slider, message, Checkbox } from "antd";
|
||||
import { createAutoAnnotationTaskUsingPost } from "../../annotation.api";
|
||||
import { queryDatasetsUsingGet } from "@/pages/DataManagement/dataset.api";
|
||||
import { mapDataset } from "@/pages/DataManagement/dataset.const";
|
||||
import { DatasetType, type DatasetFile, type Dataset } from "@/pages/DataManagement/dataset.model";
|
||||
import DatasetFileTransfer from "@/components/business/DatasetFileTransfer";
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
interface CreateAutoAnnotationDialogProps {
|
||||
visible: boolean;
|
||||
onCancel: () => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
const COCO_CLASSES = [
|
||||
{ id: 0, name: "person", label: "人" },
|
||||
{ id: 1, name: "bicycle", label: "自行车" },
|
||||
{ id: 2, name: "car", label: "汽车" },
|
||||
{ id: 3, name: "motorcycle", label: "摩托车" },
|
||||
{ id: 4, name: "airplane", label: "飞机" },
|
||||
{ id: 5, name: "bus", label: "公交车" },
|
||||
{ id: 6, name: "train", label: "火车" },
|
||||
{ id: 7, name: "truck", label: "卡车" },
|
||||
{ id: 8, name: "boat", label: "船" },
|
||||
{ id: 9, name: "traffic light", label: "红绿灯" },
|
||||
{ id: 10, name: "fire hydrant", label: "消防栓" },
|
||||
{ id: 11, name: "stop sign", label: "停止标志" },
|
||||
{ id: 12, name: "parking meter", label: "停车计时器" },
|
||||
{ id: 13, name: "bench", label: "长椅" },
|
||||
{ id: 14, name: "bird", label: "鸟" },
|
||||
{ id: 15, name: "cat", label: "猫" },
|
||||
{ id: 16, name: "dog", label: "狗" },
|
||||
{ id: 17, name: "horse", label: "马" },
|
||||
{ id: 18, name: "sheep", label: "羊" },
|
||||
{ id: 19, name: "cow", label: "牛" },
|
||||
{ id: 20, name: "elephant", label: "大象" },
|
||||
{ id: 21, name: "bear", label: "熊" },
|
||||
{ id: 22, name: "zebra", label: "斑马" },
|
||||
{ id: 23, name: "giraffe", label: "长颈鹿" },
|
||||
{ id: 24, name: "backpack", label: "背包" },
|
||||
{ id: 25, name: "umbrella", label: "雨伞" },
|
||||
{ id: 26, name: "handbag", label: "手提包" },
|
||||
{ id: 27, name: "tie", label: "领带" },
|
||||
{ id: 28, name: "suitcase", label: "行李箱" },
|
||||
{ id: 29, name: "frisbee", label: "飞盘" },
|
||||
{ id: 30, name: "skis", label: "滑雪板" },
|
||||
{ id: 31, name: "snowboard", label: "滑雪板" },
|
||||
{ id: 32, name: "sports ball", label: "球类" },
|
||||
{ id: 33, name: "kite", label: "风筝" },
|
||||
{ id: 34, name: "baseball bat", label: "棒球棒" },
|
||||
{ id: 35, name: "baseball glove", label: "棒球手套" },
|
||||
{ id: 36, name: "skateboard", label: "滑板" },
|
||||
{ id: 37, name: "surfboard", label: "冲浪板" },
|
||||
{ id: 38, name: "tennis racket", label: "网球拍" },
|
||||
{ id: 39, name: "bottle", label: "瓶子" },
|
||||
{ id: 40, name: "wine glass", label: "酒杯" },
|
||||
{ id: 41, name: "cup", label: "杯子" },
|
||||
{ id: 42, name: "fork", label: "叉子" },
|
||||
{ id: 43, name: "knife", label: "刀" },
|
||||
{ id: 44, name: "spoon", label: "勺子" },
|
||||
{ id: 45, name: "bowl", label: "碗" },
|
||||
{ id: 46, name: "banana", label: "香蕉" },
|
||||
{ id: 47, name: "apple", label: "苹果" },
|
||||
{ id: 48, name: "sandwich", label: "三明治" },
|
||||
{ id: 49, name: "orange", label: "橙子" },
|
||||
{ id: 50, name: "broccoli", label: "西兰花" },
|
||||
{ id: 51, name: "carrot", label: "胡萝卜" },
|
||||
{ id: 52, name: "hot dog", label: "热狗" },
|
||||
{ id: 53, name: "pizza", label: "披萨" },
|
||||
{ id: 54, name: "donut", label: "甜甜圈" },
|
||||
{ id: 55, name: "cake", label: "蛋糕" },
|
||||
{ id: 56, name: "chair", label: "椅子" },
|
||||
{ id: 57, name: "couch", label: "沙发" },
|
||||
{ id: 58, name: "potted plant", label: "盆栽" },
|
||||
{ id: 59, name: "bed", label: "床" },
|
||||
{ id: 60, name: "dining table", label: "餐桌" },
|
||||
{ id: 61, name: "toilet", label: "马桶" },
|
||||
{ id: 62, name: "tv", label: "电视" },
|
||||
{ id: 63, name: "laptop", label: "笔记本电脑" },
|
||||
{ id: 64, name: "mouse", label: "鼠标" },
|
||||
{ id: 65, name: "remote", label: "遥控器" },
|
||||
{ id: 66, name: "keyboard", label: "键盘" },
|
||||
{ id: 67, name: "cell phone", label: "手机" },
|
||||
{ id: 68, name: "microwave", label: "微波炉" },
|
||||
{ id: 69, name: "oven", label: "烤箱" },
|
||||
{ id: 70, name: "toaster", label: "烤面包机" },
|
||||
{ id: 71, name: "sink", label: "水槽" },
|
||||
{ id: 72, name: "refrigerator", label: "冰箱" },
|
||||
{ id: 73, name: "book", label: "书" },
|
||||
{ id: 74, name: "clock", label: "钟表" },
|
||||
{ id: 75, name: "vase", label: "花瓶" },
|
||||
{ id: 76, name: "scissors", label: "剪刀" },
|
||||
{ id: 77, name: "teddy bear", label: "玩具熊" },
|
||||
{ id: 78, name: "hair drier", label: "吹风机" },
|
||||
{ id: 79, name: "toothbrush", label: "牙刷" },
|
||||
];
|
||||
|
||||
export default function CreateAutoAnnotationDialog({
|
||||
visible,
|
||||
onCancel,
|
||||
onSuccess,
|
||||
}: CreateAutoAnnotationDialogProps) {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [datasets, setDatasets] = useState<any[]>([]);
|
||||
const [selectAllClasses, setSelectAllClasses] = useState(true);
|
||||
const [selectedFilesMap, setSelectedFilesMap] = useState<Record<string, DatasetFile>>({});
|
||||
const [selectedDataset, setSelectedDataset] = useState<Dataset | null>(null);
|
||||
const [imageFileCount, setImageFileCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
fetchDatasets();
|
||||
form.resetFields();
|
||||
form.setFieldsValue({
|
||||
modelSize: "l",
|
||||
confThreshold: 0.7,
|
||||
targetClasses: [],
|
||||
});
|
||||
}
|
||||
}, [visible, form]);
|
||||
|
||||
const fetchDatasets = async () => {
|
||||
try {
|
||||
const { data } = await queryDatasetsUsingGet({
|
||||
page: 0,
|
||||
pageSize: 1000,
|
||||
});
|
||||
const imageDatasets = (data.content || [])
|
||||
.map(mapDataset)
|
||||
.filter((ds: any) => ds.datasetType === DatasetType.IMAGE);
|
||||
setDatasets(imageDatasets);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch datasets:", error);
|
||||
message.error("获取数据集列表失败");
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const imageExtensions = [".jpg", ".jpeg", ".png", ".bmp", ".gif", ".tiff", ".webp"];
|
||||
const count = Object.values(selectedFilesMap).filter((file) => {
|
||||
const ext = file.fileName?.toLowerCase().match(/\.[^.]+$/)?.[0] || "";
|
||||
return imageExtensions.includes(ext);
|
||||
}).length;
|
||||
setImageFileCount(count);
|
||||
}, [selectedFilesMap]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
|
||||
if (imageFileCount === 0) {
|
||||
message.error("请至少选择一个图像文件");
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
const imageExtensions = [".jpg", ".jpeg", ".png", ".bmp", ".gif", ".tiff", ".webp"];
|
||||
const imageFileIds = Object.values(selectedFilesMap)
|
||||
.filter((file) => {
|
||||
const ext = file.fileName?.toLowerCase().match(/\.[^.]+$/)?.[0] || "";
|
||||
return imageExtensions.includes(ext);
|
||||
})
|
||||
.map((file) => file.id);
|
||||
|
||||
const payload = {
|
||||
name: values.name,
|
||||
datasetId: values.datasetId,
|
||||
fileIds: imageFileIds,
|
||||
config: {
|
||||
modelSize: values.modelSize,
|
||||
confThreshold: values.confThreshold,
|
||||
targetClasses: selectAllClasses ? [] : values.targetClasses || [],
|
||||
outputDatasetName: values.outputDatasetName || undefined,
|
||||
},
|
||||
};
|
||||
|
||||
await createAutoAnnotationTaskUsingPost(payload);
|
||||
message.success("自动标注任务创建成功");
|
||||
onSuccess();
|
||||
} catch (error: any) {
|
||||
if (error.errorFields) return;
|
||||
console.error("Failed to create auto annotation task:", error);
|
||||
message.error(error.message || "创建任务失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClassSelectionChange = (checked: boolean) => {
|
||||
setSelectAllClasses(checked);
|
||||
if (checked) {
|
||||
form.setFieldsValue({ targetClasses: [] });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="创建自动标注任务"
|
||||
open={visible}
|
||||
onCancel={onCancel}
|
||||
onOk={handleSubmit}
|
||||
confirmLoading={loading}
|
||||
width={600}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form form={form} layout="vertical" preserve={false}>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="任务名称"
|
||||
rules={[
|
||||
{ required: true, message: "请输入任务名称" },
|
||||
{ max: 100, message: "任务名称不能超过100个字符" },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入任务名称" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="选择数据集和图像文件" required>
|
||||
<DatasetFileTransfer
|
||||
open
|
||||
selectedFilesMap={selectedFilesMap}
|
||||
onSelectedFilesChange={setSelectedFilesMap}
|
||||
onDatasetSelect={(dataset) => {
|
||||
setSelectedDataset(dataset);
|
||||
form.setFieldsValue({ datasetId: dataset?.id ?? "" });
|
||||
}}
|
||||
datasetTypeFilter={DatasetType.IMAGE}
|
||||
/>
|
||||
{selectedDataset && (
|
||||
<div className="mt-2 p-2 bg-blue-50 rounded border border-blue-200 text-xs">
|
||||
当前数据集:<span className="font-medium">{selectedDataset.name}</span> - 已选择
|
||||
<span className="font-medium text-blue-600"> {imageFileCount} </span>个图像文件
|
||||
</div>
|
||||
)}
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item hidden name="datasetId" rules={[{ required: true, message: "请选择数据集" }]}>
|
||||
<Input type="hidden" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="modelSize" label="模型规模" rules={[{ required: true, message: "请选择模型规模" }]}>
|
||||
<Select>
|
||||
<Option value="n">YOLOv8n (最快)</Option>
|
||||
<Option value="s">YOLOv8s</Option>
|
||||
<Option value="m">YOLOv8m</Option>
|
||||
<Option value="l">YOLOv8l (推荐)</Option>
|
||||
<Option value="x">YOLOv8x (最精确)</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="confThreshold"
|
||||
label="置信度阈值"
|
||||
rules={[{ required: true, message: "请选择置信度阈值" }]}
|
||||
>
|
||||
<Slider min={0.1} max={0.9} step={0.05} tooltip={{ formatter: (v) => `${(v || 0) * 100}%` }} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="目标类别">
|
||||
<Checkbox checked={selectAllClasses} onChange={(e) => handleClassSelectionChange(e.target.checked)}>
|
||||
选中所有类别
|
||||
</Checkbox>
|
||||
{!selectAllClasses && (
|
||||
<Form.Item name="targetClasses" noStyle>
|
||||
<Select mode="multiple" placeholder="选择目标类别" style={{ marginTop: 8 }}>
|
||||
{COCO_CLASSES.map((cls) => (
|
||||
<Option key={cls.id} value={cls.id}>
|
||||
{cls.label} ({cls.name})
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="outputDatasetName" label="输出数据集名称 (可选)">
|
||||
<Input placeholder="留空则将结果写入原数据集的标签中" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./AutoAnnotation";
|
||||
@@ -1,192 +1,489 @@
|
||||
import { queryDatasetsUsingGet } from "@/pages/DataManagement/dataset.api";
|
||||
import { mapDataset } from "@/pages/DataManagement/dataset.const";
|
||||
import { Button, Form, Input, Modal, Select, message } from "antd";
|
||||
import TextArea from "antd/es/input/TextArea";
|
||||
import { useEffect, useState } from "react";
|
||||
import { createAnnotationTaskUsingPost, queryAnnotationTemplatesUsingGet } from "../../annotation.api";
|
||||
import { Dataset } from "@/pages/DataManagement/dataset.model";
|
||||
import type { AnnotationTemplate } from "../../annotation.model";
|
||||
|
||||
export default function CreateAnnotationTask({
|
||||
open,
|
||||
onClose,
|
||||
onRefresh,
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onRefresh: () => void;
|
||||
}) {
|
||||
const [form] = Form.useForm();
|
||||
const [datasets, setDatasets] = useState<Dataset[]>([]);
|
||||
const [templates, setTemplates] = useState<AnnotationTemplate[]>([]);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [nameManuallyEdited, setNameManuallyEdited] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
// Fetch datasets
|
||||
const { data: datasetData } = await queryDatasetsUsingGet({
|
||||
page: 0,
|
||||
pageSize: 1000, // Use camelCase for HTTP params
|
||||
});
|
||||
setDatasets(datasetData.content.map(mapDataset) || []);
|
||||
|
||||
// Fetch templates
|
||||
const templateResponse = await queryAnnotationTemplatesUsingGet({
|
||||
page: 1,
|
||||
size: 100, // Backend max is 100 (template API uses 'size' not 'pageSize')
|
||||
});
|
||||
|
||||
// The API returns: {code, message, data: {content, total, page, ...}}
|
||||
if (templateResponse.code === 200 && templateResponse.data) {
|
||||
const fetchedTemplates = templateResponse.data.content || [];
|
||||
console.log("Fetched templates:", fetchedTemplates);
|
||||
setTemplates(fetchedTemplates);
|
||||
} else {
|
||||
console.error("Failed to fetch templates:", templateResponse);
|
||||
setTemplates([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
setTemplates([]);
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
}, [open]);
|
||||
|
||||
// Reset form and manual-edit flag when modal opens
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
form.resetFields();
|
||||
setNameManuallyEdited(false);
|
||||
}
|
||||
}, [open, form]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
setSubmitting(true);
|
||||
// Send templateId instead of labelingConfig
|
||||
const requestData = {
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
datasetId: values.datasetId,
|
||||
templateId: values.templateId,
|
||||
};
|
||||
await createAnnotationTaskUsingPost(requestData);
|
||||
message?.success?.("创建标注任务成功");
|
||||
onClose();
|
||||
onRefresh();
|
||||
} catch (err: any) {
|
||||
console.error("Create annotation task failed", err);
|
||||
const msg = err?.message || err?.data?.message || "创建失败,请稍后重试";
|
||||
(message as any)?.error?.(msg);
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
title="创建标注任务"
|
||||
footer={
|
||||
<>
|
||||
<Button onClick={onClose} disabled={submitting}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="primary" onClick={handleSubmit} loading={submitting}>
|
||||
确定
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
width={800}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
{/* 数据集 与 标注工程名称 并排显示(数据集在左) */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Form.Item
|
||||
label="数据集"
|
||||
name="datasetId"
|
||||
rules={[{ required: true, message: "请选择数据集" }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择数据集"
|
||||
options={datasets.map((dataset) => {
|
||||
return {
|
||||
label: (
|
||||
<div className="flex items-center justify-between gap-3 py-2">
|
||||
<div className="flex items-center font-sm text-gray-900">
|
||||
<span className="mr-2">{(dataset as any).icon}</span>
|
||||
<span>{dataset.name}</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">{dataset.size}</div>
|
||||
</div>
|
||||
),
|
||||
value: dataset.id,
|
||||
};
|
||||
})}
|
||||
onChange={(value) => {
|
||||
// 如果用户未手动修改名称,则用数据集名称作为默认任务名
|
||||
if (!nameManuallyEdited) {
|
||||
const ds = datasets.find((d) => d.id === value);
|
||||
if (ds) {
|
||||
form.setFieldsValue({ name: ds.name });
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="标注工程名称"
|
||||
name="name"
|
||||
rules={[{ required: true, message: "请输入任务名称" }]}
|
||||
>
|
||||
<Input
|
||||
placeholder="输入标注工程名称"
|
||||
onChange={() => setNameManuallyEdited(true)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
{/* 描述变为可选 */}
|
||||
<Form.Item label="描述" name="description">
|
||||
<TextArea placeholder="(可选)详细描述标注任务的要求和目标" rows={3} />
|
||||
</Form.Item>
|
||||
|
||||
{/* 标注模板选择 */}
|
||||
<Form.Item
|
||||
label="标注模板"
|
||||
name="templateId"
|
||||
rules={[{ required: true, message: "请选择标注模板" }]}
|
||||
>
|
||||
<Select
|
||||
placeholder={templates.length === 0 ? "暂无可用模板,请先创建模板" : "请选择标注模板"}
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
notFoundContent={templates.length === 0 ? "暂无模板,请前往「标注模板」页面创建" : "未找到匹配的模板"}
|
||||
options={templates.map((template) => ({
|
||||
label: template.name,
|
||||
value: template.id,
|
||||
// Add description as subtitle
|
||||
title: template.description,
|
||||
}))}
|
||||
optionRender={(option) => (
|
||||
<div>
|
||||
<div style={{ fontWeight: 500 }}>{option.label}</div>
|
||||
{option.data.title && (
|
||||
<div style={{ fontSize: 12, color: '#999', marginTop: 2 }}>
|
||||
{option.data.title}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
import { queryDatasetsUsingGet } from "@/pages/DataManagement/dataset.api";
|
||||
import { mapDataset } from "@/pages/DataManagement/dataset.const";
|
||||
import { Button, Form, Input, Modal, Select, message, Tabs, Slider, Checkbox } from "antd";
|
||||
import TextArea from "antd/es/input/TextArea";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
createAnnotationTaskUsingPost,
|
||||
queryAnnotationTemplatesUsingGet,
|
||||
createAutoAnnotationTaskUsingPost,
|
||||
} from "../../annotation.api";
|
||||
import DatasetFileTransfer from "@/components/business/DatasetFileTransfer";
|
||||
import { DatasetType, type Dataset, type DatasetFile } from "@/pages/DataManagement/dataset.model";
|
||||
import type { AnnotationTemplate } from "../../annotation.model";
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
const COCO_CLASSES = [
|
||||
{ id: 0, name: "person", label: "人" },
|
||||
{ id: 1, name: "bicycle", label: "自行车" },
|
||||
{ id: 2, name: "car", label: "汽车" },
|
||||
{ id: 3, name: "motorcycle", label: "摩托车" },
|
||||
{ id: 4, name: "airplane", label: "飞机" },
|
||||
{ id: 5, name: "bus", label: "公交车" },
|
||||
{ id: 6, name: "train", label: "火车" },
|
||||
{ id: 7, name: "truck", label: "卡车" },
|
||||
{ id: 8, name: "boat", label: "船" },
|
||||
{ id: 9, name: "traffic light", label: "红绿灯" },
|
||||
{ id: 10, name: "fire hydrant", label: "消防栓" },
|
||||
{ id: 11, name: "stop sign", label: "停止标志" },
|
||||
{ id: 12, name: "parking meter", label: "停车计时器" },
|
||||
{ id: 13, name: "bench", label: "长椅" },
|
||||
{ id: 14, name: "bird", label: "鸟" },
|
||||
{ id: 15, name: "cat", label: "猫" },
|
||||
{ id: 16, name: "dog", label: "狗" },
|
||||
{ id: 17, name: "horse", label: "马" },
|
||||
{ id: 18, name: "sheep", label: "羊" },
|
||||
{ id: 19, name: "cow", label: "牛" },
|
||||
{ id: 20, name: "elephant", label: "大象" },
|
||||
{ id: 21, name: "bear", label: "熊" },
|
||||
{ id: 22, name: "zebra", label: "斑马" },
|
||||
{ id: 23, name: "giraffe", label: "长颈鹿" },
|
||||
{ id: 24, name: "backpack", label: "背包" },
|
||||
{ id: 25, name: "umbrella", label: "雨伞" },
|
||||
{ id: 26, name: "handbag", label: "手提包" },
|
||||
{ id: 27, name: "tie", label: "领带" },
|
||||
{ id: 28, name: "suitcase", label: "行李箱" },
|
||||
{ id: 29, name: "frisbee", label: "飞盘" },
|
||||
{ id: 30, name: "skis", label: "滑雪板" },
|
||||
{ id: 31, name: "snowboard", label: "滑雪板" },
|
||||
{ id: 32, name: "sports ball", label: "球类" },
|
||||
{ id: 33, name: "kite", label: "风筝" },
|
||||
{ id: 34, name: "baseball bat", label: "棒球棒" },
|
||||
{ id: 35, name: "baseball glove", label: "棒球手套" },
|
||||
{ id: 36, name: "skateboard", label: "滑板" },
|
||||
{ id: 37, name: "surfboard", label: "冲浪板" },
|
||||
{ id: 38, name: "tennis racket", label: "网球拍" },
|
||||
{ id: 39, name: "bottle", label: "瓶子" },
|
||||
{ id: 40, name: "wine glass", label: "酒杯" },
|
||||
{ id: 41, name: "cup", label: "杯子" },
|
||||
{ id: 42, name: "fork", label: "叉子" },
|
||||
{ id: 43, name: "knife", label: "刀" },
|
||||
{ id: 44, name: "spoon", label: "勺子" },
|
||||
{ id: 45, name: "bowl", label: "碗" },
|
||||
{ id: 46, name: "banana", label: "香蕉" },
|
||||
{ id: 47, name: "apple", label: "苹果" },
|
||||
{ id: 48, name: "sandwich", label: "三明治" },
|
||||
{ id: 49, name: "orange", label: "橙子" },
|
||||
{ id: 50, name: "broccoli", label: "西兰花" },
|
||||
{ id: 51, name: "carrot", label: "胡萝卜" },
|
||||
{ id: 52, name: "hot dog", label: "热狗" },
|
||||
{ id: 53, name: "pizza", label: "披萨" },
|
||||
{ id: 54, name: "donut", label: "甜甜圈" },
|
||||
{ id: 55, name: "cake", label: "蛋糕" },
|
||||
{ id: 56, name: "chair", label: "椅子" },
|
||||
{ id: 57, name: "couch", label: "沙发" },
|
||||
{ id: 58, name: "potted plant", label: "盆栽" },
|
||||
{ id: 59, name: "bed", label: "床" },
|
||||
{ id: 60, name: "dining table", label: "餐桌" },
|
||||
{ id: 61, name: "toilet", label: "马桶" },
|
||||
{ id: 62, name: "tv", label: "电视" },
|
||||
{ id: 63, name: "laptop", label: "笔记本电脑" },
|
||||
{ id: 64, name: "mouse", label: "鼠标" },
|
||||
{ id: 65, name: "remote", label: "遥控器" },
|
||||
{ id: 66, name: "keyboard", label: "键盘" },
|
||||
{ id: 67, name: "cell phone", label: "手机" },
|
||||
{ id: 68, name: "microwave", label: "微波炉" },
|
||||
{ id: 69, name: "oven", label: "烤箱" },
|
||||
{ id: 70, name: "toaster", label: "烤面包机" },
|
||||
{ id: 71, name: "sink", label: "水槽" },
|
||||
{ id: 72, name: "refrigerator", label: "冰箱" },
|
||||
{ id: 73, name: "book", label: "书" },
|
||||
{ id: 74, name: "clock", label: "钟表" },
|
||||
{ id: 75, name: "vase", label: "花瓶" },
|
||||
{ id: 76, name: "scissors", label: "剪刀" },
|
||||
{ id: 77, name: "teddy bear", label: "玩具熊" },
|
||||
{ id: 78, name: "hair drier", label: "吹风机" },
|
||||
{ id: 79, name: "toothbrush", label: "牙刷" },
|
||||
];
|
||||
|
||||
export default function CreateAnnotationTask({
|
||||
open,
|
||||
onClose,
|
||||
onRefresh,
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onRefresh: () => void;
|
||||
}) {
|
||||
const [manualForm] = Form.useForm();
|
||||
const [autoForm] = Form.useForm();
|
||||
const [datasets, setDatasets] = useState<Dataset[]>([]);
|
||||
const [templates, setTemplates] = useState<AnnotationTemplate[]>([]);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [nameManuallyEdited, setNameManuallyEdited] = useState(false);
|
||||
const [activeMode, setActiveMode] = useState<"manual" | "auto">("manual");
|
||||
|
||||
const [selectAllClasses, setSelectAllClasses] = useState(true);
|
||||
const [selectedFilesMap, setSelectedFilesMap] = useState<Record<string, DatasetFile>>({});
|
||||
const [selectedDataset, setSelectedDataset] = useState<Dataset | null>(null);
|
||||
const [imageFileCount, setImageFileCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
// Fetch datasets
|
||||
const { data: datasetData } = await queryDatasetsUsingGet({
|
||||
page: 0,
|
||||
pageSize: 1000, // Use camelCase for HTTP params
|
||||
});
|
||||
setDatasets(datasetData.content.map(mapDataset) || []);
|
||||
|
||||
// Fetch templates
|
||||
const templateResponse = await queryAnnotationTemplatesUsingGet({
|
||||
page: 1,
|
||||
size: 100, // Backend max is 100 (template API uses 'size' not 'pageSize')
|
||||
});
|
||||
|
||||
// The API returns: {code, message, data: {content, total, page, ...}}
|
||||
if (templateResponse.code === 200 && templateResponse.data) {
|
||||
const fetchedTemplates = templateResponse.data.content || [];
|
||||
console.log("Fetched templates:", fetchedTemplates);
|
||||
setTemplates(fetchedTemplates);
|
||||
} else {
|
||||
console.error("Failed to fetch templates:", templateResponse);
|
||||
setTemplates([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
setTemplates([]);
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
}, [open]);
|
||||
|
||||
// Reset form and manual-edit flag when modal opens
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
manualForm.resetFields();
|
||||
autoForm.resetFields();
|
||||
setNameManuallyEdited(false);
|
||||
setActiveMode("manual");
|
||||
setSelectAllClasses(true);
|
||||
setSelectedFilesMap({});
|
||||
setSelectedDataset(null);
|
||||
setImageFileCount(0);
|
||||
}
|
||||
}, [open, manualForm, autoForm]);
|
||||
|
||||
useEffect(() => {
|
||||
const imageExtensions = [".jpg", ".jpeg", ".png", ".bmp", ".gif", ".tiff", ".webp"];
|
||||
const count = Object.values(selectedFilesMap).filter((file) => {
|
||||
const ext = file.fileName?.toLowerCase().match(/\.[^.]+$/)?.[0] || "";
|
||||
return imageExtensions.includes(ext);
|
||||
}).length;
|
||||
setImageFileCount(count);
|
||||
}, [selectedFilesMap]);
|
||||
|
||||
const handleManualSubmit = async () => {
|
||||
try {
|
||||
const values = await manualForm.validateFields();
|
||||
setSubmitting(true);
|
||||
// Send templateId instead of labelingConfig
|
||||
const requestData = {
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
datasetId: values.datasetId,
|
||||
templateId: values.templateId,
|
||||
};
|
||||
await createAnnotationTaskUsingPost(requestData);
|
||||
message?.success?.("创建标注任务成功");
|
||||
onClose();
|
||||
onRefresh();
|
||||
} catch (err: any) {
|
||||
console.error("Create annotation task failed", err);
|
||||
const msg = err?.message || err?.data?.message || "创建失败,请稍后重试";
|
||||
(message as any)?.error?.(msg);
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAutoSubmit = async () => {
|
||||
try {
|
||||
const values = await autoForm.validateFields();
|
||||
|
||||
if (imageFileCount === 0) {
|
||||
message.error("请至少选择一个图像文件");
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmitting(true);
|
||||
|
||||
const imageExtensions = [".jpg", ".jpeg", ".png", ".bmp", ".gif", ".tiff", ".webp"];
|
||||
const imageFileIds = Object.values(selectedFilesMap)
|
||||
.filter((file) => {
|
||||
const ext = file.fileName?.toLowerCase().match(/\.[^.]+$/)?.[0] || "";
|
||||
return imageExtensions.includes(ext);
|
||||
})
|
||||
.map((file) => file.id);
|
||||
|
||||
const payload = {
|
||||
name: values.name,
|
||||
datasetId: values.datasetId,
|
||||
fileIds: imageFileIds,
|
||||
config: {
|
||||
modelSize: values.modelSize,
|
||||
confThreshold: values.confThreshold,
|
||||
targetClasses: selectAllClasses ? [] : values.targetClasses || [],
|
||||
outputDatasetName: values.outputDatasetName || undefined,
|
||||
},
|
||||
};
|
||||
|
||||
await createAutoAnnotationTaskUsingPost(payload);
|
||||
message.success("自动标注任务创建成功");
|
||||
// 触发上层刷新自动标注任务列表
|
||||
(onRefresh as any)?.("auto");
|
||||
onClose();
|
||||
} catch (error: any) {
|
||||
if (error.errorFields) return;
|
||||
console.error("Failed to create auto annotation task:", error);
|
||||
message.error(error.message || "创建自动标注任务失败");
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClassSelectionChange = (checked: boolean) => {
|
||||
setSelectAllClasses(checked);
|
||||
if (checked) {
|
||||
autoForm.setFieldsValue({ targetClasses: [] });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
title="创建标注任务"
|
||||
footer={
|
||||
<>
|
||||
<Button onClick={onClose} disabled={submitting}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={activeMode === "manual" ? handleManualSubmit : handleAutoSubmit}
|
||||
loading={submitting}
|
||||
>
|
||||
确定
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
width={800}
|
||||
>
|
||||
<Tabs
|
||||
activeKey={activeMode}
|
||||
onChange={(key) => setActiveMode(key as "manual" | "auto")}
|
||||
items={[
|
||||
{
|
||||
key: "manual",
|
||||
label: "手动标注",
|
||||
children: (
|
||||
<Form form={manualForm} layout="vertical">
|
||||
{/* 数据集 与 标注工程名称 并排显示(数据集在左) */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Form.Item
|
||||
label="数据集"
|
||||
name="datasetId"
|
||||
rules={[{ required: true, message: "请选择数据集" }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择数据集"
|
||||
options={datasets.map((dataset) => {
|
||||
return {
|
||||
label: (
|
||||
<div className="flex items-center justify-between gap-3 py-2">
|
||||
<div className="flex items-center font-sm text-gray-900">
|
||||
<span className="mr-2">{(dataset as any).icon}</span>
|
||||
<span>{dataset.name}</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">{dataset.size}</div>
|
||||
</div>
|
||||
),
|
||||
value: dataset.id,
|
||||
};
|
||||
})}
|
||||
onChange={(value) => {
|
||||
// 如果用户未手动修改名称,则用数据集名称作为默认任务名
|
||||
if (!nameManuallyEdited) {
|
||||
const ds = datasets.find((d) => d.id === value);
|
||||
if (ds) {
|
||||
let defaultName = ds.name || "";
|
||||
if (defaultName.length < 3) {
|
||||
defaultName = `${defaultName}-标注`;
|
||||
}
|
||||
manualForm.setFieldsValue({ name: defaultName });
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="标注工程名称"
|
||||
name="name"
|
||||
rules={[
|
||||
{
|
||||
validator: (_rule, value) => {
|
||||
const trimmed = (value || "").trim();
|
||||
if (!trimmed) {
|
||||
return Promise.reject(new Error("请输入任务名称"));
|
||||
}
|
||||
if (trimmed.length < 3) {
|
||||
return Promise.reject(
|
||||
new Error("任务名称至少需要 3 个字符(不含首尾空格,Label Studio 限制)"),
|
||||
);
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
placeholder="输入标注工程名称"
|
||||
onChange={() => setNameManuallyEdited(true)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
{/* 描述变为可选 */}
|
||||
<Form.Item label="描述" name="description">
|
||||
<TextArea placeholder="(可选)详细描述标注任务的要求和目标" rows={3} />
|
||||
</Form.Item>
|
||||
|
||||
{/* 标注模板选择 */}
|
||||
<Form.Item
|
||||
label="标注模板"
|
||||
name="templateId"
|
||||
rules={[{ required: true, message: "请选择标注模板" }]}
|
||||
>
|
||||
<Select
|
||||
placeholder={templates.length === 0 ? "暂无可用模板,请先创建模板" : "请选择标注模板"}
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
notFoundContent={templates.length === 0 ? "暂无模板,请前往「标注模板」页面创建" : "未找到匹配的模板"}
|
||||
options={templates.map((template) => ({
|
||||
label: template.name,
|
||||
value: template.id,
|
||||
// Add description as subtitle
|
||||
title: template.description,
|
||||
}))}
|
||||
optionRender={(option) => (
|
||||
<div>
|
||||
<div style={{ fontWeight: 500 }}>{option.label}</div>
|
||||
{option.data.title && (
|
||||
<div style={{ fontSize: 12, color: '#999', marginTop: 2 }}>
|
||||
{option.data.title}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "auto",
|
||||
label: "自动标注",
|
||||
children: (
|
||||
<Form form={autoForm} layout="vertical" preserve={false}>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="任务名称"
|
||||
rules={[
|
||||
{ required: true, message: "请输入任务名称" },
|
||||
{ max: 100, message: "任务名称不能超过100个字符" },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入任务名称" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="选择数据集和图像文件" required>
|
||||
<DatasetFileTransfer
|
||||
open
|
||||
selectedFilesMap={selectedFilesMap}
|
||||
onSelectedFilesChange={setSelectedFilesMap}
|
||||
onDatasetSelect={(dataset) => {
|
||||
setSelectedDataset(dataset as Dataset | null);
|
||||
autoForm.setFieldsValue({ datasetId: dataset?.id ?? "" });
|
||||
}}
|
||||
datasetTypeFilter={DatasetType.IMAGE}
|
||||
/>
|
||||
{selectedDataset && (
|
||||
<div className="mt-2 p-2 bg-blue-50 rounded border border-blue-200 text-xs">
|
||||
当前数据集:<span className="font-medium">{selectedDataset.name}</span> - 已选择
|
||||
<span className="font-medium text-blue-600"> {imageFileCount} </span>个图像文件
|
||||
</div>
|
||||
)}
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
hidden
|
||||
name="datasetId"
|
||||
rules={[{ required: true, message: "请选择数据集" }]}
|
||||
>
|
||||
<Input type="hidden" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="modelSize"
|
||||
label="模型规模"
|
||||
rules={[{ required: true, message: "请选择模型规模" }]}
|
||||
initialValue="l"
|
||||
>
|
||||
<Select>
|
||||
<Option value="n">YOLOv8n (最快)</Option>
|
||||
<Option value="s">YOLOv8s</Option>
|
||||
<Option value="m">YOLOv8m</Option>
|
||||
<Option value="l">YOLOv8l (推荐)</Option>
|
||||
<Option value="x">YOLOv8x (最精确)</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="confThreshold"
|
||||
label="置信度阈值"
|
||||
rules={[{ required: true, message: "请选择置信度阈值" }]}
|
||||
initialValue={0.7}
|
||||
>
|
||||
<Slider
|
||||
min={0.1}
|
||||
max={0.9}
|
||||
step={0.05}
|
||||
tooltip={{ formatter: (v) => `${(v || 0) * 100}%` }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="目标类别">
|
||||
<Checkbox
|
||||
checked={selectAllClasses}
|
||||
onChange={(e) => handleClassSelectionChange(e.target.checked)}
|
||||
>
|
||||
选中所有类别
|
||||
</Checkbox>
|
||||
{!selectAllClasses && (
|
||||
<Form.Item name="targetClasses" noStyle>
|
||||
<Select mode="multiple" placeholder="选择目标类别" style={{ marginTop: 8 }}>
|
||||
{COCO_CLASSES.map((cls) => (
|
||||
<Option key={cls.id} value={cls.id}>
|
||||
{cls.label} ({cls.name})
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="outputDatasetName" label="输出数据集名称 (可选)">
|
||||
<Input placeholder="留空则将结果写入原数据集的标签中" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,50 +1,67 @@
|
||||
import { get, post, put, del } from "@/utils/request";
|
||||
|
||||
// 标注任务管理相关接口
|
||||
export function queryAnnotationTasksUsingGet(params?: any) {
|
||||
return get("/api/annotation/project", params);
|
||||
}
|
||||
|
||||
export function createAnnotationTaskUsingPost(data: any) {
|
||||
return post("/api/annotation/project", data);
|
||||
}
|
||||
|
||||
export function syncAnnotationTaskUsingPost(data: any) {
|
||||
return post(`/api/annotation/task/sync`, data);
|
||||
}
|
||||
|
||||
export function deleteAnnotationTaskByIdUsingDelete(mappingId: string) {
|
||||
// Backend expects mapping UUID as path parameter
|
||||
return del(`/api/annotation/project/${mappingId}`);
|
||||
}
|
||||
|
||||
export function loginAnnotationUsingGet(mappingId: string) {
|
||||
return get("/api/annotation/project/${mappingId}/login");
|
||||
}
|
||||
|
||||
// 标签配置管理
|
||||
export function getTagConfigUsingGet() {
|
||||
return get("/api/annotation/tags/config");
|
||||
}
|
||||
|
||||
// 标注模板管理
|
||||
export function queryAnnotationTemplatesUsingGet(params?: any) {
|
||||
return get("/api/annotation/template", params);
|
||||
}
|
||||
|
||||
export function createAnnotationTemplateUsingPost(data: any) {
|
||||
return post("/api/annotation/template", data);
|
||||
}
|
||||
|
||||
export function updateAnnotationTemplateByIdUsingPut(
|
||||
templateId: string | number,
|
||||
data: any
|
||||
) {
|
||||
return put(`/api/annotation/template/${templateId}`, data);
|
||||
}
|
||||
|
||||
export function deleteAnnotationTemplateByIdUsingDelete(
|
||||
templateId: string | number
|
||||
) {
|
||||
return del(`/api/annotation/template/${templateId}`);
|
||||
}
|
||||
import { get, post, put, del, download } from "@/utils/request";
|
||||
|
||||
// 标注任务管理相关接口
|
||||
export function queryAnnotationTasksUsingGet(params?: any) {
|
||||
return get("/api/annotation/project", params);
|
||||
}
|
||||
|
||||
export function createAnnotationTaskUsingPost(data: any) {
|
||||
return post("/api/annotation/project", data);
|
||||
}
|
||||
|
||||
export function syncAnnotationTaskUsingPost(data: any) {
|
||||
return post(`/api/annotation/task/sync`, data);
|
||||
}
|
||||
|
||||
export function deleteAnnotationTaskByIdUsingDelete(mappingId: string) {
|
||||
// Backend expects mapping UUID as path parameter
|
||||
return del(`/api/annotation/project/${mappingId}`);
|
||||
}
|
||||
|
||||
// 标签配置管理
|
||||
export function getTagConfigUsingGet() {
|
||||
return get("/api/annotation/tags/config");
|
||||
}
|
||||
|
||||
// 标注模板管理
|
||||
export function queryAnnotationTemplatesUsingGet(params?: any) {
|
||||
return get("/api/annotation/template", params);
|
||||
}
|
||||
|
||||
export function createAnnotationTemplateUsingPost(data: any) {
|
||||
return post("/api/annotation/template", data);
|
||||
}
|
||||
|
||||
export function updateAnnotationTemplateByIdUsingPut(
|
||||
templateId: string | number,
|
||||
data: any
|
||||
) {
|
||||
return put(`/api/annotation/template/${templateId}`, data);
|
||||
}
|
||||
|
||||
export function deleteAnnotationTemplateByIdUsingDelete(
|
||||
templateId: string | number
|
||||
) {
|
||||
return del(`/api/annotation/template/${templateId}`);
|
||||
}
|
||||
|
||||
// 自动标注任务管理
|
||||
export function queryAutoAnnotationTasksUsingGet(params?: any) {
|
||||
return get("/api/annotation/auto", params);
|
||||
}
|
||||
|
||||
export function createAutoAnnotationTaskUsingPost(data: any) {
|
||||
return post("/api/annotation/auto", data);
|
||||
}
|
||||
|
||||
export function deleteAutoAnnotationTaskByIdUsingDelete(taskId: string) {
|
||||
return del(`/api/annotation/auto/${taskId}`);
|
||||
}
|
||||
|
||||
export function getAutoAnnotationTaskStatusUsingGet(taskId: string) {
|
||||
return get(`/api/annotation/auto/${taskId}/status`);
|
||||
}
|
||||
|
||||
export function downloadAutoAnnotationResultUsingGet(taskId: string) {
|
||||
return download(`/api/annotation/auto/${taskId}/download`);
|
||||
}
|
||||
|
||||
@@ -1,245 +1,260 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Breadcrumb, App, Tabs } from "antd";
|
||||
import {
|
||||
ReloadOutlined,
|
||||
DownloadOutlined,
|
||||
UploadOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
} 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,
|
||||
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 navigate = useNavigate();
|
||||
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 handleDownload = async () => {
|
||||
await downloadDatasetUsingGet(dataset.id);
|
||||
message.success("文件下载成功");
|
||||
};
|
||||
|
||||
const handleDeleteDataset = async () => {
|
||||
await deleteDatasetByIdUsingDelete(dataset.id);
|
||||
navigate("/data/management");
|
||||
message.success("数据集删除成功");
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const refreshData = () => {
|
||||
handleRefresh(false);
|
||||
};
|
||||
window.addEventListener("update:dataset", refreshData);
|
||||
return () => {
|
||||
window.removeEventListener("update:dataset", refreshData);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 基本信息描述项
|
||||
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?.updatedAt,
|
||||
},
|
||||
];
|
||||
|
||||
// 数据集操作列表
|
||||
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 /> },
|
||||
// ],
|
||||
onClick: () => handleDownload(),
|
||||
},
|
||||
{
|
||||
key: "refresh",
|
||||
label: "刷新",
|
||||
icon: <ReloadOutlined />,
|
||||
onClick: handleRefresh,
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
danger: true,
|
||||
confirm: {
|
||||
title: "确认删除该数据集?",
|
||||
description: "删除后该数据集将无法恢复,请谨慎操作。",
|
||||
okText: "删除",
|
||||
cancelText: "取消",
|
||||
okType: "danger",
|
||||
},
|
||||
icon: <DeleteOutlined />,
|
||||
onClick: handleDeleteDataset,
|
||||
},
|
||||
];
|
||||
|
||||
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="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}/>
|
||||
)}
|
||||
{activeTab === "lineage" && <DataLineageFlow dataset={dataset} />}
|
||||
{activeTab === "quality" && <DataQuality />}
|
||||
</div>
|
||||
</div>
|
||||
<ImportConfiguration
|
||||
data={dataset}
|
||||
open={showUploadDialog}
|
||||
onClose={() => setShowUploadDialog(false)}
|
||||
updateEvent="update:dataset"
|
||||
/>
|
||||
<EditDataset
|
||||
data={dataset}
|
||||
open={showEditDialog}
|
||||
onClose={() => setShowEditDialog(false)}
|
||||
onRefresh={handleRefresh}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Breadcrumb, App, Tabs } from "antd";
|
||||
import {
|
||||
ReloadOutlined,
|
||||
DownloadOutlined,
|
||||
UploadOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
} 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,
|
||||
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 navigate = useNavigate();
|
||||
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('', 1, 10); // 从根目录开始,第一页
|
||||
}, []);
|
||||
|
||||
const handleRefresh = async (showMessage = true, prefixOverride?: string) => {
|
||||
fetchDataset();
|
||||
// 刷新当前目录,保持在当前页
|
||||
const targetPrefix =
|
||||
prefixOverride !== undefined
|
||||
? prefixOverride
|
||||
: filesOperation.pagination.prefix;
|
||||
filesOperation.fetchFiles(
|
||||
targetPrefix,
|
||||
filesOperation.pagination.current,
|
||||
filesOperation.pagination.pageSize
|
||||
);
|
||||
if (showMessage) message.success({ content: "数据刷新成功" });
|
||||
};
|
||||
|
||||
const handleDownload = async () => {
|
||||
await downloadDatasetUsingGet(dataset.id);
|
||||
message.success("文件下载成功");
|
||||
};
|
||||
|
||||
const handleDeleteDataset = async () => {
|
||||
await deleteDatasetByIdUsingDelete(dataset.id);
|
||||
navigate("/data/management");
|
||||
message.success("数据集删除成功");
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const refreshData = (e: Event) => {
|
||||
const custom = e as CustomEvent<{ prefix?: string }>;
|
||||
const prefixOverride = custom.detail?.prefix;
|
||||
handleRefresh(false, prefixOverride);
|
||||
};
|
||||
window.addEventListener("update:dataset", refreshData as EventListener);
|
||||
return () => {
|
||||
window.removeEventListener(
|
||||
"update:dataset",
|
||||
refreshData as EventListener
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 基本信息描述项
|
||||
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?.updatedAt,
|
||||
},
|
||||
];
|
||||
|
||||
// 数据集操作列表
|
||||
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 /> },
|
||||
// ],
|
||||
onClick: () => handleDownload(),
|
||||
},
|
||||
{
|
||||
key: "refresh",
|
||||
label: "刷新",
|
||||
icon: <ReloadOutlined />,
|
||||
onClick: handleRefresh,
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
danger: true,
|
||||
confirm: {
|
||||
title: "确认删除该数据集?",
|
||||
description: "删除后该数据集将无法恢复,请谨慎操作。",
|
||||
okText: "删除",
|
||||
cancelText: "取消",
|
||||
okType: "danger",
|
||||
},
|
||||
icon: <DeleteOutlined />,
|
||||
onClick: handleDeleteDataset,
|
||||
},
|
||||
];
|
||||
|
||||
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="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}/>
|
||||
)}
|
||||
{activeTab === "lineage" && <DataLineageFlow dataset={dataset} />}
|
||||
{activeTab === "quality" && <DataQuality />}
|
||||
</div>
|
||||
</div>
|
||||
<ImportConfiguration
|
||||
data={dataset}
|
||||
open={showUploadDialog}
|
||||
onClose={() => setShowUploadDialog(false)}
|
||||
prefix={filesOperation.pagination.prefix}
|
||||
updateEvent="update:dataset"
|
||||
/>
|
||||
<EditDataset
|
||||
data={dataset}
|
||||
open={showEditDialog}
|
||||
onClose={() => setShowEditDialog(false)}
|
||||
onRefresh={handleRefresh}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,277 +1,294 @@
|
||||
import { Select, Input, Form, Radio, Modal, Button, UploadFile, Switch } from "antd";
|
||||
import { InboxOutlined } from "@ant-design/icons";
|
||||
import { dataSourceOptions } from "../../dataset.const";
|
||||
import { Dataset, DataSource } from "../../dataset.model";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { queryTasksUsingGet } from "@/pages/DataCollection/collection.apis";
|
||||
import { updateDatasetByIdUsingPut } from "../../dataset.api";
|
||||
import { sliceFile } from "@/utils/file.util";
|
||||
import Dragger from "antd/es/upload/Dragger";
|
||||
|
||||
export default function ImportConfiguration({
|
||||
data,
|
||||
open,
|
||||
onClose,
|
||||
updateEvent = "update:dataset",
|
||||
}: {
|
||||
data: Dataset | null;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
updateEvent?: string;
|
||||
}) {
|
||||
const [form] = Form.useForm();
|
||||
const [collectionOptions, setCollectionOptions] = useState([]);
|
||||
const [importConfig, setImportConfig] = useState<any>({
|
||||
source: DataSource.UPLOAD,
|
||||
});
|
||||
|
||||
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
||||
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,
|
||||
updateEvent,
|
||||
hasArchive: importConfig.hasArchive,
|
||||
},
|
||||
})
|
||||
);
|
||||
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 () => {
|
||||
if (importConfig.source !== DataSource.COLLECTION) return;
|
||||
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();
|
||||
setFileList([]);
|
||||
form.setFieldsValue({ files: null });
|
||||
setImportConfig({ source: importConfig.source ? importConfig.source : DataSource.UPLOAD });
|
||||
};
|
||||
|
||||
const handleImportData = async () => {
|
||||
if (!data) return;
|
||||
if (importConfig.source === DataSource.UPLOAD) {
|
||||
await handleUpload(data);
|
||||
} else if (importConfig.source === DataSource.COLLECTION) {
|
||||
await updateDatasetByIdUsingPut(data.id, {
|
||||
...importConfig,
|
||||
});
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
resetState();
|
||||
fetchCollectionTasks();
|
||||
}
|
||||
}, [open, importConfig.source]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="导入数据"
|
||||
open={open}
|
||||
width={600}
|
||||
onCancel={() => {
|
||||
onClose();
|
||||
resetState();
|
||||
}}
|
||||
maskClosable={false}
|
||||
footer={
|
||||
<>
|
||||
<Button onClick={onClose}>取消</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={!fileList?.length && !importConfig.dataSource}
|
||||
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>
|
||||
)}
|
||||
|
||||
{/* 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="hasArchive"
|
||||
valuePropName="checked"
|
||||
initialValue={true}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="上传文件"
|
||||
name="files"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "请上传文件",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Dragger
|
||||
className="w-full"
|
||||
onRemove={handleRemoveFile}
|
||||
beforeUpload={handleBeforeUpload}
|
||||
multiple
|
||||
>
|
||||
<p className="ant-upload-drag-icon">
|
||||
<InboxOutlined />
|
||||
</p>
|
||||
<p className="ant-upload-text">本地文件上传</p>
|
||||
<p className="ant-upload-hint">拖拽文件到此处或点击选择文件</p>
|
||||
</Dragger>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
import { Select, Input, Form, Radio, Modal, Button, UploadFile, Switch } from "antd";
|
||||
import { InboxOutlined } from "@ant-design/icons";
|
||||
import { dataSourceOptions } from "../../dataset.const";
|
||||
import { Dataset, DataSource } from "../../dataset.model";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { queryTasksUsingGet } from "@/pages/DataCollection/collection.apis";
|
||||
import { updateDatasetByIdUsingPut } from "../../dataset.api";
|
||||
import { sliceFile } from "@/utils/file.util";
|
||||
import Dragger from "antd/es/upload/Dragger";
|
||||
|
||||
export default function ImportConfiguration({
|
||||
data,
|
||||
open,
|
||||
onClose,
|
||||
updateEvent = "update:dataset",
|
||||
prefix,
|
||||
}: {
|
||||
data: Dataset | null;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
updateEvent?: string;
|
||||
prefix?: string;
|
||||
}) {
|
||||
const [form] = Form.useForm();
|
||||
const [collectionOptions, setCollectionOptions] = useState([]);
|
||||
const [importConfig, setImportConfig] = useState<any>({
|
||||
source: DataSource.UPLOAD,
|
||||
});
|
||||
const [currentPrefix, setCurrentPrefix] = useState<string>("");
|
||||
|
||||
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
||||
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);
|
||||
});
|
||||
console.log('[ImportConfiguration] Uploading with currentPrefix:', currentPrefix);
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("upload:dataset", {
|
||||
detail: {
|
||||
dataset,
|
||||
files: fileSliceList,
|
||||
updateEvent,
|
||||
hasArchive: importConfig.hasArchive,
|
||||
prefix: currentPrefix,
|
||||
},
|
||||
})
|
||||
);
|
||||
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 () => {
|
||||
if (importConfig.source !== DataSource.COLLECTION) return;
|
||||
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 = () => {
|
||||
console.log('[ImportConfiguration] resetState called, preserving currentPrefix:', currentPrefix);
|
||||
form.resetFields();
|
||||
setFileList([]);
|
||||
form.setFieldsValue({ files: null });
|
||||
setImportConfig({ source: importConfig.source ? importConfig.source : DataSource.UPLOAD });
|
||||
console.log('[ImportConfiguration] resetState done, currentPrefix still:', currentPrefix);
|
||||
};
|
||||
|
||||
const handleImportData = async () => {
|
||||
if (!data) return;
|
||||
console.log('[ImportConfiguration] handleImportData called, currentPrefix:', currentPrefix);
|
||||
if (importConfig.source === DataSource.UPLOAD) {
|
||||
await handleUpload(data);
|
||||
} else if (importConfig.source === DataSource.COLLECTION) {
|
||||
await updateDatasetByIdUsingPut(data.id, {
|
||||
...importConfig,
|
||||
});
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setCurrentPrefix(prefix || "");
|
||||
console.log('[ImportConfiguration] Modal opened with prefix:', prefix);
|
||||
resetState();
|
||||
fetchCollectionTasks();
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
// Separate effect for fetching collection tasks when source changes
|
||||
useEffect(() => {
|
||||
if (open && importConfig.source === DataSource.COLLECTION) {
|
||||
fetchCollectionTasks();
|
||||
}
|
||||
}, [importConfig.source]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="导入数据"
|
||||
open={open}
|
||||
width={600}
|
||||
onCancel={() => {
|
||||
onClose();
|
||||
resetState();
|
||||
}}
|
||||
maskClosable={false}
|
||||
footer={
|
||||
<>
|
||||
<Button onClick={onClose}>取消</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={!fileList?.length && !importConfig.dataSource}
|
||||
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>
|
||||
)}
|
||||
|
||||
{/* 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="hasArchive"
|
||||
valuePropName="checked"
|
||||
initialValue={true}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="上传文件"
|
||||
name="files"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "请上传文件",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Dragger
|
||||
className="w-full"
|
||||
onRemove={handleRemoveFile}
|
||||
beforeUpload={handleBeforeUpload}
|
||||
multiple
|
||||
>
|
||||
<p className="ant-upload-drag-icon">
|
||||
<InboxOutlined />
|
||||
</p>
|
||||
<p className="ant-upload-text">本地文件上传</p>
|
||||
<p className="ant-upload-hint">拖拽文件到此处或点击选择文件</p>
|
||||
</Dragger>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,316 +1,399 @@
|
||||
import { Button, Descriptions, DescriptionsProps, Modal, Table } from "antd";
|
||||
import { formatBytes, formatDateTime } from "@/utils/unit";
|
||||
import { Download, Trash2, Folder, File } from "lucide-react";
|
||||
import { datasetTypeMap } from "../../dataset.const";
|
||||
|
||||
export default function Overview({ dataset, filesOperation, fetchDataset }) {
|
||||
const {
|
||||
fileList,
|
||||
pagination,
|
||||
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: "pvcName",
|
||||
label: "存储名称",
|
||||
children: dataset.pvcName || "未知",
|
||||
},
|
||||
{
|
||||
key: "createdAt",
|
||||
label: "创建时间",
|
||||
children: dataset.createdAt,
|
||||
},
|
||||
{
|
||||
key: "updatedAt",
|
||||
label: "更新时间",
|
||||
children: dataset.updatedAt,
|
||||
},
|
||||
{
|
||||
key: "description",
|
||||
label: "描述",
|
||||
children: dataset.description || "无",
|
||||
},
|
||||
];
|
||||
|
||||
// 文件列表列定义
|
||||
const columns = [
|
||||
{
|
||||
title: "文件名",
|
||||
dataIndex: "fileName",
|
||||
key: "fileName",
|
||||
fixed: "left",
|
||||
render: (text: string, record: any) => {
|
||||
const isDirectory = record.id.startsWith('directory-');
|
||||
const iconSize = 16;
|
||||
|
||||
const content = (
|
||||
<div className="flex items-center">
|
||||
{isDirectory ? (
|
||||
<Folder className="mr-2 text-blue-500" size={iconSize} />
|
||||
) : (
|
||||
<File className="mr-2 text-black" size={iconSize} />
|
||||
)}
|
||||
<span className="truncate text-black">{text}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (isDirectory) {
|
||||
return (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={(e) => {
|
||||
const currentPath = filesOperation.pagination.prefix || '';
|
||||
const newPath = `${currentPath}${record.fileName}`;
|
||||
filesOperation.fetchFiles(newPath);
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={(e) => {}}
|
||||
>
|
||||
{content}
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "大小",
|
||||
dataIndex: "fileSize",
|
||||
key: "fileSize",
|
||||
width: 150,
|
||||
render: (text: number, record: any) => {
|
||||
const isDirectory = record.id.startsWith('directory-');
|
||||
if (isDirectory) {
|
||||
return "-";
|
||||
}
|
||||
return formatBytes(text)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "上传时间",
|
||||
dataIndex: "uploadTime",
|
||||
key: "uploadTime",
|
||||
width: 200,
|
||||
render: (text) => formatDateTime(text),
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
width: 180,
|
||||
fixed: "right",
|
||||
render: (_, record) => {
|
||||
const isDirectory = record.id.startsWith('directory-');
|
||||
if (isDirectory) {
|
||||
return <div className="flex"/>;
|
||||
}
|
||||
return (
|
||||
<div className="flex">
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
onClick={() => handleDownloadFile(record)}
|
||||
>
|
||||
下载
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
onClick={async () => {
|
||||
await handleDeleteFile(record);
|
||||
fetchDataset()
|
||||
}
|
||||
}
|
||||
>
|
||||
删除
|
||||
</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">
|
||||
<div className="mb-2">
|
||||
{(filesOperation.pagination.prefix || '') !== '' && (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => {
|
||||
// 获取上一级目录
|
||||
const currentPath = filesOperation.pagination.prefix || '';
|
||||
const pathParts = currentPath.split('/').filter(Boolean);
|
||||
pathParts.pop(); // 移除最后一个目录
|
||||
const parentPath = pathParts.length > 0 ? `${pathParts.join('/')}/` : '';
|
||||
filesOperation.fetchFiles(parentPath);
|
||||
}}
|
||||
className="p-0"
|
||||
>
|
||||
<span className="flex items-center text-blue-500">
|
||||
<svg
|
||||
className="w-4 h-4 mr-1"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M10 19l-7-7m0 0l7-7m-7 7h18"
|
||||
/>
|
||||
</svg>
|
||||
返回上一级
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
{filesOperation.pagination.prefix && (
|
||||
<span className="ml-2 text-gray-600">当前路径: {filesOperation.pagination.prefix}</span>
|
||||
)}
|
||||
</div>
|
||||
<Table
|
||||
size="middle"
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={fileList}
|
||||
// rowSelection={rowSelection}
|
||||
scroll={{ x: "max-content", y: 600 }}
|
||||
pagination={{
|
||||
...pagination,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
onChange: (page, pageSize) => {
|
||||
filesOperation.setPagination(prev => ({
|
||||
...prev,
|
||||
current: page,
|
||||
pageSize: pageSize
|
||||
}));
|
||||
filesOperation.fetchFiles(pagination.prefix, page, pageSize);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
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";
|
||||
|
||||
export default function Overview({ dataset, filesOperation, fetchDataset }) {
|
||||
const { modal, message } = App.useApp();
|
||||
const {
|
||||
fileList,
|
||||
pagination,
|
||||
selectedFiles,
|
||||
setSelectedFiles,
|
||||
previewVisible,
|
||||
previewFileName,
|
||||
previewContent,
|
||||
setPreviewVisible,
|
||||
handleDeleteFile,
|
||||
handleDownloadFile,
|
||||
handleBatchDeleteFiles,
|
||||
handleBatchExport,
|
||||
handleCreateDirectory,
|
||||
handleDownloadDirectory,
|
||||
handleDeleteDirectory,
|
||||
} = 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: "pvcName",
|
||||
label: "存储名称",
|
||||
children: dataset.pvcName || "未知",
|
||||
},
|
||||
{
|
||||
key: "createdAt",
|
||||
label: "创建时间",
|
||||
children: dataset.createdAt,
|
||||
},
|
||||
{
|
||||
key: "updatedAt",
|
||||
label: "更新时间",
|
||||
children: dataset.updatedAt,
|
||||
},
|
||||
{
|
||||
key: "description",
|
||||
label: "描述",
|
||||
children: dataset.description || "无",
|
||||
},
|
||||
];
|
||||
|
||||
// 文件列表列定义
|
||||
const columns = [
|
||||
{
|
||||
title: "文件名",
|
||||
dataIndex: "fileName",
|
||||
key: "fileName",
|
||||
fixed: "left",
|
||||
render: (text: string, record: any) => {
|
||||
const isDirectory = record.id.startsWith('directory-');
|
||||
const iconSize = 16;
|
||||
|
||||
const content = (
|
||||
<div className="flex items-center">
|
||||
{isDirectory ? (
|
||||
<Folder className="mr-2 text-blue-500" size={iconSize} />
|
||||
) : (
|
||||
<File className="mr-2 text-black" size={iconSize} />
|
||||
)}
|
||||
<span className="truncate text-black">{text}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (isDirectory) {
|
||||
return (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={(e) => {
|
||||
const currentPath = filesOperation.pagination.prefix || '';
|
||||
// 文件夹路径必须以斜杠结尾
|
||||
const newPath = `${currentPath}${record.fileName}/`;
|
||||
filesOperation.fetchFiles(newPath, 1, filesOperation.pagination.pageSize);
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={(e) => {}}
|
||||
>
|
||||
{content}
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "大小",
|
||||
dataIndex: "fileSize",
|
||||
key: "fileSize",
|
||||
width: 150,
|
||||
render: (text: number, record: any) => {
|
||||
const isDirectory = record.id.startsWith('directory-');
|
||||
if (isDirectory) {
|
||||
return formatBytes(record.fileSize || 0);
|
||||
}
|
||||
return formatBytes(text)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "包含文件数",
|
||||
dataIndex: "fileCount",
|
||||
key: "fileCount",
|
||||
width: 120,
|
||||
render: (text: number, record: any) => {
|
||||
const isDirectory = record.id.startsWith('directory-');
|
||||
if (!isDirectory) {
|
||||
return "-";
|
||||
}
|
||||
return record.fileCount ?? 0;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "上传时间",
|
||||
dataIndex: "uploadTime",
|
||||
key: "uploadTime",
|
||||
width: 200,
|
||||
render: (text) => formatDateTime(text),
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
width: 180,
|
||||
fixed: "right",
|
||||
render: (_, record) => {
|
||||
const isDirectory = record.id.startsWith('directory-');
|
||||
|
||||
if (isDirectory) {
|
||||
const currentPath = filesOperation.pagination.prefix || '';
|
||||
const fullPath = `${currentPath}${record.fileName}/`;
|
||||
|
||||
return (
|
||||
<div className="flex">
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
onClick={() => handleDownloadDirectory(fullPath, record.fileName)}
|
||||
>
|
||||
下载
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
onClick={() => {
|
||||
modal.confirm({
|
||||
title: '确认删除文件夹?',
|
||||
content: `删除文件夹 "${record.fileName}" 将同时删除其中的所有文件和子文件夹,此操作不可恢复。`,
|
||||
okText: '删除',
|
||||
okType: 'danger',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
await handleDeleteDirectory(fullPath, record.fileName);
|
||||
fetchDataset();
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex">
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
onClick={() => handleDownloadFile(record)}
|
||||
>
|
||||
下载
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
onClick={async () => {
|
||||
await handleDeleteFile(record);
|
||||
fetchDataset()
|
||||
}
|
||||
}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
)},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className=" flex flex-col gap-4">
|
||||
{/* 基本信息 */}
|
||||
<Descriptions
|
||||
title="基本信息"
|
||||
layout="vertical"
|
||||
size="small"
|
||||
items={items}
|
||||
column={5}
|
||||
/>
|
||||
|
||||
{/* 文件列表 */}
|
||||
<div className="flex items-center justify-between mt-8 mb-2">
|
||||
<h2 className="text-base font-semibold">文件列表</h2>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
let dirName = "";
|
||||
modal.confirm({
|
||||
title: "新建文件夹",
|
||||
content: (
|
||||
<Input
|
||||
autoFocus
|
||||
placeholder="请输入文件夹名称"
|
||||
onChange={(e) => {
|
||||
dirName = e.target.value?.trim();
|
||||
}}
|
||||
/>
|
||||
),
|
||||
okText: "确定",
|
||||
cancelText: "取消",
|
||||
onOk: async () => {
|
||||
if (!dirName) {
|
||||
message.warning("请输入文件夹名称");
|
||||
return Promise.reject();
|
||||
}
|
||||
await handleCreateDirectory(dirName);
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
新建文件夹
|
||||
</Button>
|
||||
</div>
|
||||
{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">
|
||||
<div className="mb-2">
|
||||
{(filesOperation.pagination.prefix || '') !== '' && (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => {
|
||||
// 获取上一级目录
|
||||
const currentPath = filesOperation.pagination.prefix || '';
|
||||
// 移除末尾的斜杠,然后按斜杠分割
|
||||
const trimmedPath = currentPath.replace(/\/$/, '');
|
||||
const pathParts = trimmedPath.split('/');
|
||||
// 移除最后一个目录名
|
||||
pathParts.pop();
|
||||
// 重新组合路径,如果还有内容则加斜杠,否则为空
|
||||
const parentPath = pathParts.length > 0 ? `${pathParts.join('/')}/` : '';
|
||||
filesOperation.fetchFiles(parentPath, 1, filesOperation.pagination.pageSize);
|
||||
}}
|
||||
className="p-0"
|
||||
>
|
||||
<span className="flex items-center text-blue-500">
|
||||
<svg
|
||||
className="w-4 h-4 mr-1"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M10 19l-7-7m0 0l7-7m-7 7h18"
|
||||
/>
|
||||
</svg>
|
||||
返回上一级
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
{filesOperation.pagination.prefix && (
|
||||
<span className="ml-2 text-gray-600">当前路径: {filesOperation.pagination.prefix}</span>
|
||||
)}
|
||||
</div>
|
||||
<Table
|
||||
size="middle"
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={fileList}
|
||||
// rowSelection={rowSelection}
|
||||
scroll={{ x: "max-content", y: 600 }}
|
||||
pagination={{
|
||||
...pagination,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
onChange: (page, pageSize) => {
|
||||
filesOperation.fetchFiles(filesOperation.pagination.prefix, page, pageSize);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,149 +1,186 @@
|
||||
import type {
|
||||
Dataset,
|
||||
DatasetFile,
|
||||
} from "@/pages/DataManagement/dataset.model";
|
||||
import { App } from "antd";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
deleteDatasetFileUsingDelete,
|
||||
downloadFileByIdUsingGet,
|
||||
exportDatasetUsingPost,
|
||||
queryDatasetFilesUsingGet,
|
||||
} from "../dataset.api";
|
||||
import { useParams } from "react-router";
|
||||
|
||||
export function useFilesOperation(dataset: Dataset) {
|
||||
const { message } = App.useApp();
|
||||
const { id } = useParams(); // 获取动态路由参数
|
||||
|
||||
// 文件相关状态
|
||||
const [fileList, setFileList] = useState<DatasetFile[]>([]);
|
||||
const [selectedFiles, setSelectedFiles] = useState<number[]>([]);
|
||||
const [pagination, setPagination] = useState<{
|
||||
current: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
prefix?: string;
|
||||
}>({ current: 1, pageSize: 10, total: 0, prefix: '' });
|
||||
|
||||
// 文件预览相关状态
|
||||
const [previewVisible, setPreviewVisible] = useState(false);
|
||||
const [previewContent, setPreviewContent] = useState("");
|
||||
const [previewFileName, setPreviewFileName] = useState("");
|
||||
|
||||
const fetchFiles = async (prefix: string = '', current, pageSize) => {
|
||||
const params: any = {
|
||||
page: current ? current : pagination.current,
|
||||
size: pageSize ? pageSize : pagination.pageSize,
|
||||
isWithDirectory: true,
|
||||
};
|
||||
|
||||
if (prefix !== undefined) {
|
||||
params.prefix = prefix;
|
||||
} else if (pagination.prefix) {
|
||||
params.prefix = pagination.prefix;
|
||||
}
|
||||
|
||||
const { data } = await queryDatasetFilesUsingGet(id!, params);
|
||||
setFileList(data.content || []);
|
||||
|
||||
// Update pagination with current prefix
|
||||
setPagination(prev => ({
|
||||
...prev,
|
||||
prefix: prefix !== undefined ? prefix : prev.prefix,
|
||||
total: data.totalElements || 0,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleBatchDeleteFiles = () => {
|
||||
if (selectedFiles.length === 0) {
|
||||
message.warning({ content: "请先选择要删除的文件" });
|
||||
return;
|
||||
}
|
||||
// 执行批量删除逻辑
|
||||
selectedFiles.forEach(async (fileId) => {
|
||||
await fetch(`/api/datasets/${dataset.id}/files/${fileId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
});
|
||||
fetchFiles(); // 刷新文件列表
|
||||
setSelectedFiles([]); // 清空选中状态
|
||||
message.success({
|
||||
content: `已删除 ${selectedFiles.length} 个文件`,
|
||||
});
|
||||
};
|
||||
|
||||
const handleDownloadFile = async (file: DatasetFile) => {
|
||||
// 实际导出逻辑
|
||||
await downloadFileByIdUsingGet(dataset.id, file.id, file.fileName);
|
||||
// 假设导出成功
|
||||
message.success({
|
||||
content: `已导出 1 个文件`,
|
||||
});
|
||||
setSelectedFiles([]); // 清空选中状态
|
||||
};
|
||||
|
||||
const handleShowFile = (file: any) => async () => {
|
||||
// 请求文件内容并弹窗预览
|
||||
try {
|
||||
const res = await fetch(`/api/datasets/${dataset.id}/file/${file.id}`);
|
||||
const data = await res.text();
|
||||
setPreviewFileName(file.fileName);
|
||||
setPreviewContent(data);
|
||||
setPreviewVisible(true);
|
||||
} catch (err) {
|
||||
message.error({ content: "文件预览失败" });
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteFile = async (file) => {
|
||||
try {
|
||||
await deleteDatasetFileUsingDelete(dataset.id, file.id);
|
||||
fetchFiles(); // 刷新文件列表
|
||||
message.success({ content: `文件 ${file.fileName} 已删除` });
|
||||
} catch (error) {
|
||||
message.error({ content: `文件 ${file.fileName} 删除失败` });
|
||||
}
|
||||
};
|
||||
|
||||
const handleBatchExport = () => {
|
||||
if (selectedFiles.length === 0) {
|
||||
message.warning({ content: "请先选择要导出的文件" });
|
||||
return;
|
||||
}
|
||||
// 执行批量导出逻辑
|
||||
exportDatasetUsingPost(dataset.id, { fileIds: selectedFiles })
|
||||
.then(() => {
|
||||
message.success({
|
||||
content: `已导出 ${selectedFiles.length} 个文件`,
|
||||
});
|
||||
setSelectedFiles([]); // 清空选中状态
|
||||
})
|
||||
.catch(() => {
|
||||
message.error({
|
||||
content: "导出失败,请稍后再试",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
fileList,
|
||||
selectedFiles,
|
||||
setSelectedFiles,
|
||||
pagination,
|
||||
setPagination,
|
||||
previewVisible,
|
||||
setPreviewVisible,
|
||||
previewContent,
|
||||
previewFileName,
|
||||
setPreviewContent,
|
||||
setPreviewFileName,
|
||||
fetchFiles,
|
||||
setFileList,
|
||||
handleBatchDeleteFiles,
|
||||
handleDownloadFile,
|
||||
handleShowFile,
|
||||
handleDeleteFile,
|
||||
handleBatchExport,
|
||||
};
|
||||
}
|
||||
import type {
|
||||
Dataset,
|
||||
DatasetFile,
|
||||
} from "@/pages/DataManagement/dataset.model";
|
||||
import { App } from "antd";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
deleteDatasetFileUsingDelete,
|
||||
downloadFileByIdUsingGet,
|
||||
exportDatasetUsingPost,
|
||||
queryDatasetFilesUsingGet,
|
||||
createDatasetDirectoryUsingPost,
|
||||
downloadDirectoryUsingGet,
|
||||
deleteDirectoryUsingDelete,
|
||||
} from "../dataset.api";
|
||||
import { useParams } from "react-router";
|
||||
|
||||
export function useFilesOperation(dataset: Dataset) {
|
||||
const { message } = App.useApp();
|
||||
const { id } = useParams(); // 获取动态路由参数
|
||||
|
||||
// 文件相关状态
|
||||
const [fileList, setFileList] = useState<DatasetFile[]>([]);
|
||||
const [selectedFiles, setSelectedFiles] = useState<number[]>([]);
|
||||
const [pagination, setPagination] = useState<{
|
||||
current: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
prefix?: string;
|
||||
}>({ current: 1, pageSize: 10, total: 0, prefix: '' });
|
||||
|
||||
// 文件预览相关状态
|
||||
const [previewVisible, setPreviewVisible] = useState(false);
|
||||
const [previewContent, setPreviewContent] = useState("");
|
||||
const [previewFileName, setPreviewFileName] = useState("");
|
||||
|
||||
const fetchFiles = async (prefix?: string, current?, pageSize?) => {
|
||||
// 如果明确传了 prefix(包括空字符串),使用传入的值;否则使用当前 pagination.prefix
|
||||
const targetPrefix = prefix !== undefined ? prefix : (pagination.prefix || '');
|
||||
|
||||
const params: any = {
|
||||
page: current !== undefined ? current : pagination.current,
|
||||
size: pageSize !== undefined ? pageSize : pagination.pageSize,
|
||||
isWithDirectory: true,
|
||||
prefix: targetPrefix,
|
||||
};
|
||||
|
||||
const { data } = await queryDatasetFilesUsingGet(id!, params);
|
||||
setFileList(data.content || []);
|
||||
|
||||
// Update pagination with current prefix
|
||||
setPagination(prev => ({
|
||||
...prev,
|
||||
current: params.page,
|
||||
pageSize: params.size,
|
||||
prefix: targetPrefix,
|
||||
total: data.totalElements || 0,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleBatchDeleteFiles = () => {
|
||||
if (selectedFiles.length === 0) {
|
||||
message.warning({ content: "请先选择要删除的文件" });
|
||||
return;
|
||||
}
|
||||
// 执行批量删除逻辑
|
||||
selectedFiles.forEach(async (fileId) => {
|
||||
await fetch(`/api/datasets/${dataset.id}/files/${fileId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
});
|
||||
fetchFiles(); // 刷新文件列表
|
||||
setSelectedFiles([]); // 清空选中状态
|
||||
message.success({
|
||||
content: `已删除 ${selectedFiles.length} 个文件`,
|
||||
});
|
||||
};
|
||||
|
||||
const handleDownloadFile = async (file: DatasetFile) => {
|
||||
// 实际导出逻辑
|
||||
await downloadFileByIdUsingGet(dataset.id, file.id, file.fileName);
|
||||
// 假设导出成功
|
||||
message.success({
|
||||
content: `已导出 1 个文件`,
|
||||
});
|
||||
setSelectedFiles([]); // 清空选中状态
|
||||
};
|
||||
|
||||
const handleShowFile = (file: any) => async () => {
|
||||
// 请求文件内容并弹窗预览
|
||||
try {
|
||||
const res = await fetch(`/api/datasets/${dataset.id}/file/${file.id}`);
|
||||
const data = await res.text();
|
||||
setPreviewFileName(file.fileName);
|
||||
setPreviewContent(data);
|
||||
setPreviewVisible(true);
|
||||
} catch (err) {
|
||||
message.error({ content: "文件预览失败" });
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteFile = async (file) => {
|
||||
try {
|
||||
await deleteDatasetFileUsingDelete(dataset.id, file.id);
|
||||
fetchFiles(); // 刷新文件列表
|
||||
message.success({ content: `文件 ${file.fileName} 已删除` });
|
||||
} catch (error) {
|
||||
message.error({ content: `文件 ${file.fileName} 删除失败` });
|
||||
}
|
||||
};
|
||||
|
||||
const handleBatchExport = () => {
|
||||
if (selectedFiles.length === 0) {
|
||||
message.warning({ content: "请先选择要导出的文件" });
|
||||
return;
|
||||
}
|
||||
// 执行批量导出逻辑
|
||||
exportDatasetUsingPost(dataset.id, { fileIds: selectedFiles })
|
||||
.then(() => {
|
||||
message.success({
|
||||
content: `已导出 ${selectedFiles.length} 个文件`,
|
||||
});
|
||||
setSelectedFiles([]); // 清空选中状态
|
||||
})
|
||||
.catch(() => {
|
||||
message.error({
|
||||
content: "导出失败,请稍后再试",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
fileList,
|
||||
selectedFiles,
|
||||
setSelectedFiles,
|
||||
pagination,
|
||||
setPagination,
|
||||
previewVisible,
|
||||
setPreviewVisible,
|
||||
previewContent,
|
||||
previewFileName,
|
||||
setPreviewContent,
|
||||
setPreviewFileName,
|
||||
fetchFiles,
|
||||
setFileList,
|
||||
handleBatchDeleteFiles,
|
||||
handleDownloadFile,
|
||||
handleShowFile,
|
||||
handleDeleteFile,
|
||||
handleBatchExport,
|
||||
handleCreateDirectory: async (directoryName: string) => {
|
||||
const currentPrefix = pagination.prefix || "";
|
||||
try {
|
||||
await createDatasetDirectoryUsingPost(dataset.id, {
|
||||
parentPrefix: currentPrefix,
|
||||
directoryName,
|
||||
});
|
||||
// 创建成功后刷新当前目录,重置到第一页
|
||||
await fetchFiles(currentPrefix, 1, pagination.pageSize);
|
||||
message.success({ content: `文件夹 ${directoryName} 创建成功` });
|
||||
} catch (error) {
|
||||
message.error({ content: `文件夹 ${directoryName} 创建失败` });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
handleDownloadDirectory: async (directoryPath: string, directoryName: string) => {
|
||||
try {
|
||||
await downloadDirectoryUsingGet(dataset.id, directoryPath);
|
||||
message.success({ content: `文件夹 ${directoryName} 下载成功` });
|
||||
} catch (error) {
|
||||
message.error({ content: `文件夹 ${directoryName} 下载失败` });
|
||||
}
|
||||
},
|
||||
handleDeleteDirectory: async (directoryPath: string, directoryName: string) => {
|
||||
try {
|
||||
await deleteDirectoryUsingDelete(dataset.id, directoryPath);
|
||||
// 删除成功后刷新当前目录
|
||||
const currentPrefix = pagination.prefix || "";
|
||||
await fetchFiles(currentPrefix, 1, pagination.pageSize);
|
||||
message.success({ content: `文件夹 ${directoryName} 已删除` });
|
||||
} catch (error) {
|
||||
message.error({ content: `文件夹 ${directoryName} 删除失败` });
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,398 +1,399 @@
|
||||
import { Card, Button, Statistic, Table, Tooltip, Tag, App } from "antd";
|
||||
import {
|
||||
DownloadOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
PlusOutlined,
|
||||
UploadOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import TagManager from "@/components/business/TagManagement";
|
||||
import { Link, useNavigate } from "react-router";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { SearchControls } from "@/components/SearchControls";
|
||||
import CardView from "@/components/CardView";
|
||||
import type { Dataset } from "@/pages/DataManagement/dataset.model";
|
||||
import { datasetStatusMap, datasetTypeMap, mapDataset } from "../dataset.const";
|
||||
import useFetchData from "@/hooks/useFetchData";
|
||||
import {
|
||||
downloadDatasetUsingGet,
|
||||
getDatasetStatisticsUsingGet,
|
||||
queryDatasetsUsingGet,
|
||||
deleteDatasetByIdUsingDelete,
|
||||
createDatasetTagUsingPost,
|
||||
queryDatasetTagsUsingGet,
|
||||
deleteDatasetTagUsingDelete,
|
||||
updateDatasetTagUsingPut,
|
||||
} from "../dataset.api";
|
||||
import { formatBytes } from "@/utils/unit";
|
||||
import EditDataset from "../Create/EditDataset";
|
||||
import ImportConfiguration from "../Detail/components/ImportConfiguration";
|
||||
|
||||
export default function DatasetManagementPage() {
|
||||
const navigate = useNavigate();
|
||||
const { message } = App.useApp();
|
||||
const [viewMode, setViewMode] = useState<"card" | "list">("card");
|
||||
const [editDatasetOpen, setEditDatasetOpen] = useState(false);
|
||||
const [currentDataset, setCurrentDataset] = useState<Dataset | null>(null);
|
||||
const [showUploadDialog, setShowUploadDialog] = useState(false);
|
||||
const [statisticsData, setStatisticsData] = useState<any>({
|
||||
count: {},
|
||||
size: {},
|
||||
});
|
||||
|
||||
async function fetchStatistics() {
|
||||
const { data } = await getDatasetStatisticsUsingGet();
|
||||
|
||||
const statistics = {
|
||||
size: [
|
||||
{
|
||||
title: "数据集总数",
|
||||
value: data?.totalDatasets || 0,
|
||||
},
|
||||
{
|
||||
title: "文件总数",
|
||||
value: data?.totalFiles || 0,
|
||||
},
|
||||
{
|
||||
title: "总大小",
|
||||
value: formatBytes(data?.totalSize) || '0 B',
|
||||
},
|
||||
],
|
||||
count: [
|
||||
{
|
||||
title: "文本",
|
||||
value: data?.count?.text || 0,
|
||||
},
|
||||
{
|
||||
title: "图像",
|
||||
value: data?.count?.image || 0,
|
||||
},
|
||||
{
|
||||
title: "音频",
|
||||
value: data?.count?.audio || 0,
|
||||
},
|
||||
{
|
||||
title: "视频",
|
||||
value: data?.count?.video || 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
setStatisticsData(statistics);
|
||||
}
|
||||
|
||||
const [tags, setTags] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchTags = async () => {
|
||||
const { data } = await queryDatasetTagsUsingGet();
|
||||
setTags(data.map((tag) => tag.name));
|
||||
};
|
||||
fetchTags();
|
||||
}, []);
|
||||
|
||||
const filterOptions = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: "type",
|
||||
label: "类型",
|
||||
options: [...Object.values(datasetTypeMap)],
|
||||
},
|
||||
{
|
||||
key: "status",
|
||||
label: "状态",
|
||||
options: [...Object.values(datasetStatusMap)],
|
||||
},
|
||||
{
|
||||
key: "tags",
|
||||
label: "标签",
|
||||
mode: "multiple",
|
||||
options: tags.map((tag) => ({ label: tag, value: tag })),
|
||||
},
|
||||
],
|
||||
[tags]
|
||||
);
|
||||
|
||||
const {
|
||||
loading,
|
||||
tableData,
|
||||
searchParams,
|
||||
pagination,
|
||||
fetchData,
|
||||
setSearchParams,
|
||||
handleFiltersChange,
|
||||
handleKeywordChange,
|
||||
} = useFetchData<Dataset>(
|
||||
queryDatasetsUsingGet,
|
||||
mapDataset,
|
||||
30000, // 30秒轮询间隔
|
||||
true, // 自动刷新
|
||||
[fetchStatistics], // 额外的轮询函数
|
||||
0
|
||||
);
|
||||
|
||||
const handleDownloadDataset = async (dataset: Dataset) => {
|
||||
await downloadDatasetUsingGet(dataset.id, dataset.name);
|
||||
message.success("数据集下载成功");
|
||||
};
|
||||
|
||||
const handleDeleteDataset = async (id: number) => {
|
||||
if (!id) return;
|
||||
await deleteDatasetByIdUsingDelete(id);
|
||||
fetchData({ pageOffset: 0 });
|
||||
message.success("数据删除成功");
|
||||
};
|
||||
|
||||
const handleImportData = (dataset: Dataset) => {
|
||||
setCurrentDataset(dataset);
|
||||
setShowUploadDialog(true);
|
||||
};
|
||||
|
||||
const handleRefresh = async (showMessage = true) => {
|
||||
await fetchData({ pageOffset: 0 });
|
||||
if (showMessage) {
|
||||
message.success("数据已刷新");
|
||||
}
|
||||
};
|
||||
|
||||
const operations = [
|
||||
{
|
||||
key: "edit",
|
||||
label: "编辑",
|
||||
icon: <EditOutlined />,
|
||||
onClick: (item: Dataset) => {
|
||||
setCurrentDataset(item);
|
||||
setEditDatasetOpen(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "import",
|
||||
label: "导入",
|
||||
icon: <UploadOutlined />,
|
||||
onClick: (item: Dataset) => {
|
||||
handleImportData(item);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "download",
|
||||
label: "下载",
|
||||
icon: <DownloadOutlined />,
|
||||
onClick: (item: Dataset) => {
|
||||
if (!item.id) return;
|
||||
handleDownloadDataset(item);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
danger: true,
|
||||
confirm: {
|
||||
title: "确认删除该数据集?",
|
||||
description: "删除后该数据集将无法恢复,请谨慎操作。",
|
||||
okText: "删除",
|
||||
cancelText: "取消",
|
||||
okType: "danger",
|
||||
},
|
||||
icon: <DeleteOutlined />,
|
||||
onClick: (item: Dataset) => handleDeleteDataset(item.id),
|
||||
},
|
||||
];
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: "名称",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
fixed: "left",
|
||||
render: (name, record) => (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => navigate(`/data/management/detail/${record.id}`)}
|
||||
>
|
||||
{name}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "类型",
|
||||
dataIndex: "type",
|
||||
key: "type",
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
render: (status: any) => {
|
||||
return (
|
||||
<Tag icon={status?.icon} color={status?.color}>
|
||||
{status?.label}
|
||||
</Tag>
|
||||
);
|
||||
},
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: "大小",
|
||||
dataIndex: "size",
|
||||
key: "size",
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: "文件数",
|
||||
dataIndex: "fileCount",
|
||||
key: "fileCount",
|
||||
width: 100,
|
||||
},
|
||||
// {
|
||||
// title: "创建者",
|
||||
// dataIndex: "createdBy",
|
||||
// key: "createdBy",
|
||||
// width: 120,
|
||||
// },
|
||||
{
|
||||
title: "存储路径",
|
||||
dataIndex: "targetLocation",
|
||||
key: "targetLocation",
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
dataIndex: "createdAt",
|
||||
key: "createdAt",
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: "更新时间",
|
||||
dataIndex: "updatedAt",
|
||||
key: "updatedAt",
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "actions",
|
||||
width: 200,
|
||||
fixed: "right",
|
||||
render: (_: any, record: Dataset) => (
|
||||
<div className="flex items-center gap-2">
|
||||
{operations.map((op) => (
|
||||
<Tooltip key={op.key} title={op.label}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={op.icon}
|
||||
onClick={() => op.onClick(record)}
|
||||
/>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const renderCardView = () => (
|
||||
<CardView
|
||||
loading={loading}
|
||||
data={tableData}
|
||||
pageSize={9}
|
||||
operations={operations}
|
||||
pagination={pagination}
|
||||
onView={(dataset) => {
|
||||
navigate("/data/management/detail/" + dataset.id);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderListView = () => (
|
||||
<Card>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={tableData}
|
||||
pagination={pagination}
|
||||
rowKey="id"
|
||||
scroll={{ x: "max-content", y: "calc(100vh - 30rem)" }}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const refresh = () => {
|
||||
handleRefresh(true);
|
||||
};
|
||||
window.addEventListener("update:datasets", refresh);
|
||||
return () => {
|
||||
window.removeEventListener("update:datasets", refresh);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="gap-4 h-full flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-xl font-bold">数据管理</h1>
|
||||
<div className="flex gap-2 items-center">
|
||||
{/* tasks */}
|
||||
<TagManager
|
||||
onCreate={createDatasetTagUsingPost}
|
||||
onDelete={(ids: string) => deleteDatasetTagUsingDelete({ ids })}
|
||||
onUpdate={updateDatasetTagUsingPut}
|
||||
onFetch={queryDatasetTagsUsingGet}
|
||||
/>
|
||||
<Link to="/data/management/create">
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined className="w-4 h-4 mr-2" />}
|
||||
>
|
||||
创建数据集
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Statistics */}
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<Card>
|
||||
<div className="grid grid-cols-3">
|
||||
{statisticsData.size?.map?.((item) => (
|
||||
<Statistic
|
||||
title={item.title}
|
||||
key={item.title}
|
||||
value={`${item.value}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<SearchControls
|
||||
searchTerm={searchParams.keyword}
|
||||
onSearchChange={handleKeywordChange}
|
||||
searchPlaceholder="搜索数据集名称、描述"
|
||||
filters={filterOptions}
|
||||
onFiltersChange={handleFiltersChange}
|
||||
onClearFilters={() => setSearchParams({ ...searchParams, filter: {} })}
|
||||
viewMode={viewMode}
|
||||
onViewModeChange={setViewMode}
|
||||
showViewToggle
|
||||
onReload={handleRefresh}
|
||||
/>
|
||||
{viewMode === "card" ? renderCardView() : renderListView()}
|
||||
<EditDataset
|
||||
open={editDatasetOpen}
|
||||
data={currentDataset}
|
||||
onClose={() => {
|
||||
setCurrentDataset(null);
|
||||
setEditDatasetOpen(false);
|
||||
}}
|
||||
onRefresh={handleRefresh}
|
||||
/>
|
||||
<ImportConfiguration
|
||||
data={currentDataset}
|
||||
open={showUploadDialog}
|
||||
onClose={() => {
|
||||
setCurrentDataset(null);
|
||||
setShowUploadDialog(false);
|
||||
}}
|
||||
updateEvent="update:datasets"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import { Card, Button, Statistic, Table, Tooltip, Tag, App } from "antd";
|
||||
import {
|
||||
DownloadOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
PlusOutlined,
|
||||
UploadOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import TagManager from "@/components/business/TagManagement";
|
||||
import { Link, useNavigate } from "react-router";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { SearchControls } from "@/components/SearchControls";
|
||||
import CardView from "@/components/CardView";
|
||||
import type { Dataset } from "@/pages/DataManagement/dataset.model";
|
||||
import { datasetStatusMap, datasetTypeMap, mapDataset } from "../dataset.const";
|
||||
import useFetchData from "@/hooks/useFetchData";
|
||||
import {
|
||||
downloadDatasetUsingGet,
|
||||
getDatasetStatisticsUsingGet,
|
||||
queryDatasetsUsingGet,
|
||||
deleteDatasetByIdUsingDelete,
|
||||
createDatasetTagUsingPost,
|
||||
queryDatasetTagsUsingGet,
|
||||
deleteDatasetTagUsingDelete,
|
||||
updateDatasetTagUsingPut,
|
||||
} from "../dataset.api";
|
||||
import { formatBytes } from "@/utils/unit";
|
||||
import EditDataset from "../Create/EditDataset";
|
||||
import ImportConfiguration from "../Detail/components/ImportConfiguration";
|
||||
|
||||
export default function DatasetManagementPage() {
|
||||
const navigate = useNavigate();
|
||||
const { message } = App.useApp();
|
||||
const [viewMode, setViewMode] = useState<"card" | "list">("card");
|
||||
const [editDatasetOpen, setEditDatasetOpen] = useState(false);
|
||||
const [currentDataset, setCurrentDataset] = useState<Dataset | null>(null);
|
||||
const [showUploadDialog, setShowUploadDialog] = useState(false);
|
||||
const [statisticsData, setStatisticsData] = useState<any>({
|
||||
count: {},
|
||||
size: {},
|
||||
});
|
||||
|
||||
async function fetchStatistics() {
|
||||
const { data } = await getDatasetStatisticsUsingGet();
|
||||
|
||||
const statistics = {
|
||||
size: [
|
||||
{
|
||||
title: "数据集总数",
|
||||
value: data?.totalDatasets || 0,
|
||||
},
|
||||
{
|
||||
title: "文件总数",
|
||||
value: data?.totalFiles || 0,
|
||||
},
|
||||
{
|
||||
title: "总大小",
|
||||
value: formatBytes(data?.totalSize) || '0 B',
|
||||
},
|
||||
],
|
||||
count: [
|
||||
{
|
||||
title: "文本",
|
||||
value: data?.count?.text || 0,
|
||||
},
|
||||
{
|
||||
title: "图像",
|
||||
value: data?.count?.image || 0,
|
||||
},
|
||||
{
|
||||
title: "音频",
|
||||
value: data?.count?.audio || 0,
|
||||
},
|
||||
{
|
||||
title: "视频",
|
||||
value: data?.count?.video || 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
setStatisticsData(statistics);
|
||||
}
|
||||
|
||||
const [tags, setTags] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchTags = async () => {
|
||||
const { data } = await queryDatasetTagsUsingGet();
|
||||
setTags(data.map((tag) => tag.name));
|
||||
};
|
||||
fetchTags();
|
||||
}, []);
|
||||
|
||||
const filterOptions = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: "type",
|
||||
label: "类型",
|
||||
options: [...Object.values(datasetTypeMap)],
|
||||
},
|
||||
{
|
||||
key: "status",
|
||||
label: "状态",
|
||||
options: [...Object.values(datasetStatusMap)],
|
||||
},
|
||||
{
|
||||
key: "tags",
|
||||
label: "标签",
|
||||
mode: "multiple",
|
||||
options: tags.map((tag) => ({ label: tag, value: tag })),
|
||||
},
|
||||
],
|
||||
[tags]
|
||||
);
|
||||
|
||||
const {
|
||||
loading,
|
||||
tableData,
|
||||
searchParams,
|
||||
pagination,
|
||||
fetchData,
|
||||
setSearchParams,
|
||||
handleFiltersChange,
|
||||
handleKeywordChange,
|
||||
} = useFetchData<Dataset>(
|
||||
queryDatasetsUsingGet,
|
||||
mapDataset,
|
||||
30000, // 30秒轮询间隔
|
||||
true, // 自动刷新
|
||||
[fetchStatistics], // 额外的轮询函数
|
||||
0
|
||||
);
|
||||
|
||||
const handleDownloadDataset = async (dataset: Dataset) => {
|
||||
await downloadDatasetUsingGet(dataset.id, dataset.name);
|
||||
message.success("数据集下载成功");
|
||||
};
|
||||
|
||||
const handleDeleteDataset = async (id: number) => {
|
||||
if (!id) return;
|
||||
await deleteDatasetByIdUsingDelete(id);
|
||||
fetchData({ pageOffset: 0 });
|
||||
message.success("数据删除成功");
|
||||
};
|
||||
|
||||
const handleImportData = (dataset: Dataset) => {
|
||||
setCurrentDataset(dataset);
|
||||
setShowUploadDialog(true);
|
||||
};
|
||||
|
||||
const handleRefresh = async (showMessage = true) => {
|
||||
await fetchData({ pageOffset: 0 });
|
||||
if (showMessage) {
|
||||
message.success("数据已刷新");
|
||||
}
|
||||
};
|
||||
|
||||
const operations = [
|
||||
{
|
||||
key: "edit",
|
||||
label: "编辑",
|
||||
icon: <EditOutlined />,
|
||||
onClick: (item: Dataset) => {
|
||||
setCurrentDataset(item);
|
||||
setEditDatasetOpen(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "import",
|
||||
label: "导入",
|
||||
icon: <UploadOutlined />,
|
||||
onClick: (item: Dataset) => {
|
||||
handleImportData(item);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "download",
|
||||
label: "下载",
|
||||
icon: <DownloadOutlined />,
|
||||
onClick: (item: Dataset) => {
|
||||
if (!item.id) return;
|
||||
handleDownloadDataset(item);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
danger: true,
|
||||
confirm: {
|
||||
title: "确认删除该数据集?",
|
||||
description: "删除后该数据集将无法恢复,请谨慎操作。",
|
||||
okText: "删除",
|
||||
cancelText: "取消",
|
||||
okType: "danger",
|
||||
},
|
||||
icon: <DeleteOutlined />,
|
||||
onClick: (item: Dataset) => handleDeleteDataset(item.id),
|
||||
},
|
||||
];
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: "名称",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
fixed: "left",
|
||||
render: (name, record) => (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => navigate(`/data/management/detail/${record.id}`)}
|
||||
>
|
||||
{name}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "类型",
|
||||
dataIndex: "type",
|
||||
key: "type",
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
render: (status: any) => {
|
||||
return (
|
||||
<Tag icon={status?.icon} color={status?.color}>
|
||||
{status?.label}
|
||||
</Tag>
|
||||
);
|
||||
},
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: "大小",
|
||||
dataIndex: "size",
|
||||
key: "size",
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: "文件数",
|
||||
dataIndex: "fileCount",
|
||||
key: "fileCount",
|
||||
width: 100,
|
||||
},
|
||||
// {
|
||||
// title: "创建者",
|
||||
// dataIndex: "createdBy",
|
||||
// key: "createdBy",
|
||||
// width: 120,
|
||||
// },
|
||||
{
|
||||
title: "存储路径",
|
||||
dataIndex: "targetLocation",
|
||||
key: "targetLocation",
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
dataIndex: "createdAt",
|
||||
key: "createdAt",
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: "更新时间",
|
||||
dataIndex: "updatedAt",
|
||||
key: "updatedAt",
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "actions",
|
||||
width: 200,
|
||||
fixed: "right",
|
||||
render: (_: any, record: Dataset) => (
|
||||
<div className="flex items-center gap-2">
|
||||
{operations.map((op) => (
|
||||
<Tooltip key={op.key} title={op.label}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={op.icon}
|
||||
onClick={() => op.onClick(record)}
|
||||
/>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const renderCardView = () => (
|
||||
<CardView
|
||||
loading={loading}
|
||||
data={tableData}
|
||||
pageSize={9}
|
||||
operations={operations}
|
||||
pagination={pagination}
|
||||
onView={(dataset) => {
|
||||
navigate("/data/management/detail/" + dataset.id);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderListView = () => (
|
||||
<Card>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={tableData}
|
||||
pagination={pagination}
|
||||
rowKey="id"
|
||||
scroll={{ x: "max-content", y: "calc(100vh - 30rem)" }}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const refresh = () => {
|
||||
handleRefresh(true);
|
||||
};
|
||||
window.addEventListener("update:datasets", refresh);
|
||||
return () => {
|
||||
window.removeEventListener("update:datasets", refresh);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="gap-4 h-full flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-xl font-bold">数据管理</h1>
|
||||
<div className="flex gap-2 items-center">
|
||||
{/* tasks */}
|
||||
<TagManager
|
||||
onCreate={createDatasetTagUsingPost}
|
||||
onDelete={(ids: string) => deleteDatasetTagUsingDelete({ ids })}
|
||||
onUpdate={updateDatasetTagUsingPut}
|
||||
onFetch={queryDatasetTagsUsingGet}
|
||||
/>
|
||||
<Link to="/data/management/create">
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined className="w-4 h-4 mr-2" />}
|
||||
>
|
||||
创建数据集
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Statistics */}
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<Card>
|
||||
<div className="grid grid-cols-3">
|
||||
{statisticsData.size?.map?.((item) => (
|
||||
<Statistic
|
||||
title={item.title}
|
||||
key={item.title}
|
||||
value={`${item.value}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<SearchControls
|
||||
searchTerm={searchParams.keyword}
|
||||
onSearchChange={handleKeywordChange}
|
||||
searchPlaceholder="搜索数据集名称、描述"
|
||||
filters={filterOptions}
|
||||
onFiltersChange={handleFiltersChange}
|
||||
onClearFilters={() => setSearchParams({ ...searchParams, filter: {} })}
|
||||
viewMode={viewMode}
|
||||
onViewModeChange={setViewMode}
|
||||
showViewToggle
|
||||
onReload={handleRefresh}
|
||||
/>
|
||||
{viewMode === "card" ? renderCardView() : renderListView()}
|
||||
<EditDataset
|
||||
open={editDatasetOpen}
|
||||
data={currentDataset}
|
||||
onClose={() => {
|
||||
setCurrentDataset(null);
|
||||
setEditDatasetOpen(false);
|
||||
}}
|
||||
onRefresh={handleRefresh}
|
||||
/>
|
||||
<ImportConfiguration
|
||||
data={currentDataset}
|
||||
open={showUploadDialog}
|
||||
onClose={() => {
|
||||
setCurrentDataset(null);
|
||||
setShowUploadDialog(false);
|
||||
}}
|
||||
prefix=""
|
||||
updateEvent="update:datasets"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,191 +1,220 @@
|
||||
import { get, post, put, del, download } from "@/utils/request";
|
||||
|
||||
// 数据集统计接口
|
||||
export function getDatasetStatisticsUsingGet() {
|
||||
return get("/api/data-management/datasets/statistics");
|
||||
}
|
||||
|
||||
export function queryDatasetStatisticsByIdUsingGet(id: string | number) {
|
||||
return get(`/api/data-management/datasets/${id}/statistics`);
|
||||
}
|
||||
|
||||
// 查询数据集列表
|
||||
export function queryDatasetsUsingGet(params?: any) {
|
||||
return get("/api/data-management/datasets", params);
|
||||
}
|
||||
|
||||
// 创建数据集
|
||||
export function createDatasetUsingPost(data: any) {
|
||||
return post("/api/data-management/datasets", data);
|
||||
}
|
||||
|
||||
// 根据ID获取数据集详情
|
||||
export function queryDatasetByIdUsingGet(id: string | number) {
|
||||
return get(`/api/data-management/datasets/${id}`);
|
||||
}
|
||||
|
||||
// 更新数据集
|
||||
export function updateDatasetByIdUsingPut(id: string | number, data: any) {
|
||||
return put(`/api/data-management/datasets/${id}`, data);
|
||||
}
|
||||
|
||||
// 删除数据集
|
||||
export function deleteDatasetByIdUsingDelete(id: string | number) {
|
||||
return del(`/api/data-management/datasets/${id}`);
|
||||
}
|
||||
|
||||
// 下载数据集
|
||||
export function downloadDatasetUsingGet(id: string | number) {
|
||||
return download(`/api/data-management/datasets/${id}/files/download`);
|
||||
}
|
||||
|
||||
// 验证数据集
|
||||
export function validateDatasetUsingPost(id: string | number, data?: any) {
|
||||
return post(`/api/data-management/datasets/${id}/validate`, data);
|
||||
}
|
||||
|
||||
// 获取数据集文件列表
|
||||
export function queryDatasetFilesUsingGet(id: string | number, params?: any) {
|
||||
return get(`/api/data-management/datasets/${id}/files`, params);
|
||||
}
|
||||
|
||||
// 上传数据集文件
|
||||
export function uploadDatasetFileUsingPost(id: string | number, data: any) {
|
||||
return post(`/api/data-management/datasets/${id}/files`, data);
|
||||
}
|
||||
|
||||
export function downloadFileByIdUsingGet(
|
||||
id: string | number,
|
||||
fileId: string | number,
|
||||
fileName: string
|
||||
) {
|
||||
return download(
|
||||
`/api/data-management/datasets/${id}/files/${fileId}/download`,
|
||||
null,
|
||||
fileName
|
||||
);
|
||||
}
|
||||
|
||||
// 删除数据集文件
|
||||
export function deleteDatasetFileUsingDelete(
|
||||
datasetId: string | number,
|
||||
fileId: string | number
|
||||
) {
|
||||
return del(`/api/data-management/datasets/${datasetId}/files/${fileId}`);
|
||||
}
|
||||
|
||||
// 文件预览
|
||||
export function previewDatasetUsingGet(id: string | number, params?: any) {
|
||||
return get(`/api/data-management/datasets/${id}/preview`, params);
|
||||
}
|
||||
|
||||
// 获取数据集标签
|
||||
export function queryDatasetTagsUsingGet(params?: any) {
|
||||
return get("/api/data-management/tags", params);
|
||||
}
|
||||
|
||||
// 创建数据集标签
|
||||
export function createDatasetTagUsingPost(data: any) {
|
||||
return post("/api/data-management/tags", data);
|
||||
}
|
||||
|
||||
// 更新数据集标签
|
||||
export function updateDatasetTagUsingPut(data: any) {
|
||||
return put(`/api/data-management/tags`, data);
|
||||
}
|
||||
|
||||
// 删除数据集标签
|
||||
export function deleteDatasetTagUsingDelete(data: any) {
|
||||
return del(`/api/data-management/tags`, data);
|
||||
}
|
||||
|
||||
// 数据集质量检查
|
||||
export function checkDatasetQualityUsingPost(id: string | number, data?: any) {
|
||||
return post(`/api/data-management/datasets/${id}/quality-check`, data);
|
||||
}
|
||||
|
||||
// 获取数据集质量报告
|
||||
export function getDatasetQualityReportUsingGet(id: string | number) {
|
||||
return get(`/api/data-management/datasets/${id}/quality-report`);
|
||||
}
|
||||
|
||||
// 数据集分析
|
||||
export function analyzeDatasetUsingPost(id: string | number, data?: any) {
|
||||
return post(`/api/data-management/datasets/${id}/analyze`, data);
|
||||
}
|
||||
|
||||
// 获取数据集分析结果
|
||||
export function getDatasetAnalysisUsingGet(id: string | number) {
|
||||
return get(`/api/data-management/datasets/${id}/analysis`);
|
||||
}
|
||||
|
||||
// 导出数据集
|
||||
export function exportDatasetUsingPost(id: string | number, data: any) {
|
||||
return post(`/api/data-management/datasets/${id}/export`, data);
|
||||
}
|
||||
|
||||
// 复制数据集
|
||||
export function copyDatasetUsingPost(id: string | number, data: any) {
|
||||
return post(`/api/data-management/datasets/${id}/copy`, data);
|
||||
}
|
||||
|
||||
// 获取数据集版本列表
|
||||
export function queryDatasetVersionsUsingGet(
|
||||
id: string | number,
|
||||
params?: any
|
||||
) {
|
||||
return get(`/api/data-management/datasets/${id}/versions`, params);
|
||||
}
|
||||
|
||||
// 创建数据集版本
|
||||
export function createDatasetVersionUsingPost(id: string | number, data: any) {
|
||||
return post(`/api/data-management/datasets/${id}/versions`, data);
|
||||
}
|
||||
|
||||
// 切换数据集版本
|
||||
export function switchDatasetVersionUsingPut(
|
||||
id: string | number,
|
||||
versionId: string | number
|
||||
) {
|
||||
return put(
|
||||
`/api/data-management/datasets/${id}/versions/${versionId}/switch`
|
||||
);
|
||||
}
|
||||
|
||||
// 删除数据集版本
|
||||
export function deleteDatasetVersionUsingDelete(
|
||||
id: string | number,
|
||||
versionId: string | number
|
||||
) {
|
||||
return del(`/api/data-management/datasets/${id}/versions/${versionId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件上传相关接口
|
||||
*/
|
||||
|
||||
export function preUploadUsingPost(id: string | number, data: any) {
|
||||
return post(
|
||||
`/api/data-management/datasets/${id}/files/upload/pre-upload`,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
export function cancelUploadUsingPut(id) {
|
||||
return put(
|
||||
`/api/data-management/datasets/upload/cancel-upload/${id}`,
|
||||
{},
|
||||
{ showLoading: false }
|
||||
);
|
||||
}
|
||||
|
||||
export function uploadFileChunkUsingPost(id: string | number, params, config) {
|
||||
return post(
|
||||
`/api/data-management/datasets/${id}/files/upload/chunk`,
|
||||
params,
|
||||
{
|
||||
showLoading: false,
|
||||
...config,
|
||||
}
|
||||
);
|
||||
}
|
||||
import { get, post, put, del, download } from "@/utils/request";
|
||||
|
||||
// 数据集统计接口
|
||||
export function getDatasetStatisticsUsingGet() {
|
||||
return get("/api/data-management/datasets/statistics");
|
||||
}
|
||||
|
||||
export function queryDatasetStatisticsByIdUsingGet(id: string | number) {
|
||||
return get(`/api/data-management/datasets/${id}/statistics`);
|
||||
}
|
||||
|
||||
// 查询数据集列表
|
||||
export function queryDatasetsUsingGet(params?: any) {
|
||||
return get("/api/data-management/datasets", params);
|
||||
}
|
||||
|
||||
// 创建数据集
|
||||
export function createDatasetUsingPost(data: any) {
|
||||
return post("/api/data-management/datasets", data);
|
||||
}
|
||||
|
||||
// 根据ID获取数据集详情
|
||||
export function queryDatasetByIdUsingGet(id: string | number) {
|
||||
return get(`/api/data-management/datasets/${id}`);
|
||||
}
|
||||
|
||||
// 更新数据集
|
||||
export function updateDatasetByIdUsingPut(id: string | number, data: any) {
|
||||
return put(`/api/data-management/datasets/${id}`, data);
|
||||
}
|
||||
|
||||
// 删除数据集
|
||||
export function deleteDatasetByIdUsingDelete(id: string | number) {
|
||||
return del(`/api/data-management/datasets/${id}`);
|
||||
}
|
||||
|
||||
// 下载数据集
|
||||
export function downloadDatasetUsingGet(id: string | number) {
|
||||
return download(`/api/data-management/datasets/${id}/files/download`);
|
||||
}
|
||||
|
||||
// 验证数据集
|
||||
export function validateDatasetUsingPost(id: string | number, data?: any) {
|
||||
return post(`/api/data-management/datasets/${id}/validate`, data);
|
||||
}
|
||||
|
||||
// 获取数据集文件列表
|
||||
export function queryDatasetFilesUsingGet(id: string | number, params?: any) {
|
||||
return get(`/api/data-management/datasets/${id}/files`, params);
|
||||
}
|
||||
|
||||
// 上传数据集文件
|
||||
export function uploadDatasetFileUsingPost(id: string | number, data: any) {
|
||||
return post(`/api/data-management/datasets/${id}/files`, data);
|
||||
}
|
||||
|
||||
// 新建数据集文件夹
|
||||
export function createDatasetDirectoryUsingPost(
|
||||
id: string | number,
|
||||
data: { parentPrefix?: string; directoryName: string }
|
||||
) {
|
||||
return post(`/api/data-management/datasets/${id}/files/directories`, data);
|
||||
}
|
||||
|
||||
// 下载文件夹(打包为zip)
|
||||
export function downloadDirectoryUsingGet(
|
||||
id: string | number,
|
||||
directoryPath: string
|
||||
) {
|
||||
const dirName = directoryPath.split('/').filter(Boolean).pop() || 'folder';
|
||||
return download(
|
||||
`/api/data-management/datasets/${id}/files/directories/download?prefix=${encodeURIComponent(directoryPath)}`,
|
||||
null,
|
||||
`${dirName}.zip`
|
||||
);
|
||||
}
|
||||
|
||||
// 删除文件夹(递归删除)
|
||||
export function deleteDirectoryUsingDelete(
|
||||
id: string | number,
|
||||
directoryPath: string
|
||||
) {
|
||||
return del(`/api/data-management/datasets/${id}/files/directories?prefix=${encodeURIComponent(directoryPath)}`);
|
||||
}
|
||||
|
||||
export function downloadFileByIdUsingGet(
|
||||
id: string | number,
|
||||
fileId: string | number,
|
||||
fileName: string
|
||||
) {
|
||||
return download(
|
||||
`/api/data-management/datasets/${id}/files/${fileId}/download`,
|
||||
null,
|
||||
fileName
|
||||
);
|
||||
}
|
||||
|
||||
// 删除数据集文件
|
||||
export function deleteDatasetFileUsingDelete(
|
||||
datasetId: string | number,
|
||||
fileId: string | number
|
||||
) {
|
||||
return del(`/api/data-management/datasets/${datasetId}/files/${fileId}`);
|
||||
}
|
||||
|
||||
// 文件预览
|
||||
export function previewDatasetUsingGet(id: string | number, params?: any) {
|
||||
return get(`/api/data-management/datasets/${id}/preview`, params);
|
||||
}
|
||||
|
||||
// 获取数据集标签
|
||||
export function queryDatasetTagsUsingGet(params?: any) {
|
||||
return get("/api/data-management/tags", params);
|
||||
}
|
||||
|
||||
// 创建数据集标签
|
||||
export function createDatasetTagUsingPost(data: any) {
|
||||
return post("/api/data-management/tags", data);
|
||||
}
|
||||
|
||||
// 更新数据集标签
|
||||
export function updateDatasetTagUsingPut(data: any) {
|
||||
return put(`/api/data-management/tags`, data);
|
||||
}
|
||||
|
||||
// 删除数据集标签
|
||||
export function deleteDatasetTagUsingDelete(data: any) {
|
||||
return del(`/api/data-management/tags`, data);
|
||||
}
|
||||
|
||||
// 数据集质量检查
|
||||
export function checkDatasetQualityUsingPost(id: string | number, data?: any) {
|
||||
return post(`/api/data-management/datasets/${id}/quality-check`, data);
|
||||
}
|
||||
|
||||
// 获取数据集质量报告
|
||||
export function getDatasetQualityReportUsingGet(id: string | number) {
|
||||
return get(`/api/data-management/datasets/${id}/quality-report`);
|
||||
}
|
||||
|
||||
// 数据集分析
|
||||
export function analyzeDatasetUsingPost(id: string | number, data?: any) {
|
||||
return post(`/api/data-management/datasets/${id}/analyze`, data);
|
||||
}
|
||||
|
||||
// 获取数据集分析结果
|
||||
export function getDatasetAnalysisUsingGet(id: string | number) {
|
||||
return get(`/api/data-management/datasets/${id}/analysis`);
|
||||
}
|
||||
|
||||
// 导出数据集
|
||||
export function exportDatasetUsingPost(id: string | number, data: any) {
|
||||
return post(`/api/data-management/datasets/${id}/export`, data);
|
||||
}
|
||||
|
||||
// 复制数据集
|
||||
export function copyDatasetUsingPost(id: string | number, data: any) {
|
||||
return post(`/api/data-management/datasets/${id}/copy`, data);
|
||||
}
|
||||
|
||||
// 获取数据集版本列表
|
||||
export function queryDatasetVersionsUsingGet(
|
||||
id: string | number,
|
||||
params?: any
|
||||
) {
|
||||
return get(`/api/data-management/datasets/${id}/versions`, params);
|
||||
}
|
||||
|
||||
// 创建数据集版本
|
||||
export function createDatasetVersionUsingPost(id: string | number, data: any) {
|
||||
return post(`/api/data-management/datasets/${id}/versions`, data);
|
||||
}
|
||||
|
||||
// 切换数据集版本
|
||||
export function switchDatasetVersionUsingPut(
|
||||
id: string | number,
|
||||
versionId: string | number
|
||||
) {
|
||||
return put(
|
||||
`/api/data-management/datasets/${id}/versions/${versionId}/switch`
|
||||
);
|
||||
}
|
||||
|
||||
// 删除数据集版本
|
||||
export function deleteDatasetVersionUsingDelete(
|
||||
id: string | number,
|
||||
versionId: string | number
|
||||
) {
|
||||
return del(`/api/data-management/datasets/${id}/versions/${versionId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件上传相关接口
|
||||
*/
|
||||
|
||||
export function preUploadUsingPost(id: string | number, data: any) {
|
||||
return post(
|
||||
`/api/data-management/datasets/${id}/files/upload/pre-upload`,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
export function cancelUploadUsingPut(id) {
|
||||
return put(
|
||||
`/api/data-management/datasets/upload/cancel-upload/${id}`,
|
||||
{},
|
||||
{ showLoading: false }
|
||||
);
|
||||
}
|
||||
|
||||
export function uploadFileChunkUsingPost(id: string | number, params, config) {
|
||||
return post(
|
||||
`/api/data-management/datasets/${id}/files/upload/chunk`,
|
||||
params,
|
||||
{
|
||||
showLoading: false,
|
||||
...config,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,67 +1,69 @@
|
||||
import {
|
||||
cancelUploadUsingPut,
|
||||
preUploadUsingPost,
|
||||
uploadFileChunkUsingPost,
|
||||
} from "@/pages/DataManagement/dataset.api";
|
||||
import { Button, Empty, Progress } from "antd";
|
||||
import { DeleteOutlined } from "@ant-design/icons";
|
||||
import { useEffect } from "react";
|
||||
import { useFileSliceUpload } from "@/hooks/useSliceUpload";
|
||||
|
||||
export default function TaskUpload() {
|
||||
const { createTask, taskList, removeTask, handleUpload } = useFileSliceUpload(
|
||||
{
|
||||
preUpload: preUploadUsingPost,
|
||||
uploadChunk: uploadFileChunkUsingPost,
|
||||
cancelUpload: cancelUploadUsingPut,
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const uploadHandler = (e: any) => {
|
||||
const { files } = e.detail;
|
||||
const task = createTask(e.detail);
|
||||
handleUpload({ task, files });
|
||||
};
|
||||
window.addEventListener("upload:dataset", uploadHandler);
|
||||
return () => {
|
||||
window.removeEventListener("upload:dataset", uploadHandler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-90 max-w-90 max-h-96 overflow-y-auto p-2"
|
||||
id="header-task-popover"
|
||||
>
|
||||
{taskList.length > 0 &&
|
||||
taskList.map((task) => (
|
||||
<div key={task.key} className="border-b border-gray-200 pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>{task.title}</div>
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
disabled={!task?.cancelFn}
|
||||
onClick={() =>
|
||||
removeTask({
|
||||
...task,
|
||||
isCancel: true,
|
||||
})
|
||||
}
|
||||
icon={<DeleteOutlined />}
|
||||
></Button>
|
||||
</div>
|
||||
|
||||
<Progress size="small" percent={task.percent} />
|
||||
</div>
|
||||
))}
|
||||
{taskList.length === 0 && (
|
||||
<Empty
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
description="暂无上传任务"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import {
|
||||
cancelUploadUsingPut,
|
||||
preUploadUsingPost,
|
||||
uploadFileChunkUsingPost,
|
||||
} from "@/pages/DataManagement/dataset.api";
|
||||
import { Button, Empty, Progress } from "antd";
|
||||
import { DeleteOutlined } from "@ant-design/icons";
|
||||
import { useEffect } from "react";
|
||||
import { useFileSliceUpload } from "@/hooks/useSliceUpload";
|
||||
|
||||
export default function TaskUpload() {
|
||||
const { createTask, taskList, removeTask, handleUpload } = useFileSliceUpload(
|
||||
{
|
||||
preUpload: preUploadUsingPost,
|
||||
uploadChunk: uploadFileChunkUsingPost,
|
||||
cancelUpload: cancelUploadUsingPut,
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const uploadHandler = (e: any) => {
|
||||
console.log('[TaskUpload] Received upload event detail:', e.detail);
|
||||
const { files } = e.detail;
|
||||
const task = createTask(e.detail);
|
||||
console.log('[TaskUpload] Created task with prefix:', task.prefix);
|
||||
handleUpload({ task, files });
|
||||
};
|
||||
window.addEventListener("upload:dataset", uploadHandler);
|
||||
return () => {
|
||||
window.removeEventListener("upload:dataset", uploadHandler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-90 max-w-90 max-h-96 overflow-y-auto p-2"
|
||||
id="header-task-popover"
|
||||
>
|
||||
{taskList.length > 0 &&
|
||||
taskList.map((task) => (
|
||||
<div key={task.key} className="border-b border-gray-200 pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>{task.title}</div>
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
disabled={!task?.cancelFn}
|
||||
onClick={() =>
|
||||
removeTask({
|
||||
...task,
|
||||
isCancel: true,
|
||||
})
|
||||
}
|
||||
icon={<DeleteOutlined />}
|
||||
></Button>
|
||||
</div>
|
||||
|
||||
<Progress size="small" percent={task.percent} />
|
||||
</div>
|
||||
))}
|
||||
{taskList.length === 0 && (
|
||||
<Empty
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
description="暂无上传任务"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user