NFC巡检部分
							parent
							
								
									2495e4b9d4
								
							
						
					
					
						commit
						6fd181636e
					
				|  | @ -36,6 +36,9 @@ class ApiService { | |||
|   static const String projectManagerUrl = | ||||
|       'https://pm.qhdsafety.com/zy-projectManage'; | ||||
| 
 | ||||
|   /// NFC巡检接口 | ||||
|   static const String baseNFCPath = | ||||
|       "http://192.168.0.37:8099/api/app/"; | ||||
|   // /// 人脸识别服务 | ||||
|   // static const String baseFacePath = | ||||
|   //     "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'; | ||||
| 
 | ||||
| class HomeNfcDetailPage extends StatefulWidget { | ||||
|   const HomeNfcDetailPage({super.key}); | ||||
|   const HomeNfcDetailPage({super.key, required this.info}); | ||||
|   final Map<String, dynamic> info; | ||||
| 
 | ||||
|   @override | ||||
|   State<HomeNfcDetailPage> createState() => _HomeNfcDetailPageState(); | ||||
|  | @ -10,15 +11,6 @@ class HomeNfcDetailPage extends StatefulWidget { | |||
| 
 | ||||
| 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 [ | ||||
|     ProgressItem(status: '未查', location: '到期哦i维护经费欺废气阀我废费欺废气阀我废费欺废气阀我废气阀', code: 'XJ1001'), | ||||
|     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'), | ||||
| 
 | ||||
|   ]; | ||||
|   Widget _pendingTopCard(Map<String, String> item) { | ||||
|   Widget _pendingTopCard(Map<String, dynamic> item) { | ||||
|     return SizedBox( | ||||
|       height: 180, | ||||
|       child: Stack( | ||||
|  | @ -69,7 +61,7 @@ class _HomeNfcDetailPageState extends State<HomeNfcDetailPage> { | |||
|                 padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), | ||||
|                 decoration: BoxDecoration( | ||||
|                   color: Colors.blue.withOpacity(0.7), | ||||
|                   borderRadius: BorderRadius.circular(15), | ||||
|                   borderRadius: BorderRadius.circular(4), | ||||
|                 ), | ||||
|                 child: Center( | ||||
|                   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( | ||||
|       children: [ | ||||
|         Expanded( | ||||
|           child: Column( | ||||
|             crossAxisAlignment: CrossAxisAlignment.start, | ||||
|             children: [ | ||||
|               Text('负责部门:${item['department']}'), | ||||
|               Text('负责部门:${item['DEPARTMENT_NAME'] ?? ''}'), | ||||
|               const SizedBox(height: 8), | ||||
|               Text('负责人:${item['owner']}'), | ||||
|               Text('巡检类型:${item['PATROL_TYPE_NAME'] ?? ''}'), | ||||
|               const SizedBox(height: 8), | ||||
|               Text('UN件类型:${item['unType']}'), | ||||
|               Text('已巡点位:${item['INSPECTED_POINTS'] ?? '0'}'), | ||||
|             ], | ||||
|           ), | ||||
|         ), | ||||
|  | @ -126,17 +118,22 @@ class _HomeNfcDetailPageState extends State<HomeNfcDetailPage> { | |||
|           child: Column( | ||||
|             crossAxisAlignment: CrossAxisAlignment.end, | ||||
|             children: [ | ||||
|               Text('巡检周期:${item['cycle']}'), | ||||
|               Text('负责人:${item['USER_NAME'] ?? ''}'), | ||||
|               const SizedBox(height: 8), | ||||
|               Text('已巡点位:${item['points']}'), | ||||
|               Text('涉及管道区域:${item['department']}') | ||||
|               Text('巡检周期:${item['PATROL_PERIOD_NAME'] ?? ''}'), | ||||
| 
 | ||||
|               // isFinish | ||||
|               //     ? Text('涉及管道区域:${item['department'] ?? ''}') | ||||
|               //     : const SizedBox(height: 25), | ||||
|             ], | ||||
|           ), | ||||
|         ), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
|   Future<void> _startCheckItem(ProgressItem item) async { | ||||
| 
 | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|  | @ -144,7 +141,7 @@ class _HomeNfcDetailPageState extends State<HomeNfcDetailPage> { | |||
|       appBar: MyAppbar(title: '任务详情'), | ||||
|       body: SafeArea(child: Padding(padding: EdgeInsets.all(16), child: Column( | ||||
|         children: [ | ||||
|           _pendingTopCard(info), | ||||
|           _pendingTopCard(widget.info), | ||||
|           Expanded(child: Container( | ||||
|             decoration: BoxDecoration( | ||||
|               color: Colors.white, | ||||
|  | @ -162,6 +159,7 @@ class _HomeNfcDetailPageState extends State<HomeNfcDetailPage> { | |||
|               child: ProgressList( | ||||
|                 items: demoData, | ||||
|                 onStartCheck: (idx) { | ||||
|                   _startCheckItem(demoData[idx]); | ||||
|                   print('开始检查第 $idx 项'); | ||||
|                 }, | ||||
|               ), | ||||
|  |  | |||
|  | @ -1,4 +1,6 @@ | |||
| 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/my_appbar.dart'; | ||||
| import 'package:qhd_prevention/tools/tools.dart'; | ||||
|  | @ -14,66 +16,270 @@ class _HomeNfcListPageState extends State<HomeNfcListPage> | |||
|     with SingleTickerProviderStateMixin { | ||||
|   late TabController _tabController; | ||||
| 
 | ||||
|   // 测试数据 | ||||
|   final List<Map<String, String>> _pendingList = [ | ||||
|     { | ||||
|       '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, dynamic>> _pendingList = []; | ||||
|   final List<Map<String, dynamic>> _recordList = []; | ||||
| 
 | ||||
|   final List<Map<String, String>> _recordList = [ | ||||
|     { | ||||
|       'title': '设备巡检 A', | ||||
|       'status': '待巡检', | ||||
|       'department': '安全部', | ||||
|       'owner': '张三', | ||||
|       'unType': 'UN1001', | ||||
|       'cycle': '7天', | ||||
|       'points': '3/5', | ||||
|     }, | ||||
|   // 分页状态 - 待巡检 | ||||
|   int _pendingPage = 1; | ||||
|   final int _pageSize = 10; | ||||
|   bool _pendingLoading = false; | ||||
|   bool _pendingHasMore = true; | ||||
|   final ScrollController _pendingScrollController = ScrollController(); | ||||
| 
 | ||||
|   ]; | ||||
|   // 分页状态 - 巡检记录 | ||||
|   int _recordPage = 1; | ||||
|   bool _recordLoading = false; | ||||
|   bool _recordHasMore = true; | ||||
|   final ScrollController _recordScrollController = ScrollController(); | ||||
| 
 | ||||
|   // 页面 loading(用于首次加载指示) | ||||
|   bool _initialLoadingPending = false; | ||||
|   bool _initialLoadingRecord = false; | ||||
| 
 | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     _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 | ||||
|   void dispose() { | ||||
|     _tabController.dispose(); | ||||
|     _pendingScrollController.dispose(); | ||||
|     _recordScrollController.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 | ||||
|   Widget build(BuildContext context) { | ||||
|     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( | ||||
|         child: Column( | ||||
|           children: [ | ||||
|  | @ -94,7 +300,12 @@ class _HomeNfcListPageState extends State<HomeNfcListPage> | |||
|             Expanded( | ||||
|               child: TabBarView( | ||||
|                 controller: _tabController, | ||||
|                 children: [_buildPendingList(), _buildRecordList()], | ||||
|                 children: [ | ||||
|                   // 待巡检 - 支持下拉刷新和上拉加载 | ||||
|                   _buildPendingList(), | ||||
|                   // 巡检记录 - 支持下拉刷新和上拉加载 | ||||
|                   _buildRecordList(), | ||||
|                 ], | ||||
|               ), | ||||
|             ), | ||||
|           ], | ||||
|  | @ -104,48 +315,91 @@ class _HomeNfcListPageState extends State<HomeNfcListPage> | |||
|   } | ||||
| 
 | ||||
|   Widget _buildPendingList() { | ||||
|     if (_pendingList.isEmpty) { | ||||
|       return NoDataWidget.show(); // 无数据提示 | ||||
|     if (_initialLoadingPending && _pendingList.isEmpty) { | ||||
|       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), | ||||
|         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]; | ||||
|           return Container( | ||||
|           margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 0), | ||||
|             margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), | ||||
|             child: GestureDetector( | ||||
|               onTap: () { | ||||
|               pushPage(HomeNfcDetailPage(), context); | ||||
|                 pushPage(HomeNfcDetailPage(info: item), context); | ||||
|               }, | ||||
|               child: _pendingCard(item, false), | ||||
|             ), | ||||
|           ); | ||||
|         }, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   Widget _buildRecordList() { | ||||
|     if (_recordList.isEmpty) { | ||||
|       return NoDataWidget.show(); // 无数据提示 | ||||
|     if (_initialLoadingRecord && _recordList.isEmpty) { | ||||
|       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), | ||||
|         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]; | ||||
|           return Container( | ||||
|           margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 0), | ||||
|             margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), | ||||
|             child: _pendingCard(item, true), | ||||
|           ); | ||||
|         }, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   /// 构建待巡检卡片 | ||||
|   Widget _pendingCard(Map<String, String> item, bool isFinish) { | ||||
|   Widget _pendingCard(Map<String, dynamic> item, bool isFinish) { | ||||
|     return SizedBox( | ||||
|       height: 180, | ||||
|       child: Stack( | ||||
|  | @ -167,7 +421,7 @@ class _HomeNfcListPageState extends State<HomeNfcListPage> | |||
|             top: 12, | ||||
|             left: 12, | ||||
|             child: Text( | ||||
|               item['title']!, | ||||
|               item['TASK_NAME'] ?? '', | ||||
|               style: const TextStyle( | ||||
|                 color: Colors.black87, | ||||
|                 fontSize: 18, | ||||
|  | @ -181,15 +435,15 @@ class _HomeNfcListPageState extends State<HomeNfcListPage> | |||
|               right: 12, | ||||
|               child: Container( | ||||
|                 height: 30, | ||||
|                 padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), | ||||
|                 padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), | ||||
|                 decoration: BoxDecoration( | ||||
|                   color: Colors.blue.withOpacity(0.7), | ||||
|                   color: Colors.white.withOpacity(0.5), | ||||
|                   borderRadius: BorderRadius.circular(15), | ||||
|                 ), | ||||
|                 child: Center( | ||||
|                   child: Text( | ||||
|                     item['status']!, | ||||
|                     style: const TextStyle(color: Colors.white, fontSize: 14), | ||||
|                     item['INSPECTED_POINTS'] > 0 ? '巡检中' : '待巡检', | ||||
|                     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), | ||||
|                   bottomRight: Radius.circular(10), | ||||
|                 ), | ||||
|                 boxShadow: [ | ||||
|                 boxShadow: const [ | ||||
|                   BoxShadow( | ||||
|                     color: Colors.black12, | ||||
|                     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( | ||||
|       children: [ | ||||
|         Expanded( | ||||
|           child: Column( | ||||
|             crossAxisAlignment: CrossAxisAlignment.start, | ||||
|             children: [ | ||||
|               Text('负责部门:${item['department']}'), | ||||
|               Text('负责部门:${item['DEPARTMENT_NAME'] ?? ''}'), | ||||
|               const SizedBox(height: 8), | ||||
|               Text('负责人:${item['owner']}'), | ||||
|               Text('巡检类型:${item['PATROL_TYPE_NAME'] ?? ''}'), | ||||
|               const SizedBox(height: 8), | ||||
|               Text('UN件类型:${item['unType']}'), | ||||
|               Text('已巡点位:${item['INSPECTED_POINTS'] ?? '0'}'), | ||||
|             ], | ||||
|           ), | ||||
|         ), | ||||
|  | @ -247,11 +502,12 @@ class _HomeNfcListPageState extends State<HomeNfcListPage> | |||
|           child: Column( | ||||
|             crossAxisAlignment: CrossAxisAlignment.end, | ||||
|             children: [ | ||||
|               Text('巡检周期:${item['cycle']}'), | ||||
|               Text('负责人:${item['USER_NAME'] ?? ''}'), | ||||
|               const SizedBox(height: 8), | ||||
|               Text('已巡点位:${item['points']}'), | ||||
|               Text('巡检周期:${item['PATROL_PERIOD_NAME'] ?? ''}'), | ||||
| 
 | ||||
|               isFinish | ||||
|                   ? Text('涉及管道区域:${item['department']}') | ||||
|                   ? Text('涉及管道区域:${item['department'] ?? ''}') | ||||
|                   : const SizedBox(height: 25), | ||||
|             ], | ||||
|           ), | ||||
|  | @ -259,5 +515,4 @@ class _HomeNfcListPageState extends State<HomeNfcListPage> | |||
|       ], | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -95,15 +95,15 @@ class _MainPageState extends State<MainPage> { | |||
|         isBack: false, | ||||
|         actions: [ | ||||
|           if (_currentIndex == 0) ...[ | ||||
|             // IconButton( | ||||
|             //   onPressed: () => Navigator.push( | ||||
|             //     context, | ||||
|             //     MaterialPageRoute( | ||||
|             //         builder: (_) => NfcTestPage()), | ||||
|             //   ), | ||||
|             //   icon: Image.asset("assets/images/ai_img.png", | ||||
|             //       width: 20, height: 20), | ||||
|             // ), | ||||
|             IconButton( | ||||
|               onPressed: () => Navigator.push( | ||||
|                 context, | ||||
|                 MaterialPageRoute( | ||||
|                     builder: (_) => NfcTestPage()), | ||||
|               ), | ||||
|               icon: Image.asset("assets/images/ai_img.png", | ||||
|                   width: 20, height: 20), | ||||
|             ), | ||||
|             IconButton( | ||||
|               onPressed: () => Navigator.push( | ||||
|                 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 | ||||
| bool isBeforeStr(String a, String b) => compareYMdHmStrings(a, b) == -1; | ||||
| 
 | ||||
| /// ------------------------------------------------------ | ||||
| /// 防多次点击 | ||||
| /// ------------------------------------------------------ | ||||
|  | @ -434,10 +435,29 @@ class NoDataWidget { | |||
| class NativeOrientation { | ||||
|   static const MethodChannel _channel = MethodChannel('app.orientation'); | ||||
| 
 | ||||
|   static Future<void> setLandscape() async { | ||||
|     await _channel.invokeMethod('setOrientation', 'landscape'); | ||||
|   } | ||||
|   static Future<void> setPortrait() async { | ||||
|     await _channel.invokeMethod('setOrientation', 'portrait'); | ||||
|   static Future<bool> setLandscape() async { | ||||
|     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; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | @ -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 | ||||
| # 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. | ||||
| version: 1.0.1+5 | ||||
| version: 2.1.2+1 | ||||
| 
 | ||||
| environment: | ||||
|   sdk: ^3.7.0 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue