From 889d8ddf4c13c1b1bdbabd04c2d80b904020151e Mon Sep 17 00:00:00 2001
From: hsy <1029150275@qq.com>
Date: 星期三, 19 八月 2026 12:49:24 +0800
Subject: [PATCH] 生产订单与报工模块改造汇总 (Git Commit 文档) feat(mes-pro): 生产订单及报工模块审批流与批次填报改造 1. 生产订单功能增强 (Work Order)

---
 src/router/routes/modules/mes.ts                               |   10 +
 src/views/mes/workbench/index.vue                              |   29 ++
 src/views/mes/pro/feedback/detail/index.vue                    |   67 +++++++
 src/views/mes/pro/task/data.ts                                 |    4 
 .idea/mom-pro2-before.iml                                      |    2 
 src/views/mes/pro/feedback/modules/form.vue                    |   63 ++++++
 src/views/mes/pro/workorder/data.ts                            |   53 +++++
 src/views/mes/workbench/types.ts                               |    1 
 src/api/system/user/index.ts                                   |    4 
 src/views/mes/workbench/components/tabs/FeedbackHistoryTab.vue |   32 +++
 src/views/mes/pro/feedback/index.vue                           |   30 ++
 src/api/mes/pro/workorder/index.ts                             |   10 +
 src/views/mes/pro/workorder/modules/form.vue                   |   21 +
 src/views/mes/workbench/components/tabs/FeedbackTab.vue        |   68 ++++++
 src/views/mes/pro/feedback/data.ts                             |   33 +++
 src/api/mes/pro/feedback/index.ts                              |   23 ++
 src/views/mes/pro/task/modules/task-form.vue                   |   48 +++-
 17 files changed, 442 insertions(+), 56 deletions(-)

diff --git a/.idea/mom-pro2-before.iml b/.idea/mom-pro2-before.iml
index 179693c..f93e5a6 100644
--- a/.idea/mom-pro2-before.iml
+++ b/.idea/mom-pro2-before.iml
@@ -7,6 +7,8 @@
       <excludeFolder url="file://$MODULE_DIR$/.cache" />
       <excludeFolder url="file://$MODULE_DIR$/dist" />
       <excludeFolder url="file://$MODULE_DIR$/build" />
+      <excludeFolder url="file://$MODULE_DIR$/.agents" />
+      <excludeFolder url="file://$MODULE_DIR$/.codex" />
     </content>
     <orderEntry type="inheritedJdk" />
     <orderEntry type="sourceFolder" forTests="false" />
diff --git a/src/api/mes/pro/feedback/index.ts b/src/api/mes/pro/feedback/index.ts
index 7666071..934eba8 100644
--- a/src/api/mes/pro/feedback/index.ts
+++ b/src/api/mes/pro/feedback/index.ts
@@ -1,8 +1,23 @@
-锘縤mport type { PageParam, PageResult } from '@vben/request';
+import type { PageParam, PageResult } from '@vben/request';
 
 import { requestClient } from '#/api/request';
 
 export namespace MesProFeedbackApi {
+  export interface FeedbackSubmitReqVO {
+    id: number;
+    produceDate?: string;
+    expireDate?: string;
+    receiptDate?: string;
+    vendorId?: number;
+    clientId?: number;
+    purchaseOrderCode?: string;
+    salesOrderCode?: string;
+    toolId?: number;
+    moldId?: number;
+    lotNumber?: string;
+    qualityStatus?: number;
+  }
+
   /** MES 鐢熶骇鎶ュ伐 */
   export interface Feedback {
     id?: number;
@@ -47,6 +62,8 @@
     remark?: string; // 澶囨敞
     creator?: string; // 鍒涘缓浜�
     createTime?: number; // 鍒涘缓鏃堕棿
+    processInstanceId?: string; // BPM 娴佺▼瀹炰緥缂栧彿
+    processDefinitionKey?: string; // BPM 娴佺▼瀹氫箟 Key
   }
 
   /** MES 鐢熶骇鎶ュ伐鍒嗛〉鏌ヨ鍙傛暟 */
@@ -100,8 +117,8 @@
 }
 
 /** 鎻愪氦鐢熶骇鎶ュ伐 */
-export function submitFeedback(id: number) {
-  return requestClient.put(`/mes/pro/feedback/submit?id=${id}`);
+export function submitFeedback(data: MesProFeedbackApi.FeedbackSubmitReqVO) {
+  return requestClient.put(`/mes/pro/feedback/submit`, data);
 }
 
 /** 椹冲洖鐢熶骇鎶ュ伐 */
diff --git a/src/api/mes/pro/workorder/index.ts b/src/api/mes/pro/workorder/index.ts
index 39bcf69..68b8037 100644
--- a/src/api/mes/pro/workorder/index.ts
+++ b/src/api/mes/pro/workorder/index.ts
@@ -40,6 +40,9 @@
     completionProgress?: number; // 瀹屾垚杩涘害
     remark?: string; // 澶囨敞
     createTime?: number; // 鍒涘缓鏃堕棿
+    feedbackApprove?: boolean; // 鏄惁鎶ュ伐瀹℃壒
+    processDefinitionKey?: string; // 鎶ュ伐瀹℃壒娴佺▼瀹氫箟 Key
+    feedbackRole?: string; // 鎶ュ伐瑙掕壊
   }
 
   /** MES 鐢熶骇璁㈠崟鍒嗛〉鏌ヨ鍙傛暟 */
