fix: BOM子件下拉修复回显丢失和索引错位问题
This commit is contained in:
@ -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 () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user