963 lines
31 KiB
Vue
963 lines
31 KiB
Vue
<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>
|