From 3b430f42c1412422aa32412463533385bf4dd44e Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期三, 01 七月 2026 09:32:08 +0800
Subject: [PATCH] 银川 1.联调产品维护页面 2.添加采购申请页面

---
 src/views/erp/purchase/in/data.ts                             |    6 
 src/api/mdm/unit/index.ts                                     |   49 +
 src/views/basicData/mdm/unit/modules/form.vue                 |   83 +
 src/views/basicData/mdm/unit/data.ts                          |  198 ++++
 src/api/erp/purchase/in/index.ts                              |   13 
 src/views/erp/purchase/order/data.ts                          |   24 
 src/api/erp/purchase/supplier/index.ts                        |    9 
 src/views/erp/purchase/in/modules/inspection-form.vue         |   98 ++
 src/views/erp/purchase/request/index.vue                      |  256 +++++
 src/views/erp/purchase/order/modules/process-select-modal.vue |   53 +
 src/views/erp/purchase/order/modules/item-form.vue            |   15 
 src/views/erp/purchase/request/data.ts                        |  293 ++++++
 src/views/erp/purchase/request/modules/form.vue               |  135 +++
 src/views/basicData/mdm/index.vue                             |   14 
 src/views/erp/purchase/statistics/price.vue                   |  133 ++
 src/views/erp/purchase/supplier/data.ts                       |  127 +-
 src/api/erp/purchase/request/index.ts                         |   84 +
 src/views/basicData/mdm/unit/index.vue                        |  134 ++
 src/api/erp/purchase/order/index.ts                           |   13 
 src/api/erp/purchase/statistics/index.ts                      |   84 +
 src/views/erp/purchase/statistics/supplier.vue                |  122 ++
 src/views/erp/purchase/request/modules/item-form.vue          |  267 +++++
 src/views/erp/purchase/statistics/uncompleted.vue             |  145 +++
 src/views/erp/purchase/statistics/index.vue                   |   85 +
 src/views/basicData/mdm/data.ts                               |  120 +-
 src/views/erp/purchase/order/modules/form.vue                 |   90 +
 src/views/erp/purchase/in/index.vue                           |   30 
 27 files changed, 2,484 insertions(+), 196 deletions(-)

diff --git a/src/api/erp/purchase/in/index.ts b/src/api/erp/purchase/in/index.ts
index 933e6ac..504c383 100644
--- a/src/api/erp/purchase/in/index.ts
+++ b/src/api/erp/purchase/in/index.ts
@@ -21,6 +21,10 @@
     otherPrice?: number; // 鍏朵粬璐圭敤
     totalProductPrice?: number; // 鍚堣鍟嗗搧閲戦
     taxPrice?: number; // 鍚堣绋庨
+    inspectionStatus?: number; // 楠屾敹鐘舵�侊紙0:寰呴獙鏀� 1:宸查獙鏀� 2:楠屾敹涓嶅悎鏍硷級
+    inspectionTime?: string; // 楠屾敹鏃堕棿
+    inspectorId?: number; // 楠屾敹浜哄憳缂栧彿
+    inspectionResult?: string; // 楠屾敹缁撴灉
     items?: PurchaseInItem[]; // 閲囪喘鍏ュ簱鏄庣粏
   }
 
@@ -99,3 +103,12 @@
     params,
   });
 }
+
+/** 楠屾敹閲囪喘鍏ュ簱 */
+export function inspectPurchaseIn(data: {
+  id: number;
+  inspectionStatus: number;
+  inspectionResult?: string;
+}) {
+  return requestClient.put('/erp/purchase-in/inspect', data);
+}
diff --git a/src/api/erp/purchase/order/index.ts b/src/api/erp/purchase/order/index.ts
index c85c7bd..6f34484 100644
--- a/src/api/erp/purchase/order/index.ts
+++ b/src/api/erp/purchase/order/index.ts
@@ -96,3 +96,16 @@
 export function exportPurchaseOrder(params: any) {
   return requestClient.download('/erp/purchase-order/export-excel', { params });
 }
+
+/** 鑾峰彇閲囪喘瀹℃牳娴佺▼鍒楄〃 */
+export function getPurchaseApproveProcessList() {
+  return requestClient.get<any[]>('/erp/purchase-order/approve-process-list');
+}
+
+/** 鑾峰彇閲囪喘瀹℃牳娴佺▼鍒楄〃鍝嶅簲 */
+export interface ProcessDefinition {
+  id: string; // 娴佺▼瀹氫箟ID
+  key: string; // 娴佺▼Key
+  name: string; // 娴佺▼鍚嶇О
+  version: number; // 鐗堟湰鍙�
+}
diff --git a/src/api/erp/purchase/request/index.ts b/src/api/erp/purchase/request/index.ts
new file mode 100644
index 0000000..c35f89c
--- /dev/null
+++ b/src/api/erp/purchase/request/index.ts
@@ -0,0 +1,84 @@
+import type { PageParam, PageResult } from '../../../../packages/effects/request/src';
+
+import { requestClient } from '#/api/request';
+
+export namespace ErpPurchaseRequestApi {
+  /** 閲囪喘鐢宠淇℃伅 */
+  export interface PurchaseRequest {
+    id?: number; // 缂栧彿
+    no?: string; // 鐢宠缂栧彿
+    supplierId?: number; // 渚涘簲鍟嗙紪鍙�
+    supplierName?: string; // 渚涘簲鍟嗗悕绉�
+    requestTime: string; // 鐢宠鏃堕棿
+    requesterId?: number; // 鐢宠浜虹紪鍙�
+    requesterName?: string; // 鐢宠浜哄悕绉�
+    reason?: string; // 鐢宠鐞嗙敱
+    estimatedAmount?: number; // 棰勪及閲戦
+    status?: number; // 鐘舵��
+    remark?: string; // 澶囨敞
+    items?: PurchaseRequestItem[]; // 鐢宠鏄庣粏
+    createTime?: string; // 鍒涘缓鏃堕棿
+  }
+
+  /** 閲囪喘鐢宠鏄庣粏 */
+  export interface PurchaseRequestItem {
+    id?: number; // 缂栧彿
+    productId: number; // 浜у搧缂栧彿
+    productName?: string; // 浜у搧鍚嶇О
+    productUnitName?: string; // 浜у搧鍗曚綅
+    productBarCode?: string; // 浜у搧鏉$爜
+    requestCount: number; // 鐢宠鏁伴噺
+    estimatedPrice?: number; // 棰勪及鍗曚环
+    requiredDate?: string; // 闇�姹傛棩鏈�
+    remark?: string; // 澶囨敞
+  }
+}
+
+/** 鍒涘缓閲囪喘鐢宠 */
+export function createPurchaseRequest(data: ErpPurchaseRequestApi.PurchaseRequest) {
+  return requestClient.post('/erp/purchase-request/create', data);
+}
+
+/** 鏇存柊閲囪喘鐢宠 */
+export function updatePurchaseRequest(data: ErpPurchaseRequestApi.PurchaseRequest) {
+  return requestClient.put('/erp/purchase-request/update', data);
+}
+
+/** 瀹℃壒閲囪喘鐢宠 */
+export function approvePurchaseRequest(params: {
+  id: number;
+  status: number;
+  remark?: string;
+}) {
+  return requestClient.put('/erp/purchase-request/approve', null, { params });
+}
+
+/** 杞负閲囪喘璁㈠崟 */
+export function convertToPurchaseOrder(id: number) {
+  return requestClient.put('/erp/purchase-request/convert', null, { params: { id } });
+}
+
+/** 鍒犻櫎閲囪喘鐢宠 */
+export function deletePurchaseRequest(ids: number[]) {
+  return requestClient.delete('/erp/purchase-request/delete', { params: { ids: ids.join(',') } });
+}
+
+/** 鑾峰彇閲囪喘鐢宠璇︽儏 */
+export function getPurchaseRequest(id: number) {
+  return requestClient.get<ErpPurchaseRequestApi.PurchaseRequest>(
+    `/erp/purchase-request/get?id=${id}`,
+  );
+}
+
+/** 鑾峰彇閲囪喘鐢宠鍒嗛〉 */
+export function getPurchaseRequestPage(params: PageParam) {
+  return requestClient.get<PageResult<ErpPurchaseRequestApi.PurchaseRequest>>(
+    '/erp/purchase-request/page',
+    { params },
+  );
+}
+
+/** 瀵煎嚭閲囪喘鐢宠 Excel */
+export function exportPurchaseRequest(params: any) {
+  return requestClient.download('/erp/purchase-request/export-excel', { params });
+}
diff --git a/src/api/erp/purchase/statistics/index.ts b/src/api/erp/purchase/statistics/index.ts
new file mode 100644
index 0000000..38ce979
--- /dev/null
+++ b/src/api/erp/purchase/statistics/index.ts
@@ -0,0 +1,84 @@
+import type { PageParam, PageResult } from '../../../../packages/effects/request/src';
+
+import { requestClient } from '#/api/request';
+
+export namespace ErpPurchaseStatisticsApi {
+  /** 閲囪喘姹囨�荤粺璁� */
+  export interface Summary {
+    totalOrderCount?: number; // 鎬昏鍗曟暟
+    totalOrderAmount?: number; // 鎬昏鍗曢噾棰�
+    totalInCount?: number; // 鎬诲叆搴撴暟
+    totalInAmount?: number; // 鎬诲叆搴撻噾棰�
+    totalReturnCount?: number; // 鎬婚��璐ф暟
+    totalReturnAmount?: number; // 鎬婚��璐ч噾棰�
+  }
+
+  /** 渚涘簲鍟嗕緵璐х粺璁� */
+  export interface SupplierStatistics {
+    supplierId?: number; // 渚涘簲鍟嗙紪鍙�
+    supplierName?: string; // 渚涘簲鍟嗗悕绉�
+    orderCount?: number; // 璁㈠崟鏁�
+    orderAmount?: number; // 璁㈠崟閲戦
+    inCount?: number; // 鍏ュ簱鏁�
+    inAmount?: number; // 鍏ュ簱閲戦
+    returnCount?: number; // 閫�璐ф暟
+    returnAmount?: number; // 閫�璐ч噾棰�
+  }
+
+  /** 閲囪喘浠锋牸鍒嗘瀽 */
+  export interface PriceAnalysis {
+    productId?: number; // 浜у搧缂栧彿
+    productName?: string; // 浜у搧鍚嶇О
+    productBarCode?: string; // 浜у搧鏉$爜
+    avgPrice?: number; // 骞冲潎浠锋牸
+    minPrice?: number; // 鏈�浣庝环鏍�
+    maxPrice?: number; // 鏈�楂樹环鏍�
+    latestPrice?: number; // 鏈�杩戜环鏍�
+    priceChangeRate?: number; // 浠锋牸鍙樺寲鐜�
+  }
+
+  /** 鏈畬鎴愯鍗曡窡韪� */
+  export interface UncompletedOrder {
+    orderId?: number; // 璁㈠崟缂栧彿
+    orderNo?: string; // 璁㈠崟鍙�
+    supplierName?: string; // 渚涘簲鍟嗗悕绉�
+    orderTime?: string; // 璁㈠崟鏃堕棿
+    productNames?: string; // 浜у搧鍚嶇О
+    orderAmount?: number; // 璁㈠崟閲戦
+    inAmount?: number; // 鍏ュ簱閲戦
+    uncompletedAmount?: number; // 鏈畬鎴愰噾棰�
+    daysOverdue?: number; // 瓒呮湡澶╂暟
+  }
+}
+
+/** 閲囪喘姹囨�荤粺璁� */
+export function getPurchaseSummary(params?: any) {
+  return requestClient.get<ErpPurchaseStatisticsApi.Summary>(
+    '/erp/purchase-statistics/summary',
+    { params },
+  );
+}
+
+/** 渚涘簲鍟嗕緵璐х粺璁� */
+export function getSupplierStatistics(params: PageParam) {
+  return requestClient.get<PageResult<ErpPurchaseStatisticsApi.SupplierStatistics>>(
+    '/erp/purchase-statistics/supplier',
+    { params },
+  );
+}
+
+/** 閲囪喘浠锋牸鍒嗘瀽 */
+export function getPriceAnalysis(params: PageParam) {
+  return requestClient.get<PageResult<ErpPurchaseStatisticsApi.PriceAnalysis>>(
+    '/erp/purchase-statistics/price',
+    { params },
+  );
+}
+
+/** 鏈畬鎴愯鍗曡窡韪� */
+export function getUncompletedOrders(params: PageParam) {
+  return requestClient.get<PageResult<ErpPurchaseStatisticsApi.UncompletedOrder>>(
+    '/erp/purchase-statistics/uncompleted',
+    { params },
+  );
+}
\ No newline at end of file
diff --git a/src/api/erp/purchase/supplier/index.ts b/src/api/erp/purchase/supplier/index.ts
index 6adfde7..8827bbb 100644
--- a/src/api/erp/purchase/supplier/index.ts
+++ b/src/api/erp/purchase/supplier/index.ts
@@ -7,19 +7,24 @@
   export interface Supplier {
     id?: number; // 渚涘簲鍟嗙紪鍙�
     name: string; // 渚涘簲鍟嗗悕绉�
+    creditCode?: string; // 缁熶竴绀句細淇$敤浠g爜
     contact: string; // 鑱旂郴浜�
     mobile: string; // 鎵嬫満鍙风爜
     telephone: string; // 鑱旂郴鐢佃瘽
+    phone?: string; // 鑱旂郴鐢佃瘽锛堝鐢級
     email: string; // 鐢靛瓙閭
     fax: string; // 浼犵湡
+    address?: string; // 鍦板潃
     remark: string; // 澶囨敞
     status: number; // 寮�鍚姸鎬�
     sort: number; // 鎺掑簭
     taxNo: string; // 绾崇◣浜鸿瘑鍒彿
     taxPercent: number; // 绋庣巼
-    bankName: string; // 寮�鎴疯
-    bankAccount: string; // 寮�鎴疯处鍙�
+    bankName: string; // 寮�鎴烽摱琛�
+    bankAccount: string; // 閾惰璐﹀彿
     bankAddress: string; // 寮�鎴峰湴鍧�
+    qualification?: string; // 璧勮川淇℃伅
+    cooperationTime?: string; // 鍚堜綔寮�濮嬫椂闂�
   }
 }
 
