From efb6a9f838cf1c036ca3bbfd0c6c307133846566 Mon Sep 17 00:00:00 2001 From: geht Date: Fri, 17 Apr 2026 19:00:30 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9EPrintDot=E6=A1=A5=E6=8E=A5?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=94=AF=E6=8C=81=E6=9C=AC=E5=9C=B0?= =?UTF-8?q?=E6=89=93=E5=8D=B0=E6=9C=BA=E8=BF=9E=E6=8E=A5=E5=92=8C=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=EF=BC=8C=E4=BC=98=E5=8C=96=E6=89=93=E5=8D=B0=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF=E8=AE=BE=E8=AE=A1=EF=BC=8C=E5=85=81=E8=AE=B8=E5=A4=9A?= =?UTF-8?q?=E9=A1=B5=E8=A1=A8=E6=A0=BC=E9=87=8D=E5=A4=8D=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=EF=BC=8C=E6=94=B9=E8=BF=9B=E6=89=93=E5=8D=B0=E9=A2=84=E8=A7=88?= =?UTF-8?q?=E5=92=8C=E8=AE=BE=E8=AE=A1=E5=99=A8=E7=95=8C=E9=9D=A2=EF=BC=8C?= =?UTF-8?q?=E7=A1=AE=E4=BF=9D=E7=94=A8=E6=88=B7=E4=BD=93=E9=AA=8C=E6=B5=81?= =?UTF-8?q?=E7=95=85=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NativeTemplateListPreviewModal.vue | 33 ++ .../src/views/print/template/index.vue | 187 ++++++--- .../template/native/NativePrintDesigner.vue | 66 +++- .../native/components/DesignerCanvas.vue | 123 +++++- .../native/components/ElementWrapper.vue | 25 +- .../native/components/PropertiesPanel.vue | 7 + .../print/template/native/core/dragResize.ts | 36 +- .../core/nativeTemplateStyleSerialize.ts | 1 + .../template/native/core/printRenderer.ts | 374 ++++++++++++++++-- .../views/print/template/native/core/types.ts | 2 + .../template/native/core/useDesignerStore.ts | 1 + .../print/template/utils/printDotBridge.ts | 201 ++++++++++ .../template/utils/printHtmlToPdfBase64.ts | 212 ++++++++++ .../template/utils/printNativeViaPrintDot.ts | 38 ++ 14 files changed, 1196 insertions(+), 110 deletions(-) create mode 100644 jeecgboot-vue3/src/views/print/template/utils/printDotBridge.ts create mode 100644 jeecgboot-vue3/src/views/print/template/utils/printHtmlToPdfBase64.ts create mode 100644 jeecgboot-vue3/src/views/print/template/utils/printNativeViaPrintDot.ts diff --git a/jeecgboot-vue3/src/views/print/template/components/NativeTemplateListPreviewModal.vue b/jeecgboot-vue3/src/views/print/template/components/NativeTemplateListPreviewModal.vue index 4e42c93f..74023dfb 100644 --- a/jeecgboot-vue3/src/views/print/template/components/NativeTemplateListPreviewModal.vue +++ b/jeecgboot-vue3/src/views/print/template/components/NativeTemplateListPreviewModal.vue @@ -95,6 +95,17 @@ 打印 + + + PrintDot + +
@@ -152,6 +163,7 @@ import { generateNativeMockDataObject } from '../native/core/nativeMockData'; import { normalizeImportedNativeSchema } from '../native/core/nativeSchemaNormalize'; import type { NativeTemplateSchema } from '../native/core/types'; + import { printNativeSchemaViaPrintDot } from '../utils/printNativeViaPrintDot'; const props = defineProps<{ open: boolean; @@ -171,6 +183,7 @@ }); const loading = ref(false); + const printDotLoading = ref(false); const errorText = ref(''); const schema = ref(null); const canvasJsonText = ref('{}'); @@ -372,6 +385,26 @@ window.setTimeout(() => updateContentMeasure(), 400); } + async function handlePrintDotPrint() { + if (!schema.value) { + createMessage.warning('模板未加载'); + return; + } + printDotLoading.value = true; + try { + await printNativeSchemaViaPrintDot({ + schema: schema.value, + data: previewData.value, + jobName: props.templateId ? `tpl-${props.templateId}` : 'native-preview', + }); + createMessage.success('已通过 PrintDot 提交打印'); + } catch (e: any) { + createMessage.error(e?.message || 'PrintDot 打印失败'); + } finally { + printDotLoading.value = false; + } + } + /** 调用浏览器打印预览 iframe 内文档(与当前预览 HTML 一致) */ function handleBrowserPrint() { const win = previewIframeRef.value?.contentWindow; diff --git a/jeecgboot-vue3/src/views/print/template/index.vue b/jeecgboot-vue3/src/views/print/template/index.vue index f3b365fd..d4c534bb 100644 --- a/jeecgboot-vue3/src/views/print/template/index.vue +++ b/jeecgboot-vue3/src/views/print/template/index.vue @@ -20,6 +20,20 @@ /> 添加打印机 刷新打印机 + PrintDot 桥接 + + 新增原生模板 快速打印 @@ -50,8 +64,13 @@ 按模板样式打印(推荐) Lodop实验(模板样式) 前端转PDF后端打印 + PrintDot 本地桥接(PDF) 服务端直打(纯文本) +
+ 需本机运行 PrintDot 客户端(默认 WebSocket ws://127.0.0.1:1122/ws)。勾选「PrintDot + 桥接」并刷新打印机后,可选择桥接器上报的打印机;支持原生模板与 hiprint 模板(均在前端转 PDF 后送出)。 +
('templateStyle'); + const quickPrintMode = ref<'templateStyle' | 'lodopTemplate' | 'pdfServer' | 'printDotBridge' | 'serverText'>( + 'templateStyle', + ); + const LS_PRINT_DOT_ENABLED = 'qhmes_print_dot_enabled'; + const printDotEnabled = ref(localStorage.getItem(LS_PRINT_DOT_ENABLED) === '1'); + const printDotCfg = getPrintDotBridgeConfig(); + const printDotWsUrl = ref(printDotCfg.wsUrl); + const printDotKey = ref(printDotCfg.key); + + function persistPrintDotConfig() { + setPrintDotBridgeConfig(printDotWsUrl.value, printDotKey.value); + } + + function onPrintDotEnabledChange() { + localStorage.setItem(LS_PRINT_DOT_ENABLED, printDotEnabled.value ? '1' : '0'); + void refreshPrinterOptions(false); + } /** 技能转换打印:示例数据(与快速打印占位说明一致) */ const SKILL_DATA_JSON_EXAMPLE = `{ "docNo": "MO-001", @@ -311,10 +359,31 @@ }); } printerOptions.value = Array.from(optionMap.values()); + if (printDotEnabled.value) { + try { + const dotList = await fetchPrintDotPrinters(); + dotList.forEach((p) => { + const name = String(p.name || '').trim(); + if (!name) { + return; + } + if (!optionMap.has(name)) { + optionMap.set(name, { label: `[PrintDot] ${name}`, value: name }); + } + }); + if (showMessage && dotList.length) { + createMessage.success(`PrintDot 已连接,识别 ${dotList.length} 台打印机`); + } + } catch (e: any) { + if (showMessage) { + createMessage.warning(`PrintDot:${e?.message || '无法连接本地桥接器'}`); + } + } + } if (showMessage) { if (names.length) { createMessage.success(`已从服务端识别到 ${names.length} 台打印机`); - } else { + } else if (!printDotEnabled.value) { const reason = String(payload?.capability?.localReason || '').trim(); createMessage.warning(`服务端未返回可用打印机。${reason || '请在后端配置网络打印机后重试。'}`); } @@ -510,7 +579,7 @@ const panel = Array.isArray(templateJson?.panels) && templateJson.panels.length ? templateJson.panels[0] : {}; const widthMm = Number(panel?.width || 210); const heightMm = Number(panel?.height || 297); - const pdfBase64 = await buildPdfBase64FromTemplate(html, widthMm, heightMm); + const pdfBase64 = await buildPdfBase64FromHtmlFragment(html, widthMm, heightMm); const printer = String(params.printerName || '').trim(); await directPrintPdf({ templateCode, @@ -1249,54 +1318,6 @@ `; } - function mmToPx(mm: number) { - return (mm * 96) / 25.4; - } - - function arrayBufferToBase64(buffer: ArrayBuffer): string { - const bytes = new Uint8Array(buffer); - const chunkSize = 0x8000; - let binary = ''; - for (let i = 0; i < bytes.length; i += chunkSize) { - const chunk = bytes.subarray(i, i + chunkSize); - binary += String.fromCharCode(...chunk); - } - return btoa(binary); - } - - async function buildPdfBase64FromTemplate(html: string, widthMm: number, heightMm: number): Promise { - const [{ jsPDF }, html2canvasModule] = await Promise.all([import('jspdf'), import('html2canvas')]); - const html2canvas = html2canvasModule.default; - const container = document.createElement('div'); - container.style.position = 'fixed'; - container.style.left = '-20000px'; - container.style.top = '0'; - container.style.width = `${Math.max(1, mmToPx(widthMm))}px`; - container.style.background = '#fff'; - container.style.zIndex = '-1'; - container.innerHTML = `
${html}
`; - document.body.appendChild(container); - try { - const target = (container.querySelector('.lodop-print-root') || container) as HTMLElement; - await new Promise((resolve) => setTimeout(resolve, 80)); - const canvas = await html2canvas(target, { - backgroundColor: '#ffffff', - scale: 2, - useCORS: true, - allowTaint: true, - logging: false, - }); - const orientation = widthMm > heightMm ? 'landscape' : 'portrait'; - const pdf = new jsPDF({ orientation, unit: 'mm', format: [widthMm, heightMm] }); - const imgData = canvas.toDataURL('image/jpeg', 0.95); - pdf.addImage(imgData, 'JPEG', 0, 0, widthMm, heightMm); - const buffer = pdf.output('arraybuffer'); - return arrayBufferToBase64(buffer); - } finally { - container.remove(); - } - } - async function handleQuickPrint() { const templateCode = String(quickPrintForm.value.templateCode || '').trim(); if (!templateCode) { @@ -1325,6 +1346,70 @@ dataJson, }); createMessage.success('已提交服务端直打任务'); + } else if (quickPrintMode.value === 'printDotBridge') { + const tplData = (await queryByCode(templateCode)) as Record; + const templateJsonText = String(tplData?.templateJson || '').trim(); + if (!templateJsonText) { + createMessage.error('模板JSON为空'); + return; + } + let templateJson: any; + try { + templateJson = JSON.parse(templateJsonText); + } catch (error: any) { + createMessage.error(`模板JSON格式错误:${error?.message || '未知错误'}`); + return; + } + let pdfBase64: string; + if (templateJson?.engine === 'native') { + const normalized = normalizeImportedNativeSchema(templateJson); + const pw = Number(tplData?.paperWidthMm); + const ph = Number(tplData?.paperHeightMm); + if (pw > 0) { + normalized.page.width = pw; + } + if (ph > 0) { + normalized.page.height = ph; + } + const fullHtml = await renderNativePrintHtml(normalized, dataJson); + const inner = extractBodyInnerHtmlFromFullDocument(fullHtml); + pdfBase64 = await buildPdfBase64FromHtmlFragment( + inner, + normalized.page.width, + normalized.page.height, + { paginate: true }, + ); + } else { + await initHiprintForQuickPrint(); + const runtimeTemplate = new hiprint.PrintTemplate({ + template: templateJson, + }); + const html = optimizeMergedHeaderHtml(await resolveTemplateHtml(runtimeTemplate, dataJson), templateJson); + if (!html) { + createMessage.error('未能生成预览 HTML,无法转 PDF'); + return; + } + const panel = Array.isArray(templateJson?.panels) && templateJson.panels.length ? templateJson.panels[0] : {}; + const widthMm = Number(panel?.width || 210); + const heightMm = Number(panel?.height || 297); + pdfBase64 = await buildPdfBase64FromHtmlFragment(html, widthMm, heightMm); + } + const dotPrinters = await fetchPrintDotPrinters(); + const resolvedPrinter = resolvePrintDotPrinterName(printer, dotPrinters); + if (!resolvedPrinter) { + createMessage.error('PrintDot:请选择具体打印机,或确认桥接器已返回打印机列表'); + return; + } + const dotResult = await printDotSendPdf({ + printer: resolvedPrinter, + pdfBase64, + jobName: templateCode, + timeoutMs: 180000, + }); + if (!dotResult.ok) { + throw new Error(dotResult.message || 'PrintDot 打印失败'); + } + createMessage.success('已通过 PrintDot 提交打印'); } else { await initHiprintForQuickPrint(); if (quickPrintMode.value === 'pdfServer') { diff --git a/jeecgboot-vue3/src/views/print/template/native/NativePrintDesigner.vue b/jeecgboot-vue3/src/views/print/template/native/NativePrintDesigner.vue index 0207e6a4..b97ca25b 100644 --- a/jeecgboot-vue3/src/views/print/template/native/NativePrintDesigner.vue +++ b/jeecgboot-vue3/src/views/print/template/native/NativePrintDesigner.vue @@ -13,6 +13,7 @@ 下移图层 即时预览 打印 + PrintDot 打印 保存模板
@@ -135,8 +136,21 @@ - - + +
+