优化打印模板界面,更新打印机选择逻辑,支持PrintDot桥接打印,改进快速打印功能,增强用户体验。新增图标显示于报表节和组件框按钮,调整布局以提升可用性。
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
allow-clear
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
placeholder="选择打印机(本地/网络)"
|
||||
:placeholder="printerSelectPlaceholder"
|
||||
/>
|
||||
<a-input
|
||||
v-model:value="manualPrinterName"
|
||||
@@ -58,50 +58,35 @@
|
||||
</BasicTable>
|
||||
<PrintTemplateModal @register="registerModal" @success="handleSuccess" />
|
||||
<NativeTemplateListPreviewModal v-model:open="nativeListPreviewOpen" :template-id="nativeListPreviewTemplateId" />
|
||||
<a-modal v-model:open="quickPrintVisible" title="快速打印" width="820px" :confirm-loading="quickPrintLoading" @ok="handleQuickPrint">
|
||||
<a-modal
|
||||
v-model:open="quickNativePrintOpen"
|
||||
title="快速打印"
|
||||
width="480px"
|
||||
:footer="null"
|
||||
destroy-on-close
|
||||
>
|
||||
<a-space direction="vertical" style="width: 100%" size="middle">
|
||||
<a-radio-group v-model:value="quickPrintMode">
|
||||
<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-alert
|
||||
type="info"
|
||||
show-icon
|
||||
message="请选择原生模板"
|
||||
description="将打开与列表「预览」相同的弹窗,在其中编辑模板 JSON、参数 JSON,并使用「浏览器打印」或 PrintDot「桥接器打印」。"
|
||||
/>
|
||||
<div>
|
||||
<div class="skill-field-label">模板编号</div>
|
||||
<a-select
|
||||
v-model:value="quickPrintForm.templateCode"
|
||||
v-model:value="quickNativeTemplateCode"
|
||||
:options="templateCodeOptions"
|
||||
style="width: 280px"
|
||||
style="width: 100%"
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
placeholder="选择模板编号"
|
||||
/>
|
||||
<a-select
|
||||
v-model:value="quickPrintForm.printerName"
|
||||
:options="printerOptions"
|
||||
style="width: 320px"
|
||||
show-search
|
||||
allow-clear
|
||||
option-filter-prop="label"
|
||||
placeholder="选择打印机(可为空使用系统默认)"
|
||||
/>
|
||||
</a-space>
|
||||
<a-textarea
|
||||
v-model:value="quickPrintForm.dataJson"
|
||||
:rows="12"
|
||||
placeholder='传入打印数据JSON,例如:{"docNo":"MO-001","mainTable":[{"materialCode":"M01","qty":10}]}'
|
||||
/>
|
||||
<a-divider plain orientation="left" style="margin: 4px 0">预览</a-divider>
|
||||
<div style="font-size: 12px; color: rgba(0, 0, 0, 0.55); line-height: 1.6">
|
||||
打开「打印设计器」并自动执行与工具栏「预览」相同的逻辑(同一套 hiprint 预览、样式注入与表格合并后处理),确保与在设计器内预览一致。
|
||||
</div>
|
||||
<a-button type="button" :loading="quickPreviewLoading" @click.prevent.stop="handleQuickPrintDesignerPreview">
|
||||
预览(与设计器一致)
|
||||
</a-button>
|
||||
<a-space style="width: 100%; justify-content: flex-end">
|
||||
<a-button @click="quickNativePrintOpen = false">取消</a-button>
|
||||
<a-button type="primary" :loading="quickNativePrintLoading" @click="confirmQuickOpenNativePreview">打开预览</a-button>
|
||||
</a-space>
|
||||
</a-space>
|
||||
</a-modal>
|
||||
|
||||
@@ -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<string | undefined>();
|
||||
const printerOptions = ref<Array<{ label: string; value: string }>>([]);
|
||||
const manualPrinterName = ref('');
|
||||
const quickPrintVisible = ref(false);
|
||||
const quickPrintLoading = ref(false);
|
||||
const quickPreviewLoading = ref(false);
|
||||
const quickNativePrintOpen = ref(false);
|
||||
const quickNativeTemplateCode = ref<string>('');
|
||||
const quickNativePrintLoading = ref(false);
|
||||
const templateCodeOptions = ref<Array<{ label: string; value: string }>>([]);
|
||||
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<string, { label: string; value: string }>();
|
||||
// 保留「系统默认」:本地模式交给浏览器/队列;桥接模式由 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<string, any>;
|
||||
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<string, { label: string; value: string }>();
|
||||
// 永远保留“系统默认打印机”兜底项,避免插件不可用时无法选择
|
||||
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<string, any>;
|
||||
const id = String(tpl?.id ?? (tpl as any)?.result?.id ?? '').trim();
|
||||
const row = (tpl?.result ?? tpl) as Record<string, any>;
|
||||
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<string, string>;
|
||||
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 @@
|
||||
</html>`;
|
||||
}
|
||||
|
||||
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<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') {
|
||||
await executePdfServerPrint({
|
||||
templateCode,
|
||||
printerName: printer,
|
||||
dataJson,
|
||||
fileName: `${templateCode}.pdf`,
|
||||
});
|
||||
createMessage.success('已提交PDF到后端打印');
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
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 });
|
||||
}
|
||||
|
||||
@@ -14,16 +14,28 @@
|
||||
<a-tabs v-model:activeKey="activeTab" size="small" class="palette-tabs">
|
||||
<a-tab-pane key="bands" tab="报表节">
|
||||
<div class="tab-scroll">
|
||||
<a-button v-for="item in bandItems" :key="item.type" block size="small" class="palette-btn" @click="emit('add', item.type)">
|
||||
{{ item.label }}
|
||||
</a-button>
|
||||
<div class="palette-item-grid">
|
||||
<a-button v-for="item in bandItems" :key="item.type" size="small" class="palette-btn" @click="emit('add', item.type)">
|
||||
<Icon :icon="item.icon" class="palette-btn-icon" />
|
||||
<span class="palette-btn-label">{{ item.label }}</span>
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="components" tab="组件框">
|
||||
<div class="tab-scroll">
|
||||
<a-button v-for="item in componentItems" :key="item.type" block size="small" class="palette-btn" @click="emit('add', item.type)">
|
||||
{{ item.label }}
|
||||
</a-button>
|
||||
<div class="palette-item-grid">
|
||||
<a-button
|
||||
v-for="item in componentItems"
|
||||
:key="item.type"
|
||||
size="small"
|
||||
class="palette-btn"
|
||||
@click="emit('add', item.type)"
|
||||
>
|
||||
<Icon :icon="item.icon" class="palette-btn-icon" />
|
||||
<span class="palette-btn-label">{{ item.label }}</span>
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="params">
|
||||
@@ -48,6 +60,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { Icon } from '/@/components/Icon';
|
||||
import BindingDetailFieldsEditor from './BindingDetailFieldsEditor.vue';
|
||||
import BindingParamsEditor from './BindingParamsEditor.vue';
|
||||
import type {
|
||||
@@ -89,22 +102,22 @@
|
||||
function onUpdateDetailTables(detailTables: NativeDataBindingDetailTable[]) {
|
||||
emit('update-data-binding', { detailTables });
|
||||
}
|
||||
const bandItems: Array<{ type: NativeElementType; label: string }> = [
|
||||
{ type: 'reportHeader', label: '报表头' },
|
||||
{ type: 'reportFooter', label: '报表尾' },
|
||||
const bandItems: Array<{ type: NativeElementType; label: string; icon: string }> = [
|
||||
{ type: 'reportHeader', label: '报表头', icon: 'ant-design:vertical-align-top-outlined' },
|
||||
{ type: 'reportFooter', label: '报表尾', icon: 'ant-design:vertical-align-bottom-outlined' },
|
||||
];
|
||||
const componentItems: Array<{ type: NativeElementType; label: string }> = [
|
||||
{ type: 'title', label: '标题' },
|
||||
{ type: 'subtitle', label: '副标题' },
|
||||
{ type: 'text', label: '文本' },
|
||||
{ type: 'date', label: '日期' },
|
||||
{ type: 'pageNo', label: '页码' },
|
||||
{ type: 'image', label: '图片' },
|
||||
{ type: 'table', label: '普通表格' },
|
||||
{ type: 'detailTable', label: '明细表格' },
|
||||
{ type: 'freeTable', label: '自由表格' },
|
||||
{ type: 'qrcode', label: '二维码' },
|
||||
{ type: 'barcode', label: '条形码' },
|
||||
const componentItems: Array<{ type: NativeElementType; label: string; icon: string }> = [
|
||||
{ type: 'title', label: '标题', icon: 'ant-design:font-size-outlined' },
|
||||
{ type: 'subtitle', label: '副标题', icon: 'ant-design:align-left-outlined' },
|
||||
{ type: 'text', label: '文本', icon: 'ant-design:file-text-outlined' },
|
||||
{ type: 'date', label: '日期', icon: 'ant-design:calendar-outlined' },
|
||||
{ type: 'pageNo', label: '页码', icon: 'ant-design:book-outlined' },
|
||||
{ type: 'image', label: '图片', icon: 'ant-design:picture-outlined' },
|
||||
{ type: 'table', label: '普通表格', icon: 'ant-design:table-outlined' },
|
||||
{ type: 'detailTable', label: '明细表格', icon: 'ant-design:insert-row-below-outlined' },
|
||||
{ type: 'freeTable', label: '自由表格', icon: 'ant-design:border-outlined' },
|
||||
{ type: 'qrcode', label: '二维码', icon: 'mdi:qrcode' },
|
||||
{ type: 'barcode', label: '条形码', icon: 'mdi:barcode' },
|
||||
];
|
||||
</script>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user