|
|
|
|
<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="false"
|
|
|
|
|
: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' },
|
|
|
|
|
},
|
|
|
|
|
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;
|
|
|
|
|
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) {
|
|
|
|
|
console.log('选中值:', val);
|
|
|
|
|
|
|
|
|
|
// 从当前列表查找
|
|
|
|
|
let selectedItem = this.optionList.find(item => item[this.valueKey] === val);
|
|
|
|
|
console.log('从列表找到:', selectedItem);
|
|
|
|
|
|
|
|
|
|
// 如果没找到且有回显接口,通过接口获取
|
|
|
|
|
if (!selectedItem && this.echoApi) {
|
|
|
|
|
console.log('列表未找到,通过接口获取');
|
|
|
|
|
this.getEchoData(val).then(item => {
|
|
|
|
|
console.log('接口返回:', item);
|
|
|
|
|
this.$emit('change', val, item);
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
// 确保发射事件
|
|
|
|
|
this.$nextTick(() => {
|
|
|
|
|
this.$emit('change', val, selectedItem);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 新增:通过接口获取完整数据
|
|
|
|
|
async getEchoData(val) {
|
|
|
|
|
try {
|
|
|
|
|
const params = { [this.valueKey]: val };
|
|
|
|
|
let res;
|
|
|
|
|
if (this.echoMethod.toLowerCase() === 'post') {
|
|
|
|
|
res = await axios.post(this.echoApi, params);
|
|
|
|
|
} else {
|
|
|
|
|
res = await axios.get(this.echoApi, { params });
|
|
|
|
|
}
|
|
|
|
|
return res.data.data || {};
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('获取选中项详情失败', e);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
async initEcho() {
|
|
|
|
|
const val = this.localValue;
|
|
|
|
|
if (!val || !this.echoApi) return;
|
|
|
|
|
const has = this.optionList.some(item => item[this.valueKey] === val);
|
|
|
|
|
if (has) return;
|
|
|
|
|
|
|
|
|
|
const info = await this.getEchoData(val);
|
|
|
|
|
if (info[this.labelKey] && info[this.valueKey]) {
|
|
|
|
|
const exist = this.optionList.some(i => i[this.valueKey] === info[this.valueKey]);
|
|
|
|
|
if (!exist) this.optionList.unshift(info);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
async fetchData() {
|
|
|
|
|
this.loading = true;
|
|
|
|
|
try {
|
|
|
|
|
const sendData = {
|
|
|
|
|
pageNum: this.pageNum,
|
|
|
|
|
pageSize: 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: 300px !important;
|
|
|
|
|
overflow-y: auto !important;
|
|
|
|
|
overflow-x: hidden !important;
|
|
|
|
|
}
|
|
|
|
|
</style>
|