vue2+element-ui el-select+el-table二次封装底层下拉组件(含自定义表头)

386 阅读2分钟

因业务需求,需要封装底层picker组件,组件需要实时请求接口筛选出最新数据,同时要含有自定义表头,label,id支持自定义。之后扩展业务组件的时候,可以基于这个底层组件再扩展一些自定义的功能,这样就大大减少了业务组件开发效率。

基于Vue2 + TypeScript + ElementUI的下拉组件

一、使用方法

1、举个例子,封装一个产品组件SkuPicker

SkuPicker.vue

<template>
  <div class="sku-picker">
    <select-picker
      v-model="selected"
      :attr-for-label="attrForLabel"
      :attr-for-value="attrForValue"
      :placeholder="placeholder"
      :columns="columns"
      :multiple="multiple"
      :clearable="clearable"
      :query-by-keyword-url="_getKeywordUrl()"
      :query-by-value-url="_getValueUrl()"
      :is-view="isView"
      :size="size"
      :width="width"
      @changeValue="onChange"
      @changeItems="onChangeItems"
      @changeItem="onChangeItem"
      @focus="_focus"
    />
  </div>
</template>
2、使用组件
<sku-picker
  v-model="productId"
  @change="skuChanged"
  :multiple="true"
></sku-picker>

效果

image.png

二、具体底层picker代码

SelectPicker.vue

<template>
  <div :style="{ width: `${width}px` }">
    <el-select
      v-if="!isView"
      ref="SelectPicker"
      v-model="selectVal"
      :size="size"
      :style="{ width: `${width}px` }"
      :popper-append-to-body="popperAppendToBody"
      :multiple="multiple"
      :filterable="filterable"
      :placeholder="placeholder"
      :value-key="attrForValue"
      :multiple-limit="multipleLimit"
      :filter-method="filterMethod || filterMethodHandle"
      :clearable="clearable"
      @remove-tag="removeTag"
      @focus="_focus"
      @clear="clearAll"
    >
      <template>
        <el-table
          class="table"
          :data.sync="tableData"
          max-height="300"
          :row-class-name="tableRowClassName"
          @row-click="rowClick"
        >
          <template v-for="(item, index) in columns">
            <el-table-column
              :key="index + 'i'"
              :type="item.type"
              :label="item.label"
              :prop="item.prop"
              :min-width="item['min-width'] || item.minWidth || item.width"
              :align="item.align || 'center'"
              :fixed="item.fixed"
              v-bind="{ ...item.bind, ...$attrs }"
              v-on="$listeners"
            >
              <template slot-scope="scope">
                <!-- 作用域插槽 -->
                <template v-if="item.slotName">
                  <slot :name="item.slotName" :scope="scope"></slot>
                </template>
                <div v-if="!item.render && !item.slotName">
                  <span>{{ scope.row[item.prop] }}</span>
                  <el-option
                    v-show="false"
                    :key="labelComputed(scope.row, attrForValue)"
                    :label="labelComputed(scope.row, attrForLabel)"
                    :value="scope.row"
                  ></el-option>
                </div>
              </template>
            </el-table-column>
          </template>
          <slot></slot>
        </el-table>
      </template>
    </el-select>
    <div v-else class="view-text">
      {{ getViewLabels(selectVal) }}
    </div>
  </div>
</template>

组件使用绑定值为对象类型来写,通过value-key作为 value 唯一标识的键名,即外层组件传入attrForValue

因为绑定的是对象类型,这里的el-select里面一定是要嵌套el-option,目的是为了能正确回显,但是我们并不需要el-option来显示下拉数据,所以这里做了个隐藏

1、自定义label显示

用attrForLabel,外层组件可以传个函数,例如:

const attrForLabel = (item: Record<string, string>) => {
  return item ? `${item.category}|${item.designation}` : ''
}
2、关键字筛选

通过外层组件传queryByKeywordUrl来查询最新数据

async filterMethodHandle(query?: string) {
  this.loading = true
  if (!this.filterable) return
  const res = await get(this.queryByKeywordUrl, { keyword: query }, false)
  if (res.data && isArray(res.data)) {
    this.tableData = res.data
  } else {
    this.tableData = []
  }
  this.loading = false
}
3、回显数据

当有值要回显的时候,可以通过接口queryByValueUrl来查询需要回显数据

async queryByValueData(ids: number | string | Array<any>) {
  this.loading = true
  const res = await get(this.queryByValueUrl, { ids }, false)
  if (!res.data && isArray(res.data)) {
    this.tableData = res.data
    if (!res.data || res.data.length <= 0) return
    if (this.multiple) {    // 多选
      const items: Array<Record<string, any>> = []
      this.selectVal = this.tableData.map((e) => {
        items.push(e)
        return e
      })
      this.selectItems = items
    } else { // 单选
      const item = this.tableData[0]
      this.selectVal = item
      this.selectItems = [item]
      this.selectItem = item
    }
    this.emitEvent()
  }
  this.loading = false
}
4、选中时数据处理

判断单选或者多选,把处理好的数据同步更新到外层组件

rowClick(item) {
  if (this.multiple) {// 多选
    // 判断当前选中的数据是否已经选中了
    const flag = this.selectItems.some(
      (e) => e[this.attrForValue] === item[this.attrForValue]
    )
    if (flag) {
      this.close()
      return
    }
    this.selectVal = Array.from(new Set([...this.selectItems, item]))
    this.selectItems = Array.from(new Set([...this.selectItems, item]))
  } else {
    this.selectVal = item
    this.selectItem = item
    this.selectItems = [item]
  }
  this.emitEvent()
  this.close()
}
5、多选时删除移除tag值

el-select组件有个remove-tag事件,我们可以利用remove-tag事件拿到删除的对象,通过外层传入的attrForValue键值进行数据处理

removeTag(tag) {
  if (this.multiple) {
    const rowIndex = this.selectItems.findIndex(
      (item) => item[this.attrForValue] === tag[this.attrForValue]
    )
    this.selectItems.splice(rowIndex, 1)
  } else {
    this.selectItems = []
    this.selectItem = {}
  }
  this.emitEvent()
}
6、处理label或者value显示
labelComputed(
  item: Record<string, any>,
  attrForLabel: string | Function
): string {
  if (util.isEmptyObject(item)) return ''
  if (util.isFunction(attrForLabel)) {
    return attrForLabel && (attrForLabel as Function)(item)
  } else {
    return item[attrForLabel as string]
  }
}
7、统一抛出事件
emitEvent() {
  const ids = this.selectItems.map((e) => e[this.attrForValue])
  this.selectValue = ids.join(',')
  this.$emit(CHANGE_ITEM_EVENT, this.selectItem) // 单选抛出对象
  this.$emit(CHANGE_ITEMS_EVENT, this.selectItems)// 抛出数组
  this.$emit(UPDATE_MODEL_EVENT, this.selectValue)
  this.$emit(INPUT_EVENT, this.selectValue)
}

参考文章:juejin.cn/post/717250…