From f08a81f01bc4a4104dd9ef04e071bb0bd0eb8afe Mon Sep 17 00:00:00 2001
From: zhangwencui <1064582902@qq.com>
Date: 星期四, 23 七月 2026 10:50:19 +0800
Subject: [PATCH] Merge branch 'dev_pro2.0' of http://114.132.189.42:9002/r/mom-pro2-before into dev_pro2.0

---
 CLAUDE.md                                                       |    6 
 src/api/mes/pro/task/index.ts                                   |   42 
 src/views/wls/materialstock/data.ts                             |   11 
 src/views/mes/pro/workorder-center/data.ts                      |  182 +++
 .claude/skills/vben-table-form.md                               |  421 +++++++
 pnpm-lock.yaml                                                  |   64 
 src/api/mes/pro/workorder/process/index.ts                      |   69 +
 src/views/mes/pro/feedback/index.vue                            |   40 
 src/views/mes/pro/task/modules/schedule-form.vue                |   50 
 src/views/mes/pro/workorder-center/modules/quick-feedback.vue   |  158 ++
 src/views/wls/materialstock/components/index.ts                 |    1 
 src/api/mes/pro/feedback/index.ts                               |   55 
 src/views/mes/pro/workorder/modules/process-list.vue            |  304 +++++
 .claude/rules/typescript.md                                     |  135 ++
 .claude/skills/api-service-pattern.md                           |  213 +++
 src/components/file-preview/file-preview-modal.vue              |    4 
 src/views/mes/pro/workorder/modules/process-form.vue            |  319 +++++
 src/views/mes/pro/feedback/modules/batch-feedback.vue           |  147 ++
 .claude/settings.local.json                                     |   13 
 src/views/mes/pro/workorder-center/modules/schedule-preview.vue |   86 +
 src/views/wls/materialstock/index.vue                           |   33 
 src/views/mes/pro/workorder/data.ts                             |  103 +
 src/views/mes/pro/workorder-center/index.vue                    |  313 +++++
 src/api/mes/pro/workorder/index.ts                              |   47 
 src/views/mes/pro/task/index.vue                                |   44 
 src/views/mes/pro/workorder/modules/form.vue                    |  115 +
 package.json                                                    |    5 
 src/api/mes/wm/materialstock/index.ts                           |   15 
 .claude/skills/vue-component-patterns.md                        |  170 ++
 internal/node-utils/package.json                                |    3 
 .claude/rules/vue-component.md                                  |  166 ++
 src/views/wls/materialstock/components/move-modal.vue           |  237 +++
 32 files changed, 3,487 insertions(+), 84 deletions(-)

diff --git a/.claude/rules/typescript.md b/.claude/rules/typescript.md
new file mode 100644
index 0000000..e2f700b
--- /dev/null
+++ b/.claude/rules/typescript.md
@@ -0,0 +1,135 @@
+---
+paths:
+  - "**/*.ts"
+  - "**/*.vue"
+---
+
+# TypeScript 鍓嶇寮�鍙戣鍒�
+
+> 鍩轰簬 ECC TypeScript 瑙勫垯鎵╁睍锛岄�傞厤 Vue Vben Admin 椤圭洰銆�
+
+## 绫诲瀷瀹氫箟
+
+### 绂佹 any
+
+```typescript
+// 鉂� 閿欒锛氫娇鐢� any
+function handleData(data: any) {
+  return data.value;
+}
+
+// 鉁� 姝g‘锛氫娇鐢ㄥ叿浣撶被鍨�
+interface DataType {
+  value: string;
+}
+
+function handleData(data: DataType) {
+  return data.value;
+}
+
+// 鉁� 姝g‘锛氫娇鐢� unknown + 绫诲瀷瀹堝崼
+function getErrorMessage(error: unknown): string {
+  if (error instanceof Error) {
+    return error.message;
+  }
+  return '鏈煡閿欒';
+}
+```
+
+### import type
+
+```typescript
+// 鉁� 姝g‘锛氱被鍨嬪鍏ヤ娇鐢� import type
+import type { UserInfo } from '#/api/system/user';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+// 鉂� 閿欒锛氭櫘閫氬鍏ョ被鍨�
+import { UserInfo } from '#/api/system/user';
+```
+
+### 鎺ュ彛瀹氫箟
+
+```typescript
+// 鉁� 姝g‘锛氬疄浣撴帴鍙�
+export namespace UserApi {
+  export interface User {
+    id?: number;
+    username: string;
+    nickname?: string;
+    status: number;
+  }
+}
+
+// 鉁� 姝g‘锛歅rops 鎺ュ彛
+interface Props {
+  userId: number;
+  userName?: string;
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  userName: '',
+});
+```
+
+## 涓嶅彲鍙樻��
+
+```typescript
+// 鉂� 閿欒锛氱洿鎺ヤ慨鏀� props
+props.userName = 'new name';
+
+// 鉁� 姝g‘锛氫娇鐢� emit 鏇存柊
+emit('update:userName', 'new name');
+
+// 鉁� 姝g‘锛氫娇鐢ㄥ睍寮�杩愮畻绗�
+const newUser = { ...user, name: 'new name' };
+```
+
+## 閿欒澶勭悊
+
+```typescript
+// 鉁� 姝g‘锛歛sync/await + try-catch
+async function loadUser(id: number) {
+  try {
+    const user = await getUser(id);
+    return user;
+  } catch (error: unknown) {
+    if (error instanceof Error) {
+      message.error(error.message);
+    }
+    throw error;
+  }
+}
+```
+
+## 璺緞鍒悕
+
+```typescript
+// 鉁� 姝g‘锛氫娇鐢� # 鍒悕
+import { getUser } from '#/api/system/user';
+import DictTag from '#/components/dict-tag/dict-tag.vue';
+
+// 鉂� 閿欒锛氫娇鐢ㄧ浉瀵硅矾寰�
+import { getUser } from '../../../api/system/user';
+```
+
+## 缁勪欢鍛藉悕
+
+```typescript
+// 鉁� 姝g‘锛氱粍浠舵枃浠跺悕 PascalCase
+UserList.vue
+DictTag.vue
+TableAction.vue
+
+// 鉁� 姝g‘锛氬畾涔夌粍浠跺悕
+defineOptions({ name: 'UserList' });
+```
+
+## 鎺у埗鍙拌緭鍑�
+
+```typescript
+// 鉂� 绂佹锛歝onsole.log 鍦ㄧ敓浜т唬鐮佷腑
+console.log('debug info');
+
+// 鉁� 姝g‘锛氫娇鐢ㄦ棩蹇楀簱鎴栫Щ闄�
+// 鐢熶骇鐜绉婚櫎鎵�鏈夎皟璇曡緭鍑�
+```
\ No newline at end of file
diff --git a/.claude/rules/vue-component.md b/.claude/rules/vue-component.md
new file mode 100644
index 0000000..922b860
--- /dev/null
+++ b/.claude/rules/vue-component.md
@@ -0,0 +1,166 @@
+---
+paths:
+  - "**/*.vue"
+---
+
+# Vue 缁勪欢寮�鍙戣鍒�
+
+> 鍩轰簬 Vue Vben Admin 椤圭洰瑙勮寖銆�
+
+## 缁勪欢缁撴瀯
+
+### script setup
+
+```vue
+<script lang="ts" setup>
+// 1. 缁勪欢鍛藉悕锛堝繀椤伙級
+defineOptions({ name: 'ComponentName' });
+
+// 2. 绫诲瀷瀵煎叆
+import type { PropType } from './types';
+
+// 3. Props 瀹氫箟
+interface Props {
+  id: number;
+  name?: string;
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  name: '',
+});
+
+// 4. Emits 瀹氫箟
+interface Emits {
+  (e: 'update', value: string): void;
+  (e: 'delete', id: number): void;
+}
+
+const emit = defineEmits<Emits>();
+
+// 5. 鍝嶅簲寮忕姸鎬�
+const loading = ref(false);
+
+// 6. 璁$畻灞炴��
+const displayName = computed(() => props.name || '鏈懡鍚�');
+
+// 7. 鏂规硶
+function handleSubmit() {
+  emit('update', displayName.value);
+}
+</script>
+
+<template>
+  <div class="component-name">
+    <!-- 妯℃澘鍐呭 -->
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.component-name {
+  /* 鏍峰紡 */
+}
+</style>
+```
+
+## Props 瑙勮寖
+
+```vue
+<script lang="ts" setup>
+// 鉁� 姝g‘锛氫娇鐢� withDefaults + defineProps
+interface Props {
+  userId: number;
+  userName?: string;
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  userName: '',
+});
+
+// 鉂� 閿欒锛氫娇鐢ㄨ繍琛屾椂澹版槑
+const props = defineProps({
+  userId: { type: Number, required: true },
+  userName: { type: String, default: '' },
+});
+</script>
+```
+
+## Emits 瑙勮寖
+
+```vue
+<script lang="ts" setup>
+// 鉁� 姝g‘锛氱被鍨嬪寲 Emits
+interface Emits {
+  (e: 'update', value: string): void;
+  (e: 'delete', id: number): void;
+  (e: 'change', data: { id: number; name: string }): void;
+}
+
+const emit = defineEmits<Emits>();
+
+// 鉂� 閿欒锛氳繍琛屾椂澹版槑
+const emit = defineEmits(['update', 'delete']);
+</script>
+```
+
+## 鏍峰紡瑙勮寖
+
+```vue
+<template>
+  <!-- 鉁� 姝g‘锛氫紭鍏堜娇鐢� Tailwind CSS -->
+  <div class="flex items-center p-4 bg-white rounded-lg">
+    <span class="text-base font-medium text-gray-900">鏍囬</span>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+/* 鉁� 姝g‘锛氱粍浠舵牱寮忎娇鐢� scoped */
+.component-name {
+  /* 鉁� 姝g‘锛氶鑹蹭娇鐢� CSS 鍙橀噺 */
+  background-color: var(--component-background);
+
+  /* 鉁� 姝g‘锛氳鐩栫粍浠跺簱鏍峰紡浣跨敤 :deep */
+  :deep(.ant-table) {
+    border-radius: 8px;
+  }
+}
+
+/* 鉂� 閿欒锛氱‖缂栫爜棰滆壊 */
+.bad-style {
+  background-color: #ffffff;
+}
+</style>
+```
+
+## 缁勪欢鍛藉悕
+
+| 绫诲瀷 | 鍛藉悕瑙勫垯 | 绀轰緥 |
+|-----|---------|-----|
+| 椤甸潰缁勪欢 | 涓庣洰褰曞悕涓�鑷� | `UserList.vue` 鈫� `name: 'UserList'` |
+| 琛ㄥ崟寮圭獥 | 鍔熻兘 + Form | `UserForm.vue` 鈫� `name: 'UserForm'` |
+| 璇︽儏寮圭獥 | 鍔熻兘 + Detail | `UserDetail.vue` 鈫� `name: 'UserDetail'` |
+| 閫夋嫨鍣� | 鍔熻兘 + Select | `UserSelect.vue` 鈫� `name: 'UserSelect'` |
+| 鍒楄〃缁勪欢 | 鍔熻兘 + List | `UserBomList.vue` 鈫� `name: 'UserBomList'` |
+
+## 瀵煎叆璺緞
+
+```vue
+<script lang="ts" setup>
+// 鉁� 姝g‘锛氫娇鐢� # 鍒悕
+import { getUser } from '#/api/system/user';
+import DictTag from '#/components/dict-tag/dict-tag.vue';
+import { useVbenForm } from '#/adapter/form';
+
+// 鉁� 姝g‘锛氱浉瀵硅矾寰勫鍏ュ悓鐩綍
+import Form from './modules/form.vue';
+import { useGridColumns } from './data';
+</script>
+```
+
+## 娉ㄦ剰浜嬮」
+
+1. **蹇呴』浣跨敤 `defineOptions` 瀹氫箟缁勪欢鍚�**
+2. **缁熶竴浣跨敤 `<script setup lang="ts">` 璇硶**
+3. **Props 浣跨敤 `withDefaults` + `defineProps<T>()`**
+4. **Emits 浣跨敤 `defineEmits<T>()`**
+5. **鏍峰紡浼樺厛浣跨敤 Tailwind CSS**
+6. **缁勪欢鏍峰紡蹇呴』 `scoped`**
\ No newline at end of file
diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index 5d7a2ad..3d8e4b9 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -9,7 +9,18 @@
       "PowerShell(Get-ChildItem -Path \"D:\\\\宸ヤ綔\\\\鑺鍏徃\\\\椤圭洰\\\\杩涢攢瀛榎\\\mom-pro2-before\\\\src\\\\views\\\\mes\" -Directory -Recurse | Where-Object { $_.GetDirectories\\(\\).Count -gt 0 -or $_.Parent.Name -in @\\('mes','md','pro','process-design','cal','dv','qc','wm','tm','pd'\\) } | Select-Object -ExpandProperty FullName | ForEach-Object { $_.Substring\\(66\\) })",
       "PowerShell(cd \"D:\\\\宸ヤ綔\\\\鑺鍏徃\\\\椤圭洰\\\\杩涢攢瀛榎\\\mom-pro2-before\"; Get-ChildItem -Path \"src/views/mes\" -Directory -Recurse | Select-Object -ExpandProperty FullName | ForEach-Object { $_.Replace\\('D:\\\\宸ヤ綔\\\\鑺鍏徃\\\\椤圭洰\\\\杩涢攢瀛榎\\\mom-pro2-before\\\\', ''\\) })",
       "PowerShell(cd \"D:\\\\宸ヤ綔\\\\鑺鍏徃\\\\椤圭洰\\\\杩涢攢瀛榎\\\mom-pro2-before\"; Get-ChildItem -Path \"src/api/mes\" -Recurse -File | Select-Object -ExpandProperty FullName | ForEach-Object { $_.Replace\\('D:\\\\宸ヤ綔\\\\鑺鍏徃\\\\椤圭洰\\\\杩涢攢瀛榎\\\mom-pro2-before\\\\', ''\\) })",
-      "PowerShell(cd \"D:\\\\宸ヤ綔\\\\鑺鍏徃\\\\椤圭洰\\\\杩涢攢瀛榎\\\mom-pro2-before\"; Get-ChildItem -Path \"src/router\" -Recurse -File | Select-Object -ExpandProperty FullName | ForEach-Object { $_.Replace\\('D:\\\\宸ヤ綔\\\\鑺鍏徃\\\\椤圭洰\\\\杩涢攢瀛榎\\\mom-pro2-before\\\\', ''\\) })"
+      "PowerShell(cd \"D:\\\\宸ヤ綔\\\\鑺鍏徃\\\\椤圭洰\\\\杩涢攢瀛榎\\\mom-pro2-before\"; Get-ChildItem -Path \"src/router\" -Recurse -File | Select-Object -ExpandProperty FullName | ForEach-Object { $_.Replace\\('D:\\\\宸ヤ綔\\\\鑺鍏徃\\\\椤圭洰\\\\杩涢攢瀛榎\\\mom-pro2-before\\\\', ''\\) })",
+      "Bash(pnpm approve-builds *)",
+      "Bash(pnpm list *)",
+      "Bash(pnpm search *)",
+      "Bash(pnpm add *)",
+      "Bash(pnpm exec *)",
+      "Bash(pnpm stub *)",
+      "Bash(pnpm dev *)",
+      "Bash(Get-Content \"C:\\\\Users\\\\浜慭\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D-----------mom-pro2-after-mom-pro2-before\\\\3ceea64d-d347-46b2-88a2-57009ccd0961\\\\tasks\\\\bak5v4put.output\")",
+      "Bash(pnpm rebuild *)",
+      "Bash(node lib/script/postinstall.js)",
+      "Bash(mvn compile *)"
     ]
   }
 }
