feat(mes): 新增二维码后台导出功能
- 在袋码、批次、托盘页面集成二维码后台导出任务模态框
- 实现异步二维码导出流程,支持大数量二维码批量处理
- 添加导出任务状态轮询和进度显示功能
- 集成取消任务和下载完成文件的功能
- 优化用户体验,避免前端长时间等待导出过程
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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 } }); |
| | | } |
| | |
| | | <script lang="ts" setup> |
| | | import { ref } from 'vue'; |
| | | import type { VxeTableGridOptions } from "#/adapter/vxe-table"; |
| | | import type { MesProBagCodeApi } from "#/api/mes/pro/bagCode"; |
| | | |
| | |
| | | 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"; |
| | |
| | | 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, |
| | |
| | | } |
| | | 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); |
| | |
| | | <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> |
| | |
| | | { 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 }, |
| | |
| | | <script lang="ts" setup> |
| | | import { ref } from 'vue'; |
| | | import type { VxeTableGridOptions } from "#/adapter/vxe-table"; |
| | | import type { MesProBatchApi } from "#/api/mes/pro/batch"; |
| | | |
| | |
| | | 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"; |
| | |
| | | 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, |
| | |
| | | } |
| | | 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 }); |
| | | } |
| | |
| | | |
| | | /** æå°æ´å¥äºç»´ç ï¼æ¹æ¬¡å¾ + å
¨é¨æçå¾ 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`, |
| | |
| | | <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> |
| | |
| | | <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 }"> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| | |
| | | <script lang="ts" setup> |
| | | import { ref } from 'vue'; |
| | | import type { VxeTableGridOptions } from "#/adapter/vxe-table"; |
| | | import type { MesProPalletApi } from "#/api/mes/pro/pallet"; |
| | | |
| | |
| | | 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"; |
| | |
| | | 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" }); |
| | | |
| | |
| | | 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 }); |
| | | } |
| | |
| | | <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> |
| | |
| | | { 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> |