【React全家桶入门之十一】引入AntDesign组件库

我们的管理系统已经有了图书、用户的增删改查以及登录功能了,可谓是五脏俱全,就是丑了点~

是不是已经有些厌倦我们系统里的白底黑字和灰色框框了?

打起精神,本篇带你使用 AntDesign 组件库为我们的系统换上产品级的UI!

安装组件库

  • 在项目目录下执行:npm i antd -S 安装组件包
  • 执行:npm i babel-plugin-import -D 安装一个babel插件用于做组件的按需加载(否则项目会打包整个组件库,非常大)
  • 根目录下新建.roadhogrc文件(别忘了前面的点,这是roadhog工具的配置文件,下面的代码用于加载上一个命令安装的import插件),写入:
{
  "extraBabelPlugins": [
    ["import", {
      "libraryName": "antd",
      "libraryDirectory": "lib",
      "style": "css"
    }]
  ]
}

改造HomeLayout

我们计划把系统改造成这个样子:

上方显示LOGO,下方左侧显示一个菜单栏,右侧显示页面的主要内容。

所以新的HomeLayout应该包括LOGO和Menu部分,然后HomeLayout的children放置在Content区域。

Menu我们使用AntDesign提供的Menu组件来完成,菜单项为:

  • 用户管理
    • 用户列表
    • 添加用户
  • 图书管理
    • 图书列表
    • 添加图书

来看新的组件代码:

import React from 'react';
import { Link } from 'react-router';
import { Menu, Icon } from 'antd';
import style from '../styles/home-layout.less';

const SubMenu = Menu.SubMenu;
const MenuItem = Menu.Item;

class HomeLayout extends React.Component {
  render () {
    const {children} = this.props;
    return (
      <div>
        <header className={style.header}>
          <Link to="/">ReactManager</Link>
        </header>

        <main className={style.main}>
          <div className={style.menu}>
            <Menu mode="inline" theme="dark" style={{width: '240px'}}>
              <SubMenu key="user" title={<span><Icon type="user"/><span>用户管理</span></span>}>
                <MenuItem key="user-list">
                  <Link to="/user/list">用户列表</Link>
                </MenuItem>
                <MenuItem key="user-add">
                  <Link to="/user/add">添加用户</Link>
                </MenuItem>
              </SubMenu>

              <SubMenu key="book" title={<span><Icon type="book"/><span>图书管理</span></span>}>
                <MenuItem key="book-list">
                  <Link to="/book/list">图书列表</Link>
                </MenuItem>
                <MenuItem key="book-add">
                  <Link to="/book/add">添加图书</Link>
                </MenuItem>
              </SubMenu>
            </Menu>
          </div>

          <div className={style.content}>
            {children}
          </div>
        </main>
      </div>
    );
  }
}

export default HomeLayout;

HomeLayout引用了/src/styles/home-layout.less这个样式文件,样式代码为:

.main {
  height: 100vh;
  padding-top: 50px;
}

.header {
  position: absolute;
  top: 0;
  height: 50px;
  width: 100%;
  font-size: 18px;
  padding: 0 20px;
  line-height: 50px;
  background-color: #108ee9;
  color: #fff;

  a {
    color: inherit;
  }
}

.menu {
  height: 100%;
  width: 240px;
  float: left;
  background-color: #404040;
}

.content {
  height: 100%;
  padding: 12px;
  overflow: auto;
  margin-left: 240px;
  align-self: stretch;
}

现在的首页是这个样子:

逼格立马就上来了有没?

改造HomePage

由于现在有菜单了,就不需要右侧那个HomePage里的链接了,把他去掉,然后放个Welcome吧(HomeLayout也去掉了,在下面会提到):

// /src/pages/Home.js

import React from 'react';
import style from '../styles/home-page.less';

class Home extends React.Component {
  render () {
    return (
      <div className={style.welcome}>
       Welcome
      </div>
    );
  }
}

export default Home;

新增样式文件/src/styles/home-page.less,代码:

.welcome {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 32px;
}

怎么样,还丑吗?

优化HomeLayout使用方式

现在的HomeLayout里有一个菜单了,菜单有展开状态需要维护,如果还是像以前那样在每个page组件里单独使用HomeLayout,会导致菜单的展开状态被重置(跳转页面之后都会渲染一个新的HomeLayout),所以需要将HomeLayout放到父级路由中来使用:


// /src/index.js

...
import HomeLayout from './layouts/HomeLayout';

