优化打印模板界面,更新打印机选择逻辑,支持PrintDot桥接打印,改进快速打印功能,增强用户体验。新增图标显示于报表节和组件框按钮,调整布局以提升可用性。
This commit is contained in:
@@ -10,7 +10,7 @@
|
|||||||
allow-clear
|
allow-clear
|
||||||
show-search
|
show-search
|
||||||
option-filter-prop="label"
|
option-filter-prop="label"
|
||||||
placeholder="选择打印机(本地/网络)"
|
:placeholder="printerSelectPlaceholder"
|
||||||
/>
|
/>
|
||||||
<a-input
|
<a-input
|
||||||
v-model:value="manualPrinterName"
|
v-model:value="manualPrinterName"
|
||||||
@@ -58,50 +58,35 @@
|
|||||||
</BasicTable>
|
</BasicTable>
|
||||||
<PrintTemplateModal @register="registerModal" @success="handleSuccess" />
|
<PrintTemplateModal @register="registerModal" @success="handleSuccess" />
|
||||||
<NativeTemplateListPreviewModal v-model:open="nativeListPreviewOpen" :template-id="nativeListPreviewTemplateId" />
|
<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-space direction="vertical" style="width: 100%" size="middle">
|
||||||
<a-radio-group v-model:value="quickPrintMode">
|
<a-alert
|
||||||
<a-radio-button value="templateStyle">按模板样式打印(推荐)</a-radio-button>
|
type="info"
|
||||||
<a-radio-button value="lodopTemplate">Lodop实验(模板样式)</a-radio-button>
|
show-icon
|
||||||
<a-radio-button value="pdfServer">前端转PDF后端打印</a-radio-button>
|
message="请选择原生模板"
|
||||||
<a-radio-button value="printDotBridge">PrintDot 本地桥接(PDF)</a-radio-button>
|
description="将打开与列表「预览」相同的弹窗,在其中编辑模板 JSON、参数 JSON,并使用「浏览器打印」或 PrintDot「桥接器打印」。"
|
||||||
<a-radio-button value="serverText">服务端直打(纯文本)</a-radio-button>
|
/>
|
||||||
</a-radio-group>
|
<div>
|
||||||
<div v-if="quickPrintMode === 'printDotBridge'" style="font-size: 12px; color: rgba(0, 0, 0, 0.55)">
|
<div class="skill-field-label">模板编号</div>
|
||||||
需本机运行 PrintDot 客户端(默认 WebSocket ws://127.0.0.1:1122/ws)。勾选「PrintDot
|
|
||||||
桥接」并刷新打印机后,可选择桥接器上报的打印机;支持原生模板与 hiprint 模板(均在前端转 PDF 后送出)。
|
|
||||||
</div>
|
|
||||||
<a-space style="width: 100%">
|
|
||||||
<a-select
|
<a-select
|
||||||
v-model:value="quickPrintForm.templateCode"
|
v-model:value="quickNativeTemplateCode"
|
||||||
:options="templateCodeOptions"
|
:options="templateCodeOptions"
|
||||||
style="width: 280px"
|
style="width: 100%"
|
||||||
show-search
|
show-search
|
||||||
option-filter-prop="label"
|
option-filter-prop="label"
|
||||||
placeholder="选择模板编号"
|
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>
|
</div>
|
||||||
<a-button type="button" :loading="quickPreviewLoading" @click.prevent.stop="handleQuickPrintDesignerPreview">
|
<a-space style="width: 100%; justify-content: flex-end">
|
||||||
预览(与设计器一致)
|
<a-button @click="quickNativePrintOpen = false">取消</a-button>
|
||||||
</a-button>
|
<a-button type="primary" :loading="quickNativePrintLoading" @click="confirmQuickOpenNativePreview">打开预览</a-button>
|
||||||
|
</a-space>
|
||||||
</a-space>
|
</a-space>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
|
|
||||||
@@ -189,7 +174,6 @@
|
|||||||
deleteOne,
|
deleteOne,
|
||||||
batchDelete,
|
batchDelete,
|
||||||
queryPrinters,
|
queryPrinters,
|
||||||
directPrint,
|
|
||||||
directPrintPdf,
|
directPrintPdf,
|
||||||
queryByCode,
|
queryByCode,
|
||||||
queryById,
|
queryById,
|
||||||
@@ -198,20 +182,12 @@
|
|||||||
import { resolveProviders } from './hiprint/qhmesProvider';
|
import { resolveProviders } from './hiprint/qhmesProvider';
|
||||||
import PrintTemplateModal from './components/PrintTemplateModal.vue';
|
import PrintTemplateModal from './components/PrintTemplateModal.vue';
|
||||||
import NativeTemplateListPreviewModal from './components/NativeTemplateListPreviewModal.vue';
|
import NativeTemplateListPreviewModal from './components/NativeTemplateListPreviewModal.vue';
|
||||||
import { QUICK_PRINT_PREVIEW_STORAGE_KEY } from './quickPrintPreviewStorage';
|
import { buildPdfBase64FromHtmlFragment } from './utils/printHtmlToPdfBase64';
|
||||||
import {
|
|
||||||
buildPdfBase64FromHtmlFragment,
|
|
||||||
extractBodyInnerHtmlFromFullDocument,
|
|
||||||
} from './utils/printHtmlToPdfBase64';
|
|
||||||
import {
|
import {
|
||||||
fetchPrintDotPrinters,
|
fetchPrintDotPrinters,
|
||||||
getPrintDotBridgeConfig,
|
getPrintDotBridgeConfig,
|
||||||
printDotSendPdf,
|
|
||||||
resolvePrintDotPrinterName,
|
|
||||||
setPrintDotBridgeConfig,
|
setPrintDotBridgeConfig,
|
||||||
} from './utils/printDotBridge';
|
} from './utils/printDotBridge';
|
||||||
import { normalizeImportedNativeSchema } from './native/core/nativeSchemaNormalize';
|
|
||||||
import { renderNativePrintHtml } from './native/core/printRenderer';
|
|
||||||
import { PRINT_TEMPLATE_SELECTED_PRINTER_KEY } from './utils/printNativeViaPrintDot';
|
import { PRINT_TEMPLATE_SELECTED_PRINTER_KEY } from './utils/printNativeViaPrintDot';
|
||||||
|
|
||||||
defineOptions({ name: 'PrintTemplateList' });
|
defineOptions({ name: 'PrintTemplateList' });
|
||||||
@@ -226,18 +202,10 @@
|
|||||||
const selectedPrinterName = ref<string | undefined>();
|
const selectedPrinterName = ref<string | undefined>();
|
||||||
const printerOptions = ref<Array<{ label: string; value: string }>>([]);
|
const printerOptions = ref<Array<{ label: string; value: string }>>([]);
|
||||||
const manualPrinterName = ref('');
|
const manualPrinterName = ref('');
|
||||||
const quickPrintVisible = ref(false);
|
const quickNativePrintOpen = ref(false);
|
||||||
const quickPrintLoading = ref(false);
|
const quickNativeTemplateCode = ref<string>('');
|
||||||
const quickPreviewLoading = ref(false);
|
const quickNativePrintLoading = ref(false);
|
||||||
const templateCodeOptions = ref<Array<{ label: string; value: string }>>([]);
|
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 LS_PRINT_DOT_ENABLED = 'qhmes_print_dot_enabled';
|
||||||
const printDotEnabled = ref(localStorage.getItem(LS_PRINT_DOT_ENABLED) === '1');
|
const printDotEnabled = ref(localStorage.getItem(LS_PRINT_DOT_ENABLED) === '1');
|
||||||
const printDotCfg = getPrintDotBridgeConfig();
|
const printDotCfg = getPrintDotBridgeConfig();
|
||||||
@@ -246,8 +214,16 @@
|
|||||||
|
|
||||||
function persistPrintDotConfig() {
|
function persistPrintDotConfig() {
|
||||||
setPrintDotBridgeConfig(printDotWsUrl.value, printDotKey.value);
|
setPrintDotBridgeConfig(printDotWsUrl.value, printDotKey.value);
|
||||||
|
// 桥接开启时 WS/密钥变更后重新拉取桥接器打印机列表
|
||||||
|
if (printDotEnabled.value) {
|
||||||
|
void refreshPrinterOptions(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const printerSelectPlaceholder = computed(() =>
|
||||||
|
printDotEnabled.value ? '选择打印机(PrintDot 桥接)' : '选择打印机(本地/网络)',
|
||||||
|
);
|
||||||
|
|
||||||
function onPrintDotEnabledChange() {
|
function onPrintDotEnabledChange() {
|
||||||
localStorage.setItem(LS_PRINT_DOT_ENABLED, printDotEnabled.value ? '1' : '0');
|
localStorage.setItem(LS_PRINT_DOT_ENABLED, printDotEnabled.value ? '1' : '0');
|
||||||
void refreshPrinterOptions(false);
|
void refreshPrinterOptions(false);
|
||||||
@@ -338,6 +314,50 @@
|
|||||||
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
|
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
|
||||||
|
|
||||||
async function refreshPrinterOptions(showMessage = true) {
|
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 payload = (await queryPrinters()) as Record<string, any>;
|
||||||
const names = [
|
const names = [
|
||||||
...(Array.isArray(payload?.serverPrinters) ? payload.serverPrinters : []),
|
...(Array.isArray(payload?.serverPrinters) ? payload.serverPrinters : []),
|
||||||
@@ -346,9 +366,6 @@
|
|||||||
.map((item) => String(item || '').trim())
|
.map((item) => String(item || '').trim())
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.filter((item, index, arr) => arr.indexOf(item) === index);
|
.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) => {
|
names.forEach((item) => {
|
||||||
optionMap.set(item, { label: item, value: item });
|
optionMap.set(item, { label: item, value: item });
|
||||||
});
|
});
|
||||||
@@ -359,31 +376,10 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
printerOptions.value = Array.from(optionMap.values());
|
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 (showMessage) {
|
||||||
if (names.length) {
|
if (names.length) {
|
||||||
createMessage.success(`已从服务端识别到 ${names.length} 台打印机`);
|
createMessage.success(`已从服务端识别到 ${names.length} 台打印机`);
|
||||||
} else if (!printDotEnabled.value) {
|
} else {
|
||||||
const reason = String(payload?.capability?.localReason || '').trim();
|
const reason = String(payload?.capability?.localReason || '').trim();
|
||||||
createMessage.warning(`服务端未返回可用打印机。${reason || '请在后端配置网络打印机后重试。'}`);
|
createMessage.warning(`服务端未返回可用打印机。${reason || '请在后端配置网络打印机后重试。'}`);
|
||||||
}
|
}
|
||||||
@@ -416,82 +412,50 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function openQuickPrintModal() {
|
async function openQuickPrintModal() {
|
||||||
quickPrintForm.value.printerName = selectedPrinterName.value || '__system_default__';
|
quickNativeTemplateCode.value = '';
|
||||||
quickPrintMode.value = 'templateStyle';
|
|
||||||
if (!templateCodeOptions.value.length) {
|
if (!templateCodeOptions.value.length) {
|
||||||
await loadTemplateCodeOptions();
|
await loadTemplateCodeOptions();
|
||||||
}
|
}
|
||||||
quickPrintVisible.value = true;
|
quickNativePrintOpen.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** 快速打印:仅支持原生模板,打开与列表相同的预览弹窗(浏览器打印 / PrintDot) */
|
||||||
* 跳转打印设计器并自动调用与设计器「预览」相同的 previewTemplate 逻辑(见 PrintDesigner runQuickPrintPreviewFromSessionStorage)
|
async function confirmQuickOpenNativePreview() {
|
||||||
*/
|
const templateCode = String(quickNativeTemplateCode.value || '').trim();
|
||||||
async function handleQuickPrintDesignerPreview() {
|
|
||||||
const templateCode = String(quickPrintForm.value.templateCode || '').trim();
|
|
||||||
if (!templateCode) {
|
if (!templateCode) {
|
||||||
createMessage.warning('请先选择模板编号');
|
createMessage.warning('请先选择模板编号');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const dataText = String(quickPrintForm.value.dataJson || '').trim();
|
quickNativePrintLoading.value = true;
|
||||||
if (!dataText) {
|
|
||||||
createMessage.warning('请先输入打印数据 JSON');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
JSON.parse(dataText);
|
|
||||||
} catch (error: any) {
|
|
||||||
createMessage.error(`打印数据JSON格式错误:${error?.message || '未知错误'}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
quickPreviewLoading.value = true;
|
|
||||||
try {
|
try {
|
||||||
const tpl = (await queryByCode(templateCode)) as Record<string, any>;
|
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) {
|
if (!id) {
|
||||||
createMessage.error(`未找到模板记录主键,无法打开设计器预览。返回字段:${Object.keys(tpl || {}).join(', ') || '空'}`);
|
createMessage.error('未找到该模板记录,请确认编号是否正确');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
sessionStorage.setItem(
|
const templateJsonText = String(row?.templateJson ?? tpl?.templateJson ?? '').trim();
|
||||||
QUICK_PRINT_PREVIEW_STORAGE_KEY,
|
let engine: string | undefined;
|
||||||
JSON.stringify({
|
if (templateJsonText) {
|
||||||
dataJsonText: dataText,
|
try {
|
||||||
}),
|
engine = JSON.parse(templateJsonText)?.engine;
|
||||||
);
|
} catch {
|
||||||
const previewQuery = {
|
createMessage.error('模板 JSON 无法解析,无法判断是否为原生模板');
|
||||||
id,
|
return;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
throw lastErr;
|
}
|
||||||
};
|
if (engine !== 'native') {
|
||||||
await navigateToDesignerPreview();
|
createMessage.warning('快速打印仅支持「原生模板」(engine 为 native),请从列表预览 hiprint 模板或使用设计器');
|
||||||
createMessage.success('正在打开设计器预览…');
|
return;
|
||||||
quickPrintVisible.value = false;
|
}
|
||||||
|
nativeListPreviewTemplateId.value = id;
|
||||||
|
nativeListPreviewOpen.value = true;
|
||||||
|
quickNativePrintOpen.value = false;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
sessionStorage.removeItem(QUICK_PRINT_PREVIEW_STORAGE_KEY);
|
createMessage.error(`加载模板失败:${error?.message || '未知错误'}`);
|
||||||
createMessage.error(`无法打开预览:${error?.message || '未知错误'}`);
|
|
||||||
} finally {
|
} finally {
|
||||||
quickPreviewLoading.value = false;
|
quickNativePrintLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1318,191 +1282,6 @@
|
|||||||
</html>`;
|
</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() {
|
function handleCreateNative() {
|
||||||
openModal(true, { isUpdate: false, isNative: true });
|
openModal(true, { isUpdate: false, isNative: true });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,16 +14,28 @@
|
|||||||
<a-tabs v-model:activeKey="activeTab" size="small" class="palette-tabs">
|
<a-tabs v-model:activeKey="activeTab" size="small" class="palette-tabs">
|
||||||
<a-tab-pane key="bands" tab="报表节">
|
<a-tab-pane key="bands" tab="报表节">
|
||||||
<div class="tab-scroll">
|
<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)">
|
<div class="palette-item-grid">
|
||||||
{{ item.label }}
|
<a-button v-for="item in bandItems" :key="item.type" size="small" class="palette-btn" @click="emit('add', item.type)">
|
||||||
</a-button>
|
<Icon :icon="item.icon" class="palette-btn-icon" />
|
||||||
|
<span class="palette-btn-label">{{ item.label }}</span>
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
<a-tab-pane key="components" tab="组件框">
|
<a-tab-pane key="components" tab="组件框">
|
||||||
<div class="tab-scroll">
|
<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)">
|
<div class="palette-item-grid">
|
||||||
{{ item.label }}
|
<a-button
|
||||||
</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>
|
</div>
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
<a-tab-pane key="params">
|
<a-tab-pane key="params">
|
||||||
@@ -48,6 +60,7 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
import { Icon } from '/@/components/Icon';
|
||||||
import BindingDetailFieldsEditor from './BindingDetailFieldsEditor.vue';
|
import BindingDetailFieldsEditor from './BindingDetailFieldsEditor.vue';
|
||||||
import BindingParamsEditor from './BindingParamsEditor.vue';
|
import BindingParamsEditor from './BindingParamsEditor.vue';
|
||||||
import type {
|
import type {
|
||||||
@@ -89,22 +102,22 @@
|
|||||||
function onUpdateDetailTables(detailTables: NativeDataBindingDetailTable[]) {
|
function onUpdateDetailTables(detailTables: NativeDataBindingDetailTable[]) {
|
||||||
emit('update-data-binding', { detailTables });
|
emit('update-data-binding', { detailTables });
|
||||||
}
|
}
|
||||||
const bandItems: Array<{ type: NativeElementType; label: string }> = [
|
const bandItems: Array<{ type: NativeElementType; label: string; icon: string }> = [
|
||||||
{ type: 'reportHeader', label: '报表头' },
|
{ type: 'reportHeader', label: '报表头', icon: 'ant-design:vertical-align-top-outlined' },
|
||||||
{ type: 'reportFooter', label: '报表尾' },
|
{ type: 'reportFooter', label: '报表尾', icon: 'ant-design:vertical-align-bottom-outlined' },
|
||||||
];
|
];
|
||||||
const componentItems: Array<{ type: NativeElementType; label: string }> = [
|
const componentItems: Array<{ type: NativeElementType; label: string; icon: string }> = [
|
||||||
{ type: 'title', label: '标题' },
|
{ type: 'title', label: '标题', icon: 'ant-design:font-size-outlined' },
|
||||||
{ type: 'subtitle', label: '副标题' },
|
{ type: 'subtitle', label: '副标题', icon: 'ant-design:align-left-outlined' },
|
||||||
{ type: 'text', label: '文本' },
|
{ type: 'text', label: '文本', icon: 'ant-design:file-text-outlined' },
|
||||||
{ type: 'date', label: '日期' },
|
{ type: 'date', label: '日期', icon: 'ant-design:calendar-outlined' },
|
||||||
{ type: 'pageNo', label: '页码' },
|
{ type: 'pageNo', label: '页码', icon: 'ant-design:book-outlined' },
|
||||||
{ type: 'image', label: '图片' },
|
{ type: 'image', label: '图片', icon: 'ant-design:picture-outlined' },
|
||||||
{ type: 'table', label: '普通表格' },
|
{ type: 'table', label: '普通表格', icon: 'ant-design:table-outlined' },
|
||||||
{ type: 'detailTable', label: '明细表格' },
|
{ type: 'detailTable', label: '明细表格', icon: 'ant-design:insert-row-below-outlined' },
|
||||||
{ type: 'freeTable', label: '自由表格' },
|
{ type: 'freeTable', label: '自由表格', icon: 'ant-design:border-outlined' },
|
||||||
{ type: 'qrcode', label: '二维码' },
|
{ type: 'qrcode', label: '二维码', icon: 'mdi:qrcode' },
|
||||||
{ type: 'barcode', label: '条形码' },
|
{ type: 'barcode', label: '条形码', icon: 'mdi:barcode' },
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -148,9 +161,39 @@
|
|||||||
padding: 6px 10px 12px 8px;
|
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 {
|
.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;
|
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 {
|
.palette-tab-help-label {
|
||||||
|
|||||||
Reference in New Issue
Block a user