0%

Ant Design 框架总结

Ant Design 常用命令汇总

一、安装

1. 安装脚手架工具

antd-init 是一个用于演示 antd 如何使用的脚手架工具,真实项目建议使用 dva-cli

更多功能请参考 脚手架工具开发工具文档

除了官方提供的脚手架,您也可以使用社区提供的脚手架和范例:

更多脚手架可以查看 脚手架市场

创建一个项目

使用命令行进行初始化。

1
2
3
4
5
$ mkdir antd-demo && cd antd-demo
$ antd-init
antd-init 会自动安装 npm 依赖,若有问题则可自行安装。

若安装缓慢报错,可尝试用 cnpm 或别的镜像源自行安装:rm -rf node_modules && cnpm install。

3. 使用组件

脚手架会生成一个 Todo 应用实例(一个很有参考价值的 React 上手示例),先不管它,我们用来测试组件。

直接用下面的代码替换 index.js 的内容,用 React 的方式直接使用 antd 组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import React from 'react';
import ReactDOM from 'react-dom';
import { LocaleProvider, DatePicker, message } from 'antd';
// 由于 antd 组件的默认文案是英文,所以需要修改为中文
import zhCN from 'antd/lib/locale-provider/zh_CN';
import moment from 'moment';
import 'moment/locale/zh-cn';

moment.locale('zh-cn');

class App extends React.Component {
constructor(props) {
super(props);
this.state = {
date: '',
};
}
handleChange(date) {
message.info('您选择的日期是: ' + date.toString());
this.setState({ date });
}
render() {
return (
<LocaleProvider locale={zhCN}>
<div style={{ width: 400, margin: '100px auto' }}>
<DatePicker onChange={value => this.handleChange(value)} />
<div style={{ marginTop: 20 }}>当前日期:{this.state.date.toString()}</div>
</div>
</LocaleProvider>
);
}
}

ReactDOM.render(<App />, document.getElementById('root'));

4. 开发调试

一键启动调试,访问 http://127.0.0.1:8000 查看效果。

1
$ npm start

5. 构建和部署

1
$ npm run build

入口文件会构建到 dist 目录中,你可以自由部署到不同环境中进行引用。

二、Ant Design Pro 安装

有两种方式进行安装:

1. 直接 clone git 仓库

1
2
$ git clone --depth=1 https://github.com/ant-design/ant-design-pro.git my-project
$ cd my-project

2. 使用命令行工具

你可以使用集成化的命令行工具 ant-design-pro-cli

1
2
3
$ npm install ant-design-pro-cli -g
$ mkdir my-project && cd my-project
$ pro new # 安装脚手架

三、目录结构

我们已经为你生成了一个完整的开发框架,提供了涵盖中后台开发的各类功能和坑位,下面是整个项目的目录结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
├── mock                     # 本地模拟数据
├── public
│ └── favicon.ico # Favicon
├── src
│ ├── assets # 本地静态资源
│ ├── common # 应用公用配置,如导航信息
│ ├── components # 业务通用组件
│ ├── e2e # 集成测试用例
│ ├── layouts # 通用布局
│ ├── models # dva model
│ ├── routes # 业务页面入口和常用模板
│ ├── services # 后台接口服务
│ ├── utils # 工具库
│ ├── g2.js # 可视化图形配置
│ ├── theme.js # 主题配置
│ ├── index.ejs # HTML 入口模板
│ ├── index.js # 应用入口
│ ├── index.less # 全局样式
│ └── router.js # 路由入口
├── tests # 测试工具
├── README.md
└── package.json

四、本地开发

安装依赖。

1
$ npm install

如果网络状况不佳,可以使用 cnpm 进行加速。

1
$ npm start

启动完成后会自动打开浏览器访问 http://localhost:8000,你看到下面的页面就代表成功了。

1. 创建一个项目

可以是已有项目,或者是使用 dva / create-react(-native)-app 新创建的空项目,你也可以从 官方示例 脚手架里拷贝并修改

