gaoluyang
2026-06-29 3b207cb93a1bb14d9b84ee216532a0388c56f147
银川
1.添加产品维护页面
已添加5个文件
已修改1个文件
1263 ■■■■■ 文件已修改
mdm_frontend_change.md 523 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mdm/item/index.ts 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/packages/constants/src/dict-enum.ts 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/mdm/data.ts 351 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/mdm/index.vue 214 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/mdm/modules/form.vue 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mdm_frontend_change.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,523 @@
# ç‰©æ–™ä¸»æ•°æ®æ¨¡å— (MDM) å‰ç«¯å˜æ›´æ–‡æ¡£
## ä¸€ã€æ–°å¢žé¡µé¢
### 1.1 ç‰©æ–™ä¸»æ•°æ®ç®¡ç†
#### æ¶‰åŠé¡µé¢
- ç‰©æ–™åˆ—表页 (`views/mdm/item/index.vue`)
- ç‰©æ–™æ–°å¢ž/编辑弹窗 (`views/mdm/item/ItemForm.vue`)
- ç‰©æ–™è¯¦æƒ…页 (`views/mdm/item/detail.vue`)
#### API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /mdm/item/page | ç‰©æ–™åˆ†é¡µæŸ¥è¯¢ |
| GET | /mdm/item/list | ç‰©æ–™åˆ—表查询 |
| GET | /mdm/item/get | èŽ·å–ç‰©æ–™è¯¦æƒ… |
| POST | /mdm/item/create | åˆ›å»ºç‰©æ–™ |
| PUT | /mdm/item/update | æ›´æ–°ç‰©æ–™ |
| DELETE | /mdm/item/delete | åˆ é™¤ç‰©æ–™ |
| PUT | /mdm/item/update-status | æ›´æ–°ç‰©æ–™çŠ¶æ€ |
**请求参数(物料查询):**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| code | String | å¦ | ç‰©æ–™ç¼–码 |
| name | String | å¦ | ç‰©æ–™åç§° |
| categoryId | Long | å¦ | åˆ†ç±»ID |
| itemType | Integer | å¦ | ç‰©æ–™ç±»åž‹(1:原料,2:半成品,3:成品,4:辅料) |
| status | Integer | å¦ | çŠ¶æ€ |
**响应:**
```json
{
  "code": 200,
  "data": {
    "list": [
      {
        "id": 1,
        "code": "M001",
        "name": "物料名称",
        "barCode": "条码",
        "specification": "规格型号",
        "categoryId": 10,
        "categoryName": "分类名称",
        "unitMeasureId": 1,
        "unitMeasureName": "单位名称",
        "brandId": 1,
        "brandName": "品牌名称",
        "itemType": 1,
        "status": 0,
        "purchasePrice": 100.00,
        "salesPrice": 150.00,
        "costPrice": 80.00,
        "safetyStock": 50.00,
        "minStock": 20.00,
        "maxStock": 200.00,
        "isBatchManaged": true,
        "expiryDay": 365,
        "remark": "备注"
      }
    ],
    "total": 100
  }
}
```
#### å‰ç«¯ä¿®æ”¹ç‚¹
**1. ç‰©æ–™åˆ—表页面结构:**
```html
<template>
  <ContentWrap>
    <!-- æœç´¢åŒºåŸŸ -->
    <el-form :model="queryParams" ref="queryFormRef" :inline="true">
      <el-form-item label="物料编码" prop="code">
        <el-input v-model="queryParams.code" placeholder="请输入物料编码" />
      </el-form-item>
      <el-form-item label="物料名称" prop="name">
        <el-input v-model="queryParams.name" placeholder="请输入物料名称" />
      </el-form-item>
      <el-form-item label="物料分类" prop="categoryId">
        <el-tree-select v-model="queryParams.categoryId" :data="categoryTree" />
      </el-form-item>
      <el-form-item label="物料类型" prop="itemType">
        <el-select v-model="queryParams.itemType" placeholder="请选择物料类型">
          <el-option label="原料" :value="1" />
          <el-option label="半成品" :value="2" />
          <el-option label="成品" :value="3" />
          <el-option label="辅料" :value="4" />
        </el-select>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="handleQuery">搜索</el-button>
        <el-button @click="resetQuery">重置</el-button>
      </el-form-item>
    </el-form>
    <!-- æ“ä½œæŒ‰é’® -->
    <el-row :gutter="10" class="mb8">
      <el-col :span="1.5">
        <el-button type="primary" plain @click="handleCreate">新增</el-button>
      </el-col>
      <el-col :span="1.5">
        <el-button type="success" plain @click="handleExport">导出</el-button>
      </el-col>
    </el-row>
    <!-- è¡¨æ ¼ -->
    <el-table v-loading="loading" :data="list">
      <el-table-column label="物料编码" prop="code" width="120" />
      <el-table-column label="物料名称" prop="name" width="180" />
      <el-table-column label="规格型号" prop="specification" width="150" />
      <el-table-column label="分类" prop="categoryName" width="100" />
      <el-table-column label="单位" prop="unitMeasureName" width="80" />
      <el-table-column label="物料类型" prop="itemType" width="80">
        <template #default="{ row }">
          <dict-tag :type="DICT_TYPE.MDM_ITEM_TYPE" :value="row.itemType" />
        </template>
      </el-table-column>
      <el-table-column label="采购价" prop="purchasePrice" width="100" />
      <el-table-column label="销售价" prop="salesPrice" width="100" />
      <el-table-column label="批次管理" prop="isBatchManaged" width="80">
        <template #default="{ row }">
          <el-tag v-if="row.isBatchManaged" type="success">是</el-tag>
          <el-tag v-else type="info">否</el-tag>
        </template>
      </el-table-column>
      <el-table-column label="状态" prop="status" width="80">
        <template #default="{ row }">
          <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="row.status" />
        </template>
      </el-table-column>
      <el-table-column label="操作" width="180" fixed="right">
        <template #default="{ row }">
          <el-button link type="primary" @click="handleUpdate(row)">编辑</el-button>
          <el-button link type="danger" @click="handleDelete(row)">删除</el-button>
        </template>
      </el-table-column>
    </el-table>
    <!-- åˆ†é¡µ -->
    <Pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" @pagination="getList" />
  </ContentWrap>
</template>
```
**2. ç‰©æ–™è¡¨å•弹窗:**
```html
<el-dialog v-model="dialogVisible" title="物料信息" width="800px">
  <el-form ref="formRef" :model="formData" :rules="formRules" label-width="120px">
    <el-row :gutter="20">
      <el-col :span="12">
        <el-form-item label="物料编码" prop="code">
          <el-input v-model="formData.code" placeholder="请输入物料编码" />
        </el-form-item>
      </el-col>
      <el-col :span="12">
        <el-form-item label="物料名称" prop="name">
          <el-input v-model="formData.name" placeholder="请输入物料名称" />
        </el-form-item>
      </el-col>
    </el-row>
    <el-row :gutter="20">
      <el-col :span="12">
        <el-form-item label="条码" prop="barCode">
          <el-input v-model="formData.barCode" placeholder="请输入条码" />
        </el-form-item>
      </el-col>
      <el-col :span="12">
        <el-form-item label="规格型号" prop="specification">
          <el-input v-model="formData.specification" placeholder="请输入规格型号" />
        </el-form-item>
      </el-col>
    </el-row>
    <el-row :gutter="20">
      <el-col :span="12">
        <el-form-item label="物料分类" prop="categoryId">
          <el-tree-select v-model="formData.categoryId" :data="categoryTree" />
        </el-form-item>
      </el-col>
      <el-col :span="12">
        <el-form-item label="计量单位" prop="unitMeasureId">
          <el-select v-model="formData.unitMeasureId" placeholder="请选择单位">
            <el-option v-for="unit in unitList" :key="unit.id" :label="unit.name" :value="unit.id" />
          </el-select>
        </el-form-item>
      </el-col>
    </el-row>
    <el-row :gutter="20">
      <el-col :span="12">
        <el-form-item label="品牌" prop="brandId">
          <el-select v-model="formData.brandId" placeholder="请选择品牌">
            <el-option v-for="brand in brandList" :key="brand.id" :label="brand.name" :value="brand.id" />
          </el-select>
        </el-form-item>
      </el-col>
      <el-col :span="12">
        <el-form-item label="物料类型" prop="itemType">
          <el-select v-model="formData.itemType" placeholder="请选择物料类型">
            <el-option label="原料" :value="1" />
            <el-option label="半成品" :value="2" />
            <el-option label="成品" :value="3" />
            <el-option label="辅料" :value="4" />
          </el-select>
        </el-form-item>
      </el-col>
    </el-row>
    <!-- ä»·æ ¼ä¿¡æ¯ -->
    <el-row :gutter="20">
      <el-col :span="8">
        <el-form-item label="采购价" prop="purchasePrice">
          <el-input-number v-model="formData.purchasePrice" :precision="2" />
        </el-form-item>
      </el-col>
      <el-col :span="8">
        <el-form-item label="销售价" prop="salesPrice">
          <el-input-number v-model="formData.salesPrice" :precision="2" />
        </el-form-item>
      </el-col>
      <el-col :span="8">
        <el-form-item label="成本价" prop="costPrice">
          <el-input-number v-model="formData.costPrice" :precision="2" />
        </el-form-item>
      </el-col>
    </el-row>
    <!-- åº“存管理 -->
    <el-row :gutter="20">
      <el-col :span="12">
        <el-form-item label="安全库存" prop="safetyStock">
          <el-input-number v-model="formData.safetyStock" :precision="2" />
        </el-form-item>
      </el-col>
      <el-col :span="12">
        <el-form-item label="批次管理" prop="isBatchManaged">
          <el-switch v-model="formData.isBatchManaged" />
        </el-form-item>
      </el-col>
    </el-row>
    <el-form-item label="备注" prop="remark">
      <el-input v-model="formData.remark" type="textarea" :rows="3" />
    </el-form-item>
  </el-form>
  <template #footer>
    <el-button @click="dialogVisible = false">取消</el-button>
    <el-button type="primary" @click="submitForm">确定</el-button>
  </template>
</el-dialog>
```
**3. æ•°æ®å®šä¹‰ï¼š**
```js
<script setup>
import { ref, reactive, onMounted } from 'vue'
import * as ItemApi from '@/api/mdm/item'
const loading = ref(false)
const list = ref([])
const total = ref(0)
const queryParams = reactive({
  pageNo: 1,
  pageSize: 10,
  code: '',
  name: '',
  categoryId: null,
  itemType: null,
  status: null
})
const dialogVisible = ref(false)
const formData = ref({
  id: undefined,
  code: '',
  name: '',
  barCode: '',
  specification: '',
  categoryId: null,
  unitMeasureId: null,
  brandId: null,
  itemType: 1,
  purchasePrice: null,
  salesPrice: null,
  costPrice: null,
  safetyStock: null,
  isBatchManaged: false,
  remark: ''
})
const getList = async () => {
  loading.value = true
  try {
    const data = await ItemApi.getItemPage(queryParams)
    list.value = data.list
    total.value = data.total
  } finally {
    loading.value = false
  }
}
const handleCreate = () => {
  formData.value = { ...initialFormData }
  dialogVisible.value = true
}
const handleUpdate = (row) => {
  formData.value = { ...row }
  dialogVisible.value = true
}
const submitForm = async () => {
  await formRef.value.validate()
  if (formData.value.id) {
    await ItemApi.updateItem(formData.value)
  } else {
    await ItemApi.createItem(formData.value)
  }
  dialogVisible.value = false
  getList()
}
onMounted(() => {
  getList()
})
</script>
```
---
### 1.2 ç‰©æ–™åˆ†ç±»ç®¡ç†
#### æ¶‰åŠé¡µé¢
- ç‰©æ–™åˆ†ç±»ç®¡ç†é¡µ (`views/mdm/category/index.vue`)
#### API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /mdm/category/list | åˆ†ç±»åˆ—表查询 |
| GET | /mdm/category/get | èŽ·å–åˆ†ç±»è¯¦æƒ… |
| POST | /mdm/category/create | åˆ›å»ºåˆ†ç±» |
| PUT | /mdm/category/update | æ›´æ–°åˆ†ç±» |
| DELETE | /mdm/category/delete | åˆ é™¤åˆ†ç±» |
---
### 1.3 è®¡é‡å•位管理
#### æ¶‰åŠé¡µé¢
- è®¡é‡å•位管理页 (`views/mdm/unit/index.vue`)
#### API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /mdm/unit/page | å•位分页查询 |
| GET | /mdm/unit/list | å•位列表查询 |
| POST | /mdm/unit/create | åˆ›å»ºå•位 |
| PUT | /mdm/unit/update | æ›´æ–°å•位 |
| DELETE | /mdm/unit/delete | åˆ é™¤å•位 |
---
### 1.4 å“ç‰Œç®¡ç†
#### æ¶‰åŠé¡µé¢
- å“ç‰Œç®¡ç†é¡µ (`views/mdm/brand/index.vue`)
#### API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /mdm/brand/page | å“ç‰Œåˆ†é¡µæŸ¥è¯¢ |
| GET | /mdm/brand/list | å“ç‰Œåˆ—表查询 |
| POST | /mdm/brand/create | åˆ›å»ºå“ç‰Œ |
| PUT | /mdm/brand/update | æ›´æ–°å“ç‰Œ |
| DELETE | /mdm/brand/delete | åˆ é™¤å“ç‰Œ |
---
## äºŒã€ä¿®æ”¹é¡µé¢
### 2.1 ERP äº§å“ç®¡ç†é¡µé¢
#### ä¿®æ”¹å†…容
**原页面**:`views/erp/product/index.vue`
**修改点**:
1. API æŽ¥å£ä»Ž `/erp/product/*` æ”¹ä¸ºè°ƒç”¨ `/mdm/item/*`
2. æ•°æ®å­—段映射调整(`productId` â†’ `itemId`)
3. å¢žåŠ ç‰©æ–™ç±»åž‹ç­›é€‰ï¼ˆåŽŸæ–™/半成品/成品/辅料)
**注意事项**:
- ERP äº§å“é¡µé¢æ•°æ®æ¥æºä»Ž MDM ç‰©æ–™èŽ·å–
- ä¿ç•™ ERP ç‰¹æœ‰å­—段(采购价、销售价、最低价)的编辑能力
---
### 2.2 MES ç‰©æ–™ç®¡ç†é¡µé¢
#### ä¿®æ”¹å†…容
**原页面**:`views/mes/md/item/index.vue`
**修改点**:
1. API æŽ¥å£ä»Ž `/mes/md-item/*` æ”¹ä¸ºè°ƒç”¨ `/mdm/item/*`
2. æ•°æ®å­—段映射调整(`itemTypeId` â†’ `categoryId`)
3. æ‰¹æ¬¡é…ç½®ä»Žç‹¬ç«‹é¡µé¢æ”¹ä¸ºç‰©æ–™è¯¦æƒ…页子模块
**注意事项**:
- MES ç‰©æ–™é¡µé¢æ•°æ®æ¥æºä»Ž MDM ç‰©æ–™èŽ·å–
- æ‰¹æ¬¡å±žæ€§é…ç½®ä¿ç•™åœ¨ MDM æ¨¡å—管理
---
### 2.3 WMS å•†å“ç®¡ç†é¡µé¢
#### ä¿®æ”¹å†…容
**原页面**:`views/wms/item/index.vue`
**修改点**:
1. API æŽ¥å£ä»Ž `/wms/item/*` æ”¹ä¸ºè°ƒç”¨ `/mdm/item/*`
2. SKU ç®¡ç†æ”¹ä¸ºç‰©æ–™è¯¦æƒ…页子模块
3. åº“存查询改用统一接口 `/wms/stock/*`
**注意事项**:
- WMS å•†å“é¡µé¢æ•°æ®æ¥æºä»Ž MDM ç‰©æ–™èŽ·å–
- SKU ä½œä¸ºç‰©æ–™çš„规格属性管理
---
## ä¸‰ã€æ–°å¢žå­—典类型
| å­—典类型 | å­—典值 | è¯´æ˜Ž |
|---------|-------|------|
| MDM_ITEM_TYPE | 1 | åŽŸæ–™ |
| MDM_ITEM_TYPE | 2 | åŠæˆå“ |
| MDM_ITEM_TYPE | 3 | æˆå“ |
| MDM_ITEM_TYPE | 4 | è¾…æ–™ |
| STOCK_BIZ_TYPE | 10 | é‡‡è´­å…¥åº“ |
| STOCK_BIZ_TYPE | 20 | é”€å”®å‡ºåº“ |
| STOCK_BIZ_TYPE | 30 | ç”Ÿäº§å…¥åº“ |
| STOCK_BIZ_TYPE | 40 | ç”Ÿäº§é¢†æ–™ |
| STOCK_BIZ_TYPE | 50 | ç”Ÿäº§é€€æ–™ |
| STOCK_BIZ_TYPE | 60 | é‡‡è´­é€€è´§å‡ºåº“ |
| STOCK_BIZ_TYPE | 70 | é”€å”®é€€è´§å…¥åº“ |
| STOCK_BIZ_TYPE | 80 | è°ƒæ‹¨å…¥åº“ |
| STOCK_BIZ_TYPE | 90 | è°ƒæ‹¨å‡ºåº“ |
| STOCK_BIZ_TYPE | 100 | ç›˜ç›ˆå…¥åº“ |
| STOCK_BIZ_TYPE | 110 | ç›˜äºå‡ºåº“ |
---
## å››ã€è·¯ç”±é…ç½®
```js
// æ–°å¢žè·¯ç”±
{
  path: '/mdm',
  component: Layout,
  name: 'MDM',
  meta: { title: '主数据管理', icon: 'ep:goods' },
  children: [
    {
      path: 'item',
      name: 'MdmItem',
      component: () => import('@/views/mdm/item/index.vue'),
      meta: { title: '物料管理' }
    },
    {
      path: 'category',
      name: 'MdmCategory',
      component: () => import('@/views/mdm/category/index.vue'),
      meta: { title: '物料分类' }
    },
    {
      path: 'unit',
      name: 'MdmUnit',
      component: () => import('@/views/mdm/unit/index.vue'),
      meta: { title: '计量单位' }
    },
    {
      path: 'brand',
      name: 'MdmBrand',
      component: () => import('@/views/mdm/brand/index.vue'),
      meta: { title: '品牌管理' }
    },
    {
      path: 'warehouse',
      name: 'MdmWarehouse',
      component: () => import('@/views/mdm/warehouse/index.vue'),
      meta: { title: '仓库管理' }
    }
  ]
}
```
---
## äº”、注意事项
1. **数据迁移**:执行数据迁移 SQL åŽï¼ŒåŽŸæœ‰ ERP/MES/WMS çš„物料页面将展示迁移后的 MDM æ•°æ®
2. **权限配置**:新增 MDM æ¨¡å—菜单和权限配置
3. **API å¯¹æŽ¥**:前端需要新增 MDM ç›¸å…³ API æ–‡ä»¶ (`api/mdm/item.js`, `api/mdm/category.js` ç­‰)
4. **字段兼容**:旧页面的字段名称需要映射到新字段,如 `productId` â†’ `itemId`
5. **状态同步**:物料状态变更后,需要同步更新各模块引用状态
---
**文档版本**: v1.0
**创建日期**: 2026-06-29
**作者**: Claude Code
src/api/mdm/item/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,88 @@
import type { PageParam, PageResult } from '#/packages/effects/request/src';
import { requestClient } from '#/api/request';
export namespace MdmItemApi {
  /** ç‰©æ–™ä¿¡æ¯ */
  export interface Item {
    id?: number;
    code: string;
    name: string;
    barCode?: string;
    specification?: string;
    categoryId?: number;
    categoryName?: string;
    unitMeasureId?: number;
    unitMeasureName?: string;
    brandId?: number;
    brandName?: string;
    itemType: number;
    status: number;
    purchasePrice?: number;
    salesPrice?: number;
    costPrice?: number;
    safetyStock?: number;
    minStock?: number;
    maxStock?: number;
    isBatchManaged: boolean;
    expiryDay?: number;
    remark?: string;
    createTime?: string;
  }
  /** ç‰©æ–™åˆ†é¡µæŸ¥è¯¢å‚æ•° */
  export interface PageParams extends PageParam {
    code?: string;
    name?: string;
    categoryId?: number;
    itemType?: number;
    status?: number;
  }
}
/** æŸ¥è¯¢ç‰©æ–™åˆ†é¡µåˆ—表 */
export function getItemPage(params: MdmItemApi.PageParams) {
  return requestClient.get<PageResult<MdmItemApi.Item>>('/mdm/item/page', {
    params,
  });
}
/** æŸ¥è¯¢ç‰©æ–™åˆ—表 */
export function getItemList(params?: Partial<MdmItemApi.Item>) {
  return requestClient.get<MdmItemApi.Item[]>('/mdm/item/list', { params });
}
/** æŸ¥è¯¢ç‰©æ–™è¯¦æƒ… */
export function getItem(id: number) {
  return requestClient.get<MdmItemApi.Item>(`/mdm/item/get?id=${id}`);
}
/** æ–°å¢žç‰©æ–™ */
export function createItem(data: MdmItemApi.Item) {
  return requestClient.post('/mdm/item/create', data);
}
/** ä¿®æ”¹ç‰©æ–™ */
export function updateItem(data: MdmItemApi.Item) {
  return requestClient.put('/mdm/item/update', data);
}
/** åˆ é™¤ç‰©æ–™ */
export function deleteItem(id: number) {
  return requestClient.delete(`/mdm/item/delete?id=${id}`);
}
/** æ‰¹é‡åˆ é™¤ç‰©æ–™ */
export function deleteItemList(ids: number[]) {
  return requestClient.delete(`/mdm/item/delete-list?ids=${ids.join(',')}`);
}
/** æ›´æ–°ç‰©æ–™çŠ¶æ€ */
export function updateItemStatus(id: number, status: number) {
  return requestClient.put('/mdm/item/update-status', { id, status });
}
/** å¯¼å‡ºç‰©æ–™ */
export function exportItem(params: Partial<MdmItemApi.Item>) {
  return requestClient.download('/mdm/item/export-excel', { params });
}
src/packages/constants/src/dict-enum.ts
@@ -279,6 +279,12 @@
  WMS_SHIPMENT_ORDER_TYPE: 'wms_shipment_order_type', // WMS å‡ºåº“单类型
} as const;
/** ========== MDM - ä¸»æ•°æ®ç®¡ç†æ¨¡å— ========== */
const MDM_DICT = {
  MDM_ITEM_TYPE: 'mdm_item_type', // MDM ç‰©æ–™ç±»åž‹
  STOCK_BIZ_TYPE: 'stock_biz_type', // åº“存业务类型
} as const;
/** å­—典类型枚举 - ç»Ÿä¸€å¯¼å‡º */
const DICT_TYPE = {
  ...AI_DICT,
@@ -289,6 +295,7 @@
  ...IM_DICT,
  ...IOT_DICT,
  ...MES_DICT,
  ...MDM_DICT,
  ...WMS_DICT,
  ...MEMBER_DICT,
  ...MP_DICT,
src/views/basicData/mdm/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,351 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { CommonStatusEnum, DICT_TYPE } from '#/packages/constants/src';
import { getDictOptions } from '#/packages/effects/hooks/src';
import { z } from '#/adapter/form';
/** ç‰©æ–™ç±»åž‹é€‰é¡¹ */
export const ITEM_TYPE_OPTIONS = [
  { label: '原料', value: 1 },
  { label: '半成品', value: 2 },
  { label: '成品', value: 3 },
  { label: '辅料', value: 4 },
];
/** æ–°å¢ž/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
  return [
    {
      component: 'Input',
      fieldName: 'id',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      component: 'Input',
      fieldName: 'code',
      label: '物料编码',
      componentProps: {
        placeholder: '请输入物料编码',
      },
      rules: 'required',
    },
    {
      component: 'Input',
      fieldName: 'name',
      label: '物料名称',
      componentProps: {
        placeholder: '请输入物料名称',
      },
      rules: 'required',
    },
    {
      component: 'Input',
      fieldName: 'barCode',
      label: '条码',
      componentProps: {
        placeholder: '请输入条码',
      },
    },
    {
      component: 'Input',
      fieldName: 'specification',
      label: '规格型号',
      componentProps: {
        placeholder: '请输入规格型号',
      },
    },
    {
      component: 'TreeSelect',
      fieldName: 'categoryId',
      label: '物料分类',
      componentProps: {
        placeholder: '请选择物料分类',
        allowClear: true,
        treeData: [],
        fieldNames: { label: 'name', value: 'id', children: 'children' },
      },
    },
    {
      component: 'Select',
      fieldName: 'unitMeasureId',
      label: '计量单位',
      componentProps: {
        placeholder: '请选择计量单位',
        allowClear: true,
        options: [],
        fieldNames: { label: 'name', value: 'id' },
      },
    },
    {
      component: 'Select',
      fieldName: 'brandId',
      label: '品牌',
      componentProps: {
        placeholder: '请选择品牌',
        allowClear: true,
        options: [],
        fieldNames: { label: 'name', value: 'id' },
      },
    },
    {
      component: 'Select',
      fieldName: 'itemType',
      label: '物料类型',
      componentProps: {
        placeholder: '请选择物料类型',
        options: ITEM_TYPE_OPTIONS,
      },
      rules: 'required',
    },
    {
      component: 'InputNumber',
      fieldName: 'purchasePrice',
      label: '采购价',
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 2,
        placeholder: '请输入采购价',
      },
    },
    {
      component: 'InputNumber',
      fieldName: 'salesPrice',
      label: '销售价',
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 2,
        placeholder: '请输入销售价',
      },
    },
    {
      component: 'InputNumber',
      fieldName: 'costPrice',
      label: '成本价',
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 2,
        placeholder: '请输入成本价',
      },
    },
    {
      component: 'InputNumber',
      fieldName: 'safetyStock',
      label: '安全库存',
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 2,
        placeholder: '请输入安全库存',
      },
    },
    {
      component: 'InputNumber',
      fieldName: 'minStock',
      label: '最低库存',
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 2,
        placeholder: '请输入最低库存',
      },
    },
    {
      component: 'InputNumber',
      fieldName: 'maxStock',
      label: '最高库存',
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 2,
        placeholder: '请输入最高库存',
      },
    },
    {
      component: 'Switch',
      fieldName: 'isBatchManaged',
      label: '批次管理',
      componentProps: {
        checkedChildren: '是',
        unCheckedChildren: '否',
      },
      rules: z.boolean().default(false),
    },
    {
      component: 'InputNumber',
      fieldName: 'expiryDay',
      label: '有效期(天)',
      componentProps: {
        class: '!w-full',
        min: 0,
        placeholder: '请输入有效期',
      },
    },
    {
      component: 'RadioGroup',
      fieldName: 'status',
      label: '状态',
      componentProps: {
        options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
        buttonStyle: 'solid',
        optionType: 'button',
      },
      rules: z.number().default(CommonStatusEnum.ENABLE),
    },
    {
      component: 'Textarea',
      fieldName: 'remark',
      label: '备注',
      componentProps: {
        placeholder: '请输入备注',
        rows: 3,
      },
    },
  ];
}
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'code',
      label: '物料编码',
      component: 'Input',
      componentProps: {
        placeholder: '请输入物料编码',
        allowClear: true,
      },
    },
    {
      fieldName: 'name',
      label: '物料名称',
      component: 'Input',
      componentProps: {
        placeholder: '请输入物料名称',
        allowClear: true,
      },
    },
    {
      fieldName: 'categoryId',
      label: '物料分类',
      component: 'TreeSelect',
      componentProps: {
        placeholder: '请选择物料分类',
        allowClear: true,
        treeData: [],
        fieldNames: { label: 'name', value: 'id', children: 'children' },
      },
    },
    {
      fieldName: 'itemType',
      label: '物料类型',
      component: 'Select',
      componentProps: {
        placeholder: '请选择物料类型',
        allowClear: true,
        options: ITEM_TYPE_OPTIONS,
      },
    },
    {
      fieldName: 'status',
      label: '状态',
      component: 'Select',
      componentProps: {
        placeholder: '请选择状态',
        allowClear: true,
        options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
      },
    },
  ];
}
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions['columns'] {
  return [
    { type: 'checkbox', width: 40 },
    {
      field: 'code',
      title: '物料编码',
      minWidth: 120,
    },
    {
      field: 'name',
      title: '物料名称',
      minWidth: 180,
    },
    {
      field: 'specification',
      title: '规格型号',
      minWidth: 150,
    },
    {
      field: 'categoryName',
      title: '分类',
      minWidth: 100,
    },
    {
      field: 'unitMeasureName',
      title: '单位',
      minWidth: 80,
    },
    {
      field: 'itemType',
      title: '物料类型',
      minWidth: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MDM_ITEM_TYPE },
      },
    },
    {
      field: 'purchasePrice',
      title: '采购价',
      minWidth: 100,
      formatter: ({ cellValue }) => {
        return cellValue != null ? `Â¥${Number(cellValue).toFixed(2)}` : '';
      },
    },
    {
      field: 'salesPrice',
      title: '销售价',
      minWidth: 100,
      formatter: ({ cellValue }) => {
        return cellValue != null ? `Â¥${Number(cellValue).toFixed(2)}` : '';
      },
    },
    {
      field: 'isBatchManaged',
      title: '批次管理',
      minWidth: 80,
      slots: { default: 'isBatchManaged' },
    },
    {
      field: 'status',
      title: '状态',
      minWidth: 80,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.COMMON_STATUS },
      },
    },
    {
      field: 'createTime',
      title: '创建时间',
      minWidth: 180,
      formatter: 'formatDateTime',
    },
    {
      title: '操作',
      width: 180,
      fixed: 'right',
      slots: { default: 'actions' },
    },
  ];
}
src/views/basicData/mdm/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,214 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MdmItemApi } from '#/api/mdm/item';
import { ref } from 'vue';
import { confirm, Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart, isEmpty } from '@vben/utils';
import { message, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  deleteItem,
  deleteItemList,
  exportItem,
  getItemPage,
  updateItemStatus,
} from '#/api/mdm/item';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
defineOptions({ name: 'MdmItem' });
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
  gridApi.query();
}
/** å¯¼å‡ºè¡¨æ ¼ */
async function handleExport() {
  const data = await exportItem(await gridApi.formApi.getValues());
  downloadFileFromBlobPart({ fileName: '物料.xls', source: data });
}
/** åˆ›å»ºç‰©æ–™ */
function handleCreate() {
  formModalApi.setData(null).open();
}
/** ç¼–辑物料 */
function handleEdit(row: MdmItemApi.Item) {
  formModalApi.setData(row).open();
}
/** åˆ é™¤ç‰©æ–™ */
async function handleDelete(row: MdmItemApi.Item) {
  const hideLoading = message.loading({
    content: $t('ui.actionMessage.deleting', [row.name]),
    duration: 0,
  });
  try {
    await deleteItem(row.id!);
    message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
    handleRefresh();
  } finally {
    hideLoading();
  }
}
/** æ‰¹é‡åˆ é™¤ç‰©æ–™ */
async function handleDeleteBatch() {
  await confirm($t('ui.actionMessage.deleteBatchConfirm'));
  const hideLoading = message.loading({
    content: $t('ui.actionMessage.deletingBatch'),
    duration: 0,
  });
  try {
    await deleteItemList(checkedIds.value);
    checkedIds.value = [];
    message.success($t('ui.actionMessage.deleteSuccess'));
    handleRefresh();
  } finally {
    hideLoading();
  }
}
/** æ›´æ–°çŠ¶æ€ */
async function handleStatusChange(row: MdmItemApi.Item, status: boolean) {
  const newStatus = status ? 0 : 1;
  const hideLoading = message.loading({
    content: '正在更新状态...',
    duration: 0,
  });
  try {
    await updateItemStatus(row.id!, newStatus);
    message.success('状态更新成功');
    handleRefresh();
  } finally {
    hideLoading();
  }
}
const checkedIds = ref<number[]>([]);
function handleRowCheckboxChange({
  records,
}: {
  records: MdmItemApi.Item[];
}) {
  checkedIds.value = records.map((item) => item.id!);
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) => {
          return await getItemPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          });
        },
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<MdmItemApi.Item>,
  gridEvents: {
    checkboxAll: handleRowCheckboxChange,
    checkboxChange: handleRowCheckboxChange,
  },
});
</script>
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <Grid table-title="物料列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['物料']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['mdm:item:create'],
              onClick: handleCreate,
            },
            {
              label: $t('ui.actionTitle.export'),
              type: 'primary',
              icon: ACTION_ICON.DOWNLOAD,
              auth: ['mdm:item:export'],
              onClick: handleExport,
            },
            {
              label: $t('ui.actionTitle.deleteBatch'),
              type: 'primary',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['mdm:item:delete'],
              disabled: isEmpty(checkedIds),
              onClick: handleDeleteBatch,
            },
          ]"
        />
      </template>
      <template #isBatchManaged="{ row }">
        <Tag v-if="row.isBatchManaged" color="success">是</Tag>
        <Tag v-else>否</Tag>
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['mdm:item:update'],
              onClick: handleEdit.bind(null, row),
            },
            {
              label: row.status === 0 ? '停用' : '启用',
              type: 'link',
              auth: ['mdm:item:update'],
              onClick: handleStatusChange.bind(null, row, row.status === 0),
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['mdm:item:delete'],
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [row.name]),
                confirm: handleDelete.bind(null, row),
              },
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/basicData/mdm/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,80 @@
<script lang="ts" setup>
import type { MdmItemApi } from '#/api/mdm/item';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createItem, getItem, updateItem } from '#/api/mdm/item';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
defineOptions({ name: 'ItemForm' });
const emit = defineEmits(['success']);
const formData = ref<MdmItemApi.Item>();
const getTitle = computed(() => {
  return formData.value?.id
    ? $t('ui.actionTitle.edit', ['物料'])
    : $t('ui.actionTitle.create', ['物料']);
});
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: {
      class: 'w-full',
    },
    formItemClass: 'col-span-1',
    labelWidth: 100,
  },
  layout: 'horizontal',
  schema: useFormSchema(),
  showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    const data = (await formApi.getValues()) as MdmItemApi.Item;
    try {
      await (formData.value?.id ? updateItem(data) : createItem(data));
      await modalApi.close();
      emit('success');
      message.success($t('ui.actionMessage.operationSuccess'));
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      formData.value = undefined;
      return;
    }
    const data = modalApi.getData<MdmItemApi.Item>();
    if (!data || !data.id) {
      return;
    }
    modalApi.lock();
    try {
      formData.value = await getItem(data.id);
      await formApi.setValues(formData.value);
    } finally {
      modalApi.unlock();
    }
  },
});
</script>
<template>
  <Modal class="w-[800px]" :title="getTitle">
    <Form class="mx-4" />
  </Modal>
</template>