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

963 lines
31 KiB
Vue
Raw Normal View History

2026-02-27 23:50:25 +08:00
<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>