Ver Fonte

添加media测试页面。

PC\19500 há 3 semanas atrás
pai
commit
8c1202756e

+ 11 - 0
android/app/src/debug/AndroidManifest.xml

@@ -4,4 +4,15 @@
          to allow setting breakpoints, to provide hot reload, etc.
     -->
     <uses-permission android:name="android.permission.INTERNET"/>
+
+    <uses-permission android:name="android.permission.CAMERA"/>
+
+    <!-- Android 13+ -->
+    <uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
+
+    <!-- Android 12- -->
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+
+    <uses-permission android:name="android.permission.RECORD_AUDIO"/>
+    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
 </manifest>

+ 9 - 0
android/app/src/profile/AndroidManifest.xml

@@ -4,6 +4,15 @@
          to allow setting breakpoints, to provide hot reload, etc.
     -->
     <uses-permission android:name="android.permission.INTERNET"/>
+
     <uses-permission android:name="android.permission.CAMERA"/>
+
+    <!-- Android 13+ -->
     <uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
+
+    <!-- Android 12- -->
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+
+    <uses-permission android:name="android.permission.RECORD_AUDIO"/>
+    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
 </manifest>

+ 37 - 4
lib/app/router.dart

@@ -1,18 +1,51 @@
+import 'package:flutter/material.dart';
 import 'package:go_router/go_router.dart';
 import 'package:sino_med_cloud/features/main_tab_page.dart';
 import '../features/auth/presentation/login_page.dart';
+import '../features/MediaTestPage/presentation/media_test_page.dart';
 
 class AppRouter {
-  static final router = GoRouter (
+  static final router = GoRouter(
     routes: [
       GoRoute(
         path: '/',
-        builder: (_, _) => const LoginPage(),
+        builder: (context, state) => const LoginPage(),
       ),
       GoRoute(
         path: '/mainTab',
-        builder: (_, _) => const MainTabPage(),
+        builder: (context, state) => const MainTabPage(),
       ),
-    ]
+      GoRoute(
+        path: '/mediaTest',
+        builder: (context, state) => const MediaTestPage(),
+      ),
+    ],
+    errorBuilder: (context, state) => Scaffold(
+      appBar: AppBar(
+        title: const Text('页面未找到'),
+      ),
+      body: Center(
+        child: Column(
+          mainAxisAlignment: MainAxisAlignment.center,
+          children: [
+            const Icon(
+              Icons.error_outline,
+              size: 64,
+              color: Colors.grey,
+            ),
+            const SizedBox(height: 16),
+            Text(
+              '页面未找到: ${state.uri}',
+              style: const TextStyle(fontSize: 16),
+            ),
+            const SizedBox(height: 16),
+            ElevatedButton(
+              onPressed: () => context.go('/'),
+              child: const Text('返回首页'),
+            ),
+          ],
+        ),
+      ),
+    ),
   );
 }

+ 584 - 0
lib/features/MediaTestPage/presentation/media_test_page.dart

