@jiangood/open-admin 2.0.1 → 2.2.5

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 (47) hide show
  1. package/config/common-plugin.js +51 -0
  2. package/config/config.js +1 -56
  3. package/package.json +5 -2
  4. package/src/.umi-production/appData.json +73 -100
  5. package/src/.umi-production/umi.ts +2 -2
  6. package/src/framework/components/DownloadFileButton/index.tsx +1 -1
  7. package/src/framework/components/OrgTree/index.tsx +1 -1
  8. package/src/framework/components/Page/index.less +1 -1
  9. package/src/framework/components/Page/index.tsx +19 -22
  10. package/src/framework/components/PageLoading/index.tsx +5 -3
  11. package/src/framework/components/ProModal/index.tsx +2 -2
  12. package/src/framework/components/ProTable/components/ToolBar/index.tsx +1 -1
  13. package/src/framework/fields/FieldEditor/index.tsx +3 -2
  14. package/src/framework/fields/FieldRemoteTreeSelect/index.tsx +3 -3
  15. package/src/framework/fields/FieldRemoteTreeSelectMultiple/index.tsx +3 -3
  16. package/src/framework/fields/FieldUploadFile/index.tsx +8 -4
  17. package/src/framework/utils/MessageUtils.tsx +21 -99
  18. package/src/framework/utils/system/HttpUtils.ts +5 -1
  19. package/src/framework/utils/system/SysUtils.ts +8 -0
  20. package/src/framework/utils/system/ThemeUtils.ts +37 -12
  21. package/src/framework/views/ViewFile/index.tsx +1 -1
  22. package/src/framework/views/ViewProcessInstanceProgress/index.tsx +1 -0
  23. package/src/layouts/PageRender.tsx +29 -0
  24. package/src/layouts/admin/HeaderRight.jsx +1 -0
  25. package/src/layouts/admin/TabPageRender.jsx +138 -158
  26. package/src/layouts/admin/index.jsx +123 -19
  27. package/src/layouts/admin/index.less +124 -6
  28. package/src/layouts/index.jsx +94 -161
  29. package/src/pages/404.jsx +7 -5
  30. package/src/pages/about.jsx +3 -3
  31. package/src/pages/index.jsx +4 -5
  32. package/src/pages/login.jsx +18 -9
  33. package/src/pages/login.less +0 -1
  34. package/src/pages/system/dict/index.jsx +8 -11
  35. package/src/pages/system/file/index.jsx +6 -6
  36. package/src/pages/system/job/index.jsx +26 -11
  37. package/src/pages/system/log/index.jsx +4 -5
  38. package/src/pages/system/manual/index.jsx +8 -6
  39. package/src/pages/system/org/index.jsx +4 -2
  40. package/src/pages/system/role/index.jsx +16 -14
  41. package/src/pages/system/role/perm.jsx +3 -4
  42. package/src/pages/system/user/UserPerm.jsx +1 -1
  43. package/src/pages/system/user/index.jsx +9 -7
  44. package/src/pages/userCenter/index.jsx +2 -2
  45. package/src/pages/userCenter/message.jsx +4 -2
  46. package/src/layouts/PageRender.d.ts +0 -22
  47. package/src/layouts/PageRender.jsx +0 -90
@@ -1,173 +1,153 @@
1
- import React from "react";
2
- import {withRouter} from "umi";
3
- import {Tabs} from "antd";
1
+ import React, {useState, useEffect, useRef} from "react";
2
+ import {history} from "umi";
4
3
  import {PageRender} from "../PageRender";
5
- import {PageUtils} from "../../framework";
4
+ import {UrlUtils} from "../../framework";
6
5
 
7
6
  const MAX_TABS = 20;
8
7
 
