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