7 小时以前 08bb5d242135e2c1f0fc17d2b5f7ae548275a3a2
feat(mes): 实现临时码预生成和补录功能

- 新增临时批次码和临时托盘码概念,支持预生成二维码
- 添加补录批次信息功能,将临时批次码转为正式批次号
- 实现托盘码绑定生产批次功能,生成正式托盘码
- 更新表单类型,增加fill类型用于临时码补录
- 修改二维码生成流程,分离预生成和正式录入步骤
- 增加扫码追溯对临时码的支持
- 更新表格列宽以适应新增的操作按钮
- 实现批量绑定批次功能
- 添加临时码状态标识和显示逻辑
已添加3个文件
已修改13个文件
757 ■■■■ 文件已修改
src/api/mes/pro/bagCode/index.ts 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pro/batch/index.ts 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pro/pallet/index.ts 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/bagCode/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/bagCode/index.vue 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/bagCode/modules/bind-batch.vue 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/bagCode/modules/generate-form.vue 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/batch/data.ts 21 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/batch/index.vue 67 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/batch/modules/form.vue 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/batch/modules/pre-generate.vue 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/components/qr-trace-modal.vue 44 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/pallet/data.ts 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/pallet/index.vue 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/pallet/modules/bind-batch.vue 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/pallet/modules/generate-form.vue 125 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pro/bagCode/index.ts
@@ -36,9 +36,17 @@
  /** ç”Ÿæˆæ•°æ®åŒ…请求 */
  export interface GenerateParams {
    batchId: number; // ç”Ÿäº§æ‰¹æ¬¡ç¼–号
    count: number; // ç”Ÿæˆè¢‹æ•°
    remark?: string; // å¤‡æ³¨
  }
  export interface GenerateResult {
    uuid?: string;
  }
  export interface BindBatchParams {
    batchId: number;
    ids: number[];
  }
  /** ç»‘定/解绑托盘请求 */
