@jiangood/open-admin 3.1.9 → 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.
package/package.json CHANGED
@@ -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.9",
58
+ "version": "3.1.10",
59
59
  "overrides": {
60
60
  "zod-validation-error": "^4.0.2"
61
61
  }
@@ -7,6 +7,8 @@ const SITE_ARTICLES_KEY = "siteArticles";
7
7
 
8
8
  export interface SiteInfo {
9
9
  title?: string;
10
+ /** 系统简称,用于侧栏等空间受限处;未配置时前端回退到 title */
11
+ shortTitle?: string;
10
12
  waterMark?: boolean;
11
13
  /** 登录失效(空闲超时/未登录)时全局弹窗的提示文案,对应 sys.login-expired-message */
12
14
  loginExpiredMessage?: string;
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
- import {CloseOutlined} from '@ant-design/icons';
2
+ import {CloseOutlined, DownOutlined} from '@ant-design/icons';
3
+ import {Dropdown} from 'antd';
3
4
  import {ContextMenu} from '../../framework';
4
5
  import './AdminTabs.less';
5
6
 
@@ -7,34 +8,84 @@ export class AdminTabs extends React.Component {
7
8
  state = {
8
9
  contextMenu: null,
9
10
  contextMenuTabKey: null,
11
+ overflowing: false,
10
12
  };
11
13
 
12
14
  barRef = React.createRef();
13
15
  activeTabRef = React.createRef();
16
+ resizeObserver = null;
14
17
 
15
18
  componentDidMount() {
16
19
  this.scrollActiveIntoView();
20
+ this.updateOverflow();
21
+ this.observeOverflow();
22
+ this.barRef.current?.addEventListener('wheel', this.handleWheel, {passive: false});
17
23
  }
18
24
 
19
25
  componentDidUpdate(prevProps) {
20
26
  if (prevProps.activeKey !== this.props.activeKey) {
21
27
  this.scrollActiveIntoView();
22
28
  }
29
+ this.updateOverflow();
23
30
  }
24
31
 
32
+ componentWillUnmount() {
33
+ this.resizeObserver?.disconnect();
34
+ this.barRef.current?.removeEventListener('wheel', this.handleWheel);
35
+ }
36
+
37
+ observeOverflow = () => {
38
+ if (typeof ResizeObserver === 'undefined' || !this.barRef.current) return;
39
+ this.resizeObserver = new ResizeObserver(() => {
40
+ this.updateOverflow();
41
+ // 窗口变窄后激活标签可能被挤出可视区,跟随重新定位
42
+ this.scrollActiveIntoView();
43
+ });
44
+ this.resizeObserver.observe(this.barRef.current);
45
+ };
46
+
47
+ /** 标签总宽是否超过条带可用宽度(不含溢出时给按钮预留的 padding) */
48
+ isOverflowing = () => {
49
+ const bar = this.barRef.current;
50
+ if (!bar) return false;
51
+ const tabsWidth = [...bar.querySelectorAll('.admin-tab')]
52
+ .reduce((sum, tab) => sum + tab.getBoundingClientRect().width, 0);
53
+ // 基础左右 padding 各 8px;溢出时额外加的 40px 不参与判断,避免无法恢复
54
+ return tabsWidth > bar.clientWidth - 16 + 1;
55
+ };
56
+
57
+ /** 标签超出可视宽度时显示右侧「全部标签」入口 */
58
+ updateOverflow = () => {
59
+ const overflowing = this.isOverflowing();
60
+ if (overflowing !== this.state.overflowing) {
61
+ this.setState({overflowing});
62
+ }
63
+ };
64
+
65
+ /** 竖向滚轮在标签栏上转为横向滚动,避免标签被隐藏时无操作入口 */
66
+ handleWheel = e => {
67
+ const bar = this.barRef.current;
68
+ if (!bar || !this.isOverflowing()) return;
69
+ if (Math.abs(e.deltaY) <= Math.abs(e.deltaX)) return;
70
+ bar.scrollLeft += e.deltaY;
71
+ e.preventDefault();
72
+ };
73
+
25
74
  /** 激活的标签被滚动到可视区外时(标签过多),横向带入视野 */
26
75
  scrollActiveIntoView = () => {
27
76
  const bar = this.barRef.current;
28
77
  const tab = this.activeTabRef.current;
29
78
  if (!bar || !tab) return;
30
79
  const margin = 8;
80
+ // 溢出时右侧有「全部标签」按钮,避免激活标签滚到按钮下面
81
+ const rightMargin = this.isOverflowing() ? 44 : margin;
31
82
  const barRect = bar.getBoundingClientRect();
32
83
  const tabRect = tab.getBoundingClientRect();
33
84
  let delta = 0;
34
85
  if (tabRect.left < barRect.left + margin) {
35
86
  delta = tabRect.left - barRect.left - margin;
36
- } else if (tabRect.right > barRect.right - margin) {
37
- delta = tabRect.right - barRect.right + margin;
87
+ } else if (tabRect.right > barRect.right - rightMargin) {
88
+ delta = tabRect.right - barRect.right + rightMargin;
38
89
  }
39
90
  if (delta !== 0) {
40
91
  bar.scrollTo({left: bar.scrollLeft + delta, behavior: 'smooth'});
@@ -66,41 +117,64 @@ export class AdminTabs extends React.Component {
66
117
 
67
118
  render() {
68
119
  const {tabs, activeKey, onChange, onClose} = this.props;
69
- const {contextMenu} = this.state;
120
+ const {contextMenu, overflowing} = this.state;
70
121
 
71
122
  return (
72
123
  <>
73
- <div className="admin-tabs-bar" ref={this.barRef}>
74
- {tabs.map(tab => (
75
- <div
76
- key={tab.key}
77
- ref={tab.key === activeKey ? this.activeTabRef : null}
78
- className={`admin-tab${tab.key === activeKey ? ' active' : ''}`}
79
- role="tab"
80
- title={tab.label}
81
- tabIndex={tab.key === activeKey ? 0 : -1}
82
- aria-selected={tab.key === activeKey}
83
- onClick={() => onChange(tab.key)}
84
- onKeyDown={e => {
85
- if (e.key === 'Enter' || e.key === ' ') {
86
- e.preventDefault();
87
- onChange(tab.key);
88
- }
89
- }}
90
- onContextMenu={e => this.handleContextMenu(e, tab.key)}
91
- >
92
- <span className="admin-tab-label">{tab.label}</span>
93
- {tabs.length > 1 && (
94
- <CloseOutlined
95
- className="admin-tab-close"
96
- onClick={e => {
97
- e.stopPropagation();
98
- onClose(tab.key);
99
- }}
100
- />
101
- )}
102
- </div>
103
- ))}
124
+ <div className={`admin-tabs-wrap${overflowing ? ' overflowing' : ''}`}>
125
+ <div className="admin-tabs-bar" ref={this.barRef}>
126
+ {tabs.map(tab => (
127
+ <div
128
+ key={tab.key}
129
+ ref={tab.key === activeKey ? this.activeTabRef : null}
130
+ className={`admin-tab${tab.key === activeKey ? ' active' : ''}`}
131
+ role="tab"
132
+ title={tab.label}
133
+ tabIndex={tab.key === activeKey ? 0 : -1}
134
+ aria-selected={tab.key === activeKey}
135
+ onClick={() => onChange(tab.key)}
136
+ onKeyDown={e => {
137
+ if (e.key === 'Enter' || e.key === ' ') {
138
+ e.preventDefault();
139
+ onChange(tab.key);
140
+ }
141
+ }}
142
+ onContextMenu={e => this.handleContextMenu(e, tab.key)}
143
+ >
144
+ <span className="admin-tab-label">{tab.label}</span>
145
+ {tabs.length > 1 && (
146
+ <CloseOutlined
147
+ className="admin-tab-close"
148
+ onClick={e => {
149
+ e.stopPropagation();
150
+ onClose(tab.key);
151
+ }}
152
+ />
153
+ )}
154
+ </div>
155
+ ))}
156
+ </div>
157
+ {overflowing && (
158
+ <>
159
+ <div className="admin-tabs-fade"/>
160
+ <Dropdown
161
+ trigger={['click']}
162
+ placement="bottomRight"
163
+ menu={{
164
+ items: tabs.map(tab => ({key: tab.key, label: tab.label})),
165
+ selectable: true,
166
+ selectedKeys: [activeKey],
167
+ onClick: ({key}) => onChange(key),
168
+ style: {maxHeight: 320, overflowY: 'auto'},
169
+ }}
170
+ >
171
+ <button type="button" className="admin-tabs-more" title="全部标签"
172
+ aria-label="全部标签">
173
+ <DownOutlined/>
174
+ </button>
175
+ </Dropdown>
176
+ </>
177
+ )}
104
178
  </div>
105
179
  {contextMenu && (
106
180
  <ContextMenu
@@ -1,15 +1,24 @@
1
+ .admin-tabs-wrap {
2
+ position: relative;
3
+ display: flex;
4
+ flex-shrink: 0;
5
+ min-width: 0;
6
+ }
7
+
1
8
  .admin-tabs-bar {
2
9
  display: flex;
3
10
  flex-wrap: nowrap;
4
- align-items: flex-end;
5
- gap: 2px;
11
+ align-items: stretch;
6
12
  overflow-x: auto;
7
13
  overflow-y: hidden;
8
- flex-shrink: 0;
9
- padding: 4px 8px 0;
10
- border-bottom: 1px solid var(--oa-color-border, #e8e8e8);
11
- /* 条带用页面底色,与白色顶栏拉开层次;选中标签再用容器色浮起 */
12
- background: var(--oa-color-bg-layout, #f5f5f5);
14
+ scrollbar-width: none;
15
+ flex: 1 1 auto;
16
+ min-width: 0;
17
+ height: var(--oa-control-height, 32px);
18
+ padding: 0 8px;
19
+ /* 条带与顶栏同色,和灰色页面拉开层次;底线用 inset 阴影绘制,选中标签可盖住 */
20
+ box-shadow: inset 0 -1px 0 var(--oa-color-border, #e8e8e8);
21
+ background: var(--oa-color-bg-container, #fff);
13
22
 
14
23
  &::-webkit-scrollbar {
15
24
  height: 0;
@@ -19,23 +28,26 @@
19
28
  display: inline-flex;
20
29
  align-items: center;
21
30
  gap: 6px;
22
- padding: 0 8px 0 12px;
23
- height: var(--oa-control-height, 32px);
24
- line-height: var(--oa-control-height, 32px);
31
+ padding: 0 8px 0 10px;
25
32
  font-size: 13px;
33
+ line-height: 1.4;
26
34
  cursor: pointer;
27
35
  white-space: nowrap;
28
36
  color: var(--oa-color-text-secondary, rgba(0, 0, 0, 0.65));
29
37
  background: transparent;
30
- border-right: none;
31
- border-radius: var(--oa-border-radius, 6px) var(--oa-border-radius, 6px) 0 0;
32
- transition: color 0.2s, background 0.2s;
33
- position: relative;
38
+ /* 方形标签,标签之间仅用 1px 竖线分隔,贴近系统 / IDEA 原生观感 */
39
+ border-radius: 0;
40
+ transition: color 0.15s, background 0.15s;
34
41
  flex-shrink: 0;
35
42
 
43
+ /* 分隔线画在相邻标签之间,条带首尾不留多余竖线 */
44
+ & + .admin-tab {
45
+ border-left: 1px solid var(--oa-color-border, #e8e8e8);
46
+ }
47
+
36
48
  &:hover {
37
49
  color: var(--oa-color-text, rgba(0, 0, 0, 0.88));
38
- background: color-mix(in srgb, var(--oa-color-text, rgba(0, 0, 0, 0.88)) 6%, transparent);
50
+ background: var(--oa-color-fill, rgba(0, 0, 0, 0.06));
39
51
  }
40
52
 
41
53
  &:focus-visible {
@@ -44,11 +56,9 @@
44
56
  }
45
57
 
46
58
  &.active {
59
+ /* IDEA 式:选中标签用页面底色盖住条带底线,文字用主色点明选中项 */
47
60
  color: var(--primary-color, #1677ff);
48
- background: var(--oa-color-bg-container, #fff);
49
- font-weight: 500;
50
- /* 顶部主色指示条;inset 保证不撑高标签 */
51
- box-shadow: inset 0 2px 0 var(--primary-color, #1677ff);
61
+ background: var(--oa-color-bg-layout, #f5f5f5);
52
62
  }
53
63
 
54
64
  .admin-tab-label {
@@ -82,3 +92,51 @@
82
92
  }
83
93
  }
84
94
  }
95
+
96
+ /* 标签溢出:右侧渐隐 + 全部标签入口,改由按钮触发下拉 */
97
+ .admin-tabs-wrap.overflowing {
98
+ .admin-tabs-bar {
99
+ /* 给右侧按钮让位,滚到最右时最后一个标签不会被遮住 */
100
+ padding-right: 40px;
101
+ }
102
+
103
+ .admin-tabs-fade {
104
+ position: absolute;
105
+ top: 0;
106
+ right: 0;
107
+ bottom: 1px;
108
+ width: 44px;
109
+ pointer-events: none;
110
+ background: linear-gradient(to right, transparent, var(--oa-color-bg-container, #fff) 68%);
111
+ }
112
+
113
+ .admin-tabs-more {
114
+ position: absolute;
115
+ top: 50%;
116
+ right: 6px;
117
+ transform: translateY(-50%);
118
+ display: inline-flex;
119
+ align-items: center;
120
+ justify-content: center;
121
+ width: 24px;
122
+ height: 24px;
123
+ padding: 0;
124
+ border: none;
125
+ border-radius: var(--oa-border-radius, 6px);
126
+ background: var(--oa-color-bg-container, #fff);
127
+ color: var(--oa-color-text-secondary, rgba(0, 0, 0, 0.65));
128
+ font-size: 12px;
129
+ cursor: pointer;
130
+ transition: background 0.15s, color 0.15s;
131
+
132
+ &:hover {
133
+ background: var(--oa-color-fill, rgba(0, 0, 0, 0.06));
134
+ color: var(--oa-color-text, rgba(0, 0, 0, 0.88));
135
+ }
136
+
137
+ &:focus-visible {
138
+ outline: 2px solid var(--primary-color, #1677ff);
139
+ outline-offset: -2px;
140
+ }
141
+ }
142
+ }
@@ -202,6 +202,8 @@ export default class AdminLayout extends React.Component {
202
202
 
203
203
  render() {
204
204
  const {siteInfo, loginInfo} = this.state
205
+ // 侧栏空间有限,优先显示系统简称;未配置时回退完整标题
206
+ const siderTitle = (siteInfo.shortTitle || '').trim() || siteInfo.title || '首页';
205
207
 
206
208
  return <Layout className='main-layout'>
207
209
  {/* 侧栏单独套暗色算法,使分割线/边框在深色底上可见,并按预设覆盖侧栏底色 */}
@@ -217,7 +219,8 @@ return <Layout className='main-layout'>
217
219
  <img src="./logo.png" alt={siteInfo.title || '首页'}/>
218
220
  </button>
219
221
  <h3 className='hide-on-mobile'>
220
- <Link to="/" style={{color: 'var(--oa-sider-text, rgba(255,255,255,0.85))'}}>{siteInfo.title}</Link>
222
+ <Link to="/" title={siteInfo.title || siderTitle}
223
+ style={{color: 'var(--oa-sider-text, rgba(255,255,255,0.85))'}}>{siderTitle}</Link>
221
224
  </h3>
222
225
  </div>
223
226
  {this.renderLeftMenu()}
@@ -61,13 +61,13 @@
61
61
  font: inherit;
62
62
 
63
63
  &:hover {
64
- color: var(--primary-color, #1677ff);
64
+ /* hover 只给底色,主色保留给当前项,避免悬停项比 active 更抢眼 */
65
+ color: var(--oa-color-text, rgba(0, 0, 0, 0.88));
65
66
  background: var(--oa-color-fill, rgba(0, 0, 0, 0.06));
66
67
  }
67
68
 
68
69
  &.active {
69
70
  color: var(--primary-color, #1677ff);
70
- box-shadow: inset 0 -2px 0 var(--primary-color, #1677ff);
71
71
  }
72
72
  }
73
73
  }
@@ -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>
@@ -225,7 +225,7 @@ export default class JobPage extends React.Component {
225
225
  more
226
226
  size="small"
227
227
  actions={[
228
- {label: '执行记录', onClick: () => this.showExecuteRecord(record)},
228
+ {label: '执行情况', perm: 'job:read', onClick: () => this.showExecuteRecord(record)},
229
229
  {label: '执行一次', perm: 'job:trigger', onClick: () => this.handleTriggerJob(record)},
230
230
  {label: '编辑', perm: 'job:update', onClick: () => this.handleEdit(record)},
231
231
  {label: '删除', perm: 'job:delete', confirm: '是否确定删除?', onClick: () => this.handleDelete(record)},
@@ -286,6 +286,14 @@ export default class JobPage extends React.Component {
286
286
  p.logId === selectedLogRecord.id && p.state === 'RUNNING')
287
287
  }
288
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
295
+ }
296
+
289
297
 
290
298
  render() {
291
299
  return <Page>
@@ -374,56 +382,54 @@ export default class JobPage extends React.Component {
374
382
  destroyOnHidden
375
383
  >
376
384
  {this.renderRunningProgress()}
377
- <ProTable columns={[{
378
- title: '开始时间',
379
- dataIndex: 'beginTime',
380
- },
385
+ <ProTable columns={[
381
386
  {
382
- title: '结束时间',
383
- 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>
384
400
  },
385
401
  {
386
- title: '耗时',
387
- dataIndex: 'jobRunTimeLabel',
388
-
389
- },
390
- {
391
- title: '执行次数',
402
+ title: '成功/失败',
392
403
  dataIndex: 'executeCount',
393
- width: 90,
394
- render: v => v == null ? 1 : v,
395
- },
396
- {
397
- title: '失败次数',
398
- dataIndex: 'failCount',
399
- width: 90,
400
- render: v => (v == null || v === 0) ? '-' : <span style={{color: 'var(--oa-color-error, #ff4d4f)'}}>{v}</span>,
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 ? '成功' : '异常'
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 '-'
415
414
  }
416
- }
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
+ },
417
422
  },
418
423
  {
419
424
  title: '返回结果',
420
425
  dataIndex: 'result',
421
- width: 300
426
+ render: v => (v == null || v === '') ? '-' : v,
422
427
  },
423
-
424
428
  {
425
429
  title: '操作',
426
430
  dataIndex: 'option',
431
+ width: 64,
432
+ fixed: 'right',
427
433
  render: (_, record) => <a onClick={() => this.showLog(record)}>日志</a>,
428
434
  }
429
435
  ]} request={(params) => {