From 1c1af9b0fc10778ae5ac13cc68fd4030affbcfe1 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期三, 09 九月 2026 10:59:53 +0800
Subject: [PATCH] feat(wm): 新增SN码管理和设备入库功能

---
 src/api/mes/wm/returnvendor/index.ts                 |   19 
 src/views/hrm/training/modules/form.vue              |   96 +
 src/views/wls/stocktaking/plan/data.ts               |   49 
 src/views/wls/sn/data.ts                             |  289 ++++
 src/views/hrm/training/data.ts                       |  243 +++
 src/views/wls/equipmentreceipt/modules/line-list.vue |  142 +
 src/views/wls/transaction/data.ts                    |    5 
 src/views/hrm/training/modules/participants.vue      |  278 +++
 src/api/hrm/work-hour-summary/index.ts               |   36 
 src/views/wls/returnvendor/modules/form.vue          |   38 
 src/views/wls/stocktaking/task/modules/form.vue      |   58 
 src/api/hrm/training/index.ts                        |  141 +
 src/api/mes/dv/machinery/index.ts                    |    1 
 src/api/mes/wm/equipmentreceipt/index.ts             |   81 +
 src/views/wls/stocktaking/task/index.vue             |   75 
 src/views/wls/returnvendor/index.vue                 |   64 
 src/views/wls/sn/index.vue                           |  298 +++
 src/views/wls/sn/modules/in-stock-modal.vue          |   62 
 src/views/wls/sn/modules/out-stock-modal.vue         |   62 
 src/api/mes/wm/miscissue/index.ts                    |   19 
 src/api/mes/wm/equipmentreceipt/line/index.ts        |   55 
 src/views/hrm/training/index.vue                     |  263 +++
 src/api/mes/wm/stocktaking/plan/index.ts             |    3 
 src/views/wls/barcode/components/index.ts            |    1 
 src/views/wls/miscreceipt/modules/form.vue           |   38 
 src/api/mes/wm/stocktaking/task/index.ts             |   21 
 src/views/wls/barcode/components/scan-modal.vue      |  178 ++
 src/views/hrm/workHourSummary/data.ts                |  107 +
 src/views/wls/equipmentreceipt/modules/line-form.vue |   98 +
 src/packages/constants/src/biz-mes-enum.ts           |   18 
 src/views/wls/miscissue/modules/form.vue             |   37 
 src/views/wls/sn/location-maps.ts                    |   51 
 src/views/wls/sn/modules/sn-detail-modal.vue         |  118 +
 src/api/mes/wm/sn/index.ts                           |   78 +
 src/components/table-action/icons.ts                 |    1 
 src/views/wls/miscissue/index.vue                    |   64 
 src/views/wls/barcode/index.vue                      |   16 
 src/views/wls/miscreceipt/index.vue                  |   64 
 src/views/wls/equipmentreceipt/data.ts               |  342 ++++
 src/views/wls/stocktaking/task/data.ts               |    8 
 src/views/hrm/workHourSummary/index.vue              |  128 +
 src/api/mes/wm/miscreceipt/index.ts                  |   19 
 src/views/wls/equipmentreceipt/modules/form.vue      |  163 ++
 src/views/wls/components/process-select-modal.vue    |   83 +
 src/api/mes/wm/barcode/index.ts                      |   29 
 src/views/wls/equipmentreceipt/index.vue             |  243 +++
 46 files changed, 4,020 insertions(+), 262 deletions(-)

diff --git a/src/api/hrm/training/index.ts b/src/api/hrm/training/index.ts
new file mode 100644
index 0000000..51c9a8e
--- /dev/null
+++ b/src/api/hrm/training/index.ts
@@ -0,0 +1,141 @@
+import type { PageParam, PageResult } from '#/packages/effects/request/src';
+
+import { requestClient } from '#/api/request';
+
+export namespace HrmTrainingApi {
+  /** 鍩硅鍙傝浜哄憳 */
+  export interface Participant {
+    id?: number;
+    trainingId?: number;
+    employeeId?: number;
+    employeeNo?: string;
+    employeeName?: string;
+    attended?: boolean;
+    score?: number;
+    isPass?: boolean;
+    certificateId?: number;
+    remark?: string;
+    createTime?: string;
+  }
+
+  /** 鍩硅娲诲姩 */
+  export interface Training {
+    id?: number;
+    trainingNo?: string;
+    name?: string;
+    trainingType?: number;
+    trainingTypeName?: string;
+    trainer?: string;
+    location?: string;
+    hours?: number;
+    startDate?: string;
+    endDate?: string;
+    issueCertificate?: boolean;
+    certValidityMonths?: number;
+    status?: number;
+    statusName?: string;
+    participantCount?: number;
+    participants?: Participant[];
+    remark?: string;
+    createTime?: string;
+  }
+
+  /** 鍩硅娲诲姩鏌ヨ鍙傛暟 */
+  export interface TrainingPageParam extends PageParam {
+    trainingNo?: string;
+    name?: string;
+    trainingType?: number;
+    status?: number;
+  }
+}
+
+/** 鏌ヨ鍩硅娲诲姩鍒嗛〉 */
+export function getTrainingPage(params: HrmTrainingApi.TrainingPageParam) {
+  return requestClient.get<PageResult<HrmTrainingApi.Training>>(
+    '/hrm/training/page',
+    { params },
+  );
+}
+
+/** 鏌ヨ鍩硅娲诲姩璇︽儏锛堝惈鍙傝浜哄憳锛� */
+export function getTraining(id: number) {
+  return requestClient.get<HrmTrainingApi.Training>(`/hrm/training/get?id=${id}`);
+}
+
+/** 鍒涘缓鍩硅娲诲姩 */
+export function createTraining(data: ReqTraining) {
+  return requestClient.post('/hrm/training/create', data);
+}
+
+/** 鏇存柊鍩硅娲诲姩 */
+export function updateTraining(data: ReqTraining) {
+  return requestClient.put('/hrm/training/update', data);
+}
+
+/** 鍒犻櫎鍩硅娲诲姩 */
+export function deleteTraining(ids: number[]) {
+  return requestClient.delete('/hrm/training/delete', { params: { ids: ids.join(',') } });
+}
+
+/** 寮�濮嬪煿璁紙鑽夌 鈫� 寰呭紑灞曪級 */
+export function startTraining(id: number) {
+  return requestClient.post('/hrm/training/start', null, { params: { id } });
+}
+
+/** 瀹屾垚鍩硅锛堣崏绋�/寰呭紑灞� 鈫� 宸插畬鎴愶級 */
+export function completeTraining(id: number) {
+  return requestClient.post('/hrm/training/complete', null, { params: { id } });
+}
+
+/** 鍙栨秷鍩硅锛堣崏绋�/寰呭紑灞� 鈫� 宸插彇娑堬級 */
+export function cancelTraining(id: number) {
+  return requestClient.post('/hrm/training/cancel', null, { params: { id } });
+}
+
+/** 鎵归噺娣诲姞鍙傝鍛樺伐 */
+export function addTrainingParticipants(trainingId: number, employeeIds: number[]) {
+  return requestClient.post('/hrm/training-participant/add', { trainingId, employeeIds });
+}
+
+/** 鏇存柊鍙傝璁板綍 */
+export function updateTrainingParticipant(data: ReqParticipant) {
+  return requestClient.put('/hrm/training-participant/update', data);
+}
+
+/** 鍒犻櫎鍙傝璁板綍 */
+export function deleteTrainingParticipants(ids: number[]) {
+  return requestClient.delete('/hrm/training-participant/delete', {
+    params: { ids: ids.join(',') },
+  });
+}
+
+/** 鑾峰彇鏌愭鍩硅鐨勫弬璁汉鍛樺垪琛� */
+export function getTrainingParticipants(trainingId: number) {
+  return requestClient.get<HrmTrainingApi.Participant[]>(
+    `/hrm/training-participant/list?trainingId=${trainingId}`,
+  );
+}
+
+/** 鍩硅娲诲姩鏂板/鏇存柊璇锋眰 */
+export interface ReqTraining {
+  id?: number;
+  name?: string;
+  trainingType?: number;
+  trainer?: string;
+  location?: string;
+  hours?: number;
+  startDate?: string;
+  endDate?: string;
+  issueCertificate?: boolean;
+  certValidityMonths?: number;
+  remark?: string;
+}
+
+/** 鍙傝璁板綍鏇存柊璇锋眰 */
+export interface ReqParticipant {
+  id?: number;
+  attended?: boolean;
+  score?: number;
+  isPass?: boolean;
+  remark?: string;
+}
\ No newline at end of file
diff --git a/src/api/hrm/work-hour-summary/index.ts b/src/api/hrm/work-hour-summary/index.ts
new file mode 100644
index 0000000..fac0244
--- /dev/null
+++ b/src/api/hrm/work-hour-summary/index.ts
@@ -0,0 +1,36 @@
+import type { PageParam, PageResult } from '#/packages/effects/request/src';
+
+import { requestClient } from '#/api/request';
+
+export namespace HrmWorkHourSummaryApi {
+  /** 鏈堝害宸ユ椂姹囨�� */
+  export interface WorkHourSummary {
+    id?: number;
+    employeeId?: number; // 鍛樺伐ID
+    employeeNo?: string; // 鍛樺伐缂栧彿
+    employeeName?: string; // 鍛樺伐濮撳悕
+    deptId?: number; // 閮ㄩ棬ID
+    deptName?: string; // 閮ㄩ棬鍚嶇О
+    period?: string; // 鏍哥畻鍛ㄦ湡锛坹yyy-MM锛�
+    workDays?: number; // 鍑哄嫟澶╂暟
+    workHours?: number; // 姝e父宸ユ椂锛堝皬鏃讹級
+    overtimeHours?: number; // 鍔犵彮宸ユ椂锛堝皬鏃讹級
+    remark?: string; // 澶囨敞
+    createTime?: string;
+  }
+}
+
+/** 鐢熸垚鏈堝害宸ユ椂姹囨�� */
+export function generateWorkHourSummary(period: string) {
+  return requestClient.post('/hrm/work-hour-summary/generate-monthly', null, {
+    params: { period },
+  });
+}
+
+/** 鑾峰彇鏈堝害宸ユ椂姹囨�诲垎椤� */
+export function getWorkHourSummaryPage(params: PageParam) {
+  return requestClient.get<PageResult<HrmWorkHourSummaryApi.WorkHourSummary>>(
+    '/hrm/work-hour-summary/page',
+    { params },
+  );
+}
\ No newline at end of file
diff --git a/src/api/mes/dv/machinery/index.ts b/src/api/mes/dv/machinery/index.ts
index 601c672..4a414b6 100644
--- a/src/api/mes/dv/machinery/index.ts
+++ b/src/api/mes/dv/machinery/index.ts
@@ -13,6 +13,7 @@
     specification?: string; // 瑙勬牸鍨嬪彿
     machineryTypeId?: number; // 璁惧绫诲瀷缂栧彿
     machineryTypeName?: string; // 璁惧绫诲瀷鍚嶇О
+    itemId?: number; // 璁惧鐗╂枡缂栧彿锛圡DM 鐗╂枡锛�
     workshopId?: number; // 鎵�灞炶溅闂寸紪鍙�
     workshopName?: string; // 鎵�灞炶溅闂村悕绉�
     status?: number; // 璁惧鐘舵��
diff --git a/src/api/mes/wm/barcode/index.ts b/src/api/mes/wm/barcode/index.ts
index 39673a3..507f02f 100644
--- a/src/api/mes/wm/barcode/index.ts
+++ b/src/api/mes/wm/barcode/index.ts
@@ -62,9 +62,32 @@
   return requestClient.download('/mes/wm/barcode/export-excel', { params });
 }
 
-/** 鐢熸垚鏉$爜鍐呭 */
-export function generateBarcodeContent(bizType: number, bizCode: string) {
+/** 鐢熸垚鏉$爜鍐呭锛堟敮鎸佸鍗犱綅绗︾粍鍚堬紝鍙紶鍏ヤ笟鍔″悕绉帮級 */
+export function generateBarcodeContent(
+  bizType: number,
+  bizCode: string,
+  bizName?: string,
+) {
   return requestClient.get<string>('/mes/wm/barcode/generate-content', {
-    params: { bizCode, bizType },
+    params: { bizCode, bizName, bizType },
+  });
+}
+
+/** 鐢熸垚鏉$爜/浜岀淮鐮佸浘鐗囷紙鏈嶅姟绔� zxing 鎴愬浘锛岃繑鍥� PNG Base64锛屼笉钀藉簱锛� */
+export function renderBarcodeImage(
+  content: string,
+  format?: number,
+  width?: number,
+  height?: number,
+) {
+  return requestClient.post<string>('/mes/wm/barcode/render', null, {
+    params: { content, format, height, width },
+  });
+}
+
+/** 瑙f瀽鏉$爜鍐呭锛堜紭鍏堢簿纭懡涓紝鍏舵鎸夊惎鐢ㄩ厤缃ā鏉垮弽瑙o紝杩斿洖涓氬姟瀛楁锛� */
+export function parseBarcodeContent(content: string) {
+  return requestClient.post<Record<string, any>>('/mes/wm/barcode/parse', null, {
+    params: { content },
   });
 }
diff --git a/src/api/mes/wm/equipmentreceipt/index.ts b/src/api/mes/wm/equipmentreceipt/index.ts
new file mode 100644
index 0000000..782a8b1
--- /dev/null
+++ b/src/api/mes/wm/equipmentreceipt/index.ts
@@ -0,0 +1,81 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+import type { MesWmEquipmentReceiptLineApi } from './line';
+
+export namespace MesWmEquipmentReceiptApi {
+  /** MES 璁惧鍏ュ簱鍗� */
+  export interface EquipmentReceipt {
+    id?: number; // 缂栧彿
+    code?: string; // 鍗曟嵁缂栫爜
+    name?: string; // 鍗曟嵁鍚嶇О
+    receiptDate?: number; // 鍏ュ簱鏃ユ湡
+    source?: string; // 鍏ュ簱鏉ユ簮
+    status?: number; // 鍗曟嵁鐘舵��
+    processInstanceId?: string; // 瀹℃壒娴佺▼瀹炰緥缂栧彿
+    remark?: string; // 澶囨敞
+    createTime?: Date; // 鍒涘缓鏃堕棿
+    lines?: MesWmEquipmentReceiptLineApi.EquipmentReceiptLine[]; // 鏄庣粏琛�
+  }
+}
+
+/** 鏌ヨ璁惧鍏ュ簱鍗曞垎椤� */
+export function getEquipmentReceiptPage(params: PageParam) {
+  return requestClient.get<
+    PageResult<MesWmEquipmentReceiptApi.EquipmentReceipt>
+  >('/mes/wm/equipment-receipt/page', { params });
+}
+
+/** 鏌ヨ璁惧鍏ュ簱鍗曡鎯� */
+export function getEquipmentReceipt(id: number) {
+  return requestClient.get<MesWmEquipmentReceiptApi.EquipmentReceipt>(
+    `/mes/wm/equipment-receipt/get?id=${id}`,
+  );
+}
+
+/** 鏂板璁惧鍏ュ簱鍗� */
+export function createEquipmentReceipt(
+  data: MesWmEquipmentReceiptApi.EquipmentReceipt,
+) {
+  return requestClient.post<number>('/mes/wm/equipment-receipt/create', data);
+}
+
+/** 淇敼璁惧鍏ュ簱鍗� */
+export function updateEquipmentReceipt(
+  data: MesWmEquipmentReceiptApi.EquipmentReceipt,
+) {
+  return requestClient.put('/mes/wm/equipment-receipt/update', data);
+}
+
+/** 鍒犻櫎璁惧鍏ュ簱鍗� */
+export function deleteEquipmentReceipt(id: number) {
+  return requestClient.delete(`/mes/wm/equipment-receipt/delete?id=${id}`);
+}
+
+/** 鏌ヨ鍙敤鐨勮澶囧叆搴撳崟瀹℃壒娴佺▼瀹氫箟 */
+export function getEquipmentReceiptApproveProcessList() {
+  return requestClient.get<{ id: string; name: string; key: string }[]>(
+    '/mes/wm/equipment-receipt/approval-process-list',
+  );
+}
+
+/** 鎻愪氦璁惧鍏ュ簱鍗曞鎵� */
+export function submitEquipmentReceiptApproval(
+  id: number,
+  processDefinitionKey: string,
+) {
+  return requestClient.put(
+    `/mes/wm/equipment-receipt/submit-approval?id=${id}&processDefinitionKey=${processDefinitionKey}`,
+  );
+}
+
+/** 鎵ц璁惧鍏ュ簱 */
+export function finishEquipmentReceipt(id: number) {
+  return requestClient.put(`/mes/wm/equipment-receipt/finish?id=${id}`);
+}
+
+/** 鍙栨秷璁惧鍏ュ簱鍗� */
+export function cancelEquipmentReceipt(id: number) {
+  return requestClient.put(`/mes/wm/equipment-receipt/cancel?id=${id}`);
+}
\ No newline at end of file
diff --git a/src/api/mes/wm/equipmentreceipt/line/index.ts b/src/api/mes/wm/equipmentreceipt/line/index.ts
new file mode 100644
index 0000000..a51138d
--- /dev/null
+++ b/src/api/mes/wm/equipmentreceipt/line/index.ts
@@ -0,0 +1,55 @@
+import { requestClient } from '#/api/request';
+
+export namespace MesWmEquipmentReceiptLineApi {
+  /** MES 璁惧鍏ュ簱鍗曡 */
+  export interface EquipmentReceiptLine {
+    id?: number; // 缂栧彿
+    receiptId?: number; // 鍏ュ簱鍗曠紪鍙�
+    machineryId?: number; // 璁惧鍙拌处缂栧彿
+    machineryCode?: string; // 璁惧缂栫爜
+    machineryName?: string; // 璁惧鍚嶇О
+    itemId?: number; // 璁惧鐗╂枡缂栧彿
+    itemCode?: string; // 鐗╂枡缂栫爜
+    itemName?: string; // 鐗╂枡鍚嶇О
+    specification?: string; // 瑙勬牸鍨嬪彿
+    unitMeasureName?: string; // 璁¢噺鍗曚綅鍚嶇О
+    quantity?: number; // 鍏ュ簱鏁伴噺
+    remark?: string; // 澶囨敞
+    createTime?: Date; // 鍒涘缓鏃堕棿
+  }
+}
+
+/** 鏌ヨ璁惧鍏ュ簱鍗曡鍒楄〃 */
+export function getEquipmentReceiptLineListByReceiptId(receiptId: number) {
+  return requestClient.get<
+    MesWmEquipmentReceiptLineApi.EquipmentReceiptLine[]
+  >(`/mes/wm/equipment-receipt-line/list-by-receipt-id?receiptId=${receiptId}`);
+}
+
+/** 鏌ヨ璁惧鍏ュ簱鍗曡璇︽儏 */
+export function getEquipmentReceiptLine(id: number) {
+  return requestClient.get<MesWmEquipmentReceiptLineApi.EquipmentReceiptLine>(
+    `/mes/wm/equipment-receipt-line/get?id=${id}`,
+  );
+}
+
+/** 鏂板璁惧鍏ュ簱鍗曡 */
+export function createEquipmentReceiptLine(
+  data: MesWmEquipmentReceiptLineApi.EquipmentReceiptLine,
+) {
+  return requestClient.post('/mes/wm/equipment-receipt-line/create', data);
+}
+
+/** 淇敼璁惧鍏ュ簱鍗曡 */
+export function updateEquipmentReceiptLine(
+  data: MesWmEquipmentReceiptLineApi.EquipmentReceiptLine,
+) {
+  return requestClient.put('/mes/wm/equipment-receipt-line/update', data);
+}
+
+/** 鍒犻櫎璁惧鍏ュ簱鍗曡 */
+export function deleteEquipmentReceiptLine(id: number) {
+  return requestClient.delete(
+    `/mes/wm/equipment-receipt-line/delete?id=${id}`,
+  );
+}
\ No newline at end of file
diff --git a/src/api/mes/wm/miscissue/index.ts b/src/api/mes/wm/miscissue/index.ts
index 1a54df4..7a7a8d2 100644
--- a/src/api/mes/wm/miscissue/index.ts
+++ b/src/api/mes/wm/miscissue/index.ts
@@ -14,6 +14,7 @@
     sourceDocCode?: string; // 鏉ユ簮鍗曟嵁缂栫爜
     issueDate?: number; // 鍑哄簱鏃ユ湡
     status?: number; // 鍗曟嵁鐘舵��
+    processInstanceId?: string; // 瀹℃壒娴佺▼瀹炰緥缂栧彿
     remark?: string; // 澶囨敞
     createTime?: Date; // 鍒涘缓鏃堕棿
   }
@@ -49,9 +50,21 @@
   return requestClient.delete(`/mes/wm/misc-issue/delete?id=${id}`);
 }
 
-/** 鎻愪氦鏉傞」鍑哄簱鍗� */
-export function submitMiscIssue(id: number) {
-  return requestClient.put(`/mes/wm/misc-issue/submit?id=${id}`);
+/** 鏌ヨ鍙敤鐨勬潅椤瑰嚭搴撳崟瀹℃壒娴佺▼瀹氫箟 */
+export function getMiscIssueApproveProcessList() {
+  return requestClient.get<{ id: string; name: string; key: string }[]>(
+    '/mes/wm/misc-issue/approval-process-list',
+  );
+}
+
+/** 鎻愪氦鏉傞」鍑哄簱鍗曞鎵� */
+export function submitMiscIssueApproval(
+  id: number,
+  processDefinitionKey: string,
+) {
+  return requestClient.put(
+    `/mes/wm/misc-issue/submit?id=${id}&processDefinitionKey=${processDefinitionKey}`,
+  );
 }
 
 /** 鎵ц鍑哄簱 */
diff --git a/src/api/mes/wm/miscreceipt/index.ts b/src/api/mes/wm/miscreceipt/index.ts
index 6c6b4de..ec3e2ee 100644
--- a/src/api/mes/wm/miscreceipt/index.ts
+++ b/src/api/mes/wm/miscreceipt/index.ts
@@ -14,6 +14,7 @@
     sourceDocCode?: string; // 鏉ユ簮鍗曟嵁缂栫爜
     receiptDate?: number; // 鍏ュ簱鏃ユ湡
     status?: number; // 鍗曟嵁鐘舵��
+    processInstanceId?: string; // 瀹℃壒娴佺▼瀹炰緥缂栧彿
     remark?: string; // 澶囨敞
     createTime?: Date; // 鍒涘缓鏃堕棿
   }
