@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
|
@@ -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 -
|
|
37
|
-
delta = tabRect.right - barRect.right +
|
|
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=
|
|
74
|
-
{
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
e.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
e
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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:
|
|
5
|
-
gap: 2px;
|
|
11
|
+
align-items: stretch;
|
|
6
12
|
overflow-x: auto;
|
|
7
13
|
overflow-y: hidden;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
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
|
-
|
|
31
|
-
border-radius:
|
|
32
|
-
transition: color 0.
|
|
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:
|
|
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-
|
|
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
|
+
}
|
|
@@ -37,6 +37,7 @@ export default class AdminLayout extends React.Component {
|
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
unsubscribeLayout = null;
|
|
40
|
+
unsubscribeSiteInfo = null;
|
|
40
41
|
|
|
41
42
|
componentDidMount() {
|
|
42
43
|
// 判断是否手机端,自动收起菜单
|
|
@@ -54,12 +55,20 @@ export default class AdminLayout extends React.Component {
|
|
|
54
55
|
this.unsubscribeLayout = EventBus.on('layoutChange', () => {
|
|
55
56
|
this.setState({layoutMode: getLayoutMode()});
|
|
56
57
|
})
|
|
58
|
+
|
|
59
|
+
// 系统配置保存后刷新标题、水印等站点信息
|
|
60
|
+
this.unsubscribeSiteInfo = EventBus.on('siteInfoChange', () => {
|
|
61
|
+
this.setState({siteInfo: GlobalData.getSiteInfo()});
|
|
62
|
+
})
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
componentWillUnmount() {
|
|
60
66
|
if (this.unsubscribeLayout) {
|
|
61
67
|
this.unsubscribeLayout();
|
|
62
68
|
}
|
|
69
|
+
if (this.unsubscribeSiteInfo) {
|
|
70
|
+
this.unsubscribeSiteInfo();
|
|
71
|
+
}
|
|
63
72
|
}
|
|
64
73
|
|
|
65
74
|
|
|
@@ -202,6 +211,8 @@ export default class AdminLayout extends React.Component {
|
|
|
202
211
|
|
|
203
212
|
render() {
|
|
204
213
|
const {siteInfo, loginInfo} = this.state
|
|
214
|
+
// 侧栏空间有限,优先显示系统简称;未配置时回退完整标题
|
|
215
|
+
const siderTitle = (siteInfo.shortTitle || '').trim() || siteInfo.title || '首页';
|
|
205
216
|
|
|
206
217
|
return <Layout className='main-layout'>
|
|
207
218
|
{/* 侧栏单独套暗色算法,使分割线/边框在深色底上可见,并按预设覆盖侧栏底色 */}
|
|
@@ -217,7 +228,8 @@ return <Layout className='main-layout'>
|
|
|
217
228
|
<img src="./logo.png" alt={siteInfo.title || '首页'}/>
|
|
218
229
|
</button>
|
|
219
230
|
<h3 className='hide-on-mobile'>
|
|
220
|
-
<Link to="/"
|
|
231
|
+
<Link to="/" title={siteInfo.title || siderTitle}
|
|
232
|
+
style={{color: 'var(--oa-sider-text, rgba(255,255,255,0.85))'}}>{siderTitle}</Link>
|
|
221
233
|
</h3>
|
|
222
234
|
</div>
|
|
223
235
|
{this.renderLeftMenu()}
|
|
@@ -61,13 +61,13 @@
|
|
|
61
61
|
font: inherit;
|
|
62
62
|
|
|
63
63
|
&:hover {
|
|
64
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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'
|
|
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={
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
{
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
|
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'
|
|
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>
|