react 项目实战(十)引入AntDesign组件库

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

安装组件库

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

改造HomeLayout

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

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

所以新的HomeLayout应该包括LOGOMenu部分,然后HomeLayoutchildren放置在Content区域。

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

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

来看新的组件代码:

/**
 * 布局组件
 */
import React from 'react';
// 路由
import { Link } from 'react-router';
// Menu 导航菜单 Icon 图标
import { Menu, Icon } from 'antd';
import '../styles/home-layout.less';

// 左侧菜单栏
const SubMenu = Menu.SubMenu;
 
class HomeLayout extends React.Component {
  render () {
    const {children} = this.props;
    return (
      <div>
        <header className="header">
          <Link to="/">ReactManager</Link>
        </header>
 
        <main className="main">
          <div className="menu">
            <Menu mode="inline" theme="dark" style={{ '240'}}>
              <SubMenu key="user" title={<span><Icon type="user"/><span>用户管理</span></span>}>
                <Menu.Item key="user-list">
                  <Link to="/user/list">用户列表</Link>
                </Menu.Item>
                <Menu.Item key="user-add">
                  <Link to="/user/add">添加用户</Link>
                </Menu.Item>
              </SubMenu>
 
              <SubMenu key="book" title={<span><Icon type="book"/><span>图书管理</span></span>}>
                <Menu.Item key="book-list">
                  <Link to="/book/list">图书列表</Link>
                </Menu.Item>
                <Menu.Item key="book-add">
                  <Link to="/book/add">添加图书</Link>
                </Menu.Item>
              </SubMenu>
            </Menu>
          </div>
 
          <div className="content">
            {children}
          </div>
        </main>
      </div>
    );
  }
}
 
export default HomeLayout;

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

@import '~antd/dist/antd.css'; // 引入antd样式表
.main {
  height: 100vh;
  padding-top: 50px;
}
 
.header {
  position: absolute;
  top: 0;
  height: 50px;
   100%;
  font-size: 18px;
  padding: 0 20px;
  line-height: 50px;
  background-color: #108ee9;
  color: #fff;
 
  a {
    color: inherit;
  }
}
 
.menu {
  height: 100%;
   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 '../styles/home-page.less';

class Home extends React.Component {
  // 构造器
  constructor(props) {
    super(props);
    // 定义初始化状态
    this.state = {};
  }

  render() {
    return (
      <div className="welcome">
        Welcome
      </div>
    );
  }
}

export default Home;

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

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

优化HomeLayout使用方式

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

src / index.js

/**
 * 配置路由
 */
import React from 'react';
import ReactDOM from 'react-dom';
// 引入react-router
import { Router, Route, hashHistory } from 'react-router';
// 引入布局组件
import HomeLayout from './layouts/HomeLayout';
import HomePage from './pages/Home'; // 首页
import LoginPage from './pages/Login'; // 登录页
import UserAddPage from './pages/UserAdd'; // 添加用户页
import UserListPage from './pages/UserList'; // 用户列表页
import UserEditPage from './pages/UserEdit'; // 用户编辑页面
import BookAddPage from './pages/BookAdd'; // 添加图书页
import BookListPage from './pages/BookList'; // 图书列表页
import BookEditPage from './pages/BookEdit'; // 用户编辑页面

// 渲染
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('root'));

效果图:

然后需要在各个页面中移除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.jsUserEdit.jsUserList.js与上面Book对应的组件做相同更改。

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

升级登录页面

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

/**
 * 登录页
 */
import React from 'react';
// 引入antd组件
import { Icon, Form, Input, Button, message } from 'antd';
// 引入 封装后的fetch工具类
import { post } from '../utils/request';
// 引入样式表
import styles from '../styles/login-page.less';
// 引入 prop-types
import PropTypes from 'prop-types';

const FormItem = Form.Item;
 
class Login extends React.Component {
  // 构造器
  constructor () {
    super();
    this.handleSubmit = this.handleSubmit.bind(this);
  }
  
