fix(bom): 管理页启停/归档即时同步与界面健壮性大修

- 只读启停用后端权威 bom_no/version,修复改状态报 BOM不存在(400)
- 成功路径统一 await reloadCurrentGroups():先更新分组摘要,再静默覆盖已展开分类明细
- 本地秒移除 applyLocalRowState:按新状态+当前筛选把不符合行立即踢出缓存并强制 Map 换引用
- 接口层与界面层 try 分离:UI 刷新异常不再误报“状态更新失败”,避免红绿/黄提示同弹
- 遍历全部改为 Array.isArray/?.keys()/entries()/||[] 防御,杜绝 undefined 误报
- 兼容 el-collapse 手风琴(String)与多开(Array):新增 isCategoryActive/setActiveList,修复 activeCategories.filter is not a function
This commit is contained in:
yueli
2026-09-08 13:58:48 +08:00
parent 66b3e4897f
commit b4971f057e

View File

@ -41,7 +41,7 @@
:title="group.category + ' (' + group.count + ')'"
:name="group.category"
>
<el-table v-if="activeCategories.includes(group.category)" :data="group.items" border style="width: 100%">
<el-table v-if="isCategoryActive(group.category)" :data="group.items" border style="width: 100%">
<el-table-column v-if="hasColumnPermission('bom_no')" prop="bom_no" label="BOM编号" min-width="180" show-overflow-tooltip>
<template #default="{ row }">
<span style="cursor: pointer; color: #409EFF;" @click="handleView(row)">{{ row.bom_no }}</span>
@ -441,7 +441,15 @@ interface GroupSummary { category: string; count: number }
const groupSummary = ref<GroupSummary[]>([])
const groupCache = ref<Map<string, any[]>>(new Map())
const groupLoadingMap = ref<Map<string, boolean>>(new Map())
const activeCategories = ref<string[]>([])
// ★ el-collapse 手风琴(accordion=true)时 v-model 是单个 String多开时才是 Array —— 统一按数组读写
const activeCategories = ref<string | string[]>([])
const activeList = (): string[] =>
Array.isArray(activeCategories.value) ? activeCategories.value : (activeCategories.value ? [activeCategories.value] : [])
const isCategoryActive = (c: string) => activeList().includes(c)
const setActiveList = (arr: string[]) => {
if (isAccordion.value) activeCategories.value = arr.length ? arr[0] : ''
else activeCategories.value = arr
}
const lastKeyword = ref('')
const groupedData = computed(() => {
@ -646,9 +654,9 @@ const rules = reactive<FormRules>({
const dialogTitle = ref('新建 BOM')
const handleSearch = () => {
activeCategories.value = []
groupCache.value = new Map()
isAccordion.value = true // 搜索/筛选重置回默认手风琴模式
groupCache.value = new Map()
setActiveList([]) // 手风琴下折叠为 '',多开下折叠为 []
fetchBomSummary()
}
@ -664,7 +672,7 @@ const fetchBomSummary = async () => {
if (res?.code === 200) {
groupSummary.value = res.data ?? []
if (searchKeyword.value !== lastKeyword.value) {
activeCategories.value = []
setActiveList([])
lastKeyword.value = searchKeyword.value
}
}
@ -680,15 +688,14 @@ const expandAllGroups = () => {
}
// ★ 全部折叠:清空面板,并恢复手风琴模式
const collapseAllGroups = () => {
activeCategories.value = []
isAccordion.value = true
setActiveList([])
}
// ★ 展开分组时懒加载该分类下的 BOM
const loadGroupItems = async (category: string) => {
if (groupCache.value.has(category)) return
if (groupLoadingMap.value.get(category)) return
// ★ 静默获取单个分类的最新数据并“覆盖”缓存:
// 不清空该分类缓存、请求失败则保留旧数据(保证面板绝不闪白/变空)。
const fetchCategoryChildren = async (category: string): Promise<boolean> => {
if (groupLoadingMap.value.get(category)) return false
groupLoadingMap.value.set(category, true)
try {
const params: any = {
@ -703,8 +710,19 @@ const loadGroupItems = async (category: string) => {
if (res?.code === 200) {
groupCache.value.set(category, res.data?.items ?? [])
}
} catch (e) { console.error(`加载BOM分类 [${category}] 失败`, e) }
finally { groupLoadingMap.value.set(category, false) }
return true
} catch (e) {
console.error(`加载BOM分类 [${category}] 失败`, e)
return false
} finally {
groupLoadingMap.value.set(category, false)
}
}
// ★ 展开分组时懒加载该分类下的 BOM仅在无缓存时拉取
const loadGroupItems = async (category: string) => {
if (groupCache.value.has(category)) return
await fetchCategoryChildren(category)
}
// ★ collapse @change 事件 → 触发懒加载
@ -713,6 +731,47 @@ const handleCollapseChange = (val: string | string[]) => {
cats.forEach(c => loadGroupItems(c))
}
// ★ 启停/归档/删除等行操作后的平滑刷新:
// - 绝不清空整体缓存,也绝不清空/关闭任何已展开面板;
// - 先更新分组摘要计数,再对“当前已展开”的分类逐个静默重取最新数据覆盖缓存;
// - 不符合当前筛选的行会因重取自然消失、符合的保留;单次请求失败则沿用旧数据,面板不闪白。
const reloadCurrentGroups = async () => {
// 1. 获取最新分组摘要
await fetchBomSummary();
// 2. 有效分类集合(防御:摘要可能为空)
const validCategoryNames = (groupSummary.value || []).map(g => g && g.category);
// 3. 兼容手风琴(String)与多开(Array),统一转数组;再过滤掉已归零分类
const currentActiveList = Array.isArray(activeCategories.value)
? activeCategories.value
: (activeCategories.value ? [activeCategories.value] : []);
const validActiveList = currentActiveList.filter(c => validCategoryNames.includes(c));
// 4. 按当前模式安全写回(手风琴只保留第一个 → 触发折叠动画;多开写回数组)
if (isAccordion.value) {
activeCategories.value = validActiveList.length > 0 ? validActiveList[0] : '';
} else {
activeCategories.value = validActiveList;
}
// 5. 确定要刷新的打开面板
const opened = [...validActiveList];
// 6. 暴力清理不需要的旧缓存(遍历 cache 的 keys而不是 summary
for (const key of Array.from((groupCache.value && groupCache.value.keys ? groupCache.value.keys() : []) || [])) {
if (!opened.includes(key)) {
groupCache.value.delete(key);
}
}
// 7. 重新拉取仍在打开状态的分类明细
await Promise.all((opened || []).map(c => fetchCategoryChildren(c)));
// 8. [关键] 强制触发 Vue 对 Map 的深度响应式更新
groupCache.value = new Map(groupCache.value || new Map());
}
const onParentChange = (val: number) => {}
const onChildChange = (val: number | null, index: number) => {
@ -989,14 +1048,11 @@ const loadDetail = async (bomNo: string, version: string) => {
parentNameInput.value = data.parent_name || '未知产品'
}
if (data.parent_spec) {
form.bom_no = (data.parent_spec || '').split('/')[0].trim()
} else {
form.bom_no = bomNo.split('/')[0].trim()
}
// ★ 只读视图的状态开关必须用后端返回的权威 bom_no/version
// 不要再用 parent_spec 首位 token 去反推(规格首位可能不等于 BOM 编号 → /status 查不到 → 400
form.bom_no = data.bom_no || bomNo
form.remark = data.remark || ''
// 查看/只读视图的独立启停需要版本与当前状态
form.version = version || data.version || 'V1.0'
form.version = data.version || version || 'V1.0'
form.is_enabled = data.is_enabled !== false
}
} catch (e) {
@ -1006,34 +1062,70 @@ const loadDetail = async (bomNo: string, version: string) => {
}
}
// ★ 前端本地“秒移除”:后端确认成功后,按“新状态 + 当前筛选”判断该行是否还可见,
// 不可见则立即从各分类缓存数组中踢掉并强制 Map 换引用(不等明细接口,列表立刻消失)
const applyLocalRowState = (bomNo: string, version: string, patch: { is_enabled?: boolean; is_archived?: boolean }) => {
const status = bomStatusFilter.value
let changed = false
for (const [, list] of Array.from((groupCache.value && groupCache.value.entries ? groupCache.value.entries() : []) || [])) {
if (!Array.isArray(list)) continue // 防御:缓存某项可能为空
const idx = list.findIndex((i: any) => i && i.bom_no === bomNo && i.version === version)
if (idx === -1) continue
const item = list[idx]
if (!item) continue
if (patch.is_enabled !== undefined) item.is_enabled = patch.is_enabled
if (patch.is_archived !== undefined) item.is_archived = patch.is_archived
let visible = true
if (status === 'enabled') visible = !!item.is_enabled && !item.is_archived
else if (status === 'disabled') visible = item.is_enabled === false
else if (status === 'archived') visible = !!item.is_archived
if (!visible) { list.splice(idx, 1); changed = true }
}
if (changed) groupCache.value = new Map(groupCache.value)
}
// ★ 启用/停用开关:查看/只读态下独立更新状态(不触发整表单保存);
// 新建/编辑/另存模式则仅记录到 form随 submitForm 一并保存。
const onEnabledStatusChange = async () => {
if (!isReadOnlyMode.value) return
if (!pureBomNo.value || !form.version) {
const curBomNo = String(form.bom_no || '').trim()
const curVersion = String(form.version || '').trim()
if (!curBomNo || !curVersion) {
ElMessage.warning('缺少 BOM 编号/版本,无法更新状态')
return
}
const target = !!form.is_enabled
// ① 接口层:只负责提交;只有这一层失败才提示“状态更新失败”
saving.value = true
try {
const res: any = await updateBomStatus({ bom_no: pureBomNo.value, version: form.version, is_enabled: target })
const res: any = await updateBomStatus({ bom_no: curBomNo, version: curVersion, is_enabled: target })
if (res?.code === 200) {
ElMessage.success(target ? '已启用' : '已停用')
// 刷新左侧列表摘要(计数与状态列)
groupCache.value = new Map()
fetchBomSummary()
form.is_enabled = target
} else {
form.is_enabled = !target
ElMessage.error(res?.msg || '状态更新失败')
return
}
} catch (e) {
console.error('启用/停用接口异常:', e)
form.is_enabled = !target
ElMessage.error('状态更新失败')
ElMessage.error((e as any)?.response ? '状态更新失败' : '网络异常,状态可能未更新,请重试')
return
} finally {
saving.value = false
}
// ② 界面层:状态已成功,本地秒移除 + 整体同步;此处异常绝不回退成“状态更新失败”
try {
applyLocalRowState(curBomNo, curVersion, { is_enabled: target })
await reloadCurrentGroups()
} catch (e) {
console.error('状态已更新,但界面刷新异常:', e)
ElMessage.warning('状态已更新,界面刷新异常,请手动刷新查看')
}
}
// ★ 归档/取消归档:仍启用可编辑,但不再作为其它 BOM 子件的版本候选
@ -1045,25 +1137,60 @@ const handleArchive = async (row: any) => {
try {
await ElMessageBox.confirm(tip, '提示', { type: 'warning' })
} catch (e) { return }
// ① 接口层
try {
const res: any = await updateBomArchive({ bom_no: row.bom_no, version: row.version, is_archived: target })
if (res?.code === 200) {
ElMessage.success(target ? '已归档' : '已取消归档')
groupCache.value = new Map()
fetchBomSummary()
} else {
ElMessage.error(res?.msg || '操作失败')
return
}
} catch (e) {
console.error('归档接口异常:', e)
ElMessage.error((e as any)?.response ? '操作失败' : '网络异常,请重试')
return
}
// ② 界面层
try {
applyLocalRowState(row.bom_no, row.version, { is_archived: target })
await reloadCurrentGroups()
} catch (e) {
console.error('归档成功,但界面刷新异常:', e)
ElMessage.warning('操作已生效,界面刷新异常,请手动刷新查看')
}
} catch (e) { ElMessage.error('归档操作失败') }
}
const handleDelete = (row: BomItem) => {
ElMessageBox.confirm(`确定删除 ${row.bom_no} (${row.version}) 吗?`, '警告', { type: 'warning' })
.then(async () => {
let deleted = false
try {
const res = await deleteBom(row.bom_no, row.version)
if (res.code === 200) { ElMessage.success('删除成功'); groupCache.value = new Map(); fetchBomSummary() }
} catch (e) {}
if (res.code === 200) {
ElMessage.success('删除成功')
// 删除不受筛选限制:本地移除该行(带数组守卫)
for (const [, list] of Array.from((groupCache.value && groupCache.value.entries ? groupCache.value.entries() : []) || [])) {
if (!Array.isArray(list)) continue
const i = list.findIndex((it: any) => it && it.bom_no === row.bom_no && it.version === row.version)
if (i > -1) list.splice(i, 1)
}
groupCache.value = new Map(groupCache.value)
deleted = true
}
} catch (e) {
console.error('删除接口异常:', e)
ElMessage.error((e as any)?.response ? '删除失败' : '网络异常,请重试')
}
if (deleted) {
try {
await reloadCurrentGroups()
} catch (e) {
console.error('删除成功,但界面刷新异常:', e)
ElMessage.warning('已删除,界面刷新异常,请手动刷新查看')
}
}
})
.catch(() => {})
}
@ -1157,8 +1284,8 @@ const submitForm = async () => {
localStorage.removeItem('pending_bom_draft_version')
originalDraftHash.value = ''
dialogVisible.value = false
groupCache.value = new Map()
fetchBomSummary()
// 关窗后仅重载已展开分类,避免列表空白
reloadCurrentGroups()
} else {
ElMessage.error(res.msg || '保存失败')
}