react 引入antd 样式_react 项目实战(十)引入AntDesign组件库

本篇带你使用 AntDesign 组件库为我们的系统换上产品级的UI!

安装组件库

在项目目录下执行:npm i [email protected] -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应该包括LOGO和Menu部分,然后HomeLayout的children放置在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 (

ReactManager

用户管理}>

用户列表

添加用户

图书管理}>

图书列表

添加图书

{children}

);

}

}

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;

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 '../styles/home-page.less';

class Home extends React.Component {

// 构造器

constructor(props) {

super(props);

// 定义初始化状态

this.state = {};

}

render() {

return (

Welcome

);

}

}

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((

), 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 (

);

}

}

export default BookAdd;

src / pages / BookEdit.js

...

render () {

const {book} = this.state;

return book ? : 加载中...;

}

...

src / pages / BookList.js

...

render () {

...

return (

...

);

}

...

剩下的UserAdd.js、UserEdit.js、UserList.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 (

ReactManager

{getFieldDecorator('account',{

rules: [

{

required: true,

message: '请输入管理员帐号',

type: 'string'

}

]

})(

} />

)}

{getFieldDecorator('password',{

rules: [

{

required: true,

message: '请输入密码',

type: 'string'

}

]

})(

} />

)}

登录

);

}

}

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下的一系列方法,可以很方便地创造表单,上面有一段代码:

...

{getFieldDecorator('account',{

rules: [

{

required: true,

message: '请输入管理员帐号',

type: 'string'

}

]

})(

} />

)}

...

这里使用了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 (

this.handleSubmit(e)}>

{getFieldDecorator('name',{

rules: [

{

required: true,

message: '请输入用户名'

},

{

pattern: /^.{1,4}$/,

message: '用户名最多4个字符'

}

]

})(

)}

{getFieldDecorator('age',{

rules: [

{

required: true,

message: '请输入年龄',

type: 'number'

},

{

min: 1,

max: 100,

message: '请输入1~100的年龄',

type: 'number'

}

]

})(

)}

{getFieldDecorator('gender',{

rules: [

{

required: true,

message: '请选择性别'

}

]

})(

)}

提交

);

}

}

// 必须给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 标签布局,同

组件

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 (

{getFieldDecorator('name',{

rules: [

{

required: true,

message: '请输入书名'

}

]

})(

)}

{getFieldDecorator('price',{

rules: [

{

required: true,

message: '请输入价格',

type: 'number'

},

{

min: 1,

max: 99999,

type: 'number',

message: '请输入1~99999的数字'

}

]

})(

)}

{getFieldDecorator('owner_id',{

rules: [

{

required: true,

message: '请输入所有者ID'

},

{

pattern: /^\d*$/,

message: '请输入正确的ID'

}

]

})(

options={recommendUsers}

onChange={this.handleOwnerIdChange}

/>

)}

提交

);

}

}

// 必须给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组件加了两个事件处理onFocus、onBlur和state.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 (

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 && (

{

options.map((item, index) => {

return (

key={index}

className={index === activeItemIndex ? styles.active : ''}

onMouseEnter={() => this.handleEnter(index)}

onClick={() => this.handleChange(getItemValue(item))}

>

{item.text || item}

);

})

}

)}

);

}

}

/**

* 由于使用了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) => ¥{record.price / 100}

},

{

title: '所有者ID',

dataIndex: 'owner_id'

},

{

title: '操作',

render: (text, record) => (

this.handleEdit(record)}>编辑

title="确定要删除吗?"

okText="确定"

cancelText="取消"

onConfirm={() => this.handleDel(record)}>

删除

)

}

];

return (

);

}

}

/**

* 任何使用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 (

this.handleEdit(record)}>编辑

title="确定要删除吗?"

okText="确定"

cancelText="取消"

onConfirm={() => this.handleDel(record)}>

删除

);

}

}

];

return (

);

}

}

/**

* 任何使用this.context.xxx的地方,必须在组件的contextTypes里定义对应的PropTypes

*/

UserList.contextTypes = {

router: PropTypes.object.isRequired

};

export default UserList;

antd的Table组件使用一个columns数组来配置表格的列,这个columns数组的元素可以包含title(列名)、dataIndex(该列数据的索引)、render(自定义的列单元格渲染方法)等字段(更多配置请参考文档)。

然后将表格数据列表传入Table的dataSource,传入一个rowKey来指定每一列的key,就可以渲染出列表了。

效果图:

你可能感兴趣的:(react,引入antd,样式)