| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  | import 'dart:io'; | 
					
						
							| 
									
										
										
										
											2025-09-05 09:16:54 +08:00
										 |  |  |  | import 'package:device_info_plus/device_info_plus.dart'; | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  | import 'package:flutter/material.dart'; | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  | import 'package:flutter/services.dart'; | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  | import 'package:image_picker/image_picker.dart'; | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  | import 'package:permission_handler/permission_handler.dart'; | 
					
						
							|  |  |  |  | import 'package:qhd_prevention/customWidget/custom_alert_dialog.dart'; | 
					
						
							| 
									
										
										
										
											2025-09-02 16:22:17 +08:00
										 |  |  |  | import 'package:qhd_prevention/customWidget/full_screen_video_page.dart'; | 
					
						
							|  |  |  |  | import 'package:qhd_prevention/customWidget/single_image_viewer.dart'; | 
					
						
							| 
									
										
										
										
											2025-09-05 09:16:54 +08:00
										 |  |  |  | import 'package:qhd_prevention/customWidget/toast_util.dart'; | 
					
						
							| 
									
										
										
										
											2025-09-02 16:22:17 +08:00
										 |  |  |  | import 'package:qhd_prevention/tools/tools.dart'; | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  | import 'package:video_compress/video_compress.dart'; | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  | import 'package:wechat_assets_picker/wechat_assets_picker.dart'; | 
					
						
							|  |  |  |  | import 'package:photo_manager/photo_manager.dart'; | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  | import 'package:path/path.dart' as p; | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  | import 'ItemWidgetFactory.dart'; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | /// 媒体选择类型
 | 
					
						
							|  |  |  |  | enum MediaType { image, video } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-07 17:33:16 +08:00
										 |  |  |  | /// 纵向滚动四列的媒体添加组件,支持拍摄、全屏相册多选,以及初始地址列表展示
 | 
					
						
							| 
									
										
										
										
											2025-08-14 15:05:48 +08:00
										 |  |  |  | /// 新增 isEdit 属性控制编辑状态
 | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  | class MediaPickerRow extends StatefulWidget { | 
					
						
							|  |  |  |  |   final int maxCount; | 
					
						
							|  |  |  |  |   final MediaType mediaType; | 
					
						
							| 
									
										
										
										
											2025-07-30 17:08:46 +08:00
										 |  |  |  |   final List<String>? initialMediaPaths; | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |   final ValueChanged<List<File>> onChanged; | 
					
						
							| 
									
										
										
										
											2025-07-30 17:08:46 +08:00
										 |  |  |  |   final ValueChanged<String>? onMediaAdded; | 
					
						
							|  |  |  |  |   final ValueChanged<String>? onMediaRemoved; | 
					
						
							| 
									
										
										
										
											2025-08-14 15:05:48 +08:00
										 |  |  |  |   final ValueChanged<String>? onMediaTapped; // 新增:媒体点击回调
 | 
					
						
							|  |  |  |  |   final bool isEdit; // 新增:控制编辑状态
 | 
					
						
							| 
									
										
										
										
											2025-08-27 16:14:50 +08:00
										 |  |  |  |   final bool isCamera; // 新增:只能拍照
 | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |   const MediaPickerRow({ | 
					
						
							|  |  |  |  |     Key? key, | 
					
						
							|  |  |  |  |     this.maxCount = 4, | 
					
						
							|  |  |  |  |     this.mediaType = MediaType.image, | 
					
						
							| 
									
										
										
										
											2025-07-30 17:08:46 +08:00
										 |  |  |  |     this.initialMediaPaths, | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |     required this.onChanged, | 
					
						
							| 
									
										
										
										
											2025-07-30 17:08:46 +08:00
										 |  |  |  |     this.onMediaAdded, | 
					
						
							|  |  |  |  |     this.onMediaRemoved, | 
					
						
							| 
									
										
										
										
											2025-08-14 15:05:48 +08:00
										 |  |  |  |     this.onMediaTapped, // 新增
 | 
					
						
							|  |  |  |  |     this.isEdit = true, // 默认可编辑
 | 
					
						
							| 
									
										
										
										
											2025-08-27 16:14:50 +08:00
										 |  |  |  |     this.isCamera = false, | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |   }) : super(key: key); | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   @override | 
					
						
							| 
									
										
										
										
											2025-08-07 17:33:16 +08:00
										 |  |  |  |   _MediaPickerGridState createState() => _MediaPickerGridState(); | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-07 17:33:16 +08:00
										 |  |  |  | class _MediaPickerGridState extends State<MediaPickerRow> { | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |   final ImagePicker _picker = ImagePicker(); | 
					
						
							| 
									
										
										
										
											2025-07-30 17:08:46 +08:00
										 |  |  |  |   late List<String> _mediaPaths; | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |   bool _isProcessing = false; // 转码或处理时显示 loading
 | 
					
						
							| 
									
										
										
										
											2025-07-30 17:08:46 +08:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  |   @override | 
					
						
							|  |  |  |  |   void initState() { | 
					
						
							|  |  |  |  |     super.initState(); | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |     _mediaPaths = | 
					
						
							|  |  |  |  |         widget.initialMediaPaths != null | 
					
						
							|  |  |  |  |             ? widget.initialMediaPaths!.take(widget.maxCount).toList() | 
					
						
							|  |  |  |  |             : []; | 
					
						
							| 
									
										
										
										
											2025-07-30 17:08:46 +08:00
										 |  |  |  |     WidgetsBinding.instance.addPostFrameCallback((_) { | 
					
						
							| 
									
										
										
										
											2025-08-07 17:33:16 +08:00
										 |  |  |  |       widget.onChanged( | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |         _mediaPaths | 
					
						
							|  |  |  |  |             .map((p) => p.startsWith('http') ? File('') : File(p)) | 
					
						
							|  |  |  |  |             .toList(), | 
					
						
							| 
									
										
										
										
											2025-08-07 17:33:16 +08:00
										 |  |  |  |       ); | 
					
						
							| 
									
										
										
										
											2025-07-30 17:08:46 +08:00
										 |  |  |  |     }); | 
					
						
							|  |  |  |  |   } | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  |   // 公共:当得到本地媒体路径时(可能是 mov/avi 等),需要在这里统一处理(转码、入队、回调)
 | 
					
						
							|  |  |  |  |   Future<void> _handlePickedPath(String path) async { | 
					
						
							|  |  |  |  |     if (!mounted) return; | 
					
						
							|  |  |  |  |     if (path.isEmpty) return; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     try { | 
					
						
							|  |  |  |  |       String finalPath = path; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |       // 如果是视频并且不是 mp4,则调用 video_compress 转码
 | 
					
						
							|  |  |  |  |       if (widget.mediaType == MediaType.video) { | 
					
						
							|  |  |  |  |         final ext = p.extension(path).toLowerCase(); | 
					
						
							|  |  |  |  |         if (ext != '.mp4') { | 
					
						
							|  |  |  |  |           setState(() => _isProcessing = true); | 
					
						
							|  |  |  |  |           try { | 
					
						
							|  |  |  |  |             final info = await VideoCompress.compressVideo( | 
					
						
							|  |  |  |  |               path, | 
					
						
							|  |  |  |  |               quality: VideoQuality.MediumQuality, | 
					
						
							|  |  |  |  |               deleteOrigin: false, | 
					
						
							|  |  |  |  |             ); | 
					
						
							|  |  |  |  |             if (info != null && info.file != null) { | 
					
						
							|  |  |  |  |               finalPath = info.file!.path; | 
					
						
							|  |  |  |  |               debugPrint('✅ 转换完成: $path -> $finalPath'); | 
					
						
							|  |  |  |  |             } else { | 
					
						
							|  |  |  |  |               throw Exception("转码失败: 返回空文件"); | 
					
						
							|  |  |  |  |             } | 
					
						
							|  |  |  |  |           } catch (e) { | 
					
						
							|  |  |  |  |             debugPrint('❌ 视频转码失败: $e'); | 
					
						
							|  |  |  |  |             if (mounted) { | 
					
						
							| 
									
										
										
										
											2025-09-05 09:16:54 +08:00
										 |  |  |  |               ToastUtil.showNormal(context, '视频转码失败'); | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |             } | 
					
						
							|  |  |  |  |             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'); | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |   Future<void> _showPickerOptions() async { | 
					
						
							| 
									
										
										
										
											2025-08-14 15:05:48 +08:00
										 |  |  |  |     if (!widget.isEdit) return; // 不可编辑时直接返回
 | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |     showModalBottomSheet( | 
					
						
							|  |  |  |  |       context: context, | 
					
						
							| 
									
										
										
										
											2025-08-11 17:40:03 +08:00
										 |  |  |  |       backgroundColor: Colors.white, | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |       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(), | 
					
						
							|  |  |  |  |                 ), | 
					
						
							|  |  |  |  |               ], | 
					
						
							| 
									
										
										
										
											2025-07-30 17:08:46 +08:00
										 |  |  |  |             ), | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |           ), | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |     ); | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   Future<void> _pickCamera() async { | 
					
						
							| 
									
										
										
										
											2025-08-14 15:05:48 +08:00
										 |  |  |  |     if (!widget.isEdit || _mediaPaths.length >= widget.maxCount) return; | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |     try { | 
					
						
							|  |  |  |  |       XFile? picked; | 
					
						
							|  |  |  |  |       if (widget.mediaType == MediaType.image) { | 
					
						
							|  |  |  |  |         picked = await _picker.pickImage(source: ImageSource.camera); | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |         if (picked != null) { | 
					
						
							|  |  |  |  |           final path = picked.path; | 
					
						
							|  |  |  |  |           setState(() => _mediaPaths.add(path)); | 
					
						
							|  |  |  |  |           widget.onChanged(_mediaPaths.map((p) => File(p)).toList()); | 
					
						
							|  |  |  |  |           widget.onMediaAdded?.call(path); | 
					
						
							|  |  |  |  |         } | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |       } else { | 
					
						
							|  |  |  |  |         picked = await _picker.pickVideo(source: ImageSource.camera); | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |         if (picked != null) { | 
					
						
							|  |  |  |  |           await _handlePickedPath(picked.path); | 
					
						
							|  |  |  |  |         } | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |       } | 
					
						
							|  |  |  |  |     } catch (e) { | 
					
						
							| 
									
										
										
										
											2025-07-28 14:22:07 +08:00
										 |  |  |  |       debugPrint('拍摄失败: $e'); | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |     } | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |   // 修改 _pickGallery 方法
 | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |   Future<void> _pickGallery() async { | 
					
						
							| 
									
										
										
										
											2025-08-14 15:05:48 +08:00
										 |  |  |  |     if (!widget.isEdit || _mediaPaths.length >= widget.maxCount) return; | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |     try { | 
					
						
							|  |  |  |  |       // iOS: 使用 PhotoManager.requestPermissionExtend() 唤起系统权限弹窗(支持 limited)
 | 
					
						
							|  |  |  |  |       if (Platform.isIOS) { | 
					
						
							|  |  |  |  |         final permission = await PhotoManager.requestPermissionExtend(); | 
					
						
							|  |  |  |  |         debugPrint('iOS photo permission state: $permission'); | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |         if (permission != PermissionState.authorized && | 
					
						
							|  |  |  |  |             permission != PermissionState.limited) { | 
					
						
							|  |  |  |  |           if (mounted) { | 
					
						
							| 
									
										
										
										
											2025-09-05 09:16:54 +08:00
										 |  |  |  |             ToastUtil.showNormal(context, '请到设置中开启相册访问权限'); | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |           } | 
					
						
							|  |  |  |  |           return; | 
					
						
							|  |  |  |  |         } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |         // 授权或 limited:继续打开 AssetPicker
 | 
					
						
							|  |  |  |  |         final remaining = widget.maxCount - _mediaPaths.length; | 
					
						
							|  |  |  |  |         final List<AssetEntity>? assets = await AssetPicker.pickAssets( | 
					
						
							|  |  |  |  |           context, | 
					
						
							|  |  |  |  |           pickerConfig: AssetPickerConfig( | 
					
						
							|  |  |  |  |             requestType: | 
					
						
							|  |  |  |  |             widget.mediaType == MediaType.image ? RequestType.image : RequestType.video, | 
					
						
							|  |  |  |  |             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 { | 
					
						
							| 
									
										
										
										
											2025-09-05 09:16:54 +08:00
										 |  |  |  |         // Android: 更可靠地请求权限(适配不同 Android 版本)
 | 
					
						
							|  |  |  |  |         final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); | 
					
						
							|  |  |  |  |         final androidInfo = await deviceInfo.androidInfo; | 
					
						
							|  |  |  |  |         final int sdkInt = androidInfo.version.sdkInt ?? 0; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |         PermissionStatus permissionStatus = PermissionStatus.denied; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |         if (sdkInt >= 33) { | 
					
						
							|  |  |  |  |           // Android 13+: 使用更细粒度的媒体权限
 | 
					
						
							|  |  |  |  |           // 如果应用只选图片,request photos;只选视频则 request videos;若两者都可能同时需要,可请求两个。
 | 
					
						
							|  |  |  |  |           if (widget.mediaType == MediaType.image) { | 
					
						
							|  |  |  |  |             permissionStatus = await Permission.photos.request(); // maps to READ_MEDIA_IMAGES on Android
 | 
					
						
							|  |  |  |  |           } else if (widget.mediaType == MediaType.video) { | 
					
						
							|  |  |  |  |             permissionStatus = await Permission.videos.request(); // maps to READ_MEDIA_VIDEO
 | 
					
						
							|  |  |  |  |           } else { | 
					
						
							|  |  |  |  |             // 两者皆可(同时请求会合并成单个系统对话)
 | 
					
						
							|  |  |  |  |             final statuses = await [Permission.photos, Permission.videos].request(); | 
					
						
							|  |  |  |  |             permissionStatus = statuses[Permission.photos] ?? statuses[Permission.videos] ?? PermissionStatus.denied; | 
					
						
							|  |  |  |  |           } | 
					
						
							|  |  |  |  |         } else if (sdkInt >= 30) { | 
					
						
							|  |  |  |  |           // Android 11/12: 通常仍然使用 READ_EXTERNAL_STORAGE;若需要“全部文件访问”,要 request manageExternalStorage(慎用)
 | 
					
						
							|  |  |  |  |           permissionStatus = await Permission.storage.request(); | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |         } else { | 
					
						
							| 
									
										
										
										
											2025-09-05 09:16:54 +08:00
										 |  |  |  |           // Android 10 及以下
 | 
					
						
							|  |  |  |  |           permissionStatus = await Permission.storage.request(); | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |         } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-05 09:16:54 +08:00
										 |  |  |  |         // 处理结果
 | 
					
						
							|  |  |  |  |         if (permissionStatus.isGranted) { | 
					
						
							|  |  |  |  |           // 有权限:继续打开 AssetPicker
 | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |           final remaining = widget.maxCount - _mediaPaths.length; | 
					
						
							|  |  |  |  |           final List<AssetEntity>? assets = await AssetPicker.pickAssets( | 
					
						
							|  |  |  |  |             context, | 
					
						
							|  |  |  |  |             pickerConfig: AssetPickerConfig( | 
					
						
							| 
									
										
										
										
											2025-09-05 09:16:54 +08:00
										 |  |  |  |               requestType: widget.mediaType == MediaType.image ? RequestType.image : RequestType.video, | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |               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()); | 
					
						
							|  |  |  |  |           } | 
					
						
							| 
									
										
										
										
											2025-09-05 09:16:54 +08:00
										 |  |  |  |         } else if (permissionStatus.isPermanentlyDenied) { | 
					
						
							|  |  |  |  |           // 用户点了“不再询问”或系统拒绝弹窗,跳转设置页并提示
 | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |           if (mounted) { | 
					
						
							| 
									
										
										
										
											2025-09-05 09:16:54 +08:00
										 |  |  |  |             ToastUtil.showNormal(context, '请到设置中开启相册访问权限'); | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |           } | 
					
						
							|  |  |  |  |           await openAppSettings(); | 
					
						
							| 
									
										
										
										
											2025-09-05 09:16:54 +08:00
										 |  |  |  |           return; | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |         } else { | 
					
						
							|  |  |  |  |           if (mounted) { | 
					
						
							| 
									
										
										
										
											2025-09-05 09:16:54 +08:00
										 |  |  |  |             ToastUtil.showNormal(context, '相册访问权限被拒绝'); | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |           } | 
					
						
							| 
									
										
										
										
											2025-09-05 09:16:54 +08:00
										 |  |  |  |           return; | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |         } | 
					
						
							|  |  |  |  |       } | 
					
						
							|  |  |  |  |     } catch (e, st) { | 
					
						
							|  |  |  |  |       debugPrint('相册选择失败: $e\n$st'); | 
					
						
							|  |  |  |  |       if (mounted) { | 
					
						
							|  |  |  |  |         ScaffoldMessenger.of(context).showSnackBar( | 
					
						
							|  |  |  |  |           const SnackBar(content: Text('相册选择失败')), | 
					
						
							|  |  |  |  |         ); | 
					
						
							|  |  |  |  |       } | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   // 修改拍照方法,使用 permission_handler 请求相机权限
 | 
					
						
							|  |  |  |  |   Future<void> _cameraAction() async { | 
					
						
							|  |  |  |  |     if (!widget.isEdit || _mediaPaths.length >= widget.maxCount) return; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     // 请求相机权限
 | 
					
						
							|  |  |  |  |     final PermissionStatus status = await Permission.camera.request(); | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     if (status != PermissionStatus.granted) { | 
					
						
							|  |  |  |  |       if (mounted) { | 
					
						
							|  |  |  |  |         ScaffoldMessenger.of( | 
					
						
							|  |  |  |  |           context, | 
					
						
							|  |  |  |  |         ).showSnackBar(const SnackBar(content: Text('相机权限被拒绝'))); | 
					
						
							|  |  |  |  |       } | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |       return; | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  |     try { | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |       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); | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |         } | 
					
						
							|  |  |  |  |       } | 
					
						
							|  |  |  |  |     } catch (e) { | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |       debugPrint('拍摄失败: $e'); | 
					
						
							|  |  |  |  |       // 友好提示
 | 
					
						
							|  |  |  |  |       if (mounted) { | 
					
						
							|  |  |  |  |         ScaffoldMessenger.of( | 
					
						
							|  |  |  |  |           context, | 
					
						
							|  |  |  |  |         ).showSnackBar(const SnackBar(content: Text('拍摄失败,请检查相机权限或设备状态'))); | 
					
						
							|  |  |  |  |       } | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |     } | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |   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); | 
					
						
							|  |  |  |  |     } | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   @override | 
					
						
							|  |  |  |  |   Widget build(BuildContext context) { | 
					
						
							| 
									
										
										
										
											2025-08-14 15:05:48 +08:00
										 |  |  |  |     final showAddButton = widget.isEdit && _mediaPaths.length < widget.maxCount; | 
					
						
							|  |  |  |  |     final itemCount = _mediaPaths.length + (showAddButton ? 1 : 0); | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |     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'); | 
					
						
							| 
									
										
										
										
											2025-08-14 15:05:48 +08:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |               return GestureDetector( | 
					
						
							|  |  |  |  |                 onTap: () => widget.onMediaTapped?.call(path), | 
					
						
							|  |  |  |  |                 child: Stack( | 
					
						
							|  |  |  |  |                   children: [ | 
					
						
							|  |  |  |  |                     ClipRRect( | 
					
						
							|  |  |  |  |                       borderRadius: BorderRadius.circular(5), | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |                       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, | 
					
						
							|  |  |  |  |                                     ), | 
					
						
							|  |  |  |  |                                   ), | 
					
						
							|  |  |  |  |                                 ), | 
					
						
							| 
									
										
										
										
											2025-08-14 15:05:48 +08:00
										 |  |  |  |                       ), | 
					
						
							| 
									
										
										
										
											2025-07-30 17:08:46 +08:00
										 |  |  |  |                     ), | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |                     // 只在可编辑状态下显示删除按钮
 | 
					
						
							|  |  |  |  |                     if (widget.isEdit) | 
					
						
							|  |  |  |  |                       Positioned( | 
					
						
							|  |  |  |  |                         top: -15, | 
					
						
							|  |  |  |  |                         right: -15, | 
					
						
							|  |  |  |  |                         child: IconButton( | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |                           icon: const Icon( | 
					
						
							|  |  |  |  |                             Icons.cancel, | 
					
						
							|  |  |  |  |                             size: 20, | 
					
						
							|  |  |  |  |                             color: Colors.red, | 
					
						
							|  |  |  |  |                           ), | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |                           onPressed: () => _removeMedia(index), | 
					
						
							|  |  |  |  |                         ), | 
					
						
							|  |  |  |  |                       ), | 
					
						
							|  |  |  |  |                   ], | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |                 ), | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |               ); | 
					
						
							|  |  |  |  |             } | 
					
						
							|  |  |  |  |             // 显示添加按钮
 | 
					
						
							|  |  |  |  |             else if (showAddButton) { | 
					
						
							|  |  |  |  |               return GestureDetector( | 
					
						
							|  |  |  |  |                 onTap: widget.isCamera ? _cameraAction : _showPickerOptions, | 
					
						
							|  |  |  |  |                 child: Container( | 
					
						
							|  |  |  |  |                   decoration: BoxDecoration( | 
					
						
							|  |  |  |  |                     border: Border.all(color: Colors.black12), | 
					
						
							|  |  |  |  |                     borderRadius: BorderRadius.circular(5), | 
					
						
							| 
									
										
										
										
											2025-08-14 15:05:48 +08:00
										 |  |  |  |                   ), | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |                   child: const Center( | 
					
						
							|  |  |  |  |                     child: Icon(Icons.camera_alt, color: Colors.black26), | 
					
						
							|  |  |  |  |                   ), | 
					
						
							|  |  |  |  |                 ), | 
					
						
							|  |  |  |  |               ); | 
					
						
							|  |  |  |  |             } else { | 
					
						
							|  |  |  |  |               return const SizedBox.shrink(); | 
					
						
							|  |  |  |  |             } | 
					
						
							|  |  |  |  |           }, | 
					
						
							|  |  |  |  |         ), | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |         // 转码/处理 loading 遮罩
 | 
					
						
							|  |  |  |  |         if (_isProcessing) | 
					
						
							|  |  |  |  |           Positioned.fill( | 
					
						
							| 
									
										
										
										
											2025-08-07 17:33:16 +08:00
										 |  |  |  |             child: Container( | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |               color: Colors.transparent, | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |               child: const Center(child: CircularProgressIndicator()), | 
					
						
							| 
									
										
										
										
											2025-08-07 17:33:16 +08:00
										 |  |  |  |             ), | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |           ), | 
					
						
							|  |  |  |  |       ], | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |     ); | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-07 17:33:16 +08:00
										 |  |  |  | /// 照片上传区域组件,使用纵向四列Grid展示
 | 
					
						
							| 
									
										
										
										
											2025-08-14 15:05:48 +08:00
										 |  |  |  | /// 新增 isEdit 属性控制编辑状态
 | 
					
						
							| 
									
										
										
										
											2025-07-31 17:33:26 +08:00
										 |  |  |  | class RepairedPhotoSection extends StatefulWidget { | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |   final int maxCount; | 
					
						
							|  |  |  |  |   final MediaType mediaType; | 
					
						
							|  |  |  |  |   final String title; | 
					
						
							| 
									
										
										
										
											2025-07-30 17:08:46 +08:00
										 |  |  |  |   final List<String>? initialMediaPaths; | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |   final ValueChanged<List<File>> onChanged; | 
					
						
							| 
									
										
										
										
											2025-07-30 17:08:46 +08:00
										 |  |  |  |   final ValueChanged<String>? onMediaAdded; | 
					
						
							|  |  |  |  |   final ValueChanged<String>? onMediaRemoved; | 
					
						
							| 
									
										
										
										
											2025-08-14 15:05:48 +08:00
										 |  |  |  |   final ValueChanged<String>? onMediaTapped; // 新增:媒体点击回调
 | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |   final VoidCallback onAiIdentify; | 
					
						
							|  |  |  |  |   final bool isShowAI; | 
					
						
							|  |  |  |  |   final double horizontalPadding; | 
					
						
							| 
									
										
										
										
											2025-08-07 17:33:16 +08:00
										 |  |  |  |   final bool isRequired; | 
					
						
							|  |  |  |  |   final bool isShowNum; | 
					
						
							| 
									
										
										
										
											2025-08-14 15:05:48 +08:00
										 |  |  |  |   final bool isEdit; // 新增:控制编辑状态
 | 
					
						
							| 
									
										
										
										
											2025-08-27 16:14:50 +08:00
										 |  |  |  |   final bool isCamera; // 新增:只能拍照
 | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |   const RepairedPhotoSection({ | 
					
						
							|  |  |  |  |     Key? key, | 
					
						
							|  |  |  |  |     this.maxCount = 4, | 
					
						
							|  |  |  |  |     this.mediaType = MediaType.image, | 
					
						
							|  |  |  |  |     required this.title, | 
					
						
							| 
									
										
										
										
											2025-07-30 17:08:46 +08:00
										 |  |  |  |     this.initialMediaPaths, | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |     this.isShowAI = false, | 
					
						
							|  |  |  |  |     required this.onChanged, | 
					
						
							|  |  |  |  |     required this.onAiIdentify, | 
					
						
							| 
									
										
										
										
											2025-08-11 17:40:03 +08:00
										 |  |  |  |     this.horizontalPadding = 5, | 
					
						
							| 
									
										
										
										
											2025-07-30 17:08:46 +08:00
										 |  |  |  |     this.onMediaAdded, | 
					
						
							|  |  |  |  |     this.onMediaRemoved, | 
					
						
							| 
									
										
										
										
											2025-08-14 15:05:48 +08:00
										 |  |  |  |     this.onMediaTapped, // 新增
 | 
					
						
							| 
									
										
										
										
											2025-08-07 17:33:16 +08:00
										 |  |  |  |     this.isRequired = false, | 
					
						
							|  |  |  |  |     this.isShowNum = true, | 
					
						
							| 
									
										
										
										
											2025-08-14 15:05:48 +08:00
										 |  |  |  |     this.isEdit = true, // 默认可编辑
 | 
					
						
							| 
									
										
										
										
											2025-08-27 16:14:50 +08:00
										 |  |  |  |     this.isCamera = false, | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |   }) : super(key: key); | 
					
						
							| 
									
										
										
										
											2025-08-07 17:33:16 +08:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-31 17:33:26 +08:00
										 |  |  |  |   @override | 
					
						
							|  |  |  |  |   _RepairedPhotoSectionState createState() => _RepairedPhotoSectionState(); | 
					
						
							|  |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | class _RepairedPhotoSectionState extends State<RepairedPhotoSection> { | 
					
						
							|  |  |  |  |   late List<String> _mediaPaths; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   @override | 
					
						
							|  |  |  |  |   void initState() { | 
					
						
							|  |  |  |  |     super.initState(); | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |     _mediaPaths = | 
					
						
							|  |  |  |  |         widget.initialMediaPaths?.take(widget.maxCount).toList() ?? []; | 
					
						
							| 
									
										
										
										
											2025-07-31 17:33:26 +08:00
										 |  |  |  |     WidgetsBinding.instance.addPostFrameCallback((_) { | 
					
						
							| 
									
										
										
										
											2025-08-07 17:33:16 +08:00
										 |  |  |  |       widget.onChanged(_mediaPaths.map((p) => File(p)).toList()); | 
					
						
							| 
									
										
										
										
											2025-07-31 17:33:26 +08:00
										 |  |  |  |     }); | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |   @override | 
					
						
							|  |  |  |  |   Widget build(BuildContext context) { | 
					
						
							|  |  |  |  |     return Container( | 
					
						
							|  |  |  |  |       color: Colors.white, | 
					
						
							| 
									
										
										
										
											2025-07-31 17:33:26 +08:00
										 |  |  |  |       padding: const EdgeInsets.only(left: 0, right: 10), | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |       child: Column( | 
					
						
							| 
									
										
										
										
											2025-07-31 17:33:26 +08:00
										 |  |  |  |         crossAxisAlignment: CrossAxisAlignment.start, | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |         children: [ | 
					
						
							|  |  |  |  |           Padding( | 
					
						
							| 
									
										
										
										
											2025-07-31 17:33:26 +08:00
										 |  |  |  |             padding: EdgeInsets.symmetric(horizontal: widget.horizontalPadding), | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |             child: ListItemFactory.createRowSpaceBetweenItem( | 
					
						
							| 
									
										
										
										
											2025-07-31 17:33:26 +08:00
										 |  |  |  |               leftText: widget.title, | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |               rightText: | 
					
						
							|  |  |  |  |                   widget.isShowNum | 
					
						
							|  |  |  |  |                       ? '${_mediaPaths.length}/${widget.maxCount}' | 
					
						
							|  |  |  |  |                       : '', | 
					
						
							| 
									
										
										
										
											2025-08-07 17:33:16 +08:00
										 |  |  |  |               isRequired: widget.isRequired, | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |             ), | 
					
						
							|  |  |  |  |           ), | 
					
						
							| 
									
										
										
										
											2025-07-31 17:33:26 +08:00
										 |  |  |  |           const SizedBox(height: 8), | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |           Padding( | 
					
						
							| 
									
										
										
										
											2025-07-31 17:33:26 +08:00
										 |  |  |  |             padding: EdgeInsets.symmetric(horizontal: widget.horizontalPadding), | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |             child: MediaPickerRow( | 
					
						
							| 
									
										
										
										
											2025-07-31 17:33:26 +08:00
										 |  |  |  |               maxCount: widget.maxCount, | 
					
						
							|  |  |  |  |               mediaType: widget.mediaType, | 
					
						
							|  |  |  |  |               initialMediaPaths: _mediaPaths, | 
					
						
							| 
									
										
										
										
											2025-08-27 16:14:50 +08:00
										 |  |  |  |               isCamera: widget.isCamera, | 
					
						
							| 
									
										
										
										
											2025-07-31 17:33:26 +08:00
										 |  |  |  |               onChanged: (files) { | 
					
						
							|  |  |  |  |                 final newPaths = files.map((f) => f.path).toList(); | 
					
						
							|  |  |  |  |                 setState(() { | 
					
						
							|  |  |  |  |                   _mediaPaths = newPaths; | 
					
						
							|  |  |  |  |                 }); | 
					
						
							|  |  |  |  |                 widget.onChanged(files); | 
					
						
							|  |  |  |  |               }, | 
					
						
							|  |  |  |  |               onMediaAdded: widget.onMediaAdded, | 
					
						
							|  |  |  |  |               onMediaRemoved: widget.onMediaRemoved, | 
					
						
							| 
									
										
										
										
											2025-09-02 16:22:17 +08:00
										 |  |  |  |               onMediaTapped: (filePath) { | 
					
						
							|  |  |  |  |                 if (widget.mediaType == MediaType.image) { | 
					
						
							|  |  |  |  |                   presentOpaque(SingleImageViewer(imageUrl: filePath), context); | 
					
						
							|  |  |  |  |                 }else{ | 
					
						
							|  |  |  |  |                   showDialog( | 
					
						
							|  |  |  |  |                     context: context, | 
					
						
							|  |  |  |  |                     barrierColor: Colors.black54, | 
					
						
							|  |  |  |  |                     builder: (_) => VideoPlayerPopup(videoUrl:filePath), | 
					
						
							|  |  |  |  |                   ); | 
					
						
							|  |  |  |  |                 } | 
					
						
							|  |  |  |  |               }, | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |               // 传递点击回调
 | 
					
						
							| 
									
										
										
										
											2025-08-14 15:05:48 +08:00
										 |  |  |  |               isEdit: widget.isEdit, // 传递编辑状态
 | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |             ), | 
					
						
							|  |  |  |  |           ), | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |           const SizedBox(height: 8), | 
					
						
							| 
									
										
										
										
											2025-08-14 15:05:48 +08:00
										 |  |  |  |           if (widget.isShowAI && widget.isEdit) // 只在可编辑状态下显示AI按钮
 | 
					
						
							| 
									
										
										
										
											2025-07-31 17:33:26 +08:00
										 |  |  |  |             Padding( | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |               padding: EdgeInsets.symmetric( | 
					
						
							|  |  |  |  |                 horizontal: widget.horizontalPadding, | 
					
						
							|  |  |  |  |               ), | 
					
						
							| 
									
										
										
										
											2025-07-31 17:33:26 +08:00
										 |  |  |  |               child: GestureDetector( | 
					
						
							|  |  |  |  |                 onTap: widget.onAiIdentify, | 
					
						
							|  |  |  |  |                 child: Container( | 
					
						
							|  |  |  |  |                   padding: const EdgeInsets.symmetric(horizontal: 15), | 
					
						
							|  |  |  |  |                   height: 36, | 
					
						
							|  |  |  |  |                   decoration: BoxDecoration( | 
					
						
							|  |  |  |  |                     color: const Color(0xFFDFEAFF), | 
					
						
							|  |  |  |  |                     borderRadius: BorderRadius.circular(18), | 
					
						
							|  |  |  |  |                   ), | 
					
						
							|  |  |  |  |                   child: Row( | 
					
						
							|  |  |  |  |                     mainAxisSize: MainAxisSize.min, | 
					
						
							|  |  |  |  |                     children: [ | 
					
						
							|  |  |  |  |                       Image.asset('assets/images/ai_img.png', width: 20), | 
					
						
							|  |  |  |  |                       const SizedBox(width: 5), | 
					
						
							|  |  |  |  |                       const Text('AI隐患识别与处理'), | 
					
						
							|  |  |  |  |                     ], | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |                   ), | 
					
						
							|  |  |  |  |                 ), | 
					
						
							| 
									
										
										
										
											2025-07-31 17:33:26 +08:00
										 |  |  |  |               ), | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |             ), | 
					
						
							|  |  |  |  |         ], | 
					
						
							|  |  |  |  |       ), | 
					
						
							|  |  |  |  |     ); | 
					
						
							|  |  |  |  |   } | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  | } |