diff --git a/.claude/skills/api-service-pattern.md b/.claude/skills/api-service-pattern.md
new file mode 100644
index 0000000..ddb07ce
--- /dev/null
+++ b/.claude/skills/api-service-pattern.md
@@ -0,0 +1,213 @@
+---
+name: api-service-pattern
+description: API 鎺ュ彛瀹氫箟妯″紡锛屽寘鎷被鍨嬪畾涔夈�佹帴鍙e嚱鏁般�佸垎椤垫煡璇€�佸懡鍚嶇┖闂磋鑼冦��
+origin: ECC
+---
+
+# API 鎺ュ彛瀹氫箟妯″紡
+
+椤圭洰 API 鎺ュ彛寮�鍙戣鑼冿紝浣跨敤 namespace 缁勭粐绫诲瀷鍜屽嚱鏁般��
+
+## When to Use
+
+- 鍒涘缓鏂扮殑 API 鎺ュ彛鏂囦欢
+- 瀹氫箟鎺ュ彛璇锋眰/鍝嶅簲绫诲瀷
+- 缂栧啓 CRUD 鎺ュ彛鍑芥暟
+- 澶勭悊鍒嗛〉鏌ヨ
+
+## 鏂囦欢缁撴瀯
+
+```
+src/api/
+鈹斺攢鈹� mes/
+    鈹斺攢鈹� pro/
+        鈹斺攢鈹� workorder/
+            鈹溾攢鈹� index.ts          # 宸ュ崟鎺ュ彛
+            鈹斺攢鈹� bom/
+                鈹斺攢鈹� index.ts      # 宸ュ崟 BOM 鎺ュ彛
+```
+
+## 鎺ュ彛瀹氫箟妯℃澘
+
+```typescript
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace WorkOrderApi {
+  /** 宸ュ崟淇℃伅 */
+  export interface WorkOrder {
+    id?: number;                    // 缂栧彿
+    code?: string;                  // 缂栫爜
+    name?: string;                  // 鍚嶇О
+    status?: number;                // 鐘舵��
+    createTime?: number;            // 鍒涘缓鏃堕棿
+  }
+
+  /** 鍒嗛〉鏌ヨ鍙傛暟 */
+  export interface PageParams extends PageParam {
+    code?: string;
+    name?: string;
+    status?: number;
+  }
+}
+
+/** 鏌ヨ宸ュ崟鍒嗛〉 */
+export function getWorkOrderPage(params: WorkOrderApi.PageParams) {
+  return requestClient.get<PageResult<WorkOrderApi.WorkOrder>>(
+    '/mes/pro/work-order/page',
+    { params },
+  );
+}
+
+/** 鏌ヨ宸ュ崟璇︽儏 */
+export function getWorkOrder(id: number) {
+  return requestClient.get<WorkOrderApi.WorkOrder>(
+    `/mes/pro/work-order/get?id=${id}`,
+  );
+}
+
+/** 鏂板宸ュ崟 */
+export function createWorkOrder(data: WorkOrderApi.WorkOrder) {
+  return requestClient.post<number>('/mes/pro/work-order/create', data);
+}
+
+/** 淇敼宸ュ崟 */
+export function updateWorkOrder(data: WorkOrderApi.WorkOrder) {
+  return requestClient.put('/mes/pro/work-order/update', data);
+}
+
+/** 鍒犻櫎宸ュ崟 */
+export function deleteWorkOrder(id: number) {
+  return requestClient.delete(`/mes/pro/work-order/delete?id=${id}`);
+}
+
+/** 瀵煎嚭宸ュ崟 */
+export function exportWorkOrder(params: any) {
+  return requestClient.download('/mes/pro/work-order/export-excel', { params });
+}
+```
+
+## 绫诲瀷瀹氫箟瑙勮寖
+
+### 瀹炰綋鎺ュ彛
+
+```typescript
+export namespace ItemApi {
+  /** 鐗╂枡淇℃伅 */
+  export interface Item {
+    id?: number;                    // 缂栧彿锛堝彲閫夛紝鏂板鏃舵棤锛�
+    code: string;                   // 缂栫爜锛堝繀濉級
+    name: string;                   // 鍚嶇О锛堝繀濉級
+    specification?: string;         // 瑙勬牸鍨嬪彿
+    unitMeasureId?: number;         // 鍗曚綅缂栧彿
+    unitMeasureName?: string;       // 鍗曚綅鍚嶇О锛堝啑浣欏瓧娈碉級
+    status?: number;                // 鐘舵��
+    remark?: string;                // 澶囨敞
+    createTime?: number;            // 鍒涘缓鏃堕棿
+  }
+}
+```
+
+### 鍒嗛〉鍙傛暟
+
+```typescript
+export namespace ItemApi {
+  /** 鍒嗛〉鏌ヨ鍙傛暟 */
+  export interface PageParams extends PageParam {
+    code?: string;                  // 缂栫爜锛堟ā绯婏級
+    name?: string;                  // 鍚嶇О锛堟ā绯婏級
+    status?: number;                // 鐘舵��
+    categoryId?: number;            // 鍒嗙被缂栧彿
+    createTime?: number[];          // 鍒涘缓鏃堕棿鑼冨洿
+  }
+}
+```
+
+### 鍒楄〃鎺ュ彛
+
+```typescript
+export namespace ProcessApi {
+  /** 宸ュ簭绮剧畝淇℃伅锛堜笅鎷夐�夋嫨鐢級 */
+  export interface ProcessSimple {
+    id: number;
+    code: string;
+    name: string;
+  }
+}
+
+/** 鏌ヨ宸ュ簭鍒楄〃 */
+export function getProcessList(params?: ProcessApi.ListParams) {
+  return requestClient.get<ProcessApi.Process[]>(
+    '/mes/pro/process/list',
+    { params },
+  );
+}
+
+/** 鏌ヨ宸ュ簭绮剧畝鍒楄〃 */
+export function getProcessSimpleList() {
+  return requestClient.get<ProcessApi.ProcessSimple[]>(
+    '/mes/pro/process/simple-list',
+  );
+}
+```
+
+## 璇锋眰鏂规硶鏄犲皠
+
+| 鎿嶄綔 | 鏂规硶 | URL 鏍煎紡 | 杩斿洖绫诲瀷 |
+|-----|------|---------|---------|
+| 鍒嗛〉鏌ヨ | GET | `/module/page` | `PageResult<T>` |
+| 鍒楄〃鏌ヨ | GET | `/module/list` | `T[]` |
+| 绮剧畝鍒楄〃 | GET | `/module/simple-list` | `SimpleT[]` |
+| 璇︽儏鏌ヨ | GET | `/module/get?id=` | `T` |
+| 鏂板 | POST | `/module/create` | `number` |
+| 淇敼 | PUT | `/module/update` | `void` |
+| 鍒犻櫎 | DELETE | `/module/delete?id=` | `void` |
+| 瀵煎嚭 | GET | `/module/export-excel` | `Blob` |
+
+## 瀛愭ā鍧楁帴鍙�
+
+瀛愭ā鍧楁帴鍙f斁鍦ㄧ埗妯″潡鐩綍涓嬶細
+
+```
+src/api/mes/pro/workorder/
+鈹溾攢鈹� index.ts          # 宸ュ崟鎺ュ彛
+鈹斺攢鈹� bom/
+    鈹斺攢鈹� index.ts      # 宸ュ崟 BOM 鎺ュ彛
+```
+
+```typescript
+// bom/index.ts
+export namespace WorkOrderBomApi {
+  export interface WorkOrderBom {
+    id?: number;
+    workOrderId?: number;           // 鍏宠仈鐖惰〃
+    itemId?: number;
+    itemName?: string;
+    quantity?: number;
+  }
+
+  export interface PageParams extends PageParam {
+    workOrderId?: number;           // 蹇呴』鏈夌埗琛� ID
+  }
+}
+```
+
+## 瀵煎叆浣跨敤
+
+```typescript
+// 瀵煎叆绫诲瀷
+import type { WorkOrderApi } from '#/api/mes/pro/workorder';
+import type { WorkOrderBomApi } from '#/api/mes/pro/workorder/bom';
+
+// 瀵煎叆鍑芥暟
+import { getWorkOrderPage, createWorkOrder } from '#/api/mes/pro/workorder';
+```
+
+## 娉ㄦ剰浜嬮」
+
+1. **浣跨敤 namespace 缁勭粐绫诲瀷**锛氶伩鍏嶇被鍨嬪懡鍚嶅啿绐�
+2. **瀹炰綋瀛楁鍏ㄩ儴鍙��**锛氶�傚簲鏂板/缂栬緫/鏌ヨ鍦烘櫙
+3. **鍒嗛〉鍙傛暟缁ф壙 PageParam**锛氬寘鍚� `pageNo`銆乣pageSize`
+4. **杩斿洖绫诲瀷鏄庣‘鎸囧畾**锛歚requestClient.get<T>`
+5. **璺緞鍙傛暟浣跨敤妯℃澘瀛楃涓�**锛歚/module/get?id=${id}`
\ No newline at end of file
diff --git a/.claude/skills/vben-table-form.md b/.claude/skills/vben-table-form.md
new file mode 100644
index 0000000..81ab6c2
--- /dev/null
+++ b/.claude/skills/vben-table-form.md
@@ -0,0 +1,421 @@
+---
+name: vben-table-form
+description: Vue Vben Admin 琛ㄦ牸鍜岃〃鍗曞紑鍙戞ā寮忥紝鍖呮嫭 useVbenVxeGrid銆乽seVbenModal銆乽seVbenForm 鐨勪娇鐢ㄣ��
+origin: ECC
+---
+
+# Vue Vben Admin 琛ㄦ牸琛ㄥ崟妯″紡
+
+椤圭洰鏍稿績缁勪欢鐨勪娇鐢ㄦ寚鍗楋細琛ㄦ牸锛坲seVbenVxeGrid锛夈�佽〃鍗曞脊绐楋紙useVbenModal锛夈�佽〃鍗曪紙useVbenForm锛夈��
+
+## When to Use
+
+- 寮�鍙戝垪琛ㄩ〉闈�
+- 寮�鍙戣〃鍗曞脊绐�
+- 閰嶇疆琛ㄦ牸鍒楀拰鎼滅储鏉′欢
+- 澶勭悊澧炲垹鏀规煡鎿嶄綔
+
+## 琛ㄦ牸缁勪欢 (useVbenVxeGrid)
+
+### 鍩虹鐢ㄦ硶
+
+```vue
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { UserApi } from '#/api/system/user';
+
+import { useVbenVxeGrid, TableAction, ACTION_ICON } from '#/adapter/vxe-table';
+import { getUserPage } from '#/api/system/user';
+import { getDictOptions } from '@vben/hooks';
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  formOptions: {
+    schema: [
+      {
+        fieldName: 'username',
+        label: '鐢ㄦ埛鍚�',
+        component: 'Input',
+      },
+      {
+        fieldName: 'status',
+        label: '鐘舵��',
+        component: 'Select',
+        componentProps: {
+          options: getDictOptions('common_status', 'number'),
+        },
+      },
+    ],
+  },
+  gridOptions: {
+    columns: [
+      { type: 'checkbox', width: 50 },
+      { field: 'username', title: '鐢ㄦ埛鍚�', width: 120 },
+      { field: 'nickname', title: '鏄电О', minWidth: 150 },
+      {
+        field: 'status',
+        title: '鐘舵��',
+        width: 100,
+        cellRender: {
+          name: 'CellDict',
+          props: { type: 'common_status' },
+        },
+      },
+      { field: 'createTime', title: '鍒涘缓鏃堕棿', width: 180, formatter: 'formatDateTime' },
+      { title: '鎿嶄綔', width: 180, fixed: 'right', slots: { default: 'actions' } },
+    ],
+    height: 'auto',
+    keepSource: true,
+    proxyConfig: {
+      ajax: {
+        query: async ({ page }, formValues) => {
+          return await getUserPage({
+            pageNo: page.currentPage,
+            pageSize: page.pageSize,
+            ...formValues,
+          });
+        },
+      },
+    },
+    rowConfig: {
+      keyField: 'id',
+      isHover: true,
+    },
+    toolbarConfig: {
+      refresh: true,
+      search: true,
+    },
+  } as VxeTableGridOptions<UserApi.User>,
+});
+
+// 鍒锋柊琛ㄦ牸
+function handleRefresh() {
+  gridApi.query();
+}
+</script>
+
+<template>
+  <Grid table-title="鐢ㄦ埛鍒楄〃">
+    <template #toolbar-tools>
+      <TableAction
+        :actions="[
+          {
+            label: '鏂板',
+            type: 'primary',
+            icon: ACTION_ICON.ADD,
+            onClick: handleCreate,
+          },
+        ]"
+      />
+    </template>
+    <template #actions="{ row }">
+      <TableAction
+        :actions="[
+          {
+            label: '缂栬緫',
+            type: 'link',
+            icon: ACTION_ICON.EDIT,
+            onClick: handleEdit.bind(null, row),
+          },
+          {
+            label: '鍒犻櫎',
+            type: 'link',
+            danger: true,
+            icon: ACTION_ICON.DELETE,
+            popConfirm: {
+              title: `纭鍒犻櫎銆�${row.username}銆戝悧锛焋,
+              confirm: handleDelete.bind(null, row),
+            },
+          },
+        ]"
+      />
+    </template>
+  </Grid>
+</template>
+```
+
+### 琛ㄦ牸鍒楁覆鏌撳櫒
+
+| 娓叉煋鍣� | 鐢ㄩ�� | 绀轰緥 |
+|-------|------|------|
+| `CellDict` | 瀛楀吀鏍囩 | `cellRender: { name: 'CellDict', props: { type: 'common_status' } }` |
+| `CellImage` | 鍗曞浘 | `cellRender: { name: 'CellImage' }` |
+| `CellImages` | 澶氬浘 | `cellRender: { name: 'CellImages' }` |
+| `CellLink` | 閾炬帴 | `cellRender: { name: 'CellLink', props: { text: '鏌ョ湅' } }` |
+| `CellSwitch` | 寮�鍏� | `cellRender: { name: 'CellSwitch', attrs: { beforeChange } }` |
+
+### 琛ㄦ牸浜嬩欢
+
+```typescript
+const [Grid, gridApi] = useVbenVxeGrid({
+  gridEvents: {
+    checkboxAll: ({ records }) => {
+      checkedIds.value = records.map(item => item.id!);
+    },
+    checkboxChange: ({ records }) => {
+      checkedIds.value = records.map(item => item.id!);
+    },
+  },
+});
+```
+
+## 琛ㄥ崟寮圭獥 (useVbenModal)
+
+### 鍒楄〃椤靛畾涔�
+
+```vue
+<script lang="ts" setup>
+import { useVbenModal } from '@vben/common-ui';
+import Form from './modules/form.vue';
+
+const [FormModal, formModalApi] = useVbenModal({
+  connectedComponent: Form,
+  destroyOnClose: true,
+});
+
+// 鏂板
+function handleCreate() {
+  formModalApi.setData({ formType: 'create' }).open();
+}
+
+// 缂栬緫
+function handleEdit(row: UserApi.User) {
+  formModalApi.setData({ formType: 'update', id: row.id }).open();
+}
+</script>
+
+<template>
+  <FormModal @success="handleRefresh" />
+</template>
+```
+
+### 琛ㄥ崟缁勪欢
+
+```vue
+<script lang="ts" setup>
+import type { UserApi } from '#/api/system/user';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+import { useVbenForm } from '#/adapter/form';
+import { createUser, updateUser, getUser } from '#/api/system/user';
+import { $t } from '#/locales';
+
+const emit = defineEmits(['success']);
+const formData = ref<UserApi.User>();
+const formType = ref<'create' | 'update'>('create');
+
+const getTitle = computed(() => {
+  return formType.value === 'update'
+    ? $t('ui.actionTitle.edit', ['鐢ㄦ埛'])
+    : $t('ui.actionTitle.create', ['鐢ㄦ埛']);
+});
+
+const [Form, formApi] = useVbenForm({
+  commonConfig: {
+    componentProps: { class: 'w-full' },
+    labelWidth: 100,
+  },
+  layout: 'horizontal',
+  schema: [
+    {
+      fieldName: 'username',
+      label: '鐢ㄦ埛鍚�',
+      component: 'Input',
+      rules: 'required',
+    },
+    {
+      fieldName: 'nickname',
+      label: '鏄电О',
+      component: 'Input',
+      rules: 'required',
+    },
+    {
+      fieldName: 'email',
+      label: '閭',
+      component: 'Input',
+      rules: 'email',
+    },
+    {
+      fieldName: 'status',
+      label: '鐘舵��',
+      component: 'Select',
+      componentProps: {
+        options: getDictOptions('common_status', 'number'),
+      },
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      formItemClass: 'col-span-2',
+    },
+  ],
+  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();
+
+    try {
+      if (formData.value?.id) {
+        await updateUser({ ...data, id: formData.value.id });
+      } else {
+        await createUser(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: string; id?: number }>();
+    formType.value = data.formType;
+
+    if (data.id) {
+      modalApi.lock();
+      try {
+        formData.value = await getUser(data.id);
+        await formApi.setValues(formData.value);
+      } finally {
+        modalApi.unlock();
+      }
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal :title="getTitle" class="w-2/5">
+    <Form class="mx-4" />
+  </Modal>
+</template>
+```
+
+## 琛ㄥ崟 Schema 閰嶇疆
+
+### 甯哥敤缁勪欢
+
+```typescript
+const schema: VbenFormSchema[] = [
+  // 杈撳叆妗�
+  {
+    fieldName: 'name',
+    label: '鍚嶇О',
+    component: 'Input',
+    componentProps: {
+      placeholder: '璇疯緭鍏ュ悕绉�',
+    },
+    rules: 'required',
+  },
+
+  // 鏁板瓧杈撳叆
+  {
+    fieldName: 'quantity',
+    label: '鏁伴噺',
+    component: 'InputNumber',
+    componentProps: {
+      class: '!w-full',
+      min: 0,
+      precision: 2,
+    },
+  },
+
+  // 涓嬫媺閫夋嫨
+  {
+    fieldName: 'status',
+    label: '鐘舵��',
+    component: 'Select',
+    componentProps: {
+      options: getDictOptions('common_status', 'number'),
+    },
+  },
+
+  // 鏃ユ湡閫夋嫨
+  {
+    fieldName: 'date',
+    label: '鏃ユ湡',
+    component: 'DatePicker',
+    componentProps: {
+      class: '!w-full',
+      format: 'YYYY-MM-DD',
+      valueFormat: 'x',
+    },
+  },
+
+  // 闅愯棌瀛楁
+  {
+    fieldName: 'id',
+    component: 'Input',
+    dependencies: {
+      triggerFields: [''],
+      show: () => false,
+    },
+  },
+
+  // 鏉′欢鏄剧ず
+  {
+    fieldName: 'clientId',
+    label: '瀹㈡埛',
+    component: markRaw(CrmCustomerSelect),
+    dependencies: {
+      triggerFields: ['orderSourceType'],
+      show: (values) => values.orderSourceType === 'ORDER',
+    },
+  },
+];
+```
+
+## TableAction 鎿嶄綔鎸夐挳
+
+```vue
+<TableAction
+  :actions="[
+    {
+      label: '缂栬緫',
+      type: 'link',
+      icon: ACTION_ICON.EDIT,
+      auth: ['system:user:update'],
+      onClick: handleEdit,
+    },
+    {
+      label: '鍒犻櫎',
+      type: 'link',
+      danger: true,
+      icon: ACTION_ICON.DELETE,
+      auth: ['system:user:delete'],
+      popConfirm: {
+        title: '纭鍒犻櫎鍚楋紵',
+        confirm: handleDelete,
+      },
+    },
+  ]"
+  :drop-down-actions="[
+    {
+      label: '鍒嗛厤瑙掕壊',
+      auth: ['system:user:assign-role'],
+      onClick: handleAssignRole,
+    },
+  ]"
+/>
+```
+
+## 娉ㄦ剰浜嬮」
+
+1. **琛ㄦ牸蹇呴』鏈� `keyField`**锛歚rowConfig: { keyField: 'id' }`
+2. **寮圭獥蹇呴』 `destroyOnClose: true`**锛氶槻姝㈡暟鎹畫鐣�
+3. **琛ㄥ崟楠岃瘉浣跨敤 `rules` 灞炴��**锛氭敮鎸� `required`銆乣email`銆乣selectRequired` 绛�
+4. **鏉冮檺鎺у埗浣跨敤 `auth` 灞炴��**锛歚auth: ['system:user:update']`
\ No newline at end of file
diff --git a/.claude/skills/vue-component-patterns.md b/.claude/skills/vue-component-patterns.md
new file mode 100644
index 0000000..d110756
--- /dev/null
+++ b/.claude/skills/vue-component-patterns.md
@@ -0,0 +1,170 @@
+---
+name: vue-component-patterns
+description: Vue 3 缁勪欢寮�鍙戞ā寮忥紝鍖呮嫭 Composition API銆丳rops/Emits 瀹氫箟銆佺粍浠跺懡鍚嶈鑼冦�傞�傜敤浜庡紑鍙� Vue 缁勪欢鏃躲��
+origin: ECC
+---
+
+# Vue 3 缁勪欢寮�鍙戞ā寮�
+
+鍩轰簬 Vue Vben Admin 椤圭洰鐨� Vue 3 缁勪欢寮�鍙戣鑼冦��
+
+## When to Use
+
+- 寮�鍙戞柊鐨� Vue 缁勪欢
+- 瀹氫箟缁勪欢 Props 鍜� Emits
+- 浣跨敤 Composition API
+- 缁勪欢鐘舵�佺鐞嗗拰鐢熷懡鍛ㄦ湡
+
+## How It Works
+
+椤圭洰浣跨敤 Vue 3.5 + TypeScript + `<script setup>` 璇硶銆�
+
+## 缁勪欢瀹氫箟瑙勮寖
+
+### 鍩虹缁撴瀯
+
+```vue
+<script lang="ts" setup>
+import type { PropsType } from './types';
+
+import { ref, computed } from 'vue';
+
+// 1. 缁勪欢鍛藉悕锛堝繀椤伙級
+defineOptions({ name: 'ComponentName' });
+
+// 2. Props 瀹氫箟
+interface Props {
+  userId: number;
+  userName?: string;
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  userName: '',
+});
+
+// 3. Emits 瀹氫箟
+interface Emits {
+  (e: 'update', value: string): void;
+  (e: 'delete', id: number): void;
+}
+
+const emit = defineEmits<Emits>();
+
+// 4. 鍝嶅簲寮忕姸鎬�
+const loading = ref(false);
+
+// 5. 璁$畻灞炴��
+const displayName = computed(() => props.userName || '鏈懡鍚�');
+
+// 6. 鏂规硶
+function handleSubmit() {
+  emit('update', displayName.value);
+}
+</script>
+
+<template>
+  <div class="component-name">
+    <!-- 妯℃澘鍐呭 -->
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.component-name {
+  /* 鏍峰紡 */
+}
+</style>
+```
+
+### TypeScript 瑙勮寖
+
+```typescript
+// 鉁� 姝g‘锛氫娇鐢� import type 瀵煎叆绫诲瀷
+import type { UserInfo } from '#/api/system/user';
+
+// 鉁� 姝g‘锛氬畾涔夊叿浣撴帴鍙�
+interface Props {
+  userId: number;
+  userName?: string;
+}
+
+// 鉂� 閿欒锛氫娇鐢� any
+const data: any = {};
+
+// 鉁� 姝g‘锛氫娇鐢� unknown + 绫诲瀷瀹堝崼
+function getErrorMessage(error: unknown): string {
+  if (error instanceof Error) {
+    return error.message;
+  }
+  return '鏈煡閿欒';
+}
+```
+
+## 椤甸潰鏂囦欢缁撴瀯
+
+鏍囧噯 CRUD 椤甸潰鐨勬枃浠剁粨鏋勶細
+
+```
+views/
+鈹斺攢鈹� system/
+    鈹斺攢鈹� user/
+        鈹溾攢鈹� index.vue        # 鍒楄〃椤�
+        鈹溾攢鈹� data.ts          # 鏁版嵁瀹氫箟锛堣〃鏍煎垪銆佽〃鍗� Schema锛�
+        鈹斺攢鈹� modules/
+            鈹溾攢鈹� form.vue     # 琛ㄥ崟寮圭獥
+            鈹斺攢鈹� detail.vue   # 璇︽儏寮圭獥锛堝彲閫夛級
+```
+
+### 瀵煎叆璺緞灞傜骇
+
+| 鏂囦欢浣嶇疆 | 瀵煎叆灞傜骇 |
+|---------|---------|
+| `index.vue` | 4 灞� `../..` |
+| `modules/form.vue` | 5 灞� `../../..` |
+
+```typescript
+// index.vue 涓殑瀵煎叆
+import { useFormSchema } from '../data';
+
+// modules/form.vue 涓殑瀵煎叆
+import { useFormSchema } from '../../data';
+```
+
+## 缁勪欢鍛藉悕瑙勮寖
+
+| 绫诲瀷 | 鍛藉悕瑙勫垯 | 绀轰緥 |
+|-----|---------|-----|
+| 椤甸潰缁勪欢 | 涓庣洰褰曞悕涓�鑷� | `UserList.vue` |
+| 涓氬姟缁勪欢 | 鍔熻兘鍚� + 绫诲瀷 | `UserSelect.vue` |
+| 鍩虹缁勪欢 | 鍔熻兘鍚� | `Button.vue` |
+| 寮圭獥缁勪欢 | 鍔熻兘 + Form/Detail | `UserForm.vue` |
+
+## 鏍峰紡瑙勮寖
+
+```vue
+<template>
+  <!-- 浼樺厛浣跨敤 Tailwind CSS -->
+  <div class="flex items-center p-4 bg-white rounded-lg">
+    <span class="text-base font-medium text-gray-900">鏍囬</span>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+/* 缁勪欢鏍峰紡浣跨敤 scoped */
+.component-name {
+  /* 棰滆壊浣跨敤 CSS 鍙橀噺 */
+  background-color: var(--component-background);
+
+  /* 瑕嗙洊缁勪欢搴撴牱寮忎娇鐢� :deep */
+  :deep(.ant-table) {
+    border-radius: 8px;
+  }
+}
+</style>
+```
+
+## 娉ㄦ剰浜嬮」
+
+1. **蹇呴』浣跨敤 `defineOptions` 瀹氫箟缁勪欢鍚�**
+2. **绂佹浣跨敤 `any` 绫诲瀷**
+3. **Props 浣跨敤 `withDefaults` + `defineProps<T>()`**
+4. **璺緞鍒悕 `#/*` 鎸囧悜 `./src/*`**
\ No newline at end of file
diff --git a/CLAUDE.md b/CLAUDE.md
index 4954570..9995cba 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -82,9 +82,9 @@
 
 ## 鍙敤鎶�鑳�
 
-- `vue3-development` - Vue 3 寮�鍙戣鑼�
-- `antd-vue-components` - Ant Design Vue 缁勪欢妯″紡
-- `frontend-code-review` - 鍓嶇浠g爜瀹℃煡鎸囧崡
+- `vue-component-patterns` - Vue 3 缁勪欢寮�鍙戞ā寮忥紙Composition API銆丳rops/Emits锛�
+- `vben-table-form` - 琛ㄦ牸琛ㄥ崟缁勪欢浣跨敤锛坲seVbenVxeGrid銆乽seVbenModal銆乽seVbenForm锛�
+- `api-service-pattern` - API 鎺ュ彛瀹氫箟妯″紡锛坣amespace銆佺被鍨嬪畾涔夈�丆RUD 鍑芥暟锛�
 
 ## 娉ㄦ剰浜嬮」
 
diff --git a/internal/node-utils/package.json b/internal/node-utils/package.json
index 35892a1..618fa4a 100644
--- a/internal/node-utils/package.json
+++ b/internal/node-utils/package.json
@@ -38,5 +38,8 @@
     "ora": "catalog:",
     "pkg-types": "catalog:",
     "rimraf": "catalog:"
+  },
+  "devDependencies": {
+    "unrun": "^0.3.1"
   }
 }
