@jiangood/open-admin 3.1.11 → 3.1.12

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 (56) hide show
  1. package/package.json +3 -3
  2. package/src/framework/GlobalData.ts +3 -1
  3. package/src/framework/biz/OrgTree.tsx +1 -1
  4. package/src/framework/biz/RoleTree.tsx +1 -1
  5. package/src/framework/biz/UserPicker.tsx +4 -4
  6. package/src/framework/biz/index.tsx +4 -4
  7. package/src/framework/components/LinkButton/index.tsx +4 -0
  8. package/src/framework/components/OrgSwitcher/index.tsx +2 -2
  9. package/src/framework/components/Page/index.tsx +6 -14
  10. package/src/framework/components/ProTable/index.tsx +3 -1
  11. package/src/framework/components/ThemeSettings/index.tsx +5 -38
  12. package/src/framework/config/index.ts +12 -0
  13. package/src/framework/config/layoutConfig.ts +47 -8
  14. package/src/framework/config/themeConfig.ts +93 -25
  15. package/src/framework/config/uiDefaults.ts +26 -0
  16. package/src/framework/fields/FieldEditor/index.tsx +1 -1
  17. package/src/framework/fields/FieldUploadFile/index.tsx +1 -1
  18. package/src/framework/fields/FieldUploadImage/index.tsx +42 -10
  19. package/src/framework/fields/FieldUserPicker/index.tsx +3 -3
  20. package/src/framework/fields/orgTree.ts +1 -1
  21. package/src/framework/router/index.ts +1 -0
  22. package/src/framework/router/matcher.ts +19 -4
  23. package/src/framework/utils/AvatarUtils.ts +64 -0
  24. package/src/framework/utils/HttpClient.ts +5 -5
  25. package/src/framework/utils/PageUtils.ts +38 -7
  26. package/src/framework/utils/TitleUtils.ts +38 -0
  27. package/src/framework/utils/UrlUtils.ts +3 -3
  28. package/src/framework/utils/index.ts +2 -0
  29. package/src/layouts/admin/HeaderRight.jsx +62 -16
  30. package/src/layouts/admin/SinglePageLayout.jsx +13 -2
  31. package/src/layouts/admin/TabLayout.jsx +38 -19
  32. package/src/layouts/admin/index.jsx +4 -4
  33. package/src/layouts/index.tsx +52 -15
  34. package/src/pages/404.jsx +4 -0
  35. package/src/pages/article/$code.jsx +5 -1
  36. package/src/pages/login.jsx +157 -0
  37. package/src/pages/{public/login.less → login.less} +16 -1
  38. package/src/pages/public/login.jsx +11 -145
  39. package/src/pages/standalone/{forceUpdatePwd.jsx → force-update-pwd.jsx} +10 -4
  40. package/src/pages/system/article/index.jsx +1 -1
  41. package/src/pages/system/config/index.jsx +46 -20
  42. package/src/pages/system/file/index.jsx +3 -3
  43. package/src/pages/system/job/index.jsx +1 -1
  44. package/src/pages/system/log/index.jsx +1 -1
  45. package/src/pages/system/online/index.jsx +95 -0
  46. package/src/pages/system/org/index.jsx +7 -7
  47. package/src/pages/system/role/index.jsx +8 -8
  48. package/src/pages/system/role/{rolePerm.jsx → role-perm.jsx} +9 -4
  49. package/src/pages/system/user/{userPerm.jsx → UserPermModal.jsx} +3 -3
  50. package/src/pages/system/user/index.jsx +6 -6
  51. package/src/pages/user-center/AvatarPicker.jsx +76 -0
  52. package/src/pages/{userCenter/permView.jsx → user-center/PermView.jsx} +48 -25
  53. package/src/pages/user-center/RecentOps.jsx +36 -0
  54. package/src/pages/user-center/index.jsx +393 -0
  55. package/vite-plugin/index.js +64 -6
  56. package/src/pages/userCenter/index.jsx +0 -93
@@ -1,24 +1,39 @@
1
1
  import React from "react";
2
- import {history, PageFrame, UrlUtils, EventBus} from "../../framework";
2
+ import {history, PageFrame, EventBus, matchRoute, setPageTitle} from "../../framework";
3
3
  import {AdminTabs} from "./AdminTabs";
