已添加1个文件
已修改33个文件
1210 ■■■■ 文件已修改
src/api/mes/pro/feedback/index.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pro/process/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pro/route/process/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pro/task/index.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/wm/productreceipt/index.ts 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/wm/productreceipt/line/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/feedback/data.ts 188 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/feedback/modules/batch-feedback.vue 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/feedback/modules/form.vue 93 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/feedback/modules/wait-countdown.vue 137 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/feedback/param-state.ts 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/process/data.ts 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/process/modules/form.vue 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/task/modules/task-form.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/workorder-center/modules/quick-feedback.vue 108 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/process-design/process/data.ts 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/process-design/process/modules/form.vue 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/process-design/route/data.ts 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/process-design/route/modules/process-form.vue 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/workbench/components/tabs/FeedbackTab.vue 187 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/workbench/components/tabs/TaskDetailTab.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/approval/config/data.ts 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/approval/config/index.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/approval/config/modules/process-config-form.vue 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/arrivalnotice/modules/po-select-dialog.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/itemreceipt/modules/an-select-dialog.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/materialstock/components/stock-adjust-modal.vue 56 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/productreceipt/data.ts 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/productreceipt/index.vue 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/productreceipt/modules/form.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/productreceipt/modules/line-list.vue 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/productsales/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/returnsales/modules/form.vue 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/returnsales/modules/line-form.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pro/feedback/index.ts
@@ -178,8 +178,6 @@
  taskId: number;
  paramValues?: MesProFeedbackApi.ParamValue[];
  feedbackQuantity: number;
  qualifiedQuantity?: number;
  unqualifiedQuantity?: number;
  remark?: string;
}
@@ -188,14 +186,14 @@
  feedbackId?: number;
  taskStatus?: number;
  workOrderStatus?: number;
  /** æŠ¥å·¥åŽå‰©ä½™å¯æŠ¥å·¥æ•°é‡ */
  reportableQuantity?: number;
}
/** æ‰¹é‡æŠ¥å·¥è¯·æ±‚ */
export interface BatchCreateReq {
  taskIds: number[];
  feedbackQuantity: number;
  qualifiedQuantity?: number;
  unqualifiedQuantity?: number;
  remark?: string;
}
src/api/mes/pro/process/index.ts
@@ -12,6 +12,8 @@
    status?: number;
    remark?: string;
    createTime?: Date;
    waitDuration?: number; // ç­‰å¾…时长(小时),报工前需倒计时等待
    needReportQuantity?: boolean; // æ˜¯å¦éœ€è¦å¡«å†™æŠ¥å·¥æ•°é‡
  }
}
src/api/mes/pro/route/process/index.ts
@@ -36,6 +36,8 @@
    remark?: string;
    bomItems?: ProcessBomItem[];
    createTime?: Date;
    waitDuration?: number; // ç­‰å¾…时长(小时),>0 è¡¨ç¤ºæŠ¥å·¥å‰éœ€å€’计时等待
    needReportQuantity?: boolean; // æ˜¯å¦éœ€è¦å¡«å†™æŠ¥å·¥æ•°é‡ï¼Œfalse è¡¨ç¤ºæŠ¥å·¥æ•°é‡å…è®¸ä¸ºç©º
  }
}
src/api/mes/pro/task/index.ts
@@ -58,6 +58,8 @@
    pauseRecords?: PauseRecord[]; // åœå·¥è®°å½•
    reportableQuantity?: number; // å¯æŠ¥å·¥æ•°é‡
    assigneeUserIds?: number[]; // æŠ¥å·¥äºº
    feedbackApprove?: boolean; // æ˜¯å¦æŠ¥å·¥å®¡æ‰¹ï¼ˆæ´¾ç”Ÿè‡ªå·¥å•)
    waitStartTime?: string; // æŠ¥å·¥å€’计时开始时间(yyyy-MM-dd HH:mm:ss)
  }
  /** MES ç”Ÿäº§ä»»åŠ¡åˆ†é¡µæŸ¥è¯¢å‚æ•° */
