NFC巡检部分
							parent
							
								
									2495e4b9d4
								
							
						
					
					
						commit
						6fd181636e
					
				|  | @ -36,6 +36,9 @@ class ApiService { | ||||||
|   static const String projectManagerUrl = |   static const String projectManagerUrl = | ||||||
|       'https://pm.qhdsafety.com/zy-projectManage'; |       'https://pm.qhdsafety.com/zy-projectManage'; | ||||||
| 
 | 
 | ||||||
|  |   /// NFC巡检接口 | ||||||
|  |   static const String baseNFCPath = | ||||||
|  |       "http://192.168.0.37:8099/api/app/"; | ||||||
|   // /// 人脸识别服务 |   // /// 人脸识别服务 | ||||||
|   // static const String baseFacePath = |   // static const String baseFacePath = | ||||||
|   //     "https://qaaqwh.qhdsafety.com/whb_stu_face/"; |   //     "https://qaaqwh.qhdsafety.com/whb_stu_face/"; | ||||||
|  | @ -3386,6 +3389,74 @@ U6Hzm1ninpWeE+awIDAQAB | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | ///TODO -------------–-------------------- NFC巡检 -------------–-------------------- | ||||||
|  |   /// 管道区域 | ||||||
|  |   static Future<Map<String, dynamic>> getNfcPipeLineAreaList() { | ||||||
|  |     return HttpManager().request( | ||||||
|  |       baseNFCPath, | ||||||
|  |       '/pipelineInspection/getPipelineAreaListAll', | ||||||
|  |       method: Method.post, | ||||||
|  |       data: { | ||||||
|  |         "CORPINFO_ID":SessionService.instance.corpinfoId, | ||||||
|  |         'STATUS':'0' | ||||||
|  |       }, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /// 管道设备列表 | ||||||
|  |   static Future<Map<String, dynamic>> getNfcEquipmentPipelineListAll(String PIPELINE_AREA_ID) { | ||||||
|  |     return HttpManager().request( | ||||||
|  |       baseNFCPath, | ||||||
|  |       '/pipelineInspection/getEquipmentPipelineListAll', | ||||||
|  |       method: Method.post, | ||||||
|  |       data: { | ||||||
|  |         "CORPINFO_ID":SessionService.instance.corpinfoId, | ||||||
|  |         'STATUS':'0', | ||||||
|  |         'PIPELINE_AREA_ID':PIPELINE_AREA_ID | ||||||
|  |       }, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |   ///NFC标签入库 | ||||||
|  |   static Future<Map<String, dynamic>> nfcTagAdd(Map data) { | ||||||
|  |     return HttpManager().request( | ||||||
|  |       baseNFCPath, | ||||||
|  |       '/pipelineInspection/nfcTagAdd', | ||||||
|  |       method: Method.post, | ||||||
|  |       data: { | ||||||
|  |         ...data, | ||||||
|  |         "CORPINFO_ID":SessionService.instance.corpinfoId, | ||||||
|  |         "USER_ID":SessionService.instance.loginUserId, | ||||||
|  | 
 | ||||||
|  |       }, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |   ///NFC任务列表 | ||||||
|  |   static Future<Map<String, dynamic>> nfcTaskList(int showCount, int currentPage) { | ||||||
|  |     return HttpManager().request( | ||||||
|  |       baseNFCPath, | ||||||
|  |       '/pipelineInspection/getPatrolTaskList?showCount=$showCount¤tPage=$currentPage', | ||||||
|  | 
 | ||||||
|  |       method: Method.post, | ||||||
|  |       data: { | ||||||
|  |         "CORPINFO_ID":SessionService.instance.corpinfoId, | ||||||
|  |         "USER_ID":SessionService.instance.loginUserId, | ||||||
|  |         "STATUS": '0' | ||||||
|  |       }, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |   ///NFC任务详情列表 | ||||||
|  |   static Future<Map<String, dynamic>> nfcTaskDetailList(int showCount, int currentPage) { | ||||||
|  |     return HttpManager().request( | ||||||
|  |       baseNFCPath, | ||||||
|  |       '/pipelineInspection/getPatrolTaskDetailList?showCount=$showCount¤tPage=$currentPage', | ||||||
|  |       method: Method.post, | ||||||
|  |       data: { | ||||||
|  |         "CORPINFO_ID":SessionService.instance.corpinfoId, | ||||||
|  |         "USER_ID":SessionService.instance.loginUserId, | ||||||
|  | 
 | ||||||
|  |       }, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -0,0 +1,221 @@ | ||||||
|  | import 'dart:convert'; | ||||||
|  | import 'dart:ffi'; | ||||||
|  | 
 | ||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:qhd_prevention/customWidget/bottom_picker.dart'; | ||||||
|  | import 'package:qhd_prevention/customWidget/custom_alert_dialog.dart'; | ||||||
|  | import 'package:qhd_prevention/customWidget/custom_button.dart'; | ||||||
|  | import 'package:qhd_prevention/customWidget/department_person_picker.dart'; | ||||||
|  | import 'package:qhd_prevention/customWidget/toast_util.dart'; | ||||||
|  | import 'package:qhd_prevention/http/ApiService.dart'; | ||||||
|  | import 'package:qhd_prevention/pages/home/tap/item_list_widget.dart'; | ||||||
|  | import 'package:qhd_prevention/pages/home/work_alert.dart'; | ||||||
|  | import 'package:qhd_prevention/pages/my_appbar.dart'; | ||||||
|  | import 'package:qhd_prevention/services/nfc_service.dart'; | ||||||
|  | import 'package:qhd_prevention/tools/tools.dart'; | ||||||
|  | 
 | ||||||
|  | class HomeNfcAddPage extends StatefulWidget { | ||||||
|  |   const HomeNfcAddPage({super.key}); | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   State<HomeNfcAddPage> createState() => _HomeNfcAddPageState(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class _HomeNfcAddPageState extends State<HomeNfcAddPage> { | ||||||
|  |   late Map<String, dynamic> pd = {}; | ||||||
|  |   int currentPage = 1; | ||||||
|  |   late List areaAllList = []; | ||||||
|  |   late List equipmentList = []; | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   void initState() { | ||||||
|  |     // TODO: implement initState | ||||||
|  |     super.initState(); | ||||||
|  |     _getData(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /// 获取管道区域列表 | ||||||
|  |   Future<void> _getData() async { | ||||||
|  |     final data = await ApiService.getNfcPipeLineAreaList(); | ||||||
|  |     if (data['result'] == 'success') { | ||||||
|  |       areaAllList = data['varList']; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /// 通过管道区域获取对应的设备列表 | ||||||
|  |   Future<void> _getPipelineForArea() async { | ||||||
|  |     final data = await ApiService.getNfcEquipmentPipelineListAll( | ||||||
|  |       pd['PIPELINE_AREA_ID'], | ||||||
|  |     ); | ||||||
|  |     if (data['result'] == 'success') { | ||||||
|  |       equipmentList = data['varList']; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   void _choosePipeLineHandle() async { | ||||||
|  |     // 创建选项列表 | ||||||
|  |     final options = areaAllList.map((e) => e['AREA_NAME'] as String).toList(); | ||||||
|  | 
 | ||||||
|  |     // 显示底部选择器 | ||||||
|  |     final choice = await BottomPicker.show<String>( | ||||||
|  |       context, | ||||||
|  |       items: options, | ||||||
|  |       itemBuilder: (item) => Text(item, textAlign: TextAlign.center), | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     if (choice != null) { | ||||||
|  |       // 找到选择的索引 | ||||||
|  |       final newIndex = options.indexOf(choice); | ||||||
|  |       if (newIndex != -1) { | ||||||
|  |         Map selectData = areaAllList[newIndex]; | ||||||
|  |         pd['PIPELINE_AREA_ID'] = selectData['PIPELINE_AREA_ID'] ?? ''; | ||||||
|  |         pd['AREA_NAME'] = selectData['AREA_NAME'] ?? ''; | ||||||
|  |         _getPipelineForArea(); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   void _choosePipeHandle() async { | ||||||
|  |     if (!FormUtils.hasValue(pd, 'PIPELINE_AREA_ID')) { | ||||||
|  |       ToastUtil.showNormal(context, "请先选择管道区域"); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     // 创建选项列表 | ||||||
|  |     final options = | ||||||
|  |         equipmentList.map((e) => e['EQUIPMENT_NAME'] as String).toList(); | ||||||
|  | 
 | ||||||
|  |     // 显示底部选择器 | ||||||
|  |     final choice = await BottomPicker.show<String>( | ||||||
|  |       context, | ||||||
|  |       items: options, | ||||||
|  |       itemBuilder: (item) => Text(item, textAlign: TextAlign.center), | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     if (choice != null) { | ||||||
|  |       // 找到选择的索引 | ||||||
|  |       final newIndex = options.indexOf(choice); | ||||||
|  |       if (newIndex != -1) { | ||||||
|  |         Map selectData = equipmentList[newIndex]; | ||||||
|  |         pd['EQUIPMENT_PIPELINE_ID'] = selectData['EQUIPMENT_PIPELINE_ID'] ?? ''; | ||||||
|  |         pd['EQUIPMENT_NAME'] = selectData['EQUIPMENT_NAME'] ?? ''; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   Future<void> _startWrite() async{ | ||||||
|  |     final textRules = <Map<String, dynamic>>[ | ||||||
|  |       {'value': pd['PIPELINE_AREA_ID'] ?? '', 'message': '请选择管道区域'}, | ||||||
|  |       {'value': pd['EQUIPMENT_PIPELINE_ID'] ?? '', 'message': '请选择对应的管道设备'}, | ||||||
|  |       {'value': pd['TAG_NAME'] ?? '', 'message': '请填写标签名称'}, | ||||||
|  |     ]; | ||||||
|  |     for (var rule in textRules) { | ||||||
|  |       if ((rule['value'] as String).isEmpty) { | ||||||
|  |         ToastUtil.showNormal(context, rule['message']); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     final confirmed = await CustomAlertDialog.showConfirm( | ||||||
|  |       context, | ||||||
|  |       title: '提示', | ||||||
|  |       content: '请将手机靠近NFC标签准备写入', | ||||||
|  |       cancelText: '', | ||||||
|  |       confirmText: '我知道了', | ||||||
|  |       barrierDismissible: false, | ||||||
|  |     ); | ||||||
|  |     if (confirmed) { | ||||||
|  |       LoadingDialogHelper.show(message: '等待手机靠近NFC标签'); | ||||||
|  |       await NfcService.instance.writeText( | ||||||
|  |         mapToCompactJson({'PIPELINE_AREA_ID':pd['PIPELINE_AREA_ID'],'EQUIPMENT_PIPELINE_ID':pd['EQUIPMENT_PIPELINE_ID']}), | ||||||
|  |         timeout: Duration(seconds: 12), | ||||||
|  |         onComplete: (ok, msg) async{ | ||||||
|  |           if (ok) { | ||||||
|  |             pd['NFC_CODE'] = msg; | ||||||
|  |             final result = await ApiService.nfcTagAdd(pd); | ||||||
|  |             if (result['result'] == 'success') { | ||||||
|  |               ToastUtil.showSuccess(context, '写入成功'); | ||||||
|  |               Navigator.pop(context); | ||||||
|  |             }else{ | ||||||
|  |               ToastUtil.showError(context, '写入失败,请重试'); | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |           LoadingDialogHelper.hide(); | ||||||
|  |         }, | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   String mapToCompactJson(Map<String, dynamic> map) { | ||||||
|  |     // 使用 jsonEncode 转换 | ||||||
|  |     String jsonStr = jsonEncode(map); | ||||||
|  |     // 去掉所有空格、换行、制表符 | ||||||
|  |     jsonStr = jsonStr.replaceAll(RegExp(r'\s+'), ''); | ||||||
|  | 
 | ||||||
|  |     return jsonStr; | ||||||
|  |   } | ||||||
|  |   void _getNfcData() async{ | ||||||
|  | 
 | ||||||
|  |     NfcService.instance.startScanOnceWithCallback( | ||||||
|  |       onResult: (uid, parsedText, rawMsg) async { | ||||||
|  |         final confirmed = await CustomAlertDialog.showConfirm( | ||||||
|  |           context, | ||||||
|  |           title: '内容', | ||||||
|  |           content: 'UID: $uid \n data: $parsedText', | ||||||
|  |           cancelText: '', | ||||||
|  |           confirmText: '我知道了', | ||||||
|  |           barrierDismissible: false, | ||||||
|  |         ); | ||||||
|  |         print('UID: $uid, text: $parsedText'); | ||||||
|  |       }, | ||||||
|  |       onError: (err) => print('err: $err'), | ||||||
|  |       timeout: Duration(seconds: 12), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return Scaffold( | ||||||
|  |       appBar: MyAppbar(title: 'NFC标签写入', actions: [ | ||||||
|  |         TextButton(onPressed: _getNfcData, child: Text('读取',style: TextStyle(color: Colors.white),)) | ||||||
|  |       ],), | ||||||
|  |       body: SafeArea( | ||||||
|  |         child: SingleChildScrollView( | ||||||
|  |           padding: EdgeInsets.all(12), | ||||||
|  |           child: Column( | ||||||
|  |             children: [ | ||||||
|  |               ItemListWidget.itemContainer( | ||||||
|  |                 horizontal: 5, | ||||||
|  |                 Column( | ||||||
|  |                   children: [ | ||||||
|  |                     ItemListWidget.selectableLineTitleTextRightButton( | ||||||
|  |                       label: '管道区域', | ||||||
|  |                       isEditable: true, | ||||||
|  |                       text: pd['AREA_NAME'] ?? '', | ||||||
|  |                       onTap: _choosePipeLineHandle, | ||||||
|  |                     ), | ||||||
|  |                     const Divider(), | ||||||
|  |                     ItemListWidget.selectableLineTitleTextRightButton( | ||||||
|  |                       label: '管道设备', | ||||||
|  |                       isEditable: true, | ||||||
|  |                       text: pd['EQUIPMENT_NAME'] ?? '', | ||||||
|  |                       onTap: _choosePipeHandle, | ||||||
|  |                     ), | ||||||
|  |                     const Divider(), | ||||||
|  |                     ItemListWidget.singleLineTitleText( | ||||||
|  |                       label: '标签名称', | ||||||
|  |                       isEditable: true, | ||||||
|  |                       hintText: '请输入标签名称', | ||||||
|  |                       onChanged: (val) { | ||||||
|  |                         pd['TAG_NAME'] = val; | ||||||
|  |                       }, | ||||||
|  |                     ), | ||||||
|  |                   ], | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|  |               SizedBox(height: 20,), | ||||||
|  |               CustomButton(text: '立即写入', backgroundColor: Colors.blue,onPressed: _startWrite,) | ||||||
|  |             ], | ||||||
|  |           ), | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,136 @@ | ||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:qhd_prevention/pages/my_appbar.dart'; | ||||||
|  | 
 | ||||||
|  | class HomeNfcCheckDangerPage extends StatefulWidget { | ||||||
|  |   const HomeNfcCheckDangerPage({super.key}); | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   State<HomeNfcCheckDangerPage> createState() => _HomeNfcCheckDangerPageState(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class _HomeNfcCheckDangerPageState extends State<HomeNfcCheckDangerPage> { | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |   Widget _pendingTopCard(Map<String, String> item) { | ||||||
|  |     return SizedBox( | ||||||
|  |       height: 180, | ||||||
|  |       child: Stack( | ||||||
|  |         clipBehavior: Clip.none, | ||||||
|  |         children: [ | ||||||
|  |           ClipRRect( | ||||||
|  |             borderRadius: BorderRadius.circular(10), | ||||||
|  |             child: Image.asset( | ||||||
|  |               'assets/images/xj_top.png', | ||||||
|  |               height: 70, | ||||||
|  |               width: double.infinity, | ||||||
|  |               fit: BoxFit.cover, | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  | 
 | ||||||
|  |           // 标题 & 状态标签 | ||||||
|  |           Positioned( | ||||||
|  |             top: 12, | ||||||
|  |             left: 12, | ||||||
|  |             child: Text( | ||||||
|  |               item['title']!, | ||||||
|  |               style: const TextStyle( | ||||||
|  |                 color: Colors.black87, | ||||||
|  |                 fontSize: 18, | ||||||
|  |                 fontWeight: FontWeight.bold, | ||||||
|  |               ), | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |           Positioned( | ||||||
|  |             top: 12, | ||||||
|  |             right: 12, | ||||||
|  |             child: Container( | ||||||
|  |               height: 30, | ||||||
|  |               padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), | ||||||
|  |               decoration: BoxDecoration( | ||||||
|  |                 color: Colors.blue.withOpacity(0.7), | ||||||
|  |                 borderRadius: BorderRadius.circular(4), | ||||||
|  |               ), | ||||||
|  |               child: Center( | ||||||
|  |                 child: Text( | ||||||
|  |                   item['status']!, | ||||||
|  |                   style: const TextStyle(color: Colors.white, fontSize: 14), | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  | 
 | ||||||
|  |           // 白色信息区域(盖住图片部分) | ||||||
|  |           Positioned( | ||||||
|  |             left: 0, | ||||||
|  |             right: 0, | ||||||
|  |             top: 50, // 盖住图片底部 | ||||||
|  |             child: Container( | ||||||
|  |               margin: const EdgeInsets.symmetric(horizontal: 0), | ||||||
|  |               padding: const EdgeInsets.all(16), | ||||||
|  |               decoration: BoxDecoration( | ||||||
|  |                 color: Colors.white, | ||||||
|  |                 borderRadius: BorderRadius.circular(10), | ||||||
|  |                 boxShadow: [ | ||||||
|  |                   BoxShadow( | ||||||
|  |                     color: Colors.black12, | ||||||
|  |                     blurRadius: 4, | ||||||
|  |                     offset: Offset(0, 1), | ||||||
|  |                   ), | ||||||
|  |                 ], | ||||||
|  |               ), | ||||||
|  |               child: _buildInfoGrid(item), | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |         ], | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /// 构建信息网格 | ||||||
|  |   Widget _buildInfoGrid(Map<String, String> item) { | ||||||
|  |     return Row( | ||||||
|  |       children: [ | ||||||
|  |         Expanded( | ||||||
|  |           child: Column( | ||||||
|  |             crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |             children: [ | ||||||
|  |               Text('负责部门:${item['department']}'), | ||||||
|  |               const SizedBox(height: 8), | ||||||
|  |               Text('负责人:${item['owner']}'), | ||||||
|  |               const SizedBox(height: 8), | ||||||
|  |               Text('UN件类型:${item['unType']}'), | ||||||
|  |             ], | ||||||
|  |           ), | ||||||
|  |         ), | ||||||
|  |         Expanded( | ||||||
|  |           child: Column( | ||||||
|  |             crossAxisAlignment: CrossAxisAlignment.end, | ||||||
|  |             children: [ | ||||||
|  |               Text('巡检周期:${item['cycle']}'), | ||||||
|  |               const SizedBox(height: 8), | ||||||
|  |               Text('已巡点位:${item['points']}'), | ||||||
|  |               Text('涉及管道区域:${item['department']}') | ||||||
|  |             ], | ||||||
|  |           ), | ||||||
|  |         ), | ||||||
|  |       ], | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return Scaffold( | ||||||
|  |         appBar: MyAppbar(title: '检查项'), | ||||||
|  |         body: SafeArea( | ||||||
|  |             child: Padding( | ||||||
|  |                 padding: EdgeInsets.all(16), | ||||||
|  |                 child: Column( | ||||||
|  |                     children: [ | ||||||
|  |                       _pendingTopCard({}), | ||||||
|  |                     ] | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | @ -2,7 +2,8 @@ import 'package:flutter/material.dart'; | ||||||
| import 'package:qhd_prevention/pages/my_appbar.dart'; | import 'package:qhd_prevention/pages/my_appbar.dart'; | ||||||
| 
 | 
 | ||||||
| class HomeNfcDetailPage extends StatefulWidget { | class HomeNfcDetailPage extends StatefulWidget { | ||||||
|   const HomeNfcDetailPage({super.key}); |   const HomeNfcDetailPage({super.key, required this.info}); | ||||||
|  |   final Map<String, dynamic> info; | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   State<HomeNfcDetailPage> createState() => _HomeNfcDetailPageState(); |   State<HomeNfcDetailPage> createState() => _HomeNfcDetailPageState(); | ||||||
|  | @ -10,15 +11,6 @@ class HomeNfcDetailPage extends StatefulWidget { | ||||||
| 
 | 
 | ||||||
| class _HomeNfcDetailPageState extends State<HomeNfcDetailPage> { | class _HomeNfcDetailPageState extends State<HomeNfcDetailPage> { | ||||||
| 
 | 
 | ||||||
|   Map<String, String> info = { |  | ||||||
|     'title': '设备巡检 A', |  | ||||||
|     'status': '专项巡检', |  | ||||||
|     'department': '安全部', |  | ||||||
|     'owner': '张三', |  | ||||||
|     'unType': 'UN1001', |  | ||||||
|     'cycle': '7天', |  | ||||||
|     'points': '3/5', |  | ||||||
|   }; |  | ||||||
|   final List<ProgressItem> demoData = const [ |   final List<ProgressItem> demoData = const [ | ||||||
|     ProgressItem(status: '未查', location: '到期哦i维护经费欺废气阀我废费欺废气阀我废费欺废气阀我废气阀', code: 'XJ1001'), |     ProgressItem(status: '未查', location: '到期哦i维护经费欺废气阀我废费欺废气阀我废费欺废气阀我废气阀', code: 'XJ1001'), | ||||||
|     ProgressItem(status: '已查', location: 'B区-配电室', code: 'XJ1002', checkTime: '2025-07-28 15:32'), |     ProgressItem(status: '已查', location: 'B区-配电室', code: 'XJ1002', checkTime: '2025-07-28 15:32'), | ||||||
|  | @ -32,7 +24,7 @@ class _HomeNfcDetailPageState extends State<HomeNfcDetailPage> { | ||||||
|     ProgressItem(status: '已查', location: 'B区-配电室', code: 'XJ1002', checkTime: '2025-07-28 15:32'), |     ProgressItem(status: '已查', location: 'B区-配电室', code: 'XJ1002', checkTime: '2025-07-28 15:32'), | ||||||
| 
 | 
 | ||||||
|   ]; |   ]; | ||||||
|   Widget _pendingTopCard(Map<String, String> item) { |   Widget _pendingTopCard(Map<String, dynamic> item) { | ||||||
|     return SizedBox( |     return SizedBox( | ||||||
|       height: 180, |       height: 180, | ||||||
|       child: Stack( |       child: Stack( | ||||||
|  | @ -69,7 +61,7 @@ class _HomeNfcDetailPageState extends State<HomeNfcDetailPage> { | ||||||
|                 padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), |                 padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), | ||||||
|                 decoration: BoxDecoration( |                 decoration: BoxDecoration( | ||||||
|                   color: Colors.blue.withOpacity(0.7), |                   color: Colors.blue.withOpacity(0.7), | ||||||
|                   borderRadius: BorderRadius.circular(15), |                   borderRadius: BorderRadius.circular(4), | ||||||
|                 ), |                 ), | ||||||
|                 child: Center( |                 child: Center( | ||||||
|                   child: Text( |                   child: Text( | ||||||
|  | @ -107,18 +99,18 @@ class _HomeNfcDetailPageState extends State<HomeNfcDetailPage> { | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|   /// 构建信息网格 |   /// 构建信息网格 | ||||||
|   Widget _buildInfoGrid(Map<String, String> item) { |   Widget _buildInfoGrid(Map<String, dynamic> item) { | ||||||
|     return Row( |     return Row( | ||||||
|       children: [ |       children: [ | ||||||
|         Expanded( |         Expanded( | ||||||
|           child: Column( |           child: Column( | ||||||
|             crossAxisAlignment: CrossAxisAlignment.start, |             crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|             children: [ |             children: [ | ||||||
|               Text('负责部门:${item['department']}'), |               Text('负责部门:${item['DEPARTMENT_NAME'] ?? ''}'), | ||||||
|               const SizedBox(height: 8), |               const SizedBox(height: 8), | ||||||
|               Text('负责人:${item['owner']}'), |               Text('巡检类型:${item['PATROL_TYPE_NAME'] ?? ''}'), | ||||||
|               const SizedBox(height: 8), |               const SizedBox(height: 8), | ||||||
|               Text('UN件类型:${item['unType']}'), |               Text('已巡点位:${item['INSPECTED_POINTS'] ?? '0'}'), | ||||||
|             ], |             ], | ||||||
|           ), |           ), | ||||||
|         ), |         ), | ||||||
|  | @ -126,17 +118,22 @@ class _HomeNfcDetailPageState extends State<HomeNfcDetailPage> { | ||||||
|           child: Column( |           child: Column( | ||||||
|             crossAxisAlignment: CrossAxisAlignment.end, |             crossAxisAlignment: CrossAxisAlignment.end, | ||||||
|             children: [ |             children: [ | ||||||
|               Text('巡检周期:${item['cycle']}'), |               Text('负责人:${item['USER_NAME'] ?? ''}'), | ||||||
|               const SizedBox(height: 8), |               const SizedBox(height: 8), | ||||||
|               Text('已巡点位:${item['points']}'), |               Text('巡检周期:${item['PATROL_PERIOD_NAME'] ?? ''}'), | ||||||
|               Text('涉及管道区域:${item['department']}') | 
 | ||||||
|  |               // isFinish | ||||||
|  |               //     ? Text('涉及管道区域:${item['department'] ?? ''}') | ||||||
|  |               //     : const SizedBox(height: 25), | ||||||
|             ], |             ], | ||||||
|           ), |           ), | ||||||
|         ), |         ), | ||||||
|       ], |       ], | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |   Future<void> _startCheckItem(ProgressItem item) async { | ||||||
| 
 | 
 | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|  | @ -144,7 +141,7 @@ class _HomeNfcDetailPageState extends State<HomeNfcDetailPage> { | ||||||
|       appBar: MyAppbar(title: '任务详情'), |       appBar: MyAppbar(title: '任务详情'), | ||||||
|       body: SafeArea(child: Padding(padding: EdgeInsets.all(16), child: Column( |       body: SafeArea(child: Padding(padding: EdgeInsets.all(16), child: Column( | ||||||
|         children: [ |         children: [ | ||||||
|           _pendingTopCard(info), |           _pendingTopCard(widget.info), | ||||||
|           Expanded(child: Container( |           Expanded(child: Container( | ||||||
|             decoration: BoxDecoration( |             decoration: BoxDecoration( | ||||||
|               color: Colors.white, |               color: Colors.white, | ||||||
|  | @ -162,6 +159,7 @@ class _HomeNfcDetailPageState extends State<HomeNfcDetailPage> { | ||||||
|               child: ProgressList( |               child: ProgressList( | ||||||
|                 items: demoData, |                 items: demoData, | ||||||
|                 onStartCheck: (idx) { |                 onStartCheck: (idx) { | ||||||
|  |                   _startCheckItem(demoData[idx]); | ||||||
|                   print('开始检查第 $idx 项'); |                   print('开始检查第 $idx 项'); | ||||||
|                 }, |                 }, | ||||||
|               ), |               ), | ||||||
|  |  | ||||||
|  | @ -1,4 +1,6 @@ | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:qhd_prevention/http/ApiService.dart'; | ||||||
|  | import 'package:qhd_prevention/pages/home/NFC/home_nfc_add_page.dart'; | ||||||
| import 'package:qhd_prevention/pages/home/NFC/home_nfc_detail_page.dart'; | import 'package:qhd_prevention/pages/home/NFC/home_nfc_detail_page.dart'; | ||||||
| import 'package:qhd_prevention/pages/my_appbar.dart'; | import 'package:qhd_prevention/pages/my_appbar.dart'; | ||||||
| import 'package:qhd_prevention/tools/tools.dart'; | import 'package:qhd_prevention/tools/tools.dart'; | ||||||
|  | @ -14,66 +16,270 @@ class _HomeNfcListPageState extends State<HomeNfcListPage> | ||||||
|     with SingleTickerProviderStateMixin { |     with SingleTickerProviderStateMixin { | ||||||
|   late TabController _tabController; |   late TabController _tabController; | ||||||
| 
 | 
 | ||||||
|   // 测试数据 |   // 数据源(真实数据) | ||||||
|   final List<Map<String, String>> _pendingList = [ |   final List<Map<String, dynamic>> _pendingList = []; | ||||||
|     { |   final List<Map<String, dynamic>> _recordList = []; | ||||||
|       'title': '设备巡检 A', |  | ||||||
|       'status': '待巡检', |  | ||||||
|       'department': '安全部', |  | ||||||
|       'owner': '张三', |  | ||||||
|       'unType': 'UN1001', |  | ||||||
|       'cycle': '7天', |  | ||||||
|       'points': '3/5', |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       'title': '设备巡检 B', |  | ||||||
|       'status': '待巡检', |  | ||||||
|       'department': '维护部', |  | ||||||
|       'owner': '李四', |  | ||||||
|       'unType': 'UN1002', |  | ||||||
|       'cycle': '30天', |  | ||||||
|       'points': '1/2', |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       'title': '设备巡检 C', |  | ||||||
|       'status': '待巡检', |  | ||||||
|       'department': '维护部', |  | ||||||
|       'owner': '李四', |  | ||||||
|       'unType': 'UN1002', |  | ||||||
|       'cycle': '30天', |  | ||||||
|       'points': '1/2', |  | ||||||
|     }, |  | ||||||
|   ]; |  | ||||||
| 
 | 
 | ||||||
|   final List<Map<String, String>> _recordList = [ |   // 分页状态 - 待巡检 | ||||||
|     { |   int _pendingPage = 1; | ||||||
|       'title': '设备巡检 A', |   final int _pageSize = 10; | ||||||
|       'status': '待巡检', |   bool _pendingLoading = false; | ||||||
|       'department': '安全部', |   bool _pendingHasMore = true; | ||||||
|       'owner': '张三', |   final ScrollController _pendingScrollController = ScrollController(); | ||||||
|       'unType': 'UN1001', |  | ||||||
|       'cycle': '7天', |  | ||||||
|       'points': '3/5', |  | ||||||
|     }, |  | ||||||
| 
 | 
 | ||||||
|   ]; |   // 分页状态 - 巡检记录 | ||||||
|  |   int _recordPage = 1; | ||||||
|  |   bool _recordLoading = false; | ||||||
|  |   bool _recordHasMore = true; | ||||||
|  |   final ScrollController _recordScrollController = ScrollController(); | ||||||
|  | 
 | ||||||
|  |   // 页面 loading(用于首次加载指示) | ||||||
|  |   bool _initialLoadingPending = false; | ||||||
|  |   bool _initialLoadingRecord = false; | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   void initState() { |   void initState() { | ||||||
|     super.initState(); |     super.initState(); | ||||||
|     _tabController = TabController(length: 2, vsync: this); |     _tabController = TabController(length: 2, vsync: this); | ||||||
|  | 
 | ||||||
|  |     // Tab 切换时触发加载(如果对应列表为空) | ||||||
|  |     _tabController.addListener(() { | ||||||
|  |       if (!_tabController.indexIsChanging) { | ||||||
|  |         final idx = _tabController.index; | ||||||
|  |         if (idx == 0 && _pendingList.isEmpty) { | ||||||
|  |           _refreshPending(); | ||||||
|  |         } else if (idx == 1 && _recordList.isEmpty) { | ||||||
|  |           _refreshRecord(); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     // 滚动监听,上拉触底加载更多 | ||||||
|  |     _pendingScrollController.addListener(() { | ||||||
|  |       if (_pendingScrollController.position.pixels >= | ||||||
|  |           _pendingScrollController.position.maxScrollExtent - 80 && | ||||||
|  |           !_pendingLoading && | ||||||
|  |           _pendingHasMore) { | ||||||
|  |         _loadMorePending(); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     _recordScrollController.addListener(() { | ||||||
|  |       if (_recordScrollController.position.pixels >= | ||||||
|  |           _recordScrollController.position.maxScrollExtent - 80 && | ||||||
|  |           !_recordLoading && | ||||||
|  |           _recordHasMore) { | ||||||
|  |         _loadMoreRecord(); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     // 首次默认加载待巡检列表 | ||||||
|  |     WidgetsBinding.instance.addPostFrameCallback((_) { | ||||||
|  |       _refreshPending(); | ||||||
|  |     }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   void dispose() { |   void dispose() { | ||||||
|     _tabController.dispose(); |     _tabController.dispose(); | ||||||
|  |     _pendingScrollController.dispose(); | ||||||
|  |     _recordScrollController.dispose(); | ||||||
|     super.dispose(); |     super.dispose(); | ||||||
|   } |   } | ||||||
|  |   String _checkStatusName(String status) { | ||||||
|  |     if (status == '0') { | ||||||
|  |       return '已巡检'; | ||||||
|  |     }else if (status == '1') { | ||||||
|  |       return '超期未巡检'; | ||||||
|  |     }else if (status == '2') { | ||||||
|  |       return '巡检中'; | ||||||
|  |     }else{ | ||||||
|  |       return '待巡检'; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   Future<void> _refreshPending() async { | ||||||
|  |     setState(() { | ||||||
|  |       _pendingPage = 1; | ||||||
|  |       _pendingHasMore = true; | ||||||
|  |       _initialLoadingPending = true; | ||||||
|  |     }); | ||||||
|  |     try { | ||||||
|  |       await _getTaskList(page: 1, replace: true); | ||||||
|  |     } finally { | ||||||
|  |       if (mounted) { | ||||||
|  |         setState(() { | ||||||
|  |           _initialLoadingPending = false; | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   Future<void> _loadMorePending() async { | ||||||
|  |     if (_pendingLoading || !_pendingHasMore) return; | ||||||
|  |     await _getTaskList(page: _pendingPage + 1, replace: false); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   Future<void> _refreshRecord() async { | ||||||
|  |     setState(() { | ||||||
|  |       _recordPage = 1; | ||||||
|  |       _recordHasMore = true; | ||||||
|  |       _initialLoadingRecord = true; | ||||||
|  |     }); | ||||||
|  |     try { | ||||||
|  |       await _getTaskDetailList(page: 1, replace: true); | ||||||
|  |     } finally { | ||||||
|  |       if (mounted) { | ||||||
|  |         setState(() { | ||||||
|  |           _initialLoadingRecord = false; | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   Future<void> _loadMoreRecord() async { | ||||||
|  |     if (_recordLoading || !_recordHasMore) return; | ||||||
|  |     await _getTaskDetailList(page: _recordPage + 1, replace: false); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // ---------- 网络请求(适配后端返回结构) ---------- | ||||||
|  |   //NFC任务列表 | ||||||
|  |   Future<void> _getTaskList({required int page, required bool replace}) async { | ||||||
|  |     setState(() { | ||||||
|  |       _pendingLoading = true; | ||||||
|  |     }); | ||||||
|  |     try { | ||||||
|  |       final res = await ApiService.nfcTaskList(_pageSize, page); | ||||||
|  |       // // 适配后端返回:尝试找到数组数据 | ||||||
|  |       // List<dynamic>? list; | ||||||
|  |       if (res['result'] == 'success') { | ||||||
|  |         List<dynamic> list = res['varList']; | ||||||
|  | 
 | ||||||
|  |       final parsed = list.map<Map<String, dynamic>>((e) { | ||||||
|  |         return e; | ||||||
|  |       }).toList(); | ||||||
|  | 
 | ||||||
|  |       // | ||||||
|  |       // 判断是否还有更多:如果返回数量 < pageSize 则没有更多 | ||||||
|  |       final bool gotLessThanPage = parsed.length < _pageSize; | ||||||
|  | 
 | ||||||
|  |       if (replace) { | ||||||
|  |         setState(() { | ||||||
|  |           _pendingList.clear(); | ||||||
|  |           _pendingList.addAll(parsed); | ||||||
|  |           _pendingPage = 1; | ||||||
|  |           _pendingHasMore = !gotLessThanPage; | ||||||
|  |         }); | ||||||
|  |       } else { | ||||||
|  |         setState(() { | ||||||
|  |           _pendingList.addAll(parsed); | ||||||
|  |           _pendingPage = page; | ||||||
|  |           if (parsed.isEmpty) _pendingHasMore = false; | ||||||
|  |           else if (parsed.length < _pageSize) _pendingHasMore = false; | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |       } | ||||||
|  |     } catch (e) { | ||||||
|  |       // 错误处理 | ||||||
|  |       if (mounted) { | ||||||
|  |         ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('获取待巡检列表失败:$e'))); | ||||||
|  |       } | ||||||
|  |     } finally { | ||||||
|  |       if (mounted) { | ||||||
|  |         setState(() { | ||||||
|  |           _pendingLoading = false; | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   Future<void> _getTaskDetailList({required int page, required bool replace}) async { | ||||||
|  |     setState(() { | ||||||
|  |       _recordLoading = true; | ||||||
|  |     }); | ||||||
|  |     try { | ||||||
|  |       final res = await ApiService.nfcTaskDetailList(_pageSize, page); | ||||||
|  |       List<dynamic>? list; | ||||||
|  |       // if (res == null) { | ||||||
|  |       //   list = []; | ||||||
|  |       // } else if (res is List) { | ||||||
|  |       //   list = res; | ||||||
|  |       // } else if (res is Map) { | ||||||
|  |       //   list = (res['data'] ?? res['list'] ?? res['items'] ?? []) as List<dynamic>?; | ||||||
|  |       // } | ||||||
|  |       // list ??= []; | ||||||
|  |       // | ||||||
|  |       // final parsed = list.map<Map<String, dynamic>>((e) { | ||||||
|  |       //   if (e is Map) { | ||||||
|  |       //     return { | ||||||
|  |       //       'title': (e['title'] ?? e['TASK_NAME'] ?? e['taskName'] ?? e['name'] ?? '未命名').toString(), | ||||||
|  |       //       'status': (e['status'] ?? e['TASK_STATUS'] ?? '已巡检').toString(), | ||||||
|  |       //       'department': (e['department'] ?? e['DEPARTMENT'] ?? e['dept'] ?? '').toString(), | ||||||
|  |       //       'owner': (e['owner'] ?? e['OWNER'] ?? e['person'] ?? '').toString(), | ||||||
|  |       //       'unType': (e['unType'] ?? e['UN_TYPE'] ?? e['un_type'] ?? '').toString(), | ||||||
|  |       //       'cycle': (e['cycle'] ?? e['CYCLE'] ?? '').toString(), | ||||||
|  |       //       'points': (e['points'] ?? e['POINTS'] ?? '').toString(), | ||||||
|  |       //     }; | ||||||
|  |       //   } else { | ||||||
|  |       //     final s = e?.toString() ?? ''; | ||||||
|  |       //     return { | ||||||
|  |       //       'title': s, | ||||||
|  |       //       'status': '已巡检', | ||||||
|  |       //       'department': '', | ||||||
|  |       //       'owner': '', | ||||||
|  |       //       'unType': '', | ||||||
|  |       //       'cycle': '', | ||||||
|  |       //       'points': '', | ||||||
|  |       //     }; | ||||||
|  |       //   } | ||||||
|  |       // }).toList(); | ||||||
|  |       // | ||||||
|  |       // final bool gotLessThanPage = parsed.length < _pageSize; | ||||||
|  |       // | ||||||
|  |       // if (replace) { | ||||||
|  |       //   setState(() { | ||||||
|  |       //     _recordList.clear(); | ||||||
|  |       //     _recordList.addAll(parsed); | ||||||
|  |       //     _recordPage = 1; | ||||||
|  |       //     _recordHasMore = !gotLessThanPage; | ||||||
|  |       //   }); | ||||||
|  |       // } else { | ||||||
|  |       //   setState(() { | ||||||
|  |       //     _recordList.addAll(parsed); | ||||||
|  |       //     _recordPage = page; | ||||||
|  |       //     if (parsed.isEmpty) _recordHasMore = false; | ||||||
|  |       //     else if (parsed.length < _pageSize) _recordHasMore = false; | ||||||
|  |       //   }); | ||||||
|  |       // } | ||||||
|  |     } catch (e) { | ||||||
|  |       if (mounted) { | ||||||
|  |         ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('获取巡检记录失败:$e'))); | ||||||
|  |       } | ||||||
|  |     } finally { | ||||||
|  |       if (mounted) { | ||||||
|  |         setState(() { | ||||||
|  |           _recordLoading = false; | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // ---------- UI ---------- | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     return Scaffold( |     return Scaffold( | ||||||
|       appBar: MyAppbar(title: '巡检列表'), |       appBar: MyAppbar(title: '巡检列表', actions: [ | ||||||
|  |         TextButton( | ||||||
|  |           onPressed: () { | ||||||
|  |             pushPage(const HomeNfcAddPage(), context); | ||||||
|  |           }, | ||||||
|  |           child: const Text( | ||||||
|  |             "NFC绑定", | ||||||
|  |             style: TextStyle(color: Colors.white, fontSize: 16), | ||||||
|  |           ), | ||||||
|  |         ) | ||||||
|  |       ]), | ||||||
|       body: SafeArea( |       body: SafeArea( | ||||||
|         child: Column( |         child: Column( | ||||||
|           children: [ |           children: [ | ||||||
|  | @ -94,7 +300,12 @@ class _HomeNfcListPageState extends State<HomeNfcListPage> | ||||||
|             Expanded( |             Expanded( | ||||||
|               child: TabBarView( |               child: TabBarView( | ||||||
|                 controller: _tabController, |                 controller: _tabController, | ||||||
|                 children: [_buildPendingList(), _buildRecordList()], |                 children: [ | ||||||
|  |                   // 待巡检 - 支持下拉刷新和上拉加载 | ||||||
|  |                   _buildPendingList(), | ||||||
|  |                   // 巡检记录 - 支持下拉刷新和上拉加载 | ||||||
|  |                   _buildRecordList(), | ||||||
|  |                 ], | ||||||
|               ), |               ), | ||||||
|             ), |             ), | ||||||
|           ], |           ], | ||||||
|  | @ -104,48 +315,91 @@ class _HomeNfcListPageState extends State<HomeNfcListPage> | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   Widget _buildPendingList() { |   Widget _buildPendingList() { | ||||||
|     if (_pendingList.isEmpty) { |     if (_initialLoadingPending && _pendingList.isEmpty) { | ||||||
|       return NoDataWidget.show(); // 无数据提示 |       return const Center(child: CircularProgressIndicator()); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return ListView.builder( |     if (_pendingList.isEmpty) { | ||||||
|  |       return NoDataWidget.show(); // 你的无数据控件 | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|       itemCount: _pendingList.length, |     return RefreshIndicator( | ||||||
|  |       onRefresh: _refreshPending, | ||||||
|  |       child: ListView.builder( | ||||||
|  |         controller: _pendingScrollController, | ||||||
|  |         itemCount: _pendingList.length + 1, // 最后一项作为加载更多指示 | ||||||
|         padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 0), |         padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 0), | ||||||
|         itemBuilder: (context, index) { |         itemBuilder: (context, index) { | ||||||
|  |           if (index == _pendingList.length) { | ||||||
|  |             // 底部加载更多区域 | ||||||
|  |             if (_pendingHasMore) { | ||||||
|  |               return const Padding( | ||||||
|  |                 padding: EdgeInsets.symmetric(vertical: 12), | ||||||
|  |                 child: Center(child: CircularProgressIndicator()), | ||||||
|  |               ); | ||||||
|  |             } else { | ||||||
|  |               return const Padding( | ||||||
|  |                 padding: EdgeInsets.symmetric(vertical: 12), | ||||||
|  |                 child: Center(child: Text('没有更多数据')), | ||||||
|  |               ); | ||||||
|  |             } | ||||||
|  |           } | ||||||
|           final item = _pendingList[index]; |           final item = _pendingList[index]; | ||||||
|           return Container( |           return Container( | ||||||
|           margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 0), |             margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), | ||||||
|             child: GestureDetector( |             child: GestureDetector( | ||||||
|             onTap: (){ |               onTap: () { | ||||||
|               pushPage(HomeNfcDetailPage(), context); |                 pushPage(HomeNfcDetailPage(info: item), context); | ||||||
|               }, |               }, | ||||||
|               child: _pendingCard(item, false), |               child: _pendingCard(item, false), | ||||||
|             ), |             ), | ||||||
|           ); |           ); | ||||||
|         }, |         }, | ||||||
|  |       ), | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   Widget _buildRecordList() { |   Widget _buildRecordList() { | ||||||
|     if (_recordList.isEmpty) { |     if (_initialLoadingRecord && _recordList.isEmpty) { | ||||||
|       return NoDataWidget.show(); // 无数据提示 |       return const Center(child: CircularProgressIndicator()); | ||||||
|     } |     } | ||||||
|     return ListView.builder( | 
 | ||||||
|       itemCount: _recordList.length, |     if (_recordList.isEmpty) { | ||||||
|  |       return NoDataWidget.show(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return RefreshIndicator( | ||||||
|  |       onRefresh: _refreshRecord, | ||||||
|  |       child: ListView.builder( | ||||||
|  |         controller: _recordScrollController, | ||||||
|  |         itemCount: _recordList.length + 1, | ||||||
|         padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 0), |         padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 0), | ||||||
|         itemBuilder: (context, index) { |         itemBuilder: (context, index) { | ||||||
|  |           if (index == _recordList.length) { | ||||||
|  |             if (_recordHasMore) { | ||||||
|  |               return const Padding( | ||||||
|  |                 padding: EdgeInsets.symmetric(vertical: 12), | ||||||
|  |                 child: Center(child: CircularProgressIndicator()), | ||||||
|  |               ); | ||||||
|  |             } else { | ||||||
|  |               return const Padding( | ||||||
|  |                 padding: EdgeInsets.symmetric(vertical: 12), | ||||||
|  |                 child: Center(child: Text('没有更多数据')), | ||||||
|  |               ); | ||||||
|  |             } | ||||||
|  |           } | ||||||
|           final item = _recordList[index]; |           final item = _recordList[index]; | ||||||
|           return Container( |           return Container( | ||||||
|           margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 0), |             margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), | ||||||
|             child: _pendingCard(item, true), |             child: _pendingCard(item, true), | ||||||
|           ); |           ); | ||||||
|         }, |         }, | ||||||
|  |       ), | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /// 构建待巡检卡片 |   /// 构建待巡检卡片 | ||||||
|   Widget _pendingCard(Map<String, String> item, bool isFinish) { |   Widget _pendingCard(Map<String, dynamic> item, bool isFinish) { | ||||||
|     return SizedBox( |     return SizedBox( | ||||||
|       height: 180, |       height: 180, | ||||||
|       child: Stack( |       child: Stack( | ||||||
|  | @ -167,7 +421,7 @@ class _HomeNfcListPageState extends State<HomeNfcListPage> | ||||||
|             top: 12, |             top: 12, | ||||||
|             left: 12, |             left: 12, | ||||||
|             child: Text( |             child: Text( | ||||||
|               item['title']!, |               item['TASK_NAME'] ?? '', | ||||||
|               style: const TextStyle( |               style: const TextStyle( | ||||||
|                 color: Colors.black87, |                 color: Colors.black87, | ||||||
|                 fontSize: 18, |                 fontSize: 18, | ||||||
|  | @ -181,15 +435,15 @@ class _HomeNfcListPageState extends State<HomeNfcListPage> | ||||||
|               right: 12, |               right: 12, | ||||||
|               child: Container( |               child: Container( | ||||||
|                 height: 30, |                 height: 30, | ||||||
|                 padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), |                 padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), | ||||||
|                 decoration: BoxDecoration( |                 decoration: BoxDecoration( | ||||||
|                   color: Colors.blue.withOpacity(0.7), |                   color: Colors.white.withOpacity(0.5), | ||||||
|                   borderRadius: BorderRadius.circular(15), |                   borderRadius: BorderRadius.circular(15), | ||||||
|                 ), |                 ), | ||||||
|                 child: Center( |                 child: Center( | ||||||
|                   child: Text( |                   child: Text( | ||||||
|                     item['status']!, |                     item['INSPECTED_POINTS'] > 0 ? '巡检中' : '待巡检', | ||||||
|                     style: const TextStyle(color: Colors.white, fontSize: 14), |                     style: TextStyle(color: item['INSPECTED_POINTS'] as int > 0 ? Colors.blue : Colors.deepOrange, fontSize: 14), | ||||||
|                   ), |                   ), | ||||||
|                 ), |                 ), | ||||||
|               ), |               ), | ||||||
|  | @ -211,7 +465,7 @@ class _HomeNfcListPageState extends State<HomeNfcListPage> | ||||||
|                   bottomLeft: Radius.circular(10), |                   bottomLeft: Radius.circular(10), | ||||||
|                   bottomRight: Radius.circular(10), |                   bottomRight: Radius.circular(10), | ||||||
|                 ), |                 ), | ||||||
|                 boxShadow: [ |                 boxShadow: const [ | ||||||
|                   BoxShadow( |                   BoxShadow( | ||||||
|                     color: Colors.black12, |                     color: Colors.black12, | ||||||
|                     blurRadius: 4, |                     blurRadius: 4, | ||||||
|  | @ -228,18 +482,19 @@ class _HomeNfcListPageState extends State<HomeNfcListPage> | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /// 构建信息网格 |   /// 构建信息网格 | ||||||
|   Widget _buildInfoGrid(Map<String, String> item, bool isFinish) { |   Widget _buildInfoGrid(Map<String, dynamic> item, bool isFinish) { | ||||||
|  | 
 | ||||||
|     return Row( |     return Row( | ||||||
|       children: [ |       children: [ | ||||||
|         Expanded( |         Expanded( | ||||||
|           child: Column( |           child: Column( | ||||||
|             crossAxisAlignment: CrossAxisAlignment.start, |             crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|             children: [ |             children: [ | ||||||
|               Text('负责部门:${item['department']}'), |               Text('负责部门:${item['DEPARTMENT_NAME'] ?? ''}'), | ||||||
|               const SizedBox(height: 8), |               const SizedBox(height: 8), | ||||||
|               Text('负责人:${item['owner']}'), |               Text('巡检类型:${item['PATROL_TYPE_NAME'] ?? ''}'), | ||||||
|               const SizedBox(height: 8), |               const SizedBox(height: 8), | ||||||
|               Text('UN件类型:${item['unType']}'), |               Text('已巡点位:${item['INSPECTED_POINTS'] ?? '0'}'), | ||||||
|             ], |             ], | ||||||
|           ), |           ), | ||||||
|         ), |         ), | ||||||
|  | @ -247,11 +502,12 @@ class _HomeNfcListPageState extends State<HomeNfcListPage> | ||||||
|           child: Column( |           child: Column( | ||||||
|             crossAxisAlignment: CrossAxisAlignment.end, |             crossAxisAlignment: CrossAxisAlignment.end, | ||||||
|             children: [ |             children: [ | ||||||
|               Text('巡检周期:${item['cycle']}'), |               Text('负责人:${item['USER_NAME'] ?? ''}'), | ||||||
|               const SizedBox(height: 8), |               const SizedBox(height: 8), | ||||||
|               Text('已巡点位:${item['points']}'), |               Text('巡检周期:${item['PATROL_PERIOD_NAME'] ?? ''}'), | ||||||
|  | 
 | ||||||
|               isFinish |               isFinish | ||||||
|                   ? Text('涉及管道区域:${item['department']}') |                   ? Text('涉及管道区域:${item['department'] ?? ''}') | ||||||
|                   : const SizedBox(height: 25), |                   : const SizedBox(height: 25), | ||||||
|             ], |             ], | ||||||
|           ), |           ), | ||||||
|  | @ -259,5 +515,4 @@ class _HomeNfcListPageState extends State<HomeNfcListPage> | ||||||
|       ], |       ], | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| 
 |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -95,15 +95,15 @@ class _MainPageState extends State<MainPage> { | ||||||
|         isBack: false, |         isBack: false, | ||||||
|         actions: [ |         actions: [ | ||||||
|           if (_currentIndex == 0) ...[ |           if (_currentIndex == 0) ...[ | ||||||
|             // IconButton( |             IconButton( | ||||||
|             //   onPressed: () => Navigator.push( |               onPressed: () => Navigator.push( | ||||||
|             //     context, |                 context, | ||||||
|             //     MaterialPageRoute( |                 MaterialPageRoute( | ||||||
|             //         builder: (_) => NfcTestPage()), |                     builder: (_) => NfcTestPage()), | ||||||
|             //   ), |               ), | ||||||
|             //   icon: Image.asset("assets/images/ai_img.png", |               icon: Image.asset("assets/images/ai_img.png", | ||||||
|             //       width: 20, height: 20), |                   width: 20, height: 20), | ||||||
|             // ), |             ), | ||||||
|             IconButton( |             IconButton( | ||||||
|               onPressed: () => Navigator.push( |               onPressed: () => Navigator.push( | ||||||
|                 context, |                 context, | ||||||
|  |  | ||||||
|  | @ -0,0 +1,393 @@ | ||||||
|  | // 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(); | ||||||
|  |   static final NfcService instance = NfcService._internal(); | ||||||
|  | 
 | ||||||
|  |   /// 是否正在进行 NFC 会话(扫描或写入) | ||||||
|  |   final ValueNotifier<bool> scanning = ValueNotifier(false); | ||||||
|  | 
 | ||||||
|  |   /// 日志广播流(方便 UI 订阅) | ||||||
|  |   final StreamController<String> _logController = StreamController.broadcast(); | ||||||
|  |   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) { | ||||||
|  |     return bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(':').toUpperCase(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /// 递归在 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); | ||||||
|  |           if (nfcA != null && nfcA.tag.id != null) return _bytesToHex(Uint8List.fromList(nfcA.tag.id)); | ||||||
|  |         } catch (_) {} | ||||||
|  |       } else if (Platform.isIOS) { | ||||||
|  |         try { | ||||||
|  |           final mifare = MiFareIos.from(tag); | ||||||
|  |           if (mifare != null && mifare.identifier != null) return _bytesToHex(Uint8List.fromList(mifare.identifier)); | ||||||
|  |         } 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 { | ||||||
|  |       final records = (msg is Map && msg['records'] != null) | ||||||
|  |           ? List<dynamic>.from(msg['records']) | ||||||
|  |           : (msg is dynamic ? (msg.records as List<dynamic>?) : null); | ||||||
|  |       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']; | ||||||
|  |             if (ptmp is Uint8List) p = ptmp; | ||||||
|  |             else if (ptmp is List<int>) p = Uint8List.fromList(ptmp); | ||||||
|  |           } else { | ||||||
|  |             final pr = (r as dynamic).payload; | ||||||
|  |             if (pr is Uint8List) p = pr; | ||||||
|  |             else if (pr is List<int>) p = Uint8List.fromList(pr); | ||||||
|  |           } | ||||||
|  |           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({ | ||||||
|  |     required void Function(String uid, String parsedText, dynamic rawMessage) onResult, | ||||||
|  |     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; | ||||||
|  |                     if (payload is Uint8List) parsedText = parseTextFromPayload(payload); | ||||||
|  |                     else if (payload is List<int>) parsedText = parseTextFromPayload(Uint8List.fromList(payload)); | ||||||
|  |                   } | ||||||
|  |                 } | ||||||
|  |               } else { | ||||||
|  |                 rawMsg = null; | ||||||
|  |               } | ||||||
|  |             } catch (_) { | ||||||
|  |               // 回退:尝试从 tag map 中嗅探 cachedMessage / records | ||||||
|  |               try { | ||||||
|  |                 if (tag is Map) { | ||||||
|  |                   rawMsg = tag['cachedMessage'] ?? tag['ndef']?['cachedMessage'] ?? tag['message']; | ||||||
|  |                   if (rawMsg != null) { | ||||||
|  |                     final recs = (rawMsg['records'] ?? (rawMsg as dynamic).records) as dynamic; | ||||||
|  |                     if (recs != null && recs is List && recs.isNotEmpty) { | ||||||
|  |                       final r = recs.first; | ||||||
|  |                       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)); | ||||||
|  |                     } | ||||||
|  |                   } | ||||||
|  |                 } | ||||||
|  |               } catch (_) {} | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // 回调结果 | ||||||
|  |             onResult(uid, parsedText, rawMsg); | ||||||
|  |             _logController.add('UID: $uid\nmessage: ${_formatMessageToString(rawMsg)}'); | ||||||
|  | 
 | ||||||
|  |             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文本') | ||||||
|  |   Future<String> readOnceText({Duration timeout = const Duration(seconds: 10)}) async { | ||||||
|  |     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 也会被触发) | ||||||
|  |   Future<bool> writeText(String text, {Duration? timeout, void Function(bool ok, Object? err)? onComplete}) async { | ||||||
|  |     final available = await isAvailable(); | ||||||
|  |     if (!available) { | ||||||
|  |       onComplete?.call(false, 'NFC not available'); | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |     if (scanning.value) { | ||||||
|  |       onComplete?.call(false, 'Another session running'); | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     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 { | ||||||
|  |       await NfcManager.instance.startSession(onDiscovered: (dynamic tag) async { | ||||||
|  |         try { | ||||||
|  |           final ndef = Ndef.from(tag); | ||||||
|  |           if (ndef == null) { | ||||||
|  |             onComplete?.call(false, 'Tag 不支持 NDEF'); | ||||||
|  |             await stopSession(); | ||||||
|  |             return; | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           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 返回出去 | ||||||
|  |           _logController.add('NFC write success, UID=$uid'); | ||||||
|  |         } catch (e) { | ||||||
|  |           debugPrint('NFC write error: $e'); | ||||||
|  |           onComplete?.call(false, e); | ||||||
|  |         } finally { | ||||||
|  |           await stopSession(); | ||||||
|  |           timer?.cancel(); | ||||||
|  |           scanning.value = false; | ||||||
|  |         } | ||||||
|  |       }, pollingOptions: {NfcPollingOption.iso14443}); | ||||||
|  | 
 | ||||||
|  |     } catch (e) { | ||||||
|  |       scanning.value = false; | ||||||
|  |       timer?.cancel(); | ||||||
|  |       onComplete?.call(false, e); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return success; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /// 释放资源 | ||||||
|  |   void dispose() { | ||||||
|  |     try { | ||||||
|  |       _logController.close(); | ||||||
|  |     } catch (_) {} | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,15 @@ | ||||||
|  | import 'dart:convert'; | ||||||
|  | import 'dart:typed_data'; | ||||||
|  | import 'dart:io'; | ||||||
|  | 
 | ||||||
|  | Uint8List compressJson(Map<String, dynamic> data) { | ||||||
|  |   final jsonStr = jsonEncode(data); | ||||||
|  |   final utf8Bytes = utf8.encode(jsonStr); | ||||||
|  |   final gzipBytes = GZipCodec().encode(utf8Bytes); | ||||||
|  |   return Uint8List.fromList(gzipBytes); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | Map<String, dynamic> decompressJson(Uint8List bytes) { | ||||||
|  |   final decoded = GZipCodec().decode(bytes); | ||||||
|  |   return jsonDecode(utf8.decode(decoded)); | ||||||
|  | } | ||||||
|  | @ -302,6 +302,7 @@ bool isAfterStr(String a, String b) => compareYMdHmStrings(a, b) == 1; | ||||||
| 
 | 
 | ||||||
| /// 便捷:a 是否 早于 b | /// 便捷:a 是否 早于 b | ||||||
| bool isBeforeStr(String a, String b) => compareYMdHmStrings(a, b) == -1; | bool isBeforeStr(String a, String b) => compareYMdHmStrings(a, b) == -1; | ||||||
|  | 
 | ||||||
| /// ------------------------------------------------------ | /// ------------------------------------------------------ | ||||||
| /// 防多次点击 | /// 防多次点击 | ||||||
| /// ------------------------------------------------------ | /// ------------------------------------------------------ | ||||||
|  | @ -434,10 +435,29 @@ class NoDataWidget { | ||||||
| class NativeOrientation { | class NativeOrientation { | ||||||
|   static const MethodChannel _channel = MethodChannel('app.orientation'); |   static const MethodChannel _channel = MethodChannel('app.orientation'); | ||||||
| 
 | 
 | ||||||
|   static Future<void> setLandscape() async { |   static Future<bool> setLandscape() async { | ||||||
|     await _channel.invokeMethod('setOrientation', 'landscape'); |     try { | ||||||
|  |       final res = await _channel.invokeMethod('setOrientation', 'landscape'); | ||||||
|  |       return res == true; | ||||||
|  |     } on PlatformException catch (e) { | ||||||
|  |       debugPrint('PlatformException setLandscape: $e'); | ||||||
|  |       return false; | ||||||
|  |     } catch (e) { | ||||||
|  |       debugPrint('Unknown error setLandscape: $e'); | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   static Future<bool> setPortrait() async { | ||||||
|  |     try { | ||||||
|  |       final res = await _channel.invokeMethod('setOrientation', 'portrait'); | ||||||
|  |       return res == true; | ||||||
|  |     } on PlatformException catch (e) { | ||||||
|  |       debugPrint('PlatformException setPortrait: $e'); | ||||||
|  |       return false; | ||||||
|  |     } catch (e) { | ||||||
|  |       debugPrint('Unknown error setPortrait: $e'); | ||||||
|  |       return false; | ||||||
|     } |     } | ||||||
|   static Future<void> setPortrait() async { |  | ||||||
|     await _channel.invokeMethod('setOrientation', 'portrait'); |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | @ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev | ||||||
| # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html | ||||||
| # In Windows, build-name is used as the major, minor, and patch parts | # In Windows, build-name is used as the major, minor, and patch parts | ||||||
| # of the product and file versions while build-number is used as the build suffix. | # of the product and file versions while build-number is used as the build suffix. | ||||||
| version: 1.0.1+5 | version: 2.1.2+1 | ||||||
| 
 | 
 | ||||||
| environment: | environment: | ||||||
|   sdk: ^3.7.0 |   sdk: ^3.7.0 | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue