feat: EBOM选择物料弹窗增加左侧分类树(issues-0311-3)

- form.vue: 选择物料弹窗从el-select远程搜索改为左右布局
  左侧el-tree加载物料分类树(getItemTypeTreeselect)
  右侧el-table展示物料列表,支持编码/名称搜索
  点击树节点按分类过滤物料,参考Item管理页实现
- form.vue: 表格和输入框去掉size=small,恢复默认高度
- list.vue: 列表列宽从固定width改为min-width,增加show-overflow-tooltip

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
panchengyong
2026-03-13 00:42:06 +08:00
parent c1110d6b08
commit 34cbf08144
2 changed files with 206 additions and 56 deletions

View File

@@ -147,7 +147,7 @@
</div> </div>
</div> </div>
<el-table :data="lines" border size="small" max-height="420"> <el-table :data="lines" border max-height="420">
<el-table-column label="序号" type="index" width="60" align="center" /> <el-table-column label="序号" type="index" width="60" align="center" />
<el-table-column label="物料编码" min-width="160"> <el-table-column label="物料编码" min-width="160">
<template #default="{ row, $index }"> <template #default="{ row, $index }">
@@ -162,7 +162,7 @@
<el-table-column prop="unitName" label="主计量" width="90" align="center" /> <el-table-column prop="unitName" label="主计量" width="90" align="center" />
<el-table-column label="计划线路" width="120" align="center"> <el-table-column label="计划线路" width="120" align="center">
<template #default="{ row }"> <template #default="{ row }">
<el-select v-model="row.supplyType" :disabled="readonly" size="small" style="width: 100%"> <el-select v-model="row.supplyType" :disabled="readonly" style="width: 100%">
<el-option label="采购" value="PURCHASE" /> <el-option label="采购" value="PURCHASE" />
<el-option label="自制" value="PRODUCE" /> <el-option label="自制" value="PRODUCE" />
<el-option label="委外" value="OUTSOURCE" /> <el-option label="委外" value="OUTSOURCE" />
@@ -171,7 +171,7 @@
</el-table-column> </el-table-column>
<el-table-column label="领料方式" width="120" align="center"> <el-table-column label="领料方式" width="120" align="center">
<template #default="{ row }"> <template #default="{ row }">
<el-select v-model="row.attr1" :disabled="readonly" size="small" style="width: 100%"> <el-select v-model="row.attr1" :disabled="readonly" style="width: 100%">
<el-option label="按单领用" value="ORDER" /> <el-option label="按单领用" value="ORDER" />
<el-option label="直接领用" value="DIRECT" /> <el-option label="直接领用" value="DIRECT" />
</el-select> </el-select>
@@ -179,12 +179,12 @@
</el-table-column> </el-table-column>
<el-table-column prop="quantity" label="分子" width="90" align="right"> <el-table-column prop="quantity" label="分子" width="90" align="right">
<template #default="{ row }"> <template #default="{ row }">
<el-input-number v-model="row.quantity" :min="0" :precision="4" :disabled="readonly" size="small" style="width: 100%" /> <el-input-number v-model="row.quantity" :min="0" :precision="4" :disabled="readonly" style="width: 100%" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="用量方式" width="120" align="center"> <el-table-column label="用量方式" width="120" align="center">
<template #default="{ row }"> <template #default="{ row }">
<el-select v-model="row.attr2" :disabled="readonly" size="small" style="width: 100%"> <el-select v-model="row.attr2" :disabled="readonly" style="width: 100%">
<el-option label="按比例" value="RATIO" /> <el-option label="按比例" value="RATIO" />
<el-option label="固定用量" value="FIXED" /> <el-option label="固定用量" value="FIXED" />
<el-option label="变动用量" value="VARIABLE" /> <el-option label="变动用量" value="VARIABLE" />
@@ -226,36 +226,80 @@
</el-dialog> </el-dialog>
<!-- 选择子件物料 --> <!-- 选择子件物料 -->
<el-dialog v-model="itemDialogVisible" title="选择物料" width="640px" destroy-on-close> <el-dialog v-model="itemDialogVisible" title="选择物料" width="980px" destroy-on-close>
<el-select <div class="item-dialog-body">
v-model="selectedItemId" <div class="item-tree-panel" v-loading="itemTypeTreeLoading">
filterable <el-input
remote v-model="itemTypeFilterText"
placeholder="输入编码或名称搜索" placeholder="输入分类名称"
:remote-method="searchItems" clearable
:loading="itemLoading" :prefix-icon="Search"
style="width: 100%" style="margin-bottom: 12px"
> />
<el-option <el-tree
v-for="it in itemOptions" ref="itemTypeTreeRef"
:key="it.itemId" :data="itemTypeTreeData"
:label="`${it.itemCode} - ${it.itemName}`" :props="{ label: 'label', children: 'children' }"
:value="it.itemId" node-key="id"
/> highlight-current
</el-select> default-expand-all
<template #footer> :expand-on-click-node="false"
<el-button @click="itemDialogVisible = false">取消</el-button> :filter-node-method="filterItemTypeNode"
<el-button type="primary" @click="confirmItem">确定</el-button> @node-click="handleItemTypeNodeClick"
</template> />
</div>
<div class="item-table-panel">
<div class="item-search-bar">
<el-form :inline="true" @submit.prevent="loadSelectableItems">
<el-form-item label="物料编码">
<el-input v-model="itemSearchCode" placeholder="请输入" clearable style="width: 140px" />
</el-form-item>
<el-form-item label="物料名称">
<el-input v-model="itemSearchName" placeholder="请输入" clearable style="width: 140px" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="itemLoading" @click="loadSelectableItems">搜索</el-button>
<el-button @click="resetItemSearch">重置</el-button>
</el-form-item>
</el-form>
<el-button link type="primary" @click="handleQueryAll">查询所有</el-button>
</div>
<el-table
v-loading="itemLoading"
:data="itemOptions"
border
stripe
max-height="380"
>
<el-table-column prop="itemCode" label="物料编码" width="160" show-overflow-tooltip />
<el-table-column prop="itemName" label="物料名称" min-width="120" show-overflow-tooltip />
<el-table-column prop="specification" label="型号规格" min-width="140" show-overflow-tooltip />
<el-table-column prop="unitName" label="主计量" width="80" align="center" />
<el-table-column prop="itemOrProduct" label="供应方式" width="90" align="center">
<template #default="{ row }">
{{ row.itemOrProduct === 'PRODUCT' ? '自制' : '采购' }}
</template>
</el-table-column>
<el-table-column label="操作" width="70" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="selectItem(row)">选择</el-button>
</template>
</el-table-column>
</el-table>
<div class="item-table-pagination">
<span> {{ itemOptions.length }} </span>
</div>
</div>
</div>
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue' import { computed, onMounted, reactive, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { ArrowDown, Plus } from '@element-plus/icons-vue' import { ArrowDown, Plus, Search } from '@element-plus/icons-vue'
import { import {
addBomHeader, addBomHeader,
addBomLine, addBomLine,
@@ -268,6 +312,7 @@ import {
type BomLine type BomLine
} from '@/api/masterdata/bom' } from '@/api/masterdata/bom'
import { listMdItem, type MdItem } from '@/api/masterdata/item' import { listMdItem, type MdItem } from '@/api/masterdata/item'
import { getItemTypeTreeselect } from '@/api/masterdata/itemtype'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
@@ -552,44 +597,95 @@ function confirmProduct() {
const itemDialogVisible = ref(false) const itemDialogVisible = ref(false)
const itemLoading = ref(false) const itemLoading = ref(false)
const itemOptions = ref<MdItem[]>([]) const itemOptions = ref<MdItem[]>([])
const selectedItemId = ref<number | undefined>()
const editingLineIndex = ref<number>(-1) const editingLineIndex = ref<number>(-1)
const itemSearchCode = ref('')
const itemSearchName = ref('')
const itemTypeTreeLoading = ref(false)
const itemTypeTreeData = ref<any[]>([])
const selectedItemTypeId = ref<number | undefined>()
const itemTypeFilterText = ref('')
const itemTypeTreeRef = ref<any>(null)
function openItemDialog(idx: number) { function filterItemTypeNode(value: string, data: any) {
editingLineIndex.value = idx if (!value) return true
selectedItemId.value = lines.value[idx]?.bomItemId return data.label?.indexOf(value) !== -1
itemDialogVisible.value = true
} }
async function searchItems(query: string) { watch(itemTypeFilterText, (val) => {
if (!query) { itemTypeTreeRef.value?.filter(val)
itemOptions.value = [] })
return
async function openItemDialog(idx: number) {
editingLineIndex.value = idx
itemSearchCode.value = ''
itemSearchName.value = ''
selectedItemTypeId.value = undefined
itemTypeFilterText.value = ''
itemDialogVisible.value = true
await loadItemTypeTree()
await loadSelectableItems()
}
async function loadItemTypeTree() {
itemTypeTreeLoading.value = true
try {
const res = await getItemTypeTreeselect()
itemTypeTreeData.value = res.data || []
} finally {
itemTypeTreeLoading.value = false
} }
}
async function loadSelectableItems() {
itemLoading.value = true itemLoading.value = true
try { try {
const res = await listMdItem({ itemCode: query, enableFlag: 'Y', pageNum: 1, pageSize: 50 }) const params: Record<string, any> = {
enableFlag: 'Y',
pageNum: 1,
pageSize: 100
}
if (selectedItemTypeId.value) params.itemTypeId = selectedItemTypeId.value
if (itemSearchCode.value.trim()) params.itemCode = itemSearchCode.value.trim()
if (itemSearchName.value.trim()) params.itemName = itemSearchName.value.trim()
const res = await listMdItem(params)
itemOptions.value = res.rows.filter(it => it.itemId !== form.itemId) itemOptions.value = res.rows.filter(it => it.itemId !== form.itemId)
} finally { } finally {
itemLoading.value = false itemLoading.value = false
} }
} }
function confirmItem() { function resetItemSearch() {
itemSearchCode.value = ''
itemSearchName.value = ''
selectedItemTypeId.value = undefined
itemTypeTreeRef.value?.setCurrentKey(null)
loadSelectableItems()
}
function handleQueryAll() {
itemSearchCode.value = ''
itemSearchName.value = ''
selectedItemTypeId.value = undefined
itemTypeTreeRef.value?.setCurrentKey(null)
loadSelectableItems()
}
function handleItemTypeNodeClick(node: any) {
selectedItemTypeId.value = node?.id ?? node?.itemTypeId
loadSelectableItems()
}
function selectItem(selected: MdItem) {
const idx = editingLineIndex.value const idx = editingLineIndex.value
const selected = itemOptions.value.find(i => i.itemId === selectedItemId.value) if (idx < 0) return
if (idx < 0 || !selected) {
itemDialogVisible.value = false
return
}
const row = lines.value[idx] const row = lines.value[idx]
row.bomItemId = selected.itemId row.bomItemId = selected.itemId
row.bomItemCode = selected.itemCode row.bomItemCode = selected.itemCode
row.bomItemName = selected.itemName row.bomItemName = selected.itemName
row.bomItemSpec = selected.specification row.bomItemSpec = selected.specification
row.unitOfMeasure = selected.unitOfMeasure || '' // 必填 row.unitOfMeasure = selected.unitOfMeasure || ''
row.unitName = selected.unitName || '' row.unitName = selected.unitName || ''
row.itemOrProduct = selected.itemOrProduct || 'ITEM' // 必填 row.itemOrProduct = selected.itemOrProduct || 'ITEM'
itemDialogVisible.value = false itemDialogVisible.value = false
} }
@@ -652,5 +748,57 @@ onMounted(load)
display: flex; display: flex;
gap: 8px; gap: 8px;
} }
.item-dialog-body {
display: flex;
gap: 12px;
min-height: 460px;
}
.item-tree-panel {
width: 220px;
min-width: 220px;
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 8px;
overflow: auto;
max-height: 460px;
}
.item-table-panel {
flex: 1;
display: flex;
flex-direction: column;
}
.item-search-bar {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 4px;
margin-bottom: 8px;
}
.item-search-bar :deep(.el-form-item) {
margin-bottom: 0;
margin-right: 8px;
}
.item-table-pagination {
margin-top: 8px;
text-align: right;
font-size: 13px;
color: #909399;
}
.lines-card :deep(.el-table__body .el-table__cell) {
padding: 8px 0;
}
.lines-card :deep(.el-input__wrapper),
.lines-card :deep(.el-input-number .el-input__wrapper),
.lines-card :deep(.el-select .el-input__wrapper) {
min-height: 36px;
}
</style> </style>

