blob: 87759c32f8a6ada6127caca72bf10d7e45517689 [file] [log] [blame]
86133aaa3f5d2025-04-20 21:33:29 +08001import Footer from '@/components/Footer';
2import { getCaptchaImg, login } from '@/services/system/auth';
3import { getFakeCaptcha } from '@/services/ant-design-pro/login';
22301110e361be52025-06-08 15:24:14 +08004import { Modal, Form, Input, Button } from 'antd';
5
86133aaa3f5d2025-04-20 21:33:29 +08006import {
7 AlipayCircleOutlined,
8 LockOutlined,
9 MobileOutlined,
10 TaobaoCircleOutlined,
11 UserOutlined,
12 WeiboCircleOutlined,
13} from '@ant-design/icons';
14import {
15 LoginForm,
16 ProFormCaptcha,
17 ProFormCheckbox,
18 ProFormText,
19} from '@ant-design/pro-components';
20import { useEmotionCss } from '@ant-design/use-emotion-css';
21import { FormattedMessage, history, SelectLang, useIntl, useModel, Helmet } from '@umijs/max';
22import { Alert, Col, message, Row, Tabs, Image } from 'antd';
23import Settings from '../../../../config/defaultSettings';
24import React, { useEffect, useState } from 'react';
25import { flushSync } from 'react-dom';
26import { clearSessionToken, setSessionToken } from '@/access';
27
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 = () => {
90 const [userLoginState, setUserLoginState] = useState<API.LoginResult>({code: 200});
91 const [type, setType] = useState<string>('account');
92 const { initialState, setInitialState } = useModel('@@initialState');
93 const [captchaCode, setCaptchaCode] = useState<string>('');
94 const [uuid, setUuid] = useState<string>('');
22301110e361be52025-06-08 15:24:14 +080095const [forgotModalVisible, setForgotModalVisible] = useState(false);
96const [form] = Form.useForm();
86133aaa3f5d2025-04-20 21:33:29 +080097
98 const containerClassName = useEmotionCss(() => {
99 return {
100 display: 'flex',
101 flexDirection: 'column',
102 height: '100vh',
103 overflow: 'auto',
104 backgroundImage:
105 "url('https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/V-_oS6r-i7wAAAAAAAAAAAAAFl94AQBr')",
106 backgroundSize: '100% 100%',
107 };
108 });
109
110 const intl = useIntl();
111
112 const getCaptchaCode = async () => {
113 const response = await getCaptchaImg();
114 const imgdata = `data:image/png;base64,${response.img}`;
115 setCaptchaCode(imgdata);
116 setUuid(response.uuid);
117 };
118
119 const fetchUserInfo = async () => {
120 const userInfo = await initialState?.fetchUserInfo?.();
121 if (userInfo) {
122 flushSync(() => {
123 setInitialState((s) => ({
124 ...s,
125 currentUser: userInfo,
126 }));
127 });
128 }
129 };
130
131 const handleSubmit = async (values: API.LoginParams) => {
132 try {
133 // 登录
134 const response = await login({ ...values, uuid });
135 if (response.code === 200) {
136 const defaultLoginSuccessMessage = intl.formatMessage({
137 id: 'pages.login.success',
138 defaultMessage: '登录成功!',
139 });
140 const current = new Date();
141 const expireTime = current.setTime(current.getTime() + 1000 * 12 * 60 * 60);
142 console.log('login response: ', response);
143 setSessionToken(response?.token, response?.token, expireTime);
144 message.success(defaultLoginSuccessMessage);
145 await fetchUserInfo();
146 console.log('login ok');
147 const urlParams = new URL(window.location.href).searchParams;
148 history.push(urlParams.get('redirect') || '/');
149 return;
150 } else {
151 console.log(response.msg);
152 clearSessionToken();
153 // 如果失败去设置用户错误信息
154 setUserLoginState({ ...response, type });
155 getCaptchaCode();
156 }
157 } catch (error) {
158 const defaultLoginFailureMessage = intl.formatMessage({
159 id: 'pages.login.failure',
160 defaultMessage: '登录失败,请重试!',
161 });
162 console.log(error);
163 message.error(defaultLoginFailureMessage);
164 }
165 };
166 const { code } = userLoginState;
167 const loginType = type;
168
169 useEffect(() => {
170 getCaptchaCode();
171 }, []);
172
173 return (
174 <div className={containerClassName}>
175 <Helmet>
176 <title>
177 {intl.formatMessage({
178 id: 'menu.login',
179 defaultMessage: '登录页',
180 })}
181 - {Settings.title}
182 </title>
183 </Helmet>
184 <Lang />
185 <div
186 style={{
187 flex: '1',
188 padding: '32px 0',
189 }}
190 >
191 <LoginForm
192 contentStyle={{
193 minWidth: 280,
194 maxWidth: '75vw',
195 }}
196 logo={<img alt="logo" src="/logo.svg" />}
22301110e361be52025-06-08 15:24:14 +0800197 title="PT Station"
86133aaa3f5d2025-04-20 21:33:29 +0800198 subTitle={intl.formatMessage({ id: 'pages.layouts.userLayout.title' })}
199 initialValues={{
200 autoLogin: true,
201 }}
202 actions={[
22301110e361be52025-06-08 15:24:14 +0800203
86133aaa3f5d2025-04-20 21:33:29 +0800204 ]}
205 onFinish={async (values) => {
206 await handleSubmit(values as API.LoginParams);
207 }}
22301110e361be52025-06-08 15:24:14 +0800208 ><Tabs
209 activeKey={type}
210 onChange={setType}
211 centered
212 items={[
213 {
214 key: 'account',
215 label: intl.formatMessage({
216 id: 'pages.login.accountLogin.tab',
217 defaultMessage: '账户密码登录',
218 }),
219 },
220 ]}
221/>
86133aaa3f5d2025-04-20 21:33:29 +0800222
223 {code !== 200 && loginType === 'account' && (
224 <LoginMessage
225 content={intl.formatMessage({
226 id: 'pages.login.accountLogin.errorMessage',
22301110e361be52025-06-08 15:24:14 +0800227 defaultMessage: '账户或密码错误',
86133aaa3f5d2025-04-20 21:33:29 +0800228 })}
229 />
230 )}
231 {type === 'account' && (
232 <>
233 <ProFormText
234 name="username"
235 initialValue="admin"
236 fieldProps={{
237 size: 'large',
238 prefix: <UserOutlined />,
239 }}
240 placeholder={intl.formatMessage({
241 id: 'pages.login.username.placeholder',
242 defaultMessage: '用户名: admin',
243 })}
244 rules={[
245 {
246 required: true,
247 message: (
248 <FormattedMessage
249 id="pages.login.username.required"
250 defaultMessage="请输入用户名!"
251 />
252 ),
253 },
254 ]}
255 />
256 <ProFormText.Password
257 name="password"
258 initialValue="admin123"
259 fieldProps={{
260 size: 'large',
261 prefix: <LockOutlined />,
262 }}
263 placeholder={intl.formatMessage({
264 id: 'pages.login.password.placeholder',
265 defaultMessage: '密码: admin123',
266 })}
267 rules={[
268 {
269 required: true,
270 message: (
271 <FormattedMessage
272 id="pages.login.password.required"
273 defaultMessage="请输入密码!"
274 />
275 ),
276 },
277 ]}
278 />
279 <Row>
280 <Col flex={3}>
281 <ProFormText
282 style={{
283 float: 'right',
284 }}
285 name="code"
286 placeholder={intl.formatMessage({
287 id: 'pages.login.captcha.placeholder',
288 defaultMessage: '请输入验证',
289 })}
290 rules={[
291 {
292 required: true,
293 message: (
294 <FormattedMessage
295 id="pages.searchTable.updateForm.ruleName.nameRules"
22301110e361be52025-06-08 15:24:14 +0800296 defaultMessage="请输入验证"
86133aaa3f5d2025-04-20 21:33:29 +0800297 />
298 ),
299 },
300 ]}
301 />
302 </Col>
303 <Col flex={2}>
304 <Image
305 src={captchaCode}
306 alt="验证码"
307 style={{
308 display: 'inline-block',
309 verticalAlign: 'top',
310 cursor: 'pointer',
311 paddingLeft: '10px',
312 width: '100px',
313 }}
314 preview={false}
315 onClick={() => getCaptchaCode()}
316 />
317 </Col>
318 </Row>
319 </>
320 )}
321
322 {code !== 200 && loginType === 'mobile' && <LoginMessage content="验证码错误" />}
323 {type === 'mobile' && (
324 <>
325 <ProFormText
326 fieldProps={{
327 size: 'large',
328 prefix: <MobileOutlined />,
329 }}
330 name="mobile"
331 placeholder={intl.formatMessage({
332 id: 'pages.login.phoneNumber.placeholder',
333 defaultMessage: '手机号',
334 })}
335 rules={[
336 {
337 required: true,
338 message: (
339 <FormattedMessage
340 id="pages.login.phoneNumber.required"
341 defaultMessage="请输入手机号!"
342 />
343 ),
344 },
345 {
346 pattern: /^1\d{10}$/,
347 message: (
348 <FormattedMessage
349 id="pages.login.phoneNumber.invalid"
350 defaultMessage="手机号格式错误!"
351 />
352 ),
353 },
354 ]}
355 />
356 <ProFormCaptcha
357 fieldProps={{
358 size: 'large',
359 prefix: <LockOutlined />,
360 }}
361 captchaProps={{
362 size: 'large',
363 }}
364 placeholder={intl.formatMessage({
365 id: 'pages.login.captcha.placeholder',
366 defaultMessage: '请输入验证码',
367 })}
368 captchaTextRender={(timing, count) => {
369 if (timing) {
370 return `${count} ${intl.formatMessage({
371 id: 'pages.getCaptchaSecondText',
372 defaultMessage: '获取验证码',
373 })}`;
374 }
375 return intl.formatMessage({
376 id: 'pages.login.phoneLogin.getVerificationCode',
377 defaultMessage: '获取验证码',
378 });
379 }}
380 name="captcha"
381 rules={[
382 {
383 required: true,
384 message: (
385 <FormattedMessage
386 id="pages.login.captcha.required"
387 defaultMessage="请输入验证码!"
388 />
389 ),
390 },
391 ]}
392 onGetCaptcha={async (phone) => {
393 const result = await getFakeCaptcha({
394 phone,
395 });
396 if (!result) {
397 return;
398 }
399 message.success('获取验证码成功!验证码为:1234');
400 }}
401 />
402 </>
403 )}
404 <div
405 style={{
406 marginBottom: 24,
407 }}
408 >
409 <ProFormCheckbox noStyle name="autoLogin">
410 <FormattedMessage id="pages.login.rememberMe" defaultMessage="自动登录" />
411 </ProFormCheckbox>
22301110e361be52025-06-08 15:24:14 +0800412 <a
413 style={{ float: 'right' }}
414 onClick={() => setForgotModalVisible(true)}
415 >
416 <FormattedMessage id="pages.login.forgotPassword" defaultMessage="忘记密码?" />
417 </a>
418
86133aaa3f5d2025-04-20 21:33:29 +0800419 </div>
420 </LoginForm>
22301110e361be52025-06-08 15:24:14 +0800421<div style={{ textAlign: 'center', marginTop: 16 }}>
422 <span style={{ marginRight: 8 }}>
423 <FormattedMessage id="pages.login.noAccount" defaultMessage="没有账号?" />
424 </span>
425 <a href="http://localhost:5173/" target="_self">
426 <FormattedMessage id="pages.login.registerNow" defaultMessage="立即注册" />
427 </a>
428</div>
429
430
86133aaa3f5d2025-04-20 21:33:29 +0800431 </div>
22301110e361be52025-06-08 15:24:14 +0800432
433
434 <Modal
435 title="重置密码"
436 open={forgotModalVisible}
437 onCancel={() => setForgotModalVisible(false)}
438 footer={null}
439>
440<Form
441 form={form}
442 onFinish={async (values) => {
443 const { email, code, newPassword, confirmPassword } = values;
444
445 if (newPassword !== confirmPassword) {
446 message.error('两次密码输入不一致');
447 return;
448 }
449
450 const res = await fetch('http://127.0.0.1:6001/reset-password', {
451 method: 'POST',
452 headers: { 'Content-Type': 'application/json' },
453
454 body: JSON.stringify({ email, code, newPassword }),
455 });
456 const data = await res.json();
457
458 if (data.success) {
459 message.success('密码重置成功');
460 setForgotModalVisible(false);
461 form.resetFields();
462 } else {
463 message.error(data.message || '重置失败');
464 }
465 }}
466>
467 <Form.Item name="email" rules={[{ required: true, message: '请输入邮箱' }, { type: 'email' }]}>
468 <Input placeholder="邮箱" />
469 </Form.Item>
470
471 <Form.Item name="code" rules={[{ required: true, message: '请输入验证码' }]}>
472 <Input
473 placeholder="验证码"
474 addonAfter={
475 <a
476 onClick={async () => {
477 const email = form.getFieldValue('email');
478 if (!email) return message.warning('请先输入邮箱');
479 const res = await fetch('http://127.0.0.1:6001/send-code', {
480 method: 'POST',
481 headers: { 'Content-Type': 'application/json' },
482 body: JSON.stringify({ email }),
483 });
484 const result = await res.json();
485 if (result.success) {
486 message.success('验证码发送成功');
487 } else {
488 message.error(result.message || '验证码发送失败');
489 }
490 }}
491 >
492 获取验证码
493 </a>
494 }
495 />
496 </Form.Item>
497
498 <Form.Item
499 name="newPassword"
500 rules={[{ required: true, message: '请输入新密码' }]}
501 hasFeedback
502 >
503 <Input.Password placeholder="新密码" />
504 </Form.Item>
505
506 <Form.Item
507 name="confirmPassword"
508 dependencies={['newPassword']}
509 hasFeedback
510 rules={[
511 { required: true, message: '请确认新密码' },
512 ({ getFieldValue }) => ({
513 validator(_, value) {
514 if (!value || getFieldValue('newPassword') === value) {
515 return Promise.resolve();
516 }
517 return Promise.reject(new Error('两次密码输入不一致'));
518 },
519 }),
520 ]}
521 >
522 <Input.Password placeholder="确认密码" />
523 </Form.Item>
524
525 <Form.Item>
526 <Button type="primary" htmlType="submit" block>
527 重置密码
528 </Button>
529 </Form.Item>
530</Form>
531
532</Modal>
533
86133aaa3f5d2025-04-20 21:33:29 +0800534 <Footer />
535 </div>
22301110e361be52025-06-08 15:24:14 +0800536
537
538
539
86133aaa3f5d2025-04-20 21:33:29 +0800540 );
541};
542
543export default Login;