前言
相信使用过elementui的同学肯定也使用过Select选择器组件,同时也肯定使用过其远程搜索模式。在开启远程搜索后,可以根据输入的关键字去后端获取列表,然后进行选择。本文说的下拉组件其实也是elementui的Select选择器组件,不过会对其进一步封装,以方便业务系统使用。
下拉组件设计
我们先简单分析一下下拉组件应该具备的功能:
先分析一下使用场景
使用到下拉组件的情况一般是需要选择的数据来源于另一个接口,比如添加用户的时候,需要选择角色。此时数据源就来自角色列表接口。这里简单讲一下可能存在的情况:
- 选择单条记录
- 修改模式下选择单条记录
- 选择多条记录
- 修改模式下选择多条记录
修改模式的场景算是一种比较特殊的场景,需要进行如下处理:
- 已选择的记录需要在当前页中能看到;
- 如是多条记录,可对当前选中的记录进行增/减
接口说明
为了尽可能的复用接口,这里依然使用的是后端默认查询的列表接口如:sys/role/list
、sys/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)
}
}
效果图
小结
本文的下拉组件还是很粗浅的封装,目前的交互也是根据自己的操作习惯去做的,除了该种做法外,还有一种做法是弹出框的方式,后续做到下拉菜单树的时候会考虑使用该种方式。
项目源码地址
- 后端
- 前端