新增PrintDot桥接功能,支持本地打印机连接和配置,优化打印模板设计,允许多页表格重复显示,改进打印预览和设计器界面,确保用户体验流畅。
This commit is contained in:
@@ -20,6 +20,20 @@
|
||||
/>
|
||||
<a-button @click="addManualPrinter">添加打印机</a-button>
|
||||
<a-button @click="refreshPrinterOptions">刷新打印机</a-button>
|
||||
<a-checkbox v-model:checked="printDotEnabled" @change="onPrintDotEnabledChange">PrintDot 桥接</a-checkbox>
|
||||
<a-input
|
||||
v-model:value="printDotWsUrl"
|
||||
style="width: 200px"
|
||||
placeholder="WS 地址"
|
||||
@blur="persistPrintDotConfig"
|
||||
/>
|
||||
<a-input-password
|
||||
v-model:value="printDotKey"
|
||||
style="width: 120px"
|
||||
placeholder="密钥(可选)"
|
||||
autocomplete="new-password"
|
||||
@blur="persistPrintDotConfig"
|
||||
/>
|
||||
</a-space>
|
||||
<a-button type="primary" ghost @click="handleCreateNative" v-auth="'print:template:add'">新增原生模板</a-button>
|
||||
<a-button type="primary" @click="openQuickPrintModal" v-auth="'print:template:list'">快速打印</a-button>
|
||||
@@ -50,8 +64,13 @@
|
||||
<a-radio-button value="templateStyle">按模板样式打印(推荐)</a-radio-button>
|
||||
<a-radio-button value="lodopTemplate">Lodop实验(模板样式)</a-radio-button>
|
||||
<a-radio-button value="pdfServer">前端转PDF后端打印</a-radio-button>
|
||||
<a-radio-button value="printDotBridge">PrintDot 本地桥接(PDF)</a-radio-button>
|
||||
<a-radio-button value="serverText">服务端直打(纯文本)</a-radio-button>
|
||||
</a-radio-group>
|
||||
<div v-if="quickPrintMode === 'printDotBridge'" style="font-size: 12px; color: rgba(0, 0, 0, 0.55)">
|
||||
需本机运行 PrintDot 客户端(默认 WebSocket ws://127.0.0.1:1122/ws)。勾选「PrintDot
|
||||
桥接」并刷新打印机后,可选择桥接器上报的打印机;支持原生模板与 hiprint 模板(均在前端转 PDF 后送出)。
|
||||
</div>
|
||||
<a-space style="width: 100%">
|
||||
<a-select
|
||||
v-model:value="quickPrintForm.templateCode"
|
||||
@@ -180,6 +199,19 @@
|
||||
import PrintTemplateModal from './components/PrintTemplateModal.vue';
|
||||
import NativeTemplateListPreviewModal from './components/NativeTemplateListPreviewModal.vue';
|
||||
import { QUICK_PRINT_PREVIEW_STORAGE_KEY } from './quickPrintPreviewStorage';
|
||||
import {
|
||||
buildPdfBase64FromHtmlFragment,
|
||||
extractBodyInnerHtmlFromFullDocument,
|
||||
} from './utils/printHtmlToPdfBase64';
|
||||
import {
|
||||
fetchPrintDotPrinters,
|
||||
getPrintDotBridgeConfig,
|
||||
printDotSendPdf,
|
||||
resolvePrintDotPrinterName,
|
||||
setPrintDotBridgeConfig,
|
||||
} from './utils/printDotBridge';
|
||||
import { normalizeImportedNativeSchema } from './native/core/nativeSchemaNormalize';
|
||||
import { renderNativePrintHtml } from './native/core/printRenderer';
|
||||
|
||||
defineOptions({ name: 'PrintTemplateList' });
|
||||
|
||||
@@ -202,7 +234,23 @@
|
||||
printerName: '__system_default__',
|
||||
dataJson: '',
|
||||
});
|
||||
const quickPrintMode = ref<'templateStyle' | 'lodopTemplate' | 'pdfServer' | 'serverText'>('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 @@
|
||||
</html>`;
|
||||
}
|
||||
|
||||
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<string> {
|
||||
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 = `<div class="lodop-print-root" style="width:${widthMm}mm;margin:0;padding:0;background:#fff;">${html}</div>`;
|
||||
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<string, any>;
|
||||
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') {
|
||||
|
||||
Reference in New Issue
Block a user