Commit acf08c9c6a15436f1d06addc5e4012cc1b9bd3be
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 | 219 | \ No newline at end of file | ... | ... |
美国版/Food Labeling Management App UniApp/src/locales/en.ts
| ... | ... | @@ -53,6 +53,11 @@ export default { |
| 53 | 53 | connectFail: 'Connection failed', |
| 54 | 54 | bleNotAvailable: 'Bluetooth not available', |
| 55 | 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 | 61 | printer1: { name: 'Kitchen Printer #1', location: 'Main Kitchen' }, |
| 57 | 62 | printer2: { name: 'Kitchen Printer #2', location: 'Main Kitchen' }, |
| 58 | 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 | 53 | connectFail: '连接失败', |
| 54 | 54 | bleNotAvailable: '蓝牙未开启或不可用', |
| 55 | 55 | noDevices: '未发现设备', |
| 56 | + pairedDevices: '已配对设备', | |
| 57 | + noPairedDevices: '没有已配对的蓝牙设备', | |
| 58 | + classic: '经典', | |
| 59 | + ble: '低功耗', | |
| 60 | + dual: '双模', | |
| 56 | 61 | printer1: { name: '厨房打印机 #1', location: '主厨房' }, |
| 57 | 62 | printer2: { name: '厨房打印机 #2', location: '主厨房' }, |
| 58 | 63 | printer3: { name: '准备区打印机', location: '准备站' }, | ... | ... |
美国版/Food Labeling Management App UniApp/src/manifest.json
| ... | ... | @@ -2,8 +2,8 @@ |
| 2 | 2 | "name" : "food.labeling", |
| 3 | 3 | "appid" : "__UNI__1BFD76D", |
| 4 | 4 | "description" : "", |
| 5 | - "versionName" : "1.0.1", | |
| 6 | - "versionCode" : 101, | |
| 5 | + "versionName" : "1.0.3", | |
| 6 | + "versionCode" : 103, | |
| 7 | 7 | "transformPx" : false, |
| 8 | 8 | /* 5+App特有相关 */ |
| 9 | 9 | "app-plus" : { |
| ... | ... | @@ -18,8 +18,9 @@ |
| 18 | 18 | }, |
| 19 | 19 | /* 模块配置 */ |
| 20 | 20 | "modules" : { |
| 21 | - "Bluetooth" : {}, | |
| 22 | - "Camera" : {} | |
| 21 | + "Camera" : {}, | |
| 22 | + "Barcode" : {}, | |
| 23 | + "Bluetooth" : {} | |
| 23 | 24 | }, |
| 24 | 25 | /* 应用发布信息 */ |
| 25 | 26 | "distribute" : { |
| ... | ... | @@ -43,6 +44,10 @@ |
| 43 | 44 | "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>", |
| 44 | 45 | "<uses-permission android:name=\"android.permission.BLUETOOTH\"/>", |
| 45 | 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 | 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 | 42 | <text class="error-text">{{ errorMsg }}</text> |
| 43 | 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 | 59 | <!-- Connected --> |
| 46 | 60 | <view v-if="connectedDevice" class="connected-card"> |
| 47 | 61 | <view class="connected-header"> |
| ... | ... | @@ -153,26 +167,21 @@ import AppIcon from '../../components/AppIcon.vue' |
| 153 | 167 | import SideMenu from '../../components/SideMenu.vue' |
| 154 | 168 | import LocationPicker from '../../components/LocationPicker.vue' |
| 155 | 169 | import { getStatusBarHeight } from '../../utils/statusBar' |
| 170 | +import classicBluetooth from '../../utils/print/bluetoothTool.js' | |
| 156 | 171 | import { |
| 157 | 172 | getPrinterType, |
| 158 | - getBluetoothConnection, | |
| 159 | - setBluetoothConnection, | |
| 160 | - setBuiltinPrinter, | |
| 161 | 173 | clearPrinter, |
| 162 | - sendToPrinter, | |
| 163 | 174 | type PrinterType, |
| 164 | 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 | 186 | const statusBarHeight = getStatusBarHeight() |
| 178 | 187 | const isMenuOpen = ref(false) |
| ... | ... | @@ -181,6 +190,17 @@ const connectingId = ref('') |
| 181 | 190 | const errorMsg = ref('') |
| 182 | 191 | const btAdapterReady = ref(false) |
| 183 | 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 | 205 | interface BtDevice { |
| 186 | 206 | deviceId: string |
| ... | ... | @@ -192,14 +212,26 @@ interface BtDevice { |
| 192 | 212 | const devices = ref<BtDevice[]>([]) |
| 193 | 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 | 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 | 236 | return null |
| 205 | 237 | }) |
| ... | ... | @@ -208,6 +240,7 @@ function switchType (type: 'bluetooth' | 'builtin') { |
| 208 | 240 | printerType.value = type |
| 209 | 241 | if (type === 'bluetooth' && getPrinterType() === 'builtin') { |
| 210 | 242 | clearPrinter() |
| 243 | + refreshCurrentPrinter() | |
| 211 | 244 | } |
| 212 | 245 | } |
| 213 | 246 | |
| ... | ... | @@ -234,37 +267,24 @@ const initBluetooth = (): Promise<void> => { |
| 234 | 267 | }) |
| 235 | 268 | } |
| 236 | 269 | |
| 237 | -const startDiscovery = () => { | |
| 270 | +const startBleScan = () => { | |
| 238 | 271 | uni.startBluetoothDevicesDiscovery({ |
| 239 | 272 | allowDuplicatesKey: false, |
| 240 | 273 | success: () => { |
| 241 | 274 | isScanning.value = true |
| 242 | 275 | errorMsg.value = '' |
| 276 | + debugInfo.value.lastBleEvent = 'scan running' | |
| 243 | 277 | }, |
| 244 | 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 | 290 | const stopDiscovery = () => { |
| ... | ... | @@ -274,7 +294,7 @@ const stopDiscovery = () => { |
| 274 | 294 | }, |
| 275 | 295 | }) |
| 276 | 296 | // #ifdef APP-PLUS |
| 277 | - const classic = getClassicBluetooth() | |
| 297 | + const classic = classicBluetooth | |
| 278 | 298 | if (classic && classic.cancelClassicDiscovery) classic.cancelClassicDiscovery() |
| 279 | 299 | // #endif |
| 280 | 300 | } |
| ... | ... | @@ -286,12 +306,12 @@ const onDeviceFound = (res: any) => { |
| 286 | 306 | const name = (d.localName || d.name || '').trim() |
| 287 | 307 | const displayName = name || 'Unknown Device' |
| 288 | 308 | discoveredIds.add(d.deviceId) |
| 289 | - devices.value.push({ | |
| 309 | + devices.value.push(describeDiscoveredPrinter({ | |
| 290 | 310 | deviceId: d.deviceId, |
| 291 | 311 | name: displayName, |
| 292 | 312 | RSSI: d.RSSI, |
| 293 | 313 | type: 'ble', |
| 294 | - }) | |
| 314 | + })) | |
| 295 | 315 | } |
| 296 | 316 | devices.value.sort((a, b) => (b.RSSI || -100) - (a.RSSI || -100)) |
| 297 | 317 | } |
| ... | ... | @@ -305,12 +325,12 @@ function mergeCachedBleDevices () { |
| 305 | 325 | if (discoveredIds.has(d.deviceId)) continue |
| 306 | 326 | const name = (d.localName || d.name || '').trim() |
| 307 | 327 | discoveredIds.add(d.deviceId) |
| 308 | - devices.value.push({ | |
| 328 | + devices.value.push(describeDiscoveredPrinter({ | |
| 309 | 329 | deviceId: d.deviceId, |
| 310 | 330 | name: name || 'Unknown Device', |
| 311 | 331 | RSSI: d.RSSI, |
| 312 | 332 | type: 'ble', |
| 313 | - }) | |
| 333 | + })) | |
| 314 | 334 | } |
| 315 | 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 | 339 | |
| 320 | 340 | function addPairedDevices () { |
| 321 | 341 | // #ifdef APP-PLUS |
| 322 | - const classic = getClassicBluetooth() | |
| 342 | + const classic = classicBluetooth | |
| 323 | 343 | if (!classic || !classic.getPairedDevices) return |
| 324 | 344 | try { |
| 325 | 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 | 348 | for (const p of paired) { |
| 327 | 349 | if (discoveredIds.has(p.deviceId)) continue |
| 328 | 350 | discoveredIds.add(p.deviceId) |
| 329 | - devices.value.push({ | |
| 351 | + devices.value.push(describeDiscoveredPrinter({ | |
| 330 | 352 | deviceId: p.deviceId, |
| 331 | - name: p.name || 'Unknown', | |
| 353 | + name: p.name || 'Unknown Device', | |
| 332 | 354 | type: p.type || 'classic', |
| 333 | - }) | |
| 355 | + })) | |
| 334 | 356 | } |
| 335 | 357 | if (paired.length > 0) { |
| 358 | + debugInfo.value.lastClassicEvent = 'paired devices loaded' | |
| 336 | 359 | devices.value.sort((a, b) => (b.RSSI || -100) - (a.RSSI || -100)) |
| 360 | + } else { | |
| 361 | + debugInfo.value.lastClassicEvent = 'no paired devices' | |
| 337 | 362 | } |
| 338 | 363 | } catch (e) { |
| 339 | 364 | console.error('addPairedDevices error:', e) |
| 365 | + debugInfo.value.lastClassicEvent = 'load paired devices failed' | |
| 340 | 366 | } |
| 341 | 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 | 403 | const handleScan = async () => { |
| ... | ... | @@ -383,15 +408,30 @@ const handleScan = async () => { |
| 383 | 408 | errorMsg.value = '' |
| 384 | 409 | devices.value = [] |
| 385 | 410 | discoveredIds.clear() |
| 411 | + debugInfo.value.lastBleError = '' | |
| 412 | + debugInfo.value.locationServiceRequired = false | |
| 413 | + debugInfo.value.lastClassicEvent = 'starting' | |
| 414 | + debugInfo.value.lastBleEvent = 'starting' | |
| 415 | + | |
| 386 | 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 | 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 | 435 | await new Promise<void>((resolve) => { |
| 396 | 436 | uni.getLocation({ |
| 397 | 437 | type: 'wgs84', |
| ... | ... | @@ -399,13 +439,15 @@ const handleScan = async () => { |
| 399 | 439 | fail: () => resolve(), |
| 400 | 440 | }) |
| 401 | 441 | }) |
| 402 | - startDiscovery() | |
| 442 | + startBleScan() | |
| 403 | 443 | uni.showToast({ title: 'Scanning...', icon: 'none' }) |
| 404 | 444 | setTimeout(() => { |
| 405 | 445 | if (isScanning.value) stopDiscovery() |
| 406 | 446 | }, 20000) |
| 407 | 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) => { |
| 416 | 458 | |
| 417 | 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 | 463 | connectingId.value = '' |
| 446 | - errorMsg.value = 'Classic Bluetooth requires the app.' | |
| 447 | - // #endif | |
| 464 | + errorMsg.value = permissionResult.message || 'Bluetooth permission denied.' | |
| 448 | 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 | 479 | const handleUseBuiltin = () => { |
| 489 | - setBuiltinPrinter() | |
| 480 | + useBuiltinPrinter() | |
| 481 | + refreshCurrentPrinter() | |
| 490 | 482 | uni.showToast({ title: 'Using built-in printer', icon: 'success' }) |
| 491 | 483 | } |
| 492 | 484 | |
| ... | ... | @@ -495,8 +487,7 @@ const handleTestPrint = async () => { |
| 495 | 487 | if (testPrinting.value) return |
| 496 | 488 | testPrinting.value = true |
| 497 | 489 | try { |
| 498 | - const data = buildTestTscLabel() | |
| 499 | - await sendToPrinter(data, (p) => { | |
| 490 | + await testPrintCurrentPrinter((p) => { | |
| 500 | 491 | if (p < 100) uni.showLoading({ title: `Printing ${p}%`, mask: true }) |
| 501 | 492 | }) |
| 502 | 493 | uni.hideLoading() |
| ... | ... | @@ -523,30 +514,14 @@ const handleTestPrint = async () => { |
| 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 | 523 | onMounted(() => { |
| 524 | + debugInfo.value.classicModuleReady = !!classicBluetooth | |
| 550 | 525 | uni.onBluetoothDeviceFound(onDeviceFound) |
| 551 | 526 | uni.onBluetoothAdapterStateChange((res: any) => { |
| 552 | 527 | if (!res.available) { |
| ... | ... | @@ -559,11 +534,22 @@ onMounted(() => { |
| 559 | 534 | } |
| 560 | 535 | }) |
| 561 | 536 | printerType.value = getPrinterType() || 'bluetooth' |
| 537 | + refreshCurrentPrinter() | |
| 562 | 538 | }) |
| 563 | 539 | |
| 564 | 540 | onUnmounted(() => { |
| 565 | 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 | 554 | </script> |
| 569 | 555 | |
| ... | ... | @@ -883,6 +869,37 @@ onUnmounted(() => { |
| 883 | 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 | 903 | .device-tag { |
| 887 | 904 | display: inline-block; |
| 888 | 905 | font-size: 20rpx; | ... | ... |
美国版/Food Labeling Management App UniApp/src/pages/labels/preview.vue
| ... | ... | @@ -118,8 +118,7 @@ import SideMenu from '../../components/SideMenu.vue' |
| 118 | 118 | import LocationPicker from '../../components/LocationPicker.vue' |
| 119 | 119 | import { getStatusBarHeight } from '../../utils/statusBar' |
| 120 | 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 | 122 | import chickenLabelImg from '../../static/chicken-lable.png' |
| 124 | 123 | |
| 125 | 124 | const statusBarHeight = getStatusBarHeight() |
| ... | ... | @@ -161,16 +160,9 @@ const displayProductName = computed(() => { |
| 161 | 160 | }) |
| 162 | 161 | |
| 163 | 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 | 168 | interface ProductData { |
| ... | ... | @@ -245,13 +237,12 @@ const handlePrint = async () => { |
| 245 | 237 | } |
| 246 | 238 | isPrinting.value = true |
| 247 | 239 | try { |
| 248 | - const data = buildTscLabel({ | |
| 240 | + await printLabelForCurrentPrinter({ | |
| 249 | 241 | productName: displayProductName.value, |
| 250 | 242 | labelId: labelId.value, |
| 251 | 243 | printQty: printQty.value, |
| 252 | 244 | extraLine: lastEdited.value, |
| 253 | - }) | |
| 254 | - await sendToPrinter(data, (percent) => { | |
| 245 | + }, (percent) => { | |
| 255 | 246 | if (percent >= 100) return |
| 256 | 247 | uni.showLoading({ title: `Printing ${percent}%`, mask: true }) |
| 257 | 248 | }) |
| ... | ... | @@ -265,8 +256,8 @@ const handlePrint = async () => { |
| 265 | 256 | const msg = (e && e.message) ? e.message : 'Print failed' |
| 266 | 257 | if (msg === 'BUILTIN_PLUGIN_NOT_FOUND' || (msg && msg.indexOf('Built-in printer') !== -1)) { |
| 267 | 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 | 261 | confirmText: 'Printer Settings', |
| 271 | 262 | cancelText: 'Cancel', |
| 272 | 263 | success: (res) => { | ... | ... |
美国版/Food Labeling Management App UniApp/src/pages/more/printers.vue
| ... | ... | @@ -21,6 +21,20 @@ |
| 21 | 21 | <text class="badge-text">{{ t('printers.connected') }}</text> |
| 22 | 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 | 38 | <!-- Connected Device --> |
| 25 | 39 | <view v-if="currentType" class="printer-card connected"> |
| 26 | 40 | <view class="printer-icon"><AppIcon name="printer" size="md" color="white" /></view> |
| ... | ... | @@ -45,6 +59,29 @@ |
| 45 | 59 | <button class="btn-connect">{{ t('printers.connect') }}</button> |
| 46 | 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 | 85 | <!-- Bluetooth Scanning Area --> |
| 49 | 86 | <view class="section-header"> |
| 50 | 87 | <text class="section-title">{{ t('printers.nearby') }}</text> |
| ... | ... | @@ -62,7 +99,10 @@ |
| 62 | 99 | <text class="printer-name">{{ d.name || 'Unknown Device' }}</text> |
| 63 | 100 | <text class="printer-loc">{{ d.deviceId }}</text> |
| 64 | 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 | 106 | </view> |
| 67 | 107 | |
| 68 | 108 | <view v-if="devices.length === 0 && !isScanning" class="empty-state"> |
| ... | ... | @@ -79,178 +119,290 @@ import { ref, onMounted, onUnmounted } from 'vue' |
| 79 | 119 | import { useI18n } from 'vue-i18n' |
| 80 | 120 | import AppIcon from '../../components/AppIcon.vue' |
| 81 | 121 | import SideMenu from '../../components/SideMenu.vue' |
| 122 | +import classicBluetooth from '../../utils/print/bluetoothTool.js' | |
| 123 | +import { ensureBluetoothPermissions } from '../../utils/print/bluetoothPermissions' | |
| 82 | 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 | 133 | const { t } = useI18n() |
| 94 | 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 | 138 | const isScanning = ref(false) |
| 100 | 139 | const devices = ref<any[]>([]) |
| 140 | +const pairedDevices = ref<any[]>([]) | |
| 101 | 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 | 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 | 237 | if (isScanning.value) return |
| 112 | - | |
| 238 | + | |
| 113 | 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 | 251 | uni.openBluetoothAdapter({ |
| 117 | 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 | 294 | uni.startBluetoothDevicesDiscovery({ |
| 119 | 295 | allowDuplicatesKey: false, |
| 120 | 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 | 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 | 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 | 324 | const stopScan = () => { |
| 143 | 325 | isScanning.value = false |
| 326 | + debugInfo.value.lastClassicEvent = 'stopped' | |
| 327 | + debugInfo.value.lastBleEvent = 'stopped' | |
| 144 | 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 | 342 | if (isConnecting.value) return |
| 153 | 343 | isConnecting.value = true |
| 154 | 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 | 367 | const connectBuiltin = () => { |
| 230 | - setBuiltinPrinter() | |
| 231 | - currentType.value = 'builtin' | |
| 368 | + useBuiltinPrinter() | |
| 369 | + refreshStatus() | |
| 232 | 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 | 376 | uni.showToast({ title: t('printers.disconnected') }) |
| 241 | 377 | } |
| 242 | 378 | |
| 243 | -// 测试打印 | |
| 244 | 379 | const doTestPrint = async () => { |
| 245 | 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 | 389 | uni.showLoading({ title: t('labels.print.printing') }) |
| 248 | - await sendToPrinter(data) | |
| 390 | + await testPrintCurrentPrinter() | |
| 249 | 391 | uni.hideLoading() |
| 250 | 392 | uni.showToast({ title: t('printers.testPrintSuccess') }) |
| 251 | 393 | } catch (e: any) { |
| 252 | 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 = () => { |
| 265 | 417 | |
| 266 | 418 | onMounted(() => { |
| 267 | 419 | refreshStatus() |
| 420 | + debugInfo.value.classicModuleReady = !!classicBluetooth | |
| 421 | + // #ifdef APP-PLUS | |
| 422 | + loadPairedDevices() | |
| 423 | + // #endif | |
| 268 | 424 | }) |
| 269 | 425 | |
| 270 | 426 | onUnmounted(() => { |
| 271 | 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 | 438 | </script> |
| 274 | 439 | |
| ... | ... | @@ -288,6 +453,11 @@ onUnmounted(() => { |
| 288 | 453 | .info-badge { display: flex; align-items: center; gap: 12rpx; margin-bottom: 32rpx; } |
| 289 | 454 | .badge-num { font-size: 40rpx; font-weight: 700; color: var(--theme-primary); } |
| 290 | 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 | 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 | 462 | .printer-card.connected { background: var(--theme-primary); border: none; } |
| 293 | 463 | .printer-card.connected .printer-name, .printer-card.connected .printer-loc, .printer-card.connected .printer-status { color: #fff; } |
| ... | ... | @@ -316,4 +486,10 @@ onUnmounted(() => { |
| 316 | 486 | |
| 317 | 487 | .empty-state { padding: 80rpx 0; text-align: center; } |
| 318 | 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 | 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 | 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 | 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 | 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 | 136 | shortToast (msg) { |
| 43 | 137 | // #ifdef APP-PLUS |
| 44 | - showToast(msg) | |
| 138 | + try { | |
| 139 | + if (activity) Toast.makeText(activity, String(msg), Toast.LENGTH_SHORT).show() | |
| 140 | + } catch (_) {} | |
| 45 | 141 | // #endif |
| 46 | 142 | }, |
| 143 | + isSupportBluetooth () { | |
| 144 | + // #ifdef APP-PLUS | |
| 145 | + return btAdapter != null | |
| 146 | + // #endif | |
| 147 | + return false | |
| 148 | + }, | |
| 47 | 149 | getBluetoothStatus () { |
| 48 | 150 | // #ifdef APP-PLUS |
| 49 | - const btAdapter = getAdapter() | |
| 50 | 151 | return btAdapter != null && btAdapter.isEnabled() |
| 51 | 152 | // #endif |
| 52 | 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 | 187 | getPairedDevices () { |
| 56 | 188 | // #ifdef APP-PLUS |
| 57 | - const pairedDevices = [] | |
| 189 | + let pairedDevices = [] | |
| 58 | 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 | 206 | } catch (e) { |
| 83 | 207 | console.error('getPairedDevices error:', e) |
| ... | ... | @@ -86,156 +210,360 @@ var blueToothTool = { |
| 86 | 210 | // #endif |
| 87 | 211 | return [] |
| 88 | 212 | }, |
| 89 | - /** 经典蓝牙扫描(发现未配对设备,如 d320fax_295c)— 不过滤任何设备 */ | |
| 90 | - startClassicDiscovery (onDeviceFound, onDiscoveryFinished) { | |
| 213 | + discoveryNewDevice () { | |
| 91 | 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 | 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 | 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 | 227 | if (BluetoothDevice.ACTION_FOUND === action) { |
| 108 | - const device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) | |
| 228 | + let device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) | |
| 109 | 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 | 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 | 247 | filter.addAction(BluetoothDevice.ACTION_FOUND) |
| 130 | 248 | filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED) |
| 131 | 249 | activity.registerReceiver(btFindReceiver, filter) |
| 132 | 250 | btAdapter.startDiscovery() |
| 251 | + this.state.discoveryDeviceState = true | |
| 133 | 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 | 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 | 303 | // #endif |
| 146 | 304 | }, |
| 147 | - /** 连接经典蓝牙设备(含 D320FAX Virtual BT Printer,支持 RFCOMM 回退) */ | |
| 148 | 305 | connDevice (address, callback) { |
| 149 | 306 | // #ifdef APP-PLUS |
| 307 | + this.cancelDiscovery() | |
| 150 | 308 | if (btSocket != null) this.closeBtSocket() |
| 151 | 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 | 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 | 320 | return false |
| 164 | 321 | } |
| 165 | 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 | 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 | 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 | 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 | 377 | return false |
| 196 | 378 | } |
| 197 | 379 | return true |
| 198 | 380 | // #endif |
| 199 | - if (callback) callback(false) | |
| 381 | + callback && callback(false) | |
| 200 | 382 | return false |
| 201 | 383 | }, |
| 202 | 384 | disConnDevice () { |
| 203 | 385 | // #ifdef APP-PLUS |
| 204 | - this.closeBtSocket() | |
| 386 | + if (btSocket != null) this.closeBtSocket() | |
| 205 | 387 | this.state.readThreadState = false |
| 206 | - this.shortToast('Disconnected') | |
| 207 | 388 | // #endif |
| 208 | 389 | }, |
| 209 | 390 | closeBtSocket () { |
| 210 | 391 | // #ifdef APP-PLUS |
| 211 | 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 | 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 | 475 | // #ifdef APP-PLUS |
| 222 | 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 | 493 | this.shortToast('Not connected') |
| 224 | 494 | return false |
| 225 | 495 | } |
| 226 | 496 | try { |
| 227 | - const CHUNK_SIZE = 4096 | |
| 497 | + const CHUNK_SIZE = 512 | |
| 498 | + this.state.lastSendMode = 'chunk-write' | |
| 499 | + this.state.lastSendError = '' | |
| 228 | 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 | 512 | return true |
| 233 | 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 | 518 | return false |
| 235 | 519 | } |
| 236 | 520 | // #endif |
| 237 | 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 | 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 | 2 | * 打印机连接与下发:蓝牙(BLE) / 一体机(TCP localhost) |
| 3 | 3 | */ |
| 4 | +import type { ActiveBtDeviceType, PrinterType } from './types/printer' | |
| 5 | +import classicBluetooth from './bluetoothTool.js' | |
| 4 | 6 | |
| 5 | 7 | const STORAGE_PRINTER_TYPE = 'printerType' |
| 6 | 8 | const STORAGE_BT_DEVICE_ID = 'btDeviceId' |
| ... | ... | @@ -9,9 +11,11 @@ const STORAGE_BT_SERVICE_ID = 'btServiceId' |
| 9 | 11 | const STORAGE_BT_CHARACTERISTIC_ID = 'btCharacteristicId' |
| 10 | 12 | const STORAGE_BT_DEVICE_TYPE = 'btDeviceType' // 'ble' | 'classic' |
| 11 | 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 | 20 | export const PrinterStorageKeys = { |
| 17 | 21 | type: STORAGE_PRINTER_TYPE, |
| ... | ... | @@ -21,6 +25,7 @@ export const PrinterStorageKeys = { |
| 21 | 25 | btCharacteristicId: STORAGE_BT_CHARACTERISTIC_ID, |
| 22 | 26 | btDeviceType: STORAGE_BT_DEVICE_TYPE, |
| 23 | 27 | bleMTU: STORAGE_BLE_MTU, |
| 28 | + driverKey: STORAGE_PRINTER_DRIVER_KEY, | |
| 24 | 29 | } as const |
| 25 | 30 | |
| 26 | 31 | export function setPrinterType (type: PrinterType) { |
| ... | ... | @@ -34,6 +39,7 @@ export function setBluetoothConnection (info: { |
| 34 | 39 | characteristicId?: string |
| 35 | 40 | deviceType?: BtDeviceType |
| 36 | 41 | mtu?: number |
| 42 | + driverKey?: string | |
| 37 | 43 | }) { |
| 38 | 44 | uni.setStorageSync(STORAGE_PRINTER_TYPE, 'bluetooth') |
| 39 | 45 | uni.setStorageSync(STORAGE_BT_DEVICE_ID, info.deviceId) |
| ... | ... | @@ -42,10 +48,12 @@ export function setBluetoothConnection (info: { |
| 42 | 48 | uni.setStorageSync(STORAGE_BT_CHARACTERISTIC_ID, info.characteristicId || '') |
| 43 | 49 | uni.setStorageSync(STORAGE_BT_DEVICE_TYPE, info.deviceType || 'ble') |
| 44 | 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 | 55 | uni.setStorageSync(STORAGE_PRINTER_TYPE, 'builtin') |
| 56 | + uni.setStorageSync(STORAGE_PRINTER_DRIVER_KEY, driverKey) | |
| 49 | 57 | } |
| 50 | 58 | |
| 51 | 59 | export function clearPrinter () { |
| ... | ... | @@ -56,6 +64,8 @@ export function clearPrinter () { |
| 56 | 64 | uni.removeStorageSync(STORAGE_BT_CHARACTERISTIC_ID) |
| 57 | 65 | uni.removeStorageSync(STORAGE_BT_DEVICE_TYPE) |
| 58 | 66 | uni.removeStorageSync(STORAGE_BLE_MTU) |
| 67 | + uni.removeStorageSync(STORAGE_BUILTIN_PORT) | |
| 68 | + uni.removeStorageSync(STORAGE_PRINTER_DRIVER_KEY) | |
| 59 | 69 | } |
| 60 | 70 | |
| 61 | 71 | const BLE_MTU_DEFAULT = 20 |
| ... | ... | @@ -64,6 +74,10 @@ export function getPrinterType (): PrinterType | '' { |
| 64 | 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 | 81 | export function getBluetoothConnection (): { |
| 68 | 82 | deviceId: string |
| 69 | 83 | deviceName: string |
| ... | ... | @@ -182,20 +196,31 @@ function sendViaClassic ( |
| 182 | 196 | } |
| 183 | 197 | return new Promise((resolve, reject) => { |
| 184 | 198 | try { |
| 185 | - const classicBluetooth = (require('./bluetoothTool.js') as any).default | |
| 186 | 199 | if (!classicBluetooth) { |
| 187 | 200 | reject(new Error('Classic Bluetooth not available')) |
| 188 | 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 | 215 | const ok = classicBluetooth.sendByteData(sendData) |
| 196 | 216 | if (onProgress) onProgress(100) |
| 197 | 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 | 224 | } catch (e: any) { |
| 200 | 225 | reject(e) |
| 201 | 226 | } |
| ... | ... | @@ -219,24 +244,46 @@ function sendViaBuiltin (data: number[]): Promise<void> { |
| 219 | 244 | const hexStr = Array.from(uint8) |
| 220 | 245 | .map(b => ('0' + (b & 0xff).toString(16)).slice(-2)) |
| 221 | 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 | 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 | 287 | } catch (e) { |
| 241 | 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 | /// </summary> |
| 6 | 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 | 1 | using FoodLabeling.Application.Contracts.Dtos.RbacRole; |
| 2 | 2 | using FoodLabeling.Application.Contracts.Dtos.Common; |
| 3 | 3 | using FoodLabeling.Application.Contracts.IServices; |
| 4 | +using FoodLabeling.Application.Services.DbModels; | |
| 4 | 5 | using Microsoft.AspNetCore.Mvc; |
| 5 | 6 | using SqlSugar; |
| 6 | 7 | using Volo.Abp; |
| ... | ... | @@ -17,17 +18,20 @@ namespace FoodLabeling.Application.Services; |
| 17 | 18 | /// </summary> |
| 18 | 19 | public class RbacRoleAppService : ApplicationService, IRbacRoleAppService |
| 19 | 20 | { |
| 21 | + private readonly ISqlSugarDbContext _dbContext; | |
| 20 | 22 | private readonly ISqlSugarRepository<RoleAggregateRoot, Guid> _roleRepository; |
| 21 | 23 | private readonly ISqlSugarRepository<RoleMenuEntity> _roleMenuRepository; |
| 22 | 24 | private readonly ISqlSugarRepository<RoleDeptEntity> _roleDeptRepository; |
| 23 | 25 | private readonly ISqlSugarRepository<UserRoleEntity> _userRoleRepository; |
| 24 | 26 | |
| 25 | 27 | public RbacRoleAppService( |
| 28 | + ISqlSugarDbContext dbContext, | |
| 26 | 29 | ISqlSugarRepository<RoleAggregateRoot, Guid> roleRepository, |
| 27 | 30 | ISqlSugarRepository<RoleMenuEntity> roleMenuRepository, |
| 28 | 31 | ISqlSugarRepository<RoleDeptEntity> roleDeptRepository, |
| 29 | 32 | ISqlSugarRepository<UserRoleEntity> userRoleRepository) |
| 30 | 33 | { |
| 34 | + _dbContext = dbContext; | |
| 31 | 35 | _roleRepository = roleRepository; |
| 32 | 36 | _roleMenuRepository = roleMenuRepository; |
| 33 | 37 | _roleDeptRepository = roleDeptRepository; |
| ... | ... | @@ -90,6 +94,11 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService |
| 90 | 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 | 102 | return new RbacRoleGetOutputDto |
| 94 | 103 | { |
| 95 | 104 | Id = entity.Id, |
| ... | ... | @@ -98,7 +107,8 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService |
| 98 | 107 | Remark = entity.Remark, |
| 99 | 108 | DataScope = (int)entity.DataScope, |
| 100 | 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 | + | ... | ... |