@@ -159,3 +162,10 @@
     `/mes/pro/work-order/check-auto-finish/${id}`,
   );
 }
+
+/** 鑾峰緱鐢熶骇璁㈠崟瀹℃壒娴佺▼鍒楄〃 */
+export function getWorkOrderApproveProcessList() {
+  return requestClient.get<Array<{ id: string; name: string; key: string }>>(
+    '/mes/pro/work-order/approve-process-list',
+  );
+}
diff --git a/src/api/system/user/index.ts b/src/api/system/user/index.ts
index cc7ce13..208d4b9 100644
--- a/src/api/system/user/index.ts
+++ b/src/api/system/user/index.ts
@@ -116,8 +116,8 @@
 }
 
 /** 閫氳繃瑙掕壊鏍囪瘑鑾峰彇鐢ㄦ埛绮剧畝淇℃伅鍒楄〃 */
-export function getSimpleUserListByRoleCode(code: string) {
+export function getSimpleUserListByRoleCode(code: Object) {
   return requestClient.get<SystemUserApi.UserSimple[]>('/system/user/list-by-role-code', {
-    params: { code },
+    params:  code ,
   });
 }
diff --git a/src/router/routes/modules/mes.ts b/src/router/routes/modules/mes.ts
index de6c512..54d4328 100644
--- a/src/router/routes/modules/mes.ts
+++ b/src/router/routes/modules/mes.ts
@@ -29,6 +29,16 @@
         },
         component: () => import('#/views/mes/pd/project/detail/index.vue'),
       },
+      {
+        path: 'pro/feedback/detail/:id',
+        name: 'MesProFeedbackDetail',
+        meta: {
+          title: '鐢熶骇鎶ュ伐璇︽儏',
+          activePath: '/mes/pro/feedback',
+        },
+        props: true,
+        component: () => import('#/views/mes/pro/feedback/detail/index.vue'),
+      },
     ],
   },
 ];
diff --git a/src/views/mes/pro/feedback/data.ts b/src/views/mes/pro/feedback/data.ts
index 29b244a..8e2580a 100644
--- a/src/views/mes/pro/feedback/data.ts
+++ b/src/views/mes/pro/feedback/data.ts
@@ -2,6 +2,7 @@
 import type { VxeTableGridOptions } from '#/adapter/vxe-table';
 import type { MesProFeedbackApi } from '#/api/mes/pro/feedback';
 import type { MesProTaskApi } from '#/api/mes/pro/task';
+import type { DescriptionItemSchema } from '#/components/description';
 
 import { h, markRaw } from 'vue';
 
@@ -23,6 +24,7 @@
 import { MdWorkstationSelect } from '#/views/mes/md/workstation/components';
 import { ProTaskSelect } from '#/views/mes/pro/task/components';
 import { ProWorkOrderSelect } from '#/views/mes/pro/workorder/components';
+import { DictTag } from '#/components/dict-tag';
 
 /** 鐢熶骇鎶ュ伐琛ㄥ崟绫诲瀷 */
 export type FormType = 'approve' | 'create' | 'detail' | 'submit' | 'update';
@@ -601,3 +603,34 @@
     },
   ];
 }
