blob: b8a44ff609b7833666bd601e47e108621da391ac [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';
import './index.less';
import { registerUser } from '@/services/bt/index';
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: 'hidden',
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:
"linear-gradient(120deg, #232526 0%, #414345 100%), url('https://images.unsplash.com/photo-1462331940025-496dfbfc7564?auto=format&fit=crop&w=1500&q=80')",
backgroundBlendMode: 'overlay',
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%)',
}}
>
<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,
}}
>
<div
style={{
background: 'rgba(30, 34, 54, 0.96)',
borderRadius: 20,
boxShadow: '0 8px 32px 0 rgba(31, 38, 135, 0.37)',
border: '1px solid rgba(255,255,255,0.12)',
padding: '36px 24px 28px 24px',
minWidth: 340,
maxWidth: 420,
width: '100%',
color: '#fff',
backdropFilter: 'blur(8px)',
position: 'relative',
transition: 'max-width 0.2s cubic-bezier(.4,2,.6,1)',
overflow: 'hidden',
}}
>
<div style={{ textAlign: 'center', marginBottom: 24 }}>
<img
src="/logo.svg"
alt="ThunderHub"
style={{
width: 54,
height: 48,
marginBottom: 8,
filter: 'drop-shadow(0 0 8px #6cf8)',
}}
/>
<div
style={{
color: '#fff',
fontWeight: 700,
fontSize: 26,
letterSpacing: 2,
marginBottom: 4,
fontFamily: 'Montserrat, sans-serif',
}}
>
ThunderHub
</div>
<div style={{ color: '#b3c7f9', fontSize: 14, letterSpacing: 1 }}>
探索你的专属星球,畅享PT世界
<span style={{ color: '#6cf', marginLeft: 8, fontWeight: 500, fontSize: 12 }}>
JRX MSY ZYT HXQ LJB
</span>
</div>
</div>
<LoginForm
contentStyle={{
background: 'transparent',
boxShadow: 'none',
padding: 0,
color: '#fff',
width: '100%',
overflow: 'hidden',
}}
initialValues={{
autoLogin: true,
}}
onFinish={async (values) => {
if (type === 'account') {
await handleSubmit(values as API.LoginParams);
} else {
try {
const response = await registerUser(
values.username ?? '',
values.password ?? '',
values.inviteCode ?? ''
);
if (response.code === 0) {
message.success('注册成功!');
// 注册成功后自动跳转到登录页或自动登录
setType('account');
} else {
message.error(response.msg || '注册失败,请重试!');
clearSessionToken();
setUserLoginState({ ...response, type });
}
} catch (error) {
message.error('注册失败,请重试!');
}
}
}}
submitter={false}
>
<Tabs
activeKey={type}
onChange={setType}
centered
items={[
{
key: 'account',
label: (
<span style={{ color: '#fff', fontWeight: 500, fontSize: 16 }}>
{intl.formatMessage({
id: 'pages.login.accountLogin.tab',
defaultMessage: '账户密码登录',
})}
</span>
),
},
{
key: 'mobile',
label: (
<span style={{ color: '#fff', fontWeight: 500, fontSize: 16 }}>
{intl.formatMessage({
id: 'pages.login.phoneLogin.tab',
defaultMessage: '注册',
})}
</span>
),
},
]}
style={{ marginBottom: 24 }}
indicatorSize={36}
/>
{code !== 200 && loginType === 'account' && (
<LoginMessage
content={intl.formatMessage({
id: 'pages.login.accountLogin.errorMessage',
defaultMessage: '账户或密码错误',
})}
/>
)}
{type === 'account' && (
<>
<ProFormText
name="username"
fieldProps={{
size: 'large',
prefix: <UserOutlined style={{ color: '#6cf' }} />,
style: {
background: 'rgba(255,255,255,0.08)',
color: '#fff',
borderRadius: 8,
},
}}
placeholder={intl.formatMessage({
id: 'pages.login.username.placeholder',
})}
rules={[
{
required: true,
message: (
<FormattedMessage
id="pages.login.username.required"
defaultMessage="请输入用户名!"
/>
),
},
]}
/>
<ProFormText.Password
name="password"
fieldProps={{
size: 'large',
prefix: <LockOutlined style={{ color: '#6cf' }} />,
style: {
background: 'rgba(255,255,255,0.08)',
color: '#fff',
borderRadius: 8,
},
}}
placeholder={intl.formatMessage({
id: 'pages.login.password.placeholder',
})}
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',
borderRadius: 8,
}}
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: '90px',
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: <LockOutlined style={{ color: '#6cf' }} />, // 换成钥匙图标
style: {
background: 'rgba(255,255,255,0.08)',
color: '#fff',
borderRadius: 8,
},
}}
name="inviteCode"
placeholder={intl.formatMessage({
id: 'pages.login.inviteCode.placeholder',
defaultMessage: '请输入邀请码',
})}
rules={[
{
required: true,
message: (
<FormattedMessage
id="pages.login.inviteCode.required"
defaultMessage="请输入邀请码!"
/>
),
},
]}
/>
<ProFormText
fieldProps={{
size: 'large',
prefix: <UserOutlined style={{ color: '#6cf' }} />,
style: {
background: 'rgba(255,255,255,0.08)',
color: '#fff',
borderRadius: 8,
},
}}
name="username"
placeholder={intl.formatMessage({
id: 'pages.login.username.placeholder',
defaultMessage: '请输入用户名',
})}
rules={[
{
required: true,
message: (
<FormattedMessage
id="pages.login.username.required"
defaultMessage="请输入用户名!"
/>
),
},
]}
/>
<ProFormText.Password
fieldProps={{
size: 'large',
prefix: <LockOutlined style={{ color: '#6cf' }} />,
style: {
background: 'rgba(255,255,255,0.08)',
color: '#fff',
borderRadius: 8,
},
}}
name="password"
placeholder={intl.formatMessage({
id: 'pages.login.password.placeholder',
defaultMessage: '请输入密码',
})}
rules={[
{
required: true,
message: (
<FormattedMessage
id="pages.login.password.required"
defaultMessage="请输入密码!"
/>
),
},
]}
/>
</>
)}
{type === 'account' && (
<div
style={{
marginBottom: 24,
color: '#b3c7f9',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
fontSize: 13,
}}
>
<ProFormCheckbox>
<span style={{ color: '#fff' }}>
<FormattedMessage id="pages.login.rememberMe" defaultMessage="自动登录" />
</span>
</ProFormCheckbox>
<a
style={{
color: '#6cf',
}}
>
<FormattedMessage id="pages.login.forgotPassword" defaultMessage="忘记密码" />
</a>
</div>
)}
{/* 登录/注册按钮 */}
<div style={{ marginTop: 24, display: 'flex', gap: 16 }}>
{type === 'account' && (
<button
type="submit"
style={{
width: '100%',
background: 'linear-gradient(90deg, #6cf 0%, #3E71FF 100%)',
color: '#fff',
border: 'none',
borderRadius: 8,
padding: '12px 0',
fontSize: 16,
fontWeight: 600,
cursor: 'pointer',
boxShadow: '0 2px 8px #6cf4',
letterSpacing: 2,
}}
onClick={() => {
// 触发表单提交,登录
document.querySelector('form')?.dispatchEvent(new Event('submit', { cancelable: true, bubbles: true }));
}}
>
登录
</button>
)}
{type === 'mobile' && (
<button
type="button"
style={{
width: '100%',
background: 'linear-gradient(90deg, #6cf 0%, #3E71FF 100%)',
color: '#fff',
border: 'none',
borderRadius: 8,
padding: '12px 0',
fontSize: 16,
fontWeight: 600,
cursor: 'pointer',
boxShadow: '0 2px 8px #6cf4',
letterSpacing: 2,
}}
onClick={async () => {
// 触发表单校验并注册
const form = document.querySelector('form');
if (form) {
form.dispatchEvent(new Event('submit', { cancelable: true, bubbles: true }));
}
}}
>
注册
</button>
)}
</div>
</LoginForm>
</div>
</div>
<Footer />
</div>
);
};
export default Login;