View File

@@ -86,10 +86,11 @@
:data="bomList" :data="bomList"
border border
stripe stripe
style="width: 100%"
@row-dblclick="goView" @row-dblclick="goView"
> >
<el-table-column label="序号" type="index" width="60" align="center" /> <el-table-column label="序号" type="index" width="60" align="center" />
<el-table-column prop="bomCode" label="单据编码" width="140"> <el-table-column prop="bomCode" label="单据编码" min-width="200" show-overflow-tooltip>
<template #default="{ row }"> <template #default="{ row }">
<el-link type="primary" @click="goView(row)">{{ row.bomCode }}</el-link> <el-link type="primary" @click="goView(row)">{{ row.bomCode }}</el-link>
</template> </template>
@@ -102,9 +103,9 @@
<el-table-column prop="version" label="版本号" width="90" align="center"> <el-table-column prop="version" label="版本号" width="90" align="center">
<template #default="{ row }">{{ row.version || '1.00' }}</template> <template #default="{ row }">{{ row.version || '1.00' }}</template>
</el-table-column> </el-table-column>
<el-table-column prop="versionDesc" label="版本说明" width="120" show-overflow-tooltip /> <el-table-column prop="versionDesc" label="版本说明" min-width="120" show-overflow-tooltip />
<el-table-column prop="drawingNo" label="图纸号" width="150" show-overflow-tooltip /> <el-table-column prop="drawingNo" label="图纸号" min-width="150" show-overflow-tooltip />
<el-table-column prop="status" label="单据状态" width="100" align="center"> <el-table-column prop="status" label="单据状态" width="100" align="center" show-overflow-tooltip>
<template #default="{ row }"> <template #default="{ row }">
<span :class="getStatusClass(row.status)">{{ getStatusLabel(row.status) }}</span> <span :class="getStatusClass(row.status)">{{ getStatusLabel(row.status) }}</span>
</template> </template>
@@ -127,22 +128,23 @@
:data="bomList" :data="bomList"
border border
stripe stripe
style="width: 100%"
@selection-change="handleSelectionChange" @selection-change="handleSelectionChange"
> >
<el-table-column type="selection" width="50" align="center" /> <el-table-column type="selection" width="50" align="center" />
<el-table-column prop="bomCode" label="单据编码" width="140" align="center" /> <el-table-column prop="bomCode" label="单据编码" min-width="200" align="center" show-overflow-tooltip />
<el-table-column prop="createTime" label="单据日期" width="110" align="center"> <el-table-column prop="createTime" label="单据日期" width="110" align="center">
<template #default="{ row }">{{ formatDate(row.createTime) }}</template> <template #default="{ row }">{{ formatDate(row.createTime) }}</template>
</el-table-column> </el-table-column>
<el-table-column prop="status" label="单据状态" width="100" align="center"> <el-table-column prop="status" label="单据状态" width="100" align="center" show-overflow-tooltip>
<template #default="{ row }"> <template #default="{ row }">
<span :class="getStatusClass(row.status)">{{ getStatusLabel(row.status) }}</span> <span :class="getStatusClass(row.status)">{{ getStatusLabel(row.status) }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="businessType" label="业务类型" width="120" align="center" /> <el-table-column prop="businessType" label="业务类型" min-width="120" align="center" />
<el-table-column prop="businessDept" label="业务部门" width="120" align="center" /> <el-table-column prop="businessDept" label="业务部门" min-width="120" align="center" />
<el-table-column prop="businessUser" label="业务人员" width="120" align="center" /> <el-table-column prop="businessUser" label="业务人员" min-width="120" align="center" />
<el-table-column prop="businessStatus" label="业务状态" width="120" align="center" /> <el-table-column prop="businessStatus" label="业务状态" min-width="120" align="center" />
<el-table-column prop="approveTime" label="审核日期" width="110" align="center" /> <el-table-column prop="approveTime" label="审核日期" width="110" align="center" />
<el-table-column label="操作" width="140" align="center" fixed="right"> <el-table-column label="操作" width="140" align="center" fixed="right">
<template #default="{ row }"> <template #default="{ row }">