打造一款适合自己的快速开发框架-前端篇之下拉组件设计与实现

2,053 阅读6分钟

前言

相信使用过elementui的同学肯定也使用过Select选择器组件,同时也肯定使用过其远程搜索模式。在开启远程搜索后,可以根据输入的关键字去后端获取列表,然后进行选择。本文说的下拉组件其实也是elementui的Select选择器组件,不过会对其进一步封装,以方便业务系统使用。

下拉组件设计

我们先简单分析一下下拉组件应该具备的功能:

先分析一下使用场景

使用到下拉组件的情况一般是需要选择的数据来源于另一个接口,比如添加用户的时候,需要选择角色。此时数据源就来自角色列表接口。这里简单讲一下可能存在的情况:

  1. 选择单条记录
  2. 修改模式下选择单条记录
  3. 选择多条记录
  4. 修改模式下选择多条记录

修改模式的场景算是一种比较特殊的场景,需要进行如下处理:

  1. 已选择的记录需要在当前页中能看到;
  2. 如是多条记录,可对当前选中的记录进行增/减

接口说明

为了尽可能的复用接口,这里依然使用的是后端默认查询的列表接口如:sys/role/listsys/user/user等。下面以用户列表为例:

请求地址

{{api_base_url}}/sys/user/user

数据类型

application/json

请求示例:

选择单条记录

{
    "m_LIKE_userName",""
}
====>由src/utils.request.js全局请求数据处理进行转换
{
	"whereParams": [{
		"operateType": "LIKE",
		"propertyName": "userName",
		"propertyValue": ""
	}]
}

修改模式下选择单条记录,每次查询都要返回当前id的记录

{
    "m_LIKE_userName","",
    "mor_EQ_id": 1
}
====>由src/utils.request.js全局请求数据处理进行转换
{
	"whereParams": [{
		"operateType": "LIKE",
		"propertyName": "userName",
		"propertyValue": ""
	}, {
		"operateType": "OR",
		"propertyName": "id",
		"propertyValue": {
			"operateType": "EQ",
			"propertyName": "id",
			"propertyValue": 1
		}
	}]
}

选择多条记录

{
    "m_LIKE_userName",""
}
====>由src/utils.request.js全局请求数据处理进行转换
{
	"whereParams": [{
		"operateType": "LIKE",
		"propertyName": "userName",
		"propertyValue": ""
	}]
}

修改模式下选择多条记录,每次查询都要返回当前id的记录

{
    "m_LIKE_userName","",
    "mor_EQ_id": [1, 2]
}
====>由src/utils.request.js全局请求数据处理进行转换
{
	"whereParams": [{
		"operateType": "LIKE",
		"propertyName": "userName",
		"propertyValue": ""
	}, {
		"operateType": "OR",
		"propertyName": "id",
		"propertyValue": {
			"operateType": "EQ",
			"propertyName": "id",
			"propertyValue": [1,2]
		}
	}]
}

响应示例:

{
	"code": 0,
	"msg": "查询用户成功",
	"data": {
		"pageNum": 1,
		"pageSize": 15,
		"recordCount": 4,
		"totalPage": 1,
		"rows": [{
			"id": 1,
			"userName": "admin",
			"realName": "mldong",
			"avatar": "",
			"email": "",
			"mobilePhone": "18676163666",
			"telephone": "",
			"password": "52618c88aa68c63d37e50d6acd8b8456",
			"salt": "v7hc7v69",
			"sex": 1,
			"isLocked": 1,
			"createTime": "2020-06-09 21:47:33",
			"updateTime": "2020-06-24 10:11:03",
			"isDeleted": 1
		}, {
			"id": 9,
			"userName": "ni",
			"realName": "大的",
			"email": "8551312@163.com",
			"mobilePhone": "13023123256",
			"password": "c079872794690156289617a60a11c316",
			"salt": "ztih3jzc",
			"sex": 2,
			"isLocked": 1,
			"createTime": "2020-06-22 10:01:12",
			"updateTime": "2020-06-22 10:04:46",
			"isDeleted": 1
		}, {
			"id": 10,
			"userName": "地方v的",
			"realName": "hghg",
			"email": "13696452586@163.com",
			"mobilePhone": "13645607895",
			"password": "d9acbc2eef540ffc8ea6dd8fdacd37cc",
			"salt": "jpqocubh",
			"sex": 2,
			"isLocked": 1,
			"createTime": "2020-06-22 10:06:41",
			"updateTime": "2020-06-22 10:06:41",
			"isDeleted": 1
		}, {
			"id": 11,
			"userName": "孙狗",
			"realName": "孙笑川",
			"mobilePhone": "17444444444",
			"password": "7941882c9fdcd4bb2a17b804d2b5c5ba",
			"salt": "ewst066f",
			"sex": 1,
			"isLocked": 1,
			"createTime": "2020-06-22 15:03:06",
			"updateTime": "2020-06-22 15:03:06",
			"isDeleted": 1
		}]
	}
}

