@jiangood/open-admin 3.1.10 → 3.1.12
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 +3 -3
- package/src/framework/GlobalData.ts +4 -0
- package/src/framework/biz/OrgTree.tsx +1 -1
- package/src/framework/biz/RoleTree.tsx +1 -1
- package/src/framework/biz/UserPicker.tsx +4 -4
- package/src/framework/biz/index.tsx +4 -4
- package/src/framework/components/CronEditor/index.tsx +318 -0
- package/src/framework/components/LinkButton/index.tsx +4 -0
- package/src/framework/components/OrgSwitcher/index.tsx +2 -2
- package/src/framework/components/Page/index.tsx +6 -14
- package/src/framework/components/ProTable/index.tsx +3 -1
- package/src/framework/components/ThemeSettings/index.tsx +5 -38
- package/src/framework/components/index.ts +1 -0
- package/src/framework/config/index.ts +12 -0
- package/src/framework/config/layoutConfig.ts +47 -8
- package/src/framework/config/themeConfig.ts +93 -25
- package/src/framework/config/uiDefaults.ts +26 -0
- package/src/framework/fields/FieldEditor/index.tsx +1 -1
- package/src/framework/fields/FieldUploadFile/index.tsx +1 -1
- package/src/framework/fields/FieldUploadImage/index.tsx +42 -10
- package/src/framework/fields/FieldUserPicker/index.tsx +3 -3
- package/src/framework/fields/orgTree.ts +1 -1
- package/src/framework/router/index.ts +1 -0
- package/src/framework/router/matcher.ts +19 -4
- package/src/framework/utils/AvatarUtils.ts +64 -0
- package/src/framework/utils/CronUtils.ts +562 -0
- package/src/framework/utils/HttpClient.ts +5 -5
- package/src/framework/utils/PageUtils.ts +38 -7
- package/src/framework/utils/TitleUtils.ts +38 -0
- package/src/framework/utils/UrlUtils.ts +3 -3
- package/src/framework/utils/index.ts +3 -0
- package/src/layouts/admin/HeaderRight.jsx +62 -16
- package/src/layouts/admin/SinglePageLayout.jsx +13 -2
- package/src/layouts/admin/TabLayout.jsx +38 -19
- package/src/layouts/admin/index.jsx +12 -3
- package/src/layouts/index.tsx +52 -15
- package/src/pages/404.jsx +4 -0
- package/src/pages/article/$code.jsx +5 -1
- package/src/pages/login.jsx +157 -0
- package/src/pages/{public/login.less → login.less} +16 -1
- package/src/pages/public/login.jsx +11 -145
- package/src/pages/standalone/{forceUpdatePwd.jsx → force-update-pwd.jsx} +10 -4
- package/src/pages/system/article/index.jsx +1 -1
- package/src/pages/system/config/index.jsx +335 -0
- package/src/pages/system/file/index.jsx +3 -3
- package/src/pages/system/job/index.jsx +62 -19
- package/src/pages/system/log/index.jsx +1 -1
- package/src/pages/system/online/index.jsx +95 -0
- package/src/pages/system/org/index.jsx +7 -7
- package/src/pages/system/role/index.jsx +8 -8
- package/src/pages/system/role/{rolePerm.jsx → role-perm.jsx} +9 -4
- package/src/pages/system/user/{userPerm.jsx → UserPermModal.jsx} +3 -3
- package/src/pages/system/user/index.jsx +6 -6
- package/src/pages/user-center/AvatarPicker.jsx +76 -0
- package/src/pages/{userCenter/permView.jsx → user-center/PermView.jsx} +48 -25
- package/src/pages/user-center/RecentOps.jsx +36 -0
- package/src/pages/user-center/index.jsx +393 -0
- package/vite-plugin/index.js +64 -6
- package/src/pages/userCenter/index.jsx +0 -93
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quartz cron 表达式工具:生成、识别与中文说明。
|
|
3
|
+
*
|
|
4
|
+
* Quartz 格式为 6 或 7 段:秒 分 时 日 月 周 [年],第一位是秒。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type CronIntervalUnit = 'second' | 'minute' | 'hour' | 'day';
|
|
8
|
+
export type CronPeriod = 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'YEARLY';
|
|
9
|
+
|
|
10
|
+
export interface CronInterval {
|
|
11
|
+
mode: 'interval';
|
|
12
|
+
every: number;
|
|
13
|
+
unit: CronIntervalUnit;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface CronSchedule {
|
|
17
|
+
mode: 'schedule';
|
|
18
|
+
period: CronPeriod;
|
|
19
|
+
hour: number;
|
|
20
|
+
minute: number;
|
|
21
|
+
second: number;
|
|
22
|
+
weekday?: string;
|
|
23
|
+
dayOfMonth?: number | 'L';
|
|
24
|
+
month?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type CronParsed = CronInterval | CronSchedule;
|
|
28
|
+
|
|
29
|
+
export interface CronScheduleOptions {
|
|
30
|
+
period: CronPeriod;
|
|
31
|
+
hour: number;
|
|
32
|
+
minute: number;
|
|
33
|
+
second?: number;
|
|
34
|
+
weekday?: string;
|
|
35
|
+
dayOfMonth?: number | 'L';
|
|
36
|
+
month?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const CRON_INTERVAL_UNITS: { value: CronIntervalUnit; label: string }[] = [
|
|
40
|
+
{value: 'second', label: '秒'},
|
|
41
|
+
{value: 'minute', label: '分钟'},
|
|
42
|
+
{value: 'hour', label: '小时'},
|
|
43
|
+
{value: 'day', label: '天'},
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
export const CRON_WEEKDAYS: { value: string; label: string }[] = [
|
|
47
|
+
{value: 'MON', label: '周一'},
|
|
48
|
+
{value: 'TUE', label: '周二'},
|
|
49
|
+
{value: 'WED', label: '周三'},
|
|
50
|
+
{value: 'THU', label: '周四'},
|
|
51
|
+
{value: 'FRI', label: '周五'},
|
|
52
|
+
{value: 'SAT', label: '周六'},
|
|
53
|
+
{value: 'SUN', label: '周日'},
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
const WEEKDAY_CN: Record<string, string> = {
|
|
57
|
+
SUN: '周日',
|
|
58
|
+
MON: '周一',
|
|
59
|
+
TUE: '周二',
|
|
60
|
+
WED: '周三',
|
|
61
|
+
THU: '周四',
|
|
62
|
+
FRI: '周五',
|
|
63
|
+
SAT: '周六',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** Quartz 数字星期:1=周日 ... 7=周六 */
|
|
67
|
+
const WEEKDAY_BY_NUMBER = ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'];
|
|
68
|
+
|
|
69
|
+
const MONTH_BY_NAME: Record<string, number> = {
|
|
70
|
+
JAN: 1,
|
|
71
|
+
FEB: 2,
|
|
72
|
+
MAR: 3,
|
|
73
|
+
APR: 4,
|
|
74
|
+
MAY: 5,
|
|
75
|
+
JUN: 6,
|
|
76
|
+
JUL: 7,
|
|
77
|
+
AUG: 8,
|
|
78
|
+
SEP: 9,
|
|
79
|
+
OCT: 10,
|
|
80
|
+
NOV: 11,
|
|
81
|
+
DEC: 12,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
type SimpleField =
|
|
85
|
+
| { kind: 'any' }
|
|
86
|
+
| { kind: 'step'; step: number }
|
|
87
|
+
| { kind: 'number'; value: number }
|
|
88
|
+
| { kind: 'list'; values: number[] }
|
|
89
|
+
| { kind: 'range'; from: number; to: number };
|
|
90
|
+
|
|
91
|
+
type DomField =
|
|
92
|
+
| { kind: 'any' }
|
|
93
|
+
| { kind: 'day'; value: number }
|
|
94
|
+
| { kind: 'days'; values: number[] }
|
|
95
|
+
| { kind: 'range'; from: number; to: number }
|
|
96
|
+
| { kind: 'step'; step: number }
|
|
97
|
+
| { kind: 'last' }
|
|
98
|
+
| { kind: 'lastOffset'; offset: number }
|
|
99
|
+
| { kind: 'lastWeekday' }
|
|
100
|
+
| { kind: 'nearestWeekday'; day: number };
|
|
101
|
+
|
|
102
|
+
type MonthField =
|
|
103
|
+
| { kind: 'any' }
|
|
104
|
+
| { kind: 'month'; value: number }
|
|
105
|
+
| { kind: 'months'; values: number[] }
|
|
106
|
+
| { kind: 'range'; from: number; to: number };
|
|
107
|
+
|
|
108
|
+
type DowField =
|
|
109
|
+
| { kind: 'any' }
|
|
110
|
+
| { kind: 'day'; name: string }
|
|
111
|
+
| { kind: 'days'; names: string[] }
|
|
112
|
+
| { kind: 'range'; from: string; to: string }
|
|
113
|
+
| { kind: 'last'; name: string }
|
|
114
|
+
| { kind: 'nth'; name: string; nth: number };
|
|
115
|
+
|
|
116
|
+
function toInt(token: string): number | null {
|
|
117
|
+
return /^\d+$/.test(token) ? Number(token) : null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function splitFields(expr: string | null | undefined): string[] | null {
|
|
121
|
+
if (!expr) return null;
|
|
122
|
+
const parts = expr.trim().split(/\s+/);
|
|
123
|
+
if (parts.length === 7 && parts[6] === '*') return parts.slice(0, 6);
|
|
124
|
+
if (parts.length !== 6) return null;
|
|
125
|
+
return parts;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function weekdayKey(token: string): string | null {
|
|
129
|
+
const upper = token.toUpperCase();
|
|
130
|
+
if (WEEKDAY_CN[upper]) return upper;
|
|
131
|
+
const n = toInt(token);
|
|
132
|
+
return n != null && n >= 1 && n <= 7 ? WEEKDAY_BY_NUMBER[n - 1] : null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function monthNumber(token: string): number | null {
|
|
136
|
+
const upper = token.toUpperCase();
|
|
137
|
+
if (MONTH_BY_NAME[upper] != null) return MONTH_BY_NAME[upper];
|
|
138
|
+
const n = toInt(token);
|
|
139
|
+
return n != null && n >= 1 && n <= 12 ? n : null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function parseSimpleField(field: string, min: number, max: number): SimpleField | null {
|
|
143
|
+
if (field === '*') return {kind: 'any'};
|
|
144
|
+
|
|
145
|
+
// */n 与 0/n 都表示从 0 开始每隔 n
|
|
146
|
+
const step = field.match(/^(?:\*|0)\/(\d+)$/);
|
|
147
|
+
if (step) {
|
|
148
|
+
const n = Number(step[1]);
|
|
149
|
+
return n >= 1 && n <= max ? {kind: 'step', step: n} : null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const range = field.match(/^(\d+)-(\d+)$/);
|
|
153
|
+
if (range) {
|
|
154
|
+
const from = Number(range[1]);
|
|
155
|
+
const to = Number(range[2]);
|
|
156
|
+
return from >= min && to <= max && from <= to ? {kind: 'range', from, to} : null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (field.includes(',')) {
|
|
160
|
+
const values = field.split(',').map(toInt);
|
|
161
|
+
if (values.every(v => v != null && v >= min && v <= max)) {
|
|
162
|
+
return {kind: 'list', values: values as number[]};
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const n = toInt(field);
|
|
168
|
+
return n != null && n >= min && n <= max ? {kind: 'number', value: n} : null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function parseDom(field: string): DomField | null {
|
|
172
|
+
if (field === '*' || field === '?') return {kind: 'any'};
|
|
173
|
+
if (field === 'L') return {kind: 'last'};
|
|
174
|
+
if (field === 'LW') return {kind: 'lastWeekday'};
|
|
175
|
+
|
|
176
|
+
const lastOffset = field.match(/^L-(\d+)$/);
|
|
177
|
+
if (lastOffset) {
|
|
178
|
+
const offset = Number(lastOffset[1]);
|
|
179
|
+
return offset >= 1 && offset <= 30 ? {kind: 'lastOffset', offset} : null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const nearest = field.match(/^(\d+)W$/);
|
|
183
|
+
if (nearest) {
|
|
184
|
+
const day = Number(nearest[1]);
|
|
185
|
+
return day >= 1 && day <= 31 ? {kind: 'nearestWeekday', day} : null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const step = field.match(/^(?:\*|0)\/(\d+)$/);
|
|
189
|
+
if (step) {
|
|
190
|
+
const n = Number(step[1]);
|
|
191
|
+
return n >= 1 && n <= 31 ? {kind: 'step', step: n} : null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (field.includes(',')) {
|
|
195
|
+
const values = field.split(',').map(toInt);
|
|
196
|
+
if (values.every(v => v != null && v >= 1 && v <= 31)) {
|
|
197
|
+
return {kind: 'days', values: values as number[]};
|
|
198
|
+
}
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const range = field.match(/^(\d+)-(\d+)$/);
|
|
203
|
+
if (range) {
|
|
204
|
+
const from = Number(range[1]);
|
|
205
|
+
const to = Number(range[2]);
|
|
206
|
+
return from >= 1 && to <= 31 && from <= to ? {kind: 'range', from, to} : null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const day = toInt(field);
|
|
210
|
+
return day != null && day >= 1 && day <= 31 ? {kind: 'day', value: day} : null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function parseMonth(field: string): MonthField | null {
|
|
214
|
+
if (field === '*') return {kind: 'any'};
|
|
215
|
+
|
|
216
|
+
const range = field.match(/^([A-Za-z0-9]+)-([A-Za-z0-9]+)$/);
|
|
217
|
+
if (range) {
|
|
218
|
+
const from = monthNumber(range[1]);
|
|
219
|
+
const to = monthNumber(range[2]);
|
|
220
|
+
return from != null && to != null && from <= to ? {kind: 'range', from, to} : null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (field.includes(',')) {
|
|
224
|
+
const values = field.split(',').map(monthNumber);
|
|
225
|
+
if (values.every(v => v != null)) {
|
|
226
|
+
return {kind: 'months', values: values as number[]};
|
|
227
|
+
}
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const month = monthNumber(field);
|
|
232
|
+
return month != null ? {kind: 'month', value: month} : null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function parseDow(field: string): DowField | null {
|
|
236
|
+
if (field === '*' || field === '?') return {kind: 'any'};
|
|
237
|
+
|
|
238
|
+
const nth = field.match(/^([A-Za-z0-9]+)#(\d+)$/);
|
|
239
|
+
if (nth) {
|
|
240
|
+
const name = weekdayKey(nth[1]);
|
|
241
|
+
const n = Number(nth[2]);
|
|
242
|
+
return name && n >= 1 && n <= 5 ? {kind: 'nth', name, nth: n} : null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const last = field.match(/^([A-Za-z0-9]+)L$/);
|
|
246
|
+
if (last) {
|
|
247
|
+
const name = weekdayKey(last[1]);
|
|
248
|
+
return name ? {kind: 'last', name} : null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const range = field.match(/^([A-Za-z0-9]+)-([A-Za-z0-9]+)$/);
|
|
252
|
+
if (range) {
|
|
253
|
+
const from = weekdayKey(range[1]);
|
|
254
|
+
const to = weekdayKey(range[2]);
|
|
255
|
+
return from && to ? {kind: 'range', from, to} : null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (field.includes(',')) {
|
|
259
|
+
const names = field.split(',').map(weekdayKey);
|
|
260
|
+
if (names.every(Boolean)) {
|
|
261
|
+
return {kind: 'days', names: names as string[]};
|
|
262
|
+
}
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const name = weekdayKey(field);
|
|
267
|
+
return name ? {kind: 'day', name} : null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function pad2(n: number): string {
|
|
271
|
+
return String(n).padStart(2, '0');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function formatTime(hour: number, minute: number, second: number): string {
|
|
275
|
+
return `${pad2(hour)}:${pad2(minute)}:${pad2(second)}`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function weekdayCn(name: string): string {
|
|
279
|
+
return WEEKDAY_CN[name] ?? name;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function joinPrefix(prefix: string, text: string): string {
|
|
283
|
+
return /^\d/.test(text) ? `${prefix} ${text}` : `${prefix}${text}`;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function describeTimePoint(hour: SimpleField, minute: SimpleField, second: SimpleField): string | null {
|
|
287
|
+
if (minute.kind !== 'number' || second.kind !== 'number') return null;
|
|
288
|
+
if (hour.kind === 'number') return formatTime(hour.value, minute.value, second.value);
|
|
289
|
+
if (hour.kind === 'list') {
|
|
290
|
+
return hour.values.map(h => formatTime(h, minute.value, second.value)).join('、');
|
|
291
|
+
}
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function describeFrequency(hour: SimpleField, minute: SimpleField, second: SimpleField): string | null {
|
|
296
|
+
if (hour.kind === 'any') {
|
|
297
|
+
if (minute.kind === 'any') {
|
|
298
|
+
if (second.kind === 'any') return '每秒';
|
|
299
|
+
if (second.kind === 'step') return second.step === 1 ? '每秒' : `每隔 ${second.step} 秒`;
|
|
300
|
+
if (second.kind === 'number') {
|
|
301
|
+
return second.value === 0 ? '每分钟' : `每分钟的第 ${second.value} 秒`;
|
|
302
|
+
}
|
|
303
|
+
if (second.kind === 'list') return `每分钟的第 ${second.values.join('、')} 秒`;
|
|
304
|
+
if (second.kind === 'range') return `每分钟的第 ${second.from}-${second.to} 秒`;
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (minute.kind === 'step') {
|
|
309
|
+
if (second.kind !== 'number') return null;
|
|
310
|
+
return second.value === 0
|
|
311
|
+
? `每隔 ${minute.step} 分钟`
|
|
312
|
+
: `每隔 ${minute.step} 分钟的第 ${second.value} 秒`;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (minute.kind === 'number') {
|
|
316
|
+
if (second.kind === 'number') {
|
|
317
|
+
if (minute.value === 0 && second.value === 0) return '每小时整点';
|
|
318
|
+
if (second.value === 0) return `每小时的第 ${minute.value} 分`;
|
|
319
|
+
return `每小时的第 ${minute.value} 分 ${second.value} 秒`;
|
|
320
|
+
}
|
|
321
|
+
if (second.kind === 'list') {
|
|
322
|
+
return `每小时的第 ${minute.value} 分的第 ${second.values.join('、')} 秒`;
|
|
323
|
+
}
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (minute.kind === 'list' && second.kind === 'number') {
|
|
328
|
+
const minuteText = `每小时的第 ${minute.values.join('、')} 分`;
|
|
329
|
+
return second.value === 0 ? minuteText : `${minuteText}的第 ${second.value} 秒`;
|
|
330
|
+
}
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (hour.kind === 'step') {
|
|
335
|
+
if (minute.kind === 'number' && second.kind === 'number') {
|
|
336
|
+
return `每隔 ${hour.step} 小时的第 ${minute.value} 分 ${second.value} 秒`;
|
|
337
|
+
}
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function describeDayOfMonth(dom: Exclude<DomField, { kind: 'any' }>): string | null {
|
|
345
|
+
switch (dom.kind) {
|
|
346
|
+
case 'day':
|
|
347
|
+
return `${dom.value} 号`;
|
|
348
|
+
case 'days':
|
|
349
|
+
return `${dom.values.join('、')} 号`;
|
|
350
|
+
case 'range':
|
|
351
|
+
return `${dom.from}-${dom.to} 号`;
|
|
352
|
+
case 'step':
|
|
353
|
+
return `从 1 号起每隔 ${dom.step} 天`;
|
|
354
|
+
case 'last':
|
|
355
|
+
return '最后一天';
|
|
356
|
+
case 'lastOffset':
|
|
357
|
+
return `倒数第 ${dom.offset} 天`;
|
|
358
|
+
case 'lastWeekday':
|
|
359
|
+
return '最后一个工作日';
|
|
360
|
+
case 'nearestWeekday':
|
|
361
|
+
return `${dom.day} 号最近的工作日`;
|
|
362
|
+
default:
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function describeMonth(month: MonthField): string | null {
|
|
368
|
+
switch (month.kind) {
|
|
369
|
+
case 'month':
|
|
370
|
+
return `${month.value} 月`;
|
|
371
|
+
case 'months':
|
|
372
|
+
return `${month.values.join('、')} 月`;
|
|
373
|
+
case 'range':
|
|
374
|
+
return `${month.from}-${month.to} 月`;
|
|
375
|
+
default:
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function describeDate(dom: DomField, dow: DowField, month: MonthField): string | null {
|
|
381
|
+
const monthAny = month.kind === 'any';
|
|
382
|
+
const monthText = monthAny ? '' : describeMonth(month);
|
|
383
|
+
if (!monthAny && !monthText) return null;
|
|
384
|
+
|
|
385
|
+
if (dow.kind !== 'any') {
|
|
386
|
+
// 同时指定日期和星期在 Quartz 中不支持,日期必须是 ?
|
|
387
|
+
if (!monthAny || dom.kind !== 'any') return null;
|
|
388
|
+
switch (dow.kind) {
|
|
389
|
+
case 'day':
|
|
390
|
+
return `每${weekdayCn(dow.name)}`;
|
|
391
|
+
case 'days':
|
|
392
|
+
return `每${dow.names.map(weekdayCn).join('、')}`;
|
|
393
|
+
case 'range':
|
|
394
|
+
return `每${weekdayCn(dow.from)}至${weekdayCn(dow.to)}`;
|
|
395
|
+
case 'last':
|
|
396
|
+
return `每月最后一个${weekdayCn(dow.name)}`;
|
|
397
|
+
case 'nth':
|
|
398
|
+
return `每月第 ${dow.nth} 个${weekdayCn(dow.name)}`;
|
|
399
|
+
default:
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (dom.kind === 'any') {
|
|
405
|
+
return monthAny ? '每天' : `每年 ${monthText}的每天`;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const dayText = describeDayOfMonth(dom);
|
|
409
|
+
if (!dayText) return null;
|
|
410
|
+
return monthAny ? joinPrefix('每月', dayText) : joinPrefix(`每年 ${monthText}`, dayText);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export class CronUtils {
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* 生成中文说明;表达式非法或暂不支持时返回空字符串。
|
|
417
|
+
*/
|
|
418
|
+
static describe(expr: string | null | undefined): string {
|
|
419
|
+
const fields = splitFields(expr);
|
|
420
|
+
if (!fields) return '';
|
|
421
|
+
|
|
422
|
+
const [secText, minText, hourText, domText, monthText, dowText] = fields;
|
|
423
|
+
const second = parseSimpleField(secText, 0, 59);
|
|
424
|
+
const minute = parseSimpleField(minText, 0, 59);
|
|
425
|
+
const hour = parseSimpleField(hourText, 0, 23);
|
|
426
|
+
const dom = parseDom(domText);
|
|
427
|
+
const month = parseMonth(monthText);
|
|
428
|
+
const dow = parseDow(dowText);
|
|
429
|
+
if (!second || !minute || !hour || !dom || !month || !dow) return '';
|
|
430
|
+
|
|
431
|
+
const dateAny = dom.kind === 'any' && dow.kind === 'any' && month.kind === 'any';
|
|
432
|
+
const timePoint = describeTimePoint(hour, minute, second);
|
|
433
|
+
|
|
434
|
+
if (timePoint) {
|
|
435
|
+
if (dateAny) return `每天 ${timePoint}`;
|
|
436
|
+
const dateText = describeDate(dom, dow, month);
|
|
437
|
+
return dateText ? `${dateText} ${timePoint}` : '';
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (!dateAny) return '';
|
|
441
|
+
return describeFrequency(hour, minute, second) ?? '';
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/** 生成按间隔执行的表达式 */
|
|
445
|
+
static buildInterval(every: number, unit: CronIntervalUnit): string {
|
|
446
|
+
const n = Math.max(1, Math.floor(every || 1));
|
|
447
|
+
switch (unit) {
|
|
448
|
+
case 'second':
|
|
449
|
+
return n === 1 ? '* * * * * ?' : `*/${n} * * * * ?`;
|
|
450
|
+
case 'minute':
|
|
451
|
+
return n === 1 ? '0 * * * * ?' : `0 */${n} * * * ?`;
|
|
452
|
+
case 'hour':
|
|
453
|
+
return n === 1 ? '0 0 * * * ?' : `0 0 */${n} * * ?`;
|
|
454
|
+
case 'day':
|
|
455
|
+
return n === 1 ? '0 0 0 * * ?' : `0 0 0 */${n} * ?`;
|
|
456
|
+
default:
|
|
457
|
+
return '';
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** 生成按固定时间点执行的表达式 */
|
|
462
|
+
static buildSchedule(options: CronScheduleOptions): string {
|
|
463
|
+
const second = clamp(options.second ?? 0, 0, 59);
|
|
464
|
+
const minute = clamp(options.minute, 0, 59);
|
|
465
|
+
const hour = clamp(options.hour, 0, 23);
|
|
466
|
+
|
|
467
|
+
switch (options.period) {
|
|
468
|
+
case 'DAILY':
|
|
469
|
+
return `${second} ${minute} ${hour} * * ?`;
|
|
470
|
+
case 'WEEKLY':
|
|
471
|
+
return `${second} ${minute} ${hour} ? * ${options.weekday || 'MON'}`;
|
|
472
|
+
case 'MONTHLY': {
|
|
473
|
+
const day = options.dayOfMonth === 'L' ? 'L' : clamp(options.dayOfMonth ?? 1, 1, 31);
|
|
474
|
+
return `${second} ${minute} ${hour} ${day} * ?`;
|
|
475
|
+
}
|
|
476
|
+
case 'YEARLY': {
|
|
477
|
+
const day = clamp(options.dayOfMonth === 'L' ? 1 : (options.dayOfMonth ?? 1), 1, 31);
|
|
478
|
+
const month = clamp(options.month ?? 1, 1, 12);
|
|
479
|
+
return `${second} ${minute} ${hour} ${day} ${month} ?`;
|
|
480
|
+
}
|
|
481
|
+
default:
|
|
482
|
+
return '';
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/** 识别编辑器生成的常见表达式,用于打开弹窗时回填 */
|
|
487
|
+
static parse(expr: string | null | undefined): CronParsed | null {
|
|
488
|
+
const fields = splitFields(expr);
|
|
489
|
+
if (!fields) return null;
|
|
490
|
+
const text = fields.join(' ');
|
|
491
|
+
|
|
492
|
+
let match = text.match(/^(\*|\*\/(\d+)) \* \* \* \* \?$/);
|
|
493
|
+
if (match) {
|
|
494
|
+
return {mode: 'interval', every: match[1] === '*' ? 1 : Number(match[2]), unit: 'second'};
|
|
495
|
+
}
|
|
496
|
+
match = text.match(/^0 (\*|\*\/(\d+)) \* \* \* \?$/);
|
|
497
|
+
if (match) {
|
|
498
|
+
return {mode: 'interval', every: match[1] === '*' ? 1 : Number(match[2]), unit: 'minute'};
|
|
499
|
+
}
|
|
500
|
+
match = text.match(/^0 0 (\*|\*\/(\d+)) \* \* \?$/);
|
|
501
|
+
if (match) {
|
|
502
|
+
return {mode: 'interval', every: match[1] === '*' ? 1 : Number(match[2]), unit: 'hour'};
|
|
503
|
+
}
|
|
504
|
+
match = text.match(/^0 0 0 (\*|\*\/(\d+)) \* \?$/);
|
|
505
|
+
if (match) {
|
|
506
|
+
return {mode: 'interval', every: match[1] === '*' ? 1 : Number(match[2]), unit: 'day'};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
match = text.match(/^(\d+) (\d+) (\d+) \* \* \?$/);
|
|
510
|
+
if (match) {
|
|
511
|
+
return {
|
|
512
|
+
mode: 'schedule',
|
|
513
|
+
period: 'DAILY',
|
|
514
|
+
second: Number(match[1]),
|
|
515
|
+
minute: Number(match[2]),
|
|
516
|
+
hour: Number(match[3]),
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
match = text.match(/^(\d+) (\d+) (\d+) \? \* ([A-Za-z0-9]+)$/);
|
|
520
|
+
if (match) {
|
|
521
|
+
const weekday = weekdayKey(match[4]);
|
|
522
|
+
if (weekday) {
|
|
523
|
+
return {
|
|
524
|
+
mode: 'schedule',
|
|
525
|
+
period: 'WEEKLY',
|
|
526
|
+
second: Number(match[1]),
|
|
527
|
+
minute: Number(match[2]),
|
|
528
|
+
hour: Number(match[3]),
|
|
529
|
+
weekday,
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
match = text.match(/^(\d+) (\d+) (\d+) (L|\d+) \* \?$/);
|
|
534
|
+
if (match) {
|
|
535
|
+
return {
|
|
536
|
+
mode: 'schedule',
|
|
537
|
+
period: 'MONTHLY',
|
|
538
|
+
second: Number(match[1]),
|
|
539
|
+
minute: Number(match[2]),
|
|
540
|
+
hour: Number(match[3]),
|
|
541
|
+
dayOfMonth: match[4] === 'L' ? 'L' : Number(match[4]),
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
match = text.match(/^(\d+) (\d+) (\d+) (\d+) (\d+) \?$/);
|
|
545
|
+
if (match) {
|
|
546
|
+
return {
|
|
547
|
+
mode: 'schedule',
|
|
548
|
+
period: 'YEARLY',
|
|
549
|
+
second: Number(match[1]),
|
|
550
|
+
minute: Number(match[2]),
|
|
551
|
+
hour: Number(match[3]),
|
|
552
|
+
dayOfMonth: Number(match[4]),
|
|
553
|
+
month: Number(match[5]),
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
return null;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function clamp(value: number, min: number, max: number): number {
|
|
561
|
+
return Math.min(max, Math.max(min, Math.floor(value)));
|
|
562
|
+
}
|
|
@@ -21,13 +21,13 @@ import {getMessageApi} from "./MessageApi";
|
|
|
21
21
|
* 用法:
|
|
22
22
|
* ```ts
|
|
23
23
|
* // 提交:成功自动弹后端 message(如"更新成功"),失败自动弹错并 reject(可用 await 感知结果,如 FormModal 提交失败保持弹窗)
|
|
24
|
-
* await HttpClient.post('admin/
|
|
24
|
+
* await HttpClient.post('admin/sys-user/create', values);
|
|
25
25
|
*
|
|
26
26
|
* // 取返回数据与后端提示文案
|
|
27
|
-
* const {data, message} = await HttpClient.post('admin/
|
|
27
|
+
* const {data, message} = await HttpClient.post('admin/sys-user/create', values);
|
|
28
28
|
*
|
|
29
29
|
* // 成功自控提示(关闭自动弹后由业务决定)
|
|
30
|
-
* await HttpClient.post('admin/
|
|
30
|
+
* await HttpClient.post('admin/sys-user/delete', {id}, null, {toastSuccess: false});
|
|
31
31
|
* messageApi.success('删除成功');
|
|
32
32
|
* reload();
|
|
33
33
|
*
|
|
@@ -37,10 +37,10 @@ import {getMessageApi} from "./MessageApi";
|
|
|
37
37
|
* .catch((e) => myHandle(e));
|
|
38
38
|
*
|
|
39
39
|
* // $.ajax 形态
|
|
40
|
-
* HttpClient.ajax({url: 'admin/
|
|
40
|
+
* HttpClient.ajax({url: 'admin/sys-user/page', method: 'GET', params}).then((rs) => render(rs.data));
|
|
41
41
|
*
|
|
42
42
|
* // 下载(返回 Promise<Blob>,成功已触发浏览器保存)
|
|
43
|
-
* HttpClient.download({url: 'admin/
|
|
43
|
+
* HttpClient.download({url: 'admin/sys-user/export', method: 'POST', data: {deptId: 1},
|
|
44
44
|
* onDownloadProgress: ({loaded, total}) => console.log(loaded, total)})
|
|
45
45
|
* .catch(() => {});
|
|
46
46
|
* ```
|
|
@@ -2,15 +2,33 @@ import {history} from '../router';
|
|
|
2
2
|
import {StringUtils} from "./StringUtils";
|
|
3
3
|
import {UrlUtils} from "./UrlUtils";
|
|
4
4
|
import {EventBus} from "./EventBus";
|
|
5
|
+
import {getPageTitle} from "./TitleUtils";
|
|
6
|
+
|
|
7
|
+
/** 登录页路径:公开页面,支持 ?redirect= 回跳 */
|
|
8
|
+
export const LOGIN_PATH = '/login';
|
|
9
|
+
|
|
10
|
+
let labelDeprecatedWarned = false;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @internal
|
|
14
|
+
* `_label` 参数已弃用:首次被使用时提示一次,方便业务项目迁移
|
|
15
|
+
*/
|
|
16
|
+
export function warnDeprecatedPageLabel(): void {
|
|
17
|
+
if (labelDeprecatedWarned) return;
|
|
18
|
+
labelDeprecatedWarned = true;
|
|
19
|
+
console.warn(
|
|
20
|
+
'[open-admin] 检测到已弃用的 _label 参数:该参数已不再生效,请改用页面文件中的 pageMeta.title。'
|
|
21
|
+
);
|
|
22
|
+
}
|
|
5
23
|
|
|
6
24
|
export class PageUtils {
|
|
7
25
|
static redirectToLogin(): void {
|
|
8
|
-
if (PageUtils.currentPathname() ===
|
|
26
|
+
if (PageUtils.currentPathname() === LOGIN_PATH) {
|
|
9
27
|
return;
|
|
10
28
|
}
|
|
11
29
|
const url = PageUtils.currentUrl();
|
|
12
30
|
const encodedUrl = encodeURIComponent(url);
|
|
13
|
-
history.push(
|
|
31
|
+
history.push(`${LOGIN_PATH}?redirect=${encodedUrl}`);
|
|
14
32
|
}
|
|
15
33
|
|
|
16
34
|
static currentParams(): Record<string, string | undefined> {
|
|
@@ -31,16 +49,29 @@ export class PageUtils {
|
|
|
31
49
|
return StringUtils.subAfterLast(path, '/') || undefined;
|
|
32
50
|
}
|
|
33
51
|
|
|
34
|
-
|
|
35
|
-
|
|
52
|
+
/**
|
|
53
|
+
* 打开页面并切换到对应 Tab
|
|
54
|
+
* @param path 页面路径
|
|
55
|
+
* @param label @deprecated 已不再生效,仅用于提示迁移;页面标题请使用 pageMeta.title
|
|
56
|
+
*/
|
|
57
|
+
static open(path: string, label?: string): void {
|
|
36
58
|
if (label) {
|
|
37
|
-
|
|
59
|
+
warnDeprecatedPageLabel();
|
|
38
60
|
}
|
|
39
|
-
history.push(
|
|
61
|
+
history.push(path);
|
|
40
62
|
}
|
|
41
63
|
|
|
64
|
+
/**
|
|
65
|
+
* @deprecated `_label` 已不再生效,页面标题请使用 pageMeta.title 或 {@link PageUtils.currentTitle}
|
|
66
|
+
*/
|
|
42
67
|
static currentLabel(): string | undefined {
|
|
43
|
-
|
|
68
|
+
warnDeprecatedPageLabel();
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 获取当前页面标题(不含系统标题);没有页面标题时返回 undefined */
|
|
73
|
+
static currentTitle(): string | undefined {
|
|
74
|
+
return getPageTitle();
|
|
44
75
|
}
|
|
45
76
|
|
|
46
77
|
static closeCurrent(): void {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {GlobalData} from '../GlobalData';
|
|
2
|
+
|
|
3
|
+
let currentPageTitle: string | undefined;
|
|
4
|
+
|
|
5
|
+
function normalize(title?: string): string | undefined {
|
|
6
|
+
return typeof title === 'string' && title.trim() ? title.trim() : undefined;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function systemTitle(): string {
|
|
10
|
+
const title = GlobalData.getSiteInfo()?.title;
|
|
11
|
+
return typeof title === 'string' && title.trim() ? title.trim() : 'open-admin';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function renderDocumentTitle(): void {
|
|
15
|
+
const next = currentPageTitle ? `${systemTitle()} | ${currentPageTitle}` : systemTitle();
|
|
16
|
+
if (document.title !== next) {
|
|
17
|
+
document.title = next;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 设置当前页面标题(不含系统标题);传空只显示系统标题。
|
|
23
|
+
* 浏览器标题格式:`系统标题 | 页面标题`
|
|
24
|
+
*/
|
|
25
|
+
export function setPageTitle(title?: string): void {
|
|
26
|
+
currentPageTitle = normalize(title);
|
|
27
|
+
renderDocumentTitle();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 站点标题变化后重新渲染文档标题 */
|
|
31
|
+
export function refreshDocumentTitle(): void {
|
|
32
|
+
renderDocumentTitle();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 当前页面标题(不含系统标题);未设置时返回 undefined */
|
|
36
|
+
export function getPageTitle(): string | undefined {
|
|
37
|
+
return currentPageTitle;
|
|
38
|
+
}
|