1. 程式人生 > >React 折騰記 - (6) 基於React 16.7 + Antd 3.10.7封裝的一個宣告式的查詢元件

React 折騰記 - (6) 基於React 16.7 + Antd 3.10.7封裝的一個宣告式的查詢元件

前言

最近把新的後臺系統寫好了..用的是上篇文章的技術棧(mobx+react16);

但是感覺mobx沒有想象中的好用,看到umi 2.x了.就著手又開始重構了...

仔細梳理了下上個系統,發現可以抽離的東西不少

有興趣的瞧瞧,沒興趣的止步,節約您的時間...


效果圖

  • 響應式傳入

  • 摺疊展開搜尋條件,預設六個隱藏展開按鈕,大於則顯示(點選直接取資料來源的長度)

  • 傳遞子元件作為搜尋按鈕區域


抽離思路及實現

思路

  • 合併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

可以自行拓展的點

  • 比如垂直展示
  • 比如表單校驗(關聯搜尋條件[就是必須有前置條件才能搜尋])

學無止境,任重而道遠...

有不對之處盡請留言,會及時修正,謝謝閱讀