From 8bb3e58b444915499709ade27af2e12688f6ad95 Mon Sep 17 00:00:00 2001 From: DXC Date: Thu, 4 Jun 2026 16:44:59 +0800 Subject: [PATCH] =?UTF-8?q?=E5=89=8D=E7=AB=AF=E5=85=A8=E5=B1=80=EF=BC=9A=20=E4=B8=89=E9=81=93=E9=98=B2=E7=BA=BF?= =?UTF-8?q?=E6=89=A9=E5=B1=95=E5=88=B0=20BOM=20=E9=85=8D=E6=96=B9/?= =?UTF-8?q?=E9=87=87=E8=B4=AD/=E9=87=87=E8=B4=AD=E5=85=A5=E5=BA=93/?= =?UTF-8?q?=E5=94=AE=E5=90=8E=E5=85=A5=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 第一道防线: 模板显式补充 reserve-keyword="true" / default-first-option="true",覆盖 4 文件 5 实例 - 第二道防线:handleRemoteSearch / handleSearchMaterial 首行深度净化 query(零宽字符/控制字符/BOM/不可见 Unicode) - 第三道防线:handleVisibleChange / handleMaterialDropdownVisible 加竞态守卫,已有 searchKeyword 或 options 非空时跳过默认列表加载;带 debounce 的场景主动 clearTimeout 互斥 - service.vue 原本缺少 searchKeyword 状态,本轮新增 ref('') 专供 el-select 守卫使用 - BomManage.vue 父件/子件共用 handleVisibleChange,两套守卫分别按 parentQueryParams.keyword 和 state.queryParams.keyword 隔离判断 --- inventory-web/src/views/bom/BomManage.vue | 21 ++++++++++---- inventory-web/src/views/purchase/index.vue | 28 ++++++++++++------- inventory-web/src/views/stock/inbound/buy.vue | 28 +++++++++++++------ .../src/views/stock/inbound/service.vue | 22 +++++++++++---- 4 files changed, 70 insertions(+), 29 deletions(-) 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