data collection page (#31)

* feat: Update site name to DataMate and refine text for AI data processing

* feat: Refactor settings page and implement model access functionality

- Created a new ModelAccess component for managing model configurations.
- Removed the old Settings component and replaced it with a new SettingsPage component that integrates ModelAccess, SystemConfig, and WebhookConfig.
- Added SystemConfig component for managing system settings.
- Implemented WebhookConfig component for managing webhook configurations.
- Updated API functions for model management in settings.apis.ts.
- Adjusted routing to point to the new SettingsPage component.

* feat: Implement Data Collection Page with Task Management and Execution Log

- Created DataCollectionPage component to manage data collection tasks.
- Added TaskManagement and ExecutionLog components for task handling and logging.
- Integrated task operations including start, stop, edit, and delete functionalities.
- Implemented filtering and searching capabilities in task management.
- Introduced SimpleCronScheduler for scheduling tasks with cron expressions.
- Updated CreateTask component to utilize new scheduling and template features.
- Enhanced BasicInformation component to conditionally render fields based on visibility settings.
- Refactored ImportConfiguration component to remove NAS import section.
This commit is contained in:
chenghh-9609
2025-10-28 16:15:06 +08:00
committed by GitHub
parent 1a6e25758e
commit fad76e7477
11 changed files with 1180 additions and 418 deletions

View File

@@ -0,0 +1,556 @@
import React, { useState, useCallback, useEffect } from "react";
import {
Card,
Select,
Input,
Space,
Typography,
Row,
Col,
Divider,
Button,
Tooltip,
} from "antd";
import { InfoCircleOutlined } from "@ant-design/icons";
const { Title, Text } = Typography;
const { Option } = Select;
export interface AdvancedCronConfig {
second: string;
minute: string;
hour: string;
day: string;
month: string;
weekday: string;
year?: string;
cronExpression: string;
}
interface AdvancedCronSchedulerProps {
value?: AdvancedCronConfig;
onChange?: (config: AdvancedCronConfig) => void;
showYear?: boolean; // 是否显示年份字段
className?: string;
}
// Cron字段的选项配置
const CRON_OPTIONS = {
second: {
label: "秒",
range: [0, 59],
examples: ["0", "*/5", "10,20,30", "0-30"],
description: "秒钟 (0-59)",
},
minute: {
label: "分钟",
range: [0, 59],
examples: ["0", "*/15", "5,10,15", "0-30"],
description: "分钟 (0-59)",
},
hour: {
label: "小时",
range: [0, 23],
examples: ["0", "*/2", "8,14,20", "9-17"],
description: "小时 (0-23)",
},
day: {
label: "日",
range: [1, 31],
examples: ["*", "1", "1,15", "1-15", "*/2"],
description: "日期 (1-31)",
},
month: {
label: "月",
range: [1, 12],
examples: ["*", "1", "1,6,12", "3-9", "*/3"],
description: "月份 (1-12)",
},
year: {
label: "年",
range: [1970, 2099],
examples: ["*", "2024", "2024-2026", "*/2"],
description: "年份 (1970-2099)",
},
weekday: {
label: "周",
range: [0, 7], // 0和7都表示周日
examples: ["*", "1", "1-5", "1,3,5", "0,6"],
description: "星期 (0-7, 0和7都表示周日)",
weekNames: ["周日", "周一", "周二", "周三", "周四", "周五", "周六"],
},
};
// 生成常用的cron表达式选项
const generateCommonOptions = (field: keyof typeof CRON_OPTIONS) => {
const options = [
{ label: "* (任意)", value: "*" },
{ label: "? (不指定)", value: "?" }, // 仅用于日和周字段
];
const config = CRON_OPTIONS[field];
const [start, end] = config.range;
// 添加具体数值选项
if (field === "weekday") {
const weekdayConfig = config as { weekNames?: string[] };
weekdayConfig.weekNames?.forEach((name: string, index: number) => {
options.push({ label: `${index} (${name})`, value: index.toString() });
});
// 添加7作为周日的别名
options.push({ label: "7 (周日)", value: "7" });
} else {
// 添加部分具体数值
const step =
field === "year" ? 5 : field === "day" || field === "month" ? 3 : 5;
for (let i = start; i <= end; i += step) {
if (i <= end) {
options.push({ label: i.toString(), value: i.toString() });
}
}
}
// 添加间隔选项
if (field !== "year") {
options.push(
{ label: "*/2 (每2个)", value: "*/2" },
{ label: "*/5 (每5个)", value: "*/5" },
{ label: "*/10 (每10个)", value: "*/10" }
);
}
// 添加范围选项
if (field === "hour") {
options.push(
{ label: "9-17 (工作时间)", value: "9-17" },
{ label: "0-6 (凌晨)", value: "0-6" }
);
} else if (field === "weekday") {
options.push(
{ label: "1-5 (工作日)", value: "1-5" },
{ label: "0,6 (周末)", value: "0,6" }
);
} else if (field === "day") {
options.push(
{ label: "1-15 (上半月)", value: "1-15" },
{ label: "16-31 (下半月)", value: "16-31" }
);
}
return options;
};
// 验证cron字段值
const validateCronField = (
value: string,
field: keyof typeof CRON_OPTIONS
): boolean => {
if (!value || value === "*" || value === "?") return true;
const config = CRON_OPTIONS[field];
const [min, max] = config.range;
// 验证基本格式
const patterns = [
/^\d+$/, // 单个数字
/^\d+-\d+$/, // 范围
/^\*\/\d+$/, // 间隔
/^(\d+,)*\d+$/, // 列表
/^(\d+-\d+,)*(\d+-\d+|\d+)$/, // 复合表达式
];
if (!patterns.some((pattern) => pattern.test(value))) {
return false;
}
// 验证数值范围
const numbers = value.match(/\d+/g);
if (numbers) {
return numbers.every((num) => {
const n = parseInt(num);
return n >= min && n <= max;
});
}
return true;
};
// 生成cron表达式
const generateCronExpression = (
config: Omit<AdvancedCronConfig, "cronExpression">
): string => {
const { second, minute, hour, day, month, weekday, year } = config;
const parts = [second, minute, hour, day, month, weekday];
if (year && year !== "*") {
parts.push(year);
}
return parts.join(" ");
};
// 解析cron表达式为人类可读的描述
const parseCronDescription = (cronExpression: string): string => {
const parts = cronExpression.split(" ");
if (parts.length < 6) return cronExpression;
const [second, minute, hour, day, month, weekday, year] = parts;
const descriptions = [];
// 时间描述
if (second === "0" && minute !== "*" && hour !== "*") {
descriptions.push(`${hour.padStart(2, "0")}:${minute.padStart(2, "0")}`);
} else {
if (hour !== "*") descriptions.push(`${hour}`);
if (minute !== "*") descriptions.push(`${minute}`);
if (second !== "*" && second !== "0") descriptions.push(`${second}`);
}
// 日期描述
if (day !== "*" && day !== "?") {
descriptions.push(`${day}`);
}
// 月份描述
if (month !== "*") {
descriptions.push(`${month}`);
}
// 星期描述
if (weekday !== "*" && weekday !== "?") {
const weekNames = [
"周日",
"周一",
"周二",
"周三",
"周四",
"周五",
"周六",
"周日",
];
if (weekday === "1-5") {
descriptions.push("工作日");
} else if (weekday === "0,6") {
descriptions.push("周末");
} else if (/^\d$/.test(weekday)) {
descriptions.push(weekNames[parseInt(weekday)]);
} else {
descriptions.push(`${weekday}`);
}
}
// 年份描述
if (year && year !== "*") {
descriptions.push(`${year}`);
}
return descriptions.length > 0 ? descriptions.join(" ") : "每秒执行";
};
const AdvancedCronScheduler: React.FC<AdvancedCronSchedulerProps> = ({
value = {
second: "0",
minute: "0",
hour: "0",
day: "*",
month: "*",
weekday: "?",
year: "*",
cronExpression: "0 0 0 * * ?",
},
onChange,
showYear = false,
className,
}) => {
const [config, setConfig] = useState<AdvancedCronConfig>(value);
const [customMode, setCustomMode] = useState(false);
// 更新配置
const updateConfig = useCallback(
(updates: Partial<AdvancedCronConfig>) => {
const newConfig = { ...config, ...updates };
newConfig.cronExpression = generateCronExpression(newConfig);
setConfig(newConfig);
onChange?.(newConfig);
},
[config, onChange]
);
// 同步外部值
useEffect(() => {
setConfig(value);
}, [value]);
// 处理字段变化
const handleFieldChange = (
field: keyof AdvancedCronConfig,
fieldValue: string
) => {
if (field === "cronExpression") {
// 直接编辑cron表达式
const parts = fieldValue.split(" ");
if (parts.length >= 6) {
const newConfig = {
second: parts[0] || "0",
minute: parts[1] || "0",
hour: parts[2] || "0",
day: parts[3] || "*",
month: parts[4] || "*",
weekday: parts[5] || "?",
year: parts[6] || "*",
cronExpression: fieldValue,
};
setConfig(newConfig);
onChange?.(newConfig);
}
} else {
updateConfig({ [field]: fieldValue });
}
};
// 快速设置预设
const setPreset = (preset: Partial<AdvancedCronConfig>) => {
updateConfig(preset);
};
// 常用预设
const commonPresets = [
{
label: "每秒",
config: {
second: "*",
minute: "*",
hour: "*",
day: "*",
month: "*",
weekday: "?",
},
},
{
label: "每分钟",
config: {
second: "0",
minute: "*",
hour: "*",
day: "*",
month: "*",
weekday: "?",
},
},
{
label: "每小时",
config: {
second: "0",
minute: "0",
hour: "*",
day: "*",
month: "*",
weekday: "?",
},
},
{
label: "每天午夜",
config: {
second: "0",
minute: "0",
hour: "0",
day: "*",
month: "*",
weekday: "?",
},
},
{
label: "每周一9点",
config: {
second: "0",
minute: "0",
hour: "9",
day: "?",
month: "*",
weekday: "1",
},
},
{
label: "每月1日0点",
config: {
second: "0",
minute: "0",
hour: "0",
day: "1",
month: "*",
weekday: "?",
},
},
{
label: "工作日9点",
config: {
second: "0",
minute: "0",
hour: "9",
day: "?",
month: "*",
weekday: "1-5",
},
},
{
label: "每15分钟",
config: {
second: "0",
minute: "*/15",
hour: "*",
day: "*",
month: "*",
weekday: "?",
},
},
];
const fields: Array<keyof typeof CRON_OPTIONS> = [
"second",
"minute",
"hour",
"day",
"month",
"weekday",
];
if (showYear) fields.push("year");
return (
<Card className={className} size="small">
<Space direction="vertical" className="w-full" size="middle">
{/* 标题和切换模式 */}
<div className="flex justify-between items-center">
<Title level={5} className="m-0">
Cron
</Title>
<Button
size="small"
type={customMode ? "primary" : "default"}
onClick={() => setCustomMode(!customMode)}
>
{customMode ? "切换到向导模式" : "切换到手动模式"}
</Button>
</div>
{/* 快速预设 */}
<div>
<Text strong></Text>
<div className="mt-2 flex gap-2 flex-wrap">
{commonPresets.map((preset, index) => (
<Button
key={index}
size="small"
onClick={() => setPreset(preset.config)}
>
{preset.label}
</Button>
))}
</div>
</div>
{customMode ? (
/* 手动编辑模式 */
<div>
<Text strong>Cron </Text>
<Input
className="mt-2"
value={config.cronExpression}
onChange={(e) =>
handleFieldChange("cronExpression", e.target.value)
}
placeholder="秒 分 时 日 月 周 [年]"
/>
<Text
type="secondary"
className="text-xs block mt-1"
>
(0-59) (0-59) (0-23) (1-31) (1-12) (0-7)
[(1970-2099)]
</Text>
</div>
) : (
/* 向导模式 */
<Row gutter={[16, 16]}>
{fields.map((field) => {
const fieldConfig = CRON_OPTIONS[field];
const options = generateCommonOptions(field);
return (
<Col xs={24} sm={12} md={8} key={field}>
<div>
<div className="flex items-center mb-1">
<Text strong>{fieldConfig.label}</Text>
<Tooltip title={fieldConfig.description}>
<InfoCircleOutlined className="ml-1 text-gray-400" />
</Tooltip>
</div>
<Select
className="w-full"
value={config[field]}
onChange={(val) => handleFieldChange(field, val)}
showSearch
optionFilterProp="label"
placeholder={`选择${fieldConfig.label}`}
>
{options.map((option) => (
<Option key={option.value} value={option.value}>
{option.label}
</Option>
))}
</Select>
{/* 自定义输入 */}
<Input
className="mt-1"
size="small"
placeholder="或输入自定义值"
value={config[field] || ""}
onChange={(e) => handleFieldChange(field, e.target.value)}
status={
validateCronField(config[field] || "", field) ? "" : "error"
}
/>
</div>
</Col>
);
})}
</Row>
)}
<Divider className="my-3" />
{/* 结果预览 */}
<div>
<Text strong> Cron </Text>
<Input
className="mt-2 bg-gray-100"
value={config.cronExpression}
readOnly
/>
<Text
type="secondary"
className="text-xs block mt-1"
>
{parseCronDescription(config.cronExpression)}
</Text>
</div>
{/* 字段说明 */}
<div>
<Text strong></Text>
<div className="mt-2 text-xs text-gray-600">
<Row gutter={[16, 8]}>
<Col span={12}> * </Col>
<Col span={12}> ? </Col>
<Col span={12}> */5 5</Col>
<Col span={12}> 1-5 </Col>
<Col span={12}> 1,3,5 </Col>
<Col span={12}> </Col>
</Row>
</div>
</div>
</Space>
</Card>
);
};
export default AdvancedCronScheduler;

View File

@@ -1,11 +1,5 @@
import React, { useState, useEffect, useRef } from "react"; import React, { useState, useEffect, useRef } from "react";
import { import { Tag, Pagination, Tooltip, Empty, Popover, Spin } from "antd";
Tag,
Pagination,
Tooltip,
Empty,
Popover,
} from "antd";
import { ClockCircleOutlined, StarFilled } from "@ant-design/icons"; import { ClockCircleOutlined, StarFilled } from "@ant-design/icons";
import type { ItemType } from "antd/es/menu/interface"; import type { ItemType } from "antd/es/menu/interface";
import { formatDateTime } from "@/utils/unit"; import { formatDateTime } from "@/utils/unit";

View File

@@ -3,37 +3,24 @@ const API = require("../mock-apis.cjs");
const { Random } = Mock; const { Random } = Mock;
// 生成模拟数据归集统计信息 // 生成模拟数据归集统计信息
const collectionStatistics = { function dataXTemplate() {
period: Random.pick(["HOUR", "DAY", "WEEK", "MONTH"]), return {
totalTasks: Random.integer(50, 200), id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
activeTasks: Random.integer(10, 50), name: Mock.Random.ctitle(5, 15),
successfulExecutions: Random.integer(30, 150), sourceType: Mock.Random.csentence(3, 10),
failedExecutions: Random.integer(0, 50), targetType: Mock.Random.csentence(3, 10),
totalExecutions: Random.integer(20, 100), description: Mock.Random.csentence(5, 20),
avgExecutionTime: Random.integer(1000, 10000), // in milliseconds version: `v${Mock.Random.integer(1, 5)}.${Mock.Random.integer(
avgThroughput: Random.integer(100, 1000), // records per second 0,
topDataSources: new Array(5).fill(null).map(() => ({ 9
dataSourceId: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""), )}.${Mock.Random.integer(0, 9)}`,
dataSourceName: Mock.Random.word(5, 15), isSystem: Mock.Random.boolean(),
type: Mock.Random.pick([ createdAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
"MySQL", updatedAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
"PostgreSQL", };
"ORACLE", }
"SQLSERVER",
"MONGODB", const templateList = new Array(20).fill(null).map(dataXTemplate);
"REDIS",
"ELASTICSEARCH",
"HIVE",
"HDFS",
"KAFKA",
"HTTP",
"FILE",
]),
taskCount: Mock.Random.integer(1, 20),
executionCount: Mock.Random.integer(1, 50),
recordsProcessed: Mock.Random.integer(70, 100), // percentage
})),
};
// 生成模拟任务数据 // 生成模拟任务数据
function taskItem() { function taskItem() {
@@ -89,30 +76,21 @@ function executionLogItem() {
const executionLogList = new Array(100).fill(null).map(executionLogItem); const executionLogList = new Array(100).fill(null).map(executionLogItem);
module.exports = function (router) { module.exports = function (router) {
// 获取数据统计信息
router.get(API.queryCollectionStatisticsUsingGet, (req, res) => {
res.send({
code: "0",
msg: "Success",
data: collectionStatistics,
});
});
// 获取任务列表 // 获取任务列表
router.post(API.queryTasksUsingPost, (req, res) => { router.get(API.queryTasksUsingGet, (req, res) => {
const { searchTerm, filters, page = 1, size = 10 } = req.body; const { keyword, status, page = 0, size = 10 } = req.query;
let filteredTasks = taskList; let filteredTasks = taskList;
if (searchTerm) { if (keyword) {
filteredTasks = filteredTasks.filter((task) => filteredTasks = filteredTasks.filter((task) =>
task.name.includes(searchTerm) task.name.includes(keyword)
); );
} }
if (filters && filters.status && filters.status.length > 0) { if (status && status.length > 0) {
filteredTasks = filteredTasks.filter((task) => filteredTasks = filteredTasks.filter((task) =>
filters.status.includes(task.status) status.includes(task.status)
); );
} }
const startIndex = (page - 1) * size; const startIndex = page * size;
const endIndex = startIndex + size; const endIndex = startIndex + size;
const paginatedTasks = filteredTasks.slice(startIndex, endIndex); const paginatedTasks = filteredTasks.slice(startIndex, endIndex);
@@ -123,7 +101,30 @@ module.exports = function (router) {
totalElements: filteredTasks.length, totalElements: filteredTasks.length,
page, page,
size, size,
results: paginatedTasks, content: paginatedTasks,
},
});
});
router.get(API.queryDataXTemplatesUsingGet, (req, res) => {
const { keyword, page = 0, size = 10 } = req.query;
let filteredTemplates = templateList;
if (keyword) {
filteredTemplates = filteredTemplates.filter((template) =>
template.name.includes(keyword)
);
}
const startIndex = page * size;
const endIndex = startIndex + size;
const paginatedTemplates = filteredTemplates.slice(startIndex, endIndex);
res.send({
code: "0",
msg: "Success",
data: {
content: paginatedTemplates,
totalElements: filteredTemplates.length,
page,
size,
}, },
}); });
}); });

View File

@@ -1,20 +1,9 @@
import { useState } from "react"; import { useState } from "react";
import { import { Input, Button, Radio, Form, InputNumber, App, Select } from "antd";
Card,
Input,
Button,
Select,
Radio,
Form,
Divider,
InputNumber,
TimePicker,
App,
} from "antd";
import { Link, useNavigate } from "react-router"; import { Link, useNavigate } from "react-router";
import { ArrowLeft } from "lucide-react"; import { ArrowLeft } from "lucide-react";
import { createTaskUsingPost } from "../collection.apis"; import { createTaskUsingPost } from "../collection.apis";
import DevelopmentInProgress from "@/components/DevelopmentInProgress"; import SimpleCronScheduler from "@/pages/DataCollection/Create/SimpleCronScheduler";
const { TextArea } = Input; const { TextArea } = Input;
@@ -30,16 +19,16 @@ interface ScheduleConfig {
const defaultTemplates = [ const defaultTemplates = [
{ {
id: "nas-to-local", id: "nas",
name: "NAS到本地", name: "NAS到本地",
description: "从NAS文件系统导入数据到本地文件系统", description: "从NAS文件系统导入数据到本地文件系统",
config: { config: {
reader: "nasreader", reader: "nfsreader",
writer: "localwriter", writer: "localwriter",
}, },
}, },
{ {
id: "obs-to-local", id: "obs",
name: "OBS到本地", name: "OBS到本地",
description: "从OBS文件系统导入数据到本地文件系统", description: "从OBS文件系统导入数据到本地文件系统",
config: { config: {
@@ -48,7 +37,7 @@ const defaultTemplates = [
}, },
}, },
{ {
id: "web-tolocal", id: "web",
name: "Web到本地", name: "Web到本地",
description: "从Web URL导入数据到本地文件系统", description: "从Web URL导入数据到本地文件系统",
config: { config: {
@@ -58,9 +47,13 @@ const defaultTemplates = [
}, },
]; ];
export default function CollectionTaskCreate() { enum TemplateType {
return <DevelopmentInProgress showTime="2025.10.30" />; NAS = "nas",
OBS = "obs",
WEB = "web",
}
export default function CollectionTaskCreate() {
const navigate = useNavigate(); const navigate = useNavigate();
const [form] = Form.useForm(); const [form] = Form.useForm();
const { message } = App.useApp(); const { message } = App.useApp();
@@ -68,7 +61,7 @@ export default function CollectionTaskCreate() {
const [templateType, setTemplateType] = useState<"default" | "custom">( const [templateType, setTemplateType] = useState<"default" | "custom">(
"default" "default"
); );
const [selectedTemplate, setSelectedTemplate] = useState(""); const [selectedTemplate, setSelectedTemplate] = useState("nas");
const [customConfig, setCustomConfig] = useState(""); const [customConfig, setCustomConfig] = useState("");
const [scheduleConfig, setScheduleConfig] = useState<ScheduleConfig>({ const [scheduleConfig, setScheduleConfig] = useState<ScheduleConfig>({
@@ -104,7 +97,7 @@ export default function CollectionTaskCreate() {
}; };
return ( return (
<div className="min-h-screen"> <div className="h-full flex flex-col">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<div className="flex items-center"> <div className="flex items-center">
<Link to="/data/collection"> <Link to="/data/collection">
@@ -116,244 +109,234 @@ export default function CollectionTaskCreate() {
</div> </div>
</div> </div>
<Card> <div className="flex-overflow-auto border-card">
<Form <div className="flex-1 overflow-auto p-6">
form={form} <Form
layout="vertical" form={form}
initialValues={{ layout="vertical"
name: "", initialValues={scheduleConfig}
datasetName: "", onValuesChange={(_, allValues) => {
fileFormat: "", // 文件格式变化时重置模板选择
description: "", if (_.fileFormat !== undefined) setSelectedTemplate("");
cronExpression: "", }}
retryCount: 3,
timeout: 3600,
incrementalField: "",
}}
onValuesChange={(_, allValues) => {
// 文件格式变化时重置模板选择
if (_.fileFormat !== undefined) setSelectedTemplate("");
}}
>
{/* 基本信息 */}
<h2 className="font-medium text-gray-900 text-lg mb-4"></h2>
<Form.Item
label="任务名称"
name="name"
rules={[{ required: true, message: "请输入任务名称" }]}
> >
<Input placeholder="请输入任务名称" /> {/* 基本信息 */}
</Form.Item> <h2 className="font-medium text-gray-900 text-lg mb-2"></h2>
<Form.Item label="描述" name="description">
<TextArea placeholder="请输入任务描述" rows={3} />
</Form.Item>
<Form.Item label="文件格式" name="fileFormat">
<Input placeholder="请填写文件格式,使用正则表达式" />
</Form.Item>
{/* 同步配置 */} <Form.Item
<h2 className="font-medium text-gray-900 my-4 text-lg"></h2> label="任务名称"
<Form.Item label="同步方式"> name="name"
<Radio.Group rules={[{ required: true, message: "请输入任务名称" }]}
value={scheduleConfig.type}
onChange={(e) =>
setScheduleConfig({
type: e.target.value as ScheduleConfig["type"],
})
}
> >
<Radio value="immediate"></Radio> <Input placeholder="请输入任务名称" />
<Radio value="scheduled"></Radio> </Form.Item>
</Radio.Group> <Form.Item label="描述" name="description">
</Form.Item> <TextArea placeholder="请输入任务描述" rows={3} />
{scheduleConfig.type === "scheduled" && ( </Form.Item>
<div className="w-full grid grid-cols-1 md:grid-cols-2 gap-4">
<Form.Item label="调度类型"> {/* 同步配置 */}
<Select <h2 className="font-medium text-gray-900 pt-6 mb-2 text-lg">
options={[
{ label: "每日", value: "day" }, </h2>
{ label: "每周", value: "week" }, <Form.Item label="同步方式">
{ label: "每月", value: "month" }, <Radio.Group
{ label: "自定义Cron", value: "custom" }, value={scheduleConfig.type}
]} onChange={(e) =>
value={scheduleConfig.scheduleType} setScheduleConfig({
type: e.target.value as ScheduleConfig["type"],
})
}
>
<Radio value="immediate"></Radio>
<Radio value="scheduled"></Radio>
</Radio.Group>
</Form.Item>
{scheduleConfig.type === "scheduled" && (
<Form.Item
label=""
name="cronExpression"
rules={[{ required: true, message: "请输入Cron表达式" }]}
>
<SimpleCronScheduler
className="px-2 rounded"
value={scheduleConfig.cronExpression || "* * * * *"}
showYear
onChange={(value) => onChange={(value) =>
setScheduleConfig((prev) => ({ setScheduleConfig({ ...scheduleConfig, cron: value })
...prev,
scheduleType: value as ScheduleConfig["scheduleType"],
}))
} }
/> />
</Form.Item> </Form.Item>
{scheduleConfig.scheduleType === "custom" ? ( )}
<Form.Item <Form.Item name="maxRetries" label="最大执行次数">
label="Cron表达式" <InputNumber min={1} style={{ width: "100%" }} />
name="cronExpression"
rules={[{ required: true, message: "请输入Cron表达式" }]}
>
<Input
placeholder="例如:0 0 * * * 表示每天午夜执行"
value={scheduleConfig.cronExpression}
onChange={(e) =>
setScheduleConfig((prev) => ({
...prev,
cronExpression: e.target.value,
}))
}
/>
</Form.Item>
) : (
<Form.Item label="执行时间" className="w-full">
{scheduleConfig.scheduleType === "day" ? (
<TimePicker />
) : (
<Select
options={
scheduleConfig.scheduleType === "week"
? [
{ label: "周一", value: "1" },
{ label: "周二", value: "2" },
{ label: "周三", value: "3" },
{ label: "周四", value: "4" },
{ label: "周五", value: "5" },
{ label: "周六", value: "6" },
{ label: "周日", value: "0" },
]
: [
{ label: "每月1日", value: "1" },
{ label: "每月5日", value: "5" },
{ label: "每月10日", value: "10" },
{ label: "每月15日", value: "15" },
{ label: "每月20日", value: "20" },
{ label: "每月25日", value: "25" },
{ label: "每月30日", value: "30" },
]
}
placeholder={
scheduleConfig.scheduleType === "week"
? "选择星期几"
: "选择日期"
}
value={scheduleConfig.dayOfWeek}
onChange={(value) =>
setScheduleConfig((prev) => ({
...prev,
dayOfWeek: value as string,
}))
}
/>
)}
</Form.Item>
)}
</div>
)}
<Form.Item label="最大执行次数">
<InputNumber
min={1}
value={scheduleConfig.maxRetries}
onChange={(value) =>
setScheduleConfig((prev) => ({
...prev,
maxRetries: value,
}))
}
className="w-full"
style={{ width: "100%" }}
/>
</Form.Item>
{/* 模板配置 */}
<h2 className="font-medium text-gray-900 my-4 text-lg"></h2>
<Form.Item label="模板类型">
<Radio.Group
value={templateType}
onChange={(e) => setTemplateType(e.target.value)}
>
<Radio value="default">使</Radio>
<Radio value="custom">DataX JSON配置</Radio>
</Radio.Group>
</Form.Item>
{templateType === "default" && (
<Form.Item label="选择模板">
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
{defaultTemplates.map((template) => (
<div
key={template.id}
className={`border p-4 rounded-md hover:shadow-lg transition-shadow ${
selectedTemplate === template.id
? "border-blue-500"
: "border-gray-300"
}`}
onClick={() => setSelectedTemplate(template.id)}
>
<div className="font-medium">{template.name}</div>
<div className="text-gray-500">{template.description}</div>
<div className="text-gray-400">
{template.config.reader} {template.config.writer}
</div>
</div>
))}
</div>
</Form.Item> </Form.Item>
)}
{templateType === "custom" && ( {/* 模板配置 */}
<Form.Item label="DataX JSON配置"> <h2 className="font-medium text-gray-900 pt-6 mb-2 text-lg">
<TextArea
placeholder="请输入DataX JSON配置..." </h2>
value={customConfig} {/* <Form.Item label="模板类型">
onChange={(e) => setCustomConfig(e.target.value)} <Radio.Group
rows={12} value={templateType}
className="w-full" onChange={(e) => setTemplateType(e.target.value)}
/>
</Form.Item>
)}
{/* 数据集配置 */}
{templateType === "default" && (
<>
<h2 className="font-medium text-gray-900 my-4 text-lg">
</h2>
<Form.Item
label="是否创建数据集"
name="createDataset"
required
rules={[{ required: true, message: "请选择是否创建数据集" }]}
> >
<Radio.Group <Radio value="default">使用默认模板</Radio>
value={isCreateDataset} <Radio value="custom">自定义DataX JSON配置</Radio>
onChange={(e) => setIsCreateDataset(e.target.value)} </Radio.Group>
> </Form.Item> */}
<Radio value={true}></Radio> {templateType === "default" && (
<Radio value={false}></Radio> <>
</Radio.Group> {/* <Form.Item label="选择模板">
</Form.Item> <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
{isCreateDataset && ( {defaultTemplates.map((template) => (
<> <div
<Form.Item key={template.id}
label="数据集名称" className={`border p-4 rounded-md hover:shadow-lg transition-shadow ${
name="datasetName" selectedTemplate === template.id
rules={[{ required: true, message: "请输入数据集名称" }]} ? "border-blue-500"
> : "border-gray-300"
<Input placeholder="请输入数据集名称" /> }`}
</Form.Item> onClick={() => setSelectedTemplate(template.id)}
</> >
)} <div className="font-medium">{template.name}</div>
</> <div className="text-gray-500">
)} {template.description}
</div>
<div className="text-gray-400">
{template.config.reader} → {template.config.writer}
</div>
</div>
))}
</div>
</Form.Item> */}
{/* nas import */}
{selectedTemplate === TemplateType.NAS && (
<div className="grid grid-cols-2 gap-3 px-2 rounded">
<Form.Item
name="nasPath"
rules={[{ required: true, message: "请输入NAS地址" }]}
label="NAS地址"
>
<Input placeholder="192.168.1.100" />
</Form.Item>
<Form.Item
name="sharePath"
rules={[{ required: true, message: "请输入共享路径" }]}
label="共享路径"
>
<Input placeholder="/share/importConfig" />
</Form.Item>
<Form.Item
name="fileList"
label="文件列表"
className="col-span-2"
>
<Select
placeholder="请选择文件列表"
mode="tags"
multiple
/>
</Form.Item>
</div>
)}
{/* 提交按钮 */} {/* obs import */}
<Divider /> {selectedTemplate === TemplateType.OBS && (
<div className="flex gap-2 justify-end"> <div className="grid grid-cols-2 gap-3 p-4 bg-blue-50 rounded-lg">
<Button onClick={() => navigate("/data/collection")}></Button> <Form.Item
<Button type="primary" onClick={handleSubmit}> name="endpoint"
rules={[{ required: true }]}
</Button> label="Endpoint"
</div> >
</Form> <Input
</Card> 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>
)}
</>
)}
{templateType === "custom" && (
<Form.Item label="DataX JSON配置">
<TextArea
placeholder="请输入DataX JSON配置..."
value={customConfig}
onChange={(e) => setCustomConfig(e.target.value)}
rows={12}
className="w-full"
/>
</Form.Item>
)}
{/* 数据集配置 */}
{templateType === "default" && (
<>
<h2 className="font-medium text-gray-900 my-4 text-lg">
</h2>
<Form.Item
label="是否创建数据集"
name="createDataset"
required
rules={[{ required: true, message: "请选择是否创建数据集" }]}
>
<Radio.Group
value={isCreateDataset}
onChange={(e) => setIsCreateDataset(e.target.value)}
>
<Radio value={true}></Radio>
<Radio value={false}></Radio>
</Radio.Group>
</Form.Item>
{isCreateDataset && (
<>
<Form.Item
label="数据集名称"
name="datasetName"
rules={[{ required: true, message: "请输入数据集名称" }]}
>
<Input placeholder="请输入数据集名称" />
</Form.Item>
</>
)}
</>
)}
</Form>
</div>
<div className="flex gap-2 justify-end border-top p-6">
<Button onClick={() => navigate("/data/collection")}></Button>
<Button type="primary" onClick={handleSubmit}>
</Button>
</div>
</div>
</div> </div>
); );
} }

View File

@@ -0,0 +1,207 @@
import React, { useState, useCallback } from "react";
import {
Card,
Radio,
Select,
Space,
Typography,
TimePicker,
Button,
Input,
Form,
} from "antd";
import type { RadioChangeEvent } from "antd";
import type { Dayjs } from "dayjs";
import dayjs from "dayjs";
const { Text } = Typography;
const { Option } = Select;
export interface SimpleCronConfig {
type: "once" | "daily" | "weekly" | "monthly";
time?: string; // HH:mm 格式
weekDay?: number; // 0-6, 0 表示周日
monthDay?: number; // 1-31
cronExpression: string;
}
interface SimpleCronSchedulerProps {
value?: SimpleCronConfig;
onChange?: (config: SimpleCronConfig) => void;
className?: string;
}
const defaultConfig: SimpleCronConfig = {
type: "once",
time: "00:00",
cronExpression: "0 0 0 * * ?",
};
// 生成周几选项
const weekDayOptions = [
{ label: "周日", value: 0 },
{ label: "周一", value: 1 },
{ label: "周二", value: 2 },
{ label: "周三", value: 3 },
{ label: "周四", value: 4 },
{ label: "周五", value: 5 },
{ label: "周六", value: 6 },
];
// 生成月份日期选项
const monthDayOptions = Array.from({ length: 31 }, (_, i) => ({
label: `${i + 1}`,
value: i + 1,
}));
// 常用时间预设
const commonTimePresets = [
{ label: "上午 9:00", value: "09:00" },
{ label: "中午 12:00", value: "12:00" },
{ label: "下午 2:00", value: "14:00" },
{ label: "下午 6:00", value: "18:00" },
{ label: "晚上 8:00", value: "20:00" },
{ label: "午夜 0:00", value: "00:00" },
];
const SimpleCronScheduler: React.FC<SimpleCronSchedulerProps> = ({
value = defaultConfig,
onChange,
className,
}) => {
const [config, setConfig] = useState<SimpleCronConfig>(value);
// 更新配置并生成 cron 表达式
const updateConfig = useCallback(
(updates: Partial<SimpleCronConfig>) => {
const newConfig = { ...config, ...updates };
const [hour, minute] = (newConfig.time || "00:00").split(":");
// 根据不同类型生成 cron 表达式
let cronExpression = "";
switch (newConfig.type) {
case "once":
cronExpression = `0 ${minute} ${hour} * * ?`;
break;
case "daily":
cronExpression = `0 ${minute} ${hour} * * ?`;
break;
case "weekly":
cronExpression = `0 ${minute} ${hour} ? * ${newConfig.weekDay}`;
break;
case "monthly":
cronExpression = `0 ${minute} ${hour} ${newConfig.monthDay} * ?`;
break;
}
newConfig.cronExpression = cronExpression;
setConfig(newConfig);
onChange?.(newConfig);
},
[config, onChange]
);
// 处理类型改变
const handleTypeChange = (type) => {
const updates: Partial<SimpleCronConfig> = { type };
// 设置默认值
if (type === "weekly" && !config.weekDay) {
updates.weekDay = 1; // 默认周一
} else if (type === "monthly" && !config.monthDay) {
updates.monthDay = 1; // 默认每月1号
}
updateConfig(updates);
};
// 处理时间改变
const handleTimeChange = (value: Dayjs | null) => {
if (value) {
updateConfig({ time: value.format("HH:mm") });
}
};
// 快速设置预设时间
const handleTimePreset = (time: string) => {
updateConfig({ time });
};
return (
<Space direction="vertical" className={`w-full ${className || ""}`}>
{/* 执行周期选择 */}
<Form.Item label="执行周期" required>
<Select value={config.type} onChange={handleTypeChange}>
<Select.Option value="once"></Select.Option>
<Select.Option value="daily"></Select.Option>
<Select.Option value="weekly"></Select.Option>
<Select.Option value="monthly"></Select.Option>
</Select>
</Form.Item>
{/* 时间选择 */}
<Form.Item label="执行时间" required>
<Space wrap>
<TimePicker
format="HH:mm"
value={config.time ? dayjs(config.time, "HH:mm") : null}
onChange={handleTimeChange}
placeholder="选择时间"
/>
<Space wrap className="mt-2">
{commonTimePresets.map((preset) => (
<Button
key={preset.value}
size="small"
className={
config.time === preset.value ? "ant-btn-primary" : ""
}
onClick={() => handleTimePreset(preset.value)}
>
{preset.label}
</Button>
))}
</Space>
</Space>
</Form.Item>
{/* 周几选择 */}
{config.type === "weekly" && (
<Form.Item label="执行日期" required>
<Select
className="w-32"
value={config.weekDay}
onChange={(weekDay) => updateConfig({ weekDay })}
placeholder="选择周几"
options={weekDayOptions}
></Select>
</Form.Item>
)}
{/* 月份日期选择 */}
{config.type === "monthly" && (
<Form.Item label="执行日期" required>
<Select
className="w-32"
value={config.monthDay}
onChange={(monthDay) => updateConfig({ monthDay })}
placeholder="选择日期"
options={monthDayOptions}
></Select>
</Form.Item>
)}
{/* Cron 表达式预览 */}
{/* <div className="mt-4 pt-4 border-t border-gray-200">
<Text>生成的 Cron 表达式</Text>
<Input
className="mt-2 bg-gray-100"
value={config.cronExpression}
readOnly
/>
</div> */}
</Space>
);
};
export default SimpleCronScheduler;

View File

@@ -1,8 +1,8 @@
import { useState } from "react"; import { useState } from "react";
import { Button, Tabs } from "antd"; import { Button, Tabs } from "antd";
import { PlusOutlined } from "@ant-design/icons"; import { PlusOutlined } from "@ant-design/icons";
import TaskManagement from "./components/TaskManagement"; import TaskManagement from "./TaskManagement";
import ExecutionLog from "./components/ExecutionLog"; import ExecutionLog from "./ExecutionLog";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import DevelopmentInProgress from "@/components/DevelopmentInProgress"; import DevelopmentInProgress from "@/components/DevelopmentInProgress";
@@ -10,10 +10,10 @@ export default function DataCollection() {
const navigate = useNavigate(); const navigate = useNavigate();
const [activeTab, setActiveTab] = useState("task-management"); const [activeTab, setActiveTab] = useState("task-management");
return <DevelopmentInProgress showTime="2025.10.30" />; // return <DevelopmentInProgress showTime="2025.10.30" />;
return ( return (
<div> <div className="gap-4 h-full flex flex-col">
<div className="flex justify-between items-end"> <div className="flex justify-between items-end">
<div> <div>
<h1 className="text-xl font-bold text-gray-900 mb-2"></h1> <h1 className="text-xl font-bold text-gray-900 mb-2"></h1>

View File

@@ -2,8 +2,8 @@ import { Card, Badge, Table } from "antd";
import type { ColumnsType } from "antd/es/table"; import type { ColumnsType } from "antd/es/table";
import { SearchControls } from "@/components/SearchControls"; import { SearchControls } from "@/components/SearchControls";
import type { CollectionLog } from "@/pages/DataCollection/collection.model"; import type { CollectionLog } from "@/pages/DataCollection/collection.model";
import { queryExecutionLogUsingPost } from "../../collection.apis"; import { queryExecutionLogUsingPost } from "../collection.apis";
import { LogStatusMap, LogTriggerTypeMap } from "../../collection.const"; import { LogStatusMap, LogTriggerTypeMap } from "../collection.const";
import useFetchData from "@/hooks/useFetchData"; import useFetchData from "@/hooks/useFetchData";
const filterOptions = [ const filterOptions = [

View File

@@ -1,16 +1,34 @@
import { Card, Button, Badge, Table, Dropdown, App } from "antd"; import {
import { EllipsisOutlined } from "@ant-design/icons"; Card,
Button,
Badge,
Table,
Dropdown,
App,
Tooltip,
Popconfirm,
} from "antd";
import {
DeleteOutlined,
EditOutlined,
EllipsisOutlined,
PauseCircleOutlined,
PauseOutlined,
PlayCircleOutlined,
StopOutlined,
} from "@ant-design/icons";
import { SearchControls } from "@/components/SearchControls"; import { SearchControls } from "@/components/SearchControls";
import { import {
deleteTaskByIdUsingDelete, deleteTaskByIdUsingDelete,
executeTaskByIdUsingPost, executeTaskByIdUsingPost,
queryTasksUsingGet, queryTasksUsingGet,
stopTaskByIdUsingPost, stopTaskByIdUsingPost,
} from "../../collection.apis"; } from "../collection.apis";
import { TaskStatus, type CollectionTask } from "../../collection.model"; import { TaskStatus, type CollectionTask } from "../collection.model";
import { StatusMap, SyncModeMap } from "../../collection.const"; import { StatusMap, SyncModeMap } from "../collection.const";
import useFetchData from "@/hooks/useFetchData"; import useFetchData from "@/hooks/useFetchData";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import { mapCollectionTask } from "../collection.const";
export default function TaskManagement() { export default function TaskManagement() {
const { message } = App.useApp(); const { message } = App.useApp();
@@ -34,7 +52,7 @@ export default function TaskManagement() {
setSearchParams, setSearchParams,
fetchData, fetchData,
handleFiltersChange, handleFiltersChange,
} = useFetchData(queryTasksUsingGet); } = useFetchData(queryTasksUsingGet, mapCollectionTask);
const handleStartTask = async (taskId: string) => { const handleStartTask = async (taskId: string) => {
await executeTaskByIdUsingPost(taskId); await executeTaskByIdUsingPost(taskId);
@@ -54,35 +72,61 @@ export default function TaskManagement() {
fetchData(); fetchData();
}; };
const taskOperations = (record: CollectionTask) => {
const isStopped = record.status === TaskStatus.STOPPED;
const startButton = {
key: "start",
label: "启动",
icon: <PlayCircleOutlined />,
onClick: () => handleStartTask(record.id),
};
const stopButton = {
key: "stop",
label: "停止",
icon: <PauseCircleOutlined />,
onClick: () => handleStopTask(record.id),
};
const items = [
isStopped ? startButton : stopButton,
{
key: "edit",
label: "编辑",
icon: <EditOutlined />,
onClick: () => {
showEditTaskModal(record);
},
},
{
key: "delete",
label: "删除",
danger: true,
icon: <DeleteOutlined />,
confirm: {
title: "确定要删除该任务吗?此操作不可撤销。",
okText: "删除",
cancelText: "取消",
okType: "danger",
},
onClick: () => handleDeleteTask(record.id),
},
];
return items;
};
const columns = [ const columns = [
{ {
title: "任务名称", title: "任务名称",
dataIndex: "name", dataIndex: "name",
key: "name", key: "name",
fixed: "left", fixed: "left",
render: (text: string, record: CollectionTask) => (
<Button
type="link"
onClick={() => navigate("`/data-collection/tasks/${record.id}`)}>")}
>
{text}
</Button>
),
}, },
{ {
title: "状态", title: "状态",
dataIndex: "status", dataIndex: "status",
key: "status", key: "status",
render: (status: string) => render: (status: string) => (
StatusMap[status] ? ( <Badge text={status.label} color={status.color} />
<Badge ),
color={StatusMap[status].color}
text={StatusMap[status].label}
/>
) : (
<Badge text={status} />
),
}, },
{ {
title: "同步方式", title: "同步方式",
@@ -115,47 +159,42 @@ export default function TaskManagement() {
title: "操作", title: "操作",
key: "action", key: "action",
fixed: "right" as const, fixed: "right" as const,
render: (_: any, record: Task) => ( render: (_: any, record: CollectionTask) => {
<Dropdown return taskOperations(record).map((op) => {
menu={{ const button = (
items: [ <Tooltip key={op.key} title={op.label}>
record.status === TaskStatus.STOPPED <Button
? { type="text"
key: "start", icon={op.icon}
label: "启动", danger={op?.danger}
onClick: () => handleStartTask(record.id), onClick={() => op.onClick(record)}
} />
: { </Tooltip>
key: "stop", );
label: "停止", if (op.confirm) {
onClick: () => handleStopTask(record.id), return (
}, <Popconfirm
{ key={op.key}
key: "edit", title={op.confirm.title}
label: "编辑", okText={op.confirm.okText}
onClick: () => handleViewDetail(record), cancelText={op.confirm.cancelText}
}, okType={op.danger ? "danger" : "primary"}
{ onConfirm={() => op.onClick(record)}
key: "delete", >
label: "删除", <Tooltip key={op.key} title={op.label}>
danger: true, <Button type="text" icon={op.icon} danger={op?.danger} />
onClick: () => handleDeleteTask(record.id), </Tooltip>
}, </Popconfirm>
], );
}} }
trigger={["click"]} return button;
> });
<Button },
type="text"
icon={<EllipsisOutlined style={{ fontSize: 20 }} />}
/>
</Dropdown>
),
}, },
]; ];
return ( return (
<div> <div className="space-y-4">
{/* Header Actions */} {/* Header Actions */}
<SearchControls <SearchControls
searchTerm={searchParams.keyword} searchTerm={searchParams.keyword}
@@ -176,7 +215,6 @@ export default function TaskManagement() {
filters: {}, filters: {},
})) }))
} }
className="mb-4"
/> />
{/* Tasks Table */} {/* Tasks Table */}
@@ -192,7 +230,7 @@ export default function TaskManagement() {
pageSize: searchParams.pageSize, pageSize: searchParams.pageSize,
total: pagination.total, total: pagination.total,
}} }}
scroll={{ x: "max-content" }} scroll={{ x: "max-content", y: "calc(100vh - 25rem)" }}
/> />
</Card> </Card>
</div> </div>

View File

@@ -1,4 +1,9 @@
import { LogStatus, SyncMode, TaskStatus, TriggerType } from "./collection.model"; import {
LogStatus,
SyncMode,
TaskStatus,
TriggerType,
} from "./collection.model";
export const StatusMap: Record< export const StatusMap: Record<
TaskStatus, TaskStatus,
@@ -67,3 +72,10 @@ export const LogTriggerTypeMap: Record<
[TriggerType.SCHEDULED]: { label: "定时", value: TriggerType.SCHEDULED }, [TriggerType.SCHEDULED]: { label: "定时", value: TriggerType.SCHEDULED },
[TriggerType.API]: { label: "API", value: TriggerType.API }, [TriggerType.API]: { label: "API", value: TriggerType.API },
}; };
export function mapCollectionTask(task: CollectionTask): CollectionTask {
return {
...task,
status: StatusMap[task.status],
};
}

View File

@@ -23,6 +23,7 @@ export default function BasicInformation({
// 获取标签 // 获取标签
const fetchTags = async () => { const fetchTags = async () => {
if (hidden.includes("tags")) return;
try { try {
const { data } = await queryDatasetTagsUsingGet(); const { data } = await queryDatasetTagsUsingGet();
const customTags = data.map((tag) => ({ const customTags = data.map((tag) => ({
@@ -47,9 +48,11 @@ export default function BasicInformation({
> >
<Input placeholder="输入数据集名称" /> <Input placeholder="输入数据集名称" />
</Form.Item> </Form.Item>
<Form.Item name="description" label="描述"> {!hidden.includes("description") && (
<Input.TextArea placeholder="描述数据集的用途和内容" rows={3} /> <Form.Item name="description" label="描述">
</Form.Item> <Input.TextArea placeholder="描述数据集的用途和内容" rows={3} />
</Form.Item>
)}
{/* 数据集类型选择 - 使用卡片形式 */} {/* 数据集类型选择 - 使用卡片形式 */}
{!hidden.includes("datasetType") && ( {!hidden.includes("datasetType") && (
@@ -65,14 +68,16 @@ export default function BasicInformation({
/> />
</Form.Item> </Form.Item>
)} )}
<Form.Item name="tags" label="标签"> {!hidden.includes("tags") && (
<Select <Form.Item label="标签" name="tags">
className="w-full" <Select
mode="tags" className="w-full"
options={tagOptions} mode="tags"
placeholder="请选择标签" options={tagOptions}
/> placeholder="请选择标签"
</Form.Item> />
</Form.Item>
)}
</> </>
); );
} }

View File

@@ -152,40 +152,6 @@ export default function ImportConfiguration({
</Form.Item> </Form.Item>
)} )}
{/* nas import */}
{importConfig?.source === DataSource.NAS && (
<div className="grid grid-cols-2 gap-3 p-4 bg-blue-50 rounded-lg">
<Form.Item
name="nasPath"
rules={[{ required: true }]}
label="NAS地址"
>
<Input placeholder="192.168.1.100" />
</Form.Item>
<Form.Item
name="sharePath"
rules={[{ required: true }]}
label="共享路径"
>
<Input placeholder="/share/importConfig" />
</Form.Item>
<Form.Item
name="username"
rules={[{ required: true }]}
label="用户名"
>
<Input placeholder="用户名" />
</Form.Item>
<Form.Item
name="password"
rules={[{ required: true }]}
label="密码"
>
<Input type="password" placeholder="密码" />
</Form.Item>
</div>
)}
{/* obs import */} {/* 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">