0%

antd 常用知识点和小技巧总结

1 form 表单中 FormItem 的布局

使用 getFieldDecorator 包裹的输入框或者 Select,必须是在最外层,也就是只有一层,否则,检验会一直不通过,所以,需要重新布局应该在 getFieldDecorator 的外层添加父节点,而不应该在里面。

例:

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
<FormItem
{...formItemLayout}
label="所属应用"
>
<div>
{getFieldDecorator('apiwgAppName', {
rules: [{ required: false, message: '请选择' }],
initialValue: apiwgAppName || ""
})(
<Input disabled={this.store.data.apiId ? true : false}
className="control-special" readOnly style={{ width: "70%" }}
onClick={this.showModal.bind(this, "apiwgApp")} />
)}
<Button className="btn-modal" type="primary" onClick={this.showModal.bind(this, "apiwgApp")}
disabled={this.store.data.apiId ? true : false}
>选择所属应用
</Button>
<a
style={{ marginLeft: '8px' }}
onClick={this.openNewAppDlg.bind(this)}
className={`api-add ${
this.store.data.apiId ? 'disabled' : ''
}`}
>
+新增应用
</a>
</div>
</FormItem>

2 form 表单,FormItem 的 rules 中新增 validator,实时请求校验

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
<FormItem
labelCol={{ span: 8 }}
wrapperCol={{ span: 15 }}
label="菜单名称"
>
{form.getFieldDecorator('menuName', {
rules: [
{ required: true, message: '菜单名称不能为空' },
{ type: 'string', max: 30, message: '菜单名称过长' },
{ validator: this.handleCheckName },
{ whitespace: true, message: '请输入非空白内容' }
],
initialValue: this.props.menuSysData.menuName,
})(
<Input
// placeholder="请输入菜单名称"
disabled={disableFlag}
/>
)}
</FormItem>



// 实时校验
handleCheckName = (rule, value, callback) => {
const { checkName, actionType } = this.state;
if (!this.trim(value) || (checkName && actionType === 'M' && this.trim(value) === checkName)) {
callback();
return;
}
let params = {
menuName: value,
state: "00A"
};
MenuSysService.checkMenuName(params).then(result => {
if (!result || !result.resultObject) {
return;
}
let code = result.resultObject.code;
if (code && code > 0) {
callback('系统名称已存在!');
}
callback();
});
}

3 利用 validator 和正则,验证中文

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
<FormItem
hasFeedback={!disableFlag}
labelCol={{ span: 6 }}
wrapperCol={{ span: 15 }}
label="账号" >
{form.getFieldDecorator('userCode', {
initialValue: '',
rules: [
{ required: !disableFlag, validator: this.usercodeValidator },
{ type: 'string', max: 30, message: '账号过长' },
{ whitespace: true, message: '内容不能为空' }
],
})(
<Input placeholder="请输入账号" disabled={account} maxLength="30" autoComplete="false" />)}
</FormItem>



usercodeValidator = (rule, value, callback) => {
const { userData } = this.props;
if (!value) {
callback('内容不能为空');
return;
}

// !!!中文验证
const reg = /[\u4E00-\u9FA5]{1,4}/; /*定义验证表达式*/
if (reg.test(value)) { /*进行验证*/
callback('账号不能为中文');
return;
}


if (userData.userCode === value) {
callback();
}
else {
let params = {
userCode: value + "", // 查一下有没有这个编码
useState: '10301'
};
SysUserMgService.checkUserCode(params).then(result => {
if (!result || result.code !== '0') {
callback(result.message);
return;
}
if (result.resultObject && result.resultObject.num !== 0) {
callback('该账号已存在');
return;
}
callback();
});
}
}

4 form.validateFields 直接获取表单的值

1
2
3
4
this.props.form.validateFields((err, fieldsValue) => {
if (err) return;
this.handleSubmit(fieldsValue);
});

5 form 表单提交 htmlType,改为 onClick

