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 @@ - - + +
+