。。。
parent
31b5a9b532
commit
172ac497b6
|
|
@ -1,77 +1,94 @@
|
|||
import UIKit
|
||||
import Flutter
|
||||
import UIKit
|
||||
import Flutter
|
||||
import AVFoundation
|
||||
|
||||
@main
|
||||
@objc class AppDelegate: FlutterAppDelegate {
|
||||
// 动态方向掩码(默认竖屏)
|
||||
static var orientationMask: UIInterfaceOrientationMask = .portrait
|
||||
@main
|
||||
@objc class AppDelegate: FlutterAppDelegate {
|
||||
// 动态方向掩码(默认竖屏)
|
||||
static var orientationMask: UIInterfaceOrientationMask = .portrait
|
||||
|
||||
override func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
) -> Bool {
|
||||
override func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
) -> Bool {
|
||||
|
||||
let controller = window?.rootViewController as! FlutterViewController
|
||||
let channel = FlutterMethodChannel(name: "app.orientation",
|
||||
let controller = window?.rootViewController as! FlutterViewController
|
||||
|
||||
// 1) orientation channel (你已有)
|
||||
let orientationChannel = FlutterMethodChannel(name: "app.orientation",
|
||||
binaryMessenger: controller.binaryMessenger)
|
||||
orientationChannel.setMethodCallHandler { [weak self] call, result in
|
||||
guard let self = self else { return }
|
||||
|
||||
channel.setMethodCallHandler { [weak self] call, result in
|
||||
guard let self = self else { return }
|
||||
|
||||
if call.method == "setOrientation" {
|
||||
guard let arg = call.arguments as? String else {
|
||||
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
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let target: UIInterfaceOrientation =
|
||||
(arg == "landscape") ? .landscapeLeft : .portrait
|
||||
UIDevice.current.setValue(target.rawValue, forKey: "orientation")
|
||||
UIViewController.attemptRotationToDeviceOrientation()
|
||||
}
|
||||
|
||||
result(true)
|
||||
} else {
|
||||
result(FlutterMethodNotImplemented)
|
||||
}
|
||||
if call.method == "setOrientation" {
|
||||
guard let arg = call.arguments as? String else {
|
||||
result(FlutterError(code: "BAD_ARGS", message: "need 'landscape' | 'portrait'", details: nil))
|
||||
return
|
||||
}
|
||||
|
||||
GeneratedPluginRegistrant.register(with: self)
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
// 先更新允许的方向掩码
|
||||
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
|
||||
}
|
||||
|
||||
// 关键:把当前的掩码提供给系统
|
||||
override func application(_ application: UIApplication,
|
||||
supportedInterfaceOrientationsFor window: UIWindow?)
|
||||
-> UIInterfaceOrientationMask {
|
||||
return AppDelegate.orientationMask
|
||||
// 再请求实际旋转
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 2) permissions channel (新增): 用于 iOS 原生请求相机访问权限
|
||||
let permissionChannel = FlutterMethodChannel(name: "qhd_prevention/permissions",
|
||||
binaryMessenger: controller.binaryMessenger)
|
||||
permissionChannel.setMethodCallHandler { (call: FlutterMethodCall, result: @escaping FlutterResult) in
|
||||
if call.method == "requestCameraAccess" {
|
||||
// AVCaptureDevice.requestAccess 会在首次调用时触发系统权限弹窗
|
||||
AVCaptureDevice.requestAccess(for: .video) { granted in
|
||||
DispatchQueue.main.async {
|
||||
result(granted)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result(FlutterMethodNotImplemented)
|
||||
}
|
||||
}
|
||||
|
||||
GeneratedPluginRegistrant.register(with: self)
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
|
||||
// 关键:把当前的掩码提供给系统
|
||||
override func application(_ application: UIApplication,
|
||||
supportedInterfaceOrientationsFor window: UIWindow?)
|
||||
-> UIInterfaceOrientationMask {
|
||||
return AppDelegate.orientationMask
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,98 +1,98 @@
|
|||
<?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">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>智守安全</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>智守安全</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>baidumap</string>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NFCReaderUsageDescription</key>
|
||||
<string>需要NFC权限来读取和写入标签</string>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>智守安全</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>智守安全</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>baidumap</string>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NFCReaderUsageDescription</key>
|
||||
<string>需要NFC权限来读取和写入标签</string>
|
||||
<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>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>
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
|
@ -15,11 +17,73 @@ import 'package:photo_manager/photo_manager.dart';
|
|||
import 'package:path/path.dart' as p;
|
||||
import 'ItemWidgetFactory.dart';
|
||||
|
||||
const String kAcceptVideoSectionKey = 'accept_video';
|
||||
|
||||
/// ---------------------- 全局 MediaBus (轻量事件总线) ----------------------
|
||||
class MediaEvent {
|
||||
final String key;
|
||||
final MediaEventType type;
|
||||
final List<String>? paths;
|
||||
|
||||
MediaEvent._(this.key, this.type, [this.paths]);
|
||||
|
||||
factory MediaEvent.clear(String key) => MediaEvent._(key, MediaEventType.clear);
|
||||
|
||||
factory MediaEvent.set(String key, List<String> paths) =>
|
||||
MediaEvent._(key, MediaEventType.set, List<String>.from(paths));
|
||||
}
|
||||
|
||||
enum MediaEventType { clear, set }
|
||||
|
||||
class MediaBus {
|
||||
MediaBus._internal();
|
||||
static final MediaBus _instance = MediaBus._internal();
|
||||
factory MediaBus() => _instance;
|
||||
|
||||
final StreamController<MediaEvent> _ctrl = StreamController<MediaEvent>.broadcast();
|
||||
|
||||
Stream<MediaEvent> get stream => _ctrl.stream;
|
||||
|
||||
void emit(MediaEvent ev) {
|
||||
if (!_ctrl.isClosed) _ctrl.add(ev);
|
||||
}
|
||||
|
||||
Future<void> dispose() async {
|
||||
await _ctrl.close();
|
||||
}
|
||||
}
|
||||
/// ---------------------- /MediaBus ----------------------
|
||||
|
||||
|
||||
/// 媒体选择类型
|
||||
enum MediaType { image, video }
|
||||
|
||||
/// 纵向滚动四列的媒体添加组件,支持拍摄、全屏相册多选,以及初始地址列表展示
|
||||
/// 新增 isEdit 属性控制编辑状态
|
||||
/// ---------- 辅助函数(文件顶部复用) ----------
|
||||
bool _isNetworkPath(String? p) {
|
||||
if (p == null) return false;
|
||||
final s = p.trim().toLowerCase();
|
||||
return s.startsWith('http://') || s.startsWith('https://');
|
||||
}
|
||||
|
||||
/// 把路径列表转换为存在的本地 File 列表(过滤掉网络路径与空路径与不存在的本地文件)
|
||||
List<File> _localFilesFromPaths(List<String>? paths) {
|
||||
if (paths == null) return <File>[];
|
||||
return paths
|
||||
.map((e) => (e ?? '').toString().trim())
|
||||
.where((s) => s.isNotEmpty && !_isNetworkPath(s))
|
||||
.where((s) => File(s).existsSync()) // 只回调真实存在的本地文件
|
||||
.map((s) => File(s))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// 规范化路径列表:trim + 过滤空字符串
|
||||
List<String> _normalizePaths(List<String>? src) {
|
||||
if (src == null) return <String>[];
|
||||
return src.map((e) => (e ?? '').toString().trim()).where((s) => s.isNotEmpty).toList();
|
||||
}
|
||||
|
||||
/// ---------- MediaPickerRow ----------
|
||||
/// ---------- MediaPickerRow ----------
|
||||
class MediaPickerRow extends StatefulWidget {
|
||||
final int maxCount;
|
||||
final MediaType mediaType;
|
||||
|
|
@ -27,9 +91,11 @@ class MediaPickerRow extends StatefulWidget {
|
|||
final ValueChanged<List<File>> onChanged;
|
||||
final ValueChanged<String>? onMediaAdded;
|
||||
final ValueChanged<String>? onMediaRemoved;
|
||||
final ValueChanged<String>? onMediaTapped; // 新增:媒体点击回调
|
||||
final bool isEdit; // 新增:控制编辑状态
|
||||
final bool isCamera; // 新增:只能拍照
|
||||
final ValueChanged<String>? onMediaTapped;
|
||||
final bool isEdit;
|
||||
final bool isCamera;
|
||||
/// 默认 false —— 仅在 initState 时读取 initialMediaPaths
|
||||
final bool followInitialUpdates;
|
||||
|
||||
const MediaPickerRow({
|
||||
Key? key,
|
||||
|
|
@ -39,9 +105,10 @@ class MediaPickerRow extends StatefulWidget {
|
|||
required this.onChanged,
|
||||
this.onMediaAdded,
|
||||
this.onMediaRemoved,
|
||||
this.onMediaTapped, // 新增
|
||||
this.isEdit = true, // 默认可编辑
|
||||
this.onMediaTapped,
|
||||
this.isEdit = true,
|
||||
this.isCamera = false,
|
||||
this.followInitialUpdates = false, // 默认 false
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
|
|
@ -51,25 +118,41 @@ class MediaPickerRow extends StatefulWidget {
|
|||
class _MediaPickerGridState extends State<MediaPickerRow> {
|
||||
final ImagePicker _picker = ImagePicker();
|
||||
late List<String> _mediaPaths;
|
||||
bool _isProcessing = false; // 转码或处理时显示 loading
|
||||
bool _isProcessing = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_mediaPaths =
|
||||
widget.initialMediaPaths != null
|
||||
? widget.initialMediaPaths!.take(widget.maxCount).toList()
|
||||
: [];
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
widget.onChanged(
|
||||
_mediaPaths
|
||||
.map((p) => p.startsWith('http') ? File('') : File(p))
|
||||
.toList(),
|
||||
);
|
||||
});
|
||||
// 初始化内部路径(保留网络路径与本地路径)
|
||||
_mediaPaths = _normalizePaths(widget.initialMediaPaths).take(widget.maxCount).toList();
|
||||
|
||||
// 仅在存在本地真实文件时才把 File 列表回调给外部(避免父组件用这个回调覆盖只有网络路径的数据)
|
||||
final initialLocalFiles = _localFilesFromPaths(_mediaPaths);
|
||||
if (initialLocalFiles.isNotEmpty) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
widget.onChanged(initialLocalFiles);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant MediaPickerRow oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.followInitialUpdates) {
|
||||
final oldList = _normalizePaths(oldWidget.initialMediaPaths);
|
||||
final newList = _normalizePaths(widget.initialMediaPaths);
|
||||
|
||||
if (!listEquals(oldList, newList)) {
|
||||
_mediaPaths = newList.take(widget.maxCount).toList();
|
||||
if (mounted) setState(() {});
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
widget.onChanged(_localFilesFromPaths(_mediaPaths));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 公共:当得到本地媒体路径时(可能是 mov/avi 等),需要在这里统一处理(转码、入队、回调)
|
||||
Future<void> _handlePickedPath(String path) async {
|
||||
if (!mounted) return;
|
||||
if (path.isEmpty) return;
|
||||
|
|
@ -77,7 +160,6 @@ class _MediaPickerGridState extends State<MediaPickerRow> {
|
|||
try {
|
||||
String finalPath = path;
|
||||
|
||||
// 如果是视频并且不是 mp4,则调用 video_compress 转码
|
||||
if (widget.mediaType == MediaType.video) {
|
||||
final ext = p.extension(path).toLowerCase();
|
||||
if (ext != '.mp4') {
|
||||
|
|
@ -106,10 +188,10 @@ class _MediaPickerGridState extends State<MediaPickerRow> {
|
|||
}
|
||||
}
|
||||
|
||||
// 添加到列表
|
||||
if (_mediaPaths.length < widget.maxCount) {
|
||||
setState(() => _mediaPaths.add(finalPath));
|
||||
widget.onChanged(_mediaPaths.map((p) => File(p)).toList());
|
||||
// 回调仅包含本地真实存在的文件(网络路径不会出现在此回调中)
|
||||
widget.onChanged(_localFilesFromPaths(_mediaPaths));
|
||||
widget.onMediaAdded?.call(finalPath);
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
@ -118,54 +200,41 @@ class _MediaPickerGridState extends State<MediaPickerRow> {
|
|||
}
|
||||
|
||||
Future<void> _showPickerOptions() async {
|
||||
if (!widget.isEdit) return; // 不可编辑时直接返回
|
||||
if (!widget.isEdit) return;
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.white,
|
||||
builder:
|
||||
(_) => SafeArea(
|
||||
child: Wrap(
|
||||
children: [
|
||||
ListTile(
|
||||
titleAlignment: ListTileTitleAlignment.center,
|
||||
leading: Icon(
|
||||
widget.mediaType == MediaType.image
|
||||
? Icons.camera_alt
|
||||
: Icons.videocam,
|
||||
),
|
||||
title: Text(
|
||||
widget.mediaType == MediaType.image ? '拍照' : '拍摄视频',
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
_pickCamera();
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
titleAlignment: ListTileTitleAlignment.center,
|
||||
leading: Icon(
|
||||
widget.mediaType == MediaType.image
|
||||
? Icons.photo_library
|
||||
: Icons.video_library,
|
||||
),
|
||||
title: Text(
|
||||
widget.mediaType == MediaType.image ? '从相册选择' : '从相册选择视频',
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
_pickGallery();
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
titleAlignment: ListTileTitleAlignment.center,
|
||||
leading: const Icon(Icons.close),
|
||||
title: const Text('取消'),
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
builder: (_) => SafeArea(
|
||||
child: Wrap(
|
||||
children: [
|
||||
ListTile(
|
||||
titleAlignment: ListTileTitleAlignment.center,
|
||||
leading: Icon(widget.mediaType == MediaType.image ? Icons.camera_alt : Icons.videocam),
|
||||
title: Text(widget.mediaType == MediaType.image ? '拍照' : '拍摄视频'),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
_pickCamera();
|
||||
},
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
titleAlignment: ListTileTitleAlignment.center,
|
||||
leading: Icon(widget.mediaType == MediaType.image ? Icons.photo_library : Icons.video_library),
|
||||
title: Text(widget.mediaType == MediaType.image ? '从相册选择' : '从相册选择视频'),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
_pickGallery();
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
titleAlignment: ListTileTitleAlignment.center,
|
||||
leading: const Icon(Icons.close),
|
||||
title: const Text('取消'),
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -179,7 +248,7 @@ class _MediaPickerGridState extends State<MediaPickerRow> {
|
|||
if (picked != null) {
|
||||
final path = picked.path;
|
||||
setState(() => _mediaPaths.add(path));
|
||||
widget.onChanged(_mediaPaths.map((p) => File(p)).toList());
|
||||
widget.onChanged(_localFilesFromPaths(_mediaPaths));
|
||||
widget.onMediaAdded?.call(path);
|
||||
}
|
||||
} else {
|
||||
|
|
@ -193,31 +262,26 @@ class _MediaPickerGridState extends State<MediaPickerRow> {
|
|||
}
|
||||
}
|
||||
|
||||
// 修改 _pickGallery 方法
|
||||
Future<void> _pickGallery() async {
|
||||
if (!widget.isEdit || _mediaPaths.length >= widget.maxCount) return;
|
||||
|
||||
try {
|
||||
// iOS: 使用 PhotoManager.requestPermissionExtend() 唤起系统权限弹窗(支持 limited)
|
||||
if (Platform.isIOS) {
|
||||
final permission = await PhotoManager.requestPermissionExtend();
|
||||
debugPrint('iOS photo permission state: $permission');
|
||||
|
||||
if (permission != PermissionState.authorized &&
|
||||
permission != PermissionState.limited) {
|
||||
if (permission != PermissionState.authorized && permission != PermissionState.limited) {
|
||||
if (mounted) {
|
||||
ToastUtil.showNormal(context, '请到设置中开启相册访问权限');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 授权或 limited:继续打开 AssetPicker
|
||||
final remaining = widget.maxCount - _mediaPaths.length;
|
||||
final List<AssetEntity>? assets = await AssetPicker.pickAssets(
|
||||
context,
|
||||
pickerConfig: AssetPickerConfig(
|
||||
requestType:
|
||||
widget.mediaType == MediaType.image ? RequestType.image : RequestType.video,
|
||||
requestType: widget.mediaType == MediaType.image ? RequestType.image : RequestType.video,
|
||||
maxAssets: remaining,
|
||||
gridCount: 4,
|
||||
),
|
||||
|
|
@ -239,11 +303,9 @@ class _MediaPickerGridState extends State<MediaPickerRow> {
|
|||
}
|
||||
}
|
||||
if (mounted) setState(() {});
|
||||
widget.onChanged(_mediaPaths.map((p) => File(p)).toList());
|
||||
widget.onChanged(_localFilesFromPaths(_mediaPaths));
|
||||
}
|
||||
|
||||
} else {
|
||||
// Android: 更可靠地请求权限(适配不同 Android 版本)
|
||||
final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
|
||||
final androidInfo = await deviceInfo.androidInfo;
|
||||
final int sdkInt = androidInfo.version.sdkInt ?? 0;
|
||||
|
|
@ -251,28 +313,21 @@ class _MediaPickerGridState extends State<MediaPickerRow> {
|
|||
PermissionStatus permissionStatus = PermissionStatus.denied;
|
||||
|
||||
if (sdkInt >= 33) {
|
||||
// Android 13+: 使用更细粒度的媒体权限
|
||||
// 如果应用只选图片,request photos;只选视频则 request videos;若两者都可能同时需要,可请求两个。
|
||||
if (widget.mediaType == MediaType.image) {
|
||||
permissionStatus = await Permission.photos.request(); // maps to READ_MEDIA_IMAGES on Android
|
||||
permissionStatus = await Permission.photos.request();
|
||||
} else if (widget.mediaType == MediaType.video) {
|
||||
permissionStatus = await Permission.videos.request(); // maps to READ_MEDIA_VIDEO
|
||||
permissionStatus = await Permission.videos.request();
|
||||
} else {
|
||||
// 两者皆可(同时请求会合并成单个系统对话)
|
||||
final statuses = await [Permission.photos, Permission.videos].request();
|
||||
permissionStatus = statuses[Permission.photos] ?? statuses[Permission.videos] ?? PermissionStatus.denied;
|
||||
}
|
||||
} else if (sdkInt >= 30) {
|
||||
// Android 11/12: 通常仍然使用 READ_EXTERNAL_STORAGE;若需要“全部文件访问”,要 request manageExternalStorage(慎用)
|
||||
permissionStatus = await Permission.storage.request();
|
||||
} else {
|
||||
// Android 10 及以下
|
||||
permissionStatus = await Permission.storage.request();
|
||||
}
|
||||
|
||||
// 处理结果
|
||||
if (permissionStatus.isGranted) {
|
||||
// 有权限:继续打开 AssetPicker
|
||||
final remaining = widget.maxCount - _mediaPaths.length;
|
||||
final List<AssetEntity>? assets = await AssetPicker.pickAssets(
|
||||
context,
|
||||
|
|
@ -299,10 +354,9 @@ class _MediaPickerGridState extends State<MediaPickerRow> {
|
|||
}
|
||||
}
|
||||
if (mounted) setState(() {});
|
||||
widget.onChanged(_mediaPaths.map((p) => File(p)).toList());
|
||||
widget.onChanged(_localFilesFromPaths(_mediaPaths));
|
||||
}
|
||||
} else if (permissionStatus.isPermanentlyDenied) {
|
||||
// 用户点了“不再询问”或系统拒绝弹窗,跳转设置页并提示
|
||||
if (mounted) {
|
||||
ToastUtil.showNormal(context, '请到设置中开启相册访问权限');
|
||||
}
|
||||
|
|
@ -318,25 +372,19 @@ class _MediaPickerGridState extends State<MediaPickerRow> {
|
|||
} catch (e, st) {
|
||||
debugPrint('相册选择失败: $e\n$st');
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('相册选择失败')),
|
||||
);
|
||||
ToastUtil.showNormal(context, '相册选择失败');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 修改拍照方法,使用 permission_handler 请求相机权限
|
||||
Future<void> _cameraAction() async {
|
||||
if (!widget.isEdit || _mediaPaths.length >= widget.maxCount) return;
|
||||
|
||||
// 请求相机权限
|
||||
final PermissionStatus status = await Permission.camera.request();
|
||||
|
||||
if (status != PermissionStatus.granted) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('相机权限被拒绝')));
|
||||
ToastUtil.showNormal(context, '相机权限被拒绝');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -346,11 +394,10 @@ class _MediaPickerGridState extends State<MediaPickerRow> {
|
|||
if (picked != null) {
|
||||
final path = picked.path;
|
||||
setState(() => _mediaPaths.add(path));
|
||||
widget.onChanged(_mediaPaths.map((p) => File(p)).toList());
|
||||
widget.onChanged(_localFilesFromPaths(_mediaPaths));
|
||||
widget.onMediaAdded?.call(path);
|
||||
}
|
||||
} else {
|
||||
// video from camera
|
||||
XFile? picked = await _picker.pickVideo(source: ImageSource.camera);
|
||||
if (picked != null) {
|
||||
await _handlePickedPath(picked.path);
|
||||
|
|
@ -358,11 +405,8 @@ class _MediaPickerGridState extends State<MediaPickerRow> {
|
|||
}
|
||||
} catch (e) {
|
||||
debugPrint('拍摄失败: $e');
|
||||
// 友好提示
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('拍摄失败,请检查相机权限或设备状态')));
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('拍摄失败,请检查相机权限或设备状态')));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -371,16 +415,22 @@ class _MediaPickerGridState extends State<MediaPickerRow> {
|
|||
final ok = await CustomAlertDialog.showConfirm(
|
||||
context,
|
||||
title: '温馨提示',
|
||||
content:
|
||||
widget.mediaType == MediaType.image ? '确定要删除这张图片吗?' : '确定要删除这个视频吗?',
|
||||
content: widget.mediaType == MediaType.image ? '确定要删除这张图片吗?' : '确定要删除这个视频吗?',
|
||||
cancelText: '取消',
|
||||
);
|
||||
if (ok) {
|
||||
final removed = _mediaPaths[index];
|
||||
setState(() => _mediaPaths.removeAt(index));
|
||||
widget.onChanged(_mediaPaths.map((p) => File(p)).toList());
|
||||
widget.onMediaRemoved?.call(removed);
|
||||
}
|
||||
if (!ok) return;
|
||||
|
||||
final removed = _mediaPaths[index];
|
||||
final wasNetwork = _isNetworkPath(removed);
|
||||
|
||||
setState(() => _mediaPaths.removeAt(index));
|
||||
|
||||
// 始终通知 onMediaRemoved(用于父端业务逻辑)
|
||||
widget.onMediaRemoved?.call(removed);
|
||||
|
||||
// 只有当本地文件集合发生变化时才触发 onChanged(避免因为删除网络路径导致父端把列表置空)
|
||||
final localFiles = _localFilesFromPaths(_mediaPaths);
|
||||
widget.onChanged(localFiles);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -401,72 +451,71 @@ class _MediaPickerGridState extends State<MediaPickerRow> {
|
|||
),
|
||||
itemCount: itemCount,
|
||||
itemBuilder: (context, index) {
|
||||
// 显示媒体项
|
||||
if (index < _mediaPaths.length) {
|
||||
final path = _mediaPaths[index];
|
||||
final isNetwork = path.startsWith('http');
|
||||
final raw = (_mediaPaths[index] ?? '').toString().trim();
|
||||
final isNetwork = _isNetworkPath(raw);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => widget.onMediaTapped?.call(path),
|
||||
onTap: () => widget.onMediaTapped?.call(raw),
|
||||
child: Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
child: SizedBox.expand(
|
||||
// 让内容强制填满格子
|
||||
child:
|
||||
widget.mediaType == MediaType.image
|
||||
? (isNetwork
|
||||
? Image.network(
|
||||
path,
|
||||
fit: BoxFit.cover, // 铺满格子并裁剪
|
||||
)
|
||||
: Image.file(
|
||||
File(path),
|
||||
fit: BoxFit.cover, // 铺满格子并裁剪
|
||||
))
|
||||
: Container(
|
||||
color: Colors.black12,
|
||||
child: const Center(
|
||||
child: Icon(
|
||||
Icons.videocam,
|
||||
color: Colors.white70,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: raw.isEmpty
|
||||
? Container(
|
||||
color: Colors.grey.shade200,
|
||||
child: const Center(child: Icon(Icons.broken_image, size: 28, color: Colors.grey)),
|
||||
)
|
||||
: (widget.mediaType == MediaType.image
|
||||
? (isNetwork
|
||||
? Image.network(
|
||||
raw,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) => Container(
|
||||
color: Colors.grey.shade200,
|
||||
child: const Center(child: Icon(Icons.broken_image)),
|
||||
),
|
||||
)
|
||||
: (File(raw).existsSync()
|
||||
? Image.file(
|
||||
File(raw),
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) => Container(
|
||||
color: Colors.grey.shade200,
|
||||
child: const Center(child: Icon(Icons.broken_image)),
|
||||
),
|
||||
)
|
||||
: Container(
|
||||
color: Colors.grey.shade200,
|
||||
child: const Center(child: Icon(Icons.broken_image)),
|
||||
)))
|
||||
: Container(
|
||||
color: Colors.black12,
|
||||
child: const Center(
|
||||
child: Icon(Icons.videocam, color: Colors.white70),
|
||||
),
|
||||
)),
|
||||
),
|
||||
),
|
||||
|
||||
// 只在可编辑状态下显示删除按钮
|
||||
if (widget.isEdit)
|
||||
Positioned(
|
||||
top: -15,
|
||||
right: -15,
|
||||
child: IconButton(
|
||||
icon: const Icon(
|
||||
Icons.cancel,
|
||||
size: 20,
|
||||
color: Colors.red,
|
||||
),
|
||||
icon: const Icon(Icons.cancel, size: 20, color: Colors.red),
|
||||
onPressed: () => _removeMedia(index),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
// 显示添加按钮
|
||||
else if (showAddButton) {
|
||||
} else if (showAddButton) {
|
||||
return GestureDetector(
|
||||
onTap: widget.isCamera ? _cameraAction : _showPickerOptions,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.black12),
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
child: const Center(
|
||||
child: Icon(Icons.camera_alt, color: Colors.black26),
|
||||
),
|
||||
decoration: BoxDecoration(border: Border.all(color: Colors.black12), borderRadius: BorderRadius.circular(5)),
|
||||
child: const Center(child: Icon(Icons.camera_alt, color: Colors.black26)),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
|
|
@ -474,8 +523,6 @@ class _MediaPickerGridState extends State<MediaPickerRow> {
|
|||
}
|
||||
},
|
||||
),
|
||||
|
||||
// 转码/处理 loading 遮罩
|
||||
if (_isProcessing)
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
|
|
@ -488,8 +535,8 @@ class _MediaPickerGridState extends State<MediaPickerRow> {
|
|||
}
|
||||
}
|
||||
|
||||
/// 照片上传区域组件,使用纵向四列Grid展示
|
||||
/// 新增 isEdit 属性控制编辑状态
|
||||
|
||||
/// ---------- RepairedPhotoSection ----------
|
||||
class RepairedPhotoSection extends StatefulWidget {
|
||||
final int maxCount;
|
||||
final MediaType mediaType;
|
||||
|
|
@ -498,14 +545,17 @@ class RepairedPhotoSection extends StatefulWidget {
|
|||
final ValueChanged<List<File>> onChanged;
|
||||
final ValueChanged<String>? onMediaAdded;
|
||||
final ValueChanged<String>? onMediaRemoved;
|
||||
final ValueChanged<String>? onMediaTapped; // 新增:媒体点击回调
|
||||
final ValueChanged<String>? onMediaTapped;
|
||||
final VoidCallback onAiIdentify;
|
||||
final bool isShowAI;
|
||||
final double horizontalPadding;
|
||||
final bool isRequired;
|
||||
final bool isShowNum;
|
||||
final bool isEdit; // 新增:控制编辑状态
|
||||
final bool isCamera; // 新增:只能拍照
|
||||
final bool isEdit;
|
||||
final bool isCamera;
|
||||
final String sectionKey;
|
||||
final bool followInitialUpdates;
|
||||
|
||||
|
||||
const RepairedPhotoSection({
|
||||
Key? key,
|
||||
|
|
@ -519,11 +569,13 @@ class RepairedPhotoSection extends StatefulWidget {
|
|||
this.horizontalPadding = 5,
|
||||
this.onMediaAdded,
|
||||
this.onMediaRemoved,
|
||||
this.onMediaTapped, // 新增
|
||||
this.onMediaTapped,
|
||||
this.isRequired = false,
|
||||
this.isShowNum = true,
|
||||
this.isEdit = true, // 默认可编辑
|
||||
this.isEdit = true,
|
||||
this.isCamera = false,
|
||||
this.followInitialUpdates = false, // 默认 false
|
||||
this.sectionKey = kAcceptVideoSectionKey,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
|
|
@ -532,17 +584,81 @@ class RepairedPhotoSection extends StatefulWidget {
|
|||
|
||||
class _RepairedPhotoSectionState extends State<RepairedPhotoSection> {
|
||||
late List<String> _mediaPaths;
|
||||
StreamSubscription<MediaEvent>? _sub;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_mediaPaths =
|
||||
widget.initialMediaPaths?.take(widget.maxCount).toList() ?? [];
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
widget.onChanged(_mediaPaths.map((p) => File(p)).toList());
|
||||
_mediaPaths = _normalizePaths(widget.initialMediaPaths).take(widget.maxCount).toList();
|
||||
|
||||
// 订阅 MediaBus(如果需要)
|
||||
_sub = MediaBus().stream.listen((ev) {
|
||||
if (ev.key != widget.sectionKey) return;
|
||||
|
||||
if (ev.type == MediaEventType.clear) {
|
||||
if (_mediaPaths.isNotEmpty) {
|
||||
setState(() {
|
||||
_mediaPaths = <String>[];
|
||||
});
|
||||
widget.onChanged(_localFilesFromPaths(_mediaPaths));
|
||||
}
|
||||
} else if (ev.type == MediaEventType.set && ev.paths != null) {
|
||||
final newList = _normalizePaths(ev.paths).take(widget.maxCount).toList();
|
||||
if (!listEquals(newList, _mediaPaths)) {
|
||||
setState(() {
|
||||
_mediaPaths = newList;
|
||||
});
|
||||
widget.onChanged(_localFilesFromPaths(_mediaPaths));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant RepairedPhotoSection oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.followInitialUpdates) {
|
||||
final oldList = _normalizePaths(oldWidget.initialMediaPaths);
|
||||
final newList = _normalizePaths(widget.initialMediaPaths);
|
||||
|
||||
if (!listEquals(oldList, newList)) {
|
||||
setState(() {
|
||||
_mediaPaths = newList.take(widget.maxCount).toList();
|
||||
});
|
||||
widget.onChanged(_localFilesFromPaths(_mediaPaths));
|
||||
}
|
||||
}
|
||||
if (oldWidget.sectionKey != widget.sectionKey) {
|
||||
_sub?.cancel();
|
||||
_sub = MediaBus().stream.listen((ev) {
|
||||
if (ev.key != widget.sectionKey) return;
|
||||
|
||||
if (ev.type == MediaEventType.clear) {
|
||||
if (_mediaPaths.isNotEmpty) {
|
||||
setState(() {
|
||||
_mediaPaths = <String>[];
|
||||
});
|
||||
widget.onChanged(_localFilesFromPaths(_mediaPaths));
|
||||
}
|
||||
} else if (ev.type == MediaEventType.set && ev.paths != null) {
|
||||
final newList = _normalizePaths(ev.paths).take(widget.maxCount).toList();
|
||||
if (!listEquals(newList, _mediaPaths)) {
|
||||
setState(() {
|
||||
_mediaPaths = newList;
|
||||
});
|
||||
widget.onChanged(_localFilesFromPaths(_mediaPaths));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_sub?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
|
|
@ -555,10 +671,7 @@ class _RepairedPhotoSectionState extends State<RepairedPhotoSection> {
|
|||
padding: EdgeInsets.symmetric(horizontal: widget.horizontalPadding),
|
||||
child: ListItemFactory.createRowSpaceBetweenItem(
|
||||
leftText: widget.title,
|
||||
rightText:
|
||||
widget.isShowNum
|
||||
? '${_mediaPaths.length}/${widget.maxCount}'
|
||||
: '',
|
||||
rightText: widget.isShowNum ? '${_mediaPaths.length}/${widget.maxCount}' : '',
|
||||
isRequired: widget.isRequired,
|
||||
),
|
||||
),
|
||||
|
|
@ -582,24 +695,21 @@ class _RepairedPhotoSectionState extends State<RepairedPhotoSection> {
|
|||
onMediaTapped: (filePath) {
|
||||
if (widget.mediaType == MediaType.image) {
|
||||
presentOpaque(SingleImageViewer(imageUrl: filePath), context);
|
||||
}else{
|
||||
} else {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierColor: Colors.black54,
|
||||
builder: (_) => VideoPlayerPopup(videoUrl:filePath),
|
||||
builder: (_) => VideoPlayerPopup(videoUrl: filePath),
|
||||
);
|
||||
}
|
||||
},
|
||||
// 传递点击回调
|
||||
isEdit: widget.isEdit, // 传递编辑状态
|
||||
isEdit: widget.isEdit,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (widget.isShowAI && widget.isEdit) // 只在可编辑状态下显示AI按钮
|
||||
if (widget.isShowAI && widget.isEdit)
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: widget.horizontalPadding,
|
||||
),
|
||||
padding: EdgeInsets.symmetric(horizontal: widget.horizontalPadding),
|
||||
child: GestureDetector(
|
||||
onTap: widget.onAiIdentify,
|
||||
child: Container(
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ class _RemoteFilePageState extends State<RemoteFilePage> {
|
|||
_secondsRemaining = widget.countdownSeconds;
|
||||
_startCountdown();
|
||||
_downloadAndLoad();
|
||||
LoadingDialogHelper.hide();
|
||||
}
|
||||
|
||||
Future<void> _downloadAndLoad() async {
|
||||
|
|
@ -135,7 +136,7 @@ class _RemoteFilePageState extends State<RemoteFilePage> {
|
|||
onPressed: isButtonEnabled
|
||||
? () {
|
||||
// TODO: 完成回调
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context, true);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@ class ApiService {
|
|||
// static const String baseFacePath = "https://qaaqwh.qhdsafety.com/whb_stu_face";
|
||||
static const String baseFacePath = "http://192.168.20.240:8500/whb_stu_face/";
|
||||
/// 登录及其他管理后台接口
|
||||
// static const String basePath = "http://192.168.20.240:8500/integrated_whb";//测试服务器
|
||||
// static const String basePath = "https://qaaqwh.qhdsafety.com/integrated_whb";
|
||||
// static const String basePath = "http://192.168.20.240:8500/integrated_whb";//测试服务器
|
||||
// static const String basePath = "http://192.168.20.240:8500/integrated_whb";
|
||||
// static const String basePath = "http://192.168.0.25:28199";//王轩服务器
|
||||
static const String basePath = "http://192.168.0.45:28199";//长久服务器
|
||||
|
|
@ -958,7 +958,8 @@ U6Hzm1ninpWeE+awIDAQAB
|
|||
static Future<Map<String, dynamic>> getWorkAreaList() {
|
||||
return HttpManager().request(
|
||||
basePath,
|
||||
'/app/electronicFence/listTree',
|
||||
// '/app/electronicFence/listTree',
|
||||
'/app/electronicFence/simple-list',
|
||||
method: Method.post,
|
||||
data: {"CORPINFO_ID": SessionService.instance.corpinfoId},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -155,7 +155,6 @@ extension HttpManagerUpload on HttpManager {
|
|||
contentType: 'multipart/form-data',
|
||||
),
|
||||
);
|
||||
|
||||
final json = resp.data is Map<String, dynamic>
|
||||
? resp.data as Map<String, dynamic>
|
||||
: <String, dynamic>{};
|
||||
|
|
@ -166,6 +165,7 @@ extension HttpManagerUpload on HttpManager {
|
|||
if (e.error is ApiException) {
|
||||
throw e.error as ApiException;
|
||||
}
|
||||
return {'network_error': e.message ?? e.toString()};
|
||||
throw ApiException('network_error', e.message ?? e.toString());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:flutter/material.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_add_page.dart';
|
||||
import 'package:qhd_prevention/pages/home/NFC/home_nfc_detail_page.dart';
|
||||
|
|
@ -241,9 +242,7 @@ class _HomeNfcListPageState extends State<HomeNfcListPage>
|
|||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('获取巡检记录失败:$e')));
|
||||
ToastUtil.showNormal(context, '获取巡检记录失败:$e');
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
|
|
|
|||
|
|
@ -281,6 +281,7 @@ class _SafecheckAssignmentDetailPageState
|
|||
isRequired: false,
|
||||
isClean: false,
|
||||
onTapClean: () {
|
||||
ToastUtil.showNormal(context, '已清除');
|
||||
setState(() {});
|
||||
},
|
||||
text: form['RECTIFICATIONDEPTNAME'] ?? '请选择',
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'package:qhd_prevention/customWidget/custom_alert_dialog.dart';
|
|||
import 'package:qhd_prevention/customWidget/custom_button.dart';
|
||||
import 'package:qhd_prevention/customWidget/dotted_border_box.dart';
|
||||
import 'package:qhd_prevention/customWidget/single_image_viewer.dart';
|
||||
import 'package:qhd_prevention/customWidget/toast_util.dart';
|
||||
import 'package:qhd_prevention/http/ApiService.dart';
|
||||
import 'package:qhd_prevention/pages/KeyProjects/SafeCheck/custom/MultiTextFieldWithTitle.dart';
|
||||
import 'package:qhd_prevention/pages/KeyProjects/SafeCheck/custom/safeCheck_table.dart';
|
||||
|
|
@ -59,6 +60,7 @@ class _SafeCheckFormViewState extends State<SafeCheckFormView> {
|
|||
label: '${index + 1}.检查人员单位:',
|
||||
isEditable: false,
|
||||
onTapClean: () {
|
||||
ToastUtil.showNormal(context, '已清除');
|
||||
setState(() {});
|
||||
},
|
||||
text: item['INSPECTION_DEPARTMENT_NAME'] ?? '请选择',
|
||||
|
|
|
|||
|
|
@ -700,6 +700,7 @@ class _SafecheckStartDetailState extends State<SafecheckStartDetail> {
|
|||
label: '${index + 1}.检查人员单位:',
|
||||
isEditable: index == 0 ? false : _isEdit,
|
||||
onTapClean: () {
|
||||
ToastUtil.showNormal(context, '已清除');
|
||||
setState(() {});
|
||||
},
|
||||
text: item['INSPECTION_DEPARTMENT_NAME'] ?? '请选择',
|
||||
|
|
|
|||
|
|
@ -138,9 +138,7 @@ class _SafecheckStartListPageState extends State<SafecheckStartListPage> {
|
|||
});
|
||||
} catch (e) {
|
||||
print('Error fetching flow data: $e');
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('获取流程图失败: $e')));
|
||||
ToastUtil.showNormal(context, '获取流程图失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -334,10 +334,6 @@ class HomePageState extends State<HomePage> {
|
|||
final data = await ApiService.getWork();
|
||||
final hidCount = data['hidCount'] as Map<String, dynamic>? ?? {};
|
||||
|
||||
// // 告知BadgeManager去更新“安全巡检”和“八项作业”角标
|
||||
// BadgeManager().updateEnvInspectCount();
|
||||
// BadgeManager().updateEightWorkCount();
|
||||
|
||||
// 拉取其他数据
|
||||
final results = await Future.wait([
|
||||
ApiService.getUserData(),
|
||||
|
|
|
|||
|
|
@ -109,6 +109,6 @@ class _LowPagePageState extends State<LowPage> {
|
|||
}
|
||||
|
||||
void _showMessage(String msg) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
|
||||
ToastUtil.showNormal(context, msg);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -334,7 +334,7 @@ class _RiskControlPageState extends State<RiskControlPage> {
|
|||
}
|
||||
|
||||
void _showMessage(String msg) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
|
||||
ToastUtil.showNormal(context, msg);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -412,6 +412,6 @@ class _RiskDetailPageState extends State<RiskDetailPage> {
|
|||
}
|
||||
|
||||
void _showMessage(String msg) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
|
||||
ToastUtil.showNormal(context, msg);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,20 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:permission_handler/permission_handler.dart' as ph;
|
||||
import 'package:qhd_prevention/customWidget/custom_alert_dialog.dart';
|
||||
import 'package:qhd_prevention/customWidget/custom_button.dart';
|
||||
import 'package:qhd_prevention/customWidget/toast_util.dart';
|
||||
import 'package:qhd_prevention/http/ApiService.dart';
|
||||
import 'package:qhd_prevention/pages/my_appbar.dart';
|
||||
import 'package:qhd_prevention/tools/tools.dart';
|
||||
import '../../../http/ApiService.dart';
|
||||
|
||||
// 在类最上面(State 内)加入一个 channel 常量
|
||||
const MethodChannel _platformChan = MethodChannel('qhd_prevention/permissions');
|
||||
|
||||
/// 人脸识别模式
|
||||
enum FaceMode { auto, manual }
|
||||
|
|
@ -25,92 +33,290 @@ class FaceRecognitionPage extends StatefulWidget {
|
|||
_FaceRecognitionPageState createState() => _FaceRecognitionPageState();
|
||||
}
|
||||
|
||||
class _FaceRecognitionPageState extends State<FaceRecognitionPage> {
|
||||
class _FaceRecognitionPageState extends State<FaceRecognitionPage>
|
||||
with WidgetsBindingObserver {
|
||||
CameraController? _cameraController;
|
||||
Timer? _timer;
|
||||
int _attempts = 0;
|
||||
|
||||
static const int _maxAttempts = 8;
|
||||
static const Duration _interval = Duration(seconds: 2);
|
||||
late String _errMsg = '';
|
||||
String _errMsg = '';
|
||||
|
||||
bool get _isManualMode => widget.mode == FaceMode.manual;
|
||||
|
||||
bool _isInitializing = false;
|
||||
bool _isTaking = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initCamera();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
// 延迟到首帧渲染后再请求权限并初始化相机,避免 iOS 在还未准备好时错过系统弹窗
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_initCameraWithPermission();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_timer?.cancel();
|
||||
_cameraController?.dispose();
|
||||
try {
|
||||
_cameraController?.dispose();
|
||||
} catch (_) {}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _initCamera() async {
|
||||
final cams = await availableCameras();
|
||||
final front = cams.firstWhere((c) => c.lensDirection == CameraLensDirection.front);
|
||||
_cameraController = CameraController(front, ResolutionPreset.medium, enableAudio: false);
|
||||
await _cameraController!.initialize();
|
||||
if (!mounted) return;
|
||||
setState(() {});
|
||||
|
||||
if (!_isManualMode) {
|
||||
_timer = Timer.periodic(_interval, (_) => _captureAndUpload());
|
||||
// 生命周期:后台/前台切换时处理相机释放/恢复
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (_cameraController == null || !_cameraController!.value.isInitialized) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/// 显示加载弹窗
|
||||
void _showLoading() {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => const Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
/// 隐藏弹窗
|
||||
void _hideLoading() {
|
||||
if (Navigator.canPop(context)) Navigator.pop(context);
|
||||
}
|
||||
/// 定时上传
|
||||
Future<void> _captureAndUpload() async {
|
||||
if (!_isManualMode) {
|
||||
if (_attempts >= _maxAttempts) return _onTimeout();
|
||||
_attempts++;
|
||||
if (state == AppLifecycleState.inactive) {
|
||||
// 可选:释放相机,节省资源
|
||||
try {
|
||||
final pic = await _cameraController!.takePicture();
|
||||
final res = await ApiService.getUserFace(pic.path, widget.studentId);
|
||||
if (res['result'] == 'success') {
|
||||
_onSuccess();
|
||||
}else{
|
||||
setState(() {
|
||||
_errMsg = res['msg'] ?? '';
|
||||
});
|
||||
}
|
||||
} catch (_) {
|
||||
|
||||
_cameraController?.dispose();
|
||||
} catch (_) {}
|
||||
_cameraController = null;
|
||||
} else if (state == AppLifecycleState.resumed) {
|
||||
// 恢复时重新初始化相机(并会再次请求权限)
|
||||
if (!_isInitializing) {
|
||||
_initCameraWithPermission();
|
||||
}
|
||||
}
|
||||
}
|
||||
/// 手动上传验证
|
||||
Future<void> _captureAndReload() async {
|
||||
_showLoading();
|
||||
|
||||
/// 请求 camera 权限(iOS:优先走原生 AVCaptureDevice.requestAccess;Android:使用 permission_handler)
|
||||
Future<bool> _requestCameraPermission() async {
|
||||
try {
|
||||
final pic = await _cameraController!.takePicture();
|
||||
// iOS:使用原生 API 强制唤起系统授权弹窗
|
||||
if (Platform.isIOS) {
|
||||
try {
|
||||
final dynamic res = await _platformChan.invokeMethod('requestCameraAccess');
|
||||
// 原生返回 true/false
|
||||
if (res == true) {
|
||||
debugPrint('[FaceRecognition] iOS native camera access granted');
|
||||
return true;
|
||||
} else {
|
||||
debugPrint('[FaceRecognition] iOS native camera access denied');
|
||||
// 如果被拒绝(非永久还是永久都返回 false),再根据 permission_handler 的状态做后续提示/引导
|
||||
final status = await ph.Permission.camera.status;
|
||||
if (status.isPermanentlyDenied) {
|
||||
await _showPermissionDialog(permanent: true);
|
||||
} else {
|
||||
await _showPermissionDialog(permanent: false);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('[FaceRecognition] platform channel error: $e — fallback to permission_handler');
|
||||
// 如果 platform channel 异常,回退到 permission_handler
|
||||
}
|
||||
}
|
||||
|
||||
// 非 iOS 或 platform 调用失败时走 permission_handler(保证 Android 正常)
|
||||
final result = await ph.Permission.camera.request();
|
||||
debugPrint('[FaceRecognition] permission_handler camera result: $result');
|
||||
|
||||
if (result.isGranted) return true;
|
||||
if (result.isPermanentlyDenied) {
|
||||
await _showPermissionDialog(permanent: true);
|
||||
return false;
|
||||
}
|
||||
if (result.isDenied) {
|
||||
await _showPermissionDialog(permanent: false);
|
||||
return false;
|
||||
}
|
||||
if (result.isRestricted) {
|
||||
if (mounted) ToastUtil.showNormal(context, '相机权限受限,无法使用本功能');
|
||||
return false;
|
||||
}
|
||||
if (result.isLimited) {
|
||||
if (mounted) ToastUtil.showNormal(context, '相机权限受限(limited)');
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
} catch (e) {
|
||||
debugPrint('[FaceRecognition] permission request error: $e');
|
||||
if (mounted) ToastUtil.showNormal(context, '请求相机权限时出错');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
/// 显示权限被拒/永久拒绝对话框(区分 permanent)
|
||||
Future<void> _showPermissionDialog({required bool permanent}) async {
|
||||
if (!mounted) return;
|
||||
await CustomAlertDialog.showConfirm(
|
||||
context,
|
||||
title: '需要相机权限',
|
||||
content:
|
||||
permanent
|
||||
? '检测到相机权限已被永久拒绝,请到系统设置中打开相机权限以继续使用人脸识别功能。'
|
||||
: '相机权限被拒绝,是否重试或前往设置打开权限?',
|
||||
cancelText: '取消',
|
||||
onConfirm: () async {
|
||||
try {
|
||||
final retry = await ph.Permission.camera.request();
|
||||
debugPrint('[FaceRecognition] retry request result: $retry');
|
||||
if (retry.isGranted) {
|
||||
if (mounted) await _initCamera();
|
||||
} else if (retry.isPermanentlyDenied) {
|
||||
try {
|
||||
await ph.openAppSettings();
|
||||
} catch (e) {
|
||||
debugPrint('[FaceRecognition] openAppSettings error: $e');
|
||||
if (mounted) ToastUtil.showNormal(context, '无法打开设置,请手动前往系统设置授权');
|
||||
}
|
||||
} else {
|
||||
if (mounted) ToastUtil.showNormal(context, '相机权限未授予');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[FaceRecognition] retry request error: $e');
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 初始化:先请求权限,再打开相机
|
||||
Future<void> _initCameraWithPermission() async {
|
||||
if (!mounted) return;
|
||||
final ok = await _requestCameraPermission();
|
||||
if (!ok) return;
|
||||
await _initCamera();
|
||||
}
|
||||
|
||||
Future<void> _initCamera() async {
|
||||
if (_isInitializing) return;
|
||||
_isInitializing = true;
|
||||
|
||||
try {
|
||||
final cams = await availableCameras();
|
||||
|
||||
CameraDescription? front;
|
||||
try {
|
||||
front = cams.firstWhere(
|
||||
(c) => c.lensDirection == CameraLensDirection.front,
|
||||
);
|
||||
} catch (_) {
|
||||
if (cams.isNotEmpty) front = cams.first;
|
||||
}
|
||||
|
||||
if (front == null) {
|
||||
if (!mounted) return;
|
||||
ToastUtil.showError(context, '未检测到可用摄像头');
|
||||
_isInitializing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_cameraController = CameraController(
|
||||
front,
|
||||
ResolutionPreset.medium,
|
||||
enableAudio: false,
|
||||
imageFormatGroup: ImageFormatGroup.yuv420,
|
||||
);
|
||||
|
||||
await _cameraController!.initialize();
|
||||
|
||||
// 尽量关闭闪光
|
||||
try {
|
||||
await _cameraController!.setFlashMode(FlashMode.off);
|
||||
} catch (_) {}
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() {});
|
||||
|
||||
// 启动自动拍照定时器(若为自动模式)
|
||||
_timer?.cancel();
|
||||
_attempts = 0;
|
||||
if (!_isManualMode) {
|
||||
_timer = Timer.periodic(_interval, (_) => _captureAndUpload());
|
||||
}
|
||||
} catch (e, st) {
|
||||
debugPrint('[FaceRecognition] init camera error: $e\n$st');
|
||||
if (mounted) {
|
||||
ToastUtil.showError(context, '初始化摄像头失败');
|
||||
}
|
||||
} finally {
|
||||
_isInitializing = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 自动模式:定时拍照并上传
|
||||
Future<void> _captureAndUpload() async {
|
||||
if (_isManualMode) return;
|
||||
if (_cameraController == null || !_cameraController!.value.isInitialized)
|
||||
return;
|
||||
if (_attempts >= _maxAttempts) return _onTimeout();
|
||||
|
||||
if (_isTaking) return;
|
||||
_isTaking = true;
|
||||
_attempts++;
|
||||
|
||||
try {
|
||||
try {
|
||||
await _cameraController!.setFlashMode(FlashMode.off);
|
||||
} catch (_) {}
|
||||
|
||||
final XFile pic = await _cameraController!.takePicture();
|
||||
final res = await ApiService.getUserFace(pic.path, widget.studentId);
|
||||
|
||||
if (res['result'] == 'success') {
|
||||
_onSuccess();
|
||||
} else {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_errMsg = (res['msg'] ?? '').toString();
|
||||
});
|
||||
}
|
||||
} catch (e, st) {
|
||||
debugPrint('[FaceRecognition] capture error: $e\n$st');
|
||||
// 忽略单次异常,等待下一次尝试
|
||||
} finally {
|
||||
_isTaking = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 手动拍照并上传
|
||||
Future<void> _captureAndReload() async {
|
||||
if (_cameraController == null || !_cameraController!.value.isInitialized)
|
||||
return;
|
||||
if (_isTaking) return;
|
||||
_isTaking = true;
|
||||
_showLoading();
|
||||
|
||||
try {
|
||||
// 再次确认 camera 权限(以防用户运行时撤销)
|
||||
final status = await ph.Permission.camera.status;
|
||||
if (!status.isGranted) {
|
||||
final ok = await _requestCameraPermission();
|
||||
if (!ok) {
|
||||
_hideLoading();
|
||||
_isTaking = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await _cameraController!.setFlashMode(FlashMode.off);
|
||||
} catch (_) {}
|
||||
|
||||
final XFile pic = await _cameraController!.takePicture();
|
||||
final res = await ApiService.reloadMyFace(pic.path, widget.studentId);
|
||||
|
||||
_hideLoading();
|
||||
|
||||
if (res['result'] == 'success') {
|
||||
_onSuccess();
|
||||
} else {
|
||||
ToastUtil.showError(context, '验证失败,请重试');
|
||||
|
||||
}
|
||||
} catch (_) {
|
||||
} catch (e, st) {
|
||||
debugPrint('[FaceRecognition] manual capture error: $e\n$st');
|
||||
_hideLoading();
|
||||
_showToast('发生错误,请重试');
|
||||
ToastUtil.showError(context, '发生错误,请重试');
|
||||
ToastUtil.showError(context, '拍照失败,请重试');
|
||||
} finally {
|
||||
_isTaking = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -118,17 +324,37 @@ class _FaceRecognitionPageState extends State<FaceRecognitionPage> {
|
|||
_timer?.cancel();
|
||||
if (widget.mode == FaceMode.manual) {
|
||||
ToastUtil.showSuccess(context, '已更新人脸信息');
|
||||
Future.delayed(const Duration(milliseconds: 800), () => Navigator.of(context).pop(true));
|
||||
Future.delayed(const Duration(milliseconds: 800), () {
|
||||
if (mounted) Navigator.of(context).pop(true);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Future.delayed(const Duration(milliseconds: 800), () => Navigator.of(context).pop(true));
|
||||
Future.delayed(const Duration(milliseconds: 800), () {
|
||||
if (mounted) Navigator.of(context).pop(true);
|
||||
});
|
||||
}
|
||||
|
||||
void _onTimeout() {
|
||||
_timer?.cancel();
|
||||
ToastUtil.showError(context, '人脸超时,请重新识别!');
|
||||
Future.delayed(const Duration(seconds: 3), () => Navigator.of(context).pop(false));
|
||||
Future.delayed(const Duration(seconds: 3), () {
|
||||
if (mounted) Navigator.of(context).pop(false);
|
||||
});
|
||||
}
|
||||
|
||||
void _showLoading() {
|
||||
if (!mounted) return;
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => const Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
void _hideLoading() {
|
||||
if (!mounted) return;
|
||||
if (Navigator.canPop(context)) Navigator.pop(context);
|
||||
}
|
||||
|
||||
void _showToast(String msg) {
|
||||
|
|
@ -143,13 +369,14 @@ class _FaceRecognitionPageState extends State<FaceRecognitionPage> {
|
|||
body: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
final previewSize = _cameraController!.value.previewSize!;
|
||||
final previewAspect = previewSize.height / previewSize.width;
|
||||
final radius = (screenWidth(context) - 100) / 2;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: MyAppbar(title: '人脸识别'),
|
||||
appBar: const MyAppbar(title: '人脸识别'),
|
||||
body: Stack(
|
||||
children: [
|
||||
Positioned.fill(child: Container(color: Colors.white)),
|
||||
|
|
@ -182,19 +409,27 @@ class _FaceRecognitionPageState extends State<FaceRecognitionPage> {
|
|||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
const Text(
|
||||
'请将人脸置于圆圈内',
|
||||
style: const TextStyle(fontSize: 16, color: Colors.black87),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Text(
|
||||
_errMsg,
|
||||
style: const TextStyle(fontSize: 14, color: Colors.red),
|
||||
style: TextStyle(fontSize: 16, color: Colors.black87),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (_errMsg.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 6.0),
|
||||
child: Text(
|
||||
_errMsg,
|
||||
style: const TextStyle(fontSize: 14, color: Colors.red),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
if (_isManualMode)
|
||||
CustomButton(text: '拍照/上传', backgroundColor: Colors.blue,onPressed: _captureAndReload,)
|
||||
CustomButton(
|
||||
text: '拍照/上传',
|
||||
backgroundColor: Colors.blue,
|
||||
onPressed: _captureAndReload,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -207,6 +442,7 @@ class _FaceRecognitionPageState extends State<FaceRecognitionPage> {
|
|||
|
||||
class _WhiteMaskPainter extends CustomPainter {
|
||||
final double radius;
|
||||
|
||||
_WhiteMaskPainter({required this.radius});
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ class _StrengthenStudyPageState extends State<StrengthenStudyPage> {
|
|||
String _videoCoverUrl = '';
|
||||
List<dynamic> _videoList = [];
|
||||
Map<String, dynamic> _info = {};
|
||||
bool _loading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -43,60 +44,87 @@ class _StrengthenStudyPageState extends State<StrengthenStudyPage> {
|
|||
|
||||
@override
|
||||
void dispose() {
|
||||
_videoController?.removeListener(_controllerListener);
|
||||
try {
|
||||
_videoController?.removeListener(_controllerListener);
|
||||
} catch (_) {}
|
||||
_videoController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _fetchData() async {
|
||||
final res = await ApiService.getListStrengthenVideo({
|
||||
'CLASS_ID': widget.classId,
|
||||
'STUDENT_ID': widget.studentId,
|
||||
'TYPE': 'APP',
|
||||
});
|
||||
if (res['result'] == 'success') {
|
||||
setState(() {
|
||||
_info = res['relation'];
|
||||
_videoList = res['videoList'];
|
||||
try {
|
||||
final res = await ApiService.getListStrengthenVideo({
|
||||
'CLASS_ID': widget.classId,
|
||||
'STUDENT_ID': widget.studentId,
|
||||
'TYPE': 'APP',
|
||||
});
|
||||
final first = _processData(_videoList);
|
||||
_loadPlayInfo(first, loadPdf: false);
|
||||
if (res['result'] == 'success') {
|
||||
setState(() {
|
||||
_info = res['relation'] ?? {};
|
||||
_videoList = res['videoList'] ?? [];
|
||||
_loading = false;
|
||||
});
|
||||
|
||||
// 选一个首个可播放项并加载(不强制加载 PDF)
|
||||
final first = _processData(_videoList);
|
||||
if (first.isNotEmpty) {
|
||||
await _loadPlayInfo(first, loadPdf: false);
|
||||
}
|
||||
} else {
|
||||
setState(() => _loading = false);
|
||||
ToastUtil.showNormal(context, res['msg'] ?? '数据加载失败');
|
||||
}
|
||||
} catch (e, st) {
|
||||
debugPrint('StrengthenStudyPage._fetchData error: $e\n$st');
|
||||
setState(() => _loading = false);
|
||||
ToastUtil.showNormal(context, '加载失败');
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> _processData(List<dynamic> data) {
|
||||
for (var item in data) {
|
||||
if (item['nodes'] != null) {
|
||||
if (item == null) continue;
|
||||
if (item['nodes'] != null && item['nodes'] is List) {
|
||||
final node = (item['nodes'] as List)
|
||||
.cast<Map<String, dynamic>>()
|
||||
.firstWhere((n) => n['IS_VIDEO'] == 0, orElse: () => {});
|
||||
if (node.isNotEmpty) return node;
|
||||
} else if (item['IS_VIDEO'] == 0) {
|
||||
return item;
|
||||
return Map<String, dynamic>.from(item);
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
Future<void> _loadPlayInfo(
|
||||
Map<String, dynamic> row, {
|
||||
required bool loadPdf,
|
||||
}) async {
|
||||
Map<String, dynamic> row, {
|
||||
required bool loadPdf,
|
||||
}) async {
|
||||
if (row.isEmpty) return;
|
||||
|
||||
final id = row['VIDEOCOURSEWARE_ID'];
|
||||
final isVideo = row['IS_VIDEO'];
|
||||
if (isVideo == 0) {
|
||||
final res = await ApiService.fnGetVideoPlayInfo(id);
|
||||
if (res['result'] == 'success') {
|
||||
setState(() => _videoCoverUrl = res['videoBase']?['coverURL'] ?? '');
|
||||
try {
|
||||
final res = await ApiService.fnGetVideoPlayInfo(id);
|
||||
if (res['result'] == 'success') {
|
||||
setState(() => _videoCoverUrl = res['videoBase']?['coverURL'] ?? '');
|
||||
|
||||
_initVideo(
|
||||
res['videoList']?[0]['playURL'] ?? '',
|
||||
_videoCoverUrl,
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(res['msg'] ?? '播放信息获取失败')));
|
||||
final playUrl = res['videoList'] != null && res['videoList'].isNotEmpty
|
||||
? (res['videoList'][0]['playURL'] ?? '')
|
||||
: '';
|
||||
|
||||
if (playUrl != null && playUrl.toString().isNotEmpty) {
|
||||
_initVideo(playUrl.toString(), _videoCoverUrl);
|
||||
} else {
|
||||
ToastUtil.showNormal(context, '未找到播放地址');
|
||||
}
|
||||
} else {
|
||||
ToastUtil.showNormal(context, res['msg'] ?? '播放信息获取失败');
|
||||
}
|
||||
} catch (e, st) {
|
||||
debugPrint('StrengthenStudyPage._loadPlayInfo error: $e\n$st');
|
||||
ToastUtil.showNormal(context, '获取播放信息失败');
|
||||
}
|
||||
} else if (loadPdf && row['VIDEOFILES'] != null) {
|
||||
_videoController?.pause();
|
||||
|
|
@ -110,19 +138,41 @@ class _StrengthenStudyPageState extends State<StrengthenStudyPage> {
|
|||
}
|
||||
}
|
||||
|
||||
void _initVideo(String url, String cover) {
|
||||
_videoController?.removeListener(_controllerListener);
|
||||
_videoController?.dispose();
|
||||
_videoController = VideoPlayerController.networkUrl(Uri.parse(url))
|
||||
..initialize().then((_) {
|
||||
_videoController!
|
||||
..play()
|
||||
..addListener(_controllerListener);
|
||||
});
|
||||
Future<void> _initVideo(String url, String cover) async {
|
||||
// 移除旧监听并释放旧 controller
|
||||
try {
|
||||
_videoController?.removeListener(_controllerListener);
|
||||
} catch (_) {}
|
||||
try {
|
||||
await _videoController?.dispose();
|
||||
} catch (_) {}
|
||||
|
||||
if (url.isEmpty) return;
|
||||
|
||||
_videoController = VideoPlayerController.networkUrl(Uri.parse(url));
|
||||
|
||||
try {
|
||||
await _videoController!.initialize();
|
||||
if (!mounted) return;
|
||||
// 在初始化完成后立即刷新 UI,这样 VideoPlayerWidget 可以正确拿到 controller
|
||||
setState(() {});
|
||||
// 添加 listener 并播放
|
||||
_videoController!
|
||||
..addListener(_controllerListener)
|
||||
..setLooping(false)
|
||||
..play();
|
||||
} catch (e, st) {
|
||||
debugPrint('StrengthenStudyPage._initVideo initialize/play error: $e\n$st');
|
||||
if (mounted) {
|
||||
ToastUtil.showNormal(context, '视频播放初始化失败');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _controllerListener() {
|
||||
if (mounted) setState(() {});
|
||||
if (!mounted) return;
|
||||
// 只在需要刷新 UI 时调用 setState
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
String _formatDuration(dynamic secs) {
|
||||
|
|
@ -153,8 +203,7 @@ class _StrengthenStudyPageState extends State<StrengthenStudyPage> {
|
|||
'STRENGTHEN_PAPER_QUESTION_ID': _info['STRENGTHEN_STAGEEXAMPAPER_INPUT_ID'],
|
||||
...data
|
||||
}, examType: type), context);
|
||||
|
||||
}else{
|
||||
} else {
|
||||
ToastUtil.showError(context, '请求错误');
|
||||
}
|
||||
}
|
||||
|
|
@ -163,33 +212,38 @@ class _StrengthenStudyPageState extends State<StrengthenStudyPage> {
|
|||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: MyAppbar(title: '加强学习课件'),
|
||||
body: Column(
|
||||
body: _loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: Column(
|
||||
children: [
|
||||
ListItemFactory.createBuildSimpleSection('加强学习课件'),
|
||||
if(_videoList.isEmpty)
|
||||
Stack(children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 200,
|
||||
|
||||
// --- 顶部播放器区域:如果有初始化好的 controller 则显示播放器,否则显示占位/封面 ---
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 250,
|
||||
child: Container(
|
||||
color: Colors.white,
|
||||
child: Center(
|
||||
child:Text("暂未播放",
|
||||
),
|
||||
),
|
||||
),
|
||||
if(_videoList.isNotEmpty)
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 250,
|
||||
child: VideoPlayerWidget(
|
||||
child: _videoController != null && _videoController!.value.isInitialized
|
||||
? VideoPlayerWidget(
|
||||
allowSeek: true,
|
||||
controller: _videoController,
|
||||
coverUrl:'',
|
||||
aspectRatio: _videoController?.value.aspectRatio ?? 16 / 9,
|
||||
coverUrl: _videoCoverUrl,
|
||||
aspectRatio: _videoController!.value.aspectRatio,
|
||||
)
|
||||
: Center(
|
||||
child: _videoCoverUrl.isNotEmpty
|
||||
? Image.network(
|
||||
ApiService.baseImgPath + _videoCoverUrl,
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (_, __, ___) => const Text('暂无封面'),
|
||||
)
|
||||
: const Text('暂未播放'),
|
||||
),
|
||||
),
|
||||
],),
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
|
|
@ -197,6 +251,8 @@ class _StrengthenStudyPageState extends State<StrengthenStudyPage> {
|
|||
itemCount: _videoList.length,
|
||||
itemBuilder: (_, i) {
|
||||
final item = _videoList[i];
|
||||
// item 可能是章节或节点结构,兼容原有显示
|
||||
final name = item['COURSEWARENAME'] ?? item['NAME'] ?? '';
|
||||
return GestureDetector(
|
||||
onTap: () => _loadPlayInfo(item, loadPdf: true),
|
||||
child: Container(
|
||||
|
|
@ -212,18 +268,15 @@ class _StrengthenStudyPageState extends State<StrengthenStudyPage> {
|
|||
Row(
|
||||
children: [
|
||||
Image.asset(
|
||||
item['IS_VIDEO'] == 0
|
||||
? 'assets/study/play.png'
|
||||
: 'assets/study/copy-one.png',
|
||||
item['IS_VIDEO'] == 0 ? 'assets/study/play.png' : 'assets/study/copy-one.png',
|
||||
width: 20,
|
||||
height: 20,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(item['COURSEWARENAME'] ?? ''),
|
||||
Text(name),
|
||||
],
|
||||
),
|
||||
if (item['IS_VIDEO'] == 0)
|
||||
Text(_formatDuration(item['VIDEOTIME'])),
|
||||
if (item['IS_VIDEO'] == 0) Text(_formatDuration(item['VIDEOTIME'])),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -231,6 +284,7 @@ class _StrengthenStudyPageState extends State<StrengthenStudyPage> {
|
|||
},
|
||||
),
|
||||
),
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: CustomButton(
|
||||
|
|
@ -241,11 +295,9 @@ class _StrengthenStudyPageState extends State<StrengthenStudyPage> {
|
|||
_startExam(TakeExamType.strengththen);
|
||||
},
|
||||
),
|
||||
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:qhd_prevention/customWidget/remote_file_page.dart';
|
||||
import 'package:qhd_prevention/pages/home/study/take_exam_page.dart';
|
||||
import 'package:qhd_prevention/pages/home/study/study_practise_page.dart';
|
||||
|
|
@ -11,6 +11,7 @@ import 'package:qhd_prevention/pages/my_appbar.dart';
|
|||
import 'package:qhd_prevention/customWidget/custom_alert_dialog.dart';
|
||||
import 'package:qhd_prevention/customWidget/custom_button.dart';
|
||||
import 'package:qhd_prevention/http/ApiService.dart';
|
||||
import 'package:qhd_prevention/services/auth_service.dart';
|
||||
import 'package:qhd_prevention/tools/tools.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
|
|
@ -78,9 +79,7 @@ class _StudyDetailPageState extends State<StudyDetailPage>
|
|||
void dispose() {
|
||||
_tabController.dispose();
|
||||
WakelockPlus.disable();
|
||||
SystemChrome.setPreferredOrientations([
|
||||
DeviceOrientation.portraitUp,
|
||||
]);
|
||||
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
|
||||
// 让未完成的提交完成(非阻塞)
|
||||
if (_ongoingSubmit != null) {
|
||||
_ongoingSubmit!.whenComplete(() {});
|
||||
|
|
@ -102,12 +101,14 @@ class _StudyDetailPageState extends State<StudyDetailPage>
|
|||
final ok = await CustomAlertDialog.showConfirm(
|
||||
context,
|
||||
title: '温馨提示',
|
||||
content:
|
||||
'重要提醒:尊敬的用户,根据规定我们会在您学习过程中多次进行人脸识别认证,为了保护您的隐私请您在摄像设备视野内确保衣冠整齐。',
|
||||
content: '重要提醒:尊敬的用户,根据规定我们会在您学习过程中多次进行人脸识别认证,为了保护您的隐私请您在摄像设备视野内确保衣冠整齐。',
|
||||
cancelText: '取消',
|
||||
confirmText: '同意并继续',
|
||||
);
|
||||
if (ok) {}
|
||||
if (!ok) {
|
||||
Navigator.of(context).pop();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadData() async {
|
||||
|
|
@ -154,21 +155,37 @@ class _StudyDetailPageState extends State<StudyDetailPage>
|
|||
required dynamic resourceTime,
|
||||
required dynamic videoTime,
|
||||
}) {
|
||||
// 播放次数大于 0 视为 100%
|
||||
final seen = int.tryParse('$playCount') ?? 0;
|
||||
if (seen > 0) return '100%';
|
||||
|
||||
final resT = double.tryParse('$resourceTime') ?? 0.0;
|
||||
final vidT = double.tryParse('$videoTime') ?? 0.0;
|
||||
if (vidT == 0) return '0%';
|
||||
final pct = ((resT / vidT) * 100).clamp(0.0, 100.0).toStringAsFixed(0);
|
||||
return '$pct%';
|
||||
|
||||
if (vidT <= 0) return '0%';
|
||||
|
||||
double pct = (resT / vidT) * 100.0;
|
||||
// 限制在合理范围
|
||||
if (pct <= 0.0) return '0%';
|
||||
if (pct >= 100.0) return '100%';
|
||||
|
||||
// 非零但非常小的值,例如 0.004%,显示为 <0.01%
|
||||
if (pct > 0.0 && pct < 0.01) return '<0.01%';
|
||||
|
||||
// 保留最多 2 位小数,然后去掉多余的零和小数点
|
||||
String s = pct.toStringAsFixed(2);
|
||||
s = s.replaceFirst(RegExp(r'\.?0+$'), ''); // 去掉尾随的 .00 或 .0
|
||||
return '$s%';
|
||||
}
|
||||
|
||||
|
||||
|
||||
Future<void> _onVideoTap(
|
||||
Map<String, dynamic> data,
|
||||
bool hasNodes,
|
||||
int fi,
|
||||
int ni,
|
||||
) async {
|
||||
Map<String, dynamic> data,
|
||||
bool hasNodes,
|
||||
int fi,
|
||||
int ni,
|
||||
) async {
|
||||
if (_isLoadingVideo) {
|
||||
debugPrint('_onVideoTap ignored because a video is loading');
|
||||
return;
|
||||
|
|
@ -189,7 +206,11 @@ class _StudyDetailPageState extends State<StudyDetailPage>
|
|||
'FIRST_INDEX': _currentFirstIndex,
|
||||
'NODE_INDEX': _currentNodeIndex,
|
||||
};
|
||||
await _submitPlayTime(snapshot: prevSnapshot, end: false, seconds: _lastReported.inSeconds);
|
||||
await _submitPlayTime(
|
||||
snapshot: prevSnapshot,
|
||||
end: false,
|
||||
seconds: _lastReported.inSeconds,
|
||||
);
|
||||
}
|
||||
_lastReported = Duration.zero;
|
||||
|
||||
|
|
@ -212,32 +233,39 @@ class _StudyDetailPageState extends State<StudyDetailPage>
|
|||
try {
|
||||
await _videoController?.pause();
|
||||
} catch (_) {}
|
||||
await pushPage(
|
||||
final success = await pushPage(
|
||||
RemoteFilePage(
|
||||
fileUrl: ApiService.baseImgPath + data['VIDEOFILES'],
|
||||
countdownSeconds: 30,
|
||||
),
|
||||
context,
|
||||
);
|
||||
// 对文档直接上报完整时长(用 data 做快照)
|
||||
final docSnapshot = <String, dynamic>{
|
||||
'VIDEOCOURSEWARE_ID': data['VIDEOCOURSEWARE_ID'] ?? '',
|
||||
'CURRICULUM_ID': data['CURRICULUM_ID'] ?? '',
|
||||
'CHAPTER_ID': data['CHAPTER_ID'] ?? '',
|
||||
'VIDEOTIME': data['VIDEOTIME'] ?? 0,
|
||||
'IS_NODE': hasNodes,
|
||||
'FIRST_INDEX': fi,
|
||||
'NODE_INDEX': ni,
|
||||
};
|
||||
await _submitPlayTime(snapshot: docSnapshot, end: true, seconds: int.parse('${data['VIDEOTIME'] ?? '0'}'));
|
||||
if (success) {
|
||||
// 对文档直接上报完整时长(用 data 做快照)
|
||||
final docSnapshot = <String, dynamic>{
|
||||
'VIDEOCOURSEWARE_ID': data['VIDEOCOURSEWARE_ID'] ?? '',
|
||||
'CURRICULUM_ID': data['CURRICULUM_ID'] ?? '',
|
||||
'CHAPTER_ID': data['CHAPTER_ID'] ?? '',
|
||||
'VIDEOTIME': data['VIDEOTIME'] ?? 0,
|
||||
'IS_NODE': hasNodes,
|
||||
'FIRST_INDEX': fi,
|
||||
'NODE_INDEX': ni,
|
||||
};
|
||||
await _submitPlayTime(
|
||||
snapshot: docSnapshot,
|
||||
end: true,
|
||||
seconds: int.parse('${data['VIDEOTIME'] ?? '0'}'),
|
||||
);
|
||||
}
|
||||
|
||||
} else {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('课件文件资源已失效,请联系管理员')));
|
||||
ToastUtil.showNormal(context, '课件文件资源已失效,请联系管理员');
|
||||
}
|
||||
} else {
|
||||
LoadingDialogHelper.show();
|
||||
// 视频
|
||||
await _getVideoPlayInfo(data['VIDEOCOURSEWARE_ID']);
|
||||
LoadingDialogHelper.hide();
|
||||
_startFaceTimer();
|
||||
}
|
||||
});
|
||||
|
|
@ -260,6 +288,7 @@ class _StudyDetailPageState extends State<StudyDetailPage>
|
|||
await _restoreDefaultOrientations();
|
||||
|
||||
if (passed == true) {
|
||||
LoadingDialogHelper.show();
|
||||
await ApiService.fnSetUserFaceTime(_faceTime);
|
||||
await onPass();
|
||||
} else {
|
||||
|
|
@ -308,14 +337,14 @@ class _StudyDetailPageState extends State<StudyDetailPage>
|
|||
|
||||
final raw = prog['pd']?['RESOURCETIME'];
|
||||
final seen =
|
||||
(() {
|
||||
if (raw == null) return 0;
|
||||
// 如果本身就是数字
|
||||
if (raw is num) return raw.toInt();
|
||||
// 否则转成字符串再 parse
|
||||
final s = raw.toString();
|
||||
return (double.tryParse(s) ?? 0.0).toInt();
|
||||
})();
|
||||
(() {
|
||||
if (raw == null) return 0;
|
||||
// 如果本身就是数字
|
||||
if (raw is num) return raw.toInt();
|
||||
// 否则转成字符串再 parse
|
||||
final s = raw.toString();
|
||||
return (double.tryParse(s) ?? 0.0).toInt();
|
||||
})();
|
||||
|
||||
// 先销毁旧 controller(安全顺序:removeListener -> pause -> dispose -> null)
|
||||
if (_videoController != null) {
|
||||
|
|
@ -391,12 +420,16 @@ class _StudyDetailPageState extends State<StudyDetailPage>
|
|||
// 串行上报:通过 _ongoingSubmit 链式调用来保证顺序
|
||||
_ongoingSubmit = (_ongoingSubmit ?? Future.value())
|
||||
.then((_) {
|
||||
return _submitPlayTime(snapshot: snapshot, end: false, seconds: pos.inSeconds);
|
||||
})
|
||||
return _submitPlayTime(
|
||||
snapshot: snapshot,
|
||||
end: false,
|
||||
seconds: pos.inSeconds,
|
||||
);
|
||||
})
|
||||
.whenComplete(() {
|
||||
// 完成后清理引用
|
||||
_ongoingSubmit = null;
|
||||
});
|
||||
// 完成后清理引用
|
||||
_ongoingSubmit = null;
|
||||
});
|
||||
}
|
||||
|
||||
// 接近结束的检测,容差 800ms,触发一次最终结束上报
|
||||
|
|
@ -419,13 +452,17 @@ class _StudyDetailPageState extends State<StudyDetailPage>
|
|||
|
||||
_ongoingSubmit = (_ongoingSubmit ?? Future.value())
|
||||
.then((_) async {
|
||||
await _submitPlayTime(snapshot: snapshot, end: true, seconds: finalSeconds);
|
||||
})
|
||||
await _submitPlayTime(
|
||||
snapshot: snapshot,
|
||||
end: true,
|
||||
seconds: finalSeconds,
|
||||
);
|
||||
})
|
||||
.whenComplete(() {
|
||||
_ongoingSubmit = null;
|
||||
// 最终上报完成后尝试退出顶层 route(退出全屏)
|
||||
_exitTopRouteIfPresent();
|
||||
});
|
||||
_ongoingSubmit = null;
|
||||
// 最终上报完成后尝试退出顶层 route(退出全屏)
|
||||
_exitTopRouteIfPresent();
|
||||
});
|
||||
|
||||
ApiService.fnClearUserFaceTime();
|
||||
_faceTimer?.cancel();
|
||||
|
|
@ -439,7 +476,9 @@ class _StudyDetailPageState extends State<StudyDetailPage>
|
|||
required int seconds,
|
||||
}) async {
|
||||
// snapshot 中应该包含 VIDEOCOURSEWARE_ID, CURRICULUM_ID, CHAPTER_ID, VIDEOTIME, ...
|
||||
if (snapshot['VIDEOCOURSEWARE_ID'] == null || snapshot['VIDEOCOURSEWARE_ID'] == '') return;
|
||||
if (snapshot['VIDEOCOURSEWARE_ID'] == null ||
|
||||
snapshot['VIDEOCOURSEWARE_ID'] == '')
|
||||
return;
|
||||
|
||||
// 如果已经上报结束并且当前不是结束上报,则跳过非结束上报
|
||||
if (_endReported && !end) return;
|
||||
|
|
@ -467,20 +506,20 @@ class _StudyDetailPageState extends State<StudyDetailPage>
|
|||
// 解析后端返回的 RESOURCETIME(可能是数字或字符串)
|
||||
final resTraw = pd['RESOURCETIME'];
|
||||
final resT =
|
||||
(resTraw is num)
|
||||
? resTraw.toDouble()
|
||||
: double.tryParse('$resTraw') ?? seconds.toDouble();
|
||||
(resTraw is num)
|
||||
? resTraw.toDouble()
|
||||
: double.tryParse('$resTraw') ?? seconds.toDouble();
|
||||
|
||||
final videoTimeRaw = snapshot['VIDEOTIME'];
|
||||
final videoTime =
|
||||
(videoTimeRaw is String)
|
||||
? double.tryParse(videoTimeRaw) ?? 1.0
|
||||
: (videoTimeRaw is num ? videoTimeRaw.toDouble() : 1.0);
|
||||
(videoTimeRaw is String)
|
||||
? double.tryParse(videoTimeRaw) ?? 1.0
|
||||
: (videoTimeRaw is num ? videoTimeRaw.toDouble() : 1.0);
|
||||
|
||||
final comp = pd['PLAYCOUNT'] != null && pd['PLAYCOUNT'] > 0;
|
||||
|
||||
final pctDouble =
|
||||
comp ? 100.0 : ((resT / (videoTime > 0 ? videoTime : 1.0)) * 100.0);
|
||||
comp ? 100.0 : ((resT / (videoTime > 0 ? videoTime : 1.0)) * 100.0);
|
||||
final pct = (pctDouble.clamp(0.00, 100.00) * 100).round() / 100;
|
||||
final str = '${pct}%';
|
||||
|
||||
|
|
@ -562,7 +601,7 @@ class _StudyDetailPageState extends State<StudyDetailPage>
|
|||
'POST_ID': pd['POST_ID'] ?? '',
|
||||
'STUDENT_ID': widget.studentId,
|
||||
'STRENGTHEN_PAPER_QUESTION_ID':
|
||||
paper['STAGEEXAMPAPERINPUT_ID'] ?? '',
|
||||
paper['STAGEEXAMPAPERINPUT_ID'] ?? '',
|
||||
...data,
|
||||
},
|
||||
examType: TakeExamType.video_study,
|
||||
|
|
@ -655,7 +694,9 @@ class _StudyDetailPageState extends State<StudyDetailPage>
|
|||
try {
|
||||
final currentPos = _videoController!.value.position;
|
||||
_lastReported = currentPos;
|
||||
debugPrint('Face auth passed — resetting _lastReported to $currentPos');
|
||||
debugPrint(
|
||||
'Face auth passed — resetting _lastReported to $currentPos',
|
||||
);
|
||||
} catch (_) {
|
||||
_lastReported = Duration.zero;
|
||||
}
|
||||
|
|
@ -665,7 +706,9 @@ class _StudyDetailPageState extends State<StudyDetailPage>
|
|||
_videoController!.addListener(_onTimeUpdate);
|
||||
} else {
|
||||
// 如果 controller 为空或未初始化,仅尝试更新 UI
|
||||
debugPrint('Face auth passed but _videoController is null or not initialized');
|
||||
debugPrint(
|
||||
'Face auth passed but _videoController is null or not initialized',
|
||||
);
|
||||
}
|
||||
setState(() {});
|
||||
} catch (e, st) {
|
||||
|
|
@ -695,9 +738,10 @@ class _StudyDetailPageState extends State<StudyDetailPage>
|
|||
return VideoPlayerWidget(
|
||||
allowSeek: false,
|
||||
controller: _videoController,
|
||||
coverUrl: _videoCoverUrl.isNotEmpty
|
||||
? ApiService.baseImgPath + _videoCoverUrl
|
||||
: ApiService.baseImgPath + (_info?['COVERPATH'] ?? ''),
|
||||
coverUrl:
|
||||
_videoCoverUrl.isNotEmpty
|
||||
? ApiService.baseImgPath + _videoCoverUrl
|
||||
: ApiService.baseImgPath + (_info?['COVERPATH'] ?? ''),
|
||||
aspectRatio: _videoController?.value.aspectRatio ?? 16 / 9,
|
||||
);
|
||||
} else {
|
||||
|
|
@ -773,56 +817,87 @@ class _StudyDetailPageState extends State<StudyDetailPage>
|
|||
itemBuilder: (ctx, idx) {
|
||||
final item = _videoList[idx] as Map<String, dynamic>;
|
||||
final nodes = item['nodes'] as List<dynamic>?;
|
||||
|
||||
// 如果是带有子节点的章节,先渲染章节头,再渲染该章节的所有子项
|
||||
if (nodes != null && nodes.isNotEmpty) {
|
||||
// 章节标题 + 直接展开的子项列表(不折叠)
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
...nodes.asMap().entries.map(
|
||||
(e) => _buildVideoItem(
|
||||
item,
|
||||
e.value as Map<String, dynamic>,
|
||||
true,
|
||||
idx,
|
||||
e.key,
|
||||
// 章节头
|
||||
Container(
|
||||
width: double.infinity,
|
||||
color: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
item['IS_VIDEO'] == 0
|
||||
? Icons.video_collection_rounded
|
||||
: Icons.file_copy_rounded,
|
||||
color: Colors.grey,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
Text(
|
||||
(item['NAME'] ?? '').toString(),
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// Text(
|
||||
// (item['NAME'] ?? '').toString(),
|
||||
// style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
// ),
|
||||
),
|
||||
const Divider(height: 1), // 每章节后一个分割线
|
||||
|
||||
// 子项列表
|
||||
...nodes.asMap().entries.map((e) {
|
||||
final node = e.value as Map<String, dynamic>;
|
||||
return _buildVideoItem(item, node, true, idx, e.key);
|
||||
}).toList(),
|
||||
|
||||
const Divider(height: 1),
|
||||
],
|
||||
);
|
||||
}
|
||||
// 没有子节点,直接显示该条目
|
||||
|
||||
// 没有子节点,把 item 本身当成单条记录显示
|
||||
return _buildVideoItem(item, item, false, idx, 0);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVideoItem(
|
||||
Map<String, dynamic> item,
|
||||
Map<String, dynamic> m,
|
||||
bool hasNodes,
|
||||
int fi,
|
||||
int ni,
|
||||
) {
|
||||
Map<String, dynamic> item,
|
||||
Map<String, dynamic> m,
|
||||
bool hasNodes,
|
||||
int fi,
|
||||
int ni,
|
||||
) {
|
||||
return Container(
|
||||
color: Colors.white,
|
||||
padding: const EdgeInsets.all(20),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Icon(Icons.file_copy_rounded, color: Colors.grey, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
item['NAME'] ?? '',
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
// Row(
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// children: [
|
||||
// Icon(m['IS_VIDEO'] == 0 ? Icons.video_collection_rounded : Icons.file_copy_rounded, color: Colors.grey, size: 20),
|
||||
// const SizedBox(width: 8),
|
||||
// Expanded(
|
||||
// child: Text(
|
||||
// item['NAME'] ?? '',
|
||||
// style: const TextStyle(fontSize: 14),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// const SizedBox(height: 10),
|
||||
GestureDetector(
|
||||
onTap: () => _onVideoTap(m, hasNodes, fi, ni),
|
||||
child: Container(
|
||||
|
|
@ -856,15 +931,19 @@ class _StudyDetailPageState extends State<StudyDetailPage>
|
|||
const Icon(Icons.play_circle, color: Colors.blue),
|
||||
],
|
||||
if (m['IS_VIDEO'] != 0)
|
||||
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween,children: [Text('-'),const SizedBox(width: 50,)],),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [Text('-'), const SizedBox(width: 50)],
|
||||
),
|
||||
SizedBox(width: 20),
|
||||
CustomButton(
|
||||
onPressed: () => pushPage(
|
||||
StudyPractisePage(
|
||||
videoCoursewareId: m['VIDEOCOURSEWARE_ID'],
|
||||
),
|
||||
context,
|
||||
),
|
||||
onPressed:
|
||||
() => pushPage(
|
||||
StudyPractisePage(
|
||||
videoCoursewareId: m['VIDEOCOURSEWARE_ID'],
|
||||
),
|
||||
context,
|
||||
),
|
||||
text: "课后练习",
|
||||
backgroundColor: Colors.blue,
|
||||
height: 30,
|
||||
|
|
|
|||
|
|
@ -22,20 +22,27 @@ class StudyMyTaskPage extends StatefulWidget {
|
|||
State<StudyMyTaskPage> createState() => _StudyMyTaskPageState();
|
||||
}
|
||||
|
||||
class _StudyMyTaskPageState extends State<StudyMyTaskPage> with RouteAware {
|
||||
class _StudyMyTaskPageState extends State<StudyMyTaskPage>
|
||||
with RouteAware, AutomaticKeepAliveClientMixin {
|
||||
int _page = 1;
|
||||
final int _showCount = 10;
|
||||
bool _isLoading = false;
|
||||
bool _hasMore = true;
|
||||
int _totalPage = 1;
|
||||
List<dynamic> _list = [];
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
Timer? _timer;
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scrollController.addListener(_scrollListener);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _getStudyList());
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
|
|
@ -50,14 +57,18 @@ class _StudyMyTaskPageState extends State<StudyMyTaskPage> with RouteAware {
|
|||
void didPopNext() {
|
||||
if (!_isLoading) _getStudyList();
|
||||
}
|
||||
|
||||
@override
|
||||
void didPush() {
|
||||
// 可选:首次 initState 已经调用过一次 _getStudyList,
|
||||
if (!_isLoading) _getStudyList();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
_scrollController.removeListener(_scrollListener);
|
||||
_scrollController.dispose();
|
||||
// 退订 routeObserver(防止内存泄漏)
|
||||
try {
|
||||
routeObserver.unsubscribe(this);
|
||||
|
|
@ -65,28 +76,13 @@ class _StudyMyTaskPageState extends State<StudyMyTaskPage> with RouteAware {
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
/// 每秒减少 remainingSeconds,用于倒计时显示
|
||||
void _startCountdownTimer() {
|
||||
if (_timer != null && _timer!.isActive) return; // 避免重复开启
|
||||
|
||||
_timer = Timer.periodic(Duration(seconds: 1), (_) {
|
||||
setState(() {
|
||||
for (var i = 0; i < _list.length; i++) {
|
||||
final endTimeStr = _list[i]["END_TIME"] as String;
|
||||
|
||||
try {
|
||||
// 直接用原始字符串解析
|
||||
final endTime = DateTime.parse(endTimeStr);
|
||||
final now = DateTime.now();
|
||||
final seconds = endTime.difference(now).inSeconds;
|
||||
|
||||
_list[i]['remainingSeconds'] = seconds > 0 ? seconds : 0;
|
||||
} catch (e) {
|
||||
_list[i]['remainingSeconds'] = 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
void _scrollListener() {
|
||||
if (_scrollController.position.pixels >
|
||||
_scrollController.position.maxScrollExtent - 100 &&
|
||||
_hasMore &&
|
||||
!_isLoading) {
|
||||
_getStudyList(loadMore: true);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _getStudyList({bool loadMore = false}) async {
|
||||
|
|
@ -100,20 +96,6 @@ class _StudyMyTaskPageState extends State<StudyMyTaskPage> with RouteAware {
|
|||
final List<dynamic> varList = res['varList'] ?? [];
|
||||
_totalPage = res['totalPage'] ?? 1;
|
||||
|
||||
// 计算初始倒计时
|
||||
final now = DateTime.now();
|
||||
for (var item in varList) {
|
||||
final endStr =
|
||||
(item['END_TIME'] as String?)?.replaceAll('-', '/') ?? '';
|
||||
try {
|
||||
final end = DateTime.parse(endStr);
|
||||
final diff = end.difference(now).inSeconds;
|
||||
item['remainingSeconds'] = diff > 0 ? diff : 0;
|
||||
} catch (_) {
|
||||
item['remainingSeconds'] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
if (loadMore) {
|
||||
_list.addAll(varList);
|
||||
|
|
@ -122,8 +104,6 @@ class _StudyMyTaskPageState extends State<StudyMyTaskPage> with RouteAware {
|
|||
}
|
||||
_hasMore = _page < _totalPage;
|
||||
if (_hasMore) _page++;
|
||||
_startCountdownTimer();
|
||||
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
@ -134,6 +114,219 @@ class _StudyMyTaskPageState extends State<StudyMyTaskPage> with RouteAware {
|
|||
}
|
||||
}
|
||||
|
||||
void _onTapSign(Map item) async {
|
||||
await NativeOrientation.setLandscape();
|
||||
|
||||
final String? imagePath = await Navigator.push<String>(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => MineSignPage()),
|
||||
);
|
||||
await NativeOrientation.setPortrait();
|
||||
|
||||
// 用户有签名并取到了 imagePath
|
||||
if (imagePath != null && imagePath.isNotEmpty) {
|
||||
// 上传
|
||||
await _uploadSignAndNavigate(item, imagePath);
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> _getImageBase64WithHeader(String imagePath) async {
|
||||
final File file = File(imagePath);
|
||||
final List<int> bytes = await file.readAsBytes();
|
||||
final String base64Data = base64Encode(bytes);
|
||||
|
||||
// 根据文件扩展名确定MIME类型
|
||||
final extension = imagePath.split('.').last.toLowerCase();
|
||||
|
||||
String header;
|
||||
switch (extension) {
|
||||
case 'jpg':
|
||||
case 'jpeg':
|
||||
header = 'data:image/jpeg;base64,';
|
||||
break;
|
||||
case 'png':
|
||||
header = 'data:image/png;base64,';
|
||||
break;
|
||||
case 'gif':
|
||||
header = 'data:image/gif;base64,';
|
||||
break;
|
||||
case 'webp':
|
||||
header = 'data:image/webp;base64,';
|
||||
break;
|
||||
case 'svg':
|
||||
header = 'data:image/svg+xml;base64,';
|
||||
break;
|
||||
default:
|
||||
// 对于未知类型,使用通用的二进制数据头
|
||||
header = 'data:application/octet-stream;base64,';
|
||||
}
|
||||
|
||||
return header + base64Data;
|
||||
}
|
||||
|
||||
Future<void> _uploadSignAndNavigate(Map item, String imagePath) async {
|
||||
try {
|
||||
final String signBase64 = await _getImageBase64WithHeader(imagePath);
|
||||
final result = await ApiService.signUpdate(
|
||||
signBase64,
|
||||
item['CLASS_ID'],
|
||||
item['STAGESTUDENTRELATION_ID'],
|
||||
);
|
||||
|
||||
if (result['result'] == 'success') {
|
||||
pushPage(StudyClassListPage(item), context);
|
||||
}
|
||||
} catch (e) {
|
||||
// 捕获并提示错误
|
||||
ToastUtil.showNormal(context, '上传出错:$e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 开始考试
|
||||
Future<void> _startExam(Map resData, TakeExamType type) async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
final arguments = {
|
||||
'STAGEEXAMPAPERINPUT_ID': resData['STAGEEXAMPAPERINPUT_ID'] ?? '',
|
||||
'STAGEEXAMPAPER_ID': resData['STAGEEXAMPAPERINPUT_ID'] ?? '',
|
||||
'CLASS_ID': resData['CLASS_ID'] ?? '',
|
||||
'POST_ID': resData['POST_ID'] ?? '',
|
||||
'STUDENT_ID': resData['STUDENT_ID'] ?? '',
|
||||
'NUMBEROFEXAMS': resData['NUMBEROFEXAMS'] ?? '',
|
||||
};
|
||||
print('--_startExam data---$arguments');
|
||||
|
||||
final data = await ApiService.getStartExam(arguments);
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
if (data['result'] == 'success') {
|
||||
pushPage(
|
||||
TakeExamPage(
|
||||
examInfo: {
|
||||
'CLASS_ID': resData['CLASS_ID'] ?? '',
|
||||
'POST_ID': resData['POST_ID'] ?? '',
|
||||
'STUDENT_ID': resData['STUDENT_ID'] ?? '',
|
||||
'STRENGTHEN_PAPER_QUESTION_ID':
|
||||
resData['STAGEEXAMPAPERINPUT_ID'] ?? '',
|
||||
...data
|
||||
},
|
||||
examType: TakeExamType.video_study),
|
||||
context);
|
||||
} else {
|
||||
ToastUtil.showError(context, '请求错误');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return Scaffold(
|
||||
appBar: MyAppbar(title: '我的学习'),
|
||||
body: SafeArea(
|
||||
child: _list.isEmpty
|
||||
? NoDataWidget.show()
|
||||
: ListView.builder(
|
||||
controller: _scrollController,
|
||||
itemCount: _list.length + (_hasMore ? 1 : 0),
|
||||
itemBuilder: (context, index) {
|
||||
if (index == _list.length) {
|
||||
return _hasMore
|
||||
? Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: CircularProgressIndicator(),
|
||||
))
|
||||
: SizedBox.shrink();
|
||||
}
|
||||
return _StudyTaskItem(
|
||||
item: _list[index],
|
||||
onTapSign: _onTapSign,
|
||||
startExam: _startExam,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StudyTaskItem extends StatefulWidget {
|
||||
final Map item;
|
||||
final Function(Map) onTapSign;
|
||||
final Function(Map, TakeExamType) startExam;
|
||||
|
||||
const _StudyTaskItem({
|
||||
required this.item,
|
||||
required this.onTapSign,
|
||||
required this.startExam,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_StudyTaskItem> createState() => _StudyTaskItemState();
|
||||
}
|
||||
|
||||
class _StudyTaskItemState extends State<_StudyTaskItem> {
|
||||
late int _remainingSeconds;
|
||||
Timer? _countdownTimer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// 初始化倒计时
|
||||
_updateRemainingSeconds();
|
||||
// 只在需要时启动定时器
|
||||
if (_remainingSeconds > 0) {
|
||||
_startCountdownTimer();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(_StudyTaskItem oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
// 如果item更新了,重新计算倒计时
|
||||
if (oldWidget.item['END_TIME'] != widget.item['END_TIME']) {
|
||||
_updateRemainingSeconds();
|
||||
// 如果之前有定时器,取消它
|
||||
_countdownTimer?.cancel();
|
||||
// 如果需要,重新启动定时器
|
||||
if (_remainingSeconds > 0) {
|
||||
_startCountdownTimer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_countdownTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _updateRemainingSeconds() {
|
||||
final endTimeStr = widget.item["END_TIME"] as String;
|
||||
try {
|
||||
final endTime = DateTime.parse(endTimeStr);
|
||||
final now = DateTime.now();
|
||||
_remainingSeconds = endTime.difference(now).inSeconds;
|
||||
if (_remainingSeconds < 0) _remainingSeconds = 0;
|
||||
} catch (e) {
|
||||
_remainingSeconds = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void _startCountdownTimer() {
|
||||
_countdownTimer = Timer.periodic(Duration(seconds: 1), (timer) {
|
||||
if (_remainingSeconds <= 0) {
|
||||
timer.cancel();
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_remainingSeconds--;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
String formatSeconds(int seconds) {
|
||||
if (seconds <= 0) return '00天 00:00:00';
|
||||
final d = Duration(seconds: seconds);
|
||||
|
|
@ -175,82 +368,15 @@ class _StudyMyTaskPageState extends State<StudyMyTaskPage> with RouteAware {
|
|||
}
|
||||
}
|
||||
|
||||
void _onTapSign(Map item) async {
|
||||
await NativeOrientation.setLandscape();
|
||||
|
||||
final String? imagePath = await Navigator.push<String>(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => MineSignPage()),
|
||||
);
|
||||
await NativeOrientation.setPortrait();
|
||||
|
||||
// 用户有签名并取到了 imagePath
|
||||
if (imagePath != null && imagePath.isNotEmpty) {
|
||||
// 上传
|
||||
await _uploadSignAndNavigate(item, imagePath);
|
||||
}
|
||||
}
|
||||
Future<String> _getImageBase64WithHeader(String imagePath) async {
|
||||
final File file = File(imagePath);
|
||||
final List<int> bytes = await file.readAsBytes();
|
||||
final String base64Data = base64Encode(bytes);
|
||||
|
||||
// 根据文件扩展名确定MIME类型
|
||||
final extension = imagePath.split('.').last.toLowerCase();
|
||||
|
||||
String header;
|
||||
switch (extension) {
|
||||
case 'jpg':
|
||||
case 'jpeg':
|
||||
header = 'data:image/jpeg;base64,';
|
||||
break;
|
||||
case 'png':
|
||||
header = 'data:image/png;base64,';
|
||||
break;
|
||||
case 'gif':
|
||||
header = 'data:image/gif;base64,';
|
||||
break;
|
||||
case 'webp':
|
||||
header = 'data:image/webp;base64,';
|
||||
break;
|
||||
case 'svg':
|
||||
header = 'data:image/svg+xml;base64,';
|
||||
break;
|
||||
default:
|
||||
// 对于未知类型,使用通用的二进制数据头
|
||||
header = 'data:application/octet-stream;base64,';
|
||||
}
|
||||
|
||||
return header + base64Data;
|
||||
}
|
||||
Future<void> _uploadSignAndNavigate(Map item, String imagePath) async {
|
||||
try {
|
||||
final String signBase64 = await _getImageBase64WithHeader(imagePath);
|
||||
final result = await ApiService.signUpdate(
|
||||
signBase64,
|
||||
item['CLASS_ID'],
|
||||
item['STAGESTUDENTRELATION_ID'],
|
||||
);
|
||||
|
||||
if (result['result'] == 'success') {
|
||||
pushPage(StudyClassListPage(item), context);
|
||||
}
|
||||
} catch (e) {
|
||||
// 捕获并提示错误
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('上传出错:$e')));
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildItem(Map item) {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final item = widget.item;
|
||||
final now = DateTime.now();
|
||||
final start = DateTime.tryParse(item['START_TIME'] ?? '');
|
||||
final end = DateTime.tryParse(item['END_TIME'] ?? '');
|
||||
final nowOk =
|
||||
start != null && end != null
|
||||
? now.isAfter(start) && now.isBefore(end)
|
||||
: false;
|
||||
final nowOk = start != null && end != null
|
||||
? now.isAfter(start) && now.isBefore(end)
|
||||
: false;
|
||||
|
||||
final int studyState = int.tryParse(item['STUDYSTATE'] ?? '') ?? 0;
|
||||
final int stageExamState = int.tryParse(item['STAGEEXAMSTATE'] ?? '') ?? 0;
|
||||
|
|
@ -259,9 +385,7 @@ class _StudyMyTaskPageState extends State<StudyMyTaskPage> with RouteAware {
|
|||
final int numberOfExams = int.tryParse('${item['NUMBEROFEXAMS']}') ?? 0;
|
||||
final int ksCount = int.tryParse('${item['ksCount']}') ?? 0;
|
||||
final int examinationFlag =
|
||||
item['EXAMINATION'] is int
|
||||
? item['EXAMINATION'] as int
|
||||
: int.tryParse('${item['EXAMINATION']}') ?? 0;
|
||||
item['EXAMINATION'] is int ? item['EXAMINATION'] as int : int.tryParse('${item['EXAMINATION']}') ?? 0;
|
||||
final String isStrengthen = item['ISSTRENGTHEN'] ?? '0';
|
||||
|
||||
return Container(
|
||||
|
|
@ -286,7 +410,9 @@ class _StudyMyTaskPageState extends State<StudyMyTaskPage> with RouteAware {
|
|||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10,),
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
Text(
|
||||
_stateText(item['STUDYSTATE']),
|
||||
style: TextStyle(color: _stateColor(item['STUDYSTATE'])),
|
||||
|
|
@ -311,7 +437,7 @@ class _StudyMyTaskPageState extends State<StudyMyTaskPage> with RouteAware {
|
|||
const Icon(Icons.access_time, size: 18, color: Colors.grey),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
formatSeconds(item['remainingSeconds'] ?? 0),
|
||||
formatSeconds(_remainingSeconds),
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
],
|
||||
|
|
@ -330,15 +456,14 @@ class _StudyMyTaskPageState extends State<StudyMyTaskPage> with RouteAware {
|
|||
padding: EdgeInsets.symmetric(horizontal: 18),
|
||||
borderRadius: 18,
|
||||
backgroundColor: Colors.blue,
|
||||
onPressed:
|
||||
() => pushPage(
|
||||
StrengthenStudyPage(
|
||||
classId: item['CLASS_ID'] ?? '',
|
||||
postId: item['POST_ID'] ?? '',
|
||||
studentId: item['STUDENT_ID'] ?? '',
|
||||
),
|
||||
context,
|
||||
),
|
||||
onPressed: () => pushPage(
|
||||
StrengthenStudyPage(
|
||||
classId: item['CLASS_ID'] ?? '',
|
||||
postId: item['POST_ID'] ?? '',
|
||||
studentId: item['STUDENT_ID'] ?? '',
|
||||
),
|
||||
context,
|
||||
),
|
||||
),
|
||||
|
||||
// 立即学习
|
||||
|
|
@ -352,7 +477,7 @@ class _StudyMyTaskPageState extends State<StudyMyTaskPage> with RouteAware {
|
|||
backgroundColor: Colors.blue,
|
||||
onPressed: () {
|
||||
if (studyState == 0) {
|
||||
_onTapSign(item);
|
||||
widget.onTapSign(item);
|
||||
} else {
|
||||
pushPage(StudyClassListPage(item), context);
|
||||
}
|
||||
|
|
@ -369,12 +494,11 @@ class _StudyMyTaskPageState extends State<StudyMyTaskPage> with RouteAware {
|
|||
height: 36,
|
||||
text: "立即考试",
|
||||
margin: const EdgeInsets.symmetric(horizontal: 2),
|
||||
|
||||
padding: EdgeInsets.symmetric(horizontal: 15),
|
||||
borderRadius: 18,
|
||||
backgroundColor: Colors.green,
|
||||
onPressed:
|
||||
() => _startExam(item, TakeExamType.video_study),
|
||||
onPressed: () =>
|
||||
widget.startExam(item, TakeExamType.video_study),
|
||||
),
|
||||
// 考试详情
|
||||
if (stageExamState == 3)
|
||||
|
|
@ -382,7 +506,6 @@ class _StudyMyTaskPageState extends State<StudyMyTaskPage> with RouteAware {
|
|||
height: 36,
|
||||
text: "考试详情",
|
||||
margin: const EdgeInsets.symmetric(horizontal: 2),
|
||||
|
||||
padding: EdgeInsets.symmetric(horizontal: 15),
|
||||
borderRadius: 18,
|
||||
backgroundColor: Colors.green,
|
||||
|
|
@ -391,6 +514,7 @@ class _StudyMyTaskPageState extends State<StudyMyTaskPage> with RouteAware {
|
|||
VideoStudyDetailPage(
|
||||
studentId: item['STUDENT_ID'] ?? '',
|
||||
classId: item['CLASS_ID'] ?? '',
|
||||
state: 1,
|
||||
),
|
||||
context,
|
||||
);
|
||||
|
|
@ -404,65 +528,4 @@ class _StudyMyTaskPageState extends State<StudyMyTaskPage> with RouteAware {
|
|||
),
|
||||
);
|
||||
}
|
||||
/// 开始考试
|
||||
Future<void> _startExam(Map resData, TakeExamType type) async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
final arguments = {
|
||||
'STAGEEXAMPAPERINPUT_ID': resData['STAGEEXAMPAPERINPUT_ID'] ?? '',
|
||||
'STAGEEXAMPAPER_ID': resData['STAGEEXAMPAPER_ID'] ?? '',
|
||||
'CLASS_ID': resData['CLASS_ID'] ?? '',
|
||||
'POST_ID': resData['POST_ID'] ?? '',
|
||||
'STUDENT_ID': resData['STUDENT_ID'] ?? '',
|
||||
'NUMBEROFEXAMS': resData['NUMBEROFEXAMS'] ?? '',
|
||||
};
|
||||
print('--_startExam data---$arguments');
|
||||
|
||||
final data = await ApiService.getStartExam(arguments);
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
if (data['result'] == 'success') {
|
||||
pushPage(TakeExamPage(examInfo: {
|
||||
'CLASS_ID': resData['CLASS_ID'] ?? '',
|
||||
'POST_ID': resData['POST_ID'] ?? '',
|
||||
'STUDENT_ID': resData['STUDENT_ID'] ?? '',
|
||||
'STRENGTHEN_PAPER_QUESTION_ID': resData['STAGEEXAMPAPERINPUT_ID'] ?? '',
|
||||
...data
|
||||
}, examType: TakeExamType.video_study), context);
|
||||
|
||||
}else{
|
||||
ToastUtil.showError(context, '请求错误');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
bool _onScroll(ScrollNotification n) {
|
||||
if (n.metrics.pixels > n.metrics.maxScrollExtent - 100 &&
|
||||
_hasMore &&
|
||||
!_isLoading) {
|
||||
_getStudyList(loadMore: true);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: MyAppbar(title: '我的学习'),
|
||||
body: SafeArea(
|
||||
child: NotificationListener<ScrollNotification>(
|
||||
onNotification: _onScroll,
|
||||
child:
|
||||
_list.isEmpty
|
||||
? NoDataWidget.show()
|
||||
: ListView.builder(
|
||||
itemCount: _list.length,
|
||||
itemBuilder: (_, i) => _buildItem(_list[i]),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:qhd_prevention/pages/home/study/video_study_detail_page.dart';
|
||||
import 'package:qhd_prevention/pages/my_appbar.dart';
|
||||
import 'package:qhd_prevention/tools/h_colors.dart';
|
||||
import '../../../http/ApiService.dart';
|
||||
|
|
@ -126,7 +127,15 @@ class _StudyScorePageState extends State<StudyScorePage> {
|
|||
onTap: () {
|
||||
final state = item['STAGEEXAMSTATE'];
|
||||
if (state == '2' || state == '3') {
|
||||
Navigator.pushNamed(context, '/exam_details', arguments: item);
|
||||
// Navigator.pushNamed(context, '/exam_details', arguments: item);
|
||||
pushPage(
|
||||
VideoStudyDetailPage(
|
||||
studentId: item['STUDENT_ID'] ?? '',
|
||||
classId: item['CLASS_ID'] ?? '',
|
||||
state: 2,
|
||||
),
|
||||
context,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import 'package:qhd_prevention/pages/my_appbar.dart';
|
|||
import 'package:qhd_prevention/http/ApiService.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:qhd_prevention/tools/tools.dart';
|
||||
|
||||
class Question {
|
||||
final Map<String, dynamic> rawData;
|
||||
final String questionDry;
|
||||
|
|
@ -83,20 +85,36 @@ class _TakeExamPageState extends State<TakeExamPage> {
|
|||
.toList();
|
||||
|
||||
final numberOfExams = widget.examInfo['NUMBEROFEXAMS'];
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async{
|
||||
if (numberOfExams is int) {
|
||||
if (numberOfExams > 0) {
|
||||
} else if (numberOfExams == -9999) {
|
||||
_showTip('强化学习考试开始,限时${info['ANSWERSHEETTIME']}分钟,请注意答题时间!');
|
||||
} else {
|
||||
_showTip('您无考试次数!');
|
||||
|
||||
final ok = await CustomAlertDialog.showConfirm(
|
||||
context,
|
||||
title: '温馨提示',
|
||||
content: '您无考试次数!',
|
||||
);
|
||||
if (ok) {
|
||||
Navigator.pop(context);
|
||||
|
||||
};
|
||||
}
|
||||
} else if (numberOfExams is String) {
|
||||
if (numberOfExams == '-9999') {
|
||||
_showTip('强化学习考试开始,限时${info['ANSWERSHEETTIME']}分钟,请注意答题时间!');
|
||||
}
|
||||
} else {
|
||||
_showTip('您无考试次数!');
|
||||
final ok = await CustomAlertDialog.showConfirm(
|
||||
context,
|
||||
title: '温馨提示',
|
||||
content: '您无考试次数!',
|
||||
);
|
||||
if (ok) {
|
||||
Navigator.pop(context);
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -184,6 +202,7 @@ class _TakeExamPageState extends State<TakeExamPage> {
|
|||
}
|
||||
|
||||
Future<void> _submit() async {
|
||||
LoadingDialogHelper.show(message: '正在提交');
|
||||
for (var q in questions) {
|
||||
if (q.questionType == '2') q.checked = q.checked.replaceAll(',', '');
|
||||
q.rawData['checked'] = q.checked;
|
||||
|
|
@ -200,10 +219,11 @@ class _TakeExamPageState extends State<TakeExamPage> {
|
|||
'options': jsonEncode(questions.map((q) => q.rawData).toList()),
|
||||
};
|
||||
final res = await ApiService.submitExam(data);
|
||||
LoadingDialogHelper.hide();
|
||||
if (res['result'] == 'success') {
|
||||
final score = res['examScore'] ?? '0';
|
||||
final passed = res['examResult'] != '0';
|
||||
final ok = await CustomAlertDialog.showConfirm(
|
||||
await CustomAlertDialog.showConfirm(
|
||||
context,
|
||||
title: '温馨提示',
|
||||
content:
|
||||
|
|
@ -214,7 +234,6 @@ class _TakeExamPageState extends State<TakeExamPage> {
|
|||
confirmText: '确定',
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,8 +8,9 @@ import '../../../tools/tools.dart';
|
|||
class VideoStudyDetailPage extends StatefulWidget {
|
||||
final String studentId;
|
||||
final String classId;
|
||||
final int state;
|
||||
|
||||
const VideoStudyDetailPage({Key? key, required this.studentId, required this.classId}) : super(key: key);
|
||||
const VideoStudyDetailPage({Key? key, required this.studentId, required this.classId, required this.state}) : super(key: key);
|
||||
|
||||
@override
|
||||
_VideoStudyDetailPageState createState() => _VideoStudyDetailPageState();
|
||||
|
|
@ -103,8 +104,8 @@ class _VideoStudyDetailPageState extends State<VideoStudyDetailPage> {
|
|||
List<String> keys = q.questionType == '3' ? ['A', 'B'] : ['A', 'B', 'C', 'D'];
|
||||
return Column(
|
||||
children: keys.map((key) {
|
||||
bool isChecked = q.answer.split(',').contains(key);
|
||||
bool isCorrect = q.answerRight.split(',').contains(key);
|
||||
bool isChecked = q.answer.contains(key);
|
||||
bool isCorrect = q.answerRight.contains(key);
|
||||
bool right = q.answered && isCorrect && isChecked;
|
||||
bool err = q.answered && !isCorrect && isChecked;
|
||||
bool warn = q.answered && isCorrect && !isChecked;
|
||||
|
|
@ -161,7 +162,7 @@ class _VideoStudyDetailPageState extends State<VideoStudyDetailPage> {
|
|||
Widget build(BuildContext context) {
|
||||
final q = questions.isNotEmpty ? questions[current] : null;
|
||||
return Scaffold(
|
||||
appBar: MyAppbar(title: '课程练习详情'),
|
||||
appBar: MyAppbar(title: widget.state == 1 ? '课程练习详情' : "考试详情"),
|
||||
body: loading
|
||||
? Center(child: CircularProgressIndicator())
|
||||
: questions.isEmpty
|
||||
|
|
@ -215,7 +216,7 @@ class _VideoStudyDetailPageState extends State<VideoStudyDetailPage> {
|
|||
SizedBox(height: 16),
|
||||
_buildOptions(q),
|
||||
Divider(),
|
||||
Text('我的答案: ${_renderAnswerText(q)}'),
|
||||
Text('我的答案: ${q.answer}'),
|
||||
Text('正确答案: ${q.answerRight}'),
|
||||
Text('权威解读: ${q.descr}'),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -655,6 +655,39 @@ class ItemListWidget {
|
|||
),
|
||||
);
|
||||
}
|
||||
/// 单行布局:
|
||||
/// 标题 + 按钮(挨着)
|
||||
static Widget OneRowStartButtonTitle({
|
||||
required String label, // 标题
|
||||
String buttonText = '分析详情', // 按钮文字
|
||||
String text = '', // 标题
|
||||
required VoidCallback onTap, // 点击回调
|
||||
double fontSize = 14, // 字体大小
|
||||
Color btnColor = Colors.green,
|
||||
}) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: vertical_inset, horizontal: horizontal_inset),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: fontSize,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
CustomButton(
|
||||
text: buttonText,
|
||||
height: 30,
|
||||
padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 5),
|
||||
backgroundColor: btnColor,
|
||||
onPressed: onTap,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 单行布局:
|
||||
/// 标题 + 网络图片
|
||||
|
|
@ -957,7 +990,44 @@ class ItemListWidget {
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
/// 两行垂直布局:
|
||||
/// 标题
|
||||
/// 第二行:多行文本展示
|
||||
static Widget twoRowTitleText({
|
||||
required String label, // 第一行标题
|
||||
required String text, // 显示内容或提示
|
||||
double fontSize = 14, // 字体大小
|
||||
bool isRequired = true,
|
||||
}) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: vertical_inset, horizontal: horizontal_inset),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 第一行:标题
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: fontSize,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
text,
|
||||
maxLines: 5,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: fontSize,
|
||||
color: detailtextColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
static Widget itemContainer(Widget child, {double horizontal = horizontal_inset, double vertical = vertical_inset}) {
|
||||
|
||||
return Container(
|
||||
|
|
|
|||
|
|
@ -236,7 +236,7 @@ class MeasuresListWidget extends StatelessWidget {
|
|||
imageUrl,
|
||||
width: 40,
|
||||
height: 40,
|
||||
fit: BoxFit.cover,
|
||||
fit: BoxFit.fill,
|
||||
// 防止图片加载失败导致布局异常
|
||||
errorBuilder:
|
||||
(_, __, ___) => Container(
|
||||
|
|
@ -1017,7 +1017,7 @@ class _SelectionPopupState extends State<SelectionPopup> {
|
|||
borderRadius: BorderRadius.circular(5), // 圆角半径
|
||||
),
|
||||
backgroundColor: Colors.white,
|
||||
insetPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 40),
|
||||
insetPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 40),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: screenHeight(context),
|
||||
|
|
@ -1144,55 +1144,67 @@ class _SelectionPopupState extends State<SelectionPopup> {
|
|||
),
|
||||
// 列表多选
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 0),
|
||||
itemCount: list.length,
|
||||
itemBuilder: (c, i) {
|
||||
final item = list[i];
|
||||
final value = selectValue;
|
||||
final key =
|
||||
widget.type == 'assignments'
|
||||
? item['CHECK_NO'] as String? ?? ''
|
||||
: item['NAME'] as String? ?? '';
|
||||
final checked = value.contains(key);
|
||||
return CheckboxListTile(
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
// <-- 把图标放左边
|
||||
contentPadding: EdgeInsets.zero,
|
||||
// 可选:移除左右默认 padding,使图标贴近左侧
|
||||
activeColor: Colors.blue,
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(key, style: TextStyle(fontSize: 13)),
|
||||
if (widget.type == 'assignments') ...[
|
||||
Text(
|
||||
'作业内容: ${item['WORK_CONTENT'] ?? ''}',
|
||||
style: TextStyle(fontSize: 13),
|
||||
),
|
||||
Text(
|
||||
'作业负责人: ${item['CONFIRM_USER_NAME'] ?? ''}',
|
||||
style: TextStyle(fontSize: 13),
|
||||
),
|
||||
Text(
|
||||
'作业申请时间: ${item['CREATTIME'] ?? ''}',
|
||||
style: TextStyle(fontSize: 13),
|
||||
),
|
||||
],
|
||||
child:
|
||||
ListTileTheme(
|
||||
dense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 0, horizontal: 0),
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
itemCount: list.length,
|
||||
itemBuilder: (c, i) {
|
||||
final item = list[i];
|
||||
final value = selectValue;
|
||||
final key =
|
||||
widget.type == 'assignments'
|
||||
? item['CHECK_NO'] as String? ?? ''
|
||||
: item['NAME'] as String? ?? '';
|
||||
final checked = value.contains(key);
|
||||
return CheckboxListTile(
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 0, horizontal: 0),
|
||||
activeColor: Colors.blue,
|
||||
dense: true, // 更紧凑
|
||||
visualDensity: const VisualDensity(vertical: -4), // 调整垂直密度:-1..-4
|
||||
title: Column(
|
||||
mainAxisSize: MainAxisSize.min, // 避免 Column 占用多余高度
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
key,
|
||||
style: const TextStyle(fontSize: 13, height: 1.0),
|
||||
),
|
||||
if (widget.type == 'assignments') ...[
|
||||
const SizedBox(height: 4), // 子文本间距可更小
|
||||
Text(
|
||||
'作业内容: ${item['WORK_CONTENT'] ?? ''}',
|
||||
style: const TextStyle(fontSize: 13, height: 1.0),
|
||||
),
|
||||
Text(
|
||||
'作业负责人: ${item['CONFIRM_USER_NAME'] ?? ''}',
|
||||
style: const TextStyle(fontSize: 13, height: 1.0),
|
||||
),
|
||||
Text(
|
||||
'作业申请时间: ${item['CREATTIME'] ?? ''}',
|
||||
style: const TextStyle(fontSize: 13, height: 1.0),
|
||||
),
|
||||
],
|
||||
),
|
||||
value: checked,
|
||||
onChanged: (v) {
|
||||
setState(() {
|
||||
if (v == true)
|
||||
selectValue.add(key);
|
||||
else
|
||||
selectValue.remove(key);
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
value: checked,
|
||||
onChanged: (v) {
|
||||
setState(() {
|
||||
if (v == true)
|
||||
selectValue.add(key);
|
||||
else
|
||||
selectValue.remove(key);
|
||||
});
|
||||
},
|
||||
)
|
||||
;
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
),
|
||||
// 确定/关闭
|
||||
Padding(
|
||||
|
|
@ -1608,7 +1620,7 @@ class ConfirmWithSignWidget extends StatelessWidget {
|
|||
),
|
||||
|
||||
// 描述内容(当 descrText 有意义才显示)
|
||||
if (descrText.isNotEmpty && descrText != '无')
|
||||
if (descrText.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
child: Text(
|
||||
|
|
|
|||
|
|
@ -96,10 +96,26 @@ class _DangerousOptionsPageState extends State<DangerousOptionsPage> {
|
|||
LoadingDialogHelper.show();
|
||||
final res = await ApiService.uploadSaveFile(localPath);
|
||||
LoadingDialogHelper.hide();
|
||||
final url = res['FILE_PATH'] as String;
|
||||
setState(() {
|
||||
imgList.add(ImageData(localPath: localPath, serverPath: url));
|
||||
});
|
||||
try {
|
||||
if (res['result'] == 'success') {
|
||||
final url = res['FILE_PATH'] as String;
|
||||
setState(() {
|
||||
imgList.add(ImageData(localPath: localPath, serverPath: url));
|
||||
});
|
||||
} else {
|
||||
ToastUtil.showError(context, '上传失败,资源过大请重新选择');
|
||||
setState(() {
|
||||
imgList = [];
|
||||
MediaBus().emit(MediaEvent.clear(kAcceptVideoSectionKey));
|
||||
});
|
||||
}
|
||||
} catch (_) {
|
||||
ToastUtil.showError(context, '上传失败,资源过大请重新选择');
|
||||
setState(() {
|
||||
imgList = [];
|
||||
MediaBus().emit(MediaEvent.clear(kAcceptVideoSectionKey));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除图片处理:调用删除接口并更新列表
|
||||
|
|
@ -333,7 +349,8 @@ class _DangerousOptionsPageState extends State<DangerousOptionsPage> {
|
|||
onChanged: (paths) {},
|
||||
onMediaAdded: _onImageAdded,
|
||||
onMediaRemoved: (path) {
|
||||
final item = imgList.firstWhere((e) => e.localPath == path);
|
||||
print(path);
|
||||
final item = imgList.firstWhere((e) => path.contains(e.localPath) );
|
||||
_onImageRemoved(item);
|
||||
},
|
||||
onAiIdentify: () {},
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:intl/intl.dart';
|
||||
import 'package:qhd_prevention/customWidget/ItemWidgetFactory.dart';
|
||||
import 'package:qhd_prevention/customWidget/picker/CupertinoDatePicker.dart';
|
||||
import 'package:qhd_prevention/customWidget/toast_util.dart';
|
||||
|
||||
import '../../../../../../tools/tools.dart';
|
||||
import '../../../item_list_widget.dart';
|
||||
|
|
@ -166,13 +167,13 @@ class _HotWorkDetailFormWidgetState extends State<HotWorkDetailFormWidget> {
|
|||
hintText: '请选择动火人及证书编号',
|
||||
onTap: widget.onChooseHotworkUser,
|
||||
),
|
||||
if (FormUtils.hasValue(pd, 'CONFIRM_DEPARTMENT_NAME') &&
|
||||
if (FormUtils.hasValue(pd, 'UNIT_NAME') &&
|
||||
!widget.isEditable) ...[
|
||||
const Divider(),
|
||||
ItemListWidget.singleLineTitleText(
|
||||
label: '作业单位:',
|
||||
isEditable: false,
|
||||
text: pd['CONFIRM_DEPARTMENT_NAME'] ?? '',
|
||||
text: pd['UNIT_NAME'] ?? '',
|
||||
),
|
||||
],
|
||||
if (FormUtils.hasValue(pd, 'CONFIRM_USER_NAME') &&
|
||||
|
|
@ -209,7 +210,7 @@ class _HotWorkDetailFormWidgetState extends State<HotWorkDetailFormWidget> {
|
|||
},
|
||||
hintText: '请输入关联的其他特殊作业及安全作业票编号',
|
||||
controller: widget.relatedController,
|
||||
text: pd['SPECIAL_WORK'] ?? '',
|
||||
text: pd['SPECIAL_WORK'] ?? '无',
|
||||
),
|
||||
const Divider(),
|
||||
ItemListWidget.twoRowButtonTitleText(
|
||||
|
|
@ -239,15 +240,10 @@ class _HotWorkDetailFormWidgetState extends State<HotWorkDetailFormWidget> {
|
|||
if (FormUtils.hasValue(pd, 'WORK_START_DATE') &&
|
||||
!widget.isEditable) ...[
|
||||
const Divider(),
|
||||
ItemListWidget.selectableLineTitleTextRightButton(
|
||||
label: '动火作业实施时间:',
|
||||
isEditable: widget.isEditable,
|
||||
text:
|
||||
FormUtils.hasValue(pd, 'WORK_END_DATE')
|
||||
? '${pd['WORK_START_DATE'] ?? ''}\n-${pd['WORK_END_DATE'] ?? ''}'
|
||||
: pd['WORK_START_DATE'] ?? '',
|
||||
ItemListWidget.twoRowTitleText(label: '动火作业实施时间', text: FormUtils.hasValue(pd, 'WORK_END_DATE')
|
||||
? '${pd['WORK_START_DATE'] ?? ''} - ${pd['WORK_END_DATE'] ?? ''}'
|
||||
: pd['WORK_START_DATE'] ?? ''),
|
||||
|
||||
),
|
||||
if (!FormUtils.hasValue(pd, 'WORK_END_DATE'))
|
||||
...[
|
||||
const Divider(),
|
||||
|
|
@ -380,6 +376,7 @@ class _HotWorkDetailFormWidgetState extends State<HotWorkDetailFormWidget> {
|
|||
isRequired: pd['WORK_LEVEL'] == '特级',
|
||||
isEditable: widget.isEditable,
|
||||
onTapClean: () {
|
||||
ToastUtil.showNormal(context, '已清除');
|
||||
setState(() {
|
||||
pd['VIDEONAME'] = '';
|
||||
pd['VIDEOMANAGER_ID'] = '';
|
||||
|
|
@ -391,7 +388,7 @@ class _HotWorkDetailFormWidgetState extends State<HotWorkDetailFormWidget> {
|
|||
|
||||
if (FormUtils.hasValue(pd, 'ANALYZE_TIME') && !widget.isEditable) ...[
|
||||
const Divider(),
|
||||
ItemListWidget.OneRowButtonTitleText(
|
||||
ItemListWidget.OneRowStartButtonTitle(
|
||||
label: '气体分析信息:',
|
||||
text: pd['ANALYZE_USER_NAME'] ?? '',
|
||||
onTap: widget.onAnalyzeTap,
|
||||
|
|
|
|||
|
|
@ -63,10 +63,26 @@ class _HotworkAqjdDetailState extends State<HotworkAqjdDetail> {
|
|||
LoadingDialogHelper.show();
|
||||
final res = await ApiService.uploadSaveFile(localPath);
|
||||
LoadingDialogHelper.hide();
|
||||
final url = res['FILE_PATH'] as String;
|
||||
setState(() {
|
||||
imgList.add(ImageData(localPath: localPath, serverPath: url));
|
||||
});
|
||||
try {
|
||||
if (res['result'] == 'success') {
|
||||
final url = res['FILE_PATH'] as String;
|
||||
setState(() {
|
||||
imgList.add(ImageData(localPath: localPath, serverPath: url));
|
||||
});
|
||||
} else {
|
||||
ToastUtil.showError(context, '上传失败,资源过大请重新选择');
|
||||
setState(() {
|
||||
imgList = [];
|
||||
MediaBus().emit(MediaEvent.clear(kAcceptVideoSectionKey));
|
||||
});
|
||||
}
|
||||
} catch (_) {
|
||||
ToastUtil.showError(context, '上传失败,资源过大请重新选择');
|
||||
setState(() {
|
||||
imgList = [];
|
||||
MediaBus().emit(MediaEvent.clear(kAcceptVideoSectionKey));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除图片处理:调用删除接口并更新列表
|
||||
|
|
|
|||
|
|
@ -203,18 +203,21 @@ class _HotworkApplyDetailState extends State<HotworkApplyDetail> {
|
|||
setState(() {
|
||||
List<String> names = [];
|
||||
List<String> ids = [];
|
||||
|
||||
List<String> units = [];
|
||||
for (String name in res) {
|
||||
for (final item in workUserList) {
|
||||
if (item['NAME'] == name) {
|
||||
names.add(item['UNAME']);
|
||||
ids.add(item['PERSONNELMANAGEMENT_ID']);
|
||||
units.add(item['UNIT_NAME']);
|
||||
}
|
||||
}
|
||||
}
|
||||
pd['WORK_USER_NAME'] = names.join(',');
|
||||
pd['WORK_USER_ID'] = ids.join(',');
|
||||
pd['WORK_USER'] = res.join(',');
|
||||
pd['UNIT_NAME'] = units.first;
|
||||
|
||||
print('${pd['WORK_USER_NAME']}\n${pd['WORK_USER_ID']}\n${pd['WORK_USER']}');
|
||||
});
|
||||
} else {
|
||||
|
|
@ -383,6 +386,7 @@ class _HotworkApplyDetailState extends State<HotworkApplyDetail> {
|
|||
isEditable: isEditable,
|
||||
isClean: isClean,
|
||||
onTapClean: () {
|
||||
ToastUtil.showNormal(context, '已清除');
|
||||
setState(() {
|
||||
pd['${type.name}_DEPARTMENT_NAME'] = '';
|
||||
pd['${type.name}_USER_NAME'] = '';
|
||||
|
|
|
|||
|
|
@ -162,9 +162,7 @@ class _HotWorkListPageState extends State<HotWorkListPage> {
|
|||
});
|
||||
} catch (e) {
|
||||
print('Error fetching flow data: $e');
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('获取流程图失败: $e')));
|
||||
ToastUtil.showNormal(context, '获取流程图失败');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -499,14 +497,11 @@ class _HotWorkListPageState extends State<HotWorkListPage> {
|
|||
// 显示底部选择器
|
||||
Future<void> _showStepPicker() async {
|
||||
if (stepList.isEmpty) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('正在加载步骤数据,请稍后...')));
|
||||
LoadingDialogHelper.show();
|
||||
await _fetchSteps();
|
||||
LoadingDialogHelper.hide();
|
||||
if (stepList.isEmpty) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('无法加载步骤数据')));
|
||||
ToastUtil.showNormal(context, '无法加载数据');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -239,10 +239,6 @@ class _HotworkSetSafeDetailState extends State<HotworkSetSafeDetail> {
|
|||
|
||||
/// 作废 -1 通过 1
|
||||
Future<void> _submit(String status) async {
|
||||
if (imagePaths.isEmpty) {
|
||||
ToastUtil.showNormal(context, '请签字');
|
||||
return;
|
||||
}
|
||||
List<Map<String, dynamic>> signers = [];
|
||||
String reasonText = '';
|
||||
|
||||
|
|
@ -495,23 +491,7 @@ class _HotworkSetSafeDetailState extends State<HotworkSetSafeDetail> {
|
|||
),
|
||||
);
|
||||
}),
|
||||
SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
SizedBox(),
|
||||
CustomButton(
|
||||
text: '新增手写签字',
|
||||
height: 36,
|
||||
backgroundColor: Colors.green,
|
||||
onPressed: () {
|
||||
_sign();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
if (imagePaths.isNotEmpty) _signListWidget(),
|
||||
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -70,10 +70,26 @@ class _HotworkYsgdDetailState extends State<HotworkYsgdDetail> {
|
|||
LoadingDialogHelper.show();
|
||||
final res = await ApiService.uploadSaveFile(localPath);
|
||||
LoadingDialogHelper.hide();
|
||||
final url = res['FILE_PATH'] as String;
|
||||
setState(() {
|
||||
imgList.add(ImageData(localPath: localPath, serverPath: url));
|
||||
});
|
||||
try {
|
||||
if (res['result'] == 'success') {
|
||||
final url = res['FILE_PATH'] as String;
|
||||
setState(() {
|
||||
imgList.add(ImageData(localPath: localPath, serverPath: url));
|
||||
});
|
||||
} else {
|
||||
ToastUtil.showError(context, '上传失败,资源过大请重新选择');
|
||||
setState(() {
|
||||
imgList = [];
|
||||
MediaBus().emit(MediaEvent.clear(kAcceptVideoSectionKey));
|
||||
});
|
||||
}
|
||||
} catch (_) {
|
||||
ToastUtil.showError(context, '上传失败,资源过大请重新选择');
|
||||
setState(() {
|
||||
imgList = [];
|
||||
MediaBus().emit(MediaEvent.clear(kAcceptVideoSectionKey));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除图片处理:调用删除接口并更新列表
|
||||
|
|
@ -190,11 +206,7 @@ class _HotworkYsgdDetailState extends State<HotworkYsgdDetail> {
|
|||
ToastUtil.showNormal(context, '请在作业结束30分钟内完成验收');
|
||||
return;
|
||||
}
|
||||
if (imgList.isEmpty) {
|
||||
ToastUtil.showNormal(context, '请上传验收视频');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
} else {
|
||||
reasonText = await CustomAlertDialog.showInput(
|
||||
context,
|
||||
|
|
@ -383,7 +395,7 @@ class _HotworkYsgdDetailState extends State<HotworkYsgdDetail> {
|
|||
),
|
||||
Divider(),
|
||||
RepairedPhotoSection(
|
||||
isRequired: true,
|
||||
isRequired: false,
|
||||
title: '验收视频',
|
||||
maxCount: 1,
|
||||
mediaType: MediaType.video,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'package:intl/intl.dart';
|
|||
import 'package:qhd_prevention/customWidget/ItemWidgetFactory.dart';
|
||||
import 'package:qhd_prevention/customWidget/picker/CupertinoDatePicker.dart';
|
||||
import 'package:qhd_prevention/customWidget/single_image_viewer.dart';
|
||||
import 'package:qhd_prevention/customWidget/toast_util.dart';
|
||||
import 'package:qhd_prevention/http/ApiService.dart';
|
||||
|
||||
import '../../../../../../tools/tools.dart';
|
||||
|
|
@ -190,7 +191,7 @@ class _CutroadDetailFormWidgetState extends State<CutroadDetailFormWidget> {
|
|||
},
|
||||
hintText: '请输入关联的其他特殊作业及安全作业票编号',
|
||||
controller: widget.relatedController,
|
||||
text: pd['SPECIAL_WORK'] ?? '',
|
||||
text: pd['SPECIAL_WORK'] ?? '无',
|
||||
),
|
||||
const Divider(),
|
||||
if (FormUtils.hasValue(signs, 'PROJECT_MANAGER') &&
|
||||
|
|
@ -235,14 +236,10 @@ class _CutroadDetailFormWidgetState extends State<CutroadDetailFormWidget> {
|
|||
),
|
||||
if (FormUtils.hasValue(pd, 'WORK_START_DATE')) ...[
|
||||
const Divider(),
|
||||
ItemListWidget.selectableLineTitleTextRightButton(
|
||||
label: '断路作业实施时间:',
|
||||
isEditable: widget.isEditable,
|
||||
text:
|
||||
FormUtils.hasValue(pd, 'WORK_END_DATE')
|
||||
? '${pd['WORK_START_DATE'] ?? ''}\n-${pd['WORK_END_DATE'] ?? ''}'
|
||||
: pd['WORK_START_DATE'] ?? '',
|
||||
),
|
||||
ItemListWidget.twoRowTitleText(label: '段路作业实施时间', text: FormUtils.hasValue(pd, 'WORK_END_DATE')
|
||||
? '${pd['WORK_START_DATE'] ?? ''} - ${pd['WORK_END_DATE'] ?? ''}'
|
||||
: pd['WORK_START_DATE'] ?? ''),
|
||||
|
||||
] else ...[
|
||||
const Divider(),
|
||||
ItemListWidget.selectableLineTitleTextRightButton(
|
||||
|
|
@ -348,6 +345,7 @@ class _CutroadDetailFormWidgetState extends State<CutroadDetailFormWidget> {
|
|||
isClean: widget.isEditable,
|
||||
cleanText: '清除监控',
|
||||
onTapClean: () {
|
||||
ToastUtil.showNormal(context, '已清除');
|
||||
setState(() {
|
||||
pd['VIDEONAME'] = '';
|
||||
pd['VIDEOMANAGER_ID'] = '';
|
||||
|
|
|
|||
|
|
@ -62,10 +62,26 @@ class _CutroadAqjdDetailState extends State<CutroadAqjdDetail> {
|
|||
LoadingDialogHelper.show();
|
||||
final res = await ApiService.uploadSaveFile(localPath);
|
||||
LoadingDialogHelper.hide();
|
||||
final url = res['FILE_PATH'] as String;
|
||||
setState(() {
|
||||
imgList.add(ImageData(localPath: localPath, serverPath: url));
|
||||
});
|
||||
try {
|
||||
if (res['result'] == 'success') {
|
||||
final url = res['FILE_PATH'] as String;
|
||||
setState(() {
|
||||
imgList.add(ImageData(localPath: localPath, serverPath: url));
|
||||
});
|
||||
} else {
|
||||
ToastUtil.showError(context, '上传失败,资源过大请重新选择');
|
||||
setState(() {
|
||||
imgList = [];
|
||||
MediaBus().emit(MediaEvent.clear(kAcceptVideoSectionKey));
|
||||
});
|
||||
}
|
||||
} catch (_) {
|
||||
ToastUtil.showError(context, '上传失败,资源过大请重新选择');
|
||||
setState(() {
|
||||
imgList = [];
|
||||
MediaBus().emit(MediaEvent.clear(kAcceptVideoSectionKey));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除图片处理:调用删除接口并更新列表
|
||||
|
|
|
|||
|
|
@ -170,9 +170,8 @@ class _CutroadListPageState extends State<CutroadListPage> {
|
|||
});
|
||||
} catch (e) {
|
||||
print('Error fetching flow data: $e');
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('获取流程图失败: $e')));
|
||||
|
||||
ToastUtil.showNormal(context, '获取流程图失败');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -445,14 +444,11 @@ class _CutroadListPageState extends State<CutroadListPage> {
|
|||
// 显示底部选择器
|
||||
Future<void> _showStepPicker() async {
|
||||
if (stepList.isEmpty) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('正在加载步骤数据,请稍后...')));
|
||||
LoadingDialogHelper.show();
|
||||
await _fetchSteps();
|
||||
LoadingDialogHelper.hide();
|
||||
if (stepList.isEmpty) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('无法加载步骤数据')));
|
||||
ToastUtil.showNormal(context, '无法加载数据');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -176,6 +176,7 @@ class _CutroadApplyDetailState extends State<CutroadApplyDetail> {
|
|||
isEditable: isEditable,
|
||||
isClean: isClean,
|
||||
onTapClean: () {
|
||||
ToastUtil.showNormal(context, '已清除');
|
||||
setState(() {
|
||||
pd['${type.name}_DEPARTMENT_NAME'] = '';
|
||||
pd['${type.name}_USER_NAME'] = '';
|
||||
|
|
|
|||
|
|
@ -297,10 +297,7 @@ class _CutroadSetSafeDetailState extends State<CutroadSetSafeDetail> {
|
|||
|
||||
/// 作废 -1 通过 1
|
||||
Future<void> _submit(String status) async {
|
||||
if (imagePaths.isEmpty) {
|
||||
ToastUtil.showNormal(context, '请签字');
|
||||
return;
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> signers = [];
|
||||
String reasonText = '';
|
||||
|
||||
|
|
@ -530,23 +527,7 @@ class _CutroadSetSafeDetailState extends State<CutroadSetSafeDetail> {
|
|||
),
|
||||
);
|
||||
}),
|
||||
SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
SizedBox(),
|
||||
CustomButton(
|
||||
text: '新增手写签字',
|
||||
height: 36,
|
||||
backgroundColor: Colors.green,
|
||||
onPressed: () {
|
||||
_sign();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
if (imagePaths.isNotEmpty) _signListWidget(),
|
||||
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -69,10 +69,26 @@ class _CutroadYsgdDetailState extends State<CutroadYsgdDetail> {
|
|||
LoadingDialogHelper.show();
|
||||
final res = await ApiService.uploadSaveFile(localPath);
|
||||
LoadingDialogHelper.hide();
|
||||
final url = res['FILE_PATH'] as String;
|
||||
setState(() {
|
||||
imgList.add(ImageData(localPath: localPath, serverPath: url));
|
||||
});
|
||||
try {
|
||||
if (res['result'] == 'success') {
|
||||
final url = res['FILE_PATH'] as String;
|
||||
setState(() {
|
||||
imgList.add(ImageData(localPath: localPath, serverPath: url));
|
||||
});
|
||||
} else {
|
||||
ToastUtil.showError(context, '上传失败,资源过大请重新选择');
|
||||
setState(() {
|
||||
imgList = [];
|
||||
MediaBus().emit(MediaEvent.clear(kAcceptVideoSectionKey));
|
||||
});
|
||||
}
|
||||
} catch (_) {
|
||||
ToastUtil.showError(context, '上传失败,资源过大请重新选择');
|
||||
setState(() {
|
||||
imgList = [];
|
||||
MediaBus().emit(MediaEvent.clear(kAcceptVideoSectionKey));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除图片处理:调用删除接口并更新列表
|
||||
|
|
@ -189,11 +205,7 @@ class _CutroadYsgdDetailState extends State<CutroadYsgdDetail> {
|
|||
ToastUtil.showNormal(context, '请在作业结束30分钟内完成验收');
|
||||
return;
|
||||
}
|
||||
if (imgList.isEmpty) {
|
||||
ToastUtil.showNormal(context, '请上传验收视频');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
} else {
|
||||
reasonText = await CustomAlertDialog.showInput(
|
||||
context,
|
||||
|
|
@ -374,7 +386,7 @@ class _CutroadYsgdDetailState extends State<CutroadYsgdDetail> {
|
|||
),
|
||||
Divider(),
|
||||
RepairedPhotoSection(
|
||||
isRequired: true,
|
||||
isRequired: false,
|
||||
title: '验收视频',
|
||||
maxCount: 1,
|
||||
mediaType: MediaType.video,
|
||||
|
|
|
|||
|
|
@ -253,10 +253,26 @@ class _CutroadZyrDetailState extends State<CutroadZyrDetail> {
|
|||
LoadingDialogHelper.show();
|
||||
final res = await ApiService.uploadSaveFile(localPath);
|
||||
LoadingDialogHelper.hide();
|
||||
final url = res['FILE_PATH'] as String;
|
||||
setState(() {
|
||||
imgList.add(ImageData(localPath: localPath, serverPath: url));
|
||||
});
|
||||
try {
|
||||
if (res['result'] == 'success') {
|
||||
final url = res['FILE_PATH'] as String;
|
||||
setState(() {
|
||||
imgList.add(ImageData(localPath: localPath, serverPath: url));
|
||||
});
|
||||
} else {
|
||||
ToastUtil.showError(context, '上传失败,资源过大请重新选择');
|
||||
setState(() {
|
||||
imgList = [];
|
||||
MediaBus().emit(MediaEvent.clear(kAcceptVideoSectionKey));
|
||||
});
|
||||
}
|
||||
} catch (_) {
|
||||
ToastUtil.showError(context, '上传失败,资源过大请重新选择');
|
||||
setState(() {
|
||||
imgList = [];
|
||||
MediaBus().emit(MediaEvent.clear(kAcceptVideoSectionKey));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除图片处理:调用删除接口并更新列表
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'package:intl/intl.dart';
|
|||
import 'package:qhd_prevention/customWidget/picker/CupertinoDatePicker.dart';
|
||||
import 'package:qhd_prevention/customWidget/ItemWidgetFactory.dart';
|
||||
import 'package:qhd_prevention/customWidget/single_image_viewer.dart';
|
||||
import 'package:qhd_prevention/customWidget/toast_util.dart';
|
||||
import 'package:qhd_prevention/http/ApiService.dart';
|
||||
import 'package:qhd_prevention/tools/h_colors.dart';
|
||||
|
||||
|
|
@ -141,17 +142,17 @@ class _BreakgroundDetailFormWidgetState
|
|||
hintText: '请输入作业内容',
|
||||
text: pd['JOB_CONTENT'] ?? '',
|
||||
),
|
||||
if (FormUtils.hasValue(pd, 'GUARDIAN_USER_NAME') &&
|
||||
!widget.isEditable) ...[
|
||||
const Divider(),
|
||||
// ItemListWidget.OneRowImageTitle(label: '作业负责人', imgPath: imgPath)
|
||||
SignRowImageTitle(
|
||||
label: '监护人:',
|
||||
signKey: 'GUARDIAN',
|
||||
signs: widget.signs,
|
||||
text: pd['GUARDIAN_USER_NAME'] ?? '',
|
||||
),
|
||||
],
|
||||
// if (FormUtils.hasValue(pd, 'GUARDIAN_USER_NAME') &&
|
||||
// !widget.isEditable) ...[
|
||||
// const Divider(),
|
||||
// // ItemListWidget.OneRowImageTitle(label: '作业负责人', imgPath: imgPath)
|
||||
// SignRowImageTitle(
|
||||
// label: '监护人:',
|
||||
// signKey: 'GUARDIAN',
|
||||
// signs: widget.signs,
|
||||
// text: pd['GUARDIAN_USER_NAME'] ?? '',
|
||||
// ),
|
||||
// ],
|
||||
if (FormUtils.hasValue(pd, 'CONFIRM_USER_NAME') &&
|
||||
!widget.isEditable) ...[
|
||||
const Divider(),
|
||||
|
|
@ -187,7 +188,7 @@ class _BreakgroundDetailFormWidgetState
|
|||
},
|
||||
hintText: '请输入关联的其他特殊作业及安全作业票编号',
|
||||
controller: widget.relatedController,
|
||||
text: pd['SPECIAL_WORK'] ?? '',
|
||||
text: pd['SPECIAL_WORK'] ?? '无',
|
||||
),
|
||||
|
||||
const Divider(),
|
||||
|
|
@ -258,14 +259,9 @@ class _BreakgroundDetailFormWidgetState
|
|||
),
|
||||
if (FormUtils.hasValue(pd, 'WORK_START_DATE')) ...[
|
||||
const Divider(),
|
||||
ItemListWidget.selectableLineTitleTextRightButton(
|
||||
label: '作业实施时间:',
|
||||
isEditable: widget.isEditable,
|
||||
text:
|
||||
FormUtils.hasValue(pd, 'WORK_END_DATE')
|
||||
? '${pd['WORK_START_DATE'] ?? ''}\n-${pd['WORK_END_DATE'] ?? ''}'
|
||||
: pd['WORK_START_DATE'] ?? '',
|
||||
),
|
||||
ItemListWidget.twoRowTitleText(label: '作业实施时间', text: FormUtils.hasValue(pd, 'WORK_END_DATE')
|
||||
? '${pd['WORK_START_DATE'] ?? ''} - ${pd['WORK_END_DATE'] ?? ''}'
|
||||
: pd['WORK_START_DATE'] ?? ''),
|
||||
] else ...[
|
||||
const Divider(),
|
||||
ItemListWidget.selectableLineTitleTextRightButton(
|
||||
|
|
@ -371,6 +367,7 @@ class _BreakgroundDetailFormWidgetState
|
|||
isClean: widget.isEditable,
|
||||
cleanText: '清除监控',
|
||||
onTapClean: () {
|
||||
ToastUtil.showNormal(context, '已清除');
|
||||
setState(() {
|
||||
pd['VIDEONAME'] = '';
|
||||
pd['VIDEOMANAGER_ID'] = '';
|
||||
|
|
|
|||
|
|
@ -62,10 +62,26 @@ class _BreakgroundAqjdDetailState extends State<BreakgroundAqjdDetail> {
|
|||
LoadingDialogHelper.show();
|
||||
final res = await ApiService.uploadSaveFile(localPath);
|
||||
LoadingDialogHelper.hide();
|
||||
final url = res['FILE_PATH'] as String;
|
||||
setState(() {
|
||||
imgList.add(ImageData(localPath: localPath, serverPath: url));
|
||||
});
|
||||
try {
|
||||
if (res['result'] == 'success') {
|
||||
final url = res['FILE_PATH'] as String;
|
||||
setState(() {
|
||||
imgList.add(ImageData(localPath: localPath, serverPath: url));
|
||||
});
|
||||
} else {
|
||||
ToastUtil.showError(context, '上传失败,资源过大请重新选择');
|
||||
setState(() {
|
||||
imgList = [];
|
||||
MediaBus().emit(MediaEvent.clear(kAcceptVideoSectionKey));
|
||||
});
|
||||
}
|
||||
} catch (_) {
|
||||
ToastUtil.showError(context, '上传失败,资源过大请重新选择');
|
||||
setState(() {
|
||||
imgList = [];
|
||||
MediaBus().emit(MediaEvent.clear(kAcceptVideoSectionKey));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除图片处理:调用删除接口并更新列表
|
||||
|
|
|
|||
|
|
@ -167,9 +167,8 @@ class _BreakgroundListPageState extends State<BreakgroundListPage> {
|
|||
});
|
||||
} catch (e) {
|
||||
print('Error fetching flow data: $e');
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('获取流程图失败: $e')));
|
||||
|
||||
ToastUtil.showNormal(context, '获取流程图失败');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -558,18 +557,16 @@ class _BreakgroundListPageState extends State<BreakgroundListPage> {
|
|||
// 显示底部选择器
|
||||
Future<void> _showStepPicker() async {
|
||||
if (stepList.isEmpty) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('正在加载步骤数据,请稍后...')));
|
||||
LoadingDialogHelper.show();
|
||||
await _fetchSteps();
|
||||
LoadingDialogHelper.hide();
|
||||
if (stepList.isEmpty) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('无法加载步骤数据')));
|
||||
ToastUtil.showNormal(context, '无法加载数据');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 创建选项列表
|
||||
final options = stepList.map((e) => e['STEP_NAME'] as String).toList();
|
||||
|
||||
|
|
|
|||
|
|
@ -325,6 +325,7 @@ class _BreakgroundApplyDetailState extends State<BreakgroundApplyDetail> {
|
|||
isEditable: isEditable,
|
||||
isClean: isClean,
|
||||
onTapClean: () {
|
||||
ToastUtil.showNormal(context, '已清除');
|
||||
setState(() {
|
||||
pd['${type.name}_DEPARTMENT_NAME'] = '';
|
||||
pd['${type.name}_USER_NAME'] = '';
|
||||
|
|
|
|||
|
|
@ -239,10 +239,7 @@ class _BreakgroundSetSafeDetailState extends State<BreakgroundSetSafeDetail> {
|
|||
|
||||
/// 作废 -1 通过 1
|
||||
Future<void> _submit(String status) async {
|
||||
if (imagePaths.isEmpty) {
|
||||
ToastUtil.showNormal(context, '请签字');
|
||||
return;
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> signers = [];
|
||||
String reasonText = '';
|
||||
|
||||
|
|
@ -506,23 +503,7 @@ class _BreakgroundSetSafeDetailState extends State<BreakgroundSetSafeDetail> {
|
|||
),
|
||||
);
|
||||
}),
|
||||
SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
SizedBox(),
|
||||
CustomButton(
|
||||
text: '新增手写签字',
|
||||
height: 36,
|
||||
backgroundColor: Colors.green,
|
||||
onPressed: () {
|
||||
_sign();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
if (imagePaths.isNotEmpty) _signListWidget(),
|
||||
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -70,10 +70,26 @@ class _BreakgroundYsgdDetailState extends State<BreakgroundYsgdDetail> {
|
|||
LoadingDialogHelper.show();
|
||||
final res = await ApiService.uploadSaveFile(localPath);
|
||||
LoadingDialogHelper.hide();
|
||||
final url = res['FILE_PATH'] as String;
|
||||
setState(() {
|
||||
imgList.add(ImageData(localPath: localPath, serverPath: url));
|
||||
});
|
||||
try {
|
||||
if (res['result'] == 'success') {
|
||||
final url = res['FILE_PATH'] as String;
|
||||
setState(() {
|
||||
imgList.add(ImageData(localPath: localPath, serverPath: url));
|
||||
});
|
||||
} else {
|
||||
ToastUtil.showError(context, '上传失败,资源过大请重新选择');
|
||||
setState(() {
|
||||
imgList = [];
|
||||
MediaBus().emit(MediaEvent.clear(kAcceptVideoSectionKey));
|
||||
});
|
||||
}
|
||||
} catch (_) {
|
||||
ToastUtil.showError(context, '上传失败,资源过大请重新选择');
|
||||
setState(() {
|
||||
imgList = [];
|
||||
MediaBus().emit(MediaEvent.clear(kAcceptVideoSectionKey));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除图片处理:调用删除接口并更新列表
|
||||
|
|
@ -190,11 +206,7 @@ class _BreakgroundYsgdDetailState extends State<BreakgroundYsgdDetail> {
|
|||
ToastUtil.showNormal(context, '请在作业结束30分钟内完成验收');
|
||||
return;
|
||||
}
|
||||
if (imgList.isEmpty) {
|
||||
ToastUtil.showNormal(context, '请上传验收视频');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
} else {
|
||||
reasonText = await CustomAlertDialog.showInput(
|
||||
context,
|
||||
|
|
@ -377,7 +389,7 @@ class _BreakgroundYsgdDetailState extends State<BreakgroundYsgdDetail> {
|
|||
),
|
||||
Divider(),
|
||||
RepairedPhotoSection(
|
||||
isRequired: true,
|
||||
isRequired: false,
|
||||
title: '验收视频',
|
||||
maxCount: 1,
|
||||
mediaType: MediaType.video,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import 'dart:math' as math;
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:qhd_prevention/customWidget/ItemWidgetFactory.dart';
|
||||
import 'package:qhd_prevention/customWidget/single_image_viewer.dart';
|
||||
import 'package:qhd_prevention/customWidget/toast_util.dart';
|
||||
import 'package:qhd_prevention/http/ApiService.dart';
|
||||
import 'package:qhd_prevention/tools/h_colors.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
|
@ -166,26 +167,26 @@ class _HoistworkDetailFormWidgetState extends State<HoistWorkDetailFormWidget> {
|
|||
text: pd['WORK_USER_USER_NAME'] ?? '',
|
||||
),
|
||||
],
|
||||
if (FormUtils.hasValue(pd, 'SISUO_USER_NAME') &&
|
||||
!widget.isEditable) ...[
|
||||
const Divider(),
|
||||
SignRowImageTitle(
|
||||
label: '司索人:',
|
||||
signKey: 'SISUO',
|
||||
signs: widget.signs,
|
||||
text: pd['SISUO_USER_NAME'] ?? '',
|
||||
),
|
||||
],
|
||||
if (FormUtils.hasValue(pd, 'GUARDIAN_USER_NAME') &&
|
||||
!widget.isEditable) ...[
|
||||
const Divider(),
|
||||
SignRowImageTitle(
|
||||
label: '监护人:',
|
||||
signKey: 'GUARDIAN',
|
||||
signs: widget.signs,
|
||||
text: pd['GUARDIAN_USER_NAME'] ?? '',
|
||||
),
|
||||
],
|
||||
// if (FormUtils.hasValue(pd, 'SISUO_USER_NAME') &&
|
||||
// !widget.isEditable) ...[
|
||||
// const Divider(),
|
||||
// SignRowImageTitle(
|
||||
// label: '司索人:',
|
||||
// signKey: 'SISUO',
|
||||
// signs: widget.signs,
|
||||
// text: pd['SISUO_USER_NAME'] ?? '',
|
||||
// ),
|
||||
// ],
|
||||
// if (FormUtils.hasValue(pd, 'GUARDIAN_USER_NAME') &&
|
||||
// !widget.isEditable) ...[
|
||||
// const Divider(),
|
||||
// SignRowImageTitle(
|
||||
// label: '监护人:',
|
||||
// signKey: 'GUARDIAN',
|
||||
// signs: widget.signs,
|
||||
// text: pd['GUARDIAN_USER_NAME'] ?? '',
|
||||
// ),
|
||||
// ],
|
||||
if (FormUtils.hasValue(pd, 'PROJECT_MANAGER_USER_NAME') &&
|
||||
!widget.isEditable) ...[
|
||||
const Divider(),
|
||||
|
|
@ -197,25 +198,40 @@ class _HoistworkDetailFormWidgetState extends State<HoistWorkDetailFormWidget> {
|
|||
),
|
||||
],
|
||||
const Divider(),
|
||||
ItemListWidget.singleLineTitleText(
|
||||
label: '吊物质量(吨):',
|
||||
keyboardType: TextInputType.numberWithOptions(
|
||||
decimal: true, // 允许小数点
|
||||
signed: false, // 允许负号(如需)
|
||||
if (widget.isEditable) ...[
|
||||
ItemListWidget.singleLineTitleText(
|
||||
label: '吊物质量(吨):',
|
||||
keyboardType: TextInputType.numberWithOptions(
|
||||
decimal: true, // 允许小数点
|
||||
signed: false, // 允许负号(如需)
|
||||
),
|
||||
isEditable: widget.isEditable,
|
||||
controller: widget.hightController,
|
||||
hintText: '请输入吊物质量(吨)',
|
||||
text: pd['PART_WEIGHT'] ?? '',
|
||||
),
|
||||
isEditable: widget.isEditable,
|
||||
controller: widget.hightController,
|
||||
hintText: '请输入吊物质量(吨)',
|
||||
text: pd['PART_WEIGHT'] ?? '',
|
||||
),
|
||||
const Divider(),
|
||||
ItemListWidget.selectableLineTitleTextRightButton(
|
||||
label: '作业级别:',
|
||||
isEditable: false,
|
||||
onTap: widget.onChooseLevel,
|
||||
text: _getWorkLevelText(pd['WORK_LEVEL']),
|
||||
),
|
||||
const Divider(),
|
||||
const Divider(),
|
||||
ItemListWidget.selectableLineTitleTextRightButton(
|
||||
label: '作业级别:',
|
||||
isEditable: false,
|
||||
onTap: widget.onChooseLevel,
|
||||
text: _getWorkLevelText(pd['WORK_LEVEL']),
|
||||
),
|
||||
const Divider(),
|
||||
]else...[
|
||||
ItemListWidget.singleLineTitleText(
|
||||
label: '吊物质量(t)及作业级别:',
|
||||
keyboardType: TextInputType.numberWithOptions(
|
||||
decimal: true, // 允许小数点
|
||||
signed: false, // 允许负号(如需)
|
||||
),
|
||||
isEditable: widget.isEditable,
|
||||
controller: widget.hightController,
|
||||
text: '${pd['PART_WEIGHT'] ?? ''}t,${_getWorkLevelText(pd['WORK_LEVEL'])}',
|
||||
),
|
||||
const Divider(),
|
||||
],
|
||||
|
||||
|
||||
ItemListWidget.twoRowButtonTitleText(
|
||||
label: '关联其他特殊作业及安全作业票编号',
|
||||
|
|
@ -239,7 +255,7 @@ class _HoistworkDetailFormWidgetState extends State<HoistWorkDetailFormWidget> {
|
|||
},
|
||||
hintText: '请输入关联的其他特殊作业及安全作业票编号',
|
||||
controller: widget.relatedController,
|
||||
text: pd['SPECIAL_WORK'] ?? '',
|
||||
text: pd['SPECIAL_WORK'] ?? '无',
|
||||
),
|
||||
const Divider(),
|
||||
ItemListWidget.twoRowButtonTitleText(
|
||||
|
|
@ -267,14 +283,10 @@ class _HoistworkDetailFormWidgetState extends State<HoistWorkDetailFormWidget> {
|
|||
),
|
||||
if (FormUtils.hasValue(pd, 'WORK_START_DATE')) ...[
|
||||
const Divider(),
|
||||
ItemListWidget.selectableLineTitleTextRightButton(
|
||||
label: '作业实施时间:',
|
||||
isEditable: widget.isEditable,
|
||||
text:
|
||||
FormUtils.hasValue(pd, 'WORK_END_DATE')
|
||||
? '${pd['WORK_START_DATE'] ?? ''}\n-${pd['WORK_END_DATE'] ?? ''}'
|
||||
: pd['WORK_START_DATE'] ?? '',
|
||||
),
|
||||
ItemListWidget.twoRowTitleText(label: '作业实施时间', text: FormUtils.hasValue(pd, 'WORK_END_DATE')
|
||||
? '${pd['WORK_START_DATE'] ?? ''} - ${pd['WORK_END_DATE'] ?? ''}'
|
||||
: pd['WORK_START_DATE'] ?? ''),
|
||||
|
||||
] else ...[
|
||||
const Divider(),
|
||||
ItemListWidget.selectableLineTitleTextRightButton(
|
||||
|
|
@ -379,6 +391,7 @@ class _HoistworkDetailFormWidgetState extends State<HoistWorkDetailFormWidget> {
|
|||
isClean: widget.isEditable,
|
||||
cleanText: '清除监控',
|
||||
onTapClean: () {
|
||||
ToastUtil.showNormal(context, '已清除');
|
||||
setState(() {
|
||||
pd['VIDEONAME'] = '';
|
||||
pd['VIDEOMANAGER_ID'] = '';
|
||||
|
|
|
|||
|
|
@ -312,13 +312,13 @@ class SpecialWorkFormBaseWork extends StatelessWidget {
|
|||
headerTitle: '所在单位意见',
|
||||
roleTitle: '',
|
||||
),
|
||||
if (FormUtils.hasValue(signs, 'AUDIT'))
|
||||
if (FormUtils.hasValue(signs, 'REVIEWER'))
|
||||
ConfirmWithSignWidget(
|
||||
signs: signs,
|
||||
pd: pd,
|
||||
baseImgPath: baseImgPath,
|
||||
sectionKey: 'AUDIT',
|
||||
nameKey: 'AUDIT_USER_NAME',
|
||||
sectionKey: 'REVIEWER',
|
||||
nameKey: 'REVIEWER_USER_NAME',
|
||||
headerTitle: '审核部门意见',
|
||||
roleTitle: '',
|
||||
),
|
||||
|
|
|
|||
|
|
@ -62,10 +62,26 @@ class _HoistworkAqjdDetailState extends State<HoistworkAqjdDetail> {
|
|||
LoadingDialogHelper.show();
|
||||
final res = await ApiService.uploadSaveFile(localPath);
|
||||
LoadingDialogHelper.hide();
|
||||
final url = res['FILE_PATH'] as String;
|
||||
setState(() {
|
||||
imgList.add(ImageData(localPath: localPath, serverPath: url));
|
||||
});
|
||||
try {
|
||||
if (res['result'] == 'success') {
|
||||
final url = res['FILE_PATH'] as String;
|
||||
setState(() {
|
||||
imgList.add(ImageData(localPath: localPath, serverPath: url));
|
||||
});
|
||||
} else {
|
||||
ToastUtil.showError(context, '上传失败,资源过大请重新选择');
|
||||
setState(() {
|
||||
imgList = [];
|
||||
MediaBus().emit(MediaEvent.clear(kAcceptVideoSectionKey));
|
||||
});
|
||||
}
|
||||
} catch (_) {
|
||||
ToastUtil.showError(context, '上传失败,资源过大请重新选择');
|
||||
setState(() {
|
||||
imgList = [];
|
||||
MediaBus().emit(MediaEvent.clear(kAcceptVideoSectionKey));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除图片处理:调用删除接口并更新列表
|
||||
|
|
|
|||
|
|
@ -325,6 +325,7 @@ class _HoistworkApplyDetailState extends State<HoistworkApplyDetail> {
|
|||
isEditable: isEditable,
|
||||
isClean: isClean,
|
||||
onTapClean: () {
|
||||
ToastUtil.showNormal(context, '已清除');
|
||||
setState(() {
|
||||
pd['${type.name}_DEPARTMENT_NAME'] = '';
|
||||
pd['${type.name}_USER_NAME'] = '';
|
||||
|
|
@ -566,8 +567,10 @@ class _HoistworkApplyDetailState extends State<HoistworkApplyDetail> {
|
|||
|
||||
// 给所有输入框赋值
|
||||
_contentController.text = pd['WORK_CONTENT'] ?? '';
|
||||
_nameController.text = pd['TOOL_NAME'] ?? '';
|
||||
_contentController.text = pd['WORK_CONTENT'] ?? '';
|
||||
_locationController.text = pd['WORK_PLACE'] ?? '';
|
||||
_hightController.text = pd['WORK_HIGH'] ?? '';
|
||||
_hightController.text = pd['PART_WEIGHT'] ?? '';
|
||||
_relatedController.text = pd['SPECIAL_WORK'] ?? '';
|
||||
_riskController.text = pd['RISK_IDENTIFICATION'] ?? '';
|
||||
});
|
||||
|
|
|
|||
|
|
@ -164,9 +164,8 @@ class _HoistworkListPageState extends State<HoistworkListPage> {
|
|||
});
|
||||
} catch (e) {
|
||||
print('Error fetching flow data: $e');
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('获取流程图失败: $e')));
|
||||
|
||||
ToastUtil.showNormal(context, '获取流程图失败');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -449,14 +448,11 @@ class _HoistworkListPageState extends State<HoistworkListPage> {
|
|||
// 显示底部选择器
|
||||
Future<void> _showStepPicker() async {
|
||||
if (stepList.isEmpty) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('正在加载步骤数据,请稍后...')));
|
||||
LoadingDialogHelper.show();
|
||||
await _fetchSteps();
|
||||
LoadingDialogHelper.hide();
|
||||
if (stepList.isEmpty) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('无法加载步骤数据')));
|
||||
ToastUtil.showNormal(context, '无法加载数据');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -238,10 +238,7 @@ class _HoistworkSetSafeDetailState extends State<HoistworkSetSafeDetail> {
|
|||
|
||||
/// 作废 -1 通过 1
|
||||
Future<void> _submit(String status) async {
|
||||
if (imagePaths.isEmpty) {
|
||||
ToastUtil.showNormal(context, '请签字');
|
||||
return;
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> signers = [];
|
||||
String reasonText = '';
|
||||
|
||||
|
|
@ -505,23 +502,7 @@ class _HoistworkSetSafeDetailState extends State<HoistworkSetSafeDetail> {
|
|||
),
|
||||
);
|
||||
}),
|
||||
SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
SizedBox(),
|
||||
CustomButton(
|
||||
text: '新增手写签字',
|
||||
height: 36,
|
||||
backgroundColor: Colors.green,
|
||||
onPressed: () {
|
||||
_sign();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
if (imagePaths.isNotEmpty) _signListWidget(),
|
||||
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -70,10 +70,26 @@ class _HoistworkYsgdDetailState extends State<HoistworkYsgdDetail> {
|
|||
LoadingDialogHelper.show();
|
||||
final res = await ApiService.uploadSaveFile(localPath);
|
||||
LoadingDialogHelper.hide();
|
||||
final url = res['FILE_PATH'] as String;
|
||||
setState(() {
|
||||
imgList.add(ImageData(localPath: localPath, serverPath: url));
|
||||
});
|
||||
try {
|
||||
if (res['result'] == 'success') {
|
||||
final url = res['FILE_PATH'] as String;
|
||||
setState(() {
|
||||
imgList.add(ImageData(localPath: localPath, serverPath: url));
|
||||
});
|
||||
} else {
|
||||
ToastUtil.showError(context, '上传失败,资源过大请重新选择');
|
||||
setState(() {
|
||||
imgList = [];
|
||||
MediaBus().emit(MediaEvent.clear(kAcceptVideoSectionKey));
|
||||
});
|
||||
}
|
||||
} catch (_) {
|
||||
ToastUtil.showError(context, '上传失败,资源过大请重新选择');
|
||||
setState(() {
|
||||
imgList = [];
|
||||
MediaBus().emit(MediaEvent.clear(kAcceptVideoSectionKey));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除图片处理:调用删除接口并更新列表
|
||||
|
|
@ -190,11 +206,7 @@ class _HoistworkYsgdDetailState extends State<HoistworkYsgdDetail> {
|
|||
ToastUtil.showNormal(context, '请在作业结束30分钟内完成验收');
|
||||
return;
|
||||
}
|
||||
if (imgList.isEmpty) {
|
||||
ToastUtil.showNormal(context, '请上传验收视频');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
} else {
|
||||
reasonText = await CustomAlertDialog.showInput(
|
||||
context,
|
||||
|
|
@ -377,7 +389,7 @@ class _HoistworkYsgdDetailState extends State<HoistworkYsgdDetail> {
|
|||
),
|
||||
Divider(),
|
||||
RepairedPhotoSection(
|
||||
isRequired: true,
|
||||
isRequired: false,
|
||||
title: '验收视频',
|
||||
maxCount: 1,
|
||||
mediaType: MediaType.video,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import 'package:qhd_prevention/customWidget/ItemWidgetFactory.dart';
|
|||
import 'package:intl/intl.dart';
|
||||
import 'package:qhd_prevention/customWidget/picker/CupertinoDatePicker.dart';
|
||||
import 'package:qhd_prevention/customWidget/single_image_viewer.dart';
|
||||
import 'package:qhd_prevention/customWidget/toast_util.dart';
|
||||
import '../../../../../../tools/tools.dart';
|
||||
import '../../../item_list_widget.dart';
|
||||
import 'package:qhd_prevention/pages/home/tap/tabList/special_wrok/MeasuresListWidget.dart';
|
||||
|
|
@ -162,7 +163,7 @@ class _HighWorkDetailFormWidgetState extends State<HighWorkDetailFormWidget> {
|
|||
ItemListWidget.selectableLineTitleTextRightButton(
|
||||
label: '高处作业级别:',
|
||||
isTip: true,
|
||||
isEditable: false,
|
||||
isEditable: widget.isEditable,
|
||||
isRequired: true,
|
||||
onTap: widget.onChooseLevel,
|
||||
onTapTip: () {
|
||||
|
|
@ -191,16 +192,16 @@ class _HighWorkDetailFormWidgetState extends State<HighWorkDetailFormWidget> {
|
|||
text: pd['WORK_USER_USER_NAME'] ?? '',
|
||||
),
|
||||
],
|
||||
if (FormUtils.hasValue(pd, 'GUARDIAN_USER_NAME') &&
|
||||
!widget.isEditable) ...[
|
||||
const Divider(),
|
||||
SignRowImageTitle(
|
||||
label: '监护人:',
|
||||
signKey: 'GUARDIAN',
|
||||
signs: widget.signs,
|
||||
text: pd['GUARDIAN_USER_NAME'] ?? '',
|
||||
),
|
||||
],
|
||||
// if (FormUtils.hasValue(pd, 'GUARDIAN_USER_NAME') &&
|
||||
// !widget.isEditable) ...[
|
||||
// const Divider(),
|
||||
// SignRowImageTitle(
|
||||
// label: '监护人:',
|
||||
// signKey: 'GUARDIAN',
|
||||
// signs: widget.signs,
|
||||
// text: pd['GUARDIAN_USER_NAME'] ?? '',
|
||||
// ),
|
||||
// ],
|
||||
if (FormUtils.hasValue(pd, 'CONFIRM_USER_NAME') &&
|
||||
!widget.isEditable) ...[
|
||||
const Divider(),
|
||||
|
|
@ -235,7 +236,7 @@ class _HighWorkDetailFormWidgetState extends State<HighWorkDetailFormWidget> {
|
|||
},
|
||||
hintText: '请输入关联的其他特殊作业及安全作业票编号',
|
||||
controller: widget.relatedController,
|
||||
text: pd['SPECIAL_WORK'] ?? '',
|
||||
text: pd['SPECIAL_WORK'] ?? '无',
|
||||
),
|
||||
const Divider(),
|
||||
ItemListWidget.twoRowButtonTitleText(
|
||||
|
|
@ -264,31 +265,11 @@ class _HighWorkDetailFormWidgetState extends State<HighWorkDetailFormWidget> {
|
|||
),
|
||||
const Divider(),
|
||||
|
||||
ItemListWidget.selectableLineTitleTextRightButton(
|
||||
label: '作业视频监控:',
|
||||
isClean: widget.isEditable,
|
||||
cleanText: '清除监控',
|
||||
onTapClean: () {
|
||||
setState(() {
|
||||
pd['VIDEONAME'] = '';
|
||||
pd['VIDEOMANAGER_ID'] = '';
|
||||
});
|
||||
},
|
||||
isRequired: false,
|
||||
isEditable: widget.isEditable,
|
||||
onTap: widget.onChooseVideoManager ?? () {},
|
||||
text: pd['VIDEONAME'] ?? '',
|
||||
),
|
||||
if (FormUtils.hasValue(pd, 'WORK_START_DATE')) ...[
|
||||
const Divider(),
|
||||
ItemListWidget.selectableLineTitleTextRightButton(
|
||||
label: '作业实施时间:',
|
||||
isEditable: widget.isEditable,
|
||||
text:
|
||||
FormUtils.hasValue(pd, 'WORK_END_DATE')
|
||||
? '${pd['WORK_START_DATE'] ?? ''}\n-${pd['WORK_END_DATE'] ?? ''}'
|
||||
: pd['WORK_START_DATE'] ?? '',
|
||||
),
|
||||
ItemListWidget.twoRowTitleText(label: '作业实施时间', text: FormUtils.hasValue(pd, 'WORK_END_DATE')
|
||||
? '${pd['WORK_START_DATE'] ?? ''} - ${pd['WORK_END_DATE'] ?? ''}'
|
||||
: pd['WORK_START_DATE'] ?? ''),
|
||||
] else ...[
|
||||
const Divider(),
|
||||
ItemListWidget.selectableLineTitleTextRightButton(
|
||||
|
|
@ -386,7 +367,22 @@ class _HighWorkDetailFormWidgetState extends State<HighWorkDetailFormWidget> {
|
|||
hintText: '',
|
||||
text: pd['LATITUDE_LONGITUDE'] ?? (widget.isEditable ? '' : '无'),
|
||||
),
|
||||
|
||||
ItemListWidget.selectableLineTitleTextRightButton(
|
||||
label: '作业视频监控:',
|
||||
isClean: widget.isEditable,
|
||||
cleanText: '清除监控',
|
||||
onTapClean: () {
|
||||
ToastUtil.showNormal(context, '已清除');
|
||||
setState(() {
|
||||
pd['VIDEONAME'] = '';
|
||||
pd['VIDEOMANAGER_ID'] = '';
|
||||
});
|
||||
},
|
||||
isRequired: false,
|
||||
isEditable: widget.isEditable,
|
||||
onTap: widget.onChooseVideoManager ?? () {},
|
||||
text: pd['VIDEONAME'] ?? '',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -62,10 +62,26 @@ class _HighworkAqjdDetailState extends State<HighworkAqjdDetail> {
|
|||
LoadingDialogHelper.show();
|
||||
final res = await ApiService.uploadSaveFile(localPath);
|
||||
LoadingDialogHelper.hide();
|
||||
final url = res['FILE_PATH'] as String;
|
||||
setState(() {
|
||||
imgList.add(ImageData(localPath: localPath, serverPath: url));
|
||||
});
|
||||
try {
|
||||
if (res['result'] == 'success') {
|
||||
final url = res['FILE_PATH'] as String;
|
||||
setState(() {
|
||||
imgList.add(ImageData(localPath: localPath, serverPath: url));
|
||||
});
|
||||
} else {
|
||||
ToastUtil.showError(context, '上传失败,资源过大请重新选择');
|
||||
setState(() {
|
||||
imgList = [];
|
||||
MediaBus().emit(MediaEvent.clear(kAcceptVideoSectionKey));
|
||||
});
|
||||
}
|
||||
} catch (_) {
|
||||
ToastUtil.showError(context, '上传失败,资源过大请重新选择');
|
||||
setState(() {
|
||||
imgList = [];
|
||||
MediaBus().emit(MediaEvent.clear(kAcceptVideoSectionKey));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除图片处理:调用删除接口并更新列表
|
||||
|
|
|
|||
|
|
@ -115,21 +115,21 @@ class _HighworkApplyDetailState extends State<HighworkApplyDetail> {
|
|||
});
|
||||
_hightController.addListener(() {
|
||||
pd['WORK_HIGH'] = _hightController.text.trim();
|
||||
setState(() {
|
||||
double height = double.parse(pd['WORK_HIGH']);
|
||||
int level = 0;
|
||||
if (height >= 2.0 && height <= 5.0) {
|
||||
level = 1;
|
||||
}else if (height >= 5.0 && height <= 15.0) {
|
||||
level = 2;
|
||||
}else if (height >= 15.0 && height <= 30.0) {
|
||||
level = 3;
|
||||
}else{
|
||||
level = 4;
|
||||
}
|
||||
pd['WORK_LEVEL'] = level;
|
||||
pd['WORK_LEVEL_NAME'] = levelList[level-1].toString();
|
||||
});
|
||||
// setState(() {
|
||||
// double height = double.parse(pd['WORK_HIGH']);
|
||||
// int level = 0;
|
||||
// if (height >= 2.0 && height <= 5.0) {
|
||||
// level = 1;
|
||||
// }else if (height >= 5.0 && height <= 15.0) {
|
||||
// level = 2;
|
||||
// }else if (height >= 15.0 && height <= 30.0) {
|
||||
// level = 3;
|
||||
// }else{
|
||||
// level = 4;
|
||||
// }
|
||||
// pd['WORK_LEVEL'] = level;
|
||||
// pd['WORK_LEVEL_NAME'] = levelList[level-1].toString();
|
||||
// });
|
||||
});
|
||||
_relatedController.addListener(() {
|
||||
pd['SPECIAL_WORK'] = _relatedController.text.trim();
|
||||
|
|
@ -343,6 +343,7 @@ class _HighworkApplyDetailState extends State<HighworkApplyDetail> {
|
|||
isEditable: isEditable,
|
||||
isClean: isClean,
|
||||
onTapClean: () {
|
||||
ToastUtil.showNormal(context, '已清除');
|
||||
setState(() {
|
||||
pd['${type.name}_DEPARTMENT_NAME'] = '';
|
||||
pd['${type.name}_USER_NAME'] = '';
|
||||
|
|
|
|||
|
|
@ -163,9 +163,7 @@ class _HighworkListPageState extends State<HighworkListPage> {
|
|||
});
|
||||
} catch (e) {
|
||||
print('Error fetching flow data: $e');
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('获取流程图失败: $e')));
|
||||
ToastUtil.showNormal(context, '获取流程图失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -443,14 +441,11 @@ class _HighworkListPageState extends State<HighworkListPage> {
|
|||
// 显示底部选择器
|
||||
Future<void> _showStepPicker() async {
|
||||
if (stepList.isEmpty) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('正在加载步骤数据,请稍后...')));
|
||||
LoadingDialogHelper.show();
|
||||
await _fetchSteps();
|
||||
LoadingDialogHelper.hide();
|
||||
if (stepList.isEmpty) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('无法加载步骤数据')));
|
||||
ToastUtil.showNormal(context, '无法加载数据');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -296,10 +296,7 @@ class _HighworkSetSafeDetailState extends State<HighworkSetSafeDetail> {
|
|||
|
||||
/// 作废 -1 通过 1
|
||||
Future<void> _submit(String status) async {
|
||||
if (imagePaths.isEmpty) {
|
||||
ToastUtil.showNormal(context, '请签字');
|
||||
return;
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> signers = [];
|
||||
String reasonText = '';
|
||||
|
||||
|
|
@ -530,23 +527,7 @@ class _HighworkSetSafeDetailState extends State<HighworkSetSafeDetail> {
|
|||
),
|
||||
);
|
||||
}),
|
||||
SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
SizedBox(),
|
||||
CustomButton(
|
||||
text: '新增手写签字',
|
||||
height: 36,
|
||||
backgroundColor: Colors.green,
|
||||
onPressed: () {
|
||||
_sign();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
if (imagePaths.isNotEmpty) _signListWidget(),
|
||||
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -70,12 +70,28 @@ class _HighworkYsgdDetailState extends State<HighworkYsgdDetail> {
|
|||
LoadingDialogHelper.show();
|
||||
final res = await ApiService.uploadSaveFile(localPath);
|
||||
LoadingDialogHelper.hide();
|
||||
final url = res['FILE_PATH'] as String;
|
||||
setState(() {
|
||||
imgList.add(ImageData(localPath: localPath, serverPath: url));
|
||||
});
|
||||
try {
|
||||
if (res['result'] == 'success') {
|
||||
final url = res['FILE_PATH'] as String;
|
||||
setState(() {
|
||||
imgList.add(ImageData(localPath: localPath, serverPath: url));
|
||||
});
|
||||
} else {
|
||||
ToastUtil.showError(context, '上传失败,资源过大请重新选择');
|
||||
setState(() {
|
||||
imgList = [];
|
||||
MediaBus().emit(MediaEvent.clear(kAcceptVideoSectionKey));
|
||||
});
|
||||
}
|
||||
} catch (_) {
|
||||
ToastUtil.showError(context, '上传失败,资源过大请重新选择');
|
||||
setState(() {
|
||||
imgList = [];
|
||||
MediaBus().emit(MediaEvent.clear(kAcceptVideoSectionKey));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// 删除图片处理:调用删除接口并更新列表
|
||||
Future<void> _onImageRemoved(ImageData item) async {
|
||||
if (item.serverPath != null) {
|
||||
|
|
@ -190,11 +206,7 @@ class _HighworkYsgdDetailState extends State<HighworkYsgdDetail> {
|
|||
ToastUtil.showNormal(context, '请在作业结束30分钟内完成验收');
|
||||
return;
|
||||
}
|
||||
if (imgList.isEmpty) {
|
||||
ToastUtil.showNormal(context, '请上传验收视频');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
} else {
|
||||
reasonText = await CustomAlertDialog.showInput(
|
||||
context,
|
||||
|
|
@ -377,7 +389,7 @@ class _HighworkYsgdDetailState extends State<HighworkYsgdDetail> {
|
|||
),
|
||||
Divider(),
|
||||
RepairedPhotoSection(
|
||||
isRequired: true,
|
||||
isRequired: false,
|
||||
title: '验收视频',
|
||||
maxCount: 1,
|
||||
mediaType: MediaType.video,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:qhd_prevention/customWidget/ItemWidgetFactory.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:qhd_prevention/customWidget/picker/CupertinoDatePicker.dart';
|
||||
import 'package:qhd_prevention/customWidget/toast_util.dart';
|
||||
import '../../../../../../tools/tools.dart';
|
||||
import '../../../item_list_widget.dart';
|
||||
import 'package:qhd_prevention/pages/home/tap/tabList/special_wrok/MeasuresListWidget.dart';
|
||||
|
|
@ -238,7 +239,7 @@ class _ElectricityDetailFormWidgetState extends State<ElectricityDetailFormWidge
|
|||
},
|
||||
hintText: '请输入关联的其他特殊作业及安全作业票编号',
|
||||
controller: widget.relatedController,
|
||||
text: pd['SPECIAL_WORK'] ?? '',
|
||||
text: pd['SPECIAL_WORK'] ?? '无',
|
||||
),
|
||||
const Divider(),
|
||||
ItemListWidget.twoRowButtonTitleText(
|
||||
|
|
@ -266,14 +267,9 @@ class _ElectricityDetailFormWidgetState extends State<ElectricityDetailFormWidge
|
|||
),
|
||||
if (FormUtils.hasValue(pd, 'WORK_START_DATE')) ...[
|
||||
const Divider(),
|
||||
ItemListWidget.selectableLineTitleTextRightButton(
|
||||
label: '作业实施时间:',
|
||||
isEditable: widget.isEditable,
|
||||
text:
|
||||
FormUtils.hasValue(pd, 'WORK_END_DATE')
|
||||
? '${pd['WORK_START_DATE'] ?? ''}\n-${pd['WORK_END_DATE'] ?? ''}'
|
||||
: pd['WORK_START_DATE'] ?? '',
|
||||
),
|
||||
ItemListWidget.twoRowTitleText(label: '作业实施时间', text: FormUtils.hasValue(pd, 'WORK_END_DATE')
|
||||
? '${pd['WORK_START_DATE'] ?? ''} - ${pd['WORK_END_DATE'] ?? ''}'
|
||||
: pd['WORK_START_DATE'] ?? ''),
|
||||
] else ...[
|
||||
const Divider(),
|
||||
ItemListWidget.selectableLineTitleTextRightButton(
|
||||
|
|
@ -377,6 +373,7 @@ class _ElectricityDetailFormWidgetState extends State<ElectricityDetailFormWidge
|
|||
isClean: widget.isEditable,
|
||||
cleanText: '清除监控',
|
||||
onTapClean: () {
|
||||
ToastUtil.showNormal(context, '已清除');
|
||||
setState(() {
|
||||
pd['VIDEONAME'] = '';
|
||||
pd['VIDEOMANAGER_ID'] = '';
|
||||
|
|
@ -390,7 +387,7 @@ class _ElectricityDetailFormWidgetState extends State<ElectricityDetailFormWidge
|
|||
|
||||
if (FormUtils.hasValue(pd, 'ANALYZE_TIME') && !widget.isEditable) ...[
|
||||
const Divider(),
|
||||
ItemListWidget.OneRowButtonTitleText(
|
||||
ItemListWidget.OneRowStartButtonTitle(
|
||||
label: '气体分析信息:',
|
||||
text: pd['ANALYZE_USER_NAME'] ?? '',
|
||||
onTap: widget.onAnalyzeTap,
|
||||
|
|
|
|||
|
|
@ -265,26 +265,26 @@ class SpecialWorkFormBaseWork extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
|
||||
if (FormUtils.hasValue(signs, 'GUARDIAN'))
|
||||
ConfirmWithSignWidget(
|
||||
signs: signs,
|
||||
pd: pd,
|
||||
baseImgPath: baseImgPath,
|
||||
sectionKey: 'GUARDIAN',
|
||||
nameKey: 'GUARDIAN_USER_NAME',
|
||||
headerTitle: '监护人签字',
|
||||
roleTitle: '监护人',
|
||||
),
|
||||
if (FormUtils.hasValue(signs, 'ELECTRICITY'))
|
||||
ConfirmWithSignWidget(
|
||||
signs: signs,
|
||||
pd: pd,
|
||||
baseImgPath: baseImgPath,
|
||||
sectionKey: 'ELECTRICITY',
|
||||
nameKey: 'ELECTRICITY_USER_NAME',
|
||||
headerTitle: '用电人签字',
|
||||
roleTitle: '用电人',
|
||||
),
|
||||
// if (FormUtils.hasValue(signs, 'GUARDIAN'))
|
||||
// ConfirmWithSignWidget(
|
||||
// signs: signs,
|
||||
// pd: pd,
|
||||
// baseImgPath: baseImgPath,
|
||||
// sectionKey: 'GUARDIAN',
|
||||
// nameKey: 'GUARDIAN_USER_NAME',
|
||||
// headerTitle: '监护人签字',
|
||||
// roleTitle: '监护人',
|
||||
// ),
|
||||
// if (FormUtils.hasValue(signs, 'ELECTRICITY'))
|
||||
// ConfirmWithSignWidget(
|
||||
// signs: signs,
|
||||
// pd: pd,
|
||||
// baseImgPath: baseImgPath,
|
||||
// sectionKey: 'ELECTRICITY',
|
||||
// nameKey: 'ELECTRICITY_USER_NAME',
|
||||
// headerTitle: '用电人签字',
|
||||
// roleTitle: '用电人',
|
||||
// ),
|
||||
if (FormUtils.hasValue(signs, 'CONFESS'))
|
||||
ConfirmWithSignWidget(
|
||||
signs: signs,
|
||||
|
|
|
|||
|
|
@ -63,10 +63,26 @@ class _ElectricityAqjdDetailState extends State<ElectricityAqjdDetail> {
|
|||
LoadingDialogHelper.show();
|
||||
final res = await ApiService.uploadSaveFile(localPath);
|
||||
LoadingDialogHelper.hide();
|
||||
final url = res['FILE_PATH'] as String;
|
||||
setState(() {
|
||||
imgList.add(ImageData(localPath: localPath, serverPath: url));
|
||||
});
|
||||
try {
|
||||
if (res['result'] == 'success') {
|
||||
final url = res['FILE_PATH'] as String;
|
||||
setState(() {
|
||||
imgList.add(ImageData(localPath: localPath, serverPath: url));
|
||||
});
|
||||
} else {
|
||||
ToastUtil.showError(context, '上传失败,资源过大请重新选择');
|
||||
setState(() {
|
||||
imgList = [];
|
||||
MediaBus().emit(MediaEvent.clear(kAcceptVideoSectionKey));
|
||||
});
|
||||
}
|
||||
} catch (_) {
|
||||
ToastUtil.showError(context, '上传失败,资源过大请重新选择');
|
||||
setState(() {
|
||||
imgList = [];
|
||||
MediaBus().emit(MediaEvent.clear(kAcceptVideoSectionKey));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除图片处理:调用删除接口并更新列表
|
||||
|
|
|
|||
|
|
@ -164,9 +164,8 @@ class _ElectricityListPageState extends State<ElectricityListPage> {
|
|||
});
|
||||
} catch (e) {
|
||||
print('Error fetching flow data: $e');
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('获取流程图失败: $e')));
|
||||
|
||||
ToastUtil.showNormal(context, '获取流程图失败');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -519,14 +518,11 @@ class _ElectricityListPageState extends State<ElectricityListPage> {
|
|||
// 显示底部选择器
|
||||
Future<void> _showStepPicker() async {
|
||||
if (stepList.isEmpty) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('正在加载步骤数据,请稍后...')));
|
||||
LoadingDialogHelper.show();
|
||||
await _fetchSteps();
|
||||
LoadingDialogHelper.hide();
|
||||
if (stepList.isEmpty) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('无法加载步骤数据')));
|
||||
ToastUtil.showNormal(context, '无法加载数据');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -313,6 +313,7 @@ class _ElectricityApplyDetailState extends State<ElectricityApplyDetail> {
|
|||
isEditable: isEditable,
|
||||
isClean: isClean,
|
||||
onTapClean: () {
|
||||
ToastUtil.showNormal(context, '已清除');
|
||||
setState(() {
|
||||
pd['${type.name}_DEPARTMENT_NAME'] = '';
|
||||
pd['${type.name}_USER_NAME'] = '';
|
||||
|
|
|
|||
|
|
@ -239,10 +239,7 @@ class _ElectricitySetSafeDetailState extends State<ElectricitySetSafeDetail> {
|
|||
|
||||
/// 作废 -1 通过 1
|
||||
Future<void> _submit(String status) async {
|
||||
if (imagePaths.isEmpty) {
|
||||
ToastUtil.showNormal(context, '请签字');
|
||||
return;
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> signers = [];
|
||||
String reasonText = '';
|
||||
|
||||
|
|
@ -494,23 +491,7 @@ class _ElectricitySetSafeDetailState extends State<ElectricitySetSafeDetail> {
|
|||
),
|
||||
);
|
||||
}),
|
||||
SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
SizedBox(),
|
||||
CustomButton(
|
||||
text: '新增手写签字',
|
||||
height: 36,
|
||||
backgroundColor: Colors.green,
|
||||
onPressed: () {
|
||||
_sign();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
if (imagePaths.isNotEmpty) _signListWidget(),
|
||||
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -70,10 +70,26 @@ class _ElectricityYsgdDetailState extends State<ElectricityYsgdDetail> {
|
|||
LoadingDialogHelper.show();
|
||||
final res = await ApiService.uploadSaveFile(localPath);
|
||||
LoadingDialogHelper.hide();
|
||||
final url = res['FILE_PATH'] as String;
|
||||
setState(() {
|
||||
imgList.add(ImageData(localPath: localPath, serverPath: url));
|
||||
});
|
||||
try {
|
||||
if (res['result'] == 'success') {
|
||||
final url = res['FILE_PATH'] as String;
|
||||
setState(() {
|
||||
imgList.add(ImageData(localPath: localPath, serverPath: url));
|
||||
});
|
||||
} else {
|
||||
ToastUtil.showError(context, '上传失败,资源过大请重新选择');
|
||||
setState(() {
|
||||
imgList = [];
|
||||
MediaBus().emit(MediaEvent.clear(kAcceptVideoSectionKey));
|
||||
});
|
||||
}
|
||||
} catch (_) {
|
||||
ToastUtil.showError(context, '上传失败,资源过大请重新选择');
|
||||
setState(() {
|
||||
imgList = [];
|
||||
MediaBus().emit(MediaEvent.clear(kAcceptVideoSectionKey));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除图片处理:调用删除接口并更新列表
|
||||
|
|
@ -190,11 +206,7 @@ class _ElectricityYsgdDetailState extends State<ElectricityYsgdDetail> {
|
|||
ToastUtil.showNormal(context, '请在作业结束30分钟内完成验收');
|
||||
return;
|
||||
}
|
||||
if (imgList.isEmpty) {
|
||||
ToastUtil.showNormal(context, '请上传验收视频');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
} else {
|
||||
reasonText = await CustomAlertDialog.showInput(
|
||||
context,
|
||||
|
|
@ -383,7 +395,7 @@ class _ElectricityYsgdDetailState extends State<ElectricityYsgdDetail> {
|
|||
),
|
||||
Divider(),
|
||||
RepairedPhotoSection(
|
||||
isRequired: true,
|
||||
isRequired: false,
|
||||
title: '验收视频',
|
||||
maxCount: 1,
|
||||
mediaType: MediaType.video,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import 'package:qhd_prevention/customWidget/custom_button.dart';
|
|||
import 'package:qhd_prevention/customWidget/single_image_viewer.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:qhd_prevention/customWidget/picker/CupertinoDatePicker.dart';
|
||||
import 'package:qhd_prevention/customWidget/toast_util.dart';
|
||||
import 'package:qhd_prevention/http/ApiService.dart';
|
||||
import '../../../../../../tools/tools.dart';
|
||||
import '../../../item_list_widget.dart';
|
||||
|
|
@ -487,14 +488,10 @@ class _BlindboardDetailFormWidgetState
|
|||
}),
|
||||
if (FormUtils.hasValue(pd, 'WORK_START_DATE')) ...[
|
||||
const Divider(),
|
||||
ItemListWidget.selectableLineTitleTextRightButton(
|
||||
label: '盲板作业实施时间:',
|
||||
isEditable: widget.isEditable,
|
||||
text:
|
||||
FormUtils.hasValue(pd, 'WORK_END_DATE')
|
||||
? '${pd['WORK_START_DATE'] ?? ''}\n-${pd['WORK_END_DATE'] ?? ''}'
|
||||
: pd['WORK_START_DATE'] ?? '',
|
||||
),
|
||||
ItemListWidget.twoRowTitleText(label: '盲板作业实施时间', text: FormUtils.hasValue(pd, 'WORK_END_DATE')
|
||||
? '${pd['WORK_START_DATE'] ?? ''} - ${pd['WORK_END_DATE'] ?? ''}'
|
||||
: pd['WORK_START_DATE'] ?? ''),
|
||||
|
||||
] else ...[
|
||||
const Divider(),
|
||||
ItemListWidget.selectableLineTitleTextRightButton(
|
||||
|
|
@ -546,7 +543,8 @@ class _BlindboardDetailFormWidgetState
|
|||
text: pd['WORK_EXPECTED_END_TIME'] ?? '',
|
||||
),
|
||||
],
|
||||
if (!widget.isEditable && FormUtils.hasValue(pd, 'BOARD_PATH_NO'))
|
||||
if (!widget.isEditable && FormUtils.hasValue(pd, 'BOARD_PATH_NO')) ...[
|
||||
const Divider(),
|
||||
ConfirmWithSignWidget(
|
||||
signs: widget.signs,
|
||||
pd: pd,
|
||||
|
|
@ -558,7 +556,7 @@ class _BlindboardDetailFormWidgetState
|
|||
contentKey: 'BOARD_PATH_NO',
|
||||
roleTitle: '',
|
||||
),
|
||||
|
||||
],
|
||||
if (FormUtils.hasValue(pd, 'CONFIRM_USER_NAME') &&
|
||||
!widget.isEditable) ...[
|
||||
const Divider(),
|
||||
|
|
@ -614,7 +612,7 @@ class _BlindboardDetailFormWidgetState
|
|||
},
|
||||
hintText: '请输入关联的其他特殊作业及安全作业票编号',
|
||||
controller: widget.relatedController,
|
||||
text: pd['SPECIAL_WORK'] ?? '',
|
||||
text: pd['SPECIAL_WORK'] ?? '无',
|
||||
),
|
||||
const Divider(),
|
||||
ItemListWidget.twoRowButtonTitleText(
|
||||
|
|
@ -694,6 +692,7 @@ class _BlindboardDetailFormWidgetState
|
|||
isClean: widget.isEditable,
|
||||
cleanText: '清除监控',
|
||||
onTapClean: () {
|
||||
ToastUtil.showNormal(context, '已清除');
|
||||
setState(() {
|
||||
pd['VIDEONAME'] = '';
|
||||
pd['VIDEOMANAGER_ID'] = '';
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ class _BlindboardSafeFuncSureState extends State<BlindboardSafeFuncSure> {
|
|||
|
||||
/// 详情
|
||||
late Map<String, dynamic> pd = {};
|
||||
late Map<String, dynamic> signs = {};
|
||||
late List<dynamic> boardList = [];
|
||||
|
||||
late List<Map<String, dynamic>> measuresList = [];
|
||||
|
|
@ -53,6 +54,15 @@ class _BlindboardSafeFuncSureState extends State<BlindboardSafeFuncSure> {
|
|||
super.initState();
|
||||
_getData();
|
||||
}
|
||||
Future<void> _getSigns(String homework_id) async {
|
||||
final data = await ApiService.listSignFinished(
|
||||
'blindboard',
|
||||
homework_id.length > 0 ? homework_id : widget.BLINDBOARD_ID,
|
||||
);
|
||||
setState(() {
|
||||
signs = data['signs'] ?? {};
|
||||
});
|
||||
}
|
||||
|
||||
/// 弹出单位选择
|
||||
void chooseUnitHandle(MeasureItem item) {
|
||||
|
|
@ -268,6 +278,7 @@ class _BlindboardSafeFuncSureState extends State<BlindboardSafeFuncSure> {
|
|||
setState(() {
|
||||
pd = data['pd'];
|
||||
boardList = data['boardList'];
|
||||
_getSigns(pd['BLINDBOARD_ID'] ?? '');
|
||||
_getMeasures();
|
||||
});
|
||||
}
|
||||
|
|
@ -367,6 +378,7 @@ class _BlindboardSafeFuncSureState extends State<BlindboardSafeFuncSure> {
|
|||
onSign: (item) {
|
||||
_itemToSign(item, measuresList.indexOf(item));
|
||||
},
|
||||
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -445,11 +457,13 @@ class _BlindboardSafeFuncSureState extends State<BlindboardSafeFuncSure> {
|
|||
pd: pd,
|
||||
isEditable: false,
|
||||
onChooseLevel: () {},
|
||||
signs: signs,
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
_setSafeDetailWidget(),
|
||||
SizedBox(height: 20),
|
||||
_bottomButtons(),
|
||||
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -63,10 +63,26 @@ class _BlindboardAqjdDetailState extends State<BlindboardAqjdDetail> {
|
|||
LoadingDialogHelper.show();
|
||||
final res = await ApiService.uploadSaveFile(localPath);
|
||||
LoadingDialogHelper.hide();
|
||||
final url = res['FILE_PATH'] as String;
|
||||
setState(() {
|
||||
imgList.add(ImageData(localPath: localPath, serverPath: url));
|
||||
});
|
||||
try {
|
||||
if (res['result'] == 'success') {
|
||||
final url = res['FILE_PATH'] as String;
|
||||
setState(() {
|
||||
imgList.add(ImageData(localPath: localPath, serverPath: url));
|
||||
});
|
||||
} else {
|
||||
ToastUtil.showError(context, '上传失败,资源过大请重新选择');
|
||||
setState(() {
|
||||
imgList = [];
|
||||
MediaBus().emit(MediaEvent.clear(kAcceptVideoSectionKey));
|
||||
});
|
||||
}
|
||||
} catch (_) {
|
||||
ToastUtil.showError(context, '上传失败,资源过大请重新选择');
|
||||
setState(() {
|
||||
imgList = [];
|
||||
MediaBus().emit(MediaEvent.clear(kAcceptVideoSectionKey));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除图片处理:调用删除接口并更新列表
|
||||
|
|
|
|||
|
|
@ -167,9 +167,8 @@ class _BlindboardListPageState extends State<BlindboardListPage> {
|
|||
});
|
||||
} catch (e) {
|
||||
print('Error fetching flow data: $e');
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('获取流程图失败: $e')));
|
||||
|
||||
ToastUtil.showNormal(context, '获取流程图失败');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -420,14 +419,11 @@ class _BlindboardListPageState extends State<BlindboardListPage> {
|
|||
// 显示底部选择器
|
||||
Future<void> _showStepPicker() async {
|
||||
if (stepList.isEmpty) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('正在加载步骤数据,请稍后...')));
|
||||
LoadingDialogHelper.show();
|
||||
await _fetchSteps();
|
||||
LoadingDialogHelper.hide();
|
||||
if (stepList.isEmpty) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('无法加载步骤数据')));
|
||||
ToastUtil.showNormal(context, '无法加载数据');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -261,10 +261,26 @@ class _BlindboardCjryDetailState extends State<BlindboardCjryDetail> {
|
|||
LoadingDialogHelper.show();
|
||||
final res = await ApiService.uploadSaveFile(localPath);
|
||||
LoadingDialogHelper.hide();
|
||||
final url = res['FILE_PATH'] as String;
|
||||
setState(() {
|
||||
imgList.add(ImageData(localPath: localPath, serverPath: url));
|
||||
});
|
||||
try {
|
||||
if (res['result'] == 'success') {
|
||||
final url = res['FILE_PATH'] as String;
|
||||
setState(() {
|
||||
imgList.add(ImageData(localPath: localPath, serverPath: url));
|
||||
});
|
||||
} else {
|
||||
ToastUtil.showError(context, '上传失败,资源过大请重新选择');
|
||||
setState(() {
|
||||
imgList = [];
|
||||
MediaBus().emit(MediaEvent.clear(kAcceptVideoSectionKey));
|
||||
});
|
||||
}
|
||||
} catch (_) {
|
||||
ToastUtil.showError(context, '上传失败,资源过大请重新选择');
|
||||
setState(() {
|
||||
imgList = [];
|
||||
MediaBus().emit(MediaEvent.clear(kAcceptVideoSectionKey));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除图片处理:调用删除接口并更新列表
|
||||
|
|
|
|||
|
|
@ -323,6 +323,7 @@ class _BlindboardApplyDetailState extends State<BlindboardApplyDetail> {
|
|||
isEditable: isEditable,
|
||||
isClean: isClean,
|
||||
onTapClean: () {
|
||||
ToastUtil.showNormal(context, '已清除');
|
||||
setState(() {
|
||||
pd['${type.name}_DEPARTMENT_NAME'] = '';
|
||||
pd['${type.name}_USER_NAME'] = '';
|
||||
|
|
|
|||
|
|
@ -250,10 +250,7 @@ class _BlindboardSetSafeDetailState extends State<BlindboardSetSafeDetail> {
|
|||
|
||||
/// 作废 -1 通过 1
|
||||
Future<void> _submit(String status) async {
|
||||
if (imagePaths.isEmpty) {
|
||||
ToastUtil.showNormal(context, '请签字');
|
||||
return;
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> signers = [];
|
||||
String reasonText = '';
|
||||
|
||||
|
|
@ -508,23 +505,7 @@ class _BlindboardSetSafeDetailState extends State<BlindboardSetSafeDetail> {
|
|||
),
|
||||
);
|
||||
}),
|
||||
SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
SizedBox(),
|
||||
CustomButton(
|
||||
text: '新增手写签字',
|
||||
height: 36,
|
||||
backgroundColor: Colors.green,
|
||||
onPressed: () {
|
||||
_sign();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
if (imagePaths.isNotEmpty) _signListWidget(),
|
||||
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -71,10 +71,26 @@ class _BlindboardYsgdDetailState extends State<BlindboardYsgdDetail> {
|
|||
LoadingDialogHelper.show();
|
||||
final res = await ApiService.uploadSaveFile(localPath);
|
||||
LoadingDialogHelper.hide();
|
||||
final url = res['FILE_PATH'] as String;
|
||||
setState(() {
|
||||
imgList.add(ImageData(localPath: localPath, serverPath: url));
|
||||
});
|
||||
try {
|
||||
if (res['result'] == 'success') {
|
||||
final url = res['FILE_PATH'] as String;
|
||||
setState(() {
|
||||
imgList.add(ImageData(localPath: localPath, serverPath: url));
|
||||
});
|
||||
} else {
|
||||
ToastUtil.showError(context, '上传失败,资源过大请重新选择');
|
||||
setState(() {
|
||||
imgList = [];
|
||||
MediaBus().emit(MediaEvent.clear(kAcceptVideoSectionKey));
|
||||
});
|
||||
}
|
||||
} catch (_) {
|
||||
ToastUtil.showError(context, '上传失败,资源过大请重新选择');
|
||||
setState(() {
|
||||
imgList = [];
|
||||
MediaBus().emit(MediaEvent.clear(kAcceptVideoSectionKey));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除图片处理:调用删除接口并更新列表
|
||||
|
|
@ -181,11 +197,7 @@ class _BlindboardYsgdDetailState extends State<BlindboardYsgdDetail> {
|
|||
return;
|
||||
}
|
||||
|
||||
if (imgList.isEmpty) {
|
||||
ToastUtil.showNormal(context, '请上传验收视频');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
} else {
|
||||
reasonText = await CustomAlertDialog.showInput(
|
||||
context,
|
||||
|
|
@ -370,7 +382,7 @@ class _BlindboardYsgdDetailState extends State<BlindboardYsgdDetail> {
|
|||
),
|
||||
Divider(),
|
||||
RepairedPhotoSection(
|
||||
isRequired: true,
|
||||
isRequired: false,
|
||||
title: '验收视频',
|
||||
maxCount: 1,
|
||||
mediaType: MediaType.video,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:qhd_prevention/customWidget/ItemWidgetFactory.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:qhd_prevention/customWidget/picker/CupertinoDatePicker.dart';
|
||||
import 'package:qhd_prevention/customWidget/toast_util.dart';
|
||||
import '../../../../../../tools/tools.dart';
|
||||
import '../../../item_list_widget.dart';
|
||||
import 'package:qhd_prevention/pages/home/tap/tabList/special_wrok/MeasuresListWidget.dart';
|
||||
|
|
@ -116,6 +117,14 @@ class _SpaceWorkDetailFormWidgetState extends State<SpaceWorkDetailFormWidget> {
|
|||
isEditable: false,
|
||||
text: pd['APPLY_USER_NAME'] ?? '',
|
||||
),
|
||||
if (FormUtils.hasValue(pd, 'CREATTIME') && !widget.isEditable) ...[
|
||||
const Divider(),
|
||||
ItemListWidget.singleLineTitleText(
|
||||
label: '作业申请时间:',
|
||||
isEditable: false,
|
||||
text: pd['CREATTIME'] ?? '',
|
||||
),
|
||||
],
|
||||
const Divider(),
|
||||
if (!widget.isEditSpaceName)
|
||||
ItemListWidget.selectableLineTitleTextRightButton(
|
||||
|
|
@ -125,6 +134,7 @@ class _SpaceWorkDetailFormWidgetState extends State<SpaceWorkDetailFormWidget> {
|
|||
isClean: widget.isEditable,
|
||||
text: pd['LIMITSPACE_NAME'] ?? '',
|
||||
onTapClean: () {
|
||||
ToastUtil.showNormal(context, '已清除');
|
||||
setState(() {
|
||||
pd['LIMITSPACE_NAME'] = '';
|
||||
pd['LIMITSPACE_NUMBER'] = '';
|
||||
|
|
@ -142,6 +152,7 @@ class _SpaceWorkDetailFormWidgetState extends State<SpaceWorkDetailFormWidget> {
|
|||
cleanText: '选择',
|
||||
text: pd['LIMITSPACE_NAME'] ?? '',
|
||||
onTapClean: () {
|
||||
ToastUtil.showNormal(context, '已清除');
|
||||
setState(() {
|
||||
pd['LIMITSPACE_NAME'] = '';
|
||||
pd['LIMITSPACE_NUMBER'] = '';
|
||||
|
|
@ -238,7 +249,7 @@ class _SpaceWorkDetailFormWidgetState extends State<SpaceWorkDetailFormWidget> {
|
|||
},
|
||||
hintText: '请输入关联的其他特殊作业及安全作业票编号',
|
||||
controller: widget.relatedController,
|
||||
text: pd['SPECIAL_WORK'] ?? '',
|
||||
text: pd['SPECIAL_WORK'] ?? '无',
|
||||
),
|
||||
const Divider(),
|
||||
ItemListWidget.twoRowButtonTitleText(
|
||||
|
|
@ -267,14 +278,9 @@ class _SpaceWorkDetailFormWidgetState extends State<SpaceWorkDetailFormWidget> {
|
|||
),
|
||||
if (FormUtils.hasValue(pd, 'WORK_START_DATE')) ...[
|
||||
const Divider(),
|
||||
ItemListWidget.selectableLineTitleTextRightButton(
|
||||
label: '作业实施时间:',
|
||||
isEditable: widget.isEditable,
|
||||
text:
|
||||
FormUtils.hasValue(pd, 'WORK_END_DATE')
|
||||
? '${pd['WORK_START_DATE'] ?? ''}\n-${pd['WORK_END_DATE'] ?? ''}'
|
||||
: pd['WORK_START_DATE'] ?? '',
|
||||
),
|
||||
ItemListWidget.twoRowTitleText(label: '作业实施时间', text: FormUtils.hasValue(pd, 'WORK_END_DATE')
|
||||
? '${pd['WORK_START_DATE'] ?? ''} - ${pd['WORK_END_DATE'] ?? ''}'
|
||||
: pd['WORK_START_DATE'] ?? ''),
|
||||
] else ...[
|
||||
const Divider(),
|
||||
ItemListWidget.selectableLineTitleTextRightButton(
|
||||
|
|
@ -379,6 +385,7 @@ class _SpaceWorkDetailFormWidgetState extends State<SpaceWorkDetailFormWidget> {
|
|||
isClean: widget.isEditable,
|
||||
cleanText: '清除监控',
|
||||
onTapClean: () {
|
||||
ToastUtil.showNormal(context, '已清除');
|
||||
setState(() {
|
||||
pd['VIDEONAME'] = '';
|
||||
pd['VIDEOMANAGER_ID'] = '';
|
||||
|
|
@ -392,7 +399,7 @@ class _SpaceWorkDetailFormWidgetState extends State<SpaceWorkDetailFormWidget> {
|
|||
|
||||
if (FormUtils.hasValue(pd, 'ANALYZE_TIME') && !widget.isEditable) ...[
|
||||
const Divider(),
|
||||
ItemListWidget.OneRowButtonTitleText(
|
||||
ItemListWidget.OneRowStartButtonTitle(
|
||||
label: '气体分析信息:',
|
||||
text: pd['ANALYZE_USER_NAME'] ?? '',
|
||||
onTap: widget.onAnalyzeTap,
|
||||
|
|
|
|||
|
|
@ -102,16 +102,16 @@ class SpecialWorkFormBaseWork extends StatelessWidget {
|
|||
],
|
||||
),
|
||||
),
|
||||
if (FormUtils.hasValue(signs, 'GUARDIAN'))
|
||||
ConfirmWithSignWidget(
|
||||
signs: signs,
|
||||
pd: pd,
|
||||
baseImgPath: baseImgPath,
|
||||
sectionKey: 'GUARDIAN',
|
||||
nameKey: 'GUARDIAN_USER_NAME',
|
||||
headerTitle: '监护人签字',
|
||||
roleTitle: '监护人',
|
||||
),
|
||||
// if (FormUtils.hasValue(signs, 'GUARDIAN'))
|
||||
// ConfirmWithSignWidget(
|
||||
// signs: signs,
|
||||
// pd: pd,
|
||||
// baseImgPath: baseImgPath,
|
||||
// sectionKey: 'GUARDIAN',
|
||||
// nameKey: 'GUARDIAN_USER_NAME',
|
||||
// headerTitle: '监护人签字',
|
||||
// roleTitle: '监护人',
|
||||
// ),
|
||||
if (FormUtils.hasValue(signs, 'CONFESS'))
|
||||
ConfirmWithSignWidget(
|
||||
signs: signs,
|
||||
|
|
|
|||
|
|
@ -64,10 +64,26 @@ class _SpaceworkAqjdDetailState extends State<SpaceworkAqjdDetail> {
|
|||
LoadingDialogHelper.show();
|
||||
final res = await ApiService.uploadSaveFile(localPath);
|
||||
LoadingDialogHelper.hide();
|
||||
final url = res['FILE_PATH'] as String;
|
||||
setState(() {
|
||||
imgList.add(ImageData(localPath: localPath, serverPath: url));
|
||||
});
|
||||
try {
|
||||
if (res['result'] == 'success') {
|
||||
final url = res['FILE_PATH'] as String;
|
||||
setState(() {
|
||||
imgList.add(ImageData(localPath: localPath, serverPath: url));
|
||||
});
|
||||
} else {
|
||||
ToastUtil.showError(context, '上传失败,资源过大请重新选择');
|
||||
setState(() {
|
||||
imgList = [];
|
||||
MediaBus().emit(MediaEvent.clear(kAcceptVideoSectionKey));
|
||||
});
|
||||
}
|
||||
} catch (_) {
|
||||
ToastUtil.showError(context, '上传失败,资源过大请重新选择');
|
||||
setState(() {
|
||||
imgList = [];
|
||||
MediaBus().emit(MediaEvent.clear(kAcceptVideoSectionKey));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除图片处理:调用删除接口并更新列表
|
||||
|
|
|
|||
|
|
@ -334,6 +334,7 @@ class _SpaceworkApplyDetailState extends State<SpaceworkApplyDetail> {
|
|||
isEditable: isEditable,
|
||||
isClean: isClean,
|
||||
onTapClean: () {
|
||||
ToastUtil.showNormal(context, '已清除');
|
||||
setState(() {
|
||||
pd['${type.name}_DEPARTMENT_NAME'] = '';
|
||||
pd['${type.name}_USER_NAME'] = '';
|
||||
|
|
|
|||
|
|
@ -167,9 +167,8 @@ class _SpaceworkListPageState extends State<SpaceworkListPage> {
|
|||
});
|
||||
} catch (e) {
|
||||
print('Error fetching flow data: $e');
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('获取流程图失败: $e')));
|
||||
|
||||
ToastUtil.showNormal(context, '获取流程图失败');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -535,14 +534,11 @@ class _SpaceworkListPageState extends State<SpaceworkListPage> {
|
|||
// 显示底部选择器
|
||||
Future<void> _showStepPicker() async {
|
||||
if (stepList.isEmpty) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('正在加载步骤数据,请稍后...')));
|
||||
LoadingDialogHelper.show();
|
||||
await _fetchSteps();
|
||||
LoadingDialogHelper.hide();
|
||||
if (stepList.isEmpty) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('无法加载步骤数据')));
|
||||
ToastUtil.showNormal(context, '无法加载数据');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -239,10 +239,7 @@ class _SpaceworkSetSafeDetailState extends State<SpaceworkSetSafeDetail> {
|
|||
|
||||
/// 作废 -1 通过 1
|
||||
Future<void> _submit(String status) async {
|
||||
if (imagePaths.isEmpty) {
|
||||
ToastUtil.showNormal(context, '请签字');
|
||||
return;
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> signers = [];
|
||||
String reasonText = '';
|
||||
|
||||
|
|
@ -493,23 +490,7 @@ class _SpaceworkSetSafeDetailState extends State<SpaceworkSetSafeDetail> {
|
|||
),
|
||||
);
|
||||
}),
|
||||
SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
SizedBox(),
|
||||
CustomButton(
|
||||
text: '新增手写签字',
|
||||
height: 36,
|
||||
backgroundColor: Colors.green,
|
||||
onPressed: () {
|
||||
_sign();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
if (imagePaths.isNotEmpty) _signListWidget(),
|
||||
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -71,10 +71,26 @@ class _SpaceworkYsgdDetailState extends State<SpaceworkYsgdDetail> {
|
|||
LoadingDialogHelper.show();
|
||||
final res = await ApiService.uploadSaveFile(localPath);
|
||||
LoadingDialogHelper.hide();
|
||||
final url = res['FILE_PATH'] as String;
|
||||
setState(() {
|
||||
imgList.add(ImageData(localPath: localPath, serverPath: url));
|
||||
});
|
||||
try {
|
||||
if (res['result'] == 'success') {
|
||||
final url = res['FILE_PATH'] as String;
|
||||
setState(() {
|
||||
imgList.add(ImageData(localPath: localPath, serverPath: url));
|
||||
});
|
||||
} else {
|
||||
ToastUtil.showError(context, '上传失败,资源过大请重新选择');
|
||||
setState(() {
|
||||
imgList = [];
|
||||
MediaBus().emit(MediaEvent.clear(kAcceptVideoSectionKey));
|
||||
});
|
||||
}
|
||||
} catch (_) {
|
||||
ToastUtil.showError(context, '上传失败,资源过大请重新选择');
|
||||
setState(() {
|
||||
imgList = [];
|
||||
MediaBus().emit(MediaEvent.clear(kAcceptVideoSectionKey));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除图片处理:调用删除接口并更新列表
|
||||
|
|
@ -191,11 +207,7 @@ class _SpaceworkYsgdDetailState extends State<SpaceworkYsgdDetail> {
|
|||
ToastUtil.showNormal(context, '请在作业结束30分钟内完成验收');
|
||||
return;
|
||||
}
|
||||
if (imgList.isEmpty) {
|
||||
ToastUtil.showNormal(context, '请上传验收视频');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
} else {
|
||||
reasonText = await CustomAlertDialog.showInput(
|
||||
context,
|
||||
|
|
@ -387,7 +399,7 @@ class _SpaceworkYsgdDetailState extends State<SpaceworkYsgdDetail> {
|
|||
),
|
||||
Divider(),
|
||||
RepairedPhotoSection(
|
||||
isRequired: true,
|
||||
isRequired: false,
|
||||
title: '验收视频',
|
||||
maxCount: 1,
|
||||
mediaType: MediaType.video,
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ class _UserinfoPageState extends State<UserinfoPage> {
|
|||
}
|
||||
|
||||
void _showMessage(String msg) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
|
||||
ToastUtil.showNormal(context, msg);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -349,7 +349,7 @@ class _CustomDriverDrawerState extends State<CustomDriverDrawer> {
|
|||
}
|
||||
|
||||
void _showMessage(String msg) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
|
||||
ToastUtil.showNormal(context, msg);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -238,7 +238,7 @@ class _LawsRegulationsPage extends State<LawsRegulationsPage> {
|
|||
}
|
||||
|
||||
void _showMessage(String msg) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
|
||||
ToastUtil.showNormal(context, msg);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -205,7 +205,7 @@ class _LawsRegulationsTwoPage extends State<LawsRegulationsTwoPage> {
|
|||
}
|
||||
|
||||
void _showMessage(String msg) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
|
||||
ToastUtil.showNormal(context, msg);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -466,6 +466,6 @@ class _WorkSetPageState extends State<WorkSetPage> {
|
|||
}
|
||||
|
||||
void _showMessage(String msg) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
|
||||
ToastUtil.showNormal(context, msg);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,22 +70,25 @@ class _LoginPageState extends State<LoginPage> {
|
|||
}
|
||||
|
||||
Future<void> _checkUpdata() async {
|
||||
final result = await AuthService.checkUpdate();
|
||||
if (FormUtils.hasValue(result, 'pd')) {
|
||||
// 有更新 提示更新
|
||||
Map pd = result['pd'];
|
||||
CustomAlertDialog.showConfirm(
|
||||
context,
|
||||
title: '更新通知',
|
||||
cancelText: '',
|
||||
confirmText: '我知道了',
|
||||
content: pd['UPLOAD_CONTENT'] ?? '',
|
||||
onConfirm: () {
|
||||
ToastUtil.showNormal(context, '更新去吧!');
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
try{
|
||||
final result = await AuthService.checkUpdate();
|
||||
if (FormUtils.hasValue(result, 'pd')) {
|
||||
// 有更新 提示更新
|
||||
Map pd = result['pd'];
|
||||
CustomAlertDialog.showConfirm(
|
||||
context,
|
||||
title: '更新通知',
|
||||
cancelText: '',
|
||||
confirmText: '我知道了',
|
||||
content: pd['UPLOAD_CONTENT'] ?? '',
|
||||
onConfirm: () {
|
||||
ToastUtil.showNormal(context, '更新去吧!');
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
}catch(_) {}
|
||||
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -400,6 +400,6 @@ class _FeedbackPageState extends State<FeedbackPage> {
|
|||
}
|
||||
|
||||
void _showMessage(String msg) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
|
||||
ToastUtil.showNormal(context, msg);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ class _MineSetPwdPageState extends State<MineSetPwdPage> {
|
|||
}
|
||||
|
||||
void _showMessage(String msg) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
|
||||
ToastUtil.showNormal(context, msg);
|
||||
}
|
||||
|
||||
Widget _buildPwdField(String hintText, TextEditingController controller) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:encrypt/encrypt.dart' as encrypt;
|
||||
import 'package:flutter/src/widgets/framework.dart';
|
||||
import 'package:pointycastle/asymmetric/api.dart' show RSAPublicKey;
|
||||
|
|
@ -6,7 +7,7 @@ import 'package:qhd_prevention/pages/mine/mine_set_pwd_page.dart';
|
|||
import 'package:qhd_prevention/tools/tools.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import '../http/ApiService.dart';
|
||||
|
||||
class AuthService {
|
||||
|
|
@ -67,7 +68,34 @@ class AuthService {
|
|||
}
|
||||
return {};
|
||||
}
|
||||
// 检查并请求相机权限
|
||||
static Future<bool> checkCameraPermission() async {
|
||||
final status = await Permission.camera.status;
|
||||
if (status.isDenied) {
|
||||
final result = await Permission.camera.request();
|
||||
return result.isGranted;
|
||||
}
|
||||
return status.isGranted;
|
||||
}
|
||||
|
||||
// 检查并请求照片库权限(iOS)和存储权限(Android)
|
||||
static Future<bool> checkPhotoLibraryPermission() async {
|
||||
if (Platform.isIOS) {
|
||||
final status = await Permission.photos.status;
|
||||
if (status.isDenied) {
|
||||
final result = await Permission.photos.request();
|
||||
return result.isGranted;
|
||||
}
|
||||
return status.isGranted;
|
||||
} else {
|
||||
final status = await Permission.storage.status;
|
||||
if (status.isDenied) {
|
||||
final result = await Permission.storage.request();
|
||||
return result.isGranted;
|
||||
}
|
||||
return status.isGranted;
|
||||
}
|
||||
}
|
||||
|
||||
/// 验证是否已登录(通过重新登录)
|
||||
static Future<Map<String, dynamic>> isLoggedIn() async {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ import 'package:flutter/material.dart';
|
|||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'dart:io';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
int getRandomWithNum(int min, int max) {
|
||||
if (max < min) {
|
||||
|
|
@ -373,8 +376,8 @@ void presentPage(BuildContext context, Widget page) {
|
|||
class LoadingDialogHelper {
|
||||
static Timer? _timer;
|
||||
|
||||
/// 显示加载框(带超时,默认 10 秒)
|
||||
static void show({String? message, Duration timeout = const Duration(seconds: 10)}) {
|
||||
/// 显示加载框(带超时,默认 60 秒)
|
||||
static void show({String? message, Duration timeout = const Duration(seconds: 60)}) {
|
||||
// 先清理上一个计时器,避免重复
|
||||
_timer?.cancel();
|
||||
|
||||
|
|
@ -521,3 +524,73 @@ class NativeOrientation {
|
|||
}
|
||||
}
|
||||
|
||||
class CameraPermissionHelper {
|
||||
static final ImagePicker _picker = ImagePicker();
|
||||
|
||||
// 检查并请求相机权限(使用 ImagePicker 触发权限请求)
|
||||
static Future<bool> checkAndRequestCameraPermission() async {
|
||||
if (Platform.isIOS) {
|
||||
// 对于 iOS,使用 ImagePicker 触发权限请求
|
||||
try {
|
||||
// 尝试获取图片(但立即取消)来触发权限请求
|
||||
final XFile? file = await _picker.pickImage(
|
||||
source: ImageSource.camera,
|
||||
maxWidth: 1, // 最小尺寸,减少处理时间
|
||||
maxHeight: 1,
|
||||
imageQuality: 1,
|
||||
).timeout(const Duration(milliseconds: 100), onTimeout: () {
|
||||
return null; // 超时返回 null,避免等待用户操作
|
||||
});
|
||||
|
||||
// 无论是否成功获取文件,权限请求已经被触发
|
||||
// 现在检查实际的权限状态
|
||||
var status = await Permission.camera.status;
|
||||
return status.isGranted;
|
||||
} catch (e) {
|
||||
// 如果出现错误,回退到直接检查权限状态
|
||||
var status = await Permission.camera.status;
|
||||
return status.isGranted;
|
||||
}
|
||||
} else {
|
||||
// Android 使用标准的权限检查方式
|
||||
var status = await Permission.camera.status;
|
||||
if (status.isDenied) {
|
||||
status = await Permission.camera.request();
|
||||
}
|
||||
return status.isGranted;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查并请求相册权限
|
||||
static Future<bool> checkAndRequestPhotoPermission() async {
|
||||
if (Platform.isIOS) {
|
||||
// 对于 iOS,使用 ImagePicker 触发权限请求
|
||||
try {
|
||||
// 尝试获取图片(但立即取消)来触发权限请求
|
||||
final XFile? file = await _picker.pickImage(
|
||||
source: ImageSource.gallery,
|
||||
maxWidth: 1,
|
||||
maxHeight: 1,
|
||||
imageQuality: 1,
|
||||
).timeout(const Duration(milliseconds: 100), onTimeout: () {
|
||||
return null;
|
||||
});
|
||||
|
||||
// 检查实际的权限状态
|
||||
var status = await Permission.photos.status;
|
||||
return status.isGranted;
|
||||
} catch (e) {
|
||||
var status = await Permission.photos.status;
|
||||
return status.isGranted;
|
||||
}
|
||||
} else {
|
||||
// Android 使用标准的权限检查方式
|
||||
var status = await Permission.storage.status;
|
||||
if (status.isDenied) {
|
||||
status = await Permission.storage.request();
|
||||
}
|
||||
return status.isGranted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
14
pubspec.lock
14
pubspec.lock
|
|
@ -117,10 +117,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: chewie
|
||||
sha256: "19b93a1e60e4ba640a792208a6543f1c7d5b124d011ce0199e2f18802199d984"
|
||||
sha256: "44bcfc5f0dfd1de290c87c9d86a61308b3282a70b63435d5557cfd60f54a69ca"
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
version: "1.12.1"
|
||||
version: "1.13.0"
|
||||
cli_util:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1222,10 +1222,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: "69ee86740f2847b9a4ba6cffa74ed12ce500bbe2b07f3dc1e643439da60637b7"
|
||||
sha256: "81777b08c498a292d93ff2feead633174c386291e35612f8da438d6e92c4447e"
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
version: "6.3.18"
|
||||
version: "6.3.20"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1358,10 +1358,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: wakelock_plus
|
||||
sha256: a474e314c3e8fb5adef1f9ae2d247e57467ad557fa7483a2b895bc1b421c5678
|
||||
sha256: "61713aa82b7f85c21c9f4cd0a148abd75f38a74ec645fcb1e446f882c82fd09b"
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
version: "1.3.2"
|
||||
version: "1.3.3"
|
||||
wakelock_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1468,4 +1468,4 @@ packages:
|
|||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.8.0 <4.0.0"
|
||||
flutter: ">=3.29.0"
|
||||
flutter: ">=3.32.0"
|
||||
|
|
|
|||
12
pubspec.yaml
12
pubspec.yaml
|
|
@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 2.1.2+7
|
||||
version: 2.1.2+8
|
||||
|
||||
environment:
|
||||
sdk: ^3.7.0
|
||||
|
|
@ -27,11 +27,11 @@ environment:
|
|||
# dependencies can be manually updated by changing the version numbers below to
|
||||
# the latest version available on pub.dev. To see which dependencies have newer
|
||||
# versions available, run `flutter pub outdated`.
|
||||
flutter_launcher_icons:
|
||||
android: true
|
||||
ios: true
|
||||
image_path: "assets/images/app-logo.png"
|
||||
min_sdk_android: 21 # (可选) Android 最低支持
|
||||
#flutter_launcher_icons:
|
||||
# android: true
|
||||
# ios: true
|
||||
# image_path: "assets/images/app-logo.png"
|
||||
# min_sdk_android: 21 # (可选) Android 最低支持
|
||||
|
||||
flutter_native_splash:
|
||||
background_image: assets/images/bg-login.png
|
||||
|
|
|
|||
Loading…
Reference in New Issue