feat(flowEditor): 增加周期性节点支持

- 新增 CronPicker 组件用于编辑周期表达式
- 在 CycleEditor 中集成 CronPicker,支持周期参数配置- 优化 NodeContent 组件,支持显示周期性节点的执行周期
- 新增 isJSON 函数用于判断是否为 JSON 字符串
master
钟良源 5 months ago
parent 682412784b
commit 943ac9e648

@ -20,6 +20,7 @@ interface CronPickerProps {
onChange?: (value: string) => void;
style?: React.CSSProperties;
className?: string;
onUpdateData: (data) => void;
}
const PRESETS = [
@ -42,7 +43,7 @@ const dayMap = {
'SUN': '周日'
};
const CronPicker: React.FC<CronPickerProps> = ({ value, onChange, style, className }) => {
const CronPicker: React.FC<CronPickerProps> = ({ value, onChange, style, className, onUpdateData }) => {
const [mode, setMode] = useState<'visual' | 'expression'>('visual');
const [cron, setCron] = useState<string>(value || '0 0 0 * * ?');
const [error, setError] = useState<string>('');
@ -56,6 +57,8 @@ const CronPicker: React.FC<CronPickerProps> = ({ value, onChange, style, classNa
const [specificDay, setSpecificDay] = useState<number>(1);
const [selectedWeekdays, setSelectedWeekdays] = useState<string[]>(['MON']);
const [selectedMonths, setSelectedMonths] = useState<string[]>(['1']);
const [monthlyType, setMonthlyType] = useState<'every' | 'specific'>('every');
const [specificMonths, setSpecificMonths] = useState<string[]>(['1']);
// 初始化
useEffect(() => {
@ -108,7 +111,11 @@ const CronPicker: React.FC<CronPickerProps> = ({ value, onChange, style, classNa
dow = selectedWeekdays.length > 0 ? selectedWeekdays.join(',') : '*';
}
const mon = selectedMonths.length < 12 ? selectedMonths.join(',') : '*';
const mon = monthlyType === 'every'
? '*'
: specificMonths.length > 0
? specificMonths.join(',')
: '*';
const newCron = `${sec} ${min} ${hour} ${dom} ${mon} ${dow}`;
return newCron;
// validateAndSet(newCron);
@ -119,7 +126,14 @@ const CronPicker: React.FC<CronPickerProps> = ({ value, onChange, style, classNa
return generateCron();
}
return cron;
}, [mode, minuteType, minutesInterval, hourType, hourlyAt, dailyType, specificDay, selectedWeekdays, selectedMonths, cron]);
}, [mode, minuteType, minutesInterval, hourType, hourlyAt, dailyType, specificDay, selectedWeekdays, selectedMonths, monthlyType, specificMonths, cron]);
useEffect(() => {
onUpdateData({
customDef: { intervalSeconds: currentCron },
type: 'CYCLE'
});
}, [currentCron]);
// 解析Cron表达式并更新图形化配置的状态
const parseCronExpression = (cronExpression: string) => {
@ -189,11 +203,18 @@ const CronPicker: React.FC<CronPickerProps> = ({ value, onChange, style, classNa
}
// 解析月份部分
if (month !== '*' && month.includes(',')) {
if (month === '*') {
setMonthlyType('every');
}
else if (month.includes(',')) {
setMonthlyType('specific');
setSelectedMonths(month.split(','));
setSpecificMonths(month.split(','));
}
else if (month !== '*') {
setMonthlyType('specific');
setSelectedMonths([month]);
setSpecificMonths([month]);
}
}
};
@ -225,7 +246,6 @@ const CronPicker: React.FC<CronPickerProps> = ({ value, onChange, style, classNa
const getNextTime = (): string => {
try {
console.log('currentCron:', currentCron);
const interval = CronExpressionParser.parse(currentCron);
const next = interval.next();
return next?.toDate ? next.toDate().toLocaleString() : '无法计算';
@ -379,6 +399,43 @@ const CronPicker: React.FC<CronPickerProps> = ({ value, onChange, style, classNa
</Form.Item>
)}
<Form.Item label="月份" className={styles.formItem}>
<Radio.Group
value={monthlyType}
onChange={(value) => setMonthlyType(value as 'every' | 'specific')}
style={{ display: 'flex' }}
>
<Space size="large">
<Radio value="every"></Radio>
<Radio value="specific"></Radio>
</Space>
</Radio.Group>
</Form.Item>
{monthlyType === 'specific' && (
<Form.Item label="指定月份" className={styles.formItem}>
<div className={styles.inlineGroup}>
{Array.from({ length: 12 }, (_, i) => i + 1).map(month => (
<Button
key={month}
type={specificMonths.includes(String(month)) ? 'primary' : 'secondary'}
size="small"
className={styles.weekButton}
onClick={() => {
setSpecificMonths(prev =>
prev.includes(String(month))
? prev.filter(m => m !== String(month))
: [...prev, String(month)]
);
}}
>
{month}
</Button>
))}
</div>
</Form.Item>
)}
<Space wrap size="small" className={styles.presetsContainer}>
{PRESETS.map(p => (
<Button key={p.value} type="text" size="small" onClick={() => updateCron(p.value)}>

@ -1,6 +1,8 @@
import React from 'react';
import styles from '@/pages/flowEditor/node/style/base.module.less';
import { Handle, Position, useStore } from '@xyflow/react';
import { isJSON } from '@/utils/common';
import cronstrue from 'cronstrue/i18n';
interface NodeContentData {
parameters?: {
@ -176,12 +178,19 @@ const renderRegularNodeHandles = (dataIns: any[], dataOuts: any[], apiIns: any[]
);
};
// 通过duration计算时分秒
const formatDuration = (duration: number) => {
const hours = Math.floor(duration / 3600);
const minutes = Math.floor((duration % 3600) / 60);
const seconds = Math.floor(duration % 60);
return `${hours}小时${minutes}分钟${seconds}`;
const formatFooter = (data: any) => {
// TODO 这里后续需要优化
if (isJSON(data)) {
const { duration } = JSON.parse(data);
const hours = Math.floor(duration / 3600);
const minutes = Math.floor((duration % 3600) / 60);
const seconds = Math.floor(duration % 60);
return `${hours}小时${minutes}分钟${seconds}`;
}
else {
const keyMap = ['intervalSeconds'];
return cronstrue.toString(data.intervalSeconds, { locale: 'zh_CN' });
}
};
const NodeContent = ({ data }: { data: NodeContentData }) => {
@ -190,7 +199,7 @@ const NodeContent = ({ data }: { data: NodeContentData }) => {
const dataIns = data.parameters?.dataIns || [];
const dataOuts = data.parameters?.dataOuts || [];
const showFooter = data?.component?.customDef || false;
const footerData = (showFooter && JSON.parse(data.component?.customDef)) || {};
const footerData = (showFooter && data.component?.customDef) || {};
// 判断节点类型
const isStartNode = data.type === 'start';
@ -225,7 +234,7 @@ const NodeContent = ({ data }: { data: NodeContentData }) => {
{/*footer栏*/}
{showFooter && (
<div className={styles['node-footer']}>
{formatDuration(footerData.duration)}
{formatFooter(footerData)}
</div>
)}

@ -1,12 +1,24 @@
import React from 'react';
import React, { useState } from 'react';
import { NodeEditorProps } from '@/pages/flowEditor/nodeEditors';
import { Typography } from '@arco-design/web-react';
import { IconUnorderedList } from '@arco-design/web-react/icon';
import ParamsTable from './ParamsTable';
import CronPicker from '@/components/CronPicker';
const CycleEditor: React.FC<NodeEditorProps> = ({ nodeData, updateNodeData }) => {
const [cron, setCron] = useState('0 0 9 * * ?');
return (
<>
<Typography.Title heading={5}><IconUnorderedList style={{ marginRight: 5 }} /></Typography.Title>
<CronPicker
value={cron}
onChange={setCron}
onUpdateData={(data) => {
updateNodeData('component', {
...data
});
}} />
<Typography.Title heading={5}><IconUnorderedList style={{ marginRight: 5 }} /></Typography.Title>
<ParamsTable
initialData={nodeData.parameters.dataIns || []}
@ -17,6 +29,16 @@ const CycleEditor: React.FC<NodeEditorProps> = ({ nodeData, updateNodeData }) =>
});
}}
/>
<Typography.Title heading={5}><IconUnorderedList style={{ marginRight: 5 }} /></Typography.Title>
<ParamsTable
initialData={nodeData.parameters.dataOuts || []}
onUpdateData={(data) => {
updateNodeData('parameters', {
...nodeData.parameters,
dataOuts: data
});
}}
/>
</>
);
};

@ -13,7 +13,6 @@ interface DurationType {
}
const WaitEditor: React.FC<NodeEditorProps> = ({ nodeData, updateNodeData }) => {
console.log('nodeData:', nodeData);
const hourOptions = Array.from({ length: 23 }, (_, i) => i);
const minuteOptions = Array.from({ length: 59 }, (_, i) => i);
const secondOptions = Array.from({ length: 59 }, (_, i) => i);

@ -106,4 +106,15 @@ export function formatDataType(value: string): string {
]);
return dataTypeAbbr.get(value) || value;
}
// 判断是否为JSON字符串
export function isJSON(str: any): boolean {
if (typeof str !== 'string') return false;
try {
JSON.parse(str);
return true;
} catch (e) {
return false;
}
}
Loading…
Cancel
Save