@@ -49,9 +50,21 @@
   return requestClient.delete(`/mes/wm/misc-receipt/delete?id=${id}`);
 }
 
-/** 鎻愪氦鏉傞」鍏ュ簱鍗� */
-export function submitMiscReceipt(id: number) {
-  return requestClient.put(`/mes/wm/misc-receipt/submit?id=${id}`);
+/** 鏌ヨ鍙敤鐨勬潅椤瑰叆搴撳崟瀹℃壒娴佺▼瀹氫箟 */
+export function getMiscReceiptApproveProcessList() {
+  return requestClient.get<{ id: string; name: string; key: string }[]>(
+    '/mes/wm/misc-receipt/approval-process-list',
+  );
+}
+
+/** 鎻愪氦鏉傞」鍏ュ簱鍗曞鎵� */
+export function submitMiscReceiptApproval(
+  id: number,
+  processDefinitionKey: string,
+) {
+  return requestClient.put(
+    `/mes/wm/misc-receipt/submit?id=${id}&processDefinitionKey=${processDefinitionKey}`,
+  );
 }
 
 /** 鎵ц鍏ュ簱 */
diff --git a/src/api/mes/wm/returnvendor/index.ts b/src/api/mes/wm/returnvendor/index.ts
index af94123..53d0b64 100644
--- a/src/api/mes/wm/returnvendor/index.ts
+++ b/src/api/mes/wm/returnvendor/index.ts
@@ -19,6 +19,7 @@
     transportCode?: string; // 杩愬崟鍙�
     transportTelephone?: string; // 鑱旂郴鐢佃瘽
     status?: number; // 鍗曟嵁鐘舵��
+    processInstanceId?: string; // 瀹℃壒娴佺▼瀹炰緥缂栧彿
     remark?: string; // 澶囨敞
     createTime?: Date; // 鍒涘缓鏃堕棿
   }
@@ -54,9 +55,21 @@
   return requestClient.delete(`/mes/wm/return-vendor/delete?id=${id}`);
 }
 
-/** 鎻愪氦渚涘簲鍟嗛��璐у崟 */
-export function submitReturnVendor(id: number) {
-  return requestClient.put(`/mes/wm/return-vendor/submit?id=${id}`);
+/** 鏌ヨ鍙敤鐨勪緵搴斿晢閫�璐у崟瀹℃壒娴佺▼瀹氫箟 */
+export function getReturnVendorApproveProcessList() {
+  return requestClient.get<{ id: string; name: string; key: string }[]>(
+    '/mes/wm/return-vendor/approval-process-list',
+  );
+}
+
+/** 鎻愪氦渚涘簲鍟嗛��璐у崟瀹℃壒 */
+export function submitReturnVendorApproval(
+  id: number,
+  processDefinitionKey: string,
+) {
+  return requestClient.put(
+    `/mes/wm/return-vendor/submit?id=${id}&processDefinitionKey=${processDefinitionKey}`,
+  );
 }
 
 /** 鎵ц鎷h揣 */
diff --git a/src/api/mes/wm/sn/index.ts b/src/api/mes/wm/sn/index.ts
index 553ac0a..bac987e 100644
--- a/src/api/mes/wm/sn/index.ts
+++ b/src/api/mes/wm/sn/index.ts
@@ -29,7 +29,60 @@
     unitName?: string; // 鍗曚綅鍚嶇О
     batchCode?: string; // 鎵规鍙�
     workOrderId?: number; // 鐢熶骇璁㈠崟缂栧彿
+    status?: number; // 鐘舵�侊紙1-鍦ㄥ簱 2-宸插嚭搴擄級
+    warehouseId?: number; // 鍦ㄥ簱浠撳簱 ID
+    locationId?: number; // 鍦ㄥ簱搴撳尯 ID
+    areaId?: number; // 鍦ㄥ簱搴撲綅 ID
     createTime?: Date; // 鐢熸垚鏃堕棿
+  }
+
+  /** MES SN 鐮佸崟浠惰鎯� */
+  export interface SnDetail {
+    id?: number; // 缂栧彿
+    code?: string; // SN 鐮�
+    itemId?: number; // 鐗╂枡缂栧彿
+    itemCode?: string; // 鐗╂枡缂栫爜
+    itemName?: string; // 鐗╂枡鍚嶇О
+    batchCode?: string; // 鎵规鍙�
+    status?: number; // 鐘舵�侊紙1-鍦ㄥ簱 2-宸插嚭搴擄級
+    warehouseId?: number; // 鍦ㄥ簱浠撳簱 ID
+    locationId?: number; // 鍦ㄥ簱搴撳尯 ID
+    areaId?: number; // 鍦ㄥ簱搴撲綅 ID
+    lastTransactionType?: number; // 鏈�杩戜簨鍔$被鍨嬶紙1-鍏ュ簱 2-鍑哄簱锛�
+    lastTransactionTypeName?: string; // 鏈�杩戜簨鍔$被鍨嬪悕绉�
+    lastTransactionQuantity?: number; // 鏈�杩戜簨鍔″彉鍔ㄦ暟閲�
+    lastTransactionBizType?: number; // 鏈�杩戜簨鍔′笟鍔$被鍨�
+    lastTransactionBizCode?: string; // 鏈�杩戜簨鍔′笟鍔″崟鍙�
+    lastTransactionTime?: string; // 鏈�杩戜簨鍔″彂鐢熸椂闂�
+  }
+
+  /** SN 鐮佸叆搴撶櫥璁板弬鏁� */
+  export interface SnInStock {
+    code?: string; // SN 鐮�
+    itemId?: number; // 鐗╂枡缂栧彿
+    warehouseId?: number; // 浠撳簱 ID
+    locationId?: number; // 搴撳尯 ID
+    areaId?: number; // 搴撲綅 ID
+    batchCode?: string; // 鎵规鍙�
+    remark?: string; // 澶囨敞
+  }
+
+  /** SN 鐮佸嚭搴撴牳閿�鍙傛暟 */
+  export interface SnOutStock {
+    code?: string; // SN 鐮�
+    itemId?: number; // 鐗╂枡缂栧彿
+    remark?: string; // 澶囨敞
+  }
+
+  /** SN 鐮佸湪搴�/鐘舵�佸垎椤垫煡璇㈠弬鏁� */
+  export interface SnInStockPageParams extends PageParam {
+    code?: string; // SN 鐮�
+    itemId?: number; // 鐗╂枡缂栧彿
+    batchCode?: string; // 鎵规鍙�
+    warehouseId?: number; // 浠撳簱 ID
+    locationId?: number; // 搴撳尯 ID
+    areaId?: number; // 搴撲綅 ID
+    status?: number; // 鐘舵�侊紙1-鍦ㄥ簱 2-宸插嚭搴擄級
   }
 
   /** MES SN 鐮佺敓鎴愬弬鏁� */
@@ -77,6 +130,31 @@
   });
 }
 
+/** SN 鐮佸叆搴撶櫥璁� */
+export function registerSnInStock(data: MesWmSnApi.SnInStock) {
+  return requestClient.post<boolean>('/mes/wm/sn/in-stock', data);
+}
+
+/** SN 鐮佸嚭搴撴牳閿� */
+export function outboundSn(data: MesWmSnApi.SnOutStock) {
+  return requestClient.post<boolean>('/mes/wm/sn/out-stock', data);
+}
+
+/** 鏌ヨ SN 鐮佸崟浠惰鎯� */
+export function getSnDetail(code: string) {
+  return requestClient.get<MesWmSnApi.SnDetail>('/mes/wm/sn/detail', {
+    params: { code },
+  });
+}
+
+/** 鏌ヨ SN 鐮佸湪搴�/鐘舵�佸垎椤� */
+export function getSnInStockPage(params: MesWmSnApi.SnInStockPageParams) {
+  return requestClient.get<PageResult<MesWmSnApi.Sn>>(
+    '/mes/wm/sn/in-stock-page',
+    { params },
+  );
+}
+
 /** 瀵煎嚭 SN 鐮佸垎缁� Excel */
 export function exportSnGroupExcel(params: MesWmSnApi.PageParams) {
   return requestClient.download('/mes/wm/sn/group-export-excel', { params });
diff --git a/src/api/mes/wm/stocktaking/plan/index.ts b/src/api/mes/wm/stocktaking/plan/index.ts
index c6f29f0..c4a4217 100644
--- a/src/api/mes/wm/stocktaking/plan/index.ts
+++ b/src/api/mes/wm/stocktaking/plan/index.ts
@@ -13,6 +13,9 @@
     endTime?: number; // 缁撴潫鏃堕棿
     blindFlag?: boolean; // 鏄惁鐩茬洏
     frozen?: boolean; // 鏄惁鍐荤粨搴撳瓨
+    autoGenerate?: boolean; // 鏄惁鑷姩鐢熸垚鐩樼偣浠诲姟
+    generateCron?: string; // 鑷姩鐢熸垚鍛ㄦ湡(Cron)
+    lastGenerateTime?: string; // 涓婃鐢熸垚浠诲姟鏃堕棿
     status?: number; // 鐘舵��
     remark?: string; // 澶囨敞
     createTime?: string; // 鍒涘缓鏃堕棿
diff --git a/src/api/mes/wm/stocktaking/task/index.ts b/src/api/mes/wm/stocktaking/task/index.ts
index 77622f3..985818a 100644
--- a/src/api/mes/wm/stocktaking/task/index.ts
+++ b/src/api/mes/wm/stocktaking/task/index.ts
@@ -20,6 +20,7 @@
     startTime?: number; // 寮�濮嬫椂闂�
     endTime?: number; // 缁撴潫鏃堕棿
     status?: number; // 鍗曟嵁鐘舵��
+    processInstanceId?: string; // 瀹℃壒娴佺▼瀹炰緥缂栧彿
     remark?: string; // 澶囨敞
     createTime?: string; // 鍒涘缓鏃堕棿
   }
@@ -59,7 +60,7 @@
   return requestClient.delete(`/mes/wm/stocktaking-task/delete?id=${id}`);
 }
 
-/** 鎻愪氦鐩樼偣浠诲姟 */
+/** 寮�濮嬬洏鐐癸紙鎻愪氦鐩樼偣浠诲姟锛� */
 export function submitStockTaking(id: number) {
   return requestClient.put('/mes/wm/stocktaking-task/submit', null, {
     params: { id },
@@ -73,9 +74,21 @@
   });
 }
 
-/** 鎵ц鐩樼偣浠诲姟 */
-export function finishStockTaking(id: number) {
-  return requestClient.put('/mes/wm/stocktaking-task/finish', { id });
+/** 鏌ヨ鍙敤鐨勭洏鐐圭粨鏋滃鎵规祦绋嬪畾涔� */
+export function getStockTakingApproveProcessList() {
+  return requestClient.get<{ id: string; name: string; key: string }[]>(
+    '/mes/wm/stocktaking-task/approval-process-list',
+  );
+}
+
+/** 鎻愪氦鐩樼偣缁撴灉瀹℃壒 */
+export function submitStockTakingApproval(
+  id: number,
+  processDefinitionKey: string,
+) {
+  return requestClient.put(
+    `/mes/wm/stocktaking-task/submit-approval?id=${id}&processDefinitionKey=${processDefinitionKey}`,
+  );
 }
 
 /** 瀵煎嚭鐩樼偣浠诲姟 */
diff --git a/src/components/table-action/icons.ts b/src/components/table-action/icons.ts
index c56cb1b..4f72d6d 100644
--- a/src/components/table-action/icons.ts
+++ b/src/components/table-action/icons.ts
@@ -14,4 +14,5 @@
   BOOK: 'lucide:book',
   AUDIT: 'lucide:file-check',
   CALC: 'lucide:calculator',
+  SCAN: 'lucide:scan-line',
 };
diff --git a/src/packages/constants/src/biz-mes-enum.ts b/src/packages/constants/src/biz-mes-enum.ts
index 14f9650..f052e36 100644
--- a/src/packages/constants/src/biz-mes-enum.ts
+++ b/src/packages/constants/src/biz-mes-enum.ts
@@ -187,6 +187,7 @@
   PRODUCTRECPT_CODE: 'PRODUCTRECPT_CODE',
   WM_MISC_ISSUE_CODE: 'WM_MISC_ISSUE_CODE',
   WM_MISC_RECEIPT_CODE: 'WM_MISC_RECEIPT_CODE',
+  WM_EQUIP_RECEIPT_CODE: 'WM_EQUIP_RECEIPT_CODE',
   WM_OUTSOURCE_ISSUE_CODE: 'WM_OUTSOURCE_ISSUE_CODE',
   WM_OUTSOURCE_RECEIPT_CODE: 'WM_OUTSOURCE_RECEIPT_CODE',
   WM_PACKAGE_CODE: 'WM_PACKAGE_CODE',
