628 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			Dart
		
	
	
			
		
		
	
	
			628 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			Dart
		
	
	
| import 'dart:io';
 | ||
| import 'package:device_info_plus/device_info_plus.dart';
 | ||
| import 'package:flutter/material.dart';
 | ||
| import 'package:flutter/services.dart';
 | ||
| import 'package:image_picker/image_picker.dart';
 | ||
| import 'package:permission_handler/permission_handler.dart';
 | ||
| import 'package:qhd_prevention/customWidget/custom_alert_dialog.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/tools/tools.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';
 | ||
| 
 | ||
| /// 媒体选择类型
 | ||
| enum MediaType { image, video }
 | ||
| 
 | ||
| /// 纵向滚动四列的媒体添加组件,支持拍摄、全屏相册多选,以及初始地址列表展示
 | ||
| /// 新增 isEdit 属性控制编辑状态
 | ||
| class MediaPickerRow extends StatefulWidget {
 | ||
|   final int maxCount;
 | ||
|   final MediaType mediaType;
 | ||
|   final List<String>? initialMediaPaths;
 | ||
|   final ValueChanged<List<File>> onChanged;
 | ||
|   final ValueChanged<String>? onMediaAdded;
 | ||
|   final ValueChanged<String>? onMediaRemoved;
 | ||
|   final ValueChanged<String>? onMediaTapped; // 新增:媒体点击回调
 | ||
|   final bool isEdit; // 新增:控制编辑状态
 | ||
|   final bool isCamera; // 新增:只能拍照
 | ||
| 
 | ||
|   const MediaPickerRow({
 | ||
|     Key? key,
 | ||
|     this.maxCount = 4,
 | ||
|     this.mediaType = MediaType.image,
 | ||
|     this.initialMediaPaths,
 | ||
|     required this.onChanged,
 | ||
|     this.onMediaAdded,
 | ||
|     this.onMediaRemoved,
 | ||
|     this.onMediaTapped, // 新增
 | ||
|     this.isEdit = true, // 默认可编辑
 | ||
|     this.isCamera = false,
 | ||
|   }) : super(key: key);
 | ||
| 
 | ||
|   @override
 | ||
|   _MediaPickerGridState createState() => _MediaPickerGridState();
 | ||
| }
 | ||
| 
 | ||
| class _MediaPickerGridState extends State<MediaPickerRow> {
 | ||
|   final ImagePicker _picker = ImagePicker();
 | ||
|   late List<String> _mediaPaths;
 | ||
|   bool _isProcessing = false; // 转码或处理时显示 loading
 | ||
| 
 | ||
|   @override
 | ||
|   void initState() {
 | ||
|     super.initState();
 | ||
|     _mediaPaths =
 | ||
|         widget.initialMediaPaths != null
 | ||
|             ? widget.initialMediaPaths!.take(widget.maxCount).toList()
 | ||
|             : [];
 | ||
|     WidgetsBinding.instance.addPostFrameCallback((_) {
 | ||
|       widget.onChanged(
 | ||
|         _mediaPaths
 | ||
|             .map((p) => p.startsWith('http') ? File('') : File(p))
 | ||
|             .toList(),
 | ||
|       );
 | ||
|     });
 | ||
|   }
 | ||
| 
 | ||
|   // 公共:当得到本地媒体路径时(可能是 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) {
 | ||
|               ToastUtil.showNormal(context, '视频转码失败');
 | ||
|             }
 | ||
|             return;
 | ||
|           } finally {
 | ||
|             if (mounted) setState(() => _isProcessing = false);
 | ||
|           }
 | ||
|         }
 | ||
|       }
 | ||
| 
 | ||
|       // 添加到列表
 | ||
|       if (_mediaPaths.length < widget.maxCount) {
 | ||
|         setState(() => _mediaPaths.add(finalPath));
 | ||
|         widget.onChanged(_mediaPaths.map((p) => File(p)).toList());
 | ||
|         widget.onMediaAdded?.call(finalPath);
 | ||
|       }
 | ||
|     } catch (e) {
 | ||
|       debugPrint('处理选中媒体失败: $e');
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   Future<void> _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();
 | ||
|                   },
 | ||
|                 ),
 | ||
|                 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(),
 | ||
|                 ),
 | ||
|               ],
 | ||
|             ),
 | ||
|           ),
 | ||
|     );
 | ||
|   }
 | ||
| 
 | ||
|   Future<void> _pickCamera() async {
 | ||
|     if (!widget.isEdit || _mediaPaths.length >= widget.maxCount) return;
 | ||
| 
 | ||
|     try {
 | ||
|       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) {
 | ||
|           await _handlePickedPath(picked.path);
 | ||
|         }
 | ||
|       }
 | ||