说明:因为之前遇到过使用 htmlType 提交表单会有问题,但是改为 onClick 后,就没问题了,所以,也记录一下。
htmlType 是官网使用的方式,具体问题本人当时忘记截个图了。

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
63
64
65
66
<Form layout="inline" onSubmit={this.handleSubmit}>
<FormItem
validateStatus={userNameError ? 'error' : ''}
help={userNameError || ''}
>
{getFieldDecorator('userName', {
rules: [{ required: true, message: 'Please input your username!' }],
})(
<Input prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />} placeholder="Username" />
)}
</FormItem>
<FormItem
validateStatus={passwordError ? 'error' : ''}
help={passwordError || ''}
>
{getFieldDecorator('password', {
rules: [{ required: true, message: 'Please input your Password!' }],
})(
<Input prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />} type="password" placeholder="Password" />
)}
</FormItem>
<FormItem>
<Button
type="primary"
htmlType="submit"
disabled={hasErrors(getFieldsError())}
>
Log in
</Button>
</FormItem>
</Form>



// 改变后:
<Form layout="inline" >
<FormItem
validateStatus={userNameError ? 'error' : ''}
help={userNameError || ''}
>
{getFieldDecorator('userName', {
rules: [{ required: true, message: 'Please input your username!' }],
})(
<Input prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />} placeholder="Username" />
)}
</FormItem>
<FormItem
validateStatus={passwordError ? 'error' : ''}
help={passwordError || ''}
>
{getFieldDecorator('password', {
rules: [{ required: true, message: 'Please input your Password!' }],
})(
<Input prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />} type="password" placeholder="Password" />
)}
</FormItem>
<FormItem>
<Button
type="primary"
disabled={hasErrors(getFieldsError())}
onClick={() => this.handleSubmit()}
>
Log in
</Button>
</FormItem>
</Form>

6 Input 组件,利用 maxLength 属性,限制最大输入内容长度

1
2
3
4
5
6
<Input
placeholder="请输入账号"
disabled={account}
maxLength="30"
autoComplete="off"
/>

7 InputNumber 只能输入数字:

1
2
3
4
5
6
7
<InputNumber
formatter={value => value}
parser={value => parseInt(value) || ''}
style={{ width: '100%' }}
step={1}
onChange={(val) => this.onChangeIpt(1, val)}
/>

8 menu 实现回缩效果注意点

说明:menu 必须放在 Sider 中,才能实现缩回去的,这个有特定的布局。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<Sider
style={{ background: '#1D2023', height: '100%' }}
trigger={null}
collapsible
collapsed={this.state.collapsed}
width={140}
collapsedWidth={40}
>
<BaseMenu
toggle={this.toggle}
collapsed={this.state.collapsed}
history={history}
location={location}
/>
</Sider>

9 左侧菜单调整宽度设置

说明:通过在 Sider 组件,设置 width,调整菜单的宽度,通过设置 collapsedWidth,调整缩进的宽度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<Sider
style={{ background: '#1D2023', height: '100%' }}
trigger={null}
collapsible
collapsed={this.state.collapsed}
width={140}
collapsedWidth={40}
>
<BaseMenu
toggle={this.toggle}
collapsed={this.state.collapsed}
history={history}
location={location}
/>
</Sider>

10 表格 Columns 字段 id 页面不展示情况

说明:一般而言,表格 Columns 字段 id 是在界面不展示的,但是,对于有些逻辑的处理,又是需要的,可以使用相应样式隐藏的处理方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
常规展示的情况:
{
title: '序号',
dataIndex: 'algoId',
key: 'algoId'
},

不展示id字段:
{
title: '',
dataIndex: 'algoId',
key: 'algoId',
width: 0,
render: item => {
return (
<span style={{ display: 'none' }} title={item}>
{item}
</span>
);
}
},

11 自定义 Modal

查看元素可知,Modal 是在界面构建完成之后,由 js 控制,动态的添加,所以想事先获取 ant-modal-body 中 DOM 元素的节点是不可能的,但是一般情况也不会去获取它。
自定义 Modal,解决上述的问题。

关键代码

说明:

1:因为我们使用的是 antd,所以,下面的样式是不需要引入的。这个跟 antd 的 Modal 样式重复。

