@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.
- package/.vscode/extensions.json +3 -0
- package/README.md +96 -0
- package/dist/index.es.js +20 -0
- package/dist/index.umd.js +1 -0
- package/dist/style.css +0 -0
- package/dist/vite.svg +1 -0
- package/index.html +13 -0
- package/package.json +47 -0
- package/public/vite.svg +1 -0
- package/src/App.vue +7 -0
- package/src/assets/vue.svg +1 -0
- package/src/components/HelloWorld.vue +41 -0
- package/src/components/PrintLabel/PrintLabel.vue +14 -0
- package/src/index.ts +10 -0
- package/src/main.ts +9 -0
- package/src/print/components/LabelPrinter.vue +58 -0
- package/src/print/components/Template1.vue +134 -0
- package/src/print/components/Template2.vue +30 -0
- package/src/print/components/Template3.vue +30 -0
- package/src/print/pages/PrintSetting.vue +330 -0
- package/src/print/utils/printConfig.ts +40 -0
- package/src/print/utils/qz.ts +55 -0
- package/src/shims-vue.d.ts +5 -0
- package/src/style.css +82 -0
- package/src/types/jsbarcode.d.ts +4 -0
- package/src/types/qrcode.d.ts +4 -0
- package/src/types/qz-tray.d.ts +4 -0
- package/tsconfig.app.json +16 -0
- package/tsconfig.decls.json +11 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +26 -0
- package/vite.config.ts +24 -0
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
|
+
```
|
package/dist/index.es.js
ADDED
|
@@ -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
|
+
}
|
package/public/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/src/App.vue
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="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>
|
package/src/index.ts
ADDED
package/src/main.ts
ADDED
|
@@ -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
|
+
}
|
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,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,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
|
+
})
|