核心思路都是:需要一个安装在用户电脑上的“中间人”程序(本地客户端)来接管打印任务,然后通过某种通信方式命令这个客户端进行打印。

下面我将分平台详细阐述各种实现思路、优缺点和适用场景。

一、核心思路与公共组件:本地客户端

无论哪种方式,都需要一个部署在用户打印电脑上的本地程序。这个程序的核心职责是:

监听来自网络的打印命令。

获取打印数据和参数(如份数、双面打印等)。

调用系统打印接口,完成实际打印。

这个本地客户端通常可以用以下技术开发:

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();
            }
        });
    }
});

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