@@ -0,0 +1,584 @@
+import 'dart:io';
+import 'package:flutter/material.dart';
+import 'package:sino_med_cloud/core/utils/logger.dart';
+import 'package:sino_med_cloud/core/utils/toast_utils.dart';
+import 'package:sino_med_cloud/l10n/app_localizations.dart';
+import 'package:sino_med_cloud/media/media.dart';
+
+/// 媒体功能测试页面
+class MediaTestPage extends StatefulWidget {
+  const MediaTestPage({super.key});
+
+  @override
+  State<MediaTestPage> createState() => _MediaTestPageState();
+}
+
+class _MediaTestPageState extends State<MediaTestPage> {
+  File? _selectedImage;
+  File? _cameraImage;
+  bool _isLoading = false;
+
+  /// 获取当前选中的图片(优先使用相册/系统相机,其次使用专业相机)
+  File? get _currentImage => _selectedImage ?? _cameraImage;
+
+  @override
+  Widget build(BuildContext context) {
+    final l10n = AppLocalizations.of(context)!;
+    return Scaffold(
+      appBar: AppBar(
+        title: Text(l10n.mediaTest),
+        elevation: 0,
+      ),
+      body: SingleChildScrollView(
+        padding: const EdgeInsets.all(16),
+        child: Column(
+          crossAxisAlignment: CrossAxisAlignment.stretch,
+          children: [
+            // 测试按钮区域
+            _buildTestButtons(),
+            
+            const SizedBox(height: 24),
+            
+            // 图片显示区域
+            _buildImageDisplay(),
+            
+            const SizedBox(height: 24),
+            
+          ],
+        ),
+      ),
+    );
+  }
+
+  /// 构建测试按钮区域
+  Widget _buildTestButtons() {
+    final l10n = AppLocalizations.of(context)!;
+    return Column(
+      crossAxisAlignment: CrossAxisAlignment.stretch,
+      children: [
+        Text(
+          l10n.quickTest,
+          style: const TextStyle(
+            fontSize: 18,
+            fontWeight: FontWeight.bold,
+          ),
+        ),
+        const SizedBox(height: 12),
+        
+        // 从相册选择
+        ElevatedButton.icon(
+          onPressed: _isLoading ? null : _testPickFromGallery,
+          icon: const Icon(Icons.photo_library),
+          label: Text(l10n.pickFromGallery),
+          style: ElevatedButton.styleFrom(
+            padding: const EdgeInsets.symmetric(vertical: 16),
+          ),
+        ),
+        
+        const SizedBox(height: 12),
+        
+        // 使用系统相机拍照
+        ElevatedButton.icon(
+          onPressed: _isLoading ? null : _testPickFromCamera,
+          icon: const Icon(Icons.camera_alt),
+          label: Text(l10n.pickFromCamera),
+          style: ElevatedButton.styleFrom(
+            padding: const EdgeInsets.symmetric(vertical: 16),
+          ),
+        ),
+        
+        const SizedBox(height: 24),
+        
+        Text(
+          l10n.professionalCameraTest,
+          style: const TextStyle(
+            fontSize: 18,
+            fontWeight: FontWeight.bold,
+          ),
+        ),
+        const SizedBox(height: 12),
+        
+        // 专业相机拍照
+        ElevatedButton.icon(
+          onPressed: _isLoading ? null : _testProfessionalCamera,
+          icon: const Icon(Icons.camera),
+          label: Text(l10n.useProfessionalCamera),
+          style: ElevatedButton.styleFrom(
+            padding: const EdgeInsets.symmetric(vertical: 16),
+          ),
+        ),
+        
+        const SizedBox(height: 24),
+        
+        Text(
+          l10n.imageProcessingTest,
+          style: const TextStyle(
+            fontSize: 18,
+            fontWeight: FontWeight.bold,
+          ),
+        ),
+        const SizedBox(height: 12),
+        
+        // 获取图片信息
+        ElevatedButton.icon(
+          onPressed: _isLoading || _currentImage == null
+              ? null
+              : _testGetImageInfo,
+          icon: const Icon(Icons.info_outline),
+          label: Text(l10n.getImageInfo),
+          style: ElevatedButton.styleFrom(
+            padding: const EdgeInsets.symmetric(vertical: 16),
+          ),
+        ),
+        
+        const SizedBox(height: 12),
+        
+        // 去除 EXIF
+        ElevatedButton.icon(
+          onPressed: _isLoading || _currentImage == null
+              ? null
+              : _testRemoveExif,
+          icon: const Icon(Icons.security),
+          label: Text(l10n.removeExif),
+          style: ElevatedButton.styleFrom(
+            padding: const EdgeInsets.symmetric(vertical: 16),
+          ),
+        ),
+        
+        const SizedBox(height: 12),
+        
+        // 压缩图片
+        ElevatedButton.icon(
+          onPressed: _isLoading || _currentImage == null
+              ? null
+              : _testCompressImage,
+          icon: const Icon(Icons.compress),
+          label: Text(l10n.compressImage),
+          style: ElevatedButton.styleFrom(
+            padding: const EdgeInsets.symmetric(vertical: 16),
+          ),
+        ),
+        
+        const SizedBox(height: 12),
+        
+        // 清除图片
+        OutlinedButton.icon(
+          onPressed: _isLoading ? null : _clearImages,
+          icon: const Icon(Icons.clear),
+          label: Text(l10n.clearAllImages),
+        ),
+      ],
+    );
+  }
+
+  /// 构建图片显示区域
+  Widget _buildImageDisplay() {
+    final l10n = AppLocalizations.of(context)!;
+    return Column(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        Text(
+          l10n.selectedImage,
+          style: const TextStyle(
+            fontSize: 18,
+            fontWeight: FontWeight.bold,
+          ),
+        ),
+        const SizedBox(height: 12),
+        
+        if (_selectedImage == null && _cameraImage == null)
+          Container(
+            height: 200,
+            decoration: BoxDecoration(
+              color: Colors.grey[200],
+              borderRadius: BorderRadius.circular(8),
+            ),
+            child: Center(
+              child: Text(
+                l10n.noImage,
+                style: const TextStyle(
+                  color: Colors.grey,
+                  fontSize: 16,
+                ),
+              ),
+            ),
+          )
+        else
+          Column(
+            children: [
+              if (_selectedImage != null) ...[
+                Text(
+                  l10n.galleryOrSystemCamera,
+                  style: const TextStyle(
+                    fontSize: 14,
+                    color: Colors.grey,
+                  ),
+                ),
+                const SizedBox(height: 8),
+                _buildImagePreview(_selectedImage!),
+                const SizedBox(height: 16),
+              ],
+              if (_cameraImage != null) ...[
+                Text(
+                  l10n.professionalCamera,
+                  style: const TextStyle(
+                    fontSize: 14,
+                    color: Colors.grey,
+                  ),
+                ),
+                const SizedBox(height: 8),
+                _buildImagePreview(_cameraImage!),
+              ],
+            ],
+          ),
+      ],
+    );
+  }
+
+  /// 构建图片预览
+  Widget _buildImagePreview(File imageFile) {
+    return Container(
+      constraints: const BoxConstraints(maxHeight: 400),
+      decoration: BoxDecoration(
+        borderRadius: BorderRadius.circular(8),
+        border: Border.all(color: Colors.grey[300]!),
+      ),
+      child: ClipRRect(
+        borderRadius: BorderRadius.circular(8),
+        child: Image.file(
+          imageFile,
+          fit: BoxFit.contain,
+          errorBuilder: (context, error, stackTrace) {
+            return Container(
+              height: 200,
+              color: Colors.grey[200],
+              child: const Center(
+                child: Text('图片加载失败'),
+              ),
+            );
+          },
+        ),
+      ),
+    );
+  }
+
+
+  /// 测试从相册选择
+  Future<void> _testPickFromGallery() async {
+    setState(() {
+      _isLoading = true;
+    });
+
+    try {
+      AppLogger.d('开始测试从相册选择图片...');
+      final file = await MediaService.pickFromGallery();
+      
+      if (file != null) {
+        if (!mounted) return;
+        final l10n = AppLocalizations.of(context)!;
+        setState(() {
+          _selectedImage = file;
+          _cameraImage = null;
+        });
+        ToastUtils.showSuccess(l10n.galleryPickSuccess);
+        AppLogger.d('相册选择成功: ${file.path}');
+      } else {
+        if (!mounted) return;
+        final l10n = AppLocalizations.of(context)!;
+        ToastUtils.show(l10n.userCancelledSelection);
+      }
+    } on MediaException catch (e) {
+      AppLogger.e('相册选择失败', e);
+      ToastUtils.showError(e.message);
+    } catch (e) {
+      AppLogger.e('相册选择异常', e);
+      ToastUtils.showError('相册选择失败: ${e.toString()}');
+    } finally {
+      setState(() {
+        _isLoading = false;
+      });
+    }
+  }
+
+  /// 测试使用系统相机拍照
+  Future<void> _testPickFromCamera() async {
+    setState(() {
+      _isLoading = true;
+    });
+
+    try {
+      AppLogger.d('开始测试使用系统相机拍照...');
+      final file = await MediaService.pickFromCamera();
+      
+      if (file != null) {
+        if (!mounted) return;
+        final l10n = AppLocalizations.of(context)!;
+        setState(() {
+          _selectedImage = file;
+          _cameraImage = null;
+        });
+        ToastUtils.showSuccess(l10n.cameraPhotoSuccess);
+        AppLogger.d('拍照成功: ${file.path}');
+      } else {
+        if (!mounted) return;
+        final l10n = AppLocalizations.of(context)!;
+        ToastUtils.show(l10n.userCancelledPhoto);
+      }
+    } on MediaException catch (e) {
+      AppLogger.e('拍照失败', e);
+      ToastUtils.showError(e.message);
+    } catch (e) {
+      AppLogger.e('拍照异常', e);
+      ToastUtils.showError('拍照失败: ${e.toString()}');
+    } finally {
+      setState(() {
+        _isLoading = false;
+      });
+    }
+  }
+
+  /// 测试专业相机
+  Future<void> _testProfessionalCamera() async {
+    setState(() {
+      _isLoading = true;
+    });
+
+    CameraService? cameraService;
+
+    try {
+      AppLogger.d('开始测试专业相机...');
+      cameraService = CameraService();
+      
+      // 初始化相机
+      await cameraService.init();
+      ToastUtils.show('相机初始化成功,请拍照');
+      
+      // 显示拍照对话框
+      if (!mounted) return;
+      final l10n = AppLocalizations.of(context)!;
+      final shouldTakePhoto = await showDialog<bool>(
+        context: context,
+        builder: (context) => AlertDialog(
+          title: Text(l10n.professionalCamera),
+          content: Text(l10n.cameraReady),
+          actions: [
+            TextButton(
+              onPressed: () => Navigator.of(context).pop(false),
+              child: Text(l10n.cancel),
+            ),
+            ElevatedButton(
+              onPressed: () => Navigator.of(context).pop(true),
+              child: const Text('拍照'),
+            ),
+          ],
+        ),
+      );
+
+      if (shouldTakePhoto == true && mounted) {
+        final file = await cameraService.takePicture();
+        
+        if (file != null) {
+          setState(() {
+            _cameraImage = file;
+            _selectedImage = null;
+          });
+          ToastUtils.showSuccess(l10n.professionalCameraSuccess);
+          AppLogger.d('专业相机拍照成功: ${file.path}');
+        }
+      }
+    } on MediaException catch (e) {
+      AppLogger.e('专业相机失败', e);
+      ToastUtils.showError(e.message);
+    } catch (e) {
+      AppLogger.e('专业相机异常', e);
+      ToastUtils.showError('专业相机失败: ${e.toString()}');
+    } finally {
+      await cameraService?.dispose();
+      setState(() {
+        _isLoading = false;
+      });
+    }
+  }
+
+  /// 测试获取图片信息
+  Future<void> _testGetImageInfo() async {
+    final currentImage = _currentImage;
+    if (currentImage == null) return;
+
+    setState(() {
+      _isLoading = true;
+    });
+
+    try {
+      AppLogger.d('开始获取图片信息...');
+      final info = await MediaService.getImageInfo(currentImage);
+      
+      final l10n = AppLocalizations.of(context)!;
+      final sizeInBytes = info['size'] as int;
+      final sizeInKB = (sizeInBytes / 1024).toStringAsFixed(2);
+      
+      final infoText = '${l10n.imageWidth}: ${info['width']}${l10n.pixels}\n'
+          '${l10n.imageHeight}: ${info['height']}${l10n.pixels}\n'
+          '${l10n.imageSize}: $sizeInKB ${l10n.kilobytes}\n'
+          '${l10n.imageFormat}: ${info['format']}';
+      
+      if (!mounted) return;
+      
+      // 使用 Alert 弹窗显示图片信息
+      showDialog(
+        context: context,
+        builder: (context) => AlertDialog(
+          title: Text(l10n.imageInfo),
+          content: Text(
+            infoText,
+            style: const TextStyle(
+              fontSize: 14,
+              fontFamily: 'monospace',
+            ),
+          ),
+          actions: [
+            TextButton(
+              onPressed: () => Navigator.of(context).pop(),
+              child: Text(l10n.confirm),
+            ),
+          ],
+        ),
+      );
+      
+      ToastUtils.showSuccess(l10n.getImageInfoSuccess);
+      AppLogger.d('图片信息: $info');
+    } on MediaException catch (e) {
+      AppLogger.e('获取图片信息失败', e);
+      ToastUtils.showError(e.message);
+    } catch (e) {
+      AppLogger.e('获取图片信息异常', e);
+      ToastUtils.showError('获取图片信息失败: ${e.toString()}');
+    } finally {
+      setState(() {
+        _isLoading = false;
+      });
+    }
+  }
+
+  /// 测试去除 EXIF
+  Future<void> _testRemoveExif() async {
+    final currentImage = _currentImage;
+    if (currentImage == null) return;
+
+    setState(() {
+      _isLoading = true;
+    });
+
+    try {
+      AppLogger.d('开始去除 EXIF...');
+      final cleanedFile = await MediaService.removeExif(currentImage);
+      
+      if (!mounted) return;
+      final l10n = AppLocalizations.of(context)!;
+      setState(() {
+        // 更新对应的图片变量
+        if (_selectedImage != null) {
+          _selectedImage = cleanedFile;
+        } else if (_cameraImage != null) {
+          _cameraImage = cleanedFile;
+        }
+      });
+      
+      ToastUtils.showSuccess(l10n.removeExifSuccess);
+      AppLogger.d('EXIF 去除成功: ${cleanedFile.path}');
+    } on MediaException catch (e) {
+      AppLogger.e('去除 EXIF 失败', e);
+      ToastUtils.showError(e.message);
+    } catch (e) {
+      AppLogger.e('去除 EXIF 异常', e);
+      ToastUtils.showError('去除 EXIF 失败: ${e.toString()}');
+    } finally {
+      setState(() {
+        _isLoading = false;
+      });
+    }
+  }
+
+  /// 测试压缩图片
+  Future<void> _testCompressImage() async {
+    final currentImage = _currentImage;
+    if (currentImage == null) return;
+
+    setState(() {
+      _isLoading = true;
+    });
+
+    try {
+      AppLogger.d('开始压缩图片...');
+      if (!mounted) return;
+      final l10n = AppLocalizations.of(context)!;
+      final originalSize = await currentImage.length();
+      final compressedFile = await MediaService.compressImage(currentImage);
+      final compressedSize = await compressedFile.length();
+      
+      setState(() {
+        // 更新对应的图片变量
+        if (_selectedImage != null) {
+          _selectedImage = compressedFile;
+        } else if (_cameraImage != null) {
+          _cameraImage = compressedFile;
+        }
+      });
+      
+      final originalSizeKB = (originalSize / 1024).toStringAsFixed(2);
+      final compressedSizeKB = (compressedSize / 1024).toStringAsFixed(2);
+      final compressionRatio = ((1 - compressedSize / originalSize) * 100).toStringAsFixed(1);
+      
+      final compressInfo = '${l10n.originalSize}: $originalSizeKB ${l10n.kilobytes}\n'
+          '${l10n.compressedSize}: $compressedSizeKB ${l10n.kilobytes}\n'
+          '${l10n.compressionRatio}: $compressionRatio%';
+      
+      if (!mounted) return;
+      
+      // 使用 Alert 弹窗显示压缩信息
+      showDialog(
+        context: context,
+        builder: (context) => AlertDialog(
+          title: Text(l10n.compressImageSuccess),
+          content: Text(
+            compressInfo,
+            style: const TextStyle(
+              fontSize: 14,
+              fontFamily: 'monospace',
+            ),
+          ),
+          actions: [
+            TextButton(
+              onPressed: () => Navigator.of(context).pop(),
+              child: Text(l10n.confirm),
+            ),
+          ],
+        ),
+      );
+      
+      ToastUtils.showSuccess(l10n.compressImageSuccess);
+      AppLogger.d('图片压缩成功: ${compressedFile.path}');
+    } on MediaException catch (e) {
+      AppLogger.e('压缩图片失败', e);
+      ToastUtils.showError(e.message);
+    } catch (e) {
+      AppLogger.e('压缩图片异常', e);
+      ToastUtils.showError('压缩图片失败: ${e.toString()}');
+    } finally {
+      setState(() {
+        _isLoading = false;
+      });
+    }
+  }
+
+  /// 清除所有图片
+  void _clearImages() {
+    final l10n = AppLocalizations.of(context)!;
+    setState(() {
+      _selectedImage = null;
+      _cameraImage = null;
+    });
+    ToastUtils.show(l10n.clearAllImagesSuccess);
+  }
+}
+

