init
This commit is contained in:
371
src/pages/user/login/index.tsx
Normal file
371
src/pages/user/login/index.tsx
Normal file
@@ -0,0 +1,371 @@
|
||||
import { Footer } from '@/components';
|
||||
import { login } from '@/services/ant-design-pro/api';
|
||||
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 { FormattedMessage, Helmet, SelectLang, useIntl, useModel } from '@umijs/max';
|
||||
import { Alert, message, Tabs } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import React, { useState } from 'react';
|
||||
import { flushSync } from 'react-dom';
|
||||
import Settings from '../../../../config/defaultSettings';
|
||||
|
||||
const useStyles = createStyles(({ token }) => {
|
||||
return {
|
||||
action: {
|
||||
marginLeft: '8px',
|
||||
color: 'rgba(0, 0, 0, 0.2)',
|
||||
fontSize: '24px',
|
||||
verticalAlign: 'middle',
|
||||
cursor: 'pointer',
|
||||
transition: 'color 0.3s',
|
||||
'&:hover': {
|
||||
color: token.colorPrimaryActive,
|
||||
},
|
||||
},
|
||||
lang: {
|
||||
width: 42,
|
||||
height: 42,
|
||||
lineHeight: '42px',
|
||||
position: 'fixed',
|
||||
right: 16,
|
||||
borderRadius: token.borderRadius,
|
||||
':hover': {
|
||||
backgroundColor: token.colorBgTextHover,
|
||||
},
|
||||
},
|
||||
container: {
|
||||
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 ActionIcons = () => {
|
||||
const { styles } = useStyles();
|
||||
|
||||
return (
|
||||
<>
|
||||
<AlipayCircleOutlined key="AlipayCircleOutlined" className={styles.action} />
|
||||
<TaobaoCircleOutlined key="TaobaoCircleOutlined" className={styles.action} />
|
||||
<WeiboCircleOutlined key="WeiboCircleOutlined" className={styles.action} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Lang = () => {
|
||||
const { styles } = useStyles();
|
||||
|
||||
return (
|
||||
<div className={styles.lang} 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>({});
|
||||
const [type, setType] = useState<string>('account');
|
||||
const { initialState, setInitialState } = useModel('@@initialState');
|
||||
const { styles } = useStyles();
|
||||
const intl = useIntl();
|
||||
|
||||
const fetchUserInfo = async () => {
|
||||
const userInfo = await initialState?.fetchUserInfo?.();
|
||||
if (userInfo) {
|
||||
flushSync(() => {
|
||||
setInitialState((s) => ({
|
||||
...s,
|
||||
currentUser: userInfo,
|
||||
}));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (values: API.LoginParams) => {
|
||||
try {
|
||||
// 登录
|
||||
const msg = await login({ ...values, type });
|
||||
if (msg.status === 'ok') {
|
||||
const defaultLoginSuccessMessage = intl.formatMessage({
|
||||
id: 'pages.login.success',
|
||||
defaultMessage: '登录成功!',
|
||||
});
|
||||
message.success(defaultLoginSuccessMessage);
|
||||
await fetchUserInfo();
|
||||
const urlParams = new URL(window.location.href).searchParams;
|
||||
window.location.href = urlParams.get('redirect') || '/';
|
||||
return;
|
||||
}
|
||||
console.log(msg);
|
||||
// 如果失败去设置用户错误信息
|
||||
setUserLoginState(msg);
|
||||
} catch (error) {
|
||||
const defaultLoginFailureMessage = intl.formatMessage({
|
||||
id: 'pages.login.failure',
|
||||
defaultMessage: '登录失败,请重试!',
|
||||
});
|
||||
console.log(error);
|
||||
message.error(defaultLoginFailureMessage);
|
||||
}
|
||||
};
|
||||
const { status, type: loginType } = userLoginState;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<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="Ant Design"
|
||||
subTitle={intl.formatMessage({ id: 'pages.layouts.userLayout.title' })}
|
||||
initialValues={{
|
||||
autoLogin: true,
|
||||
}}
|
||||
actions={[
|
||||
<FormattedMessage
|
||||
key="loginWith"
|
||||
id="pages.login.loginWith"
|
||||
defaultMessage="其他登录方式"
|
||||
/>,
|
||||
<ActionIcons key="icons" />,
|
||||
]}
|
||||
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: '账户密码登录',
|
||||
}),
|
||||
},
|
||||
{
|
||||
key: 'mobile',
|
||||
label: intl.formatMessage({
|
||||
id: 'pages.login.phoneLogin.tab',
|
||||
defaultMessage: '手机号登录',
|
||||
}),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{status === 'error' && loginType === 'account' && (
|
||||
<LoginMessage
|
||||
content={intl.formatMessage({
|
||||
id: 'pages.login.accountLogin.errorMessage',
|
||||
defaultMessage: '账户或密码错误(admin/ant.design)',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{type === 'account' && (
|
||||
<>
|
||||
<ProFormText
|
||||
name="username"
|
||||
fieldProps={{
|
||||
size: 'large',
|
||||
prefix: <UserOutlined />,
|
||||
}}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'pages.login.username.placeholder',
|
||||
defaultMessage: '用户名: admin or user',
|
||||
})}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: (
|
||||
<FormattedMessage
|
||||
id="pages.login.username.required"
|
||||
defaultMessage="请输入用户名!"
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<ProFormText.Password
|
||||
name="password"
|
||||
fieldProps={{
|
||||
size: 'large',
|
||||
prefix: <LockOutlined />,
|
||||
}}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'pages.login.password.placeholder',
|
||||
defaultMessage: '密码: ant.design',
|
||||
})}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: (
|
||||
<FormattedMessage
|
||||
id="pages.login.password.required"
|
||||
defaultMessage="请输入密码!"
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{status === 'error' && 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',
|
||||
}}
|
||||
>
|
||||
<FormattedMessage id="pages.login.forgotPassword" defaultMessage="忘记密码" />
|
||||
</a>
|
||||
</div>
|
||||
</LoginForm>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
96
src/pages/user/login/login.test.tsx
Normal file
96
src/pages/user/login/login.test.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import { TestBrowser } from '@@/testBrowser';
|
||||
import { act, fireEvent, render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
// @ts-ignore
|
||||
import { startMock } from '@@/requestRecordMock';
|
||||
|
||||
const waitTime = (time: number = 100) => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(true);
|
||||
}, time);
|
||||
});
|
||||
};
|
||||
|
||||
let server: {
|
||||
close: () => void;
|
||||
};
|
||||
|
||||
describe('Login Page', () => {
|
||||
beforeAll(async () => {
|
||||
server = await startMock({
|
||||
port: 8000,
|
||||
scene: 'login',
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
server?.close();
|
||||
});
|
||||
|
||||
it('should show login form', async () => {
|
||||
const historyRef = React.createRef<any>();
|
||||
const rootContainer = render(
|
||||
<TestBrowser
|
||||
historyRef={historyRef}
|
||||
location={{
|
||||
pathname: '/user/login',
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
await rootContainer.findAllByText('Ant Design');
|
||||
|
||||
act(() => {
|
||||
historyRef.current?.push('/user/login');
|
||||
});
|
||||
|
||||
expect(rootContainer.baseElement?.querySelector('.ant-pro-form-login-desc')?.textContent).toBe(
|
||||
'Ant Design is the most influential web design specification in Xihu district',
|
||||
);
|
||||
|
||||
expect(rootContainer.asFragment()).toMatchSnapshot();
|
||||
|
||||
rootContainer.unmount();
|
||||
});
|
||||
|
||||
it('should login success', async () => {
|
||||
const historyRef = React.createRef<any>();
|
||||
const rootContainer = render(
|
||||
<TestBrowser
|
||||
historyRef={historyRef}
|
||||
location={{
|
||||
pathname: '/user/login',
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
await rootContainer.findAllByText('Ant Design');
|
||||
|
||||
const userNameInput = await rootContainer.findByPlaceholderText('Username: admin or user');
|
||||
|
||||
act(() => {
|
||||
fireEvent.change(userNameInput, { target: { value: 'admin' } });
|
||||
});
|
||||
|
||||
const passwordInput = await rootContainer.findByPlaceholderText('Password: ant.design');
|
||||
|
||||
act(() => {
|
||||
fireEvent.change(passwordInput, { target: { value: 'ant.design' } });
|
||||
});
|
||||
|
||||
await (await rootContainer.findByText('Login')).click();
|
||||
|
||||
// 等待接口返回结果
|
||||
await waitTime(5000);
|
||||
|
||||
await rootContainer.findAllByText('Ant Design Pro');
|
||||
|
||||
expect(rootContainer.asFragment()).toMatchSnapshot();
|
||||
|
||||
await waitTime(2000);
|
||||
|
||||
rootContainer.unmount();
|
||||
});
|
||||
});
|
||||
38
src/pages/user/register-result/index.tsx
Normal file
38
src/pages/user/register-result/index.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Link, useSearchParams } from '@umijs/max';
|
||||
import { Button, Result } from 'antd';
|
||||
import React from 'react';
|
||||
import useStyles from './style.style';
|
||||
|
||||
const RegisterResult: React.FC<Record<string, unknown>> = () => {
|
||||
const { styles } = useStyles();
|
||||
const [params] = useSearchParams();
|
||||
|
||||
const actions = (
|
||||
<div className={styles.actions}>
|
||||
<a href="">
|
||||
<Button size="large" type="primary">
|
||||
<span>查看邮箱</span>
|
||||
</Button>
|
||||
</a>
|
||||
<Link to="/">
|
||||
<Button size="large">返回首页</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
|
||||
const email = params?.get('account') || 'AntDesign@example.com';
|
||||
return (
|
||||
<Result
|
||||
className={styles.registerResult}
|
||||
status="success"
|
||||
title={
|
||||
<div className={styles.title}>
|
||||
<span>你的账户:{email} 注册成功</span>
|
||||
</div>
|
||||
}
|
||||
subTitle="激活邮件已发送到你的邮箱中,邮件有效期为24小时。请及时登录邮箱,点击邮件中的链接激活帐户。"
|
||||
extra={actions}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default RegisterResult;
|
||||
26
src/pages/user/register-result/style.style.ts
Normal file
26
src/pages/user/register-result/style.style.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { createStyles } from 'antd-style';
|
||||
|
||||
const useStyles = createStyles(() => {
|
||||
return {
|
||||
registerResult: {
|
||||
width: '800px',
|
||||
minHeight: '400px',
|
||||
margin: 'auto',
|
||||
padding: '80px',
|
||||
background: 'none',
|
||||
},
|
||||
anticon: {
|
||||
fontSize: '64px',
|
||||
},
|
||||
title: {
|
||||
marginTop: '32px',
|
||||
fontSize: '20px',
|
||||
lineHeight: '28px',
|
||||
},
|
||||
actions: {
|
||||
marginTop: '40px',
|
||||
'a + a': { marginLeft: '8px' },
|
||||
},
|
||||
};
|
||||
});
|
||||
export default useStyles;
|
||||
9
src/pages/user/register/_mock.ts
Normal file
9
src/pages/user/register/_mock.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { Request, Response } from 'express';
|
||||
|
||||
export default {
|
||||
'POST /api/register': (_: Request, res: Response) => {
|
||||
res.send({
|
||||
data: { status: 'ok', currentAuthority: 'user' },
|
||||
});
|
||||
},
|
||||
};
|
||||
292
src/pages/user/register/index.tsx
Normal file
292
src/pages/user/register/index.tsx
Normal file
@@ -0,0 +1,292 @@
|
||||
import { history, Link, useRequest } from '@umijs/max';
|
||||
import { Button, Col, Form, Input, message, Popover, Progress, Row, Select, Space } from 'antd';
|
||||
import type { Store } from 'antd/es/form/interface';
|
||||
import type { FC } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { StateType } from './service';
|
||||
import { fakeRegister } from './service';
|
||||
import useStyles from './style.style';
|
||||
|
||||
const FormItem = Form.Item;
|
||||
const { Option } = Select;
|
||||
|
||||
const passwordProgressMap: {
|
||||
ok: 'success';
|
||||
pass: 'normal';
|
||||
poor: 'exception';
|
||||
} = {
|
||||
ok: 'success',
|
||||
pass: 'normal',
|
||||
poor: 'exception',
|
||||
};
|
||||
const Register: FC = () => {
|
||||
const { styles } = useStyles();
|
||||
const [count, setCount]: [number, any] = useState(0);
|
||||
const [open, setVisible]: [boolean, any] = useState(false);
|
||||
const [prefix, setPrefix]: [string, any] = useState('86');
|
||||
const [popover, setPopover]: [boolean, any] = useState(false);
|
||||
const confirmDirty = false;
|
||||
let interval: number | undefined;
|
||||
|
||||
const passwordStatusMap = {
|
||||
ok: (
|
||||
<div className={styles.success}>
|
||||
<span>强度:强</span>
|
||||
</div>
|
||||
),
|
||||
pass: (
|
||||
<div className={styles.warning}>
|
||||
<span>强度:中</span>
|
||||
</div>
|
||||
),
|
||||
poor: (
|
||||
<div className={styles.error}>
|
||||
<span>强度:太短</span>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
const [form] = Form.useForm();
|
||||
useEffect(
|
||||
() => () => {
|
||||
clearInterval(interval);
|
||||
},
|
||||
[interval],
|
||||
);
|
||||
const onGetCaptcha = () => {
|
||||
let counts = 59;
|
||||
setCount(counts);
|
||||
interval = window.setInterval(() => {
|
||||
counts -= 1;
|
||||
setCount(counts);
|
||||
if (counts === 0) {
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 1000);
|
||||
};
|
||||
const getPasswordStatus = () => {
|
||||
const value = form.getFieldValue('password');
|
||||
if (value && value.length > 9) {
|
||||
return 'ok';
|
||||
}
|
||||
if (value && value.length > 5) {
|
||||
return 'pass';
|
||||
}
|
||||
return 'poor';
|
||||
};
|
||||
const { loading: submitting, run: register } = useRequest<{
|
||||
data: StateType;
|
||||
}>(fakeRegister, {
|
||||
manual: true,
|
||||
onSuccess: (data, params) => {
|
||||
if (data.status === 'ok') {
|
||||
message.success('注册成功!');
|
||||
history.push({
|
||||
pathname: `/user/register-result?account=${params[0].email}`,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
const onFinish = (values: Store) => {
|
||||
register(values);
|
||||
};
|
||||
const checkConfirm = (_: any, value: string) => {
|
||||
const promise = Promise;
|
||||
if (value && value !== form.getFieldValue('password')) {
|
||||
return promise.reject('两次输入的密码不匹配!');
|
||||
}
|
||||
return promise.resolve();
|
||||
};
|
||||
const checkPassword = (_: any, value: string) => {
|
||||
const promise = Promise;
|
||||
// 没有值的情况
|
||||
if (!value) {
|
||||
setVisible(!!value);
|
||||
return promise.reject('请输入密码!');
|
||||
}
|
||||
// 有值的情况
|
||||
if (!open) {
|
||||
setVisible(!!value);
|
||||
}
|
||||
setPopover(!popover);
|
||||
if (value.length < 6) {
|
||||
return promise.reject('');
|
||||
}
|
||||
if (value && confirmDirty) {
|
||||
form.validateFields(['confirm']);
|
||||
}
|
||||
return promise.resolve();
|
||||
};
|
||||
const changePrefix = (value: string) => {
|
||||
setPrefix(value);
|
||||
};
|
||||
const renderPasswordProgress = () => {
|
||||
const value = form.getFieldValue('password');
|
||||
const passwordStatus = getPasswordStatus();
|
||||
return value && value.length ? (
|
||||
<div className={styles[`progress-${passwordStatus}`]}>
|
||||
<Progress
|
||||
status={passwordProgressMap[passwordStatus]}
|
||||
strokeWidth={6}
|
||||
percent={value.length * 10 > 100 ? 100 : value.length * 10}
|
||||
showInfo={false}
|
||||
/>
|
||||
</div>
|
||||
) : null;
|
||||
};
|
||||
return (
|
||||
<div className={styles.main}>
|
||||
<h3>注册</h3>
|
||||
<Form form={form} name="UserRegister" onFinish={onFinish}>
|
||||
<FormItem
|
||||
name="email"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: '请输入邮箱地址!',
|
||||
},
|
||||
{
|
||||
type: 'email',
|
||||
message: '邮箱地址格式错误!',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input size="large" placeholder="邮箱" />
|
||||
</FormItem>
|
||||
<Popover
|
||||
getPopupContainer={(node) => {
|
||||
if (node && node.parentNode) {
|
||||
return node.parentNode as HTMLElement;
|
||||
}
|
||||
return node;
|
||||
}}
|
||||
content={
|
||||
open && (
|
||||
<div
|
||||
style={{
|
||||
padding: '4px 0',
|
||||
}}
|
||||
>
|
||||
{passwordStatusMap[getPasswordStatus()]}
|
||||
{renderPasswordProgress()}
|
||||
<div
|
||||
style={{
|
||||
marginTop: 10,
|
||||
}}
|
||||
>
|
||||
<span>请至少输入 6 个字符。请不要使用容易被猜到的密码。</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
overlayStyle={{
|
||||
width: 240,
|
||||
}}
|
||||
placement="right"
|
||||
open={open}
|
||||
>
|
||||
<FormItem
|
||||
name="password"
|
||||
className={
|
||||
form.getFieldValue('password') &&
|
||||
form.getFieldValue('password').length > 0 &&
|
||||
styles.password
|
||||
}
|
||||
rules={[
|
||||
{
|
||||
validator: checkPassword,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input size="large" type="password" placeholder="至少6位密码,区分大小写" />
|
||||
</FormItem>
|
||||
</Popover>
|
||||
<FormItem
|
||||
name="confirm"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: '确认密码',
|
||||
},
|
||||
{
|
||||
validator: checkConfirm,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input size="large" type="password" placeholder="确认密码" />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
name="mobile"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: '请输入手机号!',
|
||||
},
|
||||
{
|
||||
pattern: /^\d{11}$/,
|
||||
message: '手机号格式错误!',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Space.Compact style={{ width: '100%' }}>
|
||||
<Select
|
||||
size="large"
|
||||
value={prefix}
|
||||
onChange={changePrefix}
|
||||
style={{
|
||||
width: '30%',
|
||||
}}
|
||||
>
|
||||
<Option value="86">+86</Option>
|
||||
<Option value="87">+87</Option>
|
||||
</Select>
|
||||
|
||||
<Input size="large" placeholder="手机号" />
|
||||
</Space.Compact>
|
||||
</FormItem>
|
||||
<Row gutter={8}>
|
||||
<Col span={16}>
|
||||
<FormItem
|
||||
name="captcha"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: '请输入验证码!',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input size="large" placeholder="验证码" />
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Button
|
||||
size="large"
|
||||
disabled={!!count}
|
||||
className={styles.getCaptcha}
|
||||
onClick={onGetCaptcha}
|
||||
>
|
||||
{count ? `${count} s` : '获取验证码'}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
<FormItem>
|
||||
<div className={styles.footer}>
|
||||
<Button
|
||||
size="large"
|
||||
loading={submitting}
|
||||
className={styles.submit}
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
>
|
||||
<span>注册</span>
|
||||
</Button>
|
||||
<Link to="/user/login">
|
||||
<span>使用已有账户登录</span>
|
||||
</Link>
|
||||
</div>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default Register;
|
||||
22
src/pages/user/register/service.ts
Normal file
22
src/pages/user/register/service.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { request } from '@umijs/max';
|
||||
|
||||
export interface StateType {
|
||||
status?: 'ok' | 'error';
|
||||
currentAuthority?: 'user' | 'guest' | 'admin';
|
||||
}
|
||||
|
||||
export interface UserRegisterParams {
|
||||
mail: string;
|
||||
password: string;
|
||||
confirm: string;
|
||||
mobile: string;
|
||||
captcha: string;
|
||||
prefix: string;
|
||||
}
|
||||
|
||||
export async function fakeRegister(params: UserRegisterParams) {
|
||||
return request('/api/register', {
|
||||
method: 'POST',
|
||||
data: params,
|
||||
});
|
||||
}
|
||||
46
src/pages/user/register/style.style.ts
Normal file
46
src/pages/user/register/style.style.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { createStyles } from 'antd-style';
|
||||
|
||||
const useStyles = createStyles(({ token }) => {
|
||||
return {
|
||||
main: {
|
||||
width: '368px',
|
||||
margin: '0 auto',
|
||||
h3: { marginBottom: '20px', fontSize: '16px' },
|
||||
},
|
||||
password: {
|
||||
marginBottom: '24px',
|
||||
'.ant-form-item-explain': { display: 'none' },
|
||||
},
|
||||
getCaptcha: {
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
},
|
||||
|
||||
footer: {
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
submit: {
|
||||
width: '50%',
|
||||
},
|
||||
success: {
|
||||
transition: 'color 0.3s',
|
||||
color: token.colorSuccess,
|
||||
},
|
||||
warning: {
|
||||
transition: 'color 0.3s',
|
||||
color: token.colorWarning,
|
||||
},
|
||||
error: {
|
||||
transition: 'color 0.3s',
|
||||
color: token.colorError,
|
||||
},
|
||||
'progress-pass > .progress': {
|
||||
'.ant-progress-bg': { backgroundColor: token.colorWarning },
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export default useStyles;
|
||||
Reference in New Issue
Block a user