nfc巡检暂存
parent
566ab85147
commit
b5992b94ab
|
|
@ -491,9 +491,11 @@
|
|||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 8AKCJ9LW7D;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 8AKCJ9LW7D;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
|
|
@ -504,6 +506,7 @@
|
|||
PRODUCT_BUNDLE_IDENTIFIER = com.zhuoyun.qhdprevention.qhdPrevention;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "flutter-weihua";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
|
|
@ -682,9 +685,11 @@
|
|||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 8AKCJ9LW7D;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 8AKCJ9LW7D;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
|
|
@ -695,6 +700,7 @@
|
|||
PRODUCT_BUNDLE_IDENTIFIER = com.zhuoyun.qhdprevention.qhdPrevention;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "flutter-weihua";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
|
@ -710,9 +716,11 @@
|
|||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 8AKCJ9LW7D;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 8AKCJ9LW7D;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
|
|
@ -723,6 +731,7 @@
|
|||
PRODUCT_BUNDLE_IDENTIFIER = com.zhuoyun.qhdprevention.qhdPrevention;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "flutter-weihua";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
|
|
|
|||
|
|
@ -25,11 +25,7 @@
|
|||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NFCReaderUsageDescription</key>
|
||||
<string>用于读取 NFC 标签</string>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>remote-notification</string>
|
||||
</array>
|
||||
<string>需要NFC权限来读取和写入标签</string>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
|
|
@ -65,10 +61,16 @@
|
|||
<string>app需要发送通知提醒重要信息</string>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>remote-notification</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<false/>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
|
|
@ -83,7 +85,10 @@
|
|||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<false/>
|
||||
<key>com.apple.developer.nfc.readersession.formats</key>
|
||||
<array>
|
||||
<string>NDEF</string>
|
||||
<string>TAG</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -4,7 +4,10 @@
|
|||
<dict>
|
||||
<key>com.apple.developer.nfc.readersession.formats</key>
|
||||
<array>
|
||||
<string>NDEF</string>
|
||||
<string>TAG</string>
|
||||
</array>
|
||||
|
||||
</dict>
|
||||
|
||||
</plist>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 自定义默认按钮
|
||||
/// 自定义默认按钮(支持不可点击/禁用状态)
|
||||
class CustomButton extends StatelessWidget {
|
||||
final String text; // 按钮文字
|
||||
final Color backgroundColor; // 按钮背景色
|
||||
|
|
@ -11,6 +10,16 @@ class CustomButton extends StatelessWidget {
|
|||
final double? height; // 按钮高度
|
||||
final TextStyle? textStyle; // 文字样式
|
||||
|
||||
/// 新增:是否可点击(true 可点,false 禁用)
|
||||
/// 注意:如果 onPressed 为 null,也会被视为不可点击
|
||||
final bool enabled;
|
||||
|
||||
/// 新增:禁用时的背景色(可选)
|
||||
final Color? disabledBackgroundColor;
|
||||
|
||||
/// 新增:禁用时的文字颜色(可选)
|
||||
final Color? disabledTextColor;
|
||||
|
||||
const CustomButton({
|
||||
super.key,
|
||||
required this.text,
|
||||
|
|
@ -21,27 +30,56 @@ class CustomButton extends StatelessWidget {
|
|||
this.margin,
|
||||
this.height,
|
||||
this.textStyle,
|
||||
this.enabled = true,
|
||||
this.disabledBackgroundColor,
|
||||
this.disabledTextColor,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onPressed,
|
||||
// 如果 enabled 为 false 或 onPressed 为 null,则视为不可点击
|
||||
final bool isEnabled = enabled && onPressed != null;
|
||||
|
||||
// 计算展示用背景色与文字样式
|
||||
final Color bgColor = isEnabled
|
||||
? backgroundColor
|
||||
: (disabledBackgroundColor ?? Colors.grey.shade400);
|
||||
|
||||
TextStyle finalTextStyle;
|
||||
if (textStyle != null) {
|
||||
finalTextStyle = isEnabled
|
||||
? textStyle!
|
||||
: textStyle!.copyWith(
|
||||
color: disabledTextColor ?? textStyle!.color?.withOpacity(0.8) ?? Colors.white70,
|
||||
);
|
||||
} else {
|
||||
finalTextStyle = TextStyle(
|
||||
color: isEnabled ? Colors.white : (disabledTextColor ?? Colors.white70),
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
);
|
||||
}
|
||||
|
||||
// 点击拦截器 + 视觉反馈(禁用时降低不透明度)
|
||||
return Opacity(
|
||||
opacity: isEnabled ? 1.0 : 0.65,
|
||||
child: AbsorbPointer(
|
||||
absorbing: !isEnabled,
|
||||
child: GestureDetector(
|
||||
onTap: isEnabled ? onPressed : null,
|
||||
child: Container(
|
||||
height: height ?? 45, // 默认高度45
|
||||
padding: padding ?? const EdgeInsets.all(8), // 默认内边距
|
||||
margin: margin ?? const EdgeInsets.symmetric(horizontal: 5), // 默认外边距
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
color: backgroundColor,
|
||||
color: bgColor,
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
text,
|
||||
style: textStyle ?? const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
style: finalTextStyle,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ class MediaPickerRow extends StatefulWidget {
|
|||
final ValueChanged<String>? onMediaRemoved;
|
||||
final ValueChanged<String>? onMediaTapped; // 新增:媒体点击回调
|
||||
final bool isEdit; // 新增:控制编辑状态
|
||||
final bool isCamera; // 新增:只能拍照
|
||||
|
||||
|
||||
const MediaPickerRow({
|
||||
Key? key,
|
||||
|
|
@ -31,6 +33,7 @@ class MediaPickerRow extends StatefulWidget {
|
|||
this.onMediaRemoved,
|
||||
this.onMediaTapped, // 新增
|
||||
this.isEdit = true, // 默认可编辑
|
||||
this.isCamera = false,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
|
|
@ -53,7 +56,16 @@ class _MediaPickerGridState extends State<MediaPickerRow> {
|
|||
);
|
||||
});
|
||||
}
|
||||
Future<void> _cameraAction() async {
|
||||
XFile? picked = await _picker.pickImage(source: ImageSource.camera);
|
||||
|
||||
if (picked != null) {
|
||||
final path = picked.path;
|
||||
setState(() => _mediaPaths.add(path));
|
||||
widget.onChanged(_mediaPaths.map((p) => File(p)).toList());
|
||||
widget.onMediaAdded?.call(path);
|
||||
}
|
||||
}
|
||||
Future<void> _showPickerOptions() async {
|
||||
if (!widget.isEdit) return; // 不可编辑时直接返回
|
||||
|
||||
|
|
@ -237,7 +249,7 @@ class _MediaPickerGridState extends State<MediaPickerRow> {
|
|||
// 显示添加按钮
|
||||
else if (showAddButton) {
|
||||
return GestureDetector(
|
||||
onTap: _showPickerOptions,
|
||||
onTap: widget.isCamera?_cameraAction:_showPickerOptions,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.black12),
|
||||
|
|
@ -273,6 +285,8 @@ class RepairedPhotoSection extends StatefulWidget {
|
|||
final bool isRequired;
|
||||
final bool isShowNum;
|
||||
final bool isEdit; // 新增:控制编辑状态
|
||||
final bool isCamera; // 新增:只能拍照
|
||||
|
||||
|
||||
const RepairedPhotoSection({
|
||||
Key? key,
|
||||
|
|
@ -290,6 +304,7 @@ class RepairedPhotoSection extends StatefulWidget {
|
|||
this.isRequired = false,
|
||||
this.isShowNum = true,
|
||||
this.isEdit = true, // 默认可编辑
|
||||
this.isCamera = false,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
|
|
@ -331,6 +346,7 @@ class _RepairedPhotoSectionState extends State<RepairedPhotoSection> {
|
|||
maxCount: widget.maxCount,
|
||||
mediaType: widget.mediaType,
|
||||
initialMediaPaths: _mediaPaths,
|
||||
isCamera: widget.isCamera,
|
||||
onChanged: (files) {
|
||||
final newPaths = files.map((f) => f.path).toList();
|
||||
setState(() {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,3 +1,5 @@
|
|||
// main.dart
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:qhd_prevention/pages/badge_manager.dart';
|
||||
import 'package:qhd_prevention/services/auth_service.dart';
|
||||
|
|
@ -8,6 +10,7 @@ import './pages/main_tab.dart';
|
|||
import 'package:intl/date_symbol_data_local.dart';
|
||||
import 'http/HttpManager.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter/services.dart'; // for TextInput.hide
|
||||
|
||||
// 全局导航键
|
||||
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
|
@ -28,6 +31,36 @@ class GlobalMessage {
|
|||
}
|
||||
}
|
||||
|
||||
/// 全局 helper:在弹窗前取消焦点并尽量隐藏键盘,避免弹窗后键盘自动弹起
|
||||
Future<T?> showDialogAfterUnfocus<T>(BuildContext context, Widget dialog) async {
|
||||
// 取消焦点并尝试隐藏键盘
|
||||
FocusScope.of(context).unfocus();
|
||||
try {
|
||||
await SystemChannels.textInput.invokeMethod('TextInput.hide');
|
||||
} catch (_) {}
|
||||
// 给系统一点时间,避免竞态(100-200ms 足够)
|
||||
await Future.delayed(const Duration(milliseconds: 150));
|
||||
return showDialog<T>(context: context, builder: (_) => dialog);
|
||||
}
|
||||
|
||||
/// 同理:在展示底部模态前确保键盘隐藏
|
||||
Future<T?> showModalBottomSheetAfterUnfocus<T>({
|
||||
required BuildContext context,
|
||||
required WidgetBuilder builder,
|
||||
bool isScrollControlled = false,
|
||||
}) async {
|
||||
FocusScope.of(context).unfocus();
|
||||
try {
|
||||
await SystemChannels.textInput.invokeMethod('TextInput.hide');
|
||||
} catch (_) {}
|
||||
await Future.delayed(const Duration(milliseconds: 150));
|
||||
return showModalBottomSheet<T>(
|
||||
context: context,
|
||||
isScrollControlled: isScrollControlled,
|
||||
builder: builder,
|
||||
);
|
||||
}
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
|
|
@ -70,8 +103,6 @@ void main() async {
|
|||
// 如果本地标记已登录,进一步验证 token 是否有效
|
||||
try {
|
||||
isLoggedIn = await AuthService.isLoggedIn();
|
||||
// 这里建议 AuthService.isLoggedIn() 内部实际请求一次用户信息接口
|
||||
// 如果失败,就返回 false
|
||||
} catch (e) {
|
||||
isLoggedIn = false;
|
||||
}
|
||||
|
|
@ -80,6 +111,7 @@ void main() async {
|
|||
runApp(MyApp(isLoggedIn: isLoggedIn));
|
||||
}
|
||||
|
||||
/// MyApp:恢复为 Stateless(无需监听 viewInsets)
|
||||
class MyApp extends StatelessWidget {
|
||||
final bool isLoggedIn;
|
||||
|
||||
|
|
@ -90,13 +122,15 @@ class MyApp extends StatelessWidget {
|
|||
return MaterialApp(
|
||||
title: '',
|
||||
navigatorKey: navigatorKey,
|
||||
// 在路由变化时统一取消焦点(防止 push/pop 时焦点回到 TextField)
|
||||
navigatorObservers: [KeyboardUnfocusNavigatorObserver()],
|
||||
builder: (context, child) {
|
||||
return EasyLoading.init(
|
||||
builder: (context, widget) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () {
|
||||
// FocusScope.of(context).unfocus();
|
||||
// 全局点击空白处取消焦点(隐藏键盘)
|
||||
FocusHelper.clearFocus(context);
|
||||
},
|
||||
child: widget,
|
||||
|
|
@ -136,3 +170,38 @@ class MyApp extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// NavigatorObserver:在 push/pop/remove/replace 等路由变化时统一取消焦点
|
||||
class KeyboardUnfocusNavigatorObserver extends NavigatorObserver {
|
||||
void _unfocus() {
|
||||
try {
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
} catch (e) {
|
||||
debugPrint('NavigatorObserver unfocus error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didPush(Route route, Route? previousRoute) {
|
||||
_unfocus();
|
||||
super.didPush(route, previousRoute);
|
||||
}
|
||||
|
||||
@override
|
||||
void didPop(Route route, Route? previousRoute) {
|
||||
_unfocus();
|
||||
super.didPop(route, previousRoute);
|
||||
}
|
||||
|
||||
@override
|
||||
void didRemove(Route route, Route? previousRoute) {
|
||||
_unfocus();
|
||||
super.didRemove(route, previousRoute);
|
||||
}
|
||||
|
||||
@override
|
||||
void didReplace({Route? newRoute, Route? oldRoute}) {
|
||||
_unfocus();
|
||||
super.didReplace(newRoute: newRoute, oldRoute: oldRoute);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -526,11 +526,12 @@ class _QuickReportPageState extends State<QuickReportPage> {
|
|||
String latitude=position.latitude.toString();
|
||||
|
||||
try {
|
||||
Map data = {};
|
||||
final result = await ApiService.addRiskListCheckApp(
|
||||
hazardDescription, partDescription, latitude, longitude,
|
||||
dangerDetail, dataTime, type, responsibleId,
|
||||
yinHuanTypeIds, hazardLeve, buMenId, buMenPDId,
|
||||
yinHuanTypeNames, hiddenType1, hiddenType2, hiddenType3,);
|
||||
yinHuanTypeNames, hiddenType1, hiddenType2, hiddenType3,data);
|
||||
if (result['result'] == 'success') {
|
||||
|
||||
String hiddenId = result['pd']['HIDDEN_ID'] ;
|
||||
|
|
|
|||
|
|
@ -124,6 +124,27 @@ class _HomeNfcAddPageState extends State<HomeNfcAddPage> {
|
|||
);
|
||||
if (confirmed) {
|
||||
LoadingDialogHelper.show(message: '等待手机靠近NFC标签');
|
||||
NfcService.instance.startScanOnceWithCallback(
|
||||
onResult: (uid, parsedText, rawMsg) async {
|
||||
final result = await ApiService.nfcWriteCheck(uid);
|
||||
if (result['result'] == 'success') {
|
||||
_writeNFCInfoRequest();
|
||||
}else{
|
||||
ToastUtil.showError(context, result['result'] ?? '');
|
||||
}
|
||||
LoadingDialogHelper.hide();
|
||||
},
|
||||
onError: (err) {
|
||||
ToastUtil.showNormal(context, '$err');
|
||||
LoadingDialogHelper.hide();
|
||||
|
||||
},
|
||||
timeout: Duration(seconds: 12),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _writeNFCInfoRequest() async{
|
||||
await NfcService.instance.writeText(
|
||||
mapToCompactJson({'PIPELINE_AREA_ID':pd['PIPELINE_AREA_ID'],'EQUIPMENT_PIPELINE_ID':pd['EQUIPMENT_PIPELINE_ID']}),
|
||||
timeout: Duration(seconds: 12),
|
||||
|
|
@ -137,12 +158,17 @@ class _HomeNfcAddPageState extends State<HomeNfcAddPage> {
|
|||
}else{
|
||||
ToastUtil.showError(context, '写入失败,请重试');
|
||||
}
|
||||
}else{
|
||||
ToastUtil.showError(context, '$msg');
|
||||
|
||||
}
|
||||
LoadingDialogHelper.hide();
|
||||
},
|
||||
);
|
||||
LoadingDialogHelper.hide();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
String mapToCompactJson(Map<String, dynamic> map) {
|
||||
// 使用 jsonEncode 转换
|
||||
String jsonStr = jsonEncode(map);
|
||||
|
|
|
|||
|
|
@ -1,17 +1,207 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:qhd_prevention/customWidget/custom_button.dart';
|
||||
import 'package:qhd_prevention/customWidget/toast_util.dart';
|
||||
import 'package:qhd_prevention/http/ApiService.dart';
|
||||
import 'package:qhd_prevention/pages/home/NFC/home_nfc_detail_page.dart';
|
||||
import 'package:qhd_prevention/pages/home/NFC/nfc_check_danger_detail.dart';
|
||||
import 'package:qhd_prevention/pages/home/tap/item_list_widget.dart';
|
||||
import 'package:qhd_prevention/pages/my_appbar.dart';
|
||||
import 'package:qhd_prevention/tools/tools.dart';
|
||||
|
||||
/// OptionData 模型
|
||||
class OptionData {
|
||||
final String value;
|
||||
final String label;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
|
||||
const OptionData({
|
||||
required this.value,
|
||||
required this.label,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
});
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is OptionData &&
|
||||
runtimeType == other.runtimeType &&
|
||||
value == other.value &&
|
||||
label == other.label &&
|
||||
icon == other.icon &&
|
||||
color == other.color;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
value.hashCode ^ label.hashCode ^ icon.hashCode ^ color.hashCode;
|
||||
}
|
||||
|
||||
class HomeNfcCheckDangerPage extends StatefulWidget {
|
||||
const HomeNfcCheckDangerPage({super.key});
|
||||
const HomeNfcCheckDangerPage({
|
||||
super.key,
|
||||
required this.info,
|
||||
required this.facebookImages,
|
||||
required this.isNfcError
|
||||
});
|
||||
|
||||
final Map info;
|
||||
final List<String> facebookImages;
|
||||
// nfc异常上报
|
||||
final bool isNfcError;
|
||||
|
||||
@override
|
||||
State<HomeNfcCheckDangerPage> createState() => _HomeNfcCheckDangerPageState();
|
||||
}
|
||||
|
||||
class _HomeNfcCheckDangerPageState extends State<HomeNfcCheckDangerPage> {
|
||||
late Map<String, dynamic> pd = {};
|
||||
OptionData? selectType; // 初始为 null(未选择)
|
||||
|
||||
final List<OptionData> _options = const [
|
||||
OptionData(
|
||||
value: "option1",
|
||||
label: "合格",
|
||||
icon: Icons.check_circle_rounded,
|
||||
color: Colors.green,
|
||||
),
|
||||
OptionData(
|
||||
value: "option2",
|
||||
label: "不合格",
|
||||
icon: Icons.check_circle_rounded,
|
||||
color: Colors.green,
|
||||
),
|
||||
OptionData(
|
||||
value: "option3",
|
||||
label: "不涉及",
|
||||
icon: Icons.check_circle_rounded,
|
||||
color: Colors.green,
|
||||
),
|
||||
];
|
||||
@override
|
||||
void initState() {
|
||||
// TODO: implement initState
|
||||
super.initState();
|
||||
final bool unchecked = widget.info['INSPECTED_FLAG'] == '0';
|
||||
if (!unchecked) { // 已经巡检过
|
||||
for (OptionData data in _options) {
|
||||
if (data.label == widget.info['INSPECTION_RESULT']) {
|
||||
selectType = data;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (selectType != null && selectType?.label == '不合格') { // 不合格的话要获取上传过的隐患记录
|
||||
_getCheckRecord();
|
||||
}
|
||||
|
||||
Widget _pendingTopCard(Map<String, String> item) {
|
||||
}
|
||||
Future<void> _getCheckRecord() async{
|
||||
Map data = {'PATROL_RECORD_DETAIL_ID': widget.info['PATROL_RECORD_DETAIL_ID']};
|
||||
final result = await ApiService.nfcDangerRecord(data);
|
||||
if (result['result'] == 'success') {
|
||||
setState(() {
|
||||
pd = result['pd'];
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Future<void> _submit() async {
|
||||
// 保护:如果未选择任何选项就不提交
|
||||
if (selectType == null) {
|
||||
// 可选:提示用户选择
|
||||
ToastUtil.showNormal(context, '请先选择检查结果');
|
||||
return;
|
||||
}
|
||||
Map data = {
|
||||
...widget.info,
|
||||
'INSPECTION_RESULT': selectType?.label ?? '',
|
||||
'TYPE': '0', // 记录类型(0-正常检查记录 1-超期未检查记录)
|
||||
};
|
||||
LoadingDialogHelper.show();
|
||||
// 如果选中的是不合格,需要特殊处理
|
||||
if (selectType?.label == '不合格') {
|
||||
List imgList = pd['imgList'] ?? [];
|
||||
List _videos = pd['videoList'] ?? [];
|
||||
List zgImgList = pd['gzImageList'] ?? [];
|
||||
for (int i = 0; i < imgList.length; i++) {
|
||||
await _reloadFeedBack(imgList[i], '3');
|
||||
}
|
||||
for (int i = 0; i < _videos.length; i++) {
|
||||
await _reloadFeedBack(_videos[i], '3');
|
||||
}
|
||||
for (int i = 0; i < zgImgList.length; i++) {
|
||||
await _reloadFeedBack(zgImgList[i], '4');
|
||||
}
|
||||
data = {...data,...pd};
|
||||
|
||||
}
|
||||
|
||||
if (widget.facebookImages.isNotEmpty) {
|
||||
// 手动上报 nfc 异常图片(按顺序上传)
|
||||
final List<String> uploaded = [];
|
||||
for (int i = 0; i < widget.facebookImages.length; i++) {
|
||||
String imagePath = await _reloadFeedBack(widget.facebookImages[i], '30');
|
||||
if (imagePath.isNotEmpty) {
|
||||
uploaded.add(imagePath);
|
||||
}
|
||||
}
|
||||
if (uploaded.isNotEmpty) {
|
||||
data['PHOTO_URL'] = uploaded.join(',');
|
||||
}
|
||||
}
|
||||
data['CHECK_CONTENT'] = widget.info['INSPECTION_CONTENT'];
|
||||
final result = await ApiService.nfcChekSubmit(data);
|
||||
LoadingDialogHelper.hide();
|
||||
if (result['result'] == 'success') {
|
||||
if (widget.isNfcError) { // 如果手动检查上传nfc异常,多退一个路由
|
||||
Navigator.of(context).pop();
|
||||
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
|
||||
} else {
|
||||
// 可选:根据返回显示错误
|
||||
ToastUtil.showNormal(context, result['result']?.toString() ?? '提交失败');
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> _reloadFeedBack(String imagePath, String type) async {
|
||||
try {
|
||||
Map data = {
|
||||
'TYPE': type,
|
||||
'FOREIGN_KEY': widget.info['EQUIPMENT_PIPELINE_ID'],
|
||||
};
|
||||
final raw = await ApiService.addNormalImgFiles(imagePath, data);
|
||||
if (raw['result'] == 'success') {
|
||||
Map pd = raw['pd'];
|
||||
return pd['FILEPATH'] ?? "";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
} catch (e) {
|
||||
// 出错时可以 Toast 或者在页面上显示错误状态
|
||||
debugPrint('加载首页数据失败:$e');
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
void _pushDangerDetail() async {
|
||||
// pushPage 可能返回 pd,保持原逻辑:等待详情页返回新的 pd
|
||||
final result = await pushPage<Map<String, dynamic>>(
|
||||
NfcCheckDangerDetail(info: pd),
|
||||
context,
|
||||
);
|
||||
if (result != null && result.isNotEmpty) {
|
||||
setState(() {
|
||||
// 将选中项设置为“不合格”
|
||||
selectType = _options[1];
|
||||
pd = result;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Widget _pendingTopCard(Map item) {
|
||||
return SizedBox(
|
||||
height: 180,
|
||||
child: Stack(
|
||||
|
|
@ -32,7 +222,7 @@ class _HomeNfcCheckDangerPageState extends State<HomeNfcCheckDangerPage> {
|
|||
top: 12,
|
||||
left: 12,
|
||||
child: Text(
|
||||
item['title']!,
|
||||
item['TASK_NAME']?.toString() ?? '',
|
||||
style: const TextStyle(
|
||||
color: Colors.black87,
|
||||
fontSize: 18,
|
||||
|
|
@ -52,7 +242,7 @@ class _HomeNfcCheckDangerPageState extends State<HomeNfcCheckDangerPage> {
|
|||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
item['status']!,
|
||||
item['PATROL_TYPE_NAME'] ?? '',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 14),
|
||||
),
|
||||
),
|
||||
|
|
@ -66,11 +256,11 @@ class _HomeNfcCheckDangerPageState extends State<HomeNfcCheckDangerPage> {
|
|||
top: 50, // 盖住图片底部
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 0),
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(5),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
boxShadow: [
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Colors.black12,
|
||||
blurRadius: 4,
|
||||
|
|
@ -86,32 +276,126 @@ class _HomeNfcCheckDangerPageState extends State<HomeNfcCheckDangerPage> {
|
|||
);
|
||||
}
|
||||
|
||||
/// 构建信息网格
|
||||
Widget _buildInfoGrid(Map<String, String> item) {
|
||||
return Row(
|
||||
// 构建单选按钮(现在 option 为 OptionData)
|
||||
Widget _buildOptionButton({
|
||||
required BuildContext context,
|
||||
required OptionData option,
|
||||
required double screenWidth,
|
||||
required dynamic item,
|
||||
VoidCallback? onImageTap,
|
||||
}) {
|
||||
final String value = option.value;
|
||||
final String label = option.label;
|
||||
final icon = option.icon;
|
||||
final color = option.color;
|
||||
final bool isSelected = selectType?.value == option.value;
|
||||
final buttonWidth = (screenWidth - 60) / 3 - 10; // 计算按钮宽度
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
if (value != "option2") {
|
||||
selectType = option;
|
||||
} else {
|
||||
// 选择“不合格”需要进入详情页面
|
||||
_pushDangerDetail();
|
||||
}
|
||||
});
|
||||
},
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Container(
|
||||
width: buttonWidth,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
SizedBox(
|
||||
height: 30,
|
||||
width: 90,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, color: isSelected ? color : Colors.grey, size: 30),
|
||||
const SizedBox(width: 8),
|
||||
Flexible(
|
||||
child: Text(
|
||||
label,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight:
|
||||
isSelected ? FontWeight.bold : FontWeight.normal,
|
||||
color: isSelected ? color : Colors.grey[600],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
if ((value == "option1" && item["REFERENCE_BASIS"] == "option1") ||
|
||||
(value == "option2" &&
|
||||
item["REFERENCE_BASIS"] == "option2" &&
|
||||
item.containsKey("ids") &&
|
||||
item["ids"].toString().isNotEmpty))
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
if (onImageTap != null) {
|
||||
onImageTap();
|
||||
}
|
||||
},
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Transform.translate(
|
||||
offset: const Offset(0, -6),
|
||||
child: Image.asset(
|
||||
"assets/images/gantan-blue.png",
|
||||
width: 15,
|
||||
height: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建信息网格
|
||||
Widget _buildInfoGrid(Map item) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('负责部门:${item['department']}'),
|
||||
const SizedBox(height: 8),
|
||||
Text('负责人:${item['owner']}'),
|
||||
const SizedBox(height: 8),
|
||||
Text('UN件类型:${item['unType']}'),
|
||||
],
|
||||
ItemListWidget.singleLineTitleText(
|
||||
label: '检查项:',
|
||||
isEditable: false,
|
||||
text: item['EQUIPMENT_NAME'] ?? '',
|
||||
),
|
||||
ItemListWidget.singleLineTitleText(
|
||||
label: '检查内容:',
|
||||
isEditable: false,
|
||||
text: item['INSPECTION_CONTENT'] ?? '',
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text('巡检周期:${item['cycle']}'),
|
||||
const SizedBox(height: 8),
|
||||
Text('已巡点位:${item['points']}'),
|
||||
Text('涉及管道区域:${item['department']}')
|
||||
],
|
||||
),
|
||||
// 单选按钮组
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: _options.map((option) {
|
||||
return _buildOptionButton(
|
||||
context: context,
|
||||
option: option,
|
||||
screenWidth: screenWidth,
|
||||
item: item,
|
||||
onImageTap: () {
|
||||
if (item["REFERENCE_BASIS"] == "option1") {
|
||||
// _getAlreadyUpImages(item);
|
||||
} else if (item["REFERENCE_BASIS"] == "option2") {
|
||||
// _goUnqualifiedPage(item);
|
||||
}
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
@ -119,18 +403,28 @@ class _HomeNfcCheckDangerPageState extends State<HomeNfcCheckDangerPage> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bool canSubmit = selectType != null; // 只有选中后才可以提交
|
||||
|
||||
return Scaffold(
|
||||
appBar: MyAppbar(title: '检查项'),
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_pendingTopCard({}),
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
_pendingTopCard(widget.info),
|
||||
const Spacer(),
|
||||
CustomButton(
|
||||
enabled: canSubmit,
|
||||
text: '提交',
|
||||
backgroundColor: Colors.blue,
|
||||
onPressed: canSubmit ? _submit : null,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,24 +2,32 @@ import 'dart:async';
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:qhd_prevention/customWidget/custom_alert_dialog.dart';
|
||||
import 'package:qhd_prevention/customWidget/custom_button.dart';
|
||||
import 'package:qhd_prevention/customWidget/toast_util.dart';
|
||||
import 'package:qhd_prevention/http/ApiService.dart';
|
||||
import 'package:qhd_prevention/pages/home/NFC/home_nfc_check_danger_page.dart';
|
||||
import 'package:qhd_prevention/pages/home/NFC/nfc_question_fecebook.dart';
|
||||
import 'package:qhd_prevention/pages/home/tap/item_list_widget.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 HomeNfcDetailPage extends StatefulWidget {
|
||||
const HomeNfcDetailPage({super.key, required this.info});
|
||||
|
||||
final Map<String, dynamic> info;
|
||||
final Map info;
|
||||
|
||||
@override
|
||||
State<HomeNfcDetailPage> createState() => _HomeNfcDetailPageState();
|
||||
}
|
||||
|
||||
class _HomeNfcDetailPageState extends State<HomeNfcDetailPage> {
|
||||
List<ProgressItem> items = [];
|
||||
List<dynamic> items = [];
|
||||
int currentPage = 1;
|
||||
final int pageSize = 10;
|
||||
late var _total = 0;
|
||||
late var _totalResult = 0;
|
||||
|
||||
bool isLoading = false; // 当前请求中
|
||||
bool hasMore = true; // 是否还有更多数据
|
||||
|
|
@ -61,11 +69,15 @@ class _HomeNfcDetailPageState extends State<HomeNfcDetailPage> {
|
|||
}
|
||||
|
||||
try {
|
||||
// 调用后端 API:我保持你原来的签名 ApiService.nfcTaskDetailList(pageSize, page)
|
||||
Map data = {
|
||||
"PATROL_TASK_ID": widget.info['PATROL_TASK_ID'] ?? '',
|
||||
"PERIOD_START_TIME": widget.info['PERIOD_START_TIME'] ?? '',
|
||||
"PERIOD_END_TIME": widget.info['PERIOD_END_TIME'] ?? '',
|
||||
};
|
||||
final result = await ApiService.nfcTaskDetailList(
|
||||
pageSize,
|
||||
currentPage,
|
||||
widget.info['PATROL_TASK_ID'] ?? '',
|
||||
data,
|
||||
);
|
||||
|
||||
if (result == null) {
|
||||
|
|
@ -74,35 +86,20 @@ class _HomeNfcDetailPageState extends State<HomeNfcDetailPage> {
|
|||
}
|
||||
printLongString(jsonEncode(result));
|
||||
if (result['result'] == 'success') {
|
||||
// 兼容常见返回字段,尽量从 data / rows / list 里取
|
||||
final dynamic rawList = result['varList'];
|
||||
|
||||
List<ProgressItem> fetched = [];
|
||||
if (rawList is List) {
|
||||
fetched =
|
||||
rawList.map<ProgressItem>((e) {
|
||||
if (e is ProgressItem) return e;
|
||||
if (e is Map<String, dynamic>) return ProgressItem.fromJson(e);
|
||||
return ProgressItem(
|
||||
status: '未查',
|
||||
location: e?.toString() ?? '',
|
||||
code: '',
|
||||
checkTime: null,
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
setState(() {
|
||||
Map page = result['page'];
|
||||
_totalResult = page['totalResult'] as int;
|
||||
_total = result['checkedCount'] as int;
|
||||
if (refresh) {
|
||||
items = fetched;
|
||||
items = rawList;
|
||||
} else {
|
||||
items.addAll(fetched);
|
||||
items.addAll(rawList);
|
||||
}
|
||||
// 如果本次返回少于 pageSize 则说明没有更多
|
||||
if (fetched.length < pageSize) {
|
||||
if (rawList.length < pageSize) {
|
||||
hasMore = false;
|
||||
} else {
|
||||
// 成功拿到一页,页码自增
|
||||
currentPage++;
|
||||
}
|
||||
});
|
||||
|
|
@ -126,23 +123,74 @@ class _HomeNfcDetailPageState extends State<HomeNfcDetailPage> {
|
|||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
|
||||
}
|
||||
|
||||
Future<void> _startCheckItem(ProgressItem item, int index) async {
|
||||
// TODO: 根据业务替换为真实逻辑(例如跳转到检查页面或调用写 NFC)
|
||||
// 这里示例:将当前项标记为已查并设置检查时间(仅本地更新)
|
||||
final now = DateTime.now();
|
||||
setState(() {
|
||||
items[index] = items[index].copyWith(
|
||||
status: '已查',
|
||||
checkTime:
|
||||
'${now.year}-${_two(now.month)}-${_two(now.day)} ${_two(now.hour)}:${_two(now.minute)}',
|
||||
Future<void> _startCheckItem(Map item, int index) async {
|
||||
final confirmed = await CustomAlertDialog.showConfirm(
|
||||
context,
|
||||
title: '温馨提示',
|
||||
content: '请将手机贴近设备标签',
|
||||
cancelText: '',
|
||||
confirmText: '我知道了',
|
||||
barrierDismissible: false,
|
||||
);
|
||||
});
|
||||
if (confirmed) {
|
||||
LoadingDialogHelper.show(message: '等待设备靠近设备NFC标签');
|
||||
NfcService.instance.startScanOnceWithCallback(
|
||||
onResult: (uid, parsedText, rawMsg) async {
|
||||
_getNFCForUid(uid, parsedText);
|
||||
},
|
||||
onError: (err) {
|
||||
|
||||
// 额外:你可能需要调用后端接口上报检查结果
|
||||
// await ApiService.reportCheck(items[index].code, ...);
|
||||
ToastUtil.showError(context, '$err');
|
||||
LoadingDialogHelper.hide();
|
||||
},
|
||||
timeout: Duration(seconds: 12),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _two(int v) => v.toString().padLeft(2, '0');
|
||||
Future<void> _getNFCForUid(String uid, String parsedText) async {
|
||||
if (uid.isEmpty || parsedText.isEmpty) {
|
||||
ToastUtil.showError(context, 'NFC设备标签数据为空');
|
||||
LoadingDialogHelper.hide();
|
||||
|
||||
return;
|
||||
}
|
||||
Map result = {};
|
||||
// mapToCompactJson({'PIPELINE_AREA_ID':pd['PIPELINE_AREA_ID'],'EQUIPMENT_PIPELINE_ID':pd['EQUIPMENT_PIPELINE_ID']}),
|
||||
for (Map item in items) {
|
||||
if (parsedText.isNotEmpty && item['NFC_CODE'] == uid) {
|
||||
try{
|
||||
Map parsedData = jsonDecode(parsedText);
|
||||
if (parsedData['PIPELINE_AREA_ID'] == item['PIPELINE_AREA_ID'] &&
|
||||
parsedData['EQUIPMENT_PIPELINE_ID'] == item['EQUIPMENT_PIPELINE_ID']) {
|
||||
result = item;
|
||||
}
|
||||
LoadingDialogHelper.hide();
|
||||
|
||||
}catch(e){
|
||||
LoadingDialogHelper.hide();
|
||||
|
||||
ToastUtil.showError(context, 'NFC设备数据错误');
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
if (result.isEmpty) {
|
||||
LoadingDialogHelper.hide();
|
||||
|
||||
ToastUtil.showError(context, 'NFC设备不在当前任务中');
|
||||
return;
|
||||
}
|
||||
LoadingDialogHelper.hide();
|
||||
|
||||
Map data = {...result, ...widget.info, "NFC_CODE": uid, 'MANUAL_CONFIRMATION': '0',
|
||||
};
|
||||
await pushPage(
|
||||
HomeNfcCheckDangerPage(info: data, facebookImages: [], isNfcError: false,),
|
||||
context,
|
||||
);
|
||||
_getTaskDetail(refresh: true);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
|
|
@ -150,7 +198,7 @@ class _HomeNfcDetailPageState extends State<HomeNfcDetailPage> {
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
Widget _pendingTopCard(Map<String, dynamic> item) {
|
||||
Widget _pendingTopCard(Map item) {
|
||||
return SizedBox(
|
||||
height: 180,
|
||||
child: Stack(
|
||||
|
|
@ -232,9 +280,8 @@ class _HomeNfcDetailPageState extends State<HomeNfcDetailPage> {
|
|||
}
|
||||
|
||||
/// 构建信息网格
|
||||
Widget _buildInfoGrid(Map<String, dynamic> item) {
|
||||
return Expanded(
|
||||
child: Column(
|
||||
Widget _buildInfoGrid(Map item) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// const SizedBox(height: 8),
|
||||
|
|
@ -264,13 +311,12 @@ class _HomeNfcDetailPageState extends State<HomeNfcDetailPage> {
|
|||
text: item['OPERATTIME'] ?? '',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildListItem(BuildContext context, int idx) {
|
||||
final item = items[idx];
|
||||
final bool unchecked = item.status == '未查';
|
||||
final bool unchecked = item['INSPECTED_FLAG'] == '0';
|
||||
|
||||
return Container(
|
||||
height: 100,
|
||||
|
|
@ -294,7 +340,7 @@ class _HomeNfcDetailPageState extends State<HomeNfcDetailPage> {
|
|||
Positioned(
|
||||
top: 2,
|
||||
child: Text(
|
||||
item.status,
|
||||
unchecked ? '未查' : '已查',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 14),
|
||||
),
|
||||
),
|
||||
|
|
@ -309,11 +355,22 @@ class _HomeNfcDetailPageState extends State<HomeNfcDetailPage> {
|
|||
|
||||
// 中间详情
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: (){
|
||||
if (!unchecked) {
|
||||
Map data = {...item, ...widget.info, "NFC_CODE": item['PIPELINE_AREA_ID'], 'MANUAL_CONFIRMATION': '0',
|
||||
};
|
||||
pushPage(
|
||||
HomeNfcCheckDangerPage(info: data, facebookImages: [], isNfcError: false,),
|
||||
context,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
item.location,
|
||||
item['PIPELINE_AREA_NAME'] ?? '',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
|
|
@ -322,38 +379,73 @@ class _HomeNfcDetailPageState extends State<HomeNfcDetailPage> {
|
|||
overflow: TextOverflow.ellipsis, // 超出省略号
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text('NFC编码:${item.code}'),
|
||||
Text('NFC编码:${item['NFC_CODE'] ?? ''}'),
|
||||
const SizedBox(height: 6),
|
||||
unchecked
|
||||
? InkWell(
|
||||
? Row(
|
||||
spacing: 10,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: InkWell(
|
||||
onTap: () => _startCheckItem(item, idx),
|
||||
child: Container(
|
||||
height: 35,
|
||||
// width: 120,
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.symmetric(vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFFFFA726), Color(0xFFFF7043)],
|
||||
colors: [
|
||||
Color(0xFFFFA726),
|
||||
Color(0xFFFF7043),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
child: const Text(
|
||||
'开始检查',
|
||||
style: TextStyle(color: Colors.white, fontSize: 14),
|
||||
'NFC检查',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
CustomButton(
|
||||
onPressed: () {
|
||||
pushPage(
|
||||
NfcQuestionFecebook(
|
||||
info: item,
|
||||
taskInfo: widget.info,
|
||||
),
|
||||
context,
|
||||
);
|
||||
},
|
||||
text: '手动检查',
|
||||
height: 35,
|
||||
textStyle: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 15,
|
||||
),
|
||||
backgroundColor: Colors.blue,
|
||||
),
|
||||
],
|
||||
)
|
||||
: Text(
|
||||
'检查时间:${item.checkTime ?? ''}',
|
||||
'检查时间:${item['PATROL_TIME'] ?? ''}',
|
||||
style: const TextStyle(fontSize: 14),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
),
|
||||
|
||||
// 右侧箭头
|
||||
const SizedBox(width: 8),
|
||||
if (!unchecked)
|
||||
const Icon(Icons.chevron_right, color: Colors.grey),
|
||||
],
|
||||
),
|
||||
|
|
@ -371,6 +463,7 @@ class _HomeNfcDetailPageState extends State<HomeNfcDetailPage> {
|
|||
children: [
|
||||
_pendingTopCard(widget.info),
|
||||
const SizedBox(height: 60),
|
||||
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
|
|
@ -384,6 +477,73 @@ class _HomeNfcDetailPageState extends State<HomeNfcDetailPage> {
|
|||
),
|
||||
],
|
||||
),
|
||||
// 白色容器内部:统计行 + 列表(统计行放在列表顶部)
|
||||
child: Column(
|
||||
children: [
|
||||
// 统计行(放在容器顶部,作为视觉上的头部)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 10,
|
||||
),
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 5,
|
||||
vertical: 8,
|
||||
),
|
||||
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'已查点位 $_total / $_totalResult ',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (isLoading)
|
||||
const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
else
|
||||
GestureDetector(
|
||||
onTap:
|
||||
() => _getTaskDetail(refresh: true),
|
||||
child: Row(
|
||||
children: const [
|
||||
Icon(
|
||||
Icons.refresh,
|
||||
size: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
SizedBox(width: 4),
|
||||
Text(
|
||||
'刷新',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// 列表区域(占用剩余空间)
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () => _getTaskDetail(refresh: true),
|
||||
child:
|
||||
|
|
@ -396,36 +556,38 @@ class _HomeNfcDetailPageState extends State<HomeNfcDetailPage> {
|
|||
)
|
||||
: items.isEmpty
|
||||
? ListView(
|
||||
// 保持可下拉刷新
|
||||
controller: _scrollController,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
physics:
|
||||
const AlwaysScrollableScrollPhysics(),
|
||||
children: [
|
||||
const SizedBox(height: 80),
|
||||
const SizedBox(height: 40),
|
||||
Center(
|
||||
child: Text(isLoading ? '加载中...' : '暂无数据'),
|
||||
child: Text(
|
||||
isLoading ? '加载中...' : '暂无数据',
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: ListView.builder(
|
||||
controller: _scrollController,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
itemCount: items.length + 1, // 多一个用于加载状态/底部提示
|
||||
physics:
|
||||
const AlwaysScrollableScrollPhysics(),
|
||||
itemCount:
|
||||
items.length + 1, // 多一个用于加载状态/底部提示
|
||||
itemBuilder: (ctx, idx) {
|
||||
if (idx < items.length) {
|
||||
return _buildListItem(ctx, idx);
|
||||
} else {
|
||||
// 底部加载/没有更多提示
|
||||
if (hasMore) {
|
||||
// 触发加载(以防没有触发)
|
||||
if (!isLoading) {
|
||||
// 预防性触发下一页
|
||||
// _getTaskDetail(); // 不在此直接调用,scroll listener 会负责
|
||||
}
|
||||
return const Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 12,
|
||||
),
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
child:
|
||||
CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
|
|
@ -441,6 +603,9 @@ class _HomeNfcDetailPageState extends State<HomeNfcDetailPage> {
|
|||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -449,96 +614,3 @@ class _HomeNfcDetailPageState extends State<HomeNfcDetailPage> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 单条进度数据模型
|
||||
class ProgressItem {
|
||||
final String status; // “未查” 或 “已查”
|
||||
final String location; // 地点
|
||||
final String code; // 编码
|
||||
final String? checkTime; // 检查时间(已查时有值)
|
||||
|
||||
ProgressItem({
|
||||
required this.status,
|
||||
required this.location,
|
||||
required this.code,
|
||||
this.checkTime,
|
||||
});
|
||||
|
||||
ProgressItem copyWith({
|
||||
String? status,
|
||||
String? location,
|
||||
String? code,
|
||||
String? checkTime,
|
||||
}) {
|
||||
return ProgressItem(
|
||||
status: status ?? this.status,
|
||||
location: location ?? this.location,
|
||||
code: code ?? this.code,
|
||||
checkTime: checkTime ?? this.checkTime,
|
||||
);
|
||||
}
|
||||
|
||||
/// 宽容解析后端返回(根据常见字段名)
|
||||
factory ProgressItem.fromJson(Map<String, dynamic> json) {
|
||||
String status =
|
||||
(json['status'] ??
|
||||
json['STATUS'] ??
|
||||
(json['checked'] == true ? '已查' : null) ??
|
||||
(json['is_checked'] == 1 ? '已查' : null) ??
|
||||
json['state'] ??
|
||||
json['check_status'])
|
||||
?.toString() ??
|
||||
'';
|
||||
|
||||
if (status.isEmpty) {
|
||||
// 有些 API 用 0/1 表示
|
||||
final s = json['status'] ?? json['STATUS'] ?? json['check_status'];
|
||||
if (s is num) {
|
||||
status = (s == 0) ? '未查' : '已查';
|
||||
} else if (s is String && (s == '0' || s == '1')) {
|
||||
status = (s == '0') ? '未查' : '已查';
|
||||
}
|
||||
}
|
||||
if (status.isEmpty) status = '未查';
|
||||
|
||||
final location =
|
||||
(json['location'] ??
|
||||
json['LOCATION'] ??
|
||||
json['point_name'] ??
|
||||
json['address'] ??
|
||||
json['point'] ??
|
||||
'')
|
||||
.toString();
|
||||
final code =
|
||||
(json['code'] ??
|
||||
json['CODE'] ??
|
||||
json['nfcCode'] ??
|
||||
json['nfc_code'] ??
|
||||
json['NFC_CODE'] ??
|
||||
json['id'] ??
|
||||
'')
|
||||
.toString();
|
||||
final checkTime =
|
||||
(json['checkTime'] ??
|
||||
json['CHECK_TIME'] ??
|
||||
json['checked_at'] ??
|
||||
json['check_time'] ??
|
||||
json['inspect_time'] ??
|
||||
null)
|
||||
?.toString();
|
||||
|
||||
return ProgressItem(
|
||||
status: status,
|
||||
location: location,
|
||||
code: code,
|
||||
checkTime: checkTime,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'status': status,
|
||||
'location': location,
|
||||
'code': code,
|
||||
'checkTime': checkTime,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,583 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:qhd_prevention/customWidget/ItemWidgetFactory.dart';
|
||||
import 'package:qhd_prevention/customWidget/bottom_picker.dart';
|
||||
import 'package:qhd_prevention/customWidget/bottom_picker_two.dart';
|
||||
import 'package:qhd_prevention/customWidget/custom_button.dart';
|
||||
import 'package:qhd_prevention/customWidget/date_picker_dialog.dart';
|
||||
import 'package:qhd_prevention/customWidget/department_person_picker.dart';
|
||||
import 'package:qhd_prevention/customWidget/department_picker.dart';
|
||||
import 'package:qhd_prevention/customWidget/department_picker_hidden_type.dart';
|
||||
import 'package:qhd_prevention/customWidget/department_picker_two.dart';
|
||||
import 'package:qhd_prevention/customWidget/toast_util.dart';
|
||||
import 'package:qhd_prevention/pages/home/tap/item_list_widget.dart';
|
||||
import 'package:qhd_prevention/pages/my_appbar.dart';
|
||||
import 'package:qhd_prevention/tools/tools.dart';
|
||||
import '../../../customWidget/photo_picker_row.dart';
|
||||
import '../../../http/ApiService.dart';
|
||||
|
||||
class NfcCheckDangerDetail extends StatefulWidget {
|
||||
const NfcCheckDangerDetail({super.key, required this.info});
|
||||
|
||||
final Map<String, dynamic> info;
|
||||
|
||||
@override
|
||||
State<NfcCheckDangerDetail> createState() => _NfcCheckDangerDetailState();
|
||||
}
|
||||
|
||||
class _NfcCheckDangerDetailState extends State<NfcCheckDangerDetail> {
|
||||
late Map<String, dynamic> pd = {};
|
||||
|
||||
//隐患级别
|
||||
late List<dynamic> _hazardLeveLlist = [];
|
||||
late List<dynamic> _hiddenTypeList = [];
|
||||
late bool _isDanger = false; //true 1 false 2
|
||||
|
||||
// 存储各单位的人员列表
|
||||
Map<String, dynamic> _personCache = {};
|
||||
|
||||
// 隐患图片
|
||||
late List<String> imgList = [];
|
||||
late List<String> _videos = [];
|
||||
|
||||
// 整改图片
|
||||
late List<String> zgImgList = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
// TODO: implement initState
|
||||
super.initState();
|
||||
pd = widget.info;
|
||||
if (pd.isNotEmpty) {
|
||||
imgList = pd['imgList'] ?? [];
|
||||
_videos = pd['videoList'] ?? [];
|
||||
zgImgList = pd['gzImageList'] ?? [];
|
||||
|
||||
}
|
||||
_getHazardLevel();
|
||||
}
|
||||
|
||||
Future<void> _getHazardLevel() async {
|
||||
final resultLevel = await ApiService.getHiddenLevelsListTwo();
|
||||
List<dynamic> levelList = resultLevel['list'] as List;
|
||||
String parentId = '';
|
||||
for (var item in levelList) {
|
||||
if ((item['BIANMA'] as String).contains(
|
||||
SessionService.instance.loginUser?["PROVINCE"] ?? '',
|
||||
)) {
|
||||
parentId = item['DICTIONARIES_ID'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
final result = await ApiService.getHiddenTypeList(parentId);
|
||||
final nodes = result['zTreeNodes'] as String;
|
||||
SessionService.instance.departmentHiddenTypeJsonStr = nodes;
|
||||
setState(() {
|
||||
_hiddenTypeList = json.decode(nodes) as List;
|
||||
});
|
||||
|
||||
try {
|
||||
final result = await ApiService.getHazardLevel();
|
||||
if (result['result'] == 'success') {
|
||||
final List<dynamic> newList = result['list'] ?? [];
|
||||
setState(() {
|
||||
_hazardLeveLlist = newList;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error fetching data: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _pickHazardType() async {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
barrierColor: Colors.black54,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder:
|
||||
(_) => DepartmentPickerHiddenType(
|
||||
onSelected: (result) {
|
||||
try {
|
||||
final Map m = Map.from(result);
|
||||
final ids = List<String>.from(m['id'] ?? []);
|
||||
final names = List<String>.from(m['name'] ?? []);
|
||||
setState(() {
|
||||
pd['HIDDENTYPE'] = ids;
|
||||
pd['HIDDENTYPE_NAME'] = names.join('/');
|
||||
});
|
||||
FocusHelper.clearFocus(context);
|
||||
} catch (_) {}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> chooseDangerLevel() async {
|
||||
FocusScope.of(context).unfocus();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
Map _hazardLeve = {};
|
||||
String choice = await BottomPickerTwo.show<String>(
|
||||
context,
|
||||
items: _hazardLeveLlist,
|
||||
itemBuilder: (item) => Text(item["NAME"], textAlign: TextAlign.center),
|
||||
initialIndex: 0,
|
||||
);
|
||||
if (choice != null) {
|
||||
for (int i = 0; i < _hazardLeveLlist.length; i++) {
|
||||
if (choice == _hazardLeveLlist[i]["NAME"]) {
|
||||
_hazardLeve = _hazardLeveLlist[i];
|
||||
}
|
||||
}
|
||||
setState(() {
|
||||
pd['HIDDENLEVELNAME'] = _hazardLeve["NAME"];
|
||||
pd['HIDDENLEVEL'] = _hazardLeve["BIANMA"];
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: MyAppbar(title: "隐患登记"),
|
||||
body: Column(
|
||||
children: [
|
||||
// 详情滚动区域
|
||||
_pageDetail(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 弹出单位选择
|
||||
void chooseUnitHandle(String typeStr) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
barrierColor: Colors.black54,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder:
|
||||
(_) => DepartmentPicker(
|
||||
onSelected: (id, name) async {
|
||||
setState(() {
|
||||
pd['RECTIFICATIONDEPT'] = id;
|
||||
pd['RECTIFICATIONDEPTNAME'] = name;
|
||||
});
|
||||
FocusHelper.clearFocus(context);
|
||||
_getPersonListForUnitId(typeStr);
|
||||
},
|
||||
),
|
||||
).then((_) {});
|
||||
}
|
||||
|
||||
Future<void> _getPersonListForUnitId(String typeStr) async {
|
||||
String unitId = pd['RECTIFICATIONDEPT'] ?? '';
|
||||
// 拉取该单位的人员列表并缓存
|
||||
final result = await ApiService.getListTreePersonList(unitId);
|
||||
setState(() {
|
||||
_personCache[typeStr] = List<Map<String, dynamic>>.from(
|
||||
result['userList'] as List,
|
||||
);
|
||||
});
|
||||
FocusHelper.clearFocus(context);
|
||||
}
|
||||
|
||||
/// 弹出人员选择,需先选择单位
|
||||
void choosePersonHandle(String typeStr) async {
|
||||
final personList = _personCache[typeStr];
|
||||
if (!FormUtils.hasValue(_personCache, typeStr)) {
|
||||
ToastUtil.showNormal(context, '请先选择单位');
|
||||
return;
|
||||
}
|
||||
|
||||
DepartmentPersonPicker.show(
|
||||
context,
|
||||
personsData: personList!,
|
||||
onSelectedWithIndex: (userId, name, index) {
|
||||
setState(() {
|
||||
pd['RECTIFICATIONOR'] = userId;
|
||||
pd['RECTIFICATIONORNAME'] = name;
|
||||
});
|
||||
FocusHelper.clearFocus(context);
|
||||
},
|
||||
).then((_) {});
|
||||
}
|
||||
|
||||
Widget _buildSectionContainer({required Widget child}) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: 10),
|
||||
color: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 10),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _pageDetail() {
|
||||
return Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.only(bottom: 20),
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||
color: Colors.white,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
ItemListWidget.itemContainer(
|
||||
horizontal: 5,
|
||||
Column(
|
||||
children: [
|
||||
RepairedPhotoSection(
|
||||
title: "隐患照片",
|
||||
maxCount: 4,
|
||||
initialMediaPaths: imgList,
|
||||
mediaType: MediaType.image,
|
||||
isShowAI: true,
|
||||
onMediaAdded: (localPath) {
|
||||
imgList.add(localPath);
|
||||
},
|
||||
onMediaRemoved: (localPath) {
|
||||
imgList.remove(localPath);
|
||||
},
|
||||
onChanged: (v) {},
|
||||
onAiIdentify: () {
|
||||
// AI 识别逻辑
|
||||
if (imgList.isEmpty) {
|
||||
ToastUtil.showNormal(context, "请先上传一张图片");
|
||||
return;
|
||||
}
|
||||
if (imgList.length > 1) {
|
||||
ToastUtil.showNormal(context, "识别暂时只能上传一张图片");
|
||||
return;
|
||||
}
|
||||
_identifyImg(imgList[0]);
|
||||
},
|
||||
),
|
||||
RepairedPhotoSection(
|
||||
title: "隐患视频",
|
||||
maxCount: 1,
|
||||
mediaType: MediaType.video,
|
||||
onChanged: (v) {},
|
||||
onMediaRemoved: (localPath) {
|
||||
_videos = [localPath];
|
||||
},
|
||||
onMediaAdded: (localPath) {
|
||||
_videos = [];
|
||||
},
|
||||
onAiIdentify: () {
|
||||
// AI 视频识别逻辑
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
ItemListWidget.multiLineTitleTextField(
|
||||
label: '隐患描述',
|
||||
isEditable: true,
|
||||
isRequired: false,
|
||||
text: pd['HIDDENDESCR'] ?? '',
|
||||
hintText: '请对隐患进行详细描述(必填项)',
|
||||
onChanged: (v) {
|
||||
setState(() {
|
||||
pd['HIDDENDESCR'] = v;
|
||||
});
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
|
||||
ItemListWidget.multiLineTitleTextField(
|
||||
label: '隐患部位',
|
||||
isEditable: true,
|
||||
isRequired: false,
|
||||
// controller: TextEditingController(text: pd['HIDDENPART'] ?? ''),
|
||||
text: pd['HIDDENPART'] ?? '',
|
||||
hintText: '请对隐患部位进行详细描述(必填项)',
|
||||
onChanged: (v) {
|
||||
setState(() {
|
||||
pd['HIDDENPART'] = v;
|
||||
});
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
|
||||
ItemListWidget.selectableLineTitleTextRightButton(
|
||||
label: '隐患级别',
|
||||
isRequired: false,
|
||||
isEditable: true,
|
||||
text: pd['HIDDENLEVELNAME'] ?? '',
|
||||
onTap: chooseDangerLevel,
|
||||
),
|
||||
const Divider(),
|
||||
ItemListWidget.selectableLineTitleTextRightButton(
|
||||
label: '隐患类型',
|
||||
isEditable: true,
|
||||
isRequired: false,
|
||||
text: pd['HIDDENTYPE_NAME'] ?? '',
|
||||
onTap: _pickHazardType,
|
||||
),
|
||||
const Divider(),
|
||||
|
||||
ListItemFactory.createYesNoSection(
|
||||
title: "是否立即整改",
|
||||
horizontalPadding: 0,
|
||||
verticalPadding: 0,
|
||||
yesLabel: "是",
|
||||
noLabel: "否",
|
||||
groupValue: _isDanger,
|
||||
|
||||
onChanged: (val) {
|
||||
setState(() {
|
||||
_isDanger = val;
|
||||
});
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
if (_isDanger)
|
||||
Column(
|
||||
children: [
|
||||
ItemListWidget.multiLineTitleTextField(
|
||||
label: '整改描述',
|
||||
isEditable: true,
|
||||
isRequired: false,
|
||||
text: pd['RECTIFYDESCR'] ?? '',
|
||||
hintText: '请对隐患进行详细描述(必填项)',
|
||||
onChanged: (v) {
|
||||
setState(() {
|
||||
pd['RECTIFYDESCR'] = v;
|
||||
|
||||
});
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
RepairedPhotoSection(
|
||||
horizontalPadding: 12,
|
||||
title: "整改后图片",
|
||||
maxCount: 4,
|
||||
initialMediaPaths: zgImgList,
|
||||
mediaType: MediaType.image,
|
||||
isShowAI: false,
|
||||
|
||||
onMediaAdded: (localPath) {
|
||||
zgImgList.add(localPath);
|
||||
},
|
||||
onMediaRemoved: (localPath) {
|
||||
zgImgList.remove(localPath);
|
||||
},
|
||||
onChanged: (v) {},
|
||||
onAiIdentify: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
if (!_isDanger)
|
||||
Column(
|
||||
children: [
|
||||
ItemListWidget.selectableLineTitleTextRightButton(
|
||||
label: '整改责任部门',
|
||||
isEditable: true,
|
||||
isRequired: false,
|
||||
text: pd['RECTIFICATIONDEPTNAME'] ?? '',
|
||||
onTap: () {
|
||||
chooseUnitHandle('key');
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
ItemListWidget.selectableLineTitleTextRightButton(
|
||||
label: '整改责任人',
|
||||
isEditable: true,
|
||||
isRequired: false,
|
||||
text: pd['RECTIFICATIONORNAME'] ?? '',
|
||||
onTap: () {
|
||||
choosePersonHandle('key');
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
ItemListWidget.selectableLineTitleTextRightButton(
|
||||
label: '整改期限',
|
||||
isEditable: true,
|
||||
isRequired: false,
|
||||
text: pd['RECTIFICATIONDEADLINE'] ?? '',
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder:
|
||||
(_) => HDatePickerDialog(
|
||||
initialDate: DateTime.now(),
|
||||
onCancel: () => Navigator.of(context).pop(),
|
||||
onConfirm: (selected) {
|
||||
Navigator.of(context).pop();
|
||||
setState(() {
|
||||
pd['RECTIFICATIONDEADLINE'] = DateFormat(
|
||||
'yyyy-MM-dd',
|
||||
).format(selected);
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
],
|
||||
),
|
||||
|
||||
SizedBox(height: 30),
|
||||
CustomButton(
|
||||
onPressed: () {
|
||||
_riskListCheckAppAdd();
|
||||
},
|
||||
text: "确定",
|
||||
backgroundColor: Colors.blue,
|
||||
),
|
||||
SizedBox(height: 30),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _riskListCheckAppAdd() async {
|
||||
if (imgList.isEmpty) {
|
||||
ToastUtil.showNormal(context, '请上传隐患图片');
|
||||
return;
|
||||
}
|
||||
final textRules = <Map<String, dynamic>>[
|
||||
{'value': pd['HIDDENDESCR'] ?? '', 'message': '请填隐患描述'},
|
||||
{'value': pd['HIDDENPART'] ?? '', 'message': '请填隐患部位'},
|
||||
{'value': pd['HIDDENLEVEL'] ?? '', 'message': '请选择隐患级别'},
|
||||
{'value': pd['HIDDENTYPE_NAME'] ?? '', 'message': '请选择隐患类型'},
|
||||
];
|
||||
for (Map rule in textRules) {
|
||||
String value = rule['value'];
|
||||
if (value.isEmpty) {
|
||||
ToastUtil.showNormal(context, rule['message']);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (_isDanger) {
|
||||
if (!FormUtils.hasValue(pd, 'RECTIFYDESCR')) {
|
||||
ToastUtil.showNormal(context, '请填整改描述');
|
||||
return;
|
||||
}
|
||||
if (zgImgList.isEmpty) {
|
||||
ToastUtil.showNormal(context, '请上传整改后图片');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (!FormUtils.hasValue(pd, 'RECTIFICATIONDEPT')) {
|
||||
ToastUtil.showNormal(context, '请选择整改部门');
|
||||
return;
|
||||
}
|
||||
if (!FormUtils.hasValue(pd, 'RECTIFICATIONOR')) {
|
||||
ToastUtil.showNormal(context, '请选择整改人');
|
||||
return;
|
||||
}
|
||||
if (!FormUtils.hasValue(pd, 'RECTIFICATIONDEADLINE')) {
|
||||
ToastUtil.showNormal(context, '请选择整改期限');
|
||||
return;
|
||||
}
|
||||
}
|
||||
List HIDDENTYPE = pd['HIDDENTYPE'] ?? [];
|
||||
pd['RECTIFICATIONTYPE'] = _isDanger ? '1' : '2';
|
||||
pd['CREATOR'] = SessionService.instance.loginUserId;
|
||||
pd['SOURCE'] = '6';
|
||||
pd['HIDDENTYPE1'] = HIDDENTYPE.length > 0 ? HIDDENTYPE[0] : "";
|
||||
pd['HIDDENTYPE2'] = HIDDENTYPE.length > 1 ? HIDDENTYPE[1] : "";
|
||||
pd['HIDDENTYPE3'] = HIDDENTYPE.length > 2 ? HIDDENTYPE[2] : "";
|
||||
|
||||
pd['imgList'] = imgList;
|
||||
pd['videoList'] = _videos;
|
||||
pd['gzImageList'] = zgImgList;
|
||||
|
||||
Navigator.pop(context, pd);
|
||||
}
|
||||
|
||||
Future<void> _identifyImg(String imagePath) async {
|
||||
try {
|
||||
LoadingDialogHelper.show();
|
||||
final raw = await ApiService.identifyImg(imagePath);
|
||||
if (raw['result'] == 'success') {
|
||||
final dynamic parsedRes = raw;
|
||||
final aiHiddens = parsedRes['aiHiddens'];
|
||||
|
||||
String hiddenDescr = '';
|
||||
String rectificationSuggestions = '';
|
||||
String legalBasis = '';
|
||||
|
||||
if (aiHiddens is List) {
|
||||
for (var item in aiHiddens) {
|
||||
dynamic obj = item;
|
||||
if (item is String) {
|
||||
try {
|
||||
obj = json.decode(item);
|
||||
} catch (e) {
|
||||
// 解析失败跳过
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (obj is Map) {
|
||||
hiddenDescr += (obj['hiddenDescr']?.toString() ?? '') + ';';
|
||||
rectificationSuggestions +=
|
||||
(obj['rectificationSuggestions']?.toString() ?? '') + ';';
|
||||
legalBasis += (obj['legalBasis']?.toString() ?? '') + ';';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 将结果赋回(如果 pd 是 Map)
|
||||
setState(() {
|
||||
pd['HIDDENDESCR'] = hiddenDescr;
|
||||
pd['LEGALBASIS'] = legalBasis;
|
||||
pd['RECTIFYDESCR'] = rectificationSuggestions;
|
||||
});
|
||||
LoadingDialogHelper.hide();
|
||||
} else {
|
||||
ToastUtil.showNormal(context, "识别失败");
|
||||
LoadingDialogHelper.hide();
|
||||
// _showMessage('反馈提交失败');
|
||||
// return "";
|
||||
}
|
||||
} catch (e) {
|
||||
// 出错时可以 Toast 或者在页面上显示错误状态
|
||||
print('加载首页数据失败:$e');
|
||||
// return "";
|
||||
LoadingDialogHelper.hide();
|
||||
}
|
||||
}
|
||||
|
||||
Future<Position> _determinePosition() async {
|
||||
bool serviceEnabled;
|
||||
LocationPermission permission;
|
||||
|
||||
// 检查定位服务是否启用
|
||||
serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||
if (!serviceEnabled) {
|
||||
return Future.error('Location services are disabled.');
|
||||
}
|
||||
|
||||
// 获取权限
|
||||
permission = await Geolocator.checkPermission();
|
||||
if (permission == LocationPermission.denied) {
|
||||
permission = await Geolocator.requestPermission();
|
||||
if (permission == LocationPermission.denied) {
|
||||
return Future.error('Location permissions are denied');
|
||||
}
|
||||
}
|
||||
|
||||
if (permission == LocationPermission.deniedForever) {
|
||||
return Future.error(
|
||||
'Location permissions are permanently denied, we cannot request permissions.',
|
||||
);
|
||||
}
|
||||
|
||||
// 获取当前位置
|
||||
return await Geolocator.getCurrentPosition(
|
||||
desiredAccuracy: LocationAccuracy.high,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,241 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:qhd_prevention/customWidget/photo_picker_row.dart';
|
||||
import 'package:qhd_prevention/customWidget/toast_util.dart';
|
||||
import 'package:qhd_prevention/http/ApiService.dart';
|
||||
import 'package:qhd_prevention/pages/home/NFC/home_nfc_check_danger_page.dart';
|
||||
import 'package:qhd_prevention/pages/my_appbar.dart';
|
||||
import 'package:qhd_prevention/tools/tools.dart';
|
||||
|
||||
// feedback_type.dart
|
||||
enum NfcFeedbackType {
|
||||
readError('读取失败', 0),
|
||||
nfcBld('标签损坏', 1),
|
||||
nfcLose('标签丢失', 2);
|
||||
|
||||
final String typeName;
|
||||
final int type;
|
||||
|
||||
const NfcFeedbackType(this.typeName, this.type);
|
||||
}
|
||||
|
||||
class NfcQuestionFecebook extends StatefulWidget {
|
||||
const NfcQuestionFecebook({super.key, required this.info,required this.taskInfo});
|
||||
|
||||
final Map taskInfo;
|
||||
final Map info;
|
||||
|
||||
@override
|
||||
State<NfcQuestionFecebook> createState() => _NfcQuestionFecebookState();
|
||||
}
|
||||
|
||||
class _NfcQuestionFecebookState extends State<NfcQuestionFecebook> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final TextEditingController _descriptionController = TextEditingController();
|
||||
final FocusNode _descriptionFocus = FocusNode(); // <- 新增
|
||||
|
||||
// 反馈类型
|
||||
NfcFeedbackType? _selectedType = NfcFeedbackType.readError;
|
||||
|
||||
// 上传的图片
|
||||
List<String> _images = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
// TODO: implement initState
|
||||
super.initState();
|
||||
_getFacebookDetail();
|
||||
}
|
||||
|
||||
Future<void> _getFacebookDetail() async {
|
||||
final result = await ApiService.getNfcFeedBackDetail({});
|
||||
try{
|
||||
if (result['result'] == 'success') {
|
||||
|
||||
}
|
||||
}catch(e) {}
|
||||
}
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: MyAppbar(title: "NFC异常上报"),
|
||||
body: Container(
|
||||
color: Colors.white,
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 问题描述
|
||||
const Text(
|
||||
'详细问题',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: _descriptionController,
|
||||
focusNode: _descriptionFocus,
|
||||
autofocus: false,
|
||||
maxLines: 5,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '请补充详细问题...',
|
||||
border: OutlineInputBorder(),
|
||||
contentPadding: EdgeInsets.all(10),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return '请补充详细问题';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 反馈类型
|
||||
const Text(
|
||||
'反馈类型',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 16,
|
||||
children:
|
||||
NfcFeedbackType.values.map((type) {
|
||||
return ChoiceChip(
|
||||
label: Text(type.typeName),
|
||||
selected: _selectedType == type,
|
||||
onSelected: (selected) {
|
||||
setState(() {
|
||||
if (selected) {
|
||||
_selectedType = type;
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
|
||||
// 图片上传
|
||||
const SizedBox(height: 16),
|
||||
|
||||
RepairedPhotoSection(
|
||||
horizontalPadding: 0,
|
||||
title: "请提供相关问题照片",
|
||||
maxCount: 4,
|
||||
mediaType: MediaType.image,
|
||||
isCamera: true,
|
||||
onChanged: (files) {
|
||||
// 上传 files 到服务器
|
||||
_images.clear();
|
||||
for (int i = 0; i < files.length; i++) {
|
||||
_images.add(files[i].path);
|
||||
}
|
||||
},
|
||||
onAiIdentify: () {},
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
|
||||
// 提交按钮
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: _submitFeedback,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.blue,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'下一步',
|
||||
style: TextStyle(fontSize: 18, color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 提交反馈
|
||||
Future<void> _submitFeedback() async {
|
||||
final text = _descriptionController.text.trim();
|
||||
|
||||
if (text.isEmpty) {
|
||||
_showMessage('请填写问题');
|
||||
return;
|
||||
}
|
||||
|
||||
if (_images.isEmpty) {
|
||||
_showMessage('请上传图片');
|
||||
return;
|
||||
}
|
||||
Map data = {
|
||||
...widget.info,
|
||||
...widget.taskInfo,
|
||||
'EXCEPTION_TYPE': _selectedType?.type,
|
||||
'MANUAL_CONFIRMATION': '1',
|
||||
// 'PHOTO_URL': imagePaths,
|
||||
'DESCRIPTION': text,
|
||||
};
|
||||
|
||||
pushPage(HomeNfcCheckDangerPage(info: data, facebookImages: _images, isNfcError: true,), context);
|
||||
|
||||
// String imagePaths = "";
|
||||
// for (int i = 0; i < _images.length; i++) {
|
||||
// String imagePath = await _reloadFeedBack(_images[i]);
|
||||
//
|
||||
// if (0 == i) {
|
||||
// imagePaths = imagePath;
|
||||
// } else {
|
||||
// imagePaths = "$imagePaths,$imagePath";
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// _setFeedBack(text, imagePaths);
|
||||
}
|
||||
@override
|
||||
void dispose() {
|
||||
_descriptionController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
Future<void> _setFeedBack(String text, String imagePaths) async {
|
||||
try {
|
||||
Map data = {
|
||||
"PATROL_TASK_ID":widget.taskInfo['PATROL_TASK_ID']?? '',
|
||||
"PIPELINE_AREA_ID" :widget.taskInfo['PIPELINE_AREA_ID']?? '',
|
||||
"EQUIPMENT_PIPELINE_ID" :widget.info['EQUIPMENT_PIPELINE_ID']?? '',
|
||||
"PATROL_RECORD_ID" :widget.info['EQUIPMENT_PIPELINE_ID'] ?? '',
|
||||
"PATROL_RECORD_DETAIL_ID" :widget.info['PATROL_RECORD_DETAIL_ID']?? '',
|
||||
|
||||
"NFC_CODE" :widget.info['PIPELINE_AREA_ID'],
|
||||
'EXCEPTION_TYPE': _selectedType?.type,
|
||||
'PHOTO_URL': imagePaths,
|
||||
'DESCRIPTION': text,
|
||||
};
|
||||
|
||||
final raw = await ApiService.nfcFeedBack(data);
|
||||
|
||||
if (raw['result'] == 'success') {
|
||||
_showMessage('提交成功');
|
||||
} else {
|
||||
_showMessage('提交失败');
|
||||
}
|
||||
} catch (e) {
|
||||
// 出错时可以 Toast 或者在页面上显示错误状态
|
||||
print('加载首页数据失败:$e');
|
||||
}
|
||||
}
|
||||
|
||||
void _showMessage(String msg) {
|
||||
ToastUtil.showNormal(context, msg);
|
||||
}
|
||||
}
|
||||
|
|
@ -120,8 +120,9 @@ class ItemListWidget {
|
|||
Expanded(
|
||||
child:
|
||||
isEditable
|
||||
? TextField(
|
||||
? TextFormField(
|
||||
autofocus: false,
|
||||
initialValue: text,
|
||||
controller: controller,
|
||||
keyboardType: TextInputType.multiline,
|
||||
maxLines: null,
|
||||
|
|
@ -131,9 +132,10 @@ class ItemListWidget {
|
|||
textAlignVertical: TextAlignVertical.top,
|
||||
style: TextStyle(fontSize: fontSize),
|
||||
decoration: InputDecoration(
|
||||
|
||||
hintText: hintText,
|
||||
// 去掉 TextField 默认内边距
|
||||
//contentPadding: EdgeInsets.zero,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
border: InputBorder.none,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import 'package:nfc_manager_ndef/nfc_manager_ndef.dart';
|
|||
/// 注:iOS 必须在真机测试,并在 Xcode 中打开 NFC 权限;Android 需设备支持 NFC。
|
||||
class NfcService {
|
||||
NfcService._internal();
|
||||
|
||||
static final NfcService instance = NfcService._internal();
|
||||
|
||||
/// 是否正在进行 NFC 会话(扫描或写入)
|
||||
|
|
@ -31,6 +32,7 @@ class NfcService {
|
|||
|
||||
/// 日志广播流(方便 UI 订阅)
|
||||
final StreamController<String> _logController = StreamController.broadcast();
|
||||
|
||||
Stream<String> get logs => _logController.stream;
|
||||
|
||||
/// 检查设备是否支持 NFC
|
||||
|
|
@ -50,7 +52,10 @@ class NfcService {
|
|||
|
||||
/// bytes -> "AA:BB:CC" 形式的大写十六进制字符串
|
||||
String _bytesToHex(Uint8List bytes) {
|
||||
return bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(':').toUpperCase();
|
||||
return bytes
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||
.join(':')
|
||||
.toUpperCase();
|
||||
}
|
||||
|
||||
/// 递归在 Map/List 中查找第一个可用的字节数组 (List<int> / Uint8List)
|
||||
|
|
@ -101,12 +106,14 @@ class NfcService {
|
|||
if (Platform.isAndroid) {
|
||||
try {
|
||||
final nfcA = NfcAAndroid.from(tag);
|
||||
if (nfcA != null && nfcA.tag.id != null) return _bytesToHex(Uint8List.fromList(nfcA.tag.id));
|
||||
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));
|
||||
if (mifare != null && mifare.identifier != null)
|
||||
return _bytesToHex(Uint8List.fromList(mifare.identifier));
|
||||
} catch (_) {}
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
@ -131,7 +138,8 @@ class NfcService {
|
|||
String _formatMessageToString(dynamic msg) {
|
||||
if (msg == null) return '<empty>';
|
||||
try {
|
||||
final records = (msg is Map && msg['records'] != null)
|
||||
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>';
|
||||
|
|
@ -144,12 +152,16 @@ class NfcService {
|
|||
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);
|
||||
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 (pr is Uint8List)
|
||||
p = pr;
|
||||
else if (pr is List<int>)
|
||||
p = Uint8List.fromList(pr);
|
||||
}
|
||||
if (p != null) {
|
||||
final txt = parseTextFromPayload(p);
|
||||
|
|
@ -171,7 +183,8 @@ class NfcService {
|
|||
/// onError(error) - 出错时回调
|
||||
/// timeout - 超时时间(可选)
|
||||
Future<void> startScanOnceWithCallback({
|
||||
required void Function(String uid, String parsedText, dynamic rawMessage) onResult,
|
||||
required void Function(String uid, String parsedText, dynamic rawMessage)
|
||||
onResult,
|
||||
void Function(Object error)? onError,
|
||||
Duration? timeout,
|
||||
}) async {
|
||||
|
|
@ -215,8 +228,12 @@ class NfcService {
|
|||
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));
|
||||
if (payload is Uint8List)
|
||||
parsedText = parseTextFromPayload(payload);
|
||||
else if (payload is List<int>)
|
||||
parsedText = parseTextFromPayload(
|
||||
Uint8List.fromList(payload),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -226,14 +243,24 @@ class NfcService {
|
|||
// 回退:尝试从 tag map 中嗅探 cachedMessage / records
|
||||
try {
|
||||
if (tag is Map) {
|
||||
rawMsg = tag['cachedMessage'] ?? tag['ndef']?['cachedMessage'] ?? tag['message'];
|
||||
rawMsg =
|
||||
tag['cachedMessage'] ??
|
||||
tag['ndef']?['cachedMessage'] ??
|
||||
tag['message'];
|
||||
if (rawMsg != null) {
|
||||
final recs = (rawMsg['records'] ?? (rawMsg as dynamic).records) as dynamic;
|
||||
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));
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -242,7 +269,9 @@ class NfcService {
|
|||
|
||||
// 回调结果
|
||||
onResult(uid, parsedText, rawMsg);
|
||||
_logController.add('UID: $uid\nmessage: ${_formatMessageToString(rawMsg)}');
|
||||
_logController.add(
|
||||
'UID: $uid\nmessage: ${_formatMessageToString(rawMsg)}',
|
||||
);
|
||||
|
||||
await stopSession();
|
||||
} catch (e) {
|
||||
|
|
@ -255,7 +284,6 @@ class NfcService {
|
|||
},
|
||||
// 尽量多协议尝试以提升兼容性
|
||||
pollingOptions: {NfcPollingOption.iso14443},
|
||||
|
||||
);
|
||||
} catch (e) {
|
||||
scanning.value = false;
|
||||
|
|
@ -265,7 +293,9 @@ class NfcService {
|
|||
}
|
||||
|
||||
/// Future 式读取:解析第一条文本记录并返回字符串(格式:'UID\n文本')
|
||||
Future<String> readOnceText({Duration timeout = const Duration(seconds: 10)}) async {
|
||||
Future<String> readOnceText({
|
||||
Duration timeout = const Duration(seconds: 10),
|
||||
}) async {
|
||||
final completer = Completer<String>();
|
||||
await startScanOnceWithCallback(
|
||||
onResult: (uid, parsedText, rawMsg) {
|
||||
|
|
@ -301,8 +331,14 @@ class NfcService {
|
|||
/// - onComplete: 回调 (ok, err)
|
||||
///
|
||||
/// 返回 true 表示写入成功(同时 onComplete 也会被触发)
|
||||
Future<bool> writeText(String text, {Duration? timeout, void Function(bool ok, Object? err)? onComplete}) async {
|
||||
Future<bool> writeText(
|
||||
String text, {
|
||||
Duration? timeout,
|
||||
void Function(bool ok, Object? err)? onComplete,
|
||||
}) async {
|
||||
debugPrint('writeText called - scanning=${scanning.value}');
|
||||
final available = await isAvailable();
|
||||
debugPrint('writeText: isAvailable=$available');
|
||||
if (!available) {
|
||||
onComplete?.call(false, 'NFC not available');
|
||||
return false;
|
||||
|
|
@ -312,6 +348,12 @@ class NfcService {
|
|||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await NfcManager.instance.stopSession();
|
||||
} catch (e) {
|
||||
debugPrint('stopSession ignore: $e');
|
||||
}
|
||||
|
||||
scanning.value = true;
|
||||
Timer? timer;
|
||||
if (timeout != null) {
|
||||
|
|
@ -324,15 +366,40 @@ class NfcService {
|
|||
|
||||
bool success = false;
|
||||
try {
|
||||
await NfcManager.instance.startSession(onDiscovered: (dynamic tag) async {
|
||||
// 修改了 pollingOptions 配置
|
||||
final polling =
|
||||
Platform.isIOS
|
||||
? {NfcPollingOption.iso14443} // iOS 使用更具体的配置
|
||||
: {
|
||||
NfcPollingOption.iso14443,
|
||||
NfcPollingOption.iso15693,
|
||||
NfcPollingOption.iso18092,
|
||||
};
|
||||
|
||||
await NfcManager.instance.startSession(
|
||||
pollingOptions: polling,
|
||||
onDiscovered: (dynamic tag) async {
|
||||
timer?.cancel();
|
||||
try {
|
||||
final ndef = Ndef.from(tag);
|
||||
if (ndef == null) {
|
||||
onComplete?.call(false, 'Tag 不支持 NDEF');
|
||||
onComplete?.call(false, 'Tag not NDEF');
|
||||
await stopSession();
|
||||
scanning.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否可写
|
||||
if (Platform.isIOS) {
|
||||
final miFare = MiFareIos.from(tag);
|
||||
if (miFare == null) {
|
||||
onComplete?.call(false, 'Unsupported tag type on iOS');
|
||||
await stopSession();
|
||||
scanning.value = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final payload = _buildTextPayload(text, lang: 'en');
|
||||
final record = NdefRecord(
|
||||
typeNameFormat: TypeNameFormat.wellKnown,
|
||||
|
|
@ -343,7 +410,6 @@ class NfcService {
|
|||
final message = NdefMessage(records: [record]);
|
||||
|
||||
await ndef.write(message: message);
|
||||
|
||||
// 取出 UID (优先取 nfca.identifier,如果没有就取顶层 id)
|
||||
String? uid;
|
||||
if (Platform.isAndroid) {
|
||||
|
|
@ -361,26 +427,24 @@ class NfcService {
|
|||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
success = true;
|
||||
onComplete?.call(true, uid); // ✅ 把 UID 返回出去
|
||||
_logController.add('NFC write success, UID=$uid');
|
||||
} catch (e) {
|
||||
debugPrint('NFC write error: $e');
|
||||
} catch (e, st) {
|
||||
debugPrint('iOS NFC Write Error: $e');
|
||||
debugPrint('Stack trace: $st');
|
||||
onComplete?.call(false, e);
|
||||
} finally {
|
||||
await stopSession();
|
||||
timer?.cancel();
|
||||
scanning.value = false;
|
||||
}
|
||||
}, pollingOptions: {NfcPollingOption.iso14443});
|
||||
|
||||
} catch (e) {
|
||||
scanning.value = false;
|
||||
},
|
||||
);
|
||||
} catch (e, st) {
|
||||
debugPrint('startSession exception: $e\n$st');
|
||||
timer?.cancel();
|
||||
scanning.value = false;
|
||||
onComplete?.call(false, e);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,19 @@
|
|||
import 'dart:math';
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
int getRandomWithNum(int min, int max) {
|
||||
if (max < min) {
|
||||
// 保护性处理:交换或抛错,这里交换
|
||||
final tmp = min;
|
||||
min = max;
|
||||
max = tmp;
|
||||
}
|
||||
final random = Random();
|
||||
return random.nextInt(max) + min; // 生成随机数
|
||||
return random.nextInt(max - min + 1) + min; // 生成 [min, max] 的随机数
|
||||
}
|
||||
|
||||
double screenWidth(BuildContext context) {
|
||||
|
|
@ -332,25 +339,46 @@ void presentPage(BuildContext context, Widget page) {
|
|||
MaterialPageRoute(fullscreenDialog: true, builder: (_) => page),
|
||||
);
|
||||
}
|
||||
|
||||
class LoadingDialogHelper {
|
||||
// 显示加载框
|
||||
static void show({String? message}) {
|
||||
static Timer? _timer;
|
||||
|
||||
/// 显示加载框(带超时,默认 10 秒)
|
||||
static void show({String? message, Duration timeout = const Duration(seconds: 10)}) {
|
||||
// 先清理上一个计时器,避免重复
|
||||
_timer?.cancel();
|
||||
|
||||
if (message != null) {
|
||||
EasyLoading.show(status: message);
|
||||
} else {
|
||||
EasyLoading.show();
|
||||
}
|
||||
}
|
||||
|
||||
// 隐藏加载框
|
||||
static void hide() {
|
||||
if (EasyLoading.isShow) {
|
||||
// 设置超时自动隐藏
|
||||
_timer = Timer(timeout, () {
|
||||
// 保护性调用 dismiss(避免访问不存在的 isShow)
|
||||
try {
|
||||
EasyLoading.dismiss();
|
||||
} catch (e) {
|
||||
debugPrint('EasyLoading.dismiss error: $e');
|
||||
}
|
||||
}
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
});
|
||||
}
|
||||
|
||||
/// 隐藏加载框(手动触发)
|
||||
static void hide() {
|
||||
// 清理计时器
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
|
||||
try {
|
||||
EasyLoading.dismiss();
|
||||
} catch (e) {
|
||||
debugPrint('EasyLoading.dismiss error: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// 将秒数转换为 “HH:MM:SS” 格式
|
||||
|
|
|
|||
Loading…
Reference in New Issue