参考更多官方示例集 或者你可以利用 React 生态圈中的 各种脚手架

2. 安装

1
$ npm install antd-mobile --save

3. 使用

Web 使用方式
React Native 使用方式
入口页面 (html 或 模板) 相关设置:

引入 FastClick 并且设置 html meta

引入 Promise 的 fallback 支持 (部分安卓手机不支持 Promise)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>
<html>
<head>
<!-- set `maximum-scale` for some compatibility issues -->
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" />
<script src="https://as.alipayobjects.com/g/component/fastclick/1.0.6/fastclick.js"></script>
<script>
if ('addEventListener' in document) {
document.addEventListener('DOMContentLoaded', function() {
FastClick.attach(document.body);
}, false);
}
if(!window.Promise) {
document.writeln('<script src="https://as.alipayobjects.com/g/component/es6-promise/3.2.2/es6-promise.min.js"'+'>'+'<'+'/'+'script>');
}
</script>
</head>
<body></body>
</html>

组件使用实例:

1
2
import { Button } from 'antd-mobile';
ReactDOM.render(<Button>Start</Button>, mountNode);

引入样式:

1
import 'antd-mobile/dist/antd-mobile.css';  // or 'antd-mobile/dist/antd-mobile.less'

4. 按需加载

注意:强烈推荐使用。

下面两种方式都可以只加载用到的组件,选择其中一种方式即可。

1
2
3
4
5
6
7
8
9
10
// .babelrc or babel-loader option
{
"plugins": [
["import", { libraryName: "antd-mobile", style: "css" }] // `style: true` 会加载 less 文件
]
}
然后只需从 antd-mobile 引入模块即可,无需单独引入样式。

// babel-plugin-import 会帮助你加载 JS 和 CSS
import { DatePicker } from 'antd-mobile';
  • 手动引入
1
2
3
import DatePicker from 'antd-mobile/lib/date-picker';  // 加载 JS
import 'antd-mobile/lib/date-picker/style/css'; // 加载 CSS
// import 'antd-mobile/lib/date-picker/style'; // 加载 LESS

更多增强 (非必须):

如何自定义主题?见此文档, 基于 antd-mobile 的自定义 UI 库:web-custom-ui / web-custom-ui-pro

antd 常用组件的问题及实现

在基于 react 的 web 后台开发中,常常会使用到 antd 组件,可以使用现有所提供的,也可以自己再次封装使用。基于平时开发中遇到较多关于 antd 组件问题的情况,记录一下平时常用的代码实现。

1.Radio

单选框

1
2
3
4
5
6
7
8
9
10
11
<FormItem {...formItemLayout} label="是否启用" required>
{getFieldDecorator('enabled',{
initialValue: data.enabled,
rules: [{ required: true, message: '请选择是否启用' }],
})(
<RadioGroup>
<Radio value={true}>启用</Radio>
<Radio value={false}>禁用</Radio>
</RadioGroup>
)}
</FormItem>

2.Select

下拉选择,数据源根据接口获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<FormItem {...formItemLayout} label='xxx'>
{getFieldDecorator('feedHabit', {
initialValue: (detail && detail.feedHabit) || undefined,
rules: [
{ required: false, message: 'xxx' }
]
})(
<Select
showSearch
allowClear
placeholder='xxx'
filterOption={(inputValue, option) => option.props.children.indexOf(inputValue) > -1}
notFoundContent={
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
style={{ margin: 8 }}
>
<Button
type='primary'
onClick={() => dispatch({ type: `${namespace}/getFeedHabitList` })}
>刷新
</Button>
</Empty>
}
>
{
feedHabitList && feedHabitList.map((item, index) => (
<Select.Option key={item.code} value={item.code}>{item.name}</Select.Option>
))
}
</Select>
)}
</FormItem>

3.Switch

