React 折腾记 - (6) 基于React 16.6 + Antd 3.10.7封装的一个声明式的查询组件

栏目: IOS · Android · 发布时间: 6年前

内容简介:最近把新的后台系统写好了..用的是上篇文章的技术栈(但是感觉仔细梳理了下上个系统,发现可以抽离的东西不少

最近把新的后台系统写好了..用的是上篇文章的技术栈( mobx+react16 );

但是感觉 mobx 没有想象中的好用,看到 umi 2.x 了.就着手又开始重构了...

仔细梳理了下上个系统,发现可以抽离的东西不少

有兴趣的瞧瞧,没兴趣的止步,节约您的时间...

效果图

  • 响应式传入
React 折腾记 - (6) 基于React 16.6 + Antd 3.10.7封装的一个声明式的查询组件
React 折腾记 - (6) 基于React 16.6 + Antd 3.10.7封装的一个声明式的查询组件
  • 折叠展开搜索条件,默认六个隐藏展开按钮,大于则显示(点击直接取数据源的长度)

    React 折腾记 - (6) 基于React 16.6 + Antd 3.10.7封装的一个声明式的查询组件
  • 传递子组件作为搜索按钮区域

React 折腾记 - (6) 基于React 16.6 + Antd 3.10.7封装的一个声明式的查询组件

抽离思路及实现

思路

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

实现的功能

支持的props

根据 ctype 渲染的控件有 Input,Button,Select,DatePicker,Cascader,Radio

允许传递的props有三个,所有props均有默认值,传递的会合并进去

data
accumulate
responseLayout
getSearchFormData
<AdvancedSearchForm data={searchItem}  getSearchFormData={this.searchList} accumulate="3">
              <Button type="dashed" icon="download" style={{ marginLeft: 8 }} htmlType="submit">
                下载报表
              </Button>
   </AdvancedSearchForm>
复制代码

数据源格式

data 的数据格式基本和 antd 要求的格式一致,除了个别用来判断或者渲染子组件的,

标准格式为:

ctype(controller-type:控件类型)
attr(控件支持的属性)
field(受控表单控件的配置项)
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: '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

import { PureComponent } from 'react';
import {
  Form,
  Row,
  Col,
  Input,
  Button,
  Select,
  DatePicker,
  Card,
  Cascader,
  Radio,
  Icon,
} from 'antd';

const { MonthPicker, RangePicker } = DatePicker;
const Option = Select.Option;
const FormItem = Form.Item;

const RadioButton = Radio.Button;
const RadioGroup = Radio.Group;

@Form.create()
class AdvancedSearchForm extends PureComponent {
  state = {
    expand: false,
    factoryData: [
      {
        ctype: 'input',
        attr: {
          placeholder: '请输入查询内容...',
        },
        field: {
          label: '',
          value: '',
          params: {
            initialValue: '',
          },
        },
      },
      {
        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) {
    /**
     * data: 构建的数据
     * single: 单一选择,会禁用其他输入框
     * mode: coallpse(折叠)
     */
    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 { factoryData: newData };
    }

    return null;
  }

  // 提交表单
  handleSearch = e => {
    e.preventDefault();
    this.props.form.validateFields((err, values) => {
      if (!err) {
        this.props.getSearchFormData(values);
      }
    });
  };

  // 重置表单
  handleReset = () => {
    this.props.form.resetFields();
  };

  // 生成 Form.Item
  getFields = () => {
    const { factoryData } = this.state;
    const children = [];
    if (factoryData) {
      for (let i = 0; i < factoryData.length; i++) {
        // 若是控件的名字丢.亦或filed的字段名或之值丢失则不渲染该组件
        // 若是为select或cascader没有子组件数据也跳过
        const {
          ctype,
          field: { value, label },
          selectOptionsChildren,
        } = factoryData[i];
        if (
          !ctype ||
          !value ||
          !label ||
          ((ctype === 'select' || ctype === 'cascader') &&
            selectOptionsChildren &&
            selectOptionsChildren.length < 1)
        )
          continue;

        // 渲染组件
        let formItem = this.renderItem({
          ...factoryData[i],
          itemIndex: i,
        });

        // 缓存组件数据
        children.push(formItem);
      }
      return children;
    } else {
      return [];
    }
  };

  // 合并响应式props
  combindResponseLayout = () => {
    const { responseLayout } = this.props;
    // 响应式
    return {
      xs: 24,
      sm: 24,
      md: 12,
      lg: 8,
      xxl: 6,
      ...responseLayout,
    };
  };

  // 计算外部传入需要显示隐藏的个数
  countHidden = () => {
    const { data, accumulate } = this.props;
    return this.state.expand ? data.length : accumulate ? accumulate : 6;
  };

  // 判断需要渲染的组件
  renderItem = data => {
    const { getFieldDecorator } = this.props.form;

    const { ctype, field, attr, itemIndex } = data;

    const ResponseLayout = this.combindResponseLayout();

    const count = this.countHidden();

    switch (ctype) {
      case 'input':
        return (
          <Col
            {...ResponseLayout}
            style={{ display: itemIndex < count ? 'block' : 'none' }}
            key={Math.random() * 1000000}
          >
            <FormItem label={field.label}>
              {getFieldDecorator(field.value, field.params ? field.params : {})(
                <Input {...attr} />
              )}
            </FormItem>
          </Col>
        );

      case 'select':
        return (
          <Col
            {...ResponseLayout}
            style={{ display: itemIndex < count ? 'block' : 'none' }}
            key={Math.random() * 1000000}
          >
            <FormItem label={field.label}>
              {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={Math.random() * 1000000}
          >
            <FormItem label={field.label}>
              {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={Math.random() * 1000000}
          >
            <FormItem label={field.label}>
              {getFieldDecorator(field.value, field.params ? field.params : {})(
                <DatePicker {...attr} />
              )}
            </FormItem>
          </Col>
        );

      case 'monthPicker':
        return (
          <Col
            {...ResponseLayout}
            style={{ display: itemIndex < count ? 'block' : 'none' }}
            key={Math.random() * 1000000}
          >
            <FormItem label={field.label}>
              {getFieldDecorator(field.value, field.params ? field.params : {})(
                <MonthPicker {...attr} />
              )}
            </FormItem>
          </Col>
        );

      case 'timerangePicker':
        return (
          <Col
            {...ResponseLayout}
            style={{ display: itemIndex < count ? 'block' : 'none' }}
            key={Math.random() * 1000000}
          >
            <FormItem label={field.label}>
              {getFieldDecorator(field.value, field.params ? field.params : {})(
                <RangePicker {...attr} />
              )}
            </FormItem>
          </Col>
        );

      case 'datePicker':
        return (
          <Col
            {...ResponseLayout}
            style={{ display: itemIndex < count ? 'block' : 'none' }}
            key={Math.random() * 1000000}
          >
            <FormItem label={field.label}>
              {getFieldDecorator(field.value, field.params ? field.params : {})(
                <DatePicker {...attr} />
              )}
            </FormItem>
          </Col>
        );
      case 'radio':
        return (
          <Col
            {...ResponseLayout}
            style={{ display: itemIndex < count ? 'block' : 'none' }}
            key={Math.random() * 1000000}
          >
            <FormItem label={field.label}>
              {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, children } = this.props;

    return (
      <Form className="ant-advanced-search-form" onSubmit={this.handleSearch}>
        <Card
          title="搜索区域"
          extra={
            <>
              <Button type="primary" htmlType="submit">
                搜索
              </Button>
              <Button style={{ marginLeft: 8 }} onClick={this.handleReset}>
                清除
              </Button>
              {children ? <>{children}</> : null}
            </>
          }
          style={{ width: '100%' }}
        >
          <Row gutter={24} type="flex" justify="start">
            {this.getFields()}
          </Row>
          {data && data.length === 3 ? null : (
            <Row gutter={24} type="flex" justify="center">
              <a onClick={this.toggle}>
                <Icon type={expand ? 'up' : 'down'} />{' '}
              </a>
            </Row>
          )}
        </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;
}

复制代码

总结

温馨提示

  • 没用 prop-types , 感觉没必要...(若是用 ts 的小伙伴,运行时类型推断比这个强大的多,还不会打包冗余代码)
  • 没发布 npm , 只是提供我写的思路,对您有没有帮助,见仁见智
  • 依赖 moment , antd

可以自行拓展的点

  • 比如垂直展示
  • 比如表单校验(关联搜索条件[就是必须有前置条件才能搜索])

学无止境,任重而道远...

有不对之处尽请留言,会及时修正,谢谢阅读


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Computer Age Statistical Inference

Computer Age Statistical Inference

Bradley Efron、Trevor Hastie / Cambridge University Press / 2016-7-21 / USD 74.99

The twenty-first century has seen a breathtaking expansion of statistical methodology, both in scope and in influence. 'Big data', 'data science', and 'machine learning' have become familiar terms in ......一起来看看 《Computer Age Statistical Inference》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

URL 编码/解码
URL 编码/解码

URL 编码/解码

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具