| | |
| | | |
| | | 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 emit = defineEmits(["success"]); |
| | | |
| | | // 所选品种主计量单位类型:48千克/100千克 -> 每托 N 袋;吨 -> 无托盘直接输袋码数 |
| | | // 所选品种主计量单位类型:48袋/100袋 -> 每托 N 袋;吨 -> 无托盘直接输袋码数 |
| | | const unitType = ref(""); |
| | | const kgPerPallet = ref<number | null>(null); |
| | | const bagsPerPallet = ref<number | null>(null); |
| | | const isTon = ref(false); |
| | | 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(() => { |
| | | if (unitTypeError.value) { |
| | | return "该品种主计量单位未配置类型(仅支持 48千克/100千克/吨),请先前往「计量单位」维护后重试"; |
| | | return "该品种主计量单位未配置类型(仅支持 48袋/100袋/吨),请先前往「计量单位」维护后重试"; |
| | | } |
| | | if (!unitType.value) { |
| | | return ""; |
| | |
| | | bagCountVal.value ? `,将生成 ${bagCountVal.value} 个袋码` : "" |
| | | }`; |
| | | } |
| | | const per = kgPerPallet.value ?? 0; |
| | | const per = bagsPerPallet.value ?? 0; |
| | | const count = palletCountVal.value ?? 0; |
| | | return `计量单位类型为「${unitType.value}」:每托 ${per} 袋${ |
| | | count > 0 |
| | |
| | | rules: "selectRequired", |
| | | }, |
| | | ]; |
| | | if (kgPerPallet.value) { |
| | | if (bagsPerPallet.value) { |
| | | schema.push({ |
| | | fieldName: "palletCount", |
| | | label: "托盘数", |
| | |
| | | /** 选择品种后读取主计量单位类型,动态切换托盘数/袋码数输入 */ |
| | | async function handleItemChange(item?: MdmItemApi.Item) { |
| | | unitType.value = ""; |
| | | kgPerPallet.value = null; |
| | | bagsPerPallet.value = null; |
| | | isTon.value = false; |
| | | unitTypeError.value = false; |
| | | palletCountVal.value = null; |
| | |
| | | if (type === "吨") { |
| | | isTon.value = true; |
| | | } else { |
| | | const matcher = /^(\d{1,5})千克$/.exec(type); |
| | | const matcher = /^(\d{1,5})袋$/.exec(type); |
| | | if (matcher) { |
| | | kgPerPallet.value = Number(matcher[1]); |
| | | bagsPerPallet.value = Number(matcher[1]); |
| | | } else { |
| | | unitTypeError.value = true; |
| | | } |
| | |
| | | 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 = ""; |
| | | kgPerPallet.value = null; |
| | | bagsPerPallet.value = null; |
| | | isTon.value = false; |
| | | unitTypeError.value = false; |
| | | palletCountVal.value = null; |
| | |
| | | } |
| | | }, |
| | | }); |
| | | |
| | | 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"> |
| | | <Form class="mx-4" /> |
| | | <div |
| | | v-if="tipText" |
| | | class="mx-4 mb-2 rounded bg-gray-50 px-3 py-2 text-sm" |
| | | :class="unitTypeError ? 'text-red-500' : 'text-gray-500'" |
| | | > |
| | | {{ tipText }} |
| | | </div> |
| | | <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" |
| | | class="mx-4 mb-2 rounded bg-gray-50 px-3 py-2 text-sm" |
| | | :class="unitTypeError ? 'text-red-500' : 'text-gray-500'" |
| | | > |
| | | {{ tipText }} |
| | | </div> |
| | | </template> |
| | | </Modal> |
| | | </template> |