前言
最近把新的后台系统写好了..用的是上篇文章的技术栈(mobx+react16
);
但是感觉mobx
没有想象中的好用,看到umi 2.x
了,就着手又开始重构了。
仔细梳理了下上个系统,发现可以抽离的东西不少
此篇文章是我针对我们的搜索条件抽离的一个组件,仅供参考。
调整记录
- 2018-11-15 :
- new :
reset
表单props
回调,调用则取默认不带参数的列表 - new : 待渲染的子组件布局规格的传入,
responsive
这个字段(放在待渲染的json
)
- 2018-11-16 :
- fixed:
Input
控件输入一个字符自动失焦点的问题(Math.random
的锅) - new :
InputNumber
组件引入,搜索条件也有可能是搜索ID
的..纯数字!! - new : 引入
lodash
的isEqual
进行对象深度比对,降低state
的合并次数,减少re-render
- 2018-11-19 :
- new : 表单提交前,
value
为空数组不返回,字符串value
清除两边的空格
- 2018-11-20:
- new :
props.children
传入改造,添加style
- 2018-11-30:
- new : 添加一个开启自动触发提交的props(除了
input
输入,其他选择性的控制项会直接触发)
- 2019-1-9:
- new : 若是组件没有添加
getFieldDecorator
的rules
条件,则把下margin
去掉
效果图
响应式传入
折叠展开搜索条件,默认六个隐藏展开按钮,大于则显示(点击直接取数据源的长度)
传递子组件作为搜索按钮区域
统一变动控件的规格
重置表单
子组件引入自身响应式条件(会话状态,按钮太多,等分会造成各种换行,不舒服)
非Input
的控件,自动触发表单提交, props
的autoSearch
为true
仅有一个非Input
控件的时候,去除卡片效果
抽离思路及实现
思路
- 合并
props
传递的值,尽可能的减少传递的东西(在组件内部实现默认值合并),把渲染的子组件通过遍历json
去实现; - 整个查询区域用的
antd
表单组件,聚合所有表单数据(自动双向绑定,设置默认值等); - 为了降低复杂度,子组件不考虑
dva
来维护状态,纯靠props
和state
构建,然后统一把构建的表单数据向父级暴露.. - 内部的state默认初始化都为空[
antd
对于日期控件使用null
来置空],外部初始化可以用getFieldDecorator
的initialValue
,已经暴露
实现的功能
使用姿势
<AdvancedSearchForm data={searchItem} getSearchFormData={this.searchList} resetSearchForm={this.resetSearchList} accumulate="3"> <Button type="dashed" icon="download" style={{ marginLeft: 8 }} htmlType="submit"> 下载报表 </Button> </AdvancedSearchForm>
支持的props
根据ctype
渲染的控件有Input,Button,Select,DatePicker,Cascader,Radio
允许传递的props有四个个,部分props有默认值,传递的会合并进去
字段 | 类型 | 解释 |
data |
数组对象[obj] | 数据源(构建) |
accumulate |
字符串 | 超过多少个折叠起来 |
responseLayout |
对象 | 传递对象,响应式 |
csize |
字符串 | 控件大小设置,small(小) , default(默认) |
getSearchFormData |
函数 | 回调函数,拿到表单的数据 |
resetSearchForm |
函数 | 回调函数,当重置表单数据的时候 |
autoSearch |
布尔值 | 启动非input 的控件自动触发提交的props函数 |
数据源格式
data
的数据格式基本和antd
要求的格式一致,除了个别用来判断或者渲染子组件的,
字段解释:
ctype(controller-type:控件类型) ==> string
attr(控件支持的属性) ==> object
field(受控表单控件的配置项) ==> object
responsive(子组件自身布局) ==> object
searchItem: [ { ctype: 'dayPicker', attr: { placeholder: '查询某天', }, field: { label: '日活', value: 'activeData', }, }, { ctype: 'monthPicker', attr: { placeholder: '查询月份数据', }, field: { label: '月活', value: 'activeData', }, }, { ctype: 'radio', field: { label: '设备类型', value: 'platformId', params: { initialValue: '', }, }, selectOptionsChildren: [ { label: '全部', value: '', }, { label: '未知设备', value: '0', }, { label: 'Android', value: '1', }, { label: 'IOS', value: '2', }, ], }, { ctype: 'radio', responsive: { md:24, xl:12, xxl:8 }, field: { label: '会话状态', value: 'chatStatus', params: { initialValue: '', }, }, selectOptionsChildren: [ { label: '全部', value: '', }, { label: '正常', value: '1', }, { label: '用户删除', value: '2', }, { label: '系统删除', value: '3', }, { label: '会话过期', value: '4', }, ], }, { ctype: 'cascader', field: { label: '排序', value: 'sorter', }, selectOptionsChildren: [ { label: '根据登录时间', value: 'loginAt', children: [ { label: '升序', value: 'asc', }, { label: '降序', value: 'desc', }, ], }, { label: '根据注册时间', value: 'createdAt', children: [ { label: '升序', value: 'asc', }, { label: '降序', value: 'desc', }, ], }, ], }, ],
实现代码
AdvancedSearchForm
index.js
/* * @Author: CRPER * @LastEditors: CRPER * @Github: https://github.com/crper * @Motto: 折腾是一种乐趣,求知是一种追求。不懂就学,懂则分享。 * @Description: 列表表单查询组件 */ import React, { PureComponent } from 'react'; import { Form, Row, Col, Input, Button, Select, DatePicker, Card, Cascader, Radio, Icon, Divider, InputNumber, } from 'antd'; // lodash 深比较 import isEqual from 'lodash/isEqual'; // antd const { MonthPicker, RangePicker } = DatePicker; const Option = Select.Option; const FormItem = Form.Item; const RadioButton = Radio.Button; const RadioGroup = Radio.Group; @Form.create({ onValuesChange: (props, changedValues, allValues) => { const { data, autoSearch } = props; // 传入的空间必须存在, 否则不可能触发自动提交表单的props if (data && Array.isArray(data) && data.length > 0 && autoSearch) { let autoSearchField = []; data.map(item => { const { ctype, field: { value: fieldName }, } = item; if (ctype !== 'input' && ctype !== 'inputNum') { autoSearchField.push(fieldName); } }); let keys = Object.keys(changedValues); if (autoSearchField.indexOf(keys[0]) !== -1) { if (changedValues[keys[0]]) { props.getSearchFormData(changedValues); } else { props.resetSearchForm(); } } } }, }) class AdvancedSearchForm extends PureComponent { state = { expand: false, factoryData: [ { ctype: 'input', attr: { placeholder: '请输入查询内容...', }, field: { label: '', value: '', }, }, { ctype: 'inputNum', attr: { placeholder: '请输入ID查询...', min: 0, }, field: { label: '', value: '', }, }, { ctype: 'select', attr: { placeholder: '请选择查询项', allowClear: true, }, selectOptionsChildren: [], field: { label: '', value: '', params: { initialValue: '', }, }, }, { ctype: 'cascader', attr: { placeholder: '请选择查询项', allowClear: true, }, selectOptionsChildren: [], field: { label: '', value: [], params: { initialValue: [], }, }, }, { ctype: 'dayPicker', attr: { placeholder: '请选择日期', allowClear: true, format: 'YYYY-MM-DD', }, field: { label: '', value: '', params: { initialValue: null, }, }, }, { ctype: 'monthPicker', attr: { placeholder: '请选择月份', allowClear: true, format: 'YYYY-MM', }, field: { label: '', value: '', params: { initialValue: null, }, }, }, { ctype: 'timerangePicker', attr: { placeholder: '请选择日期返回', allowClear: true, }, field: { label: '', value: '', params: { initialValue: [null, null], }, }, }, { ctype: 'radio', attr: {}, field: { label: '', value: '', params: { initialValue: '', }, }, }, ], }; // 获取props并且合并 static getDerivedStateFromProps(nextProps, prevState) { // 若是props和缓存state一致,则不更新state if (isEqual(prevState.prevData, nextProps.data)) { return null; } /** * data: 构建的数据 * single: 单一选择,会禁用其他输入框 */ const { factoryData } = prevState; const { data, csize } = nextProps; let newData = []; if (data && Array.isArray(data) && data.length > 0) { // 合并传入的props data.map(item => { // 若是有外部传入全局控制表单控件大小的则应用 if (csize && typeof csize === 'string') { item.attr = { ...item.attr, size: csize, }; } const { ctype, attr, field, ...rest } = item; let combindData = {}; factoryData.map(innerItem => { if (item.ctype === innerItem.ctype) { const { ctype: innerCtype, attr: innerAttr, field: innerField, ...innerRest } = innerItem; combindData = { ctype: item.ctype, attr: { ...innerAttr, ...attr, }, field: { ...innerField, ...field, }, ...innerRest, ...rest, }; } }); newData.push(combindData); }); // 返回合并后的数据,比如mode,渲染的数据这些 return { data: newData, prevData: nextProps.data }; } return null; } // 清除表单数据中字符串的两边的空格 // 若是key为空数组则跳过 removeNotNeedValue = obj => { // 判断必须为obj if (!(Object.prototype.toString.call(obj) === '[object Object]')) { return {}; } let tempObj = {}; for (let [key, value] of Object.entries(obj)) { let tmpValue = value; if (Array.isArray(value) && value.length <= 0) { continue; } if (tmpValue && !(Object.prototype.toString.call(tmpValue) === '[object Function]')) { if (typeof value === 'string') { value = value.trim(); } } tempObj[key] = value; } return tempObj; }; // 提交表单 handleSearch = e => { e.preventDefault(); this.props.form.validateFields((err, values) => { // 表单表单不报错,且props有传递的情况下,才返回表单数据 if (!err && this.props.getSearchFormData) { // 字符串类型全部去除两边的空格 let form_data = this.removeNotNeedValue(values); this.props.getSearchFormData(form_data); } }); }; // 重置表单 handleReset = () => { this.props.form.resetFields(); // 若是有回调函数,则返回空对象 if (this.props.resetSearchForm) { this.props.resetSearchForm(null); } }; // 生成 Form.Item getFields = () => { const { data } = this.state; const children = []; if (data) { for (let i = 0; i < data.length; i++) { // 若是控件的名字丢.亦或filed的字段名或之值丢失则不渲染该组件 // 若是为select或cascader没有子组件数据也跳过 const { ctype, field: { value, label }, selectOptionsChildren, } = data[i]; if ( !ctype || !value || !label || ((ctype === 'select' || ctype === 'cascader') && selectOptionsChildren && selectOptionsChildren.length < 1) ) continue; // 渲染组件 let formItem = this.renderItem({ ...data[i], itemIndex: i, }); // 缓存组件数据 children.push(formItem); } return children; } else { return []; } }; // 合并响应式props combindResponseLayout = (responsive = {}) => { // 从父组件接受的布局姿势 const { responseLayout } = this.props; // responsive 是子组件自身的响应式布局 // 响应式 return { xs: 24, sm: 24, md: 12, lg: 8, xxl: 6, ...responseLayout, ...responsive, }; }; // 计算外部传入需要显示隐藏的个数 countHidden = () => { const { data, accumulate } = this.props; return this.state.expand ? data.length : accumulate ? accumulate : 8; }; // 判断需要渲染的组件 renderItem = data => { const { getFieldDecorator } = this.props.form; const { ctype, field, attr, itemIndex, responsive } = data; // responsive 是子组件自身的响应式布局 const ResponseLayout = this.combindResponseLayout(responsive); const count = this.countHidden(); const isRules = field.params && field.params.rules && Array.isArray(field.params.rules) && field.params.rules.length > 0; switch (ctype) { case 'input': return ( <Col {...ResponseLayout} style={{ display: itemIndex < count ? 'block' : 'none' }} key={itemIndex} > <FormItem label={field.label} style={isRules ? null : { marginBottom: 0 }}> {getFieldDecorator(field.value, field.params ? field.params : {})( <Input {...attr} /> )} </FormItem> </Col> ); case 'inputNum': return ( <Col {...ResponseLayout} style={{ display: itemIndex < count ? 'block' : 'none' }} key={itemIndex} > <FormItem label={field.label} style={isRules ? null : { marginBottom: 0 }}> {getFieldDecorator(field.value, field.params ? field.params : {})( <InputNumber {...attr} style={{ width: '100%' }} /> )} </FormItem> </Col> ); case 'select': return ( <Col {...ResponseLayout} style={{ display: itemIndex < count ? 'block' : 'none' }} key={itemIndex} > <FormItem label={field.label} style={isRules ? null : { marginBottom: 0 }}> {getFieldDecorator(field.value, field.params ? field.params : {})( <Select {...attr}> {data.selectOptionsChildren && data.selectOptionsChildren.length > 0 && data.selectOptionsChildren.map((optionItem, index) => ( <Option value={optionItem.value} key={index}> {optionItem.label} </Option> ))} </Select> )} </FormItem> </Col> ); case 'cascader': return ( <Col {...ResponseLayout} style={{ display: itemIndex < count ? 'block' : 'none' }} key={itemIndex} > <FormItem label={field.label} style={isRules ? null : { marginBottom: 0 }}> {getFieldDecorator(field.value, field.params ? field.params : {})( <Cascader {...attr} options={data.selectOptionsChildren} /> )} </FormItem> </Col> ); case 'dayPicker': return ( <Col {...ResponseLayout} style={{ display: itemIndex < count ? 'block' : 'none' }} key={itemIndex} > <FormItem label={field.label} style={isRules ? null : { marginBottom: 0 }}> {getFieldDecorator(field.value, field.params ? field.params : {})( <DatePicker {...attr} /> )} </FormItem> </Col> ); case 'monthPicker': return ( <Col {...ResponseLayout} style={{ display: itemIndex < count ? 'block' : 'none' }} key={itemIndex} > <FormItem label={field.label} style={isRules ? null : { marginBottom: 0 }}> {getFieldDecorator(field.value, field.params ? field.params : {})( <MonthPicker {...attr} /> )} </FormItem> </Col> ); case 'timerangePicker': attr.placeholder = Array.isArray(attr.placeholder) ? attr.placeholder : ['开始日期', '结束日期']; return ( <Col {...ResponseLayout} style={{ display: itemIndex < count ? 'block' : 'none' }} key={itemIndex} > <FormItem label={field.label} style={isRules ? null : { marginBottom: 0 }}> {getFieldDecorator(field.value, field.params ? field.params : {})( <RangePicker {...attr} /> )} </FormItem> </Col> ); case 'datePicker': return ( <Col {...ResponseLayout} style={{ display: itemIndex < count ? 'block' : 'none' }} key={itemIndex} > <FormItem label={field.label} style={isRules ? null : { marginBottom: 0 }}> {getFieldDecorator(field.value, field.params ? field.params : {})( <DatePicker {...attr} /> )} </FormItem> </Col> ); case 'radio': return ( <Col {...ResponseLayout} style={{ display: itemIndex < count ? 'block' : 'none' }} key={itemIndex} > <FormItem label={field.label} style={isRules ? null : { marginBottom: 0 }}> {getFieldDecorator(field.value, field.params ? field.params : {})( <RadioGroup {...attr}> {data.selectOptionsChildren && data.selectOptionsChildren.length > 0 && data.selectOptionsChildren.map((optionItem, index) => ( <RadioButton value={optionItem.value} key={index}> {optionItem.label} </RadioButton> ))} </RadioGroup> )} </FormItem> </Col> ); default: return null; } }; // 折叠搜索框条件 toggle = () => { const { expand } = this.state; this.setState({ expand: !expand }); }; render() { const { expand } = this.state; const { data, accumulate, children } = this.props; const isRnderToggleIcon = accumulate ? (data && data.length) > accumulate ? true : false : data.length > 8; // 克隆子组件并且添加自己要添加的特性 const PropsBtn = React.Children.map(this.props.children, child => React.cloneElement(child, { style: { marginLeft: 8, }, }) ); // 若是搜索条件仅有一个情况 const hideSearchBtn = data.length === 1 && data[0].ctype !== 'input' && data[0].ctype !== 'inputNum'; const { loading = false } = this.props; return ( <Form className="ant-advanced-search-form" onSubmit={this.handleSearch}> {hideSearchBtn ? ( <div>{this.getFields()}</div> ) : ( <Card size="small" title="查询条件" extra={ <> {children ? ( <> {children} <Divider type="vertical" /> </> ) : null} <Button type="primary" htmlType="submit" loading={loading}> 搜索结果 </Button> <Button style={{ marginLeft: 8 }} onClick={this.handleReset}> 清空条件 </Button> </> } style={{ width: '100%' }} > <Row gutter={24} type="flex" justify="start"> {this.getFields()} </Row> {isRnderToggleIcon ? ( <Row gutter={24} type="flex" justify="center"> <a onClick={this.toggle}> {expand ? '收起' : '展开'} <Icon type={expand ? 'up' : 'down'} /> </a> </Row> ) : null} </Card> )} </Form> ); } } export default AdvancedSearchForm;
index.css
// 列表搜索区域 .ant-advanced-search-form { border-radius: 6px; } .ant-advanced-search-form .ant-form-item { display: flex; flex-wrap: wrap; } .ant-advanced-search-form .ant-form-item-control-wrapper { flex: 1; }