@@ -208,3 +210,10 @@
export function resumeTask(data: { id: number }) {
  return requestClient.put('/mes/pro/task/resume', data);
}
/** å¼€å§‹æŠ¥å·¥å€’计时,返回倒计时开始时间(yyyy-MM-dd HH:mm:ss) */
export function startWaitCountdown(taskId: number) {
  return requestClient.post<string>('/mes/pro/task/start-wait-countdown', {
    taskId,
  });
}
src/api/mes/wm/productreceipt/index.ts
@@ -18,7 +18,29 @@
    receiptDate?: number; // å…¥åº“日期
    status?: number; // å•据状态
    remark?: string; // å¤‡æ³¨
    feedbackId?: number; // æ¥æºæŠ¥å·¥ç¼–号(非空表示由报工自动生成)
    createTime?: Date; // åˆ›å»ºæ—¶é—´
  }
  /** ä¸Šæž¶åˆ†é…æ˜Žç»† */
  export interface StockDetail {
    warehouseId: number; // ä»“库编号
    locationId: number; // åº“区编号
    areaId: number; // åº“位编号
    quantity: number; // åˆ†é…æ•°é‡
  }
  /** ä¸Šæž¶è¡Œ */
  export interface StockRow {
    lineId: number; // å…¥åº“单行编号
    isStock: boolean; // æ˜¯å¦å…¥åº“
    details?: StockDetail[]; // isStock=true æ—¶çš„仓库/库区/库位分配
  }
  /** æ‰¹é‡ä¸Šæž¶è¯·æ±‚ */
  export interface StockRequest {
    receiptId: number; // äº§å“å…¥åº“单编号
    rows: StockRow[]; // è¡Œä¸Šæž¶åˆ—表
  }
}
@@ -83,6 +105,16 @@
  );
}
/** æ‰¹é‡ä¸Šæž¶ï¼ˆå‹¾é€‰æ˜¯å¦å…¥åº“ + åˆ†é…ä»“库/库区/库位数量) */
export function submitProductReceiptStock(
  data: MesWmProductReceiptApi.StockRequest,
) {
  return requestClient.post<boolean>(
    '/mes/wm/product-receipt/submit-stock',
    data,
  );
}
/** å¯¼å‡ºäº§å“å…¥åº“单 */
export function exportProductReceipt(params: any) {
  return requestClient.download('/mes/wm/product-receipt/export-excel', {
src/api/mes/wm/productreceipt/line/index.ts
@@ -16,6 +16,8 @@
    quantity?: number; // å…¥åº“数量
    batchId?: number; // æ‰¹æ¬¡ç¼–号
    batchCode?: string; // æ‰¹æ¬¡å·
    qualityStatus?: number; // è´¨é‡çŠ¶æ€ï¼ˆå­—å…¸ MES_WM_QUALITY_STATUS åˆæ ¼/不合格)
    isStock?: boolean; // æ˜¯å¦å…¥åº“(false/空=执行入库时默认进不合格仓库)
    remark?: string; // å¤‡æ³¨
  }
}
src/views/mes/pro/feedback/data.ts
@@ -19,7 +19,13 @@
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import { getRouteProcessByRouteAndProcess } from '#/api/mes/pro/route/process';
import { getSimpleUserList } from '#/api/system/user';
import { loadParamDetails } from './param-state';
import {
  loadParamDetails,
  resetWaitConfig,
  waitDuration,
  waitStartTime,
  waitTaskId,
} from './param-state';
import { getRangePickerDefaultProps } from '#/utils';
import { MdItemSelect } from '#/views/mes/md/item/components';
import { MdWorkstationSelect } from '#/views/mes/md/workstation/components';
@@ -174,9 +180,8 @@
 * - create / update:录入报工主体信息和数量
 * - submit / approve / detail:主体字段只读
 *
 * æ•°é‡åŒºåŸŸæ ¹æ® `checkFlag` å’Œ `unqualifiedQuantity` åŠ¨æ€æ˜¾ç¤ºï¼š
 * - éžè´¨æ£€å·¥åºï¼šæŠ¥å·¥æ•°é‡ = åˆæ ¼ + ä¸è‰¯ï¼›ä¸è‰¯ > 0 æ—¶å†å±•开工废/料废/其他废品
 * - è´¨æ£€å·¥åºï¼šåªå¡«æŠ¥å·¥æ•°é‡ï¼ˆè§†ä¸ºå¾…检数量)
 * æŠ¥å·¥åªå¡«å†™"报工数量";合格品/不良品数量由质检确定,报工环节不采集。
 * è´¨æ£€å·¥åºæŠ¥å·¥åŽè¿›å…¥å¾…检验(uncheckQuantity = æŠ¥å·¥æ•°é‡ï¼‰ï¼Œéžè´¨æ£€å·¥åºç›´æŽ¥å®Œç»“。
 */
export function useFormSchema(
  formType: FormType,
@@ -191,6 +196,12 @@
    },
    {
      fieldName: 'checkFlag',
      component: 'Input',
      dependencies: { triggerFields: [''], show: () => false },
      defaultValue: true,
    },
    {
      fieldName: 'needReportQuantity',
      component: 'Input',
      dependencies: { triggerFields: [''], show: () => false },
      defaultValue: true,
@@ -256,8 +267,10 @@
        status: MesProWorkOrderStatusEnum.CONFIRMED,
        // è®¢å•变更:清空任务及任务带出的产品信息、数量区域控制位
        onChange: async () => {
          resetWaitConfig();
          await formApi?.setValues({
            checkFlag: true,
            needReportQuantity: true,
            itemCode: undefined,
            itemId: undefined,
            itemName: undefined,
@@ -292,6 +305,8 @@
        onChange: async (task?: MesProTaskApi.Task) => {
          await loadParamDetails(task?.id);
          if (!task) {
            resetWaitConfig();
            await formApi?.setFieldValue('needReportQuantity', true);
            return;
          }
          await formApi?.setValues({
@@ -305,7 +320,9 @@
            workstationId: task.workstationId,
            reportableQuantity: task.reportableQuantity,
          });
          // å·¥è‰ºè·¯çº¿å·¥åºçš„ checkFlag å†³å®šæ•°é‡åŒºåŸŸå±•示
          waitTaskId.value = task.id;
          waitStartTime.value = task.waitStartTime;
          // å·¥è‰ºè·¯çº¿å·¥åºçš„ checkFlag / ç­‰å¾…æ—¶é•¿ / æŠ¥å·¥æ•°é‡è§„则决定展示
          if (task.routeId && task.processId) {
            try {
              const routeProcess = await getRouteProcessByRouteAndProcess(
@@ -316,9 +333,19 @@
                'checkFlag',
                routeProcess?.checkFlag ?? false,
              );
              await formApi?.setFieldValue(
                'needReportQuantity',
                routeProcess?.needReportQuantity ?? true,
              );
              waitDuration.value = routeProcess?.waitDuration;
            } catch {
              await formApi?.setFieldValue('checkFlag', true);
              await formApi?.setFieldValue('needReportQuantity', true);
              waitDuration.value = undefined;
            }
          } else {
            waitDuration.value = undefined;
            await formApi?.setFieldValue('needReportQuantity', true);
          }
        },
      },
@@ -404,150 +431,13 @@
        class: 'w-full',
        min: 0,
        precision: 2,
        placeholder: '请输入报工数量',
      },
      dependencies: {
        triggerFields: ['checkFlag'],
        // éžè´¨æ£€å·¥åºæ—¶ï¼ŒæŠ¥å·¥æ•°é‡ = åˆæ ¼ + ä¸è‰¯ï¼Œç¦ç”¨ç›´æŽ¥ç¼–辑
        componentProps: (values) => ({
          class: 'w-full',
          disabled: !values.checkFlag,
          min: 0,
          placeholder: '请输入报工数量',
          precision: 2,
        }),
      },
      rules: 'required',
    },
    {
      fieldName: 'qualifiedQuantity',
      label: '合格品数量',
      component: 'InputNumber',
      componentProps: {
        class: 'w-full',
        min: 0,
        precision: 2,
        // åˆæ ¼/不良变更,自动累计为报工数量
        onChange: async () => {
          const values = await formApi?.getValues();
          await formApi?.setFieldValue(
            'feedbackQuantity',
            (values?.qualifiedQuantity || 0) +
              (values?.unqualifiedQuantity || 0),
          );
        },
      },
      defaultValue: 0,
      dependencies: {
        triggerFields: ['checkFlag'],
        show: (values) => !values.checkFlag,
      },
    },
    {
      fieldName: 'unqualifiedQuantity',
      label: '不良品数量',
      component: 'InputNumber',
      componentProps: {
        class: 'w-full',
        min: 0,
        precision: 2,
        // åˆæ ¼/不良变更,自动累计为报工数量
        onChange: async () => {
          const values = await formApi?.getValues();
          await formApi?.setFieldValue(
            'feedbackQuantity',
            (values?.qualifiedQuantity || 0) +
              (values?.unqualifiedQuantity || 0),
          );
        },
      },
      defaultValue: 0,
      dependencies: {
        triggerFields: ['checkFlag'],
        show: (values) => !values.checkFlag,
      },
    },
    {
      fieldName: 'laborScrapQuantity',
      label: '工废数量',
      component: 'InputNumber',
      componentProps: {
        class: 'w-full',
        min: 0,
        precision: 2,
        // åºŸå“åˆ†ç±»å˜æ›´ï¼Œè‡ªåŠ¨ç´¯è®¡ä¸ºä¸è‰¯å“æ•°é‡åŠæŠ¥å·¥æ•°é‡
        onChange: async () => {
          const values = await formApi?.getValues();
          const unqualified =
            (values?.laborScrapQuantity || 0) +
            (values?.materialScrapQuantity || 0) +
            (values?.otherScrapQuantity || 0);
          await formApi?.setValues({
            feedbackQuantity: (values?.qualifiedQuantity || 0) + unqualified,
            unqualifiedQuantity: unqualified,
          });
        },
      },
      defaultValue: 0,
      dependencies: {
        triggerFields: ['checkFlag', 'unqualifiedQuantity'],
        show: (values) =>
          !values.checkFlag && (values.unqualifiedQuantity || 0) > 0,
      },
    },
    {
      fieldName: 'materialScrapQuantity',
      label: '料废数量',
      component: 'InputNumber',
      componentProps: {
        class: 'w-full',
        min: 0,
        precision: 2,
        // åºŸå“åˆ†ç±»å˜æ›´ï¼Œè‡ªåŠ¨ç´¯è®¡ä¸ºä¸è‰¯å“æ•°é‡åŠæŠ¥å·¥æ•°é‡
        onChange: async () => {
          const values = await formApi?.getValues();
          const unqualified =
            (values?.laborScrapQuantity || 0) +
            (values?.materialScrapQuantity || 0) +
            (values?.otherScrapQuantity || 0);
          await formApi?.setValues({
            feedbackQuantity: (values?.qualifiedQuantity || 0) + unqualified,
            unqualifiedQuantity: unqualified,
          });
        },
      },
      defaultValue: 0,
      dependencies: {
        triggerFields: ['checkFlag', 'unqualifiedQuantity'],
        show: (values) =>
          !values.checkFlag && (values.unqualifiedQuantity || 0) > 0,
      },
    },
    {
      fieldName: 'otherScrapQuantity',
      label: '其他废品',
      component: 'InputNumber',
      componentProps: {
        class: 'w-full',
        min: 0,
        precision: 2,
        // åºŸå“åˆ†ç±»å˜æ›´ï¼Œè‡ªåŠ¨ç´¯è®¡ä¸ºä¸è‰¯å“æ•°é‡åŠæŠ¥å·¥æ•°é‡
        onChange: async () => {
          const values = await formApi?.getValues();
          const unqualified =
            (values?.laborScrapQuantity || 0) +
            (values?.materialScrapQuantity || 0) +
            (values?.otherScrapQuantity || 0);
          await formApi?.setValues({
            feedbackQuantity: (values?.qualifiedQuantity || 0) + unqualified,
            unqualifiedQuantity: unqualified,
          });
        },
      },
      defaultValue: 0,
      dependencies: {
        triggerFields: ['checkFlag', 'unqualifiedQuantity'],
        show: (values) =>
          !values.checkFlag && (values.unqualifiedQuantity || 0) > 0,
        triggerFields: ['needReportQuantity'],
        show: (values) => values.needReportQuantity !== false,
        rules: (values) =>
          values.needReportQuantity === false ? null : 'required',
      },
    },
    {
@@ -616,8 +506,8 @@
      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: 'workOrderId', label: '生产订单', render: (_val, data) => data?.workOrderCode },
    { field: 'taskId', label: '生产任务', render: (_val, data) => data?.taskCode },
    { field: 'workstationName', label: '工作站' },
    { field: 'itemCode', label: '产品编码' },
    { field: 'itemName', label: '产品名称' },
src/views/mes/pro/feedback/modules/batch-feedback.vue
@@ -1,13 +1,14 @@
<script lang="ts" setup>
import type { MesProTaskApi } from '#/api/mes/pro/task';
import { ref } from 'vue';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Button, message, Tag } from 'ant-design-vue';
import { batchCreateFeedback } from '#/api/mes/pro/feedback';
import { getRouteProcessByRouteAndProcess } from '#/api/mes/pro/route/process';
import ProTaskSelectDialog from '#/views/mes/pro/task/components/select-dialog.vue';
defineOptions({ name: 'BatchFeedbackForm' });
@@ -24,8 +25,6 @@
      selectedTasks.value = [];
      formData.value = {
        feedbackQuantity: undefined,
        qualifiedQuantity: undefined,
        unqualifiedQuantity: undefined,
        remark: '',
      };
    }
