@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 +1 -1
- package/src/framework/GlobalData.ts +4 -0
- package/src/framework/components/CronEditor/index.tsx +318 -0
- package/src/framework/components/index.ts +1 -0
- package/src/framework/utils/CronUtils.ts +562 -0
- package/src/framework/utils/index.ts +1 -0
- package/src/layouts/admin/AdminTabs.jsx +109 -35
- package/src/layouts/admin/AdminTabs.less +77 -19
- package/src/layouts/admin/index.jsx +13 -1
- package/src/layouts/admin/index.less +2 -2
- package/src/pages/system/codegen/index.jsx +83 -44
- package/src/pages/system/config/index.jsx +309 -0
- package/src/pages/system/job/index.jsx +105 -56
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
Card,
|
|
5
|
+
Col,
|
|
6
|
+
Divider,
|
|
7
|
+
Form,
|
|
8
|
+
Input,
|
|
9
|
+
InputNumber,
|
|
10
|
+
Popconfirm,
|
|
11
|
+
Row,
|
|
12
|
+
Select,
|
|
13
|
+
Space,
|
|
14
|
+
Switch,
|
|
15
|
+
Tag,
|
|
16
|
+
Tooltip,
|
|
17
|
+
Typography,
|
|
18
|
+
message
|
|
19
|
+
} from 'antd'
|
|
20
|
+
import {QuestionCircleOutlined, UndoOutlined} from '@ant-design/icons'
|
|
21
|
+
import {EventBus, GlobalData, HttpClient, Page, Perm} from '../../../framework'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 系统配置页。
|
|
25
|
+
*
|
|
26
|
+
* 配置项由后端 ConfigProvider 声明(分组 / 类型 / 默认值 / 范围 / 说明),
|
|
27
|
+
* 本页面只负责按元数据渲染、收集修改项并提交差异。
|
|
28
|
+
*/
|
|
29
|
+
export default class SysConfigPage extends React.Component {
|
|
30
|
+
|
|
31
|
+
state = {
|
|
32
|
+
groups: [],
|
|
33
|
+
loading: true,
|
|
34
|
+
saving: 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
|
+
resetKeys = async (keys) => {
|
|
145
|
+
try {
|
|
146
|
+
const rs = await HttpClient.post('admin/sys-config/reset', {keys})
|
|
147
|
+
this.applyGroups(rs.data?.groups || [], keys && keys.length ? keys : null)
|
|
148
|
+
await this.refreshSiteInfo()
|
|
149
|
+
} catch {
|
|
150
|
+
// 错误提示由 HttpClient 统一处理
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** 站点标题、水印等配置经 site-info 下发,保存后刷新本地缓存并通知布局 */
|
|
155
|
+
refreshSiteInfo = async () => {
|
|
156
|
+
try {
|
|
157
|
+
const rs = await HttpClient.get('/admin/public/site-info', null, {toastError: false})
|
|
158
|
+
const next = rs.data || {}
|
|
159
|
+
const changed = JSON.stringify(GlobalData.getSiteInfo()) !== JSON.stringify(next)
|
|
160
|
+
GlobalData.setSiteInfo(next)
|
|
161
|
+
if (changed) {
|
|
162
|
+
EventBus.emit('siteInfoChange')
|
|
163
|
+
}
|
|
164
|
+
} catch {
|
|
165
|
+
// site-info 刷新失败不影响配置保存结果
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
controlOf = (item) => {
|
|
170
|
+
switch (item.type) {
|
|
171
|
+
case 'bool':
|
|
172
|
+
return <Switch/>
|
|
173
|
+
case 'int':
|
|
174
|
+
return <InputNumber style={{width: '100%'}} precision={0} step={1}
|
|
175
|
+
min={item.min ?? undefined} max={item.max ?? undefined}/>
|
|
176
|
+
case 'float':
|
|
177
|
+
return <InputNumber style={{width: '100%'}} step={0.01}
|
|
178
|
+
min={item.min ?? undefined} max={item.max ?? undefined}/>
|
|
179
|
+
case 'time':
|
|
180
|
+
return <Input placeholder="HH:mm" style={{width: '100%'}}/>
|
|
181
|
+
case 'list':
|
|
182
|
+
return <Select mode="tags" tokenSeparators={[',']} style={{width: '100%'}}
|
|
183
|
+
placeholder="输入后回车,或粘贴逗号分隔的内容"/>
|
|
184
|
+
case 'select':
|
|
185
|
+
return <Select style={{width: '100%'}}
|
|
186
|
+
options={(item.options || []).map(option => ({
|
|
187
|
+
value: option.value,
|
|
188
|
+
label: option.label,
|
|
189
|
+
}))}/>
|
|
190
|
+
case 'text':
|
|
191
|
+
return <Input.TextArea rows={3}/>
|
|
192
|
+
case 'string':
|
|
193
|
+
return item.secret
|
|
194
|
+
? <Input.Password autoComplete="new-password"
|
|
195
|
+
placeholder={item.hasValue ? '已设置,留空表示不修改' : '未设置'}/>
|
|
196
|
+
: <Input/>
|
|
197
|
+
default:
|
|
198
|
+
return <Input/>
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
renderItem = (item) => {
|
|
203
|
+
const hasRange = (item.min !== null && item.min !== undefined)
|
|
204
|
+
|| (item.max !== null && item.max !== undefined)
|
|
205
|
+
const range = hasRange
|
|
206
|
+
? `范围 ${item.min ?? '-∞'} ~ ${item.max ?? '+∞'}`
|
|
207
|
+
: null
|
|
208
|
+
const sourceLabel = item.source === 'db'
|
|
209
|
+
? '数据库覆盖'
|
|
210
|
+
: item.source === 'env' ? 'application.yml' : '代码默认'
|
|
211
|
+
const defaultValue = item.defaultValue === null
|
|
212
|
+
|| item.defaultValue === undefined
|
|
213
|
+
|| item.defaultValue === ''
|
|
214
|
+
? '空'
|
|
215
|
+
: JSON.stringify(item.defaultValue)
|
|
216
|
+
|
|
217
|
+
return <Col xs={24} md={12} xl={8} key={item.key}>
|
|
218
|
+
<Form.Item
|
|
219
|
+
name={[item.key]}
|
|
220
|
+
required={item.required}
|
|
221
|
+
label={<Space size={4}>
|
|
222
|
+
<span>{item.label}</span>
|
|
223
|
+
<Tooltip title={<>
|
|
224
|
+
{item.hint && <div>{item.hint}</div>}
|
|
225
|
+
{!item.secret && <div>默认值:{defaultValue}</div>}
|
|
226
|
+
{range && <div>{range}</div>}
|
|
227
|
+
<div>当前来源:{sourceLabel}</div>
|
|
228
|
+
<div>配置键:{item.key}</div>
|
|
229
|
+
</>}>
|
|
230
|
+
<QuestionCircleOutlined style={{color: 'var(--oa-color-text-tertiary)'}}/>
|
|
231
|
+
</Tooltip>
|
|
232
|
+
{item.overridden && (
|
|
233
|
+
<Space size={2}>
|
|
234
|
+
<Tag color="blue" style={{marginInlineEnd: 0}}>已覆盖</Tag>
|
|
235
|
+
<Perm code='sys-config:update'>
|
|
236
|
+
<Popconfirm title="恢复为默认值?" onConfirm={() => this.resetKeys([item.key])}>
|
|
237
|
+
<Button type="text" size="small" title="恢复默认"
|
|
238
|
+
icon={<UndoOutlined/>}
|
|
239
|
+
style={{padding: 0, width: 20, minWidth: 20, height: 20}}/>
|
|
240
|
+
</Popconfirm>
|
|
241
|
+
</Perm>
|
|
242
|
+
</Space>
|
|
243
|
+
)}
|
|
244
|
+
</Space>}
|
|
245
|
+
>
|
|
246
|
+
{this.controlOf(item)}
|
|
247
|
+
</Form.Item>
|
|
248
|
+
<div style={{marginTop: -18, marginBottom: 12}}>
|
|
249
|
+
<Typography.Text type="secondary" style={{fontSize: 12}}>{item.hint}</Typography.Text>
|
|
250
|
+
{item.secret && (
|
|
251
|
+
<Typography.Text type="secondary" style={{fontSize: 12, marginInlineStart: 8}}>
|
|
252
|
+
{item.hasValue ? '已设置' : '未设置'}
|
|
253
|
+
</Typography.Text>
|
|
254
|
+
)}
|
|
255
|
+
</div>
|
|
256
|
+
</Col>
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
render() {
|
|
260
|
+
const {groups, loading, saving, dirty} = this.state
|
|
261
|
+
const hasOverridden = groups.some(group => group.items.some(item => item.overridden))
|
|
262
|
+
|
|
263
|
+
return <Page>
|
|
264
|
+
<Form ref={this.formRef} layout="vertical" onValuesChange={this.onValuesChange}>
|
|
265
|
+
<Card loading={loading}>
|
|
266
|
+
{groups.map((group, index) => (
|
|
267
|
+
<div key={group.code}>
|
|
268
|
+
<div style={{marginBottom: 12}}>
|
|
269
|
+
<Typography.Title level={5} style={{margin: 0}}>{group.label}</Typography.Title>
|
|
270
|
+
{group.description && (
|
|
271
|
+
<Typography.Text type="secondary" style={{fontSize: 12}}>
|
|
272
|
+
{group.description}
|
|
273
|
+
</Typography.Text>
|
|
274
|
+
)}
|
|
275
|
+
</div>
|
|
276
|
+
<Row gutter={16}>
|
|
277
|
+
{group.items.map(this.renderItem)}
|
|
278
|
+
</Row>
|
|
279
|
+
{index < groups.length - 1 && <Divider style={{margin: '8px 0 20px'}}/>}
|
|
280
|
+
</div>
|
|
281
|
+
))}
|
|
282
|
+
|
|
283
|
+
{groups.length > 0 && <Divider style={{margin: '4px 0 16px'}}/>}
|
|
284
|
+
|
|
285
|
+
<Space wrap>
|
|
286
|
+
<Perm code='sys-config:update'>
|
|
287
|
+
<Button type="primary" loading={saving} disabled={!dirty} onClick={this.save}>
|
|
288
|
+
保存修改
|
|
289
|
+
</Button>
|
|
290
|
+
</Perm>
|
|
291
|
+
<Perm code='sys-config:update'>
|
|
292
|
+
<Popconfirm
|
|
293
|
+
title="恢复全部默认值?"
|
|
294
|
+
description="所有已修改的配置项都会回到默认值。"
|
|
295
|
+
disabled={!hasOverridden}
|
|
296
|
+
onConfirm={() => this.resetKeys([])}
|
|
297
|
+
>
|
|
298
|
+
<Button danger disabled={!hasOverridden}>全部恢复默认</Button>
|
|
299
|
+
</Popconfirm>
|
|
300
|
+
</Perm>
|
|
301
|
+
<Typography.Text type="secondary" style={{fontSize: 12}}>
|
|
302
|
+
配置默认值来自 application.yml 或代码声明
|
|
303
|
+
</Typography.Text>
|
|
304
|
+
</Space>
|
|
305
|
+
</Card>
|
|
306
|
+
</Form>
|
|
307
|
+
</Page>
|
|
308
|
+
}
|
|
309
|
+
}
|
|
@@ -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
|
{
|
|
@@ -225,7 +262,7 @@ export default class JobPage extends React.Component {
|
|
|
225
262
|
more
|
|
226
263
|
size="small"
|
|
227
264
|
actions={[
|
|
228
|
-
{label: '
|
|
265
|
+
{label: '执行情况', perm: 'job:read', onClick: () => this.showExecuteRecord(record)},
|
|
229
266
|
{label: '执行一次', perm: 'job:trigger', onClick: () => this.handleTriggerJob(record)},
|
|
230
267
|
{label: '编辑', perm: 'job:update', onClick: () => this.handleEdit(record)},
|
|
231
268
|
{label: '删除', perm: 'job:delete', confirm: '是否确定删除?', onClick: () => this.handleDelete(record)},
|
|
@@ -286,6 +323,14 @@ export default class JobPage extends React.Component {
|
|
|
286
323
|
p.logId === selectedLogRecord.id && p.state === 'RUNNING')
|
|
287
324
|
}
|
|
288
325
|
|
|
326
|
+
/** 结束时间与开始时间为同一天时只显示时分秒,避免重复日期 */
|
|
327
|
+
formatRecordEndTime = (beginTime, endTime) => {
|
|
328
|
+
if (!beginTime || !endTime) {
|
|
329
|
+
return endTime || '-'
|
|
330
|
+
}
|
|
331
|
+
return endTime.slice(0, 10) === beginTime.slice(0, 10) ? endTime.slice(11) : endTime
|
|
332
|
+
}
|
|
333
|
+
|
|
289
334
|
|
|
290
335
|
render() {
|
|
291
336
|
return <Page>
|
|
@@ -324,17 +369,19 @@ export default class JobPage extends React.Component {
|
|
|
324
369
|
<Input/>
|
|
325
370
|
</Form.Item>
|
|
326
371
|
|
|
327
|
-
<Form.Item label='cron表达式' name='cron'
|
|
372
|
+
<Form.Item label='cron表达式' name='cron'
|
|
373
|
+
help='必填,格式:秒 分 时 日 月 周(可选 年)'
|
|
374
|
+
extra={this.state.cronDesc ? `解析:${this.state.cronDesc}` : null}
|
|
328
375
|
rules={[{required: true}]}>
|
|
329
|
-
<
|
|
376
|
+
<CronEditor presets={cronPresets} placeholder='如 0 */5 * * * ?'/>
|
|
330
377
|
</Form.Item>
|
|
331
378
|
|
|
332
379
|
<Form.Item label='启用' name='enabled' valuePropName='checked' rules={[{required: true}]}>
|
|
333
380
|
<Switch/>
|
|
334
381
|
</Form.Item>
|
|
335
382
|
|
|
336
|
-
<Form.Item label='
|
|
337
|
-
help='
|
|
383
|
+
<Form.Item label='执行记录方式' name='logAggregation' initialValue='PER_RUN'
|
|
384
|
+
help='控制执行记录与日志文件的粒度。高频任务建议选择「每天一份记录」:一天只保留一条执行记录,日志文件按天合并'>
|
|
338
385
|
<FieldDictSelect typeCode='jobLogAggregation'/>
|
|
339
386
|
</Form.Item>
|
|
340
387
|
|
|
@@ -374,56 +421,54 @@ export default class JobPage extends React.Component {
|
|
|
374
421
|
destroyOnHidden
|
|
375
422
|
>
|
|
376
423
|
{this.renderRunningProgress()}
|
|
377
|
-
<ProTable columns={[
|
|
378
|
-
title: '开始时间',
|
|
379
|
-
dataIndex: 'beginTime',
|
|
380
|
-
},
|
|
424
|
+
<ProTable columns={[
|
|
381
425
|
{
|
|
382
|
-
title: '结束时间',
|
|
383
|
-
dataIndex: '
|
|
426
|
+
title: '开始 / 结束时间',
|
|
427
|
+
dataIndex: 'beginTime',
|
|
428
|
+
width: 170,
|
|
429
|
+
render: (v, record) => <div style={{lineHeight: 1.45}}>
|
|
430
|
+
<div>{v || '-'}</div>
|
|
431
|
+
<div style={{
|
|
432
|
+
fontSize: 12,
|
|
433
|
+
color: 'var(--oa-color-text-tertiary, rgba(0, 0, 0, 0.45))',
|
|
434
|
+
whiteSpace: 'nowrap'
|
|
435
|
+
}}>{record.endTime
|
|
436
|
+
? '至 ' + this.formatRecordEndTime(v, record.endTime)
|
|
437
|
+
: '运行中…'}</div>
|
|
438
|
+
</div>
|
|
384
439
|
},
|
|
385
440
|
{
|
|
386
|
-
title: '
|
|
387
|
-
dataIndex: 'jobRunTimeLabel',
|
|
388
|
-
|
|
389
|
-
},
|
|
390
|
-
{
|
|
391
|
-
title: '执行次数',
|
|
441
|
+
title: '成功/失败',
|
|
392
442
|
dataIndex: 'executeCount',
|
|
393
|
-
width:
|
|
394
|
-
render: v =>
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
title: '累计耗时',
|
|
404
|
-
dataIndex: 'totalRunTimeLabel',
|
|
405
|
-
width: 100,
|
|
406
|
-
render: v => v || '-',
|
|
407
|
-
},
|
|
408
|
-
{
|
|
409
|
-
title: '是否成功',
|
|
410
|
-
dataIndex: 'success',
|
|
411
|
-
width: 200,
|
|
412
|
-
render: v => {
|
|
413
|
-
if (v != null) {
|
|
414
|
-
return v ? '成功' : '异常'
|
|
443
|
+
width: 150,
|
|
444
|
+
render: (v, record) => {
|
|
445
|
+
const count = v == null ? 1 : v
|
|
446
|
+
// 旧版本升级上来的记录没有 failCount,用 success 兜底
|
|
447
|
+
const failCount = record.failCount == null
|
|
448
|
+
? (record.success === false ? 1 : 0)
|
|
449
|
+
: record.failCount
|
|
450
|
+
const successCount = Math.max(count - failCount, 0)
|
|
451
|
+
if (successCount === 0 && failCount === 0) {
|
|
452
|
+
return '-'
|
|
415
453
|
}
|
|
416
|
-
|
|
454
|
+
return <span>
|
|
455
|
+
{successCount > 0 &&
|
|
456
|
+
<Tag color='success' style={{marginRight: 4}}>成功 {successCount}</Tag>}
|
|
457
|
+
{failCount > 0 &&
|
|
458
|
+
<Tag color='error' style={{marginRight: 0}}>失败 {failCount}</Tag>}
|
|
459
|
+
</span>
|
|
460
|
+
},
|
|
417
461
|
},
|
|
418
462
|
{
|
|
419
463
|
title: '返回结果',
|
|
420
464
|
dataIndex: 'result',
|
|
421
|
-
|
|
465
|
+
render: v => (v == null || v === '') ? '-' : v,
|
|
422
466
|
},
|
|
423
|
-
|
|
424
467
|
{
|
|
425
468
|
title: '操作',
|
|
426
469
|
dataIndex: 'option',
|
|
470
|
+
width: 64,
|
|
471
|
+
fixed: 'right',
|
|
427
472
|
render: (_, record) => <a onClick={() => this.showLog(record)}>日志</a>,
|
|
428
473
|
}
|
|
429
474
|
]} request={(params) => {
|
|
@@ -454,6 +499,10 @@ export default class JobPage extends React.Component {
|
|
|
454
499
|
}
|
|
455
500
|
|
|
456
501
|
onValuesChange = (changed, values) => {
|
|
502
|
+
if (changed.cron !== undefined) {
|
|
503
|
+
this.setState({cronDesc: CronUtils.describe(values.cron)})
|
|
504
|
+
}
|
|
505
|
+
|
|
457
506
|
if (changed.jobClass) {
|
|
458
507
|
this.loadJobParamFields(values.jobClass)
|
|
459
508
|
const option = this.state.jobClassOptions.find(o => o.value === changed.jobClass)
|