Lodop经验汇总

2022-02-03 fishedee 前端

0 概述

lodop经验汇总,lodop的官网是这里,lodop以WebSocket的方式,控制本地的打印机工作的工具,比浏览器自带的window.print要强多了。

我们主要使用clodop的工具。

打印工具需要的功能有:

  • 打印模板和模板设计
  • 打印的时候选择打印机
  • 对于表格能支持自动分页
  • 支持打印页头,页尾,二维码,图片等元素。

代码在这里

1 依赖

1.1 安装

Lodop的安装比较简单,引导用户下载本地的exe文件,然后启动js文件连接本地的WebSocket即可

Lodop的下载地址在这里

1.2 js文件

class LodopError extends Error {
    public constructor(msg: string) {
        super(msg);
    }
}
class LodopLoadingError extends LodopError {
    public constructor(msg: string) {
        super(msg);
    }
}

class LodopNotInstallError extends LodopError {
    public constructor(msg: string) {
        super(msg);
    }
}

export {
    LodopError,
    LodopLoadingError,
    LodopNotInstallError,
}

先定义加载失败的异常

import { LodopLoadingError, LodopNotInstallError } from "./lodopError";

var CLodopIsLocal: boolean, CLodopJsState: string;
//加载CLodop时用双端口(http是8000/18000,而https是8443/8444)以防其中某端口被占,
//主JS文件“CLodopfuncs.js”是固定文件名,其内容是动态的,与当前打印环境有关:

type LodopPixelType = string | number;

type LodopBarCodeType = '128Auto' | 'QRCode';

type LodopPageOrient = 0 | 1 | 2 | 3;

const LODOP_PREVIEW_OPTION_HIDDEN_NORAML = 1;

const LODOP_PREVIEW_OPTION_HIDDEN_BIGGER = 2;

const LODOP_PREVIEW_OPTION_HIDDEN_SMALLER = 4;

const LODOP_PREVIEW_OPTION_HIDDEN_PRINT = 8;

const LODOP_PREVIEW_OPTION_HIDDEN_STATUS = 16;

export type LodopType = {
    PRINT_INITA: (
        Top: LodopPixelType,
        Left: LodopPixelType,
        Width: LodopPixelType,
        Height: LodopPixelType,
        strPrintTaskName: string,
    ) => void;
    ADD_PRINT_HTM: (
        Top: LodopPixelType,
        Left: LodopPixelType,
        Width: LodopPixelType,
        Height: LodopPixelType,
        strHtmlContent: string,
    ) => void;
    ADD_PRINT_TEXT: (
        Top: LodopPixelType,
        Left: LodopPixelType,
        Width: LodopPixelType,
        Height: LodopPixelType,
        strContent: string,
    ) => void;
    ADD_PRINT_TABLE: (
        Top: LodopPixelType,
        Left: LodopPixelType,
        Width: LodopPixelType,
        Height: LodopPixelType,
        Content: string,
    ) => void;
    ADD_PRINT_BARCODE: (
        Top: LodopPixelType,
        Left: LodopPixelType,
        Width: LodopPixelType,
        Height: LodopPixelType,
        Type: LodopBarCodeType,
        Content: string,
    ) => void;
    SET_PRINT_PAGESIZE: (
        Orient: LodopPageOrient,
        Width: LodopPixelType,
        Height: LodopPixelType,
        Name: string,
    ) => void;
    On_Return: (taskId: any, newValue: any) => void | undefined;
    SET_SHOW_MODE: (type: 'HIDE_PBUTTIN_PREVIEW', value: string | number) => void;
    SET_PRINT_MODE: (key: string, value: any) => void;
    SET_PRINT_STYLEA: (targetId: number, key: string, value: any) => void;
    PREVIEW: (oView?: '_dialog' | '_blank' | '' | string, iWidth?: number, iHeight?: number, iOption?: number) => void;
    //直接打印
    PRINT: () => void;
    //选择打印机后打印
    PRINTA: () => void;
    PRINT_SETUP: () => string;
    PRINT_DESIGN: () => string; //返回代码
    NewPage: () => void;
};

function loadCLodop() {
    if (CLodopJsState == 'loading' || CLodopJsState == 'complete') return;
    CLodopJsState = 'loading';
    var head =
        document.head ||
        document.getElementsByTagName('head')[0] ||
        document.documentElement;
    var JS1 = document.createElement('script');
    var JS2 = document.createElement('script');

    if (window.location.protocol == 'https:') {
        JS1.src = 'https://localhost.lodop.net:8443/CLodopfuncs.js';
        JS2.src = 'https://localhost.lodop.net:8444/CLodopfuncs.js';
    } else {
        JS1.src = 'http://localhost:8000/CLodopfuncs.js';
        JS2.src = 'http://localhost:18000/CLodopfuncs.js';
    }
    JS1.onload = JS2.onload = function () {
        CLodopJsState = 'complete';
    };
    JS1.onerror = JS2.onerror = function (evt) {
        CLodopJsState = 'complete';
    };
    head.insertBefore(JS1, head.firstChild);
    head.insertBefore(JS2, head.firstChild);
    CLodopIsLocal = !!(JS1.src + JS2.src).match(/\/\/localho|\/\/127.0.0./i);
}

