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