云
16 小时以前 f984f80ff5d6c36af7860a17d762fe92731e28bd
feat(feedback): 优化报工审批流程并增加报工人校验

- 增加报工人身份校验,防止非指定人员提交报工
- 引入行锁机制解决报工审批与驳回时的并发问题
- 优化检验标识获取逻辑,优先使用工单工序快照
- 完善联产出物料检验逻辑,独立检验且不计入主任务进度
- 修复物料更新时因未回传来源标识导致的名称唯一性误判
- 修复设备绑定工作站查询时因多条记录引发的异常
- 补充生产、质检及仓储等模块的详细业务错误码
- 更新报工服务单元测试并补充相关依赖的 Mock 配置
已添加1个文件
已修改29个文件
718 ■■■■■ 文件已修改
src/api/mes/pro/route/process/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pro/task/index.ts 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/qc/pendinginspect/index.ts 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/wm/productreceipt/index.ts 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/packages/effects/layouts/src/basic/tabbar/tabbar.vue 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/md/item/components/product-bom-select-dialog.vue 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/feedback/data.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/feedback/modules/form.vue 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/route/components/output-attribution.vue 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/route/data.ts 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/route/modules/process-form.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/route/modules/product-bom-list.vue 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/route/modules/route-configurator.vue 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/task/data.ts 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/task/index.vue 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/task/modules/auto-schedule.vue 185 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/workorder-center/index.vue 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/ipqc/data.ts 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/iqc/data.ts 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/oqc/data.ts 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/pendinginspect/data.ts 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/pendinginspect/index.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/rqc/data.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/workbench/components/WorkstationSelectDialog.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/workbench/components/tabs/FeedbackTab.vue 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/workbench/index.vue 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/arrivalnotice/modules/form.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/productreceipt/data.ts 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/productreceipt/index.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/productreceipt/modules/form.vue 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pro/route/process/index.ts
@@ -19,6 +19,8 @@
    itemId?: number;
    itemCode?: string;
    itemName?: string;
    quantity?: number; // é…æ¯”:每 1 ä¸ªä¸»äº§å‡ºçš„附带数量,主产出固定为 1
    mainFlag?: boolean; // æ˜¯å¦ä¸»äº§å‡º
    sort?: number;
    remark?: string;
  }
src/api/mes/pro/task/index.ts
@@ -92,6 +92,7 @@
    name?: string;
    checkFlag?: boolean;
    createTime?: string[];
    onlyMine?: boolean; // ä»…查看我是报工人的任务(工作台使用)
  }
}
@@ -175,6 +176,55 @@
  );
}
export interface BatchScheduleRequest {
  workOrderIds: number[];
  startTime: string;
  previewHash?: string;
  previewTime?: string;
  allowLate?: boolean;
}
export interface BatchSchedulePreview {
  canExecute: boolean;
  previewHash: string;
  previewTime: string;
  warnings: string[];
  notices: string[];
  orders: {
    workOrderId: number;
    workOrderCode?: string;
    workOrderName?: string;
    quantity?: number;
    taskCount: number;
    requestDate?: string;
    endTime?: string;
    late: boolean;
    error?: string;
  }[];
  tasks: {
    workOrderId: number;
    workOrderCode: string;
    processId: number;
    processName: string;
    workstationId: number;
    workstationName: string;
    machineryNames: string;
    quantity: number;
    hourlyCapacity: number;
    duration: number;
    startTime: string;
    endTime: string;
  }[];
}
export function previewBatchSchedule(data: BatchScheduleRequest) {
  return requestClient.post<BatchSchedulePreview>('/mes/pro/task/auto-schedule/preview', data);
}
export function executeBatchSchedule(data: BatchScheduleRequest) {
  return requestClient.post<{ taskIds: number[] }>('/mes/pro/task/auto-schedule', data);
}
/** æŸ¥è¯¢å¾…报工台账(分页) */
export function getPendingFeedbackPage(
  params: MesProTaskApi.PendingFeedbackPageParams,
@@ -186,9 +236,10 @@
}
/** æŒ‰å·¥ä½œç«™æŸ¥è¯¢å¾…报工任务数(key ä¸ºå·¥ä½œç«™ç¼–号) */
export function getPendingFeedbackCountByWorkstation() {
export function getPendingFeedbackCountByWorkstation(onlyMine?: boolean) {
  return requestClient.get<Record<string, number>>(
    '/mes/pro/task/pending-count-by-workstation',
    { params: onlyMine === undefined ? undefined : { onlyMine } },
  );
}
src/api/mes/qc/pendinginspect/index.ts
@@ -28,6 +28,7 @@
    // å®¢æˆ·ï¼ˆOQC/RQC åœºæ™¯ï¼‰
    clientId?: number; // å®¢æˆ· ID
    clientName?: string; // å®¢æˆ·åç§°
    canInspect?: boolean; // æ˜¯å¦å¯æ£€éªŒï¼ˆå·²é…ç½®å¯ç”¨çš„æ£€éªŒæ¨¡æ¿ï¼‰
    recordTime?: number; // è®°å½•时间(epoch ms)
  }
}
src/api/mes/wm/productreceipt/index.ts
@@ -10,6 +10,7 @@
    name?: string; // å…¥åº“单名称
    workOrderId?: number; // ç”Ÿäº§è®¢å•编号
    workOrderCode?: string; // ç”Ÿäº§è®¢å•编码
    produceId?: number; // æ¥æºç”Ÿäº§äº§å‡ºå•编号;为空表示手工入库单
    itemId?: number; // äº§å“ç‰©æ–™ç¼–号
    itemCode?: string; // äº§å“ç‰©æ–™ç¼–码
    itemName?: string; // äº§å“ç‰©æ–™åç§°
