。。。

main
hs 2025-09-15 15:54:03 +08:00
parent 31b5a9b532
commit 172ac497b6
91 changed files with 2343 additions and 1454 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -281,6 +281,7 @@ class _SafecheckAssignmentDetailPageState
isRequired: false,
isClean: false,
onTapClean: () {
ToastUtil.showNormal(context, '已清除');
setState(() {});
},
text: form['RECTIFICATIONDEPTNAME'] ?? '请选择',

View File

@ -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'] ?? '请选择',

View File

@ -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'] ?? '请选择',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.requestAccessAndroid使 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

View File

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

View File

@ -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();
})();
// controllerremoveListener -> 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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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'] = '';

View File

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

View File

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

View File

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

View File

@ -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'] = '';

View File

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

View File

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

View File

@ -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'] = '';

View File

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

View File

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

View File

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

View File

@ -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'] = '';

View File

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

View File

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

View File

@ -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'] = '';

View File

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

View File

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

View File

@ -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'] = '';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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'] = '';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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'] = '';

View File

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

View File

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

View File

@ -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'] = '';

View File

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

View File

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

View File

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

View File

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

View File

@ -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'] = '';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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'] = '';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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;
}
// iOSAndroid
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 {

View File

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

View File

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

View File

@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 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