Pārlūkot izejas kodu

添加标签打印功能

cc12458 2 nedēļas atpakaļ
vecāks
revīzija
d59885ac28

+ 4 - 0
package.json

@@ -24,11 +24,14 @@
     "@vueuse/components": "^13.0.0",
     "@vueuse/core": "^13.0.0",
     "@vueuse/router": "^13.4.0",
+    "@zumer/snapdom": "^1.8.0",
     "alova": "^3.2.10",
     "big.js": "^6.2.2",
     "eruda": "^3.4.1",
+    "jsbarcode": "^3.12.1",
     "pinia": "^3.0.1",
     "pinia-plugin-persistedstate": "^4.2.0",
+    "qrcode": "^1.5.4",
     "tailwindcss": "^4.0.15",
     "vant": "^4.9.18",
     "vconsole": "^3.15.1",
@@ -39,6 +42,7 @@
     "@tsconfig/node22": "^22.0.0",
     "@types/jsdom": "^21.1.7",
     "@types/node": "^22.13.9",
+    "@types/qrcode": "^1.5.5",
     "@vant/auto-import-resolver": "^1.3.0",
     "@vitejs/plugin-legacy": "^6.0.2",
     "@vitejs/plugin-vue": "^5.2.1",

+ 250 - 0
pnpm-lock.yaml

@@ -17,9 +17,15 @@ importers:
       '@vueuse/core':
         specifier: ^13.0.0
         version: 13.0.0(vue@3.5.13(typescript@5.8.2))
+      '@vueuse/integrations':
+        specifier: ^13.5.0
+        version: 13.5.0(axios@1.8.4)(qrcode@1.5.4)(vue@3.5.13(typescript@5.8.2))
       '@vueuse/router':
         specifier: ^13.4.0
         version: 13.4.0(vue-router@4.5.0(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2))
+      '@zumer/snapdom':
+        specifier: ^1.8.0
+        version: 1.8.0
       alova:
         specifier: ^3.2.10
         version: 3.2.10
@@ -29,12 +35,18 @@ importers:
       eruda:
         specifier: ^3.4.1
         version: 3.4.1
+      jsbarcode:
+        specifier: ^3.12.1
+        version: 3.12.1
       pinia:
         specifier: ^3.0.1
         version: 3.0.1(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2))
       pinia-plugin-persistedstate:
         specifier: ^4.2.0
         version: 4.2.0(pinia@3.0.1(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2)))
+      qrcode:
+        specifier: ^1.5.4
+        version: 1.5.4
       tailwindcss:
         specifier: ^4.0.15
         version: 4.0.15
@@ -60,6 +72,9 @@ importers:
       '@types/node':
         specifier: ^22.13.9
         version: 22.13.12
+      '@types/qrcode':
+        specifier: ^1.5.5
+        version: 1.5.5
       '@vant/auto-import-resolver':
         specifier: ^1.3.0
         version: 1.3.0
@@ -1259,6 +1274,9 @@ packages:
   '@types/node@22.13.12':
     resolution: {integrity: sha512-ixiWrCSRi33uqBMRuICcKECW7rtgY43TbsHDpM2XK7lXispd48opW+0IXrBVxv9NMhaz/Ue9kyj6r3NTVyXm8A==}
 
+  '@types/qrcode@1.5.5':
+    resolution: {integrity: sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==}
+
   '@types/sinonjs__fake-timers@8.1.1':
     resolution: {integrity: sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==}
 
@@ -1518,9 +1536,59 @@ packages:
     peerDependencies:
       vue: ^3.5.0
 
