@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.
- package/package.json +3 -3
- package/src/framework/GlobalData.ts +3 -1
- package/src/framework/biz/OrgTree.tsx +1 -1
- package/src/framework/biz/RoleTree.tsx +1 -1
- package/src/framework/biz/UserPicker.tsx +4 -4
- package/src/framework/biz/index.tsx +4 -4
- package/src/framework/components/LinkButton/index.tsx +4 -0
- package/src/framework/components/OrgSwitcher/index.tsx +2 -2
- package/src/framework/components/Page/index.tsx +6 -14
- package/src/framework/components/ProTable/index.tsx +3 -1
- package/src/framework/components/ThemeSettings/index.tsx +5 -38
- package/src/framework/config/index.ts +12 -0
- package/src/framework/config/layoutConfig.ts +47 -8
- package/src/framework/config/themeConfig.ts +93 -25
- package/src/framework/config/uiDefaults.ts +26 -0
- package/src/framework/fields/FieldEditor/index.tsx +1 -1
- package/src/framework/fields/FieldUploadFile/index.tsx +1 -1
- package/src/framework/fields/FieldUploadImage/index.tsx +42 -10
- package/src/framework/fields/FieldUserPicker/index.tsx +3 -3
- package/src/framework/fields/orgTree.ts +1 -1
- package/src/framework/router/index.ts +1 -0
- package/src/framework/router/matcher.ts +19 -4
- package/src/framework/utils/AvatarUtils.ts +64 -0
- package/src/framework/utils/HttpClient.ts +5 -5
- package/src/framework/utils/PageUtils.ts +38 -7
- package/src/framework/utils/TitleUtils.ts +38 -0
- package/src/framework/utils/UrlUtils.ts +3 -3
- package/src/framework/utils/index.ts +2 -0
- package/src/layouts/admin/HeaderRight.jsx +62 -16
- package/src/layouts/admin/SinglePageLayout.jsx +13 -2
- package/src/layouts/admin/TabLayout.jsx +38 -19
- package/src/layouts/admin/index.jsx +4 -4
- package/src/layouts/index.tsx +52 -15
- package/src/pages/404.jsx +4 -0
- package/src/pages/article/$code.jsx +5 -1
- package/src/pages/login.jsx +157 -0
- package/src/pages/{public/login.less → login.less} +16 -1
- package/src/pages/public/login.jsx +11 -145
- package/src/pages/standalone/{forceUpdatePwd.jsx → force-update-pwd.jsx} +10 -4
- package/src/pages/system/article/index.jsx +1 -1
- package/src/pages/system/config/index.jsx +46 -20
- package/src/pages/system/file/index.jsx +3 -3
- package/src/pages/system/job/index.jsx +1 -1
- package/src/pages/system/log/index.jsx +1 -1
- package/src/pages/system/online/index.jsx +95 -0
- package/src/pages/system/org/index.jsx +7 -7
- package/src/pages/system/role/index.jsx +8 -8
- package/src/pages/system/role/{rolePerm.jsx → role-perm.jsx} +9 -4
- package/src/pages/system/user/{userPerm.jsx → UserPermModal.jsx} +3 -3
- package/src/pages/system/user/index.jsx +6 -6
- package/src/pages/user-center/AvatarPicker.jsx +76 -0
- package/src/pages/{userCenter/permView.jsx → user-center/PermView.jsx} +48 -25
- package/src/pages/user-center/RecentOps.jsx +36 -0
- package/src/pages/user-center/index.jsx +393 -0
- package/vite-plugin/index.js +64 -6
- package/src/pages/userCenter/index.jsx +0 -93
|
@@ -1,24 +1,39 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import {history, PageFrame,
|
|
2
|
+
import {history, PageFrame, EventBus, matchRoute, setPageTitle} from "../../framework";
|
|
3
3
|
import {AdminTabs} from "./AdminTabs";
|
|
4
4
|
|
|
5
|
-
function
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
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 = (
|
|
33
|
-
const
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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]}>
|
package/src/layouts/index.tsx
CHANGED
|
@@ -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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
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 (
|
|
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/
|
|
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
|
|
198
|
-
|
|
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 {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/
|
|
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
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
46
|
-
|
|
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
|
-
|
|
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
|
}
|