src/packages/effects/layouts/src/basic/tabbar/tabbar.vue
@@ -1,3 +1,8 @@
<script lang="ts">
// æ¨¡å—级标记:浏览器刷新会重置,布局重新挂载(如从全屏工作台返回)不会重置
let tabbarInitialized = false;
</script>
<script lang="ts" setup>
import { computed } from 'vue';
import { useRoute } from 'vue-router';
@@ -46,8 +51,10 @@
  });
});
// åˆ·æ–°åŽå¦‚果不保持tab状态,关闭其他tab
if (!preferences.tabbar.persist) {
// åˆ·æ–°åŽå¦‚果不保持tab状态,关闭其他tab;
// ä»…在浏览器刷新(模块重新加载)时执行一次,布局重新挂载不清空页签
if (!preferences.tabbar.persist && !tabbarInitialized) {
  tabbarInitialized = true;
  tabbarStore.closeOtherTabs(route);
}
</script>
src/views/mes/md/item/components/product-bom-select-dialog.vue
@@ -15,19 +15,29 @@
const emit = defineEmits<{
  selected: [row: MesMdProductBomApi.ProductBom];
  'selected-many': [rows: MesMdProductBomApi.ProductBom[]];
}>();
const open = ref(false); // å¼¹çª—是否打开
const list = ref<MesMdProductBomApi.ProductBom[]>([]); // BOM å­ç‰©æ–™åˆ—表
const selectedRow = ref<MesMdProductBomApi.ProductBom>(); // å½“前选中的 BOM è¡Œ
const multiple = ref(false); // æ˜¯å¦å¤šé€‰
const disabledItemIds = ref<number[]>([]); // å·²æ·»åŠ ã€ç¦æ­¢é‡å¤é€‰æ‹©çš„ç‰©æ–™
const baseColumns = useProductBomGridColumns(true) || [];
const [Grid, gridApi] = useVbenVxeGrid({
  gridOptions: {
    border: true,
    columns: [
      { type: 'radio', width: 55, align: 'center' },
      ...(useProductBomGridColumns(true) || []),
      ...baseColumns,
    ],
    checkboxConfig: {
      highlight: true,
      checkMethod: ({ row }: { row: MesMdProductBomApi.ProductBom }) =>
        !disabledItemIds.value.includes(row.bomItemId!),
    },
    data: list.value,
    height: 500,
    keepSource: true,
@@ -49,6 +59,9 @@
  } as VxeTableGridOptions<MesMdProductBomApi.ProductBom>,
  gridEvents: {
    cellDblclick: ({ row }: { row: MesMdProductBomApi.ProductBom }) => {
      if (multiple.value || disabledItemIds.value.includes(row.bomItemId!)) {
        return;
      }
      selectedRow.value = row;
      gridApi.grid.setRadioRow(row);
      handleConfirm();
@@ -59,16 +72,35 @@
  },
});
/** åˆ‡æ¢å•选/多选列 */
function applySelectMode() {
  gridApi.setGridOptions({
    columns: [
      multiple.value
        ? { type: 'checkbox', width: 55, align: 'center' }
        : { type: 'radio', width: 55, align: 'center' },
      ...baseColumns,
    ],
  });
}
/** æ‰“å¼€ BOM ç‰©æ–™é€‰æ‹©å¼¹çª— */
async function openModal(itemId: number, selectedBomItemId?: number) {
async function openModal(
  itemId: number,
  selectedBomItemId?: number,
  options?: { disabledItemIds?: number[]; multiple?: boolean },
) {
  open.value = true;
  selectedRow.value = undefined;
  multiple.value = options?.multiple === true;
  disabledItemIds.value = options?.disabledItemIds ?? [];
  applySelectMode();
  gridApi.setLoading(true);
  try {
    list.value = await getProductBomListByItemId(itemId);
    gridApi.setGridOptions({ data: list.value });
    await nextTick();
    if (selectedBomItemId !== null) {
    if (!multiple.value && selectedBomItemId !== null) {
      const match = list.value.find(
        (row) => row.bomItemId === selectedBomItemId,
      );
@@ -87,10 +119,21 @@
  open.value = false;
  selectedRow.value = undefined;
  await gridApi.grid.clearRadioRow();
  await gridApi.grid.clearCheckboxRow();
}
/** ç¡®è®¤é€‰æ‹© BOM ç‰©æ–™ */
function handleConfirm() {
  if (multiple.value) {
    const rows = gridApi.grid.getCheckboxRecords();
    if (!rows.length) {
      message.warning('请选择至少一条数据');
      return;
    }
    emit('selected-many', rows);
    open.value = false;
    return;
  }
  if (!selectedRow.value) {
    message.warning('请选择一条数据');
    return;
src/views/mes/pro/feedback/data.ts
@@ -304,8 +304,10 @@
            workstationId: task.workstationId,
            reportableQuantity: task.reportableQuantity,
          });
          // å·¥è‰ºè·¯çº¿å·¥åºçš„ checkFlag å†³å®šæ•°é‡åŒºåŸŸå±•示
          if (task.routeId && task.processId) {
          // æ˜¯å¦è´¨æ£€ä»¥ä»»åŠ¡ï¼ˆå·¥å•å·¥åºå¿«ç…§ï¼‰ä¸ºå‡†ï¼Œä»…åœ¨ç¼ºå¤±æ—¶å›žé€€å·¥è‰ºè·¯çº¿å·¥åº
          if (typeof task.checkFlag === 'boolean') {
            await formApi?.setFieldValue('checkFlag', task.checkFlag);
          } else if (task.routeId && task.processId) {
            try {
              const routeProcess = await getRouteProcessByRouteAndProcess(
                task.routeId,
src/views/mes/pro/feedback/modules/form.vue
@@ -1,4 +1,4 @@
<script lang="ts" setup>
<script lang="ts" setup>
import type { FormType } from '../data';
import type { MesProFeedbackApi } from '#/api/mes/pro/feedback';
@@ -11,7 +11,7 @@
import { MesAutoCodeRuleCode, MesProFeedbackStatusEnum } from '@vben/constants';
import { useUserStore } from '@vben/stores';
import { Button, message, Popconfirm, Tabs } from 'ant-design-vue';
import { Alert, Button, message, Popconfirm, Tabs } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
@@ -233,7 +233,15 @@
}
/** åŠ è½½å·¥åºçš„ checkFlag ç”¨äºŽå›žæ˜¾æ•°é‡åŒºåŸŸ */
async function resolveCheckFlag(routeId?: number, processId?: number) {
async function resolveCheckFlag(
  routeId?: number,
  processId?: number,
  taskCheckFlag?: boolean,
) {
  // æ˜¯å¦è´¨æ£€ä»¥ä»»åŠ¡ï¼ˆå·¥å•å·¥åºå¿«ç…§ï¼‰ä¸ºå‡†
  if (typeof taskCheckFlag === 'boolean') {
    return taskCheckFlag;
  }
  if (!routeId || !processId) {
    return true;
  }
@@ -302,6 +310,7 @@
      const checkFlag = await resolveCheckFlag(
        formData.value.routeId,
        formData.value.processId,
        formData.value.checkFlag,
      );
      // è®¾ç½®åˆ° values
      await formApi.setValues({ ...formData.value, checkFlag });
@@ -316,6 +325,14 @@
<template>
  <Modal :title="getTitle" class="w-3/5">
    <Alert
      v-if="canApprove"
      class="mx-4 mb-4"
      type="info"
      show-icon
      message="审批前请核对生产与库存配置"
      description="自动扣料工序需要线边库有足够物料;需检工序需要配置过程检验方案。末道工序请先配置成品入库仓位。"
    />
    <Form class="mx-4" />
    <FeedbackParamFields ref="paramFields" :task-id="paramTaskId" :snapshot="paramSnapshot" :readonly="!isEditable" />
    <Tabs
src/views/mes/pro/route/components/output-attribution.vue
@@ -6,7 +6,8 @@
import { IconifyIcon } from '@vben/icons';
import { Button, Select } from 'ant-design-vue';
import { Button, Checkbox, InputNumber, Select, Tooltip } from 'ant-design-vue';
import { MdItemSelect } from '#/views/mes/md/item/components';
defineOptions({ name: 'RouteOutputAttribution' });
@@ -57,11 +58,19 @@
/** é€‰æ‹©äº§å‡ºç‰©æ–™æ—¶ï¼Œé»˜è®¤å½’属到自身产品(半成品共用时再手动改成“通用”) */
function handleItemChange(index: number, itemId: number | undefined) {
  const current = rows.value[index];
  const patch: Partial<MesProRouteProcessApi.OutputItem> = { itemId };
  if (current?.productId == null && itemId != null) {
    patch.productId = itemId;
  }
  patch.productId = productOptions.value.some((product) => product.value === itemId) ? itemId : undefined;
  // åŒä¸€å½’属分组内默认第一条为主产出,新加入的产出若分组内还没有主产出则设为主产出
  const groupKey = patch.productId ?? GENERAL_PRODUCT;
  const groupHasMain = rows.value.some(
    (row, i) =>
      i !== index &&
      row.itemId != null &&
      (row.productId ?? GENERAL_PRODUCT) === groupKey &&
      row.mainFlag === true,
  );
  patch.mainFlag = !groupHasMain;
  patch.quantity = groupHasMain ? (rows.value[index]?.quantity ?? 1) : 1;
  updateRow(index, patch);
}
@@ -71,8 +80,39 @@
  });
}
/** åˆ‡æ¢ä¸»äº§å‡ºï¼šåŒä¸€å½’属分组内只能有一个主产出,主产出配比固定为 1 */
function handleMainChange(index: number, checked: boolean) {
  const target = rows.value[index];
  if (!target) {
    return;
  }
  const groupKey = target.productId ?? GENERAL_PRODUCT;
  commit(
    rows.value.map((row, i) => {
      if (i === index) {
        return {
          ...row,
          mainFlag: checked,
          quantity: checked ? 1 : (row.quantity ?? 1),
        };
      }
      if (checked && (row.productId ?? GENERAL_PRODUCT) === groupKey) {
        return { ...row, mainFlag: false };
      }
      return row;
    }),
  );
}
function handleQuantityChange(index: number, value: number | null) {
  updateRow(index, { quantity: value ?? undefined });
}
function addRow() {
  commit([...rows.value, { itemId: undefined, productId: undefined }]);
  commit([
    ...rows.value,
    { itemId: undefined, productId: undefined, quantity: 1 },
  ]);
}
function removeRow(index: number) {
@@ -88,16 +128,7 @@
        :key="index"
        class="flex items-center gap-2"
      >
        <Select
          :disabled="disabled"
          :options="productOptions"
          :placeholder="'选择产出物料'"
          :value="row.itemId"
          class="min-w-0 flex-1"
          option-filter-prop="label"
          show-search
          @change="(value) => handleItemChange(index, value as number | undefined)"
        />
        <MdItemSelect :disabled="disabled" :model-value="row.itemId" placeholder="选择产出物料" class="min-w-0 flex-1" @update:model-value="(value) => handleItemChange(index, value as number | undefined)" />
        <Select
          :disabled="disabled || row.itemId == null"
          :options="attributionOptions"
@@ -105,6 +136,31 @@
          class="min-w-0 flex-1"
          @change="(value) => handleAttributionChange(index, value as number | undefined)"
        />
        <Tooltip
          title="配比:每 1 ä¸ªä¸»äº§å‡ºçš„附带数量,例如主产出 1 å°é™„带 2 ä¸ªåŠæˆå“åˆ™å¡« 2;主产出固定为 1"
          placement="top"
        >
          <span class="inline-flex shrink-0">
            <InputNumber
              :disabled="disabled || row.itemId == null || row.mainFlag === true"
              :min="0.000001"
              :precision="6"
              :value="row.mainFlag === true ? 1 : (row.quantity ?? 1)"
              class="w-32"
              placeholder="配比"
              addon-before="×"
              @change="(value) => handleQuantityChange(index, value as number | null)"
            />
          </span>
        </Tooltip>
        <Checkbox
          :checked="row.mainFlag === true"
          :disabled="disabled || row.itemId == null"
          class="shrink-0"
          @change="(event) => handleMainChange(index, event.target.checked)"
        >
          ä¸»äº§å‡º
        </Checkbox>
        <Button
          :disabled="disabled"
          danger
src/views/mes/pro/route/data.ts
@@ -226,6 +226,7 @@
      componentProps: {
        products,
      },
      help: '配比表示每产出 1 ä¸ªä¸»äº§å‡ºæ—¶çš„附带数量:主产出固定为 1,联产品按实际比例填写(如 1 å°ä¸»äº§å“é™„带 2 ä¸ªåŠæˆå“åˆ™å¡« 2);报工时按「本次报工数量 Ã— é…æ¯”」生成各产出数量',
      formItemClass: 'col-span-2',
    },
    {
@@ -238,6 +239,7 @@
        placeholder: '请选择',
      },
      rules: z.number().default(3),
      dependencies: { triggerFields: [''], show: () => false },
    },
    {
      fieldName: 'colorCode',
@@ -328,23 +330,18 @@
        return outputs
          .map((output) => {
            const name = output.itemName ?? output.itemCode ?? output.itemId;
            return output.productId == null
              ? `${name}(通用)`
              : `${name}(${output.productName ?? output.productId})`;
            const ratio = output.quantity ?? 1;
            const text = `${name} Ã— ${ratio}`;
            const product =
              output.productId == null
                ? '通用'
                : (output.productName ?? output.productId);
            return `${output.mainFlag ? '★' : ''}${text}(${product})`;
          })
          .join('、');
      },
    },
    { field: 'nextProcessName', title: '下一道工序', width: 140 },
    {
      field: 'linkType',
      title: '与下一道工序关系',
      width: 160,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_PRO_LINK_TYPE },
      },
    },
    {
      field: 'keyFlag',
      title: '关键工序',
src/views/mes/pro/route/modules/process-form.vue
@@ -121,7 +121,7 @@
    <Alert
      class="mx-4 mb-4"
      message="按实际生产节点配置产出"
      description="中间工序没有独立产出时可留空;形成半成品时选择对应半成品,末道工序必须选择全部成品。归属表示该产出属于哪个产品:多产品共用的半成品选“通用”,产品专属(尤其是成品)归属到自身产品。如开启报工自动扣料,报工时会按工序投料配置扣减库存。"
      description="中间工序没有独立产出时可留空;形成半成品时从基础物料选择对应半成品,末道工序必须选择全部成品。归属表示该产出属于哪个产品:多产品共用的半成品选“通用”,产品专属(尤其是成品)归属到自身产品。如开启报工自动扣料,报工时会按工序投料配置扣减库存。"
      show-icon
      type="info"
    />
src/views/mes/pro/route/modules/product-bom-list.vue
@@ -1,6 +1,7 @@
<script lang="ts" setup>
import type { FormType } from '../data';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesMdProductBomApi } from '#/api/mes/md/item/productBom';
import type { MesProRouteProcessApi } from '#/api/mes/pro/route/process';
import type { MesProRouteProductBomApi } from '#/api/mes/pro/route/productbom';
@@ -13,10 +14,12 @@
import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getRouteProcessListByRoute } from '#/api/mes/pro/route/process';
import {
  createRouteProductBom,
  deleteRouteProductBom,
  getRouteProductBomList,
} from '#/api/mes/pro/route/productbom';
import { $t } from '#/locales';
import { MdProductBomSelectDialog } from '#/views/mes/md/item/components';
import { useRouteProductBomGridColumns } from '../data';
import BomForm from './bom-form.vue';
@@ -42,6 +45,8 @@
  connectedComponent: BomForm,
  destroyOnClose: true,
});
const bomSelectRef = ref<InstanceType<typeof MdProductBomSelectDialog>>();
const [Grid, gridApi] = useVbenVxeGrid({
  gridOptions: {
@@ -88,19 +93,43 @@
  }
}
/** æ–°å¢ž BOM */
/** æ–°å¢ž BOM:打开产品 BOM ç‰©æ–™å¼¹çª—,支持多选批量添加 */
function handleCreate() {
  if (!activeProcessId.value) {
    message.warning('请先选择工序');
    return;
  }
  bomFormModalApi
    .setData({
      processId: activeProcessId.value,
      productId: props.productId,
      routeId: props.routeId,
    })
    .open();
  bomSelectRef.value?.open(props.productId, undefined, {
    multiple: true,
    disabledItemIds: list.value
      .map((item) => item.itemId)
      .filter((id): id is number => id !== undefined),
  });
}
/** æ‰¹é‡æ·»åŠ é€‰ä¸­çš„ BOM ç‰©æ–™ */
async function handleSelectedMany(rows: MesMdProductBomApi.ProductBom[]) {
  const processId = activeProcessId.value;
  if (!processId) {
    return;
  }
  gridApi.setLoading(true);
  try {
    for (const row of rows) {
      await createRouteProductBom({
        routeId: props.routeId,
        processId,
        productId: props.productId,
        itemId: row.bomItemId,
        quantity: row.quantity ?? 1,
      });
    }
    message.success(`已添加 ${rows.length} æ¡æŠ•料物料`);
    await getList();
    emit('change');
  } finally {
    gridApi.setLoading(false);
  }
}
/** ç¼–辑 BOM */
@@ -141,6 +170,10 @@
<template>
  <BomFormModal @success="handleSaved" />
  <MdProductBomSelectDialog
    ref="bomSelectRef"
    @selected-many="handleSelectedMany"
  />
  <Tabs v-model:active-key="activeProcessId" @change="getList">
    <TabPane
      v-for="item in processList"
src/views/mes/pro/route/modules/route-configurator.vue
@@ -92,16 +92,6 @@
      text: '末道工序的成品产出必须归属到自身产品,不能使用“通用”',
    });
  }
  const productIds = new Set(productList.value.map((item) => item.itemId));
  const invalidOutputCount = processList.value.filter((process) =>
    process.outputItemIds?.some((itemId) => !productIds.has(itemId)),
  ).length;
  if (invalidOutputCount) {
    issues.push({
      step: 2,
      text: `${invalidOutputCount} é“工序的产出未包含在路线产出产品中`,
    });
  }
  const noBomProducts = productList.value.filter(
    (item) => item.itemId && !configuredProductIds.value.has(item.itemId),
  );
