@jiangood/open-admin 3.1.8 → 3.1.10

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.
@@ -1,4 +1,4 @@
1
- import {Button, Card, Collapse, Drawer, Form, Input, Modal, Select, Space, Switch, Tag, Typography} from 'antd'
1
+ import {Button, Card, Checkbox, Collapse, Drawer, Form, Input, Modal, Select, Tag, Typography} from 'antd'
2
2
  import React from 'react'
3
3
  import {HttpClient, getMessageApi, getToken, Page, Perm} from '../../../framework'
4
4
 
@@ -13,9 +13,9 @@ export default class CodegenPage extends React.Component {
13
13
  module: '',
14
14
  label: '',
15
15
  parentMenu: 'sys',
16
- overwrite: false,
17
16
 
18
17
  files: [],
18
+ selectedPaths: [],
19
19
  previewOpen: false,
20
20
  confirmOpen: false,
21
21
 
@@ -47,7 +47,6 @@ export default class CodegenPage extends React.Component {
47
47
  module: this.state.module,
48
48
  label: this.state.label,
49
49
  parentMenu: this.state.parentMenu,
50
- overwrite: this.state.overwrite,
51
50
  })
52
51
 
53
52
  handlePreview = () => {
@@ -57,7 +56,13 @@ export default class CodegenPage extends React.Component {
57
56
  }
58
57
  this.setState({loading: true})
59
58
  HttpClient.post('admin/codegen/preview', this.buildReq(), null, {toastError: false}).then(rs => {
60
- this.setState({files: rs.data || [], previewOpen: true})
59
+ const files = rs.data || []
60
+ this.setState({
61
+ files,
62
+ // 默认只勾选新增文件,已存在文件需手动勾选后才会覆盖
63
+ selectedPaths: files.filter(file => !file.exists).map(file => file.path),
64
+ previewOpen: true,
65
+ })
61
66
  }).catch(e => {
62
67
  console.error('[Codegen] 预览失败:', e)
63
68
  getMessageApi().error(e?.message || '预览失败')
@@ -74,7 +79,8 @@ export default class CodegenPage extends React.Component {
74
79
 
75
80
  doGenerate = () => {
76
81
  this.setState({confirmOpen: false, loading: true})
77
- HttpClient.post('admin/codegen/generate', this.buildReq(), null, {toastError: false}).then(rs => {
82
+ const params = {...this.buildReq(), paths: this.state.selectedPaths}
83
+ HttpClient.post('admin/codegen/generate', params, null, {toastError: false}).then(rs => {
78
84
  this.setState({result: rs.data, resultOpen: true, previewOpen: false})
79
85
  }).catch(e => {
80
86
  console.error('[Codegen] 生成失败:', e)
@@ -82,6 +88,20 @@ export default class CodegenPage extends React.Component {
82
88
  }).finally(() => this.setState({loading: false}))
83
89
  }
84
90
 
91
+ togglePath = (path, checked) => {
92
+ this.setState(state => ({
93
+ selectedPaths: checked
94
+ ? [...state.selectedPaths, path]
95
+ : state.selectedPaths.filter(item => item !== path),
96
+ }))
97
+ }
98
+
99
+ toggleAll = checked => {
100
+ this.setState(state => ({
101
+ selectedPaths: checked ? state.files.map(file => file.path) : [],
102
+ }))
103
+ }
104
+
85
105
  copy = (text) => {
86
106
  if (navigator.clipboard) {
87
107
  navigator.clipboard.writeText(text)
@@ -96,14 +116,10 @@ export default class CodegenPage extends React.Component {
96
116
 
97
117
  render() {
98
118
  const selected = this.selectedOption()
99
- return <Page actions={
100
- <Space>
101
- <Perm code='sys-codegen:generate'>
102
- <Button type='primary' loading={this.state.loading} onClick={this.handlePreview}>预览</Button>
103
- </Perm>
104
- <Button onClick={this.loadEntities}>刷新实体</Button>
105
- </Space>
106
- }>
119
+ const {files, selectedPaths} = this.state
120
+ const allSelected = files.length > 0 && selectedPaths.length === files.length
121
+ const overwriteCount = files.filter(file => file.exists && selectedPaths.includes(file.path)).length
122
+ return <Page>
107
123
  <Card size='small'>
108
124
  <Form layout='vertical' style={{maxWidth: 720}}>
109
125
  <Form.Item label='实体' required>
@@ -134,11 +150,10 @@ export default class CodegenPage extends React.Component {
134
150
  <Input value={this.state.parentMenu}
135
151
  onChange={e => this.setState({parentMenu: e.target.value})}/>
136
152
  </Form.Item>
137
- <Form.Item label='覆盖已存在文件'>
138
- <Switch checked={this.state.overwrite}
139
- onChange={checked => this.setState({overwrite: checked})}/>
140
- </Form.Item>
141
153
  </Form>
154
+ <Perm code='sys-codegen:generate'>
155
+ <Button type='primary' loading={this.state.loading} onClick={this.handlePreview}>预览</Button>
156
+ </Perm>
142
157
  </Card>
143
158
 
144
159
  <Drawer title='生成预览' width={960}
@@ -146,34 +161,58 @@ export default class CodegenPage extends React.Component {
146
161
  onClose={() => this.setState({previewOpen: false})}
147
162
  extra={
148
163
  <Perm code='sys-codegen:generate'>
149
- <Button type='primary' loading={this.state.loading} onClick={this.handleGenerate}>生成并写入</Button>
164
+ <Button type='primary'
165
+ disabled={selectedPaths.length === 0}
166
+ loading={this.state.loading}
167
+ onClick={this.handleGenerate}>
168
+ 生成并写入({selectedPaths.length})
169
+ </Button>
150
170
  </Perm>
151
171
  }>
172
+ <div style={{display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12}}>
173
+ <Checkbox checked={allSelected}
174
+ indeterminate={selectedPaths.length > 0 && !allSelected}
175
+ onChange={e => this.toggleAll(e.target.checked)}>
176
+ 全选
177
+ </Checkbox>
178
+ <Typography.Text type='secondary'>
179
+ 已选 {selectedPaths.length} / 共 {files.length} 个文件,默认勾选新增文件
180
+ </Typography.Text>
181
+ </div>
152
182
  <Collapse
153
- items={this.state.files.map((file, index) => ({
154
- key: String(index),
155
- label: (
156
- <Space>
157
- <span>{file.path}</span>
158
- {file.exists
159
- ? <Tag color='orange'>已存在</Tag>
160
- : <Tag color='green'>新增</Tag>}
161
- </Space>
162
- ),
163
- children: (
164
- <>
165
- <Button size='small' style={{marginBottom: 8}}
166
- onClick={() => this.copy(file.content)}>复制</Button>
167
- <pre style={{
168
- maxHeight: 420,
169
- overflow: 'auto',
170
- background: String(getToken().colorFillTertiary ?? '#f6f8fa'),
171
- padding: 12,
172
- borderRadius: 4,
173
- }}>{file.content}</pre>
174
- </>
175
- ),
176
- }))}
183
+ items={files.map((file, index) => {
184
+ const checked = selectedPaths.includes(file.path)
185
+ return {
186
+ key: String(index),
187
+ label: (
188
+ <span onClick={e => e.stopPropagation()}
189
+ style={{display: 'flex', alignItems: 'center', gap: 8, minWidth: 0}}>
190
+ <Checkbox checked={checked}
191
+ onChange={e => this.togglePath(file.path, e.target.checked)}/>
192
+ <span title={file.path}
193
+ style={{flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap'}}>
194
+ {file.path}
195
+ </span>
196
+ {file.exists
197
+ ? <Tag color='orange' style={{marginInlineEnd: 0}}>{checked ? '将覆盖' : '已存在'}</Tag>
198
+ : <Tag color='green' style={{marginInlineEnd: 0}}>新增</Tag>}
199
+ </span>
200
+ ),
201
+ children: (
202
+ <>
203
+ <Button size='small' style={{marginBottom: 8}}
204
+ onClick={() => this.copy(file.content)}>复制</Button>
205
+ <pre style={{
206
+ maxHeight: 420,
207
+ overflow: 'auto',
208
+ background: String(getToken().colorFillTertiary ?? '#f6f8fa'),
209
+ padding: 12,
210
+ borderRadius: 4,
211
+ }}>{file.content}</pre>
212
+ </>
213
+ ),
214
+ }
215
+ })}
177
216
  />
178
217
  </Drawer>
179
218
 
@@ -184,7 +223,7 @@ export default class CodegenPage extends React.Component {
184
223
  onCancel={() => this.setState({confirmOpen: false})}>
185
224
  <p>将按以下配置生成代码到项目源码目录:</p>
186
225
  <p>模块名:{this.state.module}</p>
187
- <p>覆盖已存在文件:{this.state.overwrite ? '是' : '否'}</p>
226
+ <p>写入 {selectedPaths.length} 个文件{overwriteCount > 0 ? `,其中覆盖 ${overwriteCount} 个已存在文件` : ''}。</p>
188
227
  </Modal>
189
228
 
190
229
  <Modal title='生成结果'
@@ -194,7 +233,7 @@ export default class CodegenPage extends React.Component {
194
233
  <p>已写入 {this.state.result?.written?.length || 0} 个文件,跳过 {this.state.result?.skipped?.length || 0} 个。</p>
195
234
  {this.state.result?.skipped?.length > 0 &&
196
235
  <>
197
- <Typography.Text type='warning'>以下文件已存在已跳过(可勾选“覆盖已存在文件”后重新生成):</Typography.Text>
236
+ <Typography.Text type='warning'>以下文件未勾选,已跳过:</Typography.Text>
198
237
  <ul>
199
238
  {this.state.result.skipped.map(path => <li key={path}>{path}</li>)}
200
239
  </ul>
@@ -6,13 +6,14 @@ import {
6
6
  Form,
7
7
  Input,
8
8
  Modal,
9
+ Progress,
9
10
  Select,
10
- Spin,
11
11
  Switch,
12
- Tag
12
+ Tag,
13
+ Tooltip
13
14
  } from 'antd'
14
15
  import {PlusOutlined, SearchOutlined} from "@ant-design/icons";
15
- import {FormModal, getMessageApi, HttpClient, Page, PermActions, ProTable, UrlUtils, ValueType} from "../../../framework";
16
+ import {FormModal, DictUtils, FieldDictSelect, getMessageApi, HttpClient, LogViewer, Page, PermActions, ProTable, UrlUtils, ValueType} from "../../../framework";
16
17
 
17
18
 
18
19
  const cronOptions = [
@@ -54,11 +55,12 @@ export default class JobPage extends React.Component {
54
55
  executeRecordOpen: false,
55
56
 
56
57
  logOpen: false,
57
- logLoading: false,
58
- logContent: '',
58
+
59
+ progressMap: {},
59
60
  }
60
61
  tableRef = React.createRef()
61
62
  modalRef = React.createRef()
63
+ progressTimer = null
62
64
 
63
65
  componentDidMount() {
64
66
  HttpClient.get('admin/job/job-class-options', null, {toastError: false}).then(rs => {
@@ -66,6 +68,39 @@ export default class JobPage extends React.Component {
66
68
  }).catch(e => {
67
69
  console.error('[Job] 加载任务类选项失败:', e);
68
70
  })
71
+ this.startProgressPolling()
72
+ }
73
+
74
+ componentWillUnmount() {
75
+ this.stopProgressPolling()
76
+ }
77
+
78
+ startProgressPolling = () => {
79
+ this.loadProgress()
80
+ this.progressTimer = setInterval(() => {
81
+ if (!document.hidden) {
82
+ this.loadProgress()
83
+ }
84
+ }, 3000)
85
+ }
86
+
87
+ stopProgressPolling = () => {
88
+ if (this.progressTimer) {
89
+ clearInterval(this.progressTimer)
90
+ this.progressTimer = null
91
+ }
92
+ }
93
+
94
+ loadProgress = () => {
95
+ HttpClient.get('admin/job/progress', null, {toastError: false}).then(rs => {
96
+ const progressMap = {}
97
+ ;(rs.data || []).forEach(p => {
98
+ progressMap[p.jobId] = p
99
+ })
100
+ this.setState({progressMap})
101
+ }).catch(e => {
102
+ console.error('[Job] 加载任务进度失败:', e);
103
+ })
69
104
  }
70
105
 
71
106
  handleAdd = () => {
@@ -107,6 +142,7 @@ export default class JobPage extends React.Component {
107
142
  handleTriggerJob = row => {
108
143
  HttpClient.post('admin/job/trigger-job', {id: row.id}, null, {toastError: false}).then(() => {
109
144
  this.tableRef.current.reload();
145
+ this.loadProgress()
110
146
  }).catch(e => {
111
147
  console.error('[Job] 触发任务失败:', e);
112
148
  })
@@ -147,6 +183,38 @@ export default class JobPage extends React.Component {
147
183
  },
148
184
  },
149
185
 
186
+ {
187
+ title: '日志聚合',
188
+ dataIndex: 'logAggregation',
189
+ render: v => DictUtils.dictTag('jobLogAggregation', v) || '每次执行',
190
+ },
191
+
192
+ {
193
+ title: '运行状态',
194
+ dataIndex: 'progress',
195
+ width: 220,
196
+ render: (_, record) => {
197
+ const p = this.state.progressMap[record.id]
198
+ if (!p) {
199
+ return <Tag>未运行</Tag>
200
+ }
201
+ return <div>
202
+ <Tag color={p.stateColor || 'default'}>{p.stateLabel}</Tag>
203
+ {p.percent != null && <Progress percent={p.percent} size='small'/>}
204
+ {p.message && <Tooltip title={p.message}>
205
+ <div style={{
206
+ fontSize: 12,
207
+ color: 'var(--oa-color-text-secondary, rgba(0, 0, 0, 0.65))',
208
+ overflow: 'hidden',
209
+ textOverflow: 'ellipsis',
210
+ whiteSpace: 'nowrap',
211
+ maxWidth: 200
212
+ }}>{p.message}</div>
213
+ </Tooltip>}
214
+ </div>
215
+ },
216
+ },
217
+
150
218
  {
151
219
  title: '操作',
152
220
  dataIndex: 'option',
@@ -157,7 +225,7 @@ export default class JobPage extends React.Component {
157
225
  more
158
226
  size="small"
159
227
  actions={[
160
- {label: '执行记录', onClick: () => this.showExecuteRecord(record)},
228
+ {label: '执行情况', perm: 'job:read', onClick: () => this.showExecuteRecord(record)},
161
229
  {label: '执行一次', perm: 'job:trigger', onClick: () => this.handleTriggerJob(record)},
162
230
  {label: '编辑', perm: 'job:update', onClick: () => this.handleEdit(record)},
163
231
  {label: '删除', perm: 'job:delete', confirm: '是否确定删除?', onClick: () => this.handleDelete(record)},
@@ -179,19 +247,51 @@ export default class JobPage extends React.Component {
179
247
 
180
248
  showExecuteRecord(record) {
181
249
  this.setState({executeRecordOpen: true, selectedRecord: record})
250
+ this.loadProgress()
251
+ }
252
+
253
+ /**
254
+ * 执行记录弹窗顶部:当前任务的运行中进度(内存态)
255
+ */
256
+ renderRunningProgress() {
257
+ const {selectedRecord, progressMap} = this.state
258
+ const p = selectedRecord ? progressMap[selectedRecord.id] : null
259
+ if (!p || p.state !== 'RUNNING') {
260
+ return null
261
+ }
262
+ return <Alert
263
+ type='info'
264
+ showIcon
265
+ style={{marginBottom: 12}}
266
+ title={<div>
267
+ <div style={{marginBottom: 6}}>{p.message || '任务运行中'}</div>
268
+ {p.percent != null && <Progress percent={p.percent} size='small'/>}
269
+ </div>}
270
+ />
182
271
  }
183
272
 
184
273
  showLog = (record) => {
185
- this.setState({logOpen: true, logLoading: true, logContent: '', selectedLogRecord: record})
186
- HttpClient.get('admin/sys/log/job/' + record.id, null, {toastError: false}).then(content => {
187
- this.setState({
188
- logContent: typeof content === 'string' ? content : JSON.stringify(content),
189
- logLoading: false
190
- })
191
- }).catch(e => {
192
- console.error('[Job] 加载任务日志失败:', e)
193
- this.setState({logContent: '日志加载失败:' + (e?.message || ''), logLoading: false})
194
- })
274
+ this.setState({logOpen: true, selectedLogRecord: record})
275
+ }
276
+
277
+ /**
278
+ * 该执行记录对应的日志是否仍在写入:用进度快照里的 logId 精确匹配
279
+ */
280
+ isLogRunning = () => {
281
+ const {selectedLogRecord, progressMap} = this.state
282
+ if (!selectedLogRecord) {
283
+ return false
284
+ }
285
+ return Object.values(progressMap).some(p =>
286
+ p.logId === selectedLogRecord.id && p.state === 'RUNNING')
287
+ }
288
+
289
+ /** 结束时间与开始时间为同一天时只显示时分秒,避免重复日期 */
290
+ formatRecordEndTime = (beginTime, endTime) => {
291
+ if (!beginTime || !endTime) {
292
+ return endTime || '-'
293
+ }
294
+ return endTime.slice(0, 10) === beginTime.slice(0, 10) ? endTime.slice(11) : endTime
195
295
  }
196
296
 
197
297
 
@@ -241,6 +341,11 @@ export default class JobPage extends React.Component {
241
341
  <Switch/>
242
342
  </Form.Item>
243
343
 
344
+ <Form.Item label='日志聚合' name='logAggregation' initialValue='PER_RUN'
345
+ help='高频任务建议按天聚合:一天只保留一条执行记录,日志文件按天合并'>
346
+ <FieldDictSelect typeCode='jobLogAggregation'/>
347
+ </Form.Item>
348
+
244
349
  {this.state.paramList?.map(p => (
245
350
  <div key={p.name}>
246
351
  <Divider>任务参数</Divider>
@@ -276,38 +381,55 @@ export default class JobPage extends React.Component {
276
381
  width={1024}
277
382
  destroyOnHidden
278
383
  >
279
- <ProTable columns={[{
280
- title: '开始时间',
281
- dataIndex: 'beginTime',
282
- },
384
+ {this.renderRunningProgress()}
385
+ <ProTable columns={[
283
386
  {
284
- title: '结束时间',
285
- dataIndex: 'endTime',
387
+ title: '开始 / 结束时间',
388
+ dataIndex: 'beginTime',
389
+ width: 170,
390
+ render: (v, record) => <div style={{lineHeight: 1.45}}>
391
+ <div>{v || '-'}</div>
392
+ <div style={{
393
+ fontSize: 12,
394
+ color: 'var(--oa-color-text-tertiary, rgba(0, 0, 0, 0.45))',
395
+ whiteSpace: 'nowrap'
396
+ }}>{record.endTime
397
+ ? '至 ' + this.formatRecordEndTime(v, record.endTime)
398
+ : '运行中…'}</div>
399
+ </div>
286
400
  },
287
401
  {
288
- title: '耗时',
289
- dataIndex: 'jobRunTimeLabel',
290
-
291
- },
292
- {
293
- title: '是否成功',
294
- dataIndex: 'success',
295
- width: 200,
296
- render: v => {
297
- if (v != null) {
298
- return v ? '成功' : '异常'
402
+ title: '成功/失败',
403
+ dataIndex: 'executeCount',
404
+ width: 150,
405
+ render: (v, record) => {
406
+ const count = v == null ? 1 : v
407
+ // 旧版本升级上来的记录没有 failCount,用 success 兜底
408
+ const failCount = record.failCount == null
409
+ ? (record.success === false ? 1 : 0)
410
+ : record.failCount
411
+ const successCount = Math.max(count - failCount, 0)
412
+ if (successCount === 0 && failCount === 0) {
413
+ return '-'
299
414
  }
300
- }
415
+ return <span>
416
+ {successCount > 0 &&
417
+ <Tag color='success' style={{marginRight: 4}}>成功 {successCount}</Tag>}
418
+ {failCount > 0 &&
419
+ <Tag color='error' style={{marginRight: 0}}>失败 {failCount}</Tag>}
420
+ </span>
421
+ },
301
422
  },
302
423
  {
303
424
  title: '返回结果',
304
425
  dataIndex: 'result',
305
- width: 300
426
+ render: v => (v == null || v === '') ? '-' : v,
306
427
  },
307
-
308
428
  {
309
429
  title: '操作',
310
430
  dataIndex: 'option',
431
+ width: 64,
432
+ fixed: 'right',
311
433
  render: (_, record) => <a onClick={() => this.showLog(record)}>日志</a>,
312
434
  }
313
435
  ]} request={(params) => {
@@ -323,21 +445,16 @@ export default class JobPage extends React.Component {
323
445
  width={1024}
324
446
  destroyOnHidden
325
447
  footer={this.state.selectedLogRecord ? (
326
- <a href={UrlUtils.contextPath('/admin/sys/log/job/' + this.state.selectedLogRecord.id)}
448
+ <a href={UrlUtils.contextPath('/admin/sys/log/raw/job/' + this.state.selectedLogRecord.id)}
327
449
  target='_blank' rel='noreferrer'>在新窗口打开</a>
328
450
  ) : null}
329
451
  >
330
- <Spin spinning={this.state.logLoading}>
331
- <pre style={{
332
- margin: 0,
333
- maxHeight: '60vh',
334
- overflow: 'auto',
335
- whiteSpace: 'pre-wrap',
336
- wordBreak: 'break-all',
337
- fontSize: 12,
338
- lineHeight: 1.6
339
- }}>{this.state.logContent}</pre>
340
- </Spin>
452
+ {this.state.selectedLogRecord && (
453
+ <LogViewer key={this.state.selectedLogRecord.id}
454
+ logKey={'job/' + this.state.selectedLogRecord.id}
455
+ running={this.isLogRunning()}
456
+ height={480}/>
457
+ )}
341
458
  </Modal>
342
459
  </Page>
343
460
  }
@@ -32,7 +32,7 @@ export default class UserCenterPage extends React.Component {
32
32
 
33
33
  render() {
34
34
  const {info, changePwdOpen, changePwdSuccess} = this.state;
35
- return <Page backgroundGray>
35
+ return <Page>
36
36
 
37
37
  <Card title="个人信息"
38
38
  style={{marginBottom: 16}}
@@ -26,9 +26,9 @@ table.oa-table {
26
26
  background-color: var(--oa-color-fill, #f5f5f5); /* 悬停背景色 */
27
27
  }
28
28
 
29
- /* 斑马纹效果 - 交替行颜色 */
29
+ /* 斑马纹效果 - 用最浅一档填充色;若用页面底色,暗色下会在容器上形成纯黑条 */
30
30
  tr:nth-child(even) {
31
- background-color: var(--oa-color-bg-layout, #f9f9f9);
31
+ background-color: var(--oa-color-fill-quaternary, #f9f9f9);
32
32
  }
33
33
 
34
34
  /* 响应式表格 - 在小屏幕上添加水平滚动 */