最近把新的後臺系統寫好了..用的是上篇文章的技術棧(mobx+react16
);javascript
可是感受mobx
沒有想象中的好用,看到umi 2.x
了,就着手又開始重構了。css
仔細梳理了下上個系統,發現能夠抽離的東西很多html
此篇文章是我針對咱們的搜索條件抽離的一個組件,僅供參考。java
調整記錄react
reset
表單props
回調,調用則取默認不帶參數的列表responsive
這個字段(放在待渲染的json
)Input
控件輸入一個字符自動失焦點的問題(Math.random
的鍋)InputNumber
組件引入,搜索條件也有多是搜索ID
的..純數字!!lodash
的isEqual
進行對象深度比對,下降state
的合併次數,減小re-render
value
爲空數組不返回,字符串value
清除兩邊的空格props.children
傳入改造,添加style
input
輸入,其餘選擇性的控制項會直接觸發)getFieldDecorator
的rules
條件,則把下margin
去掉Input
的控件,自動觸發表單提交, props
的autoSearch
爲true
Input
控件的時候,去除卡片效果props
傳遞的值,儘量的減小傳遞的東西(在組件內部實現默認值合併),把渲染的子組件經過遍歷json
去實現;antd
表單組件,聚合全部表單數據(自動雙向綁定,設置默認值等);dva
來維護狀態,純靠props
和state
構建,而後統一把構建的表單數據向父級暴露..antd
對於日期控件使用null
來置空],外部初始化能夠用getFieldDecorator
的initialValue
,已經暴露使用姿式git
<AdvancedSearchForm data={searchItem} getSearchFormData={this.searchList} resetSearchForm={this.resetSearchList} accumulate="3">
<Button type="dashed" icon="download" style={{ marginLeft: 8 }} htmlType="submit">
下載報表
</Button>
</AdvancedSearchForm>
複製代碼
根據ctype
渲染的控件有Input,Button,Select,DatePicker,Cascader,Radio
github
容許傳遞的props有四個個,部分props有默認值,傳遞的會合並進去npm
字段 | 類型 | 解釋 |
---|---|---|
data |
數組對象[obj] | 數據源(構建) |
accumulate |
字符串 | 超過多少個摺疊起來 |
responseLayout |
對象 | 傳遞對象,響應式 |
csize |
字符串 | 控件大小設置,small(小) , default(默認) |
getSearchFormData |
函數 | 回調函數,拿到表單的數據 |
resetSearchForm |
函數 | 回調函數,當重置表單數據的時候 |
autoSearch |
布爾值 | 啓動非input 的控件自動觸發提交的props函數 |
data
的數據格式基本和antd
要求的格式一致,除了個別用來判斷或者渲染子組件的,json
字段解釋:數組
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',
},
],
},
],
},
],
複製代碼
/* * @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;
複製代碼
// 列表搜索區域
.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
,lodash
能夠自行拓展的點
學無止境,任重而道遠,有不對之處盡請留言,會及時修正,謝謝閱讀