HiPrint 快速上手
本文档基于 Playground 实际代码,手把手带你完成 HiPrint 静默打印集成。
第一步:安装 electron-hiprint 客户端
前往 electron-hiprint Releases 下载对应平台安装包(Windows / macOS / Linux)。
启动后默认监听 http://localhost:17521,保持运行即可。
第二步:安装前端依赖
bash
pnpm add vue-plugin-hiprint由于 vue-plugin-hiprint 没有 TypeScript 类型声明,需要手动添加一个:
ts
// src/hiprint.d.ts
declare module 'vue-plugin-hiprint'第三步:实现连接管理 Hook
这是整个集成的核心,负责连接 electron-hiprint、发现打印机、执行打印。
ts
// src/hooks/useHiPrint.ts
import { hiprint } from 'vue-plugin-hiprint'
import { computed, ref, shallowRef } from 'vue'
const NAMESPACE = 'easyink' // 多实例隔离用的命名空间
// ---- 配置持久化 ----
const CONFIG_KEY = 'easyink:printerConfig'
interface PrinterConfig {
enabled: boolean
serviceUrl: string
printerDevice: string
copies: number
forcePageSizeByDevice: Record<string, boolean>
}
function loadConfig(): PrinterConfig {
try {
const raw = localStorage.getItem(CONFIG_KEY)
if (raw) return JSON.parse(raw)
} catch {}
return { enabled: false, serviceUrl: 'http://localhost:17521', printerDevice: '', copies: 1, forcePageSizeByDevice: {} }
}
function saveConfig(config: PrinterConfig) {
localStorage.setItem(CONFIG_KEY, JSON.stringify(config))
}
// ---- 模块级单例状态 ----
const config = ref<PrinterConfig>(loadConfig())
const connectionState = ref<'idle' | 'connecting' | 'connected' | 'error'>('idle')
const lastError = ref<string>('')
const devices = ref<string[]>([])
function persistConfig() {
saveConfig(config.value)
}
// ---- 连接 ----
async function connect(): Promise<void> {
if (connectionState.value === 'connected') return
connectionState.value = 'connecting'
lastError.value = ''
try {
hiprint.hiwebSocket.setHost(config.value.serviceUrl, NAMESPACE)
// 等待 WebSocket 打开(最多 4 秒)
await new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error('连接超时,请确认 electron-hiprint 已启动')), 4000)
const check = setInterval(() => {
if (hiprint.hiwebSocket.opened) {
clearInterval(check)
clearTimeout(timeout)
resolve()
}
}, 100)
})
connectionState.value = 'connected'
await refreshDevices()
} catch (err) {
connectionState.value = 'error'
lastError.value = err instanceof Error ? err.message : '连接失败'
throw err
}
}
// ---- 刷新打印机列表 ----
async function refreshDevices(): Promise<void> {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error('获取打印机列表超时')), 2500)
setTimeout(() => {
hiprint.refreshPrinterList()
setTimeout(() => {
clearTimeout(timeout)
const list = hiprint.printers?.map((p: any) => p.name) ?? []
devices.value = list.filter(Boolean)
// 如果当前选中的打印机不在列表中,回退到默认或第一个
if (config.value.printerDevice && !devices.value.includes(config.value.printerDevice)) {
config.value.printerDevice = devices.value[0] ?? ''
persistConfig()
}
resolve()
}, 300)
}, 300)
})
}
// ---- 打印单页 HTML ----
async function printHtml(html: string, options: {
width: number // mm
height: number // mm
printer: string
landscape?: boolean
forcePageSize?: boolean
}): Promise<void> {
const tpl = new hiprint.PrintTemplate()
const panel = tpl.addPrintPanel({
width: options.width,
height: options.height,
paperNumberDisabled: true,
})
panel.addPrintHtml({ options: { content: html } })
const printOptions: Record<string, any> = {
printer: options.printer,
margins: { marginType: 'none' },
}
// 标签机需要强制指定纸张尺寸(微米)
if (options.forcePageSize) {
printOptions.pageSize = {
width: Math.round(options.width * 1000),
height: Math.round(options.height * 1000),
}
printOptions.landscape = options.landscape ?? false
printOptions.scaleFactor = 100
}
return new Promise<void>((resolve, reject) => {
tpl.on('printSuccess', () => resolve())
tpl.on('printError', (e: any) => reject(new Error(String(e))))
tpl.print2({}, printOptions)
})
}
// ---- 打印多个 DOM 页面元素 ----
async function printPages(
pages: HTMLElement[],
options: { width: number; height: number; printer: string; landscape?: boolean; forcePageSize?: boolean },
onProgress?: (progress: { current: number; total: number }) => void,
): Promise<void> {
for (let i = 0; i < pages.length; i++) {
await printHtml(pages[i].innerHTML, options)
onProgress?.({ current: i + 1, total: pages.length })
}
}
// ---- 工具函数 ----
function isForcePageSize(printerName: string): boolean {
return config.value.forcePageSizeByDevice[printerName] ?? false
}
function setForcePageSize(printerName: string, force: boolean) {
config.value.forcePageSizeByDevice[printerName] = force
persistConfig()
}
function setEnabled(enabled: boolean) {
config.value.enabled = enabled
persistConfig()
}
// ---- 导出单例 ----
export function usePrinter() {
return {
config,
connectionState,
isConnected: computed(() => connectionState.value === 'connected'),
isConnecting: computed(() => connectionState.value === 'connecting'),
isError: computed(() => connectionState.value === 'error'),
lastError,
devices,
enabled: computed(() => config.value.enabled),
printerDevice: computed(() => config.value.printerDevice),
copies: computed(() => config.value.copies),
serviceUrl: computed(() => config.value.serviceUrl),
connect,
refreshDevices,
printHtml,
printPages,
setEnabled,
setForcePageSize,
isForcePageSize,
}
}第四步:实现 PrintDriver
ts
// src/drivers/hiprint-print-driver.ts
import type { PrintDriver, ViewerPrintContext } from '@easyink/viewer'
import { usePrinter } from '../hooks/useHiPrint'
const UNIT_TO_MM: Record<string, number> = {
cm: 10, in: 25.4, inch: 25.4, mm: 1, pt: 0.352778, px: 25.4 / 96,
}
function toMillimeters(value: number, unit: string): number {
return value * (UNIT_TO_MM[unit] || 1)
}
function getViewerPages(container: HTMLElement | undefined): HTMLElement[] {
if (!container) throw new Error('找不到打印内容')
const pages = Array.from(container.querySelectorAll<HTMLElement>('.ei-viewer-page'))
if (pages.length === 0) throw new Error('没有可输出的页面')
return pages
}
export function createHiPrintDriver(): PrintDriver {
const printer = usePrinter()
return {
id: 'hiprint-driver',
async print(context: ViewerPrintContext) {
if (!printer.enabled.value) throw new Error('打印服务未启用')
if (!printer.isConnected.value) await printer.connect()
if (!printer.printerDevice.value) throw new Error('未选择打印机')
const pages = getViewerPages(context.container)
const sheetSize = context.printPolicy.sheetSize ?? context.renderedPages[0]
if (!sheetSize) throw new Error('缺少打印页面尺寸')
const width = toMillimeters(sheetSize.width, sheetSize.unit)
const height = toMillimeters(sheetSize.height, sheetSize.unit)
context.onPhase?.({ phase: 'printing', message: 'HiPrint 打印中' })
await printer.printPages(pages, {
width,
height,
orientation: context.printPolicy.orientation,
printer: printer.printerDevice.value,
forcePageSize: printer.isForcePageSize(printer.printerDevice.value),
}, (progress) => {
context.onProgress?.({ ...progress, message: 'HiPrint 打印中' })
})
},
}
}第五步:注册驱动并调用打印
ts
import { createHiPrintDriver } from './drivers/hiprint-print-driver'
// 在 Viewer 初始化后注册
runtime.registerPrintDriver(createHiPrintDriver())
// 调用打印
await runtime.print({ driverId: 'hiprint-driver', pageSizeMode: 'driver' })标签机配置
DELI 等标签打印机可能忽略模板尺寸回退到 A4 缩印。解决方法:在设置中对目标打印机开启「强制使用模板纸张尺寸」,驱动会自动传入 pageSize(微米单位)。
TIP
普通小票机、连续纸打印机不要开启此选项。传了 pageSize 反而会让驱动找不到匹配介质,把内容裁掉。只对标签机开启。
Playground 完整示例
项目 Playground 已完整集成,可直接体验:
- 启动 electron-hiprint 客户端(端口 17521)
- 启动 Playground(
pnpm dev) - 预览界面 -> 打印下拉菜单 -> 「HiPrint 打印」
- 在设置对话框中配置连接地址和打印机
相关源码:
playground/src/hooks/useHiPrint.ts-- 连接管理和打印 APIplayground/src/drivers/hiprint-print-driver.ts-- PrintDriver 实现playground/src/components/HiPrintSettingsDialog.vue-- 设置界面
常见问题
连接超时
- 确认 electron-hiprint 客户端已启动并监听 17521 端口
- 检查防火墙是否放行该端口
未发现打印机
- 确认系统打印机已正常安装
- 调用
refreshDevices()重新获取设备列表
标签机打印内容被截断 / 缩印到 A4
- 对该打印机开启「强制使用模板纸张尺寸」