我们的管理系统已经有了图书、用户的增删改查以及登录功能了,可谓是五脏俱全,就是丑了点~
是不是已经有些厌倦我们系统里的白底黑字和灰色框框了?
打起精神,本篇带你使用 AntDesign 组件库为我们的系统换上产品级的UI!
安装组件库
- 在项目目录下执行:
npm i antd -S
安装组件包 - 执行:
npm i babel-plugin-import -D
安装一个babel插件用于做组件的按需加载(否则项目会打包整个组件库,非常大) - 根目录下新建
.roadhogrc
文件(别忘了前面的点,这是roadhog工具的配置文件,下面的代码用于加载上一个命令安装的import插件),写入:
{
"extraBabelPlugins": [
["import", {
"libraryName": "antd",
"libraryDirectory": "lib",
"style": "css"
}]
]
}
改造HomeLayout
我们计划把系统改造成这个样子:
上方显示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
这个样式文件,样式代码为:
.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;
}
现在的首页是这个样子:
逼格立马就上来了有没?
改造HomePage
由于现在有菜单了,就不需要右侧那个HomePage里的链接了,把他去掉,然后放个Welcome吧(HomeLayout也去掉了,在下面会提到):
// /src/pages/Home.js
import 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
,代码:
.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 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 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.js、UserEdit.js、UserList.js与上面Book对应的组件做相同更改。
还有登录页组件在下面说。
升级登录页面
下面来对登录页面进行升级,修改/src/pages/Login.js
文件:
import React from 'react';
import { Icon, Form, Input, Button, message } from 'antd';
import { post } from '../utils/request';
import style from '../styles/login-page.less';
const FormItem = Form.Item;
class Login extends React.Component {
constructor () {
super();
this.handleSubmit = this.handleSubmit.bind(this);
}
handleSubmit (e) {
e.preventDefault();
this.props.form.validateFields((err, values) => {
if (!err) {
post('http://localhost:3000/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={style.wrapper}>
<div className={style.body}>
<header className={style.header}>
ReactManager
</header>
<section className={style.form}>
<Form onSubmit={this.handleSubmit}>
<FormItem>
{getFieldDecorator('account', {
rules: [
{
required: true,
message: '请输入管理员账号',
type: 'string'
}
]
})(
<Input type="text" addonBefore={<Icon type="user"/>}/>
)}
</FormItem>
<FormItem>
{getFieldDecorator('password', {
rules: [
{
required: true,
message: '请输入密码',
type: 'string'
}
]
})(
<Input type="password" addonBefore={<Icon type="lock"/>}/>
)}
</FormItem>
<Button className={style.btn} type="primary" htmlType="submit">Sign In</Button>
</Form>
</section>
</div>
</div>
);
}
}
Login.contextTypes = {
router: React.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" addonBefore={<Icon type="user"/>}/>
)}
</FormItem>
...
这里使用了props.form.getFieldDecorator
方法来包装一个Input输入框组件,传入的第一个参数表示这个字段的名称,第二个参数是一个配置对象,这里设置了表单控件的校验规则rules(更多配置项请查看文档)。使用getFieldDecorator方法包装后的组件会自动表单组件的value以及onChange事件;此外,这里还用到了Form.Item
这个表单项目组件(上面的FormItem),这个组件可用于配置表单项目的标签、布局等。
在handleSubmit方法中,使用了props.form.validateFields
方法对表单的各个字段进行校验,校验完成后会调用传入的回调方法,回调方法可以接收到错误信息err和表单值对象values,方便对校验结果进行处理:
...
handleSubmit (e) {
e.preventDefault();
this.props.form.validateFields((err, values) => {
if (!err) {
post('http://localhost:3000/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';
import { Form, Input, InputNumber, Select, Button, message } from 'antd';
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里进行赋值
// 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) {
let editType = '添加';
let apiUrl = 'http://localhost:3000/user';
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('/user/list');
} 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: '400px'}}>
<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.contextTypes = {
router: React.PropTypes.object.isRequired
};
UserEditor = Form.create()(UserEditor);
export default UserEditor;
升级BookEditor
BookEditor中使用了AutoComplete组件,但是由于antd提供的AutoComplete组件有一些问题(见issue),这里暂时使用我们之前实现的AutoComplete。
// /src/components/BookEditor.js
import 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
因为要继续使用自己的AutoComplete组件,这里需要把组件中的原生input控件替换为antd的Input组件,并且在Input组件加了两个事件处理onFocus、onBlur和state.show,用于在输入框失去焦点时隐藏下拉框:
// /src/components/AutoComplete.js
import 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:
.options {
z-index: 2;
...
}
升级列表页组件
最后还剩下两个列表页组件,我们使用antd的Table组件来实现这两个列表:
// /src/pages/BookList.js
import 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.js
import 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,就可以渲染出列表了。
升级后的效果
终于折腾完了,我们来看一看最终的效果,兴奋一下: