From b7eb1da8220f5b72f6891c3c6471ca386604cce4 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期三, 19 八月 2026 10:38:21 +0800
Subject: [PATCH] feat(dashboard): 添加一袋一码首页并重构相关枚举常量

---
 src/views/dashboard/bagCodeHome/index.vue                         |   69 +
 src/views/dashboard/bagCodeHome/modules/trend-chart.vue           |   48 
 src/packages/@core/ui-kit/layout-ui/src/vben-layout.vue           |    2 
 dist.zip                                                          |    0 
 src/views/dashboard/bagCodeHome/modules/basic-cards.vue           |   74 +
 src/router/routes/modules/bagcode-home.ts                         |   16 
 src/views/mes/pro/batch/index.vue                                 |  238 +++
 src/router/routes/modules/dashboard.ts                            |   15 
 src/api/mes/pro/line/index.ts                                     |   61 
 src/views/dashboard/bagCodeHome/data.ts                           |   30 
 src/views/mes/pro/line/components/index.ts                        |    1 
 src/api/mes/pro/pallet/index.ts                                   |  110 +
 src/views/mes/pro/bagCode/modules/bind-pallet.vue                 |  121 +
 src/views/mes/pro/line/modules/form.vue                           |  111 +
 src/packages/@core/preferences/src/config.ts                      |    2 
 src/views/mes/pro/line/components/select.vue                      |  113 +
 src/views/dashboard/bagCodeHome/chart-options.ts                  |   45 
 src/views/dashboard/bagCodeHome/modules/bag-code-status-chart.vue |   43 
 src/views/mes/pro/pallet/index.vue                                |  169 ++
 src/views/basicData/mdm/index.vue                                 |   18 
 src/api/mes/pro/bagCode/index.ts                                  |  129 +
 src/views/mes/pro/pallet/modules/generate-form.vue                |  122 +
 src/views/mes/pro/bagCode/modules/generate-form.vue               |  111 +
 src/views/mes/md/item/type/modules/form.vue                       |    4 
 src/views/mes/pro/components/qr-trace-modal.vue                   |  569 ++++++++
 src/views/mes/pro/pallet/data.ts                                  |  109 +
 src/views/mes/pro/bagCode/index.vue                               |  273 +++
 src/views/mes/pro/line/data.ts                                    |  152 ++
 src/packages/constants/src/biz-mes-enum.ts                        |   30 
 src/views/basicData/mdm/modules/form.vue                          |    2 
 src/views/dashboard/bagCodeHome/modules/batch-status-chart.vue    |   43 
 src/views/mes/pro/batch/modules/form.vue                          |  281 ++++
 src/main.ts                                                       |    4 
 src/views/dashboard/bagCodeHome/modules/shortcuts.vue             |   49 
 src/api/mes/bag-code-home/index.ts                                |   44 
 src/views/mes/md/item/data.ts                                     |   18 
 src/views/mes/md/item/type/data.ts                                |    6 
 src/views/mes/md/item/type/index.vue                              |    4 
 src/views/dashboard/bagCodeHome/modules/bag-code-cards.vue        |   74 +
 src/views/mes/md/item/components/select-dialog.vue                |   24 
 src/api/mes/pro/batch/index.ts                                    |  136 +
 src/views/basicData/mdm/data.ts                                   |   28 
 src/views/mes/pro/line/index.vue                                  |  143 ++
 src/views/mes/pro/bagCode/data.ts                                 |  130 +
 src/packages/constants/src/dict-enum.ts                           |    4 
 src/views/mes/pro/batch/data.ts                                   |  336 ++++
 src/views/mes/md/item/components/select.vue                       |   10 
 47 files changed, 4,049 insertions(+), 72 deletions(-)

diff --git a/dist.zip b/dist.zip
new file mode 100644
index 0000000..d7474cd
--- /dev/null
+++ b/dist.zip
Binary files differ
diff --git a/src/api/mes/bag-code-home/index.ts b/src/api/mes/bag-code-home/index.ts
new file mode 100644
index 0000000..478c99e
--- /dev/null
+++ b/src/api/mes/bag-code-home/index.ts
@@ -0,0 +1,44 @@
+import { requestClient } from '#/api/request';
+
+export namespace MesBagCodeHomeApi {
+  /** 鍩虹鏁版嵁涓庝竴琚嬩竴鐮� 鐘舵�佸垎甯冮」 */
+  export interface StatusItem {
+    status: number; // 鐘舵�佸��
+    count: number; // 鏁伴噺
+  }
+
+  /** 鍩虹鏁版嵁涓庝竴琚嬩竴鐮� 棣栭〉姹囨�荤粺璁� */
+  export interface Summary {
+    mdmItemCount: number; // 鍝佺鎬绘暟锛坢dm_item锛�
+    mesItemCount: number; // MES 鍝佺鏁帮紙mes_md_item锛�
+    itemTypeCount: number; // 鍝佺鍒嗙被鏁�
+    unitMeasureCount: number; // 璁¢噺鍗曚綅鏁�
+    lineCount: number; // 浜х嚎鏁�
+    batchCount: number; // 鎵规鎬绘暟
+    bagCodeCount: number; // 琚嬬爜鎬绘暟
+    palletCount: number; // 鎵樼洏鐮佹�绘暟
+    batchStatusList: StatusItem[]; // 鎵规鐘舵�佸垎甯�
+    bagCodeStatusList: StatusItem[]; // 琚嬬爜鐘舵�佸垎甯�
+  }
+
+  /** 鍩虹鏁版嵁涓庝竴琚嬩竴鐮� 琚嬬爜鐢熸垚瓒嬪娍 */
+  export interface Trend {
+    date: string; // 鏃ユ湡
+    count: number; // 琚嬬爜鐢熸垚鏁伴噺
+  }
+}
+
+/** 鑾峰緱鍩虹鏁版嵁涓庝竴琚嬩竴鐮� 棣栭〉姹囨�荤粺璁� */
+export function getBagCodeHomeSummary() {
+  return requestClient.get<MesBagCodeHomeApi.Summary>(
+    '/mes/bag-code-home-statistics/summary',
+  );
+}
+
+/** 鑾峰緱琚嬬爜鐢熸垚瓒嬪娍 */
+export function getBagCodeHomeTrend(days?: number) {
+  return requestClient.get<MesBagCodeHomeApi.Trend[]>(
+    '/mes/bag-code-home-statistics/trend',
+    { params: { days } },
+  );
+}
diff --git a/src/api/mes/pro/bagCode/index.ts b/src/api/mes/pro/bagCode/index.ts
new file mode 100644
index 0000000..70d332c
--- /dev/null
+++ b/src/api/mes/pro/bagCode/index.ts
@@ -0,0 +1,129 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace MesProBagCodeApi {
+  /** 琚嬬爜锛堜竴琚嬩竴鐮侊級 */
+  export interface BagCode {
+    id?: number; // 缂栧彿
+    uuid?: string; // 鐢熸垚缁� UUID
+    code?: string; // 琚嬬爜锛堜簩缁寸爜鍐呭锛�
+    bagNo?: number; // 鎵规鍐呰搴忓彿
+    batchId?: number; // 鐢熶骇鎵规缂栧彿
+    batchCode?: string; // 鎵规鍙�
+    lineCode?: string; // 浜х嚎缂栫爜
+    lineName?: string; // 浜х嚎鍚嶇О
+    palletId?: number; // 缁戝畾鎵樼洏缂栧彿
+    palletCode?: string; // 鎵樼洏鐮�
+    status?: number; // 鐘舵�侊紙瀛楀吀 mes_pro_bag_code_status锛�
+    printTime?: string; // 瀵煎嚭锛堜氦浠樺嵃鍒凤級鏃堕棿
+    remark?: string; // 澶囨敞
+    createTime?: string; // 鍒涘缓鏃堕棿
+  }
+
+  /** 琚嬬爜鍒嗛〉鏌ヨ鍙傛暟 */
+  export interface PageParams extends PageParam {
+    code?: string;
+    batchId?: number;
+    batchCode?: string;
+    uuid?: string;
+    palletId?: number;
+    status?: number;
+    createTime?: string[];
+  }
+
+  /** 鐢熸垚鏁版嵁鍖呰姹� */
+  export interface GenerateParams {
+    batchId: number; // 鐢熶骇鎵规缂栧彿
+    count: number; // 鐢熸垚琚嬫暟
+    remark?: string; // 澶囨敞
+  }
+
+  /** 缁戝畾/瑙g粦鎵樼洏璇锋眰 */
+  export interface BindParams {
+    palletId: number; // 鎵樼洏缂栧彿
+    ids: number[]; // 琚嬬爜缂栧彿鍒楄〃
+  }
+
+  /** 鎵爜杩芥函淇℃伅 */
+  export interface BagCodeTrace {
+    id?: number; // 琚嬬爜缂栧彿
+    code?: string; // 琚嬬爜锛堜簩缁寸爜鍐呭锛�
+    bagNo?: number; // 琚嬪簭鍙�
+    status?: number; // 琚嬬爜鐘舵��
+    printTime?: string; // 瀵煎嚭鍗板埛鏃堕棿
+    batchId?: number; // 鎵规缂栧彿
+    batchCode?: string; // 鎵规鍙�
+    batchStatus?: number; // 鎵规鐘舵��
+    produceDate?: string; // 鐢熶骇鏃ユ湡
+    lineCode?: string; // 浜х嚎缂栫爜
+    lineName?: string; // 浜х嚎鍚嶇О
+    ownerUserId?: number; // 璐d换浜虹紪鍙�
+    ownerUserName?: string; // 璐d换浜烘樀绉�
+    palletId?: number; // 鎵樼洏缂栧彿
+    palletCode?: string; // 鎵樼洏鐮�
+    itemId?: number; // 鍝佺锛堢墿鏂欙級缂栧彿
+    itemCode?: string; // 鍝佺缂栫爜
+    itemName?: string; // 鍝佺鍚嶇О
+    itemSpecification?: string; // 鍝佺瑙勬牸
+  }
+}
+
+/** 鐢熸垚琚嬬爜鏁版嵁鍖咃紙杩斿洖鐢熸垚缁� UUID锛� */
+export function generateBagCodes(data: MesProBagCodeApi.GenerateParams) {
+  return requestClient.post<string>('/mes/pro/bag-code/generate', data);
+}
+
+/** 鍒犻櫎琚嬬爜鐢熸垚缁� */
+export function deleteBagCodesByUuid(uuid: string) {
+  return requestClient.delete(`/mes/pro/bag-code/delete-by-uuid?uuid=${uuid}`);
+}
+
+/** 琚嬬爜缁戝畾鎵樼洏 */
+export function bindBagCodePallet(data: MesProBagCodeApi.BindParams) {
+  return requestClient.put('/mes/pro/bag-code/bind-pallet', data);
+}
+
+/** 琚嬬爜瑙g粦鎵樼洏 */
+export function unbindBagCodePallet(data: MesProBagCodeApi.BindParams) {
+  return requestClient.put('/mes/pro/bag-code/unbind-pallet', data);
+}
+
+/** 鏌ヨ琚嬬爜璇︽儏 */
+export function getBagCode(id: number) {
+  return requestClient.get<MesProBagCodeApi.BagCode>(
+    `/mes/pro/bag-code/get?id=${id}`,
+  );
+}
+
+/** 鎵爜杩芥函锛氭牴鎹鐮佸弽鏌ユ壒娆°�佷骇绾裤�佽矗浠讳汉绛変俊鎭� */
+export function getBagCodeByCode(code: string) {
+  return requestClient.get<MesProBagCodeApi.BagCodeTrace>(
+    `/mes/pro/bag-code/get-by-code?code=${encodeURIComponent(code)}`,
+  );
+}
+
+/** 鏌ヨ琚嬬爜鍒嗛〉 */
+export function getBagCodePage(params: MesProBagCodeApi.PageParams) {
+  return requestClient.get<PageResult<MesProBagCodeApi.BagCode>>(
+    '/mes/pro/bag-code/page',
+    { params },
+  );
+}
+
+/** 鏌ヨ鐢熸垚缁勭殑琚嬬爜鍒楄〃 */
+export function getBagCodeListByUuid(uuid: string) {
+  return requestClient.get<MesProBagCodeApi.BagCode[]>(
+    `/mes/pro/bag-code/list-by-uuid?uuid=${uuid}`,
+  );
+}
+
+/** 鎵归噺瀵煎嚭琚嬬爜 Excel锛堜氦浠樺嵃鍒凤級 */
+export function exportBagCodeExcel(params: MesProBagCodeApi.PageParams) {
+  return requestClient.download('/mes/pro/bag-code/export-excel', { params });
+}
+
+/** 鎵归噺瀵煎嚭琚嬬爜浜岀淮鐮佸浘鐗� ZIP锛坲uid 浼樺厛锛屽惁鍒欐寜 batchId锛� */
+export function exportBagQr(params: { uuid?: string; batchId?: number }) {
+  return requestClient.download('/mes/pro/bag-code/export-qr', { params });
+}
diff --git a/src/api/mes/pro/batch/index.ts b/src/api/mes/pro/batch/index.ts
new file mode 100644
index 0000000..01e4424
--- /dev/null
+++ b/src/api/mes/pro/batch/index.ts
@@ -0,0 +1,136 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+import type { MesProBagCodeApi } from '#/api/mes/pro/bagCode';
+import type { MesProPalletApi } from '#/api/mes/pro/pallet';
+
+export namespace MesProBatchApi {
+  /** 鐢熶骇鎵规浜哄憳閰嶇疆 */
+  export interface BatchWorker {
+    id?: number; // 缂栧彿
+    batchId?: number; // 鎵规缂栧彿
+    userId?: number; // 鐢熶骇浜哄憳鐢ㄦ埛缂栧彿
+    userName?: string; // 鐢熶骇浜哄憳鏄电О
+    workerType?: number; // 宀椾綅绫诲瀷锛堝瓧鍏� mes_pro_batch_worker_type锛�
+    remark?: string; // 澶囨敞
+  }
+
+  /** 鐢熶骇鎵规 */
+  export interface Batch {
+    id?: number; // 缂栧彿
+    code?: string; // 鎵规鍙凤紙鑷姩鐢熸垚锛�
+    itemId?: number; // 鍝佺锛堜骇鍝佺墿鏂欙級缂栧彿
+    itemCode?: string; // 鍝佺缂栫爜锛堝悗绔嫾鎺ワ級
+    itemName?: string; // 鍝佺鍚嶇О锛堝悗绔嫾鎺ワ級
+    itemSpecification?: string; // 鍝佺瑙勬牸锛堝悗绔嫾鎺ワ級
+    lineId?: number; // 浜х嚎缂栧彿
+    lineCode?: string; // 浜х嚎缂栫爜锛堝悗绔洖濉級
+    lineName?: string; // 浜х嚎鍚嶇О锛堝悗绔洖濉級
+    produceDate?: string; // 鐢熶骇鏃ユ湡
+    planQuantity?: number; // 璁″垝鏁伴噺
+    ownerUserId?: number; // 璐d换浜虹敤鎴风紪鍙�
+    ownerUserName?: string; // 璐d换浜烘樀绉�
+    bagQuantity?: number; // 宸茬敓鎴愯鐮佹暟閲�
+    palletQuantity?: number; // 宸茬敓鎴愭墭鐩樼爜鏁伴噺
+    status?: number; // 鎵规鐘舵�侊紙瀛楀吀 mes_pro_batch_status锛�
+    remark?: string; // 澶囨敞
+    createTime?: string; // 鍒涘缓鏃堕棿
+    workers?: BatchWorker[]; // 鐢熶骇浜哄憳閰嶇疆鍒楄〃
+  }
+
+  /** 鐢熶骇鎵规鎵爜杩芥函 */
+  export interface BatchTrace {
+    id?: number; // 鐢熶骇鎵规缂栧彿
+    code?: string; // 鎵规鍙凤紙浜岀淮鐮佸唴瀹癸級
+    status?: number; // 鎵规鐘舵�侊紙瀛楀吀 mes_pro_batch_status锛�
+    produceDate?: string; // 鐢熶骇鏃ユ湡
+    lineCode?: string; // 浜х嚎缂栫爜
+    lineName?: string; // 浜х嚎鍚嶇О
+    ownerUserId?: number; // 璐d换浜虹敤鎴风紪鍙�
+    ownerUserName?: string; // 璐d换浜烘樀绉�
+    planQuantity?: number; // 璁″垝鏁伴噺
+    bagQuantity?: number; // 宸茬敓鎴愯鐮佹暟閲�
+    palletQuantity?: number; // 宸茬敓鎴愭墭鐩樼爜鏁伴噺
+    remark?: string; // 澶囨敞
+    itemId?: number; // 鍝佺锛堢墿鏂欙級缂栧彿
+    itemCode?: string; // 鍝佺缂栫爜
+    itemName?: string; // 鍝佺鍚嶇О
+    itemSpecification?: string; // 鍝佺瑙勬牸
+    workers?: BatchWorker[]; // 鐢熶骇浜哄憳閰嶇疆鍒楄〃
+    pallets?: MesProPalletApi.Pallet[]; // 鎵樼洏鐮佸垪琛�
+    bags?: MesProBagCodeApi.BagCode[]; // 琚嬬爜鍒楄〃锛堟渶澶氬墠 2000 鏉★紝鎸夎搴忓彿鍗囧簭锛�
+    bagTotal?: number; // 琚嬬爜鎬绘暟
+  }
+
+  /** 鐢熶骇鎵规鍒嗛〉鏌ヨ鍙傛暟 */
+  export interface PageParams extends PageParam {
+    code?: string;
+    itemId?: number;
+    lineId?: number;
+    lineCode?: string;
+    lineName?: string;
+    ownerUserId?: number;
+    status?: number;
+    produceDate?: string[];
+    createTime?: string[];
+  }
+}
+
+/** 鏌ヨ鐢熶骇鎵规鍒嗛〉 */
+export function getBatchPage(params: MesProBatchApi.PageParams) {
+  return requestClient.get<PageResult<MesProBatchApi.Batch>>(
+    '/mes/pro/batch/page',
+    { params },
+  );
+}
+
+/** 鏌ヨ鐢熶骇鎵规璇︽儏锛堝惈浜哄憳閰嶇疆锛� */
+export function getBatch(id: number) {
+  return requestClient.get<MesProBatchApi.Batch>(
+    `/mes/pro/batch/get?id=${id}`,
+  );
+}
+
+/** 鏂板鐢熶骇鎵规锛堣嚜鍔ㄧ敓鎴愭壒娆″彿锛� */
+export function createBatch(data: MesProBatchApi.Batch) {
+  return requestClient.post<number>('/mes/pro/batch/create', data);
+}
+
+/** 淇敼鐢熶骇鎵规 */
+export function updateBatch(data: MesProBatchApi.Batch) {
+  return requestClient.put('/mes/pro/batch/update', data);
+}
+
+/** 鍒犻櫎鐢熶骇鎵规 */
+export function deleteBatch(id: number) {
+  return requestClient.delete(`/mes/pro/batch/delete?id=${id}`);
+}
+
+/** 鏇存柊鐢熶骇鎵规鐘舵�� */
+export function updateBatchStatus(id: number, status: number) {
+  return requestClient.put(
+    `/mes/pro/batch/update-status?id=${id}&status=${status}`,
+  );
+}
+
+/** 鏌ヨ鐢熶骇鎵规绮剧畝鍒楄〃锛堜緵涓嬫媺閫夋嫨锛� */
+export function getBatchSimpleList() {
+  return requestClient.get<MesProBatchApi.Batch[]>('/mes/pro/batch/simple-list');
+}
+
+/** 瀵煎嚭鐢熶骇鎵规 Excel */
+export function exportBatchExcel(params: MesProBatchApi.PageParams) {
+  return requestClient.download('/mes/pro/batch/export-excel', { params });
+}
+
+/** 鎵爜杩芥函锛氭牴鎹壒娆″彿锛堜簩缁寸爜鍐呭锛夊弽鏌ユ壒娆°�佷汉鍛樸�佹墭鐩樸�佽鐮佷俊鎭� */
+export function getBatchByCode(code: string) {
+  return requestClient.get<MesProBatchApi.BatchTrace>(
+    `/mes/pro/batch/get-by-code?code=${encodeURIComponent(code)}`,
+  );
+}
+
+/** 鎵归噺瀵煎嚭鎵规浜岀淮鐮佸浘鐗� ZIP锛堟壒娆″浘 + 璇ユ壒娆″叏閮ㄦ墭鐩樺浘锛� */
+export function exportBatchQr(batchId: number) {
+  return requestClient.download(`/mes/pro/batch/export-qr?batchId=${batchId}`);
+}
diff --git a/src/api/mes/pro/line/index.ts b/src/api/mes/pro/line/index.ts
new file mode 100644
index 0000000..e13bcb5
--- /dev/null
+++ b/src/api/mes/pro/line/index.ts
@@ -0,0 +1,61 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace MesProLineApi {
+  /** MES 浜х嚎 */
+  export interface Line {
+    id?: number; // 缂栧彿
+    code?: string; // 浜х嚎缂栫爜
+    name?: string; // 浜х嚎鍚嶇О
+    status?: number; // 鐘舵�侊紙瀛楀吀 common_status锛�
+    remark?: string; // 澶囨敞
+    createTime?: string; // 鍒涘缓鏃堕棿
+  }
+
+  /** 浜х嚎鍒嗛〉鏌ヨ鍙傛暟 */
+  export interface PageParams extends PageParam {
+    code?: string;
+    name?: string;
+    status?: number;
+    createTime?: string[];
+  }
+}
+
+/** 鏌ヨ浜х嚎鍒嗛〉 */
+export function getLinePage(params: MesProLineApi.PageParams) {
+  return requestClient.get<PageResult<MesProLineApi.Line>>(
+    '/mes/pro/line/page',
+    { params },
+  );
+}
+
+/** 鏌ヨ浜х嚎绮剧畝鍒楄〃锛堜緵涓嬫媺閫夋嫨锛屼粎鍚敤鐨勪骇绾匡級 */
+export function getLineSimpleList() {
+  return requestClient.get<MesProLineApi.Line[]>('/mes/pro/line/simple-list');
+}
+
+/** 鏌ヨ浜х嚎璇︽儏 */
+export function getLine(id: number) {
+  return requestClient.get<MesProLineApi.Line>(`/mes/pro/line/get?id=${id}`);
+}
+
+/** 鏂板浜х嚎 */
+export function createLine(data: MesProLineApi.Line) {
+  return requestClient.post('/mes/pro/line/create', data);
+}
+
+/** 淇敼浜х嚎 */
+export function updateLine(data: MesProLineApi.Line) {
+  return requestClient.put('/mes/pro/line/update', data);
+}
+
+/** 鍒犻櫎浜х嚎 */
+export function deleteLine(id: number) {
+  return requestClient.delete(`/mes/pro/line/delete?id=${id}`);
+}
+
+/** 瀵煎嚭浜х嚎 Excel */
+export function exportLineExcel(params: MesProLineApi.PageParams) {
+  return requestClient.download('/mes/pro/line/export-excel', { params });
+}
diff --git a/src/api/mes/pro/pallet/index.ts b/src/api/mes/pro/pallet/index.ts
new file mode 100644
index 0000000..2fef83b
--- /dev/null
+++ b/src/api/mes/pro/pallet/index.ts
@@ -0,0 +1,110 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+import type { MesProBagCodeApi } from '#/api/mes/pro/bagCode';
+
+export namespace MesProPalletApi {
+  /** 鎵樼洏鐮� */
+  export interface Pallet {
+    id?: number; // 缂栧彿
+    code?: string; // 鎵樼洏鐮�
+    batchId?: number; // 鐢熶骇鎵规缂栧彿
+    batchCode?: string; // 鎵规鍙�
+    palletNo?: number; // 鎵规鍐呮墭鐩樺簭鍙�
+    capacity?: number; // 鍙琚嬫暟锛堣/鎵樼洏锛�
+    bagCount?: number; // 宸茬粦瀹氳鐮佹暟閲�
+    status?: number; // 鐘舵�侊紙瀛楀吀 mes_pro_pallet_status锛�
+    remark?: string; // 澶囨敞
+    createTime?: string; // 鍒涘缓鏃堕棿
+  }
+
+  /** 鎵樼洏鐮佸垎椤垫煡璇㈠弬鏁� */
+  export interface PageParams extends PageParam {
+    code?: string;
+    batchId?: number;
+    batchCode?: string;
+    status?: number;
+    createTime?: string[];
+  }
+
+  /** 鎵樼洏鐮佹壂鐮佽拷婧� */
+  export interface PalletTrace {
+    id?: number; // 缂栧彿
+    code?: string; // 鎵樼洏鐮侊紙浜岀淮鐮佸唴瀹癸級
+    palletNo?: number; // 鎵规鍐呮墭鐩樺簭鍙�
+    capacity?: number; // 鍙琚嬫暟
+    bagCount?: number; // 宸茬粦瀹氳鐮佹暟閲�
+    status?: number; // 鐘舵�侊紙瀛楀吀 mes_pro_pallet_status锛�
+    batchId?: number; // 鐢熶骇鎵规缂栧彿
+    batchCode?: string; // 鎵规鍙�
+    remark?: string; // 澶囨敞
+    batchStatus?: number; // 鎵规鐘舵��
+    produceDate?: string; // 鐢熶骇鏃ユ湡
+    lineCode?: string; // 浜х嚎缂栫爜
+    lineName?: string; // 浜х嚎鍚嶇О
+    ownerUserId?: number; // 璐d换浜虹紪鍙�
+    ownerUserName?: string; // 璐d换浜烘樀绉�
+    itemId?: number; // 鍝佺锛堢墿鏂欙級缂栧彿
+    itemCode?: string; // 鍝佺缂栫爜
+    itemName?: string; // 鍝佺鍚嶇О
+    itemSpecification?: string; // 鍝佺瑙勬牸
+    bags?: MesProBagCodeApi.BagCode[]; // 璇ユ墭鐩樼粦瀹氱殑琚嬬爜鍒楄〃
+  }
+
+  /** 鎵归噺鐢熸垚璇锋眰 */
+  export interface BatchCreateParams {
+    batchId: number; // 鐢熶骇鎵规缂栧彿
+    count: number; // 鐢熸垚鏁伴噺
+    capacity?: number; // 鍙琚嬫暟
+    remark?: string; // 澶囨敞
+  }
+}
+
+/** 鎵归噺鐢熸垚鎵樼洏鐮侊紙杩斿洖鐢熸垚鐨勬墭鐩樼紪鍙峰垪琛級 */
+export function batchCreatePallet(data: MesProPalletApi.BatchCreateParams) {
+  return requestClient.post<number[]>('/mes/pro/pallet/batch-create', data);
+}
+
+/** 鍒犻櫎鎵樼洏鐮� */
+export function deletePallet(id: number) {
+  return requestClient.delete(`/mes/pro/pallet/delete?id=${id}`);
+}
+
+/** 鏌ヨ鎵樼洏鐮佽鎯� */
+export function getPallet(id: number) {
+  return requestClient.get<MesProPalletApi.Pallet>(
+    `/mes/pro/pallet/get?id=${id}`,
+  );
+}
+
+/** 鏌ヨ鎵樼洏鐮佸垎椤� */
+export function getPalletPage(params: MesProPalletApi.PageParams) {
+  return requestClient.get<PageResult<MesProPalletApi.Pallet>>(
+    '/mes/pro/pallet/page',
+    { params },
+  );
+}
+
+/** 鏌ヨ鎵规鐨勬墭鐩樼爜鍒楄〃锛堜緵涓嬫媺閫夋嫨锛� */
+export function getPalletSimpleList(batchId: number) {
+  return requestClient.get<MesProPalletApi.Pallet[]>(
+    `/mes/pro/pallet/simple-list?batchId=${batchId}`,
+  );
+}
+
+/** 瀵煎嚭鎵樼洏鐮� Excel */
+export function exportPalletExcel(params: MesProPalletApi.PageParams) {
+  return requestClient.download('/mes/pro/pallet/export-excel', { params });
+}
+
+/** 鎵爜杩芥函锛氭牴鎹墭鐩樼爜锛堜簩缁寸爜鍐呭锛夊弽鏌ユ壒娆°�佸搧绉嶃�佽矗浠讳汉鍙婅鐮佸垪琛� */
+export function getPalletByCode(code: string) {
+  return requestClient.get<MesProPalletApi.PalletTrace>(
+    `/mes/pro/pallet/get-by-code?code=${encodeURIComponent(code)}`,
+  );
+}
+
+/** 鎵归噺瀵煎嚭鏌愭壒娆$殑鎵樼洏鐮佷簩缁寸爜鍥剧墖 ZIP */
+export function exportPalletQr(batchId: number) {
+  return requestClient.download(`/mes/pro/pallet/export-qr?batchId=${batchId}`);
+}
diff --git a/src/main.ts b/src/main.ts
index b692f75..6a42557 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -35,6 +35,10 @@
       copyright: { companyName: __APP_NAME__ },
     });
   }
