From 5c243d9d05da668a32b1bc9a4f543ea081488d11 Mon Sep 17 00:00:00 2001
From: xiaoyi <908147524@qq.com>
Date: 星期四, 20 八月 2026 14:08:03 +0800
Subject: [PATCH] feat 功能变更

---
 src/views/mes/qc/outsourcetest/data.ts                       |  263 +
 src/views/crm/contract/components/command-form.vue           |   90 
 src/views/mes/pd/tech-confirm/index.vue                      |  198 +
 src/views/mes/pro/emergencyplan/data.ts                      |  187 +
 src/views/mes/pd/ctc-reel/index.vue                          |  150 +
 src/api/mes/pro/maintenance-plan/index.ts                    |   90 
 src/views/crm/cancellation/index.vue                         |  189 +
 src/views/wls/stocktaking/task/index.vue                     |   63 
 src/views/crm/contract/components/command-form-data.ts       |  105 
 src/views/crm/contract/components/command-list-data.ts       |   55 
 src/api/crm/cancellation/index.ts                            |   77 
 src/api/mes/pd/ctc/index.ts                                  |  128 
 src/views/crm/refund/index.vue                               |  184 +
 src/views/erp/purchase/plan/modules/process-select-modal.vue |   79 
 src/api/crm/saleQuotation/index.ts                           |    7 
 src/api/wls/warehouse/area/index.ts                          |    1 
 src/api/hrm/certificate/index.ts                             |   52 
 src/views/crm/contract/detail/index.vue                      |    8 
 src/views/hrm/certificate/modules/form.vue                   |   91 
 src/views/mes/dv/statistics/summary.vue                      |  109 
 src/api/mes/pd/tech-confirm/index.ts                         |   63 
 src/views/mes/pd/ctc/components/index.ts                     |    1 
 src/views/crm/refund/data.ts                                 |  208 +
 src/api/crm/credit-rating/index.ts                           |   63 
 src/views/mes/pd/ctc/modules/form.vue                        |  134 
 src/api/erp/purchase/plan/index.ts                           |  102 
 src/api/wls/productissue/index.ts                            |    1 
 src/api/crm/customer/index.ts                                |    5 
 src/views/mes/pro/workorder/data.ts                          |   31 
 src/views/mes/process-design/process/data.ts                 |   19 
 src/views/hrm/certificate/data.ts                            |  197 +
 src/views/mes/pd/ctc/modules/calc-modal.vue                  |  344 ++
 src/views/wls/productissue/data.ts                           |   22 
 src/views/mes/dv/machinery/modules/form.vue                  |    6 
 src/api/crm/refund/index.ts                                  |   68 
 src/views/crm/saleQuotation/data.ts                          |   87 
 src/views/erp/purchase/plan/modules/form.vue                 |  236 +
 src/views/mes/qc/outsourcetest/modules/form.vue              |  101 
 src/api/mes/qc/outsourcetest/index.ts                        |   64 
 src/views/erp/purchase/plan/data.ts                          |  242 +
 src/views/mes/pd/ctc/index.vue                               |  161 +
 src/packages/constants/src/dict-enum.ts                      |   32 
 src/views/mes/pro/emergencyplan/index.vue                    |  188 +
 src/views/aftersales/ticket/data.ts                          |    4 
 src/views/mes/pro/task/data.ts                               |   14 
 src/views/wls/warehouse/data.ts                              |   19 
 src/views/mes/pd/ctc/components/select.vue                   |   64 
 src/api/mes/pro/task/index.ts                                |    2 
 src/views/mes/pro/emergencyplan/modules/form.vue             |   91 
 src/views/mes/pd/ctc/data.ts                                 |  262 +
 src/views/crm/credit-rating/data.ts                          |  212 +
 src/views/crm/refund/modules/form.vue                        |   92 
 src/views/mes/qc/outsourcetest/index.vue                     |  210 +
 src/views/crm/contract/data.ts                               |  102 
 src/api/crm/contract-command/index.ts                        |   66 
 src/api/mes/dv/machinery/index.ts                            |    1 
 src/views/crm/cancellation/modules/form.vue                  |   95 
 vite.config.ts                                               |    2 
 src/api/mes/pro/process/index.ts                             |    1 
 src/views/erp/purchase/plan/index.vue                        |  300 ++
 src/api/mes/pro/feedback/index.ts                            |   37 
 src/views/mes/pd/tech-confirm/modules/form.vue               |  100 
 src/views/wls/warehouse/area/data.ts                         |   18 
 src/views/mes/pro/maintenance-plan/modules/form.vue          |  220 +
 src/views/mes/pd/ctc/modules/index.ts                        |    1 
 src/api/mes/dv/statistics/index.ts                           |   32 
 src/views/crm/cancellation/data.ts                           |  227 +
 src/views/crm/credit-rating/modules/form.vue                 |   94 
 src/views/hrm/certificate/index.vue                          |  169 +
 src/api/crm/contract/index.ts                                |    8 
 src/packages/constants/src/biz-mes-enum.ts                   |    4 
 src/views/mes/dv/machinery/data.ts                           |    9 
 src/api/mes/pd/ctc-reel/index.ts                             |   60 
 src/api/mes/pro/emergencyplan/index.ts                       |   53 
 src/views/crm/credit-rating/index.vue                        |  156 +
 src/views/mes/pro/maintenance-plan/index.vue                 |  271 +
 src/views/mes/pro/maintenance-plan/data.ts                   |  244 +
 src/api/wls/warehouse/index.ts                               |    1 
 src/components/table-action/icons.ts                         |    1 
 src/api/mes/pro/workorder/index.ts                           |    2 
 src/views/mes/pd/ctc-reel/modules/form.vue                   |  126 
 src/views/mes/md/item/data.ts                                |   19 
 src/views/mes/pro/workresult/summary.vue                     |  168 +
 src/api/mes/md/item/index.ts                                 |    1 
 src/api/wls/stocktaking/task/line/index.ts                   |   14 
 src/views/mes/pd/ctc-reel/data.ts                            |  206 +
 src/views/crm/customer/data.ts                               |   64 
 src/views/crm/contract/components/command-list.vue           |  188 +
 src/views/mes/pd/tech-confirm/data.ts                        |  260 +
 89 files changed, 8,790 insertions(+), 1 deletions(-)

diff --git a/src/api/crm/cancellation/index.ts b/src/api/crm/cancellation/index.ts
new file mode 100644
index 0000000..684850a
--- /dev/null
+++ b/src/api/crm/cancellation/index.ts
@@ -0,0 +1,77 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace CrmCancellationApi {
+  export interface Cancellation {
+    id?: number;
+    cancellationNo?: string;
+    customerId?: number;
+    customerName?: string;
+    contractId?: number;
+    contractNo?: string;
+    cancellationType?: string;
+    cancelReason?: string;
+    expectedCancelDate?: string;
+    actualCancelDate?: string;
+    refundAmount?: number;
+    applyUserId?: number;
+    applyUserName?: string;
+    applyTime?: string;
+    auditStatus?: number;
+    processInstanceId?: string;
+    remark?: string;
+    createTime?: string;
+  }
+  export interface PageParams extends PageParam {
+    cancellationNo?: string;
+    customerId?: number;
+    cancellationType?: string;
+    auditStatus?: number;
+  }
+}
+
+/** 鏌ヨ閿�鎴风敵璇峰垎椤� */
+export function getCancellationPage(
+  params: CrmCancellationApi.PageParams,
+) {
+  return requestClient.get<PageResult<CrmCancellationApi.Cancellation>>(
+    '/crm/cancellation/page',
+    { params },
+  );
+}
+
+/** 鏌ヨ閿�鎴风敵璇疯鎯� */
+export function getCancellation(id: number) {
+  return requestClient.get<CrmCancellationApi.Cancellation>(
+    `/crm/cancellation/get?id=${id}`,
+  );
+}
+
+/** 鏂板閿�鎴风敵璇� */
+export function createCancellation(data: CrmCancellationApi.Cancellation) {
+  return requestClient.post('/crm/cancellation/create', data);
+}
+
+/** 淇敼閿�鎴风敵璇� */
+export function updateCancellation(data: CrmCancellationApi.Cancellation) {
+  return requestClient.put('/crm/cancellation/update', data);
+}
+
+/** 瀹℃壒閿�鎴风敵璇� */
+export function auditCancellation(data: {
+  id: number;
+  auditStatus: number;
+}) {
+  return requestClient.put('/crm/cancellation/audit', data);
+}
+
+/** 鍒犻櫎閿�鎴风敵璇� */
+export function deleteCancellation(id: number) {
+  return requestClient.delete(`/crm/cancellation/delete?id=${id}`);
+}
+
+/** 瀵煎嚭閿�鎴风敵璇� */
+export function exportCancellation(params: Record<string, unknown>) {
+  return requestClient.download('/crm/cancellation/export-excel', { params });
+}
diff --git a/src/api/crm/contract-command/index.ts b/src/api/crm/contract-command/index.ts
new file mode 100644
index 0000000..cd4e15d
--- /dev/null
+++ b/src/api/crm/contract-command/index.ts
@@ -0,0 +1,66 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace CrmContractCommandApi {
+  export interface ContractCommand {
+    id?: number;
+    contractId?: number;
+    contractNo?: string;
+    commandType?: string;
+    targetModule?: string;
+    targetId?: number;
+    bizNo?: string;
+    operatorId?: number;
+    operatorName?: string;
+    operateTime?: string;
+    status?: number;
+    remark?: string;
+    createTime?: string;
+  }
+  export interface PageParams extends PageParam {
+    contractId?: number;
+    commandType?: string;
+    status?: number;
+    bizNo?: string;
+  }
+}
+
+/** 鏌ヨ鍚堝悓鎸囦护鍒嗛〉 */
+export function getContractCommandPage(
+  params: CrmContractCommandApi.PageParams,
+) {
+  return requestClient.get<PageResult<CrmContractCommandApi.ContractCommand>>(
+    '/crm/contract-command/page',
+    { params },
+  );
+}
+
+/** 鏂板鍚堝悓鎸囦护 */
+export function createContractCommand(
+  data: CrmContractCommandApi.ContractCommand,
+) {
+  return requestClient.post('/crm/contract-command/create', data);
+}
+
+/** 鏇存柊鍚堝悓鎸囦护 */
+export function updateContractCommand(
+  data: CrmContractCommandApi.ContractCommand,
+) {
+  return requestClient.put('/crm/contract-command/update', data);
+}
+
+/** 鎵ц鍚堝悓鎸囦护 */
+export function executeContractCommand(id: number) {
+  return requestClient.put(`/crm/contract-command/execute?id=${id}`);
+}
+
+/** 鍙栨秷鍚堝悓鎸囦护 */
+export function cancelContractCommand(id: number) {
+  return requestClient.put(`/crm/contract-command/cancel?id=${id}`);
+}
+
+/** 鍒犻櫎鍚堝悓鎸囦护 */
+export function deleteContractCommand(id: number) {
+  return requestClient.delete(`/crm/contract-command/delete?id=${id}`);
+}
diff --git a/src/api/crm/contract/index.ts b/src/api/crm/contract/index.ts
index 7bf66e4..35b4e05 100644
--- a/src/api/crm/contract/index.ts
+++ b/src/api/crm/contract/index.ts
@@ -43,6 +43,14 @@
     contactName?: string;
     orderId?: number; // 鍏宠仈 ERP 閿�鍞鍗曠紪鍙�
     orderNo?: string; // ERP 閿�鍞鍗曞崟鍙�
+    contractType?: string; // 鍚堝悓绫诲瀷锛堜緵鐢ㄧ數/鑳芥簮鏈嶅姟绛夛級
+    voltageLevel?: string; // 鐢靛帇绛夌骇
+    electricityType?: string; // 鐢ㄧ數绫诲瀷
+    contractCapacity?: number; // 鍚堝悓瀹归噺锛坘VA锛�
+    tariffMode?: string; // 鐢典环妯″紡
+    meterMode?: string; // 璁¢噺鏂瑰紡
+    payCycle?: number; // 缂磋垂鍛ㄦ湡锛堟湀锛�
+    commandCount?: number; // 鎸囦护鏁伴噺
   }
 
   /** 鍚堝悓浜у搧淇℃伅 */
diff --git a/src/api/crm/credit-rating/index.ts b/src/api/crm/credit-rating/index.ts
new file mode 100644
index 0000000..e33d7fd
--- /dev/null
+++ b/src/api/crm/credit-rating/index.ts
@@ -0,0 +1,63 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace CrmCreditRatingApi {
+  export interface CreditRating {
+    id?: number;
+    customerId?: number;
+    customerName?: string;
+    level?: string;
+    score?: number;
+    evalType?: string;
+    evalReason?: string;
+    operatorId?: number;
+    operatorName?: string;
+    operateTime?: string;
+    validUntil?: string;
+    remark?: string;
+    createTime?: string;
+  }
+  export interface PageParams extends PageParam {
+    customerId?: number;
+    level?: string;
+    evalType?: string;
+  }
+}
+
+/** 鏌ヨ淇$敤璇勭骇鍒嗛〉 */
+export function getCreditRatingPage(
+  params: CrmCreditRatingApi.PageParams,
+) {
+  return requestClient.get<PageResult<CrmCreditRatingApi.CreditRating>>(
+    '/crm/credit-rating/page',
+    { params },
+  );
+}
+
+/** 鏌ヨ淇$敤璇勭骇璇︽儏 */
+export function getCreditRating(id: number) {
+  return requestClient.get<CrmCreditRatingApi.CreditRating>(
+    `/crm/credit-rating/get?id=${id}`,
+  );
+}
+
+/** 鏂板淇$敤璇勭骇 */
+export function createCreditRating(data: CrmCreditRatingApi.CreditRating) {
+  return requestClient.post('/crm/credit-rating/create', data);
+}
+
+/** 淇敼淇$敤璇勭骇 */
+export function updateCreditRating(data: CrmCreditRatingApi.CreditRating) {
+  return requestClient.put('/crm/credit-rating/update', data);
+}
+
+/** 鍒犻櫎淇$敤璇勭骇 */
+export function deleteCreditRating(id: number) {
+  return requestClient.delete(`/crm/credit-rating/delete?id=${id}`);
+}
+
+/** 瀵煎嚭淇$敤璇勭骇 */
+export function exportCreditRating(params: Record<string, unknown>) {
+  return requestClient.download('/crm/credit-rating/export-excel', { params });
+}
diff --git a/src/api/crm/customer/index.ts b/src/api/crm/customer/index.ts
index 4c17fde..b5f7a1c 100644
--- a/src/api/crm/customer/index.ts
+++ b/src/api/crm/customer/index.ts
@@ -36,6 +36,11 @@
     createTime: Date; // 鍒涘缓鏃堕棿
     updateTime: Date; // 鏇存柊鏃堕棿
     poolDay?: number; // 璺濈杩涘叆鍏捣澶╂暟
+    customerType?: string; // 瀹㈡埛鍒嗙被锛堟斂浼�/宸ヤ笟/灞呮皯绛夛級
+    creditLevel?: string; // 褰撳墠淇$敤绛夌骇
+    creditScore?: number; // 褰撳墠淇$敤鍒�
+    overdueAmount?: number; // 娆犺垂閲戦锛堟湭鍥炴+閫炬湡锛�
+    overdueCount?: number; // 娆犺垂绗旀暟
   }
 
   /** 瀹㈡埛瀵煎叆璇锋眰 */
diff --git a/src/api/crm/refund/index.ts b/src/api/crm/refund/index.ts
new file mode 100644
index 0000000..24bd37a
--- /dev/null
+++ b/src/api/crm/refund/index.ts
@@ -0,0 +1,68 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace CrmRefundApi {
+  export interface Refund {
+    id?: number;
+    refundNo?: string;
+    customerId?: number;
+    customerName?: string;
+    contractId?: number;
+    contractNo?: string;
+    refundType?: string;
+    refundAmount?: number;
+    refundReason?: string;
+    applyUserId?: number;
+    applyUserName?: string;
+    applyTime?: string;
+    auditStatus?: number;
+    processInstanceId?: string;
+    remark?: string;
+    createTime?: string;
+  }
+  export interface PageParams extends PageParam {
+    refundNo?: string;
+    customerId?: number;
+    refundType?: string;
+    auditStatus?: number;
+  }
+}
+
+/** 鏌ヨ閫�璐瑰崟鍒嗛〉 */
+export function getRefundPage(params: CrmRefundApi.PageParams) {
+  return requestClient.get<PageResult<CrmRefundApi.Refund>>(
+    '/crm/refund/page',
+    { params },
+  );
+}
+
+/** 鏌ヨ閫�璐瑰崟璇︽儏 */
+export function getRefund(id: number) {
+  return requestClient.get<CrmRefundApi.Refund>(`/crm/refund/get?id=${id}`);
+}
+
+/** 鏂板閫�璐瑰崟 */
+export function createRefund(data: CrmRefundApi.Refund) {
+  return requestClient.post('/crm/refund/create', data);
+}
+
+/** 淇敼閫�璐瑰崟 */
+export function updateRefund(data: CrmRefundApi.Refund) {
+  return requestClient.put('/crm/refund/update', data);
+}
+
+/** 瀹℃壒閫�璐瑰崟 */
+export function auditRefund(data: { id: number; auditStatus: number }) {
+  return requestClient.put('/crm/refund/audit', data);
+}
+
+/** 鍒犻櫎閫�璐瑰崟 */
+export function deleteRefund(id: number) {
+  return requestClient.delete(`/crm/refund/delete?id=${id}`);
+}
+
+/** 瀵煎嚭閫�璐瑰崟 */
+export function exportRefund(params: Record<string, unknown>) {
+  return requestClient.download('/crm/refund/export-excel', { params });
+}
diff --git a/src/api/crm/saleQuotation/index.ts b/src/api/crm/saleQuotation/index.ts
index 108444b..5d1106d 100644
--- a/src/api/crm/saleQuotation/index.ts
+++ b/src/api/crm/saleQuotation/index.ts
@@ -34,6 +34,13 @@
     createTime?: Date;
     updateTime?: Date;
     items?: SaleQuotationItem[];
+    quotationType?: string; // 鎶ヤ环绫诲瀷锛坓eneral/power/energy锛�
+    capacity?: number; // 鍚堝悓瀹归噺锛坘VA锛�
+    maxDemand?: number; // 鏈�澶ч渶閲忥紙kW锛�
+    unitPrice?: number; // 鐢靛害鍗曚环锛堝厓/kWh锛�
+    tariffMode?: string; // 鐢典环妯″紡
+    powerFactorFee?: number; // 鍔涜皟鐢佃垂锛堝厓锛�
+    billingCycle?: number; // 缁撶畻鍛ㄦ湡锛堟湀锛�
   }
 
   /** 鎶ヤ环鍗曠墿鏂欐槑缁� */
diff --git a/src/api/erp/purchase/plan/index.ts b/src/api/erp/purchase/plan/index.ts
new file mode 100644
index 0000000..6ba0506
--- /dev/null
+++ b/src/api/erp/purchase/plan/index.ts
@@ -0,0 +1,102 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace ErpPurchasePlanApi {
+  /** 閲囪喘璁″垝淇℃伅 */
+  export interface PurchasePlan {
+    id?: number; // 缂栧彿
+    no?: string; // 璁″垝鍗曞彿
+    name?: string; // 璁″垝鍚嶇О
+    status?: number; // 鐘舵�侊細0鏈彁浜� 10瀹℃壒涓� 20瀹℃牳閫氳繃 30瀹℃牳涓嶉�氳繃 40宸插彇娑�
+    processInstanceId?: string; // 宸ヤ綔娴佺紪鍙�
+    demandSource?: number; // 闇�姹傛潵婧愶細1搴撳瓨棰勮 2杩愮淮/鐢熶骇璁″垝
+    demandSourceName?: string; // 闇�姹傛潵婧愬悕绉�
+    planDate?: string; // 璁″垝鏃ユ湡
+    planUserId?: number; // 缂栧埗浜虹紪鍙�
+    planUserName?: string; // 缂栧埗浜哄鍚�
+    planDeptId?: number; // 缂栧埗閮ㄩ棬缂栧彿
+    planDeptName?: string; // 缂栧埗閮ㄩ棬鍚嶇О
+    supplierId?: number; // 渚涘簲鍟嗙紪鍙�
+    supplierName?: string; // 渚涘簲鍟嗗悕绉�
+    totalCount?: number; // 鍚堣鏁伴噺
+    generatedFlag?: boolean; // 鏄惁宸茬敓鎴愰噰璐敵璇�
+    remark?: string; // 澶囨敞
+    productNames?: string; // 浜у搧鍚嶇О鍒楄〃
+    items?: PurchasePlanItem[]; // 璁″垝鏄庣粏
+    createTime?: string; // 鍒涘缓鏃堕棿
+  }
+
+  /** 閲囪喘璁″垝鏄庣粏 */
+  export interface PurchasePlanItem {
+    id?: number; // 缂栧彿
+    productId: number; // 浜у搧缂栧彿
+    productName?: string; // 浜у搧鍚嶇О
+    productUnitId?: number; // 浜у搧鍗曚綅缂栧彿
+    productUnitName?: string; // 浜у搧鍗曚綅鍚嶇О
+    count: number; // 璁″垝閲囪喘鏁伴噺
+    demandTime?: string; // 闇�姹傛棩鏈�
+    remark?: string; // 澶囨敞
+  }
+}
+
+/** 鍒涘缓閲囪喘璁″垝 */
+export function createPurchasePlan(data: ErpPurchasePlanApi.PurchasePlan) {
+  return requestClient.post('/erp/purchase-plan/create', data);
+}
+
+/** 鏇存柊閲囪喘璁″垝 */
+export function updatePurchasePlan(data: ErpPurchasePlanApi.PurchasePlan) {
+  return requestClient.put('/erp/purchase-plan/update', data);
+}
+
+/** 鎻愪氦閲囪喘璁″垝瀹℃壒 */
+export function submitPurchasePlan(id: number, processDefinitionKey: string) {
+  return requestClient.put('/erp/purchase-plan/submit', null, {
+    params: { id, processDefinitionKey },
+  });
+}
+
+/** 鏍规嵁搴撳瓨棰勮鐢熸垚閲囪喘璁″垝 */
+export function generatePlanFromStockAlert(params: { name?: string; supplierId?: number }) {
+  return requestClient.post<number>('/erp/purchase-plan/generate-from-stock-alert', null, {
+    params,
+  });
+}
+
+/** 鐢熸垚閲囪喘鐢宠 */
+export function generatePurchaseRequests(id: number) {
+  return requestClient.post<number[]>('/erp/purchase-plan/generate-requests', null, {
+    params: { id },
+  });
+}
+
+/** 鍒犻櫎閲囪喘璁″垝 */
+export function deletePurchasePlan(ids: number[]) {
+  return requestClient.delete('/erp/purchase-plan/delete', { params: { ids: ids.join(',') } });
+}
+
+/** 鑾峰彇閲囪喘璁″垝璇︽儏 */
+export function getPurchasePlan(id: number) {
+  return requestClient.get<ErpPurchasePlanApi.PurchasePlan>(
+    `/erp/purchase-plan/get?id=${id}`,
+  );
+}
+
+/** 鑾峰彇閲囪喘璁″垝鍒嗛〉 */
+export function getPurchasePlanPage(params: PageParam) {
+  return requestClient.get<PageResult<ErpPurchasePlanApi.PurchasePlan>>(
+    '/erp/purchase-plan/page',
+    { params },
+  );
+}
+
+/** 瀵煎嚭閲囪喘璁″垝 Excel */
+export function exportPurchasePlan(params: any) {
+  return requestClient.download('/erp/purchase-plan/export-excel', { params });
+}
+
+/** 鑾峰彇瀹℃壒娴佺▼鍒楄〃 */
+export function getApproveProcessList(id: number) {
+  return requestClient.get(`/erp/purchase-plan/approve-process-list`, { params: { id } });
+}
diff --git a/src/api/hrm/certificate/index.ts b/src/api/hrm/certificate/index.ts
new file mode 100644
index 0000000..de69612
--- /dev/null
+++ b/src/api/hrm/certificate/index.ts
@@ -0,0 +1,52 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace HrmCertificateApi {
+  /** 璇佷功璧勮川 */
+  export interface Certificate {
+    id?: number;
+    employeeId?: number; // 鍛樺伐缂栧彿
+    employeeName?: string; // 鍛樺伐濮撳悕
+    certType?: number; // 璇佷功绫诲瀷锛�1鐗圭浣滀笟 2浠庝笟璧勮川 3鎶�鑳界瓑绾� 4瀹夊叏鍩硅
+    certTypeName?: string; // 璇佷功绫诲瀷鍚嶇О
+    certName?: string; // 璇佷功鍚嶇О
+    certNo?: string; // 璇佷功缂栧彿
+    issueOrg?: string; // 鍙戣瘉鏈烘瀯
+    issueDate?: string; // 鍙戣瘉鏃ユ湡
+    expireDate?: string; // 鏈夋晥鏈熻嚦
+    expireStatus?: number; // 鍒版湡鐘舵�侊細0鏈夋晥 1鍗冲皢鍒版湡 2宸茶繃鏈�
+    remark?: string; // 澶囨敞
+    createTime?: string;
+  }
+}
+
+/** 鍒涘缓璇佷功璧勮川 */
+export function createCertificate(data: HrmCertificateApi.Certificate) {
+  return requestClient.post('/hrm/certificate/create', data);
+}
+
+/** 鏇存柊璇佷功璧勮川 */
+export function updateCertificate(data: HrmCertificateApi.Certificate) {
+  return requestClient.put('/hrm/certificate/update', data);
+}
+
+/** 鍒犻櫎璇佷功璧勮川 */
+export function deleteCertificate(ids: number[]) {
+  return requestClient.delete('/hrm/certificate/delete', { params: { ids: ids.join(',') } });
+}
+
+/** 鑾峰彇璇佷功璧勮川璇︽儏 */
+export function getCertificate(id: number) {
+  return requestClient.get<HrmCertificateApi.Certificate>(
+    `/hrm/certificate/get?id=${id}`,
+  );
+}
+
+/** 鑾峰彇璇佷功璧勮川鍒嗛〉 */
+export function getCertificatePage(params: PageParam) {
+  return requestClient.get<PageResult<HrmCertificateApi.Certificate>>(
+    '/hrm/certificate/page',
+    { params },
+  );
+}
diff --git a/src/api/mes/dv/machinery/index.ts b/src/api/mes/dv/machinery/index.ts
index c46b39f..601c672 100644
--- a/src/api/mes/dv/machinery/index.ts
+++ b/src/api/mes/dv/machinery/index.ts
@@ -7,6 +7,7 @@
   export interface Machinery {
     id?: number; // 璁惧缂栧彿
     code?: string; // 璁惧缂栫爜
+    tbDeviceId?: string; // 鏁伴噰璁惧ID锛堝閮ㄦ暟閲� tbDeviceId锛�
     name?: string; // 璁惧鍚嶇О
     brand?: string; // 鍝佺墝
     specification?: string; // 瑙勬牸鍨嬪彿
diff --git a/src/api/mes/dv/statistics/index.ts b/src/api/mes/dv/statistics/index.ts
new file mode 100644
index 0000000..e13b505
--- /dev/null
+++ b/src/api/mes/dv/statistics/index.ts
@@ -0,0 +1,32 @@
+import { requestClient } from '#/api/request';
+
+export namespace MesDvStatisticsApi {
+  /** 璁惧缁熻姹囨�� */
+  export interface StatisticsSummary {
+    totalMachinery?: number; // 璁惧鎬绘暟
+    producingCount?: number; // 鐢熶骇涓�
+    stoppedCount?: number; // 鍋滄満
+    maintainingCount?: number; // 缁存姢涓�
+    faultRate?: number; // 鏁呴殰鐜�%
+    totalRepair?: number; // 缁翠慨鍗曟�绘暟
+    processingRepair?: number; // 缁翠慨涓�/寰呴獙鏀�
+    finishedRepair?: number; // 宸茬‘璁ょ淮淇�
+    totalMainten?: number; // 淇濆吇璁板綍鎬绘暟
+    finishedMainten?: number; // 宸插畬鎴愪繚鍏�
+    typeDistribution?: TypeItem[]; // 鎸夎澶囩被鍨嬪垎甯�
+  }
+
+  /** 璁惧绫诲瀷鍒嗗竷椤� */
+  export interface TypeItem {
+    machineryTypeId?: number;
+    machineryTypeName?: string;
+    count?: number;
+  }
+}
+
+/** 璁惧缁熻姹囨�� */
+export function getDvStatisticsSummary() {
+  return requestClient.get<MesDvStatisticsApi.StatisticsSummary>(
+    '/mes/dv/statistics/summary',
+  );
+}
diff --git a/src/api/mes/md/item/index.ts b/src/api/mes/md/item/index.ts
index 6ad491e..4469943 100644
--- a/src/api/mes/md/item/index.ts
+++ b/src/api/mes/md/item/index.ts
@@ -18,6 +18,7 @@
     safeStockFlag?: boolean; // 鏄惁鍚敤瀹夊叏搴撳瓨
     minStock?: number; // 鏈�浣庡簱瀛橀噺
     maxStock?: number; // 鏈�楂樺簱瀛橀噺
+    materialCategory?: number; // 鐢靛姏鐗╄祫鍒嗙被
     highValue?: boolean; // 鏄惁楂樺�肩墿鏂�
     batchFlag?: boolean; // 鏄惁鍚敤鎵规绠$悊
     remark?: string; // 澶囨敞
diff --git a/src/api/mes/pd/ctc-reel/index.ts b/src/api/mes/pd/ctc-reel/index.ts
new file mode 100644
index 0000000..b8de0c3
--- /dev/null
+++ b/src/api/mes/pd/ctc-reel/index.ts
@@ -0,0 +1,60 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace MesPdCtcReelApi {
+  /** 鎹綅瀵肩嚎鐩樺叿璁$畻 */
+  export interface CtcReel {
+    id?: number; // 缂栧彿
+    ctcId?: number; // 鎹綅瀵肩嚎浜у搧ID
+    ctcOrderSpec?: string; // 鎹綅瀵肩嚎璁㈣揣瑙勬牸
+    reelType?: string; // 鐩樺叿绫诲瀷
+    reelOuterDiameter?: number; // 鐩樺叿澶栧緞 mm
+    reelInnerDiameter?: number; // 鐩樿姱鍐呭緞 mm
+    reelWidth?: number; // 鐩樺 mm
+    maxCapacity?: number; // 鏈�澶у閲� km
+    recommendedLength?: number; // 鎺ㄨ崘姣忕洏闀垮害 km
+    netWeight?: number; // 鍑�閲� kg
+    grossWeight?: number; // 姣涢噸 kg
+    calcFormula?: string; // 璁$畻鍏紡
+    remark?: string; // 澶囨敞
+    createTime?: Date; // 鍒涘缓鏃堕棿
+  }
+}
+
+/** 鏌ヨ鐩樺叿璁$畻鍒嗛〉 */
+export function getCtcReelPage(params: PageParam) {
+  return requestClient.get<PageResult<MesPdCtcReelApi.CtcReel>>(
+    '/mes/pd/ctc-reel/page',
+    { params },
+  );
+}
+
+/** 鏌ヨ鐩樺叿璁$畻璇︽儏 */
+export function getCtcReel(id: number) {
+  return requestClient.get<MesPdCtcReelApi.CtcReel>(
+    `/mes/pd/ctc-reel/get?id=${id}`,
+  );
+}
+
+/** 鏌ヨ鎸囧畾鎹綅瀵肩嚎浜у搧鐨勭洏鍏峰垪琛� */
+export function getCtcReelListByCtcId(ctcId: number) {
+  return requestClient.get<MesPdCtcReelApi.CtcReel[]>(
+    `/mes/pd/ctc-reel/list-by-ctc?ctcId=${ctcId}`,
+  );
+}
+
+/** 鏂板鐩樺叿璁$畻璁板綍 */
+export function createCtcReel(data: MesPdCtcReelApi.CtcReel) {
+  return requestClient.post('/mes/pd/ctc-reel/create', data);
+}
+
+/** 淇敼鐩樺叿璁$畻璁板綍 */
+export function updateCtcReel(data: MesPdCtcReelApi.CtcReel) {
+  return requestClient.put('/mes/pd/ctc-reel/update', data);
+}
+
+/** 鍒犻櫎鐩樺叿璁$畻璁板綍 */
+export function deleteCtcReel(id: number) {
+  return requestClient.delete(`/mes/pd/ctc-reel/delete?id=${id}`);
+}
diff --git a/src/api/mes/pd/ctc/index.ts b/src/api/mes/pd/ctc/index.ts
new file mode 100644
index 0000000..55e5a0c
--- /dev/null
+++ b/src/api/mes/pd/ctc/index.ts
@@ -0,0 +1,128 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace MesPdCtcApi {
+  /** 鎹綅瀵肩嚎浜у搧鎶�鏈弬鏁� */
+  export interface Ctc {
+    id?: number; // 缂栧彿
+    itemId?: number; // 鐗╂枡ID
+    itemName?: string; // 鐗╂枡鍚嶇О
+    itemCode?: string; // 鐗╂枡缂栫爜
+    orderSpec?: string; // 璁㈣揣瑙勬牸
+    bareConductorSpec?: string; // 瑁稿绾胯鏍�
+    enameledSpec?: string; // 婕嗗寘绾胯鏍�
+    transpositionSpec?: string; // 鎹綅瑙勬牸
+    conductorType?: string; // 瀵间綋绫诲瀷
+    crossSection?: number; // 鎴潰绉� mm虏
+    outerDiameter?: number; // 澶栧緞 mm
+    resistance20c?: number; // 20鈩冪洿娴佺數闃� 惟/km
+    wrappingAngle?: number; // 缁曞寘瑙掑害 掳
+    insulationThickness?: number; // 缁濈紭鍘氬害 mm
+    ratedVoltage?: string; // 棰濆畾鐢靛帇 kV
+    testVoltage?: string; // 璇曢獙鐢靛帇 kV
+    minBendingRadius?: number; // 鏈�灏忓集鏇插崐寰� mm
+    weightPerKm?: number; // 姣忓叕閲岄噸閲� kg/km
+    standardCode?: string; // 鎵ц鏍囧噯浠e彿
+    remark?: string; // 澶囨敞
+    createTime?: Date; // 鍒涘缓鏃堕棿
+  }
+
+  /** 缁曞寘瑙掑害璁$畻 */
+  export interface AngleCalcReq {
+    conductorThickness?: number; // 瀵间綋鍘氬害 mm
+    conductorWidth?: number; // 瀵间綋瀹藉害 mm
+    cornerRadius?: number; // 鍦嗚鍗婂緞 mm
+    insulationThickness?: number; // 缁濈紭鍘氬害 mm
+    tapeWidth?: number; // 绾稿甫瀹藉害 mm
+    overlapWidth?: number; // 閲嶅彔瀹藉害 mm
+  }
+
+  /** 缁曞寘瑙掑害璁$畻缁撴灉 */
+  export interface AngleCalcResp {
+    circumference?: number; // 瀵肩嚎鍛ㄩ暱 mm
+    wrappingAngle?: number; // 缁曞寘瑙掑害 掳
+    wrappingPitch?: number; // 缁曞寘鑺傝窛 mm
+  }
+
+  /** 鐩存祦鐢甸樆璁$畻 */
+  export interface ResistanceCalcReq {
+    conductorThickness?: number; // 瀵间綋鍘氬害 mm
+    conductorWidth?: number; // 瀵间綋瀹藉害 mm
+    temperature?: number; // 璁$畻娓╁害 鈩�
+    material?: string; // 瀵间綋鏉愯川
+  }
+
+  /** 鐩存祦鐢甸樆璁$畻缁撴灉 */
+  export interface ResistanceCalcResp {
+    crossSection?: number; // 鎴潰绉� mm虏
+    resistance20cKm?: number; // 20鈩冪洿娴佺數闃� 惟/km
+    resistanceAtTemp?: number; // 鎸囧畾娓╁害鐩存祦鐢甸樆 惟/m
+    resistanceAtTempKm?: number; // 鎸囧畾娓╁害鐩存祦鐢甸樆 惟/km
+  }
+
+  /** 鐩樺叿瀹归噺璁$畻 */
+  export interface ReelCalcReq {
+    reelOuterDiameter?: number; // 鐩樺叿澶栧緞 d1 mm
+    reelInnerDiameter?: number; // 鐩樿姱鍐呭緞 d2 mm
+    reelWidth?: number; // 鐩樺 l2 mm
+    ctcThickness?: number; // CTC 鏈�澶ч珮搴� mm
+    ctcWidth?: number; // CTC 鏈�澶у搴� mm
+  }
+
+  /** 鐩樺叿瀹归噺璁$畻缁撴灉 */
+  export interface ReelCalcResp {
+    capacityMeter?: number; // 姣忕洏闀垮害 m
+    capacityKm?: number; // 姣忕洏闀垮害 km
+  }
+}
+
+/** 鏌ヨ鎹綅瀵肩嚎浜у搧鎶�鏈弬鏁板垎椤� */
+export function getCtcPage(params: PageParam) {
+  return requestClient.get<PageResult<MesPdCtcApi.Ctc>>('/mes/pd/ctc/page', {
+    params,
+  });
+}
+
+/** 鏌ヨ鎹綅瀵肩嚎浜у搧鎶�鏈弬鏁拌鎯� */
+export function getCtc(id: number) {
+  return requestClient.get<MesPdCtcApi.Ctc>(`/mes/pd/ctc/get?id=${id}`);
+}
+
+/** 鏂板鎹綅瀵肩嚎浜у搧鎶�鏈弬鏁� */
+export function createCtc(data: MesPdCtcApi.Ctc) {
+  return requestClient.post('/mes/pd/ctc/create', data);
+}
+
+/** 淇敼鎹綅瀵肩嚎浜у搧鎶�鏈弬鏁� */
+export function updateCtc(data: MesPdCtcApi.Ctc) {
+  return requestClient.put('/mes/pd/ctc/update', data);
+}
+
+/** 鍒犻櫎鎹綅瀵肩嚎浜у搧鎶�鏈弬鏁� */
+export function deleteCtc(id: number) {
+  return requestClient.delete(`/mes/pd/ctc/delete?id=${id}`);
+}
+
+/** 瀵煎嚭鎹綅瀵肩嚎浜у搧鎶�鏈弬鏁� */
+export function exportCtc(params: any) {
+  return requestClient.download('/mes/pd/ctc/export-excel', { params });
+}
+
+/** 缁曞寘瑙掑害璁$畻 */
+export function calcCtcAngle(data: MesPdCtcApi.AngleCalcReq) {
+  return requestClient.post<MesPdCtcApi.AngleCalcResp>('/mes/pd/ctc/calc-angle', data);
+}
+
+/** 鐩存祦鐢甸樆璁$畻 */
+export function calcCtcResistance(data: MesPdCtcApi.ResistanceCalcReq) {
+  return requestClient.post<MesPdCtcApi.ResistanceCalcResp>(
+    '/mes/pd/ctc/calc-resistance',
+    data,
+  );
+}
+
+/** 鐩樺叿瀹归噺璁$畻 */
+export function calcCtcReel(data: MesPdCtcApi.ReelCalcReq) {
+  return requestClient.post<MesPdCtcApi.ReelCalcResp>('/mes/pd/ctc/calc-reel', data);
+}
diff --git a/src/api/mes/pd/tech-confirm/index.ts b/src/api/mes/pd/tech-confirm/index.ts
new file mode 100644
index 0000000..be86723
--- /dev/null
+++ b/src/api/mes/pd/tech-confirm/index.ts
@@ -0,0 +1,63 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace MesPdTechConfirmApi {
+  /** 浜у搧鎶�鏈‘璁ゅ嚱 */
+  export interface TechConfirm {
+    id?: number; // 缂栧彿
+    confirmNo?: string; // 纭鍑界紪鍙�
+    customerId?: number; // 瀹㈡埛ID
+    customerName?: string; // 瀹㈡埛鍚嶇О
+    productName?: string; // 浜у搧鍚嶇О
+    specification?: string; // 瑙勬牸鍨嬪彿
+    techParams?: string; // 鎶�鏈弬鏁� JSON
+    templateType?: string; // 妯℃澘绫诲瀷
+    drafterId?: number; // 缂栧埗浜篒D
+    drafterName?: string; // 缂栧埗浜哄鍚�
+    draftTime?: Date; // 缂栧埗鏃堕棿
+    reviewerId?: number; // 瀹℃牳浜篒D
+    reviewerName?: string; // 瀹℃牳浜哄鍚�
+    reviewTime?: Date; // 瀹℃牳鏃堕棿
+    customerConfirmUser?: string; // 瀹㈡埛纭浜�
+    customerConfirmTime?: Date; // 瀹㈡埛纭鏃堕棿
+    status?: number; // 鐘舵��
+    remark?: string; // 澶囨敞
+    createTime?: Date; // 鍒涘缓鏃堕棿
+  }
+}
+
+/** 鏌ヨ浜у搧鎶�鏈‘璁ゅ嚱鍒嗛〉 */
+export function getTechConfirmPage(params: PageParam) {
+  return requestClient.get<PageResult<MesPdTechConfirmApi.TechConfirm>>(
+    '/mes/pd/tech-confirm/page',
+    { params },
+  );
+}
+
+/** 鏌ヨ浜у搧鎶�鏈‘璁ゅ嚱璇︽儏 */
+export function getTechConfirm(id: number) {
+  return requestClient.get<MesPdTechConfirmApi.TechConfirm>(
+    `/mes/pd/tech-confirm/get?id=${id}`,
+  );
+}
+
+/** 鏂板浜у搧鎶�鏈‘璁ゅ嚱 */
+export function createTechConfirm(data: MesPdTechConfirmApi.TechConfirm) {
+  return requestClient.post('/mes/pd/tech-confirm/create', data);
+}
+
+/** 淇敼浜у搧鎶�鏈‘璁ゅ嚱 */
+export function updateTechConfirm(data: MesPdTechConfirmApi.TechConfirm) {
+  return requestClient.put('/mes/pd/tech-confirm/update', data);
+}
+
+/** 鍒犻櫎浜у搧鎶�鏈‘璁ゅ嚱 */
+export function deleteTechConfirm(id: number) {
+  return requestClient.delete(`/mes/pd/tech-confirm/delete?id=${id}`);
+}
+
+/** 瀵煎嚭浜у搧鎶�鏈‘璁ゅ嚱 */
+export function exportTechConfirm(params: any) {
+  return requestClient.download('/mes/pd/tech-confirm/export-excel', { params });
+}
diff --git a/src/api/mes/pro/emergencyplan/index.ts b/src/api/mes/pro/emergencyplan/index.ts
new file mode 100644
index 0000000..d4a0887
--- /dev/null
+++ b/src/api/mes/pro/emergencyplan/index.ts
@@ -0,0 +1,53 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace MesProEmergencyPlanApi {
+  /** 搴旀�ュ缃妗� */
+  export interface EmergencyPlan {
+    id?: number;
+    code?: string; // 棰勬缂栫爜
+    name?: string; // 棰勬鍚嶇О
+    planType?: number; // 棰勬绫诲瀷锛�1鍋滅數 2璁惧鏁呴殰 3鐏伨 4鑷劧鐏惧 5鍏朵粬
+    content?: string; // 澶勭疆鍐呭
+    orgUnit?: string; // 璐d换鍗曚綅
+    status?: number; // 鐘舵�侊細0鑽夌 10宸插彂甯�
+    remark?: string; // 澶囨敞
+    createTime?: string;
+  }
+}
+
+/** 鍒涘缓搴旀�ュ缃妗� */
+export function createPlan(data: MesProEmergencyPlanApi.EmergencyPlan) {
+  return requestClient.post('/mes/pro/emergency-plan/create', data);
+}
+
+/** 鏇存柊搴旀�ュ缃妗� */
+export function updatePlan(data: MesProEmergencyPlanApi.EmergencyPlan) {
+  return requestClient.put('/mes/pro/emergency-plan/update', data);
+}
+
+/** 鍒犻櫎搴旀�ュ缃妗� */
+export function deletePlan(ids: number[]) {
+  return requestClient.delete('/mes/pro/emergency-plan/delete', { params: { ids: ids.join(',') } });
+}
+
+/** 鑾峰彇搴旀�ュ缃妗� */
+export function getPlan(id: number) {
+  return requestClient.get<MesProEmergencyPlanApi.EmergencyPlan>(
+    `/mes/pro/emergency-plan/get?id=${id}`,
+  );
+}
+
+/** 鑾峰彇搴旀�ュ缃妗堝垎椤� */
+export function getPlanPage(params: PageParam) {
+  return requestClient.get<PageResult<MesProEmergencyPlanApi.EmergencyPlan>>(
+    '/mes/pro/emergency-plan/page',
+    { params },
+  );
+}
+
+/** 鍙戝竷搴旀�ュ缃妗� */
+export function publishPlan(id: number) {
+  return requestClient.put(`/mes/pro/emergency-plan/publish?id=${id}`);
+}
diff --git a/src/api/mes/pro/feedback/index.ts b/src/api/mes/pro/feedback/index.ts
index 7666071..44f3f35 100644
--- a/src/api/mes/pro/feedback/index.ts
+++ b/src/api/mes/pro/feedback/index.ts
@@ -204,3 +204,40 @@
     `/mes/pro/feedback/list-by-task?taskId=${taskId}`,
   );
 }
