云
3 天以前 6540316952969ed21230c0f92c3bc57e88f58142
feat(mes): 新增二维码后台导出功能

- 在袋码、批次、托盘页面集成二维码后台导出任务模态框
- 实现异步二维码导出流程,支持大数量二维码批量处理
- 添加导出任务状态轮询和进度显示功能
- 集成取消任务和下载完成文件的功能
- 优化用户体验,避免前端长时间等待导出过程
已添加2个文件
已修改3个文件
207 ■■■■■ 文件已修改
src/api/mes/pro/qrExport/index.ts 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/bagCode/index.vue 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/batch/index.vue 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/components/qr-export-task-modal.vue 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/pallet/index.vue 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pro/qrExport/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,54 @@
import { requestClient } from '#/api/request';
export type QrExportBizType =
  | 'BAG_LABEL'
  | 'BAG_BUSINESS'
  | 'PALLET_LABEL'
  | 'PALLET_BUSINESS'
  | 'BATCH_LABEL'
  | 'BATCH_BUSINESS';
export interface QrExportParams {
  bizType: QrExportBizType;
  uuid?: string;
  batchId?: number;
}
export interface QrExportCount {
  totalCount: number;
  syncThreshold: number;
  mode: 'SYNC' | 'ASYNC';
}
export interface QrExportTask {
  id: number;
  bizType: QrExportBizType;
  taskName: string;
  status: number;
  statusName: string;
  totalCount: number;
  finishedCount: number;
  progress: number;
  errorMsg?: string;
  downloadable: boolean;
}
export function countQrExport(params: QrExportParams) {
  return requestClient.get<QrExportCount>('/mes/pro/qr-export/count', { params });
}
export function submitQrExport(params: QrExportParams) {
  return requestClient.post<QrExportTask>('/mes/pro/qr-export/task', params);
}
export function getQrExportTask(id: number) {
  return requestClient.get<QrExportTask>('/mes/pro/qr-export/task/get', { params: { id } });
}
export function cancelQrExportTask(id: number) {
  return requestClient.post<boolean>(`/mes/pro/qr-export/task/cancel?id=${id}`);
}
export function downloadQrExportTask(id: number) {
  return requestClient.download('/mes/pro/qr-export/task/download', { params: { id } });
}
src/views/mes/pro/bagCode/index.vue
@@ -1,4 +1,5 @@
<script lang="ts" setup>
  import { ref } from 'vue';
  import type { VxeTableGridOptions } from "#/adapter/vxe-table";
  import type { MesProBagCodeApi } from "#/api/mes/pro/bagCode";
@@ -20,6 +21,7 @@
    unbindBagCodePallet,
  } from "#/api/mes/pro/bagCode";
  import { IconifyIcon } from "@vben/icons";
  import { countQrExport, submitQrExport } from '#/api/mes/pro/qrExport';
  import { $t } from "#/locales";
  import { useGridColumns, useGridFormSchema } from "./data";
@@ -30,8 +32,11 @@
  import ReplaceModal from "./modules/replace-form.vue";
  import QrTraceModal from "../components/qr-trace-modal.vue";
  import BusinessQrModal from "../components/business-qr.vue";
  import QrExportTaskModal from '../components/qr-export-task-modal.vue';
  defineOptions({ name: "MesProBagCode" });
  const qrExportTaskModal = ref<InstanceType<typeof QrExportTaskModal>>();
  const [GenerateFormModal, generateFormModalApi] = useVbenModal({
    connectedComponent: GenerateModal,
@@ -129,6 +134,14 @@
    }
    if (!params.uuid && !params.batchId) {
      message.warning("请先按生成组或批次筛选后再导出二维码");
      return;
    }
    const bizType = kind === 'business' ? 'BAG_BUSINESS' : 'BAG_LABEL';
    const count = await countQrExport({ bizType, ...params });
    if (count.mode === 'ASYNC') {
      const task = await submitQrExport({ bizType, ...params });
      qrExportTaskModal.value?.show(task);
      message.success(`已提交 ${count.totalCount} å¼ äºŒç»´ç çš„后台导出任务`);
      return;
    }
    const data = await (kind === 'business' ? exportBagBusinessQr : exportBagQr)(params);
@@ -347,6 +360,7 @@
    <ReplaceFormModal @success="handleReplaceSuccess" />
    <QrTraceQueryModal />
    <BusinessQrViewModal />
    <QrExportTaskModal ref="qrExportTaskModal" />
    <Grid table-title="袋码列表(一袋一码)">
      <template #table-title><span class="text-[1rem] font-bold" style="white-space:nowrap">袋码列表(一袋一码)</span></template>
      <template #toolbar-tools>
@@ -360,6 +374,7 @@
          { label: 'Excel æ•°æ®åŒ…', auth: ['mes:pro-bag-code:export'], onClick: handleExport }]">
            <template #more><Button>导出 <IconifyIcon icon="lucide:chevron-down" /></Button></template>
          </TableAction>
          <Button @click="qrExportTaskModal?.showLast()">后台导出任务</Button>
          <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 },