diff --git a/src/api/mdm/unit/index.ts b/src/api/mdm/unit/index.ts
new file mode 100644
index 0000000..d05a8f2
--- /dev/null
+++ b/src/api/mdm/unit/index.ts
@@ -0,0 +1,49 @@
+import type { PageParam, PageResult } from '#/packages/effects/request/src';
+
+import { requestClient } from '#/api/request';
+
+export namespace MdmUnitApi {
+  /** 璁¢噺鍗曚綅淇℃伅 */
+  export interface Unit {
+    id?: number; // 鍗曚綅缂栧彿
+    code: string; // 鍗曚綅缂栫爜
+    name: string; // 鍗曚綅鍚嶇О
+    primaryFlag?: boolean; // 鏄惁涓诲崟浣�
+    primaryId?: number; // 涓诲崟浣嶇紪鍙�
+    changeRate?: number; // 涓庝富鍗曚綅鎹㈢畻姣斾緥
+    status: number; // 鐘舵�侊紙0:鍚敤, 1:绂佺敤锛�
+    remark?: string; // 澶囨敞
+  }
+}
+
+/** 鏌ヨ璁¢噺鍗曚綅鍒嗛〉 */
+export function getUnitPage(params: PageParam) {
+  return requestClient.get<PageResult<MdmUnitApi.Unit>>('/mdm/unit/page', {
+    params,
+  });
+}
+
+/** 鏌ヨ璁¢噺鍗曚綅鍒楄〃锛堟牴鎹甀D锛� */
+export function getUnitListByIds(ids: string) {
+  return requestClient.get<MdmUnitApi.Unit[]>(`/mdm/unit/list-by-ids?ids=${ids}`);
+}
+
+/** 鏌ヨ璁¢噺鍗曚綅璇︽儏 */
+export function getUnit(id: number) {
+  return requestClient.get<MdmUnitApi.Unit>(`/mdm/unit/get?id=${id}`);
+}
+
+/** 鏂板璁¢噺鍗曚綅 */
+export function createUnit(data: MdmUnitApi.Unit) {
+  return requestClient.post('/mdm/unit/create', data);
+}
+
+/** 淇敼璁¢噺鍗曚綅 */
+export function updateUnit(data: MdmUnitApi.Unit) {
+  return requestClient.put('/mdm/unit/update', data);
+}
+
+/** 鍒犻櫎璁¢噺鍗曚綅 */
+export function deleteUnit(id: number) {
+  return requestClient.delete(`/mdm/unit/delete?id=${id}`);
+}
diff --git a/src/views/basicData/mdm/data.ts b/src/views/basicData/mdm/data.ts
index a809585..1808f14 100644
--- a/src/views/basicData/mdm/data.ts
+++ b/src/views/basicData/mdm/data.ts
@@ -5,6 +5,7 @@
 import { getDictOptions } from '#/packages/effects/hooks/src';
 
 import { z } from '#/adapter/form';
+import { getUnitPage } from '#/api/mdm/unit';
 
 /** 鐗╂枡绫诲瀷閫夐」 */
 export const ITEM_TYPE_OPTIONS = [
@@ -13,6 +14,22 @@
   { label: '鎴愬搧', value: 3 },
   { label: '杈呮枡', value: 4 },
 ];
+
+/** 鐗╂枡绫诲瀷鏄犲皠 */
+export const ITEM_TYPE_MAP: Record<number, string> = {
+  1: '鍘熸枡',
+  2: '鍗婃垚鍝�',
+  3: '鎴愬搧',
+  4: '杈呮枡',
+};
+
+/** 鐗╂枡绫诲瀷棰滆壊鏄犲皠 */
+export const ITEM_TYPE_COLOR_MAP: Record<number, string> = {
+  1: 'green',
+  2: 'orange',
+  3: 'blue',
+  4: 'purple',
+};
 
 /** 鏂板/淇敼鐨勮〃鍗� */
 export function useFormSchema(): VbenFormSchema[] {
@@ -44,63 +61,39 @@
       rules: 'required',
     },
     {
+      component: 'RadioGroup',
+      fieldName: 'itemType',
+      label: '鐗╂枡绫诲瀷',
+      componentProps: {
+        options: ITEM_TYPE_OPTIONS,
+        buttonStyle: 'solid',
+        optionType: 'button',
+      },
+      rules: 'required',
+    },
+    {
+      component: 'ApiSelect',
+      fieldName: 'unitMeasureId',
+      label: '璁¢噺鍗曚綅',
+      rules: 'required',
+      componentProps: {
+        placeholder: '璇烽�夋嫨璁¢噺鍗曚綅',
+        allowClear: true,
+        api: async () => {
+          const res = await getUnitPage({ pageNo: 1, pageSize: 100, status: 0 });
+          return res.list || [];
+        },
+        labelField: 'name',
+        valueField: 'id',
+      },
+    },
+    {
       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',
@@ -177,7 +170,7 @@
         checkedChildren: '鏄�',
         unCheckedChildren: '鍚�',
       },
-      rules: z.boolean().default(false),
+      rules: z.boolean().default(true),
     },
     {
       component: 'InputNumber',
@@ -235,17 +228,6 @@
       },
     },
     {
-      fieldName: 'categoryId',
-      label: '鐗╂枡鍒嗙被',
-      component: 'TreeSelect',
-      componentProps: {
-        placeholder: '璇烽�夋嫨鐗╂枡鍒嗙被',
-        allowClear: true,
-        treeData: [],
-        fieldNames: { label: 'name', value: 'id', children: 'children' },
-      },
-    },
-    {
       fieldName: 'itemType',
       label: '鐗╂枡绫诲瀷',
       component: 'Select',
@@ -288,11 +270,6 @@
       minWidth: 150,
     },
     {
-      field: 'categoryName',
-      title: '鍒嗙被',
-      minWidth: 100,
-    },
-    {
       field: 'unitMeasureName',
       title: '鍗曚綅',
       minWidth: 80,
@@ -301,10 +278,7 @@
       field: 'itemType',
       title: '鐗╂枡绫诲瀷',
       minWidth: 100,
-      cellRender: {
-        name: 'CellDict',
-        props: { type: DICT_TYPE.MDM_ITEM_TYPE },
-      },
+      slots: { default: 'itemType' },
     },
     {
       field: 'purchasePrice',
@@ -350,4 +324,4 @@
       slots: { default: 'actions' },
     },
   ];
-}
+}
\ No newline at end of file
diff --git a/src/views/basicData/mdm/index.vue b/src/views/basicData/mdm/index.vue
index 2331b95..c10a75f 100644
--- a/src/views/basicData/mdm/index.vue
+++ b/src/views/basicData/mdm/index.vue
@@ -19,7 +19,7 @@
 } from '#/api/mdm/item';
 import { $t } from '#/locales';
 
-import { useGridColumns, useGridFormSchema } from './data';
+import { useGridColumns, useGridFormSchema, ITEM_TYPE_MAP, ITEM_TYPE_COLOR_MAP } from './data';
 import Form from './modules/form.vue';
 
 defineOptions({ name: 'MdmItem' });
@@ -175,6 +175,12 @@
           ]"
         />
       </template>
+      <template #itemType="{ row }">
+        <Tag v-if="row.itemType === 1" color="green">鍘熸枡</Tag>
+        <Tag v-else-if="row.itemType === 2" color="orange">鍗婃垚鍝�</Tag>
+        <Tag v-else-if="row.itemType === 3" color="blue">鎴愬搧</Tag>
+        <Tag v-else-if="row.itemType === 4" color="purple">杈呮枡</Tag>
+      </template>
       <template #isBatchManaged="{ row }">
         <Tag v-if="row.isBatchManaged" color="success">鏄�</Tag>
         <Tag v-else>鍚�</Tag>
@@ -188,12 +194,6 @@
               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'),
