react 項目實戰(十)引入AntDesign組件庫

本篇帶你使用 AntDesign 組件庫爲咱們的系統換上產品級的UI!javascript

安裝組件庫

  • 在項目目錄下執行: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

咱們計劃把系統改形成這個樣子:css

上方顯示LOGO,下方左側顯示一個菜單欄,右側顯示頁面的主要內容。html

因此新的HomeLayout應該包括LOGOMenu部分,而後HomeLayoutchildren放置在Content區域。java

Menu咱們使用AntDesign提供的Menu組件來完成,菜單項爲:react

  • 用戶管理 
    • 用戶列表
    • 添加用戶
  • 圖書管理 
    • 圖書列表
    • 添加圖書

來看新的組件代碼:git

/**
 * 佈局組件
 */
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={{width: '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這個樣式文件,樣式代碼爲:github

@import '~antd/dist/antd.css'; // 引入antd樣式表
.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;
}

如今的首頁是這個樣子:npm

逼格立馬就上來了有沒?json

改造HomePage

因爲如今有菜單了,就不須要右側那個HomePage裏的連接了,把他去掉,而後放個Welcome吧(HomeLayout也去掉了,在下面會提到):api

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{
  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 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 {
  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" 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={{width: '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={{width:'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,就能夠渲染出列表了

效果圖:

相關文章
相關標籤/搜索