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的路线
- 本文作者: fishedee
- 版权声明: 本博客所有文章均采用 CC BY-NC-SA 3.0 CN 许可协议,转载必须注明出处!