From 40bd6e4d4dc22da76953ab9b3755ba00168b7545 Mon Sep 17 00:00:00 2001 From: hs <873121290@qq.com> Date: Fri, 29 Aug 2025 09:52:48 +0800 Subject: [PATCH] =?UTF-8?q?bug=E4=BF=AE=E5=A4=8D=E6=9A=82=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/app/build.gradle.kts | 42 +- assets/map/test_baidu_map.html | 2 +- ios/Podfile.lock | 6 + ios/Runner/Info.plist | 176 +++--- lib/customWidget/big_video_viewer.dart | 12 +- lib/customWidget/full_screen_video_page.dart | 130 ++++- lib/customWidget/photo_picker_row.dart | 264 +++++---- .../picker/CupertinoDatePicker.dart | 184 +++--- lib/customWidget/video_player_widget.dart | 536 ++++++++++++------ lib/http/ApiService.dart | 136 +++-- lib/http/HttpManager.dart | 120 ++-- lib/main.dart | 9 +- .../check_record_detail_page.dart | 1 + lib/pages/app/danger_wait_list_page.dart | 2 +- .../app/hidden_danger_acceptance_page.dart | 2 + lib/pages/app/hidden_record_detail_page.dart | 2 + .../pending_rectification_detail_page.dart | 12 +- lib/pages/home/NFC/home_nfc_add_page.dart | 5 +- .../home/NFC/home_nfc_check_danger_page.dart | 173 ++++-- lib/pages/home/NFC/home_nfc_detail_page.dart | 168 +++--- lib/pages/home/NFC/home_nfc_list_page.dart | 310 +++++----- .../home/NFC/nfc_check_danger_detail.dart | 181 ++++-- lib/pages/home/NFC/nfc_question_fecebook.dart | 2 +- .../home/study/study_class_list_page.dart | 38 +- lib/pages/home/study/study_detail_page.dart | 248 ++++---- lib/pages/home/study/study_my_task_page.dart | 52 +- lib/pages/home/study/study_practise_page.dart | 9 +- lib/pages/home/study/take_exam_page.dart | 22 +- lib/pages/home/tap/item_list_widget.dart | 6 +- .../special_wrok/MeasuresListWidget.dart | 326 +++++++---- .../dh_work/HotWorkDetailFormWidget.dart | 1 + lib/services/auth_service.dart | 3 + lib/tools/VideoConverter.dart | 47 ++ lib/tools/tools.dart | 8 +- pubspec.lock | 356 ++++++------ pubspec.yaml | 3 +- 36 files changed, 2216 insertions(+), 1378 deletions(-) create mode 100644 lib/tools/VideoConverter.dart diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index b24a9e8..3f71f7d 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -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 { diff --git a/assets/map/test_baidu_map.html b/assets/map/test_baidu_map.html index bd5fd19..010ecac 100644 --- a/assets/map/test_baidu_map.html +++ b/assets/map/test_baidu_map.html @@ -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); diff --git a/ios/Podfile.lock b/ios/Podfile.lock index e510ae3..17732bc 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -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 diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index ee7a908..74a4f8e 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -1,94 +1,94 @@ - - CADisableMinimumFrameDurationOnPhone - - CFBundleDisplayName - ${PRODUCT_NAME} - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - 智守安全 - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSRequiresIPhoneOS - - NFCReaderUsageDescription - 需要NFC权限来读取和写入标签 - NSAppTransportSecurity - NSAllowsArbitraryLoads + CADisableMinimumFrameDurationOnPhone + CFBundleDisplayName + ${PRODUCT_NAME} + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + 智守安全 + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + NFCReaderUsageDescription + 需要NFC权限来读取和写入标签 + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + NSBluetoothAlwaysUsageDescription + app需要蓝牙权限连接设备 + NSCameraUsageDescription + app需要相机权限来扫描二维码 + NSContactsUsageDescription + app需要通讯录权限添加好友 + NSHealthShareUsageDescription + app需要读取健康数据 + NSHealthUpdateUsageDescription + app需要写入健康数据 + NSLocalNetworkUsageDescription + app需要发现本地网络设备 + NSLocationAlwaysAndWhenInUseUsageDescription + app需要后台定位以实现持续跟踪 + NSLocationAlwaysUsageDescription + 需要位置权限以提供定位服务 + NSLocationWhenInUseUsageDescription + 需要位置权限以提供定位服务 + NSMicrophoneUsageDescription + app需要麦克风权限进行语音通话 + NSMotionUsageDescription + app需要访问运动数据统计步数 + NSPhotoLibraryAddUsageDescription + app需要保存图片到相册 + NSPhotoLibraryUsageDescription + app需要访问相册以上传图片 + NSUserNotificationsUsageDescription + app需要发送通知提醒重要信息 + UIApplicationSupportsIndirectInputEvents + + UIBackgroundModes + + remote-notification + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIStatusBarHidden + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + com.apple.developer.nfc.readersession.formats + + NDEF + TAG + - NSBluetoothAlwaysUsageDescription - app需要蓝牙权限连接设备 - NSCameraUsageDescription - app需要相机权限来扫描二维码 - NSContactsUsageDescription - app需要通讯录权限添加好友 - NSHealthShareUsageDescription - app需要读取健康数据 - NSHealthUpdateUsageDescription - app需要写入健康数据 - NSLocalNetworkUsageDescription - app需要发现本地网络设备 - NSLocationAlwaysAndWhenInUseUsageDescription - app需要后台定位以实现持续跟踪 - NSLocationAlwaysUsageDescription - 需要位置权限以提供定位服务 - NSLocationWhenInUseUsageDescription - 需要位置权限以提供定位服务 - NSMicrophoneUsageDescription - app需要麦克风权限进行语音通话 - NSMotionUsageDescription - app需要访问运动数据统计步数 - NSPhotoLibraryAddUsageDescription - app需要保存图片到相册 - NSPhotoLibraryUsageDescription - app需要访问相册以上传图片 - NSUserNotificationsUsageDescription - app需要发送通知提醒重要信息 - UIApplicationSupportsIndirectInputEvents - - UIBackgroundModes - - remote-notification - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UIStatusBarHidden - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - com.apple.developer.nfc.readersession.formats - - NDEF - TAG - - - + \ No newline at end of file diff --git a/lib/customWidget/big_video_viewer.dart b/lib/customWidget/big_video_viewer.dart index 9f12746..68d0000 100644 --- a/lib/customWidget/big_video_viewer.dart +++ b/lib/customWidget/big_video_viewer.dart @@ -50,14 +50,14 @@ class _BigVideoViewerState extends State { 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, ), - ), + ),) ); } } diff --git a/lib/customWidget/full_screen_video_page.dart b/lib/customWidget/full_screen_video_page.dart index d71d8e2..c14b3d4 100644 --- a/lib/customWidget/full_screen_video_page.dart +++ b/lib/customWidget/full_screen_video_page.dart @@ -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 { 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 _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 { ), 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 { 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 { ), ); } + + 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))); + } } diff --git a/lib/customWidget/photo_picker_row.dart b/lib/customWidget/photo_picker_row.dart index da6bba1..ae510bc 100644 --- a/lib/customWidget/photo_picker_row.dart +++ b/lib/customWidget/photo_picker_row.dart @@ -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 { final ImagePicker _picker = ImagePicker(); late List _mediaPaths; + bool _isProcessing = false; // 转码或处理时显示 loading @override void initState() { @@ -56,16 +59,81 @@ class _MediaPickerGridState extends State { ); }); } - Future _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 _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 _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 _showPickerOptions() async { if (!widget.isEdit) return; // 不可编辑时直接返回 @@ -78,13 +146,9 @@ class _MediaPickerGridState extends State { 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 { 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 { 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 { final List? 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 { 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 { 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 { ), ); } -} \ No newline at end of file +} diff --git a/lib/customWidget/picker/CupertinoDatePicker.dart b/lib/customWidget/picker/CupertinoDatePicker.dart index 4d1b2d3..765d5fc 100644 --- a/lib/customWidget/picker/CupertinoDatePicker.dart +++ b/lib/customWidget/picker/CupertinoDatePicker.dart @@ -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 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( 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 存在并大于 now,我们已在上面处理(minTime 优先),所以这里处理的是普通情况 - 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(); + }); + }, + ), ], ), ), diff --git a/lib/customWidget/video_player_widget.dart b/lib/customWidget/video_player_widget.dart index dfc13a8..99c2507 100644 --- a/lib/customWidget/video_player_widget.dart +++ b/lib/customWidget/video_player_widget.dart @@ -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 { bool _visibleControls = true; Timer? _hideTimer; - late Timer _positionTimer; + Timer? _positionTimer; Duration _currentPosition = Duration.zero; Duration _totalDuration = Duration.zero; bool _isPlaying = false; - final ValueNotifier _sliderValue = ValueNotifier(0.0); + final ValueNotifier _sliderValue = ValueNotifier(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 { }); } - 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 _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 { ), ), ), - ) - .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( - 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 { ); } + 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( + 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); diff --git a/lib/http/ApiService.dart b/lib/http/ApiService.dart index 95c3ecd..eb4f9c5 100644 --- a/lib/http/ApiService.dart +++ b/lib/http/ApiService.dart @@ -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> 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> 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> 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> 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> 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> 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¤tPage=$currentPage', + basePath, + '/app/pipelineInspection/getPatrolTaskList?showCount=$showCount¤tPage=$currentPage', method: Method.post, data: { @@ -3759,8 +3760,8 @@ U6Hzm1ninpWeE+awIDAQAB Map data, ) { return HttpManager().request( - baseNFCPath, - '/pipelineInspection/getPatrolTaskDetailList?showCount=$showCount¤tPage=$currentPage', + basePath, + '/app/pipelineInspection/getPatrolTaskDetailList?showCount=$showCount¤tPage=$currentPage', method: Method.post, data: { ...data, @@ -3772,8 +3773,8 @@ U6Hzm1ninpWeE+awIDAQAB static Future> 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> 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> 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> nfcInspectionRecord( + int showCount, + int currentPage, + ) async { + return HttpManager().request( + basePath, + '/app/pipelineInspection/getPatrolRecordList?showCount=$showCount¤tPage=$currentPage', + method: Method.post, + data: { + "CORPINFO_ID": SessionService.instance.corpinfoId, + "USER_ID": SessionService.instance.loginUserId, + }, + ); + } } diff --git a/lib/http/HttpManager.dart b/lib/http/HttpManager.dart index 9857a1c..052e651 100644 --- a/lib/http/HttpManager.dart +++ b/lib/http/HttpManager.dart @@ -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? data, Map? 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 = {}; - 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) { - // 如果已经是ApiException类型(401转换的) - 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 ? resp.data as Map : {}; - // 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; } } diff --git a/lib/main.dart b/lib/main.dart index fa21135..6094842 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -14,6 +14,8 @@ import 'package:flutter/services.dart'; // for TextInput.hide // 全局导航键 final GlobalKey navigatorKey = GlobalKey(); +// 全局路由 +final RouteObserver routeObserver = RouteObserver(); // 全局消息控制器 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, diff --git a/lib/pages/app/Danger_paicha/check_record_detail_page.dart b/lib/pages/app/Danger_paicha/check_record_detail_page.dart index 4f8b3a2..0d2f8f1 100644 --- a/lib/pages/app/Danger_paicha/check_record_detail_page.dart +++ b/lib/pages/app/Danger_paicha/check_record_detail_page.dart @@ -393,6 +393,7 @@ class _CheckRecordDetailPageState extends State { // 加载页面(注意:如果 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'); } diff --git a/lib/pages/app/danger_wait_list_page.dart b/lib/pages/app/danger_wait_list_page.dart index ef95ea4..c5a4b05 100644 --- a/lib/pages/app/danger_wait_list_page.dart +++ b/lib/pages/app/danger_wait_list_page.dart @@ -360,7 +360,7 @@ class _DangerWaitListPageState extends State { Future _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; diff --git a/lib/pages/app/hidden_danger_acceptance_page.dart b/lib/pages/app/hidden_danger_acceptance_page.dart index 605f933..e3eef1b 100644 --- a/lib/pages/app/hidden_danger_acceptance_page.dart +++ b/lib/pages/app/hidden_danger_acceptance_page.dart @@ -628,6 +628,8 @@ class _HiddenDangerAcceptancePageState extends State case '3': return '标准排查清单检查'; case '4': return '专项检查'; case '5': return '安全检查'; + case '6': return 'NFC设备巡检'; + default: return ''; } } diff --git a/lib/pages/app/hidden_record_detail_page.dart b/lib/pages/app/hidden_record_detail_page.dart index 58b528d..1ba5b78 100644 --- a/lib/pages/app/hidden_record_detail_page.dart +++ b/lib/pages/app/hidden_record_detail_page.dart @@ -379,6 +379,8 @@ class _HiddenRecordDetailPageState extends State { case '3': return '标准排查清单检查'; case '4': return '专项检查'; case '5': return '安全检查'; + case '6': return 'NFC设备巡检'; + default: return ''; } } diff --git a/lib/pages/app/pending_rectification_detail_page.dart b/lib/pages/app/pending_rectification_detail_page.dart index e0a0d75..b2c59fe 100644 --- a/lib/pages/app/pending_rectification_detail_page.dart +++ b/lib/pages/app/pending_rectification_detail_page.dart @@ -94,10 +94,14 @@ class _PendingRectificationDetailPageState extends State filesZheng = data['rImgs'] ?? []; for (var img in data['rImgs']) { @@ -736,6 +740,8 @@ class _PendingRectificationDetailPageState extends State { @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), diff --git a/lib/pages/home/NFC/home_nfc_check_danger_page.dart b/lib/pages/home/NFC/home_nfc_check_danger_page.dart index ed54414..23b818f 100644 --- a/lib/pages/home/NFC/home_nfc_check_danger_page.dart +++ b/lib/pages/home/NFC/home_nfc_check_danger_page.dart @@ -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 facebookImages; // nfc异常上报 final bool isNfcError; + final bool isEdit; @override State createState() => _HomeNfcCheckDangerPageState(); @@ -57,6 +59,7 @@ class HomeNfcCheckDangerPage extends StatefulWidget { class _HomeNfcCheckDangerPageState extends State { late Map pd = {}; OptionData? selectType; // 初始为 null(未选择) + late bool unchecked = true; final List _options = const [ OptionData( @@ -82,7 +85,7 @@ class _HomeNfcCheckDangerPageState extends State { 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 { if (result['result'] == 'success') { setState(() { pd = result['pd']; + List hImgs = result['hImgs']; + List gzImageList = result['rImgs']; + List imgs = []; + List 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 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 _submit() async { - // 保护:如果未选择任何选项就不提交 + // 如果未选择任何选项就不提交 if (selectType == null) { // 可选:提示用户选择 ToastUtil.showNormal(context, '请先选择检查结果'); @@ -115,49 +141,61 @@ class _HomeNfcCheckDangerPageState extends State { } 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 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 imgList = pd['imgList'] ?? []; + List _videos = pd['videoList'] ?? []; + List 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 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 { } } - Future _reloadFeedBack(String imagePath, String type) async { + Future _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 { void _pushDangerDetail() async { // pushPage 可能返回 pd,保持原逻辑:等待详情页返回新的 pd final result = await pushPage>( - 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 { 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 { 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 { 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 { children: [ _pendingTopCard(widget.info), const Spacer(), + if (widget.isEdit) CustomButton( enabled: canSubmit, text: '提交', diff --git a/lib/pages/home/NFC/home_nfc_detail_page.dart b/lib/pages/home/NFC/home_nfc_detail_page.dart index 600aaef..efdaaac 100644 --- a/lib/pages/home/NFC/home_nfc_detail_page.dart +++ b/lib/pages/home/NFC/home_nfc_detail_page.dart @@ -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 createState() => _HomeNfcDetailPageState(); @@ -139,9 +144,8 @@ class _HomeNfcDetailPageState extends State { _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 { // 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 { } 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 { text: item['USER_NAME'] ?? '', ), ItemListWidget.singleLineTitleText( + // maxLines: 1, label: '涉及管道区域:', isEditable: false, text: item['PIPELINE_AREAS_NAMES'] ?? '', @@ -319,7 +327,7 @@ class _HomeNfcDetailPageState extends State { 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 { // 中间详情 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 { 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), ], ), ); diff --git a/lib/pages/home/NFC/home_nfc_list_page.dart b/lib/pages/home/NFC/home_nfc_list_page.dart index b370c67..c7ed10a 100644 --- a/lib/pages/home/NFC/home_nfc_list_page.dart +++ b/lib/pages/home/NFC/home_nfc_list_page.dart @@ -22,7 +22,7 @@ class _HomeNfcListPageState extends State // 分页状态 - 待巡检 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 await _getTaskDetailList(page: _recordPage + 1, replace: false); } - // ---------- 网络请求(适配后端返回结构) ---------- //NFC任务列表 Future _getTaskList({required int page, required bool replace}) async { setState(() { @@ -207,59 +206,39 @@ class _HomeNfcListPageState extends State _recordLoading = true; }); try { - // final res = await ApiService.nfcTaskDetailList(_pageSize, page); - List? 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?; - // } - // list ??= []; - // - // final parsed = list.map>((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? list; + if (res['result'] == 'success') { + List list = res['varList']; + + final parsed = + list.map>((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 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 } } 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 /// 构建待巡检卡片 Widget _pendingCard(Map 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 ); } + /// 构建信息网格 Widget _buildInfoGrid(Map 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),) + ], + ) ], ); + } } diff --git a/lib/pages/home/NFC/nfc_check_danger_detail.dart b/lib/pages/home/NFC/nfc_check_danger_detail.dart index 5bbafda..a32506b 100644 --- a/lib/pages/home/NFC/nfc_check_danger_detail.dart +++ b/lib/pages/home/NFC/nfc_check_danger_detail.dart @@ -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 info; + final bool isEdit; @override State createState() => _NfcCheckDangerDetailState(); @@ -40,11 +55,11 @@ class _NfcCheckDangerDetailState extends State { Map _personCache = {}; // 隐患图片 - late List imgList = []; - late List _videos = []; + late List imgList = []; + late List _videos = []; // 整改图片 - late List zgImgList = []; + late List zgImgList = []; @override void initState() { @@ -55,7 +70,7 @@ class _NfcCheckDangerDetailState extends State { imgList = pd['imgList'] ?? []; _videos = pd['videoList'] ?? []; zgImgList = pd['gzImageList'] ?? []; - + _isDanger = pd['RECTIFICATIONTYPE'] == '1'; } _getHazardLevel(); } @@ -91,7 +106,22 @@ class _NfcCheckDangerDetailState extends State { print('Error fetching data: $e'); } } - + String _getIDForPath(String path, List imgList) { + for (nfcImgData data in imgList) { + if (data.path == path) { + return data.id; + } + } + return ''; + } + nfcImgData _getImageDataForPath(String path, List list) { + for (nfcImgData data in list) { + if (data.path == path) { + return data; + } + } + return nfcImgData(path: '', id: ''); + } Future _pickHazardType() async { showModalBottomSheet( context: context, @@ -234,14 +264,29 @@ class _NfcCheckDangerDetailState extends State { 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 { 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 { ItemListWidget.multiLineTitleTextField( label: '隐患描述', - isEditable: true, + isEditable: widget.isEdit, isRequired: false, text: pd['HIDDENDESCR'] ?? '', hintText: '请对隐患进行详细描述(必填项)', @@ -292,7 +355,7 @@ class _NfcCheckDangerDetailState extends State { 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 { 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 { title: "是否立即整改", horizontalPadding: 0, verticalPadding: 0, + text: _isDanger ? '立即整改' : '限期整改', + isEdit: widget.isEdit, yesLabel: "是", noLabel: "否", groupValue: _isDanger, @@ -342,7 +407,7 @@ class _NfcCheckDangerDetailState extends State { children: [ ItemListWidget.multiLineTitleTextField( label: '整改描述', - isEditable: true, + isEditable: widget.isEdit, isRequired: false, text: pd['RECTIFYDESCR'] ?? '', hintText: '请对隐患进行详细描述(必填项)', @@ -358,15 +423,29 @@ class _NfcCheckDangerDetailState extends State { 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 { children: [ ItemListWidget.selectableLineTitleTextRightButton( label: '整改责任部门', - isEditable: true, + isEditable: widget.isEdit, isRequired: false, text: pd['RECTIFICATIONDEPTNAME'] ?? '', onTap: () { @@ -388,7 +467,7 @@ class _NfcCheckDangerDetailState extends State { const Divider(), ItemListWidget.selectableLineTitleTextRightButton( label: '整改责任人', - isEditable: true, + isEditable: widget.isEdit, isRequired: false, text: pd['RECTIFICATIONORNAME'] ?? '', onTap: () { @@ -398,7 +477,7 @@ class _NfcCheckDangerDetailState extends State { const Divider(), ItemListWidget.selectableLineTitleTextRightButton( label: '整改期限', - isEditable: true, + isEditable: widget.isEdit, isRequired: false, text: pd['RECTIFICATIONDEADLINE'] ?? '', onTap: () { @@ -427,9 +506,13 @@ class _NfcCheckDangerDetailState extends State { 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 { ), ); } - + Future _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 _riskListCheckAppAdd() async { if (imgList.isEmpty) { ToastUtil.showNormal(context, '请上传隐患图片'); @@ -463,7 +558,7 @@ class _NfcCheckDangerDetailState extends State { ToastUtil.showNormal(context, '请填整改描述'); return; } - if (zgImgList.isEmpty) { + if (zgImgList.length == 0) { ToastUtil.showNormal(context, '请上传整改后图片'); return; } @@ -481,14 +576,16 @@ class _NfcCheckDangerDetailState extends State { 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; diff --git a/lib/pages/home/NFC/nfc_question_fecebook.dart b/lib/pages/home/NFC/nfc_question_fecebook.dart index c0980e9..b8f31e3 100644 --- a/lib/pages/home/NFC/nfc_question_fecebook.dart +++ b/lib/pages/home/NFC/nfc_question_fecebook.dart @@ -185,7 +185,7 @@ class _NfcQuestionFecebookState extends State { '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++) { diff --git a/lib/pages/home/study/study_class_list_page.dart b/lib/pages/home/study/study_class_list_page.dart index 9eb73f5..5778c90 100644 --- a/lib/pages/home/study/study_class_list_page.dart +++ b/lib/pages/home/study/study_class_list_page.dart @@ -42,7 +42,13 @@ class _StudyClassListPageState extends State { LoadingDialogHelper.hide(); } } - + Future _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 { 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 { 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); + }, + ), + ],) ], ), ), diff --git a/lib/pages/home/study/study_detail_page.dart b/lib/pages/home/study/study_detail_page.dart index 889e3be..5d62190 100644 --- a/lib/pages/home/study/study_detail_page.dart +++ b/lib/pages/home/study/study_detail_page.dart @@ -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 _tabController.dispose(); _videoController?.removeListener(_onTimeUpdate); _videoController?.dispose(); + _videoController = null; _faceTimer?.cancel(); super.dispose(); } @@ -76,15 +75,16 @@ class _StudyDetailPageState extends State Future _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 } Future _onVideoTap( - Map data, - bool hasNodes, - int fi, - int ni, - ) async { + Map 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 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 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 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 ); 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 ..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 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 // 如果结束且可考试,弹框 if (end && pd['CANEXAM'] == '1') { _videoController?.pause(); - final ok = await showDialog( - context: context, - builder: (_) => CustomAlertDialog( - title: '提示', - content: '当前任务内所有课程均已学完,是否直接参加考试?', - confirmText: '是', - cancelText: '否', - ), - ) ?? + final ok = + await showDialog( + context: context, + builder: + (_) => CustomAlertDialog( + title: '提示', + content: '当前任务内所有课程均已学完,是否直接参加考试?', + confirmText: '是', + cancelText: '否', + ), + ) ?? false; if (ok) { - _startExam(resData); } else { _videoController?.play(); @@ -337,12 +349,12 @@ class _StudyDetailPageState extends State _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 _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 } } - - 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 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 final item = _videoList[idx] as Map; final nodes = item['nodes'] as List?; if (nodes != null && nodes.isNotEmpty) { - return ExpansionTile( - title: Text(item['NAME'] ?? ''), - children: - nodes - .asMap() - .entries - .map( - (e) => _buildVideoItem( - e.value as Map, - true, - idx, - e.key, - ), - ) - .toList(), + // 章节标题 + 直接展开的子项列表(不折叠) + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...nodes.asMap().entries.map( + (e) => _buildVideoItem( + item, + e.value as Map, + true, + idx, + e.key, + ), + ), + const Divider(height: 1), // 每章节后一个分割线 + ], ); } - return _buildVideoItem(item, false, idx, 0); + // 没有子节点,直接显示该条目 + return _buildVideoItem(item, item, false, idx, 0); }, ); } Widget _buildVideoItem( + Map item, Map m, bool hasNodes, int fi, @@ -504,9 +532,10 @@ class _StudyDetailPageState extends State 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 ), 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: "课后练习", diff --git a/lib/pages/home/study/study_my_task_page.dart b/lib/pages/home/study/study_my_task_page.dart index bf8ccbe..43d1666 100644 --- a/lib/pages/home/study/study_my_task_page.dart +++ b/lib/pages/home/study/study_my_task_page.dart @@ -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 createState() => _StudyMyTaskPageState(); } -class _StudyMyTaskPageState extends State { +class _StudyMyTaskPageState extends State with RouteAware { int _page = 1; final int _showCount = 10; bool _isLoading = false; @@ -29,29 +30,60 @@ class _StudyMyTaskPageState extends State { int _totalPage = 1; List _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 { } _hasMore = _page < _totalPage; if (_hasMore) _page++; + _startCountdownTimer(); + }); } } catch (e) { @@ -181,7 +215,7 @@ class _StudyMyTaskPageState extends State { } 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 { // 顶部:名称 + 状态 Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Text( @@ -222,6 +257,7 @@ class _StudyMyTaskPageState extends State { style: const TextStyle(fontWeight: FontWeight.bold), ), ), + const SizedBox(width: 10,), Text( _stateText(item['STUDYSTATE']), style: TextStyle(color: _stateColor(item['STUDYSTATE'])), diff --git a/lib/pages/home/study/study_practise_page.dart b/lib/pages/home/study/study_practise_page.dart index 1077580..97c5713 100644 --- a/lib/pages/home/study/study_practise_page.dart +++ b/lib/pages/home/study/study_practise_page.dart @@ -188,17 +188,23 @@ class _PracticePageState extends State { }) { 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 { : 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 { Widget build(BuildContext context) { final q = options.isNotEmpty ? options[current] : null; return Scaffold( + backgroundColor: Colors.white, appBar: MyAppbar(title: '课后练习'), body: loading diff --git a/lib/pages/home/study/take_exam_page.dart b/lib/pages/home/study/take_exam_page.dart index 8e40d6a..fa94b18 100644 --- a/lib/pages/home/study/take_exam_page.dart +++ b/lib/pages/home/study/take_exam_page.dart @@ -82,13 +82,25 @@ class _TakeExamPageState extends State { .map((e) => Question.fromJson(e as Map)) .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 { 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), diff --git a/lib/pages/home/tap/item_list_widget.dart b/lib/pages/home/tap/item_list_widget.dart index d9ed4b8..e96ddb9 100644 --- a/lib/pages/home/tap/item_list_widget.dart +++ b/lib/pages/home/tap/item_list_widget.dart @@ -26,6 +26,7 @@ class ItemListWidget { bool strongRequired = false, ValueChanged? onChanged, ValueChanged? 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, diff --git a/lib/pages/home/tap/tabList/special_wrok/MeasuresListWidget.dart b/lib/pages/home/tap/tabList/special_wrok/MeasuresListWidget.dart index 532a498..0d9a28c 100644 --- a/lib/pages/home/tap/tabList/special_wrok/MeasuresListWidget.dart +++ b/lib/pages/home/tap/tabList/special_wrok/MeasuresListWidget.dart @@ -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> 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 { // 问题1~4 + 答案(可编辑或只读) 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 _buildImageRows( - BuildContext context, - List paths, - String time, - ) { + BuildContext context, + List 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 { late List> workList; late String selectedWorkType; @@ -751,7 +775,7 @@ class _SelectionPopupState extends State { 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 { } Future _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 _getData() async { @@ -795,7 +816,9 @@ class _SelectionPopupState extends State { 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 { @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( - 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( + dropdownColor: Colors.white, + isExpanded: true, + value: selectedWorkName, + // 下拉列表里的项(菜单中居中) + items: + workList + .map( + (e) => DropdownMenuItem( + 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((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 { : 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 { }); }, ); + }, ), ), @@ -1009,5 +1085,3 @@ class _SelectionPopupState extends State { ); } } - - diff --git a/lib/pages/home/tap/tabList/special_wrok/dh_work/HotWorkDetailFormWidget.dart b/lib/pages/home/tap/tabList/special_wrok/dh_work/HotWorkDetailFormWidget.dart index 886305b..fbf126c 100644 --- a/lib/pages/home/tap/tabList/special_wrok/dh_work/HotWorkDetailFormWidget.dart +++ b/lib/pages/home/tap/tabList/special_wrok/dh_work/HotWorkDetailFormWidget.dart @@ -119,6 +119,7 @@ class _HotWorkDetailFormWidgetState extends State { controller: widget.contentController, text: pd['WORK_CONTENT'] ?? '', ), + const Divider(), ItemListWidget.singleLineTitleText( label: '动火地点及动火部位:', diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart index 8846d13..27c4d5a 100644 --- a/lib/services/auth_service.dart +++ b/lib/services/auth_service.dart @@ -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; } diff --git a/lib/tools/VideoConverter.dart b/lib/tools/VideoConverter.dart new file mode 100644 index 0000000..f1fbb10 --- /dev/null +++ b/lib/tools/VideoConverter.dart @@ -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 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> convertAllToMp4(List videoPaths) async { + final results = []; + for (final path in videoPaths) { + final newPath = await convertToMp4(path); + results.add(newPath); + } + return results; + } +} diff --git a/lib/tools/tools.dart b/lib/tools/tools.dart index 1262e08..00d0281 100644 --- a/lib/tools/tools.dart +++ b/lib/tools/tools.dart @@ -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 pushPage(Widget page, BuildContext context) { return Navigator.push( 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 user) => loginUser = user; + void setStudyToken(String token) => studyToken = token; void setLoginUserId(String id) => loginUserId = id; diff --git a/pubspec.lock b/pubspec.lock index 9f72666..f18cae3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -6,7 +6,7 @@ packages: description: name: ansicolor sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.0.3" archive: @@ -14,7 +14,7 @@ packages: description: name: archive sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "4.0.7" args: @@ -22,7 +22,7 @@ packages: description: name: args sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.7.0" asn1lib: @@ -30,7 +30,7 @@ packages: description: name: asn1lib sha256: "9a8f69025044eb466b9b60ef3bc3ac99b4dc6c158ae9c56d25eeccf5bc56d024" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.6.5" async: @@ -38,7 +38,7 @@ packages: description: name: async sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.13.0" boolean_selector: @@ -46,7 +46,7 @@ packages: description: name: boolean_selector sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.1.2" camera: @@ -54,7 +54,7 @@ packages: description: name: camera sha256: d6ec2cbdbe2fa8f5e0d07d8c06368fe4effa985a4a5ddade9cc58a8cd849557d - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.11.2" camera_android_camerax: @@ -62,7 +62,7 @@ packages: description: name: camera_android_camerax sha256: "58b8fe843a3c83fd1273c00cb35f5a8ae507f6cc9b2029bcf7e2abba499e28d8" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.6.19+1" camera_avfoundation: @@ -70,7 +70,7 @@ packages: description: name: camera_avfoundation sha256: e4aca5bccaf897b70cac87e5fdd789393310985202442837922fd40325e2733b - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.9.21+1" camera_platform_interface: @@ -78,7 +78,7 @@ packages: description: name: camera_platform_interface sha256: "2f757024a48696ff4814a789b0bd90f5660c0fb25f393ab4564fb483327930e2" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.10.0" camera_web: @@ -86,7 +86,7 @@ packages: description: name: camera_web sha256: "595f28c89d1fb62d77c73c633193755b781c6d2e0ebcd8dc25b763b514e6ba8f" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.3.5" change_app_package_name: @@ -94,7 +94,7 @@ packages: description: name: change_app_package_name sha256: "8e43b754fe960426904d77ed4c62fa8c9834deaf6e293ae40963fa447482c4c5" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.5.0" characters: @@ -102,7 +102,7 @@ packages: description: name: characters sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.4.0" checked_yaml: @@ -110,7 +110,7 @@ packages: description: name: checked_yaml sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.0.4" chewie: @@ -118,7 +118,7 @@ packages: description: name: chewie sha256: "19b93a1e60e4ba640a792208a6543f1c7d5b124d011ce0199e2f18802199d984" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.12.1" cli_util: @@ -126,7 +126,7 @@ packages: description: name: cli_util sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.4.2" clock: @@ -134,7 +134,7 @@ packages: description: name: clock sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.1.2" collection: @@ -142,7 +142,7 @@ packages: description: name: collection sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.19.1" connectivity_plus: @@ -150,7 +150,7 @@ packages: description: name: connectivity_plus sha256: b5e72753cf63becce2c61fd04dfe0f1c430cc5278b53a1342dc5ad839eab29ec - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "6.1.5" connectivity_plus_platform_interface: @@ -158,7 +158,7 @@ packages: description: name: connectivity_plus_platform_interface sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.0.1" convert: @@ -166,7 +166,7 @@ packages: description: name: convert sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.1.2" cross_file: @@ -174,7 +174,7 @@ packages: description: name: cross_file sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.3.4+2" crypto: @@ -182,7 +182,7 @@ packages: description: name: crypto sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.0.6" csslib: @@ -190,7 +190,7 @@ packages: description: name: csslib sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.0.2" cupertino_icons: @@ -198,7 +198,7 @@ packages: description: name: cupertino_icons sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.0.8" dbus: @@ -206,7 +206,7 @@ packages: description: name: dbus sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.7.11" dio: @@ -214,7 +214,7 @@ packages: description: name: dio sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "5.9.0" dio_web_adapter: @@ -222,7 +222,7 @@ packages: description: name: dio_web_adapter sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.1.1" dotted_border: @@ -230,7 +230,7 @@ packages: description: name: dotted_border sha256: "99b091ec6891ba0c5331fdc2b502993c7c108f898995739a73c6845d71dad70c" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.1.0" encrypt: @@ -238,7 +238,7 @@ packages: description: name: encrypt sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "5.0.3" extended_image: @@ -246,7 +246,7 @@ packages: description: name: extended_image sha256: f6cbb1d798f51262ed1a3d93b4f1f2aa0d76128df39af18ecb77fa740f88b2e0 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "10.0.1" extended_image_library: @@ -254,7 +254,7 @@ packages: description: name: extended_image_library sha256: "1f9a24d3a00c2633891c6a7b5cab2807999eb2d5b597e5133b63f49d113811fe" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "5.0.1" extension: @@ -262,7 +262,7 @@ packages: description: name: extension sha256: be3a6b7f8adad2f6e2e8c63c895d19811fcf203e23466c6296267941d0ff4f24 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.6.0" fake_async: @@ -270,7 +270,7 @@ packages: description: name: fake_async sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.3.3" ffi: @@ -278,7 +278,7 @@ packages: description: name: ffi sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.1.4" file: @@ -286,7 +286,7 @@ packages: description: name: file sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "7.0.1" file_selector_linux: @@ -294,7 +294,7 @@ packages: description: name: file_selector_linux sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.9.3+2" file_selector_macos: @@ -302,7 +302,7 @@ packages: description: name: file_selector_macos sha256: "19124ff4a3d8864fdc62072b6a2ef6c222d55a3404fe14893a3c02744907b60c" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.9.4+4" file_selector_platform_interface: @@ -310,7 +310,7 @@ packages: description: name: file_selector_platform_interface sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.6.2" file_selector_windows: @@ -318,7 +318,7 @@ packages: description: name: file_selector_windows sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.9.3+4" fixnum: @@ -326,7 +326,7 @@ packages: description: name: fixnum sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.1.1" flutter: @@ -339,7 +339,7 @@ packages: description: name: flutter_baidu_mapapi_base sha256: c8b372f0862690a438ec12e7978d937d775d71a5027632a149448ac4e52b1259 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.9.5" flutter_baidu_mapapi_map: @@ -347,7 +347,7 @@ packages: description: name: flutter_baidu_mapapi_map sha256: a4ab01a32ffb76c93bcb9b6629736064ce7f0918c58ea3b8468dcb886326d5fb - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.9.5" flutter_easyloading: @@ -355,7 +355,7 @@ packages: description: name: flutter_easyloading sha256: ba21a3c883544e582f9cc455a4a0907556714e1e9cf0eababfcb600da191d17c - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.0.5" flutter_html: @@ -363,7 +363,7 @@ packages: description: name: flutter_html sha256: "38a2fd702ffdf3243fb7441ab58aa1bc7e6922d95a50db76534de8260638558d" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.0.0" flutter_launcher_icons: @@ -371,7 +371,7 @@ packages: description: name: flutter_launcher_icons sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.14.4" flutter_lints: @@ -379,7 +379,7 @@ packages: description: name: flutter_lints sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "5.0.0" flutter_native_splash: @@ -387,7 +387,7 @@ packages: description: name: flutter_native_splash sha256: "8321a6d11a8d13977fa780c89de8d257cce3d841eecfb7a4cadffcc4f12d82dc" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.4.6" flutter_new_badger: @@ -395,23 +395,23 @@ packages: description: name: flutter_new_badger sha256: d3742ace8009663db1ac6ba0377b092f479c35deb33e05514ba05cc0b0a5aaaa - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.1.1" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "6382ce712ff69b0f719640ce957559dde459e55ecd433c767e06d139ddf16cab" - url: "https://pub.flutter-io.cn" + sha256: b0694b7fb1689b0e6cc193b3f1fcac6423c4f93c74fb20b806c6b6f196db0c31 + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "2.0.29" + version: "2.0.30" flutter_spinkit: dependency: transitive description: name: flutter_spinkit sha256: "77850df57c00dc218bfe96071d576a8babec24cf58b2ed121c83cca4a2fdce7f" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "5.2.2" flutter_test: @@ -429,7 +429,7 @@ packages: description: name: fluttertoast sha256: "25e51620424d92d3db3832464774a6143b5053f15e382d8ffbfd40b6e795dcf1" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "8.2.12" geolocator: @@ -437,7 +437,7 @@ packages: description: name: geolocator sha256: f4efb8d3c4cdcad2e226af9661eb1a0dd38c71a9494b22526f9da80ab79520e5 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "10.1.1" geolocator_android: @@ -445,7 +445,7 @@ packages: description: name: geolocator_android sha256: fcb1760a50d7500deca37c9a666785c047139b5f9ee15aa5469fae7dbbe3170d - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "4.6.2" geolocator_apple: @@ -453,7 +453,7 @@ packages: description: name: geolocator_apple sha256: dbdd8789d5aaf14cf69f74d4925ad1336b4433a6efdf2fce91e8955dc921bf22 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.3.13" geolocator_platform_interface: @@ -461,7 +461,7 @@ packages: description: name: geolocator_platform_interface sha256: "30cb64f0b9adcc0fb36f628b4ebf4f731a2961a0ebd849f4b56200205056fe67" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "4.2.6" geolocator_web: @@ -469,7 +469,7 @@ packages: description: name: geolocator_web sha256: "102e7da05b48ca6bf0a5bda0010f886b171d1a08059f01bfe02addd0175ebece" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.2.1" geolocator_windows: @@ -477,7 +477,7 @@ packages: description: name: geolocator_windows sha256: "175435404d20278ffd220de83c2ca293b73db95eafbdc8131fe8609be1421eb6" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.2.5" html: @@ -485,7 +485,7 @@ packages: description: name: html sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.15.6" http: @@ -493,7 +493,7 @@ packages: description: name: http sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.5.0" http_client_helper: @@ -501,7 +501,7 @@ packages: description: name: http_client_helper sha256: "8a9127650734da86b5c73760de2b404494c968a3fd55602045ffec789dac3cb1" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.0.0" http_parser: @@ -509,7 +509,7 @@ packages: description: name: http_parser sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "4.1.2" image: @@ -517,7 +517,7 @@ packages: description: name: image sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "4.5.4" image_picker: @@ -525,23 +525,23 @@ packages: description: name: image_picker sha256: "736eb56a911cf24d1859315ad09ddec0b66104bc41a7f8c5b96b4e2620cf5041" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.2.0" image_picker_android: dependency: transitive description: name: image_picker_android - sha256: e83b2b05141469c5e19d77e1dfa11096b6b1567d09065b2265d7c6904560050c - url: "https://pub.flutter-io.cn" + sha256: "28f3987ca0ec702d346eae1d90eda59603a2101b52f1e234ded62cff1d5cfa6e" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "0.8.13" + version: "0.8.13+1" image_picker_for_web: dependency: transitive description: name: image_picker_for_web sha256: "40c2a6a0da15556dc0f8e38a3246064a971a9f512386c3339b89f76db87269b6" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.1.0" image_picker_ios: @@ -549,7 +549,7 @@ packages: description: name: image_picker_ios sha256: eb06fe30bab4c4497bad449b66448f50edcc695f1c59408e78aa3a8059eb8f0e - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.8.13" image_picker_linux: @@ -557,7 +557,7 @@ packages: description: name: image_picker_linux sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.2.2" image_picker_macos: @@ -565,7 +565,7 @@ packages: description: name: image_picker_macos sha256: d58cd9d67793d52beefd6585b12050af0a7663c0c2a6ece0fb110a35d6955e04 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.2.2" image_picker_platform_interface: @@ -573,7 +573,7 @@ packages: description: name: image_picker_platform_interface sha256: "9f143b0dba3e459553209e20cc425c9801af48e6dfa4f01a0fcf927be3f41665" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.11.0" image_picker_windows: @@ -581,7 +581,7 @@ packages: description: name: image_picker_windows sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.2.2" intl: @@ -589,7 +589,7 @@ packages: description: name: intl sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.20.2" js: @@ -597,7 +597,7 @@ packages: description: name: js sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.7.2" json_annotation: @@ -605,7 +605,7 @@ packages: description: name: json_annotation sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "4.9.0" launcher_name: @@ -613,7 +613,7 @@ packages: description: name: launcher_name sha256: "5fc9a8b8de9e255d5f21effc33632b5620771c9b2310d459a7710b725353b305" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.0.2" leak_tracker: @@ -621,7 +621,7 @@ packages: description: name: leak_tracker sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "10.0.9" leak_tracker_flutter_testing: @@ -629,7 +629,7 @@ packages: description: name: leak_tracker_flutter_testing sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.0.9" leak_tracker_testing: @@ -637,7 +637,7 @@ packages: description: name: leak_tracker_testing sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.0.1" lints: @@ -645,7 +645,7 @@ packages: description: name: lints sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "5.1.1" list_counter: @@ -653,7 +653,7 @@ packages: description: name: list_counter sha256: c447ae3dfcd1c55f0152867090e67e219d42fe6d4f2807db4bbe8b8d69912237 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.0.2" matcher: @@ -661,7 +661,7 @@ packages: description: name: matcher sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.12.17" material_color_utilities: @@ -669,7 +669,7 @@ packages: description: name: material_color_utilities sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.11.1" meta: @@ -677,7 +677,7 @@ packages: description: name: meta sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.16.0" mime: @@ -685,7 +685,7 @@ packages: description: name: mime sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.0.0" mobile_scanner: @@ -693,7 +693,7 @@ packages: description: name: mobile_scanner sha256: "54005bdea7052d792d35b4fef0f84ec5ddc3a844b250ecd48dc192fb9b4ebc95" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "7.0.1" ndef_record: @@ -701,7 +701,7 @@ packages: description: name: ndef_record sha256: "0c72dfac0d5c16fc264846d103ee5d8249cd3858261a5a537b455a24c1bd5857" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.2.1" nested: @@ -709,7 +709,7 @@ packages: description: name: nested sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.0.0" nfc_manager: @@ -717,7 +717,7 @@ packages: description: name: nfc_manager sha256: "164cc0223dee528d4d05a542da921f0b3a31ca0312400701c93ebf4ce757f676" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "4.0.2" nfc_manager_ndef: @@ -725,7 +725,7 @@ packages: description: name: nfc_manager_ndef sha256: "676e741c42b63ab1fda5a981015cb706ab4fdb76e5d0eec6611993bb27d7e7bf" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.0.1" nm: @@ -733,7 +733,7 @@ packages: description: name: nm sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.5.0" package_info_plus: @@ -741,7 +741,7 @@ packages: description: name: package_info_plus sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "8.3.1" package_info_plus_platform_interface: @@ -749,7 +749,7 @@ packages: description: name: package_info_plus_platform_interface sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.2.1" path: @@ -757,7 +757,7 @@ packages: description: name: path sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.9.1" path_provider: @@ -765,23 +765,23 @@ packages: description: name: path_provider sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.1.5" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 - url: "https://pub.flutter-io.cn" + sha256: "993381400e94d18469750e5b9dcb8206f15bc09f9da86b9e44a9b0092a0066db" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "2.2.17" + version: "2.2.18" path_provider_foundation: dependency: transitive description: name: path_provider_foundation sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.4.2" path_provider_linux: @@ -789,7 +789,7 @@ packages: description: name: path_provider_linux sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.2.1" path_provider_platform_interface: @@ -797,7 +797,7 @@ packages: description: name: path_provider_platform_interface sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.1.2" path_provider_windows: @@ -805,7 +805,7 @@ packages: description: name: path_provider_windows sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.3.0" pdfx: @@ -813,7 +813,7 @@ packages: description: name: pdfx sha256: "29db9b71d46bf2335e001f91693f2c3fbbf0760e4c2eb596bf4bafab211471c1" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.9.2" petitparser: @@ -821,7 +821,7 @@ packages: description: name: petitparser sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "7.0.1" photo_manager: @@ -829,7 +829,7 @@ packages: description: name: photo_manager sha256: a0d9a7a9bc35eda02d33766412bde6d883a8b0acb86bbe37dac5f691a0894e8a - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.7.1" photo_manager_image_provider: @@ -837,7 +837,7 @@ packages: description: name: photo_manager_image_provider sha256: b6015b67b32f345f57cf32c126f871bced2501236c405aafaefa885f7c821e4f - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.2.0" photo_view: @@ -845,7 +845,7 @@ packages: description: name: photo_view sha256: "1fc3d970a91295fbd1364296575f854c9863f225505c28c46e0a03e48960c75e" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.15.0" platform: @@ -853,7 +853,7 @@ packages: description: name: platform sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.1.6" plugin_platform_interface: @@ -861,7 +861,7 @@ packages: description: name: plugin_platform_interface sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.1.8" pointycastle: @@ -869,7 +869,7 @@ packages: description: name: pointycastle sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.9.1" posix: @@ -877,7 +877,7 @@ packages: description: name: posix sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "6.0.3" provider: @@ -885,7 +885,7 @@ packages: description: name: provider sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "6.1.5+1" shared_preferences: @@ -893,23 +893,23 @@ packages: description: name: shared_preferences sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.5.3" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "5bcf0772a761b04f8c6bf814721713de6f3e5d9d89caf8d3fe031b02a342379e" - url: "https://pub.flutter-io.cn" + sha256: a2608114b1ffdcbc9c120eb71a0e207c71da56202852d4aab8a5e30a82269e74 + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "2.4.11" + version: "2.4.12" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.5.4" shared_preferences_linux: @@ -917,7 +917,7 @@ packages: description: name: shared_preferences_linux sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.4.1" shared_preferences_platform_interface: @@ -925,7 +925,7 @@ packages: description: name: shared_preferences_platform_interface sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.4.1" shared_preferences_web: @@ -933,7 +933,7 @@ packages: description: name: shared_preferences_web sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.4.3" shared_preferences_windows: @@ -941,7 +941,7 @@ packages: description: name: shared_preferences_windows sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.4.1" simple_gesture_detector: @@ -949,7 +949,7 @@ packages: description: name: simple_gesture_detector sha256: ba2cd5af24ff20a0b8d609cec3f40e5b0744d2a71804a2616ae086b9c19d19a3 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.2.1" sky_engine: @@ -962,7 +962,7 @@ packages: description: name: source_span sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.10.1" sprintf: @@ -970,7 +970,7 @@ packages: description: name: sprintf sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "7.0.0" stack_trace: @@ -978,7 +978,7 @@ packages: description: name: stack_trace sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.12.1" stream_channel: @@ -986,7 +986,7 @@ packages: description: name: stream_channel sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.1.4" stream_transform: @@ -994,7 +994,7 @@ packages: description: name: stream_transform sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.1.1" string_scanner: @@ -1002,7 +1002,7 @@ packages: description: name: string_scanner sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.4.1" synchronized: @@ -1010,7 +1010,7 @@ packages: description: name: synchronized sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.4.0" table_calendar: @@ -1018,7 +1018,7 @@ packages: description: name: table_calendar sha256: "0c0c6219878b363a2d5f40c7afb159d845f253d061dc3c822aa0d5fe0f721982" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.2.0" term_glyph: @@ -1026,7 +1026,7 @@ packages: description: name: term_glyph sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.2.2" test_api: @@ -1034,7 +1034,7 @@ packages: description: name: test_api sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.7.4" typed_data: @@ -1042,7 +1042,7 @@ packages: description: name: typed_data sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.4.0" universal_io: @@ -1050,7 +1050,7 @@ packages: description: name: universal_io sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.2.2" universal_platform: @@ -1058,7 +1058,7 @@ packages: description: name: universal_platform sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.1.0" url_launcher: @@ -1066,23 +1066,23 @@ packages: description: name: url_launcher sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "6.3.2" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "0aedad096a85b49df2e4725fa32118f9fa580f3b14af7a2d2221896a02cd5656" - url: "https://pub.flutter-io.cn" + sha256: "69ee86740f2847b9a4ba6cffa74ed12ce500bbe2b07f3dc1e643439da60637b7" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "6.3.17" + version: "6.3.18" url_launcher_ios: dependency: transitive description: name: url_launcher_ios sha256: d80b3f567a617cb923546034cc94bfe44eb15f989fe670b37f26abdb9d939cb7 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "6.3.4" url_launcher_linux: @@ -1090,7 +1090,7 @@ packages: description: name: url_launcher_linux sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.2.1" url_launcher_macos: @@ -1098,7 +1098,7 @@ packages: description: name: url_launcher_macos sha256: c043a77d6600ac9c38300567f33ef12b0ef4f4783a2c1f00231d2b1941fea13f - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.2.3" url_launcher_platform_interface: @@ -1106,7 +1106,7 @@ packages: description: name: url_launcher_platform_interface sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.3.2" url_launcher_web: @@ -1114,7 +1114,7 @@ packages: description: name: url_launcher_web sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.4.1" url_launcher_windows: @@ -1122,7 +1122,7 @@ packages: description: name: url_launcher_windows sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.1.4" uuid: @@ -1130,7 +1130,7 @@ packages: description: name: uuid sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "4.5.1" vector_math: @@ -1138,31 +1138,39 @@ packages: description: name: vector_math sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.1.4" + video_compress: + dependency: "direct main" + description: + name: video_compress + sha256: "31bc5cdb9a02ba666456e5e1907393c28e6e0e972980d7d8d619a7beda0d4f20" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "3.1.4" video_player: dependency: "direct main" description: name: video_player sha256: "0d55b1f1a31e5ad4c4967bfaa8ade0240b07d20ee4af1dfef5f531056512961a" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.10.0" video_player_android: dependency: transitive description: name: video_player_android - sha256: "53f3b57c7ac88c18e6074d0f94c7146e128c515f0a4503c3061b8e71dea3a0f2" - url: "https://pub.flutter-io.cn" + sha256: "59e5a457ddcc1688f39e9aef0efb62aa845cf0cbbac47e44ac9730dc079a2385" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "2.8.12" + version: "2.8.13" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation sha256: f9a780aac57802b2892f93787e5ea53b5f43cc57dc107bee9436458365be71cd - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.8.4" video_player_platform_interface: @@ -1170,7 +1178,7 @@ packages: description: name: video_player_platform_interface sha256: cf2a1d29a284db648fd66cbd18aacc157f9862d77d2cc790f6f9678a46c1db5a - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "6.4.0" video_player_web: @@ -1178,7 +1186,7 @@ packages: description: name: video_player_web sha256: "9f3c00be2ef9b76a95d94ac5119fb843dca6f2c69e6c9968f6f2b6c9e7afbdeb" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.4.0" visibility_detector: @@ -1186,7 +1194,7 @@ packages: description: name: visibility_detector sha256: dd5cc11e13494f432d15939c3aa8ae76844c42b723398643ce9addb88a5ed420 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.4.0+2" vm_service: @@ -1194,7 +1202,7 @@ packages: description: name: vm_service sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "15.0.0" wakelock_plus: @@ -1202,7 +1210,7 @@ packages: description: name: wakelock_plus sha256: a474e314c3e8fb5adef1f9ae2d247e57467ad557fa7483a2b895bc1b421c5678 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.3.2" wakelock_plus_platform_interface: @@ -1210,7 +1218,7 @@ packages: description: name: wakelock_plus_platform_interface sha256: e10444072e50dbc4999d7316fd303f7ea53d31c824aa5eb05d7ccbdd98985207 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.2.3" web: @@ -1218,7 +1226,7 @@ packages: description: name: web sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.1.1" webview_flutter: @@ -1226,23 +1234,23 @@ packages: description: name: webview_flutter sha256: c3e4fe614b1c814950ad07186007eff2f2e5dd2935eba7b9a9a1af8e5885f1ba - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "4.13.0" webview_flutter_android: dependency: transitive description: name: webview_flutter_android - sha256: "0a42444056b24ed832bdf3442d65c5194f6416f7e782152384944053c2ecc9a3" - url: "https://pub.flutter-io.cn" + sha256: "9a25f6b4313978ba1c2cda03a242eea17848174912cfb4d2d8ee84a556f248e3" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "4.10.0" + version: "4.10.1" webview_flutter_platform_interface: dependency: transitive description: name: webview_flutter_platform_interface sha256: "63d26ee3aca7256a83ccb576a50272edd7cfc80573a4305caa98985feb493ee0" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.14.0" webview_flutter_wkwebview: @@ -1250,7 +1258,7 @@ packages: description: name: webview_flutter_wkwebview sha256: fb46db8216131a3e55bcf44040ca808423539bc6732e7ed34fb6d8044e3d512f - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.23.0" wechat_assets_picker: @@ -1258,7 +1266,7 @@ packages: description: name: wechat_assets_picker sha256: c307e50394c1e6dfcd5c4701e84efb549fce71444fedcf2e671c50d809b3e2a1 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "9.8.0" wechat_picker_library: @@ -1266,7 +1274,7 @@ packages: description: name: wechat_picker_library sha256: "5cb61b9aa935b60da5b043f8446fbb9c5077419f20ccc4856bf444aec4f44bc1" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.0.7" win32: @@ -1274,7 +1282,7 @@ packages: description: name: win32 sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "5.14.0" xdg_directories: @@ -1282,7 +1290,7 @@ packages: description: name: xdg_directories sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.1.0" xml: @@ -1290,7 +1298,7 @@ packages: description: name: xml sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "6.6.1" yaml: @@ -1298,7 +1306,7 @@ packages: description: name: yaml sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.1.3" sdks: diff --git a/pubspec.yaml b/pubspec.yaml index 93339b8..63f8256 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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