1、背景

vue后台管理系统,会有很多表格页面,表格上方会有一些搜索选项。表格直接使用el-table即可,而搜索栏区域每次写起来都很繁琐,且多人开发情况下每个人写的样式都不相同,布局样式无法统一。

所以要考虑对搜索栏做一个封装,统一配置引用,提升开发维护效率和界面统一。

完成后的效果大概就是长这样:
在这里插入图片描述

2、分析

项目使用的是elementui框架,搜索栏这种表单提交,首先要使用el-form组件来封装,而复杂点就是表单项可能有很多种,例如input输入框、select选择框、日期时间选择框、日期时间范围选择框、cascader级联选择框等,每一项的字段名prop、名称label、绑定的属性方法都不尽相同。所以不能通过普通的绑定个别属性的方式来处理,而slot插槽的方式也无法简化,最终决定通过传递一个配置项数组的形式来解析生成相应的结构。

3、实现

目前实现的方式由两部分组成,一部分是form表单组件,接受父组件传递的配置项数组,一部分是封装一些常用的表单项组件,通过v-if来控制,form表单组件里引入该表单项组件,循环遍历,根据传递的表单项类型来匹配显示具体的表单项。

(1)form表单组件(searchForm.vue)示例代码:

<el-form
  :model="formData"
  ref="formRef"
  :inline="true"
>
  <el-form-item
    v-for="(item, index) in formOptions"
    :key="newKeys[index]"
    :prop="item.prop"
    :label="item.label ? (item.label + '') : ''"
    :rules="item.rules"
  >
    <formItem
      v-model="formData[item.prop]"
      :itemOptions="item"
    />
  </el-form-item>
</el-form>

(2)formItem表单项组件(formItem.vue)示例代码:

<el-input
  v-if="isInput"
  v-model="currentVal"
  v-bind="bindProps"
  v-on="bindEvents"
  size="mini"
></el-input>

<el-select
  v-if="isSelect"
  v-model="currentVal"
  v-bind="bindProps"
  v-on="bindEvents"
  size="mini"
  clearable
 >
  <el-option
    v-for="item in itemOptions.options"
    :key="item.value"
    :label="item.label"
    :value="item.value"
  ></el-option>
</el-select>

4、关键点

由于elementui表单组件本身有很多配置属性,不可能把所有的属性和方法都写死封装,要想无缝支持,需要用到vue的v-bind和v-on特性,vue的v-bind和v-on支持赋值为对象类型,vue会自动遍历对象里的属性依次绑定,v2.4.0+支持。

5、参数配置项解释

(1)示例:

[{
	label: '用户名', // label文字
	prop: 'username', // 字段名
	element: 'el-input', // 指定elementui组件
	initValue: '阿黄', // 字段初始值
	placeholder: '请输入用户名', // elementui组件属性
	rules: [{ required: true, message: '必填项', trigger: 'blur' }], // elementui组件属性
	events: { // elementui组件方法
		input (val) {
			console.log(val)
		},
	}
}]
  • label 用于绑定给el-form-item上的label,表单项标题
  • prop 用于绑定给el-form-item上的prop,字段名,必填
  • element 指定elementui表单项的组件名,必填
  • initValue 表单项的初始值,可选
  • events 对象,对象里加方法,js原生方法或者elementui表单项组件支持的方法都可以加进去,通过v-on遍历绑定
  • … 其他elementui表单项组件支持的属性或者html原生属性都可以添加,常用的例如rules表单校验、placeholder提示,通过v-bind遍历绑定

(2)参数传递解析的流程:

  • 首先,searchForm.vue组件里通过props接收参数:
formOptions: {
  type: Array,
  required: true,
  default () {
    return []
  }
},
  • created生命周期里处理初始值:
// 添加初始值
addInitValue () {
  const obj = {}
  this.formOptions.forEach(v => {
    if (v.initValue !== undefined) {
      obj[v.prop] = v.initValue
    }
  })
  this.formData = obj
}
  • 一部分配置项绑定在el-form-item上,一部分传递给formItem表单项组件再绑定:
