NFC巡检部分

main
hs 2025-08-25 11:09:23 +08:00
parent 2495e4b9d4
commit 6fd181636e
10 changed files with 1231 additions and 122 deletions

View File

@ -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&currentPage=$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&currentPage=$currentPage',
method: Method.post,
data: {
"CORPINFO_ID":SessionService.instance.corpinfoId,
"USER_ID":SessionService.instance.loginUserId,
},
);
}
}

View File

@ -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,)
],
),
),
),
);
}
}

View File

@ -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({}),
]
)
)
)
);
}
}

View File

@ -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');
},
),

View File

@ -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,
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<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>
],
);
}
}

View File

@ -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,

View File

@ -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;
}
/// tagNfcTag / 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 payloadstatus 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 (_) {}
}
}

15
lib/tools/dataTools.dart Normal file
View File

@ -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));
}

View File

@ -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<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<void> setPortrait() async {
await _channel.invokeMethod('setOrientation', 'portrait');
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;
}
}
}

View File

@ -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