Files
my-mom-system/erp-frontend-vue/src/views/RD/Ebom/form.vue

963 lines
31 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="bom-form-page">
<!-- 表头卡片 -->
<el-card class="header-card" shadow="never">
<div class="header-bar">
<h3 class="title">EBOM清单</h3>
<div class="actions">
<el-button type="primary" @click="handleSave" :disabled="isReadonly" :loading="saveLoading">保存</el-button>
<el-button @click="handleCancel">取消</el-button>
<el-button
type="success"
@click="handleApprove"
:disabled="isReadonly || form.status === 'APPROVED'"
>审核</el-button>
<el-button
type="warning"
@click="handleUnApprove"
:disabled="form.status !== 'APPROVED'"
>反审核</el-button>
<el-button type="primary" link @click="collapsed = !collapsed">
{{ collapsed ? '展开' : '收起' }}
</el-button>
</div>
</div>
<!-- 表头表单 -->
<el-form
v-show="!collapsed"
ref="formRef"
:model="form"
:rules="formRules"
label-width="80px"
class="grid-form"
>
<!-- 第1行 -->
<el-row :gutter="24">
<el-col :span="6">
<el-form-item label="单据编码" prop="bomCode">
<el-input v-model="form.bomCode" disabled placeholder="自动生成" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="业务类型" prop="businessType">
<el-select v-model="form.businessType" :disabled="isReadonly" style="width: 100%">
<el-option v-for="opt in BIZ_TYPE_OPTIONS" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="物料编码" prop="itemCode">
<div class="inline-select">
<el-input v-model="form.itemCode" disabled />
<el-button link type="primary" :disabled="isReadonly" @click="openProductDialog">选择</el-button>
</div>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="图纸号">
<el-input v-model="form.drawingNo" :disabled="isReadonly" />
</el-form-item>
</el-col>
</el-row>
<!-- 第2行 -->
<el-row :gutter="24">
<el-col :span="6">
<el-form-item label="单据日期" prop="bomDate">
<el-date-picker
v-model="form.bomDate"
type="date"
value-format="YYYY-MM-DD"
:disabled="isReadonly"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="操作员">
<el-input v-model="form.operatorName" disabled />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="物料名称">
<el-input v-model="form.itemName" disabled />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="版本号" prop="version">
<el-input-number
v-model="versionNumber"
:min="1"
:step="0.1"
:precision="1"
:disabled="isReadonly"
controls-position="right"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<!-- 第3行 -->
<el-row :gutter="24">
<el-col :span="6">
<el-form-item label="单据状态">
<el-tag :type="form.status === 'APPROVED' ? 'success' : form.status === 'REJECTED' ? 'warning' : 'info'">
{{ STATUS_MAP[form.status]?.label || '开立' }}
</el-tag>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="审核员">
<el-input v-model="form.approverName" disabled />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="计量单位">
<el-input v-model="form.unitName" disabled />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="版本说明">
<el-input v-model="form.versionDesc" :disabled="isReadonly" />
</el-form-item>
</el-col>
</el-row>
<!-- 第4行 -->
<el-row :gutter="24">
<el-col :span="6">
<el-form-item label="业务状态">
<el-select v-model="form.businessStatus" :disabled="isReadonly" style="width: 100%">
<el-option v-for="opt in BIZ_STATUS_OPTIONS" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="审核日期">
<el-date-picker v-model="form.approveDate" type="date" value-format="YYYY-MM-DD" disabled style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="母件基数" prop="baseQty">
<el-input-number v-model="form.baseQty" :min="1" :step="1" :disabled="isReadonly" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="备注信息">
<el-input v-model="form.remark" type="textarea" :rows="1" :disabled="isReadonly" />
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
<!-- 物料明细卡片 -->
<el-card class="lines-card" shadow="never">
<div class="lines-header">
<span class="lines-title">物料信息</span>
<div class="lines-actions">
<el-button type="primary" plain :icon="Plus" :disabled="isReadonly" @click="addEmptyLine">新增物料</el-button>
</div>
</div>
<el-table :data="lines" border size="small" max-height="420">
<el-table-column label="序号" type="index" width="60" align="center" />
<el-table-column label="物料编码" min-width="160">
<template #default="{ row, $index }">
<div class="inline-select">
<span class="mono">{{ row.bomItemCode || '请添加物料' }}</span>
<el-button link type="primary" :disabled="isReadonly" @click="openItemDialog($index)">选择</el-button>
</div>
</template>
</el-table-column>
<el-table-column prop="bomItemName" label="物料名称" min-width="160" show-overflow-tooltip />
<el-table-column prop="bomItemSpec" label="型号规格" min-width="130" show-overflow-tooltip />
<el-table-column prop="unitName" label="主计量" width="80" align="center" />
<el-table-column label="计划线路" width="110" align="center">
<template #default="{ row }">
<el-select v-model="row.supplyType" :disabled="isReadonly" size="small" style="width: 100%">
<el-option v-for="opt in SUPPLY_TYPE_OPTIONS" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select>
</template>
</el-table-column>
<el-table-column label="领料方式" width="110" align="center">
<template #default="{ row }">
<el-select v-model="row.pickType" :disabled="isReadonly" size="small" style="width: 100%">
<el-option v-for="opt in PICK_TYPE_OPTIONS" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select>
</template>
</el-table-column>
<el-table-column label="分子" width="100" align="right">
<template #default="{ row }">
<el-input-number
v-model="row.quantity"
:min="0"
:precision="4"
:disabled="isReadonly"
size="small"
controls-position="right"
style="width: 100%"
/>
</template>
</el-table-column>
<el-table-column label="用量方式" width="110" align="center">
<template #default="{ row }">
<el-select v-model="row.usageType" :disabled="isReadonly" size="small" style="width: 100%">
<el-option v-for="opt in USAGE_TYPE_OPTIONS" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select>
</template>
</el-table-column>
<el-table-column label="图纸号" width="110">
<template #default="{ row }">
<el-input v-model="row.drawingNo" :disabled="isReadonly" size="small" />
</template>
</el-table-column>
<el-table-column label="备注" min-width="110">
<template #default="{ row }">
<el-input v-model="row.remark" :disabled="isReadonly" size="small" />
</template>
</el-table-column>
<el-table-column label="操作" width="70" align="center" fixed="right">
<template #default="{ $index }">
<el-button link type="danger" :disabled="isReadonly" @click="removeLine($index)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- ==================== 选择产品(母件)弹窗 ==================== -->
<el-dialog v-model="productDialogVisible" title="选择物料" width="900px" destroy-on-close>
<div class="item-select-layout">
<!-- 左侧分类树 -->
<div class="item-select-tree">
<el-input v-model="treeFilterText" placeholder="请输入分类名称" clearable style="margin-bottom: 8px" />
<el-tree
ref="treeRef"
:data="itemTypeTreeData"
:props="{ label: 'label', children: 'children' }"
:filter-node-method="filterNode"
highlight-current
default-expand-all
@node-click="handleTreeNodeClick"
/>
</div>
<!-- 右侧列表 -->
<div class="item-select-table">
<el-form :inline="true" @submit.prevent="searchProductItems" style="margin-bottom: 8px">
<el-form-item label="物料编码">
<el-input v-model="productQuery.itemCode" placeholder="请输入" clearable style="width: 120px" />
</el-form-item>
<el-form-item label="物料名称">
<el-input v-model="productQuery.itemName" placeholder="请输入" clearable style="width: 120px" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="searchProductItems">搜索</el-button>
<el-button @click="resetProductSearch">重置</el-button>
</el-form-item>
<el-form-item>
<el-button @click="queryAllProducts">查询所有</el-button>
</el-form-item>
</el-form>
<el-table :data="productList" border size="small" max-height="380" v-loading="productLoading">
<el-table-column prop="itemCode" label="物料编码" width="130" />
<el-table-column prop="itemName" label="物料名称" min-width="160" show-overflow-tooltip />
<el-table-column prop="specification" label="型号规格" width="130" show-overflow-tooltip />
<el-table-column prop="unitName" label="主计量" width="70" align="center" />
<el-table-column label="供应方式" width="80" align="center">
<template #default="{ row }">
{{ getSupplyLabel(row.itemOrProduct) }}
</template>
</el-table-column>
<el-table-column label="操作" width="70" align="center">
<template #default="{ row }">
<el-button type="primary" link @click="confirmProduct(row)">选择</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-wrapper" style="margin-top: 8px">
<el-pagination
v-model:current-page="productQuery.pageNum"
v-model:page-size="productQuery.pageSize"
:page-sizes="[50, 100]"
:total="productTotal"
layout="total, sizes, prev, pager, next"
small
@size-change="searchProductItems"
@current-change="searchProductItems"
/>
</div>
</div>
</div>
</el-dialog>
<!-- ==================== 选择子件物料弹窗 ==================== -->
<el-dialog v-model="itemDialogVisible" title="选择物料" width="900px" destroy-on-close>
<div class="item-select-layout">
<div class="item-select-tree">
<el-input v-model="itemTreeFilterText" placeholder="请输入分类名称" clearable style="margin-bottom: 8px" />
<el-tree
ref="itemTreeRef"
:data="itemTypeTreeData"
:props="{ label: 'label', children: 'children' }"
:filter-node-method="filterNode"
highlight-current
default-expand-all
@node-click="handleItemTreeClick"
/>
</div>
<div class="item-select-table">
<el-form :inline="true" @submit.prevent="searchSubItems" style="margin-bottom: 8px">
<el-form-item label="物料编码">
<el-input v-model="itemQuery.itemCode" placeholder="请输入" clearable style="width: 120px" />
</el-form-item>
<el-form-item label="物料名称">
<el-input v-model="itemQuery.itemName" placeholder="请输入" clearable style="width: 120px" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="searchSubItems">搜索</el-button>
<el-button @click="resetItemSearch">重置</el-button>
</el-form-item>
<el-form-item>
<el-button @click="queryAllItems">查询所有</el-button>
</el-form-item>
</el-form>
<el-table :data="itemList" border size="small" max-height="380" v-loading="itemLoading">
<el-table-column prop="itemCode" label="物料编码" width="130" />
<el-table-column prop="itemName" label="物料名称" min-width="160" show-overflow-tooltip />
<el-table-column prop="specification" label="型号规格" width="130" show-overflow-tooltip />
<el-table-column prop="unitName" label="主计量" width="70" align="center" />
<el-table-column label="供应方式" width="80" align="center">
<template #default="{ row }">
{{ getSupplyLabel(row.itemOrProduct) }}
</template>
</el-table-column>
<el-table-column label="操作" width="70" align="center">
<template #default="{ row }">
<el-button type="primary" link @click="confirmItem(row)">选择</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-wrapper" style="margin-top: 8px">
<el-pagination
v-model:current-page="itemQuery.pageNum"
v-model:page-size="itemQuery.pageSize"
:page-sizes="[50, 100]"
:total="itemTotal"
layout="total, sizes, prev, pager, next"
small
@size-change="searchSubItems"
@current-change="searchSubItems"
/>
</div>
</div>
</div>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox, type FormRules } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import {
getEbom,
addEbom,
updateEbom,
approveEbom,
unapproveEbom,
listEbomLines,
addEbomLine,
updateEbomLine,
STATUS_MAP,
BIZ_TYPE_OPTIONS,
BIZ_STATUS_OPTIONS,
PICK_TYPE_OPTIONS,
USAGE_TYPE_OPTIONS,
SUPPLY_TYPE_OPTIONS,
type EbomHeader,
type EbomLine
} from '@/api/rd/ebom'
import { listMdItem, type MdItem } from '@/api/masterdata/item'
import { listItemType, handleTree, type ItemType } from '@/api/masterdata/itemtype'
const route = useRoute()
const router = useRouter()
// ============ 页面模式 ============
const bomId = computed(() => {
const raw = route.params.id
if (!raw) return undefined
const n = Number(raw)
return Number.isFinite(n) ? n : undefined
})
const mode = computed<'new' | 'edit' | 'view'>(() => {
if (route.name === 'EbomNew') return 'new'
if (route.name === 'EbomEdit') return 'edit'
return 'view'
})
const isReadonly = computed(() => mode.value === 'view' || form.status === 'APPROVED')
// ============ 表单数据 ============
const collapsed = ref(false)
const saveLoading = ref(false)
const form = reactive<any>({
bomId: undefined,
bomCode: '',
bomName: '',
bomDate: new Date().toISOString().slice(0, 10),
version: '1.0',
versionDesc: '',
status: 'DRAFT',
businessType: 'MECHANICAL',
businessStatus: 'NORMAL',
drawingNo: '',
itemId: undefined,
itemCode: '',
itemName: '',
itemSpec: '',
unitName: '',
baseQty: 1,
operatorName: 'admin',
approverName: '',
approveDate: '',
remark: ''
})
const formRules: FormRules = {
bomDate: [{ required: true, message: '请选择单据日期', trigger: 'blur' }],
businessType: [{ required: true, message: '请选择业务类型', trigger: 'change' }],
itemCode: [{ required: true, message: '请选择产品物料', trigger: 'blur' }],
baseQty: [{ required: true, message: '请输入母件基数', trigger: 'blur' }]
}
const versionNumber = computed({
get() {
const n = parseFloat(form.version || '1.0')
return Number.isFinite(n) ? n : 1.0
},
set(v: number) {
form.version = v.toFixed(1)
}
})
const lines = ref<any[]>([])
// ============ 物料明细操作 ============
function addEmptyLine() {
lines.value.push({
lineId: undefined,
bomItemId: undefined,
bomItemCode: '',
bomItemName: '',
bomItemSpec: '',
unitOfMeasure: '',
unitName: '',
itemOrProduct: 'ITEM',
quantity: 1,
supplyType: 'PURCHASE',
pickType: 'ORDER',
usageType: 'RATIO',
drawingNo: '',
remark: '',
_isNew: true
})
}
function removeLine(idx: number) {
lines.value.splice(idx, 1)
}
// ============ 数据加载 ============
async function loadData() {
if (!bomId.value) {
lines.value = []
addEmptyLine()
addEmptyLine()
return
}
try {
const header = (await getEbom(bomId.value)).data
Object.assign(form, {
...header,
bomDate: header.bomDate || header.createTime?.slice(0, 10) || '',
operatorName: header.operatorName || header.createBy || 'admin',
businessType: header.businessType || 'MECHANICAL',
businessStatus: header.businessStatus || 'NORMAL'
})
const linesRes = await listEbomLines({ bomCode: header.bomCode, pageNum: 1, pageSize: 500 })
lines.value = (linesRes.rows || []).map(row => ({
...row,
pickType: row.pickType || row.attr1 || 'ORDER',
usageType: row.usageType || row.attr2 || 'RATIO',
drawingNo: row.drawingNo || ''
}))
if (lines.value.length === 0) addEmptyLine()
} catch (e) {
console.error(e)
ElMessage.error('加载BOM数据失败')
}
}
// ============ 保存 ============
/** 校验子件无重复物料同一BOM下子物料编码/ID不能重复 */
function validateNoDuplicateSubItems(): boolean {
const withItem = lines.value.filter((row) => row.bomItemId != null || (row.bomItemCode && String(row.bomItemCode).trim()))
const byCode = new Map<string, number>()
const byId = new Map<number, number>()
for (let i = 0; i < withItem.length; i++) {
const row = withItem[i]
const code = (row.bomItemCode && String(row.bomItemCode).trim()) || ''
const id = row.bomItemId
if (code && byCode.has(code)) {
ElMessage.warning(`存在重复的子物料编码:${code},请删除重复行后再保存`)
return false
}
if (id != null && byId.has(id)) {
ElMessage.warning(`存在重复的子物料:${row.bomItemCode || id},请删除重复行后再保存`)
return false
}
if (code) byCode.set(code, i)
if (id != null) byId.set(id, i)
}
return true
}
async function handleSave() {
if (!form.itemId) {
ElMessage.warning('请先选择产品物料')
return
}
if (!validateNoDuplicateSubItems()) {
return
}
saveLoading.value = true
try {
const headerPayload: Partial<EbomHeader> = {
bomId: form.bomId,
bomCode: form.bomCode,
bomName: form.bomName || `${form.itemName} BOM`,
bomDate: form.bomDate,
version: form.version,
versionDesc: form.versionDesc,
status: form.status,
businessType: form.businessType,
businessStatus: form.businessStatus,
drawingNo: form.drawingNo,
itemId: form.itemId,
itemCode: form.itemCode,
itemName: form.itemName,
itemSpec: form.itemSpec,
unitName: form.unitName,
baseQty: form.baseQty,
remark: form.remark
}
const saved = headerPayload.bomId
? (await updateEbom(headerPayload)).data
: (await addEbom(headerPayload)).data
Object.assign(form, saved)
// 保存明细行
for (const row of lines.value) {
if (!row.bomItemId) continue
const linePayload: Partial<EbomLine> = {
lineId: row.lineId || row.bomId, // 兼容旧字段
bomId: form.bomId,
bomCode: form.bomCode,
bomItemId: row.bomItemId,
bomItemCode: row.bomItemCode,
bomItemName: row.bomItemName,
bomItemSpec: row.bomItemSpec,
unitOfMeasure: row.unitOfMeasure || row.unitName || '',
unitName: row.unitName,
itemOrProduct: row.itemOrProduct || 'ITEM',
quantity: row.quantity,
supplyType: row.supplyType,
lossRate: row.lossRate,
remark: row.remark
}
if (row._isNew || !row.lineId) {
const savedLine = (await addEbomLine({
...linePayload,
lineId: undefined,
// 兼容旧 API 所需字段
itemId: form.itemId,
itemCode: form.itemCode,
itemName: form.itemName,
bomName: form.bomName,
version: form.version,
versionDesc: form.versionDesc,
status: form.status,
enableFlag: 'Y',
attr1: row.pickType,
attr2: row.usageType
} as any)).data
row.lineId = savedLine?.lineId || savedLine?.bomId
row._isNew = false
} else {
await updateEbomLine({
...linePayload,
itemId: form.itemId,
itemCode: form.itemCode,
itemName: form.itemName,
bomName: form.bomName,
version: form.version,
status: form.status,
enableFlag: 'Y',
attr1: row.pickType,
attr2: row.usageType
} as any)
}
}
ElMessage.success('保存成功')
if (mode.value === 'new' && form.bomId) {
router.replace({ name: 'EbomEdit', params: { id: form.bomId } })
}
} catch (e) {
console.error(e)
ElMessage.error('保存失败')
} finally {
saveLoading.value = false
}
}
// ============ 审核/反审核 ============
async function handleApprove() {
try {
await ElMessageBox.confirm('确认审核此BOM单据', '提示', { type: 'warning' })
if (form.bomId) {
await approveEbom(form.bomId)
}
form.status = 'APPROVED'
form.approveDate = new Date().toISOString().slice(0, 10)
ElMessage.success('审核成功')
} catch (error: any) {
if (error !== 'cancel') ElMessage.error('审核失败')
}
}
async function handleUnApprove() {
try {
await ElMessageBox.confirm('确认反审核此BOM单据', '提示', { type: 'warning' })
if (form.bomId) {
await unapproveEbom(form.bomId)
}
form.status = 'DRAFT'
form.approveDate = ''
form.approverName = ''
ElMessage.success('反审核成功')
} catch (error: any) {
if (error !== 'cancel') ElMessage.error('反审核失败')
}
}
function handleCancel() {
router.push({ name: 'EbomList' })
}
// ============ 物料分类树 ============
const itemTypeTreeData = ref<any[]>([])
const treeFilterText = ref('')
const itemTreeFilterText = ref('')
const treeRef = ref()
const itemTreeRef = ref()
function filterNode(value: string, data: any) {
if (!value) return true
return data.label?.includes(value)
}
watch(treeFilterText, (val) => {
treeRef.value?.filter(val)
})
watch(itemTreeFilterText, (val) => {
itemTreeRef.value?.filter(val)
})
async function loadItemTypeTree() {
try {
const res = await listItemType({ enableFlag: 'Y' })
const list = (res as any).data || res || []
itemTypeTreeData.value = Array.isArray(list) ? handleTree(list).map(toTreeNode) : []
} catch {
itemTypeTreeData.value = []
}
}
function toTreeNode(item: ItemType): any {
return {
id: item.itemTypeId,
label: `${item.itemTypeCode || ''}-${item.itemTypeName || ''}`.replace(/^-/, ''),
children: item.children?.map(toTreeNode) || []
}
}
function getSupplyLabel(value?: string): string {
const map: Record<string, string> = {
PRODUCT: '生产', ITEM: '采购', PRODUCE: '生产',
PURCHASE: '采购', OUTSOURCE: '委外', ASSEMBLY: '装配', PROCESS: '加工'
}
return map[value || ''] || value || '-'
}
// ============ 选择产品(母件)弹窗 ============
const productDialogVisible = ref(false)
const productLoading = ref(false)
const productList = ref<MdItem[]>([])
const productTotal = ref(0)
const productQuery = reactive({ itemCode: '', itemName: '', itemTypeId: undefined as number | undefined, pageNum: 1, pageSize: 100 })
function openProductDialog() {
productDialogVisible.value = true
productQuery.itemCode = ''
productQuery.itemName = ''
productQuery.itemTypeId = undefined
searchProductItems()
}
async function searchProductItems() {
productLoading.value = true
try {
const res = await listMdItem({
itemCode: productQuery.itemCode || undefined,
itemName: productQuery.itemName || undefined,
itemTypeId: productQuery.itemTypeId,
enableFlag: 'Y',
pageNum: productQuery.pageNum,
pageSize: productQuery.pageSize
})
productList.value = res.rows
productTotal.value = res.total
} finally {
productLoading.value = false
}
}
function resetProductSearch() {
productQuery.itemCode = ''
productQuery.itemName = ''
productQuery.pageNum = 1
searchProductItems()
}
function queryAllProducts() {
productQuery.itemCode = ''
productQuery.itemName = ''
productQuery.itemTypeId = undefined
productQuery.pageNum = 1
searchProductItems()
}
function handleTreeNodeClick(node: any) {
productQuery.itemTypeId = node.id
productQuery.pageNum = 1
searchProductItems()
}
function confirmProduct(row: MdItem) {
form.itemId = row.itemId
form.itemCode = row.itemCode
form.itemName = row.itemName
form.itemSpec = row.specification
form.unitName = row.unitName
if (!form.bomName) form.bomName = `${row.itemName} BOM`
productDialogVisible.value = false
}
// ============ 选择子件物料弹窗 ============
const itemDialogVisible = ref(false)
const itemLoading = ref(false)
const itemList = ref<MdItem[]>([])
const itemTotal = ref(0)
const itemQuery = reactive({ itemCode: '', itemName: '', itemTypeId: undefined as number | undefined, pageNum: 1, pageSize: 100 })
const editingLineIndex = ref(-1)
function openItemDialog(idx: number) {
editingLineIndex.value = idx
itemDialogVisible.value = true
itemQuery.itemCode = ''
itemQuery.itemName = ''
itemQuery.itemTypeId = undefined
searchSubItems()
}
async function searchSubItems() {
itemLoading.value = true
try {
const res = await listMdItem({
itemCode: itemQuery.itemCode || undefined,
itemName: itemQuery.itemName || undefined,
itemTypeId: itemQuery.itemTypeId,
enableFlag: 'Y',
pageNum: itemQuery.pageNum,
pageSize: itemQuery.pageSize
})
// 排除母件自身
itemList.value = res.rows.filter(it => it.itemId !== form.itemId)
itemTotal.value = res.total
} finally {
itemLoading.value = false
}
}
function resetItemSearch() {
itemQuery.itemCode = ''
itemQuery.itemName = ''
itemQuery.pageNum = 1
searchSubItems()
}
function queryAllItems() {
itemQuery.itemCode = ''
itemQuery.itemName = ''
itemQuery.itemTypeId = undefined
itemQuery.pageNum = 1
searchSubItems()
}
function handleItemTreeClick(node: any) {
itemQuery.itemTypeId = node.id
itemQuery.pageNum = 1
searchSubItems()
}
function confirmItem(row: MdItem) {
const idx = editingLineIndex.value
if (idx < 0) {
itemDialogVisible.value = false
return
}
// 检查子件重复按物料ID或物料编码避免同一BOM下重复子件
const exists = lines.value.some(
(l, i) => i !== idx && (l.bomItemId === row.itemId || (l.bomItemCode && l.bomItemCode === row.itemCode))
)
if (exists) {
ElMessage.warning(`该物料已存在(${row.itemCode || row.itemName}),请勿重复添加`)
return
}
const line = lines.value[idx]
line.bomItemId = row.itemId
line.bomItemCode = row.itemCode
line.bomItemName = row.itemName
line.bomItemSpec = row.specification
line.unitOfMeasure = row.unitOfMeasure || ''
line.unitName = row.unitName || ''
line.itemOrProduct = row.itemOrProduct || 'ITEM'
itemDialogVisible.value = false
}
// ============ 生命周期 ============
onMounted(() => {
loadData()
loadItemTypeTree()
})
</script>
<style scoped>
.bom-form-page {
padding: 12px;
display: flex;
flex-direction: column;
gap: 12px;
}
.header-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 10px;
margin-bottom: 12px;
border-bottom: 1px solid #ebeef5;
}
.title {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.actions {
display: flex;
gap: 8px;
align-items: center;
}
.grid-form :deep(.el-form-item) {
margin-bottom: 12px;
}
.inline-select {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
}
.mono {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 13px;
}
.lines-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.lines-title {
font-size: 14px;
font-weight: 600;
}
.lines-actions {
display: flex;
gap: 8px;
}
/* 物料选择弹窗布局 */
.item-select-layout {
display: flex;
gap: 12px;
min-height: 460px;
}
.item-select-tree {
width: 180px;
flex-shrink: 0;
border-right: 1px solid #ebeef5;
padding-right: 12px;
overflow-y: auto;
max-height: 500px;
}
.item-select-table {
flex: 1;
overflow: hidden;
}
.pagination-wrapper {
display: flex;
justify-content: flex-end;
}
</style>