@jiangood/open-admin 3.1.9 → 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 CHANGED
@@ -55,7 +55,7 @@
55
55
  "lint": "eslint src --max-warnings=0",
56
56
  "lint:fix": "eslint src --fix"
57
57
  },
58
- "version": "3.1.9",
58
+ "version": "3.1.11",
59
59
  "overrides": {
60
60
  "zod-validation-error": "^4.0.2"
61
61
  }
@@ -7,9 +7,13 @@ const SITE_ARTICLES_KEY = "siteArticles";
7
7
 
8
8
  export interface SiteInfo {
9
9
  title?: string;
10
+ /** 系统简称,用于侧栏等空间受限处;未配置时前端回退到 title */
11
+ shortTitle?: string;
10
12
  waterMark?: boolean;
11
13
  /** 登录失效(空闲超时/未登录)时全局弹窗的提示文案,对应 sys.login-expired-message */
12
14
  loginExpiredMessage?: string;
15
+ /** 后端声明为 publicVisible 的系统配置,key → value */
16
+ config?: Record<string, unknown>;
13
17
  [key: string]: unknown;
14
18
  }
15
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'