diff --git a/package.json b/package.json
index 109e0d2..4dbcf6f 100644
--- a/package.json
+++ b/package.json
@@ -84,5 +84,6 @@
     "tsx": "^4.22.4",
     "typescript": "catalog:",
     "vite": "catalog:"
-  }
-}
\ No newline at end of file
+  },
+  "packageManager": "pnpm@11.0.9+sha512.34ce82e6780233cf9cad8685029a8f81d2e06196c5a9bad98879f7424940c6817c4e4524fb7d38b8553ceed48b9758b8ebaf1abd3600c232c4c8cf7366086f38"
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index bc9f97f..e317c5f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -695,7 +695,7 @@
         version: 2.7.0
       tsdown:
         specifier: ^0.22.3
-        version: 0.22.3(tsx@4.22.4)(typescript@6.0.3)
+        version: 0.22.3(tsx@4.22.4)(typescript@6.0.3)(unrun@0.3.1(synckit@0.11.13))
       tsx:
         specifier: ^4.22.4
         version: 4.22.4
@@ -871,6 +871,10 @@
       rimraf:
         specifier: 'catalog:'
         version: 6.1.3
+    devDependencies:
+      unrun:
+        specifier: ^0.3.1
+        version: 0.3.1(synckit@0.11.13)
 
   internal/tailwind-config:
     dependencies:
@@ -7315,9 +7319,6 @@
   lodash.snakecase@4.1.1:
     resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==}
 
-  lodash.sortby@4.7.0:
-    resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==}
-
   lodash.startcase@4.4.0:
     resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==}
 
@@ -8635,10 +8636,9 @@
     resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==}
     engines: {node: '>= 12'}
 
-  source-map@0.8.0-beta.0:
-    resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==}
-    engines: {node: '>= 8'}
-    deprecated: The work that was done in this beta branch won't be included in future versions
+  source-map@0.8.0:
+    resolution: {integrity: sha512-d8EqvL+k/SOXCreS/SUzg2ciyHqBBLcN/yuRjFsbvVhHTE2pgei7oAhmPM7kWFbkX6OSMQfUq4KbkF3au9lhYQ==}
+    engines: {node: '>= 12'}
 
   spawndamnit@3.0.1:
     resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==}
@@ -9026,9 +9026,6 @@
   tr46@0.0.3:
     resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
 
-  tr46@1.0.1:
-    resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==}
-
   tree-kill@1.2.2:
     resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
     hasBin: true
@@ -9304,6 +9301,16 @@
   unplugin@3.0.0:
     resolution: {integrity: sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==}
     engines: {node: ^20.19.0 || >=22.12.0}
+
+  unrun@0.3.1:
+    resolution: {integrity: sha512-onIck/oNnCaytwths1ZVp1LK2Gq2hPoyFhiHebObuUXqR3S0uHuLLaBK8K6mRRgV7Ptip8AnNvaUsgzwWwBZuA==}
+    engines: {node: ^22.13.0 || >=24.0.0}
+    hasBin: true
+    peerDependencies:
+      synckit: ^0.11.11
+    peerDependenciesMeta:
+      synckit:
+        optional: true
 
   unstorage@1.17.5:
     resolution: {integrity: sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==}
@@ -9633,9 +9640,6 @@
   webidl-conversions@3.0.0:
     resolution: {integrity: sha512-GOK494qXZl0XBbr/YPRW2BL6oLfe+d0KDSWndKH7NdxJwJ0fCdhQG46Q2D7BmIS6cN/FJgtju8srRt/jlG9abA==}
 
-  webidl-conversions@4.0.2:
-    resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==}
-
   webpack-virtual-modules@0.6.2:
     resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
 
@@ -9654,9 +9658,6 @@
 
   whatwg-url@5.0.0:
     resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
-
-  whatwg-url@7.0.0:
-    resolution: {integrity: sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ==}
 
   which-boxed-primitive@1.1.1:
     resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
@@ -15596,8 +15597,6 @@
 
   lodash.snakecase@4.1.1: {}
 
-  lodash.sortby@4.7.0: {}
-
   lodash.startcase@4.4.0: {}
 
   lodash.truncate@4.4.2: {}
@@ -17085,9 +17084,7 @@
 
   source-map@0.7.6: {}
 
-  source-map@0.8.0-beta.0:
-    dependencies:
-      whatwg-url: 7.0.0
+  source-map@0.8.0: {}
 
   spawndamnit@3.0.1:
     dependencies:
@@ -17551,10 +17548,6 @@
 
   tr46@0.0.3: {}
 
-  tr46@1.0.1:
-    dependencies:
-      punycode: 2.3.1
-
   tree-kill@1.2.2: {}
 
   trim-newlines@5.0.0: {}
@@ -17576,7 +17569,7 @@
       minimist: 1.2.8
       strip-bom: 3.0.0
 
-  tsdown@0.22.3(tsx@4.22.4)(typescript@6.0.3):
+  tsdown@0.22.3(tsx@4.22.4)(typescript@6.0.3)(unrun@0.3.1(synckit@0.11.13)):
     dependencies:
       ansis: 4.3.1
       cac: 7.0.0
@@ -17596,6 +17589,7 @@
     optionalDependencies:
       tsx: 4.22.4
       typescript: 6.0.3
+      unrun: 0.3.1(synckit@0.11.13)
     transitivePeerDependencies:
       - '@ts-macro/tsc'
       - '@typescript/native-preview'
@@ -17829,6 +17823,12 @@
       '@jridgewell/remapping': 2.3.5
       picomatch: 4.0.4
       webpack-virtual-modules: 0.6.2
+
+  unrun@0.3.1(synckit@0.11.13):
+    dependencies:
+      rolldown: 1.1.2
+    optionalDependencies:
+      synckit: 0.11.13
 
   unstorage@1.17.5(db0@0.3.4)(ioredis@5.11.1):
     dependencies:
@@ -18148,8 +18148,6 @@
 
   webidl-conversions@3.0.0: {}
 
-  webidl-conversions@4.0.2: {}
-
   webpack-virtual-modules@0.6.2: {}
 
   webrtc-adapter@9.0.5:
@@ -18166,12 +18164,6 @@
     dependencies:
       tr46: 0.0.3
       webidl-conversions: 3.0.0
-
-  whatwg-url@7.0.0:
-    dependencies:
-      lodash.sortby: 4.7.0
-      tr46: 1.0.1
-      webidl-conversions: 4.0.2
 
   which-boxed-primitive@1.1.1:
     dependencies:
@@ -18254,7 +18246,7 @@
       glob: 11.0.1
       pretty-bytes: 5.6.0
       rollup: 4.62.2
-      source-map: 0.8.0-beta.0
+      source-map: 0.8.0
       stringify-object: 3.3.0
       strip-comments: 2.0.1
       tempy: 0.6.0
diff --git a/src/api/mes/pro/feedback/index.ts b/src/api/mes/pro/feedback/index.ts
index b098949..78eecc3 100644
--- a/src/api/mes/pro/feedback/index.ts
+++ b/src/api/mes/pro/feedback/index.ts
@@ -114,6 +114,61 @@
   return requestClient.put<boolean>(`/mes/pro/feedback/approve?id=${id}`);
 }
 