<el-form-item
  v-for="(item, index) in formOptions"
  :key="newKeys[index]"
  :prop="item.prop"
  :label="item.label ? (item.label + '') : ''"
  :rules="item.rules"
>
  <formItem
    v-model="formData[item.prop]"
    :itemOptions="item"
  />
</el-form-item>
  • formItem.vue表单项组件里props接受传参:
itemOptions: {
  type: Object,
  default () {
    return {}
  }
}
  • computed里处理接收的参数itemOptions,生成要绑定的所有属性对象bindProps:
// 绑定属性
bindProps () {
  let obj = { ...this.itemOptions }
  // 移除已使用的或不相关的冗余属性
  delete obj.label
  delete obj.prop
  delete obj.element
  delete obj.initValue
  delete obj.rules
  delete obj.events
  if (obj.element === 'el-select') {
    delete obj.options
  }
  return obj
},
  • computed里生成要绑定的所有方法对象bindEvents:
// 绑定方法
bindEvents () {
  return this.itemOptions.events || {}
},
  • 最后dom里使用这些数据绑定:
<el-input
  v-if="isInput"
  v-model="currentVal"
  v-bind="bindProps"
  v-on="bindEvents"
></el-input>

(3)特殊情况的处理

由于elementui的el-select里是通过el-option遍历实现的,而遍历数组options按elementui官方不是绑定在el-select上的,所以针对el-select的配置项再加一个options属性,即select选择项的数据数组。

<el-select
 v-if="isSelect"
 v-model="currentVal"
 v-bind="bindProps"
 v-on="bindEvents"
 size="mini"
 clearable
>
 <el-option
   v-for="item in itemOptions.options"
   :key="item.value"
   :label="item.label"
   :value="item.value"
 ></el-option>
</el-select>

elementui的日期时间选择器分了很多种,根据业务需要分别处理一下,我这里是根据type划分成了三种分开处理,最常用的是datetimerange日期时间范围选择器,作为默认项,还有一种monthrange,其余的都划为一种。(具体处理见文章末尾的完整代码)

6、按钮组

按钮其实就那么几个,没必要做太多的封装,根据业务有哪些按钮就封装进去,目前我这里就封装了三个按钮。
通过props接受一个字符串标识按钮组:

// 提交按钮项,多个用逗号分隔(search搜索, export导出, reset重置)
btnItems: {
  type: String,
  default () {
    return 'search'
  }
}

7、使用方式示例

  • dom里:
<!-- 搜索 -->
<searchForm :formOptions="formOptions" @onSearch="onSearch"/>
  • vue data里:
data () {
  return {
    formOptions: [
	  {
	    label: '意见内容',
	    prop: 'content',
	    element: 'el-input'
	  },
	  {
	    label: '类型',
	    prop: 'type',
	    element: 'el-select',
	    options: [
	      { label: '给点意见', value: '1' },
	      { label: '售后问题', value: '2' }
	    ]
	  },
	  {
	    label: '状态',
	    prop: 'status',
	    element: 'el-select',
	    options: getFeedbackStatus()
	  },
	  {
	    label: '提交时间',
	    prop: 'timeRange',
	    element: 'el-date-picker'
	  }
	],
  }
}

  • vue methods里:
methods: {
  // 获取搜索表单提交的数据
  onSearch (val) {
	console.log(val)
  }
}

8、完整代码

(1)searchForm.vue

/**
 * 搜索栏公共组件
 */
