import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:image_picker/image_picker.dart'; import 'package:qhd_prevention/tools/VideoConverter.dart'; import 'package:video_compress/video_compress.dart'; import 'package:wechat_assets_picker/wechat_assets_picker.dart'; import 'package:photo_manager/photo_manager.dart'; import 'package:path/path.dart' as p; import 'ItemWidgetFactory.dart'; /// 媒体选择类型 enum MediaType { image, video } /// 纵向滚动四列的媒体添加组件,支持拍摄、全屏相册多选,以及初始地址列表展示 /// 新增 isEdit 属性控制编辑状态 class MediaPickerRow extends StatefulWidget { final int maxCount; final MediaType mediaType; final List? initialMediaPaths; final ValueChanged> onChanged; final ValueChanged? onMediaAdded; final ValueChanged? onMediaRemoved; final ValueChanged? 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 { final ImagePicker _picker = ImagePicker(); late List _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 _handlePickedPath(String path) async { if (!mounted) return; if (path.isEmpty) return; try { String finalPath = path; // 如果是视频并且不是 mp4,则调用 video_compress 转码 if (widget.mediaType == MediaType.video) { final ext = p.extension(path).toLowerCase(); if (ext != '.mp4') { setState(() => _isProcessing = true); try { final info = await VideoCompress.compressVideo( path, quality: VideoQuality.MediumQuality, deleteOrigin: false, ); if (info != null && info.file != null) { finalPath = info.file!.path; debugPrint('✅ 转换完成: $path -> $finalPath'); } else { throw Exception("转码失败: 返回空文件"); } } catch (e) { debugPrint('❌ 视频转码失败: $e'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('视频转码失败: ${e.toString()}')), ); } return; } finally { if (mounted) setState(() => _isProcessing = false); } } } // 添加到列表 if (_mediaPaths.length < widget.maxCount) { setState(() => _mediaPaths.add(finalPath)); widget.onChanged(_mediaPaths.map((p) => File(p)).toList()); widget.onMediaAdded?.call(finalPath); } } catch (e) { debugPrint('处理选中媒体失败: $e'); } } Future _cameraAction() async { if (!widget.isEdit || _mediaPaths.length >= widget.maxCount) return; try { if (widget.mediaType == MediaType.image) { XFile? picked = await _picker.pickImage(source: ImageSource.camera); if (picked != null) { final path = picked.path; setState(() => _mediaPaths.add(path)); widget.onChanged(_mediaPaths.map((p) => File(p)).toList()); widget.onMediaAdded?.call(path); } } else { // video from camera XFile? picked = await _picker.pickVideo(source: ImageSource.camera); if (picked != null) { await _handlePickedPath(picked.path); } } } catch (e) { debugPrint('拍摄失败: $e'); } } Future _showPickerOptions() async { if (!widget.isEdit) return; // 不可编辑时直接返回 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 _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'); } } 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); } } setState(() {}); widget.onChanged(_mediaPaths.map((p) => File(p)).toList()); } } catch (e) { debugPrint('相册选择失败: $e'); } } void _removeMedia(int index) { if (!widget.isEdit) return; // 不可编辑时不允许删除 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: widget.mediaType == MediaType.image ? (isNetwork ? Image.network(path, fit: BoxFit.cover, width: 80, height: 80) : Image.file(File(path), width: 80, height: 80, fit: BoxFit.cover)) : Container( color: Colors.black12, child: const Center( child: Icon( Icons.videocam, color: Colors.white70, ), ), ), ), // 只在可编辑状态下显示删除按钮 if (widget.isEdit) Positioned( top: -15, right: -15, child: IconButton( icon: const Icon(Icons.cancel, size: 20, color: Colors.red), onPressed: () => _removeMedia(index), ), ), ], ), ); } // 显示添加按钮 else if (showAddButton) { return GestureDetector( onTap: widget.isCamera ? _cameraAction : _showPickerOptions, child: Container( decoration: BoxDecoration( border: Border.all(color: Colors.black12), borderRadius: BorderRadius.circular(5), ), child: const Center( child: Icon(Icons.camera_alt, color: Colors.black26), ), ), ); } 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? initialMediaPaths; final ValueChanged> onChanged; final ValueChanged? onMediaAdded; final ValueChanged? onMediaRemoved; final ValueChanged? 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 { late List _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: widget.onMediaTapped, // 传递点击回调 isEdit: widget.isEdit, // 传递编辑状态 ), ), const SizedBox(height: 20), 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隐患识别与处理'), ], ), ), ), ), ], ), ); } }