前言
最近把新的後臺系統寫好了..用的是上篇文章的技術棧(mobx+react16
);
但是感覺mobx
沒有想象中的好用,看到umi 2.x
了.就着手又開始重構了...
仔細梳理了下上個系統,發現可以抽離的東西不少
有興趣的瞧瞧,沒興趣的止步,節約您的時間...
效果圖
- 響應式傳入
- 摺疊展開搜索條件,默認六個隱藏展開按鈕,大於則顯示(點擊直接取數據源的長度)
- 傳遞子組件作爲搜索按鈕區域
抽離思路及實現
思路
- 合併
props
傳遞的值,儘可能的減少傳遞的東西(在組件內部實現默認值合併),把渲染的子組件通過遍歷json
去實現; - 整個查詢區域用的
antd
表單組件,聚合所有表單數據(自動雙向綁定,設置默認值等); - 爲了降低複雜度,子組件不考慮
dva
來維護狀態,純靠props
和state
構建,然後統一把構建的表單數據向父級暴露.. - 內部的state默認初始化都爲空[
antd
對於日期控件使用null
來置空],外部初始化可以用getFieldDecorator
的initialValue
,已經暴露
實現的功能
支持的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, accumulate } = this.props; const isRnderToggleIcon = accumulate ? data && data.length > accumulate ? true : false : data.length > 6; 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
可以自行拓展的點
- 比如垂直展示
- 比如表單校驗(關聯搜索條件[就是必須有前置條件才能搜索])
學無止境,任重而道遠...
有不對之處盡請留言,會及時修正,謝謝閱讀