s-select封装

680 阅读1分钟

基于a-select封装的通用选择器,其中的便捷新增和高级搜索属于另外封装的组件,自行清除相关代码可以正常使用

由于本系统基于页面配置从而实现表格、表单动态渲染,集成了很多的功能和交互,删除这部分代码就可以直接使用

比如pageId rowName returnName 便捷获取title之类的可以统统删掉

单选模式/多选模式 绑定的数据都是String类型,逗号隔开(a-select的多选是数组,这个自行修改回去)

image.png

image.png

image.png

<template>
  <div class="business-selector">
    <!-- style是为了控制高级选择器按钮 删掉 -->
    <!-- $txt('')是多语言,改成中文即可 -->
    <!-- url是个对象包含list add(用于新增) -->
    <a-select
      :value="value"
      :mode="mode"
      allow-clear
      show-search
      :placeholder="$txt('please_select')"
      :dropdown-match-select-width="false"
      :filter-option="url.list ? false : filterOption"
      :style="{'width': proSelect && url.list && pageId ? 'calc(100% - 40px)' : '100%'}"
      :disabled="disabled"
      :loading="loading"
      @dropdownVisibleChange="dropdownVisibleChange"
      @popupScroll="handlePopupScroll"
      @search="handleSearch"
      @change="val => {
        returnSelectedRows({ selectedRowKeys: Array.isArray(val) ? val : [val] })
      }"
    >
      <a-select-option
        v-for="item in dataSource"
        :key="item[rowKey]"
        :value="item[rowKey]"
        :disabled="optionDisabled(item)"
      >
        {{ item[rowName] }}
      </a-select-option>

      <a-select-option
        v-if="loading"
        disabled
        value="disabled-option"
      >
        <a-spin :indicator="indicator" />
      </a-select-option>

      <template
        slot="dropdownRender"
        slot-scope="menu"
      >
        <v-nodes :vnodes="menu" />

        <!-- 新增功能:1、必须有 url 2、开启该功能 3、控件启用 -->
        <template v-if="url.add && created && !disabled">
          <a-divider style="margin: 4px 0;" />
          <a-button
            type="link"
            block
            @click="handleCreate({}, 'create')"
          >
            <a-icon type="plus" /> {{ $txt('create') }}
          </a-button>
        </template>
      </template>
    </a-select>

    <!-- 高级搜索:1、必须有url 2、开启了该功能 3、pageId表格配置 4、控件启用 -->
    <a-button
      v-if="proSelect && url.list && pageId"
      icon="search"
      class="search-icon"
      :disabled="disabled"
      @click="openPop"
    />

    <s-modal-create
      ref="Create"
      :url="url"
      :config="config.formConfig || []"
      @refresh="refresh"
    />

    <s-pop-select
      ref="Pop"
      :title="title"
      :url="url"
      :row-key="rowKey"
      :config="config"
      :params="params"
      :option-disabled="optionDisabled"
      :mode="mode === 'default' ? 'radio' : 'checkbox'"
      @returnSelectedRows="returnSelectedRows"
    />
  </div>
</template>

<script>
import PageConfig from '@/utils/pageConfig'
import http from '@/api'
import T from '@/utils/tools'

// 基本覆盖所有需求
// 高级下拉选择器 - 以下功能都有 demo 参考项目 OMS 搜索关键字即可
// 功能:
// 1、数据源: 接口url.list(远程搜索,必须开启分页,否则自行拉取数据传入list) 或者 Array数组list(本地过滤)
// 2、可开启高级功能 A、新增 B、弹窗搜索
// 3、提供选中的数组 selectedRows 方便开发获取 rowKey rowName 以外的数据,事件 returnSelectedRows 接收
// 4、传入的 dataIndex 将会获取 rowKey 的值, returnName 将会获取 rowName 的值 (仅支持一个值)
//    如果要获取多个,请利用第3个功能点自行处理
// 5、提供选项禁用功能 optionDisabled 是个函数
// 6、提供参数过滤功能 param,仅在数据源为 url 时有效,list自行在传入前过滤
// 7.提供滚动加载,滚动加载支持多选

