fix: BOM草稿模块缺陷修复(事务回滚 + 外键约束 + 前端状态清理)

This commit is contained in:
DXC
2026-06-10 11:30:07 +08:00
parent 0e6d294052
commit c7b84ff3c6
7 changed files with 507 additions and 35 deletions

View File

@ -42,3 +42,22 @@ export function deleteBom(bomNo: string, version: string) {
method: 'delete'
})
}
// ==========================================
// BOM 草稿相关接口
// ==========================================
// 1. 暂存草稿
export function saveDraft(data: any) {
return request({ url: '/v1/bom/draft/save', method: 'post', data })
}
// 2. 读取草稿详情
export function getDraftDetail(params: { bom_no: string; version?: string }) {
return request({ url: '/v1/bom/draft/detail', method: 'get', params })
}
// 3. 发布草稿
export function publishDraft(data: { bom_no: string; version: string }) {
return request({ url: '/v1/bom/draft/publish', method: 'post', data })
}

View File

@ -223,7 +223,25 @@
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">{{ isReadOnlyMode ? '关闭' : '取消' }}</el-button>
<el-button v-if="!isReadOnlyMode" type="primary" :loading="saving" @click="submitForm">保存</el-button>
<el-button
v-if="!isReadOnlyMode"
type="warning"
plain
:loading="saving"
@click="saveDraftData"
>
暂存草稿
</el-button>
<el-button
v-if="!isReadOnlyMode"
type="primary"
:loading="saving"
@click="submitForm"
>
保 存
</el-button>
</span>
</template>
</el-dialog>
@ -236,7 +254,7 @@ import { useRoute } from 'vue-router'
import { ElMessage, ElMessageBox, FormInstance, FormRules } from 'element-plus'
import { Plus, Search, EditPen } from '@element-plus/icons-vue'
import { useRouter } from 'vue-router'
import { getBomList, getBomDetail, saveBom, deleteBom } from '@/api/bom'
import { getBomList, getBomDetail, saveBom, deleteBom, getDraftDetail, saveDraft, publishDraft } from '@/api/bom'
import { searchMaterialBase } from '@/api/inbound/buy'
import { useUserStore } from '@/stores/user'
@ -265,6 +283,22 @@ interface ChildRow {
remark: string
}
const originalDraftHash = ref('')
const getDraftHash = () => {
const children = form.children.map(c => ({
child_id: c.child_id,
dosage: Number(c.dosage) || 0,
remark: c.remark || ''
}))
return JSON.stringify({
bom_no: form.bom_no,
version: form.version,
parent_id: form.parent_id,
children
})
}
const userStore = useUserStore()
const route = useRoute()
const router = useRouter()
@ -277,6 +311,8 @@ const isReadOnlyMode = ref(false)
let originalVersion = ''
let currentBomNo = ''
let originalChildren: ChildRow[] = []
let pendingDraftBomNo = ''
let pendingDraftVersion = ''
const bomGroups = ref([]) // 分组结构: [{category, count, items[]}]
const activeCategories = ref([]) // 默认全部展开
@ -493,13 +529,152 @@ const onChildChange = (val: number | null, index: number) => {
}
}
const handleCreate = () => {
resetForm()
dialogTitle.value = '新建 BOM'
isEditMode.value = false
isSaveAsMode.value = false
isReadOnlyMode.value = false
dialogVisible.value = true
const getOrGenerateTempBomNo = (): string => {
let storedBomNo = localStorage.getItem('pending_bom_draft_no')
if (storedBomNo) {
return storedBomNo
}
const ts = new Date().toISOString().replace(/[-T:.Z]/g, '').slice(0, 14)
const uid = Math.random().toString(36).slice(2, 10)
const newBomNo = `DRAFT-TEMP-${ts}-${uid}`
localStorage.setItem('pending_bom_draft_no', newBomNo)
return newBomNo
}
const handleCreate = async () => {
const tempBomNo = getOrGenerateTempBomNo()
pendingDraftBomNo = tempBomNo
pendingDraftVersion = 'V1.0'
try {
const res = await getDraftDetail({ bom_no: pendingDraftBomNo, version: pendingDraftVersion })
if (res.code === 200 && res.data) {
const confirm = await ElMessageBox.confirm(
'检测到未发布的草稿,是否恢复继续编辑?',
'草稿恢复',
{ confirmButtonText: '恢复草稿', cancelButtonText: '放弃草稿', type: 'info' }
).catch(() => null)
if (confirm) {
restoreDraftToForm(res.data)
dialogTitle.value = '新建 BOM'
isEditMode.value = false
isSaveAsMode.value = false
isReadOnlyMode.value = false
dialogVisible.value = true
return
} else {
resetForm()
}
}
} catch (e) {}
resetForm()
dialogTitle.value = '新建 BOM'
isEditMode.value = false
isSaveAsMode.value = false
isReadOnlyMode.value = false
dialogVisible.value = true
}
const saveDraftData = async () => {
const draftBomNo = form.bom_no || getOrGenerateTempBomNo()
const currentHash = getDraftHash()
// 场景 A之前已经存过/恢复过草稿,且没有任何改动
if (originalDraftHash.value && currentHash === originalDraftHash.value) {
return ElMessage.warning('草稿数据无变动,无需重复暂存')
}
// 场景 B已经存过草稿且发生了改动 -> 询问覆盖还是新建
if (originalDraftHash.value && currentHash !== originalDraftHash.value) {
try {
const action = await ElMessageBox.confirm(
'检测到草稿内容已发生变动,请选择保存方式:',
'草稿变动提示',
{
confirmButtonText: '直接覆盖',
cancelButtonText: '另存为新草稿',
distinguishCancelAndClose: true,
type: 'warning'
}
)
if (action === 'confirm') {
// 选择覆盖原草稿
await executeSaveDraftRequest(draftBomNo)
}
} catch (action) {
if (action === 'cancel') {
// 选择另存为新草稿:生成新单号并保存
const ts = new Date().toISOString().replace(/[-T:.Z]/g, '').slice(0, 14)
const uid = Math.random().toString(36).slice(2, 6)
const newTempNo = `DRAFT-TEMP-${ts}-${uid}`
form.bom_no = newTempNo
await executeSaveDraftRequest(newTempNo)
}
}
return
}
// 场景 C第一次暂存
await executeSaveDraftRequest(draftBomNo)
}
const executeSaveDraftRequest = async (targetBomNo: string) => {
const draftVersion = form.version || 'V1.0'
const children = form.children
.filter(child => child.child_id !== null || child.dosage > 0)
.map(child => ({
child_id: child.child_id,
dosage: child.dosage,
remark: child.remark || ''
}))
saving.value = true
try {
const res = await saveDraft({
bom_no: targetBomNo,
version: draftVersion,
parent_id: form.parent_id,
children
})
if (res.code === 200) {
ElMessage.success('草稿暂存成功')
form.bom_no = targetBomNo
localStorage.setItem('pending_bom_draft_no', targetBomNo)
originalDraftHash.value = getDraftHash()
} else {
ElMessage.error(res.msg || '暂存失败')
}
} catch (e) {
ElMessage.error('暂存异常')
} finally {
saving.value = false
}
}
const restoreDraftToForm = (draftData: any) => {
form.children = (draftData.children || []).map((child: any, idx: number) => ({
rowKey: Date.now() + idx,
child_id: child.child_id,
material_name: child.child_name || '',
material_spec: child.child_spec || '',
dosage: child.dosage,
remark: child.remark || ''
}))
if (draftData.parent_id) {
form.parent_id = draftData.parent_id
parentNameInput.value = draftData.parent_name || ''
}
form.bom_no = draftData.bom_no || ''
form.version = draftData.version || 'V1.0'
form.remark = draftData.remark || ''
pendingDraftBomNo = draftData.bom_no || pendingDraftBomNo
pendingDraftVersion = draftData.version || pendingDraftVersion
originalDraftHash.value = getDraftHash()
}
const handleView = async (row: BomItem) => {
@ -616,6 +791,9 @@ const resetForm = () => {
currentBomNo = ''
childSearchKeyword.value = ''
parentNameInput.value = ''
originalDraftHash.value = ''
pendingDraftBomNo = ''
pendingDraftVersion = ''
if (formRef.value) formRef.value.resetFields()
}
@ -662,10 +840,19 @@ const submitForm = async () => {
saving.value = true
try {
const res = await saveBom(payload)
if (res.code === 200) { ElMessage.success('保存成功'); dialogVisible.value = false; fetchBomList() }
else ElMessage.error(res.msg || '保存失败')
} finally { saving.value = false }
const res = await publishDraft({ bom_no: pureBomNo.value, version: form.version })
if (res.code === 200) {
ElMessage.success('发布成功')
localStorage.removeItem('pending_bom_draft_no')
originalDraftHash.value = ''
dialogVisible.value = false
fetchBomList()
} else {
ElMessage.error(res.msg || '发布失败')
}
} finally {
saving.value = false
}
})
}