//开始加载
loadCLodop();

//==获取LODOP对象主过程,判断是否安装、需否升级:==
function getLodop(): LodopType {
    var strCLodopInstall_1 =
        "<br><font color='#FF00FF'>Web打印服务CLodop未安装启动,点击这里<a href='CLodop_Setup_for_Win32NT.zip' target='_self'>下载执行安装</a>";
    var strCLodopInstall_2 =
        "<br>(若此前已安装过,可<a href='CLodop.protocol:setup' target='_self'>点这里直接再次启动</a>)";
    var strCLodopInstall_3 = ',成功后请刷新或重启浏览器。</font>';
    var LODOP: any;
    try {
        const myGlobal = window as any;
        LODOP = myGlobal.getCLodop();
    } catch (err) { }
    if (!LODOP && CLodopJsState !== 'complete') {
        if (CLodopJsState == 'loading') {
            throw new LodopLoadingError('网页还没下载完毕,请稍等一下再操作.',);
        } else {
            throw new LodopLoadingError('没有加载CLodop的主js,请先调用loadCLodop过程.');
        }
    }
    if (!LODOP) {
        const message =
            strCLodopInstall_1 +
            (CLodopIsLocal ? strCLodopInstall_2 : '') +
            strCLodopInstall_3;
        throw new LodopNotInstallError(message);
    }
    //===如下空白位置适合调用统一功能(如注册语句、语言选择等):==
    LODOP.SET_LICENSES(
        '',
        '13528A153BAEE3A0254B9507DCDE2839',
        'EDE92F75B6A3D917F65910',
        'D60BC84D7CF2DE18156A6F88987304CB6D8',
    );
    let result = LODOP as LodopType;
    return result;
}

export default getLodop;

export {
    LODOP_PREVIEW_OPTION_HIDDEN_NORAML,
    LODOP_PREVIEW_OPTION_HIDDEN_BIGGER,
    LODOP_PREVIEW_OPTION_HIDDEN_SMALLER,
    LODOP_PREVIEW_OPTION_HIDDEN_PRINT,
    LODOP_PREVIEW_OPTION_HIDDEN_STATUS,
}

LODOP文件,页面启动的时候,就会尝试加载本地8000端口,或者18000端口的js文件。

import getLodop from './lodop';
import { Modal } from 'antd';
import React from 'react';
import { LodopError, LodopLoadingError, LodopNotInstallError } from './lodopError';

const getAntdLodop = () => {
    try {
        let result = getLodop();
        return result;
    } catch (e) {
        if (e instanceof LodopLoadingError) {
            Modal.warning({
                content: '正在加载打印组件中,请稍候重试',
            });
        } else if (e instanceof LodopNotInstallError) {
            const showElement = (
                <div>
                    <p>
                        {'Web打印服务CLodop未安装启动,点击这里'}
                        <a href="/clodop-4.145.zip" target="_blank">
                            {'下载执行安装'}
                        </a>
                    </p>
                    <p>
                        {'若此前已安装过,可'}
                        <a href="CLodop.protocol:setup" target="_self">
                            {'点这里直接再次启动'}
                        </a>
                    </p>
                </div>
            );
            Modal.error({
                content: showElement,
            });
        }
        throw e;
    }
};

export default getAntdLodop;

对getLodop进行包装,接收到异常的时候,弹出对应错误的对话框

2 基础

