diff --git a/package.json b/package.json
index fc6f3c9e63..515bae9442 100755
--- a/package.json
+++ b/package.json
@@ -32,6 +32,7 @@
"lodash.clonedeep": "^4.5.0",
"moment": "^2.19.1",
"numeral": "^2.0.6",
+ "omit.js": "^1.0.0",
"prop-types": "^15.5.10",
"qs": "^6.5.0",
"react": "^16.0.0",
diff --git a/src/components/Login/LoginItem.js b/src/components/Login/LoginItem.js
new file mode 100644
index 0000000000..0371b5613a
--- /dev/null
+++ b/src/components/Login/LoginItem.js
@@ -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.itemKey);
+ }
+ }
+ 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, itemKey, ...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 (
+
+
+
+ {getFieldDecorator(itemKey, options)(
+
+ )}
+
+
+
+
+
+
+ );
+ }
+ return (
+
+ {getFieldDecorator(itemKey, options)(
+
+ )}
+
+ );
+ }
+ };
+ };
+}
+
+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;
diff --git a/src/components/Login/LoginSubmit.js b/src/components/Login/LoginSubmit.js
new file mode 100644
index 0000000000..8770a97386
--- /dev/null
+++ b/src/components/Login/LoginSubmit.js
@@ -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 (
+
+
+
+ );
+};
diff --git a/src/components/Login/LoginTab.js b/src/components/Login/LoginTab.js
new file mode 100644
index 0000000000..e577b8669c
--- /dev/null
+++ b/src/components/Login/LoginTab.js
@@ -0,0 +1,34 @@
+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 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() {
+ const { tab, children, ...restProps } = this.props;
+ return (
+ { children }
+ );
+ }
+}
diff --git a/src/components/Login/demo/basic.md b/src/components/Login/demo/basic.md
index c3349f9f5a..479527b06b 100644
--- a/src/components/Login/demo/basic.md
+++ b/src/components/Login/demo/basic.md
@@ -1,103 +1,80 @@
---
order: 0
-title: 标准登录框
+title: Standard Login
---
支持账号密码及手机号登录两种模式。
````jsx
import Login from 'ant-design-pro/lib/Login';
-import { Icon, Checkbox, Alert } from 'antd';
+import { Alert, Checkbox } from 'antd';
-const data = [{
- loginType: '账户密码登录',
- key: 'account',
- inputControls: [{
- key: 'userName',
- type: 'userName',
- }, {
- key: 'password',
- type: 'password',
- }],
-}, {
- loginType: '手机号登录',
- key: 'mobile',
- inputControls: [{
- key: 'mobile',
- type: 'mobile',
- }, {
- key: 'captcha',
- type: 'captcha',
- }],
-}]
-
-const extra = (
-
-);
-
-const moreLoginTypes = {
- types: (
-
-
-
-
-
- ),
-};
+const { Tab, UserName, Password, Mobile, Captcha, Submit } = Login;
class LoginDemo extends React.Component {
- constructor(props) {
- super(props);
- this.state = {
- notice: {},
- key: 'account',
- };
+ state = {
+ notice: '',
+ type: 'tab2',
+ autoLogin: true,
}
onSubmit = (err, values) => {
- console.log(err, values);
- if (!err) {
+ console.log(`value collected ->`, {...values, autoLogin: this.state.autoLogin});
+ if (this.state.type === 'tab1') {
this.setState({
- notice: {},
+ notice: '',
}, () => {
- setTimeout(() => {
- if (this.state.key === 'account' && (values.userName !== 'admin' || values.password !== '888888')) {
- const { notice } = this.state;
- notice.message = '账号或密码错误!';
- notice.type = 'error';
- notice.closable = true;
- notice.showIcon = true;
+ if (!err && (values.username !== 'admin' || values.password !== '888888')) {
+ setTimeout(() => {
this.setState({
- notice,
+ notice: '账号或密码错误!',
})
- }
- }, 500);
+ }, 500);
+ }
})
}
}
onTabChange = (key) => {
this.setState({
- key,
- notice: {},
- });
+ type: key,
+ })
+ }
+ changeAutoLogin = (e) => {
+ this.setState({
+ autoLogin: e.target.checked,
+ })
}
render() {
return (
-
- {console.log('clicked!')}}
- />
-
+
+
+ {
+ this.state.notice &&
+
+ }
+
+
+
+
+
+ console.log('Get captcha!')} itemKey="captcha" />
+
+
+ 登录
+
+
)
}
}
diff --git a/src/components/Login/index.js b/src/components/Login/index.js
index e786649377..e55397b5b6 100644
--- a/src/components/Login/index.js
+++ b/src/components/Login/index.js
@@ -1,42 +1,63 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
-import { Link } from 'dva/router';
-import { Form, Tabs, Button, Row, Col, Alert } from 'antd';
+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';
-import map from './map';
-
-const FormItem = Form.Item;
-const { TabPane } = Tabs;
@Form.create()
-export default class Login extends Component {
+class Login extends Component {
static defaultProps = {
className: '',
- data: [],
- extra: null,
- moreLoginTypes: null,
- register: null,
+ defaultActiveKey: '',
onTabChange: () => {},
onSubmit: () => {},
- onGetCaptcha: () => {},
};
static propTypes = {
className: PropTypes.string,
- data: PropTypes.array,
- extra: PropTypes.node,
- moreLoginTypes: PropTypes.object,
- register: PropTypes.object,
+ defaultActiveKey: PropTypes.string,
onTabChange: PropTypes.func,
onSubmit: PropTypes.func,
- onGetCaptcha: PropTypes.func,
+ };
+ static childContextTypes = {
+ tabUtil: PropTypes.object,
+ form: PropTypes.object,
+ updateActive: PropTypes.func,
};
state = {
- count: 0,
type: this.props.defaultActiveKey,
+ tabs: [],
+ active: {},
};
- componentWillUnmount() {
- clearInterval(this.interval);
+ 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 = (key) => {
this.setState({
@@ -44,131 +65,56 @@ export default class Login extends Component {
});
this.props.onTabChange(key);
}
- onGetCaptcha = () => {
- let count = 59;
- this.setState({ count });
- this.props.onGetCaptcha();
- this.interval = setInterval(() => {
- count -= 1;
- this.setState({ count });
- if (count === 0) {
- clearInterval(this.interval);
- }
- }, 1000);
- }
handleSubmit = (e) => {
e.preventDefault();
- const activeKey = this.state.type;
- const { data } = this.props;
- const activeData = data.filter(item => item.key === activeKey);
- if (activeData[0]) {
- const activeFileds = activeData[0].inputControls.map(control => control.key);
- this.props.form.validateFields(activeFileds, { force: true },
- (err, values) => {
- this.props.onSubmit(err, values);
- }
- );
- }
- }
- renderNotice = (notice) => {
- if (notice.message) {
- return ;
- }
- return null;
- }
- renderControl = (control) => {
- const { getFieldDecorator } = this.props.form;
- const { count } = this.state;
- const options = {};
- const { type } = control;
- if (map[type]) {
- options.rules = control.rules ? control.rules : map[type].rules;
- let otherProps = {};
- if (control.props) {
- const { onChange, defaultValue, ...restProps } = control.props;
- if (onChange) {
- options.onChange = onChange;
- }
- if (defaultValue) {
- options.initialValue = defaultValue;
- }
- otherProps = restProps || otherProps;
+ const { active, type } = this.state;
+ const activeFileds = active[type];
+ this.props.form.validateFields(activeFileds, { force: true },
+ (err, values) => {
+ this.props.onSubmit(err, values);
}
- const TypeComponent = map[type].component;
- if (type === 'captcha') {
- return (
-
-
-
- {getFieldDecorator(control.key, options)(
-
- )}
-
-
-
-
-
-
- );
- }
- return (
-
- {getFieldDecorator(control.key, options)(
-
- )}
-
- );
- } else {
- return null;
- }
+ );
}
render() {
- const { className, data, notice, extra, moreLoginTypes, register } = this.props;
- const { type } = this.state;
+ const { className, children } = this.props;
+ const { type, tabs } = this.state;
+ const TabChildren = [];
+ const otherChildren = [];
+ React.Children.forEach(children, (item) => {
+ if (item.type.name === 'LoginTab') {
+ TabChildren.push(item);
+ } else {
+ otherChildren.push(item);
+ }
+ });
return (
- {
- (moreLoginTypes || register) &&
-
- {moreLoginTypes && (moreLoginTypes.title || '其他登录方式')}
- {moreLoginTypes && moreLoginTypes.types}
- {register &&
- (register.href ?
- {register.title || '注册账户'} :
- {register.title || '注册账户'}
- )}
-
- }
);
}
}
+
+Login.Tab = LoginTab;
+Login.Submit = LoginSubmit;
+Object.keys(LoginItem).forEach((item) => {
+ Login[item] = LoginItem[item];
+});
+
+export default Login;
diff --git a/src/components/Login/index.less b/src/components/Login/index.less
index 51648a8dd7..4f1f226897 100644
--- a/src/components/Login/index.less
+++ b/src/components/Login/index.less
@@ -40,28 +40,8 @@
width: 100%;
}
- .additional {
- text-align: left;
-
- :global {
- .ant-form-item-control {
- line-height: 22px;
- }
- }
- }
-
.submit {
width: 100%;
margin-top: 24px;
}
-
- .other {
- text-align: left;
- margin-top: 24px;
- line-height: 22px;
-
- .register {
- float: right;
- }
- }
}
diff --git a/src/components/Login/index.md b/src/components/Login/index.md
index 25eb0ecc07..20937f437b 100644
--- a/src/components/Login/index.md
+++ b/src/components/Login/index.md
@@ -4,21 +4,48 @@ title:
zh-CN: Login
subtitle: 登录
cols: 1
-order: 80
+order: 15
---
-登录控件,支持自定义输入控件。
+支持多种登录方式切换,内置了几种常见的登录控件,可以灵活组合,也支持和自定义控件配合使用。
## API
+### Login
+
+参数 | 说明 | 类型 | 默认值
+----|------|-----|------
+defaultActiveKey | 默认激活 tab 面板的 key | String | -
+onTabChange | 切换页签时的回调 | (key) => void | -
+onSubmit | 点击提交时的回调 | (err, values) => void | -
+
+### Login.Tab
+
参数 | 说明 | 类型 | 默认值
----|------|-----|------
-data | 类型及输入控件信息 | Array<{ loginType: String, key: String, inputControls: Array<{ key: String, type: Enum{'AutoComplete', 'Input', 'Select'} 或 Enum{'userName', 'password', 'mobile', 'captcha'} `后面几种为内置控件,若配置为这些选项,会自动包含默认的样式,校验规则及相关属性`, props: 各类控件支持的属性, rules: 同 [antd-form-rules](https://ant.design/components/form-cn/#校验规则) }> }> | -
-activeKey | 当前激活 tab 面板的 key | String | -
-notice | 提示信息,可用来展现服务端返回的报错,警告之类,会展示为 Alert 形式,位于输入控件上方 | 支持属性与 Alert 相同 | -
-extra | 其他内容,位于提交按钮上方 | ReactNode | -
-moreLoginTypes | 其他登录方式 | {title: ReactNode, types: ReactNode} | title 默认为 '其他登录方式'
-register | 注册操作 | {title: ReactNode, href: String} | title 默认为 '注册账户'
-onTabChange | 切换登录方式的回调 | (key) => void | -
-onSubmit | 提交信息的回调 | (err, values) => void | -
-onGetCaptcha | 点击获取校验码按钮的回调,仅当 inputControls 中包含 'captcha' 类型的控件时生效 | () => void | -
+key | 对应选项卡的 key | String | -
+tab | 选项卡头显示文字 | ReactNode | -
+
+### Login.UserName
+
+参数 | 说明 | 类型 | 默认值
+----|------|-----|------
+itemKey | 控件标记,提交数据中同样以此为 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 的所有属性。
+
diff --git a/src/components/Login/map.js b/src/components/Login/map.js
index a4f5e38b51..81fd7a20be 100644
--- a/src/components/Login/map.js
+++ b/src/components/Login/map.js
@@ -1,30 +1,9 @@
import React from 'react';
-import { Input, AutoComplete, Select, Icon } from 'antd';
+import { Input, Icon } from 'antd';
import styles from './index.less';
const map = {
- Input: {
- component: Input,
- props: {
- size: 'large',
- },
- rules: [],
- },
- AutoComplete: {
- component: AutoComplete,
- props: {
- size: 'large',
- },
- rules: [],
- },
- Select: {
- component: Select,
- props: {
- size: 'large',
- },
- rules: [],
- },
- userName: {
+ UserName: {
component: Input,
props: {
size: 'large',
@@ -35,7 +14,7 @@ const map = {
required: true, message: '请输入账户名!',
}],
},
- password: {
+ Password: {
component: Input,
props: {
size: 'large',
@@ -47,7 +26,7 @@ const map = {
required: true, message: '请输入密码!',
}],
},
- mobile: {
+ Mobile: {
component: Input,
props: {
size: 'large',
@@ -60,7 +39,7 @@ const map = {
pattern: /^1\d{10}$/, message: '手机号格式错误!',
}],
},
- captcha: {
+ Captcha: {
component: Input,
props: {
size: 'large',