You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
316 lines
7.8 KiB
316 lines
7.8 KiB
<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> |