2 changed files with 432 additions and 0 deletions
@ -0,0 +1,114 @@ |
|||
@import '~antd/es/style/themes/default.less'; |
|||
|
|||
.container { |
|||
display: flex; |
|||
flex-direction: column; |
|||
height: 100vh; |
|||
overflow: auto; |
|||
background: @layout-body-background; |
|||
} |
|||
|
|||
.lang { |
|||
width: 100%; |
|||
height: 40px; |
|||
line-height: 44px; |
|||
text-align: right; |
|||
:global(.ant-dropdown-trigger) { |
|||
margin-right: 24px; |
|||
} |
|||
} |
|||
|
|||
.content { |
|||
flex: 1; |
|||
padding: 32px 0; |
|||
} |
|||
|
|||
@media (min-width: @screen-md-min) { |
|||
.container { |
|||
background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg'); |
|||
background-repeat: no-repeat; |
|||
background-position: center 110px; |
|||
background-size: 100%; |
|||
} |
|||
|
|||
.content { |
|||
padding: 32px 0 24px; |
|||
} |
|||
} |
|||
|
|||
.top { |
|||
text-align: center; |
|||
} |
|||
|
|||
.header { |
|||
height: 44px; |
|||
line-height: 44px; |
|||
a { |
|||
text-decoration: none; |
|||
} |
|||
} |
|||
|
|||
.logo { |
|||
height: 44px; |
|||
margin-right: 16px; |
|||
vertical-align: top; |
|||
} |
|||
|
|||
.title { |
|||
position: relative; |
|||
top: 2px; |
|||
color: @heading-color; |
|||
font-weight: 600; |
|||
font-size: 33px; |
|||
font-family: Avenir, 'Helvetica Neue', Arial, Helvetica, sans-serif; |
|||
} |
|||
|
|||
.desc { |
|||
margin-top: 12px; |
|||
margin-bottom: 40px; |
|||
color: @text-color-secondary; |
|||
font-size: @font-size-base; |
|||
} |
|||
|
|||
.main { |
|||
width: 328px; |
|||
margin: 0 auto; |
|||
@media screen and (max-width: @screen-sm) { |
|||
width: 95%; |
|||
max-width: 328px; |
|||
} |
|||
|
|||
:global { |
|||
.@{ant-prefix}-tabs-nav-list { |
|||
margin: auto; |
|||
font-size: 16px; |
|||
} |
|||
} |
|||
|
|||
.icon { |
|||
margin-left: 16px; |
|||
color: rgba(0, 0, 0, 0.2); |
|||
font-size: 24px; |
|||
vertical-align: middle; |
|||
cursor: pointer; |
|||
transition: color 0.3s; |
|||
|
|||
&:hover { |
|||
color: @primary-color; |
|||
} |
|||
} |
|||
|
|||
.other { |
|||
margin-top: 24px; |
|||
line-height: 22px; |
|||
text-align: left; |
|||
.register { |
|||
float: right; |
|||
} |
|||
} |
|||
|
|||
.prefixIcon { |
|||
color: @primary-color; |
|||
font-size: @font-size-base; |
|||
} |
|||
} |
|||
@ -0,0 +1,318 @@ |
|||
import { |
|||
AlipayCircleOutlined, |
|||
LockOutlined, |
|||
MobileOutlined, |
|||
TaobaoCircleOutlined, |
|||
UserOutlined, |
|||
WeiboCircleOutlined, |
|||
} from '@ant-design/icons'; |
|||
import { Alert, Space, message, Tabs } from 'antd'; |
|||
import React, { useState } from 'react'; |
|||
import ProForm, { ProFormCaptcha, ProFormCheckbox, ProFormText } from '@ant-design/pro-form'; |
|||
import { useIntl, Link, history, FormattedMessage, SelectLang, useModel } from 'umi'; |
|||
import Footer from '@/components/Footer'; |
|||
import { login } from '@/services/ant-design-pro/api'; |
|||
import { getFakeCaptcha } from '@/services/ant-design-pro/login'; |
|||
|
|||
import styles from './index.less'; |
|||
|
|||
const LoginMessage: React.FC<{ |
|||
content: string; |
|||
}> = ({ content }) => ( |
|||
<Alert |
|||
style={{ |
|||
marginBottom: 24, |
|||
}} |
|||
message={content} |
|||
type="error" |
|||
showIcon |
|||
/> |
|||
); |
|||
|
|||
/** 此方法会跳转到 redirect 参数所在的位置 */ |
|||
const goto = () => { |
|||
if (!history) return; |
|||
setTimeout(() => { |
|||
const { query } = history.location; |
|||
const { redirect } = query as { redirect: string }; |
|||
history.push(redirect || '/'); |
|||
}, 10); |
|||
}; |
|||
|
|||
const Login: React.FC = () => { |
|||
const [submitting, setSubmitting] = useState(false); |
|||
const [userLoginState, setUserLoginState] = useState<API.LoginResult>({}); |
|||
const [type, setType] = useState<string>('account'); |
|||
const { initialState, setInitialState } = useModel('@@initialState'); |
|||
|
|||
const intl = useIntl(); |
|||
|
|||
const fetchUserInfo = async () => { |
|||
const userInfo = await initialState?.fetchUserInfo?.(); |
|||
if (userInfo) { |
|||
setInitialState({ |
|||
...initialState, |
|||
currentUser: userInfo, |
|||
}); |
|||
} |
|||
}; |
|||
|
|||
const handleSubmit = async (values: API.LoginParams) => { |
|||
setSubmitting(true); |
|||
try { |
|||
// 登录
|
|||
const msg = await login({ ...values, type }); |
|||
if (msg.status === 'ok') { |
|||
const defaultloginSuccessMessage = intl.formatMessage({ |
|||
id: 'pages.login.success', |
|||
defaultMessage: '登录成功!', |
|||
}); |
|||
message.success(defaultloginSuccessMessage); |
|||
await fetchUserInfo(); |
|||
goto(); |
|||
return; |
|||
} |
|||
// 如果失败去设置用户错误信息
|
|||
setUserLoginState(msg); |
|||
} catch (error) { |
|||
const defaultloginFailureMessage = intl.formatMessage({ |
|||
id: 'pages.login.failure', |
|||
defaultMessage: '登录失败,请重试!', |
|||
}); |
|||
|
|||
message.error(defaultloginFailureMessage); |
|||
} |
|||
setSubmitting(false); |
|||
}; |
|||
const { status, type: loginType } = userLoginState; |
|||
|
|||
return ( |
|||
<div className={styles.container}> |
|||
<div className={styles.lang} data-lang> |
|||
{SelectLang && <SelectLang />} |
|||
</div> |
|||
<div className={styles.content}> |
|||
<div className={styles.top}> |
|||
<div className={styles.header}> |
|||
<Link to="/"> |
|||
<img alt="logo" className={styles.logo} src="/logo.svg" /> |
|||
<span className={styles.title}>Ant Design</span> |
|||
</Link> |
|||
</div> |
|||
<div className={styles.desc}> |
|||
{intl.formatMessage({ id: 'pages.layouts.userLayout.title' })} |
|||
</div> |
|||
</div> |
|||
|
|||
<div className={styles.main}> |
|||
<ProForm |
|||
initialValues={{ |
|||
autoLogin: true, |
|||
}} |
|||
submitter={{ |
|||
searchConfig: { |
|||
submitText: intl.formatMessage({ |
|||
id: 'pages.login.submit', |
|||
defaultMessage: '登录', |
|||
}), |
|||
}, |
|||
render: (_, dom) => dom.pop(), |
|||
submitButtonProps: { |
|||
loading: submitting, |
|||
size: 'large', |
|||
style: { |
|||
width: '100%', |
|||
}, |
|||
}, |
|||
}} |
|||
onFinish={async (values) => { |
|||
handleSubmit(values as API.LoginParams); |
|||
}} |
|||
> |
|||
<Tabs activeKey={type} onChange={setType}> |
|||
<Tabs.TabPane |
|||
key="account" |
|||
tab={intl.formatMessage({ |
|||
id: 'pages.login.accountLogin.tab', |
|||
defaultMessage: '账户密码登录', |
|||
})} |
|||
/> |
|||
<Tabs.TabPane |
|||
key="mobile" |
|||
tab={intl.formatMessage({ |
|||
id: 'pages.login.phoneLogin.tab', |
|||
defaultMessage: '手机号登录', |
|||
})} |
|||
/> |
|||
</Tabs> |
|||
|
|||
{status === 'error' && loginType === 'account' && ( |
|||
<LoginMessage |
|||
content={intl.formatMessage({ |
|||
id: 'pages.login.accountLogin.errorMessage', |
|||
defaultMessage: '账户或密码错误(admin/ant.design)', |
|||
})} |
|||
/> |
|||
)} |
|||
{type === 'account' && ( |
|||
<> |
|||
<ProFormText |
|||
name="username" |
|||
fieldProps={{ |
|||
size: 'large', |
|||
prefix: <UserOutlined className={styles.prefixIcon} />, |
|||
}} |
|||
placeholder={intl.formatMessage({ |
|||
id: 'pages.login.username.placeholder', |
|||
defaultMessage: '用户名: admin or user', |
|||
})} |
|||
rules={[ |
|||
{ |
|||
required: true, |
|||
message: ( |
|||
<FormattedMessage |
|||
id="pages.login.username.required" |
|||
defaultMessage="请输入用户名!" |
|||
/> |
|||
), |
|||
}, |
|||
]} |
|||
/> |
|||
<ProFormText.Password |
|||
name="password" |
|||
fieldProps={{ |
|||
size: 'large', |
|||
prefix: <LockOutlined className={styles.prefixIcon} />, |
|||
}} |
|||
placeholder={intl.formatMessage({ |
|||
id: 'pages.login.password.placeholder', |
|||
defaultMessage: '密码: ant.design', |
|||
})} |
|||
rules={[ |
|||
{ |
|||
required: true, |
|||
message: ( |
|||
<FormattedMessage |
|||
id="pages.login.password.required" |
|||
defaultMessage="请输入密码!" |
|||
/> |
|||
), |
|||
}, |
|||
]} |
|||
/> |
|||
</> |
|||
)} |
|||
|
|||
{status === 'error' && loginType === 'mobile' && <LoginMessage content="验证码错误" />} |
|||
{type === 'mobile' && ( |
|||
<> |
|||
<ProFormText |
|||
fieldProps={{ |
|||
size: 'large', |
|||
prefix: <MobileOutlined className={styles.prefixIcon} />, |
|||
}} |
|||
name="mobile" |
|||
placeholder={intl.formatMessage({ |
|||
id: 'pages.login.phoneNumber.placeholder', |
|||
defaultMessage: '手机号', |
|||
})} |
|||
rules={[ |
|||
{ |
|||
required: true, |
|||
message: ( |
|||
<FormattedMessage |
|||
id="pages.login.phoneNumber.required" |
|||
defaultMessage="请输入手机号!" |
|||
/> |
|||
), |
|||
}, |
|||
{ |
|||
pattern: /^1\d{10}$/, |
|||
message: ( |
|||
<FormattedMessage |
|||
id="pages.login.phoneNumber.invalid" |
|||
defaultMessage="手机号格式错误!" |
|||
/> |
|||
), |
|||
}, |
|||
]} |
|||
/> |
|||
<ProFormCaptcha |
|||
fieldProps={{ |
|||
size: 'large', |
|||
prefix: <LockOutlined className={styles.prefixIcon} />, |
|||
}} |
|||
captchaProps={{ |
|||
size: 'large', |
|||
}} |
|||
placeholder={intl.formatMessage({ |
|||
id: 'pages.login.captcha.placeholder', |
|||
defaultMessage: '请输入验证码', |
|||
})} |
|||
captchaTextRender={(timing, count) => { |
|||
if (timing) { |
|||
return `${count} ${intl.formatMessage({ |
|||
id: 'pages.getCaptchaSecondText', |
|||
defaultMessage: '获取验证码', |
|||
})}`;
|
|||
} |
|||
return intl.formatMessage({ |
|||
id: 'pages.login.phoneLogin.getVerificationCode', |
|||
defaultMessage: '获取验证码', |
|||
}); |
|||
}} |
|||
name="captcha" |
|||
rules={[ |
|||
{ |
|||
required: true, |
|||
message: ( |
|||
<FormattedMessage |
|||
id="pages.login.captcha.required" |
|||
defaultMessage="请输入验证码!" |
|||
/> |
|||
), |
|||
}, |
|||
]} |
|||
onGetCaptcha={async (phone) => { |
|||
const result = await getFakeCaptcha({ |
|||
phone, |
|||
}); |
|||
if (result === false) { |
|||
return; |
|||
} |
|||
message.success('获取验证码成功!验证码为:1234'); |
|||
}} |
|||
/> |
|||
</> |
|||
)} |
|||
<div |
|||
style={{ |
|||
marginBottom: 24, |
|||
}} |
|||
> |
|||
<ProFormCheckbox noStyle name="autoLogin"> |
|||
<FormattedMessage id="pages.login.rememberMe" defaultMessage="自动登录" /> |
|||
</ProFormCheckbox> |
|||
<a |
|||
style={{ |
|||
float: 'right', |
|||
}} |
|||
> |
|||
<FormattedMessage id="pages.login.forgotPassword" defaultMessage="忘记密码" /> |
|||
</a> |
|||
</div> |
|||
</ProForm> |
|||
<Space className={styles.other}> |
|||
<FormattedMessage id="pages.login.loginWith" defaultMessage="其他登录方式" /> |
|||
<AlipayCircleOutlined className={styles.icon} /> |
|||
<TaobaoCircleOutlined className={styles.icon} /> |
|||
<WeiboCircleOutlined className={styles.icon} /> |
|||
</Space> |
|||
</div> |
|||
</div> |
|||
<Footer /> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default Login; |
|||
Loading…
Reference in new issue