+
+/** 鐢熶骇鎶ュ伐璇︽儏鎽樿 */
+export function useDetailSchema(): DescriptionItemSchema[] {
+  return [
+    { field: 'code', label: '鎶ヨ鍗曞彿' },
+    {
+      field: 'type',
+      label: '鎶ュ伐绫诲瀷',
+      render: (val) =>
+        h(DictTag, { type: DICT_TYPE.MES_PRO_FEEDBACK_TYPE, value: val }),
+    },
+    { field: 'workOrderId', label: '鐢熶骇璁㈠崟', render: (_val, data) => data.workOrderCode },
+    { field: 'taskId', label: '鐢熶骇浠诲姟', render: (_val, data) => data.taskCode },
+    { field: 'workstationName', label: '宸ヤ綔绔�' },
+    { field: 'itemCode', label: '浜у搧缂栫爜' },
+    { field: 'itemName', label: '浜у搧鍚嶇О' },
+    { field: 'unitMeasureName', label: '鍗曚綅' },
+    { field: 'itemSpecification', label: '瑙勬牸' },
+    { field: 'reportableQuantity', label: '鍙姤宸ユ暟閲�' },
+    { field: 'feedbackQuantity', label: '鎶ュ伐鏁伴噺' },
+    { field: 'qualifiedQuantity', label: '鍚堟牸鍝佹暟閲�' },
+    { field: 'unqualifiedQuantity', label: '涓嶈壇鍝佹暟閲�' },
+    { field: 'laborScrapQuantity', label: '宸ュ簾鏁伴噺' },
+    { field: 'materialScrapQuantity', label: '鏂欏簾鏁伴噺' },
+    { field: 'otherScrapQuantity', label: '鍏朵粬搴熷搧' },
+    { field: 'feedbackUserNickname', label: '鎶ュ伐浜�' },
+    { field: 'feedbackTime', label: '鎶ュ伐鏃堕棿' },
+    { field: 'approveUserNickname', label: '瀹℃牳浜�' },
+    { field: 'remark', label: '澶囨敞' },
+  ];
+}
diff --git a/src/views/mes/pro/feedback/detail/index.vue b/src/views/mes/pro/feedback/detail/index.vue
new file mode 100644
index 0000000..e3ecf9c
--- /dev/null
+++ b/src/views/mes/pro/feedback/detail/index.vue
@@ -0,0 +1,67 @@
+<script lang="ts" setup>
+import type { MesProFeedbackApi } from '#/api/mes/pro/feedback';
+
+import { onMounted, ref } from 'vue';
+import { useRouter } from 'vue-router';
+
+import { Page } from '@vben/common-ui';
+import { useTabs } from '@vben/hooks';
+
+import { Card, Tabs } from 'ant-design-vue';
+
+import { TableAction } from '#/adapter/vxe-table';
+import { getFeedback } from '#/api/mes/pro/feedback';
+import { useDescription } from '#/components/description';
+
+import { useDetailSchema } from '../data';
+import ItemConsumeList from '../modules/item-consume-list.vue';
+import ProductProduceList from '../modules/product-produce-list.vue';
+
+const props = defineProps<{ id: string }>();
+
+const router = useRouter();
+const tabs = useTabs();
+
+const loading = ref(false);
+const feedbackId = ref(0);
+const feedback = ref<MesProFeedbackApi.Feedback>({});
+const activeTab = ref('itemConsume');
+
+const [Descriptions] = useDescription({
+  bordered: false,
+  column: 4,
+  schema: useDetailSchema(),
+});
+
+async function loadFeedbackDetail() {
+  loading.value = true;
+  try {
+    feedback.value = await getFeedback(feedbackId.value);
+  } finally {
+    loading.value = false;
+  }
+}
+
+function handleBack() {
+  tabs.closeCurrentTab();
+  router.push({ path: '/mes/pro/feedback' });
+}
+
+onMounted(() => {
+  feedbackId.value = Number(props.id);
+  loadFeedbackDetail();
+});
+</script>
+
+<template>
+  <Page
+    auto-content-height
+    :loading="loading"
+    :title="feedback.code ? `鐢熶骇鎶ュ伐璇︽儏 - ${feedback.code}` : '鐢熶骇鎶ュ伐璇︽儏'"
+  >
+    <Card class="min-h-[10%]">
+      <Descriptions :data="feedback" />
+    </Card>
+
+  </Page>
+</template>
diff --git a/src/views/mes/pro/feedback/index.vue b/src/views/mes/pro/feedback/index.vue
index adb6ae1..c56983a 100644
--- a/src/views/mes/pro/feedback/index.vue
+++ b/src/views/mes/pro/feedback/index.vue
@@ -19,6 +19,7 @@
 import { $t } from '#/locales';
 
 import { ref } from 'vue';
+import { useRouter } from 'vue-router';
 
 import { useGridColumns, useGridFormSchema } from './data';
 import Form from './modules/form.vue';
@@ -26,6 +27,7 @@
 
 const userStore = useUserStore();
 const currentUserId = userStore.userInfo?.id; // 褰撳墠鐧诲綍鐢ㄦ埛 ID锛岀敤浜庡鎵逛汉鏉冮檺鍒ゆ柇
+const router = useRouter();
 
 const [FormModal, formModalApi] = useVbenModal({
   connectedComponent: Form,
@@ -57,6 +59,15 @@
 /** 瀹℃壒鐢熶骇鎶ュ伐 */
 function handleApprove(row: MesProFeedbackApi.Feedback) {
   formModalApi.setData({ formType: 'approve', id: row.id }).open();
+}
+
+/** 鏌ョ湅瀹℃壒璇︽儏 */
+function handleViewProcess(row: MesProFeedbackApi.Feedback) {
+  if (!row.processInstanceId) {
+    message.warning('璇ユ姤宸ュ皻鏈彁浜ゅ鎵�');
+    return;
+  }
+  router.push({ path: '/bpm/process-instance/detail', query: { id: row.processInstanceId } });
 }
 
 /** 璇︽儏鐢熶骇鎶ュ伐 */
@@ -165,14 +176,21 @@
               ifShow: () => row.status === MesProFeedbackStatusEnum.PREPARE,
               onClick: handleSubmit.bind(null, row),
             },
+            // {
+            //   label: $t('common.approve'),
+            //   type: 'link',
+            //   auth: ['mes:pro-feedback:approve'],
+            //   ifShow: () =>
+            //     row.status === MesProFeedbackStatusEnum.APPROVING &&
+            //     row.approveUserId === currentUserId,
+            //   onClick: handleApprove.bind(null, row),
+            // },
             {
-              label: $t('common.approve'),
+              label: '鏌ョ湅瀹℃壒',
               type: 'link',
-              auth: ['mes:pro-feedback:approve'],
-              ifShow: () =>
-                row.status === MesProFeedbackStatusEnum.APPROVING &&
-                row.approveUserId === currentUserId,
-              onClick: handleApprove.bind(null, row),
+              icon: ACTION_ICON.VIEW,
+              ifShow: () => row.status === MesProFeedbackStatusEnum.APPROVING,
+              onClick: handleViewProcess.bind(null, row),
             },
             {
               label: $t('common.delete'),
diff --git a/src/views/mes/pro/feedback/modules/form.vue b/src/views/mes/pro/feedback/modules/form.vue
index d0e4edf..92bc22f 100644
--- a/src/views/mes/pro/feedback/modules/form.vue
+++ b/src/views/mes/pro/feedback/modules/form.vue
@@ -1,4 +1,4 @@
-锘�<script lang="ts" setup>
+<script lang="ts" setup>
 import type { FormType } from '../data';
 
 import type { MesProFeedbackApi } from '#/api/mes/pro/feedback';
@@ -25,6 +25,7 @@
   updateFeedback,
 } from '#/api/mes/pro/feedback';
 import { getRouteProcessByRouteAndProcess } from '#/api/mes/pro/route/process';
+import { getWorkOrder } from '#/api/mes/pro/workorder';
 import { $t } from '#/locales';
 
 import { useFormSchema } from '../data';
@@ -142,6 +143,23 @@
   }
 }
 
