|
|
|
|
@ -0,0 +1,457 @@
|
|
|
|
|
<template>
|
|
|
|
|
<div class="bm-select">
|
|
|
|
|
<el-popover
|
|
|
|
|
ref="popper"
|
|
|
|
|
trigger="manual"
|
|
|
|
|
placement="bottom-start"
|
|
|
|
|
popper-class="bm-select-popover"
|
|
|
|
|
v-model="expand">
|
|
|
|
|
<el-input
|
|
|
|
|
slot="reference"
|
|
|
|
|
ref="input"
|
|
|
|
|
v-model="inputLabel"
|
|
|
|
|
:disabled="disabled"
|
|
|
|
|
:readonly="readonly"
|
|
|
|
|
:placeholder="placeholder"
|
|
|
|
|
@input="handleInput"
|
|
|
|
|
@blur="handleInputBlur"
|
|
|
|
|
@focus="handleInputFocus"
|
|
|
|
|
@keydown.down.native.prevent="handleNavigate(1)"
|
|
|
|
|
@keydown.up.native.prevent="handleNavigate(-1)"
|
|
|
|
|
@keydown.enter.native.prevent="handleEnter"
|
|
|
|
|
></el-input>
|
|
|
|
|
<div v-if="expand" class="option-content" :style="{ maxHeight: maxHeight + 'px' }">
|
|
|
|
|
<div v-for="(item, index) in searchOptions" :key="idKey ? item[idKey] : item[valueKey]"
|
|
|
|
|
class="option-item"
|
|
|
|
|
:class="{ 'option-item-hover': hoverIndex === index }"
|
|
|
|
|
@mousedown.prevent="handleSelectOption(item, index)"
|
|
|
|
|
>
|
|
|
|
|
<span v-if="showValue">{{ item[valueKey] }}-</span>
|
|
|
|
|
<span class="option-label">{{ item[labelKey] }}</span>
|
|
|
|
|
<span v-if="showSort" class="option-code">{{ item[sortKey] }}</span>
|
|
|
|
|
<span v-if="showCode" class="option-code">{{ item[codeKey] }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</el-popover>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
name: 'BMSelect',
|
|
|
|
|
props: {
|
|
|
|
|
enterIndex: [Number],
|
|
|
|
|
value: [String, Number],
|
|
|
|
|
options: {
|
|
|
|
|
type: Array,
|
|
|
|
|
default: () => []
|
|
|
|
|
},
|
|
|
|
|
// 用于选项的唯一key,避免value相同但实际对象不同的情况
|
|
|
|
|
idKey: [String],
|
|
|
|
|
valueKey: {
|
|
|
|
|
type: String,
|
|
|
|
|
default: 'value'
|
|
|
|
|
},
|
|
|
|
|
labelKey: {
|
|
|
|
|
type: String,
|
|
|
|
|
default: 'label'
|
|
|
|
|
},
|
|
|
|
|
codeKey: {
|
|
|
|
|
type: String,
|
|
|
|
|
default: 'code'
|
|
|
|
|
},
|
|
|
|
|
sortKey: {
|
|
|
|
|
type: String,
|
|
|
|
|
default: 'sort'
|
|
|
|
|
},
|
|
|
|
|
placeholder: {
|
|
|
|
|
type: String,
|
|
|
|
|
default: ''
|
|
|
|
|
},
|
|
|
|
|
// 选项展示值
|
|
|
|
|
showValue: {
|
|
|
|
|
type: Boolean,
|
|
|
|
|
default: true
|
|
|
|
|
},
|
|
|
|
|
// 选项展示编码
|
|
|
|
|
showCode: {
|
|
|
|
|
type: Boolean,
|
|
|
|
|
default: false
|
|
|
|
|
},
|
|
|
|
|
// 选项展示排序
|
|
|
|
|
showSort: {
|
|
|
|
|
type: Boolean,
|
|
|
|
|
default: false
|
|
|
|
|
},
|
|
|
|
|
// 是否强制选项值
|
|
|
|
|
forceOption: {
|
|
|
|
|
type: Boolean,
|
|
|
|
|
default: true
|
|
|
|
|
},
|
|
|
|
|
// 是否使用强制字符串
|
|
|
|
|
forceString: {
|
|
|
|
|
type: Boolean,
|
|
|
|
|
default: true
|
|
|
|
|
},
|
|
|
|
|
// 展开选项最大数量
|
|
|
|
|
maxOptionCount: {
|
|
|
|
|
type: Number,
|
|
|
|
|
default: 20
|
|
|
|
|
},
|
|
|
|
|
// 自动选择文本
|
|
|
|
|
autoSelectText: {
|
|
|
|
|
type: Boolean,
|
|
|
|
|
default: true
|
|
|
|
|
},
|
|
|
|
|
disabled: [Boolean],
|
|
|
|
|
readonly: [Boolean],
|
|
|
|
|
maxHeight: {
|
|
|
|
|
type: Number,
|
|
|
|
|
default: 320
|
|
|
|
|
},
|
|
|
|
|
// 为空时使用labelKey的值
|
|
|
|
|
inputDisplayKey: {
|
|
|
|
|
type: String,
|
|
|
|
|
default: '',
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
model: {
|
|
|
|
|
prop: 'value',
|
|
|
|
|
event: 'change'
|
|
|
|
|
},
|
|
|
|
|
watch: {
|
|
|
|
|
value: {
|
|
|
|
|
immediate: true,
|
|
|
|
|
handler(val) {
|
|
|
|
|
|
|
|
|
|
let recValue = val;
|
|
|
|
|
if (recValue !== undefined && recValue !== null && this.forceString && typeof recValue !== 'string') {
|
|
|
|
|
recValue = recValue.toString();
|
|
|
|
|
}
|
|
|
|
|
if (recValue === this.innerValue) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this.innerValue = recValue || '';
|
|
|
|
|
// 值变化了,重置显示值
|
|
|
|
|
this.inputLabel = this.getSelectedLabel();
|
|
|
|
|
// 重置后没搜索到则重新搜索赋值
|
|
|
|
|
if (!this.inputLabel) {
|
|
|
|
|
this.handleSearch();
|
|
|
|
|
this.inputLabel = this.getSelectedLabel();
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.hoverIndex = this.tempSelectedIndex;
|
|
|
|
|
if (this.hoverIndex === -1) {
|
|
|
|
|
this.hoverIndex = 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
options: {
|
|
|
|
|
immediate: true,
|
|
|
|
|
handler() {
|
|
|
|
|
this.handleSearch();
|
|
|
|
|
// 当前有实际值,但显示值没有,则重新搜索赋值
|
|
|
|
|
if (this.innerValue && !this.inputLabel) {
|
|
|
|
|
this.inputLabel = this.getSelectedLabel();
|
|
|
|
|
this.hoverIndex = this.tempSelectedIndex;
|
|
|
|
|
if (this.hoverIndex === -1) {
|
|
|
|
|
this.hoverIndex = 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
data () {
|
|
|
|
|
return {
|
|
|
|
|
expand: false,
|
|
|
|
|
// 保存实际值
|
|
|
|
|
innerValue: null,
|
|
|
|
|
// 保存显示值
|
|
|
|
|
inputLabel: null,
|
|
|
|
|
// 搜索延时器
|
|
|
|
|
searchTimer: null,
|
|
|
|
|
// 搜索到的选项
|
|
|
|
|
searchOptions: [],
|
|
|
|
|
tempSelectedIndex: -1,
|
|
|
|
|
hoverIndex: 0,
|
|
|
|
|
// 是否聚焦
|
|
|
|
|
isFocus: false,
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
methods: {
|
|
|
|
|
getSelectedIndex() {
|
|
|
|
|
if (!this.innerValue) {
|
|
|
|
|
this.tempSelectedIndex = -1;
|
|
|
|
|
return this.tempSelectedIndex;
|
|
|
|
|
}
|
|
|
|
|
this.tempSelectedIndex = this.searchOptions?.findIndex(item => item[this.valueKey] === this.innerValue);
|
|
|
|
|
return this.tempSelectedIndex;
|
|
|
|
|
},
|
|
|
|
|
getSelectedOption() {
|
|
|
|
|
const selectIndex = this.getSelectedIndex();
|
|
|
|
|
return selectIndex > -1 ? this.searchOptions?.[selectIndex] : {};
|
|
|
|
|
},
|
|
|
|
|
getSelectedLabel() {
|
|
|
|
|
return this.getSelectedOption()[this.inputDisplayKey || this.labelKey] || '';
|
|
|
|
|
},
|
|
|
|
|
// getOrgSelectedLabel() {
|
|
|
|
|
// const index = this.options?.findIndex(item => item[this.valueKey] === this.innerValue);
|
|
|
|
|
// return index > -1 ? this.options?.[index][this.labelKey] : '';
|
|
|
|
|
// },
|
|
|
|
|
// 选择
|
|
|
|
|
handleSelectOption(option, index = this.tempSelectedIndex, blur) {
|
|
|
|
|
this.tempSelectedIndex = index;
|
|
|
|
|
this.hoverIndex = index;
|
|
|
|
|
this.innerValue = option[this.valueKey];
|
|
|
|
|
this.inputLabel = option[this.labelKey];
|
|
|
|
|
this.handleValueChange(option);
|
|
|
|
|
if (blur) {
|
|
|
|
|
this.handleBlur();
|
|
|
|
|
} else {
|
|
|
|
|
this.handleUnexpand();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
},
|
|
|
|
|
// 输入
|
|
|
|
|
handleInput() {
|
|
|
|
|
if (this.inputLabel.length > 0) {
|
|
|
|
|
this.inputLabel = this.inputLabel.trim();
|
|
|
|
|
this.handleDelaySearch();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (this.expand) {
|
|
|
|
|
this.handleDelaySearch();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
// 延时搜索
|
|
|
|
|
handleDelaySearch() {
|
|
|
|
|
|
|
|
|
|
if (!this.expand) {
|
|
|
|
|
this.handleSearch();
|
|
|
|
|
this.hoverIndex = 0;
|
|
|
|
|
if (this.searchOptions.length > 0) {
|
|
|
|
|
this.handleExpand();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.searchTimer) {
|
|
|
|
|
clearTimeout(this.searchTimer);
|
|
|
|
|
}
|
|
|
|
|
this.searchTimer = setTimeout(() => {
|
|
|
|
|
this.handleSearch();
|
|
|
|
|
this.hoverIndex = 0;
|
|
|
|
|
|
|
|
|
|
if (this.searchOptions.length === 0) {
|
|
|
|
|
this.handleUnexpand();
|
|
|
|
|
if (this.forceOption) {
|
|
|
|
|
this.$message.error('没有匹配的选项');
|
|
|
|
|
this.inputLabel = '';
|
|
|
|
|
this.innerValue = '';
|
|
|
|
|
this.handleValueChange();
|
|
|
|
|
}
|
|
|
|
|
} else if (this.isFocus) {
|
|
|
|
|
this.handleExpand();
|
|
|
|
|
}
|
|
|
|
|
}, 300);
|
|
|
|
|
},
|
|
|
|
|
// 搜索
|
|
|
|
|
handleSearch() {
|
|
|
|
|
if (!this.inputLabel) {
|
|
|
|
|
let orgOption;
|
|
|
|
|
if (this.innerValue) {
|
|
|
|
|
const orgIndex = this.options?.findIndex(item => item[this.valueKey] === this.innerValue);
|
|
|
|
|
if (orgIndex > -1) {
|
|
|
|
|
orgOption = this.options[orgIndex];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const defList = this.options?.slice(0, this.maxOptionCount) || [];
|
|
|
|
|
if (orgOption) {
|
|
|
|
|
|
|
|
|
|
if (defList?.some(val => val[this.valueKey] === orgOption[this.valueKey])) {
|
|
|
|
|
this.searchOptions = defList;
|
|
|
|
|
} else {
|
|
|
|
|
this.searchOptions = [orgOption, ...defList];
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
this.searchOptions = defList;
|
|
|
|
|
}
|
|
|
|
|
// console.log(this.searchOptions);
|
|
|
|
|
|
|
|
|
|
this.handleUpdatePopper();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const searchOptions = [];
|
|
|
|
|
for (let i = 0; i < this.options?.length; i++) {
|
|
|
|
|
const item = this.options[i];
|
|
|
|
|
if (this.showValue && item[this.valueKey].includes(this.inputLabel)) {
|
|
|
|
|
searchOptions.push(item);
|
|
|
|
|
} else if (this.showCode && String(item[this.codeKey] || '').includes(this.inputLabel)) {
|
|
|
|
|
searchOptions.push(item);
|
|
|
|
|
} else if (this.showSort && String(item[this.sortKey] || '').includes(this.inputLabel)) {
|
|
|
|
|
searchOptions.push(item);
|
|
|
|
|
} else if (item[this.labelKey].includes(this.inputLabel)) {
|
|
|
|
|
searchOptions.push(item);
|
|
|
|
|
}
|
|
|
|
|
if (searchOptions.length >= this.maxOptionCount) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
this.searchOptions = searchOptions;
|
|
|
|
|
this.handleUpdatePopper();
|
|
|
|
|
},
|
|
|
|
|
// 气泡更新位置
|
|
|
|
|
handleUpdatePopper() {
|
|
|
|
|
if (this.expand && this.$refs.popper) {
|
|
|
|
|
this.$nextTick(() => {
|
|
|
|
|
this.$refs.popper.updatePopper();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
},
|
|
|
|
|
// 展开
|
|
|
|
|
handleExpand() {
|
|
|
|
|
this.expand = true;
|
|
|
|
|
},
|
|
|
|
|
// 收起
|
|
|
|
|
handleUnexpand() {
|
|
|
|
|
this.expand = false;
|
|
|
|
|
},
|
|
|
|
|
// 获取焦点
|
|
|
|
|
handleFocus() {
|
|
|
|
|
this.$refs.input.focus();
|
|
|
|
|
},
|
|
|
|
|
// 同步外部获取焦点方法
|
|
|
|
|
focus() {
|
|
|
|
|
this.handleFocus();
|
|
|
|
|
},
|
|
|
|
|
// 失去焦点
|
|
|
|
|
handleBlur() {
|
|
|
|
|
this.$refs.input.blur();
|
|
|
|
|
},
|
|
|
|
|
// 同步外部失去焦点方法
|
|
|
|
|
blur() {
|
|
|
|
|
this.handleBlur();
|
|
|
|
|
},
|
|
|
|
|
// 输入框获取焦点
|
|
|
|
|
handleInputFocus(e) {
|
|
|
|
|
this.isFocus = true;
|
|
|
|
|
if (this.autoSelectText) {
|
|
|
|
|
this.$refs.input.select();
|
|
|
|
|
}
|
|
|
|
|
this.$emit('focus', e);
|
|
|
|
|
},
|
|
|
|
|
// 输入框失去焦点
|
|
|
|
|
handleInputBlur(e) {
|
|
|
|
|
this.isFocus = false;
|
|
|
|
|
this.handleUnexpand();
|
|
|
|
|
if (this.searchOptions.length > 0 && this.inputLabel) {
|
|
|
|
|
this.handleSelectOption(this.searchOptions[this.hoverIndex], this.hoverIndex, true);
|
|
|
|
|
} else if (this.innerValue) {
|
|
|
|
|
this.innerValue = '';
|
|
|
|
|
this.handleValueChange();
|
|
|
|
|
}
|
|
|
|
|
this.$emit('blur', e);
|
|
|
|
|
},
|
|
|
|
|
// 回车
|
|
|
|
|
handleEnter(e) {
|
|
|
|
|
if (this.expand && this.searchOptions.length > 0) {
|
|
|
|
|
this.handleSelectOption(this.searchOptions[this.hoverIndex], this.hoverIndex, true);
|
|
|
|
|
} else {
|
|
|
|
|
this.handleBlur();
|
|
|
|
|
}
|
|
|
|
|
this.$emit('enter', e);
|
|
|
|
|
},
|
|
|
|
|
// 上下键选择
|
|
|
|
|
handleNavigate(move) {
|
|
|
|
|
if (!this.expand) {
|
|
|
|
|
this.handleSearch();
|
|
|
|
|
if (this.inputLabel) {
|
|
|
|
|
this.hoverIndex = this.getSelectedIndex();
|
|
|
|
|
if (this.hoverIndex === -1) {
|
|
|
|
|
this.hoverIndex = 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.searchOptions.length > 0) {
|
|
|
|
|
this.handleExpand();
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (this.searchOptions.length === 0) {
|
|
|
|
|
this.hoverIndex = 0;
|
|
|
|
|
this.tempSelectedIndex = -1;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
let hoverIndex = this.hoverIndex + move;
|
|
|
|
|
if (hoverIndex < 0) {
|
|
|
|
|
hoverIndex = this.searchOptions.length - 1;
|
|
|
|
|
} else if (hoverIndex >= this.searchOptions.length) {
|
|
|
|
|
hoverIndex = 0;
|
|
|
|
|
}
|
|
|
|
|
this.hoverIndex = hoverIndex;
|
|
|
|
|
|
|
|
|
|
},
|
|
|
|
|
handleValueChange(option) {
|
|
|
|
|
this.$emit('change', this.innerValue);
|
|
|
|
|
this.$emit('itemChange', option);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style lang="scss">
|
|
|
|
|
.bm-select {
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.bm-select-popover {
|
|
|
|
|
padding: 5px 5px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.option-content {
|
|
|
|
|
overflow-x: hidden;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.option-code {
|
|
|
|
|
color: #999;
|
|
|
|
|
margin-left: 10px;
|
|
|
|
|
}
|
|
|
|
|
.option-item {
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
border-bottom: 1px solid #DCDFE6;
|
|
|
|
|
border-radius: 2px;
|
|
|
|
|
min-height: 32px;
|
|
|
|
|
padding: 2px 5px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
color: #222222;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.option-item:hover {
|
|
|
|
|
background: #66B1FF;
|
|
|
|
|
color: #FFFFFF;
|
|
|
|
|
|
|
|
|
|
.option-code {
|
|
|
|
|
color: #CCCCCC;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.option-item-hover {
|
|
|
|
|
background: #409EFF;
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
|
|
|
|
|
.option-code {
|
|
|
|
|
color: #EEEEEE;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.option-label {
|
|
|
|
|
flex: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
</style>
|