import styles from './index.less';
import getAntdLodop from '@/util/lodopAntd';
import { LODOP_PREVIEW_OPTION_HIDDEN_PRINT, LODOP_PREVIEW_OPTION_HIDDEN_STATUS } from '@/util/lodop';
export default function IndexPage() {

    function CreateOneFormPage(LODOP: any) {
        LODOP.SET_PRINT_PAGESIZE(1, "210mm", "139.49mm", '打印控件功能演示_Lodop功能_表单一');
        LODOP.SET_PRINT_STYLE('FontSize', 18);
        LODOP.SET_PRINT_STYLE('Bold', 1);
        LODOP.ADD_PRINT_TEXT(50, 231, 260, 39, '打印页面部分内容');
        LODOP.ADD_PRINT_HTM(
            88,
            200,
            350,
            600,
            document.getElementById('form1')!.innerHTML,
        );
        LODOP.NewPage();
        LODOP.ADD_PRINT_HTM(
            88,
            200,
            350,
            600,
            document.getElementById('form1')!.innerHTML,
        );
    }

    const prn1_preview = () => {
        try {
            let LODOP = getAntdLodop();
            CreateOneFormPage(LODOP);
            LODOP.PREVIEW();
        } catch (e) {
            console.log(e);
        }
    }
    const prn1_preview2 = () => {
        try {
            let LODOP = getAntdLodop();
            CreateOneFormPage(LODOP);
            LODOP.SET_SHOW_MODE('HIDE_PBUTTIN_PREVIEW', 1);
            LODOP.PREVIEW();
        } catch (e) {
            console.log(e);
        }
    }
    const prn1_preview_dialog = () => {
        try {
            let LODOP = getAntdLodop();
            CreateOneFormPage(LODOP);
            LODOP.PREVIEW('_dialog', 0, 0, LODOP_PREVIEW_OPTION_HIDDEN_PRINT + LODOP_PREVIEW_OPTION_HIDDEN_STATUS);
        } catch (e) {
            console.log(e);
        }
    }
    const prn1_preview_blank = () => {
        try {
            let LODOP = getAntdLodop();
            CreateOneFormPage(LODOP);
            LODOP.PREVIEW('_blank', 0, 0, 0);
        } catch (e) {
            console.log(e);
        }
    }
    const prn1_preview_iframe = () => {
        try {
            let LODOP = getAntdLodop();
            CreateOneFormPage(LODOP);
            LODOP.PREVIEW('kk', 0, 0, 8);
        } catch (e) {
            console.log(e);
        }
    }
    const prn1_print = () => {
        try {
            let LODOP = getAntdLodop();
            CreateOneFormPage(LODOP);
            LODOP.PRINT();
        } catch (e) {
            console.log(e);
        }
    }
    const prn1_printA = () => {
        try {
            let LODOP = getAntdLodop();
            CreateOneFormPage(LODOP);
            LODOP.PRINTA();
        } catch (e) {
            console.log(e);
        }
    }
    return (
        <div>
            <h2 style={{ color: "#009999" }}>演示如何打印当前页面的内容:
            </h2>
            <form id="form1">
                <table width="300" id="tb01" style={{ border: 'solid 1px black', borderCollapse: 'collapse', backgroundColor: "#CCFFCC" }}><tr><td width="133" id="mtb001" style={{ fontFamily: "黑体", color: "#FF0000", fontSize: "3" }}>
                    <u> 《表单一》 </u></td></tr></table>
                <table style={{ width: "300px", height: "106px", borderCollapse: 'collapse', tableLayout: 'fixed', border: ' 1px solid black' }}><tr>
                    <td width="66" height="16" style={{ border: 'solid 1px black' }}><span style={{ color: "#0000FF" }}>A</span><span style={{ color: "#0000FF" }}>等</span></td>
                    <td width="51" height="16" style={{ border: 'solid 1px black' }}><span style={{ color: "#0000FF" }}>B</span><span style={{ color: "#0000FF" }}>等</span></td>
                    <td width="51" height="16" style={{ border: 'solid 1px black' }}><span style={{ color: "#0000FF" }}>C</span><span style={{ color: "#0000FF" }}>等</span></td></tr>
                    <tr>
                        <td width="66" height="16" style={{ border: 'solid 1px black' }}>A<sub>01</sub></td>
                        <td width="80" height="12" style={{ border: 'solid 1px black' }}>中-001</td>
                        <td width="51" height="12" style={{ border: 'solid 1px black' }}>C1<sup>x</sup></td>
                    </tr>
                    <tr>
                        <td width="66" height="16" style={{ border: 'solid 1px black' }}>A<sub>02</sub>Φ</td>
                        <td width="80" height="16" style={{ border: 'solid 1px black' }}>日-スの</td>
                        <td width="51" height="16" style={{ border: 'solid 1px black', fontFamily: 'Vernada' }}>7㎥</td>
                    </tr>
                    <tr><td width="66" height="16" style={{ border: 'solid 1px black', overflow: 'hidden' }}>A<sub>03</sub>over隐藏后面的:1234567890
                    </td><td width="80" height="16" style={{ border: 'solid 1px black', overflow: 'hidden' }}>韩-안녕</td><td width="51" height="16">C3<sup>x</sup>
                        </td></tr> </table>
            </form>
            <p>1:若只打印《表单一》,看一下<a onClick={prn1_preview}>打印预览</a>,可<a onClick={prn1_print}>直接打印</a>也可
                <a onClick={prn1_printA}>选择打印机</a>打印。<br /><br /></p>
            <p>
                <a onClick={prn1_preview2}>预览,无打印</a><br />
                <a onClick={prn1_preview_dialog}>预览在_dialog</a><br />
                <a onClick={prn1_preview_blank}>预览在_blank,别用,它会在当前页面中加入元素来展示</a><br />
                <a onClick={prn1_preview_iframe}>预览在_iframe</a><br />
            </p>
            <iframe id="kk" width={'100%'} height={'800px'}></iframe>
        </div>
    );
}

打印的步骤也比较简单:

  • 先用PRINT_INIT初始化页面
  • 用ADD_PRINT_TEXT,或者ADD_PRINT_HTM添加页面的元素
  • 使用SET_PRINT_STYLEA来设置页面样式

打印的多种方式:

  • PREVIEW,预览模式,先启动预览打印,再引导选择打印机打印。注意PREVIEW的几个选项的不同意思
  • PRINT,静默打印,直接用默认的打印机启动打印,没有预览,也没有选择打印机
  • PRINTA,无预览打印,没有预览,但是有选择打印机打印的对话框

3 二维码

