前端全局:<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:
@ -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 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="dialogVisibleImage" append-to-body width="50%"><img style="width: 100%" :src="dialogImageUrl" alt="Preview Image" /></el-dialog>
|
||||
<el-dialog v-model="cameraDialogVisible" title="拍照上传" width="500px" append-to-body destroy-on-close :close-on-click-modal="false">
|
||||
<el-dialog v-model="dialogVisibleImage" append-to-body width="50%" :close-on-click-modal="false" :close-on-press-escape="false"><img style="width: 100%" :src="dialogImageUrl" alt="Preview Image" /></el-dialog>
|
||||
<el-dialog v-model="cameraDialogVisible" title="拍照上传" width="500px" append-to-body destroy-on-close :close-on-click-modal="false" :close-on-press-escape="false">
|
||||
<WebRtcCamera
|
||||
ref="cameraRef"
|
||||
@photo-submit="handleCameraConfirm"
|
||||
@ -660,7 +660,7 @@
|
||||
/>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="printVisible" title="标签打印预览" width="400px" destroy-on-close append-to-body>
|
||||
<el-dialog v-model="printVisible" title="标签打印预览" width="400px" destroy-on-close append-to-body :close-on-click-modal="false" :close-on-press-escape="false">
|
||||
<div style="text-align: center;">
|
||||
<div v-loading="printLoading" class="preview-box">
|
||||
<img v-if="previewUrl" :src="previewUrl" alt="Label Preview" style="width: 100%; border: 1px solid #ccc;"/>
|
||||
@ -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
|
||||
|
||||
@ -85,6 +85,8 @@
|
||||
:title="dialogTitle"
|
||||
width="700px"
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
@close="resetDialog"
|
||||
>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||||
@ -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"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in materialOptions"
|
||||
@ -269,6 +271,7 @@ const perPage = ref(20)
|
||||
const total = ref(0)
|
||||
|
||||
const materialOptions = ref<any[]>([])
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user