ReactDOM.render((
  <Router history={hashHistory}>
    <Route component={HomeLayout}>
      <Route path="/" component={HomePage}/>
      <Route path="/user/add" component={UserAddPage}/>
      <Route path="/user/list" component={UserListPage}/>
      <Route path="/user/edit/:id" component={UserEditPage}/>
      <Route path="/book/add" component={BookAddPage}/>
      <Route path="/book/list" component={BookListPage}/>
      <Route path="/book/edit/:id" component={BookEditPage}/>
    </Route>
    <Route path="/login" component={LoginPage}/>
  </Router>
), document.getElementById('app'));

然后需要在各个页面中移除HomeLayout:

// /src/pages/BookAdd.js
// 这个组件除了返回BookEditor没有做任何事,其实可以直接export default BookEditor

import React from 'react';
import BookEditor from '../components/BookEditor';

class BookAdd extends React.Component {
  render () {
    return (
      <BookEditor/>
    );
  }
}

export default BookAdd;
// /src/pages/BookEdit.js

  ...
  render () {
    const {book} = this.state;
    return book ? <BookEditor editTarget={book}/> : <span>加载中...</span>;
  }
  ...
// /src/pages/BookList.js

  ...
  render () {
    ...
    return (
      <table>
        ...
      </table>
    );
  }
  ...

剩下的UserAdd.js、UserEdit.js、UserList.js与上面Book对应的组件做相同更改。

还有登录页组件在下面说。

升级登录页面

下面来对登录页面进行升级,修改/src/pages/Login.js文件:

import React from 'react';
import { Icon, Form, Input, Button, message } from 'antd';
import { post } from '../utils/request';
import style from '../styles/login-page.less';

const FormItem = Form.Item;

class Login extends React.Component {
  constructor () {
    super();
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleSubmit (e) {
    e.preventDefault();

    this.props.form.validateFields((err, values) => {
      if (!err) {
        post('http://localhost:3000/login', values)
          .then((res) => {
            if (res) {
              message.info('登录成功');
              this.context.router.push('/');
            } else {
              message.info('登录失败,账号或密码错误');
            }
          });
      }
    });
  }

  render () {
    const {form} = this.props;
    const {getFieldDecorator} = form;
    return (
      <div className={style.wrapper}>

        <div className={style.body}>
          <header className={style.header}>
            ReactManager
          </header>

          <section className={style.form}>

            <Form onSubmit={this.handleSubmit}>
              <FormItem>
                {getFieldDecorator('account', {
                  rules: [
                    {
                      required: true,
                      message: '请输入管理员账号',
                      type: 'string'
                    }
                  ]
                })(
                  <Input type="text" addonBefore={<Icon type="user"/>}/>
                )}
              </FormItem>

              <FormItem>
                {getFieldDecorator('password', {
                  rules: [
                    {
                      required: true,
                      message: '请输入密码',
                      type: 'string'
                    }
                  ]
                })(
                  <Input type="password" addonBefore={<Icon type="lock"/>}/>
                )}
              </FormItem>

              <Button className={style.btn} type="primary" htmlType="submit">Sign In</Button>
            </Form>

          </section>

        </div>

      </div>
    );
  }
}

Login.contextTypes = {
  router: React.PropTypes.object.isRequired
};

Login = Form.create()(Login);

export default Login;

新建样式文件/src/styles/login-page.less,样式代码:

.wrapper {
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
}

.body {
  width: 360px;
  box-shadow: 1px 1px 10px 0 rgba(0, 0, 0, .3);
}

.header {
  color: #fff;
  font-size: 24px;
  padding: 30px 20px;
  background-color: #108ee9;
}

.form {
  margin-top: 12px;
  padding: 24px;
}

.btn {
  width: 100%;
}

酷酷的登录页面:


改造后的登录页组件使用了antd提供的Form组件,Form组件提供了一个create方法,和我们之前写的formProvider一样,是一个高阶组件。使用Form.create({ ... })(Login)处理之后的Login组件会接收到一个props.form,使用props.form下的一系列方法,可以很方便地创造表单,上面有一段代码:

