blob: 585f97b5c2f69d727c94e126b631beb7e4af10a1 [file] [log] [blame]
import Footer from '@/components/Footer';
import { getCaptchaImg, login } from '@/services/system/auth';
import { getFakeCaptcha } from '@/services/ant-design-pro/login';
import {
AlipayCircleOutlined,
LockOutlined,
MobileOutlined,
TaobaoCircleOutlined,
UserOutlined,
WeiboCircleOutlined,
} from '@ant-design/icons';
import {
LoginForm,
ProFormCaptcha,
ProFormCheckbox,
ProFormText,
} from '@ant-design/pro-components';
import { useEmotionCss } from '@ant-design/use-emotion-css';
import { FormattedMessage, history, SelectLang, useIntl, useModel, Helmet } from '@umijs/max';
import { Alert, Col, message, Row, Tabs, Image } from 'antd';
import Settings from '../../../../config/defaultSettings';
import React, { useEffect, useState } from 'react';
import { flushSync } from 'react-dom';
import { clearSessionToken, setSessionToken } from '@/access';
const ActionIcons = () => {
const langClassName = useEmotionCss(({ token }) => {
return {
marginLeft: '8px',
color: 'rgba(0, 0, 0, 0.2)',
fontSize: '24px',
verticalAlign: 'middle',
cursor: 'pointer',
transition: 'color 0.3s',
'&:hover': {
color: token.colorPrimaryActive,
},
};
});
return (
<>
<AlipayCircleOutlined key="AlipayCircleOutlined" className={langClassName} />
<TaobaoCircleOutlined key="TaobaoCircleOutlined" className={langClassName} />
<WeiboCircleOutlined key="WeiboCircleOutlined" className={langClassName} />
</>
);
};
const Lang = () => {
const langClassName = useEmotionCss(({ token }) => {
return {
width: 42,
height: 42,
lineHeight: '42px',
position: 'fixed',
right: 16,
borderRadius: token.borderRadius,
':hover': {
backgroundColor: token.colorBgTextHover,
},
};
});
return (
<div className={langClassName} data-lang>
{SelectLang && <SelectLang />}
</div>
);
};
const LoginMessage: React.FC<{
content: string;
}> = ({ content }) => {
return (
<Alert
style={{
marginBottom: 24,
}}
message={content}
type="error"
showIcon
/>
);
};
const Login: React.FC = () => {
const [userLoginState, setUserLoginState] = useState<API.LoginResult>({code: 200});
const [type, setType] = useState<string>('account');
const { initialState, setInitialState } = useModel('@@initialState');
const [captchaCode, setCaptchaCode] = useState<string>('');
const [uuid, setUuid] = useState<string>('');
const containerClassName = useEmotionCss(() => {
return {
display: 'flex',
flexDirection: 'column',
height: '100vh',
overflow: 'auto',
backgroundImage:
"url('https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/V-_oS6r-i7wAAAAAAAAAAAAAFl94AQBr')",
backgroundSize: '100% 100%',
};
});
const intl = useIntl();
const getCaptchaCode = async () => {
const response = await getCaptchaImg();
const imgdata = `data:image/png;base64,${response.img}`;
setCaptchaCode(imgdata);
setUuid(response.uuid);
};
const fetchUserInfo = async () => {
const userInfo = await initialState?.fetchUserInfo?.();
if (userInfo) {
flushSync(() => {
setInitialState((s) => ({
...s,
currentUser: userInfo,
}));
});
}
};
const handleSubmit = async (values: API.LoginParams) => {
try {
// 登录
const response = await login({ ...values, uuid });
if (response.code === 200) {
const defaultLoginSuccessMessage = intl.formatMessage({
id: 'pages.login.success',
defaultMessage: '登录成功!',
});
const current = new Date();
const expireTime = current.setTime(current.getTime() + 1000 * 12 * 60 * 60);
console.log('login response: ', response);
setSessionToken(response?.token, response?.token, expireTime);
message.success(defaultLoginSuccessMessage);
await fetchUserInfo();
console.log('login ok');
const urlParams = new URL(window.location.href).searchParams;
history.push(urlParams.get('redirect') || '/');
return;
} else {
console.log(response.msg);
clearSessionToken();
// 如果失败去设置用户错误信息
setUserLoginState({ ...response, type });
getCaptchaCode();
}
} catch (error) {
const defaultLoginFailureMessage = intl.formatMessage({
id: 'pages.login.failure',
defaultMessage: '登录失败,请重试!',
});
console.log(error);
message.error(defaultLoginFailureMessage);
}
};
const { code } = userLoginState;
const loginType = type;
useEffect(() => {
getCaptchaCode();
}, []);
return (
<div
className={containerClassName}
style={{
backgroundImage:
"url('https://images.unsplash.com/photo-1462331940025-496dfbfc7564?auto=format&fit=crop&w=1500&q=80')",
backgroundSize: 'cover',
backgroundPosition: 'center',
minHeight: '100vh',
position: 'relative',
overflow: 'hidden',
}}
>
<Helmet>
<title>
{intl.formatMessage({
id: 'menu.login',
defaultMessage: '登录页',
})}
- {Settings.title}
</title>
</Helmet>
<Lang />
{/* 星空粒子特效层 */}
<div
style={{
position: 'absolute',
inset: 0,
zIndex: 0,
pointerEvents: 'none',
background: 'radial-gradient(ellipse at bottom, #1b2735 0%, #090a0f 100%)',
}}
>
{/* 可以集成粒子库如 tsParticles 或者简单用 CSS 动画 */}
<svg width="100%" height="100%">
{[...Array(60)].map((_, i) => (
<circle
key={i}
cx={Math.random() * 1600}
cy={Math.random() * 900}
r={Math.random() * 1.5 + 0.5}
fill="#fff"
opacity={Math.random() * 0.8 + 0.2}
/>
))}
</svg>
</div>
<div
style={{
flex: '1',
padding: '32px 0',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
minHeight: '100vh',
position: 'relative',
zIndex: 1,
}}
>
<LoginForm
contentStyle={{
minWidth: 320,
maxWidth: 400,
background: 'rgba(25, 34, 54, 0.92)',
borderRadius: 16,
boxShadow: '0 8px 32px 0 rgba(31, 38, 135, 0.37)',
border: '1px solid rgba(255,255,255,0.18)',
padding: '32px 32px 24px 32px',
color: '#fff',
}}
// logo={
// <img
// alt="logo"
// src="/planet-logo.svg"
// style={{
// width: 64,
// height: 64,
// filter: 'drop-shadow(0 0 8px #fff8) drop-shadow(0 0 16px #6cf)',
// marginBottom: 8,
// }}
// />
// }
title={
<span style={{ color: '#fff', fontWeight: 700, fontSize: 28, letterSpacing: 2 }}>
ThunderHub
</span>
}
subTitle={
<span style={{ color: '#b3c7f9', fontSize: 16 }}>
探索你的专属星球,畅享PT世界
</span>
}
initialValues={{
autoLogin: true,
}}
// actions={[
// <FormattedMessage
// key="loginWith"
// id="pages.login.loginWith"
// defaultMessage="其他登录方式"
// />,
// <ActionIcons key="icons" />,
// ]}
onFinish={async (values) => {
await handleSubmit(values as API.LoginParams);
}}
>
<Tabs
activeKey={type}
onChange={setType}
centered
items={[
{
key: 'account',
label: (
<span style={{ color: '#fff' }}>
{intl.formatMessage({
id: 'pages.login.accountLogin.tab',
defaultMessage: '账户密码登录',
})}
</span>
),
},
{
key: 'mobile',
label: (
<span style={{ color: '#fff' }}>
{intl.formatMessage({
id: 'pages.login.phoneLogin.tab',
defaultMessage: '手机号登录',
})}
</span>
),
},
]}
style={{ marginBottom: 24 }}
/>
{code !== 200 && loginType === 'account' && (
<LoginMessage
content={intl.formatMessage({
id: 'pages.login.accountLogin.errorMessage',
defaultMessage: '账户或密码错误(admin/admin123)',
})}
/>
)}
{type === 'account' && (
<>
<ProFormText
name="username"
initialValue="admin"
fieldProps={{
size: 'large',
prefix: <UserOutlined style={{ color: '#6cf' }} />,
style: { background: 'rgba(255,255,255,0.08)', color: '#fff' },
}}
placeholder={intl.formatMessage({
id: 'pages.login.username.placeholder',
defaultMessage: '用户名: admin',
})}
rules={[
{
required: true,
message: (
<FormattedMessage
id="pages.login.username.required"
defaultMessage="请输入用户名!"
/>
),
},
]}
/>
<ProFormText.Password
name="password"
initialValue="admin123"
fieldProps={{
size: 'large',
prefix: <LockOutlined style={{ color: '#6cf' }} />,
style: { background: 'rgba(255,255,255,0.08)', color: '#fff' },
}}
placeholder={intl.formatMessage({
id: 'pages.login.password.placeholder',
defaultMessage: '密码: admin123',
})}
rules={[
{
required: true,
message: (
<FormattedMessage
id="pages.login.password.required"
defaultMessage="请输入密码!"
/>
),
},
]}
/>
<Row>
<Col flex={3}>
<ProFormText
style={{
float: 'right',
background: 'rgba(255,255,255,0.08)',
color: '#fff',
}}
name="code"
placeholder={intl.formatMessage({
id: 'pages.login.captcha.placeholder',
defaultMessage: '请输入验证',
})}
rules={[
{
required: true,
message: (
<FormattedMessage
id="pages.searchTable.updateForm.ruleName.nameRules"
defaultMessage="请输入验证啊"
/>
),
},
]}
/>
</Col>
<Col flex={2}>
<Image
src={captchaCode}
alt="验证码"
style={{
display: 'inline-block',
verticalAlign: 'top',
cursor: 'pointer',
paddingLeft: '10px',
width: '100px',
borderRadius: 8,
boxShadow: '0 0 8px #6cf8',
background: '#fff',
}}
preview={false}
onClick={() => getCaptchaCode()}
/>
</Col>
</Row>
</>
)}
{code !== 200 && loginType === 'mobile' && (
<LoginMessage content="验证码错误" />
)}
{type === 'mobile' && (
<>
<ProFormText
fieldProps={{
size: 'large',
prefix: <MobileOutlined style={{ color: '#6cf' }} />,
style: { background: 'rgba(255,255,255,0.08)', color: '#fff' },
}}
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 style={{ color: '#6cf' }} />,
style: { background: 'rgba(255,255,255,0.08)', color: '#fff' },
}}
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) {
return;
}
message.success('获取验证码成功!验证码为:1234');
}}
/>
</>
)}
<div
style={{
marginBottom: 24,
color: '#b3c7f9',
}}
>
<ProFormCheckbox>
<a
style={{
float: 'right',
color: '#fff',
fontSize: 14,
}}>
<FormattedMessage id="pages.login.rememberMe" defaultMessage="自动登录" />
</a>
</ProFormCheckbox>
<a
style={{
float: 'right',
color: '#6cf',
}}
>
<FormattedMessage id="pages.login.forgotPassword" defaultMessage="忘记密码" />
</a>
</div>
</LoginForm>
</div>
<Footer />
</div>
);
};
export default Login;