fix: 上传文件时任务中心立即显示

问题描述:
在数据管理的数据集详情页上传文件时,点击确认后,弹窗消失,但是需要等待文件处理(特别是启用按行分割时)后任务中心才弹出来,用户体验不好。

修改内容:
1. useSliceUpload.tsx: 在 createTask 函数中添加立即显示任务中心的逻辑,确保任务创建后立即显示
2. ImportConfiguration.tsx: 在 handleImportData 函数中,在执行耗时的文件处理操作(如文件分割)之前,立即触发 show:task-popover 事件显示任务中心

效果:
- 修改前:点击确认 → 弹窗消失 → (等待文件处理)→ 任务中心弹出
- 修改后:点击确认 → 弹窗消失 + 任务中心立即弹出 → 文件开始处理
This commit is contained in:
2026-02-03 09:14:40 +00:00
parent 05e6842fc8
commit 893e0a1580
2 changed files with 437 additions and 429 deletions

View File

@@ -1,198 +1,202 @@
import { TaskItem } from "@/pages/DataManagement/dataset.model"; import { TaskItem } from "@/pages/DataManagement/dataset.model";
import { calculateSHA256, checkIsFilesExist } from "@/utils/file.util"; import { calculateSHA256, checkIsFilesExist } from "@/utils/file.util";
import { App } from "antd"; import { App } from "antd";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
export function useFileSliceUpload( export function useFileSliceUpload(
{ {
preUpload, preUpload,
uploadChunk, uploadChunk,
cancelUpload, cancelUpload,
}: { }: {
preUpload: (id: string, params: any) => Promise<{ data: number }>; preUpload: (id: string, params: any) => Promise<{ data: number }>;
uploadChunk: (id: string, formData: FormData, config: any) => Promise<any>; uploadChunk: (id: string, formData: FormData, config: any) => Promise<any>;
cancelUpload: ((reqId: number) => Promise<any>) | null; cancelUpload: ((reqId: number) => Promise<any>) | null;
}, },
showTaskCenter = true // 上传时是否显示任务中心 showTaskCenter = true // 上传时是否显示任务中心
) { ) {
const { message } = App.useApp(); const { message } = App.useApp();
const [taskList, setTaskList] = useState<TaskItem[]>([]); const [taskList, setTaskList] = useState<TaskItem[]>([]);
const taskListRef = useRef<TaskItem[]>([]); // 用于固定任务顺序 const taskListRef = useRef<TaskItem[]>([]); // 用于固定任务顺序
const createTask = (detail: any = {}) => { const createTask = (detail: any = {}) => {
const { dataset } = detail; const { dataset } = detail;
const title = `上传数据集: ${dataset.name} `; const title = `上传数据集: ${dataset.name} `;
const controller = new AbortController(); const controller = new AbortController();
const task: TaskItem = { const task: TaskItem = {
key: dataset.id, key: dataset.id,
title, title,
percent: 0, percent: 0,
reqId: -1, reqId: -1,
controller, controller,
size: 0, size: 0,
updateEvent: detail.updateEvent, updateEvent: detail.updateEvent,
hasArchive: detail.hasArchive, hasArchive: detail.hasArchive,
prefix: detail.prefix, prefix: detail.prefix,
}; };
taskListRef.current = [task, ...taskListRef.current]; taskListRef.current = [task, ...taskListRef.current];
setTaskList(taskListRef.current); setTaskList(taskListRef.current);
return task;
}; // 立即显示任务中心,让用户感知上传已开始
if (showTaskCenter) {
const updateTaskList = (task: TaskItem) => { window.dispatchEvent(
taskListRef.current = taskListRef.current.map((item) => new CustomEvent("show:task-popover", { detail: { show: true } })
item.key === task.key ? task : item );
); }
setTaskList(taskListRef.current);
}; return task;
};
const removeTask = (task: TaskItem) => {
const { key } = task; const updateTaskList = (task: TaskItem) => {
taskListRef.current = taskListRef.current.filter( taskListRef.current = taskListRef.current.map((item) =>
(item) => item.key !== key item.key === task.key ? task : item
); );
setTaskList(taskListRef.current); setTaskList(taskListRef.current);
if (task.isCancel && task.cancelFn) { };
task.cancelFn();
} const removeTask = (task: TaskItem) => {
if (task.updateEvent) { const { key } = task;
// 携带前缀信息,便于刷新后仍停留在当前目录 taskListRef.current = taskListRef.current.filter(
window.dispatchEvent( (item) => item.key !== key
new CustomEvent(task.updateEvent, { );
detail: { prefix: (task as any).prefix }, setTaskList(taskListRef.current);
}) if (task.isCancel && task.cancelFn) {
); task.cancelFn();
} }
if (showTaskCenter) { if (task.updateEvent) {
window.dispatchEvent( // 携带前缀信息,便于刷新后仍停留在当前目录
new CustomEvent("show:task-popover", { detail: { show: false } }) window.dispatchEvent(
); new CustomEvent(task.updateEvent, {
} detail: { prefix: (task as any).prefix },
}; })
);
async function buildFormData({ file, reqId, i, j }) { }
const formData = new FormData(); if (showTaskCenter) {
const { slices, name, size } = file; window.dispatchEvent(
const checkSum = await calculateSHA256(slices[j]); new CustomEvent("show:task-popover", { detail: { show: false } })
formData.append("file", slices[j]); );
formData.append("reqId", reqId.toString()); }
formData.append("fileNo", (i + 1).toString()); };
formData.append("chunkNo", (j + 1).toString());
formData.append("fileName", name); async function buildFormData({ file, reqId, i, j }) {
formData.append("fileSize", size.toString()); const formData = new FormData();
formData.append("totalChunkNum", slices.length.toString()); const { slices, name, size } = file;
formData.append("checkSumHex", checkSum); const checkSum = await calculateSHA256(slices[j]);
return formData; formData.append("file", slices[j]);
} formData.append("reqId", reqId.toString());
formData.append("fileNo", (i + 1).toString());
async function uploadSlice(task: TaskItem, fileInfo) { formData.append("chunkNo", (j + 1).toString());
if (!task) { formData.append("fileName", name);
return; formData.append("fileSize", size.toString());
} formData.append("totalChunkNum", slices.length.toString());
const { reqId, key } = task; formData.append("checkSumHex", checkSum);
const { loaded, i, j, files, totalSize } = fileInfo; return formData;
const formData = await buildFormData({ }
file: files[i],
i, async function uploadSlice(task: TaskItem, fileInfo) {
j, if (!task) {
reqId, return;
}); }
const { reqId, key } = task;
let newTask = { ...task }; const { loaded, i, j, files, totalSize } = fileInfo;
await uploadChunk(key, formData, { const formData = await buildFormData({
onUploadProgress: (e) => { file: files[i],
const loadedSize = loaded + e.loaded; i,
const curPercent = Number((loadedSize / totalSize) * 100).toFixed(2); j,
reqId,
newTask = { });
...newTask,
...taskListRef.current.find((item) => item.key === key), let newTask = { ...task };
size: loadedSize, await uploadChunk(key, formData, {
percent: curPercent >= 100 ? 99.99 : curPercent, onUploadProgress: (e) => {
}; const loadedSize = loaded + e.loaded;
updateTaskList(newTask); const curPercent = Number((loadedSize / totalSize) * 100).toFixed(2);
},
}); newTask = {
} ...newTask,
...taskListRef.current.find((item) => item.key === key),
async function uploadFile({ task, files, totalSize }) { size: loadedSize,
console.log('[useSliceUpload] Calling preUpload with prefix:', task.prefix); percent: curPercent >= 100 ? 99.99 : curPercent,
const { data: reqId } = await preUpload(task.key, { };
totalFileNum: files.length, updateTaskList(newTask);
totalSize, },
datasetId: task.key, });
hasArchive: task.hasArchive, }
prefix: task.prefix,
}); async function uploadFile({ task, files, totalSize }) {
console.log('[useSliceUpload] PreUpload response reqId:', reqId); console.log('[useSliceUpload] Calling preUpload with prefix:', task.prefix);
const { data: reqId } = await preUpload(task.key, {
const newTask: TaskItem = { totalFileNum: files.length,
...task, totalSize,
reqId, datasetId: task.key,
isCancel: false, hasArchive: task.hasArchive,
cancelFn: () => { prefix: task.prefix,
task.controller.abort(); });
cancelUpload?.(reqId); console.log('[useSliceUpload] PreUpload response reqId:', reqId);
if (task.updateEvent) window.dispatchEvent(new Event(task.updateEvent));
}, const newTask: TaskItem = {
}; ...task,
updateTaskList(newTask); reqId,
if (showTaskCenter) { isCancel: false,
window.dispatchEvent( cancelFn: () => {
new CustomEvent("show:task-popover", { detail: { show: true } }) task.controller.abort();
); cancelUpload?.(reqId);
} if (task.updateEvent) window.dispatchEvent(new Event(task.updateEvent));
// // 更新数据状态 },
if (task.updateEvent) window.dispatchEvent(new Event(task.updateEvent)); };
updateTaskList(newTask);
let loaded = 0; // 注意:show:task-popover 事件已在 createTask 中触发,此处不再重复触发
for (let i = 0; i < files.length; i++) { // // 更新数据状态
const { slices } = files[i]; if (task.updateEvent) window.dispatchEvent(new Event(task.updateEvent));
for (let j = 0; j < slices.length; j++) {
await uploadSlice(newTask, { let loaded = 0;
loaded, for (let i = 0; i < files.length; i++) {
i, const { slices } = files[i];
j, for (let j = 0; j < slices.length; j++) {
files, await uploadSlice(newTask, {
totalSize, loaded,
}); i,
loaded += slices[j].size; j,
} files,
} totalSize,
removeTask(newTask); });
} loaded += slices[j].size;
}
const handleUpload = async ({ task, files }) => { }
const isErrorFile = await checkIsFilesExist(files); removeTask(newTask);
if (isErrorFile) { }
message.error("文件被修改或删除,请重新选择文件上传");
removeTask({ const handleUpload = async ({ task, files }) => {
...task, const isErrorFile = await checkIsFilesExist(files);
isCancel: false, if (isErrorFile) {
...taskListRef.current.find((item) => item.key === task.key), message.error("文件被修改或删除,请重新选择文件上传");
}); removeTask({
return; ...task,
} isCancel: false,
...taskListRef.current.find((item) => item.key === task.key),
try { });
const totalSize = files.reduce((acc, file) => acc + file.size, 0); return;
await uploadFile({ task, files, totalSize }); }
} catch (err) {
console.error(err); try {
message.error("文件上传失败,请稍后重试"); const totalSize = files.reduce((acc, file) => acc + file.size, 0);
removeTask({ await uploadFile({ task, files, totalSize });
...task, } catch (err) {
isCancel: true, console.error(err);
...taskListRef.current.find((item) => item.key === task.key), message.error("文件上传失败,请稍后重试");
}); removeTask({
} ...task,
}; isCancel: true,
...taskListRef.current.find((item) => item.key === task.key),
return { });
taskList, }
createTask, };
removeTask,
handleUpload, return {
}; taskList,
} createTask,
removeTask,
handleUpload,
};
}

