@worm-vue3-print/core 1.3.0 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser/index.cjs +300 -63
- package/dist/browser/index.d.cts +2 -2
- package/dist/browser/index.d.ts +2 -2
- package/dist/browser/index.js +1 -1
- package/dist/{chunk-V4A5623C.js → chunk-B2XJSF4V.js} +1 -1
- package/dist/{chunk-VFEURLAZ.js → chunk-BBSFBAWT.js} +308 -63
- package/dist/client/index.cjs +381 -0
- package/dist/client/index.d.cts +241 -0
- package/dist/client/index.d.ts +241 -0
- package/dist/client/index.js +346 -0
- package/dist/designer/index.cjs +196 -11
- package/dist/designer/index.d.cts +21 -6
- package/dist/designer/index.d.ts +21 -6
- package/dist/designer/index.js +35 -4
- package/dist/{driver-CsWkGK4a.d.cts → driver-Dn_YAzO5.d.cts} +4 -1
- package/dist/{driver-CsWkGK4a.d.ts → driver-Dn_YAzO5.d.ts} +4 -1
- package/dist/index.cjs +316 -63
- package/dist/index.d.cts +29 -7
- package/dist/index.d.ts +29 -7
- package/dist/index.js +18 -2
- package/dist/node/index.d.cts +1 -1
- package/dist/node/index.d.ts +1 -1
- package/dist/{ports-B63x7kCX.d.ts → ports-BvwlA_km.d.ts} +1 -1
- package/dist/{ports-q934kIC7.d.cts → ports-Cf5sjwls.d.cts} +1 -1
- package/package.json +6 -1
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
declare const APP_ID = "worm-print-client";
|
|
2
|
+
declare const DEFAULT_PORT = 17521;
|
|
3
|
+
declare const PORT_SCAN_LIMIT = 20;
|
|
4
|
+
/** 服务端可能返回的错误码 */
|
|
5
|
+
type ServerErrorCode = 'INVALID_REQUEST' | 'UNAUTHORIZED' | 'PRINTER_NOT_FOUND' | 'PRINTER_OFFLINE' | 'BUSY' | 'RENDER_TIMEOUT' | 'PRINT_FAILED' | 'INTERNAL';
|
|
6
|
+
/** SDK 本地产生的错误码(服务端不会发送) */
|
|
7
|
+
type ClientErrorCode = 'CLIENT_TIMEOUT' | 'CLIENT_NOT_RUNNING';
|
|
8
|
+
type ProtocolErrorCode = ServerErrorCode | ClientErrorCode;
|
|
9
|
+
interface ClientRequest<T = unknown> {
|
|
10
|
+
id: string;
|
|
11
|
+
type: string;
|
|
12
|
+
payload: T;
|
|
13
|
+
}
|
|
14
|
+
interface ServerOk<T = unknown> {
|
|
15
|
+
id: string;
|
|
16
|
+
ok: true;
|
|
17
|
+
payload: T;
|
|
18
|
+
}
|
|
19
|
+
interface ServerError {
|
|
20
|
+
id: string;
|
|
21
|
+
ok: false;
|
|
22
|
+
error: {
|
|
23
|
+
code: ServerErrorCode;
|
|
24
|
+
message: string;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
type ServerResponse<T = unknown> = ServerOk<T> | ServerError;
|
|
28
|
+
/** 无法解析的服务端消息 */
|
|
29
|
+
interface MalformedMessage {
|
|
30
|
+
malformed: true;
|
|
31
|
+
}
|
|
32
|
+
interface PrinterInfo {
|
|
33
|
+
name: string;
|
|
34
|
+
isDefault: boolean;
|
|
35
|
+
/** 系统返回的打印机状态描述(如 idle、offline),无信息时为空串 */
|
|
36
|
+
status: string;
|
|
37
|
+
}
|
|
38
|
+
interface HelloRequestPayload {
|
|
39
|
+
}
|
|
40
|
+
interface HelloResponsePayload {
|
|
41
|
+
app: typeof APP_ID;
|
|
42
|
+
version: string;
|
|
43
|
+
port: number;
|
|
44
|
+
}
|
|
45
|
+
interface PrintersListResponsePayload {
|
|
46
|
+
printers: PrinterInfo[];
|
|
47
|
+
}
|
|
48
|
+
/** 打印参数;长度单位均为微米(1mm = 1000μm) */
|
|
49
|
+
interface PrintOptions {
|
|
50
|
+
/** 缺省走系统默认打印机 */
|
|
51
|
+
printerName?: string;
|
|
52
|
+
copies?: number;
|
|
53
|
+
/** 打印机驱动内已配置纸型名,针式打印机优先使用 */
|
|
54
|
+
paperName?: string;
|
|
55
|
+
/**
|
|
56
|
+
* 纸张覆盖项(微米)。
|
|
57
|
+
* - 普通模板:缺省(或仅给 width)时以模板自带纸张(getPaperDimensions)为准;
|
|
58
|
+
* - 连续纸模板(templateJson.paperSize === 'CONTINUOUS'):高度缺省或 <=0 时按渲染探针
|
|
59
|
+
* 测得的内容高度+连续纸底边距推导实际纸高;显式 height>0 为配置覆盖(逃生门)。
|
|
60
|
+
* 宽度缺省取模板 customWidth(默认 80mm)。
|
|
61
|
+
*/
|
|
62
|
+
paperSize?: {
|
|
63
|
+
width?: number;
|
|
64
|
+
height?: number;
|
|
65
|
+
};
|
|
66
|
+
landscape?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* 页边距覆盖(微米);缺省使用模板 margins。
|
|
69
|
+
* 连续纸的末尾留白由模板连续纸底边距(默认 0)控制,已计入推导纸高。
|
|
70
|
+
*/
|
|
71
|
+
margins?: {
|
|
72
|
+
top: number;
|
|
73
|
+
bottom: number;
|
|
74
|
+
left: number;
|
|
75
|
+
right: number;
|
|
76
|
+
};
|
|
77
|
+
color?: boolean;
|
|
78
|
+
pageRanges?: Array<{
|
|
79
|
+
from: number;
|
|
80
|
+
to: number;
|
|
81
|
+
}>;
|
|
82
|
+
}
|
|
83
|
+
/** print.submit 请求 payload;templateJson 结构由 core 定义,这里保持松散耦合 */
|
|
84
|
+
interface PrintSubmitRequest {
|
|
85
|
+
templateJson: Record<string, unknown>;
|
|
86
|
+
/** 业务数据:对象=单份;非空对象数组=批量(最多 500 份,合并为一个作业) */
|
|
87
|
+
printData?: Record<string, unknown> | Array<Record<string, unknown>>;
|
|
88
|
+
/** 相对路径图片资源解析基址 */
|
|
89
|
+
baseUrl?: string;
|
|
90
|
+
/** 相对路径字体资源解析基址;缺省回落 baseUrl(客户端必须能访问解析后的地址) */
|
|
91
|
+
fontBaseUrl?: string;
|
|
92
|
+
/** 模板名称(业务侧元数据,用于客户端任务记录展示,不参与渲染) */
|
|
93
|
+
templateName?: string;
|
|
94
|
+
print: PrintOptions;
|
|
95
|
+
}
|
|
96
|
+
interface PrintSubmitResponsePayload {
|
|
97
|
+
jobId: string;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* 浏览器侧预渲染结果(@worm-vue3-print/core/browser 的 renderHtmlPages 输出子集)。
|
|
101
|
+
* 业务页在浏览器内完成两遍渲染后,把最终 HTML 直接提交给客户端静默打印,
|
|
102
|
+
* 客户端不再持有/执行模板渲染内核。
|
|
103
|
+
*/
|
|
104
|
+
interface RenderedHtmlPages {
|
|
105
|
+
/** 最终多页 HTML(连续纸纸高已写入 @page/.print-page) */
|
|
106
|
+
html: string;
|
|
107
|
+
/** 最终纸张尺寸(mm;连续纸为探针推导高度) */
|
|
108
|
+
paperMm: {
|
|
109
|
+
width: number;
|
|
110
|
+
height: number;
|
|
111
|
+
};
|
|
112
|
+
/** 模板是否连续纸 */
|
|
113
|
+
continuous?: boolean;
|
|
114
|
+
/** 总页数(客户端任务记录用,可选) */
|
|
115
|
+
pageCount?: number;
|
|
116
|
+
}
|
|
117
|
+
/** print.submitHtml 请求 payload:浏览器预渲染 HTML 直提交通道 */
|
|
118
|
+
interface PrintSubmitHtmlRequest {
|
|
119
|
+
/** 最终多页 HTML(必须是 core 同构管线产出的完整文档) */
|
|
120
|
+
html: string;
|
|
121
|
+
/** 最终纸张尺寸(mm) */
|
|
122
|
+
paperMm: {
|
|
123
|
+
width: number;
|
|
124
|
+
height: number;
|
|
125
|
+
};
|
|
126
|
+
/** 模板是否连续纸(影响纸张覆盖语义,缺省 false) */
|
|
127
|
+
continuous?: boolean;
|
|
128
|
+
/** 总页数(可选,任务记录展示) */
|
|
129
|
+
pageCount?: number;
|
|
130
|
+
/** 模板名称(业务侧元数据,用于客户端任务记录展示,不参与渲染) */
|
|
131
|
+
templateName?: string;
|
|
132
|
+
print: PrintOptions;
|
|
133
|
+
}
|
|
134
|
+
declare const MESSAGE_TYPES: {
|
|
135
|
+
readonly HELLO: "hello";
|
|
136
|
+
readonly PRINTERS_LIST: "printers.list";
|
|
137
|
+
readonly PRINT_SUBMIT: "print.submit";
|
|
138
|
+
readonly PRINT_SUBMIT_HTML: "print.submitHtml";
|
|
139
|
+
};
|
|
140
|
+
/** 编码请求帧为 JSON 字符串 */
|
|
141
|
+
declare function encodeRequest<T>(type: string, payload: T): string;
|
|
142
|
+
/** 解码服务端消息;任何非法输入统一返回 { malformed: true },不抛异常 */
|
|
143
|
+
declare function decodeServerMessage(raw: string): ServerResponse | MalformedMessage;
|
|
144
|
+
|
|
145
|
+
/** 协议/SDK 错误:携带可程序化处理的错误码 */
|
|
146
|
+
declare class WormPrintError extends Error {
|
|
147
|
+
readonly code: ProtocolErrorCode;
|
|
148
|
+
constructor(code: ProtocolErrorCode, message: string);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
type TransportStatus = 'disconnected' | 'connecting' | 'connected';
|
|
152
|
+
interface TransportOptions {
|
|
153
|
+
/** 安全开关开启后的配对 token */
|
|
154
|
+
token?: string;
|
|
155
|
+
/** 请求默认超时(毫秒),默认 15000 */
|
|
156
|
+
timeoutMs?: number;
|
|
157
|
+
/** 覆盖探测端口列表(测试用);默认 DEFAULT_PORT 起连续 PORT_SCAN_LIMIT 个 */
|
|
158
|
+
ports?: number[];
|
|
159
|
+
/** 注入 WebSocket 构造器(测试用) */
|
|
160
|
+
WebSocketCtor?: typeof WebSocket;
|
|
161
|
+
}
|
|
162
|
+
declare class WsTransport {
|
|
163
|
+
private ws;
|
|
164
|
+
private status_;
|
|
165
|
+
private hello;
|
|
166
|
+
private pendings;
|
|
167
|
+
private statusCbs;
|
|
168
|
+
private manualClose;
|
|
169
|
+
private reconnectTimer;
|
|
170
|
+
private reconnectAttempts;
|
|
171
|
+
private readonly ctor;
|
|
172
|
+
private readonly defaultTimeout;
|
|
173
|
+
private opts;
|
|
174
|
+
constructor(opts?: TransportOptions);
|
|
175
|
+
get status(): TransportStatus;
|
|
176
|
+
onStatus(cb: (s: TransportStatus) => void): () => void;
|
|
177
|
+
setToken(token: string | undefined): void;
|
|
178
|
+
/** 依次探测端口并完成 hello 握手;全部失败抛 CLIENT_NOT_RUNNING */
|
|
179
|
+
connect(signal?: AbortSignal): Promise<HelloResponsePayload>;
|
|
180
|
+
/** 发送请求并等待同 id 响应 */
|
|
181
|
+
request<T>(type: string, payload: unknown, timeoutMs?: number): Promise<T>;
|
|
182
|
+
close(): void;
|
|
183
|
+
private buildUrl;
|
|
184
|
+
private tryPort;
|
|
185
|
+
/** 握手成功后绑定正式生命周期(消息分发、掉线重连) */
|
|
186
|
+
private bindLifecycle;
|
|
187
|
+
private scheduleReconnect;
|
|
188
|
+
private failAllPendings;
|
|
189
|
+
private setStatus;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
interface PrintClientOptions {
|
|
193
|
+
/** 配对 token;不传则自动读取 localStorage 中 pair() 保存的值 */
|
|
194
|
+
token?: string;
|
|
195
|
+
/** 单请求默认超时 ms,默认 15000 */
|
|
196
|
+
timeoutMs?: number;
|
|
197
|
+
/** @internal 注入 WebSocket 构造器(测试用) */
|
|
198
|
+
WebSocketCtor?: typeof WebSocket;
|
|
199
|
+
}
|
|
200
|
+
declare class PrintClient {
|
|
201
|
+
private readonly transport;
|
|
202
|
+
constructor(opts?: PrintClientOptions);
|
|
203
|
+
get status(): TransportStatus;
|
|
204
|
+
/** 连接本机客户端(自动端口探测 + 掉线重连) */
|
|
205
|
+
connect(signal?: AbortSignal): Promise<HelloResponsePayload>;
|
|
206
|
+
onStatusChange(cb: (s: TransportStatus) => void): () => void;
|
|
207
|
+
/** 枚举本机打印机 */
|
|
208
|
+
listPrinters(): Promise<PrinterInfo[]>;
|
|
209
|
+
/**
|
|
210
|
+
* 提交静默打印任务。
|
|
211
|
+
* @param templateJson core 模板 JSON
|
|
212
|
+
* @param printData 业务数据
|
|
213
|
+
* @param options 打印机/纸张/份数等;baseUrl 为相对图片资源基址,fontBaseUrl 为相对字体资源基址
|
|
214
|
+
* (字体常在 CDN/前端站点,与图片不在同一域时需单独指定);timeoutMs 可单独放宽大任务
|
|
215
|
+
* @param templateName 模板名称,透传给客户端用于任务记录
|
|
216
|
+
*/
|
|
217
|
+
print(templateJson: Record<string, unknown>, printData?: Record<string, unknown> | Array<Record<string, unknown>>, options?: PrintOptions & {
|
|
218
|
+
baseUrl?: string;
|
|
219
|
+
fontBaseUrl?: string;
|
|
220
|
+
timeoutMs?: number;
|
|
221
|
+
}, templateName?: string): Promise<PrintSubmitResponsePayload>;
|
|
222
|
+
/**
|
|
223
|
+
* 提交浏览器侧预渲染结果静默打印(print.submitHtml)。
|
|
224
|
+
*
|
|
225
|
+
* 业务页先用 @worm-vue3-print/core/browser 的 renderHtmlPages 在浏览器内完成
|
|
226
|
+
* 两遍渲染,再调用本方法把最终 HTML 直送客户端;客户端不再执行模板渲染。
|
|
227
|
+
*
|
|
228
|
+
* @param rendered renderHtmlPages 返回值(html/paperMm/continuous/pageCount)
|
|
229
|
+
* @param options 打印机/纸张/份数等;timeoutMs 可单独放宽大任务
|
|
230
|
+
* @param templateName 模板名称,透传给客户端用于任务记录
|
|
231
|
+
*/
|
|
232
|
+
printHtml(rendered: RenderedHtmlPages, options?: {
|
|
233
|
+
timeoutMs?: number;
|
|
234
|
+
} & Omit<PrintOptions, 'paperSize' | 'margins' | 'landscape'>, templateName?: string): Promise<PrintSubmitResponsePayload>;
|
|
235
|
+
/** 保存配对 token(localStorage),并即时更新当前连接的鉴权参数(下次连接生效) */
|
|
236
|
+
pair(token: string): void;
|
|
237
|
+
static loadStoredToken(): string | null;
|
|
238
|
+
close(): void;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export { APP_ID, type ClientErrorCode, type ClientRequest, DEFAULT_PORT, type HelloRequestPayload, type HelloResponsePayload, MESSAGE_TYPES, type MalformedMessage, PORT_SCAN_LIMIT, PrintClient, type PrintClientOptions, type PrintOptions, type PrintSubmitHtmlRequest, type PrintSubmitRequest, type PrintSubmitResponsePayload, type PrinterInfo, type PrintersListResponsePayload, type ProtocolErrorCode, type RenderedHtmlPages, type ServerError, type ServerErrorCode, type ServerOk, type ServerResponse, type TransportOptions, type TransportStatus, WormPrintError, WsTransport, decodeServerMessage, encodeRequest };
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
// src/client/protocol.ts
|
|
2
|
+
var APP_ID = "worm-print-client";
|
|
3
|
+
var DEFAULT_PORT = 17521;
|
|
4
|
+
var PORT_SCAN_LIMIT = 20;
|
|
5
|
+
var MESSAGE_TYPES = {
|
|
6
|
+
HELLO: "hello",
|
|
7
|
+
PRINTERS_LIST: "printers.list",
|
|
8
|
+
PRINT_SUBMIT: "print.submit",
|
|
9
|
+
PRINT_SUBMIT_HTML: "print.submitHtml"
|
|
10
|
+
};
|
|
11
|
+
var seq = 0;
|
|
12
|
+
function nextId() {
|
|
13
|
+
seq = (seq + 1) % Number.MAX_SAFE_INTEGER;
|
|
14
|
+
return `${Date.now().toString(36)}-${seq.toString(36)}`;
|
|
15
|
+
}
|
|
16
|
+
function encodeRequest(type, payload) {
|
|
17
|
+
const frame = { id: nextId(), type, payload };
|
|
18
|
+
return JSON.stringify(frame);
|
|
19
|
+
}
|
|
20
|
+
function decodeServerMessage(raw) {
|
|
21
|
+
let data;
|
|
22
|
+
try {
|
|
23
|
+
data = JSON.parse(raw);
|
|
24
|
+
} catch {
|
|
25
|
+
return { malformed: true };
|
|
26
|
+
}
|
|
27
|
+
if (typeof data !== "object" || data === null) return { malformed: true };
|
|
28
|
+
const obj = data;
|
|
29
|
+
if (typeof obj.id !== "string" || typeof obj.ok !== "boolean") return { malformed: true };
|
|
30
|
+
if (obj.ok) {
|
|
31
|
+
return { id: obj.id, ok: true, payload: obj.payload ?? {} };
|
|
32
|
+
}
|
|
33
|
+
const err = obj.error;
|
|
34
|
+
if (!err || typeof err.code !== "string" || typeof err.message !== "string") {
|
|
35
|
+
return { malformed: true };
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
id: obj.id,
|
|
39
|
+
ok: false,
|
|
40
|
+
error: { code: err.code, message: err.message }
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// src/client/errors.ts
|
|
45
|
+
var WormPrintError = class extends Error {
|
|
46
|
+
constructor(code, message) {
|
|
47
|
+
super(message);
|
|
48
|
+
this.name = "WormPrintError";
|
|
49
|
+
this.code = code;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// src/client/transport.ts
|
|
54
|
+
var AuthRejectedError = class extends Error {
|
|
55
|
+
constructor(message) {
|
|
56
|
+
super(message);
|
|
57
|
+
this.code = "UNAUTHORIZED";
|
|
58
|
+
this.name = "AuthRejectedError";
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
var WsTransport = class {
|
|
62
|
+
constructor(opts = {}) {
|
|
63
|
+
this.ws = null;
|
|
64
|
+
this.status_ = "disconnected";
|
|
65
|
+
this.hello = null;
|
|
66
|
+
this.pendings = /* @__PURE__ */ new Map();
|
|
67
|
+
this.statusCbs = /* @__PURE__ */ new Set();
|
|
68
|
+
this.manualClose = false;
|
|
69
|
+
this.reconnectTimer = null;
|
|
70
|
+
this.reconnectAttempts = 0;
|
|
71
|
+
this.opts = opts;
|
|
72
|
+
this.ctor = opts.WebSocketCtor ?? WebSocket;
|
|
73
|
+
this.defaultTimeout = opts.timeoutMs ?? 15e3;
|
|
74
|
+
}
|
|
75
|
+
get status() {
|
|
76
|
+
return this.status_;
|
|
77
|
+
}
|
|
78
|
+
onStatus(cb) {
|
|
79
|
+
this.statusCbs.add(cb);
|
|
80
|
+
return () => this.statusCbs.delete(cb);
|
|
81
|
+
}
|
|
82
|
+
setToken(token) {
|
|
83
|
+
this.opts = { ...this.opts, token };
|
|
84
|
+
}
|
|
85
|
+
/** 依次探测端口并完成 hello 握手;全部失败抛 CLIENT_NOT_RUNNING */
|
|
86
|
+
async connect(signal) {
|
|
87
|
+
if (this.status_ === "connected" && this.hello) return this.hello;
|
|
88
|
+
this.setStatus("connecting");
|
|
89
|
+
const ports = this.opts.ports ?? Array.from({ length: PORT_SCAN_LIMIT }, (_, i) => DEFAULT_PORT + i);
|
|
90
|
+
for (const port of ports) {
|
|
91
|
+
if (signal?.aborted) throw new WormPrintError("CLIENT_NOT_RUNNING", "\u8FDE\u63A5\u5DF2\u53D6\u6D88");
|
|
92
|
+
try {
|
|
93
|
+
this.hello = await this.tryPort(port, signal);
|
|
94
|
+
this.manualClose = false;
|
|
95
|
+
this.reconnectAttempts = 0;
|
|
96
|
+
this.setStatus("connected");
|
|
97
|
+
return this.hello;
|
|
98
|
+
} catch (err) {
|
|
99
|
+
if (err instanceof AuthRejectedError) {
|
|
100
|
+
this.setStatus("disconnected");
|
|
101
|
+
throw new WormPrintError(
|
|
102
|
+
"UNAUTHORIZED",
|
|
103
|
+
err.message || "\u914D\u5BF9 token \u65E0\u6548\u6216\u6765\u6E90\u672A\u6388\u6743"
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
this.setStatus("disconnected");
|
|
109
|
+
throw new WormPrintError(
|
|
110
|
+
"CLIENT_NOT_RUNNING",
|
|
111
|
+
"\u672A\u53D1\u73B0\u8FD0\u884C\u4E2D\u7684\u6253\u5370\u5BA2\u6237\u7AEF\uFF08\u5DF2\u63A2\u6D4B\u9ED8\u8BA4\u7AEF\u53E3\u533A\u95F4\uFF09\uFF0C\u8BF7\u786E\u8BA4\u5BA2\u6237\u7AEF\u5DF2\u542F\u52A8"
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
/** 发送请求并等待同 id 响应 */
|
|
115
|
+
request(type, payload, timeoutMs) {
|
|
116
|
+
if (this.status_ !== "connected" || !this.ws) {
|
|
117
|
+
return Promise.reject(new WormPrintError("CLIENT_NOT_RUNNING", "\u6253\u5370\u5BA2\u6237\u7AEF\u672A\u8FDE\u63A5"));
|
|
118
|
+
}
|
|
119
|
+
const raw = encodeRequest(type, payload);
|
|
120
|
+
const frame = JSON.parse(raw);
|
|
121
|
+
return new Promise((resolve, reject) => {
|
|
122
|
+
const timer = setTimeout(() => {
|
|
123
|
+
this.pendings.delete(frame.id);
|
|
124
|
+
reject(new WormPrintError("CLIENT_TIMEOUT", `\u8BF7\u6C42 ${type} \u8D85\u65F6\u672A\u54CD\u5E94`));
|
|
125
|
+
}, timeoutMs ?? this.defaultTimeout);
|
|
126
|
+
this.pendings.set(frame.id, {
|
|
127
|
+
resolve,
|
|
128
|
+
reject,
|
|
129
|
+
timer
|
|
130
|
+
});
|
|
131
|
+
this.ws.send(raw);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
close() {
|
|
135
|
+
this.manualClose = true;
|
|
136
|
+
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
137
|
+
this.reconnectTimer = null;
|
|
138
|
+
this.ws?.close();
|
|
139
|
+
this.failAllPendings(new WormPrintError("CLIENT_NOT_RUNNING", "\u8FDE\u63A5\u5DF2\u5173\u95ED"));
|
|
140
|
+
this.setStatus("disconnected");
|
|
141
|
+
}
|
|
142
|
+
// ─── 内部 ───
|
|
143
|
+
buildUrl(port) {
|
|
144
|
+
const url = new URL(`ws://127.0.0.1:${port}`);
|
|
145
|
+
if (this.opts.token) url.searchParams.set("token", this.opts.token);
|
|
146
|
+
return url.toString().replace(/\/$/, "");
|
|
147
|
+
}
|
|
148
|
+
tryPort(port, signal) {
|
|
149
|
+
return new Promise((resolve, reject) => {
|
|
150
|
+
const ws = new this.ctor(this.buildUrl(port));
|
|
151
|
+
this.ws = ws;
|
|
152
|
+
let settled = false;
|
|
153
|
+
const cleanupAbort = () => {
|
|
154
|
+
if (!settled) {
|
|
155
|
+
settled = true;
|
|
156
|
+
reject(new Error("aborted"));
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
signal?.addEventListener("abort", cleanupAbort, { once: true });
|
|
160
|
+
ws.onopen = () => {
|
|
161
|
+
ws.send(encodeRequest("hello", {}));
|
|
162
|
+
};
|
|
163
|
+
ws.onmessage = (ev) => {
|
|
164
|
+
const msg = decodeServerMessage(String(ev.data));
|
|
165
|
+
if ("malformed" in msg) return;
|
|
166
|
+
if (msg.ok && isHello(msg.payload)) {
|
|
167
|
+
if (settled) return;
|
|
168
|
+
settled = true;
|
|
169
|
+
this.bindLifecycle(ws);
|
|
170
|
+
resolve(msg.payload);
|
|
171
|
+
} else if (!msg.ok && msg.error.code === "UNAUTHORIZED") {
|
|
172
|
+
if (settled) return;
|
|
173
|
+
settled = true;
|
|
174
|
+
reject(new AuthRejectedError(msg.error.message));
|
|
175
|
+
} else if (!settled) {
|
|
176
|
+
settled = true;
|
|
177
|
+
reject(new Error("\u63E1\u624B\u5931\u8D25"));
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
ws.onerror = () => {
|
|
181
|
+
if (!settled) {
|
|
182
|
+
settled = true;
|
|
183
|
+
reject(new Error("\u8FDE\u63A5\u9519\u8BEF"));
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
ws.onclose = () => {
|
|
187
|
+
if (!settled) {
|
|
188
|
+
settled = true;
|
|
189
|
+
reject(new Error("\u8FDE\u63A5\u5173\u95ED"));
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
/** 握手成功后绑定正式生命周期(消息分发、掉线重连) */
|
|
195
|
+
bindLifecycle(ws) {
|
|
196
|
+
ws.onmessage = (ev) => {
|
|
197
|
+
const msg = decodeServerMessage(String(ev.data));
|
|
198
|
+
if ("malformed" in msg) return;
|
|
199
|
+
const pending = this.pendings.get(msg.id);
|
|
200
|
+
if (!pending) return;
|
|
201
|
+
this.pendings.delete(msg.id);
|
|
202
|
+
clearTimeout(pending.timer);
|
|
203
|
+
if (msg.ok) {
|
|
204
|
+
pending.resolve(msg.payload);
|
|
205
|
+
} else {
|
|
206
|
+
pending.reject(new WormPrintError(msg.error.code, msg.error.message));
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
ws.onclose = () => {
|
|
210
|
+
this.failAllPendings(new WormPrintError("CLIENT_NOT_RUNNING", "\u4E0E\u5BA2\u6237\u7AEF\u7684\u8FDE\u63A5\u4E2D\u65AD"));
|
|
211
|
+
this.hello = null;
|
|
212
|
+
this.ws = null;
|
|
213
|
+
if (!this.manualClose) this.scheduleReconnect();
|
|
214
|
+
};
|
|
215
|
+
ws.onerror = () => {
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
scheduleReconnect() {
|
|
219
|
+
this.setStatus("connecting");
|
|
220
|
+
const delay = Math.min(1e3 * 2 ** this.reconnectAttempts++, 1e4);
|
|
221
|
+
this.reconnectTimer = setTimeout(() => {
|
|
222
|
+
this.connect().catch(() => this.scheduleReconnect());
|
|
223
|
+
}, delay);
|
|
224
|
+
}
|
|
225
|
+
failAllPendings(err) {
|
|
226
|
+
for (const [, p] of this.pendings) {
|
|
227
|
+
clearTimeout(p.timer);
|
|
228
|
+
p.reject(err);
|
|
229
|
+
}
|
|
230
|
+
this.pendings.clear();
|
|
231
|
+
}
|
|
232
|
+
setStatus(s) {
|
|
233
|
+
if (this.status_ === s) return;
|
|
234
|
+
this.status_ = s;
|
|
235
|
+
for (const cb of this.statusCbs) cb(s);
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
function isHello(payload) {
|
|
239
|
+
return typeof payload === "object" && payload !== null && payload.app === "worm-print-client";
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// src/client/print-client.ts
|
|
243
|
+
var TOKEN_STORAGE_KEY = "worm-print-client:token";
|
|
244
|
+
var PrintClient = class _PrintClient {
|
|
245
|
+
constructor(opts = {}) {
|
|
246
|
+
const token = opts.token ?? _PrintClient.loadStoredToken() ?? void 0;
|
|
247
|
+
const transportOpts = {
|
|
248
|
+
token,
|
|
249
|
+
timeoutMs: opts.timeoutMs,
|
|
250
|
+
WebSocketCtor: opts.WebSocketCtor
|
|
251
|
+
};
|
|
252
|
+
this.transport = new WsTransport(transportOpts);
|
|
253
|
+
}
|
|
254
|
+
get status() {
|
|
255
|
+
return this.transport.status;
|
|
256
|
+
}
|
|
257
|
+
/** 连接本机客户端(自动端口探测 + 掉线重连) */
|
|
258
|
+
connect(signal) {
|
|
259
|
+
return this.transport.connect(signal);
|
|
260
|
+
}
|
|
261
|
+
onStatusChange(cb) {
|
|
262
|
+
return this.transport.onStatus(cb);
|
|
263
|
+
}
|
|
264
|
+
/** 枚举本机打印机 */
|
|
265
|
+
async listPrinters() {
|
|
266
|
+
const res = await this.transport.request(
|
|
267
|
+
MESSAGE_TYPES.PRINTERS_LIST,
|
|
268
|
+
{}
|
|
269
|
+
);
|
|
270
|
+
return res.printers;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* 提交静默打印任务。
|
|
274
|
+
* @param templateJson core 模板 JSON
|
|
275
|
+
* @param printData 业务数据
|
|
276
|
+
* @param options 打印机/纸张/份数等;baseUrl 为相对图片资源基址,fontBaseUrl 为相对字体资源基址
|
|
277
|
+
* (字体常在 CDN/前端站点,与图片不在同一域时需单独指定);timeoutMs 可单独放宽大任务
|
|
278
|
+
* @param templateName 模板名称,透传给客户端用于任务记录
|
|
279
|
+
*/
|
|
280
|
+
print(templateJson, printData, options = {}, templateName) {
|
|
281
|
+
const { baseUrl, fontBaseUrl, timeoutMs, ...print } = options;
|
|
282
|
+
const payload = { templateJson, printData, baseUrl, fontBaseUrl, print };
|
|
283
|
+
if (typeof templateName === "string" && templateName.trim().length > 0) {
|
|
284
|
+
payload.templateName = templateName;
|
|
285
|
+
}
|
|
286
|
+
return this.transport.request(
|
|
287
|
+
MESSAGE_TYPES.PRINT_SUBMIT,
|
|
288
|
+
payload,
|
|
289
|
+
timeoutMs
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* 提交浏览器侧预渲染结果静默打印(print.submitHtml)。
|
|
294
|
+
*
|
|
295
|
+
* 业务页先用 @worm-vue3-print/core/browser 的 renderHtmlPages 在浏览器内完成
|
|
296
|
+
* 两遍渲染,再调用本方法把最终 HTML 直送客户端;客户端不再执行模板渲染。
|
|
297
|
+
*
|
|
298
|
+
* @param rendered renderHtmlPages 返回值(html/paperMm/continuous/pageCount)
|
|
299
|
+
* @param options 打印机/纸张/份数等;timeoutMs 可单独放宽大任务
|
|
300
|
+
* @param templateName 模板名称,透传给客户端用于任务记录
|
|
301
|
+
*/
|
|
302
|
+
printHtml(rendered, options = {}, templateName) {
|
|
303
|
+
const { timeoutMs, ...print } = options;
|
|
304
|
+
const payload = {
|
|
305
|
+
html: rendered.html,
|
|
306
|
+
paperMm: rendered.paperMm,
|
|
307
|
+
continuous: rendered.continuous,
|
|
308
|
+
pageCount: rendered.pageCount,
|
|
309
|
+
print
|
|
310
|
+
};
|
|
311
|
+
if (typeof templateName === "string" && templateName.trim().length > 0) {
|
|
312
|
+
payload.templateName = templateName;
|
|
313
|
+
}
|
|
314
|
+
return this.transport.request(
|
|
315
|
+
MESSAGE_TYPES.PRINT_SUBMIT_HTML,
|
|
316
|
+
payload,
|
|
317
|
+
timeoutMs
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
/** 保存配对 token(localStorage),并即时更新当前连接的鉴权参数(下次连接生效) */
|
|
321
|
+
pair(token) {
|
|
322
|
+
localStorage.setItem(TOKEN_STORAGE_KEY, token);
|
|
323
|
+
this.transport.setToken(token);
|
|
324
|
+
}
|
|
325
|
+
static loadStoredToken() {
|
|
326
|
+
try {
|
|
327
|
+
return localStorage.getItem(TOKEN_STORAGE_KEY);
|
|
328
|
+
} catch {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
close() {
|
|
333
|
+
this.transport.close();
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
export {
|
|
337
|
+
APP_ID,
|
|
338
|
+
DEFAULT_PORT,
|
|
339
|
+
MESSAGE_TYPES,
|
|
340
|
+
PORT_SCAN_LIMIT,
|
|
341
|
+
PrintClient,
|
|
342
|
+
WormPrintError,
|
|
343
|
+
WsTransport,
|
|
344
|
+
decodeServerMessage,
|
|
345
|
+
encodeRequest
|
|
346
|
+
};
|