4
4
 
5
- function getLabel(pathname, search, pathMenuMap) {
6
- if (pathname === '/') return '首页';
7
- const params = UrlUtils.getParams(search);
8
- if (params._label) return params._label;
5
+ function getPathname(url) {
6
+ const qIndex = url.indexOf('?');
7
+ return qIndex === -1 ? url : url.substring(0, qIndex);
8
+ }
9
+
10
+ /** 移除已弃用的 _label 参数,避免旧 URL 产生重复 Tab;其余 query 原样保留 */
11
+ function normalizeUrl(url) {
12
+ const qIndex = url.indexOf('?');
13
+ if (qIndex === -1) return url;
14
+ const params = new URLSearchParams(url.substring(qIndex));
15
+ if (!params.has('_label')) return url;
16
+ params.delete('_label');
17
+ const search = params.toString();
18
+ return search ? `${url.substring(0, qIndex)}?${search}` : url.substring(0, qIndex);
19
+ }
20
+
21
+ function getLabel(pathname, pathMenuMap) {
22
+ if (pathname === '/' || pathname === '/index') return '首页';
23
+ const route = matchRoute(pathname);
24
+ // 页面显式声明的标题优先;未声明时回退到后端菜单名
25
+ if (route?.meta?.title) return route.meta.title;
9
26
  const menu = pathMenuMap[pathname];
10
27
  if (menu) return menu.name;
28
+ if (!route) return '页面不存在';
11
29
  return '未命名';
12
30
  }
13
31
 
14
32
  export class TabLayout extends React.Component {
15
33
  constructor(props) {
16
34
  super(props);
17
- const initUrl = history.location.pathname + history.location.search;
18
- const qIndex = initUrl.indexOf('?');
19
- const initPathname = qIndex === -1 ? initUrl : initUrl.substring(0, qIndex);
20
- const initSearch = qIndex === -1 ? '' : initUrl.substring(qIndex);
21
- const initLabel = getLabel(initPathname, initSearch, props.pathMenuMap);
35
+ const initUrl = normalizeUrl(history.location.pathname + history.location.search);
36
+ const initLabel = getLabel(getPathname(initUrl), props.pathMenuMap);
22
37
  this.state = {
23
38
  tabs: [{key: initUrl, label: initLabel, refreshKey: 0}],
24
39
  activeKey: initUrl,
@@ -29,13 +44,11 @@ export class TabLayout extends React.Component {
29
44
  unlisten = null;
30
45
  unsubscribeClosePage = null;
31
46
 
32
- openTab = (url) => {
33
- const qIndex = url.indexOf('?');
34
- const pathname = qIndex === -1 ? url : url.substring(0, qIndex);
35
- const search = qIndex === -1 ? '' : url.substring(qIndex);
47
+ openTab = (rawUrl) => {
48
+ const url = normalizeUrl(rawUrl);
36
49
  this.setState(prev => ({
37
50
  tabs: prev.tabs.some(t => t.key === url) ? prev.tabs
38
- : [...prev.tabs, {key: url, label: getLabel(pathname, search, this.props.pathMenuMap), refreshKey: 0}],
51
+ : [...prev.tabs, {key: url, label: getLabel(getPathname(url), this.props.pathMenuMap), refreshKey: 0}],
39
52
  activeKey: url,
40
53
  }));
41
54
  };
@@ -64,6 +77,13 @@ export class TabLayout extends React.Component {
64
77
  this.closeTab(url);
65
78
  };
66
79
 
80
+ /** 浏览器标题跟随当前激活标签 */
81
+ syncDocumentTitle() {
82
+ const {tabs, activeKey} = this.state;
83
+ const active = tabs.find(t => t.key === activeKey);
84
+ setPageTitle(active && active.label !== '未命名' ? active.label : undefined);
85
+ }
86
+
67
87
  handleCloseOthers = (key) => {
68
88
  this.setState(prev => ({tabs: prev.tabs.filter(t => t.key === key)}));
69
89
  if (key !== this.activeKey) {
@@ -81,6 +101,7 @@ export class TabLayout extends React.Component {
81
101
  this.openTab(location.pathname + location.search);
82
102
  });
83
103
  this.unsubscribeClosePage = EventBus.on('closePage', this.handleClosePageEvent);
104
+ this.syncDocumentTitle();
84
105
  }
85
106
 
86
107
  componentDidUpdate(prevProps) {
@@ -89,16 +110,14 @@ export class TabLayout extends React.Component {
89
110
  if (this.props.pathMenuMap !== prevProps.pathMenuMap) {
90
111
  this.refreshTabLabels();
91
112
  }
113
+ this.syncDocumentTitle();
92
114
  }
93
115
 
94
116
  refreshTabLabels() {
95
117
  this.setState(prev => {
96
118
  let changed = false;
97
119
  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);
120
+ const label = getLabel(getPathname(t.key), this.props.pathMenuMap);
102
121
  if (label === t.label) return t;
103
122
  changed = true;
104
123
  return {...t, label};
@@ -56,7 +56,7 @@ export default class AdminLayout extends React.Component {
56
56
  this.setState({layoutMode: getLayoutMode()});
57
57
  })
58
58
 
59
- // 系统配置保存后刷新标题、水印等站点信息
59
+ // 参数设置保存后刷新标题、水印等站点信息
60
60
  this.unsubscribeSiteInfo = EventBus.on('siteInfoChange', () => {
61
61
  this.setState({siteInfo: GlobalData.getSiteInfo()});
62
62
  })
@@ -180,7 +180,7 @@ export default class AdminLayout extends React.Component {
180
180
  if (!menu) return;
181
181
  const {path} = menu;
182
182
  this.setState({currentMenuKey: key});
183
- PageUtils.open(path, menu.name);
183
+ PageUtils.open(path);
184
184
  }
185
185
 
186
186
  getActiveTopMenu() {
@@ -242,7 +242,7 @@ return <Layout className='main-layout'>
242
242
  {this.renderTopMenu()}
243
243
  {this.state.headerLeftArticles.map(a => (
244
244
  <button key={a.code} type="button" className='item'
245
- onClick={() => PageUtils.open('/article/' + a.code, a.title)}>{a.title}</button>
245
+ onClick={() => PageUtils.open('/article/' + a.code)}>{a.title}</button>
246
246
  ))}
247
247
  {this.props.showOrgSwitcher?.({
248
248
  activeTopMenu: this.getActiveTopMenu(),
@@ -283,7 +283,7 @@ return <Layout className='main-layout'>
283
283
  const {siteInfo, loginInfo, layoutMode} = this.state
284
284
  // 即使菜单为空也直接显示页面内容,不要一直显示 Skeleton
285
285
  const contentNode = layoutMode === 'single'
286
- ? <SinglePageLayout/>
286
+ ? <SinglePageLayout pathMenuMap={this.state.pathMenuMap}/>
287
287
  : <TabLayout pathMenuMap={this.state.pathMenuMap}/>;
288
288
  if (siteInfo.waterMark === true) {
289
289
  return <Watermark content={[loginInfo.name, loginInfo.account]}>
@@ -3,8 +3,8 @@ 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, LOGIN_EXPIRED_REASON_CONCURRENT_LOGIN, PageFrame, PageLoading, PageUtils, history, getThemeConfig, getThemeMode, setThemeColors, setMessageApi, EventBus} from "../framework";
7
- import type {LoginExpiredPayload, ThemeColors} from "../framework";
6
+ import {ErrorBoundary, GlobalData, HttpClient, LOGIN_EXPIRED_REASON_CONCURRENT_LOGIN, LOGIN_PATH, PageFrame, PageLoading, PageUtils, history, getThemeConfig, getThemeMode, setThemeColors, setMessageApi, EventBus, matchRoute, refreshDocumentTitle, setPageTitle, warnDeprecatedPageLabel, applyServerUiDefaults} from "../framework";
7
+ import type {LoginExpiredPayload, PageLayout, ThemeColors} from "../framework";
8
8
 
9
9
  import AdminLayout from "./admin"
10
10
 
@@ -59,6 +59,7 @@ export class Layouts extends React.Component<LayoutsProps> {
59
59
  unsubscribeLoginExpired: (() => void) | null = null;
60
60
  unsubscribeLogoutSuccess: (() => void) | null = null;
61
61
  unsubscribeTheme: (() => void) | null = null;
62
+ unsubscribeSiteInfo: (() => void) | null = null;
62
63
  /** 本次登录失效弹窗的原因。同步字段(非 state):同一次失效会并发上报多个 401,setState 异步可能读到旧值 */
63
64
  private loginExpiredReason: string = '';
64
65
 
@@ -94,22 +95,42 @@ export class Layouts extends React.Component<LayoutsProps> {
94
95
  this.setState({loginExpiredVisible: false});
95
96
  };
96
97
 
97
- getPageType(pathname): 'public' | 'standalone' | 'protected' {
98
- if (pathname === '/' || pathname === '/index') return 'protected';
99
- if (pathname.startsWith('/public/')) return 'public';
100
- if (pathname.startsWith('/standalone/')) return 'standalone';
101
- return 'protected';
98
+ /**
99
+ * 解析页面登录与布局要求:
100
+ * 优先使用页面文件导出的 pageMeta;未匹配的路由按目录约定兜底
101
+ */
102
+ getPageMeta(pathname: string): {requiresAuth: boolean; layout: PageLayout} {
103
+ const meta = matchRoute(pathname)?.meta;
104
+ const isLogin = pathname === LOGIN_PATH;
105
+ const inPublicDir = pathname.startsWith('/public/');
106
+ const inStandaloneDir = pathname.startsWith('/standalone/');
107
+ const requiresAuth = meta?.requiresAuth ?? (isLogin ? false : !inPublicDir);
108
+ const layout = requiresAuth
109
+ ? (meta?.layout ?? (inStandaloneDir || isLogin ? 'blank' : 'admin'))
110
+ : 'blank';
111
+ return {requiresAuth, layout};
112
+ }
113
+
114
+ /** 公共/standalone 页用 pageMeta.title;admin 页由 TabLayout / SinglePageLayout 设置 */
115
+ syncDocumentTitle() {
116
+ const {pathname, search} = this.state.location;
117
+ if (search && new URLSearchParams(search).has('_label')) {
118
+ warnDeprecatedPageLabel();
119
+ }
120
+ if (this.getPageMeta(pathname).layout === 'blank') {
121
+ setPageTitle(matchRoute(pathname)?.meta?.title);
122
+ }
102
123
  }
103
124
 
104
125
  loadData() {
105
126
  const {siteInfoLoaded, loginChecked} = this.state;
106
- const pageType = this.getPageType(this.state.location.pathname);
127
+ const {requiresAuth} = this.getPageMeta(this.state.location.pathname);
107
128
 
108
129
  if (!siteInfoLoaded) {
109
130
  this.loadSiteInfo();
110
131
  }
111
132
 
112
- if ((pageType === 'protected' || pageType === 'standalone') && !loginChecked) {
133
+ if (requiresAuth && !loginChecked) {
113
134
  this.loadLoginInfo();
114
135
  }
115
136
  }
@@ -117,6 +138,8 @@ export class Layouts extends React.Component<LayoutsProps> {
117
138
  loadSiteInfo() {
118
139
  HttpClient.get("/admin/public/site-info", null, {toastError: false}).then(data => {
119
140
  GlobalData.setSiteInfo(data.data);
141
+ // 参数设置的默认主题/页签模式需在后台布局首次渲染前生效,避免闪一下默认样式
142
+ applyServerUiDefaults(data.data?.config);
120
143
  this.setState({siteInfoLoaded: true});
121
144
  }).catch(() => {
122
145
  console.error('[Layout] 加载站点信息失败');
@@ -131,7 +154,7 @@ export class Layouts extends React.Component<LayoutsProps> {
131
154
 
132
155
  if (data.data.needUpdatePwd) {
133
156
  this.setState({loginChecked: true});
134
- history.push('/standalone/forceUpdatePwd');
157
+ history.push('/standalone/force-update-pwd');
135
158
  return;
136
159
  }
137
160
 
@@ -154,12 +177,22 @@ export class Layouts extends React.Component<LayoutsProps> {
154
177
  themeVersion: prevState.themeVersion + 1,
155
178
  }));
156
179
  });
180
+ this.unsubscribeSiteInfo = EventBus.on('siteInfoChange', () => {
181
+ // 参数设置保存后,未自定义用户即时跟随新的默认主题/页签模式
182
+ applyServerUiDefaults(GlobalData.getSiteInfo().config);
183
+ refreshDocumentTitle();
184
+ });
185
+ this.syncDocumentTitle();
157
186
  this.loadData();
158
187
  }
159
188
 
160
189
  componentDidUpdate(prevProps: LayoutsProps, prevState: typeof this.state) {
161
190
  if (this.state.location !== prevState.location) {
162
191
  this.loadData();
192
+ this.syncDocumentTitle();
193
+ }
194
+ if (!prevState.siteInfoLoaded && this.state.siteInfoLoaded) {
195
+ refreshDocumentTitle();
163
196
  }
164
197
  }
165
198
 
@@ -176,6 +209,9 @@ export class Layouts extends React.Component<LayoutsProps> {
176
209
  if (this.unsubscribeTheme) {
177
210
  this.unsubscribeTheme();
178
211
  }
212
+ if (this.unsubscribeSiteInfo) {
213
+ this.unsubscribeSiteInfo();
214
+ }
179
215
  }
180
216
 
181
217
  /**
@@ -194,15 +230,16 @@ export class Layouts extends React.Component<LayoutsProps> {
194
230
  const {location, siteInfoLoaded, loginChecked} = this.state;
195
231
  const {pathname, search} = location;
196
232
  const ready = siteInfoLoaded && loginChecked;
197
- const pageType = this.getPageType(pathname);
198
- const showPageFrame = pageType === 'public' || (pageType === 'standalone' && ready);
233
+ const {requiresAuth, layout} = this.getPageMeta(pathname);
234
+ // 公共页直接渲染;standalone 页需登录后渲染;admin 页交给后台布局
235
+ const showPageFrame = !requiresAuth || (layout === 'blank' && ready);
199
236
 
200
237
  return (
201
238
  <ErrorBoundary minimal>
202
239
  <ConfigProvider {...baseConfigProps} theme={getThemeConfig()}>
203
240
  <App component={false}>
204
241
  <MessageBridge/>
205
- {this.renderContent(showPageFrame, ready, pathname, search)}
242
+ {this.renderContent(showPageFrame, ready, layout, pathname, search)}
206
243
  {this.renderLoginExpiredModal()}
207
244
  </App>
208
245
  </ConfigProvider>
@@ -210,11 +247,11 @@ export class Layouts extends React.Component<LayoutsProps> {
210
247
  );
211
248
  }
212
249
 
213
- renderContent(showPageFrame: boolean, ready: boolean, pathname: string, search: string) {
250
+ renderContent(showPageFrame: boolean, ready: boolean, layout: PageLayout, pathname: string, search: string) {
214
251
  if (showPageFrame) {
215
252
  return <PageFrame url={pathname + search}/>;
216
253
  }
217
- if (ready) {
254
+ if (ready && layout === 'admin') {
218
255
  return <AdminLayout headerExtra={this.props.headerExtra} showOrgSwitcher={this.props.showOrgSwitcher} loginInfo={GlobalData.getLoginInfo()}/>;
219
256
  }
220
257
  return <PageLoading messages={[
package/src/pages/404.jsx CHANGED
@@ -2,6 +2,10 @@ import React from "react";
2
2
  import {Result} from "antd";
3
3
  import {Page} from "../framework";
4
4
 
5
+ export const pageMeta = {
6
+ title: '页面不存在',
7
+ };
8
+
5
9
  export default class NotFound extends React.Component {
6
10
 
7
11
  render() {
@@ -2,6 +2,10 @@ import React from "react";
2
2
  import {HttpClient, Page, UrlUtils} from "../../framework";
3
3
  import "./article.less";
4
4
 
5
+ export const pageMeta = {
6
+ title: '文章详情',
7
+ };
8
+
5
9
  export default class ArticlePage extends React.Component {
6
10
 
7
11
  state = {
@@ -12,7 +16,7 @@ export default class ArticlePage extends React.Component {
12
16
  componentDidMount() {
13
17
  const code = this.props.params?.code
14
18
  if (code) {
15
- HttpClient.get('admin/article/getByCode', {code}, {toastError: false}).then(rs => {
19
+ HttpClient.get('admin/article/get-by-code', {code}, {toastError: false}).then(rs => {
16
20
  this.setState({article: rs.data, loading: false})
17
21
  }).catch(() => {
18
22
  this.setState({loading: false})
@@ -0,0 +1,157 @@
1
+ import React from 'react';
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
+
6
+ import "./login.less"
7
+
8
+ export const pageMeta = {
9
+ requiresAuth: false,
10
+ layout: 'blank',
11
+ title: '登录',
12
+ };
13
+
14
+ function getRedirect(query) {
15
+ let redirect = query?.redirect;
16
+ if (!redirect) {
17
+ redirect = '/'
18
+ }
19
+ return redirect;
20
+ }
21
+
22
+ function postLogin(values, query, success, error) {
23
+ HttpClient.post('/admin/auth/login', values, null, {toastError: false}).then(rs => {
24
+ EventBus.emit('loginSuccess')
25
+ history.push(getRedirect(query))
26
+ success?.(rs)
27
+ }).catch(e => {
28
+ console.error('[Login] 登录失败:', e);
29
+ getMessageApi().error(HttpClient.errToMsg(e))
30
+ error?.(e)
31
+ })
32
+ }
33
+
34
+ function encodePassword(pwd) {
35
+ const chars = [];
36
+ for (let i = 0; i < pwd.length; i++)
37
+ chars.push(pwd.codePointAt(i) + i + 2);
38
+ return btoa(String.fromCodePoint(...chars));
39
+ }
40
+
41
+ export default class LoginPage extends React.Component {
42
+
43
+ state = {
44
+ logging: false,
45
+ siteInfo: {},
46
+ /* logo.png 缺失或加载失败时,回退到矢量标记,避免首屏出现破损图 */
47
+ logoError: false
48
+ }
49
+
50
+ componentDidMount() {
51
+ const siteInfo = GlobalData.getSiteInfo()
52
+ if (siteInfo?.title) {
53
+ this.setState({siteInfo})
54
+ return
55
+ }
56
+ // localStorage 中无站点信息,从服务端重新加载
57
+ HttpClient.get('/admin/public/site-info', null, {toastError: false}).then(rs => {
58
+ GlobalData.setSiteInfo(rs.data)
59
+ this.setState({siteInfo: rs.data})
60
+ }).catch(() => {
61
+ console.error('[Login] 加载站点信息失败');
62
+ getMessageApi().error('加载站点信息失败,请刷新页面重试')
63
+ })
64
+ }
65
+
66
+ submit = values => {
67
+ this.setState({logging: true})
68
+ try {
69
+ values.password = encodePassword(values.password)
70
+ } catch (e) {
71
+ console.error('[Login] 密码编码失败:', e);
72
+ this.setState({logging: false})
73
+ getMessageApi().error('密码含不支持字符,请联系管理员重置')
74
+ return
75
+ }
76
+ postLogin(values, this.props.location?.query, () => {
77
+ this.setState({logging: false})
78
+ }, () => {
79
+ this.setState({logging: false})
80
+ })
81
+ }
82
+
83
+ render() {
84
+ const pageStyle = {
85
+ backgroundImage: `url("./login_bg.jpg")`
86
+ }
87
+
88
+ return (
89
+ <section className='login-page' style={pageStyle}>
90
+ <div className="login-content">
91
+ {this.renderBrand()}
92
+ {this.getForm()}
93
+ {this.renderFormBottom()}
94
+ </div>
95
+ </section>
96
+ );
97
+ }
98
+
99
+ renderBrand() {
100
+ const {siteInfo = {}, logoError} = this.state
101
+ return (
102
+ <div className="login-brand">
103
+ <span className="login-brand-logo">
104
+ {logoError
105
+ ? <SafetyCertificateOutlined/>
106
+ : <img src="./logo.png" alt="" onError={() => this.setState({logoError: true})}/>}
107
+ </span>
108
+ {siteInfo.title && <h1>{siteInfo.title}</h1>}
109
+ </div>
110
+ )
111
+ }
112
+
113
+ getForm = () => {
114
+ const form = (
115
+ <Form
116
+ name="normal_login"
117
+ className="login-form"
118
+ initialValues={{remember: true}}
119
+ onFinish={this.submit}
120
+ requiredMark={false}
121
+ colon={false}
122
+ >
123
+ <Form.Item name="username" rules={[{required: true, message: '请输入用户名!'}]}>
124
+ <Input size='large' prefix={<UserOutlined/>} placeholder="用户名" autoComplete="username"/>
125
+ </Form.Item>
126
+ <Form.Item name="password" rules={[{required: true, message: '请输入密码!'}]}>
127
+ <Input.Password autoComplete="current-password" prefix={<LockOutlined/>} placeholder="密码"
128
+ size='large'
129
+ />
130
+ </Form.Item>
131
+
132
+ <Form.Item className="login-submit">
133
+ <Button loading={this.state.logging} type="primary" htmlType="submit"
134
+ block size='large'>
135
+ 登录
136
+ </Button>
137
+ </Form.Item>
138
+ </Form>
139
+ );
140
+
141
+ if (this.props.formRender) {
142
+ return this.props.formRender(form);
143
+ }
144
+ return form;
145
+ };
146
+
147
+ renderFormBottom() {
148
+ const siteInfo = this.state.siteInfo || {};
149
+ if (siteInfo.loginBoxBottomTip) {
150
+ return (
151
+ <div className="login-bottom-tip">
152
+ <WarningOutlined/> {siteInfo.loginBoxBottomTip}
153
+ </div>
154
+ )
155
+ }
156
+ }
157
+ }
@@ -128,6 +128,9 @@
128
128
  height: 46px;
129
129
  padding: 0 14px;
130
130
  border-radius: var(--login-field-radius);
131
+ /* antd v6 给内部 input 声明了 `color: inherit`,文字色由容器决定。
132
+ 容器必须显式给浅色,否则会继承 antd 浅色主题的 colorText(黑字压在深色玻璃上不可读) */
133
+ color: var(--login-text);
131
134
  background-color: var(--login-field-bg);
132
135
  border: 1px solid var(--login-hairline);
133
136
  box-shadow: none;
@@ -154,10 +157,22 @@
154
157
  /* 密码可见性切换按钮 */
155
158
  .ant-input-suffix {
156
159
  color: rgba(255, 255, 255, 0.55);
160
+
161
+ /* antd 给密码图标单独着色(colorIcon),不跟随 .ant-input-suffix,需显式覆盖 */
162
+ .ant-input-password-icon {
163
+ color: inherit;
164
+
165
+ &:hover {
166
+ color: var(--login-text);
167
+ }
168
+ }
157
169
  }
158
170
  }
159
171
 
160
- .ant-input {
172
+ /* antd v6 的 `> input.ant-input { color: inherit }` 特异性为 0,2,1,
173
+ 会压过 .login-form .ant-input(0,2,0);这里提升特异性后显式上色,
174
+ 避免 antd 调整内部选择器时文字色再次失效 */
175
+ .login-page & .ant-input {
161
176
  color: var(--login-text);
162
177
  font-size: 15px;
163
178
  background: transparent;
@@ -1,151 +1,17 @@
1
- import React from 'react';
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
-
6
- import "./login.less"
7
-
8
- function getRedirect(query) {
9
- let redirect = query?.redirect;
10
- if (!redirect) {
11
- redirect = '/'
12
- }
13
- return redirect;
14
- }
15
-
16
- function postLogin(values, query, success, error) {
17
- HttpClient.post('/admin/auth/login', values, null, {toastError: false}).then(rs => {
18
- EventBus.emit('loginSuccess')
19
- history.push(getRedirect(query))
20
- success?.(rs)
21
- }).catch(e => {
22
- console.error('[Login] 登录失败:', e);
23
- getMessageApi().error(HttpClient.errToMsg(e))
24
- error?.(e)
25
- })
26
- }
27
-
28
- function encodePassword(pwd) {
29
- const chars = [];
30
- for (let i = 0; i < pwd.length; i++)
31
- chars.push(pwd.codePointAt(i) + i + 2);
32
- return btoa(String.fromCodePoint(...chars));
33
- }
34
-
35
- export default class LoginPage extends React.Component {
36
-
37
- state = {
38
- logging: false,
39
- siteInfo: {},
40
- /* logo.png 缺失或加载失败时,回退到矢量标记,避免首屏出现破损图 */
41
- logoError: false
42
- }
43
-
1
+ import React from "react";
2
+ import {history, LOGIN_PATH} from "../../framework";
3
+
4
+ /**
5
+ * 旧版登录地址兼容:/public/login?redirect=... → /login?redirect=...
6
+ * 保留一个版本后删除
7
+ */
8
+ export default class LegacyLoginRedirect extends React.Component {
44
9
  componentDidMount() {
45
- const siteInfo = GlobalData.getSiteInfo()
46
- if (siteInfo?.title) {
47
- this.setState({siteInfo})
48
- return
49
- }
50
- // localStorage 中无站点信息,从服务端重新加载
51
- HttpClient.get('/admin/public/site-info', null, {toastError: false}).then(rs => {
52
- GlobalData.setSiteInfo(rs.data)
53
- this.setState({siteInfo: rs.data})
54
- }).catch(() => {
55
- console.error('[Login] 加载站点信息失败');
56
- getMessageApi().error('加载站点信息失败,请刷新页面重试')
57
- })
58
- }
59
-
60
- submit = values => {
61
- this.setState({logging: true})
62
- try {
63
- values.password = encodePassword(values.password)
64
- } catch (e) {
65
- console.error('[Login] 密码编码失败:', e);
66
- this.setState({logging: false})
67
- getMessageApi().error('密码含不支持字符,请联系管理员重置')
68
- return
69
- }
70
- postLogin(values, this.props.location?.query, () => {
71
- this.setState({logging: false})
72
- }, () => {
73
- this.setState({logging: false})
74
- })
10
+ const search = this.props.location?.search || '';
11
+ history.replace(LOGIN_PATH + search);
75
12
  }
76
13
 
77
14
  render() {
78
- const pageStyle = {
79
- backgroundImage: `url("./login_bg.jpg")`
80
- }
81
-
82
- return (
83
- <section className='login-page' style={pageStyle}>
84
- <div className="login-content">
85
- {this.renderBrand()}
86
- {this.getForm()}
87
- {this.renderFormBottom()}
88
- </div>
89
- </section>
90
- );
91
- }
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
-
107
- getForm = () => {
108
- const form = (
109
- <Form
110
- name="normal_login"
111
- className="login-form"
112
- initialValues={{remember: true}}
113
- onFinish={this.submit}
114
- requiredMark={false}
115
- colon={false}
116
- >
117
- <Form.Item name="username" rules={[{required: true, message: '请输入用户名!'}]}>
118
- <Input size='large' prefix={<UserOutlined/>} placeholder="用户名" autoComplete="username"/>
119
- </Form.Item>
120
- <Form.Item name="password" rules={[{required: true, message: '请输入密码!'}]}>
121
- <Input.Password autoComplete="current-password" prefix={<LockOutlined/>} placeholder="密码"
122
- size='large'
123
- />
124
- </Form.Item>
125
-
126
- <Form.Item className="login-submit">
127
- <Button loading={this.state.logging} type="primary" htmlType="submit"
128
- block size='large'>
129
- 登录
130
- </Button>
131
- </Form.Item>
132
- </Form>
133
- );
134
-
135
- if (this.props.formRender) {
136
- return this.props.formRender(form);
137
- }
138
- return form;
139
- };
140
-
141
- renderFormBottom() {
142
- const siteInfo = this.state.siteInfo || {};
143
- if (siteInfo.loginBoxBottomTip) {
144
- return (
145
- <div className="login-bottom-tip">
146
- <WarningOutlined/> {siteInfo.loginBoxBottomTip}
147
- </div>
148
- )
149
- }
15
+ return null;
150
16
  }
151
17
  }