feat(mes): 完善物料分类与物料产品前端功能

1. 新增 ItemTypeTree 左侧分类树组件(支持搜索过滤、点击切换)
2. 物料列表改为左右布局(左侧树 + 右侧列表)
3. 新增 MesItemOrProductEnum 常量({ label, value } 结构)
4. 物料/产品列用 getItemOrProductLabel 翻译,安全库存列用 dict-tag 渲染
5. 表单改为 3 列布局(span=8),增加编码生成按钮和底部 Tab 占位
6. 分类管理移除导出按钮,分类表单修复 TS 类型和枚举默认值
7. 分类 API 移除 exportItemType
pull/871/MERGE
YunaiV 2026-02-15 15:43:49 +08:00
parent 6bb27899c1
commit 27f5fcb66e
7 changed files with 313 additions and 199 deletions

View File

@ -42,10 +42,5 @@ export const MdItemTypeApi = {
// 删除物料产品分类
deleteItemType: async (id: number) => {
return await request.delete({ url: `/mes/md/item-type/delete?id=` + id })
},
// 导出物料产品分类 Excel
exportItemType: async (params: any) => {
return await request.download({ url: `/mes/md/item-type/export-excel`, params })
}
}

View File

@ -0,0 +1,78 @@
<template>
<div class="head-container">
<el-input v-model="filterName" class="mb-20px" clearable placeholder="请输入分类名称">
<template #prefix>
<Icon icon="ep:search" />
</template>
</el-input>
</div>
<div class="head-container">
<el-tree
ref="treeRef"
:data="itemTypeList"
:expand-on-click-node="false"
:filter-node-method="filterNode"
:props="defaultProps"
default-expand-all
highlight-current
node-key="id"
@node-click="handleNodeClick"
/>
</div>
</template>
<script lang="ts" setup>
import { ElTree } from 'element-plus'
import { MdItemTypeApi } from '@/api/mes/md/item/type'
import { defaultProps, handleTree } from '@/utils/tree'
defineOptions({ name: 'MesItemTypeTree' })
const filterName = ref('')
const itemTypeList = ref<Tree[]>([])
const treeRef = ref<InstanceType<typeof ElTree>>()
/** 获得分类树 */
const getTree = async () => {
const res = await MdItemTypeApi.getItemTypeSimpleList()
itemTypeList.value = []
itemTypeList.value.push(...handleTree(res))
}
/** 基于名字过滤 */
const filterNode = (name: string, data: Tree) => {
if (!name) {
return true
}
return data.name.includes(name)
}
/** 处理分类被点击 */
let currentNode: any = {}
const handleNodeClick = async (row: { [key: string]: any }, treeNode: any) => {
if (currentNode && currentNode.name === row.name) {
treeNode.checked = !treeNode.checked
} else {
treeNode.checked = true
}
if (treeNode.checked) {
currentNode = row
emits('node-click', row)
} else {
treeRef.value!.setCurrentKey(undefined)
emits('node-click', undefined)
currentNode = null
}
}
const emits = defineEmits(['node-click'])
/** 监听过滤名称 */
watch(filterName, (val) => {
treeRef.value!.filter(val)
})
/** 初始化 */
onMounted(async () => {
await getTree()
})
</script>

View File