  ...
  <FormItem>
    {getFieldDecorator('account', {
      rules: [
        {
          required: true,
          message: '请输入管理员账号',
          type: 'string'
        }
      ]
    })(
      <Input type="text" addonBefore={<Icon type="user"/>}/>
    )}
  </FormItem>
  ...

这里使用了props.form.getFieldDecorator方法来包装一个Input输入框组件,传入的第一个参数表示这个字段的名称,第二个参数是一个配置对象,这里设置了表单控件的校验规则rules(更多配置项请查看文档)。使用getFieldDecorator方法包装后的组件会自动表单组件的value以及onChange事件;此外,这里还用到了Form.Item这个表单项目组件(上面的FormItem),这个组件可用于配置表单项目的标签、布局等。

在handleSubmit方法中,使用了props.form.validateFields方法对表单的各个字段进行校验,校验完成后会调用传入的回调方法,回调方法可以接收到错误信息err和表单值对象values,方便对校验结果进行处理:

  ...
  handleSubmit (e) {
    e.preventDefault();

    this.props.form.validateFields((err, values) => {
      if (!err) {
        post('http://localhost:3000/login', values)
          .then((res) => {
            if (res) {
              message.info('登录成功');
              this.context.router.push('/');
            } else {
              message.info('登录失败,账号或密码错误');
            }
          });
      }
    });
  }
  ...

升级UserEditor

升级UserEditor和登录页面组件类似,但是在componentWillMount里需要使用this.props.setFieldsValue将editTarget的值设置到表单:

// /src/components/UserEditor.js

import React from 'react';
import { Form, Input, InputNumber, Select, Button, message } from 'antd';
import request from '../utils/request';

const FormItem = Form.Item;

const formLayout = {
  labelCol: {
    span: 4
  },
  wrapperCol: {
    span: 16
  }
};

class UserEditor extends React.Component {
  componentDidMount () {
    // 在componentWillMount里使用form.setFieldsValue无法设置表单的值
    // 所以在componentDidMount里进行赋值
    // see: https://github.com/ant-design/ant-design/issues/4802
    const {editTarget, form} = this.props;
    if (editTarget) {
      form.setFieldsValue(editTarget);
    }
  }

  handleSubmit (e) {
    e.preventDefault();

    const {form, editTarget} = this.props;

    form.validateFields((err, values) => {
      if (!err) {
        let editType = '添加';
        let apiUrl = 'http://localhost:3000/user';
        let method = 'post';
        if (editTarget) {
          editType = '编辑';
          apiUrl += '/' + editTarget.id;
          method = 'put';
        }

        request(method, apiUrl, values)
          .then((res) => {
            if (res.id) {
              message.success(editType + '用户成功');
              this.context.router.push('/user/list');
            } else {
              message.error(editType + '失败');
            }
          })
          .catch((err) => console.error(err));

      } else {
        message.warn(err);
      }
    });
  }

  render () {
    const {form} = this.props;
    const {getFieldDecorator} = form;
    return (
      <div style={{width: '400px'}}>
        <Form onSubmit={(e) => this.handleSubmit(e)}>
          <FormItem label="用户名:" {...formLayout}>
            {getFieldDecorator('name', {
              rules: [
                {
                  required: true,
                  message: '请输入用户名'
                },
                {
                  pattern: /^.{1,4}$/,
                  message: '用户名最多4个字符'
                }
              ]
            })(
              <Input type="text"/>
            )}
          </FormItem>
          <FormItem label="年龄:" {...formLayout}>
            {getFieldDecorator('age', {
              rules: [
                {
                  required: true,
                  message: '请输入年龄',
                  type: 'number'
                },
                {
                  min: 1,
                  max: 100,
                  message: '请输入1~100的年龄',
                  type: 'number'
                }
              ]
            })(
              <InputNumber/>
            )}
          </FormItem>
          <FormItem label="性别:" {...formLayout}>
            {getFieldDecorator('gender', {
              rules: [
                {
                  required: true,
                  message: '请选择性别'
                }
              ]
            })(
              <Select placeholder="请选择">
                <Select.Option value="male"></Select.Option>
                <Select.Option value="female"></Select.Option>
              </Select>
            )}
          </FormItem>
          <FormItem wrapperCol={{...formLayout.wrapperCol, offset: formLayout.labelCol.span}}>
            <Button type="primary" htmlType="submit">提交</Button>
          </FormItem>
        </Form>
      </div>
    );
  }
}

UserEditor.contextTypes = {
  router: React.PropTypes.object.isRequired
};

UserEditor = Form.create()(UserEditor);

export default UserEditor;

升级BookEditor

BookEditor中使用了AutoComplete组件,但是由于antd提供的AutoComplete组件有一些问题(见issue),这里暂时使用我们之前实现的AutoComplete。

// /src/components/BookEditor.js

import React from 'react';
import { Input, InputNumber, Form, Button, message } from 'antd';
import AutoComplete from '../components/AutoComplete';
import request, { get } from '../utils/request';

const Option = AutoComplete.Option;
const FormItem = Form.Item;
const formLayout = {
  labelCol: {
    span: 4
  },
  wrapperCol: {
    span: 16
  }
};

class BookEditor extends React.Component {
  constructor (props) { ... }

