fix: BOM子件下拉修复回显丢失和索引错位问题

This commit is contained in:
DXC
2026-05-21 17:14:36 +08:00
parent 621431dcb9
commit baaaf7799a

View File

@ -164,7 +164,7 @@
<el-table :data="filteredChildren" border style="width: 100%; margin-bottom: 15px" max-height="300"> <el-table :data="filteredChildren" border style="width: 100%; margin-bottom: 15px" max-height="300">
<el-table-column label="子件物料" min-width="250" v-if="hasFormFieldPermission('child_id')"> <el-table-column label="子件物料" min-width="250" v-if="hasFormFieldPermission('child_id')">
<template #default="{ row, $index }"> <template #default="{ row }">
<!-- ====== 改造:子件下拉 - 远程搜索 + 懒加载 ====== --> <!-- ====== 改造:子件下拉 - 远程搜索 + 懒加载 ====== -->
<div style="display: flex; align-items: center; gap: 8px;"> <div style="display: flex; align-items: center; gap: 8px;">
<el-select <el-select
@ -174,14 +174,14 @@
remote remote
reserve-keyword reserve-keyword
style="flex: 1;" style="flex: 1;"
:remote-method="(q: string) => handleRemoteSearch(q, 'child', $index)" :remote-method="(q: string) => handleRemoteSearch(q, 'child', row.rowKey)"
:loading="selectLoading" :loading="selectLoading"
:loading-text="`正在加载第 ${childQueryParams.page} 页...`" :loading-text="`正在加载第 ${childQueryParams.page} 页...`"
:popper-class="`bom-loadmore-popper child-popper-${$index}`" :popper-class="`bom-loadmore-popper child-popper-${row.rowKey}`"
@visible-change="(visible: boolean) => handleVisibleChange(visible, 'child', $index)" @visible-change="(visible: boolean) => handleVisibleChange(visible, 'child', row.rowKey)"
> >
<el-option <el-option
v-for="item in getChildOptions($index)" v-for="item in getChildOptions(row.rowKey)"
:key="item.id" :key="item.id"
:label="`${item.name} (${item.spec})`" :label="`${item.name} (${item.spec})`"
:value="item.id" :value="item.id"
@ -197,7 +197,7 @@
type="primary" type="primary"
link link
:icon="EditPen" :icon="EditPen"
@click.stop="openMaterialInNewTab(row.child_id, getChildSpec($index))" @click.stop="openMaterialInNewTab(row.child_id, getChildSpec(row.rowKey))"
style="font-size: 16px; padding: 4px;" style="font-size: 16px; padding: 4px;"
/> />
</el-tooltip> </el-tooltip>
@ -218,8 +218,8 @@
</el-table-column> </el-table-column>
<el-table-column label="操作" width="60" align="center" v-if="userStore.hasPermission('bom_manage:operation')"> <el-table-column label="操作" width="60" align="center" v-if="userStore.hasPermission('bom_manage:operation')">
<template #default="{ $index }"> <template #default="{ row }">
<el-button type="danger" link @click="removeChild($index)">删</el-button> <el-button type="danger" link @click="removeChild(row.rowKey)">删</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@ -266,6 +266,7 @@ interface MaterialBase {
spec: string spec: string
} }
interface ChildRow { interface ChildRow {
rowKey: number // 唯一标识,替代 $index 作为 Map key
child_id: number | null child_id: number | null
dosage: number dosage: number
remark: string remark: string
@ -321,15 +322,15 @@ const getChildOptions = (index: number): MaterialBase[] => {
// ============================================================ // ============================================================
const fetchMaterialOptions = async ( const fetchMaterialOptions = async (
type: 'parent' | 'child', type: 'parent' | 'child',
index?: number, rowKey?: number,
isLoadMore = false isLoadMore = false
) => { ) => {
// 子件行需要 index // 子件行需要 rowKey唯一标识不再依赖数组索引
if (type === 'child' && index === undefined) return if (type === 'child' && rowKey === undefined) return
const params = type === 'parent' const params = type === 'parent'
? parentQueryParams ? parentQueryParams
: childDropdownStates.value.get(index!)?.queryParams : childDropdownStates.value.get(rowKey!)?.queryParams
if (!params) return if (!params) return
@ -353,12 +354,19 @@ const fetchMaterialOptions = async (
const newItems = list.filter(m => !existingIds.has(m.id)) const newItems = list.filter(m => !existingIds.has(m.id))
parentOptions.value.push(...newItems) parentOptions.value.push(...newItems)
} else { } else {
parentOptions.value = list // ★ 修复回显丢失:先检查当前选中项是否在列表中,不在则从原 options 保留
const selectedId = form.parent_id
let finalList = [...list]
if (selectedId && !list.find(m => m.id === selectedId)) {
const existing = parentOptions.value.find(m => m.id === selectedId)
if (existing) finalList.unshift(existing)
}
parentOptions.value = finalList
} }
// 判断是否还有更多数据 // 判断是否还有更多数据
parentHasMore.value = parentOptions.value.length < total parentHasMore.value = parentOptions.value.length < total
} else { } else {
const state = childDropdownStates.value.get(index!) const state = childDropdownStates.value.get(rowKey!)
if (!state) return if (!state) return
if (isLoadMore) { if (isLoadMore) {
@ -366,7 +374,15 @@ const fetchMaterialOptions = async (
const newItems = list.filter(m => !existingIds.has(m.id)) const newItems = list.filter(m => !existingIds.has(m.id))
state.options.push(...newItems) state.options.push(...newItems)
} else { } else {
state.options = list // ★ 修复回显丢失:先通过 rowKey 精准找到当前行,再检查选中项是否在列表中
const currentRow = form.children.find(c => c.rowKey === rowKey)
const currentSelectedId = currentRow?.child_id
let finalList = [...list]
if (currentSelectedId && !list.find(m => m.id === currentSelectedId)) {
const existing = state.options.find(m => m.id === currentSelectedId)
if (existing) finalList.unshift(existing)
}
state.options = finalList
} }
state.hasMore = state.options.length < total state.hasMore = state.options.length < total
} }
@ -384,27 +400,27 @@ const fetchMaterialOptions = async (
const handleRemoteSearch = ( const handleRemoteSearch = (
query: string, query: string,
type: 'parent' | 'child', type: 'parent' | 'child',
index?: number rowKey?: number
) => { ) => {
if (type === 'parent') { if (type === 'parent') {
parentQueryParams.keyword = query parentQueryParams.keyword = query
parentQueryParams.page = 1 parentQueryParams.page = 1
parentHasMore.value = true parentHasMore.value = true
fetchMaterialOptions('parent') fetchMaterialOptions('parent')
} else if (type === 'child' && index !== undefined) { } else if (type === 'child' && rowKey !== undefined) {
const state = childDropdownStates.value.get(index) const state = childDropdownStates.value.get(rowKey)
if (!state) return if (!state) return
state.queryParams.keyword = query state.queryParams.keyword = query
state.queryParams.page = 1 state.queryParams.page = 1
state.hasMore = true state.hasMore = true
fetchMaterialOptions('child', index) fetchMaterialOptions('child', rowKey)
} }
} }
// ============================================================ // ============================================================
// 【改造】下拉框展开/收起处理(重置分页 + 预加载第一页) // 【改造】下拉框展开/收起处理(重置分页 + 预加载第一页)
// ============================================================ // ============================================================
const handleVisibleChange = (visible: boolean, type: 'parent' | 'child', index?: number) => { const handleVisibleChange = (visible: boolean, type: 'parent' | 'child', rowKey?: number) => {
if (!visible) return if (!visible) return
if (type === 'parent') { if (type === 'parent') {
@ -412,20 +428,20 @@ const handleVisibleChange = (visible: boolean, type: 'parent' | 'child', index?:
parentQueryParams.keyword = '' parentQueryParams.keyword = ''
parentHasMore.value = true parentHasMore.value = true
fetchMaterialOptions('parent') fetchMaterialOptions('parent')
} else if (type === 'child' && index !== undefined) { } else if (type === 'child' && rowKey !== undefined) {
// 确保该行下拉状态已初始化 // 确保该行下拉状态已初始化
if (!childDropdownStates.value.has(index)) { if (!childDropdownStates.value.has(rowKey)) {
childDropdownStates.value.set(index, { childDropdownStates.value.set(rowKey, {
options: [], options: [],
queryParams: { page: 1, limit: PAGE_SIZE, keyword: '' }, queryParams: { page: 1, limit: PAGE_SIZE, keyword: '' },
hasMore: true hasMore: true
}) })
} }
const state = childDropdownStates.value.get(index)! const state = childDropdownStates.value.get(rowKey)!
state.queryParams.page = 1 state.queryParams.page = 1
state.queryParams.keyword = '' state.queryParams.keyword = ''
state.hasMore = true state.hasMore = true
fetchMaterialOptions('child', index) fetchMaterialOptions('child', rowKey)
} }
// 延迟 50ms 等待弹窗 DOM 完全渲染 // 延迟 50ms 等待弹窗 DOM 完全渲染
@ -433,7 +449,7 @@ const handleVisibleChange = (visible: boolean, type: 'parent' | 'child', index?:
// 动态拼接精确的选择器 // 动态拼接精确的选择器
const exactSelector = type === 'parent' const exactSelector = type === 'parent'
? '.parent-popper .el-select-dropdown__wrap' ? '.parent-popper .el-select-dropdown__wrap'
: `.child-popper-${index} .el-select-dropdown__wrap`; : `.child-popper-${rowKey} .el-select-dropdown__wrap`;
const popperWrap = document.querySelector(exactSelector) as HTMLElement; const popperWrap = document.querySelector(exactSelector) as HTMLElement;
@ -450,9 +466,9 @@ const handleVisibleChange = (visible: boolean, type: 'parent' | 'child', index?:
if (scrollHeight - scrollTop - clientHeight <= 10) { if (scrollHeight - scrollTop - clientHeight <= 10) {
if (type === 'parent') { if (type === 'parent') {
loadMoreParent(); loadMoreParent();
} else if (type === 'child' && index !== undefined) { } else if (type === 'child' && rowKey !== undefined) {
// 触发子件加载 // 触发子件加载
loadMoreChild(popperWrap, index); loadMoreChild(popperWrap, rowKey);
} }
} }
}; };
@ -470,20 +486,20 @@ const loadMoreParent = () => {
fetchMaterialOptions('parent', undefined, true) fetchMaterialOptions('parent', undefined, true)
} }
const loadMoreChild = (_el: HTMLElement, index: number) => { const loadMoreChild = (_el: HTMLElement, rowKey: number) => {
const state = childDropdownStates.value.get(index) const state = childDropdownStates.value.get(rowKey)
if (!state) return if (!state) return
if (selectLoading.value || !state.hasMore) return if (selectLoading.value || !state.hasMore) return
state.queryParams.page++ state.queryParams.page++
fetchMaterialOptions('child', index, true) fetchMaterialOptions('child', rowKey, true)
} }
// ============================================================ // ============================================================
// 【改造】初始化子件行下拉状态 // 【改造】初始化子件行下拉状态
// ============================================================ // ============================================================
const initChildDropdownState = (index: number) => { const initChildDropdownState = (rowKey: number) => {
if (!childDropdownStates.value.has(index)) { if (!childDropdownStates.value.has(rowKey)) {
childDropdownStates.value.set(index, { childDropdownStates.value.set(rowKey, {
options: [], options: [],
queryParams: { page: 1, limit: PAGE_SIZE, keyword: '' }, queryParams: { page: 1, limit: PAGE_SIZE, keyword: '' },
hasMore: true hasMore: true
@ -500,7 +516,7 @@ const filteredChildren = computed(() => {
} }
const kw = childSearchKeyword.value.toLowerCase() const kw = childSearchKeyword.value.toLowerCase()
return form.children.filter(child => { return form.children.filter(child => {
const state = childDropdownStates.value.get(form.children.indexOf(child)) const state = childDropdownStates.value.get(child.rowKey)
const material = state?.options.find(m => m.id === child.child_id) const material = state?.options.find(m => m.id === child.child_id)
if (!material) return false if (!material) return false
const name = (material.name || '').toLowerCase() const name = (material.name || '').toLowerCase()
@ -510,10 +526,11 @@ const filteredChildren = computed(() => {
}) })
// 获取子件规格(从 childDropdownStates 缓存中查找) // 获取子件规格(从 childDropdownStates 缓存中查找)
const getChildSpec = (index: number): string => { const getChildSpec = (rowKey: number): string => {
const state = childDropdownStates.value.get(index) const state = childDropdownStates.value.get(rowKey)
if (!state || !form.children[index]?.child_id) return '' const row = form.children.find(c => c.rowKey === rowKey)
const material = state.options.find((m: MaterialBase) => m.id === form.children[index].child_id) if (!state || !row?.child_id) return ''
const material = state.options.find((m: MaterialBase) => m.id === row.child_id)
return material?.spec || '' return material?.spec || ''
} }
@ -677,8 +694,9 @@ const loadDetail = async (bomNo: string, version: string) => {
const res = await getBomDetail(bomNo, version) const res = await getBomDetail(bomNo, version)
if (res.code === 200) { if (res.code === 200) {
const data = res.data const data = res.data
// 1. 映射子件基本数据 // 1. 映射子件基本数据(使用 idx 生成唯一 rowKey
form.children = data.children.map((child: any) => ({ form.children = data.children.map((child: any, idx: number) => ({
rowKey: idx, // 用数组索引作为唯一标识(编辑场景下不会增删行)
child_id: child.child_id, child_id: child.child_id,
dosage: child.dosage, dosage: child.dosage,
remark: child.remark || '' remark: child.remark || ''
@ -686,7 +704,7 @@ const loadDetail = async (bomNo: string, version: string) => {
// 2. 初始化子件下拉状态,并预填充 options 解决回显显示 ID 的问题 // 2. 初始化子件下拉状态,并预填充 options 解决回显显示 ID 的问题
form.children.forEach((child, idx) => { form.children.forEach((child, idx) => {
initChildDropdownState(idx) initChildDropdownState(idx) // rowKey === idx编辑场景下唯一
if (child.child_id) { if (child.child_id) {
const state = childDropdownStates.value.get(idx)! const state = childDropdownStates.value.get(idx)!
@ -755,26 +773,17 @@ const resetForm = () => {
} }
const addChild = () => { const addChild = () => {
const idx = form.children.length const rowKey = Date.now() // 生成唯一标识,不再使用数组长度
form.children.push({ child_id: null, dosage: 0, remark: '' }) form.children.push({ rowKey, child_id: null, dosage: 0, remark: '' })
initChildDropdownState(idx) initChildDropdownState(rowKey)
} }
const removeChild = (idx: number) => { const removeChild = (rowKey: number) => {
form.children.splice(idx, 1) // 通过 rowKey 找到并删除该行(不再依赖数组索引)
// 清理该行下拉状态(需要重新索引后续行的状态) const idx = form.children.findIndex(c => c.rowKey === rowKey)
rebuildChildDropdownStates() if (idx !== -1) form.children.splice(idx, 1)
} // 直接删除该行的下拉状态(无需重建索引)
childDropdownStates.value.delete(rowKey)
// 重建子件下拉状态索引(删除行后需要重新编号)
const rebuildChildDropdownStates = () => {
const newMap = new Map<number, ChildDropdownState>()
form.children.forEach((_, idx) => {
if (childDropdownStates.value.has(idx)) {
newMap.set(idx, childDropdownStates.value.get(idx)!)
}
})
childDropdownStates.value = newMap
} }
const submitForm = async () => { const submitForm = async () => {