fix: 新疆-绿能
1.报工等待时长倒计时 + 报工数量可选
2.报工自动生成产品入库单 + 勾选入库 + 不合格仓库兜底
已添加1个文件
已修改20个文件
637 ■■■■■ 文件已修改
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 8 ●●●●● 补丁 | 查看 | 原始文档 | 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 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/feedback/modules/form.vue 73 ●●●● 补丁 | 查看 | 原始文档 | 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/workorder-center/modules/quick-feedback.vue 50 ●●●●● 补丁 | 查看 | 原始文档 | 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 65 ●●●● 补丁 | 查看 | 原始文档 | 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 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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
@@ -59,6 +59,7 @@
    reportableQuantity?: number; // å¯æŠ¥å·¥æ•°é‡
    assigneeUserIds?: number[]; // æŠ¥å·¥äºº
    feedbackApprove?: boolean; // æ˜¯å¦æŠ¥å·¥å®¡æ‰¹ï¼ˆæ´¾ç”Ÿè‡ªå·¥å•)
    waitStartTime?: string; // æŠ¥å·¥å€’计时开始时间(yyyy-MM-dd HH:mm:ss)
  }
  /** MES ç”Ÿäº§ä»»åŠ¡åˆ†é¡µæŸ¥è¯¢å‚æ•° */
@@ -209,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';
@@ -195,6 +201,12 @@
      defaultValue: true,
    },
    {
      fieldName: 'needReportQuantity',
      component: 'Input',
      dependencies: { triggerFields: [''], show: () => false },
      defaultValue: true,
    },
    {
      fieldName: 'routeId',
      component: 'Input',
      dependencies: { triggerFields: [''], show: () => false },
@@ -255,8 +267,10 @@
        status: MesProWorkOrderStatusEnum.CONFIRMED,
        // è®¢å•变更:清空任务及任务带出的产品信息、数量区域控制位
        onChange: async () => {
          resetWaitConfig();
          await formApi?.setValues({
            checkFlag: true,
            needReportQuantity: true,
            itemCode: undefined,
            itemId: undefined,
            itemName: undefined,
@@ -291,6 +305,8 @@
        onChange: async (task?: MesProTaskApi.Task) => {
          await loadParamDetails(task?.id);
          if (!task) {
            resetWaitConfig();
            await formApi?.setFieldValue('needReportQuantity', true);
            return;
          }
          await formApi?.setValues({
@@ -304,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(
@@ -315,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);
          }
        },
      },
@@ -405,7 +433,12 @@
        precision: 2,
        placeholder: '请输入报工数量',
      },
      rules: 'required',
      dependencies: {
        triggerFields: ['needReportQuantity'],
        show: (values) => values.needReportQuantity !== false,
        rules: (values) =>
          values.needReportQuantity === false ? null : 'required',
      },
    },
    {
      fieldName: 'feedbackUserId',
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');
@@ -88,7 +98,13 @@
});
/** æäº¤å‰å¯¹é½æ•°é‡ï¼šæŠ¥å·¥åªå¡«å†™æŠ¥å·¥æ•°é‡ï¼Œåˆæ ¼/不良/废品数量由质检确定,报工环节不采集 */
function alignQuantity(data: MesProFeedbackApi.Feedback) {
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;
@@ -99,9 +115,12 @@
/** è¯»å–并按提交规则对齐后的表单数据,用于快照和保存 */
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;
}
@@ -292,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 };
  }
}
@@ -322,6 +346,7 @@
      subTabsName.value = 'itemConsume';
      originalSnapshot.value = '';
      paramDetails.value = [];
      resetWaitConfig();
      return;
    }
    // åŠ è½½æ•°æ®
@@ -357,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 {
@@ -375,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/workorder-center/modules/quick-feedback.vue
@@ -9,10 +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' });
@@ -41,9 +44,15 @@
        feedbackQuantity: 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 = [];
      }
@@ -59,6 +68,9 @@
  remark: '',
});
const submitting = ref(false);
const waitDuration = ref<number>();
const needReportQuantity = ref<boolean>(true);
const waitStartTime = ref<string>();
const hasPredefinedTask = computed(() => !!props.taskId);
@@ -77,6 +89,27 @@
  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() {
@@ -85,7 +118,7 @@
    message.warning('请选择任务');
    return;
  }
  if (!formData.value.feedbackQuantity) {
  if (needReportQuantity.value && !formData.value.feedbackQuantity) {
    message.warning('请输入报工数量');
    return;
  }
@@ -93,7 +126,9 @@
  try {
    await quickCreateFeedback({
      taskId,
      feedbackQuantity: formData.value.feedbackQuantity,
      feedbackQuantity: needReportQuantity.value
        ? (formData.value.feedbackQuantity ?? 0)
        : 0,
      paramValues: paramDetails.value.map((detail) => ({
        detailId: detail.id,
        value: detail.paramValue,
@@ -127,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"
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
@@ -26,14 +26,20 @@
  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 pendingSubmitMode = ref<'saveAndSubmit' | 'submit'>('submit');
@@ -79,12 +85,16 @@
/** æäº¤å‰å¯¹é½æ•°é‡ï¼šæŠ¥å·¥åªå¡«å†™æŠ¥å·¥æ•°é‡ï¼Œåˆæ ¼/不良/废品数量由质检确定,报工环节不采集 */
function alignQuantity(data: any) {
  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() {
@@ -102,14 +112,21 @@
  }));
}
/** åŠ è½½ checkFlag */
async function resolveCheckFlag(routeId?: number, processId?: number) {
  if (!routeId || !processId) return true;
/** åŠ è½½å·¥åºé…ç½®ï¼ˆ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 };
  }
}
@@ -119,13 +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,
@@ -145,7 +166,8 @@
    reportableQuantity: task.reportableQuantity,
    scheduledQuantity: task.quantity,
    feedbackApprove: task.feedbackApprove,
    checkFlag,
    checkFlag: config.checkFlag,
    needReportQuantity: config.needReportQuantity,
  });
  originalSnapshot.value = JSON.stringify(await getAlignedData());
}
@@ -337,9 +359,23 @@
  feedbackStatus.value = data.status;
  formType.value = 'update';
  formApi.setState({ schema: useFormSchema(formType.value, formApi) });
  const checkFlag = await resolveCheckFlag(data.routeId, data.processId);
  const config = await resolveRouteProcessConfig(data.routeId, data.processId);
  await loadParamDetails(data.taskId);
  await formApi.setValues({ ...data, checkFlag });
  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());
}
@@ -377,6 +413,17 @@
      <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>
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,64 @@
    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;
        }
        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 +254,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 +266,9 @@
          ]"
        />
      </template>
      <template v-if="isStock" #toolbar-tools>
        <Button type="primary" @click="handleSaveStock">保存上架分配</Button>
      </template>
      <template #detail="{ row }">
        <DetailList
          :details="getExpandedDetails(row)"
@@ -204,6 +276,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 +293,7 @@
                label: $t('common.edit'),
                type: 'link',
                icon: ACTION_ICON.EDIT,
                ifShow: isEditable,
                ifShow: canEditLine,
                onClick: handleEdit.bind(null, row),
              },
              {
@@ -221,7 +301,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),