src/views/mes/pro/batch/index.vue
@@ -1,4 +1,5 @@
<script lang="ts" setup>
  import { ref } from 'vue';
  import type { VxeTableGridOptions } from "#/adapter/vxe-table";
  import type { MesProBatchApi } from "#/api/mes/pro/batch";
@@ -19,6 +20,7 @@
    updateBatchStatus,
  } from "#/api/mes/pro/batch";
  import { IconifyIcon } from "@vben/icons";
  import { countQrExport, submitQrExport } from '#/api/mes/pro/qrExport';
  import { $t } from "#/locales";
  import { useGridColumns, useGridFormSchema } from "./data";
@@ -26,6 +28,9 @@
  import PreGenerateModal from "./modules/pre-generate.vue";
  import QrTraceModal from "../components/qr-trace-modal.vue";
  import BusinessQrModal from "../components/business-qr.vue";
  import QrExportTaskModal from '../components/qr-export-task-modal.vue';
  const qrExportTaskModal = ref<InstanceType<typeof QrExportTaskModal>>();
  const [FormModal, formModalApi] = useVbenModal({
    connectedComponent: Form,
@@ -54,6 +59,13 @@
  }
  async function handleExportBusiness(row: MesProBatchApi.Batch) {
    if (row.id == null) return;
    const params = { bizType: 'BATCH_BUSINESS' as const, batchId: row.id };
    const count = await countQrExport(params);
    if (count.mode === 'ASYNC') {
      qrExportTaskModal.value?.show(await submitQrExport(params));
      message.success(`已提交 ${count.totalCount} å¼ äºŒç»´ç çš„后台导出任务`);
      return;
    }
    const data = await exportBatchBusinessQr(row.id);
    downloadFileFromBlobPart({ fileName: '批次与托盘业务二维码.zip', source: data });
  }
@@ -79,6 +91,13 @@
  /** æ‰“印整套二维码(批次图 + å…¨éƒ¨æ‰˜ç›˜å›¾ ZIP) */
  async function handlePrintQrSet(row: MesProBatchApi.Batch) {
    const params = { bizType: 'BATCH_LABEL' as const, batchId: row.id! };
    const count = await countQrExport(params);
    if (count.mode === 'ASYNC') {
      qrExportTaskModal.value?.show(await submitQrExport(params));
      message.success(`已提交 ${count.totalCount} å¼ äºŒç»´ç çš„后台导出任务`);
      return;
    }
    const data = await exportBatchQr(row.id!);
    downloadFileFromBlobPart({
      fileName: `批次二维码-${getDisplayCode(row)}.zip`,
@@ -182,6 +201,7 @@
    <PreGenerateModalApi @success="handlePreGenerateSuccess" />
    <QrTraceQueryModal />
    <BusinessQrViewModal />
    <QrExportTaskModal ref="qrExportTaskModal" />
    <Grid table-title="生产批次列表">
      <template #table-title><span class="text-[1rem] font-bold" style="white-space:nowrap">生产批次列表</span></template>
      <template #toolbar-tools>
@@ -192,6 +212,7 @@
          <TableAction :dropdown-trigger="['click']" :drop-down-actions="[{ label: 'Excel æ•°æ®', auth: ['mes:pro-batch:export'], onClick: handleExport }]">
            <template #more><Button>导出 <IconifyIcon icon="lucide:chevron-down" /></Button></template>
          </TableAction>
          <Button @click="qrExportTaskModal?.showLast()">后台导出任务</Button>
        </div>
      </template>
      <template #code="{ row }">
src/views/mes/pro/components/qr-export-task-modal.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,103 @@
<script lang="ts" setup>
  import type { QrExportTask } from '#/api/mes/pro/qrExport';
  import { onBeforeUnmount, onMounted, ref } from 'vue';
  import { downloadFileFromBlobPart } from '@vben/utils';
  import { Button, message, Modal, Progress } from 'ant-design-vue';
  import {
    cancelQrExportTask,
    downloadQrExportTask,
    getQrExportTask,
  } from '#/api/mes/pro/qrExport';
  const storageKey = 'mes-pro-last-qr-export-task-id';
  const visible = ref(false);
  const task = ref<QrExportTask>();
  const downloading = ref(false);
  let timer: ReturnType<typeof setTimeout> | undefined;
  function stopPolling() {
    if (timer) clearTimeout(timer);
    timer = undefined;
  }
  async function refresh() {
    if (!task.value) return;
    stopPolling();
    try {
      task.value = await getQrExportTask(task.value.id);
      if (task.value.status === 10 || task.value.status === 20) {
        timer = setTimeout(refresh, 2000);
      }
    } catch {
      message.error('获取导出进度失败,请点击刷新重试');
    }
  }
  function show(value: QrExportTask) {
    task.value = value;
    localStorage.setItem(storageKey, String(value.id));
    visible.value = true;
    void refresh();
  }
  function showLast() {
    if (task.value) visible.value = true;
    else message.info('暂无后台导出任务');
  }
  async function cancel() {
    if (!task.value) return;
    await cancelQrExportTask(task.value.id);
    message.success('已申请取消导出任务');
    await refresh();
  }
  async function download() {
    if (!task.value?.downloadable || downloading.value) return;
    downloading.value = true;
    try {
      const data = await downloadQrExportTask(task.value.id);
      downloadFileFromBlobPart({ fileName: `${task.value.taskName}.zip`, source: data });
      localStorage.removeItem(storageKey);
      await refresh();
    } finally {
      downloading.value = false;
    }
  }
  onMounted(async () => {
    const id = Number(localStorage.getItem(storageKey));
    if (!Number.isSafeInteger(id) || id <= 0) return;
    try {
      const previous = await getQrExportTask(id);
      task.value = previous;
      if (previous.status === 10 || previous.status === 20 || previous.downloadable) {
        visible.value = true;
      }
      if (previous.status === 10 || previous.status === 20) timer = setTimeout(refresh, 2000);
    } catch {
      localStorage.removeItem(storageKey);
    }
  });
  onBeforeUnmount(stopPolling);
  defineExpose({ show, showLast });
</script>
<template>
  <Modal v-model:open="visible" title="二维码后台导出" :footer="null" :mask-closable="false">
    <template v-if="task">
      <p>任务 #{{ task.id }} Â· {{ task.taskName }}</p>
      <p>已生成 {{ task.finishedCount }} / {{ task.totalCount }} å¼ ï¼Œ{{ task.statusName }}</p>
      <Progress :percent="task.progress" :status="task.status === 50 ? 'exception' : task.downloadable ? 'success' : 'active'" />
      <p v-if="task.errorMsg" class="text-red-600">{{ task.errorMsg }}</p>
      <div class="mt-4 flex justify-end gap-2">
        <Button @click="refresh">刷新</Button>
        <Button v-if="task.status === 10 || task.status === 20" danger @click="cancel">取消任务</Button>
        <Button v-if="task.downloadable" type="primary" :loading="downloading" @click="download">下载 ZIP</Button>
      </div>
    </template>
  </Modal>
</template>
src/views/mes/pro/pallet/index.vue
@@ -1,4 +1,5 @@
<script lang="ts" setup>
  import { ref } from 'vue';
  import type { VxeTableGridOptions } from "#/adapter/vxe-table";
  import type { MesProPalletApi } from "#/api/mes/pro/pallet";
@@ -18,6 +19,7 @@
    getPalletPage,
  } from "#/api/mes/pro/pallet";
  import { IconifyIcon } from "@vben/icons";
  import { countQrExport, submitQrExport } from '#/api/mes/pro/qrExport';
  import { $t } from "#/locales";
  import { useGridColumns, useGridFormSchema } from "./data";
@@ -25,6 +27,9 @@
  import GenerateModal from "./modules/generate-form.vue";
  import QrTraceModal from "../components/qr-trace-modal.vue";
  import BusinessQrModal from "../components/business-qr.vue";
  import QrExportTaskModal from '../components/qr-export-task-modal.vue';
  const qrExportTaskModal = ref<InstanceType<typeof QrExportTaskModal>>();
  defineOptions({ name: "MesProPallet" });
@@ -137,6 +142,13 @@
      message.warning("请先按批次筛选后再导出二维码");
      return;
    }
    const params = { bizType: 'PALLET_LABEL' as const, batchId: values.batchId };
    const count = await countQrExport(params);
    if (count.mode === 'ASYNC') {
      qrExportTaskModal.value?.show(await submitQrExport(params));
      message.success(`已提交 ${count.totalCount} å¼ äºŒç»´ç çš„后台导出任务`);
      return;
    }
    const data = await exportPalletQr(values.batchId);
    downloadFileFromBlobPart({ fileName: "托盘码二维码.zip", source: data });
  }
@@ -204,6 +216,7 @@
    <BindBatchFormModal @success="handleRefresh" />
    <QrTraceQueryModal />
    <BusinessQrViewModal />
    <QrExportTaskModal ref="qrExportTaskModal" />
    <Grid table-title="托盘码列表">
      <template #table-title><span class="text-[1rem] font-bold" style="white-space:nowrap">托盘码列表</span></template>
      <template #toolbar-tools>
@@ -216,6 +229,7 @@
          { label: 'Excel æ•°æ®', auth: ['mes:pro-pallet:export'], onClick: handleExport }]">
            <template #more><Button>导出 <IconifyIcon icon="lucide:chevron-down" /></Button></template>
          </TableAction>
          <Button @click="qrExportTaskModal?.showLast()">后台导出任务</Button>
          <TableAction :dropdown-trigger="['click']" :drop-down-actions="[{ label: '补生成业务码(选中记录)', auth: ['mes:pro-pallet:create'], onClick: handleGenerateBusiness }]">
            <template #more><Button>更多操作 <IconifyIcon icon="lucide:chevron-down" /></Button></template>
          </TableAction>