bug修复暂存

main
hs 2025-08-29 09:52:48 +08:00
parent e7d144ce40
commit 40bd6e4d4d
36 changed files with 2216 additions and 1378 deletions

View File

@ -36,27 +36,27 @@ android {
versionName = flutter.versionName
}
// // ✅ 添加 release 签名配置
// signingConfigs {
// create("release") {
// storeFile = file(keystoreProperties["storeFile"] as String)
// storePassword = keystoreProperties["storePassword"] as String
// keyAlias = keystoreProperties["keyAlias"] as String
// keyPassword = keystoreProperties["keyPassword"] as String
// }
// }
//
// buildTypes {
// release {
// // ✅ 替换成 release 签名
// signingConfig = signingConfigs.getByName("release")
// isMinifyEnabled = false
// isShrinkResources = false
// }
// debug {
// signingConfig = signingConfigs.getByName("debug")
// }
// }
// ✅ 添加 release 签名配置
signingConfigs {
create("release") {
storeFile = file(keystoreProperties["storeFile"] as String)
storePassword = keystoreProperties["storePassword"] as String
keyAlias = keystoreProperties["keyAlias"] as String
keyPassword = keystoreProperties["keyPassword"] as String
}
}
buildTypes {
release {
// ✅ 替换成 release 签名
signingConfig = signingConfigs.getByName("release")
isMinifyEnabled = false
isShrinkResources = false
}
debug {
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {

View File

@ -227,7 +227,7 @@
const x = [e.latlng.lng, e.latlng.lat];
let inside = (GSON_LON_LAT_BD09 && GSON_LON_LAT_BD09.length > 0) ? isPointInPolygon(x, GSON_LON_LAT_BD09) : true;
if (!inside) {
alert("当前选择点位不在区域中!");
//alert("当前选择点位不在区域中!");
notifyHost({type:'point_selected', ok:false, reason:'out_of_polygon', lng:e.latlng.lng, lat:e.latlng.lat});
} else {
map.addOverlay(marker);

View File

@ -47,6 +47,8 @@ PODS:
- FlutterMacOS
- url_launcher_ios (0.0.1):
- Flutter
- video_compress (0.3.0):
- Flutter
- video_player_avfoundation (0.0.1):
- Flutter
- FlutterMacOS
@ -75,6 +77,7 @@ DEPENDENCIES:
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- video_compress (from `.symlinks/plugins/video_compress/ios`)
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`)
@ -120,6 +123,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
video_compress:
:path: ".symlinks/plugins/video_compress/ios"
video_player_avfoundation:
:path: ".symlinks/plugins/video_player_avfoundation/darwin"
wakelock_plus:
@ -147,6 +152,7 @@ SPEC CHECKSUMS:
photo_manager: 1d80ae07a89a67dfbcae95953a1e5a24af7c3e62
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
video_compress: f2133a07762889d67f0711ac831faa26f956980e
video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
webview_flutter_wkwebview: 1821ceac936eba6f7984d89a9f3bcb4dea99ebb2

View File

@ -1,94 +1,94 @@
<?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>${PRODUCT_NAME}</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>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>${PRODUCT_NAME}</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>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>
</plist>

View File

@ -50,14 +50,14 @@ class _BigVideoViewerState extends State<BigVideoViewer> {
appBar: MyAppbar(
backgroundColor: Colors.transparent, title: '',
),
body: Center(
body: SafeArea(child: Center(
child: VideoPlayerWidget(
allowSeek: false,
controller: _videoController,
coverUrl:"",
aspectRatio: _videoController?.value.aspectRatio ?? 16/9,
allowSeek: false,
controller: _videoController,
coverUrl:"",
aspectRatio: _videoController?.value.aspectRatio ?? 16/9,
),
),
),)
);
}
}

View File

@ -1,8 +1,9 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
import 'package:chewie/chewie.dart';
/// Chewie VideoPlayerPopup
/// /
class VideoPlayerPopup extends StatefulWidget {
final String videoUrl;
const VideoPlayerPopup({Key? key, required this.videoUrl}) : super(key: key);
@ -14,27 +15,84 @@ class VideoPlayerPopup extends StatefulWidget {
class _VideoPlayerPopupState extends State<VideoPlayerPopup> {
late VideoPlayerController _videoController;
ChewieController? _chewieController;
bool _isNetwork = false;
bool _initializing = true;
String? _error;
@override
void initState() {
super.initState();
_videoController = VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl))
..initialize().then((_) {
setState(() {});
});
_initController();
}
_chewieController = ChewieController(
videoPlayerController: _videoController,
autoPlay: true,
looping: false,
showOptions:false,
allowFullScreen: true,
allowPlaybackSpeedChanging: true,
allowMuting: true,
showControlsOnInitialize: true,
materialProgressColors: ChewieProgressColors(playedColor: Colors.blue,backgroundColor: Colors.white, handleColor:Colors.blue,bufferedColor:Colors.red),
aspectRatio: _videoController.value.aspectRatio,
);
Future<void> _initController() async {
try {
final uri = Uri.tryParse(widget.videoUrl);
final scheme = uri?.scheme?.toLowerCase() ?? '';
// http/https/rtsp/rtmp
_isNetwork = (scheme == 'http' || scheme == 'https' || scheme == 'rtsp' || scheme == 'rtmp');
if (_isNetwork) {
//
_videoController = VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl));
} else {
// file://
if (scheme == 'file' && uri != null) {
_videoController = VideoPlayerController.file(File(uri.toFilePath()));
} else {
// /storage/...
_videoController = VideoPlayerController.file(File(widget.videoUrl));
}
}
// VideoPlayerController
await _videoController.initialize();
// ChewieController aspectRatio
_chewieController?.dispose();
_chewieController = ChewieController(
videoPlayerController: _videoController,
autoPlay: true,
looping: false,
showOptions: false,
allowFullScreen: true,
allowPlaybackSpeedChanging: true,
allowMuting: true,
showControlsOnInitialize: true,
//
materialProgressColors: ChewieProgressColors(
playedColor: Colors.blue,
backgroundColor: Colors.white,
handleColor: Colors.blue,
bufferedColor: Colors.red,
),
aspectRatio: _videoController.value.aspectRatio > 0
? _videoController.value.aspectRatio
: 16 / 9,
errorBuilder: (context, errorMessage) {
return Center(
child: Text(
errorMessage ?? '视频播放错误',
style: const TextStyle(color: Colors.white),
),
);
},
);
if (!mounted) return;
setState(() {
_initializing = false;
});
} catch (e, st) {
//
debugPrint('Video init error: $e\n$st');
if (!mounted) return;
setState(() {
_initializing = false;
_error = e.toString();
});
}
}
@override
@ -60,12 +118,10 @@ class _VideoPlayerPopupState extends State<VideoPlayerPopup> {
),
child: Stack(
children: [
//
if (_chewieController != null &&
_videoController.value.isInitialized)
Chewie(controller: _chewieController!)
else
const Center(child: CircularProgressIndicator()),
// /
Positioned.fill(
child: _buildPlayerBody(),
),
//
Positioned(
@ -73,7 +129,9 @@ class _VideoPlayerPopupState extends State<VideoPlayerPopup> {
right: 4,
child: IconButton(
icon: const Icon(Icons.close, color: Colors.white),
onPressed: () => Navigator.of(context).pop(),
onPressed: () {
Navigator.of(context).pop();
},
),
),
],
@ -82,4 +140,28 @@ class _VideoPlayerPopupState extends State<VideoPlayerPopup> {
),
);
}
Widget _buildPlayerBody() {
if (_initializing) {
return const Center(child: CircularProgressIndicator());
}
if (_error != null) {
return Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'播放失败:$_error',
style: const TextStyle(color: Colors.white),
),
),
);
}
if (_chewieController != null && _videoController.value.isInitialized) {
return Chewie(controller: _chewieController!);
}
return const Center(child: Text('无法播放视频', style: TextStyle(color: Colors.white)));
}
}

View File

@ -1,9 +1,12 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:image_picker/image_picker.dart';
import 'package:qhd_prevention/tools/VideoConverter.dart';
import 'package:video_compress/video_compress.dart';
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:path/path.dart' as p;
import 'ItemWidgetFactory.dart';
///
@ -22,7 +25,6 @@ class MediaPickerRow extends StatefulWidget {
final bool isEdit; //
final bool isCamera; //
const MediaPickerRow({
Key? key,
this.maxCount = 4,
@ -43,6 +45,7 @@ class MediaPickerRow extends StatefulWidget {
class _MediaPickerGridState extends State<MediaPickerRow> {
final ImagePicker _picker = ImagePicker();
late List<String> _mediaPaths;
bool _isProcessing = false; // loading
@override
void initState() {
@ -56,16 +59,81 @@ class _MediaPickerGridState extends State<MediaPickerRow> {
);
});
}
Future<void> _cameraAction() async {
XFile? picked = await _picker.pickImage(source: ImageSource.camera);
if (picked != null) {
final path = picked.path;
setState(() => _mediaPaths.add(path));
widget.onChanged(_mediaPaths.map((p) => File(p)).toList());
widget.onMediaAdded?.call(path);
// mov/avi
Future<void> _handlePickedPath(String path) async {
if (!mounted) return;
if (path.isEmpty) return;
try {
String finalPath = path;
// mp4 video_compress
if (widget.mediaType == MediaType.video) {
final ext = p.extension(path).toLowerCase();
if (ext != '.mp4') {
setState(() => _isProcessing = true);
try {
final info = await VideoCompress.compressVideo(
path,
quality: VideoQuality.MediumQuality,
deleteOrigin: false,
);
if (info != null && info.file != null) {
finalPath = info.file!.path;
debugPrint('✅ 转换完成: $path -> $finalPath');
} else {
throw Exception("转码失败: 返回空文件");
}
} catch (e) {
debugPrint('❌ 视频转码失败: $e');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('视频转码失败: ${e.toString()}')),
);
}
return;
} finally {
if (mounted) setState(() => _isProcessing = false);
}
}
}
//
if (_mediaPaths.length < widget.maxCount) {
setState(() => _mediaPaths.add(finalPath));
widget.onChanged(_mediaPaths.map((p) => File(p)).toList());
widget.onMediaAdded?.call(finalPath);
}
} catch (e) {
debugPrint('处理选中媒体失败: $e');
}
}
Future<void> _cameraAction() async {
if (!widget.isEdit || _mediaPaths.length >= widget.maxCount) return;
try {
if (widget.mediaType == MediaType.image) {
XFile? picked = await _picker.pickImage(source: ImageSource.camera);
if (picked != null) {
final path = picked.path;
setState(() => _mediaPaths.add(path));
widget.onChanged(_mediaPaths.map((p) => File(p)).toList());
widget.onMediaAdded?.call(path);
}
} else {
// video from camera
XFile? picked = await _picker.pickVideo(source: ImageSource.camera);
if (picked != null) {
await _handlePickedPath(picked.path);
}
}
} catch (e) {
debugPrint('拍摄失败: $e');
}
}
Future<void> _showPickerOptions() async {
if (!widget.isEdit) return; //
@ -78,13 +146,9 @@ class _MediaPickerGridState extends State<MediaPickerRow> {
ListTile(
titleAlignment: ListTileTitleAlignment.center,
leading: Icon(
widget.mediaType == MediaType.image
? Icons.camera_alt
: Icons.videocam,
),
title: Text(
widget.mediaType == MediaType.image ? '拍照' : '拍摄视频',
widget.mediaType == MediaType.image ? Icons.camera_alt : Icons.videocam,
),
title: Text(widget.mediaType == MediaType.image ? '拍照' : '拍摄视频'),
onTap: () {
Navigator.of(context).pop();
_pickCamera();
@ -93,15 +157,9 @@ class _MediaPickerGridState extends State<MediaPickerRow> {
ListTile(
titleAlignment: ListTileTitleAlignment.center,
leading: Icon(
widget.mediaType == MediaType.image
? Icons.photo_library
: Icons.video_library,
),
title: Text(
widget.mediaType == MediaType.image
? '从相册选择'
: '从相册选择视频',
widget.mediaType == MediaType.image ? Icons.photo_library : Icons.video_library,
),
title: Text(widget.mediaType == MediaType.image ? '从相册选择' : '从相册选择视频'),
onTap: () {
Navigator.of(context).pop();
_pickGallery();
@ -126,14 +184,17 @@ class _MediaPickerGridState extends State<MediaPickerRow> {
XFile? picked;
if (widget.mediaType == MediaType.image) {
picked = await _picker.pickImage(source: ImageSource.camera);
if (picked != null) {
final path = picked.path;
setState(() => _mediaPaths.add(path));
widget.onChanged(_mediaPaths.map((p) => File(p)).toList());
widget.onMediaAdded?.call(path);
}
} else {
picked = await _picker.pickVideo(source: ImageSource.camera);
}
if (picked != null) {
final path = picked.path;
setState(() => _mediaPaths.add(path));
widget.onChanged(_mediaPaths.map((p) => File(p)).toList());
widget.onMediaAdded?.call(path);
if (picked != null) {
await _handlePickedPath(picked.path);
}
}
} catch (e) {
debugPrint('拍摄失败: $e');
@ -156,9 +217,7 @@ class _MediaPickerGridState extends State<MediaPickerRow> {
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,
),
@ -169,8 +228,8 @@ class _MediaPickerGridState extends State<MediaPickerRow> {
final file = await asset.file;
if (file != null) {
final path = file.path;
_mediaPaths.add(path);
widget.onMediaAdded?.call(path);
//
await _handlePickedPath(path);
}
}
setState(() {});
@ -195,75 +254,89 @@ class _MediaPickerGridState extends State<MediaPickerRow> {
final showAddButton = widget.isEdit && _mediaPaths.length < widget.maxCount;
final itemCount = _mediaPaths.length + (showAddButton ? 1 : 0);
return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
childAspectRatio: 1,
// mainAxisExtent: 80,
),
itemCount: itemCount,
itemBuilder: (context, index) {
//
if (index < _mediaPaths.length) {
final path = _mediaPaths[index];
final isNetwork = path.startsWith('http');
return Stack(
children: [
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
childAspectRatio: 1,
),
itemCount: itemCount,
itemBuilder: (context, index) {
//
if (index < _mediaPaths.length) {
final path = _mediaPaths[index];
final isNetwork = path.startsWith('http');
return GestureDetector(
onTap: () => widget.onMediaTapped?.call(path),
child: Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(5),
child: widget.mediaType == MediaType.image
? (isNetwork
? Image.network(path, fit: BoxFit.cover, width: 80, height: 80)
: Image.file(File(path), width: 80, height: 80, fit: BoxFit.cover))
: Container(
color: Colors.black12,
child: const Center(
child: Icon(
Icons.videocam,
color: Colors.white70,
return GestureDetector(
onTap: () => widget.onMediaTapped?.call(path),
child: Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(5),
child: widget.mediaType == MediaType.image
? (isNetwork
? Image.network(path, fit: BoxFit.cover, width: 80, height: 80)
: Image.file(File(path), width: 80, height: 80, fit: BoxFit.cover))
: 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),
onPressed: () => _removeMedia(index),
),
),
],
),
);
}
//
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),
),
),
//
if (widget.isEdit)
Positioned(
top: -15,
right: -15,
child: IconButton(
icon: const Icon(Icons.cancel, size: 20, color: Colors.red),
onPressed: () => _removeMedia(index),
),
),
],
),
);
}
//
else if (showAddButton) {
return GestureDetector(
onTap: widget.isCamera?_cameraAction:_showPickerOptions,
);
} else {
return const SizedBox.shrink();
}
},
),
// / loading
if (_isProcessing)
Positioned.fill(
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.black12),
borderRadius: BorderRadius.circular(5),
),
color: Colors.transparent,
child: const Center(
child: Icon(Icons.camera_alt, color: Colors.black26),
child: CircularProgressIndicator(),
),
),
);
} else {
return const SizedBox.shrink();
}
},
),
],
);
}
}
@ -287,7 +360,6 @@ class RepairedPhotoSection extends StatefulWidget {
final bool isEdit; //
final bool isCamera; //
const RepairedPhotoSection({
Key? key,
this.maxCount = 4,
@ -388,4 +460,4 @@ class _RepairedPhotoSectionState extends State<RepairedPhotoSection> {
),
);
}
}
}

View File

@ -4,17 +4,21 @@ import 'package:flutter/material.dart';
///
/// DateTime? picked = await BottomDateTimePicker.showDate(
/// context,
/// allowFuture: true, //
/// minTimeStr: '2025-08-20 08:30', //
/// mode: BottomPickerMode.date, // BottomPickerMode.dateTime
/// allowFuture: true,
/// minTimeStr: '2025-08-20 08:30',
/// );
/// if (picked != null) {
/// print('用户选择的时间:$picked');
/// }
enum BottomPickerMode { dateTime, date }
class BottomDateTimePicker {
static Future<DateTime?> showDate(
BuildContext context, {
bool allowFuture = false,
String? minTimeStr, // 'yyyy-MM-dd HH:mm'
String? minTimeStr, // 'yyyy-MM-dd HH:mm'
BottomPickerMode mode = BottomPickerMode.dateTime,
}) {
return showModalBottomSheet<DateTime>(
context: context,
@ -26,19 +30,22 @@ class BottomDateTimePicker {
builder: (_) => _InlineDateTimePickerContent(
allowFuture: allowFuture,
minTimeStr: minTimeStr,
mode: mode,
),
);
}
}
class _InlineDateTimePickerContent extends StatefulWidget {
final bool allowFuture; //
final String? minTimeStr; // 'yyyy-MM-dd HH:mm'
final bool allowFuture;
final String? minTimeStr;
final BottomPickerMode mode;
const _InlineDateTimePickerContent({
Key? key,
this.allowFuture = false,
this.minTimeStr,
this.mode = BottomPickerMode.dateTime,
}) : super(key: key);
@override
@ -80,13 +87,18 @@ class _InlineDateTimePickerContentState
// minTimeStr
_minTime = _parseMinTime(widget.minTimeStr);
// DateTime.now() _minTime
// now _minTime
final now = DateTime.now();
DateTime initial = now;
if (_minTime != null && _minTime!.isAfter(initial)) {
initial = _minTime!;
}
// date
if (widget.mode == BottomPickerMode.date) {
initial = DateTime(initial.year, initial.month, initial.day);
}
selectedYear = initial.year;
selectedMonth = initial.month;
selectedDay = initial.day;
@ -99,12 +111,16 @@ class _InlineDateTimePickerContentState
// controllers
yearCtrl = FixedExtentScrollController(
initialItem: years.indexOf(selectedYear).clamp(0, years.length - 1));
monthCtrl = FixedExtentScrollController(initialItem: (selectedMonth - 1).clamp(0, months.length - 1));
dayCtrl = FixedExtentScrollController(initialItem: (selectedDay - 1).clamp(0, days.length - 1));
hourCtrl = FixedExtentScrollController(initialItem: selectedHour.clamp(0, hours.length - 1));
minuteCtrl = FixedExtentScrollController(initialItem: selectedMinute.clamp(0, minutes.length - 1));
monthCtrl = FixedExtentScrollController(
initialItem: (selectedMonth - 1).clamp(0, months.length - 1));
dayCtrl = FixedExtentScrollController(
initialItem: (selectedDay - 1).clamp(0, days.length - 1));
hourCtrl = FixedExtentScrollController(
initialItem: selectedHour.clamp(0, hours.length - 1));
minuteCtrl = FixedExtentScrollController(
initialItem: selectedMinute.clamp(0, minutes.length - 1));
// minTime
// minTime
WidgetsBinding.instance.addPostFrameCallback((_) {
_enforceConstraintsAndUpdateControllers();
});
@ -122,7 +138,8 @@ class _InlineDateTimePickerContentState
try {
final trimmed = s.trim();
final parts = trimmed.split(' ');
final dateParts = parts[0].split('-').map((e) => int.parse(e)).toList();
final dateParts =
parts[0].split('-').map((e) => int.parse(e)).toList();
final timeParts = (parts.length > 1)
? parts[1].split(':').map((e) => int.parse(e)).toList()
: [0, 0];
@ -133,7 +150,6 @@ class _InlineDateTimePickerContentState
final minute = (timeParts.length > 1) ? timeParts[1] : 0;
return DateTime(year, month, day, hour, minute);
} catch (e) {
//
debugPrint('parseMinTime failed for "$s": $e');
return null;
}
@ -153,49 +169,72 @@ class _InlineDateTimePickerContentState
});
}
//
// 1) _minTime _minTime
// 2) _minTime allowFuture now
// _minTime allowFuture _minTime _minTime
//
void _enforceConstraintsAndUpdateControllers() {
final picked = DateTime(selectedYear, selectedMonth, selectedDay, selectedHour, selectedMinute);
final now = DateTime.now();
final isDateOnly = widget.mode == BottomPickerMode.date;
// 1)
if (_minTime != null && picked.isBefore(_minTime!)) {
final m = _minTime!;
selectedYear = m.year;
selectedMonth = m.month;
selectedDay = m.day;
selectedHour = m.hour;
selectedMinute = m.minute;
final DateTime picked = isDateOnly
? DateTime(selectedYear, selectedMonth, selectedDay)
: DateTime(
selectedYear, selectedMonth, selectedDay, selectedHour, selectedMinute);
//
_updateDays(jumpDay: false);
yearCtrl.jumpToItem(years.indexOf(selectedYear));
monthCtrl.jumpToItem(selectedMonth - 1);
dayCtrl.jumpToItem(selectedDay - 1);
hourCtrl.jumpToItem(selectedHour);
minuteCtrl.jumpToItem(selectedMinute);
return;
// _minTime date
if (_minTime != null) {
final DateTime minRef = isDateOnly
? DateTime(_minTime!.year, _minTime!.month, _minTime!.day)
: _minTime!;
if (picked.isBefore(minRef)) {
// minRef
selectedYear = minRef.year;
selectedMonth = minRef.month;
selectedDay = minRef.day;
if (!isDateOnly) {
selectedHour = minRef.hour;
selectedMinute = minRef.minute;
} else {
selectedHour = 0;
selectedMinute = 0;
}
//
_updateDays(jumpDay: false);
yearCtrl.jumpToItem(years.indexOf(selectedYear));
monthCtrl.jumpToItem(selectedMonth - 1);
dayCtrl.jumpToItem(selectedDay - 1);
if (!isDateOnly) {
hourCtrl.jumpToItem(selectedHour);
minuteCtrl.jumpToItem(selectedMinute);
}
return;
}
}
// 2) allowFuture == false minTime minTime <= now
// allowFuture == false
if (!widget.allowFuture) {
final now = DateTime.now();
// minTime nowminTime
if (picked.isAfter(now)) {
selectedYear = now.year;
selectedMonth = now.month;
selectedDay = now.day;
selectedHour = now.hour;
selectedMinute = now.minute;
final DateTime nowRef = isDateOnly
? DateTime(now.year, now.month, now.day)
: now;
if (picked.isAfter(nowRef)) {
selectedYear = nowRef.year;
selectedMonth = nowRef.month;
selectedDay = nowRef.day;
if (!isDateOnly) {
selectedHour = nowRef.hour;
selectedMinute = nowRef.minute;
} else {
selectedHour = 0;
selectedMinute = 0;
}
_updateDays(jumpDay: false);
yearCtrl.jumpToItem(years.indexOf(selectedYear));
monthCtrl.jumpToItem(selectedMonth - 1);
dayCtrl.jumpToItem(selectedDay - 1);
hourCtrl.jumpToItem(selectedHour);
minuteCtrl.jumpToItem(selectedMinute);
if (!isDateOnly) {
hourCtrl.jumpToItem(selectedHour);
minuteCtrl.jumpToItem(selectedMinute);
}
return;
}
}
@ -213,8 +252,9 @@ class _InlineDateTimePickerContentState
@override
Widget build(BuildContext context) {
final isDateOnly = widget.mode == BottomPickerMode.date;
return SizedBox(
height: 330,
height: isDateOnly ? 280 : 330,
child: Column(
children: [
//
@ -229,7 +269,9 @@ class _InlineDateTimePickerContentState
),
TextButton(
onPressed: () {
final result = DateTime(
final result = isDateOnly
? DateTime(selectedYear, selectedMonth, selectedDay)
: DateTime(
selectedYear,
selectedMonth,
selectedDay,
@ -245,7 +287,7 @@ class _InlineDateTimePickerContentState
),
const Divider(height: 1),
//
// date
Expanded(
child: Row(
children: [
@ -281,7 +323,6 @@ class _InlineDateTimePickerContentState
items: days.map((e) => e.toString().padLeft(2, '0')).toList(),
onSelected: (idx) {
setState(() {
// idx days
final safeIdx = idx.clamp(0, days.length - 1);
selectedDay = days[safeIdx];
_enforceConstraintsAndUpdateControllers();
@ -289,29 +330,30 @@ class _InlineDateTimePickerContentState
},
),
//
_buildPicker(
controller: hourCtrl,
items: hours.map((e) => e.toString().padLeft(2, '0')).toList(),
onSelected: (idx) {
setState(() {
selectedHour = hours[idx];
_enforceConstraintsAndUpdateControllers();
});
},
),
// dateOnly
if (!isDateOnly)
_buildPicker(
controller: hourCtrl,
items: hours.map((e) => e.toString().padLeft(2, '0')).toList(),
onSelected: (idx) {
setState(() {
selectedHour = hours[idx];
_enforceConstraintsAndUpdateControllers();
});
},
),
//
_buildPicker(
controller: minuteCtrl,
items: minutes.map((e) => e.toString().padLeft(2, '0')).toList(),
onSelected: (idx) {
setState(() {
selectedMinute = minutes[idx];
_enforceConstraintsAndUpdateControllers();
});
},
),
if (!isDateOnly)
_buildPicker(
controller: minuteCtrl,
items: minutes.map((e) => e.toString().padLeft(2, '0')).toList(),
onSelected: (idx) {
setState(() {
selectedMinute = minutes[idx];
_enforceConstraintsAndUpdateControllers();
});
},
),
],
),
),

View File

@ -3,6 +3,7 @@ import 'dart:math';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:qhd_prevention/tools/tools.dart';
import 'package:video_player/video_player.dart';
class VideoPlayerWidget extends StatefulWidget {
@ -28,57 +29,121 @@ class VideoPlayerWidget extends StatefulWidget {
class _VideoPlayerWidgetState extends State<VideoPlayerWidget> {
bool _visibleControls = true;
Timer? _hideTimer;
late Timer _positionTimer;
Timer? _positionTimer;
Duration _currentPosition = Duration.zero;
Duration _totalDuration = Duration.zero;
bool _isPlaying = false;
final ValueNotifier<double> _sliderValue = ValueNotifier(0.0);
final ValueNotifier<double> _sliderValue = ValueNotifier<double>(0.0);
// Helpers to avoid accessing controller after it's disposed
bool get _hasController => widget.controller != null;
/// Safe check whether controller is initialized without throwing.
bool _controllerInitializedSafe() {
try {
final c = widget.controller;
if (c == null) return false;
return c.value.isInitialized;
} catch (e) {
return false;
}
}
@override
void initState() {
super.initState();
// start hide timer immediately
_startHideTimer();
_startPositionTimer();
if (widget.controller != null) {
widget.controller!.addListener(_controllerListener);
if (widget.controller!.value.isInitialized) {
_updateControllerValues();
}
// start position timer only if controller exists and initialized
_maybeStartPositionTimer();
// add listener if controller present
_addControllerListenerSafely();
// if controller already initialized, fetch values
if (_controllerInitializedSafe()) {
_updateControllerValuesSafe();
}
}
@override
void didUpdateWidget(covariant VideoPlayerWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.controller != oldWidget.controller) {
oldWidget.controller?.removeListener(_controllerListener);
// controller changed -> update listeners and timers
if (oldWidget.controller != widget.controller) {
_removeControllerListenerSafely(oldWidget.controller);
_addControllerListenerSafely();
_restartPositionTimer();
_updateControllerValuesSafe();
}
}
void _addControllerListenerSafely() {
try {
widget.controller?.addListener(_controllerListener);
_updateControllerValues();
} catch (_) {
// ignore if controller already disposed
}
}
void _removeControllerListenerSafely([VideoPlayerController? ctrl]) {
final c = ctrl ?? widget.controller;
if (c == null) return;
try {
c.removeListener(_controllerListener);
} catch (_) {
// ignore
}
}
void _controllerListener() {
if (!mounted) return;
_updateControllerValues();
_updateControllerValuesSafe();
}
void _updateControllerValues() {
final c = widget.controller!;
setState(() {
_isPlaying = c.value.isPlaying;
_totalDuration = c.value.duration;
_currentPosition = c.value.position;
_sliderValue.value = _currentPosition.inMilliseconds.toDouble();
});
}
void _updateControllerValuesSafe() {
final c = widget.controller;
if (c == null) {
// clear values
if (mounted) {
setState(() {
_isPlaying = false;
_totalDuration = Duration.zero;
_currentPosition = Duration.zero;
_sliderValue.value = 0;
});
}
return;
}
@override
void dispose() {
_hideTimer?.cancel();
_positionTimer.cancel();
_sliderValue.dispose();
widget.controller?.removeListener(_controllerListener);
super.dispose();
try {
final value = c.value;
if (!value.isInitialized) {
if (mounted) {
setState(() {
_isPlaying = false;
_totalDuration = Duration.zero;
_currentPosition = Duration.zero;
_sliderValue.value = 0;
});
}
return;
}
final pos = value.position;
final dur = value.duration;
final playing = value.isPlaying;
if (mounted) {
setState(() {
_isPlaying = playing;
_totalDuration = dur;
_currentPosition = pos;
_sliderValue.value = pos.inMilliseconds.toDouble();
});
}
} catch (e) {
// If controller was disposed between checks, ignore
}
}
void _startHideTimer() {
@ -88,44 +153,80 @@ class _VideoPlayerWidgetState extends State<VideoPlayerWidget> {
});
}
void _startPositionTimer() {
_positionTimer = Timer.periodic(const Duration(milliseconds: 200), (_) {
if (!mounted ||
widget.controller == null ||
!widget.controller!.value.isInitialized) return;
setState(() {
void _maybeStartPositionTimer() {
if (_positionTimer != null && _positionTimer!.isActive) return;
// only start if controller exists
if (!_hasController) return;
_positionTimer = Timer.periodic(const Duration(milliseconds: 300), (_) {
if (!mounted) return;
if (!_controllerInitializedSafe()) return;
try {
final c = widget.controller!;
_currentPosition = c.value.position;
_totalDuration = c.value.duration;
_isPlaying = c.value.isPlaying;
_sliderValue.value = _currentPosition.inMilliseconds.toDouble();
});
final pos = c.value.position;
final dur = c.value.duration;
final playing = c.value.isPlaying;
setState(() {
_currentPosition = pos;
_totalDuration = dur;
_isPlaying = playing;
_sliderValue.value = pos.inMilliseconds.toDouble();
});
} catch (_) {
// ignore if controller disposed mid-tick
}
});
}
void _restartPositionTimer() {
_positionTimer?.cancel();
_maybeStartPositionTimer();
}
void _stopPositionTimer() {
try {
_positionTimer?.cancel();
} catch (_) {}
_positionTimer = null;
}
void _toggleControls() {
setState(() => _visibleControls = !_visibleControls);
if (_visibleControls) _startHideTimer();
else _hideTimer?.cancel();
if (_visibleControls)
_startHideTimer();
else
_hideTimer?.cancel();
}
void _togglePlayPause() {
if (widget.controller == null) return;
setState(() => _isPlaying = !_isPlaying);
if (_isPlaying) widget.controller!.play();
else widget.controller!.pause();
_startHideTimer();
final c = widget.controller;
if (c == null) return;
try {
if (_isPlaying) {
c.pause();
setState(() => _isPlaying = false);
} else {
c.play();
setState(() => _isPlaying = true);
}
_startHideTimer();
} catch (_) {
// ignore if controller disposed
}
}
void _enterFullScreen() {
SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
Future<void> _enterFullScreen() async {
// prepare full screen orientation
try {
await SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
await NativeOrientation.setLandscape();
} catch (_) {}
Navigator.of(context)
.push(
// push a new page with the same widget but isFullScreen = true
await Navigator.of(context).push(
MaterialPageRoute(
builder: (ctx) => Scaffold(
backgroundColor: Colors.black,
@ -145,153 +246,95 @@ class _VideoPlayerWidgetState extends State<VideoPlayerWidget> {
),
),
),
)
.then((_) {
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
});
);
// on return, restore orientation and UI and force rebuild
try {
await NativeOrientation.setPortrait();
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
} catch (_) {}
if (!mounted) return;
// force a rebuild so parent layouts recalc (fixes overflow after exit)
setState(() {});
}
@override
void dispose() {
_hideTimer?.cancel();
_stopPositionTimer();
_sliderValue.dispose();
_removeControllerListenerSafely();
super.dispose();
}
@override
Widget build(BuildContext context) {
final screenW = MediaQuery.of(context).size.width;
final containerW = widget.isFullScreen ? double.infinity : screenW;
// Compute constrained size so widget won't try to be infinite height in non-fullscreen usage.
final media = MediaQuery.of(context);
final screenW = media.size.width;
final screenH = media.size.height;
// Non-fullscreen preferred height: based on aspect ratio but not exceeding a fraction of screen height
final preferredNonFullHeight = min(screenW / (widget.aspectRatio <= 0 ? (16 / 9) : widget.aspectRatio),
screenH * 0.5); // at most 50% of screen height
// If isFullScreen, use available height minus safe areas; otherwise use preferredNonFullHeight
final containerW = widget.isFullScreen ? screenW : screenW;
final containerH = widget.isFullScreen
? double.infinity
: containerW / widget.aspectRatio;
? screenH
: preferredNonFullHeight;
return Center(
child: SizedBox(
width: containerW,
height: containerH,
child: GestureDetector(
behavior: HitTestBehavior.translucent, //
behavior: HitTestBehavior.translucent,
onTap: _toggleControls,
child: Stack(
fit: StackFit.expand,
children: [
//
if (widget.controller != null &&
widget.controller!.value.isInitialized)
FittedBox(
fit: BoxFit.contain,
alignment: Alignment.center,
child: SizedBox(
width: widget.controller!.value.size.width,
height: widget.controller!.value.size.height,
child: VideoPlayer(widget.controller!),
),
)
else
if (widget.coverUrl.length > 0)
Image.network(
widget.coverUrl,
fit: BoxFit.cover,
width: containerW,
height: containerH,
),
//
if (_visibleControls)
// Video or cover: use AspectRatio to avoid forcing Column to expand
_buildVideoOrCover(containerW, containerH),
if (widget.isFullScreen)
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
height: 50,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Colors.black.withOpacity(0.7),
Colors.transparent
],
),
),
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
children: [
IconButton(
padding: EdgeInsets.zero,
icon: Icon(
_isPlaying ? Icons.pause : Icons.play_arrow,
size: 28,
color: Colors.white,
),
onPressed: _togglePlayPause,
),
Expanded(
child: ValueListenableBuilder<double>(
valueListenable: _sliderValue,
builder: (_, value, __) => SliderTheme(
data: SliderTheme.of(context).copyWith(
activeTrackColor: Colors.white,
inactiveTrackColor: Colors.white54,
thumbColor: Colors.white,
overlayColor: Colors.white24,
trackHeight: 2,
thumbShape: RoundSliderThumbShape(
enabledThumbRadius: 8),
),
child: SliderTheme(
data: SliderTheme.of(context).copyWith(
activeTrackColor: Colors.white, //
inactiveTrackColor: Colors.grey[400],//
thumbColor: Colors.white, //
overlayColor: Colors.white.withAlpha(0x33), //
disabledActiveTrackColor: Colors.white, //
disabledInactiveTrackColor: Colors.grey[400],
disabledThumbColor: Colors.white,
),
child: Slider(
value: value,
min: 0,
max: _totalDuration.inMilliseconds.toDouble(),
// allowSeek onChanged
onChanged: (v) {
if (widget.allowSeek && widget.controller != null) {
widget.controller!.seekTo(Duration(milliseconds: v.toInt()));
setState(() => _currentPosition = Duration(milliseconds: v.toInt()));
_sliderValue.value = v;
_startHideTimer();
}
},
),
),
),
),
),
SizedBox(
width: 110,
child: Text(
'${_formatDuration(_currentPosition)} / ${_formatDuration(_totalDuration)}',
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontFeatures: [FontFeature.tabularFigures()],
),
),
),
IconButton(
padding: EdgeInsets.zero,
icon: Icon(
widget.isFullScreen
? Icons.fullscreen_exit
: Icons.fullscreen,
size: 28,
color: Colors.white,
),
onPressed: () {
widget.isFullScreen
? Navigator.of(context).pop()
: _enterFullScreen();
top: MediaQuery.of(context).padding.top + 8, // /
left: 8,
child: SafeArea(
top: true,
bottom: false,
child: ClipOval(
child: Material(
color: Colors.black38, //
child: InkWell(
onTap: () async {
// pop UI pop .then
try {
await NativeOrientation.setPortrait();
} catch (_) {}
try {
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
} catch (_) {}
Navigator.of(context).maybePop();
},
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(
Icons.arrow_back,
color: Colors.white,
size: 22,
),
),
),
],
),
),
),
),
// Controls overlay
if (_visibleControls) _buildControls(),
],
),
),
@ -299,6 +342,139 @@ class _VideoPlayerWidgetState extends State<VideoPlayerWidget> {
);
}
Widget _buildVideoOrCover(double containerW, double containerH) {
final c = widget.controller;
// If controller exists and is initialized (safe check), show video in AspectRatio
if (c != null) {
try {
if (c.value.isInitialized) {
// Use video's natural aspect ratio if available, otherwise widget.aspectRatio
final vidAspect = (c.value.size.height > 0) ? (c.value.size.width / c.value.size.height) : widget.aspectRatio;
return Center(
child: AspectRatio(
aspectRatio: vidAspect > 0 ? vidAspect : widget.aspectRatio,
child: VideoPlayer(c),
),
);
}
} catch (_) {
// controller may be disposed; fall through to show cover
}
}
// Fallback: show cover image (fills the container)
if (widget.coverUrl.isNotEmpty) {
return Image.network(
widget.coverUrl,
width: containerW,
height: containerH,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => Container(color: Colors.black),
);
}
// final fallback: black box
return Container(color: Colors.black);
}
Widget _buildControls() {
// Use local copies to avoid repeated reads that might throw if ctrl disposed
final totalMs = _totalDuration.inMilliseconds.toDouble();
final sliderMax = totalMs > 0 ? totalMs : 1.0;
final sliderValue = _sliderValue.value.clamp(0.0, sliderMax).toDouble();
return Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
height: 50,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Colors.black.withOpacity(0.7),
Colors.transparent,
],
),
),
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
children: [
IconButton(
padding: EdgeInsets.zero,
icon: Icon(
_isPlaying ? Icons.pause : Icons.play_arrow,
size: 28,
color: Colors.white,
),
onPressed: _togglePlayPause,
),
Expanded(
child: ValueListenableBuilder<double>(
valueListenable: _sliderValue,
builder: (_, value, __) => SliderTheme(
data: SliderTheme.of(context).copyWith(
activeTrackColor: Colors.white,
inactiveTrackColor: Colors.white54,
thumbColor: Colors.white,
overlayColor: Colors.white24,
trackHeight: 2,
thumbShape: RoundSliderThumbShape(enabledThumbRadius: 8),
),
child: Slider(
value: sliderValue,
min: 0,
max: sliderMax,
onChanged: (v) {
if (widget.allowSeek && widget.controller != null) {
try {
widget.controller!.seekTo(Duration(milliseconds: v.toInt()));
setState(() => _currentPosition = Duration(milliseconds: v.toInt()));
_sliderValue.value = v;
_startHideTimer();
} catch (_) {}
}
},
),
),
),
),
SizedBox(
width: 110,
child: Text(
'${_formatDuration(_currentPosition)} / ${_formatDuration(_totalDuration)}',
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontFeatures: [FontFeature.tabularFigures()],
),
),
),
IconButton(
padding: EdgeInsets.zero,
icon: Icon(
widget.isFullScreen ? Icons.fullscreen_exit : Icons.fullscreen,
size: 28,
color: Colors.white,
),
onPressed: () {
if (widget.isFullScreen) {
// if this widget is used inside a full screen route, popping will exit fullscreen
Navigator.of(context).maybePop();
} else {
_enterFullScreen();
}
},
),
],
),
),
);
}
String _formatDuration(Duration d) {
String twoDigits(int n) => n.toString().padLeft(2, '0');
final h = d.inHours, m = d.inMinutes.remainder(60), s = d.inSeconds.remainder(60);

View File

@ -20,10 +20,11 @@ class ApiService {
// static const String publicKey = 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDUoHAavCikaZxjlDM6Km8cX+ye78F4oF39AcEfnE1p2Yn9pJ9WFxYZ4Vkh6F8SKMi7k4nYsKceqB1RwG996SvHQ5C3pM3nbXCP4K15ad6QhN4a7lzlbLhiJcyIKszvvK8ncUDw8mVQ0j/2mwxv05yH6LN9OKU6Hzm1ninpWeE+awIDAQAB'
///
static const String baseFacePath =
"https://qaaqwh.qhdsafety.com/whb_stu_face/";
"https://qaaqwh.qhdsafety.com/whb_stu_face";
///
static const String basePath = "https://qaaqwh.qhdsafety.com/integrated_whb";
// static const String basePath = "http://192.168.0.37:8099/api";
///
static const String baseImgPath = "https://file.zcloudchina.com/YTHFile";
@ -36,8 +37,6 @@ class ApiService {
static const String projectManagerUrl =
'https://pm.qhdsafety.com/zy-projectManage';
/// NFC
static const String baseNFCPath = "http://192.168.0.37:8099/api/app";
// ///
// static const String baseFacePath =
@ -357,6 +356,19 @@ U6Hzm1ninpWeE+awIDAQAB
},
);
}
static Future<Map<String, dynamic>> getVideoPermissions() {
return HttpManager().request(
basePath,
'/app/coursestudyvideorecord/getVideoPermissions',
method: Method.post,
withToken: true,
data: {
'USERNAME': SessionService.instance.username,
'CORPINFO_ID': SessionService.instance.corpinfoId,
'USER_ID': SessionService.instance.loginUserId,
},
);
}
///
static Future<Map<String, dynamic>> getStudyDetailList(
@ -379,6 +391,7 @@ U6Hzm1ninpWeE+awIDAQAB
basePath,
'/app/edu/stagestudentrelation/getMyTask',
method: Method.post,
withToken: true,
data: {
'CLASSCURRICULUM_ID': CLASSCURRICULUM_ID,
'CLASS_ID': CLASS_ID,
@ -397,6 +410,7 @@ U6Hzm1ninpWeE+awIDAQAB
basePath,
'/app/edu/audioOrVideo/getVideoPlayInfoApp',
method: Method.post,
withToken: true,
data: {
'VIDEOCOURSEWARE_ID': VIDEOCOURSEWARE_ID,
'CORPINFO_ID': SessionService.instance.corpinfoId,
@ -424,6 +438,7 @@ U6Hzm1ninpWeE+awIDAQAB
baseFacePath,
'/app/user/getUserFaceTime',
method: Method.post,
withToken: true,
data: {
'loading': false,
'FACE_TIME': FACE_TIME,
@ -457,6 +472,7 @@ U6Hzm1ninpWeE+awIDAQAB
basePath,
'/app/edu/coursestudyvideorecord/getVideoProgress',
method: Method.post,
withToken: true,
data: {
'VIDEOCOURSEWARE_ID': VIDEOCOURSEWARE_ID,
'CURRICULUM_ID': CURRICULUM_ID,
@ -469,34 +485,19 @@ U6Hzm1ninpWeE+awIDAQAB
}
///
static Future<Map<String, dynamic>> fnSubmitPlayTime(
String VIDEOCOURSEWARE_ID,
String CURRICULUM_ID,
String IS_END,
int RESOURCETIME,
String CHAPTER_ID,
String STUDENT_ID,
String CLASSCURRICULUM_ID,
String CLASS_ID,
) {
static Future<Map<String, dynamic>> fnSubmitPlayTime(Map data) {
return HttpManager().request(
basePath,
'/app/edu/coursestudyvideorecord/save',
method: Method.post,
withToken: true,
data: {
'VIDEOCOURSEWARE_ID': VIDEOCOURSEWARE_ID,
'CURRICULUM_ID': CURRICULUM_ID,
'CHAPTER_ID': CHAPTER_ID,
'RESOURCETIME': RESOURCETIME,
'IS_END': IS_END,
'CLASS_ID': CLASS_ID,
'CLASSCURRICULUM_ID': CLASSCURRICULUM_ID,
'STUDENT_ID': STUDENT_ID,
'loading': false,
...data,
'USER_NAME': SessionService.instance.username,
'CORPINFO_ID': SessionService.instance.corpinfoId,
'USER_ID': SessionService.instance.loginUserId,
},
);
}
@ -3697,8 +3698,8 @@ U6Hzm1ninpWeE+awIDAQAB
///
static Future<Map<String, dynamic>> getNfcPipeLineAreaList() {
return HttpManager().request(
baseNFCPath,
'/pipelineInspection/getPipelineAreaListAll',
basePath,
'/app/pipelineInspection/getPipelineAreaListAll',
method: Method.post,
data: {"CORPINFO_ID": SessionService.instance.corpinfoId, 'STATUS': '0'},
);
@ -3709,8 +3710,8 @@ U6Hzm1ninpWeE+awIDAQAB
String PIPELINE_AREA_ID,
) {
return HttpManager().request(
baseNFCPath,
'/pipelineInspection/getEquipmentPipelineListAll',
basePath,
'/app/pipelineInspection/getEquipmentPipelineListAll',
method: Method.post,
data: {
"CORPINFO_ID": SessionService.instance.corpinfoId,
@ -3723,8 +3724,8 @@ U6Hzm1ninpWeE+awIDAQAB
///NFC
static Future<Map<String, dynamic>> nfcTagAdd(Map data) {
return HttpManager().request(
baseNFCPath,
'/pipelineInspection/nfcTagAdd',
basePath,
'/app/pipelineInspection/nfcTagAdd',
method: Method.post,
data: {
...data,
@ -3740,8 +3741,8 @@ U6Hzm1ninpWeE+awIDAQAB
int currentPage,
) {
return HttpManager().request(
baseNFCPath,
'/pipelineInspection/getPatrolTaskList?showCount=$showCount&currentPage=$currentPage',
basePath,
'/app/pipelineInspection/getPatrolTaskList?showCount=$showCount&currentPage=$currentPage',
method: Method.post,
data: {
@ -3759,8 +3760,8 @@ U6Hzm1ninpWeE+awIDAQAB
Map data,
) {
return HttpManager().request(
baseNFCPath,
'/pipelineInspection/getPatrolTaskDetailList?showCount=$showCount&currentPage=$currentPage',
basePath,
'/app/pipelineInspection/getPatrolTaskDetailList?showCount=$showCount&currentPage=$currentPage',
method: Method.post,
data: {
...data,
@ -3772,8 +3773,8 @@ U6Hzm1ninpWeE+awIDAQAB
static Future<Map<String, dynamic>> nfcWriteCheck(String NFC_CODE) {
return HttpManager().request(
baseNFCPath,
'/pipelineInspection/nfcTagCheck',
basePath,
'/app/pipelineInspection/nfcTagCheck',
method: Method.post,
data: {
"CORPINFO_ID": SessionService.instance.corpinfoId,
@ -3787,8 +3788,8 @@ U6Hzm1ninpWeE+awIDAQAB
Map data,
) async {
return HttpManager().request(
baseNFCPath,
'/pipelineInspection/goEditNfcExceptionRecord',
basePath,
'/app/pipelineInspection/goEditNfcExceptionRecord',
method: Method.post,
data: {
...data,
@ -3802,8 +3803,8 @@ U6Hzm1ninpWeE+awIDAQAB
Map data,
) async {
return HttpManager().request(
baseNFCPath,
'/pipelineInspection/nfcExceptionRecordAdd',
basePath,
'/app/pipelineInspection/nfcExceptionRecordAdd',
method: Method.post,
data: {
...data,
@ -3817,8 +3818,8 @@ U6Hzm1ninpWeE+awIDAQAB
Map data,
) async {
return HttpManager().request(
baseNFCPath,
'/pipelineInspection/patrolRecordDetailSaveOrUpdate',
basePath,
'/app/pipelineInspection/patrolRecordDetailSaveOrUpdate',
method: Method.post,
data: {
...data,
@ -3832,8 +3833,8 @@ U6Hzm1ninpWeE+awIDAQAB
Map data,
) async {
return HttpManager().request(
baseNFCPath,
'/pipelineInspection/goEditPatrolRecordDetailHidden',
basePath,
'/app/pipelineInspection/goEditPatrolRecordDetailHidden',
method: Method.post,
data: {
...data,
@ -3842,7 +3843,54 @@ U6Hzm1ninpWeE+awIDAQAB
},
);
}
// goEditPatrolTaskDetail
// NFC
static Future<Map<String, dynamic>> addNFCImgFiles(
String imagePath,
Map data,
) async {
final file = File(imagePath);
if (!await file.exists()) {
throw ApiException('file_not_found', '图片不存在:$imagePath');
}
final fileName = file.path.split(Platform.pathSeparator).last;
return HttpManager().uploadFaceImage(
baseUrl: basePath,
path: '/app//imgfiles/add',
fromData: {
...data,
"CORPINFO_ID": SessionService.instance.corpinfoId,
"USER_ID": SessionService.instance.loginUserId,
'FFILE': await MultipartFile.fromFile(file.path, filename: fileName),
},
);
}
///
static Future<Map<String, dynamic>> deleteNFCImage(Map data) {
return HttpManager().request(
basePath,
'/app/app/imgfiles/delete',
method: Method.post,
data: {
...data,
"CORPINFO_ID": SessionService.instance.corpinfoId,
"USER_ID": SessionService.instance.loginUserId,
},
);
}
/// nfc
static Future<Map<String, dynamic>> nfcInspectionRecord(
int showCount,
int currentPage,
) async {
return HttpManager().request(
basePath,
'/app/pipelineInspection/getPatrolRecordList?showCount=$showCount&currentPage=$currentPage',
method: Method.post,
data: {
"CORPINFO_ID": SessionService.instance.corpinfoId,
"USER_ID": SessionService.instance.loginUserId,
},
);
}
}

View File

@ -2,6 +2,7 @@ import 'dart:io';
import 'dart:ui';
import 'package:dio/dio.dart';
import 'package:qhd_prevention/customWidget/toast_util.dart';
import 'package:qhd_prevention/tools/tools.dart';
///
class ApiException implements Exception {
@ -42,24 +43,24 @@ class HttpManager {
..add(InterceptorsWrapper(onError: (err, handler) {
// TODO
// 401
// if (err.response?.statusCode == 401) {
// //
// onUnauthorized?.call();
// //
// final apiException = ApiException(
// '提示',
// '您的账号已在其他设备登录,已自动下线'
// );
// //
// return handler.reject(
// DioException(
// requestOptions: err.requestOptions,
// error: apiException,
// response: err.response,
// type: DioExceptionType.badResponse,
// ),
// );
// }
if (err.response?.statusCode == 401) {
//
onUnauthorized?.call();
//
final apiException = ApiException(
'提示',
'您的账号已在其他设备登录,已自动下线'
);
//
return handler.reject(
DioException(
requestOptions: err.requestOptions,
error: apiException,
response: err.response,
type: DioExceptionType.badResponse,
),
);
}
handler.next(err);
}));
}
@ -72,72 +73,65 @@ class HttpManager {
Map<String, dynamic>? data,
Map<String, dynamic>? params,
CancelToken? cancelToken,
bool withToken = false, // false
}) async {
Response resp;
final url = baseUrl + path;
// headers
final headers = {
'Content-Type': Headers.formUrlEncodedContentType,
};
if (withToken) {
final token = SessionService.instance.studyToken;
if (token != null && token.isNotEmpty) {
headers['Token'] = token;
}
}
final options = Options(
method: method.name.toUpperCase(),
contentType: Headers.formUrlEncodedContentType,
headers: headers,
);
try {
switch (method) {
case Method.get:
final queryParameters = <String, dynamic>{};
if (params != null) queryParameters.addAll(params);
if (data != null) queryParameters.addAll(data);
resp = await _dio.get(
url,
queryParameters: queryParameters,
cancelToken: cancelToken,
options: options,
);
break;
case Method.put:
resp = await _dio.put(
url,
data: data,
queryParameters: params,
cancelToken: cancelToken,
options: options,
);
break;
case Method.delete:
resp = await _dio.delete(
url,
queryParameters: params,
cancelToken: cancelToken,
options: options,
);
resp = await _dio.get(url,
queryParameters: {...?params, ...?data},
cancelToken: cancelToken,
options: options);
break;
case Method.post:
resp = await _dio.post(
url,
data: data,
queryParameters: params,
cancelToken: cancelToken,
options: options,
);
resp = await _dio.post(url,
data: data,
queryParameters: params,
cancelToken: cancelToken,
options: options);
break;
case Method.put:
resp = await _dio.put(url,
data: data,
queryParameters: params,
cancelToken: cancelToken,
options: options);
break;
case Method.delete:
resp = await _dio.delete(url,
queryParameters: params,
cancelToken: cancelToken,
options: options);
break;
}
} on DioException catch (e) {
// ApiException401
if (e.error is ApiException) {
throw e.error as ApiException;
}
//
if (e.error is ApiException) throw e.error as ApiException;
throw ApiException('network_error', e.message ?? e.toString());
}
// JSON
final json = resp.data is Map<String, dynamic>
? resp.data as Map<String, dynamic>
: <String, dynamic>{};
// final result = json['result'] as String?;
// final msg = json['msg'] as String? ?? json['message'] as String? ?? '';
// if (result != 'success') {
// // success
// throw ApiException(result ?? 'unknown', msg);
// }
return json;
}
}

View File

@ -14,6 +14,8 @@ import 'package:flutter/services.dart'; // for TextInput.hide
//
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
//
final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
//
class GlobalMessage {
@ -91,7 +93,7 @@ void main() async {
);
Future.delayed(const Duration(milliseconds: 100), () {
GlobalMessage.showError('会话已过期,请重新登录');
GlobalMessage.showError('您的账号已在其他设备登录,已自动下线,请使用单一设备进行学习。');
});
};
//
@ -123,7 +125,7 @@ class MyApp extends StatelessWidget {
title: '',
navigatorKey: navigatorKey,
// push/pop TextField
navigatorObservers: [KeyboardUnfocusNavigatorObserver()],
navigatorObservers: [KeyboardUnfocusNavigatorObserver(),routeObserver],
builder: (context, child) {
return EasyLoading.init(
builder: (context, widget) {
@ -139,6 +141,9 @@ class MyApp extends StatelessWidget {
)(context, child);
},
theme: ThemeData(
textTheme: const TextTheme(
bodyMedium: TextStyle(fontSize: 14), //
),
dividerTheme: const DividerThemeData(
color: Colors.black12,
thickness: .5,

View File

@ -393,6 +393,7 @@ class _CheckRecordDetailPageState extends State<CheckRecordDetailPage> {
// loadRequest
try {
await _controller!.loadRequest(Uri.parse('http://47.92.102.56:7811/file/fluteightmap/index.html'));
} catch (e, st) {
debugPrint('loadRequest 错误: $e\n$st');
}

View File

@ -360,7 +360,7 @@ class _DangerWaitListPageState extends State<DangerWaitListPage> {
Future<void> _getDangerRecord(int type, int currentPage,
String startDate,String endDate,String level,String riskStandard,String state,
String departmentId,String correctiveDepartment,
String isIndex,String keyWord,bool loadMore) async {
String isIndex,String keyWord,bool loadMore) async {
try {
if (_isLoading) return;
_isLoading = true;

View File

@ -628,6 +628,8 @@ class _HiddenDangerAcceptancePageState extends State<HiddenDangerAcceptancePage>
case '3': return '标准排查清单检查';
case '4': return '专项检查';
case '5': return '安全检查';
case '6': return 'NFC设备巡检';
default: return '';
}
}

View File

@ -379,6 +379,8 @@ class _HiddenRecordDetailPageState extends State<HiddenRecordDetailPage> {
case '3': return '标准排查清单检查';
case '4': return '专项检查';
case '5': return '安全检查';
case '6': return 'NFC设备巡检';
default: return '';
}
}

View File

@ -94,10 +94,14 @@ class _PendingRectificationDetailPageState extends State<PendingRectificationDet
hs = data['hs'] ?? {};
//
for (var img in data['hImgs']) {
files.add(img["FILEPATH"]);
}
videoList = data['hiddenVideo'] ?? [];
for (var img in data['hImgs']) {
if (img["FILEPATH"].toString().endsWith('.mp4')) {
videoList.add(img);
}else{
files.add(img["FILEPATH"]);
}
}
// List<dynamic> filesZheng = data['rImgs'] ?? [];
for (var img in data['rImgs']) {
@ -736,6 +740,8 @@ class _PendingRectificationDetailPageState extends State<PendingRectificationDet
case '3': return '标准排查清单检查';
case '4': return '专项检查';
case '5': return '安全检查';
case '6': return 'NFC设备巡检';
default: return '';
}
}

View File

@ -199,9 +199,8 @@ class _HomeNfcAddPageState extends State<HomeNfcAddPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: MyAppbar(title: 'NFC标签写入', actions: [
TextButton(onPressed: _getNfcData, child: Text('读取',style: TextStyle(color: Colors.white),))
],),
appBar: MyAppbar(title: 'NFC标签写入'
),
body: SafeArea(
child: SingleChildScrollView(
padding: EdgeInsets.all(12),

View File

@ -42,13 +42,15 @@ class HomeNfcCheckDangerPage extends StatefulWidget {
super.key,
required this.info,
required this.facebookImages,
required this.isNfcError
required this.isNfcError,
required this.isEdit
});
final Map info;
final List<String> facebookImages;
// nfc
final bool isNfcError;
final bool isEdit;
@override
State<HomeNfcCheckDangerPage> createState() => _HomeNfcCheckDangerPageState();
@ -57,6 +59,7 @@ class HomeNfcCheckDangerPage extends StatefulWidget {
class _HomeNfcCheckDangerPageState extends State<HomeNfcCheckDangerPage> {
late Map<String, dynamic> pd = {};
OptionData? selectType; // null
late bool unchecked = true;
final List<OptionData> _options = const [
OptionData(
@ -82,7 +85,7 @@ class _HomeNfcCheckDangerPageState extends State<HomeNfcCheckDangerPage> {
void initState() {
// TODO: implement initState
super.initState();
final bool unchecked = widget.info['INSPECTED_FLAG'] == '0';
unchecked = widget.info['INSPECTED_FLAG'] == '0';
if (!unchecked) { //
for (OptionData data in _options) {
if (data.label == widget.info['INSPECTION_RESULT']) {
@ -101,13 +104,36 @@ class _HomeNfcCheckDangerPageState extends State<HomeNfcCheckDangerPage> {
if (result['result'] == 'success') {
setState(() {
pd = result['pd'];
List hImgs = result['hImgs'];
List gzImageList = result['rImgs'];
List<nfcImgData> imgs = [];
List<nfcImgData> videos = [];
if (hImgs.isNotEmpty) {
for (Map item in hImgs) {
String path = item['FILEPATH'];
if (path.contains('.mp4')) {
videos.add(nfcImgData(path: '${ApiService.baseImgPath}$path', id: item['IMGFILES_ID']));
}else{
imgs.add(nfcImgData(path: '${ApiService.baseImgPath}$path', id: item['IMGFILES_ID']));
}
}
pd['imgList'] = imgs;
pd['videoList'] = videos;
}
if (gzImageList.isNotEmpty) {
List<nfcImgData> zgImgs = [];
for (Map item in gzImageList) {
String path = item['FILEPATH'];
zgImgs.add(nfcImgData(path: '${ApiService.baseImgPath}$path', id: item['IMGFILES_ID']));
}
pd['gzImageList'] = zgImgs;
}
});
}
}
Future<void> _submit() async {
//
//
if (selectType == null) {
//
ToastUtil.showNormal(context, '请先选择检查结果');
@ -115,49 +141,61 @@ class _HomeNfcCheckDangerPageState extends State<HomeNfcCheckDangerPage> {
}
Map data = {
...widget.info,
...pd,
'INSPECTION_RESULT': selectType?.label ?? '',
'TYPE': '0', // (0- 1-)
};
LoadingDialogHelper.show();
// ,
if (selectType?.label == '不合格') {
List imgList = pd['imgList'] ?? [];
List _videos = pd['videoList'] ?? [];
List zgImgList = pd['gzImageList'] ?? [];
for (int i = 0; i < imgList.length; i++) {
await _reloadFeedBack(imgList[i], '3');
}
for (int i = 0; i < _videos.length; i++) {
await _reloadFeedBack(_videos[i], '3');
}
for (int i = 0; i < zgImgList.length; i++) {
await _reloadFeedBack(zgImgList[i], '4');
}
data = {...data,...pd};
}
if (widget.facebookImages.isNotEmpty) {
// nfc
final List<String> uploaded = [];
for (int i = 0; i < widget.facebookImages.length; i++) {
String imagePath = await _reloadFeedBack(widget.facebookImages[i], '30');
if (imagePath.isNotEmpty) {
uploaded.add(imagePath);
}
}
if (uploaded.isNotEmpty) {
data['PHOTO_URL'] = uploaded.join(',');
}
}
data['CHECK_CONTENT'] = widget.info['INSPECTION_CONTENT'];
final result = await ApiService.nfcChekSubmit(data);
LoadingDialogHelper.hide();
if (result['result'] == 'success') {
Map p = result['pd'];
String HIDDEN_ID = p['HIDDEN_ID'] ?? '';
// ,
if (selectType?.label == '不合格') {
List<nfcImgData> imgList = pd['imgList'] ?? [];
List<nfcImgData> _videos = pd['videoList'] ?? [];
List<nfcImgData> zgImgList = pd['gzImageList'] ?? [];
for (int i = 0; i < imgList.length; i++) {
nfcImgData data = imgList[i];
if (data.id.isEmpty) {
await _reloadFeedBack(imgList[i].path, '3', HIDDEN_ID);
}
}
for (int i = 0; i < _videos.length; i++) {
nfcImgData data = _videos[i];
if (data.id.isEmpty) {
await _reloadFeedBack(_videos[i].path, '3', HIDDEN_ID);
}
}
for (int i = 0; i < zgImgList.length; i++) {
nfcImgData data = zgImgList[i];
if (data.id.isEmpty) {
await _reloadFeedBack(zgImgList[i].path, '4', HIDDEN_ID);
}
}
}
if (widget.isNfcError) { // nfc退
if (widget.facebookImages.isNotEmpty) {
// nfc
final List<String> uploaded = [];
for (int i = 0; i < widget.facebookImages.length; i++) {
String imagePath = await _reloadFeedBack(widget.facebookImages[i], '30', widget.info['EQUIPMENT_PIPELINE_ID']);
if (imagePath.isNotEmpty) {
uploaded.add(imagePath);
}
}
if (uploaded.isNotEmpty) {
data['PHOTO_URL'] = uploaded.join(',');
}
}
Navigator.of(context).pop();
}
LoadingDialogHelper.hide();
ToastUtil.showSuccess(context, '提交成功');
Navigator.of(context).pop();
} else {
@ -166,13 +204,13 @@ class _HomeNfcCheckDangerPageState extends State<HomeNfcCheckDangerPage> {
}
}
Future<String> _reloadFeedBack(String imagePath, String type) async {
Future<String> _reloadFeedBack(String imagePath, String type, String FOREIGN_KEY) async {
try {
Map data = {
'TYPE': type,
'FOREIGN_KEY': widget.info['EQUIPMENT_PIPELINE_ID'],
'FOREIGN_KEY': FOREIGN_KEY,
};
final raw = await ApiService.addNormalImgFiles(imagePath, data);
final raw = await ApiService.addNFCImgFiles(imagePath, data);
if (raw['result'] == 'success') {
Map pd = raw['pd'];
return pd['FILEPATH'] ?? "";
@ -189,7 +227,7 @@ class _HomeNfcCheckDangerPageState extends State<HomeNfcCheckDangerPage> {
void _pushDangerDetail() async {
// pushPage pd pd
final result = await pushPage<Map<String, dynamic>>(
NfcCheckDangerDetail(info: pd),
NfcCheckDangerDetail(info: pd, unchecked: unchecked, isEdit: widget.isEdit),
context,
);
if (result != null && result.isNotEmpty) {
@ -294,11 +332,17 @@ class _HomeNfcCheckDangerPageState extends State<HomeNfcCheckDangerPage> {
return GestureDetector(
onTap: () {
setState(() {
if (value != "option2") {
selectType = option;
} else {
//
_pushDangerDetail();
if (widget.isEdit) {
if (value != "option2") {
selectType = option;
} else {
//
_pushDangerDetail();
}
}else { //
if (value == "option2") {
_pushDangerDetail();
}
}
});
},
@ -310,24 +354,31 @@ class _HomeNfcCheckDangerPageState extends State<HomeNfcCheckDangerPage> {
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: [
SizedBox(
height: 30,
width: 90,
IntrinsicWidth(
child: Row(
children: [
Icon(icon, color: isSelected ? color : Colors.grey, size: 30),
const SizedBox(width: 8),
Icon(icon, color: isSelected ? color : Colors.grey, size: 25),
const SizedBox(width: 2),
Flexible(
child: Text(
label,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 14,
fontWeight:
isSelected ? FontWeight.bold : FontWeight.normal,
color: isSelected ? color : Colors.grey[600],
),
),
child: Row(
children: [
Text(
label,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 15,
fontWeight:
isSelected ? FontWeight.bold : FontWeight.normal,
color: isSelected ? color : Colors.grey[600],
),
),
if (label == '不合格' && !unchecked)
Column(
children: [Icon(Icons.edit_note, color: Colors.red, size: 20,), const SizedBox(height: 10,)],
)
],
)
),
],
),
@ -388,11 +439,6 @@ class _HomeNfcCheckDangerPageState extends State<HomeNfcCheckDangerPage> {
screenWidth: screenWidth,
item: item,
onImageTap: () {
if (item["REFERENCE_BASIS"] == "option1") {
// _getAlreadyUpImages(item);
} else if (item["REFERENCE_BASIS"] == "option2") {
// _goUnqualifiedPage(item);
}
},
);
}).toList(),
@ -415,6 +461,7 @@ class _HomeNfcCheckDangerPageState extends State<HomeNfcCheckDangerPage> {
children: [
_pendingTopCard(widget.info),
const Spacer(),
if (widget.isEdit)
CustomButton(
enabled: canSubmit,
text: '提交',

View File

@ -14,9 +14,14 @@ import 'package:qhd_prevention/services/nfc_service.dart';
import 'package:qhd_prevention/tools/tools.dart';
class HomeNfcDetailPage extends StatefulWidget {
const HomeNfcDetailPage({super.key, required this.info});
const HomeNfcDetailPage({
super.key,
required this.info,
required this.isEdit,
});
final Map info;
final bool isEdit;
@override
State<HomeNfcDetailPage> createState() => _HomeNfcDetailPageState();
@ -139,9 +144,8 @@ class _HomeNfcDetailPageState extends State<HomeNfcDetailPage> {
_getNFCForUid(uid, parsedText);
},
onError: (err) {
ToastUtil.showError(context, '$err');
LoadingDialogHelper.hide();
ToastUtil.showError(context, '$err');
LoadingDialogHelper.hide();
},
timeout: Duration(seconds: 12),
);
@ -159,20 +163,19 @@ class _HomeNfcDetailPageState extends State<HomeNfcDetailPage> {
// mapToCompactJson({'PIPELINE_AREA_ID':pd['PIPELINE_AREA_ID'],'EQUIPMENT_PIPELINE_ID':pd['EQUIPMENT_PIPELINE_ID']}),
for (Map item in items) {
if (parsedText.isNotEmpty && item['NFC_CODE'] == uid) {
try{
try {
Map parsedData = jsonDecode(parsedText);
if (parsedData['PIPELINE_AREA_ID'] == item['PIPELINE_AREA_ID'] &&
parsedData['EQUIPMENT_PIPELINE_ID'] == item['EQUIPMENT_PIPELINE_ID']) {
parsedData['EQUIPMENT_PIPELINE_ID'] ==
item['EQUIPMENT_PIPELINE_ID']) {
result = item;
}
LoadingDialogHelper.hide();
}catch(e){
} catch (e) {
LoadingDialogHelper.hide();
ToastUtil.showError(context, 'NFC设备数据错误');
}
}
}
if (result.isEmpty) {
@ -183,10 +186,14 @@ class _HomeNfcDetailPageState extends State<HomeNfcDetailPage> {
}
LoadingDialogHelper.hide();
Map data = {...result, ...widget.info, "NFC_CODE": uid, 'MANUAL_CONFIRMATION': '0',
Map data = {
...result,
...widget.info,
"NFC_CODE": uid,
'MANUAL_CONFIRMATION': '0',
};
await pushPage(
HomeNfcCheckDangerPage(info: data, facebookImages: [], isNfcError: false,),
HomeNfcCheckDangerPage(info: data, facebookImages: [], isNfcError: false, isEdit: widget.isEdit,),
context,
);
_getTaskDetail(refresh: true);
@ -296,6 +303,7 @@ class _HomeNfcDetailPageState extends State<HomeNfcDetailPage> {
text: item['USER_NAME'] ?? '',
),
ItemListWidget.singleLineTitleText(
// maxLines: 1,
label: '涉及管道区域:',
isEditable: false,
text: item['PIPELINE_AREAS_NAMES'] ?? '',
@ -319,7 +327,7 @@ class _HomeNfcDetailPageState extends State<HomeNfcDetailPage> {
final bool unchecked = item['INSPECTED_FLAG'] == '0';
return Container(
height: 100,
height: widget.isEdit ? 100 : 80,
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
@ -356,14 +364,25 @@ class _HomeNfcDetailPageState extends State<HomeNfcDetailPage> {
//
Expanded(
child: GestureDetector(
onTap: (){
onTap: () async {
if (!unchecked) {
Map data = {...item, ...widget.info, "NFC_CODE": item['PIPELINE_AREA_ID'], 'MANUAL_CONFIRMATION': '0',
Map data = {
...item,
...widget.info,
"NFC_CODE": item['PIPELINE_AREA_ID'],
'MANUAL_CONFIRMATION': '0',
};
pushPage(
HomeNfcCheckDangerPage(info: data, facebookImages: [], isNfcError: false,),
context,
await pushPage(
HomeNfcCheckDangerPage(
info: data,
facebookImages: [],
isNfcError: false,
isEdit: widget.isEdit,
),
context,
);
_getTaskDetail(refresh: true);
}
},
child: Column(
@ -382,71 +401,74 @@ class _HomeNfcDetailPageState extends State<HomeNfcDetailPage> {
Text('NFC编码${item['NFC_CODE'] ?? ''}'),
const SizedBox(height: 6),
unchecked
? Row(
spacing: 10,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: InkWell(
onTap: () => _startCheckItem(item, idx),
child: Container(
height: 35,
// width: 120,
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(vertical: 1),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [
Color(0xFFFFA726),
Color(0xFFFF7043),
],
? widget.isEdit
? Row(
spacing: 10,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: InkWell(
onTap: () => _startCheckItem(item, idx),
child: Container(
height: 35,
// width: 120,
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(
vertical: 1,
),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [
Color(0xFFFFA726),
Color(0xFFFF7043),
],
),
borderRadius: BorderRadius.circular(5),
),
child: const Text(
'NFC检查',
style: TextStyle(
color: Colors.white,
fontSize: 15,
),
),
),
),
),
borderRadius: BorderRadius.circular(5),
),
child: const Text(
'NFC检查',
style: TextStyle(
color: Colors.white,
fontSize: 15,
CustomButton(
onPressed: () {
pushPage(
NfcQuestionFecebook(
info: item,
taskInfo: widget.info,
),
context,
);
},
text: '手动检查',
height: 35,
textStyle: TextStyle(
color: Colors.white,
fontSize: 15,
),
backgroundColor: Colors.blue,
),
),
),
),
],
)
: const SizedBox()
: Text(
'检查时间:${item['PATROL_TIME'] ?? ''}',
style: const TextStyle(fontSize: 14),
textAlign: TextAlign.center,
),
CustomButton(
onPressed: () {
pushPage(
NfcQuestionFecebook(
info: item,
taskInfo: widget.info,
),
context,
);
},
text: '手动检查',
height: 35,
textStyle: TextStyle(
color: Colors.white,
fontSize: 15,
),
backgroundColor: Colors.blue,
),
],
)
: Text(
'检查时间:${item['PATROL_TIME'] ?? ''}',
style: const TextStyle(fontSize: 14),
textAlign: TextAlign.center,
),
],
),
)
),
),
//
const SizedBox(width: 8),
if (!unchecked)
const Icon(Icons.chevron_right, color: Colors.grey),
if (!unchecked) const Icon(Icons.chevron_right, color: Colors.grey),
],
),
);

View File

@ -22,7 +22,7 @@ class _HomeNfcListPageState extends State<HomeNfcListPage>
// -
int _pendingPage = 1;
final int _pageSize = 10;
final int _pageSize = 20;
bool _pendingLoading = false;
bool _pendingHasMore = true;
final ScrollController _pendingScrollController = ScrollController();
@ -143,7 +143,6 @@ class _HomeNfcListPageState extends State<HomeNfcListPage>
await _getTaskDetailList(page: _recordPage + 1, replace: false);
}
// ---------- ----------
//NFC
Future<void> _getTaskList({required int page, required bool replace}) async {
setState(() {
@ -207,59 +206,39 @@ class _HomeNfcListPageState extends State<HomeNfcListPage>
_recordLoading = true;
});
try {
// final res = await ApiService.nfcTaskDetailList(_pageSize, page);
List<dynamic>? list;
// if (res == null) {
// list = [];
// } else if (res is List) {
// list = res;
// } else if (res is Map) {
// list = (res['data'] ?? res['list'] ?? res['items'] ?? []) as List<dynamic>?;
// }
// list ??= [];
//
// final parsed = list.map<Map<String, dynamic>>((e) {
// if (e is Map) {
// return {
// 'title': (e['title'] ?? e['TASK_NAME'] ?? e['taskName'] ?? e['name'] ?? '未命名').toString(),
// 'status': (e['status'] ?? e['TASK_STATUS'] ?? '已巡检').toString(),
// 'department': (e['department'] ?? e['DEPARTMENT'] ?? e['dept'] ?? '').toString(),
// 'owner': (e['owner'] ?? e['OWNER'] ?? e['person'] ?? '').toString(),
// 'unType': (e['unType'] ?? e['UN_TYPE'] ?? e['un_type'] ?? '').toString(),
// 'cycle': (e['cycle'] ?? e['CYCLE'] ?? '').toString(),
// 'points': (e['points'] ?? e['POINTS'] ?? '').toString(),
// };
// } else {
// final s = e?.toString() ?? '';
// return {
// 'title': s,
// 'status': '已巡检',
// 'department': '',
// 'owner': '',
// 'unType': '',
// 'cycle': '',
// 'points': '',
// };
// }
// }).toList();
//
// final bool gotLessThanPage = parsed.length < _pageSize;
//
// if (replace) {
// setState(() {
// _recordList.clear();
// _recordList.addAll(parsed);
// _recordPage = 1;
// _recordHasMore = !gotLessThanPage;
// });
// } else {
// setState(() {
// _recordList.addAll(parsed);
// _recordPage = page;
// if (parsed.isEmpty) _recordHasMore = false;
// else if (parsed.length < _pageSize) _recordHasMore = false;
// });
// }
final res = await ApiService.nfcInspectionRecord(_pageSize, page);
// //
// List<dynamic>? list;
if (res['result'] == 'success') {
List<dynamic> list = res['varList'];
final parsed =
list.map<Map<String, dynamic>>((e) {
return e;
}).toList();
//
// < pageSize
final bool gotLessThanPage = parsed.length < _pageSize;
if (replace) {
setState(() {
_recordList.clear();
_recordList.addAll(parsed);
_recordPage = 1;
_recordHasMore = !gotLessThanPage;
});
} else {
setState(() {
_recordList.addAll(parsed);
_recordPage = page;
if (parsed.isEmpty)
_recordHasMore = false;
else if (parsed.length < _pageSize)
_recordHasMore = false;
});
}
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(
@ -363,7 +342,7 @@ class _HomeNfcListPageState extends State<HomeNfcListPage>
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: GestureDetector(
onTap: () {
pushPage(HomeNfcDetailPage(info: item), context);
pushPage(HomeNfcDetailPage(info: item, isEdit: true,), context);
},
child: _pendingCard(item, false),
),
@ -403,9 +382,14 @@ class _HomeNfcListPageState extends State<HomeNfcListPage>
}
}
final item = _recordList[index];
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: _pendingCard(item, true),
return GestureDetector(
onTap: () {
pushPage(HomeNfcDetailPage(info: item, isEdit: false,), context);
},
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 0),
child: _pendingCard(item, true),
),
);
},
),
@ -414,86 +398,88 @@ class _HomeNfcListPageState extends State<HomeNfcListPage>
///
Widget _pendingCard(Map<String, dynamic> item, bool isFinish) {
return SizedBox(
height: 180,
child: Stack(
clipBehavior: Clip.none,
children: [
// &
ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Image.asset(
'assets/images/xj_top.png',
height: 70,
width: double.infinity,
fit: BoxFit.cover,
),
),
String finishState = '';
Color textColor = Colors.blue;
if (isFinish) {
if (item['PATROL_STATUS'] == '0') {
finishState = '已巡检';
textColor = Colors.blue;
} else if (item['PATROL_STATUS'] == '1') {
finishState = '超期未巡检';
textColor = Colors.red;
} else if (item['PATROL_STATUS'] == '2') {
finishState = '巡检中';
textColor = Colors.deepOrange;
}
} else {
if (FormUtils.hasValue(item, 'INSPECTED_POINTS')) {
final inspected = (item['INSPECTED_POINTS'] is int) ? item['INSPECTED_POINTS'] as int : int.tryParse('${item['INSPECTED_POINTS']}') ?? 0;
finishState = inspected > 0 ? '巡检中' : '待巡检';
textColor = inspected > 0 ? Colors.deepOrange : Colors.blue;
}
}
// &
Positioned(
top: 12,
left: 12,
child: Text(
item['TASK_NAME'] ?? '',
style: const TextStyle(
color: Colors.black87,
fontSize: 18,
fontWeight: FontWeight.bold,
return Container(
margin: const EdgeInsets.symmetric(vertical: 0, horizontal: 0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Stack + /
Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Image.asset(
'assets/images/xj_top.png',
height: 70,
width: double.infinity,
fit: BoxFit.cover,
),
),
),
),
if (!isFinish)
Positioned(
top: 12,
right: 12,
child: Container(
height: 30,
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 4,
Positioned(
top: 12,
left: 12,
child: Text(
item['TASK_NAME'] ?? '',
style: const TextStyle(
color: Colors.black87,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.5),
borderRadius: BorderRadius.circular(15),
),
child: Center(
child: Text(
item['INSPECTED_POINTS'] > 0 ? '巡检中' : '待巡检',
style: TextStyle(
color:
item['INSPECTED_POINTS'] as int > 0
? Colors.blue
: Colors.deepOrange,
fontSize: 14,
),
Positioned(
top: 12,
right: 12,
child: Container(
height: 30,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.5),
borderRadius: BorderRadius.circular(15),
),
child: Center(
child: Text(
finishState,
style: TextStyle(color: textColor, fontSize: 14),
),
),
),
),
),
],
),
//
Positioned(
left: 0,
right: 0,
top: 50, //
// 使 Transform
Transform.translate(
offset: const Offset(0, -20),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 0),
padding: const EdgeInsets.all(16),
width: double.infinity,
padding: const EdgeInsets.fromLTRB(16, 12, 16, 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(10),
topRight: Radius.circular(10),
bottomLeft: Radius.circular(10),
bottomRight: Radius.circular(10),
),
borderRadius: BorderRadius.circular(10),
boxShadow: const [
BoxShadow(
color: Colors.black12,
blurRadius: 4,
offset: Offset(0, 1),
),
BoxShadow(color: Colors.black12, blurRadius: 4, offset: Offset(0, 1))
],
),
child: _buildInfoGrid(item, isFinish),
@ -504,37 +490,57 @@ class _HomeNfcListPageState extends State<HomeNfcListPage>
);
}
///
Widget _buildInfoGrid(Map<String, dynamic> item, bool isFinish) {
return Row(
return Column(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('负责部门:${item['DEPARTMENT_NAME'] ?? ''}'),
const SizedBox(height: 8),
Text('巡检类型:${item['PATROL_TYPE_NAME'] ?? ''}'),
const SizedBox(height: 8),
Text('已巡点位:${item['INSPECTED_POINTS'] ?? '0'}'),
],
),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text('负责人:${item['USER_NAME'] ?? ''}'),
const SizedBox(height: 8),
Text('巡检周期:${item['PATROL_PERIOD_NAME'] ?? ''}'),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('负责部门:${item['DEPARTMENT_NAME'] ?? ''}'),
const SizedBox(height: 8),
Text('巡检类型:${item['PATROL_TYPE_NAME'] ?? ''}'),
const SizedBox(height: 8),
isFinish ? Text('巡检次数:${item['ACTUAL_PATROL_TIMES'] ?? '0'}') :
Text('已巡点位:${item['INSPECTED_POINTS'] ?? '0'}'),
],
),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text('负责人:${item['USER_NAME'] ?? ''}'),
const SizedBox(height: 8),
Text('巡检周期:${item['PATROL_PERIOD_NAME'] ?? ''}'),
isFinish
? Text('涉及管道区域:${item['department'] ?? ''}')
: const SizedBox(height: 25),
],
),
],
),
),
],
),
if (isFinish)
const SizedBox(height: 10,),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: isFinish
? Text(
'涉及管道区域:${item['PIPELINE_AREAS_NAMES'] ?? ''}',
maxLines: 2,
overflow: TextOverflow.ellipsis,
)
: const SizedBox(height: 0),)
],
)
],
);
}
}

View File

@ -12,6 +12,8 @@ import 'package:qhd_prevention/customWidget/department_person_picker.dart';
import 'package:qhd_prevention/customWidget/department_picker.dart';
import 'package:qhd_prevention/customWidget/department_picker_hidden_type.dart';
import 'package:qhd_prevention/customWidget/department_picker_two.dart';
import 'package:qhd_prevention/customWidget/full_screen_video_page.dart';
import 'package:qhd_prevention/customWidget/single_image_viewer.dart';
import 'package:qhd_prevention/customWidget/toast_util.dart';
import 'package:qhd_prevention/pages/home/tap/item_list_widget.dart';
import 'package:qhd_prevention/pages/my_appbar.dart';
@ -19,10 +21,23 @@ import 'package:qhd_prevention/tools/tools.dart';
import '../../../customWidget/photo_picker_row.dart';
import '../../../http/ApiService.dart';
class NfcCheckDangerDetail extends StatefulWidget {
const NfcCheckDangerDetail({super.key, required this.info});
class nfcImgData {
final String path;
final String id;
const nfcImgData({
required this.path,
required this.id,
});
}
class NfcCheckDangerDetail extends StatefulWidget {
const NfcCheckDangerDetail({super.key, required this.info, required this.unchecked, required this.isEdit});
final bool unchecked;
final Map<String, dynamic> info;
final bool isEdit;
@override
State<NfcCheckDangerDetail> createState() => _NfcCheckDangerDetailState();
@ -40,11 +55,11 @@ class _NfcCheckDangerDetailState extends State<NfcCheckDangerDetail> {
Map<String, dynamic> _personCache = {};
//
late List<String> imgList = [];
late List<String> _videos = [];
late List<nfcImgData> imgList = [];
late List<nfcImgData> _videos = [];
//
late List<String> zgImgList = [];
late List<nfcImgData> zgImgList = [];
@override
void initState() {
@ -55,7 +70,7 @@ class _NfcCheckDangerDetailState extends State<NfcCheckDangerDetail> {
imgList = pd['imgList'] ?? [];
_videos = pd['videoList'] ?? [];
zgImgList = pd['gzImageList'] ?? [];
_isDanger = pd['RECTIFICATIONTYPE'] == '1';
}
_getHazardLevel();
}
@ -91,7 +106,22 @@ class _NfcCheckDangerDetailState extends State<NfcCheckDangerDetail> {
print('Error fetching data: $e');
}
}
String _getIDForPath(String path, List<nfcImgData> imgList) {
for (nfcImgData data in imgList) {
if (data.path == path) {
return data.id;
}
}
return '';
}
nfcImgData _getImageDataForPath(String path, List<nfcImgData> list) {
for (nfcImgData data in list) {
if (data.path == path) {
return data;
}
}
return nfcImgData(path: '', id: '');
}
Future<void> _pickHazardType() async {
showModalBottomSheet(
context: context,
@ -234,14 +264,29 @@ class _NfcCheckDangerDetailState extends State<NfcCheckDangerDetail> {
RepairedPhotoSection(
title: "隐患照片",
maxCount: 4,
initialMediaPaths: imgList,
initialMediaPaths: imgList.map((item) => item.path).toList(),
mediaType: MediaType.image,
isEdit: widget.isEdit,
isShowAI: true,
onMediaAdded: (localPath) {
imgList.add(localPath);
imgList.add(nfcImgData(path: localPath, id: ''));
},
onMediaRemoved: (localPath) {
imgList.remove(localPath);
onMediaRemoved: (localPath) async {
if (localPath.startsWith('http')) {
final result = await _removeFileWithType('img', _getIDForPath(localPath, imgList));
if (result) {
setState(() {
imgList.remove(_getImageDataForPath(localPath, imgList));
});
}
}else{
setState(() {
imgList.remove(_getImageDataForPath(localPath, imgList));
});
}
},
onMediaTapped: (path) {
presentOpaque(SingleImageViewer(imageUrl: path), context);
},
onChanged: (v) {},
onAiIdentify: () {
@ -254,23 +299,41 @@ class _NfcCheckDangerDetailState extends State<NfcCheckDangerDetail> {
ToastUtil.showNormal(context, "识别暂时只能上传一张图片");
return;
}
_identifyImg(imgList[0]);
nfcImgData data = imgList.first;
_identifyImg(data.path);
},
),
RepairedPhotoSection(
title: "隐患视频",
maxCount: 1,
isEdit: widget.isEdit,
initialMediaPaths: _videos.map((item) => item.path).toList(),
mediaType: MediaType.video,
onChanged: (v) {},
onMediaRemoved: (localPath) {
_videos = [localPath];
onMediaRemoved: (localPath) async {
if (localPath.startsWith('http')) {
final result = await _removeFileWithType('img', _getIDForPath(localPath, _videos));
if (result) {
setState(() {
_videos.remove(_getImageDataForPath(localPath, _videos));
});
}
}else{
setState(() {
_videos.remove(_getImageDataForPath(localPath, _videos));
});
}
},
onMediaTapped: (path) {
showDialog(
context: context,
barrierColor: Colors.black54,
builder: (_) => VideoPlayerPopup(videoUrl: path),
); },
onMediaAdded: (localPath) {
_videos = [];
},
onAiIdentify: () {
// AI
_videos.add(nfcImgData(path: localPath, id: ''));
},
onAiIdentify: () {},
),
],
),
@ -278,7 +341,7 @@ class _NfcCheckDangerDetailState extends State<NfcCheckDangerDetail> {
ItemListWidget.multiLineTitleTextField(
label: '隐患描述',
isEditable: true,
isEditable: widget.isEdit,
isRequired: false,
text: pd['HIDDENDESCR'] ?? '',
hintText: '请对隐患进行详细描述(必填项)',
@ -292,7 +355,7 @@ class _NfcCheckDangerDetailState extends State<NfcCheckDangerDetail> {
ItemListWidget.multiLineTitleTextField(
label: '隐患部位',
isEditable: true,
isEditable: widget.isEdit,
isRequired: false,
// controller: TextEditingController(text: pd['HIDDENPART'] ?? ''),
text: pd['HIDDENPART'] ?? '',
@ -308,14 +371,14 @@ class _NfcCheckDangerDetailState extends State<NfcCheckDangerDetail> {
ItemListWidget.selectableLineTitleTextRightButton(
label: '隐患级别',
isRequired: false,
isEditable: true,
isEditable: widget.isEdit,
text: pd['HIDDENLEVELNAME'] ?? '',
onTap: chooseDangerLevel,
),
const Divider(),
ItemListWidget.selectableLineTitleTextRightButton(
label: '隐患类型',
isEditable: true,
isEditable: widget.isEdit,
isRequired: false,
text: pd['HIDDENTYPE_NAME'] ?? '',
onTap: _pickHazardType,
@ -326,6 +389,8 @@ class _NfcCheckDangerDetailState extends State<NfcCheckDangerDetail> {
title: "是否立即整改",
horizontalPadding: 0,
verticalPadding: 0,
text: _isDanger ? '立即整改' : '限期整改',
isEdit: widget.isEdit,
yesLabel: "",
noLabel: "",
groupValue: _isDanger,
@ -342,7 +407,7 @@ class _NfcCheckDangerDetailState extends State<NfcCheckDangerDetail> {
children: [
ItemListWidget.multiLineTitleTextField(
label: '整改描述',
isEditable: true,
isEditable: widget.isEdit,
isRequired: false,
text: pd['RECTIFYDESCR'] ?? '',
hintText: '请对隐患进行详细描述(必填项)',
@ -358,15 +423,29 @@ class _NfcCheckDangerDetailState extends State<NfcCheckDangerDetail> {
horizontalPadding: 12,
title: "整改后图片",
maxCount: 4,
initialMediaPaths: zgImgList,
isEdit: widget.isEdit,
initialMediaPaths: zgImgList.map((item) => item.path).toList(),
mediaType: MediaType.image,
isShowAI: false,
onMediaAdded: (localPath) {
zgImgList.add(localPath);
onMediaTapped: (path) {
presentOpaque(SingleImageViewer(imageUrl: path), context);
},
onMediaRemoved: (localPath) {
zgImgList.remove(localPath);
onMediaAdded: (localPath) {
zgImgList.add(nfcImgData(path: localPath, id: ''));
},
onMediaRemoved: (localPath) async {
if (localPath.startsWith('http')) {
final result = await _removeFileWithType('img', _getIDForPath(localPath, zgImgList));
if (result) {
setState(() {
zgImgList.remove(_getImageDataForPath(localPath, zgImgList));
});
}
}else{
setState(() {
zgImgList.remove(_getImageDataForPath(localPath, zgImgList));
});
}
},
onChanged: (v) {},
onAiIdentify: () {},
@ -378,7 +457,7 @@ class _NfcCheckDangerDetailState extends State<NfcCheckDangerDetail> {
children: [
ItemListWidget.selectableLineTitleTextRightButton(
label: '整改责任部门',
isEditable: true,
isEditable: widget.isEdit,
isRequired: false,
text: pd['RECTIFICATIONDEPTNAME'] ?? '',
onTap: () {
@ -388,7 +467,7 @@ class _NfcCheckDangerDetailState extends State<NfcCheckDangerDetail> {
const Divider(),
ItemListWidget.selectableLineTitleTextRightButton(
label: '整改责任人',
isEditable: true,
isEditable: widget.isEdit,
isRequired: false,
text: pd['RECTIFICATIONORNAME'] ?? '',
onTap: () {
@ -398,7 +477,7 @@ class _NfcCheckDangerDetailState extends State<NfcCheckDangerDetail> {
const Divider(),
ItemListWidget.selectableLineTitleTextRightButton(
label: '整改期限',
isEditable: true,
isEditable: widget.isEdit,
isRequired: false,
text: pd['RECTIFICATIONDEADLINE'] ?? '',
onTap: () {
@ -427,9 +506,13 @@ class _NfcCheckDangerDetailState extends State<NfcCheckDangerDetail> {
SizedBox(height: 30),
CustomButton(
onPressed: () {
_riskListCheckAppAdd();
if (widget.isEdit) {
_riskListCheckAppAdd();
}else{
Navigator.pop(context, pd);
}
},
text: "确定",
text: widget.isEdit ? "确定" : "返回",
backgroundColor: Colors.blue,
),
SizedBox(height: 30),
@ -439,7 +522,19 @@ class _NfcCheckDangerDetailState extends State<NfcCheckDangerDetail> {
),
);
}
Future<bool> _removeFileWithType(String type, String id) async{
if (id.isEmpty) {
return true;
}
Map data = {'IMGFILES_ID': id};
final result = await ApiService.deleteNFCImage(data);
if (result['result'] == 'success') {
return true;
}else{
return false;
}
// return
}
Future<void> _riskListCheckAppAdd() async {
if (imgList.isEmpty) {
ToastUtil.showNormal(context, '请上传隐患图片');
@ -463,7 +558,7 @@ class _NfcCheckDangerDetailState extends State<NfcCheckDangerDetail> {
ToastUtil.showNormal(context, '请填整改描述');
return;
}
if (zgImgList.isEmpty) {
if (zgImgList.length == 0) {
ToastUtil.showNormal(context, '请上传整改后图片');
return;
}
@ -481,14 +576,16 @@ class _NfcCheckDangerDetailState extends State<NfcCheckDangerDetail> {
return;
}
}
List HIDDENTYPE = pd['HIDDENTYPE'] ?? [];
pd['RECTIFICATIONTYPE'] = _isDanger ? '1' : '2';
final HIDDENTYPE = pd['HIDDENTYPE'];
if (HIDDENTYPE is List) {
pd['HIDDENTYPE1'] = HIDDENTYPE.length > 0 ? HIDDENTYPE[0] : "";
pd['HIDDENTYPE2'] = HIDDENTYPE.length > 1 ? HIDDENTYPE[1] : "";
pd['HIDDENTYPE3'] = HIDDENTYPE.length > 2 ? HIDDENTYPE[2] : "";
}
pd['CREATOR'] = SessionService.instance.loginUserId;
pd['SOURCE'] = '6';
pd['HIDDENTYPE1'] = HIDDENTYPE.length > 0 ? HIDDENTYPE[0] : "";
pd['HIDDENTYPE2'] = HIDDENTYPE.length > 1 ? HIDDENTYPE[1] : "";
pd['HIDDENTYPE3'] = HIDDENTYPE.length > 2 ? HIDDENTYPE[2] : "";
pd['RECTIFICATIONTYPE'] = _isDanger ? '1' : '2';
pd['imgList'] = imgList;
pd['videoList'] = _videos;
pd['gzImageList'] = zgImgList;

View File

@ -185,7 +185,7 @@ class _NfcQuestionFecebookState extends State<NfcQuestionFecebook> {
'DESCRIPTION': text,
};
pushPage(HomeNfcCheckDangerPage(info: data, facebookImages: _images, isNfcError: true,), context);
pushPage(HomeNfcCheckDangerPage(info: data, facebookImages: _images, isNfcError: true,isEdit: true,), context);
// String imagePaths = "";
// for (int i = 0; i < _images.length; i++) {

View File

@ -42,7 +42,13 @@ class _StudyClassListPageState extends State<StudyClassListPage> {
LoadingDialogHelper.hide();
}
}
Future<void> _nowStudy(Map item) async {
final result = await ApiService.getVideoPermissions();
if (result['result'] == 'success') {
SessionService.instance.setStudyToken(result['token'] ?? '');
pushPage(StudyDetailPage(item, widget.studyData['STUDENT_ID']), context);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
@ -82,7 +88,7 @@ class _StudyClassListPageState extends State<StudyClassListPage> {
ApiService.baseImgPath + item['COVERPATH'],
width: 100,
height: 80,
fit: BoxFit.cover,
fit: BoxFit.fill,
),
const SizedBox(width: 10),
Expanded(
@ -103,23 +109,21 @@ class _StudyClassListPageState extends State<StudyClassListPage> {
overflow: TextOverflow.visible,
style: const TextStyle(color: Colors.black54),
),),
Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SizedBox(),
CustomButton(
text: "立即学习",
backgroundColor: Colors.blue,
height: 38,
onPressed: () {
pushPage(StudyDetailPage(item, widget.studyData['STUDENT_ID']), context);
},
),
],
)
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const SizedBox(),
CustomButton(
text: "立即学习",
backgroundColor: Colors.blue,
height: 38,
onPressed: () {
_nowStudy(item);
},
),
],)
],
),
),

View File

@ -1,5 +1,6 @@
import 'dart:async';
import 'package:flutter/material.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';
import 'package:qhd_prevention/pages/my_appbar.dart';
@ -14,15 +15,12 @@ import '../../../customWidget/video_player_widget.dart';
import '../../../http/HttpManager.dart';
import 'face_ecognition_page.dart';
enum TakeExamType {
video_study,
strengththen,
list
}
enum TakeExamType { video_study, strengththen, list }
class StudyDetailPage extends StatefulWidget {
final Map studyDetailDetail;
final String studentId;
const StudyDetailPage(this.studyDetailDetail, this.studentId, {super.key});
@override
@ -69,6 +67,7 @@ class _StudyDetailPageState extends State<StudyDetailPage>
_tabController.dispose();
_videoController?.removeListener(_onTimeUpdate);
_videoController?.dispose();
_videoController = null;
_faceTimer?.cancel();
super.dispose();
}
@ -76,15 +75,16 @@ class _StudyDetailPageState extends State<StudyDetailPage>
Future<void> _showFaceIntro() async {
await showDialog(
context: context,
builder: (_) => CustomAlertDialog(
title: '温馨提示',
content:
'重要提醒:尊敬的用户,根据规定我们会在您学习过程中多次进行人脸识别认证,为了保护您的隐私请您在摄像设备视野内确保衣冠整齐。',
cancelText: '取消',
confirmText: '同意并继续',
onCancel: () => Navigator.of(context).pop(),
onConfirm: () => {},
),
builder:
(_) => CustomAlertDialog(
title: '温馨提示',
content:
'重要提醒:尊敬的用户,根据规定我们会在您学习过程中多次进行人脸识别认证,为了保护您的隐私请您在摄像设备视野内确保衣冠整齐。',
cancelText: '取消',
confirmText: '同意并继续',
onCancel: () => Navigator.of(context).pop(),
onConfirm: () => {},
),
);
}
@ -144,11 +144,11 @@ class _StudyDetailPageState extends State<StudyDetailPage>
}
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 {
// clear face timer on backend
await ApiService.fnClearUserFaceTime();
_faceTimer?.cancel();
@ -169,8 +169,12 @@ class _StudyDetailPageState extends State<StudyDetailPage>
if ((data['IS_VIDEO'] ?? 0) == 1) {
// document
if (data['VIDEOFILES'] != null) {
_videoController?.pause();
await pushPage(
StudyPractisePage(videoCoursewareId: data['VIDEOCOURSEWARE_ID']),
RemoteFilePage(
fileUrl: ApiService.baseImgPath + data['VIDEOFILES'],
countdownSeconds: 10,
),
context,
);
await _submitPlayTime(
@ -178,9 +182,9 @@ class _StudyDetailPageState extends State<StudyDetailPage>
seconds: int.parse(data['VIDEOTIME'] ?? '0'),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('课件文件资源已失效,请联系管理员')),
);
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('课件文件资源已失效,请联系管理员')));
}
} else {
// video
@ -199,9 +203,9 @@ class _StudyDetailPageState extends State<StudyDetailPage>
if (passed == true) {
await onPass();
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('人脸验证未通过,无法继续')),
);
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('人脸验证未通过,无法继续')));
}
} else {
await onPass();
@ -221,14 +225,15 @@ 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();
})();
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();
})();
// controller
_videoController?.removeListener(_onTimeUpdate);
@ -247,15 +252,16 @@ class _StudyDetailPageState extends State<StudyDetailPage>
..addListener(_onTimeUpdate);
}
void _onTimeUpdate() {
if (_videoController == null || !_videoController!.value.isPlaying) return;
final curr = _videoController!.value.position;
if (!_throttleFlag && (curr - _lastReported).inSeconds >= 5) {
_throttleFlag = true;
_lastReported = curr;
_submitPlayTime(end: false, seconds: curr.inSeconds)
.whenComplete(() => _throttleFlag = false);
_submitPlayTime(
end: false,
seconds: curr.inSeconds,
).whenComplete(() => _throttleFlag = false);
}
final pos = _videoController!.value.position;
final dur = _videoController!.value.duration;
@ -273,29 +279,34 @@ class _StudyDetailPageState extends State<StudyDetailPage>
if (_currentVideoData == null) return;
try {
final resData = (await ApiService.fnSubmitPlayTime(
_currentVideoData!['VIDEOCOURSEWARE_ID'],
_currentVideoData!['CURRICULUM_ID'],
end ? '1' : '0',
seconds,
_currentVideoData!['CHAPTER_ID'],
widget.studentId,
_classCurriculumId,
_classId,
));
Map data = {
'VIDEOCOURSEWARE_ID': _currentVideoData!['VIDEOCOURSEWARE_ID'] ?? '',
'CURRICULUM_ID': _currentVideoData!['CURRICULUM_ID'] ?? '',
'CHAPTER_ID': _currentVideoData!['CHAPTER_ID'] ?? '',
'RESOURCETIME': seconds,
'IS_END': end ? '1' : '0',
'CLASS_ID': _classId,
'CLASSCURRICULUM_ID': _classCurriculumId,
'STUDENT_ID': widget.studentId,
'loading': false,
};
final resData = await ApiService.fnSubmitPlayTime(data);
final pd = resData['pd'] ?? {};
//
final comp = pd['PLAYCOUNT'] != null && pd['PLAYCOUNT'] > 0;
final resT = pd['RESOURCETIME'] ?? seconds;
final pct = comp
? 100
: (resT / (_currentVideoData!['VIDEOTIME'] ?? 1) * 100)
.clamp(0, 100);
final videoTimeRaw = _currentVideoData!['VIDEOTIME'];
final videoTime =
(videoTimeRaw is String)
? double.tryParse(videoTimeRaw) ?? 1
: (videoTimeRaw is num ? videoTimeRaw.toDouble() : 1);
final pct = comp ? 100 : (resT / videoTime * 100).clamp(0, 100);
final str = '${pct.floor()}%';
setState(() {
if (_hasNodes) {
_videoList[_currentFirstIndex]['nodes'][_currentNodeIndex]
['percent'] = str;
_videoList[_currentFirstIndex]['nodes'][_currentNodeIndex]['percent'] =
str;
} else {
_videoList[_currentFirstIndex]['percent'] = str;
}
@ -304,18 +315,19 @@ class _StudyDetailPageState extends State<StudyDetailPage>
//
if (end && pd['CANEXAM'] == '1') {
_videoController?.pause();
final ok = await showDialog<bool>(
context: context,
builder: (_) => CustomAlertDialog(
title: '提示',
content: '当前任务内所有课程均已学完,是否直接参加考试?',
confirmText: '',
cancelText: '',
),
) ??
final ok =
await showDialog<bool>(
context: context,
builder:
(_) => CustomAlertDialog(
title: '提示',
content: '当前任务内所有课程均已学完,是否直接参加考试?',
confirmText: '',
cancelText: '',
),
) ??
false;
if (ok) {
_startExam(resData);
} else {
_videoController?.play();
@ -337,12 +349,12 @@ class _StudyDetailPageState extends State<StudyDetailPage>
_loading = true;
});
final arguments = {
'STAGEEXAMPAPERINPUT_ID': paper['STAGEEXAMPAPERINPUT_ID']??'',
'STAGEEXAMPAPER_ID': paper['STAGEEXAMPAPER_ID']??'',
'STAGEEXAMPAPERINPUT_ID': paper['STAGEEXAMPAPERINPUT_ID'] ?? '',
'STAGEEXAMPAPER_ID': paper['STAGEEXAMPAPER_ID'] ?? '',
'CLASS_ID': _classId,
'POST_ID': pd['POST_ID'] ?? '',
'STUDENT_ID': widget.studentId,
'NUMBEROFEXAMS': pd['NUMBEROFEXAMS'] ?? ''
'NUMBEROFEXAMS': pd['NUMBEROFEXAMS'] ?? '',
};
print('--_startExam data---$arguments');
@ -351,15 +363,21 @@ class _StudyDetailPageState extends State<StudyDetailPage>
_loading = false;
});
if (data['result'] == 'success') {
pushPage(TakeExamPage(examInfo: {
'CLASS_ID':_classId,
'POST_ID': pd['POST_ID'] ?? '',
'STUDENT_ID': widget.studentId,
'STRENGTHEN_PAPER_QUESTION_ID': paper['STAGEEXAMPAPERINPUT_ID']??'',
...data
}, examType: TakeExamType.video_study), context);
}else{
pushPage(
TakeExamPage(
examInfo: {
'CLASS_ID': _classId,
'POST_ID': pd['POST_ID'] ?? '',
'STUDENT_ID': widget.studentId,
'STRENGTHEN_PAPER_QUESTION_ID':
paper['STAGEEXAMPAPERINPUT_ID'] ?? '',
...data,
},
examType: TakeExamType.video_study,
),
context,
);
} else {
ToastUtil.showError(context, '请求错误');
}
}
@ -391,9 +409,27 @@ class _StudyDetailPageState extends State<StudyDetailPage>
}
}
void _controllerListener() {
if (mounted) setState(() {});
Widget _buildVideoOrCover(double containerW, double containerH) {
final c = _videoController;
if (c != null && c.value.isInitialized) {
return VideoPlayerWidget(
allowSeek: false,
controller: _videoController,
coverUrl:
_videoCoverUrl.isNotEmpty
? ApiService.baseImgPath + _videoCoverUrl
: ApiService.baseImgPath + (_info?['COVERPATH'] ?? ''),
aspectRatio: _videoController?.value.aspectRatio ?? 16 / 9,
);
} else {
// controller
return Image.network(
'${ApiService.baseImgPath}${_info?['COVERPATH'] ?? ''}',
fit: BoxFit.fill,
width: containerW,
height: containerH,
);
}
}
@override
@ -410,18 +446,8 @@ class _StudyDetailPageState extends State<StudyDetailPage>
body: SafeArea(
child: Column(
children: [
SizedBox(
height: 250,
width: screenWidth(context),
child: VideoPlayerWidget(
allowSeek: false,
controller: _videoController,
coverUrl: _videoCoverUrl.isNotEmpty
? ApiService.baseImgPath + _videoCoverUrl
: ApiService.baseImgPath + (info['COVERPATH'] ?? ''),
aspectRatio: _videoController?.value.aspectRatio ?? 16/9,
),
),
_buildVideoOrCover(screenWidth(context), 250),
const SizedBox(height: 5,),
Container(
width: double.infinity,
color: Colors.white,
@ -467,29 +493,31 @@ class _StudyDetailPageState extends State<StudyDetailPage>
final item = _videoList[idx] as Map<String, dynamic>;
final nodes = item['nodes'] as List<dynamic>?;
if (nodes != null && nodes.isNotEmpty) {
return ExpansionTile(
title: Text(item['NAME'] ?? ''),
children:
nodes
.asMap()
.entries
.map(
(e) => _buildVideoItem(
e.value as Map<String, dynamic>,
true,
idx,
e.key,
),
)
.toList(),
// +
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...nodes.asMap().entries.map(
(e) => _buildVideoItem(
item,
e.value as Map<String, dynamic>,
true,
idx,
e.key,
),
),
const Divider(height: 1), // 线
],
);
}
return _buildVideoItem(item, false, idx, 0);
//
return _buildVideoItem(item, item, false, idx, 0);
},
);
}
Widget _buildVideoItem(
Map<String, dynamic> item,
Map<String, dynamic> m,
bool hasNodes,
int fi,
@ -504,9 +532,10 @@ class _StudyDetailPageState extends State<StudyDetailPage>
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(Icons.file_copy_rounded, color: Colors.grey, size: 20),
const SizedBox(width: 8),
Expanded(
child: Text(
m['NAME'] ?? '',
item['NAME'] ?? '',
style: const TextStyle(fontSize: 14),
),
),
@ -540,12 +569,15 @@ class _StudyDetailPageState extends State<StudyDetailPage>
),
if (m['IS_VIDEO'] == 0) ...[
Text(secondsCount(m['VIDEOTIME'])),
const SizedBox(width: 6),
const Icon(Icons.play_circle, color: Colors.blue),
],
CustomButton(
onPressed:
() => pushPage(
StudyPractisePage(videoCoursewareId: m['VIDEOCOURSEWARE_ID']),
StudyPractisePage(
videoCoursewareId: m['VIDEOCOURSEWARE_ID'],
),
context,
),
text: "课后练习",

View File

@ -3,6 +3,7 @@ import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:qhd_prevention/customWidget/custom_button.dart';
import 'package:qhd_prevention/main.dart';
import 'package:qhd_prevention/pages/home/study/strengthen_video_study_page.dart';
import 'package:qhd_prevention/pages/home/study/study_class_list_page.dart';
import 'package:qhd_prevention/pages/home/study/study_detail_page.dart';
@ -21,7 +22,7 @@ class StudyMyTaskPage extends StatefulWidget {
State<StudyMyTaskPage> createState() => _StudyMyTaskPageState();
}
class _StudyMyTaskPageState extends State<StudyMyTaskPage> {
class _StudyMyTaskPageState extends State<StudyMyTaskPage> with RouteAware {
int _page = 1;
final int _showCount = 10;
bool _isLoading = false;
@ -29,29 +30,60 @@ class _StudyMyTaskPageState extends State<StudyMyTaskPage> {
int _totalPage = 1;
List<dynamic> _list = [];
Timer? _timer;
late DateTime _now; //
@override
void initState() {
super.initState();
_now = DateTime.now();
WidgetsBinding.instance.addPostFrameCallback((_) => _getStudyList());
_startCountdownTimer();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
final modalRoute = ModalRoute.of(context);
if (modalRoute is PageRoute) {
routeObserver.subscribe(this, modalRoute);
}
}
///
@override
void didPopNext() {
if (!_isLoading) _getStudyList();
}
@override
void didPush() {
// initState _getStudyList
if (!_isLoading) _getStudyList();
}
@override
void dispose() {
_timer?.cancel();
// 退 routeObserver
try {
routeObserver.unsubscribe(this);
} catch (_) {}
super.dispose();
}
/// remainingSeconds
void _startCountdownTimer() {
if (_timer != null && _timer!.isActive) return; //
_timer = Timer.periodic(Duration(seconds: 1), (_) {
setState(() {
for (var item in _list) {
final rs = item['remainingSeconds'] as int? ?? 0;
item['remainingSeconds'] = rs > 0 ? rs - 1 : 0;
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;
}
}
});
});
@ -90,6 +122,8 @@ class _StudyMyTaskPageState extends State<StudyMyTaskPage> {
}
_hasMore = _page < _totalPage;
if (_hasMore) _page++;
_startCountdownTimer();
});
}
} catch (e) {
@ -181,7 +215,7 @@ class _StudyMyTaskPageState extends State<StudyMyTaskPage> {
}
Widget _buildItem(Map item) {
final now = _now;
final now = DateTime.now();
final start = DateTime.tryParse(item['START_TIME'] ?? '');
final end = DateTime.tryParse(item['END_TIME'] ?? '');
final nowOk =
@ -215,6 +249,7 @@ class _StudyMyTaskPageState extends State<StudyMyTaskPage> {
// +
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Text(
@ -222,6 +257,7 @@ class _StudyMyTaskPageState extends State<StudyMyTaskPage> {
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
const SizedBox(width: 10,),
Text(
_stateText(item['STUDYSTATE']),
style: TextStyle(color: _stateColor(item['STUDYSTATE'])),

View File

@ -188,17 +188,23 @@ class _PracticePageState extends State<StudyPractisePage> {
}) {
Color fg = Colors.black87;
Color bg = Colors.grey.shade200;
Color hasTextColor = Colors.black54;
if (right) {
fg = Colors.green;
bg = Colors.green;
hasTextColor = Colors.white;
}
if (err) {
fg = Colors.red;
bg = Colors.red;
hasTextColor = Colors.white;
}
if (warning) {
fg = Colors.green;
bg = Colors.green;
hasTextColor = Colors.white;
}
if (active) fg = Colors.blue;
@ -226,7 +232,7 @@ class _PracticePageState extends State<StudyPractisePage> {
: Text(label, style: TextStyle(color: fg)))
: Text(
label,
style: TextStyle(color: multiple ? fg : Colors.white),
style: TextStyle(color: multiple ? fg : hasTextColor),
),
),
SizedBox(width: 16),
@ -249,6 +255,7 @@ class _PracticePageState extends State<StudyPractisePage> {
Widget build(BuildContext context) {
final q = options.isNotEmpty ? options[current] : null;
return Scaffold(
backgroundColor: Colors.white,
appBar: MyAppbar(title: '课后练习'),
body:
loading

View File

@ -82,13 +82,25 @@ class _TakeExamPageState extends State<TakeExamPage> {
.map((e) => Question.fromJson(e as Map<String, dynamic>))
.toList();
final numberOfExams = widget.examInfo['NUMBEROFEXAMS'] as String? ?? '0';
final numberOfExams = widget.examInfo['NUMBEROFEXAMS'];
WidgetsBinding.instance.addPostFrameCallback((_) {
if (numberOfExams == '-9999') {
_showTip('强化学习考试开始,限时${info['ANSWERSHEETTIME']}分钟,请注意答题时间!');
} else {
if (numberOfExams is int) {
if (numberOfExams > 0) {
}else if (numberOfExams == -9999) {
_showTip('强化学习考试开始,限时${info['ANSWERSHEETTIME']}分钟,请注意答题时间!');
}else{
_showTip('您无考试次数!');
}
}else if (numberOfExams is String) {
if (numberOfExams == '-9999') {
_showTip('强化学习考试开始,限时${info['ANSWERSHEETTIME']}分钟,请注意答题时间!');
}
}else {
_showTip('您无考试次数!');
}
});
final minutes = info['ANSWERSHEETTIME'] as int? ?? 0;
@ -295,8 +307,8 @@ class _TakeExamPageState extends State<TakeExamPage> {
final q = questions.isNotEmpty ? questions[current] : null;
return PopScope(
canPop: false, //
child: Scaffold(
backgroundColor: Colors.white,
appBar: const MyAppbar(title: '课程考试', isBack: false,),
body: Padding(
padding: const EdgeInsets.all(16),

View File

@ -26,6 +26,7 @@ class ItemListWidget {
bool strongRequired = false,
ValueChanged<String>? onChanged,
ValueChanged<String>? onFieldSubmitted,
int maxLines = 5,
///
TextInputType keyboardType = TextInputType.text,
@ -71,7 +72,7 @@ class ItemListWidget {
)
: Expanded(child: Text(
text ?? '',
maxLines: 5,
maxLines: maxLines,
style: TextStyle(fontSize: fontSize, color: detailtextColor),
textAlign: TextAlign.right,
overflow: TextOverflow.ellipsis, //
@ -122,7 +123,7 @@ class ItemListWidget {
isEditable
? TextFormField(
autofocus: false,
initialValue: text,
initialValue: controller == null ? text : null,
controller: controller,
keyboardType: TextInputType.multiline,
maxLines: null,
@ -132,7 +133,6 @@ class ItemListWidget {
textAlignVertical: TextAlignVertical.top,
style: TextStyle(fontSize: fontSize),
decoration: InputDecoration(
hintText: hintText,
// TextField
contentPadding: EdgeInsets.zero,

View File

@ -1,6 +1,8 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:qhd_prevention/customWidget/custom_button.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/customWidget/date_picker_dialog.dart';
@ -20,8 +22,8 @@ class MeasuresListWidget extends StatelessWidget {
required this.isAllowEdit,
this.onSign,
this.isShowSign = true,
});
/// Map
final List<Map<String, dynamic>> measuresList;
@ -37,7 +39,6 @@ class MeasuresListWidget extends StatelessWidget {
///
final bool isShowSign;
@override
Widget build(BuildContext context) {
if (measuresList.isEmpty) {
@ -57,8 +58,9 @@ class MeasuresListWidget extends StatelessWidget {
borderRadius: BorderRadius.circular(4),
),
child: Table(
defaultVerticalAlignment: TableCellVerticalAlignment.middle, // .top / .bottom
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
// .top / .bottom
columnWidths: const {
0: FlexColumnWidth(3),
1: FixedColumnWidth(100),
@ -95,7 +97,6 @@ class MeasuresListWidget extends StatelessWidget {
//
for (var item in measuresList)
TableRow(
children: [
// + +
Padding(
@ -117,7 +118,8 @@ class MeasuresListWidget extends StatelessWidget {
// 14 +
for (var i = 1; i <= 4; i++)
if ((item['QUESTION$i'] as String?)?.isNotEmpty ?? false)
if ((item['QUESTION$i'] as String?)?.isNotEmpty ??
false)
_buildQnA(item, i),
],
),
@ -131,40 +133,46 @@ class MeasuresListWidget extends StatelessWidget {
//
isAllowEdit
? TextButton(
onPressed: () {
onSign?.call(item);
},
child: Text(
(item['SIGN_ITEM'] ?? '').toString().isNotEmpty
? '已签字'
: '签字',
style: TextStyle(
color: (item['SIGN_ITEM'] ?? '').toString().isNotEmpty
? Colors.grey.shade600
: Colors.blue,
),
),
)
onPressed: () {
onSign?.call(item);
},
child: Text(
(item['SIGN_ITEM'] ?? '')
.toString()
.isNotEmpty
? '已签字'
: '签字',
style: TextStyle(
color:
(item['SIGN_ITEM'] ?? '')
.toString()
.isNotEmpty
? Colors.grey.shade600
: Colors.blue,
),
),
)
: Text(
(item['STATUS'] as String?) == '-1'
? '不涉及'
: '涉及',
style: TextStyle(
color: (item['STATUS'] as String?) == '-1'
? Colors.black
: Colors.black,
),
),
(item['STATUS'] as String?) == '-1'
? '不涉及'
: '涉及',
style: TextStyle(
color:
(item['STATUS'] as String?) == '-1'
? Colors.black
: Colors.black,
),
),
//
if (item.containsKey('IMG_PATH') &&
(item['IMG_PATH'] as String).isNotEmpty && isShowSign)
(item['IMG_PATH'] as String).isNotEmpty &&
isShowSign)
..._buildImageRows(
context,
(item['IMG_PATH'] as String).split(','),
'',
),
],
),
),
@ -200,12 +208,16 @@ class MeasuresListWidget extends StatelessWidget {
flex: 1,
child: TextFormField(
initialValue: answer,
textAlign: TextAlign.center, //
textAlign: TextAlign.center,
//
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
decoration: InputDecoration(
isDense: true,
contentPadding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4),
contentPadding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 4,
),
//
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.grey.shade300),
@ -250,21 +262,23 @@ class MeasuresListWidget extends StatelessWidget {
/// +
List<Widget> _buildImageRows(
BuildContext context,
List<String> paths,
String time,
) {
BuildContext context,
List<String> paths,
String time,
) {
return paths.map((p) {
return Padding(
padding: const EdgeInsets.only(top: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.center, //
mainAxisAlignment: MainAxisAlignment.center, //
children: [
GestureDetector(
onTap: () {
presentOpaque(SingleImageViewer(imageUrl: '$baseImgPath$p'), context);
presentOpaque(
SingleImageViewer(imageUrl: '$baseImgPath$p'),
context,
);
},
child: Image.network('$baseImgPath$p', width: 60, height: 60),
),
@ -397,7 +411,10 @@ class OtherMeasuresWidget extends StatelessWidget {
children: [
GestureDetector(
onTap: () {
presentOpaque(SingleImageViewer(imageUrl: '$baseImgPath$path'), context);
presentOpaque(
SingleImageViewer(imageUrl: '$baseImgPath$path'),
context,
);
},
child: Image.network(
'$baseImgPath$path',
@ -602,9 +619,12 @@ class SignaturesListWidget extends StatelessWidget {
children: [
GestureDetector(
onTap:
() => presentOpaque(SingleImageViewer(
imageUrl: '$baseImgPath${signPaths[i]}',
), context),
() => presentOpaque(
SingleImageViewer(
imageUrl: '$baseImgPath${signPaths[i]}',
),
context,
),
child: Image.network(
'$baseImgPath${signPaths[i]}',
width: 100,
@ -693,9 +713,12 @@ class SignaturesListWidget extends StatelessWidget {
children: [
GestureDetector(
onTap:
() => presentOpaque(SingleImageViewer(
imageUrl: '$baseImgPath${signPaths[i]}',
), context),
() => presentOpaque(
SingleImageViewer(
imageUrl: '$baseImgPath${signPaths[i]}',
),
context,
),
child: Image.network(
'$baseImgPath${signPaths[i]}',
width: 100,
@ -738,6 +761,7 @@ class SelectionPopup extends StatefulWidget {
@override
_SelectionPopupState createState() => _SelectionPopupState();
}
class _SelectionPopupState extends State<SelectionPopup> {
late List<Map<String, String>> workList;
late String selectedWorkType;
@ -751,7 +775,7 @@ class _SelectionPopupState extends State<SelectionPopup> {
super.initState();
//
workList = [
{'WORK_TYPE': '', 'WORK_NAME': '作业选择'},
{'WORK_TYPE': '', 'WORK_NAME': '选择'},
{'WORK_TYPE': 'HOTWORK', 'WORK_NAME': '动火作业'},
{'WORK_TYPE': 'CONFINEDSPACE', 'WORK_NAME': '受限作业'},
{'WORK_TYPE': 'HIGHWORK', 'WORK_NAME': '高处作业'},
@ -772,20 +796,17 @@ class _SelectionPopupState extends State<SelectionPopup> {
}
Future<void> _pickDate() async {
showDialog(
context: context,
builder:
(_) => HDatePickerDialog(
initialDate: DateTime.now(),
onCancel: () => Navigator.of(context).pop(),
onConfirm: (selected) {
Navigator.of(context).pop();
setState(() {
selectedDate = selected;
});
},
),
DateTime? picked = await BottomDateTimePicker.showDate(
context,
mode: BottomPickerMode.date, // BottomPickerMode.dateTime
allowFuture: true,
minTimeStr: '0-0-0 00:00',
);
if (picked != null) {
setState(() {
selectedDate = picked;
});
}
}
Future<void> _getData() async {
@ -795,7 +816,9 @@ class _SelectionPopupState extends State<SelectionPopup> {
params = {
'WORK_TYPE': selectedWorkType,
'KEYWORDS':
selectedDate == null ? '' : selectedDate!.toString().split(' ')[0],
selectedDate == null
? ''
: DateFormat('yyyy-MM-dd').format(selectedDate!),
'CORPINFO_ID': SessionService.instance.corpinfoId,
};
} else {
@ -857,87 +880,137 @@ class _SelectionPopupState extends State<SelectionPopup> {
@override
Widget build(BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5), //
),
backgroundColor: Colors.white,
insetPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 40),
child: SizedBox(
width: double.infinity,
height: 800,
height: screenHeight(context),
child: Column(
children: [
//
if (widget.type == 'assignments')
Padding(
padding: const EdgeInsets.all(12),
child: Row(
child: Column(
children: [
//
Expanded(
child: DropdownButton<String>(
dropdownColor: Colors.white,
style: TextStyle(),
isExpanded: true,
value: selectedWorkName,
items:
workList
.map(
(e) => DropdownMenuItem(
value: e['WORK_NAME'],
child: Text(
e['WORK_NAME']!,
style: TextStyle(color: Colors.black87),
Row(
children: [
//
Text('作业类型:'),
Expanded(
child: DropdownButton<String>(
dropdownColor: Colors.white,
isExpanded: true,
value: selectedWorkName,
//
items:
workList
.map(
(e) => DropdownMenuItem<String>(
value: e['WORK_NAME'],
child: Center(
child: Text(
e['WORK_NAME'] ?? '',
style: TextStyle(
color: Colors.black87,
fontSize: 15,
),
textAlign: TextAlign.center,
),
),
),
)
.toList(),
// selectedItemBuilder
selectedItemBuilder: (BuildContext context) {
return workList.map<Widget>((e) {
return Center(
child: Text(
e['WORK_NAME'] ?? '',
style: TextStyle(
color: Colors.black87,
fontSize: 15,
),
textAlign: TextAlign.center,
),
)
.toList(),
onChanged: (v) {
final idx = workList.indexWhere(
(e) => e['WORK_NAME'] == v,
);
if (idx >= 0) {
setState(() {
selectedWorkType = workList[idx]['WORK_TYPE']!;
selectedWorkName = v!;
});
_getData();
}
},
),
),
const SizedBox(width: 12),
);
}).toList();
},
TextButton(
onPressed: _pickDate,
child: Row(
children: [
Text(
selectedDate == null
? '选择作业申请时间'
: selectedDate!.toString().split(' ')[0],
style: TextStyle(color: Colors.blue),
onChanged: (v) {
final idx = workList.indexWhere(
(e) => e['WORK_NAME'] == v,
);
if (idx >= 0) {
setState(() {
selectedWorkType =
workList[idx]['WORK_TYPE']!;
selectedWorkName = v!;
});
_getData();
}
},
),
SizedBox(width: 5),
Icon(
Icons.arrow_drop_down,
color: Colors.grey,
size: 20,
),
const SizedBox(width: 12),
TextButton(
onPressed: _pickDate,
child: Row(
children: [
Text(
selectedDate == null
? '选择作业申请时间'
: selectedDate!.toString().split(' ')[0],
style: TextStyle(color: Colors.blue),
),
SizedBox(width: 5),
Icon(
Icons.arrow_drop_down,
color: Colors.grey,
size: 20,
),
],
),
],
),
),
],
),
//
CustomButton(
text: '清空',
padding: EdgeInsets.symmetric(horizontal: 15),
height: 35,
backgroundColor: Colors.blue,
onPressed: _reset,
Row(
children: [
//
Expanded(
child: CustomButton(
text: '搜索',
padding: EdgeInsets.symmetric(horizontal: 15),
margin: const EdgeInsets.symmetric(horizontal: 0),
height: 35,
backgroundColor: Colors.green,
onPressed: _getData,
),
),
const SizedBox(width: 10,),
//
Expanded(
child: CustomButton(
text: '清空',
margin: const EdgeInsets.symmetric(horizontal: 0),
textStyle: TextStyle(color: Colors.black),
padding: EdgeInsets.symmetric(horizontal: 15),
height: 35,
backgroundColor: Colors.grey.shade200,
onPressed: _reset,
),
),
],
),
],
),
),
const Divider(),
//
Expanded(
child: ListView.builder(
@ -952,15 +1025,17 @@ class _SelectionPopupState extends State<SelectionPopup> {
: 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),
Text(key, style: TextStyle(fontSize: 15)),
if (widget.type == 'assignments') ...[
Text('作业内容: ${item['WORK_CONTENT'] ?? ''}'),
Text('作业负责人: ${item['CONFIRM_USER_NAME'] ?? ''}'),
Text('作业申请时间: ${item['CREATTIME'] ?? ''}'),
Text('作业内容: ${item['WORK_CONTENT'] ?? ''}', style: TextStyle(fontSize: 15),),
Text('作业负责人: ${item['CONFIRM_USER_NAME'] ?? ''}', style: TextStyle(fontSize: 15)),
Text('作业申请时间: ${item['CREATTIME'] ?? ''}', style: TextStyle(fontSize: 15)),
],
],
),
@ -974,6 +1049,7 @@ class _SelectionPopupState extends State<SelectionPopup> {
});
},
);
},
),
),
@ -1009,5 +1085,3 @@ class _SelectionPopupState extends State<SelectionPopup> {
);
}
}

View File

@ -119,6 +119,7 @@ class _HotWorkDetailFormWidgetState extends State<HotWorkDetailFormWidget> {
controller: widget.contentController,
text: pd['WORK_CONTENT'] ?? '',
),
const Divider(),
ItemListWidget.singleLineTitleText(
label: '动火地点及动火部位:',

View File

@ -1,6 +1,7 @@
import 'dart:convert';
import 'package:encrypt/encrypt.dart' as encrypt;
import 'package:pointycastle/asymmetric/api.dart' show RSAPublicKey;
import 'package:qhd_prevention/customWidget/toast_util.dart';
import 'package:qhd_prevention/tools/tools.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:fluttertoast/fluttertoast.dart';
@ -31,6 +32,8 @@ class AuthService {
final data = await ApiService.loginCheck(encrypted);
final result = data['result'] as String? ?? '';
if (result != 'success') {
Fluttertoast.showToast(msg: data['msg'] ?? '');
return false;
}

View File

@ -0,0 +1,47 @@
import 'dart:io';
import 'package:path/path.dart' as path;
import 'package:video_compress/video_compress.dart';
class VideoConverter {
/// mp4 mp4
static Future<String> convertToMp4(String inputPath) async {
final ext = path.extension(inputPath).toLowerCase();
// mp4
if (ext == '.mp4') {
return inputPath;
}
try {
print('开始转换: $inputPath');
// + mp4
final MediaInfo? info = await VideoCompress.compressVideo(
inputPath,
quality: VideoQuality.DefaultQuality, // : Low, Medium, High
deleteOrigin: false, //
includeAudio: true,
);
if (info == null || info.path == null) {
throw Exception('视频转换失败: $inputPath');
}
print('转换完成: ${info.path}');
return info.path!;
} catch (e) {
print('视频转换出错: $e');
rethrow;
}
}
/// mp4
static Future<List<String>> convertAllToMp4(List<String> videoPaths) async {
final results = <String>[];
for (final path in videoPaths) {
final newPath = await convertToMp4(path);
results.add(newPath);
}
return results;
}
}

View File

@ -16,11 +16,14 @@ int getRandomWithNum(int min, int max) {
return random.nextInt(max - min + 1) + min; // [min, max]
}
double screenHeight(BuildContext context) {
double screenHeight = MediaQuery.of(context).size.height;
return screenHeight;
}
double screenWidth(BuildContext context) {
double screenWidth = MediaQuery.of(context).size.width;
return screenWidth;
}
Future<T?> pushPage<T>(Widget page, BuildContext context) {
return Navigator.push<T>(
context,
@ -194,6 +197,8 @@ class SessionService {
String? customRecordDangerJson;
String? unqualifiedInspectionItemID;
String? listItemNameJson;
String? studyToken;
///
@ -205,6 +210,7 @@ class SessionService {
// setters
void setLoginUser(Map<String, dynamic> user) => loginUser = user;
void setStudyToken(String token) => studyToken = token;
void setLoginUserId(String id) => loginUserId = id;

File diff suppressed because it is too large Load Diff

View File

@ -106,7 +106,8 @@ dependencies:
#百度地图
flutter_baidu_mapapi_base: ^3.9.5
flutter_baidu_mapapi_map: ^3.9.5
#文件处理
video_compress: ^3.1.4
dev_dependencies:
flutter_test:
sdk: flutter