要求:
- 使用element-ui
- 实现接口缓存-使用map实现
- 实现自定义label和值,label可以自己设置或者按照code-name方式展示-自定义展示字段或者根据传入的参数格式化label
- 支持自己添加下拉项-在查询结果上concat一下自定义的下拉项
- 支持设置默认值-v-model绑定
- 支持自定义参数查询
- 2024年4月7日更新-增加是否缓存参数
思考:
- 怎么写一个最近最久未使用算法来比较合理的删除已缓存元素,缓存大小需要有个设置,不能无限制增大,影响内存
- 缓存key
- 使用v-bind和v-on继承原有组件的属性和事件
实现:
cache.js
class Cache {
constructor(maxSize) {
this.maxSize = maxSize
this.map = new Map()
}
set(key, value) {
const hasKey = this.map.has(key)
this.map.set(key, value)
if (!hasKey) {
this.removeOldestEntry()
}
}
get(key) {
const value = this.map.get(key)
// get时候重新设置时间
if(value){
this.map.set(key, {value:value.value,time:new Date().getTime()})
}
return this.map.get(key)
}
remove(key) {
this.map.delete(key)
}
removeOldestEntry() {
if (this.map.size <= this.maxSize) {
return
}
// 查找到需要删除元素得key-暴力删除每次删除第一个
// const keys = Array.from(this.map.keys())
// if (keys.length) {
// this.remove(keys[0])
// }
const keys = Array.from(this.map.keys())
const values = Array.from(this.map.values()).map(item => item.time)
// 循环每个key,找到最久未使用的key,返回e
const index = findMinIndex(values)
this.remove(keys[index])
}
}
function findMinIndex(arr) {
const min = Math.min(...arr)
for (let i = 0; i < arr.length; i++) {
if (arr[i] === min) {
return i
}
}
}
export default Cache
测试cache,为什么要写一个异步的方法测试?
因为如果设置为同步的,在代码执行的一瞬间相差还不到一秒,查找不到最久未使用的Key
const cache = new Cache(5)
cache.set('a', { value: 1, time: new Date().getTime() })
cache.set('b', { value: 2, time: new Date().getTime() })
cache.set('c', { value: 3, time: new Date().getTime() })
cache.set('d', { value: 4, time: new Date().getTime() })
cache.set('e', { value: 5, time: new Date().getTime() })
console.log(cache);
cache.get('a') // undefined
cache.get('b') // {value:2,time:162071210000}
cache.get('c') // {value:3,time:162071210000}
cache.get('d') // {value:4,time:162071210000}
cache.get('a') // {value:4,time:162071210000}
setTimeout(() => {
cache.set('f', { value: 6, time: new Date().getTime() })
}, 3000);
// cache.set('g', { value: 7, time: new Date().getTime() })
console.log(cache);
组件实现
<template>
<div class="hy-select">
<z-select
ref="refInputSelect"
v-model="inputValue"
style="width:100%"
:remote-method="searchRemote"
v-bind="$attrs"
v-on="$listeners"
@change="handleChange"
>
<z-option
v-for="item in options"
:key="item[fieldType] || item"
:label="showLabel(item)"
:value="item[fieldType] || item"
:disabled="item.disabled"
>
<slot :scope="item"></slot>
</z-option>
</z-select>
</div>
</template>
<script>
import { debounce } from 'lodash'
import Cache from '@/utils/cache.js'
import request from '@/utils/request'
import { HostName } from '@/config/env'
// 增加缓存
const cache = new Cache(100)
export default {
name: 'CommonSelect',
props: {
value: {
type: [String, Number, Array]
},
// 接收接口的所有参数值
param: {
type: Object,
default: () => {}
},
// 搜索api
searchApi: {
type: String,
required: true
},
// 搜索方法
searchMethod: {
type: String,
default: 'post'
},
// 搜索关键词key
searchKey: {
type: String,
default: 'name'
},
// 自定义项
addOptions: {
type: Array,
default: () => []
},
// 默认取值字段-兼容其他下拉
fieldType: {
type: String,
default: 'id'
},
// 展示标签连接符
fieldSplitor: {
type: String,
default: '-'
},
// 展示标签
fieldLabels: {
type: [Array, Function],
default: () => ['code', 'name']
},
// 默认value
defaultValue: {
type: [String, Number, Array],
default: ''
},
// 默认options
defaultOptions: {
type: Array,
default: () => []
},
// 是否缓存结果
isCache: {
type: Boolean,
default: true
}
},
data() {
return {
inputValue: '',
options: []
}
},
watch: {
defaultOptions: {
handler(val) {
this.options = this.addOptions.concat(val)
}
}
},
mounted() {
this.inputValue = this.value
if (this.defaultValue && this.defaultOptions.length) {
this.inputValue = this.defaultValue
this.options = this.addOptions.concat(this.defaultOptions)
}
// 不是远程搜索,先查询一次
if (!this.remote) {
this.getData()
}
},
methods: {
showLabel(item) {
const fields = []
if (typeof this.fieldLabels === 'function') {
return this.fieldLabels(item)
}
this.fieldLabels.forEach(field => {
if (item[field]) {
fields.push(item[field])
}
})
return fields.join(this.fieldSplitor) || item
},
searchRemote: debounce(function(keyword) {
if (!keyword) return
this.getData(keyword)
}, 500),
async getData(query) {
const searchKey = this.searchKey
const params = { ...this.param, [searchKey]: query }
const res = await this.searchRequest(params)
if (res.status) {
this.options = this.addOptions.concat(res.result)
}
},
handleChange(val) {
this.$emit('change', val, this.options)
},
// 搜索方法封装
searchRequest(data) {
// 默认使用name作为关键词参数,使用api地址+参数作为缓存key
const cacheKey = `${this.searchApi}-${this.searchMethod}-${JSON.stringify(data)}`
const cachedValue = cache.get(cacheKey)
// 缓存结果&有缓存结果
if (this.isCache) {
if (cachedValue) {
return new Promise((resolve, reject) => {
resolve(cachedValue.value)
})
} else {
return this.sendRequest(data, cacheKey)
}
} else {
// 不缓存结果,实时请求
return request({
url: `${HostName}${this.searchApi}`,
method: this.searchMethod,
data
})
}
},
// sendRequest
sendRequest(data, cacheKey) {
return request({
url: `${HostName}${this.searchApi}`,
method: this.searchMethod,
data
}).then(res => {
// 链式处理请求的结果,将结果缓存,再返回
cache.set(cacheKey, {value:res,time:new Date().getTime()})
return new Promise((resolve, reject) => {
resolve(res)
})
})
}
}
}
</script>
<style scoped lang="scss"></style>
使用方式:
<common-select
v-model="formData.provinceCode"
:filterable="true"
:multiple="true"
:remote="false"
:searchApi="'/api/provinceQuery'"
style="width: 90%"
fieldType="code"
></common-select>