@@ -111,7 +119,15 @@
/** ç”Ÿæˆè¢‹ç æ•°æ®åŒ…(返回生成组 UUID) */
export function generateBagCodes(data: MesProBagCodeApi.GenerateParams) {
  return requestClient.post<string>('/mes/pro/bag-code/generate', data);
  return requestClient.post<MesProBagCodeApi.GenerateResult>(
    '/mes/pro/bag-code/generate',
    data,
  );
}
/** å½•入生产批次 */
export function bindBagCodeBatch(data: MesProBagCodeApi.BindBatchParams) {
  return requestClient.put('/mes/pro/bag-code/bind-batch', data);
}
/** åˆ é™¤è¢‹ç ç”Ÿæˆç»„ */
src/api/mes/pro/batch/index.ts
@@ -18,7 +18,8 @@
  /** ç”Ÿäº§æ‰¹æ¬¡ */
  export interface Batch {
    id?: number; // ç¼–号
    code?: string; // æ‰¹æ¬¡å·ï¼ˆè‡ªåŠ¨ç”Ÿæˆï¼‰
    code?: string; // æ‰¹æ¬¡å·ï¼ˆè¡¥å½•正式信息后自动生成)
    tempCode?: string; // ä¸´æ—¶æ‰¹æ¬¡ç ï¼ˆé¢„生成二维码阶段使用,扫码追溯兼容)
    itemId?: number; // å“ç§ï¼ˆäº§å“ç‰©æ–™ï¼‰ç¼–号
    itemCode?: string; // å“ç§ç¼–码(后端拼接)
    itemName?: string; // å“ç§åç§°ï¼ˆåŽç«¯æ‹¼æŽ¥ï¼‰
@@ -41,7 +42,8 @@
  /** ç”Ÿäº§æ‰¹æ¬¡æ‰«ç è¿½æº¯ */
  export interface BatchTrace {
    id?: number; // ç”Ÿäº§æ‰¹æ¬¡ç¼–号
    code?: string; // æ‰¹æ¬¡å·ï¼ˆäºŒç»´ç å†…容)
    code?: string; // æ‰¹æ¬¡å·ï¼ˆè¡¥å½•后的正式批次号)
    tempCode?: string; // ä¸´æ—¶æ‰¹æ¬¡ç ï¼ˆäºŒç»´ç å†…容;未补录信息时展示)
    status?: number; // æ‰¹æ¬¡çŠ¶æ€ï¼ˆå­—å…¸ mes_pro_batch_status)
    produceDate?: string; // ç”Ÿäº§æ—¥æœŸ
    lineCode?: string; // äº§çº¿ç¼–码
@@ -65,6 +67,7 @@
  /** ç”Ÿäº§æ‰¹æ¬¡åˆ†é¡µæŸ¥è¯¢å‚æ•° */
  export interface PageParams extends PageParam {
    code?: string;
    tempCode?: string;
    itemId?: number;
    lineId?: number;
    lineCode?: string;
@@ -73,6 +76,12 @@
    status?: number;
    produceDate?: string[];
    createTime?: string[];
  }
  /** é¢„生成批次二维码返回项 */
  export interface PreGenerateResult {
    id: number; // ç”Ÿäº§æ‰¹æ¬¡ç¼–号
    tempCode: string; // ä¸´æ—¶æ‰¹æ¬¡ç ï¼ˆäºŒç»´ç å†…容)
  }
}
@@ -91,6 +100,22 @@
  );
}
/** é¢„生成批次临时二维码(仅生成临时批次码,不录入业务信息) */
export function preGenerateBatch(data: {
  count: number;
  remark?: string;
}) {
  return requestClient.post<MesProBatchApi.PreGenerateResult[]>(
    '/mes/pro/batch/pre-generate',
    data,
  );
}
/** è¡¥å½•批次正式信息(生成正式批次号) */
export function fillBatchInfo(data: MesProBatchApi.Batch) {
  return requestClient.put('/mes/pro/batch/fill-info', data);
}
/** æ–°å¢žç”Ÿäº§æ‰¹æ¬¡ï¼ˆè‡ªåŠ¨ç”Ÿæˆæ‰¹æ¬¡å·ï¼‰ */
export function createBatch(data: MesProBatchApi.Batch) {
  return requestClient.post<number>('/mes/pro/batch/create', data);
src/api/mes/pro/pallet/index.ts
@@ -7,7 +7,8 @@
  /** æ‰˜ç›˜ç  */
  export interface Pallet {
    id?: number; // ç¼–号
    code?: string; // æ‰˜ç›˜ç 
    code?: string; // æ‰˜ç›˜ç ï¼ˆç»‘定批次后生成)
    tempCode?: string; // ä¸´æ—¶æ‰˜ç›˜ç ï¼ˆé¢„生成二维码阶段使用,扫码追溯兼容)
    batchId?: number; // ç”Ÿäº§æ‰¹æ¬¡ç¼–号
    batchCode?: string; // æ‰¹æ¬¡å·
    palletNo?: number; // æ‰¹æ¬¡å†…托盘序号
@@ -21,6 +22,7 @@
  /** æ‰˜ç›˜ç åˆ†é¡µæŸ¥è¯¢å‚æ•° */
  export interface PageParams extends PageParam {
    code?: string;
    tempCode?: string;
    batchId?: number;
    batchCode?: string;
    status?: number;
@@ -30,7 +32,8 @@
  /** æ‰˜ç›˜ç æ‰«ç è¿½æº¯ */
  export interface PalletTrace {
    id?: number; // ç¼–号
    code?: string; // æ‰˜ç›˜ç ï¼ˆäºŒç»´ç å†…容)
    code?: string; // æ‰˜ç›˜ç ï¼ˆç»‘定批次后的正式托盘码)
    tempCode?: string; // ä¸´æ—¶æ‰˜ç›˜ç ï¼ˆäºŒç»´ç å†…容;未绑定批次时展示)
    palletNo?: number; // æ‰¹æ¬¡å†…托盘序号
    capacity?: number; // å¯è£…袋数
    bagCount?: number; // å·²ç»‘定袋码数量
@@ -58,11 +61,34 @@
    capacity?: number; // å¯è£…袋数
    remark?: string; // å¤‡æ³¨
  }
  /** é¢„生成托盘二维码返回项 */
  export interface PreGenerateResult {
    id: number; // æ‰˜ç›˜ç ç¼–号
    tempCode: string; // ä¸´æ—¶æ‰˜ç›˜ç ï¼ˆäºŒç»´ç å†…容)
  }
}
/** æ‰¹é‡ç”Ÿæˆæ‰˜ç›˜ç ï¼ˆè¿”回生成的托盘编号列表) */
export function batchCreatePallet(data: MesProPalletApi.BatchCreateParams) {
  return requestClient.post<number[]>('/mes/pro/pallet/batch-create', data);
}
/** é¢„生成托盘临时二维码(不绑定生产批次) */
export function preGeneratePallet(data: {
  count: number;
  capacity?: number;
  remark?: string;
}) {
  return requestClient.post<MesProPalletApi.PreGenerateResult[]>(
    '/mes/pro/pallet/pre-generate',
    data,
  );
}
/** æ‰˜ç›˜ç ç»‘定生产批次(生成正式托盘码) */
export function bindPalletBatch(data: { ids: number[]; batchId: number }) {
  return requestClient.put('/mes/pro/pallet/bind-batch', data);
}
/** åˆ é™¤æ‰˜ç›˜ç  */
@@ -108,3 +134,11 @@
export function exportPalletQr(batchId: number) {
  return requestClient.download(`/mes/pro/pallet/export-qr?batchId=${batchId}`);
}
/** æŒ‰æ‰˜ç›˜ç ç¼–号列表批量导出二维码图片 ZIP */
export function exportPalletQrByIds(ids: number[]) {
  return requestClient.download('/mes/pro/pallet/export-qr-by-ids', {
    method: 'POST',
    data: { ids },
  });
}
src/views/mes/pro/bagCode/data.ts
@@ -133,7 +133,7 @@
    },
    {
      title: '操作',
      width: 200,
      width: 350,
      fixed: 'right',
      slots: { default: 'actions' },
    },
src/views/mes/pro/bagCode/index.vue
@@ -14,11 +14,13 @@
    exportBagCodeExcel,
    exportBagQr,
    getBagCodePage,
    bindBagCodeBatch,
    unbindBagCodePallet,
  } from "#/api/mes/pro/bagCode";
  import { $t } from "#/locales";
  import { useGridColumns, useGridFormSchema } from "./data";
  import BindBatchModal from "./modules/bind-batch.vue";
  import BindPalletModal from "./modules/bind-pallet.vue";
  import GenerateModal from "./modules/generate-form.vue";
  import ReplaceModal from "./modules/replace-form.vue";
@@ -28,6 +30,11 @@
  const [GenerateFormModal, generateFormModalApi] = useVbenModal({
    connectedComponent: GenerateModal,
    destroyOnClose: true,
  });
  const [BindBatchFormModal, bindBatchFormModalApi] = useVbenModal({
    connectedComponent: BindBatchModal,
    destroyOnClose: true,
  });