+
+/** 浣滀笟鎴愭灉姹囨�婚」锛堟寜宸ュ崟绫诲瀷锛� */
+export interface WorkResultTypeItem {
+  workOrderType?: number;
+  workOrderTypeName?: string;
+  feedbackQuantity?: number;
+  qualifiedQuantity?: number;
+  feedbackCount?: number;
+}
+
+/** 浣滀笟鎴愭灉姹囨�婚」锛堟寜鏃ユ湡锛� */
+export interface WorkResultDateItem {
+  date?: string;
+  feedbackQuantity?: number;
+  qualifiedQuantity?: number;
+  feedbackCount?: number;
+}
+
+/** 浣滀笟鎴愭灉姹囨�� */
+export interface WorkResultSummary {
+  totalFeedbackQuantity?: number;
+  totalQualifiedQuantity?: number;
+  totalFeedbackCount?: number;
+  byType?: WorkResultTypeItem[];
+  byDate?: WorkResultDateItem[];
+}
+
+/** 浣滀笟鎴愭灉姹囨�荤粺璁� */
+export function getFeedbackSummary(params: {
+  beginDate?: string;
+  endDate?: string;
+  workOrderType?: number;
+}) {
+  return requestClient.get<WorkResultSummary>('/mes/pro/feedback/summary', {
+    params,
+  });
+}
diff --git a/src/api/mes/pro/maintenance-plan/index.ts b/src/api/mes/pro/maintenance-plan/index.ts
new file mode 100644
index 0000000..cd95a2b
--- /dev/null
+++ b/src/api/mes/pro/maintenance-plan/index.ts
@@ -0,0 +1,90 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace MesProMaintenancePlanApi {
+  /** 鐢靛姏浣滀笟璁″垝 */
+  export interface MaintenancePlan {
+    id?: number; // 缂栧彿
+    code?: string; // 璁″垝缂栫爜
+    name?: string; // 璁″垝鍚嶇О
+    planType?: number; // 璁″垝绫诲瀷锛�1鐢电綉杩愯 2鍙戠數 3鍙樼數杩愮淮 4绾胯矾妫�淇�
+    station?: string; // 绔欑偣
+    line?: string; // 绾胯矾
+    teamId?: number; // 鐝粍缂栧彿
+    teamName?: string; // 鐝粍鍚嶇О
+    planDate?: string; // 璁″垝鏃ユ湡
+    status?: number; // 鐘舵�侊細0鑽夌 10宸蹭笅鍙� 20宸插畬鎴� 30宸插彇娑�
+    workOrderId?: number; // 鐢熸垚鐨勫伐鍗曠紪鍙�
+    workOrderCode?: string; // 鐢熸垚鐨勫伐鍗曠紪鐮�
+    remark?: string; // 澶囨敞
+    items?: PlanItem[]; // 澶囦欢闇�姹傛槑缁�
+    createTime?: string; // 鍒涘缓鏃堕棿
+  }
+
+  /** 澶囦欢闇�姹傛槑缁� */
+  export interface PlanItem {
+    id?: number; // 缂栧彿
+    itemId: number; // 鐗╂枡/澶囦欢缂栧彿
+    itemName?: string; // 鐗╂枡鍚嶇О
+    itemCode?: string; // 鐗╂枡缂栫爜
+    unitMeasureName?: string; // 璁¢噺鍗曚綅鍚嶇О
+    quantity: number; // 闇�姹傛暟閲�
+    remark?: string; // 澶囨敞
+  }
+}
+
+/** 鍒涘缓鐢靛姏浣滀笟璁″垝 */
+export function createPlan(data: MesProMaintenancePlanApi.MaintenancePlan) {
+  return requestClient.post('/mes/pro/maintenance-plan/create', data);
+}
+
+/** 鏇存柊鐢靛姏浣滀笟璁″垝 */
+export function updatePlan(data: MesProMaintenancePlanApi.MaintenancePlan) {
+  return requestClient.put('/mes/pro/maintenance-plan/update', data);
+}
+
+/** 鍒犻櫎鐢靛姏浣滀笟璁″垝 */
+export function deletePlan(ids: number[]) {
+  return requestClient.delete('/mes/pro/maintenance-plan/delete', { params: { ids: ids.join(',') } });
+}
+
+/** 鑾峰彇鐢靛姏浣滀笟璁″垝璇︽儏 */
+export function getPlan(id: number) {
+  return requestClient.get<MesProMaintenancePlanApi.MaintenancePlan>(
+    `/mes/pro/maintenance-plan/get?id=${id}`,
+  );
+}
+
+/** 鑾峰彇鐢靛姏浣滀笟璁″垝鍒嗛〉 */
+export function getPlanPage(params: PageParam) {
+  return requestClient.get<PageResult<MesProMaintenancePlanApi.MaintenancePlan>>(
+    '/mes/pro/maintenance-plan/page',
+    { params },
+  );
+}
+
+/** 瀵煎嚭鐢靛姏浣滀笟璁″垝 */
+export function exportPlan(params: any) {
+  return requestClient.download('/mes/pro/maintenance-plan/export-excel', { params });
+}
+
+/** 涓嬪彂鐢靛姏浣滀笟璁″垝 */
+export function issuePlan(id: number) {
+  return requestClient.put(`/mes/pro/maintenance-plan/issue?id=${id}`);
+}
+
+/** 瀹屾垚鐢靛姏浣滀笟璁″垝 */
+export function finishPlan(id: number) {
+  return requestClient.put(`/mes/pro/maintenance-plan/finish?id=${id}`);
+}
+
+/** 鏍规嵁澶囦欢闇�姹傜敓鎴愰噰璐鍒� */
+export function generatePurchasePlan(id: number) {
+  return requestClient.post<number>(`/mes/pro/maintenance-plan/generate-purchase-plan?id=${id}`);
+}
+
+/** 鐢熸垚杩愮淮/鍙戠數/妫�淇伐鍗� */
+export function generateWorkOrder(id: number) {
+  return requestClient.post<number>(`/mes/pro/maintenance-plan/generate-work-order?id=${id}`);
+}
diff --git a/src/api/mes/pro/process/index.ts b/src/api/mes/pro/process/index.ts
index a4ff1ea..a55daf6 100644
--- a/src/api/mes/pro/process/index.ts
+++ b/src/api/mes/pro/process/index.ts
@@ -8,6 +8,7 @@
     id?: number;
     code?: string;
     name?: string;
+    type?: number; // 鐢靛姏宸ヨ壓鍒嗙被
     attention?: string;
     status?: number;
     remark?: string;
diff --git a/src/api/mes/pro/task/index.ts b/src/api/mes/pro/task/index.ts
index a059e1c..f832ffa 100644
--- a/src/api/mes/pro/task/index.ts
+++ b/src/api/mes/pro/task/index.ts
@@ -27,6 +27,8 @@
     workstationId?: number; // 宸ヤ綔绔欑紪鍙�
     workstationCode?: string; // 宸ヤ綔绔欑紪鐮�
     workstationName?: string; // 宸ヤ綔绔欏悕绉�
+    teamId?: number; // 鐝粍缂栧彿
+    teamName?: string; // 鐝粍鍚嶇О
     routeId?: number; // 宸ヨ壓璺嚎缂栧彿
     processId?: number; // 宸ュ簭缂栧彿
     processName?: string; // 宸ュ簭鍚嶇О
diff --git a/src/api/mes/pro/workorder/index.ts b/src/api/mes/pro/workorder/index.ts
index 39bcf69..1718d76 100644
--- a/src/api/mes/pro/workorder/index.ts
+++ b/src/api/mes/pro/workorder/index.ts
@@ -24,6 +24,8 @@
     clientId?: number; // 瀹㈡埛缂栧彿
     clientCode?: string; // 瀹㈡埛缂栫爜
     clientName?: string; // 瀹㈡埛鍚嶇О
+    saleOrderId?: number; // 鍏宠仈钀ラ攢璁㈠崟锛圗RP閿�鍞鍗曪級缂栧彿
+    saleOrderNo?: string; // 鍏宠仈钀ラ攢璁㈠崟锛圗RP閿�鍞鍗曪級鍗曞彿
     vendorId?: number; // 渚涘簲鍟嗙紪鍙�
     vendorName?: string; // 渚涘簲鍟嗗悕绉�
     vendorCode?: string; // 渚涘簲鍟嗙紪鐮�
diff --git a/src/api/mes/qc/outsourcetest/index.ts b/src/api/mes/qc/outsourcetest/index.ts
new file mode 100644
index 0000000..610095c
--- /dev/null
+++ b/src/api/mes/qc/outsourcetest/index.ts
@@ -0,0 +1,64 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace MesQcOutsourceTestApi {
+  /** 濮斿妫�娴�/璇曢獙鍗� */
+  export interface OutsourceTest {
+    id?: number;
+    code?: string; // 鍗曟嵁缂栫爜
+    name?: string; // 鍗曟嵁鍚嶇О
+    testType?: number; // 妫�娴嬬被鍨嬶細1绗笁鏂规娴� 2濮斿璇曢獙
+    itemId?: number; // 妫�娴嬪璞$紪鍙�
+    itemName?: string; // 妫�娴嬪璞″悕绉�
+    testOrg?: string; // 妫�娴嬫満鏋�
+    testDate?: string; // 妫�娴嬫棩鏈�
+    result?: number; // 妫�娴嬬粨鏋滐細1鍚堟牸 2涓嶅悎鏍�
+    reportNo?: string; // 妫�娴嬫姤鍛婄紪鍙�
+    status?: number; // 鐘舵�侊細0鑽夌 10宸叉彁浜� 20宸插畬鎴�
+    remark?: string; // 澶囨敞
+    blobIds?: number[]; // 闄勪欢
+    attachmentList?: { id: number; name?: string; url?: string }[];
+    createTime?: string;
+  }
+}
+
+/** 鍒涘缓濮斿妫�娴�/璇曢獙鍗� */
+export function createTest(data: MesQcOutsourceTestApi.OutsourceTest) {
+  return requestClient.post('/mes/qc/outsource-test/create', data);
+}
+
+/** 鏇存柊濮斿妫�娴�/璇曢獙鍗� */
+export function updateTest(data: MesQcOutsourceTestApi.OutsourceTest) {
+  return requestClient.put('/mes/qc/outsource-test/update', data);
+}
+
+/** 鍒犻櫎濮斿妫�娴�/璇曢獙鍗� */
+export function deleteTest(ids: number[]) {
+  return requestClient.delete('/mes/qc/outsource-test/delete', { params: { ids: ids.join(',') } });
+}
+
+/** 鑾峰彇濮斿妫�娴�/璇曢獙鍗曡鎯� */
+export function getTest(id: number) {
+  return requestClient.get<MesQcOutsourceTestApi.OutsourceTest>(
+    `/mes/qc/outsource-test/get?id=${id}`,
+  );
+}
+
+/** 鑾峰彇濮斿妫�娴�/璇曢獙鍗曞垎椤� */
+export function getTestPage(params: PageParam) {
+  return requestClient.get<PageResult<MesQcOutsourceTestApi.OutsourceTest>>(
+    '/mes/qc/outsource-test/page',
+    { params },
+  );
+}
+
+/** 鎻愪氦濮斿妫�娴�/璇曢獙鍗� */
+export function submitTest(id: number) {
+  return requestClient.put(`/mes/qc/outsource-test/submit?id=${id}`);
+}
+
+/** 瀹屾垚濮斿妫�娴�/璇曢獙鍗� */
+export function finishTest(id: number) {
+  return requestClient.put(`/mes/qc/outsource-test/finish?id=${id}`);
+}
diff --git a/src/api/wls/productissue/index.ts b/src/api/wls/productissue/index.ts
index 5da6598..2767812 100644
--- a/src/api/wls/productissue/index.ts
+++ b/src/api/wls/productissue/index.ts
@@ -16,6 +16,7 @@
     clientCode?: string; // 瀹㈡埛缂栫爜
     clientName?: string; // 瀹㈡埛鍚嶇О
     requiredTime?: number; // 闇�姹傛椂闂�
+    issueType?: number; // 鍗曟嵁绫诲瀷锛�1=姝e父棰嗘枡锛�2=绱ф�ヨˉ鏂欙級
     status?: number; // 鍗曟嵁鐘舵��
     remark?: string; // 澶囨敞
     createTime?: Date; // 鍒涘缓鏃堕棿
diff --git a/src/api/wls/stocktaking/task/line/index.ts b/src/api/wls/stocktaking/task/line/index.ts
index ad51625..d305c80 100644
--- a/src/api/wls/stocktaking/task/line/index.ts
+++ b/src/api/wls/stocktaking/task/line/index.ts
@@ -36,6 +36,20 @@
   >('/mes/wm/stocktaking-task-line/page', { params });
 }
 
+/** 涓嬭浇鐩樼偣瀹炵洏鏁板鍏ユā鏉� */
+export function importStockTakingQuantityTemplate() {
+  return requestClient.download('/mes/wm/stocktaking-task-line/get-import-template');
+}
+
+/** 瀵煎叆鐩樼偣瀹炵洏鏁伴噺 */
+export function importStockTakingQuantity(taskId: number, file: File) {
+  return requestClient.upload<{
+    successCount?: number;
+    failureCount?: number;
+    failureMessages?: string[];
+  }>(`/mes/wm/stocktaking-task-line/import-excel?taskId=${taskId}`, { file });
+}
+
 /** 鏌ヨ鐩樼偣浠诲姟琛岀簿绠�鍒楄〃 */
 export function getStockTakingTaskLineSimpleList(taskId: number) {
   return requestClient.get<WlsStockTakingTaskLineApi.StockTakingTaskLine[]>(
diff --git a/src/api/wls/warehouse/area/index.ts b/src/api/wls/warehouse/area/index.ts
index 3190654..15adb86 100644
--- a/src/api/wls/warehouse/area/index.ts
+++ b/src/api/wls/warehouse/area/index.ts
@@ -8,6 +8,7 @@
     id?: number; // 搴撲綅缂栧彿
     code?: string; // 搴撲綅缂栫爜
     name?: string; // 搴撲綅鍚嶇О
+    type?: number; // 搴撳尯绫诲瀷
     warehouseId?: number; // 浠撳簱缂栧彿
     warehouseName?: string; // 浠撳簱鍚嶇О
     locationId?: number; // 搴撳尯缂栧彿
diff --git a/src/api/wls/warehouse/index.ts b/src/api/wls/warehouse/index.ts
index 661b62e..66ffebb 100644
--- a/src/api/wls/warehouse/index.ts
+++ b/src/api/wls/warehouse/index.ts
@@ -8,6 +8,7 @@
     id?: number; // 浠撳簱缂栧彿
     code?: string; // 浠撳簱缂栫爜
     name?: string; // 浠撳簱鍚嶇О
+    type?: number; // 浠撳簱绫诲瀷
     address?: string; // 鍦板潃
     area?: number; // 闈㈢Н
     chargeUserId?: number; // 璐熻矗浜�
diff --git a/src/components/table-action/icons.ts b/src/components/table-action/icons.ts
index 578265f..c56cb1b 100644
--- a/src/components/table-action/icons.ts
+++ b/src/components/table-action/icons.ts
@@ -13,4 +13,5 @@
   CLOSE: 'lucide:x',
   BOOK: 'lucide:book',
   AUDIT: 'lucide:file-check',
+  CALC: 'lucide:calculator',
 };
diff --git a/src/packages/constants/src/biz-mes-enum.ts b/src/packages/constants/src/biz-mes-enum.ts
index 85c58db..14f9650 100644
--- a/src/packages/constants/src/biz-mes-enum.ts
+++ b/src/packages/constants/src/biz-mes-enum.ts
@@ -249,12 +249,16 @@
   SELF: 1, // 鑷鐢熶骇
   OUTSOURCE: 2, // 浠e伐
   PURCHASE: 3, // 閲囪喘
+  POWER_OPERATION: 4, // 鐢靛姏杩愮淮
+  POWER_GENERATION: 5, // 鍙戠數
+  POWER_MAINTENANCE: 6, // 妫�淇�
 } as const;
 
 /** MES 宸ュ崟鏉ユ簮绫诲瀷鏋氫妇 */
 export const MesProWorkOrderSourceTypeEnum = {
   ORDER: 1, // 瀹㈡埛璁㈠崟
   STORE: 2, // 搴撳瓨澶囪揣
+  MAINTENANCE_PLAN: 3, // 杩愮淮璁″垝
 } as const;
 
 /** MES 鐢熶骇浠诲姟鐘舵�佹灇涓� */
diff --git a/src/packages/constants/src/dict-enum.ts b/src/packages/constants/src/dict-enum.ts
index 2a62e5a..46210bb 100644
--- a/src/packages/constants/src/dict-enum.ts
+++ b/src/packages/constants/src/dict-enum.ts
@@ -127,6 +127,18 @@
   CRM_SALES_TARGET_PERIOD_TYPE: 'crm_sales_target_period_type', // CRM 閿�鍞洰鏍囧懆鏈熺被鍨�
   CRM_SALES_TARGET_DIMENSION: 'crm_sales_target_dimension', // CRM 閿�鍞洰鏍囩淮搴�
   CRM_SALES_TARGET_STATUS: 'crm_sales_target_status', // CRM 閿�鍞洰鏍囩姸鎬�
+  CRM_CUSTOMER_TYPE: 'crm_customer_type', // CRM 瀹㈡埛鍒嗙被锛堢數鍔涳級
+  CRM_CREDIT_LEVEL: 'crm_credit_level', // CRM 瀹㈡埛淇$敤绛夌骇
+  CRM_QUOTATION_TYPE: 'crm_quotation_type', // CRM 鎶ヤ环鍗曠被鍨�
+  CRM_TARIFF_MODE: 'crm_tariff_mode', // CRM 鐢典环妯″紡
+  CRM_CONTRACT_TYPE: 'crm_contract_type', // CRM 鍚堝悓绫诲瀷
+  CRM_VOLTAGE_LEVEL: 'crm_voltage_level', // CRM 鐢靛帇绛夌骇
+  CRM_ELECTRICITY_TYPE: 'crm_electricity_type', // CRM 鐢ㄧ數绫诲瀷
+  CRM_METER_MODE: 'crm_meter_mode', // CRM 璁¢噺鏂瑰紡
+  CRM_COMMAND_TYPE: 'crm_command_type', // CRM 鍚堝悓鎸囦护绫诲瀷
+  CRM_COMMAND_STATUS: 'crm_command_status', // CRM 鍚堝悓鎸囦护鐘舵��
+  CRM_REFUND_TYPE: 'crm_refund_type', // CRM 閫�璐圭被鍨�
+  CRM_CANCELLATION_TYPE: 'crm_cancellation_type', // CRM 閿�鎴风被鍨�
 } as const;
 
 /** ========== AFTERSALES - 鍞悗鏈嶅姟妯″潡 ========== */
@@ -212,6 +224,13 @@
 /** ========== MES - 鍒堕�犳墽琛屾ā鍧� ========== */
 const MES_DICT = {
   MES_MD_ITEM_OR_PRODUCT: 'mes_md_item_or_product', // MES 鐗╂枡/浜у搧鏍囪瘑
+  MES_MATERIAL_CATEGORY: 'mes_material_category', // MES 鐢靛姏鐗╄祫鍒嗙被
+  MES_PRO_MAINTENANCE_PLAN_TYPE: 'mes_pro_maintenance_plan_type', // MES 鐢靛姏浣滀笟璁″垝绫诲瀷
+  MES_PRO_MAINTENANCE_PLAN_STATUS: 'mes_pro_maintenance_plan_status', // MES 鐢靛姏浣滀笟璁″垝鐘舵��
+  MES_QC_OUTSOURCE_TEST_TYPE: 'mes_qc_outsource_test_type', // MES 濮斿妫�娴嬬被鍨�
+  MES_QC_OUTSOURCE_TEST_RESULT: 'mes_qc_outsource_test_result', // MES 濮斿妫�娴嬬粨鏋�
+  MES_PRO_PROCESS_TYPE: 'mes_pro_process_type', // MES 鐢靛姏宸ヨ壓鍒嗙被
+  MES_PRO_EMERGENCY_PLAN_TYPE: 'mes_pro_emergency_plan_type', // MES 搴旀�ュ缃妗堢被鍨�
   MES_MD_AUTO_CODE_CYCLE_METHOD: 'mes_md_auto_code_cycle_method', // MES 缂栫爜瑙勫垯寰幆鏂瑰紡
   MES_MD_AUTO_CODE_PADDED_METHOD: 'mes_md_auto_code_padded_method', // MES 缂栫爜瑙勫垯琛ラ綈鏂瑰紡
   MES_MD_AUTO_CODE_PART_TYPE: 'mes_md_auto_code_part_type', // MES 缂栫爜瑙勫垯鍒嗘绫诲瀷
@@ -252,6 +271,10 @@
   MES_PD_VERSION_CHANGE_TYPE: 'mes_pd_version_change_type', // MES 鐗堟湰鍙樻洿绫诲瀷
   MES_PD_DOCUMENT_TYPE: 'mes_pd_document_type', // MES 璁捐璧勬枡绫诲瀷
   MES_PD_ARCHIVE_STATUS: 'mes_pd_archive_status', // MES 璁捐褰掓。鐘舵��
+  PRODUCT_CONDUCTOR_TYPE: 'product_conductor_type', // 鎹綅瀵肩嚎瀵间綋绫诲瀷
+  PRODUCT_REEL_TYPE: 'product_reel_type', // 鎹綅瀵肩嚎鐩樺叿绫诲瀷
+  PRODUCT_TECH_CONFIRM_TEMPLATE: 'product_tech_confirm_template', // 鎶�鏈‘璁ゅ嚱妯℃澘绫诲瀷
+  PRODUCT_TECH_CONFIRM_STATUS: 'product_tech_confirm_status', // 鎶�鏈‘璁ゅ嚱鐘舵��
   MES_PRO_FEEDBACK_CHANNEL: 'mes_pro_feedback_channel', // MES 鐢熶骇鎶ュ伐閫斿緞
   MES_PRO_ANDON_STATUS: 'mes_pro_andon_status', // MES 瀹夌伅澶勭疆鐘舵��
   MES_PRO_ANDON_LEVEL: 'mes_pro_andon_level', // MES 瀹夌伅绾у埆
@@ -270,6 +293,7 @@
   MES_WM_BARCODE_FORMAT: 'mes_wm_barcode_format', // MES 鏉$爜鏍煎紡
   MES_WM_PACKAGE_STATUS: 'mes_wm_package_status', // MES 瑁呯鍗曠姸鎬�
   MES_WM_PRODUCT_SALES_STATUS: 'mes_wm_product_sales_status', // MES 閿�鍞嚭搴撳崟鐘舵��
+  MES_WM_WAREHOUSE_TYPE: 'mes_wm_warehouse_type', // MES 浠撳簱/搴撳尯绫诲瀷
   MES_WM_QUALITY_STATUS: 'mes_wm_quality_status', // MES 璐ㄩ噺鐘舵��
   MES_WM_MISC_ISSUE_TYPE: 'mes_wm_misc_issue_type', // MES 鏉傞」鍑哄簱绫诲瀷
   MES_WM_MISC_ISSUE_STATUS: 'mes_wm_misc_issue_status', // MES 鏉傞」鍑哄簱鍗曠姸鎬�
@@ -285,6 +309,7 @@
   MES_WM_RETURN_ISSUE_STATUS: 'mes_wm_return_issue_status', // MES 鐢熶骇閫�鏂欏崟鐘舵��
   MES_WM_RETURN_ISSUE_TYPE: 'mes_wm_return_issue_type', // MES 閫�鏂欑被鍨�
   MES_WM_PRODUCT_ISSUE_STATUS: 'mes_wm_product_issue_status', // MES 棰嗘枡鍑哄簱鍗曠姸鎬�
+  MES_WM_PRODUCT_ISSUE_TYPE: 'mes_wm_product_issue_type', // MES 鐢熶骇棰嗘枡鍗曟嵁绫诲瀷
   MES_WM_PRODUCT_RECEIPT_STATUS: 'mes_wm_product_receipt_status', // MES 浜у搧鍏ュ簱鍗曠姸鎬�
   MES_WM_STOCK_TAKING_TYPE: 'mes_wm_stock_taking_type', // MES 鐩樼偣绫诲瀷
   MES_WM_STOCK_TAKING_TASK_STATUS: 'mes_wm_stock_taking_task_status', // MES 鐩樼偣浠诲姟鐘舵��
@@ -296,6 +321,12 @@
   MES_PD_TEMPLATE_TYPE: 'mes_pd_template_type', // MES 宸ヨ壓妯℃澘绫诲瀷
   MES_NCR_STATUS: 'mes_ncr_status', // MES 涓嶅悎鏍煎搧澶勭悊鍗曠姸鎬�
   MES_NCR_DISPOSITION: 'mes_ncr_disposition', // MES 涓嶅悎鏍煎搧澶勭疆鏂瑰紡
+  MES_PD_PRODUCT_ARCHIVE_TYPE: 'mes_pd_product_archive_type', // MES 浜у搧妗f绫诲瀷
+  MES_PD_AUDIT_STATUS: 'mes_pd_audit_status', // MES 浜у搧璁捐瀹℃壒鐘舵��
+  MES_PD_PRODUCT_STATUS: 'mes_pd_product_status', // MES 浜у搧妗f鍚敤鐘舵��
+  MES_PD_PRICE_OBJECT_TYPE: 'mes_pd_price_object_type', // MES 瀹氫环瀵硅薄绫诲瀷
+  MES_PD_DISCOUNT_POLICY_TYPE: 'mes_pd_discount_policy_type', // MES 浼樻儬绛栫暐绫诲瀷
+  MES_PD_TIERED_PRICE_STATUS: 'mes_pd_tiered_price_status', // MES 闃舵鐢典环鍚敤鐘舵��
 } as const;
 
 /** ========== WMS - 浠撳偍绠$悊妯″潡 ========== */
@@ -317,6 +348,7 @@
 const HRM_DICT = {
   HRM_DEGREE: 'hrm_degree', // HRM 瀛﹀巻
   HRM_EMPLOYEE_STATUS: 'hrm_employee_status', // HRM 鍛樺伐鐘舵��
+  HRM_CERTIFICATE_TYPE: 'hrm_certificate_type', // HRM 璇佷功璧勮川绫诲瀷
   HRM_GENDER: 'hrm_gender', // HRM 鎬у埆
   HRM_AUDIT_STATUS: 'hrm_audit_status', // HRM 瀹℃壒鐘舵��
   HRM_LEAVE_TYPE: 'hrm_leave_type', // HRM 璇峰亣绫诲瀷
diff --git a/src/views/aftersales/ticket/data.ts b/src/views/aftersales/ticket/data.ts
index 15c83cc..50066ad 100644
--- a/src/views/aftersales/ticket/data.ts
+++ b/src/views/aftersales/ticket/data.ts
@@ -25,6 +25,10 @@
   { label: '涓�鑸棶棰�', value: 1 },
   { label: '缁翠慨闂', value: 2 },
   { label: '閫�璐ч棶棰�', value: 3 },
+  { label: '鍋滅數鎶ヤ慨', value: 4 },
+  { label: '鐢ㄧ數鏁呴殰', value: 5 },
+  { label: '鏈嶅姟鎶曡瘔', value: 6 },
+  { label: '渚涚數璐ㄩ噺寮傝', value: 7 },
 ];
 
 /** 涓ラ噸绋嬪害閫夐」 */
