From 3b207cb93a1bb14d9b84ee216532a0388c56f147 Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期一, 29 六月 2026 14:45:05 +0800
Subject: [PATCH] 银川 1.添加产品维护页面
---
mdm_frontend_change.md | 523 +++++++++++++++++++++++
src/views/basicData/mdm/data.ts | 351 +++++++++++++++
src/views/basicData/mdm/index.vue | 214 +++++++++
src/views/basicData/mdm/modules/form.vue | 80 +++
src/packages/constants/src/dict-enum.ts | 7
src/api/mdm/item/index.ts | 88 ++++
6 files changed, 1,263 insertions(+), 0 deletions(-)
diff --git a/mdm_frontend_change.md b/mdm_frontend_change.md
new file mode 100644
index 0000000..3e9c175
--- /dev/null
+++ b/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
\ No newline at end of file
diff --git a/src/api/mdm/item/index.ts b/src/api/mdm/item/index.ts
new file mode 100644
index 0000000..6ceb79a
--- /dev/null
+++ b/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 });
+}
diff --git a/src/packages/constants/src/dict-enum.ts b/src/packages/constants/src/dict-enum.ts
index 73e0095..af6fc57 100644
--- a/src/packages/constants/src/dict-enum.ts
+++ b/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,
diff --git a/src/views/basicData/mdm/data.ts b/src/views/basicData/mdm/data.ts
new file mode 100644
index 0000000..2ae684a
--- /dev/null
+++ b/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' },
+ },
+ ];
+}
diff --git a/src/views/basicData/mdm/index.vue b/src/views/basicData/mdm/index.vue
new file mode 100644
index 0000000..b5a84fa
--- /dev/null
+++ b/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: '姝e湪鏇存柊鐘舵��...',
+ 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>
\ No newline at end of file
diff --git a/src/views/basicData/mdm/modules/form.vue b/src/views/basicData/mdm/modules/form.vue
new file mode 100644
index 0000000..4d5be9a
--- /dev/null
+++ b/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>
\ No newline at end of file
--
Gitblit v1.9.3