diff --git a/package.json b/package.json
index b87c26fac951f50f5ec8215297394e57f55cab93..5492cd6e61f3962e565ac01b5668fea1b5a3d231 100755
--- a/package.json
+++ b/package.json
@@ -33,6 +33,7 @@
"lodash-decorators": "^4.4.1",
"moment": "^2.19.1",
"numeral": "^2.0.6",
+ "omit.js": "^1.0.0",
"prop-types": "^15.5.10",
"qs": "^6.5.0",
"rc-drawer-menu": "^0.5.0",
diff --git a/src/components/Login/LoginItem.js b/src/components/Login/LoginItem.js
new file mode 100644
index 0000000000000000000000000000000000000000..b7332b2e22e927c371d39edc4ed021e41010a4a1
--- /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.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 (
+
+
+
+ {getFieldDecorator(name, options)(
+
+ )}
+
+
+
+
+
+
+ );
+ }
+ return (
+
+ {getFieldDecorator(name, 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 0000000000000000000000000000000000000000..8770a97386e5edf526be071e3d863c76c4aed421
--- /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 0000000000000000000000000000000000000000..6b7d9758f487c64aa02d470abfa2b3ad17e2d7eb
--- /dev/null
+++ b/src/components/Login/LoginTab.js
@@ -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 ;
+ }
+}
diff --git a/src/components/Login/demo/basic.md b/src/components/Login/demo/basic.md
new file mode 100644
index 0000000000000000000000000000000000000000..12afef728e3db8ae2bc3376245c23c7772e75250
--- /dev/null
+++ b/src/components/Login/demo/basic.md
@@ -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 (
+
+
+ {
+ this.state.notice &&
+
+ }
+
+
+
+
+
+ console.log('Get captcha!')} name="captcha" />
+
+
+ 登录
+
+
+ )
+ }
+}
+
+ReactDOM.render(, mountNode);
+````
+
+
diff --git a/src/components/Login/index.js b/src/components/Login/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..37ece36478a90d9953e665b3617749fbc3cd3484
--- /dev/null
+++ b/src/components/Login/index.js
@@ -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 (
+
+ );
+ }
+}
+
+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
new file mode 100644
index 0000000000000000000000000000000000000000..4f1f226897bc21af04adf3b96988da29eec31b78
--- /dev/null
+++ b/src/components/Login/index.less
@@ -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;
+ }
+}
diff --git a/src/components/Login/index.md b/src/components/Login/index.md
new file mode 100644
index 0000000000000000000000000000000000000000..a56be08e0ae1d4ccda92e82c0d2b34515e563c0a
--- /dev/null
+++ b/src/components/Login/index.md
@@ -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 的所有属性。
+
diff --git a/src/components/Login/map.js b/src/components/Login/map.js
new file mode 100644
index 0000000000000000000000000000000000000000..81fd7a20be72af423674319fcc8c059c08eab1a5
--- /dev/null
+++ b/src/components/Login/map.js
@@ -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: ,
+ placeholder: 'admin',
+ },
+ rules: [{
+ required: true, message: '请输入账户名!',
+ }],
+ },
+ Password: {
+ component: Input,
+ props: {
+ size: 'large',
+ prefix: ,
+ type: 'password',
+ placeholder: '888888',
+ },
+ rules: [{
+ required: true, message: '请输入密码!',
+ }],
+ },
+ Mobile: {
+ component: Input,
+ props: {
+ size: 'large',
+ prefix: ,
+ placeholder: '手机号',
+ },
+ rules: [{
+ required: true, message: '请输入手机号!',
+ }, {
+ pattern: /^1\d{10}$/, message: '手机号格式错误!',
+ }],
+ },
+ Captcha: {
+ component: Input,
+ props: {
+ size: 'large',
+ prefix: ,
+ placeholder: '验证码',
+ },
+ rules: [{
+ required: true, message: '请输入验证码!',
+ }],
+ },
+};
+
+export default map;
diff --git a/src/routes/User/Login.js b/src/routes/User/Login.js
index 1433118fd86c722b2106977d0b1ae82ddd48c7de..a348e5361fc43d971729f13be67e2a704da21b39 100644
--- a/src/routes/User/Login.js
+++ b/src/routes/User/Login.js
@@ -1,185 +1,93 @@
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 (
-
+
);
}
render() {
- const { form, login } = this.props;
- const { getFieldDecorator } = form;
- const { count, type } = this.state;
+ const { login } = this.props;
+ const { type } = this.state;
return (
-
-
- 其他登录方式
- {/* 需要加到 Icon 中 */}
-
-
-
- 注册账户
-
+
+
+ {
+ login.status === 'error' &&
+ login.type === 'account' &&
+ login.submitting === false &&
+ this.renderMessage('账户或密码错误')
+ }
+
+
+
+
+ {
+ login.status === 'error' &&
+ login.type === 'mobile' &&
+ login.submitting === false &&
+ this.renderMessage('验证码错误')
+ }
+
+
+
+
+ 登录
+
+ 其他登录方式
+
+
+
+ 注册账户
+
+
);
}
diff --git a/src/routes/User/Login.less b/src/routes/User/Login.less
index 53af1321b4569fdd93081e27aff07fb970df2a32..b539db6e1790020a2f73663662e4157d49fc7dc7 100644
--- a/src/routes/User/Login.less
+++ b/src/routes/User/Login.less
@@ -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;
}
}