@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
package/package.json
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"less": "^4.2.0",
|
|
16
16
|
"typescript": "^5.9.3",
|
|
17
17
|
"typescript-eslint": "^8.67.0",
|
|
18
|
-
"vite": "^8.
|
|
18
|
+
"vite": "^8.3.3"
|
|
19
19
|
},
|
|
20
20
|
"peerDependencies": {
|
|
21
21
|
"@ant-design/icons": "^6.0.0",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"cropperjs": "^1.6.2",
|
|
27
27
|
"dayjs": "^1.11.13",
|
|
28
28
|
"lodash": "^4.17.23",
|
|
29
|
-
"qs": "^6.
|
|
29
|
+
"qs": "^6.16.1",
|
|
30
30
|
"react": "^19.0.0",
|
|
31
31
|
"react-dom": "^19.0.0"
|
|
32
32
|
},
|
|
@@ -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.
|
|
58
|
+
"version": "3.1.12",
|
|
59
59
|
"overrides": {
|
|
60
60
|
"zod-validation-error": "^4.0.2"
|
|
61
61
|
}
|
|
@@ -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
|
|
|
@@ -20,6 +22,8 @@ export interface LoginInfo {
|
|
|
20
22
|
account?: string;
|
|
21
23
|
phone?: string;
|
|
22
24
|
email?: string;
|
|
25
|
+
/** 头像文件 objectName,页面按 /file/{avatar} 访问 */
|
|
26
|
+
avatar?: string;
|
|
23
27
|
unit?: string;
|
|
24
28
|
dept?: string;
|
|
25
29
|
roles?: string;
|
|
@@ -22,7 +22,7 @@ export class OrgTree extends React.Component<OrgTreeProps, OrgTreeState> {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
componentDidMount() {
|
|
25
|
-
HttpClient.get('admin/
|
|
25
|
+
HttpClient.get('admin/sys-org/tree', null).then(tree => {
|
|
26
26
|
this.setState({treeData: tree.data, treeDataLoading: false})
|
|
27
27
|
}).catch(() => {
|
|
28
28
|
// 失败时结束 loading 并置空(组件会显示空状态提示),避免一直停留在骨架屏;
|
|
@@ -22,7 +22,7 @@ export class RoleTree extends React.Component<RoleTreeProps, RoleTreeState> {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
componentDidMount() {
|
|
25
|
-
HttpClient.get('admin/
|
|
25
|
+
HttpClient.get('admin/sys-role/biz-tree', null).then(tree => {
|
|
26
26
|
this.setState({treeData: tree.data, treeDataLoading: false})
|
|
27
27
|
}).catch(() => {
|
|
28
28
|
// 失败时结束 loading 并置空(组件会显示空状态提示),避免一直停留在骨架屏;
|
|
@@ -20,9 +20,9 @@ export interface UserPickerProps {
|
|
|
20
20
|
width?: number;
|
|
21
21
|
/** 内容区高度,默认 530(刚好容纳默认每页 10 行,不出现表格内部纵向滚动) */
|
|
22
22
|
height?: number;
|
|
23
|
-
/** 分页数据源地址,默认 admin/
|
|
23
|
+
/** 分页数据源地址,默认 admin/sys-user/page */
|
|
24
24
|
requestUrl?: string;
|
|
25
|
-
/** 按 id 查询用户详情地址(用于回显),默认 admin/
|
|
25
|
+
/** 按 id 查询用户详情地址(用于回显),默认 admin/sys-user/by-ids */
|
|
26
26
|
usersUrl?: string;
|
|
27
27
|
/** 确定按钮 loading */
|
|
28
28
|
confirmLoading?: boolean;
|
|
@@ -88,9 +88,9 @@ export class UserPicker extends React.Component<UserPickerProps, UserPickerState
|
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
getRequestUrl = () => this.props.requestUrl || 'admin/
|
|
91
|
+
getRequestUrl = () => this.props.requestUrl || 'admin/sys-user/page';
|
|
92
92
|
|
|
93
|
-
getUsersUrl = () => this.props.usersUrl || 'admin/
|
|
93
|
+
getUsersUrl = () => this.props.usersUrl || 'admin/sys-user/by-ids';
|
|
94
94
|
|
|
95
95
|
init = () => {
|
|
96
96
|
this.formRef.current?.resetFields();
|
|
@@ -11,22 +11,22 @@ export * from './pickerUser'
|
|
|
11
11
|
|
|
12
12
|
class InnerFieldUserSelect extends React.Component<ComponentProps<typeof FieldRemoteSelect>> {
|
|
13
13
|
render() {
|
|
14
|
-
return <FieldRemoteSelect url="admin/
|
|
14
|
+
return <FieldRemoteSelect url="admin/sys-user/options" {...this.props} />;
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
class InnerFieldUserSelectMultiple extends React.Component<ComponentProps<typeof FieldRemoteSelect>> {
|
|
18
18
|
render() {
|
|
19
|
-
return <FieldRemoteSelect url="admin/
|
|
19
|
+
return <FieldRemoteSelect url="admin/sys-user/options" multiple {...this.props} />;
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
class InnerFieldUnitTreeSelect extends React.Component<ComponentProps<typeof FieldRemoteTreeSelect>> {
|
|
23
23
|
render() {
|
|
24
|
-
return <FieldRemoteTreeSelect url="admin/
|
|
24
|
+
return <FieldRemoteTreeSelect url="admin/sys-org/unit-tree" {...this.props} />;
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
class InnerFieldDeptTreeSelect extends React.Component<ComponentProps<typeof FieldRemoteTreeSelect>> {
|
|
28
28
|
render() {
|
|
29
|
-
return <FieldRemoteTreeSelect url="admin/
|
|
29
|
+
return <FieldRemoteTreeSelect url="admin/sys-org/dept-tree" {...this.props} />;
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
class InnerFieldOrgTreeMultipleSelect extends React.Component<ComponentProps<typeof FieldRemoteTreeSelect>> {
|
|
@@ -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
|
+
}
|
|
@@ -2,6 +2,10 @@ import React from "react";
|
|
|
2
2
|
import {Button} from "antd";
|
|
3
3
|
import {PageUtils} from "../../utils";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* 链接跳转按钮。
|
|
7
|
+
* `label` 参数已弃用:目标页面的标题请使用 pageMeta.title。
|
|
8
|
+
*/
|
|
5
9
|
export function LinkButton(props) {
|
|
6
10
|
const {path, label, children, size = 'small', ...rest} = props;
|
|
7
11
|
return <Button size={size} {...rest} onClick={() => {
|
|
@@ -18,7 +18,7 @@ export class OrgSwitcher extends React.Component<Record<string, never>, {tree: O
|
|
|
18
18
|
state = {tree: [], currentOrgId: null};
|
|
19
19
|
|
|
20
20
|
componentDidMount() {
|
|
21
|
-
HttpClient.get("admin/
|
|
21
|
+
HttpClient.get("admin/my-orgs", null).then(data => {
|
|
22
22
|
if (data.data) {
|
|
23
23
|
this.setState({tree: data.data.tree || [], currentOrgId: data.data.currentOrgId || null});
|
|
24
24
|
}
|
|
@@ -26,7 +26,7 @@ export class OrgSwitcher extends React.Component<Record<string, never>, {tree: O
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
handleChange = (orgId: string) => {
|
|
29
|
-
HttpClient.post("admin/
|
|
29
|
+
HttpClient.post("admin/switch-org", {orgId}, null).then(() => {
|
|
30
30
|
setTimeout(() => location.reload(), 1000);
|
|
31
31
|
});
|
|
32
32
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { Space
|
|
2
|
+
import { Space } from "antd";
|
|
3
3
|
import './index.less'
|
|
4
4
|
import { PermActions, type PermAction } from "../../biz/PermActions";
|
|
5
5
|
import { PermUtils } from "../../utils";
|
|
@@ -19,8 +19,6 @@ interface PageProps {
|
|
|
19
19
|
/** @deprecated 页面底色已默认跟随布局底色,该参数不再产生差异;保留仅为兼容旧写法 */
|
|
20
20
|
backgroundGray?: boolean;
|
|
21
21
|
debug?: boolean;
|
|
22
|
-
title?: React.ReactNode;
|
|
23
|
-
description?: React.ReactNode;
|
|
24
22
|
/** 右侧操作区:推荐传 PermAction[](数据驱动),兼容带 perm 属性的节点 */
|
|
25
23
|
actions?: PermAction[] | React.ReactNode;
|
|
26
24
|
children?: React.ReactNode;
|
|
@@ -28,8 +26,7 @@ interface PageProps {
|
|
|
28
26
|
|
|
29
27
|
export class Page extends React.Component<PageProps> {
|
|
30
28
|
render() {
|
|
31
|
-
const { padding = true, debug = false,
|
|
32
|
-
const hasHeader = title != null || description != null || actions != null;
|
|
29
|
+
const { padding = true, debug = false, actions, children } = this.props;
|
|
33
30
|
const style: React.CSSProperties = {};
|
|
34
31
|
|
|
35
32
|
if (padding) {
|
|
@@ -41,18 +38,13 @@ export class Page extends React.Component<PageProps> {
|
|
|
41
38
|
}
|
|
42
39
|
|
|
43
40
|
return <div className={'oa-page'} style={style}>
|
|
44
|
-
{
|
|
41
|
+
{actions != null && (
|
|
45
42
|
<div className="oa-page-header">
|
|
46
43
|
<div className="oa-page-header-top">
|
|
47
|
-
<div className="oa-page-header-left"
|
|
48
|
-
|
|
49
|
-
{title && <Typography.Title level={5} style={{ margin: 0 }}>{title}</Typography.Title>}
|
|
50
|
-
{description && <div><Typography.Text type="secondary">{description}</Typography.Text></div>}
|
|
51
|
-
</div>
|
|
52
|
-
</div>
|
|
53
|
-
{actions != null && (Array.isArray(actions)
|
|
44
|
+
<div className="oa-page-header-left"/>
|
|
45
|
+
{Array.isArray(actions)
|
|
54
46
|
? <PermActions actions={actions}/>
|
|
55
|
-
: renderActionNodes(actions)
|
|
47
|
+
: renderActionNodes(actions)}
|
|
56
48
|
</div>
|
|
57
49
|
</div>
|
|
58
50
|
)}
|
|
@@ -71,6 +71,8 @@ export interface ProTableProps<T = unknown> {
|
|
|
71
71
|
bordered?: boolean;
|
|
72
72
|
/** 树形数据模式,关闭分页且不传 page/size 参数 */
|
|
73
73
|
treeMode?: boolean;
|
|
74
|
+
/** 无数据时的提示文案,默认“暂无数据” */
|
|
75
|
+
emptyText?: React.ReactNode;
|
|
74
76
|
}
|
|
75
77
|
|
|
76
78
|
interface ProTableState<T = unknown> {
|
|
@@ -256,7 +258,7 @@ this.setState({loading: true})
|
|
|
256
258
|
bordered={this.props.bordered ?? false}
|
|
257
259
|
locale={{
|
|
258
260
|
emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE}
|
|
259
|
-
description={hasFilter ? '没有符合条件的数据' : '暂无数据'}/>,
|
|
261
|
+
description={hasFilter ? '没有符合条件的数据' : (this.props.emptyText ?? '暂无数据')}/>,
|
|
260
262
|
}}
|
|
261
263
|
/>
|
|
262
264
|
</div>
|
|
@@ -7,19 +7,15 @@ import {
|
|
|
7
7
|
DEFAULT_SIDER_BG,
|
|
8
8
|
applyThemePreset,
|
|
9
9
|
getActivePresetKey,
|
|
10
|
-
getThemeMode,
|
|
11
10
|
resetAllSettings,
|
|
12
|
-
setThemeMode,
|
|
13
11
|
DENSITY_PRESETS,
|
|
14
12
|
RADIUS_PRESETS,
|
|
15
13
|
applyDensityPreset,
|
|
16
14
|
getActiveDensityKey,
|
|
17
15
|
applyRadiusPreset,
|
|
18
16
|
getActiveRadiusKey,
|
|
19
|
-
getLayoutMode,
|
|
20
|
-
setLayoutMode,
|
|
21
17
|
} from '../../config';
|
|
22
|
-
import type {
|
|
18
|
+
import type { ThemeDensity, ThemeRadius } from '../../config';
|
|
23
19
|
import { EventBus } from '../../utils/EventBus';
|
|
24
20
|
|
|
25
21
|
export interface ThemeSettingsProps {
|
|
@@ -32,29 +28,23 @@ function SectionTitle({ children }: { children: React.ReactNode }) {
|
|
|
32
28
|
}
|
|
33
29
|
|
|
34
30
|
/**
|
|
35
|
-
*
|
|
31
|
+
* 界面设置面板:主题色、界面密度、圆角。
|
|
32
|
+
* 明暗模式由顶栏的太阳/月亮按钮切换,此处不再重复提供。
|
|
36
33
|
* 所有变更即时生效并持久化到 localStorage。
|
|
37
34
|
*/
|
|
38
35
|
export function ThemeSettings({ open, onClose }: ThemeSettingsProps) {
|
|
39
36
|
const [activeKey, setActiveKey] = React.useState(getActivePresetKey());
|
|
40
|
-
const [mode, setMode] = React.useState<ThemeMode>(getThemeMode());
|
|
41
37
|
const [density, setDensity] = React.useState<ThemeDensity>(getActiveDensityKey());
|
|
42
38
|
const [radius, setRadius] = React.useState<ThemeRadius>(getActiveRadiusKey());
|
|
43
|
-
const [layout, setLayout] = React.useState<LayoutMode>(getLayoutMode());
|
|
44
39
|
|
|
45
40
|
React.useEffect(() => {
|
|
46
41
|
const offTheme = EventBus.on('themeChange', () => {
|
|
47
42
|
setActiveKey(getActivePresetKey());
|
|
48
|
-
setMode(getThemeMode());
|
|
49
43
|
setDensity(getActiveDensityKey());
|
|
50
44
|
setRadius(getActiveRadiusKey());
|
|
51
45
|
});
|
|
52
|
-
const offLayout = EventBus.on('layoutChange', () => {
|
|
53
|
-
setLayout(getLayoutMode());
|
|
54
|
-
});
|
|
55
46
|
return () => {
|
|
56
47
|
offTheme();
|
|
57
|
-
offLayout();
|
|
58
48
|
};
|
|
59
49
|
}, []);
|
|
60
50
|
|
|
@@ -63,20 +53,8 @@ export function ThemeSettings({ open, onClose }: ThemeSettingsProps) {
|
|
|
63
53
|
|
|
64
54
|
return (
|
|
65
55
|
<Drawer title="界面设置" open={open} onClose={onClose} size={340}>
|
|
66
|
-
<div style={{ fontWeight: 600 }}
|
|
67
|
-
<
|
|
68
|
-
block
|
|
69
|
-
style={{ marginTop: 12 }}
|
|
70
|
-
value={mode}
|
|
71
|
-
onChange={value => setThemeMode(value as ThemeMode)}
|
|
72
|
-
options={[
|
|
73
|
-
{ label: '浅色', value: 'light' },
|
|
74
|
-
{ label: '深色', value: 'dark' },
|
|
75
|
-
]}
|
|
76
|
-
/>
|
|
77
|
-
|
|
78
|
-
<SectionTitle>主题色</SectionTitle>
|
|
79
|
-
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 12 }}>
|
|
56
|
+
<div style={{ fontWeight: 600 }}>主题色</div>
|
|
57
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 12, marginTop: 12 }}>
|
|
80
58
|
{THEME_PRESETS.map(preset => {
|
|
81
59
|
const primary = preset.colors.colorPrimary || DEFAULT_COLORS.colorPrimary || '#1677ff';
|
|
82
60
|
const sider = preset.colors.siderBg || DEFAULT_SIDER_BG;
|
|
@@ -152,17 +130,6 @@ export function ThemeSettings({ open, onClose }: ThemeSettingsProps) {
|
|
|
152
130
|
}))}
|
|
153
131
|
/>
|
|
154
132
|
|
|
155
|
-
<SectionTitle>界面布局</SectionTitle>
|
|
156
|
-
<Segmented
|
|
157
|
-
block
|
|
158
|
-
value={layout}
|
|
159
|
-
onChange={value => setLayoutMode(value as LayoutMode)}
|
|
160
|
-
options={[
|
|
161
|
-
{ label: '多标签页', value: 'tabs' },
|
|
162
|
-
{ label: '单页', value: 'single' },
|
|
163
|
-
]}
|
|
164
|
-
/>
|
|
165
|
-
|
|
166
133
|
<Button block style={{ marginTop: 20 }} onClick={() => resetAllSettings()}>
|
|
167
134
|
恢复默认
|
|
168
135
|
</Button>
|
|
@@ -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'
|
|
@@ -8,6 +8,9 @@ export {
|
|
|
8
8
|
toggleThemeMode,
|
|
9
9
|
applyThemePreset,
|
|
10
10
|
getActivePresetKey,
|
|
11
|
+
getDefaultPresetKey,
|
|
12
|
+
setDefaultThemePreset,
|
|
13
|
+
hasStoredPreset,
|
|
11
14
|
applyDensityPreset,
|
|
12
15
|
getActiveDensityKey,
|
|
13
16
|
applyRadiusPreset,
|
|
@@ -27,13 +30,22 @@ export type { ThemeColors, ThemeMode, ThemePreset, ThemeDensity, ThemeRadius, Th
|
|
|
27
30
|
|
|
28
31
|
export {
|
|
29
32
|
getLayoutMode,
|
|
33
|
+
getDefaultLayoutMode,
|
|
30
34
|
setLayoutMode,
|
|
35
|
+
setDefaultLayoutMode,
|
|
31
36
|
toggleLayoutMode,
|
|
32
37
|
resetLayoutMode,
|
|
38
|
+
hasStoredLayoutMode,
|
|
33
39
|
DEFAULT_LAYOUT_MODE,
|
|
34
40
|
} from './layoutConfig'
|
|
35
41
|
export type { LayoutMode } from './layoutConfig'
|
|
36
42
|
|
|
43
|
+
export {
|
|
44
|
+
applyServerUiDefaults,
|
|
45
|
+
UI_THEME_PRESET_KEY,
|
|
46
|
+
UI_TABS_ENABLED_KEY,
|
|
47
|
+
} from './uiDefaults'
|
|
48
|
+
|
|
37
49
|
import { resetUserTheme } from './themeConfig'
|
|
38
50
|
import { resetLayoutMode } from './layoutConfig'
|
|
39
51
|
|