diff --git a/jeecgboot-vue3/src/views/print/template/index.vue b/jeecgboot-vue3/src/views/print/template/index.vue index 44d1757..c09c86c 100644 --- a/jeecgboot-vue3/src/views/print/template/index.vue +++ b/jeecgboot-vue3/src/views/print/template/index.vue @@ -10,7 +10,7 @@ allow-clear show-search option-filter-prop="label" - placeholder="选择打印机(本地/网络)" + :placeholder="printerSelectPlaceholder" /> - + - - 按模板样式打印(推荐) - Lodop实验(模板样式) - 前端转PDF后端打印 - PrintDot 本地桥接(PDF) - 服务端直打(纯文本) - -
- 需本机运行 PrintDot 客户端(默认 WebSocket ws://127.0.0.1:1122/ws)。勾选「PrintDot - 桥接」并刷新打印机后,可选择桥接器上报的打印机;支持原生模板与 hiprint 模板(均在前端转 PDF 后送出)。 -
- + +
+
模板编号
- - - - 预览 -
- 打开「打印设计器」并自动执行与工具栏「预览」相同的逻辑(同一套 hiprint 预览、样式注入与表格合并后处理),确保与在设计器内预览一致。
- - 预览(与设计器一致) - + + 取消 + 打开预览 + @@ -189,7 +174,6 @@ deleteOne, batchDelete, queryPrinters, - directPrint, directPrintPdf, queryByCode, queryById, @@ -198,20 +182,12 @@ import { resolveProviders } from './hiprint/qhmesProvider'; 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 { buildPdfBase64FromHtmlFragment } from './utils/printHtmlToPdfBase64'; import { fetchPrintDotPrinters, getPrintDotBridgeConfig, - printDotSendPdf, - resolvePrintDotPrinterName, setPrintDotBridgeConfig, } from './utils/printDotBridge'; - import { normalizeImportedNativeSchema } from './native/core/nativeSchemaNormalize'; - import { renderNativePrintHtml } from './native/core/printRenderer'; import { PRINT_TEMPLATE_SELECTED_PRINTER_KEY } from './utils/printNativeViaPrintDot'; defineOptions({ name: 'PrintTemplateList' }); @@ -226,18 +202,10 @@ const selectedPrinterName = ref(); const printerOptions = ref>([]); const manualPrinterName = ref(''); - const quickPrintVisible = ref(false); - const quickPrintLoading = ref(false); - const quickPreviewLoading = ref(false); + const quickNativePrintOpen = ref(false); + const quickNativeTemplateCode = ref(''); + const quickNativePrintLoading = ref(false); const templateCodeOptions = ref>([]); - const quickPrintForm = ref<{ templateCode: string; printerName?: string; dataJson: string }>({ - templateCode: '', - printerName: '__system_default__', - dataJson: '', - }); - 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(); @@ -246,8 +214,16 @@ function persistPrintDotConfig() { setPrintDotBridgeConfig(printDotWsUrl.value, printDotKey.value); + // 桥接开启时 WS/密钥变更后重新拉取桥接器打印机列表 + if (printDotEnabled.value) { + void refreshPrinterOptions(false); + } } + const printerSelectPlaceholder = computed(() => + printDotEnabled.value ? '选择打印机(PrintDot 桥接)' : '选择打印机(本地/网络)', + ); + function onPrintDotEnabledChange() { localStorage.setItem(LS_PRINT_DOT_ENABLED, printDotEnabled.value ? '1' : '0'); void refreshPrinterOptions(false); @@ -338,6 +314,50 @@ const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext; async function refreshPrinterOptions(showMessage = true) { + const optionMap = new Map(); + // 保留「系统默认」:本地模式交给浏览器/队列;桥接模式由 resolvePrintDotPrinterName 映射为桥接器默认打印机 + optionMap.set('__system_default__', { label: '系统默认打印机', value: '__system_default__' }); + + if (printDotEnabled.value) { + try { + const dotList = await fetchPrintDotPrinters(); + dotList.forEach((p) => { + const name = String(p.name || '').trim(); + if (!name) { + return; + } + const defMark = p.isDefault ? '(默认)' : ''; + optionMap.set(name, { label: `${name}${defMark}`, value: name }); + }); + if (selectedPrinterName.value && !optionMap.has(selectedPrinterName.value)) { + optionMap.set(selectedPrinterName.value, { + label: `${selectedPrinterName.value}(手动)`, + value: selectedPrinterName.value, + }); + } + printerOptions.value = Array.from(optionMap.values()); + if (showMessage) { + if (dotList.length) { + createMessage.success(`已从 PrintDot 桥接识别 ${dotList.length} 台打印机`); + } else { + createMessage.warning('PrintDot 已连接但未返回打印机列表'); + } + } + } catch (e: any) { + if (selectedPrinterName.value && !optionMap.has(selectedPrinterName.value)) { + optionMap.set(selectedPrinterName.value, { + label: `${selectedPrinterName.value}(手动)`, + value: selectedPrinterName.value, + }); + } + printerOptions.value = Array.from(optionMap.values()); + if (showMessage) { + createMessage.warning(`PrintDot:${e?.message || '无法连接本地桥接器'}`); + } + } + return; + } + const payload = (await queryPrinters()) as Record; const names = [ ...(Array.isArray(payload?.serverPrinters) ? payload.serverPrinters : []), @@ -346,9 +366,6 @@ .map((item) => String(item || '').trim()) .filter(Boolean) .filter((item, index, arr) => arr.indexOf(item) === index); - const optionMap = new Map(); - // 永远保留“系统默认打印机”兜底项,避免插件不可用时无法选择 - optionMap.set('__system_default__', { label: '系统默认打印机', value: '__system_default__' }); names.forEach((item) => { optionMap.set(item, { label: item, value: item }); }); @@ -359,31 +376,10 @@ }); } 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 if (!printDotEnabled.value) { + } else { const reason = String(payload?.capability?.localReason || '').trim(); createMessage.warning(`服务端未返回可用打印机。${reason || '请在后端配置网络打印机后重试。'}`); } @@ -416,82 +412,50 @@ } async function openQuickPrintModal() { - quickPrintForm.value.printerName = selectedPrinterName.value || '__system_default__'; - quickPrintMode.value = 'templateStyle'; + quickNativeTemplateCode.value = ''; if (!templateCodeOptions.value.length) { await loadTemplateCodeOptions(); } - quickPrintVisible.value = true; + quickNativePrintOpen.value = true; } - /** - * 跳转打印设计器并自动调用与设计器「预览」相同的 previewTemplate 逻辑(见 PrintDesigner runQuickPrintPreviewFromSessionStorage) - */ - async function handleQuickPrintDesignerPreview() { - const templateCode = String(quickPrintForm.value.templateCode || '').trim(); + /** 快速打印:仅支持原生模板,打开与列表相同的预览弹窗(浏览器打印 / PrintDot) */ + async function confirmQuickOpenNativePreview() { + const templateCode = String(quickNativeTemplateCode.value || '').trim(); if (!templateCode) { createMessage.warning('请先选择模板编号'); return; } - const dataText = String(quickPrintForm.value.dataJson || '').trim(); - if (!dataText) { - createMessage.warning('请先输入打印数据 JSON'); - return; - } - try { - JSON.parse(dataText); - } catch (error: any) { - createMessage.error(`打印数据JSON格式错误:${error?.message || '未知错误'}`); - return; - } - quickPreviewLoading.value = true; + quickNativePrintLoading.value = true; try { const tpl = (await queryByCode(templateCode)) as Record; - const id = String(tpl?.id ?? (tpl as any)?.result?.id ?? '').trim(); + const row = (tpl?.result ?? tpl) as Record; + const id = String(row?.id ?? tpl?.id ?? '').trim(); if (!id) { - createMessage.error(`未找到模板记录主键,无法打开设计器预览。返回字段:${Object.keys(tpl || {}).join(', ') || '空'}`); + createMessage.error('未找到该模板记录,请确认编号是否正确'); return; } - sessionStorage.setItem( - QUICK_PRINT_PREVIEW_STORAGE_KEY, - JSON.stringify({ - dataJsonText: dataText, - }), - ); - const previewQuery = { - id, - quickPrintPreview: '1', - _qpt: String(Date.now()), - } as Record; - const navigateToDesignerPreview = async () => { - const isDuplicateNav = (err: unknown) => /redundant|duplicated|Avoided/i.test(String((err as any)?.message ?? err ?? '')); - const candidates = [ - { name: 'print-designer' as const, query: previewQuery }, - { path: '/print/designer', query: previewQuery }, - ]; - let lastErr: unknown = null; - for (const loc of candidates) { - try { - await router.push(loc as any); - return; - } catch (e: any) { - lastErr = e; - if (isDuplicateNav(e)) { - await router.replace(loc as any); - return; - } - } + const templateJsonText = String(row?.templateJson ?? tpl?.templateJson ?? '').trim(); + let engine: string | undefined; + if (templateJsonText) { + try { + engine = JSON.parse(templateJsonText)?.engine; + } catch { + createMessage.error('模板 JSON 无法解析,无法判断是否为原生模板'); + return; } - throw lastErr; - }; - await navigateToDesignerPreview(); - createMessage.success('正在打开设计器预览…'); - quickPrintVisible.value = false; + } + if (engine !== 'native') { + createMessage.warning('快速打印仅支持「原生模板」(engine 为 native),请从列表预览 hiprint 模板或使用设计器'); + return; + } + nativeListPreviewTemplateId.value = id; + nativeListPreviewOpen.value = true; + quickNativePrintOpen.value = false; } catch (error: any) { - sessionStorage.removeItem(QUICK_PRINT_PREVIEW_STORAGE_KEY); - createMessage.error(`无法打开预览:${error?.message || '未知错误'}`); + createMessage.error(`加载模板失败:${error?.message || '未知错误'}`); } finally { - quickPreviewLoading.value = false; + quickNativePrintLoading.value = false; } } @@ -1318,191 +1282,6 @@ `; } - async function handleQuickPrint() { - const templateCode = String(quickPrintForm.value.templateCode || '').trim(); - if (!templateCode) { - createMessage.warning('请选择模板编号'); - return; - } - const dataText = String(quickPrintForm.value.dataJson || '').trim(); - if (!dataText) { - createMessage.warning('请先输入打印数据JSON'); - return; - } - let dataJson: any; - try { - dataJson = JSON.parse(dataText); - } catch (error: any) { - createMessage.error(`打印数据JSON格式错误:${error?.message || '未知错误'}`); - return; - } - quickPrintLoading.value = true; - try { - const printer = String(quickPrintForm.value.printerName || '').trim(); - if (quickPrintMode.value === 'serverText') { - await directPrint({ - templateCode, - printerName: printer, - 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') { - await executePdfServerPrint({ - templateCode, - printerName: printer, - dataJson, - fileName: `${templateCode}.pdf`, - }); - createMessage.success('已提交PDF到后端打印'); - } else { - 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; - } - const runtimeTemplate = new hiprint.PrintTemplate({ - template: templateJson, - }); - if (quickPrintMode.value === 'lodopTemplate') { - try { - await ensureClodopScriptLoaded(); - } catch (e: any) { - createMessage.error( - `无法连接 C-Lodop:${e?.message || '加载 CLodopfuncs.js 失败'}。请确认本机服务已启动;若站点为 HTTPS,需安装扩展版并在浏览器中访问一次 https://localhost.lodop.net:8443 信任证书。`, - ); - return; - } - const lodop = - (typeof (window as any).getLodop === 'function' ? (window as any).getLodop() : null) || - (window as any)?.LODOP || - (window as any)?.CLODOP; - if (!lodop) { - createMessage.error('未检测到 LODOP/C-Lodop,请先安装并启动后重试'); - return; - } - const html = optimizeMergedHeaderHtml(await resolveTemplateHtml(runtimeTemplate, dataJson), templateJson); - if (!html) { - const proto = Object.getPrototypeOf(runtimeTemplate); - const methodNames = Object.getOwnPropertyNames(proto || {}).filter((name) => name !== 'constructor'); - console.warn('[Lodop实验] 未拿到模板HTML,PrintTemplate methods:', methodNames); - createMessage.error('当前 hiprint 版本未导出可用 HTML,暂无法走 Lodop 实验模式(可先用“按模板样式打印”)'); - return; - } - const panel = Array.isArray(templateJson?.panels) && templateJson.panels.length ? templateJson.panels[0] : {}; - const widthMm = Number(panel?.width || 0); - const heightMm = Number(panel?.height || 0); - lodop.PRINT_INIT(`QH-MES-${templateCode}`); - if (widthMm > 0 && heightMm > 0 && typeof lodop.SET_PRINT_PAGESIZE === 'function') { - lodop.SET_PRINT_PAGESIZE(1, Math.round(widthMm * 10), Math.round(heightMm * 10), ''); - } - if (printer && printer !== '__system_default__' && typeof lodop.SET_PRINTER_INDEXA === 'function') { - lodop.SET_PRINTER_INDEXA(printer); - } - const docHtml = buildLodopDocumentHtml(html, widthMm > 0 ? widthMm : undefined, heightMm > 0 ? heightMm : undefined); - const printWidth = widthMm > 0 ? `${widthMm}mm` : 'RightMargin:0mm'; - lodop.ADD_PRINT_HTM('0mm', '0mm', printWidth, 'BottomMargin:0mm', docHtml); - if (typeof lodop.SET_PRINT_MODE === 'function') { - // 关闭强制满宽,尽量保持模板原始列宽比例 - lodop.SET_PRINT_MODE('FULL_WIDTH_FOR_OVERFLOW', false); - } - lodop.PRINT(); - createMessage.success('已通过 Lodop 提交打印任务(实验模式)'); - } else { - if (printer && printer !== '__system_default__') { - try { - runtimeTemplate.print(dataJson, { printer }); - } catch (_error) { - runtimeTemplate.print(dataJson, { printerName: printer }); - } - } else { - runtimeTemplate.print(dataJson); - } - createMessage.success('已按模板样式发起打印'); - } - } - } - quickPrintVisible.value = false; - } catch (error: any) { - createMessage.error(`打印失败:${error?.message || '未知错误'}`); - } finally { - quickPrintLoading.value = false; - } - } - function handleCreateNative() { openModal(true, { isUpdate: false, isNative: true }); } diff --git a/jeecgboot-vue3/src/views/print/template/native/components/ToolbarPalette.vue b/jeecgboot-vue3/src/views/print/template/native/components/ToolbarPalette.vue index dae47b8..9677c7b 100644 --- a/jeecgboot-vue3/src/views/print/template/native/components/ToolbarPalette.vue +++ b/jeecgboot-vue3/src/views/print/template/native/components/ToolbarPalette.vue @@ -14,16 +14,28 @@
- - {{ item.label }} - +
+ + + {{ item.label }} + +
- - {{ item.label }} - +
+ + + {{ item.label }} + +
@@ -48,6 +60,7 @@ @@ -148,9 +161,39 @@ padding: 6px 10px 12px 8px; } + /* 报表节 / 组件框:两列网格,图标 + 文案 */ + .palette-item-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 6px 8px; + align-items: stretch; + } + .palette-btn { - margin-bottom: 6px; + margin-bottom: 0; + width: 100%; + height: auto; + min-height: 32px; + padding: 6px 8px; + display: inline-flex; + align-items: center; + justify-content: flex-start; + gap: 6px; text-align: left; + line-height: 1.25; + white-space: normal; + } + + .palette-btn-icon { + flex-shrink: 0; + font-size: 16px; + color: rgba(0, 0, 0, 0.65); + } + + .palette-btn-label { + flex: 1; + min-width: 0; + word-break: break-all; } .palette-tab-help-label {