@jiangood/open-admin 3.1.6 → 3.1.8

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 (60) hide show
  1. package/package.json +2 -5
  2. package/src/framework/biz/PermActions.tsx +1 -1
  3. package/src/framework/biz/UserPicker.less +17 -0
  4. package/src/framework/biz/UserPicker.tsx +296 -0
  5. package/src/framework/biz/index.tsx +2 -0
  6. package/src/framework/biz/pickerUser.ts +19 -0
  7. package/src/framework/components/ContextMenu/index.tsx +11 -8
  8. package/src/framework/components/DownloadModal/index.tsx +7 -7
  9. package/src/framework/components/ErrorBoundary/index.tsx +1 -1
  10. package/src/framework/components/FormModal/index.tsx +4 -4
  11. package/src/framework/components/NamedIcon/index.tsx +1 -1
  12. package/src/framework/components/OrgSwitcher/index.tsx +1 -1
  13. package/src/framework/components/Page/index.tsx +18 -4
  14. package/src/framework/components/PageLoading/index.tsx +2 -2
  15. package/src/framework/components/ProTable/index.less +70 -8
  16. package/src/framework/components/ProTable/index.tsx +63 -16
  17. package/src/framework/components/ThemeSettings/index.tsx +173 -0
  18. package/src/framework/components/index.ts +1 -0
  19. package/src/framework/config/index.ts +44 -2
  20. package/src/framework/config/layoutConfig.ts +52 -0
  21. package/src/framework/config/themeConfig.ts +449 -14
  22. package/src/framework/fields/BaseRemoteSelect/index.tsx +4 -4
  23. package/src/framework/fields/FieldDateRange/index.tsx +1 -1
  24. package/src/framework/fields/FieldDictSelect/index.tsx +1 -1
  25. package/src/framework/fields/FieldEditor/index.tsx +2 -2
  26. package/src/framework/fields/FieldNumberRange/index.tsx +1 -1
  27. package/src/framework/fields/FieldUploadFile/index.tsx +3 -3
  28. package/src/framework/fields/FieldUploadImage/index.tsx +19 -19
  29. package/src/framework/fields/FieldUserPicker/index.tsx +124 -0
  30. package/src/framework/fields/index.ts +1 -0
  31. package/src/framework/utils/HttpClient.ts +15 -4
  32. package/src/framework/utils/MessageApi.ts +16 -3
  33. package/src/framework/utils/UrlUtils.ts +1 -1
  34. package/src/framework/views/ViewText/index.tsx +1 -1
  35. package/src/layouts/admin/AdminTabs.less +11 -11
  36. package/src/layouts/admin/HeaderRight.jsx +23 -12
  37. package/src/layouts/admin/SinglePageLayout.jsx +36 -0
  38. package/src/layouts/admin/TabLayout.jsx +21 -1
  39. package/src/layouts/admin/index.jsx +46 -26
  40. package/src/layouts/admin/index.less +68 -24
  41. package/src/layouts/index.tsx +29 -4
  42. package/src/pages/article/$code.jsx +2 -2
  43. package/src/pages/article/article.less +13 -13
  44. package/src/pages/index.jsx +1 -1
  45. package/src/pages/public/login.jsx +29 -15
  46. package/src/pages/public/login.less +243 -22
  47. package/src/pages/standalone/forceUpdatePwd.jsx +8 -7
  48. package/src/pages/system/article/index.jsx +13 -15
  49. package/src/pages/system/codegen/index.jsx +54 -51
  50. package/src/pages/system/dict/index.jsx +2 -2
  51. package/src/pages/system/file/index.jsx +3 -3
  52. package/src/pages/system/job/index.jsx +57 -20
  53. package/src/pages/system/log/index.jsx +2 -2
  54. package/src/pages/system/org/index.jsx +5 -5
  55. package/src/pages/system/role/index.jsx +28 -55
  56. package/src/pages/system/role/rolePerm.jsx +1 -1
  57. package/src/pages/system/user/index.jsx +7 -11
  58. package/src/pages/system/user/userPerm.jsx +5 -5
  59. package/src/pages/userCenter/permView.jsx +2 -2
  60. package/src/style/global.less +6 -6
