@jiangood/open-admin 3.1.6 → 3.1.7
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 -5
- package/src/framework/biz/UserPicker.less +17 -0
- package/src/framework/biz/UserPicker.tsx +296 -0
- package/src/framework/biz/index.tsx +2 -0
- package/src/framework/biz/pickerUser.ts +19 -0
- package/src/framework/components/ContextMenu/index.tsx +11 -8
- package/src/framework/components/DownloadModal/index.tsx +4 -4
- package/src/framework/components/ErrorBoundary/index.tsx +1 -1
- package/src/framework/components/OrgSwitcher/index.tsx +1 -1
- package/src/framework/components/ProTable/index.less +26 -8
- package/src/framework/components/ProTable/index.tsx +9 -3
- package/src/framework/components/ThemeSettings/index.tsx +173 -0
- package/src/framework/components/index.ts +1 -0
- package/src/framework/config/index.ts +44 -2
- package/src/framework/config/layoutConfig.ts +52 -0
- package/src/framework/config/themeConfig.ts +449 -14
- package/src/framework/fields/FieldEditor/index.tsx +2 -2
- package/src/framework/fields/FieldUploadFile/index.tsx +3 -3
- package/src/framework/fields/FieldUploadImage/index.tsx +15 -15
- package/src/framework/fields/FieldUserPicker/index.tsx +124 -0
- package/src/framework/fields/index.ts +1 -0
- package/src/framework/utils/HttpClient.ts +15 -4
- package/src/framework/utils/MessageApi.ts +16 -3
- package/src/framework/views/ViewText/index.tsx +1 -1
- package/src/layouts/admin/AdminTabs.less +11 -11
- package/src/layouts/admin/HeaderRight.jsx +23 -12
- package/src/layouts/admin/SinglePageLayout.jsx +36 -0
- package/src/layouts/admin/TabLayout.jsx +21 -1
- package/src/layouts/admin/index.jsx +45 -25
- package/src/layouts/admin/index.less +43 -23
- package/src/layouts/index.tsx +29 -4
- package/src/pages/article/article.less +13 -13
- package/src/pages/public/login.jsx +29 -15
- package/src/pages/public/login.less +243 -22
- package/src/pages/standalone/forceUpdatePwd.jsx +8 -7
- package/src/pages/system/codegen/index.jsx +56 -51
- package/src/pages/system/job/index.jsx +44 -7
- package/src/pages/system/role/index.jsx +17 -43
- package/src/pages/system/user/index.jsx +0 -4
- package/src/pages/system/user/userPerm.jsx +3 -3
- package/src/style/global.less +6 -6
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {Tag} from 'antd';
|
|
3
|
+
import {HttpClient} from '../../utils';
|
|
4
|
+
import type {FieldProps} from '../types';
|
|
5
|
+
import {formatUserLabel, type PickerUser} from '../../biz/pickerUser';
|
|
6
|
+
import {UserPicker} from '../../biz/UserPicker';
|
|
7
|
+
|
|
8
|
+
export interface FieldUserPickerProps extends FieldProps<string[]> {
|
|
9
|
+
/** 是否多选,默认 true */
|
|
10
|
+
multiple?: boolean;
|
|
11
|
+
/** 禁用 */
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
/** 空值提示,默认 请选择用户 */
|
|
14
|
+
placeholder?: string;
|
|
15
|
+
/** 弹窗标题 */
|
|
16
|
+
title?: string;
|
|
17
|
+
/** 分页数据源地址,默认 admin/sysUser/page */
|
|
18
|
+
requestUrl?: string;
|
|
19
|
+
/** 按 id 查询用户详情地址(用于回显),默认 admin/sysUser/by-ids */
|
|
20
|
+
usersUrl?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface FieldUserPickerState {
|
|
24
|
+
open: boolean;
|
|
25
|
+
users: PickerUser[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 选人表单组件
|
|
30
|
+
*
|
|
31
|
+
* 展示已选用户标签并提供弹窗选择,遵循字段组件约定:value + onChange(value 为用户 id 数组)。
|
|
32
|
+
*/
|
|
33
|
+
export class FieldUserPicker extends React.Component<FieldUserPickerProps, FieldUserPickerState> {
|
|
34
|
+
|
|
35
|
+
static readonly defaultProps = {
|
|
36
|
+
multiple: true,
|
|
37
|
+
placeholder: '请选择用户',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
state: FieldUserPickerState = {
|
|
41
|
+
open: false,
|
|
42
|
+
users: [],
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
componentDidMount() {
|
|
46
|
+
this.syncUsers();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
componentDidUpdate(prevProps: FieldUserPickerProps) {
|
|
50
|
+
if (prevProps.value !== this.props.value) {
|
|
51
|
+
this.syncUsers();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
getUsersUrl = () => this.props.usersUrl || 'admin/sysUser/by-ids';
|
|
56
|
+
|
|
57
|
+
// 根据 value(ids) 拉取并补全用户详情,按 ids 顺序展示
|
|
58
|
+
syncUsers = () => {
|
|
59
|
+
const ids = this.props.value || [];
|
|
60
|
+
const known = new Map(this.state.users.map(user => [user.id, user]));
|
|
61
|
+
const missing = ids.filter(id => !known.has(id));
|
|
62
|
+
if (missing.length === 0) {
|
|
63
|
+
this.setState({users: ids.map(id => known.get(id)).filter(Boolean) as PickerUser[]});
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
HttpClient.get<PickerUser[]>(this.getUsersUrl(), {ids}, {toastError: false}).then(rs => {
|
|
67
|
+
const merged = new Map(this.state.users.map(user => [user.id, user]));
|
|
68
|
+
(rs.data || []).forEach(user => merged.set(user.id, user));
|
|
69
|
+
const currentIds = this.props.value || [];
|
|
70
|
+
this.setState({users: currentIds.map(id => merged.get(id)).filter(Boolean) as PickerUser[]});
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
remove = (id: string) => {
|
|
75
|
+
this.props.onChange?.((this.props.value || []).filter(value => value !== id));
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
handleOk = (ids: string[], users: PickerUser[]) => {
|
|
79
|
+
this.setState({open: false, users});
|
|
80
|
+
this.props.onChange?.(ids);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
render() {
|
|
84
|
+
const {disabled, placeholder, multiple, title, requestUrl, usersUrl, value} = this.props;
|
|
85
|
+
const {open, users} = this.state;
|
|
86
|
+
return <>
|
|
87
|
+
<div style={{
|
|
88
|
+
minHeight: 32,
|
|
89
|
+
display: 'flex',
|
|
90
|
+
flexWrap: 'wrap',
|
|
91
|
+
alignItems: 'center',
|
|
92
|
+
gap: 4,
|
|
93
|
+
padding: '4px 8px',
|
|
94
|
+
border: '1px solid var(--oa-color-border, #d9d9d9)',
|
|
95
|
+
borderRadius: 'var(--oa-border-radius, 6px)',
|
|
96
|
+
background: disabled ? 'var(--oa-color-fill, #f5f5f5)' : 'var(--oa-color-bg-container, #fff)',
|
|
97
|
+
cursor: disabled ? 'not-allowed' : undefined,
|
|
98
|
+
}}>
|
|
99
|
+
{users.length === 0 && <span style={{color: 'var(--oa-color-text-tertiary, #999)'}}>{placeholder}</span>}
|
|
100
|
+
{users.map(user => (
|
|
101
|
+
<Tag key={user.id} closable={!disabled} onClose={(e) => {
|
|
102
|
+
e.preventDefault();
|
|
103
|
+
this.remove(user.id);
|
|
104
|
+
}}>
|
|
105
|
+
{formatUserLabel(user)}
|
|
106
|
+
</Tag>
|
|
107
|
+
))}
|
|
108
|
+
{!disabled && (
|
|
109
|
+
<a style={{marginLeft: users.length ? 'auto' : 0}} onClick={() => this.setState({open: true})}>选择</a>
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
<UserPicker
|
|
113
|
+
open={open}
|
|
114
|
+
title={title}
|
|
115
|
+
multiple={multiple}
|
|
116
|
+
value={value}
|
|
117
|
+
requestUrl={requestUrl}
|
|
118
|
+
usersUrl={usersUrl}
|
|
119
|
+
onOk={this.handleOk}
|
|
120
|
+
onCancel={() => this.setState({open: false})}
|
|
121
|
+
/>
|
|
122
|
+
</>;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -14,6 +14,7 @@ export * from './FieldTable'
|
|
|
14
14
|
export * from './FieldTableSelect'
|
|
15
15
|
export * from './FieldSysOrgTreeSelect'
|
|
16
16
|
export * from './FieldSysOrgTree'
|
|
17
|
+
export * from './FieldUserPicker'
|
|
17
18
|
export * from './FieldPercent'
|
|
18
19
|
export * from './FieldUploadFile';
|
|
19
20
|
export * from './FieldUploadImage';
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import axios from "axios";
|
|
2
2
|
import type {AxiosProgressEvent, AxiosRequestConfig} from "axios";
|
|
3
3
|
import qs from 'qs';
|
|
4
|
-
import {message as messageApi} from "antd";
|
|
5
4
|
import {EventBus} from "./EventBus";
|
|
5
|
+
import {getMessageApi} from "./MessageApi";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* 单一 Promise 风格 HTTP 工具类(所有请求方法均返回 Promise,无 success/error 回调)
|
|
@@ -224,6 +224,11 @@ export class HttpClient {
|
|
|
224
224
|
return url.startsWith('admin') ? '/' + url : url;
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
/** 判断是否为普通对象(用于决定是否附加 X-Body-Fields) */
|
|
228
|
+
private static isPlainObject(data: unknown): data is Record<string, unknown> {
|
|
229
|
+
return data !== null && typeof data === 'object' && !Array.isArray(data);
|
|
230
|
+
}
|
|
231
|
+
|
|
227
232
|
private static coreRequest<T = unknown>(settings: AjaxSettings): Promise<AjaxBody<T>> {
|
|
228
233
|
const {url, method, params, data, headers, toastError = true, toastSuccess = true} = settings;
|
|
229
234
|
const config: AxiosRequestConfig = {url: HttpClient.prefixUrl(url), method, params, data, headers};
|
|
@@ -231,6 +236,12 @@ export class HttpClient {
|
|
|
231
236
|
// FormData 请求交由浏览器自动生成带 boundary 的 multipart Content-Type
|
|
232
237
|
if (typeof FormData !== 'undefined' && data instanceof FormData) {
|
|
233
238
|
config.headers = {...config.headers, 'Content-Type': undefined};
|
|
239
|
+
} else if (method === 'POST' && HttpClient.isPlainObject(data)) {
|
|
240
|
+
// 告知后端本次 JSON body 的顶层字段;值为 undefined 的键不会被 JSON.stringify 序列化,需排除
|
|
241
|
+
const bodyFields = Object.keys(data)
|
|
242
|
+
.filter((key) => data[key] !== undefined)
|
|
243
|
+
.join(',');
|
|
244
|
+
config.headers = {...config.headers, 'X-Body-Fields': bodyFields};
|
|
234
245
|
}
|
|
235
246
|
|
|
236
247
|
const promise = new Promise<AjaxBody<T>>((resolve, reject) => {
|
|
@@ -249,7 +260,7 @@ export class HttpClient {
|
|
|
249
260
|
return;
|
|
250
261
|
}
|
|
251
262
|
if (toastSuccess && message) {
|
|
252
|
-
|
|
263
|
+
getMessageApi().success(message);
|
|
253
264
|
}
|
|
254
265
|
resolve(body);
|
|
255
266
|
}).catch((e: unknown) => {
|
|
@@ -268,7 +279,7 @@ export class HttpClient {
|
|
|
268
279
|
EventBus.emit('loginExpired');
|
|
269
280
|
const err: AjaxError = {code: 401, message: '登录过期'};
|
|
270
281
|
if (toastError) {
|
|
271
|
-
|
|
282
|
+
getMessageApi().error('登录过期');
|
|
272
283
|
}
|
|
273
284
|
reject(err);
|
|
274
285
|
return;
|
|
@@ -276,7 +287,7 @@ export class HttpClient {
|
|
|
276
287
|
const err = HttpClient.toAjaxError(e);
|
|
277
288
|
if (toastError) {
|
|
278
289
|
console.warn(`[HttpClient] ${label}: ${url}`, err.message);
|
|
279
|
-
|
|
290
|
+
getMessageApi().error(err.message);
|
|
280
291
|
}
|
|
281
292
|
reject(err);
|
|
282
293
|
}
|
|
@@ -1,5 +1,18 @@
|
|
|
1
|
-
import {message} from 'antd';
|
|
1
|
+
import {message as staticMessage} from 'antd';
|
|
2
|
+
import type {MessageInstance} from 'antd/es/message/interface';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* message 实例持有者。
|
|
6
|
+
* 静态 message 无法消费 ConfigProvider 主题(暗色模式下仍是浅色),
|
|
7
|
+
* 由布局内的 MessageBridge 通过 App.useApp() 注入跟随主题的实例。
|
|
8
|
+
*/
|
|
9
|
+
let messageInstance: MessageInstance = staticMessage;
|
|
10
|
+
|
|
11
|
+
/** 注入 App 上下文中的 message 实例;不传则恢复为静态 message */
|
|
12
|
+
export function setMessageApi(instance?: MessageInstance) {
|
|
13
|
+
messageInstance = instance ?? staticMessage;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getMessageApi(): MessageInstance {
|
|
17
|
+
return messageInstance;
|
|
5
18
|
}
|
|
@@ -31,7 +31,7 @@ export class ViewText extends React.Component {
|
|
|
31
31
|
style={{
|
|
32
32
|
background: 'none',
|
|
33
33
|
border: 'none',
|
|
34
|
-
borderBottom: '1px dashed #d9d9d9',
|
|
34
|
+
borderBottom: '1px dashed var(--oa-color-border, #d9d9d9)',
|
|
35
35
|
padding: 0,
|
|
36
36
|
font: 'inherit',
|
|
37
37
|
cursor: 'pointer',
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
overflow-x: auto;
|
|
5
5
|
overflow-y: hidden;
|
|
6
6
|
flex-shrink: 0;
|
|
7
|
-
border-bottom: 1px solid #e8e8e8;
|
|
8
|
-
background: #fff;
|
|
7
|
+
border-bottom: 1px solid var(--oa-color-border, #e8e8e8);
|
|
8
|
+
background: var(--oa-color-bg-container, #fff);
|
|
9
9
|
|
|
10
10
|
&::-webkit-scrollbar {
|
|
11
11
|
height: 0;
|
|
@@ -16,14 +16,14 @@
|
|
|
16
16
|
align-items: center;
|
|
17
17
|
gap: 4px;
|
|
18
18
|
padding: 0 12px;
|
|
19
|
-
height: 32px;
|
|
20
|
-
line-height: 32px;
|
|
19
|
+
height: var(--oa-control-height, 32px);
|
|
20
|
+
line-height: var(--oa-control-height, 32px);
|
|
21
21
|
font-size: 13px;
|
|
22
22
|
cursor: pointer;
|
|
23
|
-
border-right: 1px solid #e8e8e8;
|
|
23
|
+
border-right: 1px solid var(--oa-color-border, #e8e8e8);
|
|
24
24
|
white-space: nowrap;
|
|
25
|
-
color: rgba(0, 0, 0, 0.65);
|
|
26
|
-
background: #fafafa;
|
|
25
|
+
color: var(--oa-color-text-secondary, rgba(0, 0, 0, 0.65));
|
|
26
|
+
background: var(--oa-color-fill, #fafafa);
|
|
27
27
|
transition: color 0.2s, background 0.2s;
|
|
28
28
|
position: relative;
|
|
29
29
|
flex-shrink: 0;
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
|
|
35
35
|
&.active {
|
|
36
36
|
color: var(--primary-color);
|
|
37
|
-
background: #fff;
|
|
37
|
+
background: var(--oa-color-bg-container, #fff);
|
|
38
38
|
font-weight: 500;
|
|
39
39
|
}
|
|
40
40
|
|
|
@@ -52,11 +52,11 @@
|
|
|
52
52
|
height: 16px;
|
|
53
53
|
font-size: 12px;
|
|
54
54
|
border-radius: 50%;
|
|
55
|
-
color: rgba(0, 0, 0, 0.45);
|
|
55
|
+
color: var(--oa-color-text-tertiary, rgba(0, 0, 0, 0.45));
|
|
56
56
|
|
|
57
57
|
&:hover {
|
|
58
|
-
background: rgba(0, 0, 0, 0.08);
|
|
59
|
-
color: rgba(0, 0, 0, 0.88);
|
|
58
|
+
background: var(--oa-color-fill, rgba(0, 0, 0, 0.08));
|
|
59
|
+
color: var(--oa-color-text, rgba(0, 0, 0, 0.88));
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
}
|
|
@@ -1,21 +1,19 @@
|
|
|
1
1
|
import {Avatar, Dropdown, Modal} from "antd";
|
|
2
|
+
import {BgColorsOutlined} from "@ant-design/icons";
|
|
2
3
|
import React from "react";
|
|
3
|
-
import {
|
|
4
|
+
import {HttpClient, PageUtils, GlobalData, getToken, history, EventBus, ThemeSettings, ARTICLE_HEADER_AVATAR_DROPDOWN, ARTICLE_HEADER_RIGHT} from "../../framework";
|
|
4
5
|
|
|
5
6
|
export class HeaderRight extends React.Component {
|
|
6
7
|
|
|
7
8
|
state = {
|
|
8
|
-
isMobileDevice: false,
|
|
9
9
|
dropdownArticles: [],
|
|
10
10
|
headerArticles: [],
|
|
11
11
|
alertVisible: false,
|
|
12
12
|
confirmVisible: false,
|
|
13
|
+
themeSettingsVisible: false,
|
|
13
14
|
};
|
|
14
15
|
|
|
15
16
|
componentDidMount() {
|
|
16
|
-
if (DeviceUtils.isMobileDevice()) {
|
|
17
|
-
this.setState({isMobileDevice: true})
|
|
18
|
-
}
|
|
19
17
|
this.loadArticles()
|
|
20
18
|
}
|
|
21
19
|
|
|
@@ -45,17 +43,26 @@ export class HeaderRight extends React.Component {
|
|
|
45
43
|
PageUtils.open('/article/' + code, title)
|
|
46
44
|
}
|
|
47
45
|
|
|
46
|
+
renderThemeSettings = () => {
|
|
47
|
+
return (
|
|
48
|
+
<button type="button" className='item' title="界面设置" aria-label="界面设置"
|
|
49
|
+
onClick={() => this.setState({themeSettingsVisible: true})}>
|
|
50
|
+
<BgColorsOutlined/>
|
|
51
|
+
</button>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
renderThemeSettingsDrawer = () => {
|
|
56
|
+
return (
|
|
57
|
+
<ThemeSettings open={this.state.themeSettingsVisible}
|
|
58
|
+
onClose={() => this.setState({themeSettingsVisible: false})}/>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
48
62
|
render() {
|
|
49
63
|
const info = GlobalData.getLoginInfo()
|
|
50
64
|
const {dropdownArticles, headerArticles} = this.state
|
|
51
65
|
|
|
52
|
-
if (this.state.isMobileDevice) {
|
|
53
|
-
return <div className='header-right'>
|
|
54
|
-
<button type="button" style={{background: 'none', border: 'none', padding: 0, font: 'inherit', cursor: 'pointer'}}
|
|
55
|
-
onClick={this.logout}>退出</button>
|
|
56
|
-
</div>
|
|
57
|
-
}
|
|
58
|
-
|
|
59
66
|
const articleItems = dropdownArticles.map(a => ({
|
|
60
67
|
key: 'article:' + a.code,
|
|
61
68
|
label: a.title,
|
|
@@ -75,6 +82,8 @@ export class HeaderRight extends React.Component {
|
|
|
75
82
|
</button>
|
|
76
83
|
))}
|
|
77
84
|
|
|
85
|
+
{this.renderThemeSettings()}
|
|
86
|
+
|
|
78
87
|
<Dropdown menu={{
|
|
79
88
|
onClick: ({key}) => {
|
|
80
89
|
if (key === 'userCenter') {
|
|
@@ -116,6 +125,8 @@ export class HeaderRight extends React.Component {
|
|
|
116
125
|
}}>
|
|
117
126
|
退出登录失败,是否清空缓存
|
|
118
127
|
</Modal>
|
|
128
|
+
|
|
129
|
+
{this.renderThemeSettingsDrawer()}
|
|
119
130
|
</div>
|
|
120
131
|
}
|
|
121
132
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {history, PageFrame} from "../../framework";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 单页布局:不使用标签页,仅渲染当前路由对应页面。
|
|
6
|
+
* 路由变化时按 url 重新挂载 PageFrame,页面状态随之重置。
|
|
7
|
+
*/
|
|
8
|
+
export class SinglePageLayout extends React.Component {
|
|
9
|
+
state = {location: history.location};
|
|
10
|
+
|
|
11
|
+
unlisten = null;
|
|
12
|
+
|
|
13
|
+
componentDidMount() {
|
|
14
|
+
this.unlisten = history.listen(({location}) => {
|
|
15
|
+
this.setState({location});
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
componentWillUnmount() {
|
|
20
|
+
if (this.unlisten) {
|
|
21
|
+
this.unlisten();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
render() {
|
|
26
|
+
const {pathname, search} = this.state.location;
|
|
27
|
+
const url = pathname + search;
|
|
28
|
+
return (
|
|
29
|
+
<div style={{height: '100%'}}>
|
|
30
|
+
<PageFrame key={url} url={url} show/>
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default SinglePageLayout;
|
|
@@ -83,8 +83,28 @@ export class TabLayout extends React.Component {
|
|
|
83
83
|
this.unsubscribeClosePage = EventBus.on('closePage', this.handleClosePageEvent);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
componentDidUpdate() {
|
|
86
|
+
componentDidUpdate(prevProps) {
|
|
87
87
|
this.activeKey = this.state.activeKey;
|
|
88
|
+
// 菜单信息晚于 Tab 创建时才加载完成:路径/菜单映射变化后回填「未命名」Tab 标题
|
|
89
|
+
if (this.props.pathMenuMap !== prevProps.pathMenuMap) {
|
|
90
|
+
this.refreshTabLabels();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
refreshTabLabels() {
|
|
95
|
+
this.setState(prev => {
|
|
96
|
+
let changed = false;
|
|
97
|
+
const tabs = prev.tabs.map(t => {
|
|
98
|
+
const qIndex = t.key.indexOf('?');
|
|
99
|
+
const pathname = qIndex === -1 ? t.key : t.key.substring(0, qIndex);
|
|
100
|
+
const search = qIndex === -1 ? '' : t.key.substring(qIndex);
|
|
101
|
+
const label = getLabel(pathname, search, this.props.pathMenuMap);
|
|
102
|
+
if (label === t.label) return t;
|
|
103
|
+
changed = true;
|
|
104
|
+
return {...t, label};
|
|
105
|
+
});
|
|
106
|
+
return changed ? {tabs} : null;
|
|
107
|
+
});
|
|
88
108
|
}
|
|
89
109
|
|
|
90
110
|
componentWillUnmount() {
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {Layout, Menu, Skeleton, Watermark} from 'antd';
|
|
2
|
+
import {ConfigProvider, Layout, Menu, Skeleton, Watermark} from 'antd';
|
|
3
3
|
|
|
4
4
|
import "./index.less"
|
|
5
|
-
import {HttpClient, NamedIcon, PageUtils, GlobalData, TreeUtils, history, Link, ARTICLE_HEADER_LEFT, OrgSwitcher} from "../../framework";
|
|
5
|
+
import {HttpClient, NamedIcon, PageUtils, GlobalData, TreeUtils, history, Link, ARTICLE_HEADER_LEFT, OrgSwitcher, getSiderThemeConfig, getLayoutMode, EventBus} from "../../framework";
|
|
6
6
|
|
|
7
7
|
import { HeaderRight } from "./HeaderRight";
|
|
8
8
|
import { TabLayout } from "./TabLayout";
|
|
9
|
+
import { SinglePageLayout } from "./SinglePageLayout";
|
|
9
10
|
const {Header, Sider, Content} = Layout;
|
|
10
11
|
/**
|
|
11
12
|
* 带菜单的布局,主要处理布局宇框架结构
|
|
@@ -30,9 +31,13 @@ export default class AdminLayout extends React.Component {
|
|
|
30
31
|
siteInfo: {},
|
|
31
32
|
|
|
32
33
|
headerLeftArticles: [],
|
|
34
|
+
|
|
35
|
+
layoutMode: getLayoutMode(),
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
|
|
39
|
+
unsubscribeLayout = null;
|
|
40
|
+
|
|
36
41
|
componentDidMount() {
|
|
37
42
|
// 判断是否手机端,自动收起菜单
|
|
38
43
|
|
|
@@ -45,6 +50,16 @@ export default class AdminLayout extends React.Component {
|
|
|
45
50
|
|
|
46
51
|
const siteArticles = GlobalData.getSiteArticles();
|
|
47
52
|
this.setState({headerLeftArticles: siteArticles[ARTICLE_HEADER_LEFT] || []})
|
|
53
|
+
|
|
54
|
+
this.unsubscribeLayout = EventBus.on('layoutChange', () => {
|
|
55
|
+
this.setState({layoutMode: getLayoutMode()});
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
componentWillUnmount() {
|
|
60
|
+
if (this.unsubscribeLayout) {
|
|
61
|
+
this.unsubscribeLayout();
|
|
62
|
+
}
|
|
48
63
|
}
|
|
49
64
|
|
|
50
65
|
|
|
@@ -60,7 +75,7 @@ export default class AdminLayout extends React.Component {
|
|
|
60
75
|
if (item.type === 'divider') {
|
|
61
76
|
return
|
|
62
77
|
}
|
|
63
|
-
item.icon = <NamedIcon name={item.icon || 'AppstoreOutlined'} style={{fontSize:
|
|
78
|
+
item.icon = <NamedIcon name={item.icon || 'AppstoreOutlined'} style={{fontSize: 16}}/>
|
|
64
79
|
})
|
|
65
80
|
|
|
66
81
|
const {topMenus} = this.classifyMenus(menuTree);
|
|
@@ -119,7 +134,7 @@ export default class AdminLayout extends React.Component {
|
|
|
119
134
|
const defaultGroup = {
|
|
120
135
|
key: '_default_group',
|
|
121
136
|
label: '业务模块',
|
|
122
|
-
icon: <NamedIcon name="AppstoreOutlined" style={{fontSize:
|
|
137
|
+
icon: <NamedIcon name="AppstoreOutlined" style={{fontSize: 16}}/>,
|
|
123
138
|
children: leafRootNodes,
|
|
124
139
|
type: 'directory',
|
|
125
140
|
};
|
|
@@ -189,23 +204,26 @@ export default class AdminLayout extends React.Component {
|
|
|
189
204
|
const {siteInfo, loginInfo} = this.state
|
|
190
205
|
|
|
191
206
|
return <Layout className='main-layout'>
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
<
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
207
|
+
{/* 侧栏单独套暗色算法,使分割线/边框在深色底上可见,并按预设覆盖侧栏底色 */}
|
|
208
|
+
<ConfigProvider theme={getSiderThemeConfig()}>
|
|
209
|
+
<Sider id='left-sider'
|
|
210
|
+
width={180}
|
|
211
|
+
collapsible
|
|
212
|
+
breakpoint={'md'}
|
|
213
|
+
>
|
|
214
|
+
<div className='sider-header'>
|
|
215
|
+
<button type="button" className='logo-img'
|
|
216
|
+
onClick={() => history.push('/')}>
|
|
217
|
+
<img src="./logo.png" alt={siteInfo.title || '首页'}/>
|
|
218
|
+
</button>
|
|
219
|
+
<h3 className='hide-on-mobile'>
|
|
220
|
+
<Link to="/" style={{color: 'var(--oa-sider-text, rgba(255,255,255,0.85))'}}>{siteInfo.title}</Link>
|
|
221
|
+
</h3>
|
|
222
|
+
</div>
|
|
223
|
+
{this.renderLeftMenu()}
|
|
224
|
+
|
|
225
|
+
</Sider>
|
|
226
|
+
</ConfigProvider>
|
|
209
227
|
|
|
210
228
|
<Layout style={{flex: 1, overflow: 'hidden'}}>
|
|
211
229
|
<Header className='header'>
|
|
@@ -250,16 +268,18 @@ return <Layout className='main-layout'>
|
|
|
250
268
|
}
|
|
251
269
|
|
|
252
270
|
renderCenterContent = () => {
|
|
253
|
-
const {siteInfo, loginInfo} = this.state
|
|
271
|
+
const {siteInfo, loginInfo, layoutMode} = this.state
|
|
254
272
|
// 即使菜单为空也直接显示页面内容,不要一直显示 Skeleton
|
|
255
|
-
const
|
|
273
|
+
const contentNode = layoutMode === 'single'
|
|
274
|
+
? <SinglePageLayout/>
|
|
275
|
+
: <TabLayout pathMenuMap={this.state.pathMenuMap}/>;
|
|
256
276
|
if (siteInfo.waterMark === true) {
|
|
257
277
|
return <Watermark content={[loginInfo.name, loginInfo.account]}>
|
|
258
|
-
{
|
|
278
|
+
{contentNode}
|
|
259
279
|
</Watermark>
|
|
260
280
|
}
|
|
261
281
|
|
|
262
|
-
return
|
|
282
|
+
return contentNode
|
|
263
283
|
};
|
|
264
284
|
}
|
|
265
285
|
|
|
@@ -6,16 +6,45 @@
|
|
|
6
6
|
height: 48px;
|
|
7
7
|
display: flex;
|
|
8
8
|
align-items: center;
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
min-width: 0;
|
|
10
|
+
background-color: var(--oa-color-bg-container, #fff);
|
|
11
|
+
border-bottom: 1px solid var(--oa-color-border, #e8e8e8);
|
|
12
|
+
|
|
13
|
+
/* 顶栏文字按钮:一级菜单左侧入口与右侧入口统一交互态 */
|
|
14
|
+
.item {
|
|
15
|
+
display: inline-flex;
|
|
16
|
+
align-items: center;
|
|
17
|
+
height: 100%;
|
|
18
|
+
padding: 0 0.5rem;
|
|
19
|
+
cursor: pointer;
|
|
20
|
+
color: var(--oa-color-text, rgba(0, 0, 0, 0.88));
|
|
21
|
+
white-space: nowrap;
|
|
22
|
+
flex-shrink: 0;
|
|
23
|
+
background: none;
|
|
24
|
+
border: none;
|
|
25
|
+
font: inherit;
|
|
26
|
+
|
|
27
|
+
&:hover {
|
|
28
|
+
background: var(--oa-color-fill, rgba(0, 0, 0, 0.06));
|
|
29
|
+
color: var(--primary-color, #1677ff);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
11
32
|
|
|
12
33
|
.top-nav {
|
|
13
34
|
flex: 1;
|
|
35
|
+
min-width: 0;
|
|
14
36
|
display: flex;
|
|
15
37
|
align-items: center;
|
|
16
38
|
margin: 0;
|
|
17
39
|
height: 100%;
|
|
18
|
-
|
|
40
|
+
/* 一级菜单过多时横向滚动,避免被裁切 */
|
|
41
|
+
overflow-x: auto;
|
|
42
|
+
overflow-y: hidden;
|
|
43
|
+
scrollbar-width: none;
|
|
44
|
+
|
|
45
|
+
&::-webkit-scrollbar {
|
|
46
|
+
height: 0;
|
|
47
|
+
}
|
|
19
48
|
|
|
20
49
|
.top-nav-item {
|
|
21
50
|
display: inline-flex;
|
|
@@ -24,7 +53,7 @@
|
|
|
24
53
|
padding: 0 16px;
|
|
25
54
|
height: 100%;
|
|
26
55
|
cursor: pointer;
|
|
27
|
-
color: rgba(0, 0, 0, 0.65);
|
|
56
|
+
color: var(--oa-color-text-secondary, rgba(0, 0, 0, 0.65));
|
|
28
57
|
transition: color 0.2s;
|
|
29
58
|
white-space: nowrap;
|
|
30
59
|
background: none;
|
|
@@ -32,36 +61,27 @@
|
|
|
32
61
|
font: inherit;
|
|
33
62
|
|
|
34
63
|
&:hover {
|
|
35
|
-
color: var(--primary-color,
|
|
64
|
+
color: var(--primary-color, #1677ff);
|
|
65
|
+
background: var(--oa-color-fill, rgba(0, 0, 0, 0.06));
|
|
36
66
|
}
|
|
37
67
|
|
|
38
68
|
&.active {
|
|
39
|
-
color: var(--primary-color,
|
|
40
|
-
box-shadow: inset 0 -2px 0 var(--primary-color,
|
|
69
|
+
color: var(--primary-color, #1677ff);
|
|
70
|
+
box-shadow: inset 0 -2px 0 var(--primary-color, #1677ff);
|
|
41
71
|
}
|
|
42
72
|
}
|
|
43
73
|
}
|
|
44
74
|
|
|
75
|
+
/* 组织切换等固定宽度控件不被压缩 */
|
|
76
|
+
.ant-select {
|
|
77
|
+
flex-shrink: 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
45
80
|
.header-right {
|
|
46
81
|
line-height: 36px;
|
|
47
82
|
display: flex;
|
|
48
83
|
align-items: center;
|
|
49
84
|
flex-shrink: 0;
|
|
50
|
-
|
|
51
|
-
.item {
|
|
52
|
-
display: inline-flex;
|
|
53
|
-
align-items: center;
|
|
54
|
-
padding: 0 0.5rem;
|
|
55
|
-
cursor: pointer;
|
|
56
|
-
background: none;
|
|
57
|
-
border: none;
|
|
58
|
-
font: inherit;
|
|
59
|
-
|
|
60
|
-
&:hover {
|
|
61
|
-
background: var(--primary-color-hover, #4990CD);
|
|
62
|
-
color: white; /* NOSONAR: 主题色悬停态,对比度由主题色决定 */
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
85
|
}
|
|
66
86
|
|
|
67
87
|
}
|
|
@@ -111,7 +131,7 @@
|
|
|
111
131
|
|
|
112
132
|
.ant-menu-submenu-selected {
|
|
113
133
|
&>div {
|
|
114
|
-
color:
|
|
134
|
+
color: var(--oa-sider-text, rgba(255, 255, 255, 0.85));
|
|
115
135
|
}
|
|
116
136
|
}
|
|
117
137
|
|