blob: b8a44ff609b7833666bd601e47e108621da391ac [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';
Jiarenxiang56cbf662025-06-09 21:46:47 +080025import './index.less';
26import { registerUser } from '@/services/bt/index';
Jiarenxiang38dcb052025-03-13 16:40:09 +080027
28const ActionIcons = () => {
29 const langClassName = useEmotionCss(({ token }) => {
30 return {
31 marginLeft: '8px',
32 color: 'rgba(0, 0, 0, 0.2)',
33 fontSize: '24px',
34 verticalAlign: 'middle',
35 cursor: 'pointer',
36 transition: 'color 0.3s',
37 '&:hover': {
38 color: token.colorPrimaryActive,
39 },
40 };
41 });
42
43 return (
44 <>
45 <AlipayCircleOutlined key="AlipayCircleOutlined" className={langClassName} />
46 <TaobaoCircleOutlined key="TaobaoCircleOutlined" className={langClassName} />
47 <WeiboCircleOutlined key="WeiboCircleOutlined" className={langClassName} />
48 </>
49 );
50};
51
52const Lang = () => {
53 const langClassName = useEmotionCss(({ token }) => {
54 return {
55 width: 42,
56 height: 42,
57 lineHeight: '42px',
58 position: 'fixed',
59 right: 16,
60 borderRadius: token.borderRadius,
61 ':hover': {
62 backgroundColor: token.colorBgTextHover,
63 },
64 };
65 });
66
67 return (
68 <div className={langClassName} data-lang>
69 {SelectLang && <SelectLang />}
70 </div>
71 );
72};
73
74const LoginMessage: React.FC<{
75 content: string;
76}> = ({ content }) => {
77 return (
78 <Alert
79 style={{
80 marginBottom: 24,
81 }}
82 message={content}
83 type="error"
84 showIcon
85 />
86 );
87};
88
89const Login: React.FC = () => {
zhaoyumaof8f81842025-06-09 00:00:46 +080090 const [userLoginState, setUserLoginState] = useState<API.LoginResult>({ code: 200 });
Jiarenxiang38dcb052025-03-13 16:40:09 +080091 const [type, setType] = useState<string>('account');
92 const { initialState, setInitialState } = useModel('@@initialState');
93 const [captchaCode, setCaptchaCode] = useState<string>('');
94 const [uuid, setUuid] = useState<string>('');
95
96 const containerClassName = useEmotionCss(() => {
97 return {
98 display: 'flex',
99 flexDirection: 'column',
100 height: '100vh',
Jiarenxiang56cbf662025-06-09 21:46:47 +0800101 overflow: 'hidden',
Jiarenxiang38dcb052025-03-13 16:40:09 +0800102 backgroundImage:
103 "url('https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/V-_oS6r-i7wAAAAAAAAAAAAAFl94AQBr')",
104 backgroundSize: '100% 100%',
105 };
106 });
107
108 const intl = useIntl();
109
110 const getCaptchaCode = async () => {
111 const response = await getCaptchaImg();
112 const imgdata = `data:image/png;base64,${response.img}`;
113 setCaptchaCode(imgdata);
114 setUuid(response.uuid);
115 };
116
117 const fetchUserInfo = async () => {
118 const userInfo = await initialState?.fetchUserInfo?.();
119 if (userInfo) {
120 flushSync(() => {
121 setInitialState((s) => ({
122 ...s,
123 currentUser: userInfo,
124 }));
125 });
126 }
127 };
128
129 const handleSubmit = async (values: API.LoginParams) => {
130 try {
131 // 登录
132 const response = await login({ ...values, uuid });
133 if (response.code === 200) {
134 const defaultLoginSuccessMessage = intl.formatMessage({
135 id: 'pages.login.success',
136 defaultMessage: '登录成功!',
137 });
138 const current = new Date();
139 const expireTime = current.setTime(current.getTime() + 1000 * 12 * 60 * 60);
140 console.log('login response: ', response);
141 setSessionToken(response?.token, response?.token, expireTime);
142 message.success(defaultLoginSuccessMessage);
143 await fetchUserInfo();
144 console.log('login ok');
145 const urlParams = new URL(window.location.href).searchParams;
146 history.push(urlParams.get('redirect') || '/');
147 return;
148 } else {
149 console.log(response.msg);
150 clearSessionToken();
151 // 如果失败去设置用户错误信息
152 setUserLoginState({ ...response, type });
153 getCaptchaCode();
154 }
155 } catch (error) {
156 const defaultLoginFailureMessage = intl.formatMessage({
157 id: 'pages.login.failure',
158 defaultMessage: '登录失败,请重试!',
159 });
160 console.log(error);
161 message.error(defaultLoginFailureMessage);
162 }
163 };
Jiarenxiang56cbf662025-06-09 21:46:47 +0800164
Jiarenxiang38dcb052025-03-13 16:40:09 +0800165 const { code } = userLoginState;
166 const loginType = type;
167
168 useEffect(() => {
169 getCaptchaCode();
170 }, []);
171
172 return (
Jiarenxiangefbcdd42025-06-03 17:06:34 +0800173 <div
174 className={containerClassName}
175 style={{
176 backgroundImage:
Jiarenxiang56cbf662025-06-09 21:46:47 +0800177 "linear-gradient(120deg, #232526 0%, #414345 100%), url('https://images.unsplash.com/photo-1462331940025-496dfbfc7564?auto=format&fit=crop&w=1500&q=80')",
178 backgroundBlendMode: 'overlay',
Jiarenxiangefbcdd42025-06-03 17:06:34 +0800179 backgroundSize: 'cover',
180 backgroundPosition: 'center',
181 minHeight: '100vh',
182 position: 'relative',
183 overflow: 'hidden',
184 }}
185 >
Jiarenxiang38dcb052025-03-13 16:40:09 +0800186 <Helmet>
187 <title>
188 {intl.formatMessage({
189 id: 'menu.login',
190 defaultMessage: '登录页',
191 })}
192 - {Settings.title}
193 </title>
194 </Helmet>
195 <Lang />
Jiarenxiangefbcdd42025-06-03 17:06:34 +0800196 {/* 星空粒子特效层 */}
197 <div
198 style={{
199 position: 'absolute',
200 inset: 0,
201 zIndex: 0,
202 pointerEvents: 'none',
203 background: 'radial-gradient(ellipse at bottom, #1b2735 0%, #090a0f 100%)',
204 }}
205 >
Jiarenxiangefbcdd42025-06-03 17:06:34 +0800206 <svg width="100%" height="100%">
207 {[...Array(60)].map((_, i) => (
208 <circle
209 key={i}
210 cx={Math.random() * 1600}
211 cy={Math.random() * 900}
212 r={Math.random() * 1.5 + 0.5}
213 fill="#fff"
214 opacity={Math.random() * 0.8 + 0.2}
215 />
216 ))}
217 </svg>
218 </div>
Jiarenxiang38dcb052025-03-13 16:40:09 +0800219 <div
220 style={{
221 flex: '1',
222 padding: '32px 0',
Jiarenxiangefbcdd42025-06-03 17:06:34 +0800223 display: 'flex',
224 alignItems: 'center',
225 justifyContent: 'center',
226 minHeight: '100vh',
227 position: 'relative',
228 zIndex: 1,
Jiarenxiang38dcb052025-03-13 16:40:09 +0800229 }}
230 >
Jiarenxiang56cbf662025-06-09 21:46:47 +0800231 <div
232 style={{
233 background: 'rgba(30, 34, 54, 0.96)',
234 borderRadius: 20,
Jiarenxiangefbcdd42025-06-03 17:06:34 +0800235 boxShadow: '0 8px 32px 0 rgba(31, 38, 135, 0.37)',
Jiarenxiang56cbf662025-06-09 21:46:47 +0800236 border: '1px solid rgba(255,255,255,0.12)',
237 padding: '36px 24px 28px 24px',
238 minWidth: 340,
239 maxWidth: 420,
240 width: '100%',
Jiarenxiangefbcdd42025-06-03 17:06:34 +0800241 color: '#fff',
Jiarenxiang56cbf662025-06-09 21:46:47 +0800242 backdropFilter: 'blur(8px)',
243 position: 'relative',
244 transition: 'max-width 0.2s cubic-bezier(.4,2,.6,1)',
245 overflow: 'hidden',
Jiarenxiang38dcb052025-03-13 16:40:09 +0800246 }}
247 >
Jiarenxiang56cbf662025-06-09 21:46:47 +0800248 <div style={{ textAlign: 'center', marginBottom: 24 }}>
249 <img
250 src="/logo.svg"
251 alt="ThunderHub"
Jiarenxiang38dcb052025-03-13 16:40:09 +0800252 style={{
Jiarenxiang56cbf662025-06-09 21:46:47 +0800253 width: 54,
254 height: 48,
255 marginBottom: 8,
256 filter: 'drop-shadow(0 0 8px #6cf8)',
257 }}
258 />
259 <div
260 style={{
261 color: '#fff',
262 fontWeight: 700,
263 fontSize: 26,
264 letterSpacing: 2,
265 marginBottom: 4,
266 fontFamily: 'Montserrat, sans-serif',
Jiarenxiang38dcb052025-03-13 16:40:09 +0800267 }}
268 >
Jiarenxiang56cbf662025-06-09 21:46:47 +0800269 ThunderHub
270 </div>
271 <div style={{ color: '#b3c7f9', fontSize: 14, letterSpacing: 1 }}>
272 探索你的专属星球,畅享PT世界
273 <span style={{ color: '#6cf', marginLeft: 8, fontWeight: 500, fontSize: 12 }}>
274 JRX MSY ZYT HXQ LJB
275 </span>
276 </div>
Jiarenxiang38dcb052025-03-13 16:40:09 +0800277 </div>
Jiarenxiang56cbf662025-06-09 21:46:47 +0800278 <LoginForm
279 contentStyle={{
280 background: 'transparent',
281 boxShadow: 'none',
282 padding: 0,
283 color: '#fff',
284 width: '100%',
285 overflow: 'hidden',
286 }}
287 initialValues={{
288 autoLogin: true,
289 }}
290 onFinish={async (values) => {
291 if (type === 'account') {
292 await handleSubmit(values as API.LoginParams);
293 } else {
294 try {
295 const response = await registerUser(
296 values.username ?? '',
297 values.password ?? '',
298 values.inviteCode ?? ''
299 );
300 if (response.code === 0) {
301 message.success('注册成功!');
302 // 注册成功后自动跳转到登录页或自动登录
303 setType('account');
304 } else {
305 message.error(response.msg || '注册失败,请重试!');
306 clearSessionToken();
307 setUserLoginState({ ...response, type });
308 }
309 } catch (error) {
310 message.error('注册失败,请重试!');
311 }
312 }
313 }}
314 submitter={false}
315 >
316 <Tabs
317 activeKey={type}
318 onChange={setType}
319 centered
320 items={[
321 {
322 key: 'account',
323 label: (
324 <span style={{ color: '#fff', fontWeight: 500, fontSize: 16 }}>
325 {intl.formatMessage({
326 id: 'pages.login.accountLogin.tab',
327 defaultMessage: '账户密码登录',
328 })}
329 </span>
330 ),
331 },
332 {
333 key: 'mobile',
334 label: (
335 <span style={{ color: '#fff', fontWeight: 500, fontSize: 16 }}>
336 {intl.formatMessage({
337 id: 'pages.login.phoneLogin.tab',
338 defaultMessage: '注册',
339 })}
340 </span>
341 ),
342 },
343 ]}
344 style={{ marginBottom: 24 }}
345 indicatorSize={36}
346 />
347
348 {code !== 200 && loginType === 'account' && (
349 <LoginMessage
350 content={intl.formatMessage({
351 id: 'pages.login.accountLogin.errorMessage',
352 defaultMessage: '账户或密码错误',
353 })}
354 />
355 )}
356 {type === 'account' && (
357 <>
358 <ProFormText
359 name="username"
360 fieldProps={{
361 size: 'large',
362 prefix: <UserOutlined style={{ color: '#6cf' }} />,
363 style: {
364 background: 'rgba(255,255,255,0.08)',
365 color: '#fff',
366 borderRadius: 8,
367 },
368 }}
369 placeholder={intl.formatMessage({
370 id: 'pages.login.username.placeholder',
371 })}
372 rules={[
373 {
374 required: true,
375 message: (
376 <FormattedMessage
377 id="pages.login.username.required"
378 defaultMessage="请输入用户名!"
379 />
380 ),
381 },
382 ]}
383 />
384 <ProFormText.Password
385 name="password"
386 fieldProps={{
387 size: 'large',
388 prefix: <LockOutlined style={{ color: '#6cf' }} />,
389 style: {
390 background: 'rgba(255,255,255,0.08)',
391 color: '#fff',
392 borderRadius: 8,
393 },
394 }}
395 placeholder={intl.formatMessage({
396 id: 'pages.login.password.placeholder',
397 })}
398 rules={[
399 {
400 required: true,
401 message: (
402 <FormattedMessage
403 id="pages.login.password.required"
404 defaultMessage="请输入密码!"
405 />
406 ),
407 },
408 ]}
409 />
410 <Row>
411 <Col flex={3}>
412 <ProFormText
413 style={{
414 float: 'right',
415 background: 'rgba(255,255,255,0.08)',
416 color: '#fff',
417 borderRadius: 8,
418 }}
419 name="code"
420 placeholder={intl.formatMessage({
421 id: 'pages.login.captcha.placeholder',
422 defaultMessage: '请输入验证',
423 })}
424 rules={[
425 {
426 required: true,
427 message: (
428 <FormattedMessage
429 id="pages.searchTable.updateForm.ruleName.nameRules"
430 defaultMessage="请输入验证啊"
431 />
432 ),
433 },
434 ]}
435 />
436 </Col>
437 <Col flex={2}>
438 <Image
439 src={captchaCode}
440 alt="验证码"
441 style={{
442 display: 'inline-block',
443 verticalAlign: 'top',
444 cursor: 'pointer',
445 paddingLeft: '10px',
446 width: '90px',
447 borderRadius: 8,
448 boxShadow: '0 0 8px #6cf8',
449 background: '#fff',
450 }}
451 preview={false}
452 onClick={() => getCaptchaCode()}
453 />
454 </Col>
455 </Row>
456 </>
457 )}
458
459 {code !== 200 && loginType === 'mobile' && (
460 <LoginMessage content="验证码错误" />
461 )}
462 {type === 'mobile' && (
463 <>
464 <ProFormText
465 fieldProps={{
466 size: 'large',
467 prefix: <LockOutlined style={{ color: '#6cf' }} />, // 换成钥匙图标
468 style: {
469 background: 'rgba(255,255,255,0.08)',
470 color: '#fff',
471 borderRadius: 8,
472 },
473 }}
474 name="inviteCode"
475 placeholder={intl.formatMessage({
476 id: 'pages.login.inviteCode.placeholder',
477 defaultMessage: '请输入邀请码',
478 })}
479 rules={[
480 {
481 required: true,
482 message: (
483 <FormattedMessage
484 id="pages.login.inviteCode.required"
485 defaultMessage="请输入邀请码!"
486 />
487 ),
488 },
489 ]}
490 />
491 <ProFormText
492 fieldProps={{
493 size: 'large',
494 prefix: <UserOutlined style={{ color: '#6cf' }} />,
495 style: {
496 background: 'rgba(255,255,255,0.08)',
497 color: '#fff',
498 borderRadius: 8,
499 },
500 }}
501 name="username"
502 placeholder={intl.formatMessage({
503 id: 'pages.login.username.placeholder',
504 defaultMessage: '请输入用户名',
505 })}
506 rules={[
507 {
508 required: true,
509 message: (
510 <FormattedMessage
511 id="pages.login.username.required"
512 defaultMessage="请输入用户名!"
513 />
514 ),
515 },
516 ]}
517 />
518 <ProFormText.Password
519 fieldProps={{
520 size: 'large',
521 prefix: <LockOutlined style={{ color: '#6cf' }} />,
522 style: {
523 background: 'rgba(255,255,255,0.08)',
524 color: '#fff',
525 borderRadius: 8,
526 },
527 }}
528 name="password"
529 placeholder={intl.formatMessage({
530 id: 'pages.login.password.placeholder',
531 defaultMessage: '请输入密码',
532 })}
533 rules={[
534 {
535 required: true,
536 message: (
537 <FormattedMessage
538 id="pages.login.password.required"
539 defaultMessage="请输入密码!"
540 />
541 ),
542 },
543 ]}
544 />
545 </>
546 )}
547 {type === 'account' && (
548 <div
549 style={{
550 marginBottom: 24,
551 color: '#b3c7f9',
552 display: 'flex',
553 alignItems: 'center',
554 justifyContent: 'space-between',
555 fontSize: 13,
556 }}
557 >
558 <ProFormCheckbox>
559 <span style={{ color: '#fff' }}>
560 <FormattedMessage id="pages.login.rememberMe" defaultMessage="自动登录" />
561 </span>
562 </ProFormCheckbox>
563 <a
564 style={{
565 color: '#6cf',
566 }}
567 >
568 <FormattedMessage id="pages.login.forgotPassword" defaultMessage="忘记密码" />
569 </a>
570 </div>
571 )}
572 {/* 登录/注册按钮 */}
573 <div style={{ marginTop: 24, display: 'flex', gap: 16 }}>
574 {type === 'account' && (
575 <button
576 type="submit"
577 style={{
578 width: '100%',
579 background: 'linear-gradient(90deg, #6cf 0%, #3E71FF 100%)',
580 color: '#fff',
581 border: 'none',
582 borderRadius: 8,
583 padding: '12px 0',
584 fontSize: 16,
585 fontWeight: 600,
586 cursor: 'pointer',
587 boxShadow: '0 2px 8px #6cf4',
588 letterSpacing: 2,
589 }}
590 onClick={() => {
591 // 触发表单提交,登录
592 document.querySelector('form')?.dispatchEvent(new Event('submit', { cancelable: true, bubbles: true }));
593 }}
594 >
595 登录
596 </button>
597 )}
598 {type === 'mobile' && (
599 <button
600 type="button"
601 style={{
602 width: '100%',
603 background: 'linear-gradient(90deg, #6cf 0%, #3E71FF 100%)',
604 color: '#fff',
605 border: 'none',
606 borderRadius: 8,
607 padding: '12px 0',
608 fontSize: 16,
609 fontWeight: 600,
610 cursor: 'pointer',
611 boxShadow: '0 2px 8px #6cf4',
612 letterSpacing: 2,
613 }}
614 onClick={async () => {
615 // 触发表单校验并注册
616 const form = document.querySelector('form');
617 if (form) {
618 form.dispatchEvent(new Event('submit', { cancelable: true, bubbles: true }));
619 }
620 }}
621 >
622 注册
623 </button>
624 )}
625 </div>
626 </LoginForm>
627 </div>
Jiarenxiang38dcb052025-03-13 16:40:09 +0800628 </div>
629 <Footer />
630 </div>
631 );
632};
633
634export default Login;