新增PrintDot桥接功能,支持本地打印机连接和配置,优化打印模板设计,允许多页表格重复显示,改进打印预览和设计器界面,确保用户体验流畅。

This commit is contained in:
geht
2026-04-17 19:00:30 +08:00
parent 2bd4c5584d
commit efb6a9f838
14 changed files with 1196 additions and 110 deletions

View File

@@ -0,0 +1,201 @@
/**
* PrintDot 本地桥接器Wails 客户端WebSocket 协议封装。
* 默认地址 ws://127.0.0.1:1122/ws ,与 PrintDot-Client 中 Bridge 默认端口一致。
*/
const LS_WS_URL = 'qhmes_print_dot_ws_url';
const LS_KEY = 'qhmes_print_dot_key';
export type PrintDotPrinter = { name: string; isDefault?: boolean };
export function getPrintDotBridgeConfig() {
return {
wsUrl: localStorage.getItem(LS_WS_URL) || 'ws://127.0.0.1:1122/ws',
key: localStorage.getItem(LS_KEY) || '',
};
}
export function setPrintDotBridgeConfig(wsUrl: string, key: string) {
localStorage.setItem(LS_WS_URL, (wsUrl || '').trim());
localStorage.setItem(LS_KEY, key ?? '');
}
function buildWsUrl(wsUrl: string, key: string) {
const base = (wsUrl || '').trim();
const k = (key || '').trim();
if (!k) {
return base;
}
const sep = base.includes('?') ? '&' : '?';
return `${base}${sep}key=${encodeURIComponent(k)}`;
}
/** 连接后服务端会先推送 printer_list本方法读取该列表后关闭连接 */
export function fetchPrintDotPrinters(timeoutMs = 8000): Promise<PrintDotPrinter[]> {
const { wsUrl, key } = getPrintDotBridgeConfig();
return new Promise((resolve, reject) => {
let done = false;
const ws = new WebSocket(buildWsUrl(wsUrl, key));
const timer = window.setTimeout(() => {
if (done) {
return;
}
done = true;
try {
ws.close();
} catch {
// ignore
}
reject(new Error('连接 PrintDot 超时,请确认客户端已启动且 WebSocket 地址正确'));
}, timeoutMs);
const finish = (fn: () => void) => {
if (done) {
return;
}
done = true;
window.clearTimeout(timer);
try {
ws.close();
} catch {
// ignore
}
fn();
};
ws.onmessage = (ev) => {
try {
const data = JSON.parse(ev.data as string);
if (data?.type === 'printer_list' && Array.isArray(data.data)) {
const list = data.data
.map((p: any) => ({
name: String(p?.name || '').trim(),
isDefault: p?.isDefault === true,
}))
.filter((p: PrintDotPrinter) => !!p.name);
finish(() => resolve(list));
}
} catch {
// ignore
}
};
ws.onerror = () => {
finish(() => reject(new Error('无法连接 PrintDot WebSocket请检查地址与客户端是否运行')));
};
});
}
/** 将 PrintDot 返回的英文错误转为带处理步骤的提示(便于运维排查) */
function enhancePrintDotErrorMessage(raw: string): string {
const m = String(raw || '').trim();
if (/SumatraPDF\.exe not found/i.test(m) || /SUMATRAPDF_PATH/i.test(m)) {
return `${m}。本地处理PrintDot 依赖 SumatraPDF 静默打印 PDF。请安装 Sumatra PDF 后任选其一:将 SumatraPDF.exe 放在 PrintDot 客户端 exe 同目录;或将 Sumatra 安装目录加入系统 PATH或设置用户/系统环境变量 SUMATRAPDF_PATH 指向 SumatraPDF.exe 的完整路径,然后重启 PrintDot。`;
}
return m;
}
/**
* 发送 PDF 打印任务content 为 Base64 PDF可为纯 Base64 或 data:application/pdf;base64, 前缀)
* 连接建立后服务端可能先推送 printer_list需忽略直至收到 status。
*/
export function printDotSendPdf(params: {
printer: string;
pdfBase64: string;
jobName?: string;
copies?: number;
timeoutMs?: number;
}): Promise<{ ok: boolean; message: string }> {
const { wsUrl, key } = getPrintDotBridgeConfig();
const timeout = params.timeoutMs ?? 180000;
return new Promise((resolve, reject) => {
let settled = false;
const ws = new WebSocket(buildWsUrl(wsUrl, key));
const timer = window.setTimeout(() => {
if (settled) {
return;
}
settled = true;
try {
ws.close();
} catch {
// ignore
}
reject(new Error('PrintDot 打印等待结果超时'));
}, timeout);
const finishOk = (ok: boolean, message: string) => {
if (settled) {
return;
}
settled = true;
window.clearTimeout(timer);
try {
ws.close();
} catch {
// ignore
}
resolve({ ok, message });
};
ws.onopen = () => {
let content = String(params.pdfBase64 || '').trim();
if (content.startsWith('data:')) {
const idx = content.indexOf(',');
if (idx !== -1) {
content = content.slice(idx + 1);
}
}
const payload: Record<string, unknown> = {
printer: String(params.printer || '').trim(),
content,
job: {
name: String(params.jobName || 'QH-MES').trim() || 'QH-MES',
copies: Math.max(1, Number(params.copies) || 1),
},
};
ws.send(JSON.stringify(payload));
};
ws.onmessage = (ev) => {
try {
const data = JSON.parse(ev.data as string);
if (data?.type === 'printer_list') {
return;
}
if (data?.status) {
const ok = data.status === 'success';
const rawMsg = String(data.message || '');
finishOk(ok, ok ? rawMsg : enhancePrintDotErrorMessage(rawMsg));
}
} catch {
finishOk(false, 'PrintDot 返回无法解析');
}
};
ws.onerror = () => {
if (settled) {
return;
}
settled = true;
window.clearTimeout(timer);
try {
ws.close();
} catch {
// ignore
}
reject(new Error('PrintDot WebSocket 错误'));
};
});
}
/** 将列表页的「系统默认」或空选择解析为桥接器返回的默认打印机名称 */
export function resolvePrintDotPrinterName(selectedValue: string, printers: PrintDotPrinter[]): string {
const s = String(selectedValue || '').trim();
if (s && s !== '__system_default__') {
return s;
}
const def = printers.find((p) => p.isDefault);
return def?.name || printers[0]?.name || '';
}