<template>
  <div class="search-form-box">
    <el-form
      :model="formData"
      ref="formRef"
      :inline="true"
    >
      <el-form-item
        v-for="(item, index) in formOptions"
        :key="newKeys[index]"
        :prop="item.prop"
        :label="item.label ? (item.label + '') : ''"
        :rules="item.rules"
      >
        <formItem
          v-model="formData[item.prop]"
          :itemOptions="item"
        />
      </el-form-item>
      
	  <!-- 自定义插槽,可用于特殊表单块 -->
      <slot></slot>
    </el-form>

    <!-- 提交按钮 -->
    <div class="btn-box">
      <el-button
        v-if="btnItems.includes('search')"
        size="mini"
        type="primary"
        class="btn-search"
        @click="onSearch"
      >搜索</el-button>

      <el-button
        v-if="btnItems.includes('export')"
        size="mini"
        type="primary"
        class="btn-export"
        @click="onExport"
      >导出</el-button>

      <el-button
        v-if="btnItems.includes('reset')"
        size="mini"
        type="default"
        class="btn-reset"
        @click="onReset"
      >重置</el-button>
    </div>
  </div>
</template>

<script>
import formItem from './formItem'
import tools from '@/utils/tools'

export default {
  props: {
    /**
     * 表单配置
     * 示例:
     * [{
     *   label: '用户名', // label文字
     *   prop: 'username', // 字段名
     *   element: 'el-input', // 指定elementui组件
     *   initValue: '阿黄', // 字段初始值
     *   placeholder: '请输入用户名', // elementui组件属性
     *   rules: [{ required: true, message: '必填项', trigger: 'blur' }], // elementui组件属性
     *   events: { // elementui组件方法
     *     input (val) {
     *       console.log(val)
     *     },
     *     ...... // 可添加任意elementui组件支持的方法
     *   }
     *   ...... // 可添加任意elementui组件支持的属性
     * }]
     */
    formOptions: {
      type: Array,
      required: true,
      default () {
        return []
      }
    },
    // 提交按钮项,多个用逗号分隔(search, export, reset)
    btnItems: {
      type: String,
      default () {
        return 'search'
      }
    }
  },

  data () {
    return {
      formData: {}
    }
  },

  computed: {
    newKeys () {
      return this.formOptions.map(v => {
        return tools.createUniqueString()
      })
    }
  },

  created () {
    this.addInitValue()
  },

  methods: {
    // 校验
    onValidate (callback) {
      this.$refs.formRef.validate(valid => {
        if (valid) {
          console.log('提交成功')
          console.log(this.formData)
          callback()
        }
      })
    },
    // 搜索
    onSearch () {
      this.onValidate(() => {
        this.$emit('onSearch', this.formData)
      })
    },
    // 导出
    onExport () {
      this.onValidate(() => {
        this.$emit('onExport', this.formData)
      })
    },
    onReset () {
      this.$refs.formRef.resetFields()
    },
    // 添加初始值
    addInitValue () {
      const obj = {}
      this.formOptions.forEach(v => {
        if (v.initValue !== undefined) {
          obj[v.prop] = v.initValue
        }
      })
      this.formData = obj
    }
  },

  components: { formItem }
}
</script>

<style lang='less' scoped>
.search-form-box {
  display: flex;
  margin-bottom: 15px;

  .btn-box {
    padding-top: 5px;
    display: flex;

    button {
      height: 28px;
    }
  }
  .el-form {
    /deep/ .el-form-item__label {
      padding-right: 0;
    }
    .el-form-item {
      margin-bottom: 0;

      &.is-error {
        margin-bottom: 22px;
      }
    }
    // el-input宽度
    /deep/ .form-item {
      > .el-input:not(.el-date-editor) {
        width: 120px;
      }
    }
    /deep/ .el-select {
      width: 120px;
    }
    /deep/ .el-cascader {
      width: 200px;
    }
  }
}

</style>

(2)formItem.vue

/**
 * 表单匹配项
 */
