2 天以前 d2cd585f4d3b015789a5f6c8d75e352605932f85
feat(mes): 新增袋码作废时间和原因字段及破损替换功能

- 在袋码数据表中新增 cancelTime 和 cancelReason 字段用于记录作废信息
- 扩展袋码 API 接口增加作废相关字段和破损替换接口
- 实现破损袋码批量替换功能,支持选择袋码进行替换操作
- 添加替换结果展示弹窗和替换操作的业务逻辑验证
- 更新扫码追溯模态框显示作废和替换相关的追溯信息
- 新增 ReplaceModal 组件用于处理破损袋码替换表单操作
已修改4个文件
已添加1个文件
281 ■■■■■ 文件已修改
src/api/mes/pro/bagCode/index.ts 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/bagCode/data.ts 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/bagCode/index.vue 97 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/bagCode/modules/replace-form.vue 105 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/components/qr-trace-modal.vue 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pro/bagCode/index.ts
@@ -17,6 +17,8 @@
    palletCode?: string; // æ‰˜ç›˜ç 
    status?: number; // çŠ¶æ€ï¼ˆå­—å…¸ mes_pro_bag_code_status)
    printTime?: string; // å¯¼å‡ºï¼ˆäº¤ä»˜å°åˆ·ï¼‰æ—¶é—´
    cancelTime?: string; // ä½œåºŸæ—¶é—´
    cancelReason?: string; // ä½œåºŸåŽŸå› 
    remark?: string; // å¤‡æ³¨
    createTime?: string; // åˆ›å»ºæ—¶é—´
  }
