| 
									
										
										
										
											2025-08-25 11:09:23 +08:00
										 |  |  |  | // lib/services/nfc_service.dart
 | 
					
						
							|  |  |  |  | //
 | 
					
						
							|  |  |  |  | // NFC 封装服务(包含读取和写入方法,均提供回调与 Future 两种使用方式)
 | 
					
						
							|  |  |  |  | // 请将此文件放到你的项目中(例如 lib/services/nfc_service.dart)并直接使用。
 | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | import 'dart:async'; | 
					
						
							|  |  |  |  | import 'dart:convert'; | 
					
						
							|  |  |  |  | import 'dart:io'; | 
					
						
							|  |  |  |  | import 'dart:typed_data'; | 
					
						
							|  |  |  |  | import 'package:flutter/foundation.dart'; | 
					
						
							|  |  |  |  | import 'package:nfc_manager/nfc_manager.dart'; | 
					
						
							|  |  |  |  | import 'package:nfc_manager/ndef_record.dart'; | 
					
						
							|  |  |  |  | import 'package:nfc_manager/nfc_manager_android.dart'; | 
					
						
							|  |  |  |  | import 'package:nfc_manager/nfc_manager_ios.dart'; | 
					
						
							|  |  |  |  | import 'package:nfc_manager_ndef/nfc_manager_ndef.dart'; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | /// NfcService 单例:提供 NFC 的读取与写入基础能力
 | 
					
						
							|  |  |  |  | ///
 | 
					
						
							|  |  |  |  | /// - startScanOnceWithCallback(...):回调式的一次性读取(发现标签即回调)
 | 
					
						
							|  |  |  |  | /// - readOnceText(...):Future 式读取,返回解析出的文本(若失败会抛异常)
 | 
					
						
							|  |  |  |  | /// - writeText(...):写入文本记录到标签(回调/返回写入结果)
 | 
					
						
							|  |  |  |  | /// - stopSession():停止当前会话
 | 
					
						
							|  |  |  |  | ///
 | 
					
						
							|  |  |  |  | /// 注:iOS 必须在真机测试,并在 Xcode 中打开 NFC 权限;Android 需设备支持 NFC。
 | 
					
						
							|  |  |  |  | class NfcService { | 
					
						
							|  |  |  |  |   NfcService._internal(); | 
					
						
							| 
									
										
										
										
											2025-08-27 16:14:50 +08:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-25 11:09:23 +08:00
										 |  |  |  |   static final NfcService instance = NfcService._internal(); | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   /// 是否正在进行 NFC 会话(扫描或写入)
 | 
					
						
							|  |  |  |  |   final ValueNotifier<bool> scanning = ValueNotifier(false); | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   /// 日志广播流(方便 UI 订阅)
 | 
					
						
							|  |  |  |  |   final StreamController<String> _logController = StreamController.broadcast(); | 
					
						
							| 
									
										
										
										
											2025-08-27 16:14:50 +08:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-25 11:09:23 +08:00
										 |  |  |  |   Stream<String> get logs => _logController.stream; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   /// 检查设备是否支持 NFC
 | 
					
						
							|  |  |  |  |   Future<bool> isAvailable() => NfcManager.instance.isAvailable(); | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   /// 停止会话(若正在运行)
 | 
					
						
							|  |  |  |  |   Future<void> stopSession() async { | 
					
						
							|  |  |  |  |     if (scanning.value) { | 
					
						
							|  |  |  |  |       try { | 
					
						
							|  |  |  |  |         await NfcManager.instance.stopSession(); | 
					
						
							|  |  |  |  |       } catch (_) {} | 
					
						
							|  |  |  |  |       scanning.value = false; | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   // ---------------- 辅助方法 ----------------
 | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   /// bytes -> "AA:BB:CC" 形式的大写十六进制字符串
 | 
					
						
							|  |  |  |  |   String _bytesToHex(Uint8List bytes) { | 
					
						
							| 
									
										
										
										
											2025-08-27 16:14:50 +08:00
										 |  |  |  |     return bytes | 
					
						
							|  |  |  |  |         .map((b) => b.toRadixString(16).padLeft(2, '0')) | 
					
						
							|  |  |  |  |         .join(':') | 
					
						
							|  |  |  |  |         .toUpperCase(); | 
					
						
							| 
									
										
										
										
											2025-08-25 11:09:23 +08:00
										 |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   /// 递归在 Map/List 中查找第一个可用的字节数组 (List<int> / Uint8List)
 | 
					
						
							|  |  |  |  |   List<int>? _findFirstByteArray(dynamic value) { | 
					
						
							|  |  |  |  |     if (value == null) return null; | 
					
						
							|  |  |  |  |     if (value is Uint8List) return value.toList(); | 
					
						
							|  |  |  |  |     if (value is List<int>) return value; | 
					
						
							|  |  |  |  |     if (value is List) { | 
					
						
							|  |  |  |  |       for (var item in value) { | 
					
						
							|  |  |  |  |         final found = _findFirstByteArray(item); | 
					
						
							|  |  |  |  |         if (found != null) return found; | 
					
						
							|  |  |  |  |       } | 
					
						
							|  |  |  |  |       return null; | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  |     if (value is Map) { | 
					
						
							|  |  |  |  |       for (final entry in value.entries) { | 
					
						
							|  |  |  |  |         final found = _findFirstByteArray(entry.value); | 
					
						
							|  |  |  |  |         if (found != null) return found; | 
					
						
							|  |  |  |  |       } | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  |     return null; | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   /// 从 tag(NfcTag / Map)中提取 UID,兼容多种实现
 | 
					
						
							|  |  |  |  |   String extractUidFromTag(dynamic tag) { | 
					
						
							|  |  |  |  |     try { | 
					
						
							|  |  |  |  |       dynamic raw = tag; | 
					
						
							|  |  |  |  |       // 有些实现把数据放 data 字段
 | 
					
						
							|  |  |  |  |       if (tag is Map && tag.containsKey('data')) raw = tag['data']; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |       final bytes = _findFirstByteArray(raw); | 
					
						
							|  |  |  |  |       if (bytes != null && bytes.isNotEmpty) { | 
					
						
							|  |  |  |  |         return _bytesToHex(Uint8List.fromList(bytes)); | 
					
						
							|  |  |  |  |       } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |       // 再尝试一些常见的键名
 | 
					
						
							|  |  |  |  |       if (tag is Map) { | 
					
						
							|  |  |  |  |         final possible = ['id', 'identifier', 'Id', 'ID']; | 
					
						
							|  |  |  |  |         for (final k in possible) { | 
					
						
							|  |  |  |  |           if (tag[k] != null) { | 
					
						
							|  |  |  |  |             final b = _findFirstByteArray(tag[k]); | 
					
						
							|  |  |  |  |             if (b != null) return _bytesToHex(Uint8List.fromList(b)); | 
					
						
							|  |  |  |  |           } | 
					
						
							|  |  |  |  |         } | 
					
						
							|  |  |  |  |       } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |       // 最后一招:平台特定 API (如果存在)
 | 
					
						
							|  |  |  |  |       if (Platform.isAndroid) { | 
					
						
							|  |  |  |  |         try { | 
					
						
							|  |  |  |  |           final nfcA = NfcAAndroid.from(tag); | 
					
						
							| 
									
										
										
										
											2025-08-27 16:14:50 +08:00
										 |  |  |  |           if (nfcA != null && nfcA.tag.id != null) | 
					
						
							|  |  |  |  |             return _bytesToHex(Uint8List.fromList(nfcA.tag.id)); | 
					
						
							| 
									
										
										
										
											2025-08-25 11:09:23 +08:00
										 |  |  |  |         } catch (_) {} | 
					
						
							|  |  |  |  |       } else if (Platform.isIOS) { | 
					
						
							|  |  |  |  |         try { | 
					
						
							|  |  |  |  |           final mifare = MiFareIos.from(tag); | 
					
						
							| 
									
										
										
										
											2025-08-27 16:14:50 +08:00
										 |  |  |  |           if (mifare != null && mifare.identifier != null) | 
					
						
							|  |  |  |  |             return _bytesToHex(Uint8List.fromList(mifare.identifier)); | 
					
						
							| 
									
										
										
										
											2025-08-25 11:09:23 +08:00
										 |  |  |  |         } catch (_) {} | 
					
						
							|  |  |  |  |       } | 
					
						
							|  |  |  |  |     } catch (e) { | 
					
						
							|  |  |  |  |       debugPrint('extractUidFromTag error: $e'); | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  |     return 'UNKNOWN'; | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   /// 从 NDEF record 的 payload 解析文本(Text Record)并返回字符串
 | 
					
						
							|  |  |  |  |   String parseTextFromPayload(Uint8List payload) { | 
					
						
							|  |  |  |  |     if (payload.isEmpty) return ''; | 
					
						
							|  |  |  |  |     final status = payload[0]; | 
					
						
							|  |  |  |  |     final langLen = status & 0x3F; | 
					
						
							|  |  |  |  |     final textBytes = payload.sublist(1 + langLen); | 
					
						
							|  |  |  |  |     try { | 
					
						
							|  |  |  |  |       return utf8.decode(textBytes); | 
					
						
							|  |  |  |  |     } catch (e) { | 
					
						
							|  |  |  |  |       return String.fromCharCodes(textBytes); | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   String _formatMessageToString(dynamic msg) { | 
					
						
							|  |  |  |  |     if (msg == null) return '<empty>'; | 
					
						
							|  |  |  |  |     try { | 
					
						
							| 
									
										
										
										
											2025-08-27 16:14:50 +08:00
										 |  |  |  |       final records = | 
					
						
							|  |  |  |  |           (msg is Map && msg['records'] != null) | 
					
						
							|  |  |  |  |               ? List<dynamic>.from(msg['records']) | 
					
						
							|  |  |  |  |               : (msg is dynamic ? (msg.records as List<dynamic>?) : null); | 
					
						
							| 
									
										
										
										
											2025-08-25 11:09:23 +08:00
										 |  |  |  |       if (records == null || records.isEmpty) return '<empty>'; | 
					
						
							|  |  |  |  |       final sb = StringBuffer(); | 
					
						
							|  |  |  |  |       for (var i = 0; i < records.length; i++) { | 
					
						
							|  |  |  |  |         final r = records[i]; | 
					
						
							|  |  |  |  |         sb.writeln('Record $i: ${r.toString()}'); | 
					
						
							|  |  |  |  |         try { | 
					
						
							|  |  |  |  |           // 解析 payload
 | 
					
						
							|  |  |  |  |           Uint8List? p; | 
					
						
							|  |  |  |  |           if (r is Map && r['payload'] != null) { | 
					
						
							|  |  |  |  |             final ptmp = r['payload']; | 
					
						
							| 
									
										
										
										
											2025-08-27 16:14:50 +08:00
										 |  |  |  |             if (ptmp is Uint8List) | 
					
						
							|  |  |  |  |               p = ptmp; | 
					
						
							|  |  |  |  |             else if (ptmp is List<int>) | 
					
						
							|  |  |  |  |               p = Uint8List.fromList(ptmp); | 
					
						
							| 
									
										
										
										
											2025-08-25 11:09:23 +08:00
										 |  |  |  |           } else { | 
					
						
							|  |  |  |  |             final pr = (r as dynamic).payload; | 
					
						
							| 
									
										
										
										
											2025-08-27 16:14:50 +08:00
										 |  |  |  |             if (pr is Uint8List) | 
					
						
							|  |  |  |  |               p = pr; | 
					
						
							|  |  |  |  |             else if (pr is List<int>) | 
					
						
							|  |  |  |  |               p = Uint8List.fromList(pr); | 
					
						
							| 
									
										
										
										
											2025-08-25 11:09:23 +08:00
										 |  |  |  |           } | 
					
						
							|  |  |  |  |           if (p != null) { | 
					
						
							|  |  |  |  |             final txt = parseTextFromPayload(p); | 
					
						
							|  |  |  |  |             sb.writeln('  text: $txt'); | 
					
						
							|  |  |  |  |           } | 
					
						
							|  |  |  |  |         } catch (_) {} | 
					
						
							|  |  |  |  |       } | 
					
						
							|  |  |  |  |       return sb.toString(); | 
					
						
							|  |  |  |  |     } catch (e) { | 
					
						
							|  |  |  |  |       return msg.toString(); | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   // ---------------- 读取 API ----------------
 | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   /// 回调式的一次性扫描(发现标签后立即回调)
 | 
					
						
							|  |  |  |  |   ///
 | 
					
						
							|  |  |  |  |   /// onResult(uid, parsedText, rawMessage) - 当发现标签时回调
 | 
					
						
							|  |  |  |  |   /// onError(error) - 出错时回调
 | 
					
						
							|  |  |  |  |   /// timeout - 超时时间(可选)
 | 
					
						
							|  |  |  |  |   Future<void> startScanOnceWithCallback({ | 
					
						
							| 
									
										
										
										
											2025-08-27 16:14:50 +08:00
										 |  |  |  |     required void Function(String uid, String parsedText, dynamic rawMessage) | 
					
						
							|  |  |  |  |     onResult, | 
					
						
							| 
									
										
										
										
											2025-08-25 11:09:23 +08:00
										 |  |  |  |     void Function(Object error)? onError, | 
					
						
							|  |  |  |  |     Duration? timeout, | 
					
						
							|  |  |  |  |   }) async { | 
					
						
							|  |  |  |  |     final available = await isAvailable(); | 
					
						
							|  |  |  |  |     if (!available) { | 
					
						
							|  |  |  |  |       onError?.call('NFC not available'); | 
					
						
							|  |  |  |  |       return; | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  |     if (scanning.value) { | 
					
						
							|  |  |  |  |       onError?.call('Another session running'); | 
					
						
							|  |  |  |  |       return; | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     scanning.value = true; | 
					
						
							|  |  |  |  |     _logController.add('NFC scanning started'); | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     Timer? timer; | 
					
						
							|  |  |  |  |     if (timeout != null) { | 
					
						
							|  |  |  |  |       timer = Timer(timeout, () async { | 
					
						
							|  |  |  |  |         await stopSession(); | 
					
						
							|  |  |  |  |         _logController.add('NFC scanning timeout'); | 
					
						
							|  |  |  |  |         onError?.call('timeout'); | 
					
						
							|  |  |  |  |       }); | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     try { | 
					
						
							|  |  |  |  |       await NfcManager.instance.startSession( | 
					
						
							|  |  |  |  |         onDiscovered: (dynamic tag) async { | 
					
						
							|  |  |  |  |           try { | 
					
						
							|  |  |  |  |             final uid = extractUidFromTag(tag); | 
					
						
							|  |  |  |  |             dynamic rawMsg; | 
					
						
							|  |  |  |  |             String parsedText = ''; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |             // 尝试用 Ndef.from 获取消息
 | 
					
						
							|  |  |  |  |             try { | 
					
						
							|  |  |  |  |               final ndef = Ndef.from(tag); | 
					
						
							|  |  |  |  |               if (ndef != null) { | 
					
						
							|  |  |  |  |                 rawMsg = ndef.cachedMessage; | 
					
						
							|  |  |  |  |                 if (rawMsg != null) { | 
					
						
							|  |  |  |  |                   // 解析第一条文本记录(若存在)
 | 
					
						
							|  |  |  |  |                   if ((rawMsg.records as List).isNotEmpty) { | 
					
						
							|  |  |  |  |                     final first = rawMsg.records.first; | 
					
						
							|  |  |  |  |                     final payload = first.payload; | 
					
						
							| 
									
										
										
										
											2025-08-27 16:14:50 +08:00
										 |  |  |  |                     if (payload is Uint8List) | 
					
						
							|  |  |  |  |                       parsedText = parseTextFromPayload(payload); | 
					
						
							|  |  |  |  |                     else if (payload is List<int>) | 
					
						
							|  |  |  |  |                       parsedText = parseTextFromPayload( | 
					
						
							|  |  |  |  |                         Uint8List.fromList(payload), | 
					
						
							|  |  |  |  |                       ); | 
					
						
							| 
									
										
										
										
											2025-08-25 11:09:23 +08:00
										 |  |  |  |                   } | 
					
						
							|  |  |  |  |                 } | 
					
						
							|  |  |  |  |               } else { | 
					
						
							|  |  |  |  |                 rawMsg = null; | 
					
						
							|  |  |  |  |               } | 
					
						
							|  |  |  |  |             } catch (_) { | 
					
						
							|  |  |  |  |               // 回退:尝试从 tag map 中嗅探 cachedMessage / records
 | 
					
						
							|  |  |  |  |               try { | 
					
						
							|  |  |  |  |                 if (tag is Map) { | 
					
						
							| 
									
										
										
										
											2025-08-27 16:14:50 +08:00
										 |  |  |  |                   rawMsg = | 
					
						
							|  |  |  |  |                       tag['cachedMessage'] ?? | 
					
						
							|  |  |  |  |                       tag['ndef']?['cachedMessage'] ?? | 
					
						
							|  |  |  |  |                       tag['message']; | 
					
						
							| 
									
										
										
										
											2025-08-25 11:09:23 +08:00
										 |  |  |  |                   if (rawMsg != null) { | 
					
						
							| 
									
										
										
										
											2025-08-27 16:14:50 +08:00
										 |  |  |  |                     final recs = | 
					
						
							|  |  |  |  |                         (rawMsg['records'] ?? (rawMsg as dynamic).records) | 
					
						
							|  |  |  |  |                             as dynamic; | 
					
						
							| 
									
										
										
										
											2025-08-25 11:09:23 +08:00
										 |  |  |  |                     if (recs != null && recs is List && recs.isNotEmpty) { | 
					
						
							|  |  |  |  |                       final r = recs.first; | 
					
						
							| 
									
										
										
										
											2025-08-27 16:14:50 +08:00
										 |  |  |  |                       final p = | 
					
						
							|  |  |  |  |                           (r is Map) ? r['payload'] : (r as dynamic).payload; | 
					
						
							|  |  |  |  |                       if (p is Uint8List) | 
					
						
							|  |  |  |  |                         parsedText = parseTextFromPayload(p); | 
					
						
							|  |  |  |  |                       else if (p is List<int>) | 
					
						
							|  |  |  |  |                         parsedText = parseTextFromPayload( | 
					
						
							|  |  |  |  |                           Uint8List.fromList(p), | 
					
						
							|  |  |  |  |                         ); | 
					
						
							| 
									
										
										
										
											2025-08-25 11:09:23 +08:00
										 |  |  |  |                     } | 
					
						
							|  |  |  |  |                   } | 
					
						
							|  |  |  |  |                 } | 
					
						
							|  |  |  |  |               } catch (_) {} | 
					
						
							|  |  |  |  |             } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |             // 回调结果
 | 
					
						
							|  |  |  |  |             onResult(uid, parsedText, rawMsg); | 
					
						
							| 
									
										
										
										
											2025-08-27 16:14:50 +08:00
										 |  |  |  |             _logController.add( | 
					
						
							|  |  |  |  |               'UID: $uid\nmessage: ${_formatMessageToString(rawMsg)}', | 
					
						
							|  |  |  |  |             ); | 
					
						
							| 
									
										
										
										
											2025-08-25 11:09:23 +08:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  |             await stopSession(); | 
					
						
							|  |  |  |  |           } catch (e) { | 
					
						
							|  |  |  |  |             onError?.call(e); | 
					
						
							|  |  |  |  |             await stopSession(); | 
					
						
							|  |  |  |  |           } finally { | 
					
						
							|  |  |  |  |             timer?.cancel(); | 
					
						
							|  |  |  |  |             scanning.value = false; | 
					
						
							|  |  |  |  |           } | 
					
						
							|  |  |  |  |         }, | 
					
						
							|  |  |  |  |         // 尽量多协议尝试以提升兼容性
 | 
					
						
							|  |  |  |  |         pollingOptions: {NfcPollingOption.iso14443}, | 
					
						
							|  |  |  |  |       ); | 
					
						
							|  |  |  |  |     } catch (e) { | 
					
						
							|  |  |  |  |       scanning.value = false; | 
					
						
							|  |  |  |  |       timer?.cancel(); | 
					
						
							|  |  |  |  |       onError?.call(e); | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   /// Future 式读取:解析第一条文本记录并返回字符串(格式:'UID\n文本')
 | 
					
						
							| 
									
										
										
										
											2025-08-27 16:14:50 +08:00
										 |  |  |  |   Future<String> readOnceText({ | 
					
						
							|  |  |  |  |     Duration timeout = const Duration(seconds: 10), | 
					
						
							|  |  |  |  |   }) async { | 
					
						
							| 
									
										
										
										
											2025-08-25 11:09:23 +08:00
										 |  |  |  |     final completer = Completer<String>(); | 
					
						
							|  |  |  |  |     await startScanOnceWithCallback( | 
					
						
							|  |  |  |  |       onResult: (uid, parsedText, rawMsg) { | 
					
						
							|  |  |  |  |         if (parsedText.isEmpty) { | 
					
						
							|  |  |  |  |           completer.completeError('No text record found (UID: $uid)'); | 
					
						
							|  |  |  |  |         } else { | 
					
						
							|  |  |  |  |           completer.complete('UID: $uid\n$parsedText'); | 
					
						
							|  |  |  |  |         } | 
					
						
							|  |  |  |  |       }, | 
					
						
							|  |  |  |  |       onError: (err) { | 
					
						
							|  |  |  |  |         if (!completer.isCompleted) completer.completeError(err); | 
					
						
							|  |  |  |  |       }, | 
					
						
							|  |  |  |  |       timeout: timeout, | 
					
						
							|  |  |  |  |     ); | 
					
						
							|  |  |  |  |     return completer.future; | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   // ---------------- 写入 API ----------------
 | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   /// 构造 NDEF Text payload(status byte + lang + utf8 bytes)
 | 
					
						
							|  |  |  |  |   Uint8List _buildTextPayload(String text, {String lang = 'en'}) { | 
					
						
							|  |  |  |  |     final textBytes = utf8.encode(text); | 
					
						
							|  |  |  |  |     final langBytes = utf8.encode(lang); | 
					
						
							|  |  |  |  |     final status = langBytes.length & 0x3F; | 
					
						
							|  |  |  |  |     final payload = <int>[status, ...langBytes, ...textBytes]; | 
					
						
							|  |  |  |  |     return Uint8List.fromList(payload); | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   /// 写入文本到标签(单条 Text record)
 | 
					
						
							|  |  |  |  |   ///
 | 
					
						
							|  |  |  |  |   /// - text: 要写入的文本
 | 
					
						
							|  |  |  |  |   /// - timeout: 超时时间(可选)
 | 
					
						
							|  |  |  |  |   /// - onComplete: 回调 (ok, err)
 | 
					
						
							|  |  |  |  |   ///
 | 
					
						
							|  |  |  |  |   /// 返回 true 表示写入成功(同时 onComplete 也会被触发)
 | 
					
						
							| 
									
										
										
										
											2025-08-27 16:14:50 +08:00
										 |  |  |  |   Future<bool> writeText( | 
					
						
							|  |  |  |  |     String text, { | 
					
						
							|  |  |  |  |     Duration? timeout, | 
					
						
							|  |  |  |  |     void Function(bool ok, Object? err)? onComplete, | 
					
						
							|  |  |  |  |   }) async { | 
					
						
							|  |  |  |  |     debugPrint('writeText called - scanning=${scanning.value}'); | 
					
						
							| 
									
										
										
										
											2025-08-25 11:09:23 +08:00
										 |  |  |  |     final available = await isAvailable(); | 
					
						
							| 
									
										
										
										
											2025-08-27 16:14:50 +08:00
										 |  |  |  |     debugPrint('writeText: isAvailable=$available'); | 
					
						
							| 
									
										
										
										
											2025-08-25 11:09:23 +08:00
										 |  |  |  |     if (!available) { | 
					
						
							|  |  |  |  |       onComplete?.call(false, 'NFC not available'); | 
					
						
							|  |  |  |  |       return false; | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  |     if (scanning.value) { | 
					
						
							|  |  |  |  |       onComplete?.call(false, 'Another session running'); | 
					
						
							|  |  |  |  |       return false; | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-27 16:14:50 +08:00
										 |  |  |  |     try { | 
					
						
							|  |  |  |  |       await NfcManager.instance.stopSession(); | 
					
						
							|  |  |  |  |     } catch (e) { | 
					
						
							|  |  |  |  |       debugPrint('stopSession ignore: $e'); | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-25 11:09:23 +08:00
										 |  |  |  |     scanning.value = true; | 
					
						
							|  |  |  |  |     Timer? timer; | 
					
						
							|  |  |  |  |     if (timeout != null) { | 
					
						
							|  |  |  |  |       timer = Timer(timeout, () async { | 
					
						
							|  |  |  |  |         await stopSession(); | 
					
						
							|  |  |  |  |         scanning.value = false; | 
					
						
							|  |  |  |  |         onComplete?.call(false, 'timeout'); | 
					
						
							|  |  |  |  |       }); | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     bool success = false; | 
					
						
							|  |  |  |  |     try { | 
					
						
							| 
									
										
										
										
											2025-08-27 16:14:50 +08:00
										 |  |  |  |       // 修改了 pollingOptions 配置
 | 
					
						
							|  |  |  |  |       final polling = | 
					
						
							|  |  |  |  |           Platform.isIOS | 
					
						
							|  |  |  |  |               ? {NfcPollingOption.iso14443} // iOS 使用更具体的配置
 | 
					
						
							|  |  |  |  |               : { | 
					
						
							|  |  |  |  |                 NfcPollingOption.iso14443, | 
					
						
							|  |  |  |  |                 NfcPollingOption.iso15693, | 
					
						
							|  |  |  |  |                 NfcPollingOption.iso18092, | 
					
						
							|  |  |  |  |               }; | 
					
						
							| 
									
										
										
										
											2025-08-25 11:09:23 +08:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-27 16:14:50 +08:00
										 |  |  |  |       await NfcManager.instance.startSession( | 
					
						
							|  |  |  |  |         pollingOptions: polling, | 
					
						
							|  |  |  |  |         onDiscovered: (dynamic tag) async { | 
					
						
							|  |  |  |  |           timer?.cancel(); | 
					
						
							|  |  |  |  |           try { | 
					
						
							|  |  |  |  |             final ndef = Ndef.from(tag); | 
					
						
							|  |  |  |  |             if (ndef == null) { | 
					
						
							|  |  |  |  |               onComplete?.call(false, 'Tag not NDEF'); | 
					
						
							|  |  |  |  |               await stopSession(); | 
					
						
							|  |  |  |  |               scanning.value = false; | 
					
						
							|  |  |  |  |               return; | 
					
						
							|  |  |  |  |             } | 
					
						
							| 
									
										
										
										
											2025-08-25 11:09:23 +08:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-27 16:14:50 +08:00
										 |  |  |  |             // 检查是否可写
 | 
					
						
							|  |  |  |  |             if (Platform.isIOS) { | 
					
						
							|  |  |  |  |               final miFare = MiFareIos.from(tag); | 
					
						
							|  |  |  |  |               if (miFare == null) { | 
					
						
							|  |  |  |  |                 onComplete?.call(false, 'Unsupported tag type on iOS'); | 
					
						
							|  |  |  |  |                 await stopSession(); | 
					
						
							|  |  |  |  |                 scanning.value = false; | 
					
						
							|  |  |  |  |                 return; | 
					
						
							| 
									
										
										
										
											2025-08-25 11:09:23 +08:00
										 |  |  |  |               } | 
					
						
							| 
									
										
										
										
											2025-08-27 16:14:50 +08:00
										 |  |  |  |             } | 
					
						
							| 
									
										
										
										
											2025-08-25 11:09:23 +08:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-27 16:14:50 +08:00
										 |  |  |  |             final payload = _buildTextPayload(text, lang: 'en'); | 
					
						
							|  |  |  |  |             final record = NdefRecord( | 
					
						
							|  |  |  |  |               typeNameFormat: TypeNameFormat.wellKnown, | 
					
						
							|  |  |  |  |               type: Uint8List.fromList('T'.codeUnits), | 
					
						
							|  |  |  |  |               identifier: Uint8List(0), | 
					
						
							|  |  |  |  |               payload: payload, | 
					
						
							|  |  |  |  |             ); | 
					
						
							|  |  |  |  |             final message = NdefMessage(records: [record]); | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |             await ndef.write(message: message); | 
					
						
							|  |  |  |  |             // 取出 UID (优先取 nfca.identifier,如果没有就取顶层 id)
 | 
					
						
							|  |  |  |  |             String? uid; | 
					
						
							|  |  |  |  |             if (Platform.isAndroid) { | 
					
						
							|  |  |  |  |               try { | 
					
						
							|  |  |  |  |                 final nfcA = NfcAAndroid.from(tag); | 
					
						
							|  |  |  |  |                 if (nfcA != null && nfcA.tag.id != null) { | 
					
						
							|  |  |  |  |                   uid = _bytesToHex(Uint8List.fromList(nfcA.tag.id)); | 
					
						
							|  |  |  |  |                 } | 
					
						
							|  |  |  |  |               } catch (_) {} | 
					
						
							|  |  |  |  |             } else if (Platform.isIOS) { | 
					
						
							|  |  |  |  |               try { | 
					
						
							|  |  |  |  |                 final mifare = MiFareIos.from(tag); | 
					
						
							|  |  |  |  |                 if (mifare != null && mifare.identifier != null) { | 
					
						
							|  |  |  |  |                   uid = _bytesToHex(Uint8List.fromList(mifare.identifier)); | 
					
						
							|  |  |  |  |                 } | 
					
						
							|  |  |  |  |               } catch (_) {} | 
					
						
							|  |  |  |  |             } | 
					
						
							|  |  |  |  |             success = true; | 
					
						
							|  |  |  |  |             onComplete?.call(true, uid); // ✅ 把 UID 返回出去
 | 
					
						
							|  |  |  |  |           } catch (e, st) { | 
					
						
							|  |  |  |  |             debugPrint('iOS NFC Write Error: $e'); | 
					
						
							|  |  |  |  |             debugPrint('Stack trace: $st'); | 
					
						
							|  |  |  |  |             onComplete?.call(false, e); | 
					
						
							|  |  |  |  |           } finally { | 
					
						
							|  |  |  |  |             await stopSession(); | 
					
						
							|  |  |  |  |             scanning.value = false; | 
					
						
							|  |  |  |  |           } | 
					
						
							|  |  |  |  |         }, | 
					
						
							|  |  |  |  |       ); | 
					
						
							|  |  |  |  |     } catch (e, st) { | 
					
						
							|  |  |  |  |       debugPrint('startSession exception: $e\n$st'); | 
					
						
							| 
									
										
										
										
											2025-08-25 11:09:23 +08:00
										 |  |  |  |       timer?.cancel(); | 
					
						
							| 
									
										
										
										
											2025-08-27 16:14:50 +08:00
										 |  |  |  |       scanning.value = false; | 
					
						
							| 
									
										
										
										
											2025-08-25 11:09:23 +08:00
										 |  |  |  |       onComplete?.call(false, e); | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  |     return success; | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   /// 释放资源
 | 
					
						
							|  |  |  |  |   void dispose() { | 
					
						
							|  |  |  |  |     try { | 
					
						
							|  |  |  |  |       _logController.close(); | 
					
						
							|  |  |  |  |     } catch (_) {} | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | } |