Commit acf08c9c6a15436f1d06addc5e4012cc1b9bd3be

Authored by 杨鑫
2 parents abb6bea5 dbc9750f

Merge branch 'main' of http://39.98.150.180/wangming/Food-Labeling-Management-Platform

Showing 30 changed files with 2512 additions and 483 deletions
label-template-template-1773988794039 (1).json 0 → 100644
  1 +{
  2 + "id": "template-1773988794039",
  3 + "name": "未命名模板",
  4 + "labelType": "PRICE",
  5 + "unit": "inch",
  6 + "width": 4,
  7 + "height": 6,
  8 + "appliedLocation": "ALL",
  9 + "showRuler": true,
  10 + "showGrid": true,
  11 + "elements": [
  12 + {
  13 + "id": "el-1773989351080-vqc03nr",
  14 + "type": "IMAGE",
  15 + "x": 32,
  16 + "y": 24,
  17 + "width": 60,
  18 + "height": 60,
  19 + "rotation": "horizontal",
  20 + "border": "none",
  21 + "config": {
  22 + "src": "",
  23 + "scaleMode": "contain"
  24 + }
  25 + },
  26 + {
  27 + "id": "el-1773989452538-0ejrxoe",
  28 + "type": "TEXT_STATIC",
  29 + "x": 32,
  30 + "y": 104,
  31 + "width": 120,
  32 + "height": 24,
  33 + "rotation": "horizontal",
  34 + "border": "none",
  35 + "config": {
  36 + "text": "文本",
  37 + "fontFamily": "Arial",
  38 + "fontSize": 14,
  39 + "fontWeight": "normal",
  40 + "textAlign": "left"
  41 + }
  42 + },
  43 + {
  44 + "id": "el-1773989466493-ibbroio",
  45 + "type": "QRCODE",
  46 + "x": 32,
  47 + "y": 136,
  48 + "width": 80,
  49 + "height": 80,
  50 + "rotation": "horizontal",
  51 + "border": "none",
  52 + "config": {
  53 + "data": "https://example.com",
  54 + "errorLevel": "M"
  55 + }
  56 + },
  57 + {
  58 + "id": "el-1773989469008-f1l39qj",
  59 + "type": "BARCODE",
  60 + "x": 0,
  61 + "y": 224,
  62 + "width": 160,
  63 + "height": 48,
  64 + "rotation": "horizontal",
  65 + "border": "none",
  66 + "config": {
  67 + "barcodeType": "CODE128",
  68 + "data": "123456789",
  69 + "showText": true,
  70 + "orientation": "horizontal"
  71 + }
  72 + },
  73 + {
  74 + "id": "el-1773989473436-j7fdeh2",
  75 + "type": "BLANK",
  76 + "x": 32,
  77 + "y": 288,
  78 + "width": 48,
  79 + "height": 32,
  80 + "rotation": "horizontal",
  81 + "border": "none",
  82 + "config": {}
  83 + },
  84 + {
  85 + "id": "el-1773989483341-ifwcyjj",
  86 + "type": "TEXT_PRICE",
  87 + "x": 152,
  88 + "y": 24,
  89 + "width": 80,
  90 + "height": 24,
  91 + "rotation": "horizontal",
  92 + "border": "none",
  93 + "config": {
  94 + "text": "0.00",
  95 + "prefix": "¥",
  96 + "decimal": 2,
  97 + "fontFamily": "Arial",
  98 + "fontSize": 14,
  99 + "fontWeight": "bold",
  100 + "textAlign": "right"
  101 + }
  102 + },
  103 + {
  104 + "id": "el-1773989498031-e4d61j8",
  105 + "type": "IMAGE",
  106 + "x": 192,
  107 + "y": 56,
  108 + "width": 60,
  109 + "height": 60,
  110 + "rotation": "horizontal",
  111 + "border": "none",
  112 + "config": {
  113 + "src": "",
  114 + "scaleMode": "contain"
  115 + }
  116 + },
  117 + {
  118 + "id": "el-1773989505076-1lxccx7",
  119 + "type": "TEXT_PRODUCT",
  120 + "x": 200,
  121 + "y": 136,
  122 + "width": 120,
  123 + "height": 24,
  124 + "rotation": "horizontal",
  125 + "border": "none",
  126 + "config": {
  127 + "text": "商品名",
  128 + "fontFamily": "Arial",
  129 + "fontSize": 14,
  130 + "fontWeight": "normal",
  131 + "textAlign": "left"
  132 + }
  133 + },
  134 + {
  135 + "id": "el-1773989509805-ax3392v",
  136 + "type": "TEXT_STATIC",
  137 + "x": 192,
  138 + "y": 160,
  139 + "width": 120,
  140 + "height": 24,
  141 + "rotation": "horizontal",
  142 + "border": "none",
  143 + "config": {
  144 + "text": "文本",
  145 + "fontFamily": "Arial",
  146 + "fontSize": 14,
  147 + "fontWeight": "normal",
  148 + "textAlign": "left"
  149 + }
  150 + },
  151 + {
  152 + "id": "el-1773989512993-xt8bg7q",
  153 + "type": "QRCODE",
  154 + "x": 184,
  155 + "y": 184,
  156 + "width": 80,
  157 + "height": 80,
  158 + "rotation": "horizontal",
  159 + "border": "none",
  160 + "config": {
  161 + "data": "https://example.com",
  162 + "errorLevel": "M"
  163 + }
  164 + },
  165 + {
  166 + "id": "el-1773989525383-eji8p2s",
  167 + "type": "BARCODE",
  168 + "x": 0,
  169 + "y": 288,
  170 + "width": 160,
  171 + "height": 48,
  172 + "rotation": "horizontal",
  173 + "border": "none",
  174 + "config": {
  175 + "barcodeType": "CODE128",
  176 + "data": "123456789",
  177 + "showText": true,
  178 + "orientation": "horizontal"
  179 + }
  180 + },
  181 + {
  182 + "id": "el-1773989540159-dr2avdf",
  183 + "type": "NUTRITION",
  184 + "x": 184,
  185 + "y": 280,
  186 + "width": 200,
  187 + "height": 120,
  188 + "rotation": "horizontal",
  189 + "border": "none",
  190 + "config": {
  191 + "calories": 120,
  192 + "fat": "5g",
  193 + "protein": "3g",
  194 + "carbs": "10g",
  195 + "layout": "standard"
  196 + }
  197 + },
  198 + {
  199 + "id": "el-1773989549679-mcxrdnw",
  200 + "type": "TEXT_PRICE",
  201 + "x": 24,
  202 + "y": 352,
  203 + "width": 80,
  204 + "height": 24,
  205 + "rotation": "horizontal",
  206 + "border": "none",
  207 + "config": {
  208 + "text": "0.00",
  209 + "prefix": "¥",
  210 + "decimal": 2,
  211 + "fontFamily": "Arial",
  212 + "fontSize": 14,
  213 + "fontWeight": "bold",
  214 + "textAlign": "right"
  215 + }
  216 + }
  217 + ]
  218 +}