9
- class TabPageRenderComponent extends React.Component {
10
-
11
- state = {
12
- active: null,
13
- urlLabelMap: {},
14
- tabs: [],
15
- }
16
-
17
- accessOrder = [];
18
-
19
- componentDidMount() {
20
- const url = this.getUrl(this.props)
21
- this.onUrlChange(url);
22
-
23
- document.addEventListener('close-page-event', (e)=>{
24
- const url = e.detail.url
25
- this.onRemove(url)
26
- })
27
- }
28
-
29
- componentDidUpdate(prevProps, prevState, snapshot) {
30
- const url = this.getUrl(this.props)
31
- const pUrl = this.getUrl(prevProps)
32
-
33
- if (url !== pUrl) {
34
- this.onUrlChange(url);
35
- }
36
- }
37
-
38
- recordAccess(url) {
39
- this.accessOrder = this.accessOrder.filter(k => k !== url);
40
- this.accessOrder.push(url);
41
- }
8
+ function getLabel(pathname, search, pathMenuMap) {
9
+ if (pathname === '/') return '首页';
10
+ const params = UrlUtils.getParams(search);
11
+ if (params._label) return params._label;
12
+ const menu = pathMenuMap[pathname];
13
+ if (menu) return menu.name;
14
+ return '临时';
15
+ }
42
16
 
43
- onUrlChange = url => {
44
- const {location} = this.props
45
- const {pathname, search} = location
46
- const {tabs} = this.state
17
+ export function TabPageRender({pathMenuMap}) {
18
+ const [tabs, setTabs] = useState([]);
19
+ const [activeKey, setActiveKey] = useState(null);
20
+ const accessOrderRef = useRef([]);
21
+ const lastClickTimeRef = useRef({});
22
+ const activeKeyRef = useRef(activeKey);
23
+ activeKeyRef.current = activeKey;
24
+ const pathMenuMapRef = useRef(pathMenuMap);
25
+ pathMenuMapRef.current = pathMenuMap;
26
+
27
+ const recordAccess = (key) => {
28
+ accessOrderRef.current = accessOrderRef.current.filter(k => k !== key);
29
+ accessOrderRef.current.push(key);
30
+ };
47
31
 
48
- const old = tabs.find(t => t.key === url)
49
- if (old == null) {
50
- const cmp = <PageRender pathname={pathname} search={search}/>
51
- const label = this.getLabel(pathname);
52
- tabs.push({
53
- key: url,
54
- label: label,
55
- children: cmp
32
+ // Use history.listen to monitor route changes
33
+ useEffect(() => {
34
+ const unlisten = history.listen(({location}) => {
35
+ const {pathname, search} = location;
36
+ const key = pathname + (search || '');
37
+ recordAccess(key);
38
+
39
+ setTabs(prev => {
40
+ if (prev.find(t => t.key === key)) return prev;
41
+ const content = <PageRender pathname={pathname}/>;
42
+ const label = getLabel(pathname, search, pathMenuMapRef.current);
43
+ const next = [...prev, {key, label, closable: true, content}];
44
+ if (next.length > MAX_TABS) {
45
+ const keep = new Set([key, ...accessOrderRef.current.slice(-3)]);
46
+ const removeIdx = next.findIndex(t => !keep.has(t.key));
47
+ if (removeIdx > -1) next.splice(removeIdx, 1);
48
+ }
49
+ return next;
56
50
  });
57
51
 
58
- if (tabs.length > MAX_TABS) {
59
- const keep = new Set([url, ...this.accessOrder.slice(-3)]);
60
- const removeIdx = tabs.findIndex(t => !keep.has(t.key));
61
- if (removeIdx > -1) {
62
- tabs.splice(removeIdx, 1);
52
+ setActiveKey(key);
53
+ });
54
+
55
+ // Trigger for initial route
56
+ const {pathname, search} = history.location;
57
+ const key = pathname + (search || '');
58
+ recordAccess(key);
59
+ setTabs([{key, label: getLabel(pathname, search, pathMenuMap), closable: false, content: <PageRender pathname={pathname}/>}]);
60
+ setActiveKey(key);
61
+
62
+ return unlisten;
63
+ }, []);
64
+
65
+ // Listen for close-page-event (stable listener, no re-registration)
66
+ useEffect(() => {
67
+ const handler = (e) => {
68
+ const url = e.detail.url;
69
+ setTabs(prev => {
70
+ const next = prev.filter(t => t.key !== url);
71
+ accessOrderRef.current = accessOrderRef.current.filter(k => k !== url);
72
+ if (url === activeKeyRef.current && next.length > 0) {
73
+ history.push(next[next.length - 1].key);
63
74
  }
64
- }
65
-
66
- this.setState({tabs: [...tabs]})
67
- }else {
68
- const menu = this.props.pathMenuMap[pathname]
69
- if (menu && menu.refreshOnTabClick) {
70
- this.refresh(pathname)
71
- }
72
- }
73
-
74
- this.recordAccess(url);
75
- this.setState({active: url})
76
- };
77
-
78
- getLabel(path) {
79
- if (path === '/') {
80
- return '首页'
81
- }
82
- const label = PageUtils.currentLabel();
83
-
84
- if (!label) {
85
- const menu = this.props.pathMenuMap[path]
86
- if (menu) {
87
- return menu.name
88
- }
89
- }
90
-
91
- return label;
92
- }
93
-
94
-
95
- render() {
96
- const {tabs} = this.state
97
- if (tabs.length === 0) {
98
- return null
99
- }
100
-
101
- return <>
102
- <Tabs
103
- items={tabs}
104
- activeKey={this.state.active}
105
- onChange={this.onChange}
106
- onEdit={this.onRemove}
107
-
108
- hideAdd
109
- size='small'
110
- type='editable-card'
111
- style={{background: 'white'}}
112
- rootClassName='tmgg-layout-tabs'
113
-
114
- onTabClick={this.onTabClick}
115
-
116
- >
117
- </Tabs>
118
- </>
119
- }
120
-
121
- lastTabClickTime = 0
122
- onTabClick = (key, event) => {
123
- const now = new Date().getTime();
124
- const doubleClick = now - this.lastTabClickTime < 300;
125
- if (doubleClick) {
126
- this.refresh(key);
75
+ return next;
76
+ });
77
+ };
78
+ document.addEventListener('close-page-event', handler);
79
+ return () => document.removeEventListener('close-page-event', handler);
80
+ }, []);
81
+
82
+ const handleTabClick = (key) => {
83
+ const now = Date.now();
84
+ const last = lastClickTimeRef.current[key] || 0;
85
+ lastClickTimeRef.current[key] = now;
86
+ 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
+ });
105
+ } else if (key !== activeKey) {
106
+ history.push(key);
127
107
  }
128
-
129
- this.lastTabClickTime = now
130
108
  };
131
109
 
132
- refresh = key => {
133
- const tabs = this.state.tabs;
134
- const tab = tabs.find(t => t.key === key)
135
- if(tab != null){
136
- const old = tab.children
137
- if(old != null){
138
- tab.children = '刷新中...'
139
- this.setState({tabs}, () => {
140
- tab.children = old
141
- this.setState({tabs})
142
- })
110
+ const handleClose = (key, e) => {
111
+ e.stopPropagation();
112
+ setTabs(prev => {
113
+ const next = prev.filter(t => t.key !== key);
114
+ accessOrderRef.current = accessOrderRef.current.filter(k => k !== key);
115
+ if (key === activeKeyRef.current && next.length > 0) {
116
+ history.push(next[next.length - 1].key);
143
117
  }
144
- }
118
+ return next;
119
+ });
145
120
  };
146
121
 
147
- onChange = url => {
148
- if (url !== this.state.active) {
149
- this.props.history.push(url)
150
- }
151
- };
152
-
153
- onRemove = url => {
154
- let {tabs} = this.state
155
- tabs = tabs.filter(t => t.key !== url)
156
- this.accessOrder = this.accessOrder.filter(k => k !== url);
157
-
158
- this.setState({tabs})
159
- if (tabs.length > 0) {
160
- this.setState({active: tabs[tabs.length - 1].key})
161
- }
162
- };
163
-
164
- getUrl = props => {
165
- const {location} = props
166
- const {pathname, search} = location
167
- return pathname + search;
168
- }
169
-
170
- }
171
-
172
-
173
- export const TabPageRender = withRouter(TabPageRenderComponent)
122
+ if (tabs.length === 0) return null;
123
+
124
+ return (
125
+ <div className="oa-tabs">
126
+ <div className="oa-tab-bar">
127
+ {tabs.map(tab => (
128
+ <div
129
+ key={tab.key}
130
+ className={'oa-tab' + (tab.key === activeKey ? ' active' : '')}
131
+ onClick={() => handleTabClick(tab.key)}
132
+ >
133
+ <span className="oa-tab-label">{tab.label}</span>
134
+ {tab.closable && tabs.length > 1 && (
135
+ <span className="oa-tab-close" onClick={(e) => handleClose(tab.key, e)}>×</span>
136
+ )}
137
+ </div>
138
+ ))}
139
+ </div>
140
+ <div className="oa-tabs-content">
141
+ {tabs.map(tab => (
142
+ <div
143
+ key={tab.key}
144
+ style={{display: tab.key === activeKey ? undefined : 'none'}}
145
+ className="oa-tab-pane"
146
+ >
147
+ {tab.content}
148
+ </div>
149
+ ))}
150
+ </div>
151
+ </div>
152
+ );
153
+ }
@@ -25,6 +25,9 @@ export default class extends React.Component {
25
25
 
26
26
  currentMenuKey: null,
27
27
 
28
+ topMenus: [],
29
+ sideMenus: [],
30
+ activeTopMenuKey: null,
28
31
 
29
32
  siteInfo: {},
30
33
 
@@ -55,24 +58,130 @@ export default class extends React.Component {
55
58
  item.icon = <NamedIcon name={item.icon || 'AppstoreOutlined'} style={{fontSize: 12}}/>
56
59
  })
57
60
 
61
+ const {topMenus} = this.classifyMenus(menuTree);
62
+
63
+ // 确定当前活跃的顶部菜单和侧边菜单
64
+ let activeTopMenuKey = null;
65
+ let sideMenus = [];
66
+ let currentMenuKey = null;
67
+
58
68
  if (pathname !== "" && pathname !== "/") {
59
- const menu = pathMenuMap[pathname]
60
- if (menu) {
61
- this.setState({currentMenuKey: menu.key})
69
+ const menuDef = pathMenuMap[pathname];
70
+ if (menuDef) {
71
+ currentMenuKey = menuDef.id;
72
+ const matched = this.findActiveTopMenu(topMenus, menuDef, menuMap);
73
+ if (matched) {
74
+ activeTopMenuKey = matched.key;
75
+ sideMenus = matched.children || [];
76
+ }
62
77
  }
63
78
  }
64
79
 
65
- this.setState({menuTree, pathMenuMap})
80
+ // 默认选中第一个顶部菜单
81
+ if (!activeTopMenuKey && topMenus.length > 0) {
82
+ activeTopMenuKey = topMenus[0].key;
83
+ sideMenus = topMenus[0].children || [];
84
+ if (!currentMenuKey && sideMenus.length > 0) {
85
+ currentMenuKey = sideMenus[0].key;
86
+ if (sideMenus[0].path) {
87
+ history.push(sideMenus[0].path);
88
+ }
89
+ }
90
+ }
91
+
92
+ this.setState({menuTree, pathMenuMap, topMenus, sideMenus, activeTopMenuKey, currentMenuKey})
66
93
 
67
94
  this.loadBadge(menuMap)
95
+ }).catch(err => {
96
+ console.error('加载菜单失败:', err)
97
+ this.setState({menuTree: [], topMenus: [], sideMenus: []})
68
98
  }).finally(()=>{
69
99
  this.setState({menuLoading: false})
70
100
  })
101
+ }
102
+ actionRef = React.createRef()
103
+
104
+ classifyMenus = (menuTree) => {
105
+ const topMenus = [];
106
+ const leafRootNodes = [];
71
107
 
108
+ (menuTree || []).forEach(node => {
109
+ const isDir = node.type === 'directory' || (node.children && node.children.length > 0);
110
+ if (isDir) {
111
+ topMenus.push(node);
112
+ } else {
113
+ leafRootNodes.push(node);
114
+ }
115
+ });
116
+
117
+ if (leafRootNodes.length > 0) {
118
+ const defaultGroup = {
119
+ key: '_default_group',
120
+ label: '默认',
121
+ title: '默',
122
+ icon: <NamedIcon name="AppstoreOutlined" style={{fontSize: 12}}/>,
123
+ children: leafRootNodes,
124
+ type: 'directory',
125
+ };
126
+ topMenus.unshift(defaultGroup);
127
+ }
72
128
 
129
+ return {topMenus};
73
130
  }
74
- actionRef = React.createRef()
75
131
 
132
+ findActiveTopMenu = (topMenus, menuDef, menuMap) => {
133
+ if (!menuDef) return null;
134
+
135
+ // 从 menuDef 的 pid 链向上查找匹配的 topMenu
136
+ let pid = menuDef.pid || menuDef.parentKey;
137
+ while (pid) {
138
+ const match = topMenus.find(t => t.key === pid);
139
+ if (match) return match;
140
+ const parent = menuMap[pid];
141
+ pid = parent ? (parent.pid || parent.parentKey) : null;
142
+ }
143
+ return null;
144
+ }
145
+
146
+ onTopMenuClick = ({key}) => {
147
+ const topMenu = this.state.topMenus.find(t => t.key === key);
148
+ if (!topMenu) return;
149
+
150
+ const sideMenus = topMenu.children || [];
151
+ this.setState({activeTopMenuKey: key, sideMenus});
152
+
153
+ if (sideMenus.length > 0 && sideMenus[0].path) {
154
+ this.setState({currentMenuKey: sideMenus[0].key});
155
+ history.push(sideMenus[0].path);
156
+ }
157
+ }
158
+
159
+ onLeftMenuClick = ({key}) => {
160
+ const menu = this.state.menuMap[key];
161
+ if (!menu) return;
162
+ const {path} = menu;
163
+ this.setState({currentMenuKey: key});
164
+ history.push(path);
165
+ }
166
+
167
+ renderTopMenu = () => {
168
+ if (this.state.menuLoading) return null;
169
+ const {topMenus, activeTopMenuKey} = this.state;
170
+ if (!topMenus || topMenus.length === 0) return null;
171
+ return (
172
+ <nav className="top-nav">
173
+ {topMenus.map(item => (
174
+ <span
175
+ key={item.key}
176
+ className={'top-nav-item' + (item.key === activeTopMenuKey ? ' active' : '')}
177
+ onClick={() => this.onTopMenuClick({key: item.key})}
178
+ >
179
+ {item.label}
180
+ </span>
181
+ ))}
182
+ </nav>
183
+ );
184
+ }
76
185
 
77
186
  loadBadge = menuMap => {
78
187
  for (let id in menuMap) {
@@ -88,7 +197,8 @@ export default class extends React.Component {
88
197
  menu.icon = <Badge dot count={rs} size={"small"}>{menu.icon}</Badge>
89
198
  this.setState({menuTree: [...menuTree]})
90
199
  }
91
-
200
+ }).catch(e => {
201
+ console.error('[Layout] 加载消息数失败:', e);
92
202
  })
93
203
  }
94
204
  };
