From 6fd181636ea7222c288ae5218555058c0c7a2765 Mon Sep 17 00:00:00 2001 From: hs <873121290@qq.com> Date: Mon, 25 Aug 2025 11:09:23 +0800 Subject: [PATCH] =?UTF-8?q?NFC=E5=B7=A1=E6=A3=80=E9=83=A8=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/http/ApiService.dart | 71 +++ lib/pages/home/NFC/home_nfc_add_page.dart | 221 +++++++++ .../home/NFC/home_nfc_check_danger_page.dart | 136 ++++++ lib/pages/home/NFC/home_nfc_detail_page.dart | 38 +- lib/pages/home/NFC/home_nfc_list_page.dart | 429 ++++++++++++++---- lib/pages/main_tab.dart | 18 +- lib/services/nfc_service.dart | 393 ++++++++++++++++ lib/tools/dataTools.dart | 15 + lib/tools/tools.dart | 30 +- pubspec.yaml | 2 +- 10 files changed, 1231 insertions(+), 122 deletions(-) create mode 100644 lib/pages/home/NFC/home_nfc_add_page.dart create mode 100644 lib/pages/home/NFC/home_nfc_check_danger_page.dart create mode 100644 lib/services/nfc_service.dart create mode 100644 lib/tools/dataTools.dart diff --git a/lib/http/ApiService.dart b/lib/http/ApiService.dart index cceb950..762e5f8 100644 --- a/lib/http/ApiService.dart +++ b/lib/http/ApiService.dart @@ -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> getNfcPipeLineAreaList() { + return HttpManager().request( + baseNFCPath, + '/pipelineInspection/getPipelineAreaListAll', + method: Method.post, + data: { + "CORPINFO_ID":SessionService.instance.corpinfoId, + 'STATUS':'0' + }, + ); + } + + /// 管道设备列表 + static Future> 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> 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> 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> 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, + + }, + ); + } } diff --git a/lib/pages/home/NFC/home_nfc_add_page.dart b/lib/pages/home/NFC/home_nfc_add_page.dart new file mode 100644 index 0000000..76a1dc5 --- /dev/null +++ b/lib/pages/home/NFC/home_nfc_add_page.dart @@ -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 createState() => _HomeNfcAddPageState(); +} + +class _HomeNfcAddPageState extends State { + late Map pd = {}; + int currentPage = 1; + late List areaAllList = []; + late List equipmentList = []; + + @override + void initState() { + // TODO: implement initState + super.initState(); + _getData(); + } + + /// 获取管道区域列表 + Future _getData() async { + final data = await ApiService.getNfcPipeLineAreaList(); + if (data['result'] == 'success') { + areaAllList = data['varList']; + } + } + + /// 通过管道区域获取对应的设备列表 + Future _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( + 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( + 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 _startWrite() async{ + final textRules = >[ + {'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 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,) + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/home/NFC/home_nfc_check_danger_page.dart b/lib/pages/home/NFC/home_nfc_check_danger_page.dart new file mode 100644 index 0000000..7be7bcc --- /dev/null +++ b/lib/pages/home/NFC/home_nfc_check_danger_page.dart @@ -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 createState() => _HomeNfcCheckDangerPageState(); +} + +class _HomeNfcCheckDangerPageState extends State { + + + Widget _pendingTopCard(Map 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 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({}), + ] + ) + ) + ) + ); + } +} \ No newline at end of file diff --git a/lib/pages/home/NFC/home_nfc_detail_page.dart b/lib/pages/home/NFC/home_nfc_detail_page.dart index 22a7178..2ee6431 100644 --- a/lib/pages/home/NFC/home_nfc_detail_page.dart +++ b/lib/pages/home/NFC/home_nfc_detail_page.dart @@ -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 info; @override State createState() => _HomeNfcDetailPageState(); @@ -10,15 +11,6 @@ class HomeNfcDetailPage extends StatefulWidget { class _HomeNfcDetailPageState extends State { - Map info = { - 'title': '设备巡检 A', - 'status': '专项巡检', - 'department': '安全部', - 'owner': '张三', - 'unType': 'UN1001', - 'cycle': '7天', - 'points': '3/5', - }; final List 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 { ProgressItem(status: '已查', location: 'B区-配电室', code: 'XJ1002', checkTime: '2025-07-28 15:32'), ]; - Widget _pendingTopCard(Map item) { + Widget _pendingTopCard(Map item) { return SizedBox( height: 180, child: Stack( @@ -69,7 +61,7 @@ class _HomeNfcDetailPageState extends State { 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 { ); } /// 构建信息网格 - Widget _buildInfoGrid(Map item) { + Widget _buildInfoGrid(Map 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 { 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 _startCheckItem(ProgressItem item) async { + } @override Widget build(BuildContext context) { @@ -144,7 +141,7 @@ class _HomeNfcDetailPageState extends State { 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 { child: ProgressList( items: demoData, onStartCheck: (idx) { + _startCheckItem(demoData[idx]); print('开始检查第 $idx 项'); }, ), diff --git a/lib/pages/home/NFC/home_nfc_list_page.dart b/lib/pages/home/NFC/home_nfc_list_page.dart index a734f9e..e66b662 100644 --- a/lib/pages/home/NFC/home_nfc_list_page.dart +++ b/lib/pages/home/NFC/home_nfc_list_page.dart @@ -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 with SingleTickerProviderStateMixin { late TabController _tabController; - // 测试数据 - final List> _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> _pendingList = []; + final List> _recordList = []; - final List> _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 _refreshPending() async { + setState(() { + _pendingPage = 1; + _pendingHasMore = true; + _initialLoadingPending = true; + }); + try { + await _getTaskList(page: 1, replace: true); + } finally { + if (mounted) { + setState(() { + _initialLoadingPending = false; + }); + } + } + } + + Future _loadMorePending() async { + if (_pendingLoading || !_pendingHasMore) return; + await _getTaskList(page: _pendingPage + 1, replace: false); + } + + Future _refreshRecord() async { + setState(() { + _recordPage = 1; + _recordHasMore = true; + _initialLoadingRecord = true; + }); + try { + await _getTaskDetailList(page: 1, replace: true); + } finally { + if (mounted) { + setState(() { + _initialLoadingRecord = false; + }); + } + } + } + + Future _loadMoreRecord() async { + if (_recordLoading || !_recordHasMore) return; + await _getTaskDetailList(page: _recordPage + 1, replace: false); + } + + // ---------- 网络请求(适配后端返回结构) ---------- + //NFC任务列表 + Future _getTaskList({required int page, required bool replace}) async { + setState(() { + _pendingLoading = true; + }); + try { + final res = await ApiService.nfcTaskList(_pageSize, page); + // // 适配后端返回:尝试找到数组数据 + // List? list; + if (res['result'] == 'success') { + List list = res['varList']; + + final parsed = list.map>((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 _getTaskDetailList({required int page, required bool replace}) async { + setState(() { + _recordLoading = true; + }); + try { + final res = await ApiService.nfcTaskDetailList(_pageSize, page); + List? 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?; + // } + // list ??= []; + // + // final parsed = list.map>((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 Expanded( child: TabBarView( controller: _tabController, - children: [_buildPendingList(), _buildRecordList()], + children: [ + // 待巡检 - 支持下拉刷新和上拉加载 + _buildPendingList(), + // 巡检记录 - 支持下拉刷新和上拉加载 + _buildRecordList(), + ], ), ), ], @@ -104,48 +315,91 @@ class _HomeNfcListPageState extends State } 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, - padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 0), - itemBuilder: (context, index) { - final item = _pendingList[index]; - return Container( - margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 0), - child: GestureDetector( - onTap: (){ - pushPage(HomeNfcDetailPage(), context); - }, - child: _pendingCard(item, false), - ), - ); - }, + 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: 8), + child: GestureDetector( + onTap: () { + 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, - padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 0), - itemBuilder: (context, index) { - final item = _recordList[index]; - return Container( - margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 0), - child: _pendingCard(item, true), - ); - }, + + 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: 8), + child: _pendingCard(item, true), + ); + }, + ), ); } /// 构建待巡检卡片 - Widget _pendingCard(Map item, bool isFinish) { + Widget _pendingCard(Map item, bool isFinish) { return SizedBox( height: 180, child: Stack( @@ -167,7 +421,7 @@ class _HomeNfcListPageState extends State 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 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 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 } /// 构建信息网格 - Widget _buildInfoGrid(Map item, bool isFinish) { + Widget _buildInfoGrid(Map 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 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 ], ); } - } diff --git a/lib/pages/main_tab.dart b/lib/pages/main_tab.dart index 21b97c2..9d9a541 100644 --- a/lib/pages/main_tab.dart +++ b/lib/pages/main_tab.dart @@ -95,15 +95,15 @@ class _MainPageState extends State { 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, diff --git a/lib/services/nfc_service.dart b/lib/services/nfc_service.dart new file mode 100644 index 0000000..6e8d90a --- /dev/null +++ b/lib/services/nfc_service.dart @@ -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 scanning = ValueNotifier(false); + + /// 日志广播流(方便 UI 订阅) + final StreamController _logController = StreamController.broadcast(); + Stream get logs => _logController.stream; + + /// 检查设备是否支持 NFC + Future isAvailable() => NfcManager.instance.isAvailable(); + + /// 停止会话(若正在运行) + Future 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 / Uint8List) + List? _findFirstByteArray(dynamic value) { + if (value == null) return null; + if (value is Uint8List) return value.toList(); + if (value is List) 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 ''; + try { + final records = (msg is Map && msg['records'] != null) + ? List.from(msg['records']) + : (msg is dynamic ? (msg.records as List?) : null); + if (records == null || records.isEmpty) return ''; + 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) p = Uint8List.fromList(ptmp); + } else { + final pr = (r as dynamic).payload; + if (pr is Uint8List) p = pr; + else if (pr is List) 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 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) 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) 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 readOnceText({Duration timeout = const Duration(seconds: 10)}) async { + final completer = Completer(); + 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 = [status, ...langBytes, ...textBytes]; + return Uint8List.fromList(payload); + } + + /// 写入文本到标签(单条 Text record) + /// + /// - text: 要写入的文本 + /// - timeout: 超时时间(可选) + /// - onComplete: 回调 (ok, err) + /// + /// 返回 true 表示写入成功(同时 onComplete 也会被触发) + Future 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 (_) {} + } +} diff --git a/lib/tools/dataTools.dart b/lib/tools/dataTools.dart new file mode 100644 index 0000000..a38ab5e --- /dev/null +++ b/lib/tools/dataTools.dart @@ -0,0 +1,15 @@ +import 'dart:convert'; +import 'dart:typed_data'; +import 'dart:io'; + +Uint8List compressJson(Map data) { + final jsonStr = jsonEncode(data); + final utf8Bytes = utf8.encode(jsonStr); + final gzipBytes = GZipCodec().encode(utf8Bytes); + return Uint8List.fromList(gzipBytes); +} + +Map decompressJson(Uint8List bytes) { + final decoded = GZipCodec().decode(bytes); + return jsonDecode(utf8.decode(decoded)); +} diff --git a/lib/tools/tools.dart b/lib/tools/tools.dart index a905de1..a471be0 100644 --- a/lib/tools/tools.dart +++ b/lib/tools/tools.dart @@ -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 setLandscape() async { - await _channel.invokeMethod('setOrientation', 'landscape'); + static Future 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 setPortrait() async { - await _channel.invokeMethod('setOrientation', 'portrait'); + + static Future 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; + } } -} \ No newline at end of file +} diff --git a/pubspec.yaml b/pubspec.yaml index a312a81..93339b8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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