@@ -316,6 +317,7 @@
 /** MES 鏉傞」鍑哄簱鍗曠姸鎬佹灇涓� */
 export const MesWmMiscIssueStatusEnum = {
   PREPARE: MesOrderStatusConstants.DRAFT,
+  APPROVING: MesOrderStatusConstants.APPROVING,
   APPROVED: MesOrderStatusConstants.APPROVED,
   FINISHED: MesOrderStatusConstants.FINISHED,
   CANCELED: MesOrderStatusConstants.CANCELLED,
@@ -324,6 +326,7 @@
 /** MES 鏉傞」鍏ュ簱鍗曠姸鎬佹灇涓� */
 export const MesWmMiscReceiptStatusEnum = {
   PREPARE: MesOrderStatusConstants.DRAFT,
+  APPROVING: MesOrderStatusConstants.APPROVING,
   APPROVED: MesOrderStatusConstants.APPROVED,
   FINISHED: MesOrderStatusConstants.FINISHED,
   CANCELED: MesOrderStatusConstants.CANCELLED,
@@ -631,3 +634,18 @@
   USER = 604,
   CLIENT = 605,
 }
+
+/** MES SN 鐮佺姸鎬佹灇涓� */
+export enum MesWmSnStatusEnum {
+  IN_STOCK = 1, // 鍦ㄥ簱
+  OUT_STOCK = 2, // 宸插嚭搴�
+}
+
+/** MES 璁惧鍏ュ簱鍗曠姸鎬佹灇涓� */
+export const MesWmEquipmentReceiptStatusEnum = {
+  PREPARE: MesOrderStatusConstants.DRAFT,
+  APPROVING: MesOrderStatusConstants.APPROVING,
+  APPROVED: MesOrderStatusConstants.APPROVED,
+  FINISHED: MesOrderStatusConstants.FINISHED,
+  CANCELED: MesOrderStatusConstants.CANCELLED,
+} as const;
diff --git a/src/views/hrm/training/data.ts b/src/views/hrm/training/data.ts
new file mode 100644
index 0000000..e00aac7
--- /dev/null
+++ b/src/views/hrm/training/data.ts
@@ -0,0 +1,243 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { HrmTrainingApi } from '#/api/hrm/training';
+
+/** 鍩硅绫诲瀷閫夐」 */
+export const trainingTypeOptions = [
+  { label: '瀹夊叏鍩硅', value: 1 },
+  { label: '鎶�鑳藉煿璁�', value: 2 },
+  { label: '绠$悊鍩硅', value: 3 },
+  { label: '鍏朵粬', value: 4 },
+];
+
+/** 鍩硅鐘舵�侀�夐」 */
+export const trainingStatusOptions = [
+  { label: '鑽夌', value: 0 },
+  { label: '寰呭紑灞�', value: 10 },
+  { label: '宸插畬鎴�', value: 20 },
+  { label: '宸插彇娑�', value: 30 },
+];
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'edit' | 'detail';
+
+/** 琛ㄥ崟鐨勯厤缃」 */
+export function useFormSchema(formType: FormType): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: {
+        triggerFields: [''],
+        show: () => false,
+      },
+    },
+    {
+      fieldName: 'name',
+      label: '鍩硅鍚嶇О',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ煿璁悕绉�',
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'trainingType',
+      label: '鍩硅绫诲瀷',
+      component: 'Select',
+      componentProps: {
+        options: trainingTypeOptions,
+        placeholder: '璇烽�夋嫨鍩硅绫诲瀷',
+      },
+      rules: 'selectRequired',
+    },
+    {
+      fieldName: 'trainer',
+      label: '璁插笀',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ヨ甯�',
+      },
+    },
+    {
+      fieldName: 'location',
+      label: '鍩硅鍦扮偣',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ煿璁湴鐐�',
+      },
+    },
+    {
+      fieldName: 'hours',
+      label: '鍩硅瀛︽椂',
+      component: 'InputNumber',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ煿璁鏃�',
+        min: 0,
+        precision: 1,
+        style: { width: '100%' },
+      },
+    },
+    {
+      fieldName: 'startDate',
+      label: '寮�濮嬫棩鏈�',
+      component: 'DatePicker',
+      componentProps: {
+        placeholder: '閫夋嫨寮�濮嬫棩鏈�',
+        valueFormat: 'YYYY-MM-DD',
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'endDate',
+      label: '缁撴潫鏃ユ湡',
+      component: 'DatePicker',
+      componentProps: {
+        placeholder: '閫夋嫨缁撴潫鏃ユ湡',
+        valueFormat: 'YYYY-MM-DD',
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'issueCertificate',
+      label: '鑷姩鍙戣瘉',
+      component: 'Select',
+      componentProps: {
+        options: [
+          { label: '鏄�', value: 1 },
+          { label: '鍚�', value: 0 },
+        ],
+        placeholder: '鏄惁鑷姩鍙戣瘉',
+      },
+    },
+    {
+      fieldName: 'certValidityMonths',
+      label: '璇佷功鏈夋晥鏈�(鏈�)',
+      component: 'InputNumber',
+      componentProps: {
+        placeholder: '鑷姩鍙戣瘉鏃跺~鍐�',
+        min: 0,
+        precision: 0,
+        style: { width: '100%' },
+      },
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ娉�',
+        autoSize: { minRows: 1, maxRows: 2 },
+      },
+      formItemClass: 'col-span-2',
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'trainingNo',
+      label: '鍩硅缂栧彿',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ煿璁紪鍙�',
+      },
+    },
+    {
+      fieldName: 'name',
+      label: '鍩硅鍚嶇О',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ煿璁悕绉�',
+      },
+    },
+    {
+      fieldName: 'trainingType',
+      label: '鍩硅绫诲瀷',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: trainingTypeOptions,
+        placeholder: '璇烽�夋嫨鍩硅绫诲瀷',
+      },
+    },
+    {
+      fieldName: 'status',
+      label: '鐘舵��',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: trainingStatusOptions,
+        placeholder: '璇烽�夋嫨鐘舵��',
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions<HrmTrainingApi.Training>['columns'] {
+  return [
+    {
+      field: 'trainingNo',
+      title: '鍩硅缂栧彿',
+      width: 140,
+    },
+    {
+      field: 'name',
+      title: '鍩硅鍚嶇О',
+      minWidth: 180,
+    },
+    {
+      field: 'trainingTypeName',
+      title: '鍩硅绫诲瀷',
+      width: 100,
+    },
+    {
+      field: 'trainer',
+      title: '璁插笀',
+      width: 100,
+    },
+    {
+      field: 'location',
+      title: '鍩硅鍦扮偣',
+      minWidth: 120,
+    },
+    {
+      field: 'hours',
+      title: '瀛︽椂',
+      width: 80,
+    },
+    {
+      field: 'startDate',
+      title: '寮�濮嬫棩鏈�',
+      width: 105,
+      formatter: 'formatDate',
+    },
+    {
+      field: 'endDate',
+      title: '缁撴潫鏃ユ湡',
+      width: 105,
+      formatter: 'formatDate',
+    },
+    {
+      field: 'participantCount',
+      title: '鍙傝浜烘暟',
+      width: 90,
+    },
+    {
+      field: 'statusName',
+      title: '鐘舵��',
+      width: 90,
+      slots: { default: 'status' },
+    },
+    {
+      field: 'action',
+      title: '鎿嶄綔',
+      width: 240,
+      fixed: 'right',
+      slots: { default: 'actions' },
+    },
+  ];
+}
\ No newline at end of file
diff --git a/src/views/hrm/training/index.vue b/src/views/hrm/training/index.vue
new file mode 100644
index 0000000..30d130a
--- /dev/null
+++ b/src/views/hrm/training/index.vue
@@ -0,0 +1,263 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { HrmTrainingApi } from '#/api/hrm/training';
+
+import { ref } from 'vue';
+
+import { Page, useVbenModal } from '@vben/common-ui';
+import { isEmpty } from '@vben/utils';
+
+import { message, Modal, Tag } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+  cancelTraining,
+  completeTraining,
+  deleteTraining,
+  getTrainingPage,
+  startTraining,
+} from '#/api/hrm/training';
+import { $t } from '#/locales';
+
+import { useGridColumns, useGridFormSchema } from './data';
+import Form from './modules/form.vue';
+import Participants from './modules/participants.vue';
+
+/** HRM 鍩硅绠$悊鍒楄〃 */
+defineOptions({ name: 'HrmTraining' });
+
+const [FormModal, formModalApi] = useVbenModal({
+  connectedComponent: Form,
+  destroyOnClose: true,
+});
+const [ParticipantsModal, participantsModalApi] = useVbenModal({
+  connectedComponent: Participants,
+  destroyOnClose: true,
+});
+
+function handleRefresh() {
+  gridApi.query();
+}
+
+function handleCreate() {
+  formModalApi.setData({ formType: 'create' }).open();
+}
+
+function handleEdit(row: HrmTrainingApi.Training) {
+  formModalApi.setData({ formType: 'edit', id: row.id }).open();
+}
+
+function handleDetail(row: HrmTrainingApi.Training) {
+  formModalApi.setData({ formType: 'detail', id: row.id }).open();
+}
+
+function handleParticipants(row: HrmTrainingApi.Training) {
+  participantsModalApi.setData({ trainingId: row.id }).open();
+}
+
+async function handleDelete(ids: number[]) {
+  const hideLoading = message.loading({
+    content: $t('ui.actionMessage.deleting'),
+    duration: 0,
+  });
+  try {
+    await deleteTraining(ids);
+    message.success($t('ui.actionMessage.deleteSuccess'));
+    handleRefresh();
+  } finally {
+    hideLoading();
+  }
+}
+
+/** 寮�濮嬪煿璁細鑽夌 鈫� 寰呭紑灞� */
+function handleStart(row: HrmTrainingApi.Training) {
+  Modal.confirm({
+    title: '寮�濮嬪煿璁�',
+    content: `鏄惁寮�濮嬪煿璁��${row.name}銆嶏紵`,
+    onOk: async () => {
+      await startTraining(row.id!);
+      message.success('寮�濮嬪煿璁垚鍔�');
+      handleRefresh();
+    },
+  });
+}
+
+/** 瀹屾垚鍩硅锛氳崏绋�/寰呭紑灞� 鈫� 宸插畬鎴� */
+function handleComplete(row: HrmTrainingApi.Training) {
+  Modal.confirm({
+    title: '瀹屾垚鍩硅',
+    content: `鏄惁瀹屾垚鍩硅銆�${row.name}銆嶏紵`,
+    onOk: async () => {
+      await completeTraining(row.id!);
+      message.success('瀹屾垚鍩硅鎴愬姛');
+      handleRefresh();
+    },
+  });
+}
+
+/** 鍙栨秷鍩硅锛氳崏绋�/寰呭紑灞� 鈫� 宸插彇娑� */
+function handleCancel(row: HrmTrainingApi.Training) {
+  Modal.confirm({
+    title: '鍙栨秷鍩硅',
+    content: `鏄惁鍙栨秷鍩硅銆�${row.name}銆嶏紵`,
+    onOk: async () => {
+      await cancelTraining(row.id!);
+      message.success('鍙栨秷鍩硅鎴愬姛');
+      handleRefresh();
+    },
+  });
+}
+
+const checkedIds = ref<number[]>([]);
+function handleRowCheckboxChange({
+  records,
+}: {
+  records: HrmTrainingApi.Training[];
+}) {
+  checkedIds.value = records.map((item) => item.id!);
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  formOptions: {
+    schema: useGridFormSchema(),
+  },
+  gridOptions: {
+    columns: useGridColumns(),
+    height: 'auto',
+    keepSource: true,
+    proxyConfig: {
+      ajax: {
+        query: async ({ page }, formValues) => {
+          return await getTrainingPage({
+            pageNo: page.currentPage,
+            pageSize: page.pageSize,
+            ...formValues,
+          });
+        },
+      },
+    },
+    rowConfig: {
+      keyField: 'id',
+      isHover: true,
+    },
+    toolbarConfig: {
+      refresh: true,
+      search: true,
+    },
+  } as VxeTableGridOptions<HrmTrainingApi.Training>,
+  gridEvents: {
+    checkboxAll: handleRowCheckboxChange,
+    checkboxChange: handleRowCheckboxChange,
+  },
+});
+</script>
+
+<template>
+  <Page auto-content-height>
+    <FormModal @success="handleRefresh" />
+    <ParticipantsModal @success="handleRefresh" />
+    <Grid table-title="鍩硅绠$悊鍒楄〃">
+      <template #toolbar-tools>
+        <TableAction
+          :actions="[
+            {
+              label: $t('ui.actionTitle.create', ['鍩硅']),
+              type: 'primary',
+              icon: ACTION_ICON.ADD,
+              auth: ['hrm:training:create'],
+              onClick: handleCreate,
+            },
+            {
+              label: '鎵归噺鍒犻櫎',
+              type: 'primary',
+              danger: true,
+              disabled: isEmpty(checkedIds),
+              icon: ACTION_ICON.DELETE,
+              auth: ['hrm:training:delete'],
+              popConfirm: {
+                title: '鏄惁鍒犻櫎鎵�閫変腑鏁版嵁锛�',
+                confirm: handleDelete.bind(null, checkedIds),
+              },
+            },
+          ]"
+        />
+      </template>
+      <template #status="{ row }">
+        <Tag v-if="row.status === 0" color="default">鑽夌</Tag>
+        <Tag v-else-if="row.status === 10" color="processing">寰呭紑灞�</Tag>
+        <Tag v-else-if="row.status === 20" color="success">宸插畬鎴�</Tag>
+        <Tag v-else-if="row.status === 30" color="error">宸插彇娑�</Tag>
+        <Tag v-else color="default">-</Tag>
+      </template>
+      <template #actions="{ row }">
+        <TableAction
+          :actions="[
+            {
+              label: $t('common.detail'),
+              type: 'link',
+              icon: ACTION_ICON.VIEW,
+              auth: ['hrm:training:query'],
+              onClick: handleDetail.bind(null, row),
+            },
+            {
+              label: '缂栬緫',
+              type: 'link',
+              icon: ACTION_ICON.EDIT,
+              auth: ['hrm:training:update'],
+              ifShow: () => row.status === 0,
+              onClick: handleEdit.bind(null, row),
+            },
+            {
+              label: '鍙傝浜哄憳',
+              type: 'link',
+              icon: 'lucide:users',
+              auth: ['hrm:training:update'],
+              ifShow: () => row.status === 0 || row.status === 10 || row.status === 20,
+              onClick: handleParticipants.bind(null, row),
+            },
+            {
+              label: '寮�濮�',
+              type: 'link',
+              icon: 'lucide:play',
+              auth: ['hrm:training:update'],
+              ifShow: () => row.status === 0,
+              onClick: handleStart.bind(null, row),
+            },
+            {
+              label: '瀹屾垚',
+              type: 'link',
+              icon: 'lucide:check',
+              auth: ['hrm:training:update'],
+              ifShow: () => row.status === 0 || row.status === 10,
+              onClick: handleComplete.bind(null, row),
+            },
+            {
+              label: '鍙栨秷',
+              type: 'link',
+              danger: true,
+              icon: ACTION_ICON.CLOSE,
+              auth: ['hrm:training:update'],
+              ifShow: () => row.status === 0 || row.status === 10,
+              popConfirm: {
+                title: '鏄惁鍙栨秷鍩硅锛�',
+                confirm: handleCancel.bind(null, row),
+              },
+            },
+            {
+              label: $t('common.delete'),
+              type: 'link',
+              danger: true,
+              icon: ACTION_ICON.DELETE,
+              auth: ['hrm:training:delete'],
+              ifShow: () => row.status === 0,
+              popConfirm: {
+                title: $t('ui.actionMessage.deleteConfirm', [row.name]),
+                confirm: handleDelete.bind(null, [row.id!]),
+              },
+            },
+          ]"
+        />
+      </template>
+    </Grid>
+  </Page>
+</template>
\ No newline at end of file
diff --git a/src/views/hrm/training/modules/form.vue b/src/views/hrm/training/modules/form.vue
new file mode 100644
index 0000000..755e528
--- /dev/null
+++ b/src/views/hrm/training/modules/form.vue
@@ -0,0 +1,96 @@
+<script lang="ts" setup>
+import type { HrmTrainingApi, ReqTraining } from '#/api/hrm/training';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { createTraining, getTraining, updateTraining } from '#/api/hrm/training';
+import { $t } from '#/locales';
+
+import type { FormType } from '../data';
+import { useFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+const formData = ref<HrmTrainingApi.Training>();
+const formType = ref<FormType>('create');
+
+const getTitle = computed(() => {
+  if (formType.value === 'create') {
+    return $t('ui.actionTitle.create', ['鍩硅']);
+  } else if (formType.value === 'edit') {
+    return $t('ui.actionTitle.edit', ['鍩硅']);
+  }
+  return '鍩硅璇︽儏';
+});
+
+const [Form, formApi] = useVbenForm({
+  commonConfig: {
+    componentProps: {
+      class: 'w-full',
+    },
+    labelWidth: 120,
+  },
+  wrapperClass: 'grid-cols-3',
+  layout: 'vertical',
+  schema: useFormSchema(formType.value),
+  showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    const { valid } = await formApi.validate();
+    if (!valid) {
+      return;
+    }
+    modalApi.lock();
+    const values = await formApi.getValues<ReqTraining>();
+    // 鑷姩鍙戣瘉瀛楁锛氳〃鍗曠敤 0/1 涓嬫媺锛屾彁浜ゆ椂杞竷灏�
+    const data = {
+      ...values,
+      issueCertificate: Boolean(values.issueCertificate),
+    } as ReqTraining;
+    try {
+      await (formType.value === 'create' ? createTraining(data) : updateTraining(data));
+      await modalApi.close();
+      emit('success');
+      message.success($t('ui.actionMessage.operationSuccess'));
+    } finally {
+      modalApi.unlock();
+    }
+  },
+  async onOpenChange(isOpen: boolean) {
+    if (!isOpen) {
+      formData.value = undefined;
+      return;
+    }
+    const data = modalApi.getData<{ formType: FormType; id?: number }>();
+    formType.value = data.formType;
+    formApi.setDisabled(formType.value === 'detail');
+    formApi.updateSchema(useFormSchema(formType.value));
+    if (!data || !data.id) {
+      return;
+    }
+    modalApi.lock();
+    try {
+      formData.value = await getTraining(data.id);
+      await formApi.setValues({
+        ...formData.value,
+        // 甯冨皵 鈫� 0/1 涓嬫媺鍊�
+        issueCertificate: formData.value.issueCertificate ? 1 : 0,
+      });
+    } finally {
+      modalApi.unlock();
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal :title="getTitle" class="w-2/3" :show-confirm-button="formType !== 'detail'">
+    <Form class="mx-3" />
+  </Modal>
+</template>
\ No newline at end of file
diff --git a/src/views/hrm/training/modules/participants.vue b/src/views/hrm/training/modules/participants.vue
new file mode 100644
index 0000000..2a996e8
--- /dev/null
+++ b/src/views/hrm/training/modules/participants.vue
@@ -0,0 +1,278 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { HrmEmployeeApi } from '#/api/hrm/employee';
+import type { HrmTrainingApi } from '#/api/hrm/training';
+
+import { ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message, Modal as AntModal, Tag } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+  addTrainingParticipants,
+  deleteTrainingParticipants,
+  getTrainingParticipants,
+  updateTrainingParticipant,
+} from '#/api/hrm/training';
+import { $t } from '#/locales';
+
+import { HrmEmployeeSelectDialog } from '#/views/hrm/employee/components';
+
+defineOptions({ name: 'HrmTrainingParticipants' });
+
+const emit = defineEmits(['success']);
+
+const trainingId = ref<number>();
+
+/** 鍙傝浜哄憳缂栬緫琛ㄥ崟 */
+const participantFormData = ref<HrmTrainingApi.Participant>();
+const editVisible = ref(false);
+const editSaving = ref(false);
+const [EditForm, editFormApi] = useVbenForm({
+  commonConfig: {
+    componentProps: {
+      class: 'w-full',
+    },
+    labelWidth: 120,
+  },
+  wrapperClass: 'grid-cols-2',
+  layout: 'vertical',
+  schema: [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: {
+        triggerFields: [''],
+        show: () => false,
+      },
+    },
+    {
+      fieldName: 'attended',
+      label: '鏄惁鍙傝',
+      component: 'Select',
+      componentProps: {
+        options: [
+          { label: '鏄�', value: 1 },
+          { label: '鍚�', value: 0 },
+        ],
+        placeholder: '鏄惁鍙傝',
+      },
+    },
+    {
+      fieldName: 'score',
+      label: '鎴愮哗',
+      component: 'InputNumber',
+      componentProps: {
+        placeholder: '璇疯緭鍏ユ垚缁�',
+        min: 0,
+        precision: 1,
+        style: { width: '100%' },
+      },
+    },
+    {
+      fieldName: 'isPass',
+      label: '鏄惁鍚堟牸',
+      component: 'Select',
+      componentProps: {
+        options: [
+          { label: '鏄�', value: 1 },
+          { label: '鍚�', value: 0 },
+        ],
+        placeholder: '鏄惁鍚堟牸',
+      },
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ娉�',
+        autoSize: { minRows: 1, maxRows: 2 },
+      },
+      formItemClass: 'col-span-2',
+    },
+  ],
+  showDefaultActions: false,
+});
+
+/** 鎵撳紑缂栬緫寮圭獥 */
+function openEditForm(row: HrmTrainingApi.Participant) {
+  participantFormData.value = row;
+  editFormApi.setValues({
+    id: row.id,
+    attended: row.attended ? 1 : 0,
+    score: row.score,
+    isPass: row.isPass ? 1 : 0,
+    remark: row.remark,
+  });
+  editVisible.value = true;
+}
+
+/** 淇濆瓨鍙傝璁板綍 */
+async function handleEditSave() {
+  const { valid } = await editFormApi.validate();
+  if (!valid) {
+    return;
+  }
+  editSaving.value = true;
+  const values = await editFormApi.getValues<HrmTrainingApi.Participant>();
+  try {
+    await updateTrainingParticipant({
+      id: values.id,
+      attended: Boolean(values.attended),
+      score: values.score,
+      isPass: Boolean(values.isPass),
+      remark: values.remark,
+    } as HrmTrainingApi.Participant);
+    message.success($t('ui.actionMessage.operationSuccess'));
+    editVisible.value = false;
+    await loadParticipants();
+  } finally {
+    editSaving.value = false;
+  }
+}
+
+/** 鍒犻櫎鍙傝璁板綍 */
+async function handleDelete(row: HrmTrainingApi.Participant) {
+  await deleteTrainingParticipants([row.id!]);
+  message.success($t('ui.actionMessage.deleteSuccess'));
+  await loadParticipants();
+}
+
+/** 鍛樺伐閫夋嫨寮圭獥锛堝閫夛級 */
+const employeeDialogRef = ref<InstanceType<typeof HrmEmployeeSelectDialog>>();
+function openEmployeeSelect() {
+  employeeDialogRef.value?.open([], { multiple: true });
+}
+
+async function handleEmployeesSelected(rows: HrmEmployeeApi.Employee[]) {
+  const employeeIds = rows
+    .map((item) => item.id)
+    .filter((id): id is number => id !== undefined && id !== null);
+  if (employeeIds.length === 0) {
+    return;
+  }
+  try {
+    await addTrainingParticipants(trainingId.value!, employeeIds);
+    message.success('娣诲姞鍙傝浜哄憳鎴愬姛');
+    await loadParticipants();
+    emit('success');
+  } catch (e) {
+    // 鍚庣鍏滃簳鎻愮ず宸插脊鍑�
+  }
+}
+
+async function loadParticipants() {
+  if (!trainingId.value) {
+    return;
+  }
+  const list = await getTrainingParticipants(trainingId.value);
+  gridApi.grid.loadData(list);
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  gridOptions: {
+    columns: [
+      { type: 'seq', title: '搴忓彿', width: 60 },
+      { field: 'employeeNo', title: '鍛樺伐缂栧彿', width: 130 },
+      { field: 'employeeName', title: '鍛樺伐濮撳悕', minWidth: 120 },
+      {
+        field: 'attended',
+        title: '鏄惁鍙傝',
+        width: 100,
+        slots: { default: 'attended' },
+      },
+      {
+        field: 'score',
+        title: '鎴愮哗',
+        width: 90,
+      },
+      {
+        field: 'isPass',
+        title: '鏄惁鍚堟牸',
+        width: 100,
+        slots: { default: 'isPass' },
+      },
+      { field: 'remark', title: '澶囨敞', minWidth: 120 },
+      {
+        field: 'action',
+        title: '鎿嶄綔',
+        width: 140,
+        fixed: 'right',
+        slots: { default: 'actions' },
+      },
+    ],
+    height: 'auto',
+    keepSource: true,
+    rowConfig: {
+      keyField: 'id',
+      isHover: true,
+    },
+    toolbarConfig: {
+      refresh: false,
+      search: false,
+    },
+  } as VxeTableGridOptions<HrmTrainingApi.Participant>,
+});
+
+const [Modal, modalApi] = useVbenModal({
+  onOpenChange(isOpen) {
+    if (!isOpen) {
+      trainingId.value = undefined;
+      return;
+    }
+    const data = modalApi.getData<{ trainingId?: number }>();
+    trainingId.value = data?.trainingId;
+    if (trainingId.value) {
+      loadParticipants();
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal title="鍙傝浜哄憳绠$悊" class="w-[900px]" :show-confirm-button="false">
+    <HrmEmployeeSelectDialog
+      ref="employeeDialogRef"
+      @selected="handleEmployeesSelected"
+    />
+    <Grid table-title="鍙傝浜哄憳鍒楄〃">
+      <template #toolbar-tools>
+        <a-button type="primary" @click="openEmployeeSelect">
+          <i-ant-design-plus-outlined class="mr-1" />娣诲姞鍙傝浜哄憳
+        </a-button>
+      </template>
+      <template #attended="{ row }">
+        <Tag v-if="row.attended" color="success">鏄�</Tag>
+        <Tag v-else color="default">鍚�</Tag>
+      </template>
+      <template #isPass="{ row }">
+        <Tag v-if="row.isPass" color="success">鍚堟牸</Tag>
+        <Tag v-else-if="row.attended" color="error">涓嶅悎鏍�</Tag>
+        <Tag v-else color="default">-</Tag>
+      </template>
+      <template #actions="{ row }">
+        <a-button type="link" size="small" @click="openEditForm(row)">缂栬緫</a-button>
+        <a-popconfirm
+          :title="$t('ui.actionMessage.deleteConfirm', [row.employeeName ?? row.id])"
+          @confirm="handleDelete(row)"
+        >
+          <a-button type="link" danger size="small">鍒犻櫎</a-button>
+        </a-popconfirm>
+      </template>
+    </Grid>
+
+    <AntModal
+      v-model:open="editVisible"
+      title="缂栬緫鍙傝璁板綍"
+      width="480px"
+      :confirm-loading="editSaving"
+      @ok="handleEditSave"
+    >
+      <EditForm class="pt-4" />
+    </AntModal>
+  </Modal>
+</template>
\ No newline at end of file
diff --git a/src/views/hrm/workHourSummary/data.ts b/src/views/hrm/workHourSummary/data.ts
new file mode 100644
index 0000000..5156893
--- /dev/null
+++ b/src/views/hrm/workHourSummary/data.ts
@@ -0,0 +1,107 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { HrmWorkHourSummaryApi } from '#/api/hrm/work-hour-summary';
+
+import { getDeptList } from '#/api/system/dept';
+import { getEmployeeSimpleList } from '#/api/hrm/employee';
+import { handleTree } from '#/packages/utils/src';
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'period',
+      label: '鏍哥畻鍛ㄦ湡',
+      component: 'DatePicker',
+      componentProps: {
+        picker: 'month',
+        allowClear: true,
+        format: 'YYYY-MM',
+        valueFormat: 'YYYY-MM',
+        style: { width: '100%' },
+        placeholder: '璇烽�夋嫨鏍哥畻鍛ㄦ湡',
+      },
+    },
+    {
+      fieldName: 'employeeId',
+      label: '鍛樺伐',
+      component: 'ApiSelect',
+      componentProps: {
+        api: getEmployeeSimpleList,
+        labelField: 'name',
+        valueField: 'id',
+        allowClear: true,
+        placeholder: '璇烽�夋嫨鍛樺伐',
+      },
+    },
+    {
+      fieldName: 'deptId',
+      label: '閮ㄩ棬',
+      component: 'ApiTreeSelect',
+      componentProps: {
+        api: async () => {
+          const data = await getDeptList();
+          return handleTree(data);
+        },
+        labelField: 'name',
+        valueField: 'id',
+        childrenField: 'children',
+        allowClear: true,
+        placeholder: '璇烽�夋嫨閮ㄩ棬',
+        treeDefaultExpandAll: true,
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions<HrmWorkHourSummaryApi.WorkHourSummary>['columns'] {
+  return [
+    {
+      field: 'period',
+      title: '鏍哥畻鍛ㄦ湡',
+      width: 100,
+    },
+    {
+      field: 'employeeNo',
+      title: '鍛樺伐缂栧彿',
+      width: 130,
+    },
+    {
+      field: 'employeeName',
+      title: '鍛樺伐濮撳悕',
+      minWidth: 110,
+    },
+    {
+      field: 'deptName',
+      title: '閮ㄩ棬',
+      minWidth: 120,
+    },
+    {
+      field: 'workDays',
+      title: '鍑哄嫟澶╂暟',
+      width: 90,
+    },
+    {
+      field: 'workHours',
+      title: '姝e父宸ユ椂(h)',
+      width: 110,
+    },
+    {
+      field: 'overtimeHours',
+      title: '鍔犵彮宸ユ椂(h)',
+      width: 110,
+    },
+    {
+      field: 'remark',
+      title: '澶囨敞',
+      minWidth: 120,
+    },
+    {
+      field: 'createTime',
+      title: '鍒涘缓鏃堕棿',
+      width: 100,
+      formatter: 'formatDate',
+    },
+  ];
+}
\ No newline at end of file
diff --git a/src/views/hrm/workHourSummary/index.vue b/src/views/hrm/workHourSummary/index.vue
new file mode 100644
index 0000000..fa571f5
--- /dev/null
+++ b/src/views/hrm/workHourSummary/index.vue
@@ -0,0 +1,128 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { HrmWorkHourSummaryApi } from '#/api/hrm/work-hour-summary';
+
+import { ref } from 'vue';
+
+import { Page } from '#/packages/effects/common-ui/src';
+
+import { message, DatePicker, Form, FormItem, Modal } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+  generateWorkHourSummary,
+  getWorkHourSummaryPage,
+} from '#/api/hrm/work-hour-summary';
+
+import { useGridColumns, useGridFormSchema } from './data';
+
+/** HRM 鏈堝害宸ユ椂姹囨�� */
+defineOptions({ name: 'HrmWorkHourSummary' });
+
+// 鐢熸垚鏈堝害姹囨�诲脊绐�
+const generateVisible = ref(false);
+const generatePeriod = ref<string>();
+const generateLoading = ref(false);
+
+/** 鎵撳紑鐢熸垚寮圭獥锛屽甫鍑烘悳绱㈡潯浠朵腑鐨勬牳绠楀懆鏈� */
+function openGenerateModal() {
+  void gridApi.formApi.getValues().then((values) => {
+    generatePeriod.value = values.period;
+  });
+  generateVisible.value = true;
+}
+
+/** 鍒锋柊琛ㄦ牸 */
+function handleRefresh() {
+  gridApi.query();
+}
+
+/** 鎵ц鐢熸垚 */
+async function handleGenerate() {
+  if (!generatePeriod.value) {
+    message.warning('璇烽�夋嫨鏍哥畻鍛ㄦ湡');
+    return;
+  }
+  generateLoading.value = true;
+  try {
+    await generateWorkHourSummary(generatePeriod.value);
+    message.success('鏈堝害宸ユ椂姹囨�荤敓鎴愭垚鍔�');
+    generateVisible.value = false;
+    handleRefresh();
+  } finally {
+    generateLoading.value = false;
+  }
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  formOptions: {
+    schema: useGridFormSchema(),
+  },
+  gridOptions: {
+    columns: useGridColumns(),
+    height: 'auto',
+    keepSource: true,
+    proxyConfig: {
+      ajax: {
+        query: async ({ page }, formValues) => {
+          return await getWorkHourSummaryPage({
+            pageNo: page.currentPage,
+            pageSize: page.pageSize,
+            ...formValues,
+          });
+        },
+      },
+    },
+    rowConfig: {
+      keyField: 'id',
+      isHover: true,
+    },
+    toolbarConfig: {
+      refresh: true,
+      search: true,
+    },
+  } as VxeTableGridOptions<HrmWorkHourSummaryApi.WorkHourSummary>,
+});
+</script>
+
+<template>
+  <Page auto-content-height>
+    <!-- 鐢熸垚鏈堝害宸ユ椂姹囨�诲脊绐� -->
+    <Modal
+      v-model:open="generateVisible"
+      title="鐢熸垚鏈堝害宸ユ椂姹囨��"
+      width="420px"
+      :confirm-loading="generateLoading"
+      @ok="handleGenerate"
+    >
+      <Form layout="vertical" class="pt-4">
+        <FormItem label="鏍哥畻鍛ㄦ湡" required>
+          <DatePicker
+            v-model:value="generatePeriod"
+            picker="month"
+            format="YYYY-MM"
+            valueFormat="YYYY-MM"
+            placeholder="璇烽�夋嫨鏍哥畻鍛ㄦ湡"
+            style="width: 100%"
+          />
+        </FormItem>
+      </Form>
+    </Modal>
+
+    <Grid table-title="鏈堝害宸ユ椂姹囨�诲垪琛�">
+      <template #toolbar-tools>
+        <TableAction
+          :actions="[
+            {
+              label: '鐢熸垚鏈堝害姹囨��',
+              type: 'primary',
+              icon: ACTION_ICON.ADD,
+              auth: ['hrm:work-hour:generate'],
+              onClick: openGenerateModal,
+            },
+          ]"
+        />
+      </template>
+    </Grid>
+  </Page>
+</template>
\ No newline at end of file
diff --git a/src/views/wls/barcode/components/index.ts b/src/views/wls/barcode/components/index.ts
index 9bd45df..e67cfce 100644
--- a/src/views/wls/barcode/components/index.ts
+++ b/src/views/wls/barcode/components/index.ts
@@ -1,3 +1,4 @@
 export { default as Barcode } from './barcode.vue';
 export { default as BarcodeDetail } from './detail.vue';
 export { default as PrinterLabel } from './printer-label.vue';
+export { default as ScanModal } from './scan-modal.vue';
diff --git a/src/views/wls/barcode/components/scan-modal.vue b/src/views/wls/barcode/components/scan-modal.vue
new file mode 100644
index 0000000..d0838d7
--- /dev/null
+++ b/src/views/wls/barcode/components/scan-modal.vue
@@ -0,0 +1,178 @@
+<script lang="ts" setup>
+import { computed, ref } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+
+import {
+  Button,
+  Descriptions,
+  Divider,
+  Empty,
+  Image,
+  Input,
+  message,
+  Modal,
+  Tag,
+} from 'ant-design-vue';
+
+import {
+  parseBarcodeContent,
+  renderBarcodeImage,
+} from '#/api/mes/wm/barcode';
+import { DictTag } from '#/components/dict-tag';
+
+defineOptions({ name: 'WlsBarcodeScanModal' });
+
+const open = ref(false);
+const content = ref('');
+const parseResult = ref<Record<string, any>>();
+const imageBase64 = ref('');
+const parsing = ref(false);
+const rendering = ref(false);
+
+function openModal() {
+  open.value = true;
+  content.value = '';
+  parseResult.value = undefined;
+  imageBase64.value = '';
+}
+
+/** 瑙f瀽鏉$爜鍐呭 */
+async function handleParse() {
+  if (!content.value?.trim()) {
+    message.warning('璇峰厛鎵爜鏋壂鍏ユ垨鎵嬪姩杈撳叆鏉$爜鍐呭');
+    return;
+  }
+  parsing.value = true;
+  try {
+    parseResult.value = await parseBarcodeContent(content.value.trim());
+  } finally {
+    parsing.value = false;
+  }
+}
+
+/** 鏈嶅姟绔垚鍥鹃瑙堬紙PNG Base64锛屼笉钀藉簱锛� */
+async function handleRender() {
+  if (!content.value?.trim()) {
+    message.warning('璇峰厛鎵爜鏋壂鍏ユ垨鎵嬪姩杈撳叆鏉$爜鍐呭');
+    return;
+  }
+  rendering.value = true;
+  try {
+    const base64 = await renderBarcodeImage(
+      content.value.trim(),
+      parseResult.value?.format,
+    );
+    imageBase64.value = base64 ? `data:image/png;base64,${base64}` : '';
+    if (!base64) {
+      message.warning('鎴愬浘澶辫触锛岃妫�鏌ユ潯鐮佸唴瀹规槸鍚︾鍚堟墍閫夋牸寮�');
+    }
+  } finally {
+    rendering.value = false;
+  }
+}
+
+/** 鍖归厤鏂瑰紡鏂囨 */
+const matchedLabel = computed(() => {
+  if (parseResult.value?.matched) {
+    return '绮剧‘鍛戒腑';
+  }
+  if (parseResult.value?.fields) {
+    return '妯℃澘鍙嶈В';
+  }
+  return '鏈尮閰�';
+});
+const matchedColor = computed(() => {
+  if (parseResult.value?.matched) {
+    return 'green';
+  }
+  if (parseResult.value?.fields) {
+    return 'blue';
+  }
+  return 'orange';
+});
+
+defineExpose({ open: openModal });
+</script>
+
+<template>
+  <Modal v-model:open="open" title="鎵爜瑙f瀽 / 鏉$爜棰勮" width="720px">
+    <div class="space-y-4">
+      <div class="flex gap-2">
+        <Input
+          v-model:value="content"
+          placeholder="鎵爜鏋壂鍏ユ垨鎵嬪姩杈撳叆鏉$爜鍐呭锛屽洖杞﹁В鏋�"
+          @press-enter="handleParse"
+        />
+        <Button type="primary" :loading="parsing" @click="handleParse">
+          瑙f瀽
+        </Button>
+        <Button :loading="rendering" @click="handleRender">鎴愬浘棰勮</Button>
+      </div>
+
+      <div v-if="parseResult">
+        <Divider orientation="left">瑙f瀽缁撴灉</Divider>
+        <Descriptions
+          v-if="parseResult.bizType != null || parseResult.bizCode != null"
+          bordered
+          :column="2"
+          size="small"
+        >
+          <Descriptions.Item label="鍖归厤鏂瑰紡">
+            <Tag :color="matchedColor">{{ matchedLabel }}</Tag>
+          </Descriptions.Item>
+          <Descriptions.Item v-if="parseResult.format != null" label="鏉$爜鏍煎紡">
+            <DictTag
+              :type="DICT_TYPE.MES_WM_BARCODE_FORMAT"
+              :value="parseResult.format"
+            />
+          </Descriptions.Item>
+          <Descriptions.Item v-if="parseResult.bizType != null" label="涓氬姟绫诲瀷">
+            <DictTag
+              :type="DICT_TYPE.MES_WM_BARCODE_BIZ_TYPE"
+              :value="parseResult.bizType"
+            />
+          </Descriptions.Item>
+          <Descriptions.Item v-if="parseResult.barcodeId != null" label="鏉$爜缂栧彿">
+            {{ parseResult.barcodeId }}
+          </Descriptions.Item>
+          <Descriptions.Item label="涓氬姟缂栫爜">
+            {{ parseResult.bizCode || '-' }}
+          </Descriptions.Item>
+          <Descriptions.Item label="涓氬姟鍚嶇О">
+            {{ parseResult.bizName || '-' }}
+          </Descriptions.Item>
+          <Descriptions.Item v-if="parseResult.bizId != null" label="涓氬姟缂栧彿">
+            {{ parseResult.bizId }}
+          </Descriptions.Item>
+        </Descriptions>
+
+        <div v-if="parseResult.fields" class="mt-2">
+          <div class="mb-1 text-sm font-medium">妯℃澘瀛楁</div>
+          <div
+            v-for="(value, key) in parseResult.fields"
+            :key="key"
+            class="mb-1 flex items-center gap-2"
+          >
+            <Tag>{{ key }}</Tag>
+            <span>{{ value }}</span>
+          </div>
+        </div>
+        <Empty
+          v-else-if="parseResult.bizType == null && parseResult.bizCode == null"
+          description="鏈尮閰嶅埌鏉$爜璁板綍鎴栨潯鐮侀厤缃ā鏉�"
+        />
+      </div>
+
+      <div v-if="imageBase64">
+        <Divider orientation="left">鎴愬浘棰勮</Divider>
+        <div class="flex justify-center rounded bg-gray-100 p-4">
+          <Image :src="imageBase64" :width="220" />
+        </div>
+      </div>
+    </div>
+    <template #footer>
+      <Button @click="open = false">鍏抽棴</Button>
+    </template>
+  </Modal>
+</template>
\ No newline at end of file
diff --git a/src/views/wls/barcode/index.vue b/src/views/wls/barcode/index.vue
index f549980..82b53c2 100644
--- a/src/views/wls/barcode/index.vue
+++ b/src/views/wls/barcode/index.vue
@@ -18,7 +18,7 @@
 } from '#/api/mes/wm/barcode';
 import { $t } from '#/locales';
 
-import { Barcode, BarcodeDetail } from './components';
+import { Barcode, BarcodeDetail, ScanModal } from './components';
 import { useGridColumns, useGridFormSchema } from './data';
 import Form from './modules/form.vue';
 
@@ -30,6 +30,12 @@
 });
 
 const barcodeDetailRef = ref<InstanceType<typeof BarcodeDetail>>();
