前端全局:<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

@ -64,7 +64,7 @@
/>
<!-- ========== 新建/编辑弹窗 ========== -->
<el-dialog v-model="formDialogVisible" :title="dialogTitle" width="700px" destroy-on-close :close-on-click-modal="false">
<el-dialog v-model="formDialogVisible" :title="dialogTitle" width="700px" destroy-on-close :close-on-click-modal="false" :close-on-press-escape="false">
<el-form ref="formRef" :model="form" label-width="110px">
<el-row :gutter="20">
@ -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 @@
</el-dialog>
<!-- ========== 详情弹窗 ========== -->
<el-dialog v-model="detailDialogVisible" title="采购申请详情" width="700px" destroy-on-close>
<el-dialog v-model="detailDialogVisible" title="采购申请详情" width="700px" destroy-on-close :close-on-click-modal="false" :close-on-press-escape="false">
<el-descriptions :column="2" border>
<el-descriptions-item label="申请单号">{{ detail.request_no }}</el-descriptions-item>
<el-descriptions-item label="状态">
@ -215,7 +215,7 @@
</el-dialog>
<!-- ========== 驳回原因弹窗 ========== -->
<el-dialog v-model="rejectDialogVisible" title="驳回申请" width="480px" destroy-on-close>
<el-dialog v-model="rejectDialogVisible" title="驳回申请" width="480px" destroy-on-close :close-on-click-modal="false" :close-on-press-escape="false">
<el-form label-width="80px">
<el-form-item label="申请单号">
<span style="font-weight: bold; color: #409EFF;">{{ currentRejectRow?.request_no }}</span>
@ -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) => {