+ 14 - 1
lib/features/MinePage/presentation/mine_page.dart

@@ -27,7 +27,7 @@ class _MinePageState extends ConsumerState<MinePage>
         elevation: 0,
       ),
       body: ListView.builder(
-        itemCount: 2,
+        itemCount: 3,
         itemBuilder: (BuildContext context, int index) {
           if (index == 0) {
             return ListTile(
@@ -38,6 +38,13 @@ class _MinePageState extends ConsumerState<MinePage>
             );
           } else if (index == 1) {
             return ListTile(
+              leading: const Icon(Icons.camera_alt),
+              title: const Text('媒体功能测试'),
+              trailing: const Icon(Icons.chevron_right),
+              onTap: _handleMediaTest,
+            );
+          } else if (index == 2) {
+            return ListTile(
               leading: const Icon(Icons.logout, color: Colors.red),
               title: Text(
                 l10n.logout,
@@ -89,4 +96,10 @@ class _MinePageState extends ConsumerState<MinePage>
       );
     }
   }
+
+  void _handleMediaTest() {
+    if (mounted) {
+      context.push('/mediaTest');
+    }
+  }
 }

+ 210 - 0
lib/l10n/app_localizations.dart

@@ -327,6 +327,216 @@ abstract class AppLocalizations {
   /// In zh, this message translates to:
   /// **'记住密码'**
   String get rememberPassword;
+
+  /// 媒体功能测试页面标题
+  ///
+  /// In zh, this message translates to:
+  /// **'媒体功能测试'**
+  String get mediaTest;
+
+  /// 快速测试标题
+  ///
+  /// In zh, this message translates to:
+  /// **'快速测试(自动压缩)'**
+  String get quickTest;
+
+  /// 从相册选择图片按钮
+  ///
+  /// In zh, this message translates to:
+  /// **'从相册选择图片'**
+  String get pickFromGallery;
+
+  /// 使用系统相机拍照按钮
+  ///
+  /// In zh, this message translates to:
+  /// **'使用系统相机拍照'**
+  String get pickFromCamera;
+
+  /// 专业相机测试标题
+  ///
+  /// In zh, this message translates to:
+  /// **'专业相机测试'**
+  String get professionalCameraTest;
+
+  /// 使用专业相机拍照按钮
+  ///
+  /// In zh, this message translates to:
+  /// **'使用专业相机拍照'**
+  String get useProfessionalCamera;
+
+  /// 图片处理测试标题
+  ///
+  /// In zh, this message translates to:
+  /// **'图片处理测试'**
+  String get imageProcessingTest;
+
+  /// 获取图片信息按钮
+  ///
+  /// In zh, this message translates to:
+  /// **'获取图片信息'**
+  String get getImageInfo;
+
+  /// 去除 EXIF 按钮
+  ///
+  /// In zh, this message translates to:
+  /// **'去除 EXIF(医疗脱敏)'**
+  String get removeExif;
+
+  /// 压缩图片按钮
+  ///
+  /// In zh, this message translates to:
+  /// **'压缩图片'**
+  String get compressImage;
+
+  /// 清除所有图片按钮
+  ///
+  /// In zh, this message translates to:
+  /// **'清除所有图片'**
+  String get clearAllImages;
+
+  /// 选择的图片标题
+  ///
+  /// In zh, this message translates to:
+  /// **'选择的图片'**
+  String get selectedImage;
+
+  /// 相册或系统相机标签
+  ///
+  /// In zh, this message translates to:
+  /// **'相册/系统相机'**
+  String get galleryOrSystemCamera;
+
+  /// 专业相机标签
+  ///
+  /// In zh, this message translates to:
+  /// **'专业相机'**
+  String get professionalCamera;
+
+  /// 暂无图片提示
+  ///
+  /// In zh, this message translates to:
+  /// **'暂无图片'**
+  String get noImage;
+
+  /// 图片信息标题
+  ///
+  /// In zh, this message translates to:
+  /// **'图片信息'**
+  String get imageInfo;
+
+  /// 图片宽度标签
+  ///
+  /// In zh, this message translates to:
+  /// **'宽度'**
+  String get imageWidth;
+
+  /// 图片高度标签
+  ///
+  /// In zh, this message translates to:
+  /// **'高度'**
+  String get imageHeight;
+
+  /// 图片大小标签
+  ///
+  /// In zh, this message translates to:
+  /// **'大小'**
+  String get imageSize;
+
+  /// 图片格式标签
+  ///
+  /// In zh, this message translates to:
+  /// **'格式'**
+  String get imageFormat;
+
+  /// 相册选择成功提示
+  ///
+  /// In zh, this message translates to:
+  /// **'相册选择成功'**
+  String get galleryPickSuccess;
+
+  /// 拍照成功提示
+  ///
+  /// In zh, this message translates to:
+  /// **'拍照成功'**
+  String get cameraPhotoSuccess;
+
+  /// 专业相机拍照成功提示
+  ///
+  /// In zh, this message translates to:
+  /// **'专业相机拍照成功'**
+  String get professionalCameraSuccess;
+
+  /// 获取图片信息成功提示
+  ///
+  /// In zh, this message translates to:
+  /// **'获取图片信息成功'**
+  String get getImageInfoSuccess;
+
+  /// EXIF 去除成功提示
+  ///
+  /// In zh, this message translates to:
+  /// **'EXIF 去除成功(医疗脱敏完成)'**
+  String get removeExifSuccess;
+
+  /// 图片压缩成功提示
+  ///
+  /// In zh, this message translates to:
+  /// **'图片压缩成功'**
+  String get compressImageSuccess;
+
+  /// 清除所有图片成功提示
+  ///
+  /// In zh, this message translates to:
+  /// **'已清除所有图片'**
+  String get clearAllImagesSuccess;
+
+  /// 用户取消选择提示
+  ///
+  /// In zh, this message translates to:
+  /// **'用户取消了选择'**
+  String get userCancelledSelection;
+
+  /// 用户取消拍照提示
+  ///
+  /// In zh, this message translates to:
+  /// **'用户取消了拍照'**
+  String get userCancelledPhoto;
+
+  /// 专业相机就绪提示
+  ///
+  /// In zh, this message translates to:
+  /// **'相机已就绪,是否立即拍照?'**
+  String get cameraReady;
+
+  /// 原始大小标签
+  ///
+  /// In zh, this message translates to:
+  /// **'原始大小'**
+  String get originalSize;
+
+  /// 压缩后大小标签
+  ///
+  /// In zh, this message translates to:
+  /// **'压缩后'**
+  String get compressedSize;
+
+  /// 压缩率标签
+  ///
+  /// In zh, this message translates to:
+  /// **'压缩率'**
+  String get compressionRatio;
+
+  /// 像素单位
+  ///
+  /// In zh, this message translates to:
+  /// **'px'**
+  String get pixels;
+
+  /// 千字节单位
+  ///
+  /// In zh, this message translates to:
+  /// **'KB'**
+  String get kilobytes;
 }
 
 class _AppLocalizationsDelegate

+ 105 - 0
lib/l10n/app_localizations_zh.dart

@@ -126,4 +126,109 @@ class AppLocalizationsZh extends AppLocalizations {
 
   @override
   String get rememberPassword => '记住密码';
+
+  @override
+  String get mediaTest => '媒体功能测试';
+
+  @override
+  String get quickTest => '快速测试(自动压缩)';
+
+  @override
+  String get pickFromGallery => '从相册选择图片';
+
+  @override
+  String get pickFromCamera => '使用系统相机拍照';
+
+  @override
+  String get professionalCameraTest => '专业相机测试';
+
+  @override
+  String get useProfessionalCamera => '使用专业相机拍照';
+
+  @override
+  String get imageProcessingTest => '图片处理测试';
+
+  @override
+  String get getImageInfo => '获取图片信息';
+
+  @override
+  String get removeExif => '去除 EXIF(医疗脱敏)';
+
+  @override
+  String get compressImage => '压缩图片';
+
+  @override
+  String get clearAllImages => '清除所有图片';
+
+  @override
+  String get selectedImage => '选择的图片';
+
+  @override
+  String get galleryOrSystemCamera => '相册/系统相机';
+
+  @override
+  String get professionalCamera => '专业相机';
+
+  @override
+  String get noImage => '暂无图片';
+
+  @override
+  String get imageInfo => '图片信息';
+
+  @override
+  String get imageWidth => '宽度';
+
+  @override
+  String get imageHeight => '高度';
+
+  @override
+  String get imageSize => '大小';
+
+  @override
+  String get imageFormat => '格式';
+
+  @override
+  String get galleryPickSuccess => '相册选择成功';
+
+  @override
+  String get cameraPhotoSuccess => '拍照成功';
+
+  @override
+  String get professionalCameraSuccess => '专业相机拍照成功';
+
+  @override
+  String get getImageInfoSuccess => '获取图片信息成功';
+
+  @override
+  String get removeExifSuccess => 'EXIF 去除成功(医疗脱敏完成)';
+
+  @override
+  String get compressImageSuccess => '图片压缩成功';
+
+  @override
+  String get clearAllImagesSuccess => '已清除所有图片';
+
+  @override
+  String get userCancelledSelection => '用户取消了选择';
+
+  @override
+  String get userCancelledPhoto => '用户取消了拍照';
+
+  @override
+  String get cameraReady => '相机已就绪,是否立即拍照?';
+
+  @override
+  String get originalSize => '原始大小';
+
+  @override
+  String get compressedSize => '压缩后';
+
+  @override
+  String get compressionRatio => '压缩率';
+
+  @override
+  String get pixels => 'px';
+
+  @override
+  String get kilobytes => 'KB';
 }

+ 140 - 0
lib/l10n/app_zh.arb

@@ -160,6 +160,146 @@
   "rememberPassword": "记住密码",
   "@rememberPassword": {
     "description": "记住密码选项"
+  },
+  "mediaTest": "媒体功能测试",
+  "@mediaTest": {
+    "description": "媒体功能测试页面标题"
+  },
+  "quickTest": "快速测试(自动压缩)",
+  "@quickTest": {
+    "description": "快速测试标题"
+  },
+  "pickFromGallery": "从相册选择图片",
+  "@pickFromGallery": {
+    "description": "从相册选择图片按钮"
+  },
+  "pickFromCamera": "使用系统相机拍照",
+  "@pickFromCamera": {
+    "description": "使用系统相机拍照按钮"
+  },
+  "professionalCameraTest": "专业相机测试",
+  "@professionalCameraTest": {
+    "description": "专业相机测试标题"
+  },
+  "useProfessionalCamera": "使用专业相机拍照",
+  "@useProfessionalCamera": {
+    "description": "使用专业相机拍照按钮"
+  },
+  "imageProcessingTest": "图片处理测试",
+  "@imageProcessingTest": {
+    "description": "图片处理测试标题"
+  },
+  "getImageInfo": "获取图片信息",
+  "@getImageInfo": {
+    "description": "获取图片信息按钮"
+  },
+  "removeExif": "去除 EXIF(医疗脱敏)",
+  "@removeExif": {
+    "description": "去除 EXIF 按钮"
+  },
+  "compressImage": "压缩图片",
+  "@compressImage": {
+    "description": "压缩图片按钮"
+  },
+  "clearAllImages": "清除所有图片",
+  "@clearAllImages": {
+    "description": "清除所有图片按钮"
+  },
+  "selectedImage": "选择的图片",
+  "@selectedImage": {
+    "description": "选择的图片标题"
+  },
+  "galleryOrSystemCamera": "相册/系统相机",
+  "@galleryOrSystemCamera": {
+    "description": "相册或系统相机标签"
+  },
+  "professionalCamera": "专业相机",
+  "@professionalCamera": {
+    "description": "专业相机标签"
+  },
+  "noImage": "暂无图片",
+  "@noImage": {
+    "description": "暂无图片提示"
+  },
+  "imageInfo": "图片信息",
+  "@imageInfo": {
+    "description": "图片信息标题"
+  },
+  "imageWidth": "宽度",
+  "@imageWidth": {
+    "description": "图片宽度标签"
+  },
+  "imageHeight": "高度",
+  "@imageHeight": {
+    "description": "图片高度标签"
+  },
+  "imageSize": "大小",
+  "@imageSize": {
+    "description": "图片大小标签"
+  },
+  "imageFormat": "格式",
+  "@imageFormat": {
+    "description": "图片格式标签"
+  },
+  "galleryPickSuccess": "相册选择成功",
+  "@galleryPickSuccess": {
+    "description": "相册选择成功提示"
+  },
+  "cameraPhotoSuccess": "拍照成功",
+  "@cameraPhotoSuccess": {
+    "description": "拍照成功提示"
+  },
+  "professionalCameraSuccess": "专业相机拍照成功",
+  "@professionalCameraSuccess": {
+    "description": "专业相机拍照成功提示"
+  },
+  "getImageInfoSuccess": "获取图片信息成功",
+  "@getImageInfoSuccess": {
+    "description": "获取图片信息成功提示"
+  },
+  "removeExifSuccess": "EXIF 去除成功(医疗脱敏完成)",
+  "@removeExifSuccess": {
+    "description": "EXIF 去除成功提示"
+  },
+  "compressImageSuccess": "图片压缩成功",
+  "@compressImageSuccess": {
+    "description": "图片压缩成功提示"
+  },
+  "clearAllImagesSuccess": "已清除所有图片",
+  "@clearAllImagesSuccess": {
+    "description": "清除所有图片成功提示"
+  },
+  "userCancelledSelection": "用户取消了选择",
+  "@userCancelledSelection": {
+    "description": "用户取消选择提示"
+  },
+  "userCancelledPhoto": "用户取消了拍照",
+  "@userCancelledPhoto": {
+    "description": "用户取消拍照提示"
+  },
+  "cameraReady": "相机已就绪,是否立即拍照?",
+  "@cameraReady": {
+    "description": "专业相机就绪提示"
+  },
+  "originalSize": "原始大小",
+  "@originalSize": {
+    "description": "原始大小标签"
+  },
+  "compressedSize": "压缩后",
+  "@compressedSize": {
+    "description": "压缩后大小标签"
+  },
+  "compressionRatio": "压缩率",
+  "@compressionRatio": {
+    "description": "压缩率标签"
+  },
+  "pixels": "px",
+  "@pixels": {
+    "description": "像素单位"
+  },
+  "kilobytes": "KB",
+  "@kilobytes": {
+    "description": "千字节单位"
   }
 }
 

