版权声明:版权所有:CRPER([email protected]); 掘金|Github:CRPER; https://blog.csdn.net/bomess/article/details/84032394
前言
最近把新的后台系统写好了…用的是上篇文章的技术栈(mobx+react16
);
但是感觉mobx
没有想象中的好用,看到umi 2.x
了.就着手又开始重构了…
仔细梳理了下上个系统,发现可以抽离的东西不少
有兴趣的瞧瞧,没兴趣的止步,节约您的时间…
- 2018-11-15 :
- 新增了reset表单
props
回调,调用则取默认不带参数的列表 - 新增待渲染的子组件布局规格的传入,
responsive
这个字段(放在待渲染的json
)
- 新增了reset表单
效果图
响应式传入
折叠展开搜索条件,默认六个隐藏展开按钮,大于则显示(点击直接取数据源的长度)
传递子组件作为搜索按钮区域
统一变动控件的规格
重置表单
子组件引入自身响应式条件(会话状态,按钮太多,等分会造成各种换行,不舒服)
抽离思路及实现
思路
- 合并
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 |
函数 | 回调函数,当重置表单数据的时候 |
数据源格式
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
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) => {
// 表单表单不报错,且props有传递的情况下,才返回表单数据
if (!err && this.props.getSearchFormData) {
this.props.getSearchFormData(values);
}
});
};
// 重置表单
handleReset = () => {
this.props.form.resetFields();
// 若是有回调函数,则返回空对象
if (this.props.resetSearchForm) {
this.props.resetSearchForm(null);
}
};
// 生成 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 : 8;
};
// 判断需要渲染的组件
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, accumulate } = this.props;
const isRnderToggleIcon = accumulate
? (data && data.length) > accumulate
? true
: false
: data.length > 8;
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>
{isRnderToggleIcon ? (
<Row gutter={24} type="flex" justify="center">
<a onClick={this.toggle}>
<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;
}
总结
温馨提示
- 没用
prop-types
, 感觉没必要…(若是用ts
的小伙伴,运行时类型推断比这个强大的多,还不会打包冗余代码) - 没发布
npm
, 只是提供我写的思路,对您有没有帮助,见仁见智 - 依赖
moment
,antd
可以自行拓展的点
- 比如垂直展示
- 比如表单校验(关联搜索条件[就是必须有前置条件才能搜索])
学无止境,任重而道远…
有不对之处尽请留言,会及时修正,谢谢阅读