@@ -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);
@@ -97,7 +112,7 @@ export default class AdminLayout extends React.Component {
97
112
  this.setState({menuLoading: false})
98
113
  })
99
114
  }
100
- actionRef = React.createRef() // NOSONAR: React ref 由父组件通过 ref 访问
115
+ actionRef = React.createRef()
101
116
 
102
117
  classifyMenus = (menuTree) => {
103
118
  const topMenus = [];
@@ -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
  }
@@ -75,7 +95,11 @@
75
95
  display: flex;
76
96
  align-items: center;
77
97
  gap: 8px;
78
- padding: 12px 16px;
98
+ /* 与顶栏同高,保证 logo 基线与一级菜单对齐 */
99
+ height: 48px;
100
+ padding: 0 16px;
101
+ /* 底边线:与右侧顶栏/标签栏分隔线连成一线;色值由侧栏文字变量派生,避免硬编码 */
102
+ border-bottom: 1px solid color-mix(in srgb, var(--oa-sider-text, rgba(255, 255, 255, 0.85)) 14%, transparent);
79
103
 
80
104
  .logo-img {
81
105
  height: 36px;
@@ -95,10 +119,25 @@
95
119
  margin: 0;
96
120
  font-size: 14px;
97
121
  line-height: 1;
122
+ /* 允许在 flex 中收缩,长站点名省略号截断而不溢出侧栏 */
123
+ min-width: 0;
124
+ overflow: hidden;
98
125
  white-space: nowrap;
126
+
127
+ a {
128
+ display: block;
129
+ overflow: hidden;
130
+ text-overflow: ellipsis;
131
+ }
99
132
  }
100
133
  }
101
134
 
135
+ /* 折叠态:logo 居中,去掉左侧内边距 */
136
+ &.ant-layout-sider-collapsed .sider-header {
137
+ justify-content: center;
138
+ padding-inline: 0;
139
+ }
140
+
102
141
  &.ant-layout-sider-collapsed .sider-header h3 {
103
142
  display: none;
104
143
  }
@@ -109,9 +148,14 @@
109
148
  scrollbar-width: thin;
110
149
  scrollbar-color: unset;
111
150
 
151
+ /* 菜单分隔线:加大上下间距,让不同菜单分组更清晰 */
152
+ .ant-menu-item-divider {
153
+ margin-block: 8px;
154
+ }
155
+
112
156
  .ant-menu-submenu-selected {
113
157
  &>div {
114
- color: white;
158
+ color: var(--oa-sider-text, rgba(255, 255, 255, 0.85));
115
159
  }
116
160
  }
117
161
 
@@ -1,9 +1,9 @@
1
1
  import React from "react";
2
- import {ConfigProvider, Modal} from "antd";
2
+ import {App, ConfigProvider, Modal} from "antd";
3
3
  import zhCN from 'antd/locale/zh_CN';
4
4
  import dayjs from 'dayjs';
5
5
  import 'dayjs/locale/zh-cn';
6
- import {ErrorBoundary, GlobalData, HttpClient, PageFrame, PageLoading, PageUtils, history, getThemeConfig, setThemeColors, EventBus} from "../framework";
6
+ import {ErrorBoundary, GlobalData, HttpClient, PageFrame, PageLoading, PageUtils, history, getThemeConfig, getThemeMode, setThemeColors, setMessageApi, EventBus} from "../framework";
7
7
  import type {ThemeColors} from "../framework";
8
8
 
9
9
  import AdminLayout from "./admin"
@@ -25,6 +25,16 @@ export interface HeaderExtraContext {
25
25
  loginInfo: { id: string; name: string; account: string };
26
26
  }
27
27
 
