@jiangood/open-admin 3.1.10 → 3.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/framework/GlobalData.ts +2 -0
- package/src/framework/components/CronEditor/index.tsx +318 -0
- package/src/framework/components/index.ts +1 -0
- package/src/framework/utils/CronUtils.ts +562 -0
- package/src/framework/utils/index.ts +1 -0
- package/src/layouts/admin/index.jsx +9 -0
- package/src/pages/system/config/index.jsx +309 -0
- package/src/pages/system/job/index.jsx +61 -18
package/package.json
CHANGED
|
@@ -12,6 +12,8 @@ export interface SiteInfo {
|
|
|
12
12
|
waterMark?: boolean;
|
|
13
13
|
/** 登录失效(空闲超时/未登录)时全局弹窗的提示文案,对应 sys.login-expired-message */
|
|
14
14
|
loginExpiredMessage?: string;
|
|
15
|
+
/** 后端声明为 publicVisible 的系统配置,key → value */
|
|
16
|
+
config?: Record<string, unknown>;
|
|
15
17
|
[key: string]: unknown;
|
|
16
18
|
}
|
|
17
19
|
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {Button, Input, InputNumber, Modal, Segmented, Select, Space, TimePicker} from 'antd';
|
|
3
|
+
import {ScheduleOutlined} from '@ant-design/icons';
|
|
4
|
+
import dayjs from 'dayjs';
|
|
5
|
+
import type {Dayjs} from 'dayjs';
|
|
6
|
+
import {CRON_INTERVAL_UNITS, CRON_WEEKDAYS, CronUtils} from '../../utils/CronUtils';
|
|
7
|
+
import type {CronIntervalUnit, CronPeriod} from '../../utils/CronUtils';
|
|
8
|
+
|
|
9
|
+
export interface CronPreset {
|
|
10
|
+
label: React.ReactNode;
|
|
11
|
+
value: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface CronEditorProps {
|
|
15
|
+
value?: string;
|
|
16
|
+
onChange?: (value: string) => void;
|
|
17
|
+
/** 常用表达式,点击后填入编辑器 */
|
|
18
|
+
presets?: CronPreset[];
|
|
19
|
+
placeholder?: string;
|
|
20
|
+
disabled?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type Mode = 'interval' | 'schedule' | 'custom';
|
|
24
|
+
|
|
25
|
+
const PERIOD_OPTIONS: { value: CronPeriod; label: string }[] = [
|
|
26
|
+
{value: 'DAILY', label: '每天'},
|
|
27
|
+
{value: 'WEEKLY', label: '每周'},
|
|
28
|
+
{value: 'MONTHLY', label: '每月'},
|
|
29
|
+
{value: 'YEARLY', label: '每年'},
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const MONTH_OPTIONS = Array.from({length: 12}, (_, i) => ({value: i + 1, label: `${i + 1} 月`}));
|
|
33
|
+
const DAY_OPTIONS: { value: number | 'L'; label: string }[] = [
|
|
34
|
+
...Array.from({length: 31}, (_, i) => ({value: i + 1, label: `${i + 1} 号`})),
|
|
35
|
+
{value: 'L', label: '最后一天'},
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
function FieldRow({label, children}: { label: string; children: React.ReactNode }) {
|
|
39
|
+
return <div style={{display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap'}}>
|
|
40
|
+
<span style={{width: 40, color: 'var(--oa-color-text-secondary, rgba(0, 0, 0, 0.65))'}}>{label}</span>
|
|
41
|
+
{children}
|
|
42
|
+
</div>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* cron 表达式输入框 + 常用表达式助手。
|
|
47
|
+
* 值为 Quartz 格式:秒 分 时 日 月 周 [年],第一位是秒。
|
|
48
|
+
*/
|
|
49
|
+
export function CronEditor({value, onChange, presets = [], placeholder, disabled}: CronEditorProps) {
|
|
50
|
+
const [open, setOpen] = React.useState(false);
|
|
51
|
+
const [mode, setMode] = React.useState<Mode>('interval');
|
|
52
|
+
const [every, setEvery] = React.useState(5);
|
|
53
|
+
const [unit, setUnit] = React.useState<CronIntervalUnit>('minute');
|
|
54
|
+
const [period, setPeriod] = React.useState<CronPeriod>('DAILY');
|
|
55
|
+
const [time, setTime] = React.useState<Dayjs>(() => dayjs().hour(2).minute(0).second(0));
|
|
56
|
+
const [weekday, setWeekday] = React.useState('MON');
|
|
57
|
+
const [dayOfMonth, setDayOfMonth] = React.useState<number | 'L'>(1);
|
|
58
|
+
const [month, setMonth] = React.useState(1);
|
|
59
|
+
const [custom, setCustom] = React.useState('');
|
|
60
|
+
|
|
61
|
+
const generated = React.useMemo(() => {
|
|
62
|
+
if (mode === 'interval') {
|
|
63
|
+
return CronUtils.buildInterval(every, unit);
|
|
64
|
+
}
|
|
65
|
+
if (mode === 'schedule') {
|
|
66
|
+
return CronUtils.buildSchedule({
|
|
67
|
+
period,
|
|
68
|
+
hour: time.hour(),
|
|
69
|
+
minute: time.minute(),
|
|
70
|
+
second: time.second(),
|
|
71
|
+
weekday,
|
|
72
|
+
dayOfMonth,
|
|
73
|
+
month,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return custom.trim();
|
|
77
|
+
}, [mode, every, unit, period, time, weekday, dayOfMonth, month, custom]);
|
|
78
|
+
|
|
79
|
+
const description = CronUtils.describe(generated);
|
|
80
|
+
|
|
81
|
+
const changeUnit = (nextUnit: CronIntervalUnit) => {
|
|
82
|
+
const max = nextUnit === 'second' || nextUnit === 'minute' ? 59 : nextUnit === 'hour' ? 23 : 31;
|
|
83
|
+
setUnit(nextUnit);
|
|
84
|
+
setEvery(v => Math.min(Math.max(1, v), max));
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const changePeriod = (nextPeriod: CronPeriod) => {
|
|
88
|
+
setPeriod(nextPeriod);
|
|
89
|
+
if (nextPeriod === 'YEARLY' && dayOfMonth === 'L') {
|
|
90
|
+
setDayOfMonth(1);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const applyExpression = (expr: string) => {
|
|
95
|
+
const parsed = CronUtils.parse(expr);
|
|
96
|
+
if (parsed?.mode === 'interval') {
|
|
97
|
+
setMode('interval');
|
|
98
|
+
setEvery(parsed.every);
|
|
99
|
+
setUnit(parsed.unit);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (parsed?.mode === 'schedule') {
|
|
103
|
+
setMode('schedule');
|
|
104
|
+
setPeriod(parsed.period);
|
|
105
|
+
setTime(dayjs().hour(parsed.hour).minute(parsed.minute).second(parsed.second));
|
|
106
|
+
setWeekday(parsed.weekday || 'MON');
|
|
107
|
+
setDayOfMonth(parsed.dayOfMonth ?? 1);
|
|
108
|
+
setMonth(parsed.month ?? 1);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
setMode('custom');
|
|
112
|
+
setCustom(expr);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const openEditor = () => {
|
|
116
|
+
const expr = (value || '').trim();
|
|
117
|
+
if (expr) {
|
|
118
|
+
applyExpression(expr);
|
|
119
|
+
} else {
|
|
120
|
+
setMode('interval');
|
|
121
|
+
setEvery(5);
|
|
122
|
+
setUnit('minute');
|
|
123
|
+
setCustom('');
|
|
124
|
+
}
|
|
125
|
+
setOpen(true);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const handleOk = () => {
|
|
129
|
+
if (!generated) return;
|
|
130
|
+
onChange?.(generated);
|
|
131
|
+
setOpen(false);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const changeMode = (nextMode: Mode) => {
|
|
135
|
+
if (nextMode === 'custom' && !custom.trim()) {
|
|
136
|
+
setCustom(generated);
|
|
137
|
+
}
|
|
138
|
+
setMode(nextMode);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
return <>
|
|
142
|
+
<div style={{display: 'flex', gap: 8, width: '100%'}}>
|
|
143
|
+
<Input
|
|
144
|
+
value={value ?? ''}
|
|
145
|
+
onChange={e => onChange?.(e.target.value)}
|
|
146
|
+
placeholder={placeholder}
|
|
147
|
+
disabled={disabled}
|
|
148
|
+
allowClear
|
|
149
|
+
/>
|
|
150
|
+
<Button
|
|
151
|
+
icon={<ScheduleOutlined/>}
|
|
152
|
+
title='cron 表达式助手'
|
|
153
|
+
aria-label='cron 表达式助手'
|
|
154
|
+
onClick={openEditor}
|
|
155
|
+
disabled={disabled}
|
|
156
|
+
/>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<Modal
|
|
160
|
+
open={open}
|
|
161
|
+
title='cron 表达式助手'
|
|
162
|
+
width={600}
|
|
163
|
+
onCancel={() => setOpen(false)}
|
|
164
|
+
onOk={handleOk}
|
|
165
|
+
okText='使用该表达式'
|
|
166
|
+
okButtonProps={{disabled: !generated}}
|
|
167
|
+
destroyOnHidden
|
|
168
|
+
>
|
|
169
|
+
<div style={{display: 'grid', gap: 16}}>
|
|
170
|
+
{presets.length > 0 && <div>
|
|
171
|
+
<div style={{marginBottom: 8, color: 'var(--oa-color-text-secondary, rgba(0, 0, 0, 0.65))'}}>
|
|
172
|
+
常用表达式
|
|
173
|
+
</div>
|
|
174
|
+
<Space size={[8, 8]} wrap>
|
|
175
|
+
{presets.map(preset => <Button
|
|
176
|
+
key={preset.value}
|
|
177
|
+
size='small'
|
|
178
|
+
title={preset.value}
|
|
179
|
+
onClick={() => applyExpression(preset.value)}
|
|
180
|
+
>
|
|
181
|
+
{preset.label}
|
|
182
|
+
</Button>)}
|
|
183
|
+
</Space>
|
|
184
|
+
</div>}
|
|
185
|
+
|
|
186
|
+
<Segmented
|
|
187
|
+
block
|
|
188
|
+
value={mode}
|
|
189
|
+
onChange={v => changeMode(v as Mode)}
|
|
190
|
+
options={[
|
|
191
|
+
{label: '按间隔', value: 'interval'},
|
|
192
|
+
{label: '按时间点', value: 'schedule'},
|
|
193
|
+
{label: '自定义', value: 'custom'},
|
|
194
|
+
]}
|
|
195
|
+
/>
|
|
196
|
+
|
|
197
|
+
{mode === 'interval' && <div style={{display: 'grid', gap: 10}}>
|
|
198
|
+
<FieldRow label='每隔'>
|
|
199
|
+
<InputNumber
|
|
200
|
+
min={1}
|
|
201
|
+
max={unit === 'second' || unit === 'minute' ? 59 : unit === 'hour' ? 23 : 31}
|
|
202
|
+
precision={0}
|
|
203
|
+
value={every}
|
|
204
|
+
onChange={v => setEvery(Number(v) || 1)}
|
|
205
|
+
style={{width: 110}}
|
|
206
|
+
/>
|
|
207
|
+
<Select
|
|
208
|
+
value={unit}
|
|
209
|
+
onChange={changeUnit}
|
|
210
|
+
options={CRON_INTERVAL_UNITS}
|
|
211
|
+
style={{width: 100}}
|
|
212
|
+
/>
|
|
213
|
+
<span>执行一次</span>
|
|
214
|
+
</FieldRow>
|
|
215
|
+
<div style={{fontSize: 12, color: 'var(--oa-color-text-tertiary, #999)'}}>
|
|
216
|
+
秒级间隔过短可能增加系统压力,建议按实际需要设置。
|
|
217
|
+
</div>
|
|
218
|
+
</div>}
|
|
219
|
+
|
|
220
|
+
{mode === 'schedule' && <div style={{display: 'grid', gap: 10}}>
|
|
221
|
+
<FieldRow label='周期'>
|
|
222
|
+
<Select
|
|
223
|
+
value={period}
|
|
224
|
+
onChange={changePeriod}
|
|
225
|
+
options={PERIOD_OPTIONS}
|
|
226
|
+
style={{width: 110}}
|
|
227
|
+
/>
|
|
228
|
+
<span>时间</span>
|
|
229
|
+
<TimePicker
|
|
230
|
+
value={time}
|
|
231
|
+
onChange={v => {
|
|
232
|
+
if (v) setTime(v);
|
|
233
|
+
}}
|
|
234
|
+
format='HH:mm:ss'
|
|
235
|
+
allowClear={false}
|
|
236
|
+
style={{width: 130}}
|
|
237
|
+
/>
|
|
238
|
+
</FieldRow>
|
|
239
|
+
|
|
240
|
+
{period === 'WEEKLY' && <FieldRow label='星期'>
|
|
241
|
+
<Select
|
|
242
|
+
value={weekday}
|
|
243
|
+
onChange={setWeekday}
|
|
244
|
+
options={CRON_WEEKDAYS}
|
|
245
|
+
style={{width: 110}}
|
|
246
|
+
/>
|
|
247
|
+
</FieldRow>}
|
|
248
|
+
|
|
249
|
+
{period === 'MONTHLY' && <FieldRow label='日期'>
|
|
250
|
+
<Select<number | 'L'>
|
|
251
|
+
value={dayOfMonth}
|
|
252
|
+
onChange={setDayOfMonth}
|
|
253
|
+
options={DAY_OPTIONS}
|
|
254
|
+
style={{width: 110}}
|
|
255
|
+
/>
|
|
256
|
+
</FieldRow>}
|
|
257
|
+
|
|
258
|
+
{period === 'YEARLY' && <FieldRow label='日期'>
|
|
259
|
+
<Select
|
|
260
|
+
value={month}
|
|
261
|
+
onChange={setMonth}
|
|
262
|
+
options={MONTH_OPTIONS}
|
|
263
|
+
style={{width: 100}}
|
|
264
|
+
/>
|
|
265
|
+
<Select<number | 'L'>
|
|
266
|
+
value={dayOfMonth === 'L' ? 1 : dayOfMonth}
|
|
267
|
+
onChange={setDayOfMonth}
|
|
268
|
+
options={DAY_OPTIONS.filter(o => o.value !== 'L')}
|
|
269
|
+
style={{width: 100}}
|
|
270
|
+
/>
|
|
271
|
+
</FieldRow>}
|
|
272
|
+
|
|
273
|
+
{period === 'MONTHLY' && typeof dayOfMonth === 'number' && dayOfMonth > 28 &&
|
|
274
|
+
<div style={{fontSize: 12, color: 'var(--oa-color-text-tertiary, #999)'}}>
|
|
275
|
+
部分月份没有 {dayOfMonth} 号,这些月份会跳过本次执行。
|
|
276
|
+
</div>}
|
|
277
|
+
</div>}
|
|
278
|
+
|
|
279
|
+
{mode === 'custom' && <div style={{display: 'grid', gap: 8}}>
|
|
280
|
+
<Input
|
|
281
|
+
value={custom}
|
|
282
|
+
onChange={e => setCustom(e.target.value)}
|
|
283
|
+
placeholder='如 0 */5 * * * ?'
|
|
284
|
+
/>
|
|
285
|
+
<div style={{fontSize: 12, color: 'var(--oa-color-text-tertiary, #999)'}}>
|
|
286
|
+
格式:秒 分 时 日 月 周(可选 年)。例如 0 */5 * * * ? 表示每隔 5 分钟。
|
|
287
|
+
</div>
|
|
288
|
+
</div>}
|
|
289
|
+
|
|
290
|
+
<div style={{
|
|
291
|
+
padding: 12,
|
|
292
|
+
borderRadius: 6,
|
|
293
|
+
background: 'var(--oa-color-fill, #fafafa)',
|
|
294
|
+
border: '1px solid var(--oa-color-border, #f0f0f0)',
|
|
295
|
+
}}>
|
|
296
|
+
<div style={{
|
|
297
|
+
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace',
|
|
298
|
+
fontSize: 14,
|
|
299
|
+
wordBreak: 'break-all',
|
|
300
|
+
}}>
|
|
301
|
+
{generated || '—'}
|
|
302
|
+
</div>
|
|
303
|
+
<div style={{
|
|
304
|
+
marginTop: 6,
|
|
305
|
+
fontSize: 12,
|
|
306
|
+
color: 'var(--oa-color-text-secondary, rgba(0, 0, 0, 0.65))',
|
|
307
|
+
}}>
|
|
308
|
+
{generated ? (description || '暂不支持预览该表达式,保存时由后端校验') : '请先设置执行规则'}
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
|
|
312
|
+
<div style={{fontSize: 12, color: 'var(--oa-color-text-tertiary, #999)'}}>
|
|
313
|
+
Quartz 格式:秒 分 时 日 月 周(可选 年),第一位是秒。
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
</Modal>
|
|
317
|
+
</>;
|
|
318
|
+
}
|
|
@@ -10,6 +10,7 @@ export * from './DownloadModal'
|
|
|
10
10
|
export * from './OrgSwitcher'
|
|
11
11
|
export * from './ThemeSettings'
|
|
12
12
|
export * from './LogViewer'
|
|
13
|
+
export * from './CronEditor'
|
|
13
14
|
export { default as ErrorBoundary } from './ErrorBoundary'
|
|
14
15
|
export { default as FormModal } from './FormModal'
|
|
15
16
|
export { default as FormDrawer } from './DrawerForm'
|