import { LodopType } from '@/util/lodop';
import getAntdLodop from '@/util/lodopAntd';
import { Modal } from 'antd';
export default function IndexPage() {
    const printTemplate = (LODOP: LodopType, input: any) => {
        LODOP.PRINT_INITA(
            0,
            0,
            '50.01mm',
            '100.01mm',
            '打印控件功能演示_Lodop功能_表单一',
        );
        LODOP.SET_PRINT_PAGESIZE(0, 500, 1000, '条形码');
        //注意不要少了这一句
        LODOP.SET_PRINT_MODE('PROGRAM_CONTENT_BYVAR', true);
        LODOP.SET_PRINT_MODE('PRINT_NOCOLLATE', 1);
        LODOP.ADD_PRINT_BARCODE(
            10,
            16,
            '55.59mm',
            '37.99mm',
            'QRCode',
            input.code,
        );
        LODOP.SET_PRINT_STYLEA(0, 'QRCodeErrorLevel', 'H');
        //这一句是为了让lodop设计器,生成代码的时候自动替换输出代码
        LODOP.SET_PRINT_STYLEA(0, 'ContentVName', 'input.code');
        LODOP.ADD_PRINT_HTM(174, 10, 149, 191, input.desc);
        LODOP.SET_PRINT_STYLEA(0, 'ContentVName', 'input.desc');
        LODOP.ADD_PRINT_TEXT(145, 10, 164, 20, input.title);
        LODOP.SET_PRINT_STYLEA(0, 'Alignment', 2);
        LODOP.SET_PRINT_STYLEA(0, 'Horient', 2);
        LODOP.SET_PRINT_STYLEA(0, 'ContentVName', 'input.title');
    };
    const onClick = () => {
        try {
            const LODOP = getAntdLodop();
            printTemplate(LODOP, {
                code: 'abcd-123',
                title: 'abcd-123',
                desc:
                    '<p style="font-size:12px;">超文本2的HTML代码内容,超文本2的HTML代码内容,超文本2的HTML代码内容,超文本2的HTML代码内容,超文本2的HTML代码内容,超文本2的HTML代码内容,超文本2的HTML代码内容,超文本2的HTML代码内容,超文本2的HTML代码内容,</p>',
            });
            LODOP.PRINTA();
        } catch (e) {
            console.error(e);
        }

    };
    const onClick2 = () => {
        try {
            let LODOP = getAntdLodop();
            printTemplate(LODOP, {
                code: 'abcd-123',
                title: 'abcd-123',
                desc:
                    '<p style="font-size:12px;">超文本2的HTML代码内容,超文本2的HTML代码内容,超文本2的HTML代码内容,超文本2的HTML代码内容,超文本2的HTML代码内容,超文本2的HTML代码内容,超文本2的HTML代码内容,超文本2的HTML代码内容,超文本2的HTML代码内容,</p>',
            });
            let result = LODOP.PRINT_DESIGN();
        } catch (e) {
            console.error(e);
        }
    };
    return (
        <div>
            <button onClick={onClick}>{'打印'}</button>
            <button onClick={onClick2}>{'打印设计'}</button>
        </div>
    );
}

二维码打印的方式也比较简单:

  • ADD_PRINT_BARCODE,添加二维码元素
  • SET_PRINT_STYLEA,配置二维码的容错率

4 打印设计

function compileCode(src: string) {
        src = `with (exposeObj) { ${src} }`
        return new Function('exposeObj', src)
}

function proxyObj(originObj: any) {
        let exposeObj = new Proxy(originObj, {
                has: (target, key) => {
                        if (["console", "Math", "Date"].indexOf(String(key)) >= 0) {
                                return target[key]
                        }
                        if (!target.hasOwnProperty(key)) {
                                throw new Error(`Illegal operation for key ${String(key)}`)
                        }
                        return target[key]
                },
        })
        return exposeObj
}

function createSandbox(src: string, obj: any) {
        try {

                let proxy = proxyObj(obj)
                compileCode(src).call(proxy, proxy) //绑定this 防止this访问window
        } catch (e) {
                if (e instanceof SyntaxError) {
                        console.error(src);
                        throw new Error("代码语法错误");
                } else {
                        throw e;
                }
        }
}

export default createSandbox;

我们先定义一个js的沙箱,用来执行打印设计生成的js代码。注意,这种方法仅仅为Demo使用,并不安全。

import { LodopType } from '@/util/lodop';
import getAntdLodop from '@/util/lodopAntd';
import { useState } from 'react';
import jsSandBox from './jsSandBox';