  componentDidMount () {
    // 在componentWillMount里使用form.setFieldsValue无法设置表单的值
    // 所以在componentDidMount里进行赋值
    // see: https://github.com/ant-design/ant-design/issues/4802
    const {editTarget, form} = this.props;
    if (editTarget) {
      form.setFieldsValue(editTarget);
    }
  }

  handleSubmit (e) {
    e.preventDefault();

    const {form, editTarget} = this.props;

    form.validateFields((err, values) => {
      if (err) {
        message.warn(err);
        return;
      }

      let editType = '添加';
      let apiUrl = 'http://localhost:3000/book';
      let method = 'post';
      if (editTarget) {
        editType = '编辑';
        apiUrl += '/' + editTarget.id;
        method = 'put';
      }

      request(method, apiUrl, values)
        .then((res) => {
          if (res.id) {
            message.success(editType + '书本成功');
            this.context.router.push('/book/list');
          } else {
            message.error(editType + '失败');
          }
        })
        .catch((err) => console.error(err));
    });
  }

  getRecommendUsers (partialUserId) { ... }

  timer = 0;
  handleOwnerIdChange (value) { ... }

  render () {
    const {recommendUsers} = this.state;
    const {form} = this.props;
    const {getFieldDecorator} = form;
    return (
      <Form onSubmit={this.handleSubmit} style={{width: '400px'}}>
        <FormItem label="书名:" {...formLayout}>
          {getFieldDecorator('name', {
            rules: [
              {
                required: true,
                message: '请输入书名'
              }
            ]
          })(<Input type="text"/>)}
        </FormItem>

        <FormItem label="价格:" {...formLayout}>
          {getFieldDecorator('price', {
            rules: [
              {
                required: true,
                message: '请输入价格',
                type: 'number'
              },
              {
                min: 1,
                max: 99999,
                type: 'number',
                message: '请输入1~99999的数字'
              }
            ]
          })(<InputNumber/>)}
        </FormItem>

        <FormItem label="所有者:" {...formLayout}>
          {getFieldDecorator('owner_id', {
            rules: [
              {
                required: true,
                message: '请输入所有者ID'
              },
              {
                pattern: /^\d*$/,
                message: '请输入正确的ID'
              }
            ]
          })(
            <AutoComplete
              options={recommendUsers}
              onChange={this.handleOwnerIdChange}
            />
          )}
        </FormItem>
        <FormItem wrapperCol={{span: formLayout.wrapperCol.span, offset: formLayout.labelCol.span}}>
          <Button type="primary" htmlType="submit">提交</Button>
        </FormItem>
      </Form>
    );
  }
}

BookEditor.contextTypes = {
  router: React.PropTypes.object.isRequired
};

BookEditor = Form.create()(BookEditor);

export default BookEditor;

升级AutoComplete

因为要继续使用自己的AutoComplete组件,这里需要把组件中的原生input控件替换为antd的Input组件,并且在Input组件加了两个事件处理onFocus、onBlur和state.show,用于在输入框失去焦点时隐藏下拉框:

// /src/components/AutoComplete.js

import React, { PropTypes } from 'react';
import { Input } from 'antd';
import style from '../styles/auto-complete.less';

class AutoComplete extends React.Component {
  constructor (props) {
    super(props);

    this.state = {
      show: false, // 新增的下拉框显示控制开关
      displayValue: '',
      activeItemIndex: -1
    };

    this.handleKeyDown = this.handleKeyDown.bind(this);
    this.handleLeave = this.handleLeave.bind(this);
  }
  ...
  handleChange (value) {
    this.setState({activeItemIndex: -1, displayValue: ''});
    // 原来的onValueChange改为了onChange以适配antd的getFieldDecorator
    this.props.onChange(value);
  }
  ...
  render () {
    const {show, displayValue, activeItemIndex} = this.state;
    const {value, options} = this.props;
    return (
      <div className={style.wrapper}>
        <Input
          value={displayValue || value}
          onChange={e => this.handleChange(e.target.value)}
          onKeyDown={this.handleKeyDown}
          onFocus={() => this.setState({show: true})}
          onBlur={() => this.setState({show: false})}
        />
        {show && options.length > 0 && (
          <ul className={style.options} onMouseLeave={this.handleLeave}>
            {
              options.map((item, index) => {
                return (
                  <li
                    key={index}
                    className={index === activeItemIndex ? style.active : ''}
                    onMouseEnter={() => this.handleEnter(index)}
                    onClick={() => this.handleChange(getItemValue(item))}
                  >
                    {item.text || item}
                  </li>
                );
              })
            }
          </ul>
        )}
      </div>
    );
  }
}

// 由于使用了antd的form.getFieldDecorator来包装组件
// 这里取消了原来props的isRequired约束以防止报错
AutoComplete.propTypes = {
  value: PropTypes.any,
  options: PropTypes.array,
  onChange: PropTypes.func // 原来的onValueChange改为了onChange以适配antd的getFieldDecorator
};

export default AutoComplete;

同时也更新了组件的样式/src/styles/auto-complete.less,给.options加了一个z-index:

.options {
  z-index: 2;
  ...
}

升级列表页组件

最后还剩下两个列表页组件,我们使用antd的Table组件来实现这两个列表:

// /src/pages/BookList.js

import React from 'react';
import { message, Table, Button, Popconfirm } from 'antd';
import { get, del } from '../utils/request';

class BookList extends React.Component {
  ...
  handleDel (book) {
    del('http://localhost:3000/book/' + book.id)
      .then(res => {
        this.setState({
          bookList: this.state.bookList.filter(item => item.id !== book.id)
        });
        message.success('删除图书成功');
      })
      .catch(err => {
        console.error(err);
        message.error('删除图书失败');
      });
  }

