Commit 9927b97ee7774f64e0bf75edecf2cc8d889aec0a

Authored by “wangming”
1 parent 961eecae

Improve GP_R3 printing flow and printer onboarding

Enhance TCP built-in fallback with port probing and update label/Bluetooth print templates.

Made-with: Cursor
Showing 22 changed files with 1931 additions and 98 deletions
美国版/Food Labeling Management App UniApp/src/pages/labels/bluetooth.vue
... ... @@ -17,8 +17,9 @@
17 17  
18 18 <view class="content">
19 19 <!-- Printer type: Bluetooth | Built-in -->
20   - <view class="type-tabs">
  20 + <view v-if="availablePrinterTypes.length > 1" class="type-tabs">
21 21 <view
  22 + v-if="isBluetoothModeAvailable"
22 23 class="tab"
23 24 :class="{ active: printerType === 'bluetooth' }"
24 25 @click="switchType('bluetooth')"
... ... @@ -27,6 +28,7 @@
27 28 <text class="tab-text">Bluetooth</text>
28 29 </view>
29 30 <view
  31 + v-if="isBuiltinModeAvailable"
30 32 class="tab"
31 33 :class="{ active: printerType === 'builtin' }"
32 34 @click="switchType('builtin')"
... ... @@ -45,6 +47,10 @@
45 47 <view class="debug-card">
46 48 <text class="debug-title">Debug Status</text>
47 49 <text class="debug-item">Current Mode: {{ debugInfo.currentMode }}</text>
  50 + <text class="debug-item">Available Mode: {{ availablePrinterTypesLabel }}</text>
  51 + <text class="debug-item">Device Model: {{ deviceIdentity.model || '-' }}</text>
  52 + <text class="debug-item">Device Brand: {{ deviceIdentity.brand || '-' }}</text>
  53 + <text class="debug-item">Device Product: {{ deviceIdentity.product || '-' }}</text>
48 54 <text class="debug-item">Classic Module: {{ debugInfo.classicModuleReady ? 'Ready' : 'Not Ready' }}</text>
49 55 <text class="debug-item">Paired Count: {{ debugInfo.pairedCount }}</text>
50 56 <text class="debug-item">Virtual BT Printer: {{ debugInfo.foundVirtualPrinter ? 'Found' : 'Not Found' }}</text>
... ... @@ -60,7 +66,7 @@
60 66 <view v-if="connectedDevice" class="connected-card">
61 67 <view class="connected-header">
62 68 <view class="connected-icon">
63   - <AppIcon name="bluetooth" size="md" color="white" />
  69 + <AppIcon :name="printerType === 'builtin' ? 'printer' : 'bluetooth'" size="md" color="white" />
64 70 </view>
65 71 <view class="connected-info">
66 72 <text class="connected-name">{{ connectedDevice.name }}</text>
... ... @@ -167,9 +173,11 @@ import AppIcon from &#39;../../components/AppIcon.vue&#39;
167 173 import SideMenu from '../../components/SideMenu.vue'
168 174 import LocationPicker from '../../components/LocationPicker.vue'
169 175 import { getStatusBarHeight } from '../../utils/statusBar'
  176 +import { getDeviceIdentity } from '../../utils/deviceInfo'
170 177 import classicBluetooth from '../../utils/print/bluetoothTool.js'
171 178 import {
172 179 getPrinterType,
  180 + getAvailablePrinterTypes,
173 181 clearPrinter,
174 182 type PrinterType,
175 183 } from '../../utils/print/printerConnection'
... ... @@ -189,7 +197,14 @@ const isScanning = ref(false)
189 197 const connectingId = ref('')
190 198 const errorMsg = ref('')
191 199 const btAdapterReady = ref(false)
192   -const printerType = ref<PrinterType | ''>(getPrinterType() || 'bluetooth')
  200 +const availablePrinterTypes = getAvailablePrinterTypes()
  201 +const availablePrinterTypesLabel = availablePrinterTypes.map(item => item === 'builtin' ? 'Built-in' : 'Bluetooth').join(' / ')
  202 +const deviceIdentity = getDeviceIdentity()
  203 +const storedPrinterType = getPrinterType()
  204 +const preferredPrinterType = (storedPrinterType && availablePrinterTypes.includes(storedPrinterType as PrinterType))
  205 + ? storedPrinterType as PrinterType
  206 + : availablePrinterTypes[0]
  207 +const printerType = ref<PrinterType | ''>(preferredPrinterType || 'bluetooth')