+  '@vueuse/core@13.5.0':
+    resolution: {integrity: sha512-wV7z0eUpifKmvmN78UBZX8T7lMW53Nrk6JP5+6hbzrB9+cJ3jr//hUlhl9TZO/03bUkMK6gGkQpqOPWoabr72g==}
+    peerDependencies:
+      vue: ^3.5.0
+
+  '@vueuse/integrations@13.5.0':
+    resolution: {integrity: sha512-7RACJySnlpl0MkSzxbtadioNGSX4TL5/Wl2cUy4nDq/XkeHwPYvVM880HJUSiap/FXhVEup9VKTM9y/n5UspAw==}
+    peerDependencies:
+      async-validator: ^4
+      axios: ^1
+      change-case: ^5
+      drauu: ^0.4
+      focus-trap: ^7
+      fuse.js: ^7
+      idb-keyval: ^6
+      jwt-decode: ^4
+      nprogress: ^0.2
+      qrcode: ^1.5
+      sortablejs: ^1
+      universal-cookie: ^7 || ^8
+      vue: ^3.5.0
+    peerDependenciesMeta:
+      async-validator:
+        optional: true
+      axios:
+        optional: true
+      change-case:
+        optional: true
+      drauu:
+        optional: true
+      focus-trap:
+        optional: true
+      fuse.js:
+        optional: true
+      idb-keyval:
+        optional: true
+      jwt-decode:
+        optional: true
+      nprogress:
+        optional: true
+      qrcode:
+        optional: true
+      sortablejs:
+        optional: true
+      universal-cookie:
+        optional: true
+
   '@vueuse/metadata@13.0.0':
     resolution: {integrity: sha512-TRNksqmvtvqsuHf7bbgH9OSXEV2b6+M3BSN4LR5oxWKykOFT9gV78+C2/0++Pq9KCp9KQ1OQDPvGlWNQpOb2Mw==}
 
+  '@vueuse/metadata@13.5.0':
+    resolution: {integrity: sha512-euhItU3b0SqXxSy8u1XHxUCdQ8M++bsRs+TYhOLDU/OykS7KvJnyIFfep0XM5WjIFry9uAPlVSjmVHiqeshmkw==}
+
   '@vueuse/router@13.4.0':
     resolution: {integrity: sha512-3NL9RFgTiWN4SVp6CUFK/9db10BnoLU3iX+TRgcG4HEuR7GC1g+yMqxe33L6kjUTv8Dc9WeaER714vGBc9Xyjg==}
     peerDependencies:
@@ -1537,6 +1605,14 @@ packages:
     peerDependencies:
       vue: ^3.5.0
 
+  '@vueuse/shared@13.5.0':
+    resolution: {integrity: sha512-K7GrQIxJ/ANtucxIXbQlUHdB0TPA8c+q5i+zbrjxuhJCnJ9GtBg75sBSnvmLSxHKPg2Yo8w62PWksl9kwH0Q8g==}
+    peerDependencies:
+      vue: ^3.5.0
+
+  '@zumer/snapdom@1.8.0':
+    resolution: {integrity: sha512-+xjVImca5k/nUxh0yUcdwcRzLFwSzNwzEfzerlUIC8IOnW+ikengvZuSODUXdOi+T7xuLgpj0HTXf+cRjWW/0w==}
+
   abbrev@2.0.0:
     resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==}
     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
@@ -1749,6 +1825,10 @@ packages:
     resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
     engines: {node: '>=6'}
 
+  camelcase@5.3.1:
+    resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
+    engines: {node: '>=6'}
+
   caniuse-lite@1.0.30001707:
     resolution: {integrity: sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==}
 
@@ -1802,6 +1882,9 @@ packages:
     resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==}
     engines: {node: '>=8'}
 
+  cliui@6.0.0:
+    resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
+
   color-convert@2.0.1:
     resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
     engines: {node: '>=7.0.0'}
@@ -1922,6 +2005,10 @@ packages:
       supports-color:
         optional: true
 
+  decamelize@1.2.0:
+    resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
+    engines: {node: '>=0.10.0'}
+
   decimal.js@10.5.0:
     resolution: {integrity: sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==}
 
@@ -1961,6 +2048,9 @@ packages:
     resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==}
     engines: {node: '>=8'}
 
+  dijkstrajs@1.0.3:
+    resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
+
   dotenv@16.4.7:
     resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==}
     engines: {node: '>=12'}
@@ -2226,6 +2316,10 @@ packages:
     resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
     engines: {node: '>=8'}
 
+  find-up@4.1.0:
+    resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
+    engines: {node: '>=8'}
+
   find-up@5.0.0:
     resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
     engines: {node: '>=10'}
@@ -2280,6 +2374,10 @@ packages:
     resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
     engines: {node: '>=6.9.0'}
 
