|
|
|
|
<template>
|
|
|
|
|
<el-select
|
|
|
|
|
ref="selectRef"
|
|
|
|
|
v-model="localValue"
|
|
|
|
|
:placeholder="placeholder"
|
|
|
|
|
:disabled="disabled"
|
|
|
|
|
filterable
|
|
|
|
|
clearable
|
|
|
|
|
:filter-method="handleSearch"
|
|
|
|
|
@visible-change="handleVisibleChange"
|
|
|
|
|
@clear="handleClear"
|
|
|
|
|
@change="handleChange"
|
|
|
|
|
class="paged-select"
|
|
|
|
|
:teleported="true"
|
|
|
|
|
:popper-options="{
|
|
|
|
|
modifiers: [{ name: 'eventListeners', options: { scroll: false, resize: false } }],
|
|
|
|
|
}"
|
|
|
|
|
>
|
|
|
|
|
<el-option
|
|
|
|
|
v-for="item in optionList"
|
|
|
|
|
:key="item[valueKey]"
|
|
|
|
|
:label="item[labelKey]"
|
|
|
|
|
:value="item[valueKey]"
|
|
|
|
|
/>
|
|
|
|
|
<el-option v-if="loading && hasMore" label="加载中..." disabled />
|
|
|
|
|
<el-option v-if="!hasMore && optionList.length" label="没有更多了~" disabled />
|
|
|
|
|
</el-select>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
import axios from 'axios';
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
name: 'DynamicPagedSelect',
|
|
|
|
|
props: {
|
|
|
|
|
value: [String, Number],
|
|
|
|
|
apiUrl: { type: String, required: true },
|
|
|
|
|
apiMethod: { type: String, default: 'get' },
|
|
|
|
|
params: { type: Object, default: () => ({}) },
|
|
|
|
|
listKey: { type: String, required: true },
|
|
|
|
|
totalKey: { type: String, required: true },
|
|
|
|
|
labelKey: { type: String, required: true },
|
|
|
|
|
valueKey: { type: String, required: true },
|
|
|
|
|
searchKey: { type: String, default: 'keyword' },
|
|
|
|
|
pageSize: { type: Number, default: 10 },
|
|
|
|
|
placeholder: String,
|
|
|
|
|
disabled: Boolean,
|
|
|
|
|
debounceTime: { type: Number, default: 500 },
|
|
|
|
|
echoApi: { type: String, default: '' },
|
|
|
|
|
echoMethod: { type: String, default: 'get' },
|
|
|
|
|
echoParamsKey: {
|
|
|
|
|
type: String,
|
|
|
|
|
default: 'id', // 默认查询字段是 id,可自定义
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
data() {
|
|
|
|
|
return {
|
|
|
|
|
localValue: this.value,
|
|
|
|
|
optionList: [],
|
|
|
|
|
pageNum: 1,
|
|
|
|
|
loading: false,
|
|
|
|
|
hasMore: true,
|
|
|
|
|
searchText: '',
|
|
|
|
|
scrollEl: null,
|
|
|
|
|
dropdownVisible: false,
|
|
|
|
|
bindTimer: null,
|
|
|
|
|
total: 0,
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
watch: {
|
|
|
|
|
value(val) {
|
|
|
|
|
this.localValue = val;
|
|
|
|
|
console.log('value', val);
|
|
|
|
|
this.initEcho();
|
|
|
|
|
},
|
|
|
|
|
localValue(val) {
|
|
|
|
|
this.$emit('input', val);
|
|
|
|
|
},
|
|
|
|
|
optionList() {
|
|
|
|
|
if (this.dropdownVisible && this.optionList.length > 0) {
|
|
|
|
|
this.$nextTick(() => {
|
|
|
|
|
this.bindScroll();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
mounted() {
|
|
|
|
|
this.debounceSearch = this.debounce(this.fetchData, this.debounceTime);
|
|
|
|
|
this.initEcho();
|
|
|
|
|
},
|
|
|
|
|
beforeUnmount() {
|
|
|
|
|
this.cleanup();
|
|
|
|
|
},
|
|
|
|
|
methods: {
|
|
|
|
|
debounce(func, wait) {
|
|
|
|
|
let timeout = null;
|
|
|
|
|
return (...args) => {
|
|
|
|
|
clearTimeout(timeout);
|
|
|
|
|
timeout = setTimeout(() => func.apply(this, args), wait);
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
cleanup() {
|
|
|
|
|
if (this.scrollEl) {
|
|
|
|
|
this.scrollEl.removeEventListener('scroll', this.handleScroll);
|
|
|
|
|
this.scrollEl = null;
|
|
|
|
|
}
|
|
|
|
|
if (this.bindTimer) {
|
|
|
|
|
clearTimeout(this.bindTimer);
|
|
|
|
|
this.bindTimer = null;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
findScrollContainer() {
|
|
|
|
|
const selectRef = this.$refs.selectRef;
|
|
|
|
|
if (!selectRef) return null;
|
|
|
|
|
|
|
|
|
|
const popper = selectRef.popperRef?.$el || selectRef.popperRef;
|
|
|
|
|
if (!popper) return null;
|
|
|
|
|
|
|
|
|
|
const wrap = popper.querySelector('.el-scrollbar__wrap');
|
|
|
|
|
if (wrap) return wrap;
|
|
|
|
|
|
|
|
|
|
const altWrap = popper.querySelector('.el-select-dropdown__wrap');
|
|
|
|
|
return altWrap || null;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
bindScroll() {
|
|
|
|
|
this.cleanup();
|
|
|
|
|
|
|
|
|
|
const tryBind = (attempt = 1) => {
|
|
|
|
|
if (!this.dropdownVisible) return;
|
|
|
|
|
|
|
|
|
|
const scrollEl = this.findScrollContainer();
|
|
|
|
|
if (scrollEl) {
|
|
|
|
|
this.scrollEl = scrollEl;
|
|
|
|
|
scrollEl.addEventListener('scroll', this.handleScroll, { passive: true });
|
|
|
|
|
} else if (attempt < 5) {
|
|
|
|
|
this.bindTimer = setTimeout(() => tryBind(attempt + 1), 100 * attempt);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
tryBind();
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
handleScroll(e) {
|
|
|
|
|
const { scrollTop, scrollHeight, clientHeight } = e.target;
|
|
|
|
|
const threshold = scrollHeight - clientHeight - 10;
|
|
|
|
|
|
|
|
|
|
if (this.loading || !this.hasMore) return;
|
|
|
|
|
|
|
|
|
|
if (scrollTop >= threshold) {
|
|
|
|
|
this.loadMore();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
handleVisibleChange(visible) {
|
|
|
|
|
this.dropdownVisible = visible;
|
|
|
|
|
|
|
|
|
|
if (visible) {
|
|
|
|
|
this.reset();
|
|
|
|
|
this.fetchData().then(() => {
|
|
|
|
|
this.bindScroll();
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
this.cleanup();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
handleSearch(val) {
|
|
|
|
|
this.searchText = val;
|
|
|
|
|
this.reset();
|
|
|
|
|
this.debounceSearch();
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
loadMore() {
|
|
|
|
|
if (this.loading || !this.hasMore) return;
|
|
|
|
|
this.pageNum++;
|
|
|
|
|
this.fetchData();
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
handleClear() {
|
|
|
|
|
this.localValue = '';
|
|
|
|
|
this.reset();
|
|
|
|
|
this.$emit('clear');
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 关键修改:选中后返回完整 item
|
|
|
|
|
handleChange(val) {
|
|
|
|
|
// 从当前列表查找
|
|
|
|
|
let selectedItem = this.optionList.find(item => item[this.valueKey] === val);
|
|
|
|
|
|
|
|
|
|
// 如果没找到且有回显接口,通过接口获取
|
|
|
|
|
if (!selectedItem && this.echoApi) {
|
|
|
|
|
this.getEchoData(val).then(item => {
|
|
|
|
|
this.$emit('change', val, item);
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
// 确保发射事件
|
|
|
|
|
this.$nextTick(() => {
|
|
|
|
|
this.$emit('change', val, selectedItem);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 新增:通过接口获取完整数据
|
|
|
|
|
async getEchoData(val) {
|
|
|
|
|
try {
|
|
|
|
|
const params = { [this.echoParamsKey]: val };
|
|
|
|
|
let res;
|
|
|
|
|
if (this.echoMethod.toLowerCase() === 'post') {
|
|
|
|
|
res = await axios.post(this.echoApi, params);
|
|
|
|
|
} else {
|
|
|
|
|
res = await axios.get(this.echoApi, { params });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const detail = res.data.data || res.data;
|
|
|
|
|
if (!detail) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return detail;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
async initEcho() {
|
|
|
|
|
const val = this.localValue;
|
|
|
|
|
if (!val || !this.echoApi) return;
|
|
|
|
|
|
|
|
|
|
// 处理多选(数组)
|
|
|
|
|
if (this.multiple && Array.isArray(val)) {
|
|
|
|
|
const list = await this.getEchoData(this.multiple);
|
|
|
|
|
if (list && list.length) {
|
|
|
|
|
this.optionList.unshift(...list);
|
|
|
|
|
this.$forceUpdate();
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 原有单选逻辑
|
|
|
|
|
const hasItem = this.optionList.some(item => item[this.valueKey] === val);
|
|
|
|
|
if (hasItem) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const info = await this.getEchoData(val);
|
|
|
|
|
|
|
|
|
|
if (info.records.length > 0) {
|
|
|
|
|
this.optionList = info.records;
|
|
|
|
|
this.$forceUpdate();
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('回显失败', e);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
async fetchData() {
|
|
|
|
|
this.loading = true;
|
|
|
|
|
try {
|
|
|
|
|
const sendData = {
|
|
|
|
|
current: this.pageNum,
|
|
|
|
|
size: this.pageSize,
|
|
|
|
|
[this.searchKey]: this.searchText,
|
|
|
|
|
...this.params,
|
|
|
|
|
};
|
|
|
|
|
let res;
|
|
|
|
|
if (this.apiMethod.toLowerCase() === 'post') {
|
|
|
|
|
res = await axios.post(this.apiUrl, sendData);
|
|
|
|
|
} else {
|
|
|
|
|
res = await axios.get(this.apiUrl, { params: sendData });
|
|
|
|
|
}
|
|
|
|
|
const data = res.data.data || {};
|
|
|
|
|
const list = data[this.listKey] || [];
|
|
|
|
|
const total = data[this.totalKey] || 0;
|
|
|
|
|
|
|
|
|
|
this.total = total;
|
|
|
|
|
|
|
|
|
|
if (this.pageNum === 1) {
|
|
|
|
|
this.optionList = list;
|
|
|
|
|
} else {
|
|
|
|
|
this.optionList = this.optionList.concat(list);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.hasMore = this.optionList.length < total;
|
|
|
|
|
|
|
|
|
|
this.$nextTick(() => {
|
|
|
|
|
this.bindScroll();
|
|
|
|
|
});
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('请求失败', e);
|
|
|
|
|
} finally {
|
|
|
|
|
this.loading = false;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
reset() {
|
|
|
|
|
this.pageNum = 1;
|
|
|
|
|
this.optionList = [];
|
|
|
|
|
this.hasMore = true;
|
|
|
|
|
this.total = 0;
|
|
|
|
|
this.cleanup();
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
</script>
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
|
:deep(.el-select-dropdown) {
|
|
|
|
|
height: 200px !important;
|
|
|
|
|
overflow: hidden !important;
|
|
|
|
|
}
|
|
|
|
|
:deep(.el-select-dropdown .el-scrollbar__wrap) {
|
|
|
|
|
height: 200px !important;
|
|
|
|
|
overflow-y: auto !important;
|
|
|
|
|
overflow-x: hidden !important;
|
|
|
|
|
}
|
|
|
|
|
</style>
|