diff --git a/app/lib/widgets/offline_banner.dart b/app/lib/widgets/offline_banner.dart new file mode 100644 index 0000000..5eac80f --- /dev/null +++ b/app/lib/widgets/offline_banner.dart @@ -0,0 +1,102 @@ +// 全局离线提示横幅 — 监听 connectivity_plus 显示/隐藏 +// +// 放在 Scaffold body 上方,离线时显示黄色警告横幅 +// 使用: 在 responsive_scaffold 的 body 上方嵌套 + +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:flutter/material.dart'; + +import '../core/constants/design_tokens.dart'; +import '../core/theme/app_colors.dart'; + +/// 全局离线提示横幅 — 自动监听网络状态 +class OfflineBanner extends StatefulWidget { + const OfflineBanner({super.key, required this.child}); + + final Widget child; + + @override + State createState() => _OfflineBannerState(); +} + +class _OfflineBannerState extends State { + bool _isOffline = false; + + @override + void initState() { + super.initState(); + // 初始检查 + Connectivity().checkConnectivity().then((result) { + if (mounted) { + setState(() { + _isOffline = result.every((r) => r == ConnectivityResult.none); + }); + } + }); + // 监听变化 + Connectivity().onConnectivityChanged.listen((result) { + if (mounted) { + final offline = result.every((r) => r == ConnectivityResult.none); + if (offline != _isOffline) { + setState(() => _isOffline = offline); + } + } + }); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + // 离线横幅 — 带动画滑入/滑出 + AnimatedCrossFade( + firstChild: const SizedBox.shrink(), + secondChild: _OfflineBar(), + crossFadeState: _isOffline + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, + duration: DesignTokens.animNormal, + sizeCurve: DesignTokens.warmCurve, + ), + // 正常内容 + Expanded(child: widget.child), + ], + ); + } +} + +/// 离线横幅条 +class _OfflineBar extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: AppColors.warning.withValues(alpha: 0.15), + border: Border( + bottom: BorderSide( + color: AppColors.warning.withValues(alpha: 0.3), + width: 1, + ), + ), + ), + child: Row( + children: [ + Icon(Icons.wifi_off_rounded, size: 16, color: AppColors.warning), + const SizedBox(width: 8), + Expanded( + child: Text( + '网络不可用,部分功能受限', + style: TextStyle( + fontSize: 13, + color: AppColors.warning, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ); + } +} diff --git a/app/lib/widgets/responsive_scaffold.dart b/app/lib/widgets/responsive_scaffold.dart index 422fbfe..8e74b3c 100644 --- a/app/lib/widgets/responsive_scaffold.dart +++ b/app/lib/widgets/responsive_scaffold.dart @@ -9,6 +9,7 @@ import '../core/constants/breakpoints.dart'; import '../core/theme/app_colors.dart'; import '../core/theme/app_typography.dart'; import '../core/theme/app_radius.dart'; +import 'offline_banner.dart'; /// 导航项数量(不含中心 FAB) const int kNavItemCount = 4; @@ -167,7 +168,7 @@ class _MobileLayout extends StatelessWidget { appBar: appBarTitle != null ? AppBar(title: Text(appBarTitle!)) : null, - body: body, + body: OfflineBanner(child: body), extendBody: true, // 允许内容延伸到 Tab 栏下面(圆角透明效果) bottomNavigationBar: _BottomNavBar( selectedIndex: selectedIndex,