2:Modal 的隐藏和显示,是通过控制 class 为 ant-modal-mask 和 ant-modal-wrap 两个 div 的显示和隐藏。

  • 通过给 ant-modal-mask 的 div,添加另外一个 className:ant-modal-mask-hidden,来控制其隐藏,也可以通过 display 来控制。
  • 通过给 ant-modal-wrap 设置行内样式 display: none,来控制其隐藏。不过,也可以使用 className,随便都可以。
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
界面布局:

<div className="ant-modal-mask" ></div>
<div tabIndex="-1" className="ant-modal-wrap " role="dialog" aria-labelledby="rcDialogTitle0" style={{}}>
<div role="document" className="ant-modal" style={{ width: '920px' }}>
<div className="ant-modal-content">
<div className="ant-modal-header">
...
</div>
<div className="ant-modal-body" style={{ background: 'rgb(16, 16, 17)' }}>
...
</div>
</div>
</div>
<div tabIndex="0" style={{ width: '0px', height: '0px', overflow: 'hidden' }}>
sentinel
</div>
</div>


样式:
.ant-modal-mask { // 遮罩层
position: fixed;
top: 0;
right: 0;
left: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.65);
height: 100%;
z-index: 1000;
filter: alpha(opacity=50);
}

.ant-modal-wrap {
position: fixed;
overflow: auto;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1000;
-webkit-overflow-scrolling: touch;
outline: 0;
}

.ant-modal {
font-family: "Chinese Quote", -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-size: 14px;
font-variant: tabular-nums;
line-height: 1.5;
color: rgba(0, 0, 0, 0.65);
-webkit-box-sizing: border-box;
box-sizing: border-box;
margin: 0;
padding: 0;
list-style: none;
position: relative;
width: 920px;
margin: 0 auto;
top: 100px;
padding-bottom: 24px;
}

12 Select 组件清除选框内容

通过给 Select 组件新增 allowClear 属性。注意:allowClear 也会触发 onChange 方法,所以,也要单独处理一下,因为 value 和 element 为 undefined。

1
2
3
<Select {...this.props} placeholder="请选择" allowClear={true} >
...
</Select>

13 antd、mobx @注入的顺序

image

顺序

14 解决 table 组件, key 警告

一般都是使用 rowkey 方法一解决(后台数据要保证没有重复);

14.1 方法一:一般都是使用 rowkey 解决(后台数据要保证没有重复);

image

使用 rowKey

14.2 方法二:dataSource 数据新增 key

image

dataSource 数据新增 key

15 Form.create 方式

15.1 方式一:@注解

1
@Form.create({})

15.2 方式二:高阶写法

1
export default (Form.create({})(APP));

16 Form initialValue 值编辑后,表单的值不改变问题

16.1 方法一

其实,只要编辑成功后,回调调用 form.resetFields(),就可以了,如果
是使用 modal 框弹出的表单,就可以直接使用 destroyOnClose = {true} 属性。

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
import React from 'react';
import { Input, Modal, Form } from 'antd';
import styles from './UserModal.less';

const FormItem = Form.Item;

const UserModal = ({ currentItem, dispatch, form, visible }) => {

function handleOk() {
form.validateFields((err, fieldsValue) => {
if (err) return;
dispatch({
type: 'demo/update',
payload: {
currentItem: fieldsValue
}
});
});
}

function handleCancel() {
dispatch({
type: 'demo/hideModal'
})
}

const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 4 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 20 },
},
};

const { getFieldDecorator } = form;

return (
<div className={styles.root}>
<Modal
title="编辑"
visible={visible}
onOk={() => handleOk()}
onCancel={() => handleCancel()}
destroyOnClose={true}
>
<Form>
<FormItem
{...formItemLayout}
label="用户名"
>
{getFieldDecorator('name', {
initialValue: currentItem.name,
rules: [{
required: true, message: 'Please input your name!',
}],
})(
<Input placeholder="请输入用户名" />
)}
</FormItem>
<FormItem
{...formItemLayout}
label="年龄"
>
{getFieldDecorator('age', {
initialValue: currentItem.age,
rules: [{
required: true, message: 'Please input your age!',
}],
})(
<Input placeholder="请输入年龄" />
)}
</FormItem>
<FormItem
{...formItemLayout}
label="地址"
>
{getFieldDecorator('address', {
initialValue: currentItem.address,
rules: [{
required: true, message: 'Please input your address!',
}],
})(
<Input placeholder="请输入地址" />
)}
</FormItem>
</Form>
</Modal>
</div>
)
}