@@ -34,24 +33,49 @@
const taskDialogRef = ref<InstanceType<typeof ProTaskSelectDialog>>();
const selectedTasks = ref<MesProTaskApi.Task[]>([]);
const needReportQuantityMap = ref<Record<number, boolean>>({});
const formData = ref({
  feedbackQuantity: undefined as number | undefined,
  qualifiedQuantity: undefined as number | undefined,
  unqualifiedQuantity: undefined as number | undefined,
  remark: '',
});
const submitting = ref(false);
/** æ˜¯å¦éœ€è¦å¡«å†™æŠ¥å·¥æ•°é‡ï¼ˆä»»ä¸€é€‰ä¸­ä»»åŠ¡å·¥åºè¦æ±‚å¡«å†™æ•°é‡å³å¿…å¡«ï¼‰ */
const needQuantity = computed(() =>
  selectedTasks.value.some(
    (t) => needReportQuantityMap.value[t.id!] ?? true,
  ),
);
function handleOpenTaskDialog() {
  taskDialogRef.value?.open([], { multiple: true });
}
function handleTaskSelected(rows: MesProTaskApi.Task[]) {
/** æŒ‰é€‰ä¸­ä»»åŠ¡å·¥åºåŠ è½½æ˜¯å¦éœ€è¦æŠ¥å·¥æ•°é‡ */
async function handleTaskSelected(rows: MesProTaskApi.Task[]) {
  selectedTasks.value = rows;
  needReportQuantityMap.value = {};
  for (const task of rows) {
    if (!task.routeId || !task.processId) {
      continue;
    }
    try {
      const routeProcess = await getRouteProcessByRouteAndProcess(
        task.routeId,
        task.processId,
      );
      needReportQuantityMap.value[task.id!] =
        routeProcess?.needReportQuantity ?? true;
    } catch {
      // åŠ è½½å¤±è´¥æŒ‰é»˜è®¤å¤„ç†ï¼ˆæ•°é‡å¿…å¡«ï¼‰
      needReportQuantityMap.value[task.id!] = true;
    }
  }
}
function handleRemoveTask(taskId: number) {
  selectedTasks.value = selectedTasks.value.filter((t) => t.id !== taskId);
  delete needReportQuantityMap.value[taskId];
}
async function handleSubmit() {
@@ -59,7 +83,7 @@
    message.warning('请选择任务');
    return;
  }
  if (!formData.value.feedbackQuantity) {
  if (needQuantity.value && !formData.value.feedbackQuantity) {
    message.warning('请输入报工数量');
    return;
  }
@@ -67,9 +91,9 @@
  try {
    const result = await batchCreateFeedback({
      taskIds: selectedTasks.value.map((t) => t.id!),
      feedbackQuantity: formData.value.feedbackQuantity,
      qualifiedQuantity: formData.value.qualifiedQuantity,
      unqualifiedQuantity: formData.value.unqualifiedQuantity,
      feedbackQuantity: needQuantity.value
        ? (formData.value.feedbackQuantity ?? 0)
        : 0,
      remark: formData.value.remark || undefined,
    });
    if (result.failCount) {
@@ -107,30 +131,17 @@
          </Tag>
        </div>
      </a-form-item>
      <a-form-item label="报工数量" required>
      <a-form-item
        v-if="needQuantity"
        label="报工数量"
        required
        tooltip="工序配置为不需填写报工数量时,合格品/不良品数量由质检确定"
      >
        <a-input-number
          v-model:value="formData.feedbackQuantity"
          :min="0.01"
          :precision="2"
          placeholder="请输入报工数量(应用到所有选中任务)"
          class="!w-full"
        />
      </a-form-item>
      <a-form-item label="合格品数量">
        <a-input-number
          v-model:value="formData.qualifiedQuantity"
          :min="0"
          :precision="2"
          placeholder="请输入合格品数量"
          class="!w-full"
        />
      </a-form-item>
      <a-form-item label="不良品数量">
        <a-input-number
          v-model:value="formData.unqualifiedQuantity"
          :min="0"
          :precision="2"
          placeholder="请输入不良品数量"
          class="!w-full"
        />
      </a-form-item>
src/views/mes/pro/feedback/modules/form.vue
@@ -25,14 +25,24 @@
  updateFeedback,
} from '#/api/mes/pro/feedback';
import { getRouteProcessByRouteAndProcess } from '#/api/mes/pro/route/process';
import { getTask } from '#/api/mes/pro/task';
import { getWorkOrder } from '#/api/mes/pro/workorder';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
import { findMissingRequiredParams, loadParamDetails, paramDetails } from '../param-state';
import {
  findMissingRequiredParams,
  loadParamDetails,
  paramDetails,
  resetWaitConfig,
  waitDuration,
  waitStartTime,
  waitTaskId,
} from '../param-state';
import BatchInputDialog from './batch-input-dialog.vue';
import ItemConsumeList from './item-consume-list.vue';
import ProductProduceList from './product-produce-list.vue';
import WaitCountdown from './wait-countdown.vue';
const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
@@ -87,27 +97,30 @@
  wrapperClass: 'grid-cols-3',
});
/** æäº¤å‰å¯¹é½æ•°é‡ï¼šæ ¹æ® checkFlag å†³å®š uncheck/合格/不良归零策略 */
function alignQuantity(data: MesProFeedbackApi.Feedback) {
  if (data.checkFlag) {
    data.uncheckQuantity = data.feedbackQuantity;
    data.qualifiedQuantity = 0;
    data.unqualifiedQuantity = 0;
    data.laborScrapQuantity = 0;
    data.materialScrapQuantity = 0;
    data.otherScrapQuantity = 0;
  } else {
    data.feedbackQuantity =
      (data.qualifiedQuantity || 0) + (data.unqualifiedQuantity || 0);
    data.uncheckQuantity = 0;
/** æäº¤å‰å¯¹é½æ•°é‡ï¼šæŠ¥å·¥åªå¡«å†™æŠ¥å·¥æ•°é‡ï¼Œåˆæ ¼/不良/废品数量由质检确定,报工环节不采集 */
function alignQuantity(
  data: MesProFeedbackApi.Feedback,
  needReportQuantity: boolean,
) {
  if (!needReportQuantity) {
    data.feedbackQuantity = 0;
  }
  data.uncheckQuantity = data.checkFlag ? data.feedbackQuantity : 0;
  delete data.qualifiedQuantity;
  delete data.unqualifiedQuantity;
  delete data.laborScrapQuantity;
  delete data.materialScrapQuantity;
  delete data.otherScrapQuantity;
}
/** è¯»å–并按提交规则对齐后的表单数据,用于快照和保存 */
async function getAlignedData(): Promise<MesProFeedbackApi.Feedback> {
  const data = (await formApi.getValues()) as MesProFeedbackApi.Feedback;
  const data = (await formApi.getValues()) as MesProFeedbackApi.Feedback & {
    needReportQuantity?: boolean;
  };
  syncParamValues(data);
  alignQuantity(data);
  alignQuantity(data, data.needReportQuantity !== false);
  delete data.needReportQuantity;
  return data;
}
@@ -149,7 +162,7 @@
}
const batchDialogRef = ref<InstanceType<typeof BatchInputDialog>>();
const batchActionType = ref<'submit' | 'approve'>('');
const batchActionType = ref<'' | 'submit' | 'approve'>('');
function syncParamValues(data: MesProFeedbackApi.Feedback) {
  data.paramValues = paramDetails.value.map((detail) => ({
@@ -298,19 +311,24 @@
  }
}
/** åŠ è½½å·¥åºçš„ checkFlag ç”¨äºŽå›žæ˜¾æ•°é‡åŒºåŸŸ */
async function resolveCheckFlag(routeId?: number, processId?: number) {
/** åŠ è½½å·¥åºé…ç½®ï¼ˆcheckFlag / ç­‰å¾…æ—¶é•¿ / æŠ¥å·¥æ•°é‡è§„则)用于回显 */
async function resolveRouteProcessConfig(routeId?: number, processId?: number) {
  if (!routeId || !processId) {
    return true;
    return { checkFlag: true, needReportQuantity: true };
  }
  try {
    const routeProcess = await getRouteProcessByRouteAndProcess(
      routeId,
      processId,
    );
    return routeProcess?.checkFlag ?? false;
    waitDuration.value = routeProcess?.waitDuration;
    return {
      checkFlag: routeProcess?.checkFlag ?? false,
      needReportQuantity: routeProcess?.needReportQuantity ?? true,
    };
  } catch {
    return true;
    waitDuration.value = undefined;
    return { checkFlag: true, needReportQuantity: true };
  }
}
@@ -328,6 +346,7 @@
      subTabsName.value = 'itemConsume';
      originalSnapshot.value = '';
      paramDetails.value = [];
      resetWaitConfig();
      return;
    }
    // åŠ è½½æ•°æ®
@@ -363,12 +382,26 @@
    try {
      formData.value = await getFeedback(data.id);
      await loadParamDetails(formData.value.taskId);
      const checkFlag = await resolveCheckFlag(
      const config = await resolveRouteProcessConfig(
        formData.value.routeId,
        formData.value.processId,
      );
      waitTaskId.value = formData.value.taskId;
      waitStartTime.value = undefined;
      if (formData.value.taskId) {
        try {
          const task = await getTask(formData.value.taskId);
          waitStartTime.value = task?.waitStartTime;
        } catch {
          // ä»»åŠ¡ä¸å­˜åœ¨æ—¶å¿½ç•¥å€’è®¡æ—¶å›žæ˜¾
        }
      }
      // è®¾ç½®åˆ° values
      await formApi.setValues({ ...formData.value, checkFlag });
      await formApi.setValues({
        ...formData.value,
        checkFlag: config.checkFlag,
        needReportQuantity: config.needReportQuantity,
      });
      // è®°å½•初始快照(含数量对齐结果),submit æ—¶ç”¨äºŽè·³è¿‡æ— å˜æ›´çš„保存请求
      originalSnapshot.value = JSON.stringify(await getAlignedData());
    } finally {
@@ -381,6 +414,18 @@
<template>
  <Modal :title="getTitle" class="w-3/5">
    <Form class="mx-4" />
    <div
      v-if="waitDuration"
      class="mx-4 mt-4 rounded border border-gray-200 p-4"
    >
      <div class="mb-2 font-medium">报工倒计时</div>
      <WaitCountdown
        :task-id="waitTaskId"
        :wait-duration="waitDuration"
        :wait-start-time="waitStartTime"
        :disabled="!isEditable"
      />
    </div>
    <div v-if="paramDetails.length" class="mx-4 mt-4 rounded border border-gray-200 p-4">
      <div class="mb-3 font-medium">工艺参数</div>
      <div class="grid grid-cols-2 gap-3">
src/views/mes/pro/feedback/modules/wait-countdown.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,137 @@
<script lang="ts" setup>
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import dayjs from 'dayjs';
import { Button, message } from 'ant-design-vue';
import { startWaitCountdown } from '#/api/mes/pro/task';
defineOptions({ name: 'WaitCountdown' });
interface Props {
  taskId?: number;
  /** ç­‰å¾…时长(小时),>0 æ‰å±•示倒计时 */
  waitDuration?: number;
  /** å€’计时开始时间(yyyy-MM-dd HH:mm:ss) */
  waitStartTime?: string;
  /** åªè¯»æ¨¡å¼ï¼ˆè¯¦æƒ…/审批)下不展示开始按钮 */
  disabled?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
  taskId: undefined,
  waitDuration: undefined,
  waitStartTime: undefined,
  disabled: false,
});
const starting = ref(false);
const startTime = ref<string | undefined>(props.waitStartTime);
const now = ref(Date.now());
let timer: ReturnType<typeof setInterval> | undefined;
const showCountdown = computed(
  () => !!props.waitDuration && props.waitDuration > 0,
);
const endTime = computed(() => {
  if (!startTime.value || !showCountdown.value) {
    return undefined;
  }
  return dayjs(startTime.value, 'YYYY-MM-DD HH:mm:ss').valueOf() +
    props.waitDuration! * 3600 * 1000;
});
const remainingMs = computed(() => {
  if (!endTime.value) {
    return 0;
  }
  return Math.max(0, endTime.value - now.value);
});
const counting = computed(() => remainingMs.value > 0);
const remainingText = computed(() => {
  const totalSeconds = Math.ceil(remainingMs.value / 1000);
  const hours = Math.floor(totalSeconds / 3600);
  const minutes = Math.floor((totalSeconds % 3600) / 60);
  const seconds = totalSeconds % 60;
  const pad = (n: number) => String(n).padStart(2, '0');
  if (hours > 0) {
    return `剩余 ${hours} å°æ—¶ ${pad(minutes)} åˆ† ${pad(seconds)} ç§’`;
  }
  return `剩余 ${pad(minutes)} åˆ† ${pad(seconds)} ç§’`;
});
function startTimer() {
  if (timer) {
    clearInterval(timer);
  }
  timer = setInterval(() => {
    now.value = Date.now();
    if (remainingMs.value <= 0) {
      clearInterval(timer);
      timer = undefined;
    }
  }, 1000);
}
watch(
  () => props.waitStartTime,
  (value) => {
    startTime.value = value;
    if (value) {
      now.value = Date.now();
      startTimer();
    }
  },
);
onMounted(() => {
  if (startTime.value) {
    startTimer();
  }
});
onBeforeUnmount(() => {
  if (timer) {
    clearInterval(timer);
    timer = undefined;
  }
});
async function handleStart() {
  if (!props.taskId) {
    message.warning('请先选择任务');
    return;
  }
  starting.value = true;
  try {
    const time = await startWaitCountdown(props.taskId);
    startTime.value = time;
    now.value = Date.now();
    startTimer();
    message.success('倒计时已开始');
  } finally {
    starting.value = false;
  }
}
</script>
<template>
  <div v-if="showCountdown" class="flex items-center gap-2">
    <Button
      v-if="!startTime && !disabled"
      type="primary"
      :loading="starting"
      @click="handleStart"
    >
      å¼€å§‹å€’计时
    </Button>
    <span v-if="startTime" class="text-sm">
      <span v-if="counting" class="text-orange-500">{{ remainingText }}</span>
      <span v-else class="text-green-600">倒计时已结束,可报工</span>
    </span>
  </div>
</template>
src/views/mes/pro/feedback/param-state.ts
@@ -45,3 +45,17 @@
    .filter((detail) => detail.required && !detail.paramValue?.trim())
    .map((detail) => detail.paramName || detail.paramCode || '');
}
/** å½“前报工任务编号(用于倒计时接口) */
export const waitTaskId = ref<number>();
/** å½“前任务工序的等待时长(小时),>0 è¡¨ç¤ºæŠ¥å·¥å‰éœ€å€’计时 */
export const waitDuration = ref<number>();
/** å½“前任务报工倒计时开始时间(yyyy-MM-dd HH:mm:ss) */
export const waitStartTime = ref<string>();
/** é‡ç½®æŠ¥å·¥å€’计时相关状态 */
export function resetWaitConfig() {
  waitTaskId.value = undefined;
  waitDuration.value = undefined;
  waitStartTime.value = undefined;
}
src/views/mes/pro/process/data.ts
@@ -77,6 +77,29 @@
      rules: z.number().default(CommonStatusEnum.ENABLE),
    },
    {
      fieldName: 'waitDuration',
      label: '等待时长(小时)',
      component: 'InputNumber',
      formItemClass: 'col-span-2',
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 2,
        placeholder: '留空或 0 è¡¨ç¤ºæ— éœ€å€’计时',
      },
    },
    {
      fieldName: 'needReportQuantity',
      label: '需填写报工数量',
      component: 'Switch',
      formItemClass: 'col-span-1',
      defaultValue: true,
      componentProps: {
        checkedChildren: '是',
        unCheckedChildren: '否',
      },
    },
    {
      fieldName: 'attention',
      label: '工序说明',
      component: 'Textarea',
src/views/mes/pro/process/modules/form.vue
@@ -88,8 +88,11 @@
    modalApi.lock();
    try {
      formData.value = await getProcess(data.id);
      // è®¾ç½®åˆ° values
      await formApi.setValues(formData.value);
      // è®¾ç½®åˆ° values(旧数据未配置报工数量开关时按默认需要填写)
      await formApi.setValues({
        ...formData.value,
        needReportQuantity: formData.value.needReportQuantity ?? true,
      });
    } finally {
      modalApi.unlock();
    }
src/views/mes/pro/task/modules/task-form.vue
@@ -85,6 +85,8 @@
    let wid: number | undefined;
    if (data?.id) {
      // ç¼–辑模式:先设置表单 schema,再回填任务数据,避免字段未建立时 setValues ä¸¢å¤±
      formApi.setState({ schema: useTaskFormSchema(formApi, workOrderQuantity) });
      modalApi.lock();
      try {
        formData.value = await getTask(data.id);
@@ -104,14 +106,14 @@
      };
      wid = data.workOrderId;
      workOrderQuantity.value = data.quantity;
      // æ–°å¢žæ¨¡å¼ï¼šå…ˆè®¾ç½®è¡¨å• schema,再设置初始值
      formApi.setState({ schema: useTaskFormSchema(formApi, workOrderQuantity) });
      await formApi.setValues({
        colorCode: data.colorCode || '#00AEF3',
        duration: 1,
        quantity: data.quantity,
      });
    }
    formApi.setState({ schema: useTaskFormSchema(formApi, workOrderQuantity) });
  },
});
</script>
src/views/mes/pro/workorder-center/modules/quick-feedback.vue
@@ -9,6 +9,13 @@
import { message } from 'ant-design-vue';
import { quickCreateFeedback } from '#/api/mes/pro/feedback';
import { getRouteProcessByRouteAndProcess } from '#/api/mes/pro/route/process';
import { getTask } from '#/api/mes/pro/task';
import {
  loadParamDetails,
  paramDetails,
} from '#/views/mes/pro/feedback/param-state';
import WaitCountdown from '#/views/mes/pro/feedback/modules/wait-countdown.vue';
import ProTaskSelect from '#/views/mes/pro/task/components/select.vue';
defineOptions({ name: 'QuickFeedbackForm' });
@@ -35,10 +42,20 @@
      selectedTask.value = undefined;
      formData.value = {
        feedbackQuantity: undefined,
        qualifiedQuantity: undefined,
        unqualifiedQuantity: undefined,
        remark: '',
      };
      waitDuration.value = undefined;
      needReportQuantity.value = true;
      waitStartTime.value = undefined;
      // é¢„设任务时加载其工艺参数与等待配置;否则等待选择任务后再加载
      if (props.taskId) {
        loadParamDetails(props.taskId);
        getTask(props.taskId)
          .then(applyTaskWaitConfig)
          .catch(() => {});
      } else {
        paramDetails.value = [];
      }
    }
  },
});
@@ -48,11 +65,12 @@
const selectedTask = ref<MesProTaskApi.Task>();
const formData = ref({
  feedbackQuantity: undefined as number | undefined,
  qualifiedQuantity: undefined as number | undefined,
  unqualifiedQuantity: undefined as number | undefined,
  remark: '',
});
const submitting = ref(false);
const waitDuration = ref<number>();
const needReportQuantity = ref<boolean>(true);
const waitStartTime = ref<string>();
const hasPredefinedTask = computed(() => !!props.taskId);
@@ -70,6 +88,28 @@
function handleTaskChange(task: MesProTaskApi.Task | undefined) {
  selectedTask.value = task;
  selectedTaskId.value = task?.id;
  loadParamDetails(task?.id);
  applyTaskWaitConfig(task);
}
/** æŒ‰ä»»åŠ¡å·¥åºåŠ è½½ç­‰å¾…æ—¶é•¿ä¸ŽæŠ¥å·¥æ•°é‡è§„åˆ™ */
async function applyTaskWaitConfig(task?: MesProTaskApi.Task) {
  waitDuration.value = undefined;
  needReportQuantity.value = true;
  waitStartTime.value = task?.waitStartTime;
  if (!task?.routeId || !task?.processId) {
    return;
  }
  try {
    const routeProcess = await getRouteProcessByRouteAndProcess(
      task.routeId,
      task.processId,
    );
    waitDuration.value = routeProcess?.waitDuration;
    needReportQuantity.value = routeProcess?.needReportQuantity ?? true;
  } catch {
    // åŠ è½½å¤±è´¥æŒ‰é»˜è®¤å¤„ç†ï¼ˆæ— éœ€å€’è®¡æ—¶ã€æ•°é‡å¿…å¡«ï¼‰
  }
}
async function handleSubmit() {
@@ -78,7 +118,7 @@
    message.warning('请选择任务');
    return;
  }
  if (!formData.value.feedbackQuantity) {
  if (needReportQuantity.value && !formData.value.feedbackQuantity) {
    message.warning('请输入报工数量');
    return;
  }
@@ -86,9 +126,13 @@
  try {
    await quickCreateFeedback({
      taskId,
      feedbackQuantity: formData.value.feedbackQuantity,
      qualifiedQuantity: formData.value.qualifiedQuantity,
      unqualifiedQuantity: formData.value.unqualifiedQuantity,
      feedbackQuantity: needReportQuantity.value
        ? (formData.value.feedbackQuantity ?? 0)
        : 0,
      paramValues: paramDetails.value.map((detail) => ({
        detailId: detail.id,
        value: detail.paramValue,
      })),
      remark: formData.value.remark || undefined,
    });
    message.success('报工成功');
@@ -118,7 +162,14 @@
          class="!w-full"
        />
      </a-form-item>
      <a-form-item label="本次报工数量" required>
      <a-form-item v-if="waitDuration" label="报工倒计时">
        <WaitCountdown
          :task-id="selectedTaskId"
          :wait-duration="waitDuration"
          :wait-start-time="waitStartTime"
        />
      </a-form-item>
      <a-form-item v-if="needReportQuantity" label="本次报工数量" required>
        <a-input-number
          v-model:value="formData.feedbackQuantity"
          :min="0.01"
@@ -128,24 +179,27 @@
          class="!w-full"
        />
      </a-form-item>
      <a-form-item label="合格品数量">
        <a-input-number
          v-model:value="formData.qualifiedQuantity"
          :min="0"
          :precision="2"
          placeholder="请输入合格品数量"
          class="!w-full"
        />
      </a-form-item>
      <a-form-item label="不良品数量">
        <a-input-number
          v-model:value="formData.unqualifiedQuantity"
          :min="0"
          :precision="2"
          placeholder="请输入不良品数量"
          class="!w-full"
        />
      </a-form-item>
      <div v-if="paramDetails.length" class="mb-4">
        <div class="mb-1 font-medium">工艺参数</div>
        <div
          v-for="detail in paramDetails"
          :key="detail.id"
          class="mb-2 flex items-center gap-2"
        >
          <span class="w-40 flex-shrink-0 text-right text-sm text-gray-600">
            <span v-if="detail.required" class="text-red-500">*&nbsp;</span>
            {{ detail.paramName || detail.paramCode
            }}{{
              detail.unitMeasureName ? `(${detail.unitMeasureName})` : ''
            }}
          </span>
          <a-input
            v-model:value="detail.paramValue"
            placeholder="请输入参数值"
            class="!w-full"
          />
        </div>
      </div>
      <a-form-item label="备注">
        <a-textarea
          v-model:value="formData.remark"
src/views/mes/process-design/process/data.ts
@@ -77,6 +77,29 @@
      rules: z.number().default(CommonStatusEnum.ENABLE),
    },
    {
      fieldName: 'waitDuration',
      label: '等待时长(小时)',
      component: 'InputNumber',
      formItemClass: 'col-span-2',
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 2,
        placeholder: '留空或 0 è¡¨ç¤ºæ— éœ€å€’计时',
      },
    },
    {
      fieldName: 'needReportQuantity',
      label: '需填写报工数量',
      component: 'Switch',
      formItemClass: 'col-span-1',
      defaultValue: true,
      componentProps: {
        checkedChildren: '是',
        unCheckedChildren: '否',
      },
    },
    {
      fieldName: 'attention',
      label: '工序说明',
      component: 'Textarea',
src/views/mes/process-design/process/modules/form.vue
@@ -88,8 +88,11 @@
    modalApi.lock();
    try {
      formData.value = await getProcess(data.id);
      // è®¾ç½®åˆ° values
      await formApi.setValues(formData.value);
      // è®¾ç½®åˆ° values(旧数据未配置报工数量开关时按默认需要填写)
      await formApi.setValues({
        ...formData.value,
        needReportQuantity: formData.value.needReportQuantity ?? true,
      });
    } finally {
      modalApi.unlock();
    }
src/views/mes/process-design/route/data.ts
@@ -166,7 +166,10 @@
}
/** å·¥è‰ºè·¯çº¿å·¥åºæ˜Žç»†è¡¨å• */
export function useRouteProcessFormSchema(processOptions: Array<{ label: string; value: number }>): VbenFormSchema[] {
export function useRouteProcessFormSchema(
  processOptions: Array<{ label: string; value: number }>,
  onProcessChange?: (processId?: number) => void,
): VbenFormSchema[] {
  return [
    {
      fieldName: "id",
@@ -198,6 +201,7 @@
        options: processOptions,
        placeholder: "请选择工序",
        showSearch: true,
        onChange: onProcessChange,
      },
      rules: "selectRequired",
    },
@@ -279,12 +283,12 @@
    },
    {
      fieldName: "waitTime",
      label: "等待时间(分)",
      label: "等待时长(小时)",
      component: "InputNumber",
      componentProps: {
        class: "!w-full",
        min: 0,
        precision: 0,
        precision: 2,
      },
      rules: z.number().default(0),
    },
@@ -351,7 +355,7 @@
      width: 160,
    },
    { field: "prepareTime", title: "准备时间(分)", width: 110 },
    { field: "waitTime", title: "等待时间(分)", width: 110 },
    { field: "waitTime", title: "等待时长(小时)", width: 110 },
    {
      field: "colorCode",
      title: "甘特图颜色",
src/views/mes/process-design/route/modules/process-form.vue
@@ -9,7 +9,7 @@
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { getProcessSimpleList } from '#/api/mes/pro/process';
import { getProcess, getProcessSimpleList } from '#/api/mes/pro/process';
import {
  createRouteProcess,
  getRouteProcess,
@@ -39,6 +39,19 @@
  wrapperClass: 'grid-cols-2',
});
/** é€‰æ‹©å·¥åºåŽï¼Œè‡ªåŠ¨å¸¦å‡ºè¯¥å·¥åºé…ç½®çš„ç­‰å¾…æ—¶é•¿ï¼ˆç›´æŽ¥å¡«å°æ—¶æ•°ï¼Œä¸æ¢ç®—ï¼‰ */
async function handleProcessChange(processId?: number) {
  if (!processId) {
    return;
  }
  try {
    const process = await getProcess(processId);
    await formApi.setFieldValue('waitTime', process.waitDuration ?? 0);
  } catch {
    // å·¥åºè¯¦æƒ…获取失败时忽略反显
  }
}
/** åŠ è½½å·¥åºé€‰é¡¹åŽå†ç”Ÿæˆ schema(避免下拉空选项) */
async function loadSchema(): Promise<VbenFormSchema[]> {
  const list = await getProcessSimpleList();
@@ -46,7 +59,7 @@
    label: item.name ?? '',
    value: item.id!,
  }));
  return useRouteProcessFormSchema(options);
  return useRouteProcessFormSchema(options, handleProcessChange);
}
const [Modal, modalApi] = useVbenModal({
src/views/mes/workbench/components/tabs/FeedbackTab.vue
@@ -1,5 +1,5 @@
<script lang="ts" setup>
import type { PendingTask } from '../../../types';
import type { PendingTask } from '../../types';
import type { FormType } from '../../../pro/feedback/data';
import { computed, ref, watch } from 'vue';
@@ -12,7 +12,7 @@
} from '@vben/constants';
import { useUserStore } from '@vben/stores';
import { Button, message, Modal, Popconfirm } from 'ant-design-vue';
import { Button, Input, message, Modal, Popconfirm } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
@@ -26,12 +26,22 @@
  updateFeedback,
} from '#/api/mes/pro/feedback';
import { getRouteProcessByRouteAndProcess } from '#/api/mes/pro/route/process';
import { getTask } from '#/api/mes/pro/task';
import { $t } from '#/locales';
import { useFormSchema } from '../../../pro/feedback/data';
import {
  loadParamDetails,
  paramDetails,
  resetWaitConfig,
  waitDuration,
  waitStartTime,
  waitTaskId,
} from '../../../pro/feedback/param-state';
import BatchInputDialog from '../../../pro/feedback/modules/batch-input-dialog.vue';
import WaitCountdown from '../../../pro/feedback/modules/wait-countdown.vue';
const batchDialogRef = ref<InstanceType<typeof BatchInputDialog>>();
const batchActionType = ref<'submit' | 'approve'>('');
const batchActionType = ref<'' | 'submit' | 'approve'>('');
const pendingSubmitMode = ref<'saveAndSubmit' | 'submit'>('submit');
defineOptions({ name: 'WorkbenchFeedbackTab' });
@@ -73,36 +83,50 @@
  wrapperClass: 'grid-cols-3',
});
/** æ ¹æ® checkFlag å¯¹é½æ•°é‡ */
/** æäº¤å‰å¯¹é½æ•°é‡ï¼šæŠ¥å·¥åªå¡«å†™æŠ¥å·¥æ•°é‡ï¼Œåˆæ ¼/不良/废品数量由质检确定,报工环节不采集 */
function alignQuantity(data: any) {
  if (data.checkFlag) {
    data.uncheckQuantity = data.feedbackQuantity;
    data.qualifiedQuantity = 0;
    data.unqualifiedQuantity = 0;
    data.laborScrapQuantity = 0;
    data.materialScrapQuantity = 0;
    data.otherScrapQuantity = 0;
  } else {
    data.feedbackQuantity =
      (data.qualifiedQuantity || 0) + (data.unqualifiedQuantity || 0);
    data.uncheckQuantity = 0;
  if (data.needReportQuantity === false) {
    data.feedbackQuantity = 0;
  }
  data.uncheckQuantity = data.checkFlag ? data.feedbackQuantity : 0;
  delete data.qualifiedQuantity;
  delete data.unqualifiedQuantity;
  delete data.laborScrapQuantity;
  delete data.materialScrapQuantity;
  delete data.otherScrapQuantity;
  delete data.needReportQuantity;
}
async function getAlignedData() {
  const data = (await formApi.getValues()) as any;
  syncParamValues(data);
  alignQuantity(data);
  return data;
}
/** åŠ è½½ checkFlag */
async function resolveCheckFlag(routeId?: number, processId?: number) {
  if (!routeId || !processId) return true;
/** åŒæ­¥å·¥è‰ºå‚数值到报工数据(与提交数据一同发送给后端) */
function syncParamValues(data: any) {
  data.paramValues = paramDetails.value.map((detail) => ({
    detailId: detail.id,
    value: detail.paramValue,
  }));
}
/** åŠ è½½å·¥åºé…ç½®ï¼ˆcheckFlag / ç­‰å¾…æ—¶é•¿ / æŠ¥å·¥æ•°é‡è§„则) */
async function resolveRouteProcessConfig(routeId?: number, processId?: number) {
  if (!routeId || !processId) {
    return { checkFlag: true, needReportQuantity: true };
  }
  try {
    const rp = await getRouteProcessByRouteAndProcess(routeId, processId);
    return rp?.checkFlag ?? false;
    waitDuration.value = rp?.waitDuration;
    return {
      checkFlag: rp?.checkFlag ?? false,
      needReportQuantity: rp?.needReportQuantity ?? true,
    };
  } catch {
    return true;
    waitDuration.value = undefined;
    return { checkFlag: true, needReportQuantity: true };
  }
}
@@ -112,11 +136,17 @@
  feedbackId.value = undefined;
  feedbackStatus.value = undefined;
  originalSnapshot.value = '';
  resetWaitConfig();
  formApi.setState({ schema: useFormSchema(formType.value, formApi) });
  const code = await generateAutoCode(MesAutoCodeRuleCode.PRO_FEEDBACK_CODE);
  const checkFlag = await resolveCheckFlag(task.routeId, task.processId);
  const config = await resolveRouteProcessConfig(task.routeId, task.processId);
  // åŠ è½½ä»»åŠ¡ç»‘å®šçš„å·¥è‰ºå‚æ•°ï¼ˆå¿…å¡«å‚æ•°æ ¡éªŒ + å‚数值回显)
  await loadParamDetails(task.id);
  waitTaskId.value = task.id;
  waitStartTime.value = task.waitStartTime;
  await formApi.setValues({
    code,
@@ -133,9 +163,11 @@
    itemName: task.itemName,
    itemSpecification: task.itemSpecification,
    unitMeasureName: task.unitMeasureName,
    reportableQuantity: task.reportableQuantity,
    scheduledQuantity: task.quantity,
    feedbackApprove: task.feedbackApprove,
    checkFlag,
    checkFlag: config.checkFlag,
    needReportQuantity: config.needReportQuantity,
  });
  originalSnapshot.value = JSON.stringify(await getAlignedData());
}
@@ -210,7 +242,7 @@
  pendingSubmitMode.value = 'saveAndSubmit';
  // 4. è°ƒç”¨æäº¤æŽ¥å£
  const isDone = await handleOnSubmit(feedbackId.value!,props.task.feedbackApprove);
  const isDone = await handleOnSubmit(feedbackId.value!,props.task?.feedbackApprove);
  if (!isDone) return;
  feedbackId.value = record.id;
@@ -242,7 +274,7 @@
  }
  pendingSubmitMode.value = 'submit';
  const isDone = await handleOnSubmit(feedbackId.value!,props.task.feedbackApprove);
  const isDone = await handleOnSubmit(feedbackId.value!,props.task?.feedbackApprove);
  if (!isDone) return;
  message.success('报工单已提交');