开发的切换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<Form.Item {...formItemLayout} label='xxx'>
{getFieldDecorator('enabledAutoFeed', {
initialValue: (detail && detail.enabledAutoFeed) || undefined,
valuePropName: 'checked', // 记得加上这个属性
rules: [
{required: false, message: 'xxx'}
]
})(
<Switch
checkedChildren='是'
unCheckedChildren='否'
onChange={(val, event) => {
event.preventDefault();
event.stopPropagation();
this.setState({ enabledAutoFeed: val }) // 将当前改变的值存至state中,提交时直接用state中存储的值就好了
}}
/>
)}
</Form.Item>

4.Cascader

情况一:只有两个层级数

此情况组件用例是在一个新增/编辑的弹出框中使用,新增时不带数据,可选择;编辑时带出默认数据,也可选择;层级是 2 级。
开发思路:

  • (1)新增时,不带默认数据出来,但是已经请求到第一层级的数据,点击第一层级中的某一项 item 时,加载请求第二层级的数据给到 item 的 children 中,这样就可以显示第二层级的数据了;
  • (2)编辑时,带出默认数据,此时 defaultMaterialIds 应该是有两个 id 值的,如:[parentId, sonId],但是在点编辑弹出弹框而还没有点击级联组件时,只显示了第一层级的数据,因为此时页面起始已经有第一层级的数据源,而不知道第二层级的数据源,只单纯的有第二层级项的 id(也就是 sonId),是不知道它的 label(或 name)的,思路是:在点击编辑的时候,拿到它的第一层级 id(也就是 parentId),请求第二层级的数据就好了(其实就是点击编辑时再次调用 loadData 方法~~~)

附上实现逻辑代码:

1.弹框中(editModal.jsx):

render 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<Form.Item {...formItemLayout} label='物料名称'>
{getFieldDecorator(`materialId`, {
// defaultMaterialIds 是默认的数据(一个数组形式),如:[parentId, sonId]
initialValue: defaultMaterialIds || []
})(
<Cascader
fieldNames={fieldNames}
options={materialLabel}
loadData={this.loadData}
onChange={this.onChange}
placeholder='请选择物料名称'
changeOnSelect
/>
)}
</Form.Item>

相应方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 级联-选择后的回调
onChange = (value, selectedOptions) => {
const { dispatch, namespace } = this.props;
const name = [];
const id = [];
const code = [];
// console.log(value, selectedOptions);

selectedOptions.map((v) => {
name.push(v.name);
id.push(v.id);
code.push(v.code)
});

this.setState({
currentId: value
})
};


// 动态加载选项
loadData = (selectedOptions) => {
const { dispatch, namespace, materialLabel, form: { setFieldsValue }, } = this.props;
const targetOption = selectedOptions[selectedOptions.length - 1];
targetOption.loading = true;
// 异步加载数据,targetOption 是选中的那一项数据
dispatch({
type:`${namespace}/getMaterialList`,
payload:{ targetOption },
callback:(res)=>{
targetOption.loading = false;
}
})

}

2.view.jsx 中(editModal 在 view 中被引用):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 点击编辑时,传第一级id,请求第二级数据显示
loadData = (parentId) => {
const { dispatch, pigfarmConfListIndexMod: { materialLabel } }= this.props;
// materialLabel为第一层级数据源
const targetOption = materialLabel.find(item => item.id === parentId)

// 异步加载数据
dispatch({
type:`${namespace}/getMaterialList`,
payload:{ targetOption },
})
}

// 点击编辑时调用的方法
showModal = ({data = null, type = 'add' }) => {
const { dispatch } = this.props;
if(type === 'add') {
dispatch({
type: `${namespace}/updateStore`,
payload: { showModal: true, detail: data, modalType: type, }
});
}else {
// 编辑时拿到parentId请求第二层级数据
this.loadData(data.parentId);
dispatch({
type: `${namespace}/updateStore`,
payload: { showModal: true, detail: data, defaultMaterialIds: [data.parentId, data.materialId], modalType: type, }
});
}
}