|     } catch (e) {
 | ||
|       debugPrint('拍摄失败: $e');
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   // 修改 _pickGallery 方法
 | ||
|   Future<void> _pickGallery() async {
 | ||
|     if (!widget.isEdit || _mediaPaths.length >= widget.maxCount) return;
 | ||
| 
 | ||
|     try {
 | ||
|       // iOS: 使用 PhotoManager.requestPermissionExtend() 唤起系统权限弹窗(支持 limited)
 | ||
|       if (Platform.isIOS) {
 | ||
|         final permission = await PhotoManager.requestPermissionExtend();
 | ||
|         debugPrint('iOS photo permission state: $permission');
 | ||
| 
 | ||
|         if (permission != PermissionState.authorized &&
 | ||
|             permission != PermissionState.limited) {
 | ||
|           if (mounted) {
 | ||
|             ToastUtil.showNormal(context, '请到设置中开启相册访问权限');
 | ||
|           }
 | ||
|           return;
 | ||
|         }
 | ||
| 
 | ||
|         // 授权或 limited:继续打开 AssetPicker
 | ||
|         final remaining = widget.maxCount - _mediaPaths.length;
 | ||
|         final List<AssetEntity>? assets = await AssetPicker.pickAssets(
 | ||
|           context,
 | ||
|           pickerConfig: AssetPickerConfig(
 | ||
|             requestType:
 | ||
|             widget.mediaType == MediaType.image ? RequestType.image : RequestType.video,
 | ||
|             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: 更可靠地请求权限(适配不同 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();
 | ||
|         } else {
 | ||
|           // Android 10 及以下
 | ||
|           permissionStatus = await Permission.storage.request();
 | ||
|         }
 | ||
| 
 | ||
|         // 处理结果
 | ||
|         if (permissionStatus.isGranted) {
 | ||
|           // 有权限:继续打开 AssetPicker
 | ||
|           final remaining = widget.maxCount - _mediaPaths.length;
 | ||
|           final List<AssetEntity>? assets = await AssetPicker.pickAssets(
 | ||
|             context,
 | ||
|             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 (permissionStatus.isPermanentlyDenied) {
 | ||
|           // 用户点了“不再询问”或系统拒绝弹窗,跳转设置页并提示
 | ||
|           if (mounted) {
 | ||
|             ToastUtil.showNormal(context, '请到设置中开启相册访问权限');
 | ||
|           }
 | ||
|           await openAppSettings();
 | ||
|           return;
 | ||
|         } else {
 | ||
|           if (mounted) {
 | ||
|             ToastUtil.showNormal(context, '相册访问权限被拒绝');
 | ||
|           }
 | ||
|           return;
 | ||
|         }
 | ||
|       }
 | ||
|     } 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('相机权限被拒绝')));
 | ||
|       }
 | ||
|       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
 | ||
|   Widget build(BuildContext context) {
 | ||
|     final showAddButton = widget.isEdit && _mediaPaths.length < widget.maxCount;
 | ||
|     final itemCount = _mediaPaths.length + (showAddButton ? 1 : 0);
 | ||
| 
 | ||
|     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: 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,
 | ||
|                           ),
 | ||
|                           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),
 | ||
|                   ),
 | ||
|                 ),
 | ||
|               );
 | ||
|             } else {
 | ||
|               return const SizedBox.shrink();
 | ||
|             }
 | ||
|           },
 | ||
|         ),
 | ||
| 
 | ||
|         // 转码/处理 loading 遮罩
 | ||
|         if (_isProcessing)
 | ||
|           Positioned.fill(
 | ||
|             child: Container(
 | ||
|               color: Colors.transparent,
 | ||
|               child: const Center(child: CircularProgressIndicator()),
 | ||
|             ),
 | ||
|           ),
 | ||
|       ],
 | ||
|     );
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| /// 照片上传区域组件,使用纵向四列Grid展示
 | ||
| /// 新增 isEdit 属性控制编辑状态
 | ||
