Bladeren bron

网络状态监控。

PC\19500 2 weken geleden
bovenliggende
commit
4e5e23667c

+ 5 - 0
android/app/build.gradle.kts

@@ -37,6 +37,11 @@ android {
             signingConfig = signingConfigs.getByName("debug")
         }
     }
+
+    lint {
+        checkReleaseBuilds = false
+        abortOnError = false
+    }
 }
 
 flutter {

+ 6 - 1
android/gradle.properties

@@ -1,2 +1,7 @@
-org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m -XX:ReservedCodeCacheSize=256m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
+org.gradle.jvmargs=-Xmx1024m -XX:MaxMetaspaceSize=512m -XX:ReservedCodeCacheSize=240m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:+UseG1GC -XX:MaxGCPauseMillis=200
 android.useAndroidX=true
+org.gradle.parallel=true
+org.gradle.caching=true
+org.gradle.configureondemand=true
+# 禁用守护进程可能有助于减少长期内存占用,但会使构建变慢。先尝试开启,如果不行再手动关闭。
+org.gradle.daemon=true

+ 96 - 10
lib/app/app.dart

@@ -1,22 +1,108 @@
 import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:sino_med_cloud/core/network/network_provider.dart';
 import 'package:sino_med_cloud/l10n/app_localizations.dart';
+import '../core/constants/app_enum.dart';
 import 'router.dart';
 import 'theme.dart';
 
-class SinoMedApp extends StatelessWidget {
+class SinoMedApp extends ConsumerStatefulWidget {
   const SinoMedApp({super.key});
 
   @override
+  ConsumerState<SinoMedApp> createState() => _SinoMedAppState();
+}
+
+class _SinoMedAppState extends ConsumerState<SinoMedApp> {
+  @override
   Widget build(BuildContext context) {
-    return MaterialApp.router(
-      title: '中方诊药云',
-      theme: AppTheme.light,
-      routerConfig: AppRouter.router,
-      debugShowCheckedModeBanner: false,
-      // 本地化配置
-      localizationsDelegates: AppLocalizations.localizationsDelegates,
-      supportedLocales: AppLocalizations.supportedLocales,
-      locale: const Locale('zh'), // 默认中文
+    // 监听网络状态变化并自动导航
+    // ref.listen 必须在 build 方法中使用
+    ref.listen(networkStatusProvider, (previous, next) {
+      next.whenData((status) {
+        final router = AppRouter.router;
+        try {
+          // 获取当前路由路径
+          // 注意:如果 router 还没准备好,这里可能会报错,所以加了 try-catch
+          final matchList = router.routerDelegate.currentConfiguration.matches;
+          final currentLocation = matchList.isNotEmpty 
+              ? matchList.last.matchedLocation 
+              : router.routerDelegate.currentConfiguration.uri.path;
+          
+          // 判断网络是否正常(WiFi 和移动网络都视为正常)
+          final isNetworkAvailable = status == NetworkStatus.wifi || status == NetworkStatus.mobile;
+          
+          if (isNetworkAvailable) {
+            // 网络正常:如果当前在无网络页面,则返回到之前保存的页面
+            if (currentLocation == '/noNetwork') {
+              final previousPath = ref.read(previousPagePathProvider);
+              final targetPath = previousPath ?? '/'; // 如果没有保存的页面,则返回登录页
+              
+              // 使用 addPostFrameCallback 避免在构建过程中导航
+              WidgetsBinding.instance.addPostFrameCallback((_) {
+                if (mounted) {
+                  router.go(targetPath);
+                  // 清除保存的页面路径
+                  ref.read(previousPagePathProvider.notifier).state = null;
+                }
+              });
+            }
+            // 如果网络正常且不在无网络页面,则停留在当前页面
+          } else {
+            // 网络不正常(无网络):保存当前页面并导航到无网络页面
+            if (currentLocation != '/noNetwork') {
+              // 保存当前页面路径(如果当前不在无网络页面)
+              ref.read(previousPagePathProvider.notifier).state = currentLocation;
+              
+              WidgetsBinding.instance.addPostFrameCallback((_) {
+                if (mounted) {
+                  router.go('/noNetwork');
+                }
+              });
+            }
+          }
+        } catch (e) {
+          // 如果无法获取当前路由,根据网络状态直接处理
+          final isNetworkAvailable = status == NetworkStatus.wifi || status == NetworkStatus.mobile;
+          if (!isNetworkAvailable) {
+            WidgetsBinding.instance.addPostFrameCallback((_) {
+              if (mounted) {
+                router.go('/noNetwork');
+              }
+            });
+          }
+        }
+      });
+    });
+
+    final networkAsync = ref.watch(networkStatusProvider);
+
+    return networkAsync.when(
+      loading: () {
+        // Loading 状态显示加载指示器
+        return const MaterialApp(
+          debugShowCheckedModeBanner: false,
+          home: Scaffold(
+            body: Center(child: CircularProgressIndicator()),
+          ),
+        );
+      },
+      error: (error, _) {
+        return _materialApp;
+      },
+      data: (_) {
+        return _materialApp;
+      },
     );
   }
+
+  MaterialApp get _materialApp => MaterialApp.router(
+    onGenerateTitle: (context) => AppLocalizations.of(context)?.appSubtitle ?? '中方诊药云',
+    theme: AppTheme.light,
+    routerConfig: AppRouter.router,
+    debugShowCheckedModeBanner: false,
+    localizationsDelegates: AppLocalizations.localizationsDelegates,
+    supportedLocales: AppLocalizations.supportedLocales,
+    locale: const Locale('zh'),
+  );
 }

+ 35 - 26
lib/app/router.dart

@@ -1,8 +1,10 @@
 import 'package:flutter/material.dart';
 import 'package:go_router/go_router.dart';
 import 'package:sino_med_cloud/features/main_tab_page.dart';
+import 'package:sino_med_cloud/l10n/app_localizations.dart';
 import '../features/auth/presentation/login_page.dart';
 import '../features/MediaTestPage/presentation/media_test_page.dart';
+import '../core/network/pages/no_network_page.dart';
 
 class AppRouter {
   static final router = GoRouter(
@@ -19,33 +21,40 @@ class AppRouter {
         path: '/mediaTest',
         builder: (context, state) => const MediaTestPage(),
       ),
-    ],
-    errorBuilder: (context, state) => Scaffold(
-      appBar: AppBar(
-        title: const Text('页面未找到'),
+      GoRoute(
+        path: '/noNetwork',
+        builder: (context, state) => const NoNetworkPage(),
       ),
-      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('返回首页'),
-            ),
-          ],
+    ],
+    errorBuilder: (context, state) {
+      final l10n = AppLocalizations.of(context)!;
+      return Scaffold(
+        appBar: AppBar(
+          title: Text(l10n.pageNotFound),
         ),
-      ),
-    ),
+        body: Center(
+          child: Column(
+            mainAxisAlignment: MainAxisAlignment.center,
+            children: [
+              const Icon(
+                Icons.error_outline,
+                size: 64,
+                color: Colors.grey,
+              ),
+              const SizedBox(height: 16),
+              Text(
+                l10n.pageNotFoundWithUri(state.uri.toString()),
+                style: const TextStyle(fontSize: 16),
+              ),
+              const SizedBox(height: 16),
+              ElevatedButton(
+                onPressed: () => context.go('/'),
+                child: Text(l10n.returnHome),
+              ),
+            ],
+          ),
+        ),
+      );
+    },
   );
 }

