Vue 2.x折騰記 - (17) 基於Ant Design Vue 封裝一個配置式的表單組件

前言

寫了個相似上篇搜索的封裝,可是要考慮的東西更多。javascript

具體業務比展現的代碼要複雜,篇幅太長就不引入了。css

效果圖

  • 2019-04-25html

  • 添加了下拉多選的渲染,並搜索默認過濾文本而非值vue

  • 簡化了渲染的子組件的代碼java

  • 2019-04-28react

    • 增長了對input type的控制

實現思路和功能

基礎的功能直接配置上來渲染,而上傳組件就不大合適了;antd

因此選擇了slot來實現,如何保證傳入的form-item的佈局一致,則是拿slot-scope數據結構

我這邊選型用的是vue 2.6 +的版本,因此直接用的是最新的寫法app

並且做爲表單組件,校驗這些確定須要考慮,因此數據的構造改造了下,函數

對於校驗規則這些走的是antd form用的那套,因此在傳遞的時候把對應的屬性拍平了,

到裏面再進行數據結構調整,目前部分控件樣式依舊須要本身修正!!!

演示的代碼用法

<form-list @change="onFormListChange">
       <template #field="{options}">
         <a-form-item label="Upload" v-bind="options">
           <a-upload
             v-decorator="[
               'upload',
               {
                 valuePropName: 'fileList',
                 getValueFromEvent: normFile
               }
             ]"
             name="logo"
             action="/upload.do"
             list-type="picture"
           >
             <a-button> <a-icon type="upload" /> Click to upload </a-button>
           </a-upload>
         </a-form-item>
       </template>
     </form-list>

複製代碼

代碼

  • FieldRender.vue
<template>
<a-form-item :label="fieldOptions.labelText" :label-col="fieldOptions.labelCol" :wrapper-col="fieldOptions.wrapperCol" >
  <a-input v-if="fieldOptions.fieldName && fieldOptions.type === 'text'" :size="fieldOptions.size ? fieldOptions.size : 'default'" v-decorator="[ fieldOptions.fieldName, { initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : '', rules: Array.isArray(fieldOptions.rules) && fieldOptions.rules.length > 0 ? fieldOptions.rules : [] } ]" :placeholder="fieldOptions.placeholder" />
  <a-select v-else-if="fieldOptions.fieldName && fieldOptions.type === 'select'" style="width: 100%" showSearch :options="fieldOptions.options" :filterOption="selectFilterOption" :size="fieldOptions.size ? fieldOptions.size : 'default'" allowClear v-decorator="[ fieldOptions.fieldName, { initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : undefined, rules: Array.isArray(fieldOptions.rules) && fieldOptions.rules.length > 0 ? fieldOptions.rules : [] } ]" :placeholder="fieldOptions.placeholder" />
  <a-input-number v-else-if="fieldOptions.fieldName && fieldOptions.type === 'number'" :size="fieldOptions.size ? fieldOptions.size : 'default'" :min="fieldOptions.min ? fieldOptions.min : 1" style="width: 100%" v-decorator="[ fieldOptions.fieldName, { initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : '', rules: Array.isArray(fieldOptions.rules) && fieldOptions.rules.length > 0 ? fieldOptions.rules : [] } ]" :placeholder="fieldOptions.placeholder" />
  <a-radio-group v-else-if="fieldOptions.fieldName && fieldOptions.type === 'radio' && Array.isArray(fieldOptions.options)" :size="fieldOptions.size ? fieldOptions.size : 'default'" buttonStyle="solid" v-decorator="[ fieldOptions.fieldName, { initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : '', rules: Array.isArray(fieldOptions.rules) && fieldOptions.rules.length > 0 ? fieldOptions.rules : [] } ]" >
    <template v-for="(item, index) in fieldOptions.options">
      <a-radio-button :key="index" :value="item.value">{{ item.label }} </a-radio-button>
    </template>
  </a-radio-group>

  <a-date-picker v-else-if="fieldOptions.fieldName && fieldOptions.type === 'datetime'" :size="fieldOptions.size ? fieldOptions.size : 'default'" :placeholder="fieldOptions.placeholder" v-decorator="[ fieldOptions.fieldName, { initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : null, rules: Array.isArray(fieldOptions.rules) && fieldOptions.rules.length > 0 ? fieldOptions.rules : [] } ]" />
  <a-range-picker v-else-if="fieldOptions.fieldName && fieldOptions.type === 'datetimeRange'" :size="fieldOptions.size ? fieldOptions.size : 'default'" v-decorator="[ fieldOptions.fieldName, { initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : null, rules: Array.isArray(fieldOptions.rules) && fieldOptions.rules.length > 0 ? fieldOptions.rules : [] } ]" :placeholder="fieldOptions.placeholder" />
  <a-cascader v-else-if="fieldOptions.fieldName && fieldOptions.type === 'cascader'" :size="fieldOptions.size ? fieldOptions.size : 'default'" :options="fieldOptions.options" :showSearch="{ cascaderFilter }" v-decorator="[ fieldOptions.fieldName, { initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : [] } ]" :placeholder="fieldOptions.placeholder" />
  <a-time-picker v-else-if="fieldOptions.fieldName && fieldOptions.type === 'timepicker'" v-decorator="[ fieldOptions.fieldName, { initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : null, rules: Array.isArray(fieldOptions.rules) && fieldOptions.rules.length > 0 ? fieldOptions.rules : [] } ]" :size="fieldOptions.size ? fieldOptions.size : 'default'" />
  <a-textarea :placeholder="fieldOptions.placeholder" v-else-if="fieldOptions.fieldName && fieldOptions.type === 'textarea'" v-decorator="[ fieldOptions.fieldName, { initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : null, rules: Array.isArray(fieldOptions.rules) && fieldOptions.rules.length > 0 ? fieldOptions.rules : [] } ]" :autosize="{ minRows: 6, maxRows: 24 }" />
  <a-select mode="multiple" :size="fieldOptions.size ? fieldOptions.size : 'default'" optionFilterProp="children" v-else-if="fieldOptions.fieldName && fieldOptions.type === 'multiple'" :placeholder="fieldOptions.placeholder" style="width: 100%" :options="fieldOptions.options" v-decorator="[ fieldOptions.fieldName, { initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : [], rules: Array.isArray(fieldOptions.rules) && fieldOptions.rules.length > 0 ? fieldOptions.rules : [] } ]" />
