@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.
Files changed (33) hide show
  1. package/package.json +1 -1
  2. package/src/.umi-production/appData.json +58 -78
  3. package/src/.umi-production/core/route.tsx +1 -3
  4. package/src/framework/components/ContextMenu/index.tsx +91 -0
  5. package/src/framework/components/Page/index.less +19 -6
  6. package/src/framework/components/Page/index.tsx +23 -3
  7. package/src/framework/components/ProTable/index.less +60 -9
  8. package/src/framework/components/ProTable/index.tsx +31 -48
  9. package/src/framework/components/index.ts +1 -0
  10. package/src/framework/fields/FieldTableSelect/index.tsx +0 -1
  11. package/src/layouts/admin/HeaderRight.jsx +1 -6
  12. package/src/layouts/admin/TabPageRender.jsx +80 -23
  13. package/src/layouts/admin/index.jsx +16 -22
  14. package/src/layouts/admin/index.less +31 -20
  15. package/src/layouts/index.jsx +0 -9
  16. package/src/pages/about.jsx +1 -1
  17. package/src/pages/index.jsx +1 -1
  18. package/src/pages/login.jsx +1 -5
  19. package/src/pages/system/dict/index.jsx +56 -53
  20. package/src/pages/system/file/index.jsx +1 -7
  21. package/src/pages/system/job/index.jsx +9 -14
  22. package/src/pages/system/log/index.jsx +1 -1
  23. package/src/pages/system/org/index.jsx +153 -173
  24. package/src/pages/system/role/index.jsx +21 -12
  25. package/src/pages/system/role/perm.jsx +1 -2
  26. package/src/pages/system/user/index.jsx +28 -43
  27. package/src/pages/userCenter/index.jsx +3 -2
  28. package/src/pages/userCenter/message.jsx +1 -1
  29. package/src/style/global.less +1 -1
  30. package/src/framework/components/ProTable/components/ToolBar/index.less +0 -53
  31. package/src/framework/components/ProTable/components/ToolBar/index.tsx +0 -123
  32. package/src/pages/system/manual/index.jsx +0 -128
  33. 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"},"system/manual/index":{"path":"system/manual","id":"system/manual/index","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"},"userCenter/manual":{"path":"userCenter/manual","id":"userCenter/manual","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;
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
- .tmgg-page {
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
- background-color: white;
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={'tmgg-page'} style={style}>
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
- .tmgg-pro-table{
2
- .search-form {
3
- .ant-form-item {
4
- margin: 0;
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
- .form-item-double-width {
8
- .ant-form-item-control {
9
- flex: 0 0 480px;
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={'tmgg-pro-table '} id={this.id}>
125
+ return <div className={'oa-pro-table '} id={this.id}>
130
126
  {this.renderForm()}
131
- {toolbarOptions !== false && <Toolbar
132
- actionRef={actionRef}
133
- toolBarRender={this.getToolBarRenderNode(toolBarRender)}
134
-
135
- onRefresh={() => this.loadData()}
136
- onSearch={this.onSearch}
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
- return
180
- }
181
- if (this.props["searchFormItemsRender"]) {
182
- throw new Error('不再支持 searchFormItemsRender,请直接放到ProTable的子节点')
183
- }
184
-
185
- return <div style={{marginBottom: '16px'}}>
186
- <Form
187
- layout="inline"
188
- onFinish={(values) => this.onSearch(values)}
189
- ref={(instance) => {
190
- this.formRef.current = instance;
191
- if (this.props.formRef != null) {
192
- this.props.formRef.current = instance;
193
- }
194
- }}
195
- style={{gap: '8px 0px'}}
196
- labelCol={{flex: '80px'}}
197
- wrapperCol={{flex: '200px'}}
198
- className='search-form'
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) {
@@ -7,6 +7,7 @@ export * from './LinkButton'
7
7
  export * from './Gap'
8
8
  export * from './Page'
9
9
  export * from './ValueType'
10
+ export * from './ContextMenu'
10
11
  export * from './OrgTree'
11
12
  export * from './RoleTree'
12
13
  export * from './ViewRange'
@@ -50,7 +50,6 @@ export class FieldTableSelect extends React.Component {
50
50
  }}>选择</Button>;
51
51
  }
52
52
  }]}
53
- showToolbarSearch
54
53
  request={(params) => {
55
54
  params.selected = this.props.value;
56
55
  return HttpUtils.get(this.props.url, params);
@@ -1,5 +1,5 @@
1
1
  import {Badge, Dropdown} from "antd";
2
- import {NotificationOutlined, QuestionCircleOutlined, SettingOutlined, UserOutlined} from "@ant-design/icons";
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: false, content: <PageRender pathname={pathname}/>}]);
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
- // Double click → refresh
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 handleClose = (key, e) => {
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 (key === activeKeyRef.current && next.length > 0) {
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, Gap, HttpUtils, NamedIcon, PageUtils, SysUtils, ThemeUtils, TreeUtils} from "../../framework";
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
- <Header className='header'>
201
- <div className='header-left'>
202
- {siteInfo.showLogo !== false && siteInfo.logoUrl &&
203
- <img className='logo-img' src={siteInfo.logoUrl} onClick={() => history.push('/')} alt='logo'/>}
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: ThemeUtils.getColor("primary-color")}}>{siteInfo.title} </Link>
206
+ <Link to="/" style={{color: 'rgba(255,255,255,0.85)'}}>{siteInfo.title}</Link>
206
207
  </h3>
207
-
208
208
  </div>
209
- {this.renderTopMenu()}
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
- {this.renderLeftMenu()}
211
+ </Sider>
222
212
 
223
- </Sider>
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>