  render () {
    const {bookList} = this.state;

    const columns = [
      {
        title: '图书ID',
        dataIndex: 'id'
      },
      {
        title: '书名',
        dataIndex: 'name'
      },
      {
        title: '价格',
        dataIndex: 'price',
        render: (text, record) => <span>&yen;{record.price / 100}</span>
      },
      {
        title: '所有者ID',
        dataIndex: 'owner_id'
      },
      {
        title: '操作',
        render: (text, record) => (
          <Button.Group type="ghost">
            <Button size="small" onClick={() => this.handleEdit(record)}>编辑</Button>
            <Popconfirm title="确定要删除吗?" onConfirm={() => this.handleDel(record)}>
              <Button size="small">删除</Button>
            </Popconfirm>
          </Button.Group>
        )
      }
    ];

    return (
      <Table columns={columns} dataSource={bookList} rowKey={row => row.id}/>
    );
  }
}
...
// /src/pages/UserList.js

import React from 'react';
import { message, Table, Button, Popconfirm } from 'antd';
import { get, del } from '../utils/request';

class UserList extends React.Component {
  ...
  handleDel (user) {
    del('http://localhost:3000/user/' + user.id)
      .then(res => {
        this.setState({
          userList: this.state.bookList.filter(item => item.id !== user.id)
        });
        message.success('删除用户成功');
      })
      .catch(err => {
        console.error(err);
        message.error('删除用户失败');
      });
  }

  render () {
    const {userList} = this.state;

    const columns = [
      {
        title: '用户ID',
        dataIndex: 'id'
      },
      {
        title: '用户名',
        dataIndex: 'name'
      },
      {
        title: '性别',
        dataIndex: 'gender'
      },
      {
        title: '年龄',
        dataIndex: 'age'
      },
      {
        title: '操作',
        render: (text, record) => {
          return (
            <Button.Group type="ghost">
              <Button size="small" onClick={() => this.handleEdit(record)}>编辑</Button>
              <Popconfirm title="确定要删除吗?" onConfirm={() => this.handleDel(record)}>
                <Button size="small">编辑</Button>
              </Popconfirm>
            </Button.Group>
          );
        }
      }
    ];

    return (
      <Table columns={columns} dataSource={userList} rowKey={row => row.id}/>
    );
  }
}
...

antd的Table组件使用一个columns数组来配置表格的列,这个columns数组的元素可以包含title(列名)、dataIndex(该列数据的索引)、render(自定义的列单元格渲染方法)等字段(更多配置请参考文档)。

然后将表格数据列表传入Table的dataSource,传入一个rowKey来指定每一列的key,就可以渲染出列表了。

升级后的效果

终于折腾完了,我们来看一看最终的效果,兴奋一下:

  • 24
    点赞
  • 53
    收藏
    觉得还不错? 一键收藏
  • 26
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 26
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值