@jiangood/open-admin 2.5.9 → 3.0.1
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 +20 -8
- package/src/framework/GlobalData.ts +85 -0
- package/src/framework/biz/OrgTree.tsx +2 -7
- package/src/framework/biz/Perm.tsx +11 -0
- package/src/framework/biz/PermActions.tsx +18 -15
- package/src/framework/biz/index.tsx +34 -19
- package/src/framework/components/ContextMenu/index.tsx +105 -78
- package/src/framework/components/DownloadModal/index.tsx +15 -17
- package/src/framework/components/ErrorBoundary/index.tsx +2 -2
- package/src/framework/components/FormModal/index.tsx +76 -0
- package/src/framework/components/Gap/index.tsx +1 -1
- package/src/framework/components/NamedIcon/index.tsx +55 -9
- package/src/framework/components/Page/index.less +0 -1
- package/src/framework/components/Page/index.tsx +34 -27
- package/src/framework/components/PageLoading/index.less +15 -0
- package/src/framework/components/PageLoading/index.tsx +32 -16
- package/src/framework/components/ProTable/index.tsx +86 -15
- package/src/framework/components/index.ts +1 -0
- package/src/framework/config/index.ts +1 -0
- package/src/framework/config/themeConfig.ts +39 -0
- package/src/framework/fields/FieldBoolean/index.tsx +11 -2
- package/src/framework/fields/FieldDate/index.tsx +22 -7
- package/src/framework/fields/FieldDateRange/index.tsx +24 -6
- package/src/framework/fields/FieldDictSelect/index.tsx +1 -0
- package/src/framework/fields/FieldEditor/index.tsx +7 -1
- package/src/framework/fields/FieldNumberRange/index.tsx +14 -5
- package/src/framework/fields/FieldPercent/index.tsx +16 -5
- package/src/framework/fields/FieldRemoteSelect/index.tsx +4 -2
- package/src/framework/fields/FieldRemoteTreeSelect/index.tsx +2 -1
- package/src/framework/fields/FieldSysOrgTree/index.tsx +6 -1
- package/src/framework/fields/FieldSysOrgTreeSelect/index.tsx +6 -1
- package/src/framework/fields/FieldTable/index.tsx +35 -6
- package/src/framework/fields/FieldTableSelect/index.tsx +18 -2
- package/src/framework/fields/FieldUploadFile/index.tsx +69 -28
- package/src/framework/fields/index.ts +0 -1
- package/src/framework/index.ts +2 -0
- package/src/framework/router/Link.tsx +26 -0
- package/src/framework/router/PageFrame.tsx +31 -0
- package/src/framework/router/history.ts +50 -0
- package/src/framework/router/index.ts +4 -0
- package/src/framework/router/matcher.ts +58 -0
- package/src/framework/utils/{ArrayUtils.js → ArrayUtils.ts} +5 -5
- package/src/framework/utils/{DateUtils.js → DateUtils.ts} +19 -19
- package/src/framework/utils/{DeviceUtils.js → DeviceUtils.ts} +8 -1
- package/src/framework/utils/{EventBus.js → EventBus.ts} +13 -6
- package/src/framework/utils/MessageApi.ts +5 -0
- package/src/framework/utils/ModalApi.ts +5 -0
- package/src/framework/utils/ObjectUtils.ts +15 -0
- package/src/framework/utils/{StorageUtils.js → StorageUtils.ts} +7 -7
- package/src/framework/utils/{StringUtils.js → StringUtils.ts} +15 -15
- package/src/framework/utils/{TreeUtils.js → TreeUtils.ts} +19 -12
- package/src/framework/utils/{UrlUtils.js → UrlUtils.ts} +10 -10
- package/src/framework/utils/index.ts +2 -1
- package/src/framework/utils/system/{DictUtils.js → DictUtils.ts} +16 -4
- package/src/framework/utils/system/{HttpUtils.js → HttpUtils.ts} +25 -21
- package/src/framework/utils/system/PageUtils.ts +49 -0
- package/src/framework/utils/system/{PermUtils.js → PermUtils.ts} +5 -6
- package/src/framework/utils/system/index.ts +0 -2
- package/src/framework/views/ViewBoolean/index.tsx +6 -3
- package/src/framework/views/ViewFileButton/index.tsx +13 -8
- package/src/framework/views/ViewImage/index.tsx +19 -14
- package/src/framework/views/ViewRange/index.tsx +10 -7
- package/src/framework/views/ViewText/index.tsx +9 -11
- package/src/layouts/admin/AdminTabs.jsx +79 -0
- package/src/layouts/admin/AdminTabs.less +63 -0
- package/src/layouts/admin/HeaderRight.jsx +27 -13
- package/src/layouts/admin/TabLayout.jsx +124 -0
- package/src/layouts/admin/index.jsx +5 -6
- package/src/layouts/admin/index.less +1 -82
- package/src/layouts/index.tsx +148 -0
- package/src/main.jsx +8 -0
- package/src/pages/account/changePassword.jsx +14 -10
- package/src/pages/article/$code.jsx +2 -2
- package/src/pages/index.jsx +6 -4
- package/src/pages/{login.jsx → public/login.jsx} +7 -10
- package/src/pages/public/test.jsx +30 -0
- package/src/pages/{forceUpdatePwd.jsx → standalone/forceUpdatePwd.jsx} +3 -24
- package/src/pages/system/article/index.jsx +51 -56
- package/src/pages/system/dict/index.jsx +60 -91
- package/src/pages/system/file/index.jsx +23 -17
- package/src/pages/system/job/index.jsx +63 -72
- package/src/pages/system/log/index.jsx +11 -8
- package/src/pages/system/org/index.jsx +60 -80
- package/src/pages/system/role/index.jsx +50 -86
- package/src/pages/system/role/rolePerm.jsx +2 -2
- package/src/pages/system/user/index.jsx +120 -73
- package/src/pages/system/user/userPerm.jsx +2 -2
- package/vite-plugin/index.js +107 -0
- package/config/common-plugin.js +0 -145
- package/config/config.js +0 -5
- package/config/utils.js +0 -73
- package/src/.umi-production/appData.json +0 -2257
- package/src/.umi-production/core/EmptyRoute.tsx +0 -9
- package/src/.umi-production/core/defineApp.ts +0 -16
- package/src/.umi-production/core/helmet.ts +0 -10
- package/src/.umi-production/core/helmetContext.ts +0 -4
- package/src/.umi-production/core/history.ts +0 -72
- package/src/.umi-production/core/historyIntelli.ts +0 -132
- package/src/.umi-production/core/plugin.ts +0 -40
- package/src/.umi-production/core/pluginConfig.ts +0 -330
- package/src/.umi-production/core/pluginConfigJoi.d.ts +0 -7
- package/src/.umi-production/core/polyfill.ts +0 -220
- package/src/.umi-production/core/route.tsx +0 -47
- package/src/.umi-production/core/routeProps.js +0 -5
- package/src/.umi-production/core/routeProps.ts +0 -6
- package/src/.umi-production/core/terminal.ts +0 -37
- package/src/.umi-production/exports.ts +0 -17
- package/src/.umi-production/testBrowser.tsx +0 -90
- package/src/.umi-production/tsconfig.json +0 -43
- package/src/.umi-production/typings.d.ts +0 -136
- package/src/.umi-production/umi.ts +0 -83
- package/src/framework/GlobalData.d.ts +0 -10
- package/src/framework/GlobalData.js +0 -42
- package/src/framework/biz/HasPerm.tsx +0 -14
- package/src/framework/fields/FieldRemoteSelectMultiple/index.tsx +0 -29
- package/src/framework/utils/ArrayUtils.d.ts +0 -6
- package/src/framework/utils/DateUtils.d.ts +0 -20
- package/src/framework/utils/DeviceUtils.d.ts +0 -3
- package/src/framework/utils/EventBus.d.ts +0 -6
- package/src/framework/utils/MessageUtils.d.ts +0 -10
- package/src/framework/utils/MessageUtils.js +0 -43
- package/src/framework/utils/ObjectUtils.d.ts +0 -14
- package/src/framework/utils/ObjectUtils.js +0 -12
- package/src/framework/utils/StorageUtils.d.ts +0 -4
- package/src/framework/utils/StringUtils.d.ts +0 -17
- package/src/framework/utils/TreeUtils.d.ts +0 -13
- package/src/framework/utils/UrlUtils.d.ts +0 -6
- package/src/framework/utils/system/DictUtils.d.ts +0 -20
- package/src/framework/utils/system/FormRegistry.d.ts +0 -8
- package/src/framework/utils/system/FormRegistry.js +0 -31
- package/src/framework/utils/system/HttpUtils.d.ts +0 -14
- package/src/framework/utils/system/PageUtils.d.ts +0 -14
- package/src/framework/utils/system/PageUtils.js +0 -59
- package/src/framework/utils/system/PermUtils.d.ts +0 -5
- package/src/framework/utils/system/ThemeUtils.d.ts +0 -6
- package/src/framework/utils/system/ThemeUtils.js +0 -52
- package/src/layouts/PageRender.tsx +0 -29
- package/src/layouts/admin/TabPageRender.jsx +0 -210
- package/src/layouts/index.jsx +0 -120
- package/src/loading.jsx +0 -19
- package/src/pages/about.jsx +0 -16
- /package/src/pages/{login.less → public/login.less} +0 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {history} from '../../router';
|
|
2
|
+
import {StringUtils} from "../StringUtils";
|
|
3
|
+
import {UrlUtils} from "../UrlUtils";
|
|
4
|
+
import {EventBus} from "../EventBus";
|
|
5
|
+
|
|
6
|
+
export class PageUtils {
|
|
7
|
+
static redirectToLogin(): void {
|
|
8
|
+
if (PageUtils.currentPathname() === '/public/login') {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const url = PageUtils.currentUrl();
|
|
12
|
+
const encodedUrl = encodeURIComponent(url);
|
|
13
|
+
history.push(`/public/login?redirect=${encodedUrl}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
static currentParams(): Record<string, string | undefined> {
|
|
17
|
+
return UrlUtils.getParams();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static currentPathname(): string {
|
|
21
|
+
const path = window.location.hash.substring(1);
|
|
22
|
+
return StringUtils.subBefore(path, '?');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
static currentUrl(): string {
|
|
26
|
+
return window.location.hash.substring(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static currentPathnameLastPart(): string | undefined {
|
|
30
|
+
const path = this.currentPathname();
|
|
31
|
+
return StringUtils.subAfterLast(path, '/') || undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
static open(path: string, label = '未命名'): void {
|
|
35
|
+
let targetPath = path;
|
|
36
|
+
if (label) {
|
|
37
|
+
targetPath = UrlUtils.setParam(targetPath, '_label', label);
|
|
38
|
+
}
|
|
39
|
+
history.push(targetPath);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
static currentLabel(): string | undefined {
|
|
43
|
+
return this.currentParams()['_label'];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static closeCurrent(): void {
|
|
47
|
+
EventBus.emit('closePage', PageUtils.currentUrl());
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -2,14 +2,13 @@ import {GlobalData} from '../../GlobalData';
|
|
|
2
2
|
import {ArrayUtils} from '../ArrayUtils';
|
|
3
3
|
|
|
4
4
|
export class PermUtils {
|
|
5
|
-
static getPermissions() {
|
|
6
|
-
const
|
|
7
|
-
const { permissions } = info;
|
|
5
|
+
static getPermissions(): string[] {
|
|
6
|
+
const { permissions } = GlobalData.getLoginInfo();
|
|
8
7
|
if (permissions == null || permissions.length === 0) return [];
|
|
9
8
|
return permissions;
|
|
10
9
|
}
|
|
11
10
|
|
|
12
|
-
static hasPermission(perm) {
|
|
11
|
+
static hasPermission(perm: string | null | undefined): boolean {
|
|
13
12
|
if (perm === null || perm === undefined || perm === '') return false;
|
|
14
13
|
const permissions = PermUtils.getPermissions();
|
|
15
14
|
if (permissions.length === 0) return false;
|
|
@@ -17,11 +16,11 @@ export class PermUtils {
|
|
|
17
16
|
return permissions.indexOf(perm) > -1;
|
|
18
17
|
}
|
|
19
18
|
|
|
20
|
-
static isPermitted(p) {
|
|
19
|
+
static isPermitted(p: string | null | undefined): boolean {
|
|
21
20
|
return PermUtils.hasPermission(p);
|
|
22
21
|
}
|
|
23
22
|
|
|
24
|
-
static notPermitted(p) {
|
|
23
|
+
static notPermitted(p: string | null | undefined): boolean {
|
|
25
24
|
return !PermUtils.isPermitted(p);
|
|
26
25
|
}
|
|
27
26
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import React from 'react';
|
|
1
2
|
import type {ViewProps} from "../types";
|
|
2
3
|
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
export class ViewBoolean extends React.Component<ViewProps<boolean>> {
|
|
5
|
+
render() {
|
|
6
|
+
const {value} = this.props;
|
|
7
|
+
return value == null ? null : (value ? '是' : '否');
|
|
8
|
+
}
|
|
6
9
|
}
|
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import {Button} from "antd";
|
|
3
|
-
import { MessageUtils } from "../../utils";
|
|
2
|
+
import {Button, Modal} from "antd";
|
|
4
3
|
import { ViewFile } from "../ViewFile";
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* 文件按钮查看组件
|
|
8
7
|
*/
|
|
9
8
|
export class ViewFileButton extends React.Component {
|
|
9
|
+
state = { open: false }
|
|
10
|
+
|
|
10
11
|
render() {
|
|
11
|
-
const {value
|
|
12
|
+
const {value} = this.props;
|
|
12
13
|
if (!value) {
|
|
13
14
|
return null;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
return
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
return <>
|
|
18
|
+
<Button type="link" size="small" onClick={() => this.setState({open: true})}>
|
|
19
|
+
查看文件
|
|
20
|
+
</Button>
|
|
21
|
+
<Modal open={this.state.open} title="文件预览" footer={null} width="80vw"
|
|
22
|
+
onCancel={() => this.setState({open: false})}>
|
|
23
|
+
<ViewFile value={this.props.value} />
|
|
24
|
+
</Modal>
|
|
25
|
+
</>;
|
|
21
26
|
}
|
|
22
27
|
}
|
|
@@ -5,6 +5,11 @@ import {UrlUtils} from '../../utils';
|
|
|
5
5
|
|
|
6
6
|
export class ViewImage extends React.Component {
|
|
7
7
|
|
|
8
|
+
state = {
|
|
9
|
+
modalOpen: false,
|
|
10
|
+
previewUrl: null,
|
|
11
|
+
};
|
|
12
|
+
|
|
8
13
|
render() {
|
|
9
14
|
let vs = this.props.value
|
|
10
15
|
|
|
@@ -34,29 +39,29 @@ export class ViewImage extends React.Component {
|
|
|
34
39
|
urlList.push(UrlUtils.contextPath(v));
|
|
35
40
|
}
|
|
36
41
|
|
|
42
|
+
const closeModal = () => this.setState({modalOpen: false});
|
|
43
|
+
|
|
37
44
|
const imgs = urlList.map((url) => (
|
|
38
45
|
<img
|
|
39
46
|
style={{ display: 'inline-block' }}
|
|
40
47
|
key={url}
|
|
41
48
|
src={url}
|
|
42
|
-
onClick={() => this.
|
|
49
|
+
onClick={() => this.setState({modalOpen: true, previewUrl: url})}
|
|
43
50
|
width={60}
|
|
44
51
|
height={60}
|
|
45
52
|
/>
|
|
46
53
|
));
|
|
47
54
|
|
|
48
|
-
return
|
|
55
|
+
return (
|
|
56
|
+
<>
|
|
57
|
+
{imgs}
|
|
58
|
+
<Modal open={this.state.modalOpen} title="预览图片" width="70vw" footer={null}
|
|
59
|
+
onCancel={closeModal}>
|
|
60
|
+
<div style={{maxHeight:'70vh',overflow:'auto'}}>
|
|
61
|
+
<img src={this.state.previewUrl} style={{maxWidth: '100%'}}/>
|
|
62
|
+
</div>
|
|
63
|
+
</Modal>
|
|
64
|
+
</>
|
|
65
|
+
);
|
|
49
66
|
}
|
|
50
|
-
|
|
51
|
-
preview = (url) => {
|
|
52
|
-
Modal.info({
|
|
53
|
-
title: '预览图片',
|
|
54
|
-
width: '70vw',
|
|
55
|
-
content: <div style={{maxHeight:'70vh',overflow:'auto'}}>
|
|
56
|
-
<img src={url} style={{maxWidth: '100%'}}/>
|
|
57
|
-
</div>,
|
|
58
|
-
|
|
59
|
-
});
|
|
60
|
-
};
|
|
61
|
-
|
|
62
67
|
}
|
|
@@ -5,11 +5,14 @@ interface ViewRangeProps {
|
|
|
5
5
|
max?: string;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
export class ViewRange extends React.Component<ViewRangeProps> {
|
|
9
|
+
render() {
|
|
10
|
+
const { min, max } = this.props;
|
|
11
|
+
if ((min == null || min === "") && (max == null || max === "")) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
const showMin = min ?? '未知';
|
|
15
|
+
const showMax = max ?? '未知';
|
|
16
|
+
return <>{showMin} - {showMax}</>;
|
|
11
17
|
}
|
|
12
|
-
|
|
13
|
-
const showMax = max ?? '未知';
|
|
14
|
-
return <>{showMin} - {showMax}</>;
|
|
15
|
-
};
|
|
18
|
+
}
|
|
@@ -4,6 +4,10 @@ import {StringUtils} from "../../utils";
|
|
|
4
4
|
|
|
5
5
|
export class ViewText extends React.Component {
|
|
6
6
|
|
|
7
|
+
state = {
|
|
8
|
+
modalOpen: false,
|
|
9
|
+
};
|
|
10
|
+
|
|
7
11
|
static defaultProps = {
|
|
8
12
|
ellipsis: false,
|
|
9
13
|
maxLength: 15
|
|
@@ -23,20 +27,14 @@ export class ViewText extends React.Component {
|
|
|
23
27
|
const short = StringUtils.ellipsis(value, maxLength);
|
|
24
28
|
return (
|
|
25
29
|
<Tooltip placement="topLeft" title={value}>
|
|
26
|
-
<span style={{cursor: 'pointer', borderBottom: '1px dashed #d9d9d9'}} onClick={this.
|
|
30
|
+
<span style={{cursor: 'pointer', borderBottom: '1px dashed #d9d9d9'}} onClick={() => this.setState({modalOpen: true})}>
|
|
27
31
|
{short}
|
|
28
32
|
</span>
|
|
33
|
+
<Modal open={this.state.modalOpen} title="长文本内容" width={700} footer={null}
|
|
34
|
+
onCancel={() => this.setState({modalOpen: false})}>
|
|
35
|
+
<div style={{height: 500, overflowY: 'auto'}}>{value}</div>
|
|
36
|
+
</Modal>
|
|
29
37
|
</Tooltip>
|
|
30
38
|
);
|
|
31
39
|
}
|
|
32
|
-
|
|
33
|
-
showModal = () => {
|
|
34
|
-
const {value} = this.props;
|
|
35
|
-
Modal.info({
|
|
36
|
-
icon: null,
|
|
37
|
-
title: '长文本内容',
|
|
38
|
-
content: <div style={{height: 500, overflowY: 'auto'}}>{value}</div>,
|
|
39
|
-
width: 700
|
|
40
|
-
});
|
|
41
|
-
};
|
|
42
40
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {CloseOutlined} from '@ant-design/icons';
|
|
3
|
+
import {ContextMenu} from '../../framework';
|
|
4
|
+
import './AdminTabs.less';
|
|
5
|
+
|
|
6
|
+
export class AdminTabs extends React.Component {
|
|
7
|
+
state = {
|
|
8
|
+
contextMenu: null,
|
|
9
|
+
contextMenuTabKey: null,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
handleContextMenu = (e, key) => {
|
|
13
|
+
e.preventDefault();
|
|
14
|
+
this.setState({
|
|
15
|
+
contextMenu: {x: e.clientX, y: e.clientY},
|
|
16
|
+
contextMenuTabKey: key,
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
handleContextMenuClick = ({key: actionKey}) => {
|
|
21
|
+
const tabKey = this.state.contextMenuTabKey;
|
|
22
|
+
this.setState({contextMenu: null});
|
|
23
|
+
const {onRefresh, onClose, onCloseOthers, onCloseAll} = this.props;
|
|
24
|
+
if (actionKey === 'refresh') {
|
|
25
|
+
onRefresh(tabKey);
|
|
26
|
+
} else if (actionKey === 'close') {
|
|
27
|
+
onClose(tabKey);
|
|
28
|
+
} else if (actionKey === 'closeOthers') {
|
|
29
|
+
onCloseOthers(tabKey);
|
|
30
|
+
} else if (actionKey === 'closeAll') {
|
|
31
|
+
onCloseAll();
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
render() {
|
|
36
|
+
const {tabs, activeKey, onChange, onClose} = this.props;
|
|
37
|
+
const {contextMenu} = this.state;
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<>
|
|
41
|
+
<div className="admin-tabs-bar">
|
|
42
|
+
{tabs.map(tab => (
|
|
43
|
+
<div
|
|
44
|
+
key={tab.key}
|
|
45
|
+
className={`admin-tab${tab.key === activeKey ? ' active' : ''}`}
|
|
46
|
+
onClick={() => onChange(tab.key)}
|
|
47
|
+
onContextMenu={e => this.handleContextMenu(e, tab.key)}
|
|
48
|
+
>
|
|
49
|
+
<span className="admin-tab-label">{tab.label}</span>
|
|
50
|
+
{tabs.length > 1 && (
|
|
51
|
+
<CloseOutlined
|
|
52
|
+
className="admin-tab-close"
|
|
53
|
+
onClick={e => {
|
|
54
|
+
e.stopPropagation();
|
|
55
|
+
onClose(tab.key);
|
|
56
|
+
}}
|
|
57
|
+
/>
|
|
58
|
+
)}
|
|
59
|
+
</div>
|
|
60
|
+
))}
|
|
61
|
+
</div>
|
|
62
|
+
{contextMenu && (
|
|
63
|
+
<ContextMenu
|
|
64
|
+
x={contextMenu.x}
|
|
65
|
+
y={contextMenu.y}
|
|
66
|
+
items={[
|
|
67
|
+
{key: 'refresh', label: '刷新'},
|
|
68
|
+
{key: 'close', label: '关闭', disabled: tabs.length <= 1},
|
|
69
|
+
{key: 'closeOthers', label: '关闭其他', disabled: tabs.length <= 1},
|
|
70
|
+
{key: 'closeAll', label: '关闭全部'},
|
|
71
|
+
]}
|
|
72
|
+
onClick={this.handleContextMenuClick}
|
|
73
|
+
onClose={() => this.setState({contextMenu: null})}
|
|
74
|
+
/>
|
|
75
|
+
)}
|
|
76
|
+
</>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
.admin-tabs-bar {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-wrap: nowrap;
|
|
4
|
+
overflow-x: auto;
|
|
5
|
+
overflow-y: hidden;
|
|
6
|
+
flex-shrink: 0;
|
|
7
|
+
border-bottom: 1px solid #e8e8e8;
|
|
8
|
+
background: #fff;
|
|
9
|
+
|
|
10
|
+
&::-webkit-scrollbar {
|
|
11
|
+
height: 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.admin-tab {
|
|
15
|
+
display: inline-flex;
|
|
16
|
+
align-items: center;
|
|
17
|
+
gap: 4px;
|
|
18
|
+
padding: 0 12px;
|
|
19
|
+
height: 32px;
|
|
20
|
+
line-height: 32px;
|
|
21
|
+
font-size: 13px;
|
|
22
|
+
cursor: pointer;
|
|
23
|
+
border-right: 1px solid #e8e8e8;
|
|
24
|
+
white-space: nowrap;
|
|
25
|
+
color: rgba(0, 0, 0, 0.65);
|
|
26
|
+
background: #fafafa;
|
|
27
|
+
transition: color 0.2s, background 0.2s;
|
|
28
|
+
position: relative;
|
|
29
|
+
flex-shrink: 0;
|
|
30
|
+
|
|
31
|
+
&:hover {
|
|
32
|
+
color: var(--primary-color);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
&.active {
|
|
36
|
+
color: var(--primary-color);
|
|
37
|
+
background: #fff;
|
|
38
|
+
font-weight: 500;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.admin-tab-label {
|
|
42
|
+
max-width: 120px;
|
|
43
|
+
overflow: hidden;
|
|
44
|
+
text-overflow: ellipsis;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.admin-tab-close {
|
|
48
|
+
display: inline-flex;
|
|
49
|
+
align-items: center;
|
|
50
|
+
justify-content: center;
|
|
51
|
+
width: 16px;
|
|
52
|
+
height: 16px;
|
|
53
|
+
font-size: 12px;
|
|
54
|
+
border-radius: 50%;
|
|
55
|
+
color: rgba(0, 0, 0, 0.45);
|
|
56
|
+
|
|
57
|
+
&:hover {
|
|
58
|
+
background: rgba(0, 0, 0, 0.08);
|
|
59
|
+
color: rgba(0, 0, 0, 0.88);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import {Avatar, Dropdown} from "antd";
|
|
1
|
+
import {Avatar, Dropdown, Modal} from "antd";
|
|
2
2
|
import React from "react";
|
|
3
|
-
import {history} from "
|
|
4
|
-
import {DeviceUtils, HttpUtils, MessageUtils, PageUtils, GlobalData, ThemeUtils} from "../../framework";
|
|
3
|
+
import {DeviceUtils, HttpUtils, PageUtils, GlobalData, getToken, history, ARTICLE_HEADER_AVATAR_DROPDOWN, ARTICLE_HEADER_RIGHT} from "../../framework";
|
|
5
4
|
|
|
6
5
|
export class HeaderRight extends React.Component {
|
|
7
6
|
|
|
@@ -9,6 +8,8 @@ export class HeaderRight extends React.Component {
|
|
|
9
8
|
isMobileDevice: false,
|
|
10
9
|
dropdownArticles: [],
|
|
11
10
|
headerArticles: [],
|
|
11
|
+
alertVisible: false,
|
|
12
|
+
confirmVisible: false,
|
|
12
13
|
};
|
|
13
14
|
|
|
14
15
|
componentDidMount() {
|
|
@@ -21,23 +22,18 @@ export class HeaderRight extends React.Component {
|
|
|
21
22
|
loadArticles = () => {
|
|
22
23
|
const siteArticles = GlobalData.getSiteArticles();
|
|
23
24
|
this.setState({
|
|
24
|
-
dropdownArticles: siteArticles[
|
|
25
|
-
headerArticles: siteArticles[
|
|
25
|
+
dropdownArticles: siteArticles[ARTICLE_HEADER_AVATAR_DROPDOWN] || [],
|
|
26
|
+
headerArticles: siteArticles[ARTICLE_HEADER_RIGHT] || [],
|
|
26
27
|
})
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
logout = () => {
|
|
30
31
|
HttpUtils.post('admin/auth/logout').then(async () => {
|
|
31
32
|
localStorage.clear()
|
|
32
|
-
|
|
33
|
-
history.replace('/login')
|
|
33
|
+
this.setState({alertVisible: true})
|
|
34
34
|
}).catch(async e => {
|
|
35
35
|
console.error('[HeaderRight] 退出登录失败:', e);
|
|
36
|
-
|
|
37
|
-
if (confirm) {
|
|
38
|
-
localStorage.clear();
|
|
39
|
-
history.replace('/login')
|
|
40
|
-
}
|
|
36
|
+
this.setState({confirmVisible: true})
|
|
41
37
|
})
|
|
42
38
|
}
|
|
43
39
|
|
|
@@ -93,12 +89,30 @@ export class HeaderRight extends React.Component {
|
|
|
93
89
|
items: menuItems,
|
|
94
90
|
}}>
|
|
95
91
|
<div className='item' style={{cursor: 'pointer'}}>
|
|
96
|
-
<Avatar size="default" style={{backgroundColor:
|
|
92
|
+
<Avatar size="default" style={{backgroundColor: getToken().colorPrimary}}>
|
|
97
93
|
{info.name?.charAt(0)}
|
|
98
94
|
</Avatar>
|
|
99
95
|
<span style={{marginLeft: 8}}>{info.name}</span>
|
|
100
96
|
</div>
|
|
101
97
|
</Dropdown>
|
|
98
|
+
|
|
99
|
+
<Modal open={this.state.alertVisible} title="提示" okText="确定"
|
|
100
|
+
onCancel={() => this.setState({alertVisible: false})}
|
|
101
|
+
onOk={() => {
|
|
102
|
+
this.setState({alertVisible: false});
|
|
103
|
+
history.replace('/public/login')
|
|
104
|
+
}}>
|
|
105
|
+
退出登录成功
|
|
106
|
+
</Modal>
|
|
107
|
+
<Modal open={this.state.confirmVisible} title="确认操作" okText="确定" cancelText="取消"
|
|
108
|
+
onCancel={() => this.setState({confirmVisible: false})}
|
|
109
|
+
onOk={() => {
|
|
110
|
+
this.setState({confirmVisible: false});
|
|
111
|
+
localStorage.clear();
|
|
112
|
+
history.replace('/public/login')
|
|
113
|
+
}}>
|
|
114
|
+
退出登录失败,是否清空缓存
|
|
115
|
+
</Modal>
|
|
102
116
|
</div>
|
|
103
117
|
}
|
|
104
118
|
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {history, PageFrame, UrlUtils, EventBus} from "../../framework";
|
|
3
|
+
import {AdminTabs} from "./AdminTabs";
|
|
4
|
+
|
|
5
|
+
function getLabel(pathname, search, pathMenuMap) {
|
|
6
|
+
if (pathname === '/') return '首页';
|
|
7
|
+
const params = UrlUtils.getParams(search);
|
|
8
|
+
if (params._label) return params._label;
|
|
9
|
+
const menu = pathMenuMap[pathname];
|
|
10
|
+
if (menu) return menu.name;
|
|
11
|
+
return '未命名';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class TabLayout extends React.Component {
|
|
15
|
+
constructor(props) {
|
|
16
|
+
super(props);
|
|
17
|
+
const initUrl = history.location.pathname + history.location.search;
|
|
18
|
+
const qIndex = initUrl.indexOf('?');
|
|
19
|
+
const initPathname = qIndex === -1 ? initUrl : initUrl.substring(0, qIndex);
|
|
20
|
+
const initSearch = qIndex === -1 ? '' : initUrl.substring(qIndex);
|
|
21
|
+
const initLabel = getLabel(initPathname, initSearch, props.pathMenuMap);
|
|
22
|
+
this.state = {
|
|
23
|
+
tabs: [{key: initUrl, label: initLabel, refreshKey: 0}],
|
|
24
|
+
activeKey: initUrl,
|
|
25
|
+
};
|
|
26
|
+
this.activeKey = initUrl;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
unlisten = null;
|
|
30
|
+
unsubscribeClosePage = null;
|
|
31
|
+
|
|
32
|
+
openTab = (url) => {
|
|
33
|
+
const qIndex = url.indexOf('?');
|
|
34
|
+
const pathname = qIndex === -1 ? url : url.substring(0, qIndex);
|
|
35
|
+
const search = qIndex === -1 ? '' : url.substring(qIndex);
|
|
36
|
+
this.setState(prev => ({
|
|
37
|
+
tabs: prev.tabs.find(t => t.key === url) ? prev.tabs
|
|
38
|
+
: [...prev.tabs, {key: url, label: getLabel(pathname, search, this.props.pathMenuMap), refreshKey: 0}],
|
|
39
|
+
activeKey: url,
|
|
40
|
+
}));
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
closeTab = (key) => {
|
|
44
|
+
this.setState(prev => {
|
|
45
|
+
if (!prev.tabs.find(t => t.key === key)) return null;
|
|
46
|
+
const next = prev.tabs.filter(t => t.key !== key);
|
|
47
|
+
if (next.length === 0) {
|
|
48
|
+
setTimeout(() => history.push('/'), 0);
|
|
49
|
+
} else if (key === this.activeKey) {
|
|
50
|
+
const fallback = next[next.length - 1].key;
|
|
51
|
+
setTimeout(() => history.push(fallback), 0);
|
|
52
|
+
}
|
|
53
|
+
return {tabs: next};
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
refreshTab = (key) => {
|
|
58
|
+
this.setState(prev => ({
|
|
59
|
+
tabs: prev.tabs.map(t => t.key === key ? {...t, refreshKey: t.refreshKey + 1} : t),
|
|
60
|
+
}));
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
handleClosePageEvent = (url) => {
|
|
64
|
+
this.closeTab(url);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
handleCloseOthers = (key) => {
|
|
68
|
+
this.setState(prev => ({tabs: prev.tabs.filter(t => t.key === key)}));
|
|
69
|
+
if (key !== this.activeKey) {
|
|
70
|
+
history.push(key);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
handleCloseAll = () => {
|
|
75
|
+
this.setState({tabs: []});
|
|
76
|
+
setTimeout(() => history.push('/'), 0);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
componentDidMount() {
|
|
80
|
+
this.unlisten = history.listen(({location}) => {
|
|
81
|
+
this.openTab(location.pathname + location.search);
|
|
82
|
+
});
|
|
83
|
+
this.unsubscribeClosePage = EventBus.on('closePage', this.handleClosePageEvent);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
componentDidUpdate() {
|
|
87
|
+
this.activeKey = this.state.activeKey;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
componentWillUnmount() {
|
|
91
|
+
if (this.unlisten) {
|
|
92
|
+
this.unlisten();
|
|
93
|
+
}
|
|
94
|
+
if (this.unsubscribeClosePage) {
|
|
95
|
+
this.unsubscribeClosePage();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
render() {
|
|
100
|
+
const {tabs, activeKey} = this.state;
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<div style={{display: 'flex', flexDirection: 'column', height: '100%'}}>
|
|
104
|
+
<AdminTabs
|
|
105
|
+
tabs={tabs.map(t => ({key: t.key, label: t.label}))}
|
|
106
|
+
activeKey={activeKey}
|
|
107
|
+
onChange={key => history.push(key)}
|
|
108
|
+
onClose={this.closeTab}
|
|
109
|
+
onRefresh={this.refreshTab}
|
|
110
|
+
onCloseOthers={this.handleCloseOthers}
|
|
111
|
+
onCloseAll={this.handleCloseAll}
|
|
112
|
+
/>
|
|
113
|
+
<div style={{flex: 1, overflow: 'hidden'}}>
|
|
114
|
+
{tabs.map(t => (
|
|
115
|
+
<div key={t.key} style={{display: t.key === activeKey ? 'block' : 'none', height: '100%'}}>
|
|
116
|
+
<PageFrame key={t.refreshKey} url={t.key}/>
|
|
117
|
+
</div>
|
|
118
|
+
))}
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import {Layout, Menu, Skeleton, Watermark} from 'antd';
|
|
3
3
|
|
|
4
|
-
import {history, Link} from 'umi';
|
|
5
4
|
import "./index.less"
|
|
6
|
-
import {HttpUtils, NamedIcon, PageUtils, GlobalData,
|
|
5
|
+
import {HttpUtils, NamedIcon, PageUtils, GlobalData, TreeUtils, history, Link, ARTICLE_HEADER_LEFT} from "../../framework";
|
|
7
6
|
|
|
8
7
|
import { HeaderRight } from "./HeaderRight";
|
|
9
|
-
import {
|
|
8
|
+
import { TabLayout } from "./TabLayout";
|
|
10
9
|
const {Header, Sider, Content} = Layout;
|
|
11
10
|
/**
|
|
12
11
|
* 带菜单的布局,主要处理布局宇框架结构
|
|
@@ -45,7 +44,7 @@ export default class extends React.Component {
|
|
|
45
44
|
this.initMenu()
|
|
46
45
|
|
|
47
46
|
const siteArticles = GlobalData.getSiteArticles();
|
|
48
|
-
this.setState({headerLeftArticles: siteArticles[
|
|
47
|
+
this.setState({headerLeftArticles: siteArticles[ARTICLE_HEADER_LEFT] || []})
|
|
49
48
|
}
|
|
50
49
|
|
|
51
50
|
|
|
@@ -176,7 +175,7 @@ export default class extends React.Component {
|
|
|
176
175
|
render() {
|
|
177
176
|
const {siteInfo, loginInfo} = this.state
|
|
178
177
|
|
|
179
|
-
|
|
178
|
+
return <Layout className='main-layout'>
|
|
180
179
|
<Sider id='left-sider'
|
|
181
180
|
width={180}
|
|
182
181
|
collapsible
|
|
@@ -229,7 +228,7 @@ export default class extends React.Component {
|
|
|
229
228
|
renderCenterContent = () => {
|
|
230
229
|
const {siteInfo, loginInfo} = this.state
|
|
231
230
|
// 即使菜单为空也直接显示页面内容,不要一直显示 Skeleton
|
|
232
|
-
const tabPageRenderNode = <
|
|
231
|
+
const tabPageRenderNode = <TabLayout pathMenuMap={this.state.pathMenuMap}/>;
|
|
233
232
|
if (siteInfo.waterMark === true) {
|
|
234
233
|
return <Watermark content={[loginInfo.name, loginInfo.account]}>
|
|
235
234
|
{tabPageRenderNode}
|