组件参数说明

暂时定几个常用的参数,后续可能还会有追加

参数名 类型 默认值 说明
valueKey String id 列表中选项的值对应的key
labelKey String name 列表中选项的值对应的key
searchKey String name 模糊搜索的key
url String undefined 接口地址
isEdit Boolean false 是否编辑模式
value String, Number, Array undefined 绑定的值
multiple Boolean false 是否多选
size String medium 组件大小medium/small/mini
placeholder String 请选择 占位符

开始编码

目录结构

├── src
	├──	components/m
		├──	Select
			└── index.vue
	├── utils
		└── request.js
	├── views
		├──	dashboard
			└── index.vue
	└── main.js

文件详解

  • src/components/m/Select/index.vue

下拉组件

<template>
  <div class="m-select">
    <!--el-input readonly :size="size" :placeholder="placeholder" v-model="mValue">
      <el-button slot="append" icon="el-icon-search"></el-button>
    </el-input-->
    <el-select
      :size="size"
      filterable
      :multiple="multiple"
      remote
      :loading="loading"
      :remote-method="requestData"
      :placeholder="placeholder"
      @change="handleChange"
      v-model="mValue">
      <el-option
        v-for="item in options"
        :key="item[valueKey]"
        :label="item[labelKey]"
        :value="item[valueKey]">
        <slot v-bind:option="item"> </slot>
      </el-option>
    </el-select>
  </div>
</template>
<script>
import request from '@/utils/request'
export default {
  name: 'MSelect',
  props: {
    valueKey: { // 列表中选项的值对应的key
      type: String,
      default: 'id'
    },
    labelKey: { // 列表中选项的标签对应的key
      type: String,
      default: 'name'
    },
    searchKey: { // 模糊搜索的key
      type: String,
      default: 'name'
    },
    url: { // 接口地址
      type: String,
      default: undefined
    },
    isEdit: { // 是否编辑模式
      type: Boolean,
      default: false
    },
    // 绑定的值
    value: {
      type: [String, Number, Array],
      default: undefined
    },
    multiple: { // 是否多选
      type: Boolean,
      default: false
    },
    size: { // medium/small/mini
      type: String,
      default: 'medium'
    },
    placeholder: { //  占位符
      type: String,
      default: '请选择'
    }
  },
  data() {
    return {
      loading: false,
      mValue: undefined,
      options: []
    }
  },
  watch: {
    value(n, o) { // 监听父组件值变动,子组件也要变动
      this.mValue = n
      if ((o === undefined && this.isEdit) || (o !== undefined && o.length === 0 && this.isEdit)) {
        // 如果旧的值等于undefined
        this.requestData('')
      }
    }
  },
  created() {
    if (!this.isEdit) {
      this.requestData('')
    }
  },
  methods: {
    // 请求数据
    requestData(k) {
      if (this.url) {
        this.loading = true
        var operateType = this.multiple ? 'IN' : 'EQ'
        request({
          url: this.url,
          method: 'post',
          data: {
            ['m_LIKE_' + this.searchKey]: k,
            [`mor_${operateType}_` + this.valueKey]: this.value
          }
        }).then(res => {
          this.loading = false
          if (res.code === 0) {
            this.options = res.data.rows
          }
        }).catch(() => {
          this.loading = false
        })
      }
    },
    // 子组件值变化要通过父组件
    handleChange(value) {
      this.$emit('input', value)
    }
  }
}
</script>

  • src/utils/request.js