+  // 寮哄埗瑕嗙洊榛樿棣栭〉涓�"鍩虹鏁版嵁涓庝竴琚嬩竴鐮�"锛岀粫杩� localStorage 缂撳瓨
+  preferencesManager.updatePreferences({
+    app: { defaultHomePath: '/bagcode-home' },
+  });
   if (__APP_FAVICON__) {
     const link = document.querySelector('link[rel="icon"]');
     if (link) {
diff --git a/src/packages/@core/preferences/src/config.ts b/src/packages/@core/preferences/src/config.ts
index b891f7c..d1a1460 100644
--- a/src/packages/@core/preferences/src/config.ts
+++ b/src/packages/@core/preferences/src/config.ts
@@ -17,7 +17,7 @@
     contentPaddingTop: 0,
     defaultAvatar:
       'https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp',
-    defaultHomePath: '/analytics',
+    defaultHomePath: '/bagcode-home',
     dynamicTitle: true,
     enableCheckUpdates: true,
     enableCopyPreferences: true,
diff --git a/src/packages/@core/ui-kit/layout-ui/src/vben-layout.vue b/src/packages/@core/ui-kit/layout-ui/src/vben-layout.vue
index a9e5afe..1f079f2 100644
--- a/src/packages/@core/ui-kit/layout-ui/src/vben-layout.vue
+++ b/src/packages/@core/ui-kit/layout-ui/src/vben-layout.vue
@@ -495,7 +495,7 @@
 </script>
 
 <template>
-  <div class="relative flex min-h-full w-full">
+  <div class="relative flex h-full w-full">
     <LayoutSidebar
       v-if="sidebarEnableState"
       v-model:draggable="sidebarDraggable"
diff --git a/src/packages/constants/src/biz-mes-enum.ts b/src/packages/constants/src/biz-mes-enum.ts
index 85c58db..4c1981e 100644
--- a/src/packages/constants/src/biz-mes-enum.ts
+++ b/src/packages/constants/src/biz-mes-enum.ts
@@ -267,6 +267,36 @@
   CANCELLED: MesOrderStatusConstants.CANCELLED,
 } as const;
 
+/** MES 鐢熶骇鎵规鐘舵�佹灇涓撅紙瀵归綈鍚庣 MesProBatchStatusEnum锛� */
+export const MesProBatchStatusEnum = {
+  DRAFT: 10, // 鑽夌
+  PRODUCING: 20, // 鐢熶骇涓�
+  FINISHED: 30, // 宸插畬鎴�
+  CANCELED: 40, // 宸蹭綔搴�
+} as const;
+
+/** MES 鐢熶骇鎵规浜哄憳宀椾綅绫诲瀷鏋氫妇锛堝榻愬悗绔� MesProBatchWorkerTypeEnum锛� */
+export const MesProBatchWorkerTypeEnum = {
+  LEADER: 10, // 缁勯暱
+  OPERATOR: 20, // 鎿嶄綔宸�
+  PACKER: 30, // 鍖呰宸�
+  INSPECTOR: 40, // 璐ㄦ鍛�
+  OTHER: 50, // 鍏朵粬
+} as const;
+
+/** MES 琚嬬爜鐘舵�佹灇涓撅紙瀵归綈鍚庣 MesProBagCodeStatusEnum锛� */
+export const MesProBagCodeStatusEnum = {
+  GENERATED: 10, // 宸茬敓鎴�
+  PRINTED: 20, // 宸插鍑哄嵃鍒�
+  CANCELED: 30, // 宸蹭綔搴�
+} as const;
+
+/** MES 鎵樼洏鐮佺姸鎬佹灇涓撅紙瀵归綈鍚庣 MesProPalletStatusEnum锛� */
+export const MesProPalletStatusEnum = {
+  IN_USE: 10, // 鍚敤
+  CANCELED: 20, // 浣滃簾
+} as const;
+
 /** MES 鐢熶骇鎶ュ伐鐘舵�佹灇涓� */
 export const MesProFeedbackStatusEnum = {
   PREPARE: MesOrderStatusConstants.DRAFT,
diff --git a/src/packages/constants/src/dict-enum.ts b/src/packages/constants/src/dict-enum.ts
index 2a62e5a..43c3df4 100644
--- a/src/packages/constants/src/dict-enum.ts
+++ b/src/packages/constants/src/dict-enum.ts
@@ -243,6 +243,10 @@
   MES_PRO_WORK_ORDER_TYPE: 'mes_pro_work_order_type', // MES 宸ュ崟绫诲瀷
   MES_PRO_WORK_ORDER_SOURCE_TYPE: 'mes_pro_work_order_source_type', // MES 宸ュ崟鏉ユ簮绫诲瀷
   MES_PRO_TASK_STATUS: 'mes_pro_task_status', // MES 鐢熶骇浠诲姟鐘舵��
+  MES_PRO_BATCH_STATUS: 'mes_pro_batch_status', // MES 鐢熶骇鎵规鐘舵��
+  MES_PRO_BATCH_WORKER_TYPE: 'mes_pro_batch_worker_type', // MES 鐢熶骇鎵规浜哄憳宀椾綅绫诲瀷
+  MES_PRO_BAG_CODE_STATUS: 'mes_pro_bag_code_status', // MES 琚嬬爜鐘舵��
+  MES_PRO_PALLET_STATUS: 'mes_pro_pallet_status', // MES 鎵樼洏鐮佺姸鎬�
   MES_PRO_FEEDBACK_STATUS: 'mes_pro_feedback_status', // MES 鐢熶骇鎶ュ伐鐘舵��
   MES_PRO_FEEDBACK_TYPE: 'mes_pro_feedback_type', // MES 鐢熶骇鎶ュ伐绫诲瀷
   MES_PD_PROJECT_STATUS: 'mes_pd_project_status', // MES 浜у搧璁捐椤圭洰鐘舵��
diff --git a/src/router/routes/modules/bagcode-home.ts b/src/router/routes/modules/bagcode-home.ts
new file mode 100644
index 0000000..0b7b2f0
--- /dev/null
+++ b/src/router/routes/modules/bagcode-home.ts
@@ -0,0 +1,16 @@
+import type { RouteRecordRaw } from 'vue-router';
+
+const routes: RouteRecordRaw[] = [
+  {
+    name: 'BagCodeHome',
+    path: '/bagcode-home',
+    component: () => import('#/views/dashboard/bagCodeHome/index.vue'),
+    meta: {
+      icon: 'lucide:scan-barcode',
+      order: 1,
+      title: '鍩虹鏁版嵁涓庝竴琚嬩竴鐮�',
+    },
+  },
+];
+
+export default routes;
diff --git a/src/router/routes/modules/dashboard.ts b/src/router/routes/modules/dashboard.ts
index 1df827e..6f67ae2 100644
--- a/src/router/routes/modules/dashboard.ts
+++ b/src/router/routes/modules/dashboard.ts
@@ -1,17 +1,6 @@
 import type { RouteRecordRaw } from 'vue-router';
 
-const routes: RouteRecordRaw[] = [
-  {
-    name: 'Analytics',
-    path: '/analytics',
-    component: () => import('#/views/dashboard/analytics/index.vue'),
-    meta: {
-      affixTab: true,
-      icon: 'lucide:area-chart',
-      order: -1,
-      title: '棣栭〉',
-    },
-  },
-];
+// 鑰侀椤� /analytics 宸查殣钘忥紝鐧诲綍鍚庨粯璁よ烦杞� /bagcode-home
+const routes: RouteRecordRaw[] = [];
 
 export default routes;
diff --git a/src/views/basicData/mdm/data.ts b/src/views/basicData/mdm/data.ts
index a2fb42c..0ec3d1d 100644
--- a/src/views/basicData/mdm/data.ts
+++ b/src/views/basicData/mdm/data.ts
@@ -95,12 +95,12 @@
     {
       component: "Input",
       fieldName: "code",
-      label: "鐗╂枡缂栫爜",
+      label: "鍝佺缂栫爜",
       componentProps: {
         maxLength: 64,
-        placeholder: "璇疯緭鍏ョ墿鏂欑紪鐮�",
+        placeholder: "璇疯緭鍏ュ搧绉嶇紪鐮�",
       },
-      rules: z.string().min(1, '鐗╂枡缂栫爜涓嶈兘涓虹┖').max(64),
+      rules: z.string().min(1, '鍝佺缂栫爜涓嶈兘涓虹┖').max(64),
       suffix: () =>
         h(
           Button,
@@ -119,19 +119,19 @@
     {
       component: "Input",
       fieldName: "name",
-      label: "鐗╂枡鍚嶇О",
+      label: "鍝佺鍚嶇О",
       componentProps: {
-        placeholder: "璇疯緭鍏ョ墿鏂欏悕绉�",
+        placeholder: "璇疯緭鍏ュ搧绉嶅悕绉�",
       },
       rules: "required",
     },
     {
       component: "TreeSelect",
       fieldName: "categoryId",
-      label: "鐗╂枡鍒嗙被",
+      label: "鍝佺鍒嗙被",
       rules: "selectRequired",
       componentProps: {
-        placeholder: "璇烽�夋嫨鐗╂枡鍒嗙被",
+        placeholder: "璇烽�夋嫨鍝佺鍒嗙被",
         allowClear: true,
         treeDefaultExpandAll: true,
         treeData: categoryTreeData,
@@ -492,19 +492,19 @@
   return [
     {
       fieldName: "code",
-      label: "鐗╂枡缂栫爜",
+      label: "鍝佺缂栫爜",
       component: "Input",
       componentProps: {
-        placeholder: "璇疯緭鍏ョ墿鏂欑紪鐮�",
+        placeholder: "璇疯緭鍏ュ搧绉嶇紪鐮�",
         allowClear: true,
       },
     },
     {
       fieldName: "name",
-      label: "鐗╂枡鍚嶇О",
+      label: "鍝佺鍚嶇О",
       component: "Input",
       componentProps: {
-        placeholder: "璇疯緭鍏ョ墿鏂欏悕绉�",
+        placeholder: "璇疯緭鍏ュ搧绉嶅悕绉�",
         allowClear: true,
       },
     },
@@ -527,12 +527,12 @@
     { type: "checkbox", width: 40 },
     {
       field: "code",
-      title: "鐗╂枡缂栫爜",
+      title: "鍝佺缂栫爜",
       minWidth: 120,
     },
     {
       field: "name",
-      title: "鐗╂枡鍚嶇О",
+      title: "鍝佺鍚嶇О",
       minWidth: 180,
     },
     {
@@ -542,7 +542,7 @@
     },
     {
       field: "categoryName",
-      title: "鐗╂枡鍒嗙被",
+      title: "鍝佺鍒嗙被",
       minWidth: 120,
       slots: { default: "categoryName" },
     },
diff --git a/src/views/basicData/mdm/index.vue b/src/views/basicData/mdm/index.vue
index 8995f78..7d92251 100644
--- a/src/views/basicData/mdm/index.vue
+++ b/src/views/basicData/mdm/index.vue
@@ -78,20 +78,20 @@
 /** 瀵煎嚭琛ㄦ牸 */
 // async function handleExport() {
 //   const data = await exportItem(await gridApi.formApi.getValues());
-//   downloadFileFromBlobPart({ fileName: '鐗╂枡.xls', source: data });
+//   downloadFileFromBlobPart({ fileName: '鍝佺.xls', source: data });
 // }
 
-/** 鍒涘缓鐗╂枡 */
+/** 鍒涘缓鍝佺 */
 function handleCreate() {
   formModalApi.setData(null).open();
 }
 
-/** 缂栬緫鐗╂枡 */
+/** 缂栬緫鍝佺 */
 function handleEdit(row: MdmItemApi.Item) {
   formModalApi.setData(row).open();
 }
 
-/** 鍒犻櫎鐗╂枡 */
+/** 鍒犻櫎鍝佺 */
 async function handleDelete(row: MdmItemApi.Item) {
   const hideLoading = message.loading({
     content: $t('ui.actionMessage.deleting', [row.name]),
@@ -106,7 +106,7 @@
   }
 }
 
-/** 鎵归噺鍒犻櫎鐗╂枡 */
+/** 鎵归噺鍒犻櫎鍝佺 */
 async function handleDeleteBatch() {
   await confirm($t('ui.actionMessage.deleteBatchConfirm'));
   const hideLoading = message.loading({
@@ -141,10 +141,10 @@
 
 const syncingId = ref<number | null>(null);
 
-/** 鍒囨崲鍚屾鐘舵�侊細鍏抽棴寮�鍏充細浠� MES 鍒犻櫎鐗╂枡锛岄渶纭 */
+/** 鍒囨崲鍚屾鐘舵�侊細鍏抽棴寮�鍏充細浠� MES 鍒犻櫎鍝佺锛岄渶纭 */
 async function handleSyncChange(row: MdmItemApi.Item, checked: boolean) {
   if (!checked) {
-    await confirm('鍏抽棴鍚屾灏嗕粠 MES 鍒犻櫎璇ョ墿鏂欏強鍏宠仈閰嶇疆锛圔OM/SOP/SIP/鎵规閰嶇疆锛夛紝纭缁х画锛�');
+    await confirm('鍏抽棴鍚屾灏嗕粠 MES 鍒犻櫎璇ュ搧绉嶅強鍏宠仈閰嶇疆锛圔OM/SOP/SIP/鎵规閰嶇疆锛夛紝纭缁х画锛�');
   }
   syncingId.value = row.id!;
   try {
@@ -211,12 +211,12 @@
 <template>
   <Page auto-content-height>
     <FormModal @success="handleRefresh" />
-    <Grid table-title="鐗╂枡鍒楄〃">
+    <Grid table-title="鍝佺鍒楄〃">
       <template #toolbar-tools>
         <TableAction
           :actions="[
             {
-              label: $t('ui.actionTitle.create', ['鐗╂枡']),
+              label: $t('ui.actionTitle.create', ['鍝佺']),
               type: 'primary',
               icon: ACTION_ICON.ADD,
               auth: ['mdm:item:create'],
diff --git a/src/views/basicData/mdm/modules/form.vue b/src/views/basicData/mdm/modules/form.vue
index 5d82e00..c9578e9 100644
--- a/src/views/basicData/mdm/modules/form.vue
+++ b/src/views/basicData/mdm/modules/form.vue
@@ -83,7 +83,7 @@
 </script>
 
 <template>
-  <Modal class="w-[1000px]" :title="formData?.id ? $t('ui.actionTitle.edit', ['鐗╂枡']) : $t('ui.actionTitle.create', ['鐗╂枡'])">
+  <Modal class="w-[1000px]" :title="formData?.id ? $t('ui.actionTitle.edit', ['鍝佺']) : $t('ui.actionTitle.create', ['鍝佺'])">
     <Form class="mx-4" />
   </Modal>
 </template>
diff --git a/src/views/dashboard/bagCodeHome/chart-options.ts b/src/views/dashboard/bagCodeHome/chart-options.ts
new file mode 100644
index 0000000..045568b
--- /dev/null
+++ b/src/views/dashboard/bagCodeHome/chart-options.ts
@@ -0,0 +1,45 @@
+import type { EChartsOption } from '@vben/plugins/echarts';
+
+/** 琚嬬爜鐢熸垚瓒嬪娍鏌辩姸鍥鹃厤缃� */
+export function getBagCodeTrendChartOptions(
+  dates: string[],
+  counts: number[],
+): EChartsOption {
+  return {
+    grid: { bottom: 40, left: 50, right: 20, top: 20 },
+    legend: { bottom: 0, data: ['琚嬬爜鐢熸垚閲�'] },
+    series: [
+      {
+        barMaxWidth: 32,
+        data: counts,
+        itemStyle: { borderRadius: 4, color: '#409EFF' },
+        name: '琚嬬爜鐢熸垚閲�',
+        type: 'bar',
+      },
+    ],
+    tooltip: { axisPointer: { type: 'shadow' }, trigger: 'axis' },
+    xAxis: { axisTick: { show: false }, data: dates, type: 'category' },
+    yAxis: { minInterval: 1, type: 'value' },
+  };
+}
+
+/** 鐘舵�佸垎甯冪幆褰㈠浘閰嶇疆 */
+export function getStatusChartOptions(
+  data: Array<{ itemStyle: { color: string }; name: string; value: number }>,
+): EChartsOption {
+  return {
+    legend: { bottom: 0, type: 'scroll' },
+    series: [
+      {
+        avoidLabelOverlap: true,
+        data,
+        emphasis: { label: { fontSize: 14, fontWeight: 'bold', show: true } },
+        itemStyle: { borderColor: '#fff', borderRadius: 6, borderWidth: 2 },
+        label: { formatter: '{b}\n{c}', show: true },
+        radius: ['40%', '70%'],
+        type: 'pie',
+      },
+    ],
+    tooltip: { formatter: '{b}: {c} ({d}%)', trigger: 'item' },
+  };
+}
diff --git a/src/views/dashboard/bagCodeHome/data.ts b/src/views/dashboard/bagCodeHome/data.ts
new file mode 100644
index 0000000..6dfefbc
--- /dev/null
+++ b/src/views/dashboard/bagCodeHome/data.ts
@@ -0,0 +1,30 @@
+import type { MesBagCodeHomeApi } from '#/api/mes/bag-code-home';
+
+/** 棣栭〉姹囨�荤粺璁¢粯璁ゅ�� */
+export const defaultSummary: MesBagCodeHomeApi.Summary = {
+  bagCodeCount: 0,
+  bagCodeStatusList: [],
+  batchCount: 0,
+  batchStatusList: [],
+  itemTypeCount: 0,
+  lineCount: 0,
+  mdmItemCount: 0,
+  mesItemCount: 0,
+  palletCount: 0,
+  unitMeasureCount: 0,
+};
+
+/** 鎵规鐘舵�佸搴旂殑棰滆壊鏄犲皠锛堢姸鎬佸瓧鍏革細10 鑽夌 20 鐢熶骇涓� 30 宸插畬鎴� 40 宸蹭綔搴燂級 */
+export const BATCH_STATUS_COLOR_MAP: Record<number, string> = {
+  10: '#909399', // 鑽夌
+  20: '#409EFF', // 鐢熶骇涓�
+  30: '#67C23A', // 宸插畬鎴�
+  40: '#F56C6C', // 宸蹭綔搴�
+};
+
+/** 琚嬬爜鐘舵�佸搴旂殑棰滆壊鏄犲皠锛堢姸鎬佸瓧鍏革細10 宸茬敓鎴� 20 宸插鍑哄嵃鍒� 30 宸蹭綔搴燂級 */
+export const BAG_CODE_STATUS_COLOR_MAP: Record<number, string> = {
+  10: '#409EFF', // 宸茬敓鎴�
+  20: '#E6A23C', // 宸插鍑哄嵃鍒�
+  30: '#F56C6C', // 宸蹭綔搴�
+};
diff --git a/src/views/dashboard/bagCodeHome/index.vue b/src/views/dashboard/bagCodeHome/index.vue
new file mode 100644
index 0000000..3887aca
--- /dev/null
+++ b/src/views/dashboard/bagCodeHome/index.vue
@@ -0,0 +1,69 @@
+<script lang="ts" setup>
+import type { MesBagCodeHomeApi } from '#/api/mes/bag-code-home';
+
+import { onMounted, ref } from 'vue';
+import { useRouter } from 'vue-router';
+
+import { Page } from '@vben/common-ui';
+
+import { Col, Row, message } from 'ant-design-vue';
+
+import { getBagCodeHomeSummary } from '#/api/mes/bag-code-home';
+
+import { defaultSummary } from './data';
+import BagCodeCards from './modules/bag-code-cards.vue';
+import BagCodeStatusChart from './modules/bag-code-status-chart.vue';
+import BasicCards from './modules/basic-cards.vue';
+import BatchStatusChart from './modules/batch-status-chart.vue';
+import Shortcuts from './modules/shortcuts.vue';
+import TrendChart from './modules/trend-chart.vue';
+
+defineOptions({ name: 'DashboardBagCodeHome' });
+
+const router = useRouter();
+const summary = ref<MesBagCodeHomeApi.Summary>(defaultSummary); // 棣栭〉姹囨�荤粺璁�
+
+/** 璺宠浆鍒扮洰鏍囬〉闈紙鎸夎矾鐢� name锛屾湭鍔犺浇鏃舵彁绀猴級 */
+async function handleNavigate(name: string) {
+  try {
+    await router.push({ name });
+  } catch {
+    message.warning('褰撳墠鑿滃崟灏氭湭鍔犺浇锛岃浠庡乏渚ц彍鍗曡繘鍏ュ搴旈〉闈�');
+  }
+}
+
+/** 鍔犺浇棣栭〉姹囨�荤粺璁� */
+async function loadSummary() {
+  summary.value = await getBagCodeHomeSummary();
+}
+
+onMounted(() => {
+  loadSummary();
+});
+</script>
+
+<template>
+  <Page>
+    <!-- 绗竴琛岋細鍩虹鏁版嵁鍗$墖 -->
+    <BasicCards :summary="summary" class="mb-2" @navigate="handleNavigate" />
+
+    <!-- 绗簩琛岋細涓�琚嬩竴鐮佸崱鐗� -->
+    <BagCodeCards :summary="summary" class="mb-4" @navigate="handleNavigate" />
+
+    <!-- 绗笁琛岋細琚嬬爜瓒嬪娍 + 鎵规/琚嬬爜鐘舵�佸垎甯� -->
+    <Row :gutter="16" class="mb-4">
+      <Col :lg="12" :md="24" :sm="24" :xl="12" :xs="24" class="mb-4">
+        <TrendChart />
+      </Col>
+      <Col :lg="6" :md="12" :sm="24" :xl="6" :xs="24" class="mb-4">
+        <BatchStatusChart :list="summary.batchStatusList" />
+      </Col>
+      <Col :lg="6" :md="12" :sm="24" :xl="6" :xs="24" class="mb-4">
+        <BagCodeStatusChart :list="summary.bagCodeStatusList" />
+      </Col>
+    </Row>
+
+    <!-- 绗洓琛岋細蹇嵎鍏ュ彛 -->
+    <Shortcuts @navigate="handleNavigate" />
+  </Page>
+</template>
diff --git a/src/views/dashboard/bagCodeHome/modules/bag-code-cards.vue b/src/views/dashboard/bagCodeHome/modules/bag-code-cards.vue
new file mode 100644
index 0000000..42fa28b
--- /dev/null
+++ b/src/views/dashboard/bagCodeHome/modules/bag-code-cards.vue
@@ -0,0 +1,74 @@
+<script lang="ts" setup>
+import type { MesBagCodeHomeApi } from '#/api/mes/bag-code-home';
+
+import { CountTo } from '@vben/common-ui';
+import { IconifyIcon } from '@vben/icons';
+
+import { Card, Col, Row } from 'ant-design-vue';
+
+defineOptions({ name: 'BagCodeHomeBagCodeCards' });
+
+const props = defineProps<{
+  summary: MesBagCodeHomeApi.Summary;
+}>();
+
+const emit = defineEmits<{
+  navigate: [name: string];
+}>();
+
+/** 涓�琚嬩竴鐮佸崱鐗囬厤缃� */
+const cards = [
+  {
+    gradient: 'linear-gradient(135deg, #0ea5e9, #38bdf8)',
+    icon: 'lucide:factory',
+    name: '浜х嚎',
+    routeName: 'MesProLine',
+    value: () => props.summary.lineCount,
+  },
+  {
+    gradient: 'linear-gradient(135deg, #f59e0b, #fbbf24)',
+    icon: 'lucide:layers',
+    name: '鐢熶骇鎵规',
+    routeName: 'MesProBatch',
+    value: () => props.summary.batchCount,
+  },
+  {
+    gradient: 'linear-gradient(135deg, #ec4899, #f472b6)',
+    icon: 'lucide:scan-barcode',
+    name: '琚嬬爜',
+    routeName: 'MesProBagCode',
+    value: () => props.summary.bagCodeCount,
+  },
+  {
+    gradient: 'linear-gradient(135deg, #14b8a6, #2dd4bf)',
+    icon: 'lucide:grid-2x2',
+    name: '鎵樼洏鐮�',
+    routeName: 'MesProPallet',
+    value: () => props.summary.palletCount,
+  },
+];
+</script>
+
+<template>
+  <Row :gutter="16">
+    <Col v-for="card in cards" :key="card.name" :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4">
+      <Card
+        class="kpi-card cursor-pointer transition-all hover:-translate-y-1 hover:shadow-lg"
+        @click="emit('navigate', card.routeName)"
+      >
+        <div class="flex items-center gap-4">
+          <div
+            class="flex size-14 flex-shrink-0 items-center justify-center rounded-xl text-white"
+            :style="{ background: card.gradient }"
+          >
+            <IconifyIcon class="size-7" :icon="card.icon" />
+          </div>
+          <div>
+            <div class="text-muted-foreground mb-1 text-sm">{{ card.name }}</div>
+            <CountTo class="text-2xl font-bold leading-tight" :end-val="card.value()" :duration="1500" />
+          </div>
+        </div>
+      </Card>
+    </Col>
+  </Row>
+</template>
diff --git a/src/views/dashboard/bagCodeHome/modules/bag-code-status-chart.vue b/src/views/dashboard/bagCodeHome/modules/bag-code-status-chart.vue
new file mode 100644
index 0000000..f9821b3
--- /dev/null
+++ b/src/views/dashboard/bagCodeHome/modules/bag-code-status-chart.vue
@@ -0,0 +1,43 @@
+<script lang="ts" setup>
+import type { EchartsUIType } from '@vben/plugins/echarts';
+import type { MesBagCodeHomeApi } from '#/api/mes/bag-code-home';
+
+import { ref, watch } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictLabel } from '@vben/hooks';
+import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
+
+import { Card } from 'ant-design-vue';
+
+import { getStatusChartOptions } from '../chart-options';
+import { BAG_CODE_STATUS_COLOR_MAP } from '../data';
+
+defineOptions({ name: 'BagCodeHomeBagCodeStatusChart' });
+
+const props = defineProps<{
+  list: MesBagCodeHomeApi.StatusItem[];
+}>();
+
+const chartRef = ref<EchartsUIType>();
+const { renderEcharts } = useEcharts(chartRef);
+
+watch(
+  () => props.list,
+  async (list) => {
+    const data = list.map((item) => ({
+      itemStyle: { color: BAG_CODE_STATUS_COLOR_MAP[item.status] || '#909399' },
+      name: getDictLabel(DICT_TYPE.MES_PRO_BAG_CODE_STATUS, item.status) || `鐘舵��${item.status}`,
+      value: item.count,
+    }));
+    await renderEcharts(getStatusChartOptions(data));
+  },
+  { immediate: true },
+);
+</script>
+
+<template>
+  <Card title="琚嬬爜鐘舵�佸垎甯�" class="h-full">
+    <EchartsUI ref="chartRef" class="h-[320px] w-full" />
+  </Card>
+</template>
diff --git a/src/views/dashboard/bagCodeHome/modules/basic-cards.vue b/src/views/dashboard/bagCodeHome/modules/basic-cards.vue
new file mode 100644
index 0000000..3ca866a
--- /dev/null
+++ b/src/views/dashboard/bagCodeHome/modules/basic-cards.vue
@@ -0,0 +1,74 @@
+<script lang="ts" setup>
+import type { MesBagCodeHomeApi } from '#/api/mes/bag-code-home';
+
+import { CountTo } from '@vben/common-ui';
+import { IconifyIcon } from '@vben/icons';
+
+import { Card, Col, Row } from 'ant-design-vue';
+
+defineOptions({ name: 'BagCodeHomeBasicCards' });
+
+const props = defineProps<{
+  summary: MesBagCodeHomeApi.Summary;
+}>();
+
+const emit = defineEmits<{
+  navigate: [name: string];
+}>();
+
+/** 鍩虹鏁版嵁鍗$墖閰嶇疆 */
+const cards = [
+  {
+    gradient: 'linear-gradient(135deg, #409eff, #66b1ff)',
+    icon: 'lucide:package',
+    name: '鍝佺鎬绘暟',
+    routeName: 'MdmItem',
+    value: () => props.summary.mdmItemCount,
+  },
+  {
+    gradient: 'linear-gradient(135deg, #67c23a, #85ce61)',
+    icon: 'lucide:box',
+    name: 'MES 鍝佺',
+    routeName: 'MesMdItem',
+    value: () => props.summary.mesItemCount,
+  },
+  {
+    gradient: 'linear-gradient(135deg, #e6a23c, #ebb563)',
+    icon: 'lucide:folder-tree',
+    name: '鍝佺鍒嗙被',
+    routeName: 'MesMdItemType',
+    value: () => props.summary.itemTypeCount,
+  },
+  {
+    gradient: 'linear-gradient(135deg, #7c3aed, #9461f5)',
+    icon: 'lucide:ruler',
+    name: '璁¢噺鍗曚綅',
+    routeName: 'MdmUnit',
+    value: () => props.summary.unitMeasureCount,
+  },
+];
+</script>
+
+<template>
+  <Row :gutter="16">
+    <Col v-for="card in cards" :key="card.name" :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4">
+      <Card
+        class="kpi-card cursor-pointer transition-all hover:-translate-y-1 hover:shadow-lg"
+        @click="emit('navigate', card.routeName)"
+      >
+        <div class="flex items-center gap-4">
+          <div
+            class="flex size-14 flex-shrink-0 items-center justify-center rounded-xl text-white"
+            :style="{ background: card.gradient }"
+          >
+            <IconifyIcon class="size-7" :icon="card.icon" />
+          </div>
+          <div>
+            <div class="text-muted-foreground mb-1 text-sm">{{ card.name }}</div>
+            <CountTo class="text-2xl font-bold leading-tight" :end-val="card.value()" :duration="1500" />
+          </div>
+        </div>
+      </Card>
+    </Col>
+  </Row>
+</template>
diff --git a/src/views/dashboard/bagCodeHome/modules/batch-status-chart.vue b/src/views/dashboard/bagCodeHome/modules/batch-status-chart.vue
new file mode 100644
index 0000000..45a27ff
--- /dev/null
+++ b/src/views/dashboard/bagCodeHome/modules/batch-status-chart.vue
@@ -0,0 +1,43 @@
+<script lang="ts" setup>
+import type { EchartsUIType } from '@vben/plugins/echarts';
+import type { MesBagCodeHomeApi } from '#/api/mes/bag-code-home';
+
+import { ref, watch } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictLabel } from '@vben/hooks';
+import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
+
+import { Card } from 'ant-design-vue';
+
+import { getStatusChartOptions } from '../chart-options';
+import { BATCH_STATUS_COLOR_MAP } from '../data';
+
+defineOptions({ name: 'BagCodeHomeBatchStatusChart' });
+
+const props = defineProps<{
+  list: MesBagCodeHomeApi.StatusItem[];
+}>();
+
+const chartRef = ref<EchartsUIType>();
+const { renderEcharts } = useEcharts(chartRef);
+
+watch(
+  () => props.list,
+  async (list) => {
+    const data = list.map((item) => ({
+      itemStyle: { color: BATCH_STATUS_COLOR_MAP[item.status] || '#909399' },
+      name: getDictLabel(DICT_TYPE.MES_PRO_BATCH_STATUS, item.status) || `鐘舵��${item.status}`,
+      value: item.count,
+    }));
+    await renderEcharts(getStatusChartOptions(data));
+  },
+  { immediate: true },
+);
+</script>
+
+<template>
+  <Card title="鎵规鐘舵�佸垎甯�" class="h-full">
+    <EchartsUI ref="chartRef" class="h-[320px] w-full" />
+  </Card>
+</template>
diff --git a/src/views/dashboard/bagCodeHome/modules/shortcuts.vue b/src/views/dashboard/bagCodeHome/modules/shortcuts.vue
new file mode 100644
index 0000000..bac404f
--- /dev/null
+++ b/src/views/dashboard/bagCodeHome/modules/shortcuts.vue
@@ -0,0 +1,49 @@
+<script lang="ts" setup>
+import { useRouter } from 'vue-router';
+
+import { IconifyIcon } from '@vben/icons';
+
+import { Card, Col, Row, message } from 'ant-design-vue';
+
+defineOptions({ name: 'BagCodeHomeShortcuts' });
+
+const router = useRouter();
+
+/** 蹇嵎鍏ュ彛鍒楄〃锛屾寜涓氬姟鍩熺簿绠�涓� 4 涓牳蹇冨叆鍙� */
+const shortcuts = [
+  { bgColor: '#409EFF', icon: 'lucide:package', name: '鍝佺绠$悊', routeName: 'MdmItem' },
+  { bgColor: '#E6A23C', icon: 'lucide:folder-tree', name: '鍝佺鍒嗙被', routeName: 'MesMdItemType' },
+  { bgColor: '#f59e0b', icon: 'lucide:layers', name: '鎵规鐢熶骇', routeName: 'MesProBatch' },
+  { bgColor: '#ec4899', icon: 'lucide:scan-barcode', name: '璧嬬爜绠$悊', routeName: 'MesProBagCode' },
+];
+
+/** 璺宠浆鍒扮洰鏍囬〉闈紙鎸夎矾鐢� name锛屾湭鍔犺浇鏃舵彁绀猴級 */
+async function handleNavigate(routeName: string) {
+  try {
+    await router.push({ name: routeName });
+  } catch {
+    message.warning('褰撳墠鑿滃崟灏氭湭鍔犺浇锛岃浠庡乏渚ц彍鍗曡繘鍏ュ搴旈〉闈�');
+  }
+}
+</script>
+
+<template>
+  <Card title="蹇嵎鍏ュ彛" class="h-full">
+    <Row :gutter="16">
+      <Col v-for="item in shortcuts" :key="item.name" :lg="6" :md="8" :sm="12" :xs="12" class="mb-4">
+        <div
+          class="hover:bg-accent flex cursor-pointer flex-col items-center gap-2 rounded-lg py-3 transition-all hover:-translate-y-0.5"
+          @click="handleNavigate(item.routeName)"
+        >
+          <div
+            class="flex size-12 items-center justify-center rounded-xl text-white"
+            :style="{ background: item.bgColor }"
+          >
+            <IconifyIcon class="size-6" :icon="item.icon" />
+          </div>
+          <span class="text-sm">{{ item.name }}</span>
+        </div>
+      </Col>
+    </Row>
+  </Card>
+</template>
diff --git a/src/views/dashboard/bagCodeHome/modules/trend-chart.vue b/src/views/dashboard/bagCodeHome/modules/trend-chart.vue
new file mode 100644
index 0000000..2443482
--- /dev/null
+++ b/src/views/dashboard/bagCodeHome/modules/trend-chart.vue
@@ -0,0 +1,48 @@
+<script lang="ts" setup>
+import type { EchartsUIType } from '@vben/plugins/echarts';
+
+import { onMounted, ref } from 'vue';
+
+import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
+
+import { Card, RadioButton, RadioGroup } from 'ant-design-vue';
+
+import { getBagCodeHomeTrend } from '#/api/mes/bag-code-home';
+
+import { getBagCodeTrendChartOptions } from '../chart-options';
+
+defineOptions({ name: 'BagCodeHomeTrendChart' });
+
+const trendDays = ref(7); // 褰撳墠閫変腑鐨勫ぉ鏁拌寖鍥�
+const chartRef = ref<EchartsUIType>();
+const { renderEcharts } = useEcharts(chartRef);
+
+/** 鍔犺浇琚嬬爜鐢熸垚瓒嬪娍鏁版嵁骞舵覆鏌撳浘琛� */
+async function loadData() {
+  const data = await getBagCodeHomeTrend(trendDays.value);
+  const dates = data.map((d) => d.date.slice(5));
+  const counts = data.map((d) => d.count);
+  await renderEcharts(getBagCodeTrendChartOptions(dates, counts));
+}
+
+/** 鍒囨崲澶╂暟鑼冨洿 */
+function handleDaysChange() {
+  loadData();
+}
+
+onMounted(() => {
+  loadData();
+});
+</script>
+
+<template>
+  <Card title="琚嬬爜鐢熸垚瓒嬪娍" class="h-full">
+    <template #extra>
+      <RadioGroup v-model:value="trendDays" size="small" @change="handleDaysChange">
+        <RadioButton :value="7">杩� 7 澶�</RadioButton>
+        <RadioButton :value="30">杩� 30 澶�</RadioButton>
+      </RadioGroup>
+    </template>
+    <EchartsUI ref="chartRef" class="h-[320px] w-full" />
+  </Card>
+</template>
diff --git a/src/views/mes/md/item/components/select-dialog.vue b/src/views/mes/md/item/components/select-dialog.vue
index d377f66..f30fedb 100644
--- a/src/views/mes/md/item/components/select-dialog.vue
+++ b/src/views/mes/md/item/components/select-dialog.vue
@@ -23,11 +23,11 @@
 
   const open = ref(false); // 寮圭獥鏄惁鎵撳紑
   const multiple = ref(true); // 鏄惁澶氶��
-  const selectedRows = ref<MesMdItemApi.Item[]>([]); // 宸查�夌墿鏂欏垪琛�
+  const selectedRows = ref<MesMdItemApi.Item[]>([]); // 宸查�夊搧绉嶅垪琛�
   const selectedItemTypeId = ref<number>(); // 褰撳墠绛涢�夊垎绫荤紪鍙�
-  const preSelectedIds = ref<number[]>([]); // 棰勯�夌墿鏂欑紪鍙峰垪琛�
-  const productIds = ref<number[]>([]); // 闄愬畾鍙�夌殑鐗╂枡缂栧彿鍒楄〃锛堜负绌哄垯涓嶉檺鍒讹級
-  const typeTreeRef = ref<InstanceType<typeof MdItemTypeTree>>(); // 鐗╂枡鍒嗙被鏍�
+  const preSelectedIds = ref<number[]>([]); // 棰勯�夊搧绉嶇紪鍙峰垪琛�
+  const productIds = ref<number[]>([]); // 闄愬畾鍙�夌殑鍝佺缂栧彿鍒楄〃锛堜负绌哄垯涓嶉檺鍒讹級
+  const typeTreeRef = ref<InstanceType<typeof MdItemTypeTree>>(); // 鍝佺鍒嗙被鏍�
 
   /** 鑾峰彇褰撳墠琛ㄦ牸鏁版嵁 */
   function getTableRows() {
@@ -78,13 +78,13 @@
     handleConfirm();
   }
 
-  /** 鎸夊垎绫荤瓫閫夌墿鏂� */
+  /** 鎸夊垎绫荤瓫閫夊搧绉� */
   function handleItemTypeNodeClick(row: MesMdItemTypeApi.ItemType | undefined) {
     selectedItemTypeId.value = row?.id;
     gridApi.query();
   }
 
-  /** 鍥炴樉棰勯�夌墿鏂� */
+  /** 鍥炴樉棰勯�夊搧绉� */
   async function applyPreSelection() {
     if (preSelectedIds.value.length === 0) {
       return;
@@ -138,7 +138,7 @@
               itemTypeId: selectedItemTypeId.value,
               status: CommonStatusEnum.ENABLE,
             };
-            // 闄愬畾鍙�夌墿鏂欒寖鍥�
+            // 闄愬畾鍙�夊搧绉嶈寖鍥�
             if (productIds.value.length > 0) {
               params.pageSize = 1000;
               params.ids = productIds.value.join(',');
@@ -178,7 +178,7 @@
     await gridApi.formApi.resetForm();
   }
 
-  /** 鎵撳紑鐗╂枡閫夋嫨寮圭獥 */
+  /** 鎵撳紑鍝佺閫夋嫨寮圭獥 */
   async function openModal(
     selectedIds?: number[],
     options?: { multiple?: boolean; productIds?: number[] }
@@ -197,13 +197,13 @@
     await applyPreSelection();
   }
 
-  /** 鍏抽棴鐗╂枡閫夋嫨寮圭獥 */
+  /** 鍏抽棴鍝佺閫夋嫨寮圭獥 */
   async function closeModal() {
     open.value = false;
     await resetQueryState();
   }
 
-  /** 纭閫夋嫨鐗╂枡 */
+  /** 纭閫夋嫨鍝佺 */
   function handleConfirm() {
     const rows = multiple.value ? getMultipleSelectedRows() : selectedRows.value;
     if (rows.length === 0) {
@@ -219,7 +219,7 @@
 
 <template>
   <Modal v-model:open="open"
-         title="鐗╂枡浜у搧閫夋嫨"
+         title="鍝佺浜у搧閫夋嫨"
          width="80%"
          :destroy-on-close="true"
          @ok="handleConfirm"
@@ -230,7 +230,7 @@
                         @node-click="handleItemTypeNodeClick" />
       </Card>
       <div class="w-4/5">
-        <Grid table-title="鐗╂枡浜у搧鍒楄〃" />
+        <Grid table-title="鍝佺浜у搧鍒楄〃" />
       </div>
     </div>
   </Modal>
diff --git a/src/views/mes/md/item/components/select.vue b/src/views/mes/md/item/components/select.vue
index 6a45bdb..3213a01 100644
--- a/src/views/mes/md/item/components/select.vue
+++ b/src/views/mes/md/item/components/select.vue
@@ -27,7 +27,7 @@
     disabled: false,
     modelValue: undefined,
     multiple: false,
-    placeholder: '璇烽�夋嫨浜у搧鐗╂枡',
+    placeholder: '璇烽�夋嫨浜у搧鍝佺',
     productIds: () => [],
   },
 );
@@ -54,7 +54,7 @@
       : props.modelValue != null),
 );
 
-/** 鏍规嵁鐗╂枡缂栧彿鍥炴樉閫夋嫨鍣� */
+/** 鏍规嵁鍝佺缂栧彿鍥炴樉閫夋嫨鍣� */
 async function resolveItemsById(values: number | number[] | undefined) {
   if (values == null || (Array.isArray(values) && values.length === 0)) {
     selectedItems.value = [];
@@ -80,7 +80,7 @@
   { immediate: true },
 );
 
-/** 娓呯┖宸查�夌墿鏂� */
+/** 娓呯┖宸查�夊搧绉� */
 function clearSelected() {
   selectedItems.value = [];
   if (props.multiple) {
@@ -92,7 +92,7 @@
   }
 }
 
-/** 鎵撳紑鐗╂枡閫夋嫨寮圭獥 */
+/** 鎵撳紑鍝佺閫夋嫨寮圭獥 */
 function handleClick(event: MouseEvent) {
   if (props.disabled) {
     return;
@@ -114,7 +114,7 @@
   });
 }
 
-/** 鍥炲~閫変腑鐨勭墿鏂� */
+/** 鍥炲~閫変腑鐨勫搧绉� */
 function handleSelected(rows: MesMdItemApi.Item[]) {
   if (props.multiple) {
     selectedItems.value = rows;
diff --git a/src/views/mes/md/item/data.ts b/src/views/mes/md/item/data.ts
index 5d74bb2..d18c30f 100644
--- a/src/views/mes/md/item/data.ts
+++ b/src/views/mes/md/item/data.ts
@@ -332,31 +332,31 @@
   ];
 }
 
-/** 鐗╂枡閫夋嫨寮圭獥鎼滅储琛ㄥ崟 */
+/** 鍝佺閫夋嫨寮圭獥鎼滅储琛ㄥ崟 */
 export function useItemSelectGridFormSchema(): VbenFormSchema[] {
   return [
     {
       fieldName: 'code',
-      label: '鐗╂枡缂栫爜',
+      label: '鍝佺缂栫爜',
       component: 'Input',
       componentProps: {
         allowClear: true,
-        placeholder: '璇疯緭鍏ョ墿鏂欑紪鐮�',
+        placeholder: '璇疯緭鍏ュ搧绉嶇紪鐮�',
       },
     },
     {
       fieldName: 'name',
-      label: '鐗╂枡鍚嶇О',
+      label: '鍝佺鍚嶇О',
       component: 'Input',
       componentProps: {
         allowClear: true,
-        placeholder: '璇疯緭鍏ョ墿鏂欏悕绉�',
+        placeholder: '璇疯緭鍏ュ搧绉嶅悕绉�',
       },
     },
   ];
 }
 
-/** 鐗╂枡閫夋嫨寮圭獥鍒楄〃瀛楁 */
+/** 鍝佺閫夋嫨寮圭獥鍒楄〃瀛楁 */
 export function useItemSelectGridColumns(
   multiple = true,
 ): VxeTableGridOptions<MesMdItemApi.Item>['columns'] {
@@ -364,12 +364,12 @@
     { type: multiple ? 'checkbox' : 'radio', width: 50 },
     {
       field: 'code',
-      title: '鐗╂枡缂栫爜',
+      title: '鍝佺缂栫爜',
       width: 180,
     },
     {
       field: 'name',
-      title: '鐗╂枡鍚嶇О',
+      title: '鍝佺鍚嶇О',
       minWidth: 160,
       align: 'left',
     },
@@ -387,7 +387,7 @@
     },
     {
       field: 'itemOrProduct',
-      title: '鐗╂枡/浜у搧',
+      title: '鍝佺/浜у搧',
       width: 110,
       align: 'center',
       cellRender: {
diff --git a/src/views/mes/md/item/type/data.ts b/src/views/mes/md/item/type/data.ts
index 3858003..b9a95de 100644
--- a/src/views/mes/md/item/type/data.ts
+++ b/src/views/mes/md/item/type/data.ts
@@ -22,7 +22,7 @@
 /** 琛ㄥ崟绫诲瀷 */
 export type FormType = 'create' | 'detail' | 'update';
 
-/** 鏂板/淇敼鐗╂枡鍒嗙被鐨勮〃鍗� */
+/** 鏂板/淇敼鍝佺鍒嗙被鐨勮〃鍗� */
 export function useFormSchema(
   formType: FormType,
   formApi?: VbenFormApi,
@@ -99,7 +99,7 @@
     },
     {
       fieldName: 'itemOrProduct',
-      label: '鐗╂枡/浜у搧鏍囪瘑',
+      label: '鍝佺/浜у搧鏍囪瘑',
       component: 'RadioGroup',
       componentProps: {
         buttonStyle: 'solid',
@@ -185,7 +185,7 @@
     },
     {
       field: 'itemOrProduct',
-      title: '鐗╂枡/浜у搧',
+      title: '鍝佺/浜у搧',
       width: 130,
       align: 'center',
       cellRender: {
diff --git a/src/views/mes/md/item/type/index.vue b/src/views/mes/md/item/type/index.vue
index 062b7d9..5d2eccd 100644
--- a/src/views/mes/md/item/type/index.vue
+++ b/src/views/mes/md/item/type/index.vue
@@ -102,12 +102,12 @@
 
 <template>
   <Page auto-content-height><FormModal @success="handleRefresh" />
-    <Grid table-title="鐗╂枡鍒嗙被鍒楄〃">
+    <Grid table-title="鍝佺鍒嗙被鍒楄〃">
       <template #toolbar-tools>
         <TableAction
           :actions="[
             {
-              label: $t('ui.actionTitle.create', ['鐗╂枡鍒嗙被']),
+              label: $t('ui.actionTitle.create', ['鍝佺鍒嗙被']),
               type: 'primary',
               icon: ACTION_ICON.ADD,
               auth: ['mes:md-item-type:create'],
diff --git a/src/views/mes/md/item/type/modules/form.vue b/src/views/mes/md/item/type/modules/form.vue
index 50d1ede..3e3bc58 100644
--- a/src/views/mes/md/item/type/modules/form.vue
+++ b/src/views/mes/md/item/type/modules/form.vue
@@ -21,8 +21,8 @@
 const formData = ref<MesMdItemTypeApi.ItemType>();
 const getTitle = computed(() => {
   return formData.value?.id
-    ? $t('ui.actionTitle.edit', ['鐗╂枡鍒嗙被'])
-    : $t('ui.actionTitle.create', ['鐗╂枡鍒嗙被']);
+    ? $t('ui.actionTitle.edit', ['鍝佺鍒嗙被'])
+    : $t('ui.actionTitle.create', ['鍝佺鍒嗙被']);
 });
 
 const [Form, formApi] = useVbenForm({
diff --git a/src/views/mes/pro/bagCode/data.ts b/src/views/mes/pro/bagCode/data.ts
new file mode 100644
index 0000000..f611bcc
--- /dev/null
+++ b/src/views/mes/pro/bagCode/data.ts
@@ -0,0 +1,130 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesProBagCodeApi } from '#/api/mes/pro/bagCode';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'code',
+      label: '琚嬬爜',
+      component: 'Input',
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇疯緭鍏ヨ鐮侊紙浜岀淮鐮佸唴瀹癸級',
+      },
+    },
+    {
+      fieldName: 'batchCode',
+      label: '鎵规鍙�',
+      component: 'Input',
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇疯緭鍏ユ壒娆″彿',
+      },
+    },
+    {
+      fieldName: 'uuid',
+      label: '鐢熸垚缁�',
+      component: 'Input',
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇疯緭鍏ョ敓鎴愮粍缂栧彿',
+      },
+    },
+    {
+      fieldName: 'status',
+      label: '琚嬬爜鐘舵��',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: getDictOptions(DICT_TYPE.MES_PRO_BAG_CODE_STATUS, 'number'),
+        placeholder: '璇烽�夋嫨琚嬬爜鐘舵��',
+      },
+    },
+    {
+      fieldName: 'createTime',
+      label: '鍒涘缓鏃堕棿',
+      component: 'RangePicker',
+      componentProps: {
+        ...getRangePickerDefaultProps(),
+        allowClear: true,
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions<MesProBagCodeApi.BagCode>['columns'] {
+  return [
+    { type: 'checkbox', width: 50 },
+    {
+      field: 'code',
+      title: '琚嬬爜锛堜簩缁寸爜鍐呭锛�',
+      fixed: 'left',
+      minWidth: 220,
+    },
+    {
+      field: 'bagNo',
+      title: '琚嬪簭鍙�',
+      width: 80,
+    },
+    {
+      field: 'uuid',
+      title: '鐢熸垚缁�',
+      minWidth: 150,
+    },
+    {
+      field: 'batchCode',
+      title: '鎵规鍙�',
+      minWidth: 190,
+    },
+    {
+      field: 'lineCode',
+      title: '浜х嚎缂栫爜',
+      width: 100,
+    },
+    {
+      field: 'lineName',
+      title: '浜х嚎鍚嶇О',
+      width: 120,
+    },
+    {
+      field: 'palletCode',
+      title: '缁戝畾鎵樼洏鐮�',
+      minWidth: 190,
+    },
+    {
+      field: 'status',
+      title: '琚嬬爜鐘舵��',
+      width: 110,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.MES_PRO_BAG_CODE_STATUS },
+      },
+    },
+    {
+      field: 'printTime',
+      title: '瀵煎嚭鍗板埛鏃堕棿',
+      width: 170,
+      formatter: 'formatDateTime',
+    },
+    {
+      field: 'createTime',
+      title: '鍒涘缓鏃堕棿',
+      width: 170,
+      formatter: 'formatDateTime',
+    },
+    {
+      title: '鎿嶄綔',
+      width: 180,
+      fixed: 'right',
+      slots: { default: 'actions' },
+    },
+  ];
+}
diff --git a/src/views/mes/pro/bagCode/index.vue b/src/views/mes/pro/bagCode/index.vue
new file mode 100644
index 0000000..f4ffa22
--- /dev/null
+++ b/src/views/mes/pro/bagCode/index.vue
@@ -0,0 +1,273 @@
+<script lang="ts" setup>
+  import type { VxeTableGridOptions } from "#/adapter/vxe-table";
+  import type { MesProBagCodeApi } from "#/api/mes/pro/bagCode";
+
+  import { confirm, Page, useVbenModal } from "@vben/common-ui";
+  import { MesProBagCodeStatusEnum } from "@vben/constants";
+  import { downloadFileFromBlobPart } from "@vben/utils";
+
+  import { message } from "ant-design-vue";
+
+  import { ACTION_ICON, TableAction, useVbenVxeGrid } from "#/adapter/vxe-table";
+  import {
+    deleteBagCodesByUuid,
+    exportBagCodeExcel,
+    exportBagQr,
+    getBagCodePage,
+    unbindBagCodePallet,
+  } from "#/api/mes/pro/bagCode";
+  import { $t } from "#/locales";
+
+  import { useGridColumns, useGridFormSchema } from "./data";
+  import BindPalletModal from "./modules/bind-pallet.vue";
+  import GenerateModal from "./modules/generate-form.vue";
+  import QrTraceModal from "../components/qr-trace-modal.vue";
+
+  defineOptions({ name: "MesProBagCode" });
+
+  const [GenerateFormModal, generateFormModalApi] = useVbenModal({
+    connectedComponent: GenerateModal,
+    destroyOnClose: true,
+  });
+
+  const [BindPalletFormModal, bindPalletFormModalApi] = useVbenModal({
+    connectedComponent: BindPalletModal,
+    destroyOnClose: true,
+  });
+
+  const [QrTraceQueryModal, qrTraceQueryModalApi] = useVbenModal({
+    connectedComponent: QrTraceModal,
+    destroyOnClose: true,
+  });
+
+  /** 鍒锋柊琛ㄦ牸 */
+  function handleRefresh() {
+    gridApi.query();
+  }
+
+  /** 鐢熸垚鏁版嵁鍖� */
+  function handleGenerate() {
+    generateFormModalApi.open();
+  }
+
+  /** 鎵爜杩芥函 */
+  function handleTrace() {
+    qrTraceQueryModalApi.open();
+  }
+
+  /** 鏌ョ湅浜岀淮鐮侊紙琛屽唴棰勫~鐮佸�硷級 */
+  function handleViewQr(row: MesProBagCodeApi.BagCode) {
+    qrTraceQueryModalApi.setData({ code: row.code }).open();
+  }
+
+  /** 瀵煎嚭浜岀淮鐮� ZIP锛堟寜褰撳墠鏌ヨ鏉′欢鐨勭敓鎴愮粍/鎵规锛� */
+  async function handleExportQr() {
+    const values = await gridApi.formApi.getValues();
+    const params: { uuid?: string; batchId?: number } = {};
+    if (values.uuid) {
+      params.uuid = values.uuid;
+    }
+    if (values.batchId) {
+      params.batchId = values.batchId;
+    }
+    if (!params.uuid && !params.batchId) {
+      message.warning("璇峰厛鎸夌敓鎴愮粍鎴栨壒娆$瓫閫夊悗鍐嶅鍑轰簩缁寸爜");
+      return;
+    }
+    const data = await exportBagQr(params);
+    downloadFileFromBlobPart({ fileName: "琚嬬爜浜岀淮鐮�.zip", source: data });
+  }
+
+  /** 瀵煎嚭琚嬬爜 Excel锛堜氦浠樺嵃鍒凤級锛屽鍑哄悗琚嬬爜鐘舵�佸彉涓哄凡鍗板埛锛岄渶鍒锋柊鍒楄〃 */
+  async function handleExport() {
+    const data = await exportBagCodeExcel(await gridApi.formApi.getValues());
+    downloadFileFromBlobPart({ fileName: "琚嬬爜鏁版嵁鍖�.xls", source: data });
+    message.success("瀵煎嚭鎴愬姛锛屽鍑虹殑琚嬬爜宸叉爣璁颁负宸插嵃鍒�");
+    handleRefresh();
+  }
+
+  /** 鑾峰彇鍕鹃�夌殑琚嬬爜璁板綍 */
+  function getSelectedRecords(): MesProBagCodeApi.BagCode[] | undefined {
+    const records = gridApi.grid.getCheckboxRecords() as MesProBagCodeApi.BagCode[];
+    if (records.length === 0) {
+      message.warning("璇疯嚦灏戝嬀閫変竴鏉¤鐮佽褰�");
+      return undefined;
+    }
+    return records;
+  }
+
+  /** 鎵归噺缁戝畾鎵樼洏 */
+  function handleBindPallet() {
+    const records = getSelectedRecords();
+    if (!records) return;
+    const batchIds = new Set(records.map((record) => record.batchId));
+    if (batchIds.size > 1) {
+      message.warning("璇烽�夋嫨鍚屼竴鎵规涓嬬殑琚嬬爜杩涜缁戝畾");
+      return;
+    }
+    if (records.some((record) => record.palletId)) {
+      message.warning("鎵�閫夎鐮佷腑瀛樺湪宸茬粦瀹氭墭鐩樼殑璁板綍锛岃鍏堣В缁�");
+      return;
+    }
+    if (
+      records.some(
+        (record) => record.status === MesProBagCodeStatusEnum.CANCELED
+      )
+    ) {
+      message.warning("宸蹭綔搴熺殑琚嬬爜涓嶅厑璁哥粦瀹氭墭鐩�");
+      return;
+    }
+    const first = records[0]!;
+    bindPalletFormModalApi
+      .setData({
+        batchId: first.batchId,
+        batchCode: first.batchCode,
+        ids: records.map((record) => record.id!),
+      })
+      .open();
+  }
+
+  /** 鎵归噺瑙g粦鎵樼洏 */
+  async function handleUnbindPallet() {
+    const records = getSelectedRecords();
+    if (!records) return;
+    if (records.some((record) => !record.palletId)) {
+      message.warning("鎵�閫夎鐮佷腑瀛樺湪鏈粦瀹氭墭鐩樼殑璁板綍锛屾棤娉曡В缁�");
+      return;
+    }
+    const palletIds = new Set(records.map((record) => record.palletId));
+    if (palletIds.size > 1) {
+      message.warning("璇烽�夋嫨缁戝畾鍦ㄥ悓涓�鎵樼洏涓婄殑琚嬬爜杩涜瑙g粦");
+      return;
+    }
+    try {
+      await confirm(`纭瑙g粦鎵�閫� ${records.length} 鏉¤鐮佸悧锛焋);
+    } catch {
+      return;
+    }
+    await unbindBagCodePallet({
+      palletId: records[0]!.palletId!,
+      ids: records.map((record) => record.id!),
+    });
+    message.success($t("ui.actionMessage.operationSuccess"));
+    handleRefresh();
+  }
+
+  /** 鍒犻櫎琚嬬爜鐢熸垚缁勶紙鏁版嵁鍖咃級 */
+  async function handleDeleteGroup(row: MesProBagCodeApi.BagCode) {
+    await deleteBagCodesByUuid(row.uuid!);
+    message.success($t("ui.actionMessage.deleteSuccess", [row.uuid]));
+    handleRefresh();
+  }
+
+  const [Grid, gridApi] = useVbenVxeGrid({
+    formOptions: {
+      schema: useGridFormSchema(),
+    },
+    gridOptions: {
+      checkboxConfig: {
+        highlight: true,
+        reserve: true,
+      },
+      columns: useGridColumns(),
+      height: "auto",
+      keepSource: true,
+      proxyConfig: {
+        ajax: {
+          query: async ({ page }, formValues) => {
+            return await getBagCodePage({
+              pageNo: page.currentPage,
+              pageSize: page.pageSize,
+              ...formValues,
+            });
+          },
+        },
+      },
+      rowConfig: {
+        keyField: "id",
+        isHover: true,
+      },
+      showOverflow: true,
+      toolbarConfig: {
+        refresh: true,
+        search: true,
+      },
+    } as VxeTableGridOptions<MesProBagCodeApi.BagCode>,
+  });
+</script>
+
+<template>
+  <Page auto-content-height>
+    <GenerateFormModal @success="handleRefresh" />
+    <BindPalletFormModal @success="handleRefresh" />
+    <QrTraceQueryModal />
+    <Grid table-title="琚嬬爜鍒楄〃锛堜竴琚嬩竴鐮侊級">
+      <template #toolbar-tools>
+        <TableAction :actions="[
+            {
+              label: '鐢熸垚鏁版嵁鍖�',
+              type: 'primary',
+              icon: ACTION_ICON.ADD,
+              auth: ['mes:pro-bag-code:create'],
+              onClick: handleGenerate,
+            },
+            {
+              label: '缁戝畾鎵樼洏',
+              type: 'primary',
+              auth: ['mes:pro-bag-code:update'],
+              onClick: handleBindPallet,
+            },
+            {
+              label: '瑙g粦鎵樼洏',
+              auth: ['mes:pro-bag-code:update'],
+              onClick: handleUnbindPallet,
+            },
+            {
+              label: '鎵爜杩芥函',
+              icon: ACTION_ICON.SEARCH,
+              auth: ['mes:pro-bag-code:query'],
+              onClick: handleTrace,
+            },
+            {
+              label: '瀵煎嚭浜岀淮鐮�',
+              icon: ACTION_ICON.DOWNLOAD,
+              auth: ['mes:pro-bag-code:export'],
+              onClick: handleExportQr,
+            },
+            {
+              label: $t('ui.actionTitle.export'),
+              type: 'primary',
+              icon: ACTION_ICON.DOWNLOAD,
+              auth: ['mes:pro-bag-code:export'],
+              onClick: handleExport,
+            },
+          ]" />
+      </template>
+      <template #actions="{ row }">
+        <TableAction :actions="[
+            {
+              label: '鏌ョ湅浜岀淮鐮�',
+              type: 'link',
+              auth: ['mes:pro-bag-code:query'],
+              onClick: handleViewQr.bind(null, row),
+            },
+            {
+              label: $t('common.delete'),
+              type: 'link',
+              danger: true,
+              icon: ACTION_ICON.DELETE,
+              auth: ['mes:pro-bag-code:delete'],
+              ifShow:
+                row.status === MesProBagCodeStatusEnum.GENERATED &&
+                !row.palletId,
+              popConfirm: {
+                title:
+                  '鍒犻櫎灏嗙Щ闄よ琚嬬爜鎵�灞炵殑鏁翠釜鐢熸垚缁勶紙鏁版嵁鍖咃級锛岀‘璁ゅ垹闄ゅ悧锛�',
+                confirm: handleDeleteGroup.bind(null, row),
+              },
+            },
+          ]" />
+      </template>
+    </Grid>
+  </Page>
+</template>
diff --git a/src/views/mes/pro/bagCode/modules/bind-pallet.vue b/src/views/mes/pro/bagCode/modules/bind-pallet.vue
new file mode 100644
index 0000000..a84abba
--- /dev/null
+++ b/src/views/mes/pro/bagCode/modules/bind-pallet.vue
@@ -0,0 +1,121 @@
+<script lang="ts" setup>
+  import type { VbenFormSchema } from "#/adapter/form";
+
+  import { useVbenModal } from "@vben/common-ui";
+  import { MesProPalletStatusEnum } from "@vben/constants";
+
+  import { message } from "ant-design-vue";
+
+  import { useVbenForm } from "#/adapter/form";
+  import { bindBagCodePallet } from "#/api/mes/pro/bagCode";
+  import { getPalletSimpleList } from "#/api/mes/pro/pallet";
+  import { $t } from "#/locales";
+
+  defineOptions({ name: "MesProBagCodeBindPallet" });
+
+  /** 寮圭獥浼犲叆鏁版嵁 */
+  interface ModalData {
+    batchId: number;
+    batchCode?: string;
+    ids: number[];
+  }
+
+  const emit = defineEmits(["success"]);
+
+  /** 鏋勫缓琛ㄥ崟锛堟墭鐩樹笅鎷夐�夐」鐢卞閮ㄥ姞杞芥敞鍏ワ級 */
+  function buildSchema(palletOptions: { label: string; value: number }[]): VbenFormSchema[] {
+    return [
+      {
+        fieldName: "batchCode",
+        label: "鐢熶骇鎵规",
+        component: "Input",
+        componentProps: {
+          disabled: true,
+        },
+      },
+      {
+        fieldName: "bagCount",
+        label: "缁戝畾琚嬫暟",
+        component: "InputNumber",
+        componentProps: {
+          class: "!w-full",
+          disabled: true,
+        },
+      },
+      {
+        fieldName: "palletId",
+        label: "鐩爣鎵樼洏",
+        component: "Select",
+        componentProps: {
+          optionFilterProp: "label",
+          options: palletOptions,
+          placeholder: "璇烽�夋嫨鐩爣鎵樼洏",
+          showSearch: true,
+        },
+        rules: "selectRequired",
+      },
+    ];
+  }
+
+  const [Form, formApi] = useVbenForm({
+    commonConfig: {
+      componentProps: {
+        class: "w-full",
+      },
+      labelWidth: 100,
+    },
+    layout: "horizontal",
+    schema: [],
+    showDefaultActions: false,
+  });
+
+  const [Modal, modalApi] = useVbenModal({
+    async onConfirm() {
+      const { valid } = await formApi.validate();
+      if (!valid) return;
+      const data = modalApi.getData<ModalData>();
+      const values = await formApi.getValues();
+      modalApi.lock();
+      try {
+        await bindBagCodePallet({ palletId: values.palletId, ids: data.ids });
+        message.success($t("ui.actionMessage.operationSuccess"));
+        emit("success");
+        await modalApi.close();
+      } finally {
+        modalApi.unlock();
+      }
+    },
+    async onOpenChange(isOpen: boolean) {
+      if (!isOpen) return;
+      const data = modalApi.getData<ModalData>();
+      modalApi.lock();
+      try {
+        // 鍔犺浇璇ユ壒娆′笅鐨勬墭鐩橈紙鎺掗櫎宸蹭綔搴燂級
+        const pallets = await getPalletSimpleList(data.batchId);
+        const options = pallets
+          .filter(
+            (pallet) => pallet.status !== MesProPalletStatusEnum.CANCELED
+          )
+          .map((pallet) => ({
+            label: `${pallet.code}锛堝凡瑁� ${pallet.bagCount ?? 0} 琚� / 鍙 ${
+              pallet.capacity ?? "涓嶉檺"
+            } 琚嬶級`,
+            value: pallet.id!,
+          }));
+        await formApi.setState({ schema: buildSchema(options) });
+        await formApi.setValues({
+          bagCount: data.ids.length,
+          batchCode: data.batchCode,
+        });
+      } finally {
+        modalApi.unlock();
+      }
+    },
+  });
+</script>
+
+<template>
+  <Modal title="琚嬬爜缁戝畾鎵樼洏" class="w-2/5">
+    <Form class="mx-4" />
+  </Modal>
+</template>
diff --git a/src/views/mes/pro/bagCode/modules/generate-form.vue b/src/views/mes/pro/bagCode/modules/generate-form.vue
new file mode 100644
index 0000000..6ea36bc
--- /dev/null
+++ b/src/views/mes/pro/bagCode/modules/generate-form.vue
@@ -0,0 +1,111 @@
+<script lang="ts" setup>
+  import type { VbenFormSchema } from "#/adapter/form";
+  import type { MesProBagCodeApi } from "#/api/mes/pro/bagCode";
+
+  import { useVbenModal } from "@vben/common-ui";
+  import { MesProBatchStatusEnum } from "@vben/constants";
+
+  import { message } from "ant-design-vue";
+
+  import { useVbenForm } from "#/adapter/form";
+  import { generateBagCodes } from "#/api/mes/pro/bagCode";
+  import { getBatchSimpleList } from "#/api/mes/pro/batch";
+
+  defineOptions({ name: "MesProBagCodeGenerateForm" });
+
+  const emit = defineEmits(["success"]);
+
+  /** 鏋勫缓琛ㄥ崟锛堟壒娆′笅鎷夐�夐」鐢卞閮ㄥ姞杞芥敞鍏ワ級 */
+  function buildSchema(batchOptions: { label: string; value: number }[]): VbenFormSchema[] {
+    return [
+      {
+        fieldName: "batchId",
+        label: "鐢熶骇鎵规",
+        component: "Select",
+        componentProps: {
+          options: batchOptions,
+          optionFilterProp: "label",
+          placeholder: "璇烽�夋嫨鐢熶骇涓殑鎵规",
+          showSearch: true,
+        },
+        rules: "selectRequired",
+      },
+      {
+        fieldName: "count",
+        label: "鐢熸垚琚嬫暟",
+        component: "InputNumber",
+        componentProps: {
+          class: "!w-full",
+          min: 1,
+          placeholder: "璇疯緭鍏ラ渶瑕佺敓鎴愮殑琚嬬爜鏁伴噺",
+          precision: 0,
+        },
+        rules: "required",
+      },
+      {
+        fieldName: "remark",
+        label: "澶囨敞",
+        component: "Textarea",
+        componentProps: {
+          placeholder: "璇疯緭鍏ュ娉�",
+          rows: 2,
+        },
+      },
+    ];
+  }
+
+  const [Form, formApi] = useVbenForm({
+    commonConfig: {
+      componentProps: {
+        class: "w-full",
+      },
+      labelWidth: 100,
+    },
+    layout: "horizontal",
+    schema: [],
+    showDefaultActions: false,
+  });
+
+  const [Modal, modalApi] = useVbenModal({
+    async onConfirm() {
+      const { valid } = await formApi.validate();
+      if (!valid) return;
+      modalApi.lock();
+      try {
+        const values =
+          (await formApi.getValues()) as MesProBagCodeApi.GenerateParams;
+        await generateBagCodes(values);
+        message.success(`宸叉垚鍔熺敓鎴� ${values.count} 鏉¤鐮乣);
+        emit("success");
+        await modalApi.close();
+      } finally {
+        modalApi.unlock();
+      }
+    },
+    async onOpenChange(isOpen: boolean) {
+      if (!isOpen) return;
+      modalApi.lock();
+      try {
+        // 浠呯敓浜т腑鐨勬壒娆″厑璁哥敓鎴愯鐮�
+        const batches = await getBatchSimpleList();
+        const options = batches
+          .filter(
+            (batch) => batch.status === MesProBatchStatusEnum.PRODUCING
+          )
+          .map((batch) => ({
+            label: batch.code!,
+            value: batch.id!,
+          }));
+        await formApi.setState({ schema: buildSchema(options) });
+      } finally {
+        modalApi.unlock();
+      }
+    },
+  });
+</script>
+
+<template>
+  <Modal title="鐢熸垚琚嬬爜鏁版嵁鍖�" class="w-2/5">
+    <Form class="mx-4" />
+  </Modal>
+</template>
diff --git a/src/views/mes/pro/batch/data.ts b/src/views/mes/pro/batch/data.ts
new file mode 100644
index 0000000..f226782
--- /dev/null
+++ b/src/views/mes/pro/batch/data.ts
@@ -0,0 +1,336 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesProBatchApi } from '#/api/mes/pro/batch';
+
+import { markRaw } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { MdItemSelect } from '#/views/mes/md/item/components';
+import { ProLineSelect } from '#/views/mes/pro/line/components';
+import { UserSelect } from '#/views/system/user/components';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'detail' | 'update';
+
+/** 鏂板/淇敼鐨勮〃鍗� */
+export function useFormSchema(formType: FormType): VbenFormSchema[] {
+  const disabled = formType === 'detail';
+  return [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: {
+        triggerFields: [''],
+        show: () => false,
+      },
+    },
+    {
+      fieldName: 'code',
+      label: '鎵规鍙�',
+      component: 'Input',
+      componentProps: {
+        disabled: true,
+        placeholder: '淇濆瓨鍚庤嚜鍔ㄧ敓鎴�',
+      },
+      dependencies: {
+        triggerFields: [''],
+        show: () => formType !== 'create',
+      },
+    },
+    {
+      fieldName: 'status',
+      label: '鎵规鐘舵��',
+      component: 'Select',
+      componentProps: {
+        disabled: true,
+        options: getDictOptions(DICT_TYPE.MES_PRO_BATCH_STATUS, 'number'),
+      },
+      dependencies: {
+        triggerFields: [''],
+        show: () => formType !== 'create',
+      },
+    },
+    {
+      fieldName: 'itemId',
+      label: '鍝佺',
+      component: markRaw(MdItemSelect),
+      componentProps: {
+        disabled,
+        placeholder: '璇烽�夋嫨鍝佺锛堜骇鍝佸搧绉嶏級',
+      },
+      rules: 'selectRequired',
+    },
+    {
+      fieldName: 'lineId',
+      label: '浜х嚎',
+      component: markRaw(ProLineSelect),
+      componentProps: {
+        disabled,
+        placeholder: '璇烽�夋嫨浜х嚎',
+      },
+      rules: 'selectRequired',
+    },
+    {
+      fieldName: 'produceDate',
+      label: '鐢熶骇鏃ユ湡',
+      component: 'DatePicker',
+      componentProps: {
+        class: '!w-full',
+        disabled,
+        format: 'YYYY-MM-DD HH:mm:ss',
+        placeholder: '璇烽�夋嫨鐢熶骇鏃ユ湡',
+        showTime: true,
+        valueFormat: 'YYYY-MM-DD HH:mm:ss',
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'planQuantity',
+      label: '璁″垝鏁伴噺',
+      component: 'InputNumber',
+      componentProps: {
+        class: '!w-full',
+        disabled,
+        min: 0,
+        placeholder: '璇疯緭鍏ヨ鍒掓暟閲�',
+        precision: 2,
+      },
+    },
+    {
+      fieldName: 'ownerUserId',
+      label: '璐d换浜�',
+      component: markRaw(UserSelect),
+      componentProps: {
+        disabled,
+        placeholder: '璇烽�夋嫨璐d换浜�',
+      },
+      rules: 'selectRequired',
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      formItemClass: 'col-span-3',
+      componentProps: {
+        disabled,
+        placeholder: '璇疯緭鍏ュ娉�',
+        rows: 2,
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'code',
+      label: '鎵规鍙�',
+      component: 'Input',
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇疯緭鍏ユ壒娆″彿',
+      },
+    },
+    {
+      fieldName: 'itemId',
+      label: '鍝佺',
+      component: markRaw(MdItemSelect),
+      componentProps: {
+        placeholder: '璇烽�夋嫨鍝佺',
+      },
+    },
+    {
+      fieldName: 'lineCode',
+      label: '浜х嚎缂栫爜',
+      component: 'Input',
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇疯緭鍏ヤ骇绾跨紪鐮�',
+      },
+    },
+    {
+      fieldName: 'lineName',
+      label: '浜х嚎鍚嶇О',
+      component: 'Input',
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇疯緭鍏ヤ骇绾垮悕绉�',
+      },
+    },
+    {
+      fieldName: 'ownerUserId',
+      label: '璐d换浜�',
+      component: markRaw(UserSelect),
+      componentProps: {
+        placeholder: '璇烽�夋嫨璐d换浜�',
+      },
+    },
+    {
+      fieldName: 'status',
+      label: '鎵规鐘舵��',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: getDictOptions(DICT_TYPE.MES_PRO_BATCH_STATUS, 'number'),
+        placeholder: '璇烽�夋嫨鎵规鐘舵��',
+      },
+    },
+    {
+      fieldName: 'produceDate',
+      label: '鐢熶骇鏃ユ湡',
+      component: 'RangePicker',
+      componentProps: {
+        ...getRangePickerDefaultProps(),
+        allowClear: true,
+      },
+    },
+    {
+      fieldName: 'createTime',
+      label: '鍒涘缓鏃堕棿',
+      component: 'RangePicker',
+      componentProps: {
+        ...getRangePickerDefaultProps(),
+        allowClear: true,
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions<MesProBatchApi.Batch>['columns'] {
+  return [
+    {
+      field: 'code',
+      title: '鎵规鍙�',
+      fixed: 'left',
+      minWidth: 200,
+      slots: { default: 'code' },
+    },
+    {
+      field: 'itemCode',
+      title: '鍝佺缂栫爜',
+      width: 120,
+    },
+    {
+      field: 'itemName',
+      title: '鍝佺鍚嶇О',
+      width: 140,
+    },
+    {
+      field: 'lineCode',
+      title: '浜х嚎缂栫爜',
+      width: 100,
+    },
+    {
+      field: 'lineName',
+      title: '浜х嚎鍚嶇О',
+      width: 120,
+    },
+    {
+      field: 'produceDate',
+      title: '鐢熶骇鏃ユ湡',
+      width: 170,
+      formatter: 'formatDateTime',
+    },
+    {
+      field: 'planQuantity',
+      title: '璁″垝鏁伴噺',
+      width: 100,
+    },
+    {
+      field: 'ownerUserName',
+      title: '璐d换浜�',
+      width: 100,
+    },
+    {
+      field: 'bagQuantity',
+      title: '琚嬬爜鏁伴噺',
+      width: 90,
+    },
+    {
+      field: 'palletQuantity',
+      title: '鎵樼洏鐮佹暟閲�',
+      width: 100,
+    },
+    {
+      field: 'status',
+      title: '鎵规鐘舵��',
+      width: 100,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.MES_PRO_BATCH_STATUS },
+      },
+    },
+    {
+      field: 'createTime',
+      title: '鍒涘缓鏃堕棿',
+      width: 170,
+      formatter: 'formatDateTime',
+    },
+    {
+      title: '鎿嶄綔',
+      width: 200,
+      fixed: 'right',
+      slots: { default: 'actions' },
+    },
+  ];
+}
+
+/** 鐢熶骇浜哄憳閰嶇疆瀛愯〃鐨勫瓧娈碉紙鍙紪杈戞�侊級 */
+export function useWorkerEditGridColumns(): VxeTableGridOptions<MesProBatchApi.BatchWorker>['columns'] {
+  return [
+    {
+      field: 'userId',
+      title: '鐢熶骇浜哄憳',
+      minWidth: 200,
+      slots: { default: 'userId' },
+    },
+    {
+      field: 'workerType',
+      title: '宀椾綅绫诲瀷',
+      width: 160,
+      slots: { default: 'workerType' },
+    },
+    {
+      field: 'remark',
+      title: '澶囨敞',
+      minWidth: 160,
+      slots: { default: 'remark' },
+    },
+    {
+      title: '鎿嶄綔',
+      width: 80,
+      fixed: 'right',
+      slots: { default: 'actions' },
+    },
+  ];
+}
+
+/** 鐢熶骇浜哄憳閰嶇疆瀛愯〃鐨勫瓧娈碉紙鍙鎬侊級 */
+export function useWorkerReadGridColumns(): VxeTableGridOptions<MesProBatchApi.BatchWorker>['columns'] {
+  return [
+    {
+      field: 'userName',
+      title: '浜哄憳濮撳悕',
+      minWidth: 160,
+    },
+    {
+      field: 'workerType',
+      title: '宀椾綅绫诲瀷',
+      width: 160,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.MES_PRO_BATCH_WORKER_TYPE },
+      },
+    },
+    {
+      field: 'remark',
+      title: '澶囨敞',
+      minWidth: 160,
+    },
+  ];
+}
diff --git a/src/views/mes/pro/batch/index.vue b/src/views/mes/pro/batch/index.vue
new file mode 100644
index 0000000..73dfe65
--- /dev/null
+++ b/src/views/mes/pro/batch/index.vue
@@ -0,0 +1,238 @@
+<script lang="ts" setup>
+  import type { VxeTableGridOptions } from "#/adapter/vxe-table";
+  import type { MesProBatchApi } from "#/api/mes/pro/batch";
+
+  import { Page, useVbenModal } from "@vben/common-ui";
+  import { MesProBatchStatusEnum } from "@vben/constants";
+  import { downloadFileFromBlobPart } from "@vben/utils";
+
+  import { Button, message } from "ant-design-vue";
+
+  import { ACTION_ICON, TableAction, useVbenVxeGrid } from "#/adapter/vxe-table";
+  import {
+    deleteBatch,
+    exportBatchExcel,
+    exportBatchQr,
+    getBatchPage,
+    updateBatchStatus,
+  } from "#/api/mes/pro/batch";
+  import { $t } from "#/locales";
+
+  import { useGridColumns, useGridFormSchema } from "./data";
+  import Form from "./modules/form.vue";
+  import QrTraceModal from "../components/qr-trace-modal.vue";
+
+  const [FormModal, formModalApi] = useVbenModal({
+    connectedComponent: Form,
+    destroyOnClose: true,
+  });
+
+  const [QrTraceQueryModal, qrTraceQueryModalApi] = useVbenModal({
+    connectedComponent: QrTraceModal,
+    destroyOnClose: true,
+  });
+
+  /** 鍒锋柊琛ㄦ牸 */
+  function handleRefresh() {
+    gridApi.query();
+  }
+
+  /** 鎵爜杩芥函 */
+  function handleTrace() {
+    qrTraceQueryModalApi.open();
+  }
+
+  /** 鏌ョ湅浜岀淮鐮侊紙琛屽唴棰勫~鐮佸�硷級 */
+  function handleViewQr(row: MesProBatchApi.Batch) {
+    qrTraceQueryModalApi.setData({ code: row.code }).open();
+  }
+
+  /** 鎵撳嵃鏁村浜岀淮鐮侊紙鎵规鍥� + 鍏ㄩ儴鎵樼洏鍥� ZIP锛� */
+  async function handlePrintQrSet(row: MesProBatchApi.Batch) {
+    const data = await exportBatchQr(row.id!);
+    downloadFileFromBlobPart({
+      fileName: `鎵规浜岀淮鐮�-${row.code}.zip`,
+      source: data,
+    });
+  }
+
+  /** 鏂板鎵规 */
+  function handleCreate() {
+    formModalApi.setData({ formType: "create" }).open();
+  }
+
+  /** 鏌ョ湅璇︽儏 */
+  function handleDetail(row: MesProBatchApi.Batch) {
+    formModalApi.setData({ formType: "detail", id: row.id }).open();
+  }
+
+  /** 缂栬緫鎵规 */
+  function handleEdit(row: MesProBatchApi.Batch) {
+    formModalApi.setData({ formType: "update", id: row.id }).open();
+  }
+
+  /** 鍒犻櫎鎵规 */
+  async function handleDelete(row: MesProBatchApi.Batch) {
+    await deleteBatch(row.id!);
+    message.success($t("ui.actionMessage.deleteSuccess", [row.code]));
+    handleRefresh();
+  }
+
+  /** 鏇存柊鎵规鐘舵�� */
+  async function handleUpdateStatus(
+    row: MesProBatchApi.Batch,
+    status: number,
+    statusName: string
+  ) {
+    await updateBatchStatus(row.id!, status);
+    message.success(`鎵规宸�${statusName}`);
+    handleRefresh();
+  }
+
+  /** 瀵煎嚭琛ㄦ牸 */
+  async function handleExport() {
+    const data = await exportBatchExcel(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) => {
+            return await getBatchPage({
+              pageNo: page.currentPage,
+              pageSize: page.pageSize,
+              ...formValues,
+            });
+          },
+        },
+      },
+      rowConfig: {
+        keyField: "id",
+        isHover: true,
+      },
+      toolbarConfig: {
+        refresh: true,
+        search: true,
+      },
+    } as VxeTableGridOptions<MesProBatchApi.Batch>,
+  });
+</script>
+
+<template>
+  <Page auto-content-height>
+    <FormModal @success="handleRefresh" />
+    <QrTraceQueryModal />
+    <Grid table-title="鐢熶骇鎵规鍒楄〃">
+      <template #toolbar-tools>
+        <TableAction :actions="[
+            {
+              label: $t('ui.actionTitle.create', ['鎵规']),
+              type: 'primary',
+              icon: ACTION_ICON.ADD,
+              auth: ['mes:pro-batch:create'],
+              onClick: handleCreate,
+            },
+            {
+              label: '鎵爜杩芥函',
+              icon: ACTION_ICON.SEARCH,
+              auth: ['mes:pro-batch:query'],
+              onClick: handleTrace,
+            },
+            {
+              label: $t('ui.actionTitle.export'),
+              type: 'primary',
+              icon: ACTION_ICON.DOWNLOAD,
+              auth: ['mes:pro-batch:export'],
+              onClick: handleExport,
+            },
+          ]" />
+      </template>
+      <template #code="{ row }">
+        <Button type="link" @click="handleDetail(row)">
+          {{ row.code }}
+        </Button>
+      </template>
+      <template #actions="{ row }">
+        <TableAction :actions="[
+            {
+              label: '鏌ョ湅浜岀淮鐮�',
+              type: 'link',
+              auth: ['mes:pro-batch:query'],
+              onClick: handleViewQr.bind(null, row),
+            },
+            {
+              label: $t('common.edit'),
+              type: 'link',
+              icon: ACTION_ICON.EDIT,
+              auth: ['mes:pro-batch:update'],
+              ifShow:
+                row.status === MesProBatchStatusEnum.DRAFT ||
+                row.status === MesProBatchStatusEnum.PRODUCING,
+              onClick: handleEdit.bind(null, row),
+            },
+            {
+              label: $t('common.delete'),
+              type: 'link',
+              danger: true,
+              icon: ACTION_ICON.DELETE,
+              auth: ['mes:pro-batch:delete'],
+              ifShow:
+                row.status === MesProBatchStatusEnum.DRAFT ||
+                row.status === MesProBatchStatusEnum.CANCELED,
+              popConfirm: {
+                title: $t('ui.actionMessage.deleteConfirm', [row.code]),
+                confirm: handleDelete.bind(null, row),
+              },
+            },
+          ]"
+          :drop-down-actions="[
+            {
+              label: '寮�濮嬬敓浜�',
+              auth: ['mes:pro-batch:update'],
+              ifShow: row.status === MesProBatchStatusEnum.DRAFT,
+              popConfirm: {
+                title: '纭寮�濮嬬敓浜ц鎵规鍚楋紵',
+                confirm: () =>
+                  handleUpdateStatus(row, MesProBatchStatusEnum.PRODUCING, '寮�濮嬬敓浜�'),
+              },
+            },
+            {
+              label: '瀹屾垚',
+              auth: ['mes:pro-batch:update'],
+              ifShow: row.status === MesProBatchStatusEnum.PRODUCING,
+              popConfirm: {
+                title: '纭瀹屾垚璇ユ壒娆″悧锛�',
+                confirm: () =>
+                  handleUpdateStatus(row, MesProBatchStatusEnum.FINISHED, '瀹屾垚'),
+              },
+            },
+            {
+              label: '浣滃簾',
+              auth: ['mes:pro-batch:update'],
+              ifShow:
+                row.status === MesProBatchStatusEnum.DRAFT ||
+                row.status === MesProBatchStatusEnum.PRODUCING,
+              popConfirm: {
+                title: '纭浣滃簾璇ユ壒娆″悧锛熶綔搴熷悗涓嶅彲鎭㈠銆�',
+                confirm: () =>
+                  handleUpdateStatus(row, MesProBatchStatusEnum.CANCELED, '浣滃簾'),
+              },
+            },
+            {
+              label: '鎵撳嵃鏁村浜岀淮鐮�',
+              auth: ['mes:pro-batch:export'],
+              onClick: () => handlePrintQrSet(row),
+            },
+          ]" />
+      </template>
+    </Grid>
+  </Page>
+</template>
diff --git a/src/views/mes/pro/batch/modules/form.vue b/src/views/mes/pro/batch/modules/form.vue
new file mode 100644
index 0000000..2e45243
--- /dev/null
+++ b/src/views/mes/pro/batch/modules/form.vue
@@ -0,0 +1,281 @@
+<script lang="ts" setup>
+  import type { FormType } from "../data";
+
+  import type { VxeTableGridOptions } from "#/adapter/vxe-table";
+  import type { MesProBatchApi } from "#/api/mes/pro/batch";
+  import type { SystemUserApi } from "#/api/system/user";
+
+  import { computed, nextTick, ref } from "vue";
+
+  import { useVbenModal } from "@vben/common-ui";
+  import { DICT_TYPE } from "@vben/constants";
+  import { getDictOptions } from "@vben/hooks";
+
+  import { Button, Input, message, Select } from "ant-design-vue";
+
+  import { useVbenForm } from "#/adapter/form";
+  import { TableAction, useVbenVxeGrid } from "#/adapter/vxe-table";
+  import { createBatch, getBatch, updateBatch } from "#/api/mes/pro/batch";
+  import { $t } from "#/locales";
+  import { UserSelect } from "#/views/system/user/components";
+
+  import {
+    useFormSchema,
+    useWorkerEditGridColumns,
+    useWorkerReadGridColumns,
+  } from "../data";
+
+  defineOptions({ name: "MesProBatchForm" });
+
+  const emit = defineEmits(["success"]);
+
+  const formType = ref<FormType>("create");
+  const formData = ref<MesProBatchApi.Batch>();
+
+  const isEditable = computed(() =>
+    ["create", "update"].includes(formType.value)
+  );
+
+  const getTitle = computed(() => {
+    switch (formType.value) {
+      case "detail": {
+        return $t("ui.actionTitle.view", ["鐢熶骇鎵规"]);
+      }
+      case "update": {
+        return $t("ui.actionTitle.edit", ["鐢熶骇鎵规"]);
+      }
+      default: {
+        return $t("ui.actionTitle.create", ["鐢熶骇鎵规"]);
+      }
+    }
+  });
+
+  const [Form, formApi] = useVbenForm({
+    commonConfig: {
+      componentProps: {
+        class: "w-full",
+      },
+      formItemClass: "col-span-1",
+      labelWidth: 110,
+    },
+    layout: "horizontal",
+    schema: [],
+    showDefaultActions: false,
+    wrapperClass: "grid-cols-3",
+  });
+
+  // ========== 鐢熶骇浜哄憳閰嶇疆瀛愯〃 ==========
+  const [WorkerGrid, workerGridApi] = useVbenVxeGrid({
+    gridOptions: {
+      autoResize: true,
+      border: true,
+      columns: useWorkerEditGridColumns(),
+      minHeight: 160,
+      pagerConfig: { enabled: false },
+      rowConfig: { isHover: true, keyField: "_rowKey" },
+      showOverflow: true,
+      toolbarConfig: { enabled: false },
+    } as VxeTableGridOptions<MesProBatchApi.BatchWorker>,
+  });
+
+  /** 鏂板琛岀殑涓存椂琛屽彿锛堣礋鏁拌嚜澧烇紝閬垮厤涓� loadData 鐨� index 鍐茬獊锛� */
+  let workerRowSeq = 0;
+
+  /** 娣诲姞浜哄憳琛� */
+  function handleAddWorker() {
+    workerGridApi.grid.insertAt({ _rowKey: -(++workerRowSeq) }, -1);
+  }
+
+  /** 鍒犻櫎浜哄憳琛� */
+  function handleDeleteWorker(row: MesProBatchApi.BatchWorker) {
+    workerGridApi.grid.remove(row);
+  }
+
+  /** 浜哄憳閫夋嫨缂栧彿鍙樺寲锛堝崟閫夊満鏅笅鍙栫涓�涓�硷級 */
+  function handleWorkerUserIdChange(
+    value: number | number[] | undefined,
+    row: MesProBatchApi.BatchWorker
+  ) {
+    row.userId = Array.isArray(value) ? value[0] : value;
+  }
+
+  /** 浜哄憳閫夋嫨鍙樺寲鏃惰褰曞鍚嶏紙渚夸簬鏍¢獙鎻愮ず锛� */
+  function handleWorkerUserChange(
+    user: SystemUserApi.User | SystemUserApi.User[] | undefined,
+    row: MesProBatchApi.BatchWorker
+  ) {
+    row.userName = Array.isArray(user) ? user[0]?.nickname : user?.nickname;
+  }
+
+  /** 宀椾綅绫诲瀷瀛楀吀閫夐」 */
+  const workerTypeOptions = computed(() =>
+    getDictOptions(DICT_TYPE.MES_PRO_BATCH_WORKER_TYPE, "number").map(
+      (item) => ({ label: item.label, value: item.value as number })
+    )
+  );
+
+  /** 鏍¢獙骞惰幏鍙栦汉鍛橀厤缃垪琛� */
+  async function getValidWorkers(): Promise<
+    MesProBatchApi.BatchWorker[] | undefined
+  > {
+    // getData() 鍙繑鍥� loadData 鍔犺浇鐨勬暟鎹紱insertAt 鎻掑叆鐨勮闇�閫氳繃
+    // getInsertRecords() 鑾峰彇锛屽苟鎺掗櫎 getRemoveRecords() 涓凡鍒犻櫎鐨勮
+    const data = workerGridApi.grid.getData() as Array<
+      MesProBatchApi.BatchWorker & { _rowKey?: number }
+    >;
+    const removeRecords = workerGridApi.grid.getRemoveRecords() as Array<
+      MesProBatchApi.BatchWorker & { _rowKey?: number }
+    >;
+    const insertRecords = workerGridApi.grid.getInsertRecords() as Array<
+      MesProBatchApi.BatchWorker & { _rowKey?: number }
+    >;
+    const rows = [
+      ...data.filter(
+        (row) => !removeRecords.some((removed) => removed._rowKey === row._rowKey)
+      ),
+      ...insertRecords,
+    ].map(({ _rowKey, ...rest }) => rest);
+    if (rows.length === 0) {
+      message.warning("璇疯嚦灏戦厤缃竴鍚嶇敓浜т汉鍛�");
+      return undefined;
+    }
+    for (const [index, row] of rows.entries()) {
+      if (!row.userId) {
+        message.warning(`璇烽�夋嫨绗� ${index + 1} 琛岀殑鐢熶骇浜哄憳`);
+        return undefined;
+      }
+      if (!row.workerType) {
+        message.warning(`璇烽�夋嫨绗� ${index + 1} 琛岀殑宀椾綅绫诲瀷`);
+        return undefined;
+      }
+    }
+    return rows;
+  }
+
+  /** 绛夊緟浜哄憳瀛愯〃 grid 鎸傝浇瀹屾垚锛堝脊绐楁墦寮�鐬棿 onOpenChange 鍐呴儴琛ㄦ牸鍙兘灏氭湭鎸傝浇锛� */
+  async function waitWorkerGridReady(): Promise<void> {
+    await nextTick();
+    for (let i = 0; i < 100; i++) {
+      if (typeof workerGridApi.grid?.loadData === "function") {
+        return;
+      }
+      await new Promise((resolve) => setTimeout(resolve, 30));
+    }
+  }
+
+  const [Modal, modalApi] = useVbenModal({
+    showCancelButton: false, // 鑷畾涔夊彇娑�/鍏抽棴鎸夐挳锛岀鐢ㄩ粯璁ゅ彇娑堟寜閽伩鍏嶉噸澶�
+    async onConfirm() {
+      if (!isEditable.value) {
+        await modalApi.close();
+        return;
+      }
+      const { valid } = await formApi.validate();
+      if (!valid) return;
+      const workers = await getValidWorkers();
+      if (!workers) return;
+      modalApi.lock();
+      try {
+        const data = (await formApi.getValues()) as MesProBatchApi.Batch;
+        if (formData.value?.id) {
+          await updateBatch({ ...data, id: formData.value.id, workers });
+        } else {
+          await createBatch({ ...data, workers });
+        }
+        message.success($t("ui.actionMessage.operationSuccess"));
+        emit("success");
+        await modalApi.close();
+      } 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(formType.value) });
+      // 鏍规嵁琛ㄥ崟鎬佸垏鎹汉鍛樺瓙琛ㄥ垪
+      workerGridApi.setGridOptions({
+        columns: isEditable.value
+          ? useWorkerEditGridColumns()
+          : useWorkerReadGridColumns(),
+      });
+      if (data?.id) {
+        modalApi.lock();
+        try {
+          formData.value = await getBatch(data.id);
+          await formApi.setValues(formData.value);
+          const workers = (formData.value.workers ?? []).map(
+            (worker, index) => ({ ...worker, _rowKey: worker.id ?? index })
+          );
+          await waitWorkerGridReady();
+          await workerGridApi.grid.loadData(workers);
+        } finally {
+          modalApi.unlock();
+        }
+      } else {
+        await waitWorkerGridReady();
+        await workerGridApi.grid.loadData([]);
+      }
+    },
+  });
+</script>
+
+<template>
+  <Modal :title="getTitle"
+         class="w-3/5"
+         content-class="!max-h-[80vh] overflow-y-auto">
+    <Form class="mx-4" />
+    <div class="mx-4 mb-4">
+      <div class="mb-2 flex items-center justify-between">
+        <span class="font-medium">鐢熶骇浜哄憳閰嶇疆</span>
+        <Button v-if="isEditable" size="small" type="primary" @click="handleAddWorker">
+          娣诲姞浜哄憳
+        </Button>
+      </div>
+      <WorkerGrid>
+        <template #userId="{ row }">
+          <UserSelect
+            :model-value="row.userId"
+            placeholder="璇烽�夋嫨鐢熶骇浜哄憳"
+            @update:model-value="handleWorkerUserIdChange($event, row)"
+            @change="handleWorkerUserChange($event, row)"
+          />
+        </template>
+        <template #workerType="{ row }">
+          <Select
+            v-model:value="row.workerType"
+            :options="workerTypeOptions"
+            class="w-full"
+            placeholder="璇烽�夋嫨宀椾綅绫诲瀷"
+          />
+        </template>
+        <template #remark="{ row }">
+          <Input v-model:value="row.remark" placeholder="璇疯緭鍏ュ娉�" />
+        </template>
+        <template #actions="{ row }">
+          <TableAction
+            :actions="[
+              {
+                label: $t('common.delete'),
+                type: 'link',
+                danger: true,
+                onClick: handleDeleteWorker.bind(null, row),
+              },
+            ]"
+          />
+        </template>
+      </WorkerGrid>
+    </div>
+    <template #append-footer>
+      <div class="flex gap-2">
+        <Button @click="modalApi.close()">
+          {{ isEditable ? '鍙栨秷' : '鍏抽棴' }}
+        </Button>
+      </div>
+    </template>
+  </Modal>
+</template>
diff --git a/src/views/mes/pro/components/qr-trace-modal.vue b/src/views/mes/pro/components/qr-trace-modal.vue
new file mode 100644
index 0000000..44813d4
--- /dev/null
+++ b/src/views/mes/pro/components/qr-trace-modal.vue
@@ -0,0 +1,569 @@
+<script lang="ts" setup>
+  import type { MesProBagCodeApi } from "#/api/mes/pro/bagCode";
+  import type { MesProBatchApi } from "#/api/mes/pro/batch";
+  import type { MesProPalletApi } from "#/api/mes/pro/pallet";
+
+  import { ref } from "vue";
+
+  import { Barcode, useVbenModal } from "@vben/common-ui";
+  import { DICT_TYPE } from "@vben/constants";
+  import { getDictLabel } from "@vben/hooks";
+
+  import { Button, Empty, Input, message, Spin, Tag } from "ant-design-vue";
+
+  import { getBagCodeByCode } from "#/api/mes/pro/bagCode";
+  import { getBatchByCode } from "#/api/mes/pro/batch";
+  import { getPalletByCode } from "#/api/mes/pro/pallet";
+  import { useDescription } from "#/components/description";
+
+  defineOptions({ name: "MesProQrTraceModal" });
+
+  type TraceType = "batch" | "pallet" | "bag";
+
+  const MAX_CODE_LENGTH = 64;
+
+  const codeInput = ref<string>();
+  const currentCode = ref<string>();
+  const traceType = ref<TraceType>();
+  const loading = ref(false);
+  const searched = ref(false);
+  const searchError = ref(false);
+  const bagTrace = ref<MesProBagCodeApi.BagCodeTrace>();
+  const palletTrace = ref<MesProPalletApi.PalletTrace>();
+  const batchTrace = ref<MesProBatchApi.BatchTrace>();
+  const barcodeRef = ref<InstanceType<typeof Barcode>>();
+
+  const traceTypeLabel = ref("");
+
+  /** 璇嗗埆浜岀淮鐮佸唴瀹圭被鍨嬶細鎵规鍙� / 鎵樼洏鐮� / 琚嬬爜 */
+  function resolveCodeType(raw: string): TraceType | undefined {
+    const value = raw.trim().toUpperCase();
+    if (/^.*-P\d{2}$/.test(value)) {
+      return "pallet";
+    }
+    if (/^.*-\d{4}$/.test(value)) {
+      return "bag";
+    }
+    return "batch";
+  }
+
+  function resetState() {
+    currentCode.value = undefined;
+    traceType.value = undefined;
+    traceTypeLabel.value = "";
+    bagTrace.value = undefined;
+    palletTrace.value = undefined;
+    batchTrace.value = undefined;
+    searched.value = false;
+    searchError.value = false;
+  }
+
+  /** 鎵爜/杈撳叆浜岀淮鐮佸唴瀹规煡璇㈣拷婧俊鎭� */
+  async function handleSearch(raw?: string) {
+    const value = (raw ?? codeInput.value)?.trim().toUpperCase();
+    if (!value) {
+      message.warning("璇锋壂鎻忔垨杈撳叆浜岀淮鐮佸唴瀹�");
+      return;
+    }
+    if (value.length > MAX_CODE_LENGTH) {
+      message.warning("浜岀淮鐮佸唴瀹硅繃闀匡紝璇锋鏌ユ槸鍚︽壂鎻忔纭�");
+      return;
+    }
+    const type = resolveCodeType(value);
+    if (!type) {
+      message.warning("鏃犳硶璇嗗埆浜岀淮鐮佺被鍨�");
+      return;
+    }
+    loading.value = true;
+    searched.value = true;
+    searchError.value = false;
+    currentCode.value = value;
+    traceType.value = undefined;
+    traceTypeLabel.value = "";
+    bagTrace.value = undefined;
+    palletTrace.value = undefined;
+    batchTrace.value = undefined;
+    try {
+      if (type === "batch") {
+        batchTrace.value = await getBatchByCode(value);
+        traceTypeLabel.value = "鎵规鍙�";
+      } else if (type === "pallet") {
+        palletTrace.value = await getPalletByCode(value);
+        traceTypeLabel.value = "鎵樼洏鐮�";
+      } else {
+        bagTrace.value = await getBagCodeByCode(value);
+        traceTypeLabel.value = "琚嬬爜";
+      }
+      traceType.value = type;
+    } catch {
+      searchError.value = true;
+      currentCode.value = undefined;
+      traceType.value = undefined;
+    } finally {
+      loading.value = false;
+    }
+  }
+
+  /** 褰撳墠杩芥函鏁版嵁鐨勫叧閿俊鎭紙涓嬭浇/鎵撳嵃鍥剧墖鍙犲姞鏂囧瓧鐢級 */
+  function buildLabelText(): { label: string; value: string }[] {
+    if (traceType.value === "bag" && bagTrace.value) {
+      const d = bagTrace.value;
+      return [
+        { label: "琚嬬爜", value: d.code || "" },
+        { label: "琚嬪簭鍙�", value: d.bagNo ? String(d.bagNo) : "" },
+        { label: "鎵规鍙�", value: d.batchCode || "" },
+        { label: "鍝佺", value: d.itemName || "" },
+        { label: "瑙勬牸", value: d.itemSpecification || "" },
+        {
+          label: "浜х嚎",
+          value: [d.lineCode, d.lineName].filter(Boolean).join(" / "),
+        },
+        { label: "鐢熶骇鏃ユ湡", value: d.produceDate || "" },
+        { label: "璐d换浜�", value: d.ownerUserName || "" },
+      ];
+    }
+    if (traceType.value === "pallet" && palletTrace.value) {
+      const d = palletTrace.value;
+      return [
+        { label: "鎵樼洏鐮�", value: d.code || "" },
+        { label: "鎵樼洏搴忓彿", value: d.palletNo ? String(d.palletNo) : "" },
+        { label: "鎵规鍙�", value: d.batchCode || "" },
+        { label: "鍝佺", value: d.itemName || "" },
+        { label: "瑙勬牸", value: d.itemSpecification || "" },
+        {
+          label: "浜х嚎",
+          value: [d.lineCode, d.lineName].filter(Boolean).join(" / "),
+        },
+        { label: "鐢熶骇鏃ユ湡", value: d.produceDate || "" },
+        { label: "璐d换浜�", value: d.ownerUserName || "" },
+      ];
+    }
+    if (batchTrace.value) {
+      const d = batchTrace.value;
+      return [
+        { label: "鎵规鍙�", value: d.code || "" },
+        { label: "鍝佺", value: d.itemName || "" },
+        { label: "瑙勬牸", value: d.itemSpecification || "" },
+        {
+          label: "浜х嚎",
+          value: [d.lineCode, d.lineName].filter(Boolean).join(" / "),
+        },
+        { label: "鐢熶骇鏃ユ湡", value: d.produceDate || "" },
+        { label: "璁″垝鏁伴噺", value: d.planQuantity ? String(d.planQuantity) : "" },
+        { label: "璐d换浜�", value: d.ownerUserName || "" },
+      ];
+    }
+    return [];
+  }
+
+  function escapeHtml(value: string) {
+    return value
+      .replaceAll("&", "&amp;")
+      .replaceAll("<", "&lt;")
+      .replaceAll(">", "&gt;");
+  }
+
+  /** 涓嬭浇浜岀淮鐮� PNG锛堜簩缁寸爜 + 鍏抽敭淇℃伅鏂囧瓧鍚堟垚锛� */
+  function handleDownload() {
+    const base64 = barcodeRef.value?.getImageBase64();
+    if (!base64) {
+      message.warning("浜岀淮鐮佺敓鎴愬け璐ワ紝鏃犳硶涓嬭浇");
+      return;
+    }
+    const items = buildLabelText();
+    const canvas = document.createElement("canvas");
+    const ctx = canvas.getContext("2d");
+    if (!ctx) {
+      message.error("Canvas 鍒濆鍖栧け璐�");
+      return;
+    }
+    const img = new Image();
+    img.onload = () => {
+      const padding = 20;
+      const infoX = img.width + padding * 2;
+      const infoWidth = 320;
+      canvas.width = infoX + infoWidth + padding;
+      canvas.height = Math.max(img.height + padding * 2, 300);
+      ctx.fillStyle = "#ffffff";
+      ctx.fillRect(0, 0, canvas.width, canvas.height);
+      ctx.drawImage(img, padding, padding);
+      ctx.fillStyle = "#000000";
+      let y = padding + 10;
+      const lineHeight = 26;
+      items.forEach((item) => {
+        ctx.font = "bold 14px 'Microsoft YaHei', Arial, sans-serif";
+        ctx.fillText(`${item.label}:`, infoX, y);
+        y += 18;
+        ctx.font = "14px 'Microsoft YaHei', Arial, sans-serif";
+        const text = item.value || "-";
+        const maxWidth = infoWidth - 10;
+        if (ctx.measureText(text).width > maxWidth) {
+          let currentText = text;
+          while (currentText.length > 0) {
+            let line = "";
+            for (let i = 0; i < currentText.length; i++) {
+              const testLine = line + currentText[i];
+              if (ctx.measureText(testLine).width > maxWidth) {
+                break;
+              }
+              line = testLine;
+            }
+            ctx.fillText(line, infoX + 5, y);
+            y += 16;
+            currentText = currentText.slice(line.length);
+          }
+          y += 6;
+        } else {
+          ctx.fillText(text, infoX + 5, y);
+          y += lineHeight;
+        }
+      });
+      const link = document.createElement("a");
+      link.href = canvas.toDataURL("image/png");
+      link.download = `${currentCode.value || "qr"}_${Date.now()}.png`;
+      link.click();
+      message.success("涓嬭浇鎴愬姛");
+    };
+    img.src = base64;
+  }
+
+  /** 鎵撳嵃浜岀淮鐮� */
+  function handlePrint() {
+    const base64 = barcodeRef.value?.getImageBase64();
+    if (!base64) {
+      message.warning("浜岀淮鐮佺敓鎴愬け璐ワ紝鏃犳硶鎵撳嵃");
+      return;
+    }
+    const items = buildLabelText();
+    const printWindow = window.open("", "_blank");
+    if (!printWindow) {
+      message.error("鏃犳硶鎵撳紑鎵撳嵃绐楀彛锛岃妫�鏌ユ祻瑙堝櫒璁剧疆");
+      return;
+    }
+    printWindow.document.write(`<!DOCTYPE html>
+<html><head><meta charset="UTF-8"><title>鎵撳嵃浜岀淮鐮�</title>
+<style>*{margin:0;padding:0}body{font-family:'Microsoft YaHei',Arial,sans-serif;padding:20px}.print-container{text-align:center}.qr-img{max-width:100%;margin:20px 0}.info{margin-top:20px;text-align:left;font-size:13px}.info p{margin:4px 0}@media print{body{padding:0}.print-container{padding:20px}}</style>
+</head><body><div class="print-container">
+<img src="${base64}" class="qr-img" alt="浜岀淮鐮�" />
+<div class="info">
+${items.map((item) => `<p><strong>${escapeHtml(item.label)}:</strong> ${escapeHtml(item.value || "-")}</p>`).join("")}
+</div></div></body></html>`);
+    printWindow.document.close();
+    printWindow.addEventListener("load", () => {
+      setTimeout(() => printWindow.print(), 500);
+    });
+  }
+
+  const [Modal, modalApi] = useVbenModal({
+    onOpenChange(isOpen: boolean) {
+      if (!isOpen) {
+        return;
+      }
+      const data = modalApi.getData<{ code?: string }>() || {};
+      codeInput.value = data.code ?? undefined;
+      resetState();
+      if (data.code) {
+        handleSearch(data.code);
+      }
+    },
+  });
+
+  const [BagDescriptions] = useDescription({
+    bordered: true,
+    column: 2,
+    schema: [
+      {
+        field: "code",
+        label: "琚嬬爜锛堜簩缁寸爜鍐呭锛�",
+        span: 2,
+      },
+      {
+        field: "bagNo",
+        label: "琚嬪簭鍙�",
+      },
+      {
+        field: "status",
+        label: "琚嬬爜鐘舵��",
+        render: (val) =>
+          val ? getDictLabel(DICT_TYPE.MES_PRO_BAG_CODE_STATUS, val) : "",
+      },
+      {
+        field: "batchCode",
+        label: "鐢熶骇鎵规鍙�",
+      },
+      {
+        field: "batchStatus",
+        label: "鎵规鐘舵��",
+        render: (val) =>
+          val ? getDictLabel(DICT_TYPE.MES_PRO_BATCH_STATUS, val) : "",
+      },
+      {
+        field: "itemName",
+        label: "鍝佺",
+      },
+      {
+        field: "itemSpecification",
+        label: "瑙勬牸",
+      },
+      {
+        field: "produceDate",
+        label: "鐢熶骇鏃ユ湡",
+      },
+      {
+        field: "ownerUserName",
+        label: "璐d换浜�",
+      },
+      {
+        field: "lineCode",
+        label: "浜х嚎",
+        render: (val, data) =>
+          [val, data?.lineName].filter(Boolean).join(" / "),
+      },
+      {
+        field: "palletCode",
+        label: "缁戝畾鎵樼洏鐮�",
+        render: (val) => val || "鏈粦瀹�",
+      },
+      {
+        field: "printTime",
+        label: "瀵煎嚭鍗板埛鏃堕棿",
+      },
+    ],
+  });
+
+  const [PalletDescriptions] = useDescription({
+    bordered: true,
+    column: 2,
+    schema: [
+      {
+        field: "code",
+        label: "鎵樼洏鐮侊紙浜岀淮鐮佸唴瀹癸級",
+        span: 2,
+      },
+      {
+        field: "palletNo",
+        label: "鎵樼洏搴忓彿",
+      },
+      {
+        field: "capacity",
+        label: "鍙琚嬫暟锛堣/鎵樼洏锛�",
+      },
+      {
+        field: "bagCount",
+        label: "宸茶琚嬫暟",
+      },
+      {
+        field: "status",
+        label: "鎵樼洏鐘舵��",
+        render: (val) =>
+          val ? getDictLabel(DICT_TYPE.MES_PRO_PALLET_STATUS, val) : "",
+      },
+      {
+        field: "batchCode",
+        label: "鐢熶骇鎵规鍙�",
+      },
+      {
+        field: "batchStatus",
+        label: "鎵规鐘舵��",
+        render: (val) =>
+          val ? getDictLabel(DICT_TYPE.MES_PRO_BATCH_STATUS, val) : "",
+      },
+      {
+        field: "itemName",
+        label: "鍝佺",
+      },
+      {
+        field: "itemSpecification",
+        label: "瑙勬牸",
+      },
+      {
+        field: "produceDate",
+        label: "鐢熶骇鏃ユ湡",
+      },
+      {
+        field: "ownerUserName",
+        label: "璐d换浜�",
+      },
+      {
+        field: "lineCode",
+        label: "浜х嚎",
+        render: (val, data) =>
+          [val, data?.lineName].filter(Boolean).join(" / "),
+      },
+    ],
+  });
+
+  const [BatchDescriptions] = useDescription({
+    bordered: true,
+    column: 2,
+    schema: [
+      {
+        field: "code",
+        label: "鎵规鍙凤紙浜岀淮鐮佸唴瀹癸級",
+        span: 2,
+      },
+      {
+        field: "status",
+        label: "鎵规鐘舵��",
+        render: (val) =>
+          val ? getDictLabel(DICT_TYPE.MES_PRO_BATCH_STATUS, val) : "",
+      },
+      {
+        field: "itemName",
+        label: "鍝佺",
+      },
+      {
+        field: "itemSpecification",
+        label: "瑙勬牸",
+      },
+      {
+        field: "lineCode",
+        label: "浜х嚎",
+        render: (val, data) =>
+          [val, data?.lineName].filter(Boolean).join(" / "),
+      },
+      {
+        field: "produceDate",
+        label: "鐢熶骇鏃ユ湡",
+      },
+      {
+        field: "planQuantity",
+        label: "璁″垝鏁伴噺",
+      },
+      {
+        field: "bagQuantity",
+        label: "宸茬敓鎴愯鐮佹暟閲�",
+      },
+      {
+        field: "palletQuantity",
+        label: "宸茬敓鎴愭墭鐩樼爜鏁伴噺",
+      },
+      {
+        field: "ownerUserName",
+        label: "璐d换浜�",
+      },
+      {
+        field: "remark",
+        label: "澶囨敞",
+      },
+    ],
+  });
+</script>
+
+<template>
+  <Modal
+    :show-confirm-button="false"
+    cancel-text="鍏抽棴"
+    class="w-3/5"
+    title="鎵爜杩芥函 / 鏌ョ湅浜岀淮鐮�"
+  >
+    <div class="mx-4">
+      <div class="mb-4 flex gap-2">
+        <Input
+          v-model:value="codeInput"
+          allow-clear
+          class="!w-96"
+          placeholder="璇锋壂鎻忔垨杈撳叆鎵规鍙� / 鎵樼洏鐮� / 琚嬬爜锛屽洖杞︽煡璇�"
+          @press-enter="() => handleSearch()"
+        />
+        <Button :loading="loading" type="primary" @click="() => handleSearch()">
+          鏌ヨ
+        </Button>
+      </div>
+      <Spin :spinning="loading">
+        <template v-if="currentCode && traceType">
+          <div
+            class="mb-4 flex items-center gap-6 rounded bg-gray-100 p-5"
+          >
+            <div class="flex-shrink-0">
+              <Barcode ref="barcodeRef" :content="currentCode" :width="200" />
+            </div>
+            <div class="min-w-0 flex-1">
+              <p class="text-lg font-bold break-all">{{ currentCode }}</p>
+              <p class="mt-1 text-xs text-gray-500">
+                鐮佺被鍨嬶細{{ traceTypeLabel }}
+              </p>
+            </div>
+          </div>
+          <template v-if="traceType === 'batch' && batchTrace">
+            <BatchDescriptions :data="batchTrace" class="mt-2" />
+            <div
+              v-if="batchTrace.workers?.length"
+              class="mt-3 rounded border border-solid border-gray-200 p-3"
+            >
+              <p class="mb-2 font-bold">
+                鐢熶骇浜哄憳锛坽{ batchTrace.workers.length }}锛�
+              </p>
+              <div class="flex flex-wrap gap-2">
+                <Tag v-for="w in batchTrace.workers" :key="w.id">
+                  {{ w.userName
+                  }}<template v-if="w.workerType"
+                    >锛坽{
+                      getDictLabel(DICT_TYPE.MES_PRO_BATCH_WORKER_TYPE, w.workerType)
+                    }}锛�</template
+                  >
+                </Tag>
+              </div>
+            </div>
+            <div
+              v-if="batchTrace.pallets?.length"
+              class="mt-3 rounded border border-solid border-gray-200 p-3"
+            >
+              <p class="mb-2 font-bold">
+                鎵樼洏鐮侊紙{{ batchTrace.pallets.length }}锛�
+              </p>
+              <div class="flex flex-wrap gap-2">
+                <Tag v-for="p in batchTrace.pallets" :key="p.id">
+                  {{ p.code }}
+                </Tag>
+              </div>
+            </div>
+            <div
+              v-if="batchTrace.bags?.length"
+              class="mt-3 rounded border border-solid border-gray-200 p-3"
+            >
+              <p class="mb-2 font-bold">
+                琚嬬爜
+                <span
+                  v-if="
+                    batchTrace.bagTotal != null &&
+                    batchTrace.bagTotal > batchTrace.bags.length
+                  "
+                  class="text-xs text-gray-400"
+                >
+                  锛堜粎灞曠ず鍓� {{ batchTrace.bags.length }} 鏉★紝鍏�
+                  {{ batchTrace.bagTotal }} 鏉★級
+                </span>
+              </p>
+              <div class="flex max-h-48 flex-wrap gap-2 overflow-y-auto">
+                <Tag v-for="b in batchTrace.bags" :key="b.id">
+                  {{ b.code }}
+                </Tag>
+              </div>
+            </div>
+          </template>
+          <BagDescriptions
+            v-else-if="traceType === 'bag' && bagTrace"
+            :data="bagTrace"
+            class="mt-2"
+          />
+          <PalletDescriptions
+            v-else-if="traceType === 'pallet' && palletTrace"
+            :data="palletTrace"
+            class="mt-2"
+          />
+        </template>
+        <Empty
+          v-else-if="searched"
+          :description="searchError ? '璇ョ爜涓嶅瓨鍦ㄦ垨宸插垹闄�' : '鏈煡璇㈠埌杩芥函淇℃伅'"
+        />
+      </Spin>
+    </div>
+    <template v-if="currentCode && traceType" #append-footer>
+      <div class="flex gap-2">
+        <Button type="primary" @click="handlePrint">鎵撳嵃</Button>
+        <Button @click="handleDownload">涓嬭浇浜岀淮鐮�</Button>
+      </div>
+    </template>
+  </Modal>
+</template>
diff --git a/src/views/mes/pro/line/components/index.ts b/src/views/mes/pro/line/components/index.ts
new file mode 100644
index 0000000..4f469e7
--- /dev/null
+++ b/src/views/mes/pro/line/components/index.ts
@@ -0,0 +1 @@
+export { default as ProLineSelect } from './select.vue';
diff --git a/src/views/mes/pro/line/components/select.vue b/src/views/mes/pro/line/components/select.vue
new file mode 100644
index 0000000..ef58674
--- /dev/null
+++ b/src/views/mes/pro/line/components/select.vue
@@ -0,0 +1,113 @@
+<script lang="ts" setup>
+import type { MesProLineApi } from '#/api/mes/pro/line';
+
+import { computed, onMounted, ref, watch } from 'vue';
+
+import { Select, Tag, Tooltip } from 'ant-design-vue';
+
+import { getLineSimpleList } from '#/api/mes/pro/line';
+
+defineOptions({ name: 'ProLineSelect', inheritAttrs: false });
+
+const props = withDefaults(
+  defineProps<{
+    allowClear?: boolean;
+    disabled?: boolean;
+    modelValue?: number;
+    placeholder?: string;
+  }>(),
+  {
+    allowClear: true,
+    disabled: false,
+    modelValue: undefined,
+    placeholder: '璇烽�夋嫨浜х嚎',
+  },
+);
+
+const emit = defineEmits<{
+  change: [item: MesProLineApi.Line | undefined];
+  'update:modelValue': [value: number | undefined];
+}>();
+
+const allList = ref<MesProLineApi.Line[]>([]);
+const selectedItem = ref<MesProLineApi.Line>();
+
+const selectValue = computed({
+  get: () => props.modelValue,
+  set: (value: number | undefined) => {
+    emit('update:modelValue', value);
+  },
+});
+
+/** 鍓嶇杩囨护锛氭寜浜х嚎鍚嶇О鎴栫紪鐮佹ā绯婂尮閰� */
+function handleFilter(input: string, option: any) {
+  const keyword = input.toLowerCase();
+  const item = option?.item as MesProLineApi.Line | undefined;
+  return Boolean(
+    item?.name?.toLowerCase().includes(keyword) ||
+    item?.code?.toLowerCase().includes(keyword),
+  );
+}
+
+/** 鏍规嵁褰撳墠鍊煎悓姝� tooltip 灞曠ず鐨勪骇绾胯鎯� */
+function syncSelectedItem(value: number | undefined) {
+  selectedItem.value =
+    value === undefined
+      ? undefined
+      : allList.value.find((item) => item.id === value);
+}
+
+/** 闄� v-model 澶栵紝棰濆鎶涘嚭瀹屾暣浜х嚎瀵硅薄缁欎笟鍔¤〃鍗曚娇鐢� */
+function handleChange(value: any) {
+  const nextValue = value === undefined ? undefined : Number(value);
+  syncSelectedItem(nextValue);
+  emit('change', selectedItem.value);
+}
+
+watch(
+  () => props.modelValue,
+  (value) => {
+    syncSelectedItem(value);
+  },
+);
+
+onMounted(async () => {
+  allList.value = await getLineSimpleList();
+  syncSelectedItem(props.modelValue);
+});
+</script>
+
+<template>
+  <Tooltip :mouse-enter-delay="0.5" :open="selectedItem ? undefined : false">
+    <template #title>
+      <div v-if="selectedItem" class="leading-6">
+        <div>缂栫爜锛歿{ selectedItem.code || '-' }}</div>
+        <div>鍚嶇О锛歿{ selectedItem.name || '-' }}</div>
+        <div>澶囨敞锛歿{ selectedItem.remark || '-' }}</div>
+      </div>
+    </template>
+    <Select
+      v-bind="$attrs"
+      v-model:value="selectValue"
+      :allow-clear="allowClear"
+      :disabled="disabled"
+      :filter-option="handleFilter"
+      :placeholder="placeholder"
+      class="w-full"
+      show-search
+      @change="handleChange"
+    >
+      <Select.Option
+        v-for="item in allList"
+        :key="item.id"
+        :item="item"
+        :value="item.id"
+      >
+        <div class="flex items-center gap-2">
+          <span>{{ item.name }}</span>
+          <Tag v-if="item.code" color="default">{{ item.code }}</Tag>
+        </div>
+      </Select.Option>
+    </Select>
+  </Tooltip>
+</template>
diff --git a/src/views/mes/pro/line/data.ts b/src/views/mes/pro/line/data.ts
new file mode 100644
index 0000000..4614e4e
--- /dev/null
+++ b/src/views/mes/pro/line/data.ts
@@ -0,0 +1,152 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesProLineApi } from '#/api/mes/pro/line';
+
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'detail' | 'update';
+
+/** 鏂板/淇敼浜х嚎鐨勮〃鍗� */
+export function useFormSchema(formType: FormType): VbenFormSchema[] {
+  const disabled = formType === 'detail';
+  return [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: {
+        triggerFields: [''],
+        show: () => false,
+      },
+    },
+    {
+      fieldName: 'code',
+      label: '浜х嚎缂栫爜',
+      component: 'Input',
+      componentProps: {
+        disabled,
+        maxLength: 100,
+        placeholder: '璇疯緭鍏ヤ骇绾跨紪鐮侊紙鍙備笌鎵规鍙风敓鎴愶級',
+      },
+      rules: z.string().min(1, '浜х嚎缂栫爜涓嶈兘涓虹┖').max(100),
+    },
+    {
+      fieldName: 'name',
+      label: '浜х嚎鍚嶇О',
+      component: 'Input',
+      componentProps: {
+        disabled,
+        maxLength: 100,
+        placeholder: '璇疯緭鍏ヤ骇绾垮悕绉�',
+      },
+      rules: z.string().min(1, '浜х嚎鍚嶇О涓嶈兘涓虹┖').max(100),
+    },
+    {
+      fieldName: 'status',
+      label: '鐘舵��',
+      component: 'RadioGroup',
+      componentProps: {
+        buttonStyle: 'solid',
+        disabled,
+        optionType: 'button',
+        options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+      },
+      defaultValue: CommonStatusEnum.ENABLE,
+      rules: z.number().default(CommonStatusEnum.ENABLE),
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      formItemClass: 'col-span-2',
+      componentProps: {
+        disabled,
+        maxLength: 250,
+        placeholder: '璇疯緭鍏ュ娉�',
+        rows: 2,
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'code',
+      label: '浜х嚎缂栫爜',
+      component: 'Input',
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇疯緭鍏ヤ骇绾跨紪鐮�',
+      },
+    },
+    {
+      fieldName: 'name',
+      label: '浜х嚎鍚嶇О',
+      component: 'Input',
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇疯緭鍏ヤ骇绾垮悕绉�',
+      },
+    },
+    {
+      fieldName: 'status',
+      label: '鐘舵��',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+        placeholder: '璇烽�夋嫨鐘舵��',
+      },
+    },
+    {
+      fieldName: 'createTime',
+      label: '鍒涘缓鏃堕棿',
+      component: 'RangePicker',
+      componentProps: {
+        ...getRangePickerDefaultProps(),
+        allowClear: true,
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions<MesProLineApi.Line>['columns'] {
+  return [
+    {
+      field: 'code',
+      title: '浜х嚎缂栫爜',
+      minWidth: 150,
+      slots: { default: 'code' },
+    },
+    { field: 'name', title: '浜х嚎鍚嶇О', minWidth: 180 },
+    {
+      field: 'status',
+      title: '鐘舵��',
+      width: 100,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.COMMON_STATUS },
+      },
+    },
+    { field: 'remark', title: '澶囨敞', minWidth: 180 },
+    {
+      field: 'createTime',
+      title: '鍒涘缓鏃堕棿',
+      width: 180,
+      formatter: 'formatDateTime',
+    },
+    {
+      title: '鎿嶄綔',
+      width: 150,
+      fixed: 'right',
+      slots: { default: 'actions' },
+    },
+  ];
+}
diff --git a/src/views/mes/pro/line/index.vue b/src/views/mes/pro/line/index.vue
new file mode 100644
index 0000000..6c613a7
--- /dev/null
+++ b/src/views/mes/pro/line/index.vue
@@ -0,0 +1,143 @@
+<script lang="ts" setup>
+  import type { VxeTableGridOptions } from "#/adapter/vxe-table";
+  import type { MesProLineApi } from "#/api/mes/pro/line";
+
+  import { Page, useVbenModal } from "@vben/common-ui";
+  import { downloadFileFromBlobPart } from "@vben/utils";
+
+  import { Button, message } from "ant-design-vue";
+
+  import { ACTION_ICON, TableAction, useVbenVxeGrid } from "#/adapter/vxe-table";
+  import {
+    deleteLine,
+    exportLineExcel,
+    getLinePage,
+  } from "#/api/mes/pro/line";
+  import { $t } from "#/locales";
+
+  import { useGridColumns, useGridFormSchema } from "./data";
+  import Form from "./modules/form.vue";
+
+  defineOptions({ name: "MesProLine" });
+
+  const [FormModal, formModalApi] = useVbenModal({
+    connectedComponent: Form,
+    destroyOnClose: true,
+  });
+
+  /** 鍒锋柊琛ㄦ牸 */
+  function handleRefresh() {
+    gridApi.query();
+  }
+
+  /** 鏂板浜х嚎 */
+  function handleCreate() {
+    formModalApi.setData({ formType: "create" }).open();
+  }
+
+  /** 鏌ョ湅璇︽儏 */
+  function handleDetail(row: MesProLineApi.Line) {
+    formModalApi.setData({ formType: "detail", id: row.id }).open();
+  }
+
+  /** 缂栬緫浜х嚎 */
+  function handleEdit(row: MesProLineApi.Line) {
+    formModalApi.setData({ formType: "update", id: row.id }).open();
+  }
+
+  /** 鍒犻櫎浜х嚎 */
+  async function handleDelete(row: MesProLineApi.Line) {
+    await deleteLine(row.id!);
+    message.success($t("ui.actionMessage.deleteSuccess", [row.name]));
+    handleRefresh();
+  }
+
+  /** 瀵煎嚭琛ㄦ牸 */
+  async function handleExport() {
+    const data = await exportLineExcel(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) => {
+            return await getLinePage({
+              pageNo: page.currentPage,
+              pageSize: page.pageSize,
+              ...formValues,
+            });
+          },
+        },
+      },
+      rowConfig: {
+        keyField: "id",
+        isHover: true,
+      },
+      toolbarConfig: {
+        refresh: true,
+        search: true,
+      },
+    } as VxeTableGridOptions<MesProLineApi.Line>,
+  });
+</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-line:create'],
+              onClick: handleCreate,
+            },
+            {
+              label: $t('ui.actionTitle.export'),
+              type: 'primary',
+              icon: ACTION_ICON.DOWNLOAD,
+              auth: ['mes:pro-line:export'],
+              onClick: handleExport,
+            },
+          ]" />
+      </template>
+      <template #code="{ row }">
+        <Button type="link" @click="handleDetail(row)">
+          {{ row.code }}
+        </Button>
+      </template>
+      <template #actions="{ row }">
+        <TableAction :actions="[
+            {
+              label: $t('common.edit'),
+              type: 'link',
+              icon: ACTION_ICON.EDIT,
+              auth: ['mes:pro-line:update'],
+              onClick: handleEdit.bind(null, row),
+            },
+            {
+              label: $t('common.delete'),
+              type: 'link',
+              danger: true,
+              icon: ACTION_ICON.DELETE,
+              auth: ['mes:pro-line:delete'],
+              popConfirm: {
+                title: $t('ui.actionMessage.deleteConfirm', [row.name]),
+                confirm: handleDelete.bind(null, row),
+              },
+            },
+          ]" />
+      </template>
+    </Grid>
+  </Page>
+</template>
diff --git a/src/views/mes/pro/line/modules/form.vue b/src/views/mes/pro/line/modules/form.vue
new file mode 100644
index 0000000..067e1e0
--- /dev/null
+++ b/src/views/mes/pro/line/modules/form.vue
@@ -0,0 +1,111 @@
+<script lang="ts" setup>
+  import type { FormType } from "../data";
+  import type { MesProLineApi } from "#/api/mes/pro/line";
+
+  import { computed, ref } from "vue";
+
+  import { useVbenModal } from "@vben/common-ui";
+
+  import { Button, message } from "ant-design-vue";
+
+  import { useVbenForm } from "#/adapter/form";
+  import { createLine, getLine, updateLine } from "#/api/mes/pro/line";
+  import { $t } from "#/locales";
+
+  import { useFormSchema } from "../data";
+
+  defineOptions({ name: "MesProLineForm" });
+
+  const emit = defineEmits(["success"]);
+
+  const formType = ref<FormType>("create");
+  const formData = ref<MesProLineApi.Line>();
+
+  const isEditable = computed(() =>
+    ["create", "update"].includes(formType.value)
+  );
+
+  const getTitle = computed(() => {
+    switch (formType.value) {
+      case "detail": {
+        return $t("ui.actionTitle.view", ["浜х嚎"]);
+      }
+      case "update": {
+        return $t("ui.actionTitle.edit", ["浜х嚎"]);
+      }
+      default: {
+        return $t("ui.actionTitle.create", ["浜х嚎"]);
+      }
+    }
+  });
+
+  const [Form, formApi] = useVbenForm({
+    commonConfig: {
+      componentProps: {
+        class: "w-full",
+      },
+      formItemClass: "col-span-1",
+      labelWidth: 100,
+    },
+    layout: "horizontal",
+    schema: [],
+    showDefaultActions: false,
+    wrapperClass: "grid-cols-2",
+  });
+
+  const [Modal, modalApi] = useVbenModal({
+    async onConfirm() {
+      if (!isEditable.value) {
+        await modalApi.close();
+        return;
+      }
+      const { valid } = await formApi.validate();
+      if (!valid) return;
+      modalApi.lock();
+      try {
+        const data = (await formApi.getValues()) as MesProLineApi.Line;
+        if (formData.value?.id) {
+          await updateLine({ ...data, id: formData.value.id });
+        } else {
+          await createLine(data);
+        }
+        message.success($t("ui.actionMessage.operationSuccess"));
+        emit("success");
+        await modalApi.close();
+      } 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(formType.value) });
+      if (data?.id) {
+        modalApi.lock();
+        try {
+          formData.value = await getLine(data.id);
+          await formApi.setValues(formData.value);
+        } finally {
+          modalApi.unlock();
+        }
+      }
+    },
+  });
+</script>
+
+<template>
+  <Modal :title="getTitle" class="w-2/5">
+    <Form class="mx-4" />
+    <template #append-footer>
+      <div class="flex gap-2">
+        <Button @click="modalApi.close()">
+          {{ isEditable ? '鍙栨秷' : '鍏抽棴' }}
+        </Button>
+      </div>
+    </template>
+  </Modal>
+</template>
diff --git a/src/views/mes/pro/pallet/data.ts b/src/views/mes/pro/pallet/data.ts
new file mode 100644
index 0000000..ee2db7a
--- /dev/null
+++ b/src/views/mes/pro/pallet/data.ts
@@ -0,0 +1,109 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesProPalletApi } from '#/api/mes/pro/pallet';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'code',
+      label: '鎵樼洏鐮�',
+      component: 'Input',
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇疯緭鍏ユ墭鐩樼爜',
+      },
+    },
+    {
+      fieldName: 'batchCode',
+      label: '鎵规鍙�',
+      component: 'Input',
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇疯緭鍏ユ壒娆″彿',
+      },
+    },
+    {
+      fieldName: 'status',
+      label: '鎵樼洏鐘舵��',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: getDictOptions(DICT_TYPE.MES_PRO_PALLET_STATUS, 'number'),
+        placeholder: '璇烽�夋嫨鎵樼洏鐘舵��',
+      },
+    },
+    {
+      fieldName: 'createTime',
+      label: '鍒涘缓鏃堕棿',
+      component: 'RangePicker',
+      componentProps: {
+        ...getRangePickerDefaultProps(),
+        allowClear: true,
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions<MesProPalletApi.Pallet>['columns'] {
+  return [
+    {
+      field: 'code',
+      title: '鎵樼洏鐮�',
+      fixed: 'left',
+      minWidth: 210,
+    },
+    {
+      field: 'palletNo',
+      title: '鎵樼洏搴忓彿',
+      width: 90,
+    },
+    {
+      field: 'batchCode',
+      title: '鎵规鍙�',
+      minWidth: 190,
+    },
+    {
+      field: 'capacity',
+      title: '鍙琚嬫暟锛堣/鎵樼洏锛�',
+      width: 150,
+    },
+    {
+      field: 'bagCount',
+      title: '宸茬粦琚嬬爜鏁伴噺',
+      width: 120,
+    },
+    {
+      field: 'status',
+      title: '鎵樼洏鐘舵��',
+      width: 100,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.MES_PRO_PALLET_STATUS },
+      },
+    },
+    {
+      field: 'remark',
+      title: '澶囨敞',
+      minWidth: 140,
+    },
+    {
+      field: 'createTime',
+      title: '鍒涘缓鏃堕棿',
+      width: 170,
+      formatter: 'formatDateTime',
+    },
+    {
+      title: '鎿嶄綔',
+      width: 90,
+      fixed: 'right',
+      slots: { default: 'actions' },
+    },
+  ];
+}
diff --git a/src/views/mes/pro/pallet/index.vue b/src/views/mes/pro/pallet/index.vue
new file mode 100644
index 0000000..403a339
--- /dev/null
+++ b/src/views/mes/pro/pallet/index.vue
@@ -0,0 +1,169 @@
+<script lang="ts" setup>
+  import type { VxeTableGridOptions } from "#/adapter/vxe-table";
+  import type { MesProPalletApi } from "#/api/mes/pro/pallet";
+
+  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 {
+    deletePallet,
+    exportPalletExcel,
+    exportPalletQr,
+    getPalletPage,
+  } from "#/api/mes/pro/pallet";
+  import { $t } from "#/locales";
+
+  import { useGridColumns, useGridFormSchema } from "./data";
+  import GenerateModal from "./modules/generate-form.vue";
+  import QrTraceModal from "../components/qr-trace-modal.vue";
+
+  defineOptions({ name: "MesProPallet" });
+
+  const [GenerateFormModal, generateFormModalApi] = useVbenModal({
+    connectedComponent: GenerateModal,
+    destroyOnClose: true,
+  });
+
+  const [QrTraceQueryModal, qrTraceQueryModalApi] = useVbenModal({
+    connectedComponent: QrTraceModal,
+    destroyOnClose: true,
+  });
+
+  /** 鍒锋柊琛ㄦ牸 */
+  function handleRefresh() {
+    gridApi.query();
+  }
+
+  /** 鎵归噺鐢熸垚鎵樼洏鐮� */
+  function handleCreate() {
+    generateFormModalApi.open();
+  }
+
+  /** 鎵爜杩芥函 */
+  function handleTrace() {
+    qrTraceQueryModalApi.open();
+  }
+
+  /** 鏌ョ湅浜岀淮鐮侊紙琛屽唴棰勫~鐮佸�硷級 */
+  function handleViewQr(row: MesProPalletApi.Pallet) {
+    qrTraceQueryModalApi.setData({ code: row.code }).open();
+  }
+
+  /** 瀵煎嚭浜岀淮鐮� ZIP锛堟寜褰撳墠鏌ヨ鏉′欢鐨勬壒娆★級 */
+  async function handleExportQr() {
+    const values = await gridApi.formApi.getValues();
+    if (!values.batchId) {
+      message.warning("璇峰厛鎸夋壒娆$瓫閫夊悗鍐嶅鍑轰簩缁寸爜");
+      return;
+    }
+    const data = await exportPalletQr(values.batchId);
+    downloadFileFromBlobPart({ fileName: "鎵樼洏鐮佷簩缁寸爜.zip", source: data });
+  }
+
+  /** 鍒犻櫎鎵樼洏鐮� */
+  async function handleDelete(row: MesProPalletApi.Pallet) {
+    await deletePallet(row.id!);
+    message.success($t("ui.actionMessage.deleteSuccess", [row.code]));
+    handleRefresh();
+  }
+
+  /** 瀵煎嚭琛ㄦ牸 */
+  async function handleExport() {
+    const data = await exportPalletExcel(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) => {
+            return await getPalletPage({
+              pageNo: page.currentPage,
+              pageSize: page.pageSize,
+              ...formValues,
+            });
+          },
+        },
+      },
+      rowConfig: {
+        keyField: "id",
+        isHover: true,
+      },
+      toolbarConfig: {
+        refresh: true,
+        search: true,
+      },
+    } as VxeTableGridOptions<MesProPalletApi.Pallet>,
+  });
+</script>
+
+<template>
+  <Page auto-content-height>
+    <GenerateFormModal @success="handleRefresh" />
+    <QrTraceQueryModal />
+    <Grid table-title="鎵樼洏鐮佸垪琛�">
+      <template #toolbar-tools>
+        <TableAction :actions="[
+            {
+              label: '鎵归噺鐢熸垚',
+              type: 'primary',
+              icon: ACTION_ICON.ADD,
+              auth: ['mes:pro-pallet:create'],
+              onClick: handleCreate,
+            },
+            {
+              label: '鎵爜杩芥函',
+              icon: ACTION_ICON.SEARCH,
+              auth: ['mes:pro-pallet:query'],
+              onClick: handleTrace,
+            },
+            {
+              label: '瀵煎嚭浜岀淮鐮�',
+              icon: ACTION_ICON.DOWNLOAD,
+              auth: ['mes:pro-pallet:export'],
+              onClick: handleExportQr,
+            },
+            {
+              label: $t('ui.actionTitle.export'),
+              type: 'primary',
+              icon: ACTION_ICON.DOWNLOAD,
+              auth: ['mes:pro-pallet:export'],
+              onClick: handleExport,
+            },
+          ]" />
+      </template>
+      <template #actions="{ row }">
+        <TableAction :actions="[
+            {
+              label: '鏌ョ湅浜岀淮鐮�',
+              type: 'link',
+              auth: ['mes:pro-pallet:query'],
+              onClick: handleViewQr.bind(null, row),
+            },
+            {
+              label: $t('common.delete'),
+              type: 'link',
+              danger: true,
+              icon: ACTION_ICON.DELETE,
+              auth: ['mes:pro-pallet:delete'],
+              ifShow: !row.bagCount,
+              popConfirm: {
+                title: $t('ui.actionMessage.deleteConfirm', [row.code]),
+                confirm: handleDelete.bind(null, row),
+              },
+            },
+          ]" />
+      </template>
+    </Grid>
+  </Page>
+</template>
diff --git a/src/views/mes/pro/pallet/modules/generate-form.vue b/src/views/mes/pro/pallet/modules/generate-form.vue
new file mode 100644
index 0000000..c34f952
--- /dev/null
+++ b/src/views/mes/pro/pallet/modules/generate-form.vue
@@ -0,0 +1,122 @@
+<script lang="ts" setup>
+  import type { VbenFormSchema } from "#/adapter/form";
+  import type { MesProPalletApi } from "#/api/mes/pro/pallet";
+
+  import { useVbenModal } from "@vben/common-ui";
+  import { MesProBatchStatusEnum } from "@vben/constants";
+
+  import { message } from "ant-design-vue";
+
+  import { useVbenForm } from "#/adapter/form";
+  import { getBatchSimpleList } from "#/api/mes/pro/batch";
+  import { batchCreatePallet } from "#/api/mes/pro/pallet";
+
+  defineOptions({ name: "MesProPalletGenerateForm" });
+
+  const emit = defineEmits(["success"]);
+
+  /** 鏋勫缓琛ㄥ崟锛堟壒娆′笅鎷夐�夐」鐢卞閮ㄥ姞杞芥敞鍏ワ級 */
+  function buildSchema(batchOptions: { label: string; value: number }[]): VbenFormSchema[] {
+    return [
+      {
+        fieldName: "batchId",
+        label: "鐢熶骇鎵规",
+        component: "Select",
+        componentProps: {
+          options: batchOptions,
+          optionFilterProp: "label",
+          placeholder: "璇烽�夋嫨鐢熶骇涓殑鎵规",
+          showSearch: true,
+        },
+        rules: "selectRequired",
+      },
+      {
+        fieldName: "count",
+        label: "鐢熸垚鏁伴噺",
+        component: "InputNumber",
+        componentProps: {
+          class: "!w-full",
+          min: 1,
+          placeholder: "璇疯緭鍏ラ渶瑕佺敓鎴愮殑鎵樼洏鏁伴噺",
+          precision: 0,
+        },
+        rules: "required",
+      },
+      {
+        fieldName: "capacity",
+        label: "鍙琚嬫暟",
+        component: "InputNumber",
+        componentProps: {
+          class: "!w-full",
+          min: 1,
+          placeholder: "璇疯緭鍏ユ瘡鎵樼洏鍙琚嬫暟锛堣/鎵樼洏锛�",
+          precision: 0,
+        },
+      },
+      {
+        fieldName: "remark",
+        label: "澶囨敞",
+        component: "Textarea",
+        componentProps: {
+          placeholder: "璇疯緭鍏ュ娉�",
+          rows: 2,
+        },
+      },
+    ];
+  }
+
+  const [Form, formApi] = useVbenForm({
+    commonConfig: {
+      componentProps: {
+        class: "w-full",
+      },
+      labelWidth: 100,
+    },
+    layout: "horizontal",
+    schema: [],
+    showDefaultActions: false,
+  });
+
+  const [Modal, modalApi] = useVbenModal({
+    async onConfirm() {
+      const { valid } = await formApi.validate();
+      if (!valid) return;
+      modalApi.lock();
+      try {
+        const values =
+          (await formApi.getValues()) as MesProPalletApi.BatchCreateParams;
+        const ids = await batchCreatePallet(values);
+        message.success(`宸叉垚鍔熺敓鎴� ${ids.length} 涓墭鐩樼爜`);
+        emit("success");
+        await modalApi.close();
+      } finally {
+        modalApi.unlock();
+      }
+    },
+    async onOpenChange(isOpen: boolean) {
+      if (!isOpen) return;
+      modalApi.lock();
+      try {
+        // 浠呯敓浜т腑鐨勬壒娆″厑璁哥敓鎴愭墭鐩樼爜
+        const batches = await getBatchSimpleList();
+        const options = batches
+          .filter(
+            (batch) => batch.status === MesProBatchStatusEnum.PRODUCING
+          )
+          .map((batch) => ({
+            label: batch.code!,
+            value: batch.id!,
+          }));
+        await formApi.setState({ schema: buildSchema(options) });
+      } finally {
+        modalApi.unlock();
+      }
+    },
+  });
+</script>
+
+<template>
+  <Modal title="鎵归噺鐢熸垚鎵樼洏鐮�" class="w-2/5">
+    <Form class="mx-4" />
+  </Modal>
+</template>

--
Gitblit v1.9.3