半成品/成品入库:物料/BOM 远程搜索粘贴失效 Bug 修复(三层防御)
- 深度净化 query:剔除零宽字符(U+200B-U+200D)/BOM(U+FEFF)/控制字符(U+0000-U+001F,U+007F-U+009F),应对外部复制粘贴混入隐形 Unicode 导致 ilike 匹配失败的场景 - 显式 reserve-keyword="true" / default-first-option="true":物料与 BOM 两个 <el-select> 全部显式标注,防止 Element 框架在选择后清空关键字(BOM 下拉框原缺失) - handleMaterialDropdownVisible 竞态守卫:粘贴时 remote-method 与 @visible-change 同时触发,后者会 clearTimeout 前者的 debounce 定时器并加载默认列表覆盖结果。新增 !searchKeyword 守卫 + 主动 clearTimeout 互斥
This commit is contained in:
@ -287,7 +287,7 @@
|
|||||||
v-model="form.base_id"
|
v-model="form.base_id"
|
||||||
filterable
|
filterable
|
||||||
remote
|
remote
|
||||||
reserve-keyword
|
reserve-keyword="true"
|
||||||
clearable
|
clearable
|
||||||
placeholder="请输入名称或规格进行检索..."
|
placeholder="请输入名称或规格进行检索..."
|
||||||
:remote-method="handleSearchMaterial"
|
:remote-method="handleSearchMaterial"
|
||||||
@ -295,7 +295,7 @@
|
|||||||
:loading="searchLoading"
|
:loading="searchLoading"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
@change="onMaterialSelected"
|
@change="onMaterialSelected"
|
||||||
default-first-option
|
default-first-option="true"
|
||||||
v-loadmore="loadMoreMaterials"
|
v-loadmore="loadMoreMaterials"
|
||||||
popper-class="product-dropdown"
|
popper-class="product-dropdown"
|
||||||
>
|
>
|
||||||
@ -460,12 +460,14 @@
|
|||||||
v-model="form.bom_code"
|
v-model="form.bom_code"
|
||||||
filterable
|
filterable
|
||||||
remote
|
remote
|
||||||
|
reserve-keyword="true"
|
||||||
clearable
|
clearable
|
||||||
:disabled="!form.spec_model"
|
:disabled="!form.spec_model"
|
||||||
:placeholder="!form.spec_model ? '请先在上方选择入库物料' : '搜规格/编号'"
|
:placeholder="!form.spec_model ? '请先在上方选择入库物料' : '搜规格/编号'"
|
||||||
:remote-method="handleSearchBom"
|
:remote-method="handleSearchBom"
|
||||||
:loading="bomSearchLoading"
|
:loading="bomSearchLoading"
|
||||||
@change="handleBomSelect"
|
@change="handleBomSelect"
|
||||||
|
default-first-option="true"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
>
|
>
|
||||||
<el-option
|
<el-option
|
||||||
@ -956,9 +958,15 @@ const form = reactive({
|
|||||||
// BOM Search Logic
|
// BOM Search Logic
|
||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
const handleSearchBom = async (query: string) => {
|
const handleSearchBom = 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()
|
||||||
bomSearchLoading.value = true
|
bomSearchLoading.value = true
|
||||||
try {
|
try {
|
||||||
const res: any = await searchBom(query, form.spec_model)
|
const res: any = await searchBom(safeQuery, form.spec_model)
|
||||||
bomOptions.value = res.data || []
|
bomOptions.value = res.data || []
|
||||||
} finally { bomSearchLoading.value = false }
|
} finally { bomSearchLoading.value = false }
|
||||||
}
|
}
|
||||||
@ -1057,7 +1065,17 @@ const rules = {
|
|||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
// Material Search & Population Logic (已修改)
|
// 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) => {
|
const handleSearchMaterialDebounced = (query: string) => {
|
||||||
if (searchTimer) clearTimeout(searchTimer)
|
if (searchTimer) clearTimeout(searchTimer)
|
||||||
@ -1067,13 +1085,19 @@ const handleSearchMaterialDebounced = (query: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleSearchMaterial = async (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
|
searchLoading.value = true
|
||||||
searchKeyword.value = query
|
searchKeyword.value = safeQuery
|
||||||
searchPage.value = 1
|
searchPage.value = 1
|
||||||
materialOptions.value = []
|
materialOptions.value = []
|
||||||
|
|
||||||
try {
|
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 }))
|
const apiResults = (res.data?.items || []).map((i: any) => ({ ...i, isHistory: false }))
|
||||||
materialOptions.value = apiResults
|
materialOptions.value = apiResults
|
||||||
hasNextPage.value = res.data?.has_next ?? false
|
hasNextPage.value = res.data?.has_next ?? false
|
||||||
|
|||||||
@ -322,7 +322,7 @@
|
|||||||
v-model="form.base_id"
|
v-model="form.base_id"
|
||||||
filterable
|
filterable
|
||||||
remote
|
remote
|
||||||
reserve-keyword
|
reserve-keyword="true"
|
||||||
clearable
|
clearable
|
||||||
placeholder="请输入名称或规格进行检索..."
|
placeholder="请输入名称或规格进行检索..."
|
||||||
:remote-method="handleSearchMaterial"
|
:remote-method="handleSearchMaterial"
|
||||||
@ -330,7 +330,7 @@
|
|||||||
:loading="searchLoading"
|
:loading="searchLoading"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
@change="onMaterialSelected"
|
@change="onMaterialSelected"
|
||||||
default-first-option
|
default-first-option="true"
|
||||||
v-loadmore="loadMoreMaterials"
|
v-loadmore="loadMoreMaterials"
|
||||||
popper-class="long-dropdown"
|
popper-class="long-dropdown"
|
||||||
>
|
>
|
||||||
@ -525,12 +525,14 @@
|
|||||||
v-model="form.bom_code"
|
v-model="form.bom_code"
|
||||||
filterable
|
filterable
|
||||||
remote
|
remote
|
||||||
|
reserve-keyword="true"
|
||||||
clearable
|
clearable
|
||||||
:disabled="!form.spec_model"
|
:disabled="!form.spec_model"
|
||||||
:placeholder="!form.spec_model ? '请先在上方选择入库物料' : '搜规格/编号'"
|
:placeholder="!form.spec_model ? '请先在上方选择入库物料' : '搜规格/编号'"
|
||||||
:remote-method="handleSearchBom"
|
:remote-method="handleSearchBom"
|
||||||
:loading="bomSearchLoading"
|
:loading="bomSearchLoading"
|
||||||
@change="handleBomSelect"
|
@change="handleBomSelect"
|
||||||
|
default-first-option="true"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
>
|
>
|
||||||
<el-option
|
<el-option
|
||||||
@ -1003,9 +1005,15 @@ watch(
|
|||||||
// BOM Search Logic
|
// BOM Search Logic
|
||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
const handleSearchBom = async (query: string) => {
|
const handleSearchBom = 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()
|
||||||
bomSearchLoading.value = true
|
bomSearchLoading.value = true
|
||||||
try {
|
try {
|
||||||
const res: any = await searchBom(query, form.spec_model)
|
const res: any = await searchBom(safeQuery, form.spec_model)
|
||||||
bomOptions.value = res.data || []
|
bomOptions.value = res.data || []
|
||||||
} finally { bomSearchLoading.value = false }
|
} finally { bomSearchLoading.value = false }
|
||||||
}
|
}
|
||||||
@ -1051,7 +1059,17 @@ const handleManagerSelect = (item: any) => {
|
|||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
// Material Search (Matches Buy.vue)
|
// 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) => {
|
const handleSearchMaterialDebounced = (query: string) => {
|
||||||
if (searchTimer) clearTimeout(searchTimer)
|
if (searchTimer) clearTimeout(searchTimer)
|
||||||
@ -1061,13 +1079,19 @@ const handleSearchMaterialDebounced = (query: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleSearchMaterial = async (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
|
searchLoading.value = true
|
||||||
searchKeyword.value = query
|
searchKeyword.value = safeQuery
|
||||||
searchPage.value = 1
|
searchPage.value = 1
|
||||||
materialOptions.value = []
|
materialOptions.value = []
|
||||||
|
|
||||||
try {
|
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}))
|
const apiResults = (res.data?.items || []).map((i: any) => ({...i, isHistory: false}))
|
||||||
materialOptions.value = apiResults
|
materialOptions.value = apiResults
|
||||||
hasNextPage.value = res.data?.has_next ?? false
|
hasNextPage.value = res.data?.has_next ?? false
|
||||||
|
|||||||
Reference in New Issue
Block a user