+  get-caller-file@2.0.5:
+    resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
+    engines: {node: 6.* || 8.* || >= 10.*}
+
   get-intrinsic@1.3.0:
     resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
     engines: {node: '>= 0.4'}
@@ -2555,6 +2653,9 @@ packages:
     resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
     hasBin: true
 
+  jsbarcode@3.12.1:
+    resolution: {integrity: sha512-QZQSqIknC2Rr/YOUyOkCBqsoiBAOTYK+7yNN3JsqfoUtJtkazxNw1dmPpxuv7VVvqW13kA3/mKiLq+s/e3o9hQ==}
+
   jsbn@0.1.1:
     resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==}
 
@@ -2709,6 +2810,10 @@ packages:
     resolution: {integrity: sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==}
     engines: {node: '>=14'}
 
+  locate-path@5.0.0:
+    resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
+    engines: {node: '>=8'}
+
   locate-path@6.0.0:
     resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
     engines: {node: '>=10'}
@@ -2906,10 +3011,18 @@ packages:
     engines: {node: '>=8.*'}
     hasBin: true
 
+  p-limit@2.3.0:
+    resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
+    engines: {node: '>=6'}
+
   p-limit@3.1.0:
     resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
     engines: {node: '>=10'}
 
+  p-locate@4.1.0:
+    resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
+    engines: {node: '>=8'}
+
   p-locate@5.0.0:
     resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
     engines: {node: '>=10'}
@@ -2918,6 +3031,10 @@ packages:
     resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==}
     engines: {node: '>=10'}
 
+  p-try@2.2.0:
+    resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
+    engines: {node: '>=6'}
+
   package-json-from-dist@1.0.1:
     resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
 
@@ -3023,6 +3140,10 @@ packages:
   pkg-types@2.1.0:
     resolution: {integrity: sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A==}
 
+  pngjs@5.0.0:
+    resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
+    engines: {node: '>=10.13.0'}
+
   postcss-selector-parser@6.1.2:
     resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
     engines: {node: '>=4'}
@@ -3077,6 +3198,11 @@ packages:
     resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
     engines: {node: '>=6'}
 
+  qrcode@1.5.4:
+    resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
+    engines: {node: '>=10.13.0'}
+    hasBin: true
+
   qs@6.14.0:
     resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==}
     engines: {node: '>=0.6'}
@@ -3132,6 +3258,13 @@ packages:
   request-progress@3.0.0:
     resolution: {integrity: sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==}
 
+  require-directory@2.1.1:
+    resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
+    engines: {node: '>=0.10.0'}
+
+  require-main-filename@2.0.0:
+    resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
+
   resolve-from@4.0.0:
     resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
     engines: {node: '>=4'}
@@ -3317,6 +3450,9 @@ packages:
     engines: {node: '>=10'}
     hasBin: true
 
+  set-blocking@2.0.0:
+    resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
+
   shebang-command@2.0.0:
     resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
     engines: {node: '>=8'}
@@ -3846,6 +3982,9 @@ packages:
     resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==}
     engines: {node: '>=18'}
 
+  which-module@2.0.1:
+    resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
+
   which@2.0.2:
     resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
     engines: {node: '>= 8'}
@@ -3903,9 +4042,20 @@ packages:
   xmlchars@2.2.0:
     resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
 
+  y18n@4.0.3:
+    resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
+
   yallist@3.1.1:
     resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
 
+  yargs-parser@18.1.3:
+    resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
+    engines: {node: '>=6'}
+
+  yargs@15.4.1:
+    resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
+    engines: {node: '>=8'}
+
   yauzl@2.10.0:
     resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==}
 
@@ -5070,6 +5220,10 @@ snapshots:
     dependencies:
       undici-types: 6.20.0
 
+  '@types/qrcode@1.5.5':
+    dependencies:
+      '@types/node': 22.13.12
+
   '@types/sinonjs__fake-timers@8.1.1': {}
 
   '@types/sizzle@2.3.9': {}
@@ -5436,8 +5590,26 @@ snapshots:
       '@vueuse/shared': 13.0.0(vue@3.5.13(typescript@5.8.2))
       vue: 3.5.13(typescript@5.8.2)
 
