@zinghub/zing-print 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ {
2
+ "recommendations": ["Vue.volar"]
3
+ }
package/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # @zinghub/zing-print
2
+
3
+ 一个基于 Vue3 + Vite + TypeScript 的组件库基础骨架,使用 Ant Design Vue 作为 UI 基础库,支持按需引入与 app.use() 安装,采用 Vite library 模式打包(ES + UMD)。
4
+
5
+ 本仓库同时包含一个“企业级标签打印”示例页面(PrintSetting)与标签模板(Template1/2/3),用于演示配置、预览、以及通过 QZ Tray 打印。
6
+
7
+ ## 安装与构建
8
+
9
+ - 安装依赖
10
+
11
+ ```bash
12
+ pnpm install
13
+ ```
14
+
15
+ - 本地开发预览(默认入口为 PrintSetting 页面)
16
+
17
+ ```bash
18
+ pnpm dev
19
+ ```
20
+
21
+ - 构建输出(生成 dist/index.es.js、dist/index.umd.js、dist/index.d.ts)
22
+
23
+ ```bash
24
+ pnpm build
25
+ ```
26
+
27
+ ## 使用方式
28
+
29
+ ### 作为组件库使用(发布到 npm 后)
30
+
31
+ - 全局安装(app.use)
32
+
33
+ ```ts
34
+ import { createApp } from 'vue'
35
+ import ZingPrint from '@zinghub/zing-print'
36
+
37
+ const app = createApp(/* root */)
38
+ app.use(ZingPrint)
39
+ ```
40
+
41
+ - 按需引入
42
+
43
+ ```ts
44
+ import { PrintLabel } from '@zinghub/zing-print'
45
+ ```
46
+
47
+ ### 企业级标签打印示例(仓库内)
48
+
49
+ 示例页面位于 `src/print/pages/PrintSetting.vue`,当前 `src/App.vue` 已默认渲染该页面,直接运行 `pnpm dev` 即可使用。
50
+
51
+ 功能点:
52
+
53
+ - 打印机管理:连接 QZ Tray、刷新打印机列表、保存默认打印机(localStorage:`default-printer`)
54
+ - 模板设置:字体大小、行间距、条码/二维码尺寸、标签尺寸(支持 60×40 基准等比例联动)
55
+ - 预览:使用 Ant Design Vue Modal 预览 LabelPrinter 渲染结果
56
+ - 打印测试:生成 HTML 并通过 QZ Tray 调用打印
57
+
58
+ 相关模块:
59
+
60
+ - `LabelPrinter`:`src/print/components/LabelPrinter.vue`(模板统一入口)
61
+ - `Template1/2/3`:`src/print/components/Template*.vue`(模板实现,Template1 为完整示例)
62
+ - `PrintSetting`:`src/print/pages/PrintSetting.vue`(设置页与预览/打印)
63
+ - `QZ Tray` 工具:`src/print/utils/qz.ts`
64
+ - 配置持久化:`src/print/utils/printConfig.ts`(localStorage:`print-config`)
65
+
66
+ 注意:上述 “print” 目录内容目前作为仓库内示例存在,尚未从 `src/index.ts` 对外导出;如果要作为 npm 包能力对外提供,可以后续将相关组件/工具纳入导出并补充类型定义与打包策略。
67
+
68
+ ## QZ Tray 使用说明
69
+
70
+ 本项目通过 `qz-tray`(浏览器端 SDK)连接本机 QZ Tray 服务,进行打印机枚举与打印。
71
+
72
+ 1. 安装并启动 QZ Tray
73
+ - 下载地址:https://qz.io/download/
74
+ - 启动后保持后台运行
75
+ 2. 连接策略
76
+ - 优先尝试 `wss://localhost:8182`
77
+ - 若失败则回退到 `ws://localhost:8182`,再回退到默认 ws 连接方式
78
+ 3. 常见注意事项
79
+ - 访问页面是 `https` 时,浏览器通常会阻止连接 `ws`(非安全 WebSocket);建议优先确保 `wss` 可用
80
+ - 若一直提示未检测到服务:确认 QZ Tray 已启动、端口未被占用、以及浏览器/系统防火墙未拦截
81
+
82
+ ## 目录结构
83
+
84
+ ```
85
+ src/
86
+ components/
87
+ PrintLabel/
88
+ PrintLabel.vue // 空组件模板
89
+ print/
90
+ components/ // LabelPrinter + Template1/2/3
91
+ pages/ // PrintSetting
92
+ utils/ // qz.ts + printConfig.ts
93
+ index.ts // 组件库入口与 install
94
+ vite.config.ts // library 模式配置(external: vue, ant-design-vue)
95
+ tsconfig.json // TS 配置
96
+ ```
@@ -0,0 +1,20 @@
1
+ import { defineComponent as r, openBlock as s, createElementBlock as a } from "vue";
2
+ const _ = { class: "zing-print-label" }, l = /* @__PURE__ */ r({
3
+ __name: "PrintLabel",
4
+ setup(t) {
5
+ return (n, e) => (s(), a("div", _));
6
+ }
7
+ }), i = (t, n) => {
8
+ const e = t.__vccOpts || t;
9
+ for (const [o, c] of n)
10
+ e[o] = c;
11
+ return e;
12
+ }, p = /* @__PURE__ */ i(l, [["__scopeId", "data-v-6469415f"]]), f = {
13
+ install(t) {
14
+ t.component("PrintLabel", p);
15
+ }
16
+ };
17
+ export {
18
+ p as PrintLabel,
19
+ f as default
20
+ };
@@ -0,0 +1 @@
1
+ (function(e,t){typeof exports=="object"&&typeof module<"u"?t(exports,require("vue")):typeof define=="function"&&define.amd?define(["exports","vue"],t):(e=typeof globalThis<"u"?globalThis:e||self,t(e["zing-print"]={},e.Vue))})(this,function(e,t){"use strict";const c={class:"zing-print-label"},i=((n,s)=>{const o=n.__vccOpts||n;for(const[_,f]of s)o[_]=f;return o})(t.defineComponent({__name:"PrintLabel",setup(n){return(s,o)=>(t.openBlock(),t.createElementBlock("div",c))}}),[["__scopeId","data-v-6469415f"]]),r={install(n){n.component("PrintLabel",i)}};e.PrintLabel=i,e.default=r,Object.defineProperties(e,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
package/dist/style.css ADDED
File without changes
package/dist/vite.svg ADDED
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
package/index.html ADDED
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>zing-print</title>
8
+ </head>
9
+ <body>
10
+ <div id="app"></div>
11
+ <script type="module" src="/src/main.ts"></script>
12
+ </body>
13
+ </html>
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@zinghub/zing-print",
3
+ "version": "0.1.0",
4
+ "description": "ERP 打印组件库基础骨架(Vue3 + Vite + TS + Ant Design Vue)",
5
+ "type": "module",
6
+ "private": false,
7
+ "main": "dist/index.umd.js",
8
+ "module": "dist/index.es.js",
9
+ "types": "dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.es.js",
14
+ "require": "./dist/index.umd.js"
15
+ }
16
+ },
17
+ "peerDependencies": {
18
+ "ant-design-vue": "^4.0.0",
19
+ "vue": "^3.3.0"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^18.19.0",
23
+ "@vitejs/plugin-vue": "^5.0.0",
24
+ "@vue/tsconfig": "^0.8.1",
25
+ "ant-design-vue": "^4.0.0",
26
+ "typescript": "^5.3.0",
27
+ "vite": "^5.0.0",
28
+ "vue": "^3.3.0",
29
+ "vue-tsc": "^1.8.0"
30
+ },
31
+ "sideEffects": false,
32
+ "browser": {
33
+ "path": false
34
+ },
35
+ "dependencies": {
36
+ "@ant-design/icons-vue": "^7.0.1",
37
+ "jsbarcode": "^3.12.3",
38
+ "qrcode": "^1.5.4",
39
+ "qz-tray": "^2.2.4"
40
+ },
41
+ "scripts": {
42
+ "dev": "vite",
43
+ "build": "vite build && tsc -p tsconfig.decls.json",
44
+ "preview": "vite preview",
45
+ "typecheck": "tsc -p tsconfig.app.json --noEmit"
46
+ }
47
+ }
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
package/src/App.vue ADDED
@@ -0,0 +1,7 @@
1
+ <script setup lang="ts">
2
+ import PrintSetting from './print/pages/PrintSetting.vue'
3
+ </script>
4
+
5
+ <template>
6
+ <PrintSetting />
7
+ </template>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
@@ -0,0 +1,41 @@
1
+ <script setup lang="ts">
2
+ import { ref } from 'vue'
3
+
4
+ defineProps<{ msg: string }>()
5
+
6
+ const count = ref(0)
7
+ </script>
8
+
9
+ <template>
10
+ <h1>{{ msg }}</h1>
11
+
12
+ <div class="card">
13
+ <button type="button" @click="count++">count is {{ count }}</button>
14
+ <p>
15
+ Edit
16
+ <code>components/HelloWorld.vue</code> to test HMR
17
+ </p>
18
+ </div>
19
+
20
+ <p>
21
+ Check out
22
+ <a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
23
+ >create-vue</a
24
+ >, the official Vue + Vite starter
25
+ </p>
26
+ <p>
27
+ Learn more about IDE Support for Vue in the
28
+ <a
29
+ href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
30
+ target="_blank"
31
+ >Vue Docs Scaling up Guide</a
32
+ >.
33
+ </p>
34
+ <p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
35
+ </template>
36
+
37
+ <style scoped>
38
+ .read-the-docs {
39
+ color: #888;
40
+ }
41
+ </style>
@@ -0,0 +1,14 @@
1
+ <template>
2
+ <div class="zing-print-label">
3
+ <!-- 空组件模板:未来用于打印标签 -->
4
+ </div>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ // 预留:后续可加入 props、事件与逻辑
9
+ </script>
10
+
11
+ <style scoped>
12
+ .zing-print-label {
13
+ }
14
+ </style>
package/src/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ import type { App } from 'vue'
2
+ import PrintLabel from './components/PrintLabel/PrintLabel.vue'
3
+
4
+ export { PrintLabel }
5
+
6
+ export default {
7
+ install(app: App) {
8
+ app.component('PrintLabel', PrintLabel)
9
+ }
10
+ }
package/src/main.ts ADDED
@@ -0,0 +1,9 @@
1
+ import { createApp } from 'vue'
2
+ import './style.css'
3
+ import App from './App.vue'
4
+ import Antd from 'ant-design-vue'
5
+ import 'ant-design-vue/dist/reset.css'
6
+
7
+ const app = createApp(App)
8
+ app.use(Antd)
9
+ app.mount('#app')
@@ -0,0 +1,58 @@
1
+ <template>
2
+ <component
3
+ :is="current"
4
+ v-bind="{
5
+ text1, value1,
6
+ text2, value2,
7
+ text3, value3,
8
+ text4, value4,
9
+ text5, value5,
10
+ barcode, qrcode,
11
+ fontSize,
12
+ lineGapMm,
13
+ barcodeWidth,
14
+ barcodeHeight,
15
+ qrcodeWidth,
16
+ qrcodeHeight,
17
+ labelWidthMm,
18
+ labelHeightMm
19
+ }"
20
+ />
21
+ </template>
22
+
23
+ <script setup lang="ts">
24
+ import { computed } from 'vue'
25
+ import Template1 from './Template1.vue'
26
+ import Template2 from './Template2.vue'
27
+ import Template3 from './Template3.vue'
28
+
29
+ const props = defineProps<{
30
+ template: number
31
+ text1: string
32
+ value1: string
33
+ text2: string
34
+ value2: string
35
+ text3: string
36
+ value3: string
37
+ text4: string
38
+ value4: string
39
+ text5: string
40
+ value5: string
41
+ barcode: string
42
+ qrcode: string
43
+ fontSize?: number
44
+ lineGapMm?: number
45
+ barcodeWidth?: number
46
+ barcodeHeight?: number
47
+ qrcodeWidth?: number
48
+ qrcodeHeight?: number
49
+ labelWidthMm?: number
50
+ labelHeightMm?: number
51
+ }>()
52
+
53
+ const current = computed(() => {
54
+ if (props.template === 2) return Template2
55
+ if (props.template === 3) return Template3
56
+ return Template1
57
+ })
58
+ </script>
@@ -0,0 +1,134 @@
1
+ <template>
2
+ <div class="label-root" :style="rootStyle">
3
+ <div class="grid" :style="{ rowGap: lineGap, gridTemplateColumns: gridCols }">
4
+ <div class="left">
5
+ <div class="row"><span class="k">{{ props.text1 }}</span><span class="v">{{ props.value1 }}</span></div>
6
+ <div class="row"><span class="k">{{ props.text2 }}</span><span class="v">{{ props.value2 }}</span></div>
7
+ <div class="row"><span class="k">{{ props.text3 }}</span><span class="v">{{ props.value3 }}</span></div>
8
+ <div class="row"><span class="k">{{ props.text4 }}</span><span class="v">{{ props.value4 }}</span></div>
9
+ <div class="row"><span class="k">{{ props.text5 }}</span><span class="v">{{ props.value5 }}</span></div>
10
+ </div>
11
+ <div class="right">
12
+ <img v-if="qrDataUrl" :src="qrDataUrl" :style="qrStyle" />
13
+ </div>
14
+ </div>
15
+ <div class="barcode">
16
+ <img v-if="barDataUrl" :src="barDataUrl" :style="barStyle" />
17
+ </div>
18
+ </div>
19
+ </template>
20
+
21
+ <script setup lang="ts">
22
+ import { computed, watchEffect, ref } from 'vue'
23
+ import QRCode from 'qrcode'
24
+ import JsBarcode from 'jsbarcode'
25
+
26
+ const props = defineProps<{
27
+ text1: string
28
+ value1: string
29
+ text2: string
30
+ value2: string
31
+ text3: string
32
+ value3: string
33
+ text4: string
34
+ value4: string
35
+ text5: string
36
+ value5: string
37
+ barcode: string
38
+ qrcode: string
39
+ fontSize?: number
40
+ lineGapMm?: number
41
+ barcodeWidth?: number
42
+ barcodeHeight?: number
43
+ qrcodeWidth?: number
44
+ qrcodeHeight?: number
45
+ labelWidthMm?: number
46
+ labelHeightMm?: number
47
+ }>()
48
+
49
+ const mmToPx = (mm: number) => Math.round((mm / 25.4) * 96)
50
+
51
+ const qrDataUrl = ref<string>('')
52
+ const barDataUrl = ref<string>('')
53
+
54
+ watchEffect(async () => {
55
+ if (props.qrcode) {
56
+ const w = mmToPx(props.qrcodeWidth ?? 30)
57
+ qrDataUrl.value = await QRCode.toDataURL(props.qrcode, { margin: 0, width: w })
58
+ } else {
59
+ qrDataUrl.value = ''
60
+ }
61
+ })
62
+
63
+ watchEffect(async () => {
64
+ if (props.barcode) {
65
+ const canvas = document.createElement('canvas')
66
+ JsBarcode(canvas, props.barcode, { width: 2, height: mmToPx(props.barcodeHeight ?? 12), displayValue: false, margin: 0 })
67
+ barDataUrl.value = canvas.toDataURL('image/png')
68
+ } else {
69
+ barDataUrl.value = ''
70
+ }
71
+ })
72
+
73
+ const rootStyle = computed(() => ({
74
+ width: `${props.labelWidthMm ?? 60}mm`,
75
+ height: `${props.labelHeightMm ?? 40}mm`,
76
+ fontSize: `${props.fontSize ?? 12}px`,
77
+ padding: '4mm',
78
+ border: '1px solid #000'
79
+ }))
80
+
81
+ const lineGap = computed(() => `${props.lineGapMm ?? 1}mm`)
82
+
83
+ const gridCols = computed(() => `1fr ${props.qrcodeWidth ?? 20}mm`)
84
+
85
+ const qrStyle = computed(() => ({
86
+ width: `${props.qrcodeWidth ?? 20}mm`,
87
+ height: `${props.qrcodeHeight ?? 20}mm`
88
+ }))
89
+
90
+ const barStyle = computed(() => ({
91
+ width: `${props.barcodeWidth ?? 50}mm`,
92
+ height: `${props.barcodeHeight ?? 12}mm`
93
+ }))
94
+ </script>
95
+
96
+ <style scoped>
97
+ .label-root {
98
+ box-sizing: border-box;
99
+ display: flex;
100
+ flex-direction: column;
101
+ gap: 3mm;
102
+ background: #fff;
103
+ }
104
+ .grid {
105
+ display: grid;
106
+ grid-template-columns: 1fr 120px;
107
+ gap: 2mm;
108
+ }
109
+ .left {
110
+ display: flex;
111
+ flex-direction: column;
112
+ }
113
+ .row {
114
+ display: flex;
115
+ gap: 2mm;
116
+ }
117
+ .k {
118
+ font-weight: 600;
119
+ }
120
+ .v {
121
+ flex: 1;
122
+ }
123
+ .right {
124
+ display: flex;
125
+ align-items: center;
126
+ justify-content: center;
127
+ }
128
+ .barcode {
129
+ display: flex;
130
+ align-items: center;
131
+ justify-content: center;
132
+ margin-top: auto;
133
+ }
134
+ </style>
@@ -0,0 +1,30 @@
1
+ <template>
2
+ <div style="width:60mm;height:40mm;display:flex;align-items:center;justify-content:center;border:1px solid #000;">
3
+ <div>模板2</div>
4
+ </div>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ defineProps<{
9
+ text1: string
10
+ value1: string
11
+ text2: string
12
+ value2: string
13
+ text3: string
14
+ value3: string
15
+ text4: string
16
+ value4: string
17
+ text5: string
18
+ value5: string
19
+ barcode: string
20
+ qrcode: string
21
+ fontSize?: number
22
+ lineGapMm?: number
23
+ barcodeWidth?: number
24
+ barcodeHeight?: number
25
+ qrcodeWidth?: number
26
+ qrcodeHeight?: number
27
+ labelWidthMm?: number
28
+ labelHeightMm?: number
29
+ }>()
30
+ </script>
@@ -0,0 +1,30 @@
1
+ <template>
2
+ <div style="width:60mm;height:40mm;display:flex;align-items:center;justify-content:center;border:1px solid #000;">
3
+ <div>模板3</div>
4
+ </div>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ defineProps<{
9
+ text1: string
10
+ value1: string
11
+ text2: string
12
+ value2: string
13
+ text3: string
14
+ value3: string
15
+ text4: string
16
+ value4: string
17
+ text5: string
18
+ value5: string
19
+ barcode: string
20
+ qrcode: string
21
+ fontSize?: number
22
+ lineGapMm?: number
23
+ barcodeWidth?: number
24
+ barcodeHeight?: number
25
+ qrcodeWidth?: number
26
+ qrcodeHeight?: number
27
+ labelWidthMm?: number
28
+ labelHeightMm?: number
29
+ }>()
30
+ </script>
@@ -0,0 +1,330 @@
1
+ <template>
2
+ <div style="margin:50px 30px;box-sizing:border-box">
3
+ <a-card :bordered="false" style="width:100%;margin-bottom:16px">
4
+ <div style="margin-bottom:12px;font-size:18px;font-weight:600">打印设置</div>
5
+ <a-space direction="vertical" style="width:100%">
6
+ <a-space>
7
+ <a-select
8
+ v-model:value="selectedPrinter"
9
+ :options="printerOptions"
10
+ style="min-width:420px"
11
+ placeholder="请选择打印机"
12
+ :loading="loadingPrinters"
13
+ allow-clear
14
+ show-search
15
+ option-filter-prop="label"
16
+ :filter-option="filterPrinter"
17
+ />
18
+ <a-button :loading="loadingPrinters" @click="onRefreshPrinters">刷新列表</a-button>
19
+ <a-button type="primary" :loading="savingPrinter" @click="onSavePrinter">保存</a-button>
20
+ </a-space>
21
+ <a-alert type="info" show-icon message="若未启动 QZ Tray,请先启动后再点击刷新列表/保存。" />
22
+ <a-alert v-if="savedPrinterName" type="success" show-icon :message="`已保存默认打印机:${savedPrinterName}`" />
23
+ <a-alert :type="qzConnected ? 'success' : 'error'" show-icon :message="qzConnected ? '🟢 打印服务已连接' : '🔴 打印服务未连接'" />
24
+ </a-space>
25
+ </a-card>
26
+
27
+ <a-card :bordered="false" title="打印模板1设置" style="width:100%">
28
+ <a-space direction="vertical" style="width:100%">
29
+ <a-space>
30
+ <div style="width:110px">字体大小</div>
31
+ <a-input-number v-model:value="model.fontSize" :min="8" :max="28" :precision="0" style="width:240px" />
32
+ <a-button @click="onPreview">预览</a-button>
33
+ <a-button :loading="testingPrint" @click="onTestPrint">打印测试</a-button>
34
+ <a-button type="primary" :loading="savingTemplate1" @click="onSaveTemplate1">保存</a-button>
35
+ </a-space>
36
+
37
+ <a-space>
38
+ <div style="width:110px">标签尺寸</div>
39
+ <a-input-number v-model:value="labelWidthMm" :min="10" :max="200" :precision="1" :step="0.1" style="width:115px" />
40
+ <span>×</span>
41
+ <a-input-number v-model:value="labelHeightMm" :min="10" :max="200" :precision="1" :step="0.1" style="width:115px" />
42
+ <span>mm</span>
43
+ <a-switch v-model:checked="model.keepLabelRatio" checked-children="等比例" un-checked-children="自由" />
44
+ </a-space>
45
+
46
+ <a-space>
47
+ <div style="width:110px">字体上下间距</div>
48
+ <a-input-number v-model:value="model.lineGapMm" :min="0" :max="3" :precision="1" :step="0.1" style="width:240px" />
49
+ <span>mm</span>
50
+ </a-space>
51
+
52
+ <a-space>
53
+ <div style="width:110px">条形码尺寸</div>
54
+ <a-input-number v-model:value="model.barcodeWidth" :min="10" :max="60" :precision="0" style="width:115px" />
55
+ <span>×</span>
56
+ <a-input-number v-model:value="model.barcodeHeight" :min="6" :max="20" :precision="0" style="width:115px" />
57
+ <span>mm</span>
58
+ </a-space>
59
+
60
+ <a-space>
61
+ <div style="width:110px">二维码尺寸</div>
62
+ <a-input-number v-model:value="model.qrcodeWidth" :min="10" :max="30" :precision="0" style="width:115px" />
63
+ <span>×</span>
64
+ <a-input-number v-model:value="model.qrcodeHeight" :min="10" :max="30" :precision="0" style="width:115px" />
65
+ <span>mm</span>
66
+ </a-space>
67
+ </a-space>
68
+
69
+ <div style="position:fixed;left:-9999px;top:-9999px">
70
+ <LabelPrinter
71
+ ref="labelPrinterRef"
72
+ :template="1"
73
+ text1="产品"
74
+ value1="苹果"
75
+ text2="批次"
76
+ value2="A001"
77
+ text3="日期"
78
+ value3="2024-01-01"
79
+ text4="规格"
80
+ value4="500g"
81
+ text5="产地"
82
+ value5="山东"
83
+ qrcode="123456789"
84
+ barcode="123456789"
85
+ :font-size="model.fontSize"
86
+ :lineGapMm="model.lineGapMm"
87
+ :barcode-width="model.barcodeWidth"
88
+ :barcode-height="model.barcodeHeight"
89
+ :qrcode-width="model.qrcodeWidth"
90
+ :qrcode-height="model.qrcodeHeight"
91
+ :label-width-mm="model.labelWidthMm"
92
+ :label-height-mm="model.labelHeightMm"
93
+ />
94
+ </div>
95
+
96
+ <a-modal v-model:open="previewOpen" title="标签预览" :footer="null" width="520px">
97
+ <div style="display:flex;justify-content:center;padding:16px">
98
+ <div style="display:flex;flex-direction:column;gap:12px;align-items:center">
99
+ <div v-if="previewItems.length === 0" style="color:rgba(0,0,0,0.45)">暂无预览</div>
100
+ <div
101
+ v-else
102
+ v-for="(it, idx) in previewItems"
103
+ :key="idx"
104
+ style="border:1px dashed #d9d9d9;padding:8px;background:#fff"
105
+ >
106
+ <LabelPrinter
107
+ :template="1"
108
+ :text1="it.text1"
109
+ :value1="it.value1"
110
+ :text2="it.text2"
111
+ :value2="it.value2"
112
+ :text3="it.text3"
113
+ :value3="it.value3"
114
+ :text4="it.text4"
115
+ :value4="it.value4"
116
+ :text5="it.text5"
117
+ :value5="it.value5"
118
+ :qrcode="it.qrcode"
119
+ :barcode="it.barcode"
120
+ :font-size="model.fontSize"
121
+ :lineGapMm="model.lineGapMm"
122
+ :barcode-width="model.barcodeWidth"
123
+ :barcode-height="model.barcodeHeight"
124
+ :qrcode-width="model.qrcodeWidth"
125
+ :qrcode-height="model.qrcodeHeight"
126
+ :label-width-mm="model.labelWidthMm"
127
+ :label-height-mm="model.labelHeightMm"
128
+ />
129
+ </div>
130
+ </div>
131
+ </div>
132
+ </a-modal>
133
+ </a-card>
134
+
135
+ <a-modal
136
+ v-model:open="qzModalOpen"
137
+ title="未检测到打印服务"
138
+ :footer="null"
139
+ :closable="false"
140
+ :maskClosable="false"
141
+ width="480px"
142
+ >
143
+ <div style="text-align:center;padding:24px 0">
144
+ <ExclamationCircleFilled style="font-size:48px;color:#faad14;margin-bottom:16px" />
145
+ <div style="font-size:16px;font-weight:500;margin-bottom:8px">无法连接 QZ Tray 打印服务</div>
146
+ <div style="color:#666;margin-bottom:24px">请确保 QZ Tray 已安装并启动,或者下载安装。</div>
147
+ <a-space size="middle">
148
+ <a-button type="primary" size="large" @click="tryLaunchQZ">我已安装,尝试启动</a-button>
149
+ <a-button size="large" @click="downloadQZ">未安装,去下载</a-button>
150
+ </a-space>
151
+ <div style="margin-top:16px">
152
+ <a-button type="link" @click="onRefreshPrinters">已启动,点此重试</a-button>
153
+ </div>
154
+ </div>
155
+ </a-modal>
156
+ </div>
157
+ </template>
158
+
159
+ <script setup lang="ts">
160
+ import { computed, onMounted, ref, watch } from 'vue'
161
+ import { ExclamationCircleFilled } from '@ant-design/icons-vue'
162
+ import { getConfig, saveConfig } from '../utils/printConfig'
163
+ import { connectQZ, printHTML, detectDriver, listPrinters } from '../utils/qz'
164
+ import LabelPrinter from '../components/LabelPrinter.vue'
165
+ import QRCode from 'qrcode'
166
+ import JsBarcode from 'jsbarcode'
167
+
168
+ const model = ref(getConfig())
169
+ const previewOpen = ref(false)
170
+ const previewItems = ref([
171
+ { text1: '产品', value1: '苹果', text2: '批次', value2: 'A001', text3: '日期', value3: '2024-01-01', text4: '规格', value4: '500g', text5: '产地', value5: '山东', qrcode: '123456789', barcode: '123456789' }
172
+ ])
173
+
174
+ const selectedPrinter = ref<string | undefined>(localStorage.getItem('default-printer') || undefined)
175
+ const printerOptions = ref<{ label: string; value: string }[]>([])
176
+ const loadingPrinters = ref(false)
177
+ const savingPrinter = ref(false)
178
+ const qzConnected = ref(false)
179
+ const qzModalOpen = ref(false)
180
+ const testingPrint = ref(false)
181
+ const savingTemplate1 = ref(false)
182
+ const labelPrinterRef = ref()
183
+
184
+ const BASE_RATIO = 40 / 60
185
+ const round1 = (n: number) => Math.round(n * 10) / 10
186
+
187
+ const labelWidthMm = computed({
188
+ get: () => model.value.labelWidthMm,
189
+ set: (v) => {
190
+ const next = typeof v === 'number' && Number.isFinite(v) ? v : 60
191
+ model.value.labelWidthMm = next
192
+ if (model.value.keepLabelRatio) {
193
+ model.value.labelHeightMm = round1(next * BASE_RATIO)
194
+ }
195
+ }
196
+ })
197
+
198
+ const labelHeightMm = computed({
199
+ get: () => model.value.labelHeightMm,
200
+ set: (v) => {
201
+ const next = typeof v === 'number' && Number.isFinite(v) ? v : 40
202
+ model.value.labelHeightMm = next
203
+ if (model.value.keepLabelRatio) {
204
+ model.value.labelWidthMm = round1(next / BASE_RATIO)
205
+ }
206
+ }
207
+ })
208
+
209
+ watch(
210
+ () => model.value.keepLabelRatio,
211
+ (on) => {
212
+ if (on) {
213
+ model.value.labelHeightMm = round1(model.value.labelWidthMm * BASE_RATIO)
214
+ }
215
+ }
216
+ )
217
+
218
+ function filterPrinter(input: string, option: any) {
219
+ return (option?.label ?? '').toLowerCase().includes(input.toLowerCase())
220
+ }
221
+
222
+ async function onRefreshPrinters() {
223
+ loadingPrinters.value = true
224
+ try {
225
+ await connectQZ()
226
+ qzConnected.value = true
227
+ const list = await listPrinters()
228
+ printerOptions.value = list.map((n) => ({ label: n, value: n }))
229
+ } catch {
230
+ qzConnected.value = false
231
+ qzModalOpen.value = true
232
+ } finally {
233
+ loadingPrinters.value = false
234
+ }
235
+ }
236
+
237
+ function onSavePrinter() {
238
+ savingPrinter.value = true
239
+ try {
240
+ if (selectedPrinter.value) {
241
+ localStorage.setItem('default-printer', selectedPrinter.value)
242
+ } else {
243
+ localStorage.removeItem('default-printer')
244
+ }
245
+ } finally {
246
+ savingPrinter.value = false
247
+ }
248
+ }
249
+
250
+ const savedPrinterName = computed(() => localStorage.getItem('default-printer') || '')
251
+
252
+ function onPreview() {
253
+ previewOpen.value = true
254
+ }
255
+
256
+ function onSaveTemplate1() {
257
+ savingTemplate1.value = true
258
+ try {
259
+ saveConfig(model.value)
260
+ } finally {
261
+ savingTemplate1.value = false
262
+ }
263
+ }
264
+
265
+ function mmToPx(mm: number) {
266
+ return Math.round((mm / 25.4) * 96)
267
+ }
268
+
269
+ async function buildHtml() {
270
+ const cfg = model.value
271
+ const qr = await QRCode.toDataURL('123456789', { margin: 0, width: mmToPx(cfg.qrcodeWidth) })
272
+ const canvas = document.createElement('canvas')
273
+ JsBarcode(canvas, '123456789', { width: 2, height: mmToPx(cfg.barcodeHeight), displayValue: false, margin: 0 })
274
+ const bar = canvas.toDataURL('image/png')
275
+ const style = `
276
+ <style>
277
+ .label { width:${cfg.labelWidthMm}mm; height:${cfg.labelHeightMm}mm; box-sizing:border-box; padding:4mm; border:1px solid #000; font-size:${cfg.fontSize}px; background:#fff; display:flex; flex-direction:column; }
278
+ .grid { display:grid; grid-template-columns:1fr ${cfg.qrcodeWidth}mm; gap:${cfg.lineGapMm}mm; }
279
+ .row { display:flex; gap:2mm; }
280
+ .barcode { display:flex; align-items:center; justify-content:center; margin-top:auto; }
281
+ </style>
282
+ `
283
+ const html = `
284
+ <div class="label">
285
+ <div class="grid">
286
+ <div>
287
+ <div class="row"><span>产品:</span><span>苹果</span></div>
288
+ <div class="row"><span>批次:</span><span>A001</span></div>
289
+ <div class="row"><span>日期:</span><span>2024-01-01</span></div>
290
+ <div class="row"><span>规格:</span><span>500g</span></div>
291
+ <div class="row"><span>产地:</span><span>山东</span></div>
292
+ </div>
293
+ <div style="display:flex;align-items:center;justify-content:center;">
294
+ <img src="${qr}" style="width:${cfg.qrcodeWidth}mm;height:${cfg.qrcodeHeight}mm" />
295
+ </div>
296
+ </div>
297
+ <div class="barcode">
298
+ <img src="${bar}" style="width:${cfg.barcodeWidth}mm;height:${cfg.barcodeHeight}mm" />
299
+ </div>
300
+ </div>
301
+ `
302
+ return style + html
303
+ }
304
+
305
+ async function onTestPrint() {
306
+ testingPrint.value = true
307
+ try {
308
+ await connectQZ()
309
+ const html = await buildHtml()
310
+ const def = localStorage.getItem('default-printer') || undefined
311
+ await printHTML(html, def)
312
+ } catch {
313
+ qzModalOpen.value = true
314
+ } finally {
315
+ testingPrint.value = false
316
+ }
317
+ }
318
+
319
+ function tryLaunchQZ() {
320
+ onRefreshPrinters()
321
+ }
322
+
323
+ function downloadQZ() {
324
+ window.open('https://qz.io/download/', '_blank')
325
+ }
326
+
327
+ onMounted(() => {
328
+ onRefreshPrinters()
329
+ })
330
+ </script>
@@ -0,0 +1,40 @@
1
+ export type PrintConfig = {
2
+ fontSize: number
3
+ lineGapMm: number
4
+ barcodeWidth: number
5
+ barcodeHeight: number
6
+ qrcodeWidth: number
7
+ qrcodeHeight: number
8
+ labelWidthMm: number
9
+ labelHeightMm: number
10
+ keepLabelRatio: boolean
11
+ }
12
+
13
+ const KEY = 'print-config'
14
+
15
+ const defaultConfig: PrintConfig = {
16
+ fontSize: 12,
17
+ lineGapMm: 1,
18
+ barcodeWidth: 50,
19
+ barcodeHeight: 12,
20
+ qrcodeWidth: 20,
21
+ qrcodeHeight: 20,
22
+ labelWidthMm: 60,
23
+ labelHeightMm: 40,
24
+ keepLabelRatio: true
25
+ }
26
+
27
+ export function getConfig(): PrintConfig {
28
+ try {
29
+ const raw = localStorage.getItem(KEY)
30
+ return raw ? { ...defaultConfig, ...JSON.parse(raw) } : defaultConfig
31
+ } catch {
32
+ return defaultConfig
33
+ }
34
+ }
35
+
36
+ export function saveConfig(cfg: Partial<PrintConfig>) {
37
+ const merged = { ...getConfig(), ...cfg }
38
+ localStorage.setItem(KEY, JSON.stringify(merged))
39
+ return merged
40
+ }
@@ -0,0 +1,55 @@
1
+ import qz from 'qz-tray'
2
+
3
+ let connected = false
4
+ let configured = false
5
+
6
+ function configureQZ() {
7
+ if (configured) return
8
+ try {
9
+ qz.api.setPromiseType((resolver: any) => new Promise(resolver))
10
+ } catch {}
11
+ configured = true
12
+ }
13
+
14
+ export async function connectQZ() {
15
+ if (connected && qz.websocket.isActive()) return
16
+ configureQZ()
17
+ try {
18
+ await qz.websocket.connect({ host: 'localhost', usingSecure: true, port: { secure: [8182] } })
19
+ } catch (eSecure8182) {
20
+ try {
21
+ await qz.websocket.connect({ host: 'localhost', usingSecure: false, port: { insecure: [8182] } })
22
+ } catch (eInsecure8182) {
23
+ try {
24
+ await qz.websocket.connect({ host: 'localhost', usingSecure: false })
25
+ } catch (err) {
26
+ connected = false
27
+ throw err
28
+ }
29
+ }
30
+ }
31
+ connected = true
32
+ }
33
+
34
+ export async function listPrinters(): Promise<string[]> {
35
+ await connectQZ()
36
+ const result = await qz.printers.find()
37
+ if (Array.isArray(result)) return result
38
+ if (typeof result === 'string' && result) return [result]
39
+ return []
40
+ }
41
+
42
+ export async function printHTML(html: string, printer?: string) {
43
+ await connectQZ()
44
+ const p = printer || (await qz.printers.getDefault())
45
+ const cfg = qz.configs.create(p)
46
+ return qz.print(cfg, [{ type: 'html', format: 'plain', data: html }])
47
+ }
48
+
49
+ export function detectDriver() {
50
+ const active = qz?.websocket?.isActive?.() === true
51
+ if (active) return true
52
+ const url = 'https://qz.io/download/'
53
+ window.open(url, '_blank')
54
+ return false
55
+ }
@@ -0,0 +1,5 @@
1
+ declare module '*.vue' {
2
+ import type { DefineComponent } from 'vue'
3
+ const component: DefineComponent<{}, {}, any>
4
+ export default component
5
+ }
package/src/style.css ADDED
@@ -0,0 +1,82 @@
1
+ :root {
2
+ font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
3
+ line-height: 1.5;
4
+ font-weight: 400;
5
+
6
+ color-scheme: light;
7
+ color: #213547;
8
+ background-color: #f5f5f5;
9
+
10
+ font-synthesis: none;
11
+ text-rendering: optimizeLegibility;
12
+ -webkit-font-smoothing: antialiased;
13
+ -moz-osx-font-smoothing: grayscale;
14
+ }
15
+
16
+ a {
17
+ font-weight: 500;
18
+ color: #646cff;
19
+ text-decoration: inherit;
20
+ }
21
+ a:hover {
22
+ color: #535bf2;
23
+ }
24
+
25
+ body {
26
+ margin: 0;
27
+ display: flex;
28
+ align-items: flex-start;
29
+ justify-content: flex-start;
30
+ min-width: 320px;
31
+ min-height: 100vh;
32
+ background-color: #f5f5f5;
33
+ }
34
+
35
+ h1 {
36
+ font-size: 3.2em;
37
+ line-height: 1.1;
38
+ }
39
+
40
+ button {
41
+ border-radius: 8px;
42
+ border: 1px solid transparent;
43
+ padding: 0.6em 1.2em;
44
+ font-size: 1em;
45
+ font-weight: 500;
46
+ font-family: inherit;
47
+ background-color: #1a1a1a;
48
+ cursor: pointer;
49
+ transition: border-color 0.25s;
50
+ }
51
+ button:hover {
52
+ border-color: #646cff;
53
+ }
54
+ button:focus,
55
+ button:focus-visible {
56
+ outline: 4px auto -webkit-focus-ring-color;
57
+ }
58
+
59
+ .card {
60
+ padding: 2em;
61
+ }
62
+
63
+ #app {
64
+ max-width: none;
65
+ width: 100%;
66
+ margin: 0;
67
+ padding: 0;
68
+ text-align: left;
69
+ }
70
+
71
+ @media (prefers-color-scheme: light) {
72
+ :root {
73
+ color: #213547;
74
+ background-color: #f5f5f5;
75
+ }
76
+ a:hover {
77
+ color: #747bff;
78
+ }
79
+ button {
80
+ background-color: #f9f9f9;
81
+ }
82
+ }
@@ -0,0 +1,4 @@
1
+ declare module 'jsbarcode' {
2
+ const JsBarcode: any
3
+ export default JsBarcode
4
+ }
@@ -0,0 +1,4 @@
1
+ declare module 'qrcode' {
2
+ const QRCode: any
3
+ export default QRCode
4
+ }
@@ -0,0 +1,4 @@
1
+ declare module 'qz-tray' {
2
+ const qz: any
3
+ export default qz
4
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "extends": "@vue/tsconfig/tsconfig.dom.json",
3
+ "compilerOptions": {
4
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
5
+ "types": ["vite/client"],
6
+
7
+ /* Linting */
8
+ "strict": true,
9
+ "noUnusedLocals": true,
10
+ "noUnusedParameters": true,
11
+ "erasableSyntaxOnly": true,
12
+ "noFallthroughCasesInSwitch": true,
13
+ "noUncheckedSideEffectImports": true
14
+ },
15
+ "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
16
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "./tsconfig.app.json",
3
+ "compilerOptions": {
4
+ "declaration": true,
5
+ "emitDeclarationOnly": true,
6
+ "outDir": "./dist",
7
+ "skipLibCheck": true
8
+ },
9
+ "include": ["src/index.ts", "src/components/**/*.ts", "src/components/**/*.vue", "src/shims-vue.d.ts"],
10
+ "exclude": ["src/main.ts", "src/App.vue", "src/components/HelloWorld.vue", "src/assets"]
11
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" }
6
+ ]
7
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4
+ "target": "ES2023",
5
+ "lib": ["ES2023"],
6
+ "module": "ESNext",
7
+ "types": ["node"],
8
+ "skipLibCheck": true,
9
+
10
+ /* Bundler mode */
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "verbatimModuleSyntax": true,
14
+ "moduleDetection": "force",
15
+ "noEmit": true,
16
+
17
+ /* Linting */
18
+ "strict": true,
19
+ "noUnusedLocals": true,
20
+ "noUnusedParameters": true,
21
+ "erasableSyntaxOnly": true,
22
+ "noFallthroughCasesInSwitch": true,
23
+ "noUncheckedSideEffectImports": true
24
+ },
25
+ "include": ["vite.config.ts"]
26
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,24 @@
1
+ import { defineConfig } from 'vite'
2
+ import vue from '@vitejs/plugin-vue'
3
+ import { resolve } from 'path'
4
+
5
+ export default defineConfig({
6
+ plugins: [vue()],
7
+ build: {
8
+ lib: {
9
+ entry: resolve(__dirname, 'src/index.ts'),
10
+ name: 'zing-print',
11
+ fileName: (format) => `index.${format}.js`,
12
+ formats: ['es', 'umd']
13
+ },
14
+ rollupOptions: {
15
+ external: ['vue', 'ant-design-vue'],
16
+ output: {
17
+ globals: {
18
+ vue: 'Vue',
19
+ 'ant-design-vue': 'antd'
20
+ }
21
+ }
22
+ }
23
+ }
24
+ })