@@ -327,14 +359,30 @@
  feedbackStatus.value = data.status;
  formType.value = 'update';
  formApi.setState({ schema: useFormSchema(formType.value, formApi) });
  const checkFlag = await resolveCheckFlag(data.routeId, data.processId);
  await formApi.setValues({ ...data, checkFlag });
  const config = await resolveRouteProcessConfig(data.routeId, data.processId);
  await loadParamDetails(data.taskId);
  waitTaskId.value = data.taskId;
  waitStartTime.value = undefined;
  if (data.taskId) {
    try {
      const task = await getTask(data.taskId);
      waitStartTime.value = task?.waitStartTime;
    } catch {
      // ä»»åŠ¡ä¸å­˜åœ¨æ—¶å¿½ç•¥å€’è®¡æ—¶å›žæ˜¾
    }
  }
  await formApi.setValues({
    ...data,
    checkFlag: config.checkFlag,
    needReportQuantity: config.needReportQuantity,
  });
  originalSnapshot.value = JSON.stringify(await getAlignedData());
}
// ç›‘听任务变更(仅当任务 ID çœŸæ­£å˜åŒ–时才重置表单)
// ç›‘听任务变更:任务对象变化(选择任务 / åˆ—表刷新后同步的最新数据)时重置表单,
// ç¡®ä¿"可报工数量"等任务派生字段始终展示最新值
watch(
  () => props.task?.id,
  () => props.task,
  () => {
    if (props.task) {
      resetForTask(props.task);
@@ -364,6 +412,45 @@
      </div>
      <Form class="feedback-tab__form" />
      <!-- æŠ¥å·¥å€’计时(工序配置等待时长时展示) -->
      <div v-if="waitDuration" class="feedback-tab__params">
        <div class="feedback-tab__params-title">报工倒计时</div>
        <WaitCountdown
          :task-id="waitTaskId"
          :wait-duration="waitDuration"
          :wait-start-time="waitStartTime"
          :disabled="!isEditable"
        />
      </div>
      <!-- å·¥è‰ºå‚数(任务绑定参数时展示,必填参数未填会被后端拦截) -->
      <div v-if="paramDetails.length" class="feedback-tab__params">
        <div class="feedback-tab__params-title">工艺参数</div>
        <div class="feedback-tab__params-grid">
          <div
            v-for="detail in paramDetails"
            :key="detail.id"
            class="feedback-tab__params-item"
          >
            <span class="feedback-tab__params-label">
              <span v-if="detail.required" class="feedback-tab__params-required"
                >*&nbsp;</span
              >
              {{ detail.paramName || detail.paramCode
              }}{{
                detail.unitMeasureName ? `(${detail.unitMeasureName})` : ''
              }}
            </span>
            <Input
              v-model:value="detail.paramValue"
              :disabled="!isEditable"
              placeholder="请输入参数值"
              class="feedback-tab__params-input"
            />
          </div>
        </div>
      </div>
      <div class="feedback-tab__actions">
        <Button
@@ -456,4 +543,48 @@
  border-top: 1px solid #f0f0f0;
  margin-top: 16px;
}
.feedback-tab__params {
  margin-top: 16px;
  padding: 14px 16px;
  border: 1px solid #f0f0f0;
  border-radius: 8px;
  background: #fafbfc;
}
.feedback-tab__params-title {
  margin-bottom: 12px;
  font-size: 15px;
  font-weight: 600;
  color: #1a1a1a;
}
.feedback-tab__params-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px 20px;
}
.feedback-tab__params-item {
  display: flex;
  align-items: center;
  gap: 10px;
}
.feedback-tab__params-label {
  flex-shrink: 0;
  width: 176px;
  text-align: right;
  font-size: 14px;
  color: #595959;
}
.feedback-tab__params-required {
  color: #ff4d4f;
}
.feedback-tab__params-input {
  flex: 1;
  min-width: 0;
}
</style>
src/views/mes/workbench/components/tabs/TaskDetailTab.vue
@@ -1,5 +1,5 @@
<script lang="ts" setup>
import type { PendingTask } from '../../../types';
import type { PendingTask } from '../../types';
import { Descriptions, Progress, Tag, Timeline, TimelineItem } from 'ant-design-vue';
src/views/wls/approval/config/data.ts
@@ -62,8 +62,8 @@
            checked: config?.approvalEnabled ?? false,
            checkedChildren: '开',
            unCheckedChildren: '关',
            onChange: (checked: boolean) => {
              handleChange(row, 'INBOUND', checked);
            onChange: (checked) => {
              handleChange(row, 'INBOUND', Boolean(checked));
            },
          });
        },