export default (Form.create({})(UserModal));


主要代码:destroyOnClose={true}
<Modal
title="编辑"
visible={visible}
onOk={() => handleOk()}
onCancel={() => handleCancel()}
destroyOnClose={true}
>
...
</Modal>

16.2 方法二

如果是 class 类,可以使用钩子。

1
2
3
4
5
componentDidUpdate = (prevProps, prevState) => {
if (!prevProps.visible) {
prevProps.form.resetFields();
}
};

代码参考:

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
import React from 'react';
import { Input, Modal, Form } from 'antd';
import styles from './UserModal.less';

const FormItem = Form.Item;

@Form.create({})
class UserModal extends React.PureComponent {

componentDidUpdate = (prevProps, prevState) => {
if (!prevProps.visible) {
prevProps.form.resetFields();
}
};

handleOk = () => {
const { dispatch, form } = this.props;
form.validateFields((err, fieldsValue) => {
if (err) return;
dispatch({
type: 'demo/update',
payload: {
currentItem: fieldsValue
}
});
});
}

handleCancel = () => {
const { dispatch } = this.props;
dispatch({
type: 'demo/hideModal'
})
}

render() {
const { currentItem, form, visible } = this.props;
const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 4 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 20 },
},
};
const { getFieldDecorator } = form;
return (
<div className={styles.root}>
<Modal
title="编辑"
visible={visible}
onOk={() => this.handleOk()}
onCancel={() => this.handleCancel()}
>
<Form>
<FormItem
{...formItemLayout}
label="用户名"
>
{getFieldDecorator('name', {
initialValue: currentItem.name,
rules: [{
required: true, message: 'Please input your name!',
}],
})(
<Input placeholder="请输入用户名" />
)}
</FormItem>
<FormItem
{...formItemLayout}
label="年龄"
>
{getFieldDecorator('age', {
initialValue: currentItem.age,
rules: [{
required: true, message: 'Please input your age!',
}],
})(
<Input placeholder="请输入年龄" />
)}
</FormItem>
<FormItem
{...formItemLayout}
label="地址"
>
{getFieldDecorator('address', {
initialValue: currentItem.address,
rules: [{
required: true, message: 'Please input your address!',
}],
})(
<Input placeholder="请输入地址" />
)}
</FormItem>
</Form>
</Modal>
</div>
)
}
}

export default UserModal;
// export default (Form.create({})(UserModal));

一般应用场景,详情不需要底部按钮,新增和修改需要。
image

api

解决:

通过父组件传递一个空的字符串或者 {footer: null} 给 Modal 组件进行属性解构。

image

父组件需要传入的值

image

子组件 Modal

18 有 connect 和 Form 表单

1
2
3
4
5
6
7
function mapStateToProps({ onlineCamera }) {
return {
favorites: onlineCamera.favorites,
};
}

export default connect(mapStateToProps)(Form.create()(TreeModal));

19 Tree 树组件增加右键菜单

参考:
https://github.com/ant-design/ant-design/issues/5151

关键代码:

// 实现这个方法 treeNodeonRightClick

1
2
3
4
5
6
7
8
9
10
treeNodeonRightClick(e) {
this.setState({
rightClickNodeTreeItem: {
pageX: e.event.pageX,
pageY: e.event.pageY,
id: e.node.props['data-key'],
categoryName: e.node.props['data-title']
}
});
}

// id 和 categoryName 是生成时绑上去的

1
2
3
4
5
6
7
<TreeNode
key={item.id}
title={title}
data-key={item.id}
data-title={item.categoryName}
/>);