+const scanModalRef = ref<InstanceType<typeof ScanModal>>();
+
+/** 鎵爜瑙f瀽 / 鎴愬浘棰勮 */
+function handleScan() {
+  scanModalRef.value?.open();
+}
 
 const checkedIds = ref<number[]>([]);
 function handleRowCheckboxChange({
@@ -145,12 +151,20 @@
 <template>
   <Page auto-content-height><FormModal @success="handleRefresh" />
     <BarcodeDetail ref="barcodeDetailRef" />
+    <ScanModal ref="scanModalRef" />
 
     <Grid table-title="鏉$爜鍒楄〃">
       <template #toolbar-tools>
         <TableAction
           :actions="[
             {
+              label: '鎵爜瑙f瀽',
+              type: 'primary',
+              icon: ACTION_ICON.SCAN,
+              auth: ['mes:wm-barcode:query'],
+              onClick: handleScan,
+            },
+            {
               label: $t('ui.actionTitle.create', ['鏉$爜']),
               type: 'primary',
               icon: ACTION_ICON.ADD,
diff --git a/src/views/wls/components/process-select-modal.vue b/src/views/wls/components/process-select-modal.vue
new file mode 100644
index 0000000..d32d546
--- /dev/null
+++ b/src/views/wls/components/process-select-modal.vue
@@ -0,0 +1,83 @@
+<script lang="ts" setup>
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message, Select } from 'ant-design-vue';
+
+interface ProcessDefinition {
+  id: string;
+  name: string;
+  key: string;
+}
+
+type ProcessSubmitFn = (id: number, processDefinitionKey: string) => Promise<unknown>;
+
+const emit = defineEmits(['success']);
+
+const bizId = ref<number>();
+const processList = ref<ProcessDefinition[]>([]);
+const selectedProcessKey = ref<string>('');
+
+const processOptions = computed(() => {
+  return processList.value.map((p) => ({
+    label: p.name,
+    value: p.key,
+  }));
+});
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    if (!selectedProcessKey.value) {
+      message.warning('璇烽�夋嫨瀹℃壒娴佺▼');
+      return;
+    }
+    modalApi.lock();
+    try {
+      await modalApi.getData<{ submitFn: ProcessSubmitFn }>().submitFn(
+        bizId.value!,
+        selectedProcessKey.value,
+      );
+      await modalApi.close();
+      emit('success');
+      message.success('鎻愪氦瀹℃壒鎴愬姛');
+    } finally {
+      modalApi.unlock();
+    }
+  },
+  async onOpenChange(isOpen: boolean) {
+    if (!isOpen) {
+      bizId.value = undefined;
+      selectedProcessKey.value = '';
+      processList.value = [];
+      return;
+    }
+    const data = modalApi.getData<{
+      id: number;
+      processList: ProcessDefinition[];
+      submitFn: ProcessSubmitFn;
+    }>();
+    bizId.value = data.id;
+    processList.value = data.processList || [];
+    // 榛樿閫夋嫨绗竴涓祦绋�
+    const first = processList.value[0];
+    if (first) {
+      selectedProcessKey.value = first.key;
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal title="閫夋嫨瀹℃壒娴佺▼" class="w-1/3">
+    <div class="mb-4">
+      <span class="mr-2">瀹℃壒娴佺▼锛�</span>
+      <Select
+        v-model:value="selectedProcessKey"
+        :options="processOptions"
+        style="width: 200px"
+        placeholder="璇烽�夋嫨瀹℃壒娴佺▼"
+      />
+    </div>
+  </Modal>
+</template>
\ No newline at end of file
diff --git a/src/views/wls/equipmentreceipt/data.ts b/src/views/wls/equipmentreceipt/data.ts
new file mode 100644
index 0000000..d2db989
--- /dev/null
+++ b/src/views/wls/equipmentreceipt/data.ts
@@ -0,0 +1,342 @@
+import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesWmEquipmentReceiptApi } from '#/api/mes/wm/equipmentreceipt';
+import type { MesWmEquipmentReceiptLineApi } from '#/api/mes/wm/equipmentreceipt/line';
+
+import { h, markRaw } from 'vue';
+
+import { MesAutoCodeRuleCode, MesWmEquipmentReceiptStatusEnum } from '@vben/constants';
+
+import { Button } from 'ant-design-vue';
+
+import { generateAutoCode } from '#/api/mes/md/autocode/record';
+import { getRangePickerDefaultProps } from '#/utils';
+import { DvMachinerySelect } from '#/views/mes/dv/machinery/components';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'detail' | 'finish' | 'update';
+
+/** 琛ㄥ崟澶撮儴鏄惁鍙锛堣鎯呫�佹墽琛屽叆搴撴�侊級 */
+function isHeaderReadonly(formType: FormType): boolean {
+  return formType === 'detail' || formType === 'finish';
+}
+
+/** 璁惧鍏ュ簱鍗曠姸鎬佸睍绀烘槧灏� */
+export const EQUIPMENT_RECEIPT_STATUS_MAP: Record<
+  number,
+  { text: string; color: string }
+> = {
+  [MesWmEquipmentReceiptStatusEnum.PREPARE]: {
+    text: '鑽夌',
+    color: 'default',
+  },
+  [MesWmEquipmentReceiptStatusEnum.APPROVING]: {
+    text: '瀹℃壒涓�',
+    color: 'processing',
+  },
+  [MesWmEquipmentReceiptStatusEnum.APPROVED]: {
+    text: '宸插鎵�',
+    color: 'success',
+  },
+  [MesWmEquipmentReceiptStatusEnum.FINISHED]: {
+    text: '宸插畬鎴�',
+    color: 'cyan',
+  },
+  [MesWmEquipmentReceiptStatusEnum.CANCELED]: {
+    text: '宸插彇娑�',
+    color: 'warning',
+  },
+};
+
+/** 鏂板/淇敼鐨勮〃鍗� */
+export function useFormSchema(
+  formType: FormType,
+  formApi?: VbenFormApi,
+): VbenFormSchema[] {
+  const headerReadonly = isHeaderReadonly(formType);
+  return [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: {
+        triggerFields: [''],
+        show: () => false,
+      },
+    },
+    {
+      fieldName: 'status',
+      component: 'Input',
+      dependencies: {
+        triggerFields: [''],
+        show: () => false,
+      },
+    },
+    {
+      fieldName: 'code',
+      label: '鍗曟嵁缂栫爜',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ崟鎹紪鐮�',
+      },
+      rules: 'required',
+      suffix: headerReadonly
+        ? undefined
+        : () =>
+            h(
+              Button,
+              {
+                type: 'default',
+                onClick: async () => {
+                  const code = await generateAutoCode(
+                    MesAutoCodeRuleCode.WM_EQUIP_RECEIPT_CODE,
+                  );
+                  await formApi?.setFieldValue('code', code);
+                },
+              },
+              { default: () => '鐢熸垚' },
+            ),
+    },
+    {
+      fieldName: 'name',
+      label: '鍗曟嵁鍚嶇О',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ崟鎹悕绉�',
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'receiptDate',
+      label: '鍏ュ簱鏃ユ湡',
+      component: 'DatePicker',
+      componentProps: {
+        format: 'YYYY-MM-DD',
+        placeholder: '璇烽�夋嫨鍏ュ簱鏃ユ湡',
+        valueFormat: 'YYYY-MM-DD',
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'source',
+      label: '鍏ュ簱鏉ユ簮',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ叆搴撴潵婧�',
+      },
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      formItemClass: 'col-span-3',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ娉�',
+        rows: 3,
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'code',
+      label: '鍗曟嵁缂栫爜',
+      component: 'Input',
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇疯緭鍏ュ崟鎹紪鐮�',
+      },
+    },
+    {
+      fieldName: 'name',
+      label: '鍗曟嵁鍚嶇О',
+      component: 'Input',
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇疯緭鍏ュ崟鎹悕绉�',
+      },
+    },
+    {
+      fieldName: 'source',
+      label: '鍏ュ簱鏉ユ簮',
+      component: 'Input',
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇疯緭鍏ュ叆搴撴潵婧�',
+      },
+    },
+    {
+      fieldName: 'receiptDate',
+      label: '鍏ュ簱鏃ユ湡',
+      component: 'RangePicker',
+      componentProps: {
+        ...getRangePickerDefaultProps(),
+        allowClear: true,
+      },
+    },
+    {
+      fieldName: 'status',
+      label: '鍗曟嵁鐘舵��',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: Object.entries(EQUIPMENT_RECEIPT_STATUS_MAP).map(
+          ([value, item]) => ({
+            label: item.text,
+            value: Number(value),
+          }),
+        ),
+        placeholder: '璇烽�夋嫨鍗曟嵁鐘舵��',
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions<MesWmEquipmentReceiptApi.EquipmentReceipt>['columns'] {
+  return [
+    {
+      field: 'code',
+      title: '鍗曟嵁缂栫爜',
+      minWidth: 160,
+      slots: { default: 'code' },
+    },
+    {
+      field: 'name',
+      title: '鍗曟嵁鍚嶇О',
+      minWidth: 150,
+    },
+    {
+      field: 'source',
+      title: '鍏ュ簱鏉ユ簮',
+      minWidth: 120,
+    },
+    {
+      field: 'receiptDate',
+      title: '鍏ュ簱鏃ユ湡',
+      width: 180,
+      formatter: 'formatDate',
+    },
+    {
+      field: 'status',
+      title: '鍗曟嵁鐘舵��',
+      minWidth: 100,
+      slots: { default: 'status' },
+    },
+    {
+      title: '鎿嶄綔',
+      width: 240,
+      fixed: 'right',
+      slots: { default: 'actions' },
+    },
+  ];
+}
+
+/** 璁惧鍏ュ簱鍗曡瀛愯〃鐨勫瓧娈� */
+export function useLineGridColumns(
+  editable = true,
+): VxeTableGridOptions<MesWmEquipmentReceiptLineApi.EquipmentReceiptLine>['columns'] {
+  return [
+    {
+      field: 'machineryCode',
+      title: '璁惧缂栫爜',
+      minWidth: 120,
+    },
+    {
+      field: 'machineryName',
+      title: '璁惧鍚嶇О',
+      minWidth: 140,
+    },
+    {
+      field: 'itemCode',
+      title: '鐗╂枡缂栫爜',
+      minWidth: 120,
+    },
+    {
+      field: 'itemName',
+      title: '鐗╂枡鍚嶇О',
+      minWidth: 140,
+    },
+    {
+      field: 'specification',
+      title: '瑙勬牸鍨嬪彿',
+      minWidth: 120,
+    },
+    {
+      field: 'unitMeasureName',
+      title: '鍗曚綅',
+      width: 80,
+    },
+    {
+      field: 'quantity',
+      title: '鍏ュ簱鏁伴噺',
+      width: 100,
+    },
+    {
+      field: 'remark',
+      title: '澶囨敞',
+      minWidth: 120,
+    },
+    ...(editable
+      ? [
+          {
+            title: '鎿嶄綔',
+            width: 160,
+            fixed: 'right',
+            slots: { default: 'actions' },
+          } as const,
+        ]
+      : []),
+  ];
+}
+
+/** 璁惧鍏ュ簱鍗曡鏂板/淇敼鐨勮〃鍗� */
+export function useLineFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'itemId',
+      component: 'Input',
+      dependencies: {
+        triggerFields: [''],
+        show: () => false,
+      },
+    },
+    {
+      fieldName: 'machineryId',
+      label: '璁惧',
+      component: markRaw(DvMachinerySelect),
+      componentProps: {
+        placeholder: '璇烽�夋嫨璁惧',
+        onChange: async (machinery?: { itemId?: number }) => {
+          await formApi?.setFieldValue('itemId', machinery?.itemId);
+        },
+      },
+      rules: 'selectRequired',
+    },
+    {
+      fieldName: 'quantity',
+      label: '鍏ュ簱鏁伴噺',
+      component: 'InputNumber',
+      componentProps: {
+        class: '!w-full',
+        min: 0.01,
+        defaultValue: 1,
+        placeholder: '璇疯緭鍏ュ叆搴撴暟閲�',
+        precision: 2,
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      formItemClass: 'col-span-3',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ娉�',
+        rows: 3,
+      },
+    },
+  ];
+}
\ No newline at end of file
diff --git a/src/views/wls/equipmentreceipt/index.vue b/src/views/wls/equipmentreceipt/index.vue
new file mode 100644
index 0000000..fe1d8a0
--- /dev/null
+++ b/src/views/wls/equipmentreceipt/index.vue
@@ -0,0 +1,243 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesWmEquipmentReceiptApi } from '#/api/mes/wm/equipmentreceipt';
+
+import { useRouter } from 'vue-router';
+
+import { Page, useVbenModal } from '@vben/common-ui';
+import { MesWmEquipmentReceiptStatusEnum } from '@vben/constants';
+
+import { Button, message, Tag } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+  cancelEquipmentReceipt,
+  deleteEquipmentReceipt,
+  getEquipmentReceiptApproveProcessList,
+  getEquipmentReceiptPage,
+  submitEquipmentReceiptApproval,
+} from '#/api/mes/wm/equipmentreceipt';
+import { $t } from '#/locales';
+
+import ProcessSelectModal from '#/views/wls/components/process-select-modal.vue';
+import { EQUIPMENT_RECEIPT_STATUS_MAP, useGridColumns, useGridFormSchema } from './data';
+import Form from './modules/form.vue';
+
+const router = useRouter();
+
+const [FormModal, formModalApi] = useVbenModal({
+  connectedComponent: Form,
+  destroyOnClose: true,
+});
+
+const [ProcessModal, processModalApi] = useVbenModal({
+  connectedComponent: ProcessSelectModal,
+  destroyOnClose: true,
+});
+
+/** 鍒锋柊琛ㄦ牸 */
+function handleRefresh() {
+  gridApi.query();
+}
+
+/** 鍒涘缓璁惧鍏ュ簱鍗� */
+function handleCreate() {
+  formModalApi.setData({ formType: 'create' }).open();
+}
+
+/** 鏌ョ湅璁惧鍏ュ簱鍗� */
+function handleDetail(row: MesWmEquipmentReceiptApi.EquipmentReceipt) {
+  formModalApi.setData({ formType: 'detail', id: row.id }).open();
+}
+
+/** 缂栬緫璁惧鍏ュ簱鍗� */
+function handleEdit(row: MesWmEquipmentReceiptApi.EquipmentReceipt) {
+  formModalApi.setData({ formType: 'update', id: row.id }).open();
+}
+
+/** 鎵ц鍏ュ簱 */
+function handleFinish(row: MesWmEquipmentReceiptApi.EquipmentReceipt) {
+  formModalApi.setData({ formType: 'finish', id: row.id }).open();
+}
+
+/** 鎻愪氦瀹℃壒 - 鍏堣幏鍙栨祦绋嬪垪琛� */
+async function handleSubmitApproval(row: MesWmEquipmentReceiptApi.EquipmentReceipt) {
+  const hideLoading = message.loading({
+    content: '鑾峰彇瀹℃壒娴佺▼...',
+    duration: 0,
+  });
+  try {
+    const processList = await getEquipmentReceiptApproveProcessList();
+    hideLoading();
+    if (!processList || processList.length === 0) {
+      message.warning('鏆傛棤鍙敤瀹℃壒娴佺▼锛岃鍏堥厤缃� BPM 娴佺▼');
+      return;
+    }
+    processModalApi
+      .setData({
+        id: row.id,
+        processList,
+        submitFn: submitEquipmentReceiptApproval,
+      })
+      .open();
+  } catch {
+    hideLoading();
+  }
+}
+
+/** 鏌ョ湅瀹℃壒璇︽儏 */
+function handleProcessDetail(row: MesWmEquipmentReceiptApi.EquipmentReceipt) {
+  if (!row.processInstanceId) {
+    message.warning('璇ュ崟鎹皻鏈彁浜ゅ鎵�');
+    return;
+  }
+  router.push({
+    name: 'BpmProcessInstanceDetail',
+    query: { id: row.processInstanceId },
+  });
+}
+
+/** 鍒犻櫎璁惧鍏ュ簱鍗� */
+async function handleDelete(row: MesWmEquipmentReceiptApi.EquipmentReceipt) {
+  const hideLoading = message.loading({
+    content: $t('ui.actionMessage.deleting', [row.code]),
+    duration: 0,
+  });
+  try {
+    await deleteEquipmentReceipt(row.id!);
+    message.success($t('ui.actionMessage.deleteSuccess', [row.code]));
+    handleRefresh();
+  } finally {
+    hideLoading();
+  }
+}
+
+/** 鍙栨秷璁惧鍏ュ簱鍗� */
+async function handleCancel(row: MesWmEquipmentReceiptApi.EquipmentReceipt) {
+  await cancelEquipmentReceipt(row.id!);
+  message.success('鍙栨秷鎴愬姛');
+  handleRefresh();
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  formOptions: {
+    schema: useGridFormSchema(),
+  },
+  gridOptions: {
+    columns: useGridColumns(),
+    height: 'auto',
+    keepSource: true,
+    proxyConfig: {
+      ajax: {
+        query: async ({ page }, formValues) => {
+          return await getEquipmentReceiptPage({
+            pageNo: page.currentPage,
+            pageSize: page.pageSize,
+            ...formValues,
+          });
+        },
+      },
+    },
+    rowConfig: {
+      keyField: 'id',
+      isHover: true,
+    },
+    toolbarConfig: {
+      refresh: true,
+      search: true,
+    },
+  } as VxeTableGridOptions<MesWmEquipmentReceiptApi.EquipmentReceipt>,
+});
+
+const StatusEnum = MesWmEquipmentReceiptStatusEnum;
+</script>
+
+<template>
+  <Page auto-content-height><FormModal @success="handleRefresh" />
+    <ProcessModal @success="handleRefresh" />
+
+    <Grid table-title="璁惧鍏ュ簱鍗曞垪琛�">
+      <template #toolbar-tools>
+        <TableAction
+          :actions="[
+            {
+              label: $t('ui.actionTitle.create', ['璁惧鍏ュ簱鍗�']),
+              type: 'primary',
+              icon: ACTION_ICON.ADD,
+              auth: ['mes:wm:equipment-receipt:create'],
+              onClick: handleCreate,
+            },
+          ]"
+        />
+      </template>
+      <template #code="{ row }">
+        <Button type="link" @click="handleDetail(row)">
+          {{ row.code }}
+        </Button>
+      </template>
+      <template #status="{ row }">
+        <Tag :color="EQUIPMENT_RECEIPT_STATUS_MAP[row.status ?? 0]?.color">
+          {{ EQUIPMENT_RECEIPT_STATUS_MAP[row.status ?? 0]?.text ?? row.status }}
+        </Tag>
+      </template>
+      <template #actions="{ row }">
+        <TableAction
+          :actions="[
+            {
+              label: $t('common.edit'),
+              type: 'link',
+              icon: ACTION_ICON.EDIT,
+              auth: ['mes:wm:equipment-receipt:update'],
+              ifShow: row.status === StatusEnum.PREPARE,
+              onClick: handleEdit.bind(null, row),
+            },
+            {
+              label: $t('common.delete'),
+              type: 'link',
+              danger: true,
+              icon: ACTION_ICON.DELETE,
+              auth: ['mes:wm:equipment-receipt:delete'],
+              ifShow: row.status === StatusEnum.PREPARE,
+              popConfirm: {
+                title: $t('ui.actionMessage.deleteConfirm', [row.code]),
+                confirm: handleDelete.bind(null, row),
+              },
+            },
+            {
+              label: '鎻愪氦瀹℃壒',
+              type: 'link',
+              auth: ['mes:wm:equipment-receipt:submit'],
+              ifShow: row.status === StatusEnum.PREPARE,
+              onClick: handleSubmitApproval.bind(null, row),
+            },
+            {
+              label: '鏌ョ湅瀹℃壒',
+              type: 'link',
+              auth: ['mes:wm:equipment-receipt:query'],
+              ifShow: row.status === StatusEnum.APPROVING,
+              onClick: handleProcessDetail.bind(null, row),
+            },
+            {
+              label: '鎵ц鍏ュ簱',
+              type: 'link',
+              auth: ['mes:wm:equipment-receipt:finish'],
+              ifShow: row.status === StatusEnum.APPROVED,
+              onClick: handleFinish.bind(null, row),
+            },
+            {
+              label: '鍙栨秷',
+              type: 'link',
+              danger: true,
+              auth: ['mes:wm:equipment-receipt:cancel'],
+              ifShow: row.status === StatusEnum.APPROVED,
+              popConfirm: {
+                title: '纭鍙栨秷璇ヨ澶囧叆搴撳崟鍚楋紵',
+                confirm: handleCancel.bind(null, row),
+              },
+            },
+          ]"
+        />
+      </template>
+    </Grid>
+  </Page>
+</template>
\ No newline at end of file
diff --git a/src/views/wls/equipmentreceipt/modules/form.vue b/src/views/wls/equipmentreceipt/modules/form.vue
new file mode 100644
index 0000000..1a8f0f8
--- /dev/null
+++ b/src/views/wls/equipmentreceipt/modules/form.vue
@@ -0,0 +1,163 @@
+<script lang="ts" setup>
+import type { FormType } from '../data';
+
+import type { MesWmEquipmentReceiptApi } from '#/api/mes/wm/equipmentreceipt';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+import { MesWmEquipmentReceiptStatusEnum } from '@vben/constants';
+
+import { Button, Divider, message, Popconfirm } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+  createEquipmentReceipt,
+  finishEquipmentReceipt,
+  getEquipmentReceipt,
+  updateEquipmentReceipt,
+} from '#/api/mes/wm/equipmentreceipt';
+import { $t } from '#/locales';
+
+import { useFormSchema } from '../data';
+import LineList from './line-list.vue';
+
+const emit = defineEmits(['success']);
+const formType = ref<FormType>('create');
+const formData = ref<MesWmEquipmentReceiptApi.EquipmentReceipt>();
+const originalSnapshot = ref(''); // 琛ㄥ崟鍘熷鏁版嵁蹇収锛岀敤浜庢彁浜ゆ椂璺宠繃鏈彉鏇寸殑淇濆瓨璇锋眰
+const isEditable = computed(() =>
+  // 鏄惁涓虹紪杈戞ā寮忥紙鍙繚瀛橈級
+  ['create', 'update'].includes(formType.value),
+);
+const isFinish = computed(() => formType.value === 'finish'); // 鏄惁涓烘墽琛屽叆搴撴ā寮�
+const getTitle = computed(() => {
+  if (formType.value === 'detail') {
+    return $t('ui.actionTitle.view', ['璁惧鍏ュ簱鍗�']);
+  }
+  if (formType.value === 'finish') {
+    return '鎵ц鍏ュ簱';
+  }
+  return formType.value === 'update'
+    ? $t('ui.actionTitle.edit', ['璁惧鍏ュ簱鍗�'])
+    : $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',
+});
+
+/** 鎵ц鍏ュ簱 */
+async function handleFinish() {
+  if (!formData.value?.id) {
+    return;
+  }
+  modalApi.lock();
+  try {
+    await finishEquipmentReceipt(formData.value.id);
+    message.success('鍏ュ簱鎴愬姛');
+    await modalApi.close();
+    emit('success');
+  } finally {
+    modalApi.unlock();
+  }
+}
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    if (!isEditable.value) {
+      await modalApi.close();
+      return;
+    }
+    const { valid } = await formApi.validate();
+    if (!valid) {
+      return;
+    }
+    modalApi.lock();
+    // 鎻愪氦琛ㄥ崟
+    const data =
+      (await formApi.getValues()) as MesWmEquipmentReceiptApi.EquipmentReceipt;
+    try {
+      if (formData.value?.id) {
+        await updateEquipmentReceipt({ ...formData.value, ...data });
+        formData.value = { ...formData.value, ...data };
+      } else {
+        const id = await createEquipmentReceipt(data);
+        formData.value = {
+          ...data,
+          id,
+          status: MesWmEquipmentReceiptStatusEnum.PREPARE,
+        };
+        await formApi.setFieldValue('id', id);
+        await formApi.setFieldValue('status', formData.value.status);
+        formType.value = 'update';
+      }
+      originalSnapshot.value = JSON.stringify(await formApi.getValues());
+      emit('success');
+      message.success($t('ui.actionMessage.operationSuccess'));
+    } finally {
+      modalApi.unlock();
+    }
+  },
+  async onOpenChange(isOpen: boolean) {
+    if (!isOpen) {
+      formData.value = undefined;
+      originalSnapshot.value = '';
+      return;
+    }
+    // 鍔犺浇鏁版嵁
+    const data = modalApi.getData<{ formType: FormType; id?: number }>();
+    formType.value = data.formType;
+    formApi.setState({ schema: useFormSchema(formType.value, formApi) });
+    formApi.setDisabled(
+      formType.value === 'detail' || formType.value === 'finish',
+    );
+    modalApi.setState({ showConfirmButton: isEditable.value });
+    if (data?.id) {
+      modalApi.lock();
+      try {
+        formData.value = await getEquipmentReceipt(data.id);
+        // 璁剧疆鍒� values
+        await formApi.setValues(formData.value);
+      } finally {
+        modalApi.unlock();
+      }
+    }
+    originalSnapshot.value = JSON.stringify(await formApi.getValues());
+  },
+});
+</script>
+
+<template>
+  <Modal :title="getTitle" class="w-3/5">
+    <Form class="mx-4" />
+    <!-- 闈炴柊寤烘ā寮忓睍绀鸿澶囨槑缁� -->
+    <template v-if="formData?.id">
+      <Divider>璁惧鏄庣粏</Divider>
+      <div class="mx-4">
+        <LineList :form-type="formType" :receipt-id="formData.id" />
+      </div>
+    </template>
+    <template #prepend-footer>
+      <div class="flex flex-auto items-center gap-2">
+        <Popconfirm
+          v-if="isFinish"
+          title="纭鎵ц鍏ュ簱锛熸墽琛屽悗灏嗚澶囪鍏ヨ澶囦粨搴撳瓨鍙拌处銆�"
+          @confirm="handleFinish"
+        >
+          <Button type="primary">鎵ц鍏ュ簱</Button>
+        </Popconfirm>
+      </div>
+    </template>
+  </Modal>
+</template>
\ No newline at end of file
diff --git a/src/views/wls/equipmentreceipt/modules/line-form.vue b/src/views/wls/equipmentreceipt/modules/line-form.vue
new file mode 100644
index 0000000..838324c
--- /dev/null
+++ b/src/views/wls/equipmentreceipt/modules/line-form.vue
@@ -0,0 +1,98 @@
+<script lang="ts" setup>
+import type { MesWmEquipmentReceiptLineApi } from '#/api/mes/wm/equipmentreceipt/line';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+  createEquipmentReceiptLine,
+  getEquipmentReceiptLine,
+  updateEquipmentReceiptLine,
+} from '#/api/mes/wm/equipmentreceipt/line';
+import { $t } from '#/locales';
+
+import { useLineFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+const formData = ref<MesWmEquipmentReceiptLineApi.EquipmentReceiptLine>();
+const receiptId = ref<number>(); // 鎵�灞炲叆搴撳崟缂栧彿
+
+const getTitle = computed(() => {
+  return formData.value?.id
+    ? $t('ui.actionTitle.edit', ['璁惧'])
+    : $t('ui.actionTitle.create', ['璁惧']);
+});
+
+const [Form, formApi] = useVbenForm({
+  commonConfig: {
+    componentProps: {
+      class: 'w-full',
+    },
+    formItemClass: 'col-span-1',
+    labelWidth: 90,
+  },
+  layout: 'horizontal',
+  schema: useLineFormSchema(),
+  showDefaultActions: false,
+  wrapperClass: 'grid-cols-3',
+});
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    const { valid } = await formApi.validate();
+    if (!valid) {
+      return;
+    }
+    modalApi.lock();
+    // 鎻愪氦琛ㄥ崟
+    const data =
+      (await formApi.getValues()) as MesWmEquipmentReceiptLineApi.EquipmentReceiptLine;
+    data.receiptId = receiptId.value;
+    try {
+      await (formData.value?.id
+        ? updateEquipmentReceiptLine({ ...data, id: formData.value.id })
+        : createEquipmentReceiptLine(data));
+      // 鍏抽棴骞舵彁绀�
+      await modalApi.close();
+      emit('success');
+      message.success($t('ui.actionMessage.operationSuccess'));
+    } finally {
+      modalApi.unlock();
+    }
+  },
+  async onOpenChange(isOpen: boolean) {
+    if (!isOpen) {
+      formData.value = undefined;
+      return;
+    }
+    formApi.setState({ schema: useLineFormSchema(formApi) });
+    // 鍔犺浇鏁版嵁
+    const data = modalApi.getData<{
+      id?: number;
+      receiptId: number;
+    }>();
+    receiptId.value = data.receiptId;
+    if (!data.id) {
+      return;
+    }
+    modalApi.lock();
+    try {
+      formData.value = await getEquipmentReceiptLine(data.id);
+      // 璁剧疆鍒� values
+      await formApi.setValues(formData.value);
+    } finally {
+      modalApi.unlock();
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal :title="getTitle" class="w-3/5">
+    <Form class="mx-4" />
+  </Modal>
+</template>
\ No newline at end of file
diff --git a/src/views/wls/equipmentreceipt/modules/line-list.vue b/src/views/wls/equipmentreceipt/modules/line-list.vue
new file mode 100644
index 0000000..e156dab
--- /dev/null
+++ b/src/views/wls/equipmentreceipt/modules/line-list.vue
@@ -0,0 +1,142 @@
+<script lang="ts" setup>
+import type { FormType } from '../data';
+
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesWmEquipmentReceiptLineApi } from '#/api/mes/wm/equipmentreceipt/line';
+
+import { computed } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+  deleteEquipmentReceiptLine,
+  getEquipmentReceiptLineListByReceiptId,
+} from '#/api/mes/wm/equipmentreceipt/line';
+import { $t } from '#/locales';
+
+import { useLineGridColumns } from '../data';
+import LineForm from './line-form.vue';
+
+const props = defineProps<{
+  formType: FormType;
+  receiptId: number;
+}>();
+
+const isEditable = computed(() =>
+  // 鏄惁鍙紪杈戞槑缁嗚
+  ['create', 'update'].includes(props.formType),
+);
+
+const [LineFormModal, lineFormModalApi] = useVbenModal({
+  connectedComponent: LineForm,
+  destroyOnClose: true,
+});
+
+/** 鍒锋柊琛ㄦ牸 */
+function handleRefresh() {
+  gridApi.query();
+}
+
+/** 娣诲姞璁惧 */
+function handleCreate() {
+  lineFormModalApi.setData({ receiptId: props.receiptId }).open();
+}
+
+/** 缂栬緫璁惧 */
+function handleEdit(row: MesWmEquipmentReceiptLineApi.EquipmentReceiptLine) {
+  lineFormModalApi.setData({ id: row.id, receiptId: props.receiptId }).open();
+}
+
+/** 鍒犻櫎璁惧 */
+async function handleDelete(row: MesWmEquipmentReceiptLineApi.EquipmentReceiptLine) {
+  const hideLoading = message.loading({
+    content: $t('ui.actionMessage.deleting', [row.machineryName]),
+    duration: 0,
+  });
+  try {
+    await deleteEquipmentReceiptLine(row.id!);
+    message.success($t('ui.actionMessage.deleteSuccess', [row.machineryName]));
+    handleRefresh();
+  } finally {
+    hideLoading();
+  }
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  gridOptions: {
+    columns: useLineGridColumns(isEditable.value),
+    height: 360,
+    keepSource: true,
+    pagerConfig: {
+      enabled: false,
+    },
+    proxyConfig: {
+      ajax: {
+        query: async () => {
+          if (!props.receiptId) {
+            return { list: [], total: 0 };
+          }
+          const list = await getEquipmentReceiptLineListByReceiptId(
+            props.receiptId,
+          );
+          return { list, total: list.length };
+        },
+      },
+    },
+    rowConfig: {
+      keyField: 'id',
+      isHover: true,
+    },
+    toolbarConfig: {
+      refresh: true,
+    },
+  } as VxeTableGridOptions<MesWmEquipmentReceiptLineApi.EquipmentReceiptLine>,
+});
+</script>
+
+<template>
+  <div>
+    <LineFormModal @success="handleRefresh" />
+    <Grid table-title="璁惧鏄庣粏">
+      <template v-if="isEditable" #toolbar-tools>
+        <TableAction
+          :actions="[
+            {
+              label: '娣诲姞璁惧',
+              type: 'primary',
+              icon: ACTION_ICON.ADD,
+              onClick: handleCreate,
+            },
+          ]"
+        />
+      </template>
+      <template #actions="{ row }">
+        <TableAction
+          :actions="[
+            {
+              label: $t('common.edit'),
+              type: 'link',
+              icon: ACTION_ICON.EDIT,
+              ifShow: isEditable,
+              onClick: handleEdit.bind(null, row),
+            },
+            {
+              label: $t('common.delete'),
+              type: 'link',
+              danger: true,
+              icon: ACTION_ICON.DELETE,
+              ifShow: isEditable,
+              popConfirm: {
+                title: $t('ui.actionMessage.deleteConfirm', [row.machineryName]),
+                confirm: handleDelete.bind(null, row),
+              },
+            },
+          ]"
+        />
+      </template>
+    </Grid>
+  </div>
+</template>
\ No newline at end of file
diff --git a/src/views/wls/miscissue/index.vue b/src/views/wls/miscissue/index.vue
index 90406ca..382faf1 100644
--- a/src/views/wls/miscissue/index.vue
+++ b/src/views/wls/miscissue/index.vue
@@ -2,6 +2,8 @@
 import type { VxeTableGridOptions } from '#/adapter/vxe-table';
 import type { MesWmMiscIssueApi } from '#/api/mes/wm/miscissue';
 
