Unverified Commit 1a5b7aca authored by ddcat1115's avatar ddcat1115 Committed by GitHub

New Login component (#147)

* add Login component

* update

* refactor

* update Login component & reactor Login page

* fix test case

* update

* update

* fix code style

* fix
parent 2e0efbb0
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Form, Button, Row, Col } from 'antd';
import omit from 'omit.js';
import styles from './index.less';
import map from './map';
const FormItem = Form.Item;
function generator({ defaultProps, defaultRules, type }) {
return (WrappedComponent) => {
return class BasicComponent extends Component {
static contextTypes = {
form: PropTypes.object,
updateActive: PropTypes.func,
};
constructor(props) {
super(props);
this.state = {
count: 0,
};
}
componentDidMount() {
if (this.context.updateActive) {
this.context.updateActive(this.props.name);
}
}
componentWillUnmount() {
clearInterval(this.interval);
}
onGetCaptcha = () => {
let count = 59;
this.setState({ count });
if (this.props.onGetCaptcha) {
this.props.onGetCaptcha();
}
this.interval = setInterval(() => {
count -= 1;
this.setState({ count });
if (count === 0) {
clearInterval(this.interval);
}
}, 1000);
}
render() {
const { getFieldDecorator } = this.context.form;
const options = {};
let otherProps = {};
const { onChange, defaultValue, rules, name, ...restProps } = this.props;
const { count } = this.state;
options.rules = rules || defaultRules;
if (onChange) {
options.onChange = onChange;
}
if (defaultValue) {
options.initialValue = defaultValue;
}
otherProps = restProps || otherProps;
if (type === 'Captcha') {
const inputProps = omit(otherProps, ['onGetCaptcha']);
return (
<FormItem>
<Row gutter={8}>
<Col span={16}>
{getFieldDecorator(name, options)(
<WrappedComponent {...defaultProps} {...inputProps} />
)}
</Col>
<Col span={8}>
<Button
disabled={count}
className={styles.getCaptcha}
size="large"
onClick={this.onGetCaptcha}
>
{count ? `${count} s` : '获取验证码'}
</Button>
</Col>
</Row>
</FormItem>
);
}
return (
<FormItem>
{getFieldDecorator(name, options)(
<WrappedComponent {...defaultProps} {...otherProps} />
)}
</FormItem>
);
}
};
};
}
const LoginItem = {};
Object.keys(map).forEach((item) => {
LoginItem[item] = generator({
defaultProps: map[item].props,
defaultRules: map[item].rules,
type: item,
})(map[item].component);
});
export default LoginItem;
import React from 'react';
import classNames from 'classnames';
import { Button, Form } from 'antd';
import styles from './index.less';
const FormItem = Form.Item;
export default ({ className, ...rest }) => {
const clsString = classNames(styles.submit, className);
return (
<FormItem>
<Button size="large" className={clsString} type="primary" htmlType="submit" {...rest} />
</FormItem>
);
};
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Tabs } from 'antd';
const { TabPane } = Tabs;
const generateId = (() => {
let i = 0;
return (prefix: string = '') => {
i += 1;
return `${prefix}${i}`;
};
})();
export default class LoginTab extends Component {
static __ANT_PRO_LOGIN_TAB = true;
static contextTypes = {
tabUtil: PropTypes.object,
};
constructor(props) {
super(props);
this.uniqueId = generateId('login-tab-');
}
componentWillMount() {
if (this.context.tabUtil) {
this.context.tabUtil.addTab(this.uniqueId);
}
}
render() {
return <TabPane {...this.props} />;
}
}
---
order: 0
title: Standard Login
---
支持账号密码及手机号登录两种模式。
````jsx
import Login from 'ant-design-pro/lib/Login';
import { Alert, Checkbox } from 'antd';
const { Tab, UserName, Password, Mobile, Captcha, Submit } = Login;
class LoginDemo extends React.Component {
state = {
notice: '',
type: 'tab2',
autoLogin: true,
}
onSubmit = (err, values) => {
console.log(`value collected ->`, {...values, autoLogin: this.state.autoLogin});
if (this.state.type === 'tab1') {
this.setState({
notice: '',
}, () => {
if (!err && (values.username !== 'admin' || values.password !== '888888')) {
setTimeout(() => {
this.setState({
notice: '账号或密码错误!',
})
}, 500);
}
})
}
}
onTabChange = (key) => {
this.setState({
type: key,
})
}
changeAutoLogin = (e) => {
this.setState({
autoLogin: e.target.checked,
})
}
render() {
return (
<Login
defaultActiveKey={this.state.type}
onTabChange={this.onTabChange}
onSubmit={this.onSubmit}
>
<Tab key="tab1" tab="账号密码登录">
{
this.state.notice &&
<Alert style={{ marginBottom: 24 }} message={this.state.notice} type="error" showIcon closable />
}
<UserName name="username" />
<Password name="password" />
</Tab>
<Tab key="tab2" tab="手机号登录">
<Mobile name="mobile" />
<Captcha onGetCaptcha={() => console.log('Get captcha!')} name="captcha" />
</Tab>
<div>
<Checkbox checked={this.state.autoLogin} onChange={this.changeAutoLogin}>自动登录</Checkbox>
<a style={{ float: 'right' }} href="">忘记密码</a>
</div>
<Submit>登录</Submit>
<div>
其他登录方式
<span className="icon icon-alipay" />
<span className="icon icon-taobao" />
<span className="icon icon-weibo" />
<a style={{ float: 'right' }} href="">注册账户</a>
</div>
</Login>
)
}
}
ReactDOM.render(<LoginDemo />, mountNode);
````
<style>
#scaffold-src-components-Login-demo-basic .icon {
display: inline-block;
width: 24px;
height: 24px;
background: url('https://gw.alipayobjects.com/zos/rmsportal/itDzjUnkelhQNsycranf.svg');
margin-left: 16px;
vertical-align: middle;
cursor: pointer;
}
#scaffold-src-components-Login-demo-basic .icon-alipay {
background-position: -24px 0;
}
#scaffold-src-components-Login-demo-basic .icon-alipay:hover {
background-position: 0 0;
}
#scaffold-src-components-Login-demo-basic .icon-taobao {
background-position: -24px -24px;
}
#scaffold-src-components-Login-demo-basic .icon-taobao:hover {
background-position: 0 -24px;
}
#scaffold-src-components-Login-demo-basic .icon-weibo {
background-position: -24px -48px;
}
#scaffold-src-components-Login-demo-basic .icon-weibo:hover {
background-position: 0 -48px;
}
</style>
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Form, Tabs } from 'antd';
import classNames from 'classnames';
import LoginItem from './LoginItem';
import LoginTab from './LoginTab';
import LoginSubmit from './LoginSubmit';
import styles from './index.less';
@Form.create()
class Login extends Component {
static defaultProps = {
className: '',
defaultActiveKey: '',
onTabChange: () => {},
onSubmit: () => {},
};
static propTypes = {
className: PropTypes.string,
defaultActiveKey: PropTypes.string,
onTabChange: PropTypes.func,
onSubmit: PropTypes.func,
};
static childContextTypes = {
tabUtil: PropTypes.object,
form: PropTypes.object,
updateActive: PropTypes.func,
};
state = {
type: this.props.defaultActiveKey,
tabs: [],
active: {},
};
getChildContext() {
return {
tabUtil: {
addTab: (id) => {
this.setState({
tabs: [...this.state.tabs, id],
});
},
removeTab: (id) => {
this.setState({
tabs: this.state.tabs.filter(currentId => currentId !== id),
});
},
},
form: this.props.form,
updateActive: (activeItem) => {
const { type, active } = this.state;
if (active[type]) {
active[type].push(activeItem);
} else {
active[type] = [activeItem];
}
this.setState({
active,
});
},
};
}
onSwitch = (type) => {
this.setState({
type,
});
this.props.onTabChange(type);
}
handleSubmit = (e) => {
e.preventDefault();
const { active, type } = this.state;
const activeFileds = active[type];
this.props.form.validateFields(activeFileds, { force: true },
(err, values) => {
this.props.onSubmit(err, values);
}
);
}
render() {
const { className, children } = this.props;
const { type, tabs } = this.state;
const TabChildren = [];
const otherChildren = [];
React.Children.forEach(children, (item) => {
// eslint-disable-next-line
if (item.type.__ANT_PRO_LOGIN_TAB) {
TabChildren.push(item);
} else {
otherChildren.push(item);
}
});
return (
<div className={classNames(className, styles.main)}>
<Form onSubmit={this.handleSubmit}>
{
tabs.length ? (
<div>
<Tabs
animated={false}
className={styles.tabs}
activeKey={type}
onChange={this.onSwitch}
>
{ TabChildren }
</Tabs>
{ otherChildren }
</div>
) : children
}
</Form>
</div>
);
}
}
Login.Tab = LoginTab;
Login.Submit = LoginSubmit;
Object.keys(LoginItem).forEach((item) => {
Login[item] = LoginItem[item];
});
export default Login;
@import "~antd/lib/style/themes/default.less";
.main {
width: 368px;
margin: 0 auto;
.tabs {
padding: 0 2px;
margin: 0 -2px;
:global {
.ant-tabs-tab {
font-size: 16px;
line-height: 24px;
}
.ant-input-affix-wrapper .ant-input:not(:first-child) {
padding-left: 34px;
}
}
}
:global {
.ant-tabs .ant-tabs-bar {
border-bottom: 0;
margin-bottom: 24px;
text-align: center;
}
.ant-form-item {
margin-bottom: 24px;
}
}
.prefixIcon {
font-size: @font-size-base;
color: @disabled-color;
}
.getCaptcha {
display: block;
width: 100%;
}
.submit {
width: 100%;
margin-top: 24px;
}
}
---
title:
en-US: Login
zh-CN: Login
subtitle: 登录
cols: 1
order: 15
---
支持多种登录方式切换,内置了几种常见的登录控件,可以灵活组合,也支持和自定义控件配合使用。
## API
### Login
参数 | 说明 | 类型 | 默认值
----|------|-----|------
defaultActiveKey | 默认激活 tab 面板的 key | String | -
onTabChange | 切换页签时的回调 | (key) => void | -
onSubmit | 点击提交时的回调 | (err, values) => void | -
### Login.Tab
参数 | 说明 | 类型 | 默认值
----|------|-----|------
key | 对应选项卡的 key | String | -
tab | 选项卡头显示文字 | ReactNode | -
### Login.UserName
参数 | 说明 | 类型 | 默认值
----|------|-----|------
name | 控件标记,提交数据中同样以此为 key | String | -
rules | 校验规则,同 Form getFieldDecorator(id, options) 中 [option.rules 的规则](getFieldDecorator(id, options)) | object[] | -
除上述属性以外,Login.UserName 还支持 antd.Input 的所有属性,并且自带默认的基础配置,包括 `placeholder` `size` `prefix` 等,这些基础配置均可被覆盖。
### Login.Password、Login.Mobile 同 Login.UserName
### Login.Captcha
参数 | 说明 | 类型 | 默认值
----|------|-----|------
onGetCaptcha | 点击获取校验码的回调 | () => void | -
除上述属性以外,Login.Captcha 支持的属性与 Login.UserName 相同。
### Login.Submit
支持 antd.Button 的所有属性。
import React from 'react';
import { Input, Icon } from 'antd';
import styles from './index.less';
const map = {
UserName: {
component: Input,
props: {
size: 'large',
prefix: <Icon type="user" className={styles.prefixIcon} />,
placeholder: 'admin',
},
rules: [{
required: true, message: '请输入账户名!',
}],
},
Password: {
component: Input,
props: {
size: 'large',
prefix: <Icon type="lock" className={styles.prefixIcon} />,
type: 'password',
placeholder: '888888',
},
rules: [{
required: true, message: '请输入密码!',
}],
},
Mobile: {
component: Input,
props: {
size: 'large',
prefix: <Icon type="mobile" className={styles.prefixIcon} />,
placeholder: '手机号',
},
rules: [{
required: true, message: '请输入手机号!',
}, {
pattern: /^1\d{10}$/, message: '手机号格式错误!',
}],
},
Captcha: {
component: Input,
props: {
size: 'large',
prefix: <Icon type="mail" className={styles.prefixIcon} />,
placeholder: '验证码',
},
rules: [{
required: true, message: '请输入验证码!',
}],
},
};
export default map;
import React, { Component } from 'react';
import { connect } from 'dva';
import { Link } from 'dva/router';
import { Form, Input, Tabs, Button, Icon, Checkbox, Row, Col, Alert } from 'antd';
import { Checkbox, Alert, Icon } from 'antd';
import Login from '../../components/Login';
import styles from './Login.less';
const FormItem = Form.Item;
const { TabPane } = Tabs;
const { Tab, UserName, Password, Mobile, Captcha, Submit } = Login;
@connect(state => ({
login: state.login,
}))
@Form.create()
export default class Login extends Component {
export default class LoginPage extends Component {
state = {
count: 0,
type: 'account',
autoLogin: true,
}
componentWillUnmount() {
clearInterval(this.interval);
}
onSwitch = (type) => {
onTabChange = (type) => {
this.setState({ type });
}
onGetCaptcha = () => {
let count = 59;
this.setState({ count });
this.interval = setInterval(() => {
count -= 1;
this.setState({ count });
if (count === 0) {
clearInterval(this.interval);
}
}, 1000);
handleSubmit = (err, values) => {
const { type } = this.state;
if (!err) {
this.props.dispatch({
type: 'login/login',
payload: {
...values,
type,
},
});
}
}
handleSubmit = (e) => {
e.preventDefault();
this.props.form.validateFields({ force: true },
(err, values) => {
if (!err) {
this.props.dispatch({
type: 'login/login',
payload: {
...values,
type: this.state.type,
},
});
}
}
);
changeAutoLogin = (e) => {
this.setState({
autoLogin: e.target.checked,
});
}
renderMessage = (message) => {
renderMessage = (content) => {
return (
<Alert
style={{ marginBottom: 24 }}
message={message}
type="error"
showIcon
/>
<Alert style={{ marginBottom: 24 }} message={content} type="error" showIcon closable />
);
}
render() {
const { form, login } = this.props;
const { getFieldDecorator } = form;
const { count, type } = this.state;
const { login } = this.props;
const { type } = this.state;
return (
<div className={styles.main}>
<Form onSubmit={this.handleSubmit}>
<Tabs animated={false} className={styles.tabs} activeKey={type} onChange={this.onSwitch}>
<TabPane tab="账户密码登录" key="account">
{
login.status === 'error' &&
login.type === 'account' &&
login.submitting === false &&
this.renderMessage('账户或密码错误')
}
<FormItem>
{getFieldDecorator('userName', {
rules: [{
required: type === 'account', message: '请输入账户名!',
}],
})(
<Input
size="large"
prefix={<Icon type="user" className={styles.prefixIcon} />}
placeholder="admin"
/>
)}
</FormItem>
<FormItem>
{getFieldDecorator('password', {
rules: [{
required: type === 'account', message: '请输入密码!',
}],
})(
<Input
size="large"
prefix={<Icon type="lock" className={styles.prefixIcon} />}
type="password"
placeholder="888888"
/>
)}
</FormItem>
</TabPane>
<TabPane tab="手机号登录" key="mobile">
{
login.status === 'error' &&
login.type === 'mobile' &&
login.submitting === false &&
this.renderMessage('验证码错误')
}
<FormItem>
{getFieldDecorator('mobile', {
rules: [{
required: type === 'mobile', message: '请输入手机号!',
}, {
pattern: /^1\d{10}$/, message: '手机号格式错误!',
}],
})(
<Input
size="large"
prefix={<Icon type="mobile" className={styles.prefixIcon} />}
placeholder="手机号"
/>
)}
</FormItem>
<FormItem>
<Row gutter={8}>
<Col span={16}>
{getFieldDecorator('captcha', {
rules: [{
required: type === 'mobile', message: '请输入验证码!',
}],
})(
<Input
size="large"
prefix={<Icon type="mail" className={styles.prefixIcon} />}
placeholder="验证码"
/>
)}
</Col>
<Col span={8}>
<Button
disabled={count}
className={styles.getCaptcha}
size="large"
onClick={this.onGetCaptcha}
>
{count ? `${count} s` : '获取验证码'}
</Button>
</Col>
</Row>
</FormItem>
</TabPane>
</Tabs>
<FormItem className={styles.additional}>
{getFieldDecorator('remember', {
valuePropName: 'checked',
initialValue: true,
})(
<Checkbox className={styles.autoLogin}>自动登录</Checkbox>
)}
<a className={styles.forgot} href="">忘记密码</a>
<Button size="large" loading={login.submitting} className={styles.submit} type="primary" htmlType="submit">
登录
</Button>
</FormItem>
</Form>
<div className={styles.other}>
其他登录方式
{/* 需要加到 Icon 中 */}
<span className={styles.iconAlipay} />
<span className={styles.iconTaobao} />
<span className={styles.iconWeibo} />
<Link className={styles.register} to="/user/register">注册账户</Link>
</div>
<Login
defaultActiveKey={type}
onTabChange={this.onTabChange}
onSubmit={this.handleSubmit}
>
<Tab key="account" tab="账户密码登录">
{
login.status === 'error' &&
login.type === 'account' &&
login.submitting === false &&
this.renderMessage('账户或密码错误')
}
<UserName name="userName" />
<Password name="password" />
</Tab>
<Tab key="mobile" tab="手机号登录">
{
login.status === 'error' &&
login.type === 'mobile' &&
login.submitting === false &&
this.renderMessage('验证码错误')
}
<Mobile name="mobile" />
<Captcha name="captcha" />
</Tab>
<div>
<Checkbox checked={this.state.autoLogin} onChange={this.changeAutoLogin}>自动登录</Checkbox>
<a style={{ float: 'right' }} href="">忘记密码</a>
</div>
<Submit>登录</Submit>
<div className={styles.other}>
其他登录方式
<Icon className={styles.icon} type="alipay-circle" />
<Icon className={styles.icon} type="taobao-circle" />
<Icon className={styles.icon} type="weibo-circle" />
<Link className={styles.register} to="/user/register">注册账户</Link>
</div>
</Login>
</div>
);
}
......
......@@ -4,92 +4,16 @@
width: 368px;
margin: 0 auto;
.tabs {
padding: 0 2px;
margin: 0 -2px;
:global {
.ant-tabs-tab {
font-size: 16px;
line-height: 24px;
}
.ant-input-affix-wrapper .ant-input:not(:first-child) {
padding-left: 34px;
}
}
}
:global {
.ant-tabs .ant-tabs-bar {
border-bottom: 0;
margin-bottom: 24px;
text-align: center;
}
.ant-form-item {
margin-bottom: 24px;
}
}
.prefixIcon {
font-size: @font-size-base;
color: @disabled-color;
}
.getCaptcha {
display: block;
width: 100%;
}
.additional {
text-align: left;
.forgot {
float: right;
}
.submit {
width: 100%;
margin-top: 24px;
}
:global {
.ant-form-item-control {
line-height: 22px;
}
}
}
.iconAlipay, .iconTaobao, .iconWeibo {
display: inline-block;
width: 24px;
height: 24px;
background: url('https://gw.alipayobjects.com/zos/rmsportal/itDzjUnkelhQNsycranf.svg');
.icon {
font-size: 24px;
color: rgba(0, 0, 0, 0.2);
margin-left: 16px;
vertical-align: middle;
cursor: pointer;
}
.iconAlipay {
background-position: -24px 0;
&:hover {
background-position: 0 0;
}
}
.iconTaobao {
background-position: -24px -24px;
&:hover {
background-position: 0 -24px;
}
}
.iconWeibo {
background-position: -24px -48px;
&:hover {
background-position: 0 -48px;
color: @primary-color;
transition: color .3s;
}
}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment