From 2cecc157796d973f187a1ac7506576400f914beb Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期四, 02 七月 2026 14:25:57 +0800
Subject: [PATCH] 银川 1.销售报价页面开发、联调 2.销售订单页面开发、联调 3.合同管理页面开发联调

---
 src/api/crm/saleTrace/index.ts                    |   57 +
 src/views/basicData/mdm/unit/data.ts              |   67 -
 src/views/crm/saleQuotation/index.vue             |  210 +++++
 src/views/crm/contract/data.ts                    |   15 
 src/views/crm/product/components/data.ts          |   63 +
 src/views/crm/saleQuotation/modules/form.vue      |  124 +++
 src/components/crm-customer-select.vue            |   75 ++
 src/api/crm/report/index.ts                       |  106 ++
 src/views/erp/sale/return/modules/item-form.vue   |    8 
 src/views/erp/sale/return/data.ts                 |   18 
 src/api/crm/saleQuotation/index.ts                |  109 +++
 src/views/crm/contract/index.vue                  |  124 +++
 src/api/mdm/item/index.ts                         |    9 
 src/views/crm/contract/detail/index.vue           |   11 
 src/views/crm/product/components/edit-table.vue   |   58 
 src/views/erp/sale/out/modules/item-form.vue      |    8 
 src/views/erp/sale/order/modules/item-form.vue    |   18 
 src/api/crm/contract/index.ts                     |   40 
 src/views/basicData/mdm/modules/form.vue          |    4 
 src/views/erp/sale/order/data.ts                  |   34 
 src/views/crm/contract/modules/form.vue           |    2 
 src/views/crm/saleQuotation/modules/item-form.vue |  258 +++++++
 src/api/erp/sale/order/index.ts                   |    3 
 src/views/crm/saleQuotation/data.ts               |  413 +++++++++++
 src/views/erp/sale/order/index.vue                |   19 
 src/views/erp/sale/out/data.ts                    |   24 
 src/views/basicData/mdm/data.ts                   |  103 ++
 src/views/crm/report/index.vue                    |  174 ++++
 28 files changed, 1,963 insertions(+), 191 deletions(-)

diff --git a/src/api/crm/contract/index.ts b/src/api/crm/contract/index.ts
index 92e9695..4be139b 100644
--- a/src/api/crm/contract/index.ts
+++ b/src/api/crm/contract/index.ts
@@ -38,19 +38,31 @@
     updateTime?: Date;
     products?: ContractProduct[];
     contactName?: string;
+    orderId?: number; // 鍏宠仈 ERP 閿�鍞鍗曠紪鍙�
+    orderNo?: string; // ERP 閿�鍞鍗曞崟鍙�
   }
 
   /** 鍚堝悓浜у搧淇℃伅 */
   export interface ContractProduct {
     id: number;
-    productId: number;
-    productName: string;
-    productNo: string;
-    productUnit: number;
-    productPrice: number;
+    itemId: number;
+    itemName: string;
+    itemCode: string;
+    itemBarCode: string;
+    itemSpecification?: string;
+    itemUnitName: string;
+    itemUnitName2?: string;
+    itemUnitName3?: string;
+    itemPrice: number;
     contractPrice: number;
     count: number;
     totalPrice: number;
+  }
+
+  /** 瀹℃壒娴佺▼ */
+  export interface ApproveProcess {
+    key: string;
+    name: string;
   }
 }
 
@@ -113,8 +125,17 @@
 }
 
 /** 鎻愪氦瀹℃牳 */
-export function submitContract(id: number) {
-  return requestClient.put(`/crm/contract/submit?id=${id}`);
+export function submitContract(id: number, processDefinitionKey: string) {
+  return requestClient.put('/crm/contract/submit', null, {
+    params: { id, processDefinitionKey },
+  });
+}
+
+/** 鑾峰彇鍚堝悓瀹℃壒娴佺▼鍒楄〃 */
+export function getContractApproveProcessList() {
+  return requestClient.get<CrmContractApi.ApproveProcess[]>(
+    '/crm/contract/approve-process-list',
+  );
 }
 
 /** 鍚堝悓杞Щ */
@@ -131,3 +152,8 @@
 export function getRemindContractCount() {
   return requestClient.get<number>('/crm/contract/remind-count');
 }
+
+/** 鏍规嵁鍚堝悓鐢熸垚閿�鍞鍗� */
+export function generateSaleOrder(id: number) {
+  return requestClient.post<number>('/crm/contract/generate-sale-order', null, { params: { id } });
+}
diff --git a/src/api/crm/report/index.ts b/src/api/crm/report/index.ts
new file mode 100644
index 0000000..0d44c42
--- /dev/null
+++ b/src/api/crm/report/index.ts
@@ -0,0 +1,106 @@
+import { requestClient } from '#/api/request';
+
+export namespace CrmReportApi {
+  /** 瀹㈡埛鍒嗘瀽缁熻 */
+  export interface CustomerAnalyze {
+    totalCount: number;
+    todayCount: number;
+    dealCount: number;
+    dealRate: number;
+    lockCount: number;
+    poolCount: number;
+  }
+
+  /** 鍟嗘満鍒嗘瀽缁熻 */
+  export interface BusinessAnalyze {
+    totalCount: number;
+    todayCount: number;
+    totalPrice: number;
+    successCount: number;
+    successPrice: number;
+    successRate: number;
+    processingCount: number;
+    failCount: number;
+  }
+
+  /** 鍚堝悓鍒嗘瀽缁熻 */
+  export interface ContractAnalyze {
+    totalCount: number;
+    todayCount: number;
+    totalPrice: number;
+    approveCount: number;
+    approvePrice: number;
+    processCount: number;
+    receivablePrice: number;
+    receivableRate: number;
+  }
+
+  /** 閿�鍞紡鏂楅樁娈� */
+  export interface SalesFunnelStage {
+    name: string;
+    count: number | null;
+    price: number;
+    conversionRate: number;
+  }
+
+  /** 閿�鍞紡鏂楃粺璁� */
+  export interface SalesFunnel {
+    stages: SalesFunnelStage[];
+  }
+
+  /** 閿�鍞笟缁╅」 */
+  export interface SalesPerformanceItem {
+    userId: number;
+    userName: string;
+    deptName: string;
+    contractCount: number;
+    contractPrice: number;
+    receivablePrice: number;
+    businessCount: number;
+    businessPrice: number;
+  }
+
+  /** 閿�鍞笟缁╃粺璁� */
+  export interface SalesPerformance {
+    items: SalesPerformanceItem[];
+  }
+}
+
+/** 鑾峰彇瀹㈡埛鍒嗘瀽缁熻 */
+export function getCustomerAnalyze(startTime?: string, endTime?: string) {
+  return requestClient.get<CrmReportApi.CustomerAnalyze>(
+    '/crm/report/get-customer-analyze',
+    { params: { startTime, endTime } },
+  );
+}
+
+/** 鑾峰彇鍟嗘満鍒嗘瀽缁熻 */
+export function getBusinessAnalyze(startTime?: string, endTime?: string) {
+  return requestClient.get<CrmReportApi.BusinessAnalyze>(
+    '/crm/report/get-business-analyze',
+    { params: { startTime, endTime } },
+  );
+}
+
+/** 鑾峰彇鍚堝悓鍒嗘瀽缁熻 */
+export function getContractAnalyze(startTime?: string, endTime?: string) {
+  return requestClient.get<CrmReportApi.ContractAnalyze>(
+    '/crm/report/get-contract-analyze',
+    { params: { startTime, endTime } },
+  );
+}
+
+/** 鑾峰彇閿�鍞紡鏂楃粺璁� */
+export function getSalesFunnel() {
+  return requestClient.get<CrmReportApi.SalesFunnel>(
+    '/crm/report/get-sales-funnel',
+  );
+}
+
+/** 鑾峰彇閿�鍞笟缁╃粺璁� */
+export function getSalesPerformance(startTime?: string, endTime?: string) {
+  return requestClient.get<CrmReportApi.SalesPerformance>(
+    '/crm/report/get-sales-performance',
+    { params: { startTime, endTime } },
+  );
+}
\ No newline at end of file
diff --git a/src/api/crm/saleQuotation/index.ts b/src/api/crm/saleQuotation/index.ts
new file mode 100644
index 0000000..01267ff
--- /dev/null
+++ b/src/api/crm/saleQuotation/index.ts
@@ -0,0 +1,109 @@
+import type { PageParam, PageResult } from '../../../packages/effects/request/src';
+
+import { requestClient } from '#/api/request';
+
+export namespace CrmSaleQuotationApi {
+  /** 鎶ヤ环鍗曚俊鎭� */
+  export interface SaleQuotation {
+    id: number;
+    name: string;
+    no: string;
+    customerId: number;
+    customerName?: string;
+    businessId?: number;
+    businessName?: string;
+    contactId?: number;
+    contactName?: string;
+    quotationTime: Date;
+    validUntil?: Date;
+    totalProductPrice: number;
+    discountPercent: number;
+    totalPrice: number;
+    taxRate?: number;
+    fileUrl?: string;
+    remark?: string;
+    status: number;
+    contractId?: number;
+    contractNo?: string;
+    ownerUserId: number;
+    ownerUserName?: string;
+    creator: string;
+    creatorName: string;
+    createTime?: Date;
+    updateTime?: Date;
+    items?: SaleQuotationItem[];
+  }
+
+  /** 鎶ヤ环鍗曠墿鏂欐槑缁� */
+  export interface SaleQuotationItem {
+    id: number;
+    itemId: number;
+    itemName: string;
+    itemCode: string;
+    itemBarCode?: string;
+    itemSpecification?: string;
+    itemUnitName: string;
+    itemUnitName2?: string;
+    itemUnitName3?: string;
+    itemPrice: number;
+    quotationPrice: number;
+    count: number;
+    totalPrice: number;
+    taxPercent?: number;
+    taxPrice?: number;
+    remark?: string;
+  }
+}
+
+/** 鏌ヨ鎶ヤ环鍗曞垎椤靛垪琛� */
+export function getSaleQuotationPage(params: PageParam) {
+  return requestClient.get<PageResult<CrmSaleQuotationApi.SaleQuotation>>(
+    '/crm/sale-quotation/page',
+    { params },
+  );
+}
+
+/** 鏌ヨ鎶ヤ环鍗曞垎椤靛垪琛紙鍩轰簬瀹㈡埛锛� */
+export function getSaleQuotationPageByCustomer(params: PageParam) {
+  return requestClient.get<PageResult<CrmSaleQuotationApi.SaleQuotation>>(
+    '/crm/sale-quotation/page-by-customer',
+    { params },
+  );
+}
+
+/** 鏌ヨ鎶ヤ环鍗曞垎椤靛垪琛紙鍩轰簬鍟嗘満锛� */
+export function getSaleQuotationPageByBusiness(params: PageParam) {
+  return requestClient.get<PageResult<CrmSaleQuotationApi.SaleQuotation>>(
+    '/crm/sale-quotation/page-by-business',
+    { params },
+  );
+}
+
+/** 鏌ヨ鎶ヤ环鍗曡鎯� */
+export function getSaleQuotation(id: number) {
+  return requestClient.get<CrmSaleQuotationApi.SaleQuotation>(
+    `/crm/sale-quotation/get?id=${id}`,
+  );
+}
+
+/** 鏂板鎶ヤ环鍗� */
+export function createSaleQuotation(data: CrmSaleQuotationApi.SaleQuotation) {
+  return requestClient.post('/crm/sale-quotation/create', data);
+}
+
+/** 淇敼鎶ヤ环鍗� */
+export function updateSaleQuotation(data: CrmSaleQuotationApi.SaleQuotation) {
+  return requestClient.put('/crm/sale-quotation/update', data);
+}
+
+/** 鍒犻櫎鎶ヤ环鍗� */
+export function deleteSaleQuotation(id: number) {
+  return requestClient.delete(`/crm/sale-quotation/delete?id=${id}`);
+}
+
+/** 杞负鍚堝悓 */
+export function convertSaleQuotationToContract(id: number) {
+  return requestClient.post<number>('/crm/sale-quotation/convert-contract', null, {
+    params: { id },
+  });
+}
\ No newline at end of file
diff --git a/src/api/crm/saleTrace/index.ts b/src/api/crm/saleTrace/index.ts
new file mode 100644
index 0000000..baf5d62
--- /dev/null
+++ b/src/api/crm/saleTrace/index.ts
@@ -0,0 +1,57 @@
+import { requestClient } from '#/api/request';
+
+export namespace CrmSaleTraceApi {
+  /** 閿�鍞拷婧俊鎭� */
+  export interface SaleTrace {
+    businessId?: number;
+    businessName?: string;
+    businessPrice?: number;
+    businessCreateTime?: Date;
+    customerId?: number;
+    customerName?: string;
+    contractId?: number;
+    contractName?: string;
+    contractNo?: string;
+    contractPrice?: number;
+    contractCreateTime?: Date;
+    contractStatus?: number;
+    orderId?: number;
+    orderNo?: string;
+    orderPrice?: number;
+    orderCreateTime?: Date;
+    receivablePrice?: number;
+    receivableItems?: ReceivableItem[];
+  }
+
+  /** 鍥炴鏄庣粏 */
+  export interface ReceivableItem {
+    receivableId: number;
+    receivableNo: string;
+    price: number;
+    createTime?: Date;
+  }
+}
+
+/** 鏍规嵁鍟嗘満缂栧彿鑾峰彇閿�鍞祦绋嬭拷婧俊鎭� */
+export function getTraceByBusinessId(businessId: number) {
+  return requestClient.get<CrmSaleTraceApi.SaleTrace>(
+    '/crm/sale-trace/get-by-business-id',
+    { params: { businessId } },
+  );
+}
+
+/** 鏍规嵁鍚堝悓缂栧彿鑾峰彇閿�鍞祦绋嬭拷婧俊鎭� */
+export function getTraceByContractId(contractId: number) {
+  return requestClient.get<CrmSaleTraceApi.SaleTrace>(
+    '/crm/sale-trace/get-by-contract-id',
+    { params: { contractId } },
+  );
+}
+
+/** 鏍规嵁瀹㈡埛缂栧彿鑾峰彇閿�鍞祦绋嬭拷婧垪琛� */
+export function getTraceListByCustomerId(customerId: number) {
+  return requestClient.get<CrmSaleTraceApi.SaleTrace[]>(
+    '/crm/sale-trace/get-list-by-customer-id',
+    { params: { customerId } },
+  );
+}
\ No newline at end of file
diff --git a/src/api/erp/sale/order/index.ts b/src/api/erp/sale/order/index.ts
index 17ae5d6..077ca9a 100644
--- a/src/api/erp/sale/order/index.ts
+++ b/src/api/erp/sale/order/index.ts
@@ -8,6 +8,7 @@
     id?: number; // 璁㈠崟宸ュ崟缂栧彿
     no: string; // 閿�鍞鍗曞彿
     customerId: number; // 瀹㈡埛缂栧彿
+    customerName?: string; // 瀹㈡埛鍚嶇О
     accountId?: number; // 鏀舵璐︽埛缂栧彿
     orderTime: Date; // 璁㈠崟鏃堕棿
     totalCount: number; // 鍚堣鏁伴噺
@@ -22,6 +23,8 @@
     discountPercent?: number; // 浼樻儬鐜囷紝鐧惧垎姣�
     discountPrice?: number; // 浼樻儬閲戦锛屽崟浣嶏細鍏�
     depositPrice?: number; // 瀹氶噾閲戦锛屽崟浣嶏細鍏�
+    contractId?: number; // 鍏宠仈 CRM 鍚堝悓缂栧彿
+    contractNo?: string; // CRM 鍚堝悓鍗曞彿
     items?: SaleOrderItem[]; // 閿�鍞鍗曚骇鍝佹槑缁嗗垪琛�
   }
 