diff --git a/src/views/crm/cancellation/data.ts b/src/views/crm/cancellation/data.ts
new file mode 100644
index 0000000..a05ece5
--- /dev/null
+++ b/src/views/crm/cancellation/data.ts
@@ -0,0 +1,227 @@
+import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { CrmCancellationApi } from '#/api/crm/cancellation';
+import type { CrmCustomerApi } from '#/api/crm/customer';
+
+import { markRaw } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import CrmCustomerSelect from '#/components/crm-customer-select.vue';
+
+/** 鏂板/淇敼閿�鎴风敵璇� */
+export function useFormSchema(
+  formApi?: VbenFormApi,
+): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: {
+        triggerFields: [''],
+        show: () => false,
+      },
+    },
+    {
+      fieldName: 'cancellationNo',
+      label: '閿�鎴风紪鍙�',
+      component: 'Input',
+      componentProps: {
+        placeholder: '涓嶅~鍒欒嚜鍔ㄧ敓鎴�',
+      },
+    },
+    {
+      fieldName: 'customerId',
+      label: '瀹㈡埛',
+      component: markRaw(CrmCustomerSelect),
+      componentProps: {
+        placeholder: '璇烽�夋嫨瀹㈡埛',
+        onChange: (item?: CrmCustomerApi.Customer) => {
+          formApi?.setFieldValue('customerName', item?.name);
+        },
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'customerName',
+      label: '瀹㈡埛鍚嶇О',
+      component: 'Input',
+      componentProps: {
+        placeholder: '瀹㈡埛鍚嶇О',
+        readonly: true,
+      },
+    },
+    {
+      fieldName: 'cancellationType',
+      label: '閿�鎴风被鍨�',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: getDictOptions(DICT_TYPE.CRM_CANCELLATION_TYPE),
+        placeholder: '璇烽�夋嫨閿�鎴风被鍨�',
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'contractNo',
+      label: '鍚堝悓缂栧彿',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ叧鑱斿悎鍚岀紪鍙�',
+      },
+    },
+    {
+      fieldName: 'expectedCancelDate',
+      label: '棰勮閿�鎴锋棩鏈�',
+      component: 'DatePicker',
+      componentProps: {
+        class: '!w-full',
+        format: 'YYYY-MM-DD',
+        valueFormat: 'YYYY-MM-DD',
+      },
+    },
+    {
+      fieldName: 'actualCancelDate',
+      label: '瀹為檯閿�鎴锋棩鏈�',
+      component: 'DatePicker',
+      componentProps: {
+        class: '!w-full',
+        format: 'YYYY-MM-DD',
+        valueFormat: 'YYYY-MM-DD',
+      },
+    },
+    {
+      fieldName: 'refundAmount',
+      label: '閫�璐归噾棰�',
+      component: 'InputNumber',
+      componentProps: {
+        class: 'w-full',
+        min: 0,
+        precision: 2,
+        placeholder: '璇疯緭鍏ラ��璐归噾棰濓紙鍏冿級',
+      },
+    },
+    {
+      fieldName: 'applyUserName',
+      label: '鐢宠浜�',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ョ敵璇蜂汉濮撳悕',
+      },
+    },
+    {
+      fieldName: 'applyTime',
+      label: '鐢宠鏃堕棿',
+      component: 'DatePicker',
+      componentProps: {
+        class: '!w-full',
+        format: 'YYYY-MM-DD HH:mm:ss',
+        showTime: true,
+        valueFormat: 'YYYY-MM-DD HH:mm:ss',
+      },
+    },
+    {
+      fieldName: 'cancelReason',
+      label: '閿�鎴峰師鍥�',
+      component: 'Textarea',
+      formItemClass: 'col-span-2',
+      componentProps: {
+        placeholder: '璇疯緭鍏ラ攢鎴峰師鍥�',
+        rows: 2,
+      },
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      formItemClass: 'col-span-2',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ娉�',
+        rows: 2,
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'cancellationNo',
+      label: '閿�鎴风紪鍙�',
+      component: 'Input',
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇疯緭鍏ラ攢鎴风紪鍙�',
+      },
+    },
+    {
+      fieldName: 'cancellationType',
+      label: '閿�鎴风被鍨�',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: getDictOptions(DICT_TYPE.CRM_CANCELLATION_TYPE),
+        placeholder: '璇烽�夋嫨閿�鎴风被鍨�',
+      },
+    },
+    {
+      fieldName: 'auditStatus',
+      label: '瀹℃壒鐘舵��',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: getDictOptions(DICT_TYPE.CRM_AUDIT_STATUS, 'number'),
+        placeholder: '璇烽�夋嫨瀹℃壒鐘舵��',
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions<CrmCancellationApi.Cancellation>['columns'] {
+  return [
+    { field: 'cancellationNo', title: '閿�鎴风紪鍙�', minWidth: 170 },
+    { field: 'customerName', title: '瀹㈡埛鍚嶇О', minWidth: 180 },
+    {
+      field: 'cancellationType',
+      title: '閿�鎴风被鍨�',
+      width: 110,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.CRM_CANCELLATION_TYPE },
+      },
+    },
+    { field: 'contractNo', title: '鍚堝悓缂栧彿', minWidth: 140 },
+    {
+      field: 'expectedCancelDate',
+      title: '棰勮閿�鎴锋棩鏈�',
+      width: 120,
+      formatter: 'formatDate',
+    },
+    {
+      field: 'actualCancelDate',
+      title: '瀹為檯閿�鎴锋棩鏈�',
+      width: 120,
+      formatter: 'formatDate',
+    },
+    { field: 'refundAmount', title: '閫�璐归噾棰�', width: 120 },
+    { field: 'applyUserName', title: '鐢宠浜�', width: 100 },
+    {
+      field: 'auditStatus',
+      title: '瀹℃壒鐘舵��',
+      width: 110,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.CRM_AUDIT_STATUS },
+      },
+    },
+    {
+      title: '鎿嶄綔',
+      width: 200,
+      fixed: 'right',
+      slots: { default: 'actions' },
+    },
+  ];
+}
diff --git a/src/views/crm/cancellation/index.vue b/src/views/crm/cancellation/index.vue
new file mode 100644
index 0000000..731d4e1
--- /dev/null
+++ b/src/views/crm/cancellation/index.vue
@@ -0,0 +1,189 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { CrmCancellationApi } from '#/api/crm/cancellation';
+
+import { Page, useVbenModal } from '@vben/common-ui';
+import { downloadFileFromBlobPart } from '@vben/utils';
+
+import { message } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+  auditCancellation,
+  deleteCancellation,
+  exportCancellation,
+  getCancellationPage,
+} from '#/api/crm/cancellation';
+import { $t } from '#/locales';
+
+import { useGridColumns, useGridFormSchema } from './data';
+import Form from './modules/form.vue';
+
+const [FormModal, formModalApi] = useVbenModal({
+  connectedComponent: Form,
+  destroyOnClose: true,
+});
+
+/** 鍒锋柊琛ㄦ牸 */
+function handleRefresh() {
+  gridApi.query();
+}
+
+/** 瀵煎嚭琛ㄦ牸 */
+async function handleExport() {
+  const formValues = await gridApi.formApi.getValues();
+  const data = await exportCancellation(formValues);
+  downloadFileFromBlobPart({ fileName: '閿�鎴风敵璇�.xls', source: data });
+}
+
+/** 鏂板 */
+function handleCreate() {
+  formModalApi.setData(null).open();
+}
+
+/** 缂栬緫 */
+function handleEdit(row: CrmCancellationApi.Cancellation) {
+  formModalApi.setData({ cancellation: row }).open();
+}
+
+/** 鍒犻櫎 */
+async function handleDelete(row: CrmCancellationApi.Cancellation) {
+  const hideLoading = message.loading({
+    content: $t('ui.actionMessage.deleting', [row.cancellationNo ?? '']),
+    duration: 0,
+  });
+  try {
+    await deleteCancellation(row.id!);
+    message.success(
+      $t('ui.actionMessage.deleteSuccess', [row.cancellationNo ?? '']),
+    );
+    handleRefresh();
+  } finally {
+    hideLoading();
+  }
+}
+
+/** 瀹℃壒锛堥�氳繃/椹冲洖锛� */
+async function handleAudit(
+  row: CrmCancellationApi.Cancellation,
+  pass: boolean,
+) {
+  const hideLoading = message.loading({
+    content: pass ? '瀹℃壒閫氳繃涓�...' : '瀹℃壒椹冲洖涓�...',
+    duration: 0,
+  });
+  try {
+    await auditCancellation({ id: row.id!, auditStatus: pass ? 20 : 30 });
+    message.success(pass ? '瀹℃壒閫氳繃' : '瀹℃壒椹冲洖');
+    handleRefresh();
+  } finally {
+    hideLoading();
+  }
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  formOptions: {
+    schema: useGridFormSchema(),
+  },
+  gridOptions: {
+    columns: useGridColumns(),
+    height: 'auto',
+    keepSource: true,
+    proxyConfig: {
+      ajax: {
+        query: async ({ page }, formValues) =>
+          await getCancellationPage({
+            pageNo: page.currentPage,
+            pageSize: page.pageSize,
+            ...formValues,
+          }),
+      },
+    },
+    rowConfig: {
+      keyField: 'id',
+      isHover: true,
+    },
+    toolbarConfig: {
+      refresh: true,
+      search: true,
+    },
+  } as VxeTableGridOptions<CrmCancellationApi.Cancellation>,
+});
+</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: ['crm:cancellation:create'],
+              onClick: handleCreate,
+            },
+            {
+              label: $t('ui.actionTitle.export'),
+              type: 'primary',
+              icon: ACTION_ICON.DOWNLOAD,
+              auth: ['crm:cancellation:export'],
+              onClick: handleExport,
+            },
+          ]"
+        />
+      </template>
+      <template #actions="{ row }">
+        <TableAction
+          :actions="[
+            {
+              label: '閫氳繃',
+              type: 'link',
+              icon: ACTION_ICON.AUDIT,
+              auth: ['crm:cancellation:update'],
+              ifShow: row.auditStatus === 0 || row.auditStatus === 10,
+              popConfirm: {
+                title: '纭瀹℃壒閫氳繃璇ラ攢鎴风敵璇凤紵',
+                confirm: handleAudit.bind(null, row, true),
+              },
+            },
+            {
+              label: '椹冲洖',
+              type: 'link',
+              danger: true,
+              icon: ACTION_ICON.AUDIT,
+              auth: ['crm:cancellation:update'],
+              ifShow: row.auditStatus === 0 || row.auditStatus === 10,
+              popConfirm: {
+                title: '纭椹冲洖璇ラ攢鎴风敵璇凤紵',
+                confirm: handleAudit.bind(null, row, false),
+              },
+            },
+            {
+              label: $t('common.edit'),
+              type: 'link',
+              icon: ACTION_ICON.EDIT,
+              auth: ['crm:cancellation:update'],
+              onClick: handleEdit.bind(null, row),
+            },
+            {
+              label: $t('common.delete'),
+              type: 'link',
+              danger: true,
+              icon: ACTION_ICON.DELETE,
+              auth: ['crm:cancellation:delete'],
+              popConfirm: {
+                title: $t('ui.actionMessage.deleteConfirm', [
+                  row.cancellationNo ?? '',
+                ]),
+                confirm: handleDelete.bind(null, row),
+              },
+            },
+          ]"
+        />
+      </template>
+    </Grid>
+  </Page>
+</template>
diff --git a/src/views/crm/cancellation/modules/form.vue b/src/views/crm/cancellation/modules/form.vue
new file mode 100644
index 0000000..5a7fe02
--- /dev/null
+++ b/src/views/crm/cancellation/modules/form.vue
@@ -0,0 +1,95 @@
+<script lang="ts" setup>
+import type { CrmCancellationApi } from '#/api/crm/cancellation';
+
+import { computed, ref } from 'vue';
+
+import { useVbenForm, useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import {
+  createCancellation,
+  getCancellation,
+  updateCancellation,
+} from '#/api/crm/cancellation';
+import { $t } from '#/locales';
+
+import { useFormSchema } from '../data';
+
+type CancellationFormModalData = {
+  cancellation?: Pick<CrmCancellationApi.Cancellation, 'id'>;
+};
+
+const emit = defineEmits(['success']);
+const formData = ref<Partial<CrmCancellationApi.Cancellation>>();
+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: 110,
+  },
+  wrapperClass: 'grid-cols-2',
+  layout: 'horizontal',
+  schema: [],
+  showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    const { valid } = await formApi.validate();
+    if (!valid) {
+      return;
+    }
+    modalApi.lock();
+    const data =
+      (await formApi.getValues()) as CrmCancellationApi.Cancellation;
+    if (formData.value?.id) {
+      data.id = formData.value.id;
+    }
+    try {
+      await (formData.value?.id
+        ? updateCancellation(data)
+        : createCancellation(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;
+    }
+    formApi.setState({ schema: useFormSchema(formApi) });
+    await formApi.resetForm();
+    const data = modalApi.getData() as null | CancellationFormModalData;
+    if (!data?.cancellation?.id) {
+      return;
+    }
+    modalApi.lock();
+    try {
+      formData.value = await getCancellation(data.cancellation.id);
+      if (formData.value) {
+        await formApi.setValues(formData.value as any);
+      }
+    } finally {
+      modalApi.unlock();
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal :title="getTitle" class="w-3/5">
+    <Form class="mx-4" />
+  </Modal>
+</template>
diff --git a/src/views/crm/contract/components/command-form-data.ts b/src/views/crm/contract/components/command-form-data.ts
new file mode 100644
index 0000000..0a50d56
--- /dev/null
+++ b/src/views/crm/contract/components/command-form-data.ts
@@ -0,0 +1,105 @@
+import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+/** 鏂板/淇敼鍚堝悓鎸囦护 */
+export function useFormSchema(
+  formApi?: VbenFormApi,
+): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: {
+        triggerFields: [''],
+        show: () => false,
+      },
+    },
+    {
+      fieldName: 'contractId',
+      label: '鍚堝悓ID',
+      component: 'InputNumber',
+      componentProps: {
+        class: 'w-full',
+        precision: 0,
+        placeholder: '鍚堝悓ID',
+      },
+      dependencies: {
+        triggerFields: [''],
+        show: () => false,
+      },
+    },
+    {
+      fieldName: 'contractNo',
+      label: '鍚堝悓缂栧彿',
+      component: 'Input',
+      componentProps: {
+        placeholder: '鍚堝悓缂栧彿',
+        readonly: true,
+      },
+    },
+    {
+      fieldName: 'commandType',
+      label: '鎸囦护绫诲瀷',
+      component: 'Select',
+      componentProps: {
+        options: getDictOptions(DICT_TYPE.CRM_COMMAND_TYPE),
+        placeholder: '璇烽�夋嫨鎸囦护绫诲瀷',
+        allowClear: true,
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'targetModule',
+      label: '鐩爣妯″潡',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: [
+          { label: 'MES 宸ュ崟', value: 'mes-work-order' },
+          { label: 'MES 鎶勮〃', value: 'mes-metering' },
+          { label: 'SRM 鏈嶅姟鍗�', value: 'srm-service' },
+        ],
+        placeholder: '璇烽�夋嫨鐩爣妯″潡',
+      },
+    },
+    {
+      fieldName: 'bizNo',
+      label: '涓氬姟鍗曞彿',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ヤ笟鍔″崟鍙�',
+      },
+    },
+    {
+      fieldName: 'operatorName',
+      label: '鎿嶄綔浜�',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ユ搷浣滀汉濮撳悕',
+      },
+    },
+    {
+      fieldName: 'operateTime',
+      label: '鎿嶄綔鏃堕棿',
+      component: 'DatePicker',
+      componentProps: {
+        class: '!w-full',
+        format: 'YYYY-MM-DD HH:mm:ss',
+        showTime: true,
+        valueFormat: 'YYYY-MM-DD HH:mm:ss',
+      },
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      formItemClass: 'col-span-2',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ娉�',
+        rows: 2,
+      },
+    },
+  ];
+}
diff --git a/src/views/crm/contract/components/command-form.vue b/src/views/crm/contract/components/command-form.vue
new file mode 100644
index 0000000..8a64bac
--- /dev/null
+++ b/src/views/crm/contract/components/command-form.vue
@@ -0,0 +1,90 @@
+<script lang="ts" setup>
+import type { CrmContractCommandApi } from '#/api/crm/contract-command';
+
+import { ref } from 'vue';
+
+import { useVbenForm, useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import {
+  createContractCommand,
+  updateContractCommand,
+} from '#/api/crm/contract-command';
+import { $t } from '#/locales';
+
+import { useFormSchema } from './command-form-data';
+
+const emit = defineEmits(['success']);
+
+const formData = ref<Partial<CrmContractCommandApi.ContractCommand>>();
+const getTitle = () => {
+  return formData.value?.id
+    ? $t('ui.actionTitle.edit', ['鎸囦护'])
+    : $t('ui.actionTitle.create', ['鎸囦护']);
+};
+
+const [Form, formApi] = useVbenForm({
+  commonConfig: {
+    componentProps: {
+      class: 'w-full',
+    },
+    labelWidth: 110,
+  },
+  wrapperClass: 'grid-cols-2',
+  layout: 'horizontal',
+  schema: [],
+  showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    const { valid } = await formApi.validate();
+    if (!valid) {
+      return;
+    }
+    modalApi.lock();
+    const data =
+      (await formApi.getValues()) as CrmContractCommandApi.ContractCommand;
+    if (formData.value?.id) {
+      data.id = formData.value.id;
+    }
+    try {
+      await (formData.value?.id
+        ? updateContractCommand(data)
+        : createContractCommand(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;
+    }
+    formApi.setState({ schema: useFormSchema(formApi) });
+    await formApi.resetForm();
+    const data = modalApi.getData() as null | {
+      command?: CrmContractCommandApi.ContractCommand;
+      contractNo?: string;
+    };
+    if (data?.contractNo) {
+      await formApi.setFieldValue('contractNo', data.contractNo);
+    }
+    if (!data?.command?.id) {
+      return;
+    }
+    formData.value = data.command;
+    await formApi.setValues(formData.value as any);
+  },
+});
+</script>
+
+<template>
+  <Modal :title="getTitle()" class="w-2/3">
+    <Form class="mx-4" />
+  </Modal>
+</template>
diff --git a/src/views/crm/contract/components/command-list-data.ts b/src/views/crm/contract/components/command-list-data.ts
new file mode 100644
index 0000000..04b43ad
--- /dev/null
+++ b/src/views/crm/contract/components/command-list-data.ts
@@ -0,0 +1,55 @@
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { CrmContractCommandApi } from '#/api/crm/contract-command';
+
+import { DICT_TYPE } from '@vben/constants';
+
+/** 鍚堝悓鎸囦护璇︽儏鍒楄〃瀛楁 */
+export function useDetailListColumns(): VxeTableGridOptions<CrmContractCommandApi.ContractCommand>['columns'] {
+  return [
+    {
+      field: 'commandType',
+      title: '鎸囦护绫诲瀷',
+      width: 120,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.CRM_COMMAND_TYPE },
+      },
+    },
+    {
+      field: 'targetModule',
+      title: '鐩爣妯″潡',
+      width: 130,
+    },
+    {
+      field: 'bizNo',
+      title: '涓氬姟鍗曞彿',
+      minWidth: 140,
+    },
+    {
+      field: 'operatorName',
+      title: '鎿嶄綔浜�',
+      width: 100,
+    },
+    {
+      field: 'operateTime',
+      title: '鎿嶄綔鏃堕棿',
+      minWidth: 160,
+      formatter: 'formatDateTime',
+    },
+    {
+      field: 'status',
+      title: '鐘舵��',
+      width: 100,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.CRM_COMMAND_STATUS },
+      },
+    },
+    {
+      title: '鎿嶄綔',
+      width: 220,
+      fixed: 'right',
+      slots: { default: 'actions' },
+    },
+  ];
+}
diff --git a/src/views/crm/contract/components/command-list.vue b/src/views/crm/contract/components/command-list.vue
new file mode 100644
index 0000000..bea274d
--- /dev/null
+++ b/src/views/crm/contract/components/command-list.vue
@@ -0,0 +1,188 @@
+<!-- 鍚堝悓鎸囦护涓嬪彂鍒楄〃锛氱敤浜庛�愬悎鍚屻�戣鎯� Tab锛屽睍绀哄苟涓嬪彂/鎵ц/鍙栨秷鍚堝悓鎸囦护 -->
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { CrmContractCommandApi } from '#/api/crm/contract-command';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+  cancelContractCommand,
+  deleteContractCommand,
+  executeContractCommand,
+  getContractCommandPage,
+} from '#/api/crm/contract-command';
+import { $t } from '#/locales';
+
+import CommandForm from './command-form.vue';
+import { useDetailListColumns } from './command-list-data';
+
+const props = defineProps<{
+  contractId?: number; // 鍚堝悓缂栧彿
+  contractNo?: string; // 鍚堝悓鍗曞彿
+}>();
+
+const [FormModal, formModalApi] = useVbenModal({
+  connectedComponent: CommandForm,
+  destroyOnClose: true,
+});
+
+/** 鍒锋柊琛ㄦ牸 */
+function handleRefresh() {
+  gridApi.query();
+}
+
+/** 鍒涘缓鎸囦护 */
+function handleCreate() {
+  formModalApi
+    .setData({ contractNo: props.contractNo })
+    .open();
+}
+
+/** 缂栬緫鎸囦护 */
+function handleEdit(row: CrmContractCommandApi.ContractCommand) {
+  formModalApi.setData({ command: row }).open();
+}
+
+/** 鎵ц鎸囦护 */
+async function handleExecute(row: CrmContractCommandApi.ContractCommand) {
+  const hideLoading = message.loading({ content: '鎸囦护鎵ц涓�...', duration: 0 });
+  try {
+    await executeContractCommand(row.id!);
+    message.success('鎸囦护鎵ц瀹屾垚');
+    handleRefresh();
+  } finally {
+    hideLoading();
+  }
+}
+
+/** 鍙栨秷鎸囦护 */
+async function handleCancel(row: CrmContractCommandApi.ContractCommand) {
+  const hideLoading = message.loading({ content: '鎸囦护鍙栨秷涓�...', duration: 0 });
+  try {
+    await cancelContractCommand(row.id!);
+    message.success('鎸囦护宸插彇娑�');
+    handleRefresh();
+  } finally {
+    hideLoading();
+  }
+}
+
+/** 鍒犻櫎鎸囦护 */
+async function handleDelete(row: CrmContractCommandApi.ContractCommand) {
+  const hideLoading = message.loading({
+    content: $t('ui.actionMessage.deleting', [row.bizNo ?? '']),
+    duration: 0,
+  });
+  try {
+    await deleteContractCommand(row.id!);
+    message.success($t('ui.actionMessage.deleteSuccess', [row.bizNo ?? '']));
+    handleRefresh();
+  } finally {
+    hideLoading();
+  }
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  gridOptions: {
+    columns: useDetailListColumns(),
+    height: 400,
+    keepSource: true,
+    proxyConfig: {
+      ajax: {
+        query: async ({ page }) => {
+          if (!props.contractId) {
+            return { list: [], total: 0 };
+          }
+          return await getContractCommandPage({
+            pageNo: page.currentPage,
+            pageSize: page.pageSize,
+            contractId: props.contractId,
+          });
+        },
+      },
+    },
+    rowConfig: {
+      keyField: 'id',
+      isHover: true,
+    },
+    toolbarConfig: {
+      refresh: true,
+      search: false,
+    },
+  } as VxeTableGridOptions<CrmContractCommandApi.ContractCommand>,
+});
+</script>
+
+<template>
+  <div>
+    <FormModal @success="handleRefresh" />
+    <Grid>
+      <template #toolbar-tools>
+        <TableAction
+          :actions="[
+            {
+              label: '涓嬪彂鎸囦护',
+              type: 'primary',
+              icon: ACTION_ICON.ADD,
+              auth: ['crm:contract-command:create'],
+              onClick: handleCreate,
+            },
+          ]"
+        />
+      </template>
+      <template #actions="{ row }">
+        <TableAction
+          :actions="[
+            {
+              label: '鎵ц',
+              type: 'link',
+              icon: ACTION_ICON.AUDIT,
+              auth: ['crm:contract-command:update'],
+              ifShow: row.status === 0 || row.status === 10,
+              popConfirm: {
+                title: '纭鎵ц璇ユ寚浠わ紵',
+                confirm: handleExecute.bind(null, row),
+              },
+            },
+            {
+              label: '鍙栨秷',
+              type: 'link',
+              danger: true,
+              icon: ACTION_ICON.CLOSE,
+              auth: ['crm:contract-command:update'],
+              ifShow: row.status === 0 || row.status === 10,
+              popConfirm: {
+                title: '纭鍙栨秷璇ユ寚浠わ紵',
+                confirm: handleCancel.bind(null, row),
+              },
+            },
+            {
+              label: $t('common.edit'),
+              type: 'link',
+              icon: ACTION_ICON.EDIT,
+              auth: ['crm:contract-command:update'],
+              ifShow: row.status === 0,
+              onClick: handleEdit.bind(null, row),
+            },
+            {
+              label: $t('common.delete'),
+              type: 'link',
+              danger: true,
+              icon: ACTION_ICON.DELETE,
+              auth: ['crm:contract-command:delete'],
+              popConfirm: {
+                title: $t('ui.actionMessage.deleteConfirm', [
+                  row.bizNo ?? '',
+                ]),
+                confirm: handleDelete.bind(null, row),
+              },
+            },
+          ]"
+        />
+      </template>
+    </Grid>
+  </div>
+</template>
diff --git a/src/views/crm/contract/data.ts b/src/views/crm/contract/data.ts
index 7dabc09..87bfcf2 100644
--- a/src/views/crm/contract/data.ts
+++ b/src/views/crm/contract/data.ts
@@ -1,6 +1,8 @@
 import type { VbenFormSchema } from '#/adapter/form';
 import type { VxeTableGridOptions } from '#/adapter/vxe-table';
 
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
 import { useUserStore } from '@vben/stores';
 import { erpPriceInputFormatter, erpPriceMultiply } from '@vben/utils';
 