+const batchDialogRef = ref<InstanceType<typeof BatchInputDialog>>();
+const batchActionType = ref<'submit' | 'approve'>('');
+
+/** 鎵ц鎻愪氦锛堝惈鎵规灞炴�э級 */
+async function doSubmit(batchFields?: Record<string, unknown>) {
+  if (!formData.value?.id) return;
+  modalApi.lock();
+  try {
+    await submitFeedback({ id: formData.value.id, ...batchFields });
+    await modalApi.close();
+    emit('success');
+    message.success('鎶ュ伐鍗曞凡鎻愪氦');
+  } finally {
+    modalApi.unlock();
+  }
+}
+
 /** 鎻愪氦锛氫粎褰撹〃鍗曡緝蹇収鏈夊彉鏇存椂鎵嶅厛淇濆瓨锛屽啀璋冪敤鎻愪氦鎺ュ彛 */
 async function handleSubmit() {
   const { valid } = await formApi.validate();
@@ -156,16 +174,40 @@
       await updateFeedback(data);
       originalSnapshot.value = JSON.stringify(data);
     }
-    await submitFeedback(formData.value!.id!);
-    await modalApi.close();
-    emit('success');
-    message.success('鎶ヨ鍗曞凡鎻愪氦');
+    
+    // 妫�鏌ユ槸鍚﹂渶瑕佸鎵癸紝鑻ラ渶瑕佸鎵癸紝鍒欎笉闇�瑕佸湪姝ら樁娈靛~鍐欐壒娆″睘鎬э紝鐩存帴鎻愪氦
+    let feedbackApprove = false;
+    if (formData.value?.workOrderId) {
+      try {
+        const workOrder = await getWorkOrder(formData.value.workOrderId);
+        feedbackApprove = workOrder?.feedbackApprove ?? false;
+      } catch {
+        // ignore
+      }
+    }
+    
+    if (feedbackApprove) {
+      await doSubmit();
+      return;
+    }
+
+    let checkResult;
+    try {
+      checkResult = await getBatchRequirements(formData.value!.id!);
+    } catch {
+      // ignore
+    }
+
+    if (checkResult?.needBatchInput) {
+      batchActionType.value = 'submit';
+      batchDialogRef.value?.open({ checkResult });
+    } else {
+      await doSubmit();
+    }
   } finally {
     modalApi.unlock();
   }
 }