+/** 蹇�熸姤宸ヨ姹� */
+export interface QuickCreateReq {
+  taskId: number;
+  feedbackQuantity: number;
+  qualifiedQuantity?: number;
+  unqualifiedQuantity?: number;
+  remark?: string;
+}
+
+/** 蹇�熸姤宸ュ搷搴� */
+export interface QuickCreateResp {
+  feedbackId?: number;
+  taskStatus?: number;
+  workOrderStatus?: number;
+}
+
+/** 鎵归噺鎶ュ伐璇锋眰 */
+export interface BatchCreateReq {
+  taskIds: number[];
+  feedbackQuantity: number;
+  qualifiedQuantity?: number;
+  unqualifiedQuantity?: number;
+  remark?: string;
+}
+
+/** 鎵归噺鎶ュ伐鍝嶅簲 */
+export interface BatchCreateResp {
+  successCount?: number;
+  failCount?: number;
+  results?: BatchResult[];
+}
+
+export interface BatchResult {
+  taskId?: number;
+  success?: boolean;
+  feedbackId?: number;
+  errorMsg?: string;
+}
+
+/** 蹇�熸姤宸ワ紙鍒涘缓骞舵彁浜わ級 */
+export function quickCreateFeedback(data: QuickCreateReq) {
+  return requestClient.post<QuickCreateResp>(
+    '/mes/pro/feedback/quick-create',
+    data,
+  );
+}
+
+/** 鎵归噺鎶ュ伐 */
+export function batchCreateFeedback(data: BatchCreateReq) {
+  return requestClient.post<BatchCreateResp>(
+    '/mes/pro/feedback/batch-create',
+    data,
+  );
+}
+
 /** 鎸変换鍔℃煡璇㈡姤宸ュ巻鍙� */
 export function getFeedbackListByTask(taskId: number) {
   return requestClient.get<MesProFeedbackApi.Feedback[]>(
diff --git a/src/api/mes/pro/task/index.ts b/src/api/mes/pro/task/index.ts
index 2dba85e..f710da5 100644
--- a/src/api/mes/pro/task/index.ts
+++ b/src/api/mes/pro/task/index.ts
@@ -114,6 +114,48 @@
   return requestClient.get<any[]>('/mes/pro/task/gantt-list', { params });
 }
 
+/** 涓�閿帓浜ч瑙堣姹� */
+export interface AutoGenerateReq {
+  workOrderId: number;
+  routeId?: number;
+  autoAssignWorkstation?: boolean;
+  autoCalculateTime?: boolean;
+}
+
+/** 涓�閿帓浜ч瑙堝搷搴� */
+export interface AutoGeneratePreview {
+  workOrderId?: number;
+  routeId?: number;
+  routeName?: string;
+  taskList?: TaskPreview[];
+}
+
+export interface TaskPreview {
+  processId?: number;
+  processName?: string;
+  sort?: number;
+  suggestWorkstationId?: number;
+  suggestWorkstationName?: string;
+  quantity?: number;
+  keyFlag?: boolean;
+}
+
+/** 涓�閿帓浜ч瑙� */
+export function previewAutoGenerate(data: AutoGenerateReq) {
+  return requestClient.post<AutoGeneratePreview>(
+    '/mes/pro/task/auto-generate/preview',
+    data,
+  );
+}
+
+/** 涓�閿帓浜ф墽琛� */
+export function autoGenerateTasks(data: AutoGenerateReq) {
+  return requestClient.post<{ taskIds: number[] }>(
+    '/mes/pro/task/auto-generate',
+    data,
+  );
+}
+
 /** 鏌ヨ寰呮姤宸ュ彴璐︼紙鍒嗛〉锛� */
 export function getPendingFeedbackPage(
   params: MesProTaskApi.PendingFeedbackPageParams,
diff --git a/src/api/mes/pro/workorder/index.ts b/src/api/mes/pro/workorder/index.ts
index 97c689e..15a7635 100644
--- a/src/api/mes/pro/workorder/index.ts
+++ b/src/api/mes/pro/workorder/index.ts
@@ -99,3 +99,50 @@
 export function confirmWorkOrder(id: number) {
   return requestClient.put(`/mes/pro/work-order/confirm?id=${id}`);
 }
+
+/** 宸ュ崟杩涘害璇︽儏 */
+export interface WorkOrderProgress {
+  workOrderId?: number;
+  workOrderCode?: string;
+  quantity?: number;
+  quantityScheduled?: number;
+  quantityProduced?: number;
+  progressPercent?: number;
+  processList?: ProcessProgress[];
+}
+
+export interface ProcessProgress {
+  processId?: number;
+  processCode?: string;
+  processName?: string;
+  keyFlag?: boolean;
+  plannedQuantity?: number;
+  producedQuantity?: number;
+  progressPercent?: number;
+  status?: number;
+  taskList?: TaskInfo[];
+}
+
+export interface TaskInfo {
+  taskId?: number;
+  taskCode?: string;
+  workstationName?: string;
+  quantity?: number;
+  producedQuantity?: number;
+  progressPercent?: number;
+  status?: number;
+}
+
+/** 鑾峰彇宸ュ崟杩涘害璇︽儏 */
+export function getWorkOrderProgress(id: number) {
+  return requestClient.get<WorkOrderProgress>(
+    `/mes/pro/work-order/progress/${id}`,
+  );
+}
+
+/** 妫�鏌ュ苟鑷姩瀹屾垚宸ュ崟 */
+export function checkAndAutoFinishWorkOrder(id: number) {
+  return requestClient.post<boolean>(
+    `/mes/pro/work-order/check-auto-finish/${id}`,
+  );
+}
diff --git a/src/api/mes/pro/workorder/process/index.ts b/src/api/mes/pro/workorder/process/index.ts
new file mode 100644
index 0000000..6833d3f
--- /dev/null
+++ b/src/api/mes/pro/workorder/process/index.ts
@@ -0,0 +1,69 @@
+import type { MesProWorkOrderProcessApi } from './index';
+
+import { requestClient } from '#/api/request';
+
+export namespace MesProWorkOrderProcessApi {
+  /** MES 鐢熶骇宸ュ崟宸ュ簭 */
+  export interface WorkOrderProcess {
+    id?: number; // 缂栧彿
+    workOrderId?: number; // 鐢熶骇宸ュ崟缂栧彿
+    processId?: number; // 宸ュ簭缂栧彿
+    processCode?: string; // 宸ュ簭缂栫爜
+    processName?: string; // 宸ュ簭鍚嶇О
+    sort?: number; // 搴忓彿
+    nextProcessId?: number; // 涓嬩竴閬撳伐搴忕紪鍙�
+    nextProcessName?: string; // 涓嬩竴閬撳伐搴忓悕绉�
+    linkType?: number; // 涓庝笅涓�閬撳伐搴忓叧绯�
+    prepareTime?: number; // 鍑嗗鏃堕棿锛堝垎閽燂級
+    waitTime?: number; // 绛夊緟鏃堕棿锛堝垎閽燂級
+    colorCode?: string; // 鐢樼壒鍥炬樉绀洪鑹�
+    keyFlag?: boolean; // 鏄惁鍏抽敭宸ュ簭
+    checkFlag?: boolean; // 鏄惁璐ㄦ宸ュ簭
+    backflushFlag?: boolean; // 鏄惁鍊掑啿
+    outputItemId?: number; // 浜у嚭浜у搧鐗╂枡缂栧彿
+    outputItemCode?: string; // 浜у嚭浜у搧鐗╂枡缂栫爜
+    outputItemName?: string; // 浜у嚭浜у搧鐗╂枡鍚嶇О
+    remark?: string; // 澶囨敞
+  }
+}
+
+/** 鏌ヨ宸ュ崟宸ュ簭鍒楄〃 */
+export function getWorkOrderProcessListByWorkOrderId(workOrderId: number) {
+  return requestClient.get<MesProWorkOrderProcessApi.WorkOrderProcess[]>(
+    `/mes/pro/work-order-process/list-by-work-order-id?workOrderId=${workOrderId}`,
+  );
+}
+
+/** 鏍规嵁浜у搧鑾峰彇宸ヨ壓璺嚎宸ュ簭鍒楄〃锛堢敤浜庡伐鍗曢濉級 */
+export function getProcessListByProductId(productId: number) {
+  return requestClient.get<MesProWorkOrderProcessApi.WorkOrderProcess[]>(
+    `/mes/pro/work-order-process/list-by-product-id?productId=${productId}`,
+  );
+}
+
+/** 鎵归噺淇濆瓨宸ュ崟宸ュ簭锛堟柊澧�+鏇存柊+鍒犻櫎锛� */
+export function batchSaveWorkOrderProcess(data: MesProWorkOrderProcessApi.WorkOrderProcess[]) {
+  return requestClient.post('/mes/pro/work-order-process/batch-save', data);
+}
+
+/** 鏌ヨ宸ュ崟宸ュ簭璇︽儏 */
+export function getWorkOrderProcess(id: number) {
+  return requestClient.get<MesProWorkOrderProcessApi.WorkOrderProcess>(
+    `/mes/pro/work-order-process/get?id=${id}`,
+  );
+}
+
+/** 鏂板宸ュ崟宸ュ簭 */
+export function createWorkOrderProcess(data: MesProWorkOrderProcessApi.WorkOrderProcess) {
+  return requestClient.post<number>('/mes/pro/work-order-process/create', data);
+}
+
+/** 淇敼宸ュ崟宸ュ簭 */
+export function updateWorkOrderProcess(data: MesProWorkOrderProcessApi.WorkOrderProcess) {
+  return requestClient.put('/mes/pro/work-order-process/update', data);
+}
+
+/** 鍒犻櫎宸ュ崟宸ュ簭 */
+export function deleteWorkOrderProcess(id: number) {
+  return requestClient.delete(`/mes/pro/work-order-process/delete?id=${id}`);
+}
\ No newline at end of file
diff --git a/src/api/mes/wm/materialstock/index.ts b/src/api/mes/wm/materialstock/index.ts
index 97f95b1..edbd6fe 100644
--- a/src/api/mes/wm/materialstock/index.ts
+++ b/src/api/mes/wm/materialstock/index.ts
@@ -106,6 +106,21 @@
   return requestClient.post<number>('/mes/wm/material-stock/adjust-out', data);
 }
 
+/** 搴撳瓨绉诲簱 */
+export interface MaterialStockMoveParams {
+  materialStockId: number; // 搴撳瓨璁板綍ID
+  toWarehouseId: number; // 鐩爣浠撳簱ID
+  toLocationId: number; // 鐩爣搴撳尯ID
+  toAreaId: number; // 鐩爣搴撲綅ID
+  quantity: number; // 绉诲簱鏁伴噺
+  remark?: string; // 澶囨敞
+}
+
+/** 搴撳瓨绉诲簱 */
+export function moveMaterialStock(data: MaterialStockMoveParams) {
+  return requestClient.post<boolean>('/mes/wm/material-stock/move', data);
+}
+
 /** 搴撳瓨璋冩暣锛堟鏁�=鍏ュ簱锛岃礋鏁�=鍑哄簱锛�- 鏃ф帴鍙o紝淇濈暀鍏煎 */
 export interface MaterialStockAdjustParams {
   itemId: number; // 鐗╂枡ID
diff --git a/src/components/file-preview/file-preview-modal.vue b/src/components/file-preview/file-preview-modal.vue
index e6732e0..118d4d9 100644
--- a/src/components/file-preview/file-preview-modal.vue
+++ b/src/components/file-preview/file-preview-modal.vue
@@ -15,9 +15,7 @@
 const VueOfficePptx = defineAsyncComponent(() => import('@vue-office/pptx'));
 
 // @vue-office/excel 闇�瑕侀澶栧鍏� CSS
-// @vue-office/excel 闇�瑕侀澶栧鍏� CSS
-// @vue-office/excel 闇�瑕侀澶栧鍏� CSS
-import '@vue-office/excel-css';
+import '@vue-office/excel/lib/v3/style.css';
 
 const props = withDefaults(
   defineProps<{
diff --git a/src/views/mes/pro/feedback/index.vue b/src/views/mes/pro/feedback/index.vue
index b4457b7..239da4a 100644
--- a/src/views/mes/pro/feedback/index.vue
+++ b/src/views/mes/pro/feedback/index.vue
@@ -19,12 +19,24 @@
 
 import { useGridColumns, useGridFormSchema } from './data';
 import Form from './modules/form.vue';
+import QuickFeedbackForm from '../workorder-center/modules/quick-feedback.vue';
+import BatchFeedbackForm from './modules/batch-feedback.vue';
 
 const userStore = useUserStore();
 const currentUserId = userStore.userInfo?.id; // 褰撳墠鐧诲綍鐢ㄦ埛 ID锛岀敤浜庡鎵逛汉鏉冮檺鍒ゆ柇
 
 const [FormModal, formModalApi] = useVbenModal({
   connectedComponent: Form,
+  destroyOnClose: true,
+});
+
+const [QuickFeedbackModal, quickFeedbackModalApi] = useVbenModal({
+  connectedComponent: QuickFeedbackForm,
+  destroyOnClose: true,
+});
+
+const [BatchFeedbackModal, batchFeedbackModalApi] = useVbenModal({
+  connectedComponent: BatchFeedbackForm,
   destroyOnClose: true,
 });
 
@@ -79,6 +91,16 @@
   downloadFileFromBlobPart({ fileName: '鐢熶骇鎶ュ伐.xls', source: data });
 }
 
+/** 蹇�熸姤宸� */
+function handleQuickCreate() {
+  quickFeedbackModalApi.setData({}).open();
+}
+
+/** 鎵归噺鎶ュ伐 */
+function handleBatchCreate() {
+  batchFeedbackModalApi.setData({}).open();
+}
+
 const [Grid, gridApi] = useVbenVxeGrid({
   formOptions: { schema: useGridFormSchema() },
   gridOptions: {
@@ -103,7 +125,10 @@
 </script>
 
 <template>
-  <Page auto-content-height><FormModal @success="handleRefresh" />
+  <Page auto-content-height>
+    <FormModal @success="handleRefresh" />
+    <QuickFeedbackModal @success="handleRefresh" />
+    <BatchFeedbackModal @success="handleRefresh" />
     <Grid table-title="鐢熶骇鎶ュ伐鍒楄〃">
       <template #toolbar-tools>
         <TableAction
@@ -116,6 +141,19 @@
               onClick: handleCreate,
             },
             {
+              label: '蹇�熸姤宸�',
+              type: 'primary',
+              icon: ACTION_ICON.ADD,
+              auth: ['mes:pro-feedback:create'],
+              onClick: handleQuickCreate,
+            },
+            {
+              label: '鎵归噺鎶ュ伐',
+              type: 'primary',
+              auth: ['mes:pro-feedback:create'],
+              onClick: handleBatchCreate,
+            },
+            {
               label: $t('ui.actionTitle.export'),
               type: 'primary',
               icon: ACTION_ICON.DOWNLOAD,
diff --git a/src/views/mes/pro/feedback/modules/batch-feedback.vue b/src/views/mes/pro/feedback/modules/batch-feedback.vue
new file mode 100644
index 0000000..e8e08b2
--- /dev/null
+++ b/src/views/mes/pro/feedback/modules/batch-feedback.vue
@@ -0,0 +1,147 @@
+<script lang="ts" setup>
+import type { MesProTaskApi } from '#/api/mes/pro/task';
+
+import { ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { Button, message, Tag } from 'ant-design-vue';
+
+import { batchCreateFeedback } from '#/api/mes/pro/feedback';
+import ProTaskSelectDialog from '#/views/mes/pro/task/components/select-dialog.vue';
+
+defineOptions({ name: 'BatchFeedbackForm' });
+
+interface Emits {
+  (e: 'success'): void;
+}
+
+const emit = defineEmits<Emits>();
+
+const [Modal, modalApi] = useVbenModal({
+  onOpenChange: (open) => {
+    if (open) {
+      selectedTasks.value = [];
+      formData.value = {
+        feedbackQuantity: undefined,
+        qualifiedQuantity: undefined,
+        unqualifiedQuantity: undefined,
+        remark: '',
+      };
+    }
+  },
+});
+
+const taskDialogRef = ref<InstanceType<typeof ProTaskSelectDialog>>();
+const selectedTasks = ref<MesProTaskApi.Task[]>([]);
+const formData = ref({
+  feedbackQuantity: undefined as number | undefined,
+  qualifiedQuantity: undefined as number | undefined,
+  unqualifiedQuantity: undefined as number | undefined,
+  remark: '',
+});
+const submitting = ref(false);
+
+function handleOpenTaskDialog() {
+  taskDialogRef.value?.open([], { multiple: true });
+}
+
+function handleTaskSelected(rows: MesProTaskApi.Task[]) {
+  selectedTasks.value = rows;
+}
+
+function handleRemoveTask(taskId: number) {
+  selectedTasks.value = selectedTasks.value.filter((t) => t.id !== taskId);
+}
+
+async function handleSubmit() {
+  if (!selectedTasks.value.length) {
+    message.warning('璇烽�夋嫨浠诲姟');
+    return;
+  }
+  if (!formData.value.feedbackQuantity) {
+    message.warning('璇疯緭鍏ユ姤宸ユ暟閲�');
+    return;
+  }
+  submitting.value = true;
+  try {
+    const result = await batchCreateFeedback({
+      taskIds: selectedTasks.value.map((t) => t.id!),
+      feedbackQuantity: formData.value.feedbackQuantity,
+      qualifiedQuantity: formData.value.qualifiedQuantity,
+      unqualifiedQuantity: formData.value.unqualifiedQuantity,
+      remark: formData.value.remark || undefined,
+    });
+    if (result.failCount) {
+      message.warning(
+        `鎵归噺鎶ュ伐瀹屾垚锛氭垚鍔� ${result.successCount} 涓紝澶辫触 ${result.failCount} 涓猔,
+      );
+    } else {
+      message.success(`鎵归噺鎶ュ伐鎴愬姛锛屽叡 ${result.successCount} 涓猔);
+    }
+    modalApi.close();
+    emit('success');
+  } finally {
+    submitting.value = false;
+  }
+}
+</script>
+
+<template>
+  <Modal title="鎵归噺鎶ュ伐" @ok="handleSubmit">
+    <a-form layout="vertical">
+      <a-form-item label="閫夋嫨浠诲姟" required>
+        <div class="mb-2">
+          <Button type="dashed" @click="handleOpenTaskDialog">
+            {{ selectedTasks.length ? `宸查�� ${selectedTasks.length} 涓换鍔 : '鐐瑰嚮閫夋嫨浠诲姟' }}
+          </Button>
+        </div>
+        <div v-if="selectedTasks.length" class="flex flex-wrap gap-1">
+          <Tag
+            v-for="task in selectedTasks"
+            :key="task.id"
+            closable
+            @close="handleRemoveTask(task.id!)"
+          >
+            {{ task.code }}
+          </Tag>
+        </div>
+      </a-form-item>
+      <a-form-item label="鎶ュ伐鏁伴噺" required>
+        <a-input-number
+          v-model:value="formData.feedbackQuantity"
+          :min="0.01"
+          :precision="2"
+          placeholder="璇疯緭鍏ユ姤宸ユ暟閲忥紙搴旂敤鍒版墍鏈夐�変腑浠诲姟锛�"
+          class="!w-full"
+        />
+      </a-form-item>
+      <a-form-item label="鍚堟牸鍝佹暟閲�">
+        <a-input-number
+          v-model:value="formData.qualifiedQuantity"
+          :min="0"
+          :precision="2"
+          placeholder="璇疯緭鍏ュ悎鏍煎搧鏁伴噺"
+          class="!w-full"
+        />
+      </a-form-item>
+      <a-form-item label="涓嶈壇鍝佹暟閲�">
+        <a-input-number
+          v-model:value="formData.unqualifiedQuantity"
+          :min="0"
+          :precision="2"
+          placeholder="璇疯緭鍏ヤ笉鑹搧鏁伴噺"
+          class="!w-full"
+        />
+      </a-form-item>
+      <a-form-item label="澶囨敞">
+        <a-textarea
+          v-model:value="formData.remark"
+          placeholder="璇疯緭鍏ュ娉�"
+          :rows="2"
+        />
+      </a-form-item>
+    </a-form>
+    <ProTaskSelectDialog ref="taskDialogRef" @selected="handleTaskSelected" />
+  </Modal>
+</template>
diff --git a/src/views/mes/pro/task/index.vue b/src/views/mes/pro/task/index.vue
index ed4237c..cb0bb3a 100644
--- a/src/views/mes/pro/task/index.vue
+++ b/src/views/mes/pro/task/index.vue
@@ -11,21 +11,27 @@
   MesProWorkOrderTypeEnum,
 } from '@vben/constants';
 
-import { Button, Card } from 'ant-design-vue';
+import { Button, Card, message } from 'ant-design-vue';
 
 import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
-import { getGanttTaskList } from '#/api/mes/pro/task';
+import { getGanttTaskList, previewAutoGenerate, autoGenerateTasks } from '#/api/mes/pro/task';
 import { getWorkOrderPage } from '#/api/mes/pro/workorder';
 
 import { GanttChart } from './components';
 import { useGridColumns, useGridFormSchema } from './data';
 import ScheduleForm from './modules/schedule-form.vue';
+import SchedulePreview from '../workorder-center/modules/schedule-preview.vue';
 
 const router = useRouter();
 const ganttTasks = ref<any[]>([]); // 鐢樼壒鍥鹃瑙堟暟鎹�
 
 const [ScheduleModal, scheduleModalApi] = useVbenModal({
   connectedComponent: ScheduleForm,
+  destroyOnClose: true,
+});
+
+const [SchedulePreviewModal, schedulePreviewModalApi] = useVbenModal({
+  connectedComponent: SchedulePreview,
   destroyOnClose: true,
 });
 
@@ -51,6 +57,29 @@
 /** 鎺掍骇 */
 function handleSchedule(row: MesProWorkOrderApi.WorkOrder) {
   scheduleModalApi.setData({ formType: 'schedule', id: row.id }).open();
+}
+
+/** 涓�閿帓浜ч瑙� */
+async function handleAutoSchedulePreview(row: MesProWorkOrderApi.WorkOrder) {
+  try {
+    const preview = await previewAutoGenerate({ workOrderId: row.id! });
+    schedulePreviewModalApi
+      .setData({ workOrder: row, preview })
+      .open();
+  } catch {
+    message.error('鑾峰彇鎺掍骇棰勮澶辫触');
+  }
+}
+
+/** 涓�閿帓浜ф墽琛� */
+async function handleAutoSchedule(row: MesProWorkOrderApi.WorkOrder) {
+  try {
+    const result = await autoGenerateTasks({ workOrderId: row.id! });
+    message.success(`鎺掍骇鎴愬姛锛岀敓鎴� ${result.taskIds?.length || 0} 涓换鍔);
+    handleRefresh();
+  } catch {
+    message.error('鎺掍骇澶辫触');
+  }
 }
 
 /** 鎵撳紑鐢樼壒鍥剧紪杈戦〉闈� */
@@ -100,7 +129,9 @@
 </script>
 
 <template>
-  <Page auto-content-height><ScheduleModal @success="handleRefresh" />
+  <Page auto-content-height>
+    <ScheduleModal @success="handleRefresh" />
+    <SchedulePreviewModal @success="handleRefresh" />
 
     <!-- 鎺掍骇鐢樼壒鍥鹃瑙� -->
     <Card title="鎺掍骇鐢樼壒鍥�" class="mb-4">
@@ -135,6 +166,13 @@
               ifShow: row.status === MesProWorkOrderStatusEnum.CONFIRMED,
               onClick: handleSchedule.bind(null, row),
             },
+            {
+              label: '涓�閿帓浜�',
+              type: 'link',
+              auth: ['mes:pro-task:create'],
+              ifShow: row.status === MesProWorkOrderStatusEnum.CONFIRMED,
+              onClick: handleAutoSchedulePreview.bind(null, row),
+            },
           ]"
         />
       </template>
diff --git a/src/views/mes/pro/task/modules/schedule-form.vue b/src/views/mes/pro/task/modules/schedule-form.vue
index 592d470..e51d7b7 100644
--- a/src/views/mes/pro/task/modules/schedule-form.vue
+++ b/src/views/mes/pro/task/modules/schedule-form.vue
@@ -1,6 +1,6 @@
 锘�<script lang="ts" setup>
-import type { MesProRouteProcessApi } from '#/api/mes/pro/route/process';
 import type { MesProWorkOrderApi } from '#/api/mes/pro/workorder';
+import type { MesProWorkOrderProcessApi } from '#/api/mes/pro/workorder/process';
 
 import { computed, ref } from 'vue';
 
@@ -11,6 +11,7 @@
 import { useVbenForm } from '#/adapter/form';
 import { getRouteProcessListByProduct } from '#/api/mes/pro/route/process';
 import { finishWorkOrder, getWorkOrder } from '#/api/mes/pro/workorder';
+import { getWorkOrderProcessListByWorkOrderId } from '#/api/mes/pro/workorder/process';
 
 import { useScheduleFormSchema } from '../data';
 import TaskList from './task-list.vue';
@@ -18,7 +19,8 @@
 const emit = defineEmits(['success']);
 const formType = ref<'detail' | 'schedule'>('schedule');
 const workOrder = ref<MesProWorkOrderApi.WorkOrder>(); // 褰撳墠璁㈠崟璇︽儏
-const routeProcessList = ref<MesProRouteProcessApi.RouteProcess[]>([]); // 宸ヨ壓璺嚎宸ュ簭鍒楄〃
+/** 鎺掍骇宸ュ簭鍒楄〃 */
+const routeProcessList = ref<MesProWorkOrderProcessApi.WorkOrderProcess[]>([]);
 const activeProcessStep = ref(0); // 褰撳墠宸ュ簭姝ラ绱㈠紩
 const currentRouteId = ref(0); // 褰撳墠宸ヨ壓璺嚎缂栧彿
 
@@ -41,17 +43,23 @@
   wrapperClass: 'grid-cols-3',
 });
 
-/** 鍔犺浇宸ヨ壓璺嚎宸ュ簭鍒楄〃 */
-async function loadRouteProcesses(productId: number) {
-  const processes = await getRouteProcessListByProduct(productId);
-  if (!processes || processes.length === 0) {
-    message.warning('褰撳墠浜у搧鏈厤缃伐鑹鸿矾绾匡紝璇峰厛鍦ㄥ伐鑹鸿矾绾夸腑缁存姢');
+/** 鍔犺浇宸ュ崟宸ュ簭鍒楄〃锛堜粎浣跨敤宸ュ崟宸ュ簭锛屼笉鍥為��鍒板伐鑹鸿矾绾挎ā鏉匡級 */
+async function loadRouteProcesses(workOrderId: number, productId: number) {
+  const woProcesses = await getWorkOrderProcessListByWorkOrderId(workOrderId);
+  if (!woProcesses || woProcesses.length === 0) {
+    message.warning('褰撳墠璁㈠崟鏈厤缃伐搴忥紝璇峰厛鍦ㄥ伐鍗曚腑缁存姢宸ュ簭淇℃伅');
     return;
   }
-  currentRouteId.value = processes[0]!.routeId ?? 0;
-  routeProcessList.value = [...processes].toSorted(
+  routeProcessList.value = [...woProcesses].toSorted(
     (a, b) => (a.sort ?? 0) - (b.sort ?? 0),
   );
+  // 纭畾宸ヨ壓璺嚎ID锛氬伐鍗曡矾绾� 鈫� 浜у搧璺嚎
+  if (workOrder.value?.routeId) {
+    currentRouteId.value = workOrder.value.routeId;
+  } else {
+    const processes = await getRouteProcessListByProduct(productId);
+    currentRouteId.value = processes?.[0]?.routeId ?? 0;
+  }
 }
 
 /** 瀹屾垚璁㈠崟 */
@@ -94,7 +102,10 @@
       // 璁剧疆鍒� values
       await formApi.setValues(workOrder.value);
       if (workOrder.value.productId) {
-        await loadRouteProcesses(workOrder.value.productId);
+        await loadRouteProcesses(
+          workOrder.value.id!,
+          workOrder.value.productId,
+        );
       }
     } finally {
       modalApi.unlock();
@@ -138,13 +149,18 @@
     </template>
     <template #prepend-footer>
       <div class="flex flex-auto items-center gap-2">
-        <Popconfirm
-          v-if="formType === 'schedule'"
-          title="纭瑕佸畬鎴愯璁㈠崟鍚楋紵瀹屾垚鍚庤鍗曚笅鎵�鏈変换鍔″皢鏍囪涓哄凡瀹屾垚銆�"
-          @confirm="handleFinish"
-        >
-          <Button type="primary">瀹屾垚</Button>
-        </Popconfirm>
+        <template v-if="formType === 'schedule'">
+          <Popconfirm
+            v-if="routeProcessList.length > 0"
+            title="纭瑕佸畬鎴愯璁㈠崟鍚楋紵瀹屾垚鍚庤鍗曚笅鎵�鏈変换鍔″皢鏍囪涓哄凡瀹屾垚銆�"
+            @confirm="handleFinish"
+          >
+            <Button type="primary">瀹屾垚</Button>
+          </Popconfirm>
+          <span v-else class="text-gray-400 text-sm">
+            褰撳墠璁㈠崟鏈厤缃伐搴忥紝璇峰厛鍦ㄥ伐鍗曚腑缁存姢宸ュ簭淇℃伅鍚庡啀瀹屾垚
+          </span>
+        </template>
       </div>
     </template>
   </Modal>
diff --git a/src/views/mes/pro/workorder-center/data.ts b/src/views/mes/pro/workorder-center/data.ts
new file mode 100644
index 0000000..7fc7214
--- /dev/null
+++ b/src/views/mes/pro/workorder-center/data.ts
@@ -0,0 +1,182 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesProWorkOrderApi } from '#/api/mes/pro/workorder';
+
+import { markRaw } from 'vue';
+
+import {
+  DICT_TYPE,
+  MesProWorkOrderStatusEnum,
+  MesProWorkOrderTypeEnum,
+} from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import CrmCustomerSelect from '#/components/crm-customer-select.vue';
+import { getRangePickerDefaultProps } from '#/utils';
+import { MdItemSelect } from '#/views/mes/md/item/components';
+
+/** 宸ュ崟涓績鎼滅储琛ㄥ崟 */
+export function useGridFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'code',
+      label: '璁㈠崟缂栫爜',
+      component: 'Input',
+      componentProps: { allowClear: true, placeholder: '璇疯緭鍏ヨ鍗曠紪鐮�' },
+    },
+    {
+      fieldName: 'name',
+      label: '璁㈠崟鍚嶇О',
+      component: 'Input',
+      componentProps: { allowClear: true, placeholder: '璇疯緭鍏ヨ鍗曞悕绉�' },
+    },
+    {
+      fieldName: 'productId',
+      label: '浜у搧',
+      component: markRaw(MdItemSelect),
+      componentProps: { placeholder: '璇烽�夋嫨浜у搧' },
+    },
+    {
+      fieldName: 'clientId',
+      label: '瀹㈡埛',
+      component: markRaw(CrmCustomerSelect),
+      componentProps: { placeholder: '璇烽�夋嫨瀹㈡埛' },
+    },
+    {
+      fieldName: 'requestDate',
+      label: '闇�姹傛棩鏈�',
+      component: 'RangePicker',
+      componentProps: {
+        ...getRangePickerDefaultProps(),
+        allowClear: true,
+      },
+    },
+  ];
+}
+
+/** 宸ュ崟涓績鍒楄〃瀛楁 */
+export function useGridColumns(): VxeTableGridOptions<MesProWorkOrderApi.WorkOrder>['columns'] {
+  return [
+    {
+      field: 'code',
+      title: '璁㈠崟缂栫爜',
+      fixed: 'left',
+      width: 200,
+      treeNode: true,
+      slots: { default: 'code' },
+    },
+    {
+      field: 'name',
+      title: '璁㈠崟鍚嶇О',
+      minWidth: 150,
+    },
+    {
+      field: 'productCode',
+      title: '浜у搧缂栫爜',
+      width: 120,
+    },
+    {
+      field: 'productName',
+      title: '浜у搧鍚嶇О',
+      minWidth: 120,
+    },
+    {
+      field: 'productSpecification',
+      title: '瑙勬牸鍨嬪彿',
+      width: 120,
+    },
+    {
+      field: 'unitMeasureName',
+      title: '鍗曚綅',
+      width: 80,
+    },
+    {
+      field: 'quantity',
+      title: '鐢熶骇鏁伴噺',
+      width: 100,
+    },
+    {
+      field: 'quantityScheduled',
+      title: '宸叉帓浜�',
+      width: 100,
+    },
+    {
+      field: 'quantityProduced',
+      title: '宸茬敓浜�',
+      width: 100,
+    },
+    {
+      field: 'clientName',
+      title: '瀹㈡埛鍚嶇О',
+      width: 120,
+    },
+    {
+      field: 'requestDate',
+      title: '闇�姹傛棩鏈�',
+      width: 120,
+      formatter: 'formatDate',
+    },
+    {
+      field: 'status',
+      title: '鐘舵��',
+      width: 100,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.MES_PRO_WORK_ORDER_STATUS },
+      },
+    },
+    {
+      title: '鎿嶄綔',
+      width: 200,
+      fixed: 'right',
+      slots: { default: 'actions' },
+    },
+  ];
+}
+
+/** 杩涘害涓殑浠诲姟鍒楄〃瀛楁 */
+export function useTaskGridColumns(): VxeTableGridOptions<any>['columns'] {
+  return [
+    {
+      field: 'taskCode',
+      title: '浠诲姟缂栫爜',
+      width: 180,
+    },
+    {
+      field: 'workstationName',
+      title: '宸ヤ綔绔�',
+      width: 140,
+    },
+    {
+      field: 'quantity',
+      title: '鎺掍骇鏁伴噺',
+      width: 100,
+    },
+    {
+      field: 'producedQuantity',
+      title: '宸茬敓浜�',
+      width: 100,
+    },
+    {
+      field: 'progressPercent',
+      title: '杩涘害',
+      width: 120,
+      slots: { default: 'taskProgress' },
+    },
+    {
+      field: 'status',
+      title: '鐘舵��',
+      width: 100,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.MES_PRO_TASK_STATUS },
+      },
+    },
+    {
+      title: '鎿嶄綔',
+      width: 100,
+      fixed: 'right',
+      slots: { default: 'taskActions' },
+    },
+  ];
+}
diff --git a/src/views/mes/pro/workorder-center/index.vue b/src/views/mes/pro/workorder-center/index.vue
new file mode 100644
index 0000000..b90ebb8
--- /dev/null
+++ b/src/views/mes/pro/workorder-center/index.vue
@@ -0,0 +1,313 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesProWorkOrderApi, TaskInfo } from '#/api/mes/pro/workorder';
+
+import { ref } from 'vue';
+import { useRouter } from 'vue-router';
+
+import { Page, useVbenModal } from '@vben/common-ui';
+import { MesProWorkOrderStatusEnum, MesProWorkOrderTypeEnum } from '@vben/constants';
+
+import { Button, Card, Collapse, Progress, Tag, message } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+  getWorkOrderPage,
+  getWorkOrderProgress,
+  checkAndAutoFinishWorkOrder,
+} from '#/api/mes/pro/workorder';
+import { previewAutoGenerate, autoGenerateTasks } from '#/api/mes/pro/task';
+
+import { useGridColumns, useGridFormSchema, useTaskGridColumns } from './data';
+import QuickFeedbackForm from './modules/quick-feedback.vue';
+import SchedulePreviewModal from './modules/schedule-preview.vue';
+
+defineOptions({ name: 'WorkorderCenter' });
+
+const router = useRouter();
+
+const progressData = ref<MesProWorkOrderApi.WorkOrderProgress | null>(null);
+const activeProcessKeys = ref<string[]>([]);
+const expandedRowId = ref<number | null>(null);
+
+const [QuickFeedbackModal, quickFeedbackModalApi] = useVbenModal({
+  connectedComponent: QuickFeedbackForm,
+  destroyOnClose: true,
+});
+
+const [SchedulePreviewModal, schedulePreviewModalApi] = useVbenModal({
+  connectedComponent: SchedulePreviewModal,
+  destroyOnClose: true,
+});
+
+function handleRefresh() {
+  gridApi.query();
+  // 濡傛灉杩涘害璇︽儏灞曞紑涓紝鍒锋柊杩涘害鏁版嵁
+  if (expandedRowId.value) {
+    refreshProgress();
+  }
+}
+
+async function refreshProgress() {
+  if (!expandedRowId.value) return;
+  progressData.value = await getWorkOrderProgress(expandedRowId.value);
+}
+
+/** 蹇�熸姤宸ユ垚鍔熷悗鍒锋柊杩涘害骞舵鏌ヨ嚜鍔ㄥ畬鎴� */
+async function handleFeedbackSuccess() {
+  handleRefresh();
+  if (expandedRowId.value) {
+    try {
+      await checkAndAutoFinishWorkOrder(expandedRowId.value);
+    } catch {
+      // 鑷姩瀹屾垚妫�鏌ュけ璐ヤ笉褰卞搷涓绘祦绋�
+    }
+  }
+}
+
+/** 鏌ョ湅宸ュ崟杩涘害锛堝睍寮�琛岋級 */
+async function handleToggleProgress(row: MesProWorkOrderApi.WorkOrder) {
+  if (expandedRowId.value === row.id) {
+    expandedRowId.value = null;
+    progressData.value = null;
+    return;
+  }
+  expandedRowId.value = row.id!;
+  progressData.value = await getWorkOrderProgress(row.id!);
+  // 榛樿灞曞紑绗竴涓伐搴�
+  if (progressData.value?.processList?.length) {
+    activeProcessKeys.value = [String(progressData.value.processList[0].processId)];
+  }
+}
+
+/** 宸ュ簭杩涘害鏉¢鑹� */
+function getProcessStatusColor(status: number): string {
+  switch (status) {
+    case 2: return '#52c41a';
+    case 1: return '#1677ff';
+    default: return '#d9d9d9';
+  }
+}
+
+/** 宸ュ簭鐘舵�佹枃瀛� */
+function getProcessStatusText(status: number): string {
+  switch (status) {
+    case 2: return '宸插畬鎴�';
+    case 1: return '杩涜涓�';
+    default: return '鏈紑濮�';
+  }
+}
+
+/** 蹇�熸姤宸� */
+function handleQuickFeedback(task: TaskInfo) {
+  quickFeedbackModalApi
+    .setData({ taskId: task.taskId, taskCode: task.taskCode, taskQuantity: task.quantity, taskProducedQuantity: task.producedQuantity })
+    .open();
+}
+
+/** 涓�閿帓浜ч瑙� */
+async function handleSchedulePreview(row: MesProWorkOrderApi.WorkOrder) {
+  try {
+    const preview = await previewAutoGenerate({ workOrderId: row.id! });
+    schedulePreviewModalApi
+      .setData({ workOrder: row, preview })
+      .open();
+  } catch {
+    message.error('鑾峰彇鎺掍骇棰勮澶辫触');
+  }
+}
+
+/** 鎵ц涓�閿帓浜� */
+async function handleScheduleExecute(row: MesProWorkOrderApi.WorkOrder) {
+  try {
+    const result = await autoGenerateTasks({ workOrderId: row.id! });
+    message.success(`鎺掍骇鎴愬姛锛岀敓鎴� ${result.taskIds?.length || 0} 涓换鍔);
+    handleRefresh();
+  } catch {
+    message.error('鎺掍骇澶辫触');
+  }
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  formOptions: {
+    schema: useGridFormSchema(),
+  },
+  gridOptions: {
+    columns: useGridColumns(),
+    height: 'auto',
+    keepSource: true,
+    proxyConfig: {
+      ajax: {
+        query: async ({ page }, formValues) => {
+          return await getWorkOrderPage({
+            pageNo: page.currentPage,
+            pageSize: page.pageSize,
+            status: MesProWorkOrderStatusEnum.CONFIRMED,
+            type: MesProWorkOrderTypeEnum.SELF,
+            ...formValues,
+          });
+        },
+      },
+    },
+    rowConfig: {
+      keyField: 'id',
+      isHover: true,
+    },
+    toolbarConfig: {
+      refresh: true,
+      search: true,
+    },
+  } as VxeTableGridOptions<MesProWorkOrderApi.WorkOrder>,
+});
+</script>
+
+<template>
+  <Page auto-content-height>
+    <QuickFeedbackModal @success="handleFeedbackSuccess" />
+    <SchedulePreviewModal @success="handleRefresh" />
+
+    <Grid table-title="宸ュ崟涓績">
+      <template #toolbar-tools>
+        <TableAction
+          :actions="[
+            {
+              label: '鍒涘缓宸ュ崟',
+              type: 'primary',
+              icon: ACTION_ICON.ADD,
+              auth: ['mes:pro-work-order:create'],
+              onClick: () => router.push({ name: 'MesProWorkorder' }),
+            },
+          ]"
+        />
+      </template>
+
+      <template #code="{ row }">
+        <Button type="link" @click="handleToggleProgress(row)">
+          {{ row.code }}
+        </Button>
+      </template>
+
+      <template #actions="{ row }">
+        <TableAction
+          :actions="[
+            {
+              label: '鎺掍骇棰勮',
+              type: 'link',
+              auth: ['mes:pro-task:create'],
+              ifShow: row.quantityScheduled < row.quantity,
+              onClick: handleSchedulePreview.bind(null, row),
+            },
+            {
+              label: '涓�閿帓浜�',
+              type: 'link',
+              auth: ['mes:pro-task:create'],
+              ifShow: row.quantityScheduled < row.quantity,
+              popConfirm: {
+                title: '纭涓�閿帓浜э紵灏嗘寜鐓у伐鑹鸿矾绾胯嚜鍔ㄧ敓鎴愪换鍔°��',
+                confirm: handleScheduleExecute.bind(null, row),
+              },
+            },
+          ]"
+        />
+      </template>
+    </Grid>
+
+    <!-- 灞曞紑鐨勫伐鍗曡繘搴﹁鎯� -->
+    <Card
+      v-if="progressData"
+      title="宸ュ崟杩涘害璇︽儏"
+      class="mt-4"
+      size="small"
+    >
+      <template #extra>
+        <Button type="link" @click="expandedRowId = null; progressData = null">鏀惰捣</Button>
+      </template>
+
+      <!-- 鎬讳綋杩涘害 -->
+      <div class="mb-4 flex items-center gap-4">
+        <span class="text-gray-500">鎬昏繘搴︼細</span>
+        <Progress
+          :percent="Math.round(progressData.progressPercent || 0)"
+          :stroke-color="{
+            from: '#1677ff',
+            to: '#52c41a',
+          }"
+          class="flex-1"
+        />
+        <span class="text-sm text-gray-400">
+          {{ progressData.quantityProduced }} / {{ progressData.quantity }}
+        </span>
+      </div>
+
+      <!-- 宸ュ簭杩涘害姝ラ鏉� -->
+      <div class="mb-4 flex items-center gap-0">
+        <template v-for="(proc, idx) in progressData.processList" :key="proc.processId!">
+          <!-- 鑺傜偣 -->
+          <div class="flex flex-col items-center" style="flex: 1">
+            <Tag :color="getProcessStatusColor(proc.status!)">
+              {{ proc.processName }}
+            </Tag>
+            <span class="mt-1 text-xs text-gray-400">
+              {{ getProcessStatusText(proc.status!) }}
+            </span>
+            <Progress
+              v-if="proc.keyFlag"
+              :percent="Math.round(proc.progressPercent || 0)"
+              :size="80"
+              type="circle"
+              :stroke-color="getProcessStatusColor(proc.status!)"
+              class="mt-1"
+            />
+          </div>
+          <!-- 杩炵嚎 -->
+          <div
+            v-if="idx < progressData.processList!.length - 1"
+            class="h-0.5 flex-1"
+            :style="{
+              background:
+                proc.status === 2
+                  ? '#52c41a'
+                  : proc.status === 1
+                    ? '#1677ff'
+                    : '#d9d9d9',
+            }"
+          />
+        </template>
+      </div>
+
+      <!-- 浠诲姟鏄庣粏 -->
+      <Collapse v-model:activeKey="activeProcessKeys">
+        <Collapse.Panel
+          v-for="proc in progressData.processList"
+          :key="String(proc.processId)"
+          :header="`${proc.processName}锛�${proc.producedQuantity || 0} / ${proc.plannedQuantity || proc.producedQuantity || 0}锛塦"
+        >
+          <vxe-grid
+            :key="proc.processId"
+            :columns="useTaskGridColumns()"
+            :data="proc.taskList || []"
+            height="auto"
+            size="small"
+          >
+            <template #taskProgress="{ row: taskRow }">
+              <Progress
+                :percent="Math.round(taskRow.progressPercent || 0)"
+                size="small"
+              />
+            </template>
+            <template #taskActions="{ row: taskRow }">
+              <Button
+                type="link"
+                size="small"
+                :disabled="taskRow.progressPercent >= 100"
+                @click="handleQuickFeedback(taskRow)"
+              >
+                鎶ュ伐
+              </Button>
+            </template>
+          </vxe-grid>
+        </Collapse.Panel>
+      </Collapse>
+    </Card>
+  </Page>
+</template>
diff --git a/src/views/mes/pro/workorder-center/modules/quick-feedback.vue b/src/views/mes/pro/workorder-center/modules/quick-feedback.vue
new file mode 100644
index 0000000..b249fdf
--- /dev/null
+++ b/src/views/mes/pro/workorder-center/modules/quick-feedback.vue
@@ -0,0 +1,158 @@
+<script lang="ts" setup>
+import type { FormInstance } from 'ant-design-vue';
+import type { MesProTaskApi } from '#/api/mes/pro/task';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import { quickCreateFeedback } from '#/api/mes/pro/feedback';
+import ProTaskSelect from '#/views/mes/pro/task/components/select.vue';
+
+defineOptions({ name: 'QuickFeedbackForm' });
+
+interface Props {
+  taskId?: number;
+  taskCode?: string;
+  taskQuantity?: number;
+  taskProducedQuantity?: number;
+}
+
+const props = withDefaults(defineProps<Props>(), {});
+
+interface Emits {
+  (e: 'success'): void;
+}
+
+const emit = defineEmits<Emits>();
+
+const [Modal, modalApi] = useVbenModal({
+  onOpenChange: (open) => {
+    if (open) {
+      selectedTaskId.value = props.taskId;
+      selectedTask.value = undefined;
+      formData.value = {
+        feedbackQuantity: undefined,
+        qualifiedQuantity: undefined,
+        unqualifiedQuantity: undefined,
+        remark: '',
+      };
+    }
+  },
+});
+
+const formRef = ref<FormInstance>();
+const selectedTaskId = ref<number | undefined>(props.taskId);
+const selectedTask = ref<MesProTaskApi.Task>();
+const formData = ref({
+  feedbackQuantity: undefined as number | undefined,
+  qualifiedQuantity: undefined as number | undefined,
+  unqualifiedQuantity: undefined as number | undefined,
+  remark: '',
+});
+const submitting = ref(false);
+
+const hasPredefinedTask = computed(() => !!props.taskId);
+
+const pendingQuantity = computed(() => {
+  if (hasPredefinedTask.value) {
+    if (props.taskQuantity == null || props.taskProducedQuantity == null) return 0;
+    return Math.max(0, Number(props.taskQuantity) - Number(props.taskProducedQuantity));
+  }
+  if (!selectedTask.value) return 0;
+  const qty = Number(selectedTask.value.quantity || 0);
+  const produced = Number(selectedTask.value.producedQuantity || 0);
+  return Math.max(0, qty - produced);
+});
+
+function handleTaskChange(task: MesProTaskApi.Task | undefined) {
+  selectedTask.value = task;
+  selectedTaskId.value = task?.id;
+}
+
+async function handleSubmit() {
+  const taskId = selectedTaskId.value;
+  if (!taskId) {
+    message.warning('璇烽�夋嫨浠诲姟');
+    return;
+  }
+  if (!formData.value.feedbackQuantity) {
+    message.warning('璇疯緭鍏ユ姤宸ユ暟閲�');
+    return;
+  }
+  submitting.value = true;
+  try {
+    await quickCreateFeedback({
+      taskId,
+      feedbackQuantity: formData.value.feedbackQuantity,
+      qualifiedQuantity: formData.value.qualifiedQuantity,
+      unqualifiedQuantity: formData.value.unqualifiedQuantity,
+      remark: formData.value.remark || undefined,
+    });
+    message.success('鎶ュ伐鎴愬姛');
+    modalApi.close();
+    emit('success');
+  } finally {
+    submitting.value = false;
+  }
+}
+</script>
+
+<template>
+  <Modal title="蹇�熸姤宸�" @ok="handleSubmit">
+    <a-form ref="formRef" :model="formData" layout="vertical">
+      <a-form-item label="浠诲姟" required>
+        <ProTaskSelect
+          v-if="!hasPredefinedTask"
+          v-model="selectedTaskId"
+          @change="handleTaskChange"
+        />
+        <a-input v-else :value="props.taskCode" disabled />
+      </a-form-item>
+      <a-form-item label="寰呮姤宸ユ暟閲�">
+        <a-input-number
+          :value="pendingQuantity"
+          disabled
+          class="!w-full"
+        />
+      </a-form-item>
+      <a-form-item label="鏈鎶ュ伐鏁伴噺" required>
+        <a-input-number
+          v-model:value="formData.feedbackQuantity"
+          :min="0.01"
+          :max="pendingQuantity || undefined"
+          :precision="2"
+          placeholder="璇疯緭鍏ユ姤宸ユ暟閲�"
+          class="!w-full"
+        />
+      </a-form-item>
+      <a-form-item label="鍚堟牸鍝佹暟閲�">
+        <a-input-number
+          v-model:value="formData.qualifiedQuantity"
+          :min="0"
+          :precision="2"
+          placeholder="璇疯緭鍏ュ悎鏍煎搧鏁伴噺"
+          class="!w-full"
+        />
+      </a-form-item>
+      <a-form-item label="涓嶈壇鍝佹暟閲�">
+        <a-input-number
+          v-model:value="formData.unqualifiedQuantity"
+          :min="0"
+          :precision="2"
+          placeholder="璇疯緭鍏ヤ笉鑹搧鏁伴噺"
+          class="!w-full"
+        />
+      </a-form-item>
+      <a-form-item label="澶囨敞">
+        <a-textarea
+          v-model:value="formData.remark"
+          placeholder="璇疯緭鍏ュ娉�"
+          :rows="2"
+        />
+      </a-form-item>
+    </a-form>
+  </Modal>
+</template>
diff --git a/src/views/mes/pro/workorder-center/modules/schedule-preview.vue b/src/views/mes/pro/workorder-center/modules/schedule-preview.vue
new file mode 100644
index 0000000..9607a1b
--- /dev/null
+++ b/src/views/mes/pro/workorder-center/modules/schedule-preview.vue
@@ -0,0 +1,86 @@
+<script lang="ts" setup>
+import type { AutoGeneratePreview } from '#/api/mes/pro/task';
+import type { MesProWorkOrderApi } from '#/api/mes/pro/workorder';
+
+import { ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message, Tag } from 'ant-design-vue';
+
+import { autoGenerateTasks } from '#/api/mes/pro/task';
+
+defineOptions({ name: 'SchedulePreviewModal' });
+
+interface Props {
+  workOrder?: MesProWorkOrderApi.WorkOrder;
+  preview?: AutoGeneratePreview;
+}
+
+const props = withDefaults(defineProps<Props>(), {});
+
+interface Emits {
+  (e: 'success'): void;
+}
+
+const emit = defineEmits<Emits>();
+
+const [Modal, modalApi] = useVbenModal({
+  onOpenChange: (open) => {
+    if (!open) return;
+  },
+});
+
+const executing = ref(false);
+
+async function handleExecute() {
+  if (!props.workOrder?.id) return;
+  executing.value = true;
+  try {
+    const result = await autoGenerateTasks({
+      workOrderId: props.workOrder.id,
+      routeId: props.preview?.routeId,
+    });
+    message.success(`鎺掍骇鎴愬姛锛岀敓鎴� ${result.taskIds?.length || 0} 涓换鍔);
+    modalApi.close();
+    emit('success');
+  } finally {
+    executing.value = false;
+  }
+}
+</script>
+
+<template>
+  <Modal title="涓�閿帓浜ч瑙�" @ok="handleExecute">
+    <a-descriptions :column="2" bordered size="small" class="mb-4">
+      <a-descriptions-item label="宸ュ崟缂栫爜">{{ props.workOrder?.code }}</a-descriptions-item>
+      <a-descriptions-item label="鐢熶骇鏁伴噺">{{ props.workOrder?.quantity }}</a-descriptions-item>
+      <a-descriptions-item label="宸ヨ壓璺嚎">{{ props.preview?.routeName }}</a-descriptions-item>
+      <a-descriptions-item label="棰勭敓鎴愪换鍔℃暟">{{ props.preview?.taskList?.length || 0 }}</a-descriptions-item>
+    </a-descriptions>
+
+    <vxe-grid
+      :columns="[
+        { field: 'sort', title: '搴忓彿', width: 60 },
+        { field: 'processName', title: '宸ュ簭鍚嶇О', minWidth: 120 },
+        {
+          field: 'keyFlag',
+          title: '鍏抽敭宸ュ簭',
+          width: 80,
+          slots: { default: 'keyFlag' },
+        },
+        { field: 'suggestWorkstationName', title: '寤鸿宸ヤ綔绔�', width: 140 },
+        { field: 'quantity', title: '鎺掍骇鏁伴噺', width: 100 },
+      ]"
+      :data="props.preview?.taskList || []"
+      height="auto"
+      size="small"
+    >
+      <template #keyFlag="{ row: taskRow }">
+        <Tag :color="taskRow.keyFlag ? 'blue' : 'default'">
+          {{ taskRow.keyFlag ? '鏄�' : '鍚�' }}
+        </Tag>
+      </template>
+    </vxe-grid>
+  </Modal>
+</template>
diff --git a/src/views/mes/pro/workorder/data.ts b/src/views/mes/pro/workorder/data.ts
index 265c93a..9a4bf1b 100644
--- a/src/views/mes/pro/workorder/data.ts
+++ b/src/views/mes/pro/workorder/data.ts
@@ -3,6 +3,7 @@
 import type { MesMdProductBomApi } from '#/api/mes/md/item/productBom';
 import type { MesProWorkOrderApi } from '#/api/mes/pro/workorder';
 import type { MesProWorkOrderBomApi } from '#/api/mes/pro/workorder/bom';
+import type { MesProWorkOrderProcessApi } from '#/api/mes/pro/workorder/process';
 
 import { h, markRaw } from 'vue';
 
@@ -734,3 +735,105 @@
     },
   ];
 }
+
+/** 璁㈠崟宸ュ簭鍒楄〃鐨勫瓧娈� */
+export function useProcessGridColumns(
+  draggable = false,
+): VxeTableGridOptions<MesProWorkOrderProcessApi.WorkOrderProcess>['columns'] {
+  return [
+    ...(draggable
+      ? [
+          {
+            field: 'drag',
+            title: '',
+            width: 40,
+            fixed: 'left',
+            slots: { default: 'dragHandle' },
+          } as const,
+        ]
+      : []),
+    {
+      field: 'sort',
+      title: '搴忓彿',
+      width: 80,
+    },
+    {
+      field: 'processCode',
+      title: '宸ュ簭缂栫爜',
+      width: 120,
+    },
+    {
+      field: 'processName',
+      title: '宸ュ簭鍚嶇О',
+      minWidth: 150,
+    },
+    {
+      field: 'nextProcessName',
+      title: '涓嬩竴閬撳伐搴�',
+      width: 120,
+    },
+    {
+      field: 'linkType',
+      title: '宸ュ簭鍏崇郴',
+      width: 100,
+      formatter: ({ cellValue }) => {
+        const map: Record<number, string> = {
+          0: '寮�濮�-寮�濮�',
+          1: '缁撴潫-缁撴潫',
+          2: '寮�濮�-缁撴潫',
+          3: '缁撴潫-寮�濮�',
+        };
+        return map[cellValue] ?? '-';
+      },
+    },
+    {
+      field: 'prepareTime',
+      title: '鍑嗗鏃堕棿(鍒�)',
+      width: 100,
+    },
+    {
+      field: 'waitTime',
+      title: '绛夊緟鏃堕棿(鍒�)',
+      width: 100,
+    },
+    {
+      field: 'keyFlag',
+      title: '鍏抽敭宸ュ簭',
+      width: 80,
+      slots: { default: 'keyFlag' },
+    },
+    {
+      field: 'checkFlag',
+      title: '璐ㄦ宸ュ簭',
+      width: 80,
+      slots: { default: 'checkFlag' },
+    },
+    {
+      field: 'backflushFlag',
+      title: '鍊掑啿',
+      width: 80,
+      slots: { default: 'backflushFlag' },
+    },
+    {
+      field: 'outputItemCode',
+      title: '浜у嚭鐗╂枡缂栫爜',
+      width: 120,
+    },
+    {
+      field: 'outputItemName',
+      title: '浜у嚭鐗╂枡鍚嶇О',
+      width: 120,
+    },
+    {
+      field: 'remark',
+      title: '澶囨敞',
+      minWidth: 120,
+    },
+    {
+      title: '鎿嶄綔',
+      width: 120,
+      fixed: 'right',
+      slots: { default: 'actions' },
+    },
+  ];
+}
diff --git a/src/views/mes/pro/workorder/modules/form.vue b/src/views/mes/pro/workorder/modules/form.vue
index 676159e..dced20c 100644
--- a/src/views/mes/pro/workorder/modules/form.vue
+++ b/src/views/mes/pro/workorder/modules/form.vue
@@ -3,6 +3,7 @@
 
   import type { MesProWorkOrderApi } from "#/api/mes/pro/workorder";
   import type { MesProWorkOrderBomApi } from "#/api/mes/pro/workorder/bom";
+  import type { MesProWorkOrderProcessApi } from "#/api/mes/pro/workorder/process";
 
   import { computed, ref } from "vue";
 
@@ -22,29 +23,33 @@
     getWorkOrder,
     updateWorkOrder,
   } from "#/api/mes/pro/workorder";
+  import { batchSaveWorkOrderProcess, getWorkOrderProcessListByWorkOrderId } from "#/api/mes/pro/workorder/process";
   import { $t } from "#/locales";
   import { BarcodeDetail } from "#/views/wls/barcode/components";
 
   import { useFormSchema } from "../data";
   import BomList from "./bom-list.vue";
   import ItemList from "./item-list.vue";
+  import ProcessList from "./process-list.vue";
 
   const emit = defineEmits(["success"]);
   const formType = ref<FormType>("create");
   const formData = ref<MesProWorkOrderApi.WorkOrder>();
-  const originalSnapshot = ref(""); // 琛ㄥ崟鍘熷鏁版嵁蹇収锛岀敤浜庣‘璁ゆ椂璺宠繃鏈彉鏇寸殑淇濆瓨璇锋眰
-  const subTabsName = ref("bom"); // 褰撳墠閫変腑鐨勫瓙琛� Tab
-  const barcodeDetailRef = ref<InstanceType<typeof BarcodeDetail>>(); // 鏉$爜璇︽儏寮圭獥
+  const originalSnapshot = ref("");
+  const subTabsName = ref("bom");
+  const barcodeDetailRef = ref<InstanceType<typeof BarcodeDetail>>();
+  const processListRef = ref<InstanceType<typeof ProcessList>>();
+  const currentProductId = ref<number>(); // 褰撳墠閫変腑鐨勪骇鍝� ID锛堢敤浜庡伐搴忓姞杞斤級
+  const tempProcessList = ref<MesProWorkOrderProcessApi.WorkOrderProcess[]>([]); // 涓存椂宸ュ簭鍒楄〃
 
   const isEditable = computed(() =>
-    // 鏄惁涓虹紪杈戞ā寮忥紙鍙繚瀛橈級
     ["create", "update"].includes(formType.value)
   );
-  const isConfirm = computed(() => formType.value === "confirm"); // 鏄惁涓虹‘璁ゆā寮�
-  const isFinish = computed(() => formType.value === "finish"); // 鏄惁涓哄畬鎴愭ā寮�
+  const isConfirm = computed(() => formType.value === "confirm");
+  const isFinish = computed(() => formType.value === "finish");
+  const isCreateMode = computed(() => formType.value === "create");
   const canConfirm = computed(
     () =>
-      // 缂栬緫鎬佽崏绋垮彲纭
       formType.value === "update" &&
       formData.value?.status === MesProWorkOrderStatusEnum.PREPARE
   );
@@ -69,6 +74,9 @@
     }
   });
 
+  // 鍒ゆ柇鏄惁鏄剧ず Tab锛堟柊澧炴ā寮忔垨宸叉湁 ID锛�
+  const showTabs = computed(() => isCreateMode.value || formData.value?.id);
+
   const [Form, formApi] = useVbenForm({
     commonConfig: {
       componentProps: {
@@ -81,6 +89,15 @@
     schema: [],
     showDefaultActions: false,
     wrapperClass: "grid-cols-3",
+    handleValuesChange: (values, fieldsChanged) => {
+      // 浜у搧鍙樻洿鏃讹紝瑙﹀彂宸ュ簭鍔犺浇
+      if (isCreateMode.value && fieldsChanged.includes('productId')) {
+        const newProductId = values.productId;
+        if (newProductId && newProductId !== currentProductId.value) {
+          currentProductId.value = newProductId;
+        }
+      }
+    },
   });
 
   /** 閲嶆柊鎸傝浇 schema 骞舵寜琛ㄥ崟鎬佹帶鍒跺簳閮ㄦ寜閽� */
@@ -110,7 +127,7 @@
     );
   }
 
-  /** 浠� BOM 鐗╂枡琛岄濉瓙璁㈠崟锛氬鐢ㄥ綋鍓嶈鍗曚笂涓嬫枃锛岃繘鍏ユ柊澧炴�� */
+  /** 浠� BOM 鐗╂枡琛岄濉瓙璁㈠崟 */
   async function handleGenerateWorkOrder(
     bomRow: MesProWorkOrderBomApi.WorkOrderBom
   ) {
@@ -137,8 +154,15 @@
       type: current.type,
       vendorId: current.vendorId,
     });
+    // 璁剧疆褰撳墠浜у搧 ID锛岃Е鍙戝伐搴忓姞杞�
+    currentProductId.value = bomRow.itemId;
     originalSnapshot.value = JSON.stringify(await formApi.getValues());
     message.info("宸蹭粠 BOM 鐗╂枡棰勫~瀛愯鍗曪紝璇疯ˉ鍏呰鍗曠紪鐮佺瓑淇℃伅鍚庝繚瀛�");
+  }
+
+  /** 宸ュ簭鍒楄〃鍙樺寲鍥炶皟 */
+  function handleProcessListChange(list: MesProWorkOrderProcessApi.WorkOrderProcess[]) {
+    tempProcessList.value = list;
   }
 
   const [Modal, modalApi] = useVbenModal({
@@ -154,7 +178,7 @@
         }
         modalApi.lock();
         try {
-          // 缂栬緫鎬佹湁淇敼鏃跺厛淇濆瓨
+          // 1. 淇濆瓨宸ュ崟淇℃伅锛堝鏋滄湁鍙樻洿锛�
           if (isEditable.value) {
             const current = JSON.stringify(await formApi.getValues());
             if (current !== originalSnapshot.value) {
@@ -163,6 +187,21 @@
               originalSnapshot.value = current;
             }
           }
+          // 2. 鎵归噺淇濆瓨宸ュ簭锛堝鏋滄湁锛�
+          if (tempProcessList.value.length > 0) {
+            const processList = tempProcessList.value.map(item => ({
+              ...item,
+              workOrderId: formData.value!.id,
+            }));
+            await batchSaveWorkOrderProcess(processList);
+          }
+          // 3. 鏍¢獙鏄惁鏈夊伐搴�
+          const processes = await getWorkOrderProcessListByWorkOrderId(formData.value.id);
+          if (!processes || processes.length === 0) {
+            message.warning('褰撳墠璁㈠崟鏈厤缃伐搴忥紝璇峰厛鍦ㄥ伐鍗曚腑缁存姢宸ュ簭淇℃伅鍚庡啀纭');
+            return;
+          }
+          // 4. 纭宸ュ崟
           await confirmWorkOrder(formData.value.id);
           message.success('璁㈠崟宸茬‘璁�');
           await modalApi.close();
@@ -175,6 +214,12 @@
       // ---- 瀹屾垚璁㈠崟妯″紡 ----
       if (isFinish.value) {
         if (!formData.value?.id) return;
+        // 鏍¢獙鏄惁鏈夊伐搴�
+        const processes = await getWorkOrderProcessListByWorkOrderId(formData.value.id);
+        if (!processes || processes.length === 0) {
+          message.warning('褰撳墠璁㈠崟鏈厤缃伐搴忥紝璇峰厛鍦ㄥ伐鍗曚腑缁存姢宸ュ簭淇℃伅鍚庡啀瀹屾垚');
+          return;
+        }
         try {
           await confirm('纭瑕佸畬鎴愯璁㈠崟鍚楋紵');
         } catch {
@@ -202,13 +247,31 @@
       const data = (await formApi.getValues()) as MesProWorkOrderApi.WorkOrder;
       try {
         if (formData.value?.id) {
+          // 缂栬緫妯″紡
           await updateWorkOrder({ ...formData.value, ...data });
+          // 鎵归噺淇濆瓨宸ュ簭
+          if (tempProcessList.value.length > 0) {
+            const processList = tempProcessList.value.map(item => ({
+              ...item,
+              workOrderId: formData.value!.id,
+            }));
+            await batchSaveWorkOrderProcess(processList);
+          }
           formData.value = { ...formData.value, ...data };
           emit("success");
           message.success($t("ui.actionMessage.operationSuccess"));
           await modalApi.close();
         } else {
+          // 鏂板妯″紡
           const id = await createWorkOrder(data);
+          // 鎵归噺淇濆瓨宸ュ簭
+          if (tempProcessList.value.length > 0) {
+            const processList = tempProcessList.value.map(item => ({
+              ...item,
+              workOrderId: id,
+            }));
+            await batchSaveWorkOrderProcess(processList);
+          }
           formData.value = {
             ...data,
             id,
@@ -228,9 +291,10 @@
       if (!isOpen) {
         formData.value = undefined;
         originalSnapshot.value = "";
+        currentProductId.value = undefined;
+        tempProcessList.value = [];
         return;
       }
-      // 鍔犺浇鏁版嵁
       const data = modalApi.getData<{
         formType: FormType;
         id?: number;
@@ -238,18 +302,18 @@
       }>();
       formType.value = data.formType;
       subTabsName.value = "bom";
+      tempProcessList.value = [];
       applySchema();
       if (data?.id) {
         modalApi.lock();
         try {
           formData.value = await getWorkOrder(data.id);
-          // 璁剧疆鍒� values
           await formApi.setValues(formData.value);
+          currentProductId.value = formData.value.productId;
         } finally {
           modalApi.unlock();
         }
       } else if (data?.parentRow) {
-        // 鏂板瀛愯鍗曟椂锛岄濉埗璁㈠崟淇℃伅
         const parent = data.parentRow;
         await formApi.setValues({
           clientId: parent.clientId,
@@ -270,20 +334,39 @@
   <Modal :title="getTitle"
          class="w-3/5">
     <Form class="mx-4" />
-    <!-- BOM / 鐗╂枡闇�姹� Tab锛氶潪鏂板缓鎬佸睍绀� -->
-    <template v-if="formData?.id">
+    <!-- BOM / 宸ュ簭 / 鐗╂枡闇�姹� Tab -->
+    <template v-if="showTabs">
       <Tabs v-model:active-key="subTabsName"
             class="mx-4">
         <Tabs.TabPane key="bom"
                       tab="璁㈠崟 BOM">
-          <BomList :form-type="formType"
+          <BomList v-if="formData?.id"
+                   :form-type="formType"
                    :work-order="formData"
                    :work-order-id="formData.id"
                    @generate-work-order="handleGenerateWorkOrder" />
+          <div v-else class="text-gray-400 text-center py-8">
+            璇峰厛淇濆瓨宸ュ崟鍚庡啀閰嶇疆 BOM
+          </div>
+        </Tabs.TabPane>
+        <Tabs.TabPane key="process"
+                      tab="宸ュ簭">
+          <ProcessList
+            ref="processListRef"
+            :form-type="formType"
+            :work-order="formData || {}"
+            :work-order-id="formData?.id"
+            :product-id="currentProductId"
+            @process-list-change="handleProcessListChange"
+          />
         </Tabs.TabPane>
         <Tabs.TabPane key="item"
                       tab="鐗╂枡闇�姹�">
-          <ItemList :work-order-id="formData.id" />
+          <ItemList v-if="formData?.id"
+                    :work-order-id="formData.id" />
+          <div v-else class="text-gray-400 text-center py-8">
+            璇峰厛淇濆瓨宸ュ崟鍚庡啀鏌ョ湅鐗╂枡闇�姹�
+          </div>
         </Tabs.TabPane>
       </Tabs>
     </template>
diff --git a/src/views/mes/pro/workorder/modules/process-form.vue b/src/views/mes/pro/workorder/modules/process-form.vue
new file mode 100644
index 0000000..7cfb93c
--- /dev/null
+++ b/src/views/mes/pro/workorder/modules/process-form.vue
@@ -0,0 +1,319 @@
+<script lang="ts" setup>
+import type { VbenFormSchema } from '#/adapter/form';
+import type { MesProWorkOrderProcessApi } from '#/api/mes/pro/workorder/process';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { getProcessSimpleList } from '#/api/mes/pro/process';
+import {
+  createWorkOrderProcess,
+  getWorkOrderProcess,
+  updateWorkOrderProcess,
+} from '#/api/mes/pro/workorder/process';
+import { $t } from '#/locales';
+import { MdItemSelect } from '#/views/mes/md/item/components';
+
+const emit = defineEmits(['success']);
+const formData = ref<MesProWorkOrderProcessApi.WorkOrderProcess>();
+const workOrderId = ref<number>();
+const processList = ref<MesProWorkOrderProcessApi.WorkOrderProcess[]>([]);
+const isCreateMode = ref(false);
+
+const getTitle = computed(() => {
+  return formData.value?.id
+    ? $t('ui.actionTitle.edit', ['宸ュ簭'])
+    : $t('ui.actionTitle.create', ['宸ュ簭']);
+});
+
+/** 鍔犺浇宸ュ簭閫夐」 */
+async function loadProcessOptions() {
+  const list = await getProcessSimpleList();
+  return (list || []).map((item) => ({
+    label: item.name ?? '',
+    value: item.id!,
+  }));
+}
+
+/** 琛ㄥ崟 schema */
+async function loadSchema(): Promise<VbenFormSchema[]> {
+  const processOptions = await loadProcessOptions();
+  return [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: { triggerFields: [''], show: () => false },
+    },
+    {
+      fieldName: 'workOrderId',
+      component: 'Input',
+      dependencies: { triggerFields: [''], show: () => false },
+    },
+    {
+      fieldName: 'processId',
+      label: '宸ュ簭',
+      component: 'Select',
+      componentProps: {
+        options: processOptions,
+        placeholder: '璇烽�夋嫨宸ュ簭',
+        onChange: async (value: number) => {
+          const process = processOptions.find((item) => item.value === value);
+          if (process) {
+            await formApi.setFieldValue('processName', process.label);
+          }
+        },
+      },
+      rules: 'selectRequired',
+    },
+    {
+      fieldName: 'processName',
+      label: '宸ュ簭鍚嶇О',
+      component: 'Input',
+      componentProps: { disabled: true },
+    },
+    {
+      fieldName: 'sort',
+      label: '搴忓彿',
+      component: 'InputNumber',
+      componentProps: {
+        class: '!w-full',
+        min: 1,
+        placeholder: '璇疯緭鍏ュ簭鍙�',
+        precision: 0,
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'linkType',
+      label: '宸ュ簭鍏崇郴',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: [
+          { label: '寮�濮�-寮�濮�', value: 0 },
+          { label: '缁撴潫-缁撴潫', value: 1 },
+          { label: '寮�濮�-缁撴潫', value: 2 },
+          { label: '缁撴潫-寮�濮�', value: 3 },
+        ],
+        placeholder: '璇烽�夋嫨宸ュ簭鍏崇郴',
+      },
+    },
+    {
+      fieldName: 'prepareTime',
+      label: '鍑嗗鏃堕棿(鍒嗛挓)',
+      component: 'InputNumber',
+      componentProps: {
+        class: '!w-full',
+        min: 0,
+        placeholder: '璇疯緭鍏ュ噯澶囨椂闂�',
+        precision: 0,
+      },
+    },
+    {
+      fieldName: 'waitTime',
+      label: '绛夊緟鏃堕棿(鍒嗛挓)',
+      component: 'InputNumber',
+      componentProps: {
+        class: '!w-full',
+        min: 0,
+        placeholder: '璇疯緭鍏ョ瓑寰呮椂闂�',
+        precision: 0,
+      },
+    },
+    {
+      fieldName: 'colorCode',
+      label: '鐢樼壒鍥鹃鑹�',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇烽�夋嫨棰滆壊',
+        type: 'color',
+      },
+    },
+    {
+      fieldName: 'keyFlag',
+      label: '鏄惁鍏抽敭宸ュ簭',
+      component: 'RadioGroup',
+      componentProps: {
+        options: [
+          { label: '鍚�', value: false },
+          { label: '鏄�', value: true },
+        ],
+      },
+      defaultValue: false,
+    },
+    {
+      fieldName: 'checkFlag',
+      label: '鏄惁璐ㄦ宸ュ簭',
+      component: 'RadioGroup',
+      componentProps: {
+        options: [
+          { label: '鍚�', value: false },
+          { label: '鏄�', value: true },
+        ],
+      },
+      defaultValue: false,
+    },
+    {
+      fieldName: 'backflushFlag',
+      label: '鏄惁鍊掑啿',
+      component: 'RadioGroup',
+      componentProps: {
+        options: [
+          { label: '鍚�', value: false },
+          { label: '鏄�', value: true },
+        ],
+      },
+      defaultValue: true,
+    },
+    {
+      fieldName: 'outputItemId',
+      label: '浜у嚭鐗╂枡',
+      component: MdItemSelect,
+      componentProps: {
+        placeholder: '璇烽�夋嫨浜у嚭鐗╂枡',
+      },
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      formItemClass: 'col-span-2',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ娉�',
+        rows: 2,
+      },
+    },
+  ];
+}
+
+const [Form, formApi] = useVbenForm({
+  commonConfig: {
+    componentProps: { class: 'w-full' },
+    formItemClass: 'col-span-1',
+    labelWidth: 130,
+  },
+  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 MesProWorkOrderProcessApi.WorkOrderProcess;
+    data.workOrderId = workOrderId.value;
+
+    // 鏍¢獙搴忓彿鍞竴
+    const isSortDuplicate = processList.value.some(
+      (item) => item.sort === data.sort && item.processId !== data.processId
+    );
+    if (isSortDuplicate) {
+      message.error('搴忓彿宸插瓨鍦�');
+      modalApi.unlock();
+      return;
+    }
+
+    // 鏍¢獙鍏抽敭宸ュ簭鍞竴
+    if (data.keyFlag) {
+      const hasKeyProcess = processList.value.some(
+        (item) => item.keyFlag && item.processId !== data.processId
+      );
+      if (hasKeyProcess) {
+        message.error('宸插瓨鍦ㄥ叧閿伐搴忥紝姣忎釜宸ュ崟鍙兘鏈変竴涓叧閿伐搴�');
+        modalApi.unlock();
+        return;
+      }
+    }
+
+    try {
+      if (isCreateMode.value) {
+        // 鏂板妯″紡锛氱洿鎺ヨ繑鍥炴暟鎹粰鐖剁粍浠�
+        emit('success', data, !!formData.value?.processId);
+        await modalApi.close();
+      } else if (formData.value?.id) {
+        // 缂栬緫妯″紡锛氳皟鐢ㄦ洿鏂版帴鍙�
+        await updateWorkOrderProcess(data);
+        emit('success', data, true);
+        await modalApi.close();
+        message.success($t('ui.actionMessage.operationSuccess'));
+      } else {
+        // 鏂板妯″紡锛堟湁 workOrderId锛夛細璋冪敤鍒涘缓鎺ュ彛
+        await createWorkOrderProcess(data);
+        emit('success', data, false);
+        await modalApi.close();
+        message.success($t('ui.actionMessage.operationSuccess'));
+      }
+    } finally {
+      modalApi.unlock();
+    }
+  },
+  async onOpenChange(isOpen: boolean) {
+    if (!isOpen) {
+      formData.value = undefined;
+      processList.value = [];
+      isCreateMode.value = false;
+      return;
+    }
+    modalApi.lock();
+    try {
+      const schema = await loadSchema();
+      formApi.setState({ schema });
+    } finally {
+      modalApi.unlock();
+    }
+
+    const data = modalApi.getData<{
+      id?: number;
+      productId?: number;
+      workOrderId?: number;
+      row?: MesProWorkOrderProcessApi.WorkOrderProcess;
+      processList?: MesProWorkOrderProcessApi.WorkOrderProcess[];
+    }>();
+    if (!data) {
+      return;
+    }
+
+    workOrderId.value = data.workOrderId;
+    processList.value = data.processList || [];
+    isCreateMode.value = !data.workOrderId;
+
+    if (data.row) {
+      // 缂栬緫涓存椂鍒楄〃涓殑宸ュ簭
+      formData.value = data.row;
+      await formApi.setValues(data.row);
+    } else if (data.id) {
+      // 缂栬緫宸蹭繚瀛樼殑宸ュ簭
+      modalApi.lock();
+      try {
+        formData.value = await getWorkOrderProcess(data.id);
+        await formApi.setValues(formData.value);
+      } finally {
+        modalApi.unlock();
+      }
+    } else {
+      // 鏂板
+      formData.value = undefined;
+      const maxSort = Math.max(0, ...processList.value.map((item) => item.sort || 0));
+      await formApi.setValues({
+        colorCode: '#00AEF3',
+        sort: maxSort + 1,
+      });
+    }
+  },
+});
+</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/mes/pro/workorder/modules/process-list.vue b/src/views/mes/pro/workorder/modules/process-list.vue
new file mode 100644
index 0000000..35ac698
--- /dev/null
+++ b/src/views/mes/pro/workorder/modules/process-list.vue
@@ -0,0 +1,304 @@
+<script lang="ts" setup>
+import type { FormType } from '../data';
+
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesProWorkOrderApi } from '#/api/mes/pro/workorder';
+import type { MesProWorkOrderProcessApi } from '#/api/mes/pro/workorder/process';
+
+import { computed, nextTick, onUnmounted, ref, watch } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+import { MesProWorkOrderStatusEnum } from '@vben/constants';
+import { IconifyIcon } from '@vben/icons';
+
+import { useSortable } from '@vueuse/integrations/useSortable';
+import { message, Tag } from 'ant-design-vue';
+
+import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+  deleteWorkOrderProcess,
+  getWorkOrderProcessListByWorkOrderId,
+} from '#/api/mes/pro/workorder/process';
+import { getRouteProcessListByProduct } from '#/api/mes/pro/route/process';
+import { $t } from '#/locales';
+
+import { useProcessGridColumns } from '../data';
+import ProcessForm from './process-form.vue';
+
+const props = defineProps<{
+  formType: FormType;
+  workOrder: MesProWorkOrderApi.WorkOrder;
+  workOrderId?: number;
+  productId?: number;
+}>();
+
+const emit = defineEmits<{
+  processListChange: [list: MesProWorkOrderProcessApi.WorkOrderProcess[]];
+}>();
+
+// 宸ュ簭鍒楄〃鏁版嵁
+const list = ref<MesProWorkOrderProcessApi.WorkOrderProcess[]>([]);
+
+const isEditable = computed(() =>
+  ['create', 'update'].includes(props.formType) &&
+  (props.workOrder?.status === MesProWorkOrderStatusEnum.PREPARE ||
+   props.formType === 'create'),
+);
+
+const isCreateMode = computed(() => props.formType === 'create');
+
+// 鎷栨嫿鎺掑簭
+const sortableInstance = ref<any>(null);
+
+const [ProcessFormModal, processFormModalApi] = useVbenModal({
+  connectedComponent: ProcessForm,
+  destroyOnClose: true,
+});
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  gridOptions: {
+    autoResize: true,
+    border: true,
+    columns: useProcessGridColumns(isEditable.value),
+    data: [],
+    minHeight: 240,
+    pagerConfig: { enabled: false },
+    rowConfig: { isHover: true, keyField: 'processId' },
+    showOverflow: true,
+    toolbarConfig: { enabled: false },
+  } as VxeTableGridOptions<MesProWorkOrderProcessApi.WorkOrderProcess>,
+});
+
+/** 鍒濆鍖�/閿�姣佹嫋鎷芥帓搴� */
+async function initSortable() {
+  destroySortable();
+  if (!isEditable.value || list.value.length === 0) return;
+  await nextTick();
+  sortableInstance.value = useSortable(
+    '.workorder-process-list .vxe-table--body-wrapper:not(.fixed-right--wrapper) .vxe-table--body tbody',
+    list.value,
+    {
+      animation: 150,
+      draggable: '.vxe-body--row',
+      handle: '.drag-handle',
+      onEnd: ({ newDraggableIndex, oldDraggableIndex }) => {
+        if (
+          newDraggableIndex !== undefined &&
+          oldDraggableIndex !== undefined &&
+          oldDraggableIndex !== newDraggableIndex
+        ) {
+          // 閲嶆柊鎺掑簭鏁扮粍
+          list.value.splice(
+            newDraggableIndex,
+            0,
+            list.value.splice(oldDraggableIndex, 1)[0]!,
+          );
+          // 鏇存柊 sort 瀛楁
+          list.value.forEach((item, index) => {
+            item.sort = index + 1;
+          });
+          gridApi.setGridOptions({ data: list.value });
+          emit('processListChange', list.value);
+        }
+      },
+    },
+  );
+}
+
+/** 閿�姣佹嫋鎷芥帓搴忓疄渚� */
+function destroySortable() {
+  if (sortableInstance.value) {
+    sortableInstance.value.stop?.();
+    sortableInstance.value = null;
+  }
+}
+
+/** 鍔犺浇宸ュ簭鍒楄〃 */
+async function getList() {
+  gridApi.setLoading(true);
+  try {
+    if (isCreateMode.value) {
+      // 鏂板妯″紡锛氫粠宸ヨ壓璺嚎鑾峰彇
+      if (!props.productId) {
+        list.value = [];
+      } else {
+        // 璋冪敤宸ヨ壓璺嚎鎺ュ彛鑾峰彇宸ュ簭鍒楄〃
+        const routeProcessList = await getRouteProcessListByProduct(props.productId);
+        // 杞崲涓哄伐鍗曞伐搴忔牸寮�
+        list.value = routeProcessList.map((item) => ({
+          processId: item.processId,
+          processCode: item.processCode,
+          processName: item.processName,
+          sort: item.sort,
+          nextProcessId: item.nextProcessId,
+          nextProcessName: item.nextProcessName,
+          linkType: item.linkType,
+          prepareTime: item.prepareTime,
+          waitTime: item.waitTime,
+          colorCode: item.colorCode,
+          keyFlag: item.keyFlag,
+          checkFlag: item.checkFlag,
+          backflushFlag: item.backflushFlag,
+          outputItemId: item.outputItemId,
+          outputItemCode: item.outputItemCode,
+          outputItemName: item.outputItemName,
+          remark: item.remark,
+        }));
+      }
+    } else {
+      // 缂栬緫妯″紡锛氫粠宸ュ崟宸ュ簭鑾峰彇
+      if (!props.workOrderId) {
+        list.value = [];
+      } else {
+        list.value = await getWorkOrderProcessListByWorkOrderId(props.workOrderId);
+      }
+    }
+    gridApi.setGridOptions({ data: list.value });
+    emit('processListChange', list.value);
+    // 鍒濆鍖栨嫋鎷芥帓搴�
+    initSortable();
+  } finally {
+    gridApi.setLoading(false);
+  }
+}
+
+/** 娣诲姞宸ュ簭 */
+function handleCreate() {
+  processFormModalApi
+    .setData({
+      workOrderId: props.workOrderId,
+      productId: props.productId || props.workOrder?.productId,
+      processList: list.value,
+    })
+    .open();
+}
+
+/** 缂栬緫宸ュ簭 */
+function handleEdit(row: MesProWorkOrderProcessApi.WorkOrderProcess) {
+  processFormModalApi
+    .setData({
+      id: row.id,
+      workOrderId: props.workOrderId,
+      productId: props.productId || props.workOrder?.productId,
+      row,
+      processList: list.value,
+    })
+    .open();
+}
+
+/** 鏂板/缂栬緫鎴愬姛鍥炶皟 */
+function handleFormSuccess(data: MesProWorkOrderProcessApi.WorkOrderProcess, isEdit: boolean) {
+  if (isCreateMode.value) {
+    if (isEdit) {
+      const index = list.value.findIndex((item) => item.processId === data.processId);
+      if (index > -1) {
+        list.value[index] = data;
+      }
+    } else {
+      list.value.push(data);
+    }
+    gridApi.setGridOptions({ data: list.value });
+    emit('processListChange', list.value);
+    initSortable();
+  } else {
+    getList();
+  }
+}
+
+/** 鍒犻櫎宸ュ簭 */
+function handleDelete(row: MesProWorkOrderProcessApi.WorkOrderProcess) {
+  if (isCreateMode.value) {
+    // 鏂板妯″紡锛氫粠鍒楄〃鍒犻櫎
+    const index = list.value.findIndex((item) => item.processId === row.processId);
+    if (index > -1) {
+      list.value.splice(index, 1);
+      // 鏇存柊 sort 瀛楁
+      list.value.forEach((item, idx) => {
+        item.sort = idx + 1;
+      });
+      gridApi.setGridOptions({ data: list.value });
+      emit('processListChange', list.value);
+      initSortable();
+    }
+  } else {
+    // 缂栬緫妯″紡锛氳皟鐢ㄦ帴鍙e垹闄�
+    deleteWorkOrderProcess(row.id!).then(() => {
+      message.success($t('ui.actionMessage.deleteSuccess', [row.processName]));
+      getList();
+    });
+  }
+}
+
+// 缁勪欢鍗歌浇鏃堕攢姣佹帓搴忓疄渚�
+onUnmounted(() => {
+  destroySortable();
+});
+
+// 鐩戝惉浜у搧/宸ュ崟鍙樺寲
+watch(
+  () => [props.productId, props.workOrderId],
+  () => {
+    getList();
+  },
+  { immediate: true },
+);
+</script>
+
+<template>
+  <div class="workorder-process-list">
+    <ProcessFormModal @success="handleFormSuccess" />
+    <div v-if="isEditable" class="mb-3 flex items-center justify-start">
+      <TableAction
+        :actions="[
+          {
+            label: '娣诲姞宸ュ簭',
+            type: 'primary',
+            onClick: handleCreate,
+          },
+        ]"
+      />
+    </div>
+    <Grid class="w-full" table-title="宸ュ簭">
+      <template #dragHandle>
+        <IconifyIcon
+          icon="ic:round-drag-indicator"
+          class="drag-handle cursor-move text-xl text-gray-400"
+        />
+      </template>
+      <template #keyFlag="{ row }">
+        <Tag v-if="row.keyFlag" color="red">鍏抽敭</Tag>
+        <span v-else>-</span>
+      </template>
+      <template #checkFlag="{ row }">
+        <Tag v-if="row.checkFlag" color="blue">璐ㄦ</Tag>
+        <span v-else>-</span>
+      </template>
+      <template #backflushFlag="{ row }">
+        <Tag v-if="row.backflushFlag" color="green">鏄�</Tag>
+        <Tag v-else color="default">鍚�</Tag>
+      </template>
+      <template #actions="{ row }">
+        <TableAction
+          :actions="[
+            {
+              label: $t('common.edit'),
+              type: 'link',
+              ifShow: isEditable,
+              onClick: handleEdit.bind(null, row),
+            },
+            {
+              label: $t('common.delete'),
+              type: 'link',
+              danger: true,
+              ifShow: isEditable,
+              popConfirm: {
+                title: $t('ui.actionMessage.deleteConfirm', [row.processName]),
+                confirm: handleDelete.bind(null, row),
+              },
+            },
+          ]"
+        />
+      </template>
+    </Grid>
+  </div>
+</template>
\ No newline at end of file
diff --git a/src/views/wls/materialstock/components/index.ts b/src/views/wls/materialstock/components/index.ts
index 2cb4faf..60d3bf1 100644
--- a/src/views/wls/materialstock/components/index.ts
+++ b/src/views/wls/materialstock/components/index.ts
@@ -1,3 +1,4 @@
 export { default as WmMaterialStockSelectDialog } from './select-dialog.vue';
 export { default as WmMaterialStockSelect } from './select.vue';
 export { default as WmStockAdjustModal } from './stock-adjust-modal.vue';
+export { default as WmStockMoveModal } from './move-modal.vue';
diff --git a/src/views/wls/materialstock/components/move-modal.vue b/src/views/wls/materialstock/components/move-modal.vue
new file mode 100644
index 0000000..582726a
--- /dev/null
+++ b/src/views/wls/materialstock/components/move-modal.vue
@@ -0,0 +1,237 @@
+<script lang="ts" setup>
+import type { MesWmMaterialStockApi } from '#/api/mes/wm/materialstock';
+import type { VbenFormSchema } from '#/adapter/form';
+
+import { markRaw, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import { z } from '#/adapter/form';
+import { useVbenForm } from '#/adapter/form';
+import { moveMaterialStock } from '#/api/mes/wm/materialstock';
+import {
+  WmWarehouseAreaSelect,
+  WmWarehouseLocationSelect,
+  WmWarehouseSelect,
+} from '#/views/wls/warehouse/components';
+
+defineOptions({ name: 'WmStockMoveModal' });
+
+const emit = defineEmits<{ success: [] }>();
+
+const currentRow = ref<MesWmMaterialStockApi.MaterialStock>();
+
+function useFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'itemCode',
+      label: '鐗╂枡缂栫爜',
+      component: 'Input',
+      componentProps: { disabled: true },
+    },
+    {
+      fieldName: 'itemName',
+      label: '鐗╂枡鍚嶇О',
+      component: 'Input',
+      componentProps: { disabled: true },
+    },
+    {
+      fieldName: 'batchCode',
+      label: '鎵规鍙�',
+      component: 'Input',
+      componentProps: { disabled: true },
+    },
+    {
+      fieldName: 'warehouseName',
+      label: '褰撳墠浠撳簱',
+      component: 'Input',
+      componentProps: { disabled: true },
+    },
+    {
+      fieldName: 'locationName',
+      label: '褰撳墠搴撳尯',
+      component: 'Input',
+      componentProps: { disabled: true },
+    },
+    {
+      fieldName: 'areaName',
+      label: '褰撳墠搴撲綅',
+      component: 'Input',
+      componentProps: { disabled: true },
+    },
+    {
+      fieldName: 'quantity',
+      label: '褰撳墠鏁伴噺',
+      component: 'InputNumber',
+      componentProps: { disabled: true, class: '!w-full', precision: 2 },
+    },
+    {
+      fieldName: 'toWarehouseId',
+      label: '鐩爣浠撳簱',
+      component: markRaw(WmWarehouseSelect),
+      componentProps: {
+        placeholder: '璇烽�夋嫨鐩爣浠撳簱',
+        onChange: async () => {
+          await formApi.setFieldValue('toLocationId', undefined);
+          await formApi.setFieldValue('toAreaId', undefined);
+        },
+      },
+      rules: 'selectRequired',
+    },
+    {
+      fieldName: 'toLocationId',
+      label: '鐩爣搴撳尯',
+      component: markRaw(WmWarehouseLocationSelect),
+      componentProps: { placeholder: '璇烽�夋嫨鐩爣搴撳尯' },
+      dependencies: {
+        triggerFields: ['toWarehouseId'],
+        componentProps: (values) => ({
+          warehouseId: values.toWarehouseId,
+          placeholder: '璇烽�夋嫨鐩爣搴撳尯',
+          onChange: async () => {
+            await formApi.setFieldValue('toAreaId', undefined);
+          },
+        }),
+        trigger: (values, api) => {
+          if (values.toLocationId !== undefined) {
+            void api.setFieldValue('toLocationId', undefined);
+            void api.setFieldValue('toAreaId', undefined);
+          }
+        },
+      },
+      rules: 'selectRequired',
+    },
+    {
+      fieldName: 'toAreaId',
+      label: '鐩爣搴撲綅',
+      component: markRaw(WmWarehouseAreaSelect),
+      componentProps: { placeholder: '璇烽�夋嫨鐩爣搴撲綅' },
+      dependencies: {
+        triggerFields: ['toLocationId'],
+        componentProps: (values) => ({
+          locationId: values.toLocationId,
+          placeholder: '璇烽�夋嫨鐩爣搴撲綅',
+        }),
+        trigger: (values, api) => {
+          if (values.toAreaId !== undefined) {
+            void api.setFieldValue('toAreaId', undefined);
+          }
+        },
+      },
+      rules: 'selectRequired',
+    },
+    {
+      fieldName: 'moveQuantity',
+      label: '绉诲簱鏁伴噺',
+      component: 'InputNumber',
+      componentProps: {
+        class: '!w-full',
+        min: 0.01,
+        placeholder: '璇疯緭鍏ョЩ搴撴暟閲�',
+        precision: 2,
+      },
+      rules: z.number().positive('绉诲簱鏁伴噺蹇呴』澶т簬0'),
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      formItemClass: 'col-span-2',
+      componentProps: { placeholder: '璇疯緭鍏ュ娉�', rows: 3 },
+    },
+  ];
+}
+
+const [Form, formApi] = useVbenForm({
+  commonConfig: {
+    componentProps: { class: 'w-full' },
+    formItemClass: 'col-span-1',
+    labelWidth: 100,
+  },
+  layout: 'horizontal',
+  schema: useFormSchema(),
+  showDefaultActions: false,
+  wrapperClass: 'grid-cols-2',
+});
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    const { valid } = await formApi.validate();
+    if (!valid) return;
+
+    const row = currentRow.value;
+    if (!row?.id) return;
+
+    const values = await formApi.getValues();
+    const moveQuantity = values.moveQuantity as number;
+    const quantity = (row.quantity || 0) as number;
+    const availableQuantity = (row.availableQuantity || 0) as number;
+
+    if (moveQuantity > quantity) {
+      message.error(`绉诲簱鏁伴噺涓嶈兘瓒呰繃褰撳墠搴撳瓨鏁伴噺锛�${quantity.toFixed(2)}锛塦);
+      return;
+    }
+
+    if (moveQuantity > availableQuantity) {
+      message.error(`绉诲簱鏁伴噺涓嶈兘瓒呰繃鍙敤閲忥紙${availableQuantity.toFixed(2)}锛夛紝鍙敤閲� = 鍦ㄥ簱鏁伴噺 - 鍐荤粨鏁伴噺 - 鍗犵敤閲廯);
+      return;
+    }
+
+    if (row.warehouseId === values.toWarehouseId && row.locationId === values.toLocationId && row.areaId === values.toAreaId) {
+      message.error('鐩爣搴撲綅涓庡綋鍓嶅簱浣嶇浉鍚岋紝涓嶅厑璁哥Щ搴�');
+      return;
+    }
+
+    modalApi.lock();
+    try {
+      await moveMaterialStock({
+        materialStockId: row.id,
+        quantity: moveQuantity,
+        remark: values.remark,
+        toAreaId: values.toAreaId,
+        toLocationId: values.toLocationId,
+        toWarehouseId: values.toWarehouseId,
+      });
+      message.success('绉诲簱鎴愬姛');
+      emit('success');
+      await modalApi.close();
+    } finally {
+      modalApi.unlock();
+    }
+  },
+  async onOpenChange(isOpen: boolean) {
+    if (!isOpen) {
+      currentRow.value = undefined;
+      return;
+    }
+    const data = modalApi.getData<MesWmMaterialStockApi.MaterialStock>();
+    if (!data?.id) return;
+    currentRow.value = data;
+
+    if (data.frozen) {
+      message.warning('璇ュ簱瀛樿褰曞凡鍐荤粨锛屼笉鍏佽绉诲簱');
+      await modalApi.close();
+      return;
+    }
+
+    await formApi.setValues({
+      areaName: data.areaName,
+      batchCode: data.batchCode,
+      itemCode: data.itemCode,
+      itemName: data.itemName,
+      locationName: data.locationName,
+      quantity: data.quantity,
+      warehouseName: data.warehouseName,
+    });
+  },
+  title: '搴撳瓨绉诲簱',
+});
+</script>
+
+<template>
+  <Modal class="w-3/5">
+    <Form class="mx-4" />
+  </Modal>
+</template>
diff --git a/src/views/wls/materialstock/data.ts b/src/views/wls/materialstock/data.ts
index 3efe144..8cc4eb3 100644
--- a/src/views/wls/materialstock/data.ts
+++ b/src/views/wls/materialstock/data.ts
@@ -83,6 +83,7 @@
     newFrozen: boolean,
     row: MesWmMaterialStockApi.MaterialStock,
   ) => Promise<boolean | undefined>,
+  onMove?: (row: MesWmMaterialStockApi.MaterialStock) => void,
 ): VxeTableGridOptions<MesWmMaterialStockApi.MaterialStock>['columns'] {
   const { hasAccessByCodes } = useAccess();
   return [
@@ -189,6 +190,16 @@
         },
       },
     },
+    ...(onMove
+      ? [
+          {
+            title: '鎿嶄綔',
+            width: 80,
+            fixed: 'right' as const,
+            slots: { default: 'actions' } as const,
+          },
+        ]
+      : []),
   ];
 }
 
diff --git a/src/views/wls/materialstock/index.vue b/src/views/wls/materialstock/index.vue
index 3ae7731..17e3625 100644
--- a/src/views/wls/materialstock/index.vue
+++ b/src/views/wls/materialstock/index.vue
@@ -19,7 +19,7 @@
 import { MdItemTypeTree } from '#/views/mes/md/item/type/components';
 import { WmBatchDetail } from '#/views/wls/batch/components';
 import AreaForm from '#/views/wls/warehouse/area/modules/form.vue';
-import { WmStockAdjustModal } from './components';
+import { WmStockAdjustModal, WmStockMoveModal } from './components';
 
 import { useGridColumns, useGridFormSchema } from './data';
 
@@ -30,6 +30,11 @@
 
 const [StockAdjustModal, stockAdjustModalApi] = useVbenModal({
   connectedComponent: WmStockAdjustModal,
+  destroyOnClose: true,
+});
+
+const [StockMoveModal, stockMoveModalApi] = useVbenModal({
+  connectedComponent: WmStockMoveModal,
   destroyOnClose: true,
 });
 
@@ -74,6 +79,16 @@
   gridApi.query();
 }
 
+/** 鎵撳紑绉诲簱寮圭獥 */
+function handleOpenStockMove(row: MesWmMaterialStockApi.MaterialStock) {
+  stockMoveModalApi.setData(row).open();
+}
+
+/** 绉诲簱鎴愬姛鍥炶皟 */
+function handleStockMoveSuccess() {
+  gridApi.query();
+}
+
 /** 鎵撳紑鎵规璇︽儏寮圭獥 */
 function handleOpenBatchDetail(row: MesWmMaterialStockApi.MaterialStock) {
   if (!row.batchId) {
@@ -108,7 +123,7 @@
     schema: useGridFormSchema(),
   },
   gridOptions: {
-    columns: useGridColumns(handleFrozenChange),
+    columns: useGridColumns(handleFrozenChange, handleOpenStockMove),
     height: 'auto',
     keepSource: true,
     proxyConfig: {
@@ -139,6 +154,7 @@
   <Page auto-content-height><AreaModal />
     <WmBatchDetail ref="batchDetailRef" />
     <StockAdjustModal @success="handleStockAdjustSuccess" />
+    <StockMoveModal @success="handleStockMoveSuccess" />
 
     <div class="flex h-full w-full">
       <!-- 宸︿晶鐗╂枡鍒嗙被鏍� -->
@@ -192,6 +208,19 @@
             </Button>
             <span v-else>-</span>
           </template>
+          <template #actions="{ row }">
+            <TableAction
+              :actions="[
+                {
+                  label: '绉诲簱',
+                  type: 'link',
+                  auth: ['mes:wm-material-stock:move'],
+                  ifShow: !row.frozen,
+                  onClick: handleOpenStockMove.bind(null, row),
+                },
+              ]"
+            />
+          </template>
         </Grid>
       </div>
     </div>

--
Gitblit v1.9.3