+  '@vueuse/core@13.5.0(vue@3.5.13(typescript@5.8.2))':
+    dependencies:
+      '@types/web-bluetooth': 0.0.21
+      '@vueuse/metadata': 13.5.0
+      '@vueuse/shared': 13.5.0(vue@3.5.13(typescript@5.8.2))
+      vue: 3.5.13(typescript@5.8.2)
+
+  '@vueuse/integrations@13.5.0(axios@1.8.4)(qrcode@1.5.4)(vue@3.5.13(typescript@5.8.2))':
+    dependencies:
+      '@vueuse/core': 13.5.0(vue@3.5.13(typescript@5.8.2))
+      '@vueuse/shared': 13.5.0(vue@3.5.13(typescript@5.8.2))
+      vue: 3.5.13(typescript@5.8.2)
+    optionalDependencies:
+      axios: 1.8.4(debug@4.4.0)
+      qrcode: 1.5.4
+
   '@vueuse/metadata@13.0.0': {}
 
+  '@vueuse/metadata@13.5.0': {}
+
   '@vueuse/router@13.4.0(vue-router@4.5.0(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2))':
     dependencies:
       '@vueuse/shared': 13.4.0(vue@3.5.13(typescript@5.8.2))
@@ -5452,6 +5624,12 @@ snapshots:
     dependencies:
       vue: 3.5.13(typescript@5.8.2)
 
+  '@vueuse/shared@13.5.0(vue@3.5.13(typescript@5.8.2))':
+    dependencies:
+      vue: 3.5.13(typescript@5.8.2)
+
+  '@zumer/snapdom@1.8.0': {}
+
   abbrev@2.0.0: {}
 
   acorn-jsx@5.3.2(acorn@8.14.1):
@@ -5651,6 +5829,8 @@ snapshots:
 
   callsites@3.1.0: {}
 
+  camelcase@5.3.1: {}
+
   caniuse-lite@1.0.30001707: {}
 
   caseless@0.12.0: {}
@@ -5711,6 +5891,12 @@ snapshots:
       slice-ansi: 3.0.0
       string-width: 4.2.3
 
+  cliui@6.0.0:
+    dependencies:
+      string-width: 4.2.3
+      strip-ansi: 6.0.1
+      wrap-ansi: 6.2.0
+
   color-convert@2.0.1:
     dependencies:
       color-name: 1.1.4
@@ -5848,6 +6034,8 @@ snapshots:
     optionalDependencies:
       supports-color: 8.1.1
 
+  decamelize@1.2.0: {}
+
   decimal.js@10.5.0: {}
 
   deep-eql@5.0.2: {}
@@ -5873,6 +6061,8 @@ snapshots:
 
   detect-libc@2.0.3: {}
 
+  dijkstrajs@1.0.3: {}
+
   dotenv@16.4.7: {}
 
   dunder-proto@1.0.1:
@@ -6203,6 +6393,11 @@ snapshots:
     dependencies:
       to-regex-range: 5.0.1
 
+  find-up@4.1.0:
+    dependencies:
+      locate-path: 5.0.0
+      path-exists: 4.0.0
+
   find-up@5.0.0:
     dependencies:
       locate-path: 6.0.0
@@ -6255,6 +6450,8 @@ snapshots:
 
   gensync@1.0.0-beta.2: {}
 
+  get-caller-file@2.0.5: {}
+
   get-intrinsic@1.3.0:
     dependencies:
       call-bind-apply-helpers: 1.0.2
@@ -6505,6 +6702,8 @@ snapshots:
     dependencies:
       argparse: 2.0.1
 
+  jsbarcode@3.12.1: {}
+
   jsbn@0.1.1: {}
 
   jsdom@26.0.0:
@@ -6649,6 +6848,10 @@ snapshots:
       pkg-types: 2.1.0
       quansync: 0.2.10
 
+  locate-path@5.0.0:
+    dependencies:
+      p-locate: 4.1.0
+
   locate-path@6.0.0:
     dependencies:
       p-locate: 5.0.0
