6238b86dcb
- 登录/注册(login.html/register.html 1:1):两栏卡片+品牌渐变面板+主题小衣服 pill(onSurface 变体)+记住我(记录并预填最近账号)+原型式 toast 校验; 登录 fidelity 1.4–2.1% 三主题全绿;注册暂不入闸(少 门店编号/兑换券 字段, 已记 CONTRACT,screens.mjs 留存根) - ds 原子补齐:DsToast(.toast 单例)/DsCheck(.check/.agree)/DsButton lg 档/ DsSelect 替换全部旧 DropdownButton/DsField label 在上/DsInput 后缀与密码形态 - 全屏统一:对话框按钮全 DsButton、盒式输入主题钉死(visualDensity.standard、 h38、InputDecorationTheme 渗漏修复)、图标全 lucide、JetBrains Mono 三端同源、 BrandMark 真相源 logo、只读模式写操作全量守卫(WriteGuard+DsToast) - 出入库列表:版式对齐原型(卡片对齐+搜索框居中)、KPI 近30天滚动、结清后 失效财务应收应付表;商品编辑抽屉介绍库改搜索下拉、图片双击全屏预览 - 删除旧 UI 死代码:DataTableCard/FormDialog/PageScaffold/SearchChip/ SelectProductDialog/tabStateProvider - golden/fidelity:stock-in/out 补注册(9%)、login 入册(8%)、goldens 全量重打; 修复 pubspec flutter_web_plugins 非法声明(CI pub get 阻断) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
130 lines
4.0 KiB
Dart
130 lines
4.0 KiB
Dart
import 'dart:async';
|
||
import 'package:dio/dio.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
import '../core/config/app_config.dart';
|
||
import '../core/config/app_constants.dart';
|
||
|
||
/// 每次从离线恢复到在线时自增。
|
||
/// 数据 provider 通过 watch 此值实现网络恢复后自动刷新。
|
||
final networkRecoveryCountProvider = StateProvider<int>((ref) => 0);
|
||
|
||
/// 是否正在进行一次「带重试的连通性检测」(供 UI 显示「重试中…」转圈)。
|
||
final connectivityCheckingProvider = StateProvider<bool>((ref) => false);
|
||
|
||
final connectivityProvider =
|
||
StateNotifierProvider<ConnectivityNotifier, bool>((ref) {
|
||
return ConnectivityNotifier(
|
||
onRecovered: () {
|
||
ref.read(networkRecoveryCountProvider.notifier).update((s) => s + 1);
|
||
},
|
||
onCheckingChanged: (checking) {
|
||
ref.read(connectivityCheckingProvider.notifier).state = checking;
|
||
},
|
||
);
|
||
});
|
||
|
||
class ConnectivityNotifier extends StateNotifier<bool> {
|
||
final void Function()? onRecovered;
|
||
final void Function(bool checking)? onCheckingChanged;
|
||
|
||
ConnectivityNotifier({
|
||
this.onRecovered,
|
||
this.onCheckingChanged,
|
||
bool skipInit = false,
|
||
}) : super(true) {
|
||
if (!skipInit) {
|
||
// 启动首检走「带重试」:跨境冷启动 DNS+TLS 握手较慢,单次 4s 易误判离线,
|
||
// 退避重试 4s→8s→12s 可显著降低首屏假离线。
|
||
_check(withRetry: true);
|
||
_startOnlineTimer();
|
||
}
|
||
}
|
||
|
||
Timer? _timer;
|
||
|
||
// 独立轻量 Dio:无拦截器。连接预算用 Future.timeout 逐次控制,
|
||
// 故 BaseOptions 超时放宽到 15s 作为兜底。
|
||
final _dio = Dio(BaseOptions(
|
||
connectTimeout: AppConstants.connectivityTimeout,
|
||
receiveTimeout: AppConstants.connectivityTimeout,
|
||
));
|
||
|
||
/// 每次尝试的超时预算,逐次拉长(越来越长)。
|
||
static const _budgets = AppConstants.connectivityBudgets;
|
||
|
||
/// 在线时:轻量检测一次(单次,不重试)
|
||
void _startOnlineTimer() {
|
||
_timer?.cancel();
|
||
_timer =
|
||
Timer.periodic(AppConstants.connectivityOnlinePoll, (_) => _check());
|
||
}
|
||
|
||
/// 离线时:降低频率嗅探,节省资源
|
||
void _startOfflineTimer() {
|
||
_timer?.cancel();
|
||
_timer =
|
||
Timer.periodic(AppConstants.connectivityOfflinePoll, (_) => _check());
|
||
}
|
||
|
||
/// 立即触发一次检测(供外部调用,如 API 请求失败 / 启动时)。单次,不重试。
|
||
Future<void> forceCheck() => _check();
|
||
|
||
/// 用户手动重试:带退避重试,并对外广播「检测中」状态,返回最终是否在线。
|
||
Future<bool> retry() async {
|
||
onCheckingChanged?.call(true);
|
||
try {
|
||
return await _check(withRetry: true);
|
||
} finally {
|
||
onCheckingChanged?.call(false);
|
||
}
|
||
}
|
||
|
||
/// 单次 ping /health,[budget] 为本次超时预算。
|
||
Future<bool> _ping(Duration budget) async {
|
||
try {
|
||
await _dio.get(AppConfig.healthUrl).timeout(budget);
|
||
return true;
|
||
} catch (_) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// 连通性检测。[withRetry]=true 时按 [_budgets] 退避重试,间隔递增。
|
||
Future<bool> _check({bool withRetry = false}) async {
|
||
final budgets =
|
||
withRetry ? _budgets : AppConstants.connectivitySingleBudget;
|
||
var ok = false;
|
||
for (var i = 0; i < budgets.length; i++) {
|
||
ok = await _ping(budgets[i]);
|
||
if (ok) break;
|
||
// 最后一次失败后不再等待;中间失败按递增间隔退避(0.6s, 1.2s…)
|
||
if (i < budgets.length - 1) {
|
||
await Future.delayed(AppConstants.connectivityRetryStep * (i + 1));
|
||
}
|
||
}
|
||
|
||
if (ok) {
|
||
if (!state) {
|
||
// 离线 → 在线:切回高频检测,广播恢复事件
|
||
state = true;
|
||
onRecovered?.call();
|
||
_startOnlineTimer();
|
||
}
|
||
} else {
|
||
if (state) {
|
||
// 在线 → 离线:切换为低频嗅探
|
||
state = false;
|
||
_startOfflineTimer();
|
||
}
|
||
}
|
||
return ok;
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_timer?.cancel();
|
||
_dio.close(force: true);
|
||
super.dispose();
|
||
}
|
||
}
|