parent
454613a321
commit
059f15ac99
3 changed files with 342 additions and 15 deletions
@ -0,0 +1,296 @@ |
||||
<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> |
||||
.paged-select.el-select-dropdown.el-popper { |
||||
height: 200px !important; |
||||
overflow: hidden !important; |
||||
} |
||||
|
||||
.paged-select.el-select-dropdown .el-scrollbar__wrap { |
||||
height: 200px !important; |
||||
overflow-y: auto !important; |
||||
overflow-x: hidden !important; |
||||
} |
||||
</style> |
||||
Loading…
Reference in new issue