zoukankan      html  css  js  c++  java
  • 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,就可以渲染出列表了

    效果图:

  • 相关阅读:
    CSP2020 游记
    React中useLayoutEffect和useEffect的区别
    Vue前后端分离跨域踩坑
    Python 正则将link 和 script 处理为 Django static形式
    BootStrap4
    单例模式
    匈牙利算法——求二部图的最大匹配的匹配数
    抽象工厂模式
    工厂方法模式
    JDK配置步骤
  • 原文地址:https://www.cnblogs.com/crazycode2/p/8553664.html
Copyright © 2011-2022 走看看