diff --git a/inventory-web/src/views/bom/BomManage.vue b/inventory-web/src/views/bom/BomManage.vue index 2f42584..346d461 100644 --- a/inventory-web/src/views/bom/BomManage.vue +++ b/inventory-web/src/views/bom/BomManage.vue @@ -63,7 +63,7 @@ - + @@ -75,13 +75,14 @@ placeholder="请搜索并选择父件" filterable remote - reserve-keyword + reserve-keyword="true" :remote-method="(q: string) => handleRemoteSearch(q, 'parent')" :loading="selectLoading" style="width: 100%" :disabled="isReadOnlyMode || isEditMode" class="beautified-select" popper-class="bom-loadmore-popper parent-popper" + default-first-option="true" @visible-change="(visible: boolean) => handleVisibleChange(visible, 'parent')" @change="onParentChange" > @@ -172,13 +173,14 @@ placeholder="请搜索原料" filterable remote - reserve-keyword + reserve-keyword="true" style="flex: 1;" :remote-method="(q: string) => handleRemoteSearch(q, 'child', row.rowKey)" :loading="selectLoading" :loading-text="`正在加载第 ${childQueryParams.page} 页...`" :popper-class="`bom-loadmore-popper child-popper-${row.rowKey}`" :disabled="isReadOnlyMode" + default-first-option="true" @visible-change="(visible: boolean) => handleVisibleChange(visible, 'child', row.rowKey)" > { + // 防御性处理:粘贴场景常混入零宽字符 / 控制字符 / 不可见 Unicode + const rawQuery = String(query || '') + const safeQuery = rawQuery.replace(/[\x00-\x1F\x7F-\x9F\u200B-\u200D\uFEFF]/g, '').trim() if (type === 'parent') { - parentQueryParams.keyword = query + parentQueryParams.keyword = safeQuery parentQueryParams.page = 1 parentHasMore.value = true fetchMaterialOptions('parent') } else if (type === 'child' && rowKey !== undefined) { const state = childDropdownStates.value.get(rowKey) if (!state) return - state.queryParams.keyword = query + state.queryParams.keyword = safeQuery state.queryParams.page = 1 state.hasMore = true fetchMaterialOptions('child', rowKey) @@ -435,6 +440,10 @@ const handleVisibleChange = (visible: boolean, type: 'parent' | 'child', rowKey? if (!visible) return if (type === 'parent') { + // 防御性拦截:竞态条件守卫 + // 如果当前已经有搜索关键字(例如用户刚刚粘贴了内容、remote-method 已经设置了 keyword), + // 绝对不要去请求默认列表,否则会清空 keyword、覆盖正确结果。 + if (parentQueryParams.keyword || parentOptions.value.length > 0) return parentQueryParams.page = 1 parentQueryParams.keyword = '' parentHasMore.value = true @@ -449,6 +458,8 @@ const handleVisibleChange = (visible: boolean, type: 'parent' | 'child', rowKey? }) } const state = childDropdownStates.value.get(rowKey)! + // 防御性拦截:竞态条件守卫(同上) + if (state.queryParams.keyword || state.options.length > 0) return state.queryParams.page = 1 state.queryParams.keyword = '' state.hasMore = true diff --git a/inventory-web/src/views/purchase/index.vue b/inventory-web/src/views/purchase/index.vue index de83970..e606891 100644 --- a/inventory-web/src/views/purchase/index.vue +++ b/inventory-web/src/views/purchase/index.vue @@ -64,7 +64,7 @@ /> - + @@ -74,14 +74,14 @@ v-model="materialBaseId" filterable remote - reserve-keyword + reserve-keyword="true" clearable placeholder="输入名称或规格搜索..." :remote-method="handleSearchMaterialDebounced" :loading="searchLoading" style="width: 100%" @change="onMaterialSelected" - default-first-option + default-first-option="true" popper-class="long-dropdown" v-loadmore="handleLoadMoreMaterials" @visible-change="onMaterialDropdownVisibleChange" @@ -171,7 +171,7 @@ - + {{ detail.request_no }} @@ -215,7 +215,7 @@ - + {{ currentRejectRow?.request_no }} @@ -396,14 +396,17 @@ const handleSearchMaterialDebounced = (query: string) => { } const handleSearchMaterial = async (query: string) => { + // 防御性处理:粘贴场景常混入零宽字符 / 控制字符 / 不可见 Unicode + const rawQuery = String(query || '') + const safeQuery = rawQuery.replace(/[\x00-\x1F\x7F-\x9F\u200B-\u200D\uFEFF]/g, '').trim() searchLoading.value = true - searchKeyword.value = query + searchKeyword.value = safeQuery searchPage.value = 1 materialOptions.value = [] hasNextPage.value = true try { - const res: any = await searchMaterialPurchase(query, 1) + const res: any = await searchMaterialPurchase(safeQuery, 1) materialOptions.value = res.data || [] hasNextPage.value = res.has_next !== false } finally { @@ -431,9 +434,14 @@ const handleLoadMoreMaterials = async () => { } const onMaterialDropdownVisibleChange = (visible: boolean) => { - if (visible && materialOptions.value.length === 0) { - handleSearchMaterial('') - } + if (!visible) return + // 防御性拦截:竞态条件守卫 + // 如果当前已经有搜索关键字(例如用户刚刚粘贴了内容、remote-method 已经设置了 searchKeyword), + // 绝对不要去请求默认列表,否则会清空 searchKeyword、覆盖正确结果。 + if (searchKeyword.value || materialOptions.value.length > 0) return + // 打断正在排队的 debounce 定时器,避免与默认请求相互打架 + if (searchTimer) { clearTimeout(searchTimer); searchTimer = null } + handleSearchMaterial('') } const onMaterialSelected = (id: number | null) => { diff --git a/inventory-web/src/views/stock/inbound/buy.vue b/inventory-web/src/views/stock/inbound/buy.vue index baba795..9893ae7 100644 --- a/inventory-web/src/views/stock/inbound/buy.vue +++ b/inventory-web/src/views/stock/inbound/buy.vue @@ -298,7 +298,7 @@ v-model="form.base_id" filterable remote - reserve-keyword + reserve-keyword="true" clearable placeholder="请输入名称或规格进行检索..." :remote-method="handleSearchMaterialDebounced" @@ -306,7 +306,7 @@ :loading="searchLoading" style="width: 100%" @change="onMaterialSelected" - default-first-option + default-first-option="true" v-loadmore="loadMoreMaterials" popper-class="long-dropdown" > @@ -651,8 +651,8 @@ - Preview Image - + Preview Image + - +
Label Preview @@ -1136,7 +1136,16 @@ const querySearchCurrency = (queryString: string, cb: any) => { cb(filtered) } -const handleMaterialDropdownVisible = (visible: boolean) => { if (visible && materialOptions.value.length === 0) handleSearchMaterialDebounced('') } +const handleMaterialDropdownVisible = (visible: boolean) => { + if (!visible) return + // 防御性拦截:竞态条件守卫 + // 如果当前已经有搜索关键字(例如用户刚刚粘贴了内容、remote-method 已经设置了 searchKeyword), + // 绝对不要去请求默认列表,否则会清空 searchKeyword、覆盖正确结果。 + if (searchKeyword.value || materialOptions.value.length > 0) return + // 打断正在排队的 debounce 定时器,避免与默认请求相互打架 + if (searchTimer) { clearTimeout(searchTimer); searchTimer = null } + handleSearchMaterial('') +} const handleSearchMaterialDebounced = (query: string) => { if (searchTimer) clearTimeout(searchTimer) @@ -1146,13 +1155,16 @@ const handleSearchMaterialDebounced = (query: string) => { } const handleSearchMaterial = async (query: string) => { + // 防御性处理:粘贴场景常混入零宽字符 / 控制字符 / 不可见 Unicode + const rawQuery = String(query || '') + const safeQuery = rawQuery.replace(/[\x00-\x1F\x7F-\x9F\u200B-\u200D\uFEFF]/g, '').trim() searchLoading.value = true - searchKeyword.value = query + searchKeyword.value = safeQuery searchPage.value = 1 materialOptions.value = [] try { - const res: any = await searchMaterialBase(query, 1) + const res: any = await searchMaterialBase(safeQuery, 1) if (res.data) { const apiResults = (res.data || []).map((i: any) => ({...i, isHistory: false})) materialOptions.value = apiResults diff --git a/inventory-web/src/views/stock/inbound/service.vue b/inventory-web/src/views/stock/inbound/service.vue index dd8560c..801a69a 100644 --- a/inventory-web/src/views/stock/inbound/service.vue +++ b/inventory-web/src/views/stock/inbound/service.vue @@ -85,6 +85,8 @@ :title="dialogTitle" width="700px" destroy-on-close + :close-on-click-modal="false" + :close-on-press-escape="false" @close="resetDialog" > @@ -103,14 +105,14 @@ v-model="form.base_id" filterable remote - reserve-keyword + reserve-keyword="true" placeholder="输入名称或规格..." :remote-method="handleSearchMaterial" @visible-change="handleMaterialDropdownVisible" :loading="searchLoading" style="width: 100%" @change="onMaterialSelected" - default-first-option + default-first-option="true" > ([]) +const searchKeyword = ref('') const searchLoading = ref(false) const searchForm = reactive({ @@ -329,15 +332,22 @@ const handlePageChange = (val: number) => { } const handleMaterialDropdownVisible = (visible: boolean) => { - if (visible && materialOptions.value.length === 0) { - handleSearchMaterial('') - } + if (!visible) return + // 防御性拦截:竞态条件守卫 + // 如果当前已经有搜索关键字(例如用户刚刚粘贴了内容、remote-method 已经设置了 searchKeyword), + // 绝对不要去请求默认列表,否则会清空 searchKeyword、覆盖正确结果。 + if (searchKeyword.value || materialOptions.value.length > 0) return + handleSearchMaterial('') } const handleSearchMaterial = async (query: string) => { + // 防御性处理:粘贴场景常混入零宽字符 / 控制字符 / 不可见 Unicode + const rawQuery = String(query || '') + const safeQuery = rawQuery.replace(/[\x00-\x1F\x7F-\x9F\u200B-\u200D\uFEFF]/g, '').trim() + searchKeyword.value = safeQuery searchLoading.value = true try { - const res = await searchMaterialBase(query) + const res = await searchMaterialBase(safeQuery) if (res.code === 200) { const apiResults = (res.data || []).map((i: any) => ({ ...i, isHistory: false })) materialOptions.value = apiResults