<template>
  <div class='form-item'>
    <el-input
      v-if="isInput"
      v-model="currentVal"
      v-bind="bindProps"
      v-on="bindEvents"
      size="mini"
    ></el-input>

    <el-input-number
      v-if="isInputNumber"
      v-model="currentVal"
      v-bind="bindProps"
      v-on="bindEvents"
      :controls-position="itemOptions['controls-position'] || 'right'"
      size="mini"
    ></el-input-number>

    <el-select
      v-if="isSelect"
      v-model="currentVal"
      v-bind="bindProps"
      v-on="bindEvents"
      size="mini"
      clearable
     >
      <el-option
        v-for="item in itemOptions.options"
        :key="item.value"
        :label="item.label"
        :value="item.value"
      ></el-option>
    </el-select>

    <!-- datetimerange/daterange -->
    <el-date-picker
      v-if="isDatePickerDateRange"
      v-model="currentVal"
      v-bind="bindProps"
      v-on="bindEvents"
      :type="itemOptions.type || 'datetimerange'"
      size="mini"
      clearable
      :picker-options="pickerOptionsRange"
      start-placeholder="开始日期"
      range-separator=""
      end-placeholder="结束日期"
      :default-time="['00:00:00', '23:59:59']"
      value-format="yyyy-MM-dd HH:mm:ss"
    ></el-date-picker>

    <!-- monthrange -->
    <el-date-picker
      v-if="isDatePickerMonthRange"
      v-model="currentVal"
      v-bind="bindProps"
      v-on="bindEvents"
      type="monthrange"
      size="mini"
      clearable
      :picker-options="pickerOptionsRangeMonth"
      start-placeholder="开始日期"
      range-separator=""
      end-placeholder="结束日期"
      value-format="yyyy-MM"
    ></el-date-picker>

    <!-- others -->
    <el-date-picker
      v-if="isDatePickerOthers"
      v-model="currentVal"
      v-bind="bindProps"
      v-on="bindEvents"
      :type="itemOptions.type"
      size="mini"
      clearable
      placeholder="请选择日期"
    ></el-date-picker>

    <el-cascader
      v-if="isCascader"
      v-model="currentVal"
      v-bind="bindProps"
      v-on="bindEvents"
      size="mini"
      clearable
    ></el-cascader>
  </div>
</template>

<script>
import tools from '@/utils/tools'

export default {
  inheritAttrs: false,

  props: {
    value: {},
    itemOptions: {
      type: Object,
      default () {
        return {}
      }
    }
  },

  data () {
    return {
      pickerOptionsRange: tools.pickerOptionsRange,
      pickerOptionsRangeMonth: tools.pickerOptionsRangeMonth
    }
  },

  computed: {
    // 双向绑定数据值
    currentVal: {
      get () {
        return this.value
      },
      set (val) {
        this.$emit('input', val)
      }
    },
    // 绑定属性
    bindProps () {
      let obj = { ...this.itemOptions }
      // 移除冗余属性
      delete obj.label
      delete obj.prop
      delete obj.element
      delete obj.initValue
      delete obj.rules
      delete obj.events
      if (obj.element === 'el-select') {
        delete obj.options
      }
      return obj
    },
    // 绑定方法
    bindEvents () {
      return this.itemOptions.events || {}
    },
    // el-input
    isInput () {
      return this.itemOptions.element === 'el-input'
    },
    // el-input-number
    isInputNumber () {
      return this.itemOptions.element === 'el-input-number'
    },
    // el-select
    isSelect () {
      return this.itemOptions.element === 'el-select'
    },
    // el-date-picker (type: datetimerange/daterange)
    isDatePickerDateRange () {
      const isDatePicker = this.itemOptions.element === 'el-date-picker'
      const isDateRange = !this.itemOptions.type ||
        this.itemOptions.type === 'datetimerange' ||
        this.itemOptions.type === 'daterange'
      return isDatePicker && isDateRange
    },
    // el-date-picker (type: monthrange)
    isDatePickerMonthRange () {
      const isDatePicker = this.itemOptions.element === 'el-date-picker'
      const isMonthRange = this.itemOptions.type === 'monthrange'
      return isDatePicker && isMonthRange
    },
    //  el-date-picker (type: other)
    isDatePickerOthers () {
      const isDatePicker = this.itemOptions.element === 'el-date-picker'
      return isDatePicker && !this.isDatePickerDateRange && !this.isDatePickerMonthRange
    },
    // el-cascader
    isCascader () {
      return this.itemOptions.element === 'el-cascader'
    }
  },

  created () {},

  methods: {},

  components: {}
}
</script>