0 \ No newline at end of file 219 \ No newline at end of file
美国版/Food Labeling Management App UniApp/src/locales/en.ts
@@ -53,6 +53,11 @@ export default { @@ -53,6 +53,11 @@ export default {
53 connectFail: 'Connection failed', 53 connectFail: 'Connection failed',
54 bleNotAvailable: 'Bluetooth not available', 54 bleNotAvailable: 'Bluetooth not available',
55 noDevices: 'No devices found', 55 noDevices: 'No devices found',
  56 + pairedDevices: 'Paired Devices',
  57 + noPairedDevices: 'No paired Bluetooth devices',
  58 + classic: 'Classic',
  59 + ble: 'BLE',
  60 + dual: 'Dual',
56 printer1: { name: 'Kitchen Printer #1', location: 'Main Kitchen' }, 61 printer1: { name: 'Kitchen Printer #1', location: 'Main Kitchen' },
57 printer2: { name: 'Kitchen Printer #2', location: 'Main Kitchen' }, 62 printer2: { name: 'Kitchen Printer #2', location: 'Main Kitchen' },
58 printer3: { name: 'Prep Area Printer', location: 'Prep Station' }, 63 printer3: { name: 'Prep Area Printer', location: 'Prep Station' },
美国版/Food Labeling Management App UniApp/src/locales/zh.ts
@@ -53,6 +53,11 @@ export default { @@ -53,6 +53,11 @@ export default {
53 connectFail: '连接失败', 53 connectFail: '连接失败',
54 bleNotAvailable: '蓝牙未开启或不可用', 54 bleNotAvailable: '蓝牙未开启或不可用',
55 noDevices: '未发现设备', 55 noDevices: '未发现设备',
  56 + pairedDevices: '已配对设备',
  57 + noPairedDevices: '没有已配对的蓝牙设备',
  58 + classic: '经典',
  59 + ble: '低功耗',
  60 + dual: '双模',
56 printer1: { name: '厨房打印机 #1', location: '主厨房' }, 61 printer1: { name: '厨房打印机 #1', location: '主厨房' },
57 printer2: { name: '厨房打印机 #2', location: '主厨房' }, 62 printer2: { name: '厨房打印机 #2', location: '主厨房' },
58 printer3: { name: '准备区打印机', location: '准备站' }, 63 printer3: { name: '准备区打印机', location: '准备站' },
美国版/Food Labeling Management App UniApp/src/manifest.json
@@ -2,8 +2,8 @@ @@ -2,8 +2,8 @@
2 "name" : "food.labeling", 2 "name" : "food.labeling",
3 "appid" : "__UNI__1BFD76D", 3 "appid" : "__UNI__1BFD76D",
4 "description" : "", 4 "description" : "",
5 - "versionName" : "1.0.1",  
6 - "versionCode" : 101, 5 + "versionName" : "1.0.3",
  6 + "versionCode" : 103,
7 "transformPx" : false, 7 "transformPx" : false,
8 /* 5+App特有相关 */ 8 /* 5+App特有相关 */
9 "app-plus" : { 9 "app-plus" : {
@@ -18,8 +18,9 @@ @@ -18,8 +18,9 @@
18 }, 18 },
19 /* 模块配置 */ 19 /* 模块配置 */
20 "modules" : { 20 "modules" : {
21 - "Bluetooth" : {},  
22 - "Camera" : {} 21 + "Camera" : {},
  22 + "Barcode" : {},
  23 + "Bluetooth" : {}
23 }, 24 },
24 /* 应用发布信息 */ 25 /* 应用发布信息 */
25 "distribute" : { 26 "distribute" : {
@@ -43,6 +44,10 @@ @@ -43,6 +44,10 @@
43 "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>", 44 "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>",
44 "<uses-permission android:name=\"android.permission.BLUETOOTH\"/>", 45 "<uses-permission android:name=\"android.permission.BLUETOOTH\"/>",
45 "<uses-permission android:name=\"android.permission.BLUETOOTH_ADMIN\"/>", 46 "<uses-permission android:name=\"android.permission.BLUETOOTH_ADMIN\"/>",
  47 + "<uses-permission android:name=\"android.permission.BLUETOOTH_SCAN\"/>",
  48 + "<uses-permission android:name=\"android.permission.BLUETOOTH_CONNECT\"/>",
  49 + "<uses-permission android:name=\"android.permission.BLUETOOTH_ADVERTISE\"/>",
  50 + "<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>",
46 "<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>" 51 "<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>"
47 ] 52 ]
48 }, 53 },
美国版/Food Labeling Management App UniApp/src/pages/labels/bluetooth.vue
@@ -42,6 +42,20 @@ @@ -42,6 +42,20 @@
42 <text class="error-text">{{ errorMsg }}</text> 42 <text class="error-text">{{ errorMsg }}</text>
43 </view> 43 </view>
44 44
  45 + <view class="debug-card">
  46 + <text class="debug-title">Debug Status</text>
  47 + <text class="debug-item">Current Mode: {{ debugInfo.currentMode }}</text>
  48 + <text class="debug-item">Classic Module: {{ debugInfo.classicModuleReady ? 'Ready' : 'Not Ready' }}</text>
  49 + <text class="debug-item">Paired Count: {{ debugInfo.pairedCount }}</text>
  50 + <text class="debug-item">Virtual BT Printer: {{ debugInfo.foundVirtualPrinter ? 'Found' : 'Not Found' }}</text>
  51 + <text class="debug-item">Classic Scan: {{ debugInfo.lastClassicEvent }}</text>
  52 + <text class="debug-item">BLE Scan: {{ debugInfo.lastBleEvent }}</text>
  53 + <text v-if="debugInfo.lastBleError" class="debug-item debug-error">{{ debugInfo.lastBleError }}</text>
  54 + <text v-if="debugInfo.locationServiceRequired" class="debug-item debug-warn">
  55 + Android system Location service is OFF. Turn it on before BLE scan.
  56 + </text>
  57 + </view>
  58 +
45 <!-- Connected --> 59 <!-- Connected -->
46 <view v-if="connectedDevice" class="connected-card"> 60 <view v-if="connectedDevice" class="connected-card">
47 <view class="connected-header"> 61 <view class="connected-header">
@@ -153,26 +167,21 @@ import AppIcon from &#39;../../components/AppIcon.vue&#39; @@ -153,26 +167,21 @@ import AppIcon from &#39;../../components/AppIcon.vue&#39;
153 import SideMenu from '../../components/SideMenu.vue' 167 import SideMenu from '../../components/SideMenu.vue'
154 import LocationPicker from '../../components/LocationPicker.vue' 168 import LocationPicker from '../../components/LocationPicker.vue'
155 import { getStatusBarHeight } from '../../utils/statusBar' 169 import { getStatusBarHeight } from '../../utils/statusBar'
  170 +import classicBluetooth from '../../utils/print/bluetoothTool.js'
156 import { 171 import {
157 getPrinterType, 172 getPrinterType,
158 - getBluetoothConnection,  
159 - setBluetoothConnection,  
160 - setBuiltinPrinter,  
161 clearPrinter, 173 clearPrinter,
162 - sendToPrinter,  
163 type PrinterType, 174 type PrinterType,
164 } from '../../utils/print/printerConnection' 175 } from '../../utils/print/printerConnection'
165 -import { buildTestTscLabel } from '../../utils/print/tscLabelBuilder'  
166 -  
167 -function getClassicBluetooth () {  
168 - try {  
169 - // @ts-ignore - dynamic require for native plugin  
170 - const m = require('../../utils/print/bluetoothTool.js')  
171 - return m?.default ?? null  
172 - } catch {  
173 - return null  
174 - }  
175 -} 176 +import { ensureBluetoothPermissions } from '../../utils/print/bluetoothPermissions'
  177 +import {
  178 + connectBluetoothPrinter,
  179 + describeDiscoveredPrinter,
  180 + disconnectCurrentPrinter,
  181 + getCurrentPrinterSummary,
  182 + testPrintCurrentPrinter,
  183 + useBuiltinPrinter,
  184 +} from '../../utils/print/manager/printerManager'
176 185
177 const statusBarHeight = getStatusBarHeight() 186 const statusBarHeight = getStatusBarHeight()
178 const isMenuOpen = ref(false) 187 const isMenuOpen = ref(false)
@@ -181,6 +190,17 @@ const connectingId = ref(&#39;&#39;) @@ -181,6 +190,17 @@ const connectingId = ref(&#39;&#39;)
181 const errorMsg = ref('') 190 const errorMsg = ref('')
182 const btAdapterReady = ref(false) 191 const btAdapterReady = ref(false)
183 const printerType = ref<PrinterType | ''>(getPrinterType() || 'bluetooth') 192 const printerType = ref<PrinterType | ''>(getPrinterType() || 'bluetooth')
  193 +const currentPrinter = ref(getCurrentPrinterSummary())
  194 +const debugInfo = ref({
  195 + currentMode: 'none',
  196 + classicModuleReady: false,
  197 + pairedCount: 0,
  198 + foundVirtualPrinter: false,
  199 + lastClassicEvent: 'idle',
  200 + lastBleEvent: 'idle',
  201 + lastBleError: '',
  202 + locationServiceRequired: false,
  203 +})
184 204
185 interface BtDevice { 205 interface BtDevice {
186 deviceId: string 206 deviceId: string
@@ -192,14 +212,26 @@ interface BtDevice { @@ -192,14 +212,26 @@ interface BtDevice {
192 const devices = ref<BtDevice[]>([]) 212 const devices = ref<BtDevice[]>([])
193 const discoveredIds = new Set<string>() 213 const discoveredIds = new Set<string>()
194 214
  215 +function refreshCurrentPrinter () {
  216 + currentPrinter.value = getCurrentPrinterSummary()
  217 + debugInfo.value.currentMode = currentPrinter.value.type || 'none'
  218 +}
  219 +
  220 +function hasPreferredClassicDeviceInList () {
  221 + return devices.value.some((item: any) => {
  222 + const name = String(item?.name || '').toLowerCase()
  223 + const type = String(item?.type || '').toLowerCase()
  224 + return name.includes('virtual bt printer') || type === 'classic' || type === 'dual'
  225 + })
  226 +}
  227 +
195 const connectedDevice = computed(() => { 228 const connectedDevice = computed(() => {
196 - const type = getPrinterType()  
197 - if (printerType.value === 'builtin' && type === 'builtin') {  
198 - return { name: 'Built-in Printer', deviceId: 'builtin' } 229 + const summary = currentPrinter.value
  230 + if (printerType.value === 'builtin' && summary.type === 'builtin') {
  231 + return { name: summary.displayName, deviceId: summary.deviceId }
199 } 232 }
200 - if (printerType.value === 'bluetooth' && type === 'bluetooth') {  
201 - const conn = getBluetoothConnection()  
202 - if (conn) return { name: conn.deviceName, deviceId: conn.deviceId } 233 + if (printerType.value === 'bluetooth' && summary.type === 'bluetooth') {
  234 + return { name: summary.displayName, deviceId: summary.deviceId }
203 } 235 }
204 return null 236 return null
205 }) 237 })
@@ -208,6 +240,7 @@ function switchType (type: &#39;bluetooth&#39; | &#39;builtin&#39;) { @@ -208,6 +240,7 @@ function switchType (type: &#39;bluetooth&#39; | &#39;builtin&#39;) {
208 printerType.value = type 240 printerType.value = type
209 if (type === 'bluetooth' && getPrinterType() === 'builtin') { 241 if (type === 'bluetooth' && getPrinterType() === 'builtin') {
210 clearPrinter() 242 clearPrinter()
  243 + refreshCurrentPrinter()
211 } 244 }
212 } 245 }
213 246
@@ -234,37 +267,24 @@ const initBluetooth = (): Promise&lt;void&gt; =&gt; { @@ -234,37 +267,24 @@ const initBluetooth = (): Promise&lt;void&gt; =&gt; {
234 }) 267 })
235 } 268 }
236 269
237 -const startDiscovery = () => { 270 +const startBleScan = () => {
238 uni.startBluetoothDevicesDiscovery({ 271 uni.startBluetoothDevicesDiscovery({
239 allowDuplicatesKey: false, 272 allowDuplicatesKey: false,
240 success: () => { 273 success: () => {
241 isScanning.value = true 274 isScanning.value = true
242 errorMsg.value = '' 275 errorMsg.value = ''
  276 + debugInfo.value.lastBleEvent = 'scan running'
243 }, 277 },
244 fail: (err: any) => { 278 fail: (err: any) => {
245 - isScanning.value = false  
246 - errorMsg.value = 'Scan failed: ' + (err.errMsg || 'Unknown error') 279 + console.error('BLE startBluetoothDevicesDiscovery fail:', err)
  280 + debugInfo.value.lastBleEvent = 'scan failed'
  281 + debugInfo.value.lastBleError = err?.errMsg || 'BLE scan failed'
  282 + if (err?.errCode === 10016 || err?.code === 10016) {
  283 + debugInfo.value.locationServiceRequired = true
  284 + errorMsg.value = 'Bluetooth scan failed: Android system Location service is turned off.'
  285 + }
247 }, 286 },
248 }) 287 })
249 - // 同时启动经典蓝牙扫描(发现 d320fax_295c 等未配对设备,不过滤任何设备)  
250 - // #ifdef APP-PLUS  
251 - const classic = getClassicBluetooth()  
252 - if (classic && classic.startClassicDiscovery) {  
253 - classic.startClassicDiscovery(  
254 - (dev: { name: string; deviceId: string; type: string }) => {  
255 - if (discoveredIds.has(dev.deviceId)) return  
256 - discoveredIds.add(dev.deviceId)  
257 - devices.value.push({  
258 - deviceId: dev.deviceId,  
259 - name: dev.name || 'Unknown',  
260 - type: (dev.type as BtDevice['type']) || 'classic',  
261 - })  
262 - devices.value.sort((a, b) => (b.RSSI || -100) - (a.RSSI || -100))  
263 - },  
264 - () => { /* 经典蓝牙扫描完成 */ },  
265 - )  
266 - }  
267 - // #endif  
268 } 288 }
269 289
270 const stopDiscovery = () => { 290 const stopDiscovery = () => {
@@ -274,7 +294,7 @@ const stopDiscovery = () =&gt; { @@ -274,7 +294,7 @@ const stopDiscovery = () =&gt; {
274 }, 294 },
275 }) 295 })
276 // #ifdef APP-PLUS 296 // #ifdef APP-PLUS
277 - const classic = getClassicBluetooth() 297 + const classic = classicBluetooth
278 if (classic && classic.cancelClassicDiscovery) classic.cancelClassicDiscovery() 298 if (classic && classic.cancelClassicDiscovery) classic.cancelClassicDiscovery()
279 // #endif 299 // #endif
280 } 300 }
@@ -286,12 +306,12 @@ const onDeviceFound = (res: any) =&gt; { @@ -286,12 +306,12 @@ const onDeviceFound = (res: any) =&gt; {
286 const name = (d.localName || d.name || '').trim() 306 const name = (d.localName || d.name || '').trim()
287 const displayName = name || 'Unknown Device' 307 const displayName = name || 'Unknown Device'
288 discoveredIds.add(d.deviceId) 308 discoveredIds.add(d.deviceId)
289 - devices.value.push({ 309 + devices.value.push(describeDiscoveredPrinter({
290 deviceId: d.deviceId, 310 deviceId: d.deviceId,
291 name: displayName, 311 name: displayName,
292 RSSI: d.RSSI, 312 RSSI: d.RSSI,
293 type: 'ble', 313 type: 'ble',
294 - }) 314 + }))
295 } 315 }
296 devices.value.sort((a, b) => (b.RSSI || -100) - (a.RSSI || -100)) 316 devices.value.sort((a, b) => (b.RSSI || -100) - (a.RSSI || -100))
297 } 317 }
@@ -305,12 +325,12 @@ function mergeCachedBleDevices () { @@ -305,12 +325,12 @@ function mergeCachedBleDevices () {
305 if (discoveredIds.has(d.deviceId)) continue 325 if (discoveredIds.has(d.deviceId)) continue
306 const name = (d.localName || d.name || '').trim() 326 const name = (d.localName || d.name || '').trim()
307 discoveredIds.add(d.deviceId) 327 discoveredIds.add(d.deviceId)
308 - devices.value.push({ 328 + devices.value.push(describeDiscoveredPrinter({
309 deviceId: d.deviceId, 329 deviceId: d.deviceId,
310 name: name || 'Unknown Device', 330 name: name || 'Unknown Device',
311 RSSI: d.RSSI, 331 RSSI: d.RSSI,
312 type: 'ble', 332 type: 'ble',
313 - }) 333 + }))
314 } 334 }
315 if (list.length > 0) devices.value.sort((a, b) => (b.RSSI || -100) - (a.RSSI || -100)) 335 if (list.length > 0) devices.value.sort((a, b) => (b.RSSI || -100) - (a.RSSI || -100))
316 }, 336 },
@@ -319,60 +339,65 @@ function mergeCachedBleDevices () { @@ -319,60 +339,65 @@ function mergeCachedBleDevices () {
319 339
320 function addPairedDevices () { 340 function addPairedDevices () {
321 // #ifdef APP-PLUS 341 // #ifdef APP-PLUS
322 - const classic = getClassicBluetooth() 342 + const classic = classicBluetooth
323 if (!classic || !classic.getPairedDevices) return 343 if (!classic || !classic.getPairedDevices) return
324 try { 344 try {
325 const paired = classic.getPairedDevices() 345 const paired = classic.getPairedDevices()
  346 + debugInfo.value.pairedCount = (paired || []).length
  347 + debugInfo.value.foundVirtualPrinter = (paired || []).some((item: any) => String(item?.name || '').toLowerCase().includes('virtual bt printer'))
326 for (const p of paired) { 348 for (const p of paired) {
327 if (discoveredIds.has(p.deviceId)) continue 349 if (discoveredIds.has(p.deviceId)) continue
328 discoveredIds.add(p.deviceId) 350 discoveredIds.add(p.deviceId)
329 - devices.value.push({ 351 + devices.value.push(describeDiscoveredPrinter({
330 deviceId: p.deviceId, 352 deviceId: p.deviceId,
331 - name: p.name || 'Unknown', 353 + name: p.name || 'Unknown Device',
332 type: p.type || 'classic', 354 type: p.type || 'classic',
333 - }) 355 + }))
334 } 356 }
335 if (paired.length > 0) { 357 if (paired.length > 0) {
  358 + debugInfo.value.lastClassicEvent = 'paired devices loaded'
336 devices.value.sort((a, b) => (b.RSSI || -100) - (a.RSSI || -100)) 359 devices.value.sort((a, b) => (b.RSSI || -100) - (a.RSSI || -100))
  360 + } else {
  361 + debugInfo.value.lastClassicEvent = 'no paired devices'
337 } 362 }
338 } catch (e) { 363 } catch (e) {
339 console.error('addPairedDevices error:', e) 364 console.error('addPairedDevices error:', e)
  365 + debugInfo.value.lastClassicEvent = 'load paired devices failed'
340 } 366 }
341 // #endif 367 // #endif
342 } 368 }
343 369
344 -function findWriteCharacteristic (deviceId: string): Promise<{ serviceId: string; characteristicId: string } | null> {  
345 - return new Promise((resolve) => {  
346 - uni.getBLEDeviceServices({  
347 - deviceId,  
348 - success: (sres) => {  
349 - const services = sres.services || []  
350 - const tryNext = (idx: number) => {  
351 - if (idx >= services.length) {  
352 - resolve(null)  
353 - return  
354 - }  
355 - const serviceId = services[idx].uuid  
356 - uni.getBLEDeviceCharacteristics({  
357 - deviceId,  
358 - serviceId,  
359 - success: (cres) => {  
360 - const chars = cres.characteristics || []  
361 - const writeChar = chars.find((c: any) => c.properties && c.properties.write)  
362 - if (writeChar) {  
363 - resolve({ serviceId, characteristicId: writeChar.uuid })  
364 - return  
365 - }  
366 - tryNext(idx + 1)  
367 - },  
368 - fail: () => tryNext(idx + 1),  
369 - })  
370 - }  
371 - tryNext(0)  
372 - },  
373 - fail: () => resolve(null),  
374 - })  
375 - }) 370 +function startClassicScan () {
  371 + // #ifdef APP-PLUS
  372 + const classic = classicBluetooth
  373 + if (classic && classic.startClassicDiscovery) {
  374 + try {
  375 + classic.startClassicDiscovery(
  376 + (dev: { name: string; deviceId: string; type: string }) => {
  377 + debugInfo.value.lastClassicEvent = 'device found'
  378 + if (discoveredIds.has(dev.deviceId)) return
  379 + discoveredIds.add(dev.deviceId)
  380 + devices.value.push(describeDiscoveredPrinter({
  381 + deviceId: dev.deviceId,
  382 + name: dev.name || 'Unknown Device',
  383 + type: (dev.type as BtDevice['type']) || 'classic',
  384 + }))
  385 + devices.value.sort((a, b) => (b.RSSI || -100) - (a.RSSI || -100))
  386 + },
  387 + () => {
  388 + debugInfo.value.lastClassicEvent = 'scan finished'
  389 + },
  390 + )
  391 + isScanning.value = true
  392 + debugInfo.value.lastClassicEvent = 'scan running'
  393 + } catch (e) {
  394 + console.error('Classic discovery failed', e)
  395 + debugInfo.value.lastClassicEvent = 'scan failed'
  396 + }
  397 + } else {
  398 + debugInfo.value.lastClassicEvent = 'classic module unavailable'
  399 + }
  400 + // #endif
376 } 401 }
377 402
378 const handleScan = async () => { 403 const handleScan = async () => {
@@ -383,15 +408,30 @@ const handleScan = async () =&gt; { @@ -383,15 +408,30 @@ const handleScan = async () =&gt; {
383 errorMsg.value = '' 408 errorMsg.value = ''
384 devices.value = [] 409 devices.value = []
385 discoveredIds.clear() 410 discoveredIds.clear()
  411 + debugInfo.value.lastBleError = ''
  412 + debugInfo.value.locationServiceRequired = false
  413 + debugInfo.value.lastClassicEvent = 'starting'
  414 + debugInfo.value.lastBleEvent = 'starting'
  415 +
386 try { 416 try {
387 - if (!btAdapterReady.value) {  
388 - await initBluetooth() 417 + const permissionResult = await ensureBluetoothPermissions({ scan: true, connect: true })
  418 + if (!permissionResult.ok) {
  419 + errorMsg.value = permissionResult.message || 'Bluetooth permission denied.'
  420 + return
389 } 421 }
  422 + await initBluetooth()
  423 +
390 addPairedDevices() 424 addPairedDevices()
391 - mergeCachedBleDevices()  
392 - if (devices.value.length > 0) {  
393 - uni.showToast({ title: `Found ${devices.value.length} device(s)`, icon: 'none' }) 425 + startClassicScan()
  426 +
  427 + if (hasPreferredClassicDeviceInList()) {
  428 + isScanning.value = false
  429 + debugInfo.value.lastBleEvent = 'skipped (paired classic device found)'
  430 + uni.showToast({ title: 'Using paired classic Bluetooth devices', icon: 'none' })
  431 + return
394 } 432 }
  433 +
  434 + mergeCachedBleDevices()
395 await new Promise<void>((resolve) => { 435 await new Promise<void>((resolve) => {
396 uni.getLocation({ 436 uni.getLocation({
397 type: 'wgs84', 437 type: 'wgs84',
@@ -399,13 +439,15 @@ const handleScan = async () =&gt; { @@ -399,13 +439,15 @@ const handleScan = async () =&gt; {
399 fail: () => resolve(), 439 fail: () => resolve(),
400 }) 440 })
401 }) 441 })
402 - startDiscovery() 442 + startBleScan()
403 uni.showToast({ title: 'Scanning...', icon: 'none' }) 443 uni.showToast({ title: 'Scanning...', icon: 'none' })
404 setTimeout(() => { 444 setTimeout(() => {
405 if (isScanning.value) stopDiscovery() 445 if (isScanning.value) stopDiscovery()
406 }, 20000) 446 }, 20000)
407 } catch (_) { 447 } catch (_) {
408 - // error set in initBluetooth 448 + if (!isScanning.value && devices.value.length === 0) {
  449 + errorMsg.value = 'Bluetooth scan failed. Check if Bluetooth is enabled.'
  450 + }
409 } 451 }
410 } 452 }
411 453
@@ -416,77 +458,27 @@ const handleConnect = async (dev: BtDevice) =&gt; { @@ -416,77 +458,27 @@ const handleConnect = async (dev: BtDevice) =&gt; {
416 458
417 if (isScanning.value) stopDiscovery() 459 if (isScanning.value) stopDiscovery()
418 460
419 - // 经典蓝牙:classic、dual、unknown(部分设备如 D320FAX 可能误报为 unknown,也尝试经典连接)  
420 - const useClassic = dev.type === 'classic' || dev.type === 'dual' || dev.type === 'unknown'  
421 -  
422 - if (useClassic) {  
423 - // #ifdef APP-PLUS  
424 - const classic = getClassicBluetooth()  
425 - if (classic && classic.connDevice) {  
426 - classic.connDevice(dev.deviceId, (ok: boolean) => {  
427 - connectingId.value = ''  
428 - if (ok) {  
429 - setBluetoothConnection({  
430 - deviceId: dev.deviceId,  
431 - deviceName: dev.name,  
432 - deviceType: 'classic',  
433 - })  
434 - uni.showToast({ title: 'Connected!', icon: 'success' })  
435 - } else {  
436 - errorMsg.value = 'Connection failed. For D320FAX, ensure Virtual BT Printer is paired in system Bluetooth.'  
437 - }  
438 - })  
439 - } else {  
440 - connectingId.value = ''  
441 - errorMsg.value = 'Classic Bluetooth not available. Ensure app is running on the device (not simulator).'  
442 - }  
443 - // #endif  
444 - // #ifndef APP-PLUS 461 + const permissionResult = await ensureBluetoothPermissions({ connect: true })
  462 + if (!permissionResult.ok) {
445 connectingId.value = '' 463 connectingId.value = ''
446 - errorMsg.value = 'Classic Bluetooth requires the app.'  
447 - // #endif 464 + errorMsg.value = permissionResult.message || 'Bluetooth permission denied.'
448 return 465 return
449 } 466 }
450 467
451 - uni.createBLEConnection({  
452 - deviceId: dev.deviceId,  
453 - timeout: 10000,  
454 - success: async () => {  
455 - try {  
456 - const write = await findWriteCharacteristic(dev.deviceId)  
457 - if (!write) {  
458 - errorMsg.value = 'No writable characteristic found. This device may not support printing.'  
459 - connectingId.value = ''  
460 - return  
461 - }  
462 - setBluetoothConnection({  
463 - deviceId: dev.deviceId,  
464 - deviceName: dev.name,  
465 - serviceId: write.serviceId,  
466 - characteristicId: write.characteristicId,  
467 - deviceType: 'ble',  
468 - mtu: 20,  
469 - })  
470 - connectingId.value = ''  
471 - uni.showToast({ title: 'Connected!', icon: 'success' })  
472 - } catch (e: any) {  
473 - errorMsg.value = (e && e.message) ? e.message : 'Connection failed'  
474 - connectingId.value = ''  
475 - }  
476 - },  
477 - fail: (err: any) => {  
478 - connectingId.value = ''  
479 - if (err.errCode === -1) {  
480 - uni.showToast({ title: 'Already connected', icon: 'success' })  
481 - } else {  
482 - errorMsg.value = 'Connection failed: ' + (err.errMsg || 'Try again')  
483 - }  
484 - },  
485 - }) 468 + try {
  469 + await connectBluetoothPrinter(dev)
  470 + refreshCurrentPrinter()
  471 + connectingId.value = ''
  472 + uni.showToast({ title: 'Connected!', icon: 'success' })
  473 + } catch (e: any) {
  474 + errorMsg.value = (e && e.message) ? e.message : 'Connection failed'
  475 + connectingId.value = ''
  476 + }
486 } 477 }
487 478
488 const handleUseBuiltin = () => { 479 const handleUseBuiltin = () => {
489 - setBuiltinPrinter() 480 + useBuiltinPrinter()
  481 + refreshCurrentPrinter()
490 uni.showToast({ title: 'Using built-in printer', icon: 'success' }) 482 uni.showToast({ title: 'Using built-in printer', icon: 'success' })
491 } 483 }
492 484
@@ -495,8 +487,7 @@ const handleTestPrint = async () =&gt; { @@ -495,8 +487,7 @@ const handleTestPrint = async () =&gt; {
495 if (testPrinting.value) return 487 if (testPrinting.value) return
496 testPrinting.value = true 488 testPrinting.value = true
497 try { 489 try {
498 - const data = buildTestTscLabel()  
499 - await sendToPrinter(data, (p) => { 490 + await testPrintCurrentPrinter((p) => {
500 if (p < 100) uni.showLoading({ title: `Printing ${p}%`, mask: true }) 491 if (p < 100) uni.showLoading({ title: `Printing ${p}%`, mask: true })
501 }) 492 })
502 uni.hideLoading() 493 uni.hideLoading()
@@ -523,30 +514,14 @@ const handleTestPrint = async () =&gt; { @@ -523,30 +514,14 @@ const handleTestPrint = async () =&gt; {
523 } 514 }
524 } 515 }
525 516
526 -const handleDisconnect = () => {  
527 - const type = getPrinterType()  
528 - const deviceId = uni.getStorageSync('btDeviceId')  
529 - const deviceType = uni.getStorageSync('btDeviceType')  
530 - if (type === 'bluetooth' && deviceType === 'classic') {  
531 - // #ifdef APP-PLUS  
532 - const classic = getClassicBluetooth()  
533 - if (classic && classic.disConnDevice) classic.disConnDevice()  
534 - // #endif  
535 - }  
536 - clearPrinter()  
537 - if (type === 'bluetooth' && deviceId && deviceType !== 'classic') {  
538 - uni.closeBLEConnection({  
539 - deviceId,  
540 - complete: () => {  
541 - uni.showToast({ title: 'Disconnected', icon: 'none' })  
542 - },  
543 - })  
544 - } else {  
545 - uni.showToast({ title: 'Disconnected', icon: 'none' })  
546 - } 517 +const handleDisconnect = async () => {
  518 + await disconnectCurrentPrinter()
  519 + refreshCurrentPrinter()
  520 + uni.showToast({ title: 'Disconnected', icon: 'none' })
547 } 521 }
548 522
549 onMounted(() => { 523 onMounted(() => {
  524 + debugInfo.value.classicModuleReady = !!classicBluetooth
550 uni.onBluetoothDeviceFound(onDeviceFound) 525 uni.onBluetoothDeviceFound(onDeviceFound)
551 uni.onBluetoothAdapterStateChange((res: any) => { 526 uni.onBluetoothAdapterStateChange((res: any) => {
552 if (!res.available) { 527 if (!res.available) {
@@ -559,11 +534,22 @@ onMounted(() =&gt; { @@ -559,11 +534,22 @@ onMounted(() =&gt; {
559 } 534 }
560 }) 535 })
561 printerType.value = getPrinterType() || 'bluetooth' 536 printerType.value = getPrinterType() || 'bluetooth'
  537 + refreshCurrentPrinter()
562 }) 538 })
563 539
564 onUnmounted(() => { 540 onUnmounted(() => {
565 if (isScanning.value) stopDiscovery() 541 if (isScanning.value) stopDiscovery()
566 - uni.offBluetoothDeviceFound() 542 + try {
  543 + if (typeof uni.offBluetoothDeviceFound === 'function') {
  544 + uni.offBluetoothDeviceFound()
  545 + }
  546 + } catch (_) {}
  547 + try {
  548 + if (typeof uni.offBluetoothAdapterStateChange === 'function') {
  549 + uni.offBluetoothAdapterStateChange()
  550 + }
  551 + } catch (_) {}
  552 + uni.closeBluetoothAdapter({ complete: () => {} })
567 }) 553 })
568 </script> 554 </script>
569 555
@@ -883,6 +869,37 @@ onUnmounted(() =&gt; { @@ -883,6 +869,37 @@ onUnmounted(() =&gt; {
883 gap: 12rpx; 869 gap: 12rpx;
884 } 870 }
885 871
  872 +.debug-card {
  873 + background: #fff7ed;
  874 + border: 1rpx solid #fed7aa;
  875 + border-radius: 20rpx;
  876 + padding: 24rpx;
  877 + margin-bottom: 24rpx;
  878 + display: flex;
  879 + flex-direction: column;
  880 + gap: 8rpx;
  881 +}
  882 +
  883 +.debug-title {
  884 + font-size: 28rpx;
  885 + font-weight: 700;
  886 + color: #9a3412;
  887 +}
  888 +
  889 +.debug-item {
  890 + font-size: 22rpx;
  891 + color: #7c2d12;
  892 +}
  893 +
  894 +.debug-error {
  895 + color: #b91c1c;
  896 +}
  897 +
  898 +.debug-warn {
  899 + color: #92400e;
  900 + font-weight: 600;
  901 +}
  902 +
886 .device-tag { 903 .device-tag {
887 display: inline-block; 904 display: inline-block;
888 font-size: 20rpx; 905 font-size: 20rpx;
美国版/Food Labeling Management App UniApp/src/pages/labels/preview.vue
@@ -118,8 +118,7 @@ import SideMenu from &#39;../../components/SideMenu.vue&#39; @@ -118,8 +118,7 @@ import SideMenu from &#39;../../components/SideMenu.vue&#39;
118 import LocationPicker from '../../components/LocationPicker.vue' 118 import LocationPicker from '../../components/LocationPicker.vue'
119 import { getStatusBarHeight } from '../../utils/statusBar' 119 import { getStatusBarHeight } from '../../utils/statusBar'
120 import { generateNextLabelId } from '../../utils/printLog' 120 import { generateNextLabelId } from '../../utils/printLog'
121 -import { getPrinterType, getBluetoothConnection, sendToPrinter } from '../../utils/print/printerConnection'  
122 -import { buildTscLabel } from '../../utils/print/tscLabelBuilder' 121 +import { getCurrentPrinterSummary, printLabelForCurrentPrinter } from '../../utils/print/manager/printerManager'
123 import chickenLabelImg from '../../static/chicken-lable.png' 122 import chickenLabelImg from '../../static/chicken-lable.png'
124 123
125 const statusBarHeight = getStatusBarHeight() 124 const statusBarHeight = getStatusBarHeight()
@@ -161,16 +160,9 @@ const displayProductName = computed(() =&gt; { @@ -161,16 +160,9 @@ const displayProductName = computed(() =&gt; {
161 }) 160 })
162 161
163 onShow(() => { 162 onShow(() => {
164 - const type = getPrinterType()  
165 - const conn = type === 'bluetooth' ? getBluetoothConnection() : null  
166 - btConnected.value = (type === 'bluetooth' && conn) || type === 'builtin'  
167 - if (type === 'builtin') {  
168 - btDeviceName.value = 'Built-in Printer'  
169 - } else if (conn) {  
170 - btDeviceName.value = conn.deviceName || ''  
171 - } else {  
172 - btDeviceName.value = ''  
173 - } 163 + const summary = getCurrentPrinterSummary()
  164 + btConnected.value = summary.type === 'bluetooth' || summary.type === 'builtin'
  165 + btDeviceName.value = summary.displayName || ''
174 }) 166 })
175 167
176 interface ProductData { 168 interface ProductData {
@@ -245,13 +237,12 @@ const handlePrint = async () =&gt; { @@ -245,13 +237,12 @@ const handlePrint = async () =&gt; {
245 } 237 }
246 isPrinting.value = true 238 isPrinting.value = true
247 try { 239 try {
248 - const data = buildTscLabel({ 240 + await printLabelForCurrentPrinter({
249 productName: displayProductName.value, 241 productName: displayProductName.value,
250 labelId: labelId.value, 242 labelId: labelId.value,
251 printQty: printQty.value, 243 printQty: printQty.value,
252 extraLine: lastEdited.value, 244 extraLine: lastEdited.value,
253 - })  
254 - await sendToPrinter(data, (percent) => { 245 + }, (percent) => {
255 if (percent >= 100) return 246 if (percent >= 100) return
256 uni.showLoading({ title: `Printing ${percent}%`, mask: true }) 247 uni.showLoading({ title: `Printing ${percent}%`, mask: true })
257 }) 248 })
@@ -265,8 +256,8 @@ const handlePrint = async () =&gt; { @@ -265,8 +256,8 @@ const handlePrint = async () =&gt; {
265 const msg = (e && e.message) ? e.message : 'Print failed' 256 const msg = (e && e.message) ? e.message : 'Print failed'
266 if (msg === 'BUILTIN_PLUGIN_NOT_FOUND' || (msg && msg.indexOf('Built-in printer') !== -1)) { 257 if (msg === 'BUILTIN_PLUGIN_NOT_FOUND' || (msg && msg.indexOf('Built-in printer') !== -1)) {
267 uni.showModal({ 258 uni.showModal({
268 - title: 'Use Bluetooth Mode',  
269 - content: 'For GP-D320FAX, go to Printer settings, switch to Bluetooth mode and tap Scan to connect (e.g. d320fax_295c or Virtual BT Printer). Built-in mode needs custom app packaging.', 259 + title: 'Built-in Print Not Available',
  260 + content: 'This device does not support TCP built-in printing. Please switch to Bluetooth mode: go to Printer Settings, tap Scan, and connect to your printer (look for a device name like "Virtual BT Printer" or your printer model). You may need to pair it first in Android Bluetooth settings.',
270 confirmText: 'Printer Settings', 261 confirmText: 'Printer Settings',
271 cancelText: 'Cancel', 262 cancelText: 'Cancel',
272 success: (res) => { 263 success: (res) => {
美国版/Food Labeling Management App UniApp/src/pages/more/printers.vue
@@ -21,6 +21,20 @@ @@ -21,6 +21,20 @@
21 <text class="badge-text">{{ t('printers.connected') }}</text> 21 <text class="badge-text">{{ t('printers.connected') }}</text>
22 </view> 22 </view>
23 23
  24 + <view class="debug-card">
  25 + <text class="debug-title">Debug Status</text>
  26 + <text class="debug-item">Current Mode: {{ debugInfo.currentMode }}</text>
  27 + <text class="debug-item">Classic Module: {{ debugInfo.classicModuleReady ? 'Ready' : 'Not Ready' }}</text>
  28 + <text class="debug-item">Paired Count: {{ debugInfo.pairedCount }}</text>
  29 + <text class="debug-item">Virtual BT Printer: {{ debugInfo.foundVirtualPrinter ? 'Found' : 'Not Found' }}</text>
  30 + <text class="debug-item">Classic Scan: {{ debugInfo.lastClassicEvent }}</text>
  31 + <text class="debug-item">BLE Scan: {{ debugInfo.lastBleEvent }}</text>
  32 + <text v-if="debugInfo.lastBleError" class="debug-item debug-error">{{ debugInfo.lastBleError }}</text>
  33 + <text v-if="debugInfo.locationServiceRequired" class="debug-item debug-warn">
  34 + Android system Location service is OFF. Turn it on in device settings before BLE scan.
  35 + </text>
  36 + </view>
  37 +
24 <!-- Connected Device --> 38 <!-- Connected Device -->
25 <view v-if="currentType" class="printer-card connected"> 39 <view v-if="currentType" class="printer-card connected">
26 <view class="printer-icon"><AppIcon name="printer" size="md" color="white" /></view> 40 <view class="printer-icon"><AppIcon name="printer" size="md" color="white" /></view>
@@ -45,6 +59,29 @@ @@ -45,6 +59,29 @@
45 <button class="btn-connect">{{ t('printers.connect') }}</button> 59 <button class="btn-connect">{{ t('printers.connect') }}</button>
46 </view> 60 </view>
47 61
  62 + <!-- Paired Devices -->
  63 + <!-- #ifdef APP-PLUS -->
  64 + <view class="section-header">
  65 + <text class="section-title">{{ t('printers.pairedDevices') }}</text>
  66 + </view>
  67 +
  68 + <view v-for="d in pairedDevices" :key="'paired-' + d.deviceId" class="printer-card device-item" @click="connectBt(d)">
  69 + <view class="printer-icon"><AppIcon name="bluetooth" size="md" color="blue" /></view>
  70 + <view class="printer-info">
  71 + <text class="printer-name">{{ d.name || 'Unknown Device' }}</text>
  72 + <text class="printer-loc">{{ d.deviceId }}</text>
  73 + </view>
  74 + <view class="device-meta">
  75 + <text class="device-type-tag" :class="'tag-' + (d.type || 'ble')">{{ getTypeLabel(d.type) }}</text>
  76 + <button class="btn-connect" :disabled="isConnecting">{{ t('printers.connect') }}</button>
  77 + </view>
  78 + </view>
  79 +
  80 + <view v-if="pairedDevices.length === 0" class="empty-state">
  81 + <text class="empty-text">{{ t('printers.noPairedDevices') }}</text>
  82 + </view>
  83 + <!-- #endif -->
  84 +
48 <!-- Bluetooth Scanning Area --> 85 <!-- Bluetooth Scanning Area -->
49 <view class="section-header"> 86 <view class="section-header">
50 <text class="section-title">{{ t('printers.nearby') }}</text> 87 <text class="section-title">{{ t('printers.nearby') }}</text>
@@ -62,7 +99,10 @@ @@ -62,7 +99,10 @@
62 <text class="printer-name">{{ d.name || 'Unknown Device' }}</text> 99 <text class="printer-name">{{ d.name || 'Unknown Device' }}</text>
63 <text class="printer-loc">{{ d.deviceId }}</text> 100 <text class="printer-loc">{{ d.deviceId }}</text>
64 </view> 101 </view>
65 - <button class="btn-connect" :disabled="isConnecting">{{ t('printers.connect') }}</button> 102 + <view class="device-meta">
  103 + <text v-if="d.type && d.type !== 'unknown'" class="device-type-tag" :class="'tag-' + d.type">{{ getTypeLabel(d.type) }}</text>
  104 + <button class="btn-connect" :disabled="isConnecting">{{ t('printers.connect') }}</button>
  105 + </view>
66 </view> 106 </view>
67 107
68 <view v-if="devices.length === 0 && !isScanning" class="empty-state"> 108 <view v-if="devices.length === 0 && !isScanning" class="empty-state">
@@ -79,178 +119,290 @@ import { ref, onMounted, onUnmounted } from &#39;vue&#39; @@ -79,178 +119,290 @@ import { ref, onMounted, onUnmounted } from &#39;vue&#39;
79 import { useI18n } from 'vue-i18n' 119 import { useI18n } from 'vue-i18n'
80 import AppIcon from '../../components/AppIcon.vue' 120 import AppIcon from '../../components/AppIcon.vue'
81 import SideMenu from '../../components/SideMenu.vue' 121 import SideMenu from '../../components/SideMenu.vue'
  122 +import classicBluetooth from '../../utils/print/bluetoothTool.js'
  123 +import { ensureBluetoothPermissions } from '../../utils/print/bluetoothPermissions'
82 import { 124 import {
83 - getPrinterType,  
84 - getBluetoothConnection,  
85 - setBluetoothConnection,  
86 - setBuiltinPrinter,  
87 - clearPrinter,  
88 - sendToPrinter,  
89 - isBuiltinConnected  
90 -} from '../../utils/print/printerConnection'  
91 -import { buildTestTscLabel } from '../../utils/print/tscLabelBuilder' 125 + connectBluetoothPrinter,
  126 + describeDiscoveredPrinter,
  127 + disconnectCurrentPrinter,
  128 + getCurrentPrinterSummary,
  129 + testPrintCurrentPrinter,
  130 + useBuiltinPrinter,
  131 +} from '../../utils/print/manager/printerManager'
92 132
93 const { t } = useI18n() 133 const { t } = useI18n()
94 const isMenuOpen = ref(false) 134 const isMenuOpen = ref(false)
95 135
96 -// 状态管理  
97 -const currentType = ref(getPrinterType())  
98 -const currentBt = ref(getBluetoothConnection()) 136 +const currentType = ref<'' | 'bluetooth' | 'builtin'>('')
  137 +const currentBt = ref<any>(null)
99 const isScanning = ref(false) 138 const isScanning = ref(false)
100 const devices = ref<any[]>([]) 139 const devices = ref<any[]>([])
  140 +const pairedDevices = ref<any[]>([])
101 const isConnecting = ref(false) 141 const isConnecting = ref(false)
  142 +const debugInfo = ref({
  143 + currentMode: 'none',
  144 + classicModuleReady: false,
  145 + pairedCount: 0,
  146 + foundVirtualPrinter: false,
  147 + lastClassicEvent: 'idle',
  148 + lastBleEvent: 'idle',
  149 + lastBleError: '',
  150 + locationServiceRequired: false,
  151 +})
  152 +let bleListenerRegistered = false
102 153
103 -// Refresh current state  
104 const refreshStatus = () => { 154 const refreshStatus = () => {
105 - currentType.value = getPrinterType()  
106 - currentBt.value = getBluetoothConnection() 155 + const summary = getCurrentPrinterSummary()
  156 + currentType.value = summary.type
  157 + debugInfo.value.currentMode = summary.type || 'none'
  158 + currentBt.value = summary.type === 'bluetooth'
  159 + ? {
  160 + deviceName: summary.displayName,
  161 + deviceId: summary.deviceId,
  162 + deviceType: summary.deviceType,
  163 + driverName: summary.driverName,
  164 + protocol: summary.protocol,
  165 + }
  166 + : null
  167 +}
  168 +
  169 +const getTypeLabel = (type?: string) => {
  170 + switch (type) {
  171 + case 'classic': return t('printers.classic')
  172 + case 'ble': return t('printers.ble')
  173 + case 'dual': return t('printers.dual')
  174 + default: return t('printers.ble')
  175 + }
  176 +}
  177 +
  178 +const normalizeDeviceName = (device: any) => {
  179 + return (device?.localName || device?.name || '').trim() || 'Unknown Device'
  180 +}
  181 +
  182 +const hasPreferredClassicDevice = () => {
  183 + return pairedDevices.value.some((item: any) => {
  184 + const name = String(item?.name || '').toLowerCase()
  185 + const type = String(item?.type || '').toLowerCase()
  186 + return name.includes('virtual bt printer') || type === 'classic' || type === 'dual'
  187 + })
  188 +}
  189 +
  190 +const addDeviceDedup = (device: any) => {
  191 + const described = describeDiscoveredPrinter(device)
  192 + const existing = devices.value.find(d => d.deviceId === device.deviceId)
  193 + if (!existing) {
  194 + devices.value.push(described)
  195 + return
  196 + }
  197 + Object.assign(existing, described)
  198 +}
  199 +
  200 +// #ifdef APP-PLUS
  201 +const loadPairedDevices = () => {
  202 + try {
  203 + const list = classicBluetooth.getPairedDevices()
  204 + debugInfo.value.pairedCount = (list || []).length
  205 + debugInfo.value.foundVirtualPrinter = (list || []).some((item: any) => String(item?.name || '').toLowerCase().includes('virtual bt printer'))
  206 + pairedDevices.value = (list || []).map((item: any) => ({
  207 + ...describeDiscoveredPrinter(item),
  208 + name: normalizeDeviceName(item),
  209 + }))
  210 + debugInfo.value.lastClassicEvent = pairedDevices.value.length > 0 ? 'paired devices loaded' : 'no paired devices'
  211 + } catch (e) {
  212 + console.error('Failed to load paired devices', e)
  213 + pairedDevices.value = []
  214 + debugInfo.value.pairedCount = 0
  215 + debugInfo.value.foundVirtualPrinter = false
  216 + debugInfo.value.lastClassicEvent = 'load paired devices failed'
  217 + }
107 } 218 }
  219 +// #endif
108 220
109 -// 蓝牙搜索  
110 -const startScan = () => { 221 +function mergeCachedBleDevices () {
  222 + uni.getBluetoothDevices({
  223 + success: (res: any) => {
  224 + const list = res.devices || []
  225 + list.forEach((device: any) => {
  226 + addDeviceDedup({
  227 + ...device,
  228 + name: normalizeDeviceName(device),
  229 + type: device.type || 'ble',
  230 + })
  231 + })
  232 + },
  233 + })
  234 +}
  235 +
  236 +const startScan = async () => {
111 if (isScanning.value) return 237 if (isScanning.value) return
112 - 238 +
113 devices.value = [] 239 devices.value = []
114 - isScanning.value = true  
115 - 240 + debugInfo.value.lastBleError = ''
  241 + debugInfo.value.locationServiceRequired = false
  242 + debugInfo.value.lastClassicEvent = 'starting'
  243 + debugInfo.value.lastBleEvent = 'starting'
  244 +
  245 + const permissionResult = await ensureBluetoothPermissions({ scan: true, connect: true })
  246 + if (!permissionResult.ok) {
  247 + uni.showToast({ title: permissionResult.message || t('printers.bleNotAvailable'), icon: 'none' })
  248 + return
  249 + }
  250 +
116 uni.openBluetoothAdapter({ 251 uni.openBluetoothAdapter({
117 success: () => { 252 success: () => {
  253 + // #ifdef APP-PLUS
  254 + loadPairedDevices()
  255 + try {
  256 + classicBluetooth.startClassicDiscovery(
  257 + (device: any) => {
  258 + debugInfo.value.lastClassicEvent = 'device found'
  259 + addDeviceDedup({
  260 + ...device,
  261 + name: normalizeDeviceName(device),
  262 + })
  263 + },
  264 + () => {
  265 + debugInfo.value.lastClassicEvent = 'scan finished'
  266 + }
  267 + )
  268 + debugInfo.value.lastClassicEvent = 'scan running'
  269 + } catch (e) {
  270 + console.error('Classic discovery failed', e)
  271 + debugInfo.value.lastClassicEvent = 'scan failed'
  272 + }
  273 + // #endif
  274 + isScanning.value = true
  275 +
  276 + if (hasPreferredClassicDevice()) {
  277 + debugInfo.value.lastBleEvent = 'skipped (paired classic device found)'
  278 + return
  279 + }
  280 +
  281 + mergeCachedBleDevices()
  282 + if (!bleListenerRegistered) {
  283 + bleListenerRegistered = true
  284 + uni.onBluetoothDeviceFound((res) => {
  285 + res.devices.forEach(device => {
  286 + addDeviceDedup({
  287 + ...device,
  288 + name: normalizeDeviceName(device),
  289 + type: 'ble',
  290 + })
  291 + })
  292 + })
  293 + }
118 uni.startBluetoothDevicesDiscovery({ 294 uni.startBluetoothDevicesDiscovery({
119 allowDuplicatesKey: false, 295 allowDuplicatesKey: false,
120 success: () => { 296 success: () => {
121 - uni.onBluetoothDeviceFound((res) => {  
122 - res.devices.forEach(device => {  
123 - if (device.name && !devices.value.find(d => d.deviceId === device.deviceId)) {  
124 - devices.value.push(device)  
125 - }  
126 - })  
127 - }) 297 + debugInfo.value.lastBleEvent = 'scan running'
128 }, 298 },
129 fail: (err) => { 299 fail: (err) => {
130 - isScanning.value = false  
131 - uni.showToast({ title: t('printers.bleNotAvailable'), icon: 'none' }) 300 + console.error('BLE startBluetoothDevicesDiscovery fail:', err)
  301 + debugInfo.value.lastBleEvent = 'scan failed'
  302 + debugInfo.value.lastBleError = err?.errMsg || 'BLE scan failed'
  303 + if (err?.errCode === 10016 || err?.code === 10016) {
  304 + debugInfo.value.locationServiceRequired = true
  305 + debugInfo.value.lastBleError = 'BLE scan failed: system Location service is turned off.'
  306 + uni.showToast({
  307 + title: 'Turn on system Location service first',
  308 + icon: 'none',
  309 + duration: 2500,
  310 + })
  311 + }
132 } 312 }
133 }) 313 })
134 }, 314 },
135 fail: (err) => { 315 fail: (err) => {
136 - isScanning.value = false  
137 - uni.showToast({ title: t('printers.bleNotAvailable'), icon: 'none' }) 316 + console.error('openBluetoothAdapter fail:', err)
  317 + if (devices.value.length === 0 && pairedDevices.value.length === 0) {
  318 + uni.showToast({ title: t('printers.bleNotAvailable'), icon: 'none' })
  319 + }
138 } 320 }
139 }) 321 })
140 } 322 }
141 323
142 const stopScan = () => { 324 const stopScan = () => {
143 isScanning.value = false 325 isScanning.value = false
  326 + debugInfo.value.lastClassicEvent = 'stopped'
  327 + debugInfo.value.lastBleEvent = 'stopped'
144 uni.stopBluetoothDevicesDiscovery({ 328 uni.stopBluetoothDevicesDiscovery({
145 - success: () => console.log('Stop scan success'),  
146 - fail: (err) => console.error('Stop scan fail', err) 329 + success: () => console.log('Stop BLE scan success'),
  330 + fail: (err) => console.error('Stop BLE scan fail', err)
147 }) 331 })
  332 + // #ifdef APP-PLUS
  333 + try {
  334 + classicBluetooth.cancelClassicDiscovery()
  335 + } catch (e) {
  336 + console.error('Cancel classic discovery failed', e)
  337 + }
  338 + // #endif
148 } 339 }
149 340
150 -// 连接蓝牙设备  
151 -const connectBt = (device: any) => { 341 +const connectBt = async (device: any) => {
152 if (isConnecting.value) return 342 if (isConnecting.value) return
153 isConnecting.value = true 343 isConnecting.value = true
154 stopScan() 344 stopScan()
155 -  
156 - uni.showLoading({ title: t('printers.connecting') })  
157 -  
158 - uni.createBLEConnection({  
159 - deviceId: device.deviceId,  
160 - success: () => {  
161 - uni.getBLEDeviceServices({  
162 - deviceId: device.deviceId,  
163 - success: (res) => {  
164 - // 这里简化处理:寻找第一个包含写权限的特征值  
165 - // 实际项目中可能需要根据特定 UUID 匹配,官方示例中是遍历寻找  
166 - findWriteCharacteristic(device.deviceId, res.services, device.name)  
167 - },  
168 - fail: (err) => {  
169 - isConnecting.value = false  
170 - uni.hideLoading()  
171 - uni.showToast({ title: t('printers.connectFail'), icon: 'none' })  
172 - }  
173 - })  
174 - },  
175 - fail: (err) => {  
176 - isConnecting.value = false  
177 - uni.hideLoading()  
178 - uni.showToast({ title: t('printers.connectFail'), icon: 'none' })  
179 - }  
180 - })  
181 -}  
182 345
183 -const findWriteCharacteristic = (deviceId: string, services: any[], deviceName: string) => {  
184 - let found = false  
185 - let serviceIdx = 0  
186 -  
187 - const nextService = () => {  
188 - if (serviceIdx >= services.length || found) {  
189 - if (!found) {  
190 - isConnecting.value = false  
191 - uni.hideLoading()  
192 - uni.showToast({ title: 'No write characteristic found', icon: 'none' })  
193 - }  
194 - return  
195 - }  
196 -  
197 - const service = services[serviceIdx++]  
198 - uni.getBLEDeviceCharacteristics({  
199 - deviceId,  
200 - serviceId: service.uuid,  
201 - success: (res) => {  
202 - const char = res.characteristics.find(c => c.properties.write)  
203 - if (char) {  
204 - found = true  
205 - setBluetoothConnection({  
206 - deviceId,  
207 - deviceName,  
208 - serviceId: service.uuid,  
209 - characteristicId: char.uuid,  
210 - deviceType: 'ble'  
211 - })  
212 - currentType.value = 'bluetooth'  
213 - currentBt.value = getBluetoothConnection()  
214 - isConnecting.value = false  
215 - uni.hideLoading()  
216 - uni.showToast({ title: t('printers.connectSuccess') })  
217 - } else {  
218 - nextService()  
219 - }  
220 - },  
221 - fail: () => nextService()  
222 - }) 346 + const permissionResult = await ensureBluetoothPermissions({ connect: true })
  347 + if (!permissionResult.ok) {
  348 + isConnecting.value = false
  349 + uni.showToast({ title: permissionResult.message || t('printers.connectFail'), icon: 'none' })
  350 + return
  351 + }
  352 +
  353 + uni.showLoading({ title: t('printers.connecting') })
  354 + try {
  355 + await connectBluetoothPrinter(device)
  356 + refreshStatus()
  357 + uni.hideLoading()
  358 + uni.showToast({ title: t('printers.connectSuccess') })
  359 + } catch (e) {
  360 + uni.hideLoading()
  361 + uni.showToast({ title: t('printers.connectFail'), icon: 'none' })
  362 + } finally {
  363 + isConnecting.value = false
223 } 364 }
224 -  
225 - nextService()  
226 } 365 }
227 366
228 -// 连接内置打印机  
229 const connectBuiltin = () => { 367 const connectBuiltin = () => {
230 - setBuiltinPrinter()  
231 - currentType.value = 'builtin' 368 + useBuiltinPrinter()
  369 + refreshStatus()
232 uni.showToast({ title: t('printers.connectSuccess') }) 370 uni.showToast({ title: t('printers.connectSuccess') })
233 } 371 }
234 372
235 -// 断开连接  
236 -const disconnect = () => {  
237 - clearPrinter()  
238 - currentType.value = ''  
239 - currentBt.value = null 373 +const disconnect = async () => {
  374 + await disconnectCurrentPrinter()
  375 + refreshStatus()
240 uni.showToast({ title: t('printers.disconnected') }) 376 uni.showToast({ title: t('printers.disconnected') })
241 } 377 }
242 378
243 -// 测试打印  
244 const doTestPrint = async () => { 379 const doTestPrint = async () => {
245 try { 380 try {
246 - const data = buildTestTscLabel() 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 + }
247 uni.showLoading({ title: t('labels.print.printing') }) 389 uni.showLoading({ title: t('labels.print.printing') })
248 - await sendToPrinter(data) 390 + await testPrintCurrentPrinter()
249 uni.hideLoading() 391 uni.hideLoading()
250 uni.showToast({ title: t('printers.testPrintSuccess') }) 392 uni.showToast({ title: t('printers.testPrintSuccess') })
251 } catch (e: any) { 393 } catch (e: any) {
252 uni.hideLoading() 394 uni.hideLoading()
253 - uni.showToast({ title: t('printers.testPrintFail') + ': ' + e.message, icon: 'none' }) 395 + const msg = (e && e.message) ? e.message : 'Print failed'
  396 + if (msg.indexOf('Built-in printer') !== -1 || msg === 'BUILTIN_PLUGIN_NOT_FOUND') {
  397 + uni.showModal({
  398 + title: 'Print Failed',
  399 + content: 'Built-in TCP printing is not available on this device. Please switch to Bluetooth mode and scan for your printer. Check Android Bluetooth settings to pair with "Virtual BT Printer" or your printer model first.',
  400 + confirmText: 'OK',
  401 + showCancel: false,
  402 + })
  403 + } else {
  404 + uni.showToast({ title: t('printers.testPrintFail') + ': ' + msg, icon: 'none' })
  405 + }
254 } 406 }
255 } 407 }
256 408
@@ -265,10 +417,23 @@ const goBack = () =&gt; { @@ -265,10 +417,23 @@ const goBack = () =&gt; {
265 417
266 onMounted(() => { 418 onMounted(() => {
267 refreshStatus() 419 refreshStatus()
  420 + debugInfo.value.classicModuleReady = !!classicBluetooth
  421 + // #ifdef APP-PLUS
  422 + loadPairedDevices()
  423 + // #endif
268 }) 424 })
269 425
270 onUnmounted(() => { 426 onUnmounted(() => {
271 if (isScanning.value) stopScan() 427 if (isScanning.value) stopScan()
  428 + if (bleListenerRegistered) {
  429 + bleListenerRegistered = false
  430 + try {
  431 + if (typeof uni.offBluetoothDeviceFound === 'function') {
  432 + uni.offBluetoothDeviceFound()
  433 + }
  434 + } catch (_) {}
  435 + }
  436 + uni.closeBluetoothAdapter({ complete: () => {} })
272 }) 437 })
273 </script> 438 </script>
274 439
@@ -288,6 +453,11 @@ onUnmounted(() =&gt; { @@ -288,6 +453,11 @@ onUnmounted(() =&gt; {
288 .info-badge { display: flex; align-items: center; gap: 12rpx; margin-bottom: 32rpx; } 453 .info-badge { display: flex; align-items: center; gap: 12rpx; margin-bottom: 32rpx; }
289 .badge-num { font-size: 40rpx; font-weight: 700; color: var(--theme-primary); } 454 .badge-num { font-size: 40rpx; font-weight: 700; color: var(--theme-primary); }
290 .badge-text { font-size: 28rpx; color: #6b7280; } 455 .badge-text { font-size: 28rpx; color: #6b7280; }
  456 +.debug-card { background: #fff7ed; border: 1rpx solid #fed7aa; border-radius: 20rpx; padding: 24rpx; margin-bottom: 24rpx; display: flex; flex-direction: column; gap: 8rpx; }
  457 +.debug-title { font-size: 28rpx; font-weight: 700; color: #9a3412; }
  458 +.debug-item { font-size: 22rpx; color: #7c2d12; }
  459 +.debug-error { color: #b91c1c; }
  460 +.debug-warn { color: #92400e; font-weight: 600; }
291 .printer-card { background: #fff; padding: 32rpx; border-radius: 24rpx; margin-bottom: 24rpx; display: flex; align-items: center; gap: 24rpx; box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.05); border: 1rpx solid #f3f4f6; } 461 .printer-card { background: #fff; padding: 32rpx; border-radius: 24rpx; margin-bottom: 24rpx; display: flex; align-items: center; gap: 24rpx; box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.05); border: 1rpx solid #f3f4f6; }
292 .printer-card.connected { background: var(--theme-primary); border: none; } 462 .printer-card.connected { background: var(--theme-primary); border: none; }
293 .printer-card.connected .printer-name, .printer-card.connected .printer-loc, .printer-card.connected .printer-status { color: #fff; } 463 .printer-card.connected .printer-name, .printer-card.connected .printer-loc, .printer-card.connected .printer-status { color: #fff; }
@@ -316,4 +486,10 @@ onUnmounted(() =&gt; { @@ -316,4 +486,10 @@ onUnmounted(() =&gt; {
316 486
317 .empty-state { padding: 80rpx 0; text-align: center; } 487 .empty-state { padding: 80rpx 0; text-align: center; }
318 .empty-text { font-size: 28rpx; color: #9ca3af; } 488 .empty-text { font-size: 28rpx; color: #9ca3af; }
  489 +
  490 +.device-meta { display: flex; flex-direction: column; align-items: flex-end; gap: 12rpx; flex-shrink: 0; }
  491 +.device-type-tag { display: inline-block; padding: 4rpx 16rpx; font-size: 20rpx; border-radius: 8rpx; font-weight: 500; }
  492 +.tag-ble { background: #eff6ff; color: #3b82f6; }
  493 +.tag-classic { background: #fef3c7; color: #d97706; }
  494 +.tag-dual { background: #ecfdf5; color: #059669; }
319 </style> 495 </style>
美国版/Food Labeling Management App UniApp/src/utils/print/bluetoothPermissions.ts 0 → 100644
  1 +export interface BluetoothPermissionResult {
  2 + ok: boolean
  3 + message?: string
  4 +}
  5 +
  6 +function normalizePermissionName (permission: string): string {
  7 + return String(permission || '').split('.').pop() || String(permission || '')
  8 +}
  9 +
  10 +function formatDeniedMessage (permissions: string[]): string {
  11 + if (!permissions.length) return 'Bluetooth permission denied.'
  12 + return 'Please allow Bluetooth permissions: ' + permissions.map(normalizePermissionName).join(', ')
  13 +}
  14 +
  15 +function requestAndroidPermissions (permissions: string[]): Promise<BluetoothPermissionResult> {
  16 + return new Promise((resolve) => {
  17 + // #ifdef APP-PLUS
  18 + try {
  19 + if (typeof plus === 'undefined' || !plus.android || !permissions.length) {
  20 + resolve({ ok: true })
  21 + return
  22 + }
  23 + plus.android.requestPermissions(
  24 + permissions,
  25 + (resultObj: any) => {
  26 + const deniedPresent = Array.isArray(resultObj?.deniedPresent) ? resultObj.deniedPresent : []
  27 + const deniedAlways = Array.isArray(resultObj?.deniedAlways) ? resultObj.deniedAlways : []
  28 + const denied = [...deniedPresent, ...deniedAlways]
  29 + if (denied.length > 0) {
  30 + resolve({
  31 + ok: false,
  32 + message: formatDeniedMessage(denied),
  33 + })
  34 + return
  35 + }
  36 + resolve({ ok: true })
  37 + },
  38 + () => {
  39 + resolve({
  40 + ok: false,
  41 + message: 'Failed to request Bluetooth permissions.',
  42 + })
  43 + }
  44 + )
  45 + return
  46 + } catch (e: any) {
  47 + resolve({
  48 + ok: false,
  49 + message: e?.message || 'Failed to request Bluetooth permissions.',
  50 + })
  51 + return
  52 + }
  53 + // #endif
  54 + resolve({ ok: true })
  55 + })
  56 +}
  57 +
  58 +export async function ensureBluetoothPermissions (options?: {
  59 + scan?: boolean
  60 + connect?: boolean
  61 +}): Promise<BluetoothPermissionResult> {
  62 + const { scan = false, connect = false } = options || {}
  63 +
  64 + // #ifdef APP-PLUS
  65 + try {
  66 + if (typeof plus === 'undefined' || !plus.android) {
  67 + return { ok: true }
  68 + }
  69 + const Build = plus.android.importClass('android.os.Build')
  70 + const sdkInt = Number(Build.VERSION.SDK_INT || 0)
  71 +
  72 + const permissions = new Set<string>()
  73 + if (sdkInt >= 31) {
  74 + if (scan) permissions.add('android.permission.BLUETOOTH_SCAN')
  75 + if (scan || connect) permissions.add('android.permission.BLUETOOTH_CONNECT')
  76 + if (scan) permissions.add('android.permission.ACCESS_FINE_LOCATION')
  77 + } else if (scan) {
  78 + permissions.add('android.permission.ACCESS_FINE_LOCATION')
  79 + permissions.add('android.permission.ACCESS_COARSE_LOCATION')
  80 + }
  81 +
  82 + return await requestAndroidPermissions(Array.from(permissions))
  83 + } catch (e: any) {
  84 + return {
  85 + ok: false,
  86 + message: e?.message || 'Bluetooth permission check failed.',
  87 + }
  88 + }
  89 + // #endif
  90 +
  91 + return { ok: true }
  92 +}
美国版/Food Labeling Management App UniApp/src/utils/print/bluetoothTool.js
1 /** 1 /**
2 - * 经典蓝牙工具(仅 Android APP-PLUS)  
3 - * 用于佳博 D320FAX 等一体机的 Virtual BT Printer 连接  
4 - * 支持 RFCOMM 回退连接(部分设备需要) 2 + * 经典蓝牙工具(Android APP-PLUS)
  3 + * 兼容官方 UniApp SDK 经典蓝牙实现,并补充当前项目需要的简化调用:
  4 + * - getPairedDevices()
  5 + * - startClassicDiscovery(onDeviceFound, onDiscoveryFinished)
  6 + * - cancelClassicDiscovery()
  7 + * - connDevice(address, callback)
  8 + * - disConnDevice()
  9 + * - sendByteData(byteData)
5 */ 10 */
  11 +
6 // #ifdef APP-PLUS 12 // #ifdef APP-PLUS
7 -function getAdapter () {  
8 - if (typeof plus === 'undefined' || !plus.android) return null  
9 - const BluetoothAdapter = plus.android.importClass('android.bluetooth.BluetoothAdapter')  
10 - return BluetoothAdapter.getDefaultAdapter()  
11 -} 13 +let BluetoothAdapter = plus.android.importClass('android.bluetooth.BluetoothAdapter')
  14 +let Intent = plus.android.importClass('android.content.Intent')
  15 +let IntentFilter = plus.android.importClass('android.content.IntentFilter')
  16 +let BluetoothDevice = plus.android.importClass('android.bluetooth.BluetoothDevice')
  17 +let UUID = plus.android.importClass('java.util.UUID')
  18 +let Toast = plus.android.importClass('android.widget.Toast')
  19 +let MY_UUID = UUID.fromString('00001101-0000-1000-8000-00805F9B34FB')
  20 +
  21 +let invoke = plus.android.invoke
  22 +let btAdapter = BluetoothAdapter.getDefaultAdapter()
  23 +let activity = plus.android.runtimeMainActivity()
  24 +
  25 +let btSocket = null
  26 +let btInStream = null
  27 +let btOutStream = null
  28 +let setIntervalId = 0
  29 +
  30 +let btFindReceiver = null
  31 +let btStatusReceiver = null
  32 +// #endif
12 33
13 -function getActivity () {  
14 - return typeof plus !== 'undefined' ? plus.android.runtimeMainActivity() : null 34 +function normalizeDeviceType (deviceType) {
  35 + if (deviceType === 1) return 'classic'
  36 + if (deviceType === 2) return 'ble'
  37 + if (deviceType === 3) return 'dual'
  38 + return 'unknown'
15 } 39 }
16 40
17 -function showToast (msg) { 41 +function fallbackRfcommSocket (device, insecure = false) {
18 try { 42 try {
19 - const activity = getActivity()  
20 - if (activity) {  
21 - const Toast = plus.android.importClass('android.widget.Toast')  
22 - Toast.makeText(activity, String(msg), Toast.LENGTH_SHORT).show()  
23 - }  
24 - } catch (_) {} 43 + const cls = invoke(device, 'getClass')
  44 + const intClass = plus.android.importClass('java.lang.Integer').TYPE
  45 + const methodName = insecure ? 'createInsecureRfcommSocket' : 'createRfcommSocket'
  46 + const method = invoke(cls, 'getMethod', methodName, intClass)
  47 + return invoke(method, 'invoke', device, 1)
  48 + } catch (e) {
  49 + console.error('RFCOMM fallback failed:', e)
  50 + return null
  51 + }
25 } 52 }
26 53
27 -function getInvoke () {  
28 - return typeof plus !== 'undefined' && plus.android ? plus.android.invoke : null 54 +function getErrorMessage (error) {
  55 + if (!error) return 'Unknown error'
  56 + if (typeof error === 'string') return error
  57 + return String(error.message || error.errMsg || error)
29 } 58 }
30 -function getMyUuid () {  
31 - if (typeof plus === 'undefined' || !plus.android) return null  
32 - return plus.android.importClass('java.util.UUID').fromString('00001101-0000-1000-8000-00805F9B34FB') 59 +
  60 +function normalizeWriteByte (value) {
  61 + const byte = Number(value) || 0
  62 + return byte & 0xff
  63 +}
  64 +
  65 +function normalizeWriteChunk (byteData, start, end) {
  66 + const out = []
  67 + 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 + }
  75 + return out
  76 +}
  77 +
  78 +function createSocketCandidates (device) {
  79 + return [
  80 + {
  81 + name: 'secure-service-record',
  82 + create: () => invoke(device, 'createRfcommSocketToServiceRecord', MY_UUID),
  83 + },
  84 + {
  85 + name: 'insecure-service-record',
  86 + create: () => invoke(device, 'createInsecureRfcommSocketToServiceRecord', MY_UUID),
  87 + },
  88 + {
  89 + name: 'secure-channel-1',
  90 + create: () => fallbackRfcommSocket(device, false),
  91 + },
  92 + {
  93 + name: 'insecure-channel-1',
  94 + create: () => fallbackRfcommSocket(device, true),
  95 + },
  96 + ]
33 } 97 }
34 -let btSocket = null  
35 -let btInStream = null  
36 -let btOutStream = null  
37 -let btFindReceiver = null  
38 -// #endif  
39 98
40 var blueToothTool = { 99 var blueToothTool = {
41 - state: { bluetoothEnable: false, readThreadState: false }, 100 + state: {
  101 + bluetoothEnable: false,
  102 + bluetoothState: '',
  103 + discoveryDeviceState: false,
  104 + readThreadState: false,
  105 + connectionState: 'idle',
  106 + lastAddress: '',
  107 + lastSocketStrategy: '',
  108 + lastError: '',
  109 + lastSendError: '',
  110 + outputReady: false,
  111 + lastSendMode: 'idle',
  112 + },
  113 + options: {
  114 + listenBTStatusCallback: function (state) {},
  115 + discoveryDeviceCallback: function (newDevice) {},
  116 + discoveryFinishedCallback: function () {},
  117 + readDataCallback: function (dataByteArr) {},
  118 + connExceptionCallback: function (e) {},
  119 + },
  120 + init (setOptions) {
  121 + Object.assign(this.options, setOptions || {})
  122 + this.state.bluetoothEnable = this.getBluetoothStatus()
  123 + this.listenBluetoothStatus()
  124 + },
  125 + setErrorState (message, type = 'general') {
  126 + const text = String(message || '')
  127 + this.state.lastError = text
  128 + if (type === 'send') {
  129 + this.state.lastSendError = text
  130 + }
  131 + },
  132 + clearErrorState () {
  133 + this.state.lastError = ''
  134 + this.state.lastSendError = ''
  135 + },
42 shortToast (msg) { 136 shortToast (msg) {
43 // #ifdef APP-PLUS 137 // #ifdef APP-PLUS
44 - showToast(msg) 138 + try {
  139 + if (activity) Toast.makeText(activity, String(msg), Toast.LENGTH_SHORT).show()
  140 + } catch (_) {}
45 // #endif 141 // #endif
46 }, 142 },
  143 + isSupportBluetooth () {
  144 + // #ifdef APP-PLUS
  145 + return btAdapter != null
  146 + // #endif
  147 + return false
  148 + },
47 getBluetoothStatus () { 149 getBluetoothStatus () {
48 // #ifdef APP-PLUS 150 // #ifdef APP-PLUS
49 - const btAdapter = getAdapter()  
50 return btAdapter != null && btAdapter.isEnabled() 151 return btAdapter != null && btAdapter.isEnabled()
51 // #endif 152 // #endif
52 return false 153 return false
53 }, 154 },
54 - /** 获取已配对设备(含 Virtual BT Printer / D320FAX) */ 155 + turnOnBluetooth () {
  156 + // #ifdef APP-PLUS
  157 + if (btAdapter == null) {
  158 + this.shortToast('Bluetooth not available')
  159 + return
  160 + }
  161 + if (!btAdapter.isEnabled()) {
  162 + if (activity == null) {
  163 + this.shortToast('Activity not available')
  164 + return
  165 + }
  166 + let intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
  167 + activity.startActivityForResult(intent, 1)
  168 + return
  169 + }
  170 + this.shortToast('Bluetooth already enabled')
  171 + // #endif
  172 + },
  173 + turnOffBluetooth () {
  174 + // #ifdef APP-PLUS
  175 + if (btAdapter != null && btAdapter.isEnabled()) {
  176 + btAdapter.disable()
  177 + }
  178 + if (btFindReceiver != null) {
  179 + try { activity.unregisterReceiver(btFindReceiver) } catch (_) {}
  180 + btFindReceiver = null
  181 + }
  182 + this.state.bluetoothEnable = false
  183 + this.cancelDiscovery()
  184 + this.closeBtSocket()
  185 + // #endif
  186 + },
55 getPairedDevices () { 187 getPairedDevices () {
56 // #ifdef APP-PLUS 188 // #ifdef APP-PLUS
57 - const pairedDevices = [] 189 + let pairedDevices = []
58 try { 190 try {
59 - const btAdapter = getAdapter()  
60 - if (!btAdapter || !btAdapter.isEnabled()) {  
61 - this.shortToast('Bluetooth is off')  
62 - return pairedDevices  
63 - }  
64 - const bonded = btAdapter.getBondedDevices()  
65 - if (!bonded) return pairedDevices  
66 - const inv = getInvoke()  
67 - if (!inv) return pairedDevices  
68 - const it = inv(bonded, 'iterator')  
69 - while (inv(it, 'hasNext')) {  
70 - const device = inv(it, 'next')  
71 - const deviceType = inv(device, 'getType')  
72 - const deviceId = inv(device, 'getAddress')  
73 - let deviceName = inv(device, 'getName')  
74 - if (deviceName == null) deviceName = ''  
75 - deviceName = String(deviceName).trim() || 'Unknown Device'  
76 - let typeStr = 'unknown'  
77 - if (deviceType === 1) typeStr = 'classic'  
78 - else if (deviceType === 2) typeStr = 'ble'  
79 - else if (deviceType === 3) typeStr = 'dual'  
80 - pairedDevices.push({ name: deviceName, deviceId: String(deviceId), type: typeStr }) 191 + if (btAdapter == null || !btAdapter.isEnabled()) return pairedDevices
  192 + let pairedDevicesAndroid = btAdapter.getBondedDevices()
  193 + if (!pairedDevicesAndroid) return pairedDevices
  194 + let it = invoke(pairedDevicesAndroid, 'iterator')
  195 + while (invoke(it, 'hasNext')) {
  196 + let device = invoke(it, 'next')
  197 + let deviceType = invoke(device, 'getType')
  198 + let deviceId = invoke(device, 'getAddress')
  199 + let deviceName = invoke(device, 'getName')
  200 + pairedDevices.push({
  201 + name: deviceName != null ? String(deviceName).trim() || 'Unknown Device' : 'Unknown Device',
  202 + deviceId: String(deviceId || ''),
  203 + type: normalizeDeviceType(deviceType),
  204 + })
81 } 205 }
82 } catch (e) { 206 } catch (e) {
83 console.error('getPairedDevices error:', e) 207 console.error('getPairedDevices error:', e)
@@ -86,156 +210,360 @@ var blueToothTool = { @@ -86,156 +210,360 @@ var blueToothTool = {
86 // #endif 210 // #endif
87 return [] 211 return []
88 }, 212 },
89 - /** 经典蓝牙扫描(发现未配对设备,如 d320fax_295c)— 不过滤任何设备 */  
90 - startClassicDiscovery (onDeviceFound, onDiscoveryFinished) { 213 + discoveryNewDevice () {
91 // #ifdef APP-PLUS 214 // #ifdef APP-PLUS
92 - const btAdapter = getAdapter()  
93 - const activity = getActivity()  
94 - if (!btAdapter || !btAdapter.isEnabled() || !activity) return  
95 - if (btFindReceiver) {  
96 - try { activity.unregisterReceiver(btFindReceiver) } catch (_) {} 215 + if (btAdapter == null || !btAdapter.isEnabled() || activity == null) return
  216 + if (btFindReceiver != null) {
  217 + try { activity.unregisterReceiver(btFindReceiver) } catch (e) { console.error(e) }
97 btFindReceiver = null 218 btFindReceiver = null
  219 + this.cancelDiscovery()
98 } 220 }
99 - if (btAdapter.isDiscovering()) btAdapter.cancelDiscovery()  
100 - const BluetoothAdapter = plus.android.importClass('android.bluetooth.BluetoothAdapter')  
101 - const BluetoothDevice = plus.android.importClass('android.bluetooth.BluetoothDevice')  
102 - const IntentFilter = plus.android.importClass('android.content.IntentFilter')  
103 - const inv = getInvoke() 221 + let options = this.options
104 btFindReceiver = plus.android.implements('io.dcloud.android.content.BroadcastReceiver', { 222 btFindReceiver = plus.android.implements('io.dcloud.android.content.BroadcastReceiver', {
105 - onReceive (context, intent) {  
106 - const action = intent.getAction() 223 + onReceive: function (context, intent) {
  224 + plus.android.importClass(context)
  225 + plus.android.importClass(intent)
  226 + let action = intent.getAction()
107 if (BluetoothDevice.ACTION_FOUND === action) { 227 if (BluetoothDevice.ACTION_FOUND === action) {
108 - const device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) 228 + let device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
109 if (!device) return 229 if (!device) return
110 - const deviceType = inv(device, 'getType')  
111 - const deviceId = String(inv(device, 'getAddress') || '')  
112 - let deviceName = inv(device, 'getName')  
113 - if (deviceName == null) deviceName = ''  
114 - deviceName = String(deviceName).trim() || 'Unknown Device'  
115 - let typeStr = 'unknown'  
116 - if (deviceType === 1) typeStr = 'classic'  
117 - else if (deviceType === 2) typeStr = 'ble'  
118 - else if (deviceType === 3) typeStr = 'dual'  
119 - if (onDeviceFound) onDeviceFound({ name: deviceName, deviceId, type: typeStr }) 230 + let deviceType = invoke(device, 'getType')
  231 + let deviceId = invoke(device, 'getAddress')
  232 + let deviceName = invoke(device, 'getName')
  233 + let newDevice = {
  234 + name: deviceName != null ? String(deviceName).trim() || 'Unknown Device' : 'Unknown Device',
  235 + deviceId: String(deviceId || ''),
  236 + type: normalizeDeviceType(deviceType),
  237 + }
  238 + options.discoveryDeviceCallback && options.discoveryDeviceCallback(newDevice)
120 } 239 }
121 if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED === action) { 240 if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED === action) {
122 - try { activity.unregisterReceiver(btFindReceiver) } catch (_) {}  
123 - btFindReceiver = null  
124 - if (onDiscoveryFinished) onDiscoveryFinished() 241 + blueToothTool.cancelDiscovery()
  242 + options.discoveryFinishedCallback && options.discoveryFinishedCallback()
125 } 243 }
126 }, 244 },
127 }) 245 })
128 - const filter = new IntentFilter() 246 + let filter = new IntentFilter()
129 filter.addAction(BluetoothDevice.ACTION_FOUND) 247 filter.addAction(BluetoothDevice.ACTION_FOUND)
130 filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED) 248 filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)
131 activity.registerReceiver(btFindReceiver, filter) 249 activity.registerReceiver(btFindReceiver, filter)
132 btAdapter.startDiscovery() 250 btAdapter.startDiscovery()
  251 + this.state.discoveryDeviceState = true
133 // #endif 252 // #endif
134 }, 253 },
135 - /** 停止经典蓝牙扫描 */  
136 - cancelClassicDiscovery () { 254 + startClassicDiscovery (onDeviceFound, onDiscoveryFinished) {
  255 + this.init({
  256 + discoveryDeviceCallback: onDeviceFound || function () {},
  257 + discoveryFinishedCallback: onDiscoveryFinished || function () {},
  258 + })
  259 + this.discoveryNewDevice()
  260 + },
  261 + listenBluetoothStatus () {
137 // #ifdef APP-PLUS 262 // #ifdef APP-PLUS
138 - const btAdapter = getAdapter()  
139 - const activity = getActivity()  
140 - if (btAdapter && btAdapter.isDiscovering()) btAdapter.cancelDiscovery()  
141 - if (btFindReceiver && activity) {  
142 - try { activity.unregisterReceiver(btFindReceiver) } catch (_) {}  
143 - btFindReceiver = null 263 + if (activity == null) return
  264 + if (btStatusReceiver != null) {
  265 + try { activity.unregisterReceiver(btStatusReceiver) } catch (e) { console.error(e) }
  266 + btStatusReceiver = null
  267 + }
  268 + btStatusReceiver = plus.android.implements('io.dcloud.android.content.BroadcastReceiver', {
  269 + onReceive: (context, intent) => {
  270 + plus.android.importClass(context)
  271 + plus.android.importClass(intent)
  272 + let action = intent.getAction()
  273 + if (action === BluetoothAdapter.ACTION_STATE_CHANGED) {
  274 + let blueState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, 0)
  275 + let stateStr = ''
  276 + switch (blueState) {
  277 + case BluetoothAdapter.STATE_TURNING_ON:
  278 + stateStr = 'STATE_TURNING_ON'
  279 + break
  280 + case BluetoothAdapter.STATE_ON:
  281 + stateStr = 'STATE_ON'
  282 + this.state.bluetoothEnable = true
  283 + break
  284 + case BluetoothAdapter.STATE_TURNING_OFF:
  285 + stateStr = 'STATE_TURNING_OFF'
  286 + break
  287 + case BluetoothAdapter.STATE_OFF:
  288 + stateStr = 'STATE_OFF'
  289 + this.state.bluetoothEnable = false
  290 + break
  291 + }
  292 + this.state.bluetoothState = stateStr
  293 + this.options.listenBTStatusCallback && this.options.listenBTStatusCallback(stateStr)
  294 + }
  295 + },
  296 + })
  297 + let filter = new IntentFilter()
  298 + filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED)
  299 + activity.registerReceiver(btStatusReceiver, filter)
  300 + if (this.state.bluetoothEnable) {
  301 + this.options.listenBTStatusCallback && this.options.listenBTStatusCallback('STATE_ON')
144 } 302 }
145 // #endif 303 // #endif
146 }, 304 },
147 - /** 连接经典蓝牙设备(含 D320FAX Virtual BT Printer,支持 RFCOMM 回退) */  
148 connDevice (address, callback) { 305 connDevice (address, callback) {
149 // #ifdef APP-PLUS 306 // #ifdef APP-PLUS
  307 + this.cancelDiscovery()
150 if (btSocket != null) this.closeBtSocket() 308 if (btSocket != null) this.closeBtSocket()
151 this.state.readThreadState = false 309 this.state.readThreadState = false
152 - const btAdapter = getAdapter()  
153 - if (!btAdapter) { 310 + this.state.connectionState = 'connecting'
  311 + this.state.lastAddress = String(address || '')
  312 + this.state.outputReady = false
  313 + this.clearErrorState()
  314 +
  315 + if (btAdapter == null) {
154 this.shortToast('Bluetooth not available') 316 this.shortToast('Bluetooth not available')
155 - if (callback) callback(false)  
156 - return false  
157 - }  
158 - const inv = getInvoke()  
159 - const uuid = getMyUuid()  
160 - if (!inv || !uuid) {  
161 - this.shortToast('Bluetooth not ready')  
162 - if (callback) callback(false) 317 + this.state.connectionState = 'error'
  318 + this.setErrorState('Bluetooth not available')
  319 + callback && callback(false)
163 return false 320 return false
164 } 321 }
165 try { 322 try {
166 - const device = inv(btAdapter, 'getRemoteDevice', address)  
167 - btSocket = inv(device, 'createRfcommSocketToServiceRecord', uuid)  
168 - } catch (e) {  
169 - console.warn('createRfcommSocketToServiceRecord failed, try fallback:', e)  
170 - try {  
171 - const device = inv(btAdapter, 'getRemoteDevice', address)  
172 - const cls = inv(device, 'getClass')  
173 - const intClass = plus.android.importClass('java.lang.Integer').TYPE  
174 - const m = inv(cls, 'getMethod', 'createRfcommSocket', intClass)  
175 - btSocket = inv(m, 'invoke', device, 1)  
176 - } catch (e2) {  
177 - console.error('RFCOMM fallback failed:', e2)  
178 - this.shortToast('Connect failed')  
179 - if (callback) callback(false) 323 + let device = invoke(btAdapter, 'getRemoteDevice', address)
  324 + const candidates = createSocketCandidates(device)
  325 + let socket = null
  326 + let lastError = null
  327 + for (let i = 0; i < candidates.length; i++) {
  328 + const candidate = candidates[i]
  329 + try {
  330 + socket = candidate.create()
  331 + if (socket) {
  332 + this.state.lastSocketStrategy = candidate.name
  333 + break
  334 + }
  335 + } catch (e) {
  336 + lastError = e
  337 + console.warn('create socket failed:', candidate.name, e)
  338 + }
  339 + }
  340 + btSocket = socket
  341 + if (!btSocket) {
  342 + this.state.connectionState = 'error'
  343 + this.setErrorState(lastError ? getErrorMessage(lastError) : 'Unable to create Bluetooth socket')
  344 + callback && callback(false)
180 return false 345 return false
181 } 346 }
  347 + } catch (e) {
  348 + console.error(e)
  349 + this.state.connectionState = 'error'
  350 + this.setErrorState(getErrorMessage(e))
  351 + callback && callback(false)
  352 + return false
182 } 353 }
  354 +
183 try { 355 try {
184 - inv(btSocket, 'connect')  
185 - btInStream = inv(btSocket, 'getInputStream')  
186 - btOutStream = inv(btSocket, 'getOutputStream')  
187 - this.state.readThreadState = true  
188 - this.shortToast('Connected')  
189 - if (callback) callback(true) 356 + invoke(btSocket, 'connect')
  357 + const streamReady = this.readData()
  358 + if (!streamReady) {
  359 + throw new Error(this.state.lastError || 'Bluetooth output stream not ready')
  360 + }
  361 + this.state.connectionState = 'connected'
  362 + this.shortToast('Classic Bluetooth connected')
  363 + callback && callback(true)
190 } catch (e) { 364 } catch (e) {
191 - try { btSocket.close() } catch (_) {}  
192 - btSocket = null  
193 - this.shortToast('Connect failed')  
194 - if (callback) callback(false) 365 + console.error(e)
  366 + this.state.connectionState = 'error'
  367 + this.setErrorState(getErrorMessage(e))
  368 + callback && callback(false)
  369 + try {
  370 + btSocket.close()
  371 + btSocket = null
  372 + } catch (e1) {
  373 + console.error(e1)
  374 + }
  375 + btInStream = null
  376 + btOutStream = null
195 return false 377 return false
196 } 378 }
197 return true 379 return true
198 // #endif 380 // #endif
199 - if (callback) callback(false) 381 + callback && callback(false)
200 return false 382 return false
201 }, 383 },
202 disConnDevice () { 384 disConnDevice () {
203 // #ifdef APP-PLUS 385 // #ifdef APP-PLUS
204 - this.closeBtSocket() 386 + if (btSocket != null) this.closeBtSocket()
205 this.state.readThreadState = false 387 this.state.readThreadState = false
206 - this.shortToast('Disconnected')  
207 // #endif 388 // #endif
208 }, 389 },
209 closeBtSocket () { 390 closeBtSocket () {
210 // #ifdef APP-PLUS 391 // #ifdef APP-PLUS
211 this.state.readThreadState = false 392 this.state.readThreadState = false
212 - if (btSocket) {  
213 - try { btSocket.close() } catch (_) {}  
214 - btSocket = null  
215 - btInStream = null  
216 - btOutStream = null 393 + this.state.outputReady = false
  394 + this.state.connectionState = 'idle'
  395 + clearInterval(setIntervalId)
  396 + setIntervalId = 0
  397 + if (!btSocket) return
  398 + try {
  399 + btSocket.close()
  400 + } catch (e) {
  401 + console.error(e)
217 } 402 }
  403 + btSocket = null
  404 + btInStream = null
  405 + btOutStream = null
218 // #endif 406 // #endif
219 }, 407 },
220 - sendByteData (byteData) { 408 + cancelDiscovery () {
  409 + // #ifdef APP-PLUS
  410 + if (btAdapter != null && btAdapter.isDiscovering()) {
  411 + btAdapter.cancelDiscovery()
  412 + }
  413 + if (btFindReceiver != null && activity != null) {
  414 + try { activity.unregisterReceiver(btFindReceiver) } catch (_) {}
  415 + btFindReceiver = null
  416 + }
  417 + this.state.discoveryDeviceState = false
  418 + // #endif
  419 + },
  420 + cancelClassicDiscovery () {
  421 + this.cancelDiscovery()
  422 + },
  423 + readData () {
  424 + // #ifdef APP-PLUS
  425 + if (!btSocket) {
  426 + this.shortToast('Please connect Bluetooth device first.')
  427 + this.state.connectionState = 'error'
  428 + this.setErrorState('Please connect Bluetooth device first.')
  429 + return false
  430 + }
  431 + try {
  432 + btInStream = invoke(btSocket, 'getInputStream')
  433 + btOutStream = invoke(btSocket, 'getOutputStream')
  434 + } catch (e) {
  435 + console.error(e)
  436 + this.setErrorState(getErrorMessage(e))
  437 + this.closeBtSocket()
  438 + return false
  439 + }
  440 + this.read()
  441 + this.state.readThreadState = true
  442 + this.state.outputReady = !!btOutStream
  443 + return true
  444 + // #endif
  445 + return false
  446 + },
  447 + read () {
  448 + // #ifdef APP-PLUS
  449 + clearInterval(setIntervalId)
  450 + setIntervalId = setInterval(() => {
  451 + if (this.state.readThreadState) {
  452 + let start = new Date().getTime()
  453 + let dataArr = []
  454 + try {
  455 + while (btInStream && invoke(btInStream, 'available') !== 0) {
  456 + let data = invoke(btInStream, 'read')
  457 + dataArr.push(data)
  458 + let current = new Date().getTime()
  459 + if (current - start > 20) break
  460 + }
  461 + } catch (e) {
  462 + this.state.readThreadState = false
  463 + this.state.connectionState = 'error'
  464 + this.setErrorState(getErrorMessage(e))
  465 + this.options.connExceptionCallback && this.options.connExceptionCallback(e)
  466 + }
  467 + if (dataArr.length > 0) {
  468 + this.options.readDataCallback && this.options.readDataCallback(dataArr)
  469 + }
  470 + }
  471 + }, 40)
  472 + // #endif
  473 + },
  474 + sendData (dataStr) {
221 // #ifdef APP-PLUS 475 // #ifdef APP-PLUS
222 if (!btOutStream) { 476 if (!btOutStream) {
  477 + this.shortToast('Output stream not ready')
  478 + return false
  479 + }
  480 + let bytes = invoke(dataStr, 'getBytes', 'gb18030')
  481 + try {
  482 + this.sendByteData(bytes)
  483 + } catch (e) {
  484 + return false
  485 + }
  486 + return true
  487 + // #endif
  488 + return false
  489 + },
  490 + sendByteData (byteData) {
  491 + // #ifdef APP-PLUS
  492 + if (!this.ensureConnection(this.state.lastAddress)) {
223 this.shortToast('Not connected') 493 this.shortToast('Not connected')
224 return false 494 return false
225 } 495 }
226 try { 496 try {
227 - const CHUNK_SIZE = 4096 497 + const CHUNK_SIZE = 512
  498 + this.state.lastSendMode = 'chunk-write'
  499 + this.state.lastSendError = ''
228 for (let i = 0; i < byteData.length; i += CHUNK_SIZE) { 500 for (let i = 0; i < byteData.length; i += CHUNK_SIZE) {
229 - const chunk = byteData.slice(i, i + CHUNK_SIZE)  
230 - btOutStream.write(chunk) 501 + const chunk = normalizeWriteChunk(byteData, i, Math.min(i + CHUNK_SIZE, byteData.length))
  502 + try {
  503 + btOutStream.write(chunk)
  504 + } catch (writeChunkError) {
  505 + this.state.lastSendMode = 'byte-write-fallback'
  506 + for (let j = 0; j < chunk.length; j++) {
  507 + invoke(btOutStream, 'write', normalizeWriteByte(chunk[j]))
  508 + }
  509 + }
231 } 510 }
  511 + invoke(btOutStream, 'flush')
232 return true 512 return true
233 } catch (e) { 513 } catch (e) {
  514 + const message = getErrorMessage(e)
  515 + console.error('sendByteData failed:', e)
  516 + this.setErrorState(message, 'send')
  517 + this.state.connectionState = 'error'
234 return false 518 return false
235 } 519 }
236 // #endif 520 // #endif
237 return false 521 return false
238 - } 522 + },
  523 + isSocketConnected () {
  524 + // #ifdef APP-PLUS
  525 + if (!btSocket) return false
  526 + try {
  527 + return !!invoke(btSocket, 'isConnected')
  528 + } catch (_) {
  529 + return false
  530 + }
  531 + // #endif
  532 + return false
  533 + },
  534 + ensureConnection (address) {
  535 + // #ifdef APP-PLUS
  536 + const targetAddress = String(address || this.state.lastAddress || '')
  537 + if (targetAddress) {
  538 + this.state.lastAddress = targetAddress
  539 + }
  540 + if (btOutStream && this.isSocketConnected()) {
  541 + this.state.outputReady = true
  542 + return true
  543 + }
  544 + if (!targetAddress) {
  545 + this.setErrorState('Bluetooth address missing')
  546 + return false
  547 + }
  548 + let connected = false
  549 + this.connDevice(targetAddress, (ok) => {
  550 + connected = !!ok
  551 + })
  552 + return connected && !!btOutStream
  553 + // #endif
  554 + return false
  555 + },
  556 + getLastError () {
  557 + return this.state.lastSendError || this.state.lastError || ''
  558 + },
  559 + getDebugState () {
  560 + return {
  561 + ...this.state,
  562 + socketConnected: this.isSocketConnected(),
  563 + outputReady: !!btOutStream,
  564 + inputReady: !!btInStream,
  565 + }
  566 + },
239 } 567 }
240 568
241 export default blueToothTool 569 export default blueToothTool
美国版/Food Labeling Management App UniApp/src/utils/print/drivers/d320fax.ts 0 → 100644
  1 +import { buildTscLabelData, buildTscTestPrintData } from '../protocols/tscProtocol'
  2 +import type { PrinterCandidate, PrinterDriver } from '../types/printer'
  3 +
  4 +const KEYWORDS = [
  5 + 'd320fax',
  6 + 'd320fx',
  7 + 'virtual bt printer',
  8 + 'gp-d320fax',
  9 +]
  10 +
  11 +function score (device: PrinterCandidate): number {
  12 + const text = `${device.name || ''} ${device.deviceId || ''}`.toLowerCase()
  13 + let total = 0
  14 + KEYWORDS.forEach((keyword) => {
  15 + if (text.includes(keyword)) total += 40
  16 + })
  17 + return total
  18 +}
  19 +
  20 +export const d320faxDriver: PrinterDriver = {
  21 + key: 'd320fax',
  22 + brand: 'Gprinter',
  23 + model: 'D320FAX/D320FX',
  24 + displayName: 'Gprinter D320FAX',
  25 + protocol: 'tsc',
  26 + preferredConnection: 'classic',
  27 + preferredBleMtu: 20,
  28 + keywords: KEYWORDS,
  29 + matches (device) {
  30 + return score(device)
  31 + },
  32 + resolveConnectionType () {
  33 + return 'classic'
  34 + },
  35 + buildTestPrintData () {
  36 + return buildTscTestPrintData()
  37 + },
  38 + buildLabelData (payload) {
  39 + return buildTscLabelData(payload)
  40 + },
  41 +}
美国版/Food Labeling Management App UniApp/src/utils/print/drivers/genericTsc.ts 0 → 100644
  1 +import { buildTscLabelData, buildTscTestPrintData } from '../protocols/tscProtocol'
  2 +import type { PrinterCandidate, PrinterDriver } from '../types/printer'
  3 +
  4 +const GENERIC_TSC_KEYWORDS = [
  5 + 'printer', 'print', 'label', 'tsc', 'zebra', 'brother', 'epson', 'godex',
  6 + 'citizen', 'ql-', 'zd', 'zt', 'ttp', 'tdp', 'bt printer', 'virtual bt printer',
  7 +]
  8 +
  9 +function scoreByKeywords (device: PrinterCandidate, keywords: string[]): number {
  10 + const text = `${device.name || ''} ${device.deviceId || ''}`.toLowerCase()
  11 + let score = 0
  12 + keywords.forEach((keyword) => {
  13 + if (text.includes(keyword)) score += 10
  14 + })
  15 + return score
  16 +}
  17 +
  18 +export const genericTscDriver: PrinterDriver = {
  19 + key: 'generic-tsc',
  20 + brand: 'Generic',
  21 + model: 'TSC',
  22 + displayName: 'Generic TSC Printer',
  23 + protocol: 'tsc',
  24 + preferredBleMtu: 20,
  25 + keywords: GENERIC_TSC_KEYWORDS,
  26 + matches (device) {
  27 + return scoreByKeywords(device, GENERIC_TSC_KEYWORDS)
  28 + },
  29 + resolveConnectionType (device) {
  30 + return device.type === 'ble' ? 'ble' : 'classic'
  31 + },
  32 + buildTestPrintData () {
  33 + return buildTscTestPrintData()
  34 + },
  35 + buildLabelData (payload) {
  36 + return buildTscLabelData(payload)
  37 + },
  38 +}
美国版/Food Labeling Management App UniApp/src/utils/print/drivers/gpR3.ts 0 → 100644
  1 +import { buildEscPosLabelData, buildEscPosTestPrintData } from '../protocols/escPosBuilder'
  2 +import type { PrinterCandidate, PrinterDriver } from '../types/printer'
  3 +
  4 +const KEYWORDS = [
  5 + 'gp-r3',
  6 + 'gp r3',
  7 + 'gpr3',
  8 +]
  9 +
  10 +function score (device: PrinterCandidate): number {
  11 + const text = `${device.name || ''} ${device.deviceId || ''}`.toLowerCase()
  12 + let total = 0
  13 + KEYWORDS.forEach((keyword) => {
  14 + if (text.includes(keyword)) total += 60
  15 + })
  16 + if (text.includes('gprinter')) total += 10
  17 + return total
  18 +}
  19 +
  20 +export const gpR3Driver: PrinterDriver = {
  21 + key: 'gp-r3',
  22 + brand: 'Gprinter',
  23 + model: 'GP-R3',
  24 + displayName: 'Gprinter GP-R3',
  25 + protocol: 'esc',
  26 + preferredConnection: 'classic',
  27 + preferredBleMtu: 20,
  28 + keywords: KEYWORDS,
  29 + matches (device) {
  30 + return score(device)
  31 + },
  32 + resolveConnectionType (device) {
  33 + if (device.type === 'ble') return 'ble'
  34 + return 'classic'
  35 + },
  36 + buildTestPrintData () {
  37 + return buildEscPosTestPrintData()
  38 + },
  39 + buildLabelData (payload) {
  40 + return buildEscPosLabelData(payload)
  41 + },
  42 +}
美国版/Food Labeling Management App UniApp/src/utils/print/manager/driverRegistry.ts 0 → 100644
  1 +import { d320faxDriver } from '../drivers/d320fax'
  2 +import { genericTscDriver } from '../drivers/genericTsc'
  3 +import { gpR3Driver } from '../drivers/gpR3'
  4 +import type { PrinterCandidate, PrinterDriver, ResolvedPrinterCandidate } from '../types/printer'
  5 +
  6 +const printerDrivers: PrinterDriver[] = [
  7 + gpR3Driver,
  8 + d320faxDriver,
  9 + genericTscDriver,
  10 +]
  11 +
  12 +export function getPrinterDrivers (): PrinterDriver[] {
  13 + return printerDrivers
  14 +}
  15 +
  16 +export function getPrinterDriverByKey (key?: string): PrinterDriver {
  17 + return printerDrivers.find(driver => driver.key === key) || genericTscDriver
  18 +}
  19 +
  20 +export function resolvePrinterDriver (device: PrinterCandidate): PrinterDriver {
  21 + let bestDriver: PrinterDriver = genericTscDriver
  22 + let bestScore = -1
  23 + printerDrivers.forEach((driver) => {
  24 + const score = driver.matches(device)
  25 + if (score > bestScore) {
  26 + bestScore = score
  27 + bestDriver = driver
  28 + }
  29 + })
  30 + return bestDriver
  31 +}
  32 +
  33 +export function describePrinterCandidate (device: PrinterCandidate): ResolvedPrinterCandidate {
  34 + const driver = resolvePrinterDriver(device)
  35 + return {
  36 + ...device,
  37 + driverKey: driver.key,
  38 + driverName: driver.displayName,
  39 + protocol: driver.protocol,
  40 + resolvedType: driver.resolveConnectionType(device),
  41 + }
  42 +}
美国版/Food Labeling Management App UniApp/src/utils/print/manager/printerManager.ts 0 → 100644
  1 +import {
  2 + clearPrinter,
  3 + getBluetoothConnection,
  4 + getCurrentPrinterDriverKey,
  5 + getPrinterType,
  6 + sendToPrinter,
  7 + setBluetoothConnection,
  8 + setBuiltinPrinter,
  9 +} from '../printerConnection'
  10 +import classicBluetooth from '../bluetoothTool.js'
  11 +import { describePrinterCandidate, getPrinterDriverByKey, resolvePrinterDriver } from './driverRegistry'
  12 +import type {
  13 + CurrentPrinterSummary,
  14 + LabelPrintPayload,
  15 + PrinterCandidate,
  16 + PrinterDriver,
  17 +} from '../types/printer'
  18 +
  19 +function connectClassicBluetooth (device: PrinterCandidate, driver: PrinterDriver): Promise<void> {
  20 + return new Promise((resolve, reject) => {
  21 + // #ifdef APP-PLUS
  22 + const classic = classicBluetooth
  23 + if (!classic || !classic.connDevice) {
  24 + reject(new Error('Classic Bluetooth not available. Ensure app is running on the device.'))
  25 + return
  26 + }
  27 + classic.connDevice(device.deviceId, (ok: boolean) => {
  28 + if (!ok) {
  29 + reject(new Error('Classic Bluetooth connection failed.'))
  30 + return
  31 + }
  32 + setBluetoothConnection({
  33 + deviceId: device.deviceId,
  34 + deviceName: device.name || 'Bluetooth Printer',
  35 + deviceType: 'classic',
  36 + driverKey: driver.key,
  37 + mtu: driver.preferredBleMtu || 20,
  38 + })
  39 + resolve()
  40 + })
  41 + // #endif
  42 + // #ifndef APP-PLUS
  43 + reject(new Error('Classic Bluetooth requires the app.'))
  44 + // #endif
  45 + })
  46 +}
  47 +
  48 +function findBleWriteCharacteristic (deviceId: string): Promise<{ serviceId: string; characteristicId: string } | null> {
  49 + return new Promise((resolve) => {
  50 + uni.getBLEDeviceServices({
  51 + deviceId,
  52 + success: (serviceRes) => {
  53 + const services = serviceRes.services || []
  54 + const next = (index: number) => {
  55 + if (index >= services.length) {
  56 + resolve(null)
  57 + return
  58 + }
  59 + const serviceId = services[index].uuid
  60 + uni.getBLEDeviceCharacteristics({
  61 + deviceId,
  62 + serviceId,
  63 + success: (charRes) => {
  64 + const target = (charRes.characteristics || []).find((item: any) => item.properties && item.properties.write)
  65 + if (target) {
  66 + resolve({
  67 + serviceId,
  68 + characteristicId: target.uuid,
  69 + })
  70 + return
  71 + }
  72 + next(index + 1)
  73 + },
  74 + fail: () => next(index + 1),
  75 + })
  76 + }
  77 + next(0)
  78 + },
  79 + fail: () => resolve(null),
  80 + })
  81 + })
  82 +}
  83 +
  84 +function connectBlePrinter (device: PrinterCandidate, driver: PrinterDriver): Promise<void> {
  85 + const finalizeExistingBleConnection = async () => {
  86 + const write = await findBleWriteCharacteristic(device.deviceId)
  87 + if (!write) {
  88 + throw new Error('No writable characteristic found. This device may not support printing.')
  89 + }
  90 + setBluetoothConnection({
  91 + deviceId: device.deviceId,
  92 + deviceName: device.name || 'Bluetooth Printer',
  93 + serviceId: write.serviceId,
  94 + characteristicId: write.characteristicId,
  95 + deviceType: 'ble',
  96 + mtu: driver.preferredBleMtu || 20,
  97 + driverKey: driver.key,
  98 + })
  99 + }
  100 +
  101 + return new Promise((resolve, reject) => {
  102 + uni.createBLEConnection({
  103 + deviceId: device.deviceId,
  104 + timeout: 10000,
  105 + success: async () => {
  106 + try {
  107 + await finalizeExistingBleConnection()
  108 + resolve()
  109 + } catch (e: any) {
  110 + reject(e)
  111 + }
  112 + },
  113 + fail: (err: any) => {
  114 + if (err?.errCode === -1) {
  115 + finalizeExistingBleConnection().then(() => resolve()).catch(reject)
  116 + } else {
  117 + reject(new Error(err?.errMsg || 'BLE connection failed.'))
  118 + }
  119 + },
  120 + })
  121 + })
  122 +}
  123 +
  124 +export async function connectBluetoothPrinter (device: PrinterCandidate): Promise<PrinterDriver> {
  125 + const driver = resolvePrinterDriver(device)
  126 + const resolvedType = driver.resolveConnectionType(device)
  127 + if (resolvedType === 'classic') {
  128 + await connectClassicBluetooth(device, driver)
  129 + } else {
  130 + await connectBlePrinter(device, driver)
  131 + }
  132 + return driver
  133 +}
  134 +
  135 +export function useBuiltinPrinter (driverKey = 'generic-tsc') {
  136 + setBuiltinPrinter(driverKey)
  137 +}
  138 +
  139 +export function getCurrentPrinterDriver (): PrinterDriver {
  140 + const type = getPrinterType()
  141 + const storedKey = getCurrentPrinterDriverKey()
  142 + if (storedKey) return getPrinterDriverByKey(storedKey)
  143 + if (type === 'bluetooth') {
  144 + const connection = getBluetoothConnection()
  145 + if (connection) {
  146 + return resolvePrinterDriver({
  147 + deviceId: connection.deviceId,
  148 + name: connection.deviceName,
  149 + type: connection.deviceType,
  150 + })
  151 + }
  152 + }
  153 + return getPrinterDriverByKey('generic-tsc')
  154 +}
  155 +
  156 +export function getCurrentPrinterSummary (): CurrentPrinterSummary {
  157 + const type = getPrinterType()
  158 + const driver = getCurrentPrinterDriver()
  159 + if (type === 'builtin') {
  160 + return {
  161 + type,
  162 + displayName: 'Built-in Printer',
  163 + deviceId: 'builtin',
  164 + driverKey: driver.key,
  165 + driverName: driver.displayName,
  166 + protocol: driver.protocol,
  167 + deviceType: '',
  168 + }
  169 + }
  170 + if (type === 'bluetooth') {
  171 + const connection = getBluetoothConnection()
  172 + if (connection) {
  173 + return {
  174 + type,
  175 + displayName: connection.deviceName || driver.displayName,
  176 + deviceId: connection.deviceId,
  177 + driverKey: driver.key,
  178 + driverName: driver.displayName,
  179 + protocol: driver.protocol,
  180 + deviceType: connection.deviceType,
  181 + }
  182 + }
  183 + }
  184 + return {
  185 + type: '',
  186 + displayName: '',
  187 + deviceId: '',
  188 + driverKey: driver.key,
  189 + driverName: driver.displayName,
  190 + protocol: driver.protocol,
  191 + deviceType: '',
  192 + }
  193 +}
  194 +
  195 +export async function testPrintCurrentPrinter (onProgress?: (percent: number) => void): Promise<PrinterDriver> {
  196 + const driver = getCurrentPrinterDriver()
  197 + await sendToPrinter(driver.buildTestPrintData(), onProgress)
  198 + return driver
  199 +}
  200 +
  201 +export async function printLabelForCurrentPrinter (
  202 + payload: LabelPrintPayload,
  203 + onProgress?: (percent: number) => void
  204 +): Promise<PrinterDriver> {
  205 + const driver = getCurrentPrinterDriver()
  206 + await sendToPrinter(driver.buildLabelData(payload), onProgress)
  207 + return driver
  208 +}
  209 +
  210 +export function describeDiscoveredPrinter (device: PrinterCandidate) {
  211 + return describePrinterCandidate(device)
  212 +}
  213 +
  214 +export function disconnectCurrentPrinter (): Promise<void> {
  215 + return new Promise((resolve) => {
  216 + const type = getPrinterType()
  217 + const connection = getBluetoothConnection()
  218 +
  219 + if (type === 'bluetooth' && connection?.deviceType === 'classic') {
  220 + // #ifdef APP-PLUS
  221 + try {
  222 + const classic = classicBluetooth
  223 + if (classic && classic.disConnDevice) classic.disConnDevice()
  224 + } catch (e) {
  225 + console.error('Disconnect classic bluetooth failed', e)
  226 + }
  227 + // #endif
  228 + clearPrinter()
  229 + resolve()
  230 + return
  231 + }
  232 +
  233 + clearPrinter()
  234 + if (type === 'bluetooth' && connection?.deviceId) {
  235 + uni.closeBLEConnection({
  236 + deviceId: connection.deviceId,
  237 + complete: () => resolve(),
  238 + })
  239 + return
  240 + }
  241 + resolve()
  242 + })
  243 +}
美国版/Food Labeling Management App UniApp/src/utils/print/printerConnection.ts
1 /** 1 /**
2 * 打印机连接与下发:蓝牙(BLE) / 一体机(TCP localhost) 2 * 打印机连接与下发:蓝牙(BLE) / 一体机(TCP localhost)
3 */ 3 */
  4 +import type { ActiveBtDeviceType, PrinterType } from './types/printer'
  5 +import classicBluetooth from './bluetoothTool.js'
4 6
5 const STORAGE_PRINTER_TYPE = 'printerType' 7 const STORAGE_PRINTER_TYPE = 'printerType'
6 const STORAGE_BT_DEVICE_ID = 'btDeviceId' 8 const STORAGE_BT_DEVICE_ID = 'btDeviceId'
@@ -9,9 +11,11 @@ const STORAGE_BT_SERVICE_ID = &#39;btServiceId&#39; @@ -9,9 +11,11 @@ const STORAGE_BT_SERVICE_ID = &#39;btServiceId&#39;
9 const STORAGE_BT_CHARACTERISTIC_ID = 'btCharacteristicId' 11 const STORAGE_BT_CHARACTERISTIC_ID = 'btCharacteristicId'
10 const STORAGE_BT_DEVICE_TYPE = 'btDeviceType' // 'ble' | 'classic' 12 const STORAGE_BT_DEVICE_TYPE = 'btDeviceType' // 'ble' | 'classic'
11 const STORAGE_BLE_MTU = 'bleMTU' 13 const STORAGE_BLE_MTU = 'bleMTU'
  14 +const STORAGE_BUILTIN_PORT = 'builtinPort'
  15 +const STORAGE_PRINTER_DRIVER_KEY = 'printerDriverKey'
12 16
13 -export type PrinterType = 'bluetooth' | 'builtin'  
14 -export type BtDeviceType = 'ble' | 'classic' 17 +const BUILTIN_PROBE_PORTS = [9100, 4000, 9000, 6000]
  18 +export type BtDeviceType = ActiveBtDeviceType
15 19
16 export const PrinterStorageKeys = { 20 export const PrinterStorageKeys = {
17 type: STORAGE_PRINTER_TYPE, 21 type: STORAGE_PRINTER_TYPE,
@@ -21,6 +25,7 @@ export const PrinterStorageKeys = { @@ -21,6 +25,7 @@ export const PrinterStorageKeys = {
21 btCharacteristicId: STORAGE_BT_CHARACTERISTIC_ID, 25 btCharacteristicId: STORAGE_BT_CHARACTERISTIC_ID,
22 btDeviceType: STORAGE_BT_DEVICE_TYPE, 26 btDeviceType: STORAGE_BT_DEVICE_TYPE,
23 bleMTU: STORAGE_BLE_MTU, 27 bleMTU: STORAGE_BLE_MTU,
  28 + driverKey: STORAGE_PRINTER_DRIVER_KEY,
24 } as const 29 } as const
25 30
26 export function setPrinterType (type: PrinterType) { 31 export function setPrinterType (type: PrinterType) {
@@ -34,6 +39,7 @@ export function setBluetoothConnection (info: { @@ -34,6 +39,7 @@ export function setBluetoothConnection (info: {
34 characteristicId?: string 39 characteristicId?: string
35 deviceType?: BtDeviceType 40 deviceType?: BtDeviceType
36 mtu?: number 41 mtu?: number
  42 + driverKey?: string
37 }) { 43 }) {
38 uni.setStorageSync(STORAGE_PRINTER_TYPE, 'bluetooth') 44 uni.setStorageSync(STORAGE_PRINTER_TYPE, 'bluetooth')
39 uni.setStorageSync(STORAGE_BT_DEVICE_ID, info.deviceId) 45 uni.setStorageSync(STORAGE_BT_DEVICE_ID, info.deviceId)
@@ -42,10 +48,12 @@ export function setBluetoothConnection (info: { @@ -42,10 +48,12 @@ export function setBluetoothConnection (info: {
42 uni.setStorageSync(STORAGE_BT_CHARACTERISTIC_ID, info.characteristicId || '') 48 uni.setStorageSync(STORAGE_BT_CHARACTERISTIC_ID, info.characteristicId || '')
43 uni.setStorageSync(STORAGE_BT_DEVICE_TYPE, info.deviceType || 'ble') 49 uni.setStorageSync(STORAGE_BT_DEVICE_TYPE, info.deviceType || 'ble')
44 uni.setStorageSync(STORAGE_BLE_MTU, info.mtu != null ? info.mtu : BLE_MTU_DEFAULT) 50 uni.setStorageSync(STORAGE_BLE_MTU, info.mtu != null ? info.mtu : BLE_MTU_DEFAULT)
  51 + uni.setStorageSync(STORAGE_PRINTER_DRIVER_KEY, info.driverKey || '')
45 } 52 }
46 53
47 -export function setBuiltinPrinter () { 54 +export function setBuiltinPrinter (driverKey = 'generic-tsc') {
48 uni.setStorageSync(STORAGE_PRINTER_TYPE, 'builtin') 55 uni.setStorageSync(STORAGE_PRINTER_TYPE, 'builtin')
  56 + uni.setStorageSync(STORAGE_PRINTER_DRIVER_KEY, driverKey)
49 } 57 }
50 58
51 export function clearPrinter () { 59 export function clearPrinter () {
@@ -56,6 +64,8 @@ export function clearPrinter () { @@ -56,6 +64,8 @@ export function clearPrinter () {
56 uni.removeStorageSync(STORAGE_BT_CHARACTERISTIC_ID) 64 uni.removeStorageSync(STORAGE_BT_CHARACTERISTIC_ID)
57 uni.removeStorageSync(STORAGE_BT_DEVICE_TYPE) 65 uni.removeStorageSync(STORAGE_BT_DEVICE_TYPE)
58 uni.removeStorageSync(STORAGE_BLE_MTU) 66 uni.removeStorageSync(STORAGE_BLE_MTU)
  67 + uni.removeStorageSync(STORAGE_BUILTIN_PORT)
  68 + uni.removeStorageSync(STORAGE_PRINTER_DRIVER_KEY)
59 } 69 }
60 70
61 const BLE_MTU_DEFAULT = 20 71 const BLE_MTU_DEFAULT = 20
@@ -64,6 +74,10 @@ export function getPrinterType (): PrinterType | &#39;&#39; { @@ -64,6 +74,10 @@ export function getPrinterType (): PrinterType | &#39;&#39; {
64 return (uni.getStorageSync(STORAGE_PRINTER_TYPE) as PrinterType) || '' 74 return (uni.getStorageSync(STORAGE_PRINTER_TYPE) as PrinterType) || ''
65 } 75 }
66 76
  77 +export function getCurrentPrinterDriverKey (): string {
  78 + return String(uni.getStorageSync(STORAGE_PRINTER_DRIVER_KEY) || '')
  79 +}
  80 +
67 export function getBluetoothConnection (): { 81 export function getBluetoothConnection (): {
68 deviceId: string 82 deviceId: string
69 deviceName: string 83 deviceName: string
@@ -182,20 +196,31 @@ function sendViaClassic ( @@ -182,20 +196,31 @@ function sendViaClassic (
182 } 196 }
183 return new Promise((resolve, reject) => { 197 return new Promise((resolve, reject) => {
184 try { 198 try {
185 - const classicBluetooth = (require('./bluetoothTool.js') as any).default  
186 if (!classicBluetooth) { 199 if (!classicBluetooth) {
187 reject(new Error('Classic Bluetooth not available')) 200 reject(new Error('Classic Bluetooth not available'))
188 return 201 return
189 } 202 }
190 - const sendData = data.map((byte) => {  
191 - const b = byte & 0xff  
192 - if (b >= 128) return b % 128 - 128  
193 - return b  
194 - }) 203 + const ready = typeof classicBluetooth.ensureConnection === 'function'
  204 + ? classicBluetooth.ensureConnection(conn.deviceId)
  205 + : true
  206 + if (!ready) {
  207 + const errorMessage = typeof classicBluetooth.getLastError === 'function'
  208 + ? classicBluetooth.getLastError()
  209 + : ''
  210 + reject(new Error(errorMessage || 'Classic Bluetooth connection is not ready'))
  211 + return
  212 + }
  213 +
  214 + const sendData = data.map((byte) => byte & 0xff)
195 const ok = classicBluetooth.sendByteData(sendData) 215 const ok = classicBluetooth.sendByteData(sendData)
196 if (onProgress) onProgress(100) 216 if (onProgress) onProgress(100)
197 if (ok) resolve() 217 if (ok) resolve()
198 - else reject(new Error('Classic Bluetooth send failed')) 218 + else {
  219 + const errorMessage = typeof classicBluetooth.getLastError === 'function'
  220 + ? classicBluetooth.getLastError()
  221 + : ''
  222 + reject(new Error(errorMessage || 'Classic Bluetooth send failed'))
  223 + }
199 } catch (e: any) { 224 } catch (e: any) {
200 reject(e) 225 reject(e)
201 } 226 }
@@ -219,24 +244,46 @@ function sendViaBuiltin (data: number[]): Promise&lt;void&gt; { @@ -219,24 +244,46 @@ function sendViaBuiltin (data: number[]): Promise&lt;void&gt; {
219 const hexStr = Array.from(uint8) 244 const hexStr = Array.from(uint8)
220 .map(b => ('0' + (b & 0xff).toString(16)).slice(-2)) 245 .map(b => ('0' + (b & 0xff).toString(16)).slice(-2))
221 .join('') 246 .join('')
222 - return new Promise((resolve, reject) => {  
223 - moeTcp.connect({ ip: '127.0.0.1', port: 9100 }, (res: string) => {  
224 - try {  
225 - const r = typeof res === 'string' ? JSON.parse(res) : res  
226 - if (r.code !== 1) {  
227 - reject(new Error(r.msg || 'Built-in printer connection failed'))  
228 - return  
229 - }  
230 - moeTcp.sendHexStr({ message: hexStr })  
231 - setTimeout(() => { 247 +
  248 + const savedPort = parseInt(uni.getStorageSync(STORAGE_BUILTIN_PORT)) || 0
  249 + const ports = savedPort > 0
  250 + ? [savedPort, ...BUILTIN_PROBE_PORTS.filter(p => p !== savedPort)]
  251 + : [...BUILTIN_PROBE_PORTS]
  252 +
  253 + function tryPort (idx: number): Promise<void> {
  254 + if (idx >= ports.length) {
  255 + return Promise.reject(new Error(
  256 + 'Built-in printer: all ports failed (tried ' + BUILTIN_PROBE_PORTS.join(', ') +
  257 + '). Check if the printer service is running on this device.'
  258 + ))
  259 + }
  260 + const port = ports[idx]
  261 + return new Promise((resolve, reject) => {
  262 + console.log('[builtin] trying 127.0.0.1:' + port)
  263 + moeTcp.connect({ ip: '127.0.0.1', port }, (res: string) => {
  264 + try {
  265 + const r = typeof res === 'string' ? JSON.parse(res) : res
  266 + if (r.code !== 1) {
  267 + console.log('[builtin] port ' + port + ' failed: ' + (r.msg || ''))
  268 + try { moeTcp.disconnect() } catch (_) {}
  269 + tryPort(idx + 1).then(resolve).catch(reject)
  270 + return
  271 + }
  272 + console.log('[builtin] connected on port ' + port)
  273 + uni.setStorageSync(STORAGE_BUILTIN_PORT, String(port))
  274 + moeTcp.sendHexStr({ message: hexStr })
  275 + setTimeout(() => {
  276 + try { moeTcp.disconnect() } catch (_) {}
  277 + resolve()
  278 + }, 300)
  279 + } catch (e) {
232 try { moeTcp.disconnect() } catch (_) {} 280 try { moeTcp.disconnect() } catch (_) {}
233 - resolve()  
234 - }, 300)  
235 - } catch (e) {  
236 - reject(e)  
237 - } 281 + tryPort(idx + 1).then(resolve).catch(reject)
  282 + }
  283 + })
238 }) 284 })
239 - }) 285 + }
  286 + return tryPort(0)
240 } catch (e) { 287 } catch (e) {
241 return Promise.reject(e) 288 return Promise.reject(e)
242 } 289 }
美国版/Food Labeling Management App UniApp/src/utils/print/protocols/escPosBuilder.ts 0 → 100644
  1 +import type { LabelPrintPayload } from '../types/printer'
  2 +
  3 +function stringToBytes (str: string): number[] {
  4 + const out: number[] = []
  5 + for (let i = 0; i < str.length; i++) {
  6 + let c = str.charCodeAt(i)
  7 + if (c < 0x80) {
  8 + out.push(c)
  9 + } else if (c < 0x800) {
  10 + out.push(0xc0 | (c >> 6), 0x80 | (c & 0x3f))
  11 + } else if (c < 0xd800 || c >= 0xe000) {
  12 + out.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f))
  13 + } else {
  14 + i++
  15 + const c2 = str.charCodeAt(i)
  16 + const u = ((c & 0x3ff) << 10) + (c2 & 0x3ff) + 0x10000
  17 + out.push(
  18 + 0xf0 | (u >> 18),
  19 + 0x80 | ((u >> 12) & 0x3f),
  20 + 0x80 | ((u >> 6) & 0x3f),
  21 + 0x80 | (u & 0x3f)
  22 + )
  23 + }
  24 + }
  25 + return out
  26 +}
  27 +
  28 +function appendText (out: number[], text: string) {
  29 + const bytes = stringToBytes(text)
  30 + for (let i = 0; i < bytes.length; i++) out.push(bytes[i])
  31 +}
  32 +
  33 +function appendLine (out: number[], text = '') {
  34 + appendText(out, text)
  35 + out.push(0x0a)
  36 +}
  37 +
  38 +function appendAlign (out: number[], align: 0 | 1 | 2) {
  39 + out.push(0x1b, 0x61, align)
  40 +}
  41 +
  42 +function appendBold (out: number[], bold: boolean) {
  43 + out.push(0x1b, 0x45, bold ? 1 : 0)
  44 +}
  45 +
  46 +function appendSize (out: number[], width = 0, height = 0) {
  47 + const value = ((width & 0x07) << 4) | (height & 0x07)
  48 + out.push(0x1d, 0x21, value)
  49 +}
  50 +
  51 +function createEscDocument (builder: (out: number[]) => void): number[] {
  52 + const out: number[] = []
  53 + out.push(0x1b, 0x40)
  54 + builder(out)
  55 + out.push(0x1b, 0x64, 0x04)
  56 + return out
  57 +}
  58 +
  59 +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 + })
  74 +}
  75 +
  76 +export function buildEscPosLabelData (payload: LabelPrintPayload): number[] {
  77 + const {
  78 + productName,
  79 + labelId,
  80 + printQty = 1,
  81 + category = '',
  82 + extraLine = '',
  83 + } = payload
  84 +
  85 + return createEscDocument((out) => {
  86 + appendAlign(out, 1)
  87 + appendBold(out, true)
  88 + appendSize(out, 1, 1)
  89 + appendLine(out, 'FOOD LABEL')
  90 + appendBold(out, false)
  91 + appendSize(out, 0, 0)
  92 + appendLine(out, '-----------------------------')
  93 + appendLine(out, 'Product: ' + productName)
  94 + if (category) appendLine(out, 'Category: ' + category)
  95 + appendLine(out, 'Label ID: ' + labelId)
  96 + if (extraLine) appendLine(out, extraLine)
  97 + appendLine(out, 'Qty: ' + String(printQty))
  98 + appendLine(out, '-----------------------------')
  99 + })
  100 +}
美国版/Food Labeling Management App UniApp/src/utils/print/protocols/tscProtocol.ts 0 → 100644
  1 +import type { LabelPrintPayload } from '../types/printer'
  2 +import { buildTestTscLabel, buildTscLabel } from '../tscLabelBuilder'
  3 +
  4 +export function buildTscTestPrintData (): number[] {
  5 + return buildTestTscLabel()
  6 +}
  7 +
  8 +export function buildTscLabelData (payload: LabelPrintPayload): number[] {
  9 + return buildTscLabel(payload)
  10 +}
美国版/Food Labeling Management App UniApp/src/utils/print/types/printer.ts 0 → 100644
  1 +export type PrinterType = 'bluetooth' | 'builtin'
  2 +export type BtDeviceType = 'ble' | 'classic' | 'dual' | 'unknown'
  3 +export type ActiveBtDeviceType = 'ble' | 'classic'
  4 +export type PrinterProtocol = 'tsc' | 'esc'
  5 +
  6 +export interface PrinterCandidate {
  7 + deviceId: string
  8 + name: string
  9 + RSSI?: number
  10 + type?: BtDeviceType
  11 +}
  12 +
  13 +export interface LabelPrintPayload {
  14 + productName: string
  15 + labelId: string
  16 + printQty?: number
  17 + widthMm?: number
  18 + heightMm?: number
  19 + category?: string
  20 + extraLine?: string
  21 +}
  22 +
  23 +export interface PrinterDriver {
  24 + key: string
  25 + brand: string
  26 + model: string
  27 + displayName: string
  28 + protocol: PrinterProtocol
  29 + preferredConnection?: ActiveBtDeviceType
  30 + preferredBleMtu?: number
  31 + keywords: string[]
  32 + matches: (device: PrinterCandidate) => number
  33 + resolveConnectionType: (device: PrinterCandidate) => ActiveBtDeviceType
  34 + buildTestPrintData: () => number[]
  35 + buildLabelData: (payload: LabelPrintPayload) => number[]
  36 +}
  37 +
  38 +export interface ResolvedPrinterCandidate extends PrinterCandidate {
  39 + driverKey: string
  40 + driverName: string
  41 + protocol: PrinterProtocol
  42 + resolvedType: ActiveBtDeviceType
  43 +}
  44 +
  45 +export interface CurrentPrinterSummary {
  46 + type: PrinterType | ''
  47 + displayName: string
  48 + deviceId: string
  49 + driverKey: string
  50 + driverName: string
  51 + protocol: PrinterProtocol
  52 + deviceType?: ActiveBtDeviceType | ''
  53 +}
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/RbacRole/RbacRoleGetOutputDto.cs
@@ -5,5 +5,9 @@ namespace FoodLabeling.Application.Contracts.Dtos.RbacRole; @@ -5,5 +5,9 @@ namespace FoodLabeling.Application.Contracts.Dtos.RbacRole;
5 /// </summary> 5 /// </summary>
6 public class RbacRoleGetOutputDto : RbacRoleGetListOutputDto 6 public class RbacRoleGetOutputDto : RbacRoleGetListOutputDto
7 { 7 {
  8 + /// <summary>
  9 + /// 该角色已分配的菜单权限ID列表
  10 + /// </summary>
  11 + public List<string> MenuIds { get; set; } = new();
8 } 12 }
9 13
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberAssignedLocationDto.cs 0 → 100644
  1 +namespace FoodLabeling.Application.Contracts.Dtos.TeamMember;
  2 +
  3 +public class TeamMemberAssignedLocationDto
  4 +{
  5 + public string Id { get; set; } = string.Empty;
  6 +
  7 + public string LocationCode { get; set; } = string.Empty;
  8 +
  9 + public string LocationName { get; set; } = string.Empty;
  10 +}
  11 +
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberCreateInputVo.cs 0 → 100644
  1 +namespace FoodLabeling.Application.Contracts.Dtos.TeamMember;
  2 +
  3 +public class TeamMemberCreateInputVo
  4 +{
  5 + public string FullName { get; set; } = string.Empty;
  6 +
  7 + /// <summary>
  8 + /// 登录账号(建议用邮箱或自定义用户名)
  9 + /// </summary>
  10 + public string UserName { get; set; } = string.Empty;
  11 +
  12 + public string Password { get; set; } = string.Empty;
  13 +
  14 + public string? Email { get; set; }
  15 +
  16 + public long? Phone { get; set; }
  17 +
  18 + public Guid? RoleId { get; set; }
  19 +
  20 + /// <summary>
  21 + /// 关联门店(至少1个)
  22 + /// </summary>
  23 + public List<string> LocationIds { get; set; } = new();
  24 +
  25 + public bool State { get; set; } = true;
  26 +}
  27 +
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberGetListInputVo.cs 0 → 100644
  1 +using Volo.Abp.Application.Dtos;
  2 +
  3 +namespace FoodLabeling.Application.Contracts.Dtos.TeamMember;
  4 +
  5 +/// <summary>
  6 +/// 成员分页查询入参
  7 +/// </summary>
  8 +public class TeamMemberGetListInputVo : PagedAndSortedResultRequestDto
  9 +{
  10 + /// <summary>
  11 + /// 关键字(姓名/用户名/邮箱/电话)
  12 + /// </summary>
  13 + public string? Keyword { get; set; }
  14 +
  15 + /// <summary>
  16 + /// 角色ID(可选)
  17 + /// </summary>
  18 + public Guid? RoleId { get; set; }
  19 +
  20 + /// <summary>
  21 + /// 门店ID(可选)
  22 + /// </summary>
  23 + public string? LocationId { get; set; }
  24 +
  25 + /// <summary>
  26 + /// 启用状态(可选)
  27 + /// </summary>
  28 + public bool? State { get; set; }
  29 +}
  30 +
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberGetListOutputDto.cs 0 → 100644
  1 +namespace FoodLabeling.Application.Contracts.Dtos.TeamMember;
  2 +
  3 +public class TeamMemberGetListOutputDto
  4 +{
  5 + public Guid Id { get; set; }
  6 +
  7 + public string FullName { get; set; } = string.Empty;
  8 +
  9 + public string UserName { get; set; } = string.Empty;
  10 +
  11 + public string? Email { get; set; }
  12 +
  13 + public long? Phone { get; set; }
  14 +
  15 + public bool State { get; set; }
  16 +
  17 + /// <summary>
  18 + /// 角色(当前仅返回第一个)
  19 + /// </summary>
  20 + public Guid? RoleId { get; set; }
  21 +
  22 + public string? RoleName { get; set; }
  23 +
  24 + public List<TeamMemberAssignedLocationDto> AssignedLocations { get; set; } = new();
  25 +}
  26 +
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberGetOutputDto.cs 0 → 100644
  1 +namespace FoodLabeling.Application.Contracts.Dtos.TeamMember;
  2 +
  3 +public class TeamMemberGetOutputDto
  4 +{
  5 + public Guid Id { get; set; }
  6 +
  7 + public string FullName { get; set; } = string.Empty;
  8 +
  9 + public string UserName { get; set; } = string.Empty;
  10 +
  11 + public string? Email { get; set; }
  12 +
  13 + public long? Phone { get; set; }
  14 +
  15 + public bool State { get; set; }
  16 +
  17 + public Guid? RoleId { get; set; }
  18 +
  19 + public List<string> LocationIds { get; set; } = new();
  20 +
  21 + public List<TeamMemberAssignedLocationDto> AssignedLocations { get; set; } = new();
  22 +}
  23 +
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberUpdateInputVo.cs 0 → 100644
  1 +namespace FoodLabeling.Application.Contracts.Dtos.TeamMember;
  2 +
  3 +public class TeamMemberUpdateInputVo
  4 +{
  5 + public string FullName { get; set; } = string.Empty;
  6 +
  7 + public string UserName { get; set; } = string.Empty;
  8 +
  9 + /// <summary>
  10 + /// 为空表示不改密码
  11 + /// </summary>
  12 + public string? Password { get; set; }
  13 +
  14 + public string? Email { get; set; }
  15 +
  16 + public long? Phone { get; set; }
  17 +
  18 + public Guid? RoleId { get; set; }
  19 +
  20 + public List<string> LocationIds { get; set; } = new();
  21 +
  22 + public bool State { get; set; } = true;
  23 +}
  24 +
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ITeamMemberAppService.cs 0 → 100644
  1 +using FoodLabeling.Application.Contracts.Dtos.Common;
  2 +using FoodLabeling.Application.Contracts.Dtos.TeamMember;
  3 +
  4 +namespace FoodLabeling.Application.Contracts.IServices;
  5 +
  6 +public interface ITeamMemberAppService
  7 +{
  8 + Task<PagedResultWithPageDto<TeamMemberGetListOutputDto>> GetListAsync(TeamMemberGetListInputVo input);
  9 +
  10 + Task<TeamMemberGetOutputDto> GetAsync(Guid id);
  11 +
  12 + Task<TeamMemberGetOutputDto> CreateAsync(TeamMemberCreateInputVo input);
  13 +
  14 + Task<TeamMemberGetOutputDto> UpdateAsync(Guid id, TeamMemberUpdateInputVo input);
  15 +
  16 + Task DeleteAsync(Guid id);
  17 +}
  18 +
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/RoleMenuDbEntity.cs 0 → 100644
  1 +using SqlSugar;
  2 +
  3 +namespace FoodLabeling.Application.Services.DbModels;
  4 +
  5 +/// <summary>
  6 +/// rolemenu 表映射(兼容字符串类型 RoleId/MenuId)
  7 +/// </summary>
  8 +[SugarTable("rolemenu")]
  9 +public class RoleMenuDbEntity
  10 +{
  11 + [SugarColumn(IsPrimaryKey = true)]
  12 + public string Id { get; set; } = string.Empty;
  13 +
  14 + public string RoleId { get; set; } = string.Empty;
  15 +
  16 + public string MenuId { get; set; } = string.Empty;
  17 +}
  18 +
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/UserLocationDbEntity.cs 0 → 100644
  1 +using SqlSugar;
  2 +
  3 +namespace FoodLabeling.Application.Services.DbModels;
  4 +
  5 +/// <summary>
  6 +/// userlocation 表映射(成员-门店关联)
  7 +/// </summary>
  8 +[SugarTable("userlocation")]
  9 +public class UserLocationDbEntity
  10 +{
  11 + [SugarColumn(IsPrimaryKey = true)]
  12 + public string Id { get; set; } = string.Empty;
  13 +
  14 + public bool IsDeleted { get; set; }
  15 +
  16 + public DateTime CreationTime { get; set; }
  17 +
  18 + public string? CreatorId { get; set; }
  19 +
  20 + public string? LastModifierId { get; set; }
  21 +
  22 + public DateTime? LastModificationTime { get; set; }
  23 +
  24 + public string UserId { get; set; } = string.Empty;
  25 +
  26 + public string LocationId { get; set; } = string.Empty;
  27 +
  28 + public string? ConcurrencyStamp { get; set; }
  29 +}
  30 +
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/RbacRoleAppService.cs
1 using FoodLabeling.Application.Contracts.Dtos.RbacRole; 1 using FoodLabeling.Application.Contracts.Dtos.RbacRole;
2 using FoodLabeling.Application.Contracts.Dtos.Common; 2 using FoodLabeling.Application.Contracts.Dtos.Common;
3 using FoodLabeling.Application.Contracts.IServices; 3 using FoodLabeling.Application.Contracts.IServices;
  4 +using FoodLabeling.Application.Services.DbModels;
4 using Microsoft.AspNetCore.Mvc; 5 using Microsoft.AspNetCore.Mvc;
5 using SqlSugar; 6 using SqlSugar;
6 using Volo.Abp; 7 using Volo.Abp;
@@ -17,17 +18,20 @@ namespace FoodLabeling.Application.Services; @@ -17,17 +18,20 @@ namespace FoodLabeling.Application.Services;
17 /// </summary> 18 /// </summary>
18 public class RbacRoleAppService : ApplicationService, IRbacRoleAppService 19 public class RbacRoleAppService : ApplicationService, IRbacRoleAppService
19 { 20 {
  21 + private readonly ISqlSugarDbContext _dbContext;
20 private readonly ISqlSugarRepository<RoleAggregateRoot, Guid> _roleRepository; 22 private readonly ISqlSugarRepository<RoleAggregateRoot, Guid> _roleRepository;
21 private readonly ISqlSugarRepository<RoleMenuEntity> _roleMenuRepository; 23 private readonly ISqlSugarRepository<RoleMenuEntity> _roleMenuRepository;
22 private readonly ISqlSugarRepository<RoleDeptEntity> _roleDeptRepository; 24 private readonly ISqlSugarRepository<RoleDeptEntity> _roleDeptRepository;
23 private readonly ISqlSugarRepository<UserRoleEntity> _userRoleRepository; 25 private readonly ISqlSugarRepository<UserRoleEntity> _userRoleRepository;
24 26
25 public RbacRoleAppService( 27 public RbacRoleAppService(
  28 + ISqlSugarDbContext dbContext,
26 ISqlSugarRepository<RoleAggregateRoot, Guid> roleRepository, 29 ISqlSugarRepository<RoleAggregateRoot, Guid> roleRepository,
27 ISqlSugarRepository<RoleMenuEntity> roleMenuRepository, 30 ISqlSugarRepository<RoleMenuEntity> roleMenuRepository,
28 ISqlSugarRepository<RoleDeptEntity> roleDeptRepository, 31 ISqlSugarRepository<RoleDeptEntity> roleDeptRepository,
29 ISqlSugarRepository<UserRoleEntity> userRoleRepository) 32 ISqlSugarRepository<UserRoleEntity> userRoleRepository)
30 { 33 {
  34 + _dbContext = dbContext;
31 _roleRepository = roleRepository; 35 _roleRepository = roleRepository;
32 _roleMenuRepository = roleMenuRepository; 36 _roleMenuRepository = roleMenuRepository;
33 _roleDeptRepository = roleDeptRepository; 37 _roleDeptRepository = roleDeptRepository;
@@ -90,6 +94,11 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService @@ -90,6 +94,11 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService
90 throw new UserFriendlyException("角色不存在"); 94 throw new UserFriendlyException("角色不存在");
91 } 95 }
92 96
  97 + var menuIds = await _dbContext.SqlSugarClient.Queryable<RoleMenuDbEntity>()
  98 + .Where(x => x.RoleId == id.ToString())
  99 + .Select(x => x.MenuId)
  100 + .ToListAsync();
  101 +
93 return new RbacRoleGetOutputDto 102 return new RbacRoleGetOutputDto
94 { 103 {
95 Id = entity.Id, 104 Id = entity.Id,
@@ -98,7 +107,8 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService @@ -98,7 +107,8 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService
98 Remark = entity.Remark, 107 Remark = entity.Remark,
99 DataScope = (int)entity.DataScope, 108 DataScope = (int)entity.DataScope,
100 State = entity.State, 109 State = entity.State,
101 - OrderNum = entity.OrderNum 110 + OrderNum = entity.OrderNum,
  111 + MenuIds = menuIds
102 }; 112 };
103 } 113 }
104 114
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/TeamMemberAppService.cs 0 → 100644
  1 +using FoodLabeling.Application.Contracts.Dtos.Common;
  2 +using FoodLabeling.Application.Contracts.Dtos.TeamMember;
  3 +using FoodLabeling.Application.Contracts.IServices;
  4 +using FoodLabeling.Application.Services.DbModels;
  5 +using FoodLabeling.Domain.Entities;
  6 +using SqlSugar;
  7 +using Volo.Abp;
  8 +using Volo.Abp.Application.Services;
  9 +using Volo.Abp.Domain.Entities;
  10 +using Volo.Abp.Guids;
  11 +using Yi.Framework.Rbac.Domain.Entities;
  12 +using Yi.Framework.Rbac.Domain.Managers;
  13 +using Yi.Framework.SqlSugarCore.Abstractions;
  14 +
  15 +namespace FoodLabeling.Application.Services;
  16 +
  17 +/// <summary>
  18 +/// 成员(Team Member/Manager)服务,对外仅在 food-labeling-us 暴露
  19 +/// </summary>
  20 +public class TeamMemberAppService : ApplicationService, ITeamMemberAppService
  21 +{
  22 + private readonly ISqlSugarRepository<UserAggregateRoot, Guid> _userRepository;
  23 + private readonly UserManager _userManager;
  24 + private readonly ISqlSugarDbContext _dbContext;
  25 + private readonly IGuidGenerator _guidGenerator;
  26 +
  27 + public TeamMemberAppService(
  28 + ISqlSugarRepository<UserAggregateRoot, Guid> userRepository,
  29 + UserManager userManager,
  30 + ISqlSugarDbContext dbContext,
  31 + IGuidGenerator guidGenerator)
  32 + {
  33 + _userRepository = userRepository;
  34 + _userManager = userManager;
  35 + _dbContext = dbContext;
  36 + _guidGenerator = guidGenerator;
  37 + }
  38 +
  39 + /// <summary>
  40 + /// 成员分页列表(含角色与已分配门店)
  41 + /// </summary>
  42 + public async Task<PagedResultWithPageDto<TeamMemberGetListOutputDto>> GetListAsync(TeamMemberGetListInputVo input)
  43 + {
  44 + var pageIndex = input.SkipCount / input.MaxResultCount + 1;
  45 + var pageSize = input.MaxResultCount;
  46 + var keyword = input.Keyword?.Trim();
  47 +
  48 + RefAsync<int> total = 0;
  49 +
  50 + // 先按 user 表筛选分页,再批量补齐角色与门店
  51 + var users = await _userRepository._DbQueryable
  52 + .Where(u => !u.IsDeleted)
  53 + .WhereIF(!string.IsNullOrWhiteSpace(keyword),
  54 + u => (u.Name != null && u.Name.Contains(keyword!)) ||
  55 + u.UserName.Contains(keyword!) ||
  56 + (u.Email != null && u.Email.Contains(keyword!)) ||
  57 + (u.Phone != null && u.Phone.ToString()!.Contains(keyword!)))
  58 + .WhereIF(input.State != null, u => u.State == input.State)
  59 + .OrderByIF(!string.IsNullOrWhiteSpace(input.Sorting), input.Sorting!)
  60 + .OrderByDescending(u => u.CreationTime)
  61 + .ToPageListAsync(input.SkipCount, input.MaxResultCount, total);
  62 +
  63 + var userIds = users.Select(x => x.Id).ToList();
  64 + var userIdStrings = userIds.Select(x => x.ToString()).ToList();
  65 +
  66 + // user-role: 仅取第一个角色(原型表格展示单角色)
  67 + var userRolePairs = await _dbContext.SqlSugarClient.Queryable<UserRoleEntity, RoleAggregateRoot>((ur, r) => ur.RoleId == r.Id)
  68 + .Where(ur => userIds.Contains(ur.UserId))
  69 + .Select((ur, r) => new { ur.UserId, r.Id, r.RoleName })
  70 + .ToListAsync();
  71 +
  72 + var roleMap = userRolePairs
  73 + .GroupBy(x => x.UserId)
  74 + .ToDictionary(g => g.Key, g => g.FirstOrDefault());
  75 +
  76 + // user-location
  77 + var userLocations = await _dbContext.SqlSugarClient.Queryable<UserLocationDbEntity>()
  78 + .Where(x => !x.IsDeleted)
  79 + .WhereIF(!string.IsNullOrWhiteSpace(input.LocationId), x => x.LocationId == input.LocationId)
  80 + .Where(x => userIdStrings.Contains(x.UserId))
  81 + .ToListAsync();
  82 +
  83 + // 如果按 LocationId 过滤,需要反向过滤掉无关联的 user
  84 + if (!string.IsNullOrWhiteSpace(input.LocationId))
  85 + {
  86 + var allowedUserIds = userLocations.Select(x => x.UserId).ToHashSet();
  87 + users = users.Where(u => allowedUserIds.Contains(u.Id.ToString())).ToList();
  88 + }
  89 +
  90 + var locationIds = userLocations.Select(x => x.LocationId).Distinct().ToList();
  91 + var locations = await _dbContext.SqlSugarClient.Queryable<LocationAggregateRoot>()
  92 + .Where(x => !x.IsDeleted)
  93 + .WhereIF(locationIds.Count > 0, x => locationIds.Contains(x.Id.ToString()))
  94 + .Select(x => new { x.Id, x.LocationCode, x.LocationName })
  95 + .ToListAsync();
  96 + var locationMap = locations.ToDictionary(x => x.Id.ToString(), x => x);
  97 +
  98 + var assignedMap = userLocations
  99 + .GroupBy(x => x.UserId)
  100 + .ToDictionary(
  101 + g => g.Key,
  102 + g => g.Select(x =>
  103 + {
  104 + if (locationMap.TryGetValue(x.LocationId, out var loc))
  105 + {
  106 + return new TeamMemberAssignedLocationDto
  107 + {
  108 + Id = loc.Id.ToString(),
  109 + LocationCode = loc.LocationCode,
  110 + LocationName = loc.LocationName
  111 + };
  112 + }
  113 + return null;
  114 + }).Where(x => x != null).Cast<TeamMemberAssignedLocationDto>().ToList());
  115 +
  116 + var items = users.Select(u =>
  117 + {
  118 + roleMap.TryGetValue(u.Id, out var role);
  119 + assignedMap.TryGetValue(u.Id.ToString(), out var assigned);
  120 +
  121 + return new TeamMemberGetListOutputDto
  122 + {
  123 + Id = u.Id,
  124 + FullName = u.Name ?? string.Empty,
  125 + UserName = u.UserName,
  126 + Email = u.Email,
  127 + Phone = u.Phone,
  128 + State = u.State,
  129 + RoleId = role?.Id,
  130 + RoleName = role?.RoleName,
  131 + AssignedLocations = assigned ?? new List<TeamMemberAssignedLocationDto>()
  132 + };
  133 + }).ToList();
  134 +
  135 + var totalCount = (long)total;
  136 + return new PagedResultWithPageDto<TeamMemberGetListOutputDto>
  137 + {
  138 + PageIndex = pageIndex,
  139 + PageSize = pageSize,
  140 + TotalCount = totalCount,
  141 + TotalPages = (int)Math.Ceiling(totalCount / (double)pageSize),
  142 + Items = items
  143 + };
  144 + }
  145 +
  146 + /// <summary>
  147 + /// 成员详情(带门店ID列表)
  148 + /// </summary>
  149 + public async Task<TeamMemberGetOutputDto> GetAsync(Guid id)
  150 + {
  151 + var user = await _userRepository.GetByIdAsync(id);
  152 + if (user is null || user.IsDeleted)
  153 + {
  154 + throw new UserFriendlyException("成员不存在");
  155 + }
  156 +
  157 + var userIdString = id.ToString();
  158 + var links = await _dbContext.SqlSugarClient.Queryable<UserLocationDbEntity>()
  159 + .Where(x => !x.IsDeleted && x.UserId == userIdString)
  160 + .ToListAsync();
  161 +
  162 + var locationIds = links.Select(x => x.LocationId).Distinct().ToList();
  163 + var locations = await _dbContext.SqlSugarClient.Queryable<LocationAggregateRoot>()
  164 + .Where(x => !x.IsDeleted)
  165 + .WhereIF(locationIds.Count > 0, x => locationIds.Contains(x.Id.ToString()))
  166 + .Select(x => new { x.Id, x.LocationCode, x.LocationName })
  167 + .ToListAsync();
  168 +
  169 + var assigned = locations.Select(x => new TeamMemberAssignedLocationDto
  170 + {
  171 + Id = x.Id.ToString(),
  172 + LocationCode = x.LocationCode,
  173 + LocationName = x.LocationName
  174 + }).ToList();
  175 +
  176 + var role = await _dbContext.SqlSugarClient.Queryable<UserRoleEntity>().FirstAsync(x => x.UserId == id);
  177 +
  178 + return new TeamMemberGetOutputDto
  179 + {
  180 + Id = user.Id,
  181 + FullName = user.Name ?? string.Empty,
  182 + UserName = user.UserName,
  183 + Email = user.Email,
  184 + Phone = user.Phone,
  185 + State = user.State,
  186 + RoleId = role?.RoleId,
  187 + LocationIds = locationIds,
  188 + AssignedLocations = assigned
  189 + };
  190 + }
  191 +
  192 + /// <summary>
  193 + /// 新增成员(同步设置角色与门店)
  194 + /// </summary>
  195 + public async Task<TeamMemberGetOutputDto> CreateAsync(TeamMemberCreateInputVo input)
  196 + {
  197 + if (input.LocationIds is null || input.LocationIds.Count == 0)
  198 + {
  199 + throw new UserFriendlyException("成员必须至少分配一个门店");
  200 + }
  201 +
  202 + var user = new UserAggregateRoot(input.UserName.Trim(), input.Password, input.Phone, input.FullName.Trim())
  203 + {
  204 + Name = input.FullName.Trim(),
  205 + Email = input.Email?.Trim(),
  206 + State = input.State
  207 + };
  208 +
  209 + EntityHelper.TrySetId(user, _guidGenerator.Create);
  210 + user.BuildPassword();
  211 +
  212 + await _userManager.CreateAsync(user);
  213 +
  214 + if (input.RoleId != null)
  215 + {
  216 + await _userManager.GiveUserSetRoleAsync(new List<Guid> { user.Id }, new List<Guid> { input.RoleId.Value });
  217 + }
  218 +
  219 + await UpsertUserLocationsAsync(user.Id, input.LocationIds);
  220 +
  221 + return await GetAsync(user.Id);
  222 + }
  223 +
  224 + /// <summary>
  225 + /// 编辑成员(同步设置角色与门店)
  226 + /// </summary>
  227 + public async Task<TeamMemberGetOutputDto> UpdateAsync(Guid id, TeamMemberUpdateInputVo input)
  228 + {
  229 + if (input.LocationIds is null || input.LocationIds.Count == 0)
  230 + {
  231 + throw new UserFriendlyException("成员必须至少分配一个门店");
  232 + }
  233 +
  234 + var user = await _userRepository.GetByIdAsync(id);
  235 + if (user is null || user.IsDeleted)
  236 + {
  237 + throw new UserFriendlyException("成员不存在");
  238 + }
  239 +
  240 + user.Name = input.FullName.Trim();
  241 + user.UserName = input.UserName.Trim();
  242 + user.Email = input.Email?.Trim();
  243 + user.Phone = input.Phone;
  244 + user.State = input.State;
  245 +
  246 + if (!string.IsNullOrWhiteSpace(input.Password))
  247 + {
  248 + user.EncryPassword.Password = input.Password;
  249 + user.BuildPassword();
  250 + }
  251 +
  252 + await _userRepository.UpdateAsync(user);
  253 +
  254 + // 角色:覆盖式设置(只保留一个)
  255 + if (input.RoleId != null)
  256 + {
  257 + await _userManager.GiveUserSetRoleAsync(new List<Guid> { id }, new List<Guid> { input.RoleId.Value });
  258 + }
  259 + else
  260 + {
  261 + await _userManager.GiveUserSetRoleAsync(new List<Guid> { id }, new List<Guid>());
  262 + }
  263 +
  264 + await UpsertUserLocationsAsync(id, input.LocationIds);
  265 +
  266 + return await GetAsync(id);
  267 + }
  268 +
  269 + /// <summary>
  270 + /// 删除成员(逻辑删除 user;并逻辑删除关联表)
  271 + /// </summary>
  272 + public async Task DeleteAsync(Guid id)
  273 + {
  274 + var user = await _userRepository.GetByIdAsync(id);
  275 + if (user is null || user.IsDeleted)
  276 + {
  277 + return;
  278 + }
  279 +
  280 + user.IsDeleted = true;
  281 + await _userRepository.UpdateAsync(user);
  282 +
  283 + var userIdString = id.ToString();
  284 + var currentUserId = CurrentUser?.Id?.ToString();
  285 + await _dbContext.SqlSugarClient.Updateable<UserLocationDbEntity>()
  286 + .SetColumns(x => new UserLocationDbEntity
  287 + {
  288 + IsDeleted = true,
  289 + LastModificationTime = DateTime.Now,
  290 + LastModifierId = currentUserId
  291 + })
  292 + .Where(x => x.UserId == userIdString && !x.IsDeleted)
  293 + .ExecuteCommandAsync();
  294 + }
  295 +
  296 + private async Task UpsertUserLocationsAsync(Guid userId, List<string> locationIds)
  297 + {
  298 + var now = DateTime.Now;
  299 + var userIdString = userId.ToString();
  300 + var wanted = locationIds.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()).Distinct().ToList();
  301 + var currentUserId = CurrentUser?.Id?.ToString();
  302 +
  303 + // 校验门店存在且未删除
  304 + var validCount = await _dbContext.SqlSugarClient.Queryable<LocationAggregateRoot>()
  305 + .Where(x => !x.IsDeleted)
  306 + .Where(x => wanted.Contains(x.Id.ToString()))
  307 + .CountAsync();
  308 + if (validCount != wanted.Count)
  309 + {
  310 + throw new UserFriendlyException("存在无效门店,请刷新后重试");
  311 + }
  312 +
  313 + var existing = await _dbContext.SqlSugarClient.Queryable<UserLocationDbEntity>()
  314 + .Where(x => x.UserId == userIdString)
  315 + .ToListAsync();
  316 +
  317 + var existingActive = existing.Where(x => !x.IsDeleted).ToList();
  318 + var existingActiveSet = existingActive.Select(x => x.LocationId).ToHashSet();
  319 +
  320 + // 需要删除的(逻辑删除)
  321 + var toDelete = existingActive.Where(x => !wanted.Contains(x.LocationId)).ToList();
  322 + if (toDelete.Count > 0)
  323 + {
  324 + var ids = toDelete.Select(x => x.Id).ToList();
  325 + await _dbContext.SqlSugarClient.Updateable<UserLocationDbEntity>()
  326 + .SetColumns(x => new UserLocationDbEntity
  327 + {
  328 + IsDeleted = true,
  329 + LastModificationTime = now,
  330 + LastModifierId = currentUserId
  331 + })
  332 + .Where(x => ids.Contains(x.Id))
  333 + .ExecuteCommandAsync();
  334 + }
  335 +
  336 + // 需要新增的
  337 + var toInsert = wanted.Where(x => !existingActiveSet.Contains(x)).ToList();
  338 + if (toInsert.Count > 0)
  339 + {
  340 + var rows = toInsert.Select(locationId => new UserLocationDbEntity
  341 + {
  342 + Id = _guidGenerator.Create().ToString(),
  343 + IsDeleted = false,
  344 + CreationTime = now,
  345 + CreatorId = currentUserId,
  346 + UserId = userIdString,
  347 + LocationId = locationId,
  348 + ConcurrencyStamp = string.Empty
  349 + }).ToList();
  350 +
  351 + await _dbContext.SqlSugarClient.Insertable(rows).ExecuteCommandAsync();
  352 + }
  353 + }
  354 +}
  355 +