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:
@ -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 || '保存失败')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user