diff --git a/src/api/mdm/item/index.ts b/src/api/mdm/item/index.ts
index 6ceb79a..275a7da 100644
--- a/src/api/mdm/item/index.ts
+++ b/src/api/mdm/item/index.ts
@@ -14,6 +14,12 @@
     categoryName?: string;
     unitMeasureId?: number;
     unitMeasureName?: string;
+    unitMeasureId2?: number;
+    unitMeasureName2?: string;
+    unitMeasureRate1?: number;
+    unitMeasureId3?: number;
+    unitMeasureName3?: string;
+    unitMeasureRate2?: number;
     brandId?: number;
     brandName?: string;
     itemType: number;
@@ -26,6 +32,9 @@
     maxStock?: number;
     isBatchManaged: boolean;
     expiryDay?: number;
+    warehouseId?: number;
+    warehouseCode?: string;
+    warehouseName?: string;
     remark?: string;
     createTime?: string;
   }
diff --git a/src/components/crm-customer-select.vue b/src/components/crm-customer-select.vue
new file mode 100644
index 0000000..8f982bc
--- /dev/null
+++ b/src/components/crm-customer-select.vue
@@ -0,0 +1,75 @@
+<script lang="ts" setup>
+import type { CrmCustomerApi } from '#/api/crm/customer';
+
+import { computed, ref, watch } from 'vue';
+
+import { Select } from 'ant-design-vue';
+
+import { getCustomerSimpleList } from '#/api/crm/customer';
+
+interface Props {
+  value?: number;
+  disabled?: boolean;
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  value: undefined,
+  disabled: false,
+});
+
+const emit = defineEmits(['update:value', 'change']);
+
+const customerList = ref<CrmCustomerApi.Customer[]>([]);
+const loading = ref(false);
+
+const options = computed(() => {
+  return customerList.value.map((item) => ({
+    label: item.name,
+    value: item.id,
+  }));
+});
+
+const selectedValue = ref(props.value);
+
+watch(
+  () => props.value,
+  (val) => {
+    selectedValue.value = val;
+  },
+);
+
+watch(selectedValue, (val) => {
+  emit('update:value', val);
+  const customer = customerList.value.find((item) => item.id === val);
+  emit('change', customer);
+});
+
+async function loadCustomers() {
+  if (customerList.value.length > 0) return;
+  loading.value = true;
+  try {
+    customerList.value = await getCustomerSimpleList();
+  } finally {
+    loading.value = false;
+  }
+}
+
+function filterOption(input: string, option: any) {
+  return option.label.toLowerCase().includes(input.toLowerCase());
+}
+
+loadCustomers();
+</script>
+
+<template>
+  <Select
+    v-model:value="selectedValue"
+    :options="options"
+    :loading="loading"
+    :disabled="disabled"
+    placeholder="璇烽�夋嫨瀹㈡埛"
+    show-search
+    :filter-option="filterOption"
+    allow-clear
+  />
+</template>
\ No newline at end of file
diff --git a/src/views/basicData/mdm/data.ts b/src/views/basicData/mdm/data.ts
index 1808f14..479f150 100644
--- a/src/views/basicData/mdm/data.ts
+++ b/src/views/basicData/mdm/data.ts
@@ -6,6 +6,7 @@
 
 import { z } from '#/adapter/form';
 import { getUnitPage } from '#/api/mdm/unit';
+import { getWarehouseSimpleList } from '#/api/erp/stock/warehouse';
 
 /** 鐗╂枡绫诲瀷閫夐」 */
 export const ITEM_TYPE_OPTIONS = [
@@ -72,10 +73,19 @@
       rules: 'required',
     },
     {
+      component: 'Input',
+      fieldName: 'specification',
+      label: '瑙勬牸鍨嬪彿',
+      componentProps: {
+        placeholder: '璇疯緭鍏ヨ鏍煎瀷鍙�',
+      },
+    },
+    {
       component: 'ApiSelect',
       fieldName: 'unitMeasureId',
       label: '璁¢噺鍗曚綅',
       rules: 'required',
+      formItemClass: 'col-span-1',
       componentProps: {
         placeholder: '璇烽�夋嫨璁¢噺鍗曚綅',
         allowClear: true,
@@ -83,6 +93,74 @@
           const res = await getUnitPage({ pageNo: 1, pageSize: 100, status: 0 });
           return res.list || [];
         },
+        labelField: 'name',
+        valueField: 'id',
+      },
+    },
+    {
+      component: 'ApiSelect',
+      fieldName: 'unitMeasureId2',
+      label: '杈呭崟浣�1',
+      formItemClass: 'col-span-1',
+      componentProps: {
+        placeholder: '璇烽�夋嫨杈呭崟浣�1',
+        allowClear: true,
+        api: async () => {
+          const res = await getUnitPage({ pageNo: 1, pageSize: 100, status: 0 });
+          return res.list || [];
+        },
+        labelField: 'name',
+        valueField: 'id',
+      },
+    },
+    {
+      component: 'InputNumber',
+      fieldName: 'unitMeasureRate1',
+      label: '杈呭崟浣�1鎹㈢畻姣旂巼',
+      formItemClass: 'col-span-1',
+      componentProps: {
+        class: '!w-full',
+        min: 0,
+        precision: 6,
+        placeholder: '璇疯緭鍏ユ崲绠楁瘮鐜�',
+      },
+    },
+    {
+      component: 'ApiSelect',
+      fieldName: 'unitMeasureId3',
+      label: '杈呭崟浣�2',
+      formItemClass: 'col-span-1',
+      componentProps: {
+        placeholder: '璇烽�夋嫨杈呭崟浣�2',
+        allowClear: true,
+        api: async () => {
+          const res = await getUnitPage({ pageNo: 1, pageSize: 100, status: 0 });
+          return res.list || [];
+        },
+        labelField: 'name',
+        valueField: 'id',
+      },
+    },
+    {
+      component: 'InputNumber',
+      fieldName: 'unitMeasureRate2',
+      label: '杈呭崟浣�2鎹㈢畻姣旂巼',
+      formItemClass: 'col-span-1',
+      componentProps: {
+        class: '!w-full',
+        min: 0,
+        precision: 6,
+        placeholder: '璇疯緭鍏ユ崲绠楁瘮鐜�',
+      },
+    },
+    {
+      component: 'ApiSelect',
+      fieldName: 'warehouseId',
+      label: '榛樿浠撳簱',
+      componentProps: {
+        placeholder: '璇烽�夋嫨榛樿浠撳簱',
+        allowClear: true,
+        api: getWarehouseSimpleList,
         labelField: 'name',
         valueField: 'id',
       },
@@ -275,6 +353,31 @@
       minWidth: 80,
     },
     {
+      field: 'unitMeasureName2',
+      title: '杈呭崟浣�1',
+      minWidth: 80,
+    },
+    {
+      field: 'unitMeasureRate1',
+      title: '杈呭崟浣�1鎹㈢畻姣旂巼',
+      minWidth: 130,
+    },
+    {
+      field: 'unitMeasureName3',
+      title: '杈呭崟浣�2',
+      minWidth: 80,
+    },
+    {
+      field: 'unitMeasureRate2',
+      title: '杈呭崟浣�2鎹㈢畻姣旂巼',
+      minWidth: 130,
+    },
+    {
+      field: 'warehouseName',
+      title: '榛樿浠撳簱',
+      minWidth: 100,
+    },
+    {
       field: 'itemType',
       title: '鐗╂枡绫诲瀷',
       minWidth: 100,
diff --git a/src/views/basicData/mdm/modules/form.vue b/src/views/basicData/mdm/modules/form.vue
index 6b339cb..27b8097 100644
--- a/src/views/basicData/mdm/modules/form.vue
+++ b/src/views/basicData/mdm/modules/form.vue
@@ -28,7 +28,7 @@
     componentProps: {
       class: 'w-full',
     },
-    labelWidth: 100,
+    labelWidth: 110,
   },
   layout: 'horizontal',
   schema: useFormSchema(),
@@ -74,7 +74,7 @@
 </script>
 
 <template>
-  <Modal class="w-[800px]" :title="getTitle">
+  <Modal class="w-[1000px]" :title="getTitle">
     <Form class="mx-4" />
   </Modal>
 </template>