3.mod.js 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 获取物料列表---这是第二层级
*getMaterialList({ payload, callback }, { put, call, select }) {
// ...
// targetOption 是点击的第一层级的项
let targetOption = payload.targetOption;
const children = [];
// 第一层级数据源materialLabel是一个对象数组,数组中的每一项都是一个对象,带有id,name等属性
const _materialLabel = [...materialLabel]
// payload.targetOption.id是点击的第一层级的项,用来获取第二层级的数据源
const result = yield call(urlMaterialList, {id: payload.targetOption.id});
// idx是在第一层级中点击的项的位置
const idx = _materialLabel.findIndex(item=>item.id===targetOption.id)
// 深拷贝第一层级中选中的项,后面会加上一个children属性,而children就是第二层级的数据源
// 把这个第二层级数据源 替换掉 第一层级中选中的那个项
// 然后赋值给整个第一层级的数据源,因为第一层级选中的那个项已经加上了children属性,可以显示第二层级的数据了

const materialItem = cloneDeep(targetOption);
// ...
if(result.data.length > 0) {
result.data.map(item => {
// 因为第2级就是末级了,所以isLeaf为true
let obj = { ...item, isLeaf: true };
children.push(obj);
})
// 当前项加上children ,loading 属性
materialItem.children = children;
materialItem.loading = false;
// 然后替换第一层级中选中的那一项
_materialLabel.splice(idx,1,materialItem)
yield put({
type: 'updateStore',
payload: {
materialList: result.data,
materialLabel:_materialLabel
}
});
callback && callback();
}
// ...
},

情况二:多个层级(以下例子为 4 级:地址的省-市-区-镇)

此情况是用于获取一个中国地区各省市级地址信息的获取及展示/显示;
在平时开发项目中,一般会封装好一个组件,然后多个地方使用,提高开发效率。

开发思路:一开始获得第一层级的地址 a,将 a 的 id 传至后端请求 a 的第二层级 b,b 的 id 又请求第三层级 c…以此类推,将请求到的数据 b 每一项加上 isLeaf 属性(末级为 true,非末级为 false),然后将这个新的 b 赋值给 a 的 children,就可以显示第二层级 b 的数据了,依此类推,第三第四级也是…

以下是有关子组件及父组件的逻辑及实现:(主要看注释吧~)

1.子组件中(封装 export 名为 CascaderAddress ):

1
2
3
4
5
6
7
8
9
<Cascader
// 在实际编辑器中,应该不能这样注释的,这里是为理解外加的注释
{...options} // 可选项配置,如placeHolder,changeOnSelect等,当然也可直接分开写
value={defaultAddress} // 指定的或默认的数据
options={addressOptions} // 级联地址的数据源
loadData={this.loadAddressData} // 动态加载数据(如点击第一层,加载第二层...)
onChange={this.onChangeAddress} // 点击/选择完成后的回调
fieldNames={fieldNames} // 自定义数据源options的label,value,children
/>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// 地址选择
onChangeAddress = (value, selectedOptions) => {
const { changeCallback } = this.props;
const dataString = [];
const ids = [];

selectedOptions.map((v) => {
dataString.push(v.areaName);
ids.push(v.id);
});
// 选择完成后的回调,将选择地址的id,name,以及其他信息存起来备用
changeCallback({data: selectedOptions, ids, dataString});
}

// 获取地址
loadAddressData = selectedOptions => {
const { loadData, addressOptions } = this.props;
const targetOption = selectedOptions[selectedOptions.length - 1];
targetOption.loading = true;
// 将当前选择项的id请求后端拿到下一层级数据(加上isLeaf属性),赋给当前选择项的children,就可以显示出下一层级的数据了
this.getAddressOptions(targetOption.id, (list) => {
targetOption.loading = false;
targetOption.children = list;

loadData(addressOptions);
})
};

// 请求地址数据
getAddressOptions = (parentId, callback) => {
const { max } = this.state;
const { request, url } = this.props;
const params = {
'id': parentId || '0', // '0'表示请求第一层级,看后端需要传什么
}

request({
// 拿点击的那一项的id请求下一层级的数据(也就是children)
url: `${url}/${params.id}`,
method: 'get',
data: params,
})
.then((res) => {
if(res.code === '000000') {
let list = []
if(res.data && res.data.length) {
// 判断是否为末级,为每一项加上isLeaf属性
list = res.data.map(item => ({ ...item, isLeaf: item.areaLevel > max }));
}
// 回调显示当前list数据
callback(list);
} else {
message.error(res.msg);
}
})
}

