nfc巡检暂存
parent
566ab85147
commit
b5992b94ab
|
|
@ -491,9 +491,11 @@
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
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)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = 8AKCJ9LW7D;
|
DEVELOPMENT_TEAM = "";
|
||||||
|
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 8AKCJ9LW7D;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
|
|
@ -504,6 +506,7 @@
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.zhuoyun.qhdprevention.qhdPrevention;
|
PRODUCT_BUNDLE_IDENTIFIER = com.zhuoyun.qhdprevention.qhdPrevention;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "flutter-weihua";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
VERSIONING_SYSTEM = "apple-generic";
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
|
@ -682,9 +685,11 @@
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
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)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = 8AKCJ9LW7D;
|
DEVELOPMENT_TEAM = "";
|
||||||
|
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 8AKCJ9LW7D;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
|
|
@ -695,6 +700,7 @@
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.zhuoyun.qhdprevention.qhdPrevention;
|
PRODUCT_BUNDLE_IDENTIFIER = com.zhuoyun.qhdprevention.qhdPrevention;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "flutter-weihua";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
|
|
@ -710,9 +716,11 @@
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
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)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = 8AKCJ9LW7D;
|
DEVELOPMENT_TEAM = "";
|
||||||
|
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 8AKCJ9LW7D;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
|
|
@ -723,6 +731,7 @@
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.zhuoyun.qhdprevention.qhdPrevention;
|
PRODUCT_BUNDLE_IDENTIFIER = com.zhuoyun.qhdprevention.qhdPrevention;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "flutter-weihua";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
VERSIONING_SYSTEM = "apple-generic";
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
|
|
||||||
|
|
@ -1,77 +1,77 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Flutter
|
import Flutter
|
||||||
|
|
||||||
@main
|
@main
|
||||||
@objc class AppDelegate: FlutterAppDelegate {
|
@objc class AppDelegate: FlutterAppDelegate {
|
||||||
// 动态方向掩码(默认竖屏)
|
// 动态方向掩码(默认竖屏)
|
||||||
static var orientationMask: UIInterfaceOrientationMask = .portrait
|
static var orientationMask: UIInterfaceOrientationMask = .portrait
|
||||||
|
|
||||||
override func application(
|
override func application(
|
||||||
_ application: UIApplication,
|
_ application: UIApplication,
|
||||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
|
|
||||||
let controller = window?.rootViewController as! FlutterViewController
|
let controller = window?.rootViewController as! FlutterViewController
|
||||||
let channel = FlutterMethodChannel(name: "app.orientation",
|
let channel = FlutterMethodChannel(name: "app.orientation",
|
||||||
binaryMessenger: controller.binaryMessenger)
|
binaryMessenger: controller.binaryMessenger)
|
||||||
|
|
||||||
channel.setMethodCallHandler { [weak self] call, result in
|
channel.setMethodCallHandler { [weak self] call, result in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
||||||
if call.method == "setOrientation" {
|
if call.method == "setOrientation" {
|
||||||
guard let arg = call.arguments as? String else {
|
guard let arg = call.arguments as? String else {
|
||||||
result(FlutterError(code: "BAD_ARGS", message: "need 'landscape' | 'portrait'", details: nil))
|
result(FlutterError(code: "BAD_ARGS", message: "need 'landscape' | 'portrait'", details: nil))
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 先更新允许的方向掩码
|
|
||||||
if arg == "landscape" {
|
|
||||||
AppDelegate.orientationMask = .landscape
|
|
||||||
} else if arg == "portrait" {
|
|
||||||
AppDelegate.orientationMask = .portrait
|
|
||||||
} else {
|
|
||||||
result(FlutterError(code: "BAD_ARGS", message: "unknown arg", details: nil))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// 再请求实际旋转
|
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
// 通知顶层 VC:其 supportedInterfaceOrientations 需要刷新
|
|
||||||
self.window?.rootViewController?.setNeedsUpdateOfSupportedInterfaceOrientations()
|
|
||||||
|
|
||||||
if let scene = self.window?.windowScene {
|
|
||||||
let orientations: UIInterfaceOrientationMask =
|
|
||||||
(arg == "landscape") ? .landscape : .portrait
|
|
||||||
do {
|
|
||||||
try scene.requestGeometryUpdate(.iOS(interfaceOrientations: orientations))
|
|
||||||
} catch {
|
|
||||||
result(FlutterError(code: "GEOMETRY_UPDATE_FAILED",
|
|
||||||
message: error.localizedDescription, details: nil))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 先更新允许的方向掩码
|
||||||
|
if arg == "landscape" {
|
||||||
|
AppDelegate.orientationMask = .landscape
|
||||||
|
} else if arg == "portrait" {
|
||||||
|
AppDelegate.orientationMask = .portrait
|
||||||
|
} else {
|
||||||
|
result(FlutterError(code: "BAD_ARGS", message: "unknown arg", details: nil))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 再请求实际旋转
|
||||||
|
if #available(iOS 16.0, *) {
|
||||||
|
// 通知顶层 VC:其 supportedInterfaceOrientations 需要刷新
|
||||||
|
self.window?.rootViewController?.setNeedsUpdateOfSupportedInterfaceOrientations()
|
||||||
|
|
||||||
|
if let scene = self.window?.windowScene {
|
||||||
|
let orientations: UIInterfaceOrientationMask =
|
||||||
|
(arg == "landscape") ? .landscape : .portrait
|
||||||
|
do {
|
||||||
|
try scene.requestGeometryUpdate(.iOS(interfaceOrientations: orientations))
|
||||||
|
} catch {
|
||||||
|
result(FlutterError(code: "GEOMETRY_UPDATE_FAILED",
|
||||||
|
message: error.localizedDescription, details: nil))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let target: UIInterfaceOrientation =
|
||||||
|
(arg == "landscape") ? .landscapeLeft : .portrait
|
||||||
|
UIDevice.current.setValue(target.rawValue, forKey: "orientation")
|
||||||
|
UIViewController.attemptRotationToDeviceOrientation()
|
||||||
|
}
|
||||||
|
|
||||||
|
result(true)
|
||||||
|
} else {
|
||||||
|
result(FlutterMethodNotImplemented)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
let target: UIInterfaceOrientation =
|
|
||||||
(arg == "landscape") ? .landscapeLeft : .portrait
|
|
||||||
UIDevice.current.setValue(target.rawValue, forKey: "orientation")
|
|
||||||
UIViewController.attemptRotationToDeviceOrientation()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
result(true)
|
GeneratedPluginRegistrant.register(with: self)
|
||||||
} else {
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||||
result(FlutterMethodNotImplemented)
|
}
|
||||||
|
|
||||||
|
// 关键:把当前的掩码提供给系统
|
||||||
|
override func application(_ application: UIApplication,
|
||||||
|
supportedInterfaceOrientationsFor window: UIWindow?)
|
||||||
|
-> UIInterfaceOrientationMask {
|
||||||
|
return AppDelegate.orientationMask
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
GeneratedPluginRegistrant.register(with: self)
|
|
||||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 关键:把当前的掩码提供给系统
|
|
||||||
override func application(_ application: UIApplication,
|
|
||||||
supportedInterfaceOrientationsFor window: UIWindow?)
|
|
||||||
-> UIInterfaceOrientationMask {
|
|
||||||
return AppDelegate.orientationMask
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,89 +1,94 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>${PRODUCT_NAME}</string>
|
<string>${PRODUCT_NAME}</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>智守安全</string>
|
<string>智守安全</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
|
<true/>
|
||||||
|
<key>NFCReaderUsageDescription</key>
|
||||||
|
<string>需要NFC权限来读取和写入标签</string>
|
||||||
|
<key>NSAppTransportSecurity</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>NFCReaderUsageDescription</key>
|
|
||||||
<string>用于读取 NFC 标签</string>
|
|
||||||
<key>UIBackgroundModes</key>
|
|
||||||
<array>
|
|
||||||
<string>remote-notification</string>
|
|
||||||
</array>
|
|
||||||
<key>NSAppTransportSecurity</key>
|
|
||||||
<dict>
|
|
||||||
<key>NSAllowsArbitraryLoads</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
<key>NSBluetoothAlwaysUsageDescription</key>
|
|
||||||
<string>app需要蓝牙权限连接设备</string>
|
|
||||||
<key>NSCameraUsageDescription</key>
|
|
||||||
<string>app需要相机权限来扫描二维码</string>
|
|
||||||
<key>NSContactsUsageDescription</key>
|
|
||||||
<string>app需要通讯录权限添加好友</string>
|
|
||||||
<key>NSHealthShareUsageDescription</key>
|
|
||||||
<string>app需要读取健康数据</string>
|
|
||||||
<key>NSHealthUpdateUsageDescription</key>
|
|
||||||
<string>app需要写入健康数据</string>
|
|
||||||
<key>NSLocalNetworkUsageDescription</key>
|
|
||||||
<string>app需要发现本地网络设备</string>
|
|
||||||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
|
||||||
<string>app需要后台定位以实现持续跟踪</string>
|
|
||||||
<key>NSLocationAlwaysUsageDescription</key>
|
|
||||||
<string>需要位置权限以提供定位服务</string>
|
|
||||||
<key>NSLocationWhenInUseUsageDescription</key>
|
|
||||||
<string>需要位置权限以提供定位服务</string>
|
|
||||||
<key>NSMicrophoneUsageDescription</key>
|
|
||||||
<string>app需要麦克风权限进行语音通话</string>
|
|
||||||
<key>NSMotionUsageDescription</key>
|
|
||||||
<string>app需要访问运动数据统计步数</string>
|
|
||||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
|
||||||
<string>app需要保存图片到相册</string>
|
|
||||||
<key>NSPhotoLibraryUsageDescription</key>
|
|
||||||
<string>app需要访问相册以上传图片</string>
|
|
||||||
<key>NSUserNotificationsUsageDescription</key>
|
|
||||||
<string>app需要发送通知提醒重要信息</string>
|
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
|
||||||
<true/>
|
|
||||||
<key>UILaunchStoryboardName</key>
|
|
||||||
<string>LaunchScreen</string>
|
|
||||||
<key>UIMainStoryboardFile</key>
|
|
||||||
<string>Main</string>
|
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
|
||||||
<array>
|
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
|
||||||
</array>
|
|
||||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
|
||||||
<array>
|
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
|
||||||
</array>
|
|
||||||
<key>UIStatusBarHidden</key>
|
|
||||||
<false/>
|
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>NSBluetoothAlwaysUsageDescription</key>
|
||||||
|
<string>app需要蓝牙权限连接设备</string>
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>app需要相机权限来扫描二维码</string>
|
||||||
|
<key>NSContactsUsageDescription</key>
|
||||||
|
<string>app需要通讯录权限添加好友</string>
|
||||||
|
<key>NSHealthShareUsageDescription</key>
|
||||||
|
<string>app需要读取健康数据</string>
|
||||||
|
<key>NSHealthUpdateUsageDescription</key>
|
||||||
|
<string>app需要写入健康数据</string>
|
||||||
|
<key>NSLocalNetworkUsageDescription</key>
|
||||||
|
<string>app需要发现本地网络设备</string>
|
||||||
|
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||||
|
<string>app需要后台定位以实现持续跟踪</string>
|
||||||
|
<key>NSLocationAlwaysUsageDescription</key>
|
||||||
|
<string>需要位置权限以提供定位服务</string>
|
||||||
|
<key>NSLocationWhenInUseUsageDescription</key>
|
||||||
|
<string>需要位置权限以提供定位服务</string>
|
||||||
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
|
<string>app需要麦克风权限进行语音通话</string>
|
||||||
|
<key>NSMotionUsageDescription</key>
|
||||||
|
<string>app需要访问运动数据统计步数</string>
|
||||||
|
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||||
|
<string>app需要保存图片到相册</string>
|
||||||
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
|
<string>app需要访问相册以上传图片</string>
|
||||||
|
<key>NSUserNotificationsUsageDescription</key>
|
||||||
|
<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>
|
||||||
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>com.apple.developer.nfc.readersession.formats</key>
|
||||||
|
<array>
|
||||||
|
<string>NDEF</string>
|
||||||
|
<string>TAG</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
@ -3,8 +3,11 @@
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>com.apple.developer.nfc.readersession.formats</key>
|
<key>com.apple.developer.nfc.readersession.formats</key>
|
||||||
<array>
|
<array>
|
||||||
<string>TAG</string>
|
<string>NDEF</string>
|
||||||
</array>
|
<string>TAG</string>
|
||||||
|
</array>
|
||||||
|
|
||||||
</dict>
|
</dict>
|
||||||
|
|
||||||
</plist>
|
</plist>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
/// 自定义默认按钮(支持不可点击/禁用状态)
|
||||||
/// 自定义默认按钮
|
|
||||||
class CustomButton extends StatelessWidget {
|
class CustomButton extends StatelessWidget {
|
||||||
final String text; // 按钮文字
|
final String text; // 按钮文字
|
||||||
final Color backgroundColor; // 按钮背景色
|
final Color backgroundColor; // 按钮背景色
|
||||||
|
|
@ -11,6 +10,16 @@ class CustomButton extends StatelessWidget {
|
||||||
final double? height; // 按钮高度
|
final double? height; // 按钮高度
|
||||||
final TextStyle? textStyle; // 文字样式
|
final TextStyle? textStyle; // 文字样式
|
||||||
|
|
||||||
|
/// 新增:是否可点击(true 可点,false 禁用)
|
||||||
|
/// 注意:如果 onPressed 为 null,也会被视为不可点击
|
||||||
|
final bool enabled;
|
||||||
|
|
||||||
|
/// 新增:禁用时的背景色(可选)
|
||||||
|
final Color? disabledBackgroundColor;
|
||||||
|
|
||||||
|
/// 新增:禁用时的文字颜色(可选)
|
||||||
|
final Color? disabledTextColor;
|
||||||
|
|
||||||
const CustomButton({
|
const CustomButton({
|
||||||
super.key,
|
super.key,
|
||||||
required this.text,
|
required this.text,
|
||||||
|
|
@ -21,27 +30,56 @@ class CustomButton extends StatelessWidget {
|
||||||
this.margin,
|
this.margin,
|
||||||
this.height,
|
this.height,
|
||||||
this.textStyle,
|
this.textStyle,
|
||||||
|
this.enabled = true,
|
||||||
|
this.disabledBackgroundColor,
|
||||||
|
this.disabledTextColor,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GestureDetector(
|
// 如果 enabled 为 false 或 onPressed 为 null,则视为不可点击
|
||||||
onTap: onPressed,
|
final bool isEnabled = enabled && onPressed != null;
|
||||||
child: Container(
|
|
||||||
height: height ?? 45, // 默认高度45
|
// 计算展示用背景色与文字样式
|
||||||
padding: padding ?? const EdgeInsets.all(8), // 默认内边距
|
final Color bgColor = isEnabled
|
||||||
margin: margin ?? const EdgeInsets.symmetric(horizontal: 5), // 默认外边距
|
? backgroundColor
|
||||||
decoration: BoxDecoration(
|
: (disabledBackgroundColor ?? Colors.grey.shade400);
|
||||||
borderRadius: BorderRadius.circular(borderRadius),
|
|
||||||
color: backgroundColor,
|
TextStyle finalTextStyle;
|
||||||
),
|
if (textStyle != null) {
|
||||||
child: Center(
|
finalTextStyle = isEnabled
|
||||||
child: Text(
|
? textStyle!
|
||||||
text,
|
: textStyle!.copyWith(
|
||||||
style: textStyle ?? const TextStyle(
|
color: disabledTextColor ?? textStyle!.color?.withOpacity(0.8) ?? Colors.white70,
|
||||||
color: Colors.white,
|
);
|
||||||
fontSize: 15,
|
} else {
|
||||||
fontWeight: FontWeight.bold,
|
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: bgColor,
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: finalTextStyle,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ class MediaPickerRow extends StatefulWidget {
|
||||||
final ValueChanged<String>? onMediaRemoved;
|
final ValueChanged<String>? onMediaRemoved;
|
||||||
final ValueChanged<String>? onMediaTapped; // 新增:媒体点击回调
|
final ValueChanged<String>? onMediaTapped; // 新增:媒体点击回调
|
||||||
final bool isEdit; // 新增:控制编辑状态
|
final bool isEdit; // 新增:控制编辑状态
|
||||||
|
final bool isCamera; // 新增:只能拍照
|
||||||
|
|
||||||
|
|
||||||
const MediaPickerRow({
|
const MediaPickerRow({
|
||||||
Key? key,
|
Key? key,
|
||||||
|
|
@ -31,6 +33,7 @@ class MediaPickerRow extends StatefulWidget {
|
||||||
this.onMediaRemoved,
|
this.onMediaRemoved,
|
||||||
this.onMediaTapped, // 新增
|
this.onMediaTapped, // 新增
|
||||||
this.isEdit = true, // 默认可编辑
|
this.isEdit = true, // 默认可编辑
|
||||||
|
this.isCamera = false,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@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 {
|
Future<void> _showPickerOptions() async {
|
||||||
if (!widget.isEdit) return; // 不可编辑时直接返回
|
if (!widget.isEdit) return; // 不可编辑时直接返回
|
||||||
|
|
||||||
|
|
@ -237,7 +249,7 @@ class _MediaPickerGridState extends State<MediaPickerRow> {
|
||||||
// 显示添加按钮
|
// 显示添加按钮
|
||||||
else if (showAddButton) {
|
else if (showAddButton) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: _showPickerOptions,
|
onTap: widget.isCamera?_cameraAction:_showPickerOptions,
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(color: Colors.black12),
|
border: Border.all(color: Colors.black12),
|
||||||
|
|
@ -273,6 +285,8 @@ class RepairedPhotoSection extends StatefulWidget {
|
||||||
final bool isRequired;
|
final bool isRequired;
|
||||||
final bool isShowNum;
|
final bool isShowNum;
|
||||||
final bool isEdit; // 新增:控制编辑状态
|
final bool isEdit; // 新增:控制编辑状态
|
||||||
|
final bool isCamera; // 新增:只能拍照
|
||||||
|
|
||||||
|
|
||||||
const RepairedPhotoSection({
|
const RepairedPhotoSection({
|
||||||
Key? key,
|
Key? key,
|
||||||
|
|
@ -290,6 +304,7 @@ class RepairedPhotoSection extends StatefulWidget {
|
||||||
this.isRequired = false,
|
this.isRequired = false,
|
||||||
this.isShowNum = true,
|
this.isShowNum = true,
|
||||||
this.isEdit = true, // 默认可编辑
|
this.isEdit = true, // 默认可编辑
|
||||||
|
this.isCamera = false,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -331,6 +346,7 @@ class _RepairedPhotoSectionState extends State<RepairedPhotoSection> {
|
||||||
maxCount: widget.maxCount,
|
maxCount: widget.maxCount,
|
||||||
mediaType: widget.mediaType,
|
mediaType: widget.mediaType,
|
||||||
initialMediaPaths: _mediaPaths,
|
initialMediaPaths: _mediaPaths,
|
||||||
|
isCamera: widget.isCamera,
|
||||||
onChanged: (files) {
|
onChanged: (files) {
|
||||||
final newPaths = files.map((f) => f.path).toList();
|
final newPaths = files.map((f) => f.path).toList();
|
||||||
setState(() {
|
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:flutter/material.dart';
|
||||||
import 'package:qhd_prevention/pages/badge_manager.dart';
|
import 'package:qhd_prevention/pages/badge_manager.dart';
|
||||||
import 'package:qhd_prevention/services/auth_service.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 'package:intl/date_symbol_data_local.dart';
|
||||||
import 'http/HttpManager.dart';
|
import 'http/HttpManager.dart';
|
||||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||||
|
import 'package:flutter/services.dart'; // for TextInput.hide
|
||||||
|
|
||||||
// 全局导航键
|
// 全局导航键
|
||||||
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
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 {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
|
@ -70,8 +103,6 @@ void main() async {
|
||||||
// 如果本地标记已登录,进一步验证 token 是否有效
|
// 如果本地标记已登录,进一步验证 token 是否有效
|
||||||
try {
|
try {
|
||||||
isLoggedIn = await AuthService.isLoggedIn();
|
isLoggedIn = await AuthService.isLoggedIn();
|
||||||
// 这里建议 AuthService.isLoggedIn() 内部实际请求一次用户信息接口
|
|
||||||
// 如果失败,就返回 false
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
isLoggedIn = false;
|
isLoggedIn = false;
|
||||||
}
|
}
|
||||||
|
|
@ -80,6 +111,7 @@ void main() async {
|
||||||
runApp(MyApp(isLoggedIn: isLoggedIn));
|
runApp(MyApp(isLoggedIn: isLoggedIn));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// MyApp:恢复为 Stateless(无需监听 viewInsets)
|
||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatelessWidget {
|
||||||
final bool isLoggedIn;
|
final bool isLoggedIn;
|
||||||
|
|
||||||
|
|
@ -90,13 +122,15 @@ class MyApp extends StatelessWidget {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
title: '',
|
title: '',
|
||||||
navigatorKey: navigatorKey,
|
navigatorKey: navigatorKey,
|
||||||
|
// 在路由变化时统一取消焦点(防止 push/pop 时焦点回到 TextField)
|
||||||
|
navigatorObservers: [KeyboardUnfocusNavigatorObserver()],
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
return EasyLoading.init(
|
return EasyLoading.init(
|
||||||
builder: (context, widget) {
|
builder: (context, widget) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
behavior: HitTestBehavior.translucent,
|
behavior: HitTestBehavior.translucent,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
// FocusScope.of(context).unfocus();
|
// 全局点击空白处取消焦点(隐藏键盘)
|
||||||
FocusHelper.clearFocus(context);
|
FocusHelper.clearFocus(context);
|
||||||
},
|
},
|
||||||
child: widget,
|
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();
|
String latitude=position.latitude.toString();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
Map data = {};
|
||||||
final result = await ApiService.addRiskListCheckApp(
|
final result = await ApiService.addRiskListCheckApp(
|
||||||
hazardDescription, partDescription, latitude, longitude,
|
hazardDescription, partDescription, latitude, longitude,
|
||||||
dangerDetail, dataTime, type, responsibleId,
|
dangerDetail, dataTime, type, responsibleId,
|
||||||
yinHuanTypeIds, hazardLeve, buMenId, buMenPDId,
|
yinHuanTypeIds, hazardLeve, buMenId, buMenPDId,
|
||||||
yinHuanTypeNames, hiddenType1, hiddenType2, hiddenType3,);
|
yinHuanTypeNames, hiddenType1, hiddenType2, hiddenType3,data);
|
||||||
if (result['result'] == 'success') {
|
if (result['result'] == 'success') {
|
||||||
|
|
||||||
String hiddenId = result['pd']['HIDDEN_ID'] ;
|
String hiddenId = result['pd']['HIDDEN_ID'] ;
|
||||||
|
|
|
||||||
|
|
@ -124,25 +124,51 @@ class _HomeNfcAddPageState extends State<HomeNfcAddPage> {
|
||||||
);
|
);
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
LoadingDialogHelper.show(message: '等待手机靠近NFC标签');
|
LoadingDialogHelper.show(message: '等待手机靠近NFC标签');
|
||||||
await NfcService.instance.writeText(
|
NfcService.instance.startScanOnceWithCallback(
|
||||||
mapToCompactJson({'PIPELINE_AREA_ID':pd['PIPELINE_AREA_ID'],'EQUIPMENT_PIPELINE_ID':pd['EQUIPMENT_PIPELINE_ID']}),
|
onResult: (uid, parsedText, rawMsg) async {
|
||||||
timeout: Duration(seconds: 12),
|
final result = await ApiService.nfcWriteCheck(uid);
|
||||||
onComplete: (ok, msg) async{
|
if (result['result'] == 'success') {
|
||||||
if (ok) {
|
_writeNFCInfoRequest();
|
||||||
pd['NFC_CODE'] = msg;
|
}else{
|
||||||
final result = await ApiService.nfcTagAdd(pd);
|
ToastUtil.showError(context, result['result'] ?? '');
|
||||||
if (result['result'] == 'success') {
|
}
|
||||||
ToastUtil.showSuccess(context, '写入成功');
|
LoadingDialogHelper.hide();
|
||||||
Navigator.pop(context);
|
|
||||||
}else{
|
|
||||||
ToastUtil.showError(context, '写入失败,请重试');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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),
|
||||||
|
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, '写入失败,请重试');
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
ToastUtil.showError(context, '$msg');
|
||||||
|
|
||||||
|
}
|
||||||
|
LoadingDialogHelper.hide();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
LoadingDialogHelper.hide();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
String mapToCompactJson(Map<String, dynamic> map) {
|
String mapToCompactJson(Map<String, dynamic> map) {
|
||||||
// 使用 jsonEncode 转换
|
// 使用 jsonEncode 转换
|
||||||
String jsonStr = jsonEncode(map);
|
String jsonStr = jsonEncode(map);
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,207 @@
|
||||||
import 'package:flutter/material.dart';
|
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/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 {
|
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
|
@override
|
||||||
State<HomeNfcCheckDangerPage> createState() => _HomeNfcCheckDangerPageState();
|
State<HomeNfcCheckDangerPage> createState() => _HomeNfcCheckDangerPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _HomeNfcCheckDangerPageState extends State<HomeNfcCheckDangerPage> {
|
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(
|
return SizedBox(
|
||||||
height: 180,
|
height: 180,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
|
|
@ -32,7 +222,7 @@ class _HomeNfcCheckDangerPageState extends State<HomeNfcCheckDangerPage> {
|
||||||
top: 12,
|
top: 12,
|
||||||
left: 12,
|
left: 12,
|
||||||
child: Text(
|
child: Text(
|
||||||
item['title']!,
|
item['TASK_NAME']?.toString() ?? '',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.black87,
|
color: Colors.black87,
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
|
|
@ -52,7 +242,7 @@ class _HomeNfcCheckDangerPageState extends State<HomeNfcCheckDangerPage> {
|
||||||
),
|
),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
item['status']!,
|
item['PATROL_TYPE_NAME'] ?? '',
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 14),
|
style: const TextStyle(color: Colors.white, fontSize: 14),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -66,11 +256,11 @@ class _HomeNfcCheckDangerPageState extends State<HomeNfcCheckDangerPage> {
|
||||||
top: 50, // 盖住图片底部
|
top: 50, // 盖住图片底部
|
||||||
child: Container(
|
child: Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 0),
|
margin: const EdgeInsets.symmetric(horizontal: 0),
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(5),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
boxShadow: [
|
boxShadow: const [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black12,
|
color: Colors.black12,
|
||||||
blurRadius: 4,
|
blurRadius: 4,
|
||||||
|
|
@ -86,32 +276,126 @@ class _HomeNfcCheckDangerPageState extends State<HomeNfcCheckDangerPage> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建信息网格
|
// 构建单选按钮(现在 option 为 OptionData)
|
||||||
Widget _buildInfoGrid(Map<String, String> item) {
|
Widget _buildOptionButton({
|
||||||
return Row(
|
required BuildContext context,
|
||||||
children: [
|
required OptionData option,
|
||||||
Expanded(
|
required double screenWidth,
|
||||||
child: Column(
|
required dynamic item,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
VoidCallback? onImageTap,
|
||||||
children: [
|
}) {
|
||||||
Text('负责部门:${item['department']}'),
|
final String value = option.value;
|
||||||
const SizedBox(height: 8),
|
final String label = option.label;
|
||||||
Text('负责人:${item['owner']}'),
|
final icon = option.icon;
|
||||||
const SizedBox(height: 8),
|
final color = option.color;
|
||||||
Text('UN件类型:${item['unType']}'),
|
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: [
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
Expanded(
|
),
|
||||||
child: Column(
|
);
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
}
|
||||||
children: [
|
|
||||||
Text('巡检周期:${item['cycle']}'),
|
/// 构建信息网格
|
||||||
const SizedBox(height: 8),
|
Widget _buildInfoGrid(Map item) {
|
||||||
Text('已巡点位:${item['points']}'),
|
final screenWidth = MediaQuery.of(context).size.width;
|
||||||
Text('涉及管道区域:${item['department']}')
|
|
||||||
],
|
return Column(
|
||||||
),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
ItemListWidget.singleLineTitleText(
|
||||||
|
label: '检查项:',
|
||||||
|
isEditable: false,
|
||||||
|
text: item['EQUIPMENT_NAME'] ?? '',
|
||||||
|
),
|
||||||
|
ItemListWidget.singleLineTitleText(
|
||||||
|
label: '检查内容:',
|
||||||
|
isEditable: false,
|
||||||
|
text: item['INSPECTION_CONTENT'] ?? '',
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
// 单选按钮组
|
||||||
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final bool canSubmit = selectType != null; // 只有选中后才可以提交
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: MyAppbar(title: '检查项'),
|
appBar: MyAppbar(title: '检查项'),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
_pendingTopCard({}),
|
children: [
|
||||||
]
|
_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 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
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/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/home/tap/item_list_widget.dart';
|
||||||
import 'package:qhd_prevention/pages/my_appbar.dart';
|
import 'package:qhd_prevention/pages/my_appbar.dart';
|
||||||
|
import 'package:qhd_prevention/services/nfc_service.dart';
|
||||||
import 'package:qhd_prevention/tools/tools.dart';
|
import 'package:qhd_prevention/tools/tools.dart';
|
||||||
|
|
||||||
class HomeNfcDetailPage extends StatefulWidget {
|
class HomeNfcDetailPage extends StatefulWidget {
|
||||||
const HomeNfcDetailPage({super.key, required this.info});
|
const HomeNfcDetailPage({super.key, required this.info});
|
||||||
|
|
||||||
final Map<String, dynamic> info;
|
final Map info;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<HomeNfcDetailPage> createState() => _HomeNfcDetailPageState();
|
State<HomeNfcDetailPage> createState() => _HomeNfcDetailPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _HomeNfcDetailPageState extends State<HomeNfcDetailPage> {
|
class _HomeNfcDetailPageState extends State<HomeNfcDetailPage> {
|
||||||
List<ProgressItem> items = [];
|
List<dynamic> items = [];
|
||||||
int currentPage = 1;
|
int currentPage = 1;
|
||||||
final int pageSize = 10;
|
final int pageSize = 10;
|
||||||
|
late var _total = 0;
|
||||||
|
late var _totalResult = 0;
|
||||||
|
|
||||||
bool isLoading = false; // 当前请求中
|
bool isLoading = false; // 当前请求中
|
||||||
bool hasMore = true; // 是否还有更多数据
|
bool hasMore = true; // 是否还有更多数据
|
||||||
|
|
@ -61,11 +69,15 @@ class _HomeNfcDetailPageState extends State<HomeNfcDetailPage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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(
|
final result = await ApiService.nfcTaskDetailList(
|
||||||
pageSize,
|
pageSize,
|
||||||
currentPage,
|
currentPage,
|
||||||
widget.info['PATROL_TASK_ID'] ?? '',
|
data,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
|
|
@ -74,35 +86,20 @@ class _HomeNfcDetailPageState extends State<HomeNfcDetailPage> {
|
||||||
}
|
}
|
||||||
printLongString(jsonEncode(result));
|
printLongString(jsonEncode(result));
|
||||||
if (result['result'] == 'success') {
|
if (result['result'] == 'success') {
|
||||||
// 兼容常见返回字段,尽量从 data / rows / list 里取
|
|
||||||
final dynamic rawList = result['varList'];
|
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(() {
|
setState(() {
|
||||||
|
Map page = result['page'];
|
||||||
|
_totalResult = page['totalResult'] as int;
|
||||||
|
_total = result['checkedCount'] as int;
|
||||||
if (refresh) {
|
if (refresh) {
|
||||||
items = fetched;
|
items = rawList;
|
||||||
} else {
|
} else {
|
||||||
items.addAll(fetched);
|
items.addAll(rawList);
|
||||||
}
|
}
|
||||||
// 如果本次返回少于 pageSize 则说明没有更多
|
// 如果本次返回少于 pageSize 则说明没有更多
|
||||||
if (fetched.length < pageSize) {
|
if (rawList.length < pageSize) {
|
||||||
hasMore = false;
|
hasMore = false;
|
||||||
} else {
|
} else {
|
||||||
// 成功拿到一页,页码自增
|
|
||||||
currentPage++;
|
currentPage++;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -126,23 +123,74 @@ class _HomeNfcDetailPageState extends State<HomeNfcDetailPage> {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _startCheckItem(ProgressItem item, int index) async {
|
Future<void> _startCheckItem(Map item, int index) async {
|
||||||
// TODO: 根据业务替换为真实逻辑(例如跳转到检查页面或调用写 NFC)
|
final confirmed = await CustomAlertDialog.showConfirm(
|
||||||
// 这里示例:将当前项标记为已查并设置检查时间(仅本地更新)
|
context,
|
||||||
final now = DateTime.now();
|
title: '温馨提示',
|
||||||
setState(() {
|
content: '请将手机贴近设备标签',
|
||||||
items[index] = items[index].copyWith(
|
cancelText: '',
|
||||||
status: '已查',
|
confirmText: '我知道了',
|
||||||
checkTime:
|
barrierDismissible: false,
|
||||||
'${now.year}-${_two(now.month)}-${_two(now.day)} ${_two(now.hour)}:${_two(now.minute)}',
|
);
|
||||||
);
|
if (confirmed) {
|
||||||
});
|
LoadingDialogHelper.show(message: '等待设备靠近设备NFC标签');
|
||||||
|
NfcService.instance.startScanOnceWithCallback(
|
||||||
|
onResult: (uid, parsedText, rawMsg) async {
|
||||||
|
_getNFCForUid(uid, parsedText);
|
||||||
|
},
|
||||||
|
onError: (err) {
|
||||||
|
|
||||||
// 额外:你可能需要调用后端接口上报检查结果
|
ToastUtil.showError(context, '$err');
|
||||||
// await ApiService.reportCheck(items[index].code, ...);
|
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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
|
@ -150,7 +198,7 @@ class _HomeNfcDetailPageState extends State<HomeNfcDetailPage> {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _pendingTopCard(Map<String, dynamic> item) {
|
Widget _pendingTopCard(Map item) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: 180,
|
height: 180,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
|
|
@ -232,45 +280,43 @@ class _HomeNfcDetailPageState extends State<HomeNfcDetailPage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建信息网格
|
/// 构建信息网格
|
||||||
Widget _buildInfoGrid(Map<String, dynamic> item) {
|
Widget _buildInfoGrid(Map item) {
|
||||||
return Expanded(
|
return Column(
|
||||||
child: Column(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
children: [
|
||||||
children: [
|
// const SizedBox(height: 8),
|
||||||
// const SizedBox(height: 8),
|
ItemListWidget.singleLineTitleText(
|
||||||
ItemListWidget.singleLineTitleText(
|
label: '负责部门',
|
||||||
label: '负责部门',
|
isEditable: false,
|
||||||
isEditable: false,
|
text: item['DEPARTMENT_NAME'] ?? '',
|
||||||
text: item['DEPARTMENT_NAME'] ?? '',
|
),
|
||||||
),
|
ItemListWidget.singleLineTitleText(
|
||||||
ItemListWidget.singleLineTitleText(
|
label: '负责人',
|
||||||
label: '负责人',
|
isEditable: false,
|
||||||
isEditable: false,
|
text: item['USER_NAME'] ?? '',
|
||||||
text: item['USER_NAME'] ?? '',
|
),
|
||||||
),
|
ItemListWidget.singleLineTitleText(
|
||||||
ItemListWidget.singleLineTitleText(
|
label: '涉及管道区域:',
|
||||||
label: '涉及管道区域:',
|
isEditable: false,
|
||||||
isEditable: false,
|
text: item['PIPELINE_AREAS_NAMES'] ?? '',
|
||||||
text: item['PIPELINE_AREAS_NAMES'] ?? '',
|
),
|
||||||
),
|
ItemListWidget.singleLineTitleText(
|
||||||
ItemListWidget.singleLineTitleText(
|
label: '巡检周期',
|
||||||
label: '巡检周期',
|
isEditable: false,
|
||||||
isEditable: false,
|
text: item['PATROL_PERIOD_NAME'] ?? '',
|
||||||
text: item['PATROL_PERIOD_NAME'] ?? '',
|
),
|
||||||
),
|
ItemListWidget.singleLineTitleText(
|
||||||
ItemListWidget.singleLineTitleText(
|
label: '任务时间',
|
||||||
label: '任务时间',
|
isEditable: false,
|
||||||
isEditable: false,
|
text: item['OPERATTIME'] ?? '',
|
||||||
text: item['OPERATTIME'] ?? '',
|
),
|
||||||
),
|
],
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildListItem(BuildContext context, int idx) {
|
Widget _buildListItem(BuildContext context, int idx) {
|
||||||
final item = items[idx];
|
final item = items[idx];
|
||||||
final bool unchecked = item.status == '未查';
|
final bool unchecked = item['INSPECTED_FLAG'] == '0';
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
height: 100,
|
height: 100,
|
||||||
|
|
@ -294,7 +340,7 @@ class _HomeNfcDetailPageState extends State<HomeNfcDetailPage> {
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 2,
|
top: 2,
|
||||||
child: Text(
|
child: Text(
|
||||||
item.status,
|
unchecked ? '未查' : '已查',
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 14),
|
style: const TextStyle(color: Colors.white, fontSize: 14),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -309,51 +355,97 @@ class _HomeNfcDetailPageState extends State<HomeNfcDetailPage> {
|
||||||
|
|
||||||
// 中间详情
|
// 中间详情
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: GestureDetector(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
onTap: (){
|
||||||
children: [
|
if (!unchecked) {
|
||||||
Text(
|
Map data = {...item, ...widget.info, "NFC_CODE": item['PIPELINE_AREA_ID'], 'MANUAL_CONFIRMATION': '0',
|
||||||
item.location,
|
};
|
||||||
style: const TextStyle(
|
pushPage(
|
||||||
fontSize: 16,
|
HomeNfcCheckDangerPage(info: data, facebookImages: [], isNfcError: false,),
|
||||||
fontWeight: FontWeight.bold,
|
context,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
item['PIPELINE_AREA_NAME'] ?? '',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
maxLines: 1, // 最多一行
|
||||||
|
overflow: TextOverflow.ellipsis, // 超出省略号
|
||||||
),
|
),
|
||||||
maxLines: 1, // 最多一行
|
const SizedBox(height: 4),
|
||||||
overflow: TextOverflow.ellipsis, // 超出省略号
|
Text('NFC编码:${item['NFC_CODE'] ?? ''}'),
|
||||||
),
|
const SizedBox(height: 6),
|
||||||
const SizedBox(height: 4),
|
unchecked
|
||||||
Text('NFC编码:${item.code}'),
|
? Row(
|
||||||
const SizedBox(height: 6),
|
spacing: 10,
|
||||||
unchecked
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
? InkWell(
|
children: [
|
||||||
onTap: () => _startCheckItem(item, idx),
|
Expanded(
|
||||||
child: Container(
|
child: InkWell(
|
||||||
height: 35,
|
onTap: () => _startCheckItem(item, idx),
|
||||||
alignment: Alignment.center,
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 1),
|
height: 35,
|
||||||
decoration: BoxDecoration(
|
// width: 120,
|
||||||
gradient: const LinearGradient(
|
alignment: Alignment.center,
|
||||||
colors: [Color(0xFFFFA726), Color(0xFFFF7043)],
|
padding: const EdgeInsets.symmetric(vertical: 1),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: const LinearGradient(
|
||||||
|
colors: [
|
||||||
|
Color(0xFFFFA726),
|
||||||
|
Color(0xFFFF7043),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(5),
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'NFC检查',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 15,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(5),
|
|
||||||
),
|
|
||||||
child: const Text(
|
|
||||||
'开始检查',
|
|
||||||
style: TextStyle(color: Colors.white, fontSize: 14),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
CustomButton(
|
||||||
: Text(
|
onPressed: () {
|
||||||
'检查时间:${item.checkTime ?? ''}',
|
pushPage(
|
||||||
style: const TextStyle(fontSize: 14),
|
NfcQuestionFecebook(
|
||||||
textAlign: TextAlign.center,
|
info: item,
|
||||||
),
|
taskInfo: widget.info,
|
||||||
],
|
),
|
||||||
),
|
context,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
text: '手动检查',
|
||||||
|
height: 35,
|
||||||
|
textStyle: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 15,
|
||||||
|
),
|
||||||
|
backgroundColor: Colors.blue,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
'检查时间:${item['PATROL_TIME'] ?? ''}',
|
||||||
|
style: const TextStyle(fontSize: 14),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
),
|
),
|
||||||
|
|
||||||
// 右侧箭头
|
// 右侧箭头
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
|
if (!unchecked)
|
||||||
const Icon(Icons.chevron_right, color: Colors.grey),
|
const Icon(Icons.chevron_right, color: Colors.grey),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -371,6 +463,7 @@ class _HomeNfcDetailPageState extends State<HomeNfcDetailPage> {
|
||||||
children: [
|
children: [
|
||||||
_pendingTopCard(widget.info),
|
_pendingTopCard(widget.info),
|
||||||
const SizedBox(height: 60),
|
const SizedBox(height: 60),
|
||||||
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
|
@ -384,61 +477,133 @@ class _HomeNfcDetailPageState extends State<HomeNfcDetailPage> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: RefreshIndicator(
|
// 白色容器内部:统计行 + 列表(统计行放在列表顶部)
|
||||||
onRefresh: () => _getTaskDetail(refresh: true),
|
child: Column(
|
||||||
child:
|
children: [
|
||||||
firstLoad && isLoading
|
// 统计行(放在容器顶部,作为视觉上的头部)
|
||||||
? const Center(
|
Padding(
|
||||||
child: Padding(
|
padding: const EdgeInsets.symmetric(
|
||||||
padding: EdgeInsets.all(16.0),
|
horizontal: 12,
|
||||||
child: CircularProgressIndicator(),
|
vertical: 10,
|
||||||
|
),
|
||||||
|
child: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 5,
|
||||||
|
vertical: 8,
|
||||||
),
|
),
|
||||||
)
|
|
||||||
: items.isEmpty
|
child: Row(
|
||||||
? ListView(
|
children: [
|
||||||
controller: _scrollController,
|
Text(
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
'已查点位 $_total / $_totalResult ',
|
||||||
children: [
|
style: const TextStyle(
|
||||||
const SizedBox(height: 80),
|
fontSize: 14,
|
||||||
Center(
|
fontWeight: FontWeight.w600,
|
||||||
child: Text(isLoading ? '加载中...' : '暂无数据'),
|
),
|
||||||
),
|
),
|
||||||
],
|
const Spacer(),
|
||||||
)
|
if (isLoading)
|
||||||
: ListView.builder(
|
const SizedBox(
|
||||||
controller: _scrollController,
|
width: 18,
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
height: 18,
|
||||||
itemCount: items.length + 1, // 多一个用于加载状态/底部提示
|
child: CircularProgressIndicator(
|
||||||
itemBuilder: (ctx, idx) {
|
strokeWidth: 2,
|
||||||
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(),
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
} else {
|
],
|
||||||
return const Padding(
|
),
|
||||||
padding: EdgeInsets.symmetric(
|
);
|
||||||
vertical: 12,
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// 列表区域(占用剩余空间)
|
||||||
|
Expanded(
|
||||||
|
child: RefreshIndicator(
|
||||||
|
onRefresh: () => _getTaskDetail(refresh: true),
|
||||||
|
child:
|
||||||
|
firstLoad && isLoading
|
||||||
|
? const Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(16.0),
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: items.isEmpty
|
||||||
|
? ListView(
|
||||||
|
// 保持可下拉刷新
|
||||||
|
controller: _scrollController,
|
||||||
|
physics:
|
||||||
|
const AlwaysScrollableScrollPhysics(),
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
Center(
|
||||||
|
child: Text(
|
||||||
|
isLoading ? '加载中...' : '暂无数据',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Center(child: Text('没有更多了')),
|
],
|
||||||
);
|
)
|
||||||
}
|
: ListView.builder(
|
||||||
}
|
controller: _scrollController,
|
||||||
},
|
physics:
|
||||||
),
|
const AlwaysScrollableScrollPhysics(),
|
||||||
|
itemCount:
|
||||||
|
items.length + 1, // 多一个用于加载状态/底部提示
|
||||||
|
itemBuilder: (ctx, idx) {
|
||||||
|
if (idx < items.length) {
|
||||||
|
return _buildListItem(ctx, idx);
|
||||||
|
} else {
|
||||||
|
// 底部加载/没有更多提示
|
||||||
|
if (hasMore) {
|
||||||
|
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('没有更多了')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -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(
|
Expanded(
|
||||||
child:
|
child:
|
||||||
isEditable
|
isEditable
|
||||||
? TextField(
|
? TextFormField(
|
||||||
autofocus: false,
|
autofocus: false,
|
||||||
|
initialValue: text,
|
||||||
controller: controller,
|
controller: controller,
|
||||||
keyboardType: TextInputType.multiline,
|
keyboardType: TextInputType.multiline,
|
||||||
maxLines: null,
|
maxLines: null,
|
||||||
|
|
@ -131,9 +132,10 @@ class ItemListWidget {
|
||||||
textAlignVertical: TextAlignVertical.top,
|
textAlignVertical: TextAlignVertical.top,
|
||||||
style: TextStyle(fontSize: fontSize),
|
style: TextStyle(fontSize: fontSize),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
|
|
||||||
hintText: hintText,
|
hintText: hintText,
|
||||||
// 去掉 TextField 默认内边距
|
// 去掉 TextField 默认内边距
|
||||||
//contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import 'package:nfc_manager_ndef/nfc_manager_ndef.dart';
|
||||||
/// 注:iOS 必须在真机测试,并在 Xcode 中打开 NFC 权限;Android 需设备支持 NFC。
|
/// 注:iOS 必须在真机测试,并在 Xcode 中打开 NFC 权限;Android 需设备支持 NFC。
|
||||||
class NfcService {
|
class NfcService {
|
||||||
NfcService._internal();
|
NfcService._internal();
|
||||||
|
|
||||||
static final NfcService instance = NfcService._internal();
|
static final NfcService instance = NfcService._internal();
|
||||||
|
|
||||||
/// 是否正在进行 NFC 会话(扫描或写入)
|
/// 是否正在进行 NFC 会话(扫描或写入)
|
||||||
|
|
@ -31,6 +32,7 @@ class NfcService {
|
||||||
|
|
||||||
/// 日志广播流(方便 UI 订阅)
|
/// 日志广播流(方便 UI 订阅)
|
||||||
final StreamController<String> _logController = StreamController.broadcast();
|
final StreamController<String> _logController = StreamController.broadcast();
|
||||||
|
|
||||||
Stream<String> get logs => _logController.stream;
|
Stream<String> get logs => _logController.stream;
|
||||||
|
|
||||||
/// 检查设备是否支持 NFC
|
/// 检查设备是否支持 NFC
|
||||||
|
|
@ -50,7 +52,10 @@ class NfcService {
|
||||||
|
|
||||||
/// bytes -> "AA:BB:CC" 形式的大写十六进制字符串
|
/// bytes -> "AA:BB:CC" 形式的大写十六进制字符串
|
||||||
String _bytesToHex(Uint8List bytes) {
|
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)
|
/// 递归在 Map/List 中查找第一个可用的字节数组 (List<int> / Uint8List)
|
||||||
|
|
@ -101,12 +106,14 @@ class NfcService {
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
try {
|
try {
|
||||||
final nfcA = NfcAAndroid.from(tag);
|
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 (_) {}
|
} catch (_) {}
|
||||||
} else if (Platform.isIOS) {
|
} else if (Platform.isIOS) {
|
||||||
try {
|
try {
|
||||||
final mifare = MiFareIos.from(tag);
|
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 (_) {}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -131,9 +138,10 @@ class NfcService {
|
||||||
String _formatMessageToString(dynamic msg) {
|
String _formatMessageToString(dynamic msg) {
|
||||||
if (msg == null) return '<empty>';
|
if (msg == null) return '<empty>';
|
||||||
try {
|
try {
|
||||||
final records = (msg is Map && msg['records'] != null)
|
final records =
|
||||||
? List<dynamic>.from(msg['records'])
|
(msg is Map && msg['records'] != null)
|
||||||
: (msg is dynamic ? (msg.records as List<dynamic>?) : null);
|
? List<dynamic>.from(msg['records'])
|
||||||
|
: (msg is dynamic ? (msg.records as List<dynamic>?) : null);
|
||||||
if (records == null || records.isEmpty) return '<empty>';
|
if (records == null || records.isEmpty) return '<empty>';
|
||||||
final sb = StringBuffer();
|
final sb = StringBuffer();
|
||||||
for (var i = 0; i < records.length; i++) {
|
for (var i = 0; i < records.length; i++) {
|
||||||
|
|
@ -144,12 +152,16 @@ class NfcService {
|
||||||
Uint8List? p;
|
Uint8List? p;
|
||||||
if (r is Map && r['payload'] != null) {
|
if (r is Map && r['payload'] != null) {
|
||||||
final ptmp = r['payload'];
|
final ptmp = r['payload'];
|
||||||
if (ptmp is Uint8List) p = ptmp;
|
if (ptmp is Uint8List)
|
||||||
else if (ptmp is List<int>) p = Uint8List.fromList(ptmp);
|
p = ptmp;
|
||||||
|
else if (ptmp is List<int>)
|
||||||
|
p = Uint8List.fromList(ptmp);
|
||||||
} else {
|
} else {
|
||||||
final pr = (r as dynamic).payload;
|
final pr = (r as dynamic).payload;
|
||||||
if (pr is Uint8List) p = pr;
|
if (pr is Uint8List)
|
||||||
else if (pr is List<int>) p = Uint8List.fromList(pr);
|
p = pr;
|
||||||
|
else if (pr is List<int>)
|
||||||
|
p = Uint8List.fromList(pr);
|
||||||
}
|
}
|
||||||
if (p != null) {
|
if (p != null) {
|
||||||
final txt = parseTextFromPayload(p);
|
final txt = parseTextFromPayload(p);
|
||||||
|
|
@ -171,7 +183,8 @@ class NfcService {
|
||||||
/// onError(error) - 出错时回调
|
/// onError(error) - 出错时回调
|
||||||
/// timeout - 超时时间(可选)
|
/// timeout - 超时时间(可选)
|
||||||
Future<void> startScanOnceWithCallback({
|
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,
|
void Function(Object error)? onError,
|
||||||
Duration? timeout,
|
Duration? timeout,
|
||||||
}) async {
|
}) async {
|
||||||
|
|
@ -215,8 +228,12 @@ class NfcService {
|
||||||
if ((rawMsg.records as List).isNotEmpty) {
|
if ((rawMsg.records as List).isNotEmpty) {
|
||||||
final first = rawMsg.records.first;
|
final first = rawMsg.records.first;
|
||||||
final payload = first.payload;
|
final payload = first.payload;
|
||||||
if (payload is Uint8List) parsedText = parseTextFromPayload(payload);
|
if (payload is Uint8List)
|
||||||
else if (payload is List<int>) parsedText = parseTextFromPayload(Uint8List.fromList(payload));
|
parsedText = parseTextFromPayload(payload);
|
||||||
|
else if (payload is List<int>)
|
||||||
|
parsedText = parseTextFromPayload(
|
||||||
|
Uint8List.fromList(payload),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -226,14 +243,24 @@ class NfcService {
|
||||||
// 回退:尝试从 tag map 中嗅探 cachedMessage / records
|
// 回退:尝试从 tag map 中嗅探 cachedMessage / records
|
||||||
try {
|
try {
|
||||||
if (tag is Map) {
|
if (tag is Map) {
|
||||||
rawMsg = tag['cachedMessage'] ?? tag['ndef']?['cachedMessage'] ?? tag['message'];
|
rawMsg =
|
||||||
|
tag['cachedMessage'] ??
|
||||||
|
tag['ndef']?['cachedMessage'] ??
|
||||||
|
tag['message'];
|
||||||
if (rawMsg != null) {
|
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) {
|
if (recs != null && recs is List && recs.isNotEmpty) {
|
||||||
final r = recs.first;
|
final r = recs.first;
|
||||||
final p = (r is Map) ? r['payload'] : (r as dynamic).payload;
|
final p =
|
||||||
if (p is Uint8List) parsedText = parseTextFromPayload(p);
|
(r is Map) ? r['payload'] : (r as dynamic).payload;
|
||||||
else if (p is List<int>) parsedText = parseTextFromPayload(Uint8List.fromList(p));
|
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);
|
onResult(uid, parsedText, rawMsg);
|
||||||
_logController.add('UID: $uid\nmessage: ${_formatMessageToString(rawMsg)}');
|
_logController.add(
|
||||||
|
'UID: $uid\nmessage: ${_formatMessageToString(rawMsg)}',
|
||||||
|
);
|
||||||
|
|
||||||
await stopSession();
|
await stopSession();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -255,7 +284,6 @@ class NfcService {
|
||||||
},
|
},
|
||||||
// 尽量多协议尝试以提升兼容性
|
// 尽量多协议尝试以提升兼容性
|
||||||
pollingOptions: {NfcPollingOption.iso14443},
|
pollingOptions: {NfcPollingOption.iso14443},
|
||||||
|
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
scanning.value = false;
|
scanning.value = false;
|
||||||
|
|
@ -265,7 +293,9 @@ class NfcService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Future 式读取:解析第一条文本记录并返回字符串(格式:'UID\n文本')
|
/// 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>();
|
final completer = Completer<String>();
|
||||||
await startScanOnceWithCallback(
|
await startScanOnceWithCallback(
|
||||||
onResult: (uid, parsedText, rawMsg) {
|
onResult: (uid, parsedText, rawMsg) {
|
||||||
|
|
@ -301,8 +331,14 @@ class NfcService {
|
||||||
/// - onComplete: 回调 (ok, err)
|
/// - onComplete: 回调 (ok, err)
|
||||||
///
|
///
|
||||||
/// 返回 true 表示写入成功(同时 onComplete 也会被触发)
|
/// 返回 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();
|
final available = await isAvailable();
|
||||||
|
debugPrint('writeText: isAvailable=$available');
|
||||||
if (!available) {
|
if (!available) {
|
||||||
onComplete?.call(false, 'NFC not available');
|
onComplete?.call(false, 'NFC not available');
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -312,6 +348,12 @@ class NfcService {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await NfcManager.instance.stopSession();
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('stopSession ignore: $e');
|
||||||
|
}
|
||||||
|
|
||||||
scanning.value = true;
|
scanning.value = true;
|
||||||
Timer? timer;
|
Timer? timer;
|
||||||
if (timeout != null) {
|
if (timeout != null) {
|
||||||
|
|
@ -324,63 +366,85 @@ class NfcService {
|
||||||
|
|
||||||
bool success = false;
|
bool success = false;
|
||||||
try {
|
try {
|
||||||
await NfcManager.instance.startSession(onDiscovered: (dynamic tag) async {
|
// 修改了 pollingOptions 配置
|
||||||
try {
|
final polling =
|
||||||
final ndef = Ndef.from(tag);
|
Platform.isIOS
|
||||||
if (ndef == null) {
|
? {NfcPollingOption.iso14443} // iOS 使用更具体的配置
|
||||||
onComplete?.call(false, 'Tag 不支持 NDEF');
|
: {
|
||||||
await stopSession();
|
NfcPollingOption.iso14443,
|
||||||
return;
|
NfcPollingOption.iso15693,
|
||||||
}
|
NfcPollingOption.iso18092,
|
||||||
|
};
|
||||||
|
|
||||||
final payload = _buildTextPayload(text, lang: 'en');
|
await NfcManager.instance.startSession(
|
||||||
final record = NdefRecord(
|
pollingOptions: polling,
|
||||||
typeNameFormat: TypeNameFormat.wellKnown,
|
onDiscovered: (dynamic tag) async {
|
||||||
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();
|
timer?.cancel();
|
||||||
scanning.value = false;
|
try {
|
||||||
}
|
final ndef = Ndef.from(tag);
|
||||||
}, pollingOptions: {NfcPollingOption.iso14443});
|
if (ndef == null) {
|
||||||
|
onComplete?.call(false, 'Tag not NDEF');
|
||||||
|
await stopSession();
|
||||||
|
scanning.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
} catch (e) {
|
// 检查是否可写
|
||||||
scanning.value = false;
|
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,
|
||||||
|
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 返回出去
|
||||||
|
} catch (e, st) {
|
||||||
|
debugPrint('iOS NFC Write Error: $e');
|
||||||
|
debugPrint('Stack trace: $st');
|
||||||
|
onComplete?.call(false, e);
|
||||||
|
} finally {
|
||||||
|
await stopSession();
|
||||||
|
scanning.value = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (e, st) {
|
||||||
|
debugPrint('startSession exception: $e\n$st');
|
||||||
timer?.cancel();
|
timer?.cancel();
|
||||||
|
scanning.value = false;
|
||||||
onComplete?.call(false, e);
|
onComplete?.call(false, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,19 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
import 'dart:async';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
int getRandomWithNum(int min, int max) {
|
int getRandomWithNum(int min, int max) {
|
||||||
|
if (max < min) {
|
||||||
|
// 保护性处理:交换或抛错,这里交换
|
||||||
|
final tmp = min;
|
||||||
|
min = max;
|
||||||
|
max = tmp;
|
||||||
|
}
|
||||||
final random = Random();
|
final random = Random();
|
||||||
return random.nextInt(max) + min; // 生成随机数
|
return random.nextInt(max - min + 1) + min; // 生成 [min, max] 的随机数
|
||||||
}
|
}
|
||||||
|
|
||||||
double screenWidth(BuildContext context) {
|
double screenWidth(BuildContext context) {
|
||||||
|
|
@ -332,27 +339,48 @@ void presentPage(BuildContext context, Widget page) {
|
||||||
MaterialPageRoute(fullscreenDialog: true, builder: (_) => page),
|
MaterialPageRoute(fullscreenDialog: true, builder: (_) => page),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class LoadingDialogHelper {
|
class LoadingDialogHelper {
|
||||||
// 显示加载框
|
static Timer? _timer;
|
||||||
static void show({String? message}) {
|
|
||||||
|
/// 显示加载框(带超时,默认 10 秒)
|
||||||
|
static void show({String? message, Duration timeout = const Duration(seconds: 10)}) {
|
||||||
|
// 先清理上一个计时器,避免重复
|
||||||
|
_timer?.cancel();
|
||||||
|
|
||||||
if (message != null) {
|
if (message != null) {
|
||||||
EasyLoading.show(status: message);
|
EasyLoading.show(status: message);
|
||||||
} else {
|
} else {
|
||||||
EasyLoading.show();
|
EasyLoading.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 设置超时自动隐藏
|
||||||
|
_timer = Timer(timeout, () {
|
||||||
|
// 保护性调用 dismiss(避免访问不存在的 isShow)
|
||||||
|
try {
|
||||||
|
EasyLoading.dismiss();
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('EasyLoading.dismiss error: $e');
|
||||||
|
}
|
||||||
|
_timer?.cancel();
|
||||||
|
_timer = null;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 隐藏加载框
|
/// 隐藏加载框(手动触发)
|
||||||
static void hide() {
|
static void hide() {
|
||||||
if (EasyLoading.isShow) {
|
// 清理计时器
|
||||||
|
_timer?.cancel();
|
||||||
|
_timer = null;
|
||||||
|
|
||||||
|
try {
|
||||||
EasyLoading.dismiss();
|
EasyLoading.dismiss();
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('EasyLoading.dismiss error: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// 将秒数转换为 “HH:MM:SS” 格式
|
/// 将秒数转换为 “HH:MM:SS” 格式
|
||||||
String secondsCount(dynamic seconds) {
|
String secondsCount(dynamic seconds) {
|
||||||
// 先尝试解析出一个 double 值
|
// 先尝试解析出一个 double 值
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue