flutter_integrated_whb/lib/customWidget/photo_picker_row.dart

410 lines
14 KiB
Dart
Raw Normal View History

2025-07-11 11:03:21 +08:00
import 'dart:io';
import 'package:flutter/foundation.dart';
2025-07-11 11:03:21 +08:00
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-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-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-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;
@override
void initState() {
super.initState();
_mediaPaths = widget.initialMediaPaths != null
? widget.initialMediaPaths!.take(widget.maxCount).toList()
: [];
WidgetsBinding.instance.addPostFrameCallback((_) {
// 改动点:不要把网络地址换成 File(''),保持 File(path)path 可能是本地也可能是 http
2025-08-07 17:33:16 +08:00
widget.onChanged(
_mediaPaths.map((p) => File(p)).toList(),
2025-08-07 17:33:16 +08:00
);
2025-07-30 17:08:46 +08:00
});
}
2025-07-11 11:03:21 +08:00
@override
void didUpdateWidget(covariant MediaPickerRow oldWidget) {
super.didUpdateWidget(oldWidget);
// 当父组件传入的 initialMediaPaths 变化时,更新内部 _mediaPaths 并触发 onChanged
if (!listEquals(oldWidget.initialMediaPaths, widget.initialMediaPaths)) {
_mediaPaths = widget.initialMediaPaths != null
? widget.initialMediaPaths!.take(widget.maxCount).toList()
: [];
setState(() {}); // 触发 rebuild
WidgetsBinding.instance.addPostFrameCallback((_) {
// 改动点:同上,保持 File(path)
widget.onChanged(
_mediaPaths.map((p) => File(p)).toList(),
);
});
}
}
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(
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(
2025-08-11 17:40:03 +08:00
titleAlignment: ListTileTitleAlignment.center,
2025-07-30 17:08:46 +08:00
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(
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);
} 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));
// 这里本来就是 File(p)
2025-07-30 17:08:46 +08:00
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-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-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) {
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-07 17:33:16 +08:00
return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
childAspectRatio: 1,
2025-08-14 15:05:48 +08:00
// mainAxisExtent: 80,
2025-08-07 17:33:16 +08:00
),
2025-08-14 15:05:48 +08:00
itemCount: itemCount,
2025-08-07 17:33:16 +08:00
itemBuilder: (context, index) {
2025-08-14 15:05:48 +08:00
// 显示媒体项
2025-08-07 17:33:16 +08:00
if (index < _mediaPaths.length) {
final path = _mediaPaths[index];
final isNetwork = path.startsWith('http');
2025-08-14 15:05: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-07-30 17:08:46 +08:00
),
),
2025-07-11 11:03:21 +08:00
),
2025-08-14 15:05: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-08-07 17:33:16 +08:00
);
2025-08-14 15:05:48 +08:00
}
// 显示添加按钮
else if (showAddButton) {
2025-08-07 17:33:16 +08:00
return GestureDetector(
onTap: _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),
),
),
);
2025-08-14 15:05:48 +08:00
} else {
return const SizedBox.shrink();
2025-08-07 17:33:16 +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-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-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
});
}
@override
void didUpdateWidget(covariant RepairedPhotoSection oldWidget) {
super.didUpdateWidget(oldWidget);
// 父组件传入 initialMediaPaths 变更时,同步内部 _mediaPaths 并触发 onChanged
if (!listEquals(oldWidget.initialMediaPaths, widget.initialMediaPaths)) {
_mediaPaths = widget.initialMediaPaths?.take(widget.maxCount).toList() ?? [];
setState(() {});
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,
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,
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
),
],
),
);
}
}