export default function IndexPage() {
    const defaultTemplateInfo = `
LODOP.PRINT_INITA(
    0,
    0,
    '50.01mm',
    '100.01mm',
    '打印控件功能演示_Lodop功能_表单一',
);
LODOP.SET_PRINT_PAGESIZE(0, 500, 1000, '条形码');
//注意不要少了这一句
LODOP.SET_PRINT_MODE('PROGRAM_CONTENT_BYVAR', true);
LODOP.SET_PRINT_MODE('PRINT_NOCOLLATE', 1);
LODOP.ADD_PRINT_BARCODE(
    10,
    16,
    '55.59mm',
    '37.99mm',
    'QRCode',
    input.code,
);
LODOP.SET_PRINT_STYLEA(0, 'QRCodeErrorLevel', 'H');
//这一句是为了让lodop设计器,生成代码的时候自动替换输出代码
LODOP.SET_PRINT_STYLEA(0, 'ContentVName', 'input.code');
LODOP.ADD_PRINT_HTM(174, 10, 149, 191, input.desc);
LODOP.SET_PRINT_STYLEA(0, 'ContentVName', 'input.desc');
LODOP.ADD_PRINT_TEXT(145, 10, 164, 20, input.title);
LODOP.SET_PRINT_STYLEA(0, 'Alignment', 2);
LODOP.SET_PRINT_STYLEA(0, 'Horient', 2);
LODOP.SET_PRINT_STYLEA(0, 'ContentVName', 'input.title');
    `
    const [code, setCode] = useState(defaultTemplateInfo);

    const OriginData =
    {
        code: 'abcd-123',
        title: 'abcd-123',
        desc:
            '<p style="font-size:12px;">超文本2的HTML代码内容,超文本2的HTML代码内容,超文本2的HTML代码内容,超文本2的HTML代码内容,超文本2的HTML代码内容,超文本2的HTML代码内容,超文本2的HTML代码内容,超文本2的HTML代码内容,超文本2的HTML代码内容,</p>',
    };
    const print_preivew = () => {
        try {
            const LODOP = getAntdLodop();
            const packData = {
                LODOP: LODOP,
                input: OriginData,
            }
            jsSandBox(code, packData);
            LODOP.PREVIEW();
        } catch (e) {
            console.error(e);
        }
    }
    const print_setup = () => {
        try {
            const LODOP = getAntdLodop();
            //打开这一句才能在SETUP模式下获取命令
            LODOP.SET_PRINT_MODE("PRINT_SETUP_PROGRAM", true);
            LODOP.On_Return = function (TaskID: any, newValue: any) { setCode(newValue); };

            const packData = {
                LODOP: LODOP,
                input: OriginData,
            }
            jsSandBox(code, packData);
            LODOP.PRINT_SETUP();
        } catch (e) {
            console.error(e);
        }
    }
    const print_design = () => {
        try {
            const LODOP = getAntdLodop();
            const packData = {
                LODOP: LODOP,
                input: OriginData,
            }
            jsSandBox(code, packData);
            LODOP.On_Return = function (TaskID: any, newValue: any) { setCode(newValue); };
            LODOP.PRINT_DESIGN();
        } catch (e) {
            console.error(e);
        }
    }
    return (
        <div>
            <textarea style={{ width: '100%', height: '50vh' }} value={code} onChange={(e) => {
                setCode(e.target.value);
            }} />
            <div>
                <button onClick={print_preivew}>{'打印预览,只能看,不能改'}</button>
                <button onClick={print_setup}>{'打印维护,只能移动,不能增删'}</button>
                <button onClick={print_design}>{'打印设计,可完全修改'}</button>
            </div>
        </div>
    );
}

我们先预定义了一部分的打印代码,需要包括有:

  • PRINT_INITA,初始化可视化区域的偏移值和宽高。
  • SET_PRINT_PAGESIZE,打印页面的方向,和宽高。注意,这里的宽高刚好是PRINT_INITA的10倍大小。
  • SET_PRINT_MODE,设置返回打印变量,以及NOCOLLATE

然后,我们需要添加元素和设置变量

  • ADD_PRINT_BARCODE,添加元素
  • SET_PRINT_STYLEA(0, ‘ContentVName’, ‘input.code’),注意设置变量名,注意变量名和元素设置的变量名要一致

当我们用PRINT_SETUP或者PRINT_DSESIGN打开的时候,就能看到页面生成的程序代码了

另外,我们也能在SETUP或者DESIGN之后通过程序获取最新的代码,包括有:

  • SETUP,需要设置SET_PRINT_MODE以及onReturn
  • DESIGN,需要设置onReturn即可

5 手动分页


import getAntdLodop from '@/util/lodopAntd';
import { Modal } from 'antd';
export default function IndexPage() {
    const onClick = () => {
        let LODOP = getAntdLodop();
        const desc =
            '<p style="font-size:12px;">超文本2的HTML代码内容,超文本2的HTML代码内容,超文本2的HTML代码内容,超文本2的HTML代码内容,超文本2的HTML代码内容,超文本2的HTML代码内容,超文本2的HTML代码内容,超文本2的HTML代码内容,超文本2的HTML代码内容,</p>';

        LODOP.PRINT_INITA(
            0,
            0,
            '50mm',
            '100mm',
            '打印控件功能演示_Lodop功能_表单一',
        );
        LODOP.SET_PRINT_PAGESIZE(0, 500, 1000, '条形码');
        LODOP.SET_PRINT_MODE('PRINT_NOCOLLATE', 1);

        for (let i = 0; i != 10; i++) {
            //手动分页
            LODOP.NewPage();
            LODOP.ADD_PRINT_BARCODE(
                10,
                16,
                '55.59mm',
                '37.99mm',
                'QRCode',
                '7-26-6-199' + i,
            );
            LODOP.SET_PRINT_STYLEA(0, 'QRCodeErrorLevel', 'H');
            LODOP.ADD_PRINT_HTM(174, 10, 149, 191, desc);
            LODOP.ADD_PRINT_TEXT(145, 10, 164, 20, '7-26-6-199' + i);
            LODOP.SET_PRINT_STYLEA(0, 'Alignment', 2);
            LODOP.SET_PRINT_STYLEA(0, 'Horient', 2);
        }
        LODOP.PREVIEW();
    };
    return (
        <div>
            <button onClick={onClick}>{'点我'}</button>
        </div>
    );
}

通过调用NewPage来实现手动分页,这个没啥好说的,比较简单

6 表格

