AI实现超级客户端打印 支持APP 网页 小程序 调用本地客户端打印
本文介绍了跨平台打印解决方案的核心思路和实现方案。主要采用本地客户端作为中间程序,通过WebSocket或自定义URL协议接收打印任务,调用系统接口完成打印。WebSocket方案兼容性好,适合企业级应用;自定义URL协议简单直接,但仅限PC端。文章还提供了数据格式建议和实战示例,包括Electron客户端开发、服务器端实现及交互流程。整个方案支持历史记录管理、任务重打、取消等功能,并包含完整的错
核心思路都是:需要一个安装在用户电脑上的“中间人”程序(本地客户端)来接管打印任务,然后通过某种通信方式命令这个客户端进行打印。
下面我将分平台详细阐述各种实现思路、优缺点和适用场景。
一、核心思路与公共组件:本地客户端
无论哪种方式,都需要一个部署在用户打印电脑上的本地程序。这个程序的核心职责是:
监听来自网络的打印命令。
获取打印数据和参数(如份数、双面打印等)。
调用系统打印接口,完成实际打印。
这个本地客户端通常可以用以下技术开发:
Electron (Node.js, 跨平台)
二、各平台调用方案
这是最主流和推荐的方案WebSocket,适用性最广,尤其是对于浏览器环境。
工作原理:
注册与连接:本地客户端启动后,向一个已知的服务器(或直接在本地)建立一个WebSocket连接或开始HTTP长轮询,并告知服务器“我在这台电脑上,准备好接收打印任务了”。通常需要客户端上报一个唯一标识(如MAC地址、登录用户名等)。
发送打印任务:APP、网页或小程序将打印数据(JSON、HTML、PDF文件流等)和打印机参数通过API发送到业务服务器。
服务器转发:业务服务器根据一定的路由规则(如:用户A的打印任务要发到他指定的电脑B),通过WebSocket或HTTP将任务推送给正在监听的目标客户端。
客户端执行打印:目标本地客户端收到任务后,解析数据,调用本地打印机驱动完成打印。
优点:
跨平台兼容:对APP、网页、小程序一视同仁,它们只与业务服务器交互,无需关心客户端具体实现。
穿透性强:只要能上网,无论APP/网页/小程序在哪里,都能将任务发送到指定地点的打印机。
集中管理:方便在服务端做任务队列、日志记录、权限控制等。
缺点:
依赖网络:必须保证本地客户端和业务服务器的网络连通性。
架构复杂:需要额外开发和维护一个业务服务器作为中转。
适用场景:
企业级应用、ERP、SaaS系统。
需要远程打印或打印任务需要集中管理的场景。
方案二:自定义URL协议 (PC端网页常用)
工作原理:
注册协议:在安装本地客户端时,在系统注册一个自定义URL协议(例如:diygwprint://)。
网页触发:在网页中通过JavaScript代码触发这个链接(如:window.location.href = 'diygwprint://print?data=...')。
客户端响应:系统会唤起注册了该协议的本地客户端,并将URL中的参数传递给它。
客户端处理:客户端解析URL参数(如base64编码的打印数据),执行打印。
优点:
简单直接:对于本地环境,实现起来非常快速。
无中间服务器:无需业务服务器中转,延迟低。
缺点:
仅限PC浏览器:APP和小程序无法直接使用此方式。
数据量限制:URL长度有限制,不适合传输大量数据(如图片、复杂的HTML)。
安全性:需要防范恶意网站随意调用。
体验问题:浏览器通常会弹出“是否允许打开此应用”的提示,体验不完美。
适用场景:
简单的PC端网页调用本地客户端场景,传输的数据量较小。
作为WebSocket方案的补充或备选方案。
四、打印数据格式建议
传递给本地客户端的数据最好结构化且通用:
JSON + 模板:发送JSON数据和模板名称,客户端根据模板渲染后打印。灵活且数据量小。
HTML:直接发送HTML字符串,客户端使用内置浏览器控件(如C#的WebBrowser)打印。开发简单,但样式控制可能不一致。
PDF:服务器端或前端生成PDF文件流/URL,客户端下载并打印。效果最精确,跨平台一致性最好,强烈推荐。
五、实战流程示例 (以最推荐的WebSocket方案为例)
开发本地客户端:
用Electron写一个Windows程序。
集成WebSocket客户端库,连接至业务服务器的WebSocket服务。
实现登录认证、心跳保持、接收打印指令({command: ‘print’, data: {...}, printer: ‘...’})。
接收到指令后,解析数据,调用System.Drawing.Printing命名空间下的类进行打印。
开发业务服务器:
提供WebSocket服务端。
提供RESTful API供APP/网页/小程序提交打印任务。
实现任务路由和转发逻辑。
const { ipcRenderer } = require('electron');
class ElectronHistoryManager {
constructor() {
this.currentTab = 'history';
this.currentPage = 1;
this.pageSize = 20;
this.totalPages = 1;
this.allHistory = [];
this.allQueue = [];
this.filteredData = [];
this.filters = {
status: '',
date: '',
printer: '',
search: ''
};
this.init();
}
async init() {
await this.loadData();
this.setupEventListeners();
this.renderData();
this.updateStats();
}
setupEventListeners() {
// 搜索输入框事件
document.getElementById('searchInput').addEventListener('input', (e) => {
this.filters.search = e.target.value;
this.applyFilters();
});
// 筛选器事件
document.getElementById('statusFilter').addEventListener('change', (e) => {
this.filters.status = e.target.value;
this.applyFilters();
});
document.getElementById('dateFilter').addEventListener('change', (e) => {
this.filters.date = e.target.value;
this.applyFilters();
});
document.getElementById('printerFilter').addEventListener('change', (e) => {
this.filters.printer = e.target.value;
this.applyFilters();
});
// 模态框点击外部关闭
document.getElementById('contentModal').addEventListener('click', (e) => {
if (e.target.id === 'contentModal') {
this.closeModal();
}
});
}
async loadData() {
try {
const result = await ipcRenderer.invoke('get-print-history');
if (result.success) {
this.allHistory = result.history || [];
this.allQueue = result.queue || [];
this.updatePrinterFilter();
} else {
console.error('获取打印历史失败:', result.error);
this.showError('获取打印历史失败: ' + result.error);
}
} catch (error) {
console.error('加载数据失败:', error);
this.showError('加载数据失败: ' + error.message);
}
}
updatePrinterFilter() {
const printerSelect = document.getElementById('printerFilter');
const allData = [...this.allHistory, ...this.allQueue];
const printers = [...new Set(allData.map(job => job.printerName).filter(Boolean))];
// 清空现有选项(保留"全部打印机")
printerSelect.innerHTML = '<option value="">全部打印机</option>';
// 添加打印机选项
printers.forEach(printer => {
const option = document.createElement('option');
option.value = printer;
option.textContent = printer;
printerSelect.appendChild(option);
});
}
updateStats() {
const totalJobs = this.allHistory.length;
const completedJobs = this.allHistory.filter(job => job.status === 'completed').length;
const failedJobs = this.allHistory.filter(job => job.status === 'failed').length;
const queueJobs = this.allQueue.length;
document.getElementById('totalJobs').textContent = totalJobs;
document.getElementById('completedJobs').textContent = completedJobs;
document.getElementById('failedJobs').textContent = failedJobs;
document.getElementById('queueJobs').textContent = queueJobs;
}
switchTab(tab) {
this.currentTab = tab;
this.currentPage = 1;
// 更新标签样式
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
event.target.classList.add('active');
// 显示对应内容
document.getElementById('historyTab').style.display = tab === 'history' ? 'block' : 'none';
document.getElementById('queueTab').style.display = tab === 'queue' ? 'block' : 'none';
this.applyFilters();
}
applyFilters() {
const sourceData = this.currentTab === 'history' ? this.allHistory : this.allQueue;
this.filteredData = sourceData.filter(job => {
// 文本搜索
if (this.filters.search) {
const searchTerm = this.filters.search.toLowerCase();
const searchableText = [
job.id || '',
job.printerName || '',
job.content || '',
job.userId || '',
job.status || ''
].join(' ').toLowerCase();
if (!searchableText.includes(searchTerm)) {
return false;
}
}
// 状态筛选
if (this.filters.status && job.status !== this.filters.status) {
return false;
}
// 日期筛选
if (this.filters.date) {
const jobDate = new Date(job.createdAt).toISOString().split('T')[0];
if (jobDate !== this.filters.date) {
return false;
}
}
// 打印机筛选
if (this.filters.printer && job.printerName !== this.filters.printer) {
return false;
}
return true;
});
this.currentPage = 1;
this.calculatePagination();
this.renderData();
}
calculatePagination() {
this.totalPages = Math.ceil(this.filteredData.length / this.pageSize);
if (this.totalPages === 0) this.totalPages = 1;
}
renderData() {
const loadingState = document.getElementById('loadingState');
const emptyState = document.getElementById('emptyState');
const pagination = document.getElementById('pagination');
// 隐藏加载状态
loadingState.style.display = 'none';
if (this.filteredData.length === 0) {
emptyState.style.display = 'block';
pagination.style.display = 'none';
return;
}
emptyState.style.display = 'none';
// 计算当前页的数据
const startIndex = (this.currentPage - 1) * this.pageSize;
const endIndex = startIndex + this.pageSize;
const pageData = this.filteredData.slice(startIndex, endIndex);
// 渲染表格
const tbody = this.currentTab === 'history' ?
document.getElementById('historyTableBody') :
document.getElementById('queueTableBody');
tbody.innerHTML = '';
pageData.forEach(job => {
const row = this.createDataRow(job);
tbody.appendChild(row);
});
// 更新分页
this.updatePagination();
pagination.style.display = 'flex';
}
createDataRow(job) {
const row = document.createElement('tr');
const formatDate = (dateString) => {
const date = new Date(dateString);
return date.toLocaleString('zh-CN');
};
const getStatusClass = (status) => {
const statusMap = {
'success': 'status-completed',
'completed': 'status-completed',
'error': 'status-failed',
'failed': 'status-failed',
'pending': 'status-pending',
'queued': 'status-pending',
'printing': 'status-printing',
'cancelled': 'status-cancelled'
};
return statusMap[status] || 'status-pending';
};
const getStatusText = (status) => {
const statusMap = {
'success': '已完成',
'completed': '已完成',
'error': '失败',
'failed': '失败',
'pending': '等待中',
'queued': '已加入队列',
'printing': '打印中',
'cancelled': '已取消'
};
return statusMap[status] || status;
};
if (this.currentTab === 'history') {
row.innerHTML = `
<td>${job.id}</td>
<td>${formatDate(job.createdAt)}</td>
<td>${job.printerName || '-'}</td>
<td>
<div class="content-preview" onclick="showContentDetail('${job.id}')" title="点击查看完整内容">
${job.content ? job.content.substring(0, 50) + (job.content.length > 50 ? '...' : '') : '-'}
</div>
</td>
<td>
<span class="status ${getStatusClass(job.status)}">
${getStatusText(job.status)}
</span>
${job.error ? `<br><small style="color: #dc3545;">${job.error}</small>` : ''}
</td>
<td>${job.copies || 1}</td>
<td>${job.userId || '-'}</td>
<td>
<div class="actions">
${(job.status === 'completed' || job.status === 'success' || job.status === 'failed' || job.status === 'error' || job.status === 'cancelled') ?
`<button class="btn btn-success btn-sm" onclick="reprintJob('${job.id}')">重打</button>` : ''}
</div>
</td>
`;
} else {
row.innerHTML = `
<td>${job.id}</td>
<td>${formatDate(job.createdAt)}</td>
<td>${job.printerName || '-'}</td>
<td>
<div class="content-preview" onclick="showContentDetail('${job.id}')" title="点击查看完整内容">
${job.content ? job.content.substring(0, 50) + (job.content.length > 50 ? '...' : '') : '-'}
</div>
</td>
<td>
<span class="status ${getStatusClass(job.status)}">
${getStatusText(job.status)}
</span>
</td>
<td>${job.copies || 1}</td>
<td>${job.retryCount || 0}</td>
<td>
<div class="actions">
${(job.status === 'pending' || job.status === 'queued' || job.status === 'printing') ?
`<button class="btn btn-danger btn-sm" onclick="cancelJob('${job.id}')">取消</button>` : ''}
</div>
</td>
`;
}
return row;
}
updatePagination() {
const pageInfo = document.getElementById('pageInfo');
pageInfo.textContent = `第 ${this.currentPage} 页,共 ${this.totalPages} 页`;
// 更新按钮状态
const prevBtn = document.querySelector('.pagination button:first-child');
const nextBtn = document.querySelector('.pagination button:last-child');
prevBtn.disabled = this.currentPage === 1;
nextBtn.disabled = this.currentPage === this.totalPages;
}
previousPage() {
if (this.currentPage > 1) {
this.currentPage--;
this.renderData();
}
}
nextPage() {
if (this.currentPage < this.totalPages) {
this.currentPage++;
this.renderData();
}
}
showContentDetail(jobId) {
const allData = [...this.allHistory, ...this.allQueue];
const job = allData.find(j => j.id === jobId);
if (job && job.content) {
document.getElementById('contentDetail').textContent = job.content;
document.getElementById('contentModal').style.display = 'block';
}
}
closeModal() {
document.getElementById('contentModal').style.display = 'none';
}
async reprintJob(jobId) {
const job = this.allHistory.find(j => j.id === jobId);
if (!job) {
this.showError('找不到指定的打印任务');
return;
}
if (confirm(`确定要重新打印任务 ${jobId} 吗?`)) {
try {
const result = await ipcRenderer.invoke('reprint-job', {
content: job.content,
printerName: job.printerName,
copies: job.copies,
userId: job.userId,
clientId: job.clientId
});
if (result.success) {
this.showSuccess('重打任务已提交');
await this.refreshData();
} else {
this.showError('重打任务提交失败: ' + result.error);
}
} catch (error) {
console.error('重打任务失败:', error);
this.showError('重打任务失败: ' + error.message);
}
}
}
async cancelJob(jobId) {
if (confirm(`确定要取消打印任务 ${jobId} 吗?`)) {
try {
const result = await ipcRenderer.invoke('cancel-job', jobId);
if (result.success) {
this.showSuccess('打印任务已取消');
await this.refreshData();
} else {
this.showError('取消打印任务失败: ' + result.error);
}
} catch (error) {
console.error('取消打印任务失败:', error);
this.showError('取消打印任务失败: ' + error.message);
}
}
}
async clearHistory() {
if (confirm('确定要清除所有历史记录吗?此操作不可恢复。')) {
try {
const result = await ipcRenderer.invoke('clear-history');
if (result.success) {
this.showSuccess('历史记录已清除');
await this.refreshData();
} else {
this.showError('清除历史记录失败: ' + result.error);
}
} catch (error) {
console.error('清除历史记录失败:', error);
this.showError('清除历史记录失败: ' + error.message);
}
}
}
clearFilters() {
this.filters = {
status: '',
date: '',
printer: '',
search: ''
};
document.getElementById('searchInput').value = '';
document.getElementById('statusFilter').value = '';
document.getElementById('dateFilter').value = '';
document.getElementById('printerFilter').value = '';
this.applyFilters();
}
async refreshData() {
document.getElementById('loadingState').style.display = 'block';
document.getElementById('historyTab').style.display = 'none';
document.getElementById('queueTab').style.display = 'none';
document.getElementById('emptyState').style.display = 'none';
await this.loadData();
this.applyFilters();
this.updateStats();
// 恢复标签显示
if (this.currentTab === 'history') {
document.getElementById('historyTab').style.display = 'block';
} else {
document.getElementById('queueTab').style.display = 'block';
}
}
showSuccess(message) {
// 简单的成功提示
const toast = document.createElement('div');
toast.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #28a745;
color: white;
padding: 15px 20px;
border-radius: 4px;
z-index: 10000;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
document.body.removeChild(toast);
}, 3000);
}
showError(message) {
// 简单的错误提示
const toast = document.createElement('div');
toast.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #dc3545;
color: white;
padding: 15px 20px;
border-radius: 4px;
z-index: 10000;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
document.body.removeChild(toast);
}, 5000);
}
}
// 全局函数
function switchTab(tab) {
if (window.historyManager) {
window.historyManager.switchTab(tab);
}
}
function applyFilters() {
if (window.historyManager) {
window.historyManager.applyFilters();
}
}
function clearFilters() {
if (window.historyManager) {
window.historyManager.clearFilters();
}
}
function refreshData() {
if (window.historyManager) {
window.historyManager.refreshData();
}
}
function clearHistory() {
if (window.historyManager) {
window.historyManager.clearHistory();
}
}
function previousPage() {
if (window.historyManager) {
window.historyManager.previousPage();
}
}
function nextPage() {
if (window.historyManager) {
window.historyManager.nextPage();
}
}
function reprintJob(jobId) {
if (window.historyManager) {
window.historyManager.reprintJob(jobId);
}
}
function cancelJob(jobId) {
if (window.historyManager) {
window.historyManager.cancelJob(jobId);
}
}
function showContentDetail(jobId) {
if (window.historyManager) {
window.historyManager.showContentDetail(jobId);
}
}
function closeModal() {
if (window.historyManager) {
window.historyManager.closeModal();
}
}
// 标题栏控制功能
let isMaximized = false;
function minimizeWindow() {
ipcRenderer.send('history-window-minimize');
}
function toggleMaximize() {
ipcRenderer.send('history-window-toggle-maximize');
}
function closeWindow() {
ipcRenderer.send('history-window-close');
}
// 监听窗口状态变化
ipcRenderer.on('window-maximized', () => {
isMaximized = true;
updateTitlebarDrag();
});
ipcRenderer.on('window-unmaximized', () => {
isMaximized = false;
updateTitlebarDrag();
});
// 更新标题栏拖动状态
function updateTitlebarDrag() {
const titlebar = document.querySelector('.custom-titlebar');
if (titlebar) {
titlebar.style.webkitAppRegion = isMaximized ? 'no-drag' : 'drag';
}
}
// 创建全局实例
document.addEventListener('DOMContentLoaded', () => {
window.historyManager = new ElectronHistoryManager();
// 设置标题栏双击事件
const titlebar = document.querySelector('.custom-titlebar');
if (titlebar) {
titlebar.addEventListener('dblclick', (e) => {
// 排除控制按钮区域
if (!e.target.closest('.titlebar-controls')) {
toggleMaximize();
}
});
}
});
更多推荐
所有评论(0)