import 'dart:io'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'package:wechat_assets_picker/wechat_assets_picker.dart'; import 'package:photo_manager/photo_manager.dart'; /// 媒体选择类型 enum MediaType { image, video } /// 横向最多四个、可自动换行的媒体添加组件,支持拍摄和相册多选 class MediaPickerRow extends StatefulWidget { final int maxCount; final MediaType mediaType; final ValueChanged> onChanged; const MediaPickerRow({ Key? key, this.maxCount = 4, this.mediaType = MediaType.image, required this.onChanged, }) : super(key: key); @override _MediaPickerRowState createState() => _MediaPickerRowState(); } class _MediaPickerRowState extends State { final ImagePicker _picker = ImagePicker(); final List _files = []; Future _showPickerOptions() async { showModalBottomSheet( context: context, builder: (_) => SafeArea( child: Wrap( children: [ ListTile( leading: Icon( widget.mediaType == MediaType.image ? Icons.camera_alt : Icons.videocam, ), title: Text( widget.mediaType == MediaType.image ? '拍照' : '拍摄视频', ), onTap: () { Navigator.of(context).pop(); _pickCamera(); }, ), ListTile( 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( leading: const Icon(Icons.close), title: const Text('取消'), onTap: () => Navigator.of(context).pop(), ), ], ), ), ); } Future _pickCamera() async { if (_files.length >= widget.maxCount) return; try { XFile? picked = widget.mediaType == MediaType.image ? await _picker.pickImage(source: ImageSource.camera) : await _picker.pickVideo(source: ImageSource.camera); if (picked != null) { setState(() => _files.add(File(picked.path))); widget.onChanged(_files); } } catch (e) { debugPrint('拍摄失败: $e'); } } Future _pickGallery() async { if (_files.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 - _files.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 (_files.length >= widget.maxCount) break; final file = await asset.file; if (file != null) _files.add(file); } setState(() {}); widget.onChanged(_files); } } catch (e) { debugPrint('相册选择失败: $e'); } } void _removeFile(int index) { setState(() { _files.removeAt(index); }); widget.onChanged(_files); } @override Widget build(BuildContext context) { // 准备所有已选媒体和“添加”按钮 final children = [ for (int i = 0; i < _files.length; i++) _buildMediaItem(i), if (_files.length < widget.maxCount) _buildAddButton(), ]; return Padding( padding: const EdgeInsets.symmetric(vertical: 10), child: Wrap( spacing: 8, // 横向间距 runSpacing: 8, // 纵向行距 children: children, ), ); } Widget _buildMediaItem(int index) { return Stack( clipBehavior: Clip.none, // 允许子项溢出到父级之外 children: [ ClipRRect( borderRadius: BorderRadius.circular(5), child: widget.mediaType == MediaType.image ? Image.file( _files[index], width: 80, height: 80, fit: BoxFit.cover, ) : Container( width: 80, height: 80, color: Colors.black12, child: const Center( child: Icon(Icons.videocam, color: Colors.white70), ), ), ), Positioned( top: 0, // 负值让图标一半溢出 right: 0, child: GestureDetector( onTap: () => _removeFile(index), child: Image.asset( "assets/images/close.png", width: 24, height: 24, ), ), ), ], ); } Widget _buildAddButton() { return GestureDetector( onTap: _showPickerOptions, child: Container( width: 80, height: 80, decoration: BoxDecoration( border: Border.all(color: Colors.black12), borderRadius: BorderRadius.circular(5), ), child: Center( child: Icon( widget.mediaType == MediaType.image ? Icons.camera_alt : Icons.videocam, color: Colors.black26, ), ), ), ); } } /// 整改后照片上传区域组件 class RepairedPhotoSection extends StatelessWidget { final int maxCount; final MediaType mediaType; final String title; final ValueChanged> onChanged; final VoidCallback onAiIdentify; final bool isShowAI; final double horizontalPadding; const RepairedPhotoSection({ Key? key, this.maxCount = 4, this.mediaType = MediaType.image, required this.title, this.isShowAI = false, required this.onChanged, required this.onAiIdentify, this.horizontalPadding = 10, }) : super(key: key); @override Widget build(BuildContext context) { return Container( color: Colors.white, padding: const EdgeInsets.only(left: 5, right: 10), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 标题 Padding( padding: EdgeInsets.symmetric(horizontal: horizontalPadding), child: Text( title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), ), const SizedBox(height: 8), // 媒体行 MediaPickerRow( maxCount: maxCount, mediaType: mediaType, onChanged: onChanged, ), if (isShowAI) Padding( padding: const EdgeInsets.only(top: 20), child: Stack( children: [ GestureDetector( onTap: onAiIdentify, child: Container( width: double.infinity, // 让它撑满宽度,以便定位 height: 36, decoration: BoxDecoration( color: const Color(0xFFDFEAFF), borderRadius: BorderRadius.circular(18), ), alignment: Alignment.centerLeft, padding: const EdgeInsets.symmetric(horizontal: 15), child: const Text('AI隐患识别与处理'), ), ), // 这个 Positioned 会把图紧贴右上角 Positioned( top: 0, right: 0, child: Image.asset( 'assets/images/ai_img.png', width: 20, height: 20, ), ), ], ), ), ], ), ); } }