+ 26 - 0
lib/core/constants/app_enum.dart

@@ -0,0 +1,26 @@
+/// 应用枚举定义
+/// 
+/// 集中管理所有在多个文件中使用的枚举类型
+library;
+
+/// 网络状态
+enum NetworkStatus {
+  wifi,
+  mobile,
+  none,
+}
+
+/// 权限请求结果
+enum PermissionResult {
+  granted,
+  denied,
+  permanentlyDenied,
+}
+
+/// 权限类型
+enum PermissionType {
+  camera,
+  gallery,
+  microphone,
+  bluetooth,
+}

+ 18 - 0
lib/core/network/network_provider.dart

@@ -0,0 +1,18 @@
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:flutter_riverpod/legacy.dart';
+import '../constants/app_enum.dart';
+import 'network_service.dart';
+
+/// Service Provider
+final networkServiceProvider = Provider<NetworkService>((ref) {
+  return NetworkService();
+});
+
+/// 网络状态 StreamProvider
+final networkStatusProvider = StreamProvider<NetworkStatus>((ref) {
+  final service = ref.read(networkServiceProvider);
+  return service.watchNetwork().distinct();
+});
+
+/// 保存网络断开前的页面路径
+final previousPagePathProvider = StateProvider<String?>((ref) => null);