28
+ /** 将 App 上下文的 message 实例注入框架,使提示跟随 ConfigProvider 主题(含暗色) */
29
+ function MessageBridge() {
30
+ const {message} = App.useApp();
31
+ React.useEffect(() => {
32
+ setMessageApi(message);
33
+ return () => setMessageApi();
34
+ }, [message]);
35
+ return null;
36
+ }
37
+
28
38
  interface LayoutsProps {
29
39
  headerExtra?: (context: HeaderExtraContext) => React.ReactNode;
30
40
  showOrgSwitcher?: (context: HeaderExtraContext) => boolean;
@@ -38,11 +48,14 @@ export class Layouts extends React.Component<LayoutsProps> {
38
48
  siteInfoLoaded: false,
39
49
  loginChecked: false,
40
50
  loginExpiredVisible: false,
51
+ themeMode: getThemeMode(),
52
+ themeVersion: 0,
41
53
  };
42
54
 
43
55
  unlisten: (() => void) | null = null;
44
56
  unsubscribeLoginExpired: (() => void) | null = null;
45
57
  unsubscribeLogoutSuccess: (() => void) | null = null;
58
+ unsubscribeTheme: (() => void) | null = null;
46
59
 
47
60
  constructor(props: LayoutsProps) {
48
61
  super(props);
@@ -111,6 +124,12 @@ export class Layouts extends React.Component<LayoutsProps> {
111
124
  this.unsubscribeLogoutSuccess = EventBus.on('logoutSuccess', () => {
112
125
  this.setState({loginChecked: false});
113
126
  });
127
+ this.unsubscribeTheme = EventBus.on('themeChange', () => {
128
+ this.setState(prevState => ({
129
+ themeMode: getThemeMode(),
130
+ themeVersion: prevState.themeVersion + 1,
131
+ }));
132
+ });
114
133
  this.loadData();
115
134
  }
116
135
 
@@ -130,6 +149,9 @@ export class Layouts extends React.Component<LayoutsProps> {
130
149
  if (this.unsubscribeLogoutSuccess) {
131
150
  this.unsubscribeLogoutSuccess();
132
151
  }
152
+ if (this.unsubscribeTheme) {
153
+ this.unsubscribeTheme();
154
+ }
133
155
  }
134
156
 
135
157
  render() {
@@ -142,8 +164,11 @@ export class Layouts extends React.Component<LayoutsProps> {
142
164
  return (
143
165
  <ErrorBoundary minimal>
144
166
  <ConfigProvider {...baseConfigProps} theme={getThemeConfig()}>
145
- {this.renderContent(showPageFrame, ready, pathname, search)}
146
- {this.renderLoginExpiredModal()}
167
+ <App component={false}>
168
+ <MessageBridge/>
169
+ {this.renderContent(showPageFrame, ready, pathname, search)}
170
+ {this.renderLoginExpiredModal()}
171
+ </App>
147
172
  </ConfigProvider>
148
173
  </ErrorBoundary>
149
174
  );
@@ -26,13 +26,13 @@ export default class ArticlePage extends React.Component {
26
26
  const {article, loading} = this.state
27
27
 
28
28
  if (loading) {
29
- return <Page title="加载中...">
29
+ return <Page>
30
30
  <div>加载中...</div>
31
31
  </Page>
32
32
  }
33
33
 
34
34
  if (!article) {
35
- return <Page title="文章不存在">
35
+ return <Page>
36
36
  <div>文章不存在或已禁用</div>
37
37
  </Page>
38
38
  }
@@ -1,19 +1,19 @@
1
1
  .oa-article {
2
2
  max-width: 800px;
3
3
  margin: 0 auto;
4
- background: #fff;
4
+ background: var(--oa-color-bg-container, #fff);
5
5
  padding: 24px 32px;
6
6
  border-radius: 4px;
7
7
  font-size: 15px;
8
8
  line-height: 1.8;
9
- color: rgba(0, 0, 0, 0.88);
9
+ color: var(--oa-color-text, rgba(0, 0, 0, 0.88));
10
10
  word-wrap: break-word;
11
11
 
12
12
  .oa-article-title {
13
13
  text-align: center;
14
14
  padding-bottom: 16px;
15
15
  margin-bottom: 16px;
16
- border-bottom: 1px solid #eee;
16
+ border-bottom: 1px solid var(--oa-color-border, #eee);
17
17
 
18
18
  h1 {
19
19
  font-size: 26px;
@@ -23,7 +23,7 @@
23
23
  }
24
24
 
25
25
  .oa-article-meta {
26
- color: #999;
26
+ color: var(--oa-color-text-tertiary, #999);
27
27
  font-size: 13px;
28
28
  display: flex;
29
29
  justify-content: center;
@@ -64,19 +64,19 @@
64
64
  }
65
65
 
66
66
  a {
67
- color: #1677ff;
67
+ color: var(--primary-color, #1677ff);
68
68
  }
69
69
 
70
70
  blockquote {
71
71
  margin: 0 0 16px;
72
72
  padding: 8px 16px;
73
- border-left: 4px solid #e8e8e8;
74
- background: #fafafa;
75
- color: #666;
73
+ border-left: 4px solid var(--oa-color-border, #e8e8e8);
74
+ background: var(--oa-color-fill, #fafafa);
75
+ color: var(--oa-color-text-secondary, #666);
76
76
  }
77
77
 
78
78
  pre {
79
- background: #f6f8fa;
79
+ background: var(--oa-color-fill, #f6f8fa);
80
80
  padding: 12px 16px;
81
81
  border-radius: 4px;
82
82
  overflow-x: auto;
@@ -84,7 +84,7 @@
84
84
  }
85
85
 
86
86
  code {
87
- background: #f6f8fa;
87
+ background: var(--oa-color-fill, #f6f8fa);
88
88
  padding: 2px 4px;
89
89
  border-radius: 3px;
90
90
  font-size: 90%;
@@ -102,19 +102,19 @@
102
102
  }
103
103
 
104
104
  th, td {
105
- border: 1px solid #e0e0e0;
105
+ border: 1px solid var(--oa-color-border, #e0e0e0);
106
106
  padding: 8px 12px;
107
107
  text-align: left;
108
108
  }
109
109
 
110
110
  th {
111
- background: #f5f5f5;
111
+ background: var(--oa-color-fill, #f5f5f5);
112
112
  font-weight: 600;
113
113
  }
114
114
 
115
115
  hr {
116
116
  border: none;
117
- border-top: 1px solid #eee;
117
+ border-top: 1px solid var(--oa-color-border, #eee);
118
118
  margin: 24px 0;
119
119
  }
120
120
 
@@ -4,7 +4,7 @@ import {Page} from "../framework";
4
4
 
5
5
  export default class HomePage extends React.Component {
6
6
  render() {
7
- return <Page title="首页" description="欢迎使用本系统">
7
+ return <Page>
8
8
  <Card>欢迎使用本系统</Card>
9
9
  </Page>
10
10
  }
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
- import {Button, Form, Input, message} from 'antd';
3
- import {LockOutlined, UserOutlined, WarningOutlined} from '@ant-design/icons';
4
- import {EventBus, HttpClient, GlobalData, history} from "../../framework";
2
+ import {Button, Form, Input} from 'antd';
3
+ import {LockOutlined, SafetyCertificateOutlined, UserOutlined, WarningOutlined} from '@ant-design/icons';
4
+ import {EventBus, HttpClient, GlobalData, getMessageApi, history} from "../../framework";
5
5
 
6
6
  import "./login.less"
7
7
 
@@ -20,7 +20,7 @@ function postLogin(values, query, success, error) {
20
20
  success?.(rs)
21
21
  }).catch(e => {
22
22
  console.error('[Login] 登录失败:', e);
23
- message.error(HttpClient.errToMsg(e))
23
+ getMessageApi().error(HttpClient.errToMsg(e))
24
24
  error?.(e)
25
25
  })
26
26
  }
@@ -36,7 +36,9 @@ export default class LoginPage extends React.Component {
36
36
 
37
37
  state = {
38
38
  logging: false,
39
- siteInfo: {}
39
+ siteInfo: {},
40
+ /* logo.png 缺失或加载失败时,回退到矢量标记,避免首屏出现破损图 */
41
+ logoError: false
40
42
  }
41
43
 
42
44
  componentDidMount() {
@@ -51,7 +53,7 @@ export default class LoginPage extends React.Component {
51
53
  this.setState({siteInfo: rs.data})
52
54
  }).catch(() => {
53
55
  console.error('[Login] 加载站点信息失败');
54
- message.error('加载站点信息失败,请刷新页面重试')
56
+ getMessageApi().error('加载站点信息失败,请刷新页面重试')
55
57
  })
56
58
  }
57
59
 
@@ -62,7 +64,7 @@ export default class LoginPage extends React.Component {
62
64
  } catch (e) {
63
65
  console.error('[Login] 密码编码失败:', e);
64
66
  this.setState({logging: false})
65
- message.error('密码含不支持字符,请联系管理员重置')
67
+ getMessageApi().error('密码含不支持字符,请联系管理员重置')
66
68
  return
67
69
  }
68
70
  postLogin(values, this.props.location?.query, () => {
@@ -73,8 +75,6 @@ export default class LoginPage extends React.Component {
73
75
  }
74
76
 
75
77
  render() {
76
- const {siteInfo} = this.state
77
-
78
78
  const pageStyle = {
79
79
  backgroundImage: `url("./login_bg.jpg")`
80
80
  }
@@ -82,7 +82,7 @@ export default class LoginPage extends React.Component {
82
82
  return (
83
83
  <section className='login-page' style={pageStyle}>
84
84
  <div className="login-content">
85
- <h1>{siteInfo.title}</h1>
85
+ {this.renderBrand()}
86
86
  {this.getForm()}
87
87
  {this.renderFormBottom()}
88
88
  </div>
@@ -90,6 +90,20 @@ export default class LoginPage extends React.Component {
90
90
  );
91
91
  }
92
92
 
93
+ renderBrand() {
94
+ const {siteInfo = {}, logoError} = this.state
95
+ return (
96
+ <div className="login-brand">
97
+ <span className="login-brand-logo">
98
+ {logoError
99
+ ? <SafetyCertificateOutlined/>
100
+ : <img src="./logo.png" alt="" onError={() => this.setState({logoError: true})}/>}
101
+ </span>
102
+ {siteInfo.title && <h1>{siteInfo.title}</h1>}
103
+ </div>
104
+ )
105
+ }
106
+
93
107
  getForm = () => {
94
108
  const form = (
95
109
  <Form
@@ -101,15 +115,15 @@ export default class LoginPage extends React.Component {
101
115
  colon={false}
102
116
  >
103
117
  <Form.Item name="username" rules={[{required: true, message: '请输入用户名!'}]}>
104
- <Input size='large' prefix={<UserOutlined/>} placeholder="用户名" autoComplete="off"/>
118
+ <Input size='large' prefix={<UserOutlined/>} placeholder="用户名" autoComplete="username"/>
105
119
  </Form.Item>
106
120
  <Form.Item name="password" rules={[{required: true, message: '请输入密码!'}]}>
107
- <Input autoComplete="off" prefix={<LockOutlined/>} type="password" placeholder="密码"
121
+ <Input.Password autoComplete="current-password" prefix={<LockOutlined/>} placeholder="密码"
108
122
  size='large'
109
123
  />
110
124
  </Form.Item>
111
125
 
112
- <Form.Item style={{marginTop: 10}}>
126
+ <Form.Item className="login-submit">
113
127
  <Button loading={this.state.logging} type="primary" htmlType="submit"
114
128
  block size='large'>
115
129
  登录
@@ -125,10 +139,10 @@ export default class LoginPage extends React.Component {
125
139
  };
126
140
 
127
141
  renderFormBottom() {
128
- const siteInfo = this.state.siteInfo;
142
+ const siteInfo = this.state.siteInfo || {};
129
143
  if (siteInfo.loginBoxBottomTip) {
130
144
  return (
131
- <div style={{color: 'white', marginTop: 50, fontSize: '14px', textAlign: 'center'}}>
145
+ <div className="login-bottom-tip">
132
146
  <WarningOutlined/> {siteInfo.loginBoxBottomTip}
133
147
  </div>
134
148
  )