From a522525ef474d19e45fe95b51b4cd8d696c5406d Mon Sep 17 00:00:00 2001 From: DXC Date: Mon, 23 Mar 2026 15:00:10 +0800 Subject: [PATCH] perf: enforce server-side pagination for tables and audit infinite scroll logic for selects --- .../src/views/stock/inbound/service.vue | 57 ++++++++++++++++++- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/inventory-web/src/views/stock/inbound/service.vue b/inventory-web/src/views/stock/inbound/service.vue index 05d9112..efed232 100644 --- a/inventory-web/src/views/stock/inbound/service.vue +++ b/inventory-web/src/views/stock/inbound/service.vue @@ -110,6 +110,8 @@ style="width: 100%" @change="onMaterialSelected" default-first-option + v-loadmore="loadMoreMaterials" + popper-class="long-dropdown" > {{ item.spec }} +
+ 加载中... +
@@ -260,6 +265,26 @@ const hasFormFieldPermission = (fieldName: string) => { return userStore.hasPermission(code) } +// 自定义指令:v-loadmore(适配 Teleport 到 Body 的下拉框) +const vLoadmore = { + mounted(el: any, binding: any) { + const checkAndBind = () => { + const dropDownWrap = document.querySelector('.long-dropdown .el-select-dropdown__wrap') + if (dropDownWrap && !dropDownWrap.getAttribute('data-loadmore-bound')) { + dropDownWrap.setAttribute('data-loadmore-bound', 'true') + dropDownWrap.addEventListener('scroll', function (this: any) { + const condition = this.scrollHeight - this.scrollTop <= this.clientHeight + 1 + if (condition) { + binding.value() + } + }) + } + } + setTimeout(checkAndBind, 500) + el.addEventListener('click', () => setTimeout(checkAndBind, 300)) + } +} + // 表格数据 const tableData = ref([]) const loading = ref(false) @@ -269,6 +294,10 @@ const total = ref(0) const materialOptions = ref([]) const searchLoading = ref(false) +const searchPage = ref(1) +const hasNextPage = ref(true) +const loadingMore = ref(false) +const searchKeyword = ref('') const searchForm = reactive({ keyword: '', @@ -335,11 +364,15 @@ const handleMaterialDropdownVisible = (visible: boolean) => { const handleSearchMaterial = async (query: string) => { searchLoading.value = true + searchKeyword.value = query + searchPage.value = 1 + materialOptions.value = [] try { - const res = await searchMaterialBase(query) + const res: any = await searchMaterialBase(query, 1) if (res.code === 200) { - const apiResults = (res.data || []).map((i: any) => ({ ...i, isHistory: false })) + const apiResults = (res.data?.items || []).map((i: any) => ({ ...i, isHistory: false })) materialOptions.value = apiResults + hasNextPage.value = res.data?.has_next ?? false } else { materialOptions.value = [] } @@ -350,6 +383,26 @@ const handleSearchMaterial = async (query: string) => { } } +const loadMoreMaterials = async () => { + if (searchLoading.value || loadingMore.value || !hasNextPage.value) return + loadingMore.value = true + searchPage.value += 1 + try { + const res: any = await searchMaterialBase(searchKeyword.value, searchPage.value) + if (res.data && res.data.items && res.data.items.length > 0) { + const newItems = res.data.items.map((i: any) => ({ ...i, isHistory: false })) + materialOptions.value.push(...newItems) + hasNextPage.value = res.data.has_next + } else { + hasNextPage.value = false + } + } catch (e) { + searchPage.value -= 1 + } finally { + loadingMore.value = false + } +} + const onMaterialSelected = (val: number) => { const item = materialOptions.value.find(i => i.id === val) if (item) {