@jiangood/open-admin 3.1.10 → 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 +4 -0
- 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/CronEditor/index.tsx +318 -0
- 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/components/index.ts +1 -0
- 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/CronUtils.ts +562 -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 +3 -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 +12 -3
- 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 +335 -0
- package/src/pages/system/file/index.jsx +3 -3
- package/src/pages/system/job/index.jsx +62 -19
- 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
|
@@ -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
|
}
|
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import {Button, Form, Input} from "antd";
|
|
3
|
-
import {getMessageApi, getToken, HttpClient, history} from "../../framework";
|
|
3
|
+
import {getMessageApi, getToken, HttpClient, history, LOGIN_PATH} from "../../framework";
|
|
4
|
+
|
|
5
|
+
export const pageMeta = {
|
|
6
|
+
requiresAuth: true,
|
|
7
|
+
layout: 'blank',
|
|
8
|
+
title: '修改密码',
|
|
9
|
+
};
|
|
4
10
|
|
|
5
11
|
export default class ForceUpdatePwdPage extends React.Component {
|
|
6
12
|
|
|
7
13
|
onFinish = (values) => {
|
|
8
|
-
HttpClient.post('admin/
|
|
14
|
+
HttpClient.post('admin/user-center/update-pwd', values, null).then(() => {
|
|
9
15
|
getMessageApi().success('修改密码成功,请重新登录');
|
|
10
|
-
history.push(
|
|
16
|
+
history.push(LOGIN_PATH);
|
|
11
17
|
})
|
|
12
18
|
}
|
|
13
19
|
|
|
14
|
-
validator = (rule, value) => HttpClient.get("admin/
|
|
20
|
+
validator = (rule, value) => HttpClient.get("admin/sys-user/pwd-strength", {password: value}, {toastError: false})
|
|
15
21
|
.catch(e => {
|
|
16
22
|
throw new Error(e.message)
|
|
17
23
|
})
|