[vue-plugin-hiprint] 打印总结

5,617 阅读2分钟

工厂 saas 项目,出货需要打印「发货单」、「质保卡」等

核心需求:自动打印、选择打印机、自定义打印模板设计

方案考虑

  1. window.print()
    • 不能获取打印机列表,无法自动打印
  2. Lodop打印控件
    • 授权费用高
  3. vue-plugin-hiprint
    • 相比上面方案,自带模板设计,并且提供打印客户端,基本满足需求
    • 缺点:hiprint 是闭源的

最后选择使用 vue-plugin-hiprint 开发初版。

功能点实现

隐藏不需要的参数

// 配置参数
hiprint.setConfig({
    text: {
        tabs: [], // 隐藏tabs分组
        supportOptions: [
            {
                name: 'title',
                hidden: true,
            },
            {
                name: 'fontSize',
                hidden: false,
            },
        ],
    },
});

查看所有参数:window.HIPRINT_CONFIG

注意:v0.0.56 无法隐藏参数的 tabs 分组,切到 v0.0.50

也可通过 css 去隐藏,

插入背景图片

背景图片只起参照作用,不参与打印

const insertBg = () => {
    if (!state.currentTemplate.bgUrl) return
    const bgImg = document.querySelector('#bg-img')
    if (bgImg) {
        bgImg.style.opacity = state.currentTemplate.bgAph / 100
        bgImg.src = state.currentTemplate.bgUrl
    } else {
        const img = new Image()
        img.id = 'bg-img'
        img.style.display = 'block'
        img.style.width = '100%'
        img.style.height = '100%'
        img.style.position = 'relative'
        img.style.zIndex = '-1'
        img.style['-webkit-user-drag'] = 'none'
        img.style.opacity = state.currentTemplate.bgAph / 100
        img.src = state.currentTemplate.bgUrl
        img.onload = () => {
            const printPaper = document.querySelector('.hiprint-printPaper')
            printPaper.append(img)
        }
    }
}

打印预览

<!-- 打印预览弹窗 -->
<el-dialog class="preview-dialog" v-model="previewVisible" title="打印预览" top="10vh" width="1600">
    <div class="preview-wrapper">
        <div class="preview"></div>
    </div>

    <template #footer>
        <span class="dialog-footer">
            <el-button @click="previewVisible = false">关闭</el-button>
        </span>
    </template>
</el-dialog>

// 打印预览
const previewVisible = ref(false)
const printPreview = () => {
    previewVisible.value = true
    const html = hiprintTemplate.getHtml(printData)
    // console.log('html: ', html)
    do {
        setTimeout(() => {
            $('.preview').empty()
            $('.preview').html(html)
        }, 200)
        return
    } while ($('.preview').length <= 0)
}

/* 不同模板 间隙 */
.preview .hiprint-printTemplate {
    background: #fff;
    border-bottom: 10px solid #ccc;
}

/* 批量打印 间隙 */
.preview .hiprint-printTemplate .hiprint-printPanel:not(:last-of-type) {
    border-bottom: 5px solid #ccc;
}

/* 分页纸张 间隙 */
.preview .hiprint-printPaper {
    border-bottom: 10px solid #ccc;
}

设置纸张大小和缩放

// 设置纸张大小
const handleChangePageSize = () => {
    if (hiprintTemplate) {
        hiprintTemplate.setPaper(state.currentTemplate.pageWidth, state.currentTemplate.pageHeight)
    }
}

// 设置缩放
const handleChangeScale = () => {
    hiprintTemplate.zoom(state.currentTemplate.scale / 100, false)
}

插入分页符

context.addPrintElementTypes('defaultModule', [
    new hiprint.PrintElementTypeGroup('辅助', [
        {
            title: '分页符',
            tid: 'defaultModule.pageBreak',
            type: 'hline',
            options: {
                width: 200,
                height: 10,
                borderStyle: 'dotted',
                borderColor: '#ff0000',
                showInPage: 'none', // 打印隐藏
                pageBreak: true, // 强制分页
            },
        },
    ]),
]);

// 针对始终隐藏的元素(分页符)
.alwaysHide {
    background-color: unset !important;
}

