Revert "feat: fix the problem in the Operator Market frontend pages"

This commit is contained in:
Kecheng Sha
2025-12-29 12:00:37 +08:00
committed by GitHub
parent 8f30f71a68
commit 0df7a872e4
213 changed files with 45537 additions and 45547 deletions

View File

@@ -1,470 +1,470 @@
import { useState } from "react";
import { Input, Button, Radio, Form, App, Select } from "antd";
import { Link, useNavigate } from "react-router";
import { ArrowLeft } from "lucide-react";
import { createTaskUsingPost } from "../collection.apis";
import SimpleCronScheduler from "@/pages/DataCollection/Create/SimpleCronScheduler";
import RadioCard from "@/components/RadioCard";
import { datasetTypes } from "@/pages/DataManagement/dataset.const";
import { SyncModeMap } from "../collection.const";
import { SyncMode } from "../collection.model";
import { DatasetSubType } from "@/pages/DataManagement/dataset.model";
const { TextArea } = Input;
const defaultTemplates = [
{
id: "NAS",
name: "NAS到本地",
description: "从NAS文件系统导入数据到本地文件系统",
config: {
reader: "nfsreader",
writer: "localwriter",
},
},
{
id: "OBS",
name: "OBS到本地",
description: "从OBS文件系统导入数据到本地文件系统",
config: {
reader: "obsreader",
writer: "localwriter",
},
},
{
id: "MYSQL",
name: "Mysql到本地",
description: "从Mysql中导入数据到本地文件系统",
config: {
reader: "mysqlreader",
writer: "localwriter",
},
},
];
const syncModeOptions = Object.values(SyncModeMap);
enum TemplateType {
NAS = "NAS",
OBS = "OBS",
MYSQL = "MYSQL",
}
export default function CollectionTaskCreate() {
const navigate = useNavigate();
const [form] = Form.useForm();
const { message } = App.useApp();
const [templateType, setTemplateType] = useState<"default" | "custom">(
"default"
);
// 默认模板类型设为 NAS
const [selectedTemplate, setSelectedTemplate] = useState<TemplateType>(
TemplateType.NAS
);
const [customConfig, setCustomConfig] = useState("");
// 将 newTask 设为 any,并初始化 config.templateType 为 NAS
const [newTask, setNewTask] = useState<any>({
name: "",
description: "",
syncMode: SyncMode.ONCE,
cronExpression: "",
maxRetries: 10,
dataset: null,
config: { templateType: TemplateType.NAS },
createDataset: false,
});
const [scheduleExpression, setScheduleExpression] = useState({
type: "once",
time: "00:00",
cronExpression: "0 0 0 * * ?",
});
const [isCreateDataset, setIsCreateDataset] = useState(false);
const handleSubmit = async () => {
try {
await form.validateFields();
if (templateType === "default" && !selectedTemplate) {
window.alert("请选择默认模板");
return;
}
if (templateType === "custom" && !customConfig.trim()) {
window.alert("请填写自定义配置");
return;
}
// 构建最终 payload,不依赖异步 setState
const payload = {
...newTask,
taskType:
templateType === "default" ? selectedTemplate : "CUSTOM",
config: {
...((newTask && newTask.config) || {}),
...(templateType === "custom" ? { dataxJson: customConfig } : {}),
},
};
console.log("创建任务 payload:", payload);
await createTaskUsingPost(payload);
message.success("任务创建成功");
navigate("/data/collection");
} catch (error) {
message.error(`${error?.data?.message}${error?.data?.data}`);
}
};
return (
<div className="h-full flex flex-col">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center">
<Link to="/data/collection">
<Button type="text">
<ArrowLeft className="w-4 h-4 mr-1" />
</Button>
</Link>
<h1 className="text-xl font-bold bg-clip-text"></h1>
</div>
</div>
<div className="flex-overflow-auto border-card">
<div className="flex-1 overflow-auto p-6">
<Form
form={form}
layout="vertical"
initialValues={newTask}
onValuesChange={(_, allValues) => {
setNewTask({ ...newTask, ...allValues });
}}
>
{/* 基本信息 */}
<h2 className="font-medium text-gray-900 text-lg mb-2"></h2>
<Form.Item
label="名称"
name="name"
rules={[{ required: true, message: "请输入任务名称" }]}
>
<Input placeholder="请输入任务名称" />
</Form.Item>
<Form.Item label="描述" name="description">
<TextArea placeholder="请输入任务描述" rows={3} />
</Form.Item>
{/* 同步配置 */}
<h2 className="font-medium text-gray-900 pt-6 mb-2 text-lg">
</h2>
<Form.Item name="syncMode" label="同步方式">
<Radio.Group
value={newTask.syncMode}
options={syncModeOptions}
onChange={(e) => {
const value = e.target.value;
setNewTask({
...newTask,
syncMode: value,
scheduleExpression:
value === SyncMode.SCHEDULED
? scheduleExpression.cronExpression
: "",
});
}}
></Radio.Group>
</Form.Item>
{newTask.syncMode === SyncMode.SCHEDULED && (
<Form.Item
label=""
rules={[{ required: true, message: "请输入Cron表达式" }]}
>
<SimpleCronScheduler
className="px-2 rounded"
value={scheduleExpression}
onChange={(value) => {
setScheduleExpression(value);
setNewTask({
...newTask,
scheduleExpression: value.cronExpression,
});
}}
/>
</Form.Item>
)}
{/* 模板配置 */}
<h2 className="font-medium text-gray-900 pt-6 mb-2 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 as TemplateType);
// 使用函数式更新,合并之前的 config
setNewTask((prev: any) => ({
...prev,
config: {
templateType: template.id,
},
}));
// 同步表单显示
form.setFieldsValue({
config: { templateType: 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 bg-blue-50 rounded">
<Form.Item
name={["config", "ip"]}
rules={[{ required: true, message: "请输入NAS地址" }]}
label="NAS地址"
>
<Input placeholder="192.168.1.100" />
</Form.Item>
<Form.Item
name={["config", "path"]}
rules={[{ required: true, message: "请输入共享路径" }]}
label="共享路径"
>
<Input placeholder="/share/importConfig" />
</Form.Item>
<Form.Item
name={["config", "files"]}
label="文件列表"
className="col-span-2"
>
<Select placeholder="请选择文件列表" mode="tags" />
</Form.Item>
</div>
)}
{/* obs import */}
{selectedTemplate === TemplateType.OBS && (
<div className="grid grid-cols-2 gap-3 p-4 bg-blue-50 rounded-lg">
<Form.Item
name={["config", "endpoint"]}
rules={[{ required: true }]}
label="Endpoint"
>
<Input
className="h-8 text-xs"
placeholder="obs.cn-north-4.myhuaweicloud.com"
/>
</Form.Item>
<Form.Item
name={["config", "bucket"]}
rules={[{ required: true }]}
label="Bucket"
>
<Input className="h-8 text-xs" placeholder="my-bucket" />
</Form.Item>
<Form.Item
name={["config", "accessKey"]}
rules={[{ required: true }]}
label="Access Key"
>
<Input className="h-8 text-xs" placeholder="Access Key" />
</Form.Item>
<Form.Item
name={["config", "secretKey"]}
rules={[{ required: true }]}
label="Secret Key"
>
<Input
type="password"
className="h-8 text-xs"
placeholder="Secret Key"
/>
</Form.Item>
<Form.Item
name={["config", "prefix"]}
rules={[{ required: true }]}
label="Prefix"
>
<Input className="h-8 text-xs" placeholder="Prefix" />
</Form.Item>
</div>
)}
{/* mysql import */}
{selectedTemplate === TemplateType.MYSQL && (
<div className="grid grid-cols-2 gap-3 px-2 bg-blue-50 rounded">
<Form.Item
name={["config", "jdbcUrl"]}
rules={[{ required: true, message: "请输入数据库链接" }]}
label="数据库链接"
className="col-span-2"
>
<Input placeholder="jdbc:mysql://localhost:3306/mysql?useUnicode=true&characterEncoding=utf8" />
</Form.Item>
<Form.Item
name={["config", "username"]}
rules={[{ required: true, message: "请输入用户名" }]}
label="用户名"
>
<Input placeholder="mysql" />
</Form.Item>
<Form.Item
name={["config", "password"]}
rules={[{ required: true, message: "请输入密码" }]}
label="密码"
>
<Input type="password" className="h-8 text-xs" placeholder="Secret Key" />
</Form.Item>
<Form.Item
name={["config", "querySql"]}
rules={[{ required: true, message: "请输入查询语句" }]}
label="查询语句"
>
<Input placeholder="select * from your_table" />
</Form.Item>
<Form.Item
name={["config", "headers"]}
label="列名"
className="col-span-2"
>
<Select placeholder="请输入列名" mode="tags" />
</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: "请选择是否创建数据集" }]}
tooltip={"支持后续在【数据管理】中手动创建数据集并关联至此任务。"}
>
<Radio.Group
value={isCreateDataset}
onChange={(e) => {
const value = e.target.value;
let datasetInit = null;
if (value === true) {
datasetInit = {};
}
form.setFieldsValue({
dataset: datasetInit,
});
setNewTask((prev: any) => ({
...prev,
dataset: datasetInit,
}));
setIsCreateDataset(e.target.value);
}}
>
<Radio value={true}></Radio>
<Radio value={false}></Radio>
</Radio.Group>
</Form.Item>
{isCreateDataset && (
<>
<Form.Item
label="数据集名称"
name={["dataset", "name"]}
required
>
<Input
placeholder="输入数据集名称"
onChange={(e) => {
setNewTask((prev: any) => ({
...prev,
dataset: {
...(prev.dataset || {}),
name: e.target.value,
},
}));
}}
/>
</Form.Item>
<Form.Item
label="数据集类型"
name={["dataset", "datasetType"]}
rules={[{ required: true, message: "请选择数据集类型" }]}
>
<RadioCard
options={datasetTypes}
value={newTask.dataset?.datasetType}
onChange={(type) => {
form.setFieldValue(["dataset", "datasetType"], type);
setNewTask((prev: any) => ({
...prev,
dataset: {
...(prev.dataset || {}),
datasetType: type as DatasetSubType,
},
}));
}}
/>
</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>
);
}
import { useState } from "react";
import { Input, Button, Radio, Form, App, Select } from "antd";
import { Link, useNavigate } from "react-router";
import { ArrowLeft } from "lucide-react";
import { createTaskUsingPost } from "../collection.apis";
import SimpleCronScheduler from "@/pages/DataCollection/Create/SimpleCronScheduler";
import RadioCard from "@/components/RadioCard";
import { datasetTypes } from "@/pages/DataManagement/dataset.const";
import { SyncModeMap } from "../collection.const";
import { SyncMode } from "../collection.model";
import { DatasetSubType } from "@/pages/DataManagement/dataset.model";
const { TextArea } = Input;
const defaultTemplates = [
{
id: "NAS",
name: "NAS到本地",
description: "从NAS文件系统导入数据到本地文件系统",
config: {
reader: "nfsreader",
writer: "localwriter",
},
},
{
id: "OBS",
name: "OBS到本地",
description: "从OBS文件系统导入数据到本地文件系统",
config: {
reader: "obsreader",
writer: "localwriter",
},
},
{
id: "MYSQL",
name: "Mysql到本地",
description: "从Mysql中导入数据到本地文件系统",
config: {
reader: "mysqlreader",
writer: "localwriter",
},
},
];
const syncModeOptions = Object.values(SyncModeMap);
enum TemplateType {
NAS = "NAS",
OBS = "OBS",
MYSQL = "MYSQL",
}
export default function CollectionTaskCreate() {
const navigate = useNavigate();
const [form] = Form.useForm();
const { message } = App.useApp();
const [templateType, setTemplateType] = useState<"default" | "custom">(
"default"
);
// 默认模板类型设为 NAS
const [selectedTemplate, setSelectedTemplate] = useState<TemplateType>(
TemplateType.NAS
);
const [customConfig, setCustomConfig] = useState("");
// 将 newTask 设为 any,并初始化 config.templateType 为 NAS
const [newTask, setNewTask] = useState<any>({
name: "",
description: "",
syncMode: SyncMode.ONCE,
cronExpression: "",
maxRetries: 10,
dataset: null,
config: { templateType: TemplateType.NAS },
createDataset: false,
});
const [scheduleExpression, setScheduleExpression] = useState({
type: "once",
time: "00:00",
cronExpression: "0 0 0 * * ?",
});
const [isCreateDataset, setIsCreateDataset] = useState(false);
const handleSubmit = async () => {
try {
await form.validateFields();
if (templateType === "default" && !selectedTemplate) {
window.alert("请选择默认模板");
return;
}
if (templateType === "custom" && !customConfig.trim()) {
window.alert("请填写自定义配置");
return;
}
// 构建最终 payload,不依赖异步 setState
const payload = {
...newTask,
taskType:
templateType === "default" ? selectedTemplate : "CUSTOM",
config: {
...((newTask && newTask.config) || {}),
...(templateType === "custom" ? { dataxJson: customConfig } : {}),
},
};
console.log("创建任务 payload:", payload);
await createTaskUsingPost(payload);
message.success("任务创建成功");
navigate("/data/collection");
} catch (error) {
message.error(`${error?.data?.message}${error?.data?.data}`);
}
};
return (
<div className="h-full flex flex-col">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center">
<Link to="/data/collection">
<Button type="text">
<ArrowLeft className="w-4 h-4 mr-1" />
</Button>
</Link>
<h1 className="text-xl font-bold bg-clip-text"></h1>
</div>
</div>
<div className="flex-overflow-auto border-card">
<div className="flex-1 overflow-auto p-6">
<Form
form={form}
layout="vertical"
initialValues={newTask}
onValuesChange={(_, allValues) => {
setNewTask({ ...newTask, ...allValues });
}}
>
{/* 基本信息 */}
<h2 className="font-medium text-gray-900 text-lg mb-2"></h2>
<Form.Item
label="名称"
name="name"
rules={[{ required: true, message: "请输入任务名称" }]}
>
<Input placeholder="请输入任务名称" />
</Form.Item>
<Form.Item label="描述" name="description">
<TextArea placeholder="请输入任务描述" rows={3} />
</Form.Item>
{/* 同步配置 */}
<h2 className="font-medium text-gray-900 pt-6 mb-2 text-lg">
</h2>
<Form.Item name="syncMode" label="同步方式">
<Radio.Group
value={newTask.syncMode}
options={syncModeOptions}
onChange={(e) => {
const value = e.target.value;
setNewTask({
...newTask,
syncMode: value,
scheduleExpression:
value === SyncMode.SCHEDULED
? scheduleExpression.cronExpression
: "",
});
}}
></Radio.Group>
</Form.Item>
{newTask.syncMode === SyncMode.SCHEDULED && (
<Form.Item
label=""
rules={[{ required: true, message: "请输入Cron表达式" }]}
>
<SimpleCronScheduler
className="px-2 rounded"
value={scheduleExpression}
onChange={(value) => {
setScheduleExpression(value);
setNewTask({
...newTask,
scheduleExpression: value.cronExpression,
});
}}
/>
</Form.Item>
)}
{/* 模板配置 */}
<h2 className="font-medium text-gray-900 pt-6 mb-2 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 as TemplateType);
// 使用函数式更新,合并之前的 config
setNewTask((prev: any) => ({
...prev,
config: {
templateType: template.id,
},
}));
// 同步表单显示
form.setFieldsValue({
config: { templateType: 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 bg-blue-50 rounded">
<Form.Item
name={["config", "ip"]}
rules={[{ required: true, message: "请输入NAS地址" }]}
label="NAS地址"
>
<Input placeholder="192.168.1.100" />
</Form.Item>
<Form.Item
name={["config", "path"]}
rules={[{ required: true, message: "请输入共享路径" }]}
label="共享路径"
>
<Input placeholder="/share/importConfig" />
</Form.Item>
<Form.Item
name={["config", "files"]}
label="文件列表"
className="col-span-2"
>
<Select placeholder="请选择文件列表" mode="tags" />
</Form.Item>
</div>
)}
{/* obs import */}
{selectedTemplate === TemplateType.OBS && (
<div className="grid grid-cols-2 gap-3 p-4 bg-blue-50 rounded-lg">
<Form.Item
name={["config", "endpoint"]}
rules={[{ required: true }]}
label="Endpoint"
>
<Input
className="h-8 text-xs"
placeholder="obs.cn-north-4.myhuaweicloud.com"
/>
</Form.Item>
<Form.Item
name={["config", "bucket"]}
rules={[{ required: true }]}
label="Bucket"
>
<Input className="h-8 text-xs" placeholder="my-bucket" />
</Form.Item>
<Form.Item
name={["config", "accessKey"]}
rules={[{ required: true }]}
label="Access Key"
>
<Input className="h-8 text-xs" placeholder="Access Key" />
</Form.Item>
<Form.Item
name={["config", "secretKey"]}
rules={[{ required: true }]}
label="Secret Key"
>
<Input
type="password"
className="h-8 text-xs"
placeholder="Secret Key"
/>
</Form.Item>
<Form.Item
name={["config", "prefix"]}
rules={[{ required: true }]}
label="Prefix"
>
<Input className="h-8 text-xs" placeholder="Prefix" />
</Form.Item>
</div>
)}
{/* mysql import */}
{selectedTemplate === TemplateType.MYSQL && (
<div className="grid grid-cols-2 gap-3 px-2 bg-blue-50 rounded">
<Form.Item
name={["config", "jdbcUrl"]}
rules={[{ required: true, message: "请输入数据库链接" }]}
label="数据库链接"
className="col-span-2"
>
<Input placeholder="jdbc:mysql://localhost:3306/mysql?useUnicode=true&characterEncoding=utf8" />
</Form.Item>
<Form.Item
name={["config", "username"]}
rules={[{ required: true, message: "请输入用户名" }]}
label="用户名"
>
<Input placeholder="mysql" />
</Form.Item>
<Form.Item
name={["config", "password"]}
rules={[{ required: true, message: "请输入密码" }]}
label="密码"
>
<Input type="password" className="h-8 text-xs" placeholder="Secret Key" />
</Form.Item>
<Form.Item
name={["config", "querySql"]}
rules={[{ required: true, message: "请输入查询语句" }]}
label="查询语句"
>
<Input placeholder="select * from your_table" />
</Form.Item>
<Form.Item
name={["config", "headers"]}
label="列名"
className="col-span-2"
>
<Select placeholder="请输入列名" mode="tags" />
</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: "请选择是否创建数据集" }]}
tooltip={"支持后续在【数据管理】中手动创建数据集并关联至此任务。"}
>
<Radio.Group
value={isCreateDataset}
onChange={(e) => {
const value = e.target.value;
let datasetInit = null;
if (value === true) {
datasetInit = {};
}
form.setFieldsValue({
dataset: datasetInit,
});
setNewTask((prev: any) => ({
...prev,
dataset: datasetInit,
}));
setIsCreateDataset(e.target.value);
}}
>
<Radio value={true}></Radio>
<Radio value={false}></Radio>
</Radio.Group>
</Form.Item>
{isCreateDataset && (
<>
<Form.Item
label="数据集名称"
name={["dataset", "name"]}
required
>
<Input
placeholder="输入数据集名称"
onChange={(e) => {
setNewTask((prev: any) => ({
...prev,
dataset: {
...(prev.dataset || {}),
name: e.target.value,
},
}));
}}
/>
</Form.Item>
<Form.Item
label="数据集类型"
name={["dataset", "datasetType"]}
rules={[{ required: true, message: "请选择数据集类型" }]}
>
<RadioCard
options={datasetTypes}
value={newTask.dataset?.datasetType}
onChange={(type) => {
form.setFieldValue(["dataset", "datasetType"], type);
setNewTask((prev: any) => ({
...prev,
dataset: {
...(prev.dataset || {}),
datasetType: type as DatasetSubType,
},
}));
}}
/>
</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>
);
}

View File

@@ -1,209 +1,209 @@
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 || ""}`}>
{/* 执行周期选择 */}
<div className="grid grid-cols-2 gap-4">
<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>
{/* 周几选择 */}
{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>
)}
</div>
{/* 时间选择 */}
<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>
{/* 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;
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 || ""}`}>
{/* 执行周期选择 */}
<div className="grid grid-cols-2 gap-4">
<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>
{/* 周几选择 */}
{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>
)}
</div>
{/* 时间选择 */}
<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>
{/* 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,41 +1,41 @@
import { useState } from "react";
import { Button, Tabs } from "antd";
import { PlusOutlined } from "@ant-design/icons";
import TaskManagement from "./TaskManagement";
import ExecutionLog from "./ExecutionLog";
import { useNavigate } from "react-router";
export default function DataCollection() {
const navigate = useNavigate();
const [activeTab, setActiveTab] = useState("task-management");
return (
<div className="gap-4 h-full flex flex-col">
<div className="flex justify-between items-end">
<div>
<h1 className="text-xl font-bold text-gray-900 mb-2"></h1>
</div>
<div>
<Button
type="primary"
onClick={() => navigate("/data/collection/create-task")}
icon={<PlusOutlined />}
>
</Button>
</div>
</div>
<Tabs
activeKey={activeTab}
items={[
{ label: "任务管理", key: "task-management" },
// { label: "执行日志", key: "execution-log" },
]}
onChange={(tab) => {
setActiveTab(tab);
}}
/>
{activeTab === "task-management" ? <TaskManagement /> : <ExecutionLog />}
</div>
);
}
import { useState } from "react";
import { Button, Tabs } from "antd";
import { PlusOutlined } from "@ant-design/icons";
import TaskManagement from "./TaskManagement";
import ExecutionLog from "./ExecutionLog";
import { useNavigate } from "react-router";
export default function DataCollection() {
const navigate = useNavigate();
const [activeTab, setActiveTab] = useState("task-management");
return (
<div className="gap-4 h-full flex flex-col">
<div className="flex justify-between items-end">
<div>
<h1 className="text-xl font-bold text-gray-900 mb-2"></h1>
</div>
<div>
<Button
type="primary"
onClick={() => navigate("/data/collection/create-task")}
icon={<PlusOutlined />}
>
</Button>
</div>
</div>
<Tabs
activeKey={activeTab}
items={[
{ label: "任务管理", key: "task-management" },
// { label: "执行日志", key: "execution-log" },
]}
onChange={(tab) => {
setActiveTab(tab);
}}
/>
{activeTab === "task-management" ? <TaskManagement /> : <ExecutionLog />}
</div>
);
}

View File

@@ -1,149 +1,149 @@
import { Card, Badge, Table } from "antd";
import type { ColumnsType } from "antd/es/table";
import { SearchControls } from "@/components/SearchControls";
import type { CollectionLog } from "@/pages/DataCollection/collection.model";
import { queryExecutionLogUsingPost } from "../collection.apis";
import { LogStatusMap, LogTriggerTypeMap } from "../collection.const";
import useFetchData from "@/hooks/useFetchData";
const filterOptions = [
{
key: "status",
label: "状态筛选",
options: Object.values(LogStatusMap),
},
{
key: "triggerType",
label: "触发类型",
options: Object.values(LogTriggerTypeMap),
},
];
export default function ExecutionLog() {
const handleReset = () => {
setSearchParams({
keyword: "",
filters: {},
current: 1,
pageSize: 10,
dateRange: null,
});
};
const {
loading,
tableData,
pagination,
searchParams,
setSearchParams,
handleFiltersChange,
handleKeywordChange,
} = useFetchData(queryExecutionLogUsingPost);
const columns: ColumnsType<CollectionLog> = [
{
title: "任务名称",
dataIndex: "taskName",
key: "taskName",
fixed: "left",
render: (text: string) => <span style={{ fontWeight: 500 }}>{text}</span>,
},
{
title: "状态",
dataIndex: "status",
key: "status",
render: (status: string) => (
<Badge
text={LogStatusMap[status]?.label}
color={LogStatusMap[status]?.color}
/>
),
},
{
title: "触发类型",
dataIndex: "triggerType",
key: "triggerType",
render: (type: string) => LogTriggerTypeMap[type].label,
},
{
title: "开始时间",
dataIndex: "startTime",
key: "startTime",
},
{
title: "结束时间",
dataIndex: "endTime",
key: "endTime",
},
{
title: "执行时长",
dataIndex: "duration",
key: "duration",
},
{
title: "重试次数",
dataIndex: "retryCount",
key: "retryCount",
},
{
title: "进程ID",
dataIndex: "processId",
key: "processId",
render: (text: string) => (
<span style={{ fontFamily: "monospace" }}>{text}</span>
),
},
{
title: "错误信息",
dataIndex: "errorMessage",
key: "errorMessage",
render: (msg?: string) =>
msg ? (
<span style={{ color: "#f5222d" }} title={msg}>
{msg}
</span>
) : (
<span style={{ color: "#bbb" }}>-</span>
),
},
];
return (
<div className="flex flex-col gap-4">
{/* Filter Controls */}
<div className="flex items-center justify-between gap-4">
<SearchControls
searchTerm={searchParams.keyword}
onSearchChange={handleKeywordChange}
filters={filterOptions}
onFiltersChange={handleFiltersChange}
showViewToggle={false}
onClearFilters={() =>
setSearchParams((prev) => ({
...prev,
filters: {},
}))
}
showDatePicker
dateRange={searchParams.dateRange || [null, null]}
onDateChange={(date) =>
setSearchParams((prev) => ({ ...prev, dateRange: date }))
}
onReload={handleReset}
searchPlaceholder="搜索任务名称、进程ID或错误信息..."
className="flex-1"
/>
</div>
<Card>
<Table
loading={loading}
columns={columns}
dataSource={tableData}
rowKey="id"
pagination={pagination}
scroll={{ x: "max-content" }}
/>
</Card>
</div>
);
}
import { Card, Badge, Table } from "antd";
import type { ColumnsType } from "antd/es/table";
import { SearchControls } from "@/components/SearchControls";
import type { CollectionLog } from "@/pages/DataCollection/collection.model";
import { queryExecutionLogUsingPost } from "../collection.apis";
import { LogStatusMap, LogTriggerTypeMap } from "../collection.const";
import useFetchData from "@/hooks/useFetchData";
const filterOptions = [
{
key: "status",
label: "状态筛选",
options: Object.values(LogStatusMap),
},
{
key: "triggerType",
label: "触发类型",
options: Object.values(LogTriggerTypeMap),
},
];
export default function ExecutionLog() {
const handleReset = () => {
setSearchParams({
keyword: "",
filters: {},
current: 1,
pageSize: 10,
dateRange: null,
});
};
const {
loading,
tableData,
pagination,
searchParams,
setSearchParams,
handleFiltersChange,
handleKeywordChange,
} = useFetchData(queryExecutionLogUsingPost);
const columns: ColumnsType<CollectionLog> = [
{
title: "任务名称",
dataIndex: "taskName",
key: "taskName",
fixed: "left",
render: (text: string) => <span style={{ fontWeight: 500 }}>{text}</span>,
},
{
title: "状态",
dataIndex: "status",
key: "status",
render: (status: string) => (
<Badge
text={LogStatusMap[status]?.label}
color={LogStatusMap[status]?.color}
/>
),
},
{
title: "触发类型",
dataIndex: "triggerType",
key: "triggerType",
render: (type: string) => LogTriggerTypeMap[type].label,
},
{
title: "开始时间",
dataIndex: "startTime",
key: "startTime",
},
{
title: "结束时间",
dataIndex: "endTime",
key: "endTime",
},
{
title: "执行时长",
dataIndex: "duration",
key: "duration",
},
{
title: "重试次数",
dataIndex: "retryCount",
key: "retryCount",
},
{
title: "进程ID",
dataIndex: "processId",
key: "processId",
render: (text: string) => (
<span style={{ fontFamily: "monospace" }}>{text}</span>
),
},
{
title: "错误信息",
dataIndex: "errorMessage",
key: "errorMessage",
render: (msg?: string) =>
msg ? (
<span style={{ color: "#f5222d" }} title={msg}>
{msg}
</span>
) : (
<span style={{ color: "#bbb" }}>-</span>
),
},
];
return (
<div className="flex flex-col gap-4">
{/* Filter Controls */}
<div className="flex items-center justify-between gap-4">
<SearchControls
searchTerm={searchParams.keyword}
onSearchChange={handleKeywordChange}
filters={filterOptions}
onFiltersChange={handleFiltersChange}
showViewToggle={false}
onClearFilters={() =>
setSearchParams((prev) => ({
...prev,
filters: {},
}))
}
showDatePicker
dateRange={searchParams.dateRange || [null, null]}
onDateChange={(date) =>
setSearchParams((prev) => ({ ...prev, dateRange: date }))
}
onReload={handleReset}
searchPlaceholder="搜索任务名称、进程ID或错误信息..."
className="flex-1"
/>
</div>
<Card>
<Table
loading={loading}
columns={columns}
dataSource={tableData}
rowKey="id"
pagination={pagination}
scroll={{ x: "max-content" }}
/>
</Card>
</div>
);
}