表格是整个打印里面的关键部分,它一般需要的业务有:

  • 表格的高度自动适应纸质高度,当表格高度太高的时候,需要自动换页
  • 表格内的页头和页脚是每一页都需要展示的,不会随着换页和丢失
  • 表格每页的数据汇总和格式化,包括行号,该页的金额汇总,当前页的之前的金额汇总,所有页的金额汇总,数据保留小数点,中文数字输出等等
import getAntdLodop from '@/util/lodopAntd';
import { Modal } from 'antd';
import _ from 'underscore';

type DataType = {
    amount: number,
    price: number,
    total: number,
}
const data: DataType[] = [];
for (let i = 0; i != 16; i++) {
    let single = {
        amount: i + 10,
        price: i * 0.1 + 1.5,
        total: 0,
    };
    single.total = single.amount * single.price;
    data.push(single);
}
const tableContent = `
<table border="1" width="100%" cellspacing="0" cellpadding="0" style="border-collapse:collapse" bordercolor="#000000">
<caption>报表统计演示</caption>
<thead>
    <tr>
    <th width="100%" colspan="4" tindex="1">
      <span tdata="PageNO" format="ChineseNum" color="blue">当前是第##页/</span> 
      <span tdata="PageCount" format="ChineseNum" color="blue">共##页</span>
    </tr>
    <tr>
        <th width="20%">序号</th>
        <th width="26%">数量</th>
        <th width="26%">单价</th>
        <th width="28%">金额</th>
    </tr>
</thead>
<tbody>
    <% for(var i = 0 ;i != data.length;i ++){ var single = data[i];%>
    <tr>
        <!--放在td里面不能生效,放在th或者span里面都可以-->
        <td><span tdata="Count" format="#">第######行</span></td>
        <td><%= single.amount %></td>
        <td><%= single.price %></td>
        <td><%= single.total %></td>
    </tr>
    <% } %>
    <tr>
    </tr>
</tbody>
<tfoot>
        <tr>
            <th align="right" colspan="2" tdata="SubSum" format="#.##">
            本页数量小计:######</th>   
            <th align="right" colspan="2" tdata="SubSum" format="#,##0.00">
            本页金额小计:######</th>   
        </tr>
        <tr>
            <th align="right" colspan="2" tdata="AllSum" format="#.##" tindex="4">
            全部金额小计:######</th>   
            <th align="right" colspan="2" tdata="AllSum" format="ChineseNum" tindex="2">
            全部数量小计:######</th>   
        </tr>
</tfoot>
`;

export default function IndexPage() {
    const tpl = _.template(tableContent);
    const lastData = tpl({
        data: data,
    });

    const print_preivew = () => {
        let LODOP = getAntdLodop();
        LODOP.ADD_PRINT_TABLE(100, 1, "99.8%", 250, lastData);
        LODOP.PREVIEW();
    };
    return (
        <div>
            <button onClick={print_preivew}>{'预览打印'}</button>
            <div dangerouslySetInnerHTML={{ __html: lastData }}></div>
        </div>
    );
}

LODOP提供ADD_PRINT_TABLE来实现以上的功能,注意不要使用ADD_PRINT_HTM来输出表格,会丢失以上的表格输出特征

  • caption,thead,和tfoot是表格内的标题,表格内的页头和页脚
  • tdata提供了按照某一列tindex(从1开始)的汇总功能,SubSum是当前页汇总,Sum是当前和之前页汇总,AllSum是所有页汇总
  • format提供了多种输出格式,#.##,#,##0.00和ChineseNum
  • tdata的Count是输出当前行的行号。

具体可以看这里

7 页眉页脚

表格内的页眉和页脚不能解决所有问题,有时候我们希望页眉和页脚是页面级别的,不是表格级别的。这个时候,我们需要结合LinkedItem来实现

import { LodopType } from '@/util/lodop';
import getAntdLodop from '@/util/lodopAntd';
import { Modal } from 'antd';
import _ from 'underscore';

type DataType = {
    amount: number,
    price: number,
    total: number,
}
const data: DataType[] = [];
for (let i = 0; i != 60; i++) {
    let single = {
        amount: i + 10,
        price: i * 0.1 + 1.5,
        total: 0,
    };
    single.total = single.amount * single.price;
    data.push(single);
}
const tableContent = `
<table border="1" width="100%" cellspacing="0" cellpadding="0" style="border-collapse:collapse" bordercolor="#000000">
<thead>
    <tr>
    <th width="100%" colspan="4" tindex="1">
      <span tdata="PageNO" format="ChineseNum" color="blue">当前是第##页/</span> 
      <span tdata="PageCount" format="ChineseNum" color="blue">共##页</span>
    </tr>
    <tr>
        <th width="20%">序号</th>
        <th width="26%">数量</th>
        <th width="26%">单价</th>
        <th width="28%">金额</th>
    </tr>
</thead>
<tbody>
    <% for(var i = 0 ;i != data.length;i ++){ var single = data[i];%>
    <tr>
        <!--放在td里面不能生效,放在th或者span里面都可以-->
        <td><span tdata="Count" format="#">第######行</span></td>
        <td><%= single.amount %></td>
        <td><%= single.price %></td>
        <td><%= single.total %></td>
    </tr>
    <% } %>
    <tr>
    </tr>
</tbody>
<tfoot>
        <tr>
            <th align="right" colspan="2" tdata="SubSum" format="#.##">
            本页数量小计:######</th>   
            <th align="right" colspan="2" tdata="SubSum" format="#,##0.00">
            本页金额小计:######</th>   
        </tr>
        <tr>
            <th align="right" colspan="2" tdata="AllSum" format="#.##" tindex="4">
            全部金额小计:######</th>   
            <th align="right" colspan="2" tdata="AllSum" format="ChineseNum" tindex="2">
            全部数量小计:######</th>   
        </tr>
</tfoot>
`;