\ No newline at end of file
diff --git a/src/views/basicData/mdm/unit/data.ts b/src/views/basicData/mdm/unit/data.ts
index e6081e4..b9d0be3 100644
--- a/src/views/basicData/mdm/unit/data.ts
+++ b/src/views/basicData/mdm/unit/data.ts
@@ -5,7 +5,6 @@
 import { getDictOptions } from '#/packages/effects/hooks/src';
 
 import { z } from '#/adapter/form';
-import { getUnitPage } from '#/api/mdm/unit';
 
 /** 鏂板/淇敼鐨勮〃鍗� */
 export function useFormSchema(): VbenFormSchema[] {
@@ -34,61 +33,6 @@
       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: '璇疯緭鍏ユ崲绠楁瘮渚�',
       },
     },
     {
@@ -161,17 +105,6 @@
       field: 'name',
       title: '鍗曚綅鍚嶇О',
       minWidth: 150,
-    },
-    {
-      field: 'primaryFlag',
-      title: '涓诲崟浣�',
-      minWidth: 80,
-      slots: { default: 'primaryFlag' },
-    },
-    {
-      field: 'changeRate',
-      title: '鎹㈢畻姣斾緥',
-      minWidth: 100,
     },
     {
       field: 'status',
diff --git a/src/views/crm/contract/data.ts b/src/views/crm/contract/data.ts
index 454afe8..e9ac060 100644
--- a/src/views/crm/contract/data.ts
+++ b/src/views/crm/contract/data.ts
@@ -1,7 +1,6 @@
 import type { VbenFormSchema } from '#/adapter/form';
 import type { VxeTableGridOptions } from '#/adapter/vxe-table';
 
-import { DICT_TYPE } from '../../../packages/constants/src';
 import { useUserStore } from '../../../packages/stores/src';
 import { erpPriceInputFormatter, erpPriceMultiply } from '../../../packages/utils/src';
 
@@ -402,16 +401,20 @@
       field: 'auditStatus',
       fixed: 'right',
       minWidth: 120,
-      cellRender: {
-        name: 'CellDict',
-        props: { type: DICT_TYPE.CRM_AUDIT_STATUS },
-      },
+      slots: { default: 'auditStatus' },
+    },
+    {
+      title: '鍏宠仈璁㈠崟',
+      field: 'orderNo',
+      fixed: 'right',
+      minWidth: 150,
+      slots: { default: 'orderNo' },
     },
     {
       title: '鎿嶄綔',
       field: 'actions',
       fixed: 'right',
-      minWidth: 130,
+      minWidth: 220,
       slots: { default: 'actions' },
     },
   ];
diff --git a/src/views/crm/contract/detail/index.vue b/src/views/crm/contract/detail/index.vue
index ca15dfe..dbef431 100644
--- a/src/views/crm/contract/detail/index.vue
+++ b/src/views/crm/contract/detail/index.vue
@@ -38,6 +38,7 @@
 const contract = ref<CrmContractApi.Contract>({} as CrmContractApi.Contract); // 鍚堝悓璇︽儏
 const logList = ref<SystemOperateLogApi.OperateLog[]>([]); // 鎿嶄綔鏃ュ織
 const permissionListRef = ref<InstanceType<typeof PermissionList>>(); // 鍥㈤槦鎴愬憳鍒楄〃 Ref
+const activeTab = ref('1'); // 褰撳墠婵�娲荤殑 Tab
 
 const [Descriptions] = useDescription({
   bordered: false,
@@ -128,13 +129,13 @@
       <Descriptions :data="contract" />
     </Card>
     <Card class="mt-4 min-h-[60%]">
-      <Tabs>
-        <Tabs.TabPane tab="璺熻繘璁板綍" key="1" :force-render="true">
-          <FollowUp :biz-id="contractId" :biz-type="BizTypeEnum.CRM_CONTRACT" />
-        </Tabs.TabPane>
-        <Tabs.TabPane tab="鍩烘湰淇℃伅" key="2" :force-render="true">
+      <Tabs v-model:activeKey="activeTab">
+        <Tabs.TabPane tab="鍩烘湰淇℃伅" key="1" :force-render="true">
           <ContractDetailsInfo :contract="contract" />
         </Tabs.TabPane>
+        <Tabs.TabPane tab="璺熻繘璁板綍" key="2" :force-render="true">
+          <FollowUp :biz-id="contractId" :biz-type="BizTypeEnum.CRM_CONTRACT" />
+        </Tabs.TabPane>
         <Tabs.TabPane tab="浜у搧" key="3" :force-render="true">
           <ProductDetailsList
             :biz-id="contractId"
diff --git a/src/views/crm/contract/index.vue b/src/views/crm/contract/index.vue
index 374c07c..94b2b2a 100644
--- a/src/views/crm/contract/index.vue
+++ b/src/views/crm/contract/index.vue
@@ -8,12 +8,14 @@
 import { Page, useVbenModal } from '../../../packages/effects/common-ui/src';
 import { downloadFileFromBlobPart } from '../../../packages/utils/src';
 
-import { Button, message, Tabs } from 'ant-design-vue';
+import { Button, message, Modal, Select, Tabs, Tag } from 'ant-design-vue';
 
 import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
 import {
   deleteContract,
   exportContract,
+  generateSaleOrder,
+  getContractApproveProcessList,
   getContractPage,
   submitContract,
 } from '#/api/crm/contract';
@@ -76,18 +78,34 @@
   }
 }
 
-/** 鎻愪氦瀹℃牳 */
+/** 鎻愪氦瀹℃壒寮圭獥 */
+const submitModalVisible = ref(false);
+const currentRow = ref<CrmContractApi.Contract>();
+const processList = ref<CrmContractApi.ApproveProcess[]>([]);
+const selectedProcessKey = ref<string>();
+const submitLoading = ref(false);
+
 async function handleSubmit(row: CrmContractApi.Contract) {
-  const hideLoading = message.loading({
-    content: '鎻愪氦瀹℃牳涓�...',
-    duration: 0,
-  });
+  const res = await getContractApproveProcessList();
+  processList.value = res;
+  currentRow.value = row;
+  selectedProcessKey.value = undefined;
+  submitModalVisible.value = true;
+}
+
+async function confirmSubmit() {
+  if (!selectedProcessKey.value) {
+    message.warning('璇烽�夋嫨瀹℃壒娴佺▼');
+    return;
+  }
+  submitLoading.value = true;
   try {
-    await submitContract(row.id!);
-    message.success('鎻愪氦瀹℃牳鎴愬姛');
+    await submitContract(currentRow.value!.id!, selectedProcessKey.value);
+    message.success('鎻愪氦瀹℃壒鎴愬姛');
+    submitModalVisible.value = false;
     handleRefresh();
   } finally {
-    hideLoading();
+    submitLoading.value = false;
   }
 }
 
@@ -117,6 +135,39 @@
     name: 'BpmProcessInstanceDetail',
     query: { id: row.processInstanceId },
   });
+}
+
+/** 鐢熸垚閿�鍞鍗� */
+async function handleGenerateOrder(row: CrmContractApi.Contract) {
+  if (row.auditStatus !== 20) {
+    message.warning('鍙湁宸插鎵圭殑鍚堝悓鎵嶈兘鐢熸垚閿�鍞鍗�');
+    return;
+  }
+  if (row.orderId) {
+    message.warning('璇ュ悎鍚屽凡鐢熸垚閿�鍞鍗�');
+    return;
+  }
+  const hideLoading = message.loading({
+    content: '姝e湪鐢熸垚閿�鍞鍗�...',
+    duration: 0,
+  });
+  try {
+    const orderId = await generateSaleOrder(row.id!);
+    message.success('鐢熸垚閿�鍞鍗曟垚鍔�');
+    handleRefresh();
+  } finally {
+    hideLoading();
+  }
+}
+
+/** 鏌ョ湅閿�鍞鍗� */
+function handleViewOrder(row: CrmContractApi.Contract) {
+  if (row.orderId) {
+    push({
+      name: 'ErpSaleOrder',
+      query: { id: row.orderId },
+    });
+  }
 }
 
 const [Grid, gridApi] = useVbenVxeGrid({
@@ -152,7 +203,8 @@
 </script>
 
 <template>
-  <Page auto-content-height><FormModal @success="handleRefresh" />
+  <Page auto-content-height>
+    <FormModal @success="handleRefresh" />
     <Grid>
       <template #toolbar-actions>
         <Tabs class="w-full" @change="handleChangeSceneType">
@@ -201,6 +253,18 @@
           {{ row.signContactName }}
         </Button>
       </template>
+      <template #auditStatus="{ row }">
+        <Tag v-if="row.auditStatus === 0" color="default">鏈彁浜�</Tag>
+        <Tag v-if="row.auditStatus === 10" color="warning">瀹℃壒涓�</Tag>
+        <Tag v-if="row.auditStatus === 20" color="success">瀹℃牳閫氳繃</Tag>
+        <Tag v-if="row.auditStatus === 30" color="error">瀹℃牳涓嶉�氳繃</Tag>
+      </template>
+      <template #orderNo="{ row }">
+        <Button v-if="row.orderId" type="link" @click="handleViewOrder(row)">
+          {{ row.orderNo }}
+        </Button>
+        <span v-else>-</span>
+      </template>
       <template #actions="{ row }">
         <TableAction
           :actions="[
@@ -213,7 +277,7 @@
               ifShow: row.auditStatus === 0,
             },
             {
-              label: '鎻愪氦瀹℃牳',
+              label: '鎻愪氦瀹℃壒',
               type: 'link',
               auth: ['crm:contract:update'],
               onClick: handleSubmit.bind(null, row),
@@ -227,6 +291,20 @@
               ifShow: row.auditStatus !== 0,
             },
             {
+              label: '鐢熸垚閿�鍞鍗�',
+              type: 'link',
+              auth: ['crm:contract:update'],
+              onClick: handleGenerateOrder.bind(null, row),
+              ifShow: row.auditStatus === 20 && !row.orderId,
+            },
+            {
+              label: '鏌ョ湅璁㈠崟',
+              type: 'link',
+              auth: ['erp:sale-order:query'],
+              onClick: handleViewOrder.bind(null, row),
+              ifShow: !!row.orderId,
+            },
+            {
               label: $t('common.delete'),
               type: 'link',
               danger: true,
@@ -235,10 +313,34 @@
                 title: $t('ui.actionMessage.deleteConfirm', [row.name]),
                 confirm: handleDelete.bind(null, row),
               },
+              ifShow: row.auditStatus === 0,
             },
           ]"
         />
       </template>
     </Grid>
+
+    <!-- 鎻愪氦瀹℃壒寮圭獥 -->
+    <Modal
+      v-model:open="submitModalVisible"
+      title="閫夋嫨瀹℃壒娴佺▼"
+      width="400px"
+      @ok="confirmSubmit"
+      :confirmLoading="submitLoading"
+    >
+      <Select
+        v-model:value="selectedProcessKey"
+        placeholder="璇烽�夋嫨瀹℃壒娴佺▼"
+        class="w-full"
+      >
+        <Select.Option
+          v-for="item in processList"
+          :key="item.key"
+          :value="item.key"
+        >
+          {{ item.name }}
+        </Select.Option>
+      </Select>
+    </Modal>
   </Page>
 </template>
diff --git a/src/views/crm/contract/modules/form.vue b/src/views/crm/contract/modules/form.vue
index 0e02275..2e9e009 100644
--- a/src/views/crm/contract/modules/form.vue
+++ b/src/views/crm/contract/modules/form.vue
@@ -105,7 +105,7 @@
 </script>
 
 <template>
-  <Modal :title="getTitle" class="w-1/2">
+  <Modal :title="getTitle" class="w-[80%]">
     <Form class="mx-4">
       <template #product="slotProps">
         <ProductEditTable
diff --git a/src/views/crm/product/components/data.ts b/src/views/crm/product/components/data.ts
index ece3e43..2892603 100644
--- a/src/views/crm/product/components/data.ts
+++ b/src/views/crm/product/components/data.ts
@@ -1,31 +1,29 @@
 import type { VxeTableGridOptions } from '#/adapter/vxe-table';
 
