202 lines
5.9 KiB
Vue
202 lines
5.9 KiB
Vue
|
|
/**
|
|||
|
|
* 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 || '';
|
|||
|
|
}
|