@@ -97,7 +207,7 @@ export default class extends React.Component {
97
207
  render() {
98
208
  const {siteInfo, loginInfo} = this.state
99
209
 
100
- return <Layout className='main-layout'>
210
+ return <Layout className='main-layout' style={{'--primary-color': ThemeUtils.getColor("primary-color"), '--primary-color-click': ThemeUtils.getColor("primary-color-click")}}>
101
211
  <Header className='header'>
102
212
  <div className='header-left'>
103
213
  {siteInfo.showLogo !== false && siteInfo.logoUrl &&
@@ -107,10 +217,11 @@ export default class extends React.Component {
107
217
  </h3>
108
218
 
109
219
  </div>
220
+ {this.renderTopMenu()}
110
221
  <HeaderRight/>
111
222
  </Header>
112
223
 
113
- <Layout style={{height: '100%'}}>
224
+ <Layout style={{flex: 1, overflow: 'hidden'}}>
114
225
  <Sider id='left-sider'
115
226
  collapsible
116
227
  breakpoint={'md'}
@@ -137,17 +248,12 @@ export default class extends React.Component {
137
248
  if(this.state.menuLoading){
138
249
  return <div style={{padding: 16}}><Skeleton active title={false} paragraph={{rows: 8}} /></div>;
139
250
  }
140
- return <Menu items={this.state.menuTree}
251
+ return <Menu items={this.state.sideMenus}
141
252
  theme='dark'
142
253
  mode="inline"
143
254
  className='left-menu'
144
- onClick={({key}) => {
145
- const menu = this.state.menuMap[key]
146
- const {path} = menu;
147
- this.setState({currentMenuKey: key})
148
- history.push(path)
149
- }}
150
- selectedKeys={[this.state.currentMenuKey]}
255
+ onClick={this.onLeftMenuClick}
256
+ selectedKeys={this.state.currentMenuKey ? [this.state.currentMenuKey] : []}
151
257
  inlineIndent={16}
152
258
  >
153
259
  </Menu>;
@@ -155,9 +261,7 @@ export default class extends React.Component {
155
261
 
156
262
  renderCenterContent = () => {
157
263
  const {siteInfo, loginInfo} = this.state
158
- if (this.state.menuTree.length === 0) { // 加载菜单中
159
- return <Skeleton active paragraph={{rows: 10}} style={{padding: 24}} />
160
- }
264
+ // 即使菜单为空也直接显示页面内容,不要一直显示 Skeleton
161
265
  const tabPageRenderNode = <TabPageRender pathMenuMap={this.state.pathMenuMap}/>;
162
266
  if (siteInfo.waterMark === true) {
163
267
  return <Watermark content={[loginInfo.name, loginInfo.account]}>
@@ -1,10 +1,10 @@
1
1
  .main-layout {
2
- min-height: 100vh;
2
+ height: 100vh;
3
+ overflow: hidden;
3
4
  .header {
4
5
  padding: 0 20px;
5
6
  height: 48px;
6
7
  display: flex;
7
- justify-content: space-between;
8
8
  align-items: center;
9
9
  border-bottom: 1px solid rgb(233, 237, 240);
10
10
  background-color: rgb(246, 248, 250);
@@ -14,6 +14,7 @@
14
14
  display: flex;
15
15
  align-items: center;
16
16
  gap: 8px;
17
+ flex-shrink: 0;
17
18
 
18
19
  .logo-img {
19
20
  height: 38px;
@@ -21,17 +22,48 @@
21
22
  }
22
23
  }
23
24
 
25
+ .top-nav {
26
+ flex: 1;
27
+ display: flex;
28
+ align-items: center;
29
+ margin: 0 16px;
30
+ height: 100%;
31
+ overflow: hidden;
32
+
33
+ .top-nav-item {
34
+ display: inline-flex;
35
+ align-items: center;
36
+ gap: 4px;
37
+ padding: 0 16px;
38
+ height: 100%;
39
+ cursor: pointer;
40
+ color: rgba(0, 0, 0, 0.65);
41
+ transition: color 0.2s;
42
+ white-space: nowrap;
43
+
44
+ &:hover {
45
+ color: var(--primary-color, var(--primary-color, #1961AC));
46
+ }
47
+
48
+ &.active {
49
+ color: var(--primary-color, var(--primary-color, #1961AC));
50
+ box-shadow: inset 0 -2px 0 var(--primary-color, var(--primary-color, #1961AC));
51
+ }
52
+ }
53
+ }
54
+
24
55
  .header-right {
25
56
  line-height: 36px;
26
57
  display: flex;
27
58
  align-items: center;
59
+ flex-shrink: 0;
28
60
 
29
61
  .item {
30
62
  padding: 0 0.5rem;
31
63
  cursor: pointer;
32
64
 
33
65
  &:hover {
34
- background: #063A69;
66
+ background: var(--primary-color-click, #124B93);
35
67
  color: white;
36
68
  }
37
69
  }
@@ -59,10 +91,96 @@
59
91
  }
60
92
 
61
93
  #admin-layout-content {
62
- .tmgg-layout-tabs {
63
- &>.ant-tabs-nav {
64
- margin-bottom: 8px;
94
+ display: flex;
95
+ flex-direction: column;
96
+ overflow: hidden;
97
+
98
+ .oa-tabs {
99
+ flex: 1;
100
+ display: flex;
101
+ flex-direction: column;
102
+ overflow: hidden;
103
+
104
+ .oa-tab-bar {
105
+ display: flex;
106
+ flex-wrap: nowrap;
107
+ overflow-x: auto;
108
+ overflow-y: hidden;
109
+ flex-shrink: 0;
110
+ margin-bottom: 0;
111
+ border-bottom: 1px solid #e8e8e8;
112
+ background: #fff;
113
+
114
+ &::-webkit-scrollbar {
115
+ height: 0;
116
+ }
117
+ }
118
+
119
+ .oa-tab {
120
+ display: inline-flex;
121
+ align-items: center;
122
+ gap: 4px;
123
+ padding: 0 12px;
124
+ height: 32px;
125
+ line-height: 32px;
126
+ font-size: 13px;
127
+ cursor: pointer;
128
+ border-right: 1px solid #e8e8e8;
129
+ white-space: nowrap;
130
+ color: rgba(0, 0, 0, 0.65);
131
+ background: #fafafa;
132
+ transition: color 0.2s, background 0.2s;
133
+ position: relative;
134
+ flex-shrink: 0;
135
+
136
+ &:hover {
137
+ color: var(--primary-color, #1961AC);
138
+ }
139
+
140
+ &.active {
141
+ color: var(--primary-color, #1961AC);
142
+ background: #fff;
143
+ font-weight: 500;
144
+ }
145
+
146
+ .oa-tab-label {
147
+ max-width: 120px;
148
+ overflow: hidden;
149
+ text-overflow: ellipsis;
150
+ }
151
+
152
+ .oa-tab-close {
153
+ display: inline-flex;
154
+ align-items: center;
155
+ justify-content: center;
156
+ width: 16px;
157
+ height: 16px;
158
+ font-size: 12px;
159
+ line-height: 1;
160
+ border-radius: 50%;
161
+ color: rgba(0, 0, 0, 0.45);
162
+
163
+ &:hover {
164
+ background: rgba(0, 0, 0, 0.08);
165
+ color: rgba(0, 0, 0, 0.88);
166
+ }
167
+ }
168
+ }
169
+
170
+ .oa-tabs-content {
171
+ flex: 1;
172
+ overflow: hidden;
173
+
174
+ .oa-tab-pane {
175
+ height: 100%;
176
+ }
65
177
  }
66
178
  }
67
179
  }
68
180
  }
181
+
182
+ @media (max-width: 768px) {
183
+ .top-nav {
184
+ display: none !important;
185
+ }
186
+ }