横向打印

hiprintTemplate.print2(printData, {
    printer: '',
    title: '测试任务',
    landscape: true, // 横向打印
    pageSize: { // 纸张大小(必须)
        width: state.currentTemplate.pageWidth * 1000,
        height: state.currentTemplate.pageHeight * 1000,
    },
});

注意:

  • windows 系统可以直接修改打印机设置,而 mac 系统不行
  • 打印机驱动会影响打印效果,必须安装原生驱动!

获取打印机列表

const getPrinterList = async () => {
    setInterval(() => {
        if (hiprint.hiwebSocket.opened) {
            state.printerList = hiprint.hiwebSocket?.getPrinterList() || [];
            console.log('打印机列表: ', state.printerList);
        } else {
            // 重新连接客户端
            hiprint.hiwebSocket.setHost("http://localhost:17521")
        }
    }, 1000);
}

扩展:可以通过本地存储记住上次选择的打印机

批量自动打印

// 伪代码
const batchPrint = () => {
    selectRows.forEach((row: any) => {
        getOrderPrintData({ orderId: row.orderId }).then((res) => {
            if (res.success) {
                const printData = res.returnValue;
                console.log('printData', printData);

                const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

                const printQueue = async () => {
                    for (const item of printItemListFilter) {
                        // 每次打印延迟5s,防止过快导致打印机接收不到
                        // await delay(5000)

                        let hiprintTemplate = new hiprint.PrintTemplate({
                            template: JSON.parse(item.templateContent),
                        });

                        hiprintTemplate.print2(printData, { printer: item.printerName, title: '订单出货打印' });

                        // 更新打印状态
                        const updateStatus = async (isSuccess: boolean) => {

                        };

                        // 发送任务到打印机成功
                        hiprintTemplate.on('printSuccess', async function (e) {
                            await updateStatus(true);
                        });
                        // 发送任务到打印机失败
                        hiprintTemplate.on('printError', async function (e) {
                            await updateStatus(false);
                        });
                    }
                };

                printQueue();
            }
        });
    });
};

注意:mac 系统接收批量打印任务时必须要延迟,否则会丢包,而 winodws 系统不需要。当然为了保险和防止打印机过载工作,还是可以加上。

利用表格打印列表数据

// 注意点:隐藏边框、单元格合并、无数据处理、函数转义
{
    tid: `customModule.productList`,
    title: '产品列表',
    type: 'table',
    options: {
        field: 'productList',
        fields: [
            { text: '产品名称', field: 'name' },
            { text: '左上', field: 'upLeft' }
            { text: '右上', field: 'upRight' }
            { text: '左下', field: 'downLeft' }
            { text: '右下', field: 'downRight' }
        ],
        
        // 隐藏边框
        tableHeaderRepeat: 'none',
        tableBorder: 'noBorder',
        tableBodyRowBorder: 'noBorder',
        tableBodyCellBorder: 'noBorder',

        tableBodyRowHeight: 40, // 行高
        width: 700, // 总宽
    },
    columns: [
        [
            {
                title: '产品名称',
                field: `name`,
                rowspan: 2,
                colspan: 1,
                align: 'center',
            },
            {
                title: '左上',
                field: `upLeft`,
                rowspan: 1,
                colspan: 1,
                align: 'center',
            },
            {
                title: '右上',
                field: `upRight`,
                rowspan: 1,
                colspan: 1,
                align: 'center',
            },
        ],
        [
            {
                title: '左下',
                field: `downLeft`,
                rowspan: 1,
                colspan: 1,
                align: 'center',
                // 函数都需要转义,防止保存时被 JSON.stringify() 过滤掉
                renderFormatter: function (value, row, colIndex, options, rowIndex) {
                    if (!row.name) return ''
                    return `
                        <div>
                            <div style="padding: 4px;border-right: 1px solid;text-align: right;border-bottom: 1px solid;">${
                                row.upLeft || '&nbsp;'
                            }</div>
                            <div style="padding: 4px;border-right: 1px solid;text-align: right;">${
                                row.downLeft || '&nbsp;'
                            }</div>
                        </div>
                    `
                }.toString(),
                styler2: function (value, row, index, options) {
                    return { padding: 0 }
                }.toString(),
            },
            {
                title: '右下',
                field: `downRight`,
                rowspan: 1,
                colspan: 1,
                align: 'center',
                renderFormatter: function (value, row, colIndex, options, rowIndex) {
                    if (!row.name) return ''
                    return `
                        <div>
                            <div style="padding: 4px;text-align: left;border-bottom: 1px solid;">${
                                row.upRight || '&nbsp;'
                            }</div>
                            <div style="padding: 4px;text-align: left;">${
                                row.downRight || '&nbsp;'
                            }</div>
                        </div>
                    `
                }.toString(),
                styler2: function (value, row, index, options) {
                    return { padding: 0 }
                }.toString(),
            },
        ],
    ],
}