export default {
  name: 'SSelect',

  components: {
    VNodes: {
      functional: true,
      render: (h, ctx) => ctx.props.vnodes,
    },
  },

  props: {
    // 数据源的两种方式
    // 1、传接口 url: { list: 'xxx', add: 'xxx' }
    // 2、传数组 list
    url: {
      type: Object,
      default: () => ({})
    },
    list: {
      type: Array,
      default: () => []
    },
    rowKey: {
      type: String,
      required: true,
    },
    rowName: {
      type: String,
      required: true,
    },
    // 数据 key
    dataIndex: {
      type: String,
      required: true
    },
    // 数据对象(双向)
    record: {
      type: Object,
      required: true
    },
    // 页面配置 id 用于 create proSelect
    pageId: {
      type: String,
      default: ''
    },
    // 外部传入的过滤参数
    params: {
      type: Object,
      default: () => ({})
    },
    // 是否需要新增功能
    created: {
      type: Boolean,
      default: true
    },
    // 选择器需要返回的名称
    returnName: {
      type: String,
      default: ''
    },
    // 是否需要高级搜索
    proSelect: {
      type: Boolean,
      default: true
    },
    // 是否禁用
    disabled: {
      type: Boolean,
      default: false
    },
    // 单选 、 多选
    mode: {
      type: String,
      default: 'default',
      validator: (val) => {
        return ['default', 'multiple'].includes(val)
      }
    },
    // 控制 options 是否禁用 一般用于防止重复勾选
    optionDisabled: {
      type: Function,
      default: () => {
        return false
      }
    },
    // 是否自动选择(点击下拉进行查询数据,如果只有一条并且未选择时,自动勾选)
    autoSelect: {
      type: Boolean,
      default: true
    },
    title: {
      type: String,
      default: '请选择'
    }
  },

  data () {
    return {
      indicator: <a-icon type="loading" style="font-size: 24px" spin />,
      pageNo: 1,
      dataSource: [],
      config: {},
      loading: false,
      query: '',
      result: {
        pages: 1,
        current: 1,
        total: 1,
      },
      isInit: true, // 是否属于初始化
      open: false,
    }
  },

  computed: {
    value () {
      const value = this.record[this.dataIndex] || ''
      if (this.mode === 'default') return value || undefined // 单选模式的数据 undefined
      return value ? value.split(',') : [] // 多选模式的数据类型为 Array
    },
    // 是否开启滚动加载 true 一次 10条 false 全量加载 1000条 (这里就要求接口必须按规定返回,否则无法判断是否到底了)
    // 之前是放到 prop 可选是否开启的 (a-select自己限制了最大数据量,不得不强制开启,同时必须开启远程搜索)
    scrollLoad () {
      return !!this.url.list
    },
  },

  watch: {
    // 本组件采用 watch 代替 handleChange (双向处理联动)
    // 外部因为表单联动清空 record[dataindex] 也就是 value 时,该组件也会自动触发 勾选 handle 函数,完成下一级联动
    value: {
      handler (newVal, oldVal) {
        if (!this.isInit && newVal !== undefined) {
          this.handleChange(newVal)
        }

        // 如果已经是展开状态,不需要拉取数据
        // 详情页进来回显 || 表单联动带入的值不在datasource里  都需要主动查一遍
        if (this.url.list && this.open === false &&
        (this.isInit || !this.dataSource.some(item => item[this.rowKey] === newVal))) {
          this.refresh({ [this.rowKey]: this.scrollLoad ? this.record[this.dataIndex] : undefined })
        }
        this.isInit = false
      },
      immediate: true
    },
    // 使用外部数据源
    list: {
      handler () {
        this.dataSource = this.list
      },
      immediate: true
    },

    // 当外部过滤参数变化时主动进行刷新
    params: {
      handler (newVal, oldVal) {
        if (!this.isInit && JSON.stringify(newVal) !== JSON.stringify(oldVal)) {
          this.refresh()
        }
      },
      deep: true
    }
  },

  mounted () {
    this.$set(this.record, this.dataIndex, this.record[this.dataIndex])
    this.returnName && this.$set(this.record, this.returnName, this.record[this.returnName])
    // 如果有 url 则调用接口获取数据
    this.pageId && PageConfig.getConfig(this.pageId).then(res => {
      this.config = res
    })
  },

  methods: {
    async loadData (query = {}) {
      if (!this.url.list) return false
      const params = {
        ...query,
        ...this.params,
        pageNo: this.pageNo,
        pageSize: this.scrollLoad ? 10 : 1000,
      }
      this.loading = true
      const res = await http.get(this.url.list, { params })
      this.result = res.result
      this.dataSource = this.dataSource.concat(res.result.records || res.result || [])
      this.loading = false
    },

    // 本地搜索(模糊搜索)
    filterOption (input, option) {
      return (
        option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
      )
    },

    // 远程搜索(模糊搜索)
    handleSearch: T.throttle(function (input) {
      this.query = input
      this.dataSource = []
      this.pageNo = 1
      const param = {
        [this.rowName]: this.query ? `*${this.query}*` : this.query
      }
      this.loadData(param)
    }, 800, { trailing: true }),

    // 滚动加载
    handlePopupScroll (data) {
      if (!this.result.pages) return false // 没有页数说明不支持分页
      const { scrollHeight, scrollTop, clientHeight } = data.target
      if (scrollHeight - scrollTop < clientHeight + 1 && this.loading === false &&
      (this.pageNo < this.result.pages || this.dataSource.length < this.result.total)) { // 兼容没返回pages的接口,用数量判断
        this.pageNo++
        this.loadData()
      }
    },

    // 高级控件的新增
    handleCreate () {
      this.$refs.Create.handleShow({}, 'create')
    },

    // 展开时查询数据
    dropdownVisibleChange (open) {
      this.open = open
      if (this.scrollLoad && open) {
        this.refresh()
      }
    },

    openPop () {
      const selectedRowKeys = this.mode === 'default' ? [this.value] : this.value
      const keys = this.record[this.dataIndex]?.split(',') || []
      const names = this.record[this.returnName]?.split(',') || []
      const selectedRows = keys.map((item, index) => {
        return {
          [this.rowKey]: keys[index],
          [this.rowName || 'none']: names[index], // 没有returnName 随便占个位置
        }
      })
      this.$refs.Pop.handleShow(this.value ? selectedRowKeys : [], selectedRows) // 必须为数组
    },

    handleChange (val = []) {
      const selectedRowKeys = Array.isArray(val) ? val : val.split(',') // 处理 单选 多选数据
      this.returnSelectedRows({ selectedRowKeys })
    },

    // 目前不支持 value 为 number 类型
    async returnSelectedRows ({ selectedRowKeys, selectedRows = [] }) {
      if (this.loading === true) return false // 也就回显的时候 loading 会为 true, dataSource = [],会错误的清空数据

      const tar = this.dataSource.filter(item => selectedRowKeys.includes(item[this.rowKey]))
      const arr = tar.length ? tar : selectedRows

      this.record[this.dataIndex] = selectedRowKeys.join(',') // 拼接 key

      if (this.returnName) {
        if (selectedRows.length) {
          await this.refresh({ [this.rowKey]: this.scrollLoad ? this.record[this.dataIndex] : undefined })
          this.record[this.returnName] = this.dataSource.map(item => item[this.rowName]).join(',') // 拼接 Name
        } else {
          this.record[this.returnName] = arr.map(item => item[this.rowName]).join(',') // 拼接 Name
        }
      }

      this.$emit('returnSelectedRows', arr, this.record) // 抛出当前选中的 row record 丢出去 表格表单时可以直接给record赋值,不用烦恼到底是哪一行了
    },

    // 刷新数据
    async refresh (query) {
      this.pageNo = 1
      this.dataSource = []
      await this.loadData(query)
    },

    // 自动勾选 todo
    handleAutoSelect () {
      // 开启功能 && 只有一条数据 && 未勾选数据 && 没有id(非回显)  情况太多的建议弃用该功能
      if (this.autoSelect && this.dataSource.length === 1 &&
      this.record[this.dataIndex] && !this.record.id) {
        this.handleChange(this.dataSource[0][this.rowKey]) // 自动勾选
      }
    }
  }
}
</script>