使用示例说明
本文档基于真实项目场景,按照「连接 -> 读取配置 -> 检查配置 -> 设置配置 -> 打印 -> 查询日志」的完整流程,提供可落地的集成示例。
注意
文档根据项目源码AI处理生成. 虽然本人阅读排查过问题,但或许还是有些许遗漏错误.
若文档有问题, 请联系我修正.
若使用有疑惑, 请联系我答疑.
API文档
https://celmpuecfz.apifox.cn/305982s0
一、集成流程概述
真实项目中集成 sv-print 客户端,推荐按以下顺序进行:
当然 正常情况下, 获取配置,获取打印机列表等配置相关项,可以只在初始化时进行一次。
例如,在应用启动时,读取配置项,设置默认打印机,获取打印机列表等。
┌─────────────────────────────────────────────────────────────┐
│ 1. 获取服务地址(IP + 端口) │
│ ↓ │
│ 2. 连接服务(Socket.IO / HTTP) │
│ ↓ │
│ 3. 读取当前配置 ←─────────────────────┐ │
│ ↓ │ │
│ 4. 检查关键配置(token/默认打印机等) │ 配置缺失 │
│ ├─ 缺失 → 设置配置 ────────────────┘ │
│ │ └─ 若改了 socket 配置 → 重连 │
│ ↓ │
│ 5. 获取打印机列表 │
│ ↓ │
│ 6. 验证/设置默认打印机 │
│ ↓ │
│ 7. 执行打印 │
│ ↓ │
│ 8. 接收打印结果回调 │
│ ↓ │
│ 9. 查询打印日志(可选) │
└─────────────────────────────────────────────────────────────┘两种调用方式的选择
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| Web 页面集成打印 | Socket.IO | 支持实时回调,打印完成自动通知 |
| 后端服务调用 | HTTP | 简单直接,无需维持长连接 |
| 需要打印结果实时通知 | Socket.IO | 通过 success/error 事件回调 |
| 一次性查询/配置 | HTTP | 无需建立持久连接 |
二、前置准备
2.1 获取服务地址
sv-print 客户端启动后,默认开放以下服务:
| 服务 | 默认地址 | 说明 |
|---|---|---|
| Socket.IO | http://localhost:17521 | 实时通信服务 |
| HTTP API | http://localhost:7071 | RESTful API |
实际部署中,客户端可能运行在局域网其他机器上,需替换
localhost为实际 IP。
2.2 引入客户端库
Socket.IO 客户端:
<!-- 浏览器环境 -->
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script># Node.js 环境
npm install socket.io-client// Node.js 引入
const { io } = require('socket.io-client');
// 或 ES Module
import { io } from 'socket.io-client';2.3 确认服务是否可用
在正式连接前,先通过 HTTP 接口确认服务地址可用:
// 检测服务是否在线
async function checkService(httpUrl) {
try {
const res = await fetch(`${httpUrl}/controller/app/baseInfo`);
if (res.ok) {
const info = await res.json();
console.log('服务在线:', info);
return { online: true, info };
}
return { online: false };
} catch (e) {
console.error('服务不可用:', e.message);
return { online: false };
}
}
// 调用示例
const result = await checkService('http://localhost:7071');
if (!result.online) {
console.error('请确认 sv-print 客户端已启动');
}三、Socket.IO 完整集成示例
第一步:连接服务
如果集成了 sv-print 打印组件. 那么默认是会自动连接 http://localhost:17521 的. 而且全局会提供 io 对象. 和 Socket.IO 客户端一样.
当然可以通过下面方式切换:
const token = '';
hiwebSocket.setHost('http://localhost:17521', token, (data) => {
console.log('setHost', data);
});
// socket 对象: 你可以它来处理下面的 一系列事件操作.
hiwebSocket.socket;
// 比如:
// 监听配置返回
const onConfig = (config) => {
hiwebSocket.socket.off('config', onConfig); // 取消监听,避免重复
console.log('[第二步] 读取配置成功', config);
};
hiwebSocket.socket.on('config', onConfig);
hiwebSocket.socket.emit('config');socket.io 的连接方式如下:
/**
* 第一步:连接 Socket.IO 服务
* @param {string} serverUrl - 服务地址,如 http://localhost:17521
* @param {string} token - 鉴权 Token(服务端未配置时传空字符串)
*/
function connectServer(serverUrl, token = '') {
const socket = io(serverUrl, {
auth: { token }, // Token 鉴权
reconnection: true, // 自动重连
reconnectionDelay: 5000, // 重连间隔 5 秒
timeout: 10000, // 连接超时 10 秒
});
return new Promise((resolve, reject) => {
// 连接成功
socket.on('connect', () => {
console.log('[第一步] 连接成功, socket id:', socket.id);
resolve(socket);
});
// 连接失败
socket.on('connect_error', (err) => {
console.error('[第一步] 连接失败:', err.message);
// token 错误时 err.message === 'Token error'
reject(err);
});
// 断开连接
socket.on('disconnect', (reason) => {
console.warn('[第一步] 连接断开:', reason);
});
});
}
// 调用示例
const socket = await connectServer('http://localhost:17521', '');注意:连接成功后,服务端会自动推送
printerList(打印机列表)和clientInfo(客户端信息)事件。
第二步:读取当前配置
/**
* 第二步:读取当前配置
* @param {Socket} socket - Socket.IO 实例
* @returns {Promise<SchemaType>} 当前配置
*/
function getConfig(socket) {
return new Promise((resolve, reject) => {
// 监听配置返回
const onConfig = (config) => {
socket.off('config', onConfig); // 取消监听,避免重复
console.log('[第二步] 读取配置成功');
resolve(config);
};
socket.on('config', onConfig);
// 发送获取配置请求
socket.emit('config');
// 超时处理
setTimeout(() => {
socket.off('config', onConfig);
reject(new Error('读取配置超时'));
}, 5000);
});
}
// 调用示例
const config = await getConfig(socket);
console.log('当前配置:', {
port: config.port,
token: config.token || '(未设置)',
defaultPrinter: config.defaultPrinter || '(未设置)',
enableHttp: config.enableHttp,
enableHttps: config.enableHttps,
nickName: config.nickName || '(未设置)',
});第三步:检查关键配置
读取配置后,需要检查关键配置项是否满足业务需求:
/**
* 第三步:检查关键配置
* @param {SchemaType} config - 当前配置
* @returns {{ valid: boolean, missing: string[], needReconnect: boolean }}
*/
function checkConfig(config) {
const missing = [];
const warnings = [];
// 检查 1:默认打印机是否设置
if (!config.defaultPrinter) {
missing.push('defaultPrinter');
console.warn('[第三步] 未设置默认打印机,打印时将自动选择系统默认打印机');
}
// 检查 2:Token 鉴权(根据业务需求判断)
// 注意:如果服务端设置了 token,客户端连接时必须传入相同 token
// 这里假设业务要求设置 token
if (!config.token) {
warnings.push('token');
console.warn('[第三步] 未设置 Token,任何客户端都可连接(生产环境建议设置)');
}
// 检查 3:HTTP 服务是否开启
if (config.enableHttp === false) {
warnings.push('enableHttp');
console.warn('[第三步] HTTP 服务未开启,静态文件服务不可用');
}
// 检查 4:日志保留天数
if (config.logDays < 7) {
warnings.push('logDays');
console.warn('[第三步] 日志保留天数过短:', config.logDays);
}
// 检查 5:客户端别名(便于管理)
if (!config.nickName) {
missing.push('nickName');
console.warn('[第三步] 未设置客户端别名');
}
const valid = missing.length === 0;
console.log('[第三步] 配置检查结果:', { valid, missing, warnings });
return { valid, missing, warnings };
}
// 调用示例
const checkResult = checkConfig(config);第四步:补全缺失配置
如果第三步检查发现关键配置缺失,需要通过 updateConfig 补全:
/**
* 第四步:补全缺失配置
* @param {Socket} socket - Socket.IO 实例
* @param {SchemaType} currentConfig - 当前完整配置
* @param {Object} patchConfig - 需要更新的配置项
* @param {RefreshConfig} refreshConfig - 需要重启的服务
* @returns {Promise<SchemaType>} 更新后的配置
*/
function updateConfig(socket, currentConfig, patchConfig, refreshConfig) {
return new Promise((resolve, reject) => {
// 监听配置返回(updateConfig 成功后服务端会通过 config 事件返回最新配置)
const onConfig = (config) => {
socket.off('config', onConfig);
console.log('[第四步] 配置更新成功');
resolve(config);
};
socket.on('config', onConfig);
// 发送更新配置请求
// 注意:config 字段是增量更新,只需传入要修改的字段
socket.emit('updateConfig', {
config: { ...currentConfig, ...patchConfig },
refreshConfig: refreshConfig || { socket: false, cloud: false, mqtt: false, printer: false },
});
// 超时处理
setTimeout(() => {
socket.off('config', onConfig);
reject(new Error('更新配置超时'));
}, 10000);
});
}
// 调用示例:补全缺失的配置
const patchConfig = {};
const refreshConfig = { socket: false, cloud: false, mqtt: false, printer: false };
// 补全客户端别名
if (checkResult.missing.includes('nickName')) {
patchConfig.nickName = '我的打印客户端';
}
// 补全 Token(生产环境建议设置)
if (checkResult.warnings.includes('token')) {
patchConfig.token = 'my-secret-token-2026';
refreshConfig.socket = true; // 修改 token 需要重启 socket 服务
}
// 补全日志保留天数
if (checkResult.warnings.includes('logDays')) {
patchConfig.logDays = 30;
}
if (Object.keys(patchConfig).length > 0) {
console.log('[第四步] 需要更新的配置:', patchConfig);
const newConfig = await updateConfig(socket, config, patchConfig, refreshConfig);
// 重要:如果修改了 token 或 port,当前连接会失效,需要重新连接
if (refreshConfig.socket) {
console.log('[第四步] Socket 配置已变更,需要重新连接');
socket.disconnect();
// 等待服务重启(建议延迟 2 秒)
await new Promise((r) => setTimeout(r, 2000));
// 使用新 token 重新连接
const newSocket = await connectServer('http://localhost:17521', patchConfig.token || '');
// 后续操作使用 newSocket
return newSocket;
}
} else {
console.log('[第四步] 配置完整,无需更新');
}RefreshConfig 说明
修改不同配置需要重启对应服务:
| 修改的配置项 | 对应的 refreshConfig 字段 | 说明 |
|---|---|---|
port、token、enableHttps | socket: true | 重启 Socket.IO 服务 |
connectTransit、transitUrl、transitToken | cloud: true | 重启云服务连接 |
enableMqtt、mqttPort | mqtt: true | 重启 MQTT 服务 |
printViewPath、maxPrintViewNum | printer: true | 重新初始化打印窗口 |
关键:一旦设置
refreshConfig.socket = true,当前 Socket.IO 连接会断开,必须重新连接。
第五步:获取打印机列表
/**
* 第五步:获取打印机列表
* @param {Socket} socket - Socket.IO 实例
* @returns {Promise<Array>} 打印机列表
*/
function getPrinterList(socket) {
return new Promise((resolve, reject) => {
const onPrinterList = (printers) => {
socket.off('printerList', onPrinterList);
console.log('[第五步] 获取打印机列表成功, 共', printers.length, '台');
resolve(printers);
};
socket.on('printerList', onPrinterList);
// 主动请求刷新打印机列表
socket.emit('refreshPrinterList');
setTimeout(() => {
socket.off('printerList', onPrinterList);
reject(new Error('获取打印机列表超时'));
}, 5000);
});
}
// 调用示例
const printers = await getPrinterList(socket);
// 打印机列表数据结构
printers.forEach((p) => {
const status = p.disabled ? '已禁用' : p.isOk ? '正常' : '异常';
console.log(` - ${p.name} [${status}] ${p.isDefault ? '(默认)' : ''}`);
});
// 输出示例:
// - HP LaserJet Pro [正常] (默认)
// - Microsoft Print to PDF [正常]
// - OneNote [已禁用]第六步:设置默认打印机
/**
* 第六步:验证并设置默认打印机
* @param {Socket} socket - Socket.IO 实例
* @param {Array} printers - 打印机列表
* @param {string} expectedPrinter - 期望的默认打印机(可选)
*/
async function ensureDefaultPrinter(socket, printers, expectedPrinter) {
// 找到可用的打印机
const availablePrinters = printers.filter((p) => !p.disabled && p.isOk);
if (availablePrinters.length === 0) {
throw new Error('没有可用的打印机');
}
// 情况 1:指定了期望的打印机
if (expectedPrinter) {
const target = availablePrinters.find((p) => p.name === expectedPrinter);
if (!target) {
throw new Error(`指定的打印机不存在或不可用: ${expectedPrinter}`);
}
// 检查是否已是默认打印机
if (target.isDefault) {
console.log('[第六步] 默认打印机已正确设置:', target.name);
return target.name;
}
// 通过 HTTP 接口设置默认打印机(Socket.IO 没有直接设置默认打印机的事件)
const res = await fetch('http://localhost:7071/controller/printer/setDefaultPrinter', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ printerName: target.name }),
});
const result = await res.json();
if (result === false) {
throw new Error(`设置默认打印机失败: ${target.name}`);
}
console.log('[第六步] 默认打印机已设置为:', target.name);
return target.name;
}
// 情况 2:未指定,检查是否已有默认打印机
const currentDefault = availablePrinters.find((p) => p.isDefault);
if (currentDefault) {
console.log('[第六步] 当前默认打印机:', currentDefault.name);
return currentDefault.name;
}
// 情况 3:没有默认打印机,自动选择第一个可用的
const firstAvailable = availablePrinters[0];
await fetch('http://localhost:7071/controller/printer/setDefaultPrinter', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ printerName: firstAvailable.name }),
});
console.log('[第六步] 已自动设置默认打印机:', firstAvailable.name);
return firstAvailable.name;
}
// 调用示例
const defaultPrinter = await ensureDefaultPrinter(socket, printers, 'HP LaserJet Pro');第七步:执行打印
/**
* 第七步:执行打印
* @param {Socket} socket - Socket.IO 实例
* @param {EventData} printData - 打印数据
*/
function doPrint(socket, printData) {
// 生成唯一的 templateId,用于回调匹配
const templateId = printData.templateId || `print-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const data = {
...printData,
templateId,
};
console.log('[第七步] 发送打印请求, templateId:', templateId);
socket.emit('print', data);
return templateId;
}
// ========== 打印示例 ==========
// 示例 7.1:HTML 打印
const tplId1 = doPrint(socket, {
printer: defaultPrinter, // 可选,不传用默认打印机
html: `
<html>
<body>
<h1>订单打印</h1>
<p>订单号:ORD-2026-0001</p>
<p>金额:¥99.00</p>
<table border="1" cellspacing="0">
<tr><th>商品</th><th>数量</th><th>单价</th></tr>
<tr><td>商品A</td><td>2</td><td>¥30.00</td></tr>
<tr><td>商品B</td><td>1</td><td>¥39.00</td></tr>
</table>
</body>
</html>
`,
// 打印选项
silent: true, // 静默打印
printBackground: true, // 打印背景
margins: { top: 10, bottom: 10, left: 10, right: 10 },
});
// 示例 7.2:URL PDF 打印
const tplId2 = doPrint(socket, {
type: 'url_pdf',
pdf_path: 'http://example.com/files/invoice.pdf',
printer: defaultPrinter,
});
// 示例 7.3:模板打印
const tplId3 = doPrint(socket, {
template: {
panels: [
{
width: 100,
height: 60,
printElements: [
{
tid: 'element.title',
options: { field: 'title', x: 10, y: 10 },
},
],
},
],
},
templateId: 'tpl-order-001',
});第八步:接收打印结果
/**
* 第八步:监听打印结果
* @param {Socket} socket - Socket.IO 实例
* @param {string} expectedTemplateId - 期望接收的 templateId
* @returns {Promise<Object>} 打印结果
*/
function waitForPrintResult(socket, expectedTemplateId) {
return new Promise((resolve, reject) => {
let settled = false;
// 打印成功
const onSuccess = (data) => {
if (settled) return;
// 匹配 templateId(如果指定了)
if (expectedTemplateId && data.templateId !== expectedTemplateId) {
return; // 不是本次打印的结果,忽略
}
settled = true;
socket.off('success', onSuccess);
socket.off('error', onError);
console.log('[第八步] 打印成功:', data);
resolve(data);
};
// 打印失败
const onError = (data) => {
if (settled) return;
if (expectedTemplateId && data.templateId !== expectedTemplateId) {
return;
}
settled = true;
socket.off('success', onSuccess);
socket.off('error', onError);
console.error('[第八步] 打印失败:', data);
reject(new Error(data.msg || '打印失败'));
};
socket.on('success', onSuccess);
socket.on('error', onError);
// 超时处理(打印可能需要较长时间)
setTimeout(() => {
if (settled) return;
settled = true;
socket.off('success', onSuccess);
socket.off('error', onError);
reject(new Error('打印超时(60秒)'));
}, 60000);
});
}
// 调用示例:等待第一个打印任务的结果
try {
const result = await waitForPrintResult(socket, tplId1);
console.log('打印完成:', result.msg);
// result 结构: { msg: '打印成功', templateId: 'xxx', replyId: undefined }
} catch (err) {
console.error('打印失败:', err.message);
}第九步:断开连接
/**
* 第九步:断开连接
* @param {Socket} socket - Socket.IO 实例
*/
function disconnect(socket) {
// 移除所有事件监听
socket.removeAllListeners();
// 断开连接
socket.disconnect();
console.log('[第九步] 已断开连接');
}
// 调用示例
disconnect(socket);四、HTTP 完整集成示例
适用于后端服务调用场景,无需维持长连接。
/**
* HTTP 完整集成示例
* 适用于后端服务、脚本等无需长连接的场景
*/
class SvPrintHttpClient {
constructor(baseUrl = 'http://localhost:7071') {
this.baseUrl = baseUrl;
}
/**
* 通用请求方法
*/
async request(path, data = null, method = 'GET') {
const options = { method, headers: {} };
if (data) {
options.method = 'POST';
options.headers['Content-Type'] = 'application/json';
options.body = JSON.stringify(data);
}
const res = await fetch(`${this.baseUrl}${path}`, options);
return res.json();
}
// ============ 第一步:检测服务 ============
async checkService() {
try {
const info = await this.request('/controller/app/baseInfo');
console.log('[HTTP] 服务在线:', info.name, info.version);
return info;
} catch (e) {
throw new Error(`服务不可用: ${e.message}`);
}
}
// ============ 第二步:读取配置 ============
async getConfig() {
const config = await this.request('/controller/app/config');
console.log('[HTTP] 当前配置已读取');
return config;
}
// ============ 第三步:检查配置 ============
checkConfig(config) {
const issues = [];
if (!config.defaultPrinter) {
issues.push({ field: 'defaultPrinter', message: '未设置默认打印机' });
}
if (!config.nickName) {
issues.push({ field: 'nickName', message: '未设置客户端别名' });
}
if (!config.token) {
issues.push({ field: 'token', message: '未设置 Token(生产环境建议设置)' });
}
return { valid: issues.length === 0, issues };
}
// ============ 第四步:更新配置 ============
async updateConfig(patchConfig, refreshConfig) {
const currentConfig = await this.getConfig();
const newConfig = await this.request('/controller/app/updateConfig', {
config: { ...currentConfig, ...patchConfig },
refreshConfig: refreshConfig || { socket: false, cloud: false, mqtt: false, printer: false },
});
console.log('[HTTP] 配置已更新:', Object.keys(patchConfig));
return newConfig;
}
// ============ 第五步:获取打印机列表 ============
async getPrinterList() {
const printers = await this.request('/controller/printer/list');
console.log('[HTTP] 打印机列表:', printers.length, '台');
return printers;
}
// ============ 第六步:设置默认打印机 ============
async setDefaultPrinter(printerName) {
const result = await this.request('/controller/printer/setDefaultPrinter', { printerName });
console.log('[HTTP] 默认打印机:', printerName, result ? '成功' : '失败');
return result;
}
// ============ 第七步:执行打印 ============
async print(printData) {
// 注意:HTTP 调用 print 只会返回 true(表示已入队),
// 无法直接获取打印结果,需要通过日志查询确认
const result = await this.request('/controller/printer/print', printData);
console.log('[HTTP] 打印请求已提交:', result);
return result;
}
// ============ 第八步:查询打印日志 ============
async queryPrintLogs(page = 1, pageSize = 20) {
const logs = await this.request('/controller/log/printLogList', {
page: { currentPage: page, pageSize },
sort: { prop: 'created_time', order: 'desc' },
});
console.log('[HTTP] 打印日志:', logs.total, '条');
return logs;
}
// ============ 第九步:获取已连接客户端 ============
async getClients() {
const clients = await this.request('/controller/printer/clients');
console.log('[HTTP] 已连接客户端:', clients);
return clients;
}
}
// ========== 完整调用流程 ==========
async function main() {
const client = new SvPrintHttpClient('http://localhost:7071');
// 第一步:检测服务
const info = await client.checkService();
// 第二步:读取配置
const config = await client.getConfig();
// 第三步:检查配置
const check = client.checkConfig(config);
if (!check.valid) {
// 第四步:补全缺失配置
const patch = {};
const refresh = { socket: false, cloud: false, mqtt: false, printer: false };
check.issues.forEach((issue) => {
switch (issue.field) {
case 'nickName':
patch.nickName = '后端服务-打印客户端';
break;
case 'token':
patch.token = 'backend-service-token';
refresh.socket = true; // 修改 token 需重启 socket
break;
case 'defaultPrinter':
// 默认打印机在第五、六步处理
break;
}
});
if (Object.keys(patch).length > 0) {
await client.updateConfig(patch, refresh);
// 注意:如果 refresh.socket = true,HTTP 服务端口不变,但 Socket.IO 连接会断开
}
}
// 第五步:获取打印机列表
const printers = await client.getPrinterList();
const available = printers.filter((p) => !p.disabled && p.isOk);
if (available.length === 0) {
throw new Error('没有可用的打印机');
}
// 第六步:设置默认打印机
if (!config.defaultPrinter || !available.find((p) => p.name === config.defaultPrinter)) {
await client.setDefaultPrinter(available[0].name);
}
// 第七步:执行打印
await client.print({
html: '<h1>HTTP 打印测试</h1><p>通过 HTTP API 打印</p>',
templateId: `http-print-${Date.now()}`,
});
// 第八步:查询打印日志(延迟 5 秒等待打印完成)
await new Promise((r) => setTimeout(r, 5000));
const logs = await client.queryPrintLogs(1, 10);
console.log('最近打印日志:', logs);
}
main().catch(console.error);五、关键配置检查清单
| 配置项 | 默认值 | 检查要点 | 修改后是否需要重启服务 |
|---|---|---|---|
port | 17521 | Socket.IO 服务端口,必须 > 10000 | refreshConfig.socket = true |
token | '' | 鉴权 Token,为空则不鉴权 | refreshConfig.socket = true |
enableHttps | false | 是否启用 HTTPS | refreshConfig.socket = true |
enableHttp | true | 是否启用 HTTP 服务 | 无需重启(启动时读取) |
defaultPrinter | '' | 默认打印机名称 | 无需重启 |
disabledPrinterNames | [] | 禁用的打印机列表 | 无需重启 |
nickName | '' | 客户端别名,便于管理 | 无需重启 |
enableMqtt | true | 是否启用 MQTT 服务 | refreshConfig.mqtt = true |
mqttPort | 18521 | MQTT 服务端口 | refreshConfig.mqtt = true |
connectTransit | false | 是否连接中转服务器 | refreshConfig.cloud = true |
transitUrl | '' | 中转服务器地址 | refreshConfig.cloud = true |
transitToken | '' | 中转服务器 Token | refreshConfig.cloud = true |
logDays | 60 | 日志保留天数 | 无需重启 |
logReport | false | 是否启用日志上报 | 无需重启 |
logReportUrl | '' | 日志上报地址 | 无需重启 |
maxPrintViewNum | 3 | 最大打印窗口数 | refreshConfig.printer = true |
tempFilePath | '' | 打印缓存路径 | 无需重启 |
配置检查优先级
- 必须检查(影响连接):
token、port、enableHttps - 建议检查(影响打印):
defaultPrinter、disabledPrinterNames - 可选检查(影响管理):
nickName、logDays、logReport
六、完整封装类(TypeScript)
以下是一个可直接用于生产环境的 TypeScript 封装类,整合了上述所有步骤:
/**
* sv-print 客户端封装
* 提供完整的连接、配置、打印流程管理
*/
import { io, type Socket } from 'socket.io-client';
// 类型定义(简化版,完整定义见获取的配置项)
interface SchemaType {
port: number;
token: string;
enableHttp: boolean;
enableHttps: boolean;
enableMqtt: boolean;
mqttPort: number;
defaultPrinter: string;
disabledPrinterNames: string[];
nickName: string;
logDays: number;
[key: string]: any;
}
interface RefreshConfig {
socket: boolean;
cloud: boolean;
mqtt: boolean;
printer: boolean;
}
interface PrinterInfo {
name: string;
displayName: string;
isDefault: boolean;
status: number;
disabled: boolean;
isOk: boolean;
}
interface PrintResult {
msg: string;
templateId?: string;
replyId?: string;
url?: string;
buffer?: any;
}
interface SvPrintOptions {
/** Socket.IO 服务地址 */
socketUrl?: string;
/** HTTP API 服务地址 */
httpUrl?: string;
/** 连接 Token(服务端配置了 token 时必填) */
token?: string;
/** 连接超时时间(毫秒) */
connectTimeout?: number;
/** 打印超时时间(毫秒) */
printTimeout?: number;
}
class SvPrintClient {
private socket: Socket | null = null;
private config: SchemaType | null = null;
private printers: PrinterInfo[] = [];
private defaultPrinter = '';
private options: Required<SvPrintOptions>;
constructor(options: SvPrintOptions = {}) {
this.options = {
socketUrl: options.socketUrl || 'http://localhost:17521',
httpUrl: options.httpUrl || 'http://localhost:7071',
token: options.token || '',
connectTimeout: options.connectTimeout || 10000,
printTimeout: options.printTimeout || 60000,
};
}
/**
* 第一步:连接服务
*/
async connect(): Promise<void> {
return new Promise((resolve, reject) => {
this.socket = io(this.options.socketUrl, {
auth: { token: this.options.token },
reconnection: true,
reconnectionDelay: 5000,
timeout: this.options.connectTimeout,
});
const timer = setTimeout(() => {
reject(new Error('连接超时'));
}, this.options.connectTimeout);
this.socket.on('connect', () => {
clearTimeout(timer);
console.log('[SvPrint] 连接成功');
resolve();
});
this.socket.on('connect_error', (err: Error) => {
clearTimeout(timer);
reject(new Error(`连接失败: ${err.message}`));
});
this.socket.on('disconnect', (reason: string) => {
console.warn('[SvPrint] 连接断开:', reason);
});
});
}
/**
* 第二步:读取配置
*/
async loadConfig(): Promise<SchemaType> {
if (!this.socket) throw new Error('未连接');
this.config = await new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error('读取配置超时')), 5000);
const handler = (config: SchemaType) => {
clearTimeout(timer);
this.socket!.off('config', handler);
this.config = config;
resolve(config);
};
this.socket!.on('config', handler);
this.socket!.emit('config');
});
return this.config;
}
/**
* 第三步:检查配置
*/
checkConfig(): { valid: boolean; issues: Array<{ field: string; message: string }> } {
if (!this.config) throw new Error('未加载配置');
const issues: Array<{ field: string; message: string }> = [];
if (!this.config.defaultPrinter) {
issues.push({ field: 'defaultPrinter', message: '未设置默认打印机' });
}
if (!this.config.nickName) {
issues.push({ field: 'nickName', message: '未设置客户端别名' });
}
return { valid: issues.length === 0, issues };
}
/**
* 第四步:更新配置
*/
async updateConfig(patch: Partial<SchemaType>, refresh?: Partial<RefreshConfig>): Promise<SchemaType> {
if (!this.socket || !this.config) throw new Error('未连接或未加载配置');
const refreshConfig: RefreshConfig = {
socket: refresh?.socket ?? false,
cloud: refresh?.cloud ?? false,
mqtt: refresh?.mqtt ?? false,
printer: refresh?.printer ?? false,
};
this.config = await new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error('更新配置超时')), 10000);
const handler = (config: SchemaType) => {
clearTimeout(timer);
this.socket!.off('config', handler);
this.config = config;
resolve(config);
};
this.socket!.on('config', handler);
this.socket!.emit('updateConfig', {
config: { ...this.config, ...patch },
refreshConfig,
});
});
// 如果修改了 socket 配置,需要重连
if (refreshConfig.socket) {
console.log('[SvPrint] Socket 配置已变更,正在重连...');
this.socket.disconnect();
await new Promise((r) => setTimeout(r, 2000));
await this.connect();
await this.loadConfig();
}
return this.config;
}
/**
* 第五步:获取打印机列表
*/
async getPrinters(): Promise<PrinterInfo[]> {
if (!this.socket) throw new Error('未连接');
this.printers = await new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error('获取打印机列表超时')), 5000);
const handler = (printers: PrinterInfo[]) => {
clearTimeout(timer);
this.socket!.off('printerList', handler);
this.printers = printers;
resolve(printers);
};
this.socket!.on('printerList', handler);
this.socket!.emit('refreshPrinterList');
});
return this.printers;
}
/**
* 第六步:确保默认打印机已设置
*/
async ensureDefaultPrinter(expectedPrinter?: string): Promise<string> {
const available = this.printers.filter((p) => !p.disabled && p.isOk);
if (available.length === 0) {
throw new Error('没有可用的打印机');
}
// 指定了打印机
if (expectedPrinter) {
const target = available.find((p) => p.name === expectedPrinter);
if (!target) {
throw new Error(`打印机不存在或不可用: ${expectedPrinter}`);
}
if (!target.isDefault) {
await this.setPrinterViaHttp('setDefaultPrinter', { printerName: target.name });
}
this.defaultPrinter = target.name;
return target.name;
}
// 使用已有默认打印机
const current = available.find((p) => p.isDefault);
if (current) {
this.defaultPrinter = current.name;
return current.name;
}
// 自动选择第一个
await this.setPrinterViaHttp('setDefaultPrinter', { printerName: available[0].name });
this.defaultPrinter = available[0].name;
return this.defaultPrinter;
}
/**
* 第七步 + 第八步:执行打印并等待结果
*/
async print(data: any): Promise<PrintResult> {
if (!this.socket) throw new Error('未连接');
const templateId = data.templateId || `print-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
return new Promise((resolve, reject) => {
let settled = false;
const timer = setTimeout(() => {
if (settled) return;
settled = true;
this.socket!.off('success', onSuccess);
this.socket!.off('error', onError);
reject(new Error('打印超时'));
}, this.options.printTimeout);
const onSuccess = (result: PrintResult) => {
if (settled) return;
if (result.templateId !== templateId) return;
settled = true;
clearTimeout(timer);
this.socket!.off('success', onSuccess);
this.socket!.off('error', onError);
resolve(result);
};
const onError = (result: PrintResult) => {
if (settled) return;
if (result.templateId !== templateId) return;
settled = true;
clearTimeout(timer);
this.socket!.off('success', onSuccess);
this.socket!.off('error', onError);
reject(new Error(result.msg || '打印失败'));
};
this.socket!.on('success', onSuccess);
this.socket!.on('error', onError);
// 发送打印请求
this.socket!.emit('print', { ...data, templateId });
});
}
/**
* 第九步:断开连接
*/
disconnect(): void {
if (this.socket) {
this.socket.removeAllListeners();
this.socket.disconnect();
this.socket = null;
console.log('[SvPrint] 已断开连接');
}
}
/**
* 通过 HTTP 接口设置打印机(辅助方法)
*/
private async setPrinterViaHttp(action: string, data: any): Promise<any> {
const res = await fetch(`${this.options.httpUrl}/controller/printer/${action}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
return res.json();
}
/**
* 获取连接状态
*/
isConnected(): boolean {
return this.socket?.connected ?? false;
}
/**
* 获取当前配置
*/
getConfig(): SchemaType | null {
return this.config;
}
}
// ========== 使用示例 ==========
async function example() {
const client = new SvPrintClient({
socketUrl: 'http://localhost:17521',
httpUrl: 'http://localhost:7071',
token: '', // 服务端未配置 token 时传空
});
try {
// 第一步:连接
await client.connect();
// 第二步:读取配置
const config = await client.loadConfig();
console.log('当前配置:', config.nickName || '(未命名)');
// 第三步:检查配置
const check = client.checkConfig();
if (!check.valid) {
// 第四步:补全配置
const patch: Partial<SchemaType> = {};
check.issues.forEach((issue) => {
if (issue.field === 'nickName') patch.nickName = '我的打印客户端';
});
if (Object.keys(patch).length > 0) {
await client.updateConfig(patch);
}
}
// 第五步:获取打印机列表
const printers = await client.getPrinters();
console.log('打印机数量:', printers.length);
// 第六步:确保默认打印机
const defaultPrinter = await client.ensureDefaultPrinter();
// 第七步 + 第八步:打印并等待结果
const result = await client.print({
html: '<h1>打印测试</h1><p>通过 SvPrintClient 打印</p>',
printer: defaultPrinter,
});
console.log('打印结果:', result.msg);
// 第九步:断开
client.disconnect();
} catch (err) {
console.error('错误:', err);
client.disconnect();
}
}
example();七、常见问题处理
7.1 连接被拒绝(Token 错误)
现象:connect_error 事件收到 Token error
原因:服务端配置了 token,但客户端未传入或传入了错误的 token
解决:
// 先通过 HTTP 获取配置,查看 token 是否设置
const config = await fetch('http://localhost:7071/controller/app/config').then((r) => r.json());
if (config.token) {
// 服务端设置了 token,使用相同 token 连接
socket = io('http://localhost:17521', { auth: { token: config.token } });
} else {
// 服务端未设置 token,无需鉴权
socket = io('http://localhost:17521', { auth: { token: '' } });
}7.2 修改 Token 后连接断开
现象:调用 updateConfig 修改 token 后,Socket.IO 连接断开
原因:修改 token 会触发 Socket.IO 服务重启(refreshConfig.socket = true)
解决:
// 修改 token 后等待服务重启,再重新连接
await updateConfig(socket, config, { token: 'new-token' }, { socket: true });
socket.disconnect();
await new Promise((r) => setTimeout(r, 2000)); // 等待 2 秒
const newSocket = await connectServer(url, 'new-token'); // 使用新 token 重连7.3 打印机列表为空
现象:getPrinters() 返回空数组
排查步骤:
// 1. 检查系统是否安装了打印机
// 2. 检查是否所有打印机都被禁用
const allDisabled = printers.every((p) => p.disabled);
if (allDisabled) {
// 启用打印机
await fetch('http://localhost:7071/controller/printer/enablePrinter', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ printerName: printers[0].name }),
});
}
// 3. 检查打印机状态
const notOk = printers.filter((p) => !p.isOk);
if (notOk.length > 0) {
console.warn(
'状态异常的打印机:',
notOk.map((p) => p.name)
);
}7.4 打印无响应
现象:发送 print 事件后,长时间未收到 success 或 error 回调
排查步骤:
- 检查
templateId是否匹配 - 检查打印队列是否堆积
- 通过 HTTP 查询打印日志确认
// 查询最近打印日志
const logs = await fetch('http://localhost:7071/controller/log/printLogList', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
page: { currentPage: 1, pageSize: 5 },
sort: { prop: 'created_time', order: 'desc' },
}),
}).then((r) => r.json());
console.log('最近打印日志:', logs);7.5 HTTP 接口返回 404
现象:HTTP 请求返回 404
原因:路由路径不正确,或 HTTP 服务未开启
解决:
// 1. 检查 HTTP 服务是否开启
const config = await fetch('http://localhost:7071/controller/app/config').then((r) => r.json());
if (!config.enableHttp) {
// 开启 HTTP 服务
await fetch('http://localhost:7071/controller/app/updateConfig', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
config: { ...config, enableHttp: true },
refreshConfig: { socket: false, cloud: false, mqtt: false, printer: false },
}),
});
}
// 2. 确认路由路径格式:/controller/{controllerName}/{methodName}
// 正确: /controller/printer/list
// 错误: /printer/list (虽然也支持,但建议使用完整路径)
// 错误: /api/printer/list (不支持)7.6 跨域问题
现象:浏览器调用 HTTP 接口时报 CORS 错误
说明:服务端已默认开启 CORS 支持(origin: true),允许所有域名访问
解决:如果仍有问题,检查是否通过代理访问
// Socket.IO 连接时的 CORS 配置已在服务端处理
// HTTP 请求的 CORS 也在服务端通过 koa2-cors 中间件处理
// 如果从浏览器调用遇到问题,确保 fetch 请求包含 credentials
fetch(url, { credentials: 'include' });