View File

@@ -1,252 +1,252 @@
import {
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 {
deleteTaskByIdUsingDelete,
executeTaskByIdUsingPost,
queryTasksUsingGet,
stopTaskByIdUsingPost,
} from "../collection.apis";
import { TaskStatus, type CollectionTask } from "../collection.model";
import { StatusMap, SyncModeMap } from "../collection.const";
import useFetchData from "@/hooks/useFetchData";
import { useNavigate } from "react-router";
import { mapCollectionTask } from "../collection.const";
export default function TaskManagement() {
const { message } = App.useApp();
const navigate = useNavigate();
const filters = [
{
key: "status",
label: "状态筛选",
options: [
{ value: "all", label: "全部状态" },
...Object.values(StatusMap),
],
},
];
const {
loading,
tableData,
pagination,
searchParams,
setSearchParams,
fetchData,
handleFiltersChange,
} = useFetchData(queryTasksUsingGet, mapCollectionTask);
const handleStartTask = async (taskId: string) => {
await executeTaskByIdUsingPost(taskId);
message.success("任务启动请求已发送");
fetchData();
};
const handleStopTask = async (taskId: string) => {
await stopTaskByIdUsingPost(taskId);
message.success("任务停止请求已发送");
fetchData();
};
const handleDeleteTask = async (taskId: string) => {
await deleteTaskByIdUsingDelete(taskId);
message.success("任务已删除");
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 = [
{
title: "任务名称",
dataIndex: "name",
key: "name",
fixed: "left",
width: 150,
ellipsis: true,
},
{
title: "状态",
dataIndex: "status",
key: "status",
width: 150,
ellipsis: true,
render: (status: string) => (
<Badge text={status.label} color={status.color} />
),
},
{
title: "同步方式",
dataIndex: "syncMode",
key: "syncMode",
width: 150,
ellipsis: true,
render: (text: string) => <span>{SyncModeMap[text]?.label}</span>,
},
{
title: "创建时间",
dataIndex: "createdAt",
key: "createdAt",
width: 150,
ellipsis: true,
},
{
title: "更新时间",
dataIndex: "updatedAt",
key: "updatedAt",
width: 150,
ellipsis: true,
},
{
title: "最近执行ID",
dataIndex: "lastExecutionId",
key: "lastExecutionId",
width: 150,
ellipsis: true,
},
{
title: "描述",
dataIndex: "description",
key: "description",
ellipsis: true,
width: 200,
},
{
title: "操作",
key: "action",
fixed: "right" as const,
render: (_: any, record: CollectionTask) => {
return taskOperations(record).map((op) => {
const button = (
<Tooltip key={op.key} title={op.label}>
<Button
type="text"
icon={op.icon}
danger={op?.danger}
onClick={() => op.onClick(record)}
/>
</Tooltip>
);
if (op.confirm) {
return (
<Popconfirm
key={op.key}
title={op.confirm.title}
okText={op.confirm.okText}
cancelText={op.confirm.cancelText}
okType={op.danger ? "danger" : "primary"}
onConfirm={() => op.onClick(record)}
>
<Tooltip key={op.key} title={op.label}>
<Button type="text" icon={op.icon} danger={op?.danger} />
</Tooltip>
</Popconfirm>
);
}
return button;
});
},
},
];
return (
<div className="space-y-4">
{/* Header Actions */}
<SearchControls
searchTerm={searchParams.keyword}
onSearchChange={(newSearchTerm) =>
setSearchParams((prev) => ({
...prev,
keyword: newSearchTerm,
current: 1,
}))
}
searchPlaceholder="搜索任务名称或描述..."
filters={filters}
onFiltersChange={handleFiltersChange}
showViewToggle={false}
onClearFilters={() =>
setSearchParams((prev) => ({
...prev,
filters: {},
}))
}
onReload={fetchData}
/>
{/* Tasks Table */}
<Card>
<Table
columns={columns}
dataSource={tableData}
loading={loading}
rowKey="id"
pagination={{
...pagination,
current: searchParams.current,
pageSize: searchParams.pageSize,
total: pagination.total,
}}
scroll={{ x: "max-content", y: "calc(100vh - 25rem)" }}
/>
</Card>
</div>
);
}
import {
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 {
deleteTaskByIdUsingDelete,
executeTaskByIdUsingPost,
queryTasksUsingGet,
stopTaskByIdUsingPost,
} from "../collection.apis";
import { TaskStatus, type CollectionTask } from "../collection.model";
import { StatusMap, SyncModeMap } from "../collection.const";
import useFetchData from "@/hooks/useFetchData";
import { useNavigate } from "react-router";
import { mapCollectionTask } from "../collection.const";
export default function TaskManagement() {
const { message } = App.useApp();
const navigate = useNavigate();
const filters = [
{
key: "status",
label: "状态筛选",
options: [
{ value: "all", label: "全部状态" },
...Object.values(StatusMap),
],
},
];
const {
loading,
tableData,
pagination,
searchParams,
setSearchParams,
fetchData,
handleFiltersChange,
} = useFetchData(queryTasksUsingGet, mapCollectionTask);
const handleStartTask = async (taskId: string) => {
await executeTaskByIdUsingPost(taskId);
message.success("任务启动请求已发送");
fetchData();
};
const handleStopTask = async (taskId: string) => {
await stopTaskByIdUsingPost(taskId);
message.success("任务停止请求已发送");
fetchData();
};
const handleDeleteTask = async (taskId: string) => {
await deleteTaskByIdUsingDelete(taskId);
message.success("任务已删除");
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 = [
{
title: "任务名称",
dataIndex: "name",
key: "name",
fixed: "left",
width: 150,
ellipsis: true,
},
{
title: "状态",
dataIndex: "status",
key: "status",
width: 150,
ellipsis: true,
render: (status: string) => (
<Badge text={status.label} color={status.color} />
),
},
{
title: "同步方式",
dataIndex: "syncMode",
key: "syncMode",
width: 150,
ellipsis: true,
render: (text: string) => <span>{SyncModeMap[text]?.label}</span>,
},
{
title: "创建时间",
dataIndex: "createdAt",
key: "createdAt",
width: 150,
ellipsis: true,
},
{
title: "更新时间",
dataIndex: "updatedAt",
key: "updatedAt",
width: 150,
ellipsis: true,
},
{
title: "最近执行ID",
dataIndex: "lastExecutionId",
key: "lastExecutionId",
width: 150,
ellipsis: true,
},
{
title: "描述",
dataIndex: "description",
key: "description",
ellipsis: true,
width: 200,
},
{
title: "操作",
key: "action",
fixed: "right" as const,
render: (_: any, record: CollectionTask) => {
return taskOperations(record).map((op) => {
const button = (
<Tooltip key={op.key} title={op.label}>
<Button
type="text"
icon={op.icon}
danger={op?.danger}
onClick={() => op.onClick(record)}
/>
</Tooltip>
);
if (op.confirm) {
return (
<Popconfirm
key={op.key}
title={op.confirm.title}
okText={op.confirm.okText}
cancelText={op.confirm.cancelText}
okType={op.danger ? "danger" : "primary"}
onConfirm={() => op.onClick(record)}
>
<Tooltip key={op.key} title={op.label}>
<Button type="text" icon={op.icon} danger={op?.danger} />
</Tooltip>
</Popconfirm>
);
}
return button;
});
},
},
];
return (
<div className="space-y-4">
{/* Header Actions */}
<SearchControls
searchTerm={searchParams.keyword}
onSearchChange={(newSearchTerm) =>
setSearchParams((prev) => ({
...prev,
keyword: newSearchTerm,
current: 1,
}))
}
searchPlaceholder="搜索任务名称或描述..."
filters={filters}
onFiltersChange={handleFiltersChange}
showViewToggle={false}
onClearFilters={() =>
setSearchParams((prev) => ({
...prev,
filters: {},
}))
}
onReload={fetchData}
/>
{/* Tasks Table */}
<Card>
<Table
columns={columns}
dataSource={tableData}
loading={loading}
rowKey="id"
pagination={{
...pagination,
current: searchParams.current,
pageSize: searchParams.pageSize,
total: pagination.total,
}}
scroll={{ x: "max-content", y: "calc(100vh - 25rem)" }}
/>
</Card>
</div>
);
}

View File

@@ -1,60 +1,60 @@
import { get, post, put, del } from "@/utils/request";
// 数据源任务相关接口
export function queryTasksUsingGet(params?: any) {
return get("/api/data-collection/tasks", params);
}
export function createTaskUsingPost(data: any) {
return post("/api/data-collection/tasks", data);
}
export function queryTaskByIdUsingGet(id: string | number) {
return get(`/api/data-collection/tasks/${id}`);
}
export function updateTaskByIdUsingPut(
id: string | number,
data: any
) {
return put(`/api/data-collection/tasks/${id}`, data);
}
export function queryTaskDetailsByIdUsingGet(id: string | number) {
return get(`/api/data-collection/tasks/${id}`);
}
export function queryDataXTemplatesUsingGet(params?: any) {
return get("/api/data-collection/templates", params);
}
export function deleteTaskByIdUsingDelete(id: string | number) {
return del(`/api/data-collection/tasks/${id}`);
}
export function executeTaskByIdUsingPost(
id: string | number,
data?: any
) {
return post(`/api/data-collection/tasks/${id}/execute`, data);
}
export function stopTaskByIdUsingPost(
id: string | number,
data?: any
) {
return post(`/api/data-collection/tasks/${id}/stop`, data);
}
// 执行日志相关接口
export function queryExecutionLogUsingPost(params?: any) {
return post("/api/data-collection/executions", params);
}
export function queryExecutionLogByIdUsingGet(id: string | number) {
return get(`/api/data-collection/executions/${id}`);
}
// 监控统计相关接口
export function queryCollectionStatisticsUsingGet(params?: any) {
return get("/api/data-collection/monitor/statistics", params);
}
import { get, post, put, del } from "@/utils/request";
// 数据源任务相关接口
export function queryTasksUsingGet(params?: any) {
return get("/api/data-collection/tasks", params);
}
export function createTaskUsingPost(data: any) {
return post("/api/data-collection/tasks", data);
}
export function queryTaskByIdUsingGet(id: string | number) {
return get(`/api/data-collection/tasks/${id}`);
}
export function updateTaskByIdUsingPut(
id: string | number,
data: any
) {
return put(`/api/data-collection/tasks/${id}`, data);
}
export function queryTaskDetailsByIdUsingGet(id: string | number) {
return get(`/api/data-collection/tasks/${id}`);
}
export function queryDataXTemplatesUsingGet(params?: any) {
return get("/api/data-collection/templates", params);
}
export function deleteTaskByIdUsingDelete(id: string | number) {
return del(`/api/data-collection/tasks/${id}`);
}
export function executeTaskByIdUsingPost(
id: string | number,
data?: any
) {
return post(`/api/data-collection/tasks/${id}/execute`, data);
}
export function stopTaskByIdUsingPost(
id: string | number,
data?: any
) {
return post(`/api/data-collection/tasks/${id}/stop`, data);
}
// 执行日志相关接口
export function queryExecutionLogUsingPost(params?: any) {
return post("/api/data-collection/executions", params);
}
export function queryExecutionLogByIdUsingGet(id: string | number) {
return get(`/api/data-collection/executions/${id}`);
}
// 监控统计相关接口
export function queryCollectionStatisticsUsingGet(params?: any) {
return get("/api/data-collection/monitor/statistics", params);
}

View File

@@ -1,81 +1,81 @@
import {
LogStatus,
SyncMode,
TaskStatus,
TriggerType,
} from "./collection.model";
export const StatusMap: Record<
TaskStatus,
{ label: string; color: string; value: TaskStatus }
> = {
[TaskStatus.RUNNING]: {
label: "运行",
color: "blue",
value: TaskStatus.RUNNING,
},
[TaskStatus.STOPPED]: {
label: "停止",
color: "gray",
value: TaskStatus.STOPPED,
},
[TaskStatus.FAILED]: {
label: "错误",
color: "red",
value: TaskStatus.FAILED,
},
[TaskStatus.SUCCESS]: {
label: "成功",
color: "green",
value: TaskStatus.SUCCESS,
},
[TaskStatus.DRAFT]: {
label: "草稿",
color: "orange",
value: TaskStatus.DRAFT,
},
[TaskStatus.READY]: { label: "就绪", color: "cyan", value: TaskStatus.READY },
};
export const SyncModeMap: Record<SyncMode, { label: string; value: SyncMode }> =
{
[SyncMode.ONCE]: { label: "立即同步", value: SyncMode.ONCE },
[SyncMode.SCHEDULED]: { label: "定时同步", value: SyncMode.SCHEDULED },
};
export const LogStatusMap: Record<
LogStatus,
{ label: string; color: string; value: LogStatus }
> = {
[LogStatus.SUCCESS]: {
label: "成功",
color: "green",
value: LogStatus.SUCCESS,
},
[LogStatus.FAILED]: {
label: "失败",
color: "red",
value: LogStatus.FAILED,
},
[LogStatus.RUNNING]: {
label: "运行中",
color: "blue",
value: LogStatus.RUNNING,
},
};
export const LogTriggerTypeMap: Record<
TriggerType,
{ label: string; value: TriggerType }
> = {
[TriggerType.MANUAL]: { label: "手动", value: TriggerType.MANUAL },
[TriggerType.SCHEDULED]: { label: "定时", value: TriggerType.SCHEDULED },
[TriggerType.API]: { label: "API", value: TriggerType.API },
};
export function mapCollectionTask(task: CollectionTask): CollectionTask {
return {
...task,
status: StatusMap[task.status],
};
}
import {
LogStatus,
SyncMode,
TaskStatus,
TriggerType,
} from "./collection.model";
export const StatusMap: Record<
TaskStatus,
{ label: string; color: string; value: TaskStatus }
> = {
[TaskStatus.RUNNING]: {
label: "运行",
color: "blue",
value: TaskStatus.RUNNING,
},
[TaskStatus.STOPPED]: {
label: "停止",
color: "gray",
value: TaskStatus.STOPPED,
},
[TaskStatus.FAILED]: {
label: "错误",
color: "red",
value: TaskStatus.FAILED,
},
[TaskStatus.SUCCESS]: {
label: "成功",
color: "green",
value: TaskStatus.SUCCESS,
},
[TaskStatus.DRAFT]: {
label: "草稿",
color: "orange",
value: TaskStatus.DRAFT,
},
[TaskStatus.READY]: { label: "就绪", color: "cyan", value: TaskStatus.READY },
};
export const SyncModeMap: Record<SyncMode, { label: string; value: SyncMode }> =
{
[SyncMode.ONCE]: { label: "立即同步", value: SyncMode.ONCE },
[SyncMode.SCHEDULED]: { label: "定时同步", value: SyncMode.SCHEDULED },
};
export const LogStatusMap: Record<
LogStatus,
{ label: string; color: string; value: LogStatus }
> = {
[LogStatus.SUCCESS]: {
label: "成功",
color: "green",
value: LogStatus.SUCCESS,
},
[LogStatus.FAILED]: {
label: "失败",
color: "red",
value: LogStatus.FAILED,
},
[LogStatus.RUNNING]: {
label: "运行中",
color: "blue",
value: LogStatus.RUNNING,
},
};
export const LogTriggerTypeMap: Record<
TriggerType,
{ label: string; value: TriggerType }
> = {
[TriggerType.MANUAL]: { label: "手动", value: TriggerType.MANUAL },
[TriggerType.SCHEDULED]: { label: "定时", value: TriggerType.SCHEDULED },
[TriggerType.API]: { label: "API", value: TriggerType.API },
};
export function mapCollectionTask(task: CollectionTask): CollectionTask {
return {
...task,
status: StatusMap[task.status],
};
}

View File

@@ -1,52 +1,52 @@
export enum TaskStatus {
DRAFT = "DRAFT",
READY = "READY",
RUNNING = "RUNNING",
SUCCESS = "SUCCESS",
FAILED = "FAILED",
STOPPED = "STOPPED",
}
export enum SyncMode {
ONCE = "ONCE",
SCHEDULED = "SCHEDULED",
}
export interface CollectionTask {
id: string;
name: string;
description: string;
config: object; // 具体配置结构根据实际需求定义
status: TaskStatus;
syncMode: SyncMode;
scheduleExpression?: string; // 仅当 syncMode 为 SCHEDULED 时存在
lastExecutionId: string;
createdAt: string; // ISO date string
updatedAt: string; // ISO date string
}
export enum LogStatus {
RUNNING = "RUNNING",
SUCCESS = "SUCCESS",
FAILED = "FAILED",
}
export enum TriggerType {
MANUAL = "MANUAL",
SCHEDULED = "SCHEDULED",
API = "API",
}
export interface CollectionLog {
id: string;
taskId: string;
taskName: string;
status: TaskStatus; // 任务执行状态
triggerType: TriggerType; // 触发类型,如手动触发、定时触发等
startTime: string; // ISO date string
endTime: string; // ISO date string
duration: string; // 格式化的持续时间字符串
retryCount: number;
processId: string;
errorMessage?: string; // 可选,错误信息
}
export enum TaskStatus {
DRAFT = "DRAFT",
READY = "READY",
RUNNING = "RUNNING",
SUCCESS = "SUCCESS",
FAILED = "FAILED",
STOPPED = "STOPPED",
}
export enum SyncMode {
ONCE = "ONCE",
SCHEDULED = "SCHEDULED",
}
export interface CollectionTask {
id: string;
name: string;
description: string;
config: object; // 具体配置结构根据实际需求定义
status: TaskStatus;
syncMode: SyncMode;
scheduleExpression?: string; // 仅当 syncMode 为 SCHEDULED 时存在
lastExecutionId: string;
createdAt: string; // ISO date string
updatedAt: string; // ISO date string
}
export enum LogStatus {
RUNNING = "RUNNING",
SUCCESS = "SUCCESS",
FAILED = "FAILED",
}
export enum TriggerType {
MANUAL = "MANUAL",
SCHEDULED = "SCHEDULED",
API = "API",
}
export interface CollectionLog {
id: string;
taskId: string;
taskName: string;
status: TaskStatus; // 任务执行状态
triggerType: TriggerType; // 触发类型,如手动触发、定时触发等
startTime: string; // ISO date string
endTime: string; // ISO date string
duration: string; // 格式化的持续时间字符串
retryCount: number;
processId: string;
errorMessage?: string; // 可选,错误信息
}