@@ -178,10 +185,25 @@
      .open();
  }
  /** æ‰¹é‡å½•入生产批次 */
  function handleBindBatch() {
    const records = getSelectedRecords();
    if (!records) return;
    if (records.some((record) => record.status !== MesProBagCodeStatusEnum.GENERATED || record.batchId)) {
      message.warning("请选择未录入批次且状态为已生成的袋码");
      return;
    }
    bindBatchFormModalApi.setData({ ids: records.map((record) => record.id!) }).open();
  }
  /** æ‰¹é‡ç»‘定托盘 */
  function handleBindPallet() {
    const records = getSelectedRecords();
    if (!records) return;
    if (records.some((record) => !record.batchId)) {
      message.warning("请先录入生产批次,再绑定托盘");
      return;
    }
    const batchIds = new Set(records.map((record) => record.batchId));
    if (batchIds.size > 1) {
      message.warning("请选择同一批次下的袋码进行绑定");
@@ -281,6 +303,7 @@
<template>
  <Page auto-content-height>
    <GenerateFormModal @success="handleRefresh" />
    <BindBatchFormModal @success="handleRefresh" />
    <BindPalletFormModal @success="handleRefresh" />
    <ReplaceFormModal @success="handleReplaceSuccess" />
    <QrTraceQueryModal />
@@ -295,6 +318,12 @@
              onClick: handleGenerate,
            },
            {
              label: '录入批次',
              type: 'primary',
              auth: ['mes:pro-bag-code:update'],
              onClick: handleBindBatch,
            },
            {
              label: '绑定托盘',
              type: 'primary',
              auth: ['mes:pro-bag-code:update'],
src/views/mes/pro/bagCode/modules/bind-batch.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,62 @@
<script lang="ts" setup>
  import type { VbenFormSchema } from "#/adapter/form";
  import { useVbenModal } from "@vben/common-ui";
  import { MesProBatchStatusEnum } from "@vben/constants";
  import { message } from "ant-design-vue";
  import { useVbenForm } from "#/adapter/form";
  import { bindBagCodeBatch } from "#/api/mes/pro/bagCode";
  import { getBatchSimpleList } from "#/api/mes/pro/batch";
  import { $t } from "#/locales";
  defineOptions({ name: "MesProBagCodeBindBatch" });
  interface ModalData { ids: number[] }
  const emit = defineEmits(["success"]);
  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 {
        await bindBagCodeBatch({ batchId: values.batchId, ids: data.ids });
        message.success($t("ui.actionMessage.operationSuccess"));
        emit("success");
        await modalApi.close();
      } finally { modalApi.unlock(); }
    },
    async onOpenChange(isOpen: boolean) {
      if (!isOpen) return;
      modalApi.lock();
      try {
        const batches = await getBatchSimpleList();
        // ä»…已补录正式信息(批次号非空)且生产中的批次允许录入袋码
        const options = batches
          .filter(
            (batch) =>
              batch.status === MesProBatchStatusEnum.PRODUCING && batch.code,
          )
          .map((batch) => ({ label: batch.code!, value: batch.id! }));
        const schema: VbenFormSchema[] = [{
          fieldName: "batchId", label: "生产批次", component: "Select",
          componentProps: { options, optionFilterProp: "label", placeholder: "请选择生产中的批次", showSearch: true },
          rules: "selectRequired",
        }];
        await formApi.setState({ schema });
      } finally { modalApi.unlock(); }
    },
  });
</script>
<template>
  <Modal title="录入生产批次" class="w-2/5"><Form class="mx-4" /></Modal>
