flutter_integrated_whb/lib/customWidget/photo_picker_row.dart

464 lines
16 KiB
Dart
Raw Normal View History

2025-07-11 11:03:21 +08:00
import 'dart:io';
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-08-29 09:52:48 +08:00
import 'package:qhd_prevention/tools/VideoConverter.dart';
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();
_mediaPaths = widget.initialMediaPaths != null
? widget.initialMediaPaths!.take(widget.maxCount).toList()
: [];
WidgetsBinding.instance.addPostFrameCallback((_) {
2025-08-07 17:33:16 +08:00
widget.onChanged(
2025-08-19 11:06:16 +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) {
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');
}
}
2025-08-27 16:14:50 +08:00
Future<void> _cameraAction() async {
2025-08-29 09:52:48 +08:00
if (!widget.isEdit || _mediaPaths.length >= widget.maxCount) return;
2025-07-11 11:03:21 +08:00
2025-08-29 09:52:48 +08:00
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');
2025-08-27 16:14:50 +08:00
}
}
2025-08-29 09:52:48 +08:00
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-07-30 17:08:46 +08:00
builder: (_) => SafeArea(
child: Wrap(
children: [
ListTile(
2025-08-11 17:40:03 +08:00
titleAlignment: ListTileTitleAlignment.center,
2025-07-30 17:08:46 +08:00
leading: Icon(
2025-08-29 09:52:48 +08:00
widget.mediaType == MediaType.image ? Icons.camera_alt : Icons.videocam,
2025-07-30 17:08:46 +08:00
),
2025-08-29 09:52:48 +08:00
title: Text(widget.mediaType == MediaType.image ? '拍照' : '拍摄视频'),
2025-07-30 17:08:46 +08:00
onTap: () {
Navigator.of(context).pop();
_pickCamera();
},
2025-07-11 11:03:21 +08:00
),
2025-07-30 17:08:46 +08:00
ListTile(
2025-08-11 17:40:03 +08:00
titleAlignment: ListTileTitleAlignment.center,
2025-07-30 17:08:46 +08:00
leading: Icon(
2025-08-29 09:52:48 +08:00
widget.mediaType == MediaType.image ? Icons.photo_library : Icons.video_library,
2025-07-30 17:08:46 +08:00
),
2025-08-29 09:52:48 +08:00
title: Text(widget.mediaType == MediaType.image ? '从相册选择' : '从相册选择视频'),
2025-07-30 17:08:46 +08:00
onTap: () {
Navigator.of(context).pop();
_pickGallery();
},
),
ListTile(
2025-08-11 17:40:03 +08:00
titleAlignment: ListTileTitleAlignment.center,
2025-07-30 17:08:46 +08:00
leading: const Icon(Icons.close),
title: const Text('取消'),
onTap: () => Navigator.of(context).pop(),
),
],
),
),
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
}
}
Future<void> _pickGallery() 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
final permission = await PhotoManager.requestPermissionExtend();
if (permission != PermissionState.authorized &&
permission != PermissionState.limited) {
2025-07-30 17:08:46 +08:00
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('请到设置中开启相册访问权限')),
);
2025-07-11 11:03:21 +08:00
return;
}
try {
2025-07-30 17:08:46 +08:00
final remaining = widget.maxCount - _mediaPaths.length;
2025-07-11 11:03:21 +08:00
final List<AssetEntity>? assets = await AssetPicker.pickAssets(
context,
pickerConfig: AssetPickerConfig(
2025-08-29 09:52:48 +08:00
requestType: widget.mediaType == MediaType.image ? RequestType.image : RequestType.video,
2025-07-11 11:03:21 +08:00
maxAssets: remaining,
gridCount: 4,
),
);
if (assets != null) {
for (final asset in assets) {
2025-07-30 17:08:46 +08:00
if (_mediaPaths.length >= widget.maxCount) break;
2025-07-11 11:03:21 +08:00
final file = await asset.file;
if (file != null) {
2025-07-30 17:08:46 +08:00
final path = file.path;
2025-08-29 09:52:48 +08:00
// 交给统一处理(会转码视频)
await _handlePickedPath(path);
2025-07-11 11:03:21 +08:00
}
}
setState(() {});
2025-07-30 17:08:46 +08:00
widget.onChanged(_mediaPaths.map((p) => File(p)).toList());
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-07-30 17:08:46 +08:00
void _removeMedia(int index) {
2025-08-14 15:05:48 +08:00
if (!widget.isEdit) return; // 不可编辑时不允许删除
2025-07-30 17:08:46 +08:00
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),
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,
),
),
2025-08-14 15:05:48 +08:00
),
2025-07-30 17:08:46 +08:00
),
2025-08-29 09:52:48 +08:00
// 只在可编辑状态下显示删除按钮
if (widget.isEdit)
Positioned(
top: -15,
right: -15,
child: IconButton(
icon: const Icon(Icons.cancel, size: 20, color: Colors.red),
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-08-07 17:33:16 +08:00
child: const Center(
2025-08-29 09:52:48 +08:00
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();
_mediaPaths = widget.initialMediaPaths?.take(widget.maxCount).toList() ?? [];
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-08-07 17:33:16 +08:00
rightText: widget.isShowNum ? '${_mediaPaths.length}/${widget.maxCount}' : '',
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-08-14 15:05:48 +08:00
onMediaTapped: widget.onMediaTapped, // 传递点击回调
isEdit: widget.isEdit, // 传递编辑状态
2025-07-11 11:03:21 +08:00
),
),
const SizedBox(height: 20),
2025-08-14 15:05:48 +08:00
if (widget.isShowAI && widget.isEdit) // 只在可编辑状态下显示AI按钮
2025-07-31 17:33:26 +08:00
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隐患识别与处理'),
],
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
}