// 最后绑个菜单就可以实现了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
getNodeTreeRightClickMenu() {
const {pageX, pageY} = {...this.state.rightClickNodeTreeItem};
const tmpStyle = {
position: 'absolute',
left: `${pageX - 220}px`,
top: `${pageY - 70}px`
};
const menu = (
<Menu
onClick={this.handleMenuClick}
style={tmpStyle}
className={style.categs_tree_rightmenu}
>
<Menu.Item key='1'><Icon type='plus-circle'/>{'加同级'}</Menu.Item>
<Menu.Item key='2'><Icon type='plus-circle-o'/>{'加下级'}</Menu.Item>
<Menu.Item key='4'><Icon type='edit'/>{'修改'}</Menu.Item>
<Menu.Item key='3'><Icon type='minus-circle-o'/>{'删除目录'}</Menu.Item>
</Menu>
);
return (this.state.rightClickNodeTreeItem == null) ? '' : menu;
}

getNodeTreeRightClickMenu 方法放在 render 中:

getNodeTreeRightClickMenu 方法是放在生成主界面的方法里( render ),因为每一次 state 的变化后,render 方法都会执行,所以变一下任意的 this.state 里面的状态,就会执行 render 方法 ,这样 getNodeTreeRightClickMenu 方法放在 render 方法里来生成界面的一部分。就可以了

项目中实现关键代码:

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
/*
* @Author: lin.zehong
* @Date: 2018-12-02 22:13:59
* @Last Modified by: lin.zehong
* @Last Modified time: 2018-12-19 16:36:27
* @Desc: 收藏夹--树
*/
import React from 'react';
import { connect } from 'dva';
import { Tree, Menu } from 'antd';
import Zcon from 'zteui-icon';
import styles from './TreeCollect.less';

const { TreeNode } = Tree;

class TreeCollect extends React.Component {
state = {
expandedKeys: ['-1'],
}

// 树节点右键事件
treeNodeonRightClick = ({ event, node }) => {
event.persist();
const { offsetLeft, _isCollapsed } = this.props;
const menuWidth = _isCollapsed ? 80 : 200;
const { favorites, favoritesDetail } = node.props;
this.changefavorites(favorites);
const hasChild = !!(favorites && favorites.scjId); // 收藏夹
this.setState({
rightClickNodeTreeItem: {
pageX: event.pageX - offsetLeft - 16 - menuWidth,
pageY: event.target.offsetTop + 28,
key: node.props.eventKey,
id: node.props.eventKey,
title: node.props.title,
favorites,
favoritesDetail,
hasChild,
},
});
}

// 右键节点页面展示
getNodeTreeRightClickMenu = () => {
const { rightClickNodeTreeItem } = this.state;
const { pageX, pageY, hasChild, key } = { ...rightClickNodeTreeItem };
const tmpStyle = {
position: 'absolute',
left: `${pageX}px`,
top: `${pageY}px`,
boxShadow: '2px 2px 10px #333333',
};
const menuHasNode = (
<Menu
onClick={this.handleMenuClick}
style={tmpStyle}
className={styles.categs_tree_rightmenu}
>
<Menu.Item key='1'>自动巡查</Menu.Item>
<Menu.Item key='2'>重命名</Menu.Item>
<Menu.Item key='3'>添加同级目录</Menu.Item>
<Menu.Item key='4'>添加子目录</Menu.Item>
<Menu.Item key='5'>删除</Menu.Item>
</Menu>
);
const menuRoot = (
<Menu
onClick={this.handleMenuClick}
style={tmpStyle}
className={styles.categs_tree_rightmenu}
>
<Menu.Item key='1'>自动巡查</Menu.Item>
<Menu.Item key='2'>重命名</Menu.Item>
<Menu.Item key='4'>添加子目录</Menu.Item>
</Menu>
);
const menuNoNode = (
<Menu
onClick={this.handleMenuClick}
style={tmpStyle}
className={styles.categs_tree_rightmenu}
>
<Menu.Item key='6'>取消收藏</Menu.Item>
</Menu>
);

const menu = hasChild ? (key === "-1" ? menuRoot : menuHasNode) : menuNoNode;

return (rightClickNodeTreeItem == null) ? '' : menu;
}

// 隐藏右键菜单
hideTreeRight = () => {
this.setState({ rightClickNodeTreeItem: null });
}


render() {
const { expandedKeys, selectedKeys } = this.state;
const { isExpand, gData } = this.props;
const loop = data => data.map((item) => {
if (item.children && item.favorites) {
return <TreeNode key={item.key} icon={<Zcon type="thing" />} title={item.title} favorites={item.favorites}>{loop(item.children)}</TreeNode>;
}
return <TreeNode key={item.favoritesDetail.sxtxxId} title={item.title} favoritesDetail={item.favoritesDetail} />;
});
return (
<div className={`${styles.root} ${isExpand ? '' : styles.hideTree}`} onClick={() => this.hideTreeRight()}>
<Tree
showIcon
className="draggable-tree"
defaultExpandedKeys={expandedKeys}
selectedKeys={selectedKeys}
onRightClick={this.treeNodeonRightClick}
onSelect={this.onSelect}
>
{loop(gData)}
</Tree>
{this.getNodeTreeRightClickMenu()}
</div>
);
}
}