+ 16 - 0
lib/media/camera_service.dart

@@ -1,6 +1,8 @@
 import 'dart:io';
 import 'package:camera/camera.dart' as camera_pkg;
 import 'package:sino_med_cloud/core/utils/logger.dart';
+import 'package:sino_med_cloud/core/utils/toast_utils.dart';
+import 'package:sino_med_cloud/permission/permission_service.dart';
 import 'media_exception.dart';
 
 // 使用别名避免命名冲突
@@ -47,6 +49,20 @@ class CameraService {
         return;
       }
 
+      // 检查相机权限
+      final permissionResult = await PermissionService.request(PermissionType.camera);
+      if (permissionResult != PermissionResult.granted) {
+        if (permissionResult == PermissionResult.permanentlyDenied) {
+          ToastUtils.showError('相机权限被永久拒绝,请在设置中开启相机权限');
+          // 自动打开设置页面
+          await PermissionService.openSetting();
+          throw MediaCameraException('相机权限被永久拒绝,已引导用户前往设置');
+        } else {
+          ToastUtils.showError('需要相机权限才能使用相机');
+          throw MediaCameraException('相机权限被拒绝');
+        }
+      }
+
       AppLogger.d('开始初始化相机...');
       final cameras = await camera_pkg.availableCameras();
       