193 208 const currentPrinter = ref(getCurrentPrinterSummary())
194 209 const debugInfo = ref({
195 210 currentMode: 'none',
... ... @@ -211,6 +226,8 @@ interface BtDevice {
211 226  
212 227 const devices = ref<BtDevice[]>([])
213 228 const discoveredIds = new Set<string>()
  229 +const isBluetoothModeAvailable = availablePrinterTypes.includes('bluetooth')
  230 +const isBuiltinModeAvailable = availablePrinterTypes.includes('builtin')
214 231  
215 232 function refreshCurrentPrinter () {
216 233 currentPrinter.value = getCurrentPrinterSummary()
... ... @@ -237,6 +254,7 @@ const connectedDevice = computed(() =&gt; {
237 254 })
238 255  
239 256 function switchType (type: 'bluetooth' | 'builtin') {
  257 + if (!availablePrinterTypes.includes(type)) return
240 258 printerType.value = type
241 259 if (type === 'bluetooth' && getPrinterType() === 'builtin') {
242 260 clearPrinter()
... ... @@ -487,6 +505,7 @@ const handleTestPrint = async () =&gt; {
487 505 if (testPrinting.value) return
488 506 testPrinting.value = true
489 507 try {
  508 + uni.showLoading({ title: 'Preparing test print...', mask: true })
490 509 await testPrintCurrentPrinter((p) => {
491 510 if (p < 100) uni.showLoading({ title: `Printing ${p}%`, mask: true })
492 511 })
... ... @@ -533,7 +552,7 @@ onMounted(() =&gt; {
533 552 errorMsg.value = ''
534 553 }
535 554 })
536   - printerType.value = getPrinterType() || 'bluetooth'
  555 + printerType.value = preferredPrinterType || 'bluetooth'
537 556 refreshCurrentPrinter()
538 557 })
539 558  
... ...
美国版/Food Labeling Management App UniApp/src/pages/labels/labels.vue
... ... @@ -146,6 +146,7 @@ import AppIcon from &#39;../../components/AppIcon.vue&#39;
146 146 import SideMenu from '../../components/SideMenu.vue'
147 147 import LocationPicker from '../../components/LocationPicker.vue'
148 148 import { getStatusBarHeight } from '../../utils/statusBar'
  149 +import { getCurrentPrinterSummary } from '../../utils/print/manager/printerManager'
149 150  
150 151 const statusBarHeight = getStatusBarHeight()
151 152 const isMenuOpen = ref(false)
... ... @@ -160,9 +161,9 @@ const btConnected = ref(false)
160 161 const btDeviceName = ref('')
161 162  
162 163 onShow(() => {
163   - const name = uni.getStorageSync('btDeviceName')
164   - btConnected.value = !!name
165   - btDeviceName.value = name || ''
  164 + const summary = getCurrentPrinterSummary()
  165 + btConnected.value = summary.type === 'bluetooth' || summary.type === 'builtin'
  166 + btDeviceName.value = summary.displayName || ''
166 167 })
167 168  
168 169 const labelCategories = [
... ...
美国版/Food Labeling Management App UniApp/src/pages/labels/preview.vue
... ... @@ -118,8 +118,11 @@ import SideMenu from &#39;../../components/SideMenu.vue&#39;
118 118 import LocationPicker from '../../components/LocationPicker.vue'
119 119 import { getStatusBarHeight } from '../../utils/statusBar'
120 120 import { generateNextLabelId } from '../../utils/printLog'
121   -import { getCurrentPrinterSummary, printLabelForCurrentPrinter } from '../../utils/print/manager/printerManager'
  121 +import { getCurrentPrinterSummary, printSystemTemplateForCurrentPrinter } from '../../utils/print/manager/printerManager'
  122 +import { PREVIEW_SYSTEM_TEMPLATE } from '../../utils/print/templates/previewSystemTemplate'
122 123 import chickenLabelImg from '../../static/chicken-lable.png'
  124 +import label1Img from '../../static/lable1.png'
  125 +import label2Img from '../../static/lable2.png'
123 126  
124 127 const statusBarHeight = getStatusBarHeight()
125 128 const isPrinting = ref(false)
... ... @@ -146,9 +149,9 @@ const labelImage = computed(() =&gt; {
146 149 }
147 150 const size = templateSize.value
148 151 if (size.indexOf('2"x6"') >= 0 || size.indexOf('2"x4"') >= 0) {
149   - return '/static/lable2.png'
  152 + return label2Img
150 153 }
151   - return '/static/lable1.png'
  154 + return label1Img
152 155 })
153 156  
154 157 // 按详情规则与产品列表一致:chicken→Chicken,lable2→Cheese Burger Deluxe,lable1→Syrup
... ... @@ -159,6 +162,15 @@ const displayProductName = computed(() =&gt; {
159 162 return 'Syrup'
160 163 })
161 164  
  165 +const printTemplateData = computed(() => ({
  166 + product: displayProductName.value,
  167 + productName: displayProductName.value,
  168 + category: productCategory.value,
  169 + labelId: labelId.value,
  170 + qrCode: labelId.value,
  171 + barcode: labelId.value,
  172 +}))
  173 +
162 174 onShow(() => {
163 175 const summary = getCurrentPrinterSummary()
164 176 btConnected.value = summary.type === 'bluetooth' || summary.type === 'builtin'
... ... @@ -237,11 +249,10 @@ const handlePrint = async () =&gt; {
237 249 }
238 250 isPrinting.value = true
239 251 try {
240   - await printLabelForCurrentPrinter({
241   - productName: displayProductName.value,
242   - labelId: labelId.value,
  252 + uni.showLoading({ title: 'Preparing print data...', mask: true })
  253 + await new Promise(resolve => setTimeout(resolve, 30))
  254 + await printSystemTemplateForCurrentPrinter(PREVIEW_SYSTEM_TEMPLATE, printTemplateData.value, {
243 255 printQty: printQty.value,
244   - extraLine: lastEdited.value,
245 256 }, (percent) => {
246 257 if (percent >= 100) return
247 258 uni.showLoading({ title: `Printing ${percent}%`, mask: true })
... ...
美国版/Food Labeling Management App UniApp/src/pages/more/printers.vue
... ... @@ -24,6 +24,10 @@
24 24 <view class="debug-card">
25 25 <text class="debug-title">Debug Status</text>
26 26 <text class="debug-item">Current Mode: {{ debugInfo.currentMode }}</text>
  27 + <text class="debug-item">Available Mode: {{ availablePrinterTypesLabel }}</text>
  28 + <text class="debug-item">Device Model: {{ deviceIdentity.model || '-' }}</text>
  29 + <text class="debug-item">Device Brand: {{ deviceIdentity.brand || '-' }}</text>
  30 + <text class="debug-item">Device Product: {{ deviceIdentity.product || '-' }}</text>
27 31 <text class="debug-item">Classic Module: {{ debugInfo.classicModuleReady ? 'Ready' : 'Not Ready' }}</text>
28 32 <text class="debug-item">Paired Count: {{ debugInfo.pairedCount }}</text>
29 33 <text class="debug-item">Virtual BT Printer: {{ debugInfo.foundVirtualPrinter ? 'Found' : 'Not Found' }}</text>
... ... @@ -39,7 +43,7 @@
39 43 <view v-if="currentType" class="printer-card connected">
40 44 <view class="printer-icon"><AppIcon name="printer" size="md" color="white" /></view>
41 45 <view class="printer-info">
42   - <text class="printer-name">{{ currentType === 'builtin' ? t('printers.builtin') : (currentBt?.deviceName || 'Bluetooth Printer') }}</text>
  46 + <text class="printer-name">{{ currentDisplayName }}</text>
43 47 <text class="printer-loc">{{ currentType === 'builtin' ? '127.0.0.1' : (currentBt?.deviceId || '') }}</text>
44 48 <text class="printer-status">{{ t('printers.connected') }}</text>
45 49 </view>
... ... @@ -50,7 +54,7 @@
50 54 </view>
51 55  
52 56 <!-- Built-in Printer (Show when not connected) -->
53   - <view v-if="!currentType" class="printer-card" @click="connectBuiltin">
  57 + <view v-if="!currentType && isBuiltinModeAvailable" class="printer-card" @click="connectBuiltin">
54 58 <view class="printer-icon"><AppIcon name="printer" size="md" color="blue" /></view>
55 59 <view class="printer-info">
56 60 <text class="printer-name">{{ t('printers.builtin') }}</text>
... ... @@ -60,6 +64,7 @@
60 64 </view>
61 65  
62 66 <!-- Paired Devices -->
  67 + <template v-if="isBluetoothModeAvailable">
63 68 <!-- #ifdef APP-PLUS -->
64 69 <view class="section-header">
65 70 <text class="section-title">{{ t('printers.pairedDevices') }}</text>
... ... @@ -108,33 +113,60 @@
108 113 <view v-if="devices.length === 0 && !isScanning" class="empty-state">
109 114 <text class="empty-text">{{ t('printers.noDevices') }}</text>
110 115 </view>
  116 + </template>
111 117 </view>
112 118  
  119 + <canvas
  120 + canvas-id="test-print-canvas-printers"
  121 + id="test-print-canvas-printers"
  122 + :style="{
  123 + position: 'fixed',
  124 + left: '-9999px',
  125 + top: '-9999px',
  126 + width: testCanvasWidth + 'px',
  127 + height: testCanvasHeight + 'px',
  128 + opacity: 0,
  129 + pointerEvents: 'none',
  130 + }"
  131 + :width="testCanvasWidth"
  132 + :height="testCanvasHeight"
  133 + />
  134 +
113 135 <SideMenu v-model="isMenuOpen" />
114 136 </view>
115 137 </template>
116 138  
117 139 <script setup lang="ts">
118   -import { ref, onMounted, onUnmounted } from 'vue'
  140 +import { ref, onMounted, onUnmounted, getCurrentInstance, nextTick } from 'vue'
119 141 import { useI18n } from 'vue-i18n'
120 142 import AppIcon from '../../components/AppIcon.vue'
121 143 import SideMenu from '../../components/SideMenu.vue'
  144 +import { getDeviceIdentity } from '../../utils/deviceInfo'
122 145 import classicBluetooth from '../../utils/print/bluetoothTool.js'
123 146 import { ensureBluetoothPermissions } from '../../utils/print/bluetoothPermissions'
  147 +import { getAvailablePrinterTypes } from '../../utils/print/printerConnection'
124 148 import {
125 149 connectBluetoothPrinter,
126 150 describeDiscoveredPrinter,
127 151 disconnectCurrentPrinter,
  152 + getCurrentPrinterDriver,
128 153 getCurrentPrinterSummary,
129   - testPrintCurrentPrinter,
  154 + printImageDataForCurrentPrinter,
130 155 useBuiltinPrinter,
131 156 } from '../../utils/print/manager/printerManager'
  157 +import { getTestPrintCanvasSize, renderTestPrintCanvasImageData } from '../../utils/print/testPrintCanvas'
132 158  
133 159 const { t } = useI18n()
134 160 const isMenuOpen = ref(false)
  161 +const availablePrinterTypes = getAvailablePrinterTypes()
  162 +const availablePrinterTypesLabel = availablePrinterTypes.map(item => item === 'builtin' ? 'Built-in' : 'Bluetooth').join(' / ')
  163 +const deviceIdentity = getDeviceIdentity()
  164 +const isBluetoothModeAvailable = availablePrinterTypes.includes('bluetooth')
  165 +const isBuiltinModeAvailable = availablePrinterTypes.includes('builtin')
135 166  
136 167 const currentType = ref<'' | 'bluetooth' | 'builtin'>('')
137 168 const currentBt = ref<any>(null)
  169 +const currentDisplayName = ref('')
138 170 const isScanning = ref(false)
139 171 const devices = ref<any[]>([])
140 172 const pairedDevices = ref<any[]>([])
... ... @@ -149,11 +181,15 @@ const debugInfo = ref({
149 181 lastBleError: '',
150 182 locationServiceRequired: false,
151 183 })
  184 +const testCanvasWidth = ref(384)
  185 +const testCanvasHeight = ref(240)
  186 +const pageInstance = getCurrentInstance()?.proxy
152 187 let bleListenerRegistered = false
153 188  
154 189 const refreshStatus = () => {
155 190 const summary = getCurrentPrinterSummary()
156 191 currentType.value = summary.type
  192 + currentDisplayName.value = summary.displayName || ''
157 193 debugInfo.value.currentMode = summary.type || 'none'
158 194 currentBt.value = summary.type === 'bluetooth'
159 195 ? {
... ... @@ -378,16 +414,18 @@ const disconnect = async () =&gt; {
378 414  
379 415 const doTestPrint = async () => {
380 416 try {
381   - if (currentType.value === 'builtin') {
382   - uni.showModal({
383   - title: 'Current Mode is Built-in',
384   - content: 'This handheld device is currently using Built-in mode. Please disconnect it, then connect "Virtual BT Printer" from the Paired Devices list and test print again.',
385   - showCancel: false,
386   - })
387   - return
388   - }
389   - uni.showLoading({ title: t('labels.print.printing') })
390   - await testPrintCurrentPrinter()
  417 + uni.showLoading({ title: 'Rendering canvas...', mask: true })
  418 + const driver = getCurrentPrinterDriver()
  419 + const size = getTestPrintCanvasSize(driver.imageMaxWidthDots || (driver.protocol === 'esc' ? 384 : 800))
  420 + testCanvasWidth.value = size.width
  421 + testCanvasHeight.value = size.height
  422 + await nextTick()
  423 + const imageData = await renderTestPrintCanvasImageData('test-print-canvas-printers', pageInstance, size)
  424 + await printImageDataForCurrentPrinter(imageData, { printQty: 1 }, (percent) => {
  425 + if (percent < 100) {
  426 + uni.showLoading({ title: `Printing ${percent}%`, mask: true })
  427 + }
  428 + })
391 429 uni.hideLoading()
392 430 uni.showToast({ title: t('printers.testPrintSuccess') })
393 431 } catch (e: any) {
... ...
美国版/Food Labeling Management App UniApp/src/utils/deviceInfo.ts 0 → 100644
  1 +export interface DeviceIdentity {
  2 + model: string
  3 + brand: string
  4 + manufacturer: string
  5 + product: string
  6 + device: string
  7 + platform: string
  8 +}
  9 +
  10 +let cachedDeviceIdentity: DeviceIdentity | null = null
  11 +
  12 +function safeText (value: unknown): string {
  13 + return String(value || '').trim()
  14 +}
  15 +
  16 +export function getDeviceIdentity (): DeviceIdentity {
  17 + if (cachedDeviceIdentity) return cachedDeviceIdentity
  18 +
  19 + const info = (() => {
  20 + try {
  21 + return uni.getSystemInfoSync()
  22 + } catch (_) {
  23 + return {} as UniApp.GetSystemInfoResult
  24 + }
  25 + })()
  26 +
  27 + const identity: DeviceIdentity = {
  28 + model: safeText((info as any).model),
  29 + brand: safeText((info as any).brand),
  30 + manufacturer: '',
  31 + product: '',
  32 + device: '',
  33 + platform: safeText((info as any).platform),
  34 + }
  35 +
  36 + // #ifdef APP-PLUS
  37 + try {
  38 + const Build = plus.android.importClass('android.os.Build')
  39 + identity.model = safeText((Build as any).MODEL) || identity.model
  40 + identity.brand = safeText((Build as any).BRAND) || identity.brand
  41 + identity.manufacturer = safeText((Build as any).MANUFACTURER)
  42 + identity.product = safeText((Build as any).PRODUCT)
  43 + identity.device = safeText((Build as any).DEVICE)
  44 + } catch (_) {}
  45 + // #endif
  46 +
  47 + cachedDeviceIdentity = identity
  48 + return identity
  49 +}
  50 +
  51 +export function getDeviceFingerprint (): string {
  52 + const identity = getDeviceIdentity()
  53 + return [
  54 + identity.brand,
  55 + identity.manufacturer,
  56 + identity.model,
  57 + identity.product,
  58 + identity.device,
  59 + ]
  60 + .filter(Boolean)
  61 + .join(' | ')
  62 + .toLowerCase()
  63 +}
... ...
美国版/Food Labeling Management App UniApp/src/utils/print/bluetoothTool.js
... ... @@ -16,6 +16,7 @@ let IntentFilter = plus.android.importClass(&#39;android.content.IntentFilter&#39;)
16 16 let BluetoothDevice = plus.android.importClass('android.bluetooth.BluetoothDevice')
17 17 let UUID = plus.android.importClass('java.util.UUID')
18 18 let Toast = plus.android.importClass('android.widget.Toast')
  19 +let Thread = plus.android.importClass('java.lang.Thread')
19 20 let MY_UUID = UUID.fromString('00001101-0000-1000-8000-00805F9B34FB')
20 21  
21 22 let invoke = plus.android.invoke
... ... @@ -62,19 +63,78 @@ function normalizeWriteByte (value) {
62 63 return byte & 0xff
63 64 }
64 65  
  66 +function toSignedWriteByte (value) {
  67 + const byte = normalizeWriteByte(value)
  68 + return byte >= 128 ? byte - 256 : byte
  69 +}
  70 +
65 71 function normalizeWriteChunk (byteData, start, end) {
66 72 const out = []
67 73 for (let i = start; i < end; i++) {
68   - const value = normalizeWriteByte(byteData[i])
69   - if (value >= 128) {
70   - out.push(value - 256)
71   - } else {
72   - out.push(value)
73   - }
  74 + out.push(toSignedWriteByte(byteData[i]))
74 75 }
75 76 return out
76 77 }
77 78  
  79 +function isConnectionStateConnected (state) {
  80 + return String(state || '').trim().toLowerCase() === 'connected'
  81 +}
  82 +
  83 +function runOnUiThread (fn) {
  84 + // #ifdef APP-PLUS
  85 + try {
  86 + if (activity && typeof activity.runOnUiThread === 'function') {
  87 + const runnable = plus.android.implements('java.lang.Runnable', {
  88 + run: function () {
  89 + try {
  90 + fn && fn()
  91 + } catch (e) {
  92 + console.error('runOnUiThread callback failed:', e)
  93 + }
  94 + },
  95 + })
  96 + activity.runOnUiThread(runnable)
  97 + return
  98 + }
  99 + } catch (e) {
  100 + console.error('runOnUiThread failed:', e)
  101 + }
  102 + // #endif
  103 + try {
  104 + setTimeout(() => {
  105 + try {
  106 + fn && fn()
  107 + } catch (e) {
  108 + console.error('setTimeout callback failed:', e)
  109 + }
  110 + }, 0)
  111 + } catch (e) {
  112 + console.error('async callback fallback failed:', e)
  113 + }
  114 +}
  115 +
  116 +function startBackgroundTask (task) {
  117 + // #ifdef APP-PLUS
  118 + const runnable = plus.android.implements('java.lang.Runnable', {
  119 + run: function () {
  120 + try {
  121 + task && task()
  122 + } catch (e) {
  123 + console.error('Background task failed:', e)
  124 + }
  125 + },
  126 + })
  127 + const thread = new Thread(runnable)
  128 + thread.start()
  129 + return
  130 + // #endif
  131 + try {
  132 + task && task()
  133 + } catch (e) {
  134 + console.error('Fallback task failed:', e)
  135 + }
  136 +}
  137 +
78 138 function createSocketCandidates (device) {
79 139 return [
80 140 {
... ... @@ -109,6 +169,7 @@ var blueToothTool = {
109 169 lastSendError: '',
110 170 outputReady: false,
111 171 lastSendMode: 'idle',
  172 + isSending: false,
112 173 },
113 174 options: {
114 175 listenBTStatusCallback: function (state) {},
... ... @@ -489,26 +550,37 @@ var blueToothTool = {
489 550 },
490 551 sendByteData (byteData) {
491 552 // #ifdef APP-PLUS
492   - if (!this.ensureConnection(this.state.lastAddress)) {
  553 + if (!btOutStream) {
  554 + this.state.outputReady = false
  555 + this.state.connectionState = 'error'
  556 + this.setErrorState('Classic Bluetooth output stream not ready', 'send')
  557 + this.shortToast('Not connected')
  558 + return false
  559 + }
  560 + const socketConnected = this.isSocketConnected()
  561 + const connectionUsable = socketConnected || isConnectionStateConnected(this.state.connectionState)
  562 + if (!connectionUsable) {
  563 + this.state.connectionState = 'error'
  564 + this.setErrorState('Classic Bluetooth connection is not ready', 'send')
493 565 this.shortToast('Not connected')
494 566 return false
495 567 }
496 568 try {
497   - const CHUNK_SIZE = 512
  569 + const CHUNK_SIZE = 4096
498 570 this.state.lastSendMode = 'chunk-write'
499 571 this.state.lastSendError = ''
500 572 for (let i = 0; i < byteData.length; i += CHUNK_SIZE) {
501   - const chunk = normalizeWriteChunk(byteData, i, Math.min(i + CHUNK_SIZE, byteData.length))
  573 + const chunk = byteData.slice(i, Math.min(i + CHUNK_SIZE, byteData.length))
502 574 try {
503 575 btOutStream.write(chunk)
504 576 } catch (writeChunkError) {
505 577 this.state.lastSendMode = 'byte-write-fallback'
  578 + const signedChunk = normalizeWriteChunk(byteData, i, Math.min(i + CHUNK_SIZE, byteData.length))
506 579 for (let j = 0; j < chunk.length; j++) {
507   - invoke(btOutStream, 'write', normalizeWriteByte(chunk[j]))
  580 + invoke(btOutStream, 'write', normalizeWriteByte(signedChunk[j]))
508 581 }
509 582 }
510 583 }
511   - invoke(btOutStream, 'flush')
512 584 return true
513 585 } catch (e) {
514 586 const message = getErrorMessage(e)
... ... @@ -520,6 +592,53 @@ var blueToothTool = {
520 592 // #endif
521 593 return false
522 594 },
  595 + sendByteDataAsync (byteData, callback) {
  596 + // #ifdef APP-PLUS
  597 + if (!btOutStream) {
  598 + this.state.outputReady = false
  599 + this.state.connectionState = 'error'
  600 + this.setErrorState('Classic Bluetooth output stream not ready', 'send')
  601 + runOnUiThread(() => {
  602 + callback && callback(false, this.getLastError())
  603 + })
  604 + return false
  605 + }
  606 + const socketConnected = this.isSocketConnected()
  607 + const connectionUsable = socketConnected || isConnectionStateConnected(this.state.connectionState)
  608 + if (!connectionUsable) {
  609 + this.state.connectionState = 'error'
  610 + this.setErrorState('Classic Bluetooth connection is not ready', 'send')
  611 + runOnUiThread(() => {
  612 + callback && callback(false, this.getLastError())
  613 + })
  614 + return false
  615 + }
  616 + this.state.isSending = true
  617 + this.state.lastSendError = ''
  618 + startBackgroundTask(() => {
  619 + let ok = false
  620 + let errorMessage = ''
  621 + try {
  622 + ok = this.sendByteData(byteData)
  623 + if (!ok) {
  624 + errorMessage = this.getLastError() || 'Classic Bluetooth send failed'
  625 + }
  626 + } catch (e) {
  627 + errorMessage = getErrorMessage(e)
  628 + this.setErrorState(errorMessage, 'send')
  629 + this.state.connectionState = 'error'
  630 + } finally {
  631 + this.state.isSending = false
  632 + }
  633 + runOnUiThread(() => {
  634 + callback && callback(ok, errorMessage)
  635 + })
  636 + })
  637 + return true
  638 + // #endif
  639 + callback && callback(false, 'Classic Bluetooth is only available in the app.')
  640 + return false
  641 + },
523 642 isSocketConnected () {
524 643 // #ifdef APP-PLUS
525 644 if (!btSocket) return false
... ... @@ -537,7 +656,7 @@ var blueToothTool = {
537 656 if (targetAddress) {
538 657 this.state.lastAddress = targetAddress
539 658 }
540   - if (btOutStream && this.isSocketConnected()) {
  659 + if (btOutStream && (this.isSocketConnected() || isConnectionStateConnected(this.state.connectionState))) {
541 660 this.state.outputReady = true
542 661 return true
543 662 }
... ...
美国版/Food Labeling Management App UniApp/src/utils/print/drivers/d320fax.ts
... ... @@ -25,6 +25,8 @@ export const d320faxDriver: PrinterDriver = {
25 25 protocol: 'tsc',
26 26 preferredConnection: 'classic',
27 27 preferredBleMtu: 20,
  28 + imageMaxWidthDots: 800,
  29 + imageDpi: 203,
28 30 keywords: KEYWORDS,
29 31 matches (device) {
30 32 return score(device)
... ...
美国版/Food Labeling Management App UniApp/src/utils/print/drivers/genericTsc.ts
... ... @@ -22,6 +22,8 @@ export const genericTscDriver: PrinterDriver = {
22 22 displayName: 'Generic TSC Printer',
23 23 protocol: 'tsc',
24 24 preferredBleMtu: 20,
  25 + imageMaxWidthDots: 800,
  26 + imageDpi: 203,
25 27 keywords: GENERIC_TSC_KEYWORDS,
26 28 matches (device) {
27 29 return scoreByKeywords(device, GENERIC_TSC_KEYWORDS)
... ...
美国版/Food Labeling Management App UniApp/src/utils/print/drivers/gpR3.ts
... ... @@ -25,6 +25,8 @@ export const gpR3Driver: PrinterDriver = {
25 25 protocol: 'esc',
26 26 preferredConnection: 'classic',
27 27 preferredBleMtu: 20,
  28 + imageMaxWidthDots: 384,
  29 + imageDpi: 203,
28 30 keywords: KEYWORDS,
29 31 matches (device) {
30 32 return score(device)
... ...
美国版/Food Labeling Management App UniApp/src/utils/print/imageRaster.ts 0 → 100644
  1 +import type { MonochromeImageData, PrintImageOptions, PrinterDriver, RawImageDataSource } from './types/printer'
  2 +
  3 +const DEFAULT_IMAGE_THRESHOLD = 180
  4 +
  5 +function yieldToUi (): Promise<void> {
  6 + return new Promise((resolve) => {
  7 + setTimeout(resolve, 0)
  8 + })
  9 +}
  10 +
  11 +function ensureMultipleOf8 (value: number, maxValue?: number): number {
  12 + const safeMax = maxValue && maxValue > 0 ? Math.max(8, Math.floor(maxValue)) : 0
  13 + let width = Math.max(8, Math.round(value || 0))
  14 + if (safeMax && width > safeMax) width = safeMax
  15 + if (width % 8 !== 0) {
  16 + width += 8 - (width % 8)
  17 + if (safeMax && width > safeMax) {
  18 + width = safeMax - (safeMax % 8)
  19 + if (width <= 0) width = 8
  20 + }
  21 + }
  22 + return width
  23 +}
  24 +
  25 +function normalizeBase64Payload (source: string): string {
  26 + const value = String(source || '').trim()
  27 + if (!value) return ''
  28 + if (value.startsWith('data:image/')) {
  29 + const index = value.indexOf(',')
  30 + return index >= 0 ? value.slice(index + 1) : ''
  31 + }
  32 + if (/^[A-Za-z0-9+/=\r\n]+$/.test(value) && value.length > 128) {
  33 + return value.replace(/\s+/g, '')
  34 + }
  35 + return ''
  36 +}
  37 +
  38 +function resolveLocalImagePath (source: string): string {
  39 + let path = String(source || '').trim()
  40 + if (!path) return ''
  41 + if (path.startsWith('file://')) {
  42 + path = path.replace(/^file:\/\//, '')
  43 + }
  44 + // #ifdef APP-PLUS
  45 + try {
  46 + const converted = plus.io.convertLocalFileSystemURL(path)
  47 + if (converted) path = converted
  48 + } catch (_) {}
  49 + // #endif
  50 + try {
  51 + path = decodeURIComponent(path)
  52 + } catch (_) {}
  53 + return path
  54 +}
  55 +
  56 +function getDefaultMaxWidthDots (driver: PrinterDriver): number {
  57 + if (driver.imageMaxWidthDots && driver.imageMaxWidthDots > 0) return driver.imageMaxWidthDots
  58 + return driver.protocol === 'esc' ? 384 : 800
  59 +}
  60 +
  61 +export function rasterizeImageData (
  62 + source: RawImageDataSource,
  63 + options: PrintImageOptions = {}
  64 +): MonochromeImageData {
  65 + const sourceWidth = Math.max(1, Math.round(source.width || 0))
  66 + const sourceHeight = Math.max(1, Math.round(source.height || 0))
  67 + const targetWidth = ensureMultipleOf8(sourceWidth)
  68 + const threshold = options.threshold != null ? Number(options.threshold) : DEFAULT_IMAGE_THRESHOLD
  69 + const pixels: number[] = new Array(targetWidth * sourceHeight).fill(0)
  70 +
  71 + for (let y = 0; y < sourceHeight; y++) {
  72 + for (let x = 0; x < sourceWidth; x++) {
  73 + const index = (y * sourceWidth + x) * 4
  74 + const alpha = Number(source.data[index + 3] ?? 255)
  75 + const red = Number(source.data[index] ?? 255)
  76 + const green = Number(source.data[index + 1] ?? 255)
  77 + const blue = Number(source.data[index + 2] ?? 255)
  78 + const gray = red * 0.299 + green * 0.587 + blue * 0.114
  79 + pixels[y * targetWidth + x] = alpha <= 10 || gray > threshold ? 0 : 1
  80 + }
  81 + }
  82 +
  83 + return {
  84 + width: targetWidth,
  85 + height: sourceHeight,
  86 + pixels,
  87 + }
  88 +}
  89 +
  90 +export async function rasterizeImageForPrinter (
  91 + source: string,
  92 + driver: PrinterDriver,
  93 + options: PrintImageOptions = {}
  94 +): Promise<MonochromeImageData> {
  95 + // #ifdef APP-PLUS
  96 + const rawSource = String(source || '').trim()
  97 + if (!rawSource) {
  98 + throw new Error('IMAGE_SOURCE_EMPTY')
  99 + }
  100 +
  101 + const BitmapFactory = plus.android.importClass('android.graphics.BitmapFactory')
  102 + const Bitmap = plus.android.importClass('android.graphics.Bitmap')
  103 + const Base64 = plus.android.importClass('android.util.Base64')
  104 +
  105 + const base64Payload = normalizeBase64Payload(rawSource)
  106 + let sourceBitmap: any = null
  107 +
  108 + if (base64Payload) {
  109 + const bytes = Base64.decode(base64Payload, 0)
  110 + sourceBitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length)
  111 + } else {
  112 + const localPath = resolveLocalImagePath(rawSource)
  113 + sourceBitmap = BitmapFactory.decodeFile(localPath)
  114 + }
  115 +
  116 + if (!sourceBitmap) {
  117 + throw new Error('IMAGE_DECODE_FAILED')
  118 + }
  119 +
  120 + const sourceWidth = Number(sourceBitmap.getWidth ? sourceBitmap.getWidth() : 0)
  121 + const sourceHeight = Number(sourceBitmap.getHeight ? sourceBitmap.getHeight() : 0)
  122 + if (!sourceWidth || !sourceHeight) {
  123 + try {
  124 + sourceBitmap.recycle && sourceBitmap.recycle()
  125 + } catch (_) {}
  126 + throw new Error('IMAGE_SIZE_INVALID')
  127 + }
  128 +
  129 + const protocolMaxWidth = getDefaultMaxWidthDots(driver)
  130 + const maxWidthDots = options.maxWidthDots && options.maxWidthDots > 0 ? options.maxWidthDots : protocolMaxWidth
  131 + const targetWidth = ensureMultipleOf8(options.targetWidthDots || sourceWidth, maxWidthDots)
  132 + const aspectRatio = sourceHeight / sourceWidth
  133 + const targetHeight = Math.max(
  134 + 1,
  135 + Math.round(options.targetHeightDots || (targetWidth * aspectRatio))
  136 + )
  137 + const threshold = options.threshold != null ? Number(options.threshold) : DEFAULT_IMAGE_THRESHOLD
  138 +
  139 + const scaledBitmap = Bitmap.createScaledBitmap(sourceBitmap, targetWidth, targetHeight, true)
  140 + const rasterPixels: number[] = new Array(targetWidth * targetHeight)
  141 +
  142 + for (let y = 0; y < targetHeight; y++) {
  143 + if (y > 0 && y % 16 === 0) {
  144 + await yieldToUi()
  145 + }
  146 + for (let x = 0; x < targetWidth; x++) {
  147 + const color = Number(scaledBitmap.getPixel(x, y))
  148 + const alpha = (color >>> 24) & 0xff
  149 + const red = (color >>> 16) & 0xff
  150 + const green = (color >>> 8) & 0xff
  151 + const blue = color & 0xff
  152 + const gray = red * 0.299 + green * 0.587 + blue * 0.114
  153 + rasterPixels[y * targetWidth + x] = alpha <= 10 || gray > threshold ? 0 : 1
  154 + }
  155 + }
  156 +
  157 + try {
  158 + if (scaledBitmap !== sourceBitmap && sourceBitmap?.recycle) sourceBitmap.recycle()
  159 + } catch (_) {}
  160 + try {
  161 + scaledBitmap?.recycle && scaledBitmap.recycle()
  162 + } catch (_) {}
  163 +
  164 + return {
  165 + width: targetWidth,
  166 + height: targetHeight,
  167 + pixels: rasterPixels,
  168 + }
  169 + // #endif
  170 +
  171 + // #ifndef APP-PLUS
  172 + throw new Error('IMAGE_PRINT_APP_ONLY')
  173 + // #endif
  174 +}
... ...
美国版/Food Labeling Management App UniApp/src/utils/print/manager/printerManager.ts
... ... @@ -8,14 +8,29 @@ import {
8 8 setBuiltinPrinter,
9 9 } from '../printerConnection'
10 10 import classicBluetooth from '../bluetoothTool.js'
  11 +import { rasterizeImageData, rasterizeImageForPrinter } from '../imageRaster'
  12 +import { buildEscPosImageData, buildEscPosTemplateData } from '../protocols/escPosBuilder'
  13 +import { buildTscImageData, buildTscTemplateData } from '../protocols/tscProtocol'
  14 +import { adaptSystemLabelTemplate } from '../systemTemplateAdapter'
11 15 import { describePrinterCandidate, getPrinterDriverByKey, resolvePrinterDriver } from './driverRegistry'
12 16 import type {
13 17 CurrentPrinterSummary,
14 18 LabelPrintPayload,
  19 + LabelTemplateData,
  20 + RawImageDataSource,
  21 + PrintImageOptions,
15 22 PrinterCandidate,
16 23 PrinterDriver,
  24 + StructuredLabelTemplate,
  25 + SystemLabelTemplate,
17 26 } from '../types/printer'
18 27  
  28 +function getPrinterTypeDisplayName (type: '' | 'bluetooth' | 'builtin'): string {
  29 + if (type === 'bluetooth') return 'Bluetooth'
  30 + if (type === 'builtin') return 'Built-in'
  31 + return ''
  32 +}
  33 +
19 34 function connectClassicBluetooth (device: PrinterCandidate, driver: PrinterDriver): Promise<void> {
20 35 return new Promise((resolve, reject) => {
21 36 // #ifdef APP-PLUS
... ... @@ -159,7 +174,7 @@ export function getCurrentPrinterSummary (): CurrentPrinterSummary {
159 174 if (type === 'builtin') {
160 175 return {
161 176 type,
162   - displayName: 'Built-in Printer',
  177 + displayName: getPrinterTypeDisplayName(type),
163 178 deviceId: 'builtin',
164 179 driverKey: driver.key,
165 180 driverName: driver.displayName,
... ... @@ -172,7 +187,7 @@ export function getCurrentPrinterSummary (): CurrentPrinterSummary {
172 187 if (connection) {
173 188 return {
174 189 type,
175   - displayName: connection.deviceName || driver.displayName,
  190 + displayName: getPrinterTypeDisplayName(type),
176 191 deviceId: connection.deviceId,
177 192 driverKey: driver.key,
178 193 driverName: driver.displayName,
... ... @@ -207,6 +222,74 @@ export async function printLabelForCurrentPrinter (
207 222 return driver
208 223 }
209 224  
  225 +export async function printImageForCurrentPrinter (
  226 + imageSource: string,
  227 + options: PrintImageOptions = {},
  228 + onProgress?: (percent: number) => void
  229 +): Promise<PrinterDriver> {
  230 + const driver = getCurrentPrinterDriver()
  231 + const raster = await rasterizeImageForPrinter(imageSource, driver, options)
  232 + if (onProgress) onProgress(5)
  233 + let data: number[] = []
  234 +
  235 + if (driver.protocol === 'esc') {
  236 + data = buildEscPosImageData(raster, options)
  237 + } else {
  238 + data = buildTscImageData(raster, options, driver.imageDpi || 203)
  239 + }
  240 +
  241 + await sendToPrinter(data, onProgress)
  242 + return driver
  243 +}
  244 +
  245 +export async function printImageDataForCurrentPrinter (
  246 + imageData: RawImageDataSource,
  247 + options: PrintImageOptions = {},
  248 + onProgress?: (percent: number) => void
  249 +): Promise<PrinterDriver> {
  250 + const driver = getCurrentPrinterDriver()
  251 + const raster = rasterizeImageData(imageData, options)
  252 + if (onProgress) onProgress(5)
  253 + const data = driver.protocol === 'esc'
  254 + ? buildEscPosImageData(raster, options)
  255 + : buildTscImageData(raster, options, driver.imageDpi || 203)
  256 + await sendToPrinter(data, onProgress)
  257 + return driver
  258 +}
  259 +
  260 +export async function printTemplateForCurrentPrinter (
  261 + template: StructuredLabelTemplate,
  262 + data: LabelTemplateData = {},
  263 + onProgress?: (percent: number) => void
  264 +): Promise<PrinterDriver> {
  265 + const driver = getCurrentPrinterDriver()
  266 + const bytes = driver.protocol === 'esc'
  267 + ? buildEscPosTemplateData(template, data)
  268 + : buildTscTemplateData(template, data)
  269 + await sendToPrinter(bytes, onProgress)
  270 + return driver
  271 +}
  272 +
  273 +export async function printSystemTemplateForCurrentPrinter (
  274 + template: SystemLabelTemplate,
  275 + data: LabelTemplateData = {},
  276 + options: {
  277 + printQty?: number
  278 + } = {},
  279 + onProgress?: (percent: number) => void
  280 +): Promise<PrinterDriver> {
  281 + const driver = getCurrentPrinterDriver()
  282 + const structuredTemplate = adaptSystemLabelTemplate(template, data, {
  283 + dpi: driver.imageDpi || 203,
  284 + printQty: options.printQty || 1,
  285 + })
  286 + const bytes = driver.protocol === 'esc'
  287 + ? buildEscPosTemplateData(structuredTemplate)
  288 + : buildTscTemplateData(structuredTemplate)
  289 + await sendToPrinter(bytes, onProgress)
  290 + return driver
  291 +}
  292 +
210 293 export function describeDiscoveredPrinter (device: PrinterCandidate) {
211 294 return describePrinterCandidate(device)
212 295 }
... ...
美国版/Food Labeling Management App UniApp/src/utils/print/printerConnection.ts
... ... @@ -3,6 +3,7 @@
3 3 */
4 4 import type { ActiveBtDeviceType, PrinterType } from './types/printer'
5 5 import classicBluetooth from './bluetoothTool.js'
  6 +import { getDeviceFingerprint } from '../deviceInfo'
6 7  
7 8 const STORAGE_PRINTER_TYPE = 'printerType'
8 9 const STORAGE_BT_DEVICE_ID = 'btDeviceId'
... ... @@ -15,6 +16,10 @@ const STORAGE_BUILTIN_PORT = &#39;builtinPort&#39;
15 16 const STORAGE_PRINTER_DRIVER_KEY = 'printerDriverKey'
16 17  
17 18 const BUILTIN_PROBE_PORTS = [9100, 4000, 9000, 6000]
  19 +const BUILTIN_PRINTER_DEVICE_KEYWORDS: string[] = [
  20 + // 在这里补充需要走 Built-in 的设备型号关键字(小写匹配)
  21 + // 例如:'desktop-aio', 'pos-terminal-x1'
  22 +]
18 23 export type BtDeviceType = ActiveBtDeviceType
19 24  
20 25 export const PrinterStorageKeys = {
... ... @@ -71,7 +76,11 @@ export function clearPrinter () {
71 76 const BLE_MTU_DEFAULT = 20
72 77  
73 78 export function getPrinterType (): PrinterType | '' {
74   - return (uni.getStorageSync(STORAGE_PRINTER_TYPE) as PrinterType) || ''
  79 + const type = (uni.getStorageSync(STORAGE_PRINTER_TYPE) as PrinterType) || ''
  80 + if (!type) return ''
  81 + if (getAvailablePrinterTypes().includes(type)) return type
  82 + clearPrinter()
  83 + return ''
75 84 }
76 85  
77 86 export function getCurrentPrinterDriverKey (): string {
... ... @@ -116,6 +125,58 @@ export function isBuiltinConnected (): boolean {
116 125 return getPrinterType() === 'builtin'
117 126 }
118 127  
  128 +export function isBuiltinPrinterAvailable (): boolean {
  129 + // #ifdef APP-PLUS
  130 + try {
  131 + const plugin = (uni as any)?.requireNativePlugin
  132 + ? (uni as any).requireNativePlugin('moe-tcp-client')
  133 + : null
  134 + return !!plugin
  135 + } catch (_) {
  136 + return false
  137 + }
  138 + // #endif
  139 + // #ifndef APP-PLUS
  140 + return false
  141 + // #endif
  142 +}
  143 +
  144 +export function isBuiltinPrinterEnabledByDeviceModel (): boolean {
  145 + const fingerprint = getDeviceFingerprint()
  146 + if (!fingerprint) return false
  147 + return BUILTIN_PRINTER_DEVICE_KEYWORDS.some(keyword => fingerprint.includes(String(keyword || '').toLowerCase()))
  148 +}
  149 +
  150 +export function getAvailablePrinterTypes (): PrinterType[] {
  151 + if (isBuiltinPrinterAvailable() && isBuiltinPrinterEnabledByDeviceModel()) {
  152 + return ['builtin']
  153 + }
  154 + return ['bluetooth']
  155 +}
  156 +
  157 +function buildClassicBluetoothError (message: string, deviceId?: string): Error {
  158 + const baseMessage = String(message || 'Classic Bluetooth error')
  159 + if (!classicBluetooth || typeof classicBluetooth.getDebugState !== 'function') {
  160 + return new Error(baseMessage)
  161 + }
  162 + try {
  163 + const debugState = classicBluetooth.getDebugState() || {}
  164 + const details: string[] = []
  165 + if (deviceId) details.push(`device=${deviceId}`)
  166 + if (debugState.lastSocketStrategy) details.push(`socket=${debugState.lastSocketStrategy}`)
  167 + if (debugState.connectionState) details.push(`state=${debugState.connectionState}`)
  168 + if (typeof debugState.socketConnected === 'boolean') details.push(`connected=${debugState.socketConnected}`)
  169 + if (typeof debugState.outputReady === 'boolean') details.push(`outputReady=${debugState.outputReady}`)
  170 + if (debugState.lastSendMode) details.push(`sendMode=${debugState.lastSendMode}`)
  171 + if (debugState.lastSendError) details.push(`sendError=${debugState.lastSendError}`)
  172 + else if (debugState.lastError) details.push(`lastError=${debugState.lastError}`)
  173 + if (details.length === 0) return new Error(baseMessage)
  174 + return new Error(`${baseMessage}\n${details.join('\n')}`)
  175 + } catch (_) {
  176 + return new Error(baseMessage)
  177 + }
  178 +}
  179 +
119 180 /**
120 181 * 发送打印数据到当前已选打印机
121 182 * @param data 字节数组(TSC 指令)
... ... @@ -200,29 +261,53 @@ function sendViaClassic (
200 261 reject(new Error('Classic Bluetooth not available'))
201 262 return
202 263 }
203   - const ready = typeof classicBluetooth.ensureConnection === 'function'
204   - ? classicBluetooth.ensureConnection(conn.deviceId)
  264 + const debugState = typeof classicBluetooth.getDebugState === 'function'
  265 + ? classicBluetooth.getDebugState()
  266 + : null
  267 + const connectionState = String(debugState?.connectionState || '').trim().toLowerCase()
  268 + const ready = debugState
  269 + ? (!!debugState.outputReady && (!!debugState.socketConnected || connectionState === 'connected'))
205 270 : true
206 271 if (!ready) {
207 272 const errorMessage = typeof classicBluetooth.getLastError === 'function'
208 273 ? classicBluetooth.getLastError()
209 274 : ''
210   - reject(new Error(errorMessage || 'Classic Bluetooth connection is not ready'))
  275 + reject(buildClassicBluetoothError(errorMessage || 'Classic Bluetooth connection is not ready', conn.deviceId))
  276 + return
  277 + }
  278 +
  279 + const sendData = data.map((byte) => {
  280 + const value = byte & 0xff
  281 + return value >= 128 ? value - 256 : value
  282 + })
  283 +
  284 + if (typeof classicBluetooth.sendByteDataAsync === 'function') {
  285 + classicBluetooth.sendByteDataAsync(sendData, (ok: boolean, errorMessage?: string) => {
  286 + if (onProgress) onProgress(100)
  287 + if (ok) {
  288 + resolve()
  289 + return
  290 + }
  291 + reject(buildClassicBluetoothError(
  292 + errorMessage || classicBluetooth.getLastError?.() || 'Classic Bluetooth send failed',
  293 + conn.deviceId
  294 + ))
  295 + })
211 296 return
212 297 }
213 298  
214   - const sendData = data.map((byte) => byte & 0xff)
215 299 const ok = classicBluetooth.sendByteData(sendData)
216 300 if (onProgress) onProgress(100)
217   - if (ok) resolve()
218   - else {
219   - const errorMessage = typeof classicBluetooth.getLastError === 'function'
220   - ? classicBluetooth.getLastError()
221   - : ''
222   - reject(new Error(errorMessage || 'Classic Bluetooth send failed'))
  301 + if (ok) {
  302 + resolve()
  303 + return
223 304 }
  305 + const errorMessage = typeof classicBluetooth.getLastError === 'function'
  306 + ? classicBluetooth.getLastError()
  307 + : ''
  308 + reject(buildClassicBluetoothError(errorMessage || 'Classic Bluetooth send failed', conn.deviceId))
224 309 } catch (e: any) {
225   - reject(e)
  310 + reject(buildClassicBluetoothError(e?.message || String(e || 'Classic Bluetooth send exception'), conn.deviceId))
226 311 }
227 312 })
228 313 // #endif
... ...
美国版/Food Labeling Management App UniApp/src/utils/print/protocols/escPosBuilder.ts
1   -import type { LabelPrintPayload } from '../types/printer'
  1 +import type {
  2 + LabelPrintPayload,
  3 + LabelTemplateData,
  4 + MonochromeImageData,
  5 + PrintImageOptions,
  6 + StructuredLabelTemplate,
  7 +} from '../types/printer'
  8 +import { resolveEscTemplate } from '../templateRenderer'
  9 +import { createTestPrintTemplate } from '../templates/testPrintTemplate'
2 10  
3 11 function stringToBytes (str: string): number[] {
4 12 const out: number[] = []
... ... @@ -35,6 +43,10 @@ function appendLine (out: number[], text = &#39;&#39;) {
35 43 out.push(0x0a)
36 44 }
37 45  
  46 +function appendBytes (out: number[], bytes: number[]) {
  47 + for (let i = 0; i < bytes.length; i++) out.push(bytes[i])
  48 +}
  49 +
38 50 function appendAlign (out: number[], align: 0 | 1 | 2) {
39 51 out.push(0x1b, 0x61, align)
40 52 }
... ... @@ -48,6 +60,47 @@ function appendSize (out: number[], width = 0, height = 0) {
48 60 out.push(0x1d, 0x21, value)
49 61 }
50 62  
  63 +function clamp (value: number, min: number, max: number): number {
  64 + return Math.max(min, Math.min(max, Math.round(value)))
  65 +}
  66 +
  67 +function normalizeEscBarcodeType (value?: string): number {
  68 + const key = String(value || 'CODE128').trim().toUpperCase()
  69 + const map: Record<string, number> = {
  70 + UPCA: 65,
  71 + UPCE: 66,
  72 + EAN13: 67,
  73 + EAN8: 68,
  74 + CODE39: 69,
  75 + ITF: 70,
  76 + CODABAR: 71,
  77 + CODE93: 72,
  78 + CODE128: 73,
  79 + }
  80 + return map[key] || 73
  81 +}
  82 +
  83 +function normalizeEscQrLevel (value?: string): number {
  84 + const key = String(value || 'M').trim().toUpperCase()
  85 + const map: Record<string, number> = {
  86 + L: 48,
  87 + M: 49,
  88 + Q: 50,
  89 + H: 51,
  90 + }
  91 + return map[key] || 49
  92 +}
  93 +
  94 +function appendHorizontalRule (out: number[], width = 32) {
  95 + appendLine(out, '+' + '-'.repeat(Math.max(width - 2, 0)) + '+')
  96 +}
  97 +
  98 +function appendBoxLine (out: number[], text = '', width = 32) {
  99 + const innerWidth = Math.max(width - 4, 0)
  100 + const value = text.length > innerWidth ? text.slice(0, innerWidth) : text.padEnd(innerWidth, ' ')
  101 + appendLine(out, `| ${value} |`)
  102 +}
  103 +
51 104 function createEscDocument (builder: (out: number[]) => void): number[] {
52 105 const out: number[] = []
53 106 out.push(0x1b, 0x40)
... ... @@ -56,21 +109,80 @@ function createEscDocument (builder: (out: number[]) =&gt; void): number[] {
56 109 return out
57 110 }
58 111  
  112 +function appendRasterImage (out: number[], image: MonochromeImageData) {
  113 + const bytesPerRow = image.width / 8
  114 + const xL = bytesPerRow & 0xff
  115 + const xH = (bytesPerRow >> 8) & 0xff
  116 + const yL = image.height & 0xff
  117 + const yH = (image.height >> 8) & 0xff
  118 + out.push(0x1d, 0x76, 0x30, 0x00, xL, xH, yL, yH)
  119 + for (let y = 0; y < image.height; y++) {
  120 + for (let byteIndex = 0; byteIndex < bytesPerRow; byteIndex++) {
  121 + let value = 0
  122 + for (let bit = 0; bit < 8; bit++) {
  123 + const x = byteIndex * 8 + bit
  124 + const pixel = image.pixels[y * image.width + x]
  125 + if (pixel) value |= 1 << (7 - bit)
  126 + }
  127 + out.push(value & 0xff)
  128 + }
  129 + }
  130 +}
  131 +
  132 +function appendBarcode (out: number[], item: {
  133 + value: string
  134 + align?: 0 | 1 | 2
  135 + symbology?: string
  136 + height?: number
  137 + width?: number
  138 + showText?: boolean
  139 +}) {
  140 + const value = String(item.value || '')
  141 + if (!value) return
  142 + appendAlign(out, item.align ?? 1)
  143 + out.push(0x1d, 0x48, item.showText === false ? 0 : 2)
  144 + out.push(0x1d, 0x68, clamp(item.height || 96, 1, 255))
  145 + out.push(0x1d, 0x77, clamp(item.width || 3, 2, 6))
  146 +
  147 + const type = normalizeEscBarcodeType(item.symbology)
  148 + const bytes = stringToBytes(value)
  149 +
  150 + if (type >= 73) {
  151 + out.push(0x1d, 0x6b, type, clamp(bytes.length, 0, 255))
  152 + appendBytes(out, bytes)
  153 + } else {
  154 + out.push(0x1d, 0x6b, type)
  155 + appendBytes(out, bytes)
  156 + out.push(0x00)
  157 + }
  158 + out.push(0x0a)
  159 +}
  160 +
  161 +function appendQrCode (out: number[], item: {
  162 + value: string
  163 + align?: 0 | 1 | 2
  164 + size?: number
  165 + level?: 'L' | 'M' | 'Q' | 'H'
  166 +}) {
  167 + const value = String(item.value || '')
  168 + if (!value) return
  169 + const bytes = stringToBytes(value)
  170 + const storeLength = bytes.length + 3
  171 + const pL = storeLength & 0xff
  172 + const pH = (storeLength >> 8) & 0xff
  173 +
  174 + appendAlign(out, item.align ?? 1)
  175 + out.push(0x1d, 0x28, 0x6b, 0x04, 0x00, 0x31, 0x41, 0x32, 0x00)
  176 + out.push(0x1d, 0x28, 0x6b, 0x03, 0x00, 0x31, 0x43, clamp(item.size || 5, 1, 16))
  177 + out.push(0x1d, 0x28, 0x6b, 0x03, 0x00, 0x31, 0x45, normalizeEscQrLevel(item.level))
  178 + out.push(0x1d, 0x28, 0x6b, pL, pH, 0x31, 0x50, 0x30)
  179 + appendBytes(out, bytes)
  180 + out.push(0x1d, 0x28, 0x6b, 0x03, 0x00, 0x31, 0x51, 0x30)
  181 + out.push(0x0a)
  182 +}
  183 +
59 184 export function buildEscPosTestPrintData (): number[] {
60   - return createEscDocument((out) => {
61   - appendAlign(out, 1)
62   - appendBold(out, true)
63   - appendSize(out, 1, 1)
64   - appendLine(out, 'TEST PRINT')
65   - appendBold(out, false)
66   - appendSize(out, 0, 0)
67   - appendLine(out, 'GP-R3 / ESC-POS')
68   - appendLine(out, 'Food Label System')
69   - appendLine(out, '-----------------------------')
70   - appendLine(out, 'Connection OK')
71   - appendLine(out, 'Protocol OK')
72   - appendLine(out, '-----------------------------')
73   - })
  185 + return buildEscPosTemplateData(createTestPrintTemplate())
74 186 }
75 187  
76 188 export function buildEscPosLabelData (payload: LabelPrintPayload): number[] {
... ... @@ -98,3 +210,53 @@ export function buildEscPosLabelData (payload: LabelPrintPayload): number[] {
98 210 appendLine(out, '-----------------------------')
99 211 })
100 212 }
  213 +
  214 +export function buildEscPosImageData (
  215 + image: MonochromeImageData,
  216 + options: PrintImageOptions = {}
  217 +): number[] {
  218 + const printQty = Math.max(1, Math.round(options.printQty || 1))
  219 + return createEscDocument((out) => {
  220 + for (let i = 0; i < printQty; i++) {
  221 + appendAlign(out, 1)
  222 + appendRasterImage(out, image)
  223 + appendLine(out)
  224 + appendLine(out)
  225 + }
  226 + })
  227 +}
  228 +
  229 +export function buildEscPosTemplateData (
  230 + template: StructuredLabelTemplate,
  231 + data: LabelTemplateData = {}
  232 +): number[] {
  233 + const resolved = resolveEscTemplate(template, data)
  234 + const printQty = Math.max(1, Math.round(resolved.printQty || 1))
  235 + const feedLines = Math.max(1, Math.round(resolved.feedLines || 4))
  236 +
  237 + return createEscDocument((out) => {
  238 + for (let i = 0; i < printQty; i++) {
  239 + resolved.items.forEach((item) => {
  240 + if (item.type === 'rule') {
  241 + appendHorizontalRule(out, item.width || 32)
  242 + return
  243 + }
  244 + if (item.type === 'qrcode') {
  245 + appendQrCode(out, item)
  246 + return
  247 + }
  248 + if (item.type === 'barcode') {
  249 + appendBarcode(out, item)
  250 + return
  251 + }
  252 + appendAlign(out, item.align ?? 0)
  253 + appendBold(out, !!item.bold)
  254 + appendSize(out, item.widthScale || 0, item.heightScale || 0)
  255 + appendLine(out, item.text)
  256 + appendBold(out, false)
  257 + appendSize(out, 0, 0)
  258 + })
  259 + out.push(0x1b, 0x64, feedLines)
  260 + }
  261 + })
  262 +}
... ...
美国版/Food Labeling Management App UniApp/src/utils/print/protocols/tscProtocol.ts
1   -import type { LabelPrintPayload } from '../types/printer'
2   -import { buildTestTscLabel, buildTscLabel } from '../tscLabelBuilder'
  1 +import type {
  2 + LabelPrintPayload,
  3 + LabelTemplateData,
  4 + MonochromeImageData,
  5 + PrintImageOptions,
  6 + StructuredLabelTemplate,
  7 +} from '../types/printer'
  8 +import { resolveTscTemplate } from '../templateRenderer'
  9 +import { createTestPrintTemplate } from '../templates/testPrintTemplate'
  10 +import { buildTestTscLabel, buildTscImageLabel, buildTscLabel, buildTscTemplateLabel } from '../tscLabelBuilder'
3 11  
4 12 export function buildTscTestPrintData (): number[] {
5   - return buildTestTscLabel()
  13 + const template = createTestPrintTemplate(203, 1)
  14 + if (!template.tsc) {
  15 + throw new Error('Test template does not support TSC printers')
  16 + }
  17 + return buildTscTemplateLabel(template.tsc)
6 18 }
7 19  
8 20 export function buildTscLabelData (payload: LabelPrintPayload): number[] {
9 21 return buildTscLabel(payload)
10 22 }
  23 +
  24 +export function buildTscImageData (
  25 + image: MonochromeImageData,
  26 + options: PrintImageOptions = {},
  27 + dpi = 203
  28 +): number[] {
  29 + return buildTscImageLabel(image, options, dpi)
  30 +}
  31 +
  32 +export function buildTscTemplateData (
  33 + template: StructuredLabelTemplate,
  34 + data: LabelTemplateData = {}
  35 +): number[] {
  36 + return buildTscTemplateLabel(resolveTscTemplate(template, data))
  37 +}
... ...
美国版/Food Labeling Management App UniApp/src/utils/print/systemTemplateAdapter.ts 0 → 100644
  1 +import { applyTemplateData } from './templateRenderer'
  2 +import type {
  3 + EscTemplateItem,
  4 + LabelTemplateData,
  5 + PrinterTemplateUnit,
  6 + StructuredLabelTemplate,
  7 + TscTemplateItem,
  8 + StructuredTscTemplate,
  9 + StructuredEscTemplate,
  10 + SystemLabelTemplate,
  11 + SystemTemplateElementBase,
  12 + SystemTemplateTextAlign,
  13 +} from './types/printer'
  14 +
  15 +const DESIGN_DPI = 96
  16 +
  17 +function roundNumber (value: number, digits = 1): number {
  18 + const factor = Math.pow(10, digits)
  19 + return Math.round(value * factor) / factor
  20 +}
  21 +
  22 +function toMillimeter (value: number, unit: PrinterTemplateUnit = 'inch'): number {
  23 + if (unit === 'mm') return value
  24 + if (unit === 'cm') return value * 10
  25 + if (unit === 'px') return value / DESIGN_DPI * 25.4
  26 + return value * 25.4
  27 +}
  28 +
  29 +function templateWidthPx (template: SystemLabelTemplate): number {
  30 + return toMillimeter(template.width, template.unit || 'inch') / 25.4 * DESIGN_DPI
  31 +}
  32 +
  33 +function pxToDots (value: number, dpi: number): number {
  34 + return Math.max(0, Math.round((Number(value) || 0) * dpi / DESIGN_DPI))
  35 +}
  36 +
  37 +function clamp (value: number, min: number, max: number): number {
  38 + return Math.max(min, Math.min(max, Math.round(value)))
  39 +}
  40 +
  41 +function sortElements (elements: SystemTemplateElementBase[]): SystemTemplateElementBase[] {
  42 + return [...elements].sort((a, b) => {
  43 + if (a.y !== b.y) return a.y - b.y
  44 + return a.x - b.x
  45 + })
  46 +}
  47 +
  48 +function getConfigString (
  49 + config: Record<string, any>,
  50 + keys: string[],
  51 + fallback = ''
  52 +): string {
  53 + for (let i = 0; i < keys.length; i++) {
  54 + const value = config?.[keys[i]]
  55 + if (value != null && value !== '') return String(value)
  56 + }
  57 + return fallback
  58 +}
  59 +
  60 +function getConfigNumber (
  61 + config: Record<string, any>,
  62 + keys: string[],
  63 + fallback = 0
  64 +): number {
  65 + for (let i = 0; i < keys.length; i++) {
  66 + const value = Number(config?.[keys[i]])
  67 + if (!Number.isNaN(value) && Number.isFinite(value)) return value
  68 + }
  69 + return fallback
  70 +}
  71 +
  72 +function toCamelCaseKey (value: string): string {
  73 + return value
  74 + .toLowerCase()
  75 + .split(/[_\s-]+/)
  76 + .map((segment, index) => index === 0
  77 + ? segment
  78 + : segment.charAt(0).toUpperCase() + segment.slice(1))
  79 + .join('')
  80 +}
  81 +
  82 +function resolveBindingKey (element: SystemTemplateElementBase): string {
  83 + const config = element.config || {}
  84 + const explicit = getConfigString(config, ['dataKey', 'field', 'bindField', 'key', 'valueKey'])
  85 + if (explicit) return explicit
  86 +
  87 + const type = String(element.type || '').toUpperCase()
  88 + const map: Record<string, string> = {
  89 + TEXT_PRODUCT: 'productName',
  90 + TEXT_LABEL_ID: 'labelId',
  91 + TEXT_CATEGORY: 'category',
  92 + TEXT_PRICE: 'price',
  93 + TEXT_DATE: 'date',
  94 + TEXT_TIME: 'time',
  95 + QRCODE: 'qrCode',
  96 + BARCODE: 'barcode',
  97 + }
  98 + if (map[type]) return map[type]
  99 +
  100 + const pureType = type
  101 + .replace(/^TEXT_/, '')
  102 + .replace(/^FIELD_/, '')
  103 + .replace(/^VALUE_/, '')
  104 + return pureType ? toCamelCaseKey(pureType) : ''
  105 +}
  106 +
  107 +function resolveTemplateFieldValue (data: LabelTemplateData, key: string): string {
  108 + if (!key) return ''
  109 + const candidates = [key]
  110 + if (key === 'productName') candidates.push('product')
  111 + if (key === 'product') candidates.push('productName')
  112 + if (key === 'qrCode') candidates.push('labelId', 'barcode')
  113 + if (key === 'barcode') candidates.push('labelId', 'qrCode')
  114 +
  115 + for (let i = 0; i < candidates.length; i++) {
  116 + const value = data[candidates[i]]
  117 + if (value != null) return String(value)
  118 + }
  119 + return ''
  120 +}
  121 +
  122 +function resolveElementText (
  123 + element: SystemTemplateElementBase,
  124 + data: LabelTemplateData
  125 +): string {
  126 + const config = element.config || {}
  127 + const hasText = config.text != null && config.text !== ''
  128 + if (hasText && String(element.type || '').toUpperCase() === 'TEXT_STATIC') {
  129 + return applyTemplateData(String(config.text), data)
  130 + }
  131 + if (hasText && String(config.text).includes('{{')) {
  132 + return applyTemplateData(String(config.text), data)
  133 + }
  134 + const bindingKey = resolveBindingKey(element)
  135 + const boundValue = resolveTemplateFieldValue(data, bindingKey)
  136 + if (boundValue) return boundValue
  137 + if (hasText) return applyTemplateData(String(config.text), data)
  138 + return ''
  139 +}
  140 +
  141 +function resolveElementDataValue (
  142 + element: SystemTemplateElementBase,
  143 + data: LabelTemplateData
  144 +): string {
  145 + const config = element.config || {}
  146 + const raw = getConfigString(config, ['data', 'value'])
  147 + if (raw) return applyTemplateData(raw, data)
  148 + return resolveTemplateFieldValue(data, resolveBindingKey(element))
  149 +}
  150 +
  151 +function resolveElementAlign (
  152 + element: SystemTemplateElementBase,
  153 + pageWidthPx: number
  154 +): SystemTemplateTextAlign {
  155 + const config = element.config || {}
  156 + const align = String(config.textAlign || '').toLowerCase()
  157 + if (align === 'left' || align === 'center' || align === 'right') return align as SystemTemplateTextAlign
  158 + const centerX = (Number(element.x) || 0) + (Number(element.width) || 0) / 2
  159 + if (centerX <= pageWidthPx * 0.33) return 'left'
  160 + if (centerX >= pageWidthPx * 0.67) return 'right'
  161 + return 'center'
  162 +}
  163 +
  164 +function toEscAlign (align: SystemTemplateTextAlign): 0 | 1 | 2 {
  165 + if (align === 'center') return 1
  166 + if (align === 'right') return 2
  167 + return 0
  168 +}
  169 +
  170 +function resolveRotation (value?: string): number {
  171 + return value === 'vertical' ? 90 : 0
  172 +}
  173 +
  174 +function normalizeQrLevel (value?: string): 'L' | 'M' | 'Q' | 'H' {
  175 + const key = String(value || 'M').trim().toUpperCase()
  176 + if (key === 'L' || key === 'M' || key === 'Q' || key === 'H') return key
  177 + return 'M'
  178 +}
  179 +
  180 +function estimateTextWidthDots (text: string, fontDots: number): number {
  181 + let total = 0
  182 + for (let i = 0; i < text.length; i++) {
  183 + const code = text.charCodeAt(i)
  184 + total += code > 255 ? fontDots : fontDots * 0.6
  185 + }
  186 + return Math.round(total)
  187 +}
  188 +
  189 +function resolveTextScale (fontSizePx: number, dpi: number): number {
  190 + const targetDots = Math.max(12, Math.round(fontSizePx * dpi / DESIGN_DPI))
  191 + return clamp(targetDots / 24, 1, 7)
  192 +}
  193 +
  194 +function resolveTextX (params: {
  195 + align: SystemTemplateTextAlign
  196 + xPx: number
  197 + widthPx: number
  198 + dpi: number
  199 + text: string
  200 + scale: number
  201 +}): number {
  202 + const left = pxToDots(params.xPx, params.dpi)
  203 + if (params.align === 'left') return left
  204 +
  205 + const boxWidth = pxToDots(params.widthPx, params.dpi)
  206 + const fontDots = Math.max(24, params.scale * 24)
  207 + const textWidth = estimateTextWidthDots(params.text, fontDots)
  208 + if (params.align === 'center') {
  209 + return Math.max(0, left + Math.round(Math.max(0, boxWidth - textWidth) / 2))
  210 + }
  211 + return Math.max(0, left + Math.max(0, boxWidth - textWidth))
  212 +}
  213 +
  214 +function buildTscTemplate (
  215 + template: SystemLabelTemplate,
  216 + data: LabelTemplateData,
  217 + dpi: number,
  218 + printQty: number
  219 +): StructuredTscTemplate {
  220 + const widthMm = roundNumber(toMillimeter(template.width, template.unit || 'inch'))
  221 + const heightMm = roundNumber(toMillimeter(template.height, template.unit || 'inch'))
  222 + const items: TscTemplateItem[] = []
  223 +
  224 + sortElements(template.elements).forEach((element) => {
  225 + const config = element.config || {}
  226 + const type = String(element.type || '').toUpperCase()
  227 +
  228 + if (type.startsWith('TEXT_')) {
  229 + const text = resolveElementText(element, data)
  230 + if (!text) return
  231 + const scale = resolveTextScale(getConfigNumber(config, ['fontSize'], 14), dpi)
  232 + const align = resolveElementAlign(element, templateWidthPx(template))
  233 + items.push({
  234 + type: 'text',
  235 + x: resolveTextX({
  236 + align,
  237 + xPx: element.x,
  238 + widthPx: element.width,
  239 + dpi,
  240 + text,
  241 + scale,
  242 + }),
  243 + y: pxToDots(element.y, dpi),
  244 + text,
  245 + font: 'TSS24.BF2',
  246 + rotation: resolveRotation(element.rotation),
  247 + xScale: scale,
  248 + yScale: scale,
  249 + })
  250 + return
  251 + }
  252 +
  253 + if (type === 'QRCODE') {
  254 + const value = resolveElementDataValue(element, data)
  255 + if (!value) return
  256 + items.push({
  257 + type: 'qrcode',
  258 + x: pxToDots(element.x, dpi),
  259 + y: pxToDots(element.y, dpi),
  260 + value,
  261 + level: normalizeQrLevel(getConfigString(config, ['errorLevel'], 'M')),
  262 + cellWidth: clamp(Math.min(element.width, element.height) / 20, 2, 10),
  263 + mode: 'A',
  264 + })
  265 + return
  266 + }
  267 +
  268 + if (type === 'BARCODE') {
  269 + const value = resolveElementDataValue(element, data)
  270 + if (!value) return
  271 + items.push({
  272 + type: 'barcode',
  273 + x: pxToDots(element.x, dpi),
  274 + y: pxToDots(element.y, dpi),
  275 + value,
  276 + symbology: getConfigString(config, ['barcodeType'], 'CODE128'),
  277 + height: Math.max(20, pxToDots(element.height, dpi)),
  278 + readable: config.showText !== false,
  279 + rotation: resolveRotation(getConfigString(config, ['orientation'], element.rotation || 'horizontal')),
  280 + narrow: clamp(element.width / Math.max(40, value.length * 6), 1, 4),
  281 + wide: clamp(element.width / Math.max(24, value.length * 3), 2, 6),
  282 + })
  283 + }
  284 + })
  285 +
  286 + return {
  287 + widthMm,
  288 + heightMm,
  289 + gapMm: 0,
  290 + density: 14,
  291 + speed: 5,
  292 + printQty,
  293 + items,
  294 + }
  295 +}
  296 +
  297 +function buildEscTemplate (
  298 + template: SystemLabelTemplate,
  299 + data: LabelTemplateData,
  300 + printQty: number
  301 +): StructuredEscTemplate {
  302 + const pageWidth = templateWidthPx(template)
  303 + const items: EscTemplateItem[] = []
  304 +
  305 + sortElements(template.elements).forEach((element) => {
  306 + const config = element.config || {}
  307 + const type = String(element.type || '').toUpperCase()
  308 + const align = toEscAlign(resolveElementAlign(element, pageWidth))
  309 +
  310 + if (type.startsWith('TEXT_')) {
  311 + const text = resolveElementText(element, data)
  312 + if (!text) return
  313 + const fontSize = getConfigNumber(config, ['fontSize'], 14)
  314 + const scale = fontSize >= 28 ? 2 : 1
  315 + items.push({
  316 + type: 'text',
  317 + text,
  318 + align,
  319 + bold: String(config.fontWeight || '').toLowerCase() === 'bold',
  320 + widthScale: scale,
  321 + heightScale: scale,
  322 + })
  323 + return
  324 + }
  325 +
  326 + if (type === 'QRCODE') {
  327 + const value = resolveElementDataValue(element, data)
  328 + if (!value) return
  329 + items.push({
  330 + type: 'qrcode',
  331 + value,
  332 + align,
  333 + size: clamp(Math.min(element.width, element.height) / 24, 3, 10),
  334 + level: normalizeQrLevel(getConfigString(config, ['errorLevel'], 'M')),
  335 + })
  336 + return
  337 + }
  338 +
  339 + if (type === 'BARCODE') {
  340 + const value = resolveElementDataValue(element, data)
  341 + if (!value) return
  342 + items.push({
  343 + type: 'barcode',
  344 + value,
  345 + align,
  346 + symbology: getConfigString(config, ['barcodeType'], 'CODE128'),
  347 + height: clamp(element.height * 2, 48, 180),
  348 + width: clamp(element.width / Math.max(48, value.length * 4), 2, 6),
  349 + showText: config.showText !== false,
  350 + })
  351 + }
  352 + })
  353 +
  354 + return {
  355 + printQty,
  356 + feedLines: 3,
  357 + items,
  358 + }
  359 +}
  360 +
  361 +export function adaptSystemLabelTemplate (
  362 + template: SystemLabelTemplate,
  363 + data: LabelTemplateData = {},
  364 + options: {
  365 + dpi?: number
  366 + printQty?: number
  367 + } = {}
  368 +): StructuredLabelTemplate {
  369 + const dpi = options.dpi || 203
  370 + const printQty = Math.max(1, Math.round(options.printQty || 1))
  371 + return {
  372 + key: template.id || template.name || 'system-label-template',
  373 + tsc: buildTscTemplate(template, data, dpi, printQty),
  374 + esc: buildEscTemplate(template, data, printQty),
  375 + }
  376 +}
... ...
美国版/Food Labeling Management App UniApp/src/utils/print/templateRenderer.ts 0 → 100644
  1 +import type {
  2 + LabelTemplateData,
  3 + StructuredEscTemplate,
  4 + StructuredLabelTemplate,
  5 + StructuredTscTemplate,
  6 +} from './types/printer'
  7 +
  8 +function resolveTemplateValue (key: string, data: LabelTemplateData): string {
  9 + const value = data[key]
  10 + if (value == null) return ''
  11 + return String(value)
  12 +}
  13 +
  14 +export function applyTemplateData (content: string, data: LabelTemplateData = {}): string {
  15 + return String(content || '').replace(/\{\{\s*([\w.-]+)\s*\}\}/g, (_, key: string) => resolveTemplateValue(key, data))
  16 +}
  17 +
  18 +export function resolveTscTemplate (
  19 + template: StructuredLabelTemplate,
  20 + data: LabelTemplateData = {}
  21 +): StructuredTscTemplate {
  22 + if (!template.tsc) {
  23 + throw new Error(`Template "${template.key}" does not support TSC printers`)
  24 + }
  25 + return {
  26 + ...template.tsc,
  27 + items: template.tsc.items.map((item) => {
  28 + if (item.type === 'text') {
  29 + return { ...item, text: applyTemplateData(item.text, data) }
  30 + }
  31 + if (item.type === 'qrcode' || item.type === 'barcode') {
  32 + return { ...item, value: applyTemplateData(item.value, data) }
  33 + }
  34 + return { ...item }
  35 + }),
  36 + }
  37 +}
  38 +
  39 +export function resolveEscTemplate (
  40 + template: StructuredLabelTemplate,
  41 + data: LabelTemplateData = {}
  42 +): StructuredEscTemplate {
  43 + if (!template.esc) {
  44 + throw new Error(`Template "${template.key}" does not support ESC printers`)
  45 + }
  46 + return {
  47 + ...template.esc,
  48 + items: template.esc.items.map((item) => {
  49 + if (item.type === 'text') {
  50 + return { ...item, text: applyTemplateData(item.text, data) }
  51 + }
  52 + if (item.type === 'qrcode' || item.type === 'barcode') {
  53 + return { ...item, value: applyTemplateData(item.value, data) }
  54 + }
  55 + return { ...item }
  56 + }),
  57 + }
  58 +}
... ...
美国版/Food Labeling Management App UniApp/src/utils/print/templates/previewSystemTemplate.ts 0 → 100644
  1 +import type { SystemLabelTemplate } from '../types/printer'
  2 +
  3 +export const PREVIEW_SYSTEM_TEMPLATE: SystemLabelTemplate = {
  4 + id: 'template-preview-json-print',
  5 + name: 'Preview JSON Template',
  6 + labelType: 'PRICE',
  7 + unit: 'inch',
  8 + width: 4,
  9 + height: 2,
  10 + appliedLocation: 'ALL',
  11 + showRuler: true,
  12 + showGrid: true,
  13 + elements: [
  14 + {
  15 + id: 'el-title',
  16 + type: 'TEXT_STATIC',
  17 + x: 104,
  18 + y: 16,
  19 + width: 160,
  20 + height: 24,
  21 + rotation: 'horizontal',
  22 + border: 'none',
  23 + config: {
  24 + text: 'FOOD LABEL',
  25 + fontFamily: 'Arial',
  26 + fontSize: 14,
  27 + fontWeight: 'bold',
  28 + textAlign: 'center',
  29 + },
  30 + },
  31 + {
  32 + id: 'el-product-name',
  33 + type: 'TEXT_PRODUCT',
  34 + x: 96,
  35 + y: 128,
  36 + width: 120,
  37 + height: 24,
  38 + rotation: 'horizontal',
  39 + border: 'none',
  40 + config: {
  41 + text: 'Product',
  42 + fontFamily: 'Arial',
  43 + fontSize: 14,
  44 + fontWeight: 'normal',
  45 + textAlign: 'left',
  46 + },
  47 + },
  48 + {
  49 + id: 'el-category',
  50 + type: 'TEXT_STATIC',
  51 + x: 88,
  52 + y: 152,
  53 + width: 140,
  54 + height: 24,
  55 + rotation: 'horizontal',
  56 + border: 'none',
  57 + config: {
  58 + text: '{{category}}',
  59 + fontFamily: 'Arial',
  60 + fontSize: 14,
  61 + fontWeight: 'normal',
  62 + textAlign: 'left',
  63 + },
  64 + },
  65 + {
  66 + id: 'el-qrcode',
  67 + type: 'QRCODE',
  68 + x: 128,
  69 + y: 40,
  70 + width: 80,
  71 + height: 80,
  72 + rotation: 'horizontal',
  73 + border: 'none',
  74 + config: {
  75 + data: '{{qrCode}}',
  76 + errorLevel: 'M',
  77 + },
  78 + },
  79 + {
  80 + id: 'el-barcode',
  81 + type: 'BARCODE',
  82 + x: 208,
  83 + y: 128,
  84 + width: 160,
  85 + height: 48,
  86 + rotation: 'horizontal',
  87 + border: 'none',
  88 + config: {
  89 + barcodeType: 'CODE128',
  90 + data: '{{barcode}}',
  91 + showText: true,
  92 + orientation: 'horizontal',
  93 + },
  94 + },
  95 + ],
  96 +}
... ...
美国版/Food Labeling Management App UniApp/src/utils/print/templates/test.json 0 → 100644
  1 +{
  2 + "id": "template-1773998862063",
  3 + "name": "未命名模板",
  4 + "labelType": "PRICE",
  5 + "unit": "inch",
  6 + "width": 4,
  7 + "height": 2,
  8 + "appliedLocation": "ALL",
  9 + "showRuler": true,
  10 + "showGrid": true,
  11 + "elements": [
  12 + {
  13 + "id": "el-1773998886036-34sylni",
  14 + "type": "TEXT_STATIC",
  15 + "x": 104,
  16 + "y": 16,
  17 + "width": 120,
  18 + "height": 24,
  19 + "rotation": "horizontal",
  20 + "border": "none",
  21 + "config": {
  22 + "text": "文本",
  23 + "fontFamily": "Arial",
  24 + "fontSize": 14,
  25 + "fontWeight": "normal",
  26 + "textAlign": "center"
  27 + }
  28 + },
  29 + {
  30 + "id": "el-1773998909568-4jjwdx7",
  31 + "type": "TEXT_PRODUCT",
  32 + "x": 96,
  33 + "y": 128,
  34 + "width": 120,
  35 + "height": 24,
  36 + "rotation": "horizontal",
  37 + "border": "none",
  38 + "config": {
  39 + "text": "商品名",
  40 + "fontFamily": "Arial",
  41 + "fontSize": 14,
  42 + "fontWeight": "normal",
  43 + "textAlign": "left"
  44 + }
  45 + },
  46 + {
  47 + "id": "el-1773998913096-cgabpx1",
  48 + "type": "TEXT_STATIC",
  49 + "x": 88,
  50 + "y": 152,
  51 + "width": 120,
  52 + "height": 24,
  53 + "rotation": "horizontal",
  54 + "border": "none",
  55 + "config": {
  56 + "text": "文本",
  57 + "fontFamily": "Arial",
  58 + "fontSize": 14,
  59 + "fontWeight": "normal",
  60 + "textAlign": "left"
  61 + }
  62 + },
  63 + {
  64 + "id": "el-1773999052674-uzocw1j",
  65 + "type": "QRCODE",
  66 + "x": 128,
  67 + "y": 40,
  68 + "width": 80,
  69 + "height": 80,
  70 + "rotation": "horizontal",
  71 + "border": "none",
  72 + "config": {
  73 + "data": "12341千问请问抛弃我",
  74 + "errorLevel": "M"
  75 + }
  76 + },
  77 + {
  78 + "id": "el-1773999078958-5tgoru7",
  79 + "type": "BARCODE",
  80 + "x": 208,
  81 + "y": 128,
  82 + "width": 160,
  83 + "height": 48,
  84 + "rotation": "horizontal",
  85 + "border": "none",
  86 + "config": {
  87 + "barcodeType": "CODE128",
  88 + "data": "14124151231",
  89 + "showText": true,
  90 + "orientation": "horizontal"
  91 + }
  92 + }
  93 + ]
  94 +}
... ...
美国版/Food Labeling Management App UniApp/src/utils/print/templates/testPrintTemplate.ts 0 → 100644
  1 +import testTemplateJson from './test.json'
  2 +import { adaptSystemLabelTemplate } from '../systemTemplateAdapter'
  3 +import type { LabelTemplateData, StructuredLabelTemplate, SystemLabelTemplate } from '../types/printer'
  4 +
  5 +export const TEST_PRINT_SYSTEM_TEMPLATE = testTemplateJson as SystemLabelTemplate
  6 +
  7 +export const TEST_PRINT_TEMPLATE_DATA: LabelTemplateData = {
  8 + productName: '商品名',
  9 + product: '商品名',
  10 +}
  11 +
  12 +export function createTestPrintTemplate (
  13 + dpi = 203,
  14 + printQty = 1,
  15 + data: LabelTemplateData = TEST_PRINT_TEMPLATE_DATA
  16 +): StructuredLabelTemplate {
  17 + return adaptSystemLabelTemplate(TEST_PRINT_SYSTEM_TEMPLATE, data, {
  18 + dpi,
  19 + printQty,
  20 + })
  21 +}
... ...
美国版/Food Labeling Management App UniApp/src/utils/print/testPrintCanvas.ts 0 → 100644
  1 +import type { RawImageDataSource } from './types/printer'
  2 +
  3 +export function getTestPrintCanvasSize (maxWidthDots = 384): { width: number, height: number } {
  4 + const width = Math.max(256, Math.round(maxWidthDots || 384) - (Math.round(maxWidthDots || 384) % 8))
  5 + const height = Math.max(220, Math.round(width * 0.62))
  6 + return { width, height }
  7 +}
  8 +
  9 +function drawRoundedRect (
  10 + ctx: UniNamespace.CanvasContext,
  11 + x: number,
  12 + y: number,
  13 + width: number,
  14 + height: number,
  15 + radius: number
  16 +) {
  17 + const r = Math.max(0, Math.min(radius, Math.min(width, height) / 2))
  18 + ctx.beginPath()
  19 + ctx.moveTo(x + r, y)
  20 + ctx.lineTo(x + width - r, y)
  21 + ctx.arcTo(x + width, y, x + width, y + r, r)
  22 + ctx.lineTo(x + width, y + height - r)
  23 + ctx.arcTo(x + width, y + height, x + width - r, y + height, r)
  24 + ctx.lineTo(x + r, y + height)
  25 + ctx.arcTo(x, y + height, x, y + height - r, r)
  26 + ctx.lineTo(x, y + r)
  27 + ctx.arcTo(x, y, x + r, y, r)
  28 + ctx.closePath()
  29 +}
  30 +
  31 +export async function renderTestPrintCanvasImageData (
  32 + canvasId: string,
  33 + componentInstance: any,
  34 + size: { width: number, height: number }
  35 +): Promise<RawImageDataSource> {
  36 + const width = Math.max(8, size.width - (size.width % 8))
  37 + const height = Math.max(1, size.height)
  38 + const scale = width / 800
  39 + const ctx = uni.createCanvasContext(canvasId, componentInstance)
  40 +
  41 + ctx.setFillStyle('#ffffff')
  42 + ctx.fillRect(0, 0, width, height)
  43 +
  44 + drawRoundedRect(ctx, 10 * scale, 10 * scale, width - 20 * scale, height - 20 * scale, 18 * scale)
  45 + ctx.setLineWidth(Math.max(2, 3 * scale))
  46 + ctx.setStrokeStyle('#111111')
  47 + ctx.stroke()
  48 +
  49 + ctx.setFillStyle('#111111')
  50 + ctx.setFontSize(Math.max(22, Math.round(width * 0.045)))
  51 + ctx.fillText('IN USE FOOD LABEL', 26 * scale, 44 * scale)
  52 +
  53 + ctx.setLineWidth(Math.max(1, 2 * scale))
  54 + ctx.beginPath()
  55 + ctx.moveTo(24 * scale, 62 * scale)
  56 + ctx.lineTo(width - 24 * scale, 62 * scale)
  57 + ctx.stroke()
  58 +
  59 + ctx.setFontSize(Math.max(18, Math.round(width * 0.036)))
  60 + ctx.fillText('Product', 26 * scale, 96 * scale)
  61 + ctx.setFontSize(Math.max(20, Math.round(width * 0.04)))
  62 + ctx.fillText('Grilled Chicken Breast', 26 * scale, 128 * scale)
  63 +
  64 + ctx.setFontSize(Math.max(17, Math.round(width * 0.034)))
  65 + ctx.fillText('Prepared Protein / Hot Prep', 26 * scale, 162 * scale)
  66 + ctx.fillText('Prepared: 03/20/2026 10:30 AM', 26 * scale, 220 * scale)
  67 + ctx.fillText('Use By : 03/23/2026 10:30 AM', 26 * scale, 268 * scale)
  68 + ctx.fillText('Shelf Life: 72 Hours', 26 * scale, 316 * scale)
  69 +
  70 + const rightX = 540 * scale
  71 + ctx.setFontSize(Math.max(18, Math.round(width * 0.035)))
  72 + ctx.fillText('Label ID', rightX, 96 * scale)
  73 + ctx.setFontSize(Math.max(16, Math.round(width * 0.03)))
  74 + ctx.fillText('TEST-260320-001', rightX, 128 * scale)
  75 + ctx.fillText('Status: ACTIVE', rightX, 238 * scale)
  76 + ctx.fillText('Station: LINE 1', rightX, 286 * scale)
  77 + ctx.fillText('User: TEST USER', rightX, 334 * scale)
  78 +
  79 + const qrX = rightX
  80 + const qrY = 148 * scale
  81 + const qrSize = Math.round(120 * scale)
  82 + ctx.setLineWidth(Math.max(2, 3 * scale))
  83 + ctx.strokeRect(qrX, qrY, qrSize, qrSize)
  84 + ctx.strokeRect(qrX + 10 * scale, qrY + 10 * scale, 20 * scale, 20 * scale)
  85 + ctx.strokeRect(qrX + qrSize - 30 * scale, qrY + 10 * scale, 20 * scale, 20 * scale)
  86 + ctx.strokeRect(qrX + 10 * scale, qrY + qrSize - 30 * scale, 20 * scale, 20 * scale)
  87 + ctx.fillRect(qrX + 42 * scale, qrY + 18 * scale, 12 * scale, 12 * scale)
  88 + ctx.fillRect(qrX + 58 * scale, qrY + 34 * scale, 8 * scale, 8 * scale)
  89 + ctx.fillRect(qrX + 46 * scale, qrY + 54 * scale, 10 * scale, 10 * scale)
  90 + ctx.fillRect(qrX + 68 * scale, qrY + 68 * scale, 12 * scale, 12 * scale)
  91 + ctx.fillRect(qrX + 50 * scale, qrY + 84 * scale, 8 * scale, 8 * scale)
  92 +
  93 + await new Promise<void>((resolve) => {
  94 + ctx.draw(false, () => resolve())
  95 + })
  96 +
  97 + await new Promise<void>((resolve) => setTimeout(resolve, 30))
  98 +
  99 + return new Promise((resolve, reject) => {
  100 + uni.canvasGetImageData({
  101 + canvasId,
  102 + x: 0,
  103 + y: 0,
  104 + width,
  105 + height,
  106 + success: (res: any) => {
  107 + resolve({
  108 + width,
  109 + height,
  110 + data: res.data,
  111 + })
  112 + },
  113 + fail: (err: any) => {
  114 + reject(new Error(err?.errMsg || 'CANVAS_GET_IMAGE_DATA_FAILED'))
  115 + },
  116 + }, componentInstance)
  117 + })
  118 +}
... ...
美国版/Food Labeling Management App UniApp/src/utils/print/tscLabelBuilder.ts
... ... @@ -3,6 +3,7 @@
3 3 * 使用 UTF-8 编码。对于纯英文/ASCII 内容,UTF-8 与 GB18030 字节一致,佳博打印机可正常打印。
4 4 * 若需中文等非 ASCII,可考虑运行时动态加载 tsc.js(仅 APP 端)。
5 5 */
  6 +import type { MonochromeImageData, PrintImageOptions, StructuredTscTemplate } from './types/printer'
6 7  
7 8 /** 将字符串转为 UTF-8 字节数组(不依赖 TextEncoder) */
8 9 function stringToUtf8Bytes (str: string): number[] {
... ... @@ -35,10 +36,30 @@ function addCommandBytes (out: number[], str: string) {
35 36 for (let i = 0; i < bytes.length; i++) out.push(bytes[i])
36 37 }
37 38  
  39 +function addTscLine (out: number[], str: string) {
  40 + addCommandBytes(out, str + '\r\n')
  41 +}
  42 +
38 43 function escapeTscString (s: string): string {
39 44 return String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"')
40 45 }
41 46  
  47 +function normalizeTscBarcodeType (value?: string): string {
  48 + const key = String(value || 'CODE128').trim().toUpperCase()
  49 + const map: Record<string, string> = {
  50 + CODE128: '128',
  51 + CODE39: '39',
  52 + EAN13: 'EAN13',
  53 + EAN8: 'EAN8',
  54 + UPCA: 'UPCA',
  55 + UPCE: 'UPCE',
  56 + CODABAR: 'CODA',
  57 + ITF14: 'ITF14',
  58 + ITF: 'ITF',
  59 + }
  60 + return map[key] || '128'
  61 +}
  62 +
42 63 /**
43 64 * 构建标签打印指令字节数组(TSC)
44 65 * 与官方 Demo createLabel 格式一致(SIZE 100x30, GAP 0)
... ... @@ -93,38 +114,124 @@ export function buildTscLabel (options: {
93 114 */
94 115 export function buildTestTscLabel (): number[] {
95 116 const out: number[] = []
96   - const add = (s: string) => addCommandBytes(out, s + '\r\n')
97   - let y = 10
  117 + const add = (s: string) => addTscLine(out, s)
98 118  
99   - add('SIZE 100 mm,70 mm')
  119 + add('SIZE 100 mm,65 mm')
100 120 add('GAP 0 mm,0 mm')
101 121 add('CLS')
  122 + add('BOX 20,20,780,500,3')
  123 + add('BAR 20,90,760,3')
  124 + add('BAR 20,215,760,2')
  125 + add('BAR 20,335,760,2')
  126 + add('BAR 520,90,2,410')
  127 +
  128 + add('TEXT 180,42,"TSS24.BF2",0,1,1,"IN USE FOOD LABEL"')
  129 +
  130 + add('TEXT 45,115,"TSS24.BF2",0,1,1,"Product"')
  131 + add('TEXT 45,150,"TSS24.BF2",0,1,1,"Grilled Chicken Breast"')
  132 + add('TEXT 45,182,"TSS24.BF2",0,1,1,"Prepared Protein / Hot Prep"')
  133 +
  134 + add('TEXT 45,240,"TSS24.BF2",0,1,1,"Prepared"')
  135 + add('TEXT 45,275,"TSS24.BF2",0,1,1,"03/20/2026 10:30 AM"')
  136 + add('TEXT 45,360,"TSS24.BF2",0,1,1,"Use By"')
  137 + add('TEXT 45,395,"TSS24.BF2",0,1,1,"03/23/2026 10:30 AM"')
  138 + add('TEXT 45,430,"TSS24.BF2",0,1,1,"Shelf Life 72 Hours"')
  139 +
  140 + add('TEXT 545,118,"TSS24.BF2",0,1,1,"Label ID"')
  141 + add('TEXT 545,152,"TSS24.BF2",0,1,1,"TEST-260320-001"')
  142 + add('TEXT 545,330,"TSS24.BF2",0,1,1,"Status: ACTIVE"')
  143 + add('TEXT 545,365,"TSS24.BF2",0,1,1,"Station: LINE 1"')
  144 + add('TEXT 545,400,"TSS24.BF2",0,1,1,"User: TEST USER"')
  145 + add('QRCODE 555,190,L,4,A,0,"TEST-260320-001"')
102 146  
103   - add(`TEXT 50,${y},"TSS24.BF2",0,1,1,"NUTRITION FACTS"`)
104   - y += 28
105   - add(`TEXT 50,${y},"TSS24.BF2",0,1,1,"Grilled Chicken Breast"`)
106   - y += 28
107   - add(`TEXT 50,${y},"TSS24.BF2",0,1,1,"Serving Size 1 piece (100g)"`)
108   - y += 32
109   - add(`TEXT 50,${y},"TSS24.BF2",0,1,1,"Calories 165"`)
110   - y += 28
111   - add(`TEXT 50,${y},"TSS24.BF2",0,1,1,"Total Fat 4g"`)
112   - y += 24
113   - add(`TEXT 50,${y},"TSS24.BF2",0,1,1,"Saturated Fat 1g"`)
114   - y += 24
115   - add(`TEXT 50,${y},"TSS24.BF2",0,1,1,"Cholesterol 85mg"`)
116   - y += 24
117   - add(`TEXT 50,${y},"TSS24.BF2",0,1,1,"Sodium 75mg"`)
118   - y += 24
119   - add(`TEXT 50,${y},"TSS24.BF2",0,1,1,"Total Carb 0g"`)
120   - y += 24
121   - add(`TEXT 50,${y},"TSS24.BF2",0,1,1,"Protein 31g"`)
122   - y += 28
123   - add(`TEXT 50,${y},"TSS24.BF2",0,1,1,"Exp: 01/17/25 ID: TEST-251227"`)
124   - y += 35
125   - add(`QRCODE 50,${y},L,4,A,0,"TEST-251227-001"`)
126 147 add('PRINT 1,1')
127 148 add('FEED 1')
128 149  
129 150 return out
130 151 }
  152 +
  153 +export function buildTscTemplateLabel (template: StructuredTscTemplate): number[] {
  154 + const out: number[] = []
  155 + const add = (s: string) => addTscLine(out, s)
  156 +
  157 + add(`SIZE ${template.widthMm} mm,${template.heightMm} mm`)
  158 + add(`GAP ${template.gapMm || 0} mm,0 mm`)
  159 + if (template.density != null) add(`DENSITY ${template.density}`)
  160 + if (template.speed != null) add(`SPEED ${template.speed}`)
  161 + add('CLS')
  162 +
  163 + template.items.forEach((item) => {
  164 + if (item.type === 'box') {
  165 + const right = item.x + item.width
  166 + const bottom = item.y + item.height
  167 + add(`BOX ${item.x},${item.y},${right},${bottom},${item.lineWidth || 1}`)
  168 + return
  169 + }
  170 + if (item.type === 'bar') {
  171 + add(`BAR ${item.x},${item.y},${item.width},${item.height}`)
  172 + return
  173 + }
  174 + if (item.type === 'qrcode') {
  175 + add(`QRCODE ${item.x},${item.y},${item.level || 'L'},${item.cellWidth || 4},${item.mode || 'A'},0,"${escapeTscString(item.value)}"`)
  176 + return
  177 + }
  178 + if (item.type === 'barcode') {
  179 + add(`BARCODE ${item.x},${item.y},"${normalizeTscBarcodeType(item.symbology)}",${Math.max(20, Math.round(item.height || 80))},${item.readable === false ? 0 : 1},${item.rotation || 0},${Math.max(1, Math.round(item.narrow || 2))},${Math.max(2, Math.round(item.wide || 2))},"${escapeTscString(item.value)}"`)
  180 + return
  181 + }
  182 + add(`TEXT ${item.x},${item.y},"${item.font || 'TSS24.BF2'}",${item.rotation || 0},${item.xScale || 1},${item.yScale || 1},"${escapeTscString(item.text)}"`)
  183 + })
  184 +
  185 + add(`PRINT 1,${Math.max(1, Math.round(template.printQty || 1))}`)
  186 + return out
  187 +}
  188 +
  189 +function roundMm (value: number): string {
  190 + return (Math.round(value * 10) / 10).toFixed(1)
  191 +}
  192 +
  193 +function pixelsToTscBitmapBytes (image: MonochromeImageData): number[] {
  194 + const bytes: number[] = []
  195 + const bytesPerRow = image.width / 8
  196 + for (let y = 0; y < image.height; y++) {
  197 + for (let byteIndex = 0; byteIndex < bytesPerRow; byteIndex++) {
  198 + let value = 0
  199 + for (let bit = 0; bit < 8; bit++) {
  200 + const x = byteIndex * 8 + bit
  201 + const pixel = image.pixels[y * image.width + x]
  202 + const isWhite = pixel ? 0 : 1
  203 + value |= isWhite << (7 - bit)
  204 + }
  205 + bytes.push(value & 0xff)
  206 + }
  207 + }
  208 + return bytes
  209 +}
  210 +
  211 +export function buildTscImageLabel (
  212 + image: MonochromeImageData,
  213 + options: PrintImageOptions = {},
  214 + dpi = 203
  215 +): number[] {
  216 + const out: number[] = []
  217 + const add = (s: string) => addCommandBytes(out, s + '\r\n')
  218 + const widthMm = options.widthMm || (image.width * 25.4 / dpi)
  219 + const heightMm = options.heightMm || (image.height * 25.4 / dpi)
  220 + const x = Math.max(0, Math.round(options.x || 0))
  221 + const y = Math.max(0, Math.round(options.y || 0))
  222 + const printQty = Math.max(1, Math.round(options.printQty || 1))
  223 + const bytesPerRow = image.width / 8
  224 + const bitmapBytes = pixelsToTscBitmapBytes(image)
  225 +
  226 + add(`SIZE ${roundMm(widthMm)} mm,${roundMm(heightMm)} mm`)
  227 + add('GAP 0 mm,0 mm')
  228 + add('DENSITY 14')
  229 + add('SPEED 5')
  230 + add('CLS')
  231 + add(`BITMAP ${x},${y},${bytesPerRow},${image.height},0,`)
  232 + for (let i = 0; i < bitmapBytes.length; i++) out.push(bitmapBytes[i])
  233 + out.push(0x0d, 0x0a)
  234 + add(`PRINT 1,${printQty}`)
  235 +
  236 + return out
  237 +}
... ...
美国版/Food Labeling Management App UniApp/src/utils/print/types/printer.ts
... ... @@ -20,6 +20,179 @@ export interface LabelPrintPayload {
20 20 extraLine?: string
21 21 }
22 22  
  23 +export interface PrintImageOptions {
  24 + printQty?: number
  25 + threshold?: number
  26 + maxWidthDots?: number
  27 + targetWidthDots?: number
  28 + targetHeightDots?: number
  29 + widthMm?: number
  30 + heightMm?: number
  31 + x?: number
  32 + y?: number
  33 +}
  34 +
  35 +export interface MonochromeImageData {
  36 + width: number
  37 + height: number
  38 + pixels: number[] // 1 = black, 0 = white
  39 +}
  40 +
  41 +export interface RawImageDataSource {
  42 + width: number
  43 + height: number
  44 + data: ArrayLike<number>
  45 +}
  46 +
  47 +export type LabelTemplateValue = string | number
  48 +export type LabelTemplateData = Record<string, LabelTemplateValue>
  49 +export type PrinterTemplateUnit = 'inch' | 'mm' | 'cm' | 'px'
  50 +export type SystemTemplateRotation = 'horizontal' | 'vertical'
  51 +export type SystemTemplateTextAlign = 'left' | 'center' | 'right'
  52 +
  53 +export interface SystemTemplateElementBase {
  54 + id: string
  55 + type: string
  56 + x: number
  57 + y: number
  58 + width: number
  59 + height: number
  60 + rotation?: SystemTemplateRotation
  61 + border?: string
  62 + config: Record<string, any>
  63 +}
  64 +
  65 +export interface SystemLabelTemplate {
  66 + id: string
  67 + name: string
  68 + labelType?: string
  69 + unit?: PrinterTemplateUnit
  70 + width: number
  71 + height: number
  72 + appliedLocation?: string
  73 + showRuler?: boolean
  74 + showGrid?: boolean
  75 + elements: SystemTemplateElementBase[]
  76 +}
  77 +
  78 +export interface TscTemplateTextItem {
  79 + type: 'text'
  80 + x: number
  81 + y: number
  82 + text: string
  83 + font?: string
  84 + rotation?: number
  85 + xScale?: number
  86 + yScale?: number
  87 +}
  88 +
  89 +export interface TscTemplateBoxItem {
  90 + type: 'box'
  91 + x: number
  92 + y: number
  93 + width: number
  94 + height: number
  95 + lineWidth?: number
  96 +}
  97 +
  98 +export interface TscTemplateBarItem {
  99 + type: 'bar'
  100 + x: number
  101 + y: number
  102 + width: number
  103 + height: number
  104 +}
  105 +
  106 +export interface TscTemplateQrCodeItem {
  107 + type: 'qrcode'
  108 + x: number
  109 + y: number
  110 + value: string
  111 + level?: 'L' | 'M' | 'Q' | 'H'
  112 + cellWidth?: number
  113 + mode?: 'A' | 'M'
  114 +}
  115 +
  116 +export interface TscTemplateBarcodeItem {
  117 + type: 'barcode'
  118 + x: number
  119 + y: number
  120 + value: string
  121 + symbology?: string
  122 + height?: number
  123 + readable?: boolean
  124 + rotation?: number
  125 + narrow?: number
  126 + wide?: number
  127 +}
  128 +
  129 +export type TscTemplateItem =
  130 + | TscTemplateTextItem
  131 + | TscTemplateBoxItem
  132 + | TscTemplateBarItem
  133 + | TscTemplateQrCodeItem
  134 + | TscTemplateBarcodeItem
  135 +
  136 +export interface StructuredTscTemplate {
  137 + widthMm: number
  138 + heightMm: number
  139 + gapMm?: number
  140 + density?: number
  141 + speed?: number
  142 + printQty?: number
  143 + items: TscTemplateItem[]
  144 +}
  145 +
  146 +export interface EscTemplateTextItem {
  147 + type: 'text'
  148 + text: string
  149 + align?: 0 | 1 | 2
  150 + bold?: boolean
  151 + widthScale?: number
  152 + heightScale?: number
  153 +}
  154 +
  155 +export interface EscTemplateRuleItem {
  156 + type: 'rule'
  157 + width?: number
  158 +}
  159 +
  160 +export interface EscTemplateQrCodeItem {
  161 + type: 'qrcode'
  162 + value: string
  163 + align?: 0 | 1 | 2
  164 + size?: number
  165 + level?: 'L' | 'M' | 'Q' | 'H'
  166 +}
  167 +
  168 +export interface EscTemplateBarcodeItem {
  169 + type: 'barcode'
  170 + value: string
  171 + align?: 0 | 1 | 2
  172 + symbology?: string
  173 + height?: number
  174 + width?: number
  175 + showText?: boolean
  176 +}
  177 +
  178 +export type EscTemplateItem =
  179 + | EscTemplateTextItem
  180 + | EscTemplateRuleItem
  181 + | EscTemplateQrCodeItem
  182 + | EscTemplateBarcodeItem
  183 +
  184 +export interface StructuredEscTemplate {
  185 + printQty?: number
  186 + feedLines?: number
  187 + items: EscTemplateItem[]
  188 +}
  189 +
  190 +export interface StructuredLabelTemplate {
  191 + key: string
  192 + tsc?: StructuredTscTemplate
  193 + esc?: StructuredEscTemplate
  194 +}
  195 +
23 196 export interface PrinterDriver {
24 197 key: string
25 198 brand: string
... ... @@ -28,6 +201,8 @@ export interface PrinterDriver {
28 201 protocol: PrinterProtocol
29 202 preferredConnection?: ActiveBtDeviceType
30 203 preferredBleMtu?: number
  204 + imageMaxWidthDots?: number
  205 + imageDpi?: number
31 206 keywords: string[]
32 207 matches: (device: PrinterCandidate) => number
33 208 resolveConnectionType: (device: PrinterCandidate) => ActiveBtDeviceType
... ...