function mapStateToProps({ onlineCamera, publicModel }) {
return {
gData: onlineCamera.collectTree,
cameraNum: onlineCamera.cameraNum,
inspectionCamera: onlineCamera.inspectionCamera,
_isCollapsed: publicModel._isCollapsed,
};
}

export default connect(mapStateToProps)(TreeCollect);

20 直接使用 rc-form 库 createForm,与 antd Form 的 Form.create() 设置样式不同

  • 使用 antd Form 的 Form.create()
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
import React from 'react'
import PropTypes from 'prop-types'
import { Form, Button } from 'antd'

class BalloonContent extends React.Component {
render() {
const { form } = this.props;
return (
<div>
<Form
size='medium'
className={Styles.wrapForm}
>
<Form.Item
label="算子输出"
>
{form.getFieldDecorator('stdioOutput', {
rules: [
{
required: true,
message: '输出不能为空',
},
],
})(<Input />)}
</Form.Item>
</Form>
</div>
)
}
}

export default Form.create()(BalloonContent) // !!!

image

结果样式

  • 直接使用 rc-form 库 createForm
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
import React from 'react'
import PropTypes from 'prop-types'
import { Form, Button } from 'antd'
import { createForm } from 'rc-form'

class BalloonContent extends React.Component {
render() {
const { form } = this.props;
const { getFieldDecorator, getFieldError } = form ;
const stdioOutputError = getFieldError('stdioOutput'); // !!!

return (
<div>
<Form
size='medium'
className={Styles.wrapForm}
>
<Form.Item
label="算子输出"
required // !!!
validateState={stdioOutputError ? 'error' : 'success'} // !!!
help={stdioOutputError} // !!!
>
{form.getFieldDecorator('stdioOutput', {
rules: [
{
required: true,
message: '输出不能为空',
},
],
})(<Input />)}
</Form.Item>
</Form>
</div>
)
}
}

export default createForm ()(BalloonContent) // !!!

image

结果

21 Form 表单实时校验 _.debounce 应用,和不同字段相互校验影响

例如:表单字段,密码和确认密码,改变 Password,如果与 Confirm Password 不一致,也会在 Confirm Password 做提示:

image

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
官网示例:注册新用户,主要代码

compareToFirstPassword = (rule, value, callback) => {
const { form } = this.props;
if (value && value !== form.getFieldValue('password')) {
callback('Two passwords that you enter is inconsistent!');
} else {
callback();
}
};

validateToNextPassword = (rule, value, callback) => {
const { form } = this.props;
if (value && this.state.confirmDirty) {
form.validateFields(['confirm'], { force: true });
}
callback();
};


<Form.Item label="Password" hasFeedback>
{getFieldDecorator('password', {
rules: [
{
required: true,
message: 'Please input your password!',
},
{
validator: this.validateToNextPassword,
},
],
})(<Input.Password />)}
</Form.Item>