@ -1,6 +1,6 @@
<!-- MES 物料产品的新增/修改 -->
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<Dialog :title="dialogTitle" v-model="dialogVisible" width="1000px">
<el-form
ref="formRef"
:model="formData"
@ -8,31 +8,34 @@
label-width="120px"
v-loading="formLoading"
>
<!-- TODO @AI一行 3 -->
<el-row :gutter="20">
<el-col :span="12">
<!-- TODO @AI有个生成按钮类似 /Users/yunai/Java/yudao-all-in-one/yudao-ui-admin-vue3/src/views/iot/product/product/ProductForm.vue -->
<!-- TODO @芋艿先做个假的ai把交互做通后续会有个后端接口 -->
<el-col :span="8">
<el-form-item label="物料编码" prop="code">
<el-input v-model="formData.code" placeholder="请输入物料编码" />
<el-input v-model="formData.code" placeholder="请输入物料编码">
<template #append>
<el-button @click="generateCode" :disabled="formType === 'update'">
生成
</el-button>
</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-col :span="8">
<el-form-item label="物料名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入物料名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-col :span="8">
<el-form-item label="规格型号" prop="specification">
<el-input v-model="formData.specification" placeholder="请输入规格型号" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-col :span="8">
<el-form-item label="单位" prop="unitOfMeasure">
<el-input v-model="formData.unitOfMeasure" placeholder="请输入单位编码" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-col :span="8">
<el-form-item label="物料分类" prop="itemTypeId">
<el-tree-select
v-model="formData.itemTypeId"
@ -45,7 +48,7 @@
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-col :span="8">
<el-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio
@ -58,22 +61,22 @@
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-col :span="8">
<el-form-item label="高值物料" prop="highValue">
<el-switch v-model="formData.highValue" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-col :span="8">
<el-form-item label="批次管理" prop="batchFlag">
<el-switch v-model="formData.batchFlag" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-col :span="8">
<el-form-item label="安全库存" prop="safeStockFlag">
<el-switch v-model="formData.safeStockFlag" />
</el-form-item>
</el-col>
<el-col :span="12" v-if="formData.safeStockFlag">
<el-col :span="8" v-if="formData.safeStockFlag">
<el-form-item label="最低库存量" prop="minStock">
<el-input-number
v-model="formData.minStock"
@ -84,7 +87,7 @@
/>
</el-form-item>
</el-col>
<el-col :span="12" v-if="formData.safeStockFlag">
<el-col :span="8" v-if="formData.safeStockFlag">
<el-form-item label="最高库存量" prop="maxStock">
<el-input-number
v-model="formData.maxStock"
@ -102,8 +105,28 @@
</el-col>
</el-row>
</el-form>
<!-- TODO @AI底部的 tab BOM组成批次属性替代品SIPSOP做法类似/Users/yunai/Java/yudao-all-in-one/yudao-ui-admin-vue3/src/views/member/user/detail 有很多 tab只有修改时才展示 -->
<!-- TODO @AI是不是要有个组件BarcodeImg 二维码查看 -->
<!-- 底部 Tab仅修改时展示 -->
<el-tabs v-model="activeTab" v-if="formType === 'update' && formData.id">
<!-- TODO @芋艿BOM 组成 BOM 模块实现后对接 -->
<el-tab-pane label="BOM 组成" name="bom" lazy>
<el-empty description="BOM 组成(待实现)" />
</el-tab-pane>
<!-- TODO @芋艿批次属性等批次模块实现后对接 -->
<el-tab-pane label="批次属性" name="batch" lazy>
<el-empty description="批次属性(待实现)" />
</el-tab-pane>
<!-- TODO @芋艿替代品等替代品模块实现后对接 -->
<el-tab-pane label="替代品" name="substitute" lazy>
<el-empty description="替代品(待实现)" />
</el-tab-pane>
<!-- TODO @芋艿SIP/SOP等工艺模块实现后对接 -->
<el-tab-pane label="SIP" name="sip" lazy>
<el-empty description="SIP待实现" />
</el-tab-pane>
<el-tab-pane label="SOP" name="sop" lazy>
<el-empty description="SOP待实现" />
</el-tab-pane>
</el-tabs>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
@ -112,6 +135,7 @@
</template>
<script setup lang="ts">
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import { generateRandomStr } from '@/utils'
import { MdItemApi, MdItemVO } from '@/api/mes/md/item'
import { MdItemTypeApi, MdItemTypeVO } from '@/api/mes/md/item/type'
import { CommonStatusEnum } from '@/utils/constants'
@ -127,12 +151,13 @@ const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const activeTab = ref('bom') // Tab
const formData = ref({
id: undefined,
code: undefined,
name: undefined,
specification: undefined,
unitOfMeasure: undefined, // TODO @ unitMeasureId
unitOfMeasure: undefined,
itemTypeId: undefined,
status: CommonStatusEnum.ENABLE,
safeStockFlag: false,
@ -152,11 +177,18 @@ const formRules = reactive({
const formRef = ref() // Ref
const itemTypeList = ref<MdItemTypeVO[]>([]) //
/** 生成物料编码 */
const generateCode = () => {
// TODO @
formData.value.code = 'IF' + generateRandomStr(12)
}
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
activeTab.value = 'bom'
resetForm()
//
if (id) {

View File

@ -1,132 +1,144 @@
<!-- MES 物料产品列表 -->
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="物料编码" prop="code">
<el-input
v-model="queryParams.code"
placeholder="请输入物料编码"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="物料名称" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入物料名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<!-- TODO @AI需要类似 /Users/yunai/Java/yudao-all-in-one/yudao-ui-admin-vue3/src/views/system/user放在左侧 -->
<!-- TODO @AI检索的时候选择父分类时子分类也要检索出来 -->
<el-form-item label="物料分类" prop="itemTypeId">
<el-tree-select
v-model="queryParams.itemTypeId"
:data="itemTypeList"
:props="defaultProps"
check-strictly
default-expand-all
placeholder="请选择物料分类"
clearable
class="!w-240px"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable class="!w-240px">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['mes:md-item:create']"
<el-row :gutter="20">
<!-- 左侧分类树 -->
<el-col :span="4" :xs="24">
<ContentWrap class="h-1/1">
<ItemTypeTree @node-click="handleTypeNodeClick" />
</ContentWrap>
</el-col>
<el-col :span="20" :xs="24">
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['mes:md-item:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<el-form-item label="物料编码" prop="code">
<el-input
v-model="queryParams.code"
placeholder="请输入物料编码"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="物料名称" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入物料名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="请选择状态"
clearable
class="!w-240px"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"
><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button
>
<el-button @click="resetQuery"
><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button
>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['mes:md-item:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['mes:md-item:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<el-table-column label="物料编码" align="center" prop="code" />
<el-table-column label="物料名称" align="center" prop="name" />
<el-table-column label="规格型号" align="center" prop="specification" />
<!-- TODO @AI放到 constants -->
<el-table-column label="单位" align="center" prop="unitOfMeasure" />
<el-table-column label="物料分类" align="center" prop="itemTypeName" />
<el-table-column label="物料/产品" align="center" prop="itemOrProduct" />
<el-table-column label="状态" align="center" prop="status">
<template #default="scope">
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<!-- TODO @AI设置安全库存 -->
<el-table-column
label="创建时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column label="操作" align="center" width="110">
<!-- TODO @芋艿标签打印 -->
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['mes:md-item:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['mes:md-item:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<el-table-column label="物料编码" align="center" prop="code" />
<el-table-column label="物料名称" align="center" prop="name" />
<el-table-column label="规格型号" align="center" prop="specification" />
<el-table-column label="单位" align="center" prop="unitOfMeasure" />
<el-table-column label="物料分类" align="center" prop="itemTypeName" />
<el-table-column label="物料/产品" align="center" prop="itemOrProduct">
<template #default="scope">
{{ getItemOrProductLabel(scope.row.itemOrProduct) }}
</template>
</el-table-column>
<el-table-column label="安全库存" align="center" prop="safeStockFlag">
<template #default="scope">
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.safeStockFlag" />
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status">
<template #default="scope">
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column
label="创建时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column label="操作" align="center" width="110">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['mes:md-item:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['mes:md-item:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
</el-col>
</el-row>
<!-- 表单弹窗添加/修改 -->
<MdItemForm ref="formRef" @success="getList" />
@ -135,14 +147,12 @@
<script setup lang="ts">
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import { handleTree } from '@/utils/tree'
import { defaultProps } from '@/utils/tree'
import download from '@/utils/download'
import { MdItemApi, MdItemVO } from '@/api/mes/md/item'
import { MdItemTypeApi, MdItemTypeVO } from '@/api/mes/md/item/type'
import MdItemForm from './MdItemForm.vue'
import ItemTypeTree from './ItemTypeTree.vue'
import { getItemOrProductLabel } from '@/views/mes/utils/constants'
/** MES 物料产品 列表 */
defineOptions({ name: 'MesMdItem' })
const message = useMessage() //
@ -161,7 +171,6 @@ const queryParams = reactive({
})
const queryFormRef = ref() //
const exportLoading = ref(false) //
const itemTypeList = ref<MdItemTypeVO[]>([]) //
/** 查询列表 */
const getList = async () => {
@ -184,6 +193,13 @@ const handleQuery = () => {
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
queryParams.itemTypeId = undefined
handleQuery()
}
/** 处理分类树节点点击 */
const handleTypeNodeClick = (row: any) => {
queryParams.itemTypeId = row?.id
handleQuery()
}
@ -224,8 +240,5 @@ const handleExport = async () => {
/** 初始化 **/
onMounted(async () => {
await getList()
//
const categoryData = await MdItemTypeApi.getItemTypeSimpleList()
itemTypeList.value = handleTree(categoryData)
})
</script>

View File

@ -26,10 +26,9 @@
<el-input v-model="formData.name" placeholder="请输入分类名称" />
</el-form-item>
<el-form-item label="物料/产品标识" prop="itemOrProduct">
<!-- TODO @AI搞个 mes/constants类似 ts然后这个字段要翻译掉另外MdItemTypeForm 里的相关也要通过这个方式复用 -->
<el-radio-group v-model="formData.itemOrProduct">
<el-radio value="ITEM">物料</el-radio>
<el-radio value="PRODUCT">产品</el-radio>
<el-radio :value="MesItemOrProductEnum.ITEM.value">物料</el-radio>
<el-radio :value="MesItemOrProductEnum.PRODUCT.value">产品</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="显示排序" prop="sort">
@ -61,6 +60,7 @@ import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import { MdItemTypeApi, MdItemTypeVO } from '@/api/mes/md/item/type'
import { defaultProps, handleTree } from '@/utils/tree'
import { CommonStatusEnum } from '@/utils/constants'
import { MesItemOrProductEnum } from '@/views/mes/utils/constants'
defineOptions({ name: 'MdItemTypeForm' })
@ -72,14 +72,14 @@ const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const formData = ref({
id: undefined,
parentId: undefined,
code: undefined,
name: undefined,
itemOrProduct: 'ITEM',
id: undefined as unknown as number,
parentId: undefined as unknown as number,
code: undefined as unknown as string,
name: undefined as unknown as string,
itemOrProduct: MesItemOrProductEnum.ITEM.value as string,
sort: 0,
status: CommonStatusEnum.ENABLE,
remark: undefined
remark: undefined as unknown as string
})
const formRules = reactive({
parentId: [{ required: true, message: '上级分类不能为空', trigger: 'blur' }],
@ -109,7 +109,6 @@ const open = async (type: string, id?: number, parentId?: number) => {
}
//
if (parentId) {
// TODO @AITS2322: Type number is not assignable to type undefined
formData.value.parentId = parentId
}
await getItemTypeTree()
@ -147,8 +146,7 @@ const resetForm = () => {
parentId: undefined,
code: undefined,
name: undefined,
// TODO @AI使 constants
itemOrProduct: 'ITEM',
itemOrProduct: MesItemOrProductEnum.ITEM.value,
sort: 0,
status: CommonStatusEnum.ENABLE,
remark: undefined

View File

@ -39,16 +39,6 @@
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<!-- TODO @AI不需要导出 -->
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['mes:md-item-type:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
<el-button type="danger" plain @click="toggleExpandAll">
<Icon icon="ep:sort" class="mr-5px" /> 展开/折叠
</el-button>
@ -69,8 +59,11 @@
>
<el-table-column label="分类名称" align="left" prop="name" />
<el-table-column label="分类编码" align="center" prop="code" />
<!-- TODO @AI搞个 mes/constants类似 ts然后这个字段要翻译掉另外MdItemTypeForm 里的相关也要通过这个方式复用 -->
<el-table-column label="物料/产品" align="center" prop="itemOrProduct" />
<el-table-column label="物料/产品" align="center" prop="itemOrProduct">
<template #default="scope">
{{ getItemOrProductLabel(scope.row.itemOrProduct) }}
</template>
</el-table-column>
<el-table-column label="排序" align="center" prop="sort" />
<el-table-column label="状态" align="center" prop="status">
<template #default="scope">
@ -123,8 +116,8 @@
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import { handleTree } from '@/utils/tree'
import download from '@/utils/download'
import { MdItemTypeApi, MdItemTypeVO } from '@/api/mes/md/item/type'
import { getItemOrProductLabel } from '@/views/mes/utils/constants'
import MdItemTypeForm from './MdItemTypeForm.vue'
defineOptions({ name: 'MesMdItemType' })
@ -139,7 +132,6 @@ const queryParams = reactive({
status: undefined
})
const queryFormRef = ref() //
const exportLoading = ref(false) //
/** 查询列表 */
const getList = async () => {
@ -182,21 +174,6 @@ const handleDelete = async (id: number) => {
} catch {}
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
//
await message.exportConfirm()
//
exportLoading.value = true
const data = await MdItemTypeApi.exportItemType(queryParams)
download.excel(data, '物料产品分类.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 展开/折叠操作 */
const isExpandAll = ref(true) //
const refreshTable = ref(true) //

View File

@ -0,0 +1,21 @@
/** MES 物料/产品标识枚举 */
export const MesItemOrProductEnum = {
ITEM: {
label: '物料',
value: 'ITEM'
},
PRODUCT: {
label: '产品',
value: 'PRODUCT'
}
} as const
/** 获取物料/产品标识的标签 */
export const getItemOrProductLabel = (value: string): string => {
for (const item of Object.values(MesItemOrProductEnum)) {
if (item.value === value) {
return item.label
}
}
return value
}