@@ -93,6 +99,7 @@
{{ item.name }}
{{ item.spec }}
+ {{ item.company_name }}
@@ -112,7 +119,15 @@
-
+
+
@@ -184,6 +199,7 @@
{{ item.name }}
{{ item.spec }}
+ {{ item.company_name }}
@@ -196,10 +212,29 @@
style="font-size: 16px; padding: 4px;"
/>
-
-
- 自制{{ row.child_bom_version }}
+
+
+
+
onChildBomChange(row)"
+ >
+
+
+
+ 自制 {{ row.child_bom_no ? row.child_bom_no + ' ' : '' }}{{ row.child_bom_version }}
+
请选择该自制件的 BOM 版本
@@ -266,8 +301,9 @@ import { useRoute } from 'vue-router'
import { ElMessage, ElMessageBox, FormInstance, FormRules } from 'element-plus'
import { Plus, Search, EditPen, Upload } from '@element-plus/icons-vue'
import { useRouter } from 'vue-router'
-import { getBomList, getBomSummary, getBomDetail, saveBom, deleteBom, getDraftDetail, saveDraft, publishDraft } from '@/api/bom'
+import { getBomList, getBomSummary, getBomDetail, saveBom, deleteBom, getDraftDetail, saveDraft, publishDraft, getChildBomVersions, updateBomStatus } from '@/api/bom'
import ImportDialog from '@/components/ImportDialog.vue'
+import CompanySelector from '@/components/CompanySelector.vue'
import { searchMaterialBase } from '@/api/inbound/buy'
import { useUserStore } from '@/stores/user'
@@ -294,7 +330,64 @@ interface ChildRow {
material_spec: string
dosage: number
remark: string
- child_bom_version?: string // 自制件子件对应的 BOM 版本(仅展示识别)
+ child_bom_no?: string // 子件引用的自制 BOM 编号(必选:该物料作父件的配方)
+ child_bom_version?: string // 子件引用的自制 BOM 版本(必选,见 bom_version_options)
+ child_bom_key?: string // 下拉选中项编码 `${bom_no}|${version}`
+ bom_version_options?: { bom_no: string; version: string }[] // 该物料作父件的启用配方候选
+}
+
+// ★ 自制件子件"版本"候选与选中项工具
+const BOM_KEY_SEP = '|'
+const bomKeyOf = (no?: string, ver?: string) => (no || '') + BOM_KEY_SEP + (ver || '')
+const bomKeyTo = (key?: string) => {
+ const s = String(key || '')
+ const i = s.indexOf(BOM_KEY_SEP)
+ if (i < 0) return ['', '']
+ return [s.slice(0, i), s.slice(i + 1)]
+}
+const childIsSelfMade = (row: any) =>
+ !!(row && ((row.bom_version_options && row.bom_version_options.length) || row.child_bom_no || row.child_bom_version))
+
+// 拉取某子件物料作父件的启用配方列表,并维护选中项。
+// - 有候选: 若当前已有合法选中(编辑存量行)则保留,否则默认选"最新启用配方"(列表第一项)
+// - 无候选(外购件/无现行BOM): 清空版本相关字段
+const loadChildBomVersions = async (row: any) => {
+ if (!row.child_id) { row.bom_version_options = []; return }
+ let list: { bom_no: string; version: string }[] = []
+ try {
+ const res: any = await getChildBomVersions(row.child_id)
+ if (res?.code === 200 && Array.isArray(res.data)) list = res.data
+ } catch (e) { list = [] }
+ row.bom_version_options = list
+ if (!list.length) {
+ row.child_bom_no = ''
+ row.child_bom_version = ''
+ row.child_bom_key = ''
+ return
+ }
+ const existKey = bomKeyOf(row.child_bom_no, row.child_bom_version)
+ if (row.child_bom_no && row.child_bom_version && list.some(o => bomKeyOf(o.bom_no, o.version) === existKey)) {
+ row.child_bom_key = existKey
+ return
+ }
+ const first = list[0]
+ row.child_bom_no = first.bom_no
+ row.child_bom_version = first.version
+ row.child_bom_key = bomKeyOf(first.bom_no, first.version)
+}
+
+const onChildBomChange = (row: any) => {
+ const [no, ver] = bomKeyTo(row.child_bom_key)
+ row.child_bom_no = no
+ row.child_bom_version = ver
+}
+
+// 给现有 children(从详情/草稿/另存为载入的存量自制件子件)补齐版本候选,
+// 使编辑态能渲染下拉;保留其已存储的引用版本。
+const hydrateChildBomVersions = async () => {
+ for (const c of form.children) {
+ if (c.child_id) await loadChildBomVersions(c)
+ }
}
const originalDraftHash = ref('')
@@ -303,7 +396,9 @@ const getDraftHash = () => {
const children = form.children.map(c => ({
child_id: c.child_id,
dosage: Number(c.dosage) || 0,
- remark: c.remark || ''
+ remark: c.remark || '',
+ child_bom_no: c.child_bom_no || '',
+ child_bom_version: c.child_bom_version || ''
}))
return JSON.stringify({
bom_no: form.bom_no,
@@ -331,6 +426,11 @@ let pendingDraftVersion = ''
const searchKeyword = ref('')
const childSearchKeyword = ref('')
+const bomStatusFilter = ref('enabled') // 默认只看"启用",可手动切全部/停用;enabled=启用 | disabled=停用 | ''=全部
+const bomCompany = ref('') // 跨域用户按公司(父件物料公司)过滤;普通用户由后端强制隔离,此控件不显示
+// ★ 手风琴开关:默认单开;"全部展开"时临时关闭手风琴以允许多开,"全部折叠"后恢复
+const isAccordion = ref(true)
+const detailLoading = ref(false) // 弹窗查看详情 loading(防"假死"反馈)
// ★ 懒加载分组架构
interface GroupSummary { category: string; count: number }
@@ -378,7 +478,7 @@ const fetchParentSuggestions = (query: string, cb: (results: any[]) => void) =>
const rawQuery = String(query || '')
const safeQuery = rawQuery.replace(/[\x00-\x1F\x7F-\x9F\u200B-\u200D\uFEFF]/g, '').trim()
searchLoading.value = true
- searchMaterialBase(safeQuery).then((res: any) => {
+ searchMaterialBase(safeQuery, 1, bomCompany.value || '').then((res: any) => {
const items = res.data?.items || res.data || []
const formatted = items.map((i: any) => ({ ...i, name: i.name || i.material_name }))
cb(formatted)
@@ -404,7 +504,7 @@ const fetchChildSuggestions = (row: any, query: string, cb: (results: any[]) =>
const rawQuery = String(query || '')
const safeQuery = rawQuery.replace(/[\x00-\x1F\x7F-\x9F\u200B-\u200D\uFEFF]/g, '').trim()
searchLoading.value = true
- searchMaterialBase(safeQuery).then((res: any) => {
+ searchMaterialBase(safeQuery, 1, bomCompany.value || '').then((res: any) => {
const items = res.data?.items || res.data || []
const formatted = items.map((i: any) => ({ ...i, name: i.name || i.material_name }))
cb(formatted)
@@ -416,6 +516,10 @@ const onChildClear = (row: any) => {
row.material_name = ''
row.material_spec = ''
row.dosage = 0
+ row.child_bom_no = ''
+ row.child_bom_version = ''
+ row.child_bom_key = ''
+ row.bom_version_options = []
}
const onChildSelected = (row: any, item: any) => {
@@ -425,12 +529,19 @@ const onChildSelected = (row: any, item: any) => {
row.child_id = null
row.material_name = ''
row.dosage = 0
+ row.bom_version_options = []
return
}
row.child_id = item.id
row.material_name = item.name
row.material_spec = item.spec
row.dosage = 1
+ row.child_bom_no = ''
+ row.child_bom_version = ''
+ row.child_bom_key = ''
+ row.bom_version_options = []
+ // 选中物料后判定:若其为自制件(存在启用BOM),拉取候选版本并默认选最新,否则清空。
+ loadChildBomVersions(row)
}
const getChildSpec = (rowKey: number): string => {
const row = form.children.find(c => c.rowKey === rowKey)
@@ -533,6 +644,7 @@ const dialogTitle = ref('新建 BOM')
const handleSearch = () => {
activeCategories.value = []
groupCache.value = new Map()
+ isAccordion.value = true // 搜索/筛选重置回默认手风琴模式
fetchBomSummary()
}
@@ -542,6 +654,8 @@ const fetchBomSummary = async () => {
try {
const params: any = {}
if (searchKeyword.value) params.keyword = searchKeyword.value
+ if (bomStatusFilter.value) params.status = bomStatusFilter.value
+ if (bomCompany.value) params.company_name = bomCompany.value
const res: any = await getBomSummary(params)
if (res?.code === 200) {
groupSummary.value = res.data ?? []
@@ -554,12 +668,17 @@ const fetchBomSummary = async () => {
finally { loading.value = false }
}
-// ★ 默认全部展开(用户点击按钮时)
+// ★ 全部展开:先临时关闭手风琴(允许多开),再一次性展开+懒加载各分类
const expandAllGroups = () => {
+ isAccordion.value = false
activeCategories.value = groupSummary.value.map(g => g.category)
groupSummary.value.forEach(g => loadGroupItems(g.category))
}
-const collapseAllGroups = () => { activeCategories.value = [] }
+// ★ 全部折叠:清空面板,并恢复手风琴模式
+const collapseAllGroups = () => {
+ activeCategories.value = []
+ isAccordion.value = true
+}
// ★ 展开分组时懒加载该分类下的 BOM
const loadGroupItems = async (category: string) => {
@@ -574,6 +693,8 @@ const loadGroupItems = async (category: string) => {
pageSize: 9999 // 单分类内全量加载(分类内数据量可控)
}
if (searchKeyword.value) params.keyword = searchKeyword.value
+ if (bomStatusFilter.value) params.status = bomStatusFilter.value
+ if (bomCompany.value) params.company_name = bomCompany.value
const res: any = await getBomList(params)
if (res?.code === 200) {
groupCache.value.set(category, res.data?.items ?? [])
@@ -655,7 +776,8 @@ const executeSaveDraftRequest = async (targetBomNo: string) => {
child_id: child.child_id,
dosage: child.dosage,
remark: child.remark || '',
- child_bom_version: child.child_bom_version || ''
+ child_bom_no: child.child_bom_no || '',
+ child_bom_version: child.child_bom_version || ''
}))
saving.value = true
@@ -690,6 +812,7 @@ const restoreDraftToForm = (draftData: any) => {
material_spec: child.child_spec || '',
dosage: child.dosage,
remark: child.remark || '',
+ child_bom_no: child.child_bom_no || '',
child_bom_version: child.child_bom_version || ''
}))
@@ -705,6 +828,8 @@ const restoreDraftToForm = (draftData: any) => {
pendingDraftVersion = draftData.version || pendingDraftVersion
originalDraftHash.value = getDraftHash()
+ // 草稿中存量自制件子件也要补齐版本候选(保留其已存版本),便于继续编辑
+ hydrateChildBomVersions()
}
const checkUserDraft = async (bomNo: string, version: string) => {
@@ -773,12 +898,13 @@ const checkAndInterceptGlobalDraft = async (): Promise => {
}
const handleView = async (row: BomItem) => {
- await loadDetail(row.bom_no, row.version)
+ resetForm() // 清空旧内容,避免加载期间闪现上一条数据
dialogTitle.value = '查看 BOM'
isEditMode.value = false
isSaveAsMode.value = false
isReadOnlyMode.value = true
- dialogVisible.value = true
+ dialogVisible.value = true // 先开窗,让 detailLoading 骨架屏可见
+ await loadDetail(row.bom_no, row.version)
}
const handleSaveAs = async (row: any) => {
@@ -808,6 +934,7 @@ const handleSaveAs = async (row: any) => {
material_spec: child.child_spec || '',
dosage: child.dosage,
remark: child.remark || '',
+ child_bom_no: child.child_bom_no || '',
child_bom_version: child.child_bom_version || ''
}))
if (raw.parent_id) {
@@ -825,6 +952,9 @@ const handleSaveAs = async (row: any) => {
originalChildren = JSON.parse(JSON.stringify(form.children))
originalDraftHash.value = getDraftHash()
+ // 另存/升版编辑:存量自制件子件补齐版本候选(保留其已存储的引用版本)
+ hydrateChildBomVersions()
+
dialogTitle.value = '新增 BOM (版本升级)'
isEditMode.value = false
isSaveAsMode.value = true
@@ -833,6 +963,7 @@ const handleSaveAs = async (row: any) => {
}
const loadDetail = async (bomNo: string, version: string) => {
+ detailLoading.value = true
try {
const res = await getBomDetail(bomNo, version)
if (res.code === 200) {
@@ -845,6 +976,7 @@ const loadDetail = async (bomNo: string, version: string) => {
material_spec: child.child_spec || '',
dosage: child.dosage,
remark: child.remark || '',
+ child_bom_no: child.child_bom_no || '',
child_bom_version: child.child_bom_version || ''
}))
@@ -859,8 +991,45 @@ const loadDetail = async (bomNo: string, version: string) => {
form.bom_no = bomNo.split('/')[0].trim()
}
form.remark = data.remark || ''
+ // 查看/只读视图的独立启停需要版本与当前状态
+ form.version = version || data.version || 'V1.0'
+ form.is_enabled = data.is_enabled !== false
}
- } catch (e) {}
+ } catch (e) {
+ // 网络/数据异常:仅结束 loading,不弹全局错误,保持弹窗可操作
+ } finally {
+ detailLoading.value = false
+ }
+}
+
+// ★ 启用/停用开关:查看/只读态下独立更新状态(不触发整表单保存);
+// 新建/编辑/另存模式则仅记录到 form,随 submitForm 一并保存。
+const onEnabledStatusChange = async () => {
+ if (!isReadOnlyMode.value) return
+
+ if (!pureBomNo.value || !form.version) {
+ 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 })
+ if (res?.code === 200) {
+ ElMessage.success(target ? '已启用' : '已停用')
+ // 刷新左侧列表摘要(计数与状态列)
+ groupCache.value = new Map()
+ fetchBomSummary()
+ } else {
+ form.is_enabled = !target
+ ElMessage.error(res?.msg || '状态更新失败')
+ }
+ } catch (e) {
+ form.is_enabled = !target
+ ElMessage.error('状态更新失败')
+ } finally {
+ saving.value = false
+ }
}
const handleDelete = (row: BomItem) => {
@@ -918,14 +1087,25 @@ const submitForm = async () => {
}
if (isSaveAsMode.value && originalChildren.length > 0) {
- const currentSet = new Set(form.children.map(c => `${c.child_id}-${c.dosage}`))
- const originalSet = new Set(originalChildren.map(c => `${c.child_id}-${c.dosage}`))
+ const childKey = (c: any) => `${c.child_id}-${c.dosage}-${c.child_bom_no || ''}-${c.child_bom_version || ''}`
+ const currentSet = new Set(form.children.map(childKey))
+ const originalSet = new Set(originalChildren.map(childKey))
const isIdentical = currentSet.size === originalSet.size && [...currentSet].every(item => originalSet.has(item))
if (isIdentical) {
return ElMessage.warning('您未修改任何子件,与原版本内容一致,请修改后再保存')
}
}
+ // 前置校验:自制件子件必须已选版本(后端仍会强校验兜底)
+ const missingVer = form.children.find(
+ (c: any) => c.child_id !== null &&
+ (c.child_bom_no || c.child_bom_version || (c.bom_version_options && c.bom_version_options.length)) &&
+ !(c.child_bom_no && c.child_bom_version)
+ )
+ if (missingVer) {
+ return ElMessage.warning(`子件「${missingVer.material_name || missingVer.child_id}」为自制件,请先选择其 BOM 版本`)
+ }
+
const payload = {
bom_no: pureBomNo.value,
remark: form.remark,
@@ -937,7 +1117,9 @@ const submitForm = async () => {
.map((c: any) => ({
child_id: c.child_id,
dosage: c.dosage,
- remark: c.remark || ''
+ remark: c.remark || '',
+ child_bom_no: c.child_bom_no || '',
+ child_bom_version: c.child_bom_version || ''
}))
}