<Form.Item label="Confirm Password" hasFeedback>
{getFieldDecorator('confirm', {
rules: [
{
required: true,
message: 'Please confirm your password!',
},
{
validator: this.compareToFirstPassword,
},
],
})(<Input.Password onBlur={this.handleConfirmBlur} />)}
</Form.Item>

实际项目例子,选择所属数据库,校验表名:

image

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
主要代码:

import _ from "lodash";

// 写入新表,选择数据库,需要校验已有的表名
validateToTableName = (rule, value, callback) => {
const { form: { getFieldValue, validateFields }} = this.props;
const targetTableCode = getFieldValue("targetTableCode");
if (targetTableCode) {
validateFields(['targetTableCode'], { force: true });
}
callback();
};

// 写入新表,校验表名
// eslint-disable-next-line
validateTableExist = _.debounce((rule, value, callback) => {
const { form: { getFieldValue }, dispatch } = this.props;
const targetDataSource = getFieldValue("targetDataSource");
const targetTableCode = getFieldValue("targetTableCode");
dispatch({
type: "applyDetail/tableExist",
payload: {
dataSourceCode: targetDataSource,
table: targetTableCode,
},
}).then(result => {
if (result) {
callback("该表名已存在");
} else {
callback();
}
})
}, 500);




<Form.Item label="所属数据库">
{getFieldDecorator("targetDataSource", {
rules: [
{
required: true,
message: "请选择所属数据库",
},
{
validator: this.validateToTableName, // !!!
},
],
initialValue:
exchangeFormat.targetDataSource ||
(dataSourceList.length > 0
? dataSourceList[0].code
: undefined),
})(dataBaseComponent({ className: styles.formInput }))}
</Form.Item>

<Form.Item label="表名">
{getFieldDecorator("targetTableCode", {
rules: [
{
required: true,
message: "请输入新表表名",
},
{
pattern: checkBackEndTableName,
message: "只支持英文字母、数字、英文格式、下划线",
},
{
validator: this.validateTableExist, // !!!
},
],
initialValue:
(exchangeFormat.formatType === WRITE_IN_NEW_TABLE
? exchangeFormat.targetTableCode
: undefined) || undefined,
})(
<Input
className={styles.formInput}
disabled={disabled}
placeholder="请输入"
/>
)}
</Form.Item>

22 Form 组件方法 getFieldsValue 获取自定义组件的值

项目实例:对 antd RangePicker 抽取完独立组件后,form 表单获取不到值

自定义组件被 getFieldsValue 包裹,会获得以下属性:

onChange 方法, 子组件调用此方法,可将值传给父组件,从而 Form 可拿到自定义组件的值 value 属性,获得初始值

1
2
3
4
5
6
7
<Form.Item label="发送时间">
{getFieldDecorator('range-time-picker', {
rules: [{ required: false, message: '请输入开始时间-结束时间' }],
})(
<RangePickerPage />
)}
</Form.Item>

下面是对 antd RangePicker 进行封装,通过组件 RangePicker 本身的 onChange 方法,调用 this.props.onChange(子组件不用传 onChange 方法,自定义组件被 getFieldsValue 包裹,会自动获取 onChage 属性),则通过 form.validateFields 可以获取到值。

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
63
64
65
66
/*
* Author: lin.zehong
* Date: 2019-10-04 09:14:52
* Last Modified by: lin.zehong
* Last Modified time: 2019-10-04 09:14:52
* Desc: 对 antd RangePicker 进行封装
*/
import React from "react";
import moment from "moment";
import { DatePicker } from "antd";

const { RangePicker } = DatePicker;