-import { DICT_TYPE } from '../../../../packages/constants/src';
-
 /** 浜у搧璇︽儏鍒楄〃鐨勫垪瀹氫箟 */
 export function useDetailListColumns(
   showBusinessPrice: boolean,
 ): VxeTableGridOptions['columns'] {
   return [
     {
-      field: 'productName',
+      field: 'itemName',
       title: '浜у搧鍚嶇О',
     },
     {
-      field: 'productNo',
-      title: '浜у搧鏉$爜',
+      field: 'itemCode',
+      title: '鐗╂枡缂栫爜',
     },
     {
-      field: 'productUnit',
-      title: '浜у搧鍗曚綅',
-      cellRender: {
-        name: 'CellDict',
-        props: { type: DICT_TYPE.CRM_PRODUCT_UNIT },
-      },
+      field: 'itemBarCode',
+      title: '鐗╂枡鏉$爜',
     },
     {
-      field: 'productPrice',
-      title: '浜у搧浠锋牸锛堝厓锛�',
+      field: 'itemUnitName',
+      title: '鍗曚綅',
+    },
+    {
+      field: 'itemPrice',
+      title: '鍘熶环锛堝厓锛�',
       formatter: 'formatAmount2',
     },
     {
@@ -58,36 +56,47 @@
   return [
     { type: 'seq', title: '搴忓彿', minWidth: 50 },
     {
-      field: 'productId',
+      field: 'itemId',
       title: '浜у搧鍚嶇О',
       minWidth: 100,
-      slots: { default: 'productId' },
+      slots: { default: 'itemId' },
     },
     {
-      field: 'productNo',
+      field: 'itemSpecification',
+      title: '瑙勬牸鍨嬪彿',
+      minWidth: 120,
+    },
+    {
+      field: 'itemBarCode',
       title: '鏉$爜',
       minWidth: 150,
     },
     {
-      field: 'productUnit',
+      field: 'itemUnitName',
       title: '鍗曚綅',
-      minWidth: 100,
-      cellRender: {
-        name: 'CellDict',
-        props: { type: DICT_TYPE.CRM_PRODUCT_UNIT },
-      },
+      minWidth: 80,
     },
     {
-      field: 'productPrice',
-      title: '浠锋牸锛堝厓锛�',
+      field: 'itemUnitName2',
+      title: '杈呭崟浣�1',
+      minWidth: 80,
+    },
+    {
+      field: 'itemUnitName3',
+      title: '杈呭崟浣�2',
+      minWidth: 80,
+    },
+    {
+      field: 'itemPrice',
+      title: '鍘熶环锛堝厓锛�',
       minWidth: 100,
       formatter: 'formatAmount2',
     },
     {
-      field: 'sellingPrice',
-      title: '鍞环锛堝厓锛�',
+      field: 'contractPrice',
+      title: '鍚堝悓浠凤紙鍏冿級',
       minWidth: 100,
-      slots: { default: 'sellingPrice' },
+      slots: { default: 'contractPrice' },
     },
     {
       field: 'count',
diff --git a/src/views/crm/product/components/edit-table.vue b/src/views/crm/product/components/edit-table.vue
index 3866712..55d608d 100644
--- a/src/views/crm/product/components/edit-table.vue
+++ b/src/views/crm/product/components/edit-table.vue
@@ -1,7 +1,7 @@
 <script lang="ts" setup>
 import type { CrmBusinessApi } from '#/api/crm/business';
 import type { CrmContractApi } from '#/api/crm/contract';
-import type { CrmProductApi } from '#/api/crm/product';
+import type { MdmItemApi } from '#/api/mdm/item';
 
 import { nextTick, onMounted, ref, watch } from 'vue';
 
@@ -11,7 +11,7 @@
 
 import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
 import { BizTypeEnum } from '#/api/crm/permission';
-import { getProductSimpleList } from '#/api/crm/product';
+import { getItemPage } from '#/api/mdm/item';
 import { $t } from '#/locales';
 
 import { useProductEditTableColumns } from './data';
@@ -39,34 +39,36 @@
 }
 
 /** 鍒囨崲浜у搧鏃跺悓姝ュ熀纭�淇℃伅 */
-function handleProductChange(productId: any, row: any) {
-  const product = productOptions.value.find((p) => p.id === productId);
+function handleProductChange(itemId: any, row: any) {
+  const product = productOptions.value.find((p) => p.id === itemId);
   if (!product) {
     return;
   }
-  row.productUnit = product.unit;
-  row.productNo = product.no;
-  row.productPrice = product.price;
-  row.sellingPrice = product.price;
-  row.count = 0;
-  row.totalPrice = 0;
+  row.itemId = itemId;
+  row.itemName = product.name;
+  row.itemCode = product.code;
+  row.itemBarCode = product.barCode;
+  row.itemUnitName = product.unitMeasureName;
+  row.itemUnitName2 = product.unitMeasureName2;
+  row.itemUnitName3 = product.unitMeasureName3;
+  row.itemSpecification = product.specification;
+  row.itemPrice = product.salesPrice;
+  row.contractPrice = product.salesPrice;
+  row.count = row.count || 1;
+  row.totalPrice = erpPriceMultiply(row.contractPrice, row.count) ?? 0;
   handleUpdateValue(row);
 }
 
 /** 閲戦鍙樺姩鏃堕噸鏂拌绠楀悎璁� */
 function handlePriceChange(row: any) {
-  row.totalPrice = erpPriceMultiply(row.sellingPrice, row.count) ?? 0;
+  row.totalPrice = erpPriceMultiply(row.contractPrice, row.count) ?? 0;
   handleUpdateValue(row);
 }
 
 /** 灏嗘渶鏂版暟鎹啓鍥炲苟閫氱煡鐖剁粍浠� */
 function handleUpdateValue(row: any) {
   const index = tableData.value.findIndex((item) => item.id === row.id);
-  if (props.bizType === BizTypeEnum.CRM_BUSINESS) {
-    row.businessPrice = row.sellingPrice;
-  } else if (props.bizType === BizTypeEnum.CRM_CONTRACT) {
-    row.contractPrice = row.sellingPrice;
-  }
+  row.totalPrice = erpPriceMultiply(row.contractPrice, row.count) ?? 0;
   if (index === -1) {
     row.id = tableData.value.length + 1;
     tableData.value.push(row);
@@ -110,15 +112,6 @@
     }
     await nextTick();
     tableData.value = products;
-    if (props.bizType === BizTypeEnum.CRM_BUSINESS) {
-      tableData.value.forEach((item) => {
-        item.sellingPrice = item.businessPrice;
-      });
-    } else if (props.bizType === BizTypeEnum.CRM_CONTRACT) {
-      tableData.value.forEach((item) => {
-        item.sellingPrice = item.contractPrice;
-      });
-    }
     await gridApi.grid.reloadData(tableData.value);
   },
   {
@@ -127,26 +120,27 @@
 );
 
 /** 鍒濆鍖� */
-const productOptions = ref<CrmProductApi.Product[]>([]); // 浜у搧涓嬫媺閫夐」
+const productOptions = ref<MdmItemApi.Item[]>([]); // 鐗╂枡涓嬫媺閫夐」
 onMounted(async () => {
-  productOptions.value = await getProductSimpleList();
+  const res = await getItemPage({ pageNo: 1, pageSize: 100, status: 0 });
+  productOptions.value = res.list || [];
 });
 </script>
 
 <template>
   <Grid class="w-full">
-    <template #productId="{ row }">
+    <template #itemId="{ row }">
       <Select
-        v-model:value="row.productId"
+        v-model:value="row.itemId"
         :options="productOptions"
         :field-names="{ label: 'name', value: 'id' }"
         class="w-full"
         @change="handleProductChange($event, row)"
       />
     </template>
-    <template #sellingPrice="{ row }">
+    <template #contractPrice="{ row }">
       <InputNumber
-        v-model:value="row.sellingPrice"
+        v-model:value="row.contractPrice"
         :min="0.001"
         :precision="2"
         @change="handlePriceChange(row)"
@@ -180,7 +174,7 @@
             type: 'link',
             danger: true,
             popConfirm: {
-              title: $t('ui.actionMessage.deleteConfirm', [row.name]),
+              title: $t('ui.actionMessage.deleteConfirm', [row.itemName]),
               confirm: handleDelete.bind(null, row),
             },
           },
diff --git a/src/views/crm/report/index.vue b/src/views/crm/report/index.vue
new file mode 100644
index 0000000..b66cd17
--- /dev/null
+++ b/src/views/crm/report/index.vue
@@ -0,0 +1,174 @@
+<script lang="ts" setup>
+import type { CrmReportApi } from '#/api/crm/report';
+
+import { computed, onMounted, ref } from 'vue';
+
+import { Page } from '../../../packages/effects/common-ui/src';
+
+import { Card, Col, DatePicker, Row, Statistic, Table } from 'ant-design-vue';
+import dayjs from 'dayjs';
+
+import {
+  getBusinessAnalyze,
+  getContractAnalyze,
+  getCustomerAnalyze,
+  getSalesFunnel,
+  getSalesPerformance,
+} from '#/api/crm/report';
+
+defineOptions({ name: 'CrmReportDashboard' });
+
+const loading = ref(false);
+const dateRange = ref<[string, string] | null>(null);
+
+const customerAnalyze = ref<CrmReportApi.CustomerAnalyze>();
+const businessAnalyze = ref<CrmReportApi.BusinessAnalyze>();
+const contractAnalyze = ref<CrmReportApi.ContractAnalyze>();
+const salesFunnel = ref<CrmReportApi.SalesFunnel>();
+const salesPerformance = ref<CrmReportApi.SalesPerformance>();
+
+const startTime = computed(() =>
+  dateRange.value?.[0] ? dayjs(dateRange.value[0]).format('YYYY-MM-DD') : undefined,
+);
+const endTime = computed(() =>
+  dateRange.value?.[1] ? dayjs(dateRange.value[1]).format('YYYY-MM-DD') : undefined,
+);
+
+const performanceColumns = [
+  { title: '濮撳悕', dataIndex: 'userName', width: 120 },
+  { title: '閮ㄩ棬', dataIndex: 'deptName', width: 150 },
+  { title: '鍚堝悓鏁伴噺', dataIndex: 'contractCount', width: 100 },
+  { title: '鍚堝悓閲戦', dataIndex: 'contractPrice', width: 120, formatter: 'formatAmount2' },
+  { title: '鍥炴閲戦', dataIndex: 'receivablePrice', width: 120, formatter: 'formatAmount2' },
+  { title: '鍟嗘満鏁伴噺', dataIndex: 'businessCount', width: 100 },
+  { title: '鍟嗘満閲戦', dataIndex: 'businessPrice', width: 120, formatter: 'formatAmount2' },
+];
+
+async function loadData() {
+  loading.value = true;
+  try {
+    const [customer, business, contract, funnel, performance] = await Promise.all([
+      getCustomerAnalyze(startTime.value, endTime.value),
+      getBusinessAnalyze(startTime.value, endTime.value),
+      getContractAnalyze(startTime.value, endTime.value),
+      getSalesFunnel(),
+      getSalesPerformance(startTime.value, endTime.value),
+    ]);
+    customerAnalyze.value = customer;
+    businessAnalyze.value = business;
+    contractAnalyze.value = contract;
+    salesFunnel.value = funnel;
+    salesPerformance.value = performance;
+  } finally {
+    loading.value = false;
+  }
+}
+
+function handleDateChange() {
+  loadData();
+}
+
+onMounted(() => {
+  loadData();
+});
+</script>
+
+<template>
+  <Page auto-content-height>
+    <div class="p-4">
+      <Card title="CRM 鏁版嵁姒傝" class="mb-4">
+        <div class="mb-4">
+          <DatePicker.RangePicker
+            v-model:value="dateRange"
+            :placeholder="['寮�濮嬫棩鏈�', '缁撴潫鏃ユ湡']"
+            format="YYYY-MM-DD"
+            @change="handleDateChange"
+          />
+        </div>
+      </Card>
+
+      <Row :gutter="16" class="mb-4">
+        <Col :span="6">
+          <Card title="瀹㈡埛鍒嗘瀽" :loading="loading">
+            <Statistic title="鎬诲鎴锋暟" :value="customerAnalyze?.totalCount || 0" />
+            <Statistic title="浠婃棩鏂板" :value="customerAnalyze?.todayCount || 0" class="mt-2" />
+            <Statistic title="鎴愪氦瀹㈡埛" :value="customerAnalyze?.dealCount || 0" class="mt-2" />
+            <Statistic
+              title="鎴愪氦鐜�"
+              :value="customerAnalyze?.dealRate || 0"
+              :precision="2"
+              suffix="%"
+              class="mt-2"
+            />
+          </Card>
+        </Col>
+        <Col :span="6">
+          <Card title="鍟嗘満鍒嗘瀽" :loading="loading">
+            <Statistic title="鎬诲晢鏈烘暟" :value="businessAnalyze?.totalCount || 0" />
+            <Statistic title="鍟嗘満閲戦" :value="businessAnalyze?.totalPrice || 0" :precision="2" class="mt-2" />
+            <Statistic title="鎴愬姛鍟嗘満" :value="businessAnalyze?.successCount || 0" class="mt-2" />
+            <Statistic
+              title="鎴愬姛鐜�"
+              :value="businessAnalyze?.successRate || 0"
+              :precision="2"
+              suffix="%"
+              class="mt-2"
+            />
+          </Card>
+        </Col>
+        <Col :span="6">
+          <Card title="鍚堝悓鍒嗘瀽" :loading="loading">
+            <Statistic title="鎬诲悎鍚屾暟" :value="contractAnalyze?.totalCount || 0" />
+            <Statistic title="鍚堝悓閲戦" :value="contractAnalyze?.totalPrice || 0" :precision="2" class="mt-2" />
+            <Statistic title="瀹℃壒涓�" :value="contractAnalyze?.processCount || 0" class="mt-2" />
+            <Statistic
+              title="鍥炴鐜�"
+              :value="contractAnalyze?.receivableRate || 0"
+              :precision="2"
+              suffix="%"
+              class="mt-2"
+            />
+          </Card>
+        </Col>
+        <Col :span="6">
+          <Card title="寰呭鐞�" :loading="loading">
+            <Statistic title="瀹㈡埛鍏捣" :value="customerAnalyze?.poolCount || 0" />
+            <Statistic title="閿佸畾瀹㈡埛" :value="customerAnalyze?.lockCount || 0" class="mt-2" />
+            <Statistic title="杩涜涓晢鏈�" :value="businessAnalyze?.processingCount || 0" class="mt-2" />
+            <Statistic title="澶辫触鍟嗘満" :value="businessAnalyze?.failCount || 0" class="mt-2" />
+          </Card>
+        </Col>
+      </Row>
+
+      <Row :gutter="16">
+        <Col :span="12">
+          <Card title="閿�鍞紡鏂�" :loading="loading">
+            <Table
+              :dataSource="salesFunnel?.stages || []"
+              :columns="[
+                { title: '闃舵', dataIndex: 'name', width: 150 },
+                { title: '鏁伴噺', dataIndex: 'count', width: 100 },
+                { title: '閲戦', dataIndex: 'price', width: 120 },
+                { title: '杞寲鐜�', dataIndex: 'conversionRate', width: 100, customRender: ({ text }) => `${text}%` },
+              ]"
+              :pagination="false"
+              size="small"
+              rowKey="name"
+            />
+          </Card>
+        </Col>
+        <Col :span="12">
+          <Card title="閿�鍞笟缁�" :loading="loading">
+            <Table
+              :dataSource="salesPerformance?.items || []"
+              :columns="performanceColumns"
+              :pagination="false"
+              size="small"
+              rowKey="userId"
+            />
+          </Card>
+        </Col>
+      </Row>
+    </div>
+  </Page>
+</template>
\ No newline at end of file
diff --git a/src/views/crm/saleQuotation/data.ts b/src/views/crm/saleQuotation/data.ts
new file mode 100644
index 0000000..ca86211
--- /dev/null
+++ b/src/views/crm/saleQuotation/data.ts
@@ -0,0 +1,413 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { erpPriceMultiply } from '#/packages/utils/src';
+
+import { z } from '#/adapter/form';
+import { getSimpleBusinessList } from '#/api/crm/business';
+import { getSimpleContactList } from '#/api/crm/contact';
+import { getCustomerSimpleList } from '#/api/crm/customer';
+import { getItemPage } from '#/api/mdm/item';
+import { getSimpleUserList } from '#/api/system/user';
+import { useUserStore } from '#/packages/stores/src';
+
+/** 鎶ヤ环鍗曠姸鎬� */
+export const QUOTATION_STATUS = {
+  DRAFT: 0, // 鑽夌
+  CONVERTED: 10, // 宸茶浆鍚堝悓
+};
+
+/** 鏂板/淇敼鐨勮〃鍗� */
+export function useFormSchema(): VbenFormSchema[] {
+  const userStore = useUserStore();
+  return [
+    {
+      component: 'Input',
+      fieldName: 'id',
+      dependencies: {
+        triggerFields: [''],
+        show: () => false,
+      },
+    },
+    {
+      fieldName: 'no',
+      label: '鎶ヤ环鍗曞彿',
+      component: 'Input',
+      componentProps: {
+        placeholder: '淇濆瓨鏃惰嚜鍔ㄧ敓鎴�',
+        disabled: true,
+      },
+    },
+    {
+      fieldName: 'name',
+      label: '鎶ヤ环鍗曞悕绉�',
+      component: 'Input',
+      rules: 'required',
+      componentProps: {
+        placeholder: '璇疯緭鍏ユ姤浠峰崟鍚嶇О',
+      },
+    },
+    {
+      fieldName: 'ownerUserId',
+      label: '璐熻矗浜�',
+      component: 'ApiSelect',
+      componentProps: {
+        api: getSimpleUserList,
+        labelField: 'nickname',
+        valueField: 'id',
+      },
+      defaultValue: userStore.userInfo?.id,
+      rules: 'required',
+    },
+    {
+      fieldName: 'customerId',
+      label: '瀹㈡埛鍚嶇О',
+      component: 'ApiSelect',
+      rules: 'required',
+      componentProps: {
+        api: getCustomerSimpleList,
+        labelField: 'name',
+        valueField: 'id',
+        placeholder: '璇烽�夋嫨瀹㈡埛',
+      },
+    },
+    {
+      fieldName: 'businessId',
+      label: '鍟嗘満鍚嶇О',
+      component: 'Select',
+      componentProps: {
+        options: [],
+        placeholder: '璇烽�夋嫨鍟嗘満',
+        allowClear: true,
+      },
+      dependencies: {
+        triggerFields: ['customerId'],
+        disabled: (values) => !values.customerId,
+        async componentProps(values) {
+          if (!values.customerId) {
+            return { options: [], placeholder: '璇烽�夋嫨瀹㈡埛' };
+          }
+          const res = await getSimpleBusinessList();
+          const list = res.filter((item) => item.customerId === values.customerId);
+          return {
+            options: list.map((item) => ({ label: item.name, value: item.id })),
+            placeholder: '璇烽�夋嫨鍟嗘満',
+          };
+        },
+      },
+    },
+    {
+      fieldName: 'contactId',
+      label: '鑱旂郴浜�',
+      component: 'Select',
+      componentProps: {
+        options: [],
+        placeholder: '璇烽�夋嫨鑱旂郴浜�',
+        allowClear: true,
+      },
+      dependencies: {
+        triggerFields: ['customerId'],
+        disabled: (values) => !values.customerId,
+        async componentProps(values) {
+          if (!values.customerId) {
+            return { options: [], placeholder: '璇烽�夋嫨瀹㈡埛' };
+          }
+          const res = await getSimpleContactList();
+          const list = res.filter((item) => item.customerId === values.customerId);
+          return {
+            options: list.map((item) => ({ label: item.name, value: item.id })),
+            placeholder: '璇烽�夋嫨鑱旂郴浜�',
+          };
+        },
+      },
+    },
+    {
+      fieldName: 'quotationTime',
+      label: '鎶ヤ环鏃ユ湡',
+      component: 'DatePicker',
+      componentProps: {
+        showTime: false,
+        format: 'YYYY-MM-DD',
+        valueFormat: 'x',
+        placeholder: '璇烽�夋嫨鎶ヤ环鏃ユ湡',
+      },
+    },
+    {
+      fieldName: 'validUntil',
+      label: '鏈夋晥鏈熻嚦',
+      component: 'DatePicker',
+      componentProps: {
+        showTime: false,
+        format: 'YYYY-MM-DD',
+        valueFormat: 'x',
+        placeholder: '璇烽�夋嫨鏈夋晥鏈�',
+      },
+    },
+    {
+      fieldName: 'taxRate',
+      label: '绋庣巼(%)',
+      component: 'InputNumber',
+      componentProps: {
+        class: '!w-full',
+        min: 0,
+        max: 100,
+        precision: 2,
+        placeholder: '璇疯緭鍏ョ◣鐜�',
+      },
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ娉�',
+        rows: 3,
+      },
+    },
+    {
+      fieldName: 'items',
+      label: '鐗╂枡娓呭崟',
+      component: 'Input',
+      formItemClass: 'col-span-3',
+    },
+    {
+      fieldName: 'totalProductPrice',
+      label: '鐗╂枡鎬婚噾棰�',
+      component: 'InputNumber',
+      componentProps: {
+        class: '!w-full',
+        min: 0,
+        precision: 2,
+        disabled: true,
+      },
+      rules: z.number().min(0).optional().default(0),
+    },
+    {
+      fieldName: 'discountPercent',
+      label: '鏁村崟鎶樻墸锛�%锛�',
+      component: 'InputNumber',
+      componentProps: {
+        class: '!w-full',
+        min: 0,
+        precision: 2,
+        placeholder: '璇疯緭鍏ユ姌鎵�',
+      },
+      rules: z.number().min(0).max(100).optional().default(0),
+    },
+    {
+      fieldName: 'totalPrice',
+      label: '鎶樻墸鍚庨噾棰�',
+      component: 'InputNumber',
+      componentProps: {
+        class: '!w-full',
+        min: 0,
+        precision: 2,
+        disabled: true,
+      },
+      dependencies: {
+        triggerFields: ['totalProductPrice', 'discountPercent'],
+        trigger(values, form) {
+          const discountPrice =
+            erpPriceMultiply(values.totalProductPrice, values.discountPercent / 100) ?? 0;
+          form.setFieldValue('totalPrice', values.totalProductPrice - discountPrice);
+        },
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'no',
+      label: '鎶ヤ环鍗曞彿',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ユ姤浠峰崟鍙�',
+        allowClear: true,
+      },
+    },
+    {
+      fieldName: 'name',
+      label: '鎶ヤ环鍗曞悕绉�',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ユ姤浠峰崟鍚嶇О',
+        allowClear: true,
+      },
+    },
+    {
+      fieldName: 'customerId',
+      label: '瀹㈡埛',
+      component: 'ApiSelect',
+      componentProps: {
+        api: getCustomerSimpleList,
+        labelField: 'name',
+        valueField: 'id',
+        placeholder: '璇烽�夋嫨瀹㈡埛',
+        allowClear: true,
+      },
+    },
+    {
+      fieldName: 'status',
+      label: '鐘舵��',
+      component: 'Select',
+      componentProps: {
+        placeholder: '璇烽�夋嫨鐘舵��',
+        allowClear: true,
+        options: [
+          { label: '鑽夌', value: 0 },
+          { label: '宸茶浆鍚堝悓', value: 10 },
+        ],
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+  return [
+    {
+      type: 'checkbox',
+      width: 50,
+      fixed: 'left',
+    },
+    {
+      field: 'no',
+      title: '鎶ヤ环鍗曞彿',
+      minWidth: 180,
+      fixed: 'left',
+    },
+    {
+      field: 'name',
+      title: '鎶ヤ环鍗曞悕绉�',
+      minWidth: 160,
+      fixed: 'left',
+    },
+    {
+      field: 'customerName',
+      title: '瀹㈡埛鍚嶇О',
+      minWidth: 120,
+    },
+    {
+      field: 'businessName',
+      title: '鍟嗘満鍚嶇О',
+      minWidth: 130,
+    },
+    {
+      field: 'totalPrice',
+      title: '鎶ヤ环閲戦锛堝厓锛�',
+      minWidth: 140,
+      formatter: 'formatAmount2',
+    },
+    {
+      field: 'quotationTime',
+      title: '鎶ヤ环鏃ユ湡',
+      minWidth: 120,
+      formatter: 'formatDateTime',
+    },
+    {
+      field: 'validUntil',
+      title: '鏈夋晥鏈熻嚦',
+      minWidth: 120,
+      formatter: 'formatDateTime',
+    },
+    {
+      field: 'status',
+      title: '鐘舵��',
+      minWidth: 100,
+      slots: { default: 'status' },
+    },
+    {
+      field: 'contractNo',
+      title: '鍏宠仈鍚堝悓',
+      minWidth: 150,
+    },
+    {
+      field: 'ownerUserName',
+      title: '璐熻矗浜�',
+      minWidth: 120,
+    },
+    {
+      field: 'createTime',
+      title: '鍒涘缓鏃堕棿',
+      minWidth: 180,
+      formatter: 'formatDateTime',
+    },
+    {
+      title: '鎿嶄綔',
+      width: 260,
+      fixed: 'right',
+      slots: { default: 'actions' },
+    },
+  ];
+}
+
+/** 鐗╂枡鏄庣粏琛ㄦ牸鍒� */
+export function useItemColumns(): VxeTableGridOptions['columns'] {
+  return [
+    { type: 'seq', title: '搴忓彿', minWidth: 50, fixed: 'left' },
+    {
+      field: 'itemId',
+      title: '鐗╂枡鍚嶇О',
+      minWidth: 200,
+      slots: { default: 'itemId' },
+    },
+    {
+      field: 'itemSpecification',
+      title: '瑙勬牸鍨嬪彿',
+      minWidth: 120,
+    },
+    {
+      field: 'itemBarCode',
+      title: '鏉$爜',
+      minWidth: 120,
+    },
+    {
+      field: 'itemUnitName',
+      title: '鍗曚綅',
+      minWidth: 80,
+    },
+    {
+      field: 'itemUnitName2',
+      title: '杈呭崟浣�1',
+      minWidth: 80,
+    },
+    {
+      field: 'itemUnitName3',
+      title: '杈呭崟浣�2',
+      minWidth: 80,
+    },
+    {
+      field: 'itemPrice',
+      title: '鍘熶环锛堝厓锛�',
+      minWidth: 100,
+      formatter: 'formatAmount2',
+    },
+    {
+      field: 'quotationPrice',
+      title: '鎶ヤ环锛堝厓锛�',
+      minWidth: 100,
+      slots: { default: 'quotationPrice' },
+    },
+    {
+      field: 'count',
+      title: '鏁伴噺',
+      minWidth: 100,
+      slots: { default: 'count' },
+    },
+    {
+      field: 'totalPrice',
+      title: '鍚堣',
+      minWidth: 100,
+      formatter: 'formatAmount2',
+    },
+    {
+      title: '鎿嶄綔',
+      width: 50,
+      fixed: 'right',
+      slots: { default: 'actions' },
+    },
+  ];
+}
\ No newline at end of file
diff --git a/src/views/crm/saleQuotation/index.vue b/src/views/crm/saleQuotation/index.vue
new file mode 100644
index 0000000..abd5c66
--- /dev/null
+++ b/src/views/crm/saleQuotation/index.vue
@@ -0,0 +1,210 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { CrmSaleQuotationApi } from '#/api/crm/saleQuotation';
+
+import { useRouter } from 'vue-router';
+
+import { Page, useVbenModal } from '#/packages/effects/common-ui/src';
+
+import { Button, message, Modal, Tag } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+  convertSaleQuotationToContract,
+  deleteSaleQuotation,
+  getSaleQuotationPage,
+} from '#/api/crm/saleQuotation';
+import { $t } from '#/locales';
+
+import { QUOTATION_STATUS, useGridColumns, useGridFormSchema } from './data';
+import Form from './modules/form.vue';
+
+const { push } = useRouter();
+
+const [FormModal, formModalApi] = useVbenModal({
+  connectedComponent: Form,
+  destroyOnClose: true,
+});
+
+/** 鍒锋柊琛ㄦ牸 */
+function handleRefresh() {
+  gridApi.query();
+}
+
+/** 鍒涘缓鎶ヤ环鍗� */
+function handleCreate() {
+  formModalApi.setData(null).open();
+}
+
+/** 缂栬緫鎶ヤ环鍗� */
+function handleEdit(row: CrmSaleQuotationApi.SaleQuotation) {
+  formModalApi.setData(row).open();
+}
+
+/** 鍒犻櫎鎶ヤ环鍗� */
+async function handleDelete(row: CrmSaleQuotationApi.SaleQuotation) {
+  const hideLoading = message.loading({
+    content: $t('ui.actionMessage.deleting', [row.name]),
+    duration: 0,
+  });
+  try {
+    await deleteSaleQuotation(row.id!);
+    message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
+    handleRefresh();
+  } finally {
+    hideLoading();
+  }
+}
+
+/** 鏌ョ湅鎶ヤ环鍗曡鎯� */
+function handleDetail(row: CrmSaleQuotationApi.SaleQuotation) {
+  push({ name: 'CrmSaleQuotationDetail', params: { id: row.id } });
+}
+
+/** 鏌ョ湅瀹㈡埛璇︽儏 */
+function handleCustomerDetail(row: CrmSaleQuotationApi.SaleQuotation) {
+  push({ name: 'CrmCustomerDetail', params: { id: row.customerId } });
+}
+
+/** 鏌ョ湅鍟嗘満璇︽儏 */
+function handleBusinessDetail(row: CrmSaleQuotationApi.SaleQuotation) {
+  if (row.businessId) {
+    push({ name: 'CrmBusinessDetail', params: { id: row.businessId } });
+  }
+}
+
+/** 鏌ョ湅鍚堝悓璇︽儏 */
+function handleContractDetail(row: CrmSaleQuotationApi.SaleQuotation) {
+  if (row.contractId) {
+    push({ name: 'CrmContractDetail', params: { id: row.contractId } });
+  }
+}
+
+/** 杞负鍚堝悓 */
+async function handleConvertContract(row: CrmSaleQuotationApi.SaleQuotation) {
+  Modal.confirm({
+    title: '纭杞悎鍚�',
+    content: '纭灏嗚鎶ヤ环鍗曡浆涓哄悎鍚岋紵',
+    async onOk() {
+      const contractId = await convertSaleQuotationToContract(row.id!);
+      message.success('杞悎鍚屾垚鍔�');
+      push({ name: 'CrmContractDetail', params: { id: contractId } });
+    },
+  });
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  formOptions: {
+    schema: useGridFormSchema(),
+  },
+  gridOptions: {
+    columns: useGridColumns(),
+    height: 'auto',
+    keepSource: true,
+    proxyConfig: {
+      ajax: {
+        query: async ({ page }, formValues) => {
+          return await getSaleQuotationPage({
+            pageNo: page.currentPage,
+            pageSize: page.pageSize,
+            ...formValues,
+          });
+        },
+      },
+    },
+    rowConfig: {
+      keyField: 'id',
+      isHover: true,
+    },
+    toolbarConfig: {
+      refresh: true,
+      search: true,
+    },
+  } as VxeTableGridOptions<CrmSaleQuotationApi.SaleQuotation>,
+});
+</script>
+
+<template>
+  <Page auto-content-height>
+    <FormModal @success="handleRefresh" />
+    <Grid>
+      <template #toolbar-tools>
+        <TableAction
+          :actions="[
+            {
+              label: $t('ui.actionTitle.create', ['鎶ヤ环鍗�']),
+              type: 'primary',
+              icon: ACTION_ICON.ADD,
+              auth: ['crm:sale-quotation:create'],
+              onClick: handleCreate,
+            },
+          ]"
+        />
+      </template>
+      <template #name="{ row }">
+        <Button type="link" @click="handleDetail(row)">
+          {{ row.name }}
+        </Button>
+      </template>
+      <template #customerName="{ row }">
+        <Button type="link" @click="handleCustomerDetail(row)">
+          {{ row.customerName }}
+        </Button>
+      </template>
+      <template #businessName="{ row }">
+        <Button v-if="row.businessId" type="link" @click="handleBusinessDetail(row)">
+          {{ row.businessName }}
+        </Button>
+        <span v-else>-</span>
+      </template>
+      <template #contractNo="{ row }">
+        <Button v-if="row.contractId" type="link" @click="handleContractDetail(row)">
+          {{ row.contractNo }}
+        </Button>
+        <span v-else>-</span>
+      </template>
+      <template #status="{ row }">
+        <Tag v-if="row.status === QUOTATION_STATUS.DRAFT" color="default">鑽夌</Tag>
+        <Tag v-if="row.status === QUOTATION_STATUS.CONVERTED" color="success">宸茶浆鍚堝悓</Tag>
+      </template>
+      <template #actions="{ row }">
+        <TableAction
+          :actions="[
+            {
+              label: $t('common.edit'),
+              type: 'link',
+              icon: ACTION_ICON.EDIT,
+              auth: ['crm:sale-quotation:update'],
+              onClick: handleEdit.bind(null, row),
+              ifShow: row.status === QUOTATION_STATUS.DRAFT,
+            },
+            {
+              label: '杞悎鍚�',
+              type: 'link',
+              auth: ['crm:sale-quotation:update'],
+              onClick: handleConvertContract.bind(null, row),
+              ifShow: row.status === QUOTATION_STATUS.DRAFT && !row.contractId,
+            },
+            {
+              label: '鏌ョ湅鍚堝悓',
+              type: 'link',
+              onClick: handleContractDetail.bind(null, row),
+              ifShow: !!row.contractId,
+            },
+            {
+              label: $t('common.delete'),
+              type: 'link',
+              danger: true,
+              auth: ['crm:sale-quotation:delete'],
+              popConfirm: {
+                title: $t('ui.actionMessage.deleteConfirm', [row.name]),
+                confirm: handleDelete.bind(null, row),
+              },
+              ifShow: row.status === QUOTATION_STATUS.DRAFT,
+            },
+          ]"
+        />
+      </template>
+    </Grid>
+  </Page>
+</template>
\ No newline at end of file
diff --git a/src/views/crm/saleQuotation/modules/form.vue b/src/views/crm/saleQuotation/modules/form.vue
new file mode 100644
index 0000000..8fa938f
--- /dev/null
+++ b/src/views/crm/saleQuotation/modules/form.vue
@@ -0,0 +1,124 @@
+<script lang="ts" setup>
+import type { CrmSaleQuotationApi } from '#/api/crm/saleQuotation';
+
+import { computed, ref, watch } from 'vue';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+import { erpPriceMultiply } from '#/packages/utils/src';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+  createSaleQuotation,
+  getSaleQuotation,
+  updateSaleQuotation,
+} from '#/api/crm/saleQuotation';
+import { $t } from '#/locales';
+
+import ItemForm from './item-form.vue';
+import { useFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+const formData = ref<CrmSaleQuotationApi.SaleQuotation>();
+const itemFormRef = ref();
+
+const getTitle = computed(() => {
+  return formData.value?.id
+    ? $t('ui.actionTitle.edit', ['鎶ヤ环鍗�'])
+    : $t('ui.actionTitle.create', ['鎶ヤ环鍗�']);
+});
+
+const isEdit = computed(() => !!formData.value?.id);
+const isDisabled = computed(() => isEdit.value && formData.value?.status !== 0);
+
+const [Form, formApi] = useVbenForm({
+  commonConfig: {
+    componentProps: {
+      class: 'w-full',
+    },
+    labelWidth: 120,
+  },
+  wrapperClass: 'grid-cols-3',
+  layout: 'vertical',
+  schema: useFormSchema(),
+  showDefaultActions: false,
+});
+
+/** 璁$畻鐗╂枡鎬婚噾棰� */
+async function calculateTotalPrice() {
+  const items = itemFormRef.value?.getData() || [];
+  const totalProductPrice = items.reduce(
+    (prev: number, curr: any) => prev + (curr.totalPrice || 0),
+    0,
+  );
+  await formApi.setFieldValue('totalProductPrice', totalProductPrice);
+  const discountPercent = (await formApi.getValues())?.discountPercent || 0;
+  const discountPrice = erpPriceMultiply(totalProductPrice, discountPercent / 100) ?? 0;
+  await formApi.setFieldValue('totalPrice', totalProductPrice - discountPrice);
+}
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    const { valid } = await formApi.validate();
+    if (!valid) {
+      return;
+    }
+    try {
+      await itemFormRef.value?.validate();
+    } catch (e: any) {
+      message.error(e.message);
+      return;
+    }
+    modalApi.lock();
+    const data = (await formApi.getValues()) as CrmSaleQuotationApi.SaleQuotation;
+    data.items = itemFormRef.value?.getData();
+    try {
+      await (formData.value?.id ? updateSaleQuotation(data) : createSaleQuotation(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<CrmSaleQuotationApi.SaleQuotation>();
+    if (!data || !data.id) {
+      formData.value = {} as CrmSaleQuotationApi.SaleQuotation;
+      await formApi.resetForm();
+      // 鏂板鏃讹紝閲嶇疆骞舵坊鍔犱竴琛岀┖鏁版嵁
+      itemFormRef.value?.resetData();
+      return;
+    }
+    modalApi.lock();
+    try {
+      formData.value = await getSaleQuotation(data.id);
+      await formApi.setValues(formData.value);
+      // 缂栬緫鏃惰缃墿鏂欐暟鎹�
+      itemFormRef.value?.setData(formData.value.items || []);
+    } finally {
+      modalApi.unlock();
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal :title="getTitle" class="w-[80%]">
+    <Form class="mx-4">
+      <template #items>
+        <ItemForm
+          ref="itemFormRef"
+          class="w-full"
+          :disabled="isDisabled"
+          @change="calculateTotalPrice"
+        />
+      </template>
+    </Form>
+  </Modal>
+</template>
\ No newline at end of file
diff --git a/src/views/crm/saleQuotation/modules/item-form.vue b/src/views/crm/saleQuotation/modules/item-form.vue
new file mode 100644
index 0000000..e3aacab
--- /dev/null
+++ b/src/views/crm/saleQuotation/modules/item-form.vue
@@ -0,0 +1,258 @@
+<script lang="ts" setup>
+import type { MdmItemApi } from '#/api/mdm/item';
+import type { CrmSaleQuotationApi } from '#/api/crm/saleQuotation';
+
+import { computed, onMounted, ref } from 'vue';
+
+import { erpPriceMultiply } from '#/packages/utils/src';
+
+import { InputNumber, Select } from 'ant-design-vue';
+
+import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import { getItemPage } from '#/api/mdm/item';
+
+import { useItemColumns } from '../data';
+
+interface Props {
+  disabled?: boolean;
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  disabled: false,
+});
+
+const emit = defineEmits(['change']);
+
+const tableData = ref<CrmSaleQuotationApi.SaleQuotationItem[]>([]);
+const itemOptions = ref<MdmItemApi.Item[]>([]);
+
+/** 鑾峰彇琛ㄦ牸鍚堣鏁版嵁 */
+const summaries = computed(() => {
+  return tableData.value.reduce(
+    (acc, item) => {
+      acc.count += item.count || 0;
+      acc.totalPrice += item.totalPrice || 0;
+      return acc;
+    },
+    { count: 0, totalPrice: 0 },
+  );
+});
+
+/** 琛ㄦ牸閰嶇疆 */
+const [Grid, gridApi] = useVbenVxeGrid({
+  gridOptions: {
+    columns: useItemColumns(),
+    data: [],
+    minHeight: 250,
+    autoResize: true,
+    border: true,
+    rowConfig: {
+      keyField: 'seq',
+      isHover: true,
+    },
+    pagerConfig: {
+      enabled: false,
+    },
+    toolbarConfig: {
+      enabled: false,
+    },
+  },
+});
+
+/** 瑙﹀彂鍙樻洿浜嬩欢 */
+function triggerChange() {
+  emit('change');
+}
+
+/** 澶勭悊鏂板 */
+function handleAdd() {
+  tableData.value.push({
+    itemId: undefined,
+    itemName: undefined,
+    itemCode: undefined,
+    itemBarCode: undefined,
+    itemSpecification: undefined,
+    itemUnitName: undefined,
+    itemUnitName2: undefined,
+    itemUnitName3: undefined,
+    itemPrice: undefined,
+    quotationPrice: undefined,
+    count: 1,
+    totalPrice: 0,
+  });
+  gridApi.grid.loadData(tableData.value);
+}
+
+/** 澶勭悊鍒犻櫎 */
+function handleDelete(row: CrmSaleQuotationApi.SaleQuotationItem) {
+  const index = tableData.value.findIndex((item) => item.seq === row.seq);
+  if (index !== -1) {
+    tableData.value.splice(index, 1);
+    gridApi.grid.loadData(tableData.value);
+    triggerChange();
+  }
+}
+
+/** 澶勭悊鐗╂枡鍙樻洿 */
+function handleItemChange(itemId: number, row: any) {
+  const item = itemOptions.value.find((p) => p.id === itemId);
+  if (!item) {
+    return;
+  }
+  row.itemId = itemId;
+  row.itemName = item.name;
+  row.itemCode = item.code;
+  row.itemBarCode = item.barCode;
+  row.itemSpecification = item.specification;
+  row.itemUnitName = item.unitMeasureName;
+  row.itemUnitName2 = item.unitMeasureName2;
+  row.itemUnitName3 = item.unitMeasureName3;
+  row.itemPrice = item.salesPrice;
+  row.quotationPrice = item.salesPrice;
+  row.totalPrice = erpPriceMultiply(row.quotationPrice, row.count) ?? 0;
+  triggerChange();
+}
+
+/** 澶勭悊鎶ヤ环鍙樻洿 */
+function handleQuotationPriceChange(value: number, row: any) {
+  row.quotationPrice = value;
+  row.totalPrice = erpPriceMultiply(value, row.count) ?? 0;
+  triggerChange();
+}
+
+/** 澶勭悊鏁伴噺鍙樻洿 */
+function handleCountChange(value: number, row: any) {
+  row.count = value;
+  row.totalPrice = erpPriceMultiply(row.quotationPrice, value) ?? 0;
+  triggerChange();
+}
+
+/** 琛ㄥ崟鏍¢獙 */
+function validate() {
+  for (let i = 0; i < tableData.value.length; i++) {
+    const item = tableData.value[i];
+    if (item) {
+      if (!item.itemId) {
+        throw new Error(`绗� ${i + 1} 琛岋細鐗╂枡涓嶈兘涓虹┖`);
+      }
+      if (!item.count || item.count <= 0) {
+        throw new Error(`绗� ${i + 1} 琛岋細鏁伴噺涓嶈兘涓虹┖`);
+      }
+      if (!item.quotationPrice || item.quotationPrice <= 0) {
+        throw new Error(`绗� ${i + 1} 琛岋細鎶ヤ环涓嶈兘涓虹┖`);
+      }
+    }
+  }
+}
+
+/** 璁剧疆鏁版嵁锛堜緵鐖剁粍浠惰皟鐢級 */
+function setData(items: CrmSaleQuotationApi.SaleQuotationItem[]) {
+  items.forEach((item) => {
+    if (item.quotationPrice && item.count) {
+      item.totalPrice = erpPriceMultiply(item.quotationPrice, item.count) ?? 0;
+    }
+  });
+  tableData.value = [...items];
+  gridApi.grid.loadData(tableData.value);
+}
+
+/** 鑾峰彇鏁版嵁 */
+function getData() {
+  return [...tableData.value];
+}
+
+/** 閲嶇疆鏁版嵁 */
+function resetData() {
+  tableData.value = [];
+  handleAdd();
+}
+
+defineExpose({
+  validate,
+  setData,
+  getData,
+  resetData,
+});
+
+/** 鍒濆鍖� */
+onMounted(async () => {
+  const res = await getItemPage({ pageNo: 1, pageSize: 100, status: 0 });
+  itemOptions.value = res.list || [];
+});
+</script>
+
+<template>
+  <Grid class="w-full">
+    <template #itemId="{ row }">
+      <Select
+        v-if="!disabled"
+        v-model:value="row.itemId"
+        :options="itemOptions"
+        :field-names="{ label: 'name', value: 'id' }"
+        class="w-full"
+        placeholder="璇烽�夋嫨鐗╂枡"
+        @change="(val) => handleItemChange(val, row)"
+      />
+      <span v-else>{{ row.itemName || '-' }}</span>
+    </template>
+    <template #quotationPrice="{ row }">
+      <InputNumber
+        v-if="!disabled"
+        v-model:value="row.quotationPrice"
+        :min="0"
+        :precision="2"
+        @change="(val) => handleQuotationPriceChange(val, row)"
+      />
+      <span v-else>{{ row.quotationPrice || '-' }}</span>
+    </template>
+    <template #count="{ row }">
+      <InputNumber
+        v-if="!disabled"
+        v-model:value="row.count"
+        :min="0.001"
+        :precision="3"
+        @change="(val) => handleCountChange(val, row)"
+      />
+      <span v-else>{{ row.count || '-' }}</span>
+    </template>
+    <template #actions="{ row }">
+      <TableAction
+        v-if="!disabled"
+        :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>鏁伴噺锛歿{ summaries.count }}</span>
+            <span>閲戦锛歿{ summaries.totalPrice.toFixed(2) }}鍏�</span>
+          </div>
+        </div>
+      </div>
+      <TableAction
+        v-if="!disabled"
+        class="mt-2 flex justify-center"
+        :actions="[
+          {
+            label: '娣诲姞鐗╂枡',
+            type: 'default',
+            onClick: handleAdd,
+          },
+        ]"
+      />
+    </template>
+  </Grid>
+</template>
diff --git a/src/views/erp/sale/order/data.ts b/src/views/erp/sale/order/data.ts
index 8f0cdcc..3d654e2 100644
--- a/src/views/erp/sale/order/data.ts
+++ b/src/views/erp/sale/order/data.ts
@@ -7,8 +7,8 @@
 
 import { z } from '#/adapter/form';
 import { getAccountSimpleList } from '#/api/erp/finance/account';
-import { getProductSimpleList } from '#/api/erp/product/product';
-import { getCustomerSimpleList } from '#/api/erp/sale/customer';
+import { getItemPage } from '#/api/mdm/item';
+import { getCustomerSimpleList as getCrmCustomerSimpleList } from '#/api/crm/customer';
 import { getSimpleUserList } from '#/api/system/user';
 import { getRangePickerDefaultProps } from '#/utils';
 
@@ -55,7 +55,7 @@
         placeholder: '璇烽�夋嫨瀹㈡埛',
         allowClear: true,
         showSearch: true,
-        api: getCustomerSimpleList,
+        api: getCrmCustomerSimpleList,
         labelField: 'name',
         valueField: 'id',
       },