+ 29 - 0
lib/core/network/network_service.dart

@@ -0,0 +1,29 @@
+import 'package:connectivity_plus/connectivity_plus.dart';
+
+import '../constants/app_enum.dart';
+
+class NetworkService {
+  final Connectivity _connectivity = Connectivity();
+
+  Stream<NetworkStatus> watchNetwork() async* {
+    // 初始状态
+    final initial = await _connectivity.checkConnectivity();
+    yield _map(initial);
+    // 持续监听
+    await for (final result
+    in _connectivity.onConnectivityChanged) {
+      yield _map(result);
+    }
+  }
+
+  NetworkStatus _map(List<ConnectivityResult> results) {
+    // 优先返回 wifi,其次是 mobile,最后是 none
+    if (results.contains(ConnectivityResult.wifi)) {
+      return NetworkStatus.wifi;
+    }
+    if (results.contains(ConnectivityResult.mobile)) {
+      return NetworkStatus.mobile;
+    }
+    return NetworkStatus.none;
+  }
+}

+ 23 - 0
lib/core/network/pages/no_network_page.dart

@@ -0,0 +1,23 @@
+import 'package:flutter/material.dart';
+import 'package:sino_med_cloud/l10n/app_localizations.dart';
+
+class NoNetworkPage extends StatelessWidget {
+  const NoNetworkPage({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    final l10n = AppLocalizations.of(context)!;
+    return Scaffold(
+      body: Center(
+          child: Column(
+              mainAxisAlignment: MainAxisAlignment.center,
+              children: [
+                Icon(Icons.wifi_off, size: 64),
+                SizedBox(height: 16),
+                Text(l10n.noNetworkConnection),
+              ]
+          )
+      ),
+    );
+  }
+}

+ 4 - 2
lib/features/HomePage/presentation/home_page.dart

@@ -1,5 +1,6 @@
 import 'package:flutter/material.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:sino_med_cloud/l10n/app_localizations.dart';
 
 class HomePage extends ConsumerStatefulWidget {
   const HomePage({super.key});
@@ -12,8 +13,9 @@ class _HomePageState extends ConsumerState<HomePage>
     with SingleTickerProviderStateMixin {
   @override
   Widget build(BuildContext context) {
-    return const Center(
-      child: Text('首页'),
+    final l10n = AppLocalizations.of(context)!;
+    return Center(
+      child: Text(l10n.home),
     );
   }
   

+ 4 - 2
lib/features/HospitalPage/presentation/hospital_page.dart

@@ -1,5 +1,6 @@
 import 'package:flutter/material.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:sino_med_cloud/l10n/app_localizations.dart';
 
 class HospitalPage extends ConsumerStatefulWidget {
   const HospitalPage({super.key});
@@ -12,8 +13,9 @@ class _HospitalPageState extends ConsumerState<HospitalPage>
     with SingleTickerProviderStateMixin {
   @override
   Widget build(BuildContext context) {
-    return const Center(
-      child: Text('医院'),
+    final l10n = AppLocalizations.of(context)!;
+    return Center(
+      child: Text(l10n.hospital),
     );
   }
 

+ 4 - 2
lib/features/MallPage/presentation/mall_page.dart

@@ -1,5 +1,6 @@
 import 'package:flutter/material.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:sino_med_cloud/l10n/app_localizations.dart';
 
 class MallPage extends ConsumerStatefulWidget {
   const MallPage({super.key});
@@ -12,8 +13,9 @@ class _MallPageState extends ConsumerState<MallPage>
     with SingleTickerProviderStateMixin {
   @override
   Widget build(BuildContext context) {
-    return const Center(
-      child: Text('商城'),
+    final l10n = AppLocalizations.of(context)!;
+    return Center(
+      child: Text(l10n.mall),
     );
   }
 

+ 4 - 3
lib/features/MediaTestPage/presentation/media_test_page.dart

@@ -249,11 +249,12 @@ class _MediaTestPageState extends State<MediaTestPage> {
           imageFile,
           fit: BoxFit.contain,
           errorBuilder: (context, error, stackTrace) {
+            final l10n = AppLocalizations.of(context)!;
             return Container(
               height: 200,
               color: Colors.grey[200],
-              child: const Center(
-                child: Text('图片加载失败'),
+              child: Center(
+                child: Text(l10n.imageLoadFailed),
               ),
             );
           },
@@ -368,7 +369,7 @@ class _MediaTestPageState extends State<MediaTestPage> {
             ),
             ElevatedButton(
               onPressed: () => Navigator.of(context).pop(true),
-              child: const Text('拍照'),
+              child: Text(l10n.takePhoto),
             ),
           ],
         ),

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

@@ -39,7 +39,7 @@ class _MinePageState extends ConsumerState<MinePage>
           } else if (index == 1) {
             return ListTile(
               leading: const Icon(Icons.camera_alt),
-              title: const Text('媒体功能测试'),
+              title: Text(l10n.mediaTest),
               trailing: const Icon(Icons.chevron_right),
               onTap: _handleMediaTest,
             );

+ 72 - 0
lib/l10n/app_localizations.dart

@@ -537,6 +537,78 @@ abstract class AppLocalizations {
   /// In zh, this message translates to:
   /// **'KB'**
   String get kilobytes;
+
+  /// 移动网络警告提示
+  ///
+  /// In zh, this message translates to:
+  /// **'当前为移动网络\n部分功能受限'**
+  String get mobileNetworkWarning;
+
+  /// 无网络连接提示
+  ///
+  /// In zh, this message translates to:
+  /// **'当前无网络连接'**
+  String get noNetworkConnection;
+
+  /// 页面未找到标题
+  ///
+  /// In zh, this message translates to:
+  /// **'页面未找到'**
+  String get pageNotFound;
+
+  /// 页面未找到提示(带URI)
+  ///
+  /// In zh, this message translates to:
+  /// **'页面未找到: {uri}'**
+  String pageNotFoundWithUri(String uri);
+
+  /// 返回首页按钮
+  ///
+  /// In zh, this message translates to:
+  /// **'返回首页'**
+  String get returnHome;
+
+  /// 首页标签
+  ///
+  /// In zh, this message translates to:
+  /// **'首页'**
+  String get home;
+
+  /// 医院标签
+  ///
+  /// In zh, this message translates to:
+  /// **'医院'**
+  String get hospital;
+
+  /// 商城标签
+  ///
+  /// In zh, this message translates to:
+  /// **'商城'**
+  String get mall;
+
+  /// 图片加载失败提示
+  ///
+  /// In zh, this message translates to:
+  /// **'图片加载失败'**
+  String get imageLoadFailed;
+
+  /// 拍照按钮
+  ///
+  /// In zh, this message translates to:
+  /// **'拍照'**
+  String get takePhoto;
+
+  /// 重试按钮
+  ///
+  /// In zh, this message translates to:
+  /// **'重试'**
+  String get retry;
+
+  /// 加载失败提示
+  ///
+  /// In zh, this message translates to:
+  /// **'加载失败'**
+  String get loadFailed;
 }
 
 class _AppLocalizationsDelegate

+ 38 - 0
lib/l10n/app_localizations_zh.dart

@@ -231,4 +231,42 @@ class AppLocalizationsZh extends AppLocalizations {
 
   @override
   String get kilobytes => 'KB';
+
+  @override
+  String get mobileNetworkWarning => '当前为移动网络\n部分功能受限';
+
+  @override
+  String get noNetworkConnection => '当前无网络连接';
+
+  @override
+  String get pageNotFound => '页面未找到';
+
+  @override
+  String pageNotFoundWithUri(String uri) {
+    return '页面未找到: $uri';
+  }
+
+  @override
+  String get returnHome => '返回首页';
+
+  @override
+  String get home => '首页';
+
+  @override
+  String get hospital => '医院';
+
+  @override
+  String get mall => '商城';
+
+  @override
+  String get imageLoadFailed => '图片加载失败';
+
+  @override
+  String get takePhoto => '拍照';
+
+  @override
+  String get retry => '重试';
+
+  @override
+  String get loadFailed => '加载失败';
 }

+ 53 - 0
lib/l10n/app_zh.arb

@@ -300,6 +300,59 @@
   "kilobytes": "KB",
   "@kilobytes": {
     "description": "千字节单位"
+  },
+  "mobileNetworkWarning": "当前为移动网络\n部分功能受限",
+  "@mobileNetworkWarning": {
+    "description": "移动网络警告提示"
+  },
+  "noNetworkConnection": "当前无网络连接",
+  "@noNetworkConnection": {
+    "description": "无网络连接提示"
+  },
+  "pageNotFound": "页面未找到",
+  "@pageNotFound": {
+    "description": "页面未找到标题"
+  },
+  "pageNotFoundWithUri": "页面未找到: {uri}",
+  "@pageNotFoundWithUri": {
+    "description": "页面未找到提示(带URI)",
+    "placeholders": {
+      "uri": {
+        "type": "String"
+      }
+    }
+  },
+  "returnHome": "返回首页",
+  "@returnHome": {
+    "description": "返回首页按钮"
+  },
+  "home": "首页",
+  "@home": {
+    "description": "首页标签"
+  },
+  "hospital": "医院",
+  "@hospital": {
+    "description": "医院标签"
+  },
+  "mall": "商城",
+  "@mall": {
+    "description": "商城标签"
+  },
+  "imageLoadFailed": "图片加载失败",
+  "@imageLoadFailed": {
+    "description": "图片加载失败提示"
+  },
+  "takePhoto": "拍照",
+  "@takePhoto": {
+    "description": "拍照按钮"
+  },
+  "retry": "重试",
+  "@retry": {
+    "description": "重试按钮"
+  },
+  "loadFailed": "加载失败",
+  "@loadFailed": {
+    "description": "加载失败提示"
   }
 }
 

+ 1 - 0
lib/media/camera_service.dart

@@ -2,6 +2,7 @@ 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/core/constants/app_enum.dart';
 import 'package:sino_med_cloud/permission/permission_service.dart';
 import 'media_exception.dart';
 

+ 1 - 0
lib/media/picker_service.dart

@@ -2,6 +2,7 @@ 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/core/constants/app_enum.dart';
 import 'package:sino_med_cloud/permission/permission_service.dart';
 import 'media_exception.dart';
 

+ 1 - 13
lib/permission/permission_service.dart

@@ -1,18 +1,6 @@
 import 'dart:io';
 import 'package:permission_handler/permission_handler.dart';
-
-enum PermissionResult {
-  granted,
-  denied,
-  permanentlyDenied,
-}
-
-enum PermissionType {
-  camera,
-  gallery,
-  microphone,
-  bluetooth,
-}
+import 'package:sino_med_cloud/core/constants/app_enum.dart';
 
 class PermissionService {
   /// 对外统一入口

+ 4 - 2
lib/shared/widgets/error_view.dart

@@ -1,4 +1,5 @@
 import 'package:flutter/material.dart';
+import 'package:sino_med_cloud/l10n/app_localizations.dart';
 
 /// 错误状态视图组件
 class ErrorView extends StatelessWidget {
@@ -21,6 +22,7 @@ class ErrorView extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
+    final l10n = AppLocalizations.of(context)!;
     return Padding(
       padding: padding ?? const EdgeInsets.all(24.0),
       child: Center(
@@ -38,7 +40,7 @@ class ErrorView extends StatelessWidget {
             
             // 错误提示
             Text(
-              message ?? '加载失败',
+              message ?? l10n.loadFailed,
               style: Theme.of(context).textTheme.titleMedium?.copyWith(
                 color: Theme.of(context).colorScheme.onSurface,
               ),
@@ -65,7 +67,7 @@ class ErrorView extends StatelessWidget {
               ElevatedButton.icon(
                 onPressed: onRetry,
                 icon: const Icon(Icons.refresh),
-                label: const Text('重试'),
+                label: Text(l10n.retry),
                 style: ElevatedButton.styleFrom(
                   padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
                 ),