</template>
src/views/mes/pro/bagCode/modules/generate-form.vue
@@ -3,33 +3,18 @@
  import type { MesProBagCodeApi } from "#/api/mes/pro/bagCode";
  import { useVbenModal } from "@vben/common-ui";
  import { MesProBatchStatusEnum } from "@vben/constants";
  import { message } from "ant-design-vue";
  import { useVbenForm } from "#/adapter/form";
  import { generateBagCodes } from "#/api/mes/pro/bagCode";
  import { getBatchSimpleList } from "#/api/mes/pro/batch";
  defineOptions({ name: "MesProBagCodeGenerateForm" });
  const emit = defineEmits(["success"]);
  /** æž„建表单(批次下拉选项由外部加载注入) */
  function buildSchema(batchOptions: { label: string; value: number }[]): VbenFormSchema[] {
  function buildSchema(): VbenFormSchema[] {
    return [
      {
        fieldName: "batchId",
        label: "生产批次",
        component: "Select",
        componentProps: {
          options: batchOptions,
          optionFilterProp: "label",
          placeholder: "请选择生产中的批次",
          showSearch: true,
        },
        rules: "selectRequired",
      },
      {
        fieldName: "count",
        label: "生成袋数",
@@ -86,17 +71,7 @@
      if (!isOpen) return;
      modalApi.lock();
      try {
        // ä»…生产中的批次允许生成袋码
        const batches = await getBatchSimpleList();
        const options = batches
          .filter(
            (batch) => batch.status === MesProBatchStatusEnum.PRODUCING
          )
          .map((batch) => ({
            label: batch.code!,
            value: batch.id!,
          }));
        await formApi.setState({ schema: buildSchema(options) });
        await formApi.setState({ schema: buildSchema() });
      } finally {
        modalApi.unlock();
      }
src/views/mes/pro/batch/data.ts
@@ -12,8 +12,8 @@
import { UserSelect } from '#/views/system/user/components';
import { getRangePickerDefaultProps } from '#/utils';
/** è¡¨å•类型 */
export type FormType = 'create' | 'detail' | 'update';
/** è¡¨å•类型(fill:临时批次码补录正式信息) */
export type FormType = 'create' | 'detail' | 'update' | 'fill';
/** æ–°å¢ž/修改的表单 */
export function useFormSchema(formType: FormType): VbenFormSchema[] {
@@ -33,11 +33,24 @@
      component: 'Input',
      componentProps: {
        disabled: true,
        placeholder: '保存后自动生成',
        placeholder: '补录后自动生成',
      },
      dependencies: {
        triggerFields: [''],
        show: () => formType !== 'create',
      },
    },
    {
      fieldName: 'tempCode',
      label: '临时批次码',
      component: 'Input',
      componentProps: {
        disabled: true,
        placeholder: '预生成二维码阶段使用的临时码',
      },
      dependencies: {
        triggerFields: [''],
        show: () => formType === 'fill',
      },
    },
    {
@@ -273,7 +286,7 @@
    },
    {
      title: '操作',
      width: 300,
      width: 350,
      fixed: 'right',
      slots: { default: 'actions' },
    },
src/views/mes/pro/batch/index.vue
@@ -6,7 +6,7 @@
  import { MesProBatchStatusEnum } from "@vben/constants";
  import { downloadFileFromBlobPart } from "@vben/utils";
  import { Button, message } from "ant-design-vue";
  import { Button, message, Modal, Tag } from "ant-design-vue";
  import { ACTION_ICON, TableAction, useVbenVxeGrid } from "#/adapter/vxe-table";
  import {
@@ -20,10 +20,16 @@
  import { useGridColumns, useGridFormSchema } from "./data";
  import Form from "./modules/form.vue";
  import PreGenerateModal from "./modules/pre-generate.vue";
  import QrTraceModal from "../components/qr-trace-modal.vue";
  const [FormModal, formModalApi] = useVbenModal({
    connectedComponent: Form,
    destroyOnClose: true,
  });
  const [PreGenerateModalApi, preGenerateModalApi] = useVbenModal({
    connectedComponent: PreGenerateModal,
    destroyOnClose: true,
  });
@@ -42,23 +48,47 @@
    qrTraceQueryModalApi.open();
  }
  /** æŸ¥çœ‹äºŒç»´ç ï¼ˆè¡Œå†…预填码值) */
  /** èŽ·å–æ‰¹æ¬¡æ˜¾ç¤ºç”¨çš„ç å€¼ï¼ˆæ­£å¼æ‰¹æ¬¡å·ä¼˜å…ˆï¼Œå…¶æ¬¡ä¸´æ—¶æ‰¹æ¬¡ç ï¼‰ */
  function getDisplayCode(row: MesProBatchApi.Batch) {
    return row.code || row.tempCode || "";
  }
  /** æŸ¥çœ‹äºŒç»´ç ï¼ˆè¡Œå†…预填码值,临时批次用临时批次码) */
  function handleViewQr(row: MesProBatchApi.Batch) {
    qrTraceQueryModalApi.setData({ code: row.code }).open();
    qrTraceQueryModalApi.setData({ code: getDisplayCode(row) }).open();
  }
  /** æ‰“印整套二维码(批次图 + å…¨éƒ¨æ‰˜ç›˜å›¾ ZIP) */
  async function handlePrintQrSet(row: MesProBatchApi.Batch) {
    const data = await exportBatchQr(row.id!);
    downloadFileFromBlobPart({
      fileName: `批次二维码-${row.code}.zip`,
      fileName: `批次二维码-${getDisplayCode(row)}.zip`,
      source: data,
    });
  }
  /** é¢„生成批次二维码成功:展示生成的临时批次码 */
  function handlePreGenerateSuccess(result: { id: number; tempCode: string }[]) {
    Modal.success({
      title: "预生成临时批次码成功",
      content: result.map((item) => item.tempCode).join("\n"),
      centered: true,
    });
  }
  /** æ–°å¢žæ‰¹æ¬¡ */
  function handleCreate() {
    formModalApi.setData({ formType: "create" }).open();
  }
  /** ä¸´æ—¶æ‰¹æ¬¡è¡¥å½•正式信息 */
  function handleFillInfo(row: MesProBatchApi.Batch) {
    formModalApi.setData({ formType: "fill", id: row.id }).open();
  }
  /** é¢„生成批次二维码 */
  function handlePreGenerate() {
    preGenerateModalApi.open();
  }
  /** æŸ¥çœ‹è¯¦æƒ… */
@@ -74,7 +104,7 @@
  /** åˆ é™¤æ‰¹æ¬¡ */
  async function handleDelete(row: MesProBatchApi.Batch) {
    await deleteBatch(row.id!);
    message.success($t("ui.actionMessage.deleteSuccess", [row.code]));
    message.success($t("ui.actionMessage.deleteSuccess", [getDisplayCode(row)]));
    handleRefresh();
  }
@@ -129,10 +159,17 @@
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <PreGenerateModalApi @success="handlePreGenerateSuccess" />
    <QrTraceQueryModal />
    <Grid table-title="生产批次列表">
      <template #toolbar-tools>
        <TableAction :actions="[
            {
              label: '预生成二维码',
              icon: ACTION_ICON.ADD,
              auth: ['mes:pro-batch:create'],
              onClick: handlePreGenerate,
            },
            {
              label: $t('ui.actionTitle.create', ['批次']),
              type: 'primary',
@@ -157,8 +194,9 @@
      </template>
      <template #code="{ row }">
        <Button type="link" @click="handleDetail(row)">
          {{ row.code }}
          {{ getDisplayCode(row) }}
        </Button>
        <Tag v-if="!row.code" color="warning">待补录</Tag>
      </template>
      <template #actions="{ row }">
        <TableAction :actions="[
@@ -169,13 +207,22 @@
              onClick: handleViewQr.bind(null, row),
            },
            {
              label: '补录信息',
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['mes:pro-batch:update'],
              ifShow: !row.code,
              onClick: handleFillInfo.bind(null, row),
            },
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['mes:pro-batch:update'],
              ifShow:
                row.status === MesProBatchStatusEnum.DRAFT ||
                row.status === MesProBatchStatusEnum.PRODUCING,
                (row.status === MesProBatchStatusEnum.DRAFT ||
                  row.status === MesProBatchStatusEnum.PRODUCING) &&
                !!row.code,
              onClick: handleEdit.bind(null, row),
            },
            {
@@ -188,7 +235,7 @@
                row.status === MesProBatchStatusEnum.DRAFT ||
                row.status === MesProBatchStatusEnum.CANCELED,
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [row.code]),
                title: $t('ui.actionMessage.deleteConfirm', [getDisplayCode(row)]),
                confirm: handleDelete.bind(null, row),
              },
            },
@@ -197,7 +244,7 @@
            {
              label: '开始生产',
              auth: ['mes:pro-batch:update'],
              ifShow: row.status === MesProBatchStatusEnum.DRAFT,
              ifShow: row.status === MesProBatchStatusEnum.DRAFT && !!row.code,
              popConfirm: {
                title: '确认开始生产该批次吗?',
                confirm: () =>
src/views/mes/pro/batch/modules/form.vue
@@ -15,7 +15,12 @@
  import { useVbenForm } from "#/adapter/form";
  import { TableAction, useVbenVxeGrid } from "#/adapter/vxe-table";
  import { createBatch, getBatch, updateBatch } from "#/api/mes/pro/batch";
  import {
    createBatch,
    fillBatchInfo,
    getBatch,
    updateBatch,
  } from "#/api/mes/pro/batch";
  import { $t } from "#/locales";
  import { UserSelect } from "#/views/system/user/components";
@@ -33,7 +38,7 @@
  const formData = ref<MesProBatchApi.Batch>();
  const isEditable = computed(() =>
    ["create", "update"].includes(formType.value)
    ["create", "update", "fill"].includes(formType.value)
  );
  const getTitle = computed(() => {
@@ -43,6 +48,9 @@
      }
      case "update": {
        return $t("ui.actionTitle.edit", ["生产批次"]);
      }
      case "fill": {
        return "补录批次信息";
      }
      default: {
        return $t("ui.actionTitle.create", ["生产批次"]);
@@ -177,7 +185,9 @@
      modalApi.lock();
      try {
        const data = (await formApi.getValues()) as MesProBatchApi.Batch;
        if (formData.value?.id) {
        if (formType.value === "fill") {
          await fillBatchInfo({ ...data, id: formData.value!.id, workers });
        } else if (formData.value?.id) {
          await updateBatch({ ...data, id: formData.value.id, workers });
        } else {
          await createBatch({ ...data, workers });
src/views/mes/pro/batch/modules/pre-generate.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,80 @@
<script lang="ts" setup>
  import type { VbenFormSchema } from "#/adapter/form";
  import type { MesProBatchApi } from "#/api/mes/pro/batch";
  import { useVbenModal } from "@vben/common-ui";
  import { message } from "ant-design-vue";
  import { useVbenForm } from "#/adapter/form";
  import { preGenerateBatch } from "#/api/mes/pro/batch";
  defineOptions({ name: "MesProBatchPreGenerate" });
  const emit = defineEmits(["success"]);
  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;
      modalApi.lock();
      try {
        const values = await formApi.getValues();
        const result = await preGenerateBatch({
          count: values.count,
          remark: values.remark,
        });
        message.success(`已生成 ${result.length} ä¸ªä¸´æ—¶æ‰¹æ¬¡ç `);
        emit("success", result);
        await modalApi.close();
      } finally {
        modalApi.unlock();
      }
    },
    onOpenChange(isOpen: boolean) {
      if (!isOpen) return;
      const schema: VbenFormSchema[] = [
        {
          fieldName: "count",
          label: "生成个数",
          component: "InputNumber",
          componentProps: {
            class: "!w-full",
            min: 1,
            placeholder: "请输入需要生成的批次个数",
            precision: 0,
          },
          rules: "required",
        },
        {
          fieldName: "remark",
          label: "备注",
          component: "Textarea",
          componentProps: {
            placeholder: "请输入备注",
            rows: 2,
          },
        },
      ];
      formApi.setState({ schema });
    },
  });
</script>
<template>
  <Modal title="预生成批次二维码" class="w-2/5">
    <Form class="mx-4" />
  </Modal>
</template>
src/views/mes/pro/components/qr-trace-modal.vue
@@ -35,13 +35,19 @@
  const traceTypeLabel = ref("");
  /** è¯†åˆ«äºŒç»´ç å†…容类型:批次号 / æ‰˜ç›˜ç  / è¢‹ç  */
  /** è¯†åˆ«äºŒç»´ç å†…容类型:批次号(含临时批次码)/ æ‰˜ç›˜ç ï¼ˆå«ä¸´æ—¶æ‰˜ç›˜ç ï¼‰/ è¢‹ç  */
  function resolveCodeType(raw: string): TraceType | undefined {
    const value = raw.trim().toUpperCase();
    if (/^PB-\d{8}$/.test(value)) {
      return "batch";
    }
    if (/^PL-\d{8}$/.test(value)) {
      return "pallet";
    }
    if (/^.*-P\d{2}$/.test(value)) {
      return "pallet";
    }
    if (/^.*-\d{4}$/.test(value)) {
    if (/^BAG-\d{8}$/.test(value) || /^.*-\d{4}$/.test(value)) {
      return "bag";
    }
    return "batch";
@@ -125,7 +131,7 @@
    if (traceType.value === "pallet" && palletTrace.value) {
      const d = palletTrace.value;
      return [
        { label: "托盘码", value: d.code || "" },
        { label: "托盘码", value: d.code || d.tempCode || "" },
        { label: "托盘序号", value: d.palletNo ? String(d.palletNo) : "" },
        { label: "批次号", value: d.batchCode || "" },
        { label: "品种", value: d.itemName || "" },
@@ -141,7 +147,7 @@
    if (batchTrace.value) {
      const d = batchTrace.value;
      return [
        { label: "批次号", value: d.code || "" },
        { label: "批次号", value: d.code || d.tempCode || "" },
        { label: "品种", value: d.itemName || "" },
        { label: "规格", value: d.itemSpecification || "" },
        {
@@ -357,7 +363,12 @@
    schema: [
      {
        field: "code",
        label: "托盘码(二维码内容)",
        label: "托盘码(正式托盘码)",
        span: 2,
      },
      {
        field: "tempCode",
        label: "临时托盘码(未绑定批次)",
        span: 2,
      },
      {
@@ -419,7 +430,12 @@
    schema: [
      {
        field: "code",
        label: "批次号(二维码内容)",
        label: "批次号(正式批次号)",
        span: 2,
      },
      {
        field: "tempCode",
        label: "临时批次码(待补录)",
        span: 2,
      },
      {
@@ -502,6 +518,22 @@
              <p class="text-lg font-bold break-all">{{ currentCode }}</p>
              <p class="mt-1 text-xs text-gray-500">
                ç ç±»åž‹ï¼š{{ traceTypeLabel }}
                <Tag
                  v-if="traceType === 'batch' && batchTrace && !batchTrace.code"
                  color="warning"
                  class="ml-2"
                >
                  å¾…补录
                </Tag>
                <Tag
                  v-else-if="
                    traceType === 'pallet' && palletTrace && !palletTrace.code
                  "
                  color="warning"
                  class="ml-2"
                >
                  æœªç»‘定批次
                </Tag>
              </p>
            </div>
          </div>
src/views/mes/pro/pallet/data.ts
@@ -53,11 +53,13 @@
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions<MesProPalletApi.Pallet>['columns'] {
  return [
    { type: 'checkbox', width: 50 },
    {
      field: 'code',
      title: '托盘码',
      fixed: 'left',
      minWidth: 210,
      slots: { default: 'code' },
    },
    {
      field: 'palletNo',
@@ -68,6 +70,7 @@
      field: 'batchCode',
      title: '批次号',
      minWidth: 190,
      slots: { default: 'batchCode' },
    },
    {
      field: 'capacity',
@@ -101,7 +104,7 @@
    },
    {
      title: '操作',
      width: 200,
      width: 350,
      fixed: 'right',
      slots: { default: 'actions' },
    },
src/views/mes/pro/pallet/index.vue
@@ -5,18 +5,20 @@
  import { Page, useVbenModal } from "@vben/common-ui";
  import { downloadFileFromBlobPart } from "@vben/utils";
  import { message } from "ant-design-vue";
  import { message, Modal, Tag } from "ant-design-vue";
  import { ACTION_ICON, TableAction, useVbenVxeGrid } from "#/adapter/vxe-table";
  import {
    deletePallet,
    exportPalletExcel,
    exportPalletQr,
    exportPalletQrByIds,
    getPalletPage,
  } from "#/api/mes/pro/pallet";
  import { $t } from "#/locales";
  import { useGridColumns, useGridFormSchema } from "./data";
  import BindBatchModal from "./modules/bind-batch.vue";
  import GenerateModal from "./modules/generate-form.vue";
  import QrTraceModal from "../components/qr-trace-modal.vue";
@@ -24,6 +26,11 @@
  const [GenerateFormModal, generateFormModalApi] = useVbenModal({
    connectedComponent: GenerateModal,
    destroyOnClose: true,
  });
  const [BindBatchFormModal, bindBatchFormModalApi] = useVbenModal({
    connectedComponent: BindBatchModal,
    destroyOnClose: true,
  });
@@ -37,9 +44,18 @@
    gridApi.query();
  }
  /** æ‰¹é‡ç”Ÿæˆæ‰˜ç›˜ç  */
  /** æ‰¹é‡é¢„生成托盘码 */
  function handleCreate() {
    generateFormModalApi.open();
  }
  /** é¢„生成成功:展示生成的临时托盘码 */
  function handlePreGenerateSuccess(result: { id: number; tempCode: string }[]) {
    Modal.success({
      title: "预生成临时托盘码成功",
      content: result.map((item) => item.tempCode).join("\n"),
      centered: true,
    });
  }
  /** æ‰«ç è¿½æº¯ */
@@ -47,9 +63,44 @@
    qrTraceQueryModalApi.open();
  }
  /** æŸ¥çœ‹äºŒç»´ç ï¼ˆè¡Œå†…预填码值) */
  /** èŽ·å–æ‰˜ç›˜æ˜¾ç¤ºç”¨çš„ç å€¼ï¼ˆæ­£å¼æ‰˜ç›˜ç ä¼˜å…ˆï¼Œå…¶æ¬¡ä¸´æ—¶æ‰˜ç›˜ç ï¼‰ */
  function getDisplayCode(row: MesProPalletApi.Pallet) {
    return row.code || row.tempCode || "";
  }
  /** æŸ¥çœ‹äºŒç»´ç ï¼ˆè¡Œå†…预填码值,临时托盘用临时托盘码) */
  function handleViewQr(row: MesProPalletApi.Pallet) {
    qrTraceQueryModalApi.setData({ code: row.code }).open();
    qrTraceQueryModalApi.setData({ code: getDisplayCode(row) }).open();
  }
  /** èŽ·å–å‹¾é€‰çš„ä¸´æ—¶æ‰˜ç›˜è®°å½• */
  function getSelectedTempPallets(): MesProPalletApi.Pallet[] | undefined {
    const records = [
      ...(gridApi.grid.getCheckboxReserveRecords?.() ?? []),
      ...(gridApi.grid.getCheckboxRecords?.() ?? []),
    ] as MesProPalletApi.Pallet[];
    const uniqueRecords = [
      ...new Map(records.map((record) => [record.id, record])).values(),
    ];
    const tempRecords = uniqueRecords.filter((record) => !record.batchId);
    if (uniqueRecords.length === 0) {
      message.warning("请至少勾选一条托盘码记录");
      return undefined;
    }
    if (tempRecords.length === 0) {
      message.warning("勾选的记录均已绑定批次,无需绑定");
      return undefined;
    }
    return tempRecords;
  }
  /** ç»‘定批次 */
  function handleBindBatch() {
    const records = getSelectedTempPallets();
    if (!records) return;
    bindBatchFormModalApi
      .setData({ ids: records.map((record) => record.id!) })
      .open();
  }
  /** å¯¼å‡ºäºŒç»´ç  ZIP(按当前查询条件的批次) */
@@ -63,10 +114,19 @@
    downloadFileFromBlobPart({ fileName: "托盘码二维码.zip", source: data });
  }
  /** å¯¼å‡ºå•个托盘码二维码 ZIP(支持未绑定批次的临时托盘) */
  async function handleExportQrById(row: MesProPalletApi.Pallet) {
    const data = await exportPalletQrByIds([row.id!]);
    downloadFileFromBlobPart({
      fileName: `托盘码二维码-${getDisplayCode(row)}.zip`,
      source: data,
    });
  }
  /** åˆ é™¤æ‰˜ç›˜ç  */
  async function handleDelete(row: MesProPalletApi.Pallet) {
    await deletePallet(row.id!);
    message.success($t("ui.actionMessage.deleteSuccess", [row.code]));
    message.success($t("ui.actionMessage.deleteSuccess", [getDisplayCode(row)]));
    handleRefresh();
  }
@@ -81,6 +141,10 @@
      schema: useGridFormSchema(),
    },
    gridOptions: {
      checkboxConfig: {
        highlight: true,
        reserve: true,
      },
      columns: useGridColumns(),
      height: "auto",
      keepSource: true,
@@ -109,17 +173,24 @@
<template>
  <Page auto-content-height>
    <GenerateFormModal @success="handleRefresh" />
    <GenerateFormModal @success="handlePreGenerateSuccess" />
    <BindBatchFormModal @success="handleRefresh" />
    <QrTraceQueryModal />
    <Grid table-title="托盘码列表">
      <template #toolbar-tools>
        <TableAction :actions="[
            {
              label: '批量生成',
              label: '预生成二维码',
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['mes:pro-pallet:create'],
              onClick: handleCreate,
            },
            {
              label: '绑定批次',
              icon: ACTION_ICON.EDIT,
              auth: ['mes:pro-pallet:update'],
              onClick: handleBindBatch,
            },
            {
              label: '扫码追溯',
@@ -142,6 +213,14 @@
            },
          ]" />
      </template>
      <template #code="{ row }">
        <span>{{ getDisplayCode(row) }}</span>
        <Tag v-if="!row.code" color="warning">未绑定批次</Tag>
      </template>
      <template #batchCode="{ row }">
        <span v-if="row.batchCode">{{ row.batchCode }}</span>
        <Tag v-else color="warning">未绑定批次</Tag>
      </template>
      <template #actions="{ row }">
        <TableAction :actions="[
            {
@@ -151,6 +230,13 @@
              onClick: handleViewQr.bind(null, row),
            },
            {
              label: '导出二维码',
              type: 'link',
              icon: ACTION_ICON.DOWNLOAD,
              auth: ['mes:pro-pallet:export'],
              onClick: handleExportQrById.bind(null, row),
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
@@ -158,7 +244,7 @@
              auth: ['mes:pro-pallet:delete'],
              ifShow: !row.bagCount,
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [row.code]),
                title: $t('ui.actionMessage.deleteConfirm', [getDisplayCode(row)]),
                confirm: handleDelete.bind(null, row),
              },
            },
src/views/mes/pro/pallet/modules/bind-batch.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,88 @@
<script lang="ts" setup>
  import type { VbenFormSchema } from "#/adapter/form";
  import { useVbenModal } from "@vben/common-ui";
  import { MesProBatchStatusEnum } from "@vben/constants";
  import { message } from "ant-design-vue";
  import { useVbenForm } from "#/adapter/form";
  import { getBatchSimpleList } from "#/api/mes/pro/batch";
  import { bindPalletBatch } from "#/api/mes/pro/pallet";
  import { $t } from "#/locales";
  defineOptions({ name: "MesProPalletBindBatch" });
  interface ModalData {
    ids: number[];
  }
  const emit = defineEmits(["success"]);
  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 {
        await bindPalletBatch({ batchId: values.batchId, ids: data.ids });
        message.success($t("ui.actionMessage.operationSuccess"));
        emit("success");
        await modalApi.close();
      } finally {
        modalApi.unlock();
      }
    },
    async onOpenChange(isOpen: boolean) {
      if (!isOpen) return;
      modalApi.lock();
      try {
        // ä»…已补录正式信息且生产中的批次允许绑定
        const batches = await getBatchSimpleList();
        const options = batches
          .filter(
            (batch) =>
              batch.status === MesProBatchStatusEnum.PRODUCING && batch.code,
          )
          .map((batch) => ({ label: batch.code!, value: batch.id! }));
        const schema: VbenFormSchema[] = [
          {
            fieldName: "batchId",
            label: "生产批次",
            component: "Select",
            componentProps: {
              options,
              optionFilterProp: "label",
              placeholder: "请选择已补录且生产中的批次",
              showSearch: true,
            },
            rules: "selectRequired",
          },
        ];
        await formApi.setState({ schema });
      } finally {
        modalApi.unlock();
      }
    },
  });
</script>
<template>
  <Modal title="托盘码绑定生产批次" class="w-2/5">
    <Form class="mx-4" />
  </Modal>
</template>
src/views/mes/pro/pallet/modules/generate-form.vue
@@ -1,69 +1,16 @@
<script lang="ts" setup>
  import type { VbenFormSchema } from "#/adapter/form";
  import type { MesProPalletApi } from "#/api/mes/pro/pallet";
  import { useVbenModal } from "@vben/common-ui";
  import { MesProBatchStatusEnum } from "@vben/constants";
  import { message } from "ant-design-vue";
  import { useVbenForm } from "#/adapter/form";
  import { getBatchSimpleList } from "#/api/mes/pro/batch";
  import { batchCreatePallet } from "#/api/mes/pro/pallet";
  import { preGeneratePallet } from "#/api/mes/pro/pallet";
  defineOptions({ name: "MesProPalletGenerateForm" });
  const emit = defineEmits(["success"]);
  /** æž„建表单(批次下拉选项由外部加载注入) */
  function buildSchema(batchOptions: { label: string; value: number }[]): VbenFormSchema[] {
    return [
      {
        fieldName: "batchId",
        label: "生产批次",
        component: "Select",
        componentProps: {
          options: batchOptions,
          optionFilterProp: "label",
          placeholder: "请选择生产中的批次",
          showSearch: true,
        },
        rules: "selectRequired",
      },
      {
        fieldName: "count",
        label: "生成数量",
        component: "InputNumber",
        componentProps: {
          class: "!w-full",
          min: 1,
          placeholder: "请输入需要生成的托盘数量",
          precision: 0,
        },
        rules: "required",
      },
      {
        fieldName: "capacity",
        label: "可装袋数",
        component: "InputNumber",
        componentProps: {
          class: "!w-full",
          min: 1,
          placeholder: "请输入每托盘可装袋数(袋/托盘)",
          precision: 0,
        },
      },
      {
        fieldName: "remark",
        label: "备注",
        component: "Textarea",
        componentProps: {
          placeholder: "请输入备注",
          rows: 2,
        },
      },
    ];
  }
  const [Form, formApi] = useVbenForm({
    commonConfig: {
@@ -83,40 +30,62 @@
      if (!valid) return;
      modalApi.lock();
      try {
        const values =
          (await formApi.getValues()) as MesProPalletApi.BatchCreateParams;
        const ids = await batchCreatePallet(values);
        message.success(`已成功生成 ${ids.length} ä¸ªæ‰˜ç›˜ç `);
        emit("success");
        const values = await formApi.getValues();
        const result = await preGeneratePallet({
          count: values.count,
          capacity: values.capacity,
          remark: values.remark,
        });
        message.success(`已生成 ${result.length} ä¸ªä¸´æ—¶æ‰˜ç›˜ç `);
        emit("success", result);
        await modalApi.close();
      } finally {
        modalApi.unlock();
      }
    },
    async onOpenChange(isOpen: boolean) {
    onOpenChange(isOpen: boolean) {
      if (!isOpen) return;
      modalApi.lock();
      try {
        // ä»…生产中的批次允许生成托盘码
        const batches = await getBatchSimpleList();
        const options = batches
          .filter(
            (batch) => batch.status === MesProBatchStatusEnum.PRODUCING
          )
          .map((batch) => ({
            label: batch.code!,
            value: batch.id!,
          }));
        await formApi.setState({ schema: buildSchema(options) });
      } finally {
        modalApi.unlock();
      }
      const schema: VbenFormSchema[] = [
        {
          fieldName: "count",
          label: "生成数量",
          component: "InputNumber",
          componentProps: {
            class: "!w-full",
            min: 1,
            placeholder: "请输入需要生成的托盘数量",
            precision: 0,
          },
          rules: "required",
        },
        {
          fieldName: "capacity",
          label: "可装袋数",
          component: "InputNumber",
          componentProps: {
            class: "!w-full",
            min: 1,
            placeholder: "请输入每托盘可装袋数(袋/托盘)",
            precision: 0,
          },
        },
        {
          fieldName: "remark",
          label: "备注",
          component: "Textarea",
          componentProps: {
            placeholder: "请输入备注",
            rows: 2,
          },
        },
      ];
      formApi.setState({ schema });
    },
  });
</script>
<template>
  <Modal title="批量生成托盘码" class="w-2/5">
  <Modal title="预生成托盘二维码" class="w-2/5">
    <Form class="mx-4" />
  </Modal>
</template>
</template>