feat(bagCode): 添加袋码批量生成后台任务支持
- 实现幂等键机制防止重复提交
- 集成后台任务轮询和进度展示功能
- 添加任务取消和状态管理逻辑
- 重构表单组件支持异步生成流程
- 新增任务状态枚举和API接口定义
- 优化弹窗交互体验和错误处理机制
| | |
| | | palletCount?: number; // æçæ°ï¼48è¢/100è¢ åç§å¿
å¡«ï¼ |
| | | bagCount?: number; // è¢ç æ°ï¼å¨åç§å¿
å¡«ï¼ |
| | | remark?: string; // 夿³¨ |
| | | requestId?: string; // å¹çé®ï¼ç½ç»éè¯å¤ç¨åä¸ä¸ªå¼ |
| | | } |
| | | |
| | | export interface GenerateResult { |
| | |
| | | type?: string; // åç§ä¸»è®¡éåä½ç±»åï¼48è¢/100è¢/å¨ï¼ |
| | | perPallet?: number; // æ¯æè¢æ°ï¼å¨åç§ä¸º 0ï¼ |
| | | totalCount?: number; // çæè¢ç æ»æ° |
| | | palletIds?: number[]; // èªå¨çæçæçç¼å·å表ï¼å¨åç§ä¸ºç©ºï¼ |
| | | palletCount?: number; // æçæ»æ°ï¼åå§æ°æ®ä¸º 0ï¼ |
| | | palletIds?: number[]; // èªå¨çæçæçç¼å·å表ï¼ä»
忥宿è¿åï¼å¨åç§ä¸ºç©ºï¼ |
| | | async?: boolean; // true=已转åå°ä»»å¡ |
| | | taskId?: number; // åå°ä»»å¡ç¼å·ï¼å¼æ¥æ¨¡å¼è¿åï¼ |
| | | } |
| | | |
| | | /** çæåå§è¢ç 请æ±ï¼ä»
åç§+è¢ç æ°éï¼ä¸å
³è计éåä½ç±»åãä¸çææçï¼ */ |
| | |
| | | itemId: number; // åç§ï¼ç©æï¼ç¼å· |
| | | count: number; // è¢ç æ°é |
| | | remark?: string; // 夿³¨ |
| | | requestId?: string; // å¹çé®ï¼ç½ç»éè¯å¤ç¨åä¸ä¸ªå¼ |
| | | } |
| | | |
| | | /** è¢ç çæä»»å¡ï¼å¤§æ¹éçæè½¬åå°åçè¿åº¦ï¼ */ |
| | | export interface GenerateTask { |
| | | id: number; // ä»»å¡ç¼å· |
| | | bizType?: string; // PACKAGEï¼æ°æ®å
ï¼/ RAWï¼åå§æ°æ®ï¼ |
| | | status?: number; // 10 æéä¸ / 20 çæä¸ / 25 åæ¶ä¸ / 30 已宿 / 40 已忶 / 50 已失败 |
| | | statusName?: string; // ç¶æåç§° |
| | | progress?: number; // è¿åº¦ç¾åæ¯ 0~100 |
| | | uuid?: string; // çæç» UUID |
| | | itemId?: number; // åç§ç¼å· |
| | | itemCode?: string; // åç§ç¼ç |
| | | itemName?: string; // åç§åç§° |
| | | unitType?: string; // 计éåä½ç±»å |
| | | perPallet?: number; // æ¯æè¢æ° |
| | | palletCount?: number; // æçæ° |
| | | totalCount?: number; // è¢ç æ»æ° |
| | | finishedCount?: number; // å·²çæè¢ç æ° |
| | | errorMsg?: string; // 失败åå |
| | | createTime?: string; // å建æ¶é´ |
| | | finishTime?: string; // 宿/ç»æ¢æ¶é´ |
| | | } |
| | | |
| | | export interface BindBatchParams { |
| | |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢è¢ç çæä»»å¡ï¼å¤§æ¹éçæçè¿åº¦è½®è¯¢ï¼ */ |
| | | export function getBagCodeGenerateTask(id: number) { |
| | | return requestClient.get<MesProBagCodeApi.GenerateTask>( |
| | | '/mes/pro/bag-code/generate-task/get', |
| | | { params: { id } }, |
| | | ); |
| | | } |
| | | |
| | | /** åæ¶è¢ç çæä»»å¡ï¼åæ¢çæå¹¶æ¸
çåæåï¼ */ |
| | | export function cancelBagCodeGenerateTask(id: number) { |
| | | return requestClient.post<boolean>( |
| | | `/mes/pro/bag-code/generate-task/cancel?id=${id}`, |
| | | ); |
| | | } |
| | | |
| | | /** å½å
¥çäº§æ¹æ¬¡ */ |
| | | export function bindBagCodeBatch(data: MesProBagCodeApi.BindBatchParams) { |
| | | return requestClient.put('/mes/pro/bag-code/bind-batch', data); |
| | |
| | | |
| | | import { useVbenModal } from "@vben/common-ui"; |
| | | |
| | | import { message } from "ant-design-vue"; |
| | | import { Button, message, Progress } from "ant-design-vue"; |
| | | |
| | | import { useVbenForm } from "#/adapter/form"; |
| | | import { generateBagCodes } from "#/api/mes/pro/bagCode"; |
| | | import { getUnit } from "#/api/mdm/unit"; |
| | | import { MdmItemSelect } from "#/views/basicData/mdm/components"; |
| | | |
| | | import { isFinalTaskStatus, useGenerateTask } from "./use-generate-task"; |
| | | |
| | | defineOptions({ name: "MesProBagCodeGenerateForm" }); |
| | | |
| | |
| | | const unitTypeError = ref(false); |
| | | const palletCountVal = ref<number | null>(null); |
| | | const bagCountVal = ref<number | null>(null); |
| | | |
| | | /** å¹çé®ï¼æå¼å¼¹çªæ¶çæï¼ç½ç»éè¯å¤ç¨ï¼å¤±è´¥åéæ°çæ */ |
| | | const requestId = ref(""); |
| | | |
| | | function newRequestId() { |
| | | return `${Date.now()}-${Math.random().toString(36).slice(2)}-${Math.random() |
| | | .toString(36) |
| | | .slice(2)}`; |
| | | } |
| | | |
| | | /** æ¢ç®/é
ç½®æç¤ºææ¡ */ |
| | | const tipText = computed(() => { |
| | |
| | | async onConfirm() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) return; |
| | | if (task.value && !isFinalTaskStatus(task.value.status)) return; |
| | | if (unitTypeError.value) { |
| | | message.warning(tipText.value); |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | let asyncStarted = false; |
| | | try { |
| | | const values = |
| | | (await formApi.getValues()) as MesProBagCodeApi.GenerateParams; |
| | | const data: MesProBagCodeApi.GenerateParams = { |
| | | itemId: values.itemId, |
| | | remark: values.remark, |
| | | requestId: requestId.value, |
| | | }; |
| | | if (isTon.value) { |
| | | data.bagCount = values.bagCount; |
| | |
| | | data.palletCount = values.palletCount; |
| | | } |
| | | const result = await generateBagCodes(data); |
| | | if (result.async && result.taskId) { |
| | | asyncStarted = true; |
| | | modalApi.unlock(); |
| | | // 大æ¹é已转åå°ï¼ä¿æå¼¹çªæå¼ï¼å±ç¤ºè¿åº¦å¹¶è½®è¯¢ |
| | | startGenerateTask({ |
| | | id: result.taskId, |
| | | status: 10, |
| | | statusName: "æéä¸", |
| | | progress: 0, |
| | | totalCount: result.totalCount, |
| | | finishedCount: 0, |
| | | palletCount: result.palletCount, |
| | | uuid: result.uuid, |
| | | }); |
| | | return; |
| | | } |
| | | message.success( |
| | | `å·²æåçæ ${result.totalCount} æ¡è¢ç ` + |
| | | (result.palletIds?.length |
| | | ? `ï¼å¹¶èªå¨çæ ${result.palletIds.length} 个æç` |
| | | (result.palletCount |
| | | ? `ï¼å¹¶èªå¨çæ ${result.palletCount} 个æç` |
| | | : ""), |
| | | ); |
| | | emit("success"); |
| | | await modalApi.close(); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | if (!asyncStarted) modalApi.unlock(); |
| | | } |
| | | }, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) return; |
| | | if (!isOpen) { |
| | | stopGenerateTaskPolling(); |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | resetGenerateTask(); |
| | | requestId.value = newRequestId(); |
| | | unitType.value = ""; |
| | | bagsPerPallet.value = null; |
| | | isTon.value = false; |
| | |
| | | } |
| | | }, |
| | | }); |
| | | |
| | | const { |
| | | task, |
| | | canceling: generateTaskCanceling, |
| | | cancel: cancelGenerateTask, |
| | | reset: resetGenerateTask, |
| | | stopPolling: stopGenerateTaskPolling, |
| | | start: startGenerateTask, |
| | | } = useGenerateTask(modalApi, { |
| | | onSuccess: () => emit("success"), |
| | | onFailed: () => { |
| | | // å¤±è´¥åæ¢æ°çå¹çé®ï¼å
è®¸ç¨æ·ç´æ¥éæ°æäº¤ |
| | | requestId.value = newRequestId(); |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal title="çæè¢ç æ°æ®å
" class="w-2/5"> |
| | | <template v-if="task"> |
| | | <div class="mx-4 mb-2"> |
| | | <p class="mb-1 text-sm"> |
| | | ä»»å¡ #{{ task.id }} · {{ task.statusName }} |
| | | </p> |
| | | <p class="mb-2 text-sm text-gray-500"> |
| | | å·²çæ {{ task.finishedCount ?? 0 }} / {{ task.totalCount ?? 0 }} æ¡ |
| | | </p> |
| | | <Progress |
| | | :percent="task.progress ?? 0" |
| | | :status=" |
| | | task.status === 50 |
| | | ? 'exception' |
| | | : task.status === 30 |
| | | ? 'success' |
| | | : 'active' |
| | | " |
| | | /> |
| | | <p v-if="task.errorMsg" class="mt-2 text-sm text-red-500"> |
| | | {{ task.errorMsg }} |
| | | </p> |
| | | <div class="mt-4 flex justify-end"> |
| | | <Button |
| | | danger |
| | | size="small" |
| | | :loading="generateTaskCanceling" |
| | | @click="cancelGenerateTask" |
| | | > |
| | | åæ¶çæ |
| | | </Button> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | <template v-else> |
| | | <Form class="mx-4" /> |
| | | <div |
| | | v-if="tipText" |
| | |
| | | > |
| | | {{ tipText }} |
| | | </div> |
| | | </template> |
| | | </Modal> |
| | | </template> |
| | |
| | | <script lang="ts" setup> |
| | | import type { VbenFormSchema } from "#/adapter/form"; |
| | | |
| | | import { markRaw } from "vue"; |
| | | import { markRaw, ref } from "vue"; |
| | | |
| | | import { useVbenModal } from "@vben/common-ui"; |
| | | |
| | | import { message } from "ant-design-vue"; |
| | | import { Button, message, Progress } from "ant-design-vue"; |
| | | |
| | | import { useVbenForm } from "#/adapter/form"; |
| | | import { generateRawBagCodes } from "#/api/mes/pro/bagCode"; |
| | | import { MdmItemSelect } from "#/views/basicData/mdm/components"; |
| | | |
| | | import { isFinalTaskStatus, useGenerateTask } from "./use-generate-task"; |
| | | |
| | | defineOptions({ name: "MesProBagCodeRawGenerateForm" }); |
| | | |
| | | const emit = defineEmits(["success"]); |
| | | |
| | | /** å¹çé®ï¼æå¼å¼¹çªæ¶çæï¼ç½ç»éè¯å¤ç¨ï¼å¤±è´¥åéæ°çæ */ |
| | | const requestId = ref(""); |
| | | |
| | | function newRequestId() { |
| | | return `${Date.now()}-${Math.random().toString(36).slice(2)}-${Math.random() |
| | | .toString(36) |
| | | .slice(2)}`; |
| | | } |
| | | |
| | | const schema: VbenFormSchema[] = [ |
| | | { |
| | |
| | | async onConfirm() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) return; |
| | | if (task.value && !isFinalTaskStatus(task.value.status)) return; |
| | | modalApi.lock(); |
| | | let asyncStarted = false; |
| | | try { |
| | | const values = await formApi.getValues(); |
| | | const result = await generateRawBagCodes({ |
| | | itemId: values.itemId, |
| | | count: values.count, |
| | | remark: values.remark, |
| | | requestId: requestId.value, |
| | | }); |
| | | if (result.async && result.taskId) { |
| | | asyncStarted = true; |
| | | modalApi.unlock(); |
| | | // 大æ¹é已转åå°ï¼ä¿æå¼¹çªæå¼ï¼å±ç¤ºè¿åº¦å¹¶è½®è¯¢ |
| | | startGenerateTask({ |
| | | id: result.taskId, |
| | | status: 10, |
| | | statusName: "æéä¸", |
| | | progress: 0, |
| | | totalCount: result.totalCount, |
| | | finishedCount: 0, |
| | | palletCount: result.palletCount, |
| | | uuid: result.uuid, |
| | | }); |
| | | return; |
| | | } |
| | | message.success(`å·²æåçæ ${result.totalCount} æ¡åå§è¢ç `); |
| | | emit("success"); |
| | | await modalApi.close(); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | if (!asyncStarted) modalApi.unlock(); |
| | | } |
| | | }, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) return; |
| | | if (!isOpen) { |
| | | stopGenerateTaskPolling(); |
| | | return; |
| | | } |
| | | resetGenerateTask(); |
| | | requestId.value = newRequestId(); |
| | | await formApi.setValues({ |
| | | itemId: undefined, |
| | | count: undefined, |
| | |
| | | }); |
| | | }, |
| | | }); |
| | | |
| | | const { |
| | | task, |
| | | canceling: generateTaskCanceling, |
| | | cancel: cancelGenerateTask, |
| | | reset: resetGenerateTask, |
| | | stopPolling: stopGenerateTaskPolling, |
| | | start: startGenerateTask, |
| | | } = useGenerateTask(modalApi, { |
| | | onSuccess: () => emit("success"), |
| | | onFailed: () => { |
| | | // å¤±è´¥åæ¢æ°çå¹çé®ï¼å
è®¸ç¨æ·ç´æ¥éæ°æäº¤ |
| | | requestId.value = newRequestId(); |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal title="çæåå§è¢ç " class="w-2/5"> |
| | | <template v-if="task"> |
| | | <div class="mx-4 mb-2"> |
| | | <p class="mb-1 text-sm"> |
| | | ä»»å¡ #{{ task.id }} · {{ task.statusName }} |
| | | </p> |
| | | <p class="mb-2 text-sm text-gray-500"> |
| | | å·²çæ {{ task.finishedCount ?? 0 }} / {{ task.totalCount ?? 0 }} æ¡ |
| | | </p> |
| | | <Progress |
| | | :percent="task.progress ?? 0" |
| | | :status=" |
| | | task.status === 50 |
| | | ? 'exception' |
| | | : task.status === 30 |
| | | ? 'success' |
| | | : 'active' |
| | | " |
| | | /> |
| | | <p v-if="task.errorMsg" class="mt-2 text-sm text-red-500"> |
| | | {{ task.errorMsg }} |
| | | </p> |
| | | <div class="mt-4 flex justify-end"> |
| | | <Button |
| | | danger |
| | | size="small" |
| | | :loading="generateTaskCanceling" |
| | | @click="cancelGenerateTask" |
| | | > |
| | | åæ¶çæ |
| | | </Button> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | <template v-else> |
| | | <Form class="mx-4" /> |
| | | <div class="mx-4 mb-2 rounded bg-gray-50 px-3 py-2 text-sm text-gray-500"> |
| | | åå§è¢ç ä»
ééæ©åç§å¹¶è¾å
¥æ°éï¼ä¸æ¢ç®è®¡éåä½ç±»åãä¸çææçï¼åç»å¯æ£å¸¸å½å
¥æ¹æ¬¡æç»å®æç |
| | | </div> |
| | | </template> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { MesProBagCodeApi } from '#/api/mes/pro/bagCode'; |
| | | |
| | | import { onBeforeUnmount, ref } from 'vue'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { |
| | | cancelBagCodeGenerateTask, |
| | | getBagCodeGenerateTask, |
| | | } from '#/api/mes/pro/bagCode'; |
| | | |
| | | /** å¼¹çªæå°æ¥å£ï¼é¿å
ä¾èµå
é¨ ModalApi ç±»å */ |
| | | interface GenerateModalApi { |
| | | close: () => Promise<void>; |
| | | setState: (state: { |
| | | showCancelButton?: boolean; |
| | | showConfirmButton?: boolean; |
| | | }) => unknown; |
| | | } |
| | | |
| | | interface GenerateTaskCallbacks { |
| | | /** çææåï¼ç¶æ 30ï¼ï¼è°ç¨æ¹å·æ°å表 */ |
| | | onSuccess: () => void; |
| | | /** çæå¤±è´¥ï¼ç¶æ 50ï¼ï¼è°ç¨æ¹æ´æ¢å¹çé®åå
è®¸éæ°æäº¤ */ |
| | | onFailed?: () => void; |
| | | } |
| | | |
| | | /** ç»æï¼å·²å®æ / 已忶 / 已失败 */ |
| | | const FINAL_STATUSES = new Set([30, 40, 50]); |
| | | |
| | | export function isFinalTaskStatus(status?: number) { |
| | | return status !== undefined && FINAL_STATUSES.has(status); |
| | | } |
| | | |
| | | /** |
| | | * 大æ¹éè¢ç åå°çæï¼è½®è¯¢è¿åº¦ãåæ¶ä»»å¡ï¼å¹¶æå¼¹çªæé®åæ¢ä¸ºè¿åº¦å±ç¤ºæã |
| | | */ |
| | | export function useGenerateTask( |
| | | modalApi: GenerateModalApi, |
| | | callbacks: GenerateTaskCallbacks, |
| | | ) { |
| | | const task = ref<MesProBagCodeApi.GenerateTask>(); |
| | | const canceling = ref(false); |
| | | let timer: ReturnType<typeof setTimeout> | undefined; |
| | | |
| | | function stopPolling() { |
| | | if (timer) clearTimeout(timer); |
| | | timer = undefined; |
| | | } |
| | | |
| | | function restoreButtons() { |
| | | modalApi.setState({ showConfirmButton: true, showCancelButton: true }); |
| | | } |
| | | |
| | | /** éç½®å°å¯ç¼è¾æï¼å
³éå¼¹çªãéæ°æå¼ã失败éè¯åè°ç¨ */ |
| | | function reset() { |
| | | stopPolling(); |
| | | task.value = undefined; |
| | | canceling.value = false; |
| | | restoreButtons(); |
| | | } |
| | | |
| | | /** è¿å
¥è¿åº¦å±ç¤ºæï¼éèæäº¤/åæ¶æé®ï¼è¿åº¦åºèªè¡æä¾åæ¶æä½ */ |
| | | function start(initial: MesProBagCodeApi.GenerateTask) { |
| | | stopPolling(); |
| | | task.value = initial; |
| | | modalApi.setState({ showConfirmButton: false, showCancelButton: false }); |
| | | void refresh(); |
| | | } |
| | | |
| | | async function refresh() { |
| | | if (!task.value) return; |
| | | stopPolling(); |
| | | try { |
| | | const current = await getBagCodeGenerateTask(task.value.id); |
| | | task.value = current; |
| | | if (isFinalTaskStatus(current.status)) { |
| | | finish(current); |
| | | return; |
| | | } |
| | | } catch { |
| | | // 䏿¬¡æ¥è¯¢å¤±è´¥ä¸ç»æ¢è½®è¯¢ï¼é¿å
ç½ç»æå¨æè¿åº¦å¡æ» |
| | | } |
| | | timer = setTimeout(refresh, 2000); |
| | | } |
| | | |
| | | function finish(current: MesProBagCodeApi.GenerateTask) { |
| | | if (current.status === 30) { |
| | | message.success( |
| | | `çæå®æï¼å
± ${current.totalCount ?? 0} æ¡è¢ç ` + |
| | | (current.palletCount |
| | | ? `ï¼å¹¶èªå¨çæ ${current.palletCount} 个æç` |
| | | : ''), |
| | | ); |
| | | callbacks.onSuccess(); |
| | | void modalApi.close(); |
| | | return; |
| | | } |
| | | if (current.status === 40) { |
| | | message.info('çæä»»å¡å·²åæ¶ï¼å·²çæçæ°æ®å·²æ¸
ç'); |
| | | void modalApi.close(); |
| | | return; |
| | | } |
| | | // 失败ï¼åå°å¯ç¼è¾æï¼ç±è°ç¨æ¹æ´æ¢ requestId åéæ°æäº¤ |
| | | message.error(current.errorMsg ?? 'çæå¤±è´¥ï¼è¯·éæ°æäº¤'); |
| | | task.value = undefined; |
| | | restoreButtons(); |
| | | callbacks.onFailed?.(); |
| | | } |
| | | |
| | | async function cancel() { |
| | | if (!task.value || canceling.value) return; |
| | | canceling.value = true; |
| | | try { |
| | | await cancelBagCodeGenerateTask(task.value.id); |
| | | message.info('å·²ç³è¯·åæ¶ï¼æ£å¨åæ¢å¹¶æ¸
çå·²çææ°æ®'); |
| | | } catch { |
| | | message.error('åæ¶çæå¤±è´¥ï¼è¯·ç¨åéè¯'); |
| | | } finally { |
| | | canceling.value = false; |
| | | } |
| | | } |
| | | |
| | | onBeforeUnmount(stopPolling); |
| | | |
| | | return { |
| | | task, |
| | | canceling, |
| | | cancel, |
| | | reset, |
| | | stopPolling, |
| | | start, |
| | | }; |
| | | } |