@@ -192,6 +192,11 @@
       slots: { default: 'productId' },
     },
     {
+      field: 'productSpecification',
+      title: '瑙勬牸鍨嬪彿',
+      minWidth: 120,
+    },
+    {
       field: 'stockCount',
       title: '搴撳瓨',
       minWidth: 80,
@@ -205,6 +210,16 @@
     {
       field: 'productUnitName',
       title: '鍗曚綅',
+      minWidth: 80,
+    },
+    {
+      field: 'productUnitName2',
+      title: '杈呭崟浣�1',
+      minWidth: 80,
+    },
+    {
+      field: 'productUnitName3',
+      title: '杈呭崟浣�2',
       minWidth: 80,
     },
     {
@@ -285,7 +300,10 @@
         placeholder: '璇烽�夋嫨浜у搧',
         allowClear: true,
         showSearch: true,
-        api: getProductSimpleList,
+        api: async () => {
+          const res = await getItemPage({ pageNo: 1, pageSize: 100, status: 0 });
+          return res.list || [];
+        },
         labelField: 'name',
         valueField: 'id',
       },
@@ -307,7 +325,7 @@
         placeholder: '璇烽�夋嫨瀹㈡埛',
         allowClear: true,
         showSearch: true,
-        api: getCustomerSimpleList,
+        api: getCrmCustomerSimpleList,
         labelField: 'name',
         valueField: 'id',
       },