const pageHeader = `
<h1 style="LINE-HEIGHT: 30px;color:#0000FF;" align="center">销售发货单-01</h1>
<table border="0" cellspacing="0" cellpadding="0" width="100%">
  <tbody>
  <tr>
    <td width="43%" style='color:#0000FF'>所在店铺:<span id="rpt_Pro_Order_List_ctl00_lbl_eShop_Name">雅瑞专卖店</span></td>
    <td width="33%" style='color:#0000FF'>发货单号:<span>2011050810372</span></td>
    <td style='color:#0000FF'>快递单号:</td>
  </tr>
  <tr>
    <td style='color:#0000FF'>收 件 人:<span>王斌</span></td> 
    <td style='color:#0000FF'>网店单号:<span>74235823905643</span></td>
    <td style='color:#0000FF'>发货日期:2011-5-10</td>
  </tr>
  <tr>
    <td style='color:#0000FF'>电话号码:<span>13935429860 </span></td>
    <td style='color:#0000FF'>收件人ID:<span>云星王斌</span></td>
    <td style='color:#0000FF'> </td>
  </tr>
  </tbody>
</table>
`

const pageFooter = `
<div style="LINE-HEIGHT: 30px;color:#0000FF" align="center">感谢您对我们雅瑞专卖店的支持,(发货单01的表格外“页脚”,紧跟表格)</div>
`

const printHeader = `
总页号:<span tdata='pageNO' style='color:#0000ff' format='ChineseNum'>第##页</span>/<span tdata='pageCount' style='color:#0000ff' format='ChineseNum'>共##页</span>
`

export default function IndexPage() {
    const tpl = _.template(tableContent);
    const lastData = tpl({
        data: data,
    });

    const createPage = (LODOP: LodopType) => {
        //每页中间的表格,1号对象
        LODOP.ADD_PRINT_TABLE(135, "5%", "90%", 314, lastData);
        //页头的设置
        LODOP.ADD_PRINT_HTM(26, "5%", "90%", 109, pageHeader);
        LODOP.SET_PRINT_STYLEA(0, "ItemType", 1);
        //链接到1号对象
        LODOP.SET_PRINT_STYLEA(0, "LinkedItem", 1);

        //页尾的设置
        LODOP.ADD_PRINT_HTM(444, "5%", "90%", 54, pageFooter);
        LODOP.SET_PRINT_STYLEA(0, "ItemType", 1);
        LODOP.SET_PRINT_STYLEA(0, "LinkedItem", 1);


        //总页眉,没有LinkedItem,每个页面都有
        LODOP.ADD_PRINT_HTM(1, 600, 300, 100, printHeader);
        LODOP.SET_PRINT_STYLEA(0, "ItemType", 1);
        LODOP.SET_PRINT_STYLEA(0, "Horient", 1);
        LODOP.ADD_PRINT_TEXT(3, 34, 196, 20, "总页眉:《两个发货单的演示》");
        LODOP.SET_PRINT_STYLEA(0, "ItemType", 1);
    }
    const print_preivew = () => {
        let LODOP = getAntdLodop();
        createPage(LODOP);
        LODOP.PREVIEW();
    };
    const print_design = () => {
        let LODOP = getAntdLodop();
        createPage(LODOP);
        LODOP.PRINT_DESIGN();
    };
    return (
        <div>
            <button onClick={print_preivew}>{'预览打印'}</button>
            <button onClick={print_design}>{'打印设计'}</button>
            <div dangerouslySetInnerHTML={{ __html: printHeader }}></div>
            <div dangerouslySetInnerHTML={{ __html: pageHeader }}></div>
            <div dangerouslySetInnerHTML={{ __html: lastData }}></div>
            <div dangerouslySetInnerHTML={{ __html: pageFooter }}></div>
        </div>
    );
}

要点如下:

  • 页头和页尾,ItemType设置为1,并设置对应的LinkedItem即可

可以看到表格会自动分页,并且页头和页尾的位置也会跟随表格的高度变动而改变。

8 API详解

LODOP的API文档真的很烂,说得很不清楚

8.1 SET_PRINT_PAGESIZE

SET_PRINT_PAGESIZE: (
    Orient: LodopPageOrient,
    Width: LodopPixelType,
    Height: LodopPixelType,
    Name: string,
) => void;

文档看这里,这个API用来决定纸张大小。Orient的取值如下:

  • 1—纵向打印,固定纸张; 文字的方向,与打印机出纸的方式一致,这是最为常见的打印方式。
  • 2—横向打印,固定纸张; 文字的方向,与打印机出纸的方式垂直,这是特殊的打印方式。
  • 3—纵向打印,宽度固定,高度按打印内容的高度自适应(见样例18);
  • 0—方向不定,由操作者自行选择或按打印机缺省设置。