| class RepairedPhotoSection extends StatefulWidget {
 | ||
|   final int maxCount;
 | ||
|   final MediaType mediaType;
 | ||
|   final String title;
 | ||
|   final List<String>? initialMediaPaths;
 | ||
|   final ValueChanged<List<File>> onChanged;
 | ||
|   final ValueChanged<String>? onMediaAdded;
 | ||
|   final ValueChanged<String>? onMediaRemoved;
 | ||
|   final ValueChanged<String>? onMediaTapped; // 新增:媒体点击回调
 | ||
|   final VoidCallback onAiIdentify;
 | ||
|   final bool isShowAI;
 | ||
|   final double horizontalPadding;
 | ||
|   final bool isRequired;
 | ||
|   final bool isShowNum;
 | ||
|   final bool isEdit; // 新增:控制编辑状态
 | ||
|   final bool isCamera; // 新增:只能拍照
 | ||
| 
 | ||
|   const RepairedPhotoSection({
 | ||
|     Key? key,
 | ||
|     this.maxCount = 4,
 | ||
|     this.mediaType = MediaType.image,
 | ||
|     required this.title,
 | ||
|     this.initialMediaPaths,
 | ||
|     this.isShowAI = false,
 | ||
|     required this.onChanged,
 | ||
|     required this.onAiIdentify,
 | ||
|     this.horizontalPadding = 5,
 | ||
|     this.onMediaAdded,
 | ||
|     this.onMediaRemoved,
 | ||
|     this.onMediaTapped, // 新增
 | ||
|     this.isRequired = false,
 | ||
|     this.isShowNum = true,
 | ||
|     this.isEdit = true, // 默认可编辑
 | ||
|     this.isCamera = false,
 | ||
|   }) : super(key: key);
 | ||
| 
 | ||
|   @override
 | ||
|   _RepairedPhotoSectionState createState() => _RepairedPhotoSectionState();
 | ||
| }
 | ||
| 
 | ||
| class _RepairedPhotoSectionState extends State<RepairedPhotoSection> {
 | ||
|   late List<String> _mediaPaths;
 | ||
| 
 | ||
|   @override
 | ||
|   void initState() {
 | ||
|     super.initState();
 | ||
|     _mediaPaths =
 | ||
|         widget.initialMediaPaths?.take(widget.maxCount).toList() ?? [];
 | ||
|     WidgetsBinding.instance.addPostFrameCallback((_) {
 | ||
|       widget.onChanged(_mediaPaths.map((p) => File(p)).toList());
 | ||
|     });
 | ||
|   }
 | ||
| 
 | ||
|   @override
 | ||
|   Widget build(BuildContext context) {
 | ||
|     return Container(
 | ||
|       color: Colors.white,
 | ||
|       padding: const EdgeInsets.only(left: 0, right: 10),
 | ||
|       child: Column(
 | ||
|         crossAxisAlignment: CrossAxisAlignment.start,
 | ||
|         children: [
 | ||
|           Padding(
 | ||
|             padding: EdgeInsets.symmetric(horizontal: widget.horizontalPadding),
 | ||
|             child: ListItemFactory.createRowSpaceBetweenItem(
 | ||
|               leftText: widget.title,
 | ||
|               rightText:
 | ||
|                   widget.isShowNum
 | ||
|                       ? '${_mediaPaths.length}/${widget.maxCount}'
 | ||
|                       : '',
 | ||
|               isRequired: widget.isRequired,
 | ||
|             ),
 | ||
|           ),
 | ||
|           const SizedBox(height: 8),
 | ||
|           Padding(
 | ||
|             padding: EdgeInsets.symmetric(horizontal: widget.horizontalPadding),
 | ||
|             child: MediaPickerRow(
 | ||
|               maxCount: widget.maxCount,
 | ||
|               mediaType: widget.mediaType,
 | ||
|               initialMediaPaths: _mediaPaths,
 | ||
|               isCamera: widget.isCamera,
 | ||
|               onChanged: (files) {
 | ||
|                 final newPaths = files.map((f) => f.path).toList();
 | ||
|                 setState(() {
 | ||
|                   _mediaPaths = newPaths;
 | ||
|                 });
 | ||
|                 widget.onChanged(files);
 | ||
|               },
 | ||
|               onMediaAdded: widget.onMediaAdded,
 | ||
|               onMediaRemoved: widget.onMediaRemoved,
 | ||
|               onMediaTapped: (filePath) {
 | ||
|                 if (widget.mediaType == MediaType.image) {
 | ||
|                   presentOpaque(SingleImageViewer(imageUrl: filePath), context);
 | ||
|                 }else{
 | ||
|                   showDialog(
 | ||
|                     context: context,
 | ||
|                     barrierColor: Colors.black54,
 | ||
|                     builder: (_) => VideoPlayerPopup(videoUrl:filePath),
 | ||
|                   );
 | ||
|                 }
 | ||
|               },
 | ||
|               // 传递点击回调
 | ||
|               isEdit: widget.isEdit, // 传递编辑状态
 | ||
|             ),
 | ||
|           ),
 | ||
|           const SizedBox(height: 8),
 | ||
|           if (widget.isShowAI && widget.isEdit) // 只在可编辑状态下显示AI按钮
 | ||
|             Padding(
 | ||
|               padding: EdgeInsets.symmetric(
 | ||
|                 horizontal: widget.horizontalPadding,
 | ||
|               ),
 | ||
|               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隐患识别与处理'),
 | ||
|                     ],
 | ||
|                   ),
 | ||
|                 ),
 | ||
|               ),
 | ||
|             ),
 | ||
|         ],
 | ||
|       ),
 | ||
|     );
 | ||
|   }
 | ||
| }
 |