<style lang='less' scoped>

</style>

(3)依赖引入的一些函数方法 tools.js

/**
 * 创建唯一的字符串
 * @return {string} ojgdvbvaua40
 */
function createUniqueString () {
  const timestamp = +new Date() + ''
  const randomNum = parseInt((1 + Math.random()) * 65536) + ''
  return (+(randomNum + timestamp)).toString(32)
}

// elementui日期时间范围 快捷选项
const pickerOptionsRange = {
  shortcuts: [
    {
      text: '今天',
      onClick (picker) {
        const end = new Date()
        const start = new Date(new Date().toDateString())
        start.setTime(start.getTime())
        picker.$emit('pick', [start, end])
      }
    }, {
      text: '最近一周',
      onClick (picker) {
        const end = new Date()
        const start = new Date()
        start.setTime(end.getTime() - 3600 * 1000 * 24 * 7)
        picker.$emit('pick', [start, end])
      }
    }, {
      text: '最近一个月',
      onClick (picker) {
        const end = new Date()
        const start = new Date()
        start.setTime(start.getTime() - 3600 * 1000 * 24 * 30)
        picker.$emit('pick', [start, end])
      }
    }, {
      text: '最近三个月',
      onClick (picker) {
        const end = new Date()
        const start = new Date()
        start.setTime(start.getTime() - 3600 * 1000 * 24 * 90)
        picker.$emit('pick', [start, end])
      }
    }
  ]
}

// elementui月份范围 快捷选项
const pickerOptionsRangeMonth = {
  shortcuts: [
    {
      text: '今年至今',
      onClick (picker) {
        const end = new Date()
        const start = new Date(new Date().getFullYear(), 0)
        picker.$emit('pick', [start, end])
      }
    },
    {
      text: '最近半年',
      onClick (picker) {
        const end = new Date()
        const start = new Date()
        start.setMonth(start.getMonth() - 6)
        picker.$emit('pick', [start, end])
      }
    },
    {
      text: '最近一年',
      onClick (picker) {
        const end = new Date()
        const start = new Date()
        start.setMonth(start.getMonth() - 12)
        picker.$emit('pick', [start, end])
      }
    }
  ]
}

(4)一些elmentui全局样式的修改

// el-input-number (controls-position="right")
.el-input-number.is-controls-right {
  .el-input-number__decrease {
    display: none;
  }
  .el-input-number__increase {
    display: none;
    top: 2px; // fix style bug
  }
  &:hover {
    .el-input-number__decrease {
      display: inline-block;
    }
    .el-input-number__increase {
      display: inline-block;
    }
  }
  .el-input__inner {
    text-align: left;
    padding-left: 5px;
    padding-right: 40px;
  }
}

// el-date-picker datetimerange
.el-date-editor.el-date-editor--datetimerange {
  .el-range-separator {
    width: 24px;
    color: #999;
    padding: 0;
  }
  .el-range__icon {
    margin-left: 0;
  }
  &.el-input__inner {
    vertical-align: middle;
    padding: 3px 5px;
  }
  &.el-range-editor--medium {
    width: 380px;

    .el-range-separator {
      line-height: 30px;
    }
  }
  &.el-range-editor--mini {
    width: 330px;

    .el-range-separator {
      line-height: 22px;
    }
  }
}

// el-date-picker  not datetimerange
.el-date-editor {
  .el-input__prefix {
    left: 0;
    top: 1px;
  }
  .el-input__suffix {
    right: 0;
    top: 1px;
  }
  .el-input__inner {
    padding: 0 25px;
  }
  &.el-input--mini {
    width: 175px;
  }
  &.el-input--medium {
    width: 195px;
  }
}

// input padding
.el-input__inner {
  padding: 0 5px;
}

9、vue3兼容指南

https://blog.csdn.net/u010059669/article/details/111269498

Logo

快速构建 Web 应用程序

更多推荐