2.父组件中引用:

(1)view.jsx

1
2
3
4
5
6
7
8
9
10
11
// 注意farmAddress为当前级联控件的唯一标识
<Form.Item {...formItemLayout} label='地址'>
{getFieldDecorator('farmAddress', {
initialValue: defaultAddressIds || undefined,
rules: [
{ required: true, message: '请输入地址' },
],
})(
<CascaderAddress {...cascaderProps}/>
)}
</Form.Item>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 级联数据值名称配置
const cascaderOptionLabel = {
label: 'areaName', // 显示的是label,自定义为areaName
value: 'id', // 传值的是value,自定义为id
children: 'children'
}

// 级联地址选择配置项目
const cascaderProps = {
request,
url: `xxx`,
addressOptions, // 数据源
defaultAddress: defaultAddressIds, // 默认数据,
fieldNames: cascaderOptionLabel, // 自定义配置项
loadData: (data) => { // 动态加载数据
dispatch({ type: `${namespace}/updateStore`, payload: { addressOptions: [...data] } })
},
// 选择之后的回调,farmAddress为表单控件中存储的数据,可直接/处理之后提交~
changeCallback: ({data, dataString, ids}) => {
setFieldsValue({ farmAddress: ids }); // 设置form表单数据
dispatch({ type: `${namespace}/updateStore`, payload: { defaultAddressIds: ids } }); // 存储至mod备用
},
options: {
placeholder: '请选择地址',
changeOnSelect: true,
}
}

}

(2)mod.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// 获取地址列表
*getAddressOptions({ payload = { id: '' }, callback }, { put, call, select }) {
...
// ...假设调用接口请求到数据存至res.data
let addressOptions = []
if(res.data && res.data.length) {
addressOptions = res.data.map(item => ({ id: item.id, value: item.id, areaName: item.areaName, isLeaf: false }))
}

yield put({
type: 'updateStore',
payload: {
addressOptions,
}
});
},


// 根据ID获取该地址信息包含各级数据
* getAreaInfo({ payload, callback }, { call, put, select }) {
...
// ...
let addressOptions = [];
if(result.data && result.data.length) {
addressOptions = result.data[0].list;

addressOptions.map( v => { // 省
v.isLeaf = false;
if(v.id === result.data[0].selected.id) {
v.children = result.data[1].list;

v.children.map( v1 => { // 市
v1.isLeaf = false;
if(v1.id === result.data[1].selected.id) {
v1.children = result.data[2].list;

v1.children.map( v2 => { // 区
v2.isLeaf = false;
if(v2.id === result.data[2].selected.id) {
v2.children = result.data[3].list;
}
})
}
})
}
})
}

yield put({
type: 'updateStore',
payload: {
addressOptions,
detailAddress: result.data,
// 后端返回的默认地址id,组成一个数组展示
defaultAddressIds: [detail.addressProvince, detail.addressCity, detail.addressZone, detail.addressTown]
}
});

callback && callback();
// ...
},
}

其实两个情况很类似的,都是将取到的下一级的数据,每一项都加上 isLeaf 属性,然后将这个新的数据加上 children,loading 属性,替换上一级中被选择的那一项,就可以显示下一级的数据了~~~

5.Table

某些列表,后端不做分页,需要前端分页时,table 的一般配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
<Table
bordered // 给整个table加上边框
dataSource={tableList}
columns={tableColumns}
rowkey={record => record.id}
pagination={{
showQuickJumper: true,
showSizeChanger: true,
showTotal: total => `共${total}条`,
pageSizeOptions:['10','20','50','100'],
// pageSize: 5, // 固定5条一页,有pageSize就不用pageSizeOptions了
}}
/>