@@ -199,6 +201,78 @@
       },
     },
     {
+      fieldName: 'contractType',
+      label: '鍚堝悓绫诲瀷',
+      component: 'Select',
+      componentProps: {
+        options: getDictOptions(DICT_TYPE.CRM_CONTRACT_TYPE),
+        placeholder: '璇烽�夋嫨鍚堝悓绫诲瀷',
+        allowClear: true,
+      },
+    },
+    {
+      fieldName: 'voltageLevel',
+      label: '鐢靛帇绛夌骇',
+      component: 'Select',
+      componentProps: {
+        options: getDictOptions(DICT_TYPE.CRM_VOLTAGE_LEVEL),
+        placeholder: '璇烽�夋嫨鐢靛帇绛夌骇',
+        allowClear: true,
+      },
+    },
+    {
+      fieldName: 'electricityType',
+      label: '鐢ㄧ數绫诲瀷',
+      component: 'Select',
+      componentProps: {
+        options: getDictOptions(DICT_TYPE.CRM_ELECTRICITY_TYPE),
+        placeholder: '璇烽�夋嫨鐢ㄧ數绫诲瀷',
+        allowClear: true,
+      },
+    },
+    {
+      fieldName: 'contractCapacity',
+      label: '鍚堝悓瀹归噺锛坘VA锛�',
+      component: 'InputNumber',
+      componentProps: {
+        class: '!w-full',
+        min: 0,
+        precision: 2,
+        placeholder: '璇疯緭鍏ュ悎鍚屽閲�',
+      },
+    },
+    {
+      fieldName: 'tariffMode',
+      label: '鐢典环妯″紡',
+      component: 'Select',
+      componentProps: {
+        options: getDictOptions(DICT_TYPE.CRM_TARIFF_MODE),
+        placeholder: '璇烽�夋嫨鐢典环妯″紡',
+        allowClear: true,
+      },
+    },
+    {
+      fieldName: 'meterMode',
+      label: '璁¢噺鏂瑰紡',
+      component: 'Select',
+      componentProps: {
+        options: getDictOptions(DICT_TYPE.CRM_METER_MODE),
+        placeholder: '璇烽�夋嫨璁¢噺鏂瑰紡',
+        allowClear: true,
+      },
+    },
+    {
+      fieldName: 'payCycle',
+      label: '缂磋垂鍛ㄦ湡锛堟湀锛�',
+      component: 'InputNumber',
+      componentProps: {
+        class: '!w-full',
+        min: 1,
+        precision: 0,
+        placeholder: '璇疯緭鍏ョ即璐瑰懆鏈�',
+      },
+    },
+    {
       fieldName: 'remark',
       label: '澶囨敞',
       component: 'Textarea',
@@ -353,6 +427,34 @@
       slots: { default: 'businessName' },
     },
     {
+      title: '鍚堝悓绫诲瀷',
+      field: 'contractType',
+      minWidth: 110,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.CRM_CONTRACT_TYPE },
+      },
+    },
+    {
+      title: '鐢靛帇绛夌骇',
+      field: 'voltageLevel',
+      minWidth: 90,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.CRM_VOLTAGE_LEVEL },
+      },
+    },
+    {
+      title: '鍚堝悓瀹归噺',
+      field: 'contractCapacity',
+      minWidth: 100,
+    },
+    {
+      title: '鎸囦护鏁�',
+      field: 'commandCount',
+      minWidth: 80,
+    },
+    {
       title: '鍚堝悓閲戦锛堝厓锛�',
       field: 'totalPrice',
       minWidth: 140,
diff --git a/src/views/crm/contract/detail/index.vue b/src/views/crm/contract/detail/index.vue
index 972e42f..a5258db 100644
--- a/src/views/crm/contract/detail/index.vue
+++ b/src/views/crm/contract/detail/index.vue
@@ -26,6 +26,7 @@
 import Form from '../modules/form.vue';
 import { useDetailSchema } from './data';
 import ContractDetailsInfo from './modules/info.vue';
+import CommandDetailsList from '../components/command-list.vue';
 
 const props = defineProps<{ id?: number }>();
 
@@ -171,6 +172,13 @@
         <Tabs.TabPane tab="鎿嶄綔鏃ュ織" key="6" :force-render="true">
           <OperateLog :log-list="logList" />
         </Tabs.TabPane>
+        <Tabs.TabPane tab="鎸囦护涓嬪彂" key="7" :force-render="true">
+          <CommandDetailsList
+            v-if="contractId"
+            :contract-id="contractId"
+            :contract-no="contract.no"
+          />
+        </Tabs.TabPane>
       </Tabs>
     </Card>
   </Page>
diff --git a/src/views/crm/credit-rating/data.ts b/src/views/crm/credit-rating/data.ts
new file mode 100644
index 0000000..e8e59c2
--- /dev/null
+++ b/src/views/crm/credit-rating/data.ts
@@ -0,0 +1,212 @@
+import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { CrmCustomerApi } from '#/api/crm/customer';
+import type { CrmCreditRatingApi } from '#/api/crm/credit-rating';
+
+import { markRaw } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import CrmCustomerSelect from '#/components/crm-customer-select.vue';
+
+/** 鏂板/淇敼淇$敤璇勭骇 */
+export function useFormSchema(
+  formApi?: VbenFormApi,
+): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: {
+        triggerFields: [''],
+        show: () => false,
+      },
+    },
+    {
+      fieldName: 'customerId',
+      label: '瀹㈡埛',
+      component: markRaw(CrmCustomerSelect),
+      componentProps: {
+        placeholder: '璇烽�夋嫨瀹㈡埛',
+        onChange: (item?: CrmCustomerApi.Customer) => {
+          formApi?.setFieldValue('customerName', item?.name);
+        },
+      },
+    },
+    {
+      fieldName: 'customerName',
+      label: '瀹㈡埛鍚嶇О',
+      component: 'Input',
+      componentProps: {
+        placeholder: '瀹㈡埛鍚嶇О',
+        readonly: true,
+      },
+    },
+    {
+      fieldName: 'level',
+      label: '淇$敤绛夌骇',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: getDictOptions(DICT_TYPE.CRM_CREDIT_LEVEL),
+        placeholder: '璇烽�夋嫨淇$敤绛夌骇',
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'score',
+      label: '淇$敤鍒�',
+      component: 'InputNumber',
+      componentProps: {
+        class: 'w-full',
+        min: 0,
+        max: 100,
+        precision: 0,
+        placeholder: '0-100',
+      },
+    },
+    {
+      fieldName: 'evalType',
+      label: '璇勫畾绫诲瀷',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: [
+          { label: '鍒濊瘎', value: 'INITIAL' },
+          { label: '澶嶈瘎', value: 'REVIEW' },
+          { label: '鍔ㄦ�佽皟鏁�', value: 'ADJUST' },
+        ],
+        placeholder: '璇烽�夋嫨璇勫畾绫诲瀷',
+      },
+    },
+    {
+      fieldName: 'evalReason',
+      label: '璇勫畾鐞嗙敱',
+      component: 'Textarea',
+      formItemClass: 'col-span-2',
+      componentProps: {
+        placeholder: '璇疯緭鍏ヨ瘎瀹氱悊鐢�',
+        rows: 2,
+      },
+    },
+    {
+      fieldName: 'operateTime',
+      label: '璇勫畾鏃堕棿',
+      component: 'DatePicker',
+      componentProps: {
+        class: '!w-full',
+        format: 'YYYY-MM-DD HH:mm:ss',
+        showTime: true,
+        valueFormat: 'YYYY-MM-DD HH:mm:ss',
+      },
+    },
+    {
+      fieldName: 'validUntil',
+      label: '鏈夋晥鏈熻嚦',
+      component: 'DatePicker',
+      componentProps: {
+        class: '!w-full',
+        format: 'YYYY-MM-DD',
+        valueFormat: 'YYYY-MM-DD',
+      },
+    },
+    {
+      fieldName: 'operatorName',
+      label: '璇勫畾浜�',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ヨ瘎瀹氫汉濮撳悕',
+      },
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      formItemClass: 'col-span-2',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ娉�',
+        rows: 2,
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'level',
+      label: '淇$敤绛夌骇',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: getDictOptions(DICT_TYPE.CRM_CREDIT_LEVEL),
+        placeholder: '璇烽�夋嫨淇$敤绛夌骇',
+      },
+    },
+    {
+      fieldName: 'evalType',
+      label: '璇勫畾绫诲瀷',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: [
+          { label: '鍒濊瘎', value: 'INITIAL' },
+          { label: '澶嶈瘎', value: 'REVIEW' },
+          { label: '鍔ㄦ�佽皟鏁�', value: 'ADJUST' },
+        ],
+        placeholder: '璇烽�夋嫨璇勫畾绫诲瀷',
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions<CrmCreditRatingApi.CreditRating>['columns'] {
+  return [
+    { field: 'customerName', title: '瀹㈡埛鍚嶇О', minWidth: 180 },
+    {
+      field: 'level',
+      title: '淇$敤绛夌骇',
+      width: 100,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.CRM_CREDIT_LEVEL },
+      },
+    },
+    { field: 'score', title: '淇$敤鍒�', width: 90 },
+    {
+      field: 'evalType',
+      title: '璇勫畾绫诲瀷',
+      width: 120,
+      slots: { default: 'evalType' },
+    },
+    { field: 'evalReason', title: '璇勫畾鐞嗙敱', minWidth: 200, showOverflow: true },
+    { field: 'operatorName', title: '璇勫畾浜�', width: 100 },
+    {
+      field: 'operateTime',
+      title: '璇勫畾鏃堕棿',
+      width: 170,
+      formatter: 'formatDateTime',
+    },
+    {
+      field: 'validUntil',
+      title: '鏈夋晥鏈熻嚦',
+      width: 120,
+      formatter: 'formatDate',
+    },
+    {
+      field: 'createTime',
+      title: '鍒涘缓鏃堕棿',
+      width: 170,
+      formatter: 'formatDateTime',
+    },
+    {
+      title: '鎿嶄綔',
+      width: 160,
+      fixed: 'right',
+      slots: { default: 'actions' },
+    },
+  ];
+}
diff --git a/src/views/crm/credit-rating/index.vue b/src/views/crm/credit-rating/index.vue
new file mode 100644
index 0000000..4821827
--- /dev/null
+++ b/src/views/crm/credit-rating/index.vue
@@ -0,0 +1,156 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { CrmCreditRatingApi } from '#/api/crm/credit-rating';
+
+import { Page, useVbenModal } from '@vben/common-ui';
+import { downloadFileFromBlobPart } from '@vben/utils';
+
+import { message } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+  deleteCreditRating,
+  exportCreditRating,
+  getCreditRatingPage,
+} from '#/api/crm/credit-rating';
+import { $t } from '#/locales';
+
+import { useGridColumns, useGridFormSchema } from './data';
+import Form from './modules/form.vue';
+
+const [FormModal, formModalApi] = useVbenModal({
+  connectedComponent: Form,
+  destroyOnClose: true,
+});
+
+/** 鍒锋柊琛ㄦ牸 */
+function handleRefresh() {
+  gridApi.query();
+}
+
+/** 瀵煎嚭琛ㄦ牸 */
+async function handleExport() {
+  const formValues = await gridApi.formApi.getValues();
+  const data = await exportCreditRating(formValues);
+  downloadFileFromBlobPart({ fileName: '瀹㈡埛淇$敤璇勭骇.xls', source: data });
+}
+
+/** 鏂板 */
+function handleCreate() {
+  formModalApi.setData(null).open();
+}
+
+/** 缂栬緫 */
+function handleEdit(row: CrmCreditRatingApi.CreditRating) {
+  formModalApi.setData({ creditRating: row }).open();
+}
+
+/** 鍒犻櫎 */
+async function handleDelete(row: CrmCreditRatingApi.CreditRating) {
+  const hideLoading = message.loading({
+    content: $t('ui.actionMessage.deleting', [row.customerName ?? '']),
+    duration: 0,
+  });
+  try {
+    await deleteCreditRating(row.id!);
+    message.success(
+      $t('ui.actionMessage.deleteSuccess', [row.customerName ?? '']),
+    );
+    handleRefresh();
+  } finally {
+    hideLoading();
+  }
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  formOptions: {
+    schema: useGridFormSchema(),
+  },
+  gridOptions: {
+    columns: useGridColumns(),
+    height: 'auto',
+    keepSource: true,
+    proxyConfig: {
+      ajax: {
+        query: async ({ page }, formValues) =>
+          await getCreditRatingPage({
+            pageNo: page.currentPage,
+            pageSize: page.pageSize,
+            ...formValues,
+          }),
+      },
+    },
+    rowConfig: {
+      keyField: 'id',
+      isHover: true,
+    },
+    toolbarConfig: {
+      refresh: true,
+      search: true,
+    },
+  } as VxeTableGridOptions<CrmCreditRatingApi.CreditRating>,
+});
+</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: ['crm:credit-rating:create'],
+              onClick: handleCreate,
+            },
+            {
+              label: $t('ui.actionTitle.export'),
+              type: 'primary',
+              icon: ACTION_ICON.DOWNLOAD,
+              auth: ['crm:credit-rating:export'],
+              onClick: handleExport,
+            },
+          ]"
+        />
+      </template>
+      <template #evalType="{ row }">
+        {{
+          row.evalType === 'INITIAL'
+            ? '鍒濊瘎'
+            : row.evalType === 'REVIEW'
+              ? '澶嶈瘎'
+              : '鍔ㄦ�佽皟鏁�'
+        }}
+      </template>
+      <template #actions="{ row }">
+        <TableAction
+          :actions="[
+            {
+              label: $t('common.edit'),
+              type: 'link',
+              icon: ACTION_ICON.EDIT,
+              auth: ['crm:credit-rating:update'],
+              onClick: handleEdit.bind(null, row),
+            },
+            {
+              label: $t('common.delete'),
+              type: 'link',
+              danger: true,
+              icon: ACTION_ICON.DELETE,
+              auth: ['crm:credit-rating:delete'],
+              popConfirm: {
+                title: $t('ui.actionMessage.deleteConfirm', [
+                  row.customerName ?? '',
+                ]),
+                confirm: handleDelete.bind(null, row),
+              },
+            },
+          ]"
+        />
+      </template>
+    </Grid>
+  </Page>
+</template>
diff --git a/src/views/crm/credit-rating/modules/form.vue b/src/views/crm/credit-rating/modules/form.vue
new file mode 100644
index 0000000..20eb67b
--- /dev/null
+++ b/src/views/crm/credit-rating/modules/form.vue
@@ -0,0 +1,94 @@
+<script lang="ts" setup>
+import type { CrmCreditRatingApi } from '#/api/crm/credit-rating';
+
+import { computed, ref } from 'vue';
+
+import { useVbenForm, useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import {
+  createCreditRating,
+  getCreditRating,
+  updateCreditRating,
+} from '#/api/crm/credit-rating';
+import { $t } from '#/locales';
+
+import { useFormSchema } from '../data';
+
+type CreditRatingFormModalData = {
+  creditRating?: Pick<CrmCreditRatingApi.CreditRating, 'id'>;
+};
+
+const emit = defineEmits(['success']);
+const formData = ref<Partial<CrmCreditRatingApi.CreditRating>>();
+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: 110,
+  },
+  wrapperClass: 'grid-cols-2',
+  layout: 'horizontal',
+  schema: [],
+  showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    const { valid } = await formApi.validate();
+    if (!valid) {
+      return;
+    }
+    modalApi.lock();
+    const data = (await formApi.getValues()) as CrmCreditRatingApi.CreditRating;
+    if (formData.value?.id) {
+      data.id = formData.value.id;
+    }
+    try {
+      await (formData.value?.id
+        ? updateCreditRating(data)
+        : createCreditRating(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;
+    }
+    formApi.setState({ schema: useFormSchema(formApi) });
+    await formApi.resetForm();
+    const data = modalApi.getData() as null | CreditRatingFormModalData;
+    if (!data?.creditRating?.id) {
+      return;
+    }
+    modalApi.lock();
+    try {
+      formData.value = await getCreditRating(data.creditRating.id);
+      if (formData.value) {
+        await formApi.setValues(formData.value as any);
+      }
+    } finally {
+      modalApi.unlock();
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal :title="getTitle" class="w-3/5">
+    <Form class="mx-4" />
+  </Modal>
+</template>
diff --git a/src/views/crm/customer/data.ts b/src/views/crm/customer/data.ts
index d3372ee..203cd55 100644
--- a/src/views/crm/customer/data.ts
+++ b/src/views/crm/customer/data.ts
@@ -45,6 +45,16 @@
       },
     },
     {
+      fieldName: 'customerType',
+      label: '瀹㈡埛鍒嗙被',
+      component: 'Select',
+      componentProps: {
+        options: getDictOptions(DICT_TYPE.CRM_CUSTOMER_TYPE),
+        placeholder: '璇烽�夋嫨瀹㈡埛鍒嗙被',
+        allowClear: true,
+      },
+    },
+    {
       fieldName: 'mobile',
       label: '鎵嬫満',
       component: 'Input',
@@ -202,6 +212,26 @@
       },
     },
     {
+      fieldName: 'customerType',
+      label: '瀹㈡埛鍒嗙被',
+      component: 'Select',
+      componentProps: {
+        options: getDictOptions(DICT_TYPE.CRM_CUSTOMER_TYPE),
+        placeholder: '璇烽�夋嫨瀹㈡埛鍒嗙被',
+        allowClear: true,
+      },
+    },
+    {
+      fieldName: 'creditLevel',
+      label: '淇$敤绛夌骇',
+      component: 'Select',
+      componentProps: {
+        options: getDictOptions(DICT_TYPE.CRM_CREDIT_LEVEL),
+        placeholder: '璇烽�夋嫨淇$敤绛夌骇',
+        allowClear: true,
+      },
+    },
+    {
       fieldName: 'createTime',
       label: '鍒涘缓鏃堕棿',
       component: 'RangePicker',
@@ -276,6 +306,40 @@
       },
     },
     {
+      field: 'customerType',
+      title: '瀹㈡埛鍒嗙被',
+      minWidth: 100,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.CRM_CUSTOMER_TYPE },
+      },
+    },
+    {
+      field: 'creditLevel',
+      title: '淇$敤绛夌骇',
+      minWidth: 100,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.CRM_CREDIT_LEVEL },
+      },
+    },
+    {
+      field: 'creditScore',
+      title: '淇$敤鍒�',
+      minWidth: 90,
+    },
+    {
+      field: 'overdueAmount',
+      title: '娆犺垂閲戦',
+      minWidth: 110,
+      formatter: 'formatAmount2',
+    },
+    {
+      field: 'overdueCount',
+      title: '娆犺垂绗旀暟',
+      minWidth: 90,
+    },
+    {
       field: 'mobile',
       title: '鎵嬫満',
       minWidth: 120,
diff --git a/src/views/crm/refund/data.ts b/src/views/crm/refund/data.ts
new file mode 100644
index 0000000..f239359
--- /dev/null
+++ b/src/views/crm/refund/data.ts
@@ -0,0 +1,208 @@
+import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { CrmCustomerApi } from '#/api/crm/customer';
+import type { CrmRefundApi } from '#/api/crm/refund';
+
+import { markRaw } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import CrmCustomerSelect from '#/components/crm-customer-select.vue';
+
+/** 鏂板/淇敼閫�璐瑰崟 */
+export function useFormSchema(
+  formApi?: VbenFormApi,
+): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: {
+        triggerFields: [''],
+        show: () => false,
+      },
+    },
+    {
+      fieldName: 'refundNo',
+      label: '閫�璐瑰崟鍙�',
+      component: 'Input',
+      componentProps: {
+        placeholder: '涓嶅~鍒欒嚜鍔ㄧ敓鎴�',
+      },
+    },
+    {
+      fieldName: 'customerId',
+      label: '瀹㈡埛',
+      component: markRaw(CrmCustomerSelect),
+      componentProps: {
+        placeholder: '璇烽�夋嫨瀹㈡埛',
+        onChange: (item?: CrmCustomerApi.Customer) => {
+          formApi?.setFieldValue('customerName', item?.name);
+        },
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'customerName',
+      label: '瀹㈡埛鍚嶇О',
+      component: 'Input',
+      componentProps: {
+        placeholder: '瀹㈡埛鍚嶇О',
+        readonly: true,
+      },
+    },
+    {
+      fieldName: 'refundType',
+      label: '閫�璐圭被鍨�',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: getDictOptions(DICT_TYPE.CRM_REFUND_TYPE),
+        placeholder: '璇烽�夋嫨閫�璐圭被鍨�',
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'refundAmount',
+      label: '閫�璐归噾棰�',
+      component: 'InputNumber',
+      componentProps: {
+        class: 'w-full',
+        min: 0,
+        precision: 2,
+        placeholder: '璇疯緭鍏ラ��璐归噾棰濓紙鍏冿級',
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'contractNo',
+      label: '鍚堝悓缂栧彿',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ叧鑱斿悎鍚岀紪鍙�',
+      },
+    },
+    {
+      fieldName: 'applyUserName',
+      label: '鐢宠浜�',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ョ敵璇蜂汉濮撳悕',
+      },
+    },
+    {
+      fieldName: 'applyTime',
+      label: '鐢宠鏃堕棿',
+      component: 'DatePicker',
+      componentProps: {
+        class: '!w-full',
+        format: 'YYYY-MM-DD HH:mm:ss',
+        showTime: true,
+        valueFormat: 'YYYY-MM-DD HH:mm:ss',
+      },
+    },
+    {
+      fieldName: 'refundReason',
+      label: '閫�璐瑰師鍥�',
+      component: 'Textarea',
+      formItemClass: 'col-span-2',
+      componentProps: {
+        placeholder: '璇疯緭鍏ラ��璐瑰師鍥�',
+        rows: 2,
+      },
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      formItemClass: 'col-span-2',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ娉�',
+        rows: 2,
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'refundNo',
+      label: '閫�璐瑰崟鍙�',
+      component: 'Input',
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇疯緭鍏ラ��璐瑰崟鍙�',
+      },
+    },
+    {
+      fieldName: 'refundType',
+      label: '閫�璐圭被鍨�',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: getDictOptions(DICT_TYPE.CRM_REFUND_TYPE),
+        placeholder: '璇烽�夋嫨閫�璐圭被鍨�',
+      },
+    },
+    {
+      fieldName: 'auditStatus',
+      label: '瀹℃壒鐘舵��',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: getDictOptions(DICT_TYPE.CRM_AUDIT_STATUS, 'number'),
+        placeholder: '璇烽�夋嫨瀹℃壒鐘舵��',
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions<CrmRefundApi.Refund>['columns'] {
+  return [
+    { field: 'refundNo', title: '閫�璐瑰崟鍙�', minWidth: 170 },
+    { field: 'customerName', title: '瀹㈡埛鍚嶇О', minWidth: 180 },
+    {
+      field: 'refundType',
+      title: '閫�璐圭被鍨�',
+      width: 110,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.CRM_REFUND_TYPE },
+      },
+    },
+    { field: 'refundAmount', title: '閫�璐归噾棰�', width: 120 },
+    { field: 'contractNo', title: '鍚堝悓缂栧彿', minWidth: 140 },
+    { field: 'applyUserName', title: '鐢宠浜�', width: 100 },
+    {
+      field: 'applyTime',
+      title: '鐢宠鏃堕棿',
+      width: 170,
+      formatter: 'formatDateTime',
+    },
+    {
+      field: 'auditStatus',
+      title: '瀹℃壒鐘舵��',
+      width: 110,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.CRM_AUDIT_STATUS },
+      },
+    },
+    {
+      field: 'createTime',
+      title: '鍒涘缓鏃堕棿',
+      width: 170,
+      formatter: 'formatDateTime',
+    },
+    {
+      title: '鎿嶄綔',
+      width: 200,
+      fixed: 'right',
+      slots: { default: 'actions' },
+    },
+  ];
+}
diff --git a/src/views/crm/refund/index.vue b/src/views/crm/refund/index.vue
new file mode 100644
index 0000000..bb053b4
--- /dev/null
+++ b/src/views/crm/refund/index.vue
@@ -0,0 +1,184 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { CrmRefundApi } from '#/api/crm/refund';
+
+import { Page, useVbenModal } from '@vben/common-ui';
+import { downloadFileFromBlobPart } from '@vben/utils';
+
+import { message } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+  auditRefund,
+  deleteRefund,
+  exportRefund,
+  getRefundPage,
+} from '#/api/crm/refund';
+import { $t } from '#/locales';
+
+import { useGridColumns, useGridFormSchema } from './data';
+import Form from './modules/form.vue';
+
+const [FormModal, formModalApi] = useVbenModal({
+  connectedComponent: Form,
+  destroyOnClose: true,
+});
+
+/** 鍒锋柊琛ㄦ牸 */
+function handleRefresh() {
+  gridApi.query();
+}
+
+/** 瀵煎嚭琛ㄦ牸 */
+async function handleExport() {
+  const formValues = await gridApi.formApi.getValues();
+  const data = await exportRefund(formValues);
+  downloadFileFromBlobPart({ fileName: '閫�璐瑰崟.xls', source: data });
+}
+
+/** 鏂板 */
+function handleCreate() {
+  formModalApi.setData(null).open();
+}
+
+/** 缂栬緫 */
+function handleEdit(row: CrmRefundApi.Refund) {
+  formModalApi.setData({ refund: row }).open();
+}
+
+/** 鍒犻櫎 */
+async function handleDelete(row: CrmRefundApi.Refund) {
+  const hideLoading = message.loading({
+    content: $t('ui.actionMessage.deleting', [row.refundNo ?? '']),
+    duration: 0,
+  });
+  try {
+    await deleteRefund(row.id!);
+    message.success($t('ui.actionMessage.deleteSuccess', [row.refundNo ?? '']));
+    handleRefresh();
+  } finally {
+    hideLoading();
+  }
+}
+
+/** 瀹℃壒锛堥�氳繃/椹冲洖锛� */
+async function handleAudit(row: CrmRefundApi.Refund, pass: boolean) {
+  const hideLoading = message.loading({
+    content: pass ? '瀹℃壒閫氳繃涓�...' : '瀹℃壒椹冲洖涓�...',
+    duration: 0,
+  });
+  try {
+    await auditRefund({ id: row.id!, auditStatus: pass ? 20 : 30 });
+    message.success(pass ? '瀹℃壒閫氳繃' : '瀹℃壒椹冲洖');
+    handleRefresh();
+  } finally {
+    hideLoading();
+  }
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  formOptions: {
+    schema: useGridFormSchema(),
+  },
+  gridOptions: {
+    columns: useGridColumns(),
+    height: 'auto',
+    keepSource: true,
+    proxyConfig: {
+      ajax: {
+        query: async ({ page }, formValues) =>
+          await getRefundPage({
+            pageNo: page.currentPage,
+            pageSize: page.pageSize,
+            ...formValues,
+          }),
+      },
+    },
+    rowConfig: {
+      keyField: 'id',
+      isHover: true,
+    },
+    toolbarConfig: {
+      refresh: true,
+      search: true,
+    },
+  } as VxeTableGridOptions<CrmRefundApi.Refund>,
+});
+</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: ['crm:refund:create'],
+              onClick: handleCreate,
+            },
+            {
+              label: $t('ui.actionTitle.export'),
+              type: 'primary',
+              icon: ACTION_ICON.DOWNLOAD,
+              auth: ['crm:refund:export'],
+              onClick: handleExport,
+            },
+          ]"
+        />
+      </template>
+      <template #actions="{ row }">
+        <TableAction
+          :actions="[
+            {
+              label: '閫氳繃',
+              type: 'link',
+              icon: ACTION_ICON.AUDIT,
+              auth: ['crm:refund:update'],
+              ifShow: row.auditStatus === 0 || row.auditStatus === 10,
+              popConfirm: {
+                title: '纭瀹℃壒閫氳繃璇ラ��璐瑰崟锛�',
+                confirm: handleAudit.bind(null, row, true),
+              },
+            },
+            {
+              label: '椹冲洖',
+              type: 'link',
+              danger: true,
+              icon: ACTION_ICON.AUDIT,
+              auth: ['crm:refund:update'],
+              ifShow: row.auditStatus === 0 || row.auditStatus === 10,
+              popConfirm: {
+                title: '纭椹冲洖璇ラ��璐瑰崟锛�',
+                confirm: handleAudit.bind(null, row, false),
+              },
+            },
+            {
+              label: $t('common.edit'),
+              type: 'link',
+              icon: ACTION_ICON.EDIT,
+              auth: ['crm:refund:update'],
+              onClick: handleEdit.bind(null, row),
+            },
+            {
+              label: $t('common.delete'),
+              type: 'link',
+              danger: true,
+              icon: ACTION_ICON.DELETE,
+              auth: ['crm:refund:delete'],
+              popConfirm: {
+                title: $t('ui.actionMessage.deleteConfirm', [
+                  row.refundNo ?? '',
+                ]),
+                confirm: handleDelete.bind(null, row),
+              },
+            },
+          ]"
+        />
+      </template>
+    </Grid>
+  </Page>
+</template>
diff --git a/src/views/crm/refund/modules/form.vue b/src/views/crm/refund/modules/form.vue
new file mode 100644
index 0000000..60ae9e6
--- /dev/null
+++ b/src/views/crm/refund/modules/form.vue
@@ -0,0 +1,92 @@
+<script lang="ts" setup>
+import type { CrmRefundApi } from '#/api/crm/refund';
+
+import { computed, ref } from 'vue';
+
+import { useVbenForm, useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import {
+  createRefund,
+  getRefund,
+  updateRefund,
+} from '#/api/crm/refund';
+import { $t } from '#/locales';
+
+import { useFormSchema } from '../data';
+
+type RefundFormModalData = {
+  refund?: Pick<CrmRefundApi.Refund, 'id'>;
+};
+
+const emit = defineEmits(['success']);
+const formData = ref<Partial<CrmRefundApi.Refund>>();
+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: 110,
+  },
+  wrapperClass: 'grid-cols-2',
+  layout: 'horizontal',
+  schema: [],
+  showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    const { valid } = await formApi.validate();
+    if (!valid) {
+      return;
+    }
+    modalApi.lock();
+    const data = (await formApi.getValues()) as CrmRefundApi.Refund;
+    if (formData.value?.id) {
+      data.id = formData.value.id;
+    }
+    try {
+      await (formData.value?.id ? updateRefund(data) : createRefund(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;
+    }
+    formApi.setState({ schema: useFormSchema(formApi) });
+    await formApi.resetForm();
+    const data = modalApi.getData() as null | RefundFormModalData;
+    if (!data?.refund?.id) {
+      return;
+    }
+    modalApi.lock();
+    try {
+      formData.value = await getRefund(data.refund.id);
+      if (formData.value) {
+        await formApi.setValues(formData.value as any);
+      }
+    } finally {
+      modalApi.unlock();
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal :title="getTitle" class="w-3/5">
+    <Form class="mx-4" />
+  </Modal>
+</template>
diff --git a/src/views/crm/saleQuotation/data.ts b/src/views/crm/saleQuotation/data.ts
index ab372f0..109c6e4 100644
--- a/src/views/crm/saleQuotation/data.ts
+++ b/src/views/crm/saleQuotation/data.ts
@@ -1,6 +1,9 @@
 import type { VbenFormSchema } from '#/adapter/form';
 import type { VxeTableGridOptions } from '#/adapter/vxe-table';
 
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
 import { erpPriceMultiply } from '#/packages/utils/src';
 
 import { z } from '#/adapter/form';
@@ -156,6 +159,81 @@
       },
     },
     {
+      fieldName: 'quotationType',
+      label: '鎶ヤ环绫诲瀷',
+      component: 'Select',
+      componentProps: {
+        options: getDictOptions(DICT_TYPE.CRM_QUOTATION_TYPE),
+        placeholder: '璇烽�夋嫨鎶ヤ环绫诲瀷',
+        allowClear: true,
+      },
+    },
+    {
+      fieldName: 'capacity',
+      label: '鍚堝悓瀹归噺锛坘VA锛�',
+      component: 'InputNumber',
+      componentProps: {
+        class: '!w-full',
+        min: 0,
+        precision: 2,
+        placeholder: '璇疯緭鍏ュ悎鍚屽閲�',
+      },
+    },
+    {
+      fieldName: 'maxDemand',
+      label: '鏈�澶ч渶閲忥紙kW锛�',
+      component: 'InputNumber',
+      componentProps: {
+        class: '!w-full',
+        min: 0,
+        precision: 2,
+        placeholder: '璇疯緭鍏ユ渶澶ч渶閲�',
+      },
+    },
+    {
+      fieldName: 'unitPrice',
+      label: '鐢靛害鍗曚环锛堝厓/kWh锛�',
+      component: 'InputNumber',
+      componentProps: {
+        class: '!w-full',
+        min: 0,
+        precision: 4,
+        placeholder: '璇疯緭鍏ョ數搴﹀崟浠�',
+      },
+    },
+    {
+      fieldName: 'tariffMode',
+      label: '鐢典环妯″紡',
+      component: 'Select',
+      componentProps: {
+        options: getDictOptions(DICT_TYPE.CRM_TARIFF_MODE),
+        placeholder: '璇烽�夋嫨鐢典环妯″紡',
+        allowClear: true,
+      },
+    },
+    {
+      fieldName: 'powerFactorFee',
+      label: '鍔涜皟鐢佃垂锛堝厓锛�',
+      component: 'InputNumber',
+      componentProps: {
+        class: '!w-full',
+        min: 0,
+        precision: 2,
+        placeholder: '璇疯緭鍏ュ姏璋冪數璐�',
+      },
+    },
+    {
+      fieldName: 'billingCycle',
+      label: '缁撶畻鍛ㄦ湡锛堟湀锛�',
+      component: 'InputNumber',
+      componentProps: {
+        class: '!w-full',
+        min: 1,
+        precision: 0,
+        placeholder: '璇疯緭鍏ョ粨绠楀懆鏈�',
+      },
+    },
+    {
       fieldName: 'remark',
       label: '澶囨敞',
       component: 'Textarea',
@@ -283,6 +361,15 @@
       fixed: 'left',
     },
     {
+      field: 'quotationType',
+      title: '鎶ヤ环绫诲瀷',
+      minWidth: 100,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.CRM_QUOTATION_TYPE },
+      },
+    },
+    {
       field: 'customerName',
       title: '瀹㈡埛鍚嶇О',
       minWidth: 120,
diff --git a/src/views/erp/purchase/plan/data.ts b/src/views/erp/purchase/plan/data.ts
new file mode 100644
index 0000000..9a279a5
--- /dev/null
+++ b/src/views/erp/purchase/plan/data.ts
@@ -0,0 +1,242 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { getDictOptions } from '@vben/hooks';
+
+import { getSupplierSimpleList } from '#/api/srm/supplier';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'detail' | 'edit';
+
+/** 鐘舵�佸父閲� */
+export const PURCHASE_PLAN_STATUS = {
+  DRAFT: 0, // 鏈彁浜わ紙鑽夌锛�
+  PROCESSING: 10, // 瀹℃壒涓�
+  APPROVED: 20, // 瀹℃牳閫氳繃
+  REJECTED: 30, // 瀹℃牳涓嶉�氳繃
+  CANCELLED: 40, // 宸插彇娑�
+};
+
+/** 闇�姹傛潵婧愬父閲� */
+export const DEMAND_SOURCE = {
+  STOCK_ALERT: 1, // 搴撳瓨棰勮
+  PLAN: 2, // 杩愮淮/鐢熶骇璁″垝
+};
+
+/** 琛ㄥ崟鐨勯厤缃」 */
+export function useFormSchema(formType: FormType): VbenFormSchema[] {
+  const isReadonly = formType === 'detail';
+  return [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: {
+        triggerFields: [''],
+        show: () => false,
+      },
+    },
+    {
+      fieldName: 'no',
+      label: '璁″垝鍗曞彿',
+      component: 'Input',
+      componentProps: {
+        placeholder: '绯荤粺鑷姩鐢熸垚',
+        disabled: true,
+      },
+    },
+    {
+      fieldName: 'name',
+      label: '璁″垝鍚嶇О',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ヨ鍒掑悕绉�',
+        disabled: isReadonly,
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'planDate',
+      label: '璁″垝鏃ユ湡',
+      component: 'DatePicker',
+      componentProps: {
+        placeholder: '閫夋嫨璁″垝鏃ユ湡',
+        showTime: true,
+        format: 'YYYY-MM-DD HH:mm:ss',
+        valueFormat: 'YYYY-MM-DD HH:mm:ss',
+        disabled: isReadonly,
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'demandSource',
+      label: '闇�姹傛潵婧�',
+      component: 'Select',
+      componentProps: {
+        placeholder: '璇烽�夋嫨闇�姹傛潵婧�',
+        allowClear: true,
+        options: getDictOptions('erp_purchase_plan_demand_source', 'number'),
+        disabled: isReadonly,
+      },
+    },
+    {
+      label: '渚涘簲鍟�',
+      fieldName: 'supplierId',
+      component: 'ApiSelect',
+      componentProps: {
+        placeholder: '璇烽�夋嫨渚涘簲鍟�',
+        allowClear: true,
+        showSearch: true,
+        api: getSupplierSimpleList,
+        labelField: 'name',
+        valueField: 'id',
+        disabled: isReadonly,
+      },
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ娉�',
+        autoSize: { minRows: 1, maxRows: 1 },
+        disabled: isReadonly,
+      },
+      formItemClass: 'col-span-2',
+    },
+    {
+      fieldName: 'items',
+      label: '璁″垝鏄庣粏',
+      component: 'Input',
+      formItemClass: 'col-span-3',
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'no',
+      label: '璁″垝鍗曞彿',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ヨ鍒掑崟鍙�',
+      },
+    },
+    {
+      fieldName: 'name',
+      label: '璁″垝鍚嶇О',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ヨ鍒掑悕绉�',
+      },
+    },
+    {
+      fieldName: 'demandSource',
+      label: '闇�姹傛潵婧�',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: getDictOptions('erp_purchase_plan_demand_source', 'number'),
+        placeholder: '璇烽�夋嫨闇�姹傛潵婧�',
+      },
+    },
+    {
+      fieldName: 'status',
+      label: '鐘舵��',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: [
+          { label: '鏈彁浜�', value: PURCHASE_PLAN_STATUS.DRAFT },
+          { label: '瀹℃壒涓�', value: PURCHASE_PLAN_STATUS.PROCESSING },
+          { label: '瀹℃牳閫氳繃', value: PURCHASE_PLAN_STATUS.APPROVED },
+          { label: '瀹℃牳涓嶉�氳繃', value: PURCHASE_PLAN_STATUS.REJECTED },
+          { label: '宸插彇娑�', value: PURCHASE_PLAN_STATUS.CANCELLED },
+        ],
+        placeholder: '璇烽�夋嫨鐘舵��',
+      },
+    },
+    {
+      fieldName: 'planDate',
+      label: '璁″垝鏃ユ湡',
+      component: 'RangePicker',
+      componentProps: getRangePickerDefaultProps(),
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勮〃鏍煎垪 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+  return [
+    { type: 'checkbox', width: 50 },
+    { type: 'seq', title: '搴忓彿', width: 60 },
+    {
+      field: 'no',
+      title: '璁″垝鍗曞彿',
+      minWidth: 140,
+    },
+    {
+      field: 'name',
+      title: '璁″垝鍚嶇О',
+      minWidth: 180,
+    },
+    {
+      field: 'demandSourceName',
+      title: '闇�姹傛潵婧�',
+      width: 110,
+    },
+    {
+      field: 'planUserName',
+      title: '缂栧埗浜�',
+      width: 110,
+    },
+    {
+      field: 'planDeptName',
+      title: '缂栧埗閮ㄩ棬',
+      width: 120,
+    },
+    {
+      field: 'supplierName',
+      title: '渚涘簲鍟�',
+      minWidth: 130,
+    },
+    {
+      field: 'productNames',
+      title: '浜у搧',
+      minWidth: 180,
+    },
+    {
+      field: 'totalCount',
+      title: '鍚堣鏁伴噺',
+      width: 100,
+      formatter: 'formatAmount3',
+    },
+    {
+      field: 'planDate',
+      title: '璁″垝鏃ユ湡',
+      width: 170,
+      formatter: 'formatDateTime',
+    },
+    {
+      field: 'status',
+      title: '鐘舵��',
+      width: 100,
+      slots: { default: 'status' },
+    },
+    {
+      field: 'createTime',
+      title: '鍒涘缓鏃堕棿',
+      width: 170,
+      formatter: 'formatDateTime',
+    },
+    {
+      field: 'action',
+      title: '鎿嶄綔',
+      width: 280,
+      fixed: 'right',
+      slots: { default: 'actions' },
+    },
+  ];
+}
diff --git a/src/views/erp/purchase/plan/index.vue b/src/views/erp/purchase/plan/index.vue
new file mode 100644
index 0000000..671a09f
--- /dev/null
+++ b/src/views/erp/purchase/plan/index.vue
@@ -0,0 +1,300 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { ErpPurchasePlanApi } from '#/api/erp/purchase/plan';
+
+import { ref } from 'vue';
+import { useRouter } from 'vue-router';
+
+import { confirm, Page, useVbenModal } from '@vben/common-ui';
+import { downloadFileFromBlobPart, isEmpty } from '@vben/utils';
+
+import { message, Tag } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+  deletePurchasePlan,
+  exportPurchasePlan,
+  generatePlanFromStockAlert,
+  generatePurchaseRequests,
+  getApproveProcessList,
+  getPurchasePlanPage,
+} from '#/api/erp/purchase/plan';
+import { $t } from '#/locales';
+
+import { useGridColumns, useGridFormSchema } from './data';
+import Form from './modules/form.vue';
+import ProcessSelectModal from './modules/process-select-modal.vue';
+
+/** ERP 閲囪喘璁″垝鍒楄〃 */
+defineOptions({ name: 'ErpPurchasePlan' });
+
+const router = useRouter();
+
+const [FormModal, formModalApi] = useVbenModal({
+  connectedComponent: Form,
+  destroyOnClose: true,
+});
+
+const [ProcessModal, processModalApi] = useVbenModal({
+  connectedComponent: ProcessSelectModal,
+  destroyOnClose: true,
+});
+
+/** 鍒锋柊琛ㄦ牸 */
+function handleRefresh() {
+  gridApi.query();
+}
+
+/** 瀵煎嚭琛ㄦ牸 */
+async function handleExport() {
+  const data = await exportPurchasePlan(await gridApi.formApi.getValues());
+  downloadFileFromBlobPart({ fileName: '閲囪喘璁″垝.xls', source: data });
+}
+
+/** 鏂板閲囪喘璁″垝 */
+function handleCreate() {
+  formModalApi.setData({ formType: 'create' }).open();
+}
+
+/** 缂栬緫閲囪喘璁″垝 */
+function handleEdit(row: ErpPurchasePlanApi.PurchasePlan) {
+  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 deletePurchasePlan(ids);
+    message.success($t('ui.actionMessage.deleteSuccess'));
+    handleRefresh();
+  } finally {
+    hideLoading();
+  }
+}
+
+/** 搴撳瓨棰勮鐢熸垚閲囪喘璁″垝 */
+async function handleGenerateFromStockAlert() {
+  const hideLoading = message.loading({
+    content: '姝e湪鏍规嵁搴撳瓨棰勮鐢熸垚閲囪喘璁″垝...',
+    duration: 0,
+  });
+  try {
+    const planId = await generatePlanFromStockAlert({});
+    hideLoading();
+    message.success(`宸茬敓鎴愰噰璐鍒掞紙缂栧彿 ${planId}锛夛紝璇锋牳瀵规槑缁嗗悗鎻愪氦瀹℃壒`);
+    handleRefresh();
+  } catch {
+    hideLoading();
+  }
+}
+
+/** 鎻愪氦瀹℃壒 - 鍏堣幏鍙栨祦绋嬪垪琛� */
+async function handleSubmit(row: ErpPurchasePlanApi.PurchasePlan) {
+  const hideLoading = message.loading({
+    content: '鑾峰彇瀹℃壒娴佺▼...',
+    duration: 0,
+  });
+  try {
+    const processList = await getApproveProcessList(row.id!);
+    hideLoading();
+    if (!processList || processList.length === 0) {
+      message.warning('鏆傛棤鍙敤瀹℃壒娴佺▼锛岃鍏堥厤缃� BPM 娴佺▼');
+      return;
+    }
+    processModalApi.setData({ id: row.id, processList }).open();
+  } catch {
+    hideLoading();
+  }
+}
+
+/** 鐢熸垚閲囪喘鐢宠 */
+function handleGenerateRequests(row: ErpPurchasePlanApi.PurchasePlan) {
+  confirm({
+    content: `纭鏍规嵁璁″垝銆�${row.name}銆嶇敓鎴愰噰璐敵璇凤紵`,
+  }).then(() => {
+    const hideLoading = message.loading({
+      content: '姝e湪鐢熸垚閲囪喘鐢宠...',
+      duration: 0,
+    });
+    generatePurchaseRequests(row.id!)
+      .then((requestIds) => {
+        message.success(`宸茬敓鎴愰噰璐敵璇凤紙缂栧彿 ${requestIds.join(', ')}锛塦);
+        handleRefresh();
+      })
+      .finally(() => {
+        hideLoading();
+      });
+  });
+}
+
+const checkedIds = ref<number[]>([]);
+function handleRowCheckboxChange({
+  records,
+}: {
+  records: ErpPurchasePlanApi.PurchasePlan[];
+}) {
+  checkedIds.value = records.map((item) => item.id!);
+}
+
+/** 鏌ョ湅璇︽儏 */
+function handleDetail(row: ErpPurchasePlanApi.PurchasePlan) {
+  formModalApi.setData({ formType: 'detail', id: row.id }).open();
+}
+
+/** 鏌ョ湅瀹℃壒璇︽儏 */
+function handleViewProcess(row: ErpPurchasePlanApi.PurchasePlan) {
+  if (!row.processInstanceId) {
+    message.warning('璇ヨ鍒掑皻鏈彁浜ゅ鎵�');
+    return;
+  }
+  router.push({ name: 'BpmProcessInstanceDetail', query: { id: row.processInstanceId } });
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  formOptions: {
+    schema: useGridFormSchema(),
+  },
+  gridOptions: {
+    columns: useGridColumns(),
+    height: 'auto',
+    keepSource: true,
+    proxyConfig: {
+      ajax: {
+        query: async ({ page }, formValues) => {
+          return await getPurchasePlanPage({
+            pageNo: page.currentPage,
+            pageSize: page.pageSize,
+            ...formValues,
+          });
+        },
+      },
+    },
+    rowConfig: {
+      keyField: 'id',
+      isHover: true,
+    },
+    toolbarConfig: {
+      refresh: true,
+      search: true,
+    },
+  } as VxeTableGridOptions<ErpPurchasePlanApi.PurchasePlan>,
+  gridEvents: {
+    checkboxAll: handleRowCheckboxChange,
+    checkboxChange: handleRowCheckboxChange,
+  },
+});
+</script>
+
+<template>
+  <Page auto-content-height>
+    <FormModal @success="handleRefresh" />
+    <ProcessModal @success="handleRefresh" />
+    <Grid table-title="閲囪喘璁″垝鍒楄〃">
+      <template #toolbar-tools>
+        <TableAction
+          :actions="[
+            {
+              label: $t('ui.actionTitle.create', ['閲囪喘璁″垝']),
+              type: 'primary',
+              icon: ACTION_ICON.ADD,
+              auth: ['erp:purchase-plan:create'],
+              onClick: handleCreate,
+            },
+            {
+              label: '搴撳瓨棰勮鐢熸垚',
+              type: 'primary',
+              icon: ACTION_ICON.ADD,
+              auth: ['erp:purchase-plan:create'],
+              popConfirm: {
+                title: '鏍规嵁瀹夊叏搴撳瓨缂洪噺鑷姩鐢熸垚鑽夌閲囪喘璁″垝锛�',
+                confirm: handleGenerateFromStockAlert,
+              },
+            },
+            {
+              label: $t('ui.actionTitle.export'),
+              type: 'primary',
+              icon: ACTION_ICON.DOWNLOAD,
+              auth: ['erp:purchase-plan:export'],
+              onClick: handleExport,
+            },
+            {
+              label: '鎵归噺鍒犻櫎',
+              type: 'primary',
+              danger: true,
+              disabled: isEmpty(checkedIds),
+              icon: ACTION_ICON.DELETE,
+              auth: ['erp:purchase-plan:delete'],
+              popConfirm: {
+                title: `鏄惁鍒犻櫎鎵�閫変腑鏁版嵁锛焋,
+                confirm: handleDelete.bind(null, checkedIds),
+              },
+            },
+          ]"
+        />
+      </template>
+      <template #status="{ row }">
+        <Tag v-if="row.status === 0" color="default">鏈彁浜�</Tag>
+        <Tag v-else-if="row.status === 10" color="processing">瀹℃壒涓�</Tag>
+        <Tag v-else-if="row.status === 20" color="success">瀹℃牳閫氳繃</Tag>
+        <Tag v-else-if="row.status === 30" color="error">瀹℃牳涓嶉�氳繃</Tag>
+        <Tag v-else-if="row.status === 40" color="warning">宸插彇娑�</Tag>
+      </template>
+      <template #actions="{ row }">
+        <TableAction
+          :actions="[
+            {
+              label: $t('common.detail'),
+              type: 'link',
+              icon: ACTION_ICON.VIEW,
+              auth: ['erp:purchase-plan:query'],
+              onClick: handleDetail.bind(null, row),
+            },
+            {
+              label: $t('common.edit'),
+              type: 'link',
+              icon: ACTION_ICON.EDIT,
+              auth: ['erp:purchase-plan:update'],
+              ifShow: () => row.status === 0,
+              onClick: handleEdit.bind(null, row),
+            },
+            {
+              label: '鎻愪氦',
+              type: 'link',
+              ifShow: () => row.status === 0 || row.status === 30,
+              onClick: handleSubmit.bind(null, row),
+            },
+            {
+              label: '鐢熸垚鐢宠',
+              type: 'link',
+              auth: ['erp:purchase-plan:generate-request'],
+              ifShow: () => row.status === 20 && !row.generatedFlag,
+              onClick: handleGenerateRequests.bind(null, row),
+            },
+            {
+              label: '鏌ョ湅瀹℃壒',
+              type: 'link',
+              ifShow: () => row.status !== 0,
+              onClick: handleViewProcess.bind(null, row),
+            },
+            {
+              label: $t('common.delete'),
+              type: 'link',
+              danger: true,
+              icon: ACTION_ICON.DELETE,
+              auth: ['erp:purchase-plan:delete'],
+              ifShow: () => row.status === 0,
+              popConfirm: {
+                title: $t('ui.actionMessage.deleteConfirm', [row.no]),
+                confirm: handleDelete.bind(null, [row.id!]),
+              },
+            },
+          ]"
+        />
+      </template>
+    </Grid>
+  </Page>
+</template>
diff --git a/src/views/erp/purchase/plan/modules/form.vue b/src/views/erp/purchase/plan/modules/form.vue
new file mode 100644
index 0000000..8ad6dc6
--- /dev/null
+++ b/src/views/erp/purchase/plan/modules/form.vue
@@ -0,0 +1,236 @@
+<script lang="ts" setup>
+import type { ErpPurchasePlanApi } from '#/api/erp/purchase/plan';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { Button, DatePicker, Input, InputNumber, message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+  createPurchasePlan,
+  getPurchasePlan,
+  updatePurchasePlan,
+} from '#/api/erp/purchase/plan';
+import { MdmItemSelect } from '#/views/basicData/mdm/components';
+import { $t } from '#/locales';
+
+import type { FormType } from '../data';
+import { useFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+const formData = ref<ErpPurchasePlanApi.PurchasePlan>();
+const formType = ref<FormType>('create');
+const items = ref<ErpPurchasePlanApi.PurchasePlanItem[]>([]);
+
+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 handleAddItem() {
+  items.value.push({
+    productId: undefined as unknown as number,
+    productName: '',
+    productUnitId: undefined as unknown as number,
+    productUnitName: '',
+    count: 1,
+    demandTime: undefined,
+    remark: '',
+  });
+}
+
+function handleRemoveItem(index: number) {
+  items.value.splice(index, 1);
+}
+
+function handleItemSelect(index: number, item: any) {
+  const row = items.value[index];
+  if (!row) {
+    return;
+  }
+  row.productId = item?.id;
+  row.productName = item?.name;
+  row.productUnitId = item?.unitMeasureId;
+  row.productUnitName = item?.unitMeasureName;
+}
+
+/** 鍒涘缓鎴栨洿鏂伴噰璐鍒� */
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    const { valid } = await formApi.validate();
+    if (!valid) {
+      return;
+    }
+    if (!items.value.length) {
+      message.error('璇疯嚦灏戞坊鍔犱竴鏉¤鍒掓槑缁�');
+      return;
+    }
+    if (items.value.some((item) => !item.productId || !item.count)) {
+      message.error('璇峰畬鍠勮鍒掓槑缁嗙殑浜у搧涓庢暟閲�');
+      return;
+    }
+    modalApi.lock();
+    const data =
+      (await formApi.getValues()) as ErpPurchasePlanApi.PurchasePlan;
+    data.items = items.value.map((item) => ({
+      productId: item.productId,
+      productUnitId: item.productUnitId,
+      count: item.count,
+      demandTime: item.demandTime,
+      remark: item.remark,
+    }));
+    try {
+      await (formType.value === 'create'
+        ? createPurchasePlan(data)
+        : updatePurchasePlan(data));
+      await modalApi.close();
+      emit('success');
+      message.success($t('ui.actionMessage.operationSuccess'));
+    } finally {
+      modalApi.unlock();
+    }
+  },
+  async onOpenChange(isOpen: boolean) {
+    if (!isOpen) {
+      formData.value = undefined;
+      items.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) {
+      return;
+    }
+    modalApi.lock();
+    try {
+      formData.value = await getPurchasePlan(data.id);
+      await formApi.setValues(formData.value);
+      items.value = (formData.value.items ?? []).map((item) => ({
+        ...item,
+      }));
+    } finally {
+      modalApi.unlock();
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal
+    :title="getTitle"
+    class="w-3/4"
+    :show-confirm-button="formType !== 'detail'"
+  >
+    <Form class="mx-3">
+      <template #items>
+        <div class="mt-2">
+          <div v-if="formType !== 'detail'" class="mb-2">
+            <Button type="dashed" @click="handleAddItem">+ 娣诲姞鏄庣粏</Button>
+          </div>
+          <table class="w-full border-collapse border border-gray-200 text-sm">
+            <thead>
+              <tr class="bg-gray-100">
+                <th class="border border-gray-200 px-2 py-1 w-12">搴忓彿</th>
+                <th class="border border-gray-200 px-2 py-1">浜у搧</th>
+                <th class="border border-gray-200 px-2 py-1 w-20">鍗曚綅</th>
+                <th class="border border-gray-200 px-2 py-1 w-32">鏁伴噺</th>
+                <th class="border border-gray-200 px-2 py-1 w-40">闇�姹傛棩鏈�</th>
+                <th class="border border-gray-200 px-2 py-1">澶囨敞</th>
+                <th
+                  v-if="formType !== 'detail'"
+                  class="border border-gray-200 px-2 py-1 w-16"
+                >
+                  鎿嶄綔
+                </th>
+              </tr>
+            </thead>
+            <tbody>
+              <tr v-for="(item, index) in items" :key="index">
+                <td class="border border-gray-200 px-2 py-1 text-center">
+                  {{ index + 1 }}
+                </td>
+                <td class="border border-gray-200 px-2 py-1">
+                  <MdmItemSelect
+                    v-if="formType !== 'detail'"
+                    :model-value="item.productId"
+                    placeholder="璇烽�夋嫨浜у搧"
+                    @change="handleItemSelect(index, $event)"
+                  />
+                  <span v-else>{{ item.productName }}</span>
+                </td>
+                <td class="border border-gray-200 px-2 py-1 text-center">
+                  {{ item.productUnitName || '-' }}
+                </td>
+                <td class="border border-gray-200 px-2 py-1">
+                  <InputNumber
+                    v-if="formType !== 'detail'"
+                    v-model:value="item.count"
+                    class="!w-full"
+                    :min="0.01"
+                    :precision="2"
+                  />
+                  <span v-else>{{ item.count }}</span>
+                </td>
+                <td class="border border-gray-200 px-2 py-1">
+                  <DatePicker
+                    v-if="formType !== 'detail'"
+                    v-model:value="item.demandTime"
+                    class="!w-full"
+                    value-format="YYYY-MM-DD"
+                  />
+                  <span v-else>{{ item.demandTime || '-' }}</span>
+                </td>
+                <td class="border border-gray-200 px-2 py-1">
+                  <Input
+                    v-if="formType !== 'detail'"
+                    v-model:value="item.remark"
+                    placeholder="澶囨敞"
+                  />
+                  <span v-else>{{ item.remark || '-' }}</span>
+                </td>
+                <td
+                  v-if="formType !== 'detail'"
+                  class="border border-gray-200 px-2 py-1 text-center"
+                >
+                  <Button type="link" danger @click="handleRemoveItem(index)">
+                    鍒犻櫎
+                  </Button>
+                </td>
+              </tr>
+              <tr v-if="!items.length">
+                <td
+                  :colspan="formType === 'detail' ? 6 : 7"
+                  class="border border-gray-200 px-2 py-4 text-center text-gray-400"
+                >
+                  鏆傛棤鏄庣粏锛岃娣诲姞
+                </td>
+              </tr>
+            </tbody>
+          </table>
+        </div>
+      </template>
+    </Form>
+  </Modal>
+</template>
diff --git a/src/views/erp/purchase/plan/modules/process-select-modal.vue b/src/views/erp/purchase/plan/modules/process-select-modal.vue
new file mode 100644
index 0000000..dd7a2a8
--- /dev/null
+++ b/src/views/erp/purchase/plan/modules/process-select-modal.vue
@@ -0,0 +1,79 @@
+<script lang="ts" setup>
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message, Select } from 'ant-design-vue';
+
+import { submitPurchasePlan } from '#/api/erp/purchase/plan';
+
+interface ProcessDefinition {
+  id: string;
+  name: string;
+  version: number;
+  key: string;
+}
+
+const emit = defineEmits(['success']);
+
+const planId = ref<number>();
+const processList = ref<ProcessDefinition[]>([]);
+const selectedProcessKey = ref<string>('');
+
+const processOptions = computed(() => {
+  return processList.value.map((p) => ({
+    label: `${p.name} (V${p.version})`,
+    value: p.key,
+  }));
+});
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    if (!selectedProcessKey.value) {
+      message.warning('璇烽�夋嫨瀹℃壒娴佺▼');
+      return;
+    }
+    modalApi.lock();
+    try {
+      await submitPurchasePlan(planId.value!, selectedProcessKey.value);
+      await modalApi.close();
+      emit('success');
+      message.success('鎻愪氦鎴愬姛');
+    } finally {
+      modalApi.unlock();
+    }
+  },
+  async onOpenChange(isOpen: boolean) {
+    if (!isOpen) {
+      planId.value = undefined;
+      selectedProcessKey.value = '';
+      processList.value = [];
+      return;
+    }
+    const data = modalApi.getData<{ id: number; processList: ProcessDefinition[] }>();
+    if (!data) {
+      return;
+    }
+    planId.value = data.id;
+    processList.value = data.processList || [];
+    // 榛樿閫夋嫨绗竴涓祦绋�
+    if (processList.value.length > 0) {
+      selectedProcessKey.value = processList.value[0]?.key ?? '';
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal title="閫夋嫨瀹℃壒娴佺▼" class="w-1/3">
+    <div class="mb-4">
+      <span class="mr-2">瀹℃壒娴佺▼锛�</span>
+      <Select
+        v-model:value="selectedProcessKey"
+        :options="processOptions"
+        style="width: 200px"
+        placeholder="璇烽�夋嫨瀹℃壒娴佺▼"
+      />
+    </div>
+  </Modal>
+</template>
diff --git a/src/views/hrm/certificate/data.ts b/src/views/hrm/certificate/data.ts
new file mode 100644
index 0000000..be46e17
--- /dev/null
+++ b/src/views/hrm/certificate/data.ts
@@ -0,0 +1,197 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { markRaw } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { HrmEmployeeSelect } from '#/views/hrm/employee/components';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'detail' | 'edit';
+
+/** 琛ㄥ崟鐨勯厤缃」 */
+export function useFormSchema(formType: FormType): VbenFormSchema[] {
+  const isReadonly = formType === 'detail';
+  return [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: {
+        triggerFields: [''],
+        show: () => false,
+      },
+    },
+    {
+      fieldName: 'employeeId',
+      label: '鍛樺伐',
+      component: markRaw(HrmEmployeeSelect),
+      componentProps: {
+        placeholder: '璇烽�夋嫨鍛樺伐',
+        disabled: isReadonly,
+      },
+      rules: 'selectRequired',
+    },
+    {
+      fieldName: 'certType',
+      label: '璇佷功绫诲瀷',
+      component: 'Select',
+      componentProps: {
+        options: getDictOptions(DICT_TYPE.HRM_CERTIFICATE_TYPE, 'number'),
+        placeholder: '璇烽�夋嫨璇佷功绫诲瀷',
+        disabled: isReadonly,
+      },
+      rules: 'selectRequired',
+    },
+    {
+      fieldName: 'certName',
+      label: '璇佷功鍚嶇О',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ヨ瘉涔﹀悕绉�',
+        disabled: isReadonly,
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'certNo',
+      label: '璇佷功缂栧彿',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ヨ瘉涔︾紪鍙�',
+        disabled: isReadonly,
+      },
+    },
+    {
+      fieldName: 'issueOrg',
+      label: '鍙戣瘉鏈烘瀯',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ彂璇佹満鏋�',
+        disabled: isReadonly,
+      },
+    },
+    {
+      fieldName: 'issueDate',
+      label: '鍙戣瘉鏃ユ湡',
+      component: 'DatePicker',
+      componentProps: {
+        placeholder: '閫夋嫨鍙戣瘉鏃ユ湡',
+        valueFormat: 'YYYY-MM-DD',
+        disabled: isReadonly,
+      },
+    },
+    {
+      fieldName: 'expireDate',
+      label: '鏈夋晥鏈熻嚦',
+      component: 'DatePicker',
+      componentProps: {
+        placeholder: '閫夋嫨鏈夋晥鏈熻嚦',
+        valueFormat: 'YYYY-MM-DD',
+        disabled: isReadonly,
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ娉�',
+        autoSize: { minRows: 1, maxRows: 1 },
+        disabled: isReadonly,
+      },
+      formItemClass: 'col-span-2',
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'employeeId',
+      label: '鍛樺伐',
+      component: markRaw(HrmEmployeeSelect),
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇烽�夋嫨鍛樺伐',
+      },
+    },
+    {
+      fieldName: 'certType',
+      label: '璇佷功绫诲瀷',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: getDictOptions(DICT_TYPE.HRM_CERTIFICATE_TYPE, 'number'),
+        placeholder: '璇烽�夋嫨璇佷功绫诲瀷',
+      },
+    },
+    {
+      fieldName: 'certName',
+      label: '璇佷功鍚嶇О',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ヨ瘉涔﹀悕绉�',
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勮〃鏍煎垪 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+  return [
+    { type: 'checkbox', width: 50 },
+    { type: 'seq', title: '搴忓彿', width: 60 },
+    {
+      field: 'employeeName',
+      title: '鍛樺伐',
+      minWidth: 120,
+    },
+    {
+      field: 'certType',
+      title: '璇佷功绫诲瀷',
+      width: 120,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.HRM_CERTIFICATE_TYPE },
+      },
+    },
+    {
+      field: 'certName',
+      title: '璇佷功鍚嶇О',
+      minWidth: 200,
+    },
+    {
+      field: 'certNo',
+      title: '璇佷功缂栧彿',
+      minWidth: 130,
+    },
+    {
+      field: 'issueOrg',
+      title: '鍙戣瘉鏈烘瀯',
+      minWidth: 140,
+    },
+    {
+      field: 'expireDate',
+      title: '鏈夋晥鏈熻嚦',
+      width: 120,
+      formatter: 'formatDate',
+    },
+    {
+      field: 'expireStatus',
+      title: '鍒版湡鐘舵��',
+      width: 100,
+      slots: { default: 'expireStatus' },
+    },
+    {
+      field: 'action',
+      title: '鎿嶄綔',
+      width: 200,
+      fixed: 'right',
+      slots: { default: 'actions' },
+    },
+  ];
+}
diff --git a/src/views/hrm/certificate/index.vue b/src/views/hrm/certificate/index.vue
new file mode 100644
index 0000000..63953bd
--- /dev/null
+++ b/src/views/hrm/certificate/index.vue
@@ -0,0 +1,169 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { HrmCertificateApi } from '#/api/hrm/certificate';
+
+import { ref } from 'vue';
+
+import { Page, useVbenModal } from '@vben/common-ui';
+import { isEmpty } from '@vben/utils';
+
+import { message, Tag } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import { deleteCertificate, getCertificatePage } from '#/api/hrm/certificate';
+import { $t } from '#/locales';
+
+import { useGridColumns, useGridFormSchema } from './data';
+import Form from './modules/form.vue';
+
+/** HRM 璇佷功璧勮川鍒楄〃 */
+defineOptions({ name: 'HrmCertificate' });
+
+const [FormModal, formModalApi] = useVbenModal({
+  connectedComponent: Form,
+  destroyOnClose: true,
+});
+
+function handleRefresh() {
+  gridApi.query();
+}
+
+function handleCreate() {
+  formModalApi.setData({ formType: 'create' }).open();
+}
+
+function handleEdit(row: HrmCertificateApi.Certificate) {
+  formModalApi.setData({ formType: 'edit', id: row.id }).open();
+}
+
+function handleDetail(row: HrmCertificateApi.Certificate) {
+  formModalApi.setData({ formType: 'detail', id: row.id }).open();
+}
+
+async function handleDelete(ids: number[]) {
+  const hideLoading = message.loading({
+    content: $t('ui.actionMessage.deleting'),
+    duration: 0,
+  });
+  try {
+    await deleteCertificate(ids);
+    message.success($t('ui.actionMessage.deleteSuccess'));
+    handleRefresh();
+  } finally {
+    hideLoading();
+  }
+}
+
+const checkedIds = ref<number[]>([]);
+function handleRowCheckboxChange({
+  records,
+}: {
+  records: HrmCertificateApi.Certificate[];
+}) {
+  checkedIds.value = records.map((item) => item.id!);
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  formOptions: {
+    schema: useGridFormSchema(),
+  },
+  gridOptions: {
+    columns: useGridColumns(),
+    height: 'auto',
+    keepSource: true,
+    proxyConfig: {
+      ajax: {
+        query: async ({ page }, formValues) => {
+          return await getCertificatePage({
+            pageNo: page.currentPage,
+            pageSize: page.pageSize,
+            ...formValues,
+          });
+        },
+      },
+    },
+    rowConfig: {
+      keyField: 'id',
+      isHover: true,
+    },
+    toolbarConfig: {
+      refresh: true,
+      search: true,
+    },
+  } as VxeTableGridOptions<HrmCertificateApi.Certificate>,
+  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: ['hrm:certificate:create'],
+              onClick: handleCreate,
+            },
+            {
+              label: '鎵归噺鍒犻櫎',
+              type: 'primary',
+              danger: true,
+              disabled: isEmpty(checkedIds),
+              icon: ACTION_ICON.DELETE,
+              auth: ['hrm:certificate:delete'],
+              popConfirm: {
+                title: `鏄惁鍒犻櫎鎵�閫変腑鏁版嵁锛焋,
+                confirm: handleDelete.bind(null, checkedIds),
+              },
+            },
+          ]"
+        />
+      </template>
+      <template #expireStatus="{ row }">
+        <Tag v-if="row.expireStatus === 0" color="success">鏈夋晥</Tag>
+        <Tag v-else-if="row.expireStatus === 1" color="warning">鍗冲皢鍒版湡</Tag>
+        <Tag v-else-if="row.expireStatus === 2" color="error">宸茶繃鏈�</Tag>
+        <Tag v-else color="default">-</Tag>
+      </template>
+      <template #actions="{ row }">
+        <TableAction
+          :actions="[
+            {
+              label: $t('common.detail'),
+              type: 'link',
+              icon: ACTION_ICON.VIEW,
+              auth: ['hrm:certificate:query'],
+              onClick: handleDetail.bind(null, row),
+            },
+            {
+              label: $t('common.edit'),
+              type: 'link',
+              icon: ACTION_ICON.EDIT,
+              auth: ['hrm:certificate:update'],
+              onClick: handleEdit.bind(null, row),
+            },
+            {
+              label: $t('common.delete'),
+              type: 'link',
+              danger: true,
+              icon: ACTION_ICON.DELETE,
+              auth: ['hrm:certificate:delete'],
+              popConfirm: {
+                title: $t('ui.actionMessage.deleteConfirm', [row.certName]),
+                confirm: handleDelete.bind(null, [row.id!]),
+              },
+            },
+          ]"
+        />
+      </template>
+    </Grid>
+  </Page>
+</template>
diff --git a/src/views/hrm/certificate/modules/form.vue b/src/views/hrm/certificate/modules/form.vue
new file mode 100644
index 0000000..471833d
--- /dev/null
+++ b/src/views/hrm/certificate/modules/form.vue
@@ -0,0 +1,91 @@
+<script lang="ts" setup>
+import type { HrmCertificateApi } from '#/api/hrm/certificate';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { createCertificate, getCertificate, updateCertificate } from '#/api/hrm/certificate';
+import { $t } from '#/locales';
+
+import type { FormType } from '../data';
+import { useFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+const formData = ref<HrmCertificateApi.Certificate>();
+const formType = ref<FormType>('create');
+
+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,
+});
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    const { valid } = await formApi.validate();
+    if (!valid) {
+      return;
+    }
+    modalApi.lock();
+    const data =
+      (await formApi.getValues()) as HrmCertificateApi.Certificate;
+    try {
+      await (formType.value === 'create'
+        ? createCertificate(data)
+        : updateCertificate(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 getCertificate(data.id);
+      await formApi.setValues(formData.value);
+    } finally {
+      modalApi.unlock();
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal :title="getTitle" class="w-2/3" :show-confirm-button="formType !== 'detail'">
+    <Form class="mx-3" />
+  </Modal>
+</template>
diff --git a/src/views/mes/dv/machinery/data.ts b/src/views/mes/dv/machinery/data.ts
index 8b39782..ddacc2f 100644
--- a/src/views/mes/dv/machinery/data.ts
+++ b/src/views/mes/dv/machinery/data.ts
@@ -76,6 +76,15 @@
       rules: 'required',
     },
     {
+      fieldName: 'tbDeviceId',
+      label: '鏁伴噰璁惧ID',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ユ暟閲囪澶嘔D锛堝閮ㄦ暟閲� tbDeviceId锛�',
+      },
+      help: '涓庢暟閲囩郴缁熻澶嘔D瀵瑰簲锛岀敤浜庡叧鑱旇澶囧疄鏃舵暟鎹�',
+    },
+    {
       fieldName: 'brand',
       label: '鍝佺墝',
       component: 'Input',
diff --git a/src/views/mes/dv/machinery/modules/form.vue b/src/views/mes/dv/machinery/modules/form.vue
index a11e275..365f935 100644
--- a/src/views/mes/dv/machinery/modules/form.vue
+++ b/src/views/mes/dv/machinery/modules/form.vue
@@ -23,6 +23,7 @@
   import CheckRecordList from "./check-record-list.vue";
   import MaintenRecordList from "./mainten-record-list.vue";
   import RepairList from "./repair-list.vue";
+  import TelemetryList from "./telemetry-list.vue";
 
   const emit = defineEmits(["success"]);
   const formType = ref<FormType>("create"); // 琛ㄥ崟妯″紡
@@ -133,6 +134,11 @@
                     tab="缁翠慨璁板綍">
         <RepairList :machinery-id="formData.id" />
       </Tabs.TabPane>
+      <Tabs.TabPane key="telemetry"
+                    tab="鏁伴噰鏁版嵁"
+                    :disabled="!formData.tbDeviceId">
+        <TelemetryList :tb-device-id="formData.tbDeviceId" />
+      </Tabs.TabPane>
     </Tabs>
     <template #prepend-footer>
       <div class="flex flex-auto items-center">
diff --git a/src/views/mes/dv/statistics/summary.vue b/src/views/mes/dv/statistics/summary.vue
new file mode 100644
index 0000000..8f3e3cf
--- /dev/null
+++ b/src/views/mes/dv/statistics/summary.vue
@@ -0,0 +1,109 @@
+<script lang="ts" setup>
+import type { MesDvStatisticsApi } from '#/api/mes/dv/statistics';
+
+import { onMounted, ref } from 'vue';
+
+import { Page } from '@vben/common-ui';
+
+import { Card, Col, Row, Table, Tag } from 'ant-design-vue';
+
+import { getDvStatisticsSummary } from '#/api/mes/dv/statistics';
+
+defineOptions({ name: 'MesDvStatistics' });
+
+const loading = ref(false);
+const summary = ref<MesDvStatisticsApi.StatisticsSummary>({});
+
+async function loadSummary() {
+  loading.value = true;
+  try {
+    summary.value = await getDvStatisticsSummary();
+  } finally {
+    loading.value = false;
+  }
+}
+
+onMounted(loadSummary);
+</script>
+
+<template>
+  <Page>
+    <Row :gutter="16" class="mb-4">
+      <Col :span="4">
+        <Card :loading="loading">
+          <div class="text-sm text-gray-500">璁惧鎬绘暟</div>
+          <div class="mt-2 text-2xl font-bold">
+            {{ summary.totalMachinery ?? 0 }}
+          </div>
+        </Card>
+      </Col>
+      <Col :span="5">
+        <Card :loading="loading">
+          <div class="text-sm text-gray-500">鐢熶骇涓� / 鍋滄満 / 缁存姢涓�</div>
+          <div class="mt-2 text-2xl font-bold">
+            <span class="text-green-600">{{ summary.producingCount ?? 0 }}</span>
+            <span class="mx-1 text-gray-400">/</span>
+            <span class="text-red-600">{{ summary.stoppedCount ?? 0 }}</span>
+            <span class="mx-1 text-gray-400">/</span>
+            <span class="text-orange-600">{{ summary.maintainingCount ?? 0 }}</span>
+          </div>
+        </Card>
+      </Col>
+      <Col :span="5">
+        <Card :loading="loading">
+          <div class="text-sm text-gray-500">璁惧鏁呴殰鐜�</div>
+          <div class="mt-2 text-2xl font-bold text-red-600">
+            {{ (summary.faultRate ?? 0).toFixed(1) }}%
+          </div>
+        </Card>
+      </Col>
+      <Col :span="5">
+        <Card :loading="loading">
+          <div class="text-sm text-gray-500">缁翠慨鍗曪紙鎬绘暟 / 寰呭鐞嗭級</div>
+          <div class="mt-2 text-2xl font-bold">
+            {{ summary.totalRepair ?? 0 }}
+            <span class="text-sm text-gray-400">
+              / {{ summary.processingRepair ?? 0 }}
+            </span>
+          </div>
+        </Card>
+      </Col>
+      <Col :span="5">
+        <Card :loading="loading">
+          <div class="text-sm text-gray-500">淇濆吇璁板綍锛堟�绘暟 / 宸插畬鎴愶級</div>
+          <div class="mt-2 text-2xl font-bold">
+            {{ summary.totalMainten ?? 0 }}
+            <span class="text-sm text-gray-400">
+              / {{ summary.finishedMainten ?? 0 }}
+            </span>
+          </div>
+        </Card>
+      </Col>
+    </Row>
+
+    <Card title="鎸夎澶囩被鍨嬪垎甯�" :loading="loading">
+      <Table
+        row-key="machineryTypeId"
+        :data-source="summary.typeDistribution ?? []"
+        :pagination="false"
+        size="middle"
+      >
+        <Table.Column title="璁惧绫诲瀷" data-index="machineryTypeName">
+          <template #default="{ record }">
+            <Tag color="blue">{{ record.machineryTypeName || '鏈垎绫�' }}</Tag>
+          </template>
+        </Table.Column>
+        <Table.Column title="璁惧鏁伴噺" data-index="count" align="right" />
+        <Table.Column title="鍗犳瘮" align="right">
+          <template #default="{ record }">
+            {{
+              summary.totalMachinery
+                ? ((record.count / summary.totalMachinery) * 100).toFixed(1)
+                : 0
+            }}%
+          </template>
+        </Table.Column>
+      </Table>
+    </Card>
+  </Page>
+</template>
diff --git a/src/views/mes/md/item/data.ts b/src/views/mes/md/item/data.ts
index 5d74bb2..6893c75 100644
--- a/src/views/mes/md/item/data.ts
+++ b/src/views/mes/md/item/data.ts
@@ -178,6 +178,15 @@
       rules: z.number().default(0),
     },
     {
+      fieldName: 'materialCategory',
+      label: '鐗╄祫鍒嗙被',
+      component: 'Select',
+      componentProps: {
+        options: getDictOptions(DICT_TYPE.MES_MATERIAL_CATEGORY, 'number'),
+        placeholder: '璇烽�夋嫨鐗╄祫鍒嗙被',
+      },
+    },
+    {
       fieldName: 'remark',
       label: '澶囨敞',
       component: 'Textarea',
@@ -298,6 +307,16 @@
       },
     },
     {
+      field: 'materialCategory',
+      title: '鐗╄祫鍒嗙被',
+      width: 120,
+      align: 'center',
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.MES_MATERIAL_CATEGORY },
+      },
+    },
+    {
       field: 'safeStockFlag',
       title: '瀹夊叏搴撳瓨',
       width: 120,
diff --git a/src/views/mes/pd/ctc-reel/data.ts b/src/views/mes/pd/ctc-reel/data.ts
new file mode 100644
index 0000000..cc8ef48
--- /dev/null
+++ b/src/views/mes/pd/ctc-reel/data.ts
@@ -0,0 +1,206 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesPdCtcReelApi } from '#/api/mes/pd/ctc-reel';
+
+import { markRaw } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { PdCtcSelect } from '#/views/mes/pd/ctc/components';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'detail' | 'update';
+
+/** 鏂板/淇敼鐩樺叿璁$畻璁板綍 */
+export function useFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: {
+        triggerFields: [''],
+        show: () => false,
+      },
+    },
+    {
+      fieldName: 'ctcId',
+      label: '鎹綅瀵肩嚎浜у搧',
+      component: markRaw(PdCtcSelect),
+      componentProps: {
+        placeholder: '璇烽�夋嫨鎹綅瀵肩嚎浜у搧',
+      },
+      rules: 'selectRequired',
+    },
+    {
+      fieldName: 'reelType',
+      label: '鐩樺叿绫诲瀷',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: getDictOptions(DICT_TYPE.PRODUCT_REEL_TYPE, 'string'),
+        placeholder: '璇烽�夋嫨鐩樺叿绫诲瀷',
+      },
+    },
+    {
+      fieldName: 'reelOuterDiameter',
+      label: '鐩樺叿澶栧緞(mm)',
+      component: 'InputNumber',
+      componentProps: {
+        class: '!w-full',
+        min: 0,
+        precision: 0,
+        placeholder: '璇疯緭鍏ョ洏鍏峰寰� d1',
+      },
+    },
+    {
+      fieldName: 'reelInnerDiameter',
+      label: '鐩樿姱鍐呭緞(mm)',
+      component: 'InputNumber',
+      componentProps: {
+        class: '!w-full',
+        min: 0,
+        precision: 0,
+        placeholder: '璇疯緭鍏ョ洏鑺唴寰� d2',
+      },
+    },
+    {
+      fieldName: 'reelWidth',
+      label: '鐩樺(mm)',
+      component: 'InputNumber',
+      componentProps: {
+        class: '!w-full',
+        min: 0,
+        precision: 0,
+        placeholder: '璇疯緭鍏ョ洏瀹� l2',
+      },
+    },
+    {
+      fieldName: 'maxCapacity',
+      label: '鏈�澶у閲�(km)',
+      component: 'InputNumber',
+      componentProps: {
+        class: '!w-full',
+        min: 0,
+        precision: 4,
+        placeholder: '璇疯緭鍏ユ渶澶у閲�',
+      },
+    },
+    {
+      fieldName: 'recommendedLength',
+      label: '鎺ㄨ崘姣忕洏闀垮害(km)',
+      component: 'InputNumber',
+      componentProps: {
+        class: '!w-full',
+        min: 0,
+        precision: 2,
+        placeholder: '璇疯緭鍏ユ帹鑽愭瘡鐩橀暱搴�',
+      },
+    },
+    {
+      fieldName: 'netWeight',
+      label: '鍑�閲�(kg)',
+      component: 'InputNumber',
+      componentProps: {
+        class: '!w-full',
+        min: 0,
+        precision: 2,
+        placeholder: '璇疯緭鍏ュ噣閲�',
+      },
+    },
+    {
+      fieldName: 'grossWeight',
+      label: '姣涢噸(kg)',
+      component: 'InputNumber',
+      componentProps: {
+        class: '!w-full',
+        min: 0,
+        precision: 2,
+        placeholder: '璇疯緭鍏ユ瘺閲�',
+      },
+    },
+    {
+      fieldName: 'calcFormula',
+      label: '璁$畻鍏紡',
+      component: 'Textarea',
+      formItemClass: 'col-span-3',
+      componentProps: {
+        placeholder: '((d1-60)+(d2+10))脳INT(l2/(瀹�+1))脳(d1-d2-70)/(楂�+0.2)/1274',
+        rows: 2,
+      },
+      help: '瀹归噺璁$畻锛�((d1-60)+(d2+10))脳INT(l2/(CTC瀹�+1))脳(d1-d2-70)/(CTC楂�+0.2)/1274',
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      formItemClass: 'col-span-3',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ娉�',
+        rows: 2,
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'ctcId',
+      label: '鎹綅瀵肩嚎浜у搧',
+      component: markRaw(PdCtcSelect),
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇烽�夋嫨鎹綅瀵肩嚎浜у搧',
+      },
+    },
+    {
+      fieldName: 'reelType',
+      label: '鐩樺叿绫诲瀷',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: getDictOptions(DICT_TYPE.PRODUCT_REEL_TYPE, 'string'),
+        placeholder: '璇烽�夋嫨鐩樺叿绫诲瀷',
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions<MesPdCtcReelApi.CtcReel>['columns'] {
+  return [
+    { field: 'ctcOrderSpec', title: '鎹綅瀵肩嚎瑙勬牸', minWidth: 160 },
+    {
+      field: 'reelType',
+      title: '鐩樺叿绫诲瀷',
+      width: 100,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.PRODUCT_REEL_TYPE },
+      },
+    },
+    { field: 'reelOuterDiameter', title: '鐩樺叿澶栧緞(mm)', width: 110 },
+    { field: 'reelInnerDiameter', title: '鐩樿姱鍐呭緞(mm)', width: 110 },
+    { field: 'reelWidth', title: '鐩樺(mm)', width: 100 },
+    { field: 'maxCapacity', title: '鏈�澶у閲�(km)', width: 120 },
+    { field: 'recommendedLength', title: '鎺ㄨ崘闀垮害(km)', width: 120 },
+    { field: 'netWeight', title: '鍑�閲�(kg)', width: 100 },
+    { field: 'grossWeight', title: '姣涢噸(kg)', width: 100 },
+    {
+      field: 'createTime',
+      title: '鍒涘缓鏃堕棿',
+      width: 170,
+      formatter: 'formatDateTime',
+    },
+    {
+      title: '鎿嶄綔',
+      width: 220,
+      fixed: 'right',
+      slots: {
+        default: 'actions',
+      },
+    },
+  ];
+}
diff --git a/src/views/mes/pd/ctc-reel/index.vue b/src/views/mes/pd/ctc-reel/index.vue
new file mode 100644
index 0000000..8f6737a
--- /dev/null
+++ b/src/views/mes/pd/ctc-reel/index.vue
@@ -0,0 +1,150 @@
+<script lang="ts" setup>
+  import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+  import type { MesPdCtcReelApi } from '#/api/mes/pd/ctc-reel';
+
+  import { ref } from 'vue';
+
+  import { Page, useVbenModal } from '@vben/common-ui';
+  import { $t } from '@vben/locales';
+
+  import { Button, message } from 'ant-design-vue';
+
+  import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+  import {
+    deleteCtcReel,
+    getCtcReelPage,
+  } from '#/api/mes/pd/ctc-reel';
+
+  import { useGridColumns, useGridFormSchema } from './data';
+  import Form from './modules/form.vue';
+
+  const [FormModal, formModalApi] = useVbenModal({
+    connectedComponent: Form,
+    destroyOnClose: true,
+  });
+
+  /** 鍒锋柊琛ㄦ牸 */
+  function handleRefresh() {
+    gridApi.query();
+  }
+
+  /** 鏂板 */
+  function handleCreate() {
+    formModalApi.setData({ formType: 'create' }).open();
+  }
+
+  /** 鏌ョ湅 */
+  function handleDetail(row: MesPdCtcReelApi.CtcReel) {
+    formModalApi.setData({ id: row.id, formType: 'detail' }).open();
+  }
+
+  /** 缂栬緫 */
+  function handleEdit(row: MesPdCtcReelApi.CtcReel) {
+    formModalApi.setData({ id: row.id, formType: 'update' }).open();
+  }
+
+  /** 鍒犻櫎 */
+  async function handleDelete(row: MesPdCtcReelApi.CtcReel) {
+    const hideLoading = message.loading({
+      content: $t('ui.actionMessage.deleting', [row.ctcOrderSpec ?? '']),
+      duration: 0,
+    });
+    try {
+      await deleteCtcReel(row.id!);
+      message.success(
+        $t('ui.actionMessage.deleteSuccess', [row.ctcOrderSpec ?? '']),
+      );
+      handleRefresh();
+    } finally {
+      hideLoading();
+    }
+  }
+
+  const [Grid, gridApi] = useVbenVxeGrid({
+    formOptions: {
+      schema: useGridFormSchema(),
+    },
+    gridOptions: {
+      columns: useGridColumns(),
+      height: 'auto',
+      keepSource: true,
+      proxyConfig: {
+        ajax: {
+          query: async ({ page }, formValues) =>
+            await getCtcReelPage({
+              pageNo: page.currentPage,
+              pageSize: page.pageSize,
+              ...formValues,
+            }),
+        },
+      },
+      rowConfig: {
+        keyField: 'id',
+        isHover: true,
+      },
+      toolbarConfig: {
+        refresh: true,
+        search: true,
+      },
+    } as VxeTableGridOptions<MesPdCtcReelApi.CtcReel>,
+  });
+</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: ['mes:pd-ctc-reel:create'],
+              onClick: handleCreate,
+            },
+          ]"
+        />
+      </template>
+      <template #ctcOrderSpec="{ row }">
+        <Button type="link" @click="handleDetail(row)">
+          {{ row.ctcOrderSpec }}
+        </Button>
+      </template>
+      <template #actions="{ row }">
+        <TableAction
+          :actions="[
+            {
+              label: $t('common.edit'),
+              type: 'link',
+              icon: ACTION_ICON.EDIT,
+              auth: ['mes:pd-ctc-reel:update'],
+              onClick: handleEdit.bind(null, row),
+            },
+            {
+              label: $t('common.delete'),
+              type: 'link',
+              danger: true,
+              icon: ACTION_ICON.DELETE,
+              auth: ['mes:pd-ctc-reel:delete'],
+              popConfirm: {
+                title: $t('ui.actionMessage.deleteConfirm', [
+                  row.ctcOrderSpec ?? '',
+                ]),
+                confirm: handleDelete.bind(null, row),
+              },
+            },
+            {
+              label: $t('common.detail'),
+              type: 'link',
+              icon: ACTION_ICON.VIEW,
+              auth: ['mes:pd-ctc-reel:query'],
+              onClick: handleDetail.bind(null, row),
+            },
+          ]"
+        />
+      </template>
+    </Grid>
+  </Page>
+</template>
diff --git a/src/views/mes/pd/ctc-reel/modules/form.vue b/src/views/mes/pd/ctc-reel/modules/form.vue
new file mode 100644
index 0000000..970f400
--- /dev/null
+++ b/src/views/mes/pd/ctc-reel/modules/form.vue
@@ -0,0 +1,126 @@
+<script lang="ts" setup>
+  import type { FormType } from '../data';
+
+  import type { MesPdCtcReelApi } from '#/api/mes/pd/ctc-reel';
+
+  import { computed, ref } from 'vue';
+
+  import { useVbenModal } from '@vben/common-ui';
+  import { $t } from '@vben/locales';
+
+  import { Button, message } from 'ant-design-vue';
+
+  import { useVbenForm } from '#/adapter/form';
+  import {
+    createCtcReel,
+    getCtcReel,
+    updateCtcReel,
+  } from '#/api/mes/pd/ctc-reel';
+  import { CalcModal } from '#/views/mes/pd/ctc/modules';
+
+  import { useFormSchema } from '../data';
+
+  const emit = defineEmits(['success']);
+  const formType = ref<FormType>('create'); // 琛ㄥ崟妯″紡
+  const formData = ref<MesPdCtcReelApi.CtcReel>();
+  const isDetail = computed(() => formType.value === 'detail'); // 鏄惁鏌ョ湅妯″紡
+  const getTitle = computed(() => {
+    if (formType.value === 'detail') {
+      return '鏌ョ湅鐩樺叿璁$畻';
+    }
+    return formType.value === 'update' ? '淇敼鐩樺叿璁$畻' : '鏂板鐩樺叿璁$畻';
+  });
+
+  const [Form, formApi] = useVbenForm({
+    commonConfig: {
+      componentProps: {
+        class: 'w-full',
+      },
+      formItemClass: 'col-span-1',
+      labelWidth: 130,
+    },
+    wrapperClass: 'grid-cols-3',
+    layout: 'horizontal',
+    schema: [],
+    showDefaultActions: false,
+  });
+
+  const [CalcModal, calcModalApi] = useVbenModal({
+    connectedComponent: CalcModal,
+    destroyOnClose: true,
+  });
+
+  /** 鎵撳紑瀹归噺璁$畻宸ュ叿 */
+  function handleOpenCalc() {
+    calcModalApi.open();
+  }
+
+  /** 鍥炲~鏈�澶у閲忥紙km锛� */
+  function handleFillReel(value: { capacityKm: number }) {
+    formApi.setFieldValue('maxCapacity', value.capacityKm);
+  }
+
+  const [Modal, modalApi] = useVbenModal({
+    async onConfirm() {
+      if (isDetail.value) {
+        await modalApi.close();
+        return;
+      }
+      const { valid } = await formApi.validate();
+      if (!valid) {
+        return;
+      }
+      modalApi.lock();
+      // 鎻愪氦琛ㄥ崟
+      const data = (await formApi.getValues()) as MesPdCtcReelApi.CtcReel;
+      try {
+        await (data.id ? updateCtcReel(data) : createCtcReel(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.setState({ schema: useFormSchema() });
+      formApi.setDisabled(formType.value === 'detail');
+      modalApi.setState({ showConfirmButton: formType.value !== 'detail' });
+      if (!data?.id) {
+        return;
+      }
+      modalApi.lock();
+      try {
+        formData.value = await getCtcReel(data.id);
+        // 璁剧疆鍒� values
+        await formApi.setValues(formData.value);
+      } finally {
+        modalApi.unlock();
+      }
+    },
+  });
+</script>
+
+<template>
+  <Modal :title="getTitle" class="w-4/5">
+    <Form class="mx-4" />
+    <template #prepend-footer>
+      <Button
+        v-if="!isDetail"
+        type="default"
+        @click="handleOpenCalc"
+      >
+        瀹归噺璁$畻
+      </Button>
+    </template>
+    <CalcModal @reel="handleFillReel" />
+  </Modal>
+</template>
diff --git a/src/views/mes/pd/ctc/components/index.ts b/src/views/mes/pd/ctc/components/index.ts
new file mode 100644
index 0000000..1462a8b
--- /dev/null
+++ b/src/views/mes/pd/ctc/components/index.ts
@@ -0,0 +1 @@
+export { default as PdCtcSelect } from './select.vue';
diff --git a/src/views/mes/pd/ctc/components/select.vue b/src/views/mes/pd/ctc/components/select.vue
new file mode 100644
index 0000000..7508b0e
--- /dev/null
+++ b/src/views/mes/pd/ctc/components/select.vue
@@ -0,0 +1,64 @@
+<script lang="ts" setup>
+  import { onMounted, ref } from 'vue';
+
+  import { Select } from 'ant-design-vue';
+
+  import { getCtcPage } from '#/api/mes/pd/ctc';
+
+  defineOptions({ name: 'PdCtcSelect', inheritAttrs: false });
+
+  const props = withDefaults(
+    defineProps<{
+      allowClear?: boolean;
+      disabled?: boolean;
+      modelValue?: number;
+      placeholder?: string;
+    }>(),
+    {
+      allowClear: true,
+      disabled: false,
+      modelValue: undefined,
+      placeholder: '璇烽�夋嫨鎹綅瀵肩嚎浜у搧',
+    },
+  );
+
+  const emit = defineEmits<{
+    'update:modelValue': [value: number | undefined];
+  }>();
+
+  const options = ref<{ label: string; value: number }[]>([]);
+  const searchText = ref('');
+
+  async function load() {
+    const data = await getCtcPage({
+      pageNo: 1,
+      pageSize: 100,
+      orderSpec: searchText.value || undefined,
+    });
+    options.value = (data.list ?? []).map((item) => ({
+      label: `${item.orderSpec ?? ''}${item.itemName ? `锛�${item.itemName}锛塦 : ''}`,
+      value: item.id!,
+    }));
+  }
+
+  onMounted(load);
+
+  function handleSearch(val: string) {
+    searchText.value = val;
+    load();
+  }
+</script>
+
+<template>
+  <Select
+    :model-value="modelValue"
+    :options="options"
+    :disabled="disabled"
+    :placeholder="placeholder"
+    :allow-clear="allowClear"
+    show-search
+    :filter-option="false"
+    @update:value="(v: number | undefined) => emit('update:modelValue', v)"
+    @search="handleSearch"
+  />
+</template>
diff --git a/src/views/mes/pd/ctc/data.ts b/src/views/mes/pd/ctc/data.ts
new file mode 100644
index 0000000..324297c
--- /dev/null
+++ b/src/views/mes/pd/ctc/data.ts
@@ -0,0 +1,262 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesPdCtcApi } from '#/api/mes/pd/ctc';
+
+import { markRaw } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { MdItemSelect } from '#/views/mes/md/item/components';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'detail' | 'update';
+
+/** 鏂板/淇敼鎹綅瀵肩嚎浜у搧鎶�鏈弬鏁� */
+export function useFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: {
+        triggerFields: [''],
+        show: () => false,
+      },
+    },
+    {
+      fieldName: 'itemId',
+      label: '鐗╂枡',
+      component: markRaw(MdItemSelect),
+      componentProps: {
+        placeholder: '璇烽�夋嫨鎹綅瀵肩嚎鐗╂枡',
+      },
+      rules: 'selectRequired',
+    },
+    {
+      fieldName: 'orderSpec',
+      label: '璁㈣揣瑙勬牸',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ヨ璐ц鏍�',
+      },
+    },
+    {
+      fieldName: 'bareConductorSpec',
+      label: '瑁稿绾胯鏍�',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ヨ8瀵肩嚎瑙勬牸',
+      },
+    },
+    {
+      fieldName: 'enameledSpec',
+      label: '婕嗗寘绾胯鏍�',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ユ紗鍖呯嚎瑙勬牸',
+      },
+    },
+    {
+      fieldName: 'transpositionSpec',
+      label: '鎹綅瑙勬牸',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ユ崲浣嶈鏍�',
+      },
+    },
+    {
+      fieldName: 'conductorType',
+      label: '瀵间綋绫诲瀷',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: getDictOptions(DICT_TYPE.PRODUCT_CONDUCTOR_TYPE, 'string'),
+        placeholder: '璇烽�夋嫨瀵间綋绫诲瀷',
+      },
+    },
+    {
+      fieldName: 'crossSection',
+      label: '鎴潰绉�(mm虏)',
+      component: 'InputNumber',
+      componentProps: {
+        class: '!w-full',
+        min: 0,
+        precision: 2,
+        placeholder: '璇疯緭鍏ユ埅闈㈢Н',
+      },
+    },
+    {
+      fieldName: 'outerDiameter',
+      label: '澶栧緞(mm)',
+      component: 'InputNumber',
+      componentProps: {
+        class: '!w-full',
+        min: 0,
+        precision: 2,
+        placeholder: '璇疯緭鍏ュ寰�',
+      },
+    },
+    {
+      fieldName: 'resistance20c',
+      label: '20鈩冪洿娴佺數闃�(惟/km)',
+      component: 'InputNumber',
+      componentProps: {
+        class: '!w-full',
+        min: 0,
+        precision: 6,
+        placeholder: '璇疯緭鍏�20鈩冪洿娴佺數闃�',
+      },
+    },
+    {
+      fieldName: 'wrappingAngle',
+      label: '缁曞寘瑙掑害(掳)',
+      component: 'InputNumber',
+      componentProps: {
+        class: '!w-full',
+        min: 0,
+        precision: 2,
+        placeholder: '璇疯緭鍏ョ粫鍖呰搴�',
+      },
+    },
+    {
+      fieldName: 'insulationThickness',
+      label: '缁濈紭鍘氬害(mm)',
+      component: 'InputNumber',
+      componentProps: {
+        class: '!w-full',
+        min: 0,
+        precision: 2,
+        placeholder: '璇疯緭鍏ョ粷缂樺帤搴�',
+      },
+    },
+    {
+      fieldName: 'ratedVoltage',
+      label: '棰濆畾鐢靛帇(kV)',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ラ瀹氱數鍘�',
+      },
+    },
+    {
+      fieldName: 'testVoltage',
+      label: '璇曢獙鐢靛帇(kV)',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ヨ瘯楠岀數鍘�',
+      },
+    },
+    {
+      fieldName: 'minBendingRadius',
+      label: '鏈�灏忓集鏇插崐寰�(mm)',
+      component: 'InputNumber',
+      componentProps: {
+        class: '!w-full',
+        min: 0,
+        precision: 2,
+        placeholder: '璇疯緭鍏ユ渶灏忓集鏇插崐寰�',
+      },
+    },
+    {
+      fieldName: 'weightPerKm',
+      label: '姣忓叕閲岄噸閲�(kg/km)',
+      component: 'InputNumber',
+      componentProps: {
+        class: '!w-full',
+        min: 0,
+        precision: 2,
+        placeholder: '璇疯緭鍏ユ瘡鍏噷閲嶉噺',
+      },
+    },
+    {
+      fieldName: 'standardCode',
+      label: '鎵ц鏍囧噯',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ユ墽琛屾爣鍑嗕唬鍙�',
+      },
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      formItemClass: 'col-span-3',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ娉�',
+        rows: 2,
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'itemId',
+      label: '鐗╂枡',
+      component: markRaw(MdItemSelect),
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇烽�夋嫨鎹綅瀵肩嚎鐗╂枡',
+      },
+    },
+    {
+      fieldName: 'orderSpec',
+      label: '璁㈣揣瑙勬牸',
+      component: 'Input',
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇疯緭鍏ヨ璐ц鏍�',
+      },
+    },
+    {
+      fieldName: 'conductorType',
+      label: '瀵间綋绫诲瀷',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: getDictOptions(DICT_TYPE.PRODUCT_CONDUCTOR_TYPE, 'string'),
+        placeholder: '璇烽�夋嫨瀵间綋绫诲瀷',
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions<MesPdCtcApi.Ctc>['columns'] {
+  return [
+    { field: 'itemName', title: '鐗╂枡鍚嶇О', minWidth: 140 },
+    { field: 'itemCode', title: '鐗╂枡缂栫爜', minWidth: 120 },
+    { field: 'orderSpec', title: '璁㈣揣瑙勬牸', minWidth: 140 },
+    { field: 'transpositionSpec', title: '鎹綅瑙勬牸', minWidth: 120 },
+    {
+      field: 'conductorType',
+      title: '瀵间綋绫诲瀷',
+      width: 100,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.PRODUCT_CONDUCTOR_TYPE },
+      },
+    },
+    { field: 'crossSection', title: '鎴潰绉�(mm虏)', width: 110 },
+    { field: 'outerDiameter', title: '澶栧緞(mm)', width: 100 },
+    { field: 'resistance20c', title: '20鈩冪數闃�(惟/km)', width: 140 },
+    { field: 'wrappingAngle', title: '缁曞寘瑙掑害(掳)', width: 110 },
+    { field: 'ratedVoltage', title: '棰濆畾鐢靛帇(kV)', width: 110 },
+    { field: 'standardCode', title: '鎵ц鏍囧噯', minWidth: 140 },
+    {
+      field: 'createTime',
+      title: '鍒涘缓鏃堕棿',
+      width: 170,
+      formatter: 'formatDateTime',
+    },
+    {
+      title: '鎿嶄綔',
+      width: 220,
+      fixed: 'right',
+      slots: {
+        default: 'actions',
+      },
+    },
+  ];
+}
diff --git a/src/views/mes/pd/ctc/index.vue b/src/views/mes/pd/ctc/index.vue
new file mode 100644
index 0000000..d1d500e
--- /dev/null
+++ b/src/views/mes/pd/ctc/index.vue
@@ -0,0 +1,161 @@
+<script lang="ts" setup>
+  import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+  import type { MesPdCtcApi } from '#/api/mes/pd/ctc';
+
+  import { ref } from 'vue';
+
+  import { Page, useVbenModal } from '@vben/common-ui';
+  import { $t } from '@vben/locales';
+  import { downloadFileFromBlobPart } from '@vben/utils';
+
+  import { Button, message } from 'ant-design-vue';
+
+  import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+  import {
+    deleteCtc,
+    exportCtc,
+    getCtcPage,
+  } from '#/api/mes/pd/ctc';
+
+  import { useGridColumns, useGridFormSchema } from './data';
+  import Form from './modules/form.vue';
+
+  const [FormModal, formModalApi] = useVbenModal({
+    connectedComponent: Form,
+    destroyOnClose: true,
+  });
+
+  /** 鍒锋柊琛ㄦ牸 */
+  function handleRefresh() {
+    gridApi.query();
+  }
+
+  /** 鏂板 */
+  function handleCreate() {
+    formModalApi.setData({ formType: 'create' }).open();
+  }
+
+  /** 鏌ョ湅 */
+  function handleDetail(row: MesPdCtcApi.Ctc) {
+    formModalApi.setData({ id: row.id, formType: 'detail' }).open();
+  }
+
+  /** 缂栬緫 */
+  function handleEdit(row: MesPdCtcApi.Ctc) {
+    formModalApi.setData({ id: row.id, formType: 'update' }).open();
+  }
+
+  /** 鍒犻櫎 */
+  async function handleDelete(row: MesPdCtcApi.Ctc) {
+    const hideLoading = message.loading({
+      content: $t('ui.actionMessage.deleting', [row.orderSpec ?? '']),
+      duration: 0,
+    });
+    try {
+      await deleteCtc(row.id!);
+      message.success($t('ui.actionMessage.deleteSuccess', [row.orderSpec ?? '']));
+      handleRefresh();
+    } finally {
+      hideLoading();
+    }
+  }
+
+  /** 瀵煎嚭 */
+  async function handleExport() {
+    const data = await exportCtc(await gridApi.formApi.getValues());
+    downloadFileFromBlobPart({ fileName: '鎹綅瀵肩嚎鎶�鏈弬鏁�.xls', source: data });
+  }
+
+  const [Grid, gridApi] = useVbenVxeGrid({
+    formOptions: {
+      schema: useGridFormSchema(),
+    },
+    gridOptions: {
+      columns: useGridColumns(),
+      height: 'auto',
+      keepSource: true,
+      proxyConfig: {
+        ajax: {
+          query: async ({ page }, formValues) =>
+            await getCtcPage({
+              pageNo: page.currentPage,
+              pageSize: page.pageSize,
+              ...formValues,
+            }),
+        },
+      },
+      rowConfig: {
+        keyField: 'id',
+        isHover: true,
+      },
+      toolbarConfig: {
+        refresh: true,
+        search: true,
+      },
+    } as VxeTableGridOptions<MesPdCtcApi.Ctc>,
+  });
+</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: ['mes:pd-ctc:create'],
+              onClick: handleCreate,
+            },
+            {
+              label: $t('ui.actionTitle.export'),
+              type: 'primary',
+              icon: ACTION_ICON.DOWNLOAD,
+              auth: ['mes:pd-ctc:export'],
+              onClick: handleExport,
+            },
+          ]"
+        />
+      </template>
+      <template #orderSpec="{ row }">
+        <Button type="link" @click="handleDetail(row)">
+          {{ row.orderSpec || row.itemName }}
+        </Button>
+      </template>
+      <template #actions="{ row }">
+        <TableAction
+          :actions="[
+            {
+              label: $t('common.edit'),
+              type: 'link',
+              icon: ACTION_ICON.EDIT,
+              auth: ['mes:pd-ctc:update'],
+              onClick: handleEdit.bind(null, row),
+            },
+            {
+              label: $t('common.delete'),
+              type: 'link',
+              danger: true,
+              icon: ACTION_ICON.DELETE,
+              auth: ['mes:pd-ctc:delete'],
+              popConfirm: {
+                title: $t('ui.actionMessage.deleteConfirm', [row.orderSpec ?? '']),
+                confirm: handleDelete.bind(null, row),
+              },
+            },
+            {
+              label: $t('common.detail'),
+              type: 'link',
+              icon: ACTION_ICON.VIEW,
+              auth: ['mes:pd-ctc:query'],
+              onClick: handleDetail.bind(null, row),
+            },
+          ]"
+        />
+      </template>
+    </Grid>
+  </Page>
+</template>
diff --git a/src/views/mes/pd/ctc/modules/calc-modal.vue b/src/views/mes/pd/ctc/modules/calc-modal.vue
new file mode 100644
index 0000000..30fb2ba
--- /dev/null
+++ b/src/views/mes/pd/ctc/modules/calc-modal.vue
@@ -0,0 +1,344 @@
+<script lang="ts" setup>
+  import { reactive, ref } from 'vue';
+
+  import { useVbenModal } from '@vben/common-ui';
+  import { $t } from '@vben/locales';
+
+  import {
+    Button,
+    Descriptions,
+    DescriptionsItem,
+    InputNumber,
+    message,
+    Select,
+    Tabs,
+  } from 'ant-design-vue';
+
+  import {
+    calcCtcAngle,
+    calcCtcResistance,
+    calcCtcReel,
+  } from '#/api/mes/pd/ctc';
+
+  const emit = defineEmits<{
+    angle: [value: { wrappingAngle: number }];
+    resistance: [value: { resistance20c: number }];
+    reel: [value: { capacityKm: number }];
+  }>();
+
+  /** 缁曞寘瑙掑害璁$畻琛ㄥ崟 */
+  const angleForm = reactive<{
+    conductorThickness?: number;
+    conductorWidth?: number;
+    cornerRadius?: number;
+    insulationThickness?: number;
+    tapeWidth?: number;
+    overlapWidth?: number;
+  }>({});
+  const angleResult = ref<{
+    circumference?: number;
+    wrappingAngle?: number;
+    wrappingPitch?: number;
+  }>();
+
+  /** 鐩存祦鐢甸樆璁$畻琛ㄥ崟 */
+  const resistanceForm = reactive<{
+    conductorThickness?: number;
+    conductorWidth?: number;
+    temperature?: number;
+    material?: string;
+  }>({ temperature: 75, material: 'copper' });
+  const resistanceResult = ref<{
+    crossSection?: number;
+    resistance20cKm?: number;
+    resistanceAtTemp?: number;
+    resistanceAtTempKm?: number;
+  }>();
+
+  /** 鐩樺叿瀹归噺璁$畻琛ㄥ崟 */
+  const reelForm = reactive<{
+    reelOuterDiameter?: number;
+    reelInnerDiameter?: number;
+    reelWidth?: number;
+    ctcThickness?: number;
+    ctcWidth?: number;
+  }>({});
+  const reelResult = ref<{
+    capacityMeter?: number;
+    capacityKm?: number;
+  }>();
+
+  /** 缁曞寘瑙掑害璁$畻 */
+  async function handleCalcAngle() {
+    const data = await calcCtcAngle(angleForm);
+    angleResult.value = data;
+  }
+
+  /** 鐩存祦鐢甸樆璁$畻 */
+  async function handleCalcResistance() {
+    const data = await calcCtcResistance(resistanceForm);
+    resistanceResult.value = data;
+  }
+
+  /** 鐩樺叿瀹归噺璁$畻 */
+  async function handleCalcReel() {
+    const data = await calcCtcReel(reelForm);
+    reelResult.value = data;
+  }
+
+  /** 鍥炲~缁曞寘瑙掑害 */
+  function handleFillAngle() {
+    if (angleResult.value?.wrappingAngle == null) {
+      message.warning('璇峰厛鎵ц缁曞寘瑙掑害璁$畻');
+      return;
+    }
+    emit('angle', { wrappingAngle: angleResult.value.wrappingAngle });
+    message.success($t('ui.actionMessage.operationSuccess'));
+  }
+
+  /** 鍥炲~鐩存祦鐢甸樆 */
+  function handleFillResistance() {
+    if (resistanceResult.value?.resistance20cKm == null) {
+      message.warning('璇峰厛鎵ц鐩存祦鐢甸樆璁$畻');
+      return;
+    }
+    emit('resistance', { resistance20c: resistanceResult.value.resistance20cKm });
+    message.success($t('ui.actionMessage.operationSuccess'));
+  }
+
+  /** 鍥炲~鐩樺叿瀹归噺 */
+  function handleFillReel() {
+    if (reelResult.value?.capacityKm == null) {
+      message.warning('璇峰厛鎵ц鐩樺叿瀹归噺璁$畻');
+      return;
+    }
+    emit('reel', { capacityKm: reelResult.value.capacityKm });
+    message.success($t('ui.actionMessage.operationSuccess'));
+  }
+
+  const [Modal, modalApi] = useVbenModal({
+    onOpenChange(isOpen: boolean) {
+      if (!isOpen) {
+        angleResult.value = undefined;
+        resistanceResult.value = undefined;
+        reelResult.value = undefined;
+      }
+    },
+  });
+</script>
+
+<template>
+  <Modal title="鎹綅瀵肩嚎璁$畻宸ュ叿" class="w-2/3">
+    <Tabs>
+      <Tabs.TabPane key="angle" tab="缁曞寘瑙掑害">
+        <div class="grid grid-cols-3 gap-4">
+          <div class="flex items-center gap-2">
+            <span class="w-28 text-right">瀵间綋鍘氬害(mm)</span>
+            <InputNumber
+              v-model:value="angleForm.conductorThickness"
+              class="!flex-1"
+              :min="0"
+              :precision="2"
+              placeholder="濡� 19.08"
+            />
+          </div>
+          <div class="flex items-center gap-2">
+            <span class="w-28 text-right">瀵间綋瀹藉害(mm)</span>
+            <InputNumber
+              v-model:value="angleForm.conductorWidth"
+              class="!flex-1"
+              :min="0"
+              :precision="2"
+              placeholder="濡� 12.6"
+            />
+          </div>
+          <div class="flex items-center gap-2">
+            <span class="w-28 text-right">鍦嗚鍗婂緞(mm)</span>
+            <InputNumber
+              v-model:value="angleForm.cornerRadius"
+              class="!flex-1"
+              :min="0"
+              :precision="2"
+              placeholder="濡� 1"
+            />
+          </div>
+          <div class="flex items-center gap-2">
+            <span class="w-28 text-right">缁濈紭鍘氬害(mm)</span>
+            <InputNumber
+              v-model:value="angleForm.insulationThickness"
+              class="!flex-1"
+              :min="0"
+              :precision="2"
+              placeholder="濡� 0.75"
+            />
+          </div>
+          <div class="flex items-center gap-2">
+            <span class="w-28 text-right">绾稿甫瀹藉害(mm)</span>
+            <InputNumber
+              v-model:value="angleForm.tapeWidth"
+              class="!flex-1"
+              :min="0"
+              :precision="2"
+              placeholder="濡� 22"
+            />
+          </div>
+          <div class="flex items-center gap-2">
+            <span class="w-28 text-right">閲嶅彔瀹藉害(mm)</span>
+            <InputNumber
+              v-model:value="angleForm.overlapWidth"
+              class="!flex-1"
+              :min="0"
+              :precision="2"
+              placeholder="濡� 2"
+            />
+          </div>
+        </div>
+        <div class="mt-4 flex gap-2">
+          <Button type="primary" @click="handleCalcAngle">璁$畻</Button>
+          <Button @click="handleFillAngle">鍥炲~缁曞寘瑙掑害</Button>
+        </div>
+        <Descriptions v-if="angleResult" class="mt-4" :column="3" bordered>
+          <DescriptionsItem label="瀵肩嚎鍛ㄩ暱(mm)">
+            {{ angleResult.circumference }}
+          </DescriptionsItem>
+          <DescriptionsItem label="缁曞寘瑙掑害(掳)">
+            {{ angleResult.wrappingAngle }}
+          </DescriptionsItem>
+          <DescriptionsItem label="缁曞寘鑺傝窛(mm)">
+            {{ angleResult.wrappingPitch }}
+          </DescriptionsItem>
+        </Descriptions>
+      </Tabs.TabPane>
+
+      <Tabs.TabPane key="resistance" tab="鐩存祦鐢甸樆">
+        <div class="grid grid-cols-3 gap-4">
+          <div class="flex items-center gap-2">
+            <span class="w-28 text-right">瀵间綋鍘氬害(mm)</span>
+            <InputNumber
+              v-model:value="resistanceForm.conductorThickness"
+              class="!flex-1"
+              :min="0"
+              :precision="2"
+              placeholder="濡� 1.27"
+            />
+          </div>
+          <div class="flex items-center gap-2">
+            <span class="w-28 text-right">瀵间綋瀹藉害(mm)</span>
+            <InputNumber
+              v-model:value="resistanceForm.conductorWidth"
+              class="!flex-1"
+              :min="0"
+              :precision="2"
+              placeholder="濡� 5.7"
+            />
+          </div>
+          <div class="flex items-center gap-2">
+            <span class="w-28 text-right">璁$畻娓╁害(鈩�)</span>
+            <InputNumber
+              v-model:value="resistanceForm.temperature"
+              class="!flex-1"
+              :min="0"
+              :precision="0"
+            />
+          </div>
+          <div class="flex items-center gap-2">
+            <span class="w-28 text-right">瀵间綋鏉愯川</span>
+            <Select
+              v-model:value="resistanceForm.material"
+              class="!flex-1"
+              :options="[
+                { label: '閾�', value: 'copper' },
+                { label: '閾�', value: 'aluminum' },
+              ]"
+            />
+          </div>
+        </div>
+        <div class="mt-4 flex gap-2">
+          <Button type="primary" @click="handleCalcResistance">璁$畻</Button>
+          <Button @click="handleFillResistance">鍥炲~20鈩冪數闃�</Button>
+        </div>
+        <Descriptions v-if="resistanceResult" class="mt-4" :column="2" bordered>
+          <DescriptionsItem label="鎴潰绉�(mm虏)">
+            {{ resistanceResult.crossSection }}
+          </DescriptionsItem>
+          <DescriptionsItem label="20鈩冪數闃�(惟/km)">
+            {{ resistanceResult.resistance20cKm }}
+          </DescriptionsItem>
+          <DescriptionsItem label="璁$畻娓╁害鐢甸樆(惟/m)">
+            {{ resistanceResult.resistanceAtTemp }}
+          </DescriptionsItem>
+          <DescriptionsItem label="璁$畻娓╁害鐢甸樆(惟/km)">
+            {{ resistanceResult.resistanceAtTempKm }}
+          </DescriptionsItem>
+        </Descriptions>
+      </Tabs.TabPane>
+
+      <Tabs.TabPane key="reel" tab="鐩樺叿瀹归噺">
+        <div class="grid grid-cols-3 gap-4">
+          <div class="flex items-center gap-2">
+            <span class="w-28 text-right">鐩樺叿澶栧緞 d1(mm)</span>
+            <InputNumber
+              v-model:value="reelForm.reelOuterDiameter"
+              class="!flex-1"
+              :min="0"
+              :precision="0"
+              placeholder="濡� 1600"
+            />
+          </div>
+          <div class="flex items-center gap-2">
+            <span class="w-28 text-right">鐩樿姱鍐呭緞 d2(mm)</span>
+            <InputNumber
+              v-model:value="reelForm.reelInnerDiameter"
+              class="!flex-1"
+              :min="0"
+              :precision="0"
+              placeholder="濡� 1100"
+            />
+          </div>
+          <div class="flex items-center gap-2">
+            <span class="w-28 text-right">鐩樺 l2(mm)</span>
+            <InputNumber
+              v-model:value="reelForm.reelWidth"
+              class="!flex-1"
+              :min="0"
+              :precision="0"
+              placeholder="濡� 500"
+            />
+          </div>
+          <div class="flex items-center gap-2">
+            <span class="w-28 text-right">CTC楂樺害(mm)</span>
+            <InputNumber
+              v-model:value="reelForm.ctcThickness"
+              class="!flex-1"
+              :min="0"
+              :precision="2"
+              placeholder="濡� 19.08"
+            />
+          </div>
+          <div class="flex items-center gap-2">
+            <span class="w-28 text-right">CTC瀹藉害(mm)</span>
+            <InputNumber
+              v-model:value="reelForm.ctcWidth"
+              class="!flex-1"
+              :min="0"
+              :precision="2"
+              placeholder="濡� 12.6"
+            />
+          </div>
+        </div>
+        <div class="mt-4 flex gap-2">
+          <Button type="primary" @click="handleCalcReel">璁$畻</Button>
+          <Button @click="handleFillReel">鍥炲~鏈�澶у閲�</Button>
+        </div>
+        <Descriptions v-if="reelResult" class="mt-4" :column="2" bordered>
+          <DescriptionsItem label="姣忕洏闀垮害(m)">
+            {{ reelResult.capacityMeter }}
+          </DescriptionsItem>
+          <DescriptionsItem label="姣忕洏闀垮害(km)">
+            {{ reelResult.capacityKm }}
+          </DescriptionsItem>
+        </Descriptions>
+      </Tabs.TabPane>
+    </Tabs>
+  </Modal>
+</template>
diff --git a/src/views/mes/pd/ctc/modules/form.vue b/src/views/mes/pd/ctc/modules/form.vue
new file mode 100644
index 0000000..989abef
--- /dev/null
+++ b/src/views/mes/pd/ctc/modules/form.vue
@@ -0,0 +1,134 @@
+<script lang="ts" setup>
+  import type { FormType } from '../data';
+
+  import type { MesPdCtcApi } from '#/api/mes/pd/ctc';
+
+  import { computed, ref } from 'vue';
+
+  import { useVbenModal } from '@vben/common-ui';
+  import { $t } from '@vben/locales';
+
+  import { Button, message } from 'ant-design-vue';
+
+  import { useVbenForm } from '#/adapter/form';
+  import {
+    createCtc,
+    getCtc,
+    updateCtc,
+  } from '#/api/mes/pd/ctc';
+
+  import { useFormSchema } from '../data';
+  import CalcModal from './calc-modal.vue';
+
+  const emit = defineEmits(['success']);
+  const formType = ref<FormType>('create'); // 琛ㄥ崟妯″紡
+  const formData = ref<MesPdCtcApi.Ctc>();
+  const isDetail = computed(() => formType.value === 'detail'); // 鏄惁鏌ョ湅妯″紡
+  const getTitle = computed(() => {
+    if (formType.value === 'detail') {
+      return '鏌ョ湅鎹綅瀵肩嚎鎶�鏈弬鏁�';
+    }
+    return formType.value === 'update' ? '淇敼鎹綅瀵肩嚎鎶�鏈弬鏁�' : '鏂板鎹綅瀵肩嚎鎶�鏈弬鏁�';
+  });
+
+  const [Form, formApi] = useVbenForm({
+    commonConfig: {
+      componentProps: {
+        class: 'w-full',
+      },
+      formItemClass: 'col-span-1',
+      labelWidth: 130,
+    },
+    wrapperClass: 'grid-cols-3',
+    layout: 'horizontal',
+    schema: [],
+    showDefaultActions: false,
+  });
+
+  const [CalcModal, calcModalApi] = useVbenModal({
+    connectedComponent: CalcModal,
+    destroyOnClose: true,
+  });
+
+  /** 鎵撳紑璁$畻宸ュ叿 */
+  function handleOpenCalc() {
+    calcModalApi.open();
+  }
+
+  /** 鍥炲~缁曞寘瑙掑害 */
+  function handleFillAngle(value: { wrappingAngle: number }) {
+    formApi.setFieldValue('wrappingAngle', value.wrappingAngle);
+  }
+
+  /** 鍥炲~鐩存祦鐢甸樆 */
+  function handleFillResistance(value: { resistance20c: number }) {
+    formApi.setFieldValue('resistance20c', value.resistance20c);
+  }
+
+  const [Modal, modalApi] = useVbenModal({
+    async onConfirm() {
+      if (isDetail.value) {
+        await modalApi.close();
+        return;
+      }
+      const { valid } = await formApi.validate();
+      if (!valid) {
+        return;
+      }
+      modalApi.lock();
+      // 鎻愪氦琛ㄥ崟
+      const data = (await formApi.getValues()) as MesPdCtcApi.Ctc;
+      try {
+        await (data.id ? updateCtc(data) : createCtc(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.setState({ schema: useFormSchema() });
+      formApi.setDisabled(formType.value === 'detail');
+      modalApi.setState({ showConfirmButton: formType.value !== 'detail' });
+      if (!data?.id) {
+        return;
+      }
+      modalApi.lock();
+      try {
+        formData.value = await getCtc(data.id);
+        // 璁剧疆鍒� values
+        await formApi.setValues(formData.value);
+      } finally {
+        modalApi.unlock();
+      }
+    },
+  });
+</script>
+
+<template>
+  <Modal :title="getTitle" class="w-4/5">
+    <Form class="mx-4" />
+    <template #prepend-footer>
+      <Button
+        v-if="!isDetail"
+        type="default"
+        @click="handleOpenCalc"
+      >
+        璁$畻宸ュ叿
+      </Button>
+    </template>
+    <CalcModal
+      @angle="handleFillAngle"
+      @resistance="handleFillResistance"
+    />
+  </Modal>
+</template>
diff --git a/src/views/mes/pd/ctc/modules/index.ts b/src/views/mes/pd/ctc/modules/index.ts
new file mode 100644
index 0000000..42004c2
--- /dev/null
+++ b/src/views/mes/pd/ctc/modules/index.ts
@@ -0,0 +1 @@
+export { default as CalcModal } from './calc-modal.vue';
diff --git a/src/views/mes/pd/tech-confirm/data.ts b/src/views/mes/pd/tech-confirm/data.ts
new file mode 100644
index 0000000..ea967b2
--- /dev/null
+++ b/src/views/mes/pd/tech-confirm/data.ts
@@ -0,0 +1,260 @@
+import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { CrmCustomerApi } from '#/api/crm/customer';
+import type { MesPdTechConfirmApi } from '#/api/mes/pd/tech-confirm';
+
+import { markRaw } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import CrmCustomerSelect from '#/components/crm-customer-select.vue';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'detail' | 'update';
+
+/** 鏂板/淇敼浜у搧鎶�鏈‘璁ゅ嚱 */
+export function useFormSchema(
+  formApi?: VbenFormApi,
+): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: {
+        triggerFields: [''],
+        show: () => false,
+      },
+    },
+    {
+      fieldName: 'confirmNo',
+      label: '纭鍑界紪鍙�',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ョ‘璁ゅ嚱缂栧彿',
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'customerId',
+      label: '瀹㈡埛',
+      component: markRaw(CrmCustomerSelect),
+      componentProps: {
+        placeholder: '璇烽�夋嫨瀹㈡埛',
+        onChange: (item?: CrmCustomerApi.Customer) => {
+          formApi?.setFieldValue('customerName', item?.name);
+        },
+      },
+    },
+    {
+      fieldName: 'customerName',
+      label: '瀹㈡埛鍚嶇О',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ鎴峰悕绉�',
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'productName',
+      label: '浜у搧鍚嶇О',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ヤ骇鍝佸悕绉�',
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'specification',
+      label: '瑙勬牸鍨嬪彿',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ヨ鏍煎瀷鍙�',
+      },
+    },
+    {
+      fieldName: 'techParams',
+      label: '鎶�鏈弬鏁�(JSON)',
+      component: 'Textarea',
+      formItemClass: 'col-span-3',
+      componentProps: {
+        placeholder: '璇疯緭鍏ユ妧鏈弬鏁� JSON锛屽 {"thickness":"1.27","width":"5.7"}',
+        rows: 4,
+      },
+    },
+    {
+      fieldName: 'templateType',
+      label: '妯℃澘绫诲瀷',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: getDictOptions(DICT_TYPE.PRODUCT_TECH_CONFIRM_TEMPLATE, 'string'),
+        placeholder: '璇烽�夋嫨妯℃澘绫诲瀷',
+      },
+    },
+    {
+      fieldName: 'status',
+      label: '鐘舵��',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: getDictOptions(DICT_TYPE.PRODUCT_TECH_CONFIRM_STATUS, 'number'),
+        placeholder: '璇烽�夋嫨鐘舵��',
+      },
+    },
+    {
+      fieldName: 'drafterName',
+      label: '缂栧埗浜�',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ョ紪鍒朵汉濮撳悕',
+      },
+    },
+    {
+      fieldName: 'draftTime',
+      label: '缂栧埗鏃堕棿',
+      component: 'DatePicker',
+      componentProps: {
+        class: '!w-full',
+        format: 'YYYY-MM-DD HH:mm:ss',
+        showTime: true,
+        valueFormat: 'YYYY-MM-DD HH:mm:ss',
+      },
+    },
+    {
+      fieldName: 'reviewerName',
+      label: '瀹℃牳浜�',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ鏍镐汉濮撳悕',
+      },
+    },
+    {
+      fieldName: 'reviewTime',
+      label: '瀹℃牳鏃堕棿',
+      component: 'DatePicker',
+      componentProps: {
+        class: '!w-full',
+        format: 'YYYY-MM-DD HH:mm:ss',
+        showTime: true,
+        valueFormat: 'YYYY-MM-DD HH:mm:ss',
+      },
+    },
+    {
+      fieldName: 'customerConfirmUser',
+      label: '瀹㈡埛纭浜�',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ鎴风‘璁や汉',
+      },
+    },
+    {
+      fieldName: 'customerConfirmTime',
+      label: '瀹㈡埛纭鏃堕棿',
+      component: 'DatePicker',
+      componentProps: {
+        class: '!w-full',
+        format: 'YYYY-MM-DD HH:mm:ss',
+        showTime: true,
+        valueFormat: 'YYYY-MM-DD HH:mm:ss',
+      },
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      formItemClass: 'col-span-3',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ娉�',
+        rows: 2,
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'confirmNo',
+      label: '纭鍑界紪鍙�',
+      component: 'Input',
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇疯緭鍏ョ‘璁ゅ嚱缂栧彿',
+      },
+    },
+    {
+      fieldName: 'productName',
+      label: '浜у搧鍚嶇О',
+      component: 'Input',
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇疯緭鍏ヤ骇鍝佸悕绉�',
+      },
+    },
+    {
+      fieldName: 'templateType',
+      label: '妯℃澘绫诲瀷',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: getDictOptions(DICT_TYPE.PRODUCT_TECH_CONFIRM_TEMPLATE, 'string'),
+        placeholder: '璇烽�夋嫨妯℃澘绫诲瀷',
+      },
+    },
+    {
+      fieldName: 'status',
+      label: '鐘舵��',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: getDictOptions(DICT_TYPE.PRODUCT_TECH_CONFIRM_STATUS, 'number'),
+        placeholder: '璇烽�夋嫨鐘舵��',
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions<MesPdTechConfirmApi.TechConfirm>['columns'] {
+  return [
+    { field: 'confirmNo', title: '纭鍑界紪鍙�', minWidth: 160 },
+    { field: 'customerName', title: '瀹㈡埛鍚嶇О', minWidth: 180 },
+    { field: 'productName', title: '浜у搧鍚嶇О', minWidth: 140 },
+    { field: 'specification', title: '瑙勬牸鍨嬪彿', minWidth: 140 },
+    {
+      field: 'templateType',
+      title: '妯℃澘绫诲瀷',
+      width: 110,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.PRODUCT_TECH_CONFIRM_TEMPLATE },
+      },
+    },
+    { field: 'drafterName', title: '缂栧埗浜�', width: 90 },
+    {
+      field: 'status',
+      title: '鐘舵��',
+      width: 110,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.PRODUCT_TECH_CONFIRM_STATUS },
+      },
+    },
+    {
+      field: 'createTime',
+      title: '鍒涘缓鏃堕棿',
+      width: 170,
+      formatter: 'formatDateTime',
+    },
+    {
+      title: '鎿嶄綔',
+      width: 260,
+      fixed: 'right',
+      slots: {
+        default: 'actions',
+      },
+    },
+  ];
+}
diff --git a/src/views/mes/pd/tech-confirm/index.vue b/src/views/mes/pd/tech-confirm/index.vue
new file mode 100644
index 0000000..88f2bf0
--- /dev/null
+++ b/src/views/mes/pd/tech-confirm/index.vue
@@ -0,0 +1,198 @@
+<script lang="ts" setup>
+  import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+  import type { MesPdTechConfirmApi } from '#/api/mes/pd/tech-confirm';
+
+  import { ref } from 'vue';
+
+  import { Page, useVbenModal } from '@vben/common-ui';
+  import { $t } from '@vben/locales';
+  import { downloadFileFromBlobPart } from '@vben/utils';
+
+  import { Button, message } from 'ant-design-vue';
+
+  import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+  import {
+    deleteTechConfirm,
+    exportTechConfirm,
+    getTechConfirmPage,
+    updateTechConfirm,
+  } from '#/api/mes/pd/tech-confirm';
+
+  import { useGridColumns, useGridFormSchema } from './data';
+  import Form from './modules/form.vue';
+
+  const [FormModal, formModalApi] = useVbenModal({
+    connectedComponent: Form,
+    destroyOnClose: true,
+  });
+
+  /** 鍒锋柊琛ㄦ牸 */
+  function handleRefresh() {
+    gridApi.query();
+  }
+
+  /** 鏂板 */
+  function handleCreate() {
+    formModalApi.setData({ formType: 'create' }).open();
+  }
+
+  /** 鏌ョ湅 */
+  function handleDetail(row: MesPdTechConfirmApi.TechConfirm) {
+    formModalApi.setData({ id: row.id, formType: 'detail' }).open();
+  }
+
+  /** 缂栬緫 */
+  function handleEdit(row: MesPdTechConfirmApi.TechConfirm) {
+    formModalApi.setData({ id: row.id, formType: 'update' }).open();
+  }
+
+  /** 鍒犻櫎 */
+  async function handleDelete(row: MesPdTechConfirmApi.TechConfirm) {
+    const hideLoading = message.loading({
+      content: $t('ui.actionMessage.deleting', [row.confirmNo ?? '']),
+      duration: 0,
+    });
+    try {
+      await deleteTechConfirm(row.id!);
+      message.success($t('ui.actionMessage.deleteSuccess', [row.confirmNo ?? '']));
+      handleRefresh();
+    } finally {
+      hideLoading();
+    }
+  }
+
+  /** 鐘舵�佹祦杞� */
+  async function handleStatus(row: MesPdTechConfirmApi.TechConfirm, next: number) {
+    const hideLoading = message.loading({ content: '鐘舵�佹洿鏂颁腑...', duration: 0 });
+    try {
+      await updateTechConfirm({ ...row, status: next });
+      message.success($t('ui.actionMessage.operationSuccess'));
+      handleRefresh();
+    } finally {
+      hideLoading();
+    }
+  }
+
+  /** 瀵煎嚭 */
+  async function handleExport() {
+    const data = await exportTechConfirm(await gridApi.formApi.getValues());
+    downloadFileFromBlobPart({ fileName: '浜у搧鎶�鏈‘璁ゅ嚱.xls', source: data });
+  }
+
+  const [Grid, gridApi] = useVbenVxeGrid({
+    formOptions: {
+      schema: useGridFormSchema(),
+    },
+    gridOptions: {
+      columns: useGridColumns(),
+      height: 'auto',
+      keepSource: true,
+      proxyConfig: {
+        ajax: {
+          query: async ({ page }, formValues) =>
+            await getTechConfirmPage({
+              pageNo: page.currentPage,
+              pageSize: page.pageSize,
+              ...formValues,
+            }),
+        },
+      },
+      rowConfig: {
+        keyField: 'id',
+        isHover: true,
+      },
+      toolbarConfig: {
+        refresh: true,
+        search: true,
+      },
+    } as VxeTableGridOptions<MesPdTechConfirmApi.TechConfirm>,
+  });
+</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: ['mes:pd-tech-confirm:create'],
+              onClick: handleCreate,
+            },
+            {
+              label: $t('ui.actionTitle.export'),
+              type: 'primary',
+              icon: ACTION_ICON.DOWNLOAD,
+              auth: ['mes:pd-tech-confirm:export'],
+              onClick: handleExport,
+            },
+          ]"
+        />
+      </template>
+      <template #confirmNo="{ row }">
+        <Button type="link" @click="handleDetail(row)">
+          {{ row.confirmNo }}
+        </Button>
+      </template>
+      <template #actions="{ row }">
+        <TableAction
+          :actions="[
+            {
+              label: '鎻愪氦瀹℃牳',
+              type: 'link',
+              icon: ACTION_ICON.AUDIT,
+              auth: ['mes:pd-tech-confirm:update'],
+              ifShow: row.status === 0,
+              onClick: handleStatus.bind(null, row, 1),
+            },
+            {
+              label: '瀹℃牳閫氳繃',
+              type: 'link',
+              icon: ACTION_ICON.AUDIT,
+              auth: ['mes:pd-tech-confirm:update'],
+              ifShow: row.status === 1,
+              onClick: handleStatus.bind(null, row, 2),
+            },
+            {
+              label: '瀹㈡埛纭',
+              type: 'link',
+              icon: ACTION_ICON.AUDIT,
+              auth: ['mes:pd-tech-confirm:update'],
+              ifShow: row.status === 2,
+              onClick: handleStatus.bind(null, row, 3),
+            },
+            {
+              label: $t('common.edit'),
+              type: 'link',
+              icon: ACTION_ICON.EDIT,
+              auth: ['mes:pd-tech-confirm:update'],
+              onClick: handleEdit.bind(null, row),
+            },
+            {
+              label: $t('common.delete'),
+              type: 'link',
+              danger: true,
+              icon: ACTION_ICON.DELETE,
+              auth: ['mes:pd-tech-confirm:delete'],
+              popConfirm: {
+                title: $t('ui.actionMessage.deleteConfirm', [row.confirmNo ?? '']),
+                confirm: handleDelete.bind(null, row),
+              },
+            },
+            {
+              label: $t('common.detail'),
+              type: 'link',
+              icon: ACTION_ICON.VIEW,
+              auth: ['mes:pd-tech-confirm:query'],
+              onClick: handleDetail.bind(null, row),
+            },
+          ]"
+        />
+      </template>
+    </Grid>
+  </Page>
+</template>
diff --git a/src/views/mes/pd/tech-confirm/modules/form.vue b/src/views/mes/pd/tech-confirm/modules/form.vue
new file mode 100644
index 0000000..5ae8653
--- /dev/null
+++ b/src/views/mes/pd/tech-confirm/modules/form.vue
@@ -0,0 +1,100 @@
+<script lang="ts" setup>
+  import type { FormType } from '../data';
+
+  import type { MesPdTechConfirmApi } from '#/api/mes/pd/tech-confirm';
+
+  import { computed, ref } from 'vue';
+
+  import { useVbenModal } from '@vben/common-ui';
+  import { $t } from '@vben/locales';
+
+  import { message } from 'ant-design-vue';
+
+  import { useVbenForm } from '#/adapter/form';
+  import {
+    createTechConfirm,
+    getTechConfirm,
+    updateTechConfirm,
+  } from '#/api/mes/pd/tech-confirm';
+
+  import { useFormSchema } from '../data';
+
+  const emit = defineEmits(['success']);
+  const formType = ref<FormType>('create'); // 琛ㄥ崟妯″紡
+  const formData = ref<MesPdTechConfirmApi.TechConfirm>();
+  const isDetail = computed(() => formType.value === 'detail'); // 鏄惁鏌ョ湅妯″紡
+  const getTitle = computed(() => {
+    if (formType.value === 'detail') {
+      return '鏌ョ湅鎶�鏈‘璁ゅ嚱';
+    }
+    return formType.value === 'update' ? '淇敼鎶�鏈‘璁ゅ嚱' : '鏂板鎶�鏈‘璁ゅ嚱';
+  });
+
+  const [Form, formApi] = useVbenForm({
+    commonConfig: {
+      componentProps: {
+        class: 'w-full',
+      },
+      formItemClass: 'col-span-1',
+      labelWidth: 130,
+    },
+    wrapperClass: 'grid-cols-3',
+    layout: 'horizontal',
+    schema: [],
+    showDefaultActions: false,
+  });
+
+  const [Modal, modalApi] = useVbenModal({
+    async onConfirm() {
+      if (isDetail.value) {
+        await modalApi.close();
+        return;
+      }
+      const { valid } = await formApi.validate();
+      if (!valid) {
+        return;
+      }
+      modalApi.lock();
+      // 鎻愪氦琛ㄥ崟
+      const data = (await formApi.getValues()) as MesPdTechConfirmApi.TechConfirm;
+      try {
+        await (data.id ? updateTechConfirm(data) : createTechConfirm(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.setState({ schema: useFormSchema(formApi) });
+      formApi.setDisabled(formType.value === 'detail');
+      modalApi.setState({ showConfirmButton: formType.value !== 'detail' });
+      if (!data?.id) {
+        return;
+      }
+      modalApi.lock();
+      try {
+        formData.value = await getTechConfirm(data.id);
+        // 璁剧疆鍒� values
+        await formApi.setValues(formData.value);
+      } finally {
+        modalApi.unlock();
+      }
+    },
+  });
+</script>
+
+<template>
+  <Modal :title="getTitle" class="w-4/5">
+    <Form class="mx-4" />
+  </Modal>
+</template>
diff --git a/src/views/mes/pro/emergencyplan/data.ts b/src/views/mes/pro/emergencyplan/data.ts
new file mode 100644
index 0000000..04e599c
--- /dev/null
+++ b/src/views/mes/pro/emergencyplan/data.ts
@@ -0,0 +1,187 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'detail' | 'edit';
+
+/** 鐘舵�佸父閲� */
+export const PLAN_STATUS = {
+  DRAFT: 0,
+  PUBLISHED: 10,
+};
+
+/** 琛ㄥ崟鐨勯厤缃」 */
+export function useFormSchema(formType: FormType): VbenFormSchema[] {
+  const isReadonly = formType === 'detail';
+  return [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: {
+        triggerFields: [''],
+        show: () => false,
+      },
+    },
+    {
+      fieldName: 'code',
+      label: '棰勬缂栫爜',
+      component: 'Input',
+      componentProps: {
+        placeholder: '绯荤粺鑷姩鐢熸垚',
+        disabled: true,
+      },
+    },
+    {
+      fieldName: 'name',
+      label: '棰勬鍚嶇О',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ラ妗堝悕绉�',
+        disabled: isReadonly,
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'planType',
+      label: '棰勬绫诲瀷',
+      component: 'Select',
+      componentProps: {
+        options: getDictOptions(DICT_TYPE.MES_PRO_EMERGENCY_PLAN_TYPE, 'number'),
+        placeholder: '璇烽�夋嫨棰勬绫诲瀷',
+        disabled: isReadonly,
+      },
+      rules: 'selectRequired',
+    },
+    {
+      fieldName: 'orgUnit',
+      label: '璐d换鍗曚綅',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ヨ矗浠诲崟浣�/閮ㄩ棬',
+        disabled: isReadonly,
+      },
+    },
+    {
+      fieldName: 'content',
+      label: '澶勭疆鍐呭',
+      component: 'Textarea',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ簲鎬ュ缃唴瀹�',
+        rows: 4,
+        disabled: isReadonly,
+      },
+      formItemClass: 'col-span-3',
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ娉�',
+        autoSize: { minRows: 1, maxRows: 1 },
+        disabled: isReadonly,
+      },
+      formItemClass: 'col-span-3',
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'code',
+      label: '棰勬缂栫爜',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ラ妗堢紪鐮�',
+      },
+    },
+    {
+      fieldName: 'name',
+      label: '棰勬鍚嶇О',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ラ妗堝悕绉�',
+      },
+    },
+    {
+      fieldName: 'planType',
+      label: '棰勬绫诲瀷',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: getDictOptions(DICT_TYPE.MES_PRO_EMERGENCY_PLAN_TYPE, 'number'),
+        placeholder: '璇烽�夋嫨棰勬绫诲瀷',
+      },
+    },
+    {
+      fieldName: 'status',
+      label: '鐘舵��',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: [
+          { label: '鑽夌', value: PLAN_STATUS.DRAFT },
+          { label: '宸插彂甯�', value: PLAN_STATUS.PUBLISHED },
+        ],
+        placeholder: '璇烽�夋嫨鐘舵��',
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勮〃鏍煎垪 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+  return [
+    { type: 'checkbox', width: 50 },
+    { type: 'seq', title: '搴忓彿', width: 60 },
+    {
+      field: 'code',
+      title: '棰勬缂栫爜',
+      minWidth: 150,
+      slots: { default: 'code' },
+    },
+    {
+      field: 'name',
+      title: '棰勬鍚嶇О',
+      minWidth: 200,
+    },
+    {
+      field: 'planType',
+      title: '棰勬绫诲瀷',
+      width: 110,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.MES_PRO_EMERGENCY_PLAN_TYPE },
+      },
+    },
+    {
+      field: 'orgUnit',
+      title: '璐d换鍗曚綅',
+      minWidth: 140,
+    },
+    {
+      field: 'status',
+      title: '鐘舵��',
+      width: 100,
+      slots: { default: 'status' },
+    },
+    {
+      field: 'createTime',
+      title: '鍒涘缓鏃堕棿',
+      width: 170,
+      formatter: 'formatDateTime',
+    },
+    {
+      field: 'action',
+      title: '鎿嶄綔',
+      width: 260,
+      fixed: 'right',
+      slots: { default: 'actions' },
+    },
+  ];
+}
diff --git a/src/views/mes/pro/emergencyplan/index.vue b/src/views/mes/pro/emergencyplan/index.vue
new file mode 100644
index 0000000..501f310
--- /dev/null
+++ b/src/views/mes/pro/emergencyplan/index.vue
@@ -0,0 +1,188 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesProEmergencyPlanApi } from '#/api/mes/pro/emergencyplan';
+
+import { ref } from 'vue';
+
+import { Page, useVbenModal } from '@vben/common-ui';
+import { isEmpty } from '@vben/utils';
+
+import { message, Tag } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import { deletePlan, getPlanPage, publishPlan } from '#/api/mes/pro/emergencyplan';
+import { $t } from '#/locales';
+
+import { PLAN_STATUS, useGridColumns, useGridFormSchema } from './data';
+import Form from './modules/form.vue';
+
+/** MES 搴旀�ュ缃妗堝垪琛� */
+defineOptions({ name: 'MesProEmergencyPlan' });
+
+const [FormModal, formModalApi] = useVbenModal({
+  connectedComponent: Form,
+  destroyOnClose: true,
+});
+
+function handleRefresh() {
+  gridApi.query();
+}
+
+function handleCreate() {
+  formModalApi.setData({ formType: 'create' }).open();
+}
+
+function handleEdit(row: MesProEmergencyPlanApi.EmergencyPlan) {
+  formModalApi.setData({ formType: 'edit', id: row.id }).open();
+}
+
+function handleDetail(row: MesProEmergencyPlanApi.EmergencyPlan) {
+  formModalApi.setData({ formType: 'detail', id: row.id }).open();
+}
+
+async function handleDelete(ids: number[]) {
+  const hideLoading = message.loading({
+    content: $t('ui.actionMessage.deleting'),
+    duration: 0,
+  });
+  try {
+    await deletePlan(ids);
+    message.success($t('ui.actionMessage.deleteSuccess'));
+    handleRefresh();
+  } finally {
+    hideLoading();
+  }
+}
+
+async function handlePublish(row: MesProEmergencyPlanApi.EmergencyPlan) {
+  await publishPlan(row.id!);
+  message.success('宸插彂甯�');
+  handleRefresh();
+}
+
+const checkedIds = ref<number[]>([]);
+function handleRowCheckboxChange({
+  records,
+}: {
+  records: MesProEmergencyPlanApi.EmergencyPlan[];
+}) {
+  checkedIds.value = records.map((item) => item.id!);
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  formOptions: {
+    schema: useGridFormSchema(),
+  },
+  gridOptions: {
+    columns: useGridColumns(),
+    height: 'auto',
+    keepSource: true,
+    proxyConfig: {
+      ajax: {
+        query: async ({ page }, formValues) => {
+          return await getPlanPage({
+            pageNo: page.currentPage,
+            pageSize: page.pageSize,
+            ...formValues,
+          });
+        },
+      },
+    },
+    rowConfig: {
+      keyField: 'id',
+      isHover: true,
+    },
+    toolbarConfig: {
+      refresh: true,
+      search: true,
+    },
+  } as VxeTableGridOptions<MesProEmergencyPlanApi.EmergencyPlan>,
+  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: ['mes:pro-emergency-plan:create'],
+              onClick: handleCreate,
+            },
+            {
+              label: '鎵归噺鍒犻櫎',
+              type: 'primary',
+              danger: true,
+              disabled: isEmpty(checkedIds),
+              icon: ACTION_ICON.DELETE,
+              auth: ['mes:pro-emergency-plan:delete'],
+              popConfirm: {
+                title: `鏄惁鍒犻櫎鎵�閫変腑鏁版嵁锛焋,
+                confirm: handleDelete.bind(null, checkedIds),
+              },
+            },
+          ]"
+        />
+      </template>
+      <template #code="{ row }">
+        <a class="link" @click="handleDetail(row)">{{ row.code }}</a>
+      </template>
+      <template #status="{ row }">
+        <Tag v-if="row.status === PLAN_STATUS.DRAFT" color="default">鑽夌</Tag>
+        <Tag v-else-if="row.status === PLAN_STATUS.PUBLISHED" color="success">宸插彂甯�</Tag>
+      </template>
+      <template #actions="{ row }">
+        <TableAction
+          :actions="[
+            {
+              label: $t('common.detail'),
+              type: 'link',
+              icon: ACTION_ICON.VIEW,
+              auth: ['mes:pro-emergency-plan:query'],
+              onClick: handleDetail.bind(null, row),
+            },
+            {
+              label: $t('common.edit'),
+              type: 'link',
+              icon: ACTION_ICON.EDIT,
+              auth: ['mes:pro-emergency-plan:update'],
+              ifShow: row.status === PLAN_STATUS.DRAFT,
+              onClick: handleEdit.bind(null, row),
+            },
+            {
+              label: '鍙戝竷',
+              type: 'link',
+              auth: ['mes:pro-emergency-plan:update'],
+              ifShow: row.status === PLAN_STATUS.DRAFT,
+              popConfirm: {
+                title: '纭鍙戝竷璇ラ妗堬紵',
+                confirm: handlePublish.bind(null, row),
+              },
+            },
+            {
+              label: $t('common.delete'),
+              type: 'link',
+              danger: true,
+              icon: ACTION_ICON.DELETE,
+              auth: ['mes:pro-emergency-plan:delete'],
+              ifShow: row.status === PLAN_STATUS.DRAFT,
+              popConfirm: {
+                title: $t('ui.actionMessage.deleteConfirm', [row.code]),
+                confirm: handleDelete.bind(null, [row.id!]),
+              },
+            },
+          ]"
+        />
+      </template>
+    </Grid>
+  </Page>
+</template>
diff --git a/src/views/mes/pro/emergencyplan/modules/form.vue b/src/views/mes/pro/emergencyplan/modules/form.vue
new file mode 100644
index 0000000..12a80ff
--- /dev/null
+++ b/src/views/mes/pro/emergencyplan/modules/form.vue
@@ -0,0 +1,91 @@
+<script lang="ts" setup>
+import type { MesProEmergencyPlanApi } from '#/api/mes/pro/emergencyplan';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { createPlan, getPlan, updatePlan } from '#/api/mes/pro/emergencyplan';
+import { $t } from '#/locales';
+
+import type { FormType } from '../data';
+import { useFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+const formData = ref<MesProEmergencyPlanApi.EmergencyPlan>();
+const formType = ref<FormType>('create');
+
+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,
+});
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    const { valid } = await formApi.validate();
+    if (!valid) {
+      return;
+    }
+    modalApi.lock();
+    const data =
+      (await formApi.getValues()) as MesProEmergencyPlanApi.EmergencyPlan;
+    try {
+      await (formType.value === 'create'
+        ? createPlan(data)
+        : updatePlan(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 getPlan(data.id);
+      await formApi.setValues(formData.value);
+    } finally {
+      modalApi.unlock();
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal :title="getTitle" class="w-2/3" :show-confirm-button="formType !== 'detail'">
+    <Form class="mx-3" />
+  </Modal>
+</template>
diff --git a/src/views/mes/pro/maintenance-plan/data.ts b/src/views/mes/pro/maintenance-plan/data.ts
new file mode 100644
index 0000000..75dc159
--- /dev/null
+++ b/src/views/mes/pro/maintenance-plan/data.ts
@@ -0,0 +1,244 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'detail' | 'edit';
+
+/** 璁″垝鐘舵�佸父閲� */
+export const PLAN_STATUS = {
+  DRAFT: 0,
+  ISSUED: 10,
+  FINISHED: 20,
+  CANCELLED: 30,
+};
+
+/** 琛ㄥ崟鐨勯厤缃」 */
+export function useFormSchema(formType: FormType): VbenFormSchema[] {
+  const isReadonly = formType === 'detail';
+  return [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: {
+        triggerFields: [''],
+        show: () => false,
+      },
+    },
+    {
+      fieldName: 'code',
+      label: '璁″垝缂栫爜',
+      component: 'Input',
+      componentProps: {
+        placeholder: '绯荤粺鑷姩鐢熸垚',
+        disabled: true,
+      },
+    },
+    {
+      fieldName: 'name',
+      label: '璁″垝鍚嶇О',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ヨ鍒掑悕绉�',
+        disabled: isReadonly,
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'planType',
+      label: '璁″垝绫诲瀷',
+      component: 'Select',
+      componentProps: {
+        options: getDictOptions(DICT_TYPE.MES_PRO_MAINTENANCE_PLAN_TYPE, 'number'),
+        placeholder: '璇烽�夋嫨璁″垝绫诲瀷',
+        disabled: isReadonly,
+      },
+      rules: 'selectRequired',
+    },
+    {
+      fieldName: 'station',
+      label: '绔欑偣',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ョ珯鐐�',
+        disabled: isReadonly,
+      },
+    },
+    {
+      fieldName: 'line',
+      label: '绾胯矾',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ョ嚎璺�',
+        disabled: isReadonly,
+      },
+    },
+    {
+      fieldName: 'planDate',
+      label: '璁″垝鏃ユ湡',
+      component: 'DatePicker',
+      componentProps: {
+        placeholder: '閫夋嫨璁″垝鏃ユ湡',
+        showTime: true,
+        format: 'YYYY-MM-DD HH:mm:ss',
+        valueFormat: 'YYYY-MM-DD HH:mm:ss',
+        disabled: isReadonly,
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'teamId',
+      label: '鐝粍',
+      component: 'Input',
+      componentProps: {
+        disabled: true,
+        placeholder: '鐢辫鍒掓墽琛屽垎閰�',
+      },
+      dependencies: {
+        triggerFields: [''],
+        show: () => false,
+      },
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ娉�',
+        autoSize: { minRows: 1, maxRows: 1 },
+        disabled: isReadonly,
+      },
+      formItemClass: 'col-span-2',
+    },
+    {
+      fieldName: 'items',
+      label: '澶囦欢/鐗╄祫闇�姹�',
+      component: 'Input',
+      formItemClass: 'col-span-3',
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'code',
+      label: '璁″垝缂栫爜',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ヨ鍒掔紪鐮�',
+      },
+    },
+    {
+      fieldName: 'name',
+      label: '璁″垝鍚嶇О',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ヨ鍒掑悕绉�',
+      },
+    },
+    {
+      fieldName: 'planType',
+      label: '璁″垝绫诲瀷',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: getDictOptions(DICT_TYPE.MES_PRO_MAINTENANCE_PLAN_TYPE, 'number'),
+        placeholder: '璇烽�夋嫨璁″垝绫诲瀷',
+      },
+    },
+    {
+      fieldName: 'status',
+      label: '鐘舵��',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: [
+          { label: '鑽夌', value: PLAN_STATUS.DRAFT },
+          { label: '宸蹭笅鍙�', value: PLAN_STATUS.ISSUED },
+          { label: '宸插畬鎴�', value: PLAN_STATUS.FINISHED },
+          { label: '宸插彇娑�', value: PLAN_STATUS.CANCELLED },
+        ],
+        placeholder: '璇烽�夋嫨鐘舵��',
+      },
+    },
+    {
+      fieldName: 'planDate',
+      label: '璁″垝鏃ユ湡',
+      component: 'RangePicker',
+      componentProps: getRangePickerDefaultProps(),
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勮〃鏍煎垪 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+  return [
+    { type: 'checkbox', width: 50 },
+    { type: 'seq', title: '搴忓彿', width: 60 },
+    {
+      field: 'code',
+      title: '璁″垝缂栫爜',
+      minWidth: 150,
+      slots: { default: 'code' },
+    },
+    {
+      field: 'name',
+      title: '璁″垝鍚嶇О',
+      minWidth: 180,
+    },
+    {
+      field: 'planType',
+      title: '璁″垝绫诲瀷',
+      width: 110,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.MES_PRO_MAINTENANCE_PLAN_TYPE },
+      },
+    },
+    {
+      field: 'station',
+      title: '绔欑偣',
+      minWidth: 140,
+    },
+    {
+      field: 'line',
+      title: '绾胯矾',
+      minWidth: 140,
+    },
+    {
+      field: 'teamName',
+      title: '鐝粍',
+      width: 110,
+    },
+    {
+      field: 'planDate',
+      title: '璁″垝鏃ユ湡',
+      width: 170,
+      formatter: 'formatDateTime',
+    },
+    {
+      field: 'status',
+      title: '鐘舵��',
+      width: 100,
+      slots: { default: 'status' },
+    },
+    {
+      field: 'workOrderCode',
+      title: '鐢熸垚鐨勫伐鍗�',
+      minWidth: 150,
+    },
+    {
+      field: 'action',
+      title: '鎿嶄綔',
+      width: 300,
+      fixed: 'right',
+      slots: { default: 'actions' },
+    },
+  ];
+}
diff --git a/src/views/mes/pro/maintenance-plan/index.vue b/src/views/mes/pro/maintenance-plan/index.vue
new file mode 100644
index 0000000..b58d3e6
--- /dev/null
+++ b/src/views/mes/pro/maintenance-plan/index.vue
@@ -0,0 +1,271 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesProMaintenancePlanApi } from '#/api/mes/pro/maintenance-plan';
+
+import { ref } from 'vue';
+
+import { Page, useVbenModal } from '@vben/common-ui';
+import { downloadFileFromBlobPart, isEmpty } from '@vben/utils';
+
+import { message, Tag } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+  deletePlan,
+  exportPlan,
+  finishPlan,
+  generatePurchasePlan,
+  generateWorkOrder,
+  getPlanPage,
+  issuePlan,
+} from '#/api/mes/pro/maintenance-plan';
+import { $t } from '#/locales';
+
+import { PLAN_STATUS, useGridColumns, useGridFormSchema } from './data';
+import Form from './modules/form.vue';
+
+/** MES 鐢靛姏浣滀笟璁″垝鍒楄〃 */
+defineOptions({ name: 'MesProMaintenancePlan' });
+
+const [FormModal, formModalApi] = useVbenModal({
+  connectedComponent: Form,
+  destroyOnClose: true,
+});
+
+function handleRefresh() {
+  gridApi.query();
+}
+
+async function handleExport() {
+  const data = await exportPlan(await gridApi.formApi.getValues());
+  downloadFileFromBlobPart({ fileName: '鐢靛姏浣滀笟璁″垝.xls', source: data });
+}
+
+function handleCreate() {
+  formModalApi.setData({ formType: 'create' }).open();
+}
+
+function handleEdit(row: MesProMaintenancePlanApi.MaintenancePlan) {
+  formModalApi.setData({ formType: 'edit', id: row.id }).open();
+}
+
+function handleDetail(row: MesProMaintenancePlanApi.MaintenancePlan) {
+  formModalApi.setData({ formType: 'detail', id: row.id }).open();
+}
+
+async function handleDelete(ids: number[]) {
+  const hideLoading = message.loading({
+    content: $t('ui.actionMessage.deleting'),
+    duration: 0,
+  });
+  try {
+    await deletePlan(ids);
+    message.success($t('ui.actionMessage.deleteSuccess'));
+    handleRefresh();
+  } finally {
+    hideLoading();
+  }
+}
+
+async function handleIssue(row: MesProMaintenancePlanApi.MaintenancePlan) {
+  await issuePlan(row.id!);
+  message.success('宸蹭笅鍙�');
+  handleRefresh();
+}
+
+async function handleFinish(row: MesProMaintenancePlanApi.MaintenancePlan) {
+  await finishPlan(row.id!);
+  message.success('宸插畬鎴�');
+  handleRefresh();
+}
+
+async function handleGeneratePurchasePlan(
+  row: MesProMaintenancePlanApi.MaintenancePlan,
+) {
+  const hideLoading = message.loading({
+    content: '姝e湪鏍规嵁澶囦欢闇�姹傜敓鎴愰噰璐鍒�...',
+    duration: 0,
+  });
+  try {
+    const planId = await generatePurchasePlan(row.id!);
+    message.success(`宸茬敓鎴愰噰璐鍒掞紙缂栧彿 ${planId}锛夛紝璇峰湪閲囪喘绠$悊涓牳瀵规彁浜ゅ鎵筦);
+  } finally {
+    hideLoading();
+  }
+}
+
+async function handleGenerateWorkOrder(
+  row: MesProMaintenancePlanApi.MaintenancePlan,
+) {
+  const hideLoading = message.loading({
+    content: '姝e湪鐢熸垚宸ュ崟...',
+    duration: 0,
+  });
+  try {
+    const workOrderId = await generateWorkOrder(row.id!);
+    message.success(`宸茬敓鎴愬伐鍗曪紙缂栧彿 ${workOrderId}锛夛紝鍙湪鐢熶骇璁㈠崟涓煡鐪媊);
+    handleRefresh();
+  } finally {
+    hideLoading();
+  }
+}
+
+const checkedIds = ref<number[]>([]);
+function handleRowCheckboxChange({
+  records,
+}: {
+  records: MesProMaintenancePlanApi.MaintenancePlan[];
+}) {
+  checkedIds.value = records.map((item) => item.id!);
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  formOptions: {
+    schema: useGridFormSchema(),
+  },
+  gridOptions: {
+    columns: useGridColumns(),
+    height: 'auto',
+    keepSource: true,
+    proxyConfig: {
+      ajax: {
+        query: async ({ page }, formValues) => {
+          return await getPlanPage({
+            pageNo: page.currentPage,
+            pageSize: page.pageSize,
+            ...formValues,
+          });
+        },
+      },
+    },
+    rowConfig: {
+      keyField: 'id',
+      isHover: true,
+    },
+    toolbarConfig: {
+      refresh: true,
+      search: true,
+    },
+  } as VxeTableGridOptions<MesProMaintenancePlanApi.MaintenancePlan>,
+  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: ['mes:pro-maintenance-plan:create'],
+              onClick: handleCreate,
+            },
+            {
+              label: $t('ui.actionTitle.export'),
+              type: 'primary',
+              icon: ACTION_ICON.DOWNLOAD,
+              auth: ['mes:pro-maintenance-plan:export'],
+              onClick: handleExport,
+            },
+            {
+              label: '鎵归噺鍒犻櫎',
+              type: 'primary',
+              danger: true,
+              disabled: isEmpty(checkedIds),
+              icon: ACTION_ICON.DELETE,
+              auth: ['mes:pro-maintenance-plan:delete'],
+              popConfirm: {
+                title: `鏄惁鍒犻櫎鎵�閫変腑鏁版嵁锛焋,
+                confirm: handleDelete.bind(null, checkedIds),
+              },
+            },
+          ]"
+        />
+      </template>
+      <template #code="{ row }">
+        <a class="link" @click="handleDetail(row)">{{ row.code }}</a>
+      </template>
+      <template #status="{ row }">
+        <Tag v-if="row.status === PLAN_STATUS.DRAFT" color="default">鑽夌</Tag>
+        <Tag v-else-if="row.status === PLAN_STATUS.ISSUED" color="processing">宸蹭笅鍙�</Tag>
+        <Tag v-else-if="row.status === PLAN_STATUS.FINISHED" color="success">宸插畬鎴�</Tag>
+        <Tag v-else-if="row.status === PLAN_STATUS.CANCELLED" color="warning">宸插彇娑�</Tag>
+      </template>
+      <template #actions="{ row }">
+        <TableAction
+          :actions="[
+            {
+              label: $t('common.detail'),
+              type: 'link',
+              icon: ACTION_ICON.VIEW,
+              auth: ['mes:pro-maintenance-plan:query'],
+              onClick: handleDetail.bind(null, row),
+            },
+            {
+              label: $t('common.edit'),
+              type: 'link',
+              icon: ACTION_ICON.EDIT,
+              auth: ['mes:pro-maintenance-plan:update'],
+              ifShow: row.status === PLAN_STATUS.DRAFT,
+              onClick: handleEdit.bind(null, row),
+            },
+            {
+              label: '涓嬪彂',
+              type: 'link',
+              auth: ['mes:pro-maintenance-plan:update'],
+              ifShow: row.status === PLAN_STATUS.DRAFT,
+              popConfirm: {
+                title: '纭涓嬪彂璇ヨ鍒掞紵',
+                confirm: handleIssue.bind(null, row),
+              },
+            },
+            {
+              label: '鐢熸垚閲囪喘璁″垝',
+              type: 'link',
+              auth: ['mes:pro-maintenance-plan:generate-purchase-plan'],
+              ifShow: row.status === PLAN_STATUS.ISSUED,
+              onClick: handleGeneratePurchasePlan.bind(null, row),
+            },
+            {
+              label: '鐢熸垚宸ュ崟',
+              type: 'link',
+              auth: ['mes:pro-maintenance-plan:generate-work-order'],
+              ifShow: row.status === PLAN_STATUS.ISSUED && !row.workOrderId,
+              onClick: handleGenerateWorkOrder.bind(null, row),
+            },
+            {
+              label: '瀹屾垚',
+              type: 'link',
+              auth: ['mes:pro-maintenance-plan:update'],
+              ifShow: row.status === PLAN_STATUS.ISSUED,
+              popConfirm: {
+                title: '纭瀹屾垚璇ヨ鍒掞紵',
+                confirm: handleFinish.bind(null, row),
+              },
+            },
+            {
+              label: $t('common.delete'),
+              type: 'link',
+              danger: true,
+              icon: ACTION_ICON.DELETE,
+              auth: ['mes:pro-maintenance-plan:delete'],
+              ifShow: row.status === PLAN_STATUS.DRAFT,
+              popConfirm: {
+                title: $t('ui.actionMessage.deleteConfirm', [row.code]),
+                confirm: handleDelete.bind(null, [row.id!]),
+              },
+            },
+          ]"
+        />
+      </template>
+    </Grid>
+  </Page>
+</template>
diff --git a/src/views/mes/pro/maintenance-plan/modules/form.vue b/src/views/mes/pro/maintenance-plan/modules/form.vue
new file mode 100644
index 0000000..ee7a49d
--- /dev/null
+++ b/src/views/mes/pro/maintenance-plan/modules/form.vue
@@ -0,0 +1,220 @@
+<script lang="ts" setup>
+import type { MesProMaintenancePlanApi } from '#/api/mes/pro/maintenance-plan';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { Button, DatePicker, Input, InputNumber, message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+  createPlan,
+  getPlan,
+  updatePlan,
+} from '#/api/mes/pro/maintenance-plan';
+import { MdItemSelect } from '#/views/mes/md/item/components';
+import { $t } from '#/locales';
+
+import type { FormType } from '../data';
+import { useFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+const formData = ref<MesProMaintenancePlanApi.MaintenancePlan>();
+const formType = ref<FormType>('create');
+const items = ref<MesProMaintenancePlanApi.PlanItem[]>([]);
+
+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 handleAddItem() {
+  items.value.push({
+    itemId: undefined as unknown as number,
+    itemName: '',
+    itemCode: '',
+    unitMeasureName: '',
+    quantity: 1,
+    remark: '',
+  });
+}
+
+function handleRemoveItem(index: number) {
+  items.value.splice(index, 1);
+}
+
+function handleItemSelect(index: number, item: any) {
+  const row = items.value[index];
+  if (!row) {
+    return;
+  }
+  row.itemId = item?.id;
+  row.itemName = item?.name;
+  row.itemCode = item?.code;
+  row.unitMeasureName = item?.unitMeasureName;
+}
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    const { valid } = await formApi.validate();
+    if (!valid) {
+      return;
+    }
+    if (!items.value.length) {
+      message.error('璇疯嚦灏戞坊鍔犱竴鏉″浠�/鐗╄祫闇�姹�');
+      return;
+    }
+    if (items.value.some((item) => !item.itemId || !item.quantity)) {
+      message.error('璇峰畬鍠勫浠堕渶姹傜殑浜у搧涓庢暟閲�');
+      return;
+    }
+    modalApi.lock();
+    const data =
+      (await formApi.getValues()) as MesProMaintenancePlanApi.MaintenancePlan;
+    data.items = items.value.map((item) => ({
+      itemId: item.itemId,
+      quantity: item.quantity,
+      remark: item.remark,
+    }));
+    try {
+      await (formType.value === 'create'
+        ? createPlan(data)
+        : updatePlan(data));
+      await modalApi.close();
+      emit('success');
+      message.success($t('ui.actionMessage.operationSuccess'));
+    } finally {
+      modalApi.unlock();
+    }
+  },
+  async onOpenChange(isOpen: boolean) {
+    if (!isOpen) {
+      formData.value = undefined;
+      items.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) {
+      return;
+    }
+    modalApi.lock();
+    try {
+      formData.value = await getPlan(data.id);
+      await formApi.setValues(formData.value);
+      items.value = (formData.value.items ?? []).map((item) => ({ ...item }));
+    } finally {
+      modalApi.unlock();
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal
+    :title="getTitle"
+    class="w-3/4"
+    :show-confirm-button="formType !== 'detail'"
+  >
+    <Form class="mx-3">
+      <template #items>
+        <div class="mt-2">
+          <div v-if="formType !== 'detail'" class="mb-2">
+            <Button type="dashed" @click="handleAddItem">+ 娣诲姞澶囦欢闇�姹�</Button>
+          </div>
+          <table class="w-full border-collapse border border-gray-200 text-sm">
+            <thead>
+              <tr class="bg-gray-100">
+                <th class="border border-gray-200 px-2 py-1 w-12">搴忓彿</th>
+                <th class="border border-gray-200 px-2 py-1">鐗╂枡/澶囦欢</th>
+                <th class="border border-gray-200 px-2 py-1 w-20">鍗曚綅</th>
+                <th class="border border-gray-200 px-2 py-1 w-32">闇�姹傛暟閲�</th>
+                <th class="border border-gray-200 px-2 py-1">澶囨敞</th>
+                <th
+                  v-if="formType !== 'detail'"
+                  class="border border-gray-200 px-2 py-1 w-16"
+                >
+                  鎿嶄綔
+                </th>
+              </tr>
+            </thead>
+            <tbody>
+              <tr v-for="(item, index) in items" :key="index">
+                <td class="border border-gray-200 px-2 py-1 text-center">
+                  {{ index + 1 }}
+                </td>
+                <td class="border border-gray-200 px-2 py-1">
+                  <MdItemSelect
+                    v-if="formType !== 'detail'"
+                    :model-value="item.itemId"
+                    placeholder="璇烽�夋嫨鐗╂枡/澶囦欢"
+                    @change="handleItemSelect(index, $event)"
+                  />
+                  <span v-else>{{ item.itemName }}</span>
+                </td>
+                <td class="border border-gray-200 px-2 py-1 text-center">
+                  {{ item.unitMeasureName || '-' }}
+                </td>
+                <td class="border border-gray-200 px-2 py-1">
+                  <InputNumber
+                    v-if="formType !== 'detail'"
+                    v-model:value="item.quantity"
+                    class="!w-full"
+                    :min="0.01"
+                    :precision="2"
+                  />
+                  <span v-else>{{ item.quantity }}</span>
+                </td>
+                <td class="border border-gray-200 px-2 py-1">
+                  <Input
+                    v-if="formType !== 'detail'"
+                    v-model:value="item.remark"
+                    placeholder="澶囨敞"
+                  />
+                  <span v-else>{{ item.remark || '-' }}</span>
+                </td>
+                <td
+                  v-if="formType !== 'detail'"
+                  class="border border-gray-200 px-2 py-1 text-center"
+                >
+                  <Button type="link" danger @click="handleRemoveItem(index)">
+                    鍒犻櫎
+                  </Button>
+                </td>
+              </tr>
+              <tr v-if="!items.length">
+                <td
+                  :colspan="formType === 'detail' ? 5 : 6"
+                  class="border border-gray-200 px-2 py-4 text-center text-gray-400"
+                >
+                  鏆傛棤澶囦欢闇�姹傦紝璇锋坊鍔�
+                </td>
+              </tr>
+            </tbody>
+          </table>
+        </div>
+      </template>
+    </Form>
+  </Modal>
+</template>
diff --git a/src/views/mes/pro/task/data.ts b/src/views/mes/pro/task/data.ts
index aeb9868..167bac7 100644
--- a/src/views/mes/pro/task/data.ts
+++ b/src/views/mes/pro/task/data.ts
@@ -19,6 +19,7 @@
 import { getRangePickerDefaultProps } from '#/utils';
 import { MdItemSelect } from '#/views/mes/md/item/components';
 import { MdWorkstationSelect } from '#/views/mes/md/workstation/components';
+import { CalTeamSelect } from '#/views/mes/cal/team/components';
 import { ProProcessSelect } from '#/views/mes/process-design/process/components';
 import { RouteColorPicker } from '#/views/mes/process-design/route/components';
 import { ProWorkOrderSelect } from '#/views/mes/pro/workorder/components';
@@ -347,6 +348,11 @@
       width: 120,
     },
     {
+      field: 'teamName',
+      title: '鐝粍',
+      width: 120,
+    },
+    {
       field: 'quantity',
       title: '鎺掍骇鏁伴噺',
       width: 100,
@@ -406,6 +412,14 @@
       rules: 'selectRequired',
     },
     {
+      fieldName: 'teamId',
+      label: '鐝粍',
+      component: markRaw(CalTeamSelect),
+      componentProps: {
+        placeholder: '璇烽�夋嫨鐝粍',
+      },
+    },
+    {
       fieldName: 'quantity',
       label: '鎺掍骇鏁伴噺',
       component: 'InputNumber',
diff --git a/src/views/mes/pro/workorder/data.ts b/src/views/mes/pro/workorder/data.ts
index f62e323..e046fdf 100644
--- a/src/views/mes/pro/workorder/data.ts
+++ b/src/views/mes/pro/workorder/data.ts
@@ -3,6 +3,7 @@
 import type { MesProWorkOrderApi } from '#/api/mes/pro/workorder';
 import type { MesProWorkOrderBomApi } from '#/api/mes/pro/workorder/bom';
 import type { MesProWorkOrderProcessApi } from '#/api/mes/pro/workorder/process';
+import type { ErpSaleOrderApi } from '#/api/erp/sale/order';
 
 import { h, markRaw } from 'vue';
 
@@ -23,6 +24,7 @@
   MdItemSelect,
 } from '#/views/mes/md/item/components';
 import { MdVendorSelect } from '#/views/mes/md/vendor/components';
+import { ErpSaleOrderSelect } from '#/views/erp/sale/order/components';
 
 /** 琛ㄥ崟绫诲瀷 */
 export type FormType = 'confirm' | 'create' | 'detail' | 'finish' | 'update';
@@ -192,6 +194,30 @@
       componentProps: {
         disabled: headerReadonly,
         placeholder: '璇烽�夋嫨瀹㈡埛',
+      },
+      dependencies: {
+        triggerFields: ['orderSourceType'],
+        show: (values) =>
+          values.orderSourceType === MesProWorkOrderSourceTypeEnum.ORDER,
+      },
+    },
+    {
+      fieldName: 'saleOrderId',
+      label: '钀ラ攢璁㈠崟',
+      component: markRaw(ErpSaleOrderSelect),
+      componentProps: {
+        disabled: headerReadonly,
+        placeholder: '璇烽�夋嫨钀ラ攢璁㈠崟锛圗RP閿�鍞鍗曪級',
+        // 閫変腑钀ラ攢璁㈠崟鍚庤嚜鍔ㄥ甫鍏ュ鎴�
+        onChange: async (item?: ErpSaleOrderApi.SaleOrder) => {
+          if (!item) {
+            return;
+          }
+          const values = (await formApi?.getValues()) ?? {};
+          if (values.clientId == null && item.customerId) {
+            await formApi?.setFieldValue('clientId', item.customerId);
+          }
+        },
       },
       dependencies: {
         triggerFields: ['orderSourceType'],
@@ -398,6 +424,11 @@
       width: 120,
     },
     {
+      field: 'saleOrderNo',
+      title: '钀ラ攢璁㈠崟鍙�',
+      width: 140,
+    },
+    {
       field: 'requestDate',
       title: '闇�姹傛棩鏈�',
       width: 180,
diff --git a/src/views/mes/pro/workresult/summary.vue b/src/views/mes/pro/workresult/summary.vue
new file mode 100644
index 0000000..1c6eea6
--- /dev/null
+++ b/src/views/mes/pro/workresult/summary.vue
@@ -0,0 +1,168 @@
+<script lang="ts" setup>
+import type { WorkResultSummary } from '#/api/mes/pro/feedback';
+
+import { onMounted, reactive, ref } from 'vue';
+
+import { Page } from '@vben/common-ui';
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import {
+  Button,
+  Card,
+  Col,
+  DatePicker,
+  Row,
+  Select,
+  Space,
+  Table,
+  Tag,
+} from 'ant-design-vue';
+
+import { getFeedbackSummary } from '#/api/mes/pro/feedback';
+
+defineOptions({ name: 'MesWorkResultSummary' });
+
+const loading = ref(false);
+const summary = ref<WorkResultSummary>({});
+
+const queryForm = reactive<{
+  beginDate?: string;
+  endDate?: string;
+  workOrderType?: number;
+}>({});
+
+const workOrderTypeOptions = getDictOptions(
+  DICT_TYPE.MES_PRO_WORK_ORDER_TYPE,
+  'number',
+).map((option) => ({ label: option.label, value: option.value as number }));
+
+async function loadSummary() {
+  loading.value = true;
+  try {
+    summary.value = await getFeedbackSummary({
+      beginDate: queryForm.beginDate,
+      endDate: queryForm.endDate,
+      workOrderType: queryForm.workOrderType,
+    });
+  } finally {
+    loading.value = false;
+  }
+}
+
+function handleRangeChange(_: unknown, dateStrings: string[]) {
+  queryForm.beginDate = dateStrings[0] || undefined;
+  queryForm.endDate = dateStrings[1] || undefined;
+}
+
+function resetQuery() {
+  queryForm.beginDate = undefined;
+  queryForm.endDate = undefined;
+  queryForm.workOrderType = undefined;
+  loadSummary();
+}
+
+onMounted(loadSummary);
+</script>
+
+<template>
+  <Page>
+    <Card class="mb-4">
+      <Space wrap>
+        <DatePicker.RangePicker
+          :placeholder="['寮�濮嬫棩鏈�', '缁撴潫鏃ユ湡']"
+          value-format="YYYY-MM-DD"
+          @change="handleRangeChange"
+        />
+        <Select
+          v-model:value="queryForm.workOrderType"
+          allow-clear
+          :options="workOrderTypeOptions"
+          placeholder="宸ュ崟绫诲瀷"
+          style="width: 160px"
+        />
+        <Button type="primary" :loading="loading" @click="loadSummary">
+          鏌ヨ
+        </Button>
+        <Button @click="resetQuery">閲嶇疆</Button>
+      </Space>
+    </Card>
+
+    <Row :gutter="16" class="mb-4">
+      <Col :span="8">
+        <Card>
+          <div class="text-sm text-gray-500">绱鎶ュ伐鏁伴噺</div>
+          <div class="mt-2 text-2xl font-bold">
+            {{ summary.totalFeedbackQuantity ?? 0 }}
+          </div>
+        </Card>
+      </Col>
+      <Col :span="8">
+        <Card>
+          <div class="text-sm text-gray-500">绱鍚堟牸鏁伴噺</div>
+          <div class="mt-2 text-2xl font-bold text-green-600">
+            {{ summary.totalQualifiedQuantity ?? 0 }}
+          </div>
+        </Card>
+      </Col>
+      <Col :span="8">
+        <Card>
+          <div class="text-sm text-gray-500">绱鎶ュ伐鍗曟暟</div>
+          <div class="mt-2 text-2xl font-bold text-blue-600">
+            {{ summary.totalFeedbackCount ?? 0 }}
+          </div>
+        </Card>
+      </Col>
+    </Row>
+
+    <Card class="mb-4" title="鎸夊伐鍗曠被鍨嬫眹鎬�">
+      <Table
+        row-key="workOrderType"
+        :data-source="summary.byType ?? []"
+        :loading="loading"
+        :pagination="false"
+        size="middle"
+      >
+        <Table.Column title="宸ュ崟绫诲瀷" data-index="workOrderTypeName">
+          <template #default="{ record }">
+            <Tag color="blue">{{ record.workOrderTypeName }}</Tag>
+          </template>
+        </Table.Column>
+        <Table.Column
+          title="鎶ュ伐鏁伴噺"
+          data-index="feedbackQuantity"
+          align="right"
+        />
+        <Table.Column
+          title="鍚堟牸鏁伴噺"
+          data-index="qualifiedQuantity"
+          align="right"
+        />
+        <Table.Column title="鎶ュ伐鍗曟暟" data-index="feedbackCount" align="right" />
+      </Table>
+    </Card>
+
+    <Card title="鎸夋棩鏈熸眹鎬�">
+      <Table
+        row-key="date"
+        :data-source="summary.byDate ?? []"
+        :loading="loading"
+        :pagination="false"
+        size="middle"
+      >
+        <Table.Column title="鏃ユ湡" data-index="date" />
+        <Table.Column
+          title="鎶ュ伐鏁伴噺"
+          data-index="feedbackQuantity"
+          align="right"
+        />
+        <Table.Column
+          title="鍚堟牸鏁伴噺"
+          data-index="qualifiedQuantity"
+          align="right"
+        />
+        <Table.Column title="鎶ュ伐鍗曟暟" data-index="feedbackCount" align="right" />
+      </Table>
+    </Card>
+  </Page>
+</template>
diff --git a/src/views/mes/process-design/process/data.ts b/src/views/mes/process-design/process/data.ts
index 1cf6134..146b7a2 100644
--- a/src/views/mes/process-design/process/data.ts
+++ b/src/views/mes/process-design/process/data.ts
@@ -66,6 +66,16 @@
       rules: z.string().min(1, '宸ュ簭鍚嶇О涓嶈兘涓虹┖').max(100),
     },
     {
+      fieldName: 'type',
+      label: '鐢靛姏宸ヨ壓鍒嗙被',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: getDictOptions(DICT_TYPE.MES_PRO_PROCESS_TYPE, 'number'),
+        placeholder: '璇烽�夋嫨鐢靛姏宸ヨ壓鍒嗙被',
+      },
+    },
+    {
       fieldName: 'status',
       label: '鐘舵��',
       component: 'RadioGroup',
@@ -148,6 +158,15 @@
     },
     { field: 'name', title: '宸ュ簭鍚嶇О', minWidth: 180 },
     {
+      field: 'type',
+      title: '鐢靛姏宸ヨ壓鍒嗙被',
+      width: 120,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.MES_PRO_PROCESS_TYPE },
+      },
+    },
+    {
       field: 'status',
       title: '鐘舵��',
       width: 100,
diff --git a/src/views/mes/qc/outsourcetest/data.ts b/src/views/mes/qc/outsourcetest/data.ts
new file mode 100644
index 0000000..93e0f84
--- /dev/null
+++ b/src/views/mes/qc/outsourcetest/data.ts
@@ -0,0 +1,263 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'detail' | 'edit';
+
+/** 鐘舵�佸父閲� */
+export const TEST_STATUS = {
+  DRAFT: 0,
+  SUBMITTED: 10,
+  FINISHED: 20,
+};
+
+/** 琛ㄥ崟鐨勯厤缃」 */
+export function useFormSchema(formType: FormType): VbenFormSchema[] {
+  const isReadonly = formType === 'detail';
+  return [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: {
+        triggerFields: [''],
+        show: () => false,
+      },
+    },
+    {
+      fieldName: 'code',
+      label: '鍗曟嵁缂栫爜',
+      component: 'Input',
+      componentProps: {
+        placeholder: '绯荤粺鑷姩鐢熸垚',
+        disabled: true,
+      },
+    },
+    {
+      fieldName: 'name',
+      label: '鍗曟嵁鍚嶇О',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ崟鎹悕绉�',
+        disabled: isReadonly,
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'testType',
+      label: '妫�娴嬬被鍨�',
+      component: 'Select',
+      componentProps: {
+        options: getDictOptions(DICT_TYPE.MES_QC_OUTSOURCE_TEST_TYPE, 'number'),
+        placeholder: '璇烽�夋嫨妫�娴嬬被鍨�',
+        disabled: isReadonly,
+      },
+      rules: 'selectRequired',
+    },
+    {
+      fieldName: 'itemId',
+      label: '妫�娴嬪璞�',
+      component: 'Input',
+      componentProps: {
+        placeholder: '妫�娴嬪璞★紙鐗╂枡/璁惧锛�',
+        disabled: isReadonly,
+      },
+    },
+    {
+      fieldName: 'testOrg',
+      label: '妫�娴嬫満鏋�',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ユ娴嬫満鏋�',
+        disabled: isReadonly,
+      },
+    },
+    {
+      fieldName: 'testDate',
+      label: '妫�娴嬫棩鏈�',
+      component: 'DatePicker',
+      componentProps: {
+        placeholder: '閫夋嫨妫�娴嬫棩鏈�',
+        showTime: true,
+        format: 'YYYY-MM-DD HH:mm:ss',
+        valueFormat: 'YYYY-MM-DD HH:mm:ss',
+        disabled: isReadonly,
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'result',
+      label: '妫�娴嬬粨鏋�',
+      component: 'Select',
+      componentProps: {
+        options: getDictOptions(DICT_TYPE.MES_QC_OUTSOURCE_TEST_RESULT, 'number'),
+        placeholder: '璇烽�夋嫨妫�娴嬬粨鏋�',
+        disabled: isReadonly,
+      },
+    },
+    {
+      fieldName: 'reportNo',
+      label: '鎶ュ憡缂栧彿',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ユ娴嬫姤鍛婄紪鍙�',
+        disabled: isReadonly,
+      },
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ娉�',
+        autoSize: { minRows: 1, maxRows: 1 },
+        disabled: isReadonly,
+      },
+      formItemClass: 'col-span-2',
+    },
+    {
+      fieldName: 'blobIds',
+      label: '妫�娴嬫姤鍛�/璧勮川鏂囦欢',
+      component: 'FileUpload',
+      componentProps: {
+        valueKey: 'id',
+        maxNumber: 10,
+        multiple: true,
+        disabled: isReadonly,
+      },
+      formItemClass: 'col-span-3',
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'code',
+      label: '鍗曟嵁缂栫爜',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ崟鎹紪鐮�',
+      },
+    },
+    {
+      fieldName: 'name',
+      label: '鍗曟嵁鍚嶇О',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ崟鎹悕绉�',
+      },
+    },
+    {
+      fieldName: 'testType',
+      label: '妫�娴嬬被鍨�',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: getDictOptions(DICT_TYPE.MES_QC_OUTSOURCE_TEST_TYPE, 'number'),
+        placeholder: '璇烽�夋嫨妫�娴嬬被鍨�',
+      },
+    },
+    {
+      fieldName: 'result',
+      label: '妫�娴嬬粨鏋�',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: getDictOptions(DICT_TYPE.MES_QC_OUTSOURCE_TEST_RESULT, 'number'),
+        placeholder: '璇烽�夋嫨妫�娴嬬粨鏋�',
+      },
+    },
+    {
+      fieldName: 'status',
+      label: '鐘舵��',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: [
+          { label: '鑽夌', value: TEST_STATUS.DRAFT },
+          { label: '宸叉彁浜�', value: TEST_STATUS.SUBMITTED },
+          { label: '宸插畬鎴�', value: TEST_STATUS.FINISHED },
+        ],
+        placeholder: '璇烽�夋嫨鐘舵��',
+      },
+    },
+    {
+      fieldName: 'testDate',
+      label: '妫�娴嬫棩鏈�',
+      component: 'RangePicker',
+      componentProps: getRangePickerDefaultProps(),
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勮〃鏍煎垪 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+  return [
+    { type: 'checkbox', width: 50 },
+    { type: 'seq', title: '搴忓彿', width: 60 },
+    {
+      field: 'code',
+      title: '鍗曟嵁缂栫爜',
+      minWidth: 150,
+      slots: { default: 'code' },
+    },
+    {
+      field: 'name',
+      title: '鍗曟嵁鍚嶇О',
+      minWidth: 180,
+    },
+    {
+      field: 'testType',
+      title: '妫�娴嬬被鍨�',
+      width: 110,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.MES_QC_OUTSOURCE_TEST_TYPE },
+      },
+    },
+    {
+      field: 'itemName',
+      title: '妫�娴嬪璞�',
+      minWidth: 140,
+    },
+    {
+      field: 'testOrg',
+      title: '妫�娴嬫満鏋�',
+      minWidth: 160,
+    },
+    {
+      field: 'testDate',
+      title: '妫�娴嬫棩鏈�',
+      width: 170,
+      formatter: 'formatDateTime',
+    },
+    {
+      field: 'result',
+      title: '妫�娴嬬粨鏋�',
+      width: 100,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.MES_QC_OUTSOURCE_TEST_RESULT },
+      },
+    },
+    {
+      field: 'status',
+      title: '鐘舵��',
+      width: 100,
+      slots: { default: 'status' },
+    },
+    {
+      field: 'action',
+      title: '鎿嶄綔',
+      width: 260,
+      fixed: 'right',
+      slots: { default: 'actions' },
+    },
+  ];
+}
diff --git a/src/views/mes/qc/outsourcetest/index.vue b/src/views/mes/qc/outsourcetest/index.vue
new file mode 100644
index 0000000..cf9e3c7
--- /dev/null
+++ b/src/views/mes/qc/outsourcetest/index.vue
@@ -0,0 +1,210 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesQcOutsourceTestApi } from '#/api/mes/qc/outsourcetest';
+
+import { ref } from 'vue';
+
+import { Page, useVbenModal } from '@vben/common-ui';
+import { isEmpty } from '@vben/utils';
+
+import { message, Tag } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+  deleteTest,
+  finishTest,
+  getTestPage,
+  submitTest,
+} from '#/api/mes/qc/outsourcetest';
+import { $t } from '#/locales';
+
+import { TEST_STATUS, useGridColumns, useGridFormSchema } from './data';
+import Form from './modules/form.vue';
+
+/** MES 濮斿妫�娴�/璇曢獙鍗曞垪琛� */
+defineOptions({ name: 'MesQcOutsourceTest' });
+
+const [FormModal, formModalApi] = useVbenModal({
+  connectedComponent: Form,
+  destroyOnClose: true,
+});
+
+function handleRefresh() {
+  gridApi.query();
+}
+
+function handleCreate() {
+  formModalApi.setData({ formType: 'create' }).open();
+}
+
+function handleEdit(row: MesQcOutsourceTestApi.OutsourceTest) {
+  formModalApi.setData({ formType: 'edit', id: row.id }).open();
+}
+
+function handleDetail(row: MesQcOutsourceTestApi.OutsourceTest) {
+  formModalApi.setData({ formType: 'detail', id: row.id }).open();
+}
+
+async function handleDelete(ids: number[]) {
+  const hideLoading = message.loading({
+    content: $t('ui.actionMessage.deleting'),
+    duration: 0,
+  });
+  try {
+    await deleteTest(ids);
+    message.success($t('ui.actionMessage.deleteSuccess'));
+    handleRefresh();
+  } finally {
+    hideLoading();
+  }
+}
+
+async function handleSubmit(row: MesQcOutsourceTestApi.OutsourceTest) {
+  await submitTest(row.id!);
+  message.success('宸叉彁浜�');
+  handleRefresh();
+}
+
+async function handleFinish(row: MesQcOutsourceTestApi.OutsourceTest) {
+  await finishTest(row.id!);
+  message.success('宸插畬鎴�');
+  handleRefresh();
+}
+
+const checkedIds = ref<number[]>([]);
+function handleRowCheckboxChange({
+  records,
+}: {
+  records: MesQcOutsourceTestApi.OutsourceTest[];
+}) {
+  checkedIds.value = records.map((item) => item.id!);
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  formOptions: {
+    schema: useGridFormSchema(),
+  },
+  gridOptions: {
+    columns: useGridColumns(),
+    height: 'auto',
+    keepSource: true,
+    proxyConfig: {
+      ajax: {
+        query: async ({ page }, formValues) => {
+          return await getTestPage({
+            pageNo: page.currentPage,
+            pageSize: page.pageSize,
+            ...formValues,
+          });
+        },
+      },
+    },
+    rowConfig: {
+      keyField: 'id',
+      isHover: true,
+    },
+    toolbarConfig: {
+      refresh: true,
+      search: true,
+    },
+  } as VxeTableGridOptions<MesQcOutsourceTestApi.OutsourceTest>,
+  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: ['mes:qc-outsource-test:create'],
+              onClick: handleCreate,
+            },
+            {
+              label: '鎵归噺鍒犻櫎',
+              type: 'primary',
+              danger: true,
+              disabled: isEmpty(checkedIds),
+              icon: ACTION_ICON.DELETE,
+              auth: ['mes:qc-outsource-test:delete'],
+              popConfirm: {
+                title: `鏄惁鍒犻櫎鎵�閫変腑鏁版嵁锛焋,
+                confirm: handleDelete.bind(null, checkedIds),
+              },
+            },
+          ]"
+        />
+      </template>
+      <template #code="{ row }">
+        <a class="link" @click="handleDetail(row)">{{ row.code }}</a>
+      </template>
+      <template #status="{ row }">
+        <Tag v-if="row.status === TEST_STATUS.DRAFT" color="default">鑽夌</Tag>
+        <Tag v-else-if="row.status === TEST_STATUS.SUBMITTED" color="processing">宸叉彁浜�</Tag>
+        <Tag v-else-if="row.status === TEST_STATUS.FINISHED" color="success">宸插畬鎴�</Tag>
+      </template>
+      <template #actions="{ row }">
+        <TableAction
+          :actions="[
+            {
+              label: $t('common.detail'),
+              type: 'link',
+              icon: ACTION_ICON.VIEW,
+              auth: ['mes:qc-outsource-test:query'],
+              onClick: handleDetail.bind(null, row),
+            },
+            {
+              label: $t('common.edit'),
+              type: 'link',
+              icon: ACTION_ICON.EDIT,
+              auth: ['mes:qc-outsource-test:update'],
+              ifShow: row.status === TEST_STATUS.DRAFT,
+              onClick: handleEdit.bind(null, row),
+            },
+            {
+              label: '鎻愪氦',
+              type: 'link',
+              auth: ['mes:qc-outsource-test:update'],
+              ifShow: row.status === TEST_STATUS.DRAFT,
+              popConfirm: {
+                title: '纭鎻愪氦璇ュ崟鎹紵',
+                confirm: handleSubmit.bind(null, row),
+              },
+            },
+            {
+              label: '瀹屾垚',
+              type: 'link',
+              auth: ['mes:qc-outsource-test:update'],
+              ifShow: row.status === TEST_STATUS.SUBMITTED,
+              popConfirm: {
+                title: '纭瀹屾垚璇ュ崟鎹紵',
+                confirm: handleFinish.bind(null, row),
+              },
+            },
+            {
+              label: $t('common.delete'),
+              type: 'link',
+              danger: true,
+              icon: ACTION_ICON.DELETE,
+              auth: ['mes:qc-outsource-test:delete'],
+              ifShow: row.status === TEST_STATUS.DRAFT,
+              popConfirm: {
+                title: $t('ui.actionMessage.deleteConfirm', [row.code]),
+                confirm: handleDelete.bind(null, [row.id!]),
+              },
+            },
+          ]"
+        />
+      </template>
+    </Grid>
+  </Page>
+</template>
diff --git a/src/views/mes/qc/outsourcetest/modules/form.vue b/src/views/mes/qc/outsourcetest/modules/form.vue
new file mode 100644
index 0000000..a69a4b4
--- /dev/null
+++ b/src/views/mes/qc/outsourcetest/modules/form.vue
@@ -0,0 +1,101 @@
+<script lang="ts" setup>
+import type { MesQcOutsourceTestApi } from '#/api/mes/qc/outsourcetest';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { createTest, getTest, updateTest } from '#/api/mes/qc/outsourcetest';
+import { $t } from '#/locales';
+
+import type { FormType } from '../data';
+import { useFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+const formData = ref<MesQcOutsourceTestApi.OutsourceTest>();
+const formType = ref<FormType>('create');
+
+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: 140,
+  },
+  wrapperClass: 'grid-cols-3',
+  layout: 'vertical',
+  schema: useFormSchema(formType.value),
+  showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    const { valid } = await formApi.validate();
+    if (!valid) {
+      return;
+    }
+    modalApi.lock();
+    const data =
+      (await formApi.getValues()) as MesQcOutsourceTestApi.OutsourceTest;
+    try {
+      await (formType.value === 'create'
+        ? createTest(data)
+        : updateTest(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 getTest(data.id);
+      await formApi.setValues(formData.value);
+      if (formData.value?.attachmentList?.length) {
+        const blobIds = formData.value.attachmentList.map((item) => ({
+          uid: String(item.id),
+          name: item.name || '',
+          url: item.url || '',
+          status: 'done',
+          id: item.id,
+        }));
+        await formApi.setFieldValue('blobIds', blobIds);
+      }
+    } finally {
+      modalApi.unlock();
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal :title="getTitle" class="w-2/3" :show-confirm-button="formType !== 'detail'">
+    <Form class="mx-3" />
+  </Modal>
+</template>
diff --git a/src/views/wls/productissue/data.ts b/src/views/wls/productissue/data.ts
index 2ee6dcc..d76c1db 100644
--- a/src/views/wls/productissue/data.ts
+++ b/src/views/wls/productissue/data.ts
@@ -93,6 +93,19 @@
       rules: 'required',
     },
     {
+      fieldName: 'issueType',
+      label: '鍗曟嵁绫诲瀷',
+      component: 'Select',
+      componentProps: {
+        options: getDictOptions(DICT_TYPE.MES_WM_PRODUCT_ISSUE_TYPE, 'number'),
+        placeholder: '璇烽�夋嫨鍗曟嵁绫诲瀷',
+      },
+      dependencies: {
+        triggerFields: ['status'],
+        show: (values) => values.status === undefined || values.status === 0,
+      },
+    },
+    {
       fieldName: 'requiredTime',
       label: '闇�姹傛椂闂�',
       component: 'DatePicker',
@@ -196,6 +209,15 @@
       minWidth: 150,
     },
     {
+      field: 'issueType',
+      title: '鍗曟嵁绫诲瀷',
+      width: 100,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.MES_WM_PRODUCT_ISSUE_TYPE },
+      },
+    },
+    {
       field: 'workOrderCode',
       title: '鐢熶骇璁㈠崟',
       minWidth: 140,
diff --git a/src/views/wls/stocktaking/task/index.vue b/src/views/wls/stocktaking/task/index.vue
index 4ffefe8..c38699d 100644
--- a/src/views/wls/stocktaking/task/index.vue
+++ b/src/views/wls/stocktaking/task/index.vue
@@ -2,6 +2,8 @@
 import type { VxeTableGridOptions } from '#/adapter/vxe-table';
 import type { MesWmStockTakingTaskApi } from '#/api/mes/wm/stocktaking/task';
 
+import { ref } from 'vue';
+
 import { Page, useVbenModal } from '@vben/common-ui';
 import { MesWmStockTakingTaskStatusEnum } from '@vben/constants';
 import { downloadFileFromBlobPart } from '@vben/utils';
@@ -15,6 +17,10 @@
   exportStockTaking,
   getStockTakingPage,
 } from '#/api/mes/wm/stocktaking/task';
+import {
+  importStockTakingQuantity,
+  importStockTakingQuantityTemplate,
+} from '#/api/wls/stocktaking/task/line';
 import { $t } from '#/locales';
 
 import { useGridColumns, useGridFormSchema } from './data';
@@ -83,6 +89,41 @@
   downloadFileFromBlobPart({ fileName: '鐩樼偣浠诲姟.xls', source: data });
 }
 
+/** 涓嬭浇鐩樼偣瀵煎叆妯℃澘 */
+async function handleImportTemplate() {
+  const data = await importStockTakingQuantityTemplate();
+  downloadFileFromBlobPart({ fileName: '鐩樼偣瀹炵洏鏁板鍏ユā鏉�.xls', source: data });
+}
+
+const importFileRef = ref<HTMLInputElement>();
+const importTaskId = ref<number>();
+
+/** 瑙﹀彂瀵煎叆鏂囦欢閫夋嫨 */
+function handleImport(row: MesWmStockTakingTaskApi.StockTakingTask) {
+  importTaskId.value = row.id!;
+  importFileRef.value?.click();
+}
+
+/** 瀵煎叆瀹炵洏鏁版枃浠堕�夋嫨鍚庝笂浼� */
+async function handleImportFileChange(event: Event) {
+  const file = (event.target as HTMLInputElement).files?.[0];
+  if (!file || !importTaskId.value) {
+    return;
+  }
+  const hideLoading = message.loading({ content: '姝e湪瀵煎叆瀹炵洏鏁�...', duration: 0 });
+  try {
+    const result = await importStockTakingQuantity(importTaskId.value, file);
+    message.success(`瀵煎叆瀹屾垚锛氭垚鍔� ${result.successCount ?? 0} 鏉★紝澶辫触 ${result.failureCount ?? 0} 鏉);
+    if (result.failureMessages?.length) {
+      message.warning(result.failureMessages.slice(0, 5).join('锛�'));
+    }
+    handleRefresh();
+  } finally {
+    (event.target as HTMLInputElement).value = '';
+    hideLoading();
+  }
+}
+
 const [Grid, gridApi] = useVbenVxeGrid({
   formOptions: {
     schema: useGridFormSchema(),
@@ -137,6 +178,13 @@
               auth: ['mes:wm-stock-taking-task:export'],
               onClick: handleExport,
             },
+            {
+              label: '瀵煎叆妯℃澘',
+              type: 'primary',
+              icon: ACTION_ICON.DOWNLOAD,
+              auth: ['mes:wm-stock-taking-task:query'],
+              onClick: handleImportTemplate,
+            },
           ]"
         />
       </template>
@@ -183,6 +231,14 @@
               onClick: handleExecute.bind(null, row),
             },
             {
+              label: '瀵煎叆瀹炵洏鏁�',
+              type: 'link',
+              icon: ACTION_ICON.UPLOAD,
+              auth: ['mes:wm-stock-taking-task:update'],
+              ifShow: row.status === StatusEnum.APPROVING,
+              onClick: handleImport.bind(null, row),
+            },
+            {
               label: '鍙栨秷',
               type: 'link',
               danger: true,
@@ -197,5 +253,12 @@
         />
       </template>
     </Grid>
+    <input
+      ref="importFileRef"
+      class="hidden"
+      type="file"
+      accept=".xls,.xlsx"
+      @change="handleImportFileChange"
+    />
   </Page>
 </template>
diff --git a/src/views/wls/warehouse/area/data.ts b/src/views/wls/warehouse/area/data.ts
index 0d2d766..2e00eaf 100644
--- a/src/views/wls/warehouse/area/data.ts
+++ b/src/views/wls/warehouse/area/data.ts
@@ -102,6 +102,15 @@
       rules: 'required',
     },
     {
+      fieldName: 'type',
+      label: '搴撳尯绫诲瀷',
+      component: 'Select',
+      componentProps: {
+        options: getDictOptions(DICT_TYPE.MES_WM_WAREHOUSE_TYPE, 'number'),
+        placeholder: '璇烽�夋嫨搴撳尯绫诲瀷',
+      },
+    },
+    {
       fieldName: 'area',
       label: '闈㈢Н锛堛帯锛�',
       component: 'InputNumber',
@@ -275,6 +284,15 @@
       minWidth: 160,
     },
     {
+      field: 'type',
+      title: '搴撳尯绫诲瀷',
+      width: 110,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.MES_WM_WAREHOUSE_TYPE },
+      },
+    },
+    {
       field: 'area',
       title: '闈㈢Н锛堛帯锛�',
       width: 100,
diff --git a/src/views/wls/warehouse/data.ts b/src/views/wls/warehouse/data.ts
index 6eaf201..e99d439 100644
--- a/src/views/wls/warehouse/data.ts
+++ b/src/views/wls/warehouse/data.ts
@@ -6,6 +6,7 @@
 import { h } from 'vue';
 
 import { DICT_TYPE, MesAutoCodeRuleCode } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
 
 import { Button } from 'ant-design-vue';
 
@@ -68,6 +69,15 @@
         placeholder: '璇疯緭鍏ヤ粨搴撳悕绉�',
       },
       rules: 'required',
+    },
+    {
+      fieldName: 'type',
+      label: '浠撳簱绫诲瀷',
+      component: 'Select',
+      componentProps: {
+        options: getDictOptions(DICT_TYPE.MES_WM_WAREHOUSE_TYPE, 'number'),
+        placeholder: '璇烽�夋嫨浠撳簱绫诲瀷',
+      },
     },
     {
       fieldName: 'chargeUserId',
@@ -175,6 +185,15 @@
       minWidth: 160,
     },
     {
+      field: 'type',
+      title: '浠撳簱绫诲瀷',
+      width: 110,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.MES_WM_WAREHOUSE_TYPE },
+      },
+    },
+    {
       field: 'address',
       title: '浠撳簱鍦板潃',
       minWidth: 180,
diff --git a/vite.config.ts b/vite.config.ts
index 8404698..0d6b580 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -76,7 +76,7 @@
         proxy: {
           "/admin-api": {
             changeOrigin: true,
-            target: "http://192.168.0.10:48080",
+            target: "http://127.0.0.1:48080",
             ws: true,
           },
         },

--
Gitblit v1.9.3