新增or的全局处理,代码片段

// request interceptor
service.interceptors.request.use(
  config => {
    // do something before request is sent

    if (store.getters.token) {
      // 存在token,就放到请求头中
      // 这里修改一下请求头与后端一致,X-Token->Auth-Token
      config.headers['Auth-Token'] = getToken()
    }
    if (config.data) {
      // 这里对全局的请求参数做处理,主要是拼接查询条件
      var whereParams = []
      Object.keys(config.data).forEach(item => {
        if (item.startsWith('m_')) {
          var value = config.data[item]
          if (value !== undefined) {
            var arr = item.split('_')
            if (arr.length === 3) {
              whereParams.push({
                operateType: arr[1],
                propertyName: arr[2],
                propertyValue: value
              })
            }
          }
          delete config.data[item]
        } else if (item.startsWith('mor_')) {
          // 处理简单的or语句
          value = config.data[item]
          arr = item.split('_')
          if (value !== undefined) {
            if (arr.length === 3) {
              whereParams.push({
                operateType: 'OR',
                propertyName: arr[2],
                propertyValue: {
                  operateType: arr[1],
                  propertyName: arr[2],
                  propertyValue: value
                }
              })
            }
          }
          delete config.data[item]
        }
      })
      if (whereParams.length) {
        config.data.whereParams = whereParams
      }
    }

    return config
  },
  error => {
    // do something with request error
    console.log(error) // for debug
    return Promise.reject(error)
  }
)
  • src/main.js

主入口全局注册自定义组件,这里也用了require.context,代码片段

import Vue from 'vue'

// 处理自定义组件全局注册
const files = require.context('./components/m', true, /\.vue$/)
files.keys().forEach((routerPath) => {
  const componentName = routerPath.replace(/^\.\/(.*)\/index\.\w+$/, '$1')
  const value = files(routerPath)
  Vue.component('m-' + componentName.toLowerCase(), value.default)
}, {})
  • src/views/dashboard/index.vue

这里提供了使用样例:

选择单个

<m-select v-model="userId" url="sys/user/list" value-key="id" label-key="userName" search-key="userName"></m-select>

选择单个-修改模式

<m-select is-edit v-model="form.userId" url="sys/user/list" value-key="id" label-key="userName" search-key="userName"></m-select>

选择多个

<m-select multiple v-model="userIds" url="sys/user/list" value-key="id" label-key="userName" search-key="userName"></m-select>

选择多个-修改模式

<m-select multiple is-edit v-model="form.userIds" url="sys/user/list" value-key="id" label-key="userName" search-key="userName"></m-select>

自定义布局

<m-select v-model="userId" url="sys/user/list" value-key="id" label-key="userName" search-key="userName">
    <template v-slot:default="{ option }">
		<span>{{ option.id }}--{{ option.userName }}</span>
    </template>
</m-select>

js片段


export default {
  name: 'Dashboard',
  data() {
    return {
      form: {
        userId: undefined,
        userIds: []
      },
      userId: undefined,
      userIds: []
    }
  },
  created() {
    // 模拟修改异步更新
    setTimeout(() => {
      this.$set(this.form, 'userId', 1)
      this.$set(this.form, 'userIds', [1, 9])
    }, 2000)
  }
}

效果图

小结

本文的下拉组件还是很粗浅的封装,目前的交互也是根据自己的操作习惯去做的,除了该种做法外,还有一种做法是弹出框的方式,后续做到下拉菜单树的时候会考虑使用该种方式。

项目源码地址

  • 后端

gitee.com/mldong/mldo…

  • 前端

gitee.com/mldong/mldo…

相关文章

打造一款适合自己的快速开发框架-先导篇

打造一款适合自己的快速开发框架-前端脚手架搭建

打造一款适合自己的快速开发框架-前端篇之登录与路由模块化

打造一款适合自己的快速开发框架-前端篇之框架分层及CURD样例

打造一款适合自己的快速开发框架-前端篇之字典组件设计与实现