中企動(dòng)力科技股份有限公司網(wǎng)站官網(wǎng)2020十大網(wǎng)絡(luò)熱詞
文章目錄
- 封裝BrandPickerVirtual.vue組件
- 頁(yè)面使用
- 組件屬性

select下拉接口一次性返回10萬(wàn)條
數(shù)據(jù),頁(yè)面卡死,如何優(yōu)化??這里使用 分頁(yè) + 虛擬列表(vue-virtual-scroll-list)
,去模擬一個(gè)下拉的內(nèi)容顯示區(qū)域。支持單選 + 多選 + 模糊查詢(xún) + 滾動(dòng)觸底自動(dòng)分頁(yè)請(qǐng)求
。
粗略實(shí)現(xiàn),滿(mǎn)足需求即可哈哈哈哈哈哈哈:
單選:
多選:
封裝BrandPickerVirtual.vue組件
<template><div class="brand-picker-virtual"><el-popoverv-model="visible"placement="bottom-start"trigger="click"popper-class="brand-picker-popper":append-to-body="false":width="300"><div class="brand-picker-popover"><div class="search-box"><el-inputv-model="searchKeyword"placeholder="搜索品牌"prefix-icon="el-icon-search"clearable /></div><div class="brand-list" ref="brandList"><virtual-listref="virtualList"class="scroller":data-key="'brand_id'":data-sources="filteredBrands":data-component="itemComponent":estimate-size="40":keeps="20":item-class="'brand-item'":extra-props="{multiple,isSelected: isSelected,handleSelect: handleSelect,disabled}":buffer="10":bottom-threshold="30"@tobottom="handleScrollToBottom"/><div v-if="loading" class="loading-more"><i class="el-icon-loading"></i> 加載中...</div><div ref="observer" class="observer-target"></div></div><div v-if="multiple" class="footer"><el-button size="small" @click="handleClear">清空</el-button><el-button type="primary" size="small" @click="handleConfirm">確定</el-button></div></div><div slot="reference" class="el-input el-input--suffix select-trigger":class="{ 'is-focus': visible }"><div class="el-input__inner select-inner"><div class="select-tags" v-if="multiple && selectedBrands.length"><el-tagv-for="brand in selectedBrands":key="brand.brand_id"closable:disable-transitions="false"@close="handleRemoveTag(brand)"size="small"class="brand-tag">{{ brand.name }}</el-tag></div><div v-else-if="!multiple && selectedBrands.length" class="selected-single"><span class="selected-label">{{ selectedBrands[0].name }}</span></div><inputtype="text"readonly:placeholder="getPlaceholder"class="select-input"><i v-if="selectedBrands.length" class="el-icon-circle-close clear-icon" @click.stop="handleClear"></i></div></div></el-popover></div>
</template><script>
import VirtualList from 'vue-virtual-scroll-list'
import request from '@/utils/request'const BrandItem = {name: 'BrandItem',props: {source: {type: Object,required: true},multiple: Boolean,isSelected: Function,handleSelect: Function,disabled: Boolean},render(h) {const isItemSelected = this.isSelected(this.source)return h('div', {class: {'item-content': true,'is-selected': isItemSelected && !this.multiple},on: {click: (e) => {if (!this.disabled) {this.handleSelect(this.source)}}}}, [this.multiple && h('el-checkbox', {props: {value: isItemSelected,disabled: this.disabled}}),h('span', { class: 'brand-name' }, this.source.name)])}
}export default {name: 'BrandPickerVirtual',components: {VirtualList},props: {multiple: {type: Boolean,default: false},defaultBrandId: {type: [Array, String, Number],default: () => []},api: {type: String,default: 'admin/goods/brands'},disabled: {type: Boolean,default: false}},data() {return {visible: false, // 彈窗是否可見(jiàn)searchKeyword: '', // 搜索關(guān)鍵字brandList: [], // 品牌列表數(shù)據(jù)selectedBrands: [], // 已選中的品牌列表tempSelectedBrands: [], // 多選時(shí)的臨時(shí)選中列表loading: false, // 是否正在加載數(shù)據(jù)itemComponent: BrandItem, // 品牌項(xiàng)組件pageNo: 1, // 當(dāng)前頁(yè)碼pageSize: 20, // 每頁(yè)數(shù)量hasMore: true, // 是否還有更多數(shù)據(jù)searchTimer: null, // 搜索防抖定時(shí)器searchLoading: false, // 搜索加載狀態(tài)lastScrollTop: 0, // 上次滾動(dòng)位置isFirstPageLoaded: false, // 是否已加載第一頁(yè)數(shù)據(jù)observer: null // 交叉觀察器實(shí)例}},computed: {/*** 根據(jù)搜索關(guān)鍵字過(guò)濾品牌列表* @returns {Array} 過(guò)濾后的品牌列表*/filteredBrands() {if (!this.searchKeyword) return this.brandListconst keyword = this.searchKeyword.toLowerCase()return this.brandList.filter(item =>item.name.toLowerCase().includes(keyword))},/*** 選中品牌的顯示文本* @returns {string} 顯示文本*/selectedText() {if (this.multiple) {return this.selectedBrands.length? `已選擇 ${this.selectedBrands.length} 個(gè)品牌`: ''}return (this.selectedBrands[0] && this.selectedBrands[0].name) || ''},/*** 獲取占位符文本*/getPlaceholder() {if (this.multiple) {return this.selectedBrands.length ? '' : '請(qǐng)選擇品牌(可多選)'}return this.selectedBrands.length ? '' : '請(qǐng)選擇品牌'}},watch: {/*** 監(jiān)聽(tīng)默認(rèn)品牌ID變化,同步選中狀態(tài)*/defaultBrandId: {immediate: true,handler(val) {if (!val || !this.brandList.length) returnif (this.multiple) {this.selectedBrands = this.brandList.filter(item =>val.includes(item.brand_id))} else {const brand = this.brandList.find(item =>item.brand_id === val)this.selectedBrands = brand ? [brand] : []}this.tempSelectedBrands = [...this.selectedBrands]}},/*** 監(jiān)聽(tīng)彈窗顯示狀態(tài),首次打開(kāi)時(shí)加載數(shù)據(jù)*/visible(val) {if (val) {if (this.multiple) {this.tempSelectedBrands = [...this.selectedBrands]}this.resetData()this.getBrandList()// 確保虛擬列表在顯示時(shí)重新初始化this.$nextTick(() => {if (this.$refs.virtualList) {this.$refs.virtualList.reset()}})}},/*** 監(jiān)聽(tīng)搜索關(guān)鍵字變化,帶防抖的搜索處理*/searchKeyword(val) {if (this.searchTimer) {clearTimeout(this.searchTimer)}this.searchTimer = setTimeout(() => {this.resetData()this.getBrandList()}, 300)}},beforeDestroy() {if (this.observer) {this.observer.disconnect()}},methods: {/*** 初始化交叉觀察器,用于監(jiān)聽(tīng)滾動(dòng)到底部*/initObserver() {this.observer = new IntersectionObserver((entries) => {const target = entries[0]if (target.isIntersecting && !this.loading && this.hasMore) {this.getBrandList(true)}},{root: this.$el.querySelector('.scroller'),threshold: 0.1})if (this.$refs.observer) {this.observer.observe(this.$refs.observer)}},/*** 獲取品牌列表數(shù)據(jù)* @param {boolean} isLoadMore - 是否是加載更多*/async getBrandList(isLoadMore = false) {if (this.loading || (!isLoadMore && this.searchLoading)) returnif (isLoadMore && !this.hasMore) returnconst loading = isLoadMore ? 'loading' : 'searchLoading'this[loading] = truetry {if (isLoadMore) {this.pageNo++} else {this.pageNo = 1}const response = await request({url: this.api,method: 'get',params: {page_no: this.pageNo,page_size: this.pageSize,keyword: this.searchKeyword},loading: false})const { data, data_total } = response if (!isLoadMore) {this.brandList = datathis.isFirstPageLoaded = true} else {this.brandList = [...this.brandList, ...data]}this.hasMore = this.brandList.length < data_totalif (this.defaultBrandId && !isLoadMore) {this.initializeSelection()}} catch (error) {console.error('獲取品牌列表失敗:', error)} finally {this[loading] = false}},/*** 滾動(dòng)到底部的處理函數(shù)*/handleScrollToBottom() {if (!this.loading && this.hasMore) {this.getBrandList(true)}},/*** 初始化選中狀態(tài)*/initializeSelection() {if (this.multiple) {this.selectedBrands = this.brandList.filter(item =>this.defaultBrandId.includes(item.brand_id))} else {const brand = this.brandList.find(item =>item.brand_id === this.defaultBrandId)this.selectedBrands = brand ? [brand] : []}this.tempSelectedBrands = [...this.selectedBrands]},/*** 判斷品牌是否被選中* @param {Object} item - 品牌項(xiàng)* @returns {boolean} 是否選中*/isSelected(item) {return this.multiple? this.tempSelectedBrands.some(brand => brand.brand_id === item.brand_id): this.selectedBrands.some(brand => brand.brand_id === item.brand_id)},/*** 處理品牌選擇* @param {Object} item - 選中的品牌項(xiàng)*/handleSelect(item) {if (this.multiple) {const index = this.tempSelectedBrands.findIndex(brand => brand.brand_id === item.brand_id)if (index > -1) {this.tempSelectedBrands.splice(index, 1)} else {this.tempSelectedBrands.push(item)}} else {this.selectedBrands = [item]this.visible = falsethis.emitChange()}},/*** 清空選中的品牌*/handleClear(e) {// 阻止事件冒泡,防止觸發(fā)下拉框if (e) {e.stopPropagation()}this.selectedBrands = []this.tempSelectedBrands = []this.emitChange()},/*** 確認(rèn)多選結(jié)果*/handleConfirm() {this.selectedBrands = [...this.tempSelectedBrands]this.visible = falsethis.emitChange()},/*** 觸發(fā)選中值變化事件*/emitChange() {const value = this.multiple? this.selectedBrands.map(item => item.brand_id): (this.selectedBrands[0] && this.selectedBrands[0].brand_id) || nullthis.$emit('changed', value)},handleRemoveTag(brand) {const index = this.selectedBrands.findIndex(item => item.brand_id === brand.brand_id)if (index > -1) {this.selectedBrands.splice(index, 1)}this.tempSelectedBrands = [...this.selectedBrands]this.emitChange()},/*** 重置列表相關(guān)數(shù)據(jù)*/resetData() {this.brandList = []this.pageNo = 1this.hasMore = truethis.loading = falsethis.searchLoading = false}}
}
</script><style lang="scss">
.brand-picker-popper {max-height: calc(100vh - 100px);overflow: visible !important;left: 0 !important;top: 26px !important;.el-popover__title {margin: 0;padding: 0;}
}
</style><style lang="scss" scoped>
.brand-picker-virtual {display: inline-block;width: 100%;position: relative;.select-trigger {width: 100%;&.is-focus .el-input__inner {border-color: #409EFF;}}.select-inner {padding: 3px 8px;min-height: 32px;height: auto;cursor: pointer;position: relative;background-color: #fff;border: 1px solid #dcdfe6;border-radius: 4px;display: flex;align-items: center;flex-wrap: wrap;}.select-tags {display: flex;flex-wrap: wrap;gap: 4px;flex: 1;min-height: 24px;padding: 2px 0;.brand-tag {max-width: 100%;margin: 2px 0;&:last-child {margin-right: 4px;}}}.select-input {width: 0;min-width: 60px;margin: 2px 0;padding: 0;background: none;border: none;outline: none;height: 24px;line-height: 24px;font-size: 14px;color: #606266;flex: 1;&::placeholder {color: #c0c4cc;}}.clear-icon {position: absolute;right: 8px;color: #c0c4cc;font-size: 14px;cursor: pointer;transition: color .2s;&:hover {color: #909399;}}.selected-single {display: flex;align-items: center;flex: 1;padding-right: 24px;.selected-label {flex: 1;font-size: 14px;color: #606266;overflow: hidden;text-overflow: ellipsis;white-space: nowrap;}}.el-input__suffix,.el-icon-arrow-down {display: none;}.brand-picker-popover {margin-top: 4px !important;.search-box {padding: 0 0 12px;.el-input {font-size: 14px;}}.brand-list {position: relative;height: 320px;border: 1px solid #EBEEF5;border-radius: 4px;overflow: hidden;.scroller {height: 100%;overflow-y: auto !important;overflow-x: hidden;padding: 4px 0;/deep/ .virtual-list-container {position: relative !important;}/deep/ .virtual-list-phantom {position: relative !important;}/deep/ .brand-item {.item-content {padding-left: 8px;height: 40px;line-height: 40px;cursor: pointer;transition: all 0.3s;box-sizing: border-box;position: relative;font-size: 14px;color: #606266;border-bottom: 1px solid #f0f0f0;display: flex;align-items: center;user-select: none;.el-checkbox {margin-right: 8px;}.brand-name {flex: 1;overflow: hidden;text-overflow: ellipsis;white-space: nowrap;}&:hover {background-color: #F5F7FA;}&.is-selected {background-color: #F5F7FA;color: #409EFF;font-weight: 500;&::after {content: '';position: absolute;right: 15px;width: 14px;height: 14px;background: url(data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTAyNCAxMDI0IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Ik00MDYuNjU2IDcwNi45NDRsLTE2MC0xNjBjLTEyLjQ4LTEyLjQ4LTMyLjc2OC0xMi40OC00NS4yNDggMHMtMTIuNDggMzIuNzY4IDAgNDUuMjQ4bDE4Mi42MjQgMTgyLjYyNGMxMi40OCAxMi40OCAzMi43NjggMTIuNDggNDUuMjQ4IDBsNDAwLTQwMGMxMi40OC0xMi40OCAxMi40OC0zMi43NjggMC00NS4yNDhzLTMyLjc2OC0xMi40OC00NS4yNDggMEw0MDYuNjU2IDcwNi45NDR6IiBmaWxsPSIjNDA5RUZGIi8+PC9zdmc+) no-repeat center center;background-size: contain;}}}}}.loading-more {position: absolute;bottom: 0;left: 0;right: 0;padding: 8px;text-align: center;background: rgba(255, 255, 255, 0.95);color: #909399;font-size: 13px;z-index: 1;border-top: 1px solid #f0f0f0;}.observer-target {height: 2px;width: 100%;position: absolute;bottom: 0;left: 0;}}.footer {margin-top: 12px;text-align: right;padding: 0 2px;}}.selected-label {flex: 1;font-size: 14px;color: #606266;overflow: hidden;text-overflow: ellipsis;white-space: nowrap;}.selected-single {display: flex;align-items: center;flex: 1;padding: 0 4px;.selected-label {flex: 1;font-size: 14px;height: 24px;line-height: 24px;color: #606266;overflow: hidden;text-overflow: ellipsis;white-space: nowrap;}.el-icon-circle-close {margin-left: 8px;color: #c0c4cc;font-size: 14px;cursor: pointer;transition: color .2s;&:hover {color: #909399;}}}
}
</style>
頁(yè)面使用
<template><!-- 單選模式 --><brand-picker-virtual:default-brand-id="singleBrandId"@changed="handleBrandChange"/><!-- 多選模式 --><brand-picker-virtualmultiple:default-brand-id="multipleBrandIds"@changed="handleMultipleBrandChange"/>
</template><script>
// 注冊(cè)組件別忘了,我這里省略了,我是個(gè)全局注冊(cè)的
export default {data() {return {singleBrandId: null, // 單選模式:存儲(chǔ)單個(gè)品牌IDmultipleBrandIds: [] // 多選模式:存儲(chǔ)品牌ID數(shù)組}},methods: {// 單選回調(diào)handleBrandChange(brandId) {this.singleBrandId = brandId},// 多選回調(diào)handleMultipleBrandChange(brandIds) {this.multipleBrandIds = brandIds}}
}
</script>
組件屬性
props: {// 是否多選模式multiple: {type: Boolean,default: false},// 默認(rèn)選中的品牌ID(單選時(shí)為number/string,多選時(shí)為array)defaultBrandId: {type: [Array, String, Number],default: () => []},// 自定義接口地址api: {type: String,default: 'admin/goods/brands'},// 是否禁用disabled: {type: Boolean,default: false}
}