我们的管理系统已经有了图书、用户的增删改查以及登录功能了,可谓是五脏俱全,就是丑了点~
是不是已经有些厌倦我们系统里的白底黑字和灰色框框了?
打起精神,本篇带你使用 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,就可以渲染出列表了。
终于折腾完了,我们来看一看最终的效果,兴奋一下:
新闻热点
疑难解答