class RangePickerPage extends React.Component {

range = (start, end) => {
const result = [];
for (let i = start; i < end; i += 1) {
result.push(i);
}
return result;
}

disabledDate = (current) => {
// Can not select days before today and today
return current && current < moment().endOf('day');
}

disabledRangeTime = (_, type) => {
if (type === 'start') {
return {
disabledHours: () => this.range(0, 60).splice(4, 20),
disabledMinutes: () => this.range(30, 60),
disabledSeconds: () => [55, 56],
};
}
return {
disabledHours: () => this.range(0, 60).splice(20, 4),
disabledMinutes: () => this.range(0, 31),
disabledSeconds: () => [55, 56],
};
}

onChange = (dates, dateStrings) => {
const { onChange } = this.props; // !!!
onChange(dateStrings);
}

render() {
return (
<RangePicker
allowClear
disabledDate={this.disabledDate}
disabledTime={this.disabledRangeTime}
showTime={{
hideDisabledOptions: true,
defaultValue: [moment('00:00:00', 'HH:mm:ss'), moment('11:59:59', 'HH:mm:ss')],
}}
format="YYYY-MM-DD HH:mm:ss"
onChange={this.onChange} // !!!
/>
);
}
}

export default RangePickerPage;

参考:https://juejin.im/post/5c9c6c08e51d4503e514eaac

23 Form 表单清空和重置的区别以及方法

这里首先需要明确,清空和重置是不同的概念,清空是把内容都清空掉,而重置是恢复 form 表单初始值。

例如:新增功能,清空和重置就是一样的效果,而对于编辑,清空就是把初始值都清空掉,重置就是恢复刚开始的初始值。

  • 清空
1
form.setFieldsValue({"fieldName": ""});
  • 重置
1
form.resetFields();

24 DatePicker 组件,部分日期/时间为可选

24.1 不能选择今天之前的日期,包括今天的日期也不可以选择

1
2
3
const disabledDate = (current) => {
return current && current < moment().endOf('day');
}

24.2 不能选择今天之前的日期,今天日期可以选择

1
2
3
const disabledDate = (current) => {
return current && current < moment().subtract(1, 'day');
}

24.3 当前时间之后的时间点,精确到小时

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
const [upgradeTime, setUpgradeTime] = useState(moment('00:00:00', 'HH:mm:ss'))


const disabledDate = (current) => {
return current && current < moment().subtract(1, 'day'); // 今天可以选择
}


const disabledDateTime = () => {
const hours = moment().hours(); // 0~23
// 当日只能选择当前时间之后的时间点
if (upgradeTime.date() === moment().date()) {
return {
disabledHours: () => range(0, hours + 1),
};
}
}


<Form.Item label="发送时间">
{getFieldDecorator('pushTime', {
rules: [{ required: false, message: '请输入发送时间' }],
initialValue: record.pushType === 0 ? null :
(record.pushTime ? moment(record.pushTime, 'YYYY-MM-DD HH:mm:ss') : null), // 定时发送才显示时间
})(
<DatePicker
format="YYYY-MM-DD HH:mm:ss"
disabledDate={disabledDate}
disabledTime={disabledDateTime}
style={{ width: "100%" }}

onChange={(timer) => setUpgradeTime(timer)} // !!!
showTime={{ defaultValue: moment(upgradeTime) }} // !!!
/>,
)}
</Form.Item>

24.4 当前时间之后的时间点,精确到分

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
const [upgradeTime, setUpgradeTime] = useState(moment('00:00:00', 'HH:mm:ss'))


const disabledDate = (current) => {
return current && current < moment().subtract(1, 'day'); // 今天可以选择
}


const disabledDateTime = () => {
const hours = moment().hours(); // 0~23
const minutes = moment().minutes(); // 0~59
// 当日只能选择当前时间之后的时间点
if (upgradeTime.date() === moment().date()) {
return {
disabledHours: () => range(0, hours),
disabledMinutes: () => range(0, minutes), // 精确到分
};
}
}


<Form.Item label="发送时间">
{getFieldDecorator('pushTime', {
rules: [{ required: false, message: '请输入发送时间' }],
initialValue: record.pushType === 0 ? null :
(record.pushTime ? moment(record.pushTime, 'YYYY-MM-DD HH:mm:ss') : null), // 定时发送才显示时间
})(
<DatePicker
format="YYYY-MM-DD HH:mm:ss"
disabledDate={disabledDate}
disabledTime={disabledDateTime}
style={{ width: "100%" }}

onChange={(timer) => setUpgradeTime(timer)} // !!!
showTime={{ defaultValue: moment(upgradeTime) }} // !!!
/>,
)}
</Form.Item>