@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.
Files changed (41) hide show
  1. package/package.json +2 -5
  2. package/src/framework/biz/UserPicker.less +17 -0
  3. package/src/framework/biz/UserPicker.tsx +296 -0
  4. package/src/framework/biz/index.tsx +2 -0
  5. package/src/framework/biz/pickerUser.ts +19 -0
  6. package/src/framework/components/ContextMenu/index.tsx +11 -8
  7. package/src/framework/components/DownloadModal/index.tsx +4 -4
  8. package/src/framework/components/ErrorBoundary/index.tsx +1 -1
  9. package/src/framework/components/OrgSwitcher/index.tsx +1 -1
  10. package/src/framework/components/ProTable/index.less +26 -8
  11. package/src/framework/components/ProTable/index.tsx +9 -3
  12. package/src/framework/components/ThemeSettings/index.tsx +173 -0
  13. package/src/framework/components/index.ts +1 -0
  14. package/src/framework/config/index.ts +44 -2
  15. package/src/framework/config/layoutConfig.ts +52 -0
  16. package/src/framework/config/themeConfig.ts +449 -14
  17. package/src/framework/fields/FieldEditor/index.tsx +2 -2
  18. package/src/framework/fields/FieldUploadFile/index.tsx +3 -3
  19. package/src/framework/fields/FieldUploadImage/index.tsx +15 -15
  20. package/src/framework/fields/FieldUserPicker/index.tsx +124 -0
  21. package/src/framework/fields/index.ts +1 -0
  22. package/src/framework/utils/HttpClient.ts +15 -4
  23. package/src/framework/utils/MessageApi.ts +16 -3
  24. package/src/framework/views/ViewText/index.tsx +1 -1
  25. package/src/layouts/admin/AdminTabs.less +11 -11
  26. package/src/layouts/admin/HeaderRight.jsx +23 -12
  27. package/src/layouts/admin/SinglePageLayout.jsx +36 -0
  28. package/src/layouts/admin/TabLayout.jsx +21 -1
  29. package/src/layouts/admin/index.jsx +45 -25
  30. package/src/layouts/admin/index.less +43 -23
  31. package/src/layouts/index.tsx +29 -4
  32. package/src/pages/article/article.less +13 -13
  33. package/src/pages/public/login.jsx +29 -15
  34. package/src/pages/public/login.less +243 -22
  35. package/src/pages/standalone/forceUpdatePwd.jsx +8 -7
  36. package/src/pages/system/codegen/index.jsx +56 -51
  37. package/src/pages/system/job/index.jsx +44 -7
  38. package/src/pages/system/role/index.jsx +17 -43
  39. package/src/pages/system/user/index.jsx +0 -4
  40. package/src/pages/system/user/userPerm.jsx +3 -3
  41. 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
- messageApi.success(message);
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
- messageApi.error('登录过期');
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
- messageApi.error(err.message);
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
- export function getMessageApi() {
4
- return message;
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); /* NOSONAR: 次要文本颜色,设计如此 */
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); /* NOSONAR: 悬停态主文本色,对比度设计如此 */
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 {DeviceUtils, HttpClient, PageUtils, GlobalData, getToken, history, EventBus, ARTICLE_HEADER_AVATAR_DROPDOWN, ARTICLE_HEADER_RIGHT} from "../../framework";
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: 12}}/>
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: 12}}/>,
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
- <Sider id='left-sider'
193
- width={180}
194
- collapsible
195
- breakpoint={'md'}
196
- >
197
- <div className='sider-header'>
198
- <button type="button" className='logo-img'
199
- onClick={() => history.push('/')}>
200
- <img src="./logo.png" alt='logo'/>
201
- </button>
202
- <h3 className='hide-on-mobile'>
203
- <Link to="/" style={{color: 'rgba(255,255,255,0.85)'}}>{siteInfo.title}</Link>
204
- </h3>
205
- </div>
206
- {this.renderLeftMenu()}
207
-
208
- </Sider>
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 tabPageRenderNode = <TabLayout pathMenuMap={this.state.pathMenuMap}/>;
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
- {tabPageRenderNode}
278
+ {contentNode}
259
279
  </Watermark>
260
280
  }
261
281
 
262
- return tabPageRenderNode
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
- background-color: #fff;
10
- border-bottom: 1px solid #e8e8e8;
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
- overflow: hidden;
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, var(--primary-color, #1961AC));
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, var(--primary-color, #1961AC));
40
- box-shadow: inset 0 -2px 0 var(--primary-color, var(--primary-color, #1961AC));
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: white;
134
+ color: var(--oa-sider-text, rgba(255, 255, 255, 0.85));
115
135
  }
116
136
  }
117
137