diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 60d1c89..597ecbb 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -6,9 +6,13 @@
-
-
-
+
+
+
+
+
+
+
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 17732bc..9a1b7bc 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -8,6 +8,40 @@ PODS:
- Flutter
- connectivity_plus (0.0.1):
- Flutter
+ - DKImagePickerController/Core (4.3.9):
+ - DKImagePickerController/ImageDataManager
+ - DKImagePickerController/Resource
+ - DKImagePickerController/ImageDataManager (4.3.9)
+ - DKImagePickerController/PhotoGallery (4.3.9):
+ - DKImagePickerController/Core
+ - DKPhotoGallery
+ - DKImagePickerController/Resource (4.3.9)
+ - DKPhotoGallery (0.0.19):
+ - DKPhotoGallery/Core (= 0.0.19)
+ - DKPhotoGallery/Model (= 0.0.19)
+ - DKPhotoGallery/Preview (= 0.0.19)
+ - DKPhotoGallery/Resource (= 0.0.19)
+ - SDWebImage
+ - SwiftyGif
+ - DKPhotoGallery/Core (0.0.19):
+ - DKPhotoGallery/Model
+ - DKPhotoGallery/Preview
+ - SDWebImage
+ - SwiftyGif
+ - DKPhotoGallery/Model (0.0.19):
+ - SDWebImage
+ - SwiftyGif
+ - DKPhotoGallery/Preview (0.0.19):
+ - DKPhotoGallery/Model
+ - DKPhotoGallery/Resource
+ - SDWebImage
+ - SwiftyGif
+ - DKPhotoGallery/Resource (0.0.19):
+ - SDWebImage
+ - SwiftyGif
+ - file_picker (0.0.1):
+ - DKImagePickerController/PhotoGallery
+ - Flutter
- Flutter (1.0.0)
- flutter_baidu_mapapi_base (3.9.0):
- BaiduMapKit/Utils (= 6.6.4)
@@ -39,12 +73,18 @@ PODS:
- FlutterMacOS
- pdfx (1.0.0):
- Flutter
+ - permission_handler_apple (9.3.0):
+ - Flutter
- photo_manager (3.7.1):
- Flutter
- FlutterMacOS
+ - SDWebImage (5.21.1):
+ - SDWebImage/Core (= 5.21.1)
+ - SDWebImage/Core (5.21.1)
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
+ - SwiftyGif (5.4.5)
- url_launcher_ios (0.0.1):
- Flutter
- video_compress (0.3.0):
@@ -61,6 +101,7 @@ PODS:
DEPENDENCIES:
- camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
+ - file_picker (from `.symlinks/plugins/file_picker/ios`)
- Flutter (from `Flutter`)
- flutter_baidu_mapapi_base (from `.symlinks/plugins/flutter_baidu_mapapi_base/ios`)
- flutter_baidu_mapapi_map (from `.symlinks/plugins/flutter_baidu_mapapi_map/ios`)
@@ -74,6 +115,7 @@ DEPENDENCIES:
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- pdfx (from `.symlinks/plugins/pdfx/ios`)
+ - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- 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`)
@@ -85,12 +127,18 @@ DEPENDENCIES:
SPEC REPOS:
trunk:
- BaiduMapKit
+ - DKImagePickerController
+ - DKPhotoGallery
+ - SDWebImage
+ - SwiftyGif
EXTERNAL SOURCES:
camera_avfoundation:
:path: ".symlinks/plugins/camera_avfoundation/ios"
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios"
+ file_picker:
+ :path: ".symlinks/plugins/file_picker/ios"
Flutter:
:path: Flutter
flutter_baidu_mapapi_base:
@@ -117,6 +165,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
pdfx:
:path: ".symlinks/plugins/pdfx/ios"
+ permission_handler_apple:
+ :path: ".symlinks/plugins/permission_handler_apple/ios"
photo_manager:
:path: ".symlinks/plugins/photo_manager/ios"
shared_preferences_foundation:
@@ -136,6 +186,9 @@ SPEC CHECKSUMS:
BaiduMapKit: 84991811cb07b24c6ead7d59022c13245427782c
camera_avfoundation: be3be85408cd4126f250386828e9b1dfa40ab436
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
+ DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
+ DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
+ file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_baidu_mapapi_base: 24dd82034374c6f52a73e90316834c63ff8d4f64
flutter_baidu_mapapi_map: f799cc1bb3d39196b8d3d59399ca8635e690bd44
@@ -149,8 +202,11 @@ SPEC CHECKSUMS:
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
pdfx: 77f4dddc48361fbb01486fa2bdee4532cbb97ef3
+ permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
photo_manager: 1d80ae07a89a67dfbcae95953a1e5a24af7c3e62
+ SDWebImage: f29024626962457f3470184232766516dee8dfea
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
+ SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
video_compress: f2133a07762889d67f0711ac831faa26f956980e
video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b
diff --git a/lib/customWidget/ItemWidgetFactory.dart b/lib/customWidget/ItemWidgetFactory.dart
index ae767f5..f42630b 100644
--- a/lib/customWidget/ItemWidgetFactory.dart
+++ b/lib/customWidget/ItemWidgetFactory.dart
@@ -32,7 +32,7 @@ class ListItemFactory {
Text(
leftText,
style: TextStyle(
- fontSize: 15,
+ fontSize: 13,
fontWeight: FontWeight.bold,
color: textColor,
),
@@ -47,7 +47,7 @@ class ListItemFactory {
fit: FlexFit.loose,
child: Text(
rightText,
- style: TextStyle(fontSize: 15, color: Colors.grey),
+ style: TextStyle(fontSize: 13, color: Colors.grey),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
@@ -65,7 +65,7 @@ class ListItemFactory {
fit: FlexFit.loose,
child: Text(
rightText,
- style: TextStyle(fontSize: 15, color: Colors.grey),
+ style: TextStyle(fontSize: 13, color: Colors.grey),
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.right,
@@ -101,7 +101,7 @@ class ListItemFactory {
Text(
topText,
style: TextStyle(
- fontSize: 15,
+ fontSize: 13,
fontWeight: FontWeight.bold,
color: Colors.black,
),
@@ -109,7 +109,7 @@ class ListItemFactory {
const SizedBox(height: 5),
Text(
bottomText,
- style: TextStyle(fontSize: 15, color: Colors.grey),
+ style: TextStyle(fontSize: 13, color: Colors.grey),
softWrap: true,
maxLines: null, // 允许无限行数
),
@@ -139,7 +139,7 @@ class ListItemFactory {
Text(
text,
style: const TextStyle(
- fontSize: 15,
+ fontSize: 13,
fontWeight: FontWeight.bold,
color: Colors.black,
),
@@ -202,7 +202,7 @@ class ListItemFactory {
Text(
text,
style: const TextStyle(
- fontSize: 15,
+ fontSize: 13,
fontWeight: FontWeight.bold,
color: Colors.black,
),
@@ -250,7 +250,7 @@ class ListItemFactory {
children: [
Text(
text,
- style: TextStyle(fontSize: 15, color: Colors.grey),
+ style: TextStyle(fontSize: 13, color: Colors.grey),
softWrap: true,
maxLines: null, // 允许无限行数
),
@@ -296,7 +296,7 @@ class ListItemFactory {
child: Text(
title,
style: TextStyle(
- fontSize: 15,
+ fontSize: 13,
fontWeight: FontWeight.bold,
color: Colors.black,
),
@@ -418,7 +418,7 @@ class ListItemFactory {
if (isRequired) Text('* ', style: TextStyle(color: Colors.red)),
Text(
title,
- style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
+ style: const TextStyle(fontSize: 13, fontWeight: FontWeight.bold),
),
],
),
@@ -458,7 +458,7 @@ class ListItemFactory {
if (isRequired) Text('* ', style: TextStyle(color: Colors.red)),
// 标题
Expanded(
- child:HhTextStyleUtils.mainTitle(label, fontSize: 15),
+ child:HhTextStyleUtils.mainTitle(label, fontSize: 13),
),
],
),
@@ -472,7 +472,7 @@ class ListItemFactory {
keyboardType: TextInputType.multiline,
maxLines: null,
expands: true,
- style: const TextStyle(fontSize: 15),
+ style: const TextStyle(fontSize: 13),
decoration: InputDecoration(
hintText: hint,
border: InputBorder.none,
@@ -519,7 +519,7 @@ class ListItemFactory {
child: Text(
title,
style: TextStyle(
- fontSize: 15,
+ fontSize: 13,
fontWeight: FontWeight.bold,
color: Colors.black,
),
diff --git a/lib/customWidget/bottom_picker.dart b/lib/customWidget/bottom_picker.dart
index 99054ee..1dc38c5 100644
--- a/lib/customWidget/bottom_picker.dart
+++ b/lib/customWidget/bottom_picker.dart
@@ -28,7 +28,7 @@ class BottomPicker {
required List items,
required Widget Function(T item) itemBuilder,
int initialIndex = 0,
- double itemExtent = 40.0,
+ double itemExtent = 50.0,
double height = 250,
}) {
if (items.isEmpty) return Future.value(null);
@@ -82,13 +82,15 @@ class BottomPicker {
scrollController: FixedExtentScrollController(
initialItem: initialIndex,
),
- itemExtent: 30,
+ itemExtent: itemExtent,
onSelectedItemChanged: (index) {
selected = items[index];
},
- children: items.map(itemBuilder).toList(),
+ // 把 itemBuilder 返回的 Widget 用 Center 包一层
+ children: items.map((item) => Center(child: itemBuilder(item))).toList(),
),
),
+
],
),
);
diff --git a/lib/customWidget/bottom_picker_two.dart b/lib/customWidget/bottom_picker_two.dart
index 438465a..c696694 100644
--- a/lib/customWidget/bottom_picker_two.dart
+++ b/lib/customWidget/bottom_picker_two.dart
@@ -27,7 +27,7 @@ class BottomPickerTwo {
required List items,
required Widget Function(dynamic item) itemBuilder,
int initialIndex = 0,
- double itemExtent = 40.0,
+ double itemExtent = 50.0,
double height = 250,
double desiredSpacing = 16.0,
}) {
@@ -72,7 +72,7 @@ class BottomPickerTwo {
scrollController: FixedExtentScrollController(
initialItem: initialIndex,
),
- itemExtent: 35,
+ itemExtent: itemExtent,
onSelectedItemChanged: (index) {
selected = items[index];
},
@@ -88,7 +88,7 @@ class BottomPickerTwo {
// '选项 $index',
items[index]["NAME"],
style: TextStyle(
- fontSize: 18,
+ fontSize: 15,
color:
index == selected
? CupertinoColors.activeBlue
diff --git a/lib/customWidget/full_screen_video_page.dart b/lib/customWidget/full_screen_video_page.dart
index c14b3d4..9862a03 100644
--- a/lib/customWidget/full_screen_video_page.dart
+++ b/lib/customWidget/full_screen_video_page.dart
@@ -65,7 +65,7 @@ class _VideoPlayerPopupState extends State {
playedColor: Colors.blue,
backgroundColor: Colors.white,
handleColor: Colors.blue,
- bufferedColor: Colors.red,
+ bufferedColor: Colors.white,
),
aspectRatio: _videoController.value.aspectRatio > 0
? _videoController.value.aspectRatio
diff --git a/lib/customWidget/photo_picker_row.dart b/lib/customWidget/photo_picker_row.dart
index ae510bc..0099314 100644
--- a/lib/customWidget/photo_picker_row.dart
+++ b/lib/customWidget/photo_picker_row.dart
@@ -2,7 +2,8 @@ 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:permission_handler/permission_handler.dart';
+import 'package:qhd_prevention/customWidget/custom_alert_dialog.dart';
import 'package:video_compress/video_compress.dart';
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
import 'package:photo_manager/photo_manager.dart';
@@ -50,12 +51,15 @@ class _MediaPickerGridState extends State {
@override
void initState() {
super.initState();
- _mediaPaths = widget.initialMediaPaths != null
- ? widget.initialMediaPaths!.take(widget.maxCount).toList()
- : [];
+ _mediaPaths =
+ widget.initialMediaPaths != null
+ ? widget.initialMediaPaths!.take(widget.maxCount).toList()
+ : [];
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.onChanged(
- _mediaPaths.map((p) => p.startsWith('http') ? File('') : File(p)).toList(),
+ _mediaPaths
+ .map((p) => p.startsWith('http') ? File('') : File(p))
+ .toList(),
);
});
}
@@ -110,70 +114,55 @@ class _MediaPickerGridState extends State {
}
}
- 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; // 不可编辑时直接返回
showModalBottomSheet(
context: context,
backgroundColor: Colors.white,
- builder: (_) => SafeArea(
- child: Wrap(
- children: [
- ListTile(
- titleAlignment: ListTileTitleAlignment.center,
- leading: Icon(
- widget.mediaType == MediaType.image ? Icons.camera_alt : Icons.videocam,
- ),
- title: Text(widget.mediaType == MediaType.image ? '拍照' : '拍摄视频'),
- onTap: () {
- Navigator.of(context).pop();
- _pickCamera();
- },
+ builder:
+ (_) => SafeArea(
+ child: Wrap(
+ children: [
+ ListTile(
+ titleAlignment: ListTileTitleAlignment.center,
+ leading: Icon(
+ widget.mediaType == MediaType.image
+ ? Icons.camera_alt
+ : Icons.videocam,
+ ),
+ title: Text(
+ widget.mediaType == MediaType.image ? '拍照' : '拍摄视频',
+ ),
+ onTap: () {
+ Navigator.of(context).pop();
+ _pickCamera();
+ },
+ ),
+ ListTile(
+ titleAlignment: ListTileTitleAlignment.center,
+ leading: Icon(
+ widget.mediaType == MediaType.image
+ ? Icons.photo_library
+ : Icons.video_library,
+ ),
+ title: Text(
+ widget.mediaType == MediaType.image ? '从相册选择' : '从相册选择视频',
+ ),
+ onTap: () {
+ Navigator.of(context).pop();
+ _pickGallery();
+ },
+ ),
+ ListTile(
+ titleAlignment: ListTileTitleAlignment.center,
+ leading: const Icon(Icons.close),
+ title: const Text('取消'),
+ onTap: () => Navigator.of(context).pop(),
+ ),
+ ],
),
- ListTile(
- titleAlignment: ListTileTitleAlignment.center,
- leading: Icon(
- widget.mediaType == MediaType.image ? Icons.photo_library : Icons.video_library,
- ),
- title: Text(widget.mediaType == MediaType.image ? '从相册选择' : '从相册选择视频'),
- onTap: () {
- Navigator.of(context).pop();
- _pickGallery();
- },
- ),
- ListTile(
- titleAlignment: ListTileTitleAlignment.center,
- leading: const Icon(Icons.close),
- title: const Text('取消'),
- onTap: () => Navigator.of(context).pop(),
- ),
- ],
- ),
- ),
+ ),
);
}
@@ -201,52 +190,181 @@ class _MediaPickerGridState extends State {
}
}
+ // 修改 _pickGallery 方法
Future _pickGallery() async {
if (!widget.isEdit || _mediaPaths.length >= widget.maxCount) return;
- final permission = await PhotoManager.requestPermissionExtend();
- if (permission != PermissionState.authorized &&
- permission != PermissionState.limited) {
- ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(content: Text('请到设置中开启相册访问权限')),
- );
- return;
- }
try {
- final remaining = widget.maxCount - _mediaPaths.length;
- final List? assets = await AssetPicker.pickAssets(
- context,
- pickerConfig: AssetPickerConfig(
- requestType: widget.mediaType == MediaType.image ? RequestType.image : RequestType.video,
- maxAssets: remaining,
- gridCount: 4,
- ),
- );
- if (assets != null) {
- for (final asset in assets) {
- if (_mediaPaths.length >= widget.maxCount) break;
- final file = await asset.file;
- if (file != null) {
- final path = file.path;
- // 交给统一处理(会转码视频)
- await _handlePickedPath(path);
+ // iOS: 使用 PhotoManager.requestPermissionExtend() 唤起系统权限弹窗(支持 limited)
+ if (Platform.isIOS) {
+ final permission = await PhotoManager.requestPermissionExtend();
+ debugPrint('iOS photo permission state: $permission');
+
+ if (permission != PermissionState.authorized &&
+ permission != PermissionState.limited) {
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text('请到设置中开启相册访问权限')),
+ );
+ }
+ return;
+ }
+
+ // 授权或 limited:继续打开 AssetPicker
+ final remaining = widget.maxCount - _mediaPaths.length;
+ final List? assets = await AssetPicker.pickAssets(
+ context,
+ pickerConfig: AssetPickerConfig(
+ requestType:
+ widget.mediaType == MediaType.image ? RequestType.image : RequestType.video,
+ maxAssets: remaining,
+ gridCount: 4,
+ ),
+ );
+
+ if (assets != null) {
+ for (final asset in assets) {
+ if (_mediaPaths.length >= widget.maxCount) break;
+ try {
+ final file = await asset.file;
+ if (file != null) {
+ final path = file.path;
+ await _handlePickedPath(path);
+ } else {
+ debugPrint('资产获取 file 为空,asset id: ${asset.id}');
+ }
+ } catch (e) {
+ debugPrint('读取 asset 文件失败: $e');
+ }
+ }
+ if (mounted) setState(() {});
+ widget.onChanged(_mediaPaths.map((p) => File(p)).toList());
+ }
+
+ } else {
+ // Android: 使用 permission_handler 请求存储权限(简单处理)
+ final PermissionStatus current = await Permission.storage.status;
+ PermissionStatus status;
+ if (current.isGranted) {
+ status = PermissionStatus.granted;
+ } else {
+ status = await Permission.storage.request();
+ }
+
+ debugPrint('Android storage permission: $status');
+
+ if (status == PermissionStatus.granted) {
+ final remaining = widget.maxCount - _mediaPaths.length;
+ final List? assets = await AssetPicker.pickAssets(
+ context,
+ pickerConfig: AssetPickerConfig(
+ requestType:
+ widget.mediaType == MediaType.image ? RequestType.image : RequestType.video,
+ maxAssets: remaining,
+ gridCount: 4,
+ ),
+ );
+
+ if (assets != null) {
+ for (final asset in assets) {
+ if (_mediaPaths.length >= widget.maxCount) break;
+ try {
+ final file = await asset.file;
+ if (file != null) {
+ final path = file.path;
+ await _handlePickedPath(path);
+ } else {
+ debugPrint('资产获取 file 为空,asset id: ${asset.id}');
+ }
+ } catch (e) {
+ debugPrint('读取 asset 文件失败: $e');
+ }
+ }
+ if (mounted) setState(() {});
+ widget.onChanged(_mediaPaths.map((p) => File(p)).toList());
+ }
+ } else if (status == PermissionStatus.permanentlyDenied) {
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text('请到设置中开启相册访问权限')),
+ );
+ }
+ await openAppSettings();
+ } else {
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text('相册访问权限被拒绝')),
+ );
}
}
- setState(() {});
- widget.onChanged(_mediaPaths.map((p) => File(p)).toList());
}
- } catch (e) {
- debugPrint('相册选择失败: $e');
+ } catch (e, st) {
+ debugPrint('相册选择失败: $e\n$st');
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text('相册选择失败')),
+ );
+ }
}
}
- void _removeMedia(int index) {
- if (!widget.isEdit) return; // 不可编辑时不允许删除
+ // 修改拍照方法,使用 permission_handler 请求相机权限
+ Future _cameraAction() async {
+ if (!widget.isEdit || _mediaPaths.length >= widget.maxCount) return;
- final removed = _mediaPaths[index];
- setState(() => _mediaPaths.removeAt(index));
- widget.onChanged(_mediaPaths.map((p) => File(p)).toList());
- widget.onMediaRemoved?.call(removed);
+ // 请求相机权限
+ final PermissionStatus status = await Permission.camera.request();
+
+ if (status != PermissionStatus.granted) {
+ if (mounted) {
+ ScaffoldMessenger.of(
+ context,
+ ).showSnackBar(const SnackBar(content: Text('相机权限被拒绝')));
+ }
+ 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');
+ // 友好提示
+ if (mounted) {
+ ScaffoldMessenger.of(
+ context,
+ ).showSnackBar(const SnackBar(content: Text('拍摄失败,请检查相机权限或设备状态')));
+ }
+ }
+ }
+
+ void _removeMedia(int index) async {
+ final ok = await CustomAlertDialog.showConfirm(
+ context,
+ title: '温馨提示',
+ content:
+ widget.mediaType == MediaType.image ? '确定要删除这张图片吗?' : '确定要删除这个视频吗?',
+ cancelText: '取消',
+ );
+ if (ok) {
+ final removed = _mediaPaths[index];
+ setState(() => _mediaPaths.removeAt(index));
+ widget.onChanged(_mediaPaths.map((p) => File(p)).toList());
+ widget.onMediaRemoved?.call(removed);
+ }
}
@override
@@ -278,27 +396,42 @@ class _MediaPickerGridState extends State {
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,
- ),
- ),
+ child: SizedBox.expand(
+ // 让内容强制填满格子
+ child:
+ widget.mediaType == MediaType.image
+ ? (isNetwork
+ ? Image.network(
+ path,
+ fit: BoxFit.cover, // 铺满格子并裁剪
+ )
+ : Image.file(
+ File(path),
+ fit: BoxFit.cover, // 铺满格子并裁剪
+ ))
+ : Container(
+ color: Colors.black12,
+ child: const Center(
+ child: Icon(
+ Icons.videocam,
+ color: Colors.white70,
+ ),
+ ),
+ ),
),
),
+
// 只在可编辑状态下显示删除按钮
if (widget.isEdit)
Positioned(
top: -15,
right: -15,
child: IconButton(
- icon: const Icon(Icons.cancel, size: 20, color: Colors.red),
+ icon: const Icon(
+ Icons.cancel,
+ size: 20,
+ color: Colors.red,
+ ),
onPressed: () => _removeMedia(index),
),
),
@@ -331,9 +464,7 @@ class _MediaPickerGridState extends State {
Positioned.fill(
child: Container(
color: Colors.transparent,
- child: const Center(
- child: CircularProgressIndicator(),
- ),
+ child: const Center(child: CircularProgressIndicator()),
),
),
],
@@ -389,7 +520,8 @@ class _RepairedPhotoSectionState extends State {
@override
void initState() {
super.initState();
- _mediaPaths = widget.initialMediaPaths?.take(widget.maxCount).toList() ?? [];
+ _mediaPaths =
+ widget.initialMediaPaths?.take(widget.maxCount).toList() ?? [];
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.onChanged(_mediaPaths.map((p) => File(p)).toList());
});
@@ -407,7 +539,10 @@ class _RepairedPhotoSectionState extends State {
padding: EdgeInsets.symmetric(horizontal: widget.horizontalPadding),
child: ListItemFactory.createRowSpaceBetweenItem(
leftText: widget.title,
- rightText: widget.isShowNum ? '${_mediaPaths.length}/${widget.maxCount}' : '',
+ rightText:
+ widget.isShowNum
+ ? '${_mediaPaths.length}/${widget.maxCount}'
+ : '',
isRequired: widget.isRequired,
),
),
@@ -428,14 +563,17 @@ class _RepairedPhotoSectionState extends State {
},
onMediaAdded: widget.onMediaAdded,
onMediaRemoved: widget.onMediaRemoved,
- onMediaTapped: widget.onMediaTapped, // 传递点击回调
+ onMediaTapped: widget.onMediaTapped,
+ // 传递点击回调
isEdit: widget.isEdit, // 传递编辑状态
),
),
- const SizedBox(height: 20),
+ const SizedBox(height: 8),
if (widget.isShowAI && widget.isEdit) // 只在可编辑状态下显示AI按钮
Padding(
- padding: EdgeInsets.symmetric(horizontal: widget.horizontalPadding),
+ padding: EdgeInsets.symmetric(
+ horizontal: widget.horizontalPadding,
+ ),
child: GestureDetector(
onTap: widget.onAiIdentify,
child: Container(
diff --git a/lib/customWidget/picker/CupertinoDatePicker.dart b/lib/customWidget/picker/CupertinoDatePicker.dart
index 765d5fc..7c3cf31 100644
--- a/lib/customWidget/picker/CupertinoDatePicker.dart
+++ b/lib/customWidget/picker/CupertinoDatePicker.dart
@@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
/// context,
/// mode: BottomPickerMode.date, // 或 BottomPickerMode.dateTime(默认)
/// allowFuture: true,
+/// allowPast: false, // 是否允许选择过去(false 表示只能选择现在或未来)
/// minTimeStr: '2025-08-20 08:30',
/// );
/// if (picked != null) {
@@ -16,7 +17,8 @@ enum BottomPickerMode { dateTime, date }
class BottomDateTimePicker {
static Future showDate(
BuildContext context, {
- bool allowFuture = false,
+ bool allowFuture = true,
+ bool allowPast = true, // 新增:是否允许选择过去(默认允许)
String? minTimeStr, // 可选:'yyyy-MM-dd HH:mm'
BottomPickerMode mode = BottomPickerMode.dateTime,
}) {
@@ -29,6 +31,7 @@ class BottomDateTimePicker {
),
builder: (_) => _InlineDateTimePickerContent(
allowFuture: allowFuture,
+ allowPast: allowPast,
minTimeStr: minTimeStr,
mode: mode,
),
@@ -38,12 +41,14 @@ class BottomDateTimePicker {
class _InlineDateTimePickerContent extends StatefulWidget {
final bool allowFuture;
+ final bool allowPast;
final String? minTimeStr;
final BottomPickerMode mode;
const _InlineDateTimePickerContent({
Key? key,
- this.allowFuture = false,
+ this.allowFuture = true,
+ this.allowPast = true,
this.minTimeStr,
this.mode = BottomPickerMode.dateTime,
}) : super(key: key);
@@ -87,13 +92,25 @@ class _InlineDateTimePickerContentState
// 解析 minTimeStr(若提供)
_minTime = _parseMinTime(widget.minTimeStr);
- // 初始时间:取 now 与 _minTime 的较大者
+ // 初始时间:取 now 与 _minTime 的较大者(但要考虑 allowPast)
final now = DateTime.now();
DateTime initial = now;
+
+ // 如果指定了最小时间并且比 now 晚,则以最小时间为初始
if (_minTime != null && _minTime!.isAfter(initial)) {
initial = _minTime!;
}
+ // 如果不允许选择过去,则确保 initial 至少为 now(或当天的 00:00,取决于模式)
+ if (!widget.allowPast) {
+ if (widget.mode == BottomPickerMode.date) {
+ final today = DateTime(now.year, now.month, now.day);
+ if (initial.isBefore(today)) initial = today;
+ } else {
+ if (initial.isBefore(now)) initial = now;
+ }
+ }
+
// 如果是 date 模式,只保留日期部分(时分归零)
if (widget.mode == BottomPickerMode.date) {
initial = DateTime(initial.year, initial.month, initial.day);
@@ -120,7 +137,7 @@ class _InlineDateTimePickerContentState
minuteCtrl = FixedExtentScrollController(
initialItem: selectedMinute.clamp(0, minutes.length - 1));
- // 确保初始选择满足约束(例如 minTime 或禁止未来)
+ // 确保初始选择满足约束(例如 minTime 或禁止未来/禁止过去)
WidgetsBinding.instance.addPostFrameCallback((_) {
_enforceConstraintsAndUpdateControllers();
});
@@ -169,7 +186,7 @@ class _InlineDateTimePickerContentState
});
}
- // 检查并限制时间(模式感知)
+ // 检查并限制时间(模式感知),支持 allowPast 与 allowFuture
void _enforceConstraintsAndUpdateControllers() {
final now = DateTime.now();
final isDateOnly = widget.mode == BottomPickerMode.date;
@@ -179,35 +196,48 @@ class _InlineDateTimePickerContentState
: DateTime(
selectedYear, selectedMonth, selectedDay, selectedHour, selectedMinute);
- // 处理 _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;
+ // 处理最小时间约束:结合 _minTime 与 allowPast
+ DateTime? minRef;
+ if (!widget.allowPast) {
+ // 不允许选择过去:最小时间至少为 now(或当天 00:00)
+ minRef = isDateOnly
+ ? DateTime(now.year, now.month, now.day)
+ : now;
+ // 如果用户也指定了 _minTime 且比 now 晚,则以 _minTime 为准
+ if (_minTime != null && _minTime!.isAfter(minRef)) {
+ minRef = isDateOnly
+ ? DateTime(_minTime!.year, _minTime!.month, _minTime!.day)
+ : _minTime;
}
+ } else if (_minTime != null) {
+ // 允许选择过去,但若指定了 _minTime,则以 _minTime 为最小参考
+ minRef = isDateOnly
+ ? DateTime(_minTime!.year, _minTime!.month, _minTime!.day)
+ : _minTime;
+ }
+
+ if (minRef != null && 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;
}
// 处理禁止选择未来(当 allowFuture == false)
@@ -370,7 +400,7 @@ class _InlineDateTimePickerContentState
return Expanded(
child: CupertinoPicker.builder(
scrollController: controller,
- itemExtent: 32,
+ itemExtent: 40,
childCount: items.length,
onSelectedItemChanged: onSelected,
itemBuilder: (context, index) {
diff --git a/lib/http/ApiService.dart b/lib/http/ApiService.dart
index 35344cc..90bc7c7 100644
--- a/lib/http/ApiService.dart
+++ b/lib/http/ApiService.dart
@@ -237,20 +237,29 @@ U6Hzm1ninpWeE+awIDAQAB
},
);
}
-
- /// 检查记录详情地图信息
- static Future