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 {
Tag,
Pagination,
Tooltip,
Empty,
Popover,
} from "antd";
import { Tag, Pagination, Tooltip, Empty, Popover, Spin } from "antd";
import { ClockCircleOutlined, StarFilled } from "@ant-design/icons";
import type { ItemType } from "antd/es/menu/interface";
import { formatDateTime } from "@/utils/unit";