我們的管理系統已經有了圖書、用戶的增刪改查以及登錄功能了,可謂是五臟俱全,就是丑了點~
是不是已經有些厭倦我們系統里的白底黑字和灰色框框了?
打起精神,本篇帶你使用 AntDesign 組件庫為我們的系統換上產品級的UI!
npm i antd -S
安裝組件包執行:npm i babel-plugin-import -D
安裝一個babel插件用于做組件的按需加載(否則項目會打包整個組件庫,非常大)根目錄下新建.roadhogrc
文件(別忘了前面的點,這是roadhog工具的配置文件,下面的代碼用于加載上一個命令安裝的import插件),寫入:{ "extraBabelPlugins": [ ["import", { "libraryName": "antd", "libraryDirectory": "lib", "style": "CSS" }] ]}我們計劃把系統改造成這個樣子:
上方顯示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
這個樣式文件,樣式代碼為:
現在的首頁是這個樣子:
逼格立馬就上來了有沒?
由于現在有菜單了,就不需要右側那個HomePage里的鏈接了,把他去掉,然后放個Welcome吧(HomeLayout也去掉了,在下面會提到):
// /src/pages/Home.jsimport 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
,代碼:
怎么樣,還丑嗎?
現在的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 BookEditorimport 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
文件:
新建樣式文件/src/styles/login-page.less
,樣式代碼:
酷酷的登錄頁面:
改造后的登錄頁組件使用了antd提供的Form組件,Form組件提供了一個create方法,和我們之前寫的formProvider一樣,是一個高階組件。使用
Form.create({ ... })(Login)
處理之后的Login組件會接收到一個props.form
,使用props.form
下的一系列方法,可以很方便地創造表單,上面有一段代碼:
這里使用了props.form.getFieldDecorator
方法來包裝一個Input輸入框組件,傳入的第一個參數表示這個字段的名稱,第二個參數是一個配置對象,這里設置了表單控件的校驗規則rules(更多配置項請查看文檔)。使用getFieldDecorator方法包裝后的組件會自動表單組件的value以及onChange事件;此外,這里還用到了Form.Item
這個表單項目組件(上面的FormItem),這個組件可用于配置表單項目的標簽、布局等。
在handleSubmit方法中,使用了props.form.validateFields
方法對表單的各個字段進行校驗,校驗完成后會調用傳入的回調方法,回調方法可以接收到錯誤信息err和表單值對象values,方便對校驗結果進行處理:
升級UserEditor和登錄頁面組件類似,但是在componentWillMount里需要使用this.props.setFieldsValue
將editTarget的值設置到表單:
BookEditor中使用了AutoComplete組件,但是由于antd提供的AutoComplete組件有一些問題(見issue),這里暫時使用我們之前實現的AutoComplete。
// /src/components/BookEditor.jsimport 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組件,這里需要把組件中的原生input控件替換為antd的Input組件,并且在Input組件加了兩個事件處理onFocus、onBlur和state.show,用于在輸入框失去焦點時隱藏下拉框:
// /src/components/AutoComplete.jsimport 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:
最后還剩下兩個列表頁組件,我們使用antd的Table組件來實現這兩個列表:
// /src/pages/BookList.jsimport 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>¥{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.jsimport 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,就可以渲染出列表了。
終于折騰完了,我們來看一看最終的效果,興奮一下:
新聞熱點
疑難解答