@@ -82,8 +82,8 @@
            checked: config?.approvalEnabled ?? false,
            checkedChildren: '开',
            unCheckedChildren: '关',
            onChange: (checked: boolean) => {
              handleChange(row, 'OUTBOUND', checked);
            onChange: (checked) => {
              handleChange(row, 'OUTBOUND', Boolean(checked));
            },
          });
        },
@@ -102,8 +102,8 @@
            checked: config?.approvalEnabled ?? false,
            checkedChildren: '开',
            unCheckedChildren: '关',
            onChange: (checked: boolean) => {
              handleChange(row, 'TRANSFER', checked);
            onChange: (checked) => {
              handleChange(row, 'TRANSFER', Boolean(checked));
            },
          });
        },
@@ -122,8 +122,8 @@
            checked: config?.approvalEnabled ?? false,
            checkedChildren: '开',
            unCheckedChildren: '关',
            onChange: (checked: boolean) => {
              handleChange(row, 'CHECK', checked);
            onChange: (checked) => {
              handleChange(row, 'CHECK', Boolean(checked));
            },
          });
        },
@@ -173,7 +173,9 @@
}
/** æµç¨‹é…ç½®è¡¨å• */
export function useProcessFormSchema(): VbenFormSchema[] {
export function useProcessFormSchema(
  onBizTypeChange?: (value?: string) => void,
): VbenFormSchema[] {
  return [
    {
      fieldName: 'warehouseId',
@@ -190,6 +192,7 @@
      componentProps: {
        options: BIZ_TYPE_OPTIONS,
        placeholder: '请选择业务类型',
        onChange: onBizTypeChange,
      },
      rules: 'required',
    },
src/views/wls/approval/config/index.vue
@@ -2,7 +2,7 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { WmsApprovalApi } from '#/api/mes/wm/approval';
import { ref } from 'vue';
import { ref, unref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
@@ -69,7 +69,7 @@
    columns: useGridColumns(handleApprovalChange),
    height: 'auto',
    keepSource: true,
    data: warehouseConfigList,
    data: unref(warehouseConfigList),
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) => {
src/views/wls/approval/config/modules/process-config-form.vue
@@ -1,7 +1,7 @@
<script lang="ts" setup>
import type { WmsApprovalApi } from '#/api/mes/wm/approval';
import { computed, ref, watch } from 'vue';
import { ref, watch } from 'vue';
import { useVbenModal } from '@vben/common-ui';
@@ -25,7 +25,9 @@
    },
  },
  layout: 'horizontal',
  schema: useProcessFormSchema(),
  schema: useProcessFormSchema((value) => {
    currentBizType.value = value;
  }),
  showDefaultActions: false,
});
@@ -53,6 +55,7 @@
  },
  onOpenChange(isOpen) {
    if (isOpen) {
      currentBizType.value = undefined;
      const data = modalApi.getData() as {
        warehouseId: number;
        warehouseName: string;
@@ -69,11 +72,8 @@
  },
});
/** å½“前选中的业务类型 */
const currentBizType = computed(() => {
  const values = formApi.getValues();
  return values.bizType;
});
/** å½“前选中的业务类型(formApi.getValues ä¸ºå¼‚步,改由字段 onChange åŒæ­¥ï¼‰ */
const currentBizType = ref<string>();
/** ç›‘听业务类型变化,加载对应的流程列表 */
watch(currentBizType, async (bizType) => {
src/views/wls/arrivalnotice/modules/po-select-dialog.vue
@@ -105,7 +105,7 @@
      message.warning('请选择采购订单');
      return;
    }
    emit('selected', selectedRows.id);
    emit('selected', selectedRows.id!);
    await modalApi.close();
  },
  async onOpened() {
src/views/wls/itemreceipt/modules/an-select-dialog.vue
@@ -111,7 +111,7 @@
      message.warning('请选择到货通知单');
      return;
    }
    emit('selected', selectedRows.id);
    emit('selected', selectedRows.id!);
    await modalApi.close();
  },
  async onOpenChange(isOpen: boolean) {
src/views/wls/materialstock/components/stock-adjust-modal.vue
@@ -55,15 +55,15 @@
  if (!enabled) {
    formApi.updateSchema([
      { fieldName: 'batchCode', dependencies: { show: () => false } },
      { fieldName: 'manualBatchEntry', dependencies: { show: () => false } },
      { fieldName: 'produceDate', dependencies: { show: () => false } },
      { fieldName: 'expireDate', dependencies: { show: () => false } },
      { fieldName: 'receiptDate', dependencies: { show: () => false } },
      { fieldName: 'vendorId', dependencies: { show: () => true } },
      { fieldName: 'purchaseOrderCode', dependencies: { show: () => false } },
      { fieldName: 'lotNumber', dependencies: { show: () => false } },
      { fieldName: 'qualityStatus', dependencies: { show: () => false } },
      { fieldName: 'batchCode', dependencies: { show: () => false, triggerFields: [''] } },
      { fieldName: 'manualBatchEntry', dependencies: { show: () => false, triggerFields: [''] } },
      { fieldName: 'produceDate', dependencies: { show: () => false, triggerFields: [''] } },
      { fieldName: 'expireDate', dependencies: { show: () => false, triggerFields: [''] } },
      { fieldName: 'receiptDate', dependencies: { show: () => false, triggerFields: [''] } },
      { fieldName: 'vendorId', dependencies: { show: () => true, triggerFields: [''] } },
      { fieldName: 'purchaseOrderCode', dependencies: { show: () => false, triggerFields: [''] } },
      { fieldName: 'lotNumber', dependencies: { show: () => false, triggerFields: [''] } },
      { fieldName: 'qualityStatus', dependencies: { show: () => false, triggerFields: [''] } },
    ]);
    return;
  }
@@ -72,15 +72,15 @@
  const isIn = operateType.value === 1;
  formApi.updateSchema([
    { fieldName: 'batchCode', dependencies: { show: () => manualBatchEntry.value } },
    { fieldName: 'manualBatchEntry', dependencies: { show: () => true } },
    { fieldName: 'produceDate', dependencies: { show: () => isIn && !!item?.produceDateFlag } },
    { fieldName: 'expireDate', dependencies: { show: () => isIn && !!item?.expireDateFlag } },
    { fieldName: 'receiptDate', dependencies: { show: () => isIn && !!item?.receiptDateFlag } },
    { fieldName: 'vendorId', dependencies: { show: () => !isIn || !!item?.vendorFlag } },
    { fieldName: 'purchaseOrderCode', dependencies: { show: () => isIn && !!item?.purchaseOrderCodeFlag } },
    { fieldName: 'lotNumber', dependencies: { show: () => isIn && !!item?.lotNumberFlag } },
    { fieldName: 'qualityStatus', dependencies: { show: () => isIn && !!item?.qualityStatusFlag } },
    { fieldName: 'batchCode', dependencies: { show: () => manualBatchEntry.value, triggerFields: [''] } },
    { fieldName: 'manualBatchEntry', dependencies: { show: () => true, triggerFields: [''] } },
    { fieldName: 'produceDate', dependencies: { show: () => isIn && !!item?.produceDateFlag, triggerFields: [''] } },
    { fieldName: 'expireDate', dependencies: { show: () => isIn && !!item?.expireDateFlag, triggerFields: [''] } },
    { fieldName: 'receiptDate', dependencies: { show: () => isIn && !!item?.receiptDateFlag, triggerFields: [''] } },
    { fieldName: 'vendorId', dependencies: { show: () => !isIn || !!item?.vendorFlag, triggerFields: [''] } },
    { fieldName: 'purchaseOrderCode', dependencies: { show: () => isIn && !!item?.purchaseOrderCodeFlag, triggerFields: [''] } },
    { fieldName: 'lotNumber', dependencies: { show: () => isIn && !!item?.lotNumberFlag, triggerFields: [''] } },
    { fieldName: 'qualityStatus', dependencies: { show: () => isIn && !!item?.qualityStatusFlag, triggerFields: [''] } },
  ]);
}
@@ -127,6 +127,7 @@
async function handleOperateTypeChange(type: 1 | 2) {
  operateType.value = type;
  modalTitle.value = type === 1 ? '库存入库' : '库存出库';
  modalApi.setState({ title: modalTitle.value });
  await formApi.setFieldValue('operateType', type);
  // æ›´æ–°æ‰¹æ¬¡å­—段显示
  await updateBatchFields();
@@ -403,20 +404,21 @@
      batchEnabled.value = false;
      operateType.value = 1;
      modalTitle.value = '库存调整';
      modalApi.setState({ title: modalTitle.value });
      return;
    }
    // é‡ç½®è¡¨å•
    await formApi.resetForm();
    // é»˜è®¤éšè—æ‰¹æ¬¡ç›¸å…³å­—段
    formApi.updateSchema([
      { fieldName: 'batchCode', dependencies: { show: () => false } },
      { fieldName: 'manualBatchEntry', dependencies: { show: () => false } },
      { fieldName: 'produceDate', dependencies: { show: () => false } },
      { fieldName: 'expireDate', dependencies: { show: () => false } },
      { fieldName: 'receiptDate', dependencies: { show: () => false } },
      { fieldName: 'purchaseOrderCode', dependencies: { show: () => false } },
      { fieldName: 'lotNumber', dependencies: { show: () => false } },
      { fieldName: 'qualityStatus', dependencies: { show: () => false } },
      { fieldName: 'batchCode', dependencies: { show: () => false, triggerFields: [''] } },
      { fieldName: 'manualBatchEntry', dependencies: { show: () => false, triggerFields: [''] } },
      { fieldName: 'produceDate', dependencies: { show: () => false, triggerFields: [''] } },
      { fieldName: 'expireDate', dependencies: { show: () => false, triggerFields: [''] } },
      { fieldName: 'receiptDate', dependencies: { show: () => false, triggerFields: [''] } },
      { fieldName: 'purchaseOrderCode', dependencies: { show: () => false, triggerFields: [''] } },
      { fieldName: 'lotNumber', dependencies: { show: () => false, triggerFields: [''] } },
      { fieldName: 'qualityStatus', dependencies: { show: () => false, triggerFields: [''] } },
    ]);
    // é‡ç½®æ‰‹åŠ¨å¡«å†™æ‰¹æ¬¡å·çŠ¶æ€
    manualBatchEntry.value = false;
