taro-bluetooth-print 2.7.0 → 2.8.3
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/CHANGELOG.md +32 -0
- package/README.md +104 -133
- package/dist/index.cjs.js +1 -1
- package/dist/index.es.js +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/types/device/DeviceManager.d.ts +1 -1
- package/dist/types/device/DiscoveryService.d.ts +3 -2
- package/dist/types/drivers/SprtDriver.d.ts +87 -0
- package/dist/types/drivers/XprinterDriver.d.ts +84 -0
- package/dist/types/drivers/index.d.ts +2 -0
- package/dist/types/services/CloudPrintManager.d.ts +123 -0
- package/dist/types/services/PrintScheduler.d.ts +3 -3
- package/dist/types/services/QRCodeDiscoveryService.d.ts +120 -0
- package/dist/types/services/QRCodeParser.d.ts +60 -0
- package/dist/types/services/index.d.ts +3 -0
- package/dist/types/types.d.ts +1 -1
- package/package.json +1 -1
- package/src/device/DeviceManager.ts +2 -2
- package/src/device/DiscoveryService.ts +39 -26
- package/src/drivers/SprtDriver.ts +163 -0
- package/src/drivers/XprinterDriver.ts +155 -0
- package/src/drivers/index.ts +4 -0
- package/src/services/CloudPrintManager.ts +372 -0
- package/src/services/PrintScheduler.ts +18 -8
- package/src/services/QRCodeDiscoveryService.ts +429 -0
- package/src/services/QRCodeParser.ts +185 -0
- package/src/services/index.ts +29 -0
- package/src/template/TemplateEngine.ts +2 -2
- package/src/types.ts +1 -1
- package/src/utils/image.ts +37 -38
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-base-to-string */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* QRCodeDiscoveryService - 二维码打印机配对服务
|
|
5
|
+
*
|
|
6
|
+
* 用于扫描打印机屏幕二维码获取设备信息并直连
|
|
7
|
+
*
|
|
8
|
+
* 支持的二维码格式:
|
|
9
|
+
* 1. 商米格式 (Sunmi): 包含蓝牙地址和服务信息
|
|
10
|
+
* 2. 标准格式: 包含 MAC 地址
|
|
11
|
+
* 3. 自定义格式: 可扩展
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 二维码发现服务配置选项
|
|
16
|
+
*/
|
|
17
|
+
export interface QRCodeDiscoveryOptions {
|
|
18
|
+
/** 二维码格式类型 */
|
|
19
|
+
format: 'sunmi' | 'standard' | 'custom';
|
|
20
|
+
/** 是否自动连接 */
|
|
21
|
+
autoConnect?: boolean;
|
|
22
|
+
/** 自定义解析函数 */
|
|
23
|
+
parser?: (content: string) => ParsedDeviceInfo;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 解析后的设备信息
|
|
28
|
+
*/
|
|
29
|
+
export interface ParsedDeviceInfo {
|
|
30
|
+
/** 设备名称 */
|
|
31
|
+
name?: string;
|
|
32
|
+
/** 蓝牙 MAC 地址 */
|
|
33
|
+
address?: string;
|
|
34
|
+
/** 服务 UUID */
|
|
35
|
+
serviceUuid?: string;
|
|
36
|
+
/** 设备类型 */
|
|
37
|
+
type?: 'printer' | 'scanner' | 'other';
|
|
38
|
+
/** 其他元数据 */
|
|
39
|
+
metadata?: Record<string, string>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 发现结果
|
|
44
|
+
*/
|
|
45
|
+
export interface DiscoveryResult {
|
|
46
|
+
/** 解析后的设备信息 */
|
|
47
|
+
device: ParsedDeviceInfo;
|
|
48
|
+
/** 原始二维码内容 */
|
|
49
|
+
raw: string;
|
|
50
|
+
/** 解析格式 */
|
|
51
|
+
format: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* MAC 地址正则表达式 (冒号分隔)
|
|
56
|
+
*/
|
|
57
|
+
const MAC_COLON_PATTERN =
|
|
58
|
+
/^([0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2})$/;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* MAC 地址正则表达式 (连字符分隔)
|
|
62
|
+
*/
|
|
63
|
+
const MAC_HYPHEN_PATTERN =
|
|
64
|
+
/^([0-9A-Fa-f]{2}-[0-9A-Fa-f]{2}-[0-9A-Fa-f]{2}-[0-9A-Fa-f]{2}-[0-9A-Fa-f]{2}-[0-9A-Fa-f]{2})$/;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* MAC 地址正则表达式 (无分隔符)
|
|
68
|
+
*/
|
|
69
|
+
const MAC_PLAIN_PATTERN = /^([0-9A-Fa-f]{12})$/;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 商米 JSON 格式正则
|
|
73
|
+
*/
|
|
74
|
+
const SUNMI_JSON_PATTERN = /^\{"name"\s*:\s*"([^"]+)"\s*,\s*"mac"\s*:\s*"([^"]+)"[^}]*\}$/;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 商米分隔符格式正则 (格式: name|MAC|type)
|
|
78
|
+
*/
|
|
79
|
+
const SUNMI_PIPE_PATTERN = /^([^|]+)\|([0-9A-Fa-f:]+)\|([^|]+)$/;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 二维码发现服务
|
|
83
|
+
*/
|
|
84
|
+
export class QRCodeDiscoveryService {
|
|
85
|
+
private options: QRCodeDiscoveryOptions;
|
|
86
|
+
|
|
87
|
+
constructor(options: QRCodeDiscoveryOptions) {
|
|
88
|
+
this.options = options;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 解析二维码内容
|
|
93
|
+
* @param content 二维码字符串内容
|
|
94
|
+
* @returns 解析结果
|
|
95
|
+
*/
|
|
96
|
+
parse(content: string): DiscoveryResult {
|
|
97
|
+
if (!content || typeof content !== 'string') {
|
|
98
|
+
return {
|
|
99
|
+
device: { type: 'other' },
|
|
100
|
+
raw: content || '',
|
|
101
|
+
format: 'unknown',
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const trimmedContent = content.trim();
|
|
106
|
+
|
|
107
|
+
// 根据格式选择解析器
|
|
108
|
+
switch (this.options.format) {
|
|
109
|
+
case 'sunmi':
|
|
110
|
+
return this.parseSunmi(trimmedContent);
|
|
111
|
+
case 'standard':
|
|
112
|
+
return this.parseStandard(trimmedContent);
|
|
113
|
+
case 'custom':
|
|
114
|
+
return this.options.parser
|
|
115
|
+
? { device: this.options.parser(trimmedContent), raw: trimmedContent, format: 'custom' }
|
|
116
|
+
: this.parseStandard(trimmedContent);
|
|
117
|
+
default:
|
|
118
|
+
return this.parseStandard(trimmedContent);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 解析商米格式二维码
|
|
124
|
+
* 商米打印机二维码通常包含:
|
|
125
|
+
* - 设备名称
|
|
126
|
+
* - 蓝牙地址
|
|
127
|
+
* - 连接参数
|
|
128
|
+
*
|
|
129
|
+
* 支持的格式:
|
|
130
|
+
* 1. JSON: {"name":"SUNMI P2","mac":"AA:BB:CC:DD:EE:FF","type":"printer"}
|
|
131
|
+
* 2. 分隔符: SUNMI P2|AA:BB:CC:DD:EE:FF|printer
|
|
132
|
+
*/
|
|
133
|
+
parseSunmi(content: string): DiscoveryResult {
|
|
134
|
+
// 尝试解析 JSON 格式
|
|
135
|
+
const jsonResult = this.parseSunmiJson(content);
|
|
136
|
+
if (jsonResult) {
|
|
137
|
+
return jsonResult;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 尝试解析分隔符格式
|
|
141
|
+
const pipeResult = this.parseSunmiPipe(content);
|
|
142
|
+
if (pipeResult) {
|
|
143
|
+
return pipeResult;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 尝试解析纯 MAC 地址(商米设备有时只显示 MAC)
|
|
147
|
+
const macResult = this.parseMacAddress(content);
|
|
148
|
+
if (macResult) {
|
|
149
|
+
return {
|
|
150
|
+
device: { ...macResult.device, name: 'Sunmi Device', type: 'printer' },
|
|
151
|
+
raw: content,
|
|
152
|
+
format: 'sunmi-mac',
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 无法解析,返回原始内容
|
|
157
|
+
return {
|
|
158
|
+
device: { type: 'other', metadata: { raw: content } },
|
|
159
|
+
raw: content,
|
|
160
|
+
format: 'sunmi-unknown',
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* 解析商米 JSON 格式
|
|
166
|
+
*/
|
|
167
|
+
private parseSunmiJson(content: string): DiscoveryResult | null {
|
|
168
|
+
// 检查是否是 JSON 格式
|
|
169
|
+
if (!content.startsWith('{') || !content.endsWith('}')) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
// 尝试使用正则解析(更可靠)
|
|
175
|
+
const match = content.match(SUNMI_JSON_PATTERN);
|
|
176
|
+
if (match) {
|
|
177
|
+
return {
|
|
178
|
+
device: {
|
|
179
|
+
name: match[1],
|
|
180
|
+
address: match[2],
|
|
181
|
+
type: 'printer',
|
|
182
|
+
metadata: this.extractJsonMetadata(content),
|
|
183
|
+
},
|
|
184
|
+
raw: content,
|
|
185
|
+
format: 'sunmi-json',
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// 尝试完整 JSON 解析
|
|
190
|
+
const json = JSON.parse(content);
|
|
191
|
+
if (json.mac || json.MAC || json.address || json.bluetooth) {
|
|
192
|
+
const address = json.mac || json.MAC || json.address || json.bluetooth;
|
|
193
|
+
const name = json.name || json.Name || json.deviceName || 'Sunmi Device';
|
|
194
|
+
const deviceType = this.normalizeDeviceType(json.type || json.deviceType);
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
device: {
|
|
198
|
+
name: name || 'Unknown Device',
|
|
199
|
+
address: this.normalizeMacAddress(address) || '',
|
|
200
|
+
type: deviceType || 'printer',
|
|
201
|
+
serviceUuid: ((json.serviceUuid || json.uuid) as string) || undefined,
|
|
202
|
+
metadata: this.extractJsonMetadata(content),
|
|
203
|
+
},
|
|
204
|
+
raw: content,
|
|
205
|
+
format: 'sunmi-json',
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return null;
|
|
210
|
+
} catch {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* 解析商米分隔符格式 (name|MAC|type)
|
|
217
|
+
*/
|
|
218
|
+
private parseSunmiPipe(content: string): DiscoveryResult | null {
|
|
219
|
+
const match = content.match(SUNMI_PIPE_PATTERN);
|
|
220
|
+
if (!match) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const [, name, mac, type] = match;
|
|
225
|
+
const normalizedMac = this.normalizeMacAddress(mac || '');
|
|
226
|
+
if (!normalizedMac) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
device: {
|
|
232
|
+
name: (name || '').trim() || 'Unknown Device',
|
|
233
|
+
address: normalizedMac,
|
|
234
|
+
type: this.normalizeDeviceType((type || '').trim()),
|
|
235
|
+
metadata: { originalType: type || '' },
|
|
236
|
+
},
|
|
237
|
+
raw: content,
|
|
238
|
+
format: 'sunmi-pipe',
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* 解析标准格式二维码
|
|
244
|
+
* 标准格式通常是 MAC 地址或蓝牙设备信息
|
|
245
|
+
*
|
|
246
|
+
* 支持的格式:
|
|
247
|
+
* 1. AA:BB:CC:DD:EE:FF (冒号分隔)
|
|
248
|
+
* 2. AA-BB-CC-DD-EE-FF (连字符分隔)
|
|
249
|
+
* 3. AABBCCDDEEFF (无分隔符)
|
|
250
|
+
*/
|
|
251
|
+
parseStandard(content: string): DiscoveryResult {
|
|
252
|
+
// 尝试解析 MAC 地址
|
|
253
|
+
const macResult = this.parseMacAddress(content);
|
|
254
|
+
if (macResult) {
|
|
255
|
+
return macResult;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// 无法解析,返回原始内容
|
|
259
|
+
return {
|
|
260
|
+
device: { type: 'other', metadata: { raw: content } },
|
|
261
|
+
raw: content,
|
|
262
|
+
format: 'standard-unknown',
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* 解析 MAC 地址
|
|
268
|
+
*/
|
|
269
|
+
private parseMacAddress(content: string): DiscoveryResult | null {
|
|
270
|
+
// 尝试冒号分隔格式
|
|
271
|
+
if (MAC_COLON_PATTERN.test(content)) {
|
|
272
|
+
return {
|
|
273
|
+
device: {
|
|
274
|
+
address: content.toUpperCase(),
|
|
275
|
+
type: 'other',
|
|
276
|
+
},
|
|
277
|
+
raw: content,
|
|
278
|
+
format: 'mac-colon',
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// 尝试连字符分隔格式
|
|
283
|
+
if (MAC_HYPHEN_PATTERN.test(content)) {
|
|
284
|
+
return {
|
|
285
|
+
device: {
|
|
286
|
+
address: content.toUpperCase(),
|
|
287
|
+
type: 'other',
|
|
288
|
+
},
|
|
289
|
+
raw: content,
|
|
290
|
+
format: 'mac-hyphen',
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// 尝试无分隔符格式
|
|
295
|
+
if (MAC_PLAIN_PATTERN.test(content)) {
|
|
296
|
+
const formatted = this.formatMacWithColons(content.toUpperCase());
|
|
297
|
+
return {
|
|
298
|
+
device: {
|
|
299
|
+
address: formatted,
|
|
300
|
+
type: 'other',
|
|
301
|
+
},
|
|
302
|
+
raw: content,
|
|
303
|
+
format: 'mac-plain',
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* 标准化 MAC 地址格式(转换为大写冒号分隔)
|
|
312
|
+
*/
|
|
313
|
+
private normalizeMacAddress(mac: string): string | null {
|
|
314
|
+
if (!mac || typeof mac !== 'string') {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// 移除空格
|
|
319
|
+
const cleaned = mac.replace(/\s/g, '').toUpperCase();
|
|
320
|
+
|
|
321
|
+
// 冒号格式
|
|
322
|
+
if (MAC_COLON_PATTERN.test(cleaned)) {
|
|
323
|
+
return cleaned;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// 连字符格式
|
|
327
|
+
if (MAC_HYPHEN_PATTERN.test(cleaned)) {
|
|
328
|
+
return cleaned.replace(/-/g, ':');
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// 无分隔符格式
|
|
332
|
+
if (MAC_PLAIN_PATTERN.test(cleaned)) {
|
|
333
|
+
return this.formatMacWithColons(cleaned);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* 格式化 MAC 地址为冒号分隔格式
|
|
341
|
+
*/
|
|
342
|
+
private formatMacWithColons(mac: string): string {
|
|
343
|
+
const parts: string[] = [];
|
|
344
|
+
for (let i = 0; i < mac.length; i += 2) {
|
|
345
|
+
parts.push(mac.substring(i, i + 2));
|
|
346
|
+
}
|
|
347
|
+
return parts.join(':');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* 标准化设备类型
|
|
352
|
+
*/
|
|
353
|
+
private normalizeDeviceType(type: string | undefined): 'printer' | 'scanner' | 'other' {
|
|
354
|
+
if (!type) {
|
|
355
|
+
return 'other';
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const normalized = type.toLowerCase().trim();
|
|
359
|
+
|
|
360
|
+
if (normalized.includes('print') || normalized === 'p') {
|
|
361
|
+
return 'printer';
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (normalized.includes('scan') || normalized === 's') {
|
|
365
|
+
return 'scanner';
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return 'other';
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* 从 JSON 字符串中提取元数据
|
|
373
|
+
*/
|
|
374
|
+
private extractJsonMetadata(jsonStr: string): Record<string, string> {
|
|
375
|
+
const metadata: Record<string, string> = {};
|
|
376
|
+
try {
|
|
377
|
+
const json = JSON.parse(jsonStr);
|
|
378
|
+
const knownKeys = [
|
|
379
|
+
'name',
|
|
380
|
+
'mac',
|
|
381
|
+
'MAC',
|
|
382
|
+
'address',
|
|
383
|
+
'type',
|
|
384
|
+
'deviceType',
|
|
385
|
+
'serviceUuid',
|
|
386
|
+
'uuid',
|
|
387
|
+
];
|
|
388
|
+
for (const [key, value] of Object.entries(json)) {
|
|
389
|
+
if (!knownKeys.includes(key) && value !== undefined && value !== null) {
|
|
390
|
+
metadata[key] = String(value);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
} catch {
|
|
394
|
+
// Ignore parse errors
|
|
395
|
+
}
|
|
396
|
+
return metadata;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* 获取配置选项
|
|
401
|
+
*/
|
|
402
|
+
getOptions(): QRCodeDiscoveryOptions {
|
|
403
|
+
return { ...this.options };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* 验证解析结果是否有效
|
|
408
|
+
*/
|
|
409
|
+
static isValidResult(result: DiscoveryResult): boolean {
|
|
410
|
+
return !!(
|
|
411
|
+
result &&
|
|
412
|
+
result.device &&
|
|
413
|
+
result.device.address &&
|
|
414
|
+
/^[0-9A-Fa-f:]{17}$/.test(result.device.address)
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Re-export QRCodeParser utilities for convenience
|
|
420
|
+
export {
|
|
421
|
+
parseQRCode,
|
|
422
|
+
parseMultipleQRCodes,
|
|
423
|
+
detectQRCodeFormat,
|
|
424
|
+
addQRCodeFormat,
|
|
425
|
+
removeQRCodeFormat,
|
|
426
|
+
getSupportedFormats,
|
|
427
|
+
QR_CODE_FORMATS,
|
|
428
|
+
} from './QRCodeParser';
|
|
429
|
+
export type { QRCodeFormat } from './QRCodeParser';
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QRCodeParser - 二维码解析工具
|
|
3
|
+
*
|
|
4
|
+
* 提供通用的二维码解析能力,支持多种格式的自动识别
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { ParsedDeviceInfo } from './QRCodeDiscoveryService';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 二维码格式定义
|
|
11
|
+
*/
|
|
12
|
+
export interface QRCodeFormat {
|
|
13
|
+
/** 格式名称 */
|
|
14
|
+
name: string;
|
|
15
|
+
/** 正则表达式 */
|
|
16
|
+
pattern: RegExp;
|
|
17
|
+
/** 解析函数 */
|
|
18
|
+
parse: (match: RegExpMatchArray, raw: string) => ParsedDeviceInfo;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 内置支持的二维码格式
|
|
23
|
+
*/
|
|
24
|
+
export const QR_CODE_FORMATS: QRCodeFormat[] = [
|
|
25
|
+
// 商米 JSON 格式
|
|
26
|
+
{
|
|
27
|
+
name: 'sunmi-json',
|
|
28
|
+
pattern: /^\{"name"\s*:\s*"([^"]+)"\s*,\s*"mac"\s*:\s*"([^"]+)"[^}]*\}$/,
|
|
29
|
+
parse: match => ({
|
|
30
|
+
name: match[1] ?? '',
|
|
31
|
+
address: (match[2] ?? '').toUpperCase(),
|
|
32
|
+
type: 'printer',
|
|
33
|
+
}),
|
|
34
|
+
},
|
|
35
|
+
// 商米分隔符格式
|
|
36
|
+
{
|
|
37
|
+
name: 'sunmi-pipe',
|
|
38
|
+
pattern: /^([^|]+)\|([0-9A-Fa-f:]+)\|([^|]+)$/,
|
|
39
|
+
parse: match => ({
|
|
40
|
+
name: (match[1] ?? '').trim(),
|
|
41
|
+
address: (match[2] ?? '').toUpperCase(),
|
|
42
|
+
type: 'printer',
|
|
43
|
+
}),
|
|
44
|
+
},
|
|
45
|
+
// MAC 地址(冒号分隔)
|
|
46
|
+
{
|
|
47
|
+
name: 'mac-colon',
|
|
48
|
+
pattern:
|
|
49
|
+
/^([0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2})$/,
|
|
50
|
+
parse: match => ({
|
|
51
|
+
address: (match[1] ?? '').toUpperCase(),
|
|
52
|
+
type: 'other',
|
|
53
|
+
}),
|
|
54
|
+
},
|
|
55
|
+
// MAC 地址(连字符分隔)
|
|
56
|
+
{
|
|
57
|
+
name: 'mac-hyphen',
|
|
58
|
+
pattern:
|
|
59
|
+
/^([0-9A-Fa-f]{2}-[0-9A-Fa-f]{2}-[0-9A-Fa-f]{2}-[0-9A-Fa-f]{2}-[0-9A-Fa-f]{2}-[0-9A-Fa-f]{2})$/,
|
|
60
|
+
parse: match => ({
|
|
61
|
+
address: (match[1] ?? '').toUpperCase().replace(/-/g, ':'),
|
|
62
|
+
type: 'other',
|
|
63
|
+
}),
|
|
64
|
+
},
|
|
65
|
+
// MAC 地址(无分隔符)
|
|
66
|
+
{
|
|
67
|
+
name: 'mac-plain',
|
|
68
|
+
pattern: /^([0-9A-Fa-f]{12})$/,
|
|
69
|
+
parse: match => {
|
|
70
|
+
const mac = (match[1] ?? '').toUpperCase();
|
|
71
|
+
const formatted = `${mac.slice(0, 2)}:${mac.slice(2, 4)}:${mac.slice(4, 6)}:${mac.slice(6, 8)}:${mac.slice(8, 10)}:${mac.slice(10, 12)}`;
|
|
72
|
+
return {
|
|
73
|
+
address: formatted,
|
|
74
|
+
type: 'other',
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 解析二维码字符串
|
|
82
|
+
*
|
|
83
|
+
* @param content 二维码原始内容
|
|
84
|
+
* @returns 解析后的设备信息,如果无法解析则返回 null
|
|
85
|
+
*/
|
|
86
|
+
export function parseQRCode(content: string): ParsedDeviceInfo | null {
|
|
87
|
+
if (!content || typeof content !== 'string') {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const trimmedContent = content.trim();
|
|
92
|
+
|
|
93
|
+
for (const format of QR_CODE_FORMATS) {
|
|
94
|
+
const match = trimmedContent.match(format.pattern);
|
|
95
|
+
if (match) {
|
|
96
|
+
return format.parse(match, trimmedContent);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 批量解析多个二维码
|
|
105
|
+
*
|
|
106
|
+
* @param contents 二维码内容数组
|
|
107
|
+
* @returns 解析结果数组,包含成功解析和失败的结果
|
|
108
|
+
*/
|
|
109
|
+
export function parseMultipleQRCodes(contents: string[]): {
|
|
110
|
+
success: Array<{ content: string; device: ParsedDeviceInfo }>;
|
|
111
|
+
failed: string[];
|
|
112
|
+
} {
|
|
113
|
+
const success: Array<{ content: string; device: ParsedDeviceInfo }> = [];
|
|
114
|
+
const failed: string[] = [];
|
|
115
|
+
|
|
116
|
+
for (const content of contents) {
|
|
117
|
+
const result = parseQRCode(content);
|
|
118
|
+
if (result) {
|
|
119
|
+
success.push({ content, device: result });
|
|
120
|
+
} else {
|
|
121
|
+
failed.push(content);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { success, failed };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 检测二维码内容类型
|
|
130
|
+
*
|
|
131
|
+
* @param content 二维码内容
|
|
132
|
+
* @returns 格式名称,如果无法识别则返回 null
|
|
133
|
+
*/
|
|
134
|
+
export function detectQRCodeFormat(content: string): string | null {
|
|
135
|
+
if (!content || typeof content !== 'string') {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const trimmedContent = content.trim();
|
|
140
|
+
|
|
141
|
+
for (const format of QR_CODE_FORMATS) {
|
|
142
|
+
if (format.pattern.test(trimmedContent)) {
|
|
143
|
+
return format.name;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 添加自定义二维码格式
|
|
152
|
+
*
|
|
153
|
+
* @param format 格式定义
|
|
154
|
+
*/
|
|
155
|
+
export function addQRCodeFormat(format: QRCodeFormat): void {
|
|
156
|
+
// 检查是否已存在同名格式
|
|
157
|
+
const existingIndex = QR_CODE_FORMATS.findIndex(f => f.name === format.name);
|
|
158
|
+
if (existingIndex >= 0) {
|
|
159
|
+
QR_CODE_FORMATS[existingIndex] = format;
|
|
160
|
+
} else {
|
|
161
|
+
QR_CODE_FORMATS.push(format);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* 移除自定义二维码格式
|
|
167
|
+
*
|
|
168
|
+
* @param formatName 格式名称
|
|
169
|
+
* @returns 是否成功移除
|
|
170
|
+
*/
|
|
171
|
+
export function removeQRCodeFormat(formatName: string): boolean {
|
|
172
|
+
const index = QR_CODE_FORMATS.findIndex(f => f.name === formatName);
|
|
173
|
+
if (index >= 0) {
|
|
174
|
+
QR_CODE_FORMATS.splice(index, 1);
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* 获取所有支持的格式名称列表
|
|
182
|
+
*/
|
|
183
|
+
export function getSupportedFormats(): string[] {
|
|
184
|
+
return QR_CODE_FORMATS.map(f => f.name);
|
|
185
|
+
}
|
package/src/services/index.ts
CHANGED
|
@@ -67,3 +67,32 @@ export {
|
|
|
67
67
|
} from './PrintScheduler';
|
|
68
68
|
|
|
69
69
|
export * from './interfaces';
|
|
70
|
+
|
|
71
|
+
// 云打印服务
|
|
72
|
+
export {
|
|
73
|
+
CloudPrintManager,
|
|
74
|
+
type CloudPrintOptions,
|
|
75
|
+
type PrintJob,
|
|
76
|
+
type CloudPrinterStatus,
|
|
77
|
+
type CloudPrintEvents,
|
|
78
|
+
type CloudPrintEvent,
|
|
79
|
+
} from './CloudPrintManager';
|
|
80
|
+
|
|
81
|
+
// 二维码打印机配对服务
|
|
82
|
+
export {
|
|
83
|
+
QRCodeDiscoveryService,
|
|
84
|
+
type QRCodeDiscoveryOptions,
|
|
85
|
+
type ParsedDeviceInfo,
|
|
86
|
+
type DiscoveryResult,
|
|
87
|
+
} from './QRCodeDiscoveryService';
|
|
88
|
+
|
|
89
|
+
export {
|
|
90
|
+
parseQRCode,
|
|
91
|
+
parseMultipleQRCodes,
|
|
92
|
+
detectQRCodeFormat,
|
|
93
|
+
addQRCodeFormat,
|
|
94
|
+
removeQRCodeFormat,
|
|
95
|
+
getSupportedFormats,
|
|
96
|
+
QR_CODE_FORMATS,
|
|
97
|
+
type QRCodeFormat,
|
|
98
|
+
} from './QRCodeParser';
|
|
@@ -928,8 +928,8 @@ export class TemplateEngine implements ITemplateEngine {
|
|
|
928
928
|
const padded = text.padEnd(width).substring(0, width);
|
|
929
929
|
switch (align) {
|
|
930
930
|
case TextAlign.CENTER: {
|
|
931
|
-
const leftPad = Math.floor((width -
|
|
932
|
-
return
|
|
931
|
+
const leftPad = Math.floor((width - padded.length) / 2);
|
|
932
|
+
return padded.padStart(leftPad + padded.length).padEnd(width);
|
|
933
933
|
}
|
|
934
934
|
case TextAlign.RIGHT:
|
|
935
935
|
return text.padStart(width);
|
package/src/types.ts
CHANGED