QinGang_interested/lib/customWidget/photo_picker_row.dart

993 lines
37 KiB
Dart
Raw Permalink Normal View History

2025-12-12 09:11:30 +08:00
// <your_file_name>.dart
import 'dart:async';
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:image_picker/image_picker.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:qhd_prevention/customWidget/custom_alert_dialog.dart';
import 'package:qhd_prevention/customWidget/full_screen_video_page.dart';
import 'package:qhd_prevention/customWidget/single_image_viewer.dart';
import 'package:qhd_prevention/customWidget/toast_util.dart';
import 'package:qhd_prevention/pages/mine/face_ecognition_page.dart';
import 'package:qhd_prevention/tools/tools.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';
const String kAcceptVideoSectionKey = 'accept_video';
/// ---------------------- 全局 MediaBus (轻量事件总线) ----------------------
class MediaEvent {
final String key;
final MediaEventType type;
final List<String>? paths;
MediaEvent._(this.key, this.type, [this.paths]);
factory MediaEvent.clear(String key) => MediaEvent._(key, MediaEventType.clear);
factory MediaEvent.set(String key, List<String> paths) =>
MediaEvent._(key, MediaEventType.set, List<String>.from(paths));
}
enum MediaEventType { clear, set }
class MediaBus {
MediaBus._internal();
static final MediaBus _instance = MediaBus._internal();
factory MediaBus() => _instance;
final StreamController<MediaEvent> _ctrl = StreamController<MediaEvent>.broadcast();
Stream<MediaEvent> get stream => _ctrl.stream;
void emit(MediaEvent ev) {
if (!_ctrl.isClosed) _ctrl.add(ev);
}
Future<void> dispose() async {
await _ctrl.close();
}
}
/// ---------------------- /MediaBus ----------------------
/// 媒体选择类型
enum MediaType { image, video }
/// ---------- 辅助函数(文件顶部复用) ----------
bool _isNetworkPath(String? p) {
if (p == null) return false;
final s = p.trim().toLowerCase();
return s.startsWith('http://') || s.startsWith('https://');
}
/// 把路径列表转换为存在的本地 File 列表(过滤掉网络路径与空路径与不存在的本地文件)
/// 注意:这个函数**可能**会同步访问文件系统existsSync它只应在用户触发后调用不应在 build() 中被频繁调用。
List<File> _localFilesFromPaths(List<String>? paths) {
if (paths == null) return <File>[];
return paths
.map((e) => (e ?? '').toString().trim())
.where((s) => s.isNotEmpty && !_isNetworkPath(s))
.where((s) => File(s).existsSync())
.map((s) => File(s))
.toList();
}
/// 规范化路径列表trim + 过滤空字符串
List<String> _normalizePaths(List<String>? src) {
if (src == null) return <String>[];
return src.map((e) => (e ?? '').toString().trim()).where((s) => s.isNotEmpty).toList();
}
/// ---------- MediaPickerRow ----------
// ------------------ 修改后的 MediaPickerRow完整类 ------------------
class MediaPickerRow extends StatefulWidget {
final int maxCount;
final MediaType mediaType;
final List<String>? initialMediaPaths;
final ValueChanged<List<File>> onChanged;
final ValueChanged<String>? onMediaAdded;
final ValueChanged<String>? onMediaRemoved;
final ValueChanged<int>? onMediaRemovedForIndex;
final ValueChanged<String>? onMediaTapped;
final bool isEdit;
final bool isCamera;
/// 默认 false —— 仅在 initState 时读取 initialMediaPaths
final bool followInitialUpdates;
/// 新增:网格列数(默认 4可在需要单列/自适应宽度时指定 1
final int crossAxisCount;
const MediaPickerRow({
Key? key,
this.maxCount = 4,
this.mediaType = MediaType.image,
this.initialMediaPaths,
required this.onChanged,
this.onMediaAdded,
this.onMediaRemoved,
this.onMediaRemovedForIndex,
this.onMediaTapped,
this.isEdit = true,
this.isCamera = false,
this.followInitialUpdates = false, // 默认 false
this.crossAxisCount = 4, // 默认 4 列
}) : super(key: key);
@override
_MediaPickerGridState createState() => _MediaPickerGridState();
}
class _MediaPickerGridState extends State<MediaPickerRow> {
final ImagePicker _picker = ImagePicker();
late List<String> _mediaPaths;
bool _isProcessing = false;
/// 缓存每个本地路径是否存在(避免在 build 中反复同步 IO
final Map<String, bool> _localExistsCache = {};
@override
void initState() {
super.initState();
// 初始化内部路径(保留网络路径与本地路径)
_mediaPaths = _normalizePaths(widget.initialMediaPaths).take(widget.maxCount).toList();
// 预先检查一次本地文件是否存在(只在 init 时做一次同步检查)
for (final pth in _mediaPaths) {
final t = pth.trim();
if (!_isNetworkPath(t)) {
try {
_localExistsCache[t] = File(t).existsSync();
} catch (_) {
_localExistsCache[t] = false;
}
} else {
_localExistsCache[pth] = false;
}
}
// 仅在存在本地真实文件时才把 File 列表回调给外部(避免父组件用这个回调覆盖只有网络路径的数据)
final initialLocalFiles = _localFilesFromPaths(_mediaPaths);
if (initialLocalFiles.isNotEmpty) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
widget.onChanged(initialLocalFiles);
});
}
}
@override
void didUpdateWidget(covariant MediaPickerRow oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.followInitialUpdates) {
final oldList = _normalizePaths(oldWidget.initialMediaPaths);
final newList = _normalizePaths(widget.initialMediaPaths);
if (!listEquals(oldList, newList)) {
_mediaPaths = newList.take(widget.maxCount).toList();
// 更新本地存在缓存(同步检查,仅在更新时执行)
for (final pth in _mediaPaths) {
final t = pth.trim();
if (!_localExistsCache.containsKey(t)) {
if (!_isNetworkPath(t)) {
try {
_localExistsCache[t] = File(t).existsSync();
} catch (_) {
_localExistsCache[t] = false;
}
} else {
_localExistsCache[t] = false;
}
}
}
if (mounted) setState(() {});
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.onChanged(_localFilesFromPaths(_mediaPaths));
});
}
}
}
Future<void> _handlePickedPath(String path) async {
if (!mounted) return;
if (path.isEmpty) return;
try {
String finalPath = path;
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) {
ToastUtil.showNormal(context, '视频转码失败');
}
return;
} finally {
if (mounted) setState(() => _isProcessing = false);
}
}
}
if (_mediaPaths.length < widget.maxCount) {
setState(() => _mediaPaths.add(finalPath));
// 记录缓存(只在添加时检查一次文件是否真实存在)
if (!_isNetworkPath(finalPath)) {
try {
_localExistsCache[finalPath] = File(finalPath).existsSync();
} catch (_) {
_localExistsCache[finalPath] = false;
}
} else {
_localExistsCache[finalPath] = false;
}
// 回调仅包含本地真实存在的文件(网络路径不会出现在此回调中)
widget.onChanged(_localFilesFromPaths(_mediaPaths));
widget.onMediaAdded?.call(finalPath);
}
} catch (e) {
debugPrint('处理选中媒体失败: $e');
}
}
Future<void> _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<void> _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));
// 记录存在
try {
_localExistsCache[path] = File(path).existsSync();
} catch (_) {
_localExistsCache[path] = false;
}
widget.onChanged(_localFilesFromPaths(_mediaPaths));
widget.onMediaAdded?.call(path);
}
} else {
picked = await _picker.pickVideo(source: ImageSource.camera);
if (picked != null) {
await _handlePickedPath(picked.path);
}
}
} catch (e) {
debugPrint('拍摄失败: $e');
}
}
Future<void> _pickGallery() async {
if (!widget.isEdit || _mediaPaths.length >= widget.maxCount) return;
try {
if (Platform.isIOS) {
final permission = await PhotoManager.requestPermissionExtend();
debugPrint('iOS photo permission state: $permission');
if (permission != PermissionState.authorized && permission != PermissionState.limited) {
if (mounted) {
ToastUtil.showNormal(context, '请到设置中开启相册访问权限');
}
return;
}
final remaining = widget.maxCount - _mediaPaths.length;
final List<AssetEntity>? 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;
try {
final file = await asset.file;
if (file != null) {
final path = file.path;
await _handlePickedPath(path);
} else {
debugPrint('资产获取 file 为空asset id: ${asset.id}');
}
} catch (e) {
debugPrint('读取 asset 文件失败: $e');
}
}
if (mounted) setState(() {});
widget.onChanged(_localFilesFromPaths(_mediaPaths));
}
} else {
final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
final androidInfo = await deviceInfo.androidInfo;
final int sdkInt = androidInfo.version.sdkInt ?? 0;
PermissionStatus permissionStatus = PermissionStatus.denied;
if (sdkInt >= 33) {
if (widget.mediaType == MediaType.image) {
permissionStatus = await Permission.photos.request();
} else if (widget.mediaType == MediaType.video) {
permissionStatus = await Permission.videos.request();
} else {
final statuses = await [Permission.photos, Permission.videos].request();
permissionStatus = statuses[Permission.photos] ?? statuses[Permission.videos] ?? PermissionStatus.denied;
}
} else if (sdkInt >= 30) {
permissionStatus = await Permission.storage.request();
} else {
permissionStatus = await Permission.storage.request();
}
if (permissionStatus.isGranted) {
final remaining = widget.maxCount - _mediaPaths.length;
final List<AssetEntity>? 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;
try {
final file = await asset.file;
if (file != null) {
final path = file.path;
await _handlePickedPath(path);
} else {
debugPrint('资产获取 file 为空asset id: ${asset.id}');
}
} catch (e) {
debugPrint('读取 asset 文件失败: $e');
}
}
if (mounted) setState(() {});
widget.onChanged(_localFilesFromPaths(_mediaPaths));
}
} else if (permissionStatus.isPermanentlyDenied) {
if (mounted) {
ToastUtil.showNormal(context, '请到设置中开启相册访问权限');
}
await openAppSettings();
return;
} else {
if (mounted) {
ToastUtil.showNormal(context, '相册访问权限被拒绝');
}
return;
}
}
} catch (e, st) {
debugPrint('相册选择失败: $e\n$st');
if (mounted) {
ToastUtil.showNormal(context, '相册选择失败');
}
}
}
Future<void> _cameraAction() async {
if (!widget.isEdit || _mediaPaths.length >= widget.maxCount) return;
final PermissionStatus status = await Permission.camera.request();
if (status != PermissionStatus.granted) {
if (mounted) {
ToastUtil.showNormal(context, '相机权限被拒绝');
}
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));
try {
_localExistsCache[path] = File(path).existsSync();
} catch (_) {
_localExistsCache[path] = false;
}
widget.onChanged(_localFilesFromPaths(_mediaPaths));
widget.onMediaAdded?.call(path);
}
} else {
XFile? picked = await _picker.pickVideo(source: ImageSource.camera);
if (picked != null) {
await _handlePickedPath(picked.path);
}
}
} catch (e) {
debugPrint('拍摄失败: $e');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('拍摄失败,请检查相机权限或设备状态')));
}
}
}
void _removeMedia(int index) async {
final ok = await CustomAlertDialog.showConfirm(
context,
title: '温馨提示',
content: widget.mediaType == MediaType.image ? '确定要删除这张图片吗?' : '确定要删除这个视频吗?',
cancelText: '取消',
);
if (!ok) return;
final removed = _mediaPaths[index];
setState(() => _mediaPaths.removeAt(index));
// 从缓存中移除
_localExistsCache.remove(removed);
// 始终通知 onMediaRemoved用于父端业务逻辑
widget.onMediaRemoved?.call(removed);
widget.onMediaRemovedForIndex?.call(index);
// 只有当本地文件集合发生变化时才触发 onChanged避免因为删除网络路径导致父端把列表置空
final localFiles = _localFilesFromPaths(_mediaPaths);
widget.onChanged(localFiles);
}
@override
Widget build(BuildContext context) {
final showAddButton = widget.isEdit && _mediaPaths.length < widget.maxCount;
final itemCount = _mediaPaths.length + (showAddButton ? 1 : 0);
// 使用 LayoutBuilder 获取父容器宽度,然后按 crossAxisCount 计算每个 tile 的逻辑宽度
return LayoutBuilder(builder: (context, constraints) {
final maxWidth = (constraints.maxWidth.isFinite && constraints.maxWidth > 0)
? constraints.maxWidth
: MediaQuery.of(context).size.width;
final tileLogicalW = (maxWidth / widget.crossAxisCount).round();
final cacheWidth = (tileLogicalW * MediaQuery.of(context).devicePixelRatio).round();
return Stack(
children: [
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: widget.crossAxisCount,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
childAspectRatio: 1,
),
itemCount: itemCount,
itemBuilder: (context, index) {
if (index < _mediaPaths.length) {
final raw = (_mediaPaths[index] ?? '').toString().trim();
final isNetwork = _isNetworkPath(raw);
return GestureDetector(
onTap: () => widget.onMediaTapped?.call(raw),
child: Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(5),
child: SizedBox.expand(
child: raw.isEmpty
? Container(
color: Colors.grey.shade200,
child: const Center(child: Icon(Icons.broken_image, size: 28, color: Colors.grey)),
)
: (widget.mediaType == MediaType.image
? (isNetwork
? Image.network(
raw,
fit: BoxFit.cover,
width: tileLogicalW.toDouble(),
height: tileLogicalW.toDouble(),
errorBuilder: (_, __, ___) => Container(
color: Colors.grey.shade200,
child: const Center(child: Icon(Icons.broken_image)),
),
)
: Image.file(
File(raw),
fit: BoxFit.cover,
width: tileLogicalW.toDouble(),
height: tileLogicalW.toDouble(),
cacheWidth: cacheWidth,
errorBuilder: (_, __, ___) => Container(
color: Colors.grey.shade200,
child: const Center(child: Icon(Icons.broken_image)),
),
))
: 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();
}
},
),
if (_isProcessing)
Positioned.fill(
child: Container(
color: Colors.transparent,
child: const Center(child: CircularProgressIndicator()),
),
),
],
);
});
}
}
/// ---------- RepairedPhotoSection ----------
// ------------------ 修改后的 RepairedPhotoSection完整类 ------------------
class RepairedPhotoSection extends StatefulWidget {
final int maxCount;
final MediaType mediaType;
final String title;
final List<String>? initialMediaPaths;
final ValueChanged<List<File>> onChanged;
final ValueChanged<String>? onMediaAdded;
final ValueChanged<String>? onMediaRemoved;
final ValueChanged<int>? onMediaRemovedForIndex;
final ValueChanged<String>? onMediaTapped;
final bool isFaceImage;
final VoidCallback onAiIdentify;
final bool isShowAI;
final double horizontalPadding;
final bool isRequired;
final bool isShowNum;
final bool isEdit;
final bool isCamera;
final String sectionKey;
final bool followInitialUpdates;
/// 新增inlineSingle = true -> 标题和照片在同一行,且只能上传 1 张
final bool inlineSingle;
/// 可选:当 inlineSingle 为 true 时可以定制缩略图宽度px
final double inlineImageWidth;
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.isFaceImage = false,
this.horizontalPadding = 5,
this.onMediaAdded,
this.onMediaRemoved,
this.onMediaRemovedForIndex,
this.onMediaTapped,
this.isRequired = false,
this.isShowNum = true,
this.isEdit = true,
this.isCamera = false,
this.followInitialUpdates = false, // 默认 false
this.sectionKey = kAcceptVideoSectionKey,
this.inlineSingle = false,
this.inlineImageWidth = 88.0,
}) : super(key: key);
@override
_RepairedPhotoSectionState createState() => _RepairedPhotoSectionState();
}
class _RepairedPhotoSectionState extends State<RepairedPhotoSection> {
late List<String> _mediaPaths;
StreamSubscription<MediaEvent>? _sub;
@override
void initState() {
super.initState();
_mediaPaths = _normalizePaths(widget.initialMediaPaths).take(widget.maxCount).toList();
// 订阅 MediaBus如果需要
_sub = MediaBus().stream.listen((ev) {
if (ev.key != widget.sectionKey) return;
if (ev.type == MediaEventType.clear) {
if (_mediaPaths.isNotEmpty) {
setState(() {
_mediaPaths = <String>[];
});
widget.onChanged(_localFilesFromPaths(_mediaPaths));
}
} else if (ev.type == MediaEventType.set && ev.paths != null) {
final newList = _normalizePaths(ev.paths).take(widget.maxCount).toList();
if (!listEquals(newList, _mediaPaths)) {
setState(() {
_mediaPaths = newList;
});
widget.onChanged(_localFilesFromPaths(_mediaPaths));
}
}
});
}
@override
void didUpdateWidget(covariant RepairedPhotoSection oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.followInitialUpdates) {
final oldList = _normalizePaths(oldWidget.initialMediaPaths);
final newList = _normalizePaths(widget.initialMediaPaths);
if (!listEquals(oldList, newList)) {
setState(() {
_mediaPaths = newList.take(widget.maxCount).toList();
});
widget.onChanged(_localFilesFromPaths(_mediaPaths));
}
}
if (oldWidget.sectionKey != widget.sectionKey) {
_sub?.cancel();
_sub = MediaBus().stream.listen((ev) {
if (ev.key != widget.sectionKey) return;
if (ev.type == MediaEventType.clear) {
if (_mediaPaths.isNotEmpty) {
setState(() {
_mediaPaths = <String>[];
});
widget.onChanged(_localFilesFromPaths(_mediaPaths));
}
} else if (ev.type == MediaEventType.set && ev.paths != null) {
final newList = _normalizePaths(ev.paths).take(widget.maxCount).toList();
if (!listEquals(newList, _mediaPaths)) {
setState(() {
_mediaPaths = newList;
});
widget.onChanged(_localFilesFromPaths(_mediaPaths));
}
}
});
}
}
@override
void dispose() {
_sub?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
// 如果要求 inlineSingle 样式:标题与单张照片在同一行
if (widget.inlineSingle) {
final displayPath = _mediaPaths.isNotEmpty ? _mediaPaths.first : '';
final isNetwork = _isNetworkPath(displayPath);
return Container(
color: Colors.white,
padding: EdgeInsets.only(left: widget.horizontalPadding, right: widget.horizontalPadding),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// 标题(左)
Expanded(
child: ListItemFactory.createRowSpaceBetweenItem(
leftText: widget.title,
rightText: '', // inline 时不显示数量
isRequired: widget.isRequired,
),
),
// 右侧 单张 缩略图 / 上传按钮
SizedBox(
width: widget.inlineImageWidth,
height: widget.inlineImageWidth,
child: GestureDetector(
onTap: () async {
if (widget.isEdit) {
// 打开 MediaPickerRow 的选择逻辑我们通过弹出一个底部sheet 让用户拍照或选图
// 复用 MediaPickerRow 的选择入口:这里直接展示同样的选项
if (widget.isFaceImage) {
final filePath = await pushPage(
FaceRecognitionPage(
studentId: '',
data: {},
mode: FaceMode.initSave,
),
context,
);
setState(() {
_mediaPaths = [filePath];
widget.onChanged(_localFilesFromPaths(_mediaPaths));
});
}else{
showModalBottomSheet(
context: context,
builder: (ctx) => 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: () async {
Navigator.of(ctx).pop();
// 触发 MediaPickerRow 中的 camera 逻辑:我们在这里简单复用 ImagePicker
final picker = ImagePicker();
try {
if (widget.mediaType == MediaType.image) {
final x = await picker.pickImage(source: ImageSource.camera);
if (x != null) {
setState(() {
_mediaPaths = [x.path];
});
widget.onChanged(_localFilesFromPaths(_mediaPaths));
widget.onMediaAdded?.call(x.path);
}
} else {
final x = await picker.pickVideo(source: ImageSource.camera);
if (x != null) {
// 若需要转码、压缩请复用 VideoCompress
setState(() {
_mediaPaths = [x.path];
});
widget.onChanged(_localFilesFromPaths(_mediaPaths));
widget.onMediaAdded?.call(x.path);
}
}
} catch (e) {
debugPrint('camera pick error: $e');
ToastUtil.showNormal(context, '拍摄失败');
}
},
),
ListTile(
titleAlignment: ListTileTitleAlignment.center,
leading: Icon(widget.mediaType == MediaType.image ? Icons.photo_library : Icons.video_library),
title: Text(widget.mediaType == MediaType.image ? '从相册选择' : '从相册选择视频'),
onTap: () async {
Navigator.of(ctx).pop();
// 这里直接调用 AssetPicker与 MediaPickerRow 的行为保持一致)
try {
final List<AssetEntity>? assets = await AssetPicker.pickAssets(
context,
pickerConfig: AssetPickerConfig(
requestType: widget.mediaType == MediaType.image ? RequestType.image : RequestType.video,
maxAssets: 1,
gridCount: 4,
),
);
if (assets != null && assets.isNotEmpty) {
final file = await assets.first.file;
if (file != null) {
setState(() {
_mediaPaths = [file.path];
});
widget.onChanged(_localFilesFromPaths(_mediaPaths));
widget.onMediaAdded?.call(file.path);
}
}
} catch (e) {
debugPrint('pick asset error: $e');
ToastUtil.showNormal(context, '选择图片失败');
}
},
),
ListTile(
titleAlignment: ListTileTitleAlignment.center,
leading: const Icon(Icons.close),
title: const Text('取消'),
onTap: () => Navigator.of(ctx).pop(),
),
],
),
),
);
}
} else {
// 非编辑模式:触发查看
presentOpaque(SingleImageViewer(imageUrl: displayPath), context);
}
},
child: ClipRRect(
borderRadius: BorderRadius.circular(5),
child: Container(
color: Colors.grey.shade100,
child: displayPath.isEmpty
? Center(child: Icon(Icons.camera_alt, color: Colors.black26))
: (widget.mediaType == MediaType.image
? (isNetwork
? Image.network(displayPath, fit: BoxFit.cover)
: Image.file(File(displayPath), fit: BoxFit.cover))
: Stack(
children: [
Container(color: Colors.black12),
const Center(child: Icon(Icons.videocam, color: Colors.white70)),
],
)),
),
),
),
),
const SizedBox(width: 8),
],
),
);
}
// 原始布局title 在上,网格在下)——保持不变
return Container(
color: Colors.white,
padding: 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,
onMediaRemovedForIndex: widget.onMediaRemovedForIndex,
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: (filePath) {
if (widget.mediaType == MediaType.image) {
presentOpaque(SingleImageViewer(imageUrl: filePath), context);
} else {
showDialog(
context: context,
barrierColor: Colors.black54,
builder: (_) => VideoPlayerPopup(videoUrl: filePath),
);
}
},
isEdit: widget.isEdit,
),
),
const SizedBox(height: 8),
if (widget.isShowAI && widget.isEdit)
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隐患识别与处理'),
],
),
),
),
),
],
),
);
}
}