blob: 585f97b5c2f69d727c94e126b631beb7e4af10a1 [file] [log] [blame]
Jiarenxiang38dcb052025-03-13 16:40:09 +08001import Footer from '@/components/Footer';
2import { getCaptchaImg, login } from '@/services/system/auth';
3import { getFakeCaptcha } from '@/services/ant-design-pro/login';
4import {
5 AlipayCircleOutlined,
6 LockOutlined,
7 MobileOutlined,
8 TaobaoCircleOutlined,
9 UserOutlined,
10 WeiboCircleOutlined,
11} from '@ant-design/icons';
12import {
13 LoginForm,
14 ProFormCaptcha,
15 ProFormCheckbox,
16 ProFormText,
17} from '@ant-design/pro-components';
18import { useEmotionCss } from '@ant-design/use-emotion-css';
19import { FormattedMessage, history, SelectLang, useIntl, useModel, Helmet } from '@umijs/max';
20import { Alert, Col, message, Row, Tabs, Image } from 'antd';
21import Settings from '../../../../config/defaultSettings';
22import React, { useEffect, useState } from 'react';
23import { flushSync } from 'react-dom';
24import { clearSessionToken, setSessionToken } from '@/access';
25
26const ActionIcons = () => {
27 const langClassName = useEmotionCss(({ token }) => {
28 return {
29 marginLeft: '8px',
30 color: 'rgba(0, 0, 0, 0.2)',
31 fontSize: '24px',
32 verticalAlign: 'middle',
33 cursor: 'pointer',
34 transition: 'color 0.3s',
35 '&:hover': {
36 color: token.colorPrimaryActive,
37 },
38 };
39 });
40
41 return (
42 <>
43 <AlipayCircleOutlined key="AlipayCircleOutlined" className={langClassName} />
44 <TaobaoCircleOutlined key="TaobaoCircleOutlined" className={langClassName} />
45 <WeiboCircleOutlined key="WeiboCircleOutlined" className={langClassName} />
46 </>
47 );
48};
49
50const Lang = () => {
51 const langClassName = useEmotionCss(({ token }) => {
52 return {
53 width: 42,
54 height: 42,
55 lineHeight: '42px',
56 position: 'fixed',
57 right: 16,
58 borderRadius: token.borderRadius,
59 ':hover': {
60 backgroundColor: token.colorBgTextHover,
61 },
62 };
63 });
64
65 return (
66 <div className={langClassName} data-lang>
67 {SelectLang && <SelectLang />}
68 </div>
69 );
70};
71
72const LoginMessage: React.FC<{
73 content: string;
74}> = ({ content }) => {
75 return (
76 <Alert
77 style={{
78 marginBottom: 24,
79 }}
80 message={content}
81 type="error"
82 showIcon
83 />
84 );
85};
86
87const Login: React.FC = () => {
88 const [userLoginState, setUserLoginState] = useState<API.LoginResult>({code: 200});
89 const [type, setType] = useState<string>('account');
90 const { initialState, setInitialState } = useModel('@@initialState');
91 const [captchaCode, setCaptchaCode] = useState<string>('');
92 const [uuid, setUuid] = useState<string>('');
93
94 const containerClassName = useEmotionCss(() => {
95 return {
96 display: 'flex',
97 flexDirection: 'column',
98 height: '100vh',
99 overflow: 'auto',
100 backgroundImage:
101 "url('https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/V-_oS6r-i7wAAAAAAAAAAAAAFl94AQBr')",
102 backgroundSize: '100% 100%',
103 };
104 });
105
106 const intl = useIntl();
107
108 const getCaptchaCode = async () => {
109 const response = await getCaptchaImg();
110 const imgdata = `data:image/png;base64,${response.img}`;
111 setCaptchaCode(imgdata);
112 setUuid(response.uuid);
113 };
114
115 const fetchUserInfo = async () => {
116 const userInfo = await initialState?.fetchUserInfo?.();
117 if (userInfo) {
118 flushSync(() => {
119 setInitialState((s) => ({
120 ...s,
121 currentUser: userInfo,
122 }));
123 });
124 }
125 };
126
127 const handleSubmit = async (values: API.LoginParams) => {
128 try {
129 // 登录
130 const response = await login({ ...values, uuid });
131 if (response.code === 200) {
132 const defaultLoginSuccessMessage = intl.formatMessage({
133 id: 'pages.login.success',
134 defaultMessage: '登录成功!',
135 });
136 const current = new Date();
137 const expireTime = current.setTime(current.getTime() + 1000 * 12 * 60 * 60);
138 console.log('login response: ', response);
139 setSessionToken(response?.token, response?.token, expireTime);
140 message.success(defaultLoginSuccessMessage);
141 await fetchUserInfo();
142 console.log('login ok');
143 const urlParams = new URL(window.location.href).searchParams;
144 history.push(urlParams.get('redirect') || '/');
145 return;
146 } else {
147 console.log(response.msg);
148 clearSessionToken();
149 // 如果失败去设置用户错误信息
150 setUserLoginState({ ...response, type });
151 getCaptchaCode();
152 }
153 } catch (error) {
154 const defaultLoginFailureMessage = intl.formatMessage({
155 id: 'pages.login.failure',
156 defaultMessage: '登录失败,请重试!',
157 });
158 console.log(error);
159 message.error(defaultLoginFailureMessage);
160 }
161 };
162 const { code } = userLoginState;
163 const loginType = type;
164
165 useEffect(() => {
166 getCaptchaCode();
167 }, []);
168
169 return (
Jiarenxiangefbcdd42025-06-03 17:06:34 +0800170 <div
171 className={containerClassName}
172 style={{
173 backgroundImage:
174 "url('https://images.unsplash.com/photo-1462331940025-496dfbfc7564?auto=format&fit=crop&w=1500&q=80')",
175 backgroundSize: 'cover',
176 backgroundPosition: 'center',
177 minHeight: '100vh',
178 position: 'relative',
179 overflow: 'hidden',
180 }}
181 >
Jiarenxiang38dcb052025-03-13 16:40:09 +0800182 <Helmet>
183 <title>
184 {intl.formatMessage({
185 id: 'menu.login',
186 defaultMessage: '登录页',
187 })}
188 - {Settings.title}
189 </title>
190 </Helmet>
191 <Lang />
Jiarenxiangefbcdd42025-06-03 17:06:34 +0800192 {/* 星空粒子特效层 */}
193 <div
194 style={{
195 position: 'absolute',
196 inset: 0,
197 zIndex: 0,
198 pointerEvents: 'none',
199 background: 'radial-gradient(ellipse at bottom, #1b2735 0%, #090a0f 100%)',
200 }}
201 >
202 {/* 可以集成粒子库如 tsParticles 或者简单用 CSS 动画 */}
203 <svg width="100%" height="100%">
204 {[...Array(60)].map((_, i) => (
205 <circle
206 key={i}
207 cx={Math.random() * 1600}
208 cy={Math.random() * 900}
209 r={Math.random() * 1.5 + 0.5}
210 fill="#fff"
211 opacity={Math.random() * 0.8 + 0.2}
212 />
213 ))}
214 </svg>
215 </div>
Jiarenxiang38dcb052025-03-13 16:40:09 +0800216 <div
217 style={{
218 flex: '1',
219 padding: '32px 0',
Jiarenxiangefbcdd42025-06-03 17:06:34 +0800220 display: 'flex',
221 alignItems: 'center',
222 justifyContent: 'center',
223 minHeight: '100vh',
224 position: 'relative',
225 zIndex: 1,
Jiarenxiang38dcb052025-03-13 16:40:09 +0800226 }}
227 >
228 <LoginForm
229 contentStyle={{
Jiarenxiangefbcdd42025-06-03 17:06:34 +0800230 minWidth: 320,
231 maxWidth: 400,
232 background: 'rgba(25, 34, 54, 0.92)',
233 borderRadius: 16,
234 boxShadow: '0 8px 32px 0 rgba(31, 38, 135, 0.37)',
235 border: '1px solid rgba(255,255,255,0.18)',
236 padding: '32px 32px 24px 32px',
237 color: '#fff',
Jiarenxiang38dcb052025-03-13 16:40:09 +0800238 }}
Jiarenxiangefbcdd42025-06-03 17:06:34 +0800239 // logo={
240 // <img
241 // alt="logo"
242 // src="/planet-logo.svg"
243 // style={{
244 // width: 64,
245 // height: 64,
246 // filter: 'drop-shadow(0 0 8px #fff8) drop-shadow(0 0 16px #6cf)',
247 // marginBottom: 8,
248 // }}
249 // />
250 // }
251 title={
252 <span style={{ color: '#fff', fontWeight: 700, fontSize: 28, letterSpacing: 2 }}>
Jiarenxiang36728482025-06-07 21:51:26 +0800253 ThunderHub
Jiarenxiangefbcdd42025-06-03 17:06:34 +0800254 </span>
255 }
256 subTitle={
257 <span style={{ color: '#b3c7f9', fontSize: 16 }}>
258 探索你的专属星球,畅享PT世界
259 </span>
260 }
Jiarenxiang38dcb052025-03-13 16:40:09 +0800261 initialValues={{
262 autoLogin: true,
263 }}
Jiarenxiangefbcdd42025-06-03 17:06:34 +0800264 // actions={[
265 // <FormattedMessage
266 // key="loginWith"
267 // id="pages.login.loginWith"
268 // defaultMessage="其他登录方式"
269 // />,
270 // <ActionIcons key="icons" />,
271 // ]}
Jiarenxiang38dcb052025-03-13 16:40:09 +0800272 onFinish={async (values) => {
273 await handleSubmit(values as API.LoginParams);
274 }}
275 >
276 <Tabs
277 activeKey={type}
278 onChange={setType}
279 centered
280 items={[
281 {
282 key: 'account',
Jiarenxiangefbcdd42025-06-03 17:06:34 +0800283 label: (
284 <span style={{ color: '#fff' }}>
285 {intl.formatMessage({
286 id: 'pages.login.accountLogin.tab',
287 defaultMessage: '账户密码登录',
288 })}
289 </span>
290 ),
Jiarenxiang38dcb052025-03-13 16:40:09 +0800291 },
292 {
293 key: 'mobile',
Jiarenxiangefbcdd42025-06-03 17:06:34 +0800294 label: (
295 <span style={{ color: '#fff' }}>
296 {intl.formatMessage({
297 id: 'pages.login.phoneLogin.tab',
298 defaultMessage: '手机号登录',
299 })}
300 </span>
301 ),
Jiarenxiang38dcb052025-03-13 16:40:09 +0800302 },
303 ]}
Jiarenxiangefbcdd42025-06-03 17:06:34 +0800304 style={{ marginBottom: 24 }}
Jiarenxiang38dcb052025-03-13 16:40:09 +0800305 />
306
307 {code !== 200 && loginType === 'account' && (
308 <LoginMessage
309 content={intl.formatMessage({
310 id: 'pages.login.accountLogin.errorMessage',
311 defaultMessage: '账户或密码错误(admin/admin123)',
312 })}
313 />
314 )}
315 {type === 'account' && (
316 <>
317 <ProFormText
318 name="username"
319 initialValue="admin"
320 fieldProps={{
321 size: 'large',
Jiarenxiangefbcdd42025-06-03 17:06:34 +0800322 prefix: <UserOutlined style={{ color: '#6cf' }} />,
323 style: { background: 'rgba(255,255,255,0.08)', color: '#fff' },
Jiarenxiang38dcb052025-03-13 16:40:09 +0800324 }}
325 placeholder={intl.formatMessage({
326 id: 'pages.login.username.placeholder',
327 defaultMessage: '用户名: admin',
328 })}
329 rules={[
330 {
331 required: true,
332 message: (
333 <FormattedMessage
334 id="pages.login.username.required"
335 defaultMessage="请输入用户名!"
336 />
337 ),
338 },
339 ]}
340 />
341 <ProFormText.Password
342 name="password"
343 initialValue="admin123"
344 fieldProps={{
345 size: 'large',
Jiarenxiangefbcdd42025-06-03 17:06:34 +0800346 prefix: <LockOutlined style={{ color: '#6cf' }} />,
347 style: { background: 'rgba(255,255,255,0.08)', color: '#fff' },
Jiarenxiang38dcb052025-03-13 16:40:09 +0800348 }}
349 placeholder={intl.formatMessage({
350 id: 'pages.login.password.placeholder',
351 defaultMessage: '密码: admin123',
352 })}
353 rules={[
354 {
355 required: true,
356 message: (
357 <FormattedMessage
358 id="pages.login.password.required"
359 defaultMessage="请输入密码!"
360 />
361 ),
362 },
363 ]}
364 />
365 <Row>
366 <Col flex={3}>
367 <ProFormText
368 style={{
369 float: 'right',
Jiarenxiangefbcdd42025-06-03 17:06:34 +0800370 background: 'rgba(255,255,255,0.08)',
371 color: '#fff',
Jiarenxiang38dcb052025-03-13 16:40:09 +0800372 }}
373 name="code"
374 placeholder={intl.formatMessage({
375 id: 'pages.login.captcha.placeholder',
376 defaultMessage: '请输入验证',
377 })}
378 rules={[
379 {
380 required: true,
381 message: (
382 <FormattedMessage
383 id="pages.searchTable.updateForm.ruleName.nameRules"
384 defaultMessage="请输入验证啊"
385 />
386 ),
387 },
388 ]}
389 />
390 </Col>
391 <Col flex={2}>
392 <Image
393 src={captchaCode}
394 alt="验证码"
395 style={{
396 display: 'inline-block',
397 verticalAlign: 'top',
398 cursor: 'pointer',
399 paddingLeft: '10px',
400 width: '100px',
Jiarenxiangefbcdd42025-06-03 17:06:34 +0800401 borderRadius: 8,
402 boxShadow: '0 0 8px #6cf8',
403 background: '#fff',
Jiarenxiang38dcb052025-03-13 16:40:09 +0800404 }}
405 preview={false}
406 onClick={() => getCaptchaCode()}
407 />
408 </Col>
409 </Row>
410 </>
411 )}
412
Jiarenxiangefbcdd42025-06-03 17:06:34 +0800413 {code !== 200 && loginType === 'mobile' && (
414 <LoginMessage content="验证码错误" />
415 )}
Jiarenxiang38dcb052025-03-13 16:40:09 +0800416 {type === 'mobile' && (
417 <>
418 <ProFormText
419 fieldProps={{
420 size: 'large',
Jiarenxiangefbcdd42025-06-03 17:06:34 +0800421 prefix: <MobileOutlined style={{ color: '#6cf' }} />,
422 style: { background: 'rgba(255,255,255,0.08)', color: '#fff' },
Jiarenxiang38dcb052025-03-13 16:40:09 +0800423 }}
424 name="mobile"
425 placeholder={intl.formatMessage({
426 id: 'pages.login.phoneNumber.placeholder',
427 defaultMessage: '手机号',
428 })}
429 rules={[
430 {
431 required: true,
432 message: (
433 <FormattedMessage
434 id="pages.login.phoneNumber.required"
435 defaultMessage="请输入手机号!"
436 />
437 ),
438 },
439 {
440 pattern: /^1\d{10}$/,
441 message: (
442 <FormattedMessage
443 id="pages.login.phoneNumber.invalid"
444 defaultMessage="手机号格式错误!"
445 />
446 ),
447 },
448 ]}
449 />
450 <ProFormCaptcha
451 fieldProps={{
452 size: 'large',
Jiarenxiangefbcdd42025-06-03 17:06:34 +0800453 prefix: <LockOutlined style={{ color: '#6cf' }} />,
454 style: { background: 'rgba(255,255,255,0.08)', color: '#fff' },
Jiarenxiang38dcb052025-03-13 16:40:09 +0800455 }}
456 captchaProps={{
457 size: 'large',
458 }}
459 placeholder={intl.formatMessage({
460 id: 'pages.login.captcha.placeholder',
461 defaultMessage: '请输入验证码',
462 })}
463 captchaTextRender={(timing, count) => {
464 if (timing) {
465 return `${count} ${intl.formatMessage({
466 id: 'pages.getCaptchaSecondText',
467 defaultMessage: '获取验证码',
468 })}`;
469 }
470 return intl.formatMessage({
471 id: 'pages.login.phoneLogin.getVerificationCode',
472 defaultMessage: '获取验证码',
473 });
474 }}
475 name="captcha"
476 rules={[
477 {
478 required: true,
479 message: (
480 <FormattedMessage
481 id="pages.login.captcha.required"
482 defaultMessage="请输入验证码!"
483 />
484 ),
485 },
486 ]}
487 onGetCaptcha={async (phone) => {
488 const result = await getFakeCaptcha({
489 phone,
490 });
491 if (!result) {
492 return;
493 }
494 message.success('获取验证码成功!验证码为:1234');
495 }}
496 />
497 </>
498 )}
499 <div
500 style={{
501 marginBottom: 24,
Jiarenxiangefbcdd42025-06-03 17:06:34 +0800502 color: '#b3c7f9',
Jiarenxiang38dcb052025-03-13 16:40:09 +0800503 }}
504 >
Jiarenxiangefbcdd42025-06-03 17:06:34 +0800505 <ProFormCheckbox>
506 <a
507 style={{
508 float: 'right',
509 color: '#fff',
510 fontSize: 14,
511 }}>
512 <FormattedMessage id="pages.login.rememberMe" defaultMessage="自动登录" />
513 </a>
Jiarenxiang38dcb052025-03-13 16:40:09 +0800514 </ProFormCheckbox>
515 <a
516 style={{
517 float: 'right',
Jiarenxiangefbcdd42025-06-03 17:06:34 +0800518 color: '#6cf',
Jiarenxiang38dcb052025-03-13 16:40:09 +0800519 }}
520 >
521 <FormattedMessage id="pages.login.forgotPassword" defaultMessage="忘记密码" />
522 </a>
523 </div>
524 </LoginForm>
525 </div>
526 <Footer />
527 </div>
528 );
529};
530
531export default Login;