flutter_integrated_whb/lib/customWidget/photo_picker_row.dart

361 lines
11 KiB
Dart
Raw Normal View History

2025-07-11 11:03:21 +08:00
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';
import 'ItemWidgetFactory.dart';
/// 媒体选择类型
enum MediaType { image, video }
2025-07-30 17:08:46 +08:00
/// 横向一行最多四个媒体的添加组件,支持拍摄、全屏相册多选,以及初始地址列表展示
2025-07-11 11:03:21 +08:00
/// 使用示例:
/// MediaPickerRow(
/// maxCount: 4,
/// mediaType: MediaType.video,
2025-07-30 17:08:46 +08:00
/// initialMediaPaths: ['https://...', '/local/path.png'],
/// onChanged: (List<File> medias) {},
/// onMediaAdded: (String path) {},
/// onMediaRemoved: (String path) {},
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-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-07-11 11:03:21 +08:00
}) : super(key: key);
@override
_MediaPickerRowState createState() => _MediaPickerRowState();
}
class _MediaPickerRowState extends State<MediaPickerRow> {
final ImagePicker _picker = ImagePicker();
2025-07-30 17:08:46 +08:00
late List<String> _mediaPaths;
@override
void initState() {
super.initState();
_mediaPaths = widget.initialMediaPaths != null
? widget.initialMediaPaths!.take(widget.maxCount).toList()
: [];
WidgetsBinding.instance.addPostFrameCallback((_) {
// 初始回调,转换为 File 列表
widget.onChanged(_mediaPaths.map((p) => p.startsWith('http') ? File('') : File(p)).toList());
});
}
2025-07-11 11:03:21 +08:00
Future<void> _showPickerOptions() async {
showModalBottomSheet(
context: context,
2025-07-30 17:08:46 +08:00
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();
},
2025-07-11 11:03:21 +08:00
),
2025-07-30 17:08:46 +08:00
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(),
),
],
),
),
2025-07-11 11:03:21 +08:00
);
}
Future<void> _pickCamera() async {
2025-07-30 17:08:46 +08:00
if (_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);
} else {
picked = await _picker.pickVideo(source: ImageSource.camera);
}
if (picked != null) {
2025-07-30 17:08:46 +08:00
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
}
} 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-07-30 17:08:46 +08:00
if (_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-07-30 17:08:46 +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;
_mediaPaths.add(path);
widget.onMediaAdded?.call(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) {
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) {
return SizedBox(
height: 80,
child: ListView.separated(
scrollDirection: Axis.horizontal,
2025-07-30 17:08:46 +08:00
itemCount: _mediaPaths.length < widget.maxCount
? _mediaPaths.length + 1
: widget.maxCount,
2025-07-11 11:03:21 +08:00
separatorBuilder: (_, __) => const SizedBox(width: 8),
itemBuilder: (context, index) {
2025-07-30 17:08:46 +08:00
if (index < _mediaPaths.length) {
final path = _mediaPaths[index];
final isNetwork = path.startsWith('http');
2025-07-11 11:03:21 +08:00
return Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(5),
2025-07-30 17:08:46 +08:00
child: widget.mediaType == MediaType.image
? (isNetwork
? Image.network(path,
width: 80, height: 80, fit: BoxFit.cover)
: Image.file(File(path),
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,
),
),
),
2025-07-11 11:03:21 +08:00
),
Positioned(
2025-07-30 17:08:46 +08:00
top: -15,
right: -15,
2025-07-11 11:03:21 +08:00
child: IconButton(
icon: const Icon(Icons.cancel, size: 20, color: Colors.red),
2025-07-30 17:08:46 +08:00
onPressed: () => _removeMedia(index),
2025-07-11 11:03:21 +08:00
),
),
],
);
} else {
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,
),
),
),
);
}
},
),
);
}
}
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-07-11 11:03:21 +08:00
final VoidCallback onAiIdentify;
final bool isShowAI;
final double horizontalPadding;
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,
this.horizontalPadding = 10,
2025-07-30 17:08:46 +08:00
this.onMediaAdded,
this.onMediaRemoved,
2025-07-11 11:03:21 +08:00
}) : super(key: key);
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((_) {
widget.onChanged(
_mediaPaths.map((p) => File(p)).toList(),
);
});
}
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,
// 动态展示已选数量
rightText: '${_mediaPaths.length}/${widget.maxCount}',
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,
// 统一在 onChanged 中维护 mediaPaths避免重复添加
onChanged: (files) {
final newPaths = files.map((f) => f.path).toList();
setState(() {
_mediaPaths = newPaths;
});
widget.onChanged(files);
},
// 仅回调外部,不再修改本地 _mediaPaths
onMediaAdded: widget.onMediaAdded,
onMediaRemoved: widget.onMediaRemoved,
2025-07-11 11:03:21 +08:00
),
),
const SizedBox(height: 20),
2025-07-31 17:33:26 +08:00
if (widget.isShowAI)
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-07-31 21:15:00 +08:00