@@ -6836,10 +7039,18 @@ snapshots:
       '@oxlint/win32-arm64': 0.15.15
       '@oxlint/win32-x64': 0.15.15
 
+  p-limit@2.3.0:
+    dependencies:
+      p-try: 2.2.0
+
   p-limit@3.1.0:
     dependencies:
       yocto-queue: 0.1.0
 
+  p-locate@4.1.0:
+    dependencies:
+      p-limit: 2.3.0
+
   p-locate@5.0.0:
     dependencies:
       p-limit: 3.1.0
@@ -6848,6 +7059,8 @@ snapshots:
     dependencies:
       aggregate-error: 3.1.0
 
+  p-try@2.2.0: {}
+
   package-json-from-dist@1.0.1: {}
 
   parent-module@1.0.1:
@@ -6931,6 +7144,8 @@ snapshots:
       exsolve: 1.0.4
       pathe: 2.0.3
 
+  pngjs@5.0.0: {}
+
   postcss-selector-parser@6.1.2:
     dependencies:
       cssesc: 3.0.0
@@ -6975,6 +7190,12 @@ snapshots:
 
   punycode@2.3.1: {}
 
+  qrcode@1.5.4:
+    dependencies:
+      dijkstrajs: 1.0.3
+      pngjs: 5.0.0
+      yargs: 15.4.1
+
   qs@6.14.0:
     dependencies:
       side-channel: 1.1.0
@@ -7032,6 +7253,10 @@ snapshots:
     dependencies:
       throttleit: 1.0.1
 
+  require-directory@2.1.1: {}
+
+  require-main-filename@2.0.0: {}
+
   resolve-from@4.0.0: {}
 
   resolve@1.22.10:
@@ -7193,6 +7418,8 @@ snapshots:
 
   semver@7.7.1: {}
 
+  set-blocking@2.0.0: {}
+
   shebang-command@2.0.0:
     dependencies:
       shebang-regex: 3.0.0
@@ -7767,6 +7994,8 @@ snapshots:
       tr46: 5.1.0
       webidl-conversions: 7.0.0
 
+  which-module@2.0.1: {}
+
   which@2.0.2:
     dependencies:
       isexe: 2.0.0
@@ -7810,8 +8039,29 @@ snapshots:
 
   xmlchars@2.2.0: {}
 
+  y18n@4.0.3: {}
+
   yallist@3.1.1: {}
 
+  yargs-parser@18.1.3:
+    dependencies:
+      camelcase: 5.3.1
+      decamelize: 1.2.0
+
+  yargs@15.4.1:
+    dependencies:
+      cliui: 6.0.0
+      decamelize: 1.2.0
+      find-up: 4.1.0
+      get-caller-file: 2.0.5
+      require-directory: 2.1.1
+      require-main-filename: 2.0.0
+      set-blocking: 2.0.0
+      string-width: 4.2.3
+      which-module: 2.0.1
+      y18n: 4.0.3
+      yargs-parser: 18.1.3
+
   yauzl@2.10.0:
     dependencies:
       buffer-crc32: 0.2.13

+ 2 - 0
src/api/pda.api.ts