@@ -295,8 +285,8 @@
        <section v-if="routeId" v-show="activeStep === 1" class="route-step-panel">
          <Alert
            class="mb-4"
            message="先维护所有可能的工序产出"
            description="实际形成的中间半成品和末道工序的成品都要加入,并指定入库仓位;选择一个成品作为模板引用,供工艺流程模板下载使用。"
            message="维护成品及报工入库位置"
            description="这里只维护成品并指定入库仓位;中间半成品在工序产出中从基础物料选择。选择一个成品作为模板引用,供工艺流程模板下载使用。"
            show-icon
            type="info"
          />
src/views/mes/pro/task/data.ts
@@ -85,6 +85,7 @@
/** å¾…排产订单列表的字段 */
export function useGridColumns(): VxeTableGridOptions<MesProWorkOrderApi.WorkOrder>['columns'] {
  return [
    { type: 'checkbox', width: 48, fixed: 'left' },
    {
      field: 'code',
      title: '订单编码',
src/views/mes/pro/task/index.vue
@@ -6,19 +6,18 @@
import { useRouter } from 'vue-router';
import { Page, useVbenModal } from '@vben/common-ui';
import { MesProWorkOrderStatusEnum } from '@vben/constants';
import { MesProWorkOrderStatusEnum, MesProWorkOrderTypeEnum } from '@vben/constants';
import { Button, Card, message, Tabs } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getGanttTaskList, previewAutoGenerate, autoGenerateTasks } from '#/api/mes/pro/task';
import { getWorkOrderPage } from '#/api/mes/pro/workorder';
import { getGanttTaskList } from '#/api/mes/pro/task';
import ArchiveDetail from '#/views/mes/pd/archive/modules/detail.vue';
import { GanttChart } from './components';
import { useGridColumns, useGridFormSchema } from './data';
import ScheduleForm from './modules/schedule-form.vue';
import SchedulePreview from '../workorder-center/modules/schedule-preview.vue';
import AutoSchedule from './modules/auto-schedule.vue';
const router = useRouter();
const ganttTasks = ref<any[]>([]); // ç”˜ç‰¹å›¾é¢„览数据
@@ -32,13 +31,14 @@
  destroyOnClose: true,
});
const [SchedulePreviewModal, schedulePreviewModalApi] = useVbenModal({
  connectedComponent: SchedulePreview,
const [AutoScheduleModal, autoScheduleModalApi] = useVbenModal({
  connectedComponent: AutoSchedule,
  destroyOnClose: true,
});
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
  gridApi.grid.clearCheckboxRow();
  gridApi.query();
  loadGanttPreview();
}
@@ -60,27 +60,17 @@
  scheduleModalApi.setData({ formType: 'schedule', id: row.id }).open();
}
/** ä¸€é”®æŽ’产预览 */
async function handleAutoSchedulePreview(row: MesProWorkOrderApi.WorkOrder) {
  try {
    const preview = await previewAutoGenerate({ workOrderId: row.id! });
    schedulePreviewModalApi
      .setData({ workOrder: row, preview })
      .open();
  } catch {
    message.error('获取排产预览失败');
  }
function canAutoSchedule(row: MesProWorkOrderApi.WorkOrder) {
  return row.status === MesProWorkOrderStatusEnum.CONFIRMED
    && row.type === MesProWorkOrderTypeEnum.SELF
    && (row.quantity ?? 0) > 0 && (row.quantityProduced ?? 0) < (row.quantity ?? 0);
}
/** ä¸€é”®æŽ’产执行 */
async function handleAutoSchedule(row: MesProWorkOrderApi.WorkOrder) {
  try {
    const result = await autoGenerateTasks({ workOrderId: row.id! });
    message.success(`排产成功,生成 ${result.taskIds?.length || 0} ä¸ªä»»åŠ¡`);
    handleRefresh();
  } catch {
    message.error('排产失败');
  }
function handleAutoSchedule(row?: MesProWorkOrderApi.WorkOrder) {
  const rows: MesProWorkOrderApi.WorkOrder[] = row ? [row] : gridApi.grid.getCheckboxRecords();
  if (!rows.length) return message.warning('请先勾选待排产工单');
  if (rows.length > 30) return message.warning('一次最多选择 30 ä¸ªå·¥å•,请分批排产');
  if (rows.some((order) => !canAutoSchedule(order))) return message.warning('请选择已确认且未生产完成的自行生产工单');
  autoScheduleModalApi.setData({ workOrderIds: rows.map((order) => order.id!) }).open();
}
/** æ‰“开归档详情 */
@@ -101,6 +91,7 @@
  gridOptions: {
    columns: useGridColumns(),
    keepSource: true,
    checkboxConfig: { checkStrictly: true, showHeader: true, checkMethod: ({ row }: { row: MesProWorkOrderApi.WorkOrder }) => canAutoSchedule(row) },
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) => {
@@ -146,7 +137,7 @@
<template>
  <Page auto-content-height>
    <ScheduleModal @success="handleRefresh" />
    <SchedulePreviewModal @success="handleRefresh" />
    <AutoScheduleModal @success="handleRefresh" />
    <!-- æŽ’产甘特图预览 -->
    <Card title="排产甘特图" class="mb-4">
@@ -168,6 +159,13 @@
        <TableAction
          :actions="[
            {
              label: '一键排产',
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['mes:pro-task:create'],
              onClick: () => handleAutoSchedule(),
            },
            {
              label: '甘特图编辑',
              type: 'primary',
              icon: ACTION_ICON.EDIT,
@@ -185,6 +183,13 @@
        <TableAction
          :actions="[
            {
              label: '一键排产',
              type: 'link',
              auth: ['mes:pro-task:create'],
              ifShow: canAutoSchedule(row),
              onClick: () => handleAutoSchedule(row),
            },
            {
              label: '排产',
              type: 'link',
              auth: ['mes:pro-task:create'],
src/views/mes/pro/task/modules/auto-schedule.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,185 @@
<script setup lang="ts">
import type { BatchSchedulePreview, BatchScheduleRequest } from '#/api/mes/pro/task';
import { computed, ref, watch } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Alert, Button, Checkbox, DatePicker, Descriptions, Table, Tag, message } from 'ant-design-vue';
import dayjs from 'dayjs';
import { executeBatchSchedule, previewBatchSchedule } from '#/api/mes/pro/task';
const emit = defineEmits(['success']);
const ids = ref<number[]>([]);
function defaultStartTime() {
  let value = dayjs().add(5, 'minute').startOf('minute');
  if (value.hour() >= 18) value = value.add(1, 'day').hour(9).minute(0).second(0);
  else if (value.hour() < 9) value = value.hour(9).minute(0).second(0);
  while (value.day() === 0 || value.day() === 6) value = value.add(1, 'day').hour(9).minute(0).second(0);
  return value;
}
const startTime = ref(defaultStartTime());
const preview = ref<BatchSchedulePreview>();
const loading = ref(false);
const executing = ref(false);
const acceptLate = ref(false);
const errorText = ref('');
let sequence = 0;
let open = false;
const canConfirm = computed(() => !!preview.value?.canExecute && !loading.value && !executing.value
  && (!preview.value.warnings.length || acceptLate.value));
const [Modal, modalApi] = useVbenModal({
  confirmText: '确认排产',
  confirmDisabled: true,
  onBeforeClose: () => !executing.value,
  onConfirm: execute,
  async onOpenChange(isOpen) {
    open = isOpen;
    sequence++;
    if (!isOpen) {
      preview.value = undefined;
      ids.value = [];
      loading.value = false;
      errorText.value = '';
      return;
    }
    ids.value = modalApi.getData<{ workOrderIds: number[] }>().workOrderIds;
    startTime.value = defaultStartTime();
    await calculate();
  },
});
watch(canConfirm, (value) => modalApi.setState({ confirmDisabled: !value }));
watch(startTime, () => {
  sequence++;
  preview.value = undefined;
  acceptLate.value = false;
  loading.value = false;
  errorText.value = '';
}, { flush: 'sync' });
function request(): BatchScheduleRequest {
  return { workOrderIds: [...ids.value], startTime: startTime.value.format('YYYY-MM-DD HH:mm:ss') };
}
async function calculate() {
  if (executing.value || !startTime.value || !ids.value.length) return;
  const current = ++sequence;
  preview.value = undefined;
  acceptLate.value = false;
  errorText.value = '';
  loading.value = true;
  try {
    const result = await previewBatchSchedule(request());
    if (open && current === sequence) preview.value = result;
  } catch (error) {
    if (open && current === sequence) errorText.value = error instanceof Error ? error.message : '预览失败,请稍后重试';
  } finally {
    if (current === sequence) loading.value = false;
  }
}
async function execute() {
  if (!canConfirm.value || !preview.value) return;
  executing.value = true;
  errorText.value = '';
  modalApi.lock();
  try {
    const result = await executeBatchSchedule({ ...request(), previewHash: preview.value.previewHash,
      previewTime: preview.value.previewTime, allowLate: acceptLate.value });
    message.success(`排产成功:${ids.value.length} ä¸ªå·¥å•,生成 ${result.taskIds.length} ä¸ªä»»åŠ¡`);
    executing.value = false;
    await modalApi.close();
    emit('success');
  } catch (error) {
    errorText.value = error instanceof Error ? error.message : '执行结果未确认,请刷新列表并重新预览';
    // è¯·æ±‚失败或结果未确认后必须重算;旧预览不能直接再次提交。
    preview.value = undefined;
    acceptLate.value = false;
    emit('success'); // åˆ·æ–°å·¥å•和甘特图,核对超时请求是否已经提交。
  } finally {
    executing.value = false;
    modalApi.unlock();
  }
}
const orderColumns = [
  { title: '工单编码', dataIndex: 'workOrderCode', width: 170 },
  { title: '生产数量', dataIndex: 'quantity', width: 100 },
  { title: '新增任务', dataIndex: 'taskCount', width: 90 },
  { title: '需求日期', dataIndex: 'requestDate', width: 120 },
  { title: '预计完成', dataIndex: 'endTime', width: 175 },
  { title: '校验结果', dataIndex: 'error', width: 300 },
];
const taskColumns = [
  { title: '工单', dataIndex: 'workOrderCode', width: 160 },
  { title: '工序', dataIndex: 'processName', width: 130 },
  { title: '工作站', dataIndex: 'workstationName', width: 150 },
  { title: '设备', dataIndex: 'machineryNames', width: 180 },
  { title: '本次数量', dataIndex: 'quantity', width: 100 },
  { title: '小时产能', dataIndex: 'hourlyCapacity', width: 100 },
  { title: '工时(h)', dataIndex: 'duration', width: 85 },
  { title: '开工时间', dataIndex: 'startTime', width: 180 },
  { title: '完成时间', dataIndex: 'endTime', width: 180 },
];
</script>
<template>
  <Modal title="一键排产" class="w-[min(96vw,1200px)]">
    <div class="space-y-5 px-1">
      <Alert type="info" show-icon message="先校验与预览,再确认排产"
        description="按交期优先及工序顺序安排工作站与设备;每天09:00–18:00,排除周末和已配置节假日,非工作时间自动顺延。已部分排产的工单只补齐剩余数量,最多30个工单、200个新任务。" />
      <div class="flex flex-wrap items-end gap-3">
        <label class="flex flex-col gap-2">
          <span class="font-medium">最早开工时间</span>
          <DatePicker v-model:value="startTime" show-time :allow-clear="false" :disabled="executing"
            format="YYYY-MM-DD HH:mm:ss" :disabled-date="(date) => date.endOf('day').isBefore(dayjs())" />
        </label>
        <Button :loading="loading" :disabled="executing" @click="calculate">
          {{ preview ? '重新计算预览' : '生成排产预览' }}
        </Button>
        <span class="text-sm text-muted-foreground">已选择 {{ ids.length }} ä¸ªå·¥å•</span>
      </div>
      <Alert v-if="errorText" type="error" show-icon :message="errorText" />
      <template v-if="preview">
        <Descriptions :column="{ xs: 1, sm: 3 }" size="small" bordered>
          <Descriptions.Item label="工单数">{{ preview.orders.length }}</Descriptions.Item>
          <Descriptions.Item label="新增任务数">{{ preview.tasks.length }}</Descriptions.Item>
          <Descriptions.Item label="预览有效期">15分钟,提交时重新校验</Descriptions.Item>
        </Descriptions>
        <Alert v-if="!preview.canExecute" type="error" show-icon message="部分工单未通过校验,本批次不能提交"
          description="请根据下方校验结果修正工单、设备或已有任务的配置,再重新计算预览。" />
        <div>
          <h3 class="mb-2 font-medium">工单校验</h3>
          <Table :columns="orderColumns" :data-source="preview.orders" row-key="workOrderId"
            :pagination="false" size="small" :scroll="{ x: 1055 }">
            <template #bodyCell="{ column, record, text }">
              <template v-if="column.dataIndex === 'error'">
                <span v-if="record.error" class="text-destructive">{{ record.error }}</span>
                <Tag v-else :color="record.late ? 'orange' : 'green'">{{ record.late ? '有延期风险' : '通过' }}</Tag>
              </template>
              <template v-else-if="column.dataIndex === 'requestDate'">{{ text ? String(text).slice(0, 10) : '未指定' }}</template>
            </template>
          </Table>
        </div>
        <div v-if="preview.tasks.length">
          <h3 class="mb-2 font-medium">任务安排</h3>
          <Table :columns="taskColumns" :data-source="preview.tasks"
            :row-key="(row) => `${row.workOrderId}-${row.processId}`" :pagination="{ pageSize: 10, showSizeChanger: false }"
            size="small" :scroll="{ x: 1365 }" />
        </div>
        <Alert v-if="preview.notices.length" type="info" show-icon message="联产出提示">
          <template #description>
            <p v-for="notice in preview.notices" :key="notice">{{ notice }}</p>
          </template>
        </Alert>
        <Alert v-if="preview.warnings.length" type="warning" show-icon message="请核对交期风险">
          <template #description>
            <p v-for="warning in preview.warnings" :key="warning">{{ warning }}</p>
            <Checkbox v-model:checked="acceptLate" :disabled="executing" class="mt-2">
              æˆ‘已核对并接受以上交期延期风险
            </Checkbox>
          </template>
        </Alert>
      </template>
    </div>
  </Modal>
</template>
src/views/mes/pro/workorder-center/index.vue
@@ -16,12 +16,12 @@
  getWorkOrderProgress,
  checkAndAutoFinishWorkOrder,
} from '#/api/mes/pro/workorder';
import { previewAutoGenerate, autoGenerateTasks } from '#/api/mes/pro/task';
import ArchiveDetail from '#/views/mes/pd/archive/modules/detail.vue';
import { useGridColumns, useGridFormSchema, useTaskGridColumns } from './data';
import QuickFeedbackForm from './modules/quick-feedback.vue';
import SchedulePreviewModal from './modules/schedule-preview.vue';
import AutoSchedule from '../task/modules/auto-schedule.vue';
defineOptions({ name: 'WorkorderCenter' });
const archiveDetailRef = ref<InstanceType<typeof ArchiveDetail>>();
@@ -38,7 +38,7 @@
});
const [SchedulePreviewModalWrap, schedulePreviewModalApi] = useVbenModal({
  connectedComponent: SchedulePreviewModal,
  connectedComponent: AutoSchedule,
  destroyOnClose: true,
});
@@ -107,27 +107,8 @@
    .open();
}
/** ä¸€é”®æŽ’产预览 */
async function handleSchedulePreview(row: MesProWorkOrderApi.WorkOrder) {
  try {
    const preview = await previewAutoGenerate({ workOrderId: row.id! });
    schedulePreviewModalApi
      .setData({ workOrder: row, preview })
      .open();
  } catch {
    message.error('获取排产预览失败');
  }
}
/** æ‰§è¡Œä¸€é”®æŽ’产 */
async function handleScheduleExecute(row: MesProWorkOrderApi.WorkOrder) {
  try {
    const result = await autoGenerateTasks({ workOrderId: row.id! });
    message.success(`排产成功,生成 ${result.taskIds?.length || 0} ä¸ªä»»åŠ¡`);
    handleRefresh();
  } catch {
    message.error('排产失败');
  }
function handleSchedulePreview(row: MesProWorkOrderApi.WorkOrder) {
  schedulePreviewModalApi.setData({ workOrderIds: [row.id!] }).open();
}
const [Grid, gridApi] = useVbenVxeGrid({
@@ -193,21 +174,11 @@
        <TableAction
          :actions="[
            {
              label: '排产预览',
              type: 'link',
              auth: ['mes:pro-task:create'],
              ifShow: row.quantityScheduled < row.quantity,
              onClick: handleSchedulePreview.bind(null, row),
            },
            {
              label: '一键排产',
              type: 'link',
              auth: ['mes:pro-task:create'],
              ifShow: row.quantityScheduled < row.quantity,
              popConfirm: {
                title: '确认一键排产?将按照工艺路线自动生成任务。',
                confirm: handleScheduleExecute.bind(null, row),
              },
              ifShow: row.status === MesProWorkOrderStatusEnum.CONFIRMED && row.type === MesProWorkOrderTypeEnum.SELF && (row.quantity ?? 0) > 0 && (row.quantityProduced ?? 0) < (row.quantity ?? 0),
              onClick: handleSchedulePreview.bind(null, row),
            },
            {
              label: '归档',
src/views/mes/qc/ipqc/data.ts
@@ -264,7 +264,6 @@
        onChange: () => syncUnqualified(formApi),
      },
      defaultValue: 0,
      rules: 'required',
      dependencies: {
        triggerFields: ['unqualifiedQuantity'],
        show: (values) =>
@@ -283,7 +282,6 @@
        onChange: () => syncUnqualified(formApi),
      },
      defaultValue: 0,
      rules: 'required',
      dependencies: {
        triggerFields: ['unqualifiedQuantity'],
        show: (values) =>
@@ -302,7 +300,6 @@
        onChange: () => syncUnqualified(formApi),
      },
      defaultValue: 0,
      rules: 'required',
      dependencies: {
        triggerFields: ['unqualifiedQuantity'],
        show: (values) =>
@@ -320,7 +317,6 @@
        placeholder: '请选择检测人员',
        valueField: 'id',
      },
      rules: 'selectRequired',
    },
    {
      fieldName: 'inspectDate',
@@ -331,7 +327,6 @@
        placeholder: '请选择检测日期',
        valueFormat: 'YYYY-MM-DD',
      },
      rules: 'required',
    },
    {
      fieldName: 'checkResult',
src/views/mes/qc/iqc/data.ts
@@ -219,7 +219,6 @@
        placeholder: '请选择来料日期',
        valueFormat: 'YYYY-MM-DD',
      },
      rules: 'required',
    },
    {
      fieldName: 'inspectorUserId',
@@ -232,7 +231,6 @@
        placeholder: '请选择检测人员',
        valueField: 'id',
      },
      rules: 'selectRequired',
    },
    {
      fieldName: 'inspectDate',
@@ -243,7 +241,6 @@
        placeholder: '请选择检测日期',
        valueFormat: 'YYYY-MM-DD',
      },
      rules: 'required',
    },
    {
      fieldName: 'checkResult',
src/views/mes/qc/oqc/data.ts
@@ -139,7 +139,6 @@
      componentProps: {
        placeholder: '请选择客户',
      },
      rules: 'selectRequired',
      dependencies: {
        triggerFields: ['sourceDocId'],
        componentProps: (values) => ({
@@ -246,7 +245,6 @@
        placeholder: '请选择检测人员',
        valueField: 'id',
      },
      rules: 'selectRequired',
    },
    {
      fieldName: 'outDate',
@@ -257,7 +255,6 @@
        placeholder: '请选择出货日期',
        valueFormat: 'YYYY-MM-DD',
      },
      rules: 'required',
    },
    {
      fieldName: 'inspectDate',
@@ -268,7 +265,6 @@
        placeholder: '请选择检测日期',
        valueFormat: 'YYYY-MM-DD',
      },
      rules: 'required',
    },
    {
      fieldName: 'checkResult',
src/views/mes/qc/pendinginspect/data.ts
@@ -94,6 +94,13 @@
      width: 80,
    },
    {
      field: 'canInspect',
      title: '可检验',
      width: 100,
      align: 'center',
      slots: { default: 'canInspect' },
    },
    {
      title: '操作',
      width: 130,
      fixed: 'right',
src/views/mes/qc/pendinginspect/index.vue
@@ -8,6 +8,7 @@
import { Page, useVbenModal } from '@vben/common-ui';
import { MesQcTypeEnum } from '@vben/constants';
import { Alert, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getPendingInspectPage } from '#/api/mes/qc/pendinginspect';
@@ -141,7 +142,18 @@
    <OqcFormModal @success="handleRefresh" />
    <RqcFormModal @success="handleRefresh" />
    <Alert
      class="mb-4"
      type="info"
      show-icon
      message="从待检任务创建检验单"
      description="请先为物料配置并启用对应类型的质检方案。未显示「可检验」的物料无法创建检验单。生产过程检验完成后,系统会回写报工并处理合格产出。"
    />
    <Grid table-title="待检任务列表">
      <template #canInspect="{ row }">
        <Tag v-if="row.canInspect" color="success">可检验</Tag>
        <Tag v-else color="error">未配置模板</Tag>
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
@@ -150,6 +162,7 @@
              type: 'link',
              icon: ACTION_ICON.ADD,
              auth: ['mes:qc-iqc:create'],
              disabled: row.canInspect === false,
              ifShow: row.qcType === MesQcTypeEnum.IQC,
              onClick: handleCreateIqc.bind(null, row),
            },
