@jiangood/open-admin 3.1.2 → 3.1.3
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 +2 -3
- package/src/framework/biz/index.tsx +3 -7
- package/src/framework/components/ProTable/index.less +18 -0
- package/src/framework/components/ProTable/index.tsx +21 -9
- package/src/framework/fields/FieldDate/index.tsx +12 -58
- package/src/framework/fields/FieldDateRange/index.tsx +12 -58
- package/src/framework/fields/FieldEditor/index.tsx +1 -1
- package/src/framework/fields/FieldSysOrgTree/index.tsx +3 -5
- package/src/framework/fields/FieldSysOrgTreeSelect/index.tsx +2 -8
- package/src/framework/fields/datePickerSpecs.ts +22 -0
- package/src/framework/fields/orgTree.ts +3 -0
- package/src/pages/system/codegen/index.jsx +205 -0
- package/src/pages/public/test.jsx +0 -30
package/package.json
CHANGED
|
@@ -40,8 +40,7 @@
|
|
|
40
40
|
"type": "module",
|
|
41
41
|
"files": [
|
|
42
42
|
"src/*",
|
|
43
|
-
"vite-plugin/*"
|
|
44
|
-
"!src/pages/test/*"
|
|
43
|
+
"vite-plugin/*"
|
|
45
44
|
],
|
|
46
45
|
"main": "src/index.ts",
|
|
47
46
|
"exports": {
|
|
@@ -59,7 +58,7 @@
|
|
|
59
58
|
"test:e2e": "playwright test",
|
|
60
59
|
"test:e2e:ui": "playwright test --ui"
|
|
61
60
|
},
|
|
62
|
-
"version": "3.1.
|
|
61
|
+
"version": "3.1.3",
|
|
63
62
|
"overrides": {
|
|
64
63
|
"zod-validation-error": "^4.0.2"
|
|
65
64
|
}
|
|
@@ -27,14 +27,9 @@ class InnerFieldDeptTreeSelect extends React.Component<ComponentProps<typeof Fie
|
|
|
27
27
|
return <FieldRemoteTreeSelect url="admin/sysOrg/dept-tree" {...this.props} />;
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
-
class InnerFieldOrgTreeSelect extends React.Component<ComponentProps<typeof FieldRemoteTreeSelect>> {
|
|
31
|
-
render() {
|
|
32
|
-
return <FieldRemoteTreeSelect url="admin/sysOrg/dept-tree" {...this.props} />;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
30
|
class InnerFieldOrgTreeMultipleSelect extends React.Component<ComponentProps<typeof FieldRemoteTreeSelect>> {
|
|
36
31
|
render() {
|
|
37
|
-
return <
|
|
32
|
+
return <InnerFieldDeptTreeSelect multiple {...this.props} />;
|
|
38
33
|
}
|
|
39
34
|
}
|
|
40
35
|
|
|
@@ -42,6 +37,7 @@ export const FieldUserSelect = InnerFieldUserSelect;
|
|
|
42
37
|
export const FieldUserSelectMultiple = InnerFieldUserSelectMultiple;
|
|
43
38
|
export const FieldUnitTreeSelect = InnerFieldUnitTreeSelect;
|
|
44
39
|
export const FieldDeptTreeSelect = InnerFieldDeptTreeSelect;
|
|
45
|
-
|
|
40
|
+
/** FieldDeptTreeSelect 的别名 */
|
|
41
|
+
export const FieldOrgTreeSelect = InnerFieldDeptTreeSelect;
|
|
46
42
|
export const FieldOrgTreeMultipleSelect = InnerFieldOrgTreeMultipleSelect;
|
|
47
43
|
|
|
@@ -59,7 +59,25 @@
|
|
|
59
59
|
display: flex;
|
|
60
60
|
align-items: center;
|
|
61
61
|
justify-content: space-between;
|
|
62
|
+
gap: 8px;
|
|
62
63
|
margin: 8px;
|
|
64
|
+
|
|
65
|
+
.pro-table-toolbar-left {
|
|
66
|
+
display: flex;
|
|
67
|
+
align-items: center;
|
|
68
|
+
gap: 8px;
|
|
69
|
+
flex: 1;
|
|
70
|
+
min-width: 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.pro-table-toolbar-right {
|
|
74
|
+
display: flex;
|
|
75
|
+
align-items: center;
|
|
76
|
+
gap: 8px;
|
|
77
|
+
flex-shrink: 0;
|
|
78
|
+
/* 只提供右侧插槽时,space-between 退化为 flex-start,靠 auto 兜住右对齐 */
|
|
79
|
+
margin-left: auto;
|
|
80
|
+
}
|
|
63
81
|
}
|
|
64
82
|
}
|
|
65
83
|
|
|
@@ -23,6 +23,15 @@ export interface ProTableRequestResult<T = unknown> {
|
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/** 工具栏(含右侧插槽)渲染函数签名 */
|
|
27
|
+
export type ProTableToolBarRender<T = unknown> = (
|
|
28
|
+
params: Record<string, unknown>,
|
|
29
|
+
selection: {
|
|
30
|
+
selectedRows: T[];
|
|
31
|
+
selectedRowKeys: React.Key[];
|
|
32
|
+
}
|
|
33
|
+
) => React.ReactNode;
|
|
34
|
+
|
|
26
35
|
export interface ProTableProps<T = unknown> {
|
|
27
36
|
/**
|
|
28
37
|
* 数据请求(Promise 式),框架自动注入 page/size/sort 参数。
|
|
@@ -37,11 +46,10 @@ export interface ProTableProps<T = unknown> {
|
|
|
37
46
|
actionRef?: React.RefObject<ProTableActionRef | undefined>;
|
|
38
47
|
/** 获取搜索表单实例 */
|
|
39
48
|
formRef?: React.RefObject<FormInstance | undefined>;
|
|
40
|
-
/**
|
|
41
|
-
toolBarRender?:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}) => React.ReactNode;
|
|
49
|
+
/** 工具栏渲染(左侧),参数为当前搜索值与行选择状态 */
|
|
50
|
+
toolBarRender?: ProTableToolBarRender<T>;
|
|
51
|
+
/** 工具栏右侧渲染,参数同 toolBarRender;用于导出/清理/刷新等右侧操作 */
|
|
52
|
+
toolBarRightRender?: ProTableToolBarRender<T>;
|
|
45
53
|
rowKey?: string;
|
|
46
54
|
/** 行选择:true 为默认 checkbox,对象可覆盖 type/onChange */
|
|
47
55
|
rowSelection?: boolean | {
|
|
@@ -185,6 +193,7 @@ this.setState({loading: true})
|
|
|
185
193
|
render() {
|
|
186
194
|
const {
|
|
187
195
|
toolBarRender,
|
|
196
|
+
toolBarRightRender,
|
|
188
197
|
columns,
|
|
189
198
|
rowSelection,
|
|
190
199
|
rowKey = "id",
|
|
@@ -194,10 +203,13 @@ this.setState({loading: true})
|
|
|
194
203
|
return <div className={'oa-pro-table '} id={this.id}>
|
|
195
204
|
{this.renderForm()}
|
|
196
205
|
<div className="pro-table-wrapper">
|
|
197
|
-
{toolBarRender && <div className="pro-table-toolbar">
|
|
198
|
-
<div className="pro-table-toolbar-left">
|
|
206
|
+
{(toolBarRender || toolBarRightRender) && <div className="pro-table-toolbar">
|
|
207
|
+
{toolBarRender && <div className="pro-table-toolbar-left">
|
|
199
208
|
{this.getToolBarRenderNode(toolBarRender)}
|
|
200
|
-
</div>
|
|
209
|
+
</div>}
|
|
210
|
+
{toolBarRightRender && <div className="pro-table-toolbar-right">
|
|
211
|
+
{this.getToolBarRenderNode(toolBarRightRender)}
|
|
212
|
+
</div>}
|
|
201
213
|
</div>}
|
|
202
214
|
|
|
203
215
|
|
|
@@ -268,7 +280,7 @@ this.setState({loading: true})
|
|
|
268
280
|
)
|
|
269
281
|
};
|
|
270
282
|
|
|
271
|
-
getToolBarRenderNode(toolBarRender:
|
|
283
|
+
getToolBarRenderNode(toolBarRender: ProTableToolBarRender<T>) {
|
|
272
284
|
if (!toolBarRender) {
|
|
273
285
|
return
|
|
274
286
|
}
|
|
@@ -5,6 +5,7 @@ import React from "react";
|
|
|
5
5
|
import dayjs from "dayjs";
|
|
6
6
|
import {DatePicker, TimePicker} from "antd";
|
|
7
7
|
import {DateUtils} from "../../utils";
|
|
8
|
+
import {DATE_PICKER_SPECS} from '../datePickerSpecs';
|
|
8
9
|
import type {FieldProps} from '../types';
|
|
9
10
|
|
|
10
11
|
export interface FieldDateProps extends FieldProps<string> {
|
|
@@ -28,67 +29,20 @@ export class FieldDate extends React.Component<FieldDateProps> {
|
|
|
28
29
|
render() {
|
|
29
30
|
const {type, value, onChange, ...rest} = this.props;
|
|
30
31
|
const formattedType = DateUtils.convertTypeToFormat(type as string);
|
|
32
|
+
const spec = DATE_PICKER_SPECS[formattedType];
|
|
31
33
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return <DatePicker
|
|
35
|
-
value={this.strToDate(value, formattedType)}
|
|
36
|
-
onChange={v => onChange?.(this.dateToStr(v, 'YYYY'))}
|
|
37
|
-
picker="year"
|
|
38
|
-
{...rest}
|
|
39
|
-
/>;
|
|
40
|
-
case 'YYYY-MM':
|
|
41
|
-
return <DatePicker
|
|
42
|
-
value={this.strToDate(value, 'YYYY-MM')}
|
|
43
|
-
onChange={v => onChange?.(this.dateToStr(v, 'YYYY-MM'))}
|
|
44
|
-
picker="month"
|
|
45
|
-
{...rest}
|
|
46
|
-
/>;
|
|
47
|
-
case 'YYYY-QQ':
|
|
48
|
-
return <DatePicker
|
|
49
|
-
value={this.strToDate(value, 'YYYY-QQ')}
|
|
50
|
-
onChange={v => onChange?.(this.dateToStr(v, 'YYYY-QQ'))}
|
|
51
|
-
picker="quarter"
|
|
52
|
-
{...rest}
|
|
53
|
-
/>;
|
|
54
|
-
case 'YYYY-MM-DD':
|
|
55
|
-
return <DatePicker
|
|
56
|
-
value={this.strToDate(value, 'YYYY-MM-DD')}
|
|
57
|
-
onChange={v => onChange?.(this.dateToStr(v, 'YYYY-MM-DD'))}
|
|
58
|
-
{...rest}
|
|
59
|
-
></DatePicker>;
|
|
60
|
-
case 'YYYY-MM-DD HH:mm':
|
|
61
|
-
return <DatePicker
|
|
62
|
-
value={this.strToDate(value, 'YYYY-MM-DD HH:mm')}
|
|
63
|
-
onChange={v => onChange?.(this.dateToStr(v, 'YYYY-MM-DD HH:mm'))}
|
|
64
|
-
format='YYYY-MM-DD HH:mm'
|
|
65
|
-
showTime
|
|
66
|
-
{...rest}
|
|
67
|
-
></DatePicker>;
|
|
68
|
-
case 'YYYY-MM-DD HH:mm:ss':
|
|
69
|
-
return <DatePicker
|
|
70
|
-
value={this.strToDate(value, 'YYYY-MM-DD HH:mm:ss')}
|
|
71
|
-
onChange={v => onChange?.(this.dateToStr(v, 'YYYY-MM-DD HH:mm:ss'))}
|
|
72
|
-
showTime
|
|
73
|
-
{...rest}
|
|
74
|
-
></DatePicker>;
|
|
75
|
-
case 'HH:mm':
|
|
76
|
-
return <TimePicker
|
|
77
|
-
format='HH:mm'
|
|
78
|
-
value={this.strToDate(value, 'HH:mm')}
|
|
79
|
-
onChange={v => onChange?.(this.dateToStr(v, 'HH:mm'))}
|
|
80
|
-
{...rest}
|
|
81
|
-
></TimePicker>;
|
|
82
|
-
case 'HH:mm:ss':
|
|
83
|
-
return <TimePicker
|
|
84
|
-
value={this.strToDate(value, 'HH:mm:ss')}
|
|
85
|
-
onChange={v => onChange?.(this.dateToStr(v, 'HH:mm:ss'))}
|
|
86
|
-
{...rest}
|
|
87
|
-
></TimePicker>;
|
|
88
|
-
default:
|
|
89
|
-
return <div>未知组件 {formattedType}</div>;
|
|
34
|
+
if (!spec) {
|
|
35
|
+
return <div>未知组件 {formattedType}</div>;
|
|
90
36
|
}
|
|
91
37
|
|
|
38
|
+
const Picker = spec.time ? TimePicker : DatePicker;
|
|
39
|
+
return <Picker
|
|
40
|
+
value={this.strToDate(value, formattedType)}
|
|
41
|
+
onChange={v => onChange?.(this.dateToStr(v, formattedType))}
|
|
42
|
+
{...spec}
|
|
43
|
+
{...rest}
|
|
44
|
+
/>;
|
|
45
|
+
|
|
92
46
|
}
|
|
93
47
|
|
|
94
48
|
strToDate(value: string | number | dayjs.Dayjs | null | undefined, fmt: string): dayjs.Dayjs | null | undefined {
|
|
@@ -5,6 +5,7 @@ import React from "react";
|
|
|
5
5
|
import dayjs from "dayjs";
|
|
6
6
|
import {DatePicker, TimePicker} from "antd";
|
|
7
7
|
import {DateUtils, StringUtils} from "../../utils";
|
|
8
|
+
import {DATE_PICKER_SPECS} from '../datePickerSpecs';
|
|
8
9
|
import type {FieldProps} from '../types';
|
|
9
10
|
|
|
10
11
|
const SP = StringUtils.ISO_SPLITTER;
|
|
@@ -33,67 +34,20 @@ export class FieldDateRange extends React.Component<FieldDateRangeProps> {
|
|
|
33
34
|
render() {
|
|
34
35
|
const {type, value, onChange, ...rest} = this.props;
|
|
35
36
|
const formattedType = DateUtils.convertTypeToFormat(type as string);
|
|
37
|
+
const spec = DATE_PICKER_SPECS[formattedType];
|
|
36
38
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return <DatePicker.RangePicker
|
|
40
|
-
value={this.strToDate(value)}
|
|
41
|
-
onChange={v => onChange?.(this.dateToStr(v, 'YYYY'))}
|
|
42
|
-
picker="year"
|
|
43
|
-
{...rest}
|
|
44
|
-
/>;
|
|
45
|
-
case 'YYYY-MM':
|
|
46
|
-
return <DatePicker.RangePicker
|
|
47
|
-
value={this.strToDate(value)}
|
|
48
|
-
onChange={v => onChange?.(this.dateToStr(v, 'YYYY-MM'))}
|
|
49
|
-
picker="month"
|
|
50
|
-
{...rest}
|
|
51
|
-
/>;
|
|
52
|
-
case 'YYYY-QQ':
|
|
53
|
-
return <DatePicker.RangePicker
|
|
54
|
-
value={this.strToDate(value)}
|
|
55
|
-
onChange={v => onChange?.(this.dateToStr(v, 'YYYY-QQ'))}
|
|
56
|
-
picker="quarter"
|
|
57
|
-
{...rest}
|
|
58
|
-
/>;
|
|
59
|
-
case 'YYYY-MM-DD':
|
|
60
|
-
return <DatePicker.RangePicker
|
|
61
|
-
value={this.strToDate(value)}
|
|
62
|
-
onChange={v => onChange?.(this.dateToStr(v, 'YYYY-MM-DD'))}
|
|
63
|
-
{...rest}
|
|
64
|
-
/>;
|
|
65
|
-
case 'YYYY-MM-DD HH:mm':
|
|
66
|
-
return <DatePicker.RangePicker
|
|
67
|
-
value={this.strToDate(value)}
|
|
68
|
-
onChange={v => onChange?.(this.dateToStr(v, 'YYYY-MM-DD HH:mm'))}
|
|
69
|
-
format='YYYY-MM-DD HH:mm'
|
|
70
|
-
showTime
|
|
71
|
-
{...rest}
|
|
72
|
-
/>;
|
|
73
|
-
case 'YYYY-MM-DD HH:mm:ss':
|
|
74
|
-
return <DatePicker.RangePicker
|
|
75
|
-
value={this.strToDate(value)}
|
|
76
|
-
onChange={v => onChange?.(this.dateToStr(v, 'YYYY-MM-DD HH:mm:ss'))}
|
|
77
|
-
showTime
|
|
78
|
-
{...rest}
|
|
79
|
-
/>;
|
|
80
|
-
case 'HH:mm':
|
|
81
|
-
return <TimePicker.RangePicker
|
|
82
|
-
format='HH:mm'
|
|
83
|
-
value={this.strToDate(value)}
|
|
84
|
-
onChange={v => onChange?.(this.dateToStr(v, 'HH:mm'))}
|
|
85
|
-
{...rest}
|
|
86
|
-
/>;
|
|
87
|
-
case 'HH:mm:ss':
|
|
88
|
-
return <TimePicker.RangePicker
|
|
89
|
-
value={this.strToDate(value)}
|
|
90
|
-
onChange={v => onChange?.(this.dateToStr(v, 'HH:mm:ss'))}
|
|
91
|
-
{...rest}
|
|
92
|
-
/>;
|
|
93
|
-
default:
|
|
94
|
-
return <div>未知组件 {formattedType}</div>;
|
|
39
|
+
if (!spec) {
|
|
40
|
+
return <div>未知组件 {formattedType}</div>;
|
|
95
41
|
}
|
|
96
42
|
|
|
43
|
+
const Picker = spec.time ? TimePicker.RangePicker : DatePicker.RangePicker;
|
|
44
|
+
return <Picker
|
|
45
|
+
value={this.strToDate(value)}
|
|
46
|
+
onChange={v => onChange?.(this.dateToStr(v, formattedType))}
|
|
47
|
+
{...spec}
|
|
48
|
+
{...rest}
|
|
49
|
+
/>;
|
|
50
|
+
|
|
97
51
|
}
|
|
98
52
|
|
|
99
53
|
strToDate(v: string | null | undefined): [dayjs.Dayjs, dayjs.Dayjs] | null {
|
|
@@ -62,7 +62,7 @@ export class FieldEditor extends React.Component<FieldEditorProps, {loading: boo
|
|
|
62
62
|
plugins: [
|
|
63
63
|
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
|
|
64
64
|
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
|
|
65
|
-
'insertdatetime', 'media', 'table', '
|
|
65
|
+
'insertdatetime', 'media', 'table', 'help', 'wordcount',
|
|
66
66
|
'emoticons'
|
|
67
67
|
],
|
|
68
68
|
image_description: false,
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import React from "react";
|
|
5
5
|
import {FieldRemoteTree} from "../FieldRemoteTree";
|
|
6
|
+
import {orgTreeUrl} from '../orgTree';
|
|
6
7
|
import type {FieldProps} from '../types';
|
|
7
8
|
|
|
8
9
|
interface FieldSysOrgTreeProps extends FieldProps<string[]> {
|
|
@@ -16,10 +17,7 @@ export class FieldSysOrgTree extends React.Component<FieldSysOrgTreeProps> {
|
|
|
16
17
|
|
|
17
18
|
render() {
|
|
18
19
|
const {type, ...rest} = this.props;
|
|
19
|
-
|
|
20
|
-
'/admin/sysOrg/dept-tree':
|
|
21
|
-
'/admin/sysOrg/unit-tree';
|
|
22
|
-
return <FieldRemoteTree url={url} {...rest} />;
|
|
20
|
+
return <FieldRemoteTree url={orgTreeUrl(type)} {...rest} />;
|
|
23
21
|
}
|
|
24
22
|
|
|
25
|
-
}
|
|
23
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import {FieldRemoteTreeSelect} from "../FieldRemoteTreeSelect";
|
|
3
|
+
import {orgTreeUrl} from '../orgTree';
|
|
3
4
|
import type {FieldProps} from '../types';
|
|
4
5
|
|
|
5
6
|
interface FieldSysOrgTreeSelectProps extends FieldProps<string | string[]> {
|
|
@@ -14,13 +15,6 @@ export class FieldSysOrgTreeSelect extends React.Component<FieldSysOrgTreeSelect
|
|
|
14
15
|
|
|
15
16
|
render() {
|
|
16
17
|
const {type, ...rest} = this.props;
|
|
17
|
-
|
|
18
|
-
if (type === 'unit') {
|
|
19
|
-
url = '/admin/sysOrg/unit-tree';
|
|
20
|
-
} else if (type === 'shop') {
|
|
21
|
-
url = '/admin/sysOrg/tree?type=3';
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
return <FieldRemoteTreeSelect url={url} {...rest}/>;
|
|
18
|
+
return <FieldRemoteTreeSelect url={orgTreeUrl(type)} {...rest}/>;
|
|
25
19
|
}
|
|
26
20
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface DatePickerSpec {
|
|
2
|
+
/** antd picker 属性 */
|
|
3
|
+
picker?: 'year' | 'month' | 'quarter';
|
|
4
|
+
/** antd showTime 属性 */
|
|
5
|
+
showTime?: boolean;
|
|
6
|
+
/** antd format 属性 */
|
|
7
|
+
format?: string;
|
|
8
|
+
/** 使用 TimePicker 而非 DatePicker */
|
|
9
|
+
time?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** 日期类型 → antd 选择器属性映射(FieldDate / FieldDateRange 共用) */
|
|
13
|
+
export const DATE_PICKER_SPECS: Record<string, DatePickerSpec> = {
|
|
14
|
+
'YYYY': {picker: 'year'},
|
|
15
|
+
'YYYY-MM': {picker: 'month'},
|
|
16
|
+
'YYYY-QQ': {picker: 'quarter'},
|
|
17
|
+
'YYYY-MM-DD': {},
|
|
18
|
+
'YYYY-MM-DD HH:mm': {showTime: true, format: 'YYYY-MM-DD HH:mm'},
|
|
19
|
+
'YYYY-MM-DD HH:mm:ss': {showTime: true},
|
|
20
|
+
'HH:mm': {time: true, format: 'HH:mm'},
|
|
21
|
+
'HH:mm:ss': {time: true},
|
|
22
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import {Button, Collapse, Drawer, Form, Input, message, Modal, Select, Space, Switch, Tag, Typography} from 'antd'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import {HttpClient, Page, Perm} from '../../../framework'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 代码生成:动态扫描实体,按实体生成常见 CRUD 代码并写入项目源码目录。
|
|
7
|
+
*/
|
|
8
|
+
export default class CodegenPage extends React.Component {
|
|
9
|
+
|
|
10
|
+
state = {
|
|
11
|
+
entities: [],
|
|
12
|
+
className: undefined,
|
|
13
|
+
module: '',
|
|
14
|
+
label: '',
|
|
15
|
+
parentMenu: 'sys',
|
|
16
|
+
overwrite: false,
|
|
17
|
+
|
|
18
|
+
files: [],
|
|
19
|
+
previewOpen: false,
|
|
20
|
+
confirmOpen: false,
|
|
21
|
+
|
|
22
|
+
result: null,
|
|
23
|
+
resultOpen: false,
|
|
24
|
+
|
|
25
|
+
loading: false,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
componentDidMount() {
|
|
29
|
+
this.loadEntities()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
loadEntities = () => {
|
|
33
|
+
HttpClient.get('admin/codegen/entity-options', null, {toastError: false}).then(rs => {
|
|
34
|
+
this.setState({entities: rs.data || []})
|
|
35
|
+
}).catch(e => {
|
|
36
|
+
console.error('[Codegen] 加载实体列表失败:', e)
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
handleEntityChange = (value, option) => {
|
|
41
|
+
const data = option?.data || {}
|
|
42
|
+
this.setState({className: value, module: data.module || '', label: data.label || ''})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
buildReq = () => ({
|
|
46
|
+
className: this.state.className,
|
|
47
|
+
module: this.state.module,
|
|
48
|
+
label: this.state.label,
|
|
49
|
+
parentMenu: this.state.parentMenu,
|
|
50
|
+
overwrite: this.state.overwrite,
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
handlePreview = () => {
|
|
54
|
+
if (!this.state.className) {
|
|
55
|
+
message.warning('请先选择实体')
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
this.setState({loading: true})
|
|
59
|
+
HttpClient.post('admin/codegen/preview', this.buildReq(), null, {toastError: false}).then(rs => {
|
|
60
|
+
this.setState({files: rs.data || [], previewOpen: true})
|
|
61
|
+
}).catch(e => {
|
|
62
|
+
console.error('[Codegen] 预览失败:', e)
|
|
63
|
+
message.error(e?.message || '预览失败')
|
|
64
|
+
}).finally(() => this.setState({loading: false}))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
handleGenerate = () => {
|
|
68
|
+
if (!this.state.className) {
|
|
69
|
+
message.warning('请先选择实体')
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
this.setState({confirmOpen: true})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
doGenerate = () => {
|
|
76
|
+
this.setState({confirmOpen: false, loading: true})
|
|
77
|
+
HttpClient.post('admin/codegen/generate', this.buildReq(), null, {toastError: false}).then(rs => {
|
|
78
|
+
this.setState({result: rs.data, resultOpen: true, previewOpen: false})
|
|
79
|
+
}).catch(e => {
|
|
80
|
+
console.error('[Codegen] 生成失败:', e)
|
|
81
|
+
message.error(e?.message || '生成失败')
|
|
82
|
+
}).finally(() => this.setState({loading: false}))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
copy = (text) => {
|
|
86
|
+
if (navigator.clipboard) {
|
|
87
|
+
navigator.clipboard.writeText(text)
|
|
88
|
+
.then(() => message.success('已复制'))
|
|
89
|
+
.catch(() => message.error('复制失败'))
|
|
90
|
+
} else {
|
|
91
|
+
message.warning('当前浏览器不支持剪贴板')
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
selectedOption = () => this.state.entities.find(e => e.value === this.state.className)
|
|
96
|
+
|
|
97
|
+
render() {
|
|
98
|
+
const selected = this.selectedOption()
|
|
99
|
+
return <Page title='代码生成' description='扫描实体,生成 Repository / Service / Controller / 前端页面 / 菜单 代码并写入项目源码目录'>
|
|
100
|
+
<Form layout='vertical' style={{maxWidth: 720}}>
|
|
101
|
+
<Form.Item label='实体' required>
|
|
102
|
+
<Select
|
|
103
|
+
showSearch
|
|
104
|
+
optionFilterProp='label'
|
|
105
|
+
placeholder='请选择实体'
|
|
106
|
+
options={this.state.entities}
|
|
107
|
+
value={this.state.className}
|
|
108
|
+
onChange={this.handleEntityChange}
|
|
109
|
+
/>
|
|
110
|
+
</Form.Item>
|
|
111
|
+
{selected?.data?.frameworkEntity &&
|
|
112
|
+
<Typography.Text type='warning'>
|
|
113
|
+
该实体属于框架内置包({selected.data.packageName}),生成的文件会写入同一包路径,请确认输出位置后再生成。
|
|
114
|
+
</Typography.Text>
|
|
115
|
+
}
|
|
116
|
+
<Form.Item label='模块名(请求前缀 / 前端目录)'>
|
|
117
|
+
<Input placeholder='如 customer'
|
|
118
|
+
value={this.state.module}
|
|
119
|
+
onChange={e => this.setState({module: e.target.value})}/>
|
|
120
|
+
</Form.Item>
|
|
121
|
+
<Form.Item label='中文名'>
|
|
122
|
+
<Input value={this.state.label}
|
|
123
|
+
onChange={e => this.setState({label: e.target.value})}/>
|
|
124
|
+
</Form.Item>
|
|
125
|
+
<Form.Item label='父菜单 id' tooltip='默认挂在系统管理(sys)下'>
|
|
126
|
+
<Input value={this.state.parentMenu}
|
|
127
|
+
onChange={e => this.setState({parentMenu: e.target.value})}/>
|
|
128
|
+
</Form.Item>
|
|
129
|
+
<Form.Item label='覆盖已存在文件'>
|
|
130
|
+
<Switch checked={this.state.overwrite}
|
|
131
|
+
onChange={checked => this.setState({overwrite: checked})}/>
|
|
132
|
+
</Form.Item>
|
|
133
|
+
<Space>
|
|
134
|
+
<Perm code='sys-codegen:generate'>
|
|
135
|
+
<Button type='primary' loading={this.state.loading} onClick={this.handlePreview}>预览</Button>
|
|
136
|
+
</Perm>
|
|
137
|
+
<Button onClick={this.loadEntities}>刷新实体</Button>
|
|
138
|
+
</Space>
|
|
139
|
+
</Form>
|
|
140
|
+
|
|
141
|
+
<Drawer title='生成预览' width={960}
|
|
142
|
+
open={this.state.previewOpen}
|
|
143
|
+
onClose={() => this.setState({previewOpen: false})}
|
|
144
|
+
extra={
|
|
145
|
+
<Perm code='sys-codegen:generate'>
|
|
146
|
+
<Button type='primary' loading={this.state.loading} onClick={this.handleGenerate}>生成并写入</Button>
|
|
147
|
+
</Perm>
|
|
148
|
+
}>
|
|
149
|
+
<Collapse
|
|
150
|
+
items={this.state.files.map((file, index) => ({
|
|
151
|
+
key: String(index),
|
|
152
|
+
label: (
|
|
153
|
+
<Space>
|
|
154
|
+
<span>{file.path}</span>
|
|
155
|
+
{file.exists
|
|
156
|
+
? <Tag color='orange'>已存在</Tag>
|
|
157
|
+
: <Tag color='green'>新增</Tag>}
|
|
158
|
+
</Space>
|
|
159
|
+
),
|
|
160
|
+
children: (
|
|
161
|
+
<>
|
|
162
|
+
<Button size='small' style={{marginBottom: 8}}
|
|
163
|
+
onClick={() => this.copy(file.content)}>复制</Button>
|
|
164
|
+
<pre style={{
|
|
165
|
+
maxHeight: 420,
|
|
166
|
+
overflow: 'auto',
|
|
167
|
+
background: '#f6f8fa',
|
|
168
|
+
padding: 12,
|
|
169
|
+
borderRadius: 4,
|
|
170
|
+
}}>{file.content}</pre>
|
|
171
|
+
</>
|
|
172
|
+
),
|
|
173
|
+
}))}
|
|
174
|
+
/>
|
|
175
|
+
</Drawer>
|
|
176
|
+
|
|
177
|
+
<Modal title='确认生成'
|
|
178
|
+
open={this.state.confirmOpen}
|
|
179
|
+
confirmLoading={this.state.loading}
|
|
180
|
+
onOk={this.doGenerate}
|
|
181
|
+
onCancel={() => this.setState({confirmOpen: false})}>
|
|
182
|
+
<p>将按以下配置生成代码到项目源码目录:</p>
|
|
183
|
+
<p>模块名:{this.state.module}</p>
|
|
184
|
+
<p>覆盖已存在文件:{this.state.overwrite ? '是' : '否'}</p>
|
|
185
|
+
</Modal>
|
|
186
|
+
|
|
187
|
+
<Modal title='生成结果'
|
|
188
|
+
open={this.state.resultOpen}
|
|
189
|
+
footer={null}
|
|
190
|
+
onCancel={() => this.setState({resultOpen: false})}>
|
|
191
|
+
<p>已写入 {this.state.result?.written?.length || 0} 个文件,跳过 {this.state.result?.skipped?.length || 0} 个。</p>
|
|
192
|
+
{this.state.result?.skipped?.length > 0 &&
|
|
193
|
+
<>
|
|
194
|
+
<Typography.Text type='warning'>以下文件已存在已跳过(可勾选“覆盖已存在文件”后重新生成):</Typography.Text>
|
|
195
|
+
<ul>
|
|
196
|
+
{this.state.result.skipped.map(path => <li key={path}>{path}</li>)}
|
|
197
|
+
</ul>
|
|
198
|
+
</>}
|
|
199
|
+
<ul>
|
|
200
|
+
{(this.state.result?.written || []).map(path => <li key={path}>{path}</li>)}
|
|
201
|
+
</ul>
|
|
202
|
+
</Modal>
|
|
203
|
+
</Page>
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import {Page} from "../../framework";
|
|
3
|
-
import {Button, message, Modal, Space} from "antd";
|
|
4
|
-
|
|
5
|
-
export default class TestPage extends React.Component {
|
|
6
|
-
|
|
7
|
-
state = {
|
|
8
|
-
modalOpen:false
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
render() {
|
|
12
|
-
return <Page title="测试组件">
|
|
13
|
-
|
|
14
|
-
<Space>
|
|
15
|
-
<Button type={"primary"} onClick={()=>{message.info('你好,李白!')}} >message.info</Button>
|
|
16
|
-
|
|
17
|
-
<Button danger onClick={()=>{Modal.info({content:'你好,李白!'})}} >modal.info</Button>
|
|
18
|
-
|
|
19
|
-
<Button onClick={()=>{this.setState({modalOpen:true})}} >modal.组件</Button>
|
|
20
|
-
|
|
21
|
-
</Space>
|
|
22
|
-
|
|
23
|
-
<Modal title='modal组件' open={this.state.modalOpen} onCancel={()=>this.setState({modalOpen:false})}>
|
|
24
|
-
内容内容
|
|
25
|
-
</Modal>
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
</Page>
|
|
29
|
-
}
|
|
30
|
-
}
|