使用 getFieldDecorator 包裹的输入框或者 Select,必须是在最外层,也就是只有一层,否则,检验会一直不通过,所以,需要重新布局应该在 getFieldDecorator 的外层添加父节点,而不应该在里面。
例:
{getFieldDecorator('apiwgAppName', {
rules: [{ required: false, message: '请选择' }],
initialValue: apiwgAppName || ""
})(
)}
+新增应用
{form.getFieldDecorator('menuName', {
rules: [
{ required: true, message: '菜单名称不能为空' },
{ type: 'string', max: 30, message: '菜单名称过长' },
{ validator: this.handleCheckName },
{ whitespace: true, message: '请输入非空白内容' }
],
initialValue: this.props.menuSysData.menuName,
})(
)}
// 实时校验
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();
});
}
{form.getFieldDecorator('userCode', {
initialValue: '',
rules: [
{ required: !disableFlag, validator: this.usercodeValidator },
{ type: 'string', max: 30, message: '账号过长' },
{ whitespace: true, message: '内容不能为空' }
],
})(
)}
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();
});
}
}
this.props.form.validateFields((err, fieldsValue) => {
if (err) return;
this.handleSubmit(fieldsValue);
});
说明:因为之前遇到过使用 htmlType 提交表单会有问题,但是改为 onClick 后,就没问题了,所以,也记录一下。
htmlType 是官网使用的方式,具体问题本人当时忘记截个图了。
// 改变后:
value}
parser={value => parseInt(value) || ''}
style={{ width: '100%' }}
step={1}
onChange={(val) => this.onChangeIpt(1, val)}
/>
说明:menu 必须放在 Sider 中,才能实现缩回去的,这个有特定的布局。
说明:通过在 Sider 组件,设置 width,调整菜单的宽度,通过设置 collapsedWidth,调整调整缩进的宽度。
说明:一般而言,表格 Columns 字段 id 是在界面不展示的,但是,对于有些逻辑的处理,又是需要的,可以使用相应样式隐藏的处理方式。
常规展示的情况:
{
title: '序号',
dataIndex: 'algoId',
key: 'algoId'
},
不展示id字段:
{
title: '',
dataIndex: 'algoId',
key: 'algoId',
width: 0,
render: item => {
return (
{item}
);
}
},
查看元素可知,Modal 是在界面构建完成之后,由 js 控制,动态的添加,所以想事先获取 ant-modal-body 中 DOM 元素的节点是不可能的,但是一般情况也不会去获取它。
自定义 Modal,解决上述的问题。
关键代码:
说明:
1:因为我们使用的是 antd,所以,下面的样式是不需要引入的。这个跟 antd 的 Modal 样式重复。
2:Modal 的隐藏和显示,是通过控制 class 为 ant-modal-mask 和 ant-modal-wrap 两个 div 的显示和隐藏。
界面布局:
...
...
sentinel
样式:
.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;
}
通过给 Select 组件新增 allowClear 属性。注意:allowClear 也会触发 onChange 方法,所以,也要单独处理一下,因为 value 和 element 为undefined。
顺序
一般都是使用 rowkey 方法一解决(后台数据要保证没有重复);
使用irowKey
dataSource数据新增key
@Form.create({})
export default (Form.create({})(APP));
其实,只要编辑成功后,回调调用 form.resetFields(),就可以了,如果
是使用 modal 框弹出的表单,就可以直接使用 destroyOnClose = {true} 属性。
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 (
handleOk()}
onCancel={() => handleCancel()}
destroyOnClose={true}
>
)
}
export default (Form.create({})(UserModal));
主要代码:destroyOnClose={true}
handleOk()}
onCancel={() => handleCancel()}
destroyOnClose={true}
>
...
如果是 class 类,可以使用钩子。
componentDidUpdate = (prevProps, prevState) => {
if (!prevProps.visible) {
prevProps.form.resetFields();
}
};
代码参考:
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 (
this.handleOk()}
onCancel={() => this.handleCancel()}
>
)
}
}
export default UserModal;
// export default (Form.create({})(UserModal));
一般应用场景,详情不需要底部按钮,新增和修改需要。
api
解决:
通过父组件传递一个空的字符串或者 {footer: null} 给 Modal 组件进行属性解构。
父组件需要传入的值
子组件 Modal
function mapStateToProps({ onlineCamera }) {
return {
favorites: onlineCamera.favorites,
};
}
export default connect(mapStateToProps)(Form.create()(TreeModal));
参考:
https://github.com/ant-design/ant-design/issues/5151
关键代码:
// 实现这个方法 treeNodeonRightClick
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 是生成时绑上去的
);
// 最后绑个菜单就可以实现了
getNodeTreeRightClickMenu() {
const {pageX, pageY} = {...this.state.rightClickNodeTreeItem};
const tmpStyle = {
position: 'absolute',
left: `${pageX - 220}px`,
top: `${pageY - 70}px`
};
const menu = (
);
return (this.state.rightClickNodeTreeItem == null) ? '' : menu;
}
getNodeTreeRightClickMenu 方法放在 render 中:
getNodeTreeRightClickMenu 方法是放在生成主界面的方法里( render ),因为每一次 state 的变化后,render 方法都会执行,所以变一下任意的 this.state 里面的状态,就会执行 render 方法 ,这样 getNodeTreeRightClickMenu 方法放在 render 方法里来生成界面的一部分。就可以了
项目中实现关键代码:
/*
* @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 = (
);
const menuRoot = (
);
const menuNoNode = (
);
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 } title={item.title} favorites={item.favorites}>{loop(item.children)};
}
return ;
});
return (
this.hideTreeRight()}>
{loop(gData)}
{this.getNodeTreeRightClickMenu()}
);
}
}
function mapStateToProps({ onlineCamera, publicModel }) {
return {
gData: onlineCamera.collectTree,
cameraNum: onlineCamera.cameraNum,
inspectionCamera: onlineCamera.inspectionCamera,
_isCollapsed: publicModel._isCollapsed,
};
}
export default connect(mapStateToProps)(TreeCollect);
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 (
{form.getFieldDecorator('stdioOutput', {
rules: [
{
required: true,
message: '输出不能为空',
},
],
})()}
)
}
}
export default Form.create()(BalloonContent) // !!!
结果样式
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 (
{form.getFieldDecorator('stdioOutput', {
rules: [
{
required: true,
message: '输出不能为空',
},
],
})()}
)
}
}
export default createForm ()(BalloonContent) // !!!
结果
例如:表单字段,密码和确认密码,改变 Password,如果与 Confirm Password 不一致,也会在 Confirm Password 做提示:
官网示例:注册新用户,主要代码
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();
};
{getFieldDecorator('password', {
rules: [
{
required: true,
message: 'Please input your password!',
},
{
validator: this.validateToNextPassword,
},
],
})( )}
{getFieldDecorator('confirm', {
rules: [
{
required: true,
message: 'Please confirm your password!',
},
{
validator: this.compareToFirstPassword,
},
],
})( )}
实际项目例子,选择所属数据库,校验表名:
主要代码:
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);
{getFieldDecorator("targetDataSource", {
rules: [
{
required: true,
message: "请选择所属数据库",
},
{
validator: this.validateToTableName, // !!!
},
],
initialValue:
exchangeFormat.targetDataSource ||
(dataSourceList.length > 0
? dataSourceList[0].code
: undefined),
})(dataBaseComponent({ className: styles.formInput }))}
{getFieldDecorator("targetTableCode", {
rules: [
{
required: true,
message: "请输入新表表名",
},
{
pattern: checkBackEndTableName,
message: "只支持英文字母、数字、英文格式、下划线",
},
{
validator: this.validateTableExist, // !!!
},
],
initialValue:
(exchangeFormat.formatType === WRITE_IN_NEW_TABLE
? exchangeFormat.targetTableCode
: undefined) || undefined,
})(
)}
项目实例:对 antd RangePicker 抽取完独立组件后,form 表单获取不到值
自定义组件被 getFieldsValue 包裹,会获得以下属性:
onChange方法, 子组件调用此方法,可将值传给父组件,从而Form可拿到自定义组件的值 value属性,获得初始值
{getFieldDecorator('range-time-picker', {
rules: [{ required: false, message: '请输入开始时间-结束时间' }],
})(
)}
下面是对 antd RangePicker 进行封装,通过组件 RangePicker 本身的 onChange 方法,调用 this.props.onChange(子组件不用传 onChange 方法,自定义组件被 getFieldsValue 包裹,会自动获取 onChage 属性),则通过 form.validateFields 可以获取到值。
/*
* 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 (
);
}
}
export default RangePickerPage;
参考:https://juejin.im/post/5c9c6c08e51d4503e514eaac
这里首先需要明确,清空和重置是不同的概念,清空是把内容都清空掉,而重置是恢复 form 表单初始值。
例如:新增功能,清空和重置就是一样的效果,而对于编辑,清空就是把初始值都清空掉,重置就是恢复刚开始的初始值。
form.setFieldsValue({"fieldName": ""});
form.resetFields();
const disabledDate = (current) => {
return current && current < moment().endOf('day');
}
const disabledDate = (current) => {
return current && current < moment().subtract(1, 'day');
}
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),
};
}
}
{getFieldDecorator('pushTime', {
rules: [{ required: false, message: '请输入发送时间' }],
initialValue: record.pushType === 0 ? null :
(record.pushTime ? moment(record.pushTime, 'YYYY-MM-DD HH:mm:ss') : null), // 定时发送才显示时间
})(
setUpgradeTime(timer)} // !!!
showTime={{ defaultValue: moment(upgradeTime) }} // !!!
/>,
)}
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), // 精确到分
};
}
}
{getFieldDecorator('pushTime', {
rules: [{ required: false, message: '请输入发送时间' }],
initialValue: record.pushType === 0 ? null :
(record.pushTime ? moment(record.pushTime, 'YYYY-MM-DD HH:mm:ss') : null), // 定时发送才显示时间
})(
setUpgradeTime(timer)} // !!!
showTime={{ defaultValue: moment(upgradeTime) }} // !!!
/>,
)}