+import { useRouter } from 'vue-router';
+
 import { Page, useVbenModal } from '@vben/common-ui';
 import { MesWmMiscIssueStatusEnum } from '@vben/constants';
 import { downloadFileFromBlobPart } from '@vben/utils';
@@ -13,15 +15,25 @@
   cancelMiscIssue,
   deleteMiscIssue,
   exportMiscIssue,
+  getMiscIssueApproveProcessList,
   getMiscIssuePage,
+  submitMiscIssueApproval,
 } from '#/api/mes/wm/miscissue';
 import { $t } from '#/locales';
 
+import ProcessSelectModal from '#/views/wls/components/process-select-modal.vue';
 import { useGridColumns, useGridFormSchema } from './data';
 import Form from './modules/form.vue';
 
+const router = useRouter();
+
 const [FormModal, formModalApi] = useVbenModal({
   connectedComponent: Form,
+  destroyOnClose: true,
+});
+
+const [ProcessModal, processModalApi] = useVbenModal({
+  connectedComponent: ProcessSelectModal,
   destroyOnClose: true,
 });
 
@@ -48,6 +60,43 @@
 /** 鎵ц鍑哄簱 */
 function handleFinish(row: MesWmMiscIssueApi.MiscIssue) {
   formModalApi.setData({ formType: 'finish', id: row.id }).open();
+}
+
+/** 鎻愪氦瀹℃壒 - 鍏堣幏鍙栨祦绋嬪垪琛� */
+async function handleSubmitApproval(row: MesWmMiscIssueApi.MiscIssue) {
+  const hideLoading = message.loading({
+    content: '鑾峰彇瀹℃壒娴佺▼...',
+    duration: 0,
+  });
+  try {
+    const processList = await getMiscIssueApproveProcessList();
+    hideLoading();
+    if (!processList || processList.length === 0) {
+      message.warning('鏆傛棤鍙敤瀹℃壒娴佺▼锛岃鍏堥厤缃� BPM 娴佺▼');
+      return;
+    }
+    processModalApi
+      .setData({
+        id: row.id,
+        processList,
+        submitFn: submitMiscIssueApproval,
+      })
+      .open();
+  } catch {
+    hideLoading();
+  }
+}
+
+/** 鏌ョ湅瀹℃壒璇︽儏 */
+function handleProcessDetail(row: MesWmMiscIssueApi.MiscIssue) {
+  if (!row.processInstanceId) {
+    message.warning('璇ュ崟鎹皻鏈彁浜ゅ鎵�');
+    return;
+  }
+  router.push({
+    name: 'BpmProcessInstanceDetail',
+    query: { id: row.processInstanceId },
+  });
 }
 
 /** 鍒犻櫎鏉傞」鍑哄簱鍗� */