View File

@@ -1,13 +1,13 @@
import { Select, Input, Form, Radio, Modal, Button, UploadFile, Switch, Tooltip } from "antd"; import { Select, Input, Form, Radio, Modal, Button, UploadFile, Switch, Tooltip } from "antd";
import { InboxOutlined, QuestionCircleOutlined } from "@ant-design/icons"; import { InboxOutlined, QuestionCircleOutlined } from "@ant-design/icons";
import { dataSourceOptions } from "../../dataset.const"; import { dataSourceOptions } from "../../dataset.const";
import { Dataset, DatasetType, DataSource } from "../../dataset.model"; import { Dataset, DatasetType, DataSource } from "../../dataset.model";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { queryTasksUsingGet } from "@/pages/DataCollection/collection.apis"; import { queryTasksUsingGet } from "@/pages/DataCollection/collection.apis";
import { updateDatasetByIdUsingPut } from "../../dataset.api"; import { updateDatasetByIdUsingPut } from "../../dataset.api";
import { sliceFile } from "@/utils/file.util"; import { sliceFile } from "@/utils/file.util";
import Dragger from "antd/es/upload/Dragger"; import Dragger from "antd/es/upload/Dragger";
const TEXT_FILE_MIME_PREFIX = "text/"; const TEXT_FILE_MIME_PREFIX = "text/";
const TEXT_FILE_MIME_TYPES = new Set([ const TEXT_FILE_MIME_TYPES = new Set([
"application/json", "application/json",
@@ -131,18 +131,18 @@ type ImportConfig = {
}; };
export default function ImportConfiguration({ export default function ImportConfiguration({
data, data,
open, open,
onClose, onClose,
updateEvent = "update:dataset", updateEvent = "update:dataset",
prefix, prefix,
}: { }: {
data: Dataset | null; data: Dataset | null;
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
updateEvent?: string; updateEvent?: string;
prefix?: string; prefix?: string;
}) { }) {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [collectionOptions, setCollectionOptions] = useState<SelectOption[]>([]); const [collectionOptions, setCollectionOptions] = useState<SelectOption[]>([]);
const availableSourceOptions = dataSourceOptions.filter( const availableSourceOptions = dataSourceOptions.filter(
@@ -160,13 +160,13 @@ export default function ImportConfiguration({
return files.some((file) => !isTextUploadFile(file)); return files.some((file) => !isTextUploadFile(file));
}, [importConfig.files]); }, [importConfig.files]);
const isTextDataset = data?.datasetType === DatasetType.TEXT; const isTextDataset = data?.datasetType === DatasetType.TEXT;
// 本地上传文件相关逻辑 // 本地上传文件相关逻辑
const handleUpload = async (dataset: Dataset) => { const handleUpload = async (dataset: Dataset) => {
let filesToUpload = let filesToUpload =
(form.getFieldValue("files") as UploadFile[] | undefined) || []; (form.getFieldValue("files") as UploadFile[] | undefined) || [];
// 如果启用分行分割,处理文件 // 如果启用分行分割,处理文件
if (importConfig.splitByLine && !hasNonTextFile) { if (importConfig.splitByLine && !hasNonTextFile) {
const splitResults = await Promise.all( const splitResults = await Promise.all(
@@ -174,9 +174,9 @@ export default function ImportConfiguration({
); );
filesToUpload = splitResults.flat(); filesToUpload = splitResults.flat();
} }
// 计算分片列表 // 计算分片列表
const sliceList = filesToUpload.map((file) => { const sliceList = filesToUpload.map((file) => {
const originFile = (file.originFileObj ?? file) as Blob; const originFile = (file.originFileObj ?? file) as Blob;
const slices = sliceFile(originFile); const slices = sliceFile(originFile);
return { return {
@@ -185,22 +185,22 @@ export default function ImportConfiguration({
name: file.name, name: file.name,
size: originFile.size || 0, size: originFile.size || 0,
}; };
}); });
console.log("[ImportConfiguration] Uploading with currentPrefix:", currentPrefix); console.log("[ImportConfiguration] Uploading with currentPrefix:", currentPrefix);
window.dispatchEvent( window.dispatchEvent(
new CustomEvent("upload:dataset", { new CustomEvent("upload:dataset", {
detail: { detail: {
dataset, dataset,
files: sliceList, files: sliceList,
updateEvent, updateEvent,
hasArchive: importConfig.hasArchive, hasArchive: importConfig.hasArchive,
prefix: currentPrefix, prefix: currentPrefix,
}, },
}) })
); );
}; };
const fetchCollectionTasks = useCallback(async () => { const fetchCollectionTasks = useCallback(async () => {
if (importConfig.source !== DataSource.COLLECTION) return; if (importConfig.source !== DataSource.COLLECTION) return;
try { try {
@@ -212,7 +212,7 @@ export default function ImportConfiguration({
label: task.name, label: task.name,
value: task.id, value: task.id,
})); }));
setCollectionOptions(options); setCollectionOptions(options);
} catch (error) { } catch (error) {
console.error("Error fetching collection tasks:", error); console.error("Error fetching collection tasks:", error);
} }
@@ -229,27 +229,31 @@ export default function ImportConfiguration({
}); });
console.log('[ImportConfiguration] resetState done, currentPrefix still:', currentPrefix); console.log('[ImportConfiguration] resetState done, currentPrefix still:', currentPrefix);
}, [currentPrefix, form]); }, [currentPrefix, form]);
const handleImportData = async () => { const handleImportData = async () => {
if (!data) return; if (!data) return;
console.log('[ImportConfiguration] handleImportData called, currentPrefix:', currentPrefix); console.log('[ImportConfiguration] handleImportData called, currentPrefix:', currentPrefix);
if (importConfig.source === DataSource.UPLOAD) { if (importConfig.source === DataSource.UPLOAD) {
await handleUpload(data); // 立即显示任务中心,让用户感知上传已开始(在文件分割等耗时操作之前)
} else if (importConfig.source === DataSource.COLLECTION) { window.dispatchEvent(
await updateDatasetByIdUsingPut(data.id, { new CustomEvent("show:task-popover", { detail: { show: true } })
...importConfig, );
}); await handleUpload(data);
} } else if (importConfig.source === DataSource.COLLECTION) {
onClose(); await updateDatasetByIdUsingPut(data.id, {
}; ...importConfig,
});
}
onClose();
};
useEffect(() => { useEffect(() => {
if (open) { if (open) {
setCurrentPrefix(prefix || ""); setCurrentPrefix(prefix || "");
console.log('[ImportConfiguration] Modal opened with prefix:', prefix); console.log('[ImportConfiguration] Modal opened with prefix:', prefix);
resetState(); resetState();
fetchCollectionTasks(); fetchCollectionTasks();
} }
}, [fetchCollectionTasks, open, prefix, resetState]); }, [fetchCollectionTasks, open, prefix, resetState]);
useEffect(() => { useEffect(() => {
@@ -259,111 +263,111 @@ export default function ImportConfiguration({
form.setFieldsValue({ splitByLine: false }); form.setFieldsValue({ splitByLine: false });
setImportConfig((prev) => ({ ...prev, splitByLine: false })); setImportConfig((prev) => ({ ...prev, splitByLine: false }));
}, [form, hasNonTextFile, importConfig.files, importConfig.splitByLine]); }, [form, hasNonTextFile, importConfig.files, importConfig.splitByLine]);
// Separate effect for fetching collection tasks when source changes // Separate effect for fetching collection tasks when source changes
useEffect(() => { useEffect(() => {
if (open && importConfig.source === DataSource.COLLECTION) { if (open && importConfig.source === DataSource.COLLECTION) {
fetchCollectionTasks(); fetchCollectionTasks();
} }
}, [fetchCollectionTasks, importConfig.source, open]); }, [fetchCollectionTasks, importConfig.source, open]);
return ( return (
<Modal <Modal
title="导入数据" title="导入数据"
open={open} open={open}
width={600} width={600}
onCancel={() => { onCancel={() => {
onClose(); onClose();
resetState(); resetState();
}} }}
maskClosable={false} maskClosable={false}
footer={ footer={
<> <>
<Button onClick={onClose}></Button> <Button onClick={onClose}></Button>
<Button <Button
type="primary" type="primary"
disabled={!importConfig?.files?.length && !importConfig.dataSource} disabled={!importConfig?.files?.length && !importConfig.dataSource}
onClick={handleImportData} onClick={handleImportData}
> >
</Button> </Button>
</> </>
} }
> >
<Form <Form
form={form} form={form}
layout="vertical" layout="vertical"
initialValues={importConfig || {}} initialValues={importConfig || {}}
onValuesChange={(_, allValues) => setImportConfig(allValues)} onValuesChange={(_, allValues) => setImportConfig(allValues)}
> >
<Form.Item <Form.Item
label="数据源" label="数据源"
name="source" name="source"
rules={[{ required: true, message: "请选择数据源" }]} rules={[{ required: true, message: "请选择数据源" }]}
> >
<Radio.Group <Radio.Group
buttonStyle="solid" buttonStyle="solid"
options={availableSourceOptions} options={availableSourceOptions}
optionType="button" optionType="button"
/> />
</Form.Item> </Form.Item>
{importConfig?.source === DataSource.COLLECTION && ( {importConfig?.source === DataSource.COLLECTION && (
<Form.Item name="dataSource" label="归集任务" required> <Form.Item name="dataSource" label="归集任务" required>
<Select placeholder="请选择归集任务" options={collectionOptions} /> <Select placeholder="请选择归集任务" options={collectionOptions} />
</Form.Item> </Form.Item>
)} )}
{/* obs import */} {/* obs import */}
{importConfig?.source === DataSource.OBS && ( {importConfig?.source === DataSource.OBS && (
<div className="grid grid-cols-2 gap-3 p-4 bg-blue-50 rounded-lg"> <div className="grid grid-cols-2 gap-3 p-4 bg-blue-50 rounded-lg">
<Form.Item <Form.Item
name="endpoint" name="endpoint"
rules={[{ required: true }]} rules={[{ required: true }]}
label="Endpoint" label="Endpoint"
> >
<Input <Input
className="h-8 text-xs" className="h-8 text-xs"
placeholder="obs.cn-north-4.myhuaweicloud.com" placeholder="obs.cn-north-4.myhuaweicloud.com"
/> />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name="bucket" name="bucket"
rules={[{ required: true }]} rules={[{ required: true }]}
label="Bucket" label="Bucket"
> >
<Input className="h-8 text-xs" placeholder="my-bucket" /> <Input className="h-8 text-xs" placeholder="my-bucket" />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name="accessKey" name="accessKey"
rules={[{ required: true }]} rules={[{ required: true }]}
label="Access Key" label="Access Key"
> >
<Input className="h-8 text-xs" placeholder="Access Key" /> <Input className="h-8 text-xs" placeholder="Access Key" />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name="secretKey" name="secretKey"
rules={[{ required: true }]} rules={[{ required: true }]}
label="Secret Key" label="Secret Key"
> >
<Input <Input
type="password" type="password"
className="h-8 text-xs" className="h-8 text-xs"
placeholder="Secret Key" placeholder="Secret Key"
/> />
</Form.Item> </Form.Item>
</div> </div>
)} )}
{/* Local Upload Component */} {/* Local Upload Component */}
{importConfig?.source === DataSource.UPLOAD && ( {importConfig?.source === DataSource.UPLOAD && (
<> <>
<Form.Item <Form.Item
label="自动解压上传的压缩包" label="自动解压上传的压缩包"
name="hasArchive" name="hasArchive"
valuePropName="checked" valuePropName="checked"
> >
<Switch /> <Switch />
</Form.Item> </Form.Item>
{isTextDataset && ( {isTextDataset && (
<Form.Item <Form.Item
label={ label={
@@ -386,10 +390,10 @@ export default function ImportConfiguration({
<Switch disabled={hasNonTextFile} /> <Switch disabled={hasNonTextFile} />
</Form.Item> </Form.Item>
)} )}
<Form.Item <Form.Item
label="上传文件" label="上传文件"
name="files" name="files"
valuePropName="fileList" valuePropName="fileList"
getValueFromEvent={( getValueFromEvent={(
event: { fileList?: UploadFile[] } | UploadFile[] event: { fileList?: UploadFile[] } | UploadFile[]
) => { ) => {
@@ -398,69 +402,69 @@ export default function ImportConfiguration({
} }
return event?.fileList; return event?.fileList;
}} }}
rules={[ rules={[
{ {
required: true, required: true,
message: "请上传文件", message: "请上传文件",
}, },
]} ]}
> >
<Dragger <Dragger
className="w-full" className="w-full"
beforeUpload={() => false} beforeUpload={() => false}
multiple multiple
> >
<p className="ant-upload-drag-icon"> <p className="ant-upload-drag-icon">
<InboxOutlined /> <InboxOutlined />
</p> </p>
<p className="ant-upload-text"></p> <p className="ant-upload-text"></p>
<p className="ant-upload-hint"></p> <p className="ant-upload-hint"></p>
</Dragger> </Dragger>
</Form.Item> </Form.Item>
</> </>
)} )}
{/* Target Configuration */} {/* Target Configuration */}
{importConfig?.target && importConfig?.target !== DataSource.UPLOAD && ( {importConfig?.target && importConfig?.target !== DataSource.UPLOAD && (
<div className="space-y-3 p-4 bg-blue-50 rounded-lg"> <div className="space-y-3 p-4 bg-blue-50 rounded-lg">
{importConfig?.target === DataSource.DATABASE && ( {importConfig?.target === DataSource.DATABASE && (
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<Form.Item <Form.Item
name="databaseType" name="databaseType"
rules={[{ required: true }]} rules={[{ required: true }]}
label="数据库类型" label="数据库类型"
> >
<Select <Select
className="w-full" className="w-full"
options={[ options={[
{ label: "MySQL", value: "mysql" }, { label: "MySQL", value: "mysql" },
{ label: "PostgreSQL", value: "postgresql" }, { label: "PostgreSQL", value: "postgresql" },
{ label: "MongoDB", value: "mongodb" }, { label: "MongoDB", value: "mongodb" },
]} ]}
></Select> ></Select>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name="tableName" name="tableName"
rules={[{ required: true }]} rules={[{ required: true }]}
label="表名" label="表名"
> >
<Input className="h-8 text-xs" placeholder="dataset_table" /> <Input className="h-8 text-xs" placeholder="dataset_table" />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name="connectionString" name="connectionString"
rules={[{ required: true }]} rules={[{ required: true }]}
label="连接字符串" label="连接字符串"
> >
<Input <Input
className="h-8 text-xs col-span-2" className="h-8 text-xs col-span-2"
placeholder="数据库连接字符串" placeholder="数据库连接字符串"
/> />
</Form.Item> </Form.Item>
</div> </div>
)} )}
</div> </div>
)} )}
</Form> </Form>
</Modal> </Modal>
); );
} }