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:
@@ -199,15 +199,11 @@ function CardView<T extends BaseCardDataType>(props: CardViewProps<T>) {
|
||||
? ""
|
||||
: "bg-gradient-to-br from-sky-300 to-blue-500 text-white"
|
||||
}`}
|
||||
style={{
|
||||
...(item?.iconColor
|
||||
style={
|
||||
item?.iconColor
|
||||
? { backgroundColor: item.iconColor }
|
||||
: {}),
|
||||
backgroundImage:
|
||||
"linear-gradient(180deg, rgba(255,255,255,0.35), rgba(255,255,255,0.05))",
|
||||
boxShadow:
|
||||
"inset 0 0 0 1px rgba(255,255,255,0.25)",
|
||||
}}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
<div className="w-[2.1rem] h-[2.1rem] text-gray-50">{item?.icon}</div>
|
||||
</div>
|
||||
|
||||
@@ -1,331 +1,406 @@
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
import { Button, Input, Table } from "antd";
|
||||
import { RightOutlined } from "@ant-design/icons";
|
||||
import { mapDataset } from "@/pages/DataManagement/dataset.const";
|
||||
import {
|
||||
Dataset,
|
||||
DatasetFile,
|
||||
DatasetType,
|
||||
} from "@/pages/DataManagement/dataset.model";
|
||||
import {
|
||||
queryDatasetFilesUsingGet,
|
||||
queryDatasetsUsingGet,
|
||||
} from "@/pages/DataManagement/dataset.api";
|
||||
import { formatBytes } from "@/utils/unit";
|
||||
import { useDebouncedEffect } from "@/hooks/useDebouncedEffect";
|
||||
|
||||
interface DatasetFileTransferProps
|
||||
extends React.HTMLAttributes<HTMLDivElement> {
|
||||
open: boolean;
|
||||
selectedFilesMap: { [key: string]: DatasetFile };
|
||||
onSelectedFilesChange: (filesMap: { [key: string]: DatasetFile }) => void;
|
||||
onDatasetSelect?: (dataset: Dataset | null) => void;
|
||||
}
|
||||
|
||||
const fileCols = [
|
||||
{
|
||||
title: "所属数据集",
|
||||
dataIndex: "datasetName",
|
||||
key: "datasetName",
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "文件名",
|
||||
dataIndex: "fileName",
|
||||
key: "fileName",
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "大小",
|
||||
dataIndex: "fileSize",
|
||||
key: "fileSize",
|
||||
ellipsis: true,
|
||||
render: formatBytes,
|
||||
},
|
||||
];
|
||||
|
||||
// Customize Table Transfer
|
||||
const DatasetFileTransfer: React.FC<DatasetFileTransferProps> = ({
|
||||
open,
|
||||
selectedFilesMap,
|
||||
onSelectedFilesChange,
|
||||
onDatasetSelect,
|
||||
...props
|
||||
}) => {
|
||||
const [datasets, setDatasets] = React.useState<Dataset[]>([]);
|
||||
const [datasetSearch, setDatasetSearch] = React.useState<string>("");
|
||||
const [datasetPagination, setDatasetPagination] = React.useState<{
|
||||
current: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
}>({ current: 1, pageSize: 10, total: 0 });
|
||||
|
||||
const [files, setFiles] = React.useState<DatasetFile[]>([]);
|
||||
const [filesSearch, setFilesSearch] = React.useState<string>("");
|
||||
const [filesPagination, setFilesPagination] = React.useState<{
|
||||
current: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
}>({ current: 1, pageSize: 10, total: 0 });
|
||||
|
||||
const [showFiles, setShowFiles] = React.useState<boolean>(false);
|
||||
const [selectedDataset, setSelectedDataset] = React.useState<Dataset | null>(
|
||||
null
|
||||
);
|
||||
const [datasetSelections, setDatasetSelections] = React.useState<Dataset[]>(
|
||||
[]
|
||||
);
|
||||
|
||||
const fetchDatasets = async () => {
|
||||
const { data } = await queryDatasetsUsingGet({
|
||||
// Ant Design Table pagination.current is 1-based; ensure backend also receives 1-based value
|
||||
page: datasetPagination.current,
|
||||
size: datasetPagination.pageSize,
|
||||
keyword: datasetSearch,
|
||||
type: DatasetType.TEXT,
|
||||
});
|
||||
setDatasets(data.content.map(mapDataset) || []);
|
||||
setDatasetPagination((prev) => ({
|
||||
...prev,
|
||||
total: data.totalElements,
|
||||
}));
|
||||
};
|
||||
|
||||
useDebouncedEffect(
|
||||
() => {
|
||||
fetchDatasets();
|
||||
},
|
||||
[datasetSearch, datasetPagination.pageSize, datasetPagination.current],
|
||||
300
|
||||
);
|
||||
|
||||
const fetchFiles = useCallback(
|
||||
async (
|
||||
options?: Partial<{ page: number; pageSize: number; keyword: string }>
|
||||
) => {
|
||||
if (!selectedDataset) return;
|
||||
const page = options?.page ?? filesPagination.current;
|
||||
const pageSize = options?.pageSize ?? filesPagination.pageSize;
|
||||
const keyword = options?.keyword ?? filesSearch;
|
||||
|
||||
const { data } = await queryDatasetFilesUsingGet(selectedDataset.id, {
|
||||
page,
|
||||
size: pageSize,
|
||||
keyword,
|
||||
});
|
||||
setFiles(
|
||||
(data.content || []).map((item: DatasetFile) => ({
|
||||
...item,
|
||||
key: item.id,
|
||||
datasetName: selectedDataset.name,
|
||||
}))
|
||||
);
|
||||
setFilesPagination((prev) => ({
|
||||
...prev,
|
||||
current: page,
|
||||
pageSize,
|
||||
total: data.totalElements,
|
||||
}));
|
||||
},
|
||||
[selectedDataset, filesPagination.current, filesPagination.pageSize, filesSearch]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// 当数据集变化时,重置文件分页并拉取第一页文件,避免额外的循环请求
|
||||
if (selectedDataset) {
|
||||
setFilesPagination({ current: 1, pageSize: 10, total: 0 });
|
||||
fetchFiles({ page: 1, pageSize: 10 }).catch(() => {});
|
||||
} else {
|
||||
setFiles([]);
|
||||
setFilesPagination({ current: 1, pageSize: 10, total: 0 });
|
||||
}
|
||||
// 只在 selectedDataset 变化时触发
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedDataset]);
|
||||
|
||||
useEffect(() => {
|
||||
onDatasetSelect?.(selectedDataset);
|
||||
}, [selectedDataset, onDatasetSelect]);
|
||||
|
||||
const toggleSelectFile = (record: DatasetFile) => {
|
||||
if (!selectedFilesMap[record.id]) {
|
||||
onSelectedFilesChange({
|
||||
...selectedFilesMap,
|
||||
[record.id]: record,
|
||||
});
|
||||
} else {
|
||||
const newSelectedFiles = { ...selectedFilesMap };
|
||||
delete newSelectedFiles[record.id];
|
||||
onSelectedFilesChange(newSelectedFiles);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
// 重置状态
|
||||
setDatasets([]);
|
||||
setDatasetSearch("");
|
||||
setDatasetPagination({ current: 1, pageSize: 10, total: 0 });
|
||||
setFiles([]);
|
||||
setFilesSearch("");
|
||||
setFilesPagination({ current: 1, pageSize: 10, total: 0 });
|
||||
setShowFiles(false);
|
||||
setSelectedDataset(null);
|
||||
setDatasetSelections([]);
|
||||
onDatasetSelect?.(null);
|
||||
}
|
||||
}, [open, onDatasetSelect]);
|
||||
|
||||
const datasetCols = [
|
||||
{
|
||||
title: "数据集名称",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "文件数",
|
||||
dataIndex: "fileCount",
|
||||
key: "fileCount",
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "大小",
|
||||
dataIndex: "totalSize",
|
||||
key: "totalSize",
|
||||
ellipsis: true,
|
||||
render: formatBytes,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div {...props}>
|
||||
<div className="grid grid-cols-25 gap-4 w-full">
|
||||
<div className="border-card flex flex-col col-span-12">
|
||||
<div className="border-bottom p-2 font-bold">选择数据集</div>
|
||||
<div className="p-2">
|
||||
<Input
|
||||
placeholder="搜索数据集名称..."
|
||||
value={datasetSearch}
|
||||
allowClear
|
||||
onChange={(e) => setDatasetSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Table
|
||||
scroll={{ y: 400 }}
|
||||
rowKey="id"
|
||||
size="small"
|
||||
rowClassName={(record) =>
|
||||
selectedDataset?.id === record.id ? "bg-blue-100" : ""
|
||||
}
|
||||
onRow={(record: Dataset) => ({
|
||||
onClick: () => {
|
||||
setSelectedDataset(record);
|
||||
if (!datasetSelections.find((d) => d.id === record.id)) {
|
||||
setDatasetSelections([...datasetSelections, record]);
|
||||
} else {
|
||||
setDatasetSelections(
|
||||
datasetSelections.filter((d) => d.id !== record.id)
|
||||
);
|
||||
}
|
||||
},
|
||||
})}
|
||||
dataSource={datasets}
|
||||
columns={datasetCols}
|
||||
pagination={{
|
||||
...datasetPagination,
|
||||
onChange: (page, pageSize) =>
|
||||
setDatasetPagination({
|
||||
current: page,
|
||||
pageSize: pageSize || datasetPagination.pageSize,
|
||||
total: datasetPagination.total,
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<RightOutlined />
|
||||
<div className="border-card flex flex-col col-span-12">
|
||||
<div className="border-bottom p-2 font-bold">选择文件</div>
|
||||
<div className="p-2">
|
||||
<Input
|
||||
placeholder="搜索文件名称..."
|
||||
value={filesSearch}
|
||||
onChange={(e) => setFilesSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Table
|
||||
scroll={{ y: 400 }}
|
||||
rowKey="id"
|
||||
size="small"
|
||||
dataSource={files}
|
||||
columns={fileCols.slice(1, fileCols.length)}
|
||||
pagination={{
|
||||
...filesPagination,
|
||||
onChange: (page, pageSize) => {
|
||||
const nextPageSize = pageSize || filesPagination.pageSize;
|
||||
setFilesPagination((prev) => ({
|
||||
...prev,
|
||||
current: page,
|
||||
pageSize: nextPageSize,
|
||||
}));
|
||||
fetchFiles({ page, pageSize: nextPageSize }).catch(() => {});
|
||||
},
|
||||
}}
|
||||
onRow={(record: DatasetFile) => ({
|
||||
onClick: () => toggleSelectFile(record),
|
||||
})}
|
||||
rowSelection={{
|
||||
type: "checkbox",
|
||||
selectedRowKeys: Object.keys(selectedFilesMap),
|
||||
|
||||
// 单选
|
||||
onSelect: (record: DatasetFile) => {
|
||||
toggleSelectFile(record);
|
||||
},
|
||||
|
||||
// 全选
|
||||
onSelectAll: (selected, selectedRows: DatasetFile[]) => {
|
||||
if (selected) {
|
||||
// ✔ 全选 -> 将 files 列表全部加入 selectedFilesMap
|
||||
const newMap: Record<string, DatasetFile> = { ...selectedFilesMap };
|
||||
selectedRows.forEach((f) => {
|
||||
newMap[f.id] = f;
|
||||
});
|
||||
onSelectedFilesChange(newMap);
|
||||
} else {
|
||||
// ✘ 取消全选 -> 清空 map
|
||||
const newMap = { ...selectedFilesMap };
|
||||
Object.keys(newMap).forEach((id) => {
|
||||
if (files.some((f) => String(f.id) === id)) {
|
||||
// 仅移除当前页对应文件
|
||||
delete newMap[id];
|
||||
}
|
||||
});
|
||||
onSelectedFilesChange(newMap);
|
||||
}
|
||||
},
|
||||
|
||||
getCheckboxProps: (record: DatasetFile) => ({
|
||||
name: record.fileName,
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button className="mt-4" onClick={() => setShowFiles(!showFiles)}>
|
||||
{showFiles ? "取消预览" : "预览"}
|
||||
</Button>
|
||||
<div hidden={!showFiles}>
|
||||
<Table
|
||||
scroll={{ y: 400 }}
|
||||
rowKey="id"
|
||||
size="small"
|
||||
dataSource={Object.values(selectedFilesMap)}
|
||||
columns={fileCols}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DatasetFileTransfer;
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
import { Button, Input, Table, message } from "antd";
|
||||
import { RightOutlined } from "@ant-design/icons";
|
||||
import { mapDataset } from "@/pages/DataManagement/dataset.const";
|
||||
import {
|
||||
Dataset,
|
||||
DatasetFile,
|
||||
DatasetType,
|
||||
} from "@/pages/DataManagement/dataset.model";
|
||||
import {
|
||||
queryDatasetFilesUsingGet,
|
||||
queryDatasetsUsingGet,
|
||||
} from "@/pages/DataManagement/dataset.api";
|
||||
import { formatBytes } from "@/utils/unit";
|
||||
import { useDebouncedEffect } from "@/hooks/useDebouncedEffect";
|
||||
|
||||
interface DatasetFileTransferProps
|
||||
extends React.HTMLAttributes<HTMLDivElement> {
|
||||
open: boolean;
|
||||
selectedFilesMap: { [key: string]: DatasetFile };
|
||||
onSelectedFilesChange: (filesMap: { [key: string]: DatasetFile }) => void;
|
||||
onDatasetSelect?: (dataset: Dataset | null) => void;
|
||||
datasetTypeFilter?: DatasetType;
|
||||
}
|
||||
|
||||
const fileCols = [
|
||||
{
|
||||
title: "所属数据集",
|
||||
dataIndex: "datasetName",
|
||||
key: "datasetName",
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "文件名",
|
||||
dataIndex: "fileName",
|
||||
key: "fileName",
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "大小",
|
||||
dataIndex: "fileSize",
|
||||
key: "fileSize",
|
||||
ellipsis: true,
|
||||
render: formatBytes,
|
||||
},
|
||||
];
|
||||
|
||||
// Customize Table Transfer
|
||||
const DatasetFileTransfer: React.FC<DatasetFileTransferProps> = ({
|
||||
open,
|
||||
selectedFilesMap,
|
||||
onSelectedFilesChange,
|
||||
onDatasetSelect,
|
||||
datasetTypeFilter = DatasetType.TEXT,
|
||||
...props
|
||||
}) => {
|
||||
const [datasets, setDatasets] = React.useState<Dataset[]>([]);
|
||||
const [datasetSearch, setDatasetSearch] = React.useState<string>("");
|
||||
const [datasetPagination, setDatasetPagination] = React.useState<{
|
||||
current: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
}>({ current: 1, pageSize: 10, total: 0 });
|
||||
|
||||
const [files, setFiles] = React.useState<DatasetFile[]>([]);
|
||||
const [filesSearch, setFilesSearch] = React.useState<string>("");
|
||||
const [filesPagination, setFilesPagination] = React.useState<{
|
||||
current: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
}>({ current: 1, pageSize: 10, total: 0 });
|
||||
|
||||
const [showFiles, setShowFiles] = React.useState<boolean>(false);
|
||||
const [selectedDataset, setSelectedDataset] = React.useState<Dataset | null>(
|
||||
null
|
||||
);
|
||||
const [datasetSelections, setDatasetSelections] = React.useState<Dataset[]>(
|
||||
[]
|
||||
);
|
||||
const [selectingAll, setSelectingAll] = React.useState<boolean>(false);
|
||||
|
||||
const fetchDatasets = async () => {
|
||||
const { data } = await queryDatasetsUsingGet({
|
||||
// Ant Design Table pagination.current is 1-based; ensure backend also receives 1-based value
|
||||
page: datasetPagination.current,
|
||||
size: datasetPagination.pageSize,
|
||||
keyword: datasetSearch,
|
||||
type: datasetTypeFilter,
|
||||
});
|
||||
setDatasets(data.content.map(mapDataset) || []);
|
||||
setDatasetPagination((prev) => ({
|
||||
...prev,
|
||||
total: data.totalElements,
|
||||
}));
|
||||
};
|
||||
|
||||
useDebouncedEffect(
|
||||
() => {
|
||||
fetchDatasets();
|
||||
},
|
||||
[datasetSearch, datasetPagination.pageSize, datasetPagination.current],
|
||||
300
|
||||
);
|
||||
|
||||
const fetchFiles = useCallback(
|
||||
async (
|
||||
options?: Partial<{ page: number; pageSize: number; keyword: string }>
|
||||
) => {
|
||||
if (!selectedDataset) return;
|
||||
const page = options?.page ?? filesPagination.current;
|
||||
const pageSize = options?.pageSize ?? filesPagination.pageSize;
|
||||
const keyword = options?.keyword ?? filesSearch;
|
||||
|
||||
const { data } = await queryDatasetFilesUsingGet(selectedDataset.id, {
|
||||
page,
|
||||
size: pageSize,
|
||||
keyword,
|
||||
});
|
||||
setFiles(
|
||||
(data.content || []).map((item: DatasetFile) => ({
|
||||
...item,
|
||||
id: item.id,
|
||||
key: String(item.id), // rowKey 使用字符串,确保与 selectedRowKeys 类型一致
|
||||
datasetName: selectedDataset.name,
|
||||
}))
|
||||
);
|
||||
setFilesPagination((prev) => ({
|
||||
...prev,
|
||||
current: page,
|
||||
pageSize,
|
||||
total: data.totalElements,
|
||||
}));
|
||||
},
|
||||
[selectedDataset, filesPagination.current, filesPagination.pageSize, filesSearch]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// 当数据集变化时,重置文件分页并拉取第一页文件,避免额外的循环请求
|
||||
if (selectedDataset) {
|
||||
setFilesPagination({ current: 1, pageSize: 10, total: 0 });
|
||||
// 后端 page 参数为 0-based,这里传 0 获取第一页
|
||||
fetchFiles({ page: 0, pageSize: 10 }).catch(() => {});
|
||||
} else {
|
||||
setFiles([]);
|
||||
setFilesPagination({ current: 1, pageSize: 10, total: 0 });
|
||||
}
|
||||
// 只在 selectedDataset 变化时触发
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedDataset]);
|
||||
|
||||
useEffect(() => {
|
||||
onDatasetSelect?.(selectedDataset);
|
||||
}, [selectedDataset, onDatasetSelect]);
|
||||
|
||||
const handleSelectAllInDataset = useCallback(async () => {
|
||||
if (!selectedDataset) {
|
||||
message.warning("请先选择一个数据集");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setSelectingAll(true);
|
||||
|
||||
const pageSize = 1000; // 分批拉取,避免后端单页限制
|
||||
let page = 0; // 后端 page 参数为 0-based,从 0 开始
|
||||
let total = 0;
|
||||
const allFiles: DatasetFile[] = [];
|
||||
|
||||
while (true) {
|
||||
const { data } = await queryDatasetFilesUsingGet(selectedDataset.id, {
|
||||
page,
|
||||
size: pageSize,
|
||||
});
|
||||
|
||||
const content: DatasetFile[] = (data.content || []).map(
|
||||
(item: DatasetFile) => ({
|
||||
...item,
|
||||
key: item.id,
|
||||
datasetName: selectedDataset.name,
|
||||
}),
|
||||
);
|
||||
|
||||
if (!content.length) {
|
||||
break;
|
||||
}
|
||||
|
||||
allFiles.push(...content);
|
||||
// 优先用后端的 totalElements,否则用当前累积数
|
||||
total = typeof data.totalElements === "number" ? data.totalElements : allFiles.length;
|
||||
|
||||
// 如果这一页数量小于 pageSize,说明已经拿完;否则继续下一页
|
||||
if (content.length < pageSize) {
|
||||
break;
|
||||
}
|
||||
|
||||
page += 1;
|
||||
}
|
||||
|
||||
const newMap: { [key: string]: DatasetFile } = { ...selectedFilesMap };
|
||||
allFiles.forEach((file) => {
|
||||
if (file && file.id != null) {
|
||||
newMap[String(file.id)] = file;
|
||||
}
|
||||
});
|
||||
|
||||
onSelectedFilesChange(newMap);
|
||||
|
||||
const count = total || allFiles.length;
|
||||
if (count > 0) {
|
||||
message.success(`已选中当前数据集的全部 ${count} 个文件`);
|
||||
} else {
|
||||
message.info("当前数据集下没有可选文件");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to select all files in dataset", error);
|
||||
message.error("全选整个数据集失败,请稍后重试");
|
||||
} finally {
|
||||
setSelectingAll(false);
|
||||
}
|
||||
}, [selectedDataset, selectedFilesMap, onSelectedFilesChange]);
|
||||
|
||||
const toggleSelectFile = (record: DatasetFile) => {
|
||||
if (!selectedFilesMap[record.id]) {
|
||||
onSelectedFilesChange({
|
||||
...selectedFilesMap,
|
||||
[record.id]: record,
|
||||
});
|
||||
} else {
|
||||
const newSelectedFiles = { ...selectedFilesMap };
|
||||
delete newSelectedFiles[record.id];
|
||||
onSelectedFilesChange(newSelectedFiles);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
// 重置状态
|
||||
setDatasets([]);
|
||||
setDatasetSearch("");
|
||||
setDatasetPagination({ current: 1, pageSize: 10, total: 0 });
|
||||
setFiles([]);
|
||||
setFilesSearch("");
|
||||
setFilesPagination({ current: 1, pageSize: 10, total: 0 });
|
||||
setShowFiles(false);
|
||||
setSelectedDataset(null);
|
||||
setDatasetSelections([]);
|
||||
onDatasetSelect?.(null);
|
||||
}
|
||||
}, [open, onDatasetSelect]);
|
||||
|
||||
const datasetCols = [
|
||||
{
|
||||
title: "数据集名称",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "文件数",
|
||||
dataIndex: "fileCount",
|
||||
key: "fileCount",
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "大小",
|
||||
dataIndex: "totalSize",
|
||||
key: "totalSize",
|
||||
ellipsis: true,
|
||||
render: formatBytes,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div {...props}>
|
||||
<div className="grid grid-cols-25 gap-4 w-full">
|
||||
<div className="border-card flex flex-col col-span-12">
|
||||
<div className="border-bottom p-2 font-bold">选择数据集</div>
|
||||
<div className="p-2">
|
||||
<Input
|
||||
placeholder="搜索数据集名称..."
|
||||
value={datasetSearch}
|
||||
allowClear
|
||||
onChange={(e) => setDatasetSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Table
|
||||
scroll={{ y: 400 }}
|
||||
rowKey="id"
|
||||
size="small"
|
||||
rowClassName={(record) =>
|
||||
selectedDataset?.id === record.id ? "bg-blue-100" : ""
|
||||
}
|
||||
onRow={(record: Dataset) => ({
|
||||
onClick: () => {
|
||||
setSelectedDataset(record);
|
||||
if (!datasetSelections.find((d) => d.id === record.id)) {
|
||||
setDatasetSelections([...datasetSelections, record]);
|
||||
} else {
|
||||
setDatasetSelections(
|
||||
datasetSelections.filter((d) => d.id !== record.id)
|
||||
);
|
||||
}
|
||||
},
|
||||
})}
|
||||
dataSource={datasets}
|
||||
columns={datasetCols}
|
||||
pagination={{
|
||||
...datasetPagination,
|
||||
onChange: (page, pageSize) =>
|
||||
setDatasetPagination({
|
||||
current: page,
|
||||
pageSize: pageSize || datasetPagination.pageSize,
|
||||
total: datasetPagination.total,
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<RightOutlined />
|
||||
<div className="border-card flex flex-col col-span-12">
|
||||
<div className="border-bottom p-2 font-bold flex justify-between items-center">
|
||||
<span>选择文件</span>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={handleSelectAllInDataset}
|
||||
disabled={!selectedDataset}
|
||||
loading={selectingAll}
|
||||
>
|
||||
全选当前数据集
|
||||
</Button>
|
||||
</div>
|
||||
<div className="p-2">
|
||||
<Input
|
||||
placeholder="搜索文件名称..."
|
||||
value={filesSearch}
|
||||
onChange={(e) => setFilesSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Table
|
||||
scroll={{ y: 400 }}
|
||||
rowKey={(record) => String(record.id)}
|
||||
size="small"
|
||||
dataSource={files}
|
||||
columns={fileCols.slice(1, fileCols.length)}
|
||||
pagination={{
|
||||
...filesPagination,
|
||||
onChange: (page, pageSize) => {
|
||||
const nextPageSize = pageSize || filesPagination.pageSize;
|
||||
setFilesPagination((prev) => ({
|
||||
...prev,
|
||||
current: page,
|
||||
pageSize: nextPageSize,
|
||||
}));
|
||||
// 前端分页是 1-based,后端是 0-based,所以这里传 page - 1
|
||||
fetchFiles({ page: page - 1, pageSize: nextPageSize }).catch(() => {});
|
||||
},
|
||||
}}
|
||||
onRow={(record: DatasetFile) => ({
|
||||
onClick: () => toggleSelectFile(record),
|
||||
})}
|
||||
rowSelection={{
|
||||
type: "checkbox",
|
||||
selectedRowKeys: Object.keys(selectedFilesMap),
|
||||
preserveSelectedRowKeys: true,
|
||||
|
||||
// 单选
|
||||
onSelect: (record: DatasetFile) => {
|
||||
toggleSelectFile(record);
|
||||
},
|
||||
|
||||
// 全选 - 改为全选整个数据集而不是当前页
|
||||
onSelectAll: (selected, selectedRows: DatasetFile[]) => {
|
||||
if (selected) {
|
||||
// 点击表头“全选”时,改为一键全选当前数据集的全部文件
|
||||
// 而不是只选中当前页
|
||||
handleSelectAllInDataset();
|
||||
} else {
|
||||
// 取消表头“全选”时,清空当前已选文件
|
||||
onSelectedFilesChange({});
|
||||
}
|
||||
},
|
||||
|
||||
getCheckboxProps: (record: DatasetFile) => ({
|
||||
name: record.fileName,
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button className="mt-4" onClick={() => setShowFiles(!showFiles)}>
|
||||
{showFiles ? "取消预览" : "预览"}
|
||||
</Button>
|
||||
<div hidden={!showFiles}>
|
||||
<Table
|
||||
scroll={{ y: 400 }}
|
||||
rowKey="id"
|
||||
size="small"
|
||||
dataSource={Object.values(selectedFilesMap)}
|
||||
columns={fileCols}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DatasetFileTransfer;
|
||||
|
||||
Reference in New Issue
Block a user