@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,335 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
Card,
|
|
5
|
+
Col,
|
|
6
|
+
Form,
|
|
7
|
+
Input,
|
|
8
|
+
InputNumber,
|
|
9
|
+
Popconfirm,
|
|
10
|
+
Row,
|
|
11
|
+
Select,
|
|
12
|
+
Space,
|
|
13
|
+
Switch,
|
|
14
|
+
Tag,
|
|
15
|
+
Tooltip,
|
|
16
|
+
Typography,
|
|
17
|
+
message
|
|
18
|
+
} from 'antd'
|
|
19
|
+
import {QuestionCircleOutlined, UndoOutlined} from '@ant-design/icons'
|
|
20
|
+
import {EventBus, GlobalData, HttpClient, Page, Perm} from '../../../framework'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 参数设置页。
|
|
24
|
+
*
|
|
25
|
+
* 配置项由后端 ConfigProvider 声明(分组 / 类型 / 默认值 / 范围 / 说明),
|
|
26
|
+
* 本页面只负责按元数据渲染、收集修改项并提交差异。
|
|
27
|
+
*/
|
|
28
|
+
export default class SysConfigPage extends React.Component {
|
|
29
|
+
|
|
30
|
+
state = {
|
|
31
|
+
groups: [],
|
|
32
|
+
loading: true,
|
|
33
|
+
saving: false,
|
|
34
|
+
testingMail: false,
|
|
35
|
+
dirty: false,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
formRef = React.createRef()
|
|
39
|
+
|
|
40
|
+
componentDidMount() {
|
|
41
|
+
this.load()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 多 Tab 下页面保持 mounted,切回时重新拉取;有未保存修改时不覆盖用户输入 */
|
|
45
|
+
onShow = () => {
|
|
46
|
+
if (this.state.dirty || this.state.loading) {
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
this.load()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
load = async () => {
|
|
53
|
+
this.setState({loading: true})
|
|
54
|
+
try {
|
|
55
|
+
const rs = await HttpClient.get('admin/sys-config/items')
|
|
56
|
+
this.applyGroups(rs.data?.groups || [])
|
|
57
|
+
} catch {
|
|
58
|
+
this.setState({loading: false})
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 用后端返回值更新页面,不触发整页 loading。
|
|
64
|
+
* syncKeys 为空时同步全部字段;否则只同步指定项,保留其他未保存的修改。
|
|
65
|
+
*/
|
|
66
|
+
applyGroups = (groups, syncKeys = null) => {
|
|
67
|
+
this.setState({groups, loading: false}, () => {
|
|
68
|
+
const form = this.formRef.current
|
|
69
|
+
if (!form) {
|
|
70
|
+
this.setState({dirty: false})
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
const target = syncKeys ? new Set(syncKeys) : null
|
|
74
|
+
groups.forEach(group => {
|
|
75
|
+
group.items.forEach(item => {
|
|
76
|
+
if (!target || target.has(item.key)) {
|
|
77
|
+
form.setFieldValue([item.key], item.secret ? undefined : item.value)
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
this.setState({dirty: Object.keys(this.buildPatch(form.getFieldsValue())).length > 0})
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** 只收集与当前生效值不同的项 */
|
|
86
|
+
buildPatch = (values) => {
|
|
87
|
+
const items = new Map()
|
|
88
|
+
this.state.groups.forEach(group => {
|
|
89
|
+
group.items.forEach(item => items.set(item.key, item))
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const patch = {}
|
|
93
|
+
Object.entries(values || {}).forEach(([key, value]) => {
|
|
94
|
+
const item = items.get(key)
|
|
95
|
+
if (!item || value === undefined) {
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
// 敏感值留空表示不修改
|
|
99
|
+
if (item.secret && (value === null || value === '')) {
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
const before = item.value
|
|
103
|
+
const same = Array.isArray(value) || Array.isArray(before)
|
|
104
|
+
? JSON.stringify(value ?? []) === JSON.stringify(before ?? [])
|
|
105
|
+
: value === before
|
|
106
|
+
if (!same) {
|
|
107
|
+
patch[key] = value
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
return patch
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
onValuesChange = (_, allValues) => {
|
|
114
|
+
const dirty = Object.keys(this.buildPatch(allValues)).length > 0
|
|
115
|
+
if (dirty !== this.state.dirty) {
|
|
116
|
+
this.setState({dirty})
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
save = async () => {
|
|
121
|
+
const form = this.formRef.current
|
|
122
|
+
if (!form) {
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
const patch = this.buildPatch(form.getFieldsValue())
|
|
126
|
+
const values = Object.entries(patch).map(([key, value]) => ({key, value}))
|
|
127
|
+
if (values.length === 0) {
|
|
128
|
+
message.info('没有需要保存的修改')
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
this.setState({saving: true})
|
|
133
|
+
try {
|
|
134
|
+
const rs = await HttpClient.post('admin/sys-config/update', {values})
|
|
135
|
+
this.applyGroups(rs.data?.groups || [], Object.keys(patch))
|
|
136
|
+
await this.refreshSiteInfo()
|
|
137
|
+
} catch {
|
|
138
|
+
// 错误提示由 HttpClient 统一处理
|
|
139
|
+
} finally {
|
|
140
|
+
this.setState({saving: false})
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** 用已保存的配置发一封测试邮件到发件邮箱自身 */
|
|
145
|
+
sendTestMail = async () => {
|
|
146
|
+
this.setState({testingMail: true})
|
|
147
|
+
try {
|
|
148
|
+
// 成功提示由后端 message 自动弹出
|
|
149
|
+
await HttpClient.post('admin/mail/test')
|
|
150
|
+
} catch {
|
|
151
|
+
// 错误提示由 HttpClient 统一处理
|
|
152
|
+
} finally {
|
|
153
|
+
this.setState({testingMail: false})
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
resetKeys = async (keys) => {
|
|
158
|
+
try {
|
|
159
|
+
const rs = await HttpClient.post('admin/sys-config/reset', {keys})
|
|
160
|
+
this.applyGroups(rs.data?.groups || [], keys && keys.length ? keys : null)
|
|
161
|
+
await this.refreshSiteInfo()
|
|
162
|
+
} catch {
|
|
163
|
+
// 错误提示由 HttpClient 统一处理
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** 站点标题、水印等配置经 site-info 下发,保存后刷新本地缓存并通知布局 */
|
|
168
|
+
refreshSiteInfo = async () => {
|
|
169
|
+
try {
|
|
170
|
+
const rs = await HttpClient.get('/admin/public/site-info', null, {toastError: false})
|
|
171
|
+
const next = rs.data || {}
|
|
172
|
+
const changed = JSON.stringify(GlobalData.getSiteInfo()) !== JSON.stringify(next)
|
|
173
|
+
GlobalData.setSiteInfo(next)
|
|
174
|
+
if (changed) {
|
|
175
|
+
EventBus.emit('siteInfoChange')
|
|
176
|
+
}
|
|
177
|
+
} catch {
|
|
178
|
+
// site-info 刷新失败不影响配置保存结果
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
controlOf = (item) => {
|
|
183
|
+
switch (item.type) {
|
|
184
|
+
case 'bool':
|
|
185
|
+
return <Switch/>
|
|
186
|
+
case 'int':
|
|
187
|
+
return <InputNumber style={{width: '100%'}} precision={0} step={1}
|
|
188
|
+
min={item.min ?? undefined} max={item.max ?? undefined}/>
|
|
189
|
+
case 'float':
|
|
190
|
+
return <InputNumber style={{width: '100%'}} step={0.01}
|
|
191
|
+
min={item.min ?? undefined} max={item.max ?? undefined}/>
|
|
192
|
+
case 'time':
|
|
193
|
+
return <Input placeholder="HH:mm" style={{width: '100%'}}/>
|
|
194
|
+
case 'list':
|
|
195
|
+
return <Select mode="tags" tokenSeparators={[',']} style={{width: '100%'}}
|
|
196
|
+
placeholder="输入后回车,或粘贴逗号分隔的内容"/>
|
|
197
|
+
case 'select':
|
|
198
|
+
return <Select style={{width: '100%'}}
|
|
199
|
+
options={(item.options || []).map(option => ({
|
|
200
|
+
value: option.value,
|
|
201
|
+
label: option.label,
|
|
202
|
+
}))}/>
|
|
203
|
+
case 'text':
|
|
204
|
+
return <Input.TextArea rows={3}/>
|
|
205
|
+
case 'proxy':
|
|
206
|
+
return <Input placeholder="http://user:pass@192.168.1.10:8080" autoComplete="off"/>
|
|
207
|
+
case 'string':
|
|
208
|
+
return item.secret
|
|
209
|
+
? <Input.Password autoComplete="new-password"
|
|
210
|
+
placeholder={item.hasValue ? '已设置,留空表示不修改' : '未设置'}/>
|
|
211
|
+
: <Input/>
|
|
212
|
+
default:
|
|
213
|
+
return <Input/>
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
renderItem = (item) => {
|
|
218
|
+
const hasRange = (item.min !== null && item.min !== undefined)
|
|
219
|
+
|| (item.max !== null && item.max !== undefined)
|
|
220
|
+
const range = hasRange
|
|
221
|
+
? `范围 ${item.min ?? '-∞'} ~ ${item.max ?? '+∞'}`
|
|
222
|
+
: null
|
|
223
|
+
const sourceLabel = item.source === 'db'
|
|
224
|
+
? '数据库覆盖'
|
|
225
|
+
: item.source === 'env' ? 'application.yml' : '代码默认'
|
|
226
|
+
const defaultValue = item.defaultValue === null
|
|
227
|
+
|| item.defaultValue === undefined
|
|
228
|
+
|| item.defaultValue === ''
|
|
229
|
+
? '空'
|
|
230
|
+
: JSON.stringify(item.defaultValue)
|
|
231
|
+
|
|
232
|
+
return <Col xs={24} md={12} xl={8} key={item.key}>
|
|
233
|
+
<Form.Item
|
|
234
|
+
name={[item.key]}
|
|
235
|
+
required={item.required}
|
|
236
|
+
label={<Space size={4}>
|
|
237
|
+
<span>{item.label}</span>
|
|
238
|
+
<Tooltip title={<>
|
|
239
|
+
{item.hint && <div>{item.hint}</div>}
|
|
240
|
+
{!item.secret && <div>默认值:{defaultValue}</div>}
|
|
241
|
+
{range && <div>{range}</div>}
|
|
242
|
+
<div>当前来源:{sourceLabel}</div>
|
|
243
|
+
<div>配置键:{item.key}</div>
|
|
244
|
+
</>}>
|
|
245
|
+
<QuestionCircleOutlined style={{color: 'var(--oa-color-text-tertiary)'}}/>
|
|
246
|
+
</Tooltip>
|
|
247
|
+
{item.overridden && (
|
|
248
|
+
<Space size={2}>
|
|
249
|
+
<Tag color="blue" style={{marginInlineEnd: 0}}>已覆盖</Tag>
|
|
250
|
+
<Perm code='sys-config:update'>
|
|
251
|
+
<Popconfirm title="恢复为默认值?" onConfirm={() => this.resetKeys([item.key])}>
|
|
252
|
+
<Button type="text" size="small" title="恢复默认"
|
|
253
|
+
icon={<UndoOutlined/>}
|
|
254
|
+
style={{padding: 0, width: 20, minWidth: 20, height: 20}}/>
|
|
255
|
+
</Popconfirm>
|
|
256
|
+
</Perm>
|
|
257
|
+
</Space>
|
|
258
|
+
)}
|
|
259
|
+
</Space>}
|
|
260
|
+
>
|
|
261
|
+
{this.controlOf(item)}
|
|
262
|
+
</Form.Item>
|
|
263
|
+
<div style={{marginTop: -18, marginBottom: 12}}>
|
|
264
|
+
<Typography.Text type="secondary" style={{fontSize: 12}}>{item.hint}</Typography.Text>
|
|
265
|
+
{item.secret && (
|
|
266
|
+
<Typography.Text type="secondary" style={{fontSize: 12, marginInlineStart: 8}}>
|
|
267
|
+
{item.hasValue ? '已设置' : '未设置'}
|
|
268
|
+
</Typography.Text>
|
|
269
|
+
)}
|
|
270
|
+
</div>
|
|
271
|
+
</Col>
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
render() {
|
|
275
|
+
const {groups, loading, saving, testingMail, dirty} = this.state
|
|
276
|
+
const hasOverridden = groups.some(group => group.items.some(item => item.overridden))
|
|
277
|
+
// 首次加载显示骨架屏;切回 Tab 等刷新时保留现有内容,避免闪白
|
|
278
|
+
const initialLoading = loading && groups.length === 0
|
|
279
|
+
|
|
280
|
+
return <Page>
|
|
281
|
+
<Form ref={this.formRef} layout="vertical" onValuesChange={this.onValuesChange}>
|
|
282
|
+
{initialLoading && <Card loading style={{marginBottom: 16}}/>}
|
|
283
|
+
<div style={{display: 'flex', flexDirection: 'column', gap: 16}}>
|
|
284
|
+
{groups.map(group => (
|
|
285
|
+
<Card key={group.code}
|
|
286
|
+
title={group.label}
|
|
287
|
+
extra={group.code === 'mail' && (
|
|
288
|
+
<Perm code='sys-config:update'>
|
|
289
|
+
<Button loading={testingMail}
|
|
290
|
+
disabled={dirty}
|
|
291
|
+
title={dirty ? '请先保存配置修改' : '发送一封测试邮件到发件邮箱'}
|
|
292
|
+
onClick={this.sendTestMail}>
|
|
293
|
+
发送测试邮件
|
|
294
|
+
</Button>
|
|
295
|
+
</Perm>
|
|
296
|
+
)}>
|
|
297
|
+
{group.description && (
|
|
298
|
+
<Typography.Text type="secondary"
|
|
299
|
+
style={{display: 'block', fontSize: 12, marginBottom: 16}}>
|
|
300
|
+
{group.description}
|
|
301
|
+
</Typography.Text>
|
|
302
|
+
)}
|
|
303
|
+
<Row gutter={16}>
|
|
304
|
+
{group.items.map(this.renderItem)}
|
|
305
|
+
</Row>
|
|
306
|
+
</Card>
|
|
307
|
+
))}
|
|
308
|
+
</div>
|
|
309
|
+
|
|
310
|
+
{groups.length > 0 && (
|
|
311
|
+
<Space wrap style={{marginTop: 16}}>
|
|
312
|
+
<Perm code='sys-config:update'>
|
|
313
|
+
<Button type="primary" loading={saving} disabled={!dirty} onClick={this.save}>
|
|
314
|
+
保存修改
|
|
315
|
+
</Button>
|
|
316
|
+
</Perm>
|
|
317
|
+
<Perm code='sys-config:update'>
|
|
318
|
+
<Popconfirm
|
|
319
|
+
title="恢复全部默认值?"
|
|
320
|
+
description="所有已修改的配置项都会回到默认值。"
|
|
321
|
+
disabled={!hasOverridden}
|
|
322
|
+
onConfirm={() => this.resetKeys([])}
|
|
323
|
+
>
|
|
324
|
+
<Button danger disabled={!hasOverridden}>全部恢复默认</Button>
|
|
325
|
+
</Popconfirm>
|
|
326
|
+
</Perm>
|
|
327
|
+
<Typography.Text type="secondary" style={{fontSize: 12}}>
|
|
328
|
+
配置默认值来自 application.yml 或代码声明
|
|
329
|
+
</Typography.Text>
|
|
330
|
+
</Space>
|
|
331
|
+
)}
|
|
332
|
+
</Form>
|
|
333
|
+
</Page>
|
|
334
|
+
}
|
|
335
|
+
}
|
|
@@ -15,13 +15,13 @@ export default class FilePage extends React.Component {
|
|
|
15
15
|
formRef = React.createRef()
|
|
16
16
|
|
|
17
17
|
handleDelete = row => {
|
|
18
|
-
HttpClient.post('admin/
|
|
18
|
+
HttpClient.post('admin/sys-file/delete', {value: row.objectName}, null).then(() => {
|
|
19
19
|
this.tableRef.current.reload()
|
|
20
20
|
})
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
handleBatchDelete = ids => {
|
|
24
|
-
HttpClient.post('admin/
|
|
24
|
+
HttpClient.post('admin/sys-file/delete-batch', ids, null).then(() => {
|
|
25
25
|
this.tableRef.current.reload()
|
|
26
26
|
this.tableRef.current.clearSelection()
|
|
27
27
|
})
|
|
@@ -167,7 +167,7 @@ export default class FilePage extends React.Component {
|
|
|
167
167
|
</Space>
|
|
168
168
|
)}
|
|
169
169
|
request={(params) => {
|
|
170
|
-
return HttpClient.get('admin/
|
|
170
|
+
return HttpClient.get('admin/sys-file/page', params);
|
|
171
171
|
}}
|
|
172
172
|
|
|
173
173
|
columns={this.columns}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import {
|
|
3
3
|
Alert,
|
|
4
|
-
AutoComplete,
|
|
5
4
|
Divider,
|
|
6
5
|
Form,
|
|
7
6
|
Input,
|
|
@@ -13,29 +12,47 @@ import {
|
|
|
13
12
|
Tooltip
|
|
14
13
|
} from 'antd'
|
|
15
14
|
import {PlusOutlined, SearchOutlined} from "@ant-design/icons";
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
import {
|
|
16
|
+
CronEditor,
|
|
17
|
+
CronUtils,
|
|
18
|
+
FormModal,
|
|
19
|
+
DictUtils,
|
|
20
|
+
FieldDictSelect,
|
|
21
|
+
getMessageApi,
|
|
22
|
+
HttpClient,
|
|
23
|
+
LogViewer,
|
|
24
|
+
Page,
|
|
25
|
+
PermActions,
|
|
26
|
+
ProTable,
|
|
27
|
+
UrlUtils,
|
|
28
|
+
ValueType
|
|
29
|
+
} from "../../../framework";
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
const cronPresets = [
|
|
20
33
|
{
|
|
21
|
-
label: '
|
|
34
|
+
label: '每隔 5 秒',
|
|
22
35
|
value: '*/5 * * * * ?'
|
|
23
36
|
},
|
|
24
37
|
{
|
|
25
|
-
label: '
|
|
38
|
+
label: '每隔 5 分钟',
|
|
26
39
|
value: '0 */5 * * * ?'
|
|
27
40
|
},
|
|
28
41
|
{
|
|
29
|
-
label: '
|
|
42
|
+
label: '每天 22:00',
|
|
30
43
|
value: '0 0 22 * * ?'
|
|
31
44
|
},
|
|
32
45
|
{
|
|
33
|
-
label: '
|
|
46
|
+
label: '每天 01:00',
|
|
34
47
|
value: '0 0 1 * * ?'
|
|
35
48
|
},
|
|
36
49
|
{
|
|
37
|
-
label: '
|
|
50
|
+
label: '每月 1 号 01:00',
|
|
38
51
|
value: '0 0 1 1 * ?'
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
label: '工作日 09:00',
|
|
55
|
+
value: '0 0 9 ? * MON-FRI'
|
|
39
56
|
}
|
|
40
57
|
]
|
|
41
58
|
|
|
@@ -57,6 +74,8 @@ export default class JobPage extends React.Component {
|
|
|
57
74
|
logOpen: false,
|
|
58
75
|
|
|
59
76
|
progressMap: {},
|
|
77
|
+
|
|
78
|
+
cronDesc: '',
|
|
60
79
|
}
|
|
61
80
|
tableRef = React.createRef()
|
|
62
81
|
modalRef = React.createRef()
|
|
@@ -104,11 +123,12 @@ export default class JobPage extends React.Component {
|
|
|
104
123
|
}
|
|
105
124
|
|
|
106
125
|
handleAdd = () => {
|
|
107
|
-
this.setState({paramList: []})
|
|
126
|
+
this.setState({paramList: [], cronDesc: ''})
|
|
108
127
|
this.modalRef.current.open({})
|
|
109
128
|
}
|
|
110
129
|
|
|
111
130
|
handleEdit = (record) => {
|
|
131
|
+
this.setState({cronDesc: CronUtils.describe(record.cron)})
|
|
112
132
|
this.loadJobParamFields(record.jobClass, record.jobData)
|
|
113
133
|
this.modalRef.current.open(record)
|
|
114
134
|
}
|
|
@@ -160,8 +180,25 @@ export default class JobPage extends React.Component {
|
|
|
160
180
|
},
|
|
161
181
|
|
|
162
182
|
{
|
|
163
|
-
title: 'cron',
|
|
183
|
+
title: 'cron 表达式',
|
|
164
184
|
dataIndex: 'cron',
|
|
185
|
+
width: 220,
|
|
186
|
+
render: (v) => {
|
|
187
|
+
if (!v) {
|
|
188
|
+
return '-'
|
|
189
|
+
}
|
|
190
|
+
const desc = CronUtils.describe(v)
|
|
191
|
+
return <div style={{lineHeight: 1.45}}>
|
|
192
|
+
<div style={{
|
|
193
|
+
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace',
|
|
194
|
+
wordBreak: 'break-all'
|
|
195
|
+
}}>{v}</div>
|
|
196
|
+
{desc && <div style={{
|
|
197
|
+
fontSize: 12,
|
|
198
|
+
color: 'var(--oa-color-text-tertiary, rgba(0, 0, 0, 0.45))'
|
|
199
|
+
}}>{desc}</div>}
|
|
200
|
+
</div>
|
|
201
|
+
}
|
|
165
202
|
},
|
|
166
203
|
|
|
167
204
|
|
|
@@ -184,9 +221,9 @@ export default class JobPage extends React.Component {
|
|
|
184
221
|
},
|
|
185
222
|
|
|
186
223
|
{
|
|
187
|
-
title: '
|
|
224
|
+
title: '执行记录方式',
|
|
188
225
|
dataIndex: 'logAggregation',
|
|
189
|
-
render: v => DictUtils.dictTag('jobLogAggregation', v) || '
|
|
226
|
+
render: v => DictUtils.dictTag('jobLogAggregation', v) || '每次一份记录',
|
|
190
227
|
},
|
|
191
228
|
|
|
192
229
|
{
|
|
@@ -325,24 +362,26 @@ export default class JobPage extends React.Component {
|
|
|
325
362
|
onFinish={this.onFinish}
|
|
326
363
|
onValuesChange={this.onValuesChange}>
|
|
327
364
|
<Form.Item label='执行类' name='jobClass' rules={[{required: true}]}
|
|
328
|
-
tooltip='继承 BaseJob
|
|
365
|
+
tooltip='继承 BaseJob'>
|
|
329
366
|
<Select options={this.state.jobClassOptions}/>
|
|
330
367
|
</Form.Item>
|
|
331
368
|
<Form.Item label='名称' name='name' rules={[{required: true}]}>
|
|
332
369
|
<Input/>
|
|
333
370
|
</Form.Item>
|
|
334
371
|
|
|
335
|
-
<Form.Item label='cron表达式' name='cron'
|
|
372
|
+
<Form.Item label='cron表达式' name='cron'
|
|
373
|
+
help='必填,格式:秒 分 时 日 月 周(可选 年)'
|
|
374
|
+
extra={this.state.cronDesc ? `解析:${this.state.cronDesc}` : null}
|
|
336
375
|
rules={[{required: true}]}>
|
|
337
|
-
<
|
|
376
|
+
<CronEditor presets={cronPresets} placeholder='如 0 */5 * * * ?'/>
|
|
338
377
|
</Form.Item>
|
|
339
378
|
|
|
340
379
|
<Form.Item label='启用' name='enabled' valuePropName='checked' rules={[{required: true}]}>
|
|
341
380
|
<Switch/>
|
|
342
381
|
</Form.Item>
|
|
343
382
|
|
|
344
|
-
<Form.Item label='
|
|
345
|
-
help='
|
|
383
|
+
<Form.Item label='执行记录方式' name='logAggregation' initialValue='PER_RUN'
|
|
384
|
+
help='控制执行记录与日志文件的粒度。高频任务建议选择「每天一份记录」:一天只保留一条执行记录,日志文件按天合并'>
|
|
346
385
|
<FieldDictSelect typeCode='jobLogAggregation'/>
|
|
347
386
|
</Form.Item>
|
|
348
387
|
|
|
@@ -460,6 +499,10 @@ export default class JobPage extends React.Component {
|
|
|
460
499
|
}
|
|
461
500
|
|
|
462
501
|
onValuesChange = (changed, values) => {
|
|
502
|
+
if (changed.cron !== undefined) {
|
|
503
|
+
this.setState({cronDesc: CronUtils.describe(values.cron)})
|
|
504
|
+
}
|
|
505
|
+
|
|
463
506
|
if (changed.jobClass) {
|
|
464
507
|
this.loadJobParamFields(values.jobClass)
|
|
465
508
|
const option = this.state.jobClassOptions.find(o => o.value === changed.jobClass)
|
|
@@ -55,7 +55,7 @@ export default class LogPage extends React.Component {
|
|
|
55
55
|
render() {
|
|
56
56
|
return <Page>
|
|
57
57
|
<ProTable
|
|
58
|
-
request={(params) => HttpClient.get('admin/
|
|
58
|
+
request={(params) => HttpClient.get('admin/sys-log/page', params)}
|
|
59
59
|
columns={this.columns}
|
|
60
60
|
searchFormRender={() => (
|
|
61
61
|
<>
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {Form, Input, Tag} from 'antd';
|
|
3
|
+
import {HttpClient, Page, PermActions, ProTable} from "../../../framework";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 在线用户:一个登录会话一行,支持强制下线。
|
|
7
|
+
* 数据来自内存中的 HttpSession 元数据,仅单实例部署准确。
|
|
8
|
+
*/
|
|
9
|
+
export default class OnlineUserPage extends React.Component {
|
|
10
|
+
|
|
11
|
+
tableRef = React.createRef();
|
|
12
|
+
|
|
13
|
+
handleKick = row => {
|
|
14
|
+
HttpClient.post('admin/sys-online-user/kick', {id: row.id}).then(() => {
|
|
15
|
+
this.tableRef.current.reload();
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
columns = [
|
|
20
|
+
{
|
|
21
|
+
title: '登录账号',
|
|
22
|
+
dataIndex: 'username',
|
|
23
|
+
width: 140,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
title: '姓名',
|
|
27
|
+
dataIndex: 'name',
|
|
28
|
+
width: 120,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
title: '所属机构',
|
|
32
|
+
dataIndex: 'unitName',
|
|
33
|
+
width: 180,
|
|
34
|
+
render: (_, record) => {
|
|
35
|
+
// 单位与机构可能指向同一节点(如 admin 挂在根单位),去重后避免「默认单位 / 默认单位」
|
|
36
|
+
const names = [...new Set([record.unitName, record.orgName].filter(Boolean))];
|
|
37
|
+
return names.length ? names.join(' / ') : '-';
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
title: 'IP',
|
|
42
|
+
dataIndex: 'ip',
|
|
43
|
+
width: 140,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
title: '登录时间',
|
|
47
|
+
dataIndex: 'loginTime',
|
|
48
|
+
width: 170,
|
|
49
|
+
render: v => v || '-',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
title: '最后活跃',
|
|
53
|
+
dataIndex: 'lastActiveTime',
|
|
54
|
+
width: 170,
|
|
55
|
+
render: v => v || '-',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
title: '操作',
|
|
59
|
+
dataIndex: 'option',
|
|
60
|
+
width: 130,
|
|
61
|
+
fixed: 'right',
|
|
62
|
+
render: (_, record) => {
|
|
63
|
+
if (record.current) {
|
|
64
|
+
return <Tag color="green">当前会话</Tag>;
|
|
65
|
+
}
|
|
66
|
+
return <PermActions
|
|
67
|
+
size="small"
|
|
68
|
+
actions={[{
|
|
69
|
+
label: '强制下线',
|
|
70
|
+
perm: 'sys-online:kick',
|
|
71
|
+
danger: true,
|
|
72
|
+
confirm: `确定强制下线「${record.name || record.username}」吗?`,
|
|
73
|
+
onClick: () => this.handleKick(record),
|
|
74
|
+
}]}
|
|
75
|
+
/>;
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
render() {
|
|
81
|
+
return <Page>
|
|
82
|
+
<ProTable
|
|
83
|
+
actionRef={this.tableRef}
|
|
84
|
+
request={params => HttpClient.get('admin/sys-online-user/page', params)}
|
|
85
|
+
columns={this.columns}
|
|
86
|
+
searchFormRender={() => (
|
|
87
|
+
<Form.Item label='账号/姓名/IP' name='keyword'>
|
|
88
|
+
<Input placeholder='支持账号、姓名、IP 模糊搜索' allowClear/>
|
|
89
|
+
</Form.Item>
|
|
90
|
+
)}
|
|
91
|
+
/>
|
|
92
|
+
</Page>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
}
|