React 16.x折腾记 - (6) 基于React 16.x+ Antd 3.x封装的一个声明式的查询组件(实用强大)

简介: 最近把新的后台系统写好了..用的是上篇文章的技术栈(mobx+react16);但是感觉mobx没有想象中的好用,看到umi 2.x了,就着手又开始重构了。仔细梳理了下上个系统,发现可以抽离的东西不少此篇文章是我针对我们的搜索条件抽离的一个组件,仅供参考。


前言


最近把新的后台系统写好了..用的是上篇文章的技术栈(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 : 引入lodashisEqual进行对象深度比对,降低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 : 若是组件没有添加getFieldDecoratorrules条件,则把下margin去掉


效果图


响应式传入





折叠展开搜索条件,默认六个隐藏展开按钮,大于则显示(点击直接取数据源的长度)



传递子组件作为搜索按钮区域



统一变动控件的规格



重置表单



子组件引入自身响应式条件(会话状态,按钮太多,等分会造成各种换行,不舒服)



Input的控件,自动触发表单提交, propsautoSearchtrue



仅有一个非Input控件的时候,去除卡片效果



抽离思路及实现


思路


  • 合并props传递的值,尽可能的减少传递的东西(在组件内部实现默认值合并),把渲染的子组件通过遍历json去实现;
  • 整个查询区域用的antd表单组件,聚合所有表单数据(自动双向绑定,设置默认值等);
  • 为了降低复杂度,子组件不考虑dva来维护状态,纯靠propsstate构建,然后统一把构建的表单数据向父级暴露..
  • 内部的state默认初始化都为空[antd对于日期控件使用null来置空],外部初始化可以用getFieldDecoratorinitialValue,已经暴露


实现的功能


使用姿势


<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;
}
目录
相关文章
|
1月前
|
前端开发 JavaScript 测试技术
从零开始搭建react+typescript+antd+redux+less+vw自适应项目
从零开始搭建react+typescript+antd+redux+less+vw自适应项目
49 0
|
2月前
|
前端开发
React查询、搜索类功能的实现
React查询、搜索类功能的实现
19 0
|
3月前
|
资源调度 前端开发 JavaScript
React 的antd-mobile 组件库,嵌套路由
React 的antd-mobile 组件库,嵌套路由
40 0
|
1月前
|
前端开发 JavaScript 架构师
react+typescript+umi+dva+antd
react+typescript+umi+dva+antd
24 0
|
2月前
|
存储 前端开发 中间件
React组件间的通信
React组件间的通信
16 1
|
2月前
|
前端开发 应用服务中间件 数据库
react服务端组件
react服务端组件
21 0
|
2月前
|
前端开发 JavaScript
快速上手React:从概述到组件与事件处理
快速上手React:从概述到组件与事件处理
|
3月前
|
前端开发 JavaScript API
React组件生命周期
React组件生命周期
74 1
|
6月前
|
前端开发
React-父子组件通讯
React-父子组件通讯
25 0
React-父子组件通讯
|
3月前
|
存储 前端开发 JavaScript
React组件中如何通讯
React组件中如何通讯
16 0