Width与Height的大小可以设置为两种

  • 字符串,“210mm”
  • 数字,“210mm”相当于数字2100。我们建议使用数字的方式设置

我们最为常见的错误是,明明用该代码设置了纸张的大小,但是预览的时候死活没用上,预览的时候有一圈的虚线和展示不齐全的问题。

实际原因是,纸张不仅与用户设置有关,而且与打印机支持的类型有关。如果我们没有对应的打印机,我们不妨改为使用XPS Document Writer打印机来测试纸张效果。另外,打印以后使用XPS Viewer来查看打印效果就可以了。

附上常见的纸张尺寸。

  • A4,210mm * 297mm
  • 复印二开,214mm * 139mm
  • 复印一开,214mm * 280mm

8.2 SET_PRINT_STYLEA

SET_PRINT_STYLEA: (targetId: number, key: string, value: any) => void;

SET_PRINT_STYLEA可以说是最为复杂的API了,当targetId设置为0的时候,代表设置最后一个添加对象的属性。

  • ItemType,ItemType为0的时候,代表正常显示内容。ItemType为1的时候,代表页眉页脚,每页都会出现的
  • LinkedItem,关联顺序。默认情况下,每个元素以页面作为的top与left作为偏移量为布局。当设置了LinkedItem指向对应的元素ID以后,以指定元素的偏移量为布局。具体看这里的文档
  • Vorient,我也不知道这个属性有啥用,当设置为3的时候,有时候会显示出错

8.3 ADD_PRINT_TABLE

ADD_PRINT_TABLE: (
    Top: LodopPixelType,
    Left: LodopPixelType,
    Width: LodopPixelType,
    Height: LodopPixelType,
    Content: string,
) => void;

ADD_PRINT_TABLE是最为复杂的元素,它是实现表格自动分页的关键。它的Content只能为一个Table元素,而这个Table元素里面的thead和tfoot内容是自动变为页面的页眉和页脚的。Height指定的是tbody里面的高度,不包含thead和tfoot的高度,所以当tbody的高度大于Height的时候,就会自动分页。

我们常见用ADD_PRINT_TABLE的做法是:

  • 先用填写ADD_PRINT_TABLE的内容。
  • 使用ADD_PRINT_HTM来填充页眉和页脚,并使用它们的LinkedItem为1来自动指向到首个元素Table,同时使用ItemType为1,设置为每页均显示。

常见的问题是:

  • 页眉看不到,可能是因为页眉的高度太小了,导致Table元素覆盖了它的内容
  • 页脚看不到,Table元素的Height只包括tbody,不包括thead与tfoot,所以,不要只看设计页面的高度来设定Table的高度。解决方法是,降低Table的Height,或者是提高页脚的top值。

9 FAQ

9.1 证书问题

有些时候,Lodop明明安装了依然启动不了,而这种问题只出现在https的环境中。这是因为加载https版本的lodopfunc.js出错了,证书有问题。解决方法是单独打开该文件,并忽略该证书即可。

9.2 兼容性问题

LODOP使用IE内核作为表格和富文本的展示,在不同的Windows系统中显示会有少许差异。建议使用IE的兼容性视图,来做不同环境中的提前测试。

9.3 文字竖排

有时候,我们需要文字竖列展示。

LODOP.ADD_PRINT_TEXT(166,758,19,336,"①\r\n白\r\n存\r\n根\r\n②\r\n红\r\n客\r\n户\r\n③\r\n蓝\r\n收\r\n款\r\n凭\r\n证\r\n④\r\n黄\r\n仓\r\n库");

解决办法是让,在属性中在文字内容上输入回车字符即可。

10 总结

LODOP是一个省事和成熟的工具,相关缺陷有:

  • API和文档设计得有点混乱。另外,Sample所用的Html标准也比较陈旧。
  • 对于html5语法兼容比较差,采用Windows自带的IE内核来渲染,不同机器上的效果有一点不同。
  • 预览效果兼容也差,选择用不同的打印机的时候,预览的效果是不一样的,这使得每次都要到客户的机器上调试才能得到最好的打印效果。
  • 打印的效果不清晰,不知道是不是使用了富文本有关,打印的效果与直接用网页打印,要模糊一点。
  • 不支持数据与样式分离,自带的数据与样式分离做得不够彻底,它将整个富文本看成了数据,设计略有瑕疵,但这点问题不大。

类似的工具还有:

  • jatools,比lodop的兼容性要好不少,可以使用标准的html5语法,而且设计页面也好用。唯一的缺点是不是面向报表,是面向打印的,所以还是需要自己做表格样式的生成,而不是直接代入数据就可以了。另外的一个缺点就是贵,也没啥好说的了,值得推荐。
  • 康虎云报表,2017年以后就没有更新,不建议使用。
  • 锐浪报表,不支持Chrome,不建议使用。

其他方式:

  • 使用低代码的逻辑来做页面设计
  • html转换为pdf然后打印,pdf相比html打印的优点在于,兼容性要好,预览和实际打印相差不大。

低代码的开发逻辑:

html转pdf的路线

相关文章