src/views/mes/pro/bagCode/index.vue
@@ -6,17 +6,20 @@
  import { MesProBagCodeStatusEnum } from "@vben/constants";
  import { downloadFileFromBlobPart } from "@vben/utils";
  import { message, Modal } from "ant-design-vue";
  import { Button, message, Modal } from "ant-design-vue";
  import { ACTION_ICON, TableAction, useVbenVxeGrid } from "#/adapter/vxe-table";
  import {
    deleteBagCodesByUuid,
    exportBagCodeExcel,
    exportBagQr,
    exportBagBusinessQr,
    generateBagBusinessCodes,
    getBagCodePage,
    bindBagCodeBatch,
    unbindBagCodePallet,
  } from "#/api/mes/pro/bagCode";
  import { IconifyIcon } from "@vben/icons";
  import { $t } from "#/locales";
  import { useGridColumns, useGridFormSchema } from "./data";
@@ -26,6 +29,9 @@
  import RawGenerateModal from "./modules/raw-generate-form.vue";
  import ReplaceModal from "./modules/replace-form.vue";
  import QrTraceModal from "../components/qr-trace-modal.vue";
  import BusinessQrModal from "../components/business-qr.vue";
  import QrExportProgress from "../qrexport/qr-export-progress.vue";
  import { useQrExport } from "../qrexport/use-qr-export";
  defineOptions({ name: "MesProBagCode" });