@@ -401,6 +419,12 @@
       minWidth: 120,
     },
     {
+      field: 'contractNo',
+      title: '鍏宠仈鍚堝悓',
+      minWidth: 150,
+      slots: { default: 'contractNo' },
+    },
+    {
       field: 'orderTime',
       title: '璁㈠崟鏃堕棿',
       width: 160,
diff --git a/src/views/erp/sale/order/index.vue b/src/views/erp/sale/order/index.vue
index 8c663a6..d72d704 100644
--- a/src/views/erp/sale/order/index.vue
+++ b/src/views/erp/sale/order/index.vue
@@ -3,6 +3,7 @@
 import type { ErpSaleOrderApi } from '#/api/erp/sale/order';
 
 import { ref } from 'vue';
+import { useRouter } from 'vue-router';
 
 import { Page, useVbenModal } from '../../../../packages/effects/common-ui/src';
 import { downloadFileFromBlobPart, isEmpty } from '../../../../packages/utils/src';
@@ -23,6 +24,8 @@
 
 /** ERP 閿�鍞鍗曞垪琛� */
 defineOptions({ name: 'ErpSaleOrder' });
+
+const router = useRouter();
 
 const [FormModal, formModalApi] = useVbenModal({
   connectedComponent: Form,
@@ -97,6 +100,16 @@
   formModalApi.setData({ formType: 'detail', id: row.id }).open();
 }
 
+/** 鏌ョ湅鍏宠仈鍚堝悓 */
+function handleViewContract(row: ErpSaleOrderApi.SaleOrder) {
+  if (row.contractId) {
+    router.push({
+      path: '/crm/contract/detail',
+      query: { id: row.contractId },
+    });
+  }
+}
+
 const [Grid, gridApi] = useVbenVxeGrid({
   formOptions: {
     schema: useGridFormSchema(),
@@ -167,6 +180,12 @@
           ]"
         />
       </template>
+      <template #contractNo="{ row }">
+        <a v-if="row.contractId" class="text-primary cursor-pointer" @click="handleViewContract(row)">
+          {{ row.contractNo }}
+        </a>
+        <span v-else>-</span>
+      </template>
       <template #actions="{ row }">
         <TableAction
           :actions="[
diff --git a/src/views/erp/sale/order/modules/item-form.vue b/src/views/erp/sale/order/modules/item-form.vue
index 3c42fec..20389e6 100644
--- a/src/views/erp/sale/order/modules/item-form.vue
+++ b/src/views/erp/sale/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 { ErpSaleOrderApi } from '#/api/erp/sale/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<ErpSaleOrderApi.SaleOrderItem[]>([]); // 琛ㄦ牸鏁版嵁
-const productOptions = ref<ErpProductApi.Product[]>([]); // 浜у搧涓嬫媺閫夐」
+const productOptions = ref<MdmItemApi.Item[]>([]); // 鐗╂枡涓嬫媺閫夐」
 
 /** 鑾峰彇琛ㄦ牸鍚堣鏁版嵁 */
 const summaries = computed(() => {
@@ -158,12 +158,15 @@
     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.productUnitName2 = product.unitMeasureName2;
+  row.productUnitName3 = product.unitMeasureName3;
+  row.productSpecification = product.specification;
   row.productName = product.name;
   row.stockCount = (await getStockCount(productId)) || 0;
-  row.productPrice = product.salePrice || 0;
+  row.productPrice = product.salesPrice || 0;
   row.count = row.count || 1;
   handleRowChange(row);
 }
@@ -213,7 +216,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();
diff --git a/src/views/erp/sale/out/data.ts b/src/views/erp/sale/out/data.ts
index ffef251..82dcfe5 100644
--- a/src/views/erp/sale/out/data.ts
+++ b/src/views/erp/sale/out/data.ts
@@ -7,8 +7,8 @@
 
 import { z } from '#/adapter/form';
 import { getAccountSimpleList } from '#/api/erp/finance/account';
-import { getProductSimpleList } from '#/api/erp/product/product';
-import { getCustomerSimpleList } from '#/api/erp/sale/customer';
+import { getItemPage } from '#/api/mdm/item';
+import { getCustomerSimpleList as getCrmCustomerSimpleList } from '#/api/crm/customer';
 import { getWarehouseSimpleList } from '#/api/erp/stock/warehouse';
 import { getSimpleUserList } from '#/api/system/user';
 import { getRangePickerDefaultProps } from '#/utils';
@@ -69,7 +69,7 @@
         placeholder: '璇烽�夋嫨瀹㈡埛',
         allowClear: true,
         showSearch: true,
-        api: getCustomerSimpleList,
+        api: getCrmCustomerSimpleList,
         fieldNames: {
           label: 'name',
           value: 'id',
@@ -350,11 +350,12 @@
         placeholder: '璇烽�夋嫨浜у搧',
         allowClear: true,
         showSearch: true,
-        api: getProductSimpleList,
-        fieldNames: {
-          label: 'name',
-          value: 'id',
+        api: async () => {
+          const res = await getItemPage({ pageNo: 1, pageSize: 100, status: 0 });
+          return res.list || [];
         },
+        labelField: 'name',
+        valueField: 'id',
       },
     },
     {
@@ -374,7 +375,7 @@
         placeholder: '璇烽�夋嫨瀹㈡埛',
         allowClear: true,
         showSearch: true,
-        api: getCustomerSimpleList,
+        api: getCrmCustomerSimpleList,
         fieldNames: {
           label: 'name',
           value: 'id',
@@ -554,7 +555,10 @@
         placeholder: '璇烽�夋嫨浜у搧',
         allowClear: true,
         showSearch: true,
-        api: getProductSimpleList,
+        api: async () => {
+          const res = await getItemPage({ pageNo: 1, pageSize: 100, status: 0 });
+          return res.list || [];
+        },
         labelField: 'name',
         valueField: 'id',
       },
@@ -572,6 +576,8 @@
 }
 
 /** 鍒楄〃鐨勫瓧娈� */
+
+/** 鍒楄〃鐨勫瓧娈� */
 export function useOrderGridColumns(): VxeTableGridOptions['columns'] {
   return [
     {
diff --git a/src/views/erp/sale/out/modules/item-form.vue b/src/views/erp/sale/out/modules/item-form.vue
index 6134f18..f7fb76c 100644
--- a/src/views/erp/sale/out/modules/item-form.vue
+++ b/src/views/erp/sale/out/modules/item-form.vue
@@ -1,4 +1,5 @@
 <script lang="ts" setup>
+import type { MdmItemApi } from '#/api/mdm/item';
 import type { ErpSaleOutApi } from '#/api/erp/sale/out';
 
 import { computed, nextTick, onMounted, ref, watch } from 'vue';
@@ -12,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 { getWarehouseStockCount } from '#/api/erp/stock/stock';
 import { getWarehouseSimpleList } from '#/api/erp/stock/warehouse';
 
@@ -40,7 +41,7 @@
 ]);
 
 const tableData = ref<ErpSaleOutApi.SaleOutItem[]>([]); // 琛ㄦ牸鏁版嵁
-const productOptions = ref<any[]>([]); // 浜у搧涓嬫媺閫夐」
+const productOptions = ref<MdmItemApi.Item[]>([]); // 鐗╂枡涓嬫媺閫夐」
 const warehouseOptions = ref<any[]>([]); // 浠撳簱涓嬫媺閫夐」
 
 /** 鑾峰彇琛ㄦ牸鍚堣鏁版嵁 */
@@ -192,7 +193,8 @@
 
 /** 鍒濆鍖� */
 onMounted(async () => {
-  productOptions.value = await getProductSimpleList();
+  const res = await getItemPage({ pageNo: 1, pageSize: 100, status: 0 });
+  productOptions.value = res.list || [];
   warehouseOptions.value = await getWarehouseSimpleList();
 });
 </script>
diff --git a/src/views/erp/sale/return/data.ts b/src/views/erp/sale/return/data.ts
index 102f692..1614bd5 100644
--- a/src/views/erp/sale/return/data.ts
+++ b/src/views/erp/sale/return/data.ts
@@ -7,8 +7,8 @@
 
 import { z } from '#/adapter/form';
 import { getAccountSimpleList } from '#/api/erp/finance/account';
-import { getProductSimpleList } from '#/api/erp/product/product';
-import { getCustomerSimpleList } from '#/api/erp/sale/customer';
+import { getItemPage } from '#/api/mdm/item';
+import { getCustomerSimpleList as getCrmCustomerSimpleList } from '#/api/crm/customer';
 import { getWarehouseSimpleList } from '#/api/erp/stock/warehouse';
 import { getSimpleUserList } from '#/api/system/user';
 import { getRangePickerDefaultProps } from '#/utils';
@@ -69,7 +69,7 @@
         placeholder: '璇烽�夋嫨瀹㈡埛',
         allowClear: true,
         showSearch: true,
-        api: getCustomerSimpleList,
+        api: getCrmCustomerSimpleList,
         labelField: 'name',
         valueField: 'id',
       },
@@ -343,7 +343,10 @@
         placeholder: '璇烽�夋嫨浜у搧',
         allowClear: true,
         showSearch: true,
-        api: getProductSimpleList,
+        api: async () => {
+          const res = await getItemPage({ pageNo: 1, pageSize: 100, status: 0 });
+          return res.list || [];
+        },
         labelField: 'name',
         valueField: 'id',
       },
@@ -365,7 +368,7 @@
         placeholder: '璇烽�夋嫨瀹㈡埛',
         allowClear: true,
         showSearch: true,
-        api: getCustomerSimpleList,
+        api: getCrmCustomerSimpleList,
         labelField: 'name',
         valueField: 'id',
       },
@@ -541,7 +544,10 @@
         placeholder: '璇烽�夋嫨浜у搧',
         allowClear: true,
         showSearch: true,
-        api: getProductSimpleList,
+        api: async () => {
+          const res = await getItemPage({ pageNo: 1, pageSize: 100, status: 0 });
+          return res.list || [];
+        },
         labelField: 'name',
         valueField: 'id',
       },
diff --git a/src/views/erp/sale/return/modules/item-form.vue b/src/views/erp/sale/return/modules/item-form.vue
index e927e43..2344596 100644
--- a/src/views/erp/sale/return/modules/item-form.vue
+++ b/src/views/erp/sale/return/modules/item-form.vue
@@ -1,4 +1,5 @@
 <script lang="ts" setup>
+import type { MdmItemApi } from '#/api/mdm/item';
 import type { ErpSaleReturnApi } from '#/api/erp/sale/return';
 
 import { computed, nextTick, onMounted, ref, watch } from 'vue';
@@ -12,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 { getWarehouseStockCount } from '#/api/erp/stock/stock';
 import { getWarehouseSimpleList } from '#/api/erp/stock/warehouse';
 
@@ -40,7 +41,7 @@
 ]);
 
 const tableData = ref<ErpSaleReturnApi.SaleReturnItem[]>([]); // 琛ㄦ牸鏁版嵁
-const productOptions = ref<any[]>([]); // 浜у搧涓嬫媺閫夐」
+const productOptions = ref<MdmItemApi.Item[]>([]); // 鐗╂枡涓嬫媺閫夐」
 const warehouseOptions = ref<any[]>([]); // 浠撳簱涓嬫媺閫夐」
 
 /** 鑾峰彇琛ㄦ牸鍚堣鏁版嵁 */
@@ -192,7 +193,8 @@
 
 /** 鍒濆鍖� */
 onMounted(async () => {
-  productOptions.value = await getProductSimpleList();
+  const res = await getItemPage({ pageNo: 1, pageSize: 100, status: 0 });
+  productOptions.value = res.list || [];
   warehouseOptions.value = await getWarehouseSimpleList();
 });
 </script>

--
Gitblit v1.9.3