Files
flutterApp/lib/features/v2/report/presentation/widgets/media_uploader.dart

275 lines
9.9 KiB
Dart
Raw Normal View History

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
2026-08-18 08:52:33 +08:00
import 'package:maibu_satabot_v2/core/theme/AppTheme.dart';
import '../constants/report_constants.dart';
/// 媒体上传模块
class MediaUploader extends StatelessWidget {
final List<String> mediaFiles;
final VoidCallback onCameraTap;
final VoidCallback onVideoTap;
final VoidCallback onAddFromGallery;
final Function(int) onRemove;
final Function(int)? onTap;
const MediaUploader({
super.key,
required this.mediaFiles,
required this.onCameraTap,
required this.onVideoTap,
required this.onAddFromGallery,
required this.onRemove,
this.onTap,
});
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
return Container(
margin: const EdgeInsets.symmetric(
horizontal: AppDimensions.horizontalPadding,
),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
2026-08-18 08:52:33 +08:00
color: context.appColors.cardBackground,
borderRadius: BorderRadius.circular(AppDimensions.borderRadius),
boxShadow: [
BoxShadow(
2026-08-18 08:52:33 +08:00
color: context.appColors.cardShadow,
blurRadius: AppDimensions.cardShadowBlur,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
loc.translate('report.upload_media'),
2026-08-18 08:52:33 +08:00
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
2026-08-18 08:52:33 +08:00
color: context.appColors.textPrimary,
),
),
const SizedBox(height: 12),
2026-08-07 08:49:29 +08:00
SizedBox(
width: double.infinity,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
physics: const ClampingScrollPhysics(),
child: Row(
children: [
// 拍照按钮
_buildUploadButton(
2026-08-18 08:52:33 +08:00
context,
2026-08-07 08:49:29 +08:00
icon: Icons.camera_alt,
label: loc.translate('report.take_photo'),
onTap: onCameraTap,
),
const SizedBox(width: 8),
// 录像按钮
_buildUploadButton(
2026-08-18 08:52:33 +08:00
context,
2026-08-07 08:49:29 +08:00
icon: Icons.videocam,
label: loc.translate('report.record_video'),
onTap: onVideoTap,
),
const SizedBox(width: 8),
// 已上传的媒体预览
...mediaFiles.asMap().entries.map((entry) {
final String filePath = entry.value;
final bool isNetworkImage = filePath.startsWith('http');
final bool isVideo = _isVideoFile(filePath);
2026-08-07 08:49:29 +08:00
return GestureDetector(
onTap: () => onTap?.call(entry.key),
child: Stack(
children: [
Container(
width: 80,
height: 80,
margin: const EdgeInsets.only(right: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(
AppDimensions.borderRadiusSmall,
),
2026-08-18 08:52:33 +08:00
color: context.appColors.fillBackground,
2026-08-07 08:49:29 +08:00
),
child: ClipRRect(
borderRadius: BorderRadius.circular(
AppDimensions.borderRadiusSmall,
),
2026-08-07 08:49:29 +08:00
child: isVideo
? _buildVideoThumbnail(filePath)
: (isNetworkImage
? Image.network(
filePath,
fit: BoxFit.cover,
errorBuilder:
(context, error, stackTrace) {
return _buildPlaceholderIcon(
Icons.broken_image,
);
},
)
: Image.file(
File(filePath),
fit: BoxFit.cover,
errorBuilder:
(context, error, stackTrace) {
return _buildPlaceholderIcon(
Icons.broken_image,
);
},
)),
),
),
Positioned(
2026-08-07 08:49:29 +08:00
top: 4,
right: 12,
child: GestureDetector(
onTap: () => onRemove(entry.key),
child: Container(
2026-08-07 08:49:29 +08:00
width: 20,
height: 20,
decoration: const BoxDecoration(
2026-08-07 08:49:29 +08:00
color: Colors.black54,
shape: BoxShape.circle,
),
child: const Icon(
2026-08-07 08:49:29 +08:00
Icons.close,
size: 14,
color: Colors.white,
),
),
),
),
2026-08-07 08:49:29 +08:00
// 视频播放图标
if (isVideo)
Positioned(
child: Container(
width: 80,
height: 80,
margin: const EdgeInsets.only(right: 8),
alignment: Alignment.center,
child: Container(
padding: const EdgeInsets.all(8),
decoration: const BoxDecoration(
color: Colors.black38,
shape: BoxShape.circle,
),
child: const Icon(
Icons.play_arrow,
color: Colors.white,
size: 24,
),
),
),
),
],
),
);
}).toList(),
// 添加按钮
_buildUploadButton(
2026-08-18 08:52:33 +08:00
context,
2026-08-07 08:49:29 +08:00
icon: Icons.add,
label: '',
onTap: onAddFromGallery,
isAddButton: true,
),
],
),
),
),
],
),
);
}
2026-08-18 08:52:33 +08:00
Widget _buildUploadButton(
BuildContext context, {
required IconData icon,
required String label,
required VoidCallback onTap,
bool isAddButton = false,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
2026-08-18 08:52:33 +08:00
color: isAddButton
? context.appColors.fillBackground
: context.appColors.cardBackground,
borderRadius: BorderRadius.circular(AppDimensions.borderRadiusMedium),
border: Border.all(
2026-08-18 08:52:33 +08:00
color: isAddButton
? context.appColors.divider
: context.appColors.primary,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
2026-08-18 08:52:33 +08:00
color: isAddButton
? context.appColors.textTertiary
: context.appColors.primary,
size: 24,
),
if (label.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
fontSize: 12,
2026-08-18 08:52:33 +08:00
color: isAddButton
? context.appColors.textTertiary
: context.appColors.primary,
),
),
],
],
),
),
);
}
/// 判断是否为视频文件
bool _isVideoFile(String filePath) {
final lowerPath = filePath.toLowerCase();
return lowerPath.endsWith('.mp4') ||
lowerPath.endsWith('.mov') ||
lowerPath.endsWith('.avi') ||
lowerPath.endsWith('.mkv') ||
lowerPath.endsWith('.wmv') ||
lowerPath.endsWith('.flv');
}
/// 构建视频缩略图(显示占位符)
Widget _buildVideoThumbnail(String filePath) {
return Stack(
fit: StackFit.expand,
children: [
// 灰色背景
Container(color: Colors.grey[300]),
// 视频图标
const Center(child: Icon(Icons.videocam, color: Colors.grey, size: 32)),
],
);
}
/// 构建占位图标
Widget _buildPlaceholderIcon(IconData icon) {
return Container(
color: Colors.grey[300],
child: Center(child: Icon(icon, color: Colors.grey, size: 32)),
);
}
}