diff --git a/src/views/basicData/mdm/unit/data.ts b/src/views/basicData/mdm/unit/data.ts
new file mode 100644
index 0000000..e6081e4
--- /dev/null
+++ b/src/views/basicData/mdm/unit/data.ts
@@ -0,0 +1,198 @@
+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';
+import { getUnitPage } from '#/api/mdm/unit';
+
+/** 鏂板/淇敼鐨勮〃鍗� */
+export function useFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      component: 'Input',
+      fieldName: 'id',
+      dependencies: {
+        triggerFields: [''],
+        show: () => false,
+      },
+    },
+    {
+      component: 'Input',
+      fieldName: 'code',
+      label: '鍗曚綅缂栫爜',
+      rules: 'required',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ崟浣嶇紪鐮�',
+      },
+    },
+    {
+      component: 'Input',
+      fieldName: 'name',
+      label: '鍗曚綅鍚嶇О',
+      rules: 'required',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ崟浣嶅悕绉�',
+      },
+    },
+    {
+      component: 'Switch',
+      fieldName: 'primaryFlag',
+      label: '鏄惁涓诲崟浣�',
+      componentProps: {
+        class: 'w-auto',
+        checkedChildren: '鏄�',
+        unCheckedChildren: '鍚�',
+      },
+      rules: z.boolean().default(false),
+    },
+    {
+      component: 'ApiSelect',
+      fieldName: 'primaryId',
+      label: '涓诲崟浣�',
+      dependencies: {
+        triggerFields: ['primaryFlag'],
+        show: (values) => {
+          return values.primaryFlag === false;
+        },
+        required: (values) => {
+          return values.primaryFlag === false;
+        },
+      },
+      componentProps: {
+        placeholder: '璇烽�夋嫨涓诲崟浣�',
+        allowClear: true,
+        api: async () => {
+          const res = await getUnitPage({ pageNo: 1, pageSize: 100, primaryFlag: true, status: 0 });
+          return res.list || [];
+        },
+        labelField: 'name',
+        valueField: 'id',
+      },
+    },
+    {
+      component: 'InputNumber',
+      fieldName: 'changeRate',
+      label: '鎹㈢畻姣斾緥',
+      dependencies: {
+        triggerFields: ['primaryFlag'],
+        show: (values) => {
+          return values.primaryFlag === false;
+        },
+        required: (values) => {
+          return values.primaryFlag === false;
+        },
+      },
+      componentProps: {
+        class: '!w-full',
+        min: 0,
+        precision: 4,
+        placeholder: '璇疯緭鍏ユ崲绠楁瘮渚�',
+      },
+    },
+    {
+      fieldName: 'status',
+      label: '鐘舵��',
+      component: 'RadioGroup',
+      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: '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: 150,
+    },
+    {
+      field: 'primaryFlag',
+      title: '涓诲崟浣�',
+      minWidth: 80,
+      slots: { default: 'primaryFlag' },
+    },
+    {
+      field: 'changeRate',
+      title: '鎹㈢畻姣斾緥',
+      minWidth: 100,
+    },
+    {
+      field: 'status',
+      title: '鐘舵��',
+      minWidth: 80,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.COMMON_STATUS },
+      },
+    },
+    {
+      field: 'createTime',
+      title: '鍒涘缓鏃堕棿',
+      minWidth: 180,
+      formatter: 'formatDateTime',
+    },
+    {
+      title: '鎿嶄綔',
+      width: 130,
+      fixed: 'right',
+      slots: { default: 'actions' },
+    },
+  ];
+}
\ No newline at end of file
diff --git a/src/views/basicData/mdm/unit/index.vue b/src/views/basicData/mdm/unit/index.vue
new file mode 100644
index 0000000..e2d3a44
--- /dev/null
+++ b/src/views/basicData/mdm/unit/index.vue
@@ -0,0 +1,134 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MdmUnitApi } from '#/api/mdm/unit';
+
+import { Page, useVbenModal } from '#/packages/effects/common-ui/src';
+
+import { message, Tag } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+  deleteUnit,
+  getUnitPage,
+} from '#/api/mdm/unit';
+import { $t } from '#/locales';
+
+import { useGridColumns, useGridFormSchema } from './data';
+import Form from './modules/form.vue';
+
+defineOptions({ name: 'MdmUnit' });
+
+const [FormModal, formModalApi] = useVbenModal({
+  connectedComponent: Form,
+  destroyOnClose: true,
+});
+
+/** 鍒锋柊琛ㄦ牸 */
+function handleRefresh() {
+  gridApi.query();
+}
+
+/** 鍒涘缓璁¢噺鍗曚綅 */
+function handleCreate() {
+  formModalApi.setData(null).open();
+}
+
+/** 缂栬緫璁¢噺鍗曚綅 */
+function handleEdit(row: MdmUnitApi.Unit) {
+  formModalApi.setData(row).open();
+}
+
+/** 鍒犻櫎璁¢噺鍗曚綅 */
+async function handleDelete(row: MdmUnitApi.Unit) {
+  const hideLoading = message.loading({
+    content: $t('ui.actionMessage.deleting', [row.name]),
+    duration: 0,
+  });
+  try {
+    await deleteUnit(row.id as number);
+    message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
+    handleRefresh();
+  } finally {
+    hideLoading();
+  }
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  formOptions: {
+    schema: useGridFormSchema(),
+  },
+  gridOptions: {
+    columns: useGridColumns(),
+    height: 'auto',
+    keepSource: true,
+    proxyConfig: {
+      ajax: {
+        query: async ({ page }, formValues) => {
+          return await getUnitPage({
+            pageNo: page.currentPage,
+            pageSize: page.pageSize,
+            ...formValues,
+          });
+        },
+      },
+    },
+    rowConfig: {
+      keyField: 'id',
+      isHover: true,
+    },
+    toolbarConfig: {
+      refresh: true,
+      search: true,
+    },
+  } as VxeTableGridOptions<MdmUnitApi.Unit>,
+});
+</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:unit:create'],
+              onClick: handleCreate,
+            },
+          ]"
+        />
+      </template>
+      <template #primaryFlag="{ row }">
+        <Tag v-if="row.primaryFlag" 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:unit:update'],
+              onClick: handleEdit.bind(null, row),
+            },
+            {
+              label: $t('common.delete'),
+              type: 'link',
+              danger: true,
+              icon: ACTION_ICON.DELETE,
+              auth: ['mdm:unit:delete'],
+              popConfirm: {
+                title: $t('ui.actionMessage.deleteConfirm', [row.name]),
+                confirm: handleDelete.bind(null, row),
+              },
+            },
+          ]"
+        />
+      </template>
+    </Grid>
+  </Page>
+</template>
diff --git a/src/views/basicData/mdm/unit/modules/form.vue b/src/views/basicData/mdm/unit/modules/form.vue
new file mode 100644
index 0000000..f01ea05
--- /dev/null
+++ b/src/views/basicData/mdm/unit/modules/form.vue
@@ -0,0 +1,83 @@
+<script lang="ts" setup>
+import type { MdmUnitApi } from '#/api/mdm/unit';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+  createUnit,
+  getUnit,
+  updateUnit,
+} from '#/api/mdm/unit';
+import { $t } from '#/locales';
+
+import { useFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+const formData = ref<MdmUnitApi.Unit>();
+const getTitle = computed(() => {
+  return formData.value?.id
+    ? $t('ui.actionTitle.edit', ['璁¢噺鍗曚綅'])
+    : $t('ui.actionTitle.create', ['璁¢噺鍗曚綅']);
+});
+
+const [Form, formApi] = useVbenForm({
+  commonConfig: {
+    componentProps: {
+      class: 'w-full',
+    },
+    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 MdmUnitApi.Unit;
+    try {
+      await (formData.value?.id
+        ? updateUnit(data)
+        : createUnit(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<MdmUnitApi.Unit>();
+    if (!data || !data.id) {
+      return;
+    }
+    modalApi.lock();
+    try {
+      formData.value = await getUnit(data.id);
+      await formApi.setValues(formData.value);
+    } finally {
+      modalApi.unlock();
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal class="w-[500px]" :title="getTitle">
+    <Form class="mx-4" />
+  </Modal>
+</template>
\ No newline at end of file
diff --git a/src/views/erp/purchase/in/data.ts b/src/views/erp/purchase/in/data.ts
index 0b957db..613bcaf 100644
--- a/src/views/erp/purchase/in/data.ts
+++ b/src/views/erp/purchase/in/data.ts
@@ -503,6 +503,12 @@
       minWidth: 120,
     },
     {
+      field: 'inspectionStatus',
+      title: '楠屾敹鐘舵��',
+      minWidth: 120,
+      slots: { default: 'inspectionStatus' },
+    },
+    {
       field: 'status',
       title: '瀹℃壒鐘舵��',
       minWidth: 120,
diff --git a/src/views/erp/purchase/in/index.vue b/src/views/erp/purchase/in/index.vue
index a9a2e4b..9c82b12 100644
--- a/src/views/erp/purchase/in/index.vue
+++ b/src/views/erp/purchase/in/index.vue
@@ -7,25 +7,32 @@
 import { Page, useVbenModal } from '../../../../packages/effects/common-ui/src';
 import { downloadFileFromBlobPart, isEmpty } from '../../../../packages/utils/src';
 
-import { message } from 'ant-design-vue';
+import { message, Tag } from 'ant-design-vue';
 
 import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
 import {
   deletePurchaseIn,
   exportPurchaseIn,
   getPurchaseInPage,
+  inspectPurchaseIn,
   updatePurchaseInStatus,
 } from '#/api/erp/purchase/in';
 import { $t } from '#/locales';
 
 import { useGridColumns, useGridFormSchema } from './data';
 import Form from './modules/form.vue';
+import InspectionForm from './modules/inspection-form.vue';
 
 /** ERP 閲囪喘鍏ュ簱鍒楄〃 */
 defineOptions({ name: 'ErpPurchaseIn' });
 
 const [FormModal, formModalApi] = useVbenModal({
   connectedComponent: Form,
+  destroyOnClose: true,
+});
+
+const [InspectionModal, inspectionModalApi] = useVbenModal({
+  connectedComponent: InspectionForm,
   destroyOnClose: true,
 });
 
@@ -83,6 +90,11 @@
   }
 }
 
+/** 楠屾敹鎿嶄綔 */
+function handleInspection(row: ErpPurchaseInApi.PurchaseIn) {
+  inspectionModalApi.setData({ id: row.id }).open();
+}
+
 const checkedIds = ref<number[]>([]);
 function handleRowCheckboxChange({
   records,
@@ -133,7 +145,9 @@
 </script>
 
 <template>
-  <Page auto-content-height><FormModal @success="handleRefresh" />
+  <Page auto-content-height>
+    <FormModal @success="handleRefresh" />
+    <InspectionModal @success="handleRefresh" />
     <Grid table-title="閲囪喘鍏ュ簱鍒楄〃">
       <template #toolbar-tools>
         <TableAction
@@ -167,6 +181,11 @@
           ]"
         />
       </template>
+      <template #inspectionStatus="{ row }">
+        <Tag v-if="row.inspectionStatus === 0" color="warning">寰呴獙鏀�</Tag>
+        <Tag v-else-if="row.inspectionStatus === 1" color="success">宸查獙鏀�</Tag>
+        <Tag v-else-if="row.inspectionStatus === 2" color="error">楠屾敹涓嶅悎鏍�</Tag>
+      </template>
       <template #actions="{ row }">
         <TableAction
           :actions="[
@@ -186,6 +205,13 @@
               onClick: handleEdit.bind(null, row),
             },
             {
+              label: '楠屾敹',
+              type: 'link',
+              auth: ['erp:purchase-in:inspect'],
+              ifShow: () => row.inspectionStatus === 0 && row.status === 20,
+              onClick: handleInspection.bind(null, row),
+            },
+            {
               label: row.status === 10 ? '瀹℃壒' : '鍙嶅鎵�',
               type: 'link',
               icon: ACTION_ICON.AUDIT,
diff --git a/src/views/erp/purchase/in/modules/inspection-form.vue b/src/views/erp/purchase/in/modules/inspection-form.vue
new file mode 100644
index 0000000..38293a3
--- /dev/null
+++ b/src/views/erp/purchase/in/modules/inspection-form.vue
@@ -0,0 +1,98 @@
+<script lang="ts" setup>
+import { ref } from 'vue';
+
+import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { inspectPurchaseIn } from '#/api/erp/purchase/in';
+import { $t } from '#/locales';
+
+const emit = defineEmits(['success']);
+const purchaseInId = ref<number>();
+
+const [Form, formApi] = useVbenForm({
+  commonConfig: {
+    componentProps: {
+      class: 'w-full',
+    },
+    labelWidth: 100,
+  },
+  wrapperClass: 'grid-cols-1',
+  layout: 'horizontal',
+  schema: [
+    {
+      fieldName: 'inspectionStatus',
+      label: '楠屾敹鐘舵��',
+      component: 'RadioGroup',
+      componentProps: {
+        options: [
+          { label: '楠屾敹鍚堟牸', value: 1 },
+          { label: '楠屾敹涓嶅悎鏍�', value: 2 },
+        ],
+        buttonStyle: 'solid',
+        optionType: 'button',
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'inspectionResult',
+      label: '楠屾敹缁撴灉',
+      component: 'Textarea',
+      componentProps: {
+        placeholder: '璇疯緭鍏ラ獙鏀剁粨鏋�',
+        rows: 3,
+      },
+    },
+  ],
+  showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    const { valid } = await formApi.validate();
+    if (!valid) {
+      return;
+    }
+    if (!purchaseInId.value) {
+      message.error('鍏ュ簱鍗旾D涓嶅瓨鍦�');
+      return;
+    }
+    modalApi.lock();
+    const data = await formApi.getValues();
+    try {
+      await inspectPurchaseIn({
+        id: purchaseInId.value,
+        inspectionStatus: data.inspectionStatus,
+        inspectionResult: data.inspectionResult,
+      });
+      await modalApi.close();
+      emit('success');
+      message.success('楠屾敹鎴愬姛');
+    } finally {
+      modalApi.unlock();
+    }
+  },
+  async onOpenChange(isOpen: boolean) {
+    if (!isOpen) {
+      purchaseInId.value = undefined;
+      await formApi.resetForm();
+      return;
+    }
+    const data = modalApi.getData<{ id: number }>();
+    if (!data || !data.id) {
+      return;
+    }
+    purchaseInId.value = data.id;
+    // 榛樿閫夋嫨楠屾敹鍚堟牸
+    await formApi.setValues({ inspectionStatus: 1 });
+  },
+});
+</script>
+
+<template>
+  <Modal title="閲囪喘鍏ュ簱楠屾敹" class="w-1/3">
+    <Form class="mx-4" />
+  </Modal>
+</template>
\ No newline at end of file
diff --git a/src/views/erp/purchase/order/data.ts b/src/views/erp/purchase/order/data.ts
index 0c2a84d..9a2991d 100644
--- a/src/views/erp/purchase/order/data.ts
+++ b/src/views/erp/purchase/order/data.ts
@@ -62,6 +62,18 @@
       rules: 'required',
     },
     {
+      component: 'InputNumber',
+      componentProps: {
+        class: '!w-full',
+        placeholder: '璇疯緭鍏ユ敮浠樿閲�',
+        precision: 2,
+        min: 0,
+      },
+      fieldName: 'depositPrice',
+      label: '鏀粯璁㈤噾',
+      rules: 'required',
+    },
+    {
       fieldName: 'remark',
       label: '澶囨敞',
       component: 'Textarea',
@@ -150,18 +162,6 @@
         labelField: 'name',
         valueField: 'id',
       },
-    },
-    {
-      component: 'InputNumber',
-      componentProps: {
-        class: '!w-full',
-        placeholder: '璇疯緭鍏ユ敮浠樿閲�',
-        precision: 2,
-        min: 0,
-      },
-      fieldName: 'depositPrice',
-      label: '鏀粯璁㈤噾',
-      rules: z.number().min(0).optional(),
     },
   ];
 }
diff --git a/src/views/erp/purchase/order/modules/form.vue b/src/views/erp/purchase/order/modules/form.vue
index 58a42fe..37981be 100644
--- a/src/views/erp/purchase/order/modules/form.vue
+++ b/src/views/erp/purchase/order/modules/form.vue
@@ -1,7 +1,7 @@
 <script lang="ts" setup>
 import type { FormType } from '../data';
 
-import type { ErpPurchaseOrderApi } from '#/api/erp/purchase/order';
+import type { ErpPurchaseOrderApi, ProcessDefinition } from '#/api/erp/purchase/order';
 
 import { computed, ref } from 'vue';
 
@@ -13,6 +13,7 @@
 import { getAccountSimpleList } from '#/api/erp/finance/account';
 import {
   createPurchaseOrder,
+  getPurchaseApproveProcessList,
   getPurchaseOrder,
   updatePurchaseOrder,
 } from '#/api/erp/purchase/order';
@@ -20,11 +21,13 @@
 
 import { useFormSchema } from '../data';
 import PurchaseOrderItemForm from './item-form.vue';
+import ProcessSelectModal from './process-select-modal.vue';
 
 const emit = defineEmits(['success']);
 const formData = ref<ErpPurchaseOrderApi.PurchaseOrder>();
-const formType = ref<FormType>('create'); // 琛ㄥ崟绫诲瀷锛�'create' | 'edit' | 'detail'
+const formType = ref<FormType>('create');
 const itemFormRef = ref<InstanceType<typeof PurchaseOrderItemForm>>();
+const selectedProcessId = ref<string>('');
 
 const getTitle = computed(() => {
   if (formType.value === 'create') {
@@ -48,11 +51,15 @@
   schema: useFormSchema(formType.value),
   showDefaultActions: false,
   handleValuesChange: (values, changedFields) => {
-    // 鐩殑锛氬悓姝ュ埌 item-form 缁勪欢锛岃Е鍙戞暣浣撶殑浠锋牸璁$畻
     if (formData.value && changedFields.includes('discountPercent')) {
       formData.value.discountPercent = values.discountPercent;
     }
   },
+});
+
+const [ProcessModal, processModalApi] = useVbenModal({
+  connectedComponent: ProcessSelectModal,
+  destroyOnClose: true,
 });
 
 /** 鏇存柊閲囪喘璁㈠崟椤� */
@@ -78,6 +85,33 @@
   });
 }
 
+/** 娴佺▼閫夋嫨鎴愬姛 */
+function handleProcessSelectSuccess(processId: string) {
+  selectedProcessId.value = processId;
+  submitOrder();
+}
+
+/** 鎻愪氦璁㈠崟 */
+async function submitOrder() {
+  const data = (await formApi.getValues()) as ErpPurchaseOrderApi.PurchaseOrder;
+  data.items = formData.value?.items?.map((item) => ({
+    ...item,
+    id: undefined,
+  }));
+  if (data.fileUrl && Array.isArray(data.fileUrl)) {
+    data.fileUrl = data.fileUrl.length > 0 ? data.fileUrl[0] : '';
+  }
+  if (selectedProcessId.value) {
+    data.processDefinitionId = selectedProcessId.value;
+  }
+  await (formType.value === 'create'
+    ? createPurchaseOrder(data)
+    : updatePurchaseOrder(data));
+  await modalApi.close();
+  emit('success');
+  message.success($t('ui.actionMessage.operationSuccess'));
+}
+
 /** 鍒涘缓鎴栨洿鏂伴噰璐鍗� */
 const [Modal, modalApi] = useVbenModal({
   async onConfirm() {
@@ -96,42 +130,42 @@
     }
 
     modalApi.lock();
-    // 鎻愪氦琛ㄥ崟
-    const data =
-      (await formApi.getValues()) as ErpPurchaseOrderApi.PurchaseOrder;
-    data.items = formData.value?.items?.map((item) => ({
-      ...item,
-      // 瑙e喅鏂板閲囪喘璁㈠崟鎶ラ敊
-      id: undefined,
-    }));
-    // 灏嗘枃浠舵暟缁勮浆鎹负瀛楃涓�
-    if (data.fileUrl && Array.isArray(data.fileUrl)) {
-      data.fileUrl = data.fileUrl.length > 0 ? data.fileUrl[0] : '';
-    }
-    try {
-      await (formType.value === 'create'
-        ? createPurchaseOrder(data)
-        : updatePurchaseOrder(data));
-      // 鍏抽棴骞舵彁绀�
-      await modalApi.close();
-      emit('success');
-      message.success($t('ui.actionMessage.operationSuccess'));
-    } finally {
-      modalApi.unlock();
+
+    if (formType.value === 'create') {
+      try {
+        const processes = await getPurchaseApproveProcessList();
+        if (processes.length === 1) {
+          selectedProcessId.value = processes[0].id;
+          await submitOrder();
+        } else if (processes.length > 1) {
+          processModalApi.setData(processes).open();
+        } else {
+          message.error('閲囪喘瀹℃牳鍒嗙被涓嬫病鏈夊彲鐢ㄧ殑娴佺▼妯″瀷锛岃鍏堝垱寤哄苟鍙戝竷娴佺▼');
+        }
+      } catch (error: any) {
+        message.error(error.message || '鑾峰彇娴佺▼鍒楄〃澶辫触');
+      } finally {
+        modalApi.unlock();
+      }
+    } else {
+      try {
+        await submitOrder();
+      } finally {
+        modalApi.unlock();
+      }
     }
   },
   async onOpenChange(isOpen: boolean) {
     if (!isOpen) {
       formData.value = undefined;
+      selectedProcessId.value = '';
       return;
     }
-    // 鍔犺浇鏁版嵁
     const data = modalApi.getData<{ formType: FormType; id?: number }>();
     formType.value = data.formType;
     formApi.setDisabled(formType.value === 'detail');
     formApi.updateSchema(useFormSchema(formType.value));
     if (!data || !data.id) {
-      // 鏂板鏃讹紝榛樿閫変腑璐︽埛
       const accountList = await getAccountSimpleList();
       const defaultAccount = accountList.find((item) => item.defaultStatus);
       if (defaultAccount) {
@@ -142,7 +176,6 @@
     modalApi.lock();
     try {
       formData.value = await getPurchaseOrder(data.id);
-      // 璁剧疆鍒� values
       await formApi.setValues(formData.value);
     } finally {
       modalApi.unlock();
@@ -157,6 +190,7 @@
     class="w-3/4"
     :show-confirm-button="formType !== 'detail'"
   >
+    <ProcessModal @success="handleProcessSelectSuccess" />
     <Form class="mx-3">
       <template #items>
         <PurchaseOrderItemForm
diff --git a/src/views/erp/purchase/order/modules/item-form.vue b/src/views/erp/purchase/order/modules/item-form.vue
index fefe146..777ca3f 100644
--- a/src/views/erp/purchase/order/modules/item-form.vue
+++ b/src/views/erp/purchase/order/modules/item-form.vue
@@ -1,5 +1,5 @@
 <script lang="ts" setup>
-import type { ErpProductApi } from '#/api/erp/product/product';
+import type { MdmItemApi } from '#/api/mdm/item';
 import type { ErpPurchaseOrderApi } from '#/api/erp/purchase/order';
 
 import { computed, nextTick, onMounted, ref, watch } from 'vue';
@@ -13,7 +13,7 @@
 import { Input, InputNumber, Select } from 'ant-design-vue';
 
 import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
-import { getProductSimpleList } from '#/api/erp/product/product';
+import { getItemPage } from '#/api/mdm/item';
 import { getStockCount } from '#/api/erp/stock/stock';
 
 import { useFormItemColumns } from '../data';
@@ -37,7 +37,7 @@
 ]);
 
 const tableData = ref<ErpPurchaseOrderApi.PurchaseOrderItem[]>([]); // 琛ㄦ牸鏁版嵁
-const productOptions = ref<ErpProductApi.Product[]>([]); // 浜у搧涓嬫媺閫夐」
+const productOptions = ref<MdmItemApi.Item[]>([]); // 浜у搧涓嬫媺閫夐」
 
 /** 鑾峰彇琛ㄦ牸鍚堣鏁版嵁 */
 const summaries = computed(() => {
@@ -158,9 +158,9 @@
     return;
   }
   row.productId = productId;
-  row.productUnitId = product.unitId;
+  row.productUnitId = product.unitMeasureId;
   row.productBarCode = product.barCode;
-  row.productUnitName = product.unitName;
+  row.productUnitName = product.unitMeasureName;
   row.productName = product.name;
   row.stockCount = (await getStockCount(productId)) || 0;
   row.productPrice = product.purchasePrice || 0;
@@ -213,7 +213,8 @@
 
 /** 鍒濆鍖� */
 onMounted(async () => {
-  productOptions.value = await getProductSimpleList();
+  const res = await getItemPage({ pageNo: 1, pageSize: 100, status: 0 });
+  productOptions.value = res.list || [];
   // 鐩殑锛氭柊澧炴椂锛岄粯璁ゆ坊鍔犱竴琛�
   if (tableData.value.length === 0) {
     handleAdd();
@@ -225,6 +226,7 @@
   <Grid class="w-full">
     <template #productId="{ row }">
       <Select
+        v-if="!disabled"
         v-model:value="row.productId"
         :options="productOptions"
         :field-names="{ label: 'name', value: 'id' }"
@@ -233,6 +235,7 @@
         show-search
         @change="handleProductChange($event, row)"
       />
+      <span v-else>{{ row.productName || '-' }}</span>
     </template>
     <template #count="{ row }">
       <InputNumber
diff --git a/src/views/erp/purchase/order/modules/process-select-modal.vue b/src/views/erp/purchase/order/modules/process-select-modal.vue
new file mode 100644
index 0000000..05cb637
--- /dev/null
+++ b/src/views/erp/purchase/order/modules/process-select-modal.vue
@@ -0,0 +1,53 @@
+<script lang="ts" setup>
+import type { ProcessDefinition } from '#/api/erp/purchase/order';
+
+import { ref, computed } from 'vue';
+
+import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
+
+import { Select } from 'ant-design-vue';
+
+const emit = defineEmits(['success']);
+
+const processList = ref<ProcessDefinition[]>([]);
+const selectedProcessId = ref<string>('');
+
+const processOptions = computed(() => {
+  return processList.value.map((p) => ({
+    label: `${p.name} (V${p.version})`,
+    value: p.id,
+  }));
+});
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    if (!selectedProcessId.value) {
+      return;
+    }
+    await modalApi.close();
+    emit('success', selectedProcessId.value);
+  },
+  async onOpenChange(isOpen: boolean) {
+    if (!isOpen) {
+      selectedProcessId.value = '';
+      processList.value = [];
+      return;
+    }
+    const data = modalApi.getData<ProcessDefinition[]>();
+    if (data && data.length > 0) {
+      processList.value = data;
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal title="閫夋嫨瀹℃壒娴佺▼" class="w-1/3">
+    <Select
+      v-model:value="selectedProcessId"
+      :options="processOptions"
+      style="width: 100%"
+      placeholder="璇烽�夋嫨瀹℃壒娴佺▼"
+    />
+  </Modal>
+</template>
\ No newline at end of file
diff --git a/src/views/erp/purchase/request/data.ts b/src/views/erp/purchase/request/data.ts
new file mode 100644
index 0000000..34682d6
--- /dev/null
+++ b/src/views/erp/purchase/request/data.ts
@@ -0,0 +1,293 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '../../../../packages/constants/src';
+import { getDictOptions } from '../../../../packages/effects/hooks/src';
+import { erpPriceInputFormatter } from '../../../../packages/utils/src';
+
+import { getSupplierSimpleList } from '#/api/erp/purchase/supplier';
+import { getSimpleUserList } from '#/api/system/user';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'detail' | 'edit';
+
+/** 鐘舵�佸父閲� */
+export const PURCHASE_REQUEST_STATUS = {
+  PENDING: 0, // 寰呭鎵�
+  APPROVED: 1, // 宸插鎵�
+  REJECTED: 2, // 宸叉嫆缁�
+  CONVERTED: 3, // 宸茶浆璁㈠崟
+};
+
+/** 琛ㄥ崟鐨勯厤缃」 */
+export function useFormSchema(formType: FormType): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: {
+        triggerFields: [''],
+        show: () => false,
+      },
+    },
+    {
+      fieldName: 'no',
+      label: '鐢宠缂栧彿',
+      component: 'Input',
+      componentProps: {
+        placeholder: '绯荤粺鑷姩鐢熸垚',
+        disabled: true,
+      },
+    },
+    {
+      fieldName: 'requestTime',
+      label: '鐢宠鏃堕棿',
+      component: 'DatePicker',
+      componentProps: {
+        placeholder: '閫夋嫨鐢宠鏃堕棿',
+        showTime: true,
+        format: 'YYYY-MM-DD HH:mm:ss',
+        valueFormat: 'x',
+        disabled: formType === 'detail',
+      },
+      rules: 'required',
+    },
+    {
+      label: '渚涘簲鍟�',
+      fieldName: 'supplierId',
+      component: 'ApiSelect',
+      componentProps: {
+        placeholder: '璇烽�夋嫨渚涘簲鍟�',
+        allowClear: true,
+        showSearch: true,
+        api: getSupplierSimpleList,
+        labelField: 'name',
+        valueField: 'id',
+        disabled: formType === 'detail',
+      },
+    },
+    {
+      fieldName: 'reason',
+      label: '鐢宠鐞嗙敱',
+      component: 'Textarea',
+      componentProps: {
+        placeholder: '璇疯緭鍏ョ敵璇风悊鐢�',
+        autoSize: { minRows: 1, maxRows: 1 },
+        disabled: formType === 'detail',
+      },
+      formItemClass: 'col-span-2',
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ娉�',
+        autoSize: { minRows: 1, maxRows: 1 },
+        disabled: formType === 'detail',
+      },
+      formItemClass: 'col-span-2',
+    },
+    {
+      fieldName: 'items',
+      label: '鐢宠鏄庣粏',
+      component: 'Input',
+      formItemClass: 'col-span-3',
+    },
+  ];
+}
+
+/** 琛ㄥ崟鐨勬槑缁嗚〃鏍煎垪 */
+export function useFormItemColumns(
+  disabled: boolean,
+): VxeTableGridOptions['columns'] {
+  return [
+    { type: 'seq', title: '搴忓彿', minWidth: 50, fixed: 'left' },
+    {
+      field: 'productId',
+      title: '浜у搧鍚嶇О',
+      minWidth: 200,
+      slots: { default: 'productId' },
+    },
+    {
+      field: 'productBarCode',
+      title: '鏉$爜',
+      minWidth: 120,
+    },
+    {
+      field: 'productUnitName',
+      title: '鍗曚綅',
+      minWidth: 80,
+    },
+    {
+      field: 'requestCount',
+      title: '鐢宠鏁伴噺',
+      minWidth: 120,
+      fixed: 'right',
+      slots: { default: 'requestCount' },
+    },
+    {
+      field: 'estimatedPrice',
+      title: '棰勪及鍗曚环',
+      minWidth: 120,
+      fixed: 'right',
+      slots: { default: 'estimatedPrice' },
+    },
+    {
+      field: 'totalPrice',
+      title: '棰勪及閲戦',
+      minWidth: 120,
+      fixed: 'right',
+      formatter: 'formatAmount2',
+    },
+    {
+      field: 'requiredDate',
+      title: '闇�姹傛棩鏈�',
+      minWidth: 140,
+      fixed: 'right',
+      slots: { default: 'requiredDate' },
+    },
+    {
+      field: 'remark',
+      title: '澶囨敞',
+      minWidth: 150,
+      slots: { default: 'remark' },
+    },
+    {
+      title: '鎿嶄綔',
+      width: 50,
+      fixed: 'right',
+      slots: { default: 'actions' },
+      visible: !disabled,
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'no',
+      label: '鐢宠缂栧彿',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ョ敵璇风紪鍙�',
+        allowClear: true,
+      },
+    },
+    {
+      fieldName: 'supplierId',
+      label: '渚涘簲鍟�',
+      component: 'ApiSelect',
+      componentProps: {
+        placeholder: '璇烽�夋嫨渚涘簲鍟�',
+        allowClear: true,
+        showSearch: true,
+        api: getSupplierSimpleList,
+        labelField: 'name',
+        valueField: 'id',
+      },
+    },
+    {
+      fieldName: 'requestTime',
+      label: '鐢宠鏃堕棿',
+      component: 'RangePicker',
+      componentProps: {
+        ...getRangePickerDefaultProps(),
+        allowClear: true,
+      },
+    },
+    {
+      fieldName: 'requesterId',
+      label: '鐢宠浜�',
+      component: 'ApiSelect',
+      componentProps: {
+        placeholder: '璇烽�夋嫨鐢宠浜�',
+        allowClear: true,
+        showSearch: true,
+        api: getSimpleUserList,
+        labelField: 'nickname',
+        valueField: 'id',
+      },
+    },
+    {
+      fieldName: 'status',
+      label: '鐘舵��',
+      component: 'Select',
+      componentProps: {
+        options: [
+          { label: '寰呭鎵�', value: 0 },
+          { label: '宸插鎵�', value: 1 },
+          { label: '宸叉嫆缁�', value: 2 },
+          { label: '宸茶浆璁㈠崟', value: 3 },
+        ],
+        placeholder: '璇烽�夋嫨鐘舵��',
+        allowClear: true,
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+  return [
+    {
+      type: 'checkbox',
+      width: 50,
+      fixed: 'left',
+    },
+    {
+      field: 'no',
+      title: '鐢宠缂栧彿',
+      width: 200,
+      fixed: 'left',
+    },
+    {
+      field: 'supplierName',
+      title: '渚涘簲鍟�',
+      minWidth: 120,
+    },
+    {
+      field: 'requestTime',
+      title: '鐢宠鏃堕棿',
+      width: 160,
+      formatter: 'formatDate',
+    },
+    {
+      field: 'requesterName',
+      title: '鐢宠浜�',
+      minWidth: 120,
+    },
+    {
+      field: 'estimatedAmount',
+      title: '棰勪及閲戦',
+      formatter: 'formatAmount2',
+      minWidth: 120,
+    },
+    {
+      field: 'status',
+      title: '鐘舵��',
+      minWidth: 120,
+      slots: { default: 'status' },
+    },
+    {
+      field: 'reason',
+      title: '鐢宠鐞嗙敱',
+      minWidth: 150,
+      showOverflow: 'tooltip',
+    },
+    {
+      field: 'createTime',
+      title: '鍒涘缓鏃堕棿',
+      width: 160,
+      formatter: 'formatDate',
+    },
+    {
+      title: '鎿嶄綔',
+      width: 200,
+      fixed: 'right',
+      slots: { default: 'actions' },
+    },
+  ];
+}
diff --git a/src/views/erp/purchase/request/index.vue b/src/views/erp/purchase/request/index.vue
new file mode 100644
index 0000000..a3cd39c
--- /dev/null
+++ b/src/views/erp/purchase/request/index.vue
@@ -0,0 +1,256 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { ErpPurchaseRequestApi } from '#/api/erp/purchase/request';
+
+import { ref } from 'vue';
+
+import { Page, useVbenModal } from '../../../../packages/effects/common-ui/src';
+import { downloadFileFromBlobPart, isEmpty } from '../../../../packages/utils/src';
+
+import { message, Tag } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+  approvePurchaseRequest,
+  convertToPurchaseOrder,
+  deletePurchaseRequest,
+  exportPurchaseRequest,
+  getPurchaseRequestPage,
+} from '#/api/erp/purchase/request';
+import { $t } from '#/locales';
+
+import { useGridColumns, useGridFormSchema, PURCHASE_REQUEST_STATUS } from './data';
+import Form from './modules/form.vue';
+
+/** ERP 閲囪喘鐢宠鍒楄〃 */
+defineOptions({ name: 'ErpPurchaseRequest' });
+
+const [FormModal, formModalApi] = useVbenModal({
+  connectedComponent: Form,
+  destroyOnClose: true,
+});
+
+/** 鍒锋柊琛ㄦ牸 */
+function handleRefresh() {
+  gridApi.query();
+}
+
+/** 瀵煎嚭琛ㄦ牸 */
+async function handleExport() {
+  const data = await exportPurchaseRequest(await gridApi.formApi.getValues());
+  downloadFileFromBlobPart({ fileName: '閲囪喘鐢宠.xls', source: data });
+}
+
+/** 鏂板閲囪喘鐢宠 */
+function handleCreate() {
+  formModalApi.setData({ formType: 'create' }).open();
+}
+
+/** 缂栬緫閲囪喘鐢宠 */
+function handleEdit(row: ErpPurchaseRequestApi.PurchaseRequest) {
+  formModalApi.setData({ formType: 'edit', id: row.id }).open();
+}
+
+/** 鍒犻櫎閲囪喘鐢宠 */
+async function handleDelete(ids: number[]) {
+  const hideLoading = message.loading({
+    content: $t('ui.actionMessage.deleting'),
+    duration: 0,
+  });
+  try {
+    await deletePurchaseRequest(ids);
+    message.success($t('ui.actionMessage.deleteSuccess'));
+    handleRefresh();
+  } finally {
+    hideLoading();
+  }
+}
+
+/** 瀹℃壒閲囪喘鐢宠 */
+async function handleApprove(row: ErpPurchaseRequestApi.PurchaseRequest, status: number) {
+  const hideLoading = message.loading({
+    content: `纭畾${status === 1 ? '瀹℃壒閫氳繃' : '瀹℃壒鎷掔粷'}璇ョ敵璇峰悧锛焋,
+    duration: 0,
+  });
+  try {
+    await approvePurchaseRequest({ id: row.id!, status, remark: '' });
+    message.success(`${status === 1 ? '瀹℃壒閫氳繃' : '瀹℃壒鎷掔粷'}鎴愬姛`);
+    handleRefresh();
+  } finally {
+    hideLoading();
+  }
+}
+
+/** 杞负閲囪喘璁㈠崟 */
+async function handleConvert(row: ErpPurchaseRequestApi.PurchaseRequest) {
+  const hideLoading = message.loading({
+    content: '姝e湪杞负閲囪喘璁㈠崟...',
+    duration: 0,
+  });
+  try {
+    await convertToPurchaseOrder(row.id!);
+    message.success('宸叉垚鍔熻浆涓洪噰璐鍗�');
+    handleRefresh();
+  } finally {
+    hideLoading();
+  }
+}
+
+const checkedIds = ref<number[]>([]);
+function handleRowCheckboxChange({
+  records,
+}: {
+  records: ErpPurchaseRequestApi.PurchaseRequest[];
+}) {
+  checkedIds.value = records.map((item) => item.id!);
+}
+
+/** 鏌ョ湅璇︽儏 */
+function handleDetail(row: ErpPurchaseRequestApi.PurchaseRequest) {
+  formModalApi.setData({ formType: 'detail', id: row.id }).open();
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  formOptions: {
+    schema: useGridFormSchema(),
+  },
+  gridOptions: {
+    columns: useGridColumns(),
+    height: 'auto',
+    keepSource: true,
+    proxyConfig: {
+      ajax: {
+        query: async ({ page }, formValues) => {
+          return await getPurchaseRequestPage({
+            pageNo: page.currentPage,
+            pageSize: page.pageSize,
+            ...formValues,
+          });
+        },
+      },
+    },
+    rowConfig: {
+      keyField: 'id',
+      isHover: true,
+    },
+    toolbarConfig: {
+      refresh: true,
+      search: true,
+    },
+  } as VxeTableGridOptions<ErpPurchaseRequestApi.PurchaseRequest>,
+  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: ['erp:purchase-request:create'],
+              onClick: handleCreate,
+            },
+            {
+              label: $t('ui.actionTitle.export'),
+              type: 'primary',
+              icon: ACTION_ICON.DOWNLOAD,
+              auth: ['erp:purchase-request:export'],
+              onClick: handleExport,
+            },
+            {
+              label: '鎵归噺鍒犻櫎',
+              type: 'primary',
+              danger: true,
+              disabled: isEmpty(checkedIds),
+              icon: ACTION_ICON.DELETE,
+              auth: ['erp:purchase-request:delete'],
+              popConfirm: {
+                title: `鏄惁鍒犻櫎鎵�閫変腑鏁版嵁锛焋,
+                confirm: handleDelete.bind(null, checkedIds),
+              },
+            },
+          ]"
+        />
+      </template>
+      <template #status="{ row }">
+        <Tag v-if="row.status === 0" color="warning">寰呭鎵�</Tag>
+        <Tag v-else-if="row.status === 1" color="success">宸插鎵�</Tag>
+        <Tag v-else-if="row.status === 2" color="error">宸叉嫆缁�</Tag>
+        <Tag v-else-if="row.status === 3" color="default">宸茶浆璁㈠崟</Tag>
+      </template>
+      <template #actions="{ row }">
+        <TableAction
+          :actions="[
+            {
+              label: $t('common.detail'),
+              type: 'link',
+              icon: ACTION_ICON.VIEW,
+              auth: ['erp:purchase-request:query'],
+              onClick: handleDetail.bind(null, row),
+            },
+            {
+              label: $t('common.edit'),
+              type: 'link',
+              icon: ACTION_ICON.EDIT,
+              auth: ['erp:purchase-request:update'],
+              ifShow: () => row.status === 0,
+              onClick: handleEdit.bind(null, row),
+            },
+            {
+              label: '瀹℃壒閫氳繃',
+              type: 'link',
+              auth: ['erp:purchase-request:approve'],
+              ifShow: () => row.status === 0,
+              popConfirm: {
+                title: '纭瀹℃壒閫氳繃璇ョ敵璇峰悧锛�',
+                confirm: handleApprove.bind(null, row, 1),
+              },
+            },
+            {
+              label: '瀹℃壒鎷掔粷',
+              type: 'link',
+              danger: true,
+              auth: ['erp:purchase-request:approve'],
+              ifShow: () => row.status === 0,
+              popConfirm: {
+                title: '纭瀹℃壒鎷掔粷璇ョ敵璇峰悧锛�',
+                confirm: handleApprove.bind(null, row, 2),
+              },
+            },
+            {
+              label: '杞鍗�',
+              type: 'link',
+              auth: ['erp:purchase-request:convert'],
+              ifShow: () => row.status === 1,
+              popConfirm: {
+                title: '纭杞负閲囪喘璁㈠崟鍚楋紵',
+                confirm: handleConvert.bind(null, row),
+              },
+            },
+            {
+              label: $t('common.delete'),
+              type: 'link',
+              danger: true,
+              icon: ACTION_ICON.DELETE,
+              auth: ['erp:purchase-request:delete'],
+              ifShow: () => row.status === 0,
+              popConfirm: {
+                title: $t('ui.actionMessage.deleteConfirm', [row.no]),
+                confirm: handleDelete.bind(null, [row.id!]),
+              },
+            },
+          ]"
+        />
+      </template>
+    </Grid>
+  </Page>
+</template>
\ No newline at end of file
diff --git a/src/views/erp/purchase/request/modules/form.vue b/src/views/erp/purchase/request/modules/form.vue
new file mode 100644
index 0000000..4bf6954
--- /dev/null
+++ b/src/views/erp/purchase/request/modules/form.vue
@@ -0,0 +1,135 @@
+<script lang="ts" setup>
+import type { FormType } from '../data';
+
+import type { ErpPurchaseRequestApi } from '#/api/erp/purchase/request';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
+
+import { message, Tag } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+  createPurchaseRequest,
+  getPurchaseRequest,
+  updatePurchaseRequest,
+} from '#/api/erp/purchase/request';
+import { $t } from '#/locales';
+
+import { useFormSchema, PURCHASE_REQUEST_STATUS } from '../data';
+import PurchaseRequestItemForm from './item-form.vue';
+
+const emit = defineEmits(['success']);
+const formData = ref<ErpPurchaseRequestApi.PurchaseRequest>();
+const formType = ref<FormType>('create');
+const itemFormRef = ref<InstanceType<typeof PurchaseRequestItemForm>>();
+
+const getTitle = computed(() => {
+  if (formType.value === 'create') {
+    return $t('ui.actionTitle.create', ['閲囪喘鐢宠']);
+  } else if (formType.value === 'edit') {
+    return $t('ui.actionTitle.edit', ['閲囪喘鐢宠']);
+  } else {
+    return '閲囪喘鐢宠璇︽儏';
+  }
+});
+
+const [Form, formApi] = useVbenForm({
+  commonConfig: {
+    componentProps: {
+      class: 'w-full',
+    },
+    labelWidth: 120,
+  },
+  wrapperClass: 'grid-cols-3',
+  layout: 'vertical',
+  schema: useFormSchema(formType.value),
+  showDefaultActions: false,
+});
+
+/** 鏇存柊閲囪喘鐢宠椤� */
+function handleUpdateItems(items: ErpPurchaseRequestApi.PurchaseRequestItem[]) {
+  formData.value = modalApi.getData<ErpPurchaseRequestApi.PurchaseRequest>();
+  formData.value.items = items;
+  formApi.setValues({
+    items,
+  });
+}
+
+/** 鍒涘缓鎴栨洿鏂伴噰璐敵璇� */
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    const { valid } = await formApi.validate();
+    if (!valid) {
+      return;
+    }
+    const itemFormInstance = Array.isArray(itemFormRef.value)
+      ? itemFormRef.value[0]
+      : itemFormRef.value;
+    try {
+      itemFormInstance.validate();
+    } catch (error: any) {
+      message.error(error.message || '瀛愯〃鍗曢獙璇佸け璐�');
+      return;
+    }
+
+    modalApi.lock();
+    const data =
+      (await formApi.getValues()) as ErpPurchaseRequestApi.PurchaseRequest;
+    data.items = formData.value?.items?.map((item) => ({
+      ...item,
+      id: undefined,
+    }));
+    try {
+      await (formType.value === 'create'
+        ? createPurchaseRequest(data)
+        : updatePurchaseRequest(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<{ formType: FormType; id?: number }>();
+    formType.value = data.formType;
+    formApi.setDisabled(formType.value === 'detail');
+    formApi.updateSchema(useFormSchema(formType.value));
+    if (!data || !data.id) {
+      return;
+    }
+    modalApi.lock();
+    try {
+      formData.value = await getPurchaseRequest(data.id);
+      await formApi.setValues(formData.value);
+    } finally {
+      modalApi.unlock();
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal
+    :title="getTitle"
+    class="w-3/4"
+    :show-confirm-button="formType !== 'detail'"
+  >
+    <Form class="mx-3">
+      <template #items>
+        <PurchaseRequestItemForm
+          ref="itemFormRef"
+          :items="formData?.items ?? []"
+          :disabled="formType === 'detail'"
+          @update:items="handleUpdateItems"
+        />
+      </template>
+    </Form>
+  </Modal>
+</template>
\ No newline at end of file
diff --git a/src/views/erp/purchase/request/modules/item-form.vue b/src/views/erp/purchase/request/modules/item-form.vue
new file mode 100644
index 0000000..14b2713
--- /dev/null
+++ b/src/views/erp/purchase/request/modules/item-form.vue
@@ -0,0 +1,267 @@
+<script lang="ts" setup>
+import type { MdmItemApi } from '#/api/mdm/item';
+import type { ErpPurchaseRequestApi } from '#/api/erp/purchase/request';
+
+import { computed, nextTick, onMounted, ref, watch } from 'vue';
+
+import {
+  erpCountInputFormatter,
+  erpPriceInputFormatter,
+  erpPriceMultiply,
+} from '../../../../../packages/utils/src';
+
+import { Input, InputNumber, Select, DatePicker } from 'ant-design-vue';
+
+import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import { getItemPage } from '#/api/mdm/item';
+
+import { useFormItemColumns } from '../data';
+
+interface Props {
+  items?: ErpPurchaseRequestApi.PurchaseRequestItem[];
+  disabled?: boolean;
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  items: () => [],
+  disabled: false,
+});
+
+const emit = defineEmits(['update:items']);
+
+const tableData = ref<ErpPurchaseRequestApi.PurchaseRequestItem[]>([]);
+const productOptions = ref<MdmItemApi.Item[]>([]);
+
+/** 鑾峰彇琛ㄦ牸鍚堣鏁版嵁 */
+const summaries = computed(() => {
+  return {
+    count: tableData.value.reduce((sum, item) => sum + (item.requestCount || 0), 0),
+    totalPrice: tableData.value.reduce(
+      (sum, item) => {
+        const price = erpPriceMultiply(item.estimatedPrice || 0, item.requestCount || 0) ?? 0;
+        return sum + price;
+      },
+      0,
+    ),
+  };
+});
+
+/** 琛ㄦ牸閰嶇疆 */
+const [Grid, gridApi] = useVbenVxeGrid({
+  gridOptions: {
+    columns: useFormItemColumns(props.disabled),
+    data: tableData.value,
+    minHeight: 250,
+    autoResize: true,
+    border: true,
+    rowConfig: {
+      keyField: 'seq',
+      isHover: true,
+    },
+    pagerConfig: {
+      enabled: false,
+    },
+    toolbarConfig: {
+      enabled: false,
+    },
+  },
+});
+
+/** 鐩戝惉澶栭儴浼犲叆鐨勫垪鏁版嵁 */
+watch(
+  () => props.items,
+  async (items) => {
+    if (!items) {
+      return;
+    }
+    items.forEach((item) => initRow(item));
+    tableData.value = [...items];
+    await nextTick();
+    await gridApi.grid.reloadData(tableData.value);
+  },
+  {
+    immediate: true,
+  },
+);
+
+/** 澶勭悊鏂板 */
+function handleAdd() {
+  const newRow = {
+    id: undefined,
+    productId: undefined,
+    productUnitName: undefined,
+    productBarCode: undefined,
+    requestCount: 1,
+    estimatedPrice: undefined,
+    requiredDate: undefined,
+    remark: undefined,
+    totalPrice: 0,
+  };
+  tableData.value.push(newRow);
+  emit('update:items', [...tableData.value]);
+}
+
+/** 澶勭悊鍒犻櫎 */
+function handleDelete(row: ErpPurchaseRequestApi.PurchaseRequestItem) {
+  const index = tableData.value.findIndex((item) => item.seq === row.seq);
+  if (index !== -1) {
+    tableData.value.splice(index, 1);
+  }
+  emit('update:items', [...tableData.value]);
+}
+
+/** 澶勭悊浜у搧鍙樻洿 */
+async function handleProductChange(productId: any, row: any) {
+  const product = productOptions.value.find((p) => p.id === productId);
+  if (!product) {
+    return;
+  }
+  row.productId = productId;
+  row.productBarCode = product.barCode;
+  row.productUnitName = product.unitMeasureName;
+  row.productName = product.name;
+  row.estimatedPrice = product.purchasePrice || 0;
+  row.requestCount = row.requestCount || 1;
+  handleRowChange(row);
+}
+
+/** 澶勭悊琛屾暟鎹彉鏇� */
+function handleRowChange(row: any) {
+  const index = tableData.value.findIndex((item) => item.seq === row.seq);
+  if (index === -1) {
+    tableData.value.push(row);
+  } else {
+    tableData.value[index] = row;
+  }
+  // 璁$畻棰勪及閲戦
+  if (row.estimatedPrice && row.requestCount) {
+    row.totalPrice = erpPriceMultiply(row.estimatedPrice, row.requestCount) ?? 0;
+  }
+  emit('update:items', [...tableData.value]);
+}
+
+/** 鍒濆鍖栬鏁版嵁 */
+function initRow(row: ErpPurchaseRequestApi.PurchaseRequestItem) {
+  if (row.estimatedPrice && row.requestCount) {
+    row.totalPrice = erpPriceMultiply(row.estimatedPrice, row.requestCount) ?? 0;
+  }
+}
+
+/** 琛ㄥ崟鏍¢獙 */
+function validate() {
+  for (let i = 0; i < tableData.value.length; i++) {
+    const item = tableData.value[i];
+    if (item) {
+      if (!item.productId) {
+        throw new Error(`绗� ${i + 1} 琛岋細浜у搧涓嶈兘涓虹┖`);
+      }
+      if (!item.requestCount || item.requestCount <= 0) {
+        throw new Error(`绗� ${i + 1} 琛岋細鐢宠鏁伴噺涓嶈兘涓虹┖`);
+      }
+    }
+  }
+}
+
+defineExpose({
+  validate,
+});
+
+/** 鍒濆鍖� */
+onMounted(async () => {
+  const res = await getItemPage({ pageNo: 1, pageSize: 100, status: 0 });
+  productOptions.value = res.list || [];
+  if (tableData.value.length === 0) {
+    handleAdd();
+  }
+});
+</script>
+
+<template>
+  <Grid class="w-full">
+    <template #productId="{ row }">
+      <Select
+        v-if="!disabled"
+        v-model:value="row.productId"
+        :options="productOptions"
+        :field-names="{ label: 'name', value: 'id' }"
+        class="w-full"
+        placeholder="璇烽�夋嫨浜у搧"
+        show-search
+        @change="handleProductChange($event, row)"
+      />
+      <span v-else>{{ row.productName || '-' }}</span>
+    </template>
+    <template #requestCount="{ row }">
+      <InputNumber
+        v-if="!disabled"
+        v-model:value="row.requestCount"
+        :min="0"
+        :precision="3"
+        @change="handleRowChange(row)"
+      />
+      <span v-else>{{ erpCountInputFormatter(row.requestCount) || '-' }}</span>
+    </template>
+    <template #estimatedPrice="{ row }">
+      <InputNumber
+        v-if="!disabled"
+        v-model:value="row.estimatedPrice"
+        :min="0"
+        :precision="2"
+        @change="handleRowChange(row)"
+      />
+      <span v-else>{{ erpPriceInputFormatter(row.estimatedPrice) || '-' }}</span>
+    </template>
+    <template #requiredDate="{ row }">
+      <DatePicker
+        v-if="!disabled"
+        v-model:value="row.requiredDate"
+        valueFormat="YYYY-MM-DD"
+        class="w-full"
+        @change="handleRowChange(row)"
+      />
+      <span v-else>{{ row.requiredDate || '-' }}</span>
+    </template>
+    <template #remark="{ row }">
+      <Input v-if="!disabled" v-model:value="row.remark" class="w-full" @change="handleRowChange(row)" />
+      <span v-else>{{ row.remark || '-' }}</span>
+    </template>
+    <template #actions="{ row }">
+      <TableAction
+        :actions="[
+          {
+            label: '鍒犻櫎',
+            type: 'link',
+            danger: true,
+            popConfirm: {
+              title: '纭鍒犻櫎璇ヤ骇鍝佸悧锛�',
+              confirm: handleDelete.bind(null, row),
+            },
+          },
+        ]"
+      />
+    </template>
+
+    <template #bottom>
+      <div class="mt-2 rounded border border-border bg-muted p-2">
+        <div class="flex justify-between text-sm text-muted-foreground">
+          <span class="font-medium text-foreground">鍚堣锛�</span>
+          <div class="flex space-x-4">
+            <span>鐢宠鏁伴噺锛歿{ erpCountInputFormatter(summaries.count) }}</span>
+            <span>棰勪及閲戦锛歿{ erpPriceInputFormatter(summaries.totalPrice) }}</span>
+          </div>
+        </div>
+      </div>
+      <TableAction
+        v-if="!disabled"
+        class="mt-2 flex justify-center"
+        :actions="[
+          {
+            label: '娣诲姞鐢宠浜у搧',
+            type: 'default',
+            onClick: handleAdd,
+          },
+        ]"
+      />
+    </template>
+  </Grid>
+</template>
\ No newline at end of file
diff --git a/src/views/erp/purchase/statistics/index.vue b/src/views/erp/purchase/statistics/index.vue
new file mode 100644
index 0000000..1fc05c1
--- /dev/null
+++ b/src/views/erp/purchase/statistics/index.vue
@@ -0,0 +1,85 @@
+<script lang="ts" setup>
+import { onMounted, ref } from 'vue';
+
+import { Page } from '../../../../packages/effects/common-ui/src';
+import { erpPriceInputFormatter } from '../../../../packages/utils/src';
+
+import { Card, Col, Row, Statistic } from 'ant-design-vue';
+
+import { getPurchaseSummary } from '#/api/erp/purchase/statistics';
+import type { ErpPurchaseStatisticsApi } from '#/api/erp/purchase/statistics';
+
+defineOptions({ name: 'ErpPurchaseStatistics' });
+
+const loading = ref(false);
+const summary = ref<ErpPurchaseStatisticsApi.Summary>({});
+
+/** 鑾峰彇缁熻鏁版嵁 */
+async function fetchSummary() {
+  loading.value = true;
+  try {
+    summary.value = await getPurchaseSummary();
+  } finally {
+    loading.value = false;
+  }
+}
+
+onMounted(() => {
+  fetchSummary();
+});
+</script>
+
+<template>
+  <Page auto-content-height>
+    <div class="p-4">
+      <Card title="閲囪喘姹囨�荤粺璁�" :loading="loading">
+        <Row :gutter="16">
+          <Col :span="6">
+            <Statistic
+              title="鎬昏鍗曟暟"
+              :value="summary.totalOrderCount || 0"
+              suffix="鍗�"
+            />
+          </Col>
+          <Col :span="6">
+            <Statistic
+              title="鎬昏鍗曢噾棰�"
+              :value="erpPriceInputFormatter(summary.totalOrderAmount) || '0.00'"
+              suffix="鍏�"
+            />
+          </Col>
+          <Col :span="6">
+            <Statistic
+              title="鎬诲叆搴撴暟"
+              :value="summary.totalInCount || 0"
+              suffix="鍗�"
+            />
+          </Col>
+          <Col :span="6">
+            <Statistic
+              title="鎬诲叆搴撻噾棰�"
+              :value="erpPriceInputFormatter(summary.totalInAmount) || '0.00'"
+              suffix="鍏�"
+            />
+          </Col>
+        </Row>
+        <Row :gutter="16" class="mt-4">
+          <Col :span="6">
+            <Statistic
+              title="鎬婚��璐ф暟"
+              :value="summary.totalReturnCount || 0"
+              suffix="鍗�"
+            />
+          </Col>
+          <Col :span="6">
+            <Statistic
+              title="鎬婚��璐ч噾棰�"
+              :value="erpPriceInputFormatter(summary.totalReturnAmount) || '0.00'"
+              suffix="鍏�"
+            />
+          </Col>
+        </Row>
+      </Card>
+    </div>
+  </Page>
+</template>
\ No newline at end of file
diff --git a/src/views/erp/purchase/statistics/price.vue b/src/views/erp/purchase/statistics/price.vue
new file mode 100644
index 0000000..86e6f1f
--- /dev/null
+++ b/src/views/erp/purchase/statistics/price.vue
@@ -0,0 +1,133 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { ErpPurchaseStatisticsApi } from '#/api/erp/purchase/statistics';
+
+import { Page } from '../../../../packages/effects/common-ui/src';
+
+import { Tag } from 'ant-design-vue';
+
+import { useVbenVxeGrid } from '#/adapter/vxe-table';
+import { getPriceAnalysis } from '#/api/erp/purchase/statistics';
+import { getProductSimpleList } from '#/api/erp/product/product';
+import { getRangePickerDefaultProps } from '#/utils';
+
+import type { VbenFormSchema } from '#/adapter/form';
+
+defineOptions({ name: 'ErpPurchaseStatisticsPrice' });
+
+/** 鎼滅储琛ㄥ崟 */
+const formSchema: VbenFormSchema[] = [
+  {
+    fieldName: 'productId',
+    label: '浜у搧',
+    component: 'ApiSelect',
+    componentProps: {
+      placeholder: '璇烽�夋嫨浜у搧',
+      allowClear: true,
+      showSearch: true,
+      api: getProductSimpleList,
+      labelField: 'name',
+      valueField: 'id',
+    },
+  },
+  {
+    fieldName: 'orderTime',
+    label: '閲囪喘鏃堕棿',
+    component: 'RangePicker',
+    componentProps: {
+      ...getRangePickerDefaultProps(),
+      allowClear: true,
+    },
+  },
+];
+
+/** 鍒楄〃瀛楁 */
+const columns: VxeTableGridOptions['columns'] = [
+  {
+    field: 'productName',
+    title: '浜у搧鍚嶇О',
+    minWidth: 150,
+  },
+  {
+    field: 'productBarCode',
+    title: '浜у搧鏉$爜',
+    minWidth: 120,
+  },
+  {
+    field: 'avgPrice',
+    title: '骞冲潎浠锋牸',
+    minWidth: 120,
+    formatter: 'formatAmount2',
+  },
+  {
+    field: 'minPrice',
+    title: '鏈�浣庝环鏍�',
+    minWidth: 120,
+    formatter: 'formatAmount2',
+  },
+  {
+    field: 'maxPrice',
+    title: '鏈�楂樹环鏍�',
+    minWidth: 120,
+    formatter: 'formatAmount2',
+  },
+  {
+    field: 'latestPrice',
+    title: '鏈�杩戜环鏍�',
+    minWidth: 120,
+    formatter: 'formatAmount2',
+  },
+  {
+    field: 'priceChangeRate',
+    title: '浠锋牸鍙樺寲鐜�',
+    minWidth: 120,
+    slots: { default: 'priceChangeRate' },
+  },
+];
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  formOptions: {
+    schema: formSchema,
+  },
+  gridOptions: {
+    columns,
+    height: 'auto',
+    keepSource: true,
+    proxyConfig: {
+      ajax: {
+        query: async ({ page }, formValues) => {
+          return await getPriceAnalysis({
+            pageNo: page.currentPage,
+            pageSize: page.pageSize,
+            ...formValues,
+          });
+        },
+      },
+    },
+    rowConfig: {
+      keyField: 'productId',
+      isHover: true,
+    },
+    toolbarConfig: {
+      refresh: true,
+      search: true,
+    },
+  } as VxeTableGridOptions<ErpPurchaseStatisticsApi.PriceAnalysis>,
+});
+</script>
+
+<template>
+  <Page auto-content-height>
+    <Grid table-title="閲囪喘浠锋牸鍒嗘瀽">
+      <template #priceChangeRate="{ row }">
+        <Tag v-if="row.priceChangeRate > 0" color="red">
+          +{{ row.priceChangeRate }}%
+        </Tag>
+        <Tag v-else-if="row.priceChangeRate < 0" color="green">
+          {{ row.priceChangeRate }}%
+        </Tag>
+        <Tag v-else color="default">{{ row.priceChangeRate }}%</Tag>
+      </template>
+    </Grid>
+  </Page>
+</template>
\ No newline at end of file
diff --git a/src/views/erp/purchase/statistics/supplier.vue b/src/views/erp/purchase/statistics/supplier.vue
new file mode 100644
index 0000000..a2a73b4
--- /dev/null
+++ b/src/views/erp/purchase/statistics/supplier.vue
@@ -0,0 +1,122 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { ErpPurchaseStatisticsApi } from '#/api/erp/purchase/statistics';
+
+import { Page } from '../../../../packages/effects/common-ui/src';
+
+import { useVbenVxeGrid } from '#/adapter/vxe-table';
+import { getSupplierStatistics } from '#/api/erp/purchase/statistics';
+import { getSupplierSimpleList } from '#/api/erp/purchase/supplier';
+import { getRangePickerDefaultProps } from '#/utils';
+
+import type { VbenFormSchema } from '#/adapter/form';
+
+defineOptions({ name: 'ErpPurchaseStatisticsSupplier' });
+
+/** 鎼滅储琛ㄥ崟 */
+const formSchema: VbenFormSchema[] = [
+  {
+    fieldName: 'supplierId',
+    label: '渚涘簲鍟�',
+    component: 'ApiSelect',
+    componentProps: {
+      placeholder: '璇烽�夋嫨渚涘簲鍟�',
+      allowClear: true,
+      showSearch: true,
+      api: getSupplierSimpleList,
+      labelField: 'name',
+      valueField: 'id',
+    },
+  },
+  {
+    fieldName: 'orderTime',
+    label: '璁㈠崟鏃堕棿',
+    component: 'RangePicker',
+    componentProps: {
+      ...getRangePickerDefaultProps(),
+      allowClear: true,
+    },
+  },
+];
+
+/** 鍒楄〃瀛楁 */
+const columns: VxeTableGridOptions['columns'] = [
+  {
+    field: 'supplierName',
+    title: '渚涘簲鍟�',
+    minWidth: 150,
+  },
+  {
+    field: 'orderCount',
+    title: '璁㈠崟鏁�',
+    minWidth: 100,
+    formatter: 'formatAmount3',
+  },
+  {
+    field: 'orderAmount',
+    title: '璁㈠崟閲戦',
+    minWidth: 120,
+    formatter: 'formatAmount2',
+  },
+  {
+    field: 'inCount',
+    title: '鍏ュ簱鏁�',
+    minWidth: 100,
+    formatter: 'formatAmount3',
+  },
+  {
+    field: 'inAmount',
+    title: '鍏ュ簱閲戦',
+    minWidth: 120,
+    formatter: 'formatAmount2',
+  },
+  {
+    field: 'returnCount',
+    title: '閫�璐ф暟',
+    minWidth: 100,
+    formatter: 'formatAmount3',
+  },
+  {
+    field: 'returnAmount',
+    title: '閫�璐ч噾棰�',
+    minWidth: 120,
+    formatter: 'formatAmount2',
+  },
+];
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  formOptions: {
+    schema: formSchema,
+  },
+  gridOptions: {
+    columns,
+    height: 'auto',
+    keepSource: true,
+    proxyConfig: {
+      ajax: {
+        query: async ({ page }, formValues) => {
+          return await getSupplierStatistics({
+            pageNo: page.currentPage,
+            pageSize: page.pageSize,
+            ...formValues,
+          });
+        },
+      },
+    },
+    rowConfig: {
+      keyField: 'supplierId',
+      isHover: true,
+    },
+    toolbarConfig: {
+      refresh: true,
+      search: true,
+    },
+  } as VxeTableGridOptions<ErpPurchaseStatisticsApi.SupplierStatistics>,
+});
+</script>
+
+<template>
+  <Page auto-content-height>
+    <Grid table-title="渚涘簲鍟嗕緵璐х粺璁�" />
+  </Page>
+</template>
\ No newline at end of file
diff --git a/src/views/erp/purchase/statistics/uncompleted.vue b/src/views/erp/purchase/statistics/uncompleted.vue
new file mode 100644
index 0000000..8aebf45
--- /dev/null
+++ b/src/views/erp/purchase/statistics/uncompleted.vue
@@ -0,0 +1,145 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { ErpPurchaseStatisticsApi } from '#/api/erp/purchase/statistics';
+
+import { Page } from '../../../../packages/effects/common-ui/src';
+
+import { Tag } from 'ant-design-vue';
+
+import { useVbenVxeGrid } from '#/adapter/vxe-table';
+import { getUncompletedOrders } from '#/api/erp/purchase/statistics';
+import { getSupplierSimpleList } from '#/api/erp/purchase/supplier';
+import { getRangePickerDefaultProps } from '#/utils';
+
+import type { VbenFormSchema } from '#/adapter/form';
+
+defineOptions({ name: 'ErpPurchaseStatisticsUncompleted' });
+
+/** 鎼滅储琛ㄥ崟 */
+const formSchema: VbenFormSchema[] = [
+  {
+    fieldName: 'orderNo',
+    label: '璁㈠崟鍙�',
+    component: 'Input',
+    componentProps: {
+      placeholder: '璇疯緭鍏ヨ鍗曞彿',
+      allowClear: true,
+    },
+  },
+  {
+    fieldName: 'supplierId',
+    label: '渚涘簲鍟�',
+    component: 'ApiSelect',
+    componentProps: {
+      placeholder: '璇烽�夋嫨渚涘簲鍟�',
+      allowClear: true,
+      showSearch: true,
+      api: getSupplierSimpleList,
+      labelField: 'name',
+      valueField: 'id',
+    },
+  },
+  {
+    fieldName: 'orderTime',
+    label: '璁㈠崟鏃堕棿',
+    component: 'RangePicker',
+    componentProps: {
+      ...getRangePickerDefaultProps(),
+      allowClear: true,
+    },
+  },
+];
+
+/** 鍒楄〃瀛楁 */
+const columns: VxeTableGridOptions['columns'] = [
+  {
+    field: 'orderNo',
+    title: '璁㈠崟鍙�',
+    minWidth: 150,
+  },
+  {
+    field: 'supplierName',
+    title: '渚涘簲鍟�',
+    minWidth: 150,
+  },
+  {
+    field: 'orderTime',
+    title: '璁㈠崟鏃堕棿',
+    minWidth: 160,
+    formatter: 'formatDate',
+  },
+  {
+    field: 'productNames',
+    title: '浜у搧淇℃伅',
+    minWidth: 150,
+    showOverflow: 'tooltip',
+  },
+  {
+    field: 'orderAmount',
+    title: '璁㈠崟閲戦',
+    minWidth: 120,
+    formatter: 'formatAmount2',
+  },
+  {
+    field: 'inAmount',
+    title: '鍏ュ簱閲戦',
+    minWidth: 120,
+    formatter: 'formatAmount2',
+  },
+  {
+    field: 'uncompletedAmount',
+    title: '鏈畬鎴愰噾棰�',
+    minWidth: 120,
+    formatter: 'formatAmount2',
+  },
+  {
+    field: 'daysOverdue',
+    title: '瓒呮湡澶╂暟',
+    minWidth: 100,
+    slots: { default: 'daysOverdue' },
+  },
+];
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  formOptions: {
+    schema: formSchema,
+  },
+  gridOptions: {
+    columns,
+    height: 'auto',
+    keepSource: true,
+    proxyConfig: {
+      ajax: {
+        query: async ({ page }, formValues) => {
+          return await getUncompletedOrders({
+            pageNo: page.currentPage,
+            pageSize: page.pageSize,
+            ...formValues,
+          });
+        },
+      },
+    },
+    rowConfig: {
+      keyField: 'orderId',
+      isHover: true,
+    },
+    toolbarConfig: {
+      refresh: true,
+      search: true,
+    },
+  } as VxeTableGridOptions<ErpPurchaseStatisticsApi.UncompletedOrder>,
+});
+</script>
+
+<template>
+  <Page auto-content-height>
+    <Grid table-title="鏈畬鎴愯鍗曡窡韪�">
+      <template #daysOverdue="{ row }">
+        <Tag v-if="row.daysOverdue > 0" color="error">
+          宸茶秴鏈� {{ row.daysOverdue }} 澶�
+        </Tag>
+        <Tag v-else color="success">鏈秴鏈�</Tag>
+      </template>
+    </Grid>
+  </Page>
+</template>
\ No newline at end of file
diff --git a/src/views/erp/purchase/supplier/data.ts b/src/views/erp/purchase/supplier/data.ts
index fdebf70..2dc2f3f 100644
--- a/src/views/erp/purchase/supplier/data.ts
+++ b/src/views/erp/purchase/supplier/data.ts
@@ -27,6 +27,22 @@
       },
     },
     {
+      fieldName: 'creditCode',
+      label: '缁熶竴绀句細淇$敤浠g爜',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ョ粺涓�绀句細淇$敤浠g爜',
+      },
+    },
+    {
+      fieldName: 'taxNo',
+      label: '绋庡彿',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ョ◣鍙�',
+      },
+    },
+    {
       fieldName: 'contact',
       label: '鑱旂郴浜�',
       component: 'Input',
@@ -35,15 +51,7 @@
       },
     },
     {
-      fieldName: 'mobile',
-      label: '鎵嬫満鍙风爜',
-      component: 'Input',
-      componentProps: {
-        placeholder: '璇疯緭鍏ユ墜鏈哄彿鐮�',
-      },
-    },
-    {
-      fieldName: 'telephone',
+      fieldName: 'phone',
       label: '鑱旂郴鐢佃瘽',
       component: 'Input',
       componentProps: {
@@ -52,83 +60,70 @@
     },
     {
       fieldName: 'email',
-      label: '鐢靛瓙閭',
+      label: '閭',
       component: 'Input',
       componentProps: {
-        placeholder: '璇疯緭鍏ョ數瀛愰偖绠�',
-      },
-    },
-    {
-      fieldName: 'fax',
-      label: '浼犵湡',
-      component: 'Input',
-      componentProps: {
-        placeholder: '璇疯緭鍏ヤ紶鐪�',
-      },
-    },
-    {
-      fieldName: 'status',
-      label: '寮�鍚姸鎬�',
-      component: 'RadioGroup',
-      componentProps: {
-        options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
-        buttonStyle: 'solid',
-        optionType: 'button',
-      },
-      rules: z.number().default(CommonStatusEnum.ENABLE),
-    },
-    {
-      fieldName: 'sort',
-      label: '鎺掑簭',
-      component: 'InputNumber',
-      componentProps: {
-        class: '!w-full',
-        placeholder: '璇疯緭鍏ユ帓搴�',
-      },
-      rules: 'required',
-    },
-    {
-      fieldName: 'taxNo',
-      label: '绾崇◣浜鸿瘑鍒彿',
-      component: 'Input',
-      componentProps: {
-        placeholder: '璇疯緭鍏ョ撼绋庝汉璇嗗埆鍙�',
-      },
-    },
-    {
-      fieldName: 'taxPercent',
-      label: '绋庣巼(%)',
-      component: 'InputNumber',
-      componentProps: {
-        class: '!w-full',
-        placeholder: '璇疯緭鍏ョ◣鐜�',
-        min: 0,
-        precision: 2,
+        placeholder: '璇疯緭鍏ラ偖绠�',
       },
     },
     {
       fieldName: 'bankName',
-      label: '寮�鎴疯',
+      label: '寮�鎴烽摱琛�',
       component: 'Input',
       componentProps: {
-        placeholder: '璇疯緭鍏ュ紑鎴疯',
+        placeholder: '璇疯緭鍏ュ紑鎴烽摱琛�',
       },
     },
     {
       fieldName: 'bankAccount',
-      label: '寮�鎴疯处鍙�',
+      label: '閾惰璐﹀彿',
       component: 'Input',
       componentProps: {
-        placeholder: '璇疯緭鍏ュ紑鎴疯处鍙�',
+        placeholder: '璇疯緭鍏ラ摱琛岃处鍙�',
       },
     },
     {
-      fieldName: 'bankAddress',
-      label: '寮�鎴峰湴鍧�',
+      fieldName: 'cooperationTime',
+      label: '鍚堜綔寮�濮嬫椂闂�',
+      component: 'DatePicker',
+      componentProps: {
+        placeholder: '璇烽�夋嫨鍚堜綔寮�濮嬫椂闂�',
+        class: '!w-full',
+        valueFormat: 'x',
+      },
+    },
+    {
+      fieldName: 'address',
+      label: '鍦板潃',
       component: 'Input',
       componentProps: {
-        placeholder: '璇疯緭鍏ュ紑鎴峰湴鍧�',
+        placeholder: '璇疯緭鍏ュ湴鍧�',
       },
+      formItemClass: 'col-span-2',
+    },
+    {
+      fieldName: 'status',
+      label: '鐘舵��',
+      component: 'RadioGroup',
+      componentProps: {
+        options: [
+          { label: '鍚敤', value: 0 },
+          { label: '绂佺敤', value: 1 },
+        ],
+        buttonStyle: 'solid',
+        optionType: 'button',
+      },
+      rules: z.number().default(0),
+    },
+    {
+      fieldName: 'qualification',
+      label: '璧勮川淇℃伅',
+      component: 'Textarea',
+      componentProps: {
+        placeholder: '璇疯緭鍏ヨ祫璐ㄤ俊鎭�',
+        rows: 3,
+      },
+      formItemClass: 'col-span-2',
     },
     {
       fieldName: 'remark',

--
Gitblit v1.9.3