| | |
| | | <script lang="ts" setup> |
| | | import type { MesWmBarcodeApi } from '#/api/mes/wm/barcode'; |
| | | |
| | | import { ref } from 'vue'; |
| | | |
| | | import { BarcodeBizTypeEnum } from '@vben/constants'; |
| | | |
| | | import { Button, message } from 'ant-design-vue'; |
| | | |
| | | import { |
| | | getBarcodeByBusiness, |
| | | renderBarcodeImage, |
| | | } from '#/api/mes/wm/barcode'; |
| | | |
| | | defineOptions({ name: 'WlsPrinterLabel' }); |
| | | |
| | | withDefaults( |
| | | const props = withDefaults( |
| | | defineProps<{ |
| | | bizCode?: string; |
| | | bizId?: number; |
| | | bizType?: string; |
| | | bizName?: string; |
| | | bizType?: number | string; |
| | | labelText?: string; |
| | | }>(), |
| | | { |
| | | bizCode: undefined, |
| | | bizId: undefined, |
| | | bizName: undefined, |
| | | bizType: undefined, |
| | | labelText: '标签打印', |
| | | }, |
| | | ); |
| | | |
| | | function handlePrint() { |
| | | message.warning('标签打印功能暂未实现,敬请期待'); |
| | | const printing = ref(false); |
| | | |
| | | function resolveBizType(value?: number | string) { |
| | | if (typeof value === 'number') { |
| | | return value; |
| | | } |
| | | const normalized = value?.trim().toUpperCase(); |
| | | if (!normalized) { |
| | | return undefined; |
| | | } |
| | | // ITEM_BATCH 是部分入库页面沿用的旧别名,实际仍对应“批次”。 |
| | | if (normalized === 'ITEM_BATCH') { |
| | | return BarcodeBizTypeEnum.BATCH; |
| | | } |
| | | const numericValue = Number(normalized); |
| | | if (Number.isInteger(numericValue)) { |
| | | return numericValue; |
| | | } |
| | | const enumValue = |
| | | BarcodeBizTypeEnum[ |
| | | normalized as keyof typeof BarcodeBizTypeEnum |
| | | ]; |
| | | return typeof enumValue === 'number' ? enumValue : undefined; |
| | | } |
| | | |
| | | function getBizTypeLabel(bizType: number) { |
| | | const labels: Partial<Record<BarcodeBizTypeEnum, string>> = { |
| | | [BarcodeBizTypeEnum.AREA]: '库位', |
| | | [BarcodeBizTypeEnum.BATCH]: '批次', |
| | | [BarcodeBizTypeEnum.CLIENT]: '客户', |
| | | [BarcodeBizTypeEnum.ITEM]: '物料', |
| | | [BarcodeBizTypeEnum.LOCATION]: '库区', |
| | | [BarcodeBizTypeEnum.MACHINERY]: '设备', |
| | | [BarcodeBizTypeEnum.PACKAGE]: '装箱单', |
| | | [BarcodeBizTypeEnum.PROCARD]: '流转卡', |
| | | [BarcodeBizTypeEnum.SN]: 'SN 码', |
| | | [BarcodeBizTypeEnum.STOCK]: '库存', |
| | | [BarcodeBizTypeEnum.TOOL]: '工具', |
| | | [BarcodeBizTypeEnum.USER]: '人员', |
| | | [BarcodeBizTypeEnum.VENDOR]: '供应商', |
| | | [BarcodeBizTypeEnum.WAREHOUSE]: '仓库', |
| | | [BarcodeBizTypeEnum.WORKORDER]: '工单', |
| | | [BarcodeBizTypeEnum.WORKSHOP]: '车间', |
| | | [BarcodeBizTypeEnum.WORKSTATION]: '工作站', |
| | | }; |
| | | return labels[bizType as BarcodeBizTypeEnum] || '业务'; |
| | | } |
| | | |
| | | function escapeHtml(value?: null | string) { |
| | | return (value || '') |
| | | .replaceAll('&', '&') |
| | | .replaceAll('<', '<') |
| | | .replaceAll('>', '>') |
| | | .replaceAll('"', '"') |
| | | .replaceAll("'", '''); |
| | | } |
| | | |
| | | async function getBarcodeForPrint(bizType: number) { |
| | | const barcode = await getBarcodeByBusiness(bizType, props.bizId!); |
| | | if (barcode?.content) { |
| | | return barcode; |
| | | } |
| | | if (!props.bizCode) { |
| | | throw new Error('缺少业务编码,无法打印标签'); |
| | | } |
| | | |
| | | // 历史批次可能未配置条码规则。此时使用业务编码即时生成 CODE128, |
| | | // 只用于本次打印,不写入条码表,避免“条码配置不存在”阻断业务。 |
| | | return { |
| | | bizCode: props.bizCode, |
| | | bizId: props.bizId, |
| | | bizName: props.bizName || props.bizCode, |
| | | bizType, |
| | | content: props.bizCode, |
| | | } satisfies MesWmBarcodeApi.Barcode; |
| | | } |
| | | |
| | | function writePrintDocument( |
| | | printWindow: Window, |
| | | barcode: MesWmBarcodeApi.Barcode, |
| | | imageBase64: string, |
| | | bizType: number, |
| | | ) { |
| | | const typeLabel = getBizTypeLabel(bizType); |
| | | const isQrCode = barcode.format === 1; |
| | | printWindow.document.open(); |
| | | printWindow.document.write(`<!doctype html> |
| | | <html lang="zh-CN"> |
| | | <head> |
| | | <meta charset="UTF-8" /> |
| | | <title>${escapeHtml(typeLabel)}标签</title> |
| | | <style> |
| | | @page { size: 80mm 55mm; margin: 0; } |
| | | * { box-sizing: border-box; } |
| | | html, body { width: 80mm; height: 55mm; margin: 0; color: #000; font-family: "Microsoft YaHei", Arial, sans-serif; } |
| | | .label { width: 80mm; height: 55mm; padding: 3mm; border: .35mm solid #000; display: grid; grid-template-columns: ${isQrCode ? '31mm 1fr' : '1fr'}; gap: 3mm; overflow: hidden; } |
| | | .barcode { display: flex; align-items: center; justify-content: center; min-width: 0; } |
| | | .barcode img { display: block; max-width: 100%; max-height: ${isQrCode ? '38mm' : '22mm'}; object-fit: contain; } |
| | | .info { min-width: 0; display: flex; flex-direction: column; justify-content: center; } |
| | | .title { margin-bottom: 2mm; padding-bottom: 1.5mm; border-bottom: .3mm solid #000; font-size: 15pt; font-weight: 700; text-align: center; } |
| | | .row { margin: .8mm 0; font-size: 9.5pt; line-height: 1.35; overflow-wrap: anywhere; } |
| | | .key { font-weight: 700; } |
| | | .content { margin-top: 1mm; font-family: Consolas, monospace; font-size: 7.5pt; text-align: center; overflow-wrap: anywhere; } |
| | | ${isQrCode ? '' : '.label { grid-template-rows: auto 1fr; } .barcode { order: 2; } .info { order: 1; } .title { margin-bottom: 1mm; } .row { display: inline-block; margin-right: 4mm; }'} |
| | | @media screen { body { margin: 8px; } } |
| | | </style> |
| | | </head> |
| | | <body> |
| | | <main class="label"> |
| | | <section class="barcode"> |
| | | <img src="${imageBase64}" alt="${escapeHtml(typeLabel)}条码" /> |
| | | </section> |
| | | <section class="info"> |
| | | <div class="title">${escapeHtml(typeLabel)}标签</div> |
| | | <div class="row"><span class="key">业务编码:</span>${escapeHtml(barcode.bizCode || props.bizCode)}</div> |
| | | <div class="row"><span class="key">业务名称:</span>${escapeHtml(barcode.bizName || props.bizName || '-')}</div> |
| | | <div class="content">${escapeHtml(barcode.content)}</div> |
| | | </section> |
| | | </main> |
| | | <script> |
| | | window.addEventListener('load', function () { |
| | | window.focus(); |
| | | window.onafterprint = function () { window.close(); }; |
| | | setTimeout(function () { window.print(); }, 150); |
| | | }); |
| | | <\/script> |
| | | </body> |
| | | </html>`); |
| | | printWindow.document.close(); |
| | | } |
| | | |
| | | async function handlePrint() { |
| | | if (printing.value) { |
| | | return; |
| | | } |
| | | const bizType = resolveBizType(props.bizType); |
| | | if (!props.bizId || !bizType) { |
| | | message.warning('缺少业务类型或业务编号,无法打印标签'); |
| | | return; |
| | | } |
| | | |
| | | // 在用户点击事件中同步打开窗口,避免异步请求结束后被浏览器拦截。 |
| | | const printWindow = window.open('', '_blank', 'width=900,height=650'); |
| | | if (!printWindow) { |
| | | message.error('无法打开打印窗口,请允许浏览器弹出窗口后重试'); |
| | | return; |
| | | } |
| | | printWindow.opener = null; |
| | | printWindow.document.write( |
| | | '<!doctype html><html><head><meta charset="UTF-8"><title>生成标签</title></head><body style="font-family:Microsoft YaHei;padding:24px">正在生成标签,请稍候……</body></html>', |
| | | ); |
| | | printWindow.document.close(); |
| | | |
| | | printing.value = true; |
| | | try { |
| | | const barcode = await getBarcodeForPrint(bizType); |
| | | const isQrCode = barcode.format === 1; |
| | | const imageBase64 = await renderBarcodeImage( |
| | | barcode.content!, |
| | | barcode.format, |
| | | isQrCode ? 360 : 720, |
| | | isQrCode ? 360 : 220, |
| | | ); |
| | | if (!imageBase64?.startsWith('data:image/png;base64,')) { |
| | | throw new Error('服务端未返回有效的标签图片'); |
| | | } |
| | | if (!printWindow.closed) { |
| | | writePrintDocument(printWindow, barcode, imageBase64, bizType); |
| | | } |
| | | } catch (error) { |
| | | if (!printWindow.closed) { |
| | | printWindow.close(); |
| | | } |
| | | const errorMessage = |
| | | error instanceof Error ? error.message : '标签打印失败,请稍后重试'; |
| | | message.error(errorMessage); |
| | | } finally { |
| | | printing.value = false; |
| | | } |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <Button type="link" @click="handlePrint"> |
| | | <Button :loading="printing" type="link" @click="handlePrint"> |
| | | {{ labelText }} |
| | | </Button> |
| | | </template> |