@@ -58,6 +58,8 @@ export function getDataMethod(no: string, mode?: string) {
             remark1: data?.extendedTxt,
             remark2: data?.pharmacistsremarks,
             entrust: data?.entrust,
+
+            date: data?.prescriptionTime,
           },
           medicines:
             data?.oralPreItemList?.map((item: ResponseData) => {

+ 39 - 0
src/core/hook/useQRCode.ts

@@ -0,0 +1,39 @@
+import { type QRCodeToDataURLOptions, toDataURL } from 'qrcode';
+import { type MaybeComputedElementRef, tryOnMounted, unrefElement, useElementSize } from '@vueuse/core';
+
+export function useQRCode(target: MaybeComputedElementRef, text: MaybeRefOrGetter<string | void>, options?: QRCodeToDataURLOptions & { append?: boolean }) {
+  const src = toRef(text);
+  const result = shallowRef('');
+
+  const { width, height } = useElementSize(target);
+
+  const create = () => {
+    if (src.value && width.value > 0) {
+      toDataURL(src.value, Object.assign({ width: width.value }, options), (_, url) => {
+        result.value = url;
+        if (options?.append) {
+          const el = unrefElement(target) as HTMLElement;
+          appendResult(el, url);
+        }
+      });
+    }
+  };
+
+  tryOnMounted(create);
+  watch([src, width], create, { immediate: false });
+
+  return result;
+}
+
+function appendResult(target: HTMLElement, src: string) {
+  if (target == null || !src) return;
+
+  // 清空 el 的所有子元素
+  while (target.firstChild) {
+    target.removeChild(target.firstChild);
+  }
+  // 创建 img 元素并设置 src
+  const img = document.createElement('img');
+  img.src = src;
+  target.appendChild(img);
+}

+ 2 - 0
src/model/step.model.ts

@@ -30,6 +30,8 @@ export interface StepModel {
       phone: string;
       address: string;
     };
+
+    date?: string;
   };
   medicines: {
     id: string;

+ 141 - 0
src/module/print/Tag_60_40.vue

@@ -0,0 +1,141 @@
+<script setup lang="ts">
+import { useElementSize } from '@vueuse/core';
+import { useStepStore } from '@/stores';
+import { useQRCode } from '@/core/hook/useQRCode.ts';
+
+export interface Props {
+  count: number;
+}
+
+const page = reactive({
+  width: 60,
+  height: 40,
+  dpi: 8,
+});
+
+const props = defineProps<Props>();
+const emits = defineEmits<{ rendered: [((base64: string) => (total?: number) => string)?] }>();
+
+const stepStore = useStepStore();
+const { dataset } = storeToRefs(stepStore);
+
+const decoction = computed(() => {
+  return { 是: '代煎', 否: '自煎' }[dataset.value?.prescription.decoction as string] ?? '';
+});
+const date = computed(() => {
+  const value = dataset.value?.prescription.date;
+  return typeof value === 'string' ? value.split(' ')[0] : '';
+});
+
+const yard = useTemplateRef('yard-wrapper');
+const { width: size } = useElementSize(yard);
+
+const qrcode = computed(() => dataset.value?.no);
+const result = useQRCode(useTemplateRef('yard-container'), qrcode, {
+  margin: 0,
+  append: true,
+});
+
+const tspl = (base64: string) => (total = 1) => `
+SIZE ${page.width} mm,${page.height} mm
+SPEED 2
+DENSITY 8
+GAP 2 mm,0 mm
+CLS
+BEEP
+BITMAP 0,0,${page.width},${page.height * page.dpi},0,${base64}
+PRINT ${total}
+`;
+
+watch(
+  result,
+  () => {
+    if (result.value) emits('rendered', tspl);
+  },
+  { immediate: true },
+);
+</script>
+
+<template>
+  <div class="print-container relative flex flex-col">
+    <div class="text-22 text-center font-black">{{ dataset?.patient?.hospital }}</div>
+    <div class="text-16 text-center">{{ dataset?.no }}</div>
+    <div class="text-22 flex items-center text-center">
+      <div class="text-24 snippet-wrapper" style="flex: 1 1 60%">
+        <span v-if="dataset?.patient?.name" class="font-semibold" data-snippet=",">{{ dataset?.patient?.name }}</span>
+        <span v-if="dataset?.patient?.gender" class="font-semibold" data-snippet=",">{{ dataset?.patient?.gender }}</span>
+        <span v-if="dataset?.patient?.age" class="font-semibold" data-snippet=",">{{ dataset?.patient?.age }}</span>
+      </div>
+      <div style="flex: 1 1 40%">
+        <span class="font-semibold px-2">{{ dataset?.prescription?.count }}</span>
+        <span> 帖 </span>
+        <span class="font-semibold px-2">{{ props.count }}</span>
+        <span> 包 </span>
+      </div>
+    </div>
+    <div class="text-20 flex">
+      <div class="flex-auto">
+        <span>备注:</span>
+        <span class="font-medium">{{ dataset?.prescription?.entrust }}</span>
+      </div>
+      <div class="font-semibold flex-none">{{ decoction }}</div>
+    </div>
+    <div class="pt-2 flex-auto flex text-14">
+      <div class="flex-auto pr-2">{{ dataset?.prescription?.remark2 ?? '' }}</div>
+      <div class="yard-shade"></div>
+    </div>
+    <div class="text-20 flex justify-evenly">
+      <div class="flex-auto font-semibold">{{ dataset?.prescription?.dispatch?.method }}</div>
+      <div class="flex-auto text-center">{{ date }}</div>
+      <div class="flex-auto" ref="yard-wrapper"></div>
+    </div>
+
+    <div class="yard-container absolute" ref="yard-container"></div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+$p: 12px;
+
+.print-container {
+  --width: v-bind(page.width);
+  --height: v-bind(page.height);
+  --dpi: v-bind(page.dpi);
+  width: calc(var(--width) * var(--dpi) * 1px);
+  height: calc(var(--height) * var(--dpi) * 1px);
+  padding: $p;
+
+  border: 1px solid #d0d0d0;
+
+  $font-map: (
+    16: 1.2,
+    18: 1.5,
+    20: 1.5,
+    22: 1.5,
+    24: 1.5,
+  );
+
+  @each $size, $lh in $font-map {
+    .text-#{$size} {
+      font-size: calc(#{$size}px * 1.2);
+      line-height: $lh;
+    }
+  }
+}
+
+.yard-container,
+.yard-shade {
+  --size: v-bind(size);
+  width: calc(var(--size) * 1px);
+  height: calc(var(--size) * 1px);
+}
+
+.yard-container {
+  bottom: $p;
+  right: $p;
+}
+
+.snippet-wrapper > span + span::before {
+  content: attr(data-snippet);
+}
+</style>

+ 76 - 0
src/module/print/useSnapshot.ts

@@ -0,0 +1,76 @@
+import { tryOnMounted, tryOnUnmounted } from '@vueuse/core';
+import { snapdom } from '@zumer/snapdom';
+
+export interface SnapshotOption {
+  immediate?: boolean;
+}
+
+type SnapshotTspl = (total?: number) => string;
+type SnapshotTemplate = (base64: string) => SnapshotTspl;
+
+export function useSnapshot<P extends Record<string, any>>(loader: () => Promise<{ default: Component }>, props?: P, option?: SnapshotOption) {
+  const snapshot = shallowRef<string>();
+  const tspl = shallowRef<SnapshotTspl | void>();
+
+  let app: ReturnType<typeof createApp> | null = null;
+  let container: HTMLDivElement | null = null;
+
+  const mounted = (component: Component, _props?: Partial<P>) => {
+    if (container == null) return;
+    const { promise, resolve } = Promise.withResolvers<SnapshotTemplate>();
+    app = createApp(component, { ...props, ..._props, onRendered: resolve });
+    app.mount(container);
+
+    return promise;
+  };
+
+  const unmounted = () => {
+    app?.unmount();
+    app = null;
+  };
+
+  const capture = async (template?: SnapshotTemplate) => {
+    if (container == null) return;
+
+    const dom = await snapdom(container, { scale: 1 });
+    const img = await dom.toPng({ scale: 2 });
+    snapshot.value = img.src;
+    tspl.value = template?.(img.src);
+  };
+
+  async function render(props?: Partial<P>, forced = false): Promise<{ snapshot: string; tspl: SnapshotTspl }> {
+    if (forced) unmounted();
+
+    if (forced || !snapshot.value) {
+      const { default: component } = await loader();
+
+      const tspl = await mounted(component, props);
+      await capture(tspl);
+      unmounted();
+    }
+    return {
+      snapshot: snapshot.value!!,
+      tspl: tspl.value!!,
+    };
+  }
+
+  tryOnMounted(() => {
+    container = document.createElement('div');
+    container.style.position = 'fixed';
+    container.style.left = '-99999px';
+    container.style.top = '-99999px';
+    container.style.zIndex = '-1';
+    document.body.appendChild(container);
+    if (option?.immediate) render().then();
+  });
+  tryOnUnmounted(() => {
+    if (container) document.body.removeChild(container);
+    container = null;
+
+    unmounted();
+    snapshot.value = '';
+    tspl.value = void 0;
+  });
+
+  return { snapshot, tspl, render };
+}

+ 12 - 1
src/module/step/StepPack.vue

@@ -7,11 +7,12 @@ import { fromPackModel, type PackModel } from '@/model/step.model.ts';
 import { useStepStore } from '@/stores';
 
 import type { ScanData } from '@/core/hook/useScan.ts';
+import Printer from '@/module/print/Printer.vue';
 
 const emits = defineEmits<{ back: [delta?: number] }>();
 
 const storeStore = useStepStore();
-const { dataset } = storeToRefs(storeStore);
+const { dataset, id } = storeToRefs(storeStore);
 
 const {
   loading: submitting,
@@ -34,6 +35,8 @@ const { data, loading } = useWatcher(() => getPackDataMethod(dataset.value?.id!)
     showDialog({ title: '温馨提示', message, closeOnClickOverlay: true }).then(() => emits('back'));
   });
 
+const loader = () => import('@/module/print/Tag_60_40.vue');
+
 defineExpose({
   scan(data: ScanData) {
     model.value.deviceCode = data?.code ?? '';
@@ -54,6 +57,14 @@ defineExpose({
   <van-field v-model="model.packingNote" :readonly="submitting" rows="2" autosize label="打包备注" type="textarea" placeholder="请输入备注内容" />
 
   <slot name="submit" title="打包节点上传" :submitting="submitting" :submit="send"></slot>
+  <teleport v-if="id" to="#bottom-handle">
+    <printer
+      :loader="loader"
+      :loader-params="{ count: data.packageNumber }"
+      :total="+data?.packageNumber"
+      :disabled="submitting"
+    ></printer>
+  </teleport>
 </template>
 
 <style scoped lang="scss">

+ 22 - 4
src/module/step/StepPackRecheck.vue

@@ -5,11 +5,12 @@ import { useForm, useWatcher } from 'alova/client';
 import { getPackDataMethod, setPackRecheckDataMethod } from '@/api/pda.api.ts';
 import { fromPackModel, type PackModel } from '@/model/step.model.ts';
 import { useStepStore } from '@/stores';
+import Printer from '@/module/print/Printer.vue';
 
 const emits = defineEmits<{ back: [delta?: number] }>();
 
 const storeStore = useStepStore();
-const { dataset } = storeToRefs(storeStore);
+const { dataset, id } = storeToRefs(storeStore);
 
 const {
   loading: submitting,
@@ -33,6 +34,8 @@ const { loading } = useWatcher(() => getPackDataMethod(dataset.value?.id!), [dat
   });
 
 function submit(picture: string[]) {
+  if (printing.value) return;
+
   let message = '';
   if (!+model.value.packageDose) {
     message = '请输入包装量';
@@ -46,6 +49,14 @@ function submit(picture: string[]) {
   }
 }
 
+const printing = ref(false);
+const loader = () => import('@/module/print/Tag_60_40.vue');
+function printBefore() {
+  if (+model.value.packageDose) return true;
+  showNotify({ message: '请输入包数', type: 'warning' });
+  return false;
+}
+
 defineExpose({
   reset,
 });
@@ -70,9 +81,16 @@ defineExpose({
   <van-field v-model="model.packingNote" :readonly="submitting" rows="2" autosize label="打包复核备注" type="textarea" placeholder="请输入备注内容" />
 
   <slot name="submit" title="打包复核节点上传" :submitting="submitting" :submit="submit"></slot>
-  <!--<teleport to="#bottom-handle">
-    <van-button block :loading="submitting">打印标签</van-button>
-  </teleport>-->
+  <teleport v-if="id" to="#bottom-handle">
+    <printer
+      :loader="loader"
+      :loader-params="{ count: model.packageNumber }"
+      :total="+model?.packageNumber"
+      :disabled="submitting"
+      v-model:loading="printing"
+      :on-print-before="printBefore"
+    ></printer>
+  </teleport>
 </template>
 
 <style scoped lang="scss">

+ 1 - 1
src/router/index.ts

@@ -17,7 +17,7 @@ const router = createRouter({
           path: 'home',
           name: 'home',
           component: () => import('@/pages/HomePage.vue'),
-          meta: { title: '调配间' },
+          meta: { title: '' },
         },
         {
           path: `step/:mode`,