@@ -111,6 +160,7 @@
 
 <template>
   <Page auto-content-height><FormModal @success="handleRefresh" />
+    <ProcessModal @success="handleRefresh" />
 
     <Grid table-title="鏉傞」鍑哄簱鍗曞垪琛�">
       <template #toolbar-tools>
@@ -162,6 +212,20 @@
               },
             },
             {
+              label: '鎻愪氦瀹℃壒',
+              type: 'link',
+              auth: ['mes:wm-misc-issue:update'],
+              ifShow: row.status === MesWmMiscIssueStatusEnum.PREPARE,
+              onClick: handleSubmitApproval.bind(null, row),
+            },
+            {
+              label: '鏌ョ湅瀹℃壒',
+              type: 'link',
+              auth: ['mes:wm-misc-issue:query'],
+              ifShow: row.status === MesWmMiscIssueStatusEnum.APPROVING,
+              onClick: handleProcessDetail.bind(null, row),
+            },
+            {
               label: '鎵ц鍑哄簱',
               type: 'link',
               auth: ['mes:wm-misc-issue:finish'],
diff --git a/src/views/wls/miscissue/modules/form.vue b/src/views/wls/miscissue/modules/form.vue
index 3883b97..61f126a 100644
--- a/src/views/wls/miscissue/modules/form.vue
+++ b/src/views/wls/miscissue/modules/form.vue
@@ -15,7 +15,6 @@
   createMiscIssue,
   finishMiscIssue,
   getMiscIssue,
-  submitMiscIssue,
   updateMiscIssue,
 } from '#/api/mes/wm/miscissue';
 import { $t } from '#/locales';
@@ -32,12 +31,6 @@
   ['create', 'update'].includes(formType.value),
 );
 const isFinish = computed(() => formType.value === 'finish'); // 鏄惁涓烘墽琛屽嚭搴撴ā寮�
-const canSubmit = computed(
-  () =>
-    // 鏄惁鍙彁浜�
-    formType.value === 'update' &&
-    formData.value?.status === MesWmMiscIssueStatusEnum.PREPARE,
-);
 const getTitle = computed(() => {
   if (formType.value === 'detail') {
     return $t('ui.actionTitle.view', ['鏉傞」鍑哄簱鍗�']);
@@ -63,29 +56,6 @@
   showDefaultActions: false,
   wrapperClass: 'grid-cols-3',
 });
-
-/** 鎻愪氦鍑哄簱鍗曪細琛ㄥ崟鏈変慨鏀规椂鍏堜繚瀛橈紝鍐嶈皟鐢ㄦ彁浜ゆ帴鍙� */
-async function handleSubmit() {
-  const { valid } = await formApi.validate();
-  if (!valid || !formData.value?.id) {
-    return;
-  }
-  modalApi.lock();
-  try {
-    const current = JSON.stringify(await formApi.getValues());
-    if (current !== originalSnapshot.value) {
-      const data = (await formApi.getValues()) as MesWmMiscIssueApi.MiscIssue;
-      await updateMiscIssue({ ...formData.value, ...data });
-      originalSnapshot.value = current;
-    }
-    await submitMiscIssue(formData.value.id);
-    message.success('鎻愪氦鎴愬姛');
-    await modalApi.close();
-    emit('success');
-  } finally {
-    modalApi.unlock();
-  }
-}
 
 /** 鎵ц鍑哄簱 */
 async function handleFinish() {
@@ -179,13 +149,6 @@
     </template>
     <template #prepend-footer>
       <div class="flex flex-auto items-center gap-2">
-        <Popconfirm
-          v-if="canSubmit"
-          title="纭鎻愪氦璇ユ潅椤瑰嚭搴撳崟锛熴�愭彁浜ゅ悗灏嗕笉鑳戒慨鏀广��"
-          @confirm="handleSubmit"
-        >
-          <Button type="primary">鎻愪氦</Button>
-        </Popconfirm>
         <Popconfirm
           v-if="isFinish"
           title="纭鎵ц鍑哄簱锛熸墽琛屽悗灏嗘洿鏂板簱瀛樺彴璐︺��"
diff --git a/src/views/wls/miscreceipt/index.vue b/src/views/wls/miscreceipt/index.vue
index b9d2d2f..03a6f12 100644
--- a/src/views/wls/miscreceipt/index.vue
+++ b/src/views/wls/miscreceipt/index.vue
@@ -2,6 +2,8 @@
 import type { VxeTableGridOptions } from '#/adapter/vxe-table';
 import type { MesWmMiscReceiptApi } from '#/api/mes/wm/miscreceipt';
 
+import { useRouter } from 'vue-router';
+
 import { Page, useVbenModal } from '@vben/common-ui';
 import { MesWmMiscReceiptStatusEnum } from '@vben/constants';
 import { downloadFileFromBlobPart } from '@vben/utils';
@@ -13,15 +15,25 @@
   cancelMiscReceipt,
   deleteMiscReceipt,
   exportMiscReceipt,
+  getMiscReceiptApproveProcessList,
   getMiscReceiptPage,
+  submitMiscReceiptApproval,
 } from '#/api/mes/wm/miscreceipt';
 import { $t } from '#/locales';
 
+import ProcessSelectModal from '#/views/wls/components/process-select-modal.vue';
 import { useGridColumns, useGridFormSchema } from './data';
 import Form from './modules/form.vue';
 
+const router = useRouter();
+
 const [FormModal, formModalApi] = useVbenModal({
   connectedComponent: Form,
+  destroyOnClose: true,
+});
+
+const [ProcessModal, processModalApi] = useVbenModal({
+  connectedComponent: ProcessSelectModal,
   destroyOnClose: true,
 });
 
@@ -48,6 +60,43 @@
 /** 鎵ц鍏ュ簱 */
 function handleFinish(row: MesWmMiscReceiptApi.MiscReceipt) {
   formModalApi.setData({ formType: 'finish', id: row.id }).open();
+}
+
+/** 鎻愪氦瀹℃壒 - 鍏堣幏鍙栨祦绋嬪垪琛� */
+async function handleSubmitApproval(row: MesWmMiscReceiptApi.MiscReceipt) {
+  const hideLoading = message.loading({
+    content: '鑾峰彇瀹℃壒娴佺▼...',
+    duration: 0,
+  });
+  try {
+    const processList = await getMiscReceiptApproveProcessList();
+    hideLoading();
+    if (!processList || processList.length === 0) {
+      message.warning('鏆傛棤鍙敤瀹℃壒娴佺▼锛岃鍏堥厤缃� BPM 娴佺▼');
+      return;
+    }
+    processModalApi
+      .setData({
+        id: row.id,
+        processList,
+        submitFn: submitMiscReceiptApproval,
+      })
+      .open();
+  } catch {
+    hideLoading();
+  }
+}
+
+/** 鏌ョ湅瀹℃壒璇︽儏 */
+function handleProcessDetail(row: MesWmMiscReceiptApi.MiscReceipt) {
+  if (!row.processInstanceId) {
+    message.warning('璇ュ崟鎹皻鏈彁浜ゅ鎵�');
+    return;
+  }
+  router.push({
+    name: 'BpmProcessInstanceDetail',
+    query: { id: row.processInstanceId },
+  });
 }
 
 /** 鍒犻櫎鏉傞」鍏ュ簱鍗� */
@@ -111,6 +160,7 @@
 
 <template>
   <Page auto-content-height><FormModal @success="handleRefresh" />
+    <ProcessModal @success="handleRefresh" />
 
     <Grid table-title="鏉傞」鍏ュ簱鍗曞垪琛�">
       <template #toolbar-tools>
@@ -162,6 +212,20 @@
               },
             },
             {
+              label: '鎻愪氦瀹℃壒',
+              type: 'link',
+              auth: ['mes:wm:misc-receipt:submit'],
+              ifShow: row.status === MesWmMiscReceiptStatusEnum.PREPARE,
+              onClick: handleSubmitApproval.bind(null, row),
+            },
+            {
+              label: '鏌ョ湅瀹℃壒',
+              type: 'link',
+              auth: ['mes:wm:misc-receipt:query'],
+              ifShow: row.status === MesWmMiscReceiptStatusEnum.APPROVING,
+              onClick: handleProcessDetail.bind(null, row),
+            },
+            {
               label: '鎵ц鍏ュ簱',
               type: 'link',
               auth: ['mes:wm-misc-receipt:finish'],
diff --git a/src/views/wls/miscreceipt/modules/form.vue b/src/views/wls/miscreceipt/modules/form.vue
index cfab99d..435f58c 100644
--- a/src/views/wls/miscreceipt/modules/form.vue
+++ b/src/views/wls/miscreceipt/modules/form.vue
@@ -15,7 +15,6 @@
   createMiscReceipt,
   finishMiscReceipt,
   getMiscReceipt,
-  submitMiscReceipt,
   updateMiscReceipt,
 } from '#/api/mes/wm/miscreceipt';
 import { $t } from '#/locales';
@@ -32,12 +31,6 @@
   ['create', 'update'].includes(formType.value),
 );
 const isFinish = computed(() => formType.value === 'finish'); // 鏄惁涓烘墽琛屽叆搴撴ā寮�
-const canSubmit = computed(
-  () =>
-    // 鏄惁鍙彁浜�
-    formType.value === 'update' &&
-    formData.value?.status === MesWmMiscReceiptStatusEnum.PREPARE,
-);
 const getTitle = computed(() => {
   if (formType.value === 'detail') {
     return $t('ui.actionTitle.view', ['鏉傞」鍏ュ簱鍗�']);
@@ -63,30 +56,6 @@
   showDefaultActions: false,
   wrapperClass: 'grid-cols-3',
 });
-
-/** 鎻愪氦鍏ュ簱鍗曪細琛ㄥ崟鏈変慨鏀规椂鍏堜繚瀛橈紝鍐嶈皟鐢ㄦ彁浜ゆ帴鍙� */
-async function handleSubmit() {
-  const { valid } = await formApi.validate();
-  if (!valid || !formData.value?.id) {
-    return;
-  }
-  modalApi.lock();
-  try {
-    const current = JSON.stringify(await formApi.getValues());
-    if (current !== originalSnapshot.value) {
-      const data =
-        (await formApi.getValues()) as MesWmMiscReceiptApi.MiscReceipt;
-      await updateMiscReceipt({ ...formData.value, ...data });
-      originalSnapshot.value = current;
-    }
-    await submitMiscReceipt(formData.value.id);
-    message.success('鎻愪氦鎴愬姛');
-    await modalApi.close();
-    emit('success');
-  } finally {
-    modalApi.unlock();
-  }
-}
 
 /** 鎵ц鍏ュ簱 */
 async function handleFinish() {
@@ -180,13 +149,6 @@
     </template>
     <template #prepend-footer>
       <div class="flex flex-auto items-center gap-2">
-        <Popconfirm
-          v-if="canSubmit"
-          title="纭鎻愪氦璇ユ潅椤瑰叆搴撳崟锛熴�愭彁浜ゅ悗灏嗕笉鑳戒慨鏀广��"
-          @confirm="handleSubmit"
-        >
-          <Button type="primary">鎻愪氦</Button>
-        </Popconfirm>
         <Popconfirm
           v-if="isFinish"
           title="纭鎵ц鍏ュ簱锛熸墽琛屽悗灏嗘洿鏂板簱瀛樺彴璐︺��"
diff --git a/src/views/wls/returnvendor/index.vue b/src/views/wls/returnvendor/index.vue
index 895e494..62d5716 100644
--- a/src/views/wls/returnvendor/index.vue
+++ b/src/views/wls/returnvendor/index.vue
@@ -2,6 +2,8 @@
 import type { VxeTableGridOptions } from '#/adapter/vxe-table';
 import type { MesWmReturnVendorApi } from '#/api/mes/wm/returnvendor';
 
+import { useRouter } from 'vue-router';
+
 import { Page, useVbenModal } from '@vben/common-ui';
 import { MesWmReturnVendorStatusEnum } from '@vben/constants';
 import { downloadFileFromBlobPart } from '@vben/utils';
@@ -13,15 +15,25 @@
   cancelReturnVendor,
   deleteReturnVendor,
   exportReturnVendor,
+  getReturnVendorApproveProcessList,
   getReturnVendorPage,
+  submitReturnVendorApproval,
 } from '#/api/mes/wm/returnvendor';
 import { $t } from '#/locales';
 
+import ProcessSelectModal from '#/views/wls/components/process-select-modal.vue';
 import { useGridColumns, useGridFormSchema } from './data';
 import Form from './modules/form.vue';
 
+const router = useRouter();
+
 const [FormModal, formModalApi] = useVbenModal({
   connectedComponent: Form,
+  destroyOnClose: true,
+});
+
+const [ProcessModal, processModalApi] = useVbenModal({
+  connectedComponent: ProcessSelectModal,
   destroyOnClose: true,
 });
 
@@ -48,6 +60,43 @@
 /** 鎵ц鎷h揣 */
 function handleStock(row: MesWmReturnVendorApi.ReturnVendor) {
   formModalApi.setData({ formType: 'stock', id: row.id }).open();
+}
+
+/** 鎻愪氦瀹℃壒 - 鍏堣幏鍙栨祦绋嬪垪琛� */
+async function handleSubmitApproval(row: MesWmReturnVendorApi.ReturnVendor) {
+  const hideLoading = message.loading({
+    content: '鑾峰彇瀹℃壒娴佺▼...',
+    duration: 0,
+  });
+  try {
+    const processList = await getReturnVendorApproveProcessList();
+    hideLoading();
+    if (!processList || processList.length === 0) {
+      message.warning('鏆傛棤鍙敤瀹℃壒娴佺▼锛岃鍏堥厤缃� BPM 娴佺▼');
+      return;
+    }
+    processModalApi
+      .setData({
+        id: row.id,
+        processList,
+        submitFn: submitReturnVendorApproval,
+      })
+      .open();
+  } catch {
+    hideLoading();
+  }
+}
+
+/** 鏌ョ湅瀹℃壒璇︽儏 */
+function handleProcessDetail(row: MesWmReturnVendorApi.ReturnVendor) {
+  if (!row.processInstanceId) {
+    message.warning('璇ュ崟鎹皻鏈彁浜ゅ鎵�');
+    return;
+  }
+  router.push({
+    name: 'BpmProcessInstanceDetail',
+    query: { id: row.processInstanceId },
+  });
 }
 
 /** 瀹屾垚閫�璐� */
@@ -116,6 +165,7 @@
 
 <template>
   <Page auto-content-height><FormModal @success="handleRefresh" />
+    <ProcessModal @success="handleRefresh" />
 
     <Grid table-title="渚涘簲鍟嗛��璐у崟鍒楄〃">
       <template #toolbar-tools>
@@ -147,6 +197,13 @@
         <TableAction
           :actions="[
             {
+              label: '鎻愪氦瀹℃壒',
+              type: 'link',
+              auth: ['mes:wm-return-vendor:update'],
+              ifShow: row.status === MesWmReturnVendorStatusEnum.PREPARE,
+              onClick: handleSubmitApproval.bind(null, row),
+            },
+            {
               label: $t('common.edit'),
               type: 'link',
               icon: ACTION_ICON.EDIT,
@@ -167,6 +224,13 @@
               },
             },
             {
+              label: '鏌ョ湅瀹℃壒',
+              type: 'link',
+              auth: ['mes:wm-return-vendor:query'],
+              ifShow: row.status === MesWmReturnVendorStatusEnum.APPROVING,
+              onClick: handleProcessDetail.bind(null, row),
+            },
+            {
               label: '鎵ц鎷h揣',
               type: 'link',
               auth: ['mes:wm-return-vendor:update'],
diff --git a/src/views/wls/returnvendor/modules/form.vue b/src/views/wls/returnvendor/modules/form.vue
index 252e785..ebca15a 100644
--- a/src/views/wls/returnvendor/modules/form.vue
+++ b/src/views/wls/returnvendor/modules/form.vue
@@ -18,7 +18,6 @@
   finishReturnVendor,
   getReturnVendor,
   stockReturnVendor,
-  submitReturnVendor,
   updateReturnVendor,
 } from '#/api/mes/wm/returnvendor';
 import { $t } from '#/locales';
@@ -36,12 +35,6 @@
 );
 const isStock = computed(() => formType.value === 'stock'); // 鏄惁涓烘嫞璐фā寮�
 const isFinish = computed(() => formType.value === 'finish'); // 鏄惁涓哄畬鎴愰��璐фā寮�
-const canSubmit = computed(
-  () =>
-    // 鏄惁鍙彁浜�
-    formType.value === 'update' &&
-    formData.value?.status === MesWmReturnVendorStatusEnum.PREPARE,
-);
 const getTitle = computed(() => {
   if (formType.value === 'detail') {
     return $t('ui.actionTitle.view', ['渚涘簲鍟嗛��璐у崟']);
@@ -80,30 +73,6 @@
     return formatDate(value, 'YYYY-MM-DD HH:mm:ss');
   }
   return undefined;
-}
-
-/** 鎻愪氦閫�璐у崟锛氳〃鍗曟湁淇敼鏃跺厛淇濆瓨锛屽啀璋冪敤鎻愪氦鎺ュ彛 */
-async function handleSubmit() {
-  const { valid } = await formApi.validate();
-  if (!valid || !formData.value?.id) {
-    return;
-  }
-  modalApi.lock();
-  try {
-    const current = JSON.stringify(await formApi.getValues());
-    if (current !== originalSnapshot.value) {
-      const data =
-        (await formApi.getValues()) as MesWmReturnVendorApi.ReturnVendor;
-      await updateReturnVendor({ ...formData.value, ...data });
-      originalSnapshot.value = current;
-    }
-    await submitReturnVendor(formData.value.id);
-    message.success('鎻愪氦鎴愬姛');
-    await modalApi.close();
-    emit('success');
-  } finally {
-    modalApi.unlock();
-  }
 }
 
 /** 鎵ц鎷h揣锛氭嫞璐ф暟閲忎笌閫�璐ф暟閲忎笉涓�鑷存椂浜屾纭 */
@@ -228,13 +197,6 @@
     </template>
     <template #prepend-footer>
       <div class="flex flex-auto items-center gap-2">
-        <Popconfirm
-          v-if="canSubmit"
-          title="纭鎻愪氦璇ラ��璐у崟锛熴�愭彁浜ゅ悗灏嗕笉鑳戒慨鏀广��"
-          @confirm="handleSubmit"
-        >
-          <Button type="primary">鎻愪氦</Button>
-        </Popconfirm>
         <Button v-if="isStock" type="primary" @click="handleStock">
           鎵ц鎷h揣
         </Button>
diff --git a/src/views/wls/sn/data.ts b/src/views/wls/sn/data.ts
index 2023366..880c7a0 100644
--- a/src/views/wls/sn/data.ts
+++ b/src/views/wls/sn/data.ts
@@ -1,11 +1,26 @@
-import type { VbenFormSchema } from '#/adapter/form';
+import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
 import type { VxeTableGridOptions } from '#/adapter/vxe-table';
 import type { MesWmSnApi } from '#/api/mes/wm/sn';
+import type { DescriptionItemSchema } from '#/components/description';
 
 import { markRaw } from 'vue';
 
+import { MesWmSnStatusEnum } from '@vben/constants';
+import { formatDateTime } from '@vben/utils';
+
 import { getRangePickerDefaultProps } from '#/utils';
 import { MdItemSelect } from '#/views/mes/md/item/components';
+import {
+  WmWarehouseAreaSelect,
+  WmWarehouseLocationSelect,
+  WmWarehouseSelect,
+} from '#/views/wls/warehouse/components';
+
+/** SN 鐘舵�侀�夐」锛堟棤鐙珛瀛楀吀锛屼娇鐢ㄥ父閲忔槧灏勶級 */
+export const SN_STATUS_OPTIONS = [
+  { label: '鍦ㄥ簱', value: MesWmSnStatusEnum.IN_STOCK },
+  { label: '宸插嚭搴�', value: MesWmSnStatusEnum.OUT_STOCK },
+];
 
 /** 鐢熸垚 SN 鐮佺殑琛ㄥ崟 */
 export function useFormSchema(): VbenFormSchema[] {
@@ -181,3 +196,275 @@
     },
   ];
 }