局限:必须拖拽整个表格,无法将列单独拆开

自动根据文本长度来设置字号大小

    if (item.name === '质保卡') {
        template.panels[0].printElements.forEach((item) => {
            // 根据产品名称的长度(字符数)来设置字号大小,名称越长,字号越小
            if (item.options.field === 'order$productNames') {
                const valueLength = printData['order$productNames'].length;

                const FONT_SIZE_1 = 8.25;
                const FONT_SIZE_2 = 7.5;
                const FONT_SIZE_3 = 6.75;
                const FONT_SIZE_4 = 6;

                let fontSize = FONT_SIZE_1;
                if (valueLength > 40) {
                    fontSize = FONT_SIZE_4;
                    // 超过40字,截断产品名称并显示省略号
                    printData['order$productNames'] = printData['order$productNames'].slice(0, 40) + '...';
                } else if (valueLength > 24) {
                    fontSize = FONT_SIZE_4;
                } else if (valueLength > 16) {
                    fontSize = FONT_SIZE_3;
                } else if (valueLength > 8) {
                    fontSize = FONT_SIZE_2;
                } else {
                    fontSize = FONT_SIZE_1;
                }

                item.options.fontSize = fontSize;
            }
        });
    }

列表数据打印处理

思路:就是获取产品列表的长度,然后根据模板合理的拆分打印,比如一个订单包含8个产品,分成3次打印(假设模板最多适合打印3个产品),拆分打印数据为三份,然后执行三次打印。

    for (const item of printItemListFilter) {
        if (item.name === '发货单') {
            if (printData['orderItemList'].length > 3) {
                // 拆分打印,每次最多只打印3个产品
                const orderItemList = printData['orderItemList'];
                const orderItemListLength = orderItemList.length;
                for (let i = 0; i < orderItemListLength; i += 3) {
                    // handle是异步的打印方法,需要传入打印项和打印数据
                    await handle(item, {
                        ...printData,
                        orderItemList: orderItemList.slice(i, i + 3),
                    });
                    console.log('发货单', printData['order$orderNo'], orderItemList.slice(i, i + 3));
                }
                return;
            }
        }

        handle(item, printData);
    }

关于打印客户端

mac 系统必须使用 x64 的安装包,否则打不开客户端

如果未连接到打印客户端并调用直接打印,会弹出 alert ,建议在触发打印的功能按钮上根据 hiprint.hiwebSocket.opened 做禁用和悬浮提示

可以将官网的安装包放到服务器上,方便用户直接下载

打印机驱动和设置

必须安装打印机的原生驱动(官网或驱动天空下载),否则修改打印机设置可能失效!因为不同驱动,打印机设置不同,有的无法控制打印机。打印机使用驱动可以在打印机属性中切换。

换驱动: 换驱动.jpg

hiprint依赖打印机设置,比如:横向打印、纸张大小等,只用代码是无法控制的!

坑1:纸张大小只通过打印机设置调整,pageSize无用,设置了pageSize反而可能打印不准

坑2:横向打印不仅要代码修改,打印机设置也得改成横向才会生效

无法兼容mac的原因

1. mac在批量打印时会丢包,导致打印失败

2. mac不能修改打印机设置

最后

参考学习

欢迎交流沟通。

更新记录:

2024/9/1:更新章节【打印机驱动和设置】、【无法兼容mac的原因】、【自动根据文本长度来设置字号大小】、【列表数据打印处理】