@@ -45,6 +47,39 @@
    ids: number[]; // è¢‹ç ç¼–号列表
  }
  /** ç ´æŸè¢‹ç æ›¿æ¢è¯·æ±‚ */
  export interface ReplaceParams {
    batchId: number; // ç”Ÿäº§æ‰¹æ¬¡ç¼–号
    oldIds: number[]; // å¾…替换旧袋码编号列表
    reason: string; // æ›¿æ¢åŽŸå› 
  }
  /** ç ´æŸè¢‹ç æ›¿æ¢æ˜Žç»† */
  export interface ReplaceItem {
    oldBagCodeId?: number;
    oldCode?: string;
    oldBagNo?: number;
    oldStatus?: number;
    oldPalletId?: number;
    oldPalletCode?: string;
    newBagCodeId?: number;
    newCode?: string;
    newBagNo?: number;
    newPalletId?: number;
    newPalletCode?: string;
    reason?: string;
    replaceTime?: string;
  }
  /** ç ´æŸè¢‹ç æ›¿æ¢ç»“æžœ */
  export interface ReplaceResult {
    replaceUuid?: string;
    batchId?: number;
    batchCode?: string;
    count?: number;
    items?: ReplaceItem[];
  }
  /** æ‰«ç è¿½æº¯ä¿¡æ¯ */
  export interface BagCodeTrace {
    id?: number; // è¢‹ç ç¼–号
@@ -52,6 +87,11 @@
    bagNo?: number; // è¢‹åºå·
    status?: number; // è¢‹ç çŠ¶æ€
    printTime?: string; // å¯¼å‡ºå°åˆ·æ—¶é—´
    cancelTime?: string; // ä½œåºŸæ—¶é—´
    cancelReason?: string; // ä½œåºŸåŽŸå› 
    replacementUuid?: string; // æ›¿æ¢ç»„ UUID
    replacedByCode?: string; // æ›¿æ¢åŽçš„æ–°è¢‹ç 
    replacementOfCode?: string; // è¢«æ›¿æ¢çš„æ—§è¢‹ç 
    batchId?: number; // æ‰¹æ¬¡ç¼–号
    batchCode?: string; // æ‰¹æ¬¡å·
    batchStatus?: number; // æ‰¹æ¬¡çŠ¶æ€
@@ -89,6 +129,14 @@
  return requestClient.put('/mes/pro/bag-code/unbind-pallet', data);
}
/** ä½œåºŸç ´æŸè¢‹ç å¹¶ç”Ÿæˆç­‰é‡æ›¿æ¢è¢‹ç  */
export function replaceBagCodes(data: MesProBagCodeApi.ReplaceParams) {
  return requestClient.post<MesProBagCodeApi.ReplaceResult>(
    '/mes/pro/bag-code/replace',
    data,
  );
}
/** æŸ¥è¯¢è¢‹ç è¯¦æƒ… */
export function getBagCode(id: number) {
  return requestClient.get<MesProBagCodeApi.BagCode>(
src/views/mes/pro/bagCode/data.ts
@@ -115,6 +115,17 @@
      formatter: 'formatDateTime',
    },
    {
      field: 'cancelTime',
      title: '作废时间',
      width: 170,
      formatter: 'formatDateTime',
    },
    {
      field: 'cancelReason',
      title: '作废原因',
      minWidth: 160,
    },
    {
      field: 'createTime',
      title: '创建时间',
      width: 170,
src/views/mes/pro/bagCode/index.vue
@@ -6,7 +6,7 @@
  import { MesProBagCodeStatusEnum } from "@vben/constants";
  import { downloadFileFromBlobPart } from "@vben/utils";
  import { message } from "ant-design-vue";
  import { message, Modal } from "ant-design-vue";
  import { ACTION_ICON, TableAction, useVbenVxeGrid } from "#/adapter/vxe-table";
  import {
@@ -21,6 +21,7 @@
  import { useGridColumns, useGridFormSchema } from "./data";
  import BindPalletModal from "./modules/bind-pallet.vue";
  import GenerateModal from "./modules/generate-form.vue";
  import ReplaceModal from "./modules/replace-form.vue";
  import QrTraceModal from "../components/qr-trace-modal.vue";
  defineOptions({ name: "MesProBagCode" });
@@ -32,6 +33,11 @@
  const [BindPalletFormModal, bindPalletFormModalApi] = useVbenModal({
    connectedComponent: BindPalletModal,
    destroyOnClose: true,
  });
  const [ReplaceFormModal, replaceFormModalApi] = useVbenModal({
    connectedComponent: ReplaceModal,
    destroyOnClose: true,
  });
@@ -48,6 +54,12 @@
  /** ç”Ÿæˆæ•°æ®åŒ… */
  function handleGenerate() {
    generateFormModalApi.open();
  }
  /** æ›¿æ¢æˆåŠŸåŽå±•ç¤ºç»“æžœå¹¶åˆ·æ–°åˆ—è¡¨ */
  function handleReplaceSuccess(result: MesProBagCodeApi.ReplaceResult) {
    showReplaceResult(result);
    handleRefresh();
  }
  /** æ‰«ç è¿½æº¯ */
@@ -88,12 +100,82 @@
  /** èŽ·å–å‹¾é€‰çš„è¢‹ç è®°å½• */
  function getSelectedRecords(): MesProBagCodeApi.BagCode[] | undefined {
    const records = gridApi.grid.getCheckboxRecords() as MesProBagCodeApi.BagCode[];
    if (records.length === 0) {
    const records = [
      ...(gridApi.grid.getCheckboxReserveRecords?.() ?? []),
      ...(gridApi.grid.getCheckboxRecords?.() ?? []),
    ] as MesProBagCodeApi.BagCode[];
    const uniqueRecords = [
      ...new Map(records.map((record) => [record.id, record])).values(),
    ];
    if (uniqueRecords.length === 0) {
      message.warning("请至少勾选一条袋码记录");
      return undefined;
    }
    return records;
    return uniqueRecords;
  }
  /** å±•示替换结果 */
  function showReplaceResult(result: MesProBagCodeApi.ReplaceResult) {
    const items = result.items ?? [];
    const content = items.length
      ? items
          .map(
            (item) =>
              `${item.oldCode ?? "-"} â†’ ${item.newCode ?? "-"}${
                item.newPalletCode ? `(托盘:${item.newPalletCode})` : ""
              }`,
          )
          .join("\n")
      : "替换明细暂无";
    Modal.success({
      title: "破损袋码替换成功",
      content: `批次:${result.batchCode ?? "-"}\n替换数量:${
        result.count ?? items.length
      }\n${content}`,
      centered: true,
    });
  }
  /** æ‰¹é‡æ›¿æ¢ç ´æŸè¢‹ç  */
  async function handleReplace() {
    const records = getSelectedRecords();
    if (!records) return;
    if (records.length > 200) {
      message.warning("单次最多替换 200 ä¸ªè¢‹ç ");
      return;
    }
    const batchIds = new Set(records.map((record) => record.batchId));
    if (batchIds.size > 1 || !records[0]?.batchId) {
      message.warning("请选择同一批次下的袋码进行替换");
      return;
    }
    if (
      records.some(
        (record) =>
          record.status !== MesProBagCodeStatusEnum.GENERATED &&
          record.status !== MesProBagCodeStatusEnum.PRINTED,
      )
    ) {
      message.warning("仅已生成或已导出印刷的袋码允许替换");
      return;
    }
    if (records.some((record) => !record.id)) {
      message.warning("所选袋码缺少编号,无法替换");
      return;
    }
    try {
      await confirm(`确认作废所选 ${records.length} ä¸ªè¢‹ç å¹¶ç”Ÿæˆç­‰é‡æ–°ç å—?`);
    } catch {
      return;
    }
    const first = records[0]!;
    replaceFormModalApi
      .setData({
        batchId: first.batchId!,
        batchCode: first.batchCode,
        ids: records.map((record) => record.id!),
      })
      .open();
  }
  /** æ‰¹é‡ç»‘定托盘 */
@@ -200,6 +282,7 @@
  <Page auto-content-height>
    <GenerateFormModal @success="handleRefresh" />
    <BindPalletFormModal @success="handleRefresh" />
    <ReplaceFormModal @success="handleReplaceSuccess" />
    <QrTraceQueryModal />
    <Grid table-title="袋码列表(一袋一码)">
      <template #toolbar-tools>
@@ -218,6 +301,12 @@
              onClick: handleBindPallet,
            },
            {
              label: '破损替换',
              type: 'primary',
              auth: ['mes:pro-bag-code:replace'],
              onClick: handleReplace,
            },
            {
              label: '解绑托盘',
              auth: ['mes:pro-bag-code:update'],
              onClick: handleUnbindPallet,
src/views/mes/pro/bagCode/modules/replace-form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,105 @@
<script lang="ts" setup>
  import type { VbenFormSchema } from "#/adapter/form";
  import type { MesProBagCodeApi } from "#/api/mes/pro/bagCode";
  import { useVbenModal } from "@vben/common-ui";
  import { message } from "ant-design-vue";
  import { useVbenForm } from "#/adapter/form";
  import { replaceBagCodes } from "#/api/mes/pro/bagCode";
  defineOptions({ name: "MesProBagCodeReplaceForm" });
  interface ModalData {
    batchId: number;
    batchCode?: string;
    ids: number[];
  }
  const emit = defineEmits<{
    success: [result: MesProBagCodeApi.ReplaceResult];
  }>();
  const schema: VbenFormSchema[] = [
    {
      fieldName: "batchCode",
      label: "生产批次",
      component: "Input",
      componentProps: {
        disabled: true,
      },
    },
    {
      fieldName: "bagCount",
      label: "替换袋数",
      component: "InputNumber",
      componentProps: {
        class: "!w-full",
        disabled: true,
      },
    },
    {
      fieldName: "reason",
      label: "破损原因",
      component: "Textarea",
      formItemClass: "col-span-2",
      componentProps: {
        maxlength: 255,
        placeholder: "请输入破损原因",
        rows: 3,
        showCount: true,
      },
      rules: "required",
    },
  ];
  const [Form, formApi] = useVbenForm({
    commonConfig: {
      componentProps: {
        class: "w-full",
      },
      labelWidth: 100,
    },
    layout: "horizontal",
    schema,
    showDefaultActions: false,
  });
  const [Modal, modalApi] = useVbenModal({
    async onConfirm() {
      const { valid } = await formApi.validate();
      if (!valid) return;
      const data = modalApi.getData<ModalData>();
      const values = await formApi.getValues();
      modalApi.lock();
      try {
        const result = await replaceBagCodes({
          batchId: data.batchId,
          oldIds: data.ids,
          reason: values.reason,
        });
        message.success(`已成功替换 ${result.count ?? data.ids.length} ä¸ªè¢‹ç `);
        emit("success", result);
        await modalApi.close();
      } finally {
        modalApi.unlock();
      }
    },
    onOpenChange(isOpen: boolean) {
      if (!isOpen) return;
      const data = modalApi.getData<ModalData>();
      void formApi.setValues({
        bagCount: data.ids.length,
        batchCode: data.batchCode,
        reason: undefined,
      });
    },
  });
</script>
<template>
  <Modal title="破损袋码替换" class="w-2/5">
    <Form class="mx-4" />
  </Modal>
</template>
src/views/mes/pro/components/qr-trace-modal.vue
@@ -328,6 +328,26 @@
        field: "printTime",
        label: "导出印刷时间",
      },
      {
        field: "cancelTime",
        label: "作废时间",
      },
      {
        field: "cancelReason",
        label: "作废原因",
      },
      {
        field: "replacementUuid",
        label: "替换组 UUID",
      },
      {
        field: "replacedByCode",
        label: "替换后袋码",
      },
      {
        field: "replacementOfCode",
        label: "替换来源袋码",
      },
    ],
  });