- 引导页协议多语言支持(languageId传递) - 登录页双书名号修复 + 注册页协议勾选 - 个人中心页面多语言(18个翻译键) - 网络断开提示增加关闭/刷新按钮 - 了解我们:新增秋叶qy开发者 + ayk签名修改 + 贡献者精简 + 微风暴微信搜索 - iOS快捷按钮重复修复(删除Info.plist静态定义) - 测试账号123456警告提示 - 扫码登录自动跳转(HTTP轮询+WebSocket双通道) - 登录页老用户按钮改次要色 - Syncfusion图表崩溃修复(DeferredBuilder+animationDuration:0) - macOS标题栏跟随软件夜间模式 - 平台兼容分发渠道弹窗 - 软件著作权图片+交叉水印 - 桌面小部件平台兼容说明默认收起 - iOS/macOS图标更新+名称确认为闲言 - 12个语言文件补全roleNative+7个分发渠道翻译字段
249 lines
7.5 KiB
Dart
249 lines
7.5 KiB
Dart
/// ============================================================
|
||
/// 闲言APP — 网络状态横幅组件
|
||
/// 创建时间: 2026-05-04
|
||
/// 更新时间: 2026-06-02
|
||
/// 作用: 在页面顶部显示网络状态提示(离线/网络异常),支持关闭和网络恢复自动隐藏
|
||
/// 上次更新: OfflineBanner增加刷新/关闭按钮,改为ConsumerStatefulWidget
|
||
/// ============================================================
|
||
|
||
import 'package:flutter/cupertino.dart';
|
||
import 'package:flutter_animate/flutter_animate.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
|
||
import '../../../core/network/connectivity_provider.dart';
|
||
import '../../../core/services/network/connectivity_provider.dart'
|
||
show connectivityCheckProvider;
|
||
import '../../../core/theme/app_spacing.dart';
|
||
import '../../../core/theme/app_typography.dart';
|
||
import 'app_toast.dart';
|
||
|
||
/// 网络状态横幅 — 显示在页面顶部
|
||
///
|
||
/// 两种状态:
|
||
/// - 离线:网络完全断开,显示"网络未连接"
|
||
/// - 异常:有网络但DNS不通(VPN等问题),显示"网络异常"
|
||
///
|
||
/// 特性:
|
||
/// - 可手动关闭(关闭后本次会话不再显示,直到网络状态变化)
|
||
/// - 网络恢复时自动隐藏
|
||
/// - iOS 风格,使用主题系统
|
||
class NetworkStatusBanner extends ConsumerStatefulWidget {
|
||
const NetworkStatusBanner({super.key});
|
||
|
||
@override
|
||
ConsumerState<NetworkStatusBanner> createState() =>
|
||
_NetworkStatusBannerState();
|
||
}
|
||
|
||
class _NetworkStatusBannerState extends ConsumerState<NetworkStatusBanner> {
|
||
bool _dismissed = false;
|
||
bool _wasOffline = false;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final isOffline = ref.isOffline;
|
||
final dnsCheck = ref.watch(connectivityCheckProvider);
|
||
final hasDnsIssue = dnsCheck.value == false;
|
||
|
||
final shouldShow = (isOffline || hasDnsIssue) && !_dismissed;
|
||
|
||
if (_wasOffline && !isOffline && !hasDnsIssue) {
|
||
_dismissed = false;
|
||
}
|
||
_wasOffline = isOffline || hasDnsIssue;
|
||
|
||
if (!shouldShow) return const SizedBox.shrink();
|
||
|
||
final isNoNetwork = isOffline;
|
||
final icon = isNoNetwork
|
||
? CupertinoIcons.wifi_slash
|
||
: CupertinoIcons.exclamationmark_triangle_fill;
|
||
final message = isNoNetwork ? '📡 网络未连接' : '⚠️ 网络异常,部分功能可能不可用';
|
||
final tintColor = isNoNetwork
|
||
? CupertinoColors.systemOrange
|
||
: CupertinoColors.systemYellow;
|
||
|
||
return Container(
|
||
width: double.infinity,
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: AppSpacing.md,
|
||
vertical: AppSpacing.sm,
|
||
),
|
||
decoration: BoxDecoration(
|
||
color: tintColor.withValues(alpha: 0.12),
|
||
border: Border(
|
||
bottom: BorderSide(
|
||
color: tintColor.withValues(alpha: 0.2),
|
||
width: 0.5,
|
||
),
|
||
),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Icon(icon, size: 14, color: tintColor.darkColor),
|
||
const SizedBox(width: AppSpacing.sm),
|
||
Expanded(
|
||
child: Text(
|
||
message,
|
||
style: AppTypography.caption1.copyWith(
|
||
color: tintColor.darkColor,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
),
|
||
GestureDetector(
|
||
onTap: () {
|
||
if (mounted) {
|
||
setState(() {
|
||
_dismissed = true;
|
||
});
|
||
}
|
||
},
|
||
child: Padding(
|
||
padding: const EdgeInsets.only(left: AppSpacing.sm),
|
||
child: Icon(
|
||
CupertinoIcons.xmark_circle_fill,
|
||
size: 16,
|
||
color: tintColor.withValues(alpha: 0.6),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
).animate().fadeIn(duration: 300.ms).slideY(begin: -0.5, end: 0);
|
||
}
|
||
}
|
||
|
||
/// 简洁离线横幅 — 显示离线状态,支持刷新和关闭
|
||
class OfflineBanner extends ConsumerStatefulWidget {
|
||
const OfflineBanner({super.key});
|
||
|
||
@override
|
||
ConsumerState<OfflineBanner> createState() => _OfflineBannerState();
|
||
}
|
||
|
||
class _OfflineBannerState extends ConsumerState<OfflineBanner> {
|
||
bool _dismissed = false;
|
||
bool _wasOffline = false;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final isOffline = ref.isOffline;
|
||
|
||
if (_wasOffline && !isOffline) {
|
||
_dismissed = false;
|
||
}
|
||
_wasOffline = isOffline;
|
||
|
||
if (!isOffline || _dismissed) return const SizedBox.shrink();
|
||
|
||
return Container(
|
||
width: double.infinity,
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: AppSpacing.md,
|
||
vertical: AppSpacing.sm,
|
||
),
|
||
decoration: BoxDecoration(
|
||
color: CupertinoColors.systemOrange.withValues(alpha: 0.12),
|
||
border: Border(
|
||
bottom: BorderSide(
|
||
color: CupertinoColors.systemOrange.withValues(alpha: 0.2),
|
||
width: 0.5,
|
||
),
|
||
),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Icon(
|
||
CupertinoIcons.wifi_slash,
|
||
size: 14,
|
||
color: CupertinoColors.systemOrange.darkColor,
|
||
),
|
||
const SizedBox(width: AppSpacing.sm),
|
||
Expanded(
|
||
child: Text(
|
||
'离线模式 · 使用缓存数据',
|
||
style: AppTypography.caption1.copyWith(
|
||
color: CupertinoColors.systemOrange.darkColor,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
),
|
||
GestureDetector(
|
||
onTap: () {
|
||
final stillOffline = ref.read(connectivityProvider) == false;
|
||
if (stillOffline) {
|
||
AppToast.showWarning('网络仍未连接');
|
||
}
|
||
},
|
||
child: Padding(
|
||
padding: const EdgeInsets.only(left: AppSpacing.sm),
|
||
child: Icon(
|
||
CupertinoIcons.refresh,
|
||
size: 16,
|
||
color: CupertinoColors.systemOrange.darkColor
|
||
.withValues(alpha: 0.7),
|
||
),
|
||
),
|
||
),
|
||
GestureDetector(
|
||
onTap: () {
|
||
if (mounted) {
|
||
setState(() {
|
||
_dismissed = true;
|
||
});
|
||
}
|
||
},
|
||
child: Padding(
|
||
padding: const EdgeInsets.only(left: AppSpacing.sm),
|
||
child: Icon(
|
||
CupertinoIcons.xmark_circle_fill,
|
||
size: 16,
|
||
color: CupertinoColors.systemOrange.darkColor
|
||
.withValues(alpha: 0.6),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
).animate().fadeIn(duration: 300.ms).slideY(begin: -0.5, end: 0);
|
||
}
|
||
}
|
||
|
||
/// 离线指示点 — 小圆点+文字
|
||
class OfflineIndicator extends ConsumerWidget {
|
||
const OfflineIndicator({super.key, this.showText = true});
|
||
|
||
final bool showText;
|
||
|
||
@override
|
||
Widget build(BuildContext context, WidgetRef ref) {
|
||
final isOffline = ref.isOffline;
|
||
if (!isOffline) return const SizedBox.shrink();
|
||
|
||
return Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Container(
|
||
width: 6,
|
||
height: 6,
|
||
decoration: const BoxDecoration(
|
||
color: CupertinoColors.systemOrange,
|
||
shape: BoxShape.circle,
|
||
),
|
||
),
|
||
if (showText) ...[
|
||
const SizedBox(width: 4),
|
||
Text(
|
||
'离线',
|
||
style: AppTypography.caption2.copyWith(
|
||
color: CupertinoColors.systemOrange,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
],
|
||
],
|
||
);
|
||
}
|
||
}
|