简介
在现代Web应用开发中,打印功能是不可或缺的一部分,尤其是在需要输出标准化文档的场景下。本文将详细介绍如何在Vue3项目中利用vue-plugin-hiprint
插件实现一个可定制的打印模板设计器,并通过具体示例来展示其配置与使用方法,注意
:下面代码中有个人的js和css样式代码,根据自己的需求去改即可。
技术栈
本项目基于以下技术栈:
- Vue3
- TypeScript (Ts)
- Element Plus
- Vite
插件安装
首先通过npm安装vue-plugin-hiprint
插件:
npm install vue-plugin-hiprint
main.ts引入组件
在项目的入口文件main.ts
中引入hiPrintPlugin
并注册到Vue应用实例上。同时引入全局CSS(从node_modules/vue-plugin-hiprint/dist/目录下复制一份print-lock.css
到静态资源目录下)。
import { createApp } from 'vue';
import App from './App.vue';
// 引入全局CSS
import './assets/public/print-lock.css'
import { hiPrintPlugin } from 'vue-plugin-hiprint';
hiPrintPlugin.disAutoConnect(); // 取消自动连接直接打印客户端
const app = createApp(App);
app.use(hiPrintPlugin, '$pluginName');
CSS样式配置
为了确保打印效果符合预期,在原有的print-lock.css
基础上进行了扩展和调整,以适应不同的浏览器环境,尤其是Firefox浏览器中可能出现的打印重叠问题。如下是个人的iconfont.css
,等会主组件要用,其中里面的SVG字体
样式点击下载:
@font-face {
font-family: "iconfont"; /* Project id 3559670 */
src: url("iconfont.woff2?t=1667531544868") format("woff2"),
url("iconfont.woff?t=1667531544868") format("woff"),
url("iconfont.ttf?t=1667531544868") format("truetype");
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.sv-edit-data:before {
content: "\e655";
}
.sv-shimmer:before {
content: "\e6d6";
}
.sv-origin:before {
content: "\e6ac";
}
.sv-zIndex:before {
content: "\e603";
}
.sv-structure:before {
content: "\ec6f";
}
.sv-list:before {
content: "\e742";
}
.sv-grid:before {
content: "\e849";
}
.sv-flow:before {
content: "\e611";
}
.sv-switch:before {
content: "\e6f6";
}
.sv-theme:before {
content: "\e644";
}
.sv-element:before {
content: "\e615";
}
.sv-pdf:before {
content: "\e67a";
}
.sv-browser:before {
content: "\e726";
}
.sv-font-big:before {
content: "\eb04";
}
.sv-font-small:before {
content: "\eb05";
}
.sv-font-bold:before {
content: "\ec83";
}
.sv-font-tiny:before {
content: "\e6c1";
}
.sv-options:before {
content: "\e607";
}
.sv-close:before {
content: "\e646";
}
.sv-clone:before {
content: "\ec7a";
}
.sv-cut:before {
content: "\e643";
}
.sv-preview:before {
content: "\e61c";
}
.sv-zoom-in:before {
content: "\e60f";
}
.sv-zoom-out:before {
content: "\e610";
}
.sv-edit:before {
content: "\e6b9";
}
.sv-paste:before {
content: "\e6c0";
}
.sv-copy:before {
content: "\e6c2";
}
.sv-unlock:before {
content: "\e6e7";
}
.sv-lock:before {
content: "\e6e8";
}
.sv-zIndex-plus:before {
content: "\e715";
}
.sv-zIndex-minus:before {
content: "\e716";
}
.sv-zIndex-top:before {
content: "\e71f";
}
.sv-sigh:before {
content: "\e724";
}
.sv-ask:before {
content: "\e725";
}
.sv-dev-code:before {
content: "\e733";
}
.sv-bug:before {
content: "\e73f";
}
.sv-zIndex-bottom:before {
content: "\e71d";
}
.sv-new:before {
content: "\e64d";
}
.sv-clear:before {
content: "\e62d";
}
.sv-base:before {
content: "\e7d0";
}
.sv-export:before {
content: "\eabf";
}
.sv-import:before {
content: "\eac0";
}
.sv-add:before {
content: "\eaf3";
}
.sv-printer:before {
content: "\eabe";
}
.sv-save:before {
content: "\eabd";
}
.sv-more:before {
content: "\e625";
}
.sv-menu:before {
content: "\e628";
}
.sv-nav-right:before {
content: "\e629";
}
.sv-nav-up:before {
content: "\e62a";
}
.sv-nav-left:before {
content: "\e62b";
}
.sv-nav-down:before {
content: "\e62c";
}
.sv-setting:before {
content: "\e62e";
}
.sv-delete:before {
content: "\e630";
}
.sv-undo:before {
content: "\e631";
}
.sv-redo:before {
content: "\e632";
}
.sv-refresh:before {
content: "\e634";
}
.sv-history:before {
content: "\e635";
}
.sv-html:before {
content: "\e633";
}
.sv-longText:before {
content: "\e64c";
}
.sv-table:before {
content: "\ec15";
}
.sv-qrcode:before {
content: "\e642";
}
.sv-image:before {
content: "\e8ba";
}
.sv-barcode:before {
content: "\eb64";
}
.sv-text:before {
content: "\e60b";
}
.sv-vline:before {
content: "\e63a";
}
.sv-oval:before {
content: "\eb99";
}
.sv-rect:before {
content: "\e620";
}
.sv-hline:before {
content: "\e60a";
}
.sv-print-c:before {
content: "\e602";
}
.sv-print:before {
content: "\e601";
}
.sv-c:before {
content: "\e600";
}
.sv-vertical:before {
content: "\e706";
}
.sv-distributeHor:before {
content: "\e707";
}
.sv-right:before {
content: "\e708";
}
.sv-left:before {
content: "\e709";
}
.sv-distributeVer:before {
content: "\e70f";
}
.sv-bottom:before {
content: "\e710";
}
.sv-top:before {
content: "\e711";
}
.sv-horizontal:before {
content: "\e712";
}
.sv-rotate:before {
content: "\e66f";
}
.sv-butongbu:before {
content: "\e636";
}
.sv-synchronization:before {
content: "\e676";
}
/* 重写全局 hiprint 样式 */
.hiprint-headerLine,
.hiprint-footerLine {
border-color: purple !important;
}
.hiprint-headerLine:hover,
.hiprint-footerLine:hover {
border-top: 3px dashed purple !important;
}
.hiprint-headerLine:hover:before {
content: "页眉线";
left: calc(50% - 18px);
position: relative;
background: #ffff;
top: -14px;
color: purple;
font-size: 12px;
}
.hiprint-footerLine:hover:before {
content: "页脚线";
left: calc(50% - 18px);
position: relative;
color: purple;
background: #ffff;
top: -14px;
font-size: 12px;
}
/* 区域 */
.left {
background: white;
border-radius: 4px;
border: 1px solid #d9d9d9;
padding: 10px 0;
box-shadow: 2px 2px 2px 0px rgb(128 0 128 / 20%);
overflow: auto;
}
.center {
margin: 0 10px;
background: white;
border-radius: 4px;
border: 1px solid #d9d9d9;
padding: 20px;
box-shadow: 2px 2px 2px 0px rgb(128 0 128 / 20%);
overflow: auto;
}
.right {
background: white;
border-radius: 4px;
border: 1px solid #d9d9d9;
padding: 10px 0;
box-shadow: 2px 2px 2px 0px rgb(128 0 128 / 20%);
overflow: auto;
}
/* 左侧拖拽元素样式 */
.title {
font-size: 16px;
font-weight: 500;
width: 100%;
margin: 10px 0 0 24px;
}
.item {
display: flex;
flex-direction: column;
align-items: center;
background: white;
padding: 4px 10px;
margin: 10px 8px 4px 8px;
width: 38%;
min-height: 60px;
border-radius: 4px;
box-shadow: 2px 2px 2px 2px rgba(171, 171, 171, 0.2);
}
.item .iconfont {
font-size: 1.5rem;
}
.item span {
font-size: 14px;
}
/* scrollbar */
::-webkit-scrollbar {
height: 4px;
width: 4px;
}
::-webkit-scrollbar-corner {
height: 4px;
width: 4px;
}
::-webkit-scrollbar-thumb {
background: purple;
border-radius: 2px;
background-image: -webkit-linear-gradient(
45deg,
rgba(255, 255, 255, 0.2) 25%,
transparent 25%,
transparent 50%,
rgba(255, 255, 255, 0.2) 50%,
rgba(255, 255, 255, 0.2) 75%,
transparent 75%,
transparent
);
}
::-webkit-scrollbar-thumb:hover {
background: purple;
}
/* flex */
.flex-row {
display: flex;
}
.flex-col {
display: flex;
flex-direction: column;
}
.flex-wrap {
flex-wrap: wrap;
}
.align-center {
align-items: center;
}
.justify-center {
justify-content: center;
}
.flex-1 {
flex: 1;
}
.flex-2 {
flex: 2;
}
.flex-3 {
flex: 3;
}
.flex-4 {
flex: 4;
}
.flex-5 {
flex: 5;
}
.ml-10 {
margin-left: 10px;
}
.mr-10 {
margin-right: 10px;
}
.mt-10 {
margin-top: 10px;
}
.mb-10 {
margin-bottom: 10px;
}
button:hover {
opacity: 1;
}
button i {
font-size: 16px !important;
}
.circle,
.circle-4 {
border-radius: 4px !important;
}
.circle-10 {
border-radius: 10px !important;
}
/* modal */
.modal {
padding: 0;
margin: 0;
}
.modal .mask {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1000;
height: 100%;
background-color: #00000073;
}
.modal .wrap {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1000;
overflow: auto;
background-color: #00000073;
outline: 0;
}
.modal .wrap .box {
position: relative;
margin: 10% auto;
width: 40%;
background: #fff;
border-radius: 4px;
z-index: 1001;
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
transition: all 0.3s ease;
}
.modal-box__header {
padding: 10px 14px;
border-bottom: 1px solid #e9e9e9;
}
.modal-box__footer {
text-align: end;
}
.modal-box__footer button {
min-width: 100px;
}
.modal-box__footer button:not(:last-child) {
margin-right: 10px;
}
拖拽组件
为了提供更加丰富的设计体验,可以通过自定义拖拽组件来增强打印模板设计器的功能性。例如,这里customProvider.js
:可以添加个人业务
(追溯业务)相关的元素,如文本、条形码、二维码等。
import { hiprint } from "vue-plugin-hiprint";
export const iCustomProvider = function (options) {
console.log(options.moduleList.value);
var addElementTypes = function (context) {
context.removePrintElementTypes("providerModule1");
context.addPrintElementTypes("providerModule1", [
new hiprint.PrintElementTypeGroup("追溯业务", [
// options.config,
...options.moduleList.value.map(item => ({
tid: item.defaultModule,
title: item.title,
data: item.title,
type: item.type,
options: {
field: item.field,
height: 14,
testData: "默认",
fontSize: 12,
fontWeight: '500',
textAlign: 'center',
textContentVerticalAlign: 'middle',
...(item.textType ? { textType: item.textType } : {}),
},
})),
{
tid: "providerModule1.text",
title: "文本",
data: "文本",
type: "text",
options: {
field: "customText",
testData: "文本",
height: 14,
fontSize: 12,
fontWeight: "500",
textAlign: "left",
textContentVerticalAlign: "middle",
},
},
{
tid: "providerModule1.barcode",
title: "条形码",
data: "XS888888888",
type: "text",
options: {
field: "barcode",
testData: "XS888888888",
height: 32,
fontSize: 12,
lineHeight: 18,
textAlign: "left",
textType: "barcode",
},
},
{
tid: "providerModule1.qrcode",
title: "二维码",
data: "XS888888888",
type: "qrcode",
options: {
field: "qrcode",
testData: "XS888888888",
height: 32,
fontSize: 12,
lineHeight: 18,
textType: "qrcode",
},
},
]),
]);
};
return {
addElementTypes: addElementTypes,
};
};
注意
:如上代码中的moduleList
是在主组件调用时,访问后台接口的数据传递过来的,根据自己的需求返回格式即可。需要注意的是,field
是组件字段名,title
是组件的标题或标签,type
是组件的类型和对应的基础模块或组件实现defaultModule
,了解更多type和对应的defaultModule可以点击这里,如下是个人接口返回的数据格式:
{
"code": 200,
"msg": "操作成功",
"data": {
"style": [{
"field": "wlName",
"title": "物料名称",
"type": "text",
"defaultModule": "defaultModule.wlName"
},
{
"field": "specification",
"title": "物料规格",
"type": "text",
"defaultModule": "defaultModule.specification"
},
{
"field": "printQrCodeUrl",
"textType": "qrcode",
"title": "追溯码",
"type": "qrcode",
"defaultModule": "defaultModule.qrCode"
},
...
]
}
}
主组件调用
在主组件中,我们使用el-drawer
来显示打印模板的设计界面,引入上述我们创建好的customProvider.js
和iconfont.css
。此界面包括纸张尺寸的选择或自定义输入,以及打印、导出模板为JSON格式的功能按钮。
<el-drawer v-model="drawer" title="创建模板" size="100%" @opened="buildDesigner">
<!-- 纸张尺寸选择区域 -->
<el-button-group>
<el-button
v-for="(value, type) in paperTypes"
:key="type"
:type="getButtonType(type)"
@click="setPaper(type, value)">
{{ type }}
</el-button>
<!-- 自定义纸张尺寸输入框 -->
<el-popover
v-model="paperPopVisible"
title="设置纸张宽高(mm)"
:width="240"
trigger="click">
<!-- 输入框 -->
<el-input
type="number"
v-model="paperWidth"
placeholder="宽(mm)">
</el-input>
<el-input
type="number"
v-model="paperHeight"
placeholder="高(mm)">
</el-input>
<el-button type="primary" @click="otherPaper">确定</el-button>
</el-popover>
</el-button-group>
<!-- 功能按钮 -->
<button @click.stop="print"><i class="iconfont sv-printer" />浏览器打印</button>
<button @click.stop="exportJson"><i class="iconfont sv-export" />保存模板</button>
<el-popconfirm
title="是否确认清空?"
@confirm="clearPaper"
>
<template #reference>
<el-button type="danger" style="margin-left: 10px;">
清空
<template #icon>
<el-icon><Close /></el-icon>
</template>
</el-button>
</template>
</el-popconfirm>
</div>
<div class="flex-row" style="height: 87vh">
<div class="flex-2 flex-wrap">
<!-- <div class="title">追溯设计模版</div> -->
<div id="provider-container1" class="container rect-printElement-types"></div>
</div>
<div class="flex-5 center">
<!-- 设计器的 容器 -->
<div id="hiprint-printTemplate"></div>
</div>
<div class="flex-2 right">
<!-- 元素参数的 容器 -->
<div id="PrintElementOptionSetting"></div>
</div>
</div>
</div>
</el-drawer>
<script setup name="Template" lang="ts">
import { hiprint } from 'vue-plugin-hiprint';
import { iCustomProvider } from './customProvider'
// ...其他业务代码
// 当前纸张
const curPaper = ref({
type: 'other',
width: 80,
height: 60
});
// 纸张类型
const paperTypes = ref({
'A3': {
width: 420,
height: 296.6
},
'A4': {
width: 210,
height: 296.6
},
'A5': {
width: 210,
height: 147.6
},
'B3': {
width: 500,
height: 352.6
},
'B4': {
width: 250,
height: 352.6
},
'B5': {
width: 250,
height: 175.6
}
});
// 自定义纸张
const paperPopVisible = ref(false);
const paperWidth = ref('80');
const paperHeight = ref('60');
// 计算按钮类型
const getButtonType = (type) => {
return curPaper.value.type === type ? 'primary' : 'default';
};
const drawer = ref(false);
/**
* 构建左侧可拖拽元素
* 注意: 可拖拽元素必须在 hiprint.init() 之后调用
* 而且 必须包含 class="ep-draggable-item" 否则无法拖拽进设计器
*/
const buildLeftElement = () => {
// eslint-disable-next-line no-undef
// hiprint.PrintElementTypeManager.buildByHtml($(".ep-draggable-item"));
$("#provider-container1").empty(); // 先清空, 避免重复构建
// eslint-disable-next-line no-undef
hiprint.PrintElementTypeManager.build($("#provider-container1"), "providerModule1");
};
/**
* 构建设计器
* 注意: 必须要在 onMounted 中去构建
* 因为都是把元素挂载到对应容器中, 必须要先找到该容器
*/
let hiprintTemplate;
const buildDesigner = () => {
//构建拖拽元素
buildLeftElement();
// eslint-disable-next-line no-undef
$("#hiprint-printTemplate").empty(); // 先清空, 避免重复构建
hiprintTemplate = new hiprint.PrintTemplate({
settingContainer: "#PrintElementOptionSetting", // 元素参数容器
});
hiprintTemplate.design("#hiprint-printTemplate");
if (data.form.templateJson) {
mergeTemplate(data.form.templateJson);
}
};
/**
* 浏览器打印
*/
const print = () => {
// 打印数据,key 对应 元素的 字段名
let printData = { name: "CcSimple" };
// 参数: 打印时设置 左偏移量,上偏移量
let options = { leftOffset: -1, topOffset: -1 };
// 扩展
let ext = {
callback: () => {
console.log("浏览器打印窗口已打开");
},
styleHandler: () => {
// 重写 文本 打印样式
return "<style>.hiprint-printElement-text{color:red !important;}</style>";
},
};
// 调用浏览器打印
hiprintTemplate.print(printData, options, ext);
};
/**
* 导出模板 json
* 必须确保 hiprintTemplate 已成功创建
*/
const exportJson = () => {
const jsonIns = hiprintTemplate.getJson();
data.form.templateJson = JSON.stringify(jsonIns);
drawer.value = false;
validateTemplateJson();
};
/**
* 导出模板 json tid
* 仅导出 options, 不导出 printElementType
* 必须确保 hiprintTemplate 已成功创建
*/
const exportJsonTid = () => {
const jsonIns = hiprintTemplate.getJsonTid();
console.log(jsonIns);
alert("导出成功! 请查看控制台输出");
};
/**
* 更新出新的模板内容
*/
const mergeTemplate = (jsonIn: string) => {
if (hiprintTemplate) {
try {
hiprintTemplate.update(JSON.parse(jsonIn))
} catch (e) {
ElMessage.error(`更新失败: ${e}`)
}
}
};
/**
* 设置纸张大小
* @param type [A3, A4, A5, B3, B4, B5, other]
* @param value {width,height} mm
*/
const setPaper = (type, value) => {
try {
// 更新当前纸张信息
curPaper.value = {type: type, width: value.width, height: value.height};
// 设置打印模板的纸张尺寸
hiprintTemplate.setPaper(value.width, value.height);
} catch (error) {
ElMessage.error(`操作失败: ${error}`);
}
};
// 触发模板数据的验证
const validateTemplateJson = () => {
if (templateFormRef.value) {
templateFormRef.value.validateField('templateJson').catch(() => { });
}
};
/** 查询打印模板列表 */
const getList = async () => {
loading.value = true;
const res = await listTemplate(queryParams.value);
templateList.value = res.rows;
total.value = res.total;
loading.value = false;
}
/** 获取模板组件数据 **/
let moduleList = ref([]);
const templateModules = async () => {
try {
const res = await getTemplateModule();
moduleList.value = res.data.style;
} catch (error) {
console.error('Failed to fetch template module:', error);
}
};
onMounted(async () => {
getList();
await templateModules();
hiprint.init({
providers: [iCustomProvider({ moduleList: moduleList })]
});
buildDesigner();
});
</script>
<style>
@import '@/assets/css/iconfont.css';
</style>
展示效果
注意事项
- 纸张尺寸自定义:在设置纸张尺寸时,需确保输入的数值符合实际打印设备支持的范围。
- 打印样式兼容性:不同浏览器对打印样式的处理可能略有差异,特别是在处理页面布局时,需要对特定浏览器进行针对性优化。
- 自定义元素类型:当添加自定义元素类型时,需确保每个元素的属性值正确无误,特别是对于条形码和二维码这样的复杂元素。