@@ -158,6 +171,7 @@
              type: 'link',
              icon: ACTION_ICON.ADD,
              auth: ['mes:qc-ipqc:create'],
              disabled: row.canInspect === false,
              ifShow: row.qcType === MesQcTypeEnum.IPQC,
              onClick: handleCreateIpqc.bind(null, row),
            },
@@ -166,6 +180,7 @@
              type: 'link',
              icon: ACTION_ICON.ADD,
              auth: ['mes:qc-oqc:create'],
              disabled: row.canInspect === false,
              ifShow: row.qcType === MesQcTypeEnum.OQC,
              onClick: handleCreateOqc.bind(null, row),
            },
@@ -174,6 +189,7 @@
              type: 'link',
              icon: ACTION_ICON.ADD,
              auth: ['mes:qc-rqc:create'],
              disabled: row.canInspect === false,
              ifShow: row.qcType === MesQcTypeEnum.RQC,
              onClick: handleCreateRqc.bind(null, row),
            },
src/views/mes/qc/rqc/data.ts
@@ -222,7 +222,6 @@
        showTime: true,
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
      rules: 'required',
    },
    {
      fieldName: 'inspectorUserId',
@@ -235,7 +234,6 @@
        placeholder: '请选择检测人员',
        valueField: 'id',
      },
      rules: 'selectRequired',
    },
    {
      fieldName: 'checkResult',
src/views/mes/workbench/components/WorkstationSelectDialog.vue
@@ -36,7 +36,7 @@
  try {
    const [result, counts] = await Promise.all([
      getWorkstationPage({ pageNo: 1, pageSize: 200 }),
      getPendingFeedbackCountByWorkstation().catch(() => null),
      getPendingFeedbackCountByWorkstation(true).catch(() => null),
    ]);
    workstations.value = result.list ?? [];
    pendingCounts.value = counts;
@@ -50,14 +50,22 @@
  return pendingCounts.value?.[String(ws.id)] ?? 0;
}
/** è¿‡æ»¤åŽçš„工作站 */
/** è¿‡æ»¤åŽçš„工作站:仅展示当前用户有可报工任务的;统计失败时不过滤,避免全部隐藏 */
const filtered = computed(() => {
  const kw = searchKeyword.value.trim().toLowerCase();
  if (!kw) return workstations.value;
  return workstations.value.filter((ws) => {
    if (pendingCounts.value && getPendingCount(ws) <= 0) return false;
    if (!kw) return true;
    const fields = [ws.code, ws.name, ws.processName, ws.workshopName, ws.address];
    return fields.some((f) => String(f ?? '').toLowerCase().includes(kw));
  });
});
/** ç©ºçŠ¶æ€æ–‡æ¡ˆ */
const emptyText = computed(() => {
  if (!pendingCounts.value) return '暂无工作站数据';
  if (searchKeyword.value.trim()) return '没有匹配的工作站';
  return '暂无可报工的工作站';
});
/** é€‰æ‹©å·¥ä½œç«™ */
@@ -96,7 +104,7 @@
      <!-- ç©ºçŠ¶æ€ -->
      <div v-if="!filtered.length && !loading" class="ws-select__empty">
        <IconifyIcon icon="lucide:inbox" class="ws-select__empty-icon" />
        <p>暂无工作站数据</p>
        <p>{{ emptyText }}</p>
      </div>
      <!-- å¡ç‰‡ç½‘æ ¼ -->
src/views/mes/workbench/components/tabs/FeedbackTab.vue
@@ -101,8 +101,13 @@
  return data;
}
/** åŠ è½½ checkFlag */
async function resolveCheckFlag(routeId?: number, processId?: number) {
/** åŠ è½½ checkFlag:任务(工单工序快照)优先,缺失时回退工艺路线工序 */
async function resolveCheckFlag(
  routeId?: number,
  processId?: number,
  taskCheckFlag?: boolean,
) {
  if (typeof taskCheckFlag === 'boolean') return taskCheckFlag;
  if (!routeId || !processId) return true;
  try {
    const rp = await getRouteProcessByRouteAndProcess(routeId, processId);
@@ -124,7 +129,11 @@
  formApi.setState({ schema: useFormSchema(formType.value, formApi) });
  const code = await generateAutoCode(MesAutoCodeRuleCode.PRO_FEEDBACK_CODE);
  const checkFlag = await resolveCheckFlag(task.routeId, task.processId);
  const checkFlag = await resolveCheckFlag(
    task.routeId,
    task.processId,
    task.checkFlag,
  );
  await formApi.setValues({
    code,
@@ -285,7 +294,11 @@
  feedbackStatus.value = data.status;
  formType.value = 'update';
  formApi.setState({ schema: useFormSchema(formType.value, formApi) });
  const checkFlag = await resolveCheckFlag(data.routeId, data.processId);
  const checkFlag = await resolveCheckFlag(
    data.routeId,
    data.processId,
    data.checkFlag,
  );
  await formApi.setValues({ ...data, checkFlag });
  originalSnapshot.value = JSON.stringify(await getAlignedData(false));
}
src/views/mes/workbench/index.vue
@@ -113,6 +113,7 @@
      workstationId: workstationId.value,
      pageNo: 1,
      pageSize: 200,
      onlyMine: true,
    });
    taskList.value = result.list ?? [];
    // åŒæ­¥ selectedTask åˆ°æœ€æ–°æ•°æ®ï¼ˆä¸è‡ªåŠ¨é€‰ä¸­ç¬¬ä¸€ä¸ªï¼Œéœ€ç”¨æˆ·æ‰‹åŠ¨ç‚¹å‡»ï¼‰
@@ -156,7 +157,12 @@
// --- è¿”回系统 ---
function goBack() {
  router.push('/');
  // è¿”回上一步;直接打开工作台(无历史记录)时兜底回首页
  if (window.history.state?.back) {
    router.back();
  } else {
    router.push('/');
  }
}
</script>
src/views/wls/arrivalnotice/modules/form.vue
@@ -8,7 +8,7 @@
import { useVbenModal } from '@vben/common-ui';
import { MesWmArrivalNoticeStatusEnum } from '@vben/constants';
import { Button, Divider, message, Popconfirm } from 'ant-design-vue';
import { Alert, Button, Divider, message, Popconfirm } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
@@ -75,7 +75,7 @@
      originalSnapshot.value = current;
    }
    await submitArrivalNotice(formData.value.id);
    message.success('提交成功');
    message.success('提交成功,请按单据状态继续质检或入库');
    await modalApi.close();
    emit('success');
  } finally {
@@ -148,6 +148,7 @@
<template>
  <Modal :title="getTitle" class="w-3/5">
    <Alert v-if="isEditable" class="mx-4 mb-4" type="info" show-icon message="提交前请完善物料明细" description="需检物料请先配置启用的来料检验模板。提交后系统会生成检验单;检验完成后再办理入库。不需检的物料可直接进入待入库。" />
    <Form class="mx-4" />
    <!-- éžæ–°å»ºæ¨¡å¼å±•示物料信息 -->
    <template v-if="formData?.id">
src/views/wls/productreceipt/data.ts
@@ -182,6 +182,13 @@
      formatter: 'formatDate',
    },
    {
      field: 'produceId',
      title: '来源',
      width: 110,
      align: 'center',
      slots: { default: 'source' },
    },
    {
      field: 'status',
      title: '单据状态',
      minWidth: 110,
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 {
@@ -172,6 +172,10 @@
          {{ row.code }}
        </Button>
      </template>
      <template #source="{ row }">
        <Tag v-if="row.produceId" color="blue">产出自动生成</Tag>
        <Tag v-else>手工创建</Tag>
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
src/views/wls/productreceipt/modules/form.vue
@@ -8,7 +8,7 @@
import { confirm, useVbenModal } from '@vben/common-ui';
import { MesWmProductReceiptStatusEnum } from '@vben/constants';
import { Button, Divider, message, Popconfirm } from 'ant-design-vue';
import { Alert, Button, Divider, message, Popconfirm } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
@@ -214,6 +214,14 @@
<template>
  <Modal :title="getTitle" class="w-3/5">
    <Alert
      v-if="formData?.produceId"
      class="mx-4 mb-3"
      type="info"
      show-icon
      message="产出自动生成的入库单"
      description="该单据由生产产出自动生成,库存已在产出时入账。执行入库仅关闭业务单据,不会重复更新库存。"
    />
    <Form class="mx-4" />
    <!-- éžæ–°å»ºæ¨¡å¼å±•示物料信息 -->
    <template v-if="formData?.id">