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>
|