| import Footer from '@/components/Footer'; |
| import { getCaptchaImg, login } from '@/services/system/auth'; |
| import { getFakeCaptcha } from '@/services/ant-design-pro/login'; |
| import { Modal, Form, Input, Button } from 'antd'; |
| |
| 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 [forgotModalVisible, setForgotModalVisible] = useState(false); |
| const [form] = Form.useForm(); |
| |
| 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}> |
| <Helmet> |
| <title> |
| {intl.formatMessage({ |
| id: 'menu.login', |
| defaultMessage: '登录页', |
| })} |
| - {Settings.title} |
| </title> |
| </Helmet> |
| <Lang /> |
| <div |
| style={{ |
| flex: '1', |
| padding: '32px 0', |
| }} |
| > |
| <LoginForm |
| contentStyle={{ |
| minWidth: 280, |
| maxWidth: '75vw', |
| }} |
| logo={<img alt="logo" src="/logo.svg" />} |
| title="PT Station" |
| subTitle={intl.formatMessage({ id: 'pages.layouts.userLayout.title' })} |
| initialValues={{ |
| autoLogin: true, |
| }} |
| actions={[ |
| |
| ]} |
| onFinish={async (values) => { |
| await handleSubmit(values as API.LoginParams); |
| }} |
| ><Tabs |
| activeKey={type} |
| onChange={setType} |
| centered |
| items={[ |
| { |
| key: 'account', |
| label: intl.formatMessage({ |
| id: 'pages.login.accountLogin.tab', |
| defaultMessage: '账户密码登录', |
| }), |
| }, |
| ]} |
| /> |
| |
| {code !== 200 && loginType === 'account' && ( |
| <LoginMessage |
| content={intl.formatMessage({ |
| id: 'pages.login.accountLogin.errorMessage', |
| defaultMessage: '账户或密码错误', |
| })} |
| /> |
| )} |
| {type === 'account' && ( |
| <> |
| <ProFormText |
| name="username" |
| initialValue="admin" |
| fieldProps={{ |
| size: 'large', |
| prefix: <UserOutlined />, |
| }} |
| 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 />, |
| }} |
| 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', |
| }} |
| 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', |
| }} |
| preview={false} |
| onClick={() => getCaptchaCode()} |
| /> |
| </Col> |
| </Row> |
| </> |
| )} |
| |
| {code !== 200 && loginType === 'mobile' && <LoginMessage content="验证码错误" />} |
| {type === 'mobile' && ( |
| <> |
| <ProFormText |
| fieldProps={{ |
| size: 'large', |
| prefix: <MobileOutlined />, |
| }} |
| 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 />, |
| }} |
| 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, |
| }} |
| > |
| <ProFormCheckbox noStyle name="autoLogin"> |
| <FormattedMessage id="pages.login.rememberMe" defaultMessage="自动登录" /> |
| </ProFormCheckbox> |
| <a |
| style={{ float: 'right' }} |
| onClick={() => setForgotModalVisible(true)} |
| > |
| <FormattedMessage id="pages.login.forgotPassword" defaultMessage="忘记密码?" /> |
| </a> |
| |
| </div> |
| </LoginForm> |
| <div style={{ textAlign: 'center', marginTop: 16 }}> |
| <span style={{ marginRight: 8 }}> |
| <FormattedMessage id="pages.login.noAccount" defaultMessage="没有账号?" /> |
| </span> |
| <a href="http://localhost:5173/" target="_self"> |
| <FormattedMessage id="pages.login.registerNow" defaultMessage="立即注册" /> |
| </a> |
| </div> |
| |
| |
| </div> |
| |
| |
| <Modal |
| title="重置密码" |
| open={forgotModalVisible} |
| onCancel={() => setForgotModalVisible(false)} |
| footer={null} |
| > |
| <Form |
| form={form} |
| onFinish={async (values) => { |
| const { email, code, newPassword, confirmPassword } = values; |
| |
| if (newPassword !== confirmPassword) { |
| message.error('两次密码输入不一致'); |
| return; |
| } |
| |
| const res = await fetch('http://127.0.0.1:6001/reset-password', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| |
| body: JSON.stringify({ email, code, newPassword }), |
| }); |
| const data = await res.json(); |
| |
| if (data.success) { |
| message.success('密码重置成功'); |
| setForgotModalVisible(false); |
| form.resetFields(); |
| } else { |
| message.error(data.message || '重置失败'); |
| } |
| }} |
| > |
| <Form.Item name="email" rules={[{ required: true, message: '请输入邮箱' }, { type: 'email' }]}> |
| <Input placeholder="邮箱" /> |
| </Form.Item> |
| |
| <Form.Item name="code" rules={[{ required: true, message: '请输入验证码' }]}> |
| <Input |
| placeholder="验证码" |
| addonAfter={ |
| <a |
| onClick={async () => { |
| const email = form.getFieldValue('email'); |
| if (!email) return message.warning('请先输入邮箱'); |
| const res = await fetch('http://127.0.0.1:6001/send-code', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ email }), |
| }); |
| const result = await res.json(); |
| if (result.success) { |
| message.success('验证码发送成功'); |
| } else { |
| message.error(result.message || '验证码发送失败'); |
| } |
| }} |
| > |
| 获取验证码 |
| </a> |
| } |
| /> |
| </Form.Item> |
| |
| <Form.Item |
| name="newPassword" |
| rules={[{ required: true, message: '请输入新密码' }]} |
| hasFeedback |
| > |
| <Input.Password placeholder="新密码" /> |
| </Form.Item> |
| |
| <Form.Item |
| name="confirmPassword" |
| dependencies={['newPassword']} |
| hasFeedback |
| rules={[ |
| { required: true, message: '请确认新密码' }, |
| ({ getFieldValue }) => ({ |
| validator(_, value) { |
| if (!value || getFieldValue('newPassword') === value) { |
| return Promise.resolve(); |
| } |
| return Promise.reject(new Error('两次密码输入不一致')); |
| }, |
| }), |
| ]} |
| > |
| <Input.Password placeholder="确认密码" /> |
| </Form.Item> |
| |
| <Form.Item> |
| <Button type="primary" htmlType="submit" block> |
| 重置密码 |
| </Button> |
| </Form.Item> |
| </Form> |
| |
| </Modal> |
| |
| <Footer /> |
| </div> |
| |
| |
| |
| |
| ); |
| }; |
| |
| export default Login; |