+
+/** 鍦ㄥ簱鍗曚欢鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useInStockGridFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'code',
+      label: 'SN 鐮�',
+      component: 'Input',
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇疯緭鍏� SN 鐮�',
+      },
+    },
+    {
+      fieldName: 'itemId',
+      label: '鐗╂枡',
+      component: markRaw(MdItemSelect),
+      componentProps: {
+        placeholder: '璇烽�夋嫨鐗╂枡',
+      },
+    },
+    {
+      fieldName: 'batchCode',
+      label: '鎵规鍙�',
+      component: 'Input',
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇疯緭鍏ユ壒娆″彿',
+      },
+    },
+    {
+      fieldName: 'status',
+      label: '鐘舵��',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: SN_STATUS_OPTIONS,
+        placeholder: '璇烽�夋嫨鐘舵��',
+      },
+    },
+  ];
+}
+
+/** 鍦ㄥ簱鍗曚欢鍒楄〃鐨勫瓧娈� */
+export function useInStockGridColumns(): VxeTableGridOptions<MesWmSnApi.Sn>['columns'] {
+  return [
+    {
+      field: 'code',
+      title: 'SN 鐮�',
+      minWidth: 180,
+    },
+    {
+      field: 'itemCode',
+      title: '鐗╂枡缂栫爜',
+      minWidth: 120,
+    },
+    {
+      field: 'itemName',
+      title: '鐗╂枡鍚嶇О',
+      minWidth: 150,
+    },
+    {
+      field: 'specification',
+      title: '瑙勬牸鍨嬪彿',
+      minWidth: 120,
+    },
+    {
+      field: 'unitName',
+      title: '鍗曚綅',
+      width: 80,
+    },
+    {
+      field: 'batchCode',
+      title: '鎵规鍙�',
+      minWidth: 120,
+    },
+    {
+      field: 'status',
+      title: '鐘舵��',
+      width: 90,
+      slots: { default: 'status' },
+    },
+    {
+      field: 'warehouseId',
+      title: '浠撳簱',
+      minWidth: 140,
+      slots: { default: 'locations' },
+    },
+    {
+      field: 'createTime',
+      title: '鐢熸垚鏃堕棿',
+      width: 180,
+      formatter: 'formatDateTime',
+    },
+    {
+      title: '鎿嶄綔',
+      width: 140,
+      fixed: 'right',
+      slots: { default: 'actions' },
+    },
+  ];
+}
+
+/** SN 鐮佸叆搴撶櫥璁扮殑琛ㄥ崟 */
+export function useInStockFormSchema(
+  formApi?: VbenFormApi,
+): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'code',
+      label: 'SN 鐮�',
+      component: 'Input',
+      componentProps: {
+        placeholder: '鎵爜鏋壂鍏ユ垨杈撳叆 SN 鐮�',
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'itemId',
+      label: '鐗╂枡',
+      component: markRaw(MdItemSelect),
+      componentProps: {
+        placeholder: '璇烽�夋嫨鐗╂枡锛堥』鍚敤搴忓垪鍙风鐞嗭級',
+      },
+      rules: 'selectRequired',
+    },
+    {
+      fieldName: 'warehouseId',
+      label: '浠撳簱',
+      component: markRaw(WmWarehouseSelect),
+      componentProps: {
+        onChange: () =>
+          formApi?.setValues({ areaId: undefined, locationId: undefined }),
+        placeholder: '璇烽�夋嫨浠撳簱',
+      },
+      rules: 'selectRequired',
+    },
+    {
+      fieldName: 'locationId',
+      label: '搴撳尯',
+      component: markRaw(WmWarehouseLocationSelect),
+      dependencies: {
+        triggerFields: ['warehouseId'],
+        show: (values) => !!values.warehouseId,
+        componentProps: (values) => ({
+          onChange: () => formApi?.setFieldValue('areaId', undefined),
+          placeholder: '璇烽�夋嫨搴撳尯',
+          warehouseId: values.warehouseId,
+        }),
+      },
+      rules: 'selectRequired',
+    },
+    {
+      fieldName: 'areaId',
+      label: '搴撲綅',
+      component: markRaw(WmWarehouseAreaSelect),
+      dependencies: {
+        triggerFields: ['locationId'],
+        show: (values) => !!values.locationId,
+        componentProps: (values) => ({
+          locationId: values.locationId,
+          placeholder: '璇烽�夋嫨搴撲綅',
+        }),
+      },
+      rules: 'selectRequired',
+    },
+    {
+      fieldName: 'batchCode',
+      label: '鎵规鍙�',
+      component: 'Input',
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇疯緭鍏ユ壒娆″彿',
+      },
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      formItemClass: 'col-span-2',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ娉�',
+        rows: 2,
+      },
+    },
+  ];
+}
+
+/** SN 鐮佸嚭搴撴牳閿�鐨勮〃鍗� */
+export function useOutStockFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'code',
+      label: 'SN 鐮�',
+      component: 'Input',
+      componentProps: {
+        placeholder: '鎵爜鏋壂鍏ユ垨杈撳叆 SN 鐮�',
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'itemId',
+      label: '鐗╂枡',
+      component: markRaw(MdItemSelect),
+      componentProps: {
+        placeholder: '璇烽�夋嫨鐗╂枡',
+      },
+      rules: 'selectRequired',
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      formItemClass: 'col-span-2',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ娉�',
+        rows: 2,
+      },
+    },
+  ];
+}
+
+/** SN 鐮佸崟浠惰鎯呯殑鎻忚堪瀛楁 */
+export function useSnDetailSchema(): DescriptionItemSchema[] {
+  return [
+    {
+      field: 'code',
+      label: 'SN 鐮�',
+      render: (value) => value || '-',
+    },
+    {
+      field: 'itemCode',
+      label: '鐗╂枡缂栫爜',
+      render: (value) => value || '-',
+    },
+    {
+      field: 'itemName',
+      label: '鐗╂枡鍚嶇О',
+      render: (value) => value || '-',
+    },
+    {
+      field: 'batchCode',
+      label: '鎵规鍙�',
+      render: (value) => value || '-',
+    },
+    {
+      field: 'status',
+      label: '鐘舵��',
+      render: (value) =>
+        value === MesWmSnStatusEnum.OUT_STOCK
+          ? '宸插嚭搴�'
+          : value === MesWmSnStatusEnum.IN_STOCK
+            ? '鍦ㄥ簱'
+            : '-',
+    },
+    {
+      field: 'warehouseId',
+      label: '鍦ㄥ簱浣嶇疆',
+      slot: 'location',
+    },
+    {
+      field: 'lastTransactionTypeName',
+      label: '鏈�杩戞祦姘�',
+      slot: 'lastTransaction',
+    },
+    {
+      field: 'lastTransactionTime',
+      label: '鏈�杩戞祦姘存椂闂�',
+      render: (value) => formatDateTime(value) || '-',
+    },
+  ];
+}
diff --git a/src/views/wls/sn/index.vue b/src/views/wls/sn/index.vue
index c881303..f1c5572 100644
--- a/src/views/wls/sn/index.vue
+++ b/src/views/wls/sn/index.vue
@@ -2,10 +2,12 @@
 import type { VxeTableGridOptions } from '#/adapter/vxe-table';
 import type { MesWmSnApi } from '#/api/mes/wm/sn';
 
+import { ref } from 'vue';
+
 import { Page, useVbenModal } from '@vben/common-ui';
 import { downloadFileFromBlobPart } from '@vben/utils';
 
-import { message } from 'ant-design-vue';
+import { message, Tabs, Tag } from 'ant-design-vue';
 
 import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
 import {
@@ -13,12 +15,28 @@
   exportSnDetailExcel,
   exportSnGroupExcel,
   getSnGroupPage,
+  getSnInStockPage,
 } from '#/api/mes/wm/sn';
 import { $t } from '#/locales';
 
-import { useGridColumns, useGridFormSchema } from './data';
+import {
+  useGridColumns,
+  useGridFormSchema,
+  useInStockGridColumns,
+  useInStockGridFormSchema,
+} from './data';
+import {
+  formatWmLocation,
+  getWmLocationMaps,
+  type WmLocationMaps,
+} from './location-maps';
 import Detail from './modules/detail.vue';
 import Form from './modules/form.vue';
+import InStockModal from './modules/in-stock-modal.vue';
+import OutStockModal from './modules/out-stock-modal.vue';
+import SnDetailModal from './modules/sn-detail-modal.vue';
+
+const activeKey = ref('group');
 
 const [FormModal, formModalApi] = useVbenModal({
   connectedComponent: Form,
@@ -30,9 +48,25 @@
   destroyOnClose: true,
 });
 
-/** 鍒锋柊琛ㄦ牸 */
+const [InStockModalRef, inStockModalApi] = useVbenModal({
+  connectedComponent: InStockModal,
+  destroyOnClose: true,
+});
+
+const [OutStockModalRef, outStockModalApi] = useVbenModal({
+  connectedComponent: OutStockModal,
+  destroyOnClose: true,
+});
+
+const snDetailModalRef = ref<InstanceType<typeof SnDetailModal>>();
+
+/** 鍒锋柊褰撳墠椤电 */
 function handleRefresh() {
-  gridApi.query();
+  if (activeKey.value === 'group') {
+    gridApi.query();
+  } else {
+    inStockGridApi.query();
+  }
 }
 
 /** 鐢熸垚 SN 鐮� */
@@ -74,6 +108,34 @@
   }
 }
 
+/** 鍏ュ簱鐧昏 */
+function handleInStock() {
+  inStockModalApi.open();
+}
+
+/** 鍑哄簱鏍搁攢 */
+function handleOutStock() {
+  outStockModalApi.open();
+}
+
+/** 鏌ョ湅鍗曚欢璇︽儏 */
+function handleSnDetail(row: MesWmSnApi.Sn) {
+  snDetailModalRef.value?.open(row.code);
+}
+
+/** 鍦ㄥ簱鍗曚欢鍒楄〃浣嶇疆鍚嶇О鏄犲皠锛堟噿鍔犺浇锛岄娆¤闂椂鍒濆鍖栵級 */
+const locationMapsRef = ref<Awaited<ReturnType<typeof getWmLocationMaps>>>();
+const EMPTY_MAPS: WmLocationMaps = {
+  warehouseNameMap: {},
+  locationNameMap: {},
+  areaNameMap: {},
+};
+async function ensureLocationMaps() {
+  if (!locationMapsRef.value) {
+    locationMapsRef.value = await getWmLocationMaps();
+  }
+}
+
 const [Grid, gridApi] = useVbenVxeGrid({
   formOptions: {
     schema: useGridFormSchema(),
@@ -103,75 +165,171 @@
     },
   } as VxeTableGridOptions<MesWmSnApi.SnGroup>,
 });
+
+const [InStockGrid, inStockGridApi] = useVbenVxeGrid({
+  formOptions: {
+    schema: useInStockGridFormSchema(),
+  },
+  gridOptions: {
+    columns: useInStockGridColumns(),
+    height: 'auto',
+    keepSource: true,
+    proxyConfig: {
+      ajax: {
+        query: async ({ page }, formValues) => {
+          return await getSnInStockPage({
+            pageNo: page.currentPage,
+            pageSize: page.pageSize,
+            ...formValues,
+          });
+        },
+      },
+    },
+    rowConfig: {
+      keyField: 'id',
+      isHover: true,
+    },
+    toolbarConfig: {
+      refresh: true,
+      search: true,
+    },
+  } as VxeTableGridOptions<MesWmSnApi.Sn>,
+});
+
+// 棣栨鎸傝浇鏃堕鐑綅缃悕绉版槧灏勶紝渚涘垪琛ㄥ睍绀�
+void ensureLocationMaps();
 </script>
 
 <template>
-  <Page auto-content-height><FormModal @success="handleRefresh" />
+  <Page auto-content-height>
+    <FormModal @success="handleRefresh" />
     <DetailModal />
+    <InStockModalRef @success="handleRefresh" />
+    <OutStockModalRef @success="handleRefresh" />
+    <SnDetailModal ref="snDetailModalRef" />
 
-    <Grid table-title="SN 鐮佸垪琛�">
-      <template #toolbar-tools>
-        <TableAction
-          :actions="[
-            {
-              label: '鐢熸垚 SN 鐮�',
-              type: 'primary',
-              icon: ACTION_ICON.ADD,
-              auth: ['mes:wm-sn:create'],
-              onClick: handleCreate,
-            },
-            {
-              label: $t('ui.actionTitle.export'),
-              type: 'primary',
-              icon: ACTION_ICON.DOWNLOAD,
-              auth: ['mes:wm-sn:export'],
-              onClick: handleExport,
-            },
-          ]"
-        />
-      </template>
-      <template #count="{ row }">
-        <TableAction
-          :actions="[
-            {
-              label: `${row.count ?? 0}`,
-              type: 'link',
-              auth: ['mes:wm-sn:query'],
-              onClick: handleDetail.bind(null, row),
-            },
-          ]"
-        />
-      </template>
-      <template #actions="{ row }">
-        <TableAction
-          :actions="[
-            {
-              label: '鏌ョ湅鏄庣粏',
-              type: 'link',
-              auth: ['mes:wm-sn:query'],
-              onClick: handleDetail.bind(null, row),
-            },
-            {
-              label: '瀵煎嚭鏄庣粏',
-              type: 'link',
-              auth: ['mes:wm-sn:export'],
-              onClick: handleExportDetail.bind(null, row),
-            },
-            {
-              label: $t('common.delete'),
-              type: 'link',
-              danger: true,
-              auth: ['mes:wm-sn:delete'],
-              popConfirm: {
-                title: $t('ui.actionMessage.deleteConfirm', [
-                  row.batchCode || row.itemName,
-                ]),
-                confirm: handleDelete.bind(null, row),
-              },
-            },
-          ]"
-        />
-      </template>
-    </Grid>
+    <Tabs v-model:active-key="activeKey" class="px-4 pt-1" type="card">
+      <Tabs.TabPane key="group" tab="SN 鐮佸垎缁�">
+        <Grid table-title="SN 鐮佸垪琛�">
+          <template #toolbar-tools>
+            <TableAction
+              :actions="[
+                {
+                  label: '鐢熸垚 SN 鐮�',
+                  type: 'primary',
+                  icon: ACTION_ICON.ADD,
+                  auth: ['mes:wm-sn:create'],
+                  onClick: handleCreate,
+                },
+                {
+                  label: $t('ui.actionTitle.export'),
+                  type: 'primary',
+                  icon: ACTION_ICON.DOWNLOAD,
+                  auth: ['mes:wm-sn:export'],
+                  onClick: handleExport,
+                },
+              ]"
+            />
+          </template>
+          <template #count="{ row }">
+            <TableAction
+              :actions="[
+                {
+                  label: `${row.count ?? 0}`,
+                  type: 'link',
+                  auth: ['mes:wm-sn:query'],
+                  onClick: handleDetail.bind(null, row),
+                },
+              ]"
+            />
+          </template>
+          <template #actions="{ row }">
+            <TableAction
+              :actions="[
+                {
+                  label: '鏌ョ湅鏄庣粏',
+                  type: 'link',
+                  auth: ['mes:wm-sn:query'],
+                  onClick: handleDetail.bind(null, row),
+                },
+                {
+                  label: '瀵煎嚭鏄庣粏',
+                  type: 'link',
+                  auth: ['mes:wm-sn:export'],
+                  onClick: handleExportDetail.bind(null, row),
+                },
+                {
+                  label: $t('common.delete'),
+                  type: 'link',
+                  danger: true,
+                  auth: ['mes:wm-sn:delete'],
+                  popConfirm: {
+                    title: $t('ui.actionMessage.deleteConfirm', [
+                      row.batchCode || row.itemName,
+                    ]),
+                    confirm: handleDelete.bind(null, row),
+                  },
+                },
+              ]"
+            />
+          </template>
+        </Grid>
+      </Tabs.TabPane>
+      <Tabs.TabPane key="in-stock" tab="鍦ㄥ簱鍗曚欢">
+        <InStockGrid table-title="SN 鍗曚欢鐘舵�佸垪琛�">
+          <template #toolbar-tools>
+            <TableAction
+              :actions="[
+                {
+                  label: '鍏ュ簱鐧昏',
+                  type: 'primary',
+                  icon: ACTION_ICON.ADD,
+                  auth: ['mes:wm-sn:update'],
+                  onClick: handleInStock,
+                },
+                {
+                  label: '鍑哄簱鏍搁攢',
+                  type: 'primary',
+                  danger: true,
+                  icon: ACTION_ICON.DELETE,
+                  auth: ['mes:wm-sn:update'],
+                  onClick: handleOutStock,
+                },
+              ]"
+            />
+          </template>
+          <template #status="{ row }">
+            <Tag :color="row.status === 1 ? 'green' : 'orange'">
+              {{ row.status === 1 ? '鍦ㄥ簱' : '宸插嚭搴�' }}
+            </Tag>
+          </template>
+          <template #locations="{ row }">
+            <span v-if="row.status === 1">
+              {{
+                formatWmLocation(
+                  locationMapsRef ?? EMPTY_MAPS,
+                  row.warehouseId,
+                  row.locationId,
+                  row.areaId,
+                )
+              }}
+            </span>
+            <span v-else>-</span>
+          </template>
+          <template #actions="{ row }">
+            <TableAction
+              :actions="[
+                {
+                  label: '璇︽儏',
+                  type: 'link',
+                  auth: ['mes:wm-sn:query'],
+                  onClick: handleSnDetail.bind(null, row),
+                },
+              ]"
+            />
+          </template>
+        </InStockGrid>
+      </Tabs.TabPane>
+    </Tabs>
   </Page>