  handleSubmit (e) {
    // 通知 Web 浏览器不要执行与事件关联的默认动作
    e.preventDefault();
    // 表单验证
    this.props.form.validateFields((err, values) => {
      if(!err){
        // 发起请求
        post('http://localhost:8000/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={styles.wrapper}>
        <div className={styles.body}>
          <header className={styles.header}>
            ReactManager
          </header>

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

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

              <Button className={styles.btn} type="primary" htmlType="submit">登录</Button>
            </Form>
          </section>
        </div>
      </div>
    );
  }
}
 
Login.contextTypes = {
  router: 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 {
   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 {
   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" prefix={<Icon type="user" />} />
  )}
</FormItem>
...

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

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

...
handleSubmit (e) {
  // 通知 Web 浏览器不要执行与事件关联的默认动作
  e.preventDefault();
  // 表单验证
  this.props.form.validateFields((err, values) => {
    if(!err){
      // 发起请求
      post('http://localhost:8000/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';
// 引入 antd 组件
import { Form, Input, InputNumber, Select, Button, message } from 'antd';
// 引入 prop-types
import PropTypes from 'prop-types';
// 引入 封装fetch工具类
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里进行赋值
     */
    const { editTarget, form } = this.props;
    if(editTarget){
      // 将editTarget的值设置到表单
      form.setFieldsValue(editTarget);
    }
  }

  // 按钮提交事件
  handleSubmit(e){
    // 阻止表单submit事件自动跳转页面的动作
    e.preventDefault();
    // 定义常量
    const { form, editTarget } = this.props; // 组件传值

    // 验证
    form.validateFields((err, values) => {
      if(!err){
        // 默认值
        let editType = '添加';
        let apiUrl = 'http://localhost:8000/user';
        let method = 'post';
        // 判断类型
        if(editTarget){
          editType = '编辑';
          apiUrl += '/' + editTarget.id;
          method = 'put';
        }

        // 发送请求
        request(method,apiUrl,values)
          // 成功的回调
          .then((res) => {
            // 当添加成功时,返回的json对象中应包含一个有效的id字段
            // 所以可以使用res.id来判断添加是否成功
            if(res.id){
              message.success(editType + '添加用户成功!');
              // 跳转到用户列表页面
              this.context.router.push('/user/list');
              return;
            }else{
              message.error(editType + '添加用户失败!');
            }
          })
          // 失败的回调
          .catch((err) => console.error(err));
      }else{
        message.warn(err);
      }
    });
  }
  
  render() {
    // 定义常量
    const { form } = this.props;
    const { getFieldDecorator } = form;
    return (
      <div style={{ '400'}}>
        <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定义一个包含router属性的contextTypes
// 使得组件中可以通过this.context.router来使用React Router提供的方法
UserEditor.contextTypes = {
  router: PropTypes.object.isRequired
};

/**
 * 使用Form.create({ ... })(UserEditor)处理之后的UserEditor组件会接收到一个props.form
 * 使用props.form下的一系列方法,可以很方便地创造表单
 */
UserEditor = Form.create()(UserEditor);

export default UserEditor;

升级BookEditor

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

src/components/BookEditor.js

/**
 * 图书编辑器组件
 */
import React from 'react';
// 引入 antd 组件
import { Input, InputNumber, Form, Button, message } from 'antd';
// 引入 prop-types
import PropTypes from 'prop-types';
// 引入自动完成组件
import AutoComplete from '../components/AutoComplete'; // 也可以写为 './AutoComplete'
// 引入 封装fetch工具类
import request,{get} from '../utils/request';

// const Option = AutoComplete.Option;
const FormItem = Form.Item;
// 表单布局
const formLayout = {
  // label 标签布局,同 <Col> 组件
  labelCol: {
    span: 4
  },
  wrapperCol: {
    span: 16
  }
};

class BookEditor extends React.Component {
  // 构造器
  constructor(props) {
    super(props);
  
    this.state = {
      recommendUsers: []
    };
    // 绑定this
    this.handleSubmit = this.handleSubmit.bind(this);
    this.handleOwnerIdChange = this.handleOwnerIdChange.bind(this);
  }

  // 生命周期--组件加载完毕
  componentDidMount(){
    /**
     * 在componentWillMount里使用form.setFieldsValue无法设置表单的值
     * 所以在componentDidMount里进行赋值
     */
    const {editTarget, form} = this.props;
    if(editTarget){
      form.setFieldsValue(editTarget);
    }
  }

  // 按钮提交事件
  handleSubmit(e){
    // 阻止submit默认行为
    e.preventDefault();
    // 定义常量
    const { form, editTarget } = this.props; // 组件传值
    // 验证
    form.validateFields((err, values) => {
      if(err){
        message.warn(err);
        return;
      }

      // 默认值
      let editType = '添加';
      let apiUrl = 'http://localhost:8000/book';
      let method = 'post';
      // 判断类型
      if(editTarget){
        editType = '编辑';
        apiUrl += '/' + editTarget.id;
        method = 'put';
      }

      // 发送请求
      request(method,apiUrl,values)
        // 成功的回调
        .then((res) => {
          // 当添加成功时,返回的json对象中应包含一个有效的id字段
          // 所以可以使用res.id来判断添加是否成功
          if(res.id){
            message.success(editType + '添加图书成功!');
            // 跳转到用户列表页面
            this.context.router.push('/book/list');
          }else{
            message.error(editType + '添加图书失败!');
          }
        })
        // 失败的回调
        .catch((err) => console.error(err));
    });    
  }

  // 获取推荐用户信息
  getRecommendUsers (partialUserId) {
    // 请求数据
    get('http://localhost:8000/user?id_like=' + partialUserId)
    .then((res) => {
      if(res.length === 1 && res[0].id === partialUserId){
        // 如果结果只有1条且id与输入的id一致,说明输入的id已经完整了,没必要再设置建议列表
        return;
      }

      // 设置建议列表
      this.setState({
        recommendUsers: res.map((user) => {
          return {
            text: `${user.id}(${user.name})`,
            value: user.id
          }
        })
      });
    })
  }

  // 计时器
  timer = 0;
  handleOwnerIdChange(value){
    this.setState({
      recommendUsers: []
    });

    // 使用"节流"的方式进行请求,防止用户输入的过程中过多地发送请求
    if(this.timer){
      // 清除计时器
      clearTimeout(this.timer);
    }

    if(value){
      // 200毫秒内只会发送1次请求
      this.timer = setTimeout(() => {
        // 真正的请求方法
        this.getRecommendUsers(value);
        this.timer = 0;
      }, 200);
    }
  }
  
  render() {
    // 定义常量
    const {recommendUsers} = this.state;
    const {form} = this.props;
    const {getFieldDecorator} = form;

    return (
      <Form onSubmit={this.handleSubmit} style={{'400'}}>
        <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定义一个包含router属性的contextTypes
// 使得组件中可以通过this.context.router来使用React Router提供的方法
BookEditor.contextTypes = {
  router: PropTypes.object.isRequired
};

BookEditor = Form.create()(BookEditor);

export default BookEditor;

升级AutoComplete

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

src/components/AutoComplete.js

/**
 * 自动完成组件
 */
import React from 'react';
// 引入 antd 组件
import { Input } from 'antd';
// 引入 prop-types
import PropTypes from 'prop-types';
// 引入样式
import styles from '../styles/auto-complete.less';

// 获得当前元素value值
function getItemValue (item) {
  return item.value || item;
}

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);
  }

  // 处理上下键、回车键点击事件
  handleKeyDown(e){
    const {activeItemIndex} = this.state;
    const {options} = this.props;

    /**
     * 判断键码
     */
    switch (e.keyCode) {
      // 13为回车键的键码(keyCode)
      case 13: {
        // 判断是否有列表项处于选中状态
        if(activeItemIndex >= 0){
          // 防止按下回车键后自动提交表单
          e.preventDefault();
          e.stopPropagation();
          // 输入框改变事件
          this.handleChange(getItemValue(options[activeItemIndex]));
        }
        break;
      }
      // 38为上方向键,40为下方向键
      case 38:
      case 40: {
        e.preventDefault();
        // 使用moveItem方法对更新或取消选中项
        this.moveItem(e.keyCode === 38 ? 'up' : 'down');
        break;
      }
      default: {
        //
      }
    }
  }

  // 使用moveItem方法对更新或取消选中项
  moveItem(direction){
    const {activeItemIndex} = this.state;
    const {options} = this.props;
    const lastIndex = options.length - 1;
    let newIndex = -1;

    // 计算新的activeItemIndex
    if(direction === 'up'){ // 点击上方向键
      if(activeItemIndex === -1){
        // 如果没有选中项则选择最后一项
        newIndex = lastIndex;
      }else{
        newIndex = activeItemIndex - 1;
      }
    }else{ // 点击下方向键
      if(activeItemIndex < lastIndex){
        newIndex = activeItemIndex + 1;
      }
    }

    // 获取新的displayValue
    let newDisplayValue = '';
    if(newIndex >= 0){
      newDisplayValue = getItemValue(options[newIndex]);
    }

    // 更新状态
    this.setState({
      displayValue: newDisplayValue,
      activeItemIndex: newIndex
    });
  }

  // 处理鼠标移入事件
  handleEnter(index){
    const currentItem = this.props.options[index];
    this.setState({
      activeItemIndex: index,
      displayValue: getItemValue(currentItem)
    });
  }

  // 处理鼠标移出事件
  handleLeave(){
    this.setState({
      activeItemIndex: -1,
      displayValue: ''
    });
  }

  // 渲染
  render() {
    const {show, displayValue, activeItemIndex} = this.state;
    // 组件传值
    const {value, options} = this.props;
    return (
      <div className={styles.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={styles.options} onMouseLeave={this.handleLeave}>
            {
              options.map((item, index) => {
                return (
                  <li
                    key={index}
                    className={index === activeItemIndex ? styles.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 // 函数
};

// 向外暴露
export default AutoComplete;

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

.options {
  z-index: 2;
  background-color:#fff;  
  ...
}

升级列表页组件

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

src/pages/BookList.js

/**
 * 图书列表页面
 */
import React from 'react';
// 引入 antd 组件
import { message, Table, Button, Popconfirm } from 'antd';
// 引入 prop-types
import PropTypes from 'prop-types';
// 引入 封装fetch工具类
import { get, del } from '../utils/request'; 

class BookList extends React.Component {
  // 构造器
  constructor(props) {
    super(props);
    // 定义初始化状态
    this.state = {
      bookList: []
    };
  }

  /**
   * 生命周期
   * componentWillMount
   * 组件初始化时只调用,以后组件更新不调用,整个生命周期只调用一次
   */
  componentWillMount(){
    // 请求数据
    get('http://localhost:8000/book')
      .then((res) => {
        /**
         * 成功的回调
         * 数据赋值
         */
        this.setState({
          bookList: res
        });
      });
  }

  /**
   * 编辑
   */
  handleEdit(book){
    // 跳转编辑页面
    this.context.router.push('/book/edit/' + book.id);
  }

  /**
   * 删除
   */
  handleDel(book){
    // 执行删除数据操作
    del('http://localhost:8000/book/' + book.id, {
    })
      .then(res => {
        /**
         * 设置状态
         * array.filter
         * 把Array的某些元素过滤掉,然后返回剩下的元素
         */
        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;
    // antd的Table组件使用一个columns数组来配置表格的列
    const columns = [
      {
        title: '图书ID',
        dataIndex: 'id'
      },
      {
        title: '书名',
        dataIndex: 'name'
      },
      {
        title: '价格',
        dataIndex: 'price',
        render: (text, record) => <span>¥{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="确定要删除吗?"
              okText="确定"
              cancelText="取消"
              onConfirm={() => this.handleDel(record)}>
              <Button size="small">删除</Button>
            </Popconfirm>
          </Button.Group>
        )
      }
    ];

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

/**
 * 任何使用this.context.xxx的地方,必须在组件的contextTypes里定义对应的PropTypes
 */
BookList.contextTypes = {
  router: PropTypes.object.isRequired
};

export default BookList;

src/pages/UserList.js

/**
 * 用户列表页面
 */
import React from 'react';
// 引入 antd 组件
import { message, Table, Button, Popconfirm } from 'antd';
// 引入 prop-types
import PropTypes from 'prop-types';
// 引入 封装后的fetch工具类
import { get, del } from '../utils/request';

class UserList extends React.Component {
  // 构造器
  constructor(props) {
    super(props);
    // 定义初始化状态
    this.state = {
      userList: []
    };
  }

  /**
   * 生命周期
   * componentWillMount
   * 组件初始化时只调用,以后组件更新不调用,整个生命周期只调用一次
   */
  componentWillMount(){
    // 请求数据
    get('http://localhost:8000/user')
      .then((res) => {
        /**
         * 成功的回调
         * 数据赋值
         */
        this.setState({
          userList: res
        });
      });
  }

  /**
   * 编辑
   */
  handleEdit(user){
    // 跳转编辑页面
    this.context.router.push('/user/edit/' + user.id);
  }

  /**
   * 删除
   */
  handleDel(user){
    // 执行删除数据操作
    del('http://localhost:8000/user/' + user.id, {
    })
      .then((res) => {
        /**
         * 设置状态
         * array.filter
         * 把Array的某些元素过滤掉,然后返回剩下的元素
         */
        this.setState({
          userList: this.state.userList.filter(item => item.id !== user.id)
        });
        message.success('删除用户成功');
      })
      .catch(err => {
        console.error(err);
        message.error('删除用户失败');
      });
  }

  render() {
    // 定义变量
    const { userList } = this.state;
    // antd的Table组件使用一个columns数组来配置表格的列
    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="确定要删除吗?"
                okText="确定"
                cancelText="取消"
                onConfirm={() => this.handleDel(record)}>
                <Button size="small">删除</Button>
              </Popconfirm>
            </Button.Group>
          );
        }
      }
    ];

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

/**
 * 任何使用this.context.xxx的地方,必须在组件的contextTypes里定义对应的PropTypes
 */
UserList.contextTypes = {
  router: PropTypes.object.isRequired
};

export default UserList;

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

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

效果图:

原文地址:https://www.cnblogs.com/crazycode2/p/8553664.html