</a-form-item>
</template>

<script> export default { props: { fieldOptions: { // 待渲染的對象 type: Object, default: function() { return {}; } } }, methods: { selectFilterOption(input, option) { // 下拉框過濾函數 return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0; }, cascaderFilter(inputValue, path) { // 級聯過濾函數 return path.some(option => option.label.toLowerCase().indexOf(inputValue.toLowerCase()) > -1); } } }; </script>


複製代碼
  • FormList.vue
<template>
 <div class="form-list-wrapper">
   <a-form :layout="formLayout" :form="form">
     <template v-for="(item, index) in renderDataSource">
       <template v-if="item.type && item.fieldName">
         <field-render :fieldOptions="item" :key="item.fieldName" />
       </template>
     </template>
     <slot name="field" :options="GlobalOptions" />

     <a-form-item :wrapper-col="buttonItemLayout.wrapperCol">
       <a-tooltip placement="bottom">
         <template slot="title">
           <span>提交表單</span>
         </template>
         <a-button type="primary" :size="size" @click="handleSubmit">提交</a-button>
       </a-tooltip>

       <a-tooltip placement="bottom">
         <template slot="title">
           <span>清空全部控件的值</span>
         </template>
         <a-button :size="size" style="margin-left: 8px" @click="resetSearchForm">重置</a-button>
       </a-tooltip>
     </a-form-item>
   </a-form>
 </div>
</template>

<script> import FieldRender from './FieldRender'; export default { name: 'FormList', components: { FieldRender }, props: { formLayout: { // 表單佈局 type: String, // 'horizontal'|'vertical'|'inline' default: 'horizontal' }, datetimeTotimeStamp: { // 是否把時間控件的返回值所有轉爲時間戳 type: Boolean, default: false }, size: { // 全局控件大小 type: String, default: 'default' }, responsive: { // 表單項的響應佈局 type: Object, default: function() { return { labelCol: { span: 5 }, wrapperCol: { span: 16 } }; } }, dataSource: { type: Array, default: function() { return [ { type: 'text', // 控件類型 labelText: '控件名稱', // 控件顯示的文本 fieldName: 'formField1', placeholder: '文本輸入區域', // 默認控件的空值文本 rules: [ { required: true, message: '必填' } ] }, { labelText: '數字輸入框', type: 'number', fieldName: 'formField2', placeholder: '這只是一個數字的文本輸入框' }, { labelText: '單選框', type: 'radio', fieldName: 'formField3', defaultValue: '0', options: [ { label: '選項1', value: '0' }, { label: '選項2', value: '1' } ] }, { labelText: '日期選擇', type: 'datetime', fieldName: 'formField4', placeholder: '選擇日期' }, { labelText: '日期範圍', type: 'datetimeRange', fieldName: 'formField5', placeholder: ['開始日期', '選擇日期'] }, { labelText: '時刻選擇', type: 'timepicker', fieldName: 'formField8', placeholder: '請選擇時刻(時間)' }, { labelText: '文本區域', type: 'textarea', fieldName: 'formField9', placeholder: '請輸入文本了內容' }, { type: 'multiple', labelText: '角色', fieldName: 'role', defaultValue: [], rules: [ { required: true, message: '必須選擇一種角色' } ], options: [ { label: '系統管理員', value: '0' }, { label: '風控管理員', value: '1' }, { label: '催收管理員', value: '2' }, { label: '催收員', value: '3' }, { label: '審覈員', value: '4' }, { label: '財務', value: '5' } ] }, { labelText: '下拉框', type: 'select', fieldName: 'formField7', placeholder: '下拉選擇你要的', options: [ { label: 'text1', value: '0' }, { label: 'text2', value: '1' } ] }, { labelText: '聯動', type: 'cascader', fieldName: 'formField6', placeholder: '級聯選擇', options: [ { value: 'zhejiang', label: 'Zhejiang', children: [ { value: 'hangzhou', label: 'Hangzhou', children: [ { value: 'xihu', label: 'West Lake' }, { value: 'xiasha', label: 'Xia Sha', disabled: true } ] } ] }, { value: 'jiangsu', label: 'Jiangsu', children: [ { value: 'nanjing', label: 'Nanjing', children: [ { value: 'zhonghuamen', label: 'Zhong Hua men' } ] } ] } ] } ]; } } }, beforeCreate() { this.form = this.$form.createForm(this); }, computed: { GlobalOptions() { // 全局配置 return { size: this.size, ...this.formItemLayout }; }, renderDataSource() { // 重組傳入的數據,合併全局配置,子項的配置優先全局 return this.dataSource.map(item => ({ ...this.GlobalOptions, ...item })); }, formItemLayout() { // 更改佈局項目的尺寸 const { formLayout } = this; if (formLayout === 'horizontal') { return this.responsive; } else { return {}; } }, buttonItemLayout() { // 提交按鈕佈局 const { formLayout } = this; return formLayout === 'horizontal' ? { wrapperCol: { span: 14, offset: 4 } } : {}; } }, methods: { handleParams(obj) { // 判斷必須爲obj if (!(Object.prototype.toString.call(obj) === '[object Object]')) { return {}; } let tempObj = {}; for (let [key, value] of Object.entries(obj)) { if (Array.isArray(value) && value.length <= 0) continue; if (Object.prototype.toString.call(value) === '[object Function]') continue; if (this.datetimeTotimeStamp) { // 如果爲true,則轉爲時間戳 if (Object.prototype.toString.call(value) === '[object Object]' && value._isAMomentObject) { // 判斷moment value = value.valueOf(); } if (Array.isArray(value) && value[0]._isAMomentObject && value[1]._isAMomentObject) { // 判斷moment value = value.map(item => item.valueOf()); } } // 如果爲字符串則清除兩邊空格 if (value && typeof value === 'string') { value = value.trim(); } tempObj[key] = value; } return tempObj; }, handleSubmit(e) { // 觸發表單提交,也就是搜索按鈕 e.preventDefault(); this.form.validateFields((err, values) => { if (!err) { console.log('處理前的表單數據', values); const queryParams = this.handleParams(values); this.$emit('change', queryParams); } }); }, resetSearchForm() { // 重置整個查詢表單 this.form.resetFields(); this.$emit('change', null); } } }; </script>

<style lang="scss"> .form-list-wrapper { .ant-form-inline { .ant-form-item { display: flex; margin-bottom: 12px; margin-right: 0; .ant-form-item-control-wrapper { flex: 1; display: inline-block; vertical-align: middle; } > .ant-form-item-label { line-height: 32px; padding-right: 8px; width: auto; } .ant-form-item-control { height: 32px; line-height: 32px; display: flex; justify-content: flex-start; align-items: center; .ant-form-item-children { min-width: 160px; } } } } .table-page-search-submitButtons { display: block; margin-bottom: 24px; white-space: nowrap; } } </style>


複製代碼

問題

暴露的方法和搜索組件同樣,@change回來表單數據;

問題:

操做父的props會形成死循環(在有slot的狀況下,因slot-scope拿的是父props通過computed後的值)。

解決方案:

已經改用其餘實現姿式,抽離成獨立組件,再聯動數據。

總結

antd vue版本目前的功能復現上,仍是有所欠缺,可能vuereact實現的機子不一致致使;

無論怎麼說,不考慮極端狀況下,目前這個庫用起來感受還好;

至少是可用狀態,後續如有修正,會繼續更新文章,謝謝閱讀

相關文章
相關標籤/搜索