生产订单与报工模块改造汇总 (Git Commit 文档)
feat(mes-pro): 生产订单及报工模块审批流与批次填报改造
1. 生产订单功能增强 (Work Order)

新增字段支持: 生产订单新增 feedbackApprove (是否报工审批)、processDefinitionKey (审批流程定义) 及 feedbackRole (报工角色) 字段。
表单控件完善: 生产订单新增/编辑表单中,添加了对应的流程模型选择下拉框与多选报工角色下拉组件。
任务分配联动: 生产排产报工页面中,新增生产任务选择报工人列表时,现已支持根据生产订单配置的 feedbackRole 去动态拉取并过滤用户;若未配置则降级使用默认角色。
2. 报工接口参数升级 (Feedback Submission)

参数结构调整: 根据后端 /mes/pro/feedback/submit 接口的最新文档,将原先的 ?id=xxx 传参方式改造为 JSON Body 传参 (MesProFeedbackSubmitReqVO),以支持在无需审批场景下直接提交批次号、生产日期等扩展属性。
3. 报工单提交与批次弹框交互优化 (Feedback Forms)

填报流程解耦: 在 FeedbackTab.vue 和 form.vue 中重构了提交与审批逻辑。点击“提交”或“审批通过”时,系统会先请求 getBatchRequirements 检查是否需要填写批次属性。
按需显示批次弹窗: 若需要填报,弹出 BatchInputDialog 收集信息;完成后再将属性与工单 ID 合并提交至后端。
审批跳过机制 (feedbackApprove): 在表单提交阶段,系统会判断所属生产订单的 feedbackApprove 字段。如果启用了报工审批,则在“提交”阶段跳过批次填报,直接提交单据,将填报动作留给审批环节处理。
时序与状态修复: 修复了此前“保存并提交”时会提前弹出成功提示并重置表单,导致批次弹窗内丢失 feedbackId 的问题。现已引入 pendingSubmitMode 与阻塞式提交回调 (handleBatchConfirm),确保所有异步接口和弹窗流转完毕后再统一展示提示并重置状态。
涉及的主要文件:

src/views/mes/pro/workorder/... (生产订单表单)
src/api/mes/pro/feedback/index.ts (API 定义)
src/views/mes/pro/feedback/modules/form.vue (报工台账表单)
src/views/mes/workbench/components/tabs/FeedbackTab.vue (工作台报工页)
src/views/mes/pro/feedback/modules/batch-input-dialog.vue (批次属性录入)
已修改16个文件
已添加1个文件
498 ■■■■ 文件已修改
.idea/mom-pro2-before.iml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pro/feedback/index.ts 23 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pro/workorder/index.ts 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/user/index.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/routes/modules/mes.ts 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/feedback/data.ts 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/feedback/detail/index.vue 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/feedback/index.vue 30 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/feedback/modules/form.vue 63 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/task/data.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/task/modules/task-form.vue 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/workorder/data.ts 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/workorder/modules/form.vue 21 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/workbench/components/tabs/FeedbackHistoryTab.vue 32 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/workbench/components/tabs/FeedbackTab.vue 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/workbench/index.vue 29 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/workbench/types.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.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" />
src/api/mes/pro/feedback/index.ts
@@ -1,8 +1,23 @@
import 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);
}
/** é©³å›žç”Ÿäº§æŠ¥å·¥ */
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',
  );
}
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 ,
  });
}
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'),
      },
    ],
  },
];
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: '备注' },
  ];
}
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>
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'),
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);
  }
}
/** å®¡æ‰¹ä¸é€šè¿‡ */
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',
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>
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',
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();
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>
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>
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"
src/views/mes/workbench/types.ts
@@ -8,6 +8,7 @@
  key: WorkbenchTabKey;
  label: string;
  icon: string;
  auth?: string[];
}
/** å¾…报工任务(从 API ç±»åž‹é‡å¯¼å‡ºï¼Œä¾¿äºŽç»„件引用) */