-</template>
+</template>
\ No newline at end of file
diff --git a/src/views/wls/sn/location-maps.ts b/src/views/wls/sn/location-maps.ts
new file mode 100644
index 0000000..762f3c4
--- /dev/null
+++ b/src/views/wls/sn/location-maps.ts
@@ -0,0 +1,51 @@
+import { getWarehouseAreaSimpleList } from '#/api/mes/wm/warehouse/area';
+import { getWarehouseSimpleList } from '#/api/mes/wm/warehouse';
+import { getWarehouseLocationSimpleList } from '#/api/mes/wm/warehouse/location';
+
+export interface WmLocationMaps {
+  warehouseNameMap: Record<number, string>;
+  locationNameMap: Record<number, string>;
+  areaNameMap: Record<number, string>;
+}
+
+let cached: WmLocationMaps | null = null;
+
+/** 鍔犺浇浠撳簱/搴撳尯/搴撲綅 ID -> 鍚嶇О鏄犲皠锛堟ā鍧楃骇缂撳瓨锛屼粎棣栨鍔犺浇锛� */
+export async function getWmLocationMaps(): Promise<WmLocationMaps> {
+  if (cached) {
+    return cached;
+  }
+  const [warehouses, locations, areas] = await Promise.all([
+    getWarehouseSimpleList(),
+    getWarehouseLocationSimpleList(),
+    getWarehouseAreaSimpleList(),
+  ]);
+  cached = {
+    warehouseNameMap: Object.fromEntries(
+      warehouses.map((w) => [w.id!, w.name || w.code || '']),
+    ),
+    locationNameMap: Object.fromEntries(
+      locations.map((l) => [l.id!, l.name || l.code || '']),
+    ),
+    areaNameMap: Object.fromEntries(
+      areas.map((a) => [a.id!, a.name || a.code || '']),
+    ),
+  };
+  return cached;
+}
+
+/** 鏍规嵁 ID 娓叉煋 浠撳簱/搴撳尯/搴撲綅 鏂囨湰锛圛D 缂哄け鏃惰繑鍥炵┖涓诧級 */
+export function formatWmLocation(
+  maps: WmLocationMaps,
+  warehouseId?: number,
+  locationId?: number,
+  areaId?: number,
+): string {
+  return [
+    warehouseId ? maps.warehouseNameMap[warehouseId] : '',
+    locationId ? maps.locationNameMap[locationId] : '',
+    areaId ? maps.areaNameMap[areaId] : '',
+  ]
+    .filter(Boolean)
+    .join(' / ');
+}
\ No newline at end of file
diff --git a/src/views/wls/sn/modules/in-stock-modal.vue b/src/views/wls/sn/modules/in-stock-modal.vue
new file mode 100644
index 0000000..540ad6c
--- /dev/null
+++ b/src/views/wls/sn/modules/in-stock-modal.vue
@@ -0,0 +1,62 @@
+<script lang="ts" setup>
+import type { MesWmSnApi } from '#/api/mes/wm/sn';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { registerSnInStock } from '#/api/mes/wm/sn';
+
+import { useInStockFormSchema } from '../data';
+
+defineOptions({ name: 'WlsSnInStockModal' });
+
+const emit = defineEmits(['success']);
+
+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() {
+    const { valid } = await formApi.validate();
+    if (!valid) {
+      return;
+    }
+    modalApi.lock();
+    const data = (await formApi.getValues()) as MesWmSnApi.SnInStock;
+    try {
+      await registerSnInStock(data);
+      await modalApi.close();
+      emit('success');
+      message.success('鍏ュ簱鐧昏鎴愬姛');
+    } finally {
+      modalApi.unlock();
+    }
+  },
+  async onOpenChange(isOpen: boolean) {
+    if (!isOpen) {
+      return;
+    }
+    formApi.setState({ schema: useInStockFormSchema(formApi) });
+    await formApi.resetForm();
+  },
+});
+</script>
+
+<template>
+  <Modal title="SN 鐮佸叆搴撶櫥璁�" class="w-1/2">
+    <Form class="mx-4" />
+  </Modal>
+</template>
\ No newline at end of file
diff --git a/src/views/wls/sn/modules/out-stock-modal.vue b/src/views/wls/sn/modules/out-stock-modal.vue
new file mode 100644
index 0000000..0550842
--- /dev/null
+++ b/src/views/wls/sn/modules/out-stock-modal.vue
@@ -0,0 +1,62 @@
+<script lang="ts" setup>
+import type { MesWmSnApi } from '#/api/mes/wm/sn';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { outboundSn } from '#/api/mes/wm/sn';
+
+import { useOutStockFormSchema } from '../data';
+
+defineOptions({ name: 'WlsSnOutStockModal' });
+
+const emit = defineEmits(['success']);
+
+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() {
+    const { valid } = await formApi.validate();
+    if (!valid) {
+      return;
+    }
+    modalApi.lock();
+    const data = (await formApi.getValues()) as MesWmSnApi.SnOutStock;
+    try {
+      await outboundSn(data);
+      await modalApi.close();
+      emit('success');
+      message.success('鍑哄簱鏍搁攢鎴愬姛');
+    } finally {
+      modalApi.unlock();
+    }
+  },
+  async onOpenChange(isOpen: boolean) {
+    if (!isOpen) {
+      return;
+    }
+    formApi.setState({ schema: useOutStockFormSchema() });
+    await formApi.resetForm();
+  },
+});
+</script>
+
+<template>
+  <Modal title="SN 鐮佸嚭搴撴牳閿�" class="w-1/2">
+    <Form class="mx-4" />
+  </Modal>
+</template>
\ No newline at end of file
diff --git a/src/views/wls/sn/modules/sn-detail-modal.vue b/src/views/wls/sn/modules/sn-detail-modal.vue
new file mode 100644
index 0000000..757cf09
--- /dev/null
+++ b/src/views/wls/sn/modules/sn-detail-modal.vue
@@ -0,0 +1,118 @@
+<script lang="ts" setup>
+import type { MesWmSnApi } from '#/api/mes/wm/sn';
+
+import { computed, ref } from 'vue';
+
+import { Button, Empty, Input, message, Modal } from 'ant-design-vue';
+
+import { getSnDetail } from '#/api/mes/wm/sn';
+import { useDescription } from '#/components/description';
+
+import { useSnDetailSchema } from '../data';
+import { formatWmLocation, getWmLocationMaps } from '../location-maps';
+
+defineOptions({ name: 'WlsSnDetailModal' });
+
+const open = ref(false);
+const code = ref('');
+const detail = ref<MesWmSnApi.SnDetail>();
+const loading = ref(false);
+const locationMaps = ref<Awaited<ReturnType<typeof getWmLocationMaps>>>();
+
+const locationText = computed(() =>
+  detail.value
+    ? formatWmLocation(
+        locationMaps.value ?? {
+          warehouseNameMap: {},
+          locationNameMap: {},
+          areaNameMap: {},
+        },
+        detail.value.warehouseId,
+        detail.value.locationId,
+        detail.value.areaId,
+      )
+    : '-',
+);
+
+const lastTransactionText = computed(() => {
+  const d = detail.value;
+  if (!d) {
+    return '-';
+  }
+  const direction = d.lastTransactionTypeName || '';
+  const quantity = d.lastTransactionQuantity ?? '';
+  const bizCode = d.lastTransactionBizCode || '-';
+  return [direction, quantity, bizCode].filter(Boolean).join(' ');
+});
+
+async function loadDetail(snCode: string) {
+  loading.value = true;
+  try {
+    detail.value = await getSnDetail(snCode);
+    if (!locationMaps.value) {
+      locationMaps.value = await getWmLocationMaps();
+    }
+  } finally {
+    loading.value = false;
+  }
+}
+
+async function handleQuery() {
+  if (!code.value?.trim()) {
+    message.warning('璇疯緭鍏ユ垨鎵爜鑾峰彇 SN 鐮�');
+    return;
+  }
+  await loadDetail(code.value.trim());
+}
+
+function openModal(snCode?: string) {
+  open.value = true;
+  code.value = snCode ?? '';
+  detail.value = undefined;
+  if (snCode) {
+    void loadDetail(snCode);
+  }
+}
+
+defineExpose({ open: openModal });
+
+const [Descriptions] = useDescription({
+  bordered: true,
+  column: 1,
+  schema: useSnDetailSchema(),
+  useCard: false,
+  labelStyle: { width: '80px', whiteSpace: 'nowrap' },
+});
+</script>
+
+<template>
+  <Modal v-model:open="open" title="SN 鐮佸崟浠惰鎯�" width="720px">
+    <div class="flex gap-2">
+      <Input
+        v-model:value="code"
+        placeholder="杈撳叆鎴栨壂鐮� SN 鐮佸悗鏌ヨ"
+        @press-enter="handleQuery"
+      />
+      <Button type="primary" :loading="loading" @click="handleQuery">
+        鏌ヨ
+      </Button>
+    </div>
+    <div class="mt-4">
+      <Descriptions v-if="detail" :data="detail">
+        <template #location>
+          {{ locationText }}
+        </template>
+        <template #lastTransaction>
+          {{ lastTransactionText }}
+        </template>
+      </Descriptions>
+      <Empty
+        v-else
+        :description="loading ? '鏌ヨ涓�...' : '鏆傛棤鏁版嵁锛岃杈撳叆 SN 鐮佹煡璇�'"
+      />
+    </div>
+    <template #footer>
+      <Button @click="open = false">鍏抽棴</Button>
+    </template>
+  </Modal>
+</template>
\ No newline at end of file
diff --git a/src/views/wls/stocktaking/plan/data.ts b/src/views/wls/stocktaking/plan/data.ts
index 263e3fa..c301458 100644
--- a/src/views/wls/stocktaking/plan/data.ts
+++ b/src/views/wls/stocktaking/plan/data.ts
@@ -133,6 +133,40 @@
       rules: z.boolean().default(false),
     },
     {
+      fieldName: 'autoGenerate',
+      label: '鑷姩鐢熸垚',
+      component: 'Switch',
+      componentProps: {
+        checkedChildren: '鏄�',
+        unCheckedChildren: '鍚�',
+      },
+      rules: z.boolean().default(false),
+    },
+    {
+      fieldName: 'generateCron',
+      label: '鐢熸垚鍛ㄦ湡(Cron)',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏� Cron 琛ㄨ揪寮忥紝濡� 0 0 2 * * ?',
+        helpMessage:
+          '鍚敤鑷姩鐢熸垚鍚庯紝闇�鍦ㄣ�岀郴缁熺鐞�->瀹氭椂浠诲姟銆嶉厤缃� handler=MesWmStockTakingPlanAutoJob 鐨� cron 浠诲姟锛屽埌鏈熻嚜鍔ㄦ寜鏈柟妗堢敓鎴愮洏鐐逛换鍔�',
+      },
+      dependencies: {
+        triggerFields: ['autoGenerate'],
+        show: (values) => !!values.autoGenerate,
+      },
+    },
+    {
+      fieldName: 'lastGenerateTime',
+      label: '涓婃鐢熸垚鏃堕棿',
+      component: 'Input',
+      disabled: true,
+      dependencies: {
+        triggerFields: [''],
+        show: () => false,
+      },
+    },
+    {
       fieldName: 'remark',
       label: '澶囨敞',
       component: 'Textarea',
@@ -238,6 +272,21 @@
       },
     },
     {
+      field: 'autoGenerate',
+      title: '鑷姩鐢熸垚',
+      width: 100,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
+      },
+    },
+    {
+      field: 'lastGenerateTime',
+      title: '涓婃鐢熸垚鏃堕棿',
+      width: 180,
+      formatter: 'formatDateTime',
+    },
+    {
       field: 'status',
       title: '鐘舵��',
       width: 120,
diff --git a/src/views/wls/stocktaking/task/data.ts b/src/views/wls/stocktaking/task/data.ts
index ed62e0c..f16203e 100644
--- a/src/views/wls/stocktaking/task/data.ts
+++ b/src/views/wls/stocktaking/task/data.ts
@@ -29,13 +29,11 @@
 } from '#/views/wls/warehouse/components';
 
 /** 琛ㄥ崟绫诲瀷 */
-export type FormType = 'create' | 'detail' | 'execute' | 'submit' | 'update';
+export type FormType = 'create' | 'detail' | 'submit' | 'update';
 
-/** 琛ㄥ崟澶撮儴鏄惁鍙锛堟彁浜ゃ�佹墽琛岀洏鐐广�佽鎯呮�侊級 */
+/** 琛ㄥ崟澶撮儴鏄惁鍙锛堟彁浜ゃ�佽鎯呮�侊級 */
 function isHeaderReadonly(formType: FormType): boolean {
-  return (
-    formType === 'detail' || formType === 'execute' || formType === 'submit'
-  );
+  return formType === 'detail' || formType === 'submit';
 }
 
 /** 鏂板/淇敼鐨勮〃鍗� */
diff --git a/src/views/wls/stocktaking/task/index.vue b/src/views/wls/stocktaking/task/index.vue
index c38699d..30c46cc 100644
--- a/src/views/wls/stocktaking/task/index.vue
+++ b/src/views/wls/stocktaking/task/index.vue
@@ -3,6 +3,7 @@
 import type { MesWmStockTakingTaskApi } from '#/api/mes/wm/stocktaking/task';
 
 import { ref } from 'vue';
+import { useRouter } from 'vue-router';
 
 import { Page, useVbenModal } from '@vben/common-ui';
 import { MesWmStockTakingTaskStatusEnum } from '@vben/constants';
@@ -15,7 +16,9 @@
   cancelStockTaking,
   deleteStockTaking,
   exportStockTaking,
+  getStockTakingApproveProcessList,
   getStockTakingPage,
+  submitStockTakingApproval,
 } from '#/api/mes/wm/stocktaking/task';
 import {
   importStockTakingQuantity,
@@ -23,11 +26,19 @@
 } from '#/api/wls/stocktaking/task/line';
 import { $t } from '#/locales';
 
+import ProcessSelectModal from '#/views/wls/components/process-select-modal.vue';
 import { useGridColumns, useGridFormSchema } from './data';
 import Form from './modules/form.vue';
 
+const router = useRouter();
+
 const [FormModal, formModalApi] = useVbenModal({
   connectedComponent: Form,
+  destroyOnClose: true,
+});
+
+const [ProcessModal, processModalApi] = useVbenModal({
+  connectedComponent: ProcessSelectModal,
   destroyOnClose: true,
 });
 
@@ -51,14 +62,46 @@
   formModalApi.setData({ formType: 'update', id: row.id }).open();
 }
 
-/** 鎻愪氦鐩樼偣浠诲姟 */
+/** 寮�濮嬬洏鐐� */
 function handleSubmit(row: MesWmStockTakingTaskApi.StockTakingTask) {
   formModalApi.setData({ formType: 'submit', id: row.id }).open();
 }
 
-/** 鎵ц鐩樼偣 */
-function handleExecute(row: MesWmStockTakingTaskApi.StockTakingTask) {
-  formModalApi.setData({ formType: 'execute', id: row.id }).open();
+/** 鎻愪氦缁撴灉瀹℃壒 - 鍏堣幏鍙栨祦绋嬪垪琛� */
+async function handleSubmitApproval(row: MesWmStockTakingTaskApi.StockTakingTask) {
+  const hideLoading = message.loading({
+    content: '鑾峰彇瀹℃壒娴佺▼...',
+    duration: 0,
+  });
+  try {
+    const processList = await getStockTakingApproveProcessList();
+    hideLoading();
+    if (!processList || processList.length === 0) {
+      message.warning('鏆傛棤鍙敤瀹℃壒娴佺▼锛岃鍏堥厤缃� BPM 娴佺▼');
+      return;
+    }
+    processModalApi
+      .setData({
+        id: row.id,
+        processList,
+        submitFn: submitStockTakingApproval,
+      })
+      .open();
+  } catch {
+    hideLoading();
+  }
+}
+
+/** 鏌ョ湅瀹℃壒璇︽儏 */
+function handleProcessDetail(row: MesWmStockTakingTaskApi.StockTakingTask) {
+  if (!row.processInstanceId) {
+    message.warning('璇ヤ换鍔″皻鏈彁浜ょ粨鏋滃鎵�');
+    return;
+  }
+  router.push({
+    name: 'BpmProcessInstanceDetail',
+    query: { id: row.processInstanceId },
+  });
 }
 
 /** 鍙栨秷鐩樼偣浠诲姟 */
@@ -159,6 +202,7 @@
 
 <template>
   <Page auto-content-height><FormModal @success="handleRefresh" />
+    <ProcessModal @success="handleRefresh" />
 
     <Grid table-title="鐩樼偣浠诲姟鍒楄〃">
       <template #toolbar-tools>
@@ -205,7 +249,7 @@
               onClick: handleEdit.bind(null, row),
             },
             {
-              label: '鎻愪氦',
+              label: '寮�濮嬬洏鐐�',
               type: 'link',
               auth: ['mes:wm-stock-taking-task:update'],
               ifShow: row.status === StatusEnum.PREPARE,
@@ -224,13 +268,6 @@
               },
             },
             {
-              label: '鎵ц鐩樼偣',
-              type: 'link',
-              auth: ['mes:wm-stock-taking-task:update'],
-              ifShow: row.status === StatusEnum.APPROVING,
-              onClick: handleExecute.bind(null, row),
-            },
-            {
               label: '瀵煎叆瀹炵洏鏁�',
               type: 'link',
               icon: ACTION_ICON.UPLOAD,
@@ -239,6 +276,20 @@
               onClick: handleImport.bind(null, row),
             },
             {
+              label: '鎻愪氦缁撴灉瀹℃壒',
+              type: 'link',
+              auth: ['mes:wm-stock-taking-task:update'],
+              ifShow: row.status === StatusEnum.APPROVING,
+              onClick: handleSubmitApproval.bind(null, row),
+            },
+            {
+              label: '鏌ョ湅瀹℃壒',
+              type: 'link',
+              auth: ['mes:wm-stock-taking-task:query'],
+              ifShow: row.status === StatusEnum.APPROVING,
+              onClick: handleProcessDetail.bind(null, row),
+            },
+            {
               label: '鍙栨秷',
               type: 'link',
               danger: true,
diff --git a/src/views/wls/stocktaking/task/modules/form.vue b/src/views/wls/stocktaking/task/modules/form.vue
index ec1c5ba..05cfca2 100644
--- a/src/views/wls/stocktaking/task/modules/form.vue
+++ b/src/views/wls/stocktaking/task/modules/form.vue
@@ -14,7 +14,6 @@
 import { useVbenForm } from '#/adapter/form';
 import {
   createStockTaking,
-  finishStockTaking,
   getStockTaking,
   submitStockTaking,
   updateStockTaking,
@@ -35,29 +34,23 @@
 const isEditable = computed(() =>
   ['create', 'update'].includes(formType.value),
 );
-const isExecute = computed(() => formType.value === 'execute'); // 鏄惁鎵ц鐩樼偣妯″紡
-const isSubmit = computed(() => formType.value === 'submit'); // 鏄惁鎻愪氦妯″紡
-const canSubmit = computed(
-  () =>
-    formType.value === 'update' &&
-    formData.value?.status === MesWmStockTakingTaskStatusEnum.PREPARE,
+const isSubmit = computed(() => formType.value === 'submit'); // 鏄惁寮�濮嬬洏鐐规ā寮�
+const isResultEditable = computed(
+  // 鐩樼偣涓紙瀹℃壒涓級鍙淮鎶ょ洏鐐圭粨鏋�
+  () => formData.value?.status === MesWmStockTakingTaskStatusEnum.APPROVING,
 );
 const showLineTab = computed(() => !formData.value?.blindFlag); // 鐩茬洏涓嶅睍绀虹洏鐐规竻鍗�
 const showResultTab = computed(
   () =>
-    isExecute.value ||
-    (!!formData.value?.status &&
-      formData.value.status !== MesWmStockTakingTaskStatusEnum.PREPARE),
+    !!formData.value?.status &&
+    formData.value.status !== MesWmStockTakingTaskStatusEnum.PREPARE,
 );
 const getTitle = computed(() => {
   if (formType.value === 'detail') {
     return $t('ui.actionTitle.view', ['鐩樼偣浠诲姟']);
   }
-  if (formType.value === 'execute') {
-    return '鎵ц鐩樼偣';
-  }
   if (formType.value === 'submit') {
-    return '鎻愪氦鐩樼偣浠诲姟';
+    return '寮�濮嬬洏鐐�';
   }
   return formType.value === 'update'
     ? $t('ui.actionTitle.edit', ['鐩樼偣浠诲姟'])
@@ -78,7 +71,7 @@
   wrapperClass: 'grid-cols-3',
 });
 
-/** 鎻愪氦鐩樼偣浠诲姟锛氳〃鍗曟湁淇敼鏃跺厛淇濆瓨锛屽啀璋冪敤鎻愪氦鎺ュ彛 */
+/** 寮�濮嬬洏鐐癸細琛ㄥ崟鏈変慨鏀规椂鍏堜繚瀛橈紝鍐嶆彁浜� */
 async function handleSubmit() {
   const { valid } = await formApi.validate();
   if (!valid || !formData.value?.id) {
@@ -94,23 +87,7 @@
       originalSnapshot.value = current;
     }
     await submitStockTaking(formData.value.id);
-    message.success('鎻愪氦鎴愬姛');
-    await modalApi.close();
-    emit('success');
-  } finally {
-    modalApi.unlock();
-  }
-}
-
-/** 鎵ц鐩樼偣 */
-async function handleExecute() {
-  if (!formData.value?.id) {
-    return;
-  }
-  modalApi.lock();
-  try {
-    await finishStockTaking(formData.value.id);
-    message.success('鎵ц鐩樼偣鎴愬姛');
+    message.success('寮�濮嬬洏鐐规垚鍔�');
     await modalApi.close();
     emit('success');
   } finally {
@@ -163,7 +140,7 @@
     // 鍔犺浇鏁版嵁
     const data = modalApi.getData<{ formType: FormType; id?: number }>();
     formType.value = data.formType;
-    subTabsName.value = data.formType === 'execute' ? 'results' : 'lines';
+    subTabsName.value = 'lines';
     formApi.setState({ schema: useFormSchema(formType.value, formApi) });
     formApi.setDisabled(!isEditable.value);
     modalApi.setState({ showConfirmButton: isEditable.value });
@@ -199,7 +176,7 @@
       </Tabs.TabPane>
       <Tabs.TabPane v-if="showResultTab" key="results" tab="鐩樼偣缁撴灉">
         <ResultList
-          :form-type="isExecute ? 'execute' : 'detail'"
+          :form-type="isResultEditable ? 'execute' : 'detail'"
           :task-id="formData.id"
         />
       </Tabs.TabPane>
@@ -207,18 +184,11 @@
     <template #prepend-footer>
       <div class="flex flex-auto items-center gap-2">
         <Popconfirm
-          v-if="canSubmit || isSubmit"
-          title="纭鎻愪氦璇ョ洏鐐逛换鍔★紵銆愭彁浜ゅ悗灏嗕笉鑳戒慨鏀广��"
+          v-if="isSubmit"
+          title="纭寮�濮嬬洏鐐癸紵寮�濮嬪悗灏嗗喕缁撳簱瀛樺苟鍙綍鍏�/瀵煎叆瀹炵洏鏁般��"
           @confirm="handleSubmit"
         >
-          <Button type="primary">鎻愪氦</Button>
-        </Popconfirm>
-        <Popconfirm
-          v-if="isExecute"
-          title="纭鎵ц鐩樼偣鎿嶄綔锛�"
-          @confirm="handleExecute"
-        >
-          <Button type="primary">鎵ц鐩樼偣</Button>
+          <Button type="primary">寮�濮嬬洏鐐�</Button>
         </Popconfirm>
       </div>
     </template>
diff --git a/src/views/wls/transaction/data.ts b/src/views/wls/transaction/data.ts
index 117aa3f..a294a3e 100644
--- a/src/views/wls/transaction/data.ts
+++ b/src/views/wls/transaction/data.ts
@@ -30,6 +30,11 @@
   { label: '閿�鍞��璐�', value: 120 },
   { label: '澶栧崗鍙戞枡', value: 121 },
   { label: '澶栧崗鍏ュ簱', value: 122 },
+  { label: '鐩樼偣璋冩暣', value: 107 },
+  { label: 'SN 鍑哄叆搴�', value: 109 },
+  { label: '閲囪喘鍏ュ簱绾㈠啿', value: 111 },
+  { label: '璁惧鍙拌处鍏ュ簱', value: 400 },
+  { label: '璁惧鍏ュ簱', value: 401 },
 ];
 
 /** 鍒楄〃鐨勬悳绱㈣〃鍗� */

--
Gitblit v1.9.3