diff --git a/inventory-web/src/views/stock/inbound/product.vue b/inventory-web/src/views/stock/inbound/product.vue index fa6e193..8fe88df 100644 --- a/inventory-web/src/views/stock/inbound/product.vue +++ b/inventory-web/src/views/stock/inbound/product.vue @@ -287,7 +287,7 @@ v-model="form.base_id" filterable remote - reserve-keyword + reserve-keyword="true" clearable placeholder="请输入名称或规格进行检索..." :remote-method="handleSearchMaterial" @@ -295,7 +295,7 @@ :loading="searchLoading" style="width: 100%" @change="onMaterialSelected" - default-first-option + default-first-option="true" v-loadmore="loadMoreMaterials" popper-class="product-dropdown" > @@ -460,12 +460,14 @@ v-model="form.bom_code" filterable remote + reserve-keyword="true" clearable :disabled="!form.spec_model" :placeholder="!form.spec_model ? '请先在上方选择入库物料' : '搜规格/编号'" :remote-method="handleSearchBom" :loading="bomSearchLoading" @change="handleBomSelect" + default-first-option="true" style="width: 100%" > { + // 防御性处理:粘贴场景常混入零宽字符 / 控制字符 / 不可见 Unicode + // 1) 强制转字符串,防 ClipboardEvent 对象 + // 2) 深度净化:剔除所有控制字符、零宽字符、BOM + // 3) 常规 trim + const rawQuery = String(query || '') + const safeQuery = rawQuery.replace(/[\x00-\x1F\x7F-\x9F\u200B-\u200D\uFEFF]/g, '').trim() bomSearchLoading.value = true try { - const res: any = await searchBom(query, form.spec_model) + const res: any = await searchBom(safeQuery, form.spec_model) bomOptions.value = res.data || [] } finally { bomSearchLoading.value = false } } @@ -1057,7 +1065,17 @@ const rules = { // ------------------------------------ // Material Search & Population Logic (已修改) // ------------------------------------ -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) @@ -1067,13 +1085,19 @@ const handleSearchMaterialDebounced = (query: string) => { } const handleSearchMaterial = async (query: string) => { + // 防御性处理:粘贴场景常混入零宽字符 / 控制字符 / 不可见 Unicode + // 1) 强制转字符串,防 ClipboardEvent 对象 + // 2) 深度净化:剔除所有控制字符、零宽字符、BOM + // 3) 常规 trim + 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) const apiResults = (res.data?.items || []).map((i: any) => ({ ...i, isHistory: false })) materialOptions.value = apiResults hasNextPage.value = res.data?.has_next ?? false diff --git a/inventory-web/src/views/stock/inbound/semi.vue b/inventory-web/src/views/stock/inbound/semi.vue index 51a6942..91754c5 100644 --- a/inventory-web/src/views/stock/inbound/semi.vue +++ b/inventory-web/src/views/stock/inbound/semi.vue @@ -322,7 +322,7 @@ v-model="form.base_id" filterable remote - reserve-keyword + reserve-keyword="true" clearable placeholder="请输入名称或规格进行检索..." :remote-method="handleSearchMaterial" @@ -330,7 +330,7 @@ :loading="searchLoading" style="width: 100%" @change="onMaterialSelected" - default-first-option + default-first-option="true" v-loadmore="loadMoreMaterials" popper-class="long-dropdown" > @@ -525,12 +525,14 @@ v-model="form.bom_code" filterable remote + reserve-keyword="true" clearable :disabled="!form.spec_model" :placeholder="!form.spec_model ? '请先在上方选择入库物料' : '搜规格/编号'" :remote-method="handleSearchBom" :loading="bomSearchLoading" @change="handleBomSelect" + default-first-option="true" style="width: 100%" > { + // 防御性处理:粘贴场景常混入零宽字符 / 控制字符 / 不可见 Unicode + // 1) 强制转字符串,防 ClipboardEvent 对象 + // 2) 深度净化:剔除所有控制字符、零宽字符、BOM + // 3) 常规 trim + const rawQuery = String(query || '') + const safeQuery = rawQuery.replace(/[\x00-\x1F\x7F-\x9F\u200B-\u200D\uFEFF]/g, '').trim() bomSearchLoading.value = true try { - const res: any = await searchBom(query, form.spec_model) + const res: any = await searchBom(safeQuery, form.spec_model) bomOptions.value = res.data || [] } finally { bomSearchLoading.value = false } } @@ -1051,7 +1059,17 @@ const handleManagerSelect = (item: any) => { // ------------------------------------ // Material Search (Matches Buy.vue) // ------------------------------------ -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) @@ -1061,13 +1079,19 @@ const handleSearchMaterialDebounced = (query: string) => { } const handleSearchMaterial = async (query: string) => { + // 防御性处理:粘贴场景常混入零宽字符 / 控制字符 / 不可见 Unicode + // 1) 强制转字符串,防 ClipboardEvent 对象 + // 2) 深度净化:剔除所有控制字符、零宽字符、BOM + // 3) 常规 trim + 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) const apiResults = (res.data?.items || []).map((i: any) => ({...i, isHistory: false})) materialOptions.value = apiResults hasNextPage.value = res.data?.has_next ?? false