@@ -59,10 +65,35 @@
    destroyOnClose: true,
  });
  const [BusinessQrViewModal, businessQrApi] = useVbenModal({
    connectedComponent: BusinessQrModal, destroyOnClose: true,
  });
  function handleBusinessQr(row: MesProBagCodeApi.BagCode) {
    if (!row.businessCode) { message.warning('请先补生成业务码'); return; }
    businessQrApi.setData({ code: row.businessCode }).open();
  }
  async function handleGenerateBusinessCodes() {
    const records = getSelectedRecords();
    if (!records) return;
    if (records.length > 200) { message.warning('单次最多补生成 200 条'); return; }
    if (records.some(row => row.status === MesProBagCodeStatusEnum.CANCELED)) {
      message.warning('已作废袋码不可补生成业务码'); return;
    }
    const ids = records.map(row => row.id).filter((id): id is number => id != null);
    if (ids.length !== records.length) { message.warning('所选袋码缺少编号'); return; }
    const count = await generateBagBusinessCodes(ids);
    message.success(`已补生成 ${count} 份业务码,已有码值保持不变`);
    handleRefresh();
  }
  /** 刷新表格 */
  function handleRefresh() {
    gridApi.query();
  }
  const { start: startQrExport } = useQrExport();
  /** 生成数据包 */
  function handleGenerate() {
@@ -91,7 +122,7 @@
  }
  /** 导出二维码 ZIP(按当前查询条件的生成组/批次) */
  async function handleExportQr() {
  async function handleExportQr(kind: 'trace' | 'business' = 'trace') {
    const values = await gridApi.formApi.getValues();
    const params: { uuid?: string; batchId?: number } = {};
    if (values.uuid) {
@@ -104,8 +135,24 @@
      message.warning("请先按生成组或批次筛选后再导出二维码");
      return;
    }
    const data = await exportBagQr(params);
    downloadFileFromBlobPart({ fileName: "袋码二维码.zip", source: data });
    // 数量少则同步秒出;超过分界线自动转后台任务,由进度浮层展示真实进度并支持取消/隐藏
    const bizType = kind === "business" ? "BAG_BUSINESS" : "BAG_LABEL";
    const fileName = kind === "business" ? "袋码业务二维码" : "袋码追溯二维码";
    await startQrExport(
      { bizType, ...params },
      {
        fileName,
        syncDownload: async () => {
          const data = await (kind === "business"
            ? exportBagBusinessQr
            : exportBagQr)(params);
          downloadFileFromBlobPart({
            fileName: `${fileName}.zip`,
            source: data,
          });
        },
      },
    );
  }
  /** 导出袋码 Excel(交付印刷),导出后袋码状态变为已印刷,需刷新列表 */
@@ -211,11 +258,14 @@
  function handleBindPallet() {
    const records = getSelectedRecords();
    if (!records) return;
    if (records.some((record) => !record.batchId)) {
      message.warning("请先录入生产批次,再绑定托盘");
    // 已录入批次的袋码只能绑到本批次的托盘;未录入批次的袋码可绑任意托盘(选到正式托盘时后端会先补录批次)。
    // 两者混选时批次归属不明确,要求分开操作。
    const boundRecords = records.filter((record) => record.batchId);
    if (boundRecords.length > 0 && boundRecords.length < records.length) {
      message.warning("所选袋码中既有已录入批次、也有未录入批次的记录,请分开绑定");
      return;
    }
    const batchIds = new Set(records.map((record) => record.batchId));
    const batchIds = new Set(boundRecords.map((record) => record.batchId));
    if (batchIds.size > 1) {
      message.warning("请选择同一批次下的袋码进行绑定");
      return;
@@ -237,6 +287,7 @@
      .setData({
        batchId: first.batchId,
        batchCode: first.batchCode,
        itemId: first.itemId,
        ids: records.map((record) => record.id!),
      })
      .open();
@@ -319,73 +370,40 @@
    <BindPalletFormModal @success="handleRefresh" />
    <ReplaceFormModal @success="handleReplaceSuccess" />
    <QrTraceQueryModal />
    <BusinessQrViewModal />
    <QrExportProgress />
    <Grid table-title="袋码列表(一袋一码)">
      <template #table-title><span class="text-[1rem] font-bold" style="white-space:nowrap">袋码列表(一袋一码)</span></template>
      <template #toolbar-tools>
        <TableAction :actions="[
            {
              label: '生成数据包',
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['mes:pro-bag-code:create'],
              onClick: handleGenerate,
            },
            {
              label: '生成原始袋码',
              icon: ACTION_ICON.ADD,
              auth: ['mes:pro-bag-code:create'],
              onClick: handleRawGenerate,
            },
            {
              label: '录入批次',
              type: 'primary',
              auth: ['mes:pro-bag-code:update'],
              onClick: handleBindBatch,
            },
            {
              label: '绑定托盘',
              type: 'primary',
              auth: ['mes:pro-bag-code:update'],
              onClick: handleBindPallet,
            },
            {
              label: '破损替换',
              type: 'primary',
              auth: ['mes:pro-bag-code:replace'],
              onClick: handleReplace,
            },
            {
              label: '解绑托盘',
              auth: ['mes:pro-bag-code:update'],
              onClick: handleUnbindPallet,
            },
            {
              label: '扫码追溯',
              icon: ACTION_ICON.SEARCH,
              auth: ['mes:pro-bag-code:query'],
              onClick: handleTrace,
            },
            {
              label: '导出二维码',
              icon: ACTION_ICON.DOWNLOAD,
              auth: ['mes:pro-bag-code:export'],
              onClick: handleExportQr,
            },
            {
              label: $t('ui.actionTitle.export'),
              type: 'primary',
              icon: ACTION_ICON.DOWNLOAD,
              auth: ['mes:pro-bag-code:export'],
              onClick: handleExport,
            },
          ]" />
        <div class="mes-code-toolbar">
          <TableAction :actions="[{ label: '生成数据包', type: 'primary', icon: ACTION_ICON.ADD, auth: ['mes:pro-bag-code:create'], onClick: handleGenerate },
          { label: '录入批次', auth: ['mes:pro-bag-code:update'], onClick: handleBindBatch },
          { label: '绑定托盘', auth: ['mes:pro-bag-code:update'], onClick: handleBindPallet },
          { label: '扫码追溯', icon: ACTION_ICON.SEARCH, auth: ['mes:pro-bag-code:query'], onClick: handleTrace }]" />
          <TableAction :dropdown-trigger="['click']" :drop-down-actions="[{ label: '追溯二维码', auth: ['mes:pro-bag-code:export'], onClick: () => handleExportQr('trace') },
          { label: '业务二维码', auth: ['mes:pro-bag-code:export'], onClick: () => handleExportQr('business') },
          { label: 'Excel 数据包', auth: ['mes:pro-bag-code:export'], onClick: handleExport }]">
            <template #more><Button>导出 <IconifyIcon icon="lucide:chevron-down" /></Button></template>
          </TableAction>
          <TableAction :dropdown-trigger="['click']" :drop-down-actions="[{ label: '生成原始袋码', auth: ['mes:pro-bag-code:create'], onClick: handleRawGenerate },
          { label: '补生成业务码', auth: ['mes:pro-bag-code:create'], onClick: handleGenerateBusinessCodes },
          { label: '破损替换', auth: ['mes:pro-bag-code:replace'], onClick: handleReplace },
          { label: '解绑托盘', auth: ['mes:pro-bag-code:update'], onClick: handleUnbindPallet }]">
            <template #more><Button>更多操作 <IconifyIcon icon="lucide:chevron-down" /></Button></template>
          </TableAction>
        </div>
      </template>
      <template #actions="{ row }">
        <TableAction :actions="[
            {
              label: '查看二维码',
              label: '追溯二维码',
              type: 'link',
              auth: ['mes:pro-bag-code:query'],
              onClick: handleViewQr.bind(null, row),
            },
            {
              label: '业务二维码', type: 'link', auth: ['mes:pro-bag-code:query'],
              ifShow: !!row.businessCode, onClick: handleBusinessQr.bind(null, row),
            },
            {
              label: $t('common.delete'),
@@ -407,3 +425,9 @@
    </Grid>
  </Page>
</template>
<style scoped>
.mes-code-toolbar { display:flex;flex-wrap:wrap;align-items:center;justify-content:flex-end;gap:8px; }
.mes-code-toolbar :deep(.ant-btn) { white-space:nowrap; }
.mes-code-toolbar :deep(.ant-btn > .iconify) { display:inline-block;width:14px;height:14px;vertical-align:middle; }
</style>