From 081f242ea3c019f4eddd0fe342c5cf6189da1468 Mon Sep 17 00:00:00 2001
From: spring <2396852758@qq.com>
Date: 星期五, 03 七月 2026 13:58:01 +0800
Subject: [PATCH] fix: 产品设计

---
 src/router/routes/modules/mes.ts                                          |    9 
 src/views/mes/process-design/process/modules/content-list.vue             |  132 
 src/views/mes/process-design/route/components/index.ts                    |    1 
 src/views/mes/pd/project/modules/bom-list.vue                             |  122 
 src/views/mes/pd/project/modules/material-panel.vue                       |   28 
 src/api/mes/pd/mock/enabled.ts                                            |    4 
 src/views/mes/pd/project/index.vue                                        |  212 +
 src/views/mes/pd/project/modules/document-form.vue                        |  142 
 src/views/mes/pd/project/modules/drawing-review/review-form.vue           |   63 
 src/api/mes/pd/archive/index.ts                                           |   62 
 src/views/mes/process-design/parameter/index.vue                          |   65 
 src/views/mes/process-design/route/modules/product-list.vue               |  134 
 src/api/mes/pd/bom/index.ts                                               |   59 
 src/api/mes/pd/mock/seed.ts                                               |  386 ++
 src/views/mes/pd/project/detail/index.vue                                 |  224 +
 src/views/mes/pd/project/modules/task-draft-banner.vue                    |   32 
 src/views/mes/process-design/process/components/index.ts                  |    1 
 src/api/mes/pd/project/index.ts                                           |  119 
 src/views/mes/pd/archive/modules/detail.vue                               |  104 
 src/views/mes/pd/project/detail/data.ts                                   |   43 
 src/views/mes/process-design/route/modules/product-form.vue               |  125 
 src/api/mes/pd/mock/store.ts                                              |  554 +++
 src/views/mes/process-design/route/modules/form.vue                       |  121 
 src/views/mes/process-design/route/modules/product-bom-list.vue           |  171 +
 src/api/mes/pro/processdesign/parameter/index.ts                          |   64 
 src/views/mes/process-design/route/data.ts                                |  517 +++
 src/views/mes/process-design/process/modules/content-form.vue             |   98 
 src/api/mes/pd/version-change/index.ts                                    |   83 
 src/api/mes/pd/drawing-review/index.ts                                    |   74 
 src/views/mes/process-design/template/modules/parameter-select-dialog.vue |  207 +
 src/views/mes/pd/project/modules/task/task-form.vue                       |  114 
 src/views/mes/process-design/process/modules/form.vue                     |  114 
 mdm_frontend_change.md                                                    |    2 
 src/views/mes/process-design/route/modules/process-list.vue               |  136 
 src/views/mes/pd/project/modules/task/data.ts                             |  147 
 src/views/mes/process-design/process/data.ts                              |  274 +
 src/views/mes/pd/project/modules/workflow-action-form.vue                 |  126 
 .env.development                                                          |    3 
 src/views/mes/pd/project/modules/version-change/version-change-form.vue   |  108 
 src/views/mes/process-design/template/data.ts                             |  128 
 src/api/mes/pd/workflow/index.ts                                          |   28 
 src/views/mes/md/vendor/data.ts                                           |  381 --
 src/views/mes/pd/project/modules/document-list.vue                        |  165 +
 src/packages/constants/src/dict-enum.ts                                   |    8 
 src/views/mes/process-design/process/components/select.vue                |  114 
 src/views/mes/pd/project/modules/form.vue                                 |  106 
 src/views/mes/pd/archive/data.ts                                          |   71 
 FRONTEND_DEVELOPMENT.md                                                   |  311 
 src/views/mes/pro/task/data.ts                                            |    4 
 src/api/mes/pd/task/index.ts                                              |   98 
 src/views/mes/process-design/route/components/color-picker.vue            |   66 
 CLAUDE.md                                                                 |   11 
 src/views/mes/pd/project/data.ts                                          |  535 +++
 src/views/mes/pro/card/data.ts                                            |    2 
 src/views/mes/process-design/template/modules/form.vue                    |  173 +
 src/views/mes/pd/project/modules/create-form.vue                          |   77 
 vite.config.ts                                                            |   10 
 src/views/mes/pd/project/modules/publish-form.vue                         |  103 
 src/views/mes/pd/project/modules/drawing-review/review-list.vue           |   77 
 src/views/mes/pd/project/modules/task/task-list.vue                       |  149 
 src/views/mes/process-design/template/index.vue                           |   65 
 src/views/mes/pro/andon/record/data.ts                                    |    2 
 src/views/mes/pd/project/modules/version-change/data.ts                   |  167 +
 src/api/mes/pro/processdesign/template/index.ts                           |   53 
 src/views/mes/pd/project/modules/bom-form.vue                             |  124 
 .claude/settings.local.json                                               |    8 
 src/packages/constants/src/biz-mes-enum.ts                                |   62 
 src/views/mes/pd/project/modules/drawing-review/data.ts                   |   88 
 src/views/mes/process-design/route/modules/bom-form.vue                   |  118 
 src/views/mes/pd/archive/index.vue                                        |   75 
 src/views/mes/process-design/parameter/modules/form.vue                   |   99 
 /dev/null                                                                 |  103 
 src/views/mes/md/workstation/data.ts                                      |    2 
 src/views/mes/process-design/parameter/data.ts                            |  197 +
 src/views/mes/process-design/process/index.vue                            |   66 
 src/api/mes/pd/document/index.ts                                          |   71 
 src/api/mes/pd/mock/utils.ts                                              |   28 
 src/views/mes/md/item/data.ts                                             |    2 
 src/views/mes/pd/project/modules/version-change/version-change-list.vue   |  186 +
 src/views/mes/md/client/data.ts                                           |  387 --
 src/views/mes/process-design/route/index.vue                              |  180 +
 src/views/mes/pd/project/modules/workflow-timeline.vue                    |   53 
 src/views/mes/process-design/route/modules/process-form.vue               |  121 
 83 files changed, 8,727 insertions(+), 1,127 deletions(-)

diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index 82123c6..5d7a2ad 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -3,7 +3,13 @@
     "allow": [
       "WebFetch(domain:github.com)",
       "Bash(gh repo *)",
-      "WebSearch"
+      "WebSearch",
+      "PowerShell(pnpm typecheck *)",
+      "PowerShell(pnpm dev *)",
+      "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\\\\', ''\\) })"
     ]
   }
 }
diff --git a/.env.development b/.env.development
index cff2557..5ade162 100644
--- a/.env.development
+++ b/.env.development
@@ -19,3 +19,6 @@
 VITE_APP_DEFAULT_USERNAME=admin
 # 榛樿鐧诲綍瀵嗙爜
 VITE_APP_DEFAULT_PASSWORD=admin123
+
+# 浜у搧璁捐妯″潡鏈湴 Mock锛堟棤鍚庣鏃舵紨绀猴級
+VITE_PD_MOCK=true
diff --git a/CLAUDE.md b/CLAUDE.md
index b6cdce2..4954570 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -92,3 +92,14 @@
 2. 浣跨敤 VeeValidate + Zod 杩涜琛ㄥ崟楠岃瘉
 3. 浣跨敤 `v-dompurify-html` 澶勭悊鐢ㄦ埛杈撳叆鐨� HTML
 4. 鏁忔劅鏁版嵁浣跨敤 `secure-ls` 鍔犲瘑瀛樺偍
+
+## MES 妯″潡寮�鍙�
+
+> **閲嶈**锛氬紑鍙� MES 妯″潡鍓嶏紝蹇呴』鍏堥槄璇� `MES_DEVELOPMENT.md`锛岃鏂囦欢鍖呭惈锛�
+> - 椤甸潰鏂囦欢缁撴瀯瑙勮寖锛坉ata.ts / index.vue / form.vue锛�
+> - API 鎺ュ彛缂栧啓妯℃澘
+> - 瀵煎叆璺緞灞傜骇瑙勫垯锛坕ndex.vue 鐢� 4 灞� `..`锛宖orm.vue 鐢� 5 灞� `..`锛�
+> - 瀛楀吀/鏋氫妇娉ㄥ唽浣嶇疆
+> - 璺敱瑙勫垯锛堝姩鎬佽矾鐢憋紝鏃犻渶鎵嬪姩閰嶇疆锛�
+> - Select/SelectDialog 缁勪欢妯″紡
+> - 寮�鍙戞鏌ユ竻鍗�
diff --git a/FRONTEND_DEVELOPMENT.md b/FRONTEND_DEVELOPMENT.md
index 423b6ba..367ad5a 100644
--- a/FRONTEND_DEVELOPMENT.md
+++ b/FRONTEND_DEVELOPMENT.md
@@ -12,13 +12,13 @@
 
 ```typescript
 // 鉁� 姝g‘锛氱粍浠舵枃浠朵娇鐢� PascalCase
-UserList.vue
-DictTag.vue
-TableAction.vue
+UserList.vue;
+DictTag.vue;
+TableAction.vue;
 
 // 鉂� 閿欒锛氫娇鐢� kebab-case 鎴栧叾浠栧懡鍚�
-user-list.vue
-dict_tag.vue
+user - list.vue;
+dict_tag.vue;
 ```
 
 #### 缁勪欢瀹氫箟
@@ -26,7 +26,7 @@
 ```vue
 <script lang="ts" setup>
 // 鉁� 姝g‘锛氱粺涓�浣跨敤 <script setup lang="ts">
-defineOptions({ name: 'UserList' });
+defineOptions({ name: "UserList" });
 
 // Props 瀹氫箟
 interface Props {
@@ -35,13 +35,13 @@
 }
 
 const props = withDefaults(defineProps<Props>(), {
-  userName: '',
+  userName: "",
 });
 
 // Emits 瀹氫箟
 interface Emits {
-  (e: 'update', value: string): void;
-  (e: 'delete', id: number): void;
+  (e: "update", value: string): void;
+  (e: "delete", id: number): void;
 }
 
 const emit = defineEmits<Emits>();
@@ -68,10 +68,10 @@
 }
 
 // 鉁� 姝g‘锛氫娇鐢� import type 瀵煎叆绫诲瀷
-import type { UserInfo } from '#/api/system/user';
+import type { UserInfo } from "#/api/system/user";
 
 // 鉂� 閿欒锛氭櫘閫氬鍏ョ被鍨�
-import { UserInfo } from '#/api/system/user';
+import { UserInfo } from "#/api/system/user";
 ```
 
 ### 1.2 鐩綍缁撴瀯瑙勮寖
@@ -105,7 +105,7 @@
 ```typescript
 // src/api/system/user/index.ts
 
-import { request } from '#/utils/request';
+import { request } from "#/utils/request";
 
 export namespace SystemUserApi {
   /** 鐢ㄦ埛淇℃伅 */
@@ -137,7 +137,7 @@
 
 /** 鑾峰彇鐢ㄦ埛鍒嗛〉鍒楄〃 */
 export function getUserPage(params: SystemUserApi.PageParams) {
-  return request.get<SystemUserApi.PageResult>('/system/user/page', { params });
+  return request.get<SystemUserApi.PageResult>("/system/user/page", { params });
 }
 
 /** 鑾峰彇鐢ㄦ埛璇︽儏 */
@@ -147,12 +147,12 @@
 
 /** 鍒涘缓鐢ㄦ埛 */
 export function createUser(data: SystemUserApi.User) {
-  return request.post('/system/user/create', data);
+  return request.post("/system/user/create", data);
 }
 
 /** 鏇存柊鐢ㄦ埛 */
 export function updateUser(data: SystemUserApi.User) {
-  return request.put('/system/user/update', data);
+  return request.put("/system/user/update", data);
 }
 
 /** 鍒犻櫎鐢ㄦ埛 */
@@ -199,28 +199,28 @@
 
 ```vue
 <script lang="ts" setup>
-import type { VxeTableGridOptions } from '#/adapter/vxe-table';
-import type { SystemUserApi } from '#/api/system/user';
+import type { VxeTableGridOptions } from "#/adapter/vxe-table";
+import type { SystemUserApi } from "#/api/system/user";
 
-import { useVbenVxeGrid, TableAction, ACTION_ICON } from '#/adapter/vxe-table';
-import { getUserPage } from '#/api/system/user';
+import { useVbenVxeGrid, TableAction, ACTION_ICON } from "#/adapter/vxe-table";
+import { getUserPage } from "#/api/system/user";
 
 // 琛ㄦ牸鍒楅厤缃�
-const columns: VxeTableGridOptions['columns'] = [
-  { type: 'checkbox', width: 50 },
-  { field: 'username', title: '鐢ㄦ埛鍚�', width: 120 },
-  { field: 'nickname', title: '鏄电О', width: 120 },
+const columns: VxeTableGridOptions["columns"] = [
+  { type: "checkbox", width: 50 },
+  { field: "username", title: "鐢ㄦ埛鍚�", width: 120 },
+  { field: "nickname", title: "鏄电О", width: 120 },
   {
-    field: 'status',
-    title: '鐘舵��',
+    field: "status",
+    title: "鐘舵��",
     width: 100,
     cellRender: {
-      name: 'CellDict',
-      props: { type: 'common_status' },
+      name: "CellDict",
+      props: { type: "common_status" },
     },
   },
-  { field: 'createTime', title: '鍒涘缓鏃堕棿', width: 180 },
-  { field: 'action', title: '鎿嶄綔', width: 200, slots: { default: 'actions' } },
+  { field: "createTime", title: "鍒涘缓鏃堕棿", width: 180 },
+  { field: "action", title: "鎿嶄綔", width: 200, slots: { default: "actions" } },
 ];
 
 // 琛ㄦ牸瀹炰緥
@@ -228,23 +228,23 @@
   formOptions: {
     schema: [
       {
-        fieldName: 'username',
-        label: '鐢ㄦ埛鍚�',
-        component: 'Input',
+        fieldName: "username",
+        label: "鐢ㄦ埛鍚�",
+        component: "Input",
       },
       {
-        fieldName: 'status',
-        label: '鐘舵��',
-        component: 'Select',
+        fieldName: "status",
+        label: "鐘舵��",
+        component: "Select",
         componentProps: {
-          options: getDictOptions('common_status'),
+          options: getDictOptions("common_status"),
         },
       },
     ],
   },
   gridOptions: {
     columns,
-    height: 'auto',
+    height: "auto",
     keepSource: true,
     proxyConfig: {
       ajax: {
@@ -258,7 +258,7 @@
       },
     },
     rowConfig: {
-      keyField: 'id',
+      keyField: "id",
       isHover: true,
     },
   } as VxeTableGridOptions<SystemUserApi.User>,
@@ -302,22 +302,22 @@
 
 #### 琛ㄦ牸鍒楁覆鏌撳櫒
 
-| 娓叉煋鍣� | 鐢ㄩ�� | 绀轰緥 |
-|--------|------|------|
-| `CellImage` | 鍗曞浘灞曠ず | `cellRender: { name: 'CellImage' }` |
-| `CellImages` | 澶氬浘灞曠ず | `cellRender: { name: 'CellImages' }` |
-| `CellLink` | 閾炬帴鎸夐挳 | `cellRender: { name: 'CellLink', props: { text: '鏌ョ湅' } }` |
-| `CellTag` | 鏍囩灞曠ず | `cellRender: { name: 'CellTag', props: { color: 'blue' } }` |
-| `CellDict` | 瀛楀吀鏍囩 | `cellRender: { name: 'CellDict', props: { type: 'common_status' } }` |
-| `CellSwitch` | 寮�鍏冲垏鎹� | `cellRender: { name: 'CellSwitch', attrs: { beforeChange } }` |
+| 娓叉煋鍣�          | 鐢ㄩ��     | 绀轰緥                                                                 |
+| --------------- | -------- | -------------------------------------------------------------------- |
+| `CellImage`     | 鍗曞浘灞曠ず | `cellRender: { name: 'CellImage' }`                                  |
+| `CellImages`    | 澶氬浘灞曠ず | `cellRender: { name: 'CellImages' }`                                 |
+| `CellLink`      | 閾炬帴鎸夐挳 | `cellRender: { name: 'CellLink', props: { text: '鏌ョ湅' } }`          |
+| `CellTag`       | 鏍囩灞曠ず | `cellRender: { name: 'CellTag', props: { color: 'blue' } }`          |
+| `CellDict`      | 瀛楀吀鏍囩 | `cellRender: { name: 'CellDict', props: { type: 'common_status' } }` |
+| `CellSwitch`    | 寮�鍏冲垏鎹� | `cellRender: { name: 'CellSwitch', attrs: { beforeChange } }`        |
 | `CellOperation` | 鎿嶄綔鎸夐挳 | `cellRender: { name: 'CellOperation', options: ['edit', 'delete'] }` |
 
 ### 2.2 琛ㄥ崟寮圭獥 (useVbenModal)
 
 ```vue
 <script lang="ts" setup>
-import { useVbenModal } from '#/packages/effects/common-ui/src';
-import Form from './modules/form.vue';
+import { useVbenModal } from "#/packages/effects/common-ui/src";
+import Form from "./modules/form.vue";
 
 // 寮圭獥瀹炰緥
 const [FormModal, formModalApi] = useVbenModal({
@@ -346,14 +346,14 @@
 ```vue
 <!-- modules/form.vue -->
 <script lang="ts" setup>
-import type { SystemUserApi } from '#/api/system/user';
+import type { SystemUserApi } from "#/api/system/user";
 
-import { computed, ref } from 'vue';
-import { useVbenModal } from '#/packages/effects/common-ui/src';
-import { useVbenForm } from '#/adapter/form';
-import { createUser, updateUser } from '#/api/system/user';
+import { computed, ref } from "vue";
+import { useVbenModal } from "#/packages/effects/common-ui/src";
+import { useVbenForm } from "#/adapter/form";
+import { createUser, updateUser } from "#/api/system/user";
 
-const emit = defineEmits(['success']);
+const emit = defineEmits(["success"]);
 
 const [Modal, modalApi] = useVbenModal({
   async onConfirm() {
@@ -363,7 +363,7 @@
     } else {
       await createUser(values);
     }
-    emit('success');
+    emit("success");
     modalApi.close();
   },
 });
@@ -373,27 +373,27 @@
 const [Form, formApi] = useVbenForm({
   schema: [
     {
-      fieldName: 'username',
-      label: '鐢ㄦ埛鍚�',
-      rules: 'required',
-      component: 'Input',
+      fieldName: "username",
+      label: "鐢ㄦ埛鍚�",
+      rules: "required",
+      component: "Input",
     },
     {
-      fieldName: 'nickname',
-      label: '鏄电О',
-      rules: 'required',
-      component: 'Input',
+      fieldName: "nickname",
+      label: "鏄电О",
+      rules: "required",
+      component: "Input",
     },
     {
-      fieldName: 'email',
-      label: '閭',
-      rules: 'email',
-      component: 'Input',
+      fieldName: "email",
+      label: "閭",
+      rules: "email",
+      component: "Input",
     },
     {
-      fieldName: 'deptId',
-      label: '閮ㄩ棬',
-      component: 'TreeSelect',
+      fieldName: "deptId",
+      label: "閮ㄩ棬",
+      component: "TreeSelect",
     },
   ],
 });
@@ -422,7 +422,7 @@
 
 ```vue
 <script lang="ts" setup>
-import DictTag from '#/components/dict-tag/dict-tag.vue';
+import DictTag from "#/components/dict-tag/dict-tag.vue";
 </script>
 
 <template>
@@ -436,7 +436,7 @@
 
 ```vue
 <script lang="ts" setup>
-import DictSelect from '#/components/form-create/components/dict-select.vue';
+import DictSelect from "#/components/form-create/components/dict-select.vue";
 </script>
 
 <template>
@@ -462,16 +462,20 @@
 #### 瀛楀吀宸ュ叿鍑芥暟
 
 ```typescript
-import { getDictOptions, getDictLabel, getDictObj } from '#/packages/effects/hooks/src';
+import {
+  getDictOptions,
+  getDictLabel,
+  getDictObj,
+} from "#/packages/effects/hooks/src";
 
 // 鑾峰彇瀛楀吀閫夐」鍒楄〃锛堢敤浜� Select/Radio 绛夌粍浠讹級
-const options = getDictOptions('common_status', 'number');
+const options = getDictOptions("common_status", "number");
 
 // 鑾峰彇瀛楀吀鏍囩鏂囨湰
-const label = getDictLabel('common_status', 1); // "寮�鍚�"
+const label = getDictLabel("common_status", 1); // "寮�鍚�"
 
 // 鑾峰彇瀛楀吀瀹屾暣瀵硅薄
-const dictObj = getDictObj('common_status', 1);
+const dictObj = getDictObj("common_status", 1);
 // { label: '寮�鍚�', value: 1, colorType: 'success', cssClass: '' }
 ```
 
@@ -481,7 +485,7 @@
 
 ```vue
 <script lang="ts" setup>
-import { TableAction, ACTION_ICON } from '#/adapter/vxe-table';
+import { TableAction, ACTION_ICON } from "#/adapter/vxe-table";
 </script>
 
 <template>
@@ -523,9 +527,9 @@
 
 ```vue
 <script lang="ts" setup>
-import ImageUpload from '#/components/upload/image-upload.vue';
+import ImageUpload from "#/components/upload/image-upload.vue";
 
-const imageUrl = ref('');
+const imageUrl = ref("");
 const imageList = ref<string[]>([]);
 </script>
 
@@ -549,7 +553,7 @@
 
 ```vue
 <script lang="ts" setup>
-import FileUpload from '#/components/upload/file-upload.vue';
+import FileUpload from "#/components/upload/file-upload.vue";
 
 const fileList = ref<string[]>([]);
 </script>
@@ -570,26 +574,26 @@
 
 ```vue
 <script lang="ts" setup>
-import Description from '#/components/description/description.vue';
+import Description from "#/components/description/description.vue";
 
 const data = {
-  username: 'admin',
-  nickname: '绠$悊鍛�',
-  email: 'admin@example.com',
+  username: "admin",
+  nickname: "绠$悊鍛�",
+  email: "admin@example.com",
   status: 0,
-  createTime: '2024-01-01 12:00:00',
+  createTime: "2024-01-01 12:00:00",
 };
 
 const schema = [
-  { field: 'username', label: '鐢ㄦ埛鍚�' },
-  { field: 'nickname', label: '鏄电О' },
-  { field: 'email', label: '閭' },
+  { field: "username", label: "鐢ㄦ埛鍚�" },
+  { field: "nickname", label: "鏄电О" },
+  { field: "email", label: "閭" },
   {
-    field: 'status',
-    label: '鐘舵��',
-    render: (value) => h(DictTag, { type: 'common_status', value }),
+    field: "status",
+    label: "鐘舵��",
+    render: (value) => h(DictTag, { type: "common_status", value }),
   },
-  { field: 'createTime', label: '鍒涘缓鏃堕棿' },
+  { field: "createTime", label: "鍒涘缓鏃堕棿" },
 ];
 </script>
 
@@ -608,16 +612,16 @@
 
 #### 甯哥敤 Store
 
-| Store | 鐢ㄩ�� | 瀵煎叆璺緞 |
-|-------|------|----------|
-| `useUserStore` | 鐢ㄦ埛淇℃伅 | `#/packages/stores/src` |
+| Store            | 鐢ㄩ��       | 瀵煎叆璺緞                |
+| ---------------- | ---------- | ----------------------- |
+| `useUserStore`   | 鐢ㄦ埛淇℃伅   | `#/packages/stores/src` |
 | `useAccessStore` | 鏉冮檺/Token | `#/packages/stores/src` |
-| `useDictStore` | 鏁版嵁瀛楀吀 | `#/packages/stores/src` |
+| `useDictStore`   | 鏁版嵁瀛楀吀   | `#/packages/stores/src` |
 
 #### 绀轰緥
 
 ```typescript
-import { useUserStore, useAccessStore } from '#/packages/stores/src';
+import { useUserStore, useAccessStore } from "#/packages/stores/src";
 
 const userStore = useUserStore();
 const accessStore = useAccessStore();
@@ -627,7 +631,7 @@
 
 // 妫�鏌ユ潈闄�
 const { hasAccessByCodes } = useAccess();
-const canEdit = hasAccessByCodes(['system:user:update']);
+const canEdit = hasAccessByCodes(["system:user:update"]);
 ```
 
 ---
@@ -638,7 +642,7 @@
 
 ```vue
 <script lang="ts" setup>
-import { useAccess } from '#/packages/effects/access/src';
+import { useAccess } from "#/packages/effects/access/src";
 
 const { hasAccessByCodes } = useAccess();
 </script>
@@ -686,12 +690,12 @@
 
 ```vue
 <script lang="ts" setup>
-import { $t } from '#/locales';
+import { $t } from "#/locales";
 </script>
 
 <template>
-  <Button>{{ $t('common.save') }}</Button>
-  <span>{{ $t('ui.actionMessage.deleteSuccess', ['鐢ㄦ埛']) }}</span>
+  <Button>{{ $t("common.save") }}</Button>
+  <span>{{ $t("ui.actionMessage.deleteSuccess", ["鐢ㄦ埛"]) }}</span>
 </template>
 ```
 
@@ -721,7 +725,7 @@
 ### 6.1 usePagination - 鍒嗛〉
 
 ```typescript
-import { usePagination } from '#/packages/effects/hooks/src';
+import { usePagination } from "#/packages/effects/hooks/src";
 
 const { page, pageSize, total, setPage, setTotal } = usePagination();
 ```
@@ -729,7 +733,7 @@
 ### 6.2 useTabs - 鏍囩椤垫搷浣�
 
 ```typescript
-import { useTabs } from '#/packages/effects/hooks/src';
+import { useTabs } from "#/packages/effects/hooks/src";
 
 const { closeOtherTabs, closeTab, refreshTab } = useTabs();
 
@@ -743,13 +747,13 @@
 ### 6.3 useWatermark - 姘村嵃
 
 ```typescript
-import { useWatermark } from '#/packages/effects/hooks/src';
+import { useWatermark } from "#/packages/effects/hooks/src";
 
 const { updateWatermark, destroyWatermark } = useWatermark();
 
 // 璁剧疆姘村嵃
 await updateWatermark({
-  content: '鐢ㄦ埛鍚�',
+  content: "鐢ㄦ埛鍚�",
 });
 
 // 閿�姣佹按鍗�
@@ -764,20 +768,20 @@
 
 ```typescript
 import {
-  formatDateTime,      // 鏃ユ湡鏍煎紡鍖�
-  formatDate,          // 鏃ユ湡鏍煎紡鍖栵紙涓嶅惈鏃堕棿锛�
+  formatDateTime, // 鏃ユ湡鏍煎紡鍖�
+  formatDate, // 鏃ユ湡鏍煎紡鍖栵紙涓嶅惈鏃堕棿锛�
   downloadFileFromBlobPart, // 鏂囦欢涓嬭浇
-  isEmpty,             // 鍒ょ┖
-  isFunction,          // 鍒ゆ柇鍑芥暟
-  isString,            // 鍒ゆ柇瀛楃涓�
-} from '#/packages/utils/src';
+  isEmpty, // 鍒ょ┖
+  isFunction, // 鍒ゆ柇鍑芥暟
+  isString, // 鍒ゆ柇瀛楃涓�
+} from "#/packages/utils/src";
 
 // 鏃ユ湡鏍煎紡鍖�
-formatDateTime('2024-01-01T12:00:00'); // "2024-01-01 12:00:00"
+formatDateTime("2024-01-01T12:00:00"); // "2024-01-01 12:00:00"
 
 // 鏂囦欢涓嬭浇
 downloadFileFromBlobPart({
-  fileName: '鐢ㄦ埛鍒楄〃.xlsx',
+  fileName: "鐢ㄦ埛鍒楄〃.xlsx",
   source: blobData,
 });
 
@@ -791,10 +795,10 @@
 
 ```typescript
 import {
-  erpNumberFormatter,    // 鏁板瓧鏍煎紡鍖�
-  fenToYuan,             // 鍒嗚浆鍏�
-  formatFileSize,        // 鏂囦欢澶у皬鏍煎紡鍖�
-} from '#/packages/utils/src';
+  erpNumberFormatter, // 鏁板瓧鏍煎紡鍖�
+  fenToYuan, // 鍒嗚浆鍏�
+  formatFileSize, // 鏂囦欢澶у皬鏍煎紡鍖�
+} from "#/packages/utils/src";
 
 // 淇濈暀涓や綅灏忔暟
 erpNumberFormatter(1234.567, 2); // "1,234.57"
@@ -812,28 +816,28 @@
 
 ```vue
 <script lang="ts" setup>
-import type { VxeTableGridOptions } from '#/adapter/vxe-table';
-import type { SystemUserApi } from '#/api/system/user';
+import type { VxeTableGridOptions } from "#/adapter/vxe-table";
+import type { SystemUserApi } from "#/api/system/user";
 
-import { ref } from 'vue';
+import { ref } from "vue";
 
-import { confirm, Page, useVbenModal } from '#/packages/effects/common-ui/src';
-import { getDictOptions } from '#/packages/effects/hooks/src';
-import { downloadFileFromBlobPart, isEmpty } from '#/packages/utils/src';
+import { confirm, Page, useVbenModal } from "#/packages/effects/common-ui/src";
+import { getDictOptions } from "#/packages/effects/hooks/src";
+import { downloadFileFromBlobPart, isEmpty } from "#/packages/utils/src";
 
-import { message } from 'ant-design-vue';
+import { message } from "ant-design-vue";
 
-import { useVbenVxeGrid, TableAction, ACTION_ICON } from '#/adapter/vxe-table';
+import { useVbenVxeGrid, TableAction, ACTION_ICON } from "#/adapter/vxe-table";
 import {
   getUserPage,
   deleteUser,
   createUser,
   updateUser,
   exportUser,
-} from '#/api/system/user';
-import { $t } from '#/locales';
+} from "#/api/system/user";
+import { $t } from "#/locales";
 
-import Form from './modules/form.vue';
+import Form from "./modules/form.vue";
 
 // ========== 寮圭獥瀹氫箟 ==========
 const [FormModal, formModalApi] = useVbenModal({
@@ -848,37 +852,42 @@
   formOptions: {
     schema: [
       {
-        fieldName: 'username',
-        label: '鐢ㄦ埛鍚�',
-        component: 'Input',
-        componentProps: { placeholder: '璇疯緭鍏ョ敤鎴峰悕' },
+        fieldName: "username",
+        label: "鐢ㄦ埛鍚�",
+        component: "Input",
+        componentProps: { placeholder: "璇疯緭鍏ョ敤鎴峰悕" },
       },
       {
-        fieldName: 'status',
-        label: '鐘舵��',
-        component: 'Select',
+        fieldName: "status",
+        label: "鐘舵��",
+        component: "Select",
         componentProps: {
-          options: getDictOptions('common_status', 'number'),
-          placeholder: '璇烽�夋嫨鐘舵��',
+          options: getDictOptions("common_status", "number"),
+          placeholder: "璇烽�夋嫨鐘舵��",
         },
       },
     ],
   },
   gridOptions: {
     columns: [
-      { type: 'checkbox', width: 50 },
-      { field: 'username', title: '鐢ㄦ埛鍚�', width: 120 },
-      { field: 'nickname', title: '鏄电О', width: 120 },
+      { type: "checkbox", width: 50 },
+      { field: "username", title: "鐢ㄦ埛鍚�", width: 120 },
+      { field: "nickname", title: "鏄电О", width: 120 },
       {
-        field: 'status',
-        title: '鐘舵��',
+        field: "status",
+        title: "鐘舵��",
         width: 100,
-        cellRender: { name: 'CellDict', props: { type: 'common_status' } },
+        cellRender: { name: "CellDict", props: { type: "common_status" } },
       },
-      { field: 'createTime', title: '鍒涘缓鏃堕棿', width: 180 },
-      { field: 'action', title: '鎿嶄綔', width: 180, slots: { default: 'actions' } },
+      { field: "createTime", title: "鍒涘缓鏃堕棿", width: 180 },
+      {
+        field: "action",
+        title: "鎿嶄綔",
+        width: 180,
+        slots: { default: "actions" },
+      },
     ],
-    height: 'auto',
+    height: "auto",
     keepSource: true,
     proxyConfig: {
       ajax: {
@@ -891,7 +900,7 @@
         },
       },
     },
-    rowConfig: { keyField: 'id', isHover: true },
+    rowConfig: { keyField: "id", isHover: true },
     toolbarConfig: { refresh: true, search: true },
   } as VxeTableGridOptions<SystemUserApi.User>,
   gridEvents: {
@@ -911,7 +920,7 @@
 
 async function handleExport() {
   const data = await exportUser(await gridApi.formApi.getValues());
-  downloadFileFromBlobPart({ fileName: '鐢ㄦ埛.xls', source: data });
+  downloadFileFromBlobPart({ fileName: "鐢ㄦ埛.xls", source: data });
 }
 
 function handleCreate() {
@@ -924,12 +933,12 @@
 
 async function handleDelete(row: SystemUserApi.User) {
   await deleteUser(row.id!);
-  message.success($t('ui.actionMessage.deleteSuccess', [row.username]));
+  message.success($t("ui.actionMessage.deleteSuccess", [row.username]));
   handleRefresh();
 }
 
 async function handleDeleteBatch() {
-  await confirm($t('ui.actionMessage.deleteBatchConfirm'));
+  await confirm($t("ui.actionMessage.deleteBatchConfirm"));
   // ... 鎵归噺鍒犻櫎閫昏緫
 }
 </script>
diff --git a/mdm_frontend_change.md b/mdm_frontend_change.md
index 3e9c175..5db78df 100644
--- a/mdm_frontend_change.md
+++ b/mdm_frontend_change.md
@@ -1,5 +1,7 @@
 # 鐗╂枡涓绘暟鎹ā鍧� (MDM) 鍓嶇鍙樻洿鏂囨。
 
+> **鍩哄噯鏂囨。**锛氭湰鏂囨。涓� MDM 妯″潡鐨�**涓氬姟涓� API 鍙樻洿鍩哄噯**銆傞〉闈㈠疄鐜版柟寮忥紙缁勪欢銆佽〃鏍笺�佽〃鍗曘�佺洰褰曠粨鏋勶級椤婚伒寰� [`FRONTEND_DEVELOPMENT.md`](./FRONTEND_DEVELOPMENT.md)锛屾湰鏂囨。涓殑 Element Plus 绀轰緥浠呮弿杩颁笟鍔″瓧娈典笌浜や簰锛屼笉鍙洿鎺ョ収鎼� UI 妗嗘灦銆�
+
 ## 涓�銆佹柊澧為〉闈�
 
 ### 1.1 鐗╂枡涓绘暟鎹鐞�
diff --git a/src/api/mes/pd/archive/index.ts b/src/api/mes/pd/archive/index.ts
new file mode 100644
index 0000000..33c6efb
--- /dev/null
+++ b/src/api/mes/pd/archive/index.ts
@@ -0,0 +1,62 @@
+import type { PageParam, PageResult } from '#/packages/effects/request/src';
+
+import { requestClient } from '#/api/request';
+
+import { isPdMockEnabled } from '../mock/enabled';
+import { pdMockStore } from '../mock/store';
+
+export namespace MesPdArchiveApi {
+  /** 鏁板瓧鍖栬璁″綊妗e彴璐� */
+  export interface Archive {
+    id?: number;
+    code?: string;
+    projectId?: number;
+    projectCode?: string;
+    projectName?: string;
+    productCode?: string;
+    productName?: string;
+    version?: string;
+    archiveStatus?: number;
+    archiveTime?: number;
+    archiverId?: number;
+    archiverName?: string;
+    documentCount?: number;
+    bomCount?: number;
+    storagePath?: string;
+    remark?: string;
+    createTime?: number;
+  }
+
+  export interface PageParams extends PageParam {
+    code?: string;
+    projectCode?: string;
+    productCode?: string;
+    archiveStatus?: number;
+  }
+}
+
+export function getPdArchivePage(params: MesPdArchiveApi.PageParams) {
+  if (isPdMockEnabled()) {
+    return pdMockStore.getArchivePage(params);
+  }
+  return requestClient.get<PageResult<MesPdArchiveApi.Archive>>(
+    '/mes/pd/archive/page',
+    { params },
+  );
+}
+
+export function getPdArchive(id: number) {
+  if (isPdMockEnabled()) {
+    return pdMockStore.getArchive(id);
+  }
+  return requestClient.get<MesPdArchiveApi.Archive>(
+    `/mes/pd/archive/get?id=${id}`,
+  );
+}
+
+export function exportPdArchive(params: MesPdArchiveApi.PageParams) {
+  if (isPdMockEnabled()) {
+    return Promise.resolve(new Blob());
+  }
+  return requestClient.download('/mes/pd/archive/export-excel', { params });
+}
diff --git a/src/api/mes/pd/bom/index.ts b/src/api/mes/pd/bom/index.ts
new file mode 100644
index 0000000..9e4ad3f
--- /dev/null
+++ b/src/api/mes/pd/bom/index.ts
@@ -0,0 +1,59 @@
+import type { PageParam, PageResult } from '#/packages/effects/request/src';
+
+import { requestClient } from '#/api/request';
+
+import { isPdMockEnabled } from '../mock/enabled';
+import { pdMockStore } from '../mock/store';
+
+export namespace MesPdBomApi {
+  /** 璁捐 BOM 琛� */
+  export interface BomLine {
+    id?: number;
+    projectId?: number;
+    itemId?: number;
+    itemCode?: string;
+    itemName?: string;
+    specification?: string;
+    unitMeasureName?: string;
+    quantity?: number;
+    remark?: string;
+    createTime?: number;
+  }
+
+  export interface PageParams extends PageParam {
+    projectId: number;
+    itemCode?: string;
+    itemName?: string;
+  }
+}
+
+export function getPdBomPage(params: MesPdBomApi.PageParams) {
+  if (isPdMockEnabled()) {
+    return pdMockStore.getBomPage(params);
+  }
+  return requestClient.get<PageResult<MesPdBomApi.BomLine>>(
+    '/mes/pd/bom/page',
+    { params },
+  );
+}
+
+export function createPdBomLine(data: MesPdBomApi.BomLine) {
+  if (isPdMockEnabled()) {
+    return pdMockStore.createBomLine(data);
+  }
+  return requestClient.post<number>('/mes/pd/bom/create', data);
+}
+
+export function updatePdBomLine(data: MesPdBomApi.BomLine) {
+  if (isPdMockEnabled()) {
+    return pdMockStore.updateBomLine(data);
+  }
+  return requestClient.put('/mes/pd/bom/update', data);
+}
+
+export function deletePdBomLine(id: number) {
+  if (isPdMockEnabled()) {
+    return pdMockStore.deleteBomLine(id);
+  }
+  return requestClient.delete(`/mes/pd/bom/delete?id=${id}`);
+}
diff --git a/src/api/mes/pd/document/index.ts b/src/api/mes/pd/document/index.ts
new file mode 100644
index 0000000..3a637e9
--- /dev/null
+++ b/src/api/mes/pd/document/index.ts
@@ -0,0 +1,71 @@
+import type { PageParam, PageResult } from '#/packages/effects/request/src';
+
+import { requestClient } from '#/api/request';
+
+import { isPdMockEnabled } from '../mock/enabled';
+import { pdMockStore } from '../mock/store';
+
+export namespace MesPdDocumentApi {
+  /** 璁捐璧勬枡 */
+  export interface Document {
+    id?: number;
+    projectId?: number;
+    documentType?: number;
+    name?: string;
+    version?: string;
+    fileUrl?: string;
+    fileName?: string;
+    fileSize?: number;
+    uploaderId?: number;
+    uploaderName?: string;
+    remark?: string;
+    createTime?: number;
+  }
+
+  export interface PageParams extends PageParam {
+    projectId: number;
+    documentType?: number;
+    name?: string;
+    version?: string;
+  }
+}
+
+export function getPdDocumentPage(params: MesPdDocumentApi.PageParams) {
+  if (isPdMockEnabled()) {
+    return pdMockStore.getDocumentPage(params);
+  }
+  return requestClient.get<PageResult<MesPdDocumentApi.Document>>(
+    '/mes/pd/document/page',
+    { params },
+  );
+}
+
+export function getPdDocument(id: number) {
+  if (isPdMockEnabled()) {
+    return pdMockStore.getDocument(id);
+  }
+  return requestClient.get<MesPdDocumentApi.Document>(
+    `/mes/pd/document/get?id=${id}`,
+  );
+}
+
+export function createPdDocument(data: MesPdDocumentApi.Document) {
+  if (isPdMockEnabled()) {
+    return pdMockStore.createDocument(data);
+  }
+  return requestClient.post<number>('/mes/pd/document/create', data);
+}
+
+export function updatePdDocument(data: MesPdDocumentApi.Document) {
+  if (isPdMockEnabled()) {
+    return pdMockStore.updateDocument(data);
+  }
+  return requestClient.put('/mes/pd/document/update', data);
+}
+
+export function deletePdDocument(id: number) {
+  if (isPdMockEnabled()) {
+    return pdMockStore.deleteDocument(id);
+  }
+  return requestClient.delete(`/mes/pd/document/delete?id=${id}`);
+}
diff --git a/src/api/mes/pd/drawing-review/index.ts b/src/api/mes/pd/drawing-review/index.ts
new file mode 100644
index 0000000..3aa08c3
--- /dev/null
+++ b/src/api/mes/pd/drawing-review/index.ts
@@ -0,0 +1,74 @@
+import type { PageParam, PageResult } from '#/packages/effects/request/src';
+
+import { requestClient } from '#/api/request';
+
+import { isPdMockEnabled } from '../mock/enabled';
+import { pdMockStore } from '../mock/store';
+
+export namespace MesPdDrawingReviewApi {
+  /** 鍥剧焊瀹℃牳 */
+  export interface DrawingReview {
+    id?: number;
+    code?: string;
+    projectId?: number;
+    projectCode?: string;
+    projectName?: string;
+    documentId?: number;
+    documentName?: string;
+    documentVersion?: string;
+    reviewerId?: number;
+    reviewerName?: string;
+    status?: number;
+    reviewComment?: string;
+    reviewTime?: number;
+    createTime?: number;
+  }
+
+  export interface PageParams extends PageParam {
+    code?: string;
+    projectId?: number;
+    status?: number;
+    reviewerId?: number;
+  }
+
+  export interface ReviewParams {
+    id: number;
+    status: number;
+    reviewComment?: string;
+  }
+}
+
+export function getPdDrawingReviewPage(
+  params: MesPdDrawingReviewApi.PageParams,
+) {
+  if (isPdMockEnabled()) {
+    return pdMockStore.getDrawingReviewPage(params);
+  }
+  return requestClient.get<PageResult<MesPdDrawingReviewApi.DrawingReview>>(
+    '/mes/pd/drawing-review/page',
+    { params },
+  );
+}
+
+export function getPdDrawingReview(id: number) {
+  if (isPdMockEnabled()) {
+    return pdMockStore.getDrawingReview(id);
+  }
+  return requestClient.get<MesPdDrawingReviewApi.DrawingReview>(
+    `/mes/pd/drawing-review/get?id=${id}`,
+  );
+}
+
+export function createPdDrawingReview(data: MesPdDrawingReviewApi.DrawingReview) {
+  if (isPdMockEnabled()) {
+    return Promise.resolve(1);
+  }
+  return requestClient.post<number>('/mes/pd/drawing-review/create', data);
+}
+
+export function reviewPdDrawing(data: MesPdDrawingReviewApi.ReviewParams) {
+  if (isPdMockEnabled()) {
+    return pdMockStore.reviewDrawing(data);
+  }
+  return requestClient.put('/mes/pd/drawing-review/review', data);
+}
diff --git a/src/api/mes/pd/mock/enabled.ts b/src/api/mes/pd/mock/enabled.ts
new file mode 100644
index 0000000..d22db56
--- /dev/null
+++ b/src/api/mes/pd/mock/enabled.ts
@@ -0,0 +1,4 @@
+/** 鏄惁鍚敤浜у搧璁捐锛圥D锛夋ā鍧楁湰鍦� Mock */
+export function isPdMockEnabled(): boolean {
+  return import.meta.env.VITE_PD_MOCK === 'true';
+}
diff --git a/src/api/mes/pd/mock/seed.ts b/src/api/mes/pd/mock/seed.ts
new file mode 100644
index 0000000..f011b92
--- /dev/null
+++ b/src/api/mes/pd/mock/seed.ts
@@ -0,0 +1,386 @@
+import type { MesPdArchiveApi } from '#/api/mes/pd/archive';
+import type { MesPdBomApi } from '#/api/mes/pd/bom';
+import type { MesPdDocumentApi } from '#/api/mes/pd/document';
+import type { MesPdDrawingReviewApi } from '#/api/mes/pd/drawing-review';
+import type { MesPdProjectApi } from '#/api/mes/pd/project';
+import type { MesPdVersionChangeApi } from '#/api/mes/pd/version-change';
+import type { MesPdWorkflowApi } from '#/api/mes/pd/workflow';
+
+import {
+  MesPdDocumentTypeEnum,
+  MesPdDrawingReviewStatusEnum,
+  MesPdProjectStatusEnum,
+  MesPdVersionChangeTypeEnum,
+} from '#/packages/constants/src';
+
+const DAY = 86_400_000;
+const now = Date.now();
+
+function ts(daysAgo = 0): number {
+  return now - daysAgo * DAY;
+}
+
+/** 璁捐浠诲姟锛氬悓涓�浜у搧鍙娆″彂璧凤紝姣忔瀵瑰簲涓�涓増鏈� */
+export const pdMockProjects: MesPdProjectApi.Project[] = [
+  {
+    id: 1,
+    code: 'DT-2026-0001',
+    name: '鏅鸿兘娓╂帶妯$粍 V1.0 璁捐',
+    productCode: 'PRD-TC-100',
+    productName: '鏅鸿兘娓╂帶妯$粍',
+    version: 'V1.0',
+    status: MesPdProjectStatusEnum.DESIGNING,
+    designerName: '寮犲伐',
+    reviewerName: '鏉庝富绠�',
+    planStartDate: ts(30),
+    planEndDate: ts(-15),
+    remark: '棣栨璁捐锛屼富鎺ф澘 + 浼犳劅鍣ㄤ竴浣撳寲',
+    createTime: ts(35),
+  },
+  {
+    id: 2,
+    code: 'DT-2026-0002',
+    name: '宸ヤ笟缃戝叧 V2.1 璁捐',
+    productCode: 'PRD-GW-200',
+    productName: '宸ヤ笟鐗╄仈缃戠綉鍏�',
+    version: 'V2.1',
+    status: MesPdProjectStatusEnum.REVIEWING,
+    designerName: '鐜嬪伐',
+    reviewerName: '璧电粡鐞�',
+    planStartDate: ts(20),
+    planEndDate: ts(-5),
+    remark: '閫氫俊鍗忚鍗囩骇锛屽澹崇粨鏋勮皟鏁�',
+    createTime: ts(25),
+  },
+  {
+    id: 3,
+    code: 'DT-2025-0012',
+    name: '澶氶�氶亾閲囬泦鏉� V1.2 璁捐',
+    productCode: 'PRD-SC-050',
+    productName: '澶氶�氶亾閲囬泦鏉�',
+    version: 'V1.2',
+    status: MesPdProjectStatusEnum.ARCHIVED,
+    designerName: '闄堝伐',
+    reviewerName: '鏉庝富绠�',
+    planStartDate: ts(90),
+    planEndDate: ts(45),
+    actualFinishDate: ts(40),
+    remark: '宸查噺浜у綊妗�',
+    createTime: ts(95),
+  },
+  {
+    id: 4,
+    code: 'DT-2026-0004',
+    name: '浼烘湇鐢垫満椹卞姩鍣� V0.9 璁捐',
+    productCode: 'PRD-MD-300',
+    productName: '浼烘湇鐢垫満椹卞姩鍣�',
+    version: 'V0.9',
+    status: MesPdProjectStatusEnum.DRAFT,
+    designerName: '鍒樺伐',
+    reviewerName: '璧电粡鐞�',
+    planStartDate: ts(5),
+    planEndDate: ts(-30),
+    createTime: ts(3),
+  },
+  {
+    id: 5,
+    code: 'DT-2024-0008',
+    name: '澶氶�氶亾閲囬泦鏉� V1.0 璁捐',
+    productCode: 'PRD-SC-050',
+    productName: '澶氶�氶亾閲囬泦鏉�',
+    version: 'V1.0',
+    status: MesPdProjectStatusEnum.ARCHIVED,
+    designerName: '闄堝伐',
+    reviewerName: '鏉庝富绠�',
+    planStartDate: ts(200),
+    planEndDate: ts(170),
+    actualFinishDate: ts(165),
+    remark: '鍒濅唬鐗堟湰褰掓。',
+    createTime: ts(205),
+  },
+  {
+    id: 6,
+    code: 'DT-2026-0006',
+    name: '鏅鸿兘娓╂帶妯$粍 V1.1 璁捐',
+    productCode: 'PRD-TC-100',
+    productName: '鏅鸿兘娓╂帶妯$粍',
+    version: 'V1.1',
+    status: MesPdProjectStatusEnum.APPROVED,
+    designerName: '寮犲伐',
+    reviewerName: '鏉庝富绠�',
+    planStartDate: ts(10),
+    planEndDate: ts(-20),
+    remark: '鏁g儹浼樺寲鏀圭増锛屽緟褰掓。',
+    createTime: ts(12),
+  },
+];
+
+export const pdMockArchives: MesPdArchiveApi.Archive[] = [
+  {
+    id: 1,
+    code: 'AR-2025-0012',
+    projectId: 3,
+    projectCode: 'DT-2025-0012',
+    projectName: '澶氶�氶亾閲囬泦鏉� V1.2 璁捐',
+    productCode: 'PRD-SC-050',
+    productName: '澶氶�氶亾閲囬泦鏉�',
+    version: 'V1.2',
+    archiveStatus: 1,
+    archiveTime: ts(38),
+    archiverName: '闄堝伐',
+    documentCount: 3,
+    bomCount: 1,
+    storagePath: '/archive/2026/PRD-SC-050/V1.2',
+    remark: 'V1.2 閲忎骇鐗堟湰瀹屾暣褰掓。',
+    createTime: ts(38),
+  },
+  {
+    id: 2,
+    code: 'AR-2024-0008',
+    projectId: 5,
+    projectCode: 'DT-2024-0008',
+    projectName: '澶氶�氶亾閲囬泦鏉� V1.0 璁捐',
+    productCode: 'PRD-SC-050',
+    productName: '澶氶�氶亾閲囬泦鏉�',
+    version: 'V1.0',
+    archiveStatus: 1,
+    archiveTime: ts(165),
+    archiverName: '闄堝伐',
+    documentCount: 4,
+    bomCount: 8,
+    storagePath: '/archive/2024/PRD-SC-050/V1.0',
+    remark: '鍒濅唬鐗堟湰褰掓。',
+    createTime: ts(165),
+  },
+];
+
+export const pdMockDocuments: MesPdDocumentApi.Document[] = [
+  {
+    id: 1,
+    projectId: 1,
+    documentType: MesPdDocumentTypeEnum.SCHEME,
+    name: '娓╂帶鏂规璇存槑',
+    version: 'V1.0',
+    fileName: 'scheme-v1.pdf',
+    fileUrl: 'https://example.com/scheme-v1.pdf',
+    uploaderName: '寮犲伐',
+    createTime: ts(25),
+  },
+  {
+    id: 2,
+    projectId: 1,
+    documentType: MesPdDocumentTypeEnum.DRAWING,
+    name: '涓绘帶鏉垮師鐞嗗浘',
+    version: 'V1.0',
+    fileName: 'main-sch.pdf',
+    fileUrl: 'https://example.com/main-sch.pdf',
+    uploaderName: '寮犲伐',
+    createTime: ts(22),
+  },
+  {
+    id: 3,
+    projectId: 2,
+    documentType: MesPdDocumentTypeEnum.DRAWING,
+    name: '缃戝叧澶栧3宸ョ▼鍥�',
+    version: 'V2.1',
+    fileName: 'gw-enclosure.dwg',
+    fileUrl: 'https://example.com/gw-enclosure.dwg',
+    uploaderName: '鐜嬪伐',
+    createTime: ts(12),
+  },
+  {
+    id: 4,
+    projectId: 3,
+    documentType: MesPdDocumentTypeEnum.OTHER,
+    name: '褰掓。妫�鏌ユ竻鍗�',
+    version: 'V1.2',
+    fileName: 'archive-checklist.xlsx',
+    uploaderName: '闄堝伐',
+    createTime: ts(39),
+  },
+  {
+    id: 5,
+    projectId: 6,
+    documentType: MesPdDocumentTypeEnum.DRAWING,
+    name: '鏁g儹浼樺寲 PCB 鍥�',
+    version: 'V1.1',
+    fileName: 'thermal-pcb-v1.1.pdf',
+    fileUrl: 'https://example.com/thermal-pcb.pdf',
+    uploaderName: '寮犲伐',
+    createTime: ts(8),
+  },
+  {
+    id: 6,
+    projectId: 1,
+    documentType: MesPdDocumentTypeEnum.BOM,
+    name: 'BOM 娓呭崟',
+    version: 'V1.0',
+    fileName: 'bom-v1.0.xlsx',
+    fileUrl: 'https://example.com/bom-v1.0.xlsx',
+    uploaderName: '寮犲伐',
+    createTime: ts(20),
+  },
+  {
+    id: 7,
+    projectId: 3,
+    documentType: MesPdDocumentTypeEnum.BOM,
+    name: 'BOM 娓呭崟',
+    version: 'V1.2',
+    fileName: 'bom-v1.2.xlsx',
+    fileUrl: 'https://example.com/bom-v1.2.xlsx',
+    uploaderName: '闄堝伐',
+    createTime: ts(48),
+  },
+];
+
+export const pdMockBomLines: MesPdBomApi.BomLine[] = [
+  {
+    id: 1,
+    projectId: 1,
+    itemCode: 'MAT-001',
+    itemName: 'STM32 涓绘帶鑺墖',
+    specification: 'LQFP-64',
+    unitMeasureName: 'PCS',
+    quantity: 1,
+    createTime: ts(20),
+  },
+  {
+    id: 2,
+    projectId: 1,
+    itemCode: 'MAT-018',
+    itemName: 'NTC 鐑晱鐢甸樆',
+    specification: '10K B3950',
+    unitMeasureName: 'PCS',
+    quantity: 2,
+    createTime: ts(20),
+  },
+  {
+    id: 3,
+    projectId: 3,
+    itemCode: 'MAT-102',
+    itemName: '杩愮畻鏀惧ぇ鍣�',
+    specification: 'SOP-8',
+    unitMeasureName: 'PCS',
+    quantity: 4,
+    createTime: ts(50),
+  },
+];
+
+export const pdMockDrawingReviews: MesPdDrawingReviewApi.DrawingReview[] = [
+  {
+    id: 1,
+    code: 'DR-2026-0001',
+    projectId: 2,
+    projectCode: 'DT-2026-0002',
+    projectName: '宸ヤ笟缃戝叧 V2.1 璁捐',
+    documentId: 3,
+    documentName: '缃戝叧澶栧3宸ョ▼鍥�',
+    documentVersion: 'V2.1',
+    reviewerName: '璧电粡鐞�',
+    status: MesPdDrawingReviewStatusEnum.PENDING,
+    createTime: ts(10),
+  },
+  {
+    id: 2,
+    code: 'DR-2026-0002',
+    projectId: 6,
+    projectCode: 'DT-2026-0006',
+    projectName: '鏅鸿兘娓╂帶妯$粍 V1.1 璁捐',
+    documentId: 5,
+    documentName: '鏁g儹浼樺寲 PCB 鍥�',
+    documentVersion: 'V1.1',
+    reviewerName: '鏉庝富绠�',
+    status: MesPdDrawingReviewStatusEnum.APPROVED,
+    reviewTime: ts(6),
+    createTime: ts(7),
+  },
+];
+
+export const pdMockVersionChanges: MesPdVersionChangeApi.VersionChange[] = [
+  {
+    id: 1,
+    code: 'VC-2025-0001',
+    projectId: 3,
+    projectCode: 'DT-2025-0012',
+    projectName: '澶氶�氶亾閲囬泦鏉� V1.2 璁捐',
+    changeType: MesPdVersionChangeTypeEnum.MINOR,
+    fromVersion: 'V1.0',
+    toVersion: 'V1.2',
+    applicantName: '闄堝伐',
+    changeReason: '閫氶亾鏁版墿灞曚笌绮惧害鎻愬崌',
+    changeContent: '鐢� 4 閫氶亾鍗囩骇涓� 8 閫氶亾锛屾洿鎹㈤珮绮惧害 ADC',
+    createTime: ts(92),
+  },
+  {
+    id: 2,
+    code: 'VC-2026-0001',
+    projectId: 6,
+    projectCode: 'DT-2026-0006',
+    projectName: '鏅鸿兘娓╂帶妯$粍 V1.1 璁捐',
+    changeType: MesPdVersionChangeTypeEnum.PATCH,
+    fromVersion: 'V1.0',
+    toVersion: 'V1.1',
+    applicantName: '寮犲伐',
+    changeReason: '浼樺寲鏁g儹甯冨眬',
+    changeContent: '璋冩暣 NTC 鎽嗘斁浣嶇疆锛屾洿鏂� PCB 涓濆嵃',
+    createTime: ts(8),
+  },
+];
+
+export const pdMockWorkflows: Record<number, MesPdWorkflowApi.WorkflowNode[]> = {
+  1: [
+    { id: 11, projectId: 1, nodeName: '鍙戣捣浠诲姟', operatorName: '寮犲伐', status: 1, operateTime: ts(35) },
+    { id: 12, projectId: 1, nodeName: '鍙戝竷浠诲姟', operatorName: '寮犲伐', status: 1, operateTime: ts(34) },
+    { id: 13, projectId: 1, nodeName: '涓婁紶璧勬枡', operatorName: '寮犲伐', status: 2, remark: '杩涜涓�' },
+    { id: 14, projectId: 1, nodeName: '璧勬枡瀹℃牳', status: 0 },
+    { id: 15, projectId: 1, nodeName: '褰掓。', status: 0 },
+  ],
+  2: [
+    { id: 21, projectId: 2, nodeName: '鍙戣捣浠诲姟', operatorName: '鐜嬪伐', status: 1, operateTime: ts(25) },
+    { id: 22, projectId: 2, nodeName: '鍙戝竷浠诲姟', operatorName: '鐜嬪伐', status: 1, operateTime: ts(24) },
+    { id: 23, projectId: 2, nodeName: '涓婁紶璧勬枡', operatorName: '鐜嬪伐', status: 1, operateTime: ts(12) },
+    { id: 24, projectId: 2, nodeName: '璧勬枡瀹℃牳', operatorName: '璧电粡鐞�', status: 2, remark: '寰呭鏍�' },
+    { id: 25, projectId: 2, nodeName: '褰掓。', status: 0 },
+  ],
+  3: [
+    { id: 31, projectId: 3, nodeName: '鍙戣捣浠诲姟', operatorName: '闄堝伐', status: 1, operateTime: ts(95) },
+    { id: 32, projectId: 3, nodeName: '鍙戝竷浠诲姟', operatorName: '闄堝伐', status: 1, operateTime: ts(94) },
+    { id: 33, projectId: 3, nodeName: '涓婁紶璧勬枡', operatorName: '闄堝伐', status: 1, operateTime: ts(50) },
+    { id: 34, projectId: 3, nodeName: '璧勬枡瀹℃牳', operatorName: '鏉庝富绠�', status: 1, operateTime: ts(42) },
+    { id: 35, projectId: 3, nodeName: '褰掓。', operatorName: '闄堝伐', status: 1, operateTime: ts(38) },
+  ],
+  4: [
+    { id: 41, projectId: 4, nodeName: '鍙戣捣浠诲姟', operatorName: '鍒樺伐', status: 1, operateTime: ts(3) },
+    { id: 42, projectId: 4, nodeName: '鍙戝竷浠诲姟', status: 0 },
+    { id: 43, projectId: 4, nodeName: '涓婁紶璧勬枡', status: 0 },
+    { id: 44, projectId: 4, nodeName: '璧勬枡瀹℃牳', status: 0 },
+    { id: 45, projectId: 4, nodeName: '褰掓。', status: 0 },
+  ],
+  5: [
+    { id: 51, projectId: 5, nodeName: '鍙戣捣浠诲姟', operatorName: '闄堝伐', status: 1, operateTime: ts(205) },
+    { id: 52, projectId: 5, nodeName: '鍙戝竷浠诲姟', operatorName: '闄堝伐', status: 1, operateTime: ts(200) },
+    { id: 53, projectId: 5, nodeName: '涓婁紶璧勬枡', operatorName: '闄堝伐', status: 1, operateTime: ts(170) },
+    { id: 54, projectId: 5, nodeName: '璧勬枡瀹℃牳', operatorName: '鏉庝富绠�', status: 1, operateTime: ts(168) },
+    { id: 55, projectId: 5, nodeName: '褰掓。', operatorName: '闄堝伐', status: 1, operateTime: ts(165) },
+  ],
+  6: [
+    { id: 61, projectId: 6, nodeName: '鍙戣捣浠诲姟', operatorName: '寮犲伐', status: 1, operateTime: ts(12) },
+    { id: 62, projectId: 6, nodeName: '鍙戝竷浠诲姟', operatorName: '寮犲伐', status: 1, operateTime: ts(11) },
+    { id: 63, projectId: 6, nodeName: '涓婁紶璧勬枡', operatorName: '寮犲伐', status: 1, operateTime: ts(8) },
+    { id: 64, projectId: 6, nodeName: '璧勬枡瀹℃牳', operatorName: '鏉庝富绠�', status: 1, operateTime: ts(6) },
+    { id: 65, projectId: 6, nodeName: '褰掓。', status: 2, remark: '寰呭綊妗�' },
+  ],
+};
+
+export const pdMockSeq = {
+  project: 7,
+  archive: 3,
+  task: 4,
+  document: 8,
+  bom: 4,
+  drawingReview: 3,
+  versionChange: 3,
+};
+
+/** @deprecated 璁捐浠诲姟鍗抽」鐩紝瀛愪换鍔″垎鍙戝凡搴熷純 */
+export const pdMockTasks = [];
diff --git a/src/api/mes/pd/mock/store.ts b/src/api/mes/pd/mock/store.ts
new file mode 100644
index 0000000..0519016
--- /dev/null
+++ b/src/api/mes/pd/mock/store.ts
@@ -0,0 +1,554 @@
+import type { MesPdArchiveApi } from '#/api/mes/pd/archive';
+import type { MesPdBomApi } from '#/api/mes/pd/bom';
+import type { MesPdDocumentApi } from '#/api/mes/pd/document';
+import type { MesPdDrawingReviewApi } from '#/api/mes/pd/drawing-review';
+import type { MesPdProjectApi } from '#/api/mes/pd/project';
+import type { MesPdTaskApi } from '#/api/mes/pd/task';
+import type { MesPdVersionChangeApi } from '#/api/mes/pd/version-change';
+import type { MesPdWorkflowApi } from '#/api/mes/pd/workflow';
+
+import { MesPdDrawingReviewStatusEnum, MesPdDocumentTypeEnum, MesPdProjectStatusEnum } from '#/packages/constants/src';
+
+import {
+  pdMockArchives,
+  pdMockBomLines,
+  pdMockDocuments,
+  pdMockDrawingReviews,
+  pdMockProjects,
+  pdMockSeq,
+  pdMockTasks,
+  pdMockVersionChanges,
+  pdMockWorkflows,
+} from './seed';
+import { mockDelay, mockNextCode, mockPage } from './utils';
+
+function matchLike(value: unknown, keyword?: string): boolean {
+  if (!keyword) {
+    return true;
+  }
+  return String(value ?? '')
+    .toLowerCase()
+    .includes(String(keyword).toLowerCase());
+}
+
+function syncProjectRef(project: MesPdProjectApi.Project) {
+  const ref = {
+    projectCode: project.code,
+    projectName: project.name,
+    productCode: project.productCode,
+    productName: project.productName,
+    version: project.version,
+  };
+  pdMockTasks.forEach((task) => {
+    if (task.projectId === project.id) {
+      Object.assign(task, ref);
+    }
+  });
+  pdMockDrawingReviews.forEach((item) => {
+    if (item.projectId === project.id) {
+      Object.assign(item, ref);
+    }
+  });
+  pdMockVersionChanges.forEach((item) => {
+    if (item.projectId === project.id) {
+      Object.assign(item, ref);
+    }
+  });
+}
+
+const WORKFLOW_STEPS = [
+  '鍙戣捣浠诲姟',
+  '鍙戝竷浠诲姟',
+  '涓婁紶璧勬枡',
+  '璧勬枡瀹℃牳',
+  '褰掓。',
+] as const;
+
+function initWorkflow(projectId: number, designerName?: string) {
+  pdMockWorkflows[projectId] = WORKFLOW_STEPS.map((nodeName, index) => ({
+    id: projectId * 10 + index,
+    projectId,
+    nodeName,
+    operatorName: index === 0 ? designerName : undefined,
+    status: index === 0 ? 1 : 0,
+    operateTime: index === 0 ? Date.now() : undefined,
+  }));
+}
+
+function advanceWorkflow(projectId: number, completedStep: string, operatorName?: string) {
+  const nodes = pdMockWorkflows[projectId];
+  if (!nodes) {
+    return;
+  }
+  const completedIndex = nodes.findIndex((n) => n.nodeName === completedStep);
+  if (completedIndex >= 0) {
+    nodes[completedIndex]!.status = 1;
+    nodes[completedIndex]!.operateTime = Date.now();
+    if (operatorName) {
+      nodes[completedIndex]!.operatorName = operatorName;
+    }
+  }
+  const next = nodes[completedIndex + 1];
+  if (next) {
+    next.status = 2;
+    if (operatorName) {
+      next.operatorName = operatorName;
+    }
+  }
+}
+
+function createDocumentReviews(projectId: number, project: MesPdProjectApi.Project) {
+  const docs = pdMockDocuments.filter((d) => d.projectId === projectId);
+  for (const doc of docs) {
+    const exists = pdMockDrawingReviews.some((r) => r.documentId === doc.id);
+    if (exists) {
+      continue;
+    }
+    const reviewId = pdMockSeq.drawingReview++;
+    pdMockDrawingReviews.push({
+      id: reviewId,
+      code: mockNextCode('DR-2026', reviewId),
+      projectId,
+      projectCode: project.code,
+      projectName: project.name,
+      documentId: doc.id,
+      documentName: doc.name,
+      documentVersion: doc.version,
+      reviewerName: project.reviewerName,
+      status: MesPdDrawingReviewStatusEnum.PENDING,
+      createTime: Date.now(),
+    });
+  }
+}
+
+export const pdMockStore = {
+  // ---------- 璁捐椤圭洰 ----------
+  getProjectPage(params: MesPdProjectApi.PageParams) {
+    return mockDelay(
+      mockPage(pdMockProjects, params, (item) => {
+        return (
+          matchLike(item.code, params.code) &&
+          matchLike(item.name, params.name) &&
+          matchLike(item.productCode, params.productCode) &&
+          (params.status === undefined || item.status === params.status)
+        );
+      }),
+    );
+  },
+
+  getProject(id: number) {
+    const project = pdMockProjects.find((item) => item.id === id);
+    if (!project) {
+      return Promise.reject(new Error('璁捐浠诲姟涓嶅瓨鍦�'));
+    }
+    return mockDelay({ ...project });
+  },
+
+  createProject(data: MesPdProjectApi.Project) {
+    const id = pdMockSeq.project++;
+    const project: MesPdProjectApi.Project = {
+      ...data,
+      id,
+      code: data.code || mockNextCode('DT-2026', id),
+      status: MesPdProjectStatusEnum.DRAFT,
+      createTime: Date.now(),
+    };
+    pdMockProjects.unshift(project);
+    initWorkflow(id, project.designerName);
+    return mockDelay(id);
+  },
+
+  updateProject(data: MesPdProjectApi.Project) {
+    const index = pdMockProjects.findIndex((item) => item.id === data.id);
+    if (index < 0) {
+      return Promise.reject(new Error('璁捐浠诲姟涓嶅瓨鍦�'));
+    }
+    pdMockProjects[index] = { ...pdMockProjects[index], ...data };
+    syncProjectRef(pdMockProjects[index]!);
+    return mockDelay(undefined);
+  },
+
+  deleteProject(id: number) {
+    const index = pdMockProjects.findIndex((item) => item.id === id);
+    if (index >= 0) {
+      pdMockProjects.splice(index, 1);
+    }
+    return mockDelay(undefined);
+  },
+
+  publishProject(id: number) {
+    const project = pdMockProjects.find((item) => item.id === id);
+    if (!project || project.status !== MesPdProjectStatusEnum.DRAFT) {
+      return Promise.reject(new Error('浠呰崏绋跨姸鎬佸彲鍙戝竷'));
+    }
+    project.status = MesPdProjectStatusEnum.DESIGNING;
+    advanceWorkflow(id, '鍙戣捣浠诲姟', project.designerName);
+    advanceWorkflow(id, '鍙戝竷浠诲姟', project.designerName);
+    return mockDelay(undefined);
+  },
+
+  submitProjectReview(id: number) {
+    const project = pdMockProjects.find((item) => item.id === id);
+    if (!project || project.status !== MesPdProjectStatusEnum.DESIGNING) {
+      return Promise.reject(new Error('璇峰厛鍙戝竷浠诲姟骞朵笂浼犺祫鏂�'));
+    }
+    const docCount = pdMockDocuments.filter((d) => d.projectId === id).length;
+    if (docCount === 0) {
+      return Promise.reject(new Error('璇峰厛涓婁紶璁捐璧勬枡'));
+    }
+    project.status = MesPdProjectStatusEnum.REVIEWING;
+    createDocumentReviews(id, project);
+    advanceWorkflow(id, '涓婁紶璧勬枡', project.designerName);
+    advanceWorkflow(id, '璧勬枡瀹℃牳', project.reviewerName);
+    return mockDelay(undefined);
+  },
+
+  approveProject(id: number) {
+    const project = pdMockProjects.find((item) => item.id === id);
+    if (!project || project.status !== MesPdProjectStatusEnum.REVIEWING) {
+      return Promise.reject(new Error('褰撳墠鐘舵�佷笉鍙鏍搁�氳繃'));
+    }
+    const pending = pdMockDrawingReviews.filter(
+      (r) =>
+        r.projectId === id &&
+        r.status === MesPdDrawingReviewStatusEnum.PENDING,
+    );
+    if (pending.length > 0) {
+      return Promise.reject(new Error('灏氭湁璧勬枡鏈畬鎴愬鏍�'));
+    }
+    project.status = MesPdProjectStatusEnum.APPROVED;
+    const nodes = pdMockWorkflows[id];
+    const reviewNode = nodes?.find((n) => n.nodeName === '璧勬枡瀹℃牳');
+    if (reviewNode) {
+      reviewNode.status = 1;
+      reviewNode.operateTime = Date.now();
+    }
+    const archiveNode = nodes?.find((n) => n.nodeName === '褰掓。');
+    if (archiveNode) {
+      archiveNode.status = 2;
+    }
+    return mockDelay(undefined);
+  },
+
+  archiveProject(id: number) {
+    const project = pdMockProjects.find((item) => item.id === id);
+    if (!project) {
+      return Promise.reject(new Error('璁捐浠诲姟涓嶅瓨鍦�'));
+    }
+    if (project.status !== MesPdProjectStatusEnum.APPROVED) {
+      return Promise.reject(new Error('浠呭鏍搁�氳繃鐨勪换鍔″彲褰掓。'));
+    }
+    project.status = MesPdProjectStatusEnum.ARCHIVED;
+    project.actualFinishDate = Date.now();
+    const archiveId = pdMockSeq.archive++;
+    pdMockArchives.unshift({
+      id: archiveId,
+      code: mockNextCode('AR-2026', archiveId),
+      projectId: project.id,
+      projectCode: project.code,
+      projectName: project.name,
+      productCode: project.productCode,
+      productName: project.productName,
+      version: project.version,
+      archiveStatus: 1,
+      archiveTime: Date.now(),
+      archiverName: project.designerName,
+      documentCount: pdMockDocuments.filter((d) => d.projectId === id).length,
+      bomCount: pdMockDocuments.filter(
+        (d) =>
+          d.projectId === id &&
+          d.documentType === MesPdDocumentTypeEnum.BOM,
+      ).length,
+      storagePath: `/archive/2026/${project.productCode}/${project.version}`,
+      createTime: Date.now(),
+    });
+    advanceWorkflow(id, '褰掓。', project.designerName);
+    const archiveNode = pdMockWorkflows[id]?.find((n) => n.nodeName === '褰掓。');
+    if (archiveNode) {
+      archiveNode.status = 1;
+      archiveNode.operateTime = Date.now();
+    }
+    return mockDelay(undefined);
+  },
+
+  // ---------- 褰掓。鍙拌处 ----------
+  getArchivePage(params: MesPdArchiveApi.PageParams) {
+    return mockDelay(
+      mockPage(pdMockArchives, params, (item) => {
+        return (
+          matchLike(item.code, params.code) &&
+          matchLike(item.projectCode, params.projectCode) &&
+          matchLike(item.productCode, params.productCode) &&
+          (params.archiveStatus === undefined ||
+            item.archiveStatus === params.archiveStatus)
+        );
+      }),
+    );
+  },
+
+  getArchive(id: number) {
+    const archive = pdMockArchives.find((item) => item.id === id);
+    if (!archive) {
+      return Promise.reject(new Error('褰掓。璁板綍涓嶅瓨鍦�'));
+    }
+    return mockDelay({ ...archive });
+  },
+
+  // ---------- 浠诲姟 ----------
+  getTaskPage(params: MesPdTaskApi.PageParams) {
+    return mockDelay(
+      mockPage(pdMockTasks, params, (item) => {
+        return (
+          item.projectId === params.projectId &&
+          matchLike(item.code, params.code) &&
+          matchLike(item.taskName, params.taskName) &&
+          (params.status === undefined || item.status === params.status)
+        );
+      }),
+    );
+  },
+
+  getTask(id: number) {
+    const task = pdMockTasks.find((item) => item.id === id);
+    if (!task) {
+      return Promise.reject(new Error('璁捐浠诲姟涓嶅瓨鍦�'));
+    }
+    return mockDelay({ ...task });
+  },
+
+  createTask(data: MesPdTaskApi.Task) {
+    const id = pdMockSeq.task++;
+    const project = pdMockProjects.find((p) => p.id === data.projectId);
+    const task: MesPdTaskApi.Task = {
+      ...data,
+      id,
+      code: data.code || mockNextCode('TK-2026', id),
+      projectCode: project?.code,
+      projectName: project?.name,
+      status: data.status ?? 0,
+      createTime: Date.now(),
+    };
+    pdMockTasks.unshift(task);
+    return mockDelay(id);
+  },
+
+  updateTask(data: MesPdTaskApi.Task) {
+    const index = pdMockTasks.findIndex((item) => item.id === data.id);
+    if (index < 0) {
+      return Promise.reject(new Error('璁捐浠诲姟涓嶅瓨鍦�'));
+    }
+    pdMockTasks[index] = { ...pdMockTasks[index], ...data };
+    return mockDelay(undefined);
+  },
+
+  deleteTask(id: number) {
+    const index = pdMockTasks.findIndex((item) => item.id === id);
+    if (index >= 0) {
+      pdMockTasks.splice(index, 1);
+    }
+    return mockDelay(undefined);
+  },
+
+  dispatchTask(data: MesPdTaskApi.DispatchParams) {
+    const task = pdMockTasks.find((item) => item.id === data.id);
+    if (task) {
+      task.status = 1;
+      task.planFinishDate = data.planFinishDate;
+      task.remark = data.remark;
+    }
+    return mockDelay(undefined);
+  },
+
+  completeTask(id: number) {
+    const task = pdMockTasks.find((item) => item.id === id);
+    if (task) {
+      task.status = 3;
+      task.actualFinishDate = Date.now();
+    }
+    return mockDelay(undefined);
+  },
+
+  // ---------- 璧勬枡 ----------
+  getDocumentPage(params: MesPdDocumentApi.PageParams) {
+    return mockDelay(
+      mockPage(pdMockDocuments, params, (item) => {
+        return (
+          item.projectId === params.projectId &&
+          (params.documentType === undefined ||
+            item.documentType === params.documentType) &&
+          (params.version === undefined || item.version === params.version) &&
+          matchLike(item.name, params.name)
+        );
+      }),
+    );
+  },
+
+  getDocument(id: number) {
+    const doc = pdMockDocuments.find((item) => item.id === id);
+    if (!doc) {
+      return Promise.reject(new Error('璁捐璧勬枡涓嶅瓨鍦�'));
+    }
+    return mockDelay({ ...doc });
+  },
+
+  createDocument(data: MesPdDocumentApi.Document) {
+    const id = pdMockSeq.document++;
+    pdMockDocuments.unshift({
+      ...data,
+      id,
+      uploaderName: data.uploaderName ?? '褰撳墠鐢ㄦ埛',
+      createTime: Date.now(),
+    });
+    return mockDelay(id);
+  },
+
+  updateDocument(data: MesPdDocumentApi.Document) {
+    const index = pdMockDocuments.findIndex((item) => item.id === data.id);
+    if (index < 0) {
+      return Promise.reject(new Error('璁捐璧勬枡涓嶅瓨鍦�'));
+    }
+    pdMockDocuments[index] = { ...pdMockDocuments[index], ...data };
+    return mockDelay(undefined);
+  },
+
+  deleteDocument(id: number) {
+    const index = pdMockDocuments.findIndex((item) => item.id === id);
+    if (index >= 0) {
+      pdMockDocuments.splice(index, 1);
+    }
+    return mockDelay(undefined);
+  },
+
+  // ---------- BOM ----------
+  getBomPage(params: MesPdBomApi.PageParams) {
+    return mockDelay(
+      mockPage(pdMockBomLines, params, (item) => {
+        return (
+          item.projectId === params.projectId &&
+          matchLike(item.itemCode, params.itemCode) &&
+          matchLike(item.itemName, params.itemName)
+        );
+      }),
+    );
+  },
+
+  createBomLine(data: MesPdBomApi.BomLine) {
+    const id = pdMockSeq.bom++;
+    pdMockBomLines.unshift({ ...data, id, createTime: Date.now() });
+    return mockDelay(id);
+  },
+
+  updateBomLine(data: MesPdBomApi.BomLine) {
+    const index = pdMockBomLines.findIndex((item) => item.id === data.id);
+    if (index < 0) {
+      return Promise.reject(new Error('BOM 琛屼笉瀛樺湪'));
+    }
+    pdMockBomLines[index] = { ...pdMockBomLines[index], ...data };
+    return mockDelay(undefined);
+  },
+
+  deleteBomLine(id: number) {
+    const index = pdMockBomLines.findIndex((item) => item.id === id);
+    if (index >= 0) {
+      pdMockBomLines.splice(index, 1);
+    }
+    return mockDelay(undefined);
+  },
+
+  // ---------- 鍥剧焊瀹℃牳 ----------
+  getDrawingReviewPage(params: MesPdDrawingReviewApi.PageParams) {
+    return mockDelay(
+      mockPage(pdMockDrawingReviews, params, (item) => {
+        return (
+          item.projectId === params.projectId &&
+          matchLike(item.code, params.code) &&
+          (params.status === undefined || item.status === params.status)
+        );
+      }),
+    );
+  },
+
+  getDrawingReview(id: number) {
+    const row = pdMockDrawingReviews.find((item) => item.id === id);
+    if (!row) {
+      return Promise.reject(new Error('鍥剧焊瀹℃牳鍗曚笉瀛樺湪'));
+    }
+    return mockDelay({ ...row });
+  },
+
+  reviewDrawing(data: MesPdDrawingReviewApi.ReviewParams) {
+    const row = pdMockDrawingReviews.find((item) => item.id === data.id);
+    if (row) {
+      row.status = data.status;
+      row.reviewComment = data.reviewComment;
+      row.reviewTime = Date.now();
+    }
+    return mockDelay(undefined);
+  },
+  getVersionChangePage(params: MesPdVersionChangeApi.PageParams) {
+    return mockDelay(
+      mockPage(pdMockVersionChanges, params, (item) => {
+        return (
+          item.projectId === params.projectId &&
+          matchLike(item.code, params.code) &&
+          (params.changeType === undefined ||
+            item.changeType === params.changeType)
+        );
+      }),
+    );
+  },
+
+  getVersionChange(id: number) {
+    const row = pdMockVersionChanges.find((item) => item.id === id);
+    if (!row) {
+      return Promise.reject(new Error('鐗堟湰鍙樻洿鍗曚笉瀛樺湪'));
+    }
+    return mockDelay({ ...row });
+  },
+
+  createVersionChange(data: MesPdVersionChangeApi.VersionChange) {
+    const id = pdMockSeq.versionChange++;
+    const project = pdMockProjects.find((p) => p.id === data.projectId);
+    pdMockVersionChanges.unshift({
+      ...data,
+      id,
+      code: data.code || mockNextCode('VC-2026', id),
+      projectCode: project?.code,
+      projectName: project?.name,
+      applicantName: data.applicantName ?? '褰撳墠鐢ㄦ埛',
+      createTime: Date.now(),
+    });
+    return mockDelay(id);
+  },
+
+  updateVersionChange(data: MesPdVersionChangeApi.VersionChange) {
+    const index = pdMockVersionChanges.findIndex((item) => item.id === data.id);
+    if (index < 0) {
+      return Promise.reject(new Error('鐗堟湰鍙樻洿鍗曚笉瀛樺湪'));
+    }
+    pdMockVersionChanges[index] = { ...pdMockVersionChanges[index], ...data };
+    return mockDelay(undefined);
+  },
+
+  deleteVersionChange(id: number) {
+    const index = pdMockVersionChanges.findIndex((item) => item.id === id);
+    if (index >= 0) {
+      pdMockVersionChanges.splice(index, 1);
+    }
+    return mockDelay(undefined);
+  },
+
+  approveVersionChange(id: number) {
+    const row = pdMockVersionChanges.find((item) => item.id === id);
+    const project = pdMockProjects.find((p) => p.id === row?.projectId);
+    if (row && project && row.toVersion) {
+      project.version = row.toVersion;
+    }
+    return mockDelay(undefined);
+  },
+
+  // ---------- 娴佺▼ ----------
+  getWorkflowList(projectId: number) {
+    return mockDelay(pdMockWorkflows[projectId] ?? []);
+  },
+};
diff --git a/src/api/mes/pd/mock/utils.ts b/src/api/mes/pd/mock/utils.ts
new file mode 100644
index 0000000..70e85c5
--- /dev/null
+++ b/src/api/mes/pd/mock/utils.ts
@@ -0,0 +1,28 @@
+import type { PageParam, PageResult } from '#/packages/effects/request/src';
+
+/** 妯℃嫙鎺ュ彛寤惰繜 */
+export function mockDelay<T>(data: T, ms = 120): Promise<T> {
+  return new Promise((resolve) => {
+    setTimeout(() => resolve(data), ms);
+  });
+}
+
+/** 鍒嗛〉涓庣畝鍗曞瓧娈佃繃婊� */
+export function mockPage<T extends Record<string, unknown>>(
+  list: T[],
+  params: PageParam & Record<string, unknown>,
+  filter?: (item: T) => boolean,
+): PageResult<T> {
+  const filtered = filter ? list.filter(filter) : [...list];
+  const pageNo = params.pageNo ?? 1;
+  const pageSize = params.pageSize ?? 10;
+  const start = (pageNo - 1) * pageSize;
+  return {
+    list: filtered.slice(start, start + pageSize),
+    total: filtered.length,
+  };
+}
+
+export function mockNextCode(prefix: string, seq: number): string {
+  return `${prefix}-${String(seq).padStart(4, '0')}`;
+}
diff --git a/src/api/mes/pd/project/index.ts b/src/api/mes/pd/project/index.ts
new file mode 100644
index 0000000..a637d27
--- /dev/null
+++ b/src/api/mes/pd/project/index.ts
@@ -0,0 +1,119 @@
+import type { PageParam, PageResult } from '#/packages/effects/request/src';
+
+import { requestClient } from '#/api/request';
+
+import { isPdMockEnabled } from '../mock/enabled';
+import { pdMockStore } from '../mock/store';
+
+export namespace MesPdProjectApi {
+  /** 浜у搧璁捐椤圭洰 */
+  export interface Project {
+    id?: number;
+    code?: string;
+    name?: string;
+    productCode?: string;
+    productName?: string;
+    version?: string;
+    status?: number;
+    designerId?: number;
+    designerName?: string;
+    reviewerId?: number;
+    reviewerName?: string;
+    planStartDate?: number;
+    planEndDate?: number;
+    actualFinishDate?: number;
+    remark?: string;
+    createTime?: number;
+  }
+
+  export interface PageParams extends PageParam {
+    code?: string;
+    name?: string;
+    productCode?: string;
+    status?: number;
+    designerId?: number;
+  }
+}
+
+export function getPdProjectPage(params: MesPdProjectApi.PageParams) {
+  if (isPdMockEnabled()) {
+    return pdMockStore.getProjectPage(params);
+  }
+  return requestClient.get<PageResult<MesPdProjectApi.Project>>(
+    '/mes/pd/project/page',
+    { params },
+  );
+}
+
+export function getPdProject(id: number) {
+  if (isPdMockEnabled()) {
+    return pdMockStore.getProject(id);
+  }
+  return requestClient.get<MesPdProjectApi.Project>(
+    `/mes/pd/project/get?id=${id}`,
+  );
+}
+
+export function createPdProject(data: MesPdProjectApi.Project) {
+  if (isPdMockEnabled()) {
+    return pdMockStore.createProject(data);
+  }
+  return requestClient.post<number>('/mes/pd/project/create', data);
+}
+
+export function updatePdProject(data: MesPdProjectApi.Project) {
+  if (isPdMockEnabled()) {
+    return pdMockStore.updateProject(data);
+  }
+  return requestClient.put('/mes/pd/project/update', data);
+}
+
+export function deletePdProject(id: number) {
+  if (isPdMockEnabled()) {
+    return pdMockStore.deleteProject(id);
+  }
+  return requestClient.delete(`/mes/pd/project/delete?id=${id}`);
+}
+
+export function exportPdProject(params: MesPdProjectApi.PageParams) {
+  if (isPdMockEnabled()) {
+    return Promise.resolve(new Blob());
+  }
+  return requestClient.download('/mes/pd/project/export-excel', { params });
+}
+
+export function submitPdProjectReview(id: number) {
+  if (isPdMockEnabled()) {
+    return pdMockStore.submitProjectReview(id);
+  }
+  return requestClient.put('/mes/pd/project/submit-review', undefined, {
+    params: { id },
+  });
+}
+
+export function publishPdProject(id: number) {
+  if (isPdMockEnabled()) {
+    return pdMockStore.publishProject(id);
+  }
+  return requestClient.put('/mes/pd/project/publish', undefined, {
+    params: { id },
+  });
+}
+
+export function approvePdProject(id: number) {
+  if (isPdMockEnabled()) {
+    return pdMockStore.approveProject(id);
+  }
+  return requestClient.put('/mes/pd/project/approve', undefined, {
+    params: { id },
+  });
+}
+
+export function archivePdProject(id: number) {
+  if (isPdMockEnabled()) {
+    return pdMockStore.archiveProject(id);
+  }
+  return requestClient.put('/mes/pd/project/archive', undefined, {
+    params: { id },
+  });
+}
diff --git a/src/api/mes/pd/task/index.ts b/src/api/mes/pd/task/index.ts
new file mode 100644
index 0000000..ebd7c8c
--- /dev/null
+++ b/src/api/mes/pd/task/index.ts
@@ -0,0 +1,98 @@
+import type { PageParam, PageResult } from '#/packages/effects/request/src';
+
+import { requestClient } from '#/api/request';
+
+import { isPdMockEnabled } from '../mock/enabled';
+import { pdMockStore } from '../mock/store';
+
+export namespace MesPdTaskApi {
+  /** 浜у搧璁捐浠诲姟 */
+  export interface Task {
+    id?: number;
+    code?: string;
+    projectId?: number;
+    projectCode?: string;
+    projectName?: string;
+    taskName?: string;
+    taskType?: number;
+    assigneeId?: number;
+    assigneeName?: string;
+    assignerId?: number;
+    assignerName?: string;
+    priority?: number;
+    status?: number;
+    planFinishDate?: number;
+    actualFinishDate?: number;
+    remark?: string;
+    createTime?: number;
+  }
+
+  export interface PageParams extends PageParam {
+    code?: string;
+    projectId?: number;
+    taskName?: string;
+    assigneeId?: number;
+    status?: number;
+  }
+
+  export interface DispatchParams {
+    id: number;
+    assigneeId: number;
+    planFinishDate?: number;
+    remark?: string;
+  }
+}
+
+export function getPdTaskPage(params: MesPdTaskApi.PageParams) {
+  if (isPdMockEnabled()) {
+    return pdMockStore.getTaskPage(params);
+  }
+  return requestClient.get<PageResult<MesPdTaskApi.Task>>(
+    '/mes/pd/task/page',
+    { params },
+  );
+}
+
+export function getPdTask(id: number) {
+  if (isPdMockEnabled()) {
+    return pdMockStore.getTask(id);
+  }
+  return requestClient.get<MesPdTaskApi.Task>(`/mes/pd/task/get?id=${id}`);
+}
+
+export function createPdTask(data: MesPdTaskApi.Task) {
+  if (isPdMockEnabled()) {
+    return pdMockStore.createTask(data);
+  }
+  return requestClient.post<number>('/mes/pd/task/create', data);
+}
+
+export function updatePdTask(data: MesPdTaskApi.Task) {
+  if (isPdMockEnabled()) {
+    return pdMockStore.updateTask(data);
+  }
+  return requestClient.put('/mes/pd/task/update', data);
+}
+
+export function deletePdTask(id: number) {
+  if (isPdMockEnabled()) {
+    return pdMockStore.deleteTask(id);
+  }
+  return requestClient.delete(`/mes/pd/task/delete?id=${id}`);
+}
+
+export function dispatchPdTask(data: MesPdTaskApi.DispatchParams) {
+  if (isPdMockEnabled()) {
+    return pdMockStore.dispatchTask(data);
+  }
+  return requestClient.put('/mes/pd/task/dispatch', data);
+}
+
+export function completePdTask(id: number) {
+  if (isPdMockEnabled()) {
+    return pdMockStore.completeTask(id);
+  }
+  return requestClient.put('/mes/pd/task/complete', undefined, {
+    params: { id },
+  });
+}
diff --git a/src/api/mes/pd/version-change/index.ts b/src/api/mes/pd/version-change/index.ts
new file mode 100644
index 0000000..7e521fb
--- /dev/null
+++ b/src/api/mes/pd/version-change/index.ts
@@ -0,0 +1,83 @@
+import type { PageParam, PageResult } from '#/packages/effects/request/src';
+
+import { requestClient } from '#/api/request';
+
+import { isPdMockEnabled } from '../mock/enabled';
+import { pdMockStore } from '../mock/store';
+
+export namespace MesPdVersionChangeApi {
+  /** 鐗堟湰鍙樻洿 */
+  export interface VersionChange {
+    id?: number;
+    code?: string;
+    projectId?: number;
+    projectCode?: string;
+    projectName?: string;
+    changeType?: number;
+    fromVersion?: string;
+    toVersion?: string;
+    changeReason?: string;
+    changeContent?: string;
+    applicantId?: number;
+    applicantName?: string;
+    remark?: string;
+    createTime?: number;
+  }
+
+  export interface PageParams extends PageParam {
+    code?: string;
+    projectId?: number;
+    changeType?: number;
+  }
+}
+
+export function getPdVersionChangePage(
+  params: MesPdVersionChangeApi.PageParams,
+) {
+  if (isPdMockEnabled()) {
+    return pdMockStore.getVersionChangePage(params);
+  }
+  return requestClient.get<PageResult<MesPdVersionChangeApi.VersionChange>>(
+    '/mes/pd/version-change/page',
+    { params },
+  );
+}
+
+export function getPdVersionChange(id: number) {
+  if (isPdMockEnabled()) {
+    return pdMockStore.getVersionChange(id);
+  }
+  return requestClient.get<MesPdVersionChangeApi.VersionChange>(
+    `/mes/pd/version-change/get?id=${id}`,
+  );
+}
+
+export function createPdVersionChange(data: MesPdVersionChangeApi.VersionChange) {
+  if (isPdMockEnabled()) {
+    return pdMockStore.createVersionChange(data);
+  }
+  return requestClient.post<number>('/mes/pd/version-change/create', data);
+}
+
+export function updatePdVersionChange(data: MesPdVersionChangeApi.VersionChange) {
+  if (isPdMockEnabled()) {
+    return pdMockStore.updateVersionChange(data);
+  }
+  return requestClient.put('/mes/pd/version-change/update', data);
+}
+
+export function deletePdVersionChange(id: number) {
+  if (isPdMockEnabled()) {
+    return pdMockStore.deleteVersionChange(id);
+  }
+  return requestClient.delete(`/mes/pd/version-change/delete?id=${id}`);
+}
+
+export function approvePdVersionChange(id: number) {
+  if (isPdMockEnabled()) {
+    return pdMockStore.approveVersionChange(id);
+  }
+  return requestClient.put('/mes/pd/version-change/approve', undefined, {
+    params: { id },
+  });
+}
diff --git a/src/api/mes/pd/workflow/index.ts b/src/api/mes/pd/workflow/index.ts
new file mode 100644
index 0000000..bb22e8b
--- /dev/null
+++ b/src/api/mes/pd/workflow/index.ts
@@ -0,0 +1,28 @@
+import { requestClient } from '#/api/request';
+
+import { isPdMockEnabled } from '../mock/enabled';
+import { pdMockStore } from '../mock/store';
+
+export namespace MesPdWorkflowApi {
+  /** 璁捐娴佺▼鑺傜偣 */
+  export interface WorkflowNode {
+    id?: number;
+    projectId?: number;
+    nodeName?: string;
+    nodeType?: string;
+    operatorName?: string;
+    status?: number;
+    operateTime?: number;
+    remark?: string;
+  }
+}
+
+export function getPdWorkflowList(projectId: number) {
+  if (isPdMockEnabled()) {
+    return pdMockStore.getWorkflowList(projectId);
+  }
+  return requestClient.get<MesPdWorkflowApi.WorkflowNode[]>(
+    '/mes/pd/workflow/list',
+    { params: { projectId } },
+  );
+}
diff --git a/src/api/mes/pro/processdesign/parameter/index.ts b/src/api/mes/pro/processdesign/parameter/index.ts
new file mode 100644
index 0000000..391964a
--- /dev/null
+++ b/src/api/mes/pro/processdesign/parameter/index.ts
@@ -0,0 +1,64 @@
+import type { PageParam, PageResult } from '../../../../../packages/effects/request/src';
+
+import { requestClient } from '#/api/request';
+
+export namespace MesPdParameterApi {
+  /** 宸ヨ壓鍙傛暟 */
+  export interface Parameter {
+    id?: number;
+    code?: string;
+    name?: string;
+    type?: number;
+    unit?: string;
+    requiredFlag?: boolean;
+    dictType?: string;
+    apiUrl?: string;
+    remark?: string;
+    createTime?: Date;
+  }
+}
+
+/** 鏌ヨ宸ヨ壓鍙傛暟鍒嗛〉 */
+export function getParameterPage(params: PageParam) {
+  return requestClient.get<PageResult<MesPdParameterApi.Parameter>>(
+    '/mes/pro/process-design/parameter/page',
+    { params },
+  );
+}
+
+/** 鏌ヨ宸ヨ壓鍙傛暟绮剧畝鍒楄〃 */
+export function getParameterSimpleList() {
+  return requestClient.get<MesPdParameterApi.Parameter[]>(
+    '/mes/pro/process-design/parameter/simple-list',
+  );
+}
+
+/** 鏌ヨ宸ヨ壓鍙傛暟璇︽儏 */
+export function getParameter(id: number) {
+  return requestClient.get<MesPdParameterApi.Parameter>(
+    `/mes/pro/process-design/parameter/get?id=${id}`,
+  );
+}
+
+/** 鏂板宸ヨ壓鍙傛暟 */
+export function createParameter(data: MesPdParameterApi.Parameter) {
+  return requestClient.post(
+    '/mes/pro/process-design/parameter/create',
+    data,
+  );
+}
+
+/** 淇敼宸ヨ壓鍙傛暟 */
+export function updateParameter(data: MesPdParameterApi.Parameter) {
+  return requestClient.put(
+    '/mes/pro/process-design/parameter/update',
+    data,
+  );
+}
+
+/** 鍒犻櫎宸ヨ壓鍙傛暟 */
+export function deleteParameter(id: number) {
+  return requestClient.delete(
+    `/mes/pro/process-design/parameter/delete?id=${id}`,
+  );
+}
diff --git a/src/api/mes/pro/processdesign/template/index.ts b/src/api/mes/pro/processdesign/template/index.ts
new file mode 100644
index 0000000..ab05241
--- /dev/null
+++ b/src/api/mes/pro/processdesign/template/index.ts
@@ -0,0 +1,53 @@
+import type { PageParam, PageResult } from '../../../../../packages/effects/request/src';
+
+import { requestClient } from '#/api/request';
+
+export namespace MesPdTemplateApi {
+  /** 宸ヨ壓鍙傛暟妯℃澘 */
+  export interface Template {
+    id?: number;
+    name?: string;
+    type?: number;
+    parameterIds?: number[];
+    createUser?: string;
+    createTime?: Date;
+  }
+}
+
+/** 鏌ヨ宸ヨ壓鍙傛暟妯℃澘鍒嗛〉 */
+export function getTemplatePage(params: PageParam) {
+  return requestClient.get<PageResult<MesPdTemplateApi.Template>>(
+    '/mes/pro/process-design/template/page',
+    { params },
+  );
+}
+
+/** 鏌ヨ宸ヨ壓鍙傛暟妯℃澘璇︽儏 */
+export function getTemplate(id: number) {
+  return requestClient.get<MesPdTemplateApi.Template>(
+    `/mes/pro/process-design/template/get?id=${id}`,
+  );
+}
+
+/** 鏂板宸ヨ壓鍙傛暟妯℃澘 */
+export function createTemplate(data: MesPdTemplateApi.Template) {
+  return requestClient.post(
+    '/mes/pro/process-design/template/create',
+    data,
+  );
+}
+
+/** 淇敼宸ヨ壓鍙傛暟妯℃澘 */
+export function updateTemplate(data: MesPdTemplateApi.Template) {
+  return requestClient.put(
+    '/mes/pro/process-design/template/update',
+    data,
+  );
+}
+
+/** 鍒犻櫎宸ヨ壓鍙傛暟妯℃澘 */
+export function deleteTemplate(id: number) {
+  return requestClient.delete(
+    `/mes/pro/process-design/template/delete?id=${id}`,
+  );
+}
diff --git a/src/packages/constants/src/biz-mes-enum.ts b/src/packages/constants/src/biz-mes-enum.ts
index 9394395..5419d1b 100644
--- a/src/packages/constants/src/biz-mes-enum.ts
+++ b/src/packages/constants/src/biz-mes-enum.ts
@@ -133,6 +133,9 @@
   MD_VENDOR_CODE: 'MD_VENDOR_CODE',
   MD_WORKSTATION_CODE: 'MD_WORKSTATION_CODE',
   MD_WORKSHOP_CODE: 'MD_WORKSHOP_CODE',
+  PD_PROJECT_CODE: 'PD_PROJECT_CODE',
+  PD_TASK_CODE: 'PD_TASK_CODE',
+  PD_VERSION_CODE: 'PD_VERSION_CODE',
   PRO_CARD_CODE: 'PRO_CARD_CODE',
   PRO_FEEDBACK_CODE: 'PRO_FEEDBACK_CODE',
   PRO_PROCESS_CODE: 'PRO_PROCESS_CODE',
@@ -492,6 +495,49 @@
   RIGHT: 2, // 鍙宠ˉ榻�
 } as const;
 
+/** MES 浜у搧璁捐椤圭洰鐘舵�佹灇涓� */
+export const MesPdProjectStatusEnum = {
+  DRAFT: MesOrderStatusConstants.DRAFT,
+  DESIGNING: 10,
+  REVIEWING: MesOrderStatusConstants.APPROVING,
+  APPROVED: MesOrderStatusConstants.APPROVED,
+  ARCHIVED: 20,
+  CANCELLED: MesOrderStatusConstants.CANCELLED,
+} as const;
+
+/** MES 浜у搧璁捐浠诲姟鐘舵�佹灇涓� */
+export const MesPdTaskStatusEnum = {
+  PENDING: 0,
+  ASSIGNED: 1,
+  IN_PROGRESS: 2,
+  COMPLETED: 3,
+  CANCELLED: 4,
+} as const;
+
+/** MES 浜у搧璁捐鍥剧焊瀹℃牳鐘舵�佹灇涓� */
+export const MesPdDrawingReviewStatusEnum = {
+  PENDING: MesOrderStatusConstants.APPROVING,
+  APPROVED: MesOrderStatusConstants.APPROVED,
+  REJECTED: 5,
+} as const;
+
+/** MES 浜у搧璁捐鐗堟湰鍙樻洿绫诲瀷鏋氫妇 */
+export const MesPdVersionChangeTypeEnum = {
+  MAJOR: 1,
+  MINOR: 2,
+  PATCH: 3,
+} as const;
+
+/** MES 浜у搧璁捐璧勬枡绫诲瀷鏋氫妇 */
+export const MesPdDocumentTypeEnum = {
+  SCHEME: 1,
+  DRAWING: 2,
+  MODEL: 3,
+  PRODUCT_DESIGN: 4,
+  OTHER: 5,
+  BOM: 6,
+} as const;
+
 /** MES 鏉$爜鏍煎紡鏋氫妇 */
 export enum BarcodeFormatEnum {
   QR_CODE = 1,
@@ -508,6 +554,22 @@
   [BarcodeFormatEnum.UPC_A]: 'UPC_A',
 };
 
+/** MES 宸ヨ壓鍙傛暟绫诲瀷鏋氫妇 */
+export const MesPdParamTypeEnum = {
+  NUMERIC: 1, // 鏁板�兼牸寮�
+  TEXT: 2, // 鏂囨湰鏍煎紡
+  SELECT: 3, // 涓嬫媺閫夐」
+  TIME: 4, // 鏃堕棿鏍煎紡
+  DATE: 5, // 鏃ユ湡鏍煎紡
+} as const;
+
+/** MES 宸ヨ壓妯℃澘绫诲瀷鏋氫妇 */
+export const MesPdTemplateTypeEnum = {
+  PRODUCTION: 1, // 鐢熶骇瑕佹眰
+  SELF_CHECK: 2, // 鑷瑕佹眰
+  PATROL_CHECK: 3, // 宸℃瑕佹眰
+} as const;
+
 /** MES 鏉$爜涓氬姟绫诲瀷鏋氫妇 */
 export enum BarcodeBizTypeEnum {
   WAREHOUSE = 102,
diff --git a/src/packages/constants/src/dict-enum.ts b/src/packages/constants/src/dict-enum.ts
index af6fc57..f0a2d99 100644
--- a/src/packages/constants/src/dict-enum.ts
+++ b/src/packages/constants/src/dict-enum.ts
@@ -228,6 +228,12 @@
   MES_PRO_TASK_STATUS: 'mes_pro_task_status', // MES 鐢熶骇浠诲姟鐘舵��
   MES_PRO_FEEDBACK_STATUS: 'mes_pro_feedback_status', // MES 鐢熶骇鎶ュ伐鐘舵��
   MES_PRO_FEEDBACK_TYPE: 'mes_pro_feedback_type', // MES 鐢熶骇鎶ュ伐绫诲瀷
+  MES_PD_PROJECT_STATUS: 'mes_pd_project_status', // MES 浜у搧璁捐椤圭洰鐘舵��
+  MES_PD_TASK_STATUS: 'mes_pd_task_status', // MES 浜у搧璁捐浠诲姟鐘舵��
+  MES_PD_DRAWING_REVIEW_STATUS: 'mes_pd_drawing_review_status', // MES 鍥剧焊瀹℃牳鐘舵��
+  MES_PD_VERSION_CHANGE_TYPE: 'mes_pd_version_change_type', // MES 鐗堟湰鍙樻洿绫诲瀷
+  MES_PD_DOCUMENT_TYPE: 'mes_pd_document_type', // MES 璁捐璧勬枡绫诲瀷
+  MES_PD_ARCHIVE_STATUS: 'mes_pd_archive_status', // MES 璁捐褰掓。鐘舵��
   MES_PRO_FEEDBACK_CHANNEL: 'mes_pro_feedback_channel', // MES 鐢熶骇鎶ュ伐閫斿緞
   MES_PRO_ANDON_STATUS: 'mes_pro_andon_status', // MES 瀹夌伅澶勭疆鐘舵��
   MES_PRO_ANDON_LEVEL: 'mes_pro_andon_level', // MES 瀹夌伅绾у埆
@@ -268,6 +274,8 @@
   MES_WM_STOCK_TAKING_PLAN_PARAM_TYPE: 'mes_wm_stock_taking_plan_param_type', // MES 鐩樼偣鏂规鍙傛暟绫诲瀷
   MES_WM_TRANSFER_STATUS: 'mes_wm_transfer_status', // MES 杞Щ鍗曠姸鎬�
   MES_WM_TRANSFER_TYPE: 'mes_wm_transfer_type', // MES 杞Щ鍗曠被鍨�
+  MES_PD_PARAM_TYPE: 'mes_pd_param_type', // MES 宸ヨ壓鍙傛暟绫诲瀷
+  MES_PD_TEMPLATE_TYPE: 'mes_pd_template_type', // MES 宸ヨ壓妯℃澘绫诲瀷
 } as const;
 
 /** ========== WMS - 浠撳偍绠$悊妯″潡 ========== */
diff --git a/src/router/routes/modules/mes.ts b/src/router/routes/modules/mes.ts
index 2b62534..91aec0a 100644
--- a/src/router/routes/modules/mes.ts
+++ b/src/router/routes/modules/mes.ts
@@ -47,6 +47,15 @@
         },
         component: () => import('#/views/mes/pro/task/edit/index.vue'),
       },
+      {
+        path: 'pd/project/detail/:id',
+        name: 'MesPdProjectDetail',
+        meta: {
+          title: '璁捐浠诲姟璇︽儏',
+          activePath: '/mes/pd/project',
+        },
+        component: () => import('#/views/mes/pd/project/detail/index.vue'),
+      },
     ],
   },
 ];
diff --git a/src/views/mes/md/client/data.ts b/src/views/mes/md/client/data.ts
index ebc61a8..45df0e7 100644
--- a/src/views/mes/md/client/data.ts
+++ b/src/views/mes/md/client/data.ts
@@ -1,389 +1,10 @@
-import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
+import type { VbenFormSchema } from '#/adapter/form';
 import type { VxeTableGridOptions } from '#/adapter/vxe-table';
 import type { MesMdClientApi } from '#/api/mes/md/client';
 
-import { h } from 'vue';
+import { DICT_TYPE } from '#/packages/constants/src';
 
-import {
-  CommonStatusEnum,
-  DICT_TYPE,
-  MesAutoCodeRuleCode,
-} from '../../../../packages/constants/src';
-import { getDictOptions } from '../../../../packages/effects/hooks/src';
-
-import { Button } from 'ant-design-vue';
-
-import { z } from '#/adapter/form';
-import { generateAutoCode } from '#/api/mes/md/autocode/record';
-
-/** 琛ㄥ崟绫诲瀷 */
-export type FormType = 'create' | 'detail' | 'update';
-
-/** 鏂板/淇敼瀹㈡埛鐨勮〃鍗� */
-export function useFormSchema(
-  formType: FormType,
-  formApi?: VbenFormApi,
-): VbenFormSchema[] {
-  return [
-    {
-      fieldName: 'id',
-      component: 'Input',
-      dependencies: {
-        triggerFields: [''],
-        show: () => false,
-      },
-    },
-    {
-      fieldName: 'code',
-      label: '瀹㈡埛缂栫爜',
-      component: 'Input',
-      componentProps: {
-        placeholder: '璇疯緭鍏ュ鎴风紪鐮�',
-      },
-      rules: 'required',
-      suffix:
-        formType === 'detail'
-          ? undefined
-          : () =>
-              h(
-                Button,
-                {
-                  type: 'default',
-                  onClick: async () => {
-                    const code = await generateAutoCode(
-                      MesAutoCodeRuleCode.MD_CLIENT_CODE,
-                    );
-                    await formApi?.setFieldValue('code', code);
-                  },
-                },
-                { default: () => '鑷姩鐢熸垚' },
-              ),
-    },
-    {
-      fieldName: 'name',
-      label: '瀹㈡埛鍚嶇О',
-      component: 'Input',
-      componentProps: {
-        placeholder: '璇疯緭鍏ュ鎴峰悕绉�',
-      },
-      rules: z.string().min(1, '瀹㈡埛鍚嶇О涓嶈兘涓虹┖').max(100),
-    },
-    {
-      fieldName: 'nickname',
-      label: '瀹㈡埛绠�绉�',
-      component: 'Input',
-      componentProps: {
-        placeholder: '璇疯緭鍏ュ鎴风畝绉�',
-      },
-    },
-    {
-      fieldName: 'englishName',
-      label: '瀹㈡埛鑻辨枃鍚嶇О',
-      component: 'Input',
-      componentProps: {
-        placeholder: '璇疯緭鍏ュ鎴疯嫳鏂囧悕绉�',
-      },
-    },
-    {
-      fieldName: 'type',
-      label: '瀹㈡埛绫诲瀷',
-      component: 'Select',
-      componentProps: {
-        allowClear: true,
-        options: getDictOptions(DICT_TYPE.MES_CLIENT_TYPE, 'number'),
-        placeholder: '璇烽�夋嫨瀹㈡埛绫诲瀷',
-      },
-      rules: 'selectRequired',
-    },
-    {
-      fieldName: 'description',
-      label: '瀹㈡埛绠�浠�',
-      component: 'Textarea',
-      formItemClass: 'col-span-3',
-      componentProps: {
-        placeholder: '璇疯緭鍏ュ鎴风畝浠�',
-        rows: 2,
-      },
-    },
-    {
-      fieldName: 'address',
-      label: '瀹㈡埛鍦板潃',
-      component: 'Textarea',
-      formItemClass: 'col-span-3',
-      componentProps: {
-        placeholder: '璇疯緭鍏ュ鎴峰湴鍧�',
-        rows: 2,
-      },
-    },
-    {
-      fieldName: 'website',
-      label: '瀹㈡埛瀹樼綉鍦板潃',
-      component: 'Input',
-      componentProps: {
-        placeholder: '璇疯緭鍏ュ鎴峰畼缃戝湴鍧�',
-      },
-    },
-    {
-      fieldName: 'email',
-      label: '瀹㈡埛閭鍦板潃',
-      component: 'Input',
-      componentProps: {
-        placeholder: '璇疯緭鍏ュ鎴烽偖绠卞湴鍧�',
-      },
-      rules: z.string().email('閭鏍煎紡涓嶆纭�').or(z.literal('')).optional(),
-    },
-    {
-      fieldName: 'telephone',
-      label: '瀹㈡埛鐢佃瘽',
-      component: 'Input',
-      componentProps: {
-        placeholder: '璇疯緭鍏ュ鎴风數璇�',
-      },
-    },
-    {
-      fieldName: 'logo',
-      label: '瀹㈡埛 LOGO',
-      component: 'Input',
-      componentProps: {
-        placeholder: '璇疯緭鍏ュ鎴� LOGO 鍦板潃',
-      },
-    },
-    {
-      fieldName: 'contact1Name',
-      label: '鑱旂郴浜�1',
-      component: 'Input',
-      componentProps: {
-        placeholder: '璇疯緭鍏ヨ仈绯讳汉1',
-      },
-    },
-    {
-      fieldName: 'contact1Telephone',
-      label: '鑱旂郴浜�1鐢佃瘽',
-      component: 'Input',
-      componentProps: {
-        placeholder: '璇疯緭鍏ヨ仈绯讳汉1鐢佃瘽',
-      },
-    },
-    {
-      fieldName: 'contact1Email',
-      label: '鑱旂郴浜�1閭',
-      component: 'Input',
-      componentProps: {
-        placeholder: '璇疯緭鍏ヨ仈绯讳汉1閭',
-      },
-      rules: z.string().email('閭鏍煎紡涓嶆纭�').or(z.literal('')).optional(),
-    },
-    {
-      fieldName: 'contact2Name',
-      label: '鑱旂郴浜�2',
-      component: 'Input',
-      componentProps: {
-        placeholder: '璇疯緭鍏ヨ仈绯讳汉2',
-      },
-    },
-    {
-      fieldName: 'contact2Telephone',
-      label: '鑱旂郴浜�2鐢佃瘽',
-      component: 'Input',
-      componentProps: {
-        placeholder: '璇疯緭鍏ヨ仈绯讳汉2鐢佃瘽',
-      },
-    },
-    {
-      fieldName: 'contact2Email',
-      label: '鑱旂郴浜�2閭',
-      component: 'Input',
-      componentProps: {
-        placeholder: '璇疯緭鍏ヨ仈绯讳汉2閭',
-      },
-      rules: z.string().email('閭鏍煎紡涓嶆纭�').or(z.literal('')).optional(),
-    },
-    {
-      fieldName: 'creditCode',
-      label: '绀句細淇$敤浠g爜',
-      component: 'Input',
-      componentProps: {
-        placeholder: '璇疯緭鍏ョ粺涓�绀句細淇$敤浠g爜',
-      },
-    },
-    {
-      fieldName: 'status',
-      label: '鐘舵��',
-      component: 'RadioGroup',
-      componentProps: {
-        buttonStyle: 'solid',
-        optionType: 'button',
-        options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
-      },
-      rules: z.number().default(CommonStatusEnum.ENABLE),
-    },
-    {
-      fieldName: 'remark',
-      label: '澶囨敞',
-      component: 'Textarea',
-      formItemClass: 'col-span-3',
-      componentProps: {
-        placeholder: '璇疯緭鍏ュ娉�',
-        rows: 3,
-      },
-    },
-  ];
-}
-
-/** 瀵煎叆瀹㈡埛鐨勮〃鍗� */
-export function useImportFormSchema(): VbenFormSchema[] {
-  return [
-    {
-      fieldName: 'file',
-      label: '瀹㈡埛鏁版嵁',
-      component: 'Upload',
-      rules: 'required',
-      help: '浠呭厑璁稿鍏� xls銆亁lsx 鏍煎紡鏂囦欢',
-    },
-    {
-      fieldName: 'updateSupport',
-      label: '鏄惁瑕嗙洊',
-      component: 'Switch',
-      componentProps: {
-        checkedChildren: '鏄�',
-        unCheckedChildren: '鍚�',
-      },
-      rules: z.boolean().default(false),
-      help: '鏄惁鏇存柊宸茬粡瀛樺湪鐨勫鎴锋暟鎹�',
-    },
-  ];
-}
-
-/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
-export function useGridFormSchema(): VbenFormSchema[] {
-  return [
-    {
-      fieldName: 'code',
-      label: '瀹㈡埛缂栫爜',
-      component: 'Input',
-      componentProps: {
-        allowClear: true,
-        placeholder: '璇疯緭鍏ュ鎴风紪鐮�',
-      },
-    },
-    {
-      fieldName: 'name',
-      label: '瀹㈡埛鍚嶇О',
-      component: 'Input',
-      componentProps: {
-        allowClear: true,
-        placeholder: '璇疯緭鍏ュ鎴峰悕绉�',
-      },
-    },
-    {
-      fieldName: 'nickname',
-      label: '瀹㈡埛绠�绉�',
-      component: 'Input',
-      componentProps: {
-        allowClear: true,
-        placeholder: '璇疯緭鍏ュ鎴风畝绉�',
-      },
-    },
-    {
-      fieldName: 'englishName',
-      label: '鑻辨枃鍚嶇О',
-      component: 'Input',
-      componentProps: {
-        allowClear: true,
-        placeholder: '璇疯緭鍏ュ鎴疯嫳鏂囧悕绉�',
-      },
-    },
-    {
-      fieldName: 'type',
-      label: '瀹㈡埛绫诲瀷',
-      component: 'Select',
-      componentProps: {
-        allowClear: true,
-        options: getDictOptions(DICT_TYPE.MES_CLIENT_TYPE, 'number'),
-        placeholder: '璇烽�夋嫨瀹㈡埛绫诲瀷',
-      },
-    },
-    {
-      fieldName: 'status',
-      label: '鐘舵��',
-      component: 'Select',
-      componentProps: {
-        allowClear: true,
-        options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
-        placeholder: '璇烽�夋嫨鐘舵��',
-      },
-    },
-  ];
-}
-
-/** 鍒楄〃鐨勫瓧娈� */
-export function useGridColumns(): VxeTableGridOptions<MesMdClientApi.Client>['columns'] {
-  return [
-    {
-      field: 'code',
-      title: '瀹㈡埛缂栫爜',
-      minWidth: 150,
-      slots: { default: 'code' },
-    },
-    {
-      field: 'name',
-      title: '瀹㈡埛鍚嶇О',
-      minWidth: 160,
-    },
-    {
-      field: 'nickname',
-      title: '瀹㈡埛绠�绉�',
-      minWidth: 130,
-    },
-    {
-      field: 'type',
-      title: '瀹㈡埛绫诲瀷',
-      width: 120,
-      cellRender: {
-        name: 'CellDict',
-        props: { type: DICT_TYPE.MES_CLIENT_TYPE },
-      },
-    },
-    {
-      field: 'telephone',
-      title: '瀹㈡埛鐢佃瘽',
-      minWidth: 140,
-    },
-    {
-      field: 'contact1Name',
-      title: '鑱旂郴浜�1',
-      width: 120,
-    },
-    {
-      field: 'contact1Telephone',
-      title: '鑱旂郴浜�1鐢佃瘽',
-      minWidth: 140,
-    },
-    {
-      field: 'status',
-      title: '鐘舵��',
-      width: 120,
-      cellRender: {
-        name: 'CellDict',
-        props: { type: DICT_TYPE.COMMON_STATUS },
-      },
-    },
-    {
-      field: 'createTime',
-      title: '鍒涘缓鏃堕棿',
-      width: 180,
-      formatter: 'formatDateTime',
-    },
-    {
-      title: '鎿嶄綔',
-      width: 160,
-      fixed: 'right',
-      slots: { default: 'actions' },
-    },
-  ];
-}
-
-/** 瀹㈡埛閫夋嫨寮圭獥鐨勬悳绱㈣〃鍗� */
+/** 瀹㈡埛閫夋嫨寮圭獥鎼滅储琛ㄥ崟 */
 export function useClientSelectGridFormSchema(): VbenFormSchema[] {
   return [
     {
@@ -425,7 +46,7 @@
   ];
 }
 
-/** 瀹㈡埛閫夋嫨寮圭獥鐨勫瓧娈� */
+/** 瀹㈡埛閫夋嫨寮圭獥鍒楄〃瀛楁 */
 export function useClientSelectGridColumns(
   multiple = true,
 ): VxeTableGridOptions<MesMdClientApi.Client>['columns'] {
diff --git a/src/views/mes/md/client/index.vue b/src/views/mes/md/client/index.vue
deleted file mode 100644
index 084f73f..0000000
--- a/src/views/mes/md/client/index.vue
+++ /dev/null
@@ -1,168 +0,0 @@
-<script lang="ts" setup>
-import type { VxeTableGridOptions } from '#/adapter/vxe-table';
-import type { MesMdClientApi } from '#/api/mes/md/client';
-
-import { Page, useVbenModal } from '../../../../packages/effects/common-ui/src';
-import { downloadFileFromBlobPart } from '../../../../packages/utils/src';
-
-import { Button, message } from 'ant-design-vue';
-
-import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
-import { deleteClient, exportClient, getClientPage } from '#/api/mes/md/client';
-import { $t } from '#/locales';
-
-import { useGridColumns, useGridFormSchema } from './data';
-import Form from './modules/form.vue';
-import ImportForm from './modules/import-form.vue';
-
-const [FormModal, formModalApi] = useVbenModal({
-  connectedComponent: Form,
-  destroyOnClose: true,
-});
-
-const [ImportModal, importModalApi] = useVbenModal({
-  connectedComponent: ImportForm,
-  destroyOnClose: true,
-});
-
-/** 鍒锋柊琛ㄦ牸 */
-function handleRefresh() {
-  gridApi.query();
-}
-
-/** 鍒涘缓瀹㈡埛 */
-function handleCreate() {
-  formModalApi.setData({ formType: 'create' }).open();
-}
-
-/** 鏌ョ湅瀹㈡埛 */
-function handleDetail(row: MesMdClientApi.Client) {
-  formModalApi.setData({ id: row.id, formType: 'detail' }).open();
-}
-
-/** 缂栬緫瀹㈡埛 */
-function handleEdit(row: MesMdClientApi.Client) {
-  formModalApi.setData({ id: row.id, formType: 'update' }).open();
-}
-
-/** 鍒犻櫎瀹㈡埛 */
-async function handleDelete(row: MesMdClientApi.Client) {
-  const hideLoading = message.loading({
-    content: $t('ui.actionMessage.deleting', [row.name]),
-    duration: 0,
-  });
-  try {
-    await deleteClient(row.id!);
-    message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
-    handleRefresh();
-  } finally {
-    hideLoading();
-  }
-}
-
-/** 瀵煎嚭琛ㄦ牸 */
-async function handleExport() {
-  const data = await exportClient(await gridApi.formApi.getValues());
-  downloadFileFromBlobPart({ fileName: '瀹㈡埛.xls', source: data });
-}
-
-/** 瀵煎叆瀹㈡埛 */
-function handleImport() {
-  importModalApi.open();
-}
-
-const [Grid, gridApi] = useVbenVxeGrid({
-  formOptions: {
-    schema: useGridFormSchema(),
-  },
-  gridOptions: {
-    columns: useGridColumns(),
-    height: 'auto',
-    keepSource: true,
-    proxyConfig: {
-      ajax: {
-        query: async ({ page }, formValues) => {
-          return await getClientPage({
-            pageNo: page.currentPage,
-            pageSize: page.pageSize,
-            ...formValues,
-          });
-        },
-      },
-    },
-    rowConfig: {
-      keyField: 'id',
-      isHover: true,
-    },
-    toolbarConfig: {
-      refresh: true,
-      search: true,
-    },
-  } as VxeTableGridOptions<MesMdClientApi.Client>,
-});
-</script>
-
-<template>
-  <Page auto-content-height><FormModal @success="handleRefresh" />
-    <ImportModal @success="handleRefresh" />
-
-    <Grid table-title="瀹㈡埛鍒楄〃">
-      <template #toolbar-tools>
-        <TableAction
-          :actions="[
-            {
-              label: $t('ui.actionTitle.create', ['瀹㈡埛']),
-              type: 'primary',
-              icon: ACTION_ICON.ADD,
-              auth: ['mes:md-client:create'],
-              onClick: handleCreate,
-            },
-            {
-              label: $t('ui.actionTitle.import', ['瀹㈡埛']),
-              type: 'primary',
-              icon: ACTION_ICON.UPLOAD,
-              auth: ['mes:md-client:import'],
-              onClick: handleImport,
-            },
-            {
-              label: $t('ui.actionTitle.export'),
-              type: 'primary',
-              icon: ACTION_ICON.DOWNLOAD,
-              auth: ['mes:md-client:export'],
-              onClick: handleExport,
-            },
-          ]"
-        />
-      </template>
-      <template #code="{ row }">
-        <Button type="link" @click="handleDetail(row)">
-          {{ row.code }}
-        </Button>
-      </template>
-      <template #actions="{ row }">
-        <TableAction
-          :actions="[
-            {
-              label: $t('common.edit'),
-              type: 'link',
-              icon: ACTION_ICON.EDIT,
-              auth: ['mes:md-client:update'],
-              onClick: handleEdit.bind(null, row),
-            },
-            {
-              label: $t('common.delete'),
-              type: 'link',
-              danger: true,
-              icon: ACTION_ICON.DELETE,
-              auth: ['mes:md-client:delete'],
-              popConfirm: {
-                title: $t('ui.actionMessage.deleteConfirm', [row.name]),
-                confirm: handleDelete.bind(null, row),
-              },
-            },
-          ]"
-        />
-      </template>
-    </Grid>
-  </Page>
-</template>
diff --git a/src/views/mes/md/client/modules/form.vue b/src/views/mes/md/client/modules/form.vue
deleted file mode 100644
index 56fe9ae..0000000
--- a/src/views/mes/md/client/modules/form.vue
+++ /dev/null
@@ -1,113 +0,0 @@
-<script lang="ts" setup>
-import type { FormType } from '../data';
-
-import type { MesMdClientApi } from '#/api/mes/md/client';
-
-import { computed, ref } from 'vue';
-
-import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
-
-import { message, Tabs } from 'ant-design-vue';
-
-import { useVbenForm } from '#/adapter/form';
-import { createClient, getClient, updateClient } from '#/api/mes/md/client';
-import { $t } from '#/locales';
-
-import { useFormSchema } from '../data';
-import ClientProductSalesLineList from './product-sales-line-list.vue';
-import ClientProductSalesList from './product-sales-list.vue';
-
-const emit = defineEmits(['success']);
-const formType = ref<FormType>('create'); // 琛ㄥ崟妯″紡
-const subTabsName = ref('productSalesLine'); // 褰撳墠瀛愯〃椤电
-const formData = ref<MesMdClientApi.Client>();
-
-const isDetail = computed(() => formType.value === 'detail'); // 鏄惁鏌ョ湅妯″紡
-const getTitle = computed(() => {
-  if (formType.value === 'detail') {
-    return '鏌ョ湅瀹㈡埛';
-  }
-  return formType.value === 'update' ? '淇敼瀹㈡埛' : '鏂板瀹㈡埛';
-});
-
-const [Form, formApi] = useVbenForm({
-  commonConfig: {
-    componentProps: {
-      class: 'w-full',
-    },
-    formItemClass: 'col-span-1',
-    labelWidth: 120,
-  },
-  wrapperClass: 'grid-cols-3',
-  layout: 'horizontal',
-  schema: [],
-  showDefaultActions: false,
-});
-
-const [Modal, modalApi] = useVbenModal({
-  async onConfirm() {
-    if (isDetail.value) {
-      await modalApi.close();
-      return;
-    }
-    const { valid } = await formApi.validate();
-    if (!valid) {
-      return;
-    }
-    modalApi.lock();
-    // 鎻愪氦琛ㄥ崟
-    const data = (await formApi.getValues()) as MesMdClientApi.Client;
-    try {
-      await (formData.value?.id ? updateClient(data) : createClient(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;
-    }
-    subTabsName.value = 'productSalesLine';
-    // 鍔犺浇鏁版嵁
-    const data = modalApi.getData<{ formType: FormType; id?: number }>();
-    formType.value = data.formType;
-    formApi.setState({ schema: useFormSchema(data.formType, formApi) });
-    formApi.setDisabled(formType.value === 'detail');
-    modalApi.setState({ showConfirmButton: formType.value !== 'detail' });
-    if (!data?.id) {
-      return;
-    }
-    modalApi.lock();
-    try {
-      formData.value = await getClient(data.id);
-      // 璁剧疆鍒� values
-      await formApi.setValues(formData.value);
-    } finally {
-      modalApi.unlock();
-    }
-  },
-});
-</script>
-
-<template>
-  <Modal :title="getTitle" class="w-4/5">
-    <Form class="mx-4" />
-    <Tabs
-      v-if="formType !== 'create' && formData?.id"
-      v-model:active-key="subTabsName"
-      class="mx-4 mt-4"
-    >
-      <Tabs.TabPane key="productSalesLine" tab="浜у搧娓呭崟">
-        <ClientProductSalesLineList :client-id="formData.id" />
-      </Tabs.TabPane>
-      <Tabs.TabPane key="productSales" tab="閿�鍞褰�">
-        <ClientProductSalesList :client-id="formData.id" />
-      </Tabs.TabPane>
-    </Tabs>
-  </Modal>
-</template>
diff --git a/src/views/mes/md/client/modules/import-form.vue b/src/views/mes/md/client/modules/import-form.vue
deleted file mode 100644
index 1b547c7..0000000
--- a/src/views/mes/md/client/modules/import-form.vue
+++ /dev/null
@@ -1,101 +0,0 @@
-<script lang="ts" setup>
-import type { FileType } from 'ant-design-vue/es/upload/interface';
-
-import type { MesMdClientApi } from '#/api/mes/md/client';
-
-import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
-import { downloadFileFromBlobPart } from '../../../../../packages/utils/src';
-
-import { Button, message, Upload } from 'ant-design-vue';
-
-import { useVbenForm } from '#/adapter/form';
-import { importClient, importClientTemplate } from '#/api/mes/md/client';
-import { $t } from '#/locales';
-
-import { useImportFormSchema } from '../data';
-
-const emit = defineEmits(['success']);
-
-const [Form, formApi] = useVbenForm({
-  commonConfig: {
-    componentProps: {
-      class: 'w-full',
-    },
-    formItemClass: 'col-span-2',
-    labelWidth: 120,
-  },
-  layout: 'horizontal',
-  schema: useImportFormSchema(),
-  showDefaultActions: false,
-});
-
-const [Modal, modalApi] = useVbenModal({
-  async onConfirm() {
-    const { valid } = await formApi.validate();
-    if (!valid) {
-      return;
-    }
-    modalApi.lock();
-    // 鎻愪氦琛ㄥ崟
-    const data = await formApi.getValues();
-    try {
-      const result = await importClient(data.file, data.updateSupport);
-      const importData = result as MesMdClientApi.ClientImportRespVO;
-      let text = `涓婁紶鎴愬姛鏁伴噺锛�${importData.createCodes?.length || 0};`;
-      for (const code of importData.createCodes || []) {
-        text += `< ${code} >`;
-      }
-      text += `鏇存柊鎴愬姛鏁伴噺锛�${importData.updateCodes?.length || 0};`;
-      for (const code of importData.updateCodes || []) {
-        text += `< ${code} >`;
-      }
-      text += `鏇存柊澶辫触鏁伴噺锛�${Object.keys(importData.failureCodes || {}).length};`;
-      for (const code in importData.failureCodes || {}) {
-        text += `< ${code}: ${importData.failureCodes?.[code]} >`;
-      }
-      message.info(text);
-      // 鍏抽棴骞舵彁绀�
-      await modalApi.close();
-      emit('success');
-      message.success($t('ui.actionMessage.operationSuccess'));
-    } finally {
-      modalApi.unlock();
-    }
-  },
-});
-
-/** 涓婁紶鍓� */
-function beforeUpload(file: FileType) {
-  formApi.setFieldValue('file', file);
-  return false;
-}
-
-/** 涓嬭浇妯℃澘 */
-async function handleDownload() {
-  const data = await importClientTemplate();
-  downloadFileFromBlobPart({ fileName: '瀹㈡埛瀵煎叆妯℃澘.xls', source: data });
-}
-</script>
-
-<template>
-  <Modal :title="$t('ui.actionTitle.import', ['瀹㈡埛'])" class="w-1/3">
-    <Form class="mx-4">
-      <template #file>
-        <div class="w-full">
-          <Upload
-            :before-upload="beforeUpload"
-            :max-count="1"
-            accept=".xls,.xlsx"
-          >
-            <Button type="primary">閫夋嫨 Excel 鏂囦欢</Button>
-          </Upload>
-        </div>
-      </template>
-    </Form>
-    <template #prepend-footer>
-      <div class="flex flex-auto items-center">
-        <Button @click="handleDownload">涓嬭浇瀵煎叆妯℃澘</Button>
-      </div>
-    </template>
-  </Modal>
-</template>
diff --git a/src/views/mes/md/client/modules/product-sales-line-list.vue b/src/views/mes/md/client/modules/product-sales-line-list.vue
deleted file mode 100644
index de8b427..0000000
--- a/src/views/mes/md/client/modules/product-sales-line-list.vue
+++ /dev/null
@@ -1,95 +0,0 @@
-<script lang="ts" setup>
-import type { VxeTableGridOptions } from '#/adapter/vxe-table';
-import type { MesWmProductSalesLineApi } from '#/api/mes/wm/productsales/line';
-
-import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
-
-import { Button } from 'ant-design-vue';
-
-import { useVbenVxeGrid } from '#/adapter/vxe-table';
-import { getProductSalesLinePage } from '#/api/mes/wm/productsales/line';
-import ItemForm from '#/views/mes/md/item/modules/form.vue';
-
-const props = defineProps<{
-  clientId: number;
-}>();
-
-const [ItemFormModal, itemFormModalApi] = useVbenModal({
-  connectedComponent: ItemForm,
-  destroyOnClose: true,
-});
-
-/** 鏌ョ湅鐗╂枡璇︽儏 */
-function handleViewItem(row: MesWmProductSalesLineApi.ProductSalesLine) {
-  itemFormModalApi.setData({ id: row.itemId, formType: 'detail' }).open();
-}
-
-const [Grid] = useVbenVxeGrid({
-  gridOptions: {
-    columns: [
-      {
-        field: 'itemCode',
-        title: '鐗╂枡缂栫爜',
-        width: 140,
-        slots: { default: 'itemCode' },
-      },
-      {
-        field: 'itemName',
-        title: '鐗╂枡鍚嶇О',
-        minWidth: 150,
-      },
-      {
-        field: 'specification',
-        title: '瑙勬牸鍨嬪彿',
-        minWidth: 140,
-      },
-      {
-        field: 'unitMeasureName',
-        title: '鍗曚綅',
-        width: 100,
-      },
-      {
-        field: 'quantity',
-        title: '鍑哄簱鏁伴噺',
-        width: 120,
-      },
-      {
-        field: 'batchCode',
-        title: '鎵规鍙�',
-        minWidth: 140,
-      },
-    ],
-    height: 320,
-    keepSource: true,
-    proxyConfig: {
-      ajax: {
-        query: async ({ page }) => {
-          return await getProductSalesLinePage({
-            pageNo: page.currentPage,
-            pageSize: page.pageSize,
-            clientId: props.clientId,
-          });
-        },
-      },
-    },
-    rowConfig: {
-      keyField: 'id',
-      isHover: true,
-    },
-    toolbarConfig: {
-      refresh: true,
-    },
-  } as VxeTableGridOptions<MesWmProductSalesLineApi.ProductSalesLine>,
-});
-</script>
-
-<template>
-  <ItemFormModal />
-  <Grid table-title="浜у搧娓呭崟">
-    <template #itemCode="{ row }">
-      <Button type="link" @click="handleViewItem(row)">
-        {{ row.itemCode }}
-      </Button>
-    </template>
-  </Grid>
-</template>
diff --git a/src/views/mes/md/client/modules/product-sales-list.vue b/src/views/mes/md/client/modules/product-sales-list.vue
deleted file mode 100644
index fe171b0..0000000
--- a/src/views/mes/md/client/modules/product-sales-list.vue
+++ /dev/null
@@ -1,98 +0,0 @@
-<script lang="ts" setup>
-import type { VxeTableGridOptions } from '#/adapter/vxe-table';
-import type { MesWmProductSalesApi } from '#/api/mes/wm/productsales';
-
-import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
-import { DICT_TYPE } from '../../../../../packages/constants/src';
-
-import { Button } from 'ant-design-vue';
-
-import { useVbenVxeGrid } from '#/adapter/vxe-table';
-import { getProductSalesPage } from '#/api/mes/wm/productsales';
-import ProductSalesForm from '#/views/mes/wm/productsales/modules/form.vue';
-
-const props = defineProps<{
-  clientId: number;
-}>();
-
-const [DetailModal, detailModalApi] = useVbenModal({
-  connectedComponent: ProductSalesForm,
-  destroyOnClose: true,
-});
-
-/** 鏌ョ湅閿�鍞嚭搴撳崟璇︽儏 */
-function handleViewSales(row: MesWmProductSalesApi.ProductSales) {
-  if (row.id) {
-    detailModalApi.setData({ formType: 'detail', id: row.id }).open();
-  }
-}
-
-const [Grid] = useVbenVxeGrid({
-  gridOptions: {
-    columns: [
-      {
-        field: 'code',
-        title: '鍑哄簱鍗曠紪鍙�',
-        minWidth: 160,
-        slots: { default: 'code' },
-      },
-      {
-        field: 'name',
-        title: '鍑哄簱鍗曞悕绉�',
-        minWidth: 150,
-      },
-      {
-        field: 'salesOrderCode',
-        title: '閿�鍞鍗曠紪鍙�',
-        minWidth: 140,
-      },
-      {
-        field: 'salesDate',
-        title: '鍑哄簱鏃ユ湡',
-        width: 180,
-        formatter: 'formatDateTime',
-      },
-      {
-        field: 'status',
-        title: '鍗曟嵁鐘舵��',
-        width: 120,
-        cellRender: {
-          name: 'CellDict',
-          props: { type: DICT_TYPE.MES_WM_PRODUCT_SALES_STATUS },
-        },
-      },
-    ],
-    height: 320,
-    keepSource: true,
-    proxyConfig: {
-      ajax: {
-        query: async ({ page }) => {
-          return await getProductSalesPage({
-            pageNo: page.currentPage,
-            pageSize: page.pageSize,
-            clientId: props.clientId,
-          });
-        },
-      },
-    },
-    rowConfig: {
-      keyField: 'id',
-      isHover: true,
-    },
-    toolbarConfig: {
-      refresh: true,
-    },
-  } as VxeTableGridOptions<MesWmProductSalesApi.ProductSales>,
-});
-</script>
-
-<template>
-  <DetailModal />
-  <Grid table-title="閿�鍞褰�">
-    <template #code="{ row }">
-      <Button type="link" @click="handleViewSales(row)">
-        {{ row.code }}
-      </Button>
-    </template>
-  </Grid>
-</template>
diff --git a/src/views/mes/md/item/data.ts b/src/views/mes/md/item/data.ts
index bb418e6..3e8be0e 100644
--- a/src/views/mes/md/item/data.ts
+++ b/src/views/mes/md/item/data.ts
@@ -96,7 +96,7 @@
       label: '鐗╂枡鍒嗙被',
       component: markRaw(MdItemTypeSelect),
       componentProps: {
-        onChange: async (itemType: any) => {
+        onChange: async (itemType: { itemOrProduct?: string }) => {
           await formApi?.setFieldValue(
             'itemOrProduct',
             itemType?.itemOrProduct,
diff --git a/src/views/mes/md/unitmeasure/data.ts b/src/views/mes/md/unitmeasure/data.ts
deleted file mode 100644
index cd7885e..0000000
--- a/src/views/mes/md/unitmeasure/data.ts
+++ /dev/null
@@ -1,196 +0,0 @@
-import type { VbenFormSchema } from '#/adapter/form';
-import type { VxeTableGridOptions } from '#/adapter/vxe-table';
-import type { MesMdUnitMeasureApi } from '#/api/mes/md/unitmeasure';
-
-import { CommonStatusEnum, DICT_TYPE } from '../../../../packages/constants/src';
-import { getDictOptions } from '../../../../packages/effects/hooks/src';
-
-import { z } from '#/adapter/form';
-import { getUnitMeasureSimpleList } from '#/api/mes/md/unitmeasure';
-
-/** 鏂板/淇敼璁¢噺鍗曚綅鐨勮〃鍗� */
-export function useFormSchema(): VbenFormSchema[] {
-  return [
-    {
-      fieldName: 'id',
-      component: 'Input',
-      dependencies: {
-        triggerFields: [''],
-        show: () => false,
-      },
-    },
-    {
-      fieldName: 'code',
-      label: '鍗曚綅缂栫爜',
-      component: 'Input',
-      componentProps: {
-        placeholder: '璇疯緭鍏ュ崟浣嶇紪鐮�',
-      },
-      rules: 'required',
-    },
-    {
-      fieldName: 'name',
-      label: '鍗曚綅鍚嶇О',
-      component: 'Input',
-      componentProps: {
-        placeholder: '璇疯緭鍏ュ崟浣嶅悕绉�',
-      },
-      rules: 'required',
-    },
-    {
-      fieldName: 'primaryFlag',
-      label: '鏄惁涓诲崟浣�',
-      component: 'RadioGroup',
-      componentProps: {
-        buttonStyle: 'solid',
-        optionType: 'button',
-        options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
-      },
-      rules: z.boolean().default(true),
-    },
-    {
-      fieldName: 'primaryId',
-      label: '涓诲崟浣�',
-      component: 'ApiSelect',
-      componentProps: {
-        allowClear: true,
-        api: async () => {
-          const list = await getUnitMeasureSimpleList();
-          return list.filter((item) => item.primaryFlag === true);
-        },
-        labelField: 'name',
-        placeholder: '璇烽�夋嫨涓诲崟浣�',
-        valueField: 'id',
-      },
-      dependencies: {
-        triggerFields: ['primaryFlag'],
-        show: (values) => values.primaryFlag === false,
-      },
-    },
-    {
-      fieldName: 'changeRate',
-      label: '涓庝富鍗曚綅鎹㈢畻姣斾緥',
-      component: 'InputNumber',
-      componentProps: {
-        class: '!w-full',
-        min: 0,
-        precision: 4,
-        step: 1,
-      },
-      dependencies: {
-        triggerFields: ['primaryFlag'],
-        show: (values) => values.primaryFlag === false,
-      },
-    },
-    {
-      fieldName: 'status',
-      label: '鐘舵��',
-      component: 'RadioGroup',
-      componentProps: {
-        buttonStyle: 'solid',
-        optionType: 'button',
-        options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
-      },
-      rules: z.number().default(CommonStatusEnum.ENABLE),
-    },
-    {
-      fieldName: 'remark',
-      label: '澶囨敞',
-      component: 'Textarea',
-      componentProps: {
-        placeholder: '璇疯緭鍏ュ娉�',
-        rows: 3,
-      },
-    },
-  ];
-}
-
-/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
-export function useGridFormSchema(): VbenFormSchema[] {
-  return [
-    {
-      fieldName: 'code',
-      label: '鍗曚綅缂栫爜',
-      component: 'Input',
-      componentProps: {
-        allowClear: true,
-        placeholder: '璇疯緭鍏ュ崟浣嶇紪鐮�',
-      },
-    },
-    {
-      fieldName: 'name',
-      label: '鍗曚綅鍚嶇О',
-      component: 'Input',
-      componentProps: {
-        allowClear: true,
-        placeholder: '璇疯緭鍏ュ崟浣嶅悕绉�',
-      },
-    },
-    {
-      fieldName: 'status',
-      label: '鐘舵��',
-      component: 'Select',
-      componentProps: {
-        allowClear: true,
-        options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
-        placeholder: '璇烽�夋嫨鐘舵��',
-      },
-    },
-  ];
-}
-
-/** 鍒楄〃鐨勫瓧娈� */
-export function useGridColumns(): VxeTableGridOptions<MesMdUnitMeasureApi.UnitMeasure>['columns'] {
-  return [
-    {
-      field: 'code',
-      title: '鍗曚綅缂栫爜',
-      minWidth: 160,
-    },
-    {
-      field: 'name',
-      title: '鍗曚綅鍚嶇О',
-      minWidth: 160,
-    },
-    {
-      field: 'primaryFlag',
-      title: '鏄惁涓诲崟浣�',
-      width: 120,
-      cellRender: {
-        name: 'CellDict',
-        props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
-      },
-    },
-    {
-      field: 'changeRate',
-      title: '涓庝富鍗曚綅鎹㈢畻姣斾緥',
-      minWidth: 150,
-    },
-    {
-      field: 'status',
-      title: '鐘舵��',
-      width: 120,
-      cellRender: {
-        name: 'CellDict',
-        props: { type: DICT_TYPE.COMMON_STATUS },
-      },
-    },
-    {
-      field: 'remark',
-      title: '澶囨敞',
-      minWidth: 180,
-    },
-    {
-      field: 'createTime',
-      title: '鍒涘缓鏃堕棿',
-      width: 180,
-      formatter: 'formatDateTime',
-    },
-    {
-      title: '鎿嶄綔',
-      width: 160,
-      fixed: 'right',
-      slots: { default: 'actions' },
-    },
-  ];
-}
diff --git a/src/views/mes/md/unitmeasure/modules/form.vue b/src/views/mes/md/unitmeasure/modules/form.vue
deleted file mode 100644
index 7eec15a..0000000
--- a/src/views/mes/md/unitmeasure/modules/form.vue
+++ /dev/null
@@ -1,92 +0,0 @@
-<script lang="ts" setup>
-import type { MesMdUnitMeasureApi } from '#/api/mes/md/unitmeasure';
-
-import { computed, ref } from 'vue';
-
-import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
-
-import { message } from 'ant-design-vue';
-
-import { useVbenForm } from '#/adapter/form';
-import {
-  createUnitMeasure,
-  getUnitMeasure,
-  updateUnitMeasure,
-} from '#/api/mes/md/unitmeasure';
-import { $t } from '#/locales';
-
-import { useFormSchema } from '../data';
-
-const emit = defineEmits(['success']);
-const formData = ref<MesMdUnitMeasureApi.UnitMeasure>();
-const getTitle = computed(() => {
-  return formData.value?.id
-    ? $t('ui.actionTitle.edit', ['璁¢噺鍗曚綅'])
-    : $t('ui.actionTitle.create', ['璁¢噺鍗曚綅']);
-});
-
-const [Form, formApi] = useVbenForm({
-  commonConfig: {
-    componentProps: {
-      class: 'w-full',
-    },
-    formItemClass: 'col-span-2',
-    labelWidth: 140,
-  },
-  layout: 'horizontal',
-  schema: useFormSchema(),
-  showDefaultActions: false,
-});
-
-const [Modal, modalApi] = useVbenModal({
-  async onConfirm() {
-    const { valid } = await formApi.validate();
-    if (!valid) {
-      return;
-    }
-    modalApi.lock();
-    // 鎻愪氦琛ㄥ崟
-    const data = (await formApi.getValues()) as MesMdUnitMeasureApi.UnitMeasure;
-    if (data.primaryFlag) {
-      data.primaryId = undefined;
-      data.changeRate = undefined;
-    }
-    try {
-      await (formData.value?.id
-        ? updateUnitMeasure(data)
-        : createUnitMeasure(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<MesMdUnitMeasureApi.UnitMeasure>();
-    if (!data || !data.id) {
-      return;
-    }
-    modalApi.lock();
-    try {
-      formData.value = await getUnitMeasure(data.id);
-      // 璁剧疆鍒� values
-      await formApi.setValues(formData.value);
-    } finally {
-      modalApi.unlock();
-    }
-  },
-});
-</script>
-
-<template>
-  <Modal :title="getTitle" class="w-1/3">
-    <Form class="mx-4" />
-  </Modal>
-</template>
diff --git a/src/views/mes/md/vendor/data.ts b/src/views/mes/md/vendor/data.ts
index a1a2032..6a0bec1 100644
--- a/src/views/mes/md/vendor/data.ts
+++ b/src/views/mes/md/vendor/data.ts
@@ -1,383 +1,10 @@
-import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
+import type { VbenFormSchema } from '#/adapter/form';
 import type { VxeTableGridOptions } from '#/adapter/vxe-table';
 import type { MesMdVendorApi } from '#/api/mes/md/vendor';
 
-import { h } from 'vue';
+import { DICT_TYPE } from '#/packages/constants/src';
 
-import {
-  CommonStatusEnum,
-  DICT_TYPE,
-  MesAutoCodeRuleCode,
-} from '../../../../packages/constants/src';
-import { getDictOptions } from '../../../../packages/effects/hooks/src';
-
-import { Button } from 'ant-design-vue';
-
-import { z } from '#/adapter/form';
-import { generateAutoCode } from '#/api/mes/md/autocode/record';
-
-/** 琛ㄥ崟绫诲瀷 */
-export type FormType = 'create' | 'detail' | 'update';
-
-/** 鏂板/淇敼渚涘簲鍟嗙殑琛ㄥ崟 */
-export function useFormSchema(
-  formType: FormType,
-  formApi?: VbenFormApi,
-): VbenFormSchema[] {
-  return [
-    {
-      fieldName: 'id',
-      component: 'Input',
-      dependencies: {
-        triggerFields: [''],
-        show: () => false,
-      },
-    },
-    {
-      fieldName: 'code',
-      label: '渚涘簲鍟嗙紪鐮�',
-      component: 'Input',
-      componentProps: {
-        placeholder: '璇疯緭鍏ヤ緵搴斿晢缂栫爜',
-      },
-      rules: 'required',
-      suffix:
-        formType === 'detail'
-          ? undefined
-          : () =>
-              h(
-                Button,
-                {
-                  type: 'default',
-                  onClick: async () => {
-                    const code = await generateAutoCode(
-                      MesAutoCodeRuleCode.MD_VENDOR_CODE,
-                    );
-                    await formApi?.setFieldValue('code', code);
-                  },
-                },
-                { default: () => '鑷姩鐢熸垚' },
-              ),
-    },
-    {
-      fieldName: 'name',
-      label: '渚涘簲鍟嗗悕绉�',
-      component: 'Input',
-      componentProps: {
-        placeholder: '璇疯緭鍏ヤ緵搴斿晢鍚嶇О',
-      },
-      rules: z.string().min(1, '渚涘簲鍟嗗悕绉颁笉鑳戒负绌�').max(100),
-    },
-    {
-      fieldName: 'nickname',
-      label: '渚涘簲鍟嗙畝绉�',
-      component: 'Input',
-      componentProps: {
-        placeholder: '璇疯緭鍏ヤ緵搴斿晢绠�绉�',
-      },
-    },
-    {
-      fieldName: 'englishName',
-      label: '鑻辨枃鍚嶇О',
-      component: 'Input',
-      componentProps: {
-        placeholder: '璇疯緭鍏ヤ緵搴斿晢鑻辨枃鍚嶇О',
-      },
-    },
-    {
-      fieldName: 'level',
-      label: '渚涘簲鍟嗙瓑绾�',
-      component: 'Select',
-      componentProps: {
-        allowClear: true,
-        options: getDictOptions(DICT_TYPE.MES_VENDOR_LEVEL),
-        placeholder: '璇烽�夋嫨渚涘簲鍟嗙瓑绾�',
-      },
-    },
-    {
-      fieldName: 'description',
-      label: '渚涘簲鍟嗙畝浠�',
-      component: 'Textarea',
-      formItemClass: 'col-span-3',
-      componentProps: {
-        placeholder: '璇疯緭鍏ヤ緵搴斿晢绠�浠�',
-        rows: 2,
-      },
-    },
-    {
-      fieldName: 'address',
-      label: '渚涘簲鍟嗗湴鍧�',
-      component: 'Textarea',
-      formItemClass: 'col-span-3',
-      componentProps: {
-        placeholder: '璇疯緭鍏ヤ緵搴斿晢鍦板潃',
-        rows: 2,
-      },
-    },
-    {
-      fieldName: 'website',
-      label: '瀹樼綉鍦板潃',
-      component: 'Input',
-      componentProps: {
-        placeholder: '璇疯緭鍏ヤ緵搴斿晢瀹樼綉鍦板潃',
-      },
-    },
-    {
-      fieldName: 'email',
-      label: '閭鍦板潃',
-      component: 'Input',
-      componentProps: {
-        placeholder: '璇疯緭鍏ヤ緵搴斿晢閭鍦板潃',
-      },
-      rules: z.string().email('閭鏍煎紡涓嶆纭�').or(z.literal('')).optional(),
-    },
-    {
-      fieldName: 'telephone',
-      label: '渚涘簲鍟嗙數璇�',
-      component: 'Input',
-      componentProps: {
-        placeholder: '璇疯緭鍏ヤ緵搴斿晢鐢佃瘽',
-      },
-    },
-    {
-      fieldName: 'score',
-      label: '渚涘簲鍟嗚瘎鍒�',
-      component: 'InputNumber',
-      componentProps: {
-        class: '!w-full',
-        max: 100,
-        min: 0,
-        precision: 0,
-      },
-    },
-    {
-      fieldName: 'contact1Name',
-      label: '鑱旂郴浜�1',
-      component: 'Input',
-      componentProps: {
-        placeholder: '璇疯緭鍏ヨ仈绯讳汉1',
-      },
-    },
-    {
-      fieldName: 'contact1Telephone',
-      label: '鑱旂郴浜�1鐢佃瘽',
-      component: 'Input',
-      componentProps: {
-        placeholder: '璇疯緭鍏ヨ仈绯讳汉1鐢佃瘽',
-      },
-    },
-    {
-      fieldName: 'contact1Email',
-      label: '鑱旂郴浜�1閭',
-      component: 'Input',
-      componentProps: {
-        placeholder: '璇疯緭鍏ヨ仈绯讳汉1閭',
-      },
-      rules: z.string().email('閭鏍煎紡涓嶆纭�').or(z.literal('')).optional(),
-    },
-    {
-      fieldName: 'contact2Name',
-      label: '鑱旂郴浜�2',
-      component: 'Input',
-      componentProps: {
-        placeholder: '璇疯緭鍏ヨ仈绯讳汉2',
-      },
-    },
-    {
-      fieldName: 'contact2Telephone',
-      label: '鑱旂郴浜�2鐢佃瘽',
-      component: 'Input',
-      componentProps: {
-        placeholder: '璇疯緭鍏ヨ仈绯讳汉2鐢佃瘽',
-      },
-    },
-    {
-      fieldName: 'contact2Email',
-      label: '鑱旂郴浜�2閭',
-      component: 'Input',
-      componentProps: {
-        placeholder: '璇疯緭鍏ヨ仈绯讳汉2閭',
-      },
-      rules: z.string().email('閭鏍煎紡涓嶆纭�').or(z.literal('')).optional(),
-    },
-    {
-      fieldName: 'creditCode',
-      label: '绀句細淇$敤浠g爜',
-      component: 'Input',
-      componentProps: {
-        placeholder: '璇疯緭鍏ョ粺涓�绀句細淇$敤浠g爜',
-      },
-    },
-    {
-      fieldName: 'status',
-      label: '鐘舵��',
-      component: 'RadioGroup',
-      componentProps: {
-        buttonStyle: 'solid',
-        optionType: 'button',
-        options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
-      },
-      rules: z.number().default(CommonStatusEnum.ENABLE),
-    },
-    {
-      fieldName: 'logo',
-      label: '渚涘簲鍟� LOGO',
-      component: 'Input',
-      componentProps: {
-        placeholder: '璇疯緭鍏ヤ緵搴斿晢 LOGO 鍦板潃',
-      },
-    },
-    {
-      fieldName: 'remark',
-      label: '澶囨敞',
-      component: 'Textarea',
-      formItemClass: 'col-span-3',
-      componentProps: {
-        placeholder: '璇疯緭鍏ュ娉�',
-        rows: 3,
-      },
-    },
-  ];
-}
-
-/** 瀵煎叆渚涘簲鍟嗙殑琛ㄥ崟 */
-export function useImportFormSchema(): VbenFormSchema[] {
-  return [
-    {
-      fieldName: 'file',
-      label: '渚涘簲鍟嗘暟鎹�',
-      component: 'Upload',
-      rules: 'required',
-      help: '浠呭厑璁稿鍏� xls銆亁lsx 鏍煎紡鏂囦欢',
-    },
-    {
-      fieldName: 'updateSupport',
-      label: '鏄惁瑕嗙洊',
-      component: 'Switch',
-      componentProps: {
-        checkedChildren: '鏄�',
-        unCheckedChildren: '鍚�',
-      },
-      rules: z.boolean().default(false),
-      help: '鏄惁鏇存柊宸茬粡瀛樺湪鐨勪緵搴斿晢鏁版嵁',
-    },
-  ];
-}
-
-/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
-export function useGridFormSchema(): VbenFormSchema[] {
-  return [
-    {
-      fieldName: 'code',
-      label: '渚涘簲鍟嗙紪鐮�',
-      component: 'Input',
-      componentProps: {
-        allowClear: true,
-        placeholder: '璇疯緭鍏ヤ緵搴斿晢缂栫爜',
-      },
-    },
-    {
-      fieldName: 'name',
-      label: '渚涘簲鍟嗗悕绉�',
-      component: 'Input',
-      componentProps: {
-        allowClear: true,
-        placeholder: '璇疯緭鍏ヤ緵搴斿晢鍚嶇О',
-      },
-    },
-    {
-      fieldName: 'nickname',
-      label: '渚涘簲鍟嗙畝绉�',
-      component: 'Input',
-      componentProps: {
-        allowClear: true,
-        placeholder: '璇疯緭鍏ヤ緵搴斿晢绠�绉�',
-      },
-    },
-    {
-      fieldName: 'englishName',
-      label: '鑻辨枃鍚嶇О',
-      component: 'Input',
-      componentProps: {
-        allowClear: true,
-        placeholder: '璇疯緭鍏ヨ嫳鏂囧悕绉�',
-      },
-    },
-    {
-      fieldName: 'status',
-      label: '鐘舵��',
-      component: 'Select',
-      componentProps: {
-        allowClear: true,
-        options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
-        placeholder: '璇烽�夋嫨鐘舵��',
-      },
-    },
-  ];
-}
-
-/** 鍒楄〃鐨勫瓧娈� */
-export function useGridColumns(): VxeTableGridOptions<MesMdVendorApi.Vendor>['columns'] {
-  return [
-    {
-      field: 'code',
-      title: '渚涘簲鍟嗙紪鐮�',
-      minWidth: 150,
-      slots: { default: 'code' },
-    },
-    {
-      field: 'name',
-      title: '渚涘簲鍟嗗悕绉�',
-      minWidth: 180,
-    },
-    {
-      field: 'nickname',
-      title: '渚涘簲鍟嗙畝绉�',
-      width: 120,
-    },
-    {
-      field: 'level',
-      title: '渚涘簲鍟嗙瓑绾�',
-      width: 130,
-      cellRender: {
-        name: 'CellDict',
-        props: { type: DICT_TYPE.MES_VENDOR_LEVEL },
-      },
-    },
-    {
-      field: 'score',
-      title: '渚涘簲鍟嗚瘎鍒�',
-      width: 120,
-    },
-    {
-      field: 'telephone',
-      title: '渚涘簲鍟嗙數璇�',
-      minWidth: 140,
-    },
-    {
-      field: 'status',
-      title: '鐘舵��',
-      width: 120,
-      cellRender: {
-        name: 'CellDict',
-        props: { type: DICT_TYPE.COMMON_STATUS },
-      },
-    },
-    {
-      field: 'remark',
-      title: '澶囨敞',
-      minWidth: 180,
-    },
-    {
-      title: '鎿嶄綔',
-      width: 160,
-      fixed: 'right',
-      slots: { default: 'actions' },
-    },
-  ];
-}
-
-/** 渚涘簲鍟嗛�夋嫨寮圭獥鐨勬悳绱㈣〃鍗� */
+/** 渚涘簲鍟嗛�夋嫨寮圭獥鎼滅储琛ㄥ崟 */
 export function useVendorSelectGridFormSchema(): VbenFormSchema[] {
   return [
     {
@@ -419,7 +46,7 @@
   ];
 }
 
-/** 渚涘簲鍟嗛�夋嫨寮圭獥鐨勫瓧娈� */
+/** 渚涘簲鍟嗛�夋嫨寮圭獥鍒楄〃瀛楁 */
 export function useVendorSelectGridColumns(
   multiple = true,
 ): VxeTableGridOptions<MesMdVendorApi.Vendor>['columns'] {
diff --git a/src/views/mes/md/vendor/index.vue b/src/views/mes/md/vendor/index.vue
deleted file mode 100644
index 3f68372..0000000
--- a/src/views/mes/md/vendor/index.vue
+++ /dev/null
@@ -1,168 +0,0 @@
-<script lang="ts" setup>
-import type { VxeTableGridOptions } from '#/adapter/vxe-table';
-import type { MesMdVendorApi } from '#/api/mes/md/vendor';
-
-import { Page, useVbenModal } from '../../../../packages/effects/common-ui/src';
-import { downloadFileFromBlobPart } from '../../../../packages/utils/src';
-
-import { Button, message } from 'ant-design-vue';
-
-import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
-import { deleteVendor, exportVendor, getVendorPage } from '#/api/mes/md/vendor';
-import { $t } from '#/locales';
-
-import { useGridColumns, useGridFormSchema } from './data';
-import Form from './modules/form.vue';
-import ImportForm from './modules/import-form.vue';
-
-const [FormModal, formModalApi] = useVbenModal({
-  connectedComponent: Form,
-  destroyOnClose: true,
-});
-
-const [ImportModal, importModalApi] = useVbenModal({
-  connectedComponent: ImportForm,
-  destroyOnClose: true,
-});
-
-/** 鍒锋柊琛ㄦ牸 */
-function handleRefresh() {
-  gridApi.query();
-}
-
-/** 鍒涘缓渚涘簲鍟� */
-function handleCreate() {
-  formModalApi.setData({ formType: 'create' }).open();
-}
-
-/** 鏌ョ湅渚涘簲鍟� */
-function handleDetail(row: MesMdVendorApi.Vendor) {
-  formModalApi.setData({ id: row.id, formType: 'detail' }).open();
-}
-
-/** 缂栬緫渚涘簲鍟� */
-function handleEdit(row: MesMdVendorApi.Vendor) {
-  formModalApi.setData({ id: row.id, formType: 'update' }).open();
-}
-
-/** 鍒犻櫎渚涘簲鍟� */
-async function handleDelete(row: MesMdVendorApi.Vendor) {
-  const hideLoading = message.loading({
-    content: $t('ui.actionMessage.deleting', [row.name]),
-    duration: 0,
-  });
-  try {
-    await deleteVendor(row.id!);
-    message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
-    handleRefresh();
-  } finally {
-    hideLoading();
-  }
-}
-
-/** 瀵煎嚭琛ㄦ牸 */
-async function handleExport() {
-  const data = await exportVendor(await gridApi.formApi.getValues());
-  downloadFileFromBlobPart({ fileName: '渚涘簲鍟�.xls', source: data });
-}
-
-/** 瀵煎叆渚涘簲鍟� */
-function handleImport() {
-  importModalApi.open();
-}
-
-const [Grid, gridApi] = useVbenVxeGrid({
-  formOptions: {
-    schema: useGridFormSchema(),
-  },
-  gridOptions: {
-    columns: useGridColumns(),
-    height: 'auto',
-    keepSource: true,
-    proxyConfig: {
-      ajax: {
-        query: async ({ page }, formValues) => {
-          return await getVendorPage({
-            pageNo: page.currentPage,
-            pageSize: page.pageSize,
-            ...formValues,
-          });
-        },
-      },
-    },
-    rowConfig: {
-      keyField: 'id',
-      isHover: true,
-    },
-    toolbarConfig: {
-      refresh: true,
-      search: true,
-    },
-  } as VxeTableGridOptions<MesMdVendorApi.Vendor>,
-});
-</script>
-
-<template>
-  <Page auto-content-height><FormModal @success="handleRefresh" />
-    <ImportModal @success="handleRefresh" />
-
-    <Grid table-title="渚涘簲鍟嗗垪琛�">
-      <template #toolbar-tools>
-        <TableAction
-          :actions="[
-            {
-              label: $t('ui.actionTitle.create', ['渚涘簲鍟�']),
-              type: 'primary',
-              icon: ACTION_ICON.ADD,
-              auth: ['mes:md-vendor:create'],
-              onClick: handleCreate,
-            },
-            {
-              label: $t('ui.actionTitle.import', ['渚涘簲鍟�']),
-              type: 'primary',
-              icon: ACTION_ICON.UPLOAD,
-              auth: ['mes:md-vendor:import'],
-              onClick: handleImport,
-            },
-            {
-              label: $t('ui.actionTitle.export'),
-              type: 'primary',
-              icon: ACTION_ICON.DOWNLOAD,
-              auth: ['mes:md-vendor:export'],
-              onClick: handleExport,
-            },
-          ]"
-        />
-      </template>
-      <template #code="{ row }">
-        <Button type="link" @click="handleDetail(row)">
-          {{ row.code }}
-        </Button>
-      </template>
-      <template #actions="{ row }">
-        <TableAction
-          :actions="[
-            {
-              label: $t('common.edit'),
-              type: 'link',
-              icon: ACTION_ICON.EDIT,
-              auth: ['mes:md-vendor:update'],
-              onClick: handleEdit.bind(null, row),
-            },
-            {
-              label: $t('common.delete'),
-              type: 'link',
-              danger: true,
-              icon: ACTION_ICON.DELETE,
-              auth: ['mes:md-vendor:delete'],
-              popConfirm: {
-                title: $t('ui.actionMessage.deleteConfirm', [row.name]),
-                confirm: handleDelete.bind(null, row),
-              },
-            },
-          ]"
-        />
-      </template>
-    </Grid>
-  </Page>
-</template>
diff --git a/src/views/mes/md/vendor/modules/form.vue b/src/views/mes/md/vendor/modules/form.vue
deleted file mode 100644
index 4c2b919..0000000
--- a/src/views/mes/md/vendor/modules/form.vue
+++ /dev/null
@@ -1,115 +0,0 @@
-<script lang="ts" setup>
-import type { FormType } from '../data';
-
-import type { MesMdVendorApi } from '#/api/mes/md/vendor';
-
-import { computed, ref } from 'vue';
-
-import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
-
-import { message, Tabs } from 'ant-design-vue';
-
-import { useVbenForm } from '#/adapter/form';
-import { createVendor, getVendor, updateVendor } from '#/api/mes/md/vendor';
-import { $t } from '#/locales';
-
-import { useFormSchema } from '../data';
-import VendorItemReceiptLineList from './item-receipt-line-list.vue';
-import VendorItemReceiptList from './item-receipt-list.vue';
-
-const emit = defineEmits(['success']);
-const formType = ref<FormType>('create'); // 琛ㄥ崟妯″紡
-const subTabsName = ref('itemReceiptLine'); // 褰撳墠瀛愯〃椤电
-const formData = ref<MesMdVendorApi.Vendor>();
-
-const isDetail = computed(() => formType.value === 'detail'); // 鏄惁鏌ョ湅妯″紡
-const getTitle = computed(() => {
-  if (formType.value === 'detail') {
-    return $t('ui.actionTitle.view', ['渚涘簲鍟�']);
-  }
-  return formType.value === 'update'
-    ? $t('ui.actionTitle.edit', ['渚涘簲鍟�'])
-    : $t('ui.actionTitle.create', ['渚涘簲鍟�']);
-});
-
-const [Form, formApi] = useVbenForm({
-  commonConfig: {
-    componentProps: {
-      class: 'w-full',
-    },
-    formItemClass: 'col-span-1',
-    labelWidth: 120,
-  },
-  wrapperClass: 'grid-cols-3',
-  layout: 'horizontal',
-  schema: [],
-  showDefaultActions: false,
-});
-
-const [Modal, modalApi] = useVbenModal({
-  async onConfirm() {
-    if (isDetail.value) {
-      await modalApi.close();
-      return;
-    }
-    const { valid } = await formApi.validate();
-    if (!valid) {
-      return;
-    }
-    modalApi.lock();
-    // 鎻愪氦琛ㄥ崟
-    const data = (await formApi.getValues()) as MesMdVendorApi.Vendor;
-    try {
-      await (formData.value?.id ? updateVendor(data) : createVendor(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;
-    }
-    subTabsName.value = 'itemReceiptLine';
-    // 鍔犺浇鏁版嵁
-    const data = modalApi.getData<{ formType: FormType; id?: number }>();
-    formType.value = data.formType;
-    formApi.setState({ schema: useFormSchema(data.formType, formApi) });
-    formApi.setDisabled(formType.value === 'detail');
-    modalApi.setState({ showConfirmButton: formType.value !== 'detail' });
-    if (!data?.id) {
-      return;
-    }
-    modalApi.lock();
-    try {
-      formData.value = await getVendor(data.id);
-      // 璁剧疆鍒� values
-      await formApi.setValues(formData.value);
-    } finally {
-      modalApi.unlock();
-    }
-  },
-});
-</script>
-
-<template>
-  <Modal :title="getTitle" class="w-4/5">
-    <Form class="mx-4" />
-    <Tabs
-      v-if="formType !== 'create' && formData?.id"
-      v-model:active-key="subTabsName"
-      class="mx-4 mt-4"
-    >
-      <Tabs.TabPane key="itemReceiptLine" tab="鐗╂枡娓呭崟">
-        <VendorItemReceiptLineList :vendor-id="formData.id" />
-      </Tabs.TabPane>
-      <Tabs.TabPane key="itemReceipt" tab="閲囪喘璁板綍">
-        <VendorItemReceiptList :vendor-id="formData.id" />
-      </Tabs.TabPane>
-    </Tabs>
-  </Modal>
-</template>
diff --git a/src/views/mes/md/vendor/modules/import-form.vue b/src/views/mes/md/vendor/modules/import-form.vue
deleted file mode 100644
index 2e0c814..0000000
--- a/src/views/mes/md/vendor/modules/import-form.vue
+++ /dev/null
@@ -1,101 +0,0 @@
-<script lang="ts" setup>
-import type { FileType } from 'ant-design-vue/es/upload/interface';
-
-import type { MesMdVendorApi } from '#/api/mes/md/vendor';
-
-import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
-import { downloadFileFromBlobPart } from '../../../../../packages/utils/src';
-
-import { Button, message, Upload } from 'ant-design-vue';
-
-import { useVbenForm } from '#/adapter/form';
-import { importVendor, importVendorTemplate } from '#/api/mes/md/vendor';
-import { $t } from '#/locales';
-
-import { useImportFormSchema } from '../data';
-
-const emit = defineEmits(['success']);
-
-const [Form, formApi] = useVbenForm({
-  commonConfig: {
-    componentProps: {
-      class: 'w-full',
-    },
-    formItemClass: 'col-span-2',
-    labelWidth: 120,
-  },
-  layout: 'horizontal',
-  schema: useImportFormSchema(),
-  showDefaultActions: false,
-});
-
-const [Modal, modalApi] = useVbenModal({
-  async onConfirm() {
-    const { valid } = await formApi.validate();
-    if (!valid) {
-      return;
-    }
-    modalApi.lock();
-    // 鎻愪氦琛ㄥ崟
-    const data = await formApi.getValues();
-    try {
-      const result = await importVendor(data.file, data.updateSupport);
-      const importData = result as MesMdVendorApi.VendorImportRespVO;
-      let text = `涓婁紶鎴愬姛鏁伴噺锛�${importData.createCodes?.length || 0};`;
-      for (const code of importData.createCodes || []) {
-        text += `< ${code} >`;
-      }
-      text += `鏇存柊鎴愬姛鏁伴噺锛�${importData.updateCodes?.length || 0};`;
-      for (const code of importData.updateCodes || []) {
-        text += `< ${code} >`;
-      }
-      text += `鏇存柊澶辫触鏁伴噺锛�${Object.keys(importData.failureCodes || {}).length};`;
-      for (const code in importData.failureCodes || {}) {
-        text += `< ${code}: ${importData.failureCodes?.[code]} >`;
-      }
-      message.info(text);
-      // 鍏抽棴骞舵彁绀�
-      await modalApi.close();
-      emit('success');
-      message.success($t('ui.actionMessage.operationSuccess'));
-    } finally {
-      modalApi.unlock();
-    }
-  },
-});
-
-/** 涓婁紶鍓� */
-function beforeUpload(file: FileType) {
-  formApi.setFieldValue('file', file);
-  return false;
-}
-
-/** 涓嬭浇妯℃澘 */
-async function handleDownload() {
-  const data = await importVendorTemplate();
-  downloadFileFromBlobPart({ fileName: '渚涘簲鍟嗗鍏ユā鏉�.xls', source: data });
-}
-</script>
-
-<template>
-  <Modal :title="$t('ui.actionTitle.import', ['渚涘簲鍟�'])" class="w-1/3">
-    <Form class="mx-4">
-      <template #file>
-        <div class="w-full">
-          <Upload
-            :before-upload="beforeUpload"
-            :max-count="1"
-            accept=".xls,.xlsx"
-          >
-            <Button type="primary">閫夋嫨 Excel 鏂囦欢</Button>
-          </Upload>
-        </div>
-      </template>
-    </Form>
-    <template #prepend-footer>
-      <div class="flex flex-auto items-center">
-        <Button @click="handleDownload">涓嬭浇瀵煎叆妯℃澘</Button>
-      </div>
-    </template>
-  </Modal>
-</template>
diff --git a/src/views/mes/md/vendor/modules/item-receipt-detail.vue b/src/views/mes/md/vendor/modules/item-receipt-detail.vue
deleted file mode 100644
index 0fafb7a..0000000
--- a/src/views/mes/md/vendor/modules/item-receipt-detail.vue
+++ /dev/null
@@ -1,162 +0,0 @@
-<script lang="ts" setup>
-import type { VxeTableGridOptions } from '#/adapter/vxe-table';
-import type { MesWmItemReceiptApi } from '#/api/mes/wm/itemreceipt';
-import type { MesWmItemReceiptLineApi } from '#/api/mes/wm/itemreceipt/line';
-
-import { nextTick, ref } from 'vue';
-
-import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
-import { formatDateTime } from '../../../../../packages/utils/src';
-
-import { Descriptions, Spin } from 'ant-design-vue';
-
-import { useVbenVxeGrid } from '#/adapter/vxe-table';
-import { getItemReceipt } from '#/api/mes/wm/itemreceipt';
-import { getItemReceiptLinePage } from '#/api/mes/wm/itemreceipt/line';
-
-const loading = ref(false); // 璇︽儏鍔犺浇鐘舵��
-const receiptId = ref<number>(); // 褰撳墠閲囪喘鍏ュ簱鍗曠紪鍙�
-const receipt = ref<MesWmItemReceiptApi.ItemReceipt>(); // 褰撳墠閲囪喘鍏ュ簱鍗曡鎯�
-
-/** 鏍煎紡鍖栫┖鍊� */
-function formatEmpty(value: null | number | string | undefined) {
-  return value ?? '-';
-}
-
-/** 鏍煎紡鍖栨棩鏈� */
-function formatDate(value: Date | number | string | undefined) {
-  return value ? (formatDateTime(value) as string) : '-';
-}
-
-const [Grid, gridApi] = useVbenVxeGrid({
-  gridOptions: {
-    columns: [
-      {
-        field: 'itemCode',
-        title: '鐗╂枡缂栫爜',
-        width: 140,
-      },
-      {
-        field: 'itemName',
-        title: '鐗╂枡鍚嶇О',
-        minWidth: 150,
-      },
-      {
-        field: 'specification',
-        title: '瑙勬牸鍨嬪彿',
-        minWidth: 140,
-      },
-      {
-        field: 'unitMeasureName',
-        title: '鍗曚綅',
-        width: 100,
-      },
-      {
-        field: 'receivedQuantity',
-        title: '鍏ュ簱鏁伴噺',
-        width: 120,
-      },
-      {
-        field: 'batchCode',
-        title: '鎵规鍙�',
-        minWidth: 140,
-      },
-    ],
-    height: 280,
-    keepSource: true,
-    proxyConfig: {
-      ajax: {
-        query: async ({ page }) => {
-          if (!receiptId.value) {
-            return { list: [], total: 0 };
-          }
-          return await getItemReceiptLinePage({
-            pageNo: page.currentPage,
-            pageSize: page.pageSize,
-            receiptId: receiptId.value,
-          });
-        },
-      },
-    },
-    rowConfig: {
-      keyField: 'id',
-      isHover: true,
-    },
-    toolbarConfig: {
-      refresh: true,
-    },
-  } as VxeTableGridOptions<MesWmItemReceiptLineApi.ItemReceiptLine>,
-});
-
-const [Modal, modalApi] = useVbenModal({
-  async onOpenChange(isOpen: boolean) {
-    if (!isOpen) {
-      receiptId.value = undefined;
-      receipt.value = undefined;
-      return;
-    }
-    const data = modalApi.getData<{ id?: number }>();
-    if (!data?.id) {
-      return;
-    }
-    receiptId.value = data.id;
-    loading.value = true;
-    modalApi.lock();
-    try {
-      receipt.value = await getItemReceipt(data.id);
-      await nextTick();
-      await gridApi.query();
-    } finally {
-      loading.value = false;
-      modalApi.unlock();
-    }
-  },
-});
-</script>
-
-<template>
-  <Modal
-    title="閲囪喘鍏ュ簱鍗曡鎯�"
-    class="w-[900px]"
-    :show-cancel-button="false"
-    :show-confirm-button="false"
-  >
-    <Spin :spinning="loading">
-      <Descriptions bordered size="small" :column="3">
-        <Descriptions.Item label="鍏ュ簱鍗曠紪鍙�">
-          {{ formatEmpty(receipt?.code) }}
-        </Descriptions.Item>
-        <Descriptions.Item label="鍏ュ簱鍗曞悕绉�">
-          {{ formatEmpty(receipt?.name) }}
-        </Descriptions.Item>
-        <Descriptions.Item label="鍏ュ簱鏃ユ湡">
-          {{ formatDate(receipt?.receiptDate) }}
-        </Descriptions.Item>
-        <Descriptions.Item label="鍒拌揣閫氱煡鍗�">
-          {{ formatEmpty(receipt?.noticeCode) }}
-        </Descriptions.Item>
-        <Descriptions.Item label="渚涘簲鍟�">
-          {{ formatEmpty(receipt?.vendorName) }}
-        </Descriptions.Item>
-        <Descriptions.Item label="閲囪喘璁㈠崟鍙�">
-          {{ formatEmpty(receipt?.purchaseOrderCode) }}
-        </Descriptions.Item>
-        <Descriptions.Item label="浠撳簱">
-          {{ formatEmpty(receipt?.warehouseName) }}
-        </Descriptions.Item>
-        <Descriptions.Item label="搴撳尯">
-          {{ formatEmpty(receipt?.locationName) }}
-        </Descriptions.Item>
-        <Descriptions.Item label="搴撲綅">
-          {{ formatEmpty(receipt?.areaName) }}
-        </Descriptions.Item>
-        <Descriptions.Item label="澶囨敞" :span="3">
-          {{ formatEmpty(receipt?.remark) }}
-        </Descriptions.Item>
-      </Descriptions>
-      <div class="mt-4">
-        <Grid table-title="鍏ュ簱鐗╂枡" />
-      </div>
-    </Spin>
-  </Modal>
-</template>
diff --git a/src/views/mes/md/vendor/modules/item-receipt-line-list.vue b/src/views/mes/md/vendor/modules/item-receipt-line-list.vue
deleted file mode 100644
index 2811d89..0000000
--- a/src/views/mes/md/vendor/modules/item-receipt-line-list.vue
+++ /dev/null
@@ -1,95 +0,0 @@
-<script lang="ts" setup>
-import type { VxeTableGridOptions } from '#/adapter/vxe-table';
-import type { MesWmItemReceiptLineApi } from '#/api/mes/wm/itemreceipt/line';
-
-import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
-
-import { Button } from 'ant-design-vue';
-
-import { useVbenVxeGrid } from '#/adapter/vxe-table';
-import { getItemReceiptLinePage } from '#/api/mes/wm/itemreceipt/line';
-import ItemForm from '#/views/mes/md/item/modules/form.vue';
-
-const props = defineProps<{
-  vendorId: number;
-}>();
-
-const [ItemFormModal, itemFormModalApi] = useVbenModal({
-  connectedComponent: ItemForm,
-  destroyOnClose: true,
-});
-
-/** 鏌ョ湅鐗╂枡璇︽儏 */
-function handleViewItem(row: MesWmItemReceiptLineApi.ItemReceiptLine) {
-  itemFormModalApi.setData({ id: row.itemId, formType: 'detail' }).open();
-}
-
-const [Grid] = useVbenVxeGrid({
-  gridOptions: {
-    columns: [
-      {
-        field: 'itemCode',
-        title: '鐗╂枡缂栫爜',
-        width: 140,
-        slots: { default: 'itemCode' },
-      },
-      {
-        field: 'itemName',
-        title: '鐗╂枡鍚嶇О',
-        minWidth: 150,
-      },
-      {
-        field: 'specification',
-        title: '瑙勬牸鍨嬪彿',
-        minWidth: 140,
-      },
-      {
-        field: 'unitMeasureName',
-        title: '鍗曚綅',
-        width: 100,
-      },
-      {
-        field: 'receivedQuantity',
-        title: '鍏ュ簱鏁伴噺',
-        width: 120,
-      },
-      {
-        field: 'batchCode',
-        title: '鎵规鍙�',
-        minWidth: 140,
-      },
-    ],
-    height: 320,
-    keepSource: true,
-    proxyConfig: {
-      ajax: {
-        query: async ({ page }) => {
-          return await getItemReceiptLinePage({
-            pageNo: page.currentPage,
-            pageSize: page.pageSize,
-            vendorId: props.vendorId,
-          });
-        },
-      },
-    },
-    rowConfig: {
-      keyField: 'id',
-      isHover: true,
-    },
-    toolbarConfig: {
-      refresh: true,
-    },
-  } as VxeTableGridOptions<MesWmItemReceiptLineApi.ItemReceiptLine>,
-});
-</script>
-
-<template>
-  <ItemFormModal />
-  <Grid table-title="鐗╂枡娓呭崟">
-    <template #itemCode="{ row }">
-      <Button type="link" @click="handleViewItem(row)">
-        {{ row.itemCode }}
-      </Button>
-    </template>
-  </Grid>
-</template>
diff --git a/src/views/mes/md/vendor/modules/item-receipt-list.vue b/src/views/mes/md/vendor/modules/item-receipt-list.vue
deleted file mode 100644
index e712049..0000000
--- a/src/views/mes/md/vendor/modules/item-receipt-list.vue
+++ /dev/null
@@ -1,103 +0,0 @@
-<script lang="ts" setup>
-import type { VxeTableGridOptions } from '#/adapter/vxe-table';
-import type { MesWmItemReceiptLineApi } from '#/api/mes/wm/itemreceipt/line';
-
-import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
-
-import { Button } from 'ant-design-vue';
-
-import { useVbenVxeGrid } from '#/adapter/vxe-table';
-import { getItemReceiptLinePage } from '#/api/mes/wm/itemreceipt/line';
-
-import ItemReceiptDetail from './item-receipt-detail.vue';
-
-const props = defineProps<{
-  vendorId: number;
-}>();
-
-const [DetailModal, detailModalApi] = useVbenModal({
-  connectedComponent: ItemReceiptDetail,
-  destroyOnClose: true,
-});
-
-/** 鏌ョ湅閲囪喘鍏ュ簱鍗曡鎯� */
-function handleViewReceipt(row: MesWmItemReceiptLineApi.ItemReceiptLine) {
-  if (row.receiptId) {
-    detailModalApi.setData({ id: row.receiptId }).open();
-  }
-}
-
-const [Grid] = useVbenVxeGrid({
-  gridOptions: {
-    columns: [
-      {
-        field: 'receiptCode',
-        title: '鍏ュ簱鍗曠紪鍙�',
-        minWidth: 160,
-        slots: { default: 'receiptCode' },
-      },
-      {
-        field: 'purchaseOrderCode',
-        title: '閲囪喘璁㈠崟鍙�',
-        minWidth: 150,
-      },
-      {
-        field: 'itemCode',
-        title: '鐗╂枡缂栫爜',
-        width: 140,
-      },
-      {
-        field: 'itemName',
-        title: '鐗╂枡鍚嶇О',
-        minWidth: 150,
-      },
-      {
-        field: 'specification',
-        title: '瑙勬牸鍨嬪彿',
-        minWidth: 140,
-      },
-      {
-        field: 'unitMeasureName',
-        title: '鍗曚綅',
-        width: 100,
-      },
-      {
-        field: 'receivedQuantity',
-        title: '鍏ュ簱鏁伴噺',
-        width: 120,
-      },
-    ],
-    height: 320,
-    keepSource: true,
-    proxyConfig: {
-      ajax: {
-        query: async ({ page }) => {
-          return await getItemReceiptLinePage({
-            pageNo: page.currentPage,
-            pageSize: page.pageSize,
-            vendorId: props.vendorId,
-          });
-        },
-      },
-    },
-    rowConfig: {
-      keyField: 'id',
-      isHover: true,
-    },
-    toolbarConfig: {
-      refresh: true,
-    },
-  } as VxeTableGridOptions<MesWmItemReceiptLineApi.ItemReceiptLine>,
-});
-</script>
-
-<template>
-  <DetailModal />
-  <Grid table-title="閲囪喘璁板綍">
-    <template #receiptCode="{ row }">
-      <Button type="link" @click="handleViewReceipt(row)">
-        {{ row.receiptCode }}
-      </Button>
-    </template>
-  </Grid>
-</template>
diff --git a/src/views/mes/md/workstation/data.ts b/src/views/mes/md/workstation/data.ts
index 69d6170..d10750e 100644
--- a/src/views/mes/md/workstation/data.ts
+++ b/src/views/mes/md/workstation/data.ts
@@ -18,7 +18,7 @@
 import { getWarehouseSimpleList } from '#/api/mes/wm/warehouse';
 import { getWarehouseAreaSimpleList } from '#/api/mes/wm/warehouse/area';
 import { getWarehouseLocationSimpleList } from '#/api/mes/wm/warehouse/location';
-import { ProProcessSelect } from '#/views/mes/pro/process/components';
+import { ProProcessSelect } from '#/views/mes/process-design/process/components';
 
 import { MdWorkshopSelect } from './components';
 
diff --git a/src/views/mes/pd/archive/data.ts b/src/views/mes/pd/archive/data.ts
new file mode 100644
index 0000000..7146fcf
--- /dev/null
+++ b/src/views/mes/pd/archive/data.ts
@@ -0,0 +1,71 @@
+import type { VbenFormSchema } from "#/adapter/form";
+import type { VxeTableGridOptions } from "#/adapter/vxe-table";
+
+import { DICT_TYPE } from "#/packages/constants/src";
+import { getDictOptions } from "#/packages/effects/hooks/src";
+
+export function useGridFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: "code",
+      label: "褰掓。缂栧彿",
+      component: "Input",
+      componentProps: { placeholder: "璇疯緭鍏ュ綊妗g紪鍙�" },
+    },
+    {
+      fieldName: "projectCode",
+      label: "浠诲姟缂栫爜",
+      component: "Input",
+      componentProps: { placeholder: "璇疯緭鍏ヤ换鍔$紪鐮�" },
+    },
+    {
+      fieldName: "productCode",
+      label: "浜у搧缂栫爜",
+      component: "Input",
+      componentProps: { placeholder: "璇疯緭鍏ヤ骇鍝佺紪鐮�" },
+    },
+    {
+      fieldName: "archiveStatus",
+      label: "褰掓。鐘舵��",
+      component: "Select",
+      componentProps: {
+        allowClear: true,
+        options: getDictOptions(DICT_TYPE.MES_PD_ARCHIVE_STATUS, "number"),
+        placeholder: "璇烽�夋嫨褰掓。鐘舵��",
+      },
+    },
+  ];
+}
+
+export function useGridColumns(): VxeTableGridOptions["columns"] {
+  return [
+    {
+      field: "code",
+      title: "褰掓。缂栧彿",
+      minWidth: 130,
+      slots: { default: "code" },
+    },
+    { field: "projectCode", title: "浠诲姟缂栫爜", minWidth: 120 },
+    { field: "projectName", title: "浠诲姟鍚嶇О", minWidth: 140 },
+    { field: "productCode", title: "浜у搧缂栫爜", minWidth: 120 },
+    { field: "productName", title: "浜у搧鍚嶇О", minWidth: 140 },
+    { field: "version", title: "鐗堟湰鍙�", minWidth: 90 },
+    {
+      field: "archiveStatus",
+      title: "褰掓。鐘舵��",
+      minWidth: 100,
+      cellRender: {
+        name: "CellDict",
+        props: { type: DICT_TYPE.MES_PD_ARCHIVE_STATUS },
+      },
+    },
+    { field: "documentCount", title: "璧勬枡鏁�", minWidth: 80 },
+    { field: "archiverName", title: "褰掓。浜�", minWidth: 100 },
+    {
+      field: "archiveTime",
+      title: "褰掓。鏃堕棿",
+      minWidth: 160,
+      formatter: "formatDateTime",
+    },
+  ];
+}
diff --git a/src/views/mes/pd/archive/index.vue b/src/views/mes/pd/archive/index.vue
new file mode 100644
index 0000000..9949bdb
--- /dev/null
+++ b/src/views/mes/pd/archive/index.vue
@@ -0,0 +1,75 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesPdArchiveApi } from '#/api/mes/pd/archive';
+
+import { ref } from 'vue';
+
+import { Page } from '#/packages/effects/common-ui/src';
+import { downloadFileFromBlobPart } from '#/packages/utils/src';
+
+import { Button } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import { exportPdArchive, getPdArchivePage } from '#/api/mes/pd/archive';
+
+import { useGridColumns, useGridFormSchema } from './data';
+import Detail from './modules/detail.vue';
+
+const detailRef = ref<InstanceType<typeof Detail>>();
+
+function handleDetail(row: MesPdArchiveApi.Archive) {
+  detailRef.value?.open(row.id!);
+}
+
+async function handleExport() {
+  const data = await exportPdArchive(await gridApi.formApi.getValues());
+  downloadFileFromBlobPart({ fileName: '璁捐褰掓。鍙拌处.xls', source: data });
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  formOptions: { schema: useGridFormSchema() },
+  gridOptions: {
+    columns: useGridColumns(),
+    height: 'auto',
+    keepSource: true,
+    proxyConfig: {
+      ajax: {
+        query: async ({ page }, formValues) => {
+          return await getPdArchivePage({
+            pageNo: page.currentPage,
+            pageSize: page.pageSize,
+            ...formValues,
+          });
+        },
+      },
+    },
+    rowConfig: { keyField: 'id', isHover: true },
+    toolbarConfig: { refresh: true, search: true },
+  } as VxeTableGridOptions<MesPdArchiveApi.Archive>,
+});
+</script>
+
+<template>
+  <Page auto-content-height>
+    <Detail ref="detailRef" />
+
+    <Grid table-title="浜у搧鐗堟湰褰掓。鍙拌处">
+      <template #toolbar-tools>
+        <TableAction
+          :actions="[
+            {
+              label: '瀵煎嚭',
+              type: 'primary',
+              icon: ACTION_ICON.DOWNLOAD,
+              auth: ['mes:pd-archive:export'],
+              onClick: handleExport,
+            },
+          ]"
+        />
+      </template>
+      <template #code="{ row }">
+        <Button type="link" @click="handleDetail(row)">{{ row.code }}</Button>
+      </template>
+    </Grid>
+  </Page>
+</template>
diff --git a/src/views/mes/pd/archive/modules/detail.vue b/src/views/mes/pd/archive/modules/detail.vue
new file mode 100644
index 0000000..6b3875f
--- /dev/null
+++ b/src/views/mes/pd/archive/modules/detail.vue
@@ -0,0 +1,104 @@
+<script lang="ts" setup>
+import type { MesPdArchiveApi } from '#/api/mes/pd/archive';
+import type { MesPdVersionChangeApi } from '#/api/mes/pd/version-change';
+
+import { ref } from 'vue';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+
+import { Alert, Descriptions, Tabs } from 'ant-design-vue';
+
+import { getPdArchive } from '#/api/mes/pd/archive';
+
+import MaterialPanel from '#/views/mes/pd/project/modules/material-panel.vue';
+import VersionChangeList from '#/views/mes/pd/project/modules/version-change/version-change-list.vue';
+
+const detail = ref<MesPdArchiveApi.Archive>();
+const activeTab = ref('version-change');
+const materialVersion = ref<string>();
+const materialVersionHint = ref<string>();
+
+function handleViewMaterials(row: MesPdVersionChangeApi.VersionChange) {
+  materialVersion.value = row.toVersion;
+  materialVersionHint.value = `鍙樻洿鍗� ${row.code}锛�${row.fromVersion} 鈫� ${row.toVersion}`;
+  activeTab.value = 'material';
+}
+
+function clearMaterialVersionFilter() {
+  materialVersion.value = detail.value?.version;
+  materialVersionHint.value = undefined;
+}
+
+const [Modal, modalApi] = useVbenModal({
+  async onOpenChange(isOpen: boolean) {
+    if (!isOpen) {
+      detail.value = undefined;
+      activeTab.value = 'version-change';
+      materialVersion.value = undefined;
+      materialVersionHint.value = undefined;
+      return;
+    }
+    const data = modalApi.getData<{ id: number }>();
+    modalApi.lock();
+    try {
+      detail.value = await getPdArchive(data.id);
+      materialVersion.value = detail.value.version;
+    } finally {
+      modalApi.unlock();
+    }
+  },
+});
+
+defineExpose({
+  open: (id: number) => modalApi.setData({ id }).open(),
+});
+</script>
+
+<template>
+  <Modal class="w-4/5" title="褰掓。璇︽儏">
+    <Descriptions v-if="detail" :column="3" bordered class="mx-4 mb-4" size="small">
+      <Descriptions.Item label="褰掓。缂栧彿">{{ detail.code }}</Descriptions.Item>
+      <Descriptions.Item label="浜у搧缂栫爜">{{ detail.productCode }}</Descriptions.Item>
+      <Descriptions.Item label="浜у搧鍚嶇О">{{ detail.productName }}</Descriptions.Item>
+      <Descriptions.Item label="璁捐鐗堟湰">{{ detail.version }}</Descriptions.Item>
+      <Descriptions.Item label="浠诲姟缂栫爜">{{ detail.projectCode }}</Descriptions.Item>
+      <Descriptions.Item label="浠诲姟鍚嶇О">{{ detail.projectName }}</Descriptions.Item>
+      <Descriptions.Item label="璧勬枡鏁�">{{ detail.documentCount ?? '-' }}</Descriptions.Item>
+      <Descriptions.Item label="褰掓。浜�">{{ detail.archiverName || '-' }}</Descriptions.Item>
+      <Descriptions.Item label="褰掓。璺緞" :span="3">
+        {{ detail.storagePath || '-' }}
+      </Descriptions.Item>
+      <Descriptions.Item label="澶囨敞" :span="3">{{ detail.remark || '-' }}</Descriptions.Item>
+    </Descriptions>
+
+    <template v-if="detail?.projectId">
+      <Tabs v-model:active-key="activeTab" class="mx-4">
+        <Tabs.TabPane key="version-change" tab="鐗堟湰鍙樻洿璁板綍">
+          <VersionChangeList
+            form-type="detail"
+            :project-id="detail.projectId"
+            :project-version="detail.version"
+            @view-materials="handleViewMaterials"
+          />
+        </Tabs.TabPane>
+        <Tabs.TabPane key="material" tab="鐗堟湰璧勬枡">
+          <Alert
+            v-if="materialVersionHint"
+            class="mb-3"
+            closable
+            show-icon
+            type="info"
+            :message="`姝e湪鏌ョ湅 ${materialVersion} 鐗堟湰璧勬枡锛�${materialVersionHint}锛塦"
+            @close="clearMaterialVersionFilter"
+          />
+          <MaterialPanel
+            form-type="detail"
+            :project-id="detail.projectId"
+            table-title="鐗堟湰璧勬枡"
+            :version="materialVersion"
+          />
+        </Tabs.TabPane>
+      </Tabs>
+    </template>
+  </Modal>
+</template>
diff --git a/src/views/mes/pd/project/data.ts b/src/views/mes/pd/project/data.ts
new file mode 100644
index 0000000..9d9cac6
--- /dev/null
+++ b/src/views/mes/pd/project/data.ts
@@ -0,0 +1,535 @@
+import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesMdItemApi } from '#/api/mes/md/item';
+import type { MesPdProjectApi } from '#/api/mes/pd/project';
+
+import { h, markRaw } from 'vue';
+
+import {
+  DICT_TYPE,
+  MesAutoCodeRuleCode,
+  MesPdDocumentTypeEnum,
+} from '#/packages/constants/src';
+import { getDictOptions } from '#/packages/effects/hooks/src';
+
+import { Button } from 'ant-design-vue';
+
+import { generateAutoCode } from '#/api/mes/md/autocode/record';
+import { isPdMockEnabled } from '#/api/mes/pd/mock/enabled';
+import { mockNextCode } from '#/api/mes/pd/mock/utils';
+import { getRangePickerDefaultProps } from '#/utils';
+import { MdItemSelect } from '#/views/mes/md/item/components';
+
+export type FormType = 'create' | 'detail' | 'update';
+
+async function generateTaskCode(formApi?: VbenFormApi) {
+  const code = isPdMockEnabled()
+    ? mockNextCode('DT-2026', Math.floor(Date.now() % 10_000))
+    : await generateAutoCode(MesAutoCodeRuleCode.PD_PROJECT_CODE);
+  await formApi?.setFieldValue('code', code);
+}
+
+async function syncTaskNameFromProduct(
+  formApi: VbenFormApi | undefined,
+  productName?: string,
+) {
+  const version = (await formApi?.getFieldValue('version')) as
+    | string
+    | undefined;
+  const name = productName?.trim();
+  if (name && version?.trim()) {
+    await formApi?.setFieldValue('name', `${name} ${version.trim()} 璁捐`);
+  }
+}
+
+/** 浜у搧閫夋嫨锛氶�夋嫨鍚庤嚜鍔ㄥ甫鍑轰骇鍝佺紪鐮� */
+function useProductSelectSchema(
+  formApi: VbenFormApi | undefined,
+  readonly: boolean,
+): VbenFormSchema[] {
+  if (readonly) {
+    return [
+      {
+        fieldName: 'productCode',
+        label: '浜у搧缂栫爜',
+        component: 'Input',
+        componentProps: { disabled: true },
+      },
+      {
+        fieldName: 'productName',
+        label: '浜у搧鍚嶇О',
+        component: 'Input',
+        componentProps: { disabled: true },
+      },
+    ];
+  }
+  return [
+    {
+      fieldName: 'productId',
+      label: '浜у搧',
+      component: markRaw(MdItemSelect),
+      componentProps: {
+        placeholder: '璇烽�夋嫨浜у搧',
+        onChange: async (item: MesMdItemApi.Item | undefined) => {
+          await formApi?.setFieldValue('productCode', item?.code ?? '');
+          await formApi?.setFieldValue('productName', item?.name ?? '');
+          await syncTaskNameFromProduct(formApi, item?.name);
+        },
+      },
+      rules: 'selectRequired',
+    },
+    {
+      fieldName: 'productCode',
+      label: '浜у搧缂栫爜',
+      component: 'Input',
+      componentProps: {
+        disabled: true,
+        placeholder: '閫夋嫨浜у搧鍚庤嚜鍔ㄥ甫鍑�',
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'productName',
+      component: 'Input',
+      dependencies: { triggerFields: [''], show: () => false },
+    },
+  ];
+}
+
+/** 鍙戣捣璁捐浠诲姟琛ㄥ崟 */
+export function useCreateTaskSchema(formApi?: VbenFormApi): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: { triggerFields: [''], show: () => false },
+    },
+    {
+      fieldName: 'code',
+      label: '浠诲姟缂栫爜',
+      component: 'Input',
+      componentProps: { placeholder: '璇风偣鍑荤敓鎴愪换鍔$紪鐮�' },
+      rules: 'required',
+      suffix: () =>
+        h(
+          Button,
+          { type: 'default', onClick: () => generateTaskCode(formApi) },
+          { default: () => '鐢熸垚' },
+        ),
+    },
+    ...useProductSelectSchema(formApi, false),
+    {
+      fieldName: 'version',
+      label: '璁捐鐗堟湰',
+      component: 'Input',
+      componentProps: { placeholder: '濡� V1.0銆乂2.1' },
+      rules: 'required',
+    },
+    {
+      fieldName: 'name',
+      label: '浠诲姟鍚嶇О',
+      component: 'Input',
+      componentProps: { placeholder: '灏嗘牴鎹骇鍝佷笌鐗堟湰鑷姩鐢熸垚锛屽彲淇敼' },
+      rules: 'required',
+      dependencies: {
+        triggerFields: ['productName', 'version'],
+        trigger(values: Record<string, string>, form) {
+          const productName = values.productName?.trim();
+          const version = values.version?.trim();
+          if (productName && version) {
+            form.setFieldValue('name', `${productName} ${version} 璁捐`);
+          }
+        },
+      },
+    },
+    {
+      fieldName: 'designerName',
+      label: '涓昏璁″笀',
+      component: 'Input',
+      componentProps: { placeholder: '璇疯緭鍏ヤ富璁捐甯�' },
+      rules: 'required',
+    },
+    {
+      fieldName: 'reviewerName',
+      label: '瀹℃牳浜�',
+      component: 'Input',
+      componentProps: { placeholder: '璧勬枡瀹℃牳璐熻矗浜�' },
+      rules: 'required',
+    },
+    {
+      fieldName: 'planDateRange',
+      label: '璁″垝鍛ㄦ湡',
+      component: 'RangePicker',
+      componentProps: getRangePickerDefaultProps(),
+      rules: 'required',
+    },
+    {
+      fieldName: 'remark',
+      label: '浠诲姟璇存槑',
+      component: 'Textarea',
+      componentProps: {
+        placeholder: '绠�杩版湰娆¤璁$洰鏍囥�佸彉鏇磋儗鏅瓑',
+        rows: 3,
+      },
+      formItemClass: 'col-span-2',
+    },
+  ];
+}
+
+/** 鍙戝竷浠诲姟纭琛ㄥ崟 */
+export function usePublishFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'publishRemark',
+      label: '鍙戝竷璇存槑',
+      component: 'Textarea',
+      componentProps: {
+        placeholder: '鍙�夛紝濉啓鍙戝竷瑕佹眰鎴栨敞鎰忎簨椤�',
+        rows: 3,
+      },
+      formItemClass: 'col-span-2',
+    },
+  ];
+}
+
+export type WorkflowActionType = 'approve' | 'archive' | 'submit-review';
+
+export interface WorkflowActionConfig {
+  title: string;
+  alertMessage: string;
+  alertDescription: string;
+  remarkLabel: string;
+  remarkTag: string;
+  remarkPlaceholder: string;
+  successMessage: string;
+  alertType: 'info' | 'success' | 'warning';
+}
+
+export const WORKFLOW_ACTION_CONFIG: Record<
+  WorkflowActionType,
+  WorkflowActionConfig
+> = {
+  'submit-review': {
+    title: '鎻愪氦璧勬枡瀹℃牳',
+    alertMessage: '纭鎻愪氦瀹℃牳',
+    alertDescription:
+      '鎻愪氦鍚庡皢杩涘叆璧勬枡瀹℃牳鐜妭锛岃纭璁捐璧勬枡宸蹭笂浼犲畬鏁淬�傛彁浜ゅ悗璁捐甯堝皢鏃犳硶缁х画淇敼璧勬枡銆�',
+    remarkLabel: '鎻愪氦璇存槑',
+    remarkTag: '銆愭彁浜よ鏄庛��',
+    remarkPlaceholder: '鍙�夛紝濉啓鎻愪氦瀹℃牳鐨勮ˉ鍏呰鏄�',
+    successMessage: '宸叉彁浜よ祫鏂欏鏍�',
+    alertType: 'info',
+  },
+  approve: {
+    title: '瀹℃牳閫氳繃',
+    alertMessage: '纭瀹℃牳閫氳繃',
+    alertDescription:
+      '瀹℃牳閫氳繃鍚庝换鍔¤繘鍏ャ�屽凡閫氳繃銆嶇姸鎬侊紝鍙繘琛屽綊妗f搷浣溿�傝纭璁捐璧勬枡绗﹀悎瑕佹眰銆�',
+    remarkLabel: '瀹℃牳鎰忚',
+    remarkTag: '銆愬鏍告剰瑙併��',
+    remarkPlaceholder: '鍙�夛紝濉啓瀹℃牳閫氳繃鎰忚',
+    successMessage: '璧勬枡瀹℃牳閫氳繃',
+    alertType: 'success',
+  },
+  archive: {
+    title: '褰掓。璁捐浠诲姟',
+    alertMessage: '纭褰掓。',
+    alertDescription:
+      '褰掓。鍚庡皢鐢熸垚鏈増鏈綊妗e彴璐﹁褰曪紝浠诲姟鐘舵�佸彉涓哄凡褰掓。锛岃璁¤祫鏂欏皢閿佸畾涓嶅彲淇敼銆�',
+    remarkLabel: '褰掓。璇存槑',
+    remarkTag: '銆愬綊妗h鏄庛��',
+    remarkPlaceholder: '鍙�夛紝濉啓褰掓。澶囨敞',
+    successMessage: '浠诲姟宸插綊妗�',
+    alertType: 'warning',
+  },
+};
+
+/** 娴佺▼鎿嶄綔纭琛ㄥ崟 */
+export function useWorkflowActionFormSchema(
+  action: WorkflowActionType,
+): VbenFormSchema[] {
+  const config = WORKFLOW_ACTION_CONFIG[action];
+  return [
+    {
+      fieldName: 'actionRemark',
+      label: config.remarkLabel,
+      component: 'Textarea',
+      componentProps: {
+        placeholder: config.remarkPlaceholder,
+        rows: 3,
+      },
+      formItemClass: 'col-span-2',
+    },
+  ];
+}
+
+export function useGridFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'code',
+      label: '浠诲姟缂栫爜',
+      component: 'Input',
+      componentProps: { placeholder: '璇疯緭鍏ヤ换鍔$紪鐮�' },
+    },
+    {
+      fieldName: 'name',
+      label: '浠诲姟鍚嶇О',
+      component: 'Input',
+      componentProps: { placeholder: '璇疯緭鍏ヤ换鍔″悕绉�' },
+    },
+    {
+      fieldName: 'productCode',
+      label: '浜у搧缂栫爜',
+      component: 'Input',
+      componentProps: { placeholder: '璇疯緭鍏ヤ骇鍝佺紪鐮�' },
+    },
+    {
+      fieldName: 'status',
+      label: '浠诲姟鐘舵��',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: getDictOptions(DICT_TYPE.MES_PD_PROJECT_STATUS, 'number'),
+        placeholder: '璇烽�夋嫨椤圭洰鐘舵��',
+      },
+    },
+  ];
+}
+
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+  return [
+    { field: 'code', title: '浠诲姟缂栫爜', minWidth: 140, slots: { default: 'code' } },
+    { field: 'name', title: '浠诲姟鍚嶇О', minWidth: 160 },
+    { field: 'productCode', title: '浜у搧缂栫爜', minWidth: 120 },
+    { field: 'productName', title: '浜у搧鍚嶇О', minWidth: 140 },
+    { field: 'version', title: '鐗堟湰鍙�', minWidth: 90 },
+    {
+      field: 'status',
+      title: '鐘舵��',
+      minWidth: 100,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.MES_PD_PROJECT_STATUS },
+      },
+    },
+    { field: 'designerName', title: '涓昏璁″笀', minWidth: 100 },
+    { field: 'reviewerName', title: '瀹℃牳浜�', minWidth: 100 },
+    { field: 'planEndDate', title: '璁″垝瀹屾垚', minWidth: 120, formatter: 'formatDate' },
+    { field: 'createTime', title: '鍒涘缓鏃堕棿', minWidth: 160, formatter: 'formatDateTime' },
+    { field: 'action', title: '鎿嶄綔', width: 220, fixed: 'right', slots: { default: 'actions' } },
+  ];
+}
+
+export function useFormSchema(
+  formType: FormType,
+  formApi?: VbenFormApi,
+): VbenFormSchema[] {
+  const readonly = formType === 'detail';
+  return [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: { triggerFields: [''], show: () => false },
+    },
+    {
+      fieldName: 'code',
+      label: '浠诲姟缂栫爜',
+      component: 'Input',
+      componentProps: { disabled: readonly, placeholder: '璇疯緭鍏ヤ换鍔$紪鐮�' },
+      rules: 'required',
+      suffix:
+        formType === 'detail'
+          ? undefined
+          : () =>
+              h(
+                Button,
+                {
+                  type: 'default',
+                  onClick: async () => {
+                    const code = isPdMockEnabled()
+                      ? mockNextCode(
+                          'DT-2026',
+                          Math.floor(Date.now() % 10_000),
+                        )
+                      : await generateAutoCode(
+                          MesAutoCodeRuleCode.PD_PROJECT_CODE,
+                        );
+                    await formApi?.setFieldValue('code', code);
+                  },
+                },
+                { default: () => '鐢熸垚' },
+              ),
+    },
+    ...useProductSelectSchema(formApi, readonly),
+    {
+      fieldName: 'version',
+      label: '鐗堟湰鍙�',
+      component: 'Input',
+      componentProps: { disabled: readonly, placeholder: '濡� V1.0' },
+      rules: 'required',
+    },
+    {
+      fieldName: 'name',
+      label: '浠诲姟鍚嶇О',
+      component: 'Input',
+      componentProps: { disabled: readonly, placeholder: '濡傦細鏅鸿兘娓╂帶妯$粍 V1.0 璁捐' },
+      rules: 'required',
+      dependencies: {
+        triggerFields: ['productName', 'version'],
+        trigger(values: Record<string, string>, form) {
+          if (readonly) {
+            return;
+          }
+          const productName = values.productName?.trim();
+          const version = values.version?.trim();
+          if (productName && version) {
+            form.setFieldValue('name', `${productName} ${version} 璁捐`);
+          }
+        },
+      },
+    },
+    {
+      fieldName: 'status',
+      label: '浠诲姟鐘舵��',
+      component: 'Select',
+      componentProps: {
+        disabled: true,
+        options: getDictOptions(DICT_TYPE.MES_PD_PROJECT_STATUS, 'number'),
+      },
+      dependencies: { triggerFields: [''], show: () => formType !== 'create' },
+    },
+    {
+      fieldName: 'designerName',
+      label: '涓昏璁″笀',
+      component: 'Input',
+      componentProps: { disabled: readonly, placeholder: '璇疯緭鍏ヤ富璁捐甯�' },
+    },
+    {
+      fieldName: 'reviewerName',
+      label: '瀹℃牳浜�',
+      component: 'Input',
+      componentProps: { disabled: readonly, placeholder: '璇疯緭鍏ュ鏍镐汉' },
+    },
+    {
+      fieldName: 'planDateRange',
+      label: '璁″垝鍛ㄦ湡',
+      component: 'RangePicker',
+      componentProps: {
+        ...getRangePickerDefaultProps(),
+        disabled: readonly,
+      },
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      componentProps: { disabled: readonly, placeholder: '璇疯緭鍏ュ娉�', rows: 3 },
+      formItemClass: 'col-span-3',
+    },
+  ];
+}
+
+/** 璁捐璧勬枡绫诲瀷閫夐」锛堜笂浼犳椂閫夋嫨锛� */
+export function getPdDocumentTypeOptions() {
+  return [
+    { label: '璁捐鏂规', value: MesPdDocumentTypeEnum.SCHEME },
+    { label: '璁捐鍥剧焊', value: MesPdDocumentTypeEnum.DRAWING },
+    { label: '璁捐妯″瀷', value: MesPdDocumentTypeEnum.MODEL },
+    { label: '浜у搧璁捐', value: MesPdDocumentTypeEnum.PRODUCT_DESIGN },
+    { label: 'BOM 娓呭崟', value: MesPdDocumentTypeEnum.BOM },
+    { label: '鍏朵粬闄勪欢', value: MesPdDocumentTypeEnum.OTHER },
+  ];
+}
+
+/** 缁熶竴璁捐璧勬枡鍒楄〃鍒� */
+export function useMaterialListGridFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'name',
+      label: '璧勬枡鍚嶇О',
+      component: 'Input',
+      componentProps: { placeholder: '璇疯緭鍏ヨ祫鏂欏悕绉�' },
+    },
+    {
+      fieldName: 'documentType',
+      label: '璧勬枡绫诲瀷',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: getPdDocumentTypeOptions(),
+        placeholder: '璇烽�夋嫨璧勬枡绫诲瀷',
+      },
+    },
+  ];
+}
+
+export function useMaterialListGridColumns(): VxeTableGridOptions['columns'] {
+  return [
+    {
+      field: 'documentType',
+      title: '璧勬枡绫诲瀷',
+      minWidth: 110,
+      formatter: ({ cellValue }: { cellValue: number }) => {
+        const option = getPdDocumentTypeOptions().find(
+          (item) => item.value === cellValue,
+        );
+        return option?.label ?? String(cellValue ?? '');
+      },
+    },
+    { field: 'name', title: '璧勬枡鍚嶇О', minWidth: 180 },
+    { field: 'version', title: '鐗堟湰', minWidth: 90 },
+    { field: 'fileName', title: '鏂囦欢鍚�', minWidth: 200 },
+    { field: 'uploaderName', title: '涓婁紶浜�', minWidth: 100 },
+    {
+      field: 'createTime',
+      title: '涓婁紶鏃堕棿',
+      minWidth: 160,
+      formatter: 'formatDateTime',
+    },
+    {
+      field: 'action',
+      title: '鎿嶄綔',
+      width: 140,
+      fixed: 'right',
+      slots: { default: 'actions' },
+    },
+  ];
+}
+
+/** @deprecated 鎸夌被鍨嬪垎 Tab 鏃朵娇鐢紝璇锋敼鐢� useMaterialListGridColumns */
+export function useDocumentGridColumns(documentType: number): VxeTableGridOptions['columns'] {
+  const typeLabel =
+    documentType === MesPdDocumentTypeEnum.SCHEME
+      ? '鏂规'
+      : documentType === MesPdDocumentTypeEnum.DRAWING
+        ? '鍥剧焊'
+        : documentType === MesPdDocumentTypeEnum.MODEL
+          ? '妯″瀷'
+          : documentType === MesPdDocumentTypeEnum.PRODUCT_DESIGN
+            ? '浜у搧璁捐'
+            : '璧勬枡';
+
+  return [
+    { field: 'name', title: `${typeLabel}鍚嶇О`, minWidth: 180 },
+    { field: 'version', title: '鐗堟湰', minWidth: 90 },
+    { field: 'fileName', title: '鏂囦欢鍚�', minWidth: 200 },
+    { field: 'uploaderName', title: '涓婁紶浜�', minWidth: 100 },
+    { field: 'createTime', title: '涓婁紶鏃堕棿', minWidth: 160, formatter: 'formatDateTime' },
+    { field: 'action', title: '鎿嶄綔', width: 140, slots: { default: 'actions' } },
+  ];
+}
+
+/** BOM 琛ㄦ牸鍒� */
+export function useBomGridColumns(): VxeTableGridOptions['columns'] {
+  return [
+    { field: 'itemCode', title: '鐗╂枡缂栫爜', minWidth: 120 },
+    { field: 'itemName', title: '鐗╂枡鍚嶇О', minWidth: 160 },
+    { field: 'specification', title: '瑙勬牸鍨嬪彿', minWidth: 140 },
+    { field: 'unitMeasureName', title: '鍗曚綅', minWidth: 80 },
+    { field: 'quantity', title: '鐢ㄩ噺', minWidth: 90 },
+    { field: 'remark', title: '澶囨敞', minWidth: 140 },
+    { field: 'action', title: '鎿嶄綔', width: 140, slots: { default: 'actions' } },
+  ];
+}
+
+export type { MesPdProjectApi };
diff --git a/src/views/mes/pd/project/detail/data.ts b/src/views/mes/pd/project/detail/data.ts
new file mode 100644
index 0000000..5ad04da
--- /dev/null
+++ b/src/views/mes/pd/project/detail/data.ts
@@ -0,0 +1,43 @@
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { h } from 'vue';
+
+import { DICT_TYPE } from '#/packages/constants/src';
+import { formatDate, formatDateTime } from '#/packages/utils/src';
+
+import { DictTag } from '#/components/dict-tag';
+
+/** 璁捐浠诲姟璇︽儏鎽樿 */
+export function useDetailSchema(): DescriptionItemSchema[] {
+  return [
+    { field: 'code', label: '浠诲姟缂栫爜' },
+    { field: 'name', label: '浠诲姟鍚嶇О' },
+    { field: 'productCode', label: '浜у搧缂栫爜' },
+    { field: 'productName', label: '浜у搧鍚嶇О' },
+    { field: 'version', label: '璁捐鐗堟湰' },
+    {
+      field: 'status',
+      label: '浠诲姟鐘舵��',
+      render: (val) =>
+        h(DictTag, { type: DICT_TYPE.MES_PD_PROJECT_STATUS, value: val }),
+    },
+    { field: 'designerName', label: '璁捐甯�' },
+    { field: 'reviewerName', label: '瀹℃牳浜�' },
+    {
+      field: 'planStartDate',
+      label: '璁″垝寮�濮�',
+      render: (val) => (val ? formatDate(val) : '-'),
+    },
+    {
+      field: 'planEndDate',
+      label: '璁″垝瀹屾垚',
+      render: (val) => (val ? formatDate(val) : '-'),
+    },
+    {
+      field: 'createTime',
+      label: '鍙戣捣鏃堕棿',
+      render: (val) => (val ? formatDateTime(val) : '-'),
+    },
+    { field: 'remark', label: '澶囨敞' },
+  ];
+}
diff --git a/src/views/mes/pd/project/detail/index.vue b/src/views/mes/pd/project/detail/index.vue
new file mode 100644
index 0000000..ea6bf27
--- /dev/null
+++ b/src/views/mes/pd/project/detail/index.vue
@@ -0,0 +1,224 @@
+<script lang="ts" setup>
+import type { FormType } from '../data';
+import type { MesPdProjectApi } from '#/api/mes/pd/project';
+
+import { computed, onMounted, ref } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+
+import { Page, useVbenModal } from '#/packages/effects/common-ui/src';
+import { useTabs } from '#/packages/effects/hooks/src';
+import { MesPdProjectStatusEnum } from '#/packages/constants/src';
+
+import { Card, Tabs } from 'ant-design-vue';
+
+import {
+  getPdProject,
+} from '#/api/mes/pd/project';
+import { useDescription } from '#/components/description';
+import { ACTION_ICON, TableAction } from '#/adapter/vxe-table';
+import { $t } from '#/locales';
+
+import Form from '../modules/form.vue';
+import PublishForm from '../modules/publish-form.vue';
+import WorkflowActionForm from '../modules/workflow-action-form.vue';
+import ReviewList from '../modules/drawing-review/review-list.vue';
+import MaterialPanel from '../modules/material-panel.vue';
+import TaskDraftBanner from '../modules/task-draft-banner.vue';
+import WorkflowTimeline from '../modules/workflow-timeline.vue';
+import { useDetailSchema } from './data';
+
+const route = useRoute();
+const router = useRouter();
+const tabs = useTabs();
+
+const loading = ref(false);
+const projectId = ref(0);
+const project = ref<MesPdProjectApi.Project>({});
+const activeTab = ref('workflow');
+const workflowKey = ref(0);
+
+const isDraft = computed(
+  () => project.value.status === MesPdProjectStatusEnum.DRAFT,
+);
+const isDesigning = computed(
+  () => project.value.status === MesPdProjectStatusEnum.DESIGNING,
+);
+const isReviewing = computed(
+  () => project.value.status === MesPdProjectStatusEnum.REVIEWING,
+);
+const isApproved = computed(
+  () => project.value.status === MesPdProjectStatusEnum.APPROVED,
+);
+const isArchived = computed(
+  () => project.value.status === MesPdProjectStatusEnum.ARCHIVED,
+);
+
+const isFormEditable = computed(
+  () =>
+    !isArchived.value &&
+    project.value.status !== MesPdProjectStatusEnum.CANCELLED &&
+    (isDraft.value || isDesigning.value),
+);
+
+const materialFormType = computed<FormType>(() =>
+  isDesigning.value ? 'update' : 'detail',
+);
+
+const showReviewTab = computed(
+  () => isReviewing.value || isApproved.value || isArchived.value,
+);
+
+const [Descriptions] = useDescription({
+  bordered: false,
+  column: 4,
+  schema: useDetailSchema(),
+});
+
+const [FormModal, formModalApi] = useVbenModal({
+  connectedComponent: Form,
+  destroyOnClose: true,
+});
+
+const [PublishModal, publishModalApi] = useVbenModal({
+  connectedComponent: PublishForm,
+  destroyOnClose: true,
+});
+
+const [WorkflowActionModal, workflowActionModalApi] = useVbenModal({
+  connectedComponent: WorkflowActionForm,
+  destroyOnClose: true,
+});
+
+async function loadProjectDetail() {
+  loading.value = true;
+  try {
+    project.value = await getPdProject(projectId.value);
+    workflowKey.value += 1;
+    if (isDesigning.value) {
+      activeTab.value = 'material';
+    } else if (isReviewing.value) {
+      activeTab.value = 'review';
+    }
+  } finally {
+    loading.value = false;
+  }
+}
+
+function handleBack() {
+  tabs.closeCurrentTab();
+  router.push({ path: '/mes/pd/project' });
+}
+
+function handleEdit() {
+  formModalApi.setData({ formType: 'update', id: projectId.value }).open();
+}
+
+function handlePublish() {
+  publishModalApi.setData({ id: projectId.value }).open();
+}
+
+function handleWorkflowAction(action: 'approve' | 'archive' | 'submit-review') {
+  workflowActionModalApi.setData({ id: projectId.value, action }).open();
+}
+
+onMounted(() => {
+  projectId.value = Number(route.params.id);
+  loadProjectDetail();
+});
+</script>
+
+<template>
+  <Page
+    auto-content-height
+    :loading="loading"
+    :title="project.name || '璁捐浠诲姟璇︽儏'"
+  >
+    <FormModal @success="loadProjectDetail" />
+    <PublishModal @success="loadProjectDetail" />
+    <WorkflowActionModal @success="loadProjectDetail" />
+
+    <template #extra>
+      <TableAction
+        :actions="[
+          {
+            label: '杩斿洖',
+            type: 'default',
+            icon: 'lucide:arrow-left',
+            onClick: handleBack,
+          },
+          {
+            label: $t('common.edit'),
+            type: 'primary',
+            icon: ACTION_ICON.EDIT,
+            auth: ['mes:pd-project:update'],
+            ifShow: isFormEditable,
+            onClick: handleEdit,
+          },
+          {
+            label: '鍙戝竷浠诲姟',
+            type: 'primary',
+            auth: ['mes:pd-project:update'],
+            ifShow: isDraft,
+            onClick: handlePublish,
+          },
+          {
+            label: '鎻愪氦瀹℃牳',
+            type: 'default',
+            auth: ['mes:pd-project:submit-review'],
+            ifShow: isDesigning,
+            onClick: () => handleWorkflowAction('submit-review'),
+          },
+          {
+            label: '瀹℃牳閫氳繃',
+            type: 'primary',
+            auth: ['mes:pd-drawing-review:review'],
+            ifShow: isReviewing,
+            onClick: () => handleWorkflowAction('approve'),
+          },
+          {
+            label: '褰掓。',
+            type: 'default',
+            auth: ['mes:pd-project:archive'],
+            ifShow: isApproved,
+            onClick: () => handleWorkflowAction('archive'),
+          },
+        ]"
+      />
+    </template>
+
+    <TaskDraftBanner
+      v-if="isDraft"
+      :designer-name="project.designerName"
+      :product-name="project.productName"
+      :version="project.version"
+      @edit="handleEdit"
+      @publish="handlePublish"
+    />
+
+    <Card class="min-h-[10%]">
+      <Descriptions :data="project" />
+    </Card>
+
+    <Card class="mt-4 min-h-[60%]">
+      <Tabs v-model:active-key="activeTab">
+        <Tabs.TabPane key="workflow" tab="娴佺▼杩涘害" :force-render="true">
+          <WorkflowTimeline :key="workflowKey" :project-id="projectId" />
+        </Tabs.TabPane>
+        <Tabs.TabPane key="material" tab="璁捐璧勬枡" :force-render="true">
+          <p v-if="isDraft" class="mb-3 text-sm text-amber-600">
+            璇峰厛鍙戝竷浠诲姟鍚庡啀涓婁紶璁捐璧勬枡
+          </p>
+          <MaterialPanel :form-type="materialFormType" :project-id="projectId" />
+        </Tabs.TabPane>
+        <Tabs.TabPane
+          v-if="showReviewTab"
+          key="review"
+          tab="璧勬枡瀹℃牳"
+          :force-render="true"
+        >
+          <ReviewList form-type="detail" :project-id="projectId" />
+        </Tabs.TabPane>
+      </Tabs>
+    </Card>
+  </Page>
+</template>
diff --git a/src/views/mes/pd/project/index.vue b/src/views/mes/pd/project/index.vue
new file mode 100644
index 0000000..1428d31
--- /dev/null
+++ b/src/views/mes/pd/project/index.vue
@@ -0,0 +1,212 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesPdProjectApi } from '#/api/mes/pd/project';
+
+import { useRouter } from 'vue-router';
+
+import { Page, useVbenModal } from '#/packages/effects/common-ui/src';
+import { MesPdProjectStatusEnum } from '#/packages/constants/src';
+import { downloadFileFromBlobPart } from '#/packages/utils/src';
+
+import { Button, message } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+  deletePdProject,
+  exportPdProject,
+  getPdProjectPage,
+} from '#/api/mes/pd/project';
+import { $t } from '#/locales';
+
+import { useGridColumns, useGridFormSchema } from './data';
+import CreateForm from './modules/create-form.vue';
+import Form from './modules/form.vue';
+import PublishForm from './modules/publish-form.vue';
+import WorkflowActionForm from './modules/workflow-action-form.vue';
+
+const router = useRouter();
+
+const [CreateFormModal, createFormModalApi] = useVbenModal({
+  connectedComponent: CreateForm,
+  destroyOnClose: true,
+});
+
+const [FormModal, formModalApi] = useVbenModal({
+  connectedComponent: Form,
+  destroyOnClose: true,
+});
+
+const [PublishModal, publishModalApi] = useVbenModal({
+  connectedComponent: PublishForm,
+  destroyOnClose: true,
+});
+
+const [WorkflowActionModal, workflowActionModalApi] = useVbenModal({
+  connectedComponent: WorkflowActionForm,
+  destroyOnClose: true,
+});
+
+function handleRefresh() {
+  gridApi.query();
+}
+
+function handleCreateSuccess(projectId: number) {
+  handleRefresh();
+  router.push({ name: 'MesPdProjectDetail', params: { id: projectId } });
+}
+
+function handleCreate() {
+  createFormModalApi.open();
+}
+
+function handleDetail(row: MesPdProjectApi.Project) {
+  router.push({ name: 'MesPdProjectDetail', params: { id: row.id } });
+}
+
+function handleEdit(row: MesPdProjectApi.Project) {
+  formModalApi.setData({ formType: 'update', id: row.id }).open();
+}
+
+function handlePublish(row: MesPdProjectApi.Project) {
+  publishModalApi.setData({ id: row.id }).open();
+}
+
+function handleWorkflowAction(
+  row: MesPdProjectApi.Project,
+  action: 'approve' | 'archive' | 'submit-review',
+) {
+  workflowActionModalApi.setData({ id: row.id, action }).open();
+}
+
+async function handleDelete(row: MesPdProjectApi.Project) {
+  const hideLoading = message.loading({
+    content: $t('ui.actionMessage.deleting', [row.name]),
+    duration: 0,
+  });
+  try {
+    await deletePdProject(row.id!);
+    message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
+    handleRefresh();
+  } finally {
+    hideLoading();
+  }
+}
+
+async function handleExport() {
+  const data = await exportPdProject(await gridApi.formApi.getValues());
+  downloadFileFromBlobPart({ fileName: '璁捐浠诲姟.xls', source: data });
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  formOptions: { schema: useGridFormSchema() },
+  gridOptions: {
+    columns: useGridColumns(),
+    height: 'auto',
+    keepSource: true,
+    proxyConfig: {
+      ajax: {
+        query: async ({ page }, formValues) => {
+          return await getPdProjectPage({
+            pageNo: page.currentPage,
+            pageSize: page.pageSize,
+            ...formValues,
+          });
+        },
+      },
+    },
+    rowConfig: { keyField: 'id', isHover: true },
+    toolbarConfig: { refresh: true, search: true },
+  } as VxeTableGridOptions<MesPdProjectApi.Project>,
+});
+</script>
+
+<template>
+  <Page auto-content-height>
+    <CreateFormModal @success="handleCreateSuccess" />
+    <FormModal @success="handleRefresh" />
+    <PublishModal @success="handleRefresh" />
+    <WorkflowActionModal @success="handleRefresh" />
+
+    <Grid table-title="璁捐浠诲姟">
+      <template #toolbar-tools>
+        <TableAction
+          :actions="[
+            {
+              label: '鍙戣捣璁捐浠诲姟',
+              type: 'primary',
+              icon: ACTION_ICON.ADD,
+              auth: ['mes:pd-project:create'],
+              onClick: handleCreate,
+            },
+            {
+              label: $t('ui.actionTitle.export'),
+              type: 'primary',
+              icon: ACTION_ICON.DOWNLOAD,
+              auth: ['mes:pd-project:export'],
+              onClick: handleExport,
+            },
+          ]"
+        />
+      </template>
+      <template #code="{ row }">
+        <Button type="link" @click="handleDetail(row)">{{ row.code }}</Button>
+      </template>
+      <template #actions="{ row }">
+        <TableAction
+          :actions="[
+            {
+              label: '鍙戝竷浠诲姟',
+              type: 'link',
+              auth: ['mes:pd-project:update'],
+              ifShow: row.status === MesPdProjectStatusEnum.DRAFT,
+              onClick: handlePublish.bind(null, row),
+            },
+            {
+              label: $t('common.edit'),
+              type: 'link',
+              icon: ACTION_ICON.EDIT,
+              auth: ['mes:pd-project:update'],
+              ifShow:
+                row.status === MesPdProjectStatusEnum.DRAFT ||
+                row.status === MesPdProjectStatusEnum.DESIGNING,
+              onClick: handleEdit.bind(null, row),
+            },
+            {
+              label: '鎻愪氦瀹℃牳',
+              type: 'link',
+              auth: ['mes:pd-project:submit-review'],
+              ifShow: row.status === MesPdProjectStatusEnum.DESIGNING,
+              onClick: () => handleWorkflowAction(row, 'submit-review'),
+            },
+            {
+              label: '瀹℃牳閫氳繃',
+              type: 'link',
+              auth: ['mes:pd-drawing-review:review'],
+              ifShow: row.status === MesPdProjectStatusEnum.REVIEWING,
+              onClick: () => handleWorkflowAction(row, 'approve'),
+            },
+            {
+              label: '褰掓。',
+              type: 'link',
+              auth: ['mes:pd-project:archive'],
+              ifShow: row.status === MesPdProjectStatusEnum.APPROVED,
+              onClick: () => handleWorkflowAction(row, 'archive'),
+            },
+            {
+              label: $t('common.delete'),
+              type: 'link',
+              danger: true,
+              icon: ACTION_ICON.DELETE,
+              auth: ['mes:pd-project:delete'],
+              ifShow: row.status === MesPdProjectStatusEnum.DRAFT,
+              popConfirm: {
+                title: $t('ui.actionMessage.deleteConfirm', [row.name]),
+                confirm: handleDelete.bind(null, row),
+              },
+            },
+          ]"
+        />
+      </template>
+    </Grid>
+  </Page>
+</template>
diff --git a/src/views/mes/pd/project/modules/bom-form.vue b/src/views/mes/pd/project/modules/bom-form.vue
new file mode 100644
index 0000000..7699f06
--- /dev/null
+++ b/src/views/mes/pd/project/modules/bom-form.vue
@@ -0,0 +1,124 @@
+<script lang="ts" setup>
+import type { MesMdItemApi } from '#/api/mes/md/item';
+import type { MesPdBomApi } from '#/api/mes/pd/bom';
+
+import { ref } from 'vue';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { createPdBomLine, updatePdBomLine } from '#/api/mes/pd/bom';
+import { $t } from '#/locales';
+import { MdItemSelect } from '#/views/mes/md/item/components';
+
+const emit = defineEmits(['success']);
+
+const isUpdate = ref(false);
+
+const [Form, formApi] = useVbenForm({
+  commonConfig: {
+    componentProps: { class: 'w-full' },
+    labelWidth: 100,
+  },
+  layout: 'horizontal',
+  schema: [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: { triggerFields: [''], show: () => false },
+    },
+    {
+      fieldName: 'projectId',
+      component: 'Input',
+      dependencies: { triggerFields: [''], show: () => false },
+    },
+    {
+      fieldName: 'itemId',
+      label: '鐗╂枡',
+      component: MdItemSelect,
+      componentProps: {
+        placeholder: '璇烽�夋嫨鐗╂枡',
+        onChange: async (item: MesMdItemApi.Item | undefined) => {
+          await formApi.setValues({
+            itemCode: item?.code,
+            itemName: item?.name,
+            specification: item?.specification,
+            unitMeasureName: item?.unitMeasureName,
+          });
+        },
+      },
+      rules: 'selectRequired',
+    },
+    {
+      fieldName: 'itemCode',
+      label: '鐗╂枡缂栫爜',
+      component: 'Input',
+      componentProps: { disabled: true },
+    },
+    {
+      fieldName: 'itemName',
+      label: '鐗╂枡鍚嶇О',
+      component: 'Input',
+      componentProps: { disabled: true },
+    },
+    {
+      fieldName: 'quantity',
+      label: '鐢ㄩ噺',
+      component: 'InputNumber',
+      componentProps: { min: 0, precision: 4, class: 'w-full' },
+      rules: 'required',
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      componentProps: { placeholder: '璇疯緭鍏ュ娉�', rows: 2 },
+    },
+  ],
+  showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    const { valid } = await formApi.validate();
+    if (!valid) {
+      return;
+    }
+    modalApi.lock();
+    const data = (await formApi.getValues()) as MesPdBomApi.BomLine;
+    try {
+      if (isUpdate.value) {
+        await updatePdBomLine(data);
+      } else {
+        await createPdBomLine(data);
+      }
+      emit('success');
+      message.success($t('ui.actionMessage.operationSuccess'));
+      await modalApi.close();
+    } finally {
+      modalApi.unlock();
+    }
+  },
+  async onOpenChange(isOpen: boolean) {
+    if (!isOpen) {
+      return;
+    }
+    const data = modalApi.getData<{ projectId: number; row?: MesPdBomApi.BomLine }>();
+    isUpdate.value = !!data.row?.id;
+    modalApi.setState({ title: isUpdate.value ? '缂栬緫 BOM 琛�' : '娣诲姞 BOM 琛�' });
+    if (data.row) {
+      await formApi.setValues(data.row);
+      return;
+    }
+    await formApi.setValues({ projectId: data.projectId, quantity: 1 });
+  },
+});
+</script>
+
+<template>
+  <Modal class="w-[560px]">
+    <Form class="mx-4" />
+  </Modal>
+</template>
diff --git a/src/views/mes/pd/project/modules/bom-list.vue b/src/views/mes/pd/project/modules/bom-list.vue
new file mode 100644
index 0000000..a6874ca
--- /dev/null
+++ b/src/views/mes/pd/project/modules/bom-list.vue
@@ -0,0 +1,122 @@
+<script lang="ts" setup>
+import type { FormType } from '../data';
+
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesPdBomApi } from '#/api/mes/pd/bom';
+
+import { computed } from 'vue';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+
+import { message } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import { deletePdBomLine, getPdBomPage } from '#/api/mes/pd/bom';
+import { $t } from '#/locales';
+
+import { useBomGridColumns } from '../data';
+import BomForm from './bom-form.vue';
+
+const props = defineProps<{
+  formType: FormType;
+  projectId: number;
+}>();
+
+const isEditable = computed(() =>
+  ['create', 'update'].includes(props.formType),
+);
+
+const [BomFormModal, bomFormModalApi] = useVbenModal({
+  connectedComponent: BomForm,
+  destroyOnClose: true,
+});
+
+function handleRefresh() {
+  gridApi.query();
+}
+
+function handleCreate() {
+  bomFormModalApi.setData({ projectId: props.projectId }).open();
+}
+
+function handleEdit(row: MesPdBomApi.BomLine) {
+  bomFormModalApi.setData({ row, projectId: props.projectId }).open();
+}
+
+async function handleDelete(row: MesPdBomApi.BomLine) {
+  const hideLoading = message.loading({
+    content: $t('ui.actionMessage.deleting', [row.itemName]),
+    duration: 0,
+  });
+  try {
+    await deletePdBomLine(row.id!);
+    message.success($t('ui.actionMessage.deleteSuccess', [row.itemName]));
+    handleRefresh();
+  } finally {
+    hideLoading();
+  }
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  gridOptions: {
+    columns: useBomGridColumns(),
+    height: 360,
+    keepSource: true,
+    proxyConfig: {
+      ajax: {
+        query: async ({ page }) => {
+          return await getPdBomPage({
+            pageNo: page.currentPage,
+            pageSize: page.pageSize,
+            projectId: props.projectId,
+          });
+        },
+      },
+    },
+    rowConfig: { keyField: 'id', isHover: true },
+    toolbarConfig: { refresh: true },
+  } as VxeTableGridOptions<MesPdBomApi.BomLine>,
+});
+</script>
+
+<template>
+  <BomFormModal @success="handleRefresh" />
+  <Grid>
+    <template v-if="isEditable" #toolbar-tools>
+      <TableAction
+        :actions="[
+          {
+            label: '娣诲姞 BOM 琛�',
+            type: 'primary',
+            icon: ACTION_ICON.ADD,
+            onClick: handleCreate,
+          },
+        ]"
+      />
+    </template>
+    <template #actions="{ row }">
+      <TableAction
+        :actions="[
+          {
+            label: $t('common.edit'),
+            type: 'link',
+            icon: ACTION_ICON.EDIT,
+            ifShow: isEditable,
+            onClick: handleEdit.bind(null, row),
+          },
+          {
+            label: $t('common.delete'),
+            type: 'link',
+            danger: true,
+            icon: ACTION_ICON.DELETE,
+            ifShow: isEditable,
+            popConfirm: {
+              title: $t('ui.actionMessage.deleteConfirm', [row.itemName]),
+              confirm: handleDelete.bind(null, row),
+            },
+          },
+        ]"
+      />
+    </template>
+  </Grid>
+</template>
diff --git a/src/views/mes/pd/project/modules/create-form.vue b/src/views/mes/pd/project/modules/create-form.vue
new file mode 100644
index 0000000..0663079
--- /dev/null
+++ b/src/views/mes/pd/project/modules/create-form.vue
@@ -0,0 +1,77 @@
+<script lang="ts" setup>
+import type { MesPdProjectApi } from '#/api/mes/pd/project';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+
+import { Alert, message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { createPdProject } from '#/api/mes/pd/project';
+import { $t } from '#/locales';
+
+import { useCreateTaskSchema } from '../data';
+
+const emit = defineEmits<{
+  success: [projectId: number];
+}>();
+
+const [Form, formApi] = useVbenForm({
+  commonConfig: {
+    componentProps: { class: 'w-full' },
+    formItemClass: 'col-span-1',
+    labelWidth: 110,
+  },
+  wrapperClass: 'grid-cols-2',
+  layout: 'horizontal',
+  schema: [],
+  showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    const { valid } = await formApi.validate();
+    if (!valid) {
+      return;
+    }
+    modalApi.lock();
+    const raw = (await formApi.getValues()) as MesPdProjectApi.Project & {
+      planDateRange?: [string, string];
+      productId?: number;
+    };
+    const { productId: _productId, planDateRange, ...data } = raw;
+    if (planDateRange?.length === 2) {
+      data.planStartDate = new Date(planDateRange[0]).getTime();
+      data.planEndDate = new Date(planDateRange[1]).getTime();
+    }
+    try {
+      const id = await createPdProject(data);
+      emit('success', id);
+      message.success('璁捐浠诲姟宸插彂璧凤紝璇峰彂甯冧换鍔″悗寮�濮嬩笂浼犺祫鏂�');
+      await modalApi.close();
+    } finally {
+      modalApi.unlock();
+    }
+  },
+  async onOpenChange(isOpen: boolean) {
+    if (!isOpen) {
+      await formApi.resetForm();
+      return;
+    }
+    formApi.setState({ schema: useCreateTaskSchema(formApi) });
+    modalApi.setState({ title: '鍙戣捣璁捐浠诲姟' });
+  },
+});
+</script>
+
+<template>
+  <Modal class="w-[720px]">
+    <Alert
+      class="mx-4 mb-4"
+      message="鍙戣捣浠诲姟"
+      description="涓烘寚瀹氫骇鍝佺殑鏌愪竴璁捐鐗堟湰鍒涘缓璁捐浠诲姟銆備换鍔′繚瀛樹负鑽夌锛屽彂甯冨悗鏂瑰彲涓婁紶璁捐璧勬枡銆�"
+      show-icon
+      type="info"
+    />
+    <Form class="mx-4" />
+  </Modal>
+</template>
diff --git a/src/views/mes/pd/project/modules/document-form.vue b/src/views/mes/pd/project/modules/document-form.vue
new file mode 100644
index 0000000..9d015cf
--- /dev/null
+++ b/src/views/mes/pd/project/modules/document-form.vue
@@ -0,0 +1,142 @@
+<script lang="ts" setup>
+import type { MesPdDocumentApi } from '#/api/mes/pd/document';
+
+import { ref } from 'vue';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+import { MesPdDocumentTypeEnum } from '#/packages/constants/src';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+  createPdDocument,
+  getPdDocument,
+  updatePdDocument,
+} from '#/api/mes/pd/document';
+import { $t } from '#/locales';
+
+import { getPdDocumentTypeOptions } from '../data';
+
+const emit = defineEmits(['success']);
+
+const isUpdate = ref(false);
+
+const [Form, formApi] = useVbenForm({
+  commonConfig: {
+    componentProps: { class: 'w-full' },
+    labelWidth: 100,
+  },
+  layout: 'horizontal',
+  schema: [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: { triggerFields: [''], show: () => false },
+    },
+    {
+      fieldName: 'projectId',
+      component: 'Input',
+      dependencies: { triggerFields: [''], show: () => false },
+    },
+    {
+      fieldName: 'documentType',
+      label: '璧勬枡绫诲瀷',
+      component: 'Select',
+      componentProps: {
+        options: getPdDocumentTypeOptions(),
+        placeholder: '璇烽�夋嫨璧勬枡绫诲瀷',
+      },
+      rules: 'selectRequired',
+    },
+    {
+      fieldName: 'name',
+      label: '璧勬枡鍚嶇О',
+      component: 'Input',
+      componentProps: { placeholder: '璇疯緭鍏ヨ祫鏂欏悕绉�' },
+      rules: 'required',
+    },
+    {
+      fieldName: 'version',
+      label: '鐗堟湰鍙�',
+      component: 'Input',
+      componentProps: { placeholder: '濡� V1.0' },
+      rules: 'required',
+    },
+    {
+      fieldName: 'fileUrl',
+      label: '鏂囦欢鍦板潃',
+      component: 'Input',
+      componentProps: { placeholder: '璇疯緭鍏ユ枃浠� URL 鎴栦笂浼犲悗濉叆' },
+    },
+    {
+      fieldName: 'fileName',
+      label: '鏂囦欢鍚�',
+      component: 'Input',
+      componentProps: { placeholder: '濡� bom-list.xlsx' },
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      componentProps: { placeholder: '璇疯緭鍏ュ娉�', rows: 3 },
+    },
+  ],
+  showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    const { valid } = await formApi.validate();
+    if (!valid) {
+      return;
+    }
+    modalApi.lock();
+    const data = (await formApi.getValues()) as MesPdDocumentApi.Document;
+    try {
+      if (isUpdate.value) {
+        await updatePdDocument(data);
+      } else {
+        await createPdDocument(data);
+      }
+      emit('success');
+      message.success($t('ui.actionMessage.operationSuccess'));
+      await modalApi.close();
+    } finally {
+      modalApi.unlock();
+    }
+  },
+  async onOpenChange(isOpen: boolean) {
+    if (!isOpen) {
+      return;
+    }
+    const data = modalApi.getData<{
+      id?: number;
+      projectId: number;
+      documentType?: number;
+    }>();
+    isUpdate.value = !!data.id;
+    modalApi.setState({ title: isUpdate.value ? '缂栬緫闄勪欢' : '涓婁紶闄勪欢' });
+    if (data.id) {
+      modalApi.lock();
+      try {
+        const detail = await getPdDocument(data.id);
+        await formApi.setValues(detail);
+      } finally {
+        modalApi.unlock();
+      }
+      return;
+    }
+    await formApi.setValues({
+      projectId: data.projectId,
+      documentType: data.documentType ?? MesPdDocumentTypeEnum.SCHEME,
+    });
+  },
+});
+</script>
+
+<template>
+  <Modal class="w-[560px]">
+    <Form class="mx-4" />
+  </Modal>
+</template>
diff --git a/src/views/mes/pd/project/modules/document-list.vue b/src/views/mes/pd/project/modules/document-list.vue
new file mode 100644
index 0000000..cfb129f
--- /dev/null
+++ b/src/views/mes/pd/project/modules/document-list.vue
@@ -0,0 +1,165 @@
+<script lang="ts" setup>
+import type { FormType } from '../data';
+
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesPdDocumentApi } from '#/api/mes/pd/document';
+
+import { computed, watch } from 'vue';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+import { MesPdDocumentTypeEnum } from '#/packages/constants/src';
+
+import { message } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+  deletePdDocument,
+  getPdDocumentPage,
+} from '#/api/mes/pd/document';
+import { $t } from '#/locales';
+
+import {
+  useMaterialListGridColumns,
+  useMaterialListGridFormSchema,
+} from '../data';
+import DocumentForm from './document-form.vue';
+
+const props = withDefaults(
+  defineProps<{
+    formType: FormType;
+    gridHeight?: number | string;
+    projectId: number;
+    tableTitle?: string;
+    version?: string;
+  }>(),
+  {
+    gridHeight: 360,
+    tableTitle: '璁捐璧勬枡',
+  },
+);
+
+const isEditable = computed(() =>
+  ['create', 'update'].includes(props.formType),
+);
+
+const [DocumentFormModal, documentFormModalApi] = useVbenModal({
+  connectedComponent: DocumentForm,
+  destroyOnClose: true,
+});
+
+function handleRefresh() {
+  gridApi.query();
+}
+
+function handleCreate() {
+  documentFormModalApi
+    .setData({
+      projectId: props.projectId,
+      documentType: MesPdDocumentTypeEnum.SCHEME,
+    })
+    .open();
+}
+
+function handleEdit(row: MesPdDocumentApi.Document) {
+  documentFormModalApi.setData({ id: row.id, projectId: props.projectId }).open();
+}
+
+async function handleDelete(row: MesPdDocumentApi.Document) {
+  const hideLoading = message.loading({
+    content: $t('ui.actionMessage.deleting', [row.name]),
+    duration: 0,
+  });
+  try {
+    await deletePdDocument(row.id!);
+    message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
+    handleRefresh();
+  } finally {
+    hideLoading();
+  }
+}
+
+function handlePreview(row: MesPdDocumentApi.Document) {
+  if (row.fileUrl) {
+    window.open(row.fileUrl, '_blank');
+  }
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  formOptions: { schema: useMaterialListGridFormSchema() },
+  gridOptions: {
+    columns: useMaterialListGridColumns(),
+    height: props.gridHeight,
+    keepSource: true,
+    proxyConfig: {
+      ajax: {
+        query: async ({ page }, formValues) => {
+          return await getPdDocumentPage({
+            pageNo: page.currentPage,
+            pageSize: page.pageSize,
+            projectId: props.projectId,
+            version: props.version,
+            ...formValues,
+          });
+        },
+      },
+    },
+    rowConfig: { keyField: 'id', isHover: true },
+    toolbarConfig: { refresh: true, search: true },
+  } as VxeTableGridOptions<MesPdDocumentApi.Document>,
+});
+
+watch(
+  () => props.version,
+  () => {
+    gridApi.query();
+  },
+);
+</script>
+
+<template>
+  <DocumentFormModal @success="handleRefresh" />
+  <Grid :table-title="tableTitle">
+    <template v-if="isEditable" #toolbar-tools>
+      <TableAction
+        :actions="[
+          {
+            label: '涓婁紶闄勪欢',
+            type: 'primary',
+            icon: ACTION_ICON.ADD,
+            onClick: handleCreate,
+          },
+        ]"
+      />
+    </template>
+    <template #actions="{ row }">
+      <TableAction
+        :actions="[
+          {
+            label: '棰勮',
+            type: 'link',
+            ifShow: !!row.fileUrl,
+            onClick: handlePreview.bind(null, row),
+          },
+          {
+            label: $t('common.edit'),
+            type: 'link',
+            icon: ACTION_ICON.EDIT,
+            ifShow: isEditable,
+            onClick: handleEdit.bind(null, row),
+          },
+          {
+            label: $t('common.delete'),
+            type: 'link',
+            danger: true,
+            icon: ACTION_ICON.DELETE,
+            ifShow: isEditable,
+            popConfirm: {
+              title: $t('ui.actionMessage.deleteConfirm', [row.name]),
+              confirm: handleDelete.bind(null, row),
+            },
+          },
+        ]"
+      />
+    </template>
+  </Grid>
+</template>
diff --git a/src/views/mes/pd/project/modules/drawing-review/data.ts b/src/views/mes/pd/project/modules/drawing-review/data.ts
new file mode 100644
index 0000000..5b598d8
--- /dev/null
+++ b/src/views/mes/pd/project/modules/drawing-review/data.ts
@@ -0,0 +1,88 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '#/packages/constants/src';
+import { getDictOptions } from '#/packages/effects/hooks/src';
+
+export function useDrawingReviewGridFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'code',
+      label: '瀹℃牳鍗曞彿',
+      component: 'Input',
+      componentProps: { placeholder: '璇疯緭鍏ュ鏍稿崟鍙�' },
+    },
+    {
+      fieldName: 'status',
+      label: '瀹℃牳鐘舵��',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: getDictOptions(DICT_TYPE.MES_PD_DRAWING_REVIEW_STATUS, 'number'),
+        placeholder: '璇烽�夋嫨瀹℃牳鐘舵��',
+      },
+    },
+  ];
+}
+
+export function useDrawingReviewGridColumns(): VxeTableGridOptions['columns'] {
+  return [
+    { field: 'code', title: '瀹℃牳鍗曞彿', minWidth: 130 },
+    { field: 'documentName', title: '璧勬枡鍚嶇О', minWidth: 160 },
+    { field: 'documentVersion', title: '璧勬枡鐗堟湰', minWidth: 90 },
+    { field: 'reviewerName', title: '瀹℃牳浜�', minWidth: 100 },
+    {
+      field: 'status',
+      title: '瀹℃牳鐘舵��',
+      minWidth: 100,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.MES_PD_DRAWING_REVIEW_STATUS },
+      },
+    },
+    {
+      field: 'reviewTime',
+      title: '瀹℃牳鏃堕棿',
+      minWidth: 160,
+      formatter: 'formatDateTime',
+    },
+    {
+      field: 'action',
+      title: '鎿嶄綔',
+      width: 160,
+      fixed: 'right',
+      slots: { default: 'actions' },
+    },
+  ];
+}
+
+export function useDrawingReviewFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: { triggerFields: [''], show: () => false },
+    },
+    {
+      fieldName: 'documentName',
+      label: '璧勬枡鍚嶇О',
+      component: 'Input',
+      componentProps: { disabled: true },
+    },
+    {
+      fieldName: 'status',
+      label: '瀹℃牳缁撴灉',
+      component: 'RadioGroup',
+      componentProps: {
+        options: getDictOptions(DICT_TYPE.MES_PD_DRAWING_REVIEW_STATUS, 'number'),
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'reviewComment',
+      label: '瀹℃牳鎰忚',
+      component: 'Textarea',
+      componentProps: { placeholder: '璇疯緭鍏ュ鏍告剰瑙�', rows: 4 },
+    },
+  ];
+}
diff --git a/src/views/mes/pd/project/modules/drawing-review/review-form.vue b/src/views/mes/pd/project/modules/drawing-review/review-form.vue
new file mode 100644
index 0000000..9bdaf2b
--- /dev/null
+++ b/src/views/mes/pd/project/modules/drawing-review/review-form.vue
@@ -0,0 +1,63 @@
+<script lang="ts" setup>
+import type { MesPdDrawingReviewApi } from '#/api/mes/pd/drawing-review';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { getPdDrawingReview, reviewPdDrawing } from '#/api/mes/pd/drawing-review';
+import { $t } from '#/locales';
+
+import { useDrawingReviewFormSchema } from './data';
+
+const emit = defineEmits(['success']);
+
+const [Form, formApi] = useVbenForm({
+  commonConfig: {
+    componentProps: { class: 'w-full' },
+    labelWidth: 100,
+  },
+  layout: 'horizontal',
+  schema: useDrawingReviewFormSchema(),
+  showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    const { valid } = await formApi.validate();
+    if (!valid) {
+      return;
+    }
+    modalApi.lock();
+    try {
+      const data = await formApi.getValues();
+      await reviewPdDrawing(data as MesPdDrawingReviewApi.ReviewParams);
+      emit('success');
+      message.success('瀹℃牳瀹屾垚');
+      await modalApi.close();
+    } finally {
+      modalApi.unlock();
+    }
+  },
+  async onOpenChange(isOpen: boolean) {
+    if (!isOpen) {
+      return;
+    }
+    const data = modalApi.getData<{ id: number }>();
+    modalApi.lock();
+    try {
+      const detail = await getPdDrawingReview(data.id);
+      await formApi.setValues(detail);
+    } finally {
+      modalApi.unlock();
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal class="w-[560px]" title="璧勬枡瀹℃牳">
+    <Form class="mx-4" />
+  </Modal>
+</template>
diff --git a/src/views/mes/pd/project/modules/drawing-review/review-list.vue b/src/views/mes/pd/project/modules/drawing-review/review-list.vue
new file mode 100644
index 0000000..c08b75a
--- /dev/null
+++ b/src/views/mes/pd/project/modules/drawing-review/review-list.vue
@@ -0,0 +1,77 @@
+<script lang="ts" setup>
+import type { FormType } from '../../data';
+
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesPdDrawingReviewApi } from '#/api/mes/pd/drawing-review';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import { getPdDrawingReviewPage } from '#/api/mes/pd/drawing-review';
+
+import {
+  useDrawingReviewGridColumns,
+  useDrawingReviewGridFormSchema,
+} from './data';
+import ReviewForm from './review-form.vue';
+
+const props = defineProps<{
+  formType: FormType;
+  projectId: number;
+}>();
+
+const [ReviewModal, reviewModalApi] = useVbenModal({
+  connectedComponent: ReviewForm,
+  destroyOnClose: true,
+});
+
+function handleRefresh() {
+  gridApi.query();
+}
+
+function handleReview(row: MesPdDrawingReviewApi.DrawingReview) {
+  reviewModalApi.setData({ id: row.id }).open();
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  formOptions: { schema: useDrawingReviewGridFormSchema() },
+  gridOptions: {
+    columns: useDrawingReviewGridColumns(),
+    height: 360,
+    keepSource: true,
+    proxyConfig: {
+      ajax: {
+        query: async ({ page }, formValues) => {
+          return await getPdDrawingReviewPage({
+            pageNo: page.currentPage,
+            pageSize: page.pageSize,
+            projectId: props.projectId,
+            ...formValues,
+          });
+        },
+      },
+    },
+    rowConfig: { keyField: 'id', isHover: true },
+    toolbarConfig: { refresh: true, search: true },
+  } as VxeTableGridOptions<MesPdDrawingReviewApi.DrawingReview>,
+});
+</script>
+
+<template>
+  <ReviewModal @success="handleRefresh" />
+  <Grid table-title="璧勬枡瀹℃牳">
+    <template #actions="{ row }">
+      <TableAction
+        :actions="[
+          {
+            label: '瀹℃牳',
+            type: 'link',
+            icon: ACTION_ICON.EDIT,
+            auth: ['mes:pd-drawing-review:review'],
+            onClick: handleReview.bind(null, row),
+          },
+        ]"
+      />
+    </template>
+  </Grid>
+</template>
diff --git a/src/views/mes/pd/project/modules/form.vue b/src/views/mes/pd/project/modules/form.vue
new file mode 100644
index 0000000..375f096
--- /dev/null
+++ b/src/views/mes/pd/project/modules/form.vue
@@ -0,0 +1,106 @@
+<script lang="ts" setup>
+import type { FormType } from '../data';
+import type { MesPdProjectApi } from '#/api/mes/pd/project';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { getPdProject, updatePdProject } from '#/api/mes/pd/project';
+import { $t } from '#/locales';
+
+import { useFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+
+const formType = ref<FormType>('update');
+const formData = ref<MesPdProjectApi.Project>();
+
+const isDetail = computed(() => formType.value === 'detail');
+const getTitle = computed(() => {
+  if (formType.value === 'detail') {
+    return '鏌ョ湅璁捐浠诲姟';
+  }
+  return '缂栬緫璁捐浠诲姟';
+});
+
+const [Form, formApi] = useVbenForm({
+  commonConfig: {
+    componentProps: { class: 'w-full' },
+    formItemClass: 'col-span-1',
+    labelWidth: 110,
+  },
+  wrapperClass: 'grid-cols-3',
+  layout: 'horizontal',
+  schema: [],
+  showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    if (isDetail.value) {
+      await modalApi.close();
+      return;
+    }
+    const { valid } = await formApi.validate();
+    if (!valid) {
+      return;
+    }
+    modalApi.lock();
+    const raw = (await formApi.getValues()) as MesPdProjectApi.Project & {
+      planDateRange?: [string, string];
+      productId?: number;
+    };
+    const { productId: _productId, planDateRange, ...data } = raw;
+    if (planDateRange?.length === 2) {
+      data.planStartDate = new Date(planDateRange[0]).getTime();
+      data.planEndDate = new Date(planDateRange[1]).getTime();
+    }
+    try {
+      await updatePdProject(data);
+      formData.value = { ...formData.value, ...data };
+      emit('success');
+      message.success($t('ui.actionMessage.operationSuccess'));
+      await modalApi.close();
+    } finally {
+      modalApi.unlock();
+    }
+  },
+  async onOpenChange(isOpen: boolean) {
+    if (!isOpen) {
+      formData.value = undefined;
+      return;
+    }
+    const data = modalApi.getData<{ formType: FormType; id?: number }>();
+    formType.value = data.formType;
+    formApi.setState({ schema: useFormSchema(data.formType, formApi) });
+    formApi.setDisabled(formType.value === 'detail');
+    modalApi.setState({ showConfirmButton: formType.value !== 'detail' });
+    if (!data.id) {
+      return;
+    }
+    modalApi.lock();
+    try {
+      formData.value = await getPdProject(data.id);
+      await formApi.setValues({
+        ...formData.value,
+        planDateRange:
+          formData.value.planStartDate && formData.value.planEndDate
+            ? [formData.value.planStartDate, formData.value.planEndDate]
+            : undefined,
+      });
+    } finally {
+      modalApi.unlock();
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal :title="getTitle" class="w-4/5">
+    <Form class="mx-4" />
+  </Modal>
+</template>
diff --git a/src/views/mes/pd/project/modules/material-panel.vue b/src/views/mes/pd/project/modules/material-panel.vue
new file mode 100644
index 0000000..63daa47
--- /dev/null
+++ b/src/views/mes/pd/project/modules/material-panel.vue
@@ -0,0 +1,28 @@
+<script lang="ts" setup>
+import type { FormType } from '../data';
+
+import DocumentList from './document-list.vue';
+
+withDefaults(
+  defineProps<{
+    formType: FormType;
+    gridHeight?: number | string;
+    projectId: number;
+    tableTitle?: string;
+    version?: string;
+  }>(),
+  {
+    tableTitle: '璁捐璧勬枡',
+  },
+);
+</script>
+
+<template>
+  <DocumentList
+    :form-type="formType"
+    :grid-height="gridHeight"
+    :project-id="projectId"
+    :table-title="tableTitle"
+    :version="version"
+  />
+</template>
diff --git a/src/views/mes/pd/project/modules/publish-form.vue b/src/views/mes/pd/project/modules/publish-form.vue
new file mode 100644
index 0000000..dd9ad6f
--- /dev/null
+++ b/src/views/mes/pd/project/modules/publish-form.vue
@@ -0,0 +1,103 @@
+<script lang="ts" setup>
+import type { MesPdProjectApi } from '#/api/mes/pd/project';
+
+import { ref } from 'vue';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+import { formatDate } from '#/packages/utils/src';
+
+import { Alert, Descriptions, message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+  getPdProject,
+  publishPdProject,
+  updatePdProject,
+} from '#/api/mes/pd/project';
+
+import { usePublishFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+
+const project = ref<MesPdProjectApi.Project>();
+
+const [Form, formApi] = useVbenForm({
+  commonConfig: {
+    componentProps: { class: 'w-full' },
+    labelWidth: 100,
+  },
+  layout: 'horizontal',
+  schema: usePublishFormSchema(),
+  showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    if (!project.value?.id) {
+      return;
+    }
+    modalApi.lock();
+    try {
+      const { publishRemark } = await formApi.getValues();
+      if (publishRemark) {
+        const mergedRemark = [project.value.remark, `銆愬彂甯冭鏄庛��${publishRemark}`]
+          .filter(Boolean)
+          .join('\n');
+        await updatePdProject({ ...project.value, remark: mergedRemark });
+      }
+      await publishPdProject(project.value.id);
+      emit('success');
+      message.success('浠诲姟宸插彂甯冿紝璇蜂笂浼犺璁¤祫鏂�');
+      await modalApi.close();
+    } finally {
+      modalApi.unlock();
+    }
+  },
+  async onOpenChange(isOpen: boolean) {
+    if (!isOpen) {
+      project.value = undefined;
+      await formApi.resetForm();
+      return;
+    }
+    const data = modalApi.getData<{ id: number }>();
+    modalApi.setState({ title: '鍙戝竷璁捐浠诲姟' });
+    modalApi.lock();
+    try {
+      project.value = await getPdProject(data.id);
+    } finally {
+      modalApi.unlock();
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal class="w-[640px]">
+    <Alert
+      class="mx-4 mb-4"
+      message="纭鍙戝竷"
+      description="鍙戝竷鍚庝换鍔¤繘鍏ャ�岃璁′腑銆嶇姸鎬侊紝璁捐甯堝彲涓婁紶璁捐璧勬枡銆傚彂甯冨墠璇风‘璁や骇鍝併�佺増鏈強浜哄憳瀹夋帓鏃犺銆�"
+      show-icon
+      type="warning"
+    />
+    <Descriptions
+      v-if="project"
+      bordered
+      class="mx-4 mb-4"
+      :column="2"
+      size="small"
+    >
+      <Descriptions.Item label="浠诲姟缂栫爜">{{ project.code }}</Descriptions.Item>
+      <Descriptions.Item label="浠诲姟鍚嶇О">{{ project.name }}</Descriptions.Item>
+      <Descriptions.Item label="浜у搧缂栫爜">{{ project.productCode }}</Descriptions.Item>
+      <Descriptions.Item label="浜у搧鍚嶇О">{{ project.productName }}</Descriptions.Item>
+      <Descriptions.Item label="璁捐鐗堟湰">{{ project.version }}</Descriptions.Item>
+      <Descriptions.Item label="涓昏璁″笀">{{ project.designerName || '-' }}</Descriptions.Item>
+      <Descriptions.Item label="瀹℃牳浜�">{{ project.reviewerName || '-' }}</Descriptions.Item>
+      <Descriptions.Item label="璁″垝瀹屾垚">
+        {{ project.planEndDate ? formatDate(project.planEndDate) : '-' }}
+      </Descriptions.Item>
+    </Descriptions>
+    <Form class="mx-4" />
+  </Modal>
+</template>
diff --git a/src/views/mes/pd/project/modules/task-draft-banner.vue b/src/views/mes/pd/project/modules/task-draft-banner.vue
new file mode 100644
index 0000000..c812428
--- /dev/null
+++ b/src/views/mes/pd/project/modules/task-draft-banner.vue
@@ -0,0 +1,32 @@
+<script lang="ts" setup>
+import { Button } from 'ant-design-vue';
+
+defineProps<{
+  designerName?: string;
+  productName?: string;
+  version?: string;
+}>();
+
+const emit = defineEmits<{
+  edit: [];
+  publish: [];
+}>();
+</script>
+
+<template>
+  <div
+    class="mb-4 flex items-center justify-between rounded-lg border border-amber-200 bg-amber-50 px-4 py-3"
+  >
+    <div class="text-sm text-amber-900">
+      <span class="font-medium">浠诲姟宸插彂璧凤紙鑽夌锛�</span>
+      <span class="ml-2 text-amber-700">
+        {{ productName }} {{ version }} 路 璁捐甯� {{ designerName || '鏈寚瀹�' }}
+      </span>
+      <span class="ml-2 text-amber-600">鈥� 璇风‘璁や俊鎭悗鍙戝竷浠诲姟锛屽彂甯冨悗鏂瑰彲涓婁紶璁捐璧勬枡</span>
+    </div>
+    <div class="flex shrink-0 gap-2">
+      <Button size="small" @click="emit('edit')">淇敼浠诲姟</Button>
+      <Button size="small" type="primary" @click="emit('publish')">鍙戝竷浠诲姟</Button>
+    </div>
+  </div>
+</template>
diff --git a/src/views/mes/pd/project/modules/task/data.ts b/src/views/mes/pd/project/modules/task/data.ts
new file mode 100644
index 0000000..f7d551a
--- /dev/null
+++ b/src/views/mes/pd/project/modules/task/data.ts
@@ -0,0 +1,147 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '#/packages/constants/src';
+import { getDictOptions } from '#/packages/effects/hooks/src';
+
+export type TaskFormType = 'create' | 'detail' | 'dispatch' | 'update';
+
+export function useTaskGridFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'code',
+      label: '浠诲姟缂栫爜',
+      component: 'Input',
+      componentProps: { placeholder: '璇疯緭鍏ヤ换鍔$紪鐮�' },
+    },
+    {
+      fieldName: 'taskName',
+      label: '浠诲姟鍚嶇О',
+      component: 'Input',
+      componentProps: { placeholder: '璇疯緭鍏ヤ换鍔″悕绉�' },
+    },
+    {
+      fieldName: 'status',
+      label: '浠诲姟鐘舵��',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: getDictOptions(DICT_TYPE.MES_PD_TASK_STATUS, 'number'),
+        placeholder: '璇烽�夋嫨浠诲姟鐘舵��',
+      },
+    },
+  ];
+}
+
+export function useTaskGridColumns(): VxeTableGridOptions['columns'] {
+  return [
+    { field: 'code', title: '浠诲姟缂栫爜', minWidth: 130 },
+    { field: 'taskName', title: '浠诲姟鍚嶇О', minWidth: 140 },
+    { field: 'assigneeName', title: '鎵ц浜�', minWidth: 100 },
+    {
+      field: 'status',
+      title: '鐘舵��',
+      minWidth: 100,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.MES_PD_TASK_STATUS },
+      },
+    },
+    {
+      field: 'planFinishDate',
+      title: '璁″垝瀹屾垚',
+      minWidth: 120,
+      formatter: 'formatDate',
+    },
+    {
+      field: 'createTime',
+      title: '鍒涘缓鏃堕棿',
+      minWidth: 160,
+      formatter: 'formatDateTime',
+    },
+    {
+      field: 'action',
+      title: '鎿嶄綔',
+      width: 200,
+      fixed: 'right',
+      slots: { default: 'actions' },
+    },
+  ];
+}
+
+export function useTaskFormSchema(formType: TaskFormType): VbenFormSchema[] {
+  const readonly = formType === 'detail';
+  return [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: { triggerFields: [''], show: () => false },
+    },
+    {
+      fieldName: 'projectId',
+      component: 'Input',
+      dependencies: { triggerFields: [''], show: () => false },
+    },
+    {
+      fieldName: 'code',
+      label: '浠诲姟缂栫爜',
+      component: 'Input',
+      componentProps: { disabled: readonly, placeholder: '璇疯緭鍏ヤ换鍔$紪鐮�' },
+      rules: 'required',
+    },
+    {
+      fieldName: 'taskName',
+      label: '浠诲姟鍚嶇О',
+      component: 'Input',
+      componentProps: { disabled: readonly, placeholder: '璇疯緭鍏ヤ换鍔″悕绉�' },
+      rules: 'required',
+    },
+    {
+      fieldName: 'assigneeName',
+      label: '鎵ц浜�',
+      component: 'Input',
+      componentProps: { disabled: readonly, placeholder: '璇疯緭鍏ユ墽琛屼汉' },
+    },
+    {
+      fieldName: 'planFinishDate',
+      label: '璁″垝瀹屾垚',
+      component: 'DatePicker',
+      componentProps: { disabled: readonly, class: 'w-full', valueFormat: 'x' },
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      componentProps: { disabled: readonly, placeholder: '璇疯緭鍏ュ娉�', rows: 3 },
+    },
+  ];
+}
+
+export function useTaskDispatchSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: { triggerFields: [''], show: () => false },
+    },
+    {
+      fieldName: 'assigneeName',
+      label: '鎵ц浜�',
+      component: 'Input',
+      componentProps: { placeholder: '璇疯緭鍏ユ墽琛屼汉濮撳悕' },
+      rules: 'required',
+    },
+    {
+      fieldName: 'planFinishDate',
+      label: '璁″垝瀹屾垚',
+      component: 'DatePicker',
+      componentProps: { class: 'w-full', valueFormat: 'x' },
+    },
+    {
+      fieldName: 'remark',
+      label: '鍒嗗彂璇存槑',
+      component: 'Textarea',
+      componentProps: { placeholder: '璇疯緭鍏ュ垎鍙戣鏄�', rows: 3 },
+    },
+  ];
+}
diff --git a/src/views/mes/pd/project/modules/task/task-form.vue b/src/views/mes/pd/project/modules/task/task-form.vue
new file mode 100644
index 0000000..ea95b92
--- /dev/null
+++ b/src/views/mes/pd/project/modules/task/task-form.vue
@@ -0,0 +1,114 @@
+<script lang="ts" setup>
+import type { TaskFormType } from './data';
+
+import { ref } from 'vue';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+  createPdTask,
+  dispatchPdTask,
+  getPdTask,
+  updatePdTask,
+} from '#/api/mes/pd/task';
+import { $t } from '#/locales';
+
+import { useTaskDispatchSchema, useTaskFormSchema } from './data';
+
+const emit = defineEmits(['success']);
+
+const mode = ref<TaskFormType>('create');
+
+const [Form, formApi] = useVbenForm({
+  commonConfig: {
+    componentProps: { class: 'w-full' },
+    labelWidth: 100,
+  },
+  layout: 'horizontal',
+  schema: [],
+  showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    const { valid } = await formApi.validate();
+    if (!valid) {
+      return;
+    }
+    modalApi.lock();
+    try {
+      if (mode.value === 'dispatch') {
+        const data = await formApi.getValues();
+        await dispatchPdTask({
+          id: data.id,
+          assigneeId: 0,
+          planFinishDate: data.planFinishDate,
+          remark: data.remark,
+        });
+      } else if (mode.value === 'create') {
+        await createPdTask(await formApi.getValues());
+      } else if (mode.value === 'update') {
+        await updatePdTask(await formApi.getValues());
+      }
+      emit('success');
+      message.success($t('ui.actionMessage.operationSuccess'));
+      await modalApi.close();
+    } finally {
+      modalApi.unlock();
+    }
+  },
+  async onOpenChange(isOpen: boolean) {
+    if (!isOpen) {
+      return;
+    }
+    const data = modalApi.getData<{
+      formType: TaskFormType;
+      id?: number;
+      projectId?: number;
+    }>();
+    mode.value = data.formType;
+    const readonly = data.formType === 'detail';
+    formApi.setState({
+      schema:
+        data.formType === 'dispatch'
+          ? useTaskDispatchSchema()
+          : useTaskFormSchema(data.formType),
+    });
+    formApi.setDisabled(readonly);
+    modalApi.setState({
+      showConfirmButton: data.formType !== 'detail',
+      title:
+        data.formType === 'dispatch'
+          ? '浠诲姟鍒嗗彂'
+          : data.formType === 'detail'
+            ? '鏌ョ湅浠诲姟'
+            : data.formType === 'update'
+              ? '缂栬緫浠诲姟'
+              : '鏂板浠诲姟',
+    });
+    if (data.formType === 'create' && data.projectId) {
+      await formApi.setValues({ projectId: data.projectId });
+      return;
+    }
+    if (!data.id) {
+      return;
+    }
+    modalApi.lock();
+    try {
+      const detail = await getPdTask(data.id);
+      await formApi.setValues(detail);
+    } finally {
+      modalApi.unlock();
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal class="w-[560px]">
+    <Form class="mx-4" />
+  </Modal>
+</template>
diff --git a/src/views/mes/pd/project/modules/task/task-list.vue b/src/views/mes/pd/project/modules/task/task-list.vue
new file mode 100644
index 0000000..8ab4700
--- /dev/null
+++ b/src/views/mes/pd/project/modules/task/task-list.vue
@@ -0,0 +1,149 @@
+<script lang="ts" setup>
+import type { FormType } from '../../data';
+
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesPdTaskApi } from '#/api/mes/pd/task';
+
+import { computed } from 'vue';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+
+import { message } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+  completePdTask,
+  deletePdTask,
+  getPdTaskPage,
+} from '#/api/mes/pd/task';
+import { $t } from '#/locales';
+
+import { useTaskGridColumns, useTaskGridFormSchema } from './data';
+import TaskForm from './task-form.vue';
+
+const props = defineProps<{
+  formType: FormType;
+  projectId: number;
+}>();
+
+const isEditable = computed(() =>
+  ['create', 'update'].includes(props.formType),
+);
+
+const [FormModal, formModalApi] = useVbenModal({
+  connectedComponent: TaskForm,
+  destroyOnClose: true,
+});
+
+function handleRefresh() {
+  gridApi.query();
+}
+
+function handleCreate() {
+  formModalApi
+    .setData({ formType: 'create', projectId: props.projectId })
+    .open();
+}
+
+function handleEdit(row: MesPdTaskApi.Task) {
+  formModalApi.setData({ formType: 'update', id: row.id }).open();
+}
+
+function handleDispatch(row: MesPdTaskApi.Task) {
+  formModalApi.setData({ formType: 'dispatch', id: row.id }).open();
+}
+
+async function handleComplete(row: MesPdTaskApi.Task) {
+  await completePdTask(row.id!);
+  message.success('浠诲姟宸插畬鎴�');
+  handleRefresh();
+}
+
+async function handleDelete(row: MesPdTaskApi.Task) {
+  await deletePdTask(row.id!);
+  message.success($t('ui.actionMessage.deleteSuccess', [row.taskName]));
+  handleRefresh();
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  formOptions: { schema: useTaskGridFormSchema() },
+  gridOptions: {
+    columns: useTaskGridColumns(),
+    height: 360,
+    keepSource: true,
+    proxyConfig: {
+      ajax: {
+        query: async ({ page }, formValues) => {
+          return await getPdTaskPage({
+            pageNo: page.currentPage,
+            pageSize: page.pageSize,
+            projectId: props.projectId,
+            ...formValues,
+          });
+        },
+      },
+    },
+    rowConfig: { keyField: 'id', isHover: true },
+    toolbarConfig: { refresh: true, search: true },
+  } as VxeTableGridOptions<MesPdTaskApi.Task>,
+});
+</script>
+
+<template>
+  <FormModal @success="handleRefresh" />
+  <Grid>
+    <template v-if="isEditable" #toolbar-tools>
+      <TableAction
+        :actions="[
+          {
+            label: $t('ui.actionTitle.create', ['璁捐浠诲姟']),
+            type: 'primary',
+            icon: ACTION_ICON.ADD,
+            auth: ['mes:pd-task:create'],
+            onClick: handleCreate,
+          },
+        ]"
+      />
+    </template>
+    <template #actions="{ row }">
+      <TableAction
+        :actions="[
+          {
+            label: '鍒嗗彂',
+            type: 'link',
+            auth: ['mes:pd-task:dispatch'],
+            ifShow: isEditable,
+            onClick: handleDispatch.bind(null, row),
+          },
+          {
+            label: $t('common.edit'),
+            type: 'link',
+            icon: ACTION_ICON.EDIT,
+            auth: ['mes:pd-task:update'],
+            ifShow: isEditable,
+            onClick: handleEdit.bind(null, row),
+          },
+          {
+            label: '瀹屾垚',
+            type: 'link',
+            auth: ['mes:pd-task:complete'],
+            ifShow: isEditable,
+            onClick: handleComplete.bind(null, row),
+          },
+          {
+            label: $t('common.delete'),
+            type: 'link',
+            danger: true,
+            icon: ACTION_ICON.DELETE,
+            auth: ['mes:pd-task:delete'],
+            ifShow: isEditable,
+            popConfirm: {
+              title: $t('ui.actionMessage.deleteConfirm', [row.taskName]),
+              confirm: handleDelete.bind(null, row),
+            },
+          },
+        ]"
+      />
+    </template>
+  </Grid>
+</template>
diff --git a/src/views/mes/pd/project/modules/version-change/data.ts b/src/views/mes/pd/project/modules/version-change/data.ts
new file mode 100644
index 0000000..e4756cc
--- /dev/null
+++ b/src/views/mes/pd/project/modules/version-change/data.ts
@@ -0,0 +1,167 @@
+import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { h } from 'vue';
+
+import { DICT_TYPE, MesAutoCodeRuleCode } from '#/packages/constants/src';
+import { getDictOptions } from '#/packages/effects/hooks/src';
+
+import { Button } from 'ant-design-vue';
+
+import { generateAutoCode } from '#/api/mes/md/autocode/record';
+import { isPdMockEnabled } from '#/api/mes/pd/mock/enabled';
+import { mockNextCode } from '#/api/mes/pd/mock/utils';
+
+export type VersionChangeFormType = 'create' | 'detail' | 'update';
+
+export function useVersionChangeGridFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'code',
+      label: '鍙樻洿鍗曞彿',
+      component: 'Input',
+      componentProps: { placeholder: '璇疯緭鍏ュ彉鏇村崟鍙�' },
+    },
+    {
+      fieldName: 'changeType',
+      label: '鍙樻洿绫诲瀷',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: getDictOptions(DICT_TYPE.MES_PD_VERSION_CHANGE_TYPE, 'number'),
+        placeholder: '璇烽�夋嫨鍙樻洿绫诲瀷',
+      },
+    },
+  ];
+}
+
+export function useVersionChangeGridColumns(
+  showAction = true,
+): VxeTableGridOptions['columns'] {
+  const columns: VxeTableGridOptions['columns'] = [
+    { field: 'code', title: '鍙樻洿鍗曞彿', minWidth: 130 },
+    {
+      field: 'changeType',
+      title: '鍙樻洿绫诲瀷',
+      minWidth: 100,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.MES_PD_VERSION_CHANGE_TYPE },
+      },
+    },
+    { field: 'fromVersion', title: '鍘熺増鏈�', minWidth: 90 },
+    { field: 'toVersion', title: '鏂扮増鏈�', minWidth: 90 },
+    { field: 'applicantName', title: '鐢宠浜�', minWidth: 100 },
+    {
+      field: 'createTime',
+      title: '鐢宠鏃堕棿',
+      minWidth: 160,
+      formatter: 'formatDateTime',
+    },
+  ];
+
+  if (showAction) {
+    columns.push({
+      field: 'action',
+      title: '鎿嶄綔',
+      width: 180,
+      fixed: 'right',
+      slots: { default: 'actions' },
+    });
+  }
+
+  return columns;
+}
+
+export function useVersionChangeFormSchema(
+  formType: VersionChangeFormType,
+  formApi?: VbenFormApi,
+): VbenFormSchema[] {
+  const readonly = formType === 'detail';
+  return [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: { triggerFields: [''], show: () => false },
+    },
+    {
+      fieldName: 'projectId',
+      component: 'Input',
+      dependencies: { triggerFields: [''], show: () => false },
+    },
+    {
+      fieldName: 'code',
+      label: '鍙樻洿鍗曞彿',
+      component: 'Input',
+      componentProps: { disabled: readonly, placeholder: '璇疯緭鍏ュ彉鏇村崟鍙�' },
+      rules: 'required',
+      suffix:
+        formType === 'detail'
+          ? undefined
+          : () =>
+              h(
+                Button,
+                {
+                  type: 'default',
+                  onClick: async () => {
+                    const code = isPdMockEnabled()
+                      ? mockNextCode(
+                          'VC-2026',
+                          Math.floor(Date.now() % 10_000),
+                        )
+                      : await generateAutoCode(
+                          MesAutoCodeRuleCode.PD_VERSION_CODE,
+                        );
+                    await formApi?.setFieldValue('code', code);
+                  },
+                },
+                { default: () => '鐢熸垚' },
+              ),
+    },
+    {
+      fieldName: 'changeType',
+      label: '鍙樻洿绫诲瀷',
+      component: 'Select',
+      componentProps: {
+        disabled: readonly,
+        options: getDictOptions(DICT_TYPE.MES_PD_VERSION_CHANGE_TYPE, 'number'),
+        placeholder: '璇烽�夋嫨鍙樻洿绫诲瀷',
+      },
+      rules: 'selectRequired',
+    },
+    {
+      fieldName: 'fromVersion',
+      label: '鍘熺増鏈�',
+      component: 'Input',
+      componentProps: { disabled: readonly, placeholder: '濡� V1.0' },
+      rules: 'required',
+    },
+    {
+      fieldName: 'toVersion',
+      label: '鏂扮増鏈�',
+      component: 'Input',
+      componentProps: { disabled: readonly, placeholder: '濡� V1.1' },
+      rules: 'required',
+    },
+    {
+      fieldName: 'changeReason',
+      label: '鍙樻洿鍘熷洜',
+      component: 'Textarea',
+      componentProps: { disabled: readonly, placeholder: '璇疯緭鍏ュ彉鏇村師鍥�', rows: 2 },
+      rules: 'required',
+    },
+    {
+      fieldName: 'changeContent',
+      label: '鍙樻洿鍐呭',
+      component: 'Textarea',
+      componentProps: { disabled: readonly, placeholder: '璇疯緭鍏ュ彉鏇村唴瀹�', rows: 4 },
+      rules: 'required',
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      componentProps: { disabled: readonly, placeholder: '璇疯緭鍏ュ娉�', rows: 2 },
+    },
+  ];
+}
diff --git a/src/views/mes/pd/project/modules/version-change/version-change-form.vue b/src/views/mes/pd/project/modules/version-change/version-change-form.vue
new file mode 100644
index 0000000..a969658
--- /dev/null
+++ b/src/views/mes/pd/project/modules/version-change/version-change-form.vue
@@ -0,0 +1,108 @@
+<script lang="ts" setup>
+import type { VersionChangeFormType } from './data';
+
+import type { MesPdVersionChangeApi } from '#/api/mes/pd/version-change';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+  createPdVersionChange,
+  getPdVersionChange,
+  updatePdVersionChange,
+} from '#/api/mes/pd/version-change';
+import { $t } from '#/locales';
+
+import { useVersionChangeFormSchema } from './data';
+
+const emit = defineEmits(['success']);
+
+const formType = ref<VersionChangeFormType>('create');
+const isDetail = computed(() => formType.value === 'detail');
+
+const [Form, formApi] = useVbenForm({
+  commonConfig: {
+    componentProps: { class: 'w-full' },
+    labelWidth: 100,
+  },
+  layout: 'horizontal',
+  schema: [],
+  showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    if (isDetail.value) {
+      await modalApi.close();
+      return;
+    }
+    const { valid } = await formApi.validate();
+    if (!valid) {
+      return;
+    }
+    modalApi.lock();
+    const data = (await formApi.getValues()) as MesPdVersionChangeApi.VersionChange;
+    try {
+      if (formType.value === 'create') {
+        await createPdVersionChange(data);
+      } else {
+        await updatePdVersionChange(data);
+      }
+      emit('success');
+      message.success($t('ui.actionMessage.operationSuccess'));
+      await modalApi.close();
+    } finally {
+      modalApi.unlock();
+    }
+  },
+  async onOpenChange(isOpen: boolean) {
+    if (!isOpen) {
+      return;
+    }
+    const data = modalApi.getData<{
+      formType: VersionChangeFormType;
+      id?: number;
+      projectId?: number;
+      fromVersion?: string;
+    }>();
+    formType.value = data.formType;
+    formApi.setState({ schema: useVersionChangeFormSchema(data.formType, formApi) });
+    formApi.setDisabled(isDetail.value);
+    modalApi.setState({
+      showConfirmButton: !isDetail.value,
+      title:
+        data.formType === 'detail'
+          ? '鏌ョ湅鐗堟湰鍙樻洿'
+          : data.formType === 'update'
+            ? '缂栬緫鐗堟湰鍙樻洿'
+            : '鏂板鐗堟湰鍙樻洿',
+    });
+    if (data.formType === 'create' && data.projectId) {
+      await formApi.setValues({
+        projectId: data.projectId,
+        fromVersion: data.fromVersion,
+      });
+      return;
+    }
+    if (!data.id) {
+      return;
+    }
+    modalApi.lock();
+    try {
+      await formApi.setValues(await getPdVersionChange(data.id));
+    } finally {
+      modalApi.unlock();
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal class="w-[640px]">
+    <Form class="mx-4" />
+  </Modal>
+</template>
diff --git a/src/views/mes/pd/project/modules/version-change/version-change-list.vue b/src/views/mes/pd/project/modules/version-change/version-change-list.vue
new file mode 100644
index 0000000..4cb2a67
--- /dev/null
+++ b/src/views/mes/pd/project/modules/version-change/version-change-list.vue
@@ -0,0 +1,186 @@
+<script lang="ts" setup>
+import type { FormType } from '../../data';
+
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesPdVersionChangeApi } from '#/api/mes/pd/version-change';
+
+import { computed } from 'vue';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+
+import { message } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+  approvePdVersionChange,
+  deletePdVersionChange,
+  getPdVersionChangePage,
+} from '#/api/mes/pd/version-change';
+import { $t } from '#/locales';
+
+import {
+  useVersionChangeGridColumns,
+  useVersionChangeGridFormSchema,
+} from './data';
+import VersionChangeForm from './version-change-form.vue';
+
+const props = withDefaults(
+  defineProps<{
+    formType: FormType;
+    gridHeight?: number | string;
+    projectId: number;
+    projectVersion?: string;
+    selectable?: boolean;
+  }>(),
+  {
+    gridHeight: 360,
+    selectable: false,
+  },
+);
+
+const emit = defineEmits<{
+  viewMaterials: [row: MesPdVersionChangeApi.VersionChange];
+}>();
+
+const isEditable = computed(() =>
+  ['create', 'update'].includes(props.formType),
+);
+
+const [FormModal, formModalApi] = useVbenModal({
+  connectedComponent: VersionChangeForm,
+  destroyOnClose: true,
+});
+
+function handleRefresh() {
+  gridApi.query();
+}
+
+function handleCreate() {
+  formModalApi
+    .setData({
+      formType: 'create',
+      projectId: props.projectId,
+      fromVersion: props.projectVersion,
+    })
+    .open();
+}
+
+function handleDetail(row: MesPdVersionChangeApi.VersionChange) {
+  if (!isEditable.value) {
+    emit('viewMaterials', row);
+    return;
+  }
+  formModalApi.setData({ formType: 'detail', id: row.id }).open();
+}
+
+function handleEdit(row: MesPdVersionChangeApi.VersionChange) {
+  formModalApi.setData({ formType: 'update', id: row.id }).open();
+}
+
+async function handleApprove(row: MesPdVersionChangeApi.VersionChange) {
+  await approvePdVersionChange(row.id!);
+  message.success('鐗堟湰鍙樻洿宸叉壒鍑�');
+  handleRefresh();
+}
+
+async function handleDelete(row: MesPdVersionChangeApi.VersionChange) {
+  await deletePdVersionChange(row.id!);
+  message.success($t('ui.actionMessage.deleteSuccess', [row.code]));
+  handleRefresh();
+}
+
+function handleSelectRow(row: MesPdVersionChangeApi.VersionChange) {
+  emit('viewMaterials', row);
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  formOptions: { schema: useVersionChangeGridFormSchema() },
+  gridOptions: {
+    columns: useVersionChangeGridColumns(!props.selectable),
+    height: props.gridHeight,
+    keepSource: true,
+    proxyConfig: {
+      ajax: {
+        query: async ({ page }, formValues) => {
+          return await getPdVersionChangePage({
+            pageNo: page.currentPage,
+            pageSize: page.pageSize,
+            projectId: props.projectId,
+            ...formValues,
+          });
+        },
+      },
+    },
+    rowConfig: {
+      keyField: 'id',
+      isCurrent: props.selectable,
+      isHover: true,
+    },
+    toolbarConfig: { refresh: true, search: true },
+  } as VxeTableGridOptions<MesPdVersionChangeApi.VersionChange>,
+  gridEvents: props.selectable
+    ? {
+        cellClick: ({ row }: { row: MesPdVersionChangeApi.VersionChange }) => {
+          handleSelectRow(row);
+        },
+      }
+    : undefined,
+});
+</script>
+
+<template>
+  <FormModal @success="handleRefresh" />
+  <Grid :table-title="selectable ? '鐗堟湰鍙樻洿璁板綍' : undefined">
+    <template v-if="isEditable" #toolbar-tools>
+      <TableAction
+        :actions="[
+          {
+            label: $t('ui.actionTitle.create', ['鐗堟湰鍙樻洿']),
+            type: 'primary',
+            icon: ACTION_ICON.ADD,
+            auth: ['mes:pd-version-change:create'],
+            onClick: handleCreate,
+          },
+        ]"
+      />
+    </template>
+    <template #actions="{ row }">
+      <TableAction
+        :actions="[
+          {
+            label: isEditable ? '鏌ョ湅' : '鏌ョ湅璧勬枡',
+            type: 'link',
+            onClick: handleDetail.bind(null, row),
+          },
+          {
+            label: $t('common.edit'),
+            type: 'link',
+            icon: ACTION_ICON.EDIT,
+            auth: ['mes:pd-version-change:update'],
+            ifShow: isEditable,
+            onClick: handleEdit.bind(null, row),
+          },
+          {
+            label: '鎵瑰噯',
+            type: 'link',
+            auth: ['mes:pd-version-change:approve'],
+            ifShow: isEditable,
+            onClick: handleApprove.bind(null, row),
+          },
+          {
+            label: $t('common.delete'),
+            type: 'link',
+            danger: true,
+            icon: ACTION_ICON.DELETE,
+            auth: ['mes:pd-version-change:delete'],
+            ifShow: isEditable,
+            popConfirm: {
+              title: $t('ui.actionMessage.deleteConfirm', [row.code]),
+              confirm: handleDelete.bind(null, row),
+            },
+          },
+        ]"
+      />
+    </template>
+  </Grid>
+</template>
diff --git a/src/views/mes/pd/project/modules/workflow-action-form.vue b/src/views/mes/pd/project/modules/workflow-action-form.vue
new file mode 100644
index 0000000..1a62efa
--- /dev/null
+++ b/src/views/mes/pd/project/modules/workflow-action-form.vue
@@ -0,0 +1,126 @@
+<script lang="ts" setup>
+import type { WorkflowActionType } from '../data';
+import type { MesPdProjectApi } from '#/api/mes/pd/project';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+import { formatDate } from '#/packages/utils/src';
+
+import { Alert, Descriptions, message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+  approvePdProject,
+  archivePdProject,
+  getPdProject,
+  submitPdProjectReview,
+  updatePdProject,
+} from '#/api/mes/pd/project';
+
+import { useWorkflowActionFormSchema, WORKFLOW_ACTION_CONFIG } from '../data';
+
+const emit = defineEmits(['success']);
+
+const project = ref<MesPdProjectApi.Project>();
+const actionType = ref<WorkflowActionType>('submit-review');
+
+const actionConfig = computed(() => WORKFLOW_ACTION_CONFIG[actionType.value]);
+
+const [Form, formApi] = useVbenForm({
+  commonConfig: {
+    componentProps: { class: 'w-full' },
+    labelWidth: 100,
+  },
+  layout: 'horizontal',
+  schema: [],
+  showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    if (!project.value?.id) {
+      return;
+    }
+    modalApi.lock();
+    try {
+      const { actionRemark } = await formApi.getValues();
+      const config = actionConfig.value;
+      if (actionRemark) {
+        const mergedRemark = [project.value.remark, `${config.remarkTag}${actionRemark}`]
+          .filter(Boolean)
+          .join('\n');
+        await updatePdProject({ ...project.value, remark: mergedRemark });
+      }
+      const projectId = project.value.id;
+      switch (actionType.value) {
+        case 'approve': {
+          await approvePdProject(projectId);
+          break;
+        }
+        case 'archive': {
+          await archivePdProject(projectId);
+          break;
+        }
+        case 'submit-review': {
+          await submitPdProjectReview(projectId);
+          break;
+        }
+      }
+      emit('success');
+      message.success(config.successMessage);
+      await modalApi.close();
+    } finally {
+      modalApi.unlock();
+    }
+  },
+  async onOpenChange(isOpen: boolean) {
+    if (!isOpen) {
+      project.value = undefined;
+      await formApi.resetForm();
+      return;
+    }
+    const data = modalApi.getData<{ action: WorkflowActionType; id: number }>();
+    actionType.value = data.action;
+    formApi.setState({ schema: useWorkflowActionFormSchema(data.action) });
+    modalApi.setState({ title: WORKFLOW_ACTION_CONFIG[data.action].title });
+    modalApi.lock();
+    try {
+      project.value = await getPdProject(data.id);
+    } finally {
+      modalApi.unlock();
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal class="w-[640px]">
+    <Alert
+      class="mx-4 mb-4"
+      :message="actionConfig.alertMessage"
+      :description="actionConfig.alertDescription"
+      show-icon
+      :type="actionConfig.alertType"
+    />
+    <Descriptions
+      v-if="project"
+      bordered
+      class="mx-4 mb-4"
+      :column="2"
+      size="small"
+    >
+      <Descriptions.Item label="浠诲姟缂栫爜">{{ project.code }}</Descriptions.Item>
+      <Descriptions.Item label="浠诲姟鍚嶇О">{{ project.name }}</Descriptions.Item>
+      <Descriptions.Item label="浜у搧缂栫爜">{{ project.productCode }}</Descriptions.Item>
+      <Descriptions.Item label="浜у搧鍚嶇О">{{ project.productName }}</Descriptions.Item>
+      <Descriptions.Item label="璁捐鐗堟湰">{{ project.version }}</Descriptions.Item>
+      <Descriptions.Item label="涓昏璁″笀">{{ project.designerName || '-' }}</Descriptions.Item>
+      <Descriptions.Item label="瀹℃牳浜�">{{ project.reviewerName || '-' }}</Descriptions.Item>
+      <Descriptions.Item label="璁″垝瀹屾垚">
+        {{ project.planEndDate ? formatDate(project.planEndDate) : '-' }}
+      </Descriptions.Item>
+    </Descriptions>
+    <Form class="mx-4" />
+  </Modal>
+</template>
diff --git a/src/views/mes/pd/project/modules/workflow-timeline.vue b/src/views/mes/pd/project/modules/workflow-timeline.vue
new file mode 100644
index 0000000..58a6d8a
--- /dev/null
+++ b/src/views/mes/pd/project/modules/workflow-timeline.vue
@@ -0,0 +1,53 @@
+<script lang="ts" setup>
+import type { MesPdWorkflowApi } from '#/api/mes/pd/workflow';
+
+import { onMounted, ref, watch } from 'vue';
+
+import { Steps } from 'ant-design-vue';
+
+import { getPdWorkflowList } from '#/api/mes/pd/workflow';
+
+const props = defineProps<{
+  projectId: number;
+}>();
+
+const loading = ref(false);
+const nodes = ref<MesPdWorkflowApi.WorkflowNode[]>([]);
+const current = ref(0);
+
+async function loadData() {
+  loading.value = true;
+  try {
+    nodes.value = await getPdWorkflowList(props.projectId);
+    const activeIndex = nodes.value.findIndex((item) => item.status !== 1);
+    current.value = activeIndex === -1 ? nodes.value.length : activeIndex;
+  } finally {
+    loading.value = false;
+  }
+}
+
+watch(
+  () => props.projectId,
+  () => loadData(),
+  { immediate: true },
+);
+
+onMounted(loadData);
+</script>
+
+<template>
+  <Steps :current="current" direction="vertical" size="small">
+    <Steps.Step
+      v-for="node in nodes"
+      :key="node.id"
+      :description="node.remark"
+      :sub-title="node.operatorName"
+      :title="node.nodeName"
+    />
+    <Steps.Step
+      v-if="!loading && nodes.length === 0"
+      description="鏆傛棤娴佺▼璁板綍锛屼繚瀛橀」鐩悗灏嗚嚜鍔ㄧ敓鎴�"
+      title="寰呭惎鍔�"
+    />
+  </Steps>
+</template>
diff --git a/src/views/mes/pro/andon/record/data.ts b/src/views/mes/pro/andon/record/data.ts
index a9f1f19..b390630 100644
--- a/src/views/mes/pro/andon/record/data.ts
+++ b/src/views/mes/pro/andon/record/data.ts
@@ -11,7 +11,7 @@
 import { getSimpleUserList } from '#/api/system/user';
 import { getRangePickerDefaultProps } from '#/utils';
 import { MdWorkstationSelect } from '#/views/mes/md/workstation/components';
-import { ProProcessSelect } from '#/views/mes/pro/process/components';
+import { ProProcessSelect } from '#/views/mes/process-design/process/components';
 import { ProWorkOrderSelect } from '#/views/mes/pro/workorder/components';
 
 import { AndonConfigSelect } from '../config/components';
diff --git a/src/views/mes/pro/card/data.ts b/src/views/mes/pro/card/data.ts
index 011a355..17c3172 100644
--- a/src/views/mes/pro/card/data.ts
+++ b/src/views/mes/pro/card/data.ts
@@ -16,7 +16,7 @@
 import { generateAutoCode } from '#/api/mes/md/autocode/record';
 import { MdItemSelect } from '#/views/mes/md/item/components';
 import { MdWorkstationSelect } from '#/views/mes/md/workstation/components';
-import { ProProcessSelect } from '#/views/mes/pro/process/components';
+import { ProProcessSelect } from '#/views/mes/process-design/process/components';
 import { ProWorkOrderSelect } from '#/views/mes/pro/workorder/components';
 import { UserSelect } from '#/views/system/user/components';
 
diff --git a/src/views/mes/pro/task/data.ts b/src/views/mes/pro/task/data.ts
index ab0c5de..aeef143 100644
--- a/src/views/mes/pro/task/data.ts
+++ b/src/views/mes/pro/task/data.ts
@@ -12,8 +12,8 @@
 import { MdClientSelect } from '#/views/mes/md/client/components';
 import { MdItemSelect } from '#/views/mes/md/item/components';
 import { MdWorkstationSelect } from '#/views/mes/md/workstation/components';
-import { ProProcessSelect } from '#/views/mes/pro/process/components';
-import { RouteColorPicker } from '#/views/mes/pro/route/components';
+import { ProProcessSelect } from '#/views/mes/process-design/process/components';
+import { RouteColorPicker } from '#/views/mes/process-design/route/components';
 import { ProWorkOrderSelect } from '#/views/mes/pro/workorder/components';
 
 /** 寰呮帓浜у伐鍗曞垪琛ㄧ殑鎼滅储琛ㄥ崟 */
diff --git a/src/views/mes/process-design/parameter/data.ts b/src/views/mes/process-design/parameter/data.ts
new file mode 100644
index 0000000..e90cfed
--- /dev/null
+++ b/src/views/mes/process-design/parameter/data.ts
@@ -0,0 +1,197 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesPdParameterApi } from '#/api/mes/pro/processdesign/parameter';
+
+import { DICT_TYPE } from '../../../../packages/constants/src';
+import { getDictOptions } from '../../../../packages/effects/hooks/src';
+
+import { z } from '#/adapter/form';
+
+/** 鍙傛暟绫诲瀷 - 涓嬫媺閫夐」鐨勫�� */
+const PARAM_TYPE_SELECT = 3;
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'detail' | 'update';
+
+/** 鏂板/淇敼宸ヨ壓鍙傛暟鐨勮〃鍗� */
+export function useFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: {
+        triggerFields: [''],
+        show: () => false,
+      },
+    },
+    {
+      fieldName: 'code',
+      label: '鍙傛暟缂栫爜',
+      component: 'Input',
+      componentProps: {
+        maxLength: 64,
+        placeholder: '璇疯緭鍏ュ弬鏁扮紪鐮�',
+      },
+      rules: z.string().min(1, '鍙傛暟缂栫爜涓嶈兘涓虹┖').max(64),
+    },
+    {
+      fieldName: 'name',
+      label: '鍙傛暟鍚嶇О',
+      component: 'Input',
+      componentProps: {
+        maxLength: 100,
+        placeholder: '璇疯緭鍏ュ弬鏁板悕绉�',
+      },
+      rules: z.string().min(1, '鍙傛暟鍚嶇О涓嶈兘涓虹┖').max(100),
+    },
+    {
+      fieldName: 'type',
+      label: '鍙傛暟绫诲瀷',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: getDictOptions(DICT_TYPE.MES_PD_PARAM_TYPE, 'number'),
+        placeholder: '璇烽�夋嫨鍙傛暟绫诲瀷',
+      },
+      rules: 'selectRequired',
+    },
+    {
+      fieldName: 'unit',
+      label: '鍗曚綅',
+      component: 'Input',
+      componentProps: {
+        maxLength: 20,
+        placeholder: '璇疯緭鍏ュ崟浣�',
+      },
+    },
+    {
+      fieldName: 'requiredFlag',
+      label: '鏄惁蹇呭~',
+      component: 'Switch',
+      componentProps: {
+        checkedChildren: '鏄�',
+        unCheckedChildren: '鍚�',
+      },
+      rules: z.boolean().default(false),
+    },
+    {
+      fieldName: 'dictType',
+      label: '瀛楀吀绫诲瀷',
+      component: 'Input',
+      componentProps: {
+        maxLength: 64,
+        placeholder: '璇疯緭鍏ュ瓧鍏哥被鍨嬶紙涓嬫媺閫夐」鏃堕厤缃級',
+      },
+      dependencies: {
+        triggerFields: ['type'],
+        show: (values) => values?.type === PARAM_TYPE_SELECT,
+      },
+    },
+    {
+      fieldName: 'apiUrl',
+      label: '鎺ュ彛鍦板潃',
+      component: 'Input',
+      componentProps: {
+        maxLength: 250,
+        placeholder: '璇疯緭鍏ユ帴鍙e湴鍧�锛堜笅鎷夐�夐」鏃堕厤缃級',
+      },
+      dependencies: {
+        triggerFields: ['type'],
+        show: (values) => values?.type === PARAM_TYPE_SELECT,
+      },
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      formItemClass: 'col-span-2',
+      componentProps: {
+        maxLength: 250,
+        placeholder: '璇疯緭鍏ュ娉�',
+        rows: 3,
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'code',
+      label: '鍙傛暟缂栫爜',
+      component: 'Input',
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇疯緭鍏ュ弬鏁扮紪鐮�',
+      },
+    },
+    {
+      fieldName: 'name',
+      label: '鍙傛暟鍚嶇О',
+      component: 'Input',
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇疯緭鍏ュ弬鏁板悕绉�',
+      },
+    },
+    {
+      fieldName: 'type',
+      label: '鍙傛暟绫诲瀷',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: getDictOptions(DICT_TYPE.MES_PD_PARAM_TYPE, 'number'),
+        placeholder: '璇烽�夋嫨鍙傛暟绫诲瀷',
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions<MesPdParameterApi.Parameter>['columns'] {
+  return [
+    {
+      type: 'seq',
+      title: '搴忓彿',
+      width: 60,
+      align: 'center',
+    },
+    { field: 'code', title: '鍙傛暟缂栫爜', minWidth: 150 },
+    { field: 'name', title: '鍙傛暟鍚嶇О', minWidth: 180 },
+    {
+      field: 'type',
+      title: '鍙傛暟绫诲瀷',
+      width: 120,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.MES_PD_PARAM_TYPE },
+      },
+    },
+    { field: 'unit', title: '鍗曚綅', width: 80 },
+    {
+      field: 'requiredFlag',
+      title: '鏄惁蹇呭~',
+      width: 90,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
+      },
+    },
+    { field: 'remark', title: '澶囨敞', minWidth: 180 },
+    {
+      field: 'createTime',
+      title: '鍒涘缓鏃堕棿',
+      width: 180,
+      formatter: 'formatDateTime',
+    },
+    {
+      title: '鎿嶄綔',
+      width: 150,
+      fixed: 'right',
+      slots: {
+        default: 'actions',
+      },
+    },
+  ];
+}
diff --git a/src/views/mes/md/unitmeasure/index.vue b/src/views/mes/process-design/parameter/index.vue
similarity index 63%
copy from src/views/mes/md/unitmeasure/index.vue
copy to src/views/mes/process-design/parameter/index.vue
index f9f49d4..dfd7e51 100644
--- a/src/views/mes/md/unitmeasure/index.vue
+++ b/src/views/mes/process-design/parameter/index.vue
@@ -1,18 +1,16 @@
 <script lang="ts" setup>
 import type { VxeTableGridOptions } from '#/adapter/vxe-table';
-import type { MesMdUnitMeasureApi } from '#/api/mes/md/unitmeasure';
+import type { MesPdParameterApi } from '#/api/mes/pro/processdesign/parameter';
 
 import { Page, useVbenModal } from '../../../../packages/effects/common-ui/src';
-import { downloadFileFromBlobPart } from '../../../../packages/utils/src';
 
-import { message } from 'ant-design-vue';
+import { Button, message } from 'ant-design-vue';
 
 import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
 import {
-  deleteUnitMeasure,
-  exportUnitMeasure,
-  getUnitMeasurePage,
-} from '#/api/mes/md/unitmeasure';
+  deleteParameter,
+  getParameterPage,
+} from '#/api/mes/pro/processdesign/parameter';
 import { $t } from '#/locales';
 
 import { useGridColumns, useGridFormSchema } from './data';
@@ -28,30 +26,29 @@
   gridApi.query();
 }
 
-/** 瀵煎嚭琛ㄦ牸 */
-async function handleExport() {
-  const data = await exportUnitMeasure(await gridApi.formApi.getValues());
-  downloadFileFromBlobPart({ fileName: '璁¢噺鍗曚綅.xls', source: data });
-}
-
-/** 鍒涘缓璁¢噺鍗曚綅 */
+/** 鍒涘缓宸ヨ壓鍙傛暟 */
 function handleCreate() {
-  formModalApi.setData(null).open();
+  formModalApi.setData({ formType: 'create' }).open();
 }
 
-/** 缂栬緫璁¢噺鍗曚綅 */
-function handleEdit(row: MesMdUnitMeasureApi.UnitMeasure) {
-  formModalApi.setData(row).open();
+/** 缂栬緫宸ヨ壓鍙傛暟 */
+function handleEdit(row: MesPdParameterApi.Parameter) {
+  formModalApi.setData({ id: row.id, formType: 'update' }).open();
 }
 
-/** 鍒犻櫎璁¢噺鍗曚綅 */
-async function handleDelete(row: MesMdUnitMeasureApi.UnitMeasure) {
+/** 鏌ョ湅宸ヨ壓鍙傛暟璇︽儏 */
+function handleDetail(row: MesPdParameterApi.Parameter) {
+  formModalApi.setData({ id: row.id, formType: 'detail' }).open();
+}
+
+/** 鍒犻櫎宸ヨ壓鍙傛暟 */
+async function handleDelete(row: MesPdParameterApi.Parameter) {
   const hideLoading = message.loading({
     content: $t('ui.actionMessage.deleting', [row.name]),
     duration: 0,
   });
   try {
-    await deleteUnitMeasure(row.id!);
+    await deleteParameter(row.id!);
     message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
     handleRefresh();
   } finally {
@@ -70,7 +67,7 @@
     proxyConfig: {
       ajax: {
         query: async ({ page }, formValues) => {
-          return await getUnitMeasurePage({
+          return await getParameterPage({
             pageNo: page.currentPage,
             pageSize: page.pageSize,
             ...formValues,
@@ -79,39 +76,35 @@
       },
     },
     rowConfig: {
-      keyField: 'id',
       isHover: true,
+      keyField: 'id',
     },
     toolbarConfig: {
       refresh: true,
       search: true,
     },
-  } as VxeTableGridOptions<MesMdUnitMeasureApi.UnitMeasure>,
+  } as VxeTableGridOptions<MesPdParameterApi.Parameter>,
 });
 </script>
 
 <template>
   <Page auto-content-height><FormModal @success="handleRefresh" />
-    <Grid table-title="璁¢噺鍗曚綅鍒楄〃">
+    <Grid table-title="宸ヨ壓鍙傛暟鍒楄〃">
       <template #toolbar-tools>
         <TableAction
           :actions="[
             {
-              label: $t('ui.actionTitle.create', ['璁¢噺鍗曚綅']),
+              label: $t('ui.actionTitle.create', ['鍙傛暟']),
               type: 'primary',
               icon: ACTION_ICON.ADD,
-              auth: ['mes:md-unit-measure:create'],
+              auth: ['mes:pd:parameter:create'],
               onClick: handleCreate,
-            },
-            {
-              label: $t('ui.actionTitle.export'),
-              type: 'primary',
-              icon: ACTION_ICON.DOWNLOAD,
-              auth: ['mes:md-unit-measure:export'],
-              onClick: handleExport,
             },
           ]"
         />
+      </template>
+      <template #code="{ row }">
+        <Button type="link" @click="handleDetail(row)">{{ row.code }}</Button>
       </template>
       <template #actions="{ row }">
         <TableAction
@@ -120,7 +113,7 @@
               label: $t('common.edit'),
               type: 'link',
               icon: ACTION_ICON.EDIT,
-              auth: ['mes:md-unit-measure:update'],
+              auth: ['mes:pd:parameter:update'],
               onClick: handleEdit.bind(null, row),
             },
             {
@@ -128,7 +121,7 @@
               type: 'link',
               danger: true,
               icon: ACTION_ICON.DELETE,
-              auth: ['mes:md-unit-measure:delete'],
+              auth: ['mes:pd:parameter:delete'],
               popConfirm: {
                 title: $t('ui.actionMessage.deleteConfirm', [row.name]),
                 confirm: handleDelete.bind(null, row),
diff --git a/src/views/mes/process-design/parameter/modules/form.vue b/src/views/mes/process-design/parameter/modules/form.vue
new file mode 100644
index 0000000..122a5a8
--- /dev/null
+++ b/src/views/mes/process-design/parameter/modules/form.vue
@@ -0,0 +1,99 @@
+<script lang="ts" setup>
+import type { FormType } from '../data';
+
+import type { MesPdParameterApi } from '#/api/mes/pro/processdesign/parameter';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+  createParameter,
+  getParameter,
+  updateParameter,
+} from '#/api/mes/pro/processdesign/parameter';
+import { $t } from '#/locales';
+
+import { useFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+const formType = ref<FormType>('create');
+const formData = ref<MesPdParameterApi.Parameter>();
+
+const isDetail = computed(() => formType.value === 'detail');
+const getTitle = computed(() => {
+  if (formType.value === 'detail') {
+    return $t('ui.actionTitle.detail', ['宸ヨ壓鍙傛暟']);
+  }
+  return formType.value === 'update'
+    ? $t('ui.actionTitle.edit', ['宸ヨ壓鍙傛暟'])
+    : $t('ui.actionTitle.create', ['宸ヨ壓鍙傛暟']);
+});
+
+const [Form, formApi] = useVbenForm({
+  commonConfig: {
+    componentProps: {
+      class: 'w-full',
+    },
+    formItemClass: 'col-span-1',
+    labelWidth: 100,
+  },
+  layout: 'horizontal',
+  schema: [],
+  showDefaultActions: false,
+  wrapperClass: 'grid-cols-2',
+});
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    if (isDetail.value) {
+      await modalApi.close();
+      return;
+    }
+    const { valid } = await formApi.validate();
+    if (!valid) {
+      return;
+    }
+    modalApi.lock();
+    const data = (await formApi.getValues()) as MesPdParameterApi.Parameter;
+    try {
+      await (formData.value?.id ? updateParameter(data) : createParameter(data));
+      await modalApi.close();
+      emit('success');
+      message.success($t('ui.actionMessage.operationSuccess'));
+    } finally {
+      modalApi.unlock();
+    }
+  },
+  async onOpenChange(isOpen: boolean) {
+    if (!isOpen) {
+      formData.value = undefined;
+      return;
+    }
+    formApi.setState({ schema: useFormSchema() });
+    const data = modalApi.getData<{ formType: FormType; id?: number }>();
+    formType.value = data.formType;
+    formApi.setDisabled(formType.value === 'detail');
+    modalApi.setState({ showConfirmButton: formType.value !== 'detail' });
+    if (!data?.id) {
+      return;
+    }
+    modalApi.lock();
+    try {
+      formData.value = await getParameter(data.id);
+      await formApi.setValues(formData.value);
+    } finally {
+      modalApi.unlock();
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal :title="getTitle" class="w-3/5">
+    <Form class="mx-4" />
+  </Modal>
+</template>
diff --git a/src/views/mes/process-design/process/components/index.ts b/src/views/mes/process-design/process/components/index.ts
new file mode 100644
index 0000000..4204017
--- /dev/null
+++ b/src/views/mes/process-design/process/components/index.ts
@@ -0,0 +1 @@
+export { default as ProProcessSelect } from './select.vue';
diff --git a/src/views/mes/process-design/process/components/select.vue b/src/views/mes/process-design/process/components/select.vue
new file mode 100644
index 0000000..ba87738
--- /dev/null
+++ b/src/views/mes/process-design/process/components/select.vue
@@ -0,0 +1,114 @@
+<script lang="ts" setup>
+import type { MesProProcessApi } from '#/api/mes/pro/process';
+
+import { computed, onMounted, ref, watch } from 'vue';
+
+import { Select, Tag, Tooltip } from 'ant-design-vue';
+
+import { getProcessSimpleList } from '#/api/mes/pro/process';
+
+defineOptions({ name: 'ProProcessSelect', inheritAttrs: false });
+
+const props = withDefaults(
+  defineProps<{
+    allowClear?: boolean;
+    disabled?: boolean;
+    modelValue?: number;
+    placeholder?: string;
+  }>(),
+  {
+    allowClear: true,
+    disabled: false,
+    modelValue: undefined,
+    placeholder: '璇烽�夋嫨宸ュ簭',
+  },
+);
+
+const emit = defineEmits<{
+  change: [item: MesProProcessApi.Process | undefined];
+  'update:modelValue': [value: number | undefined];
+}>();
+
+const allList = ref<MesProProcessApi.Process[]>([]);
+const selectedItem = ref<MesProProcessApi.Process>();
+
+const selectValue = computed({
+  get: () => props.modelValue,
+  set: (value: number | undefined) => {
+    emit('update:modelValue', value);
+  },
+});
+
+/** 鍓嶇杩囨护锛氭寜宸ュ簭鍚嶇О鎴栫紪鐮佹ā绯婂尮閰� */
+function handleFilter(input: string, option: any) {
+  const keyword = input.toLowerCase();
+  const item = option?.item as MesProProcessApi.Process | undefined;
+  return Boolean(
+    item?.name?.toLowerCase().includes(keyword) ||
+    item?.code?.toLowerCase().includes(keyword),
+  );
+}
+
+/** 鏍规嵁褰撳墠鍊煎悓姝� tooltip 灞曠ず鐨勫伐搴忚鎯� */
+function syncSelectedItem(value: number | undefined) {
+  selectedItem.value =
+    value === undefined
+      ? undefined
+      : allList.value.find((item) => item.id === value);
+}
+
+/** 闄� v-model 澶栵紝棰濆鎶涘嚭瀹屾暣宸ュ簭瀵硅薄缁欎笟鍔¤〃鍗曚娇鐢� */
+function handleChange(value: any) {
+  const nextValue = value === undefined ? undefined : Number(value);
+  syncSelectedItem(nextValue);
+  emit('change', selectedItem.value);
+}
+
+watch(
+  () => props.modelValue,
+  (value) => {
+    syncSelectedItem(value);
+  },
+);
+
+onMounted(async () => {
+  allList.value = await getProcessSimpleList();
+  syncSelectedItem(props.modelValue);
+});
+</script>
+
+<template>
+  <Tooltip :mouse-enter-delay="0.5" :open="selectedItem ? undefined : false">
+    <template #title>
+      <div v-if="selectedItem" class="leading-6">
+        <div>缂栫爜锛歿{ selectedItem.code || '-' }}</div>
+        <div>鍚嶇О锛歿{ selectedItem.name || '-' }}</div>
+        <div>宸ヨ壓瑕佹眰锛歿{ selectedItem.attention || '-' }}</div>
+        <div>澶囨敞锛歿{ selectedItem.remark || '-' }}</div>
+      </div>
+    </template>
+    <Select
+      v-bind="$attrs"
+      v-model:value="selectValue"
+      :allow-clear="allowClear"
+      :disabled="disabled"
+      :filter-option="handleFilter"
+      :placeholder="placeholder"
+      class="w-full"
+      show-search
+      @change="handleChange"
+    >
+      <Select.Option
+        v-for="item in allList"
+        :key="item.id"
+        :item="item"
+        :value="item.id"
+      >
+        <div class="flex items-center gap-2">
+          <span>{{ item.name }}</span>
+          <Tag v-if="item.code" color="default">{{ item.code }}</Tag>
+        </div>
+      </Select.Option>
+    </Select>
+  </Tooltip>
+</template>
diff --git a/src/views/mes/process-design/process/data.ts b/src/views/mes/process-design/process/data.ts
new file mode 100644
index 0000000..d6ce88a
--- /dev/null
+++ b/src/views/mes/process-design/process/data.ts
@@ -0,0 +1,274 @@
+import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesProProcessApi } from '#/api/mes/pro/process';
+import type { MesProProcessContentApi } from '#/api/mes/pro/process/content';
+
+import { h } from 'vue';
+
+import {
+  CommonStatusEnum,
+  DICT_TYPE,
+  MesAutoCodeRuleCode,
+} from '../../../../packages/constants/src';
+import { getDictOptions } from '../../../../packages/effects/hooks/src';
+
+import { Button } from 'ant-design-vue';
+
+import { z } from '#/adapter/form';
+import { generateAutoCode } from '#/api/mes/md/autocode/record';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'detail' | 'update';
+
+/** 鏂板/淇敼鐢熶骇宸ュ簭鐨勮〃鍗� */
+export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: {
+        triggerFields: [''],
+        show: () => false,
+      },
+    },
+    {
+      fieldName: 'code',
+      label: '宸ュ簭缂栫爜',
+      component: 'Input',
+      componentProps: {
+        maxLength: 64,
+        placeholder: '璇疯緭鍏ュ伐搴忕紪鐮�',
+      },
+      rules: z.string().min(1, '宸ュ簭缂栫爜涓嶈兘涓虹┖').max(64),
+      suffix: () =>
+        h(
+          Button,
+          {
+            type: 'default',
+            onClick: async () => {
+              const code = await generateAutoCode(
+                MesAutoCodeRuleCode.PRO_PROCESS_CODE,
+              );
+              await formApi?.setFieldValue('code', code);
+            },
+          },
+          { default: () => '鐢熸垚' },
+        ),
+    },
+    {
+      fieldName: 'name',
+      label: '宸ュ簭鍚嶇О',
+      component: 'Input',
+      componentProps: {
+        maxLength: 100,
+        placeholder: '璇疯緭鍏ュ伐搴忓悕绉�',
+      },
+      rules: z.string().min(1, '宸ュ簭鍚嶇О涓嶈兘涓虹┖').max(100),
+    },
+    {
+      fieldName: 'status',
+      label: '鐘舵��',
+      component: 'RadioGroup',
+      componentProps: {
+        buttonStyle: 'solid',
+        optionType: 'button',
+        options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+      },
+      rules: z.number().default(CommonStatusEnum.ENABLE),
+    },
+    {
+      fieldName: 'attention',
+      label: '宸ュ簭璇存槑',
+      component: 'Textarea',
+      formItemClass: 'col-span-3',
+      componentProps: {
+        maxLength: 500,
+        placeholder: '璇疯緭鍏ュ伐搴忚鏄�',
+        rows: 3,
+      },
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      formItemClass: 'col-span-3',
+      componentProps: {
+        maxLength: 250,
+        placeholder: '璇疯緭鍏ュ娉�',
+        rows: 3,
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'code',
+      label: '宸ュ簭缂栫爜',
+      component: 'Input',
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇疯緭鍏ュ伐搴忕紪鐮�',
+      },
+    },
+    {
+      fieldName: 'name',
+      label: '宸ュ簭鍚嶇О',
+      component: 'Input',
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇疯緭鍏ュ伐搴忓悕绉�',
+      },
+    },
+    {
+      fieldName: 'status',
+      label: '鐘舵��',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+        placeholder: '璇烽�夋嫨鐘舵��',
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions<MesProProcessApi.Process>['columns'] {
+  return [
+    {
+      field: 'code',
+      title: '宸ュ簭缂栫爜',
+      minWidth: 150,
+      slots: {
+        default: 'code',
+      },
+    },
+    { field: 'name', title: '宸ュ簭鍚嶇О', minWidth: 180 },
+    {
+      field: 'status',
+      title: '鐘舵��',
+      width: 100,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.COMMON_STATUS },
+      },
+    },
+    { field: 'remark', title: '澶囨敞', minWidth: 180 },
+    {
+      field: 'createTime',
+      title: '鍒涘缓鏃堕棿',
+      width: 180,
+      formatter: 'formatDateTime',
+    },
+    {
+      title: '鎿嶄綔',
+      width: 150,
+      fixed: 'right',
+      slots: {
+        default: 'actions',
+      },
+    },
+  ];
+}
+
+/** 宸ュ簭鍐呭锛堟搷浣滄楠わ級琛ㄥ崟 */
+export function useContentFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: {
+        triggerFields: [''],
+        show: () => false,
+      },
+    },
+    {
+      fieldName: 'processId',
+      component: 'Input',
+      dependencies: {
+        triggerFields: [''],
+        show: () => false,
+      },
+    },
+    {
+      fieldName: 'sort',
+      label: '搴忓彿',
+      component: 'InputNumber',
+      componentProps: {
+        class: '!w-full',
+        max: 999,
+        min: 1,
+        precision: 0,
+      },
+      rules: z.number().default(1),
+    },
+    {
+      fieldName: 'content',
+      label: '姝ラ璇存槑',
+      component: 'Textarea',
+      componentProps: {
+        maxLength: 500,
+        placeholder: '璇疯緭鍏ユ楠よ鏄�',
+        rows: 3,
+      },
+    },
+    {
+      fieldName: 'device',
+      label: '杈呭姪璁惧',
+      component: 'Input',
+      componentProps: {
+        maxLength: 100,
+        placeholder: '璇疯緭鍏ヨ緟鍔╄澶�',
+      },
+    },
+    {
+      fieldName: 'material',
+      label: '杈呭姪鏉愭枡',
+      component: 'Input',
+      componentProps: {
+        maxLength: 100,
+        placeholder: '璇疯緭鍏ヨ緟鍔╂潗鏂�',
+      },
+    },
+    {
+      fieldName: 'docUrl',
+      label: '鏉愭枡鏂囨。 URL',
+      component: 'Input',
+      componentProps: {
+        maxLength: 250,
+        placeholder: '璇疯緭鍏ユ潗鏂欐枃妗� URL',
+      },
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      componentProps: {
+        maxLength: 250,
+        placeholder: '璇疯緭鍏ュ娉�',
+        rows: 2,
+      },
+    },
+  ];
+}
+
+/** 宸ュ簭鍐呭鍒楄〃鐨勫瓧娈� */
+export function useContentGridColumns(): VxeTableGridOptions<MesProProcessContentApi.ProcessContent>['columns'] {
+  return [
+    { field: 'sort', title: '搴忓彿', width: 80, align: 'center' },
+    { field: 'content', title: '姝ラ璇存槑', minWidth: 220 },
+    { field: 'device', title: '杈呭姪璁惧', width: 150 },
+    { field: 'material', title: '杈呭姪鏉愭枡', width: 150 },
+    { field: 'docUrl', title: '鏉愭枡鏂囨。', minWidth: 180 },
+    { field: 'remark', title: '澶囨敞', minWidth: 160 },
+    {
+      title: '鎿嶄綔',
+      width: 130,
+      fixed: 'right',
+      slots: { default: 'actions' },
+    },
+  ];
+}
diff --git a/src/views/mes/md/unitmeasure/index.vue b/src/views/mes/process-design/process/index.vue
similarity index 66%
rename from src/views/mes/md/unitmeasure/index.vue
rename to src/views/mes/process-design/process/index.vue
index f9f49d4..8662934 100644
--- a/src/views/mes/md/unitmeasure/index.vue
+++ b/src/views/mes/process-design/process/index.vue
@@ -1,18 +1,18 @@
 <script lang="ts" setup>
 import type { VxeTableGridOptions } from '#/adapter/vxe-table';
-import type { MesMdUnitMeasureApi } from '#/api/mes/md/unitmeasure';
+import type { MesProProcessApi } from '#/api/mes/pro/process';
 
 import { Page, useVbenModal } from '../../../../packages/effects/common-ui/src';
 import { downloadFileFromBlobPart } from '../../../../packages/utils/src';
 
-import { message } from 'ant-design-vue';
+import { Button, message } from 'ant-design-vue';
 
 import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
 import {
-  deleteUnitMeasure,
-  exportUnitMeasure,
-  getUnitMeasurePage,
-} from '#/api/mes/md/unitmeasure';
+  deleteProcess,
+  exportProcess,
+  getProcessPage,
+} from '#/api/mes/pro/process';
 import { $t } from '#/locales';
 
 import { useGridColumns, useGridFormSchema } from './data';
@@ -28,35 +28,40 @@
   gridApi.query();
 }
 
-/** 瀵煎嚭琛ㄦ牸 */
-async function handleExport() {
-  const data = await exportUnitMeasure(await gridApi.formApi.getValues());
-  downloadFileFromBlobPart({ fileName: '璁¢噺鍗曚綅.xls', source: data });
-}
-
-/** 鍒涘缓璁¢噺鍗曚綅 */
+/** 鍒涘缓鐢熶骇宸ュ簭 */
 function handleCreate() {
-  formModalApi.setData(null).open();
+  formModalApi.setData({ formType: 'create' }).open();
 }
 
-/** 缂栬緫璁¢噺鍗曚綅 */
-function handleEdit(row: MesMdUnitMeasureApi.UnitMeasure) {
-  formModalApi.setData(row).open();
+/** 缂栬緫鐢熶骇宸ュ簭 */
+function handleEdit(row: MesProProcessApi.Process) {
+  formModalApi.setData({ id: row.id, formType: 'update' }).open();
 }
 
-/** 鍒犻櫎璁¢噺鍗曚綅 */
-async function handleDelete(row: MesMdUnitMeasureApi.UnitMeasure) {
+/** 鏌ョ湅鐢熶骇宸ュ簭璇︽儏 */
+function handleDetail(row: MesProProcessApi.Process) {
+  formModalApi.setData({ id: row.id, formType: 'detail' }).open();
+}
+
+/** 鍒犻櫎鐢熶骇宸ュ簭 */
+async function handleDelete(row: MesProProcessApi.Process) {
   const hideLoading = message.loading({
     content: $t('ui.actionMessage.deleting', [row.name]),
     duration: 0,
   });
   try {
-    await deleteUnitMeasure(row.id!);
+    await deleteProcess(row.id!);
     message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
     handleRefresh();
   } finally {
     hideLoading();
   }
+}
+
+/** 瀵煎嚭鐢熶骇宸ュ簭 */
+async function handleExport() {
+  const data = await exportProcess(await gridApi.formApi.getValues());
+  downloadFileFromBlobPart({ fileName: '鐢熶骇宸ュ簭.xls', source: data });
 }
 
 const [Grid, gridApi] = useVbenVxeGrid({
@@ -70,7 +75,7 @@
     proxyConfig: {
       ajax: {
         query: async ({ page }, formValues) => {
-          return await getUnitMeasurePage({
+          return await getProcessPage({
             pageNo: page.currentPage,
             pageSize: page.pageSize,
             ...formValues,
@@ -79,39 +84,42 @@
       },
     },
     rowConfig: {
-      keyField: 'id',
       isHover: true,
+      keyField: 'id',
     },
     toolbarConfig: {
       refresh: true,
       search: true,
     },
-  } as VxeTableGridOptions<MesMdUnitMeasureApi.UnitMeasure>,
+  } as VxeTableGridOptions<MesProProcessApi.Process>,
 });
 </script>
 
 <template>
   <Page auto-content-height><FormModal @success="handleRefresh" />
-    <Grid table-title="璁¢噺鍗曚綅鍒楄〃">
+    <Grid table-title="鐢熶骇宸ュ簭鍒楄〃">
       <template #toolbar-tools>
         <TableAction
           :actions="[
             {
-              label: $t('ui.actionTitle.create', ['璁¢噺鍗曚綅']),
+              label: $t('ui.actionTitle.create', ['宸ュ簭']),
               type: 'primary',
               icon: ACTION_ICON.ADD,
-              auth: ['mes:md-unit-measure:create'],
+              auth: ['mes:pro-process:create'],
               onClick: handleCreate,
             },
             {
               label: $t('ui.actionTitle.export'),
               type: 'primary',
               icon: ACTION_ICON.DOWNLOAD,
-              auth: ['mes:md-unit-measure:export'],
+              auth: ['mes:pro-process:export'],
               onClick: handleExport,
             },
           ]"
         />
+      </template>
+      <template #code="{ row }">
+        <Button type="link" @click="handleDetail(row)">{{ row.code }}</Button>
       </template>
       <template #actions="{ row }">
         <TableAction
@@ -120,7 +128,7 @@
               label: $t('common.edit'),
               type: 'link',
               icon: ACTION_ICON.EDIT,
-              auth: ['mes:md-unit-measure:update'],
+              auth: ['mes:pro-process:update'],
               onClick: handleEdit.bind(null, row),
             },
             {
@@ -128,7 +136,7 @@
               type: 'link',
               danger: true,
               icon: ACTION_ICON.DELETE,
-              auth: ['mes:md-unit-measure:delete'],
+              auth: ['mes:pro-process:delete'],
               popConfirm: {
                 title: $t('ui.actionMessage.deleteConfirm', [row.name]),
                 confirm: handleDelete.bind(null, row),
diff --git a/src/views/mes/process-design/process/modules/content-form.vue b/src/views/mes/process-design/process/modules/content-form.vue
new file mode 100644
index 0000000..a3d79dd
--- /dev/null
+++ b/src/views/mes/process-design/process/modules/content-form.vue
@@ -0,0 +1,98 @@
+<script lang="ts" setup>
+import type { MesProProcessContentApi } from '#/api/mes/pro/process/content';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+  createProcessContent,
+  getProcessContent,
+  updateProcessContent,
+} from '#/api/mes/pro/process/content';
+import { $t } from '#/locales';
+
+import { useContentFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+const formData = ref<MesProProcessContentApi.ProcessContent>();
+const getTitle = computed(() => {
+  return formData.value?.id
+    ? $t('ui.actionTitle.edit', ['宸ュ簭姝ラ'])
+    : $t('ui.actionTitle.create', ['宸ュ簭姝ラ']);
+});
+
+const [Form, formApi] = useVbenForm({
+  commonConfig: {
+    componentProps: {
+      class: 'w-full',
+    },
+    formItemClass: 'col-span-2',
+    labelWidth: 120,
+  },
+  layout: 'horizontal',
+  schema: useContentFormSchema(),
+  showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    const { valid } = await formApi.validate();
+    if (!valid) {
+      return;
+    }
+    modalApi.lock();
+    // 鎻愪氦琛ㄥ崟
+    const data =
+      (await formApi.getValues()) as MesProProcessContentApi.ProcessContent;
+    try {
+      await (formData.value?.id
+        ? updateProcessContent(data)
+        : createProcessContent(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<{
+      id?: number;
+      maxSort?: number;
+      processId: number;
+    }>();
+    if (!data?.id) {
+      // 鏂板鏃讹紝榛樿搴忓彿 = maxSort + 1
+      await formApi.setValues({
+        processId: data?.processId,
+        sort: (data?.maxSort || 0) + 1,
+      });
+      return;
+    }
+    modalApi.lock();
+    try {
+      formData.value = await getProcessContent(data.id);
+      // 璁剧疆鍒� values
+      await formApi.setValues(formData.value);
+    } finally {
+      modalApi.unlock();
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal :title="getTitle" class="w-2/5">
+    <Form class="mx-4" />
+  </Modal>
+</template>
diff --git a/src/views/mes/process-design/process/modules/content-list.vue b/src/views/mes/process-design/process/modules/content-list.vue
new file mode 100644
index 0000000..4ee6c92
--- /dev/null
+++ b/src/views/mes/process-design/process/modules/content-list.vue
@@ -0,0 +1,132 @@
+<script lang="ts" setup>
+import type { FormType } from '../data';
+
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesProProcessContentApi } from '#/api/mes/pro/process/content';
+
+import { ref, watch } from 'vue';
+
+import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
+
+import { message } from 'ant-design-vue';
+
+import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+  deleteProcessContent,
+  getProcessContentListByProcessId,
+} from '#/api/mes/pro/process/content';
+import { $t } from '#/locales';
+
+import { useContentGridColumns } from '../data';
+import ContentForm from './content-form.vue';
+
+const props = defineProps<{
+  formType: FormType;
+  processId: number;
+}>();
+
+const isEditable = ref(props.formType !== 'detail'); // 鏄惁鍙紪杈�
+const list = ref<MesProProcessContentApi.ProcessContent[]>([]);
+
+const [ContentFormModal, contentFormModalApi] = useVbenModal({
+  connectedComponent: ContentForm,
+  destroyOnClose: true,
+});
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  gridOptions: {
+    autoResize: true,
+    border: true,
+    columns: useContentGridColumns(),
+    data: list.value,
+    minHeight: 240,
+    pagerConfig: { enabled: false },
+    rowConfig: {
+      isHover: true,
+      keyField: 'id',
+    },
+    showOverflow: true,
+    toolbarConfig: { enabled: false },
+  } as VxeTableGridOptions<MesProProcessContentApi.ProcessContent>,
+});
+
+/** 鍔犺浇宸ュ簭鍐呭鍒楄〃 */
+async function getList() {
+  gridApi.setLoading(true);
+  try {
+    list.value = await getProcessContentListByProcessId(props.processId);
+    gridApi.setGridOptions({ data: list.value });
+  } finally {
+    gridApi.setLoading(false);
+  }
+}
+
+/** 鏂板宸ュ簭姝ラ */
+function handleCreate() {
+  const maxSort = Math.max(0, ...list.value.map((item) => item.sort || 0));
+  contentFormModalApi.setData({ maxSort, processId: props.processId }).open();
+}
+
+/** 缂栬緫宸ュ簭姝ラ */
+function handleEdit(row: MesProProcessContentApi.ProcessContent) {
+  contentFormModalApi
+    .setData({ id: row.id, processId: props.processId })
+    .open();
+}
+
+/** 鍒犻櫎宸ュ簭姝ラ */
+async function handleDelete(row: MesProProcessContentApi.ProcessContent) {
+  await deleteProcessContent(row.id!);
+  message.success($t('ui.actionMessage.deleteSuccess', ['宸ュ簭姝ラ']));
+  await getList();
+}
+
+watch(
+  () => props.processId,
+  (value) => {
+    if (value) {
+      getList();
+    }
+  },
+  { immediate: true },
+);
+</script>
+
+<template>
+  <ContentFormModal @success="getList" />
+  <div v-if="isEditable" class="mb-3 flex items-center justify-start">
+    <TableAction
+      :actions="[
+        {
+          label: $t('ui.actionTitle.create', ['姝ラ']),
+          type: 'primary',
+          onClick: handleCreate,
+        },
+      ]"
+    />
+  </div>
+  <Grid class="w-full" table-title="宸ュ簭鎿嶄綔姝ラ">
+    <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', ['宸ュ簭姝ラ']),
+              confirm: handleDelete.bind(null, row),
+            },
+          },
+        ]"
+      />
+    </template>
+  </Grid>
+</template>
diff --git a/src/views/mes/process-design/process/modules/form.vue b/src/views/mes/process-design/process/modules/form.vue
new file mode 100644
index 0000000..f88df1e
--- /dev/null
+++ b/src/views/mes/process-design/process/modules/form.vue
@@ -0,0 +1,114 @@
+<script lang="ts" setup>
+import type { FormType } from '../data';
+
+import type { MesProProcessApi } from '#/api/mes/pro/process';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
+
+import { Divider, message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+  createProcess,
+  getProcess,
+  updateProcess,
+} from '#/api/mes/pro/process';
+import { $t } from '#/locales';
+
+import { useFormSchema } from '../data';
+import ContentList from './content-list.vue';
+
+const emit = defineEmits(['success']);
+const formType = ref<FormType>('create'); // 琛ㄥ崟妯″紡
+const formData = ref<MesProProcessApi.Process>();
+
+const isDetail = computed(() => formType.value === 'detail'); // 鏄惁鏌ョ湅妯″紡
+const getTitle = computed(() => {
+  if (formType.value === 'detail') {
+    return $t('ui.actionTitle.detail', ['鐢熶骇宸ュ簭']);
+  }
+  return formType.value === 'update'
+    ? $t('ui.actionTitle.edit', ['鐢熶骇宸ュ簭'])
+    : $t('ui.actionTitle.create', ['鐢熶骇宸ュ簭']);
+});
+
+const [Form, formApi] = useVbenForm({
+  commonConfig: {
+    componentProps: {
+      class: 'w-full',
+    },
+    formItemClass: 'col-span-3',
+    labelWidth: 100,
+  },
+  layout: 'horizontal',
+  schema: [],
+  showDefaultActions: false,
+  wrapperClass: 'grid-cols-3',
+});
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    if (isDetail.value) {
+      await modalApi.close();
+      return;
+    }
+    const { valid } = await formApi.validate();
+    if (!valid) {
+      return;
+    }
+    modalApi.lock();
+    // 鎻愪氦琛ㄥ崟
+    const data = (await formApi.getValues()) as MesProProcessApi.Process;
+    try {
+      await (formData.value?.id ? updateProcess(data) : createProcess(data));
+      // 鍏抽棴骞舵彁绀�
+      await modalApi.close();
+      emit('success');
+      message.success($t('ui.actionMessage.operationSuccess'));
+    } finally {
+      modalApi.unlock();
+    }
+  },
+  async onOpenChange(isOpen: boolean) {
+    if (!isOpen) {
+      formData.value = undefined;
+      return;
+    }
+    formApi.setState({ schema: useFormSchema(formApi) });
+    // 鍔犺浇鏁版嵁
+    const data = modalApi.getData<{ formType: FormType; id?: number }>();
+    formType.value = data.formType;
+    formApi.setDisabled(formType.value === 'detail');
+    modalApi.setState({ showConfirmButton: formType.value !== 'detail' });
+    if (!data?.id) {
+      return;
+    }
+    modalApi.lock();
+    try {
+      formData.value = await getProcess(data.id);
+      // 璁剧疆鍒� values
+      await formApi.setValues(formData.value);
+    } finally {
+      modalApi.unlock();
+    }
+  },
+});
+
+/** 鐢ㄤ簬宸ュ簭鍐呭瀛愯〃鐨勫伐搴忕紪鍙� */
+const processId = computed(() => formData.value?.id);
+</script>
+
+<template>
+  <Modal :title="getTitle" class="w-3/5">
+    <Form class="mx-4" />
+    <!-- 缂栬緫/璇︽儏妯″紡涓嬪睍绀哄伐搴忔搷浣滄楠ゅ瓙琛紝鏂板妯″紡涓嬮殣钘� -->
+    <template v-if="processId">
+      <Divider class="!my-3" orientation="left">鎿嶄綔姝ラ</Divider>
+      <div class="mx-4">
+        <ContentList :form-type="formType" :process-id="processId" />
+      </div>
+    </template>
+  </Modal>
+</template>
diff --git a/src/views/mes/process-design/route/components/color-picker.vue b/src/views/mes/process-design/route/components/color-picker.vue
new file mode 100644
index 0000000..f3edab9
--- /dev/null
+++ b/src/views/mes/process-design/route/components/color-picker.vue
@@ -0,0 +1,66 @@
+<script lang="ts" setup>
+import { computed } from 'vue';
+
+/** 鐢樼壒鍥鹃鑹查�夋嫨鍣紙ant-design-vue 4.x 鏆傛湭鎻愪緵 ColorPicker锛屽皝瑁呭師鐢� input[type=color]锛� */
+defineOptions({ name: 'RouteColorPicker' });
+
+const props = withDefaults(
+  defineProps<{
+    disabled?: boolean;
+    modelValue?: string;
+  }>(),
+  {
+    disabled: false,
+    modelValue: '',
+  },
+);
+
+const emit = defineEmits<{
+  change: [value: string];
+  'update:modelValue': [value: string];
+}>();
+
+/** 鐢ㄤ簬 input[type=color] 鐨勫睍绀哄�硷紝蹇呴』鏄悎娉� hex锛涢潪娉曟椂鍥為��鍒� #000000 浣嗕笉淇敼 modelValue */
+const swatchValue = computed(() =>
+  /^#[0-9a-f]{6}$/i.test(props.modelValue ?? '')
+    ? (props.modelValue as string)
+    : '#000000',
+);
+
+/** 棰滆壊鍙樺寲鏃跺悓姝� modelValue */
+function handleColorChange(event: Event) {
+  const value = (event.target as HTMLInputElement).value;
+  emit('update:modelValue', value);
+  emit('change', value);
+}
+</script>
+
+<template>
+  <div class="flex items-center gap-2">
+    <input
+      class="route-color-picker__swatch"
+      :disabled="disabled"
+      type="color"
+      :value="swatchValue"
+      @change="handleColorChange"
+      @input="handleColorChange"
+    />
+    <span v-if="modelValue">{{ modelValue }}</span>
+  </div>
+</template>
+
+<style scoped>
+.route-color-picker__swatch {
+  inline-size: 36px;
+  block-size: 28px;
+  padding: 2px;
+  cursor: pointer;
+  border: 1px solid var(--ant-color-border, #d9d9d9);
+  border-radius: 4px;
+}
+
+.route-color-picker__swatch:disabled {
+  cursor: not-allowed;
+  opacity: 0.6;
+}
+</style>
diff --git a/src/views/mes/process-design/route/components/index.ts b/src/views/mes/process-design/route/components/index.ts
new file mode 100644
index 0000000..4a78a4c
--- /dev/null
+++ b/src/views/mes/process-design/route/components/index.ts
@@ -0,0 +1 @@
+export { default as RouteColorPicker } from './color-picker.vue';
diff --git a/src/views/mes/process-design/route/data.ts b/src/views/mes/process-design/route/data.ts
new file mode 100644
index 0000000..a6c6810
--- /dev/null
+++ b/src/views/mes/process-design/route/data.ts
@@ -0,0 +1,517 @@
+import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesProRouteApi } from '#/api/mes/pro/route';
+import type { MesProRouteProcessApi } from '#/api/mes/pro/route/process';
+import type { MesProRouteProductApi } from '#/api/mes/pro/route/product';
+import type { MesProRouteProductBomApi } from '#/api/mes/pro/route/productbom';
+
+import { h, markRaw } from 'vue';
+
+import {
+  CommonStatusEnum,
+  DICT_TYPE,
+  MesAutoCodeRuleCode,
+} from '../../../../packages/constants/src';
+import { getDictOptions } from '../../../../packages/effects/hooks/src';
+
+import { Button } from 'ant-design-vue';
+
+import { z } from '#/adapter/form';
+import { generateAutoCode } from '#/api/mes/md/autocode/record';
+import {
+  MdItemSelect,
+  MdProductBomSelect,
+} from '#/views/mes/md/item/components';
+
+import { RouteColorPicker } from './components';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'detail' | 'update';
+
+/** 宸ヨ壓璺嚎琛ㄥ崟 */
+export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: { triggerFields: [''], show: () => false },
+    },
+    {
+      fieldName: 'code',
+      label: '璺嚎缂栫爜',
+      component: 'Input',
+      componentProps: {
+        maxLength: 64,
+        placeholder: '璇疯緭鍏ュ伐鑹鸿矾绾跨紪鐮�',
+      },
+      rules: z.string().min(1, '璺嚎缂栫爜涓嶈兘涓虹┖').max(64),
+      suffix: () =>
+        h(
+          Button,
+          {
+            type: 'default',
+            onClick: async () => {
+              const code = await generateAutoCode(
+                MesAutoCodeRuleCode.PRO_ROUTE_CODE,
+              );
+              await formApi?.setFieldValue('code', code);
+            },
+          },
+          { default: () => '鐢熸垚' },
+        ),
+    },
+    {
+      fieldName: 'name',
+      label: '璺嚎鍚嶇О',
+      component: 'Input',
+      componentProps: {
+        maxLength: 100,
+        placeholder: '璇疯緭鍏ュ伐鑹鸿矾绾垮悕绉�',
+      },
+      rules: z.string().min(1, '璺嚎鍚嶇О涓嶈兘涓虹┖').max(100),
+    },
+    {
+      fieldName: 'description',
+      label: '璺嚎璇存槑',
+      component: 'Textarea',
+      formItemClass: 'col-span-2',
+      componentProps: {
+        maxLength: 500,
+        placeholder: '璇疯緭鍏ュ伐鑹鸿矾绾胯鏄�',
+        rows: 3,
+      },
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      formItemClass: 'col-span-2',
+      componentProps: {
+        maxLength: 250,
+        placeholder: '璇疯緭鍏ュ娉�',
+        rows: 2,
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鎼滅储琛ㄥ崟 */
+export function useGridFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'code',
+      label: '璺嚎缂栫爜',
+      component: 'Input',
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇疯緭鍏ヨ矾绾跨紪鐮�',
+      },
+    },
+    {
+      fieldName: 'name',
+      label: '璺嚎鍚嶇О',
+      component: 'Input',
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇疯緭鍏ヨ矾绾垮悕绉�',
+      },
+    },
+    {
+      fieldName: 'status',
+      label: '鐘舵��',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+        placeholder: '璇烽�夋嫨鐘舵��',
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃瀛楁 */
+export function useGridColumns(
+  onStatusChange?: (
+    newStatus: number,
+    row: MesProRouteApi.Route,
+  ) => PromiseLike<boolean | undefined>,
+  statusEditable = true,
+): VxeTableGridOptions<MesProRouteApi.Route>['columns'] {
+  return [
+    {
+      field: 'code',
+      title: '璺嚎缂栫爜',
+      minWidth: 160,
+      slots: { default: 'code' },
+    },
+    { field: 'name', title: '璺嚎鍚嶇О', minWidth: 180 },
+    { field: 'description', title: '璺嚎璇存槑', minWidth: 200 },
+    {
+      field: 'status',
+      title: '鐘舵��',
+      width: 110,
+      align: 'center',
+      cellRender: {
+        attrs: { beforeChange: onStatusChange },
+        name: 'CellSwitch',
+        props: {
+          checkedValue: CommonStatusEnum.ENABLE,
+          disabled: !statusEditable,
+          unCheckedValue: CommonStatusEnum.DISABLE,
+        },
+      },
+    },
+    { field: 'remark', title: '澶囨敞', minWidth: 160 },
+    {
+      field: 'createTime',
+      title: '鍒涘缓鏃堕棿',
+      width: 180,
+      formatter: 'formatDateTime',
+    },
+    {
+      title: '鎿嶄綔',
+      width: 160,
+      fixed: 'right',
+      slots: { default: 'actions' },
+    },
+  ];
+}
+
+/** 宸ヨ壓璺嚎宸ュ簭鏄庣粏琛ㄥ崟 */
+export function useRouteProcessFormSchema(
+  processOptions: Array<{ label: string; value: number }>,
+): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: { triggerFields: [''], show: () => false },
+    },
+    {
+      fieldName: 'routeId',
+      component: 'Input',
+      dependencies: { triggerFields: [''], show: () => false },
+    },
+    {
+      fieldName: 'sort',
+      label: '搴忓彿',
+      component: 'InputNumber',
+      componentProps: {
+        class: '!w-full',
+        min: 1,
+        precision: 0,
+      },
+      rules: z.number().default(1),
+    },
+    {
+      fieldName: 'processId',
+      label: '宸ュ簭',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: processOptions,
+        placeholder: '璇烽�夋嫨宸ュ簭',
+        showSearch: true,
+      },
+      rules: 'selectRequired',
+    },
+    {
+      fieldName: 'linkType',
+      label: '涓庝笅閬撳伐搴忓叧绯�',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: getDictOptions(DICT_TYPE.MES_PRO_LINK_TYPE, 'number'),
+        placeholder: '璇烽�夋嫨',
+      },
+      rules: z.number().default(3),
+    },
+    {
+      fieldName: 'colorCode',
+      label: '鐢樼壒鍥鹃鑹�',
+      component: RouteColorPicker,
+    },
+    {
+      fieldName: 'keyFlag',
+      label: '鏄惁鍏抽敭宸ュ簭',
+      component: 'Switch',
+      componentProps: {
+        checkedChildren: '鏄�',
+        unCheckedChildren: '鍚�',
+      },
+      rules: z.boolean().default(false),
+    },
+    {
+      fieldName: 'checkFlag',
+      label: '鏄惁璐ㄦ纭',
+      component: 'Switch',
+      componentProps: {
+        checkedChildren: '鏄�',
+        unCheckedChildren: '鍚�',
+      },
+      rules: z.boolean().default(false),
+    },
+    {
+      fieldName: 'prepareTime',
+      label: '鍑嗗鏃堕棿(鍒�)',
+      component: 'InputNumber',
+      componentProps: {
+        class: '!w-full',
+        min: 0,
+        precision: 0,
+      },
+      rules: z.number().default(0),
+    },
+    {
+      fieldName: 'waitTime',
+      label: '绛夊緟鏃堕棿(鍒�)',
+      component: 'InputNumber',
+      componentProps: {
+        class: '!w-full',
+        min: 0,
+        precision: 0,
+      },
+      rules: z.number().default(0),
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      formItemClass: 'col-span-2',
+      componentProps: {
+        maxLength: 250,
+        placeholder: '璇疯緭鍏ュ娉�',
+        rows: 2,
+      },
+    },
+  ];
+}
+
+/** 宸ヨ壓璺嚎宸ュ簭鍒楄〃瀛楁 */
+export function useRouteProcessGridColumns(): VxeTableGridOptions<MesProRouteProcessApi.RouteProcess>['columns'] {
+  return [
+    { field: 'sort', title: '搴忓彿', width: 70, align: 'center', fixed: 'left' },
+    { field: 'processCode', title: '宸ュ簭缂栫爜', width: 140, fixed: 'left' },
+    { field: 'processName', title: '宸ュ簭鍚嶇О', width: 140, fixed: 'left' },
+    { field: 'nextProcessName', title: '涓嬩竴閬撳伐搴�', width: 140 },
+    {
+      field: 'linkType',
+      title: '涓庝笅涓�閬撳伐搴忓叧绯�',
+      width: 160,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.MES_PRO_LINK_TYPE },
+      },
+    },
+    {
+      field: 'keyFlag',
+      title: '鍏抽敭宸ュ簭',
+      width: 90,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
+      },
+    },
+    {
+      field: 'checkFlag',
+      title: '璐ㄦ纭',
+      width: 100,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
+      },
+    },
+    { field: 'prepareTime', title: '鍑嗗鏃堕棿(鍒�)', width: 110 },
+    { field: 'waitTime', title: '绛夊緟鏃堕棿(鍒�)', width: 110 },
+    {
+      field: 'colorCode',
+      title: '鐢樼壒鍥鹃鑹�',
+      width: 130,
+      slots: { default: 'colorCode' },
+    },
+    { field: 'remark', title: '澶囨敞', minWidth: 160 },
+    {
+      title: '鎿嶄綔',
+      width: 130,
+      fixed: 'right',
+      slots: { default: 'actions' },
+    },
+  ];
+}
+
+/** 宸ヨ壓璺嚎浜у搧鍒楄〃瀛楁 */
+export function useRouteProductGridColumns(): VxeTableGridOptions<MesProRouteProductApi.RouteProduct>['columns'] {
+  return [
+    { field: 'itemCode', title: '浜у搧鐗╂枡缂栫爜', width: 150 },
+    { field: 'itemName', title: '浜у搧鐗╂枡鍚嶇О', width: 150 },
+    { field: 'specification', title: '瑙勬牸鍨嬪彿', width: 150 },
+    { field: 'unitName', title: '鍗曚綅', width: 80 },
+    { field: 'quantity', title: '鐢熶骇鏁伴噺', width: 100 },
+    {
+      field: 'productionTime',
+      title: '鐢熶骇鐢ㄦ椂',
+      width: 130,
+      slots: { default: 'productionTime' },
+    },
+    { field: 'remark', title: '澶囨敞', minWidth: 160 },
+    {
+      title: '鎿嶄綔',
+      width: 130,
+      fixed: 'right',
+      slots: { default: 'actions' },
+    },
+  ];
+}
+
+/** 宸ヨ壓璺嚎浜у搧 BOM 鍒楄〃瀛楁 */
+export function useRouteProductBomGridColumns(): VxeTableGridOptions<MesProRouteProductBomApi.RouteProductBom>['columns'] {
+  return [
+    { field: 'itemCode', title: 'BOM 鐗╂枡缂栫爜', width: 150 },
+    { field: 'itemName', title: 'BOM 鐗╂枡鍚嶇О', width: 150 },
+    { field: 'specification', title: '瑙勬牸鍨嬪彿', width: 150 },
+    { field: 'unitName', title: '鍗曚綅', width: 80 },
+    { field: 'quantity', title: '鐢ㄦ枡姣斾緥', width: 100 },
+    { field: 'remark', title: '澶囨敞', minWidth: 160 },
+    {
+      title: '鎿嶄綔',
+      width: 130,
+      fixed: 'right',
+      slots: { default: 'actions' },
+    },
+  ];
+}
+
+/** 宸ヨ壓璺嚎浜у搧琛ㄥ崟 */
+export function useRouteProductFormSchema(
+  onItemChange?: (item: any) => void,
+): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: { triggerFields: [''], show: () => false },
+    },
+    {
+      fieldName: 'routeId',
+      component: 'Input',
+      dependencies: { triggerFields: [''], show: () => false },
+    },
+    // 浜у搧鐗╂枡锛氫娇鐢ㄤ笟鍔¤嚜瀹氫箟閫夋嫨鍣紝change 鍥炲~缂栫爜/鍚嶇О/瑙勬牸/鍗曚綅
+    {
+      fieldName: 'itemId',
+      label: '浜у搧',
+      component: markRaw(MdItemSelect),
+      componentProps: {
+        onChange: onItemChange,
+      },
+      formItemClass: 'col-span-2',
+      rules: 'selectRequired',
+    },
+    {
+      fieldName: 'quantity',
+      label: '鐢熶骇鏁伴噺',
+      component: 'InputNumber',
+      componentProps: {
+        class: '!w-full',
+        min: 1,
+        precision: 0,
+      },
+      rules: z.number().default(1),
+    },
+    {
+      fieldName: 'productionTime',
+      label: '鐢熶骇鐢ㄦ椂',
+      component: 'InputNumber',
+      componentProps: {
+        class: '!w-full',
+        min: 0,
+        precision: 2,
+      },
+      rules: z.number().default(1),
+    },
+    {
+      fieldName: 'timeUnitType',
+      label: '鏃堕棿鍗曚綅',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: getDictOptions(DICT_TYPE.MES_TIME_UNIT_TYPE),
+        placeholder: '璇烽�夋嫨',
+      },
+      rules: z.string().default('MINUTE'),
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      formItemClass: 'col-span-2',
+      componentProps: {
+        maxLength: 250,
+        placeholder: '璇疯緭鍏ュ娉�',
+        rows: 2,
+      },
+    },
+  ];
+}
+
+/** 宸ヨ壓璺嚎浜у搧 BOM 琛ㄥ崟 */
+export function useRouteProductBomFormSchema(
+  itemId: () => number,
+  onBomChange?: (bom: any) => void,
+): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: { triggerFields: [''], show: () => false },
+    },
+    {
+      fieldName: 'routeId',
+      component: 'Input',
+      dependencies: { triggerFields: [''], show: () => false },
+    },
+    {
+      fieldName: 'processId',
+      component: 'Input',
+      dependencies: { triggerFields: [''], show: () => false },
+    },
+    {
+      fieldName: 'productId',
+      component: 'Input',
+      dependencies: { triggerFields: [''], show: () => false },
+    },
+    // BOM 鐗╂枡锛氫緷璧栦骇鍝佺墿鏂欙紝浣跨敤涓氬姟鑷畾涔夐�夋嫨鍣�
+    {
+      fieldName: 'itemId',
+      label: 'BOM 鐗╂枡',
+      component: markRaw(MdProductBomSelect),
+      componentProps: () => ({
+        itemId: itemId(),
+        onChange: onBomChange,
+        placeholder: '璇烽�夋嫨 BOM 鐗╂枡',
+      }),
+      rules: 'selectRequired',
+    },
+    {
+      fieldName: 'quantity',
+      label: '鐢ㄦ枡姣斾緥',
+      component: 'InputNumber',
+      componentProps: {
+        class: '!w-full',
+        min: 0,
+        precision: 2,
+      },
+      rules: z.number().default(1),
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      componentProps: {
+        maxLength: 250,
+        placeholder: '璇疯緭鍏ュ娉�',
+        rows: 2,
+      },
+    },
+  ];
+}
diff --git a/src/views/mes/process-design/route/index.vue b/src/views/mes/process-design/route/index.vue
new file mode 100644
index 0000000..2cb7372
--- /dev/null
+++ b/src/views/mes/process-design/route/index.vue
@@ -0,0 +1,180 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesProRouteApi } from '#/api/mes/pro/route';
+
+import { useAccess } from '../../../../packages/effects/access/src';
+import { confirm, Page, useVbenModal } from '../../../../packages/effects/common-ui/src';
+import { CommonStatusEnum, DICT_TYPE } from '../../../../packages/constants/src';
+import { getDictLabel } from '../../../../packages/effects/hooks/src';
+import { downloadFileFromBlobPart } from '../../../../packages/utils/src';
+
+import { Button, message, Tooltip } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+  deleteRoute,
+  exportRoute,
+  getRoutePage,
+  updateRouteStatus,
+} from '#/api/mes/pro/route';
+import { $t } from '#/locales';
+
+import { useGridColumns, useGridFormSchema } from './data';
+import Form from './modules/form.vue';
+
+const { hasAccessByCodes } = useAccess();
+const statusEditable = hasAccessByCodes(['mes:pro-route:update']); // 鏄惁鍙垏鎹㈢姸鎬�
+
+const [FormModal, formModalApi] = useVbenModal({
+  connectedComponent: Form,
+  destroyOnClose: true,
+});
+
+/** 鍒锋柊琛ㄦ牸 */
+function handleRefresh() {
+  gridApi.query();
+}
+
+/** 鍒涘缓宸ヨ壓璺嚎 */
+function handleCreate() {
+  formModalApi.setData({ formType: 'create' }).open();
+}
+
+/** 缂栬緫宸ヨ壓璺嚎锛堜粎鍋滅敤鐘舵�佸彲缂栬緫锛� */
+function handleEdit(row: MesProRouteApi.Route) {
+  formModalApi.setData({ id: row.id, formType: 'update' }).open();
+}
+
+/** 璇︽儏鏌ョ湅 */
+function handleDetail(row: MesProRouteApi.Route) {
+  formModalApi.setData({ id: row.id, formType: 'detail' }).open();
+}
+
+/** 鍒囨崲鐘舵�� */
+async function handleStatusChange(
+  newStatus: number,
+  row: MesProRouteApi.Route,
+): Promise<boolean | undefined> {
+  try {
+    await confirm(
+      `纭瑕佸皢"${row.name}"宸ヨ壓璺嚎鍒囨崲涓恒��${getDictLabel(DICT_TYPE.COMMON_STATUS, newStatus)}銆戝悧锛焋,
+    );
+  } catch {
+    return false;
+  }
+  await updateRouteStatus(row.id!, newStatus);
+  message.success($t('ui.actionMessage.operationSuccess'));
+  return true;
+}
+
+/** 鍒犻櫎锛堜粎鍋滅敤鐘舵�佸彲鍒犻櫎锛� */
+async function handleDelete(row: MesProRouteApi.Route) {
+  const hideLoading = message.loading({
+    content: $t('ui.actionMessage.deleting', [row.name]),
+    duration: 0,
+  });
+  try {
+    await deleteRoute(row.id!);
+    message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
+    handleRefresh();
+  } finally {
+    hideLoading();
+  }
+}
+
+/** 瀵煎嚭 */
+async function handleExport() {
+  const data = await exportRoute(await gridApi.formApi.getValues());
+  downloadFileFromBlobPart({ fileName: '宸ヨ壓璺嚎.xls', source: data });
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  formOptions: {
+    schema: useGridFormSchema(),
+  },
+  gridOptions: {
+    columns: useGridColumns(handleStatusChange, statusEditable),
+    height: 'auto',
+    keepSource: true,
+    proxyConfig: {
+      ajax: {
+        query: async ({ page }, formValues) => {
+          return await getRoutePage({
+            pageNo: page.currentPage,
+            pageSize: page.pageSize,
+            ...formValues,
+          });
+        },
+      },
+    },
+    rowConfig: {
+      isHover: true,
+      keyField: 'id',
+    },
+    toolbarConfig: { refresh: true, search: true },
+  } as VxeTableGridOptions<MesProRouteApi.Route>,
+});
+</script>
+
+<template>
+  <Page auto-content-height><FormModal @success="handleRefresh" />
+    <Grid table-title="宸ヨ壓璺嚎鍒楄〃">
+      <template #toolbar-tools>
+        <TableAction
+          :actions="[
+            {
+              label: $t('ui.actionTitle.create', ['宸ヨ壓璺嚎']),
+              type: 'primary',
+              icon: ACTION_ICON.ADD,
+              auth: ['mes:pro-route:create'],
+              onClick: handleCreate,
+            },
+            {
+              label: $t('ui.actionTitle.export'),
+              type: 'primary',
+              icon: ACTION_ICON.DOWNLOAD,
+              auth: ['mes:pro-route:export'],
+              onClick: handleExport,
+            },
+          ]"
+        />
+      </template>
+      <template #code="{ row }">
+        <Button type="link" @click="handleDetail(row)">{{ row.code }}</Button>
+      </template>
+      <template #actions="{ row }">
+        <Tooltip
+          :open="row.status === CommonStatusEnum.DISABLE ? false : undefined"
+          title="浠呭仠鐢ㄧ姸鎬侊紝鎵嶅彲浠ユ搷浣�"
+        >
+          <span class="inline-block">
+            <TableAction
+              :actions="[
+                {
+                  label: $t('common.edit'),
+                  type: 'link',
+                  icon: ACTION_ICON.EDIT,
+                  auth: ['mes:pro-route:update'],
+                  disabled: row.status !== CommonStatusEnum.DISABLE,
+                  onClick: handleEdit.bind(null, row),
+                },
+                {
+                  label: $t('common.delete'),
+                  type: 'link',
+                  danger: true,
+                  icon: ACTION_ICON.DELETE,
+                  auth: ['mes:pro-route:delete'],
+                  disabled: row.status !== CommonStatusEnum.DISABLE,
+                  popConfirm: {
+                    title: $t('ui.actionMessage.deleteConfirm', [row.name]),
+                    confirm: handleDelete.bind(null, row),
+                  },
+                },
+              ]"
+            />
+          </span>
+        </Tooltip>
+      </template>
+    </Grid>
+  </Page>
+</template>
diff --git a/src/views/mes/process-design/route/modules/bom-form.vue b/src/views/mes/process-design/route/modules/bom-form.vue
new file mode 100644
index 0000000..5c9b0e2
--- /dev/null
+++ b/src/views/mes/process-design/route/modules/bom-form.vue
@@ -0,0 +1,118 @@
+<script lang="ts" setup>
+import type { MesProRouteProductBomApi } from '#/api/mes/pro/route/productbom';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+  createRouteProductBom,
+  getRouteProductBom,
+  updateRouteProductBom,
+} from '#/api/mes/pro/route/productbom';
+import { $t } from '#/locales';
+
+import { useRouteProductBomFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+const formData = ref<MesProRouteProductBomApi.RouteProductBom>();
+const productId = ref<number>(0); // 褰撳墠浜у搧鐗╂枡缂栧彿锛屼緵 BOM 閫夋嫨鍣ㄧ瓫閫夊彲鐢� BOM
+const getTitle = computed(() =>
+  formData.value?.id
+    ? $t('ui.actionTitle.edit', ['BOM 鐗╂枡'])
+    : $t('ui.actionTitle.create', ['BOM 鐗╂枡']),
+);
+
+/** MdProductBomSelect change 鍥炶皟锛氭妸 BOM 鐗╂枡缂栫爜/鍚嶇О/瑙勬牸/鍗曚綅/榛樿鐢ㄩ噺鍥炲~鍒� form */
+async function handleBomChange(bom?: any) {
+  if (!bom) {
+    return;
+  }
+  await formApi.setValues({
+    itemCode: bom.bomItemCode,
+    itemName: bom.bomItemName,
+    quantity: bom.quantity ?? 1,
+    specification: bom.specification,
+    unitName: bom.unitName,
+  });
+}
+
+const [Form, formApi] = useVbenForm({
+  commonConfig: {
+    componentProps: { class: 'w-full' },
+    formItemClass: 'col-span-1',
+    labelWidth: 110,
+  },
+  layout: 'horizontal',
+  schema: useRouteProductBomFormSchema(() => productId.value, handleBomChange),
+  showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    const { valid } = await formApi.validate();
+    if (!valid) {
+      return;
+    }
+    modalApi.lock();
+    // 鎻愪氦琛ㄥ崟
+    const data =
+      (await formApi.getValues()) as MesProRouteProductBomApi.RouteProductBom;
+    try {
+      await (formData.value?.id
+        ? updateRouteProductBom(data)
+        : createRouteProductBom(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<{
+      id?: number;
+      processId: number;
+      productId: number;
+      routeId: number;
+    }>();
+    if (!data) {
+      return;
+    }
+    productId.value = data.productId;
+    if (!data.id) {
+      // 鏂板鏃讹紝缁� routeId/processId/productId 鍏滃簳榛樿鍊�
+      await formApi.setValues({
+        processId: data.processId,
+        productId: data.productId,
+        quantity: 1,
+        routeId: data.routeId,
+      });
+      return;
+    }
+    modalApi.lock();
+    try {
+      formData.value = await getRouteProductBom(data.id);
+      // 璁剧疆鍒� values
+      await formApi.setValues(formData.value);
+    } finally {
+      modalApi.unlock();
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal :title="getTitle" class="w-1/3">
+    <Form class="mx-4" />
+  </Modal>
+</template>
diff --git a/src/views/mes/process-design/route/modules/form.vue b/src/views/mes/process-design/route/modules/form.vue
new file mode 100644
index 0000000..5e7f807
--- /dev/null
+++ b/src/views/mes/process-design/route/modules/form.vue
@@ -0,0 +1,121 @@
+<script lang="ts" setup>
+import type { FormType } from '../data';
+
+import type { MesProRouteApi } from '#/api/mes/pro/route';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
+
+import { message, TabPane, Tabs } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { createRoute, getRoute, updateRoute } from '#/api/mes/pro/route';
+import { $t } from '#/locales';
+
+import { useFormSchema } from '../data';
+import ProcessList from './process-list.vue';
+import ProductList from './product-list.vue';
+
+const emit = defineEmits(['success']);
+const formType = ref<FormType>('create'); // 琛ㄥ崟妯″紡
+const subTab = ref('process'); // 褰撳墠婵�娲荤殑瀛愯〃 Tab
+const formData = ref<MesProRouteApi.Route>();
+
+const isDetail = computed(() => formType.value === 'detail'); // 鏄惁鏌ョ湅妯″紡
+const getTitle = computed(() => {
+  if (formType.value === 'detail') {
+    return $t('ui.actionTitle.detail', ['宸ヨ壓璺嚎']);
+  }
+  return formType.value === 'update'
+    ? $t('ui.actionTitle.edit', ['宸ヨ壓璺嚎'])
+    : $t('ui.actionTitle.create', ['宸ヨ壓璺嚎']);
+});
+
+const [Form, formApi] = useVbenForm({
+  commonConfig: {
+    componentProps: { class: 'w-full' },
+    formItemClass: 'col-span-1',
+    labelWidth: 100,
+  },
+  layout: 'horizontal',
+  schema: [],
+  showDefaultActions: false,
+  wrapperClass: 'grid-cols-2',
+});
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    if (isDetail.value) {
+      await modalApi.close();
+      return;
+    }
+    const { valid } = await formApi.validate();
+    if (!valid) {
+      return;
+    }
+    modalApi.lock();
+    // 鎻愪氦琛ㄥ崟
+    const data = (await formApi.getValues()) as MesProRouteApi.Route;
+    try {
+      if (formType.value === 'create') {
+        // 鏂板鎴愬姛鍚庡垏鍒扮紪杈戞ā寮忥紝鏂逛究缁х画缁存姢缁勬垚宸ュ簭銆佸叧鑱斾骇鍝�
+        const id = await createRoute(data);
+        formData.value = { ...data, id };
+        await formApi.setFieldValue('id', id);
+        formType.value = 'update';
+      } else {
+        await updateRoute(data);
+        formData.value = { ...formData.value, ...data };
+      }
+      emit('success');
+      message.success($t('ui.actionMessage.operationSuccess'));
+    } finally {
+      modalApi.unlock();
+    }
+  },
+  async onOpenChange(isOpen: boolean) {
+    if (!isOpen) {
+      formData.value = undefined;
+      return;
+    }
+    formApi.setState({ schema: useFormSchema(formApi) });
+    subTab.value = 'process';
+    // 鍔犺浇鏁版嵁
+    const data = modalApi.getData<{ formType: FormType; id?: number }>();
+    formType.value = data.formType;
+    formApi.setDisabled(formType.value === 'detail');
+    modalApi.setState({ showConfirmButton: formType.value !== 'detail' });
+    if (!data?.id) {
+      return;
+    }
+    modalApi.lock();
+    try {
+      formData.value = await getRoute(data.id);
+      // 璁剧疆鍒� values
+      await formApi.setValues(formData.value);
+    } finally {
+      modalApi.unlock();
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal :title="getTitle" class="w-4/5">
+    <Form class="mx-4" />
+    <!-- 缂栬緫/璇︽儏妯″紡涓嬪睍绀哄瓙琛� Tab锛屾柊澧炴ā寮忎笅闅愯棌 -->
+    <Tabs
+      v-if="formType !== 'create' && formData?.id"
+      v-model:active-key="subTab"
+      class="mx-4 mt-4"
+    >
+      <TabPane key="process" tab="缁勬垚宸ュ簭">
+        <ProcessList :form-type="formType" :route-id="formData.id" />
+      </TabPane>
+      <TabPane key="product" tab="鍏宠仈浜у搧">
+        <ProductList :form-type="formType" :route-id="formData.id" />
+      </TabPane>
+    </Tabs>
+  </Modal>
+</template>
diff --git a/src/views/mes/process-design/route/modules/process-form.vue b/src/views/mes/process-design/route/modules/process-form.vue
new file mode 100644
index 0000000..5ef7593
--- /dev/null
+++ b/src/views/mes/process-design/route/modules/process-form.vue
@@ -0,0 +1,121 @@
+<script lang="ts" setup>
+import type { VbenFormSchema } from '#/adapter/form';
+import type { MesProRouteProcessApi } from '#/api/mes/pro/route/process';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { getProcessSimpleList } from '#/api/mes/pro/process';
+import {
+  createRouteProcess,
+  getRouteProcess,
+  updateRouteProcess,
+} from '#/api/mes/pro/route/process';
+import { $t } from '#/locales';
+
+import { useRouteProcessFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+const formData = ref<MesProRouteProcessApi.RouteProcess>();
+const getTitle = computed(() => {
+  return formData.value?.id
+    ? $t('ui.actionTitle.edit', ['宸ヨ壓璺嚎宸ュ簭'])
+    : $t('ui.actionTitle.create', ['宸ヨ壓璺嚎宸ュ簭']);
+});
+
+const [Form, formApi] = useVbenForm({
+  commonConfig: {
+    componentProps: { class: 'w-full' },
+    formItemClass: 'col-span-1',
+    labelWidth: 130,
+  },
+  layout: 'horizontal',
+  schema: [],
+  showDefaultActions: false,
+  wrapperClass: 'grid-cols-2',
+});
+
+/** 鍔犺浇宸ュ簭閫夐」鍚庡啀鐢熸垚 schema锛堥伩鍏嶄笅鎷夌┖閫夐」锛� */
+async function loadSchema(): Promise<VbenFormSchema[]> {
+  const list = await getProcessSimpleList();
+  const options = (list || []).map((item) => ({
+    label: item.name ?? '',
+    value: item.id!,
+  }));
+  return useRouteProcessFormSchema(options);
+}
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    const { valid } = await formApi.validate();
+    if (!valid) {
+      return;
+    }
+    modalApi.lock();
+    // 鎻愪氦琛ㄥ崟
+    const data =
+      (await formApi.getValues()) as MesProRouteProcessApi.RouteProcess;
+    try {
+      await (formData.value?.id
+        ? updateRouteProcess(data)
+        : createRouteProcess(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;
+    }
+    modalApi.lock();
+    try {
+      // 宸ュ簭涓嬫媺渚濊禆杩滅▼鏁版嵁锛宻chema 鍦ㄥ脊绐楁墦寮�鏃跺啀鐢熸垚
+      const schema = await loadSchema();
+      formApi.setState({ schema });
+    } finally {
+      modalApi.unlock();
+    }
+    // 鍔犺浇鏁版嵁
+    const data = modalApi.getData<{
+      id?: number;
+      maxSort?: number;
+      routeId: number;
+    }>();
+    if (!data) {
+      return;
+    }
+    if (!data.id) {
+      // 鏂板鏃讹紝榛樿搴忓彿 = maxSort + 1锛屽苟缁欎竴涓粯璁ょ敇鐗瑰浘棰滆壊
+      await formApi.setValues({
+        colorCode: '#00AEF3',
+        routeId: data.routeId,
+        sort: (data.maxSort || 0) + 1,
+      });
+      return;
+    }
+    modalApi.lock();
+    try {
+      formData.value = await getRouteProcess(data.id);
+      // 璁剧疆鍒� values
+      await formApi.setValues(formData.value);
+    } finally {
+      modalApi.unlock();
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal :title="getTitle" class="w-3/5">
+    <Form class="mx-4" />
+  </Modal>
+</template>
diff --git a/src/views/mes/process-design/route/modules/process-list.vue b/src/views/mes/process-design/route/modules/process-list.vue
new file mode 100644
index 0000000..70a992d
--- /dev/null
+++ b/src/views/mes/process-design/route/modules/process-list.vue
@@ -0,0 +1,136 @@
+<script lang="ts" setup>
+import type { FormType } from '../data';
+
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesProRouteProcessApi } from '#/api/mes/pro/route/process';
+
+import { ref, watch } from 'vue';
+
+import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
+
+import { message } from 'ant-design-vue';
+
+import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+  deleteRouteProcess,
+  getRouteProcessListByRoute,
+} from '#/api/mes/pro/route/process';
+import { $t } from '#/locales';
+
+import { useRouteProcessGridColumns } from '../data';
+import ProcessForm from './process-form.vue';
+
+const props = defineProps<{
+  formType: FormType;
+  routeId: number;
+}>();
+
+const isEditable = ref(props.formType !== 'detail'); // 鏄惁鍙紪杈�
+const list = ref<MesProRouteProcessApi.RouteProcess[]>([]); // 宸ヨ壓璺嚎宸ュ簭鍒楄〃
+
+const [ProcessFormModal, processFormModalApi] = useVbenModal({
+  connectedComponent: ProcessForm,
+  destroyOnClose: true,
+});
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  gridOptions: {
+    autoResize: true,
+    border: true,
+    columns: useRouteProcessGridColumns(),
+    data: list.value,
+    minHeight: 240,
+    pagerConfig: { enabled: false },
+    rowConfig: { isHover: true, keyField: 'id' },
+    showOverflow: true,
+    toolbarConfig: { enabled: false },
+  } as VxeTableGridOptions<MesProRouteProcessApi.RouteProcess>,
+});
+
+/** 鍔犺浇宸ヨ壓璺嚎宸ュ簭鍒楄〃 */
+async function getList() {
+  gridApi.setLoading(true);
+  try {
+    list.value = await getRouteProcessListByRoute(props.routeId);
+    gridApi.setGridOptions({ data: list.value });
+  } finally {
+    gridApi.setLoading(false);
+  }
+}
+
+/** 鏂板宸ヨ壓璺嚎宸ュ簭 */
+function handleCreate() {
+  const maxSort = Math.max(0, ...list.value.map((item) => item.sort || 0));
+  processFormModalApi.setData({ maxSort, routeId: props.routeId }).open();
+}
+
+/** 缂栬緫宸ヨ壓璺嚎宸ュ簭 */
+function handleEdit(row: MesProRouteProcessApi.RouteProcess) {
+  processFormModalApi.setData({ id: row.id, routeId: props.routeId }).open();
+}
+
+/** 鍒犻櫎宸ヨ壓璺嚎宸ュ簭 */
+async function handleDelete(row: MesProRouteProcessApi.RouteProcess) {
+  await deleteRouteProcess(row.id!);
+  message.success($t('ui.actionMessage.deleteSuccess', ['宸ヨ壓璺嚎宸ュ簭']));
+  await getList();
+}
+
+watch(
+  () => props.routeId,
+  (value) => {
+    if (value) {
+      getList();
+    }
+  },
+  { immediate: true },
+);
+</script>
+
+<template>
+  <ProcessFormModal @success="getList" />
+  <div v-if="isEditable" class="mb-3 flex items-center justify-start">
+    <TableAction
+      :actions="[
+        {
+          label: $t('ui.actionTitle.create', ['宸ュ簭']),
+          type: 'primary',
+          onClick: handleCreate,
+        },
+      ]"
+    />
+  </div>
+  <Grid class="w-full" table-title="缁勬垚宸ュ簭">
+    <template #colorCode="{ row }">
+      <div v-if="row.colorCode" class="flex items-center justify-center gap-1">
+        <div
+          class="h-4 w-4 rounded"
+          :style="{ backgroundColor: row.colorCode }"
+        ></div>
+        <span>{{ row.colorCode }}</span>
+      </div>
+    </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', ['宸ヨ壓璺嚎宸ュ簭']),
+              confirm: handleDelete.bind(null, row),
+            },
+          },
+        ]"
+      />
+    </template>
+  </Grid>
+</template>
diff --git a/src/views/mes/process-design/route/modules/product-bom-list.vue b/src/views/mes/process-design/route/modules/product-bom-list.vue
new file mode 100644
index 0000000..d4d8995
--- /dev/null
+++ b/src/views/mes/process-design/route/modules/product-bom-list.vue
@@ -0,0 +1,171 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesProRouteProcessApi } from '#/api/mes/pro/route/process';
+import type { MesProRouteProductBomApi } from '#/api/mes/pro/route/productbom';
+
+import { ref, watch } from 'vue';
+
+import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
+
+import { message, TabPane, Tabs } from 'ant-design-vue';
+
+import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import { getRouteProcessListByRoute } from '#/api/mes/pro/route/process';
+import {
+  deleteRouteProductBom,
+  getRouteProductBomList,
+} from '#/api/mes/pro/route/productbom';
+import { $t } from '#/locales';
+
+import { useRouteProductBomGridColumns } from '../data';
+import BomForm from './bom-form.vue';
+
+const props = defineProps<{
+  productId: number;
+  productName?: string;
+  routeId: number;
+}>();
+
+const processList = ref<MesProRouteProcessApi.RouteProcess[]>([]); // 宸ュ簭鍒楄〃锛堢敤浜� Tab锛�
+const activeProcessId = ref<number>(); // 褰撳墠閫変腑鐨勫伐搴� Tab
+const list = ref<MesProRouteProductBomApi.RouteProductBom[]>([]); // 褰撳墠宸ュ簭涓嬬殑 BOM 鍒楄〃
+
+const [BomFormModal, bomFormModalApi] = useVbenModal({
+  connectedComponent: BomForm,
+  destroyOnClose: true,
+});
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  gridOptions: {
+    autoResize: true,
+    border: true,
+    columns: useRouteProductBomGridColumns(),
+    data: list.value,
+    minHeight: 200,
+    pagerConfig: { enabled: false },
+    rowConfig: { isHover: true, keyField: 'id' },
+    showOverflow: true,
+    toolbarConfig: { enabled: false },
+  } as VxeTableGridOptions<MesProRouteProductBomApi.RouteProductBom>,
+});
+
+/** 鍔犺浇璺嚎涓嬬殑宸ュ簭鍒楄〃锛岀敤浜庡伐搴� Tab */
+async function loadProcessList() {
+  processList.value = (await getRouteProcessListByRoute(props.routeId)) || [];
+  if (processList.value.length > 0) {
+    activeProcessId.value = processList.value[0]!.processId;
+    await getList();
+  } else {
+    activeProcessId.value = undefined;
+    list.value = [];
+    gridApi.setGridOptions({ data: list.value });
+  }
+}
+
+/** 鍔犺浇褰撳墠宸ュ簭涓嬬殑 BOM 鍒楄〃 */
+async function getList() {
+  if (!activeProcessId.value) {
+    return;
+  }
+  gridApi.setLoading(true);
+  try {
+    list.value = await getRouteProductBomList({
+      processId: activeProcessId.value,
+      productId: props.productId,
+      routeId: props.routeId,
+    });
+    gridApi.setGridOptions({ data: list.value });
+  } finally {
+    gridApi.setLoading(false);
+  }
+}
+
+/** 鏂板 BOM */
+function handleCreate() {
+  if (!activeProcessId.value) {
+    message.warning('璇峰厛閫夋嫨宸ュ簭');
+    return;
+  }
+  bomFormModalApi
+    .setData({
+      processId: activeProcessId.value,
+      productId: props.productId,
+      routeId: props.routeId,
+    })
+    .open();
+}
+
+/** 缂栬緫 BOM */
+function handleEdit(row: MesProRouteProductBomApi.RouteProductBom) {
+  bomFormModalApi
+    .setData({
+      id: row.id,
+      processId: activeProcessId.value!,
+      productId: props.productId,
+      routeId: props.routeId,
+    })
+    .open();
+}
+
+/** 鍒犻櫎 BOM */
+async function handleDelete(row: MesProRouteProductBomApi.RouteProductBom) {
+  await deleteRouteProductBom(row.id!);
+  message.success($t('ui.actionMessage.deleteSuccess', ['BOM 鐗╂枡']));
+  await getList();
+}
+
+watch(
+  () => [props.routeId, props.productId],
+  () => {
+    if (props.routeId && props.productId) {
+      loadProcessList();
+    }
+  },
+  { immediate: true },
+);
+</script>
+
+<template>
+  <BomFormModal @success="getList" />
+  <Tabs v-model:active-key="activeProcessId" @change="getList">
+    <TabPane
+      v-for="item in processList"
+      :key="item.processId"
+      :tab="item.processName"
+    />
+  </Tabs>
+  <div class="mb-3 flex items-center justify-start">
+    <TableAction
+      :actions="[
+        {
+          label: $t('ui.actionTitle.create', ['BOM 鐗╂枡']),
+          type: 'primary',
+          disabled: !activeProcessId,
+          onClick: handleCreate,
+        },
+      ]"
+    />
+  </div>
+  <Grid class="w-full" table-title="浜у搧 BOM">
+    <template #actions="{ row }">
+      <TableAction
+        :actions="[
+          {
+            label: $t('common.edit'),
+            type: 'link',
+            onClick: handleEdit.bind(null, row),
+          },
+          {
+            label: $t('common.delete'),
+            type: 'link',
+            danger: true,
+            popConfirm: {
+              title: $t('ui.actionMessage.deleteConfirm', ['BOM 鐗╂枡']),
+              confirm: handleDelete.bind(null, row),
+            },
+          },
+        ]"
+      />
+    </template>
+  </Grid>
+</template>
diff --git a/src/views/mes/process-design/route/modules/product-form.vue b/src/views/mes/process-design/route/modules/product-form.vue
new file mode 100644
index 0000000..2d0db55
--- /dev/null
+++ b/src/views/mes/process-design/route/modules/product-form.vue
@@ -0,0 +1,125 @@
+<script lang="ts" setup>
+import type { MesProRouteProductApi } from '#/api/mes/pro/route/product';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
+
+import { Divider, message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+  createRouteProduct,
+  getRouteProduct,
+  updateRouteProduct,
+} from '#/api/mes/pro/route/product';
+import { $t } from '#/locales';
+
+import { useRouteProductFormSchema } from '../data';
+import ProductBomList from './product-bom-list.vue';
+
+const emit = defineEmits(['success']);
+const formData = ref<MesProRouteProductApi.RouteProduct>();
+const getTitle = computed(() =>
+  formData.value?.id
+    ? $t('ui.actionTitle.edit', ['宸ヨ壓璺嚎浜у搧'])
+    : $t('ui.actionTitle.create', ['宸ヨ壓璺嚎浜у搧']),
+);
+
+/** MdItemSelect change 鍥炶皟锛氭妸鐗╂枡缂栫爜/鍚嶇О/瑙勬牸/鍗曚綅鍥炲~鍒� form */
+async function handleItemChange(item?: any) {
+  if (!item) {
+    return;
+  }
+  await formApi.setValues({
+    itemCode: item.code,
+    itemName: item.name,
+    specification: item.specification,
+    unitName: item.unitMeasureName,
+  });
+}
+
+const [Form, formApi] = useVbenForm({
+  commonConfig: {
+    componentProps: { class: 'w-full' },
+    formItemClass: 'col-span-1',
+    labelWidth: 100,
+  },
+  layout: 'horizontal',
+  schema: useRouteProductFormSchema(handleItemChange),
+  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 MesProRouteProductApi.RouteProduct;
+    try {
+      if (formData.value?.id) {
+        await updateRouteProduct(data);
+        // 鐢ㄦ渶鏂拌〃鍗曞�煎悓姝� formData锛岀‘淇濅骇鍝� BOM 瀛愯〃缁戝畾鐨� itemId 涓庤〃鍗曚竴鑷�
+        formData.value = { ...formData.value, ...data };
+      } else {
+        // 鏂板鎴愬姛鍚庡垏鎹㈠埌缂栬緫妯″紡锛屾柟渚跨户缁淮鎶や骇鍝� BOM
+        const id = await createRouteProduct(data);
+        formData.value = { ...data, id };
+        await formApi.setFieldValue('id', id);
+      }
+      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<{
+      id?: number;
+      routeId: number;
+    }>();
+    if (!data) {
+      return;
+    }
+    if (!data.id) {
+      await formApi.setValues({ routeId: data.routeId });
+      return;
+    }
+    modalApi.lock();
+    try {
+      formData.value = await getRouteProduct(data.id);
+      // 璁剧疆鍒� values
+      await formApi.setValues(formData.value);
+    } finally {
+      modalApi.unlock();
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal :title="getTitle" class="w-3/5">
+    <Form class="mx-4" />
+    <!-- 缂栬緫妯″紡涓嬪睍绀轰骇鍝� BOM 瀛愯〃锛屾柊澧炴ā寮忎笅闅愯棌 -->
+    <template v-if="formData?.id && formData?.itemId">
+      <Divider class="!my-3" orientation="left">浜у搧 BOM 閰嶇疆</Divider>
+      <div class="mx-4">
+        <ProductBomList
+          :route-id="formData.routeId!"
+          :product-id="formData.itemId"
+          :product-name="formData.itemName"
+        />
+      </div>
+    </template>
+  </Modal>
+</template>
diff --git a/src/views/mes/process-design/route/modules/product-list.vue b/src/views/mes/process-design/route/modules/product-list.vue
new file mode 100644
index 0000000..e21a6d5
--- /dev/null
+++ b/src/views/mes/process-design/route/modules/product-list.vue
@@ -0,0 +1,134 @@
+<script lang="ts" setup>
+import type { FormType } from '../data';
+
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesProRouteProductApi } from '#/api/mes/pro/route/product';
+
+import { ref, watch } from 'vue';
+
+import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
+import { DICT_TYPE } from '../../../../../packages/constants/src';
+import { getDictLabel } from '../../../../../packages/effects/hooks/src';
+
+import { message } from 'ant-design-vue';
+
+import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+  deleteRouteProduct,
+  getRouteProductListByRoute,
+} from '#/api/mes/pro/route/product';
+import { $t } from '#/locales';
+
+import { useRouteProductGridColumns } from '../data';
+import ProductForm from './product-form.vue';
+
+const props = defineProps<{
+  formType: FormType;
+  routeId: number;
+}>();
+
+const isEditable = ref(props.formType !== 'detail'); // 鏄惁鍙紪杈�
+const list = ref<MesProRouteProductApi.RouteProduct[]>([]); // 宸ヨ壓璺嚎浜у搧鍒楄〃
+
+const [ProductFormModal, productFormModalApi] = useVbenModal({
+  connectedComponent: ProductForm,
+  destroyOnClose: true,
+});
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  gridOptions: {
+    autoResize: true,
+    border: true,
+    columns: useRouteProductGridColumns(),
+    data: list.value,
+    minHeight: 240,
+    pagerConfig: { enabled: false },
+    rowConfig: { isHover: true, keyField: 'id' },
+    showOverflow: true,
+    toolbarConfig: { enabled: false },
+  } as VxeTableGridOptions<MesProRouteProductApi.RouteProduct>,
+});
+
+/** 鍔犺浇宸ヨ壓璺嚎浜у搧鍒楄〃 */
+async function getList() {
+  gridApi.setLoading(true);
+  try {
+    list.value = await getRouteProductListByRoute(props.routeId);
+    gridApi.setGridOptions({ data: list.value });
+  } finally {
+    gridApi.setLoading(false);
+  }
+}
+
+/** 鏂板宸ヨ壓璺嚎浜у搧 */
+function handleCreate() {
+  productFormModalApi.setData({ routeId: props.routeId }).open();
+}
+
+/** 缂栬緫宸ヨ壓璺嚎浜у搧 */
+function handleEdit(row: MesProRouteProductApi.RouteProduct) {
+  productFormModalApi.setData({ id: row.id, routeId: props.routeId }).open();
+}
+
+/** 鍒犻櫎宸ヨ壓璺嚎浜у搧 */
+async function handleDelete(row: MesProRouteProductApi.RouteProduct) {
+  await deleteRouteProduct(row.id!);
+  message.success($t('ui.actionMessage.deleteSuccess', ['宸ヨ壓璺嚎浜у搧']));
+  await getList();
+}
+
+watch(
+  () => props.routeId,
+  (value) => {
+    if (value) {
+      getList();
+    }
+  },
+  { immediate: true },
+);
+</script>
+
+<template>
+  <ProductFormModal @success="getList" />
+  <div v-if="isEditable" class="mb-3 flex items-center justify-start">
+    <TableAction
+      :actions="[
+        {
+          label: $t('ui.actionTitle.create', ['鍏宠仈浜у搧']),
+          type: 'primary',
+          onClick: handleCreate,
+        },
+      ]"
+    />
+  </div>
+  <Grid class="w-full" table-title="鍏宠仈浜у搧">
+    <template #productionTime="{ row }">
+      <span v-if="row.productionTime">
+        {{ row.productionTime }}
+        {{ getDictLabel(DICT_TYPE.MES_TIME_UNIT_TYPE, row.timeUnitType) }}
+      </span>
+    </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', ['宸ヨ壓璺嚎浜у搧']),
+              confirm: handleDelete.bind(null, row),
+            },
+          },
+        ]"
+      />
+    </template>
+  </Grid>
+</template>
diff --git a/src/views/mes/process-design/template/data.ts b/src/views/mes/process-design/template/data.ts
new file mode 100644
index 0000000..0e2f655
--- /dev/null
+++ b/src/views/mes/process-design/template/data.ts
@@ -0,0 +1,128 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesPdTemplateApi } from '#/api/mes/pro/processdesign/template';
+
+import { DICT_TYPE } from '../../../../packages/constants/src';
+import { getDictOptions } from '../../../../packages/effects/hooks/src';
+
+import { z } from '#/adapter/form';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'detail' | 'update';
+
+/** 鏂板/淇敼宸ヨ壓鍙傛暟妯℃澘鐨勮〃鍗� */
+export function useFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: {
+        triggerFields: [''],
+        show: () => false,
+      },
+    },
+    {
+      fieldName: 'name',
+      label: '妯℃澘鍚嶇О',
+      component: 'Input',
+      componentProps: {
+        maxLength: 100,
+        placeholder: '璇疯緭鍏ユā鏉垮悕绉�',
+      },
+      rules: z.string().min(1, '妯℃澘鍚嶇О涓嶈兘涓虹┖').max(100),
+    },
+    {
+      fieldName: 'type',
+      label: '妯℃澘绫诲瀷',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: getDictOptions(DICT_TYPE.MES_PD_TEMPLATE_TYPE, 'number'),
+        placeholder: '璇烽�夋嫨妯℃澘绫诲瀷',
+      },
+      rules: 'selectRequired',
+    },
+    {
+      fieldName: 'parameterIds',
+      label: '鍏宠仈鍙傛暟',
+      component: 'Input',
+      dependencies: {
+        triggerFields: [''],
+        show: () => false,
+      },
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      formItemClass: 'col-span-2',
+      componentProps: {
+        maxLength: 250,
+        placeholder: '璇疯緭鍏ュ娉�',
+        rows: 3,
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'name',
+      label: '妯℃澘鍚嶇О',
+      component: 'Input',
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇疯緭鍏ユā鏉垮悕绉�',
+      },
+    },
+    {
+      fieldName: 'type',
+      label: '妯℃澘绫诲瀷',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: getDictOptions(DICT_TYPE.MES_PD_TEMPLATE_TYPE, 'number'),
+        placeholder: '璇烽�夋嫨妯℃澘绫诲瀷',
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions<MesPdTemplateApi.Template>['columns'] {
+  return [
+    {
+      type: 'seq',
+      title: '搴忓彿',
+      width: 60,
+      align: 'center',
+    },
+    { field: 'name', title: '妯℃澘鍚嶇О', minWidth: 180 },
+    {
+      field: 'type',
+      title: '妯℃澘绫诲瀷',
+      width: 120,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.MES_PD_TEMPLATE_TYPE },
+      },
+    },
+    { field: 'createUser', title: '鍒涘缓浜�', width: 120 },
+    {
+      field: 'createTime',
+      title: '鍒涘缓鏃堕棿',
+      width: 180,
+      formatter: 'formatDateTime',
+    },
+    {
+      title: '鎿嶄綔',
+      width: 150,
+      fixed: 'right',
+      slots: {
+        default: 'actions',
+      },
+    },
+  ];
+}
diff --git a/src/views/mes/md/unitmeasure/index.vue b/src/views/mes/process-design/template/index.vue
similarity index 63%
copy from src/views/mes/md/unitmeasure/index.vue
copy to src/views/mes/process-design/template/index.vue
index f9f49d4..0c6faa2 100644
--- a/src/views/mes/md/unitmeasure/index.vue
+++ b/src/views/mes/process-design/template/index.vue
@@ -1,18 +1,16 @@
 <script lang="ts" setup>
 import type { VxeTableGridOptions } from '#/adapter/vxe-table';
-import type { MesMdUnitMeasureApi } from '#/api/mes/md/unitmeasure';
+import type { MesPdTemplateApi } from '#/api/mes/pro/processdesign/template';
 
 import { Page, useVbenModal } from '../../../../packages/effects/common-ui/src';
-import { downloadFileFromBlobPart } from '../../../../packages/utils/src';
 
-import { message } from 'ant-design-vue';
+import { Button, message } from 'ant-design-vue';
 
 import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
 import {
-  deleteUnitMeasure,
-  exportUnitMeasure,
-  getUnitMeasurePage,
-} from '#/api/mes/md/unitmeasure';
+  deleteTemplate,
+  getTemplatePage,
+} from '#/api/mes/pro/processdesign/template';
 import { $t } from '#/locales';
 
 import { useGridColumns, useGridFormSchema } from './data';
@@ -28,30 +26,29 @@
   gridApi.query();
 }
 
-/** 瀵煎嚭琛ㄦ牸 */
-async function handleExport() {
-  const data = await exportUnitMeasure(await gridApi.formApi.getValues());
-  downloadFileFromBlobPart({ fileName: '璁¢噺鍗曚綅.xls', source: data });
-}
-
-/** 鍒涘缓璁¢噺鍗曚綅 */
+/** 鍒涘缓宸ヨ壓鍙傛暟妯℃澘 */
 function handleCreate() {
-  formModalApi.setData(null).open();
+  formModalApi.setData({ formType: 'create' }).open();
 }
 
-/** 缂栬緫璁¢噺鍗曚綅 */
-function handleEdit(row: MesMdUnitMeasureApi.UnitMeasure) {
-  formModalApi.setData(row).open();
+/** 缂栬緫宸ヨ壓鍙傛暟妯℃澘 */
+function handleEdit(row: MesPdTemplateApi.Template) {
+  formModalApi.setData({ id: row.id, formType: 'update' }).open();
 }
 
-/** 鍒犻櫎璁¢噺鍗曚綅 */
-async function handleDelete(row: MesMdUnitMeasureApi.UnitMeasure) {
+/** 鏌ョ湅宸ヨ壓鍙傛暟妯℃澘璇︽儏 */
+function handleDetail(row: MesPdTemplateApi.Template) {
+  formModalApi.setData({ id: row.id, formType: 'detail' }).open();
+}
+
+/** 鍒犻櫎宸ヨ壓鍙傛暟妯℃澘 */
+async function handleDelete(row: MesPdTemplateApi.Template) {
   const hideLoading = message.loading({
     content: $t('ui.actionMessage.deleting', [row.name]),
     duration: 0,
   });
   try {
-    await deleteUnitMeasure(row.id!);
+    await deleteTemplate(row.id!);
     message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
     handleRefresh();
   } finally {
@@ -70,7 +67,7 @@
     proxyConfig: {
       ajax: {
         query: async ({ page }, formValues) => {
-          return await getUnitMeasurePage({
+          return await getTemplatePage({
             pageNo: page.currentPage,
             pageSize: page.pageSize,
             ...formValues,
@@ -79,39 +76,35 @@
       },
     },
     rowConfig: {
-      keyField: 'id',
       isHover: true,
+      keyField: 'id',
     },
     toolbarConfig: {
       refresh: true,
       search: true,
     },
-  } as VxeTableGridOptions<MesMdUnitMeasureApi.UnitMeasure>,
+  } as VxeTableGridOptions<MesPdTemplateApi.Template>,
 });
 </script>
 
 <template>
   <Page auto-content-height><FormModal @success="handleRefresh" />
-    <Grid table-title="璁¢噺鍗曚綅鍒楄〃">
+    <Grid table-title="宸ヨ壓鍙傛暟妯℃澘鍒楄〃">
       <template #toolbar-tools>
         <TableAction
           :actions="[
             {
-              label: $t('ui.actionTitle.create', ['璁¢噺鍗曚綅']),
+              label: $t('ui.actionTitle.create', ['妯℃澘']),
               type: 'primary',
               icon: ACTION_ICON.ADD,
-              auth: ['mes:md-unit-measure:create'],
+              auth: ['mes:pd:template:create'],
               onClick: handleCreate,
-            },
-            {
-              label: $t('ui.actionTitle.export'),
-              type: 'primary',
-              icon: ACTION_ICON.DOWNLOAD,
-              auth: ['mes:md-unit-measure:export'],
-              onClick: handleExport,
             },
           ]"
         />
+      </template>
+      <template #name="{ row }">
+        <Button type="link" @click="handleDetail(row)">{{ row.name }}</Button>
       </template>
       <template #actions="{ row }">
         <TableAction
@@ -120,7 +113,7 @@
               label: $t('common.edit'),
               type: 'link',
               icon: ACTION_ICON.EDIT,
-              auth: ['mes:md-unit-measure:update'],
+              auth: ['mes:pd:template:update'],
               onClick: handleEdit.bind(null, row),
             },
             {
@@ -128,7 +121,7 @@
               type: 'link',
               danger: true,
               icon: ACTION_ICON.DELETE,
-              auth: ['mes:md-unit-measure:delete'],
+              auth: ['mes:pd:template:delete'],
               popConfirm: {
                 title: $t('ui.actionMessage.deleteConfirm', [row.name]),
                 confirm: handleDelete.bind(null, row),
diff --git a/src/views/mes/process-design/template/modules/form.vue b/src/views/mes/process-design/template/modules/form.vue
new file mode 100644
index 0000000..8d824f3
--- /dev/null
+++ b/src/views/mes/process-design/template/modules/form.vue
@@ -0,0 +1,173 @@
+<script lang="ts" setup>
+import type { FormType } from '../data';
+
+import type { MesPdParameterApi } from '#/api/mes/pro/processdesign/parameter';
+import type { MesPdTemplateApi } from '#/api/mes/pro/processdesign/template';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
+
+import { Button, message, Tag } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+  createTemplate,
+  getTemplate,
+  updateTemplate,
+} from '#/api/mes/pro/processdesign/template';
+import { $t } from '#/locales';
+
+import { useFormSchema } from '../data';
+import ParameterSelectDialog from './parameter-select-dialog.vue';
+
+const emit = defineEmits(['success']);
+const formType = ref<FormType>('create');
+const formData = ref<MesPdTemplateApi.Template>();
+
+/** 宸查�夊弬鏁板垪琛紙鐢ㄤ簬椤甸潰灞曠ず锛� */
+const selectedParameters = ref<MesPdParameterApi.Parameter[]>([]);
+const parameterSelectDialogRef = ref<InstanceType<typeof ParameterSelectDialog>>();
+
+const isDetail = computed(() => formType.value === 'detail');
+const getTitle = computed(() => {
+  if (formType.value === 'detail') {
+    return $t('ui.actionTitle.detail', ['宸ヨ壓鍙傛暟妯℃澘']);
+  }
+  return formType.value === 'update'
+    ? $t('ui.actionTitle.edit', ['宸ヨ壓鍙傛暟妯℃澘'])
+    : $t('ui.actionTitle.create', ['宸ヨ壓鍙傛暟妯℃澘']);
+});
+
+const [Form, formApi] = useVbenForm({
+  commonConfig: {
+    componentProps: {
+      class: 'w-full',
+    },
+    formItemClass: 'col-span-1',
+    labelWidth: 100,
+  },
+  layout: 'horizontal',
+  schema: [],
+  showDefaultActions: false,
+  wrapperClass: 'grid-cols-2',
+});
+
+/** 鎵撳紑宸ヨ壓鍙傛暟閫夋嫨寮圭獥 */
+function handleSelectParameters() {
+  parameterSelectDialogRef.value?.open(
+    selectedParameters.value
+      .map((p) => p.id)
+      .filter((id): id is number => id !== undefined),
+  );
+}
+
+/** 鎺ユ敹閫変腑鐨勫弬鏁� */
+function onParametersSelected(rows: MesPdParameterApi.Parameter[]) {
+  selectedParameters.value = rows;
+}
+
+/** 绉婚櫎宸查�夊弬鏁� */
+function removeParameter(param: MesPdParameterApi.Parameter) {
+  selectedParameters.value = selectedParameters.value.filter(
+    (p) => p.id !== param.id,
+  );
+}
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    if (isDetail.value) {
+      await modalApi.close();
+      return;
+    }
+    const { valid } = await formApi.validate();
+    if (!valid) {
+      return;
+    }
+    if (selectedParameters.value.length === 0) {
+      message.warning('璇疯嚦灏戦�夋嫨涓�涓伐鑹哄弬鏁�');
+      return;
+    }
+    modalApi.lock();
+    const data = (await formApi.getValues()) as MesPdTemplateApi.Template;
+    data.parameterIds = selectedParameters.value
+      .map((p) => p.id)
+      .filter((id): id is number => id !== undefined);
+    try {
+      await (formData.value?.id ? updateTemplate(data) : createTemplate(data));
+      await modalApi.close();
+      emit('success');
+      message.success($t('ui.actionMessage.operationSuccess'));
+    } finally {
+      modalApi.unlock();
+    }
+  },
+  async onOpenChange(isOpen: boolean) {
+    if (!isOpen) {
+      formData.value = undefined;
+      selectedParameters.value = [];
+      return;
+    }
+    formApi.setState({ schema: useFormSchema() });
+    const data = modalApi.getData<{ formType: FormType; id?: number }>();
+    formType.value = data.formType;
+    formApi.setDisabled(formType.value === 'detail');
+    modalApi.setState({ showConfirmButton: formType.value !== 'detail' });
+    if (!data?.id) {
+      return;
+    }
+    modalApi.lock();
+    try {
+      formData.value = await getTemplate(data.id);
+      await formApi.setValues(formData.value);
+      // 鍥炴樉宸查�夊弬鏁帮紙浠庤鎯呮帴鍙h繑鍥炵殑鍙傛暟鍒楄〃涓幏鍙栵級
+      if (formData.value.parameterIds) {
+        // parameterIds 宸茬敱鍚庣杩斿洖锛宻electedParameters 鍦ㄨ鎯呮ā寮忎笅灞曠ず
+      }
+    } finally {
+      modalApi.unlock();
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal :title="getTitle" class="w-4/5">
+    <Form class="mx-4" />
+    <!-- 鍏宠仈鍙傛暟閫夋嫨鍖哄煙 -->
+    <div class="mx-4 mt-4">
+      <div class="mb-2 flex items-center justify-between">
+        <span class="text-sm font-medium">鍏宠仈宸ヨ壓鍙傛暟</span>
+        <Button
+          v-if="!isDetail"
+          type="primary"
+          size="small"
+          @click="handleSelectParameters"
+        >
+          閫夋嫨鍙傛暟
+        </Button>
+      </div>
+      <div
+        v-if="selectedParameters.length > 0"
+        class="min-h-10 rounded border border-gray-200 p-2"
+      >
+        <Tag
+          v-for="param in selectedParameters"
+          :key="param.id"
+          :closable="!isDetail"
+          class="mb-1 mr-1"
+          @close="removeParameter(param)"
+        >
+          {{ param.name }}({{ param.code }})
+        </Tag>
+      </div>
+      <div v-else class="rounded border border-dashed border-gray-300 p-4 text-center text-gray-400">
+        鏆傛湭閫夋嫨鍙傛暟
+      </div>
+    </div>
+    <ParameterSelectDialog
+      ref="parameterSelectDialogRef"
+      @selected="onParametersSelected"
+    />
+  </Modal>
+</template>
diff --git a/src/views/mes/process-design/template/modules/parameter-select-dialog.vue b/src/views/mes/process-design/template/modules/parameter-select-dialog.vue
new file mode 100644
index 0000000..b4f87c6
--- /dev/null
+++ b/src/views/mes/process-design/template/modules/parameter-select-dialog.vue
@@ -0,0 +1,207 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesPdParameterApi } from '#/api/mes/pro/processdesign/parameter';
+
+import { nextTick, ref } from 'vue';
+
+import { DICT_TYPE } from '../../../../../packages/constants/src';
+
+import { Button, message, Modal } from 'ant-design-vue';
+
+import { useVbenVxeGrid } from '#/adapter/vxe-table';
+import { getParameterPage } from '#/api/mes/pro/processdesign/parameter';
+
+defineOptions({ name: 'PdParameterSelectDialog' });
+
+const emit = defineEmits<{
+  selected: [rows: MesPdParameterApi.Parameter[]];
+}>();
+
+const open = ref(false);
+const selectedRows = ref<MesPdParameterApi.Parameter[]>([]);
+const preSelectedIds = ref<number[]>([]);
+
+/** 鑾峰彇澶氶�夎褰曪紝鍖呭惈 VXE reserve 璺ㄩ〉璁板綍 */
+function getMultipleSelectedRows() {
+  const selectedMap = new Map<number, MesPdParameterApi.Parameter>();
+  const records = [
+    ...(gridApi.grid.getCheckboxReserveRecords?.() ?? []),
+    ...(gridApi.grid.getCheckboxRecords?.() ?? []),
+  ] as MesPdParameterApi.Parameter[];
+  records.forEach((row) => {
+    if (row.id) {
+      selectedMap.set(row.id, row);
+    }
+  });
+  return [...selectedMap.values()];
+}
+
+/** 澶勭悊鍕鹃�夊彉鍖� */
+function handleCheckboxSelectChange() {
+  selectedRows.value = getMultipleSelectedRows();
+}
+
+/** 澶氶�夋ā寮忎笅鍒囨崲琛屽嬀閫� */
+async function toggleMultipleRow(row: MesPdParameterApi.Parameter) {
+  const selected = gridApi.grid.isCheckedByCheckboxRow(row);
+  await gridApi.grid.setCheckboxRow(row, !selected);
+  selectedRows.value = getMultipleSelectedRows();
+}
+
+/** 澶勭悊琛屽弻鍑� */
+async function handleCellDblclick({ row }: { row: MesPdParameterApi.Parameter }) {
+  await toggleMultipleRow(row);
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  formOptions: {
+    schema: [
+      {
+        fieldName: 'code',
+        label: '鍙傛暟缂栫爜',
+        component: 'Input',
+        componentProps: {
+          allowClear: true,
+          placeholder: '璇疯緭鍏ュ弬鏁扮紪鐮�',
+        },
+      },
+      {
+        fieldName: 'name',
+        label: '鍙傛暟鍚嶇О',
+        component: 'Input',
+        componentProps: {
+          allowClear: true,
+          placeholder: '璇疯緭鍏ュ弬鏁板悕绉�',
+        },
+      },
+    ],
+  },
+  gridOptions: {
+    columns: [
+      { type: 'checkbox', width: 50 },
+      { field: 'code', title: '鍙傛暟缂栫爜', minWidth: 150 },
+      { field: 'name', title: '鍙傛暟鍚嶇О', minWidth: 180 },
+      {
+        field: 'type',
+        title: '鍙傛暟绫诲瀷',
+        width: 120,
+        cellRender: {
+          name: 'CellDict',
+          props: { type: DICT_TYPE.MES_PD_PARAM_TYPE },
+        },
+      },
+      { field: 'unit', title: '鍗曚綅', width: 80 },
+      {
+        field: 'requiredFlag',
+        title: '鏄惁蹇呭~',
+        width: 90,
+        cellRender: {
+          name: 'CellDict',
+          props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
+        },
+      },
+    ],
+    height: 520,
+    keepSource: true,
+    checkboxConfig: {
+      highlight: true,
+      range: true,
+      reserve: true,
+    },
+    proxyConfig: {
+      ajax: {
+        query: async ({ page }, formValues) => {
+          return await getParameterPage({
+            pageNo: page.currentPage,
+            pageSize: page.pageSize,
+            ...formValues,
+          });
+        },
+      },
+    },
+    rowConfig: {
+      keyField: 'id',
+      isHover: true,
+    },
+    toolbarConfig: {
+      refresh: true,
+      search: true,
+    },
+  } as VxeTableGridOptions<MesPdParameterApi.Parameter>,
+  gridEvents: {
+    cellDblclick: handleCellDblclick,
+    checkboxAll: handleCheckboxSelectChange,
+    checkboxChange: handleCheckboxSelectChange,
+  },
+});
+
+/** 鍥炴樉棰勯�夊弬鏁� */
+async function applyPreSelection() {
+  if (preSelectedIds.value.length === 0) {
+    return;
+  }
+  const rows = gridApi.grid.getTableData().fullData as MesPdParameterApi.Parameter[];
+  for (const row of rows) {
+    if (row.id === undefined || row.id === null || !preSelectedIds.value.includes(row.id)) {
+      continue;
+    }
+    await gridApi.grid.setCheckboxRow(row, true);
+  }
+  selectedRows.value = getMultipleSelectedRows();
+}
+
+/** 閲嶇疆鏌ヨ鍜岄�夋嫨鐘舵�� */
+async function resetQueryState() {
+  selectedRows.value = [];
+  await gridApi.grid.clearCheckboxRow();
+  await gridApi.grid.clearCheckboxReserve();
+  await gridApi.formApi.resetForm();
+}
+
+/** 鎵撳紑鍙傛暟閫夋嫨寮圭獥 */
+async function openModal(selectedIds?: number[]) {
+  open.value = true;
+  preSelectedIds.value = selectedIds || [];
+  await nextTick();
+  await resetQueryState();
+  await gridApi.query();
+  await nextTick();
+  await applyPreSelection();
+}
+
+/** 鍏抽棴鍙傛暟閫夋嫨寮圭獥 */
+async function closeModal() {
+  open.value = false;
+  await resetQueryState();
+}
+
+/** 纭閫夋嫨鍙傛暟 */
+function handleConfirm() {
+  const rows = getMultipleSelectedRows();
+  if (rows.length === 0) {
+    message.warning('璇疯嚦灏戦�夋嫨涓�鏉℃暟鎹�');
+    return;
+  }
+  emit('selected', rows);
+  open.value = false;
+}
+
+defineExpose({ open: openModal });
+</script>
+
+<template>
+  <Modal
+    v-model:open="open"
+    title="宸ヨ壓鍙傛暟閫夋嫨"
+    width="70%"
+    :destroy-on-close="true"
+    @ok="handleConfirm"
+    @cancel="closeModal"
+  >
+    <Grid table-title="宸ヨ壓鍙傛暟鍒楄〃" />
+    <template #footer>
+      <Button @click="closeModal"> 鍙栨秷 </Button>
+      <Button type="primary" @click="handleConfirm"> 纭畾 </Button>
+    </template>
+  </Modal>
+</template>
diff --git a/vite.config.ts b/vite.config.ts
index 44d7717..4f673c6 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,4 +1,4 @@
-import { defineConfig } from './internal/vite-config/src';
+import { defineConfig } from "./internal/vite-config/src";
 
 export default defineConfig(async () => {
   return {
@@ -10,14 +10,14 @@
       server: {
         allowedHosts: true,
         proxy: {
-          '/admin-api': {
+          "/admin-api": {
             changeOrigin: true,
-            rewrite: (path) => path.replace(/^\/admin-api/, ''),
-            target: 'http://192.168.0.226:48080/admin-api',
+            rewrite: (path) => path.replace(/^\/admin-api/, ""),
+            target: "http://192.168.0.244:48080/admin-api",
             ws: true,
           },
         },
       },
     },
   };
-});
\ No newline at end of file
+});

--
Gitblit v1.9.3