Files
KCGL/inventory-web/src/views/bom/BomManage.vue

882 lines
32 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="app-container">
<el-card shadow="always">
<template #header>
<div class="card-header">
<span class="title">BOM 配方管理</span>
<div class="header-right">
<el-input
v-model="searchKeyword"
placeholder="搜索 编号/名称/规格/子件..."
style="width: 300px; margin-right: 10px;"
clearable
@clear="fetchBomList"
@keyup.enter="fetchBomList"
>
<template #append>
<el-button :icon="Search" @click="fetchBomList" />
</template>
</el-input>
<el-button @click="activeCategories = bomGroups.map((g: any) => g.category)" size="small" style="margin-right: 6px;">全部展开</el-button>
<el-button @click="activeCategories = []" size="small" style="margin-right: 10px;">全部折叠</el-button>
<el-button v-if="userStore.hasPermission('bom_manage:operation')" type="primary" :icon="Plus" @click="handleCreate">新建 BOM</el-button>
</div>
</div>
</template>
<div v-loading="loading">
<el-collapse v-model="activeCategories" class="bom-category-collapse">
<el-collapse-item
v-for="group in bomGroups"
:key="group.category"
:title="group.category + ' (' + group.count + ')'"
:name="group.category"
>
<el-table :data="group.items" border style="width: 100%">
<el-table-column v-if="hasColumnPermission('bom_no')" prop="bom_no" label="BOM编号" min-width="180" sortable>
<template #default="{ row }">
<span style="cursor: pointer; color: #409EFF;" @click="handleEdit(row)">{{ row.bom_no }}</span>
</template>
</el-table-column>
<el-table-column v-if="hasColumnPermission('parent_name')" prop="parent_name" label="父件名称" min-width="150" show-overflow-tooltip />
<el-table-column v-if="hasColumnPermission('parent_spec')" prop="parent_spec" label="父件规格" min-width="150" show-overflow-tooltip />
<el-table-column v-if="hasColumnPermission('version')" label="版本" width="100" align="center">
<template #default="{ row }">
<el-tag>{{ row.version }}</el-tag>
</template>
</el-table-column>
<el-table-column v-if="hasColumnPermission('status')" label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.is_enabled ? 'success' : 'danger'">{{ row.is_enabled ? '启用' : '禁用' }}</el-tag>
</template>
</el-table-column>
<el-table-column v-if="hasColumnPermission('child_count')" prop="child_count" label="子件数" width="80" align="center" />
<el-table-column v-if="userStore.hasPermission('bom_manage:operation')" label="操作" width="250" align="center" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button type="success" link @click="handleSaveAs(row)">另存为</el-button>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-collapse-item>
</el-collapse>
</div>
</el-card>
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="850px" destroy-on-close :close-on-click-modal="false">
<el-form :model="form" label-width="120px" ref="formRef" :rules="rules">
<el-row :gutter="20">
<el-col :span="16">
<el-form-item label="父件 (成品)" prop="parent_id" v-if="hasFormFieldPermission('parent_id')">
<!-- ====== 改造父件下拉 - 远程搜索 + 懒加载 ====== -->
<el-select
v-model="form.parent_id"
placeholder="请搜索并选择父件"
filterable
remote
reserve-keyword
:remote-method="(q: string) => handleRemoteSearch(q, 'parent')"
:loading="selectLoading"
style="width: 100%"
:disabled="isEditMode"
class="beautified-select"
popper-class="bom-loadmore-popper parent-popper"
@visible-change="(visible: boolean) => handleVisibleChange(visible, 'parent')"
@change="onParentChange"
>
<el-option
v-for="item in parentOptions"
:key="item.id"
:label="`${item.name} (${item.spec})`"
:value="item.id"
>
<div class="option-row">
<span class="option-name">{{ item.name }}</span>
<span class="option-spec">{{ item.spec }}</span>
</div>
</el-option>
</el-select>
<el-link
v-if="form.parent_id"
type="primary"
:underline="false"
style="margin-left: 12px; font-size: 13px;"
@click="openParentMaterial"
>
<el-icon style="margin-right: 4px"><EditPen /></el-icon>前往修改基础信息
</el-link>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="是否启用" prop="is_enabled" v-if="hasFormFieldPermission('is_enabled')">
<el-switch v-model="form.is_enabled" active-text="启用" inactive-text="禁用" :disabled="!userStore.hasPermission('bom_manage:operation')" />
</el-form-item>
</el-col>
<el-col :span="16"></el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="10">
<el-form-item label="BOM 编号" required v-if="hasFormFieldPermission('bom_no')">
<el-input v-model="form.bom_no" placeholder="选择父件后自动生成" disabled />
<div style="font-size: 12px; color: #909399; line-height: 1.2; margin-top: 4px;">
编号预览: <span style="font-weight: bold">{{ form.bom_no || '请选择父件' }}</span>
</div>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="备注" v-if="hasFormFieldPermission('remark')">
<el-input v-model="form.remark" placeholder="备注信息可选" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="版本号" prop="version" v-if="hasFormFieldPermission('version')">
<template v-if="isSaveAsMode">
<el-radio-group v-model="form.versionUpgradeType" @change="onVersionUpgradeTypeChange">
<el-radio-button label="minor">升级次版本 ({{ versionOptions.minor }})</el-radio-button>
<el-radio-button label="major">升级主版本 ({{ versionOptions.major }})</el-radio-button>
</el-radio-group>
</template>
<template v-else>
<el-input v-model="form.version" placeholder=": V1.0" />
</template>
</el-form-item>
</el-col>
</el-row>
<div style="font-weight: bold; margin: 15px 0 10px 0; border-left: 4px solid #409EFF; padding-left: 10px;">
子件列表
<span style="font-weight: normal; font-size: 12px; color: #909399; margin-left: 10px;">(已选 {{ filteredChildren.length }} 条)</span>
</div>
<el-input
v-model="childSearchKeyword"
placeholder="请输入子件名称或规格型号搜索"
clearable
style="width: 300px; margin-bottom: 10px;"
:prefix-icon="Search"
/>
<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')">
<template #default="{ row, $index }">
<!-- ====== 改造:子件下拉 - 远程搜索 + 懒加载 ====== -->
<div style="display: flex; align-items: center; gap: 8px;">
<el-select
v-model="row.child_id"
placeholder="请搜索原料"
filterable
remote
reserve-keyword
style="flex: 1;"
:remote-method="(q: string) => handleRemoteSearch(q, 'child', $index)"
:loading="selectLoading"
:loading-text="`正在加载第 ${childQueryParams.page} 页...`"
:popper-class="`bom-loadmore-popper child-popper-${$index}`"
@visible-change="(visible: boolean) => handleVisibleChange(visible, 'child', $index)"
>
<el-option
v-for="item in getChildOptions($index)"
:key="item.id"
:label="`${item.name} (${item.spec})`"
:value="item.id"
>
<div class="option-row">
<span class="option-name">{{ item.name }}</span>
<span class="option-spec">{{ item.spec }}</span>
</div>
</el-option>
</el-select>
<el-tooltip content="前往修改基础信息" placement="top" v-if="row.child_id">
<el-button
type="primary"
link
:icon="EditPen"
@click.stop="openMaterialInNewTab(row.child_id, getChildSpec($index))"
style="font-size: 16px; padding: 4px;"
/>
</el-tooltip>
</div>
</template>
</el-table-column>
<el-table-column label="用量" width="140" v-if="hasFormFieldPermission('dosage')">
<template #default="{ row }">
<el-input-number v-model="row.dosage" :min="0" :precision="0" style="width: 100%" :controls="false" />
</template>
</el-table-column>
<el-table-column label="备注" width="150" v-if="hasFormFieldPermission('remark')">
<template #default="{ row }">
<el-input v-model="row.remark" placeholder="备注" />
</template>
</el-table-column>
<el-table-column label="操作" width="60" align="center" v-if="userStore.hasPermission('bom_manage:operation')">
<template #default="{ $index }">
<el-button type="danger" link @click="removeChild($index)">删</el-button>
</template>
</el-table-column>
</el-table>
<div style="margin-top: 10px; text-align: center;" v-if="hasFormFieldPermission('child_id')">
<el-button type="primary" plain :icon="Plus" @click="addChild" style="width: 100%">添加一行子件</el-button>
</div>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="saving" @click="submitForm">保存</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed, nextTick } from 'vue'
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 { getMaterialBaseList } from '@/api/inbound/stock'
import { useUserStore } from '@/stores/user'
// ============================================================
// 类型定义
// ============================================================
interface BomItem {
bom_no: string
parent_id: number
parent_name: string
version: string
is_enabled: boolean
child_count: number
}
interface MaterialBase {
id: number
name: string
spec: string
}
interface ChildRow {
child_id: number | null
dosage: number
remark: string
}
const userStore = useUserStore()
const route = useRoute()
const router = useRouter()
const loading = ref(false)
const dialogVisible = ref(false)
const saving = ref(false)
const isEditMode = ref(false)
const isSaveAsMode = ref(false)
let originalVersion = ''
let currentBomNo = ''
let originalChildren: ChildRow[] = []
const bomGroups = ref([]) // 分组结构: [{category, count, items[]}]
const activeCategories = ref([]) // 默认全部展开
const searchKeyword = ref('')
const childSearchKeyword = ref('')
// ============================================================
// 【改造】分页 + 远程搜索相关状态
// ============================================================
const PAGE_SIZE = 20
// 父件下拉
const parentOptions = ref<MaterialBase[]>([])
const parentQueryParams = reactive({ page: 1, limit: PAGE_SIZE, keyword: '' })
const parentHasMore = ref(true)
// 子件下拉(每行独立状态)
interface ChildDropdownState {
options: MaterialBase[]
queryParams: { page: number; limit: number; keyword: string }
hasMore: boolean
}
const childDropdownStates = ref<Map<number, ChildDropdownState>>(new Map())
// 子件下拉专用的查询参数(用于 loading-text 显示)
const childQueryParams = reactive({ page: 1 })
// 加载状态(父件和子件共用一个 loading避免闪烁
const selectLoading = ref(false)
const getChildOptions = (index: number): MaterialBase[] => {
return childDropdownStates.value.get(index)?.options ?? []
}
// ============================================================
// 【改造】获取物料列表(分页版本)
// ============================================================
const fetchMaterialOptions = async (
type: 'parent' | 'child',
index?: number,
isLoadMore = false
) => {
// 子件行需要 index
if (type === 'child' && index === undefined) return
const params = type === 'parent'
? parentQueryParams
: childDropdownStates.value.get(index!)?.queryParams
if (!params) return
selectLoading.value = true
try {
const res = await getMaterialBaseList({
page: params.page,
limit: params.limit,
keyword: params.keyword
})
if (res.code === 200) {
const list: MaterialBase[] = res.data?.list ?? res.data ?? []
const total = res.data?.total ?? list.length
if (type === 'parent') {
if (isLoadMore) {
// 去重追加
const existingIds = new Set(parentOptions.value.map(m => m.id))
const newItems = list.filter(m => !existingIds.has(m.id))
parentOptions.value.push(...newItems)
} else {
parentOptions.value = list
}
// 判断是否还有更多数据
parentHasMore.value = parentOptions.value.length < total
} else {
const state = childDropdownStates.value.get(index!)
if (!state) return
if (isLoadMore) {
const existingIds = new Set(state.options.map(m => m.id))
const newItems = list.filter(m => !existingIds.has(m.id))
state.options.push(...newItems)
} else {
state.options = list
}
state.hasMore = state.options.length < total
}
}
} catch (error) {
// 错误已由全局拦截器统一处理
} finally {
selectLoading.value = false
}
}
// ============================================================
// 【改造】远程搜索处理函数
// ============================================================
const handleRemoteSearch = (
query: string,
type: 'parent' | 'child',
index?: number
) => {
if (type === 'parent') {
parentQueryParams.keyword = query
parentQueryParams.page = 1
parentHasMore.value = true
fetchMaterialOptions('parent')
} else if (type === 'child' && index !== undefined) {
const state = childDropdownStates.value.get(index)
if (!state) return
state.queryParams.keyword = query
state.queryParams.page = 1
state.hasMore = true
fetchMaterialOptions('child', index)
}
}
// ============================================================
// 【改造】下拉框展开/收起处理(重置分页 + 预加载第一页)
// ============================================================
const handleVisibleChange = (visible: boolean, type: 'parent' | 'child', index?: number) => {
if (!visible) return
if (type === 'parent') {
parentQueryParams.page = 1
parentQueryParams.keyword = ''
parentHasMore.value = true
fetchMaterialOptions('parent')
} else if (type === 'child' && index !== undefined) {
// 确保该行下拉状态已初始化
if (!childDropdownStates.value.has(index)) {
childDropdownStates.value.set(index, {
options: [],
queryParams: { page: 1, limit: PAGE_SIZE, keyword: '' },
hasMore: true
})
}
const state = childDropdownStates.value.get(index)!
state.queryParams.page = 1
state.queryParams.keyword = ''
state.hasMore = true
fetchMaterialOptions('child', index)
}
// 延迟 50ms 等待弹窗 DOM 完全渲染
setTimeout(() => {
// 动态拼接精确的选择器
const exactSelector = type === 'parent'
? '.parent-popper .el-select-dropdown__wrap'
: `.child-popper-${index} .el-select-dropdown__wrap`;
const popperWrap = document.querySelector(exactSelector) as HTMLElement;
if (popperWrap) {
// 解绑旧事件,防止重复触发
if ((popperWrap as any)._scrollHandler) {
popperWrap.removeEventListener('scroll', (popperWrap as any)._scrollHandler);
}
// 定义滚动触发逻辑
(popperWrap as any)._scrollHandler = function() {
const { scrollTop, scrollHeight, clientHeight } = this;
// 距离底部 10px 触发
if (scrollHeight - scrollTop - clientHeight <= 10) {
if (type === 'parent') {
loadMoreParent();
} else if (type === 'child' && index !== undefined) {
// 触发子件加载
loadMoreChild(popperWrap, index);
}
}
};
popperWrap.addEventListener('scroll', (popperWrap as any)._scrollHandler);
}
}, 50);
}
// ============================================================
// 【改造】滚动触底加载更多
// ============================================================
const loadMoreParent = () => {
if (selectLoading.value || !parentHasMore.value) return
parentQueryParams.page++
fetchMaterialOptions('parent', undefined, true)
}
const loadMoreChild = (_el: HTMLElement, index: number) => {
const state = childDropdownStates.value.get(index)
if (!state) return
if (selectLoading.value || !state.hasMore) return
state.queryParams.page++
fetchMaterialOptions('child', index, true)
}
// ============================================================
// 【改造】初始化子件行下拉状态
// ============================================================
const initChildDropdownState = (index: number) => {
if (!childDropdownStates.value.has(index)) {
childDropdownStates.value.set(index, {
options: [],
queryParams: { page: 1, limit: PAGE_SIZE, keyword: '' },
hasMore: true
})
}
}
// ============================================================
// 原有逻辑保留
// ============================================================
const filteredChildren = computed(() => {
if (!childSearchKeyword.value) {
return form.children
}
const kw = childSearchKeyword.value.toLowerCase()
return form.children.filter(child => {
const state = childDropdownStates.value.get(form.children.indexOf(child))
const material = state?.options.find(m => m.id === child.child_id)
if (!material) return false
const name = (material.name || '').toLowerCase()
const spec = (material.spec || '').toLowerCase()
return name.includes(kw) || spec.includes(kw)
})
})
// 获取子件规格(从 childDropdownStates 缓存中查找)
const getChildSpec = (index: number): string => {
const state = childDropdownStates.value.get(index)
if (!state || !form.children[index]?.child_id) return ''
const material = state.options.find((m: MaterialBase) => m.id === form.children[index].child_id)
return material?.spec || ''
}
// 在新标签页打开基础信息编辑
const openMaterialInNewTab = (targetId: number | null, keyword: string = '') => {
if (!targetId) return ElMessage.warning('请先选择物料')
const routeUrl = router.resolve({
path: '/material',
query: { edit_id: targetId, keyword }
})
window.open(routeUrl.href, '_blank')
}
const openParentMaterial = () => {
if (!form.parent_id) return ElMessage.warning('请先选择父件')
const parent = parentOptions.value.find((p: MaterialBase) => p.id === form.parent_id)
const keyword = parent?.spec || parent?.name || ''
openMaterialInNewTab(form.parent_id, keyword)
}
// 列与权限Code的映射关系数据库中的code
const permissionMap: Record<string, string> = {
bom_no: 'bom_manage:bom_no',
parent_name: 'bom_manage:parent_name',
parent_spec: 'bom_manage:parent_spec',
version: 'bom_manage:version',
status: 'bom_manage:status',
child_count: 'bom_manage:child_count',
parent_id: 'bom_manage:parent_id',
is_enabled: 'bom_manage:status',
child_id: 'bom_manage:child_id',
dosage: 'bom_manage:dosage',
remark: 'bom_manage:remark',
}
const hasColumnPermission = (prop: string) => {
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') return true
const code = permissionMap[prop]
return code ? userStore.hasPermission(code) : false
}
const hasFormFieldPermission = (fieldName: string) => {
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') return true
const code = permissionMap[fieldName]
return code ? userStore.hasPermission(code) : false
}
const formRef = ref<FormInstance>()
const form = reactive({
bom_no: '',
remark: '',
parent_id: null as number | null,
version: 'V1.0',
versionUpgradeType: 'minor' as 'minor' | 'major',
is_enabled: true,
children: [] as ChildRow[]
})
const pureBomNo = computed(() => form.bom_no)
const versionOptions = computed(() => {
const ver = originalVersion || 'V1.0'
const allItems = bomGroups.value.flatMap((g: any) => g.items)
const occupiedVersions = new Set(
allItems.filter((item: any) => item.bom_no === currentBomNo).map((item: any) => item.version)
)
const getNextMinor = (baseMajor: number, baseMinor: number): string => {
let minor = baseMinor + 1
let candidate = `V${baseMajor}.${minor}`
while (occupiedVersions.has(candidate)) { minor++; candidate = `V${baseMajor}.${minor}` }
return candidate
}
const getNextMajor = (baseMajor: number): string => {
let major = baseMajor + 1
let candidate = `V${major}.0`
while (occupiedVersions.has(candidate)) { major++; candidate = `V${major}.0` }
return candidate
}
const match = ver.match(/^V(\d+)\.(\d+)$/)
if (match) {
return { minor: getNextMinor(parseInt(match[1]), parseInt(match[2])), major: getNextMajor(parseInt(match[1])) }
}
return { minor: getNextMinor(1, 0), major: getNextMajor(1) }
})
const onVersionUpgradeTypeChange = (type: 'minor' | 'major') => {
form.version = versionOptions.value[type]
}
const rules = reactive<FormRules>({
parent_id: [{ required: true, message: '请选择父件', trigger: 'change' }],
version: [{ required: true, message: '请输入版本号', trigger: 'blur' }]
})
const dialogTitle = ref('新建 BOM')
const fetchBomList = async () => {
loading.value = true
try {
const res = await getBomList({ keyword: searchKeyword.value })
if (res.code === 200) {
bomGroups.value = res.data
activeCategories.value = []
}
} finally { loading.value = false }
}
const onParentChange = (val: number) => {
const selected = parentOptions.value.find(m => m.id === val)
if (selected && selected.spec) {
form.bom_no = selected.spec.split('/')[0].trim()
} else {
form.bom_no = ''
}
}
const onChildChange = (val: number | null, index: number) => {
if (val !== null) {
const existingIndex = form.children.findIndex((child, idx) => idx !== index && child.child_id === val)
if (existingIndex !== -1) {
ElMessage.warning(`该物料已在第 ${existingIndex + 1} 行存在,请合并用量或删除重复项`)
form.children[index].child_id = null
form.children[index].dosage = 0
return
}
form.children[index].dosage = 1
}
}
const handleCreate = () => {
resetForm()
dialogTitle.value = '新建 BOM'
isEditMode.value = false
isSaveAsMode.value = false
dialogVisible.value = true
}
const handleEdit = async (row: BomItem) => {
await loadDetail(row.bom_no, row.version)
dialogTitle.value = '编辑 BOM'
isEditMode.value = true
isSaveAsMode.value = false
dialogVisible.value = true
}
const handleSaveAs = async (row: BomItem) => {
await loadDetail(row.bom_no, row.version)
dialogTitle.value = '另存为新版/变体'
isEditMode.value = true
isSaveAsMode.value = true
originalVersion = form.version
currentBomNo = row.bom_no
originalChildren = JSON.parse(JSON.stringify(form.children))
form.versionUpgradeType = 'minor'
form.version = versionOptions.value.minor
dialogVisible.value = true
}
const loadDetail = async (bomNo: string, version: string) => {
try {
const res = await getBomDetail(bomNo, version)
if (res.code === 200) {
const data = res.data
// 1. 映射子件基本数据
form.children = data.children.map((child: any) => ({
child_id: child.child_id,
dosage: child.dosage,
remark: child.remark || ''
}))
// 2. 初始化子件下拉状态,并预填充 options 解决回显显示 ID 的问题
form.children.forEach((child, idx) => {
initChildDropdownState(idx)
if (child.child_id) {
const state = childDropdownStates.value.get(idx)!
// 从原始 data.children 中取对应的名称和规格注入 options
const rawChildData = data.children[idx]
state.options = [{
id: rawChildData.child_id,
name: rawChildData.child_name || '未知物料', // 依赖后端返回 child_name
spec: rawChildData.child_spec || '' // 依赖后端返回 child_spec
}]
state.hasMore = false
}
})
// 3. 处理父件回显,预填充 parentOptions
if (data.parent_id) {
form.parent_id = data.parent_id
parentOptions.value = [{
id: data.parent_id,
name: data.parent_name || '未知产品', // 依赖后端返回 parent_name
spec: data.parent_spec || '' // 依赖后端返回 parent_spec
}]
}
if (data.parent_spec) {
form.bom_no = (data.parent_spec || '').split('/')[0].trim()
} else {
form.bom_no = bomNo.split('/')[0].trim()
}
form.remark = data.remark || ''
}
} catch (e) {}
}
const handleDelete = (row: BomItem) => {
ElMessageBox.confirm(`确定删除 ${row.bom_no} (${row.version}) 吗?`, '警告', { type: 'warning' })
.then(async () => {
try {
const res = await deleteBom(row.bom_no, row.version)
if (res.code === 200) { ElMessage.success('删除成功'); fetchBomList() }
} catch (e) {}
})
.catch(() => {})
}
const resetForm = () => {
form.bom_no = ''
form.remark = ''
form.parent_id = null
form.version = 'V1.0'
form.versionUpgradeType = 'minor'
form.is_enabled = true
form.children = []
isSaveAsMode.value = false
originalVersion = ''
currentBomNo = ''
childSearchKeyword.value = ''
// 重置子件下拉状态
childDropdownStates.value.clear()
// 重置父件下拉状态
parentOptions.value = []
parentQueryParams.page = 1
parentQueryParams.keyword = ''
parentHasMore.value = true
if (formRef.value) formRef.value.resetFields()
}
const addChild = () => {
const idx = form.children.length
form.children.push({ child_id: null, dosage: 0, remark: '' })
initChildDropdownState(idx)
}
const removeChild = (idx: number) => {
form.children.splice(idx, 1)
// 清理该行下拉状态(需要重新索引后续行的状态)
rebuildChildDropdownStates()
}
// 重建子件下拉状态索引(删除行后需要重新编号)
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 () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (!valid) return
if (!pureBomNo.value) return ElMessage.warning('BOM编号不能为空')
if (form.children.length === 0) return ElMessage.warning('请至少添加一个子件')
const childIds = form.children.map(c => c.child_id).filter(id => id !== null)
const uniqueIds = new Set(childIds)
if (childIds.length !== uniqueIds.size) {
return ElMessage.warning('子件列表中存在重复物料,请合并用量或删除重复项')
}
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 isIdentical = currentSet.size === originalSet.size && [...currentSet].every(item => originalSet.has(item))
if (isIdentical) {
return ElMessage.warning('您未修改任何子件,与原版本内容一致,请修改后再保存')
}
}
const payload = {
bom_no: pureBomNo.value,
remark: form.remark,
version: form.version,
parent_id: form.parent_id,
is_enabled: form.is_enabled,
children: form.children
}
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 }
})
}
onMounted(() => {
fetchBomList()
// 【新增】:处理外部跳转自动打开 BOM带查重保护
if (route.query.create_for_id) {
const parentId = Number(route.query.create_for_id);
const parentName = (route.query.parent_name as string) || '';
const parentSpec = (route.query.parent_spec as string) || '';
// 把名称填入背景搜索框让背后的表格也只显示相关的BOM
searchKeyword.value = parentName;
// 延迟等待基础渲染
setTimeout(() => {
// 1. 先用 keyword 查询是否已有该父件的 BOM
getBomList({ keyword: parentName }).then((res: any) => {
const rows = res.data || [];
// 严格校验 parent_id
const existingBom = rows.find((b: any) => b.parent_id === parentId);
if (existingBom) {
// ★ 情况 A已经有BOM了直接打开编辑弹窗并拉取历史数据
ElMessage.success('检测到该物料已有 BOM已自动为您打开编辑');
handleEdit(existingBom);
} else {
// ★ 情况 B还没建过BOM打开新建并注入父件
handleCreate();
// 强行注入父件远程搜索选项
parentOptions.value = [{
id: parentId,
name: parentName,
spec: parentSpec
}];
// 给表单赋值
form.parent_id = parentId;
// 触发联动逻辑(自动带出版本和生成编号)
if (typeof onParentChange === 'function') {
setTimeout(() => {
onParentChange(parentId);
}, 100);
}
}
}).catch(err => {
console.error('BOM 查重失败', err);
ElMessage.error('获取 BOM 状态失败,请手动操作');
});
}, 300);
}
})
</script>
<style scoped>
.card-header { display: flex; justify-content: space-between; align-items: center; }
.header-right { display: flex; align-items: center; }
.title { font-size: 18px; font-weight: bold; }
.option-row { display: flex; justify-content: space-between; width: 100%; }
.option-name { font-weight: bold; color: #303133; }
.option-spec { font-size: 12px; color: #909399; margin-left: 15px; }
</style>