简介
在现代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>
复制
展示效果
注意事项
- 纸张尺寸自定义:在设置纸张尺寸时,需确保输入的数值符合实际打印设备支持的范围。
- 打印样式兼容性:不同浏览器对打印样式的处理可能略有差异,特别是在处理页面布局时,需要对特定浏览器进行针对性优化。
- 自定义元素类型:当添加自定义元素类型时,需确保每个元素的属性值正确无误,特别是对于条形码和二维码这样的复杂元素。