Browse Source
* add Login component * update * refactor * update Login component & reactor Login page * fix test case * update * update * fix code style * fixpull/524/head
committed by
GitHub
11 changed files with 607 additions and 236 deletions
@ -0,0 +1,104 @@ |
|||
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; |
|||
@ -0,0 +1,15 @@ |
|||
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> |
|||
); |
|||
}; |
|||
@ -0,0 +1,32 @@ |
|||
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} />; |
|||
} |
|||
} |
|||
@ -0,0 +1,113 @@ |
|||
--- |
|||
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> |
|||
@ -0,0 +1,121 @@ |
|||
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; |
|||
@ -0,0 +1,47 @@ |
|||
@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; |
|||
} |
|||
} |
|||
@ -0,0 +1,51 @@ |
|||
--- |
|||
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 的所有属性。 |
|||
|
|||
@ -0,0 +1,55 @@ |
|||
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; |
|||
Loading…
Reference in new issue