前端全局:<el-select remote> 三道防线扩展到 BOM 配方/采购/采购入库/售后入库

- 第一道防线:<el-select> 模板显式补充 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 隔离判断
This commit is contained in:
DXC
2026-06-04 16:44:59 +08:00
parent cdac915a4b
commit 8bb3e58b44
4 changed files with 70 additions and 29 deletions

View File

@ -63,7 +63,7 @@
</div>
</el-card>
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="850px" destroy-on-close :close-on-click-modal="false">
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="850px" destroy-on-close :close-on-click-modal="false" :close-on-press-escape="false">
<el-form :model="form" label-width="120px" ref="formRef" :rules="rules">
<el-row :gutter="20">
@ -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)"
>
<el-option
@ -413,15 +415,18 @@ const handleRemoteSearch = (
type: 'parent' | 'child',
rowKey?: number
) => {
// 防御性处理:粘贴场景常混入零宽字符 / 控制字符 / 不可见 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