-
-const batchDialogRef = ref<InstanceType<typeof BatchInputDialog>>();
 
 /** 鎵ц瀹℃壒锛堝惈鎵规灞炴�э級 */
 async function doApprove(batchFields?: Record<string, unknown>) {
@@ -195,6 +237,7 @@
     const checkResult = await getBatchRequirements(formData.value.id);
     if (checkResult?.needBatchInput) {
       // 闇�瑕佸~鍐欐壒娆″睘鎬э紝寮圭獥鏀堕泦
+      batchActionType.value = 'approve';
       batchDialogRef.value?.open({ checkResult });
     } else {
       await doApprove();
@@ -207,7 +250,11 @@
 
 /** 鎵规寮圭獥纭鍥炶皟 */
 function handleBatchConfirm(batchData: Record<string, unknown>) {
-  doApprove(batchData);
+  if (batchActionType.value === 'submit') {
+    doSubmit(batchData);
+  } else {
+    doApprove(batchData);
+  }
 }
 
 /** 瀹℃壒涓嶉�氳繃 */
diff --git a/src/views/mes/pro/task/data.ts b/src/views/mes/pro/task/data.ts
index aeb9868..12cfcb1 100644
--- a/src/views/mes/pro/task/data.ts
+++ b/src/views/mes/pro/task/data.ts
@@ -393,7 +393,7 @@
 }
 
 /** 鐢熶骇浠诲姟鏂板/淇敼鐨勮〃鍗� */
-export function useTaskFormSchema(formApi?: VbenFormApi, workOrderQuantity?: Ref<number | undefined>): VbenFormSchema[] {
+export function useTaskFormSchema(formApi?: VbenFormApi, workOrderQuantity?: Ref<number | undefined>, feedbackRole?: Ref<string | undefined>): VbenFormSchema[] {
   return [
     {
       fieldName: 'workstationId',
@@ -476,7 +476,7 @@
       componentProps: {
         placeholder: '璇烽�夋嫨鎶ュ伐浜�',
         api: getSimpleUserListByRoleCode,
-        params: { code: 'production_submit_work' },
+        params: { code: feedbackRole?.value || 'production_submit_work' },
         labelField: 'nickname',
         valueField: 'id',
         mode: 'multiple',
diff --git a/src/views/mes/pro/task/modules/task-form.vue b/src/views/mes/pro/task/modules/task-form.vue
index fd83dcd..5b64e26 100644
--- a/src/views/mes/pro/task/modules/task-form.vue
+++ b/src/views/mes/pro/task/modules/task-form.vue
@@ -9,6 +9,7 @@
 
 import { useVbenForm } from '#/adapter/form';
 import { createTask, getTask, updateTask } from '#/api/mes/pro/task';
+import { getWorkOrder } from '#/api/mes/pro/workorder';
 import { $t } from '#/locales';
 
 import { useTaskFormSchema } from '../data';
@@ -23,6 +24,7 @@
   routeId?: number;
   workOrderId?: number;
 }>({}); // 鏂板浠诲姟鏃剁殑璁㈠崟 / 宸ュ簭涓婁笅鏂�
+const feedbackRole = ref<string>();
 
 const getTitle = computed(() => {
   return formData.value?.id
@@ -72,8 +74,6 @@
       context.value = {};
       return;
     }
-    formApi.setState({ schema: useTaskFormSchema(formApi, workOrderQuantity) });
-    // 鍔犺浇鏁版嵁
     const data = modalApi.getData<{
       colorCode?: string;
       id?: number;
@@ -83,30 +83,46 @@
       routeId?: number;
       workOrderId?: number;
     }>();
+
+    let wid: number | undefined;
+
     if (data?.id) {
       modalApi.lock();
       try {
         formData.value = await getTask(data.id);
+        wid = formData.value.workOrderId;
         // 璁剧疆鍒� values
         await formApi.setValues(formData.value);
       } finally {
         modalApi.unlock();
       }
-      return;
+    } else {
+      context.value = {
+        colorCode: data.colorCode,
+        itemId: data.itemId,
+        processId: data.processId,
+        routeId: data.routeId,
+        workOrderId: data.workOrderId,
+      };
+      wid = data.workOrderId;
+      workOrderQuantity.value = data.quantity;
+      await formApi.setValues({
+        colorCode: data.colorCode || '#00AEF3',
+        duration: 1,
+        quantity: data.quantity,
+      });
     }
-    context.value = {
-      colorCode: data.colorCode,
-      itemId: data.itemId,
-      processId: data.processId,
-      routeId: data.routeId,
-      workOrderId: data.workOrderId,
-    };
-    workOrderQuantity.value = data.quantity;
-    await formApi.setValues({
-      colorCode: data.colorCode || '#00AEF3',
-      duration: 1,
-      quantity: data.quantity,
-    });
+
+    if (wid) {
+      try {
+        const wo = await getWorkOrder(wid);
+        feedbackRole.value = wo.feedbackRole;
+      } catch {
+        // ignore
+      }
+    }
+
+    formApi.setState({ schema: useTaskFormSchema(formApi, workOrderQuantity, feedbackRole) });
   },
 });
 </script>
diff --git a/src/views/mes/pro/workorder/data.ts b/src/views/mes/pro/workorder/data.ts
index f62e323..85e98cf 100644
--- a/src/views/mes/pro/workorder/data.ts
+++ b/src/views/mes/pro/workorder/data.ts
@@ -1,6 +1,7 @@
 import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
 import type { VxeTableGridOptions } from '#/adapter/vxe-table';
-import type { MesProWorkOrderApi } from '#/api/mes/pro/workorder';
+import { type MesProWorkOrderApi, getWorkOrderApproveProcessList } from '#/api/mes/pro/workorder';
+import { getSimpleRoleList } from '#/api/system/role';
 import type { MesProWorkOrderBomApi } from '#/api/mes/pro/workorder/bom';
 import type { MesProWorkOrderProcessApi } from '#/api/mes/pro/workorder/process';
 
@@ -237,6 +238,56 @@
       rules: 'required',
     },
     {
+      fieldName: 'feedbackRole',
+      label: '鎶ュ伐瑙掕壊',
+      component: 'ApiSelect',
+      componentProps: {
+        api: getSimpleRoleList,
+        disabled: headerReadonly,
+        labelField: 'name',
+        valueField: 'code',
+        mode: 'multiple',
+        placeholder: '璇烽�夋嫨鎶ュ伐瑙掕壊',
+      },
+      rules: 'selectRequired',
+    },
+    {
+      fieldName: 'feedbackApprove',
+      label: '鏄惁鎶ュ伐瀹℃壒',
+      component: 'Switch',
+      componentProps: {
+        class: '!w-auto',
+        checkedChildren: '鏄�',
+        unCheckedChildren: '鍚�',
+        disabled: headerReadonly,
+      },
+      defaultValue: false,
+      rules: 'required',
+    },
+    {
+      fieldName: 'processDefinitionKey',
+      label: '瀹℃壒娴佺▼',
+      component: 'ApiSelect',
+      componentProps: {
+        api: getWorkOrderApproveProcessList,
+        disabled: headerReadonly,
+        labelField: 'name',
+        valueField: 'key',
+        placeholder: '璇烽�夋嫨瀹℃壒娴佺▼',
+      },
+      dependencies: {
+        triggerFields: ['feedbackApprove'],
+        show: (values) => !!values.feedbackApprove,
+        rules(values) {
+          if (values.feedbackApprove) {
+            return 'selectRequired';
+          }
+          return undefined;
+        },
+      },
+    },
+
+    {
       fieldName: 'remark',
       label: '澶囨敞',
       component: 'Textarea',
diff --git a/src/views/mes/pro/workorder/modules/form.vue b/src/views/mes/pro/workorder/modules/form.vue
index 2a51ec9..5cca3a7 100644
--- a/src/views/mes/pro/workorder/modules/form.vue
+++ b/src/views/mes/pro/workorder/modules/form.vue
@@ -127,7 +127,10 @@
     if (!valid || !formData.value?.id) return;
     modalApi.lock();
     try {
-      const data = (await formApi.getValues()) as MesProWorkOrderApi.WorkOrder;
+      const data = (await formApi.getValues()) as any;
+      if (Array.isArray(data.feedbackRole)) {
+        data.feedbackRole = data.feedbackRole.join(',');
+      }
       await updateWorkOrder({ ...formData.value, ...data });
       if (tempProcessList.value.length > 0) {
         const processList = tempProcessList.value.map(item => ({
@@ -180,7 +183,10 @@
           if (isEditable.value) {
             const current = JSON.stringify(await formApi.getValues());
             if (current !== originalSnapshot.value) {
-              const data = (await formApi.getValues()) as MesProWorkOrderApi.WorkOrder;
+              const data = (await formApi.getValues()) as any;
+              if (Array.isArray(data.feedbackRole)) {
+                data.feedbackRole = data.feedbackRole.join(',');
+              }
               await updateWorkOrder({ ...formData.value, ...data });
               originalSnapshot.value = current;
             }
@@ -242,7 +248,10 @@
       const { valid } = await formApi.validate();
       if (!valid) return;
       modalApi.lock();
-      const data = (await formApi.getValues()) as MesProWorkOrderApi.WorkOrder;
+      const data = (await formApi.getValues()) as any;
+      if (Array.isArray(data.feedbackRole)) {
+        data.feedbackRole = data.feedbackRole.join(',');
+      }
       try {
         if (formData.value?.id) {
           // 缂栬緫妯″紡
@@ -306,7 +315,11 @@
         modalApi.lock();
         try {
           formData.value = await getWorkOrder(data.id);
-          await formApi.setValues(formData.value);
+          const formValues: any = { ...formData.value };
+          if (formValues.feedbackRole && typeof formValues.feedbackRole === 'string') {
+            formValues.feedbackRole = formValues.feedbackRole.split(',');
+          }
+          await formApi.setValues(formValues);
           currentProductId.value = formData.value.productId;
         } finally {
           modalApi.unlock();
diff --git a/src/views/mes/workbench/components/tabs/FeedbackHistoryTab.vue b/src/views/mes/workbench/components/tabs/FeedbackHistoryTab.vue
index 8620c70..0372990 100644
--- a/src/views/mes/workbench/components/tabs/FeedbackHistoryTab.vue
+++ b/src/views/mes/workbench/components/tabs/FeedbackHistoryTab.vue
@@ -3,12 +3,14 @@
 import type { MesProFeedbackApi } from '#/api/mes/pro/feedback';
 
 import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
+import { useRouter } from 'vue-router';
 
 import {
   DICT_TYPE,
   MesProFeedbackStatusEnum,
 } from '@vben/constants';
 import { useVbenModal } from '@vben/common-ui';
+import { useAccess } from '@vben/access';
 
 import { Button, message, Popconfirm, Space } from 'ant-design-vue';
 
@@ -40,6 +42,9 @@
   connectedComponent: FeedbackForm,
   destroyOnClose: true,
 });
+
+const { hasAccessByCodes } = useAccess();
+const router = useRouter();
 
 const batchDialogRef = ref<InstanceType<typeof BatchInputDialog>>();
 const approvingRow = ref<MesProFeedbackApi.Feedback>();
@@ -74,7 +79,7 @@
 
 /** 鎻愪氦锛堜粎鑽夌锛� */
 async function handleSubmit(row: MesProFeedbackApi.Feedback) {
-  await submitFeedback(row.id!);
+  await submitFeedback({ id: row.id! });
   message.success('鎶ュ伐鍗曞凡鎻愪氦');
   refreshGrid();
   emit('reported');
@@ -108,6 +113,15 @@
   message.success('鎶ュ伐鍗曞凡椹冲洖');
   refreshGrid();
   emit('reported');
+}
+
+/** 鏌ョ湅瀹℃壒璇︽儏 */
+function handleViewProcess(row: MesProFeedbackApi.Feedback) {
+  if (!row.processInstanceId) {
+    message.warning('璇ユ姤宸ュ皻鏈彁浜ゅ鎵�');
+    return;
+  }
+  router.push({ path: '/bpm/process-instance/detail', query: { id: row.processInstanceId } });
 }
 
 /** 鐘舵�佸父閲忎緵妯℃澘浣跨敤 */
@@ -274,9 +288,18 @@
                 鍒犻櫎
               </Button>
             </Popconfirm>
-            <!-- 瀹℃壒涓細瀹℃壒閫氳繃銆侀┏鍥� -->
-            <Popconfirm
+            <!-- 瀹℃壒涓細鏌ョ湅瀹℃壒 -->
+            <Button
               v-if="row.status === STATUS_APPROVING"
+              type="link"
+              size="small"
+              @click="handleViewProcess(row)"
+            >
+              鏌ョ湅瀹℃壒
+            </Button>
+            <!-- 
+            <Popconfirm
+              v-if="row.status === STATUS_APPROVING && hasAccessByCodes(['mes:pro-feedback:approve'])"
               title="纭瀹℃壒閫氳繃璇ユ姤宸ュ崟锛�"
               @confirm="handleApprove(row)"
             >
@@ -285,7 +308,7 @@
               </Button>
             </Popconfirm>
             <Popconfirm
-              v-if="row.status === STATUS_APPROVING"
+              v-if="row.status === STATUS_APPROVING && hasAccessByCodes(['mes:pro-feedback:approve'])"
               title="纭椹冲洖璇ユ姤宸ュ崟锛�"
               @confirm="handleReject(row)"
             >
@@ -293,6 +316,7 @@
                 椹冲洖
               </Button>
             </Popconfirm>
+            -->
           </Space>
         </template>
       </Grid>
diff --git a/src/views/mes/workbench/components/tabs/FeedbackTab.vue b/src/views/mes/workbench/components/tabs/FeedbackTab.vue
index 8439b83..f867d68 100644
--- a/src/views/mes/workbench/components/tabs/FeedbackTab.vue
+++ b/src/views/mes/workbench/components/tabs/FeedbackTab.vue
@@ -18,7 +18,7 @@
 import { generateAutoCode } from '#/api/mes/md/autocode/record';
 import {
   approveFeedback,
-  createFeedback,
+  createFeedback, getBatchRequirements,
   getFeedback,
   getFeedbackPage,
   rejectFeedback,
@@ -29,7 +29,10 @@
 import { $t } from '#/locales';
 
 import { useFormSchema } from '../../../pro/feedback/data';
-
+import BatchInputDialog from '../../../pro/feedback/modules/batch-input-dialog.vue';
+const batchDialogRef = ref<InstanceType<typeof BatchInputDialog>>();
+const batchActionType = ref<'submit' | 'approve'>('');
+const pendingSubmitMode = ref<'saveAndSubmit' | 'submit'>('submit');
 defineOptions({ name: 'WorkbenchFeedbackTab' });
 
 const props = defineProps<{
@@ -131,9 +134,9 @@
     itemSpecification: task.itemSpecification,
     unitMeasureName: task.unitMeasureName,
     scheduledQuantity: task.quantity,
+    feedbackApprove: task.feedbackApprove,
     checkFlag,
   });
-
   originalSnapshot.value = JSON.stringify(await getAlignedData());
 }
 
@@ -168,6 +171,7 @@
   if (props.task) {
     await resetForTask(props.task);
   }
+
   emit('reported');
 }
 
@@ -203,8 +207,12 @@
     return;
   }
 
+  pendingSubmitMode.value = 'saveAndSubmit';
+
   // 4. 璋冪敤鎻愪氦鎺ュ彛
-  await submitFeedback(record.id!);
+  const isDone = await handleOnSubmit(feedbackId.value!,props.task.feedbackApprove);
+  if (!isDone) return;
+
   feedbackId.value = record.id;
   feedbackStatus.value = record.status;
 
@@ -232,13 +240,60 @@
     await updateFeedback(data);
     originalSnapshot.value = JSON.stringify(data);
   }
-  await submitFeedback(feedbackId.value!);
-  message.success('鎶ヨ鍗曞凡鎻愪氦');
+
+  pendingSubmitMode.value = 'submit';
+  const isDone = await handleOnSubmit(feedbackId.value!,props.task.feedbackApprove);
+  if (!isDone) return;
+
+  message.success('鎶ュ伐鍗曞凡鎻愪氦');
   // 閲嶇疆琛ㄥ崟涓烘柊寤烘ā寮�
   if (props.task) {
     await resetForTask(props.task);
   }
   emit('reported');
+}
+
+
+/** 鎻愪氦鏂规硶 */
+async function handleOnSubmit(id:any,feedbackRate:any): Promise<boolean> {
+  if (feedbackRate&&feedbackRate==true){
+    await submitFeedback({ id: feedbackId.value! });
+    return true;
+  }
+
+  const checkResult = await getBatchRequirements(id);
+  if (checkResult?.needBatchInput) {
+    batchActionType.value = 'submit';
+    batchDialogRef.value?.open({ checkResult });
+    return false;
+  } else {
+    await submitFeedback({ id: feedbackId.value! });
+    return true;
+  }
+}
+
+/** 鎵规纭鍥炶皟 */
+async function handleBatchConfirm(batchData: Record<string, unknown>) {
+  if (batchActionType.value === 'submit') {
+    await submitFeedback({ id: feedbackId.value!, ...batchData });
+    
+    if (pendingSubmitMode.value === 'saveAndSubmit') {
+      const data = await getAlignedData();
+      const code = data.code as string;
+      Modal.success({
+        title: '淇濆瓨骞舵彁浜ゆ垚鍔�',
+        content: `鎶ュ伐鍗� ${code} 宸叉彁浜,
+        centered: true,
+      });
+    } else {
+      message.success('鎶ュ伐鍗曞凡鎻愪氦');
+    }
+
+    if (props.task) {
+      await resetForTask(props.task);
+    }
+    emit('reported');
+  }
 }
 
 /** 瀹℃壒閫氳繃 */
@@ -294,6 +349,7 @@
 
 <template>
   <div class="feedback-tab">
+    <BatchInputDialog ref="batchDialogRef" @confirm="handleBatchConfirm" />
     <div v-if="!task" class="feedback-tab__empty">
       璇蜂粠宸︿晶宸ュ崟鍒楄〃閫夋嫨涓�鏉′换鍔�
     </div>
diff --git a/src/views/mes/workbench/index.vue b/src/views/mes/workbench/index.vue
index b49191b..3661475 100644
--- a/src/views/mes/workbench/index.vue
+++ b/src/views/mes/workbench/index.vue
@@ -17,6 +17,7 @@
 } from '#/api/mes/pro/workrecord';
 import { getPendingFeedbackPage } from '#/api/mes/pro/task';
 import { router } from '#/router';
+import { useAccess } from '@vben/access';
 
 import TaskListPanel from './components/TaskListPanel.vue';
 import WorkstationPanel from './components/WorkstationPanel.vue';
@@ -33,16 +34,36 @@
 // --- Tab 閰嶇疆 ---
 const tabItems: WorkbenchTabItem[] = [
   { key: 'detail', label: '宸ュ崟璇︽儏', icon: 'lucide:file-text' },
-  { key: 'feedback', label: '鎶ュ伐', icon: 'lucide:edit-3' },
-  { key: 'history', label: '鎶ュ伐璁板綍', icon: 'lucide:history' },
+  { key: 'feedback', label: '鎶ュ伐', icon: 'lucide:edit-3', auth: ['mes:pro-feedback:create'] },
+  { key: 'history', label: '鎶ュ伐璁板綍', icon: 'lucide:history' ,auth: ['mes:pro-feedback:query']},
   { key: 'record-param', label: '鐢熶骇璁板綍鍙傛暟', icon: 'lucide:file-cog' },
   { key: 'product-issue', label: '棰嗘枡', icon: 'lucide:clipboard-list' },
   { key: 'return-issue', label: '閫�鏂�', icon: 'lucide:rotate-ccw' },
 ];
 
+const { hasAccessByCodes } = useAccess();
+
+const availableTabs = computed(() => {
+  return tabItems.filter((tab) => {
+    if (!tab.auth || tab.auth.length === 0) return true;
+    return hasAccessByCodes(tab.auth);
+  });
+});
+
 const activeTab = ref<WorkbenchTabKey>('detail');
 const activeTabLabel = computed(
-  () => tabItems.find((t) => t.key === activeTab.value)?.label ?? '',
+  () => availableTabs.value.find((t) => t.key === activeTab.value)?.label ?? '',
+);
+
+// 褰撳彲鐢� tab 鍙樺寲鏃讹紝濡傛灉褰撳墠閫変腑鐨� tab 涓嶅彲鐢紝鍒欒嚜鍔ㄩ�夋嫨绗竴涓彲鐢ㄧ殑 tab
+watch(
+  availableTabs,
+  (tabs) => {
+    if (tabs.length > 0 && !tabs.find((t) => t.key === activeTab.value)) {
+      activeTab.value = tabs[0]!.key;
+    }
+  },
+  { immediate: true },
 );
 
 // --- 宸ヤ綔绔� ---
@@ -176,7 +197,7 @@
 
       <nav class="workbench-nav">
         <button
-          v-for="tab in tabItems"
+          v-for="tab in availableTabs"
           :key="tab.key"
           type="button"
           class="workbench-nav__item"
diff --git a/src/views/mes/workbench/types.ts b/src/views/mes/workbench/types.ts
index 8f74ad3..af0dd1c 100644
--- a/src/views/mes/workbench/types.ts
+++ b/src/views/mes/workbench/types.ts
@@ -8,6 +8,7 @@
   key: WorkbenchTabKey;
   label: string;
   icon: string;
+  auth?: string[];
 }
 
 /** 寰呮姤宸ヤ换鍔★紙浠� API 绫诲瀷閲嶅鍑猴紝渚夸簬缁勪欢寮曠敤锛� */

--
Gitblit v1.9.3