@@ -436,7 +438,7 @@
      }
    }
  },
  title: modalTitle,
  title: '库存调整',
});
/** æäº¤è¡¨å• */
src/views/wls/productreceipt/data.ts
@@ -192,6 +192,12 @@
      },
    },
    {
      field: 'feedbackId',
      title: '来源',
      width: 100,
      slots: { default: 'source' },
    },
    {
      title: '操作',
      width: 240,
      fixed: 'right',
@@ -237,10 +243,29 @@
      width: 100,
    },
    {
      field: 'qualityStatus',
      title: '质量状态',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_WM_QUALITY_STATUS },
      },
    },
    {
      field: 'batchCode',
      title: '批次号',
      minWidth: 120,
    },
    ...(stockable
      ? [
          {
            field: 'isStock',
            title: '是否入库',
            width: 100,
            slots: { default: 'isStock' },
          } as const,
        ]
      : []),
    ...(editable || stockable
      ? [
          {
src/views/wls/productreceipt/index.vue
@@ -6,7 +6,7 @@
import { MesWmProductReceiptStatusEnum } from '@vben/constants';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message } from 'ant-design-vue';
import { Button, message, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
@@ -143,6 +143,9 @@
          {{ row.code }}
        </Button>
      </template>
      <template #source="{ row }">
        <Tag v-if="row.feedbackId" color="blue">自动生成</Tag>
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
src/views/wls/productreceipt/modules/form.vue
@@ -219,7 +219,11 @@
    <template v-if="formData?.id">
      <Divider>物料信息</Divider>
      <div class="mx-4">
        <LineList :form-type="formType" :receipt-id="formData.id" />
        <LineList
          :form-type="formType"
          :receipt-id="formData.id"
          :auto-generated="!!formData.feedbackId"
        />
      </div>
    </template>
    <template #prepend-footer>
src/views/wls/productreceipt/modules/line-list.vue
@@ -2,6 +2,7 @@
import type { FormType } from '../data';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmProductReceiptApi } from '#/api/mes/wm/productreceipt';
import type { MesWmProductReceiptDetailApi } from '#/api/mes/wm/productreceipt/detail';
import type { MesWmProductReceiptLineApi } from '#/api/mes/wm/productreceipt/line';
@@ -10,9 +11,10 @@
import { useVbenModal } from '@vben/common-ui';
import { BarcodeBizTypeEnum } from '@vben/constants';
import { message } from 'ant-design-vue';
import { Button, Checkbox, message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { submitProductReceiptStock } from '#/api/mes/wm/productreceipt';
import { getProductReceiptDetailListByLineId } from '#/api/mes/wm/productreceipt/detail';
import {
  deleteProductReceiptLine,
@@ -29,16 +31,22 @@
const props = defineProps<{
  formType: FormType;
  receiptId: number;
  autoGenerated?: boolean; // æ˜¯å¦ç”±æŠ¥å·¥è‡ªåŠ¨ç”Ÿæˆï¼ˆè‡ªåŠ¨ç”Ÿæˆçš„è¡Œä¸å¯æ‰‹å·¥å¢žåˆ æ”¹ï¼‰
}>();
const isEditable = computed(() =>
  // æ˜¯å¦å¯ç¼–辑明细行
  ['create', 'update'].includes(props.formType),
);
const canEditLine = computed(
  // è‡ªåŠ¨ç”Ÿæˆçš„å…¥åº“å•è¡Œä¸å¯æ‰‹å·¥å¢žåˆ æ”¹
  () => isEditable.value && !props.autoGenerated,
);
const isStock = computed(() => props.formType === 'stock'); // æ˜¯å¦ä¸ºä¸Šæž¶æ¨¡å¼
const detailMap = reactive<
  Record<number, MesWmProductReceiptDetailApi.ProductReceiptDetail[]>
>({}); // å·²å±•开行的上架明细缓存
const checkedMap = reactive<Record<number, boolean>>({}); // è¡Œæ˜¯å¦å…¥åº“勾选状态缓存
const barcodeDetailRef = ref(); // æ¡ç è¯¦æƒ…弹窗实例
const [LineFormModal, lineFormModalApi] = useVbenModal({
@@ -55,6 +63,9 @@
function handleRefresh() {
  for (const id of Object.keys(detailMap)) {
    delete detailMap[Number(id)];
  }
  for (const id of Object.keys(checkedMap)) {
    delete checkedMap[Number(id)];
  }
  gridApi.query();
}
@@ -99,6 +110,80 @@
    row.itemCode,
    row.itemName,
  );
}
/** è¡Œæ˜¯å¦å…¥åº“勾选状态(未手动勾选时回退到行自带值) */
function isLineStock(row: MesWmProductReceiptLineApi.ProductReceiptLine) {
  return checkedMap[row.id!] ?? row.isStock ?? false;
}
/** åˆ‡æ¢è¡Œæ˜¯å¦å…¥åº“ */
function setLineStock(
  row: MesWmProductReceiptLineApi.ProductReceiptLine,
  checked: boolean,
) {
  checkedMap[row.id!] = checked;
}
/** ä¿å­˜ä¸Šæž¶åˆ†é…ï¼šæ”¶é›†æ‰€æœ‰è¡Œçš„勾选状态与仓库/库区/库位分配,调用批量上架接口 */
async function handleSaveStock() {
  if (!props.receiptId) {
    return;
  }
  const hideLoading = message.loading({
    content: '正在保存上架分配...',
    duration: 0,
  });
  try {
    const { list } = await getProductReceiptLinePage({
      pageNo: 1,
      pageSize: 10000,
      receiptId: props.receiptId,
    });
    const rows: MesWmProductReceiptApi.StockRow[] = [];
    for (const line of list) {
      const isStock = isLineStock(line);
      const row: MesWmProductReceiptApi.StockRow = {
        lineId: line.id!,
        isStock,
      };
      if (isStock) {
        let details = detailMap[line.id!];
        if (!details) {
          details = await getProductReceiptDetailListByLineId(line.id!);
          detailMap[line.id!] = details;
        }
        // å‹¾é€‰å…¥åº“的行必须已有上架明细,且明细数量之和等于行收货数量
        if (!details.length) {
          message.error(`物料「${line.itemName}」已勾选入库,请先添加上架明细`);
          return;
        }
        const lineQty = Number(line.quantity ?? 0);
        const sumQty = details.reduce(
          (acc, detail) => acc + Number(detail.quantity ?? 0),
          0,
        );
        if (lineQty !== sumQty) {
          message.error(
            `物料「${line.itemName}」上架明细数量之和(${sumQty})与收货数量(${lineQty})不一致`,
          );
          return;
        }
        row.details = details.map((detail) => ({
          warehouseId: detail.warehouseId!,
          locationId: detail.locationId!,
          areaId: detail.areaId!,
          quantity: detail.quantity!,
        }));
      }
      rows.push(row);
    }
    await submitProductReceiptStock({ receiptId: props.receiptId, rows });
    message.success('上架分配保存成功');
    handleRefresh();
  } finally {
    hideLoading();
  }
}
/** æ‰“开上架明细表单 */
@@ -185,7 +270,7 @@
    <LineFormModal @success="handleRefresh" />
    <DetailFormModal @success="handleDetailSuccess" />
    <Grid table-title="物料信息">
      <template v-if="isEditable" #toolbar-tools>
      <template v-if="canEditLine" #toolbar-tools>
        <TableAction
          :actions="[
            {
@@ -197,6 +282,9 @@
          ]"
        />
      </template>
      <template v-if="isStock" #toolbar-tools>
        <Button type="primary" @click="handleSaveStock">保存上架分配</Button>
      </template>
      <template #detail="{ row }">
        <DetailList
          :details="getExpandedDetails(row)"
@@ -204,6 +292,14 @@
          @edit="(detailId) => openDetailForm(row.id!, row.itemId, detailId)"
          @refresh="loadLineDetails(row.id!)"
        />
      </template>
      <template #isStock="{ row }">
        <div class="flex justify-center">
          <Checkbox
            :checked="isLineStock(row)"
            @update:checked="(checked: boolean) => setLineStock(row, checked)"
          />
        </div>
      </template>
      <template #actions="{ row }">
        <div class="flex items-center justify-center">
@@ -213,7 +309,7 @@
                label: $t('common.edit'),
                type: 'link',
                icon: ACTION_ICON.EDIT,
                ifShow: isEditable,
                ifShow: canEditLine,
                onClick: handleEdit.bind(null, row),
              },
              {
@@ -221,7 +317,7 @@
                type: 'link',
                danger: true,
                icon: ACTION_ICON.DELETE,
                ifShow: isEditable,
                ifShow: canEditLine,
                popConfirm: {
                  title: $t('ui.actionMessage.deleteConfirm', [row.itemName]),
                  confirm: handleDelete.bind(null, row),
src/views/wls/productsales/data.ts
@@ -132,7 +132,7 @@
            contactAddress: notice?.recipientAddress,
            contactName: notice?.recipientName,
            contactTelephone: notice?.recipientTelephone,
            salesOrderCode: notice?.salesOrderCode,
            salesOrderCode: notice?.saleOrderCode,
          });
        },
        status: MesWmSalesNoticeStatusEnum.APPROVED,
src/views/wls/returnsales/modules/form.vue
@@ -191,16 +191,19 @@
      try {
        formData.value = await getReturnSales(data.id);
        // é€šè¿‡é”€å”®è®¢å•号反查订单ID,确保 ErpSaleOrderSelect èƒ½æ­£ç¡®å›žæ˜¾
        if (formData.value?.salesOrderCode && !formData.value.saleOrderId) {
        const formValues = formData.value as MesWmReturnSalesApi.ReturnSales & {
          saleOrderId?: number;
        };
        if (formValues.salesOrderCode && !formValues.saleOrderId) {
          const orderPage = await getSaleOrderPage({
            pageNo: 1,
            pageSize: 1,
            no: formData.value.salesOrderCode,
          } as Record<string, unknown> as never);
          const list = (orderPage as Record<string, unknown>)?.list as Array<Record<string, unknown>> | undefined;
          if (list?.length) {
            const orderId = list[0].id as number;
            (formData.value as Record<string, unknown>).saleOrderId = orderId;
            no: formValues.salesOrderCode,
          } as never);
          const first = orderPage?.list?.[0];
          if (first) {
            const orderId = Number(first.id);
            formValues.saleOrderId = orderId;
            const order = await getSaleOrder(orderId);
            saleOrderItems.value = order?.items || [];
          }
src/views/wls/returnsales/modules/line-form.vue
@@ -98,9 +98,9 @@
                pageNo: 1,
                pageSize: 1,
              });
              const batchList = batchPage?.list;
              if (batchList?.length) {
                await formApi.setFieldValue('batchId', batchList[0].id);
              const firstBatch = batchPage?.list?.[0];
              if (firstBatch) {
                await formApi.setFieldValue('batchId', firstBatch.id);
              }
            } catch {
              // æŸ¥è¯¢å¤±è´¥æ—¶å¿½ç•¥