@jiangood/open-admin 2.4.7 → 2.5.0
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/.umi-production/appData.json +58 -78
- package/src/.umi-production/core/route.tsx +1 -3
- package/src/framework/components/ContextMenu/index.tsx +91 -0
- package/src/framework/components/Page/index.less +19 -6
- package/src/framework/components/Page/index.tsx +23 -3
- package/src/framework/components/ProTable/index.less +60 -9
- package/src/framework/components/ProTable/index.tsx +31 -48
- package/src/framework/components/index.ts +1 -0
- package/src/framework/fields/FieldTableSelect/index.tsx +0 -1
- package/src/layouts/admin/HeaderRight.jsx +1 -6
- package/src/layouts/admin/TabPageRender.jsx +80 -23
- package/src/layouts/admin/index.jsx +16 -22
- package/src/layouts/admin/index.less +31 -20
- package/src/layouts/index.jsx +0 -9
- package/src/pages/about.jsx +1 -1
- package/src/pages/index.jsx +1 -1
- package/src/pages/login.jsx +1 -5
- package/src/pages/system/dict/index.jsx +56 -53
- package/src/pages/system/file/index.jsx +1 -7
- package/src/pages/system/job/index.jsx +9 -14
- package/src/pages/system/log/index.jsx +1 -1
- package/src/pages/system/org/index.jsx +153 -173
- package/src/pages/system/role/index.jsx +21 -12
- package/src/pages/system/role/perm.jsx +1 -2
- package/src/pages/system/user/index.jsx +28 -43
- package/src/pages/userCenter/index.jsx +3 -2
- package/src/pages/userCenter/message.jsx +1 -1
- package/src/style/global.less +1 -1
- package/src/framework/components/ProTable/components/ToolBar/index.less +0 -53
- package/src/framework/components/ProTable/components/ToolBar/index.tsx +0 -123
- package/src/pages/system/manual/index.jsx +0 -128
- package/src/pages/userCenter/manual.jsx +0 -57
|
@@ -19,20 +19,18 @@ if (process.env.NODE_ENV === 'development') {
|
|
|
19
19
|
import React from 'react';
|
|
20
20
|
|
|
21
21
|
export async function getRoutes() {
|
|
22
|
-
const routes = {"404":{"path":"*","id":"404","parentId":"@@/global-layout"},"userCenter/ChangePassword":{"path":"userCenter/ChangePassword","id":"userCenter/ChangePassword","parentId":"@@/global-layout"},"system/user/UserPerm":{"path":"system/user/UserPerm","id":"system/user/UserPerm","parentId":"@@/global-layout"},"
|
|
22
|
+
const routes = {"404":{"path":"*","id":"404","parentId":"@@/global-layout"},"userCenter/ChangePassword":{"path":"userCenter/ChangePassword","id":"userCenter/ChangePassword","parentId":"@@/global-layout"},"system/user/UserPerm":{"path":"system/user/UserPerm","id":"system/user/UserPerm","parentId":"@@/global-layout"},"userCenter/message":{"path":"userCenter/message","id":"userCenter/message","parentId":"@@/global-layout"},"system/dict/index":{"path":"system/dict","id":"system/dict/index","parentId":"@@/global-layout"},"system/file/index":{"path":"system/file","id":"system/file/index","parentId":"@@/global-layout"},"system/role/index":{"path":"system/role","id":"system/role/index","parentId":"@@/global-layout"},"system/user/index":{"path":"system/user","id":"system/user/index","parentId":"@@/global-layout"},"system/job/index":{"path":"system/job","id":"system/job/index","parentId":"@@/global-layout"},"system/log/index":{"path":"system/log","id":"system/log/index","parentId":"@@/global-layout"},"system/org/index":{"path":"system/org","id":"system/org/index","parentId":"@@/global-layout"},"system/role/perm":{"path":"system/role/perm","id":"system/role/perm","parentId":"@@/global-layout"},"userCenter/index":{"path":"userCenter","id":"userCenter/index","parentId":"@@/global-layout"},"about":{"path":"about","id":"about","parentId":"@@/global-layout"},"index":{"path":"/","id":"index","parentId":"@@/global-layout"},"login":{"path":"login","id":"login","parentId":"@@/global-layout"},"@@/global-layout":{"id":"@@/global-layout","path":"/","isLayout":true}} as const;
|
|
23
23
|
return {
|
|
24
24
|
routes,
|
|
25
25
|
routeComponents: {
|
|
26
26
|
'404': React.lazy(() => import(/* webpackChunkName: "src__pages__404" */'../../../src/pages/404.jsx')),
|
|
27
27
|
'userCenter/ChangePassword': React.lazy(() => import(/* webpackChunkName: "src__pages__userCenter__ChangePassword" */'../../../src/pages/userCenter/ChangePassword.jsx')),
|
|
28
28
|
'system/user/UserPerm': React.lazy(() => import(/* webpackChunkName: "src__pages__system__user__UserPerm" */'../../../src/pages/system/user/UserPerm.jsx')),
|
|
29
|
-
'system/manual/index': React.lazy(() => import(/* webpackChunkName: "src__pages__system__manual__index" */'../../../src/pages/system/manual/index.jsx')),
|
|
30
29
|
'userCenter/message': React.lazy(() => import(/* webpackChunkName: "src__pages__userCenter__message" */'../../../src/pages/userCenter/message.jsx')),
|
|
31
30
|
'system/dict/index': React.lazy(() => import(/* webpackChunkName: "src__pages__system__dict__index" */'../../../src/pages/system/dict/index.jsx')),
|
|
32
31
|
'system/file/index': React.lazy(() => import(/* webpackChunkName: "src__pages__system__file__index" */'../../../src/pages/system/file/index.jsx')),
|
|
33
32
|
'system/role/index': React.lazy(() => import(/* webpackChunkName: "src__pages__system__role__index" */'../../../src/pages/system/role/index.jsx')),
|
|
34
33
|
'system/user/index': React.lazy(() => import(/* webpackChunkName: "src__pages__system__user__index" */'../../../src/pages/system/user/index.jsx')),
|
|
35
|
-
'userCenter/manual': React.lazy(() => import(/* webpackChunkName: "src__pages__userCenter__manual" */'../../../src/pages/userCenter/manual.jsx')),
|
|
36
34
|
'system/job/index': React.lazy(() => import(/* webpackChunkName: "src__pages__system__job__index" */'../../../src/pages/system/job/index.jsx')),
|
|
37
35
|
'system/log/index': React.lazy(() => import(/* webpackChunkName: "src__pages__system__log__index" */'../../../src/pages/system/log/index.jsx')),
|
|
38
36
|
'system/org/index': React.lazy(() => import(/* webpackChunkName: "src__pages__system__org__index" */'../../../src/pages/system/org/index.jsx')),
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
interface ContextMenuItem {
|
|
4
|
+
key: string;
|
|
5
|
+
label: string;
|
|
6
|
+
icon?: React.ReactNode;
|
|
7
|
+
danger?: boolean;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
divider?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface ContextMenuProps {
|
|
13
|
+
x: number;
|
|
14
|
+
y: number;
|
|
15
|
+
items: ContextMenuItem[];
|
|
16
|
+
onClick: (info: { key: string }) => void;
|
|
17
|
+
onClose: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const ContextMenu: React.FC<ContextMenuProps> = ({ x, y, items, onClick, onClose }) => {
|
|
21
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
const handler = (e: MouseEvent) => {
|
|
25
|
+
if (ref.current && !ref.current.contains(e.target as Node)) {
|
|
26
|
+
onClose();
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const timer = setTimeout(() => {
|
|
30
|
+
document.addEventListener('mousedown', handler);
|
|
31
|
+
}, 0);
|
|
32
|
+
return () => {
|
|
33
|
+
clearTimeout(timer);
|
|
34
|
+
document.removeEventListener('mousedown', handler);
|
|
35
|
+
};
|
|
36
|
+
}, [onClose]);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div
|
|
40
|
+
ref={ref}
|
|
41
|
+
style={{
|
|
42
|
+
position: 'fixed',
|
|
43
|
+
left: x,
|
|
44
|
+
top: y,
|
|
45
|
+
zIndex: 1050,
|
|
46
|
+
background: '#fff',
|
|
47
|
+
borderRadius: 4,
|
|
48
|
+
padding: '4px 0',
|
|
49
|
+
minWidth: 120,
|
|
50
|
+
boxShadow: '0 6px 16px 0 rgba(0,0,0,0.08), 0 3px 6px -4px rgba(0,0,0,0.12), 0 9px 28px 8px rgba(0,0,0,0.05)',
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
{items.map(item => (
|
|
54
|
+
item.divider ? (
|
|
55
|
+
<div key={item.key} style={{ height: 1, background: '#f0f0f0', margin: '4px 0' }} />
|
|
56
|
+
) : (
|
|
57
|
+
<div
|
|
58
|
+
key={item.key}
|
|
59
|
+
onClick={() => {
|
|
60
|
+
if (!item.disabled) {
|
|
61
|
+
onClick({ key: item.key });
|
|
62
|
+
onClose();
|
|
63
|
+
}
|
|
64
|
+
}}
|
|
65
|
+
style={{
|
|
66
|
+
padding: '5px 12px',
|
|
67
|
+
fontSize: 13,
|
|
68
|
+
cursor: item.disabled ? 'not-allowed' : 'pointer',
|
|
69
|
+
color: item.danger ? '#ff4d4f' : item.disabled ? '#rgba(0,0,0,0.25)' : '#333',
|
|
70
|
+
display: 'flex',
|
|
71
|
+
alignItems: 'center',
|
|
72
|
+
gap: 8,
|
|
73
|
+
background: 'transparent',
|
|
74
|
+
transition: 'background 0.15s',
|
|
75
|
+
userSelect: 'none',
|
|
76
|
+
}}
|
|
77
|
+
onMouseEnter={(e) => {
|
|
78
|
+
if (!item.disabled) e.currentTarget.style.background = '#f5f5f5';
|
|
79
|
+
}}
|
|
80
|
+
onMouseLeave={(e) => {
|
|
81
|
+
e.currentTarget.style.background = 'transparent';
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
{item.icon && <span style={{ fontSize: 14, lineHeight: 1 }}>{item.icon}</span>}
|
|
85
|
+
<span>{item.label}</span>
|
|
86
|
+
</div>
|
|
87
|
+
)
|
|
88
|
+
))}
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
};
|
|
@@ -1,10 +1,23 @@
|
|
|
1
|
-
.
|
|
1
|
+
.oa-page {
|
|
2
2
|
width: 100%;
|
|
3
|
-
|
|
4
|
-
// header 48 footer 43, tab 36
|
|
5
3
|
height: 100%;
|
|
6
|
-
|
|
7
4
|
overflow: auto;
|
|
5
|
+
background-color: #f5f7fa;
|
|
6
|
+
|
|
7
|
+
.oa-page-header {
|
|
8
|
+
margin-bottom: 16px;
|
|
9
|
+
|
|
10
|
+
.oa-page-header-top {
|
|
11
|
+
display: flex;
|
|
12
|
+
align-items: center;
|
|
13
|
+
justify-content: space-between;
|
|
14
|
+
}
|
|
8
15
|
|
|
9
|
-
|
|
10
|
-
|
|
16
|
+
.oa-page-header-left {
|
|
17
|
+
min-width: 0;
|
|
18
|
+
display: flex;
|
|
19
|
+
align-items: center;
|
|
20
|
+
gap: 8px;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
import { Typography } from "antd";
|
|
2
3
|
import './index.less'
|
|
3
4
|
import {ThemeUtils} from "../../utils";
|
|
5
|
+
import { ButtonList } from "../system/ButtonList";
|
|
4
6
|
|
|
5
7
|
interface PageProps {
|
|
6
8
|
padding?: boolean;
|
|
7
9
|
backgroundGray?: boolean;
|
|
8
10
|
debug?: boolean;
|
|
11
|
+
title?: React.ReactNode;
|
|
12
|
+
description?: React.ReactNode;
|
|
13
|
+
actions?: React.ReactNode;
|
|
9
14
|
children?: React.ReactNode;
|
|
10
15
|
}
|
|
11
16
|
|
|
12
|
-
export const Page: React.FC<PageProps> = ({ padding = true, backgroundGray = false, debug = false, children }) => {
|
|
17
|
+
export const Page: React.FC<PageProps> = ({ padding = true, backgroundGray = false, debug = false, title, description, actions, children }) => {
|
|
18
|
+
const hasHeader = title != null;
|
|
13
19
|
const style: React.CSSProperties = {};
|
|
20
|
+
|
|
14
21
|
if (padding) {
|
|
15
22
|
style.padding = 16;
|
|
16
23
|
}
|
|
@@ -21,7 +28,20 @@ export const Page: React.FC<PageProps> = ({ padding = true, backgroundGray = fal
|
|
|
21
28
|
style.backgroundColor = 'rgba(255, 0, 0, 0.08)';
|
|
22
29
|
}
|
|
23
30
|
|
|
24
|
-
return <div className={'
|
|
31
|
+
return <div className={'oa-page'} style={style}>
|
|
32
|
+
{hasHeader && (
|
|
33
|
+
<div className="oa-page-header">
|
|
34
|
+
<div className="oa-page-header-top">
|
|
35
|
+
<div className="oa-page-header-left">
|
|
36
|
+
<div>
|
|
37
|
+
{title && <Typography.Title level={5} style={{ margin: 0 }}>{title}</Typography.Title>}
|
|
38
|
+
{description && <div><Typography.Text type="secondary">{description}</Typography.Text></div>}
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
{actions && <ButtonList>{actions}</ButtonList>}
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
)}
|
|
25
45
|
{children}
|
|
26
46
|
</div>
|
|
27
|
-
};
|
|
47
|
+
};
|
|
@@ -1,14 +1,65 @@
|
|
|
1
|
-
.
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
.oa-pro-table {
|
|
2
|
+
.filter-bar {
|
|
3
|
+
display: grid;
|
|
4
|
+
grid-template-columns: repeat(4, 1fr);
|
|
5
|
+
gap: 12px 20px;
|
|
6
|
+
align-items: center;
|
|
7
|
+
padding: 16px 20px;
|
|
8
|
+
background: #fff;
|
|
9
|
+
border: 1px solid #e8e8e8;
|
|
10
|
+
border-radius: 6px;
|
|
11
|
+
margin-bottom: 16px;
|
|
12
|
+
|
|
13
|
+
.ant-form-item {
|
|
14
|
+
margin: 0;
|
|
15
|
+
|
|
16
|
+
.ant-form-item-row {
|
|
17
|
+
display: flex;
|
|
18
|
+
align-items: center;
|
|
19
|
+
gap: 8px;
|
|
20
|
+
flex-direction: row;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.ant-form-item-label {
|
|
24
|
+
flex-shrink: 0;
|
|
25
|
+
overflow: visible;
|
|
26
|
+
padding: 0;
|
|
27
|
+
white-space: nowrap;
|
|
28
|
+
|
|
29
|
+
label {
|
|
30
|
+
font-size: 13px;
|
|
31
|
+
color: #666;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.ant-form-item-control {
|
|
36
|
+
flex: 1;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.filter-actions {
|
|
41
|
+
grid-column: -2 / -1;
|
|
42
|
+
display: flex;
|
|
43
|
+
gap: 8px;
|
|
44
|
+
justify-content: flex-end;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.span-2 {
|
|
48
|
+
grid-column: span 2;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.pro-table-wrapper {
|
|
53
|
+
background: #fff;
|
|
54
|
+
border: 1px solid #e8e8e8;
|
|
55
|
+
border-radius: 6px;
|
|
5
56
|
}
|
|
6
57
|
|
|
7
|
-
.
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
58
|
+
.pro-table-toolbar {
|
|
59
|
+
display: flex;
|
|
60
|
+
align-items: center;
|
|
61
|
+
justify-content: space-between;
|
|
62
|
+
margin: 8px;
|
|
11
63
|
}
|
|
12
|
-
}
|
|
13
64
|
}
|
|
14
65
|
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import {Button, Form, Table} from 'antd';
|
|
3
|
-
import {SearchOutlined} from "@ant-design/icons";
|
|
4
3
|
|
|
5
|
-
import { Toolbar } from './components/ToolBar';
|
|
6
4
|
import {StringUtils} from "../../utils";
|
|
7
5
|
|
|
8
6
|
import './index.less'
|
|
@@ -47,7 +45,6 @@ export class ProTable extends React.Component {
|
|
|
47
45
|
this.state.pageSize = props.defaultPageSize
|
|
48
46
|
}
|
|
49
47
|
this.id = StringUtils.random(32)
|
|
50
|
-
this.showToolbarSearch = this.props.showToolbarSearch
|
|
51
48
|
}
|
|
52
49
|
|
|
53
50
|
formRef = React.createRef()
|
|
@@ -122,23 +119,17 @@ export class ProTable extends React.Component {
|
|
|
122
119
|
columns,
|
|
123
120
|
rowSelection,
|
|
124
121
|
rowKey = "id",
|
|
125
|
-
toolbarOptions,
|
|
126
122
|
} = this.props
|
|
127
123
|
|
|
128
124
|
|
|
129
|
-
return <div className={'
|
|
125
|
+
return <div className={'oa-pro-table '} id={this.id}>
|
|
130
126
|
{this.renderForm()}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
loading={this.state.loading}
|
|
138
|
-
params={this.state.params}
|
|
139
|
-
showSearch={this.showToolbarSearch}
|
|
140
|
-
changeFormValues={this.changeFormValues}
|
|
141
|
-
/>}
|
|
127
|
+
<div className="pro-table-wrapper">
|
|
128
|
+
{toolBarRender && <div className="pro-table-toolbar">
|
|
129
|
+
<div className="pro-table-toolbar-left">
|
|
130
|
+
{this.getToolBarRenderNode(toolBarRender)}
|
|
131
|
+
</div>
|
|
132
|
+
</div>}
|
|
142
133
|
|
|
143
134
|
|
|
144
135
|
<Table
|
|
@@ -167,45 +158,37 @@ export class ProTable extends React.Component {
|
|
|
167
158
|
}}
|
|
168
159
|
|
|
169
160
|
footer={this.state.extData.summary ? () => this.state.extData.summary : null}
|
|
170
|
-
bordered={this.props.bordered}
|
|
161
|
+
bordered={this.props.bordered ?? false}
|
|
171
162
|
/>
|
|
163
|
+
</div>
|
|
172
164
|
</div>
|
|
173
165
|
|
|
174
166
|
}
|
|
175
167
|
|
|
176
168
|
|
|
177
169
|
renderForm = () => {
|
|
178
|
-
if (!this.props.children)
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
{this.props.children}
|
|
202
|
-
<div style={{marginLeft: '16px'}}>
|
|
203
|
-
<Button type='primary' htmlType="submit" icon={<SearchOutlined/>}> 查询
|
|
204
|
-
</Button>
|
|
205
|
-
</div>
|
|
206
|
-
|
|
207
|
-
</Form>
|
|
208
|
-
</div>;
|
|
170
|
+
if (!this.props.children) return
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<Form
|
|
174
|
+
className="filter-bar"
|
|
175
|
+
style={{gridTemplateColumns: `repeat(${this.props.searchColumns ?? 4}, 1fr)`}}
|
|
176
|
+
onFinish={(values) => this.onSearch(values)}
|
|
177
|
+
ref={(instance) => {
|
|
178
|
+
this.formRef.current = instance;
|
|
179
|
+
if (this.props.formRef) this.props.formRef.current = instance;
|
|
180
|
+
}}
|
|
181
|
+
>
|
|
182
|
+
{this.props.children}
|
|
183
|
+
<div className="filter-actions">
|
|
184
|
+
<Button type='primary' htmlType="submit">查询</Button>
|
|
185
|
+
<Button onClick={() => {
|
|
186
|
+
this.formRef.current?.resetFields();
|
|
187
|
+
this.formRef.current?.submit();
|
|
188
|
+
}}>重置</Button>
|
|
189
|
+
</div>
|
|
190
|
+
</Form>
|
|
191
|
+
)
|
|
209
192
|
};
|
|
210
193
|
|
|
211
194
|
getToolBarRenderNode(toolBarRender) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {Badge, Dropdown} from "antd";
|
|
2
|
-
import {NotificationOutlined,
|
|
2
|
+
import {NotificationOutlined, SettingOutlined, UserOutlined} from "@ant-design/icons";
|
|
3
3
|
import React from "react";
|
|
4
4
|
import {history} from "umi";
|
|
5
5
|
import {DeviceUtils, HttpUtils, MessageUtils, PageUtils, SysUtils} from "../../framework";
|
|
@@ -62,11 +62,6 @@ export class HeaderRight extends React.Component {
|
|
|
62
62
|
</Badge>
|
|
63
63
|
</div>
|
|
64
64
|
|
|
65
|
-
<div className='item' title='操作手册' onClick={() => PageUtils.open('/userCenter/manual', '操作手册')}>
|
|
66
|
-
<QuestionCircleOutlined />
|
|
67
|
-
</div>
|
|
68
|
-
|
|
69
|
-
|
|
70
65
|
<div className='item'>
|
|
71
66
|
|
|
72
67
|
<Dropdown menu={{
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, {useState, useEffect, useRef} from "react";
|
|
2
2
|
import {history} from "umi";
|
|
3
3
|
import {PageRender} from "../PageRender";
|
|
4
|
-
import {UrlUtils} from "../../framework";
|
|
4
|
+
import {UrlUtils, ContextMenu} from "../../framework";
|
|
5
5
|
|
|
6
6
|
const MAX_TABS = 20;
|
|
7
7
|
|
|
@@ -17,6 +17,7 @@ function getLabel(pathname, search, pathMenuMap) {
|
|
|
17
17
|
export function TabPageRender({pathMenuMap}) {
|
|
18
18
|
const [tabs, setTabs] = useState([]);
|
|
19
19
|
const [activeKey, setActiveKey] = useState(null);
|
|
20
|
+
const [contextMenu, setContextMenu] = useState(null);
|
|
20
21
|
const accessOrderRef = useRef([]);
|
|
21
22
|
const lastClickTimeRef = useRef({});
|
|
22
23
|
const activeKeyRef = useRef(activeKey);
|
|
@@ -56,7 +57,7 @@ export function TabPageRender({pathMenuMap}) {
|
|
|
56
57
|
const {pathname, search} = history.location;
|
|
57
58
|
const key = pathname + (search || '');
|
|
58
59
|
recordAccess(key);
|
|
59
|
-
setTabs([{key, label: getLabel(pathname, search, pathMenuMap), closable:
|
|
60
|
+
setTabs([{key, label: getLabel(pathname, search, pathMenuMap), closable: true, content: <PageRender pathname={pathname}/>}]);
|
|
60
61
|
setActiveKey(key);
|
|
61
62
|
|
|
62
63
|
return unlisten;
|
|
@@ -79,46 +80,83 @@ export function TabPageRender({pathMenuMap}) {
|
|
|
79
80
|
return () => document.removeEventListener('close-page-event', handler);
|
|
80
81
|
}, []);
|
|
81
82
|
|
|
83
|
+
const handleTabRefresh = (key) => {
|
|
84
|
+
setTabs(prev => {
|
|
85
|
+
const idx = prev.findIndex(t => t.key === key);
|
|
86
|
+
if (idx === -1) return prev;
|
|
87
|
+
const originalContent = prev[idx].content;
|
|
88
|
+
const next = [...prev];
|
|
89
|
+
next[idx] = {...next[idx], content: '刷新中...'};
|
|
90
|
+
requestAnimationFrame(() => {
|
|
91
|
+
setTabs(prev2 => {
|
|
92
|
+
const idx2 = prev2.findIndex(t => t.key === key);
|
|
93
|
+
if (idx2 === -1) return prev2;
|
|
94
|
+
const next2 = [...prev2];
|
|
95
|
+
next2[idx2] = {...next2[idx2], content: originalContent};
|
|
96
|
+
return next2;
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
return next;
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
|
|
82
103
|
const handleTabClick = (key) => {
|
|
83
104
|
const now = Date.now();
|
|
84
105
|
const last = lastClickTimeRef.current[key] || 0;
|
|
85
106
|
lastClickTimeRef.current[key] = now;
|
|
86
107
|
if (now - last < 300) {
|
|
87
|
-
|
|
88
|
-
setTabs(prev => {
|
|
89
|
-
const idx = prev.findIndex(t => t.key === key);
|
|
90
|
-
if (idx === -1) return prev;
|
|
91
|
-
const originalContent = prev[idx].content;
|
|
92
|
-
const next = [...prev];
|
|
93
|
-
next[idx] = {...next[idx], content: '刷新中...'};
|
|
94
|
-
requestAnimationFrame(() => {
|
|
95
|
-
setTabs(prev2 => {
|
|
96
|
-
const idx2 = prev2.findIndex(t => t.key === key);
|
|
97
|
-
if (idx2 === -1) return prev2;
|
|
98
|
-
const next2 = [...prev2];
|
|
99
|
-
next2[idx2] = {...next2[idx2], content: originalContent};
|
|
100
|
-
return next2;
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
return next;
|
|
104
|
-
});
|
|
108
|
+
handleTabRefresh(key);
|
|
105
109
|
} else if (key !== activeKey) {
|
|
106
110
|
history.push(key);
|
|
107
111
|
}
|
|
108
112
|
};
|
|
109
113
|
|
|
110
|
-
const
|
|
111
|
-
e.stopPropagation();
|
|
114
|
+
const closeTab = (key) => {
|
|
112
115
|
setTabs(prev => {
|
|
113
116
|
const next = prev.filter(t => t.key !== key);
|
|
114
117
|
accessOrderRef.current = accessOrderRef.current.filter(k => k !== key);
|
|
115
|
-
if (
|
|
118
|
+
if (next.length === 0) {
|
|
119
|
+
history.push('/');
|
|
120
|
+
} else if (key === activeKeyRef.current) {
|
|
116
121
|
history.push(next[next.length - 1].key);
|
|
117
122
|
}
|
|
118
123
|
return next;
|
|
119
124
|
});
|
|
120
125
|
};
|
|
121
126
|
|
|
127
|
+
const handleClose = (key, e) => {
|
|
128
|
+
e.stopPropagation();
|
|
129
|
+
closeTab(key);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const handleTabContextMenu = (e, key) => {
|
|
133
|
+
e.preventDefault();
|
|
134
|
+
setContextMenu({x: e.clientX, y: e.clientY, tabKey: key});
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const handleContextMenuClick = ({key: actionKey}) => {
|
|
138
|
+
const {tabKey} = contextMenu;
|
|
139
|
+
setContextMenu(null);
|
|
140
|
+
if (actionKey === 'refresh') {
|
|
141
|
+
handleTabRefresh(tabKey);
|
|
142
|
+
} else if (actionKey === 'close') {
|
|
143
|
+
closeTab(tabKey);
|
|
144
|
+
} else if (actionKey === 'closeOthers') {
|
|
145
|
+
setTabs(prev => {
|
|
146
|
+
const next = prev.filter(t => t.key === tabKey || !t.closable);
|
|
147
|
+
accessOrderRef.current = [tabKey];
|
|
148
|
+
if (tabKey !== activeKeyRef.current) {
|
|
149
|
+
history.push(tabKey);
|
|
150
|
+
}
|
|
151
|
+
return next;
|
|
152
|
+
});
|
|
153
|
+
} else if (actionKey === 'closeAll') {
|
|
154
|
+
accessOrderRef.current = [];
|
|
155
|
+
setTabs([]);
|
|
156
|
+
setTimeout(() => history.push('/'), 0);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
122
160
|
if (tabs.length === 0) return null;
|
|
123
161
|
|
|
124
162
|
return (
|
|
@@ -129,6 +167,7 @@ export function TabPageRender({pathMenuMap}) {
|
|
|
129
167
|
key={tab.key}
|
|
130
168
|
className={'oa-tab' + (tab.key === activeKey ? ' active' : '')}
|
|
131
169
|
onClick={() => handleTabClick(tab.key)}
|
|
170
|
+
onContextMenu={(e) => handleTabContextMenu(e, tab.key)}
|
|
132
171
|
>
|
|
133
172
|
<span className="oa-tab-label">{tab.label}</span>
|
|
134
173
|
{tab.closable && tabs.length > 1 && (
|
|
@@ -137,6 +176,24 @@ export function TabPageRender({pathMenuMap}) {
|
|
|
137
176
|
</div>
|
|
138
177
|
))}
|
|
139
178
|
</div>
|
|
179
|
+
{contextMenu && (() => {
|
|
180
|
+
const tab = tabs.find(t => t.key === contextMenu.tabKey);
|
|
181
|
+
const closableCount = tabs.filter(t => t.closable).length;
|
|
182
|
+
return (
|
|
183
|
+
<ContextMenu
|
|
184
|
+
x={contextMenu.x}
|
|
185
|
+
y={contextMenu.y}
|
|
186
|
+
items={[
|
|
187
|
+
{key: 'refresh', label: '刷新'},
|
|
188
|
+
{key: 'close', label: '关闭', disabled: !tab?.closable},
|
|
189
|
+
{key: 'closeOthers', label: '关闭其他', disabled: tabs.length <= 1},
|
|
190
|
+
{key: 'closeAll', label: '关闭全部', disabled: closableCount === 0},
|
|
191
|
+
]}
|
|
192
|
+
onClick={handleContextMenuClick}
|
|
193
|
+
onClose={() => setContextMenu(null)}
|
|
194
|
+
/>
|
|
195
|
+
);
|
|
196
|
+
})()}
|
|
140
197
|
<div className="oa-tabs-content">
|
|
141
198
|
{tabs.map(tab => (
|
|
142
199
|
<div
|
|
@@ -3,11 +3,10 @@ import {Badge, Layout, Menu, Skeleton, Watermark} from 'antd';
|
|
|
3
3
|
|
|
4
4
|
import {history, Link} from 'umi';
|
|
5
5
|
import "./index.less"
|
|
6
|
-
import {ErrorBoundary,
|
|
6
|
+
import {ErrorBoundary, HttpUtils, NamedIcon, PageUtils, SysUtils, ThemeUtils, TreeUtils} from "../../framework";
|
|
7
7
|
|
|
8
8
|
import { HeaderRight } from "./HeaderRight";
|
|
9
9
|
import { TabPageRender } from "./TabPageRender";
|
|
10
|
-
|
|
11
10
|
const {Header, Sider, Content} = Layout;
|
|
12
11
|
/**
|
|
13
12
|
* 带菜单的布局,主要处理布局宇框架结构
|
|
@@ -112,7 +111,6 @@ export default class extends React.Component {
|
|
|
112
111
|
const defaultGroup = {
|
|
113
112
|
key: '_default_group',
|
|
114
113
|
label: '业务模块',
|
|
115
|
-
title: '业',
|
|
116
114
|
icon: <NamedIcon name="AppstoreOutlined" style={{fontSize: 12}}/>,
|
|
117
115
|
children: leafRootNodes,
|
|
118
116
|
type: 'directory',
|
|
@@ -197,30 +195,26 @@ export default class extends React.Component {
|
|
|
197
195
|
const {siteInfo, loginInfo} = this.state
|
|
198
196
|
|
|
199
197
|
return <Layout className='main-layout' style={{'--primary-color': ThemeUtils.getColor("primary-color"), '--primary-color-click': ThemeUtils.getColor("primary-color-click")}}>
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
198
|
+
<Sider id='left-sider'
|
|
199
|
+
width={180}
|
|
200
|
+
collapsible
|
|
201
|
+
breakpoint={'md'}
|
|
202
|
+
>
|
|
203
|
+
<div className='sider-header'>
|
|
204
|
+
<img className='logo-img' src="./logo.svg" onClick={() => history.push('/')} alt='logo'/>
|
|
204
205
|
<h3 className='hide-on-mobile'>
|
|
205
|
-
<Link to="/" style={{color:
|
|
206
|
+
<Link to="/" style={{color: 'rgba(255,255,255,0.85)'}}>{siteInfo.title}</Link>
|
|
206
207
|
</h3>
|
|
207
|
-
|
|
208
208
|
</div>
|
|
209
|
-
{this.
|
|
210
|
-
<HeaderRight/>
|
|
211
|
-
</Header>
|
|
212
|
-
|
|
213
|
-
<Layout style={{flex: 1, overflow: 'hidden'}}>
|
|
214
|
-
<Sider id='left-sider'
|
|
215
|
-
collapsible
|
|
216
|
-
breakpoint={'md'}
|
|
217
|
-
>
|
|
218
|
-
<Gap/>
|
|
219
|
-
|
|
209
|
+
{this.renderLeftMenu()}
|
|
220
210
|
|
|
221
|
-
|
|
211
|
+
</Sider>
|
|
222
212
|
|
|
223
|
-
|
|
213
|
+
<Layout style={{flex: 1, overflow: 'hidden'}}>
|
|
214
|
+
<Header className='header'>
|
|
215
|
+
{this.renderTopMenu()}
|
|
216
|
+
<HeaderRight/>
|
|
217
|
+
</Header>
|
|
224
218
|
|
|
225
219
|
<Content id='admin-layout-content'>
|
|
226
220
|
<ErrorBoundary>
|