QinGang_interested/lib/customWidget/photo_picker_row.dart

993 lines
37 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

// <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隐患识别与处理'),
],
),
),
),
),
],
),
);
}
}