+ 44 - 0
lib/media/picker_service.dart

@@ -1,6 +1,8 @@
 import 'dart:io';
 import 'package:image_picker/image_picker.dart';
 import 'package:sino_med_cloud/core/utils/logger.dart';
+import 'package:sino_med_cloud/core/utils/toast_utils.dart';
+import 'package:sino_med_cloud/permission/permission_service.dart';
 import 'media_exception.dart';
 
 /// 相册和快速拍照服务
@@ -36,6 +38,20 @@ class PickerService {
     double? maxHeight,
   }) async {
     try {
+      // 检查相册权限
+      final permissionResult = await PermissionService.request(PermissionType.gallery);
+      if (permissionResult != PermissionResult.granted) {
+        if (permissionResult == PermissionResult.permanentlyDenied) {
+          ToastUtils.showError('相册权限被永久拒绝,请在设置中开启相册权限');
+          // 自动打开设置页面
+          await PermissionService.openSetting();
+          throw MediaPermissionException('相册权限被永久拒绝,已引导用户前往设置');
+        } else {
+          ToastUtils.showError('需要相册权限才能选择图片');
+          throw MediaPermissionException('相册权限被拒绝');
+        }
+      }
+
       AppLogger.d('开始从相册选择图片...');
       final XFile? file = await _picker.pickImage(
         source: ImageSource.gallery,
@@ -88,6 +104,20 @@ class PickerService {
     double? maxHeight,
   }) async {
     try {
+      // 检查相机权限
+      final permissionResult = await PermissionService.request(PermissionType.camera);
+      if (permissionResult != PermissionResult.granted) {
+        if (permissionResult == PermissionResult.permanentlyDenied) {
+          ToastUtils.showError('相机权限被永久拒绝,请在设置中开启相机权限');
+          // 自动打开设置页面
+          await PermissionService.openSetting();
+          throw MediaPermissionException('相机权限被永久拒绝,已引导用户前往设置');
+        } else {
+          ToastUtils.showError('需要相机权限才能拍照');
+          throw MediaPermissionException('相机权限被拒绝');
+        }
+      }
+
       AppLogger.d('开始使用系统相机拍照...');
       final XFile? file = await _picker.pickImage(
         source: ImageSource.camera,
@@ -132,6 +162,20 @@ class PickerService {
   /// 抛出 [MediaException] 如果选择失败
   static Future<File?> pickVideoFromGallery() async {
     try {
+      // 检查相册权限
+      final permissionResult = await PermissionService.request(PermissionType.gallery);
+      if (permissionResult != PermissionResult.granted) {
+        if (permissionResult == PermissionResult.permanentlyDenied) {
+          ToastUtils.showError('相册权限被永久拒绝,请在设置中开启相册权限');
+          // 自动打开设置页面
+          await PermissionService.openSetting();
+          throw MediaPermissionException('相册权限被永久拒绝,已引导用户前往设置');
+        } else {
+          ToastUtils.showError('需要相册权限才能选择视频');
+          throw MediaPermissionException('相册权限被拒绝');
+        }
+      }
+
       AppLogger.d('开始从相册选择视频...');
       final XFile? file = await _picker.pickVideo(
         source: ImageSource.gallery,

+ 71 - 0
lib/permission/permission_service.dart

@@ -0,0 +1,71 @@
+import 'dart:io';
+import 'package:permission_handler/permission_handler.dart';
+
+enum PermissionResult {
+  granted,
+  denied,
+  permanentlyDenied,
+}
+
+enum PermissionType {
+  camera,
+  gallery,
+  microphone,
+  bluetooth,
+}
+
+class PermissionService {
+  /// 对外统一入口
+  static Future<PermissionResult> request(
+      PermissionType type,
+      ) async {
+    final permission = _mapPermission(type);
+    final status = await permission.status;
+
+    if (status.isGranted) {
+      return PermissionResult.granted;
+    }
+
+    final result = await permission.request();
+
+    if (result.isGranted) {
+      return PermissionResult.granted;
+    }
+
+    if (result.isPermanentlyDenied) {
+      return PermissionResult.permanentlyDenied;
+    }
+
+    return PermissionResult.denied;
+  }
+
+
+  /// 跳转系统设置
+  static Future<void> openSetting() async {
+    await openAppSettings();
+  }
+
+  /// 权限映射(平台差异处理)
+  static Permission _mapPermission(PermissionType type) {
+    switch (type) {
+      case PermissionType.camera:
+        return Permission.camera;
+
+      case PermissionType.microphone:
+        return Permission.microphone;
+
+      case PermissionType.gallery:
+        if (Platform.isAndroid) {
+          return Permission.photos;
+        }
+        return Permission.photos;
+
+      case PermissionType.bluetooth:
+        if (Platform.isAndroid) {
+          return Permission.bluetoothConnect;
+        }
+        return Permission.bluetooth;
+    }
+  }
+
+}