my-select-field/src/SelectField.vue

<template>
  <el-popover 
    v-bind="{
      ...popPropsProxy,
      trigger: popoverType
    }" 
    v-on="{...popoverListener}"
    v-model="popVisible">
    <slot name="field" slot="reference" :selItems="selItems">
      <my-tag-input class="my-select-field" v-bind="$attrs" :allow-create="false" v-model="selItemNames" @click.native="openPicker" @remove="selRemove"></my-tag-input>
    </slot> 
    <div class="picker-warp" v-if="type==='popover' && popVisible" :style="{'height': `${popPropsProxy.height || 300}px`}">
      <slot name="picker"></slot>
    </div>
    <div style="text-align:right" v-if="popPropsProxy.footer" >
      <el-button type="primary" size="small" @click="confirmHandle">确定</el-button>
      <el-button type="warning" size="small"  @click="cancelHandle">取消</el-button>  
    </div> 
    <my-dialog  :visible.sync="dialogVisible" v-if="type==='dialog'" v-bind="{...dialogPropsProxy}" @submit="confirmHandle" @cancel="cancelHandle"  v-on="$listeners"> 
       <slot name="picker"></slot>
    </my-dialog>
  </el-popover> 
</template>
<script>
/**
 * 自定义多选框
 * 组件继承与my-tag-input 组件
 * @module $ui/components/my-select-field
 */ 
import {isEqual} from '$ui/utils/util'
const DefaultDialogProps = {
  target: 'body', 
  title: '选择', 
  width: '700px',
  height: '580px', 
  footer: true,
  modal: true
}
const DefaultPopProps = {
  placement: 'bottom-start',
  title: '选择',
  width: 480,
  height: 300,
  footer: true 
}
/**
 * 插槽
 * @member slots
 * @property {string} field 表单区域(选中内容显示)插槽
 * @property {string} picker 弹窗、popover 选择器插槽
 */
export default {
  name: 'MySelectField',
  mixins: [],
  components: {},
  /**
   * 属性参数
   * @member props
   * @property {array} [value] 实现双向绑定v-model
   * @property {string} [type] 选择器打开方式,dialog / popover, 默认popover
   * @property {object} [fieldPropsMap] 表单中显示内容的字段映射,通常自定义选中选择的数据为对象格式,需要选择对象中某个字段作为 tag-input 中的标签显示字段
   * @property {string} [fieldPropsMap.label] 确定tag-input标签的显示字段
   * @property {string} [fieldPropsMap.id] 确定选择数据的唯一标识
   * @property {object} [dialogProps] dialog弹窗的配置项(继承my-dialog的props)
   * @property {object} [popProps] popover 的配置项(继承el-popover的props)
   * @property {number} [popProps.height] popover 的特殊配置项(el-popover没有的),控制popover高度 
   * @property {boolean} [popProps.footer] popover 的特殊配置项(el-popover没有的),控制popover是否显示确认、取消按钮
   * @property {string} [size] 尺寸,可选值'medium', 'small', 'mini', ''(my-tag-input参数)
   * @property {boolean} [disabled] 禁用(my-tag-input参数)
   * @property {boolean} [readonly] 只读(my-tag-input参数)
   * @property {boolean} [collapseTags] 折叠标签(my-tag-input参数)
   * @property {string} [placeholder] 输入框占位文本(my-tag-input参数)
   * @property {boolean} [closable=true] 允许删除标签(my-tag-input参数) 
   * @property {string} [icon=el-icon-price-tag] 输入框后缀的图标样式 (my-tag-input参数)
  */
  props: {
    value: {
      type: Array,
      default: () => { return [] }
    },
    type: {
      type: String,
      default: 'popover',
      validator: function(t) {
        return ['dialog', 'popover'].includes(t)
      }
    },
    fieldPropsMap: {
      type: Object,
      default: () => {
        return {
          label: 'name',
          id: 'id'
        }
      }
    },
    dialogProps: {
      type: Object,
      default: () => {
        return {}
      }
    },
    popProps: {
      type: Object,
      default: () => {
        return {}
      }
    }
  },
  data() {
    return {
      selItemNames: [],
      selItems: [],

      popVisible: false,
      dialogVisible: false,
      dialogPropsProxy: {
        ...DefaultDialogProps,
        ...this.dialogProps
      },
      popPropsProxy: {
        ...DefaultPopProps,
        ...this.popProps
      }
      
    }
  },
  computed: {
    popoverType() {
      return this.type === 'popover' ? this.popPropsProxy.footer ? 'manual' : 'click' : 'manual' 
    },
    popoverListener() {
      const listeners = {...this.$listeners}
      delete listeners.input
      return listeners
    }
  },
  watch: {
    value: {
      immediate: false,
      handler(val) { 
        if (!isEqual(val, this.selItems)) {
          this.selItems = [...val] 
        }
      }
    },
    selItems(val) {
      this.selItemNames = val.map((item) => {
        return item[this.fieldPropsMap.label]
      }) 
      this.$emit('change', val)
      this.$emit('input', val) 
      
    }
  },
  methods: {
    /**
     * 打开选择器
     * @method openPicker 
     */
    openPicker() { 
      
      if (this.type === 'dialog') {
        this.dialogVisible = true
      }
      if (this.popoverType === 'manual' && this.type === 'popover') {
        this.popVisible = true
      } 
      
    }, 
    /**
     * 关闭选择器
     * @method closePicker 
     */
    closePicker() {
      this.dialogVisible = false 
      // if (this.popoverType === 'manual') {
        this.popVisible = false
      // } 
    }, 
    selRemove(index, label) {
      const item = this.selItems.splice(index, 1) 
      this.$emit('on-remove', item[0], index)
    },
    /**
     * 去重函数(将一个数组添加到另一个数组里,需要将已有的数据去重)
     * @method _removeDuplicate
     * @param {array} [data] 需要排重的数据
     * @param {array} [targets] 去重数据的放置目标
     * @param {string} [key] 排重的标识字段
     */
    _removeDuplicate(data, targets, key) {
      return data.filter((item) => {
        const Key = item[key] // 唯一表示
        const targetIndex = targets.findIndex((node) => {
          return node[key] === Key
        })
        return targetIndex < 0
      })
    },
    confirmHandle() {
      /**
       * 点击确认事件
       * @event on-confirm 
       */
      this.$emit('on-confirm') 
    },
    cancelHandle() {
      /**
       * 点击取